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

678 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
---
## 确认后删除此区块
确认所有决策后,可以删除未选中的选项,保留最终方案作为实现依据。