diff --git a/.memory/worklog.json b/.memory/worklog.json
index da1df88..64b492e 100644
--- a/.memory/worklog.json
+++ b/.memory/worklog.json
@@ -1,12 +1,5 @@
{
"entries": [
- {
- "files_changed": 3,
- "hash": "5e0afce",
- "message": "auto-save 2026-05-20 19:44 (~3)",
- "ts": "2026-05-20T19:44:10+08:00",
- "type": "commit"
- },
{
"files_changed": 2,
"message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 2 项未提交变更 · 最近提交:auto-save 2026-05-20 19:44 (~3)",
@@ -3196,6 +3189,13 @@
"message": "auto-save 2026-05-27 17:13 (~2)",
"hash": "8999fe0",
"files_changed": 2
+ },
+ {
+ "ts": "2026-05-27T17:18:45+08:00",
+ "type": "commit",
+ "message": "auto-save 2026-05-27 17:18 (~9)",
+ "hash": "9ab5417",
+ "files_changed": 9
}
]
}
diff --git a/docs/source-analysis.html b/docs/source-analysis.html
index f1439a9..72841ea 100644
--- a/docs/source-analysis.html
+++ b/docs/source-analysis.html
@@ -624,9 +624,10 @@
2026-05-26 我的工作流云端版:工作流面板从只有公共模板扩展为“公共工作流 / 我的工作流”两类。当前画布可以保存成当前登录用户自己的云端工作流模板,后续在同一账号的其他电脑或浏览器打开后可插回画布;保存时只沉淀节点结构、连线、配置和提示词,主动清掉已生成图片、视频、任务进度、错误和运行态字段,避免把一次性生成结果误当模板复用。
2026-05-27 画布点击响应优化:大画布启用 Vue Flow 可见节点渲染,并在载入旧项目时补齐节点尺寸,让几百个节点的项目不再一次性渲染全部节点;新增节点、批量插入节点/边和复制节点改为原地追加,避免单次点击触发整条节点数组重复替换。大项目右下角 MiniMap 只在 120 个节点以内显示,优先保证编辑响应速度。
2026-05-27 上传参考图持久化:画布图片节点上传本地文件时先写入后端 creative job,再把 /api/jobs/... 资产 URL 保存到节点和服务端画布项目;不再把浏览器 data: base64 当作图片地址保存。项目自动保存增加内容签名去重和 2 秒防抖,减少连续点击或节点测量触发的重复 PUT /canvas-projects。
+ 2026-05-27 图片模型配置化:图片生成不再把主模型写死为 gpt-image-2。后端通过 IMAGE_MODEL、IMAGE_FALLBACK_MODELS、IMAGE_EXTRA_MODELS、IMAGE_MODEL_CONFIGS_JSON 和 Ark 专用 ARK_IMAGE_BASE_URL/ARK_IMAGE_API_KEY/ARK_SEEDREAM_IMAGE_MODEL 注册模型;默认仍保持 GPT Image 2 + Gemini 兜底,新增可选 doubao-seedream-4-5-251128,Seedream 走 /images/generations + reference_images 并使用 2K/4K 尺寸。
2026-05-26 生图配置恢复版:按用户要求撤回后续“低/中/高画质、自定义尺寸、Gemini 官方 1K/2K/4K 尺寸、取消自动模型”的实验改动,恢复最初简单配置:图片模型为 auto、gpt-image-2、gemini-3-pro-image-preview,尺寸只保留 auto、1024x1536、1024x1024、1536x1024,画质回到单一标准项;auto 仍按后端既有策略优先 GPT Image 2,必要时由熔断/兜底走 Gemini。
- 当前默认业务管线是“个人隔离任务 → 根域名进入个人画布 → 画布项目同步到服务端 Postgres → 用提示词、推荐词、AI 润色、公共工作流或我的工作流创建节点 → 画布自动执行或手动连接图片/视频/文本节点 → 生成结果沉淀在当前个人画布 → 可把当前节点结构保存为我的工作流 → 需要时进入详情页继续编辑”。画布不再被削成三模式入口;首帧、尾帧、参考图、图生视频、多角度分镜、故事板和绘本等上游概念按节点能力保留。底层生成仍由 web/canvas-app/src/hooks/useApi.js 适配到本项目 /creative/jobs/image、/jobs/{id}/frames/{idx}/generate、/jobs/{id}/frames/{idx}/storyboard/video,AI 润色和通用 LLM 文本生成走 /prompt/polish 并保持中性专业:不主动套入 SKG,不主动补产品、平台、广告语境或人物,只扩写用户明确写出的主体、动作、场景、镜头、光线和质量细节;视频提交若带参考图,会在最终提示词中条件声明“参考图里若有人物,应按 AI 生成的虚拟角色处理”,避免把 AI 人像素材误当成真实肖像。生成资产按当前登录用户写入个人 job。图片尺寸只显示 auto、1024x1536、1024x1024、1536x1024;视频画幅只显示 720x1280、1280x720、1024x1024、960x1280;视频时长只显示 5/8/10/12/15 秒。多人互不影响依赖后端 owner_id、画布项目 owner、我的工作流 owner 和飞书 / 备用登录会话隔离。旧 React 单对话框首页、信息流复刻链路仍保留在源码里作为回滚/高级能力,但不作为生产默认入口。
+ 当前默认业务管线是“个人隔离任务 → 根域名进入个人画布 → 画布项目同步到服务端 Postgres → 用提示词、推荐词、AI 润色、公共工作流或我的工作流创建节点 → 画布自动执行或手动连接图片/视频/文本节点 → 生成结果沉淀在当前个人画布 → 可把当前节点结构保存为我的工作流 → 需要时进入详情页继续编辑”。画布不再被削成三模式入口;首帧、尾帧、参考图、图生视频、多角度分镜、故事板和绘本等上游概念按节点能力保留。底层生成仍由 web/canvas-app/src/hooks/useApi.js 适配到本项目 /creative/jobs/image、/jobs/{id}/frames/{idx}/generate、/jobs/{id}/frames/{idx}/storyboard/video,AI 润色和通用 LLM 文本生成走 /prompt/polish 并保持中性专业:不主动套入 SKG,不主动补产品、平台、广告语境或人物,只扩写用户明确写出的主体、动作、场景、镜头、光线和质量细节;视频提交若带参考图,会在最终提示词中条件声明“参考图里若有人物,应按 AI 生成的虚拟角色处理”,避免把 AI 人像素材误当成真实肖像。生成资产按当前登录用户写入个人 job。图片模型由后端运行时配置决定,默认 auto 仍优先当前 IMAGE_MODEL 并按 IMAGE_FALLBACK_MODELS 兜底;前端同时提供 GPT/Gemini 旧尺寸和 Seedream 2K/4K 尺寸。视频画幅只显示 720x1280、1280x720、1024x1024、960x1280;视频时长只显示 5/8/10/12/15 秒。多人互不影响依赖后端 owner_id、画布项目 owner、我的工作流 owner 和飞书 / 备用登录会话隔离。旧 React 单对话框首页、信息流复刻链路仍保留在源码里作为回滚/高级能力,但不作为生产默认入口。
01
个人任务
GET /jobs 按当前登录用户过滤;旧无 owner 任务只对备用账号可见。
02
进入画布
用户直接在根域名个人画布里操作;项目列表优先读取服务端 /canvas-projects,本地旧项目会首次导入。
@@ -1307,6 +1308,19 @@ ProductRefStateItem {
变更记录
这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。
+
+
+ 2026-05-27 · 图片 API 改为运行时可配置并接入 Ark Seedream
+ API
+ Model
+ Canvas
+
+
+
问题:生图链路把主模型写死为 gpt-image-2,新增火山方舟 doubao-seedream-4-5-251128 时不能只靠 env 切换;同时 Seedream 4.5 的尺寸要求和图生图请求格式不同于现有 OpenAI-compatible /images/edits 路径。
+
改动:api/main.py 新增图片模型运行时注册:IMAGE_MODEL 决定主模型,IMAGE_FALLBACK_MODELS 支持多备用模型,IMAGE_EXTRA_MODELS 和 IMAGE_MODEL_CONFIGS_JSON 支持后续扩展;Ark Seedream 使用 ARK_IMAGE_BASE_URL、ARK_IMAGE_API_KEY、ARK_SEEDREAM_IMAGE_MODEL 独立配置。普通模型继续走 /images/generations / /images/edits,Seedream 图生图改走 /images/generations + reference_images。
+
影响:web/canvas-app/src/config/models.js 增加 Seedream 4.5 和 2K/2048/1440x2560/2560x1440/4K 尺寸;/health 返回每个图片模型的 provider、base URL、配置状态和尺寸能力。真实 Ark key 只应写入本地或 VPS 的 gitignored env 文件,不能提交到仓库。
+
+
2026-05-27 · 修复上传参考图刷新后丢失并降低保存频次
diff --git a/web/canvas-app/src/stores/pinia/models.js b/web/canvas-app/src/stores/pinia/models.js
index ef1a2bc..cbdb60d 100644
--- a/web/canvas-app/src/stores/pinia/models.js
+++ b/web/canvas-app/src/stores/pinia/models.js
@@ -103,6 +103,49 @@ const isModelSupported = (model, provider) => {
return model.provider.includes(provider)
}
+const normalizeRuntimeSizeOptions = (items = []) => {
+ if (!Array.isArray(items)) return []
+ return items
+ .map(item => {
+ const key = item?.value || item?.key || item?.id
+ if (!key) return null
+ return {
+ label: item.label || key,
+ key
+ }
+ })
+ .filter(Boolean)
+}
+
+const normalizeRuntimeImageModel = (item) => {
+ const key = item?.id || item?.model
+ if (!key) return null
+ const sizeOptions = normalizeRuntimeSizeOptions(item.size_options)
+ return {
+ label: item.label || item.model || key,
+ key,
+ provider: ['chatfire'],
+ sizes: sizeOptions.map(option => option.key),
+ sizeOptions,
+ qualities: [{ label: '标准', key: 'standard' }],
+ defaultParams: {
+ size: item.default_size || sizeOptions[0]?.key || 'auto',
+ quality: 'standard',
+ style: item.provider === 'ark_seedream' ? 'commercial' : 'vivid'
+ },
+ available: item.available !== false,
+ providerName: item.provider,
+ isRuntime: true
+ }
+}
+
+const mergeModels = (builtInModels, runtimeModels) => {
+ const byKey = new Map()
+ builtInModels.forEach(model => byKey.set(model.key, { ...model, isCustom: false }))
+ runtimeModels.forEach(model => byKey.set(model.key, { ...byKey.get(model.key), ...model, isCustom: false }))
+ return Array.from(byKey.values())
+}
+
export const useModelStore = defineStore('model', () => {
// ============ Provider 状态 | Provider State ============
@@ -162,6 +205,7 @@ export const useModelStore = defineStore('model', () => {
const customChatModelsByProvider = ref(getStoredJson(STORAGE_KEYS.CUSTOM_CHAT_MODELS_BY_PROVIDER, {}))
const customImageModelsByProvider = ref(getStoredJson(STORAGE_KEYS.CUSTOM_IMAGE_MODELS_BY_PROVIDER, {}))
const customVideoModelsByProvider = ref(getStoredJson(STORAGE_KEYS.CUSTOM_VIDEO_MODELS_BY_PROVIDER, {}))
+ const runtimeImageModels = ref([])
// 选中的模型
const selectedChatModel = ref(getStored(STORAGE_KEYS.SELECTED_CHAT_MODEL, DEFAULT_CHAT_MODEL))
@@ -211,7 +255,7 @@ export const useModelStore = defineStore('model', () => {
])
const allImageModels = computed(() =>
- IMAGE_MODELS.map(m => ({ ...m, isCustom: false }))
+ mergeModels(IMAGE_MODELS, runtimeImageModels.value)
)
const allVideoModels = computed(() =>
@@ -226,7 +270,7 @@ export const useModelStore = defineStore('model', () => {
)
const availableImageModels = computed(() =>
- allImageModels.value.filter(m => isModelSupported(m, currentProvider.value))
+ allImageModels.value.filter(m => isModelSupported(m, currentProvider.value) && m.available !== false)
)
const availableVideoModels = computed(() =>
@@ -239,7 +283,8 @@ export const useModelStore = defineStore('model', () => {
const allImageModelOptions = computed(() =>
allImageModels.value.map(m => ({
label: m.label,
- key: m.key
+ key: m.key,
+ disabled: m.available === false
}))
)
@@ -263,7 +308,8 @@ export const useModelStore = defineStore('model', () => {
const imageModelOptions = computed(() =>
availableImageModels.value.map(m => ({
label: m.label,
- key: m.key
+ key: m.key,
+ disabled: m.available === false
}))
)
@@ -343,6 +389,23 @@ export const useModelStore = defineStore('model', () => {
const getImageModel = (key) => allImageModels.value.find(m => m.key === key)
const getVideoModel = (key) => allVideoModels.value.find(m => m.key === key)
+ const loadRuntimeModels = async () => {
+ try {
+ const response = await fetch('/api/health', { credentials: 'include' })
+ if (!response.ok) return false
+ const data = await response.json()
+ const imageOptions = data?.models?.image_options || []
+ runtimeImageModels.value = imageOptions
+ .filter(item => item?.id && item.id !== 'auto')
+ .map(normalizeRuntimeImageModel)
+ .filter(Boolean)
+ return true
+ } catch (err) {
+ console.warn('[model store] runtime model load failed', err)
+ return false
+ }
+ }
+
// ============ Methods: Get API Endpoints ============
// 获取图片端点
@@ -383,7 +446,7 @@ export const useModelStore = defineStore('model', () => {
provider: [provider]
}))
]
- const image = IMAGE_MODELS
+ const image = allImageModels.value
.filter(m => isModelSupported(m, provider))
.map(m => ({ ...m, isCustom: false }))
const video = VIDEO_MODELS
@@ -500,6 +563,7 @@ export const useModelStore = defineStore('model', () => {
allChatModels,
allImageModels,
allVideoModels,
+ runtimeImageModels,
// Available models filtered by provider
availableChatModels,
@@ -551,6 +615,7 @@ export const useModelStore = defineStore('model', () => {
getChatModel,
getImageModel,
getVideoModel,
+ loadRuntimeModels,
// Get API endpoints
getImageEndpoint,
diff --git a/web/canvas-app/src/views/Canvas.vue b/web/canvas-app/src/views/Canvas.vue
index f960004..f41aea2 100644
--- a/web/canvas-app/src/views/Canvas.vue
+++ b/web/canvas-app/src/views/Canvas.vue
@@ -319,6 +319,7 @@ const isApiConfigured = computed(() => !!modelStore.currentApiKey)
// Initialize models on page load | 页面加载时初始化模型
onMounted(() => {
loadAllModels()
+ modelStore.loadRuntimeModels()
})
// Chat templates | 问答模板