auto-save 2026-05-27 17:24 (~4)
This commit is contained in:
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -624,9 +624,10 @@
|
||||
<p><strong>2026-05-26 我的工作流云端版:</strong>工作流面板从只有公共模板扩展为“公共工作流 / 我的工作流”两类。当前画布可以保存成当前登录用户自己的云端工作流模板,后续在同一账号的其他电脑或浏览器打开后可插回画布;保存时只沉淀节点结构、连线、配置和提示词,主动清掉已生成图片、视频、任务进度、错误和运行态字段,避免把一次性生成结果误当模板复用。</p>
|
||||
<p><strong>2026-05-27 画布点击响应优化:</strong>大画布启用 Vue Flow 可见节点渲染,并在载入旧项目时补齐节点尺寸,让几百个节点的项目不再一次性渲染全部节点;新增节点、批量插入节点/边和复制节点改为原地追加,避免单次点击触发整条节点数组重复替换。大项目右下角 MiniMap 只在 120 个节点以内显示,优先保证编辑响应速度。</p>
|
||||
<p><strong>2026-05-27 上传参考图持久化:</strong>画布图片节点上传本地文件时先写入后端 creative job,再把 <code>/api/jobs/...</code> 资产 URL 保存到节点和服务端画布项目;不再把浏览器 <code>data:</code> base64 当作图片地址保存。项目自动保存增加内容签名去重和 2 秒防抖,减少连续点击或节点测量触发的重复 <code>PUT /canvas-projects</code>。</p>
|
||||
<p><strong>2026-05-27 图片模型配置化:</strong>图片生成不再把主模型写死为 <code>gpt-image-2</code>。后端通过 <code>IMAGE_MODEL</code>、<code>IMAGE_FALLBACK_MODELS</code>、<code>IMAGE_EXTRA_MODELS</code>、<code>IMAGE_MODEL_CONFIGS_JSON</code> 和 Ark 专用 <code>ARK_IMAGE_BASE_URL</code>/<code>ARK_IMAGE_API_KEY</code>/<code>ARK_SEEDREAM_IMAGE_MODEL</code> 注册模型;默认仍保持 GPT Image 2 + Gemini 兜底,新增可选 <code>doubao-seedream-4-5-251128</code>,Seedream 走 <code>/images/generations</code> + <code>reference_images</code> 并使用 2K/4K 尺寸。</p>
|
||||
<p><strong>2026-05-26 生图配置恢复版:</strong>按用户要求撤回后续“低/中/高画质、自定义尺寸、Gemini 官方 1K/2K/4K 尺寸、取消自动模型”的实验改动,恢复最初简单配置:图片模型为 <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>auto</code> 仍按后端既有策略优先 GPT Image 2,必要时由熔断/兜底走 Gemini。</p>
|
||||
</div>
|
||||
<p>当前默认业务管线是“个人隔离任务 → 根域名进入个人画布 → 画布项目同步到服务端 Postgres → 用提示词、推荐词、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>,AI 润色和通用 LLM 文本生成走 <code>/prompt/polish</code> 并保持中性专业:不主动套入 SKG,不主动补产品、平台、广告语境或人物,只扩写用户明确写出的主体、动作、场景、镜头、光线和质量细节;视频提交若带参考图,会在最终提示词中条件声明“参考图里若有人物,应按 AI 生成的虚拟角色处理”,避免把 AI 人像素材误当成真实肖像。生成资产按当前登录用户写入个人 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>、画布项目 owner、我的工作流 owner 和飞书 / 备用登录会话隔离。旧 React 单对话框首页、信息流复刻链路仍保留在源码里作为回滚/高级能力,但不作为生产默认入口。</p>
|
||||
<p>当前默认业务管线是“个人隔离任务 → 根域名进入个人画布 → 画布项目同步到服务端 Postgres → 用提示词、推荐词、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>,AI 润色和通用 LLM 文本生成走 <code>/prompt/polish</code> 并保持中性专业:不主动套入 SKG,不主动补产品、平台、广告语境或人物,只扩写用户明确写出的主体、动作、场景、镜头、光线和质量细节;视频提交若带参考图,会在最终提示词中条件声明“参考图里若有人物,应按 AI 生成的虚拟角色处理”,避免把 AI 人像素材误当成真实肖像。生成资产按当前登录用户写入个人 job。图片模型由后端运行时配置决定,默认 <code>auto</code> 仍优先当前 <code>IMAGE_MODEL</code> 并按 <code>IMAGE_FALLBACK_MODELS</code> 兜底;前端同时提供 GPT/Gemini 旧尺寸和 Seedream 2K/4K 尺寸。视频画幅只显示 <code>720x1280</code>、<code>1280x720</code>、<code>1024x1024</code>、<code>960x1280</code>;视频时长只显示 <code>5/8/10/12/15</code> 秒。多人互不影响依赖后端 <code>owner_id</code>、画布项目 owner、我的工作流 owner 和飞书 / 备用登录会话隔离。旧 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>用户直接在根域名个人画布里操作;项目列表优先读取服务端 <code>/canvas-projects</code>,本地旧项目会首次导入。</p></div>
|
||||
@@ -1307,6 +1308,19 @@ ProductRefStateItem {
|
||||
<h2>变更记录</h2>
|
||||
<p>这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。</p>
|
||||
<div class="changelog">
|
||||
<article class="change">
|
||||
<header>
|
||||
<h3>2026-05-27 · 图片 API 改为运行时可配置并接入 Ark Seedream</h3>
|
||||
<span class="tag blue">API</span>
|
||||
<span class="tag violet">Model</span>
|
||||
<span class="tag green">Canvas</span>
|
||||
</header>
|
||||
<div class="body">
|
||||
<p><strong>问题:</strong>生图链路把主模型写死为 <code>gpt-image-2</code>,新增火山方舟 <code>doubao-seedream-4-5-251128</code> 时不能只靠 env 切换;同时 Seedream 4.5 的尺寸要求和图生图请求格式不同于现有 OpenAI-compatible <code>/images/edits</code> 路径。</p>
|
||||
<p><strong>改动:</strong><code>api/main.py</code> 新增图片模型运行时注册:<code>IMAGE_MODEL</code> 决定主模型,<code>IMAGE_FALLBACK_MODELS</code> 支持多备用模型,<code>IMAGE_EXTRA_MODELS</code> 和 <code>IMAGE_MODEL_CONFIGS_JSON</code> 支持后续扩展;Ark Seedream 使用 <code>ARK_IMAGE_BASE_URL</code>、<code>ARK_IMAGE_API_KEY</code>、<code>ARK_SEEDREAM_IMAGE_MODEL</code> 独立配置。普通模型继续走 <code>/images/generations</code> / <code>/images/edits</code>,Seedream 图生图改走 <code>/images/generations</code> + <code>reference_images</code>。</p>
|
||||
<p><strong>影响:</strong><code>web/canvas-app/src/config/models.js</code> 增加 <code>Seedream 4.5</code> 和 <code>2K/2048/1440x2560/2560x1440/4K</code> 尺寸;<code>/health</code> 返回每个图片模型的 provider、base URL、配置状态和尺寸能力。真实 Ark key 只应写入本地或 VPS 的 gitignored env 文件,不能提交到仓库。</p>
|
||||
</div>
|
||||
</article>
|
||||
<article class="change">
|
||||
<header>
|
||||
<h3>2026-05-27 · 修复上传参考图刷新后丢失并降低保存频次</h3>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -319,6 +319,7 @@ const isApiConfigured = computed(() => !!modelStore.currentApiKey)
|
||||
// Initialize models on page load | 页面加载时初始化模型
|
||||
onMounted(() => {
|
||||
loadAllModels()
|
||||
modelStore.loadRuntimeModels()
|
||||
})
|
||||
|
||||
// Chat templates | 问答模板
|
||||
|
||||
Reference in New Issue
Block a user