From fb939b8fcf53ed8e2d34cbc4cd26fd5012ec7ab1 Mon Sep 17 00:00:00 2001 From: kang Date: Wed, 27 May 2026 17:24:16 +0800 Subject: [PATCH] auto-save 2026-05-27 17:24 (~4) --- .memory/worklog.json | 14 ++--- docs/source-analysis.html | 16 ++++- web/canvas-app/src/stores/pinia/models.js | 75 +++++++++++++++++++++-- web/canvas-app/src/views/Canvas.vue | 1 + 4 files changed, 93 insertions(+), 13 deletions(-) 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_MODELIMAGE_FALLBACK_MODELSIMAGE_EXTRA_MODELSIMAGE_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 尺寸、取消自动模型”的实验改动,恢复最初简单配置:图片模型为 autogpt-image-2gemini-3-pro-image-preview,尺寸只保留 auto1024x15361024x10241536x1024,画质回到单一标准项;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。图片尺寸只显示 auto1024x15361024x10241536x1024;视频画幅只显示 720x12801280x7201024x1024960x1280;视频时长只显示 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 尺寸。视频画幅只显示 720x12801280x7201024x1024960x1280;视频时长只显示 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_MODELSIMAGE_MODEL_CONFIGS_JSON 支持后续扩展;Ark Seedream 使用 ARK_IMAGE_BASE_URLARK_IMAGE_API_KEYARK_SEEDREAM_IMAGE_MODEL 独立配置。普通模型继续走 /images/generations / /images/edits,Seedream 图生图改走 /images/generations + reference_images

+

影响:web/canvas-app/src/config/models.js 增加 Seedream 4.52K/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 | 问答模板