Files
dreamweaver/.claude/specs/product-roadmap/PROVIDER-PLATFORM-RFC.md
zhangtuo e9d7f8832a Initial commit: clean project structure
- Backend: FastAPI + SQLAlchemy + Celery (Python 3.11+)
- Frontend: Vue 3 + TypeScript + Pinia + Tailwind
- Admin Frontend: separate Vue 3 app for management
- Docker Compose: 9 services orchestration
- Specs: design prototypes, memory system PRD, product roadmap

Cleanup performed:
- Removed temporary debug scripts from backend root
- Removed deprecated admin_app.py (embedded UI)
- Removed duplicate docs from admin-frontend
- Updated .gitignore for Vite cache and egg-info
2026-01-20 18:20:03 +08:00

23 KiB
Raw Blame History

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 接口定义

# 统一适配器接口
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 适配器注册表

# 适配器注册表 - 支持动态注册
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 适配器实现示例

# 图像适配器示例: 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 智能路由器

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. 数据模型扩展

-- 供应商表 (已有,需扩展)
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: 并行执行

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: 部分成功处理

核心原则: 部分成功 > 全部失败

@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 兼容性好

后端实现:

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())

前端实现:

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: 断点续传 (可选)

适用于网络不稳定场景,支持刷新页面后继续:

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


确认后删除此区块

确认所有决策后,可以删除未选中的选项,保留最终方案作为实现依据。