fix: align canvas model options with backend

This commit is contained in:
2026-05-25 18:55:33 +08:00
parent 103907ca3a
commit 84d9de6b30
15 changed files with 181 additions and 397 deletions

View File

@@ -12,7 +12,7 @@
- 详见 `CLAUDE.md` 立项决策段 + `.memory/plan.md` 七步管线拆解
- 风格:`04-Dark-Gallery-Ambient`(路径:`~/Projects/research/20260305-网页风格库/04-Dark-Gallery-Ambient.md`
- 第一冲刺:步骤 1-4下载 / 拆轨 / 关键帧 / ASR+翻译)
- 当前产品方向2026-05-25 上游画布能力恢复版):默认入口是多人通用的 SKG 营销内容生产平台,`https://marketing.skg.com` 登录后直接进入个人生成画布,`/canvas/` 只作为旧链接兼容跳转到根域名。终端可见品牌位只放 SKG logo不在主界面展示“生图生视频”“SKG 生成画布”或长系统名。画布本体尽量恢复 `chatfire-AI/huobao-canvas` 的成熟交互不再削成三模式单输入框保留首页推荐词、画布底部推荐词、AI 润色、自动执行、工作流模板、首帧/尾帧/参考图节点、图片/视频/LLM 配置节点、模型配置和批量下载等上游能力多角度分镜、故事板、图转视频、绘本等工作流按上游结构创建节点。API 接入是例外:生成调用继续走本项目后端 `/api` 和当前登录 Cookie不要求员工在浏览器配置个人 API KeyAPI 设置弹窗只保留模型/端点配置外观和本地模型管理,不能出现上游注册链接或外部品牌。用户登录后仍只看到自己的任务、结果和详情页,继续沿用后端 owner 隔离;每个浏览器的画布项目先保存在本地 localStorage图片/视频资产按登录用户写入后端 job。旧 TK 复刻工作台、Agent Cut 一键出片和营销图文方案保留为高级/详情页能力,不再作为默认首页入口或默认理解框架。
- 当前产品方向2026-05-25 上游画布能力恢复版):默认入口是多人通用的 SKG 营销内容生产平台,`https://marketing.skg.com` 登录后直接进入个人生成画布,`/canvas/` 只作为旧链接兼容跳转到根域名。终端可见品牌位只放 SKG logo不在主界面展示“生图生视频”“SKG 生成画布”或长系统名。画布本体尽量恢复 `chatfire-AI/huobao-canvas` 的成熟交互不再削成三模式单输入框保留首页推荐词、画布底部推荐词、AI 润色、自动执行、工作流模板、首帧/尾帧/参考图节点、图片/视频/LLM 配置节点、模型配置和批量下载等上游能力多角度分镜、故事板、图转视频、绘本等工作流按上游结构创建节点。API 接入是例外:生成调用继续走本项目后端 `/api` 和当前登录 Cookie不要求员工在浏览器配置个人 API Key图片/视频模型选择只显示后端已经接通的媒体模型,不能让浏览器本地自定义或旧缓存模型进入生成下拉。API 设置弹窗只保留模型/端点配置外观,不能出现上游注册链接或外部品牌。用户登录后仍只看到自己的任务、结果和详情页,继续沿用后端 owner 隔离;每个浏览器的画布项目先保存在本地 localStorage图片/视频资产按登录用户写入后端 job。旧 TK 复刻工作台、Agent Cut 一键出片和营销图文方案保留为高级/详情页能力,不再作为默认首页入口或默认理解框架。
## 部署事实
- 平台VPS `76.13.31.179`Ubuntu 24.04 / Docker Compose / Coolify Traefik

View File

