From 9e1a17fa674f9161c1ae5f006cfb3d37a45f99e9 Mon Sep 17 00:00:00 2001 From: Yuyan Date: Sat, 18 Apr 2026 12:35:37 +0800 Subject: [PATCH] chore: clear lint and sync admin story views --- .../src/components/CreateStoryModal.vue | 17 +- admin-frontend/src/router.ts | 2 +- admin-frontend/src/stores/storybook.ts | 22 +- admin-frontend/src/utils/storyStatus.ts | 79 +++ admin-frontend/src/views/MyStories.vue | 7 +- admin-frontend/src/views/StoryDetail.vue | 191 +++++-- admin-frontend/src/views/StorybookViewer.vue | 537 +++++++++++------- backend/app/api/admin_providers.py | 7 +- backend/app/api/profiles.py | 19 +- backend/app/api/reading_events.py | 6 +- backend/app/db/admin_models.py | 5 +- .../app/services/adapters/image/__init__.py | 7 +- .../services/adapters/image/antigravity.py | 22 +- backend/app/services/memory_service.py | 12 +- backend/app/services/provider_cache.py | 89 +-- backend/tests/test_auth.py | 3 +- 16 files changed, 670 insertions(+), 355 deletions(-) create mode 100644 admin-frontend/src/utils/storyStatus.ts diff --git a/admin-frontend/src/components/CreateStoryModal.vue b/admin-frontend/src/components/CreateStoryModal.vue index 7a4c75a..e83cc19 100644 --- a/admin-frontend/src/components/CreateStoryModal.vue +++ b/admin-frontend/src/components/CreateStoryModal.vue @@ -154,14 +154,15 @@ async function generateStory() { universe_id: selectedUniverseId.value || undefined }) - storybookStore.setStorybook(response) - close() - router.push('/storybook/view') - } else { - const result = await api.post('/api/generate/full', payload) - const query: Record = {} - if (result.errors && Object.keys(result.errors).length > 0) { - if (result.errors.image) query.imageError = '1' + storybookStore.setStorybook(response) + close() + const storybookPath = response.id ? `/storybook/view/${response.id}` : '/storybook/view' + router.push(storybookPath) + } else { + const result = await api.post('/api/stories/generate/full', payload) + const query: Record = {} + if (result.errors && Object.keys(result.errors).length > 0) { + if (result.errors.image) query.imageError = '1' } close() router.push({ path: `/story/${result.id}`, query }) diff --git a/admin-frontend/src/router.ts b/admin-frontend/src/router.ts index fb0767e..2b993f9 100644 --- a/admin-frontend/src/router.ts +++ b/admin-frontend/src/router.ts @@ -43,7 +43,7 @@ const router = createRouter({ component: () => import('./views/StoryDetail.vue'), }, { - path: '/storybook/view', + path: '/storybook/view/:id?', name: 'storybook-viewer', component: () => import('./views/StorybookViewer.vue'), }, diff --git a/admin-frontend/src/stores/storybook.ts b/admin-frontend/src/stores/storybook.ts index da45149..5d00f3f 100644 --- a/admin-frontend/src/stores/storybook.ts +++ b/admin-frontend/src/stores/storybook.ts @@ -9,15 +9,19 @@ export interface StorybookPage { image_url?: string } -export interface Storybook { - id?: number // 新增 - title: string - main_character: string - art_style: string - pages: StorybookPage[] - cover_prompt: string - cover_url?: string -} +export interface Storybook { + id?: number // 新增 + title: string + main_character: string + art_style: string + pages: StorybookPage[] + cover_prompt: string + cover_url?: string + generation_status?: string + image_status?: string + audio_status?: string + last_error?: string | null +} export const useStorybookStore = defineStore('storybook', () => { const currentStorybook = ref(null) diff --git a/admin-frontend/src/utils/storyStatus.ts b/admin-frontend/src/utils/storyStatus.ts new file mode 100644 index 0000000..363a279 --- /dev/null +++ b/admin-frontend/src/utils/storyStatus.ts @@ -0,0 +1,79 @@ +export type StoryGenerationStatus = + | 'narrative_ready' + | 'assets_generating' + | 'completed' + | 'degraded_completed' + | 'failed' + +export type StoryAssetStatus = + | 'not_requested' + | 'generating' + | 'ready' + | 'failed' + +interface StatusMeta { + label: string + description: string + badgeClass: string +} + +const generationStatusMetaMap: Record = { + narrative_ready: { + label: '文本已完成', + description: '故事内容已经生成,可以继续补充封面或音频。', + badgeClass: 'bg-sky-50 text-sky-700 border border-sky-100', + }, + assets_generating: { + label: '资源生成中', + description: '封面或音频正在生成中,请稍候查看结果。', + badgeClass: 'bg-amber-50 text-amber-700 border border-amber-100', + }, + completed: { + label: '内容可用', + description: '当前内容已经达到可阅读状态。', + badgeClass: 'bg-emerald-50 text-emerald-700 border border-emerald-100', + }, + degraded_completed: { + label: '部分降级完成', + description: '核心内容可用,但有部分资源生成失败。', + badgeClass: 'bg-orange-50 text-orange-700 border border-orange-100', + }, + failed: { + label: '生成失败', + description: '当前内容还未成功生成,请稍后重试。', + badgeClass: 'bg-rose-50 text-rose-700 border border-rose-100', + }, +} + +const assetStatusMetaMap: Record = { + not_requested: { + label: '未请求', + description: '还没有发起该资源生成。', + badgeClass: 'bg-slate-100 text-slate-600 border border-slate-200', + }, + generating: { + label: '生成中', + description: '资源正在生成,请稍候。', + badgeClass: 'bg-amber-50 text-amber-700 border border-amber-100', + }, + ready: { + label: '已就绪', + description: '该资源可使用。', + badgeClass: 'bg-emerald-50 text-emerald-700 border border-emerald-100', + }, + failed: { + label: '失败', + description: '最近一次生成失败,可以稍后重试。', + badgeClass: 'bg-rose-50 text-rose-700 border border-rose-100', + }, +} + +export function getGenerationStatusMeta(status?: string): StatusMeta { + return generationStatusMetaMap[(status ?? 'narrative_ready') as StoryGenerationStatus] + ?? generationStatusMetaMap.narrative_ready +} + +export function getAssetStatusMeta(status?: string): StatusMeta { + return assetStatusMetaMap[(status ?? 'not_requested') as StoryAssetStatus] + ?? assetStatusMetaMap.not_requested +} diff --git a/admin-frontend/src/views/MyStories.vue b/admin-frontend/src/views/MyStories.vue index 3b46ebb..2078003 100644 --- a/admin-frontend/src/views/MyStories.vue +++ b/admin-frontend/src/views/MyStories.vue @@ -21,6 +21,7 @@ interface StoryItem { title: string image_url: string | null created_at: string + mode: string } const router = useRouter() @@ -60,6 +61,10 @@ function goToCreate() { showCreateModal.value = true } +function getStoryPath(story: StoryItem) { + return story.mode === 'storybook' ? `/storybook/view/${story.id}` : `/story/${story.id}` +} + onMounted(() => { fetchStories() if (router.currentRoute.value.query.openCreate) { @@ -140,7 +145,7 @@ onMounted(() => { diff --git a/admin-frontend/src/views/StoryDetail.vue b/admin-frontend/src/views/StoryDetail.vue index 581e5bc..d42f5b9 100644 --- a/admin-frontend/src/views/StoryDetail.vue +++ b/admin-frontend/src/views/StoryDetail.vue @@ -1,10 +1,11 @@ - + diff --git a/admin-frontend/src/views/StorybookViewer.vue b/admin-frontend/src/views/StorybookViewer.vue index 8cfac8f..8db7563 100644 --- a/admin-frontend/src/views/StorybookViewer.vue +++ b/admin-frontend/src/views/StorybookViewer.vue @@ -1,197 +1,340 @@ - - - - - - + + + + + diff --git a/backend/app/api/admin_providers.py b/backend/app/api/admin_providers.py index d66e829..d665f0c 100644 --- a/backend/app/api/admin_providers.py +++ b/backend/app/api/admin_providers.py @@ -6,7 +6,9 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.core.admin_auth import admin_guard from app.db.admin_models import Provider from app.db.database import get_db +from app.services.adapters.registry import AdapterRegistry from app.services.cost_tracker import cost_tracker +from app.services.provider_router import DEFAULT_PROVIDERS from app.services.secret_service import SecretService router = APIRouter(dependencies=[Depends(admin_guard)]) @@ -54,11 +56,6 @@ class ProviderResponse(BaseModel): model_config = ConfigDict(from_attributes=True) - -from app.services.adapters.registry import AdapterRegistry -from app.services.provider_router import DEFAULT_PROVIDERS - - @router.get("/providers/adapters") async def list_available_adapters(): """获取所有可用的适配器类型 (定义的类)。""" diff --git a/backend/app/api/profiles.py b/backend/app/api/profiles.py index 7ccd25d..2c6bc81 100644 --- a/backend/app/api/profiles.py +++ b/backend/app/api/profiles.py @@ -266,13 +266,18 @@ async def get_profile_timeline( if not obt_at: obt_at = u.updated_at.isoformat() - events.append(TimelineEvent( - date=obt_at, - type="achievement", - title=f"获得成就:{ach.get('type')}", - description=ach.get('description'), - metadata={"universe_id": u.id, "source_story_id": ach.get("source_story_id")} - )) + events.append( + TimelineEvent( + date=obt_at, + type="achievement", + title=f"获得成就:{ach.get('type')}", + description=ach.get("description"), + metadata={ + "universe_id": u.id, + "source_story_id": ach.get("source_story_id"), + }, + ) + ) # Sort by date desc events.sort(key=lambda x: x.date, reverse=True) diff --git a/backend/app/api/reading_events.py b/backend/app/api/reading_events.py index 2f9b6ab..3c9b15a 100644 --- a/backend/app/api/reading_events.py +++ b/backend/app/api/reading_events.py @@ -45,7 +45,11 @@ class ReadingEventResponse(BaseModel): from_attributes = True -@router.post("/reading-events", response_model=ReadingEventResponse, status_code=status.HTTP_201_CREATED) +@router.post( + "/reading-events", + response_model=ReadingEventResponse, + status_code=status.HTTP_201_CREATED, +) async def create_reading_event( payload: ReadingEventCreate, user: User = Depends(require_user), diff --git a/backend/app/db/admin_models.py b/backend/app/db/admin_models.py index 639ac68..ea8dfc7 100644 --- a/backend/app/db/admin_models.py +++ b/backend/app/db/admin_models.py @@ -29,7 +29,10 @@ class Provider(Base): weight: Mapped[int] = mapped_column(Integer, default=1) priority: Mapped[int] = mapped_column(Integer, default=0) enabled: Mapped[bool] = mapped_column(Boolean, default=True) - config_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) # 存储额外配置(speed, vol, etc) + config_json: Mapped[dict | None] = mapped_column( + JSON, + nullable=True, + ) # 存储额外配置(speed, vol, etc) config_ref: Mapped[str] = mapped_column(String(100), nullable=True) # 环境变量 key 名称(回退) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=datetime.utcnow) updated_at: Mapped[datetime] = mapped_column( diff --git a/backend/app/services/adapters/image/__init__.py b/backend/app/services/adapters/image/__init__.py index dd5c46c..919f82a 100644 --- a/backend/app/services/adapters/image/__init__.py +++ b/backend/app/services/adapters/image/__init__.py @@ -1,3 +1,4 @@ -"""图像生成适配器。"""# Image adapters -from app.services.adapters.image import cqtai as _image_cqtai_adapter # noqa: F401 -from app.services.adapters.image import antigravity as _image_antigravity_adapter # noqa: F401 +"""图像生成适配器。""" + +from app.services.adapters.image import antigravity as _image_antigravity_adapter # noqa: F401 +from app.services.adapters.image import cqtai as _image_cqtai_adapter # noqa: F401 diff --git a/backend/app/services/adapters/image/antigravity.py b/backend/app/services/adapters/image/antigravity.py index a50571e..fb41871 100644 --- a/backend/app/services/adapters/image/antigravity.py +++ b/backend/app/services/adapters/image/antigravity.py @@ -4,11 +4,9 @@ 支持 gemini-3-pro-image 等模型。 """ -import base64 -import time -from typing import Any - -from openai import AsyncOpenAI +import time + +from openai import AsyncOpenAI from tenacity import ( retry, retry_if_exception_type, @@ -105,13 +103,13 @@ class AntigravityImageAdapter(BaseAdapter[str]): return image_url async def health_check(self) -> bool: - """检查 Antigravity API 是否可用。""" - try: - # 简单测试连通性 - response = await self.client.chat.completions.create( - model=self.config.model or DEFAULT_MODEL, - messages=[{"role": "user", "content": "test"}], - max_tokens=1, + """检查 Antigravity API 是否可用。""" + try: + # 简单测试连通性 + await self.client.chat.completions.create( + model=self.config.model or DEFAULT_MODEL, + messages=[{"role": "user", "content": "test"}], + max_tokens=1, ) return True except Exception as e: diff --git a/backend/app/services/memory_service.py b/backend/app/services/memory_service.py index ccf9a92..028092a 100644 --- a/backend/app/services/memory_service.py +++ b/backend/app/services/memory_service.py @@ -98,10 +98,14 @@ async def build_enhanced_memory_context( p_desc = f"{protagonist.get('name', '主角')} ({protagonist.get('personality', '')})" context_parts.append(f"主角设定:{p_desc}") - # 常驻角色 - if universe.recurring_characters: - chars = [f"{c.get('name')} ({c.get('type')})" for c in universe.recurring_characters if isinstance(c, dict)] - context_parts.append(f"已知伙伴:{'、'.join(chars)}") + # 常驻角色 + if universe.recurring_characters: + chars = [ + f"{c.get('name')} ({c.get('type')})" + for c in universe.recurring_characters + if isinstance(c, dict) + ] + context_parts.append(f"已知伙伴:{'、'.join(chars)}") # 成就 if universe.achievements: diff --git a/backend/app/services/provider_cache.py b/backend/app/services/provider_cache.py index 23000f5..f3421e2 100644 --- a/backend/app/services/provider_cache.py +++ b/backend/app/services/provider_cache.py @@ -17,9 +17,10 @@ logger = get_logger(__name__) ProviderType = Literal["text", "image", "tts", "storybook"] -class CachedProvider(BaseModel): - """Serializable provider configuration matching DB model fields.""" - id: str +class CachedProvider(BaseModel): + """Serializable provider configuration matching DB model fields.""" + + id: str name: str type: str adapter: str @@ -43,28 +44,30 @@ CACHE_KEY = "dreamweaver:providers:config" async def reload_providers(db: AsyncSession) -> dict[ProviderType, list[CachedProvider]]: """Reload providers from DB and update Redis cache.""" try: - result = await db.execute(select(Provider).where(Provider.enabled == True)) # noqa: E712 - providers = result.scalars().all() - - # Convert to Pydantic models - cached_list = [] - for p in providers: - cached_list.append(CachedProvider( - id=p.id, - name=p.name, - type=p.type, - adapter=p.adapter, - model=p.model, - api_base=p.api_base, - api_key=p.api_key, - timeout_ms=p.timeout_ms, - max_retries=p.max_retries, - weight=p.weight, - priority=p.priority, - enabled=p.enabled, - config_json=p.config_json, - config_ref=p.config_ref - )) + result = await db.execute(select(Provider).where(Provider.enabled == True)) # noqa: E712 + providers = result.scalars().all() + + # Convert to Pydantic models + cached_list = [] + for p in providers: + cached_list.append( + CachedProvider( + id=p.id, + name=p.name, + type=p.type, + adapter=p.adapter, + model=p.model, + api_base=p.api_base, + api_key=p.api_key, + timeout_ms=p.timeout_ms, + max_retries=p.max_retries, + weight=p.weight, + priority=p.priority, + enabled=p.enabled, + config_json=p.config_json, + config_ref=p.config_ref, + ) + ) # Group by type grouped: dict[str, list[CachedProvider]] = defaultdict(list) @@ -77,19 +80,19 @@ async def reload_providers(db: AsyncSession) -> dict[ProviderType, list[CachedPr # Update Redis redis = await get_redis() - # Serialize entire dict structure - # Pydantic -> dict -> json - json_data = {k: [p.model_dump() for p in v] for k, v in grouped.items()} - await redis.set(CACHE_KEY, json.dumps(json_data)) - - # Update local cache - _local_cache.clear() - _local_cache.update(grouped) - return grouped - - except Exception as e: - logger.error("failed_to_reload_providers", error=str(e)) - raise + # Serialize entire dict structure + # Pydantic -> dict -> json + json_data = {k: [p.model_dump() for p in v] for k, v in grouped.items()} + await redis.set(CACHE_KEY, json.dumps(json_data)) + + # Update local cache + _local_cache.clear() + _local_cache.update(grouped) + return grouped + + except Exception as e: + logger.error("failed_to_reload_providers", error=str(e)) + raise async def get_providers(provider_type: ProviderType) -> list[CachedProvider]: @@ -102,8 +105,8 @@ async def get_providers(provider_type: ProviderType) -> list[CachedProvider]: if provider_type in raw_dict: return [CachedProvider(**item) for item in raw_dict[provider_type]] return [] - except Exception as e: - logger.warning("redis_cache_read_failed", error=str(e)) - - # Fallback to local memory - return _local_cache.get(provider_type, []) + except Exception as e: + logger.warning("redis_cache_read_failed", error=str(e)) + + # Fallback to local memory + return _local_cache.get(provider_type, []) diff --git a/backend/tests/test_auth.py b/backend/tests/test_auth.py index cea70ba..69f2adc 100644 --- a/backend/tests/test_auth.py +++ b/backend/tests/test_auth.py @@ -1,7 +1,6 @@ """认证相关测试。""" -import pytest -from fastapi.testclient import TestClient +from fastapi.testclient import TestClient from app.core.security import create_access_token, decode_access_token