diff --git a/admin-frontend/src/views/Home.vue b/admin-frontend/src/views/Home.vue
index ca2ac9d..3495a78 100644
--- a/admin-frontend/src/views/Home.vue
+++ b/admin-frontend/src/views/Home.vue
@@ -1,19 +1,16 @@
diff --git a/backend/app/api/stories.py b/backend/app/api/stories.py
index 24419b8..13acfa5 100644
--- a/backend/app/api/stories.py
+++ b/backend/app/api/stories.py
@@ -5,33 +5,34 @@ import uuid
from typing import AsyncGenerator
from fastapi import APIRouter, Depends, Response
-from sse_starlette.sse import EventSourceResponse
-from sqlalchemy.ext.asyncio import AsyncSession
-
-from app.core.deps import require_user
-from app.core.logging import get_logger
-from app.core.rate_limiter import check_rate_limit
-from app.db.database import get_db
-from app.db.models import User
+from sqlalchemy.ext.asyncio import AsyncSession
+from sse_starlette.sse import EventSourceResponse
+
+from app.core.deps import require_user
+from app.core.logging import get_logger
+from app.core.rate_limiter import check_rate_limit
+from app.db.database import get_db
+from app.db.models import User
from app.schemas.story_schemas import (
AchievementItem,
FullStoryResponse,
GenerateRequest,
+ StoryAssetRetryRequest,
+ StorybookRequest,
+ StorybookResponse,
StoryDetailResponse,
StoryImageResponse,
StoryListItem,
StoryResponse,
- StorybookRequest,
- StorybookResponse,
)
from app.services import story_service
from app.services.memory_service import build_enhanced_memory_context
from app.services.provider_router import (
- generate_story_content,
generate_image,
+ generate_story_content,
)
from app.services.story_status import StoryAssetStatus, sync_story_status
-
+
logger = get_logger(__name__)
router = APIRouter()
@@ -69,12 +70,12 @@ async def generate_story_stream(
):
"""流式生成故事(SSE)。"""
await check_rate_limit(f"story:{user.id}", RATE_LIMIT_REQUESTS, RATE_LIMIT_WINDOW)
-
+
# Validation
profile_id, universe_id = await story_service.validate_profile_and_universe(
request.child_profile_id, request.universe_id, user.id, db
)
-
+
# Build Context
memory_context = await build_enhanced_memory_context(profile_id, universe_id, db)
@@ -241,10 +242,21 @@ async def generate_story_image(
"audio_status": story.audio_status,
"last_error": story.last_error,
}
-
-
-@router.get("/audio/{story_id}")
-async def get_story_audio(
+
+
+@router.post("/stories/{story_id}/assets/retry", response_model=StoryDetailResponse)
+async def retry_story_assets(
+ story_id: int,
+ payload: StoryAssetRetryRequest,
+ user: User = Depends(require_user),
+ db: AsyncSession = Depends(get_db),
+):
+ """Retry selected generated assets for a story."""
+ return await story_service.retry_story_assets(story_id, user.id, payload.assets, db)
+
+
+@router.get("/audio/{story_id}")
+async def get_story_audio(
story_id: int,
user: User = Depends(require_user),
db: AsyncSession = Depends(get_db),
diff --git a/backend/app/schemas/story_schemas.py b/backend/app/schemas/story_schemas.py
index aff4e04..95ba6ef 100644
--- a/backend/app/schemas/story_schemas.py
+++ b/backend/app/schemas/story_schemas.py
@@ -5,7 +5,6 @@ from typing import Literal
from pydantic import BaseModel, Field
-
MAX_DATA_LENGTH = 2000
MAX_EDU_THEME_LENGTH = 200
MAX_TTS_LENGTH = 4000
@@ -120,6 +119,12 @@ class StoryImageResponse(StoryStatusMixin):
image_url: str | None
+class StoryAssetRetryRequest(BaseModel):
+ """Retry selected generated assets for a story."""
+
+ assets: list[Literal["image", "audio"]] = Field(..., min_length=1)
+
+
class AchievementItem(BaseModel):
"""Achievement item returned for a story."""
diff --git a/backend/app/services/story_service.py b/backend/app/services/story_service.py
index ebac765..f283e61 100644
--- a/backend/app/services/story_service.py
+++ b/backend/app/services/story_service.py
@@ -10,12 +10,12 @@ from sqlalchemy.orm import joinedload
from app.core.logging import get_logger
from app.db.models import ChildProfile, Story, StoryUniverse
from app.schemas.story_schemas import (
- GenerateRequest,
- StorybookRequest,
- FullStoryResponse,
- StorybookResponse,
- StorybookPageResponse,
AchievementItem,
+ FullStoryResponse,
+ GenerateRequest,
+ StorybookPageResponse,
+ StorybookRequest,
+ StorybookResponse,
)
from app.services.audio_storage import (
audio_cache_exists,
@@ -24,8 +24,8 @@ from app.services.audio_storage import (
)
from app.services.memory_service import build_enhanced_memory_context
from app.services.provider_router import (
- generate_story_content,
generate_image,
+ generate_story_content,
generate_storybook,
)
from app.services.story_status import (
@@ -140,7 +140,7 @@ async def generate_and_save_story(
profile_id, universe_id = await validate_profile_and_universe(
request.child_profile_id, request.universe_id, user_id, db
)
-
+
# 2. Build Context
memory_context = await build_enhanced_memory_context(profile_id, universe_id, db)
@@ -153,8 +153,11 @@ async def generate_and_save_story(
memory_context=memory_context,
db=db,
)
- except Exception as exc:
- raise HTTPException(status_code=502, detail="Story generation failed, please try again.") from exc
+ except Exception as exc:
+ raise HTTPException(
+ status_code=502,
+ detail="Story generation failed, please try again.",
+ ) from exc
# 4. Save
story = Story(
@@ -247,7 +250,7 @@ async def generate_storybook_service(
profile_id, universe_id = await validate_profile_and_universe(
request.child_profile_id, request.universe_id, user_id, db
)
-
+
logger.info(
"storybook_request",
user_id=user_id,
@@ -418,11 +421,11 @@ async def get_story_detail(
return story
-async def delete_story(
- story_id: int,
- user_id: str,
- db: AsyncSession,
-) -> None:
+async def delete_story(
+ story_id: int,
+ user_id: str,
+ db: AsyncSession,
+) -> None:
"""Delete a story."""
story = await get_story_detail(story_id, user_id, db)
await db.delete(story)
@@ -456,12 +459,131 @@ async def create_story_from_result(
await db.commit()
await db.refresh(story)
- if universe_id:
- extract_story_achievements.delay(story.id, universe_id)
-
- return story
-
-
+ if universe_id:
+ extract_story_achievements.delay(story.id, universe_id)
+
+ return story
+
+
+async def _retry_cover_image_asset(story: Story, db: AsyncSession) -> None:
+ """Retry cover generation for a text story."""
+
+ if not story.cover_prompt:
+ raise HTTPException(status_code=400, detail="Story has no cover prompt")
+
+ sync_story_status(story, image_status=StoryAssetStatus.GENERATING)
+ await db.commit()
+
+ try:
+ story.image_url = await generate_image(story.cover_prompt, db=db)
+ sync_story_status(story, image_status=StoryAssetStatus.READY)
+ except Exception as exc:
+ sync_story_status(
+ story,
+ image_status=StoryAssetStatus.FAILED,
+ last_error=f"封面生成失败: {exc}",
+ )
+ logger.error("cover_asset_retry_failed", story_id=story.id, error=str(exc))
+
+ await db.commit()
+
+
+async def _retry_storybook_image_assets(story: Story, db: AsyncSession) -> None:
+ """Retry missing storybook cover/page images."""
+
+ pages_data = [dict(page) for page in story.pages or [] if isinstance(page, dict)]
+ has_image_prompt = bool(story.cover_prompt) or any(
+ page.get("image_prompt") for page in pages_data
+ )
+ if not has_image_prompt:
+ raise HTTPException(status_code=400, detail="Storybook has no image prompts")
+
+ sync_story_status(story, image_status=StoryAssetStatus.GENERATING)
+ await db.commit()
+
+ cover_failed = False
+ failed_pages: list[int] = []
+
+ if story.cover_prompt and not story.image_url:
+ try:
+ story.image_url = await generate_image(story.cover_prompt, db=db)
+ except Exception as exc:
+ cover_failed = True
+ logger.warning(
+ "storybook_cover_asset_retry_failed",
+ story_id=story.id,
+ error=str(exc),
+ )
+
+ for page in pages_data:
+ if not page.get("image_prompt") or page.get("image_url"):
+ continue
+
+ try:
+ page["image_url"] = await generate_image(page["image_prompt"], db=db)
+ except Exception as exc:
+ page_number = page.get("page_number")
+ if isinstance(page_number, int):
+ failed_pages.append(page_number)
+ logger.warning(
+ "storybook_page_asset_retry_failed",
+ story_id=story.id,
+ page=page_number,
+ error=str(exc),
+ )
+
+ story.pages = pages_data
+ sync_story_status(
+ story,
+ image_status=_resolve_storybook_image_status(
+ generate_images=True,
+ cover_prompt=story.cover_prompt,
+ cover_url=story.image_url,
+ pages_data=pages_data,
+ ),
+ last_error=_build_storybook_error_message(
+ cover_failed=cover_failed,
+ failed_pages=failed_pages,
+ ),
+ )
+ await db.commit()
+
+
+async def _retry_audio_asset(story_id: int, user_id: str, db: AsyncSession) -> None:
+ """Retry audio generation while preserving persisted status on provider failure."""
+
+ try:
+ await generate_story_audio(story_id, user_id, db)
+ except HTTPException as exc:
+ if exc.status_code >= 500:
+ logger.warning("audio_asset_retry_failed", story_id=story_id, error=exc.detail)
+ return
+ raise
+
+
+async def retry_story_assets(
+ story_id: int,
+ user_id: str,
+ assets: list[str],
+ db: AsyncSession,
+) -> Story:
+ """Retry selected assets through one workflow-level endpoint."""
+
+ story = await get_story_detail(story_id, user_id, db)
+ requested_assets = list(dict.fromkeys(assets))
+
+ if "image" in requested_assets:
+ if story.mode == "storybook":
+ await _retry_storybook_image_assets(story, db)
+ else:
+ await _retry_cover_image_asset(story, db)
+
+ if "audio" in requested_assets:
+ await _retry_audio_asset(story_id, user_id, db)
+
+ return await get_story_detail(story_id, user_id, db)
+
+
async def generate_story_cover(
story_id: int,
user_id: str,
@@ -469,7 +591,7 @@ async def generate_story_cover(
) -> str:
"""Generate cover image for an existing story."""
story = await get_story_detail(story_id, user_id, db)
-
+
if not story.cover_prompt:
raise HTTPException(status_code=400, detail="Story has no cover prompt")
@@ -503,7 +625,7 @@ async def generate_story_audio(
) -> bytes:
"""Generate audio for a story."""
story = await get_story_detail(story_id, user_id, db)
-
+
if not story.story_text:
raise HTTPException(status_code=400, detail="Story has no text")
diff --git a/backend/tests/test_stories.py b/backend/tests/test_stories.py
index 21d6b21..b05eee2 100644
--- a/backend/tests/test_stories.py
+++ b/backend/tests/test_stories.py
@@ -264,7 +264,10 @@ class TestAudio:
mock_tts_provider.assert_awaited_once()
def test_get_audio_failure_updates_status(self, auth_client: TestClient, test_story):
- with patch("app.services.provider_router.text_to_speech", new_callable=AsyncMock) as mock_tts:
+ with patch(
+ "app.services.provider_router.text_to_speech",
+ new_callable=AsyncMock,
+ ) as mock_tts:
mock_tts.side_effect = Exception("TTS provider timeout")
response = auth_client.get(f"/api/audio/{test_story.id}")
assert response.status_code == 500
@@ -383,12 +386,83 @@ class TestImageGenerateSuccess:
assert data["last_error"] is None
+class TestAssetRetry:
+ """Tests for unified asset retry endpoint."""
+
+ def test_retry_cover_image_success(
+ self,
+ auth_client: TestClient,
+ degraded_story_with_text,
+ mock_image_provider,
+ ):
+ response = auth_client.post(
+ f"/api/stories/{degraded_story_with_text.id}/assets/retry",
+ json={"assets": ["image"]},
+ )
+
+ assert response.status_code == 200
+ data = response.json()
+ assert data["image_url"] == "https://example.com/image.png"
+ assert data["generation_status"] == "completed"
+ assert data["image_status"] == "ready"
+ assert data["audio_status"] == "not_requested"
+ assert data["last_error"] is None
+ mock_image_provider.assert_awaited_once()
+
+ def test_retry_storybook_missing_page_image_success(
+ self,
+ auth_client: TestClient,
+ storybook_story,
+ ):
+ async def image_side_effect(prompt: str, **kwargs):
+ return "https://example.com/retried-page.png"
+
+ with patch(
+ "app.services.story_service.generate_image",
+ new_callable=AsyncMock,
+ ) as mock_image:
+ mock_image.side_effect = image_side_effect
+
+ response = auth_client.post(
+ f"/api/stories/{storybook_story.id}/assets/retry",
+ json={"assets": ["image"]},
+ )
+
+ assert response.status_code == 200
+ data = response.json()
+ assert data["generation_status"] == "completed"
+ assert data["image_status"] == "ready"
+ assert data["audio_status"] == "not_requested"
+ assert data["last_error"] is None
+ assert data["pages"][1]["image_url"] == "https://example.com/retried-page.png"
+ mock_image.assert_awaited_once()
+
+ def test_retry_audio_on_storybook_is_rejected(
+ self,
+ auth_client: TestClient,
+ storybook_story,
+ ):
+ response = auth_client.post(
+ f"/api/stories/{storybook_story.id}/assets/retry",
+ json={"assets": ["audio"]},
+ )
+
+ assert response.status_code == 400
+ assert response.json()["detail"] == "Story has no text"
+
+
class TestStorybookGenerate:
"""Tests for storybook generation status handling."""
def test_generate_storybook_success(self, auth_client: TestClient):
- with patch("app.services.story_service.generate_storybook", new_callable=AsyncMock) as mock_storybook:
- with patch("app.services.story_service.generate_image", new_callable=AsyncMock) as mock_image:
+ with patch(
+ "app.services.story_service.generate_storybook",
+ new_callable=AsyncMock,
+ ) as mock_storybook:
+ with patch(
+ "app.services.story_service.generate_image",
+ new_callable=AsyncMock,
+ ) as mock_image:
mock_storybook.return_value = build_storybook_output()
mock_image.side_effect = [
"https://example.com/storybook-cover.png",
@@ -422,8 +496,14 @@ class TestStorybookGenerate:
slug = prompt.split()[0].lower()
return f"https://example.com/{slug}.png"
- with patch("app.services.story_service.generate_storybook", new_callable=AsyncMock) as mock_storybook:
- with patch("app.services.story_service.generate_image", new_callable=AsyncMock) as mock_image:
+ with patch(
+ "app.services.story_service.generate_storybook",
+ new_callable=AsyncMock,
+ ) as mock_storybook:
+ with patch(
+ "app.services.story_service.generate_image",
+ new_callable=AsyncMock,
+ ) as mock_image:
mock_storybook.return_value = build_storybook_output()
mock_image.side_effect = image_side_effect
diff --git a/docs/planning/week-1-execution-backlog.md b/docs/planning/week-1-execution-backlog.md
index a915696..8e63521 100644
--- a/docs/planning/week-1-execution-backlog.md
+++ b/docs/planning/week-1-execution-backlog.md
@@ -37,7 +37,7 @@
## 2.1 Current Progress Snapshot
-**Updated**: 2026-04-17 evening
+**Updated**: 2026-04-18 morning
### What Has Been Completed
@@ -54,35 +54,43 @@
- `0009_add_story_generation_statuses.py`
- `0010_add_story_audio_cache_path.py`
- 已完成一轮后端回归验证:`backend/` 下 `pytest -q` 结果为 `53 passed`
+- 已修复 admin-frontend 构建阻塞,主前端与管理端前端均可生产构建
+- 已落地首版统一资产重试入口:`POST /api/stories/{story_id}/assets/retry`
### What Is In Progress
- 统一状态模型已落地,但统一 service workflow 仍未真正收束成单一路径
- 普通故事、完整生成、绘本生成仍存在多条 service / API 路径
-- 资产补全虽然已经支持图片与音频状态,但“统一重试入口”尚未实现
+- 资产补全已经具备统一重试入口首版,但仍需要继续抽象统一补全过程和 generation job 边界
### What Is Still Pending
-- admin-frontend 的处理决策与演示范围收敛
- Provider 的 Capability / Provider / Routing Policy 边界整理
- Week 2 可直接执行的开发任务表
- 演示 checklist 与最终收尾策略
### Remote Checkpoint Scope
-当前远端已同步一个阶段性 checkpoint:
+当前远端已同步到 2026-04-17 晚间 WIP 快照:
-- Commit: `a97a2fe`
-- Message: `feat: persist story generation states and cache audio`
+- Commit: `b8d3cb4`
+- Message: `wip: snapshot full local workspace state`
-这个 checkpoint **不是今天下午所有本地修改的全集**。它只覆盖以下主线:
+这个 checkpoint 包含 `a97a2fe` 的统一状态模型主线,也包含后续文档治理和接手机制整理:
- 统一生成状态模型
- Storybook 按 ID 恢复
- 故事列表/详情/绘本页状态展示
- 音频缓存与状态语义修正
+- `docs/` 信息架构整理
+- 求职版 PRD、统一生成工作流 PRD、Week 1 backlog、文档状态盘点
+- `AGENTS.md` 接手说明
-当前工作区里仍存在其他未提交、本机独有的改动,周末换电脑后不会自动带过去。
+2026-04-18 在个人电脑接手时已确认:
+
+- 本地 `main` 与 `origin/main` 对齐
+- 工作树起始状态干净
+- Gitea HTTPS 推送凭据已配置并通过 dry-run 验证
---
@@ -171,7 +179,7 @@
| W1-05 | Product / Backend | 定义统一工作流下的 API / 数据结构影响 | 接口与模型变更清单 | P0 | 0.5d | In Progress |
| W1-06 | Product / Backend | 梳理 Provider 概念层:Capability / Provider / Routing Policy | 分层图与术语表 | P1 | 0.5d | Pending |
| W1-07 | Product / Frontend | 梳理 Storybook 当前问题与恢复方案 | 恢复方案说明 | P0 | 0.5d | Done |
-| W1-08 | Product / Frontend | 确认 admin 前端是修复、裁剪还是暂时降级 | 决策记录 | P0 | 0.5d | Pending |
+| W1-08 | Product / Frontend | 确认 admin 前端是修复、裁剪还是暂时降级 | 决策记录 | P0 | 0.5d | Done |
| W1-09 | Planning | 产出 Week 2 开发任务清单 | 下周 backlog | P1 | 0.5d | In Progress |
| W1-10 | Review | 形成求职演示版检查清单 | 演示清单 | P1 | 0.5d | Pending |
diff --git a/docs/planning/weekend-handoff-2026-04-17.md b/docs/planning/weekend-handoff-2026-04-17.md
index d4c33b4..5d73bed 100644
--- a/docs/planning/weekend-handoff-2026-04-17.md
+++ b/docs/planning/weekend-handoff-2026-04-17.md
@@ -8,7 +8,7 @@
## What Is Already On Remote
-当前远端已经包含一个阶段性 checkpoint:
+当前远端 `main` 已经包含两个连续 checkpoint:
- Commit: `a97a2fe`
- Message: `feat: persist story generation states and cache audio`
@@ -25,16 +25,30 @@
- 音频首次生成后缓存落盘并可复用
- 统一状态语义中 `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` 重新整理。
+
---
-## What Is Not Yet On Remote
+## Current Local Status On 2026-04-18
-当前这台机器的工作区里仍存在大量未提交改动,它们 **不属于上面的 checkpoint**,也不会自动出现在另一台电脑上。
+2026-04-18 在个人电脑接手后已确认:
-因此,周末接手时应该默认:
-
-- 远端 `main` 只包含“统一状态模型 + Storybook 恢复 + 音频缓存”这一条主线
-- 其他本机未提交内容需要后续再整理,不应假设它们已经同步
+- 本地 `main` 与 `origin/main` 对齐到 `b8d3cb4`
+- 工作树起始状态干净
+- 已配置 Gitea HTTPS 推送凭据,并通过 `git push --dry-run origin HEAD:main`
+- 后端本地虚拟环境可用:`backend/.venv`
+- 主前端与管理端依赖已安装到各自 `node_modules`
---
@@ -55,7 +69,7 @@
1. `git pull`
2. `cd backend && alembic upgrade head`
-3. `cd backend && ./.venv/Scripts/python.exe -m pytest -q`
+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`
@@ -120,6 +134,6 @@
如果周末是在另一台电脑上继续,不要默认“今天下午所有本地修改”都已经上远端。当前最可靠的 source of truth 是:
-- 远端代码:以 commit `a97a2fe` 为准
+- 远端代码:以 commit `b8d3cb4` 为准
- 产品目标:以 `docs/product/job-search-relaunch-prd.md` 为准
- 当前执行主线:以 `docs/product/unified-generation-workflow-prd.md` 与 `docs/planning/week-1-execution-backlog.md` 为准
diff --git a/docs/product/unified-generation-workflow-prd.md b/docs/product/unified-generation-workflow-prd.md
index 58c5b35..4149b80 100644
--- a/docs/product/unified-generation-workflow-prd.md
+++ b/docs/product/unified-generation-workflow-prd.md
@@ -17,7 +17,7 @@ DreamWeaver 当前同时支持普通故事生成、完整故事生成和绘本
## Implementation Snapshot
-**Updated**: 2026-04-17 evening
+**Updated**: 2026-04-18 morning
当前代码已经从“纯目标态设计”进入“部分落地”阶段,主要进展如下:
@@ -33,12 +33,15 @@ DreamWeaver 当前同时支持普通故事生成、完整故事生成和绘本
- 故事列表页、故事详情页、绘本阅读页已接入统一状态展示
- 故事音频已支持首次生成后缓存复用
- `degraded_completed` 已在服务层和前端语义中落地
+- 已新增首版统一资产重试入口:`POST /api/stories/{story_id}/assets/retry`
+- 故事详情页封面补全已切换到统一资产重试入口
+- 管理端前端构建阻塞已修复,主前端与 admin 前端均可完成生产构建
### Still Missing
- 统一的 `POST /api/generations` 风格入口尚未建立
- 普通故事、完整生成、绘本生成仍通过多条 service 路径实现
-- “统一资产重试入口”尚未落地
+- 统一资产重试入口仍是首版:已覆盖普通故事封面、绘本缺失插图、故事音频,但尚未抽象成完整 generation job 模型
- `partial_ready`、`retryable_assets` 等更细粒度状态仍停留在目标态
### What This Means
diff --git a/frontend/src/views/StoryDetail.vue b/frontend/src/views/StoryDetail.vue
index 6c59ec9..d43770b 100644
--- a/frontend/src/views/StoryDetail.vue
+++ b/frontend/src/views/StoryDetail.vue
@@ -34,14 +34,6 @@ interface Story {
}> | null
}
-interface ImageGenerationResponse {
- image_url: string | null
- generation_status: string
- image_status: string
- audio_status: string
- last_error: string | null
-}
-
const route = useRoute()
const router = useRouter()
@@ -93,12 +85,9 @@ async function generateImage() {
error.value = ''
try {
- const result = await api.post(`/api/image/generate/${story.value.id}`)
- story.value.image_url = result.image_url
- story.value.generation_status = result.generation_status
- story.value.image_status = result.image_status
- story.value.audio_status = result.audio_status
- story.value.last_error = result.last_error
+ story.value = await api.post(`/api/stories/${story.value.id}/assets/retry`, {
+ assets: ['image'],
+ })
} catch (e) {
error.value = e instanceof Error ? e.message : '图片生成失败'
await refreshStorySnapshot().catch(() => undefined)