@@ -4404,25 +4404,27 @@ def _normalize_video_size(raw: str | None) -> str:
def video_model_options() -> list[dict]:
label_map = {
"seedance": "Seedance",
"seedance": "Seedance 2.0 Fast",
"kling": "Kling",
"veo3": "Veo 3",
"veo": "Veo",
"voe": "Veo",
}
seen: set[str] = set()
concrete_label_map = {
"doubao-seedance-2-0-fast-260128": "Seedance 2.0 Fast",
}
seen_models: set[str] = set()
options: list[dict] = []
for key in ["seedance", "kling", "veo3", "veo"]:
if key not in VIDEO_MODEL_ALIASES:
continue
model = VIDEO_MODEL_ALIASES[key]
unique_key = f"{key}:{model}"
if unique_key in seen:
if model in seen_models:
continue
seen.add(unique_key)
seen_models.add(model)
options.append({
"id": key,
"label": label_map.get(key, key),
"label": concrete_label_map.get(model, label_map.get(key, key)),
"model": model,
"description": f"当前视频网关可选模型;单次时长最高 {max(video_duration_options())}",
"duration_options": video_duration_options(),
@@ -7814,7 +7816,7 @@ def resolve_video_model(raw: str | None) -> str:
requested = (raw or VIDEO_MODEL or "seedance").strip()
lowered = requested.lower()
if lowered in {"sora", "sora-2", "sora_2"}:
raise HTTPException(400, "Sora 已停用,请选择 Seedance / Kling / Veo 3")
raise HTTPException(400, "Sora 已停用,请选择当前已接入的 Seedance")
return VIDEO_MODEL_ALIASES.get(lowered, requested)

View File

@@ -580,8 +580,9 @@
<p><strong>2026-05-25 三模式版:</strong>默认首页再收敛为一个中央对话框,首页和画布底部输入框只让用户选文生图、文生视频、图生视频,然后手写提示词生成。图生视频只显示“上传图片”,不再把首帧 / 首尾帧这类模型实现概念作为主入口;营销图文不再作为首页默认入口。后端 <code>/health</code> 返回可选图片 / 视频模型、图片尺寸、视频画幅和真实可用视频时长,首页按返回值显示模型和规格选择;当前 Doubao / Seedance 生产链路单条最长 15 秒,不向用户暴露 30 秒按钮。</p>
<p><strong>2026-05-25 根域名画布版:</strong><code>https://marketing.skg.com</code> 登录后直接进入个人生成画布,不再先进入 React 单对话框首页再点画布;<code>/canvas/</code> 只保留为旧链接兼容跳转。后续优先少改成熟画布结构,只在必要时改模式文案、生成接入和结果/队列显示。</p>
<p><strong>2026-05-25 上游能力恢复版:</strong>用户明确要求“API 没关系,其他恢复,别削弱”。因此根域名画布恢复 <code>chatfire-AI/huobao-canvas</code> 的成熟节点和工作流结构推荐词、AI 润色、自动执行、工作流模板、首帧/尾帧/参考图节点、图片/视频/LLM 配置、多角度分镜、故事板、绘本和批量下载都保留;只继续替换品牌、路由和 API 接入。生成请求仍走 SKG 后端 <code>/api</code> 与登录 Cookie员工不需要个人 API Key。</p>
<p><strong>2026-05-25 媒体模型接入收口:</strong>图片和视频模型选择只暴露当前后端真实可用项:图片为 <code>auto</code><code>gpt-image-2</code><code>gemini-3-pro-image-preview</code>;视频当前只接通 <code>Seedance 2.0 Fast</code>(真实模型 <code>doubao-seedance-2-0-fast-260128</code>)。旧上游的 Nano Banana、Seedream、Kling、Veo 或浏览器本地自定义媒体模型不能进入生成下拉,避免同事选到实际不可用的模型。</p>
</div>
<p>当前默认业务管线是“个人隔离任务 → 根域名进入个人画布 → 用提示词、推荐词、AI 润色或工作流模板创建节点 → 画布自动执行或手动连接图片/视频/文本节点 → 生成结果沉淀在当前个人画布 → 需要时进入详情页继续编辑”。画布不再被削成三模式入口;首帧、尾帧、参考图、图生视频、多角度分镜、故事板和绘本等上游概念按节点能力保留。底层生成仍由 <code>web/canvas-app/src/hooks/useApi.js</code> 适配到本项目 <code>/creative/jobs/image</code><code>/jobs/{id}/frames/{idx}/generate</code><code>/jobs/{id}/frames/{idx}/storyboard/video</code>,并按当前登录用户写入个人 job。多人互不影响依赖后端 <code>owner_id</code> 和飞书 / 备用登录会话隔离。旧 React 单对话框首页、信息流复刻链路仍保留在源码里作为回滚/高级能力,但不作为生产默认入口。</p>
<p>当前默认业务管线是“个人隔离任务 → 根域名进入个人画布 → 用提示词、推荐词、AI 润色或工作流模板创建节点 → 画布自动执行或手动连接图片/视频/文本节点 → 生成结果沉淀在当前个人画布 → 需要时进入详情页继续编辑”。画布不再被削成三模式入口;首帧、尾帧、参考图、图生视频、多角度分镜、故事板和绘本等上游概念按节点能力保留。底层生成仍由 <code>web/canvas-app/src/hooks/useApi.js</code> 适配到本项目 <code>/creative/jobs/image</code><code>/jobs/{id}/frames/{idx}/generate</code><code>/jobs/{id}/frames/{idx}/storyboard/video</code>,并按当前登录用户写入个人 job。图片尺寸只显示 <code>auto</code><code>1024x1536</code><code>1024x1024</code><code>1536x1024</code>;视频画幅只显示 <code>720x1280</code><code>1280x720</code><code>1024x1024</code><code>960x1280</code>;视频时长只显示 <code>5/8/10/12/15</code> 秒。多人互不影响依赖后端 <code>owner_id</code> 和飞书 / 备用登录会话隔离。旧 React 单对话框首页、信息流复刻链路仍保留在源码里作为回滚/高级能力,但不作为生产默认入口。</p>
<div class="pipeline">
<div class="step"><div class="num">01</div><h3>个人任务</h3><p><code>GET /jobs</code> 按当前登录用户过滤;旧无 owner 任务只对备用账号可见。</p></div>
<div class="step"><div class="num">02</div><h3>进入画布</h3><p>用户直接在根域名个人画布里操作,上游项目列表、推荐词、节点菜单、工作流模板和批量下载能力保留。</p></div>
@@ -607,6 +608,7 @@
<tr><td><code>web/app/page.tsx</code></td><td>旧 React 单对话框生成台源码仍保留,便于以后回滚或抽能力;当前生产根域名已经由 <code>web/canvas-app/</code> 画布产物覆盖,不再把这个 React 首页作为默认首屏。该页面里的模式也已收敛为文生图、文生视频、图生视频;图生视频只显示“上传图片”,不把“首帧/首尾帧”作为用户入口。旧 TK 复刻工作台组件仍保留在 <code>web/components/ad-recreation-board.tsx</code>,但不再作为默认首页渲染。</td></tr>
<tr><td><code>web/canvas-app/</code></td><td>SKG 内部画布应用:从 <code>chatfire-AI/huobao-canvas</code> 交互逻辑改造而来。当前策略是“保留成熟画布能力,替换品牌/路由/API”Vue Flow 节点画布、项目列表、推荐词、AI 润色、自动执行、工作流模板、首帧/尾帧/参考图节点、图片/视频/LLM 配置节点、模型配置和批量下载都保留;可见品牌收敛为 SKG logo不展示上游注册链接或外部品牌。生产路径固定为根域名 <code>/</code>,内部路由用 <code>/p/:id?</code>;来源说明保存在 <code>THIRD_PARTY_NOTICES.md</code>,不展示给终端用户。</td></tr>
<tr><td><code>web/canvas-app/src/views/Canvas.vue</code></td><td>画布主交互:恢复上游底部 prompt composer、<code>AI 润色</code><code>自动执行</code>、推荐词、节点菜单、工作流面板、API/模型设置入口和批量下载入口。自动执行会调用 <code>useWorkflowOrchestrator</code> 分析提示词,创建文生图、图转视频、故事板、多角度分镜或绘本节点组;手动模式只创建文本节点,用户自行连接节点。</td></tr>
<tr><td><code>web/canvas-app/src/config/models.js</code></td><td>画布媒体模型和规格的前端白名单:图片只内置 <code>auto</code><code>gpt-image-2</code><code>gemini-3-pro-image-preview</code>,尺寸只内置 <code>auto</code><code>1024x1536</code><code>1024x1024</code><code>1536x1024</code>;视频只内置 <code>seedance</code> / <code>Seedance 2.0 Fast</code>,画幅和时长对齐后端 <code>/health</code> 能力边界。<code>useModelConfig.js</code> 和 Pinia 模型 store 会忽略浏览器本地自定义图片/视频模型,防止旧缓存把不可用模型带回生成下拉。</td></tr>
<tr><td><code>web/canvas-app/src/hooks/useApi.js</code></td><td>画布到本项目后端的适配层:不再读取浏览器 API Key而是使用当前登录会话 Cookie 调用 <code>/api</code>。文生图 / 图生图先创建轻量 creative job再调用 <code>/frames/0/generate</code>;文生视频 / 图生视频调用 <code>/storyboard/video</code> 并轮询 <code>/jobs/{id}</code>,完成后把图片或 mp4 URL 写回画布节点。</td></tr>
<tr><td><code>web/scripts/sync-canvas-root.mjs</code></td><td>构建桥接脚本:在 <code>next build</code> 静态导出完成后,把 Vite 画布产物 <code>web/canvas-app/dist</code> 覆盖到 <code>web/out</code> 根目录,使 <code>https://marketing.skg.com</code> 登录后直接进入画布;旧 <code>web/scripts/sync-canvas-dist.mjs</code> 保留但不再由生产构建调用。</td></tr>
<tr><td><code>web/app/detail/page.tsx</code></td><td>任务详情页:静态导出路由 <code>/detail/?job=&lt;id&gt;</code>,通过 query 读取 job id调用 <code>getJob</code> 恢复同一任务。页面展示参考图、全部生成图、视频候选、营销图文方案和历史提示词,可继续调用 <code>generateImage</code><code>generateStoryboardVideo</code><code>generateCreativeCopy</code>,并支持删除图片/视频。该页继续依赖后端 owner 过滤,用户不能通过切换 URL 读取别人的任务。</td></tr>
@@ -639,6 +641,7 @@
<table>
<tbody>
<tr><td><code>api/main.py</code></td><td>FastAPI 单文件后端登录会话、状态模型、任务恢复、下载、抽帧、Vision、清洗、元素、分镜、原音频转写/翻译、声音与背景音分析、后续口播改写/TTS、文件返回同时承载全局 <code>prompt_library</code><code>asset_library</code> 的磁盘索引、CRUD、删除保护和复制到 job API。轻量创作入口 <code>POST /creative/jobs/image</code> 把上传图片或空白底图写成一个只有 0 号关键帧的 <code>Job</code>,让首页直接复用生图/生视频接口;该接口兼容无 body / JSON 空对象 / 正常 multipart 上传,避免无首帧文生图或文生视频时空 multipart 被 FastAPI 在业务前置解析阶段拒绝;<code>/health</code> 返回 <code>image_options</code><code>image_size_options</code><code>video_options</code><code>video_size_options</code><code>video_duration_options</code><code>video_max_duration_seconds</code><code>/frames/{idx}/generate</code><code>model</code> 字段用于图片模型偏好,<code>size</code> 字段用于图片输出尺寸;<code>/storyboard/video</code> 继续使用 <code>model</code> 字段选择视频别名,并先校验画幅与时长能力边界,然后把 <code>GeneratedVideo</code> 写成 <code>queued</code> 占位并进入进程内视频队列。队列默认 <code>VIDEO_QUEUE_MAX_CONCURRENT=2</code><code>VIDEO_QUEUE_MAX_CONCURRENT_PER_USER=1</code>,同一用户连续提交不会占满全局并发;排队任务会回写 <code>queue_position</code><code>queue_size</code><code>queue_message</code>。旧 <code>AgentRun</code> 一键出片状态机、TK 复刻接口和 <code>POST /creative/copy</code> 继续保留。</td></tr>
<tr><td><code>video_model_options()</code></td><td>视频模型能力出口:如果 <code>seedance</code><code>kling</code><code>veo3</code><code>veo</code> 等业务别名实际都映射到同一个真实模型,会按真实模型去重,只给前端返回一个可用选项;当前生产真实模型为 <code>doubao-seedance-2-0-fast-260128</code>,前端显示为 <code>Seedance 2.0 Fast</code>。后续只有在服务器真的配置了不同可用视频模型时,才应把新的模型重新暴露给画布。</td></tr>
<tr><td><code>api/product_library/skg-products</code></td><td>内置 SKG 白底产品图库:<code>manifest.json</code> 记录从桌面产品图筛出的 gallery 白底图和桌面 4 张产品角度图,<code>images/</code> 存 45 张参考图。</td></tr>
<tr><td><code>api/character_library/skg-characters</code></td><td>内置相似主体形象库:从桌面 5 套策划形象导入,<code>manifest.json</code> 记录运动阳光男、都市型男、优雅白领女、运动辣妹、绅士大叔,每套含 7 张透明骨架参考图和一段 <code>prompt_brief</code>。相似主体生成时优先使用文字 brief 作为创意方向,避免把内置图作为强参考图复制。</td></tr>
<tr><td><code>asset_library/</code></td><td>全局素材库目录,和 <code>jobs/</code> 平级,不写入任何 job state。四类目录为 <code>subjects</code><code>products</code><code>scenes</code><code>videos</code>;每个素材自带 <code>manifest.json</code> 和图片/视频文件,<code>index.json</code> 只是启动扫描重建出来的缓存。库素材选用到 job 时必须复制文件到 <code>jobs/&lt;jobId&gt;/assets</code><code>storyboard-videos</code>,禁止直接保存 library 引用。</td></tr>
@@ -1206,6 +1209,20 @@ ProductRefStateItem {
<h2>变更记录</h2>
<p>这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。</p>
<div class="changelog">
<article class="change">
<header>
<h3>2026-05-25 · 媒体模型选择对齐真实后端能力</h3>
<span class="tag amber">API</span>
<span class="tag rose">UI</span>
<span class="tag blue">Docs</span>
</header>
<div class="body">
<p><strong>问题:</strong>恢复上游画布后,前端仍可能显示 Nano Banana、Seedream、Kling、Veo 等上游或旧缓存模型,但这些并不是当前 SKG 后端已经验证可用的生成通道,用户一旦选择就会失败或误解模型能力。</p>
<p><strong>改动:</strong><code>web/canvas-app/src/config/models.js</code> 将图片模型收口为 <code>auto</code><code>gpt-image-2</code><code>gemini-3-pro-image-preview</code>,视频模型收口为 <code>seedance</code> / <code>Seedance 2.0 Fast</code>;图片尺寸、视频画幅和视频时长对齐后端真实能力。<code>useModelConfig.js</code> 和 Pinia 模型 store 会忽略浏览器本地自定义图片/视频模型,并把旧 localStorage 选中值回退到默认可用模型。</p>
<p><strong>后端:</strong><code>api/main.py</code><code>video_model_options()</code> 按真实模型去重:当前 <code>kling</code><code>veo</code> 等别名如果都指向 <code>doubao-seedance-2-0-fast-260128</code><code>/health</code> 只返回一个 Seedance 选项,不再假装有多个不同视频模型。</p>
<p><strong>影响:</strong>员工在画布里只能选到当前可直接生成的图片/视频模型;后续新增 Kling、Veo 或其他图片模型时,必须先在后端完成真实模型配置和探针验证,再更新前端白名单。</p>
</div>
</article>
<article class="change">
<header>
<h3>2026-05-25 · 恢复上游画布能力API 保持 SKG 接入</h3>

View File

@@ -102,17 +102,6 @@
<span class="model-group-title">图片模型</span>
<n-tag size="tiny" type="success">{{ allImageModels.length }} </n-tag>
</div>
<div class="model-input-row">
<n-input
v-model:value="newImageModel"
placeholder="输入模型名称,如 dall-e-3"
size="small"
@keyup.enter="handleAddImageModel"
/>
<n-button size="small" type="primary" @click="handleAddImageModel" :disabled="!newImageModel">
添加
</n-button>
</div>
<div class="model-tags">
<n-tag
v-for="model in allImageModels"
@@ -133,17 +122,6 @@
<span class="model-group-title">视频模型</span>
<n-tag size="tiny" type="warning">{{ allVideoModels.length }} </n-tag>
</div>
<div class="model-input-row">
<n-input
v-model:value="newVideoModel"
placeholder="输入模型名称,如 sora-2"
size="small"
@keyup.enter="handleAddVideoModel"
/>
<n-button size="small" type="primary" @click="handleAddVideoModel" :disabled="!newVideoModel">
添加
</n-button>
</div>
<div class="model-tags">
<n-tag
v-for="model in allVideoModels"
@@ -235,8 +213,6 @@ const formData = reactive({
// New model inputs | 新模型输入
const newChatModel = ref('')
const newImageModel = ref('')
const newVideoModel = ref('')
// 初始化或切换渠道时,更新 API 配置
const updateFormApiConfig = () => {
@@ -273,20 +249,6 @@ const handleAddChatModel = () => {
}
}
const handleAddImageModel = () => {
if (newImageModel.value.trim()) {
modelStore.addCustomImageModel(newImageModel.value.trim())
newImageModel.value = ''
}
}
const handleAddVideoModel = () => {
if (newVideoModel.value.trim()) {
modelStore.addCustomVideoModel(newVideoModel.value.trim())
newVideoModel.value = ''
}
}
// Handle remove models | 处理删除模型
const handleRemoveChatModel = (modelKey) => {
modelStore.removeCustomChatModel(modelKey)

View File

@@ -166,7 +166,7 @@ import { useImageGeneration } from '../../hooks'
import { updateNode, addNode, addEdge, nodes, edges, duplicateNode, removeNode } from '../../stores/canvas'
import NodeHandleMenu from './NodeHandleMenu.vue'
import { useModelStore } from '../../stores/pinia'
import { getModelSizeOptions, getModelQualityOptions, getModelConfig, DEFAULT_IMAGE_MODEL } from '../../stores/models'
import { getModelSizeOptions, getModelQualityOptions, getModelConfig, DEFAULT_IMAGE_MODEL, DEFAULT_IMAGE_SIZE } from '../../stores/models'
import { parseMentions } from '../../hooks/useNodeRef'
// 使用 Pinia store 获取模型选项(根据渠道过滤)
@@ -189,7 +189,7 @@ const { loading, error, images: generatedImages, generate } = useImageGeneration
// Local state | 本地状态
const showHandleMenu = ref(false)
const localModel = ref(props.data?.model || DEFAULT_IMAGE_MODEL)
const localSize = ref(props.data?.size || '2048x2048')
const localSize = ref(props.data?.size || DEFAULT_IMAGE_SIZE)
const localQuality = ref(props.data?.quality || 'standard')
// Label editing state | Label 编辑状态
@@ -288,7 +288,8 @@ onMounted(() => {
if (!localModel.value || !isModelAvailable) {
// 使用 store 中的默认模型或第一个可用模型
localModel.value = modelStore.selectedImageModel || availableModels[0]?.key || DEFAULT_IMAGE_MODEL
const selected = availableModels.find(m => m.key === modelStore.selectedImageModel)?.key
localModel.value = selected || availableModels[0]?.key || DEFAULT_IMAGE_MODEL
updateNode(props.id, { model: localModel.value })
}
})
@@ -486,8 +487,7 @@ const handleModelSelect = (key) => {
let defaultSize = config?.defaultParams?.size
if (!defaultSize && newSizeOptions.length > 0) {
// 备用逻辑:查找 2048 或最接近的尺寸
defaultSize = newSizeOptions.find(o => o.key === '2048x2048')?.key
defaultSize = newSizeOptions.find(o => o.key === DEFAULT_IMAGE_SIZE)?.key
|| newSizeOptions.find(o => o.key.includes('1024'))?.key
|| newSizeOptions[0].key
}
@@ -508,7 +508,7 @@ const handleQualitySelect = (quality) => {
// Update size to first option of new quality | 更新尺寸为新画质的第一个选项
const newSizeOptions = getModelSizeOptions(localModel.value, quality)
if (newSizeOptions.length > 0) {
const defaultSize = quality === '4k' ? newSizeOptions.find(o => o.key.includes('4096'))?.key || newSizeOptions[4]?.key : newSizeOptions[4]?.key
const defaultSize = newSizeOptions.find(o => o.key === DEFAULT_IMAGE_SIZE)?.key
localSize.value = defaultSize || newSizeOptions[0].key
updateNode(props.id, { quality, size: localSize.value })
} else {

View File

@@ -417,8 +417,8 @@ const handleSelect = (item) => {
// Create imageConfig node
const configNodeId = addNode('imageConfig', { x: nodeX + 900, y: nodeY }, {
model: 'doubao-seedream-4-5-251128',
size: '2048x2048',
model: 'auto',
size: '1024x1536',
label: '生图配置'
})
@@ -626,8 +626,8 @@ const createInpaintWorkflow = () => {
// Create imageConfig node for inpainting | 创建图生图配置节点
const configNodeId = addNode('imageConfig', { x: nodeX + 600, y: nodeY }, {
model: 'doubao-seedream-4-5-251128',
size: '2048x2048',
model: 'auto',
size: '1024x1536',
label: '局部重绘',
inpaintMode: true
})
@@ -848,8 +848,8 @@ const handleImageGen = () => {
// Create imageConfig node for generation | 创建生图配置节点
const configNodeId = addNode('imageConfig', { x: nodeX + 900, y: nodeY }, {
model: 'doubao-seedream-4-5-251128',
size: '2048x2048',
model: 'auto',
size: '1024x1536',
label: '生图配置'
})

View File

@@ -417,7 +417,7 @@ const handleSelect = (item) => {
const nodeY = currentNode?.position?.y || 0
const defaultData = {
imageConfig: { model: 'doubao-seedream-4-5-251128', size: '2048x2048', label: '文生图' },
imageConfig: { model: 'auto', size: '1024x1536', label: '文生图' },
videoConfig: { label: '视频生成' },
text: { content: '', label: '文本输入' }
}
@@ -1003,8 +1003,8 @@ const handleSplitToTextWithImage = () => {
position: { x: baseX + colSpacing, y: segY },
data: {
label: `图片 ${i + 1}`,
model: 'doubao-seedream-4-5-251128',
size: '2048x2048'
model: 'auto',
size: '1024x1536'
}
}
nodeSpecs.push(imageConfigSpec)

View File

@@ -440,7 +440,7 @@ const handleSelect = (item) => {
const nodeY = currentNode?.position?.y || 0
const defaultData = {
imageConfig: { model: 'doubao-seedream-4-5-251128', size: '2048x2048', label: '文生图' },
imageConfig: { model: 'auto', size: '1024x1536', label: '文生图' },
videoConfig: { label: '视频生成' },
llmConfig: { label: 'LLM文本生成' }
}
@@ -695,8 +695,8 @@ const handleImageGen = () => {
// Create imageConfig node | 创建text生图配置节点
const configNodeId = addNode('imageConfig', { x: nodeX + 400, y: nodeY }, {
model: 'doubao-seedream-4-5-251128',
size: '2048x2048',
model: 'auto',
size: '1024x1536',
label: '文生图'
})

View File

@@ -489,7 +489,8 @@ onMounted(() => {
if (!localModel.value || !isModelAvailable) {
// 使用 store 中的默认模型或第一个可用模型
localModel.value = modelStore.selectedVideoModel || availableModels[0]?.key || DEFAULT_VIDEO_MODEL
const selected = availableModels.find(m => m.key === modelStore.selectedVideoModel)?.key
localModel.value = selected || availableModels[0]?.key || DEFAULT_VIDEO_MODEL
updateNode(props.id, { model: localModel.value })
}
})

View File

@@ -3,36 +3,20 @@
* Centralized model configuration | 集中模型配置
*/
// Seedream image size options | 豆包图片尺寸选项
// SKG backend image size options | SKG 后端图片尺寸选项
export const SEEDREAM_SIZE_OPTIONS = [
{ label: '21:9', key: '3024x1296' },
{ label: '16:9', key: '2560x1440' },
{ label: '4:3', key: '2304x1728' },
{ label: '3:2', key: '2496x1664' },
{ label: '1:1', key: '2048x2048' },
{ label: '2:3', key: '1664x2496' },
{ label: '3:4', key: '1728x2304' },
{ label: '9:16', key: '1440x2560' },
{ label: '9:21', key: '1296x3024' }
{ label: '自动', key: 'auto' },
{ label: '竖图 2:3', key: '1024x1536' },
{ label: '方图 1:1', key: '1024x1024' },
{ label: '横图 3:2', key: '1536x1024' }
]
// Seedream 4K image size options | 豆包4K图片尺寸选项
export const SEEDREAM_4K_SIZE_OPTIONS = [
{ label: '21:9', key: '6198x2656' },
{ label: '16:9', key: '5404x3040' },
{ label: '4:3', key: '4694x3520' },
{ label: '3:2', key: '4992x3328' },
{ label: '1:1', key: '4096x4096' },
{ label: '2:3', key: '3328x4992' },
{ label: '3:4', key: '3520x4694' },
{ label: '9:16', key: '3040x5404' },
{ label: '9:21', key: '2656x6198' }
]
// Kept for compatibility with upstream model helpers.
export const SEEDREAM_4K_SIZE_OPTIONS = SEEDREAM_SIZE_OPTIONS
// Seedream quality options | 豆包画质选项
// SKG backend currently exposes model choice and size; quality is retained as a no-op UI field.
export const SEEDREAM_QUALITY_OPTIONS = [
{ label: '标准画质', key: 'standard' },
{ label: '4K 高清', key: '4k' }
{ label: '标准', key: 'standard' }
]
export const BANANA_SIZE_OPTIONS = [
@@ -48,51 +32,37 @@ export const BANANA_SIZE_OPTIONS = [
// Image generation models | 图片生成模型
export const IMAGE_MODELS = [
{
label: 'Nano Banana 2',
key: 'nano-banana-2',
provider: ['chatfire'], // 火宝渠道
sizes: BANANA_SIZE_OPTIONS.map(s => s.key),
// qualities: SEEDREAM_QUALITY_OPTIONS,
// getSizesByQuality: (quality) => quality === '4k' ? SEEDREAM_4K_SIZE_OPTIONS : SEEDREAM_SIZE_OPTIONS,
defaultParams: {
size: '1x1',
quality: 'standard',
style: 'vivid'
}
},
{
label: 'Nano Banana Pro',
key: 'nano-banana-pro',
provider: ['chatfire'], // 火宝渠道
sizes: BANANA_SIZE_OPTIONS.map(s => s.key),
// qualities: SEEDREAM_QUALITY_OPTIONS,
// getSizesByQuality: (quality) => quality === '4k' ? SEEDREAM_4K_SIZE_OPTIONS : SEEDREAM_SIZE_OPTIONS,
defaultParams: {
size: '1x1',
quality: 'standard',
style: 'vivid'
}
},
{
label: '豆包 Seedream 4.5',
key: 'doubao-seedream-4-5-251128',
provider: ['chatfire'], // 火宝渠道
label: '自动',
key: 'auto',
provider: ['chatfire'],
sizes: SEEDREAM_SIZE_OPTIONS.map(s => s.key),
qualities: SEEDREAM_QUALITY_OPTIONS,
getSizesByQuality: (quality) => quality === '4k' ? SEEDREAM_4K_SIZE_OPTIONS : SEEDREAM_SIZE_OPTIONS,
defaultParams: {
size: '2048x2048',
size: '1024x1536',
quality: 'standard',
style: 'vivid'
}
},
{
label: 'Nano Banana',
key: 'nano-banana',
provider: ['chatfire'], // 火宝渠道
tips: '尺寸写在提示词中: 尺寸 9:16',
sizes: [],
label: 'GPT Image 2',
key: 'gpt-image-2',
provider: ['chatfire'],
sizes: SEEDREAM_SIZE_OPTIONS.map(s => s.key),
qualities: SEEDREAM_QUALITY_OPTIONS,
defaultParams: {
size: '1024x1536',
quality: 'standard',
style: 'vivid'
}
},
{
label: 'Gemini 图片',
key: 'gemini-3-pro-image-preview',
provider: ['chatfire'],
sizes: SEEDREAM_SIZE_OPTIONS.map(s => s.key),
qualities: SEEDREAM_QUALITY_OPTIONS,
defaultParams: {
size: '1024x1536',
quality: 'standard',
style: 'vivid'
}
@@ -102,11 +72,10 @@ export const IMAGE_MODELS = [
// Video ratio options | 视频比例选项
export const VIDEO_RATIO_LIST = [
{ label: '16:9 (横版)', key: '16x9' },
{ label: '4:3', key: '4x3' },
{ label: '1:1 (方形)', key: '1x1' },
{ label: '3:4', key: '3x4' },
{ label: '9:16 (竖版)', key: '9x16' }
{ label: '竖屏 9:16', key: '720x1280' },
{ label: '横屏 16:9', key: '1280x720' },
{ label: '方形 1:1', key: '1024x1024' },
{ label: '竖屏 3:4', key: '960x1280' }
]
// Video resolution options for Seedance | Seedance 分辨率选项
@@ -118,102 +87,23 @@ export const SEEDANCE_RESOLUTION_OPTIONS = [
// Video generation models | 视频生成模型
export const VIDEO_MODELS = [
// Seedance 模型 - 1.5 Pro
{
label: 'Seedance 1.5 Pro (图文视频)',
key: 'doubao-seedance-1-5-pro-251215',
label: 'Seedance 2.0 Fast',
key: 'seedance',
provider: ['chatfire'],
type: 't2v+i2v',
ratios: ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'],
durs: [{ label: '5 秒', key: 5 }, { label: '10 秒', key: 10 }],
resolutions: ['480p', '720p', '1080p'],
defaultResolution: '1080p',
defaultParams: { ratio: '16:9', duration: 10, resolution: '1080p' }
},
// Seedance 模型 - 文生视频
{
label: 'Seedance 1.0 Lite (文生视频)',
key: 'doubao-seedance-1-0-lite-t2v-250428',
provider: ['chatfire'],
type: 't2v', // 文生视频
ratios: ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'],
durs: [{ label: '5 秒', key: 5 }, { label: '10 秒', key: 10 }],
resolutions: ['480p', '720p', '1080p'],
ratios: ['720x1280', '1280x720', '1024x1024', '960x1280'],
durs: [
{ label: '5 秒', key: 5 },
{ label: '8 秒', key: 8 },
{ label: '10 秒', key: 10 },
{ label: '12 秒', key: 12 },
{ label: '15 秒', key: 15 }
],
resolutions: ['720p'],
defaultResolution: '720p',
defaultParams: { ratio: '16:9', duration: 5, resolution: '720p' }
defaultParams: { ratio: '720x1280', duration: 10, resolution: '720p' }
},
// Seedance 模型 - 图生视频
{
label: 'Seedance 1.0 Lite (图生视频)',
key: 'doubao-seedance-1-0-lite-i2v-250428',
provider: ['chatfire'],
type: 'i2v', // 图生视频
ratios: ['16:9'],
durs: [{ label: '5 秒', key: 5 }, { label: '10 秒', key: 10 }],
resolutions: ['480p', '720p', '1080p'],
defaultResolution: '720p',
defaultParams: { ratio: '16:9', duration: 5, resolution: '720p' }
},
// Seedance 模型 - 图文视频 Pro
{
label: 'Seedance 1.0 Pro (图文视频)',
key: 'doubao-seedance-1-0-pro-250528',
provider: ['chatfire'],
type: 't2v+i2v', // 图文视频
ratios: ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9', '16:9'],
durs: [{ label: '5 秒', key: 5 }, { label: '10 秒', key: 10 }],
resolutions: ['480p', '720p', '1080p'],
defaultResolution: '1080p',
defaultParams: { ratio: '16:9', duration: 5, resolution: '1080p' }
},
// Seedance 模型 - 1.0 Pro Fast
{
label: 'Seedance 1.0 Pro Fast (图文视频)',
key: 'doubao-seedance-1-0-pro-fast-251015',
provider: ['chatfire'],
type: 't2v+i2v',
ratios: ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'],
durs: [{ label: '5 秒', key: 5 }, { label: '10 秒', key: 10 }],
resolutions: ['480p', '720p', '1080p'],
defaultResolution: '1080p',
defaultParams: { ratio: '16:9', duration: 5, resolution: '1080p' }
},
// 可灵 Kling
// {
// label: '可灵 Kling v2.5-turbo',
// key: 'kling-v2-1',
// provider: ['chatfire'], // 仅火宝渠道
// ratios: VIDEO_RATIO_LIST.map(s => s.key),
// durs: [{ label: '5 秒', key: 5 }, { label: '10 秒', key: 10 }],
// defaultParams: { ratio: '9:16', duration: 10 }
// },
// {
// label: 'runway/gen4-turbo',
// key: 'runway/gen4-turbo',
// ratios: VIDEO_RATIO_LIST.map(s => s.key),
// durs: [{ label: '5 秒', key: 5 }, { label: '10 秒', key: 10 }],
// defaultParams: { ratio: '16:9', duration: 5 }
// },
// {
// label: '可灵视频 O1',
// key: 'kling-video-o1',
// ratios: VIDEO_RATIO_LIST.map(s => s.key),
// durs: [{ label: '5 秒', key: 5 }, { label: '10 秒', key: 10 }],
// defaultParams: { ratio: '16:9', duration: 5 }
// },
// {
// label: 'viduq2-pro_720p', key: 'viduq2-pro_720p',
// ratios: VIDEO_RATIO_LIST.map(s => s.key),
// durs: [{ label: '5 秒', key: 5 }, { label: '10 秒', key: 10 }],
// defaultParams: { ratio: '16:9', duration: 5 }
// },
// {
// label: 'Sora 2', key: 'sora-2',
// ratios: VIDEO_RATIO_LIST.map(s => s.key),
// durs: [{ label: '5 秒', key: 5 }, { label: '10 秒', key: 10 }],
// defaultParams: { ratio: '16:9', duration: 5 }
// }
]
// Chat/LLM models | 对话模型
@@ -228,9 +118,10 @@ export const CHAT_MODELS = [
// Image size options | 图片尺寸选项
export const IMAGE_SIZE_OPTIONS = [
{ label: '2048x2048', key: '2048x2048' },
{ label: '1792x1024 (横版)', key: '1792x1024' },
{ label: '1024x1792 (竖版)', key: '1024x1792' }
{ label: '自动', key: 'auto' },
{ label: '竖图 2:3', key: '1024x1536' },
{ label: '方图 1:1', key: '1024x1024' },
{ label: '横图 3:2', key: '1536x1024' }
]
// Image quality options | 图片质量选项
@@ -251,16 +142,19 @@ export const VIDEO_RATIO_OPTIONS = VIDEO_RATIO_LIST
// Video duration options | 视频时长选项
export const VIDEO_DURATION_OPTIONS = [
{ label: '5 秒', key: 5 },
{ label: '10 秒', key: 10 }
{ label: '8 秒', key: 8 },
{ label: '10 秒', key: 10 },
{ label: '12 秒', key: 12 },
{ label: '15 秒', key: 15 }
]
// Default values | 默认值
export const DEFAULT_IMAGE_MODEL = 'nano-banana-pro'
export const DEFAULT_VIDEO_MODEL = 'doubao-seedance-1-5-pro-251215'
export const DEFAULT_IMAGE_MODEL = 'auto'
export const DEFAULT_VIDEO_MODEL = 'seedance'
export const DEFAULT_CHAT_MODEL = 'gpt-4o-mini'
export const DEFAULT_IMAGE_SIZE = '2048x2048'
export const DEFAULT_VIDEO_RATIO = '16:9'
export const DEFAULT_VIDEO_DURATION = 5
export const DEFAULT_IMAGE_SIZE = '1024x1536'
export const DEFAULT_VIDEO_RATIO = '720x1280'
export const DEFAULT_VIDEO_DURATION = 10
// Get model by key | 根据 key 获取模型
export const getModelByName = (key) => {

View File

@@ -81,8 +81,8 @@ export const WORKFLOW_TEMPLATES = [
position: { x: startPosition.x + nodeSpacing, y: startPosition.y + rowSpacing * 1.5 },
data: {
label: '主角色图',
model: 'doubao-seedream-4-5-251128',
size: '2048x2048'
model: 'auto',
size: '1024x1536'
}
})
@@ -145,8 +145,8 @@ export const WORKFLOW_TEMPLATES = [
position: { x: currentX, y: angleY },
data: {
label: `${angleConfig.label} (${angleConfig.english})`,
model: 'doubao-seedream-4-5-251128',
size: '2048x2048'
model: 'auto',
size: '1024x1536'
}
})
@@ -281,8 +281,8 @@ export const WORKFLOW_TEMPLATES = [
position: { x: startPosition.x + colSpacing * 2, y: startPosition.y },
data: {
label: '生成模特图',
model: 'doubao-seedream-4-5-251128',
size: '2048x2048'
model: 'auto',
size: '1024x1536'
}
})
@@ -294,8 +294,8 @@ export const WORKFLOW_TEMPLATES = [
position: { x: startPosition.x + colSpacing * 2, y: startPosition.y + rowSpacing },
data: {
label: '侧面展示图',
model: 'doubao-seedream-4-5-251128',
size: '2048x2048'
model: 'auto',
size: '1024x1536'
}
})
@@ -307,8 +307,8 @@ export const WORKFLOW_TEMPLATES = [
position: { x: startPosition.x + colSpacing * 2, y: startPosition.y + rowSpacing * 2 },
data: {
label: '俯瞰展示图',
model: 'doubao-seedream-4-5-251128',
size: '2048x2048'
model: 'auto',
size: '1024x1536'
}
})
@@ -320,8 +320,8 @@ export const WORKFLOW_TEMPLATES = [
position: { x: startPosition.x + colSpacing * 2, y: startPosition.y + rowSpacing * 3 },
data: {
label: '拆解图',
model: 'doubao-seedream-4-5-251128',
size: '2048x2048'
model: 'auto',
size: '1024x1536'
}
})
@@ -507,8 +507,8 @@ export const WORKFLOW_TEMPLATES = [
position: { x: startPosition.x + colSpacing * 2, y: startPosition.y },
data: {
label: '生成正面全身图',
model: 'doubao-seedream-4-5-251128',
size: '1440x2560'
model: 'auto',
size: '1024x1536'
}
})
@@ -569,8 +569,8 @@ export const WORKFLOW_TEMPLATES = [
position: { x: startPosition.x + colSpacing * 4 + 100, y: startPosition.y + rowSpacing },
data: {
label: '侧面半身图',
model: 'doubao-seedream-4-5-251128',
size: '2048x2048'
model: 'auto',
size: '1024x1536'
}
})
@@ -582,8 +582,8 @@ export const WORKFLOW_TEMPLATES = [
position: { x: startPosition.x + colSpacing * 4 + 100, y: startPosition.y + rowSpacing * 2 },
data: {
label: '表情特写图',
model: 'doubao-seedream-4-5-251128',
size: '2048x2048'
model: 'auto',
size: '1024x1536'
}
})
@@ -595,8 +595,8 @@ export const WORKFLOW_TEMPLATES = [
position: { x: startPosition.x + colSpacing * 4 + 100, y: startPosition.y + rowSpacing * 3 },
data: {
label: '背面全身图',
model: 'doubao-seedream-4-5-251128',
size: '1440x2560'
model: 'auto',
size: '1024x1536'
}
})
@@ -753,8 +753,8 @@ export const WORKFLOW_TEMPLATES = [
position: { x: startPosition.x + colSpacing * 2, y: startPosition.y },
data: {
label: '生成基础场景',
model: 'doubao-seedream-4-5-251128',
size: '2560x1440'
model: 'auto',
size: '1536x1024'
}
})
@@ -815,8 +815,8 @@ export const WORKFLOW_TEMPLATES = [
position: { x: startPosition.x + colSpacing * 4 + 100, y: startPosition.y + rowSpacing },
data: {
label: '傍晚场景',
model: 'doubao-seedream-4-5-251128',
size: '2560x1440'
model: 'auto',
size: '1536x1024'
}
})
@@ -828,8 +828,8 @@ export const WORKFLOW_TEMPLATES = [
position: { x: startPosition.x + colSpacing * 4 + 100, y: startPosition.y + rowSpacing * 2 },
data: {
label: '夜晚场景',
model: 'doubao-seedream-4-5-251128',
size: '2560x1440'
model: 'auto',
size: '1536x1024'
}
})
@@ -841,8 +841,8 @@ export const WORKFLOW_TEMPLATES = [
position: { x: startPosition.x + colSpacing * 4 + 100, y: startPosition.y + rowSpacing * 3 },
data: {
label: '雨天场景',
model: 'doubao-seedream-4-5-251128',
size: '2560x1440'
model: 'auto',
size: '1536x1024'
}
})
@@ -996,8 +996,8 @@ export const WORKFLOW_TEMPLATES = [
// position: { x: startPosition.x + colSpacing * 2, y: startPosition.y + rowSpacing },
// data: {
// label: '分镜画面',
// model: 'doubao-seedream-4-5-251128',
// size: '2560x1440'
// model: 'auto',
// size: '1536x1024'
// }
// })
@@ -1123,8 +1123,8 @@ export const WORKFLOW_TEMPLATES = [
position: { x: startPosition.x + colSpacing * 2, y: startPosition.y - rowSpacing },
data: {
label: '角色参考图',
model: 'doubao-seedream-4-5-251128',
size: '2048x2048'
model: 'auto',
size: '1024x1536'
}
})

View File

@@ -79,6 +79,11 @@ const setStored = (key, value) => {
}
}
const getValidStoredModel = (key, defaultValue, builtInModels) => {
const stored = getStored(key, defaultValue)
return builtInModels.some(model => model.key === stored) ? stored : defaultValue
}
// Shared reactive state (singleton pattern)
const customChatModels = ref(getStoredJson(STORAGE_KEYS.CUSTOM_CHAT_MODELS, []))
const customImageModels = ref(getStoredJson(STORAGE_KEYS.CUSTOM_IMAGE_MODELS, []))
@@ -90,8 +95,8 @@ const customImageModelsByProvider = ref(getStoredJson(STORAGE_KEYS.CUSTOM_IMAGE_
const customVideoModelsByProvider = ref(getStoredJson(STORAGE_KEYS.CUSTOM_VIDEO_MODELS_BY_PROVIDER || 'custom-video-models-by-provider', {}))
const selectedChatModel = ref(getStored(STORAGE_KEYS.SELECTED_CHAT_MODEL, DEFAULT_CHAT_MODEL))
const selectedImageModel = ref(getStored(STORAGE_KEYS.SELECTED_IMAGE_MODEL, DEFAULT_IMAGE_MODEL))
const selectedVideoModel = ref(getStored(STORAGE_KEYS.SELECTED_VIDEO_MODEL, DEFAULT_VIDEO_MODEL))
const selectedImageModel = ref(getValidStoredModel(STORAGE_KEYS.SELECTED_IMAGE_MODEL, DEFAULT_IMAGE_MODEL, IMAGE_MODELS))
const selectedVideoModel = ref(getValidStoredModel(STORAGE_KEYS.SELECTED_VIDEO_MODEL, DEFAULT_VIDEO_MODEL, VIDEO_MODELS))
/**
* Model Configuration Hook
@@ -117,47 +122,13 @@ export const useModelConfig = () => {
}))
])
const allImageModels = computed(() => [
...IMAGE_MODELS.map(m => ({ ...m, isCustom: false })),
...customImageModels.value.map(m => ({
label: m.label || m.key,
key: m.key,
isCustom: true,
sizes: [],
defaultParams: { quality: 'standard', style: 'vivid' }
})),
// 添加当前渠道的自定义模型
...(customImageModelsByProvider.value[currentProvider.value] || []).map(m => ({
label: m.label || m.key,
key: m.key,
isCustom: true,
sizes: [],
defaultParams: { quality: 'standard', style: 'vivid' },
provider: [currentProvider.value]
}))
])
const allImageModels = computed(() =>
IMAGE_MODELS.map(m => ({ ...m, isCustom: false }))
)
const allVideoModels = computed(() => [
...VIDEO_MODELS.map(m => ({ ...m, isCustom: false })),
...customVideoModels.value.map(m => ({
label: m.label || m.key,
key: m.key,
isCustom: true,
ratios: ['16x9', '9:16', '1:1'],
durs: [{ label: '5 秒', key: 5 }, { label: '10 秒', key: 10 }],
defaultParams: { ratio: '16:9', duration: 5 }
})),
// 添加当前渠道的自定义模型
...(customVideoModelsByProvider.value[currentProvider.value] || []).map(m => ({
label: m.label || m.key,
key: m.key,
isCustom: true,
ratios: ['16x9', '9:16', '1:1'],
durs: [{ label: '5 秒', key: 5 }, { label: '10 秒', key: 10 }],
defaultParams: { ratio: '16:9', duration: 5 },
provider: [currentProvider.value]
}))
])
const allVideoModels = computed(() =>
VIDEO_MODELS.map(m => ({ ...m, isCustom: false }))
)
// Available models filtered by provider | 根据渠道过滤的可用模型
const availableChatModels = computed(() =>
@@ -188,29 +159,12 @@ export const useModelConfig = () => {
provider: [provider]
}))
]
const image = [
...IMAGE_MODELS.filter(m => isModelSupported(m, provider)).map(m => ({ ...m, isCustom: false })),
...(customImageModelsByProvider.value[provider] || []).map(m => ({
label: m.label || m.key,
key: m.key,
isCustom: true,
sizes: [],
defaultParams: { quality: 'standard', style: 'vivid' },
provider: [provider]
}))
]
const video = [
...VIDEO_MODELS.filter(m => isModelSupported(m, provider)).map(m => ({ ...m, isCustom: false })),
...(customVideoModelsByProvider.value[provider] || []).map(m => ({
label: m.label || m.key,
key: m.key,
isCustom: true,
ratios: ['16x9', '9:16', '1:1'],
durs: [{ label: '5 秒', key: 5 }, { label: '10 秒', key: 10 }],
defaultParams: { ratio: '16:9', duration: 5 },
provider: [provider]
}))
]
const image = IMAGE_MODELS
.filter(m => isModelSupported(m, provider))
.map(m => ({ ...m, isCustom: false }))
const video = VIDEO_MODELS
.filter(m => isModelSupported(m, provider))
.map(m => ({ ...m, isCustom: false }))
return { chat, image, video }
}

View File

@@ -401,7 +401,7 @@ export const initSampleData = () => {
// Add image config node | 添加文生图配置节点
addNode('imageConfig', { x: 450, y: 150 }, {
prompt: '',
model: 'doubao-seedream-4-5-251128',
model: 'auto',
ratio: '16:9 | 4张 | 高清',
label: '文生图'
})

View File

@@ -88,6 +88,11 @@ const setStoredJson = (key, value) => {
}
}
const getValidStoredModel = (key, defaultValue, builtInModels) => {
const stored = getStored(key, defaultValue)
return builtInModels.some(model => model.key === stored) ? stored : defaultValue
}
/**
* 检查模型是否支持指定渠道
*/
@@ -160,8 +165,8 @@ export const useModelStore = defineStore('model', () => {
// 选中的模型
const selectedChatModel = ref(getStored(STORAGE_KEYS.SELECTED_CHAT_MODEL, DEFAULT_CHAT_MODEL))
const selectedImageModel = ref(getStored(STORAGE_KEYS.SELECTED_IMAGE_MODEL, DEFAULT_IMAGE_MODEL))
const selectedVideoModel = ref(getStored(STORAGE_KEYS.SELECTED_VIDEO_MODEL, DEFAULT_VIDEO_MODEL))
const selectedImageModel = ref(getValidStoredModel(STORAGE_KEYS.SELECTED_IMAGE_MODEL, DEFAULT_IMAGE_MODEL, IMAGE_MODELS))
const selectedVideoModel = ref(getValidStoredModel(STORAGE_KEYS.SELECTED_VIDEO_MODEL, DEFAULT_VIDEO_MODEL, VIDEO_MODELS))
// 按渠道存储的 API 配置
const apiKeysByProvider = ref(getStoredJson(STORAGE_KEYS.API_KEYS_BY_PROVIDER, {}))
@@ -205,47 +210,13 @@ export const useModelStore = defineStore('model', () => {
}))
])
const allImageModels = computed(() => [
...IMAGE_MODELS.map(m => ({ ...m, isCustom: false })),
...customImageModels.value.map(m => ({
label: m.label || m.key,
key: m.key,
isCustom: true,
sizes: [],
defaultParams: { quality: 'standard', style: 'vivid' }
})),
// 添加当前渠道的自定义模型
...(customImageModelsByProvider.value[currentProvider.value] || []).map(m => ({
label: m.label || m.key,
key: m.key,
isCustom: true,
sizes: [],
defaultParams: { quality: 'standard', style: 'vivid' },
provider: [currentProvider.value]
}))
])
const allImageModels = computed(() =>
IMAGE_MODELS.map(m => ({ ...m, isCustom: false }))
)
const allVideoModels = computed(() => [
...VIDEO_MODELS.map(m => ({ ...m, isCustom: false })),
...customVideoModels.value.map(m => ({
label: m.label || m.key,
key: m.key,
isCustom: true,
ratios: ['16x9', '9:16', '1:1'],
durs: [{ label: '5 秒', key: 5 }, { label: '10 秒', key: 10 }],
defaultParams: { ratio: '16:9', duration: 5 }
})),
// 添加当前渠道的自定义模型
...(customVideoModelsByProvider.value[currentProvider.value] || []).map(m => ({
label: m.label || m.key,
key: m.key,
isCustom: true,
ratios: ['16x9', '9:16', '1:1'],
durs: [{ label: '5 秒', key: 5 }, { label: '10 秒', key: 10 }],
defaultParams: { ratio: '16:9', duration: 5 },
provider: [currentProvider.value]
}))
])
const allVideoModels = computed(() =>
VIDEO_MODELS.map(m => ({ ...m, isCustom: false }))
)
// ============ Computed: Available Models (filtered by provider) ============
@@ -412,29 +383,12 @@ export const useModelStore = defineStore('model', () => {
provider: [provider]
}))
]
const image = [
...IMAGE_MODELS.filter(m => isModelSupported(m, provider)).map(m => ({ ...m, isCustom: false })),
...(customImageModelsByProvider.value[provider] || []).map(m => ({
label: m.label || m.key,
key: m.key,
isCustom: true,
sizes: [],
defaultParams: { quality: 'standard', style: 'vivid' },
provider: [provider]
}))
]
const video = [
...VIDEO_MODELS.filter(m => isModelSupported(m, provider)).map(m => ({ ...m, isCustom: false })),
...(customVideoModelsByProvider.value[provider] || []).map(m => ({
label: m.label || m.key,
key: m.key,
isCustom: true,
ratios: ['16x9', '9:16', '1:1'],
durs: [{ label: '5 秒', key: 5 }, { label: '10 秒', key: 10 }],
defaultParams: { ratio: '16:9', duration: 5 },
provider: [provider]
}))
]
const image = IMAGE_MODELS
.filter(m => isModelSupported(m, provider))
.map(m => ({ ...m, isCustom: false }))
const video = VIDEO_MODELS
.filter(m => isModelSupported(m, provider))
.map(m => ({ ...m, isCustom: false }))
return { chat, image, video }
}

View File

@@ -345,8 +345,8 @@ export const initProjectsStore = () => {
position: { x: 500, y: 150 },
data: {
prompt: '',
model: 'doubao-seedream-4-5-251128',
size: '512x512',
model: 'auto',
size: '1024x1024',
label: '文生图'
}
}