From 33d93d789ebe71cdbbf9f37b634acd1aacb386bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Sat, 18 Jul 2026 09:56:18 +0800 Subject: [PATCH 01/67] =?UTF-8?q?feat:=20AI=20Agent=20=E4=BE=A7=E8=BE=B9?= =?UTF-8?q?=E8=81=8A=E5=A4=A9=E9=9D=A2=E6=9D=BF=20+=20API=20=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E7=95=8C=E9=9D=A2=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 AI Agent 侧边聊天面板(smart-canvas.html/js/css) - 支持上传 .md skill 文档引导生图风格 - 支持上传参考图片作为角色/风格附件 - 理解模型与生图模型独立选择 - 出图参数:比例/分辨率/数量 - 多轮对话上下文,自动携带最近生成的图 - localStorage 持久化对话状态 - API 设置界面优化(api-settings.html/js) - 推荐 API 快捷配置(APIMART/RunningHub/Agnes AI 等) - 模型拉取与分类选择 - 协议与图片模式选择 - 添加 .gitignore 排除敏感文件和运行时数据 --- .gitignore | 11 + API/.env | 0 main.py | 8 + static/api-settings.html | 14 +- static/css/smart-canvas.css | 60 ++++ static/index.html | 28 +- static/js/api-settings.js | 38 ++- static/js/i18n/smart-canvas.js | 29 +- static/js/smart-canvas.js | 508 ++++++++++++++++++++++++++++++++- static/online.html | 16 +- static/smart-canvas.html | 86 +++++- 11 files changed, 738 insertions(+), 60 deletions(-) create mode 100644 .gitignore delete mode 100644 API/.env mode change 100644 => 100755 main.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..45ad51396 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# 敏感信息 +API/.env + +# 运行时数据 +assets/ +data/ + +# 系统文件 +.DS_Store +__pycache__/ +*.pyc diff --git a/API/.env b/API/.env deleted file mode 100644 index e69de29bb..000000000 diff --git a/main.py b/main.py old mode 100644 new mode 100755 index b1dc49e51..343573afb --- a/main.py +++ b/main.py @@ -75,6 +75,14 @@ def filter(self, record): allow_headers=["*"], ) +@app.middleware("http") +async def no_cache_html_middleware(request: Request, call_next): + response = await call_next(request) + path = request.url.path.lower() + if path.endswith(".html"): + response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate" + return response + # --- WebSocket 状态管理器 --- class ConnectionManager: def __init__(self): diff --git a/static/api-settings.html b/static/api-settings.html index 24cffcd9f..c9b08b7c9 100644 --- a/static/api-settings.html +++ b/static/api-settings.html @@ -16,12 +16,12 @@ } catch(e) {} })(); - - - - - - + + + + + +
@@ -565,6 +565,6 @@
- + diff --git a/static/css/smart-canvas.css b/static/css/smart-canvas.css index 6cc1ef466..1ec36968f 100644 --- a/static/css/smart-canvas.css +++ b/static/css/smart-canvas.css @@ -1154,6 +1154,66 @@ input[type=range].smart-range::-webkit-slider-thumb { -webkit-appearance:none; w .theme-dark .asset-grid { scrollbar-color:rgba(148,163,184,.34) transparent; } .theme-dark .asset-grid::-webkit-scrollbar-thumb { background:rgba(148,163,184,.32); background-clip:content-box; } .theme-dark .asset-grid::-webkit-scrollbar-thumb:hover { background:rgba(203,213,225,.54); background-clip:content-box; } + +/* --- AI Agent 面板 --- */ +.agent-toggle { position:absolute; right:454px; top:22px; z-index:56; height:40px; padding:0 16px; border-radius:999px; background:var(--panel); color:var(--text); border:1px solid var(--line); box-shadow:0 14px 34px var(--shadow); backdrop-filter:blur(16px); display:flex; align-items:center; justify-content:center; gap:8px; font-size:12px; font-weight:750; white-space:nowrap; transition:transform .14s ease, border-color .14s ease; } +.agent-toggle:hover { border-color:var(--text); transform:translateY(-1px); } +.agent-toggle.active { background:var(--strong); color:var(--strong-text); border-color:var(--strong); box-shadow:0 16px 38px var(--shadow); } +.agent-toggle i,.agent-toggle svg { width:16px; height:16px; } +.agent-panel { position:absolute; right:22px; top:66px; bottom:168px; z-index:55; width:360px; max-width:calc(100vw - 44px); min-height:0; display:flex; flex-direction:column; gap:10px; padding:12px; border-radius:18px; background:var(--panel); border:1px solid var(--line); box-shadow:0 22px 58px var(--shadow); backdrop-filter:blur(20px); overflow:hidden; transform:translateX(16px); opacity:0; visibility:hidden; pointer-events:none; transition:opacity .16s ease, transform .16s ease, visibility .16s ease; } +.agent-panel.open { opacity:1; visibility:visible; pointer-events:auto; transform:translateX(0); } +.agent-skill-zone { display:flex; flex-direction:column; gap:6px; } +.agent-skill-drop { min-height:44px; border:1px dashed var(--line); border-radius:13px; display:flex; align-items:center; justify-content:center; gap:7px; text-align:center; padding:8px; color:var(--faint); font-size:10.5px; font-weight:600; background:var(--soft); cursor:pointer; transition:border-color .14s ease, color .14s ease, background .14s ease; } +.agent-skill-drop:hover { border-color:var(--strong); color:var(--text); } +.agent-skill-drop.drag-over { border-color:var(--strong); color:var(--text); background:var(--card); } +.agent-skill-drop i,.agent-skill-drop svg { width:14px; height:14px; } +.agent-skill-card { display:flex; align-items:center; gap:8px; padding:8px 10px; border-radius:12px; border:1px solid var(--line); background:var(--card); } +.agent-skill-card > i,.agent-skill-card > svg { width:15px; height:15px; color:var(--strong); flex:0 0 auto; } +.agent-skill-meta { flex:1; min-width:0; } +.agent-skill-name { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:11px; font-weight:750; color:var(--text); } +.agent-skill-size { font-size:9.5px; font-weight:600; color:var(--faint); } +.agent-models { display:flex; flex-direction:column; gap:6px; padding:9px; border-radius:13px; background:var(--soft); border:1px solid var(--line); } +.agent-model-row { display:flex; align-items:center; gap:5px; } +.agent-model-label { flex:0 0 52px; font-size:10px; font-weight:750; color:var(--muted); } +.agent-select { min-width:0; flex:1; height:28px; border-radius:9px; border:1px solid var(--line); background:var(--card); color:var(--text); padding:0 6px; font-size:10.5px; outline:none; } +.agent-select:focus { border-color:var(--strong); } +.agent-autocontext { display:flex; align-items:center; gap:6px; font-size:10px; font-weight:650; color:var(--muted); cursor:pointer; user-select:none; } +.agent-autocontext input { accent-color:var(--strong); } +.agent-messages { min-height:0; flex:1 1 auto; overflow-x:hidden; overflow-y:auto; overscroll-behavior:contain; display:flex; flex-direction:column; gap:10px; padding:4px 4px 8px 2px; scrollbar-width:thin; scrollbar-color:rgba(148,163,184,.42) transparent; } +.agent-messages::-webkit-scrollbar { width:7px; } +.agent-messages::-webkit-scrollbar-thumb { background:rgba(148,163,184,.42); border-radius:999px; border:2px solid transparent; background-clip:content-box; } +.agent-empty { margin:auto; text-align:center; color:var(--faint); font-size:11px; line-height:1.7; padding:20px 12px; } +.agent-empty i,.agent-empty svg { width:26px; height:26px; display:block; margin:0 auto 8px; } +.agent-msg { display:flex; flex-direction:column; gap:5px; max-width:88%; } +.agent-msg.user { align-self:flex-end; align-items:flex-end; } +.agent-msg.assistant { align-self:flex-start; align-items:flex-start; } +.agent-msg-bubble { padding:8px 11px; border-radius:14px; font-size:11.5px; line-height:1.55; white-space:pre-wrap; word-break:break-word; } +.agent-msg.user .agent-msg-bubble { background:var(--strong); color:var(--strong-text); border-bottom-right-radius:5px; } +.agent-msg.assistant .agent-msg-bubble { background:var(--card); color:var(--text); border:1px solid var(--line); border-bottom-left-radius:5px; } +.agent-msg-thumbs { display:grid; grid-template-columns:repeat(3, 1fr); gap:5px; max-width:240px; } +.agent-msg-thumbs img { width:100%; aspect-ratio:1/1; object-fit:cover; border-radius:9px; border:1px solid var(--line); cursor:pointer; display:block; } +.agent-gen-card { width:250px; max-width:100%; border-radius:12px; border:1px solid var(--line); background:var(--card); padding:8px 10px; display:flex; flex-direction:column; gap:6px; } +.agent-gen-prompt { font-size:10px; color:var(--muted); line-height:1.45; display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical; overflow:hidden; } +.agent-gen-status { display:flex; align-items:center; gap:6px; font-size:10px; font-weight:750; color:var(--muted); } +.agent-gen-status.error { color:#e11d48; } +.agent-gen-status.done { color:#059669; } +.agent-gen-spinner { width:12px; height:12px; border-radius:50%; border:2px solid var(--line); border-top-color:var(--strong); animation:agentSpin .8s linear infinite; flex:0 0 auto; } +@keyframes agentSpin { to { transform:rotate(360deg); } } +.agent-input-area { display:flex; flex-direction:column; gap:6px; } +.agent-attach-row { display:flex; flex-wrap:wrap; gap:6px; } +.agent-attach-row:empty { display:none; } +.agent-attach-chip { position:relative; width:44px; height:44px; border-radius:10px; overflow:hidden; border:1px solid var(--line); } +.agent-attach-chip img { width:100%; height:100%; object-fit:cover; display:block; } +.agent-attach-chip button { position:absolute; top:2px; right:2px; width:15px; height:15px; border-radius:50%; background:rgba(15,23,42,.78); color:#fff; display:flex; align-items:center; justify-content:center; border:none; cursor:pointer; padding:0; } +.agent-attach-chip button i,.agent-attach-chip button svg { width:9px; height:9px; } +.agent-input-row { display:flex; align-items:flex-end; gap:7px; } +.agent-input-row textarea { flex:1; min-width:0; resize:none; max-height:110px; border-radius:13px; border:1px solid var(--line); background:var(--card); color:var(--text); padding:8px 10px; font-size:11.5px; line-height:1.5; outline:none; font-family:inherit; } +.agent-input-row textarea:focus { border-color:var(--strong); } +.agent-send-btn { flex:0 0 auto; width:32px; height:32px; border-radius:11px; background:var(--strong); color:var(--strong-text); display:inline-flex; align-items:center; justify-content:center; border:none; cursor:pointer; } +.agent-send-btn:disabled { opacity:.45; cursor:not-allowed; } +.agent-send-btn i,.agent-send-btn svg { width:14px; height:14px; } +.agent-panel.drag-over-input .agent-input-row textarea { border-color:var(--strong); } + .asset-item { min-width:0; align-self:start; border-radius:12px; border:1px solid var(--line); background:var(--card); overflow:hidden; cursor:grab; } .asset-item:active { cursor:grabbing; } .asset-thumb { width:100%; aspect-ratio:1/1; background:var(--soft); display:block; object-fit:cover; } diff --git a/static/index.html b/static/index.html index 0ab487e46..0806fdec9 100644 --- a/static/index.html +++ b/static/index.html @@ -23,11 +23,11 @@ } catch(e) {} })(); - - - + + + - + - + diff --git a/static/asset-manager.html b/static/asset-manager.html index 7a08d8a01..ffedaf8ea 100644 --- a/static/asset-manager.html +++ b/static/asset-manager.html @@ -16,10 +16,10 @@ } catch(e) {} })(); - - - - + + + +
@@ -47,6 +47,6 @@
- + diff --git a/static/canvas-list.html b/static/canvas-list.html index d50014c28..7270272b9 100644 --- a/static/canvas-list.html +++ b/static/canvas-list.html @@ -16,13 +16,13 @@ } catch(e) {} })(); - - - - - - - + + + + + + +
@@ -90,6 +90,6 @@
- + diff --git a/static/canvas.html b/static/canvas.html index 738c4eb9f..2bf163f58 100644 --- a/static/canvas.html +++ b/static/canvas.html @@ -16,13 +16,13 @@ } catch(e) {} })(); - - - - - - - + + + + + + +
@@ -350,7 +350,7 @@
- - + + diff --git a/static/comfyui-settings.html b/static/comfyui-settings.html index 89ef5c68b..eecf32c60 100644 --- a/static/comfyui-settings.html +++ b/static/comfyui-settings.html @@ -15,12 +15,12 @@ } catch(e) {} })(); - - - - - - + + + + + +
@@ -122,6 +122,6 @@ preview
- + diff --git a/static/css/smart-canvas.css b/static/css/smart-canvas.css index b2d4828a1..b62598042 100644 --- a/static/css/smart-canvas.css +++ b/static/css/smart-canvas.css @@ -1196,9 +1196,10 @@ input[type=range].smart-range::-webkit-slider-thumb { -webkit-appearance:none; w .agent-chat-empty { padding:16px; text-align:center; font-size:10.5px; color:var(--faint); } /* 消息选项按钮 */ -.agent-msg-options { display:flex; gap:6px; margin-top:6px; } -.agent-msg-option-btn { height:28px; padding:0 12px; border-radius:8px; border:1px solid var(--line); background:var(--card); color:var(--text); font-size:10.5px; font-weight:700; cursor:pointer; transition:border-color .14s ease, background .14s ease; } +.agent-msg-options { display:flex; flex-direction:column; gap:6px; margin-top:6px; align-items:stretch; } +.agent-msg-option-btn { min-height:28px; height:auto; padding:6px 12px; border-radius:8px; border:1px solid var(--line); background:var(--card); color:var(--text); font-size:10.5px; font-weight:700; cursor:pointer; text-align:left; white-space:normal; word-break:break-word; line-height:1.4; transition:border-color .14s ease, background .14s ease; } .agent-msg-option-btn:hover { border-color:var(--strong); background:var(--soft); } +.agent-msg-free-hint { margin-top:4px; font-size:9.5px; opacity:.45; } /* 消息操作按钮 */ .agent-msg-actions { display:flex; gap:2px; opacity:0; transition:opacity .14s ease; } @@ -1218,7 +1219,11 @@ input[type=range].smart-range::-webkit-slider-thumb { -webkit-appearance:none; w .agent-msg-thumbs { display:grid; grid-template-columns:repeat(3, 1fr); gap:5px; max-width:240px; } .agent-msg-thumbs img { width:100%; aspect-ratio:1/1; object-fit:cover; border-radius:9px; border:1px solid var(--line); cursor:pointer; display:block; } .agent-gen-card { width:250px; max-width:100%; border-radius:12px; border:1px solid var(--line); background:var(--card); padding:8px 10px; display:flex; flex-direction:column; gap:6px; } -.agent-gen-prompt { font-size:10px; color:var(--muted); line-height:1.45; display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical; overflow:hidden; } +.agent-gen-prompt { font-size:10px; color:var(--muted); line-height:1.45; position:relative; } +.agent-gen-prompt-collapsed { display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical; overflow:hidden; } +.agent-gen-prompt-expanded { /* 展开后不截断 */ } +.agent-gen-prompt-toggle { display:inline-block; margin-left:4px; padding:0 2px; border:none; background:none; color:var(--strong); font-size:10px; font-weight:700; cursor:pointer; vertical-align:baseline; white-space:nowrap; } +.agent-gen-prompt-toggle:hover { text-decoration:underline; } .agent-gen-status { display:flex; align-items:center; gap:6px; font-size:10px; font-weight:750; color:var(--muted); } .agent-gen-status.error { color:#e11d48; } .agent-gen-status.done { color:#059669; } diff --git a/static/enhance.html b/static/enhance.html index 5a109f3db..cf35dad9d 100644 --- a/static/enhance.html +++ b/static/enhance.html @@ -17,14 +17,14 @@ } catch(e) {} })(); - - - - - - + + + + + + - + diff --git a/static/gpt-chat.html b/static/gpt-chat.html index 354897b33..453f2f037 100644 --- a/static/gpt-chat.html +++ b/static/gpt-chat.html @@ -16,13 +16,13 @@ } catch(e) {} })(); - - - - - + + + + + - + diff --git a/static/smart-canvas.html b/static/smart-canvas.html index 46e2a9175..b9f755228 100644 --- a/static/smart-canvas.html +++ b/static/smart-canvas.html @@ -551,6 +551,6 @@
preview
- + diff --git a/static/zimage.html b/static/zimage.html index b01d91025..2413f812f 100644 --- a/static/zimage.html +++ b/static/zimage.html @@ -17,12 +17,12 @@ } catch(e) {} })(); - - - - - - + + + + + + - + diff --git "a/\345\256\211\350\243\205\345\215\263\346\242\246CLI.command" "b/\345\256\211\350\243\205\345\215\263\346\242\246CLI.command" old mode 100644 new mode 100755 diff --git "a/\347\231\273\345\275\225\345\215\263\346\242\246CLI.command" "b/\347\231\273\345\275\225\345\215\263\346\242\246CLI.command" old mode 100644 new mode 100755 From 0adc213524469234ede5aaf80b52f2e70ef3887b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Sat, 18 Jul 2026 22:45:57 +0800 Subject: [PATCH 24/67] =?UTF-8?q?=E4=BC=98=E5=8C=96agent=E7=94=9F=E5=9B=BE?= =?UTF-8?q?=E8=BD=AE=E8=AF=A2=E9=97=B4=E9=9A=94=E5=92=8C=E5=8D=A0=E4=BD=8D?= =?UTF-8?q?=E8=8A=82=E7=82=B9=E6=8C=89=E6=AF=94=E4=BE=8B=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- history.json | 42 +++++++++++++++++++++++++++++++++++++++ static/js/smart-canvas.js | 18 +++++++++++++++-- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/history.json b/history.json index 5ace97cb2..9ce4840ef 100644 --- a/history.json +++ b/history.json @@ -1,4 +1,46 @@ [ + { + "prompt": "[generated 1 image(s) with prompt: 皮克斯3D动画风格的粉色小猪在阳光下奔跑,圆润的卡通造型搭配夸张的面部表情,草地呈现柔和的蓝绿色渐变,丁达尔光线效果转化为光粒子特效,超精细的毛发渲染(每根毛发都有高光),背景树木卡通化为柔和的墨绿色团块,16:9电影画幅带景深模糊,迪士尼皮克斯经典色彩管理]", + "images": [ + "/assets/output/online_f57ee16724.png" + ], + "image_items": [ + { + "url": "/assets/output/online_f57ee16724.png", + "kind": "image", + "name": "online_f57ee16724.png", + "natural_w": 1254, + "natural_h": 1254, + "width": 1254, + "height": 1254 + } + ], + "timestamp": 1784385800.8972008, + "type": "online", + "model": "gpt-image-2", + "provider_id": "apimart", + "provider_name": "APIMART", + "task_id": "task_01KXTTS4QTJHHARYFSF31C8J52", + "request_id": null, + "params": { + "provider_id": "apimart", + "model": "gpt-image-2", + "size": "1024x1024", + "requested_size": "1024x1024", + "quality": "auto", + "n": 1, + "reference_images": [ + { + "url": "/assets/output/online_6c457f9d08.png", + "name": "online_6c457f9d08.png", + "role": "", + "kind": "", + "mime": "" + } + ] + }, + "raw_usage": null + }, { "prompt": "[generated 1 image(s) with prompt: 写实风格的棕色猪在阳光下的草地上奔跑,午后柔和的光线穿透云层形成光束,猪的身体前倾呈现奔跑姿态,泥蹄翻起草皮,远处有模糊的树木轮廓与狗场景相同,8K高清画质,极致细节刻画卷曲的猪毛与草地露珠,自然纪录片电影色调保持统一]", "images": [ diff --git a/static/js/smart-canvas.js b/static/js/smart-canvas.js index a56a0dafe..3e80f23ce 100644 --- a/static/js/smart-canvas.js +++ b/static/js/smart-canvas.js @@ -15210,7 +15210,9 @@ async function pollSmartCanvasTask(taskId){ if(activeSmartTaskPolls.has(taskId)) return activeSmartTaskPolls.get(taskId); const promise = (async () => { for(let i = 0; i < 900; i++){ - await new Promise(resolve => setTimeout(resolve, 2000)); + // 动态轮询间隔:前5次快速轮询(500ms),之后逐步增加到 2000ms + const pollInterval = i < 5 ? 500 : i < 15 ? 1000 : 2000; + await new Promise(resolve => setTimeout(resolve, pollInterval)); const task = await fetch(`/api/canvas-image-tasks/${encodeURIComponent(taskId)}`).then(async r => { if(!r.ok) throw new Error(await r.text()); return r.json(); @@ -17670,7 +17672,7 @@ async function runAgentGenerations(assistantMsg, userMsg){ await Promise.all(gens.map(async gen => { gen.status = 'running'; renderAgentMessages(); - // 先创建占位节点(按照图片尺寸来) + // 先创建占位节点(按照当前选择的比例来) const pos = agentFindEmptyPosition(gen.count); const placeholderNode = createImageNodeAt(pos, []); if(placeholderNode){ @@ -17679,6 +17681,18 @@ async function runAgentGenerations(assistantMsg, userMsg){ // 修复:设置计时开始时间,使占位节点显示正计时 placeholderNode.runStartedAt = nowMs(); placeholderNode.runTimerHidden = false; + // 修复:按当前选择的比例设置占位节点尺寸 + const ratioSize = apiImageSize(agentState.genRatio || 'square', agentState.genResolution || '1k') || '1024x1024'; + const sizeParts = String(ratioSize).split('x'); + if(sizeParts.length === 2){ + const rw = Number(sizeParts[0]) || 1024; + const rh = Number(sizeParts[1]) || 1024; + const ratio = rw / rh; + const baseW = 260; + const baseH = Math.round(baseW / ratio); + placeholderNode.w = baseW; + placeholderNode.h = baseH; + } // 修复:顶部对齐 — 找到已有图片节点的最小顶部 y,将占位节点顶部对齐 const existingNodes = (nodes || []).filter(n => isSmartImageNode(n) && n.id !== placeholderNode.id && (n.images || []).some(img => img?.url)); if(existingNodes.length){ From 70d412febb474f78bce740d4edaf42c60fb760d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Sat, 18 Jul 2026 22:57:31 +0800 Subject: [PATCH 25/67] =?UTF-8?q?=E4=BF=AE=E5=A4=8DAgent=E7=94=9F=E5=9B=BE?= =?UTF-8?q?=E5=BB=B6=E8=BF=9F=EF=BC=9AWebSocket=E5=AE=9E=E6=97=B6=E9=80=9A?= =?UTF-8?q?=E7=9F=A5=E6=9B=BF=E4=BB=A3=E8=BD=AE=E8=AF=A2+=E7=B2=BE?= =?UTF-8?q?=E7=AE=80=E7=B3=BB=E7=BB=9F=E6=8F=90=E7=A4=BA=E8=AF=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.py | 21 +++++++ static/js/smart-canvas.js | 118 +++++++++++--------------------------- 2 files changed, 56 insertions(+), 83 deletions(-) diff --git a/main.py b/main.py index 47364fcf2..293332e6e 100755 --- a/main.py +++ b/main.py @@ -160,6 +160,15 @@ async def broadcast_asset_library_updated(self, updated_at: int = 0): print(f"Broadcast asset library error: {e}") self.active_connections.remove(connection) + async def broadcast_canvas_task_done(self, task_id: str, status: str): + data = json.dumps({"type": "canvas_task_done", "task_id": task_id, "status": status}) + for connection in self.active_connections[:]: + try: + await connection.send_text(data) + except Exception as e: + print(f"Broadcast canvas task done error: {e}") + self.active_connections.remove(connection) + async def send_personal_message(self, message: dict, client_id: str): ws = self.user_connections.get(client_id) if ws: @@ -13388,6 +13397,10 @@ async def run_canvas_image_task(task_id: str, payload: OnlineImageRequest): "error": "", "updated_at": time.time(), }) + try: + await manager.broadcast_canvas_task_done(task_id, "succeeded") + except Exception: + pass except JimengPendingError as exc: # 即梦云端还在排队:标记为 jimeng_pending,前端据 submit_id 持久续查(任务未丢失) info = jimeng_pending_payload(exc) @@ -13402,6 +13415,10 @@ async def run_canvas_image_task(task_id: str, payload: OnlineImageRequest): "error": "", "updated_at": time.time(), }) + try: + await manager.broadcast_canvas_task_done(task_id, "jimeng_pending") + except Exception: + pass except Exception as exc: detail = getattr(exc, "detail", None) or str(exc) status_code = getattr(exc, "status_code", 500) @@ -13414,6 +13431,10 @@ async def run_canvas_image_task(task_id: str, payload: OnlineImageRequest): "upstream_task_id": upstream_task_id, "updated_at": time.time(), }) + try: + await manager.broadcast_canvas_task_done(task_id, "failed") + except Exception: + pass @app.post("/api/canvas-image-tasks") async def create_canvas_image_task(payload: OnlineImageRequest): diff --git a/static/js/smart-canvas.js b/static/js/smart-canvas.js index 3e80f23ce..e6e081e9d 100644 --- a/static/js/smart-canvas.js +++ b/static/js/smart-canvas.js @@ -5270,6 +5270,7 @@ function connectAssetLibrarySyncSocket(){ const data = JSON.parse(event.data); if(data?.type === 'asset_library_updated') handleAssetLibraryUpdatedMessage(data); if(data?.type === 'canvas_updated') handleCanvasUpdatedMessage(data); + if(data?.type === 'canvas_task_done') handleCanvasTaskDoneMessage(data); } catch(e) {} }; socket.onclose = () => { @@ -15205,14 +15206,31 @@ function resumeJimengPendingNodes(){ startJimengPoll(n); }); } +// WebSocket 实时任务完成通知:消除轮询延迟 +const _canvasTaskWaiters = new Map(); +function handleCanvasTaskDoneMessage(data){ + const taskId = data?.task_id; + const status = data?.status; + if(!taskId) return; + const waiter = _canvasTaskWaiters.get(taskId); + if(waiter) waiter(status); +} async function pollSmartCanvasTask(taskId){ if(!taskId) throw new Error(tr('smart.errRunFailed')); if(activeSmartTaskPolls.has(taskId)) return activeSmartTaskPolls.get(taskId); const promise = (async () => { for(let i = 0; i < 900; i++){ - // 动态轮询间隔:前5次快速轮询(500ms),之后逐步增加到 2000ms + // 注册 WebSocket 等待器:收到任务完成通知时立即查询,无需等轮询间隔 + const wsNotify = new Promise(resolve => { + _canvasTaskWaiters.set(taskId, (status) => resolve(status || 'done')); + }); + // 同时设置超时轮询作为保底 const pollInterval = i < 5 ? 500 : i < 15 ? 1000 : 2000; - await new Promise(resolve => setTimeout(resolve, pollInterval)); + const timeout = new Promise(resolve => setTimeout(() => resolve('poll'), pollInterval)); + // 哪个先触发就用哪个 + const trigger = await Promise.race([wsNotify, timeout]); + _canvasTaskWaiters.delete(taskId); + // 如果是 WebSocket 通知触发,立即查询;如果是超时触发,也查询 const task = await fetch(`/api/canvas-image-tasks/${encodeURIComponent(taskId)}`).then(async r => { if(!r.ok) throw new Error(await r.text()); return r.json(); @@ -15232,6 +15250,7 @@ async function pollSmartCanvasTask(taskId){ return await promise; } finally { activeSmartTaskPolls.delete(taskId); + _canvasTaskWaiters.delete(taskId); } } function finalizeSmartPendingTask(node, taskId, images, kind='image'){ @@ -16694,89 +16713,22 @@ const AGENT_LLM_IMAGE_MAX = 8; const AGENT_GEN_MAX_PER_MSG = 8; const AGENT_MSG_MAX = 60; const AGENT_NL = String.fromCharCode(10); -const AGENT_FORMAT_INSTRUCTION = `You are an AI image-generation agent inside an infinite-canvas app. If a skill document is provided above, follow its style and rules closely. - -You MUST reply with a raw JSON object only. No markdown fences, no explanation, no extra text: -{"reply":"your conversational reply","options":[],"prompts":[],"generations":[{"prompt":"详细的中文生图提示词","count":1,"use_last_outputs":false,"use_attachments":false}]} - -## Field Reference -- "reply": text shown to the user, written in the user's language. -- "options": array of action buttons shown below your reply. Each option has "label" (button text) and "value" (text sent when clicked). Omit or use [] when no buttons are needed. -- "prompts": prompt strings used when you propose a plan but wait for user confirmation. Each prompt should be a detailed, self-contained image prompt written in Chinese. -- "generations": images to generate right away. Use [] when no image is needed. -- "prompt": detailed, self-contained image prompt written in Chinese (中文). Include subject, style, composition, lighting, colors, details, and atmosphere. -- "count": integer 1 to 8. -- "use_last_outputs": true when the request refers to or modifies the most recently generated images. -- "use_attachments": true when the request refers to or modifies images the user attached. -- At most 8 generation items. - -## Core Principle: Generate, don't ask -The goal is to help users get images, not to have conversations. Follow these rules strictly: -1. If the user's request has a clear subject (what to draw) AND enough detail (style, scene, or features) → EXPAND the prompt and GENERATE immediately (Pattern 1). Do NOT ask for confirmation. -2. If the user just selected an option or answered a clarification question → their request is now clear → GENERATE immediately (Pattern 1). Do NOT ask "确认要生成吗?" — that is meaningless and wastes a turn. -3. Only ask questions when you genuinely cannot form a reasonable plan (Pattern 3 or 4). -4. NEVER reply with only "你确认要生成...吗?" without providing a full detailed prompt. If you want to confirm, you MUST include the complete prompt in "prompts" so the user can review it (Pattern 2). - -## Response Patterns — choose the right one each time - -### Pattern 1: DIRECT GENERATION (default — use this whenever possible) -The user gave enough detail, OR just answered a clarification question, OR just selected an option. Expand their request into a detailed Chinese prompt and generate right away. -- "generations": [{"prompt":"你的详细中文提示词","count":1}] -- "options": [] -- "prompts": [] -- "reply": brief description of what you're generating (e.g., "好的,正在为您生成一条守护青铜鼎的红色龙...") -Triggers: "画一只橘猫坐在窗台上" / user selected "红色龙守护青铜鼎" / user answered "卡通风格" - -### Pattern 2: CONFIRM WITH FULL PROMPT (use sparingly — only when the plan is complex and user might want to adjust) -You have a specific plan but it involves many creative choices the user might want to tweak. You MUST provide the FULL detailed Chinese prompt in "prompts" for the user to review — NOT just ask "确认吗?". -- "prompts": ["完整的中文提示词,包含主体、风格、构图、光线、色彩、细节、氛围"] -- "options": [{"label":"确认","value":"确认"},{"label":"修改","value":"修改"}] -- "generations": [] -- "reply": the full prompt text, so the user can read it -IMPORTANT: If you can't provide a meaningfully different or expanded prompt beyond what the user already said, use Pattern 1 instead. - -### Pattern 3: MULTIPLE CHOICE (request is vague, several distinct directions possible) -The user's request is too vague to form a single plan. Put each choice as a SEPARATE button. After the user selects one → go to Pattern 1 (generate directly). -- "options": [ - {"label":"盘旋云端的金色龙","value":"我要生成一条盘旋于云端的金色龙"}, - {"label":"飞越山川的蓝色龙","value":"我要生成一条飞越山川的蓝色龙"}, - {"label":"守护宫殿的红色龙","value":"我要生成一条守护宫殿的红色龙"} - ] -- "reply": a short question like "您希望生成哪种龙?" -- "generations": [] -- "prompts": [] -IMPORTANT: After the user clicks an option, do NOT ask for confirmation again — generate directly using Pattern 1. - -### Pattern 4: CLARIFY (critical info is missing, cannot form options) -The request is missing critical info and you cannot guess 2-3 reasonable options. Ask 1-3 short questions. -- "reply": your questions -- "options": [] -- "generations": [] -- "prompts": [] -Example: "画一个角色" → ask: male or female? what style? what scene? - -### Pattern 5: BATCH GENERATION (user asks for multiple different images) -- "generations": [{prompt: "中文提示词1", count:1}, {prompt: "中文提示词2", count:1}, ...] -- "options": [] +const AGENT_FORMAT_INSTRUCTION = `You are an AI image-generation agent. Reply with raw JSON only (no markdown, no extra text): +{"reply":"回复用户的话","options":[],"prompts":[],"generations":[{"prompt":"详细中文提示词","count":1,"use_last_outputs":false,"use_attachments":false}]} -## How to choose -- User request has subject + any detail → Pattern 1 (expand and generate) -- User just selected an option or answered a question → Pattern 1 (generate now) -- User request is vague, multiple distinct directions → Pattern 3 (offer choices) -- User request is missing critical info, can't form options → Pattern 4 (ask) -- User asks for N different images → Pattern 5 -- AVOID Pattern 2 unless the plan is truly complex and the prompt adds significant value beyond what the user said +Fields: "reply"=对话回复; "options"=[{label,value}]按钮; "prompts"=待确认的中文提示词; "generations"=立即生成的图片(最多8项). "use_last_outputs":引用/修改上一轮图片时true; "use_attachments":引用/修改用户上传图时true. -## Important rules -- All prompts MUST be written in Chinese (中文). Include 主体、风格、构图、光线、色彩、细节、氛围. -- When the user uploads a character reference image, remember the character's appearance and maintain consistency in all subsequent generations. -- When a skill document is provided, follow its style rules strictly. -- When the user refers to a previous image (e.g., "把背景换成蓝色", "修改这张"), use "use_last_outputs": true. -- When generating multiple images, ensure each prompt is sufficiently different. -- When the user specifies image parameters (e.g., "16:9", "高清", "4K"), include these in the prompt. -- When the user asks to convert an image to a different style (e.g., "把这张照片转成小黑风格"), describe the target style in detail in the prompt. -- NEVER ask "确认要生成吗?" without providing a full prompt. This is the most important rule. -- When the user asks about an image's content (e.g., "这张图里有什么", "描述一下这张图"), analyze the image and provide a detailed description.`; +核心原则:能生成就生成,不要问。 +- 用户有明确主体+任何细节 → 扩写prompt并立即生成(generations) +- 用户刚选了选项或回答了问题 → 立即生成,不要再确认 +- 请求模糊有多个方向 → 给2-4个选项(options),用户选后立即生成 +- 关键信息缺失无法给选项 → 问1-3个短问题(reply) +- 用户要多张不同图 → generations放多项 +- 所有prompt必须中文,包含主体/风格/构图/光线/色彩/细节/氛围 +- 用户提到修改上一张图(如"换个背景"、"改成像素风") → use_last_outputs:true +- 用户上传角色参考图 → 后续生成保持角色一致性 +- 有skill文档时遵循其风格规则 +- 不要只问"确认要生成吗"而不给完整prompt`; let agentOpen = false; let agentSending = false; let agentThinking = false; From 9aeaf5c9cbf7f98b93a03e77bb0d674202d2110e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Sat, 18 Jul 2026 23:01:53 +0800 Subject: [PATCH 26/67] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=8D=A0=E4=BD=8D?= =?UTF-8?q?=E6=AF=94=E4=BE=8B=E5=8A=A8=E6=80=81=E6=98=BE=E7=A4=BA+?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=9C=BA=E6=99=AF=E6=8F=90=E7=A4=BA=E8=AF=8D?= =?UTF-8?q?=E8=BF=87=E5=BA=A6=E5=8F=91=E6=95=A3=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/js/smart-canvas.js | 64 ++++++++++++++++++++++++++++++--------- 1 file changed, 49 insertions(+), 15 deletions(-) diff --git a/static/js/smart-canvas.js b/static/js/smart-canvas.js index e6e081e9d..37ccca04f 100644 --- a/static/js/smart-canvas.js +++ b/static/js/smart-canvas.js @@ -16725,10 +16725,17 @@ Fields: "reply"=对话回复; "options"=[{label,value}]按钮; "prompts"=待确 - 关键信息缺失无法给选项 → 问1-3个短问题(reply) - 用户要多张不同图 → generations放多项 - 所有prompt必须中文,包含主体/风格/构图/光线/色彩/细节/氛围 -- 用户提到修改上一张图(如"换个背景"、"改成像素风") → use_last_outputs:true - 用户上传角色参考图 → 后续生成保持角色一致性 - 有skill文档时遵循其风格规则 -- 不要只问"确认要生成吗"而不给完整prompt`; +- 不要只问"确认要生成吗"而不给完整prompt + +修改场景规则(非常重要): +当用户要求修改上一张图时(如"换成像素风"、"改成卡通"、"背景换成蓝色"),必须严格遵守: +1. use_last_outputs必须为true +2. 保持原图的主体、姿态、构图完全不变,只修改用户指定的部分 +3. prompt应简洁聚焦,只描述要修改的内容+保持原图其他部分不变 +4. 绝对不要添加用户没要求的内容,特别是:动画帧、序列帧、多角度视图、动作分解、分镜 +5. 例如用户说"换成像素风",prompt应该是"将原图转换为像素艺术风格,保持原有的主体、姿态、构图和色彩不变,仅改变渲染风格为像素化",而不是扩写成包含动画帧等无关内容的长描述`; let agentOpen = false; let agentSending = false; let agentThinking = false; @@ -17601,6 +17608,37 @@ function agentFindEmptyPosition(count=1){ const gap = 40; return {x:rect.x + rect.width + gap + 130, y:rect.y}; } +// Agent 占位节点尺寸:复用主画布的 pendingBoxSize 逻辑,但使用 Agent 自己的比例设置 +function agentPendingBoxSize(count, options={}){ + // 用 Agent 的比例设置算出请求尺寸 + const ratioSize = apiImageSize(agentState.genRatio || 'square', agentState.genResolution || '1k') || '1024x1024'; + const parsed = parseSizeValue(ratioSize); + const requestSize = parsed ? {w:Number(parsed.width) || 1024, h:Number(parsed.height) || 1024} : {w:1024, h:1024}; + // 如果有参考图/源节点,优先用参考图尺寸(和主画布一致) + const sourceSize = pendingSourceBoxSize(options); + if(sourceSize?.display) return {w:sourceSize.w, h:sourceSize.h}; + if(sourceSize) return displayBoxFromNaturalSize(sourceSize); + // 用请求尺寸算显示尺寸(复用主画布的 singleImageLayout) + const base = displayBoxFromNaturalSize(requestSize); + // 多张图时按网格排列(和主画布的 pendingBoxSize 完全一致) + const c = Math.max(1, Number(count) || 1); + if(c <= 1) return {w:Math.round(base.w), h:Math.round(base.h)}; + const aspect = base.w / Math.max(1, base.h); + const cols = Math.min(4, Math.max(2, Math.ceil(Math.sqrt(c)))); + const rows = Math.ceil(c / cols); + const cellMax = Math.max(96, Math.min(220, Math.max(base.w, base.h) * 0.42)); + let cellW, cellH; + if(base.w >= base.h){ + cellW = cellMax; + cellH = Math.max(40 * MEDIA_NODE_DEFAULT_SCALE, Math.round(cellMax / aspect)); + } else { + cellH = cellMax; + cellW = Math.max(40 * MEDIA_NODE_DEFAULT_SCALE, Math.round(cellMax * aspect)); + } + const w = cols * (cellW + 8) + 16; + const h = rows * (cellH + 8) + 16; + return {w, h}; +} async function runAgentGenerations(assistantMsg, userMsg){ const gens = assistantMsg.generations || []; if(!gens.length) return; @@ -17624,7 +17662,7 @@ async function runAgentGenerations(assistantMsg, userMsg){ await Promise.all(gens.map(async gen => { gen.status = 'running'; renderAgentMessages(); - // 先创建占位节点(按照当前选择的比例来) + // 先创建占位节点(复用主画布的 pendingBoxSize 逻辑,按当前选择的比例动态计算) const pos = agentFindEmptyPosition(gen.count); const placeholderNode = createImageNodeAt(pos, []); if(placeholderNode){ @@ -17633,18 +17671,14 @@ async function runAgentGenerations(assistantMsg, userMsg){ // 修复:设置计时开始时间,使占位节点显示正计时 placeholderNode.runStartedAt = nowMs(); placeholderNode.runTimerHidden = false; - // 修复:按当前选择的比例设置占位节点尺寸 - const ratioSize = apiImageSize(agentState.genRatio || 'square', agentState.genResolution || '1k') || '1024x1024'; - const sizeParts = String(ratioSize).split('x'); - if(sizeParts.length === 2){ - const rw = Number(sizeParts[0]) || 1024; - const rh = Number(sizeParts[1]) || 1024; - const ratio = rw / rh; - const baseW = 260; - const baseH = Math.round(baseW / ratio); - placeholderNode.w = baseW; - placeholderNode.h = baseH; - } + // 修复:按当前选择的比例设置占位节点尺寸(复用主画布逻辑) + let refsForBox = []; + if(gen.use_last_outputs) refsForBox = refsForBox.concat(lastResults); + if(gen.use_attachments) refsForBox = refsForBox.concat(attachRefs); + refsForBox = imageRefsOnly(refsForBox).slice(0, SMART_REFERENCE_IMAGE_MAX); + const pendingBox = agentPendingBoxSize(gen.count, {refs: refsForBox}); + placeholderNode.w = pendingBox.w; + placeholderNode.h = pendingBox.h; // 修复:顶部对齐 — 找到已有图片节点的最小顶部 y,将占位节点顶部对齐 const existingNodes = (nodes || []).filter(n => isSmartImageNode(n) && n.id !== placeholderNode.id && (n.images || []).some(img => img?.url)); if(existingNodes.length){ From 5a860dd62ead9835f09c864c3777f2067d2dc06e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Sat, 18 Jul 2026 23:30:05 +0800 Subject: [PATCH 27/67] =?UTF-8?q?Agent=E6=9E=B6=E6=9E=84=E9=87=8D=E6=9E=84?= =?UTF-8?q?=EF=BC=9ALLM=E4=BB=BB=E5=8A=A1=E5=90=8E=E7=AB=AF=E5=8C=96+?= =?UTF-8?q?=E6=81=AF=E5=B1=8F/=E5=88=B7=E6=96=B0=E6=81=A2=E5=A4=8D+?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=84=8F=E5=9B=BE=E9=93=BE=E8=B7=AF=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.py | 68 ++++++ static/js/i18n/smart-canvas.js | 1 + static/js/smart-canvas.js | 394 +++++++++++++++++++++++++-------- 3 files changed, 370 insertions(+), 93 deletions(-) diff --git a/main.py b/main.py index 293332e6e..9f6c092b6 100755 --- a/main.py +++ b/main.py @@ -169,6 +169,15 @@ async def broadcast_canvas_task_done(self, task_id: str, status: str): print(f"Broadcast canvas task done error: {e}") self.active_connections.remove(connection) + async def broadcast_agent_llm_done(self, task_id: str, status: str): + data = json.dumps({"type": "agent_llm_done", "task_id": task_id, "status": status}) + for connection in self.active_connections[:]: + try: + await connection.send_text(data) + except Exception as e: + print(f"Broadcast agent llm done error: {e}") + self.active_connections.remove(connection) + async def send_personal_message(self, message: dict, client_id: str): ws = self.user_connections.get(client_id) if ws: @@ -2523,6 +2532,8 @@ class ImageTaskQueryRequest(BaseModel): CANVAS_TASKS: Dict[str, Dict[str, Any]] = {} CANVAS_TASK_LOCK = Lock() +AGENT_LLM_TASKS: Dict[str, Dict[str, Any]] = {} +AGENT_LLM_TASK_LOCK = Lock() class CanvasVideoRequest(BaseModel): prompt: str = Field(min_length=1, max_length=VIDEO_PROMPT_MAX_LENGTH) @@ -13436,6 +13447,63 @@ async def run_canvas_image_task(task_id: str, payload: OnlineImageRequest): except Exception: pass +async def run_agent_llm_task(task_id: str, payload: CanvasLLMRequest): + with AGENT_LLM_TASK_LOCK: + if task_id in AGENT_LLM_TASKS: + AGENT_LLM_TASKS[task_id]["status"] = "running" + AGENT_LLM_TASKS[task_id]["updated_at"] = time.time() + try: + result = await canvas_llm(payload) + with AGENT_LLM_TASK_LOCK: + AGENT_LLM_TASKS[task_id].update({ + "status": "succeeded", + "result": result, + "error": "", + "updated_at": time.time(), + }) + try: + await manager.broadcast_agent_llm_done(task_id, "succeeded") + except Exception: + pass + except Exception as exc: + detail = getattr(exc, "detail", None) or str(exc) + status_code = getattr(exc, "status_code", 500) + with AGENT_LLM_TASK_LOCK: + AGENT_LLM_TASKS[task_id].update({ + "status": "failed", + "error": str(detail), + "status_code": status_code, + "updated_at": time.time(), + }) + try: + await manager.broadcast_agent_llm_done(task_id, "failed") + except Exception: + pass + +@app.post("/api/agent-llm-task") +async def create_agent_llm_task(payload: CanvasLLMRequest): + task_id = f"agent_llm_{uuid.uuid4().hex}" + with AGENT_LLM_TASK_LOCK: + AGENT_LLM_TASKS[task_id] = { + "id": task_id, + "type": "agent-llm", + "status": "queued", + "created_at": time.time(), + "updated_at": time.time(), + "result": None, + "error": "", + } + asyncio.create_task(run_agent_llm_task(task_id, payload)) + return {"task_id": task_id, "status": "queued"} + +@app.get("/api/agent-llm-task/{task_id}") +async def get_agent_llm_task(task_id: str): + with AGENT_LLM_TASK_LOCK: + task = dict(AGENT_LLM_TASKS.get(task_id) or {}) + if not task: + raise HTTPException(status_code=404, detail="Agent LLM 任务不存在,可能服务已重启") + return task + @app.post("/api/canvas-image-tasks") async def create_canvas_image_task(payload: OnlineImageRequest): task_id = f"canvas_img_{uuid.uuid4().hex}" diff --git a/static/js/i18n/smart-canvas.js b/static/js/i18n/smart-canvas.js index ac481c973..5933e6002 100644 --- a/static/js/i18n/smart-canvas.js +++ b/static/js/i18n/smart-canvas.js @@ -304,6 +304,7 @@ "smart.agentCollapse": { zh: "收起", en: "Collapse" }, "smart.agentInterrupted": { zh: "上次操作被中断,请重新发送", en: "Last operation was interrupted, please resend" }, "smart.agentRetry": { zh: "重新发送", en: "Resend" }, +"smart.agentRecovering": { zh: "正在恢复上次操作...", en: "Recovering last operation..." }, "smart.agentThinking": { zh: "正在思考...", en: "Thinking..." }, "smart.agentNeedChatModel": { zh: "请先在 API 设置中配置聊天模型", en: "Please configure a chat provider in API settings first" }, "smart.agentNeedGenModel": { zh: "请先在 API 设置中配置生图模型", en: "Please configure an image provider in API settings first" }, diff --git a/static/js/smart-canvas.js b/static/js/smart-canvas.js index 37ccca04f..a8b3e192a 100644 --- a/static/js/smart-canvas.js +++ b/static/js/smart-canvas.js @@ -5271,6 +5271,7 @@ function connectAssetLibrarySyncSocket(){ if(data?.type === 'asset_library_updated') handleAssetLibraryUpdatedMessage(data); if(data?.type === 'canvas_updated') handleCanvasUpdatedMessage(data); if(data?.type === 'canvas_task_done') handleCanvasTaskDoneMessage(data); + if(data?.type === 'agent_llm_done') handleAgentLlmDoneMessage(data); } catch(e) {} }; socket.onclose = () => { @@ -15215,6 +15216,34 @@ function handleCanvasTaskDoneMessage(data){ const waiter = _canvasTaskWaiters.get(taskId); if(waiter) waiter(status); } +// Agent LLM 任务 WebSocket 通知 +const _agentLlmWaiters = new Map(); +function handleAgentLlmDoneMessage(data){ + const taskId = data?.task_id; + const status = data?.status; + if(!taskId) return; + const waiter = _agentLlmWaiters.get(taskId); + if(waiter) waiter(status || 'done'); +} +async function pollAgentLlmTask(taskId){ + if(!taskId) throw new Error('Invalid task ID'); + for(let i = 0; i < 600; i++){ + const wsNotify = new Promise(resolve => { + _agentLlmWaiters.set(taskId, (status) => resolve(status || 'done')); + }); + const pollInterval = i < 5 ? 1000 : 3000; + const timeout = new Promise(resolve => setTimeout(() => resolve('poll'), pollInterval)); + await Promise.race([wsNotify, timeout]); + _agentLlmWaiters.delete(taskId); + const task = await fetch(`/api/agent-llm-task/${encodeURIComponent(taskId)}`).then(async r => { + if(!r.ok) throw new Error(await r.text()); + return r.json(); + }); + if(task.status === 'succeeded') return task.result || {}; + if(task.status === 'failed') throw new Error(task.error || 'LLM task failed'); + } + throw new Error('LLM task timeout'); +} async function pollSmartCanvasTask(taskId){ if(!taskId) throw new Error(tr('smart.errRunFailed')); if(activeSmartTaskPolls.has(taskId)) return activeSmartTaskPolls.get(taskId); @@ -16783,26 +16812,104 @@ function loadAgentState(){ // 加载当前对话的 messages const activeConv = agentState.conversations.find(c => c.id === agentState.activeConversationId); agentState.messages = activeConv ? (activeConv.messages || []).slice(-AGENT_MSG_MAX) : []; - // 检测上次中断的操作,添加提示消息 - if(agentState._pendingMessage !== undefined){ - const pendingText = String(agentState._pendingMessage || ''); - if(pendingText && agentState.messages.length){ - // 检查最后一条是否是用户消息(说明 assistant 还没回复就被中断了) - const lastMsg = agentState.messages[agentState.messages.length - 1]; - if(lastMsg && lastMsg.role === 'user'){ - agentState.messages.push({ - id: uid('am'), - role: 'assistant', - text: '⚠️ ' + (tr('smart.agentInterrupted') || '上次操作被中断,请重新发送'), - options: [{label: tr('smart.agentRetry') || '重新发送', value: pendingText}], - generations: [], - ts: Date.now() - }); + // 恢复中断的操作 + _setupAgentRecovery(); +} +// 恢复中断的 Agent 操作(页面刷新后调用) +let _agentRecoveryInProgress = false; +function _setupAgentRecovery(){ + if(!agentState) return; + const pendingLlmTaskId = agentState._pendingLlmTaskId; + const pendingText = String(agentState._pendingMessage || ''); + const pendingAttachments = Array.isArray(agentState._pendingAttachments) ? agentState._pendingAttachments : []; + const pendingUserMsg = agentState._pendingUserMsg; + // 情况1:LLM task 还在后端跑 → 恢复等待 + if(pendingLlmTaskId && pendingText && pendingUserMsg){ + const lastMsg = agentState.messages[agentState.messages.length - 1]; + if(lastMsg && lastMsg.role === 'user' && lastMsg.text === pendingText){ + // 在最后一条 user 消息后插入一条"恢复中"的 assistant 占位消息 + agentState.messages.push({ + id: uid('am'), + role: 'assistant', + text: '⏳ ' + (tr('smart.agentRecovering') || '正在恢复上次操作...'), + generations: [], + ts: Date.now() + }); + agentThinking = true; + agentSending = true; + renderAgentMessages(); + _agentRecoveryInProgress = true; + // 异步恢复 LLM task + (async () => { + try { + const result = await pollAgentLlmTask(pendingLlmTaskId); + // 移除占位的"恢复中"消息 + agentState.messages = agentState.messages.filter(m => m.text !== '⏳ ' + (tr('smart.agentRecovering') || '正在恢复上次操作...')); + await processAgentLlmResult(result, pendingText, pendingAttachments, pendingUserMsg); + } catch(e) { + agentState.messages = agentState.messages.filter(m => !m.text?.startsWith('⏳')); + agentState.messages.push({id:uid('am'), role:'assistant', text:`⚠️ ${String(e.message || e).slice(0, 300)}`, generations:[], ts:Date.now()}); + renderAgentMessages(); + saveAgentState(); + } finally { + agentThinking = false; + agentSending = false; + _agentRecoveryInProgress = false; + delete agentState._pendingMessage; + delete agentState._pendingAttachments; + delete agentState._pendingUserMsg; + delete agentState._pendingLlmTaskId; + renderAgentMessages(); + saveAgentState(); + } + })(); + return; + } + } + // 情况2:生图 task 还在后端跑(LLM 已完成但生图未完成) + const msgs = agentState.messages || []; + let hasRunningGen = false; + for(let i = msgs.length - 1; i >= 0; i--){ + if(msgs[i].role !== 'assistant') continue; + const gens = msgs[i].generations || []; + for(const gen of gens){ + if(gen.status === 'running' && gen.taskIds && gen.taskIds.length){ + hasRunningGen = true; + break; } } - delete agentState._pendingMessage; - delete agentState._pendingAttachments; + break; } + if(hasRunningGen){ + renderAgentMessages(); + _agentRecoveryInProgress = true; + (async () => { + try { + await recoverAgentGenerations(); + } finally { + _agentRecoveryInProgress = false; + } + })(); + return; + } + // 情况3:只有 pendingMessage 但没有 LLM task(LLM 还没创建就断了)→ 提示重新发送 + if(pendingText && agentState.messages.length){ + const lastMsg = agentState.messages[agentState.messages.length - 1]; + if(lastMsg && lastMsg.role === 'user'){ + agentState.messages.push({ + id: uid('am'), + role: 'assistant', + text: '⚠️ ' + (tr('smart.agentInterrupted') || '上次操作被中断,请重新发送'), + options: [{label: tr('smart.agentRetry') || '重新发送', value: pendingText}], + generations: [], + ts: Date.now() + }); + } + } + delete agentState._pendingMessage; + delete agentState._pendingAttachments; + delete agentState._pendingUserMsg; + delete agentState._pendingLlmTaskId; } function saveAgentState(){ clearTimeout(agentSaveTimer); @@ -17445,6 +17552,101 @@ function parseAgentResponse(raw, lastUserText){ } return {reply:text, generations:[]}; } +// 处理 LLM 返回结果:解析、兜底、创建 assistant 消息、运行生图 +// 提取为独立函数,以便刷新恢复时复用 +async function processAgentLlmResult(result, text, attachments, userMsg){ + const parsed = parseAgentResponse(result.text || '', text); + // 生图意图兜底 + 修改意图检测 + { + const lastUser = [...(agentState.messages || [])].reverse().find(m => m.role === 'user'); + if(lastUser && lastUser.text && parsed.reply){ + const userText = String(lastUser.text || '').trim(); + const replyText = String(parsed.reply || ''); + const genPrompt = extractGenPrompt(replyText); + // 修改/转换意图检测 + const userModifyRe = /改成|转换成|换成|修改为|变成|转为|改为|转成|调整为|修改成|变回|调成|重新画|重画|重新生成|修改一下|改一下|调整一下/i; + const replyModifyRe = /为您(?:将|把).{0,30}?(?:转换|改成|换成|修改|变成|调整|转为|调成|重新画|重画)|(?:将|把).{0,20}?(?:转换|改成|换成|修改|变成).{0,10}?(?:风格|效果|版本|色调)/i; + const hasUserModifyIntent = userModifyRe.test(userText); + const hasReplyModify = replyModifyRe.test(replyText); + let isModifyScenario = hasUserModifyIntent || hasReplyModify; + // 修复链路断裂:如果上一条 assistant 有 prompts(确认模式),且当前用户消息是确认/生成意图 + // 则继承上上条用户消息的修改意图 + if(!isModifyScenario){ + const msgs = agentState.messages || []; + // 找到最后一条 assistant 消息 + for(let i = msgs.length - 1; i >= 0; i--){ + if(msgs[i].role === 'assistant'){ + const prevAssistant = msgs[i]; + // 如果上一条 assistant 有 prompts,说明是确认模式 + if(Array.isArray(prevAssistant.prompts) && prevAssistant.prompts.length > 0){ + // 当前用户消息是确认/生成意图 + const confirmRe = /^\s*(确认|生成|好的|好|可以|没问题|就这样|执行|继续|1|yes|ok)\s*$/i; + if(confirmRe.test(userText)){ + // 找到上上条用户消息(即提出修改需求的那条) + for(let j = i - 1; j >= 0; j--){ + if(msgs[j].role === 'user'){ + const prevUserText = String(msgs[j].text || '').trim(); + if(userModifyRe.test(prevUserText)){ + isModifyScenario = true; + // 如果当前消息没有明确 prompt,使用上上条的修改需求作为 prompt + break; + } + } + } + } + } + break; + } + } + } + if(parsed.generations.length === 0){ + // 场景A:LLM 没返回 generations,需要兜底构造 + const genInProgressRe = /正在生成|正在为你生成|正在为您生成|生成中|开始生成|马上生成|这就为你生成|这就为您生成|好的[,,]?\s*我来生成|好的[,,]?\s*马上|我将为你生成|我将为您生成|我来为你生成|我来为您生成|正在为你创建|正在为您创建|正在画|正在创建/i; + const userGenIntentRe = /我要生成|帮我生成|帮我画|画一|生成一|创建一|制作一|来一张|来幅|来张|给我画|给我生成|帮我创建|帮我做/i; + const meaninglessConfirmRe = /确认要生成|确认生成|确认要画|要为您生成.*吗|要生成.*吗|确认.*吗.*[??]/i; + const noOptions = !parsed.options || parsed.options.length === 0; + const hasGenInProgress = genInProgressRe.test(replyText); + const hasUserGenIntent = noOptions && userGenIntentRe.test(userText); + const hasMeaninglessConfirm = noOptions && meaninglessConfirmRe.test(replyText); + const hasAnyIntent = hasGenInProgress || hasUserGenIntent || isModifyScenario || hasMeaninglessConfirm; + if(hasAnyIntent){ + const finalPrompt = genPrompt || userText; + parsed.generations = [{ + prompt: finalPrompt, + count: 1, + use_last_outputs: isModifyScenario, + use_attachments: !!(lastUser.images && lastUser.images.length), + results: [], + status: 'running' + }]; + if(hasGenInProgress && !isModifyScenario && !hasMeaninglessConfirm){ + parsed.reply = tr('smart.agentGenerating') || '正在为您生成图片...'; + } + } + } else if(isModifyScenario){ + // 场景B:LLM 返回了 generations,但修改场景下强制设置 use_last_outputs: true + parsed.generations.forEach(g => { g.use_last_outputs = true; }); + } + } + } + // 如果用户点击了"重新生成提示词",强制将 generations 设为空数组 + const lastUserMsg = [...(agentState.messages || [])].reverse().find(m => m.role === 'user'); + if(lastUserMsg && String(lastUserMsg.text || '').includes('重新生成提示词')){ + parsed.generations = []; + } + // 批量完整性检查 + const requestedCount = chatRequestedImageCount(text); + if(requestedCount > 0 && parsed.generations.length > 0 && parsed.generations.length < requestedCount){ + parsed.reply += `${AGENT_NL}(注意:请求了 ${requestedCount} 张,但仅生成了 ${parsed.generations.length} 张的提示词)`; + } + const assistantMsg = {id:uid('am'), role:'assistant', text:parsed.reply, options:parsed.options || [], prompts:parsed.prompts || [], generations:parsed.generations, ts:Date.now()}; + agentState.messages.push(assistantMsg); + agentState.messages = agentState.messages.slice(-AGENT_MSG_MAX); + agentThinking = false; + renderAgentMessages(); + saveAgentState(); + if(assistantMsg.generations.length) await runAgentGenerations(assistantMsg, userMsg); +} async function sendAgentMessage(){ if(agentSending || !agentState) return; const text = String(agentInput?.value || '').trim(); @@ -17466,6 +17668,7 @@ async function sendAgentMessage(){ // 保存待处理消息,刷新后可恢复 agentState._pendingMessage = text; agentState._pendingAttachments = attachments.slice(); + agentState._pendingUserMsg = userMsg; renderAgentMessages(); saveAgentState(); const contextImages = attachments.slice(); @@ -17474,89 +17677,36 @@ async function sendAgentMessage(){ if(item?.url && contextImages.length < AGENT_LLM_IMAGE_MAX && !contextImages.some(i => i.url === item.url)) contextImages.push(item); }); } + const llmPayload = { + message:text || '(please help me edit these images)', + messages:agentHistoryMessages().slice(0, -1), + images:contextImages.slice(0, AGENT_LLM_IMAGE_MAX).map(i => i.url), + videos:[], + model, + provider, + ms_model:provider === 'modelscope' ? model : '', + system_prompt:agentSystemPrompt() + }; try { - const result = await fetch('/api/canvas-llm', { + // 创建后端 LLM 任务(息屏/刷新不会丢失) + const taskRes = await fetch('/api/agent-llm-task', { method:'POST', headers:{'Content-Type':'application/json'}, - body:JSON.stringify({ - message:text || '(please help me edit these images)', - messages:agentHistoryMessages().slice(0, -1), - images:contextImages.slice(0, AGENT_LLM_IMAGE_MAX).map(i => i.url), - videos:[], - model, - provider, - ms_model:provider === 'modelscope' ? model : '', - system_prompt:agentSystemPrompt() - }) + body:JSON.stringify(llmPayload) }).then(async r => { if(!r.ok) throw new Error(await responseErrorMessage(r, tr('smart.promptLlmFailed'))); return r.json(); }); - const parsed = parseAgentResponse(result.text || '', text); - // 生图意图兜底:当 LLM 不遵循 JSON 格式、没返回 generations 时, - // 根据生图/修改/确认意图自动构造生图任务 - // 同时:即使 LLM 返回了 generations,如果检测到修改意图但 use_last_outputs 未设置,也强制覆盖 - { - const lastUser = [...(agentState.messages || [])].reverse().find(m => m.role === 'user'); - if(lastUser && lastUser.text && parsed.reply){ - const userText = String(lastUser.text || '').trim(); - const replyText = String(parsed.reply || ''); - const genPrompt = extractGenPrompt(replyText); - // 修改/转换意图检测 - const userModifyRe = /改成|转换成|换成|修改为|变成|转为|改为|转成|调整为|修改成|变回|调成|重新画|重画|重新生成|修改一下|改一下|调整一下/i; - const replyModifyRe = /为您(?:将|把).{0,30}?(?:转换|改成|换成|修改|变成|调整|转为|调成|重新画|重画)|(?:将|把).{0,20}?(?:转换|改成|换成|修改|变成).{0,10}?(?:风格|效果|版本|色调)/i; - const hasUserModifyIntent = userModifyRe.test(userText); - const hasReplyModify = replyModifyRe.test(replyText); - const isModifyScenario = hasUserModifyIntent || hasReplyModify; - - if(parsed.generations.length === 0){ - // 场景A:LLM 没返回 generations,需要兜底构造 - const genInProgressRe = /正在生成|正在为你生成|正在为您生成|生成中|开始生成|马上生成|这就为你生成|这就为您生成|好的[,,]?\s*我来生成|好的[,,]?\s*马上|我将为你生成|我将为您生成|我来为你生成|我来为您生成|正在为你创建|正在为您创建|正在画|正在创建/i; - const userGenIntentRe = /我要生成|帮我生成|帮我画|画一|生成一|创建一|制作一|来一张|来幅|来张|给我画|给我生成|帮我创建|帮我做/i; - const meaninglessConfirmRe = /确认要生成|确认生成|确认要画|要为您生成.*吗|要生成.*吗|确认.*吗.*[??]/i; - const noOptions = !parsed.options || parsed.options.length === 0; - const hasGenInProgress = genInProgressRe.test(replyText); - const hasUserGenIntent = noOptions && userGenIntentRe.test(userText); - const hasMeaninglessConfirm = noOptions && meaninglessConfirmRe.test(replyText); - const hasAnyIntent = hasGenInProgress || hasUserGenIntent || isModifyScenario || hasMeaninglessConfirm; - if(hasAnyIntent){ - const finalPrompt = genPrompt || userText; - parsed.generations = [{ - prompt: finalPrompt, - count: 1, - use_last_outputs: isModifyScenario, - use_attachments: !!(lastUser.images && lastUser.images.length), - results: [], - status: 'running' - }]; - if(hasGenInProgress && !isModifyScenario && !hasMeaninglessConfirm){ - parsed.reply = tr('smart.agentGenerating') || '正在为您生成图片...'; - } - } - } else if(isModifyScenario){ - // 场景B:LLM 返回了 generations,但修改场景下强制设置 use_last_outputs: true - // 确保 GPT Image 2 走 /images/edits 而不是 /images/generations - parsed.generations.forEach(g => { g.use_last_outputs = true; }); - } - } - } - // 如果用户点击了"重新生成提示词",强制将 generations 设为空数组(只返回提示词,不生图) - const lastUserMsg = [...(agentState.messages || [])].reverse().find(m => m.role === 'user'); - if(lastUserMsg && String(lastUserMsg.text || '').includes('重新生成提示词')){ - parsed.generations = []; - } - // 批量完整性检查:用户请求 N 张但 generations 不足时提示 - const requestedCount = chatRequestedImageCount(text); - if(requestedCount > 0 && parsed.generations.length > 0 && parsed.generations.length < requestedCount){ - parsed.reply += `${AGENT_NL}(注意:请求了 ${requestedCount} 张,但仅生成了 ${parsed.generations.length} 张的提示词)`; - } - const assistantMsg = {id:uid('am'), role:'assistant', text:parsed.reply, options:parsed.options || [], prompts:parsed.prompts || [], generations:parsed.generations, ts:Date.now()}; - agentState.messages.push(assistantMsg); - agentState.messages = agentState.messages.slice(-AGENT_MSG_MAX); - agentThinking = false; - renderAgentMessages(); + const llmTaskId = taskRes.task_id; + if(!llmTaskId) throw new Error('Failed to create LLM task'); + // 保存 LLM task ID,刷新后可恢复 + agentState._pendingLlmTaskId = llmTaskId; saveAgentState(); - if(assistantMsg.generations.length) await runAgentGenerations(assistantMsg, userMsg); + // 等待 LLM 结果(WebSocket 实时通知 + 轮询保底) + const result = await pollAgentLlmTask(llmTaskId); + delete agentState._pendingLlmTaskId; + // 处理结果 + await processAgentLlmResult(result, text, attachments, userMsg); } catch(e) { agentThinking = false; agentState.messages.push({id:uid('am'), role:'assistant', text:`⚠️ ${String(e.message || e).slice(0, 300)}`, generations:[], ts:Date.now()}); @@ -17570,6 +17720,8 @@ async function sendAgentMessage(){ if(agentState._pendingMessage !== undefined){ delete agentState._pendingMessage; delete agentState._pendingAttachments; + delete agentState._pendingUserMsg; + delete agentState._pendingLlmTaskId; saveAgentState(); } renderAgentMessages(); @@ -17698,7 +17850,12 @@ async function runAgentGenerations(assistantMsg, userMsg){ if(!r.ok) throw new Error(await responseErrorMessage(r, tr('smart.agentGenFail'))); return r.json(); }))); - const results = await Promise.all(tasks.map(t => t.task_id).filter(Boolean).map(id => pollSmartCanvasTask(id))); + const imageTaskIds = tasks.map(t => t.task_id).filter(Boolean); + // 保存 task IDs 和 placeholderNodeId,以便刷新恢复 + gen.taskIds = imageTaskIds; + if(placeholderNode) gen.placeholderNodeId = placeholderNode.id; + saveAgentState(); + const results = await Promise.all(imageTaskIds.map(id => pollSmartCanvasTask(id))); const urls = results.flatMap(res => resultMediaUrls(res)).map((item, i) => { const url = typeof item === 'string' ? item : item?.url || ''; return {url, name:(typeof item === 'object' && item?.name) || `agent-${Date.now()}-${i + 1}.png`, kind:'image'}; @@ -17734,6 +17891,57 @@ async function runAgentGenerations(assistantMsg, userMsg){ scheduleSave(); })); } +// 恢复中断的 Agent 生图任务(页面刷新后调用) +async function recoverAgentGenerations(){ + if(!agentState?.messages) return; + const msgs = agentState.messages; + for(let i = msgs.length - 1; i >= 0; i--){ + const msg = msgs[i]; + if(msg.role !== 'assistant') continue; + const gens = msg.generations || []; + for(const gen of gens){ + if(gen.status !== 'running' || !gen.taskIds || !gen.taskIds.length) continue; + // 找到占位节点 + const placeholderNode = gen.placeholderNodeId ? nodes.find(n => n.id === gen.placeholderNodeId) : null; + if(placeholderNode){ + placeholderNode.runStartedAt = placeholderNode.runStartedAt || nowMs(); + placeholderNode.pending = gen.taskIds.length; + } + try { + const results = await Promise.all(gen.taskIds.map(id => pollSmartCanvasTask(id))); + const urls = results.flatMap(res => resultMediaUrls(res)).map((item, idx) => { + const url = typeof item === 'string' ? item : item?.url || ''; + return {url, name:(typeof item === 'object' && item?.name) || `agent-${Date.now()}-${idx + 1}.png`, kind:'image'}; + }).filter(i => i.url); + gen.results = urls; + gen.status = 'done'; + if(urls.length && placeholderNode){ + undoSuppressed = true; + placeholderNode.images = urls.map(u => ({...u})); + placeholderNode.pending = 0; + placeholderNode.title = urls.length > 1 ? 'Group' : 'Image'; + placeholderNode.runFinishedAt = nowMs(); + selectedId = placeholderNode.id; + undoSuppressed = false; + gen.results = gen.results.map((r, idx) => ({...r, nodeId: placeholderNode.id, nodeX: Number(placeholderNode.x) || 0, nodeY: Number(placeholderNode.y) || 0})); + } + } catch(e) { + gen.status = 'error'; + gen.error = String(e.message || e).slice(0, 200); + if(placeholderNode){ + undoSuppressed = true; + nodes = nodes.filter(n => n.id !== placeholderNode.id); + undoSuppressed = false; + } + } + renderAgentMessages(); + saveAgentState(); + render(); + scheduleSave(); + } + break; // 只恢复最后一条 assistant 消息的生图 + } +} function agentCanvasImages(){ const items = []; (nodes || []).forEach(node => { From 661ef38c8f6c8c8cd0fbe03d30cbebfea6e733ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Sat, 18 Jul 2026 23:41:54 +0800 Subject: [PATCH 28/67] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=94=9F=E5=9B=BE?= =?UTF-8?q?=E5=AE=8C=E6=88=90=E5=90=8E=E4=BB=8D=E6=98=BE=E7=A4=BA=E6=AD=A3?= =?UTF-8?q?=E5=9C=A8=E7=94=9F=E5=9B=BE=E4=B8=AD+=E5=A4=8D=E5=88=B6?= =?UTF-8?q?=E5=AF=B9=E8=AF=9D=E6=A1=86=E4=BC=98=E5=85=88=E5=A4=8D=E5=88=B6?= =?UTF-8?q?prompt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/js/smart-canvas.js | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/static/js/smart-canvas.js b/static/js/smart-canvas.js index a8b3e192a..8f6a33e65 100644 --- a/static/js/smart-canvas.js +++ b/static/js/smart-canvas.js @@ -17173,7 +17173,12 @@ function renderAgentMessages(){ btn.onclick = e => { e.stopPropagation(); const msg = (agentState?.messages || []).find(m => m.id === btn.dataset.agentCopy); - if(msg?.text) agentCopyMessage(msg.text); + if(!msg) return; + // 优先复制生图 prompt,其次复制消息文本 + const gens = msg.generations || []; + const firstGen = gens.find(g => g.prompt); + const text = firstGen ? firstGen.prompt : msg.text; + if(text) agentCopyMessage(text); }; }); agentMessages.querySelectorAll('[data-agent-retry]').forEach(btn => { @@ -17610,19 +17615,18 @@ async function processAgentLlmResult(result, text, attachments, userMsg){ const hasMeaninglessConfirm = noOptions && meaninglessConfirmRe.test(replyText); const hasAnyIntent = hasGenInProgress || hasUserGenIntent || isModifyScenario || hasMeaninglessConfirm; if(hasAnyIntent){ - const finalPrompt = genPrompt || userText; - parsed.generations = [{ - prompt: finalPrompt, - count: 1, - use_last_outputs: isModifyScenario, - use_attachments: !!(lastUser.images && lastUser.images.length), - results: [], - status: 'running' - }]; - if(hasGenInProgress && !isModifyScenario && !hasMeaninglessConfirm){ - parsed.reply = tr('smart.agentGenerating') || '正在为您生成图片...'; + const finalPrompt = genPrompt || userText; + parsed.generations = [{ + prompt: finalPrompt, + count: 1, + use_last_outputs: isModifyScenario, + use_attachments: !!(lastUser.images && lastUser.images.length), + results: [], + status: 'running' + }]; + // 不覆盖 parsed.reply,保留 LLM 原始回复 + // generation card 会独立显示状态和 prompt } - } } else if(isModifyScenario){ // 场景B:LLM 返回了 generations,但修改场景下强制设置 use_last_outputs: true parsed.generations.forEach(g => { g.use_last_outputs = true; }); From b88ae5bfd5702fdc2c50f94ef48a6df554cfcadd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Sat, 18 Jul 2026 23:48:44 +0800 Subject: [PATCH 29/67] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dprompt=E5=90=AB?= =?UTF-8?q?=E6=96=87=E5=AD=97=E5=AF=BC=E8=87=B4=E7=94=BB=E9=9D=A2=E5=87=BA?= =?UTF-8?q?=E7=8E=B0=E6=A0=87=E9=A2=98+=E6=8F=90=E7=A4=BA=E8=AF=8D?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E8=A1=8C=E6=95=B0=E5=A2=9E=E5=8A=A0=E5=88=B0?= =?UTF-8?q?5=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/smart-canvas.css | 2 +- static/js/smart-canvas.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/static/css/smart-canvas.css b/static/css/smart-canvas.css index b62598042..9bbc14b82 100644 --- a/static/css/smart-canvas.css +++ b/static/css/smart-canvas.css @@ -1220,7 +1220,7 @@ input[type=range].smart-range::-webkit-slider-thumb { -webkit-appearance:none; w .agent-msg-thumbs img { width:100%; aspect-ratio:1/1; object-fit:cover; border-radius:9px; border:1px solid var(--line); cursor:pointer; display:block; } .agent-gen-card { width:250px; max-width:100%; border-radius:12px; border:1px solid var(--line); background:var(--card); padding:8px 10px; display:flex; flex-direction:column; gap:6px; } .agent-gen-prompt { font-size:10px; color:var(--muted); line-height:1.45; position:relative; } -.agent-gen-prompt-collapsed { display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical; overflow:hidden; } +.agent-gen-prompt-collapsed { display:-webkit-box; -webkit-line-clamp:5; -webkit-box-orient:vertical; overflow:hidden; } .agent-gen-prompt-expanded { /* 展开后不截断 */ } .agent-gen-prompt-toggle { display:inline-block; margin-left:4px; padding:0 2px; border:none; background:none; color:var(--strong); font-size:10px; font-weight:700; cursor:pointer; vertical-align:baseline; white-space:nowrap; } .agent-gen-prompt-toggle:hover { text-decoration:underline; } diff --git a/static/js/smart-canvas.js b/static/js/smart-canvas.js index 8f6a33e65..738bf6fe2 100644 --- a/static/js/smart-canvas.js +++ b/static/js/smart-canvas.js @@ -16754,6 +16754,7 @@ Fields: "reply"=对话回复; "options"=[{label,value}]按钮; "prompts"=待确 - 关键信息缺失无法给选项 → 问1-3个短问题(reply) - 用户要多张不同图 → generations放多项 - 所有prompt必须中文,包含主体/风格/构图/光线/色彩/细节/氛围 +- prompt中绝对不要包含任何文字内容、标题、对白、台词、旁白、字幕。生图模型会把prompt中出现的文字渲染到画面里。prompt只描述画面视觉元素 - 用户上传角色参考图 → 后续生成保持角色一致性 - 有skill文档时遵循其风格规则 - 不要只问"确认要生成吗"而不给完整prompt From 9011d548d75be1029bf7f0e1d38b372dd141c070 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Sat, 18 Jul 2026 23:53:36 +0800 Subject: [PATCH 30/67] =?UTF-8?q?=E6=96=87=E5=AD=97=E8=A7=84=E5=88=99?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E6=8C=89=E9=9C=80=E5=88=A4=E6=96=AD=EF=BC=9A?= =?UTF-8?q?skill=E6=88=96=E7=94=A8=E6=88=B7=E6=98=8E=E7=A1=AE=E8=A6=81?= =?UTF-8?q?=E6=B1=82=E6=97=B6=E6=89=8D=E5=9C=A8prompt=E4=B8=AD=E5=8C=85?= =?UTF-8?q?=E5=90=AB=E6=96=87=E5=AD=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/js/smart-canvas.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/js/smart-canvas.js b/static/js/smart-canvas.js index 738bf6fe2..02321ebf2 100644 --- a/static/js/smart-canvas.js +++ b/static/js/smart-canvas.js @@ -16754,7 +16754,7 @@ Fields: "reply"=对话回复; "options"=[{label,value}]按钮; "prompts"=待确 - 关键信息缺失无法给选项 → 问1-3个短问题(reply) - 用户要多张不同图 → generations放多项 - 所有prompt必须中文,包含主体/风格/构图/光线/色彩/细节/氛围 -- prompt中绝对不要包含任何文字内容、标题、对白、台词、旁白、字幕。生图模型会把prompt中出现的文字渲染到画面里。prompt只描述画面视觉元素 +- 文字规则:默认情况下prompt不要包含文字内容(标题、对白、台词、旁白、字幕),只描述画面视觉元素;以下情况可以包含文字:1.skill文档明确要求画面中有文字(如标题、批注、标语);2.用户明确要求"加文字/标题/标语/配文"。当需要文字时用引号标注,如:画面顶部有文字"标题内容" - 用户上传角色参考图 → 后续生成保持角色一致性 - 有skill文档时遵循其风格规则 - 不要只问"确认要生成吗"而不给完整prompt From c5f47bc6943f8a8ca75067c140a3f5a400aac592 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Sun, 19 Jul 2026 00:05:33 +0800 Subject: [PATCH 31/67] =?UTF-8?q?=E4=BF=AE=E5=A4=8D:=20Agent=E4=B8=BB?= =?UTF-8?q?=E4=BD=93=E6=9B=B4=E6=8D=A2=E5=A4=B1=E8=B4=A5+=E5=8D=A0?= =?UTF-8?q?=E4=BD=8D/=E6=9C=80=E7=BB=88=E5=9B=BE=E7=89=87=E6=AF=94?= =?UTF-8?q?=E4=BE=8B=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题1: 企鹅换成鲨鱼失败 - 修改场景盲目强制use_last_outputs=true导致编辑API保留原图主体; 修复: 系统提示词区分风格修改vs主体更换, 场景B信任LLM决策, 场景A添加风格词检测 问题2: 占位图和最终图片使用第一次选择的比例 - agentPendingBoxSize优先用参考图尺寸; 修复: 始终用选中比例, 生图后delete w/h让节点重新计算(和主画布一致) --- static/js/smart-canvas.js | 58 +++++++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/static/js/smart-canvas.js b/static/js/smart-canvas.js index 02321ebf2..79a00f6f0 100644 --- a/static/js/smart-canvas.js +++ b/static/js/smart-canvas.js @@ -16760,12 +16760,24 @@ Fields: "reply"=对话回复; "options"=[{label,value}]按钮; "prompts"=待确 - 不要只问"确认要生成吗"而不给完整prompt 修改场景规则(非常重要): -当用户要求修改上一张图时(如"换成像素风"、"改成卡通"、"背景换成蓝色"),必须严格遵守: -1. use_last_outputs必须为true -2. 保持原图的主体、姿态、构图完全不变,只修改用户指定的部分 -3. prompt应简洁聚焦,只描述要修改的内容+保持原图其他部分不变 -4. 绝对不要添加用户没要求的内容,特别是:动画帧、序列帧、多角度视图、动作分解、分镜 -5. 例如用户说"换成像素风",prompt应该是"将原图转换为像素艺术风格,保持原有的主体、姿态、构图和色彩不变,仅改变渲染风格为像素化",而不是扩写成包含动画帧等无关内容的长描述`; +当用户要求修改上一张图时,必须区分两种情况: + +A. 风格/属性修改(use_last_outputs必须为true): + - 用户只想改变画风、色调、背景、光影等属性,主体不变 + - 例如:"换成像素风"、"改成卡通"、"背景换成蓝色"、"调成暖色调" + - prompt应简洁聚焦:只描述要修改的内容+保持原图其他部分不变 + - 例如用户说"换成像素风",prompt应该是"将原图转换为像素艺术风格,保持原有的主体、姿态、构图和色彩不变,仅改变渲染风格为像素化" + +B. 主体更换(use_last_outputs必须为false): + - 用户要把图片中的主体换成另一个完全不同的主体 + - 例如:"企鹅换成鲨鱼"、"把猫变成狗"、"换成一只老鹰" + - 此时不要引用上一轮图片,因为编辑API会保留原图主体导致更换失败 + - prompt应完整描述新主体的全部细节,作为全新生成处理 + - 例如用户说"企鹅换成鲨鱼",prompt应该是"鲨鱼在清澈海水中游泳,流线型身体,锋利的牙齿,深蓝色海洋背景,光影穿透水面",而不是"把企鹅改成鲨鱼" + +通用规则: +1. 绝对不要添加用户没要求的内容,特别是:动画帧、序列帧、多角度视图、动作分解、分镜 +2. 判断依据:如果新主体和原主体是不同的物体/生物/人物 → 主体更换(B);如果只是改变同一个主体的呈现方式 → 风格修改(A)`; let agentOpen = false; let agentSending = false; let agentThinking = false; @@ -17615,12 +17627,17 @@ async function processAgentLlmResult(result, text, attachments, userMsg){ const hasUserGenIntent = noOptions && userGenIntentRe.test(userText); const hasMeaninglessConfirm = noOptions && meaninglessConfirmRe.test(replyText); const hasAnyIntent = hasGenInProgress || hasUserGenIntent || isModifyScenario || hasMeaninglessConfirm; + // 主体更换检测:区分"换成像素风"(风格修改→edit)和"企鹅换成鲨鱼"(主体更换→new gen) + const styleWordsRe = /像素|卡通|动漫|写实|油画|水彩|水墨|素描|3d|2d|赛博|蒸汽波|极简|扁平|矢量|霓虹|复古|怀旧|黑白|彩色|暖色|冷色|色调|风格|画风|效果|版本|背景|光影|渲染|手绘|厚涂|薄涂|赛璐璐|哥特|巴洛克|印象派|超现实|波普|抽象|极繁|lowpoly| voxel|点彩|工笔|白描|版画|木刻|剪纸|泥塑|陶艺|蜡笔|马克笔|钢笔|铅笔|粉彩|色粉|丙烯|坦培拉|壁画|浮世绘|年画|春画|浮世|绘本|童画|童话|绘本风|童画风|q版|q版|萌系|治愈系|暗黑系|恐怖谷|超现实|魔幻|科幻|废土|末日|末世|赛博|蒸汽|柴油|原子|太空|星际|深海|丛林|沙漠|雪原|火山|冰川|天空|云端|海底|地底|星空|极光|彩虹|闪电|暴风|龙卷|海啸|地震|火山喷发|陨石|黑洞|星云|银河|宇宙/i; + const isStyleModify = isModifyScenario && styleWordsRe.test(userText); + // use_last_outputs 仅在风格修改时为 true;主体更换时为 false(全新生成) + const fallbackUseLastOutputs = isStyleModify; if(hasAnyIntent){ const finalPrompt = genPrompt || userText; parsed.generations = [{ prompt: finalPrompt, count: 1, - use_last_outputs: isModifyScenario, + use_last_outputs: fallbackUseLastOutputs, use_attachments: !!(lastUser.images && lastUser.images.length), results: [], status: 'running' @@ -17629,8 +17646,15 @@ async function processAgentLlmResult(result, text, attachments, userMsg){ // generation card 会独立显示状态和 prompt } } else if(isModifyScenario){ - // 场景B:LLM 返回了 generations,但修改场景下强制设置 use_last_outputs: true - parsed.generations.forEach(g => { g.use_last_outputs = true; }); + // 场景B:LLM 返回了 generations。 + // 不再盲目强制 use_last_outputs: true —— LLM 会根据系统提示词区分: + // 风格修改(use_last_outputs: true)vs 主体更换(use_last_outputs: false) + // 仅当 LLM 没有明确设置 use_last_outputs 时,作为兜底设为 true + parsed.generations.forEach(g => { + if(g.use_last_outputs === undefined || g.use_last_outputs === null){ + g.use_last_outputs = true; + } + }); } } } @@ -17771,11 +17795,9 @@ function agentPendingBoxSize(count, options={}){ const ratioSize = apiImageSize(agentState.genRatio || 'square', agentState.genResolution || '1k') || '1024x1024'; const parsed = parseSizeValue(ratioSize); const requestSize = parsed ? {w:Number(parsed.width) || 1024, h:Number(parsed.height) || 1024} : {w:1024, h:1024}; - // 如果有参考图/源节点,优先用参考图尺寸(和主画布一致) - const sourceSize = pendingSourceBoxSize(options); - if(sourceSize?.display) return {w:sourceSize.w, h:sourceSize.h}; - if(sourceSize) return displayBoxFromNaturalSize(sourceSize); - // 用请求尺寸算显示尺寸(复用主画布的 singleImageLayout) + // Agent 始终用选中比例计算占位尺寸,不使用参考图尺寸。 + // 因为 Agent 总是发送 size 参数(基于选中比例),占位应与最终生成尺寸一致。 + // 参考图只用于内容编辑,不影响输出尺寸。 const base = displayBoxFromNaturalSize(requestSize); // 多张图时按网格排列(和主画布的 pendingBoxSize 完全一致) const c = Math.max(1, Number(count) || 1); @@ -17875,6 +17897,10 @@ async function runAgentGenerations(assistantMsg, userMsg){ placeholderNode.title = urls.length > 1 ? 'Group' : 'Image'; // 修复:设置计时结束时间 placeholderNode.runFinishedAt = nowMs(); + // 修复:删除占位尺寸,让节点根据实际图片自然尺寸重新计算(和主画布 finalizePendingNode 一致) + placeholderNode.scale = mediaNodeDefaultScale(placeholderNode); + delete placeholderNode.w; + delete placeholderNode.h; selectedId = placeholderNode.id; undoSuppressed = false; // 保存节点 ID 到 results,用于点击跳转 @@ -17926,6 +17952,10 @@ async function recoverAgentGenerations(){ placeholderNode.pending = 0; placeholderNode.title = urls.length > 1 ? 'Group' : 'Image'; placeholderNode.runFinishedAt = nowMs(); + // 修复:删除占位尺寸,让节点根据实际图片自然尺寸重新计算 + placeholderNode.scale = mediaNodeDefaultScale(placeholderNode); + delete placeholderNode.w; + delete placeholderNode.h; selectedId = placeholderNode.id; undoSuppressed = false; gen.results = gen.results.map((r, idx) => ({...r, nodeId: placeholderNode.id, nodeX: Number(placeholderNode.x) || 0, nodeY: Number(placeholderNode.y) || 0})); From 63524c7d93b06fb72f151240afecadb0ca8f5704 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Sun, 19 Jul 2026 00:18:10 +0800 Subject: [PATCH 32/67] =?UTF-8?q?=E4=BF=AE=E5=A4=8D:=20=E4=B8=BB=E4=BD=93?= =?UTF-8?q?=E6=9B=B4=E6=8D=A2=E5=9C=BA=E6=99=AF=E4=B8=A2=E5=A4=B1=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主体更换(龙换马)改为use_last_outputs:true引用原图保场景 + 明确替换提示词让模型换主体, 不再走全新生成路径 --- static/js/smart-canvas.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/static/js/smart-canvas.js b/static/js/smart-canvas.js index 79a00f6f0..fa5595b25 100644 --- a/static/js/smart-canvas.js +++ b/static/js/smart-canvas.js @@ -16768,16 +16768,18 @@ A. 风格/属性修改(use_last_outputs必须为true): - prompt应简洁聚焦:只描述要修改的内容+保持原图其他部分不变 - 例如用户说"换成像素风",prompt应该是"将原图转换为像素艺术风格,保持原有的主体、姿态、构图和色彩不变,仅改变渲染风格为像素化" -B. 主体更换(use_last_outputs必须为false): +B. 主体更换(use_last_outputs必须为true): - 用户要把图片中的主体换成另一个完全不同的主体 - - 例如:"企鹅换成鲨鱼"、"把猫变成狗"、"换成一只老鹰" - - 此时不要引用上一轮图片,因为编辑API会保留原图主体导致更换失败 - - prompt应完整描述新主体的全部细节,作为全新生成处理 - - 例如用户说"企鹅换成鲨鱼",prompt应该是"鲨鱼在清澈海水中游泳,流线型身体,锋利的牙齿,深蓝色海洋背景,光影穿透水面",而不是"把企鹅改成鲨鱼" + - 例如:"企鹅换成鲨鱼"、"龙换成马"、"把猫变成狗" + - 重要:必须引用上一轮图片(use_last_outputs: true),以保留原图的场景、背景、构图 + - prompt必须明确指示"替换主体+保留场景",格式:"将原图中的[原主体]替换为[新主体],保持原有的场景、背景、构图、光影、色彩和氛围完全不变,仅改变主体本身,新主体出现在原来主体的位置,保持相似的姿态和构图" + - 例如用户说"龙换成马",prompt应该是"将原图中的龙替换为一匹马,保持原有的场景、背景、构图、光影、色彩和氛围完全不变,仅将主体从龙改为马,马出现在原来龙的位置,保持相似的姿态" + - 绝对不要写成全新生成的描述(如"马在草原上奔跑"),那样会丢失原图场景 通用规则: 1. 绝对不要添加用户没要求的内容,特别是:动画帧、序列帧、多角度视图、动作分解、分镜 -2. 判断依据:如果新主体和原主体是不同的物体/生物/人物 → 主体更换(B);如果只是改变同一个主体的呈现方式 → 风格修改(A)`; +2. 判断依据:如果新主体和原主体是不同的物体/生物/人物 → 主体更换(B);如果只是改变同一个主体的呈现方式 → 风格修改(A) +3. 无论A还是B,都使用use_last_outputs: true引用原图,区别在于prompt的写法不同`; let agentOpen = false; let agentSending = false; let agentThinking = false; @@ -17627,11 +17629,9 @@ async function processAgentLlmResult(result, text, attachments, userMsg){ const hasUserGenIntent = noOptions && userGenIntentRe.test(userText); const hasMeaninglessConfirm = noOptions && meaninglessConfirmRe.test(replyText); const hasAnyIntent = hasGenInProgress || hasUserGenIntent || isModifyScenario || hasMeaninglessConfirm; - // 主体更换检测:区分"换成像素风"(风格修改→edit)和"企鹅换成鲨鱼"(主体更换→new gen) - const styleWordsRe = /像素|卡通|动漫|写实|油画|水彩|水墨|素描|3d|2d|赛博|蒸汽波|极简|扁平|矢量|霓虹|复古|怀旧|黑白|彩色|暖色|冷色|色调|风格|画风|效果|版本|背景|光影|渲染|手绘|厚涂|薄涂|赛璐璐|哥特|巴洛克|印象派|超现实|波普|抽象|极繁|lowpoly| voxel|点彩|工笔|白描|版画|木刻|剪纸|泥塑|陶艺|蜡笔|马克笔|钢笔|铅笔|粉彩|色粉|丙烯|坦培拉|壁画|浮世绘|年画|春画|浮世|绘本|童画|童话|绘本风|童画风|q版|q版|萌系|治愈系|暗黑系|恐怖谷|超现实|魔幻|科幻|废土|末日|末世|赛博|蒸汽|柴油|原子|太空|星际|深海|丛林|沙漠|雪原|火山|冰川|天空|云端|海底|地底|星空|极光|彩虹|闪电|暴风|龙卷|海啸|地震|火山喷发|陨石|黑洞|星云|银河|宇宙/i; - const isStyleModify = isModifyScenario && styleWordsRe.test(userText); - // use_last_outputs 仅在风格修改时为 true;主体更换时为 false(全新生成) - const fallbackUseLastOutputs = isStyleModify; + // 修改场景(风格修改+主体更换)都使用 use_last_outputs: true 引用原图 + // 区别在于 prompt 写法:风格修改→描述风格变化;主体更换→明确指示替换主体+保留场景 + const fallbackUseLastOutputs = isModifyScenario; if(hasAnyIntent){ const finalPrompt = genPrompt || userText; parsed.generations = [{ From cbfcd0525440a27b92ad2ff9e1d8549bfc798105 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Sun, 19 Jul 2026 00:27:40 +0800 Subject: [PATCH 33/67] =?UTF-8?q?=E4=BF=AE=E5=A4=8D:=20Agent=E5=B9=B6?= =?UTF-8?q?=E5=8F=91=E7=94=9F=E6=88=90=E5=A4=9A=E5=BC=A0=E5=9B=BE=E6=97=B6?= =?UTF-8?q?=E5=8D=A0=E4=BD=8D=E8=8A=82=E7=82=B9=E5=8F=A0=E5=9C=A8=E4=B8=80?= =?UTF-8?q?=E8=B5=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因: agentFindEmptyPosition只查找有图片URL的节点,占位节点images为空被排除,导致并发创建的占位节点都放在同一位置; 修复: 过滤条件增加pending>0的占位节点 --- static/js/smart-canvas.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/static/js/smart-canvas.js b/static/js/smart-canvas.js index fa5595b25..b4784ab2d 100644 --- a/static/js/smart-canvas.js +++ b/static/js/smart-canvas.js @@ -17772,7 +17772,8 @@ function agentCenterOnNode(node){ } function agentFindEmptyPosition(count=1){ // A+C 方案:计算空白区域 + 右侧追加(水平排列,顶部对齐) - const imageNodes = (nodes || []).filter(n => isSmartImageNode(n) && (n.images || []).some(img => img?.url)); + // 注意:必须包含 pending 状态的占位节点,否则并发生成多张图时占位节点会叠在一起 + const imageNodes = (nodes || []).filter(n => isSmartImageNode(n) && ((n.images || []).some(img => img?.url) || Number(n.pending) > 0)); const center = viewportCenter(); if(!imageNodes.length) return {x:center.x, y:center.y}; // 找到最右边的节点 From aa8335ff628087001e9521cd94bf497ec79dd19f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Sun, 19 Jul 2026 00:36:47 +0800 Subject: [PATCH 34/67] =?UTF-8?q?docs:=20=E7=89=88=E6=9C=AC=E5=8F=B7?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=B8=BAv1.1,=20=E6=B7=BB=E5=8A=A0v1.0/v1.1?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E6=9B=B4=E6=96=B0=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 38 +++++++++++++++++++++++++++++++++++++- VERSION | 2 +- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index abb78d8c8..047ac967c 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,10 @@ Supports comfyui/API calls/modelscope calls ## 本分支改动说明(AI Agent 版) > 本仓库是基于原项目 [hero8152/Infinite-Canvas](https://github.com/hero8152/Infinite-Canvas) 的二次开发 fork,新增了智能画布 AI Agent 面板。 +> +> **当前版本:v1.1**(查看 [版本更新日志](#版本更新日志)) -### 新增功能 +### v1.0 新增功能(AI Agent 面板基线) 1. AI Agent 侧边聊天面板(OneBox 风格),可与画布联动生图。 2. `@` 引用当前画布中的图片,最新的图片排在最上面。 3. 统一附件管理:Skill 文档与图片在同一处增删改查,支持一次上传多个。 @@ -19,6 +21,33 @@ Supports comfyui/API calls/modelscope calls 9. 生图占位:生成开始时先在画布放置占位骨架,完成后替换为实际图片;多张并发生成、顶部对齐向右排列。 10. 在画布选中图片的悬浮工具栏最左侧新增「发送至 Agent」按钮。 +### v1.1 更新内容 + +#### 一、Agent 对话与意图理解 +1. **系统提示词重写**:精简为 5 种对话模式(直接生成 / 确认 / 多选 / 澄清 / 批量),强制中文提示词,避免 LLM 返回纯文字不生图。 +2. **修改场景智能区分**:区分「风格修改」(换成像素风 → 引用原图编辑)与「主体更换」(龙换成马 → 引用原图替换主体并保留场景),给出不同的 prompt 模板,避免主体换不掉或场景丢失。 +3. **文字规则按需判断**:prompt 默认不含文字(标题/对白/字幕),仅在 skill 文档明确要求或用户主动要求「加文字/标题」时才用引号标注文字内容。 +4. **非 JSON 回复兜底解析**:LLM 未遵循 JSON 格式时,自动提取编号选项与澄清问题,保证交互不中断。 +5. **修改意图链路修复**:确认模式下用户点击「确认」后,正确继承上一轮的修改意图,不再断裂。 + +#### 二、生图稳定性与架构 +6. **LLM 任务后端化**:新增 `/api/agent-llm-task` 后端任务系统,LLM 推理在后端运行,前端通过 WebSocket 实时通知 + 轮询保底获取结果。页面刷新 / 息屏不再丢失思考状态。 +7. **生图任务恢复**:刷新后自动恢复后端仍在运行的生图任务(通过 `taskIds` + `placeholderNodeId`),不丢失已提交的生图。 +8. **WebSocket 实时通知**:生图完成时后端主动推送 `canvas_task_done` / `agent_llm_done`,前端无需等待轮询间隔,降低延迟。 +9. **agy CLI 生图修复**:Antigravity CLI 返回纯文字无法生图时,自动回退到 `gpt-image-2-skill` 执行生图。 + +#### 三、画布占位与比例 +10. **占位按选中比例展示**:占位节点始终按用户当前选择的比例(1:1 / 16:9 / 9:16 等)计算尺寸,不再错误使用参考图比例。 +11. **生图后尺寸重算**:生图完成后删除占位的 `w/h`,让节点按实际图片自然尺寸重新计算(与主画布 `finalizePendingNode` 行为一致),双击放大与画布显示比例一致。 +12. **占位顶部对齐**:新占位节点与已有图片节点顶部对齐,不再阶梯式错位。 +13. **占位计时修复**:占位节点显示正确的正计时(`runStartedAt` → `runFinishedAt`),不再卡在 0。 +14. **并发多图不叠加**:修复一次性生成多张图时占位节点叠在同一位置的问题(位置计算纳入 pending 占位节点)。 + +#### 四、交互体验 +15. **提示词展开/收起**:Agent 生图卡片支持点击展开完整 prompt,收起时显示 5 行。 +16. **复制优先 prompt**:复制 Agent 消息时优先复制生图 prompt,而非「正在生图中」状态文字。 +17. **多选场景自由输入提示**:仅在需要用户多选/澄清时显示「也可直接输入」提示,不常驻。 + ### 重要警示 - **已关闭自动更新**:导航栏版本号显示为「Agent版(无自动更新)」。若从上游拉取/合并最新代码,可能覆盖本分支的 AI Agent 功能,请谨慎操作并先备份。 - **main 分支与上游分叉**:本 fork 的 `main` 通过强制推送覆盖,历史与上游不一致,切勿直接向上游发起合并。 @@ -27,6 +56,13 @@ Supports comfyui/API calls/modelscope calls > This fork adds an AI Agent panel to the canvas. Auto-update is disabled; pulling upstream changes may overwrite the Agent features. The `main` branch was force-pushed and diverges from upstream. Do not commit API keys. Commercial use remains prohibited. +### 版本更新日志 + +| 版本 | 日期 | 说明 | +|------|------|------| +| v1.0 | 2026-07-18 | AI Agent 面板基线:聊天面板、画布联动生图、多对话管理、参数面板、结构化确认流程、生图占位 | +| v1.1 | 2026-07-19 | 系统提示词重写、修改场景智能区分、LLM 任务后端化、刷新恢复、WebSocket 实时通知、占位比例修复、并发多图不叠加、提示词展开收起 | + ---- 配套的chrome采集插件已经上线:https://chromewebstore.google.com/detail/infinite-canvas-%E5%9B%BE%E5%83%8F%E8%A7%86%E9%A2%91%E6%96%87%E5%AD%97%E6%8A%93%E5%8F%96%E5%B7%A5/ajfhnbklbmpfaaookhfakohabnpmlcic?authuser=0&hl=en diff --git a/VERSION b/VERSION index 4d773bbce..3a6a8d03e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2026.07.15 \ No newline at end of file +v1.1 From 7f21dff2503c99ffe426d65a5ced7fcdba4738a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Sun, 19 Jul 2026 00:43:14 +0800 Subject: [PATCH 35/67] =?UTF-8?q?chore:=20=E5=B0=86history.json=E5=8A=A0?= =?UTF-8?q?=E5=85=A5.gitignore,=20=E4=B8=8D=E5=86=8D=E8=B7=9F=E8=B8=AA?= =?UTF-8?q?=E8=BF=90=E8=A1=8C=E6=97=B6=E7=BC=93=E5=AD=98=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + history.json | 2206 -------------------------------------------------- 2 files changed, 1 insertion(+), 2206 deletions(-) delete mode 100644 history.json diff --git a/.gitignore b/.gitignore index 45ad51396..631b97999 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ data/ .DS_Store __pycache__/ *.pyc +history.json diff --git a/history.json b/history.json deleted file mode 100644 index 9ce4840ef..000000000 --- a/history.json +++ /dev/null @@ -1,2206 +0,0 @@ -[ - { - "prompt": "[generated 1 image(s) with prompt: 皮克斯3D动画风格的粉色小猪在阳光下奔跑,圆润的卡通造型搭配夸张的面部表情,草地呈现柔和的蓝绿色渐变,丁达尔光线效果转化为光粒子特效,超精细的毛发渲染(每根毛发都有高光),背景树木卡通化为柔和的墨绿色团块,16:9电影画幅带景深模糊,迪士尼皮克斯经典色彩管理]", - "images": [ - "/assets/output/online_f57ee16724.png" - ], - "image_items": [ - { - "url": "/assets/output/online_f57ee16724.png", - "kind": "image", - "name": "online_f57ee16724.png", - "natural_w": 1254, - "natural_h": 1254, - "width": 1254, - "height": 1254 - } - ], - "timestamp": 1784385800.8972008, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXTTS4QTJHHARYFSF31C8J52", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1024x1024", - "requested_size": "1024x1024", - "quality": "auto", - "n": 1, - "reference_images": [ - { - "url": "/assets/output/online_6c457f9d08.png", - "name": "online_6c457f9d08.png", - "role": "", - "kind": "", - "mime": "" - } - ] - }, - "raw_usage": null - }, - { - "prompt": "[generated 1 image(s) with prompt: 写实风格的棕色猪在阳光下的草地上奔跑,午后柔和的光线穿透云层形成光束,猪的身体前倾呈现奔跑姿态,泥蹄翻起草皮,远处有模糊的树木轮廓与狗场景相同,8K高清画质,极致细节刻画卷曲的猪毛与草地露珠,自然纪录片电影色调保持统一]", - "images": [ - "/assets/output/online_6c457f9d08.png" - ], - "image_items": [ - { - "url": "/assets/output/online_6c457f9d08.png", - "kind": "image", - "name": "online_6c457f9d08.png", - "natural_w": 1254, - "natural_h": 1254, - "width": 1254, - "height": 1254 - } - ], - "timestamp": 1784385521.2965388, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXTTGJ9D1V9B7ZYECPPH5G0A", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1024x1024", - "requested_size": "1024x1024", - "quality": "auto", - "n": 1, - "reference_images": [ - { - "url": "/assets/output/online_b5ddc6e732.png", - "name": "online_b5ddc6e732.png", - "role": "", - "kind": "", - "mime": "" - } - ] - }, - "raw_usage": null - }, - { - "prompt": "写实风格的棕色拉布拉多犬在阳光下的草地上奔跑,午后柔和的光线穿透云层形成光束,狗的毛发在风中飘动呈现动态模糊,草地上的蒲公英随风飘散,远处有模糊的树木轮廓,8K高清画质,极致细节刻画毛发与草地纹理,自然纪录片电影色调", - "images": [ - "/assets/output/online_b5ddc6e732.png" - ], - "image_items": [ - { - "url": "/assets/output/online_b5ddc6e732.png", - "kind": "image", - "name": "online_b5ddc6e732.png", - "natural_w": 1254, - "natural_h": 1254, - "width": 1254, - "height": 1254 - } - ], - "timestamp": 1784385332.265981, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXTTARTXDQ29A6K1KVJJ8PJS", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1024x1024", - "requested_size": "1024x1024", - "quality": "auto", - "n": 1, - "reference_images": [] - }, - "raw_usage": null - }, - { - "prompt": "粉色卡通猪,戴着魔法帽子在空中飞行,背景是漂浮的奇幻岛屿和发光植物,柔和的夕阳光线,明亮鲜艳的色彩,夸张的卡通风格,充满童趣和魔法氛围", - "images": [ - "/assets/output/online_17e0875d7d.png" - ], - "image_items": [ - { - "url": "/assets/output/online_17e0875d7d.png", - "kind": "image", - "name": "online_17e0875d7d.png", - "natural_w": 1254, - "natural_h": 1254, - "width": 1254, - "height": 1254 - } - ], - "timestamp": 1784384203.955441, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXTSAMR4KV4JA9R5KC03BQJN", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1024x1024", - "requested_size": "1024x1024", - "quality": "auto", - "n": 1, - "reference_images": [] - }, - "raw_usage": null - }, - { - "prompt": "A 3D rendered futuristic mechanical horse standing in a neon-lit cyberpunk city street, glowing neon blue and pink accents on its metallic armor, detailed engineering design, realistic lighting and shadows, 3D modeling style, Blender render, 8k resolution", - "images": [ - "/assets/output/online_2639b2da77.png" - ], - "image_items": [ - { - "url": "/assets/output/online_2639b2da77.png", - "kind": "image", - "name": "online_2639b2da77.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784383874.240329, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXTS009RMABCKHD3S74YQYXX", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [ - { - "url": "/assets/output/online_21959f61f2.png", - "name": "online_21959f61f2.png", - "role": "", - "kind": "", - "mime": "" - } - ] - }, - "raw_usage": null - }, - { - "prompt": "换成像素风格", - "images": [ - "/assets/output/online_21959f61f2.png" - ], - "image_items": [ - { - "url": "/assets/output/online_21959f61f2.png", - "kind": "image", - "name": "online_21959f61f2.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784383695.669037, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXTRTWCC1ANC48FGHS257CRR", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [] - }, - "raw_usage": null - }, - { - "prompt": "A majestic red dragon coiled around an ancient bronze ding (ritual vessel), intricate dragon scales in crimson and gold hues, glowing amber eyes, detailed bronze patterns on the vessel, misty mountain backdrop with pine trees, dramatic lighting, traditional Chinese ink painting style", - "images": [ - "/assets/output/online_7b4434862b.png" - ], - "image_items": [ - { - "url": "/assets/output/online_7b4434862b.png", - "kind": "image", - "name": "online_7b4434862b.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784383436.121542, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXTRE2VJ1PG9MW5ZHE3XKMS6", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [] - }, - "raw_usage": null - }, - { - "prompt": "我要生成一匹充满赛博朋克风格的未来感机械马", - "images": [ - "/assets/output/online_906ae680de.png" - ], - "image_items": [ - { - "url": "/assets/output/online_906ae680de.png", - "kind": "image", - "name": "online_906ae680de.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784382947.380919, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXTR3QS2PC4EF9EN7KYTJMB1", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [] - }, - "raw_usage": null - }, - { - "prompt": "Majestic golden Chinese dragon coiled in dynamic motion, shimmering golden scales, flowing mane merging with swirling clouds, vibrant red and gold cloud patterns, clear blue sky background with radiant sunlight, traditional Chinese ink painting style with intricate details", - "images": [ - "/assets/output/online_c080b73bf1.png" - ], - "image_items": [ - { - "url": "/assets/output/online_c080b73bf1.png", - "kind": "image", - "name": "online_c080b73bf1.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784379405.288364, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXTMN5NG9XM97R6WRHMR3ZS4", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [] - }, - "raw_usage": null - }, - { - "prompt": "A golden retriever with fluffy golden fur, standing in a sunny park with green grass and blue sky background, realistic style, high detail", - "images": [ - "/assets/output/online_084f459c23.png" - ], - "image_items": [ - { - "url": "/assets/output/online_084f459c23.png", - "kind": "image", - "name": "online_084f459c23.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784375822.133792, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXTHAJWRF4MBS74FA10PD4ZZ", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [] - }, - "raw_usage": null - }, - { - "prompt": "A 16:9 hand-drawn illustration on a pure white background. The character is a boy with short, messy black hair, half-lidded dead-fish eyes, wearing a plain white t-shirt, black shorts, and white crocs. Orange headphones are hanging around his neck. He has a blank, serious expression. He is standing and holding a giant, extendable, accordion-style vegetable peeler, attempting to peel a comically oversized potato. The drawing is done in a loose, sketchy black line-art style. There is a small handwritten Chinese annotation near the potato that reads '削皮?' (Peeling?). The composition is minimal with ample white space.", - "images": [ - "/assets/output/online_5d2df89731.png" - ], - "image_items": [ - { - "url": "/assets/output/online_5d2df89731.png", - "kind": "image", - "name": "online_5d2df89731.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784374533.9050171, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXTG0Q93KF0T41KTB5TPJ5CV", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [] - }, - "raw_usage": null - }, - { - "prompt": "A 16:9 hand-drawn illustration on a pure white background. The boy character is standing next to a wok on a stove, using a giant slotted spoon to scoop up a portion of cooked food. His expression is one of focused seriousness, as if he is performing a delicate operation. The drawing is in a sketchy black line-art style. A small handwritten Chinese annotation points to the food being scooped and reads '出锅' (Serving). The background is pure white, maintaining a minimalist and spacious feel.", - "images": [ - "/assets/output/online_d658ce6318.png" - ], - "image_items": [ - { - "url": "/assets/output/online_d658ce6318.png", - "kind": "image", - "name": "online_d658ce6318.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784374460.923496, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXTG0QCYHP0Z43QD18PTS4AW", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [] - }, - "raw_usage": null - }, - { - "prompt": "A 16:9 hand-drawn illustration on a pure white background. The same boy character is squatting next to a large wok on a portable gas stove. He is blowing air onto a tiny, almost invisible flame with his mouth, while simultaneously fanning it with a giant feather duster held in his other hand. His expression is one of intense, serious focus. The drawing is in a sketchy black line-art style. A small handwritten Chinese annotation points to the flame and reads '点火' (Ignite). The scene is set against a clean white background with plenty of negative space.", - "images": [ - "/assets/output/online_042c467ed0.png" - ], - "image_items": [ - { - "url": "/assets/output/online_042c467ed0.png", - "kind": "image", - "name": "online_042c467ed0.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784374460.127413, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXTG0QK78AZNWEV3RN9PZRJX", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [] - }, - "raw_usage": null - }, - { - "prompt": "A 16:9 hand-drawn illustration on a pure white background. The boy character is standing next to a giant mixing bowl filled with food. He is gripping an oversized wooden spoon with both hands, stirring the contents with slow, deliberate effort. His expression is completely blank and serious. The drawing is in a sketchy black line-art style. A small handwritten Chinese annotation points to the bowl and reads '翻炒' (Stir-frying). The composition is simple, set against a stark white background.", - "images": [ - "/assets/output/online_f8561fde4d.png" - ], - "image_items": [ - { - "url": "/assets/output/online_f8561fde4d.png", - "kind": "image", - "name": "online_f8561fde4d.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784374449.3757062, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXTG0QD278RM3T3VQQXNP4YM", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [] - }, - "raw_usage": null - }, - { - "prompt": "A 16:9 hand-drawn illustration on a pure white background. The boy character is seen from the back, standing over a massive, deep wok. He is holding a huge armful of various vegetables, including cabbage, lotus root, and mushrooms, poised to drop them into the wok. His posture suggests a heavy effort. The drawing is in a loose, sketchy black line-art style. A small handwritten Chinese annotation points towards the vegetables and reads '下料' (Adding Ingredients). The background is pure white with significant empty space.", - "images": [ - "/assets/output/online_f7b7a36fb8.png" - ], - "image_items": [ - { - "url": "/assets/output/online_f7b7a36fb8.png", - "kind": "image", - "name": "online_f7b7a36fb8.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784374446.294841, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXTG0QCKR261WG8M4QCGAFCJ", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [] - }, - "raw_usage": null - }, - { - "prompt": "A 16:9 hand-drawn illustration with a pure white background. The main subject is a boy named 'Xiao Yang' with short black hair, dead-fish eyes, wearing a white t-shirt, black shorts, white crocs, and orange headphones around his neck. He has a blank, serious expression. In this image, Xiao Yang stands with a deadpan look, holding a giant, accordion-like vegetable peeler, attempting to peel a potato that is larger than his own head. The drawing is primarily black line art with slight sketchy lines. There are large areas of white space. Handwritten Chinese annotations are sparse, e.g., '削皮?' (Peeling?). The overall style is quirky, minimalist, and slightly absurd.", - "images": [ - "/assets/output/online_c061e254cb.png" - ], - "image_items": [ - { - "url": "/assets/output/online_c061e254cb.png", - "kind": "image", - "name": "online_c061e254cb.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784374187.240597, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXTFQZ3WESCMDNJ8431TEQ4J", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [] - }, - "raw_usage": null - }, - { - "prompt": "A 16:9 hand-drawn illustration with a pure white background. The main subject is a boy named 'Xiao Yang' with short black hair, dead-fish eyes, wearing a white t-shirt, black shorts, white crocs, and orange headphones around his neck. He has a blank, serious expression. In this image, Xiao Yang stands beside a giant bowl-shaped container, slowly and deliberately stirring its contents with an oversized spoon, looking extremely focused. The drawing is primarily black line art with slight sketchy lines. There are large areas of white space. Handwritten Chinese annotations are sparse, e.g., '翻炒' (Stir-fry). The overall style is quirky, minimalist, and slightly absurd.", - "images": [ - "/assets/output/online_5c7d5d65ff.png" - ], - "image_items": [ - { - "url": "/assets/output/online_5c7d5d65ff.png", - "kind": "image", - "name": "online_5c7d5d65ff.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784374153.361258, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXTFQZ3W8DN2NTT2DJWA5FBE", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [] - }, - "raw_usage": null - }, - { - "prompt": "A 16:9 hand-drawn illustration with a pure white background. The main subject is a boy named 'Xiao Yang' with short black hair, dead-fish eyes, wearing a white t-shirt, black shorts, white crocs, and orange headphones around his neck. He has a blank, serious expression. In this image, Xiao Yang is seen from behind, holding a large pile of vegetables with both hands, about to drop them into a massive, deep wok. The drawing is primarily black line art with slight sketchy lines. There are large areas of white space. Handwritten Chinese annotations are sparse, e.g., '下料' (Add ingredients). The overall style is quirky, minimalist, and slightly absurd.", - "images": [ - "/assets/output/online_a2b41f88f6.png" - ], - "image_items": [ - { - "url": "/assets/output/online_a2b41f88f6.png", - "kind": "image", - "name": "online_a2b41f88f6.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784374153.2409332, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXTFQZ3XRD0NW9HNN3WTNSFY", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [] - }, - "raw_usage": null - }, - { - "prompt": "A 16:9 hand-drawn illustration with a pure white background. The main subject is a boy named 'Xiao Yang' with short black hair, dead-fish eyes, wearing a white t-shirt, black shorts, white crocs, and orange headphones around his neck. He has a blank, serious expression. In this image, Xiao Yang uses a giant slotted spoon to carefully lift a portion of stir-fried food from a pot, his expression one of intense concentration as if he's retrieving a precious artifact. The drawing is primarily black line art with slight sketchy lines. There are large areas of white space. Handwritten Chinese annotations are sparse, e.g., '出锅' (Serve). The overall style is quirky, minimalist, and slightly absurd.", - "images": [ - "/assets/output/online_4c76884468.png" - ], - "image_items": [ - { - "url": "/assets/output/online_4c76884468.png", - "kind": "image", - "name": "online_4c76884468.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784374147.7586, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXTFQZ3T76BWFGPCYM6H483T", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [] - }, - "raw_usage": null - }, - { - "prompt": "A 16:9 hand-drawn illustration with a pure white background. The main subject is a boy named 'Xiao Yang' with short black hair, dead-fish eyes, wearing a white t-shirt, black shorts, white crocs, and orange headphones around his neck. He has a blank, serious expression. In this image, Xiao Yang is crouching sideways next to a giant frying pan, intensely blowing into a tiny flame on the stove with a giant feather fan in his other hand. The drawing is primarily black line art with slight sketchy lines. There are large areas of white space. Handwritten Chinese annotations are sparse, e.g., '点火' (Ignite). The overall style is quirky, minimalist, and slightly absurd.", - "images": [ - "/assets/output/online_05068793f0.png" - ], - "image_items": [ - { - "url": "/assets/output/online_05068793f0.png", - "kind": "image", - "name": "online_05068793f0.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784374147.1708758, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXTFQZ3VZYYPVB7KJMFW7HGM", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [] - }, - "raw_usage": null - }, - { - "prompt": "A 16:9 hand-drawn illustration with a pure white background. The main subject is a boy named 'Xiao Yang' with short black hair, dead-fish eyes, wearing a white t-shirt, black shorts, white crocs, and orange headphones around his neck. He has a blank, serious expression. In this image, Xiao Yang is seen from behind, holding a large pile of vegetables with both hands, about to drop them into a massive, deep wok. The drawing is primarily black line art with slight sketchy lines. There are large areas of white space. Handwritten Chinese annotations are sparse, e.g., '下料' (Add ingredients). The overall style is quirky, minimalist, and slightly absurd.", - "images": [ - "/assets/output/online_aa3fd247d8.png" - ], - "image_items": [ - { - "url": "/assets/output/online_aa3fd247d8.png", - "kind": "image", - "name": "online_aa3fd247d8.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784358990.3717818, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXT18VCS9FWAQ9E8W46SK2BA", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [] - }, - "raw_usage": null - }, - { - "prompt": "A 16:9 hand-drawn illustration with a pure white background. The main subject is a boy named 'Xiao Yang' with short black hair, dead-fish eyes, wearing a white t-shirt, black shorts, white crocs, and orange headphones around his neck. He has a blank, serious expression. In this image, Xiao Yang is crouching sideways next to a giant frying pan, intensely blowing into a tiny flame on the stove with a giant feather fan in his other hand. The drawing is primarily black line art with slight sketchy lines. There are large areas of white space. Handwritten Chinese annotations are sparse, e.g., '点火' (Ignite). The overall style is quirky, minimalist, and slightly absurd.", - "images": [ - "/assets/output/online_7a845f9124.png" - ], - "image_items": [ - { - "url": "/assets/output/online_7a845f9124.png", - "kind": "image", - "name": "online_7a845f9124.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784358983.6882198, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXT18VCK99VSJ1RNQYYN63PP", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [] - }, - "raw_usage": null - }, - { - "prompt": "A 16:9 hand-drawn illustration with a pure white background. The main subject is a boy named 'Xiao Yang' with short black hair, dead-fish eyes, wearing a white t-shirt, black shorts, white crocs, and orange headphones around his neck. He has a blank, serious expression. In this image, Xiao Yang uses a giant slotted spoon to carefully lift a portion of stir-fried food from a pot, his expression one of intense concentration as if he's retrieving a precious artifact. The drawing is primarily black line art with slight sketchy lines. There are large areas of white space. Handwritten Chinese annotations are sparse, e.g., '出锅' (Serve). The overall style is quirky, minimalist, and slightly absurd.", - "images": [ - "/assets/output/online_8703f209fb.png" - ], - "image_items": [ - { - "url": "/assets/output/online_8703f209fb.png", - "kind": "image", - "name": "online_8703f209fb.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784358981.7302961, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXT18VCHGVXNX1FFVEXR0XNK", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [] - }, - "raw_usage": null - }, - { - "prompt": "A 16:9 hand-drawn illustration with a pure white background. The main subject is a boy named 'Xiao Yang' with short black hair, dead-fish eyes, wearing a white t-shirt, black shorts, white crocs, and orange headphones around his neck. He has a blank, serious expression. In this image, Xiao Yang stands with a deadpan look, holding a giant, accordion-like vegetable peeler, attempting to peel a potato that is larger than his own head. The drawing is primarily black line art with slight sketchy lines. There are large areas of white space. Handwritten Chinese annotations are sparse, e.g., '削皮?' (Peeling?). The overall style is quirky, minimalist, and slightly absurd.", - "images": [ - "/assets/output/online_3bfe93190a.png" - ], - "image_items": [ - { - "url": "/assets/output/online_3bfe93190a.png", - "kind": "image", - "name": "online_3bfe93190a.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784358980.1975942, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXT18VCJ4GY35D1QMFGFYTT6", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [] - }, - "raw_usage": null - }, - { - "prompt": "A 16:9 hand-drawn illustration with a pure white background. The main subject is a boy named 'Xiao Yang' with short black hair, dead-fish eyes, wearing a white t-shirt, black shorts, white crocs, and orange headphones around his neck. He has a blank, serious expression. In this image, Xiao Yang stands beside a giant bowl-shaped container, slowly and deliberately stirring its contents with an oversized spoon, looking extremely focused. The drawing is primarily black line art with slight sketchy lines. There are large areas of white space. Handwritten Chinese annotations are sparse, e.g., '翻炒' (Stir-fry). The overall style is quirky, minimalist, and slightly absurd.", - "images": [ - "/assets/output/online_0bed9be12f.png" - ], - "image_items": [ - { - "url": "/assets/output/online_0bed9be12f.png", - "kind": "image", - "name": "online_0bed9be12f.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784358979.954345, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXT18VECW4ZF3N58NEDWMKZ0", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [] - }, - "raw_usage": null - }, - { - "prompt": "一只狗", - "images": [ - "/assets/output/online_bdbaa580fe.png" - ], - "image_items": [ - { - "url": "/assets/output/online_bdbaa580fe.png", - "kind": "image", - "name": "online_bdbaa580fe.png", - "natural_w": 1024, - "natural_h": 1024, - "width": 1024, - "height": 1024 - } - ], - "timestamp": 1784356206.881227, - "type": "online", - "model": "agnes-image-2.0-flash", - "provider_id": "agnes-ai", - "provider_name": "Agnes AI", - "task_id": null, - "request_id": null, - "params": { - "provider_id": "agnes-ai", - "model": "agnes-image-2.0-flash", - "size": "1024x1024", - "requested_size": "1024x1024", - "quality": "auto", - "n": 1, - "reference_images": [] - }, - "raw_usage": { - "total_tokens": 0, - "input_tokens": 0, - "input_tokens_details": { - "image_tokens": 0, - "text_tokens": 0 - }, - "output_tokens": 0 - } - }, - { - "prompt": "A series of 4 hand-drawn style illustrations in 16:9 aspect ratio, featuring the character 'Xiao Yang' (a boy with short black hair, dead-fish eyes, orange headphones around his neck, wearing a white t-shirt, black shorts, and white clogs). The style is minimalist line art on a pure white background with ample negative space. \n\nImage 1 (Preparation): Xiao Yang is standing seriously, holding a giant red tomato that he is slicing into perfect geometric cubes with a knife. Handwritten annotation in Chinese: '切块'.\n\nImage 2 (Cooking Egg): Xiao Yang is acting as a human whisk, vigorously stirring a bowl of yellow egg liquid with chopsticks. Handwritten annotation in Chinese: '打蛋'.\n\nImage 3 (Frying): Xiao Yang is holding a frying pan, tossing the scrambled eggs and tomatoes together with a focused expression. Handwritten annotation in Chinese: '翻炒'.\n\nImage 4 (Plating): Xiao Yang is placing a large, steaming plate of the finished dish onto a table, looking satisfied. Handwritten annotation in Chinese: '出锅'.\n\nColor palette: Black lines, white background, orange accents for headphones, red for tomato, yellow for egg.", - "images": [ - "/assets/output/online_cbebddf053.png" - ], - "image_items": [ - { - "url": "/assets/output/online_cbebddf053.png", - "kind": "image", - "name": "online_cbebddf053.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784355898.197712, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXSY822RBXXEQS9S8K5B9NRE", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [ - { - "url": "/assets/input/ai_ref_e248b6ab1614.png", - "name": "online_445550fed2.png", - "role": "", - "kind": "", - "mime": "" - } - ] - }, - "raw_usage": null - }, - { - "prompt": "A series of 4 hand-drawn style illustrations in 16:9 aspect ratio, featuring the character 'Xiao Yang' (a boy with short black hair, dead-fish eyes, orange headphones around his neck, wearing a white t-shirt, black shorts, and white clogs). The style is minimalist line art on a pure white background with ample negative space. \n\nImage 1 (Preparation): Xiao Yang is standing seriously, holding a giant red tomato that he is slicing into perfect geometric cubes with a knife. Handwritten annotation in Chinese: '切块'.\n\nImage 2 (Cooking Egg): Xiao Yang is acting as a human whisk, vigorously stirring a bowl of yellow egg liquid with chopsticks. Handwritten annotation in Chinese: '打蛋'.\n\nImage 3 (Frying): Xiao Yang is holding a frying pan, tossing the scrambled eggs and tomatoes together with a focused expression. Handwritten annotation in Chinese: '翻炒'.\n\nImage 4 (Plating): Xiao Yang is placing a large, steaming plate of the finished dish onto a table, looking satisfied. Handwritten annotation in Chinese: '出锅'.\n\nColor palette: Black lines, white background, orange accents for headphones, red for tomato, yellow for egg.", - "images": [ - "/assets/output/online_e9df932bdd.png" - ], - "image_items": [ - { - "url": "/assets/output/online_e9df932bdd.png", - "kind": "image", - "name": "online_e9df932bdd.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784355882.091896, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXSY822SVDWTKWD7N83PZ2FE", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [ - { - "url": "/assets/input/ai_ref_e248b6ab1614.png", - "name": "online_445550fed2.png", - "role": "", - "kind": "", - "mime": "" - } - ] - }, - "raw_usage": null - }, - { - "prompt": "A series of 4 hand-drawn style illustrations in 16:9 aspect ratio, featuring the character 'Xiao Yang' (a boy with short black hair, dead-fish eyes, orange headphones around his neck, wearing a white t-shirt, black shorts, and white clogs). The style is minimalist line art on a pure white background with ample negative space. \n\nImage 1 (Preparation): Xiao Yang is standing seriously, holding a giant red tomato that he is slicing into perfect geometric cubes with a knife. Handwritten annotation in Chinese: '切块'.\n\nImage 2 (Cooking Egg): Xiao Yang is acting as a human whisk, vigorously stirring a bowl of yellow egg liquid with chopsticks. Handwritten annotation in Chinese: '打蛋'.\n\nImage 3 (Frying): Xiao Yang is holding a frying pan, tossing the scrambled eggs and tomatoes together with a focused expression. Handwritten annotation in Chinese: '翻炒'.\n\nImage 4 (Plating): Xiao Yang is placing a large, steaming plate of the finished dish onto a table, looking satisfied. Handwritten annotation in Chinese: '出锅'.\n\nColor palette: Black lines, white background, orange accents for headphones, red for tomato, yellow for egg.", - "images": [ - "/assets/output/online_7a35c2bfc0.png" - ], - "image_items": [ - { - "url": "/assets/output/online_7a35c2bfc0.png", - "kind": "image", - "name": "online_7a35c2bfc0.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784355830.1989062, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXSY8226CGNAJQK2Y8T1S69B", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [ - { - "url": "/assets/input/ai_ref_e248b6ab1614.png", - "name": "online_445550fed2.png", - "role": "", - "kind": "", - "mime": "" - } - ] - }, - "raw_usage": null - }, - { - "prompt": "A series of 4 hand-drawn style illustrations in 16:9 aspect ratio, featuring the character 'Xiao Yang' (a boy with short black hair, dead-fish eyes, orange headphones around his neck, wearing a white t-shirt, black shorts, and white clogs). The style is minimalist line art on a pure white background with ample negative space. \n\nImage 1 (Preparation): Xiao Yang is standing seriously, holding a giant red tomato that he is slicing into perfect geometric cubes with a knife. Handwritten annotation in Chinese: '切块'.\n\nImage 2 (Cooking Egg): Xiao Yang is acting as a human whisk, vigorously stirring a bowl of yellow egg liquid with chopsticks. Handwritten annotation in Chinese: '打蛋'.\n\nImage 3 (Frying): Xiao Yang is holding a frying pan, tossing the scrambled eggs and tomatoes together with a focused expression. Handwritten annotation in Chinese: '翻炒'.\n\nImage 4 (Plating): Xiao Yang is placing a large, steaming plate of the finished dish onto a table, looking satisfied. Handwritten annotation in Chinese: '出锅'.\n\nColor palette: Black lines, white background, orange accents for headphones, red for tomato, yellow for egg.", - "images": [ - "/assets/output/online_10a0e08c0d.png" - ], - "image_items": [ - { - "url": "/assets/output/online_10a0e08c0d.png", - "kind": "image", - "name": "online_10a0e08c0d.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784355827.67314, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXSY8241FH91GKCA7WXD462R", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [ - { - "url": "/assets/input/ai_ref_e248b6ab1614.png", - "name": "online_445550fed2.png", - "role": "", - "kind": "", - "mime": "" - } - ] - }, - "raw_usage": null - }, - { - "prompt": "A 16:9 hand-drawn illustration on a pure white background. Scene: Step 2 - Seasoning. The boy character is standing at a counter, carefully pouring spices from various bottles into a mixing bowl. To his right, Xiaohei the devil is holding a giant, oversized salt shaker, looking serious. Handwritten Chinese annotations in black ink: '调配酱汁 (Seasoning)', '灵魂酱汁', '加点盐!', '小心手残!'. Style matches the provided reference images exactly.", - "images": [ - "/assets/output/online_d46a5665c4.png" - ], - "image_items": [ - { - "url": "/assets/output/online_d46a5665c4.png", - "kind": "image", - "name": "online_d46a5665c4.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784354288.717885, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXSWSCRRYG7KAQZSGS1PCT0S", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [ - { - "url": "/assets/input/ai_ref_ac525e0fbbf1.png", - "name": "online_445550fed2.png", - "role": "", - "kind": "", - "mime": "" - } - ] - }, - "raw_usage": null - }, - { - "prompt": "A 16:9 hand-drawn illustration on a pure white background. Scene: Step 5 - Enjoying. The boy character and Xiaohei the devil are sitting at a dining table. In front of them is a delicious-looking dish. They both look happy and ready to eat. Handwritten Chinese annotations in black ink: '开饭享用 (Enjoying)', '开饭!', '真香!', '干杯!'. Style matches the provided reference images exactly.", - "images": [ - "/assets/output/online_3a0210856a.png" - ], - "image_items": [ - { - "url": "/assets/output/online_3a0210856a.png", - "kind": "image", - "name": "online_3a0210856a.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784354285.115314, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXSWSBVXF4FBME8BBHJQTGWG", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [ - { - "url": "/assets/input/ai_ref_ac525e0fbbf1.png", - "name": "online_445550fed2.png", - "role": "", - "kind": "", - "mime": "" - } - ] - }, - "raw_usage": null - }, - { - "prompt": "A 16:9 hand-drawn illustration on a pure white background. Scene: Step 3 - Cooking. The boy character is standing in front of a stove, holding a wok and tossing food. Steam is rising from the wok. To his right, Xiaohei the devil is fanning the fire under the stove with a hand fan, looking intense. Handwritten Chinese annotations in black ink: '下锅翻炒 (Cooking)', '大火快炒', '火候要准!', '别糊了!'. Style matches the provided reference images exactly.", - "images": [ - "/assets/output/online_3ff77f1a6e.png" - ], - "image_items": [ - { - "url": "/assets/output/online_3ff77f1a6e.png", - "kind": "image", - "name": "online_3ff77f1a6e.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784354282.357785, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXSWSBWYJGYPJDY0E7YD2BEY", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [ - { - "url": "/assets/input/ai_ref_ac525e0fbbf1.png", - "name": "online_445550fed2.png", - "role": "", - "kind": "", - "mime": "" - } - ] - }, - "raw_usage": null - }, - { - "prompt": "A 16:9 hand-drawn illustration on a pure white background. Scene: Step 4 - Plating. The boy character is holding a plate, carefully arranging the cooked food with chopsticks. He looks focused. To his right, Xiaohei the devil is holding a small garnish (like a sprig of cilantro) ready to place on top. Handwritten Chinese annotations in black ink: '装盘摆饰 (Plating)', '小心装盘', '颜值即正义!', '最后点缀'. Style matches the provided reference images exactly.", - "images": [ - "/assets/output/online_46a9c34826.png" - ], - "image_items": [ - { - "url": "/assets/output/online_46a9c34826.png", - "kind": "image", - "name": "online_46a9c34826.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784354280.252577, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXSWSBVJS7D7G33NF5HQ5FN7", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [ - { - "url": "/assets/input/ai_ref_ac525e0fbbf1.png", - "name": "online_445550fed2.png", - "role": "", - "kind": "", - "mime": "" - } - ] - }, - "raw_usage": null - }, - { - "prompt": "A 16:9 hand-drawn illustration on a pure white background. Scene: Step 1 - Prep Ingredients. A boy with spiky black hair, white T-shirt, black shorts, and orange headphones around his neck sits at a kitchen counter. He is washing vegetables under a tap. To his right, the 'Xiaohei' creature, a cute black devil with small horns and wings, is carrying a large basket overflowing with fresh vegetables and meat. Handwritten Chinese annotations in black ink: '准备食材 (Prep)', '洗切备料', '这些都很新鲜!'. Style matches the provided reference images exactly.", - "images": [ - "/assets/output/online_34f53beb06.png" - ], - "image_items": [ - { - "url": "/assets/output/online_34f53beb06.png", - "kind": "image", - "name": "online_34f53beb06.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784354263.6680949, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXSWSBVZ9727C7G1Y4QFJ9Z1", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [ - { - "url": "/assets/input/ai_ref_ac525e0fbbf1.png", - "name": "online_445550fed2.png", - "role": "", - "kind": "", - "mime": "" - } - ] - }, - "raw_usage": null - }, - { - "prompt": "A 16:9 hand-drawn illustration on a pure white background. The boy character stands in the middle, holding a net to catch small red spheres labeled 'Bug'. These bugs are flowing through a complex network of hand-drawn pipes. On the left, the pipes are labeled '应用流量' (App Traffic) and go through a '安全检测' (Security Scan) funnel. On the right, the 'Xiaohei' devil creature is operating a large lever on a machine labeled '测试强度' (Test Intensity). Annotations include: '测试阶段 (Testing)', '压力测试 (Stress Test)', '漏洞 (Vulnerability)', '缺陷记录 (Defect Log)', '系统输出 (System Output)'. The style should match the third reference image.", - "images": [ - "/assets/output/online_6ba6d25fd3.png" - ], - "image_items": [ - { - "url": "/assets/output/online_6ba6d25fd3.png", - "kind": "image", - "name": "online_6ba6d25fd3.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784353425.363507, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXSVWVG9BD0B1645JMR74SKX", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [ - { - "url": "/assets/input/ai_ref_ac525e0fbbf1.png", - "name": "online_445550fed2.png", - "role": "", - "kind": "", - "mime": "" - } - ] - }, - "raw_usage": null - }, - { - "prompt": "A 16:9 hand-drawn illustration on a pure white background. On the left, the same boy character is sitting at a desk in front of a laptop, typing code. A speech bubble shows HTML-like code. To the right, the 'Xiaohei' creature, now depicted as a small, cute black devil with horns and a tail, is using a wrench to assemble large black blocks labeled with Chinese characters for app functions: '登录' (Login), '支付' (Payment), '消息' (Messages). Handwritten Chinese annotations include: '开发阶段 · Development', '功能', '模块', '小黑 (Xiaohei)', '注意:每个功能都要稳定可靠!'. The style should match the second reference image.", - "images": [ - "/assets/output/online_dfb505b1bf.png" - ], - "image_items": [ - { - "url": "/assets/output/online_dfb505b1bf.png", - "kind": "image", - "name": "online_dfb505b1bf.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784353354.260889, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXSVWVG2WGZE44A1B37FB0S3", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [ - { - "url": "/assets/input/ai_ref_ac525e0fbbf1.png", - "name": "online_445550fed2.png", - "role": "", - "kind": "", - "mime": "" - } - ] - }, - "raw_usage": null - }, - { - "prompt": "A 16:9 hand-drawn illustration on a pure white background. On the left, a boy with spiky black hair, wearing a white T-shirt, black shorts, and orange headphones around his neck, is sitting at a desk drawing a rough prototype of a mobile app on a large sheet of paper with a pencil. To the right, the 'Xiaohei' creature, a solid black blob with two white dot eyes and thin legs, is carrying a large stack of colorful sticky notes representing user requirements. Handwritten Chinese annotations in black ink are scattered around: '设计阶段', '先画个原型草图!', '用户需求', '这些都是用户的想法!', '收集并整理需求'. The style should mimic the provided reference image exactly.", - "images": [ - "/assets/output/online_a9d34656df.png" - ], - "image_items": [ - { - "url": "/assets/output/online_a9d34656df.png", - "kind": "image", - "name": "online_a9d34656df.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784353345.373652, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXSVWVEW0XBBKHD973BFTR52", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [ - { - "url": "/assets/input/ai_ref_ac525e0fbbf1.png", - "name": "online_445550fed2.png", - "role": "", - "kind": "", - "mime": "" - } - ] - }, - "raw_usage": null - }, - { - "prompt": "A 16:9 hand-drawn illustration on a pure white background. The boy character and the 'Xiaohei' devil creature are pushing a large, stylized rocket on a launchpad. The rocket has a simple icon on it. To the right, there is a signpost with an arrow pointing forward, labeled '发布' (Publish) and 'Go!'. The floor is a simple grid. The main annotation is '上线' (Launch). The style should match the fourth reference image.", - "images": [ - "/assets/output/online_bdfef07739.png" - ], - "image_items": [ - { - "url": "/assets/output/online_bdfef07739.png", - "kind": "image", - "name": "online_bdfef07739.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784353343.473737, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXSVWVDMKE5MHY9R593N8K85", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [ - { - "url": "/assets/input/ai_ref_ac525e0fbbf1.png", - "name": "online_445550fed2.png", - "role": "", - "kind": "", - "mime": "" - } - ] - }, - "raw_usage": null - }, - { - "prompt": "A 16:9 hand-drawn illustration on a pure white background. The scene depicts the 'Launch' phase of an app. The boy character and the 'Xiaohei' creature are working together to push a large, stylized rocket onto a launchpad. The rocket has a simple, hand-drawn icon on it representing the finished app. The background is empty except for some simple grid lines suggesting a digital space. Handwritten Chinese annotations include '上线' (Launch) and '发布' (Release). The art style is consistent with the previous images: a black-and-white line drawing with minimal color accents (orange for headphones, red for a 'Go!' sign), a slightly irregular hand-drawn feel, and a large amount of white space.", - "images": [ - "/assets/output/online_abd24e31cb.png" - ], - "image_items": [ - { - "url": "/assets/output/online_abd24e31cb.png", - "kind": "image", - "name": "online_abd24e31cb.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784351967.916376, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXSTJW2D10PA85TVZW3ESRJ2", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [ - { - "url": "/assets/input/ai_ref_ac525e0fbbf1.png", - "name": "online_445550fed2.png", - "role": "", - "kind": "", - "mime": "" - } - ] - }, - "raw_usage": null - }, - { - "prompt": "A 16:9 hand-drawn illustration on a pure white background. The scene depicts the 'Testing' phase of an app. The boy character stands in the center, looking focused. He is holding a net, trying to catch small, red, spherical objects labeled 'Bug' that are flowing through a complex, hand-drawn pipe system. The 'Xiaohei' creature is operating a large lever connected to the pipes, looking serious and concentrated. The pipes are drawn with simple black lines. Handwritten Chinese annotations point to different parts of the system, such as '压力测试' (Stress Test) and '漏洞' (Vulnerability). The style remains a minimalist, sketchy line drawing on a pure white background with plenty of negative space.", - "images": [ - "/assets/output/online_23f9dd6d1a.png" - ], - "image_items": [ - { - "url": "/assets/output/online_23f9dd6d1a.png", - "kind": "image", - "name": "online_23f9dd6d1a.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784351910.274147, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXSTGMVF6R95ZZSEDR9M69GJ", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [ - { - "url": "/assets/input/ai_ref_ac525e0fbbf1.png", - "name": "online_445550fed2.png", - "role": "", - "kind": "", - "mime": "" - } - ] - }, - "raw_usage": null - }, - { - "prompt": "A 16:9 hand-drawn illustration on a pure white background. The scene depicts the 'Development' phase of an app. The boy character is sitting at a desk in front of a laptop, typing code. The screen shows abstract lines of code. To his right, the 'Xiaohei' creature is acting like a mechanic, using a wrench to tighten large black screws labeled with Chinese characters for app features, such as '登录' (Login) and '支付' (Payment). The overall style is a clean, black-and-white line drawing with a few orange highlights on the boy's headphones and a red highlight on a warning sign. Handwritten Chinese annotations like '功能' (Function) and '模块' (Module) are present. The composition leaves significant white space.", - "images": [ - "/assets/output/online_081ab5a319.png" - ], - "image_items": [ - { - "url": "/assets/output/online_081ab5a319.png", - "kind": "image", - "name": "online_081ab5a319.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784351836.35097, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXSTCVBTFK4NW9RDPBCWTRYW", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [ - { - "url": "/assets/input/ai_ref_ac525e0fbbf1.png", - "name": "online_445550fed2.png", - "role": "", - "kind": "", - "mime": "" - } - ] - }, - "raw_usage": null - }, - { - "prompt": "A 16:9 hand-drawn illustration on a pure white background. The scene depicts the 'Design' phase of an app. On the left, a boy character, drawn in a simple black line-art style with spiky hair, orange headphones around his neck, a white t-shirt, and black shorts, holds a large pencil and is drawing a rough sketch of an app interface on a piece of paper. To his right, a small, solid black creature ('Xiaohei') with two white dot eyes and thin legs is carrying a stack of colorful sticky notes representing user requirements and ideas. The drawing style is loose and sketchy, not vector-like. There are several handwritten Chinese annotations in black ink, such as '需求' (Requirements) and '原型' (Prototype), scattered around the scene. The composition has ample white space.", - "images": [ - "/assets/output/online_791f2c6c23.png" - ], - "image_items": [ - { - "url": "/assets/output/online_791f2c6c23.png", - "kind": "image", - "name": "online_791f2c6c23.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784351713.299083, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXSTATM6GAKGA0T5MTV01XNS", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [ - { - "url": "/assets/input/ai_ref_ac525e0fbbf1.png", - "name": "online_445550fed2.png", - "role": "", - "kind": "", - "mime": "" - } - ] - }, - "raw_usage": null - }, - { - "prompt": "A 16:9 horizontal illustration on a pure white background. The main character is a boy with black spiky hair, wearing a white t-shirt, black shorts, and white clogs, with orange headphones around his neck, drawn in a clean black hand-drawn line art style. He is sitting on the ground in front of a large pile of jigsaw puzzle pieces. He is holding the final piece, fitting it into the assembled section which forms a complete picture. The unassembled part remains blank space. Handwritten Chinese annotations in black: '碎片信息' (Fragmented Information) near the loose pieces, and '全局视角' (Global Perspective) near the completed puzzle. Minimalist, hand-drawn sketch style, ample white space.", - "images": [ - "/assets/output/online_e5d009a11a.png" - ], - "image_items": [ - { - "url": "/assets/output/online_e5d009a11a.png", - "kind": "image", - "name": "online_e5d009a11a.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784339271.614291, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXSEFB06JVY2APGQ2NWTYB47", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [ - { - "url": "/assets/output/online_da1f6b7c8f.png", - "name": "online_da1f6b7c8f.png", - "role": "", - "kind": "", - "mime": "" - }, - { - "url": "/assets/input/ai_ref_e19bbc60010d.png", - "name": "online_445550fed2.png", - "role": "", - "kind": "", - "mime": "" - } - ] - }, - "raw_usage": null - }, - { - "prompt": "A 16:9 horizontal illustration on a pure white background. The main character is a boy with black spiky hair, wearing a white t-shirt, black shorts, and white clogs, with orange headphones around his neck, drawn in a clean black hand-drawn line art style. He is squatting in a garden, holding pruning shears in his left hand to trim messy branches, and a watering can in his right hand watering a small flower. Some flowers are blooming, others are withered. Handwritten Chinese annotations in black: '删减' (Delete/Cut) near the scissors, and '培养' (Cultivate/Nurture) near the watering can. Minimalist, hand-drawn sketch style, ample white space.", - "images": [ - "/assets/output/online_37b073d27b.png" - ], - "image_items": [ - { - "url": "/assets/output/online_37b073d27b.png", - "kind": "image", - "name": "online_37b073d27b.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784339211.215051, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXSEDFZ0R8WJGJSYSHS0RE0W", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [ - { - "url": "/assets/output/online_da1f6b7c8f.png", - "name": "online_da1f6b7c8f.png", - "role": "", - "kind": "", - "mime": "" - }, - { - "url": "/assets/input/ai_ref_e19bbc60010d.png", - "name": "online_445550fed2.png", - "role": "", - "kind": "", - "mime": "" - } - ] - }, - "raw_usage": null - }, - { - "prompt": "A 16:9 horizontal illustration on a pure white background. The main character is a boy with black spiky hair, wearing a white t-shirt, black shorts, and white clogs, with orange headphones around his neck, drawn in a clean black hand-drawn line art style. He is plugged into a giant power bank via a cable. The power bank display shows the battery jumping from 20% to 80%. Empty coffee cups are scattered nearby. Handwritten Chinese annotations in black: '回血' (Recharging/Recovering HP) next to the power bank, and '透支' (Overdrawn) next to the empty coffee cups. Minimalist, hand-drawn sketch style, ample white space.", - "images": [ - "/assets/output/online_3665fbe081.png" - ], - "image_items": [ - { - "url": "/assets/output/online_3665fbe081.png", - "kind": "image", - "name": "online_3665fbe081.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784339151.703498, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXSEC06J7Y33RAJ3SNFNN46K", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [ - { - "url": "/assets/output/online_da1f6b7c8f.png", - "name": "online_da1f6b7c8f.png", - "role": "", - "kind": "", - "mime": "" - }, - { - "url": "/assets/input/ai_ref_e19bbc60010d.png", - "name": "online_445550fed2.png", - "role": "", - "kind": "", - "mime": "" - } - ] - }, - "raw_usage": null - }, - { - "prompt": "A 16:9 horizontal illustration on a pure white background. The main character is a boy with black spiky hair, wearing a white t-shirt, black shorts, and white clogs, with orange headphones around his neck, drawn in a clean black hand-drawn line art style. He is bending over, intently inspecting a giant sieve. On top of the sieve is a chaotic pile of colorful information fragments (papers, shapes). Underneath the sieve, only a few clean blue gems have fallen through. Handwritten Chinese annotations in black: '噪音' (Noise) near the pile on top, and '信号' (Signal) next to the blue gems. Minimalist, hand-drawn sketch style, ample white space.", - "images": [ - "/assets/output/online_5536b7f8eb.png" - ], - "image_items": [ - { - "url": "/assets/output/online_5536b7f8eb.png", - "kind": "image", - "name": "online_5536b7f8eb.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784339102.695761, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXSEAB47Z2D8WHGN76ARPAEH", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [ - { - "url": "/assets/output/online_da1f6b7c8f.png", - "name": "online_da1f6b7c8f.png", - "role": "", - "kind": "", - "mime": "" - }, - { - "url": "/assets/input/ai_ref_e19bbc60010d.png", - "name": "online_445550fed2.png", - "role": "", - "kind": "", - "mime": "" - } - ] - }, - "raw_usage": null - }, - { - "prompt": "A 16:9 horizontal illustration on a pure white background. The main character is a boy with black spiky hair, wearing a white t-shirt, black shorts, and white clogs, with orange headphones around his neck, drawn in a clean black hand-drawn line art style. He stands triumphantly on top of a small hill made of a neat stack of yellow sticky notes. He is holding a small flag that has the Chinese characters \"搞定\" (Done) written on it. Below him, at the bottom of the hill, is a messy pile of scattered, disorganized sticky notes. There are several small, handwritten Chinese annotations in black ink pointing to the messy pile (e.g., '待办' - To-do) and the clean pile (e.g., '已归档' - Archived). The overall style is minimalist, slightly quirky, with ample white space, mimicking a hand-drawn sketch.", - "images": [ - "/assets/output/online_da1f6b7c8f.png" - ], - "image_items": [ - { - "url": "/assets/output/online_da1f6b7c8f.png", - "kind": "image", - "name": "online_da1f6b7c8f.png", - "natural_w": 1672, - "natural_h": 941, - "width": 1672, - "height": 941 - } - ], - "timestamp": 1784338861.963657, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXSE3ACBHTEFGFFYXF05BZ7S", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1280x720", - "requested_size": "1280x720", - "quality": "auto", - "n": 1, - "reference_images": [ - { - "url": "/assets/input/ai_ref_e19bbc60010d.png", - "name": "online_445550fed2.png", - "role": "", - "kind": "", - "mime": "" - } - ] - }, - "raw_usage": null - }, - { - "prompt": "A 16:9 hand-drawn illustration on a pure white background. A boy with black hair, wearing a white t-shirt, black shorts, white crocs, and orange headphones around his neck, stands on top of a giant, sketchy, hand-drawn funnel. The funnel is overflowing with colorful, jagged shards of paper representing information fragments. At the very bottom tip of the funnel, a single, glowing golden drop is dripping down. The style is quirky and minimalist, with black ink line art and small handwritten Chinese annotations. The annotation '信息过载' (Information Overload) points to the colorful mess at the top, and '提炼' (Refining) points to the single golden drop at the bottom.", - "images": [ - "/assets/output/online_b1cbc7e172.png" - ], - "image_items": [ - { - "url": "/assets/output/online_b1cbc7e172.png", - "kind": "image", - "name": "online_b1cbc7e172.png", - "natural_w": 1024, - "natural_h": 1024, - "width": 1024, - "height": 1024 - } - ], - "timestamp": 1784335620.4760902, - "type": "online", - "model": "agnes-image-2.1-flash", - "provider_id": "agnes-ai", - "provider_name": "Agnes AI", - "task_id": "task_SauiwzHNWiiEN1k2zYeVjHWlVjzTswC0", - "request_id": null, - "params": { - "provider_id": "agnes-ai", - "model": "agnes-image-2.1-flash", - "size": "1024x1024", - "requested_size": "1024x1024", - "quality": "auto", - "n": 1, - "reference_images": [ - { - "url": "/assets/input/ai_ref_f11c8eb2f7e1.png", - "name": "online_445550fed2-1631624a.png", - "role": "", - "kind": "", - "mime": "" - } - ] - }, - "raw_usage": null - }, - { - "prompt": "A 16:9 hand-drawn illustration on a pure white background. The main character is a boy with black hair, wearing a white t-shirt, black shorts, white crocs, and orange headphones around his neck. He stands in front of a large, crude, hand-drawn meat grinder made of black lines. The boy is shoving a pile of messy, scribbled sticky notes into the top of the machine. From the bottom chute, a neat, organized conveyor belt of clean papers comes out. The style is quirky and minimalist, resembling 'Xiaohei' art but featuring this specific character. Black line art for the main elements, orange for the headphones and some accents, red for a few chaotic scribbles on the input notes, blue for the orderly output. Lots of negative space. Handwritten Chinese annotations like '想法' (Ideas) near the input and '流水线' (Assembly Line) near the output.", - "images": [ - "/assets/output/online_b19784e092.png" - ], - "image_items": [ - { - "url": "/assets/output/online_b19784e092.png", - "kind": "image", - "name": "online_b19784e092.png", - "natural_w": 1024, - "natural_h": 1024, - "width": 1024, - "height": 1024 - } - ], - "timestamp": 1784335493.431113, - "type": "online", - "model": "agnes-image-2.1-flash", - "provider_id": "agnes-ai", - "provider_name": "Agnes AI", - "task_id": "task_XZ1XFbXr2W9vSIF99Nt4YYbc0Xq216fI", - "request_id": null, - "params": { - "provider_id": "agnes-ai", - "model": "agnes-image-2.1-flash", - "size": "1024x1024", - "requested_size": "1024x1024", - "quality": "auto", - "n": 1, - "reference_images": [ - { - "url": "/assets/input/ai_ref_f11c8eb2f7e1.png", - "name": "online_445550fed2-1631624a.png", - "role": "", - "kind": "", - "mime": "" - } - ] - }, - "raw_usage": null - }, - { - "prompt": "A soft watercolor illustration of a Shiba Inu running joyfully against a sunset sky. The artwork features visible paper texture, low saturation colors, and translucent layering typical of hand-painted watercolors. Avoid pure black; use warm oranges, pinks, and soft purples for the sunset. The composition has ample negative space with the dog centered slightly towards the top. Whimsical and gentle atmosphere.", - "images": [ - "/assets/output/online_b77d71a97c.png" - ], - "image_items": [ - { - "url": "/assets/output/online_b77d71a97c.png", - "kind": "image", - "name": "online_b77d71a97c.png", - "natural_w": 1024, - "natural_h": 1024, - "width": 1024, - "height": 1024 - } - ], - "timestamp": 1784334938.176192, - "type": "online", - "model": "agnes-image-2.1-flash", - "provider_id": "agnes-ai", - "provider_name": "Agnes AI", - "task_id": "task_DRnkrCJ9bFh1z2WOMh42KBykhKbLA8HL", - "request_id": null, - "params": { - "provider_id": "agnes-ai", - "model": "agnes-image-2.1-flash", - "size": "1024x1024", - "requested_size": "1024x1024", - "quality": "auto", - "n": 1, - "reference_images": [ - { - "url": "/assets/output/online_def6fb8e0d.png", - "name": "online_def6fb8e0d.png", - "role": "", - "kind": "", - "mime": "" - } - ] - }, - "raw_usage": null - }, - { - "prompt": "A soft watercolor illustration of a Shiba Inu running joyfully under a starry night sky. The artwork features visible paper texture, low saturation colors, and translucent layering typical of hand-painted watercolors. Avoid pure black; use deep blues and purples for the night sky. The composition has ample negative space with the dog centered slightly towards the top. Whimsical and gentle atmosphere.", - "images": [ - "/assets/output/online_def6fb8e0d.png" - ], - "image_items": [ - { - "url": "/assets/output/online_def6fb8e0d.png", - "kind": "image", - "name": "online_def6fb8e0d.png", - "natural_w": 1024, - "natural_h": 1024, - "width": 1024, - "height": 1024 - } - ], - "timestamp": 1784334840.4123142, - "type": "online", - "model": "agnes-image-2.1-flash", - "provider_id": "agnes-ai", - "provider_name": "Agnes AI", - "task_id": null, - "request_id": null, - "params": { - "provider_id": "agnes-ai", - "model": "agnes-image-2.1-flash", - "size": "1024x1024", - "requested_size": "1024x1024", - "quality": "auto", - "n": 1, - "reference_images": [] - }, - "raw_usage": { - "total_tokens": 0, - "input_tokens": 0, - "input_tokens_details": { - "image_tokens": 0, - "text_tokens": 0 - }, - "output_tokens": 0 - } - }, - { - "prompt": "Create a four-panel horizontal comic illustration in the exact character style of the provided reference images. The character should closely match the reference: a black round-headed boy-like figure with white dot eyes, minimal mouth, short black hair shape in some panels, plain white oversized T-shirt, orange headphones around the neck, thin limbs, simple black shorts, crocs-like shoes, clean black hand-drawn outlines, pure white background, sparse orange accent, no extra style drift. Keep the same visual language as the reference: simple, flat, hand-drawn, clean, slightly awkward, deadpan, not cute, not commercial, not manga-detailed.\n\nTheme: When someone is happily configuring KIMI K3, but after finishing everything, the brain goes blank and they can only send a timid “你好”.\n\nPanel 1: The character is excitedly setting up KIMI K3. He is holding or presenting a keyboard or setup device with sparkling orange emphasis lines, looking slightly proud and energized. Title in the top-left: “1 配置 K3”.\n\nPanel 2: He sits at a laptop, staring blankly at the screen. A small speech bubble with “...” above his head. His face should be empty and frozen, conveying complete mental blankness. Title: “2 大脑空白”.\n\nPanel 3: Extreme close-up of a finger hovering over the Enter key on a keyboard, with visible tension lines and tiny shaking marks. The key clearly says “Enter”. The composition should be dramatic and compressed, matching the reference’s close-up humor. Title: “3 输入…”.\n\nPanel 4: The character is slumped over the desk, exhausted and embarrassed, facing the laptop. The laptop screen shows the sent message “你好” in blue text, with a small sent/check indicator beneath it. The character’s posture should feel defeated and awkward. Title: “4 你好”.\n\nComposition rules: 4 equal quadrants, clean black border lines separating panels, lots of white space, minimal props, no complex background. Keep the character silhouette and clothing very close to the reference image. Use only a few orange accents for headphones and emotional emphasis. Do not add a title banner or explanatory text beyond the panel labels and the shown Chinese words. The illustration should feel like a direct visual continuation of the reference image style, with a funny emotional beat and simple, clear staging.", - "images": [ - "/assets/output/online_71caa86019.png" - ], - "image_items": [ - { - "url": "/assets/output/online_71caa86019.png", - "kind": "image", - "name": "online_71caa86019.png", - "natural_w": 1086, - "natural_h": 1448, - "width": 1086, - "height": 1448 - } - ], - "timestamp": 1784300317.3612318, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXR96BFQ95PXQ1FPMPG7M4WK", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1008x1344", - "requested_size": "1008x1344", - "quality": "auto", - "n": 1, - "reference_images": [ - { - "url": "/assets/input/ai_ref_a1b1721a87c8.png", - "name": "online_445550fed2.png", - "role": "", - "kind": "", - "mime": "" - } - ] - }, - "raw_usage": null - }, - { - "prompt": "A 4-panel manga grid illustration, 16:9 aspect ratio, pure white background, minimalist black hand-drawn line art, slightly wobbly pen lines, lots of empty white space, clean absurd product-sketch feeling.\n\nPanel 1 (Top Left): Close up of 小黑 (a small solid-black absurd creature with white dot eyes, tiny thin legs, blank serious expression, wearing a white t-shirt and orange headphones around neck) looking excited, holding a glowing keyboard, sparks flying. Label: \"配置 K3\" (Configure K3).\nPanel 2 (Top Right): 小黑 sitting at a desk, hands on a laptop, staring at the screen with a blank, confused expression. The screen shows nothing but a blinking cursor. Label: \"大脑空白\" (Brain Blank).\nPanel 3 (Bottom Left): A close up of 小黑's finger hovering over the 'Enter' key, trembling slightly. The atmosphere is tense. Label: \"输入...\" (Typing...).\nPanel 4 (Bottom Right): 小黑 slumped over the desk, defeated. On the laptop screen, the only text visible is \"你好\" (Hello) sent out into the void. Label: \"你好\" (Hello).\n\nStyle: Not cute, not commercial, deadpan, slightly bizarre. Hand-drawn aesthetic. Sparse use of color: Black for line art and character, Orange for headphones and arrows, Red for emphasis, Blue for system state.", - "images": [ - "/assets/output/online_a95e258ade.png" - ], - "image_items": [ - { - "url": "/assets/output/online_a95e258ade.png", - "kind": "image", - "name": "online_a95e258ade.png", - "natural_w": 1086, - "natural_h": 1448, - "width": 1086, - "height": 1448 - } - ], - "timestamp": 1784299966.032398, - "type": "online", - "model": "gpt-image-2", - "provider_id": "apimart", - "provider_name": "APIMART", - "task_id": "task_01KXR8VR4HE6QX56RY8WPRPNQF", - "request_id": null, - "params": { - "provider_id": "apimart", - "model": "gpt-image-2", - "size": "1008x1344", - "requested_size": "1008x1344", - "quality": "auto", - "n": 1, - "reference_images": [ - { - "url": "/assets/input/ai_ref_a1b1721a87c8.png", - "name": "online_445550fed2.png", - "role": "", - "kind": "", - "mime": "" - } - ] - }, - "raw_usage": null - }, - { - "prompt": "A minimalist pet brand logo combining geometric bridge structure with abstract dog ear silhouette, using blue-to-gold gradient to represent professionalism and warmth, with clean negative space and modern sans-serif typography below, flat vector style on white background", - "images": [ - "/assets/output/online_15c1be8c2a.png" - ], - "image_items": [ - { - "url": "/assets/output/online_15c1be8c2a.png", - "kind": "image", - "name": "online_15c1be8c2a.png", - "natural_w": 1024, - "natural_h": 1024, - "width": 1024, - "height": 1024 - } - ], - "timestamp": 1784287891.3438442, - "type": "online", - "model": "agnes-image-2.0-flash", - "provider_id": "agnes-ai", - "provider_name": "Agnes AI", - "task_id": null, - "request_id": null, - "params": { - "provider_id": "agnes-ai", - "model": "agnes-image-2.0-flash", - "size": "1024x1024", - "requested_size": "1024x1024", - "quality": "auto", - "n": 1, - "reference_images": [] - }, - "raw_usage": { - "total_tokens": 0, - "input_tokens": 0, - "input_tokens_details": { - "image_tokens": 0, - "text_tokens": 0 - }, - "output_tokens": 0 - } - }, - { - "prompt": "A realistic high-resolution photo of a light golden Golden Retriever running on a sunlit grass field with trees and blue sky in the background, detailed fur texture, dynamic motion blur, cinematic lighting", - "images": [ - "/assets/output/online_6244a8d5da.png" - ], - "image_items": [ - { - "url": "/assets/output/online_6244a8d5da.png", - "kind": "image", - "name": "online_6244a8d5da.png", - "natural_w": 1248, - "natural_h": 832, - "width": 1248, - "height": 832 - } - ], - "timestamp": 1784287466.141331, - "type": "online", - "model": "agnes-image-2.0-flash", - "provider_id": "agnes-ai", - "provider_name": "Agnes AI", - "task_id": null, - "request_id": null, - "params": { - "provider_id": "agnes-ai", - "model": "agnes-image-2.0-flash", - "size": "1536x1024", - "requested_size": "1536x1024", - "quality": "auto", - "n": 1, - "reference_images": [] - }, - "raw_usage": { - "total_tokens": 0, - "input_tokens": 0, - "input_tokens_details": { - "image_tokens": 0, - "text_tokens": 0 - }, - "output_tokens": 0 - } - } -] \ No newline at end of file From 6cb013ff24f70463dee86379aa0ee4b59116b19f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Sun, 19 Jul 2026 03:32:53 +0800 Subject: [PATCH 36/67] =?UTF-8?q?v1.3:=20=E6=80=9D=E7=BB=B4=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E5=BC=80=E5=85=B3+=E5=85=A8=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E6=A8=A1=E7=B3=8A=E9=9C=80=E6=B1=82=E7=BB=9F=E4=B8=80+?= =?UTF-8?q?=E5=8F=91=E9=80=81=E6=8C=89=E9=92=AE=E4=BF=AE=E5=A4=8D+LLM?= =?UTF-8?q?=E8=B6=85=E6=97=B6=E4=BF=9D=E6=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 24 ++++- VERSION | 2 +- static/css/smart-canvas.css | 54 ++++++++++ static/js/smart-canvas.js | 194 +++++++++++++++++++++++++++++++++++- static/smart-canvas.html | 8 ++ 5 files changed, 275 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 047ac967c..8086efcf1 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Supports comfyui/API calls/modelscope calls > 本仓库是基于原项目 [hero8152/Infinite-Canvas](https://github.com/hero8152/Infinite-Canvas) 的二次开发 fork,新增了智能画布 AI Agent 面板。 > -> **当前版本:v1.1**(查看 [版本更新日志](#版本更新日志)) +> **当前版本:v1.3**(查看 [版本更新日志](#版本更新日志)) ### v1.0 新增功能(AI Agent 面板基线) 1. AI Agent 侧边聊天面板(OneBox 风格),可与画布联动生图。 @@ -48,6 +48,27 @@ Supports comfyui/API calls/modelscope calls 16. **复制优先 prompt**:复制 Agent 消息时优先复制生图 prompt,而非「正在生图中」状态文字。 17. **多选场景自由输入提示**:仅在需要用户多选/澄清时显示「也可直接输入」提示,不常驻。 +### v1.3 更新内容 + +#### 一、思维模式(扩写/优化提示词) +1. **思维模式开关**:在发送按钮左侧新增「思维模式」开关(脑图标 + 文字 + Tooltip)。开启后强制每次发送都先走 LLM 扩写提示词,再进入「确认 / 重新生成 / 修改提示词」三扣确认流程,用户完全掌控最终 prompt 后才生图。 +2. **确认流程**:LLM 返回的 `generations` 自动转换为 `prompts` 数组,通过 `promptIdx` 实现多图逐一确认。点击「确认」即生图并推进到下一条;「重新生成」会要求 LLM 换个方向重写;「修改」则把当前提示词回填输入框并设置绕过标志,下一条指令直接生图。 +3. **绕过机制**:用户点击「修改提示词」后设置 `agentBypassThinkingNext`,使下一条指令跳过思维模式直接生图,避免修改后还要再确认一次的冗余。 +4. **修改请求识别**:思维模式下识别「改成/换成/重新画」等修改请求关键词,不强制走扩写流程,直接生图。 + +#### 二、模糊需求统一处理 +5. **全模型统一**:所有模型(不再仅限魔搭)在面对模糊指令(如「一只猫」)时,都会触发风格选项流,而非直接发散生成。 +6. **前端兜底**:思维模式关闭时,通过 `isVagueImageRequest` 检测模糊输入并自动生成固定风格选项。兜底逻辑不再依赖 LLM `generations` 数量,只要 `options` 和 `prompts` 为空即触发。 + +#### 三、稳定性修复 +7. **发送按钮失效修复**:修复 `agentBypassThinkingNext` 变量未声明导致 `sendAgentMessage` 抛出 `ReferenceError`、发送按钮点击无反应的问题。 +8. **LLM 任务超时保护**:`pollAgentLlmTask` 由 600 次循环(约 30 分钟)缩减为 120 次并增加 5 分钟硬超时,防止任务卡死导致 `agentSending` 永远为 `true`。 +9. **恢复逻辑超时清理**:页面刷新后若 pending 任务超过 5 分钟,自动清除 `_pendingLlmTaskId` / `_pendingLlmTaskTs` 等状态,不再尝试恢复已过期的任务。 + +#### 四、UI/UX +10. **思维模式开关视觉**:按钮带高亮 `active` 态、`flex-shrink:0` 防压缩遮挡发送按钮;Tooltip 使用固定深色背景 + 高 `z-index`(9999)确保可见。 +11. **提示词确认卡片**:结构化展示当前提示词、序号计数(如 1/3)及三个操作按钮。 + ### 重要警示 - **已关闭自动更新**:导航栏版本号显示为「Agent版(无自动更新)」。若从上游拉取/合并最新代码,可能覆盖本分支的 AI Agent 功能,请谨慎操作并先备份。 - **main 分支与上游分叉**:本 fork 的 `main` 通过强制推送覆盖,历史与上游不一致,切勿直接向上游发起合并。 @@ -62,6 +83,7 @@ Supports comfyui/API calls/modelscope calls |------|------|------| | v1.0 | 2026-07-18 | AI Agent 面板基线:聊天面板、画布联动生图、多对话管理、参数面板、结构化确认流程、生图占位 | | v1.1 | 2026-07-19 | 系统提示词重写、修改场景智能区分、LLM 任务后端化、刷新恢复、WebSocket 实时通知、占位比例修复、并发多图不叠加、提示词展开收起 | +| v1.3 | 2026-07-19 | 思维模式开关(扩写/确认/重新生成/修改)、全模型模糊需求统一触发选择、发送按钮失效修复、LLM 任务 5 分钟超时保护、恢复逻辑超时清理 | ---- diff --git a/VERSION b/VERSION index 3a6a8d03e..e7f45a810 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v1.1 +v1.3 diff --git a/static/css/smart-canvas.css b/static/css/smart-canvas.css index 9bbc14b82..15349ee0f 100644 --- a/static/css/smart-canvas.css +++ b/static/css/smart-canvas.css @@ -1199,6 +1199,60 @@ input[type=range].smart-range::-webkit-slider-thumb { -webkit-appearance:none; w .agent-msg-options { display:flex; flex-direction:column; gap:6px; margin-top:6px; align-items:stretch; } .agent-msg-option-btn { min-height:28px; height:auto; padding:6px 12px; border-radius:8px; border:1px solid var(--line); background:var(--card); color:var(--text); font-size:10.5px; font-weight:700; cursor:pointer; text-align:left; white-space:normal; word-break:break-word; line-height:1.4; transition:border-color .14s ease, background .14s ease; } .agent-msg-option-btn:hover { border-color:var(--strong); background:var(--soft); } + +/* 思维模式开关按钮 */ +.agent-thinking-btn { + display:inline-flex; align-items:center; gap:3px; + height:28px; padding:0 8px; border-radius:8px; border:none; + background:var(--soft); color:var(--muted); + font-size:10.5px; font-weight:700; cursor:pointer; position:relative; + transition:all .14s ease; white-space:nowrap; +} +.agent-thinking-btn:hover { color:var(--text); background:var(--line); } +.agent-thinking-btn .agent-thinking-icon { width:14px; height:14px; flex-shrink:0; } +.agent-thinking-btn .agent-thinking-label { line-height:1; } +.agent-thinking-btn.active { + background:var(--strong); color:var(--strong-text); border-color:var(--strong); +} +.agent-thinking-btn.active .agent-thinking-icon { color:var(--strong-text); } +.agent-thinking-btn.active:hover { opacity:.88; } +.agent-thinking-tooltip { + position:absolute; bottom:calc(100% + 8px); right:0; + background:#1e293b; color:#f1f5f9; + padding:8px 12px; border-radius:8px; white-space:nowrap; + pointer-events:none; opacity:0; visibility:hidden; + transform:translateY(4px); + transition:opacity .14s ease, transform .14s ease, visibility .14s ease; + z-index:9999; + box-shadow:0 4px 12px rgba(0,0,0,.25); +} +.agent-thinking-btn:hover .agent-thinking-tooltip { opacity:1; visibility:visible; transform:translateY(0); } +.agent-tooltip-title { font-size:12px; font-weight:700; } +.agent-tooltip-sub { font-size:10px; opacity:.7; margin-top:3px; } + +/* 思维模式提示词确认卡片 */ +.agent-prompt-card { + margin-top:6px; border:1px solid var(--line); border-radius:10px; + background:var(--card); overflow:hidden; +} +.agent-prompt-card-header { + padding:5px 10px; font-size:10px; font-weight:700; color:var(--muted); + background:var(--soft); display:flex; align-items:center; gap:4px; +} +.agent-prompt-card-body { + padding:8px 10px; font-size:11px; line-height:1.5; color:var(--text); + word-break:break-word; white-space:pre-wrap; max-height:200px; overflow-y:auto; +} +.agent-prompt-card-actions { display:flex; gap:4px; padding:6px 8px; } +.agent-prompt-btn { + flex:1; min-height:26px; padding:4px 6px; border-radius:6px; + border:1px solid var(--line); background:var(--card); color:var(--text); + font-size:10px; font-weight:650; cursor:pointer; text-align:center; + transition:border-color .14s ease, background .14s ease; white-space:nowrap; +} +.agent-prompt-btn:hover { border-color:var(--muted); background:var(--soft); } +.agent-prompt-btn.primary { background:var(--strong); color:var(--strong-text); border-color:var(--strong); } +.agent-prompt-btn.primary:hover { opacity:.88; } .agent-msg-free-hint { margin-top:4px; font-size:9.5px; opacity:.45; } /* 消息操作按钮 */ diff --git a/static/js/smart-canvas.js b/static/js/smart-canvas.js index b4784ab2d..dcc077a04 100644 --- a/static/js/smart-canvas.js +++ b/static/js/smart-canvas.js @@ -15227,7 +15227,12 @@ function handleAgentLlmDoneMessage(data){ } async function pollAgentLlmTask(taskId){ if(!taskId) throw new Error('Invalid task ID'); - for(let i = 0; i < 600; i++){ + const startTime = Date.now(); + const MAX_DURATION = 5 * 60 * 1000; // 5 分钟硬超时 + for(let i = 0; i < 120; i++){ + if(Date.now() - startTime > MAX_DURATION){ + throw new Error('LLM task timeout (5min)'); + } const wsNotify = new Promise(resolve => { _agentLlmWaiters.set(taskId, (status) => resolve(status || 'done')); }); @@ -16783,6 +16788,7 @@ B. 主体更换(use_last_outputs必须为true): let agentOpen = false; let agentSending = false; let agentThinking = false; +let agentBypassThinkingNext = false; let agentSaveTimer = null; let agentState = null; let agentMentionIdx = -1; @@ -16834,6 +16840,16 @@ function loadAgentState(){ let _agentRecoveryInProgress = false; function _setupAgentRecovery(){ if(!agentState) return; + // 超时保护:如果 pending 任务超过 5 分钟,直接清除,不恢复 + const pendingTs = agentState._pendingLlmTaskTs || 0; + if(pendingTs && (Date.now() - pendingTs > 5 * 60 * 1000)){ + delete agentState._pendingMessage; + delete agentState._pendingAttachments; + delete agentState._pendingUserMsg; + delete agentState._pendingLlmTaskId; + delete agentState._pendingLlmTaskTs; + saveAgentState(); + } const pendingLlmTaskId = agentState._pendingLlmTaskId; const pendingText = String(agentState._pendingMessage || ''); const pendingAttachments = Array.isArray(agentState._pendingAttachments) ? agentState._pendingAttachments : []; @@ -16874,6 +16890,7 @@ function _setupAgentRecovery(){ delete agentState._pendingAttachments; delete agentState._pendingUserMsg; delete agentState._pendingLlmTaskId; + delete agentState._pendingLlmTaskTs; renderAgentMessages(); saveAgentState(); } @@ -16925,6 +16942,7 @@ function _setupAgentRecovery(){ delete agentState._pendingAttachments; delete agentState._pendingUserMsg; delete agentState._pendingLlmTaskId; + delete agentState._pendingLlmTaskTs; } function saveAgentState(){ clearTimeout(agentSaveTimer); @@ -17169,7 +17187,17 @@ function agentMessageHtml(msg){ const hasGenerations = Array.isArray(msg.generations) && msg.generations.length > 0; const options = (!hasGenerations && Array.isArray(msg.options)) ? msg.options : []; const optionsHtml = options.length ? `
${options.map(opt => ``).join('')}
` : ''; - return `
${msg.text ? `
${escapeHtml(msg.text)}
` : ''}${imgs ? `
${imgs}
` : ''}${gens}${optionsHtml}${actions}
`; + // 思维模式提示词确认卡片(有未确认的 prompts 时显示) + let promptCardHtml = ''; + if(msg.role === 'assistant' && Array.isArray(msg.prompts) && msg.prompts.length > 0){ + const idx = msg.promptIdx || 0; + if(idx < msg.prompts.length){ + const currentPrompt = msg.prompts[idx]; + const counter = msg.prompts.length > 1 ? ` (${idx + 1}/${msg.prompts.length})` : ''; + promptCardHtml = `
📝 提示词${counter}
${escapeHtml(currentPrompt)}
`; + } + } + return `
${msg.text ? `
${escapeHtml(msg.text)}
` : ''}${imgs ? `
${imgs}
` : ''}${gens}${promptCardHtml}${optionsHtml}${actions}
`; } function renderAgentMessages(){ if(!agentMessages || !agentState) return; @@ -17247,6 +17275,21 @@ function renderAgentMessages(){ } }; }); + // 绑定提示词确认卡片按钮事件 + agentMessages.querySelectorAll('[data-agent-prompt-action]').forEach(btn => { + btn.disabled = agentSending; + btn.onclick = e => { + e.stopPropagation(); + if(agentSending) return; + const action = btn.dataset.agentPromptAction; + const msgId = btn.dataset.agentPromptId; + const msg = (agentState.messages || []).find(m => m.id === msgId); + if(!msg) return; + if(action === 'confirm') confirmAgentPrompt(msg); + else if(action === 'regenerate') regenerateAgentPrompts(msg); + else if(action === 'edit') editAgentPrompt(msg); + }; + }); // 绑定生成图片点击跳转事件 agentMessages.querySelectorAll('[data-agent-gen-jump]').forEach(img => { img.onclick = e => { @@ -17668,13 +17711,30 @@ async function processAgentLlmResult(result, text, attachments, userMsg){ if(requestedCount > 0 && parsed.generations.length > 0 && parsed.generations.length < requestedCount){ parsed.reply += `${AGENT_NL}(注意:请求了 ${requestedCount} 张,但仅生成了 ${parsed.generations.length} 张的提示词)`; } + const bypassThinking = userMsg?.bypassThinking === true; + const thinkingModeOn = agentState?.thinkingMode && !bypassThinking; + if(thinkingModeOn){ + const userModifyRe = /改成|换成|转换成|修改为|变成|转为|改为|转成|调整为|修改成|变回|调成|重新画|重画|重新生成|修改一下|改一下|调整一下/i; + const isModifyRequest = userModifyRe.test(text); + if(!isModifyRequest){ + if(parsed.generations.length > 0 && parsed.options.length === 0){ + parsed.prompts = parsed.generations.map(g => String(g.prompt || '').trim()).filter(p => p); + parsed.generations = []; + } + if(parsed.prompts.length === 0 && parsed.options.length === 0 && parsed.generations.length === 0){ + parsed.prompts = [text]; + if(!parsed.reply) parsed.reply = '请确认以下提示词:'; + } + } + } const assistantMsg = {id:uid('am'), role:'assistant', text:parsed.reply, options:parsed.options || [], prompts:parsed.prompts || [], generations:parsed.generations, ts:Date.now()}; + if(assistantMsg.prompts.length > 0) assistantMsg.promptIdx = 0; agentState.messages.push(assistantMsg); agentState.messages = agentState.messages.slice(-AGENT_MSG_MAX); agentThinking = false; renderAgentMessages(); saveAgentState(); - if(assistantMsg.generations.length) await runAgentGenerations(assistantMsg, userMsg); + if(assistantMsg.generations.length && assistantMsg.prompts.length === 0) await runAgentGenerations(assistantMsg, userMsg); } async function sendAgentMessage(){ if(agentSending || !agentState) return; @@ -17687,6 +17747,9 @@ async function sendAgentMessage(){ agentState.chatProvider = provider; agentState.chatModel = model; const userMsg = {id:uid('am'), role:'user', text, images:attachments, ts:Date.now()}; + const bypassThinking = agentBypassThinkingNext; + agentBypassThinkingNext = false; + userMsg.bypassThinking = bypassThinking; agentState.messages.push(userMsg); agentState.messages = agentState.messages.slice(-AGENT_MSG_MAX); agentState.attachments = []; @@ -17714,7 +17777,7 @@ async function sendAgentMessage(){ model, provider, ms_model:provider === 'modelscope' ? model : '', - system_prompt:agentSystemPrompt() + system_prompt:agentSystemPrompt(bypassThinking) }; try { // 创建后端 LLM 任务(息屏/刷新不会丢失) @@ -17730,10 +17793,12 @@ async function sendAgentMessage(){ if(!llmTaskId) throw new Error('Failed to create LLM task'); // 保存 LLM task ID,刷新后可恢复 agentState._pendingLlmTaskId = llmTaskId; + agentState._pendingLlmTaskTs = Date.now(); saveAgentState(); // 等待 LLM 结果(WebSocket 实时通知 + 轮询保底) const result = await pollAgentLlmTask(llmTaskId); delete agentState._pendingLlmTaskId; + delete agentState._pendingLlmTaskTs; // 处理结果 await processAgentLlmResult(result, text, attachments, userMsg); } catch(e) { @@ -17751,6 +17816,7 @@ async function sendAgentMessage(){ delete agentState._pendingAttachments; delete agentState._pendingUserMsg; delete agentState._pendingLlmTaskId; + delete agentState._pendingLlmTaskTs; saveAgentState(); } renderAgentMessages(); @@ -17819,6 +17885,109 @@ function agentPendingBoxSize(count, options={}){ const h = rows * (cellH + 8) + 16; return {w, h}; } +// 确认当前提示词:创建 generation 并运行,然后展示下一个提示词 +async function confirmAgentPrompt(assistantMsg){ + const idx = assistantMsg.promptIdx || 0; + const prompts = assistantMsg.prompts || []; + if(idx >= prompts.length) return; + const prompt = prompts[idx]; + // 找到对应的用户消息(用于 use_attachments 判断) + const msgs = agentState.messages || []; + const msgIdx = msgs.indexOf(assistantMsg); + let userMsg = null; + for(let i = msgIdx - 1; i >= 0; i--){ + if(msgs[i].role === 'user'){ userMsg = msgs[i]; break; } + } + // 创建 generation 并运行 + const gen = {prompt, count:1, use_last_outputs:false, use_attachments:!!(userMsg?.images?.length), results:[], status:'running'}; + if(!Array.isArray(assistantMsg.generations)) assistantMsg.generations = []; + assistantMsg.generations.push(gen); + // 推进到下一个提示词 + assistantMsg.promptIdx = idx + 1; + renderAgentMessages(); + saveAgentState(); + // 运行生图(异步,不阻塞下一个提示词的展示) + runAgentGenerations(assistantMsg, userMsg); +} +// 重新生成提示词:重新发送原始用户消息给 LLM +async function regenerateAgentPrompts(assistantMsg){ + const msgs = agentState.messages || []; + const msgIdx = msgs.indexOf(assistantMsg); + let originalUserText = ''; + let userMsg = null; + for(let i = msgIdx - 1; i >= 0; i--){ + if(msgs[i].role === 'user'){ + originalUserText = msgs[i].text || ''; + userMsg = msgs[i]; + break; + } + } + if(!originalUserText) return; + const provider = resolveChatProviderId(agentState.chatProvider); + const model = resolveChatModel(agentState.chatModel, provider); + agentSending = true; + agentThinking = true; + renderAgentMessages(); + const llmPayload = { + message: originalUserText + '\n\n请重新生成不同方向的提示词,与之前的提示词有所区别。', + messages: agentHistoryMessages().slice(0, -1), + images: userMsg?.images ? userMsg.images.map(i => i.url) : [], + videos: [], + model, + provider, + ms_model: provider === 'modelscope' ? model : '', + system_prompt: agentSystemPrompt(false) + }; + try { + const taskRes = await fetch('/api/agent-llm-task', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(llmPayload) + }).then(async r => { + if(!r.ok) throw new Error(await responseErrorMessage(r, tr('smart.promptLlmFailed'))); + return r.json(); + }); + const result = await pollAgentLlmTask(taskRes.task_id); + const parsed = parseAgentResponse(result.text || '', originalUserText); + // 替换当前消息的 prompts + assistantMsg.prompts = parsed.prompts && parsed.prompts.length > 0 ? parsed.prompts : (parsed.generations && parsed.generations.length > 0 ? parsed.generations.map(g => g.prompt) : [originalUserText]); + assistantMsg.promptIdx = 0; + if(parsed.reply) assistantMsg.text = parsed.reply; + if(parsed.options && parsed.options.length > 0){ + assistantMsg.options = parsed.options; + assistantMsg.prompts = []; + } else { + assistantMsg.options = []; + } + } catch(e) { + assistantMsg.text = `⚠️ ${String(e.message || e).slice(0, 300)}`; + } finally { + agentSending = false; + agentThinking = false; + renderAgentMessages(); + saveAgentState(); + } +} +// 修改提示词:复制到输入框,设置绕过标志 +function editAgentPrompt(assistantMsg){ + const idx = assistantMsg.promptIdx || 0; + const prompts = assistantMsg.prompts || []; + if(idx >= prompts.length) return; + const prompt = prompts[idx]; + if(agentInput){ + agentInput.value = prompt; + agentInput.focus(); + // 自动调整高度 + agentInput.style.height = 'auto'; + agentInput.style.height = Math.min(agentInput.scrollHeight, 200) + 'px'; + } + // 设置绕过标志:下次发送时不走思维模式,直接生图 + agentBypassThinkingNext = true; + // 标记当前提示词已处理(用户选择了修改,不再显示) + assistantMsg.promptIdx = (assistantMsg.promptIdx || 0) + 1; + renderAgentMessages(); + saveAgentState(); +} async function runAgentGenerations(assistantMsg, userMsg){ const gens = assistantMsg.generations || []; if(!gens.length) return; @@ -17839,7 +18008,7 @@ async function runAgentGenerations(assistantMsg, userMsg){ const lastResults = agentLastResults(); const currentAttach = (userMsg?.images || []).filter(i => i?.url); const attachRefs = currentAttach.length ? currentAttach : agentLastUserAttachments(); - await Promise.all(gens.map(async gen => { + await Promise.all(gens.filter(gen => !(gen.results && gen.results.length) && gen.status !== 'done' && gen.status !== 'error').map(async gen => { gen.status = 'running'; renderAgentMessages(); // 先创建占位节点(复用主画布的 pendingBoxSize 逻辑,按当前选择的比例动态计算) @@ -18243,6 +18412,21 @@ function initAgentPanel(){ agentImageInput.value = ''; }); agentSendBtn?.addEventListener('click', () => sendAgentMessage()); + // 思维模式开关按钮 + const agentThinkingBtn = document.getElementById('agentThinkingBtn'); + function syncAgentThinkingBtn(){ + if(agentThinkingBtn){ + agentThinkingBtn.classList.toggle('active', !!agentState?.thinkingMode); + } + } + syncAgentThinkingBtn(); + agentThinkingBtn?.addEventListener('click', () => { + if(!agentState) return; + agentState.thinkingMode = !agentState.thinkingMode; + syncAgentThinkingBtn(); + saveAgentState(); + toast(agentState.thinkingMode ? '思维模式已开启:扩写/优化提示词' : '思维模式已关闭:直接生成'); + }); agentInput?.addEventListener('input', () => { const val = agentInput.value; const cursorPos = agentInput.selectionStart || 0; diff --git a/static/smart-canvas.html b/static/smart-canvas.html index b9f755228..50699f4d9 100644 --- a/static/smart-canvas.html +++ b/static/smart-canvas.html @@ -299,6 +299,14 @@
+ From 24012abf6d5bb63cd7dec0c9b0f2801461ca7442 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Sun, 19 Jul 2026 15:33:53 +0800 Subject: [PATCH 37/67] =?UTF-8?q?v1.4:=20Agent=20=E6=80=9D=E7=BB=B4?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E5=A4=9A=E5=9B=BE=E7=A1=AE=E8=AE=A4=E6=B5=81?= =?UTF-8?q?=E7=A8=8B=E9=87=8D=E6=9E=84=20+=20skill=20=E5=AE=8C=E6=95=B4?= =?UTF-8?q?=E4=BF=9D=E7=95=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PLAN_MULTI_IMAGE.md | 336 ++++++++++++++++++++ static/angle.html | 18 +- static/api-settings.html | 14 +- static/asset-manager.html | 10 +- static/canvas-list.html | 16 +- static/canvas.html | 18 +- static/comfyui-settings.html | 14 +- static/css/smart-canvas.css | 28 ++ static/enhance.html | 16 +- static/gpt-chat.html | 12 +- static/index.html | 28 +- static/js/smart-canvas.js | 573 +++++++++++++++++++++++++++++------ static/klein.html | 16 +- static/online.html | 16 +- static/smart-canvas.html | 12 +- static/zimage.html | 14 +- 16 files changed, 944 insertions(+), 197 deletions(-) create mode 100644 PLAN_MULTI_IMAGE.md diff --git a/PLAN_MULTI_IMAGE.md b/PLAN_MULTI_IMAGE.md new file mode 100644 index 000000000..b48d8d6a2 --- /dev/null +++ b/PLAN_MULTI_IMAGE.md @@ -0,0 +1,336 @@ +# 思维模式多图确认流程重构计划 + +## 项目背景 + +项目路径:`/Users/yangfan/Desktop/Infinite-Canvas` +核心文件: +- `static/js/smart-canvas.js`(约18863行)—— Agent 面板全部逻辑 +- `static/css/smart-canvas.css`(约1560行)—— Agent 面板样式 +- `static/smart-canvas.html`—— Agent 面板 HTML 结构 +- `main.py`(约17677行)—— 后端服务 + +当前版本:v1.3,已有思维模式开关功能,但多图确认流程有严重 bug。 + +--- + +## 用户反馈的核心问题 + +### 问题1:选项流丢失数量信息 +用户说"生成4张龙",LLM 返回4个龙的风格选项(options)。用户选了一个后,代码把选项的 value 填入输入框并发送 sendAgentMessage()。但这个 value 是类似"龙,风格:水墨"的描述,不包含"4张"。LLM 收到后只生成1张。 + +代码位置:`static/js/smart-canvas.js` 第17268行 +```js +agentInput.value = value; // value 只有风格描述,没有数量 +sendAgentMessage(); +``` + +### 问题2:确认一张就开始生图,不等全部确认 +思维模式下,confirmAgentPrompt 每次确认后立即调用 runAgentGenerations。用户确认第1个后画布就开始出占位/生图了,还没确认完后面3个。 + +代码位置:`static/js/smart-canvas.js` 第17888-17911行 +```js +async function confirmAgentPrompt(assistantMsg){ + // ... + assistantMsg.generations.push(gen); + assistantMsg.promptIdx = idx + 1; + // 确认后立即生图!应该等全部确认完再生 + runAgentGenerations(assistantMsg, userMsg); +} +``` + +--- + +## 所有需要修复的问题(按优先级) + +### P0:必须修复(解决核心 bug) + +#### 1. prompts 数据结构升级(string[] → object[]) + +当前 prompts 是纯字符串数组,确认时无法携带 count、use_last_outputs、use_attachments。 + +当前代码(第17721行): +```js +parsed.prompts = parsed.generations.map(g => String(g.prompt || '').trim()).filter(p => p); +``` +只提取了文本,count/use_last_outputs/use_attachments 全丢了。 + +改为对象数组: +```js +prompts: [ + {prompt: "水墨龙...", count: 1, use_last_outputs: false, use_attachments: true, status: "pending"}, + {prompt: "油画龙...", count: 1, use_last_outputs: false, use_attachments: true, status: "pending"}, +] +``` + +每个 prompt 对象携带完整生成属性 + 独立状态。 + +#### 2. 每个 prompt 独立状态 + +当前只有一个 `promptIdx` 指向"当前显示第几个"。 + +改为每个 prompt 有独立 status: +- `pending`:还没轮到 +- `current`:当前正在看 +- `confirmed`:已确认 +- `skipped`:跳过不生成 +- `editing`:正在内联编辑 + +#### 3. 四个操作按钮重新定义 + +当前操作(第17278-17291行):确认/重新生成/修改 + +改为:**确认 / 修改 / 重新生成 / 跳过** + +- **确认**:标记当前为 confirmed,自动跳到下一个 pending,如果没有下一个了→触发生图 +- **修改**:当前提示词进入内联编辑模式(直接在卡片里改文本,不跳到输入框),用户改完点"保存"→标记为 confirmed。不再设置 agentBypassThinkingNext,不跳出确认流程 +- **重新生成**:只重新生成当前这一条,不影响其他已确认的。发给 LLM:"请重新生成第N条提示词,要求与之前不同"。LLM 返回新 prompt 文本,替换当前这一条 +- **跳过**(新增):标记当前为 skipped,不生成这张,跳到下一个 + +#### 4. 全部确认后才触发生图 + +改 `confirmAgentPrompt`(第17888行): +- 确认后只标记 status=confirmed,推进到下一个 pending +- 检查是否还有 pending 状态的 prompt +- 如果没有 pending 了(全部 confirmed 或 skipped)→ 才调用 runAgentGenerations +- 生图时只生成 status==="confirmed" 的,跳过 skipped 的 + +`runAgentGenerations` 一次性接收所有 confirmed prompts,统一计算占位位置,整齐排列。 + +#### 5. 选项选择时补全上下文 + +改第17268行,选项被选中后不是只发选项 value,而是带上原始请求: + +```js +// 找到触发选项流的原始用户消息 +const originalRequest = findOriginalRequest(); +agentInput.value = `${originalRequest},选择:${value}`; +// 例如:"生成4张龙,选择:龙,风格:水墨" +``` + +#### 6. 透传 LLM 的 generation 属性 + +确认创建 generation 时(第17902行),不再重置 count/use_last_outputs/use_attachments,而是保留 LLM 原始返回的值: + +当前代码: +```js +const gen = {prompt, count:1, use_last_outputs:false, use_attachments:false, results:[], status:'running'}; +``` + +改为: +```js +const gen = { + prompt: confirmedPrompt.prompt, + count: confirmedPrompt.count || 1, + use_last_outputs: confirmedPrompt.use_last_outputs || false, + use_attachments: confirmedPrompt.use_attachments || false, + results: [], + status: 'running' +}; +``` + +--- + +### P1:体验提升 + +#### 7. 全部确认快捷按钮 + +当 prompts ≥ 2 时,显示"全部确认并生成"按钮。点击后所有 pending 标记为 confirmed,立即触发生图。 + +#### 8. 修改改为内联编辑 + +当前 `editAgentPrompt`(第17971行)把 prompt 复制到输入框,设置 bypass 标志,发送后跳出流程。 + +改为:点"修改"→卡片内提示词文本变成可编辑 textarea→用户改完点"保存"→标记 confirmed→不离开确认流程。 + +#### 9. 系统提示词动态化 + +当前 `agentSystemPrompt()`(第17481行)不接受参数,思维模式开/关发给 LLM 的提示词完全相同。 + +改为根据思维模式开关动态构建: + +思维模式 ON 时追加: +``` +当前为思维模式。请返回 prompts 数组(不要返回 generations)。 +每个 prompt 对象包含:prompt(中文提示词), count, use_last_outputs, use_attachments。 +用户会逐个确认后才生图。 +如果请求模糊,可以先返回 options 让用户选方向,下一轮再返回 prompts。 +如果是修改请求("换成像素风"),仍返回 prompts,但 use_last_outputs 设为 true。 +用户请求N张图时,prompts 必须返回恰好N条。 +``` + +思维模式 OFF 时追加: +``` +当前为直接模式。能生成就生成,返回 generations 数组。 +``` + +注意:`agentSystemPrompt(bypassThinking)` 在第17780行已经被调用并传参,但函数定义(第17481行)没接收参数。需要修复函数签名。 + +#### 10. 确认进度持久化(刷新恢复) + +当前 `_setupAgentRecovery`(第16841行)只恢复 LLM task 和生图 task。 + +需要增加分支:检测到消息有 prompts 且有 pending 状态的 → 恢复确认卡片显示,不触发生图,用户继续从上次中断的地方确认。 + +需持久化:每个 prompt 的 status、当前 promptIdx、prompts 数组本身(已有)。 + +#### 11. 状态显示 UI(进度、折叠) + +卡片 UI 展示进度: +``` +┌─────────────────────────────────┐ +│ 📝 提示词确认 (2/4 已确认) │ +│ ✓ #1 水墨龙... [已确认] │ ← 折叠,可点击展开反悔 +│ ✓ #2 油画龙... [已确认] │ ← 折叠,可点击展开反悔 +│ ▶ #3 赛博龙... │ ← 当前,展开操作按钮 +│ [确认] [修改] [重新生成] [跳过] │ +│ ○ #4 Q版龙... │ ← 待处理,灰色 +│ [全部确认并生成] [全部取消] │ +└─────────────────────────────────┘ +``` + +--- + +### P2:完善 + +#### 12. 数量校验 + +用户说"4张龙",LLM 返回3个 prompts。 +- 卡片顶部显示"用户请求4张,当前3条提示词" +- 提供"补充提示词"按钮 → 让 LLM 再生成几条补齐 +- 如果 LLM 返回比请求的多,用户可以跳过多余的 + +#### 13. 已确认可反悔 + +已确认的卡片显示为折叠状态 + ✓ 标记。点击展开重新显示操作按钮,用户可以"取消确认"(改回 pending)或"修改"。 + +#### 14. 确认中发送新消息拦截 + +检测到有未完成的 prompts(pending 状态存在)时,弹 toast:"还有N条提示词未确认,是否放弃当前确认?"用户确认放弃→清除 prompts,发送新消息。 + +#### 15. 全部取消按钮 + +用户看到 prompts 后完全不满意,点"全部取消"→清除当前 assistant 消息的 prompts,不触发生图,用户重新输入。 + +--- + +## 需要去掉的前端限制(数据驱动改造) + +以下前端逻辑替 LLM 做了决定,应该去掉或弱化: + +### 去掉:思维模式强制转 prompts(第17716-17728行) + +当前思维模式开启时,强制把 generations 转 prompts,不管 LLM 原本返回什么。 +改为:系统提示词告诉 LLM 返回 prompts,前端不做强制转换。如果 LLM 仍返回 generations,说明它认为不需要确认,直接生图。 + +### 去掉:`chatRequestedImageCount`(第16972行) + +当前用正则提取"4张",只认1-4。 +改为:完全交给 LLM 理解数量。LLM 在 generations/prompts 中返回对应数量。 + +### 去掉:修改请求正则识别(第17717行) + +当前用正则匹配"改成/换成"等关键词,命中就跳过扩写流程。 +改为:LLM 自己判断是不是修改请求,在 prompts 中设 use_last_outputs。 + +### 去掉:兜底构造 generation(第17665-17690行) + +当前 LLM 没返回 generations 时,前端用正则猜意图自己构造。 +改为:LLM 没返回就不生,显示 LLM 的 reply/options。 + +### 保留:JSON 兜底解析(第17502-17548行) + +只在 JSON 解析失败时用,合理。 + +### 保留:占位节点/生图任务/WebSocket(前端职责) + +这些是纯前端职责,LLM 管不了。 + +--- + +## 呈现规则(纯数据驱动) + +LLM 返回的 JSON 字段组合 → 前端呈现方式: + +``` +{generations: [...]} → 直接生图(LLM 认为可以生成了) +{options: [...]} → 显示选项按钮(LLM 认为需要用户选择) +{prompts: [...]} → 显示确认卡片(LLM 认为需要用户确认) +{reply: "..."} → 纯文字回复(LLM 认为需要对话) +{options + generations} → 先选项,选完直接生图 +{options + prompts} → 先选项,选完进入确认 +{reply + generations} → 一边回复一边生图 +``` + +不再用 thinkingModeOn / isModifyRequest / chatRequestedImageCount 来决定走哪个分支,完全看 LLM 返回了什么。 + +--- + +## 关键代码位置索引 + +| 功能 | 文件 | 行号 | 函数/变量名 | +|------|------|------|------------| +| 系统提示词 | smart-canvas.js | 16750 | AGENT_FORMAT_INSTRUCTION | +| agentSystemPrompt | smart-canvas.js | 17481 | agentSystemPrompt()(需改成接收参数) | +| 数量提取 | smart-canvas.js | 16972 | chatRequestedImageCount()(建议去掉) | +| 修改请求正则 | smart-canvas.js | 17717 | userModifyRe(建议去掉) | +| 兜底构造generation | smart-canvas.js | 17665-17690 | genInProgressRe等(建议去掉) | +| 思维模式强制转prompts | smart-canvas.js | 17716-17728 | thinkingModeOn 块(建议去掉) | +| 批量完整性提示 | smart-canvas.js | 17711 | requestedCount(改为校验) | +| 消息HTML渲染 | smart-canvas.js | 17185-17201 | agentMessageHtml() | +| 选项按钮绑定 | smart-canvas.js | 17243-17277 | data-agent-option | +| 确认卡片绑定 | smart-canvas.js | 17278-17292 | data-agent-prompt-action | +| 选项"确认"处理 | smart-canvas.js | 17248-17267 | value === '确认' 分支 | +| 选项选择后发送 | smart-canvas.js | 17268-17275 | sendAgentMessage() | +| 确认提示词 | smart-canvas.js | 17888-17911 | confirmAgentPrompt() | +| 重新生成提示词 | smart-canvas.js | 17912-17970 | regenerateAgentPrompts() | +| 修改提示词 | smart-canvas.js | 17971-17990 | editAgentPrompt() | +| 执行生图 | smart-canvas.js | 17991+ | runAgentGenerations() | +| 恢复中断操作 | smart-canvas.js | 16841 | _setupAgentRecovery() | +| LLM 任务轮询 | smart-canvas.js | 15228 | pollAgentLlmTask() | +| 发送消息 | smart-canvas.js | 17739 | sendAgentMessage() | +| 处理LLM结果 | smart-canvas.js | ~17600 | processAgentLlmResult() | +| 变量声明区 | smart-canvas.js | 16788-16794 | agentSending/agentThinking/agentBypassThinkingNext | +| 提示词卡片HTML | smart-canvas.js | 17190-17199 | promptCardHtml | +| 提示词卡片CSS | smart-canvas.css | ~1340+ | .agent-prompt-card* | +| 思维模式按钮HTML | smart-canvas.html | ~302 | agentThinkingBtn | +| 思维模式按钮CSS | smart-canvas.css | ~1204 | .agent-thinking-btn | + +--- + +## 改造步骤建议 + +### 第一步:P0 核心 bug 修复 +1. prompts 数据结构升级为 object[] +2. 每个 prompt 独立 status +3. 重写 confirmAgentPrompt(全部确认后才生图) +4. 重写 editAgentPrompt(内联编辑,不跳出流程) +5. 选项选择时补全上下文 +6. 透传 generation 属性 + +### 第二步:P1 体验提升 +7. 全部确认快捷按钮 +8. 系统提示词动态化 +9. 确认进度持久化 +10. 状态显示 UI + +### 第三步:P2 完善 + 数据驱动改造 +11. 去掉前端限制(chatRequestedImageCount/修改正则/兜底构造) +12. 数量校验 +13. 已确认可反悔 +14. 确认中发送新消息拦截 +15. 全部取消按钮 + +--- + +## 注意事项 + +1. **版本号不要动**:main.py 的 sync_static_html_versions() 会在启动时自动改 HTML 版本号,不要手动改版本号文件。用户已关闭自动更新,git 里不要提交 HTML 版本号变更。 +2. **不要启动服务测试**:启动服务会触发 sync_static_html_versions 改版本号。改完代码让用户自己重启。 +3. **git 提交规范**:用 `git reset --soft` 合并相关提交成一个,不要留一堆零散 commit。 +4. **测试要点**: + - 思维模式 ON + "生成4张龙" → 应返回4个prompts → 逐个确认 → 全部确认后才生图 + - 思维模式 ON + 模糊请求"一只猫" → 应返回options → 选完后再返回prompts + - 思维模式 OFF + "生成4张龙" → 应直接生4张 + - 确认过程中刷新页面 → 应恢复确认进度 + - 修改单个提示词 → 应内联编辑,不跳出流程 + - 跳过某个提示词 → 应只生成未跳过的 diff --git a/static/angle.html b/static/angle.html index ecb741885..f7fb4d5cf 100644 --- a/static/angle.html +++ b/static/angle.html @@ -17,21 +17,21 @@ } catch(e) {} })(); - - - - - - + + + + + + - + diff --git a/static/api-settings.html b/static/api-settings.html index c9b08b7c9..02afa3dd7 100644 --- a/static/api-settings.html +++ b/static/api-settings.html @@ -16,12 +16,12 @@ } catch(e) {} })(); - - - - - - + + + + + +
@@ -565,6 +565,6 @@
- + diff --git a/static/asset-manager.html b/static/asset-manager.html index ffedaf8ea..736980eb4 100644 --- a/static/asset-manager.html +++ b/static/asset-manager.html @@ -16,10 +16,10 @@ } catch(e) {} })(); - - - - + + + +
@@ -47,6 +47,6 @@
- + diff --git a/static/canvas-list.html b/static/canvas-list.html index 7270272b9..465827989 100644 --- a/static/canvas-list.html +++ b/static/canvas-list.html @@ -16,13 +16,13 @@ } catch(e) {} })(); - - - - - - - + + + + + + +
@@ -90,6 +90,6 @@
- + diff --git a/static/canvas.html b/static/canvas.html index 2bf163f58..0a513ccbc 100644 --- a/static/canvas.html +++ b/static/canvas.html @@ -16,13 +16,13 @@ } catch(e) {} })(); - - - - - - - + + + + + + +
@@ -350,7 +350,7 @@
- - + + diff --git a/static/comfyui-settings.html b/static/comfyui-settings.html index eecf32c60..61c61f45d 100644 --- a/static/comfyui-settings.html +++ b/static/comfyui-settings.html @@ -15,12 +15,12 @@ } catch(e) {} })(); - - - - - - + + + + + +
@@ -122,6 +122,6 @@ preview
- + diff --git a/static/css/smart-canvas.css b/static/css/smart-canvas.css index 15349ee0f..e7b6dc425 100644 --- a/static/css/smart-canvas.css +++ b/static/css/smart-canvas.css @@ -1253,6 +1253,34 @@ input[type=range].smart-range::-webkit-slider-thumb { -webkit-appearance:none; w .agent-prompt-btn:hover { border-color:var(--muted); background:var(--soft); } .agent-prompt-btn.primary { background:var(--strong); color:var(--strong-text); border-color:var(--strong); } .agent-prompt-btn.primary:hover { opacity:.88; } +.agent-prompt-edit-area { + width:100%; min-height:60px; max-height:200px; margin:0; + padding:8px 10px; border:none; border-top:1px solid var(--line); + background:var(--card); color:var(--text); + font-size:11px; line-height:1.5; resize:vertical; + font-family:inherit; outline:none; word-break:break-word; white-space:pre-wrap; +} +.agent-prompt-edit-area:focus { background:var(--soft); } +.agent-prompt-list { display:flex; flex-direction:column; } +.agent-prompt-list-item { border-bottom:1px solid var(--line); } +.agent-prompt-list-item:last-child { border-bottom:none; } +.agent-prompt-list-item.confirmed, .agent-prompt-list-item.skipped { cursor:pointer; opacity:.7; transition:opacity .14s ease; } +.agent-prompt-list-item.confirmed:hover, .agent-prompt-list-item.skipped:hover { opacity:1; } +.agent-prompt-list-item.pending { opacity:.45; } +.agent-prompt-list-header { + display:flex; align-items:center; gap:5px; padding:5px 10px; + font-size:10.5px; line-height:1.4; +} +.agent-prompt-list-icon { font-size:11px; font-weight:700; flex-shrink:0; } +.agent-prompt-list-item.confirmed .agent-prompt-list-icon { color:#16a34a; } +.agent-prompt-list-item.skipped .agent-prompt-list-icon { color:var(--faint); } +.agent-prompt-list-item.current .agent-prompt-list-icon { color:var(--strong); } +.agent-prompt-list-index { font-weight:650; color:var(--muted); flex-shrink:0; font-size:9.5px; } +.agent-prompt-list-text { flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:var(--text); } +.agent-prompt-item-actions { display:flex; gap:4px; padding:4px 8px 6px; } +.agent-prompt-card-footer { display:flex; gap:4px; padding:6px 8px; border-top:1px solid var(--line); background:var(--soft); } +.agent-prompt-confirm-all { flex:2; } +.agent-prompt-cancel-all { flex:1; } .agent-msg-free-hint { margin-top:4px; font-size:9.5px; opacity:.45; } /* 消息操作按钮 */ diff --git a/static/enhance.html b/static/enhance.html index cf35dad9d..e52239e44 100644 --- a/static/enhance.html +++ b/static/enhance.html @@ -17,14 +17,14 @@ } catch(e) {} })(); - - - - - - + + + + + + - + diff --git a/static/gpt-chat.html b/static/gpt-chat.html index 453f2f037..86c5aa77f 100644 --- a/static/gpt-chat.html +++ b/static/gpt-chat.html @@ -16,13 +16,13 @@ } catch(e) {} })(); - - - - - + + + + + - + diff --git a/static/online.html b/static/online.html index 80a6cd95b..baa7dbff9 100644 --- a/static/online.html +++ b/static/online.html @@ -16,14 +16,14 @@ } catch(e) {} })(); - - - - - - + + + + + + - + - + From f964b948d2b474460d7f883c6dbe33177c76321e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Sun, 19 Jul 2026 15:40:53 +0800 Subject: [PATCH 38/67] =?UTF-8?q?fix:=20=E5=A4=9A=E5=9B=BE=E6=95=B0?= =?UTF-8?q?=E9=87=8F=E6=A0=A1=E5=87=86=20-=20=E9=80=892=E5=BC=A04K?= =?UTF-8?q?=E4=BD=86LLM=E8=BF=94=E5=9B=9E4=E5=BC=A0=E6=97=B6=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E6=88=AA=E6=96=AD=E5=88=B0=E8=AF=B7=E6=B1=82=E6=95=B0?= =?UTF-8?q?=E9=87=8F=EF=BC=9B=E5=8D=87=E7=BA=A7=E7=89=88=E6=9C=AC=E5=8F=B7?= =?UTF-8?q?=E5=88=B0=20v1.4.1784450000?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/angle.html | 16 ++++++++-------- static/api-settings.html | 14 +++++++------- static/asset-manager.html | 10 +++++----- static/canvas-list.html | 16 ++++++++-------- static/canvas.html | 18 +++++++++--------- static/comfyui-settings.html | 14 +++++++------- static/enhance.html | 16 ++++++++-------- static/gpt-chat.html | 12 ++++++------ static/index.html | 28 ++++++++++++++-------------- static/js/smart-canvas.js | 29 +++++++++++++++++++++++++---- static/klein.html | 16 ++++++++-------- static/online.html | 16 ++++++++-------- static/smart-canvas.html | 12 ++++++------ static/zimage.html | 14 +++++++------- 14 files changed, 126 insertions(+), 105 deletions(-) diff --git a/static/angle.html b/static/angle.html index f7fb4d5cf..6aa793e29 100644 --- a/static/angle.html +++ b/static/angle.html @@ -17,12 +17,12 @@ } catch(e) {} })(); - - - - - - + + + + + + - + diff --git a/static/api-settings.html b/static/api-settings.html index 02afa3dd7..c90b7e316 100644 --- a/static/api-settings.html +++ b/static/api-settings.html @@ -16,12 +16,12 @@ } catch(e) {} })(); - - - - - - + + + + + +
@@ -565,6 +565,6 @@
- + diff --git a/static/asset-manager.html b/static/asset-manager.html index 736980eb4..8c2289ebb 100644 --- a/static/asset-manager.html +++ b/static/asset-manager.html @@ -16,10 +16,10 @@ } catch(e) {} })(); - - - - + + + +
@@ -47,6 +47,6 @@
- + diff --git a/static/canvas-list.html b/static/canvas-list.html index 465827989..008e1066e 100644 --- a/static/canvas-list.html +++ b/static/canvas-list.html @@ -16,13 +16,13 @@ } catch(e) {} })(); - - - - - - - + + + + + + +
@@ -90,6 +90,6 @@
- + diff --git a/static/canvas.html b/static/canvas.html index 0a513ccbc..fdcb6375e 100644 --- a/static/canvas.html +++ b/static/canvas.html @@ -16,13 +16,13 @@ } catch(e) {} })(); - - - - - - - + + + + + + +
@@ -350,7 +350,7 @@
- - + + diff --git a/static/comfyui-settings.html b/static/comfyui-settings.html index 61c61f45d..4089d9858 100644 --- a/static/comfyui-settings.html +++ b/static/comfyui-settings.html @@ -15,12 +15,12 @@ } catch(e) {} })(); - - - - - - + + + + + +
@@ -122,6 +122,6 @@ preview
- + diff --git a/static/enhance.html b/static/enhance.html index e52239e44..6f517f007 100644 --- a/static/enhance.html +++ b/static/enhance.html @@ -17,14 +17,14 @@ } catch(e) {} })(); - - - - - - + + + + + + - + diff --git a/static/gpt-chat.html b/static/gpt-chat.html index 86c5aa77f..dc77ff633 100644 --- a/static/gpt-chat.html +++ b/static/gpt-chat.html @@ -16,13 +16,13 @@ } catch(e) {} })(); - - - - - + + + + + - + diff --git a/static/online.html b/static/online.html index baa7dbff9..7a8db7345 100644 --- a/static/online.html +++ b/static/online.html @@ -16,14 +16,14 @@ } catch(e) {} })(); - - - - - - + + + + + + - + - + From 377ea30d1e7c4488faddb1124f15d194410d0416 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Sun, 19 Jul 2026 15:51:48 +0800 Subject: [PATCH 39/67] =?UTF-8?q?fix:=20=E9=80=9A=E7=94=A8=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E6=89=80=E6=9C=89=20LLM=20provider=20=E9=81=B5?= =?UTF-8?q?=E5=BE=AA=20skill=20-=20=E9=87=8D=E6=9E=84=20system=5Fprompt=20?= =?UTF-8?q?=E7=BB=93=E6=9E=84(=E9=A6=96=E5=9B=A0+=E8=BF=91=E5=9B=A0+?= =?UTF-8?q?=E4=B8=AD=E8=8B=B1=E5=8F=8C=E8=AF=AD)=20+=20user=20message=20?= =?UTF-8?q?=E6=B3=A8=E5=85=A5=20skill=20=E5=BC=BA=E5=88=B6=E6=8F=90?= =?UTF-8?q?=E9=86=92=EF=BC=8C=E7=A1=AE=E4=BF=9D=20agy(gemini-cli)/agnes/?= =?UTF-8?q?=E9=AD=94=E6=90=AD=E5=8F=8A=E6=89=80=E6=9C=89=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E9=83=BD=E8=83=BD=E5=AE=8C=E6=95=B4=E4=BF=9D=E7=95=99=20skill?= =?UTF-8?q?=20=E5=86=85=E5=AE=B9=EF=BC=9B=E5=8D=87=E7=BA=A7=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E5=8F=B7=E5=88=B0=20v1.4.1784458000?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/angle.html | 16 ++++++------ static/api-settings.html | 14 +++++------ static/asset-manager.html | 10 ++++---- static/canvas-list.html | 16 ++++++------ static/canvas.html | 18 +++++++------- static/comfyui-settings.html | 14 +++++------ static/enhance.html | 16 ++++++------ static/gpt-chat.html | 12 ++++----- static/index.html | 28 ++++++++++----------- static/js/smart-canvas.js | 47 +++++++++++++++++++++++++++--------- static/klein.html | 16 ++++++------ static/online.html | 16 ++++++------ static/smart-canvas.html | 12 ++++----- static/zimage.html | 14 +++++------ 14 files changed, 136 insertions(+), 113 deletions(-) diff --git a/static/angle.html b/static/angle.html index 6aa793e29..7603333db 100644 --- a/static/angle.html +++ b/static/angle.html @@ -17,12 +17,12 @@ } catch(e) {} })(); - - - - - - + + + + + + - + diff --git a/static/api-settings.html b/static/api-settings.html index c90b7e316..f37305f0d 100644 --- a/static/api-settings.html +++ b/static/api-settings.html @@ -16,12 +16,12 @@ } catch(e) {} })(); - - - - - - + + + + + +
@@ -565,6 +565,6 @@
- + diff --git a/static/asset-manager.html b/static/asset-manager.html index 8c2289ebb..aad952860 100644 --- a/static/asset-manager.html +++ b/static/asset-manager.html @@ -16,10 +16,10 @@ } catch(e) {} })(); - - - - + + + +
@@ -47,6 +47,6 @@
- + diff --git a/static/canvas-list.html b/static/canvas-list.html index 008e1066e..14d4d2296 100644 --- a/static/canvas-list.html +++ b/static/canvas-list.html @@ -16,13 +16,13 @@ } catch(e) {} })(); - - - - - - - + + + + + + +
@@ -90,6 +90,6 @@
- + diff --git a/static/canvas.html b/static/canvas.html index fdcb6375e..afc9d286f 100644 --- a/static/canvas.html +++ b/static/canvas.html @@ -16,13 +16,13 @@ } catch(e) {} })(); - - - - - - - + + + + + + +
@@ -350,7 +350,7 @@
- - + + diff --git a/static/comfyui-settings.html b/static/comfyui-settings.html index 4089d9858..a7ff32d88 100644 --- a/static/comfyui-settings.html +++ b/static/comfyui-settings.html @@ -15,12 +15,12 @@ } catch(e) {} })(); - - - - - - + + + + + +
@@ -122,6 +122,6 @@ preview
- + diff --git a/static/enhance.html b/static/enhance.html index 6f517f007..5fcb51857 100644 --- a/static/enhance.html +++ b/static/enhance.html @@ -17,14 +17,14 @@ } catch(e) {} })(); - - - - - - + + + + + + - + diff --git a/static/gpt-chat.html b/static/gpt-chat.html index dc77ff633..85c60c375 100644 --- a/static/gpt-chat.html +++ b/static/gpt-chat.html @@ -16,13 +16,13 @@ } catch(e) {} })(); - - - - - + + + + + - + diff --git a/static/online.html b/static/online.html index 7a8db7345..7b570030f 100644 --- a/static/online.html +++ b/static/online.html @@ -16,14 +16,14 @@ } catch(e) {} })(); - - - - - - + + + + + + - + - + From 43930ff2b98395faea09305ac611c4c344d346e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Sun, 19 Jul 2026 16:10:08 +0800 Subject: [PATCH 40/67] =?UTF-8?q?fix:=20=E6=80=9D=E7=BB=B4=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E4=B8=8B=E5=85=9C=E5=BA=95=E7=94=9F=E5=9B=BE=E7=BB=95?= =?UTF-8?q?=E8=BF=87=E7=A1=AE=E8=AE=A4=E6=B5=81=E7=A8=8B=E7=9A=84bug=20-?= =?UTF-8?q?=20=E5=85=9C=E5=BA=95=E9=80=BB=E8=BE=91=E4=B8=AD=E6=80=9D?= =?UTF-8?q?=E7=BB=B4=E6=A8=A1=E5=BC=8F=E4=B8=8D=E8=A7=A6=E5=8F=91hasGenInP?= =?UTF-8?q?rogress=20-=20generations=E8=BD=ACprompts=E7=A7=BB=E9=99=A4opti?= =?UTF-8?q?ons=E9=99=90=E5=88=B6=20-=20thinkingModeOn=E8=AE=A1=E7=AE=97?= =?UTF-8?q?=E6=8F=90=E5=89=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/angle.html | 16 ++++++++-------- static/api-settings.html | 14 +++++++------- static/asset-manager.html | 10 +++++----- static/canvas-list.html | 16 ++++++++-------- static/canvas.html | 18 +++++++++--------- static/comfyui-settings.html | 14 +++++++------- static/enhance.html | 16 ++++++++-------- static/gpt-chat.html | 12 ++++++------ static/index.html | 28 ++++++++++++++-------------- static/js/smart-canvas.js | 14 +++++++++----- static/klein.html | 16 ++++++++-------- static/online.html | 16 ++++++++-------- static/smart-canvas.html | 12 ++++++------ static/zimage.html | 14 +++++++------- 14 files changed, 110 insertions(+), 106 deletions(-) diff --git a/static/angle.html b/static/angle.html index 7603333db..c432b1d6b 100644 --- a/static/angle.html +++ b/static/angle.html @@ -17,12 +17,12 @@ } catch(e) {} })(); - - - - - - + + + + + + - + diff --git a/static/api-settings.html b/static/api-settings.html index f37305f0d..f682f893c 100644 --- a/static/api-settings.html +++ b/static/api-settings.html @@ -16,12 +16,12 @@ } catch(e) {} })(); - - - - - - + + + + + +
@@ -565,6 +565,6 @@
- + diff --git a/static/asset-manager.html b/static/asset-manager.html index aad952860..4328c0da2 100644 --- a/static/asset-manager.html +++ b/static/asset-manager.html @@ -16,10 +16,10 @@ } catch(e) {} })(); - - - - + + + +
@@ -47,6 +47,6 @@
- + diff --git a/static/canvas-list.html b/static/canvas-list.html index 14d4d2296..f99827977 100644 --- a/static/canvas-list.html +++ b/static/canvas-list.html @@ -16,13 +16,13 @@ } catch(e) {} })(); - - - - - - - + + + + + + +
@@ -90,6 +90,6 @@
- + diff --git a/static/canvas.html b/static/canvas.html index afc9d286f..9c77ec6dd 100644 --- a/static/canvas.html +++ b/static/canvas.html @@ -16,13 +16,13 @@ } catch(e) {} })(); - - - - - - - + + + + + + +
@@ -350,7 +350,7 @@
- - + + diff --git a/static/comfyui-settings.html b/static/comfyui-settings.html index a7ff32d88..1451bfbc0 100644 --- a/static/comfyui-settings.html +++ b/static/comfyui-settings.html @@ -15,12 +15,12 @@ } catch(e) {} })(); - - - - - - + + + + + +
@@ -122,6 +122,6 @@ preview
- + diff --git a/static/enhance.html b/static/enhance.html index 5fcb51857..656055095 100644 --- a/static/enhance.html +++ b/static/enhance.html @@ -17,14 +17,14 @@ } catch(e) {} })(); - - - - - - + + + + + + - + diff --git a/static/gpt-chat.html b/static/gpt-chat.html index 85c60c375..02bc4cad2 100644 --- a/static/gpt-chat.html +++ b/static/gpt-chat.html @@ -16,13 +16,13 @@ } catch(e) {} })(); - - - - - + + + + + - + diff --git a/static/online.html b/static/online.html index 7b570030f..790766e1e 100644 --- a/static/online.html +++ b/static/online.html @@ -16,14 +16,14 @@ } catch(e) {} })(); - - - - - - + + + + + + - + - + From d30a951496737a1e587f7091ce0e1a8f4776cb9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Sun, 19 Jul 2026 16:36:43 +0800 Subject: [PATCH 41/67] =?UTF-8?q?feat:=20=E7=A1=AC=E8=BD=AF=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=88=86=E5=B1=82=20-=20Skill=3D=E5=8D=95=E5=BC=A0?= =?UTF-8?q?=E5=9B=BE=E6=A0=B7=E5=BC=8F=E4=B8=8D=E5=86=B3=E5=AE=9A=E6=95=B0?= =?UTF-8?q?=E9=87=8F,=20=E5=87=BA=E5=9B=BE=E6=95=B0=E9=87=8F=3D=E8=BE=93?= =?UTF-8?q?=E5=85=A5=E6=A1=86>=E5=B7=A5=E5=85=B7=E6=A0=8F=20-=20=E9=87=8D?= =?UTF-8?q?=E6=9E=84agentSystemPrompt:=20Skill=E6=8F=8F=E8=BF=B0=E5=8D=95?= =?UTF-8?q?=E5=BC=A0=E5=9B=BE=E6=A0=B7=E5=BC=8F(=E5=90=AB=E7=94=BB?= =?UTF-8?q?=E9=9D=A2=E5=86=85=E6=8E=92=E5=88=97),=E4=B8=8D=E5=86=B3?= =?UTF-8?q?=E5=AE=9A=E5=87=BA=E5=9B=BE=E6=95=B0=E9=87=8F=20-=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9EresolveFinalGenCount:=20=E8=BE=93=E5=85=A5=E6=A1=86?= =?UTF-8?q?=E6=98=BE=E5=BC=8F=E8=A6=81=E6=B1=82(=E8=BD=AF=E5=8F=82?= =?UTF-8?q?=E6=95=B0)>=E5=B7=A5=E5=85=B7=E6=A0=8F(=E9=BB=98=E8=AE=A4),=20?= =?UTF-8?q?=E6=AF=94=E4=BE=8B/=E5=88=86=E8=BE=A8=E7=8E=87=E4=B8=BA?= =?UTF-8?q?=E7=A1=AC=E5=8F=82=E6=95=B0=E5=B7=A5=E5=85=B7=E6=A0=8F=E8=AF=B4?= =?UTF-8?q?=E4=BA=86=E7=AE=97=20-=20=E5=A2=9E=E5=BC=BAchatRequestedImageCo?= =?UTF-8?q?unt:=20=E6=94=AF=E6=8C=811-8,=E5=A2=9E=E5=8A=A0=E6=9D=A1/?= =?UTF-8?q?=E5=8F=AA/=E7=89=88/=E6=AC=BE=E7=AD=89=E9=87=8F=E8=AF=8D=20-=20?= =?UTF-8?q?sendAgentMessage:=20=E7=AE=97finalCount=E4=BC=A0=E7=BB=99agentS?= =?UTF-8?q?ystemPrompt,=20Skill=E6=8F=90=E9=86=92=E5=8A=A0=E6=95=B0?= =?UTF-8?q?=E9=87=8F=E5=BD=92=E5=B1=9E=E8=AF=B4=E6=98=8E=20-=20processAgen?= =?UTF-8?q?tLlmResult:=20requestedCount=E7=94=A8resolveFinalGenCount?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E5=86=B3=E7=AD=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/angle.html | 16 ++++---- static/api-settings.html | 14 +++---- static/asset-manager.html | 10 ++--- static/canvas-list.html | 16 ++++---- static/canvas.html | 18 ++++----- static/comfyui-settings.html | 14 +++---- static/enhance.html | 16 ++++---- static/gpt-chat.html | 12 +++--- static/index.html | 28 +++++++------- static/js/smart-canvas.js | 73 ++++++++++++++++++++++-------------- static/klein.html | 16 ++++---- static/online.html | 16 ++++---- static/smart-canvas.html | 12 +++--- static/zimage.html | 14 +++---- 14 files changed, 145 insertions(+), 130 deletions(-) diff --git a/static/angle.html b/static/angle.html index c432b1d6b..5e55621da 100644 --- a/static/angle.html +++ b/static/angle.html @@ -17,12 +17,12 @@ } catch(e) {} })(); - - - - - - + + + + + + - + diff --git a/static/api-settings.html b/static/api-settings.html index f682f893c..12d91a7fb 100644 --- a/static/api-settings.html +++ b/static/api-settings.html @@ -16,12 +16,12 @@ } catch(e) {} })(); - - - - - - + + + + + +
@@ -565,6 +565,6 @@
- + diff --git a/static/asset-manager.html b/static/asset-manager.html index 4328c0da2..4870194d9 100644 --- a/static/asset-manager.html +++ b/static/asset-manager.html @@ -16,10 +16,10 @@ } catch(e) {} })(); - - - - + + + +
@@ -47,6 +47,6 @@
- + diff --git a/static/canvas-list.html b/static/canvas-list.html index f99827977..00c8d298b 100644 --- a/static/canvas-list.html +++ b/static/canvas-list.html @@ -16,13 +16,13 @@ } catch(e) {} })(); - - - - - - - + + + + + + +
@@ -90,6 +90,6 @@
- + diff --git a/static/canvas.html b/static/canvas.html index 9c77ec6dd..6342cc8d3 100644 --- a/static/canvas.html +++ b/static/canvas.html @@ -16,13 +16,13 @@ } catch(e) {} })(); - - - - - - - + + + + + + +
@@ -350,7 +350,7 @@
- - + + diff --git a/static/comfyui-settings.html b/static/comfyui-settings.html index 1451bfbc0..c1da0da3b 100644 --- a/static/comfyui-settings.html +++ b/static/comfyui-settings.html @@ -15,12 +15,12 @@ } catch(e) {} })(); - - - - - - + + + + + +
@@ -122,6 +122,6 @@ preview
- + diff --git a/static/enhance.html b/static/enhance.html index 656055095..9130d1c08 100644 --- a/static/enhance.html +++ b/static/enhance.html @@ -17,14 +17,14 @@ } catch(e) {} })(); - - - - - - + + + + + + - + diff --git a/static/gpt-chat.html b/static/gpt-chat.html index 02bc4cad2..4b3866b4d 100644 --- a/static/gpt-chat.html +++ b/static/gpt-chat.html @@ -16,13 +16,13 @@ } catch(e) {} })(); - - - - - + + + + + - + diff --git a/static/online.html b/static/online.html index 790766e1e..001e5f32a 100644 --- a/static/online.html +++ b/static/online.html @@ -16,14 +16,14 @@ } catch(e) {} })(); - - - - - - + + + + + + - + - + From d0c8e5a492e5794d621cdecb7e0b6c7ee19f00be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Sun, 19 Jul 2026 17:04:09 +0800 Subject: [PATCH 42/67] =?UTF-8?q?release:=20v1.5=20-=20=E7=A1=AC=E8=BD=AF?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E5=88=86=E5=B1=82=20+=20Skill=E5=AE=9A?= =?UTF-8?q?=E4=BD=8D=E6=98=8E=E7=A1=AE=20-=20README=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=94=B9=E4=B8=BA=E5=88=97=E8=A1=A8=E5=A4=A7?= =?UTF-8?q?=E7=BA=B2,=E5=A2=9E=E5=8A=A0v1.4/v1.5=20-=20VERSION=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E4=B8=BAv1.5=20-=20HTML=E7=89=88=E6=9C=AC=E5=8F=B7?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=B8=BAv1.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PLAN_MULTI_IMAGE.md | 336 ----------------------------------- README.md | 117 ++++++------ VERSION | 2 +- static/angle.html | 16 +- static/api-settings.html | 14 +- static/asset-manager.html | 10 +- static/canvas-list.html | 16 +- static/canvas.html | 18 +- static/comfyui-settings.html | 14 +- static/enhance.html | 16 +- static/gpt-chat.html | 12 +- static/index.html | 28 +-- static/klein.html | 16 +- static/online.html | 16 +- static/smart-canvas.html | 12 +- static/zimage.html | 14 +- 16 files changed, 158 insertions(+), 499 deletions(-) delete mode 100644 PLAN_MULTI_IMAGE.md diff --git a/PLAN_MULTI_IMAGE.md b/PLAN_MULTI_IMAGE.md deleted file mode 100644 index b48d8d6a2..000000000 --- a/PLAN_MULTI_IMAGE.md +++ /dev/null @@ -1,336 +0,0 @@ -# 思维模式多图确认流程重构计划 - -## 项目背景 - -项目路径:`/Users/yangfan/Desktop/Infinite-Canvas` -核心文件: -- `static/js/smart-canvas.js`(约18863行)—— Agent 面板全部逻辑 -- `static/css/smart-canvas.css`(约1560行)—— Agent 面板样式 -- `static/smart-canvas.html`—— Agent 面板 HTML 结构 -- `main.py`(约17677行)—— 后端服务 - -当前版本:v1.3,已有思维模式开关功能,但多图确认流程有严重 bug。 - ---- - -## 用户反馈的核心问题 - -### 问题1:选项流丢失数量信息 -用户说"生成4张龙",LLM 返回4个龙的风格选项(options)。用户选了一个后,代码把选项的 value 填入输入框并发送 sendAgentMessage()。但这个 value 是类似"龙,风格:水墨"的描述,不包含"4张"。LLM 收到后只生成1张。 - -代码位置:`static/js/smart-canvas.js` 第17268行 -```js -agentInput.value = value; // value 只有风格描述,没有数量 -sendAgentMessage(); -``` - -### 问题2:确认一张就开始生图,不等全部确认 -思维模式下,confirmAgentPrompt 每次确认后立即调用 runAgentGenerations。用户确认第1个后画布就开始出占位/生图了,还没确认完后面3个。 - -代码位置:`static/js/smart-canvas.js` 第17888-17911行 -```js -async function confirmAgentPrompt(assistantMsg){ - // ... - assistantMsg.generations.push(gen); - assistantMsg.promptIdx = idx + 1; - // 确认后立即生图!应该等全部确认完再生 - runAgentGenerations(assistantMsg, userMsg); -} -``` - ---- - -## 所有需要修复的问题(按优先级) - -### P0:必须修复(解决核心 bug) - -#### 1. prompts 数据结构升级(string[] → object[]) - -当前 prompts 是纯字符串数组,确认时无法携带 count、use_last_outputs、use_attachments。 - -当前代码(第17721行): -```js -parsed.prompts = parsed.generations.map(g => String(g.prompt || '').trim()).filter(p => p); -``` -只提取了文本,count/use_last_outputs/use_attachments 全丢了。 - -改为对象数组: -```js -prompts: [ - {prompt: "水墨龙...", count: 1, use_last_outputs: false, use_attachments: true, status: "pending"}, - {prompt: "油画龙...", count: 1, use_last_outputs: false, use_attachments: true, status: "pending"}, -] -``` - -每个 prompt 对象携带完整生成属性 + 独立状态。 - -#### 2. 每个 prompt 独立状态 - -当前只有一个 `promptIdx` 指向"当前显示第几个"。 - -改为每个 prompt 有独立 status: -- `pending`:还没轮到 -- `current`:当前正在看 -- `confirmed`:已确认 -- `skipped`:跳过不生成 -- `editing`:正在内联编辑 - -#### 3. 四个操作按钮重新定义 - -当前操作(第17278-17291行):确认/重新生成/修改 - -改为:**确认 / 修改 / 重新生成 / 跳过** - -- **确认**:标记当前为 confirmed,自动跳到下一个 pending,如果没有下一个了→触发生图 -- **修改**:当前提示词进入内联编辑模式(直接在卡片里改文本,不跳到输入框),用户改完点"保存"→标记为 confirmed。不再设置 agentBypassThinkingNext,不跳出确认流程 -- **重新生成**:只重新生成当前这一条,不影响其他已确认的。发给 LLM:"请重新生成第N条提示词,要求与之前不同"。LLM 返回新 prompt 文本,替换当前这一条 -- **跳过**(新增):标记当前为 skipped,不生成这张,跳到下一个 - -#### 4. 全部确认后才触发生图 - -改 `confirmAgentPrompt`(第17888行): -- 确认后只标记 status=confirmed,推进到下一个 pending -- 检查是否还有 pending 状态的 prompt -- 如果没有 pending 了(全部 confirmed 或 skipped)→ 才调用 runAgentGenerations -- 生图时只生成 status==="confirmed" 的,跳过 skipped 的 - -`runAgentGenerations` 一次性接收所有 confirmed prompts,统一计算占位位置,整齐排列。 - -#### 5. 选项选择时补全上下文 - -改第17268行,选项被选中后不是只发选项 value,而是带上原始请求: - -```js -// 找到触发选项流的原始用户消息 -const originalRequest = findOriginalRequest(); -agentInput.value = `${originalRequest},选择:${value}`; -// 例如:"生成4张龙,选择:龙,风格:水墨" -``` - -#### 6. 透传 LLM 的 generation 属性 - -确认创建 generation 时(第17902行),不再重置 count/use_last_outputs/use_attachments,而是保留 LLM 原始返回的值: - -当前代码: -```js -const gen = {prompt, count:1, use_last_outputs:false, use_attachments:false, results:[], status:'running'}; -``` - -改为: -```js -const gen = { - prompt: confirmedPrompt.prompt, - count: confirmedPrompt.count || 1, - use_last_outputs: confirmedPrompt.use_last_outputs || false, - use_attachments: confirmedPrompt.use_attachments || false, - results: [], - status: 'running' -}; -``` - ---- - -### P1:体验提升 - -#### 7. 全部确认快捷按钮 - -当 prompts ≥ 2 时,显示"全部确认并生成"按钮。点击后所有 pending 标记为 confirmed,立即触发生图。 - -#### 8. 修改改为内联编辑 - -当前 `editAgentPrompt`(第17971行)把 prompt 复制到输入框,设置 bypass 标志,发送后跳出流程。 - -改为:点"修改"→卡片内提示词文本变成可编辑 textarea→用户改完点"保存"→标记 confirmed→不离开确认流程。 - -#### 9. 系统提示词动态化 - -当前 `agentSystemPrompt()`(第17481行)不接受参数,思维模式开/关发给 LLM 的提示词完全相同。 - -改为根据思维模式开关动态构建: - -思维模式 ON 时追加: -``` -当前为思维模式。请返回 prompts 数组(不要返回 generations)。 -每个 prompt 对象包含:prompt(中文提示词), count, use_last_outputs, use_attachments。 -用户会逐个确认后才生图。 -如果请求模糊,可以先返回 options 让用户选方向,下一轮再返回 prompts。 -如果是修改请求("换成像素风"),仍返回 prompts,但 use_last_outputs 设为 true。 -用户请求N张图时,prompts 必须返回恰好N条。 -``` - -思维模式 OFF 时追加: -``` -当前为直接模式。能生成就生成,返回 generations 数组。 -``` - -注意:`agentSystemPrompt(bypassThinking)` 在第17780行已经被调用并传参,但函数定义(第17481行)没接收参数。需要修复函数签名。 - -#### 10. 确认进度持久化(刷新恢复) - -当前 `_setupAgentRecovery`(第16841行)只恢复 LLM task 和生图 task。 - -需要增加分支:检测到消息有 prompts 且有 pending 状态的 → 恢复确认卡片显示,不触发生图,用户继续从上次中断的地方确认。 - -需持久化:每个 prompt 的 status、当前 promptIdx、prompts 数组本身(已有)。 - -#### 11. 状态显示 UI(进度、折叠) - -卡片 UI 展示进度: -``` -┌─────────────────────────────────┐ -│ 📝 提示词确认 (2/4 已确认) │ -│ ✓ #1 水墨龙... [已确认] │ ← 折叠,可点击展开反悔 -│ ✓ #2 油画龙... [已确认] │ ← 折叠,可点击展开反悔 -│ ▶ #3 赛博龙... │ ← 当前,展开操作按钮 -│ [确认] [修改] [重新生成] [跳过] │ -│ ○ #4 Q版龙... │ ← 待处理,灰色 -│ [全部确认并生成] [全部取消] │ -└─────────────────────────────────┘ -``` - ---- - -### P2:完善 - -#### 12. 数量校验 - -用户说"4张龙",LLM 返回3个 prompts。 -- 卡片顶部显示"用户请求4张,当前3条提示词" -- 提供"补充提示词"按钮 → 让 LLM 再生成几条补齐 -- 如果 LLM 返回比请求的多,用户可以跳过多余的 - -#### 13. 已确认可反悔 - -已确认的卡片显示为折叠状态 + ✓ 标记。点击展开重新显示操作按钮,用户可以"取消确认"(改回 pending)或"修改"。 - -#### 14. 确认中发送新消息拦截 - -检测到有未完成的 prompts(pending 状态存在)时,弹 toast:"还有N条提示词未确认,是否放弃当前确认?"用户确认放弃→清除 prompts,发送新消息。 - -#### 15. 全部取消按钮 - -用户看到 prompts 后完全不满意,点"全部取消"→清除当前 assistant 消息的 prompts,不触发生图,用户重新输入。 - ---- - -## 需要去掉的前端限制(数据驱动改造) - -以下前端逻辑替 LLM 做了决定,应该去掉或弱化: - -### 去掉:思维模式强制转 prompts(第17716-17728行) - -当前思维模式开启时,强制把 generations 转 prompts,不管 LLM 原本返回什么。 -改为:系统提示词告诉 LLM 返回 prompts,前端不做强制转换。如果 LLM 仍返回 generations,说明它认为不需要确认,直接生图。 - -### 去掉:`chatRequestedImageCount`(第16972行) - -当前用正则提取"4张",只认1-4。 -改为:完全交给 LLM 理解数量。LLM 在 generations/prompts 中返回对应数量。 - -### 去掉:修改请求正则识别(第17717行) - -当前用正则匹配"改成/换成"等关键词,命中就跳过扩写流程。 -改为:LLM 自己判断是不是修改请求,在 prompts 中设 use_last_outputs。 - -### 去掉:兜底构造 generation(第17665-17690行) - -当前 LLM 没返回 generations 时,前端用正则猜意图自己构造。 -改为:LLM 没返回就不生,显示 LLM 的 reply/options。 - -### 保留:JSON 兜底解析(第17502-17548行) - -只在 JSON 解析失败时用,合理。 - -### 保留:占位节点/生图任务/WebSocket(前端职责) - -这些是纯前端职责,LLM 管不了。 - ---- - -## 呈现规则(纯数据驱动) - -LLM 返回的 JSON 字段组合 → 前端呈现方式: - -``` -{generations: [...]} → 直接生图(LLM 认为可以生成了) -{options: [...]} → 显示选项按钮(LLM 认为需要用户选择) -{prompts: [...]} → 显示确认卡片(LLM 认为需要用户确认) -{reply: "..."} → 纯文字回复(LLM 认为需要对话) -{options + generations} → 先选项,选完直接生图 -{options + prompts} → 先选项,选完进入确认 -{reply + generations} → 一边回复一边生图 -``` - -不再用 thinkingModeOn / isModifyRequest / chatRequestedImageCount 来决定走哪个分支,完全看 LLM 返回了什么。 - ---- - -## 关键代码位置索引 - -| 功能 | 文件 | 行号 | 函数/变量名 | -|------|------|------|------------| -| 系统提示词 | smart-canvas.js | 16750 | AGENT_FORMAT_INSTRUCTION | -| agentSystemPrompt | smart-canvas.js | 17481 | agentSystemPrompt()(需改成接收参数) | -| 数量提取 | smart-canvas.js | 16972 | chatRequestedImageCount()(建议去掉) | -| 修改请求正则 | smart-canvas.js | 17717 | userModifyRe(建议去掉) | -| 兜底构造generation | smart-canvas.js | 17665-17690 | genInProgressRe等(建议去掉) | -| 思维模式强制转prompts | smart-canvas.js | 17716-17728 | thinkingModeOn 块(建议去掉) | -| 批量完整性提示 | smart-canvas.js | 17711 | requestedCount(改为校验) | -| 消息HTML渲染 | smart-canvas.js | 17185-17201 | agentMessageHtml() | -| 选项按钮绑定 | smart-canvas.js | 17243-17277 | data-agent-option | -| 确认卡片绑定 | smart-canvas.js | 17278-17292 | data-agent-prompt-action | -| 选项"确认"处理 | smart-canvas.js | 17248-17267 | value === '确认' 分支 | -| 选项选择后发送 | smart-canvas.js | 17268-17275 | sendAgentMessage() | -| 确认提示词 | smart-canvas.js | 17888-17911 | confirmAgentPrompt() | -| 重新生成提示词 | smart-canvas.js | 17912-17970 | regenerateAgentPrompts() | -| 修改提示词 | smart-canvas.js | 17971-17990 | editAgentPrompt() | -| 执行生图 | smart-canvas.js | 17991+ | runAgentGenerations() | -| 恢复中断操作 | smart-canvas.js | 16841 | _setupAgentRecovery() | -| LLM 任务轮询 | smart-canvas.js | 15228 | pollAgentLlmTask() | -| 发送消息 | smart-canvas.js | 17739 | sendAgentMessage() | -| 处理LLM结果 | smart-canvas.js | ~17600 | processAgentLlmResult() | -| 变量声明区 | smart-canvas.js | 16788-16794 | agentSending/agentThinking/agentBypassThinkingNext | -| 提示词卡片HTML | smart-canvas.js | 17190-17199 | promptCardHtml | -| 提示词卡片CSS | smart-canvas.css | ~1340+ | .agent-prompt-card* | -| 思维模式按钮HTML | smart-canvas.html | ~302 | agentThinkingBtn | -| 思维模式按钮CSS | smart-canvas.css | ~1204 | .agent-thinking-btn | - ---- - -## 改造步骤建议 - -### 第一步:P0 核心 bug 修复 -1. prompts 数据结构升级为 object[] -2. 每个 prompt 独立 status -3. 重写 confirmAgentPrompt(全部确认后才生图) -4. 重写 editAgentPrompt(内联编辑,不跳出流程) -5. 选项选择时补全上下文 -6. 透传 generation 属性 - -### 第二步:P1 体验提升 -7. 全部确认快捷按钮 -8. 系统提示词动态化 -9. 确认进度持久化 -10. 状态显示 UI - -### 第三步:P2 完善 + 数据驱动改造 -11. 去掉前端限制(chatRequestedImageCount/修改正则/兜底构造) -12. 数量校验 -13. 已确认可反悔 -14. 确认中发送新消息拦截 -15. 全部取消按钮 - ---- - -## 注意事项 - -1. **版本号不要动**:main.py 的 sync_static_html_versions() 会在启动时自动改 HTML 版本号,不要手动改版本号文件。用户已关闭自动更新,git 里不要提交 HTML 版本号变更。 -2. **不要启动服务测试**:启动服务会触发 sync_static_html_versions 改版本号。改完代码让用户自己重启。 -3. **git 提交规范**:用 `git reset --soft` 合并相关提交成一个,不要留一堆零散 commit。 -4. **测试要点**: - - 思维模式 ON + "生成4张龙" → 应返回4个prompts → 逐个确认 → 全部确认后才生图 - - 思维模式 ON + 模糊请求"一只猫" → 应返回options → 选完后再返回prompts - - 思维模式 OFF + "生成4张龙" → 应直接生4张 - - 确认过程中刷新页面 → 应恢复确认进度 - - 修改单个提示词 → 应内联编辑,不跳出流程 - - 跳过某个提示词 → 应只生成未跳过的 diff --git a/README.md b/README.md index 8086efcf1..4a9989eac 100644 --- a/README.md +++ b/README.md @@ -7,67 +7,50 @@ Supports comfyui/API calls/modelscope calls > 本仓库是基于原项目 [hero8152/Infinite-Canvas](https://github.com/hero8152/Infinite-Canvas) 的二次开发 fork,新增了智能画布 AI Agent 面板。 > -> **当前版本:v1.3**(查看 [版本更新日志](#版本更新日志)) - -### v1.0 新增功能(AI Agent 面板基线) -1. AI Agent 侧边聊天面板(OneBox 风格),可与画布联动生图。 -2. `@` 引用当前画布中的图片,最新的图片排在最上面。 -3. 统一附件管理:Skill 文档与图片在同一处增删改查,支持一次上传多个。 -4. 输入框支持自定义拖拽/自动拉高。 -5. 多对话管理(新建 / 删除 / 对话列表,与画布绑定),消息支持复制 / 重试。 -6. 生图参数面板:质量(自动/高/中/低)、比例(9 种带图标)、分辨率(1K/2K/4K)、数量(1-8)。 -7. LLM 结构化确认流程:提出方案后返回选项按钮,「确认」直接开始生成、「修改」重新生成提示词。 -8. LLM 生图能力升级(角色/风格一致性、图片编辑、批量多样性、质量控制、实时反馈、图片组合、动态参数、风格迁移、图片理解等)。 -9. 生图占位:生成开始时先在画布放置占位骨架,完成后替换为实际图片;多张并发生成、顶部对齐向右排列。 -10. 在画布选中图片的悬浮工具栏最左侧新增「发送至 Agent」按钮。 - -### v1.1 更新内容 - -#### 一、Agent 对话与意图理解 -1. **系统提示词重写**:精简为 5 种对话模式(直接生成 / 确认 / 多选 / 澄清 / 批量),强制中文提示词,避免 LLM 返回纯文字不生图。 -2. **修改场景智能区分**:区分「风格修改」(换成像素风 → 引用原图编辑)与「主体更换」(龙换成马 → 引用原图替换主体并保留场景),给出不同的 prompt 模板,避免主体换不掉或场景丢失。 -3. **文字规则按需判断**:prompt 默认不含文字(标题/对白/字幕),仅在 skill 文档明确要求或用户主动要求「加文字/标题」时才用引号标注文字内容。 -4. **非 JSON 回复兜底解析**:LLM 未遵循 JSON 格式时,自动提取编号选项与澄清问题,保证交互不中断。 -5. **修改意图链路修复**:确认模式下用户点击「确认」后,正确继承上一轮的修改意图,不再断裂。 - -#### 二、生图稳定性与架构 -6. **LLM 任务后端化**:新增 `/api/agent-llm-task` 后端任务系统,LLM 推理在后端运行,前端通过 WebSocket 实时通知 + 轮询保底获取结果。页面刷新 / 息屏不再丢失思考状态。 -7. **生图任务恢复**:刷新后自动恢复后端仍在运行的生图任务(通过 `taskIds` + `placeholderNodeId`),不丢失已提交的生图。 -8. **WebSocket 实时通知**:生图完成时后端主动推送 `canvas_task_done` / `agent_llm_done`,前端无需等待轮询间隔,降低延迟。 -9. **agy CLI 生图修复**:Antigravity CLI 返回纯文字无法生图时,自动回退到 `gpt-image-2-skill` 执行生图。 - -#### 三、画布占位与比例 -10. **占位按选中比例展示**:占位节点始终按用户当前选择的比例(1:1 / 16:9 / 9:16 等)计算尺寸,不再错误使用参考图比例。 -11. **生图后尺寸重算**:生图完成后删除占位的 `w/h`,让节点按实际图片自然尺寸重新计算(与主画布 `finalizePendingNode` 行为一致),双击放大与画布显示比例一致。 -12. **占位顶部对齐**:新占位节点与已有图片节点顶部对齐,不再阶梯式错位。 -13. **占位计时修复**:占位节点显示正确的正计时(`runStartedAt` → `runFinishedAt`),不再卡在 0。 -14. **并发多图不叠加**:修复一次性生成多张图时占位节点叠在同一位置的问题(位置计算纳入 pending 占位节点)。 - -#### 四、交互体验 -15. **提示词展开/收起**:Agent 生图卡片支持点击展开完整 prompt,收起时显示 5 行。 -16. **复制优先 prompt**:复制 Agent 消息时优先复制生图 prompt,而非「正在生图中」状态文字。 -17. **多选场景自由输入提示**:仅在需要用户多选/澄清时显示「也可直接输入」提示,不常驻。 - -### v1.3 更新内容 - -#### 一、思维模式(扩写/优化提示词) -1. **思维模式开关**:在发送按钮左侧新增「思维模式」开关(脑图标 + 文字 + Tooltip)。开启后强制每次发送都先走 LLM 扩写提示词,再进入「确认 / 重新生成 / 修改提示词」三扣确认流程,用户完全掌控最终 prompt 后才生图。 -2. **确认流程**:LLM 返回的 `generations` 自动转换为 `prompts` 数组,通过 `promptIdx` 实现多图逐一确认。点击「确认」即生图并推进到下一条;「重新生成」会要求 LLM 换个方向重写;「修改」则把当前提示词回填输入框并设置绕过标志,下一条指令直接生图。 -3. **绕过机制**:用户点击「修改提示词」后设置 `agentBypassThinkingNext`,使下一条指令跳过思维模式直接生图,避免修改后还要再确认一次的冗余。 -4. **修改请求识别**:思维模式下识别「改成/换成/重新画」等修改请求关键词,不强制走扩写流程,直接生图。 - -#### 二、模糊需求统一处理 -5. **全模型统一**:所有模型(不再仅限魔搭)在面对模糊指令(如「一只猫」)时,都会触发风格选项流,而非直接发散生成。 -6. **前端兜底**:思维模式关闭时,通过 `isVagueImageRequest` 检测模糊输入并自动生成固定风格选项。兜底逻辑不再依赖 LLM `generations` 数量,只要 `options` 和 `prompts` 为空即触发。 - -#### 三、稳定性修复 -7. **发送按钮失效修复**:修复 `agentBypassThinkingNext` 变量未声明导致 `sendAgentMessage` 抛出 `ReferenceError`、发送按钮点击无反应的问题。 -8. **LLM 任务超时保护**:`pollAgentLlmTask` 由 600 次循环(约 30 分钟)缩减为 120 次并增加 5 分钟硬超时,防止任务卡死导致 `agentSending` 永远为 `true`。 -9. **恢复逻辑超时清理**:页面刷新后若 pending 任务超过 5 分钟,自动清除 `_pendingLlmTaskId` / `_pendingLlmTaskTs` 等状态,不再尝试恢复已过期的任务。 - -#### 四、UI/UX -10. **思维模式开关视觉**:按钮带高亮 `active` 态、`flex-shrink:0` 防压缩遮挡发送按钮;Tooltip 使用固定深色背景 + 高 `z-index`(9999)确保可见。 -11. **提示词确认卡片**:结构化展示当前提示词、序号计数(如 1/3)及三个操作按钮。 +> **当前版本:v1.5**(查看 [版本更新日志](#版本更新日志)) + +### 版本更新大纲 + +#### v1.0 — AI Agent 面板基线 +- AI Agent 侧边聊天面板(OneBox 风格),画布联动生图 +- `@` 引用画布图片,附件统一管理(Skill 文档 + 图片) +- 多对话管理(新建/删除/列表,与画布绑定) +- 生图参数面板:质量/比例(9种)/分辨率(1K-4K)/数量(1-8) +- LLM 结构化确认流程(选项按钮 → 确认/修改) +- 生图占位机制(占位骨架 → 完成替换,顶部对齐排列) +- 选中图片悬浮工具栏「发送至 Agent」按钮 + +#### v1.1 — 对话理解与生图稳定性 +- 系统提示词重写(5 种对话模式,强制中文提示词) +- 修改场景智能区分(风格修改 vs 主体更换) +- LLM 任务后端化(`/api/agent-llm-task` + WebSocket 实时通知) +- 刷新恢复生图任务(`taskIds` + `placeholderNodeId`) +- agy CLI 生图修复(纯文字回退 `gpt-image-2-skill`) +- 占位按选中比例展示 + 生图后尺寸重算 +- 占位顶部对齐 + 正计时 + 并发多图不叠加 +- 提示词展开/收起,复制优先 prompt + +#### v1.3 — 思维模式与稳定性 +- 思维模式开关(LLM 扩写 → 确认/重新生成/修改 三步流程) +- 绕过机制(修改后跳过二次确认直接生图) +- 修改请求关键词识别(改成/换成/重新画) +- 全模型模糊需求统一触发风格选项 +- 发送按钮失效修复(`agentBypassThinkingNext` 未声明) +- LLM 任务 5 分钟超时保护 + 恢复逻辑超时清理 +- 思维模式开关视觉优化(active 态 + Tooltip) + +#### v1.4 — 多图确认流程重构 + Skill 完整保留 +- **全部确认后统一生图**:确认流程从「逐张确认即生图」改为「全部确认/跳过后统一生图」,占位节点整齐排列 +- **prompts 状态机**:`pending → current → confirmed/skipped`,支持逐条确认/跳过/反悔 +- **内联编辑**:修改提示词改为卡片内 textarea 编辑,不再跳出确认流程 +- **全部确认/取消快捷按钮**:prompts ≥ 2 时显示「全部确认并生成」「全部取消」 +- **确认中发送新消息拦截**:有未确认 prompts 时弹窗提示 +- **刷新恢复确认进度**:中断后恢复到确认卡片,不触发生图 +- **画布对齐修复**:占位节点串行创建,解决阶梯上升/重叠 +- **生图数量校准**:按工具栏/输入框数量自动补充或截断 prompts/generations +- **Skill 完整保留**:首因+近因效应中英双语指令,确保所有 LLM provider 逐字保留 Skill 描述 +- **用户消息注入 Skill 提醒**:适配 gemini-cli 等 system_prompt 处理较弱的 provider +- **思维模式兜底 bug 修复**:LLM 回复含「正在为您生成」时不再绕过确认流程直接生图 ### 重要警示 - **已关闭自动更新**:导航栏版本号显示为「Agent版(无自动更新)」。若从上游拉取/合并最新代码,可能覆盖本分支的 AI Agent 功能,请谨慎操作并先备份。 @@ -77,6 +60,16 @@ Supports comfyui/API calls/modelscope calls > This fork adds an AI Agent panel to the canvas. Auto-update is disabled; pulling upstream changes may overwrite the Agent features. The `main` branch was force-pushed and diverges from upstream. Do not commit API keys. Commercial use remains prohibited. +#### v1.5 — 硬软参数分层 + Skill 定位明确 +- **硬软参数分层设计**: + - 软参数(出图数量):输入框显式要求 > 工具栏设置 + - 硬参数(比例/分辨率):工具栏说了算,输入框不覆盖 +- **Skill 定位明确**:Skill 描述「单张图样式」(含画面内元素排列如横3竖4),不决定出图数量 +- **统一数量决策函数** `resolveFinalGenCount`:前端决策数量,LLM 不碰参数,agy 等弱 provider 也不翻车 +- **数量提取增强**:支持 1-8,增加条/只/名/版/款等量词,中文数字支持到八 +- **系统提示词重构**:明确告知 LLM「数量已由系统决定,你无需判断」,消除 Skill「合集/一整页」被误读为只出1张的歧义 +- **用户消息 Skill 提醒加数量归属**:提示用户当前数量来自输入框还是工具栏 + ### 版本更新日志 | 版本 | 日期 | 说明 | @@ -84,6 +77,8 @@ Supports comfyui/API calls/modelscope calls | v1.0 | 2026-07-18 | AI Agent 面板基线:聊天面板、画布联动生图、多对话管理、参数面板、结构化确认流程、生图占位 | | v1.1 | 2026-07-19 | 系统提示词重写、修改场景智能区分、LLM 任务后端化、刷新恢复、WebSocket 实时通知、占位比例修复、并发多图不叠加、提示词展开收起 | | v1.3 | 2026-07-19 | 思维模式开关(扩写/确认/重新生成/修改)、全模型模糊需求统一触发选择、发送按钮失效修复、LLM 任务 5 分钟超时保护、恢复逻辑超时清理 | +| v1.4 | 2026-07-19 | 多图确认流程重构(全部确认后统一生图)、prompts 状态机、内联编辑、Skill 完整保留(首因近因双语)、思维模式兜底 bug 修复 | +| v1.5 | 2026-07-19 | 硬软参数分层(数量=软参数输入框>工具栏,比例/分辨率=硬参数工具栏)、Skill=单张图样式不决定数量、统一数量决策函数 | ---- diff --git a/VERSION b/VERSION index e7f45a810..1ce69a78a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v1.3 +v1.5 diff --git a/static/angle.html b/static/angle.html index 5e55621da..c02cca34a 100644 --- a/static/angle.html +++ b/static/angle.html @@ -17,12 +17,12 @@ } catch(e) {} })(); - - - - - - + + + + + + - + diff --git a/static/api-settings.html b/static/api-settings.html index 12d91a7fb..f60d67f31 100644 --- a/static/api-settings.html +++ b/static/api-settings.html @@ -16,12 +16,12 @@ } catch(e) {} })(); - - - - - - + + + + + +
@@ -565,6 +565,6 @@
- + diff --git a/static/asset-manager.html b/static/asset-manager.html index 4870194d9..b139effec 100644 --- a/static/asset-manager.html +++ b/static/asset-manager.html @@ -16,10 +16,10 @@ } catch(e) {} })(); - - - - + + + +
@@ -47,6 +47,6 @@
- + diff --git a/static/canvas-list.html b/static/canvas-list.html index 00c8d298b..0200b9ce3 100644 --- a/static/canvas-list.html +++ b/static/canvas-list.html @@ -16,13 +16,13 @@ } catch(e) {} })(); - - - - - - - + + + + + + +
@@ -90,6 +90,6 @@
- + diff --git a/static/canvas.html b/static/canvas.html index 6342cc8d3..98a5a8239 100644 --- a/static/canvas.html +++ b/static/canvas.html @@ -16,13 +16,13 @@ } catch(e) {} })(); - - - - - - - + + + + + + +
@@ -350,7 +350,7 @@
- - + + diff --git a/static/comfyui-settings.html b/static/comfyui-settings.html index c1da0da3b..dcf183cc5 100644 --- a/static/comfyui-settings.html +++ b/static/comfyui-settings.html @@ -15,12 +15,12 @@ } catch(e) {} })(); - - - - - - + + + + + +
@@ -122,6 +122,6 @@ preview
- + diff --git a/static/enhance.html b/static/enhance.html index 9130d1c08..675f263f6 100644 --- a/static/enhance.html +++ b/static/enhance.html @@ -17,14 +17,14 @@ } catch(e) {} })(); - - - - - - + + + + + + - + diff --git a/static/gpt-chat.html b/static/gpt-chat.html index 4b3866b4d..905eb0cef 100644 --- a/static/gpt-chat.html +++ b/static/gpt-chat.html @@ -16,13 +16,13 @@ } catch(e) {} })(); - - - - - + + + + + - + diff --git a/static/online.html b/static/online.html index 001e5f32a..f6fc36c98 100644 --- a/static/online.html +++ b/static/online.html @@ -16,14 +16,14 @@ } catch(e) {} })(); - - - - - - + + + + + + - + - + From 3d3e9dd88e001443f1f395964dc6dd4bf62241bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Sun, 19 Jul 2026 17:09:32 +0800 Subject: [PATCH 43/67] =?UTF-8?q?fix:=20=E7=9B=B4=E6=8E=A5=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E9=87=8D=E5=A4=8D=E7=94=9F=E5=9B=BE=20-=20count>1?= =?UTF-8?q?=E7=94=A8=E5=90=8C=E4=B8=80prompt=E5=8F=91=E5=A4=9A=E6=AC=A1?= =?UTF-8?q?=E8=AF=B7=E6=B1=82=E5=AF=BC=E8=87=B4=E9=87=8D=E5=A4=8D=20-=20?= =?UTF-8?q?=E7=9B=B4=E6=8E=A5=E6=A8=A1=E5=BC=8F=E6=95=B0=E9=87=8F=E6=A0=A1?= =?UTF-8?q?=E5=87=86=E6=94=B9=E4=B8=BA=E8=BF=BD=E5=8A=A0=E6=96=B0generatio?= =?UTF-8?q?n(count=3D1)=E8=80=8C=E9=9D=9E=E5=A2=9E=E5=8A=A0count=20-=20?= =?UTF-8?q?=E5=BC=BA=E5=88=B6=E6=89=80=E6=9C=89generation=E7=9A=84count=3D?= =?UTF-8?q?1=20-=20=E7=9B=B4=E6=8E=A5=E6=A8=A1=E5=BC=8F=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=E8=AF=8D=E6=98=8E=E7=A1=AE=E8=A6=81=E6=B1=82?= =?UTF-8?q?=E8=BF=94=E5=9B=9EN=E6=9D=A1=E4=B8=8D=E5=90=8Cgeneration=20-=20?= =?UTF-8?q?=E5=87=BA=E5=9B=BE=E6=95=B0=E9=87=8F=E6=8C=87=E4=BB=A4=E6=8E=AA?= =?UTF-8?q?=E8=BE=9E=E7=BB=9F=E4=B8=80=E4=B8=BA'=E8=BF=94=E5=9B=9EN?= =?UTF-8?q?=E6=9D=A1'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/angle.html | 16 ++++----- static/api-settings.html | 14 ++++---- static/asset-manager.html | 10 +++--- static/canvas-list.html | 16 ++++----- static/canvas.html | 18 +++++----- static/comfyui-settings.html | 14 ++++---- static/enhance.html | 16 ++++----- static/gpt-chat.html | 12 +++---- static/index.html | 28 +++++++-------- static/js/smart-canvas.js | 66 ++++++++++++++---------------------- static/klein.html | 16 ++++----- static/online.html | 16 ++++----- static/smart-canvas.html | 12 +++---- static/zimage.html | 14 ++++---- 14 files changed, 127 insertions(+), 141 deletions(-) diff --git a/static/angle.html b/static/angle.html index c02cca34a..55cd95199 100644 --- a/static/angle.html +++ b/static/angle.html @@ -17,12 +17,12 @@ } catch(e) {} })(); - - - - - - + + + + + + - + diff --git a/static/api-settings.html b/static/api-settings.html index f60d67f31..5cd26f7cf 100644 --- a/static/api-settings.html +++ b/static/api-settings.html @@ -16,12 +16,12 @@ } catch(e) {} })(); - - - - - - + + + + + +
@@ -565,6 +565,6 @@
- + diff --git a/static/asset-manager.html b/static/asset-manager.html index b139effec..d8cd066fa 100644 --- a/static/asset-manager.html +++ b/static/asset-manager.html @@ -16,10 +16,10 @@ } catch(e) {} })(); - - - - + + + +
@@ -47,6 +47,6 @@
- + diff --git a/static/canvas-list.html b/static/canvas-list.html index 0200b9ce3..7106f2c7c 100644 --- a/static/canvas-list.html +++ b/static/canvas-list.html @@ -16,13 +16,13 @@ } catch(e) {} })(); - - - - - - - + + + + + + +
@@ -90,6 +90,6 @@
- + diff --git a/static/canvas.html b/static/canvas.html index 98a5a8239..fa1e84e56 100644 --- a/static/canvas.html +++ b/static/canvas.html @@ -16,13 +16,13 @@ } catch(e) {} })(); - - - - - - - + + + + + + +
@@ -350,7 +350,7 @@
- - + + diff --git a/static/comfyui-settings.html b/static/comfyui-settings.html index dcf183cc5..e13a5c8dd 100644 --- a/static/comfyui-settings.html +++ b/static/comfyui-settings.html @@ -15,12 +15,12 @@ } catch(e) {} })(); - - - - - - + + + + + +
@@ -122,6 +122,6 @@ preview
- + diff --git a/static/enhance.html b/static/enhance.html index 675f263f6..b249b7955 100644 --- a/static/enhance.html +++ b/static/enhance.html @@ -17,14 +17,14 @@ } catch(e) {} })(); - - - - - - + + + + + + - + diff --git a/static/gpt-chat.html b/static/gpt-chat.html index 905eb0cef..ca542caa0 100644 --- a/static/gpt-chat.html +++ b/static/gpt-chat.html @@ -16,13 +16,13 @@ } catch(e) {} })(); - - - - - + + + + + - + diff --git a/static/online.html b/static/online.html index f6fc36c98..16cf60741 100644 --- a/static/online.html +++ b/static/online.html @@ -16,14 +16,14 @@ } catch(e) {} })(); - - - - - - + + + + + + - + - + From 93b2532249ba8e0d8bf2cb908170c3a977cf55b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Sun, 19 Jul 2026 17:22:30 +0800 Subject: [PATCH 44/67] =?UTF-8?q?docs:=20=E7=B2=BE=E7=AE=80=20README?= =?UTF-8?q?=EF=BC=8C=E7=89=88=E6=9C=AC=E5=A4=A7=E7=BA=B2=E7=A7=BB=E8=87=B3?= =?UTF-8?q?=20CHANGELOG.md=EF=BC=88README=20=E4=BB=85=E4=BF=9D=E7=95=99?= =?UTF-8?q?=E9=87=8D=E8=A6=81=E6=8F=90=E7=A4=BA=E5=92=8C=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E8=A1=A8=E6=A0=BC=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 53 ++++++++++++++++++++++++++++++++++++++++++++++ README.md | 59 +++------------------------------------------------- 2 files changed, 56 insertions(+), 56 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..879611ac3 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,53 @@ +# 版本更新大纲 + +#### v1.0 — AI Agent 面板基线 +- AI Agent 侧边聊天面板(OneBox 风格),画布联动生图 +- `@` 引用画布图片,附件统一管理(Skill 文档 + 图片) +- 多对话管理(新建/删除/列表,与画布绑定) +- 生图参数面板:质量/比例(9种)/分辨率(1K-4K)/数量(1-8) +- LLM 结构化确认流程(选项按钮 → 确认/修改) +- 生图占位机制(占位骨架 → 完成替换,顶部对齐排列) +- 选中图片悬浮工具栏「发送至 Agent」按钮 + +#### v1.1 — 对话理解与生图稳定性 +- 系统提示词重写(5 种对话模式,强制中文提示词) +- 修改场景智能区分(风格修改 vs 主体更换) +- LLM 任务后端化(`/api/agent-llm-task` + WebSocket 实时通知) +- 刷新恢复生图任务(`taskIds` + `placeholderNodeId`) +- agy CLI 生图修复(纯文字回退 `gpt-image-2-skill`) +- 占位按选中比例展示 + 生图后尺寸重算 +- 占位顶部对齐 + 正计时 + 并发多图不叠加 +- 提示词展开/收起,复制优先 prompt + +#### v1.3 — 思维模式与稳定性 +- 思维模式开关(LLM 扩写 → 确认/重新生成/修改 三步流程) +- 绕过机制(修改后跳过二次确认直接生图) +- 修改请求关键词识别(改成/换成/重新画) +- 全模型模糊需求统一触发风格选项 +- 发送按钮失效修复(`agentBypassThinkingNext` 未声明) +- LLM 任务 5 分钟超时保护 + 恢复逻辑超时清理 +- 思维模式开关视觉优化(active 态 + Tooltip) + +#### v1.4 — 多图确认流程重构 + Skill 完整保留 +- **全部确认后统一生图**:确认流程从「逐张确认即生图」改为「全部确认/跳过后统一生图」,占位节点整齐排列 +- **prompts 状态机**:`pending → current → confirmed/skipped`,支持逐条确认/跳过/反悔 +- **内联编辑**:修改提示词改为卡片内 textarea 编辑,不再跳出确认流程 +- **全部确认/取消快捷按钮**:prompts ≥ 2 时显示「全部确认并生成」「全部取消」 +- **确认中发送新消息拦截**:有未确认 prompts 时弹窗提示 +- **刷新恢复确认进度**:中断后恢复到确认卡片,不触发生图 +- **画布对齐修复**:占位节点串行创建,解决阶梯上升/重叠 +- **生图数量校准**:按工具栏/输入框数量自动补充或截断 prompts/generations +- **Skill 完整保留**:首因+近因效应中英双语指令,确保所有 LLM provider 逐字保留 Skill 描述 +- **用户消息注入 Skill 提醒**:适配 gemini-cli 等 system_prompt 处理较弱的 provider +- **思维模式兜底 bug 修复**:LLM 回复含「正在为您生成」时不再绕过确认流程直接生图 + +#### v1.5 — 硬软参数分层 + Skill 定位明确 +- **硬软参数分层设计**: + - 软参数(出图数量):输入框显式要求 > 工具栏设置 + - 硬参数(比例/分辨率):工具栏说了算,输入框不覆盖 +- **Skill 定位明确**:Skill 描述「单张图样式」(含画面内元素排列如横3竖4),不决定出图数量 +- **统一数量决策函数** `resolveFinalGenCount`:前端决策数量,LLM 不碰参数,agy 等弱 provider 也不翻车 +- **数量提取增强**:支持 1-8,增加条/只/名/版/款等量词,中文数字支持到八 +- **系统提示词重构**:明确告知 LLM「数量已由系统决定,你无需判断」,消除 Skill「合集/一整页」被误读为只出1张的歧义 +- **用户消息 Skill 提醒加数量归属**:提示用户当前数量来自输入框还是工具栏 +- **直接模式重复生图修复**:数量校准改为追加新 generation(count=1)而非增加 count,强制所有 generation count=1 diff --git a/README.md b/README.md index 4a9989eac..881709c60 100644 --- a/README.md +++ b/README.md @@ -7,52 +7,9 @@ Supports comfyui/API calls/modelscope calls > 本仓库是基于原项目 [hero8152/Infinite-Canvas](https://github.com/hero8152/Infinite-Canvas) 的二次开发 fork,新增了智能画布 AI Agent 面板。 > -> **当前版本:v1.5**(查看 [版本更新日志](#版本更新日志)) - -### 版本更新大纲 - -#### v1.0 — AI Agent 面板基线 -- AI Agent 侧边聊天面板(OneBox 风格),画布联动生图 -- `@` 引用画布图片,附件统一管理(Skill 文档 + 图片) -- 多对话管理(新建/删除/列表,与画布绑定) -- 生图参数面板:质量/比例(9种)/分辨率(1K-4K)/数量(1-8) -- LLM 结构化确认流程(选项按钮 → 确认/修改) -- 生图占位机制(占位骨架 → 完成替换,顶部对齐排列) -- 选中图片悬浮工具栏「发送至 Agent」按钮 - -#### v1.1 — 对话理解与生图稳定性 -- 系统提示词重写(5 种对话模式,强制中文提示词) -- 修改场景智能区分(风格修改 vs 主体更换) -- LLM 任务后端化(`/api/agent-llm-task` + WebSocket 实时通知) -- 刷新恢复生图任务(`taskIds` + `placeholderNodeId`) -- agy CLI 生图修复(纯文字回退 `gpt-image-2-skill`) -- 占位按选中比例展示 + 生图后尺寸重算 -- 占位顶部对齐 + 正计时 + 并发多图不叠加 -- 提示词展开/收起,复制优先 prompt - -#### v1.3 — 思维模式与稳定性 -- 思维模式开关(LLM 扩写 → 确认/重新生成/修改 三步流程) -- 绕过机制(修改后跳过二次确认直接生图) -- 修改请求关键词识别(改成/换成/重新画) -- 全模型模糊需求统一触发风格选项 -- 发送按钮失效修复(`agentBypassThinkingNext` 未声明) -- LLM 任务 5 分钟超时保护 + 恢复逻辑超时清理 -- 思维模式开关视觉优化(active 态 + Tooltip) - -#### v1.4 — 多图确认流程重构 + Skill 完整保留 -- **全部确认后统一生图**:确认流程从「逐张确认即生图」改为「全部确认/跳过后统一生图」,占位节点整齐排列 -- **prompts 状态机**:`pending → current → confirmed/skipped`,支持逐条确认/跳过/反悔 -- **内联编辑**:修改提示词改为卡片内 textarea 编辑,不再跳出确认流程 -- **全部确认/取消快捷按钮**:prompts ≥ 2 时显示「全部确认并生成」「全部取消」 -- **确认中发送新消息拦截**:有未确认 prompts 时弹窗提示 -- **刷新恢复确认进度**:中断后恢复到确认卡片,不触发生图 -- **画布对齐修复**:占位节点串行创建,解决阶梯上升/重叠 -- **生图数量校准**:按工具栏/输入框数量自动补充或截断 prompts/generations -- **Skill 完整保留**:首因+近因效应中英双语指令,确保所有 LLM provider 逐字保留 Skill 描述 -- **用户消息注入 Skill 提醒**:适配 gemini-cli 等 system_prompt 处理较弱的 provider -- **思维模式兜底 bug 修复**:LLM 回复含「正在为您生成」时不再绕过确认流程直接生图 - -### 重要警示 +> **当前版本:v1.5**(查看 [版本更新大纲](./CHANGELOG.md)) + +### 重要提示 - **已关闭自动更新**:导航栏版本号显示为「Agent版(无自动更新)」。若从上游拉取/合并最新代码,可能覆盖本分支的 AI Agent 功能,请谨慎操作并先备份。 - **main 分支与上游分叉**:本 fork 的 `main` 通过强制推送覆盖,历史与上游不一致,切勿直接向上游发起合并。 - **禁止商业用途**:沿用原作者版权声明(见文末),二次开发须保持开源并注明来源作者。 @@ -60,16 +17,6 @@ Supports comfyui/API calls/modelscope calls > This fork adds an AI Agent panel to the canvas. Auto-update is disabled; pulling upstream changes may overwrite the Agent features. The `main` branch was force-pushed and diverges from upstream. Do not commit API keys. Commercial use remains prohibited. -#### v1.5 — 硬软参数分层 + Skill 定位明确 -- **硬软参数分层设计**: - - 软参数(出图数量):输入框显式要求 > 工具栏设置 - - 硬参数(比例/分辨率):工具栏说了算,输入框不覆盖 -- **Skill 定位明确**:Skill 描述「单张图样式」(含画面内元素排列如横3竖4),不决定出图数量 -- **统一数量决策函数** `resolveFinalGenCount`:前端决策数量,LLM 不碰参数,agy 等弱 provider 也不翻车 -- **数量提取增强**:支持 1-8,增加条/只/名/版/款等量词,中文数字支持到八 -- **系统提示词重构**:明确告知 LLM「数量已由系统决定,你无需判断」,消除 Skill「合集/一整页」被误读为只出1张的歧义 -- **用户消息 Skill 提醒加数量归属**:提示用户当前数量来自输入框还是工具栏 - ### 版本更新日志 | 版本 | 日期 | 说明 | From c53d39da0e2bf026e68f7d9b6aa859921192d59f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Sun, 19 Jul 2026 17:35:53 +0800 Subject: [PATCH 45/67] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=8D=95=20gene?= =?UTF-8?q?ration=20=E5=A4=9A=E5=9B=BE=E6=B3=84=E6=BC=8F=E2=80=94=E2=80=94?= =?UTF-8?q?=E9=99=90=E5=88=B6=E6=AF=8F=20gen=20=E6=9C=80=E5=A4=9A=20gen.co?= =?UTF-8?q?unt=20=E5=BC=A0=E5=9B=BE+=E8=BF=87=E6=BB=A4=E5=8F=82=E8=80=83?= =?UTF-8?q?=E5=9B=BE=20URL+count=3D1=20=E5=BC=BA=E5=88=B6=E8=8C=83?= =?UTF-8?q?=E5=9B=B4=E6=89=A9=E5=A4=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 5 +++++ README.md | 1 + static/js/smart-canvas.js | 15 ++++++++++++--- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 879611ac3..8f3470d42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,3 +51,8 @@ - **系统提示词重构**:明确告知 LLM「数量已由系统决定,你无需判断」,消除 Skill「合集/一整页」被误读为只出1张的歧义 - **用户消息 Skill 提醒加数量归属**:提示用户当前数量来自输入框还是工具栏 - **直接模式重复生图修复**:数量校准改为追加新 generation(count=1)而非增加 count,强制所有 generation count=1 + +#### v1.5.1 — 单 generation 多图泄漏修复 +- **API 单次返回多图修复**:某些 provider 单次生图调用会返回多张图,导致单个 generation 节点出现多张图。现在限制每个 generation 最多只取 `gen.count` 张图 +- **参考图 URL 过滤**:某些 provider 会在响应中回显输入的参考图 URL,造成「重复」问题。现在过滤掉与参考图 URL 相同的结果 +- **count=1 强制范围扩大**:直接模式下无论 `requestedCount` 是否大于 1,都强制所有 generation 的 `count=1`(之前只在 `requestedCount > 1` 时才强制,存在漏洞) diff --git a/README.md b/README.md index 881709c60..1b35128be 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Supports comfyui/API calls/modelscope calls | v1.3 | 2026-07-19 | 思维模式开关(扩写/确认/重新生成/修改)、全模型模糊需求统一触发选择、发送按钮失效修复、LLM 任务 5 分钟超时保护、恢复逻辑超时清理 | | v1.4 | 2026-07-19 | 多图确认流程重构(全部确认后统一生图)、prompts 状态机、内联编辑、Skill 完整保留(首因近因双语)、思维模式兜底 bug 修复 | | v1.5 | 2026-07-19 | 硬软参数分层(数量=软参数输入框>工具栏,比例/分辨率=硬参数工具栏)、Skill=单张图样式不决定数量、统一数量决策函数 | +| v1.5.1 | 2026-07-19 | 单 generation 多图泄漏修复(API 返回多图限制+参考图 URL 过滤)、count=1 强制范围扩大到所有直接模式 | ---- diff --git a/static/js/smart-canvas.js b/static/js/smart-canvas.js index e7dba8424..9eb323f8e 100644 --- a/static/js/smart-canvas.js +++ b/static/js/smart-canvas.js @@ -17993,7 +17993,10 @@ async function processAgentLlmResult(result, text, attachments, userMsg){ // 多于请求数量 → 截断到请求数量 parsed.generations = parsed.generations.slice(0, requestedCount); } - // 确保每条 generation 的 count=1(防止 LLM 返回 count>1 导致重复) + } + // 直接模式下,无论 requestedCount 多少,强制所有 generation 的 count=1 + // (防止 LLM 返回 count>1 导致同一 prompt 发多次请求、生成重复图) + if(!thinkingModeOn && parsed.generations.length > 0){ parsed.generations.forEach(g => { g.count = 1; }); } const assistantMsg = {id:uid('am'), role:'assistant', text:parsed.reply, options:parsed.options || [], prompts:parsed.prompts || [], generations:parsed.generations, ts:Date.now()}; @@ -18490,10 +18493,14 @@ async function runAgentGenerations(assistantMsg, userMsg){ if(placeholderNode) gen.placeholderNodeId = placeholderNode.id; saveAgentState(); const results = await Promise.all(imageTaskIds.map(id => pollSmartCanvasTask(id))); + // 限制每个 generation 最多只取 gen.count 张图(防止 API 单次返回多张图导致节点图片重复) + // 同时过滤掉参考图 URL(防止某些 provider 在响应中回显输入的参考图,造成"重复"问题) + const _maxCount = Math.max(1, Math.min(8, Number(gen.count) || 1)); + const _refUrlSet = new Set((refs || []).map(r => r?.url).filter(Boolean)); const urls = results.flatMap(res => resultMediaUrls(res)).map((item, i) => { const url = typeof item === 'string' ? item : item?.url || ''; return {url, name:(typeof item === 'object' && item?.name) || `agent-${Date.now()}-${i + 1}.png`, kind:'image'}; - }).filter(i => i.url); + }).filter(i => i.url && !_refUrlSet.has(i.url)).slice(0, _maxCount); gen.results = urls; gen.status = 'done'; if(urls.length && placeholderNode){ @@ -18542,10 +18549,12 @@ async function recoverAgentGenerations(){ } try { const results = await Promise.all(gen.taskIds.map(id => pollSmartCanvasTask(id))); + // 限制每个 generation 最多只取 gen.count 张图(与 runAgentGenerations 一致,防止 API 单次返回多张图) + const _maxCount = Math.max(1, Math.min(8, Number(gen.count) || 1)); const urls = results.flatMap(res => resultMediaUrls(res)).map((item, idx) => { const url = typeof item === 'string' ? item : item?.url || ''; return {url, name:(typeof item === 'object' && item?.name) || `agent-${Date.now()}-${idx + 1}.png`, kind:'image'}; - }).filter(i => i.url); + }).filter(i => i.url).slice(0, _maxCount); gen.results = urls; gen.status = 'done'; if(urls.length && placeholderNode){ From 8ef2cf08c768a0d66c11133da8e49b36ec5b4a74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Sun, 19 Jul 2026 18:29:56 +0800 Subject: [PATCH 46/67] =?UTF-8?q?feat(smart-canvas):=20=E6=80=9D=E7=BB=B4?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E4=B8=A4=E9=98=B6=E6=AE=B5=E6=B5=81=E7=A8=8B?= =?UTF-8?q?+=E5=8E=BB=E6=8E=89=E8=B7=B3=E8=BF=87=E6=8C=89=E9=92=AE+?= =?UTF-8?q?=E4=BF=AE=E5=A4=8DUI=E8=A3=81=E5=88=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/smart-canvas.css | 4 +- static/js/smart-canvas.js | 97 ++++++++++++++++++++++++++++--------- 2 files changed, 77 insertions(+), 24 deletions(-) diff --git a/static/css/smart-canvas.css b/static/css/smart-canvas.css index e7b6dc425..74aebef0e 100644 --- a/static/css/smart-canvas.css +++ b/static/css/smart-canvas.css @@ -1241,7 +1241,9 @@ input[type=range].smart-range::-webkit-slider-thumb { -webkit-appearance:none; w } .agent-prompt-card-body { padding:8px 10px; font-size:11px; line-height:1.5; color:var(--text); - word-break:break-word; white-space:pre-wrap; max-height:200px; overflow-y:auto; + word-break:break-word; white-space:pre-wrap; + max-height:min(260px, 40vh); overflow-y:auto; + scrollbar-width:thin; scrollbar-color:rgba(148,163,184,.42) transparent; } .agent-prompt-card-actions { display:flex; gap:4px; padding:6px 8px; } .agent-prompt-btn { diff --git a/static/js/smart-canvas.js b/static/js/smart-canvas.js index 9eb323f8e..96534f820 100644 --- a/static/js/smart-canvas.js +++ b/static/js/smart-canvas.js @@ -17061,6 +17061,21 @@ function resolveFinalGenCount(text){ const toolbar = Math.max(1, Math.min(8, Number(agentState?.genCount) || 1)); return {count: toolbar, source:'toolbar'}; } +// 判断输入是否"模糊"(缺风格维度),用于思维模式前端兜底 +// 判断标准:字数少 + 不含风格/艺术流派关键词 +// 返回 true 表示需要先走阶段一(返回 options 让用户选风格) +function isVagueImageRequest(text){ + const t = String(text || '').trim(); + if(!t) return false; + // 修改请求不算模糊(有明确的修改方向) + if(/改成|换成|转换成|修改为|变成|转为|改为|转成|调整|重新画|重画/i.test(t)) return false; + // 风格/艺术流派关键词 + const styleKeywords = ['风','风格','主义','流派','艺术','画法','画风','渲染','摄影','插画','海报','logo','标志','图标','3d','3D','写实','动漫','水墨','油画','水彩','素描','速写','像素','赛博','蒸汽波','极简','极繁','扁平','卡通','可爱','复古','复古风','霓虹','蒸汽','lowpoly','low poly','波普','波普艺术','印象派','抽象','超现实','涂鸦','手绘','国风','中国风','日式','和风','美式','欧式','赛博朋克','蒸汽朋克','未来主义','装饰艺术','artdeco','art deco','bauhaus','包豪斯','印象','点彩','浮世绘','赛璐珞','吉卜力','新海诚','皮克斯','迪士尼','漫威','dc','chibi','q版','q版','q版','q版','q版']; + const hasStyle = styleKeywords.some(k => t.toLowerCase().includes(k.toLowerCase())); + // 字数少且无风格 → 模糊 + if(t.length < 25 && !hasStyle) return true; + return false; +} function agentRatioLabel(key){ const map = {square:'1:1', portrait:'2:3', portrait43:'3:4', landscape43:'4:3', landscape:'3:2', story:'9:16', wide:'16:9', ultrawide:'21:9', ultratall:'9:21'}; return map[key] || key || '1:1'; @@ -17295,7 +17310,7 @@ function agentMessageHtml(msg){ // 当前项展开操作按钮 let itemActionsHtml = ''; if(p.status === 'current'){ - itemActionsHtml = `
`; + itemActionsHtml = `
`; } else if(p.status === 'editing'){ itemActionsHtml = `
`; } @@ -17331,7 +17346,10 @@ function renderAgentMessages(){ agentMessages.innerHTML = msgs.map(agentMessageHtml).join('') + thinking; } if(window.lucide) lucide.createIcons(); - agentMessages.scrollTop = agentMessages.scrollHeight; + // 延迟滚动:确保布局完成后再滚动到底部,避免卡片高度变化导致底部按钮被裁切 + requestAnimationFrame(() => { + if(agentMessages) agentMessages.scrollTop = agentMessages.scrollHeight; + }); if(agentSendBtn) agentSendBtn.disabled = agentSending; // 绑定消息操作按钮 agentMessages.querySelectorAll('[data-agent-copy]').forEach(btn => { @@ -17418,7 +17436,6 @@ function renderAgentMessages(){ const msg = (agentState.messages || []).find(m => m.id === msgId); if(!msg) return; if(action === 'confirm') confirmAgentPrompt(msg); - else if(action === 'skip') skipAgentPrompt(msg); else if(action === 'regenerate') regenerateAgentPrompts(msg); else if(action === 'edit') editAgentPrompt(msg); else if(action === 'save-edit') saveAgentPromptEdit(msg); @@ -17659,19 +17676,49 @@ When a skill document is provided, every prompt you generate MUST fully and verb // P1-9: 系统提示词动态化 —— 根据思维模式开关追加不同指令 const thinkingModeOn = agentState?.thinkingMode && !bypassThinking; if(thinkingModeOn){ - parts.push(`当前为思维模式。请只返回 prompts 数组(不要返回 generations 数组)。 + parts.push(`当前为思维模式(用户参与决策模式)。核心原则:用户在生图前必须看到并确认每一条提示词,确认后才统一生图。 -重要规则: -1. 每个 prompt 对象的 count 固定为 1。不要在单个 prompt 中用 count>1 来生成多张图。 -2. 系统要求生成N张图时(见上方"出图数量"),prompts 数组必须返回恰好N条不同的 prompt(每条 count=1),让用户逐个确认。 - 数量由系统决定,你无需判断用户想要几张,只需按系统给定的数量返回对应条数。 - 例如系统要求4张,就返回4条不同的 prompt(水墨龙、油画龙、赛博龙、Q版龙等)。 -3. 每个 prompt 对象包含:prompt(中文提示词), count(固定为1), use_last_outputs(bool), use_attachments(bool)。 -4. 用户会逐个确认后才统一生图,所以每条 prompt 都应该是完整、独立、可单独生成的。 - Skill 描述的是单张图样式(含画面内元素排列),不决定出图数量。即使 skill 写了"合集/一整页/系列",也按系统给定的数量返回多条 prompt,每条都完整包含 skill 的样式描述。 - ${hasSkills ? '每条 prompt 都必须完整包含 skill 文档的所有描述内容,只在主题/变体上做差异化(见上方"skill 完整保留"规则)。' : ''} -5. 如果请求模糊,可以先返回 options 让用户选方向,下一轮再返回 prompts。 -6. 如果是修改请求("换成像素风"),仍返回 prompts,但 use_last_outputs 设为 true。`); +【参数权重 / Parameter Priority】(由系统决定,你无需判断,也不要在prompt里加数量/比例/分辨率描述) +- 出图数量:输入框显式要求 > 工具栏设置(软参数) +- 比例/分辨率:工具栏说了算(硬参数),输入框不覆盖 +- 你只需按系统给定的数量返回对应条数,不要自行增减。 + +【两阶段流程 / Two-Stage Flow】 + +■ 阶段一:风格引导(仅当输入"缺风格"时触发) +判断标准——用户的输入有6个信息维度:①主体 ②风格(画风/艺术流派) ③场景/背景 ④配色 ⑤构图 ⑥细节特征。 +- 如果只有①主体,缺②风格 → 必须返回 2-4 个 options(风格/方向选项)让用户选择,不要直接返回 prompts。 + 选项示例:[水墨风、油画风、赛博朋克、Q版卡通] 或 [极简logo、复古徽章、几何扁平、手绘插画] + 返回 {"reply":"请选择一个风格方向:","options":[{"label":"水墨风","value":"水墨风"},...],"prompts":[],"generations":[]} +- 如果②风格已存在(即使简短如"水墨风的狗"),跳过阶段一,直接进入阶段二。 + +■ 阶段二:扩写/优化 + 确认(用户选了风格后,或输入本身已有风格时进入) +根据输入完整度分三档处理: + +【档A:大幅扩写】——有主体+风格,但场景/配色/构图/细节缺失3项以上 +你必须大幅扩写,补全所有缺失维度,生成完整可直接生图的中文提示词。 +示例:用户输入"一只狗,水墨风" → 你扩写为"一只水墨风格的西高地白犬,中国传统水墨画技法,淡墨晕染毛发,浓墨点睛,大量留白营造意境,毛笔笔触可见,站姿端正微微侧头,眼神温和灵动,背景大面积留白,右下角有朱砂印章,意境悠远,水墨画质感,宣纸纹理" + +【档B:适度优化】——主体+风格+场景+配色+构图+细节大部分都有 +不要重写用户的核心描述,只做适度优化:补充技术参数(如"高质量、印刷级精度")、理顺语句、确保完整性。 +如果输入已经足够完整专业,可以基本保持原样,但在 reply 里说明"你的提示词已较完整,可直接确认或微调"。 +示例:用户输入"海报设计,大师级排版,极繁主义,配色协调,波点,半调图案..." → 你保持核心描述,补充"高质量海报输出,印刷级精度"即可,不要把"极繁主义"改成"极简主义",不要删减用户写的细节。 + +【档C:修改请求】——用户说"换成像素风""改成夜景"等 +返回 prompts,use_last_outputs 设为 true(引用上一轮图片)。在已有基础上做修改,保留未要求改动的部分。 + +【返回规则】 +1. 只返回 prompts 数组,不要返回 generations 数组。 +2. 每个 prompt 对象的 count 固定为 1。不要用 count>1。 +3. 系统要求生成N张图时(见上方"出图数量"),prompts 数组返回恰好N条。 + 每条必须是完整的、可直接生图的中文提示词。 + N条之间在主题方向/变体方向上有差异(不是换风格,而是在同一风格下做变体)。 + 例如用户选了"水墨风"+系统要3张 → 返回3条水墨风prompt,分别画不同姿态/场景/氛围的狗。 +4. 每个 prompt 对象包含:prompt(中文提示词), count(固定为1), use_last_outputs(bool), use_attachments(bool)。 +5. 用户会逐条确认(确认/修改/重新生成),全部确认后才统一生图。 +6. 所有prompt必须中文,包含主体/风格/构图/光线/色彩/细节/氛围。 +7. 文字规则:默认prompt不包含文字内容,除非skill文档明确要求或用户明确要求"加文字"。 +${hasSkills ? '8. 每条 prompt 必须完整包含 skill 文档的所有描述内容,只在主题/变体上做差异化(见上方"skill 完整保留"规则)。Skill 描述单张图样式,不决定出图数量。' : ''}`); } else { parts.push(`当前为直接模式。能生成就生成,返回 generations 数组。 重要规则: @@ -17943,6 +17990,18 @@ async function processAgentLlmResult(result, text, attachments, userMsg){ parsed.prompts = convertedPrompts.concat(parsed.prompts); parsed.generations = []; } + // 1.5 前端兜底:思维模式下,如果输入模糊(缺风格)但 LLM 返回了 prompts(没走阶段一),强制走 options + // 这样即使 LLM 没按系统提示词执行阶段一,前端也能保证"先选风格再扩写"的流程 + if(!isModifyRequest && parsed.prompts.length > 0 && parsed.options.length === 0 && isVagueImageRequest(text)){ + parsed.prompts = []; + parsed.options = [ + {label:'水墨风', value:'水墨风'}, + {label:'油画风', value:'油画风'}, + {label:'赛博朋克', value:'赛博朋克'}, + {label:'Q版卡通', value:'Q版卡通'} + ]; + parsed.reply = '你的输入比较简略,请先选择一个风格方向,我再为你扩写完整提示词:'; + } // 2. 如果 prompts 仍为空,创建默认 prompt if(parsed.prompts.length === 0 && parsed.options.length === 0 && parsed.generations.length === 0){ parsed.prompts = [{prompt:text, count:1, use_last_outputs:isModifyRequest, use_attachments:false, status:'pending'}]; @@ -18251,14 +18310,6 @@ async function confirmAgentPrompt(assistantMsg){ prompts[idx].status = 'confirmed'; await _advanceToNextOrGenerate(assistantMsg); } -// 跳过当前提示词:标记为 skipped,推进到下一个 pending -async function skipAgentPrompt(assistantMsg){ - const prompts = assistantMsg.prompts || []; - const idx = prompts.findIndex(p => p.status === 'current' || p.status === 'editing'); - if(idx < 0) return; - prompts[idx].status = 'skipped'; - await _advanceToNextOrGenerate(assistantMsg); -} // 修改提示词:进入内联编辑模式(不跳出确认流程,不设置 bypass 标志) function editAgentPrompt(assistantMsg){ const prompts = assistantMsg.prompts || []; From 2fec373835336c973c23c02a2b44ea6e005d6f0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Sun, 19 Jul 2026 18:51:45 +0800 Subject: [PATCH 47/67] =?UTF-8?q?fix(css):=20=E4=BF=AE=E5=A4=8D=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=E8=AF=8D=E5=8D=A1=E7=89=87=E5=92=8C=E6=8C=89=E9=92=AE?= =?UTF-8?q?=E8=B6=85=E5=87=BA=E5=AF=B9=E8=AF=9D=E6=A1=86=E8=A2=AB=E8=A3=81?= =?UTF-8?q?=E5=88=87=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/smart-canvas.css | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/static/css/smart-canvas.css b/static/css/smart-canvas.css index 74aebef0e..523f68e8d 100644 --- a/static/css/smart-canvas.css +++ b/static/css/smart-canvas.css @@ -1234,6 +1234,7 @@ input[type=range].smart-range::-webkit-slider-thumb { -webkit-appearance:none; w .agent-prompt-card { margin-top:6px; border:1px solid var(--line); border-radius:10px; background:var(--card); overflow:hidden; + width:100%; min-width:0; box-sizing:border-box; } .agent-prompt-card-header { padding:5px 10px; font-size:10px; font-weight:700; color:var(--muted); @@ -1245,18 +1246,19 @@ input[type=range].smart-range::-webkit-slider-thumb { -webkit-appearance:none; w max-height:min(260px, 40vh); overflow-y:auto; scrollbar-width:thin; scrollbar-color:rgba(148,163,184,.42) transparent; } -.agent-prompt-card-actions { display:flex; gap:4px; padding:6px 8px; } +.agent-prompt-card-actions { display:flex; gap:4px; padding:6px 8px; min-width:0; width:100%; box-sizing:border-box; } .agent-prompt-btn { - flex:1; min-height:26px; padding:4px 6px; border-radius:6px; + flex:1 1 0; min-width:0; min-height:26px; padding:4px 6px; border-radius:6px; border:1px solid var(--line); background:var(--card); color:var(--text); font-size:10px; font-weight:650; cursor:pointer; text-align:center; transition:border-color .14s ease, background .14s ease; white-space:nowrap; + overflow:hidden; text-overflow:ellipsis; } .agent-prompt-btn:hover { border-color:var(--muted); background:var(--soft); } .agent-prompt-btn.primary { background:var(--strong); color:var(--strong-text); border-color:var(--strong); } .agent-prompt-btn.primary:hover { opacity:.88; } .agent-prompt-edit-area { - width:100%; min-height:60px; max-height:200px; margin:0; + width:100%; min-width:0; box-sizing:border-box; min-height:60px; max-height:200px; margin:0; padding:8px 10px; border:none; border-top:1px solid var(--line); background:var(--card); color:var(--text); font-size:11px; line-height:1.5; resize:vertical; @@ -1279,8 +1281,8 @@ input[type=range].smart-range::-webkit-slider-thumb { -webkit-appearance:none; w .agent-prompt-list-item.current .agent-prompt-list-icon { color:var(--strong); } .agent-prompt-list-index { font-weight:650; color:var(--muted); flex-shrink:0; font-size:9.5px; } .agent-prompt-list-text { flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:var(--text); } -.agent-prompt-item-actions { display:flex; gap:4px; padding:4px 8px 6px; } -.agent-prompt-card-footer { display:flex; gap:4px; padding:6px 8px; border-top:1px solid var(--line); background:var(--soft); } +.agent-prompt-item-actions { display:flex; gap:4px; padding:4px 8px 6px; min-width:0; width:100%; box-sizing:border-box; } +.agent-prompt-card-footer { display:flex; gap:4px; padding:6px 8px; border-top:1px solid var(--line); background:var(--soft); min-width:0; width:100%; box-sizing:border-box; } .agent-prompt-confirm-all { flex:2; } .agent-prompt-cancel-all { flex:1; } .agent-msg-free-hint { margin-top:4px; font-size:9.5px; opacity:.45; } @@ -1294,7 +1296,7 @@ input[type=range].smart-range::-webkit-slider-thumb { -webkit-appearance:none; w .agent-empty { margin:auto; text-align:center; color:var(--faint); font-size:11px; line-height:1.7; padding:20px 12px; } .agent-empty i,.agent-empty svg { width:26px; height:26px; display:block; margin:0 auto 8px; } -.agent-msg { display:flex; flex-direction:column; gap:5px; max-width:88%; } +.agent-msg { display:flex; flex-direction:column; gap:5px; max-width:88%; min-width:0; } .agent-msg.user { align-self:flex-end; align-items:flex-end; } .agent-msg.assistant { align-self:flex-start; align-items:flex-start; } .agent-msg-bubble { padding:8px 11px; border-radius:14px; font-size:11.5px; line-height:1.55; white-space:pre-wrap; word-break:break-word; } From 531f145e3708eb73f33c4a6ca05f1dde27cc8b05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Sun, 19 Jul 2026 19:03:26 +0800 Subject: [PATCH 48/67] =?UTF-8?q?fix:=20codex=20CLI=20--size=20=E4=B8=8D?= =?UTF-8?q?=E6=94=AF=E6=8C=81=201K=EF=BC=8C=E6=98=A0=E5=B0=84=E4=B8=BA=20a?= =?UTF-8?q?uto=EF=BC=9Bprompt=20=E6=96=87=E6=9C=AC=E4=BB=8D=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=201K?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/main.py b/main.py index 9f6c092b6..71afa7848 100755 --- a/main.py +++ b/main.py @@ -4601,14 +4601,14 @@ def gpt_image_2_skill_size_arg(size="", model="", prompt="", provider="openai"): size_text = str(size or "").strip() if str(provider or "").strip().lower() == "codex": if "1k" in text or "1024" in text: - return "1K" + return "auto" if "2k" in text or "2048" in text: return "2K" if "4k" in text or "3840" in text: return "4K" width, height = parse_size_pair(size_text) if 0 < max(width, height) < 1800: - return "1K" + return "auto" if 1800 <= max(width, height) < 3000: return "2K" return "4K" @@ -4631,7 +4631,7 @@ def gpt_image_2_skill_size_arg(size="", model="", prompt="", provider="openai"): if "4k" in text or "3840" in text: return "4K" if "1k" in text or "1024" in text: - return "1K" + return "auto" return "2K" def gpt_image_2_skill_prompt_arg(prompt="", size="", provider="openai"): @@ -4639,6 +4639,7 @@ def gpt_image_2_skill_prompt_arg(prompt="", size="", provider="openai"): if str(provider or "").strip().lower() != "codex": return prompt_text size_arg = gpt_image_2_skill_size_arg(size, "", prompt, provider) + size_display = "1K" if size_arg == "auto" else size_arg size_text = str(size or "").strip() width, height = parse_size_pair(size_text) ratio_text = "" @@ -4652,15 +4653,15 @@ def gpt_image_2_skill_prompt_arg(prompt="", size="", provider="openai"): height = int(ratio_match.group(2)) ratio_text = f"{width}:{height}" if not ratio_text: - return f"{prompt_text} 画质要求:目标输出 {size_arg} 高分辨率图片。 Image quality requirement: output a {size_arg} high-resolution image." + return f"{prompt_text} 画质要求:目标输出 {size_display} 高分辨率图片。 Image quality requirement: output a {size_display} high-resolution image." orientation_zh = "横版/宽幅" if width > height else ("竖版/长幅" if height > width else "正方形") orientation_en = "landscape/wide" if width > height else ("portrait/tall" if height > width else "square") return ( f"{prompt_text} " - f"画质要求:目标输出 {size_arg} 高分辨率图片。" + f"画质要求:目标输出 {size_display} 高分辨率图片。" f"画幅要求:必须生成 {orientation_zh} 图片,宽高比 {ratio_text}。" f"请不要交换宽高,不要输出反向比例。" - f" Image quality requirement: output a {size_arg} high-resolution image." + f" Image quality requirement: output a {size_display} high-resolution image." f" Canvas requirement: generate a {orientation_en} image with aspect ratio {ratio_text}; " "do not swap width and height." ) From 5d05d735b2fd3572f64c632f4332c76b30eb5f91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Mon, 20 Jul 2026 10:56:54 +0800 Subject: [PATCH 49/67] =?UTF-8?q?fix:=20=E6=80=9D=E7=BB=B4=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E9=98=B6=E6=AE=B5=E4=BA=8C=E6=95=B0=E9=87=8F=E7=BB=A7?= =?UTF-8?q?=E6=89=BF+=E8=A1=A5=E5=85=85prompts=E5=B7=AE=E5=BC=82=E5=8C=96+?= =?UTF-8?q?generations=E9=87=8D=E5=A4=8D=E8=BF=BD=E5=8A=A0=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/js/smart-canvas.js | 51 +++++++++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/static/js/smart-canvas.js b/static/js/smart-canvas.js index 96534f820..74d565265 100644 --- a/static/js/smart-canvas.js +++ b/static/js/smart-canvas.js @@ -17957,7 +17957,15 @@ async function processAgentLlmResult(result, text, attachments, userMsg){ // 批量完整性检查(P2-12:弱化为显示提示,不再强制追加 reply) // 前端数量决策:输入框显式要求 > 工具栏设置(与 sendAgentMessage 一致) // 优先用 sendAgentMessage 已存入 userMsg 的值,避免重复计算导致不一致 - let requestedCount = userMsg?.requestedCount || resolveFinalGenCount(text).count; + let requestedCount = userMsg?.requestedCount || 0; + // 阶段二继承:如果 userMsg 没有 requestedCount,从上一条 user 消息继承 + if(requestedCount <= 1){ + const _prevUserMsg = [...(agentState.messages || [])].reverse().find(m => m.role === 'user'); + if(_prevUserMsg?.requestedCount > 1){ + requestedCount = _prevUserMsg.requestedCount; + } + } + if(requestedCount <= 1) requestedCount = resolveFinalGenCount(text).count; // 如果最终数量 <= 1,相当于没有明确请求多张,设为 0 不触发数量逻辑 if(requestedCount <= 1) requestedCount = 0; if(thinkingModeOn){ @@ -18008,14 +18016,20 @@ async function processAgentLlmResult(result, text, attachments, userMsg){ if(!parsed.reply) parsed.reply = '请确认以下提示词:'; } // 3. 数量校准:如果用户设置了 genCount>1 或文本请求了N张 - // 3a. 少于请求数量 → 补充 + // 3a. 少于请求数量 → 补充(加入差异化方向,避免生成的图几乎一样) if(requestedCount > 1 && parsed.prompts.length > 0 && parsed.prompts.length < requestedCount){ const basePrompts = parsed.prompts.slice(); + const variantDirections = [ + '不同姿态与动作', '不同场景与氛围', '不同视角与构图', + '不同配色与光线', '不同细节与装饰', '不同表情与神态', + '不同背景与环境', '不同材质与质感' + ]; while(parsed.prompts.length < requestedCount){ const base = basePrompts[parsed.prompts.length % basePrompts.length]; - const variantIdx = Math.floor(parsed.prompts.length / basePrompts.length) + 1; + const variantIdx = Math.floor(parsed.prompts.length / basePrompts.length); + const direction = variantDirections[variantIdx % variantDirections.length]; parsed.prompts.push({ - prompt: base.prompt + `(变体${variantIdx})`, + prompt: base.prompt + `(变体${variantIdx + 1},${direction})`, count: 1, use_last_outputs: base.use_last_outputs, use_attachments: base.use_attachments, @@ -18133,6 +18147,15 @@ async function sendAgentMessage(){ let messageText = text || '(please help me edit these images)'; // 前端数量决策:输入框显式要求 > 工具栏设置(软参数覆盖) const _finalCount = resolveFinalGenCount(text); + // 阶段二继承:如果当前没有明确请求多张,但上一条 user 消息有 requestedCount,继承它 + // 这确保阶段二(选风格后)和阶段一的数量一致 + if(_finalCount.count <= 1){ + const _prevUserMsg = [...(agentState.messages || [])].reverse().find(m => m.role === 'user'); + if(_prevUserMsg?.requestedCount > 1){ + _finalCount.count = _prevUserMsg.requestedCount; + _finalCount.source = 'inherited'; + } + } if(_finalCount.count > 1) userMsg.requestedCount = _finalCount.count; const _skills = Array.isArray(agentState?.skills) ? agentState.skills : []; if(_skills.length > 0){ @@ -18271,17 +18294,15 @@ async function _triggerGenerationsIfAllDone(assistantMsg){ if(msgs[i].role === 'user'){ userMsg = msgs[i]; break; } } // 构建 generations(透传 LLM 返回的 count/use_last_outputs/use_attachments) - if(!Array.isArray(assistantMsg.generations)) assistantMsg.generations = []; - confirmedPrompts.forEach(cp => { - assistantMsg.generations.push({ - prompt:cp.prompt, - count:cp.count || 1, - use_last_outputs:cp.use_last_outputs || false, - use_attachments:cp.use_attachments || false, - results:[], - status:'running' - }); - }); + // 用赋值而非 push,避免重复调用时 generations 重复追加 + assistantMsg.generations = confirmedPrompts.map(cp => ({ + prompt:cp.prompt, + count:cp.count || 1, + use_last_outputs:cp.use_last_outputs || false, + use_attachments:cp.use_attachments || false, + results:[], + status:'running' + })); // 统一生图(所有 confirmed prompts 一次性传入,整齐排列) await runAgentGenerations(assistantMsg, userMsg); } From 44d80e9a6371ac5112b9d8ee8408fc0ef6d03d64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Mon, 20 Jul 2026 11:35:42 +0800 Subject: [PATCH 50/67] =?UTF-8?q?fix:=20=E4=B8=80=E6=9D=A1=E9=BE=99?= =?UTF-8?q?=E7=AD=89=E5=9B=BA=E5=AE=9A=E8=AF=8D=E7=BB=84=E8=AF=AF=E5=88=A4?= =?UTF-8?q?=E6=95=B0=E9=87=8F=E4=B8=BA1+=E5=8A=A0=E5=BC=BA=E5=8F=98?= =?UTF-8?q?=E4=BD=93=E5=B7=AE=E5=BC=82=E8=A6=81=E6=B1=82=E5=87=8F=E5=B0=91?= =?UTF-8?q?=E7=94=9F=E5=9B=BE=E9=87=8D=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/js/smart-canvas.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/static/js/smart-canvas.js b/static/js/smart-canvas.js index 74d565265..5e3877f3e 100644 --- a/static/js/smart-canvas.js +++ b/static/js/smart-canvas.js @@ -17057,7 +17057,9 @@ function chatRequestedImageCount(text){ // 返回 {count, source},count 始终 >=1 function resolveFinalGenCount(text){ const fromInput = chatRequestedImageCount(text); - if(fromInput > 0) return {count: Math.min(8, fromInput), source:'input'}; + // 只有输入框明确请求 >1 张时才覆盖工具栏设置 + // 原因:数量1是默认值,且"一条龙"等固定词组会被误判为1 + if(fromInput > 1) return {count: Math.min(8, fromInput), source:'input'}; const toolbar = Math.max(1, Math.min(8, Number(agentState?.genCount) || 1)); return {count: toolbar, source:'toolbar'}; } @@ -17714,6 +17716,9 @@ When a skill document is provided, every prompt you generate MUST fully and verb 每条必须是完整的、可直接生图的中文提示词。 N条之间在主题方向/变体方向上有差异(不是换风格,而是在同一风格下做变体)。 例如用户选了"水墨风"+系统要3张 → 返回3条水墨风prompt,分别画不同姿态/场景/氛围的狗。 + 【重要】每条 prompt 必须在姿态、场景、构图、光线、配色中至少3个维度有实质差异。 + 不能只换颜色或微调细节,否则生成的图会几乎一样。 + 如果只返回1条让前端补充,补充的变体质量会远不如你直接写的。请务必返回恰好N条。 4. 每个 prompt 对象包含:prompt(中文提示词), count(固定为1), use_last_outputs(bool), use_attachments(bool)。 5. 用户会逐条确认(确认/修改/重新生成),全部确认后才统一生图。 6. 所有prompt必须中文,包含主体/风格/构图/光线/色彩/细节/氛围。 @@ -17726,6 +17731,8 @@ ${hasSkills ? '8. 每条 prompt 必须完整包含 skill 文档的所有描述 2. 系统要求生成N张图时(见上方"出图数量"),generations 数组必须返回恰好N条不同的 generation(每条 count=1),每条在主题/品牌方向上必须有明显差异。 例如系统要求3张 Logo 合集图,就返回3条 generation,分别用不同的品牌主题(如科技品牌、教育品牌、生活方式品牌)。 3. 不要返回2条或1条然后让前端补充——必须返回恰好N条,否则会导致重复生成。 + 【重要】每条 generation 的 prompt 必须在姿态、场景、构图、光线、配色中至少3个维度有实质差异。 + 不能只换颜色或微调细节,否则生成的图会几乎一样。 4. 每个 generation 对象包含:prompt(中文提示词), count(固定为1), use_last_outputs(bool), use_attachments(bool)。 ${hasSkills ? '5. 每条 generation 的 prompt 必须完整包含 skill 文档的所有描述内容,只在主题/品牌方向上做差异化(见上方"skill 完整保留"规则)。' : ''}`); } From 0d279ed708cd4b5fdd25f291ea7d2d76870689f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Mon, 20 Jul 2026 12:15:06 +0800 Subject: [PATCH 51/67] =?UTF-8?q?fix:=20=E7=94=9F=E5=9B=BE=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E7=BD=91=E7=BB=9C=E9=94=99=E8=AF=AF=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E9=87=8D=E8=AF=95=EF=BC=8C=E5=87=8F=E5=B0=91=E5=B9=B6=E5=8F=91?= =?UTF-8?q?=E6=97=B6=E9=97=B4=E6=AD=87=E6=80=A7=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index 71afa7848..d990776be 100755 --- a/main.py +++ b/main.py @@ -13212,7 +13212,20 @@ async def build_online_image_result(payload: OnlineImageRequest): image_refs = image_references(refs) count = max(1, min(8, int(payload.n or 1))) async def generate_one(): - image_data, raw_item = await generate_ai_image(payload.prompt, request_size, payload.quality, model, image_refs, provider["id"]) + # 网络错误自动重试(httpx.HTTPError 但非 HTTPStatusError) + # 解决高并发时间歇性网络中断导致"请求上游生图接口失败"但图实际已生成的问题 + max_retries = 2 + for attempt in range(max_retries + 1): + try: + image_data, raw_item = await generate_ai_image(payload.prompt, request_size, payload.quality, model, image_refs, provider["id"]) + break + except httpx.HTTPStatusError: + raise # HTTP 状态码错误不重试(如 400/401/403) + except httpx.HTTPError as exc: + if attempt + 1 <= max_retries: + await asyncio.sleep(1.5 * (attempt + 1)) + continue + raise try: image_items = extract_images(raw_item) if isinstance(raw_item, dict) else [image_data] except HTTPException: From 53a037085277d6aecf7d926c861623879394fa2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Mon, 20 Jul 2026 12:25:27 +0800 Subject: [PATCH 52/67] =?UTF-8?q?fix:=20=E5=8F=82=E8=80=83=E5=9B=BE?= =?UTF-8?q?=E5=8E=8B=E7=BC=A9=E5=88=B01024px=E5=86=8D=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=EF=BC=8C=E8=A7=A3=E5=86=B3=E5=A4=A7=E5=9B=BE=E5=AF=BC=E8=87=B4?= =?UTF-8?q?413=20Request=20Entity=20Too=20Large?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.py | 43 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/main.py b/main.py index d990776be..f22b234d2 100755 --- a/main.py +++ b/main.py @@ -4928,7 +4928,13 @@ async def codex_reference_paths(reference_images=None): continue path, created = await codex_prepare_local_media(url) if path: - paths.append(path) + # 压缩大图避免 CLI 传输超时 + compressed = compress_reference_image(path, max_size=1024) + if compressed: + paths.append(compressed) + temp_paths.append(compressed) + else: + paths.append(path) temp_paths.extend(created) return paths, temp_paths except Exception: @@ -7573,6 +7579,26 @@ def convert_output_to_jpg(url, quality=88): print(f"转换 JPG 失败: {e}") return url +def compress_reference_image(path, max_size=1024): + """压缩参考图到最长边 max_size 像素,返回临时文件路径。如果原图已足够小则返回 None。""" + try: + with Image.open(path) as img: + img.load() + w, h = img.size + if max(w, h) <= max_size: + return None # 原图足够小,不需要压缩 + img.thumbnail((max_size, max_size), Image.LANCZOS) + if img.mode not in ("RGB", "RGBA"): + img = img.convert("RGB") + fmt = "PNG" if img.mode == "RGBA" else "JPEG" + fd, tmp_path = tempfile.mkstemp(prefix="ref_comp_", suffix=f".{fmt.lower()}") + with os.fdopen(fd, "wb") as f: + img.save(f, format=fmt, quality=88 if fmt == "JPEG" else None) + return tmp_path + except Exception as e: + print(f"compress_reference_image failed, using original: {e}") + return None + def reference_to_data_url(ref, max_size=None): """把本地输出文件转为 data URL(base64)。max_size 限制最长边像素,避免 payload 过大。""" path = output_file_from_url(ref.get("url", "")) @@ -10679,6 +10705,7 @@ async def post_openai_edits(edit_files=None): # GPT-Image-2 参考图不能走 /images/generations JSON,否则部分平台会忽略原图或报 Images API unsupported。 files = [] opened = [] + compressed_paths = [] edit_failed_status = None edit_failed_text = "" try: @@ -10686,9 +10713,13 @@ async def post_openai_edits(edit_files=None): path = output_file_from_url(ref.get("url", "")) if not path: continue - fh = open(path, "rb") + # 压缩缩放参考图到最长边1024px,避免大文件导致 413 Request Entity Too Large + compressed_path = compress_reference_image(path, max_size=1024) + if compressed_path: + compressed_paths.append(compressed_path) + fh = open(compressed_path or path, "rb") opened.append(fh) - files.append(("image", (os.path.basename(path), fh, content_type_for_path(path)))) + files.append(("image", (os.path.basename(compressed_path or path), fh, content_type_for_path(compressed_path or path)))) if mask_refs: mask_path = output_file_from_url(mask_refs[0].get("url", "")) if mask_path: @@ -10708,6 +10739,12 @@ async def post_openai_edits(edit_files=None): finally: for fh in opened: fh.close() + # 清理压缩临时文件 + for cp in compressed_paths: + try: + os.remove(cp) + except Exception: + pass # 2) edits 失败 → 非 GPT-Image-2 可回退到 /images/generations + JSON image:[urls/base64](grsai 风格) if response is None: if is_gpt2: From c88ceab57aff9035b9e77108edc971d6d0308159 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Mon, 20 Jul 2026 12:39:01 +0800 Subject: [PATCH 53/67] =?UTF-8?q?feat:=20=E5=90=AF=E5=8A=A8=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E5=90=8E=E8=87=AA=E5=8A=A8=E6=89=93=E5=BC=80=E6=B5=8F?= =?UTF-8?q?=E8=A7=88=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "mac-\345\220\257\345\212\250\346\234\215\345\212\241.command" | 3 +++ 1 file changed, 3 insertions(+) diff --git "a/mac-\345\220\257\345\212\250\346\234\215\345\212\241.command" "b/mac-\345\220\257\345\212\250\346\234\215\345\212\241.command" index 0729c5e72..23b3971c8 100755 --- "a/mac-\345\220\257\345\212\250\346\234\215\345\212\241.command" +++ "b/mac-\345\220\257\345\212\250\346\234\215\345\212\241.command" @@ -58,6 +58,9 @@ echo "本机访问: http://127.0.0.1:3000/" echo "============================================" echo "" +# 后台延迟3秒自动打开浏览器(等服务器启动完成) +(sleep 3 && open "http://127.0.0.1:3000/") & + # 优先使用 Homebrew Python,避免部分工具管理的 Python 签名问题 if [ -x /opt/homebrew/bin/python3 ]; then /opt/homebrew/bin/python3 main.py From 2929593db13d66892517c79b358614bbee73a215 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Mon, 20 Jul 2026 14:36:39 +0800 Subject: [PATCH 54/67] =?UTF-8?q?fix:=20=E5=A4=9A=E5=8F=82=E8=80=83?= =?UTF-8?q?=E5=9B=BE=E7=94=9F=E5=9B=BE=E5=A4=B1=E8=B4=A5=E2=80=94=E2=80=94?= =?UTF-8?q?=E5=8E=8B=E7=BC=A9=E5=88=B0768px+=E9=99=90=E5=88=B66=E5=BC=A0+5?= =?UTF-8?q?03/429=E8=87=AA=E5=8A=A8=E9=87=8D=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.py | 66 +++++++++++------------------------- static/angle.html | 18 +++++----- static/api-settings.html | 14 ++++---- static/asset-manager.html | 10 +++--- static/canvas-list.html | 16 ++++----- static/canvas.html | 18 +++++----- static/comfyui-settings.html | 14 ++++---- static/enhance.html | 16 ++++----- static/gpt-chat.html | 12 +++---- static/index.html | 28 +++++++-------- static/klein.html | 16 ++++----- static/online.html | 16 ++++----- static/smart-canvas.html | 12 +++---- static/zimage.html | 14 ++++---- 14 files changed, 121 insertions(+), 149 deletions(-) diff --git a/main.py b/main.py index f22b234d2..770bb3410 100755 --- a/main.py +++ b/main.py @@ -4928,13 +4928,7 @@ async def codex_reference_paths(reference_images=None): continue path, created = await codex_prepare_local_media(url) if path: - # 压缩大图避免 CLI 传输超时 - compressed = compress_reference_image(path, max_size=1024) - if compressed: - paths.append(compressed) - temp_paths.append(compressed) - else: - paths.append(path) + paths.append(path) temp_paths.extend(created) return paths, temp_paths except Exception: @@ -7579,26 +7573,6 @@ def convert_output_to_jpg(url, quality=88): print(f"转换 JPG 失败: {e}") return url -def compress_reference_image(path, max_size=1024): - """压缩参考图到最长边 max_size 像素,返回临时文件路径。如果原图已足够小则返回 None。""" - try: - with Image.open(path) as img: - img.load() - w, h = img.size - if max(w, h) <= max_size: - return None # 原图足够小,不需要压缩 - img.thumbnail((max_size, max_size), Image.LANCZOS) - if img.mode not in ("RGB", "RGBA"): - img = img.convert("RGB") - fmt = "PNG" if img.mode == "RGBA" else "JPEG" - fd, tmp_path = tempfile.mkstemp(prefix="ref_comp_", suffix=f".{fmt.lower()}") - with os.fdopen(fd, "wb") as f: - img.save(f, format=fmt, quality=88 if fmt == "JPEG" else None) - return tmp_path - except Exception as e: - print(f"compress_reference_image failed, using original: {e}") - return None - def reference_to_data_url(ref, max_size=None): """把本地输出文件转为 data URL(base64)。max_size 限制最长边像素,避免 payload 过大。""" path = output_file_from_url(ref.get("url", "")) @@ -10670,12 +10644,16 @@ async def post_openai_edits(edit_files=None): responses_url = provider_endpoint_url(provider, "image_generation_endpoint", "/v1/responses") response = await post_openai_responses(client, responses_url, api_headers(provider=provider, model=model), body) elif image_request_mode == "openai-json": - # Agnes 等“OpenAI JSON 图片接口”统一走 /images/generations: + # Agnes 等"OpenAI JSON 图片接口"统一走 /images/generations: # 不使用 /images/edits,不传顶层 response_format/n/quality; # 文生图只传 extra_body.response_format,图生图把参考图放进 extra_body.image。 + # 参考图压缩到 768px:多张参考图 base64 后 payload 会急剧膨胀(8张1536px≈8.6MB), + # 降为 768px 可缩减到 ~2MB,避免上游 "image queue is full" 503 错误。 + # 上游限制最多 6 张参考图(too many input images: at most 6 allowed)。 + OPENAI_JSON_REF_MAX = 6 extra_body = {"response_format": "url"} if image_refs: - extra_body["image"] = [reference_to_data_url(ref, max_size=1536) for ref in image_refs[:ONLINE_IMAGE_REFERENCE_MAX]] + extra_body["image"] = [reference_to_data_url(ref, max_size=768) for ref in image_refs[:OPENAI_JSON_REF_MAX]] body = {"model": model, "prompt": prompt, "size": size, "extra_body": extra_body} response = await client.post(gen_url, headers=api_headers(provider=provider, model=model), json=body) elif is_apimart: @@ -10691,7 +10669,7 @@ async def post_openai_edits(edit_files=None): "official_fallback": False, } if image_refs: - body["image_urls"] = [reference_to_data_url(ref, max_size=1536) for ref in image_refs[:ONLINE_IMAGE_REFERENCE_MAX]] + body["image_urls"] = [reference_to_data_url(ref, max_size=768) for ref in image_refs[:ONLINE_IMAGE_REFERENCE_MAX]] response = await client.post(gen_url, headers=api_headers(provider=provider, model=model), json=body) elif is_gpt2 and not image_refs and not mask_refs: body = {"model": model, "prompt": prompt, "size": size} @@ -10705,7 +10683,6 @@ async def post_openai_edits(edit_files=None): # GPT-Image-2 参考图不能走 /images/generations JSON,否则部分平台会忽略原图或报 Images API unsupported。 files = [] opened = [] - compressed_paths = [] edit_failed_status = None edit_failed_text = "" try: @@ -10713,13 +10690,9 @@ async def post_openai_edits(edit_files=None): path = output_file_from_url(ref.get("url", "")) if not path: continue - # 压缩缩放参考图到最长边1024px,避免大文件导致 413 Request Entity Too Large - compressed_path = compress_reference_image(path, max_size=1024) - if compressed_path: - compressed_paths.append(compressed_path) - fh = open(compressed_path or path, "rb") + fh = open(path, "rb") opened.append(fh) - files.append(("image", (os.path.basename(compressed_path or path), fh, content_type_for_path(compressed_path or path)))) + files.append(("image", (os.path.basename(path), fh, content_type_for_path(path)))) if mask_refs: mask_path = output_file_from_url(mask_refs[0].get("url", "")) if mask_path: @@ -10739,12 +10712,6 @@ async def post_openai_edits(edit_files=None): finally: for fh in opened: fh.close() - # 清理压缩临时文件 - for cp in compressed_paths: - try: - os.remove(cp) - except Exception: - pass # 2) edits 失败 → 非 GPT-Image-2 可回退到 /images/generations + JSON image:[urls/base64](grsai 风格) if response is None: if is_gpt2: @@ -10753,7 +10720,7 @@ async def post_openai_edits(edit_files=None): detail=f"GPT-Image-2 编辑接口 /images/edits 调用失败:{edit_failed_text[:300] or edit_failed_status}。已停止自动重试,避免上游可能已扣费后再次请求。" ) print(f"/images/edits failed ({edit_failed_status}): {edit_failed_text[:200]} → 回退到 /images/generations + image:[] JSON") - image_payload = [reference_to_data_url(ref, max_size=1536) for ref in image_refs[:ONLINE_IMAGE_REFERENCE_MAX]] + image_payload = [reference_to_data_url(ref, max_size=768) for ref in image_refs[:ONLINE_IMAGE_REFERENCE_MAX]] body = { "model": model, "prompt": prompt, "size": size, "response_format": "url", "n": 1, @@ -13251,15 +13218,20 @@ async def build_online_image_result(payload: OnlineImageRequest): async def generate_one(): # 网络错误自动重试(httpx.HTTPError 但非 HTTPStatusError) # 解决高并发时间歇性网络中断导致"请求上游生图接口失败"但图实际已生成的问题 + # 同时对 503(queue is full)和 429(rate limit)等临时性错误也重试 max_retries = 2 for attempt in range(max_retries + 1): try: image_data, raw_item = await generate_ai_image(payload.prompt, request_size, payload.quality, model, image_refs, provider["id"]) break - except httpx.HTTPStatusError: - raise # HTTP 状态码错误不重试(如 400/401/403) + except httpx.HTTPStatusError as exc: + # 503(队列满/服务暂不可用)和 429(限流)是临时性错误,重试可解决 + if exc.response.status_code in (429, 503) and attempt < max_retries: + await asyncio.sleep(2.0 * (attempt + 1)) + continue + raise except httpx.HTTPError as exc: - if attempt + 1 <= max_retries: + if attempt < max_retries: await asyncio.sleep(1.5 * (attempt + 1)) continue raise diff --git a/static/angle.html b/static/angle.html index 55cd95199..961619a03 100644 --- a/static/angle.html +++ b/static/angle.html @@ -17,21 +17,21 @@ } catch(e) {} })(); - - - - - - + + + + + + - + diff --git a/static/api-settings.html b/static/api-settings.html index 5cd26f7cf..ebf8eeba1 100644 --- a/static/api-settings.html +++ b/static/api-settings.html @@ -16,12 +16,12 @@ } catch(e) {} })(); - - - - - - + + + + + +
@@ -565,6 +565,6 @@
- + diff --git a/static/asset-manager.html b/static/asset-manager.html index d8cd066fa..a66cc7ee7 100644 --- a/static/asset-manager.html +++ b/static/asset-manager.html @@ -16,10 +16,10 @@ } catch(e) {} })(); - - - - + + + +
@@ -47,6 +47,6 @@
- + diff --git a/static/canvas-list.html b/static/canvas-list.html index 7106f2c7c..884849589 100644 --- a/static/canvas-list.html +++ b/static/canvas-list.html @@ -16,13 +16,13 @@ } catch(e) {} })(); - - - - - - - + + + + + + +
@@ -90,6 +90,6 @@
- + diff --git a/static/canvas.html b/static/canvas.html index fa1e84e56..c39a9aaf7 100644 --- a/static/canvas.html +++ b/static/canvas.html @@ -16,13 +16,13 @@ } catch(e) {} })(); - - - - - - - + + + + + + +
@@ -350,7 +350,7 @@
- - + + diff --git a/static/comfyui-settings.html b/static/comfyui-settings.html index e13a5c8dd..0495a0f70 100644 --- a/static/comfyui-settings.html +++ b/static/comfyui-settings.html @@ -15,12 +15,12 @@ } catch(e) {} })(); - - - - - - + + + + + +
@@ -122,6 +122,6 @@ preview
- + diff --git a/static/enhance.html b/static/enhance.html index b249b7955..29cd8eb5a 100644 --- a/static/enhance.html +++ b/static/enhance.html @@ -17,14 +17,14 @@ } catch(e) {} })(); - - - - - - + + + + + + - + diff --git a/static/gpt-chat.html b/static/gpt-chat.html index ca542caa0..c0f88b843 100644 --- a/static/gpt-chat.html +++ b/static/gpt-chat.html @@ -16,13 +16,13 @@ } catch(e) {} })(); - - - - - + + + + + - + diff --git a/static/online.html b/static/online.html index 16cf60741..1491f14ec 100644 --- a/static/online.html +++ b/static/online.html @@ -16,14 +16,14 @@ } catch(e) {} })(); - - - - - - + + + + + + - + - + From ac82edb0bfb5c9e39ffc3cad149508bfb1510f34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Mon, 20 Jul 2026 15:45:36 +0800 Subject: [PATCH 55/67] =?UTF-8?q?feat:=20provider=E5=8F=82=E8=80=83?= =?UTF-8?q?=E5=9B=BE=E4=B8=8A=E9=99=90=E6=8F=90=E7=A4=BA+LLM=E5=8F=82?= =?UTF-8?q?=E8=80=83=E5=9B=BE=E5=88=86=E6=9E=90=E4=BC=98=E5=8C=96+?= =?UTF-8?q?=E7=94=9F=E5=9B=BE=E6=88=AA=E5=8F=96=E5=85=9C=E5=BA=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.py | 39 +++++++++++++++++++++++---------------- static/js/smart-canvas.js | 37 +++++++++++++++++++++++++++++++------ static/smart-canvas.html | 2 +- 3 files changed, 55 insertions(+), 23 deletions(-) diff --git a/main.py b/main.py index 770bb3410..f982fda33 100755 --- a/main.py +++ b/main.py @@ -634,6 +634,17 @@ def load_env_file(): CHAT_ATTACHMENT_MAX = int(os.getenv("CHAT_ATTACHMENT_MAX", "20")) ONLINE_IMAGE_REFERENCE_MAX = int(os.getenv("ONLINE_IMAGE_REFERENCE_MAX", "20")) +def provider_max_reference_images(provider): + """返回当前 provider 的生图参考图上限。 + agnes-ai (openai-json): 6 张(上游硬限制) + 其他 provider: ONLINE_IMAGE_REFERENCE_MAX(20 张)""" + if not provider: + return ONLINE_IMAGE_REFERENCE_MAX + image_request_mode = effective_image_request_mode(provider, "") + if image_request_mode == "openai-json": + return 6 + return ONLINE_IMAGE_REFERENCE_MAX + FIELD_LABELS = { "prompt": "提示词", "message": "文本", @@ -1367,6 +1378,7 @@ def public_provider(provider): "has_key": bool(key), "key_preview": mask_secret(key), "key_env": provider_key_env(provider["id"]), + "max_reference_images": provider_max_reference_images(provider), } if provider.get("id") == "runninghub": wallet_key = runninghub_wallet_key_value() @@ -10644,16 +10656,12 @@ async def post_openai_edits(edit_files=None): responses_url = provider_endpoint_url(provider, "image_generation_endpoint", "/v1/responses") response = await post_openai_responses(client, responses_url, api_headers(provider=provider, model=model), body) elif image_request_mode == "openai-json": - # Agnes 等"OpenAI JSON 图片接口"统一走 /images/generations: + # Agnes 等“OpenAI JSON 图片接口”统一走 /images/generations: # 不使用 /images/edits,不传顶层 response_format/n/quality; # 文生图只传 extra_body.response_format,图生图把参考图放进 extra_body.image。 - # 参考图压缩到 768px:多张参考图 base64 后 payload 会急剧膨胀(8张1536px≈8.6MB), - # 降为 768px 可缩减到 ~2MB,避免上游 "image queue is full" 503 错误。 - # 上游限制最多 6 张参考图(too many input images: at most 6 allowed)。 - OPENAI_JSON_REF_MAX = 6 extra_body = {"response_format": "url"} if image_refs: - extra_body["image"] = [reference_to_data_url(ref, max_size=768) for ref in image_refs[:OPENAI_JSON_REF_MAX]] + extra_body["image"] = [reference_to_data_url(ref, max_size=1536) for ref in image_refs[:ONLINE_IMAGE_REFERENCE_MAX]] body = {"model": model, "prompt": prompt, "size": size, "extra_body": extra_body} response = await client.post(gen_url, headers=api_headers(provider=provider, model=model), json=body) elif is_apimart: @@ -10669,7 +10677,7 @@ async def post_openai_edits(edit_files=None): "official_fallback": False, } if image_refs: - body["image_urls"] = [reference_to_data_url(ref, max_size=768) for ref in image_refs[:ONLINE_IMAGE_REFERENCE_MAX]] + body["image_urls"] = [reference_to_data_url(ref, max_size=1536) for ref in image_refs[:ONLINE_IMAGE_REFERENCE_MAX]] response = await client.post(gen_url, headers=api_headers(provider=provider, model=model), json=body) elif is_gpt2 and not image_refs and not mask_refs: body = {"model": model, "prompt": prompt, "size": size} @@ -10720,7 +10728,7 @@ async def post_openai_edits(edit_files=None): detail=f"GPT-Image-2 编辑接口 /images/edits 调用失败:{edit_failed_text[:300] or edit_failed_status}。已停止自动重试,避免上游可能已扣费后再次请求。" ) print(f"/images/edits failed ({edit_failed_status}): {edit_failed_text[:200]} → 回退到 /images/generations + image:[] JSON") - image_payload = [reference_to_data_url(ref, max_size=768) for ref in image_refs[:ONLINE_IMAGE_REFERENCE_MAX]] + image_payload = [reference_to_data_url(ref, max_size=1536) for ref in image_refs[:ONLINE_IMAGE_REFERENCE_MAX]] body = { "model": model, "prompt": prompt, "size": size, "response_format": "url", "n": 1, @@ -13214,24 +13222,23 @@ async def build_online_image_result(payload: OnlineImageRequest): request_size = snap_size_to_multiple(payload.size, 16) refs = [ref.dict() for ref in payload.reference_images if ref.url] image_refs = image_references(refs) + _max_refs = provider_max_reference_images(provider) + if len(image_refs) > _max_refs: + print(f"[ref-limit] provider={provider.get('id')} refs={len(image_refs)}>max={_max_refs}, truncating") + image_refs = image_refs[:_max_refs] count = max(1, min(8, int(payload.n or 1))) async def generate_one(): # 网络错误自动重试(httpx.HTTPError 但非 HTTPStatusError) # 解决高并发时间歇性网络中断导致"请求上游生图接口失败"但图实际已生成的问题 - # 同时对 503(queue is full)和 429(rate limit)等临时性错误也重试 max_retries = 2 for attempt in range(max_retries + 1): try: image_data, raw_item = await generate_ai_image(payload.prompt, request_size, payload.quality, model, image_refs, provider["id"]) break - except httpx.HTTPStatusError as exc: - # 503(队列满/服务暂不可用)和 429(限流)是临时性错误,重试可解决 - if exc.response.status_code in (429, 503) and attempt < max_retries: - await asyncio.sleep(2.0 * (attempt + 1)) - continue - raise + except httpx.HTTPStatusError: + raise # HTTP 状态码错误不重试(如 400/401/403) except httpx.HTTPError as exc: - if attempt < max_retries: + if attempt + 1 <= max_retries: await asyncio.sleep(1.5 * (attempt + 1)) continue raise diff --git a/static/js/smart-canvas.js b/static/js/smart-canvas.js index 5e3877f3e..a2da2e560 100644 --- a/static/js/smart-canvas.js +++ b/static/js/smart-canvas.js @@ -18,6 +18,10 @@ const apiKindToggle = document.getElementById('apiKindToggle'); const inputThumbsRow = document.getElementById('inputThumbsRow'); const SMART_UPLOAD_MAX = 20; const SMART_REFERENCE_IMAGE_MAX = 20; +function providerMaxReferenceImages(providerId){ + const p = apiProviderById(providerId); + return Number(p?.max_reference_images) > 0 ? Number(p.max_reference_images) : SMART_REFERENCE_IMAGE_MAX; +} const inputPromptPreview = document.getElementById('inputPromptPreview'); const minimap = document.getElementById('minimap'); const minimapContent = document.getElementById('minimapContent'); @@ -7304,13 +7308,18 @@ function runSmartNodeToolbarAction(nodeId, action){ if(!agentOpen) toggleAgentPanel(true); if(agentState){ if(!Array.isArray(agentState.attachments)) agentState.attachments = []; - if(agentState.attachments.length < AGENT_LLM_IMAGE_MAX && !agentState.attachments.some(a => a.url === item.url)){ + const _genProv = agentGenProviders().some(p => p.id === agentState.genProvider) ? agentState.genProvider : (agentGenProviders()[0]?.id || ''); + const _refMax = _genProv ? providerMaxReferenceImages(_genProv) : AGENT_LLM_IMAGE_MAX; + if(agentState.attachments.length < _refMax && !agentState.attachments.some(a => a.url === item.url)){ agentState.attachments.push({url:item.url, name:item.name || node.title || 'image', nodeId:node.id, x:Number(node.x) || 0, y:Number(node.y) || 0}); renderAgentAttachments(); saveAgentState(); toast('已发送至 Agent'); + } else if(agentState.attachments.length >= _refMax){ + const _pName = apiProviderById(_genProv)?.name || _genProv; + toast(`当前生图平台 ${_pName} 最多支持 ${_refMax} 张参考图,已达上限`); } else { - toast('附件已存在或已达上限'); + toast('附件已存在'); } } return; @@ -14717,7 +14726,8 @@ function comfyFieldKind(field){ async function runApiGeneration(prompt, refs, runSettings=settings){ if(!runSettings.provider_id || !runSettings.model) throw new Error(tr('smart.errNoApiModel')); const count = Math.max(1, Math.min(8, Number(runSettings.count || 1))); - const payload = {prompt, provider_id:runSettings.provider_id, model:runSettings.model, size:sizeForRun(runSettings), quality:runSettings.quality || 'auto', n:1, reference_images:imageRefsOnly(refs).slice(0, SMART_REFERENCE_IMAGE_MAX)}; + const _refMax = providerMaxReferenceImages(runSettings.provider_id); + const payload = {prompt, provider_id:runSettings.provider_id, model:runSettings.model, size:sizeForRun(runSettings), quality:runSettings.quality || 'auto', n:1, reference_images:imageRefsOnly(refs).slice(0, _refMax)}; const tasks = await Promise.all(Array.from({length:count}, () => fetch('/api/canvas-image-tasks', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload)}).then(async r => { if(!r.ok) throw new Error(await r.text()); return r.json(); @@ -17258,10 +17268,24 @@ async function agentAttachFiles(files){ skillFiles.forEach(f => setAgentSkillFile(f)); if(!imageFiles.length) return; if(!Array.isArray(agentState.attachments)) agentState.attachments = []; + // 检查生图 provider 的参考图上限 + const _genProvider = agentGenProviders().some(p => p.id === agentState.genProvider) ? agentState.genProvider : (agentGenProviders()[0]?.id || ''); + const _refMax = _genProvider ? providerMaxReferenceImages(_genProvider) : AGENT_LLM_IMAGE_MAX; + const _currentCount = agentState.attachments.length; + const _available = Math.max(0, _refMax - _currentCount); + if(_available <= 0){ + const _pName = apiProviderById(_genProvider)?.name || _genProvider; + toast(`当前生图平台 ${_pName} 最多支持 ${_refMax} 张参考图,已达上限`); + return; + } + if(imageFiles.length > _available){ + const _pName = apiProviderById(_genProvider)?.name || _genProvider; + toast(`当前生图平台 ${_pName} 最多支持 ${_refMax} 张参考图,仅添加前 ${_available} 张`); + } try { const uploaded = await uploadFiles(imageFiles); (uploaded || []).filter(f => f?.url).forEach(f => { - if(agentState.attachments.length < AGENT_LLM_IMAGE_MAX) agentState.attachments.push({url:f.url, name:f.name || 'image'}); + if(agentState.attachments.length < _refMax) agentState.attachments.push({url:f.url, name:f.name || 'image'}); }); renderAgentAttachments(); saveAgentState(); @@ -18539,7 +18563,7 @@ async function runAgentGenerations(assistantMsg, userMsg){ let refsForBox = []; if(gen.use_last_outputs) refsForBox = refsForBox.concat(lastResults); if(gen.use_attachments) refsForBox = refsForBox.concat(attachRefs); - refsForBox = imageRefsOnly(refsForBox).slice(0, SMART_REFERENCE_IMAGE_MAX); + refsForBox = imageRefsOnly(refsForBox).slice(0, providerMaxReferenceImages(providerId)); const pendingBox = agentPendingBoxSize(gen.count, {refs: refsForBox}); placeholderNode.w = pendingBox.w; placeholderNode.h = pendingBox.h; @@ -18561,7 +18585,8 @@ async function runAgentGenerations(assistantMsg, userMsg){ let refs = []; if(gen.use_last_outputs) refs = refs.concat(lastResults); if(gen.use_attachments) refs = refs.concat(attachRefs); - refs = imageRefsOnly(refs).slice(0, SMART_REFERENCE_IMAGE_MAX).map(r => ({url:r.url, name:r.name || 'ref'})); + const _agentRefMax = providerMaxReferenceImages(providerId); + refs = imageRefsOnly(refs).slice(0, _agentRefMax).map(r => ({url:r.url, name:r.name || 'ref'})); const payload = {prompt:gen.prompt, provider_id:providerId, model:genModel, size, quality:agentState.genQuality || 'auto', n:1, reference_images:refs}; const tasks = await Promise.all(Array.from({length:gen.count}, () => fetch('/api/canvas-image-tasks', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload)}).then(async r => { if(!r.ok) throw new Error(await responseErrorMessage(r, tr('smart.agentGenFail'))); diff --git a/static/smart-canvas.html b/static/smart-canvas.html index 5fe8d2761..1bcc0d40c 100644 --- a/static/smart-canvas.html +++ b/static/smart-canvas.html @@ -559,6 +559,6 @@
preview
- + From e2f4be1b2f6117dde8c41adf1535d816ca283315 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Mon, 20 Jul 2026 18:10:35 +0800 Subject: [PATCH 56/67] =?UTF-8?q?v1.6:=20LLM=E4=B8=8E=E7=94=9F=E5=9B=BE?= =?UTF-8?q?=E8=A7=A3=E8=80=A6=E3=80=81=E6=80=9D=E7=BB=B4=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E5=A4=9A=E8=BD=AE=E7=BB=B4=E5=BA=A6=E9=87=87=E9=9B=86=E3=80=81?= =?UTF-8?q?=E6=89=B9=E9=87=8F=E5=8F=91=E9=80=81=E8=87=B3Agent=E3=80=81?= =?UTF-8?q?=E9=99=84=E4=BB=B6=E6=8B=96=E6=8B=BD=E6=8E=92=E5=BA=8F=E3=80=81?= =?UTF-8?q?=E5=8F=91=E9=80=81=E6=8C=89=E9=92=AEbug=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 15 ++ README.md | 3 +- VERSION | 2 +- static/css/smart-canvas.css | 5 + static/js/smart-canvas.js | 445 +++++++++++++++++++++++++++++------- static/smart-canvas.html | 25 +- 6 files changed, 395 insertions(+), 100 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f3470d42..09aadea8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,3 +56,18 @@ - **API 单次返回多图修复**:某些 provider 单次生图调用会返回多张图,导致单个 generation 节点出现多张图。现在限制每个 generation 最多只取 `gen.count` 张图 - **参考图 URL 过滤**:某些 provider 会在响应中回显输入的参考图 URL,造成「重复」问题。现在过滤掉与参考图 URL 相同的结果 - **count=1 强制范围扩大**:直接模式下无论 `requestedCount` 是否大于 1,都强制所有 generation 的 `count=1`(之前只在 `requestedCount > 1` 时才强制,存在漏洞) + +#### v1.6 — LLM 与生图解耦 + 思维模式多轮维度采集 +- **LLM 与生图解耦**:思维模式 OFF 时前端跳过 LLM,直接构建 generations 并调用生图 API,降低延迟与成本 +- **思维模式多轮维度采集**:从「两阶段流程」重构为「渐进式多维采集」——逐轮提问风格/场景/构图/配色/细节等维度,每轮返回选项 + 自定义输入,所有维度确认后生成最终提示词 +- **参考图分析规则**:思维模式下 LLM 先分析参考图共同特征,再让用户选择保留哪些特征 +- **LLM 模型选择移入思维模式面板**:模型选择栏只保留生图模型,理解模型选择整合到思维模式按钮的下拉面板中(点击思维按钮即展开) +- **框选批量发送至 Agent**:画布框选图片节点后,底部居中显示「发送至Agent(x张)」按钮,一键批量添加为参考图 +- **附件拖拽排序**:Agent 聊天框中的参考图附件支持拖拽调整顺序 +- **发送按钮 bug 修复**: + - 移除 `agentThinkingModelBtn` 的 CSS `display:inline-flex` 覆盖 `hidden` 属性导致按钮挤压发送按钮的问题 + - `agentSendBtn` 事件监听提前到 `initAgentPanel` 首行,防止中间初始化异常导致监听未注册 + - 思维模式 OFF 路径补充 `agentSending` 状态管理(`true` → `finally false`),防止按钮卡在 disabled + - 页面加载时 `agentSending` 安全重置 +- **`parseAgentResponse` 防御性修复**:所有返回路径补全 `options`/`prompts` 字段,`processAgentLlmResult` 开头添加数组类型检查,消除 "Cannot read properties of undefined (reading 'length')" 错误 +- **点击空白处清除批量发送按钮**:`shell.onclick` 补充 `syncSelectionUi()` 调用 diff --git a/README.md b/README.md index 1b35128be..0a654a388 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Supports comfyui/API calls/modelscope calls > 本仓库是基于原项目 [hero8152/Infinite-Canvas](https://github.com/hero8152/Infinite-Canvas) 的二次开发 fork,新增了智能画布 AI Agent 面板。 > -> **当前版本:v1.5**(查看 [版本更新大纲](./CHANGELOG.md)) +> **当前版本:v1.6**(查看 [版本更新大纲](./CHANGELOG.md)) ### 重要提示 - **已关闭自动更新**:导航栏版本号显示为「Agent版(无自动更新)」。若从上游拉取/合并最新代码,可能覆盖本分支的 AI Agent 功能,请谨慎操作并先备份。 @@ -27,6 +27,7 @@ Supports comfyui/API calls/modelscope calls | v1.4 | 2026-07-19 | 多图确认流程重构(全部确认后统一生图)、prompts 状态机、内联编辑、Skill 完整保留(首因近因双语)、思维模式兜底 bug 修复 | | v1.5 | 2026-07-19 | 硬软参数分层(数量=软参数输入框>工具栏,比例/分辨率=硬参数工具栏)、Skill=单张图样式不决定数量、统一数量决策函数 | | v1.5.1 | 2026-07-19 | 单 generation 多图泄漏修复(API 返回多图限制+参考图 URL 过滤)、count=1 强制范围扩大到所有直接模式 | +| v1.6 | 2026-07-20 | LLM 与生图解耦(思维模式 OFF 跳过 LLM 直接生图)、思维模式多轮维度采集(渐进式问答+自定义输入)、LLM 模型选择移入思维模式面板、框选批量发送至 Agent、附件拖拽排序、发送按钮 bug 修复 | ---- diff --git a/VERSION b/VERSION index 1ce69a78a..1d81d23b2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v1.5 +v1.6 diff --git a/static/css/smart-canvas.css b/static/css/smart-canvas.css index 523f68e8d..bb97bc04c 100644 --- a/static/css/smart-canvas.css +++ b/static/css/smart-canvas.css @@ -1039,6 +1039,8 @@ input[type=range].smart-range::-webkit-slider-thumb { -webkit-appearance:none; w .minimap-arrange-btn.visible { opacity:1; pointer-events:auto; } .minimap-arrange-btn:hover { color:var(--strong); background:var(--panel); transform:translateY(-1px); } .minimap-arrange-btn i,.minimap-arrange-btn svg { width:14px; height:14px; } +#smartSendAgentBtn { left:50%; right:auto; bottom:22px; transform:translateX(-50%); } +#smartSendAgentBtn:hover { transform:translateX(-50%) translateY(-1px); } .smart-minimap-content { position:absolute; inset:10px; border-radius:12px; overflow:hidden; background-image:radial-gradient(var(--grid) 1px, transparent 1px); background-size:12px 12px; opacity:.95; } .minimap-node { position:absolute; min-width:4px; min-height:4px; border-radius:3px; background:var(--strong); opacity:.8; } .smart-minimap-viewport { position:absolute; border-radius:8px; border:2px solid var(--strong); background:rgba(255,255,255,.12); } @@ -1325,6 +1327,9 @@ input[type=range].smart-range::-webkit-slider-thumb { -webkit-appearance:none; w .agent-attach-chip img { width:100%; height:100%; object-fit:cover; display:block; } .agent-attach-chip button { position:absolute; top:2px; right:2px; width:15px; height:15px; border-radius:50%; background:rgba(15,23,42,.78); color:#fff; display:flex; align-items:center; justify-content:center; border:none; cursor:pointer; padding:0; } .agent-attach-chip button i,.agent-attach-chip button svg { width:9px; height:9px; } +.agent-attach-chip.dragging { opacity:.4; } +.agent-attach-chip.drop-before { box-shadow:-2px 0 0 0 var(--strong); } +.agent-attach-chip.drop-after { box-shadow:2px 0 0 0 var(--strong); } .agent-onebox textarea { display:block; width:100%; min-width:0; resize:none; max-height:200px; border:none; background:transparent; color:var(--text); padding:8px 12px; font-size:11.5px; line-height:1.5; outline:none; font-family:inherit; } .agent-toolbar { display:flex; align-items:center; gap:4px; padding:4px 8px 6px; } .agent-toolbar-icon { width:28px; height:28px; border-radius:8px; display:inline-flex; align-items:center; justify-content:center; border:none; background:none; color:var(--muted); cursor:pointer; transition:color .14s ease, background .14s ease; } diff --git a/static/js/smart-canvas.js b/static/js/smart-canvas.js index a2da2e560..800da6797 100644 --- a/static/js/smart-canvas.js +++ b/static/js/smart-canvas.js @@ -26,6 +26,7 @@ const inputPromptPreview = document.getElementById('inputPromptPreview'); const minimap = document.getElementById('minimap'); const minimapContent = document.getElementById('minimapContent'); const smartArrangeBtn = document.getElementById('smartArrangeBtn'); +const smartSendAgentBtn = document.getElementById('smartSendAgentBtn'); const imageEditModal = document.getElementById('imageEditModal'); const smartLogModal = document.getElementById('smartLogModal'); const smartLogList = document.getElementById('smartLogList'); @@ -1275,6 +1276,19 @@ function syncSelectionUi(){ if(selectedImage.nodeId) touchedIds.add(selectedImage.nodeId); world.classList.toggle('smart-multi-selected', ids.length > 1); smartArrangeBtn?.classList.toggle('visible', ids.length > 0); + // 框选了多张图片节点时显示「发送至Agent」按钮 + const selectedImgNodes = ids.map(id => nodes.find(n => n.id === id)).filter(n => n && isSmartImageNode(n) && (n.images || []).some(img => img?.url)); + if(smartSendAgentBtn){ + if(selectedImgNodes.length > 0){ + smartSendAgentBtn.hidden = false; + smartSendAgentBtn.classList.add('visible'); + const labelEl = smartSendAgentBtn.querySelector('span'); + if(labelEl) labelEl.textContent = `发送至Agent(${selectedImgNodes.length}张)`; + } else { + smartSendAgentBtn.hidden = true; + smartSendAgentBtn.classList.remove('visible'); + } + } smartNodeElementsByIds(touchedIds).forEach(el => { const id = el.dataset.id || ''; el.classList.toggle('selected', isNodeSelected(id)); @@ -15430,6 +15444,7 @@ function finishSelection(event){ selectionJustFinished = true; selectionBox.style.display = 'none'; render(); + syncSelectionUi(); setTimeout(() => { selectionJustFinished = false; }, 0); } function groupSelectedNodes(){ @@ -15735,6 +15750,7 @@ shell.onclick = e => { closeCreateMenu(); clearSelection(); render(); + syncSelectionUi(); }; minimap?.addEventListener('mousedown', e => { if(e.button !== 0) return; @@ -15746,9 +15762,37 @@ minimap?.addEventListener('mousedown', e => { }); smartArrangeBtn?.addEventListener('mousedown', e => e.stopPropagation()); smartArrangeBtn?.addEventListener('click', e => { - e.preventDefault(); - e.stopPropagation(); - arrangeSelectedSmartNodes(); +e.preventDefault(); +e.stopPropagation(); +arrangeSelectedSmartNodes(); +}); +smartSendAgentBtn?.addEventListener('mousedown', e => e.stopPropagation()); +smartSendAgentBtn?.addEventListener('click', e => { +e.preventDefault(); +e.stopPropagation(); +// 收集所有选中节点的第一张图片,批量发送至 Agent +const ids = selectedNodeIds(); +const imgNodes = ids.map(id => nodes.find(n => n.id === id)).filter(n => n && isSmartImageNode(n) && (n.images || []).some(img => img?.url)); +if(!imgNodes.length){ toast('没有选中的图片节点'); return; } +if(!agentOpen) toggleAgentPanel(true); +if(!agentState) return; +if(!Array.isArray(agentState.attachments)) agentState.attachments = []; +const _genProv = agentGenProviders().some(p => p.id === agentState.genProvider) ? agentState.genProvider : (agentGenProviders()[0]?.id || ''); +const _refMax = _genProv ? providerMaxReferenceImages(_genProv) : AGENT_LLM_IMAGE_MAX; +let added = 0, skipped = 0; +for(const node of imgNodes){ +if(agentState.attachments.length >= _refMax){ skipped++; continue; } +const item = imageForDisplay(node.images[0]); +if(!item?.url) continue; +if(agentState.attachments.some(a => a.url === item.url)){ skipped++; continue; } +agentState.attachments.push({url:item.url, name:item.name || node.title || 'image', nodeId:node.id, x:Number(node.x) || 0, y:Number(node.y) || 0}); +added++; +} +renderAgentAttachments(); +saveAgentState(); +if(added > 0 && skipped > 0) toast(`已发送 ${added} 张至 Agent,${skipped} 张因重复或超限跳过`); +else if(added > 0) toast(`已发送 ${added} 张至 Agent`); +else if(skipped > 0) toast(`参考图已达上限或全部重复`); }); window.onmousemove = e => { lastMouseWorld = screenToWorld(e); @@ -17114,11 +17158,12 @@ function agentUpdateToolbarLabels(){ } } function agentMoveSelectsToDropdown(){ - const chatSelects = document.getElementById('agentChatSelects'); + const chatModelSelects = document.getElementById('agentChatModelSelects'); const genSelects = document.getElementById('agentGenSelects'); - if(chatSelects && agentChatProvider && agentChatModel){ - chatSelects.appendChild(agentChatProvider); - chatSelects.appendChild(agentChatModel); + // 将 LLM 模型选择器放到思维模式面板中 + if(chatModelSelects && agentChatProvider && agentChatModel){ + chatModelSelects.appendChild(agentChatProvider); + chatModelSelects.appendChild(agentChatModel); } if(genSelects && agentGenProvider && agentGenModel){ genSelects.appendChild(agentGenProvider); @@ -17127,8 +17172,10 @@ function agentMoveSelectsToDropdown(){ // 确保下拉面板初始隐藏 const modelPanel = document.getElementById('agentModelPanel'); const paramsPanel = document.getElementById('agentParamsPanel'); + const chatModelPanel = document.getElementById('agentChatModelPanel'); if(modelPanel) modelPanel.hidden = true; if(paramsPanel) paramsPanel.hidden = true; + if(chatModelPanel) chatModelPanel.hidden = true; } function renderAgentModelSelectors(){ if(!agentState) return; @@ -17214,7 +17261,7 @@ function renderAgentAttachments(){ html += `
${escapeHtml(skill.name || 'skill.md')}
`; }); attachments.forEach((att, i) => { - html += `
`; + html += `
`; }); agentAttachRow.innerHTML = html; if(window.lucide) lucide.createIcons(); @@ -17234,10 +17281,10 @@ function renderAgentAttachments(){ saveAgentState(); }; }); - agentAttachRow.querySelectorAll('[data-agent-att-jump]').forEach(el => { + agentAttachRow.querySelectorAll('[data-agent-att-index]').forEach(el => { el.onclick = e => { if(e.target.closest('[data-agent-att-remove]')) return; - const att = agentState.attachments[Number(el.dataset.agentAttJump)]; + const att = agentState.attachments[Number(el.dataset.agentAttIndex)]; if(!att) return; // 跳转到画布中图片位置 if(att.nodeId){ @@ -17255,6 +17302,54 @@ function renderAgentAttachments(){ agentCenterOnPoint(Number(att.x) || 0, Number(att.y) || 0); } }; + // 拖拽排序 + el.addEventListener('dragstart', e => { + e.stopPropagation(); + el.classList.add('dragging'); + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData('text/plain', el.dataset.agentAttIndex); + }); + el.addEventListener('dragend', e => { + e.stopPropagation(); + el.classList.remove('dragging'); + agentAttachRow.querySelectorAll('.agent-attach-chip').forEach(c => c.classList.remove('drop-before', 'drop-after')); + }); + el.addEventListener('dragover', e => { + const fromIdx = Number(e.dataTransfer.getData('text/plain')); + const toIdx = Number(el.dataset.agentAttIndex); + if(!Number.isFinite(fromIdx) || fromIdx < 0 || fromIdx === toIdx) return; + e.preventDefault(); + e.stopPropagation(); + e.dataTransfer.dropEffect = 'move'; + agentAttachRow.querySelectorAll('.agent-attach-chip').forEach(c => c.classList.remove('drop-before', 'drop-after')); + const rect = el.getBoundingClientRect(); + const placement = e.clientX < rect.left + rect.width / 2 ? 'before' : 'after'; + el.classList.add(placement === 'before' ? 'drop-before' : 'drop-after'); + }); + el.addEventListener('dragleave', e => { + if(el.contains(e.relatedTarget)) return; + el.classList.remove('drop-before', 'drop-after'); + }); + el.addEventListener('drop', e => { + const fromIdx = Number(e.dataTransfer.getData('text/plain')); + const toIdx = Number(el.dataset.agentAttIndex); + if(!Number.isFinite(fromIdx) || fromIdx < 0 || fromIdx === toIdx) return; + e.preventDefault(); + e.stopPropagation(); + const rect = el.getBoundingClientRect(); + const placement = e.clientX < rect.left + rect.width / 2 ? 'before' : 'after'; + agentAttachRow.querySelectorAll('.agent-attach-chip').forEach(c => c.classList.remove('drop-before', 'drop-after')); + // 重排 attachments 数组 + const atts = agentState.attachments.slice(); + const [moved] = atts.splice(fromIdx, 1); + let insertAt = toIdx; + if(placement === 'after') insertAt += 1; + if(fromIdx < insertAt) insertAt -= 1; + atts.splice(Math.max(0, Math.min(atts.length, insertAt)), 0, moved); + agentState.attachments = atts; + renderAgentAttachments(); + saveAgentState(); + }); }); } async function agentAttachFiles(files){ @@ -17702,63 +17797,71 @@ When a skill document is provided, every prompt you generate MUST fully and verb // P1-9: 系统提示词动态化 —— 根据思维模式开关追加不同指令 const thinkingModeOn = agentState?.thinkingMode && !bypassThinking; if(thinkingModeOn){ - parts.push(`当前为思维模式(用户参与决策模式)。核心原则:用户在生图前必须看到并确认每一条提示词,确认后才统一生图。 + parts.push(`当前为思维模式(渐进式多维采集模式)。核心原则:通过多轮提问逐步收集用户需求,所有维度确认后生成详细提示词。 -【参数权重 / Parameter Priority】(由系统决定,你无需判断,也不要在prompt里加数量/比例/分辨率描述) -- 出图数量:输入框显式要求 > 工具栏设置(软参数) -- 比例/分辨率:工具栏说了算(硬参数),输入框不覆盖 -- 你只需按系统给定的数量返回对应条数,不要自行增减。 +【流程规则 / Process Rules】 -【两阶段流程 / Two-Stage Flow】 +总体流程:逐轮提问维度 → 用户选择 → 下一轮提问下一个维度 → ... → 所有维度确认 → 生成最终提示词 -■ 阶段一:风格引导(仅当输入"缺风格"时触发) -判断标准——用户的输入有6个信息维度:①主体 ②风格(画风/艺术流派) ③场景/背景 ④配色 ⑤构图 ⑥细节特征。 -- 如果只有①主体,缺②风格 → 必须返回 2-4 个 options(风格/方向选项)让用户选择,不要直接返回 prompts。 - 选项示例:[水墨风、油画风、赛博朋克、Q版卡通] 或 [极简logo、复古徽章、几何扁平、手绘插画] - 返回 {"reply":"请选择一个风格方向:","options":[{"label":"水墨风","value":"水墨风"},...],"prompts":[],"generations":[]} -- 如果②风格已存在(即使简短如"水墨风的狗"),跳过阶段一,直接进入阶段二。 +轮次判断规则: +- 如果还有 ≥2 个维度未确认 → 返回 options,继续提问 +- 如果只剩 1 个维度未确认 → 返回 options,最后一轮提问 +- 如果所有维度已确认 → 生成最终提示词(返回 prompts) -■ 阶段二:扩写/优化 + 确认(用户选了风格后,或输入本身已有风格时进入) -根据输入完整度分三档处理: +维度优先级(按重要性排序): +1. 风格 (画风/艺术流派) - 如水墨风、油画风、赛博朋克、Q版卡通 +2. 场景/背景 - 如留白山水、竹林、雪景、庭院、城市街道 +3. 构图 - 如正面站姿、仰视特写、奔跑动态、侧卧休息、三分法 +4. 配色 - 如暖色调、冷色调、低饱和度、高对比度 +5. 细节特征 - 如毛发质感、光影效果、材质表现、装饰元素 +6. 其他补充 - 如文字要求、品牌元素、特殊效果 -【档A:大幅扩写】——有主体+风格,但场景/配色/构图/细节缺失3项以上 -你必须大幅扩写,补全所有缺失维度,生成完整可直接生图的中文提示词。 -示例:用户输入"一只狗,水墨风" → 你扩写为"一只水墨风格的西高地白犬,中国传统水墨画技法,淡墨晕染毛发,浓墨点睛,大量留白营造意境,毛笔笔触可见,站姿端正微微侧头,眼神温和灵动,背景大面积留白,右下角有朱砂印章,意境悠远,水墨画质感,宣纸纹理" +【参考图分析规则】 -【档B:适度优化】——主体+风格+场景+配色+构图+细节大部分都有 -不要重写用户的核心描述,只做适度优化:补充技术参数(如"高质量、印刷级精度")、理顺语句、确保完整性。 -如果输入已经足够完整专业,可以基本保持原样,但在 reply 里说明"你的提示词已较完整,可直接确认或微调"。 -示例:用户输入"海报设计,大师级排版,极繁主义,配色协调,波点,半调图案..." → 你保持核心描述,补充"高质量海报输出,印刷级精度"即可,不要把"极繁主义"改成"极简主义",不要删减用户写的细节。 +当用户上传了参考图时,第一轮或第二轮必须先分析参考图并提问: +- 返回 reply 说明参考图的共同特征(风格、配色、构图、光影等) +- 选项必须包含用户对参考图特征的选择(全部保留/部分保留/不保留) +- 示例:{"reply":"我看到了7张参考图,它们有共同的特征:低饱和度配色、极简构图、柔和光影。你希望产品图保留哪些特征?","options":[{"label":"全部保留","value":"保留参考图的所有视觉特征:低饱和度配色、极简构图、柔和光影"},{"label":"只保留配色","value":"只保留参考图的低饱和度配色"},{"label":"只保留构图","value":"只保留参考图的极简构图"},{"label":"自定义输入","value":"CUSTOM_INPUT"}],"collected":{"参考图特征":"已分析"}} -【档C:修改请求】——用户说"换成像素风""改成夜景"等 -返回 prompts,use_last_outputs 设为 true(引用上一轮图片)。在已有基础上做修改,保留未要求改动的部分。 +【选项规则】 -【返回规则】 -1. 只返回 prompts 数组,不要返回 generations 数组。 -2. 每个 prompt 对象的 count 固定为 1。不要用 count>1。 -3. 系统要求生成N张图时(见上方"出图数量"),prompts 数组返回恰好N条。 - 每条必须是完整的、可直接生图的中文提示词。 - N条之间在主题方向/变体方向上有差异(不是换风格,而是在同一风格下做变体)。 - 例如用户选了"水墨风"+系统要3张 → 返回3条水墨风prompt,分别画不同姿态/场景/氛围的狗。 - 【重要】每条 prompt 必须在姿态、场景、构图、光线、配色中至少3个维度有实质差异。 - 不能只换颜色或微调细节,否则生成的图会几乎一样。 - 如果只返回1条让前端补充,补充的变体质量会远不如你直接写的。请务必返回恰好N条。 -4. 每个 prompt 对象包含:prompt(中文提示词), count(固定为1), use_last_outputs(bool), use_attachments(bool)。 -5. 用户会逐条确认(确认/修改/重新生成),全部确认后才统一生图。 -6. 所有prompt必须中文,包含主体/风格/构图/光线/色彩/细节/氛围。 -7. 文字规则:默认prompt不包含文字内容,除非skill文档明确要求或用户明确要求"加文字"。 -${hasSkills ? '8. 每条 prompt 必须完整包含 skill 文档的所有描述内容,只在主题/变体上做差异化(见上方"skill 完整保留"规则)。Skill 描述单张图样式,不决定出图数量。' : ''}`); +- 每轮返回 2-4 个选项(推荐数量为3) +- 每个选项必须是简洁明确的值,不是长句子 +- 每轮 options 末尾必须追加一个 {"label":"自定义输入","value":"CUSTOM_INPUT"} 选项 +- 选项示例:[水墨风, 油画风, 赛博朋克, 自定义输入] + +【返回字段】 + +每轮必须返回以下字段: +{ + "reply": "简短的问题描述(如'请选择风格方向:')", + "options": [{"label":"选项1","value":"选项1值"}, {"label":"选项2","value":"选项2值"}, {"label":"自定义输入","value":"CUSTOM_INPUT"}], + "collected": {"维度1":"已确认值1", "维度2":"已确认值2", ...}, // 累积已确认的维度 + "next_dimension": "场景", // 下一轮要问的维度 + "remaining_dimensions": ["场景", "构图", "配色"], // 剩余未确认的维度 + "prompts": [], // 问答阶段始终为空 + "generations": [] // 问答阶段始终为空 +} + +【最终轮规则】 + +当所有维度确认后(remaining_dimensions 为空或用户明确要求): +- 返回 prompts 数组,每条是完整可直接生图的中文提示词 +- 提示词要综合所有 collected 维度的信息 +- 系统要求生成N张图时(见上方"出图数量"),prompts 数组返回恰好N条 +- 每条 prompt 目标长度:200-500 字,尽可能详细丰富 +- 每条必须包含:主体、风格、场景、构图、光线、色彩、细节、氛围 + +【修改请求规则】 + +当用户说"换成...""改成..."等修改指令时: +- 返回 prompts,use_last_outputs 设为 true +- prompt 应简洁聚焦,只描述要修改的内容+保持原图其他部分不变 + +${hasSkills ? '【Skill 规则】\n当有 Skill 文档时,最终提示词必须完整包含 Skill 的所有描述,只在主题/变体上做差异化。Skill 描述单张图样式,不决定出图数量。' : ''}`); } else { - parts.push(`当前为直接模式。能生成就生成,返回 generations 数组。 -重要规则: -1. 每条 generation 的 count 固定为 1。不要用 count>1 来生成多张图。 -2. 系统要求生成N张图时(见上方"出图数量"),generations 数组必须返回恰好N条不同的 generation(每条 count=1),每条在主题/品牌方向上必须有明显差异。 - 例如系统要求3张 Logo 合集图,就返回3条 generation,分别用不同的品牌主题(如科技品牌、教育品牌、生活方式品牌)。 -3. 不要返回2条或1条然后让前端补充——必须返回恰好N条,否则会导致重复生成。 - 【重要】每条 generation 的 prompt 必须在姿态、场景、构图、光线、配色中至少3个维度有实质差异。 - 不能只换颜色或微调细节,否则生成的图会几乎一样。 -4. 每个 generation 对象包含:prompt(中文提示词), count(固定为1), use_last_outputs(bool), use_attachments(bool)。 -${hasSkills ? '5. 每条 generation 的 prompt 必须完整包含 skill 文档的所有描述内容,只在主题/品牌方向上做差异化(见上方"skill 完整保留"规则)。' : ''}`); + // 直接模式已废弃(思维模式 OFF 时前端跳过 LLM),此处保留兼容性 + parts.push(`当前为直接模式。能生成就生成,返回 generations 数组。`); } // 3. 末尾再强调 skill(近因效应),确保所有 provider 都不会遗漏 if(hasSkills){ @@ -17865,8 +17968,12 @@ function parseAgentResponse(raw, lastUserText){ const reply = typeof data.reply === 'string' ? data.reply : (typeof data.text === 'string' ? data.text : ''); let options = (Array.isArray(data.options) ? data.options : []) .filter(o => o && typeof o.label === 'string' && typeof o.value === 'string') - .slice(0, 4) + .slice(0, 8) // 增加到 8 以容纳自定义输入 .map(o => ({label:o.label.trim(), value:o.value.trim()})); + // 如果有 options 且末尾不是"自定义输入",自动追加 + if(options.length > 0 && options.length < 8 && !options.some(o => o.value === 'CUSTOM_INPUT')){ + options.push({label:'自定义输入', value:'CUSTOM_INPUT'}); + } if(options.length === 0 && reply){ const numbered = extractNumberedOptions(reply); if(numbered) options = numbered.options; @@ -17876,25 +17983,43 @@ function parseAgentResponse(raw, lastUserText){ .filter(g => g && typeof g.prompt === 'string' && g.prompt.trim()) .slice(0, AGENT_GEN_MAX_PER_MSG) .map(g => ({prompt:g.prompt.trim(), count:Math.max(1, Math.min(8, Number(g.count) || 1)), use_last_outputs:!!g.use_last_outputs, use_attachments:!!g.use_attachments, results:[], status:'running'})); - return {reply, options, prompts, generations}; + // 解析新字段:collected、next_dimension、remaining_dimensions + const collected = (data.collected && typeof data.collected === 'object') ? data.collected : {}; + const nextDimension = typeof data.next_dimension === 'string' ? data.next_dimension : ''; + const remainingDimensions = Array.isArray(data.remaining_dimensions) ? data.remaining_dimensions : []; + return {reply, options, prompts, generations, collected, next_dimension, remaining_dimensions}; } catch(e) { /* 尝试下一个候选 */ } } // JSON 解析失败时的 fallback 链 const numberedFallback = extractNumberedOptions(text); if(numberedFallback){ - return {reply:numberedFallback.reply || text, options:numberedFallback.options, prompts:[], generations:[]}; + const fallbackOptions = numberedFallback.options || []; + // 自动追加自定义输入选项 + if(fallbackOptions.length > 0 && fallbackOptions.length < 8 && !fallbackOptions.some(o => o.value === 'CUSTOM_INPUT')){ + fallbackOptions.push({label:'自定义输入', value:'CUSTOM_INPUT'}); + } + return {reply:numberedFallback.reply || text, options:fallbackOptions, prompts:[], generations:[], collected:{}, next_dimension:'', remaining_dimensions:[]}; } const clarifyOptions = extractClarifyOptions(text, lastUserText); if(clarifyOptions){ - return {reply:text, options:clarifyOptions, prompts:[], generations:[]}; + const fallbackOptions = clarifyOptions || []; + // 自动追加自定义输入选项 + if(fallbackOptions.length > 0 && fallbackOptions.length < 8 && !fallbackOptions.some(o => o.value === 'CUSTOM_INPUT')){ + fallbackOptions.push({label:'自定义输入', value:'CUSTOM_INPUT'}); + } + return {reply:text, options:fallbackOptions, prompts:[], generations:[], collected:{}, next_dimension:'', remaining_dimensions:[]}; } - return {reply:text, generations:[]}; + return {reply:text, options:[], prompts:[], generations:[], collected:{}, next_dimension:'', remaining_dimensions:[]}; } // 处理 LLM 返回结果:解析、兜底、创建 assistant 消息、运行生图 // 提取为独立函数,以便刷新恢复时复用 async function processAgentLlmResult(result, text, attachments, userMsg){ - const parsed = parseAgentResponse(result.text || '', text); - // 提前计算思维模式状态,使兜底逻辑能感知 +const parsed = parseAgentResponse(result.text || '', text); +// 防御性检查:确保 options/prompts/generations 始终是数组 +if(!Array.isArray(parsed.options)) parsed.options = []; +if(!Array.isArray(parsed.prompts)) parsed.prompts = []; +if(!Array.isArray(parsed.generations)) parsed.generations = []; +// 提前计算思维模式状态,使兜底逻辑能感知 const bypassThinking = userMsg?.bypassThinking === true; const thinkingModeOn = agentState?.thinkingMode && !bypassThinking; // 生图意图兜底 + 修改意图检测 @@ -18100,10 +18225,21 @@ async function processAgentLlmResult(result, text, attachments, userMsg){ } // 直接模式下,无论 requestedCount 多少,强制所有 generation 的 count=1 // (防止 LLM 返回 count>1 导致同一 prompt 发多次请求、生成重复图) - if(!thinkingModeOn && parsed.generations.length > 0){ - parsed.generations.forEach(g => { g.count = 1; }); - } - const assistantMsg = {id:uid('am'), role:'assistant', text:parsed.reply, options:parsed.options || [], prompts:parsed.prompts || [], generations:parsed.generations, ts:Date.now()}; +if(!thinkingModeOn && parsed.generations.length > 0){ + parsed.generations.forEach(g => { g.count = 1; }); +} +const assistantMsg = { + id:uid('am'), + role:'assistant', + text:parsed.reply, + options:parsed.options || [], + prompts:parsed.prompts || [], + generations:parsed.generations, + ts:Date.now(), + collected: parsed.collected || {}, + next_dimension: parsed.next_dimension || '', + remaining_dimensions: parsed.remaining_dimensions || [] +}; // P2-12: 记录请求数量到消息,用于卡片显示校验 if(requestedCount > 0) assistantMsg.requestedCount = requestedCount; if(assistantMsg.prompts.length > 0){ @@ -18142,6 +18278,101 @@ async function sendAgentMessage(){ const text = String(agentInput?.value || '').trim(); const attachments = (Array.isArray(agentState.attachments) ? agentState.attachments : []).slice(); if(!text && !attachments.length) return; + + // ============ 改造:思维模式 OFF 时跳过 LLM,直接生图 ============ + const thinkingModeOn = agentState?.thinkingMode; + if(!thinkingModeOn){ + // 思维模式关闭:直接构建 generations 并生图 + const genProviderId = agentState.genProvider; + const genModel = agentState.genModel; + + // 检查生图模型是否配置 + const genProviders = agentGenProviders(); + const genProvider = genProviders.find(p => p.id === genProviderId) || genProviders[0]; + if(!genProvider){ toast(tr('smart.agentNoProviders') || '未配置生图模型'); return; } + + const models = providerImageModels(genProvider.id) || []; + const model = genModel && models.includes(genModel) ? genModel : (models[0] || ''); + if(!model){ toast(tr('smart.agentNoProviders') || '生图模型不可用'); return; } + + // 计算生图参数 + const ratio = agentState.genRatio || 'square'; + const resolution = agentState.genResolution || '1k'; + const count = Math.max(1, Math.min(8, Number(agentState.genCount) || 1)); + + // 构建 generations + const generations = []; + const _finalCount = resolveFinalGenCount(text); + const requestedCount = _finalCount.count > 1 ? _finalCount.count : count; + + // 判断是否是修改请求 + const userModifyRe = /改成|换成|转换成|修改为|变成|转为|改为|转成|调整为|修改成|变回|调成|重新画|重画|重新生成|修改一下|改一下|调整一下/i; + const isModifyRequest = userModifyRe.test(text); + const useLastOutputs = isModifyRequest; + const useAttachments = attachments.length > 0; + + // 根据数量构建多条 generations(如果需要) + for(let i = 0; i < Math.max(requestedCount, 1); i++){ + const promptText = requestedCount > 1 ? `${text}(变体${i + 1},不同构图/角度/场景)` : text; + generations.push({ + prompt: promptText, + count: 1, + use_last_outputs: useLastOutputs, + use_attachments: useAttachments, + results: [], + status: 'running' + }); + } + + // 创建 user 消息 + const userMsg = { + id: uid('am'), + role: 'user', + text, + images: attachments, + ts: Date.now() + }; + if(requestedCount > 1) userMsg.requestedCount = requestedCount; + agentState.messages.push(userMsg); + agentState.messages = agentState.messages.slice(-AGENT_MSG_MAX); + agentState.attachments = []; + if(agentInput) agentInput.value = ''; + renderAgentAttachments(); + + // 创建 assistant 消息(直接生图模式) + const assistantMsg = { + id: uid('am'), + role: 'assistant', + text: '', + options: [], + prompts: [], + generations: generations, + ts: Date.now(), + requestedCount: requestedCount + }; + agentState.messages.push(assistantMsg); + agentState.messages = agentState.messages.slice(-AGENT_MSG_MAX); + agentSending = true; + saveAgentState(); + renderAgentMessages(); + + // 直接执行生图 + try { + await runAgentGenerations(assistantMsg, userMsg); + } catch(e) { + assistantMsg.generations.forEach(g => { g.status = 'failed'; g.error = String(e.message || e); }); + renderAgentMessages(); + saveAgentState(); + } finally { + agentSending = false; + renderAgentMessages(); + saveAgentState(); + } + return; + } + // ============ 改造结束 ============ + + // 思维模式开启:走原有 LLM 流程 if(!chatApiProviders().length){ toast(tr('smart.agentNeedChatModel')); return; } const provider = resolveChatProviderId(agentState.chatProvider); const model = resolveChatModel(agentState.chatModel, provider); @@ -18176,6 +18407,15 @@ async function sendAgentMessage(){ // - gemini-cli(agy):system_prompt 被拼成普通文本"系统要求:...",优先级低,易忽略 ✗ // 在 user message 里追加提醒,确保所有 provider 都能看到 skill 要求 let messageText = text || '(please help me edit these images)'; + // 多轮对话:注入已确认的维度信息 + const lastAssistant = [...(agentState.messages || [])].reverse().find(m => m.role === 'assistant'); + const prevCollected = lastAssistant?.collected || {}; + if(Object.keys(prevCollected).length > 0){ + messageText += `${AGENT_NL}${AGENT_NL}【已确认维度】以下维度已在之前的对话中确认:`; + for(const [key, value] of Object.entries(prevCollected)){ + messageText += `${AGENT_NL}- ${key}:${value}`; + } + } // 前端数量决策:输入框显式要求 > 工具栏设置(软参数覆盖) const _finalCount = resolveFinalGenCount(text); // 阶段二继承:如果当前没有明确请求多张,但上一条 user 消息有 requestedCount,继承它 @@ -18817,7 +19057,11 @@ function initAgentInputResize(){ } function initAgentPanel(){ if(!agentPanel) return; + // 提前绑定发送按钮事件,确保即使后续初始化出错发送按钮也能用 + agentSendBtn?.addEventListener('click', () => sendAgentMessage()); loadAgentState(); + // 如果没有恢复中的任务,确保 agentSending 重置为 false + if(!_agentRecoveryInProgress) agentSending = false; agentMoveSelectsToDropdown(); renderAgentModelSelectors(); renderAgentAttachments(); @@ -18831,8 +19075,10 @@ function initAgentPanel(){ function closeAllDropdowns(){ if(modelPanel) modelPanel.hidden = true; if(paramsPanel) paramsPanel.hidden = true; + const chatModelPanel = document.getElementById('agentChatModelPanel'); + if(chatModelPanel) chatModelPanel.hidden = true; } - function showDropdown(btn, panel){ + function showDropdown(btn, panel, opts){ if(!btn || !panel) return; // 把面板移到 document.body 中,避免被 Agent 面板的 backdrop-filter 遮挡 if(panel.parentElement !== document.body) document.body.appendChild(panel); @@ -18843,10 +19089,19 @@ function initAgentPanel(){ const panelHeight = panel.offsetHeight; const panelWidth = panel.offsetWidth; panel.style.visibility = ''; - // 水平位置:确保面板不会超出屏幕右边界 - const maxLeft = window.innerWidth - panelWidth - 8; - panel.style.left = Math.min(Math.max(8, rect.left), Math.max(8, maxLeft)) + 'px'; - panel.style.right = 'auto'; + // 水平位置 + const alignRight = opts && opts.alignRight; + if(alignRight){ + // 右对齐:面板右边缘对齐按钮右边缘,向左展开 + const rightEdge = window.innerWidth - rect.right; + panel.style.right = Math.max(8, rightEdge) + 'px'; + panel.style.left = 'auto'; + } else { + // 默认左对齐:确保面板不会超出屏幕右边界 + const maxLeft = window.innerWidth - panelWidth - 8; + panel.style.left = Math.min(Math.max(8, rect.left), Math.max(8, maxLeft)) + 'px'; + panel.style.right = 'auto'; + } // 垂直位置:优先在按钮上方显示,如果空间不够则在下方显示 const spaceAbove = rect.top; const spaceBelow = window.innerHeight - rect.bottom; @@ -18859,6 +19114,7 @@ function initAgentPanel(){ } panel.hidden = false; } + window.__agentShowDropdown = showDropdown; modelBtn?.addEventListener('click', e => { e.stopPropagation(); const wasHidden = modelPanel?.hidden; @@ -18871,6 +19127,7 @@ function initAgentPanel(){ closeAllDropdowns(); if(wasHidden) showDropdown(paramsBtn, paramsPanel); }); + const chatModelPanel = document.getElementById('agentChatModelPanel'); document.addEventListener('pointerdown', e => { if(!e.target.closest('.agent-toolbar-dropdown-wrap') && !e.target.closest('.agent-dropdown-panel')) closeAllDropdowns(); }, true); @@ -18956,22 +19213,34 @@ function initAgentPanel(){ agentAttachFiles(agentImageInput.files); agentImageInput.value = ''; }); - agentSendBtn?.addEventListener('click', () => sendAgentMessage()); - // 思维模式开关按钮 - const agentThinkingBtn = document.getElementById('agentThinkingBtn'); - function syncAgentThinkingBtn(){ - if(agentThinkingBtn){ - agentThinkingBtn.classList.toggle('active', !!agentState?.thinkingMode); - } +// 思维模式开关按钮 +const agentThinkingBtn = document.getElementById('agentThinkingBtn'); +function syncAgentThinkingBtn(){ + if(agentThinkingBtn){ + agentThinkingBtn.classList.toggle('active', !!agentState?.thinkingMode); + } + // 思维模式关闭时收起理解模型下拉面板 + if(!agentState?.thinkingMode){ + const chatModelPanel = document.getElementById('agentChatModelPanel'); + if(chatModelPanel) chatModelPanel.hidden = true; } +} +syncAgentThinkingBtn(); +agentThinkingBtn?.addEventListener('click', () => { + if(!agentState) return; + agentState.thinkingMode = !agentState.thinkingMode; syncAgentThinkingBtn(); - agentThinkingBtn?.addEventListener('click', () => { - if(!agentState) return; - agentState.thinkingMode = !agentState.thinkingMode; - syncAgentThinkingBtn(); - saveAgentState(); - toast(agentState.thinkingMode ? '思维模式已开启:扩写/优化提示词' : '思维模式已关闭:直接生成'); - }); + saveAgentState(); + toast(agentState.thinkingMode ? '思维模式已开启:扩写/优化提示词' : '思维模式已关闭:直接生成'); + // 开启思维模式时,展开理解模型选择面板(右对齐,避免覆盖发送按钮) + if(agentState.thinkingMode){ + const chatModelPanel = document.getElementById('agentChatModelPanel'); + const thinkingBtn = document.getElementById('agentThinkingBtn'); + if(chatModelPanel && thinkingBtn && window.__agentShowDropdown){ + window.__agentShowDropdown(thinkingBtn, chatModelPanel, {alignRight:true}); + } + } +}); agentInput?.addEventListener('input', () => { const val = agentInput.value; const cursorPos = agentInput.selectionStart || 0; diff --git a/static/smart-canvas.html b/static/smart-canvas.html index 1bcc0d40c..9e3af60e7 100644 --- a/static/smart-canvas.html +++ b/static/smart-canvas.html @@ -20,7 +20,7 @@ - +
@@ -133,6 +133,7 @@
+
- + - + @@ -559,6 +564,6 @@
preview
- + From f3a8738af1a7bbb78b32e6168a1f0c1e9161fc76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Mon, 20 Jul 2026 22:43:50 +0800 Subject: [PATCH 57/67] =?UTF-8?q?v1.6:=20=E6=80=9D=E7=BB=B4=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E6=B8=90=E8=BF=9B=E5=BC=8F=E4=BF=AE=E5=A4=8D=20+=20?= =?UTF-8?q?=E9=99=84=E4=BB=B6=E7=BC=96=E5=8F=B7=20+=20=E7=BA=AF=E6=96=87?= =?UTF-8?q?=E5=AD=97=E5=BC=95=E5=AF=BC=E8=A7=A3=E6=9E=90=20+=20=E7=A1=AE?= =?UTF-8?q?=E8=AE=A4=E9=9D=A2=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.py | 128 ++++++++--- static/angle.html | 18 +- static/api-settings.html | 14 +- static/asset-manager.html | 10 +- static/canvas-list.html | 16 +- static/canvas.html | 18 +- static/comfyui-settings.html | 14 +- static/css/smart-canvas.css | 24 ++ static/enhance.html | 16 +- static/gpt-chat.html | 12 +- static/index.html | 28 +-- static/js/smart-canvas.js | 428 +++++++++++++++++++++++++++++------ static/klein.html | 16 +- static/online.html | 16 +- static/smart-canvas.html | 23 +- static/zimage.html | 14 +- 16 files changed, 599 insertions(+), 196 deletions(-) diff --git a/main.py b/main.py index f982fda33..db9cd0638 100755 --- a/main.py +++ b/main.py @@ -188,7 +188,7 @@ async def send_personal_message(self, message: dict, client_id: str): manager = ConnectionManager() GLOBAL_LOOP = None -APP_VERSION = "2026.06.03" +APP_VERSION = "2026.06.04" GITHUB_REPO_URL = "https://github.com/hero8152/Infinite-Canvas" GITHUB_VERSION_URL = "https://raw.githubusercontent.com/hero8152/Infinite-Canvas/main/VERSION" GITHUB_TREE_URL = "https://api.github.com/repos/hero8152/Infinite-Canvas/git/trees/main?recursive=1" @@ -7585,8 +7585,20 @@ def convert_output_to_jpg(url, quality=88): print(f"转换 JPG 失败: {e}") return url +def ref_image_max_size(num_refs): + """根据参考图数量动态计算 max_size,控制总 payload 在 nginx 限制内。 + nginx 默认 client_max_body_size=1MB,base64 膨胀 33%,需留余量。""" + if not num_refs or num_refs <= 1: + return 1536 + if num_refs <= 3: + return 1024 + if num_refs <= 6: + return 768 + return 512 + def reference_to_data_url(ref, max_size=None): - """把本地输出文件转为 data URL(base64)。max_size 限制最长边像素,避免 payload 过大。""" + """把本地输出文件转为 data URL(base64)。max_size 限制最长边像素,避免 payload 过大。 + 始终使用 JPEG 格式(RGBA 合成到白底),避免 PNG 过大导致 413。""" path = output_file_from_url(ref.get("url", "")) if not path: return ref.get("url", "") @@ -7597,20 +7609,56 @@ def reference_to_data_url(ref, max_size=None): w, h = img.size if max(w, h) > max_size: img.thumbnail((max_size, max_size), Image.LANCZOS) - if img.mode not in ("RGB", "RGBA"): + # 始终转 RGB:RGBA 合成到白底,P/L 等模式直接转 + if img.mode == "RGBA": + bg = Image.new("RGB", img.size, (255, 255, 255)) + bg.paste(img, mask=img.split()[3]) + img = bg + elif img.mode != "RGB": img = img.convert("RGB") buf = BytesIO() - fmt = "PNG" if img.mode == "RGBA" else "JPEG" - img.save(buf, format=fmt, quality=88 if fmt == "JPEG" else None) + img.save(buf, format="JPEG", quality=85) encoded = base64.b64encode(buf.getvalue()).decode("ascii") - mime = "image/png" if fmt == "PNG" else "image/jpeg" - return f"data:{mime};base64,{encoded}" + return f"data:image/jpeg;base64,{encoded}" except Exception as e: print(f"reference resize failed, fallback to raw: {e}") with open(path, "rb") as f: encoded = base64.b64encode(f.read()).decode("ascii") return f"data:{content_type_for_path(path)};base64,{encoded}" +def prepare_reference_for_multipart(ref, max_size=1536): + """准备参考图用于 multipart 上传。 + 图片 > max_size 时缩放到 max_size 并保存到临时文件(JPEG quality 88)。 + 图片 <= max_size 时返回原始路径(不压缩、不转码)。 + 返回 (path, cleanup_temp_path_or_None)。""" + raw_url = ref.get("url", "") if isinstance(ref, dict) else str(ref or "") + path = output_file_from_url(raw_url) + if not path: + return None, None + if not max_size: + return path, None + try: + with Image.open(path) as img: + img.load() + w, h = img.size + if max(w, h) <= max_size: + return path, None + img.thumbnail((max_size, max_size), Image.LANCZOS) + # 始终转 RGB:RGBA 合成到白底,避免 PNG 过大 + if img.mode == "RGBA": + bg = Image.new("RGB", img.size, (255, 255, 255)) + bg.paste(img, mask=img.split()[3]) + img = bg + elif img.mode != "RGB": + img = img.convert("RGB") + tmp_fd, tmp_path = tempfile.mkstemp(suffix=".jpg", prefix="ref_multipart_") + os.close(tmp_fd) + img.save(tmp_path, format="JPEG", quality=85) + return tmp_path, tmp_path + except Exception as e: + print(f"prepare_reference_for_multipart resize failed, fallback to raw: {e}") + return path, None + def is_image_reference(ref): if not isinstance(ref, dict): return False @@ -9238,11 +9286,13 @@ async def generate_modelscope_provider_image(prompt, size, model, reference_imag raise HTTPException(status_code=400, detail="未配置 ModelScope API Key,请在 API 设置中填写。") width, height = parse_size_pair(size) refs = [] - for ref in (reference_images or [])[:ONLINE_IMAGE_REFERENCE_MAX]: + _ms_refs = (reference_images or [])[:ONLINE_IMAGE_REFERENCE_MAX] + _ms_max = ref_image_max_size(len(_ms_refs)) + for ref in _ms_refs: if not ref.get("url"): continue # 本地参考图转为 data URL;前端已生成的 data URL 保持原样,贴近旧版稳定链路。 - refs.append(modelscope_image_url(ref.get("url", ""), max_size=1536)) + refs.append(modelscope_image_url(ref.get("url", ""), max_size=_ms_max)) headers = { "Authorization": f"Bearer {clean_token}", "Content-Type": "application/json", @@ -9313,8 +9363,8 @@ def gemini_image_config(size): aspect_ratio, resolution = apimart_size_resolution(size) return {"aspectRatio": aspect_ratio, "imageSize": resolution.upper()} -def gemini_reference_part(ref): - value = reference_to_data_url(ref, max_size=1536) +def gemini_reference_part(ref, max_size=1536): + value = reference_to_data_url(ref, max_size=max_size) if not value: return None if isinstance(value, str) and value.startswith("data:image/") and ";base64," in value: @@ -9329,8 +9379,10 @@ async def generate_gemini_provider_image(prompt, size, model, reference_images=N model_name = gemini_model_name(model) endpoint = gemini_endpoint_url(provider, model_name) parts = [{"text": prompt.strip()}] - for ref in (reference_images or [])[:ONLINE_IMAGE_REFERENCE_MAX]: - part = gemini_reference_part(ref) + _refs = (reference_images or [])[:ONLINE_IMAGE_REFERENCE_MAX] + _max_sz = ref_image_max_size(len(_refs)) + for ref in _refs: + part = gemini_reference_part(ref, max_size=_max_sz) if part: parts.append(part) body = { @@ -9349,8 +9401,8 @@ async def generate_gemini_provider_image(prompt, size, model, reference_images=N def volcengine_endpoint_url(provider): return provider_endpoint_url(provider, "image_generation_endpoint", "/api/v3/images/generations") -def volcengine_image_payload(ref): - value = reference_to_data_url(ref, max_size=1536) +def volcengine_image_payload(ref, max_size=1536): + value = reference_to_data_url(ref, max_size=max_size) if not value: return None return value @@ -9364,7 +9416,9 @@ async def generate_volcengine_provider_image(prompt, size, model, reference_imag "size": size, "response_format": "url", } - images = [volcengine_image_payload(ref) for ref in (reference_images or [])[:ONLINE_IMAGE_REFERENCE_MAX]] + _vrefs = (reference_images or [])[:ONLINE_IMAGE_REFERENCE_MAX] + _vmax = ref_image_max_size(len(_vrefs)) + images = [volcengine_image_payload(ref, max_size=_vmax) for ref in _vrefs] images = [value for value in images if value] if images: body["image"] = images @@ -10661,7 +10715,8 @@ async def post_openai_edits(edit_files=None): # 文生图只传 extra_body.response_format,图生图把参考图放进 extra_body.image。 extra_body = {"response_format": "url"} if image_refs: - extra_body["image"] = [reference_to_data_url(ref, max_size=1536) for ref in image_refs[:ONLINE_IMAGE_REFERENCE_MAX]] + _ojson_refs = image_refs[:ONLINE_IMAGE_REFERENCE_MAX] + extra_body["image"] = [reference_to_data_url(ref, max_size=ref_image_max_size(len(_ojson_refs))) for ref in _ojson_refs] body = {"model": model, "prompt": prompt, "size": size, "extra_body": extra_body} response = await client.post(gen_url, headers=api_headers(provider=provider, model=model), json=body) elif is_apimart: @@ -10677,7 +10732,8 @@ async def post_openai_edits(edit_files=None): "official_fallback": False, } if image_refs: - body["image_urls"] = [reference_to_data_url(ref, max_size=1536) for ref in image_refs[:ONLINE_IMAGE_REFERENCE_MAX]] + _apimart_refs = image_refs[:ONLINE_IMAGE_REFERENCE_MAX] + body["image_urls"] = [reference_to_data_url(ref, max_size=ref_image_max_size(len(_apimart_refs))) for ref in _apimart_refs] response = await client.post(gen_url, headers=api_headers(provider=provider, model=model), json=body) elif is_gpt2 and not image_refs and not mask_refs: body = {"model": model, "prompt": prompt, "size": size} @@ -10691,16 +10747,19 @@ async def post_openai_edits(edit_files=None): # GPT-Image-2 参考图不能走 /images/generations JSON,否则部分平台会忽略原图或报 Images API unsupported。 files = [] opened = [] + temp_paths = [] edit_failed_status = None edit_failed_text = "" try: for ref in image_refs[:ONLINE_IMAGE_REFERENCE_MAX]: - path = output_file_from_url(ref.get("url", "")) - if not path: + ref_path, tmp_cleanup = prepare_reference_for_multipart(ref, max_size=1536) + if not ref_path: continue - fh = open(path, "rb") + if tmp_cleanup: + temp_paths.append(tmp_cleanup) + fh = open(ref_path, "rb") opened.append(fh) - files.append(("image", (os.path.basename(path), fh, content_type_for_path(path)))) + files.append(("image", (os.path.basename(ref_path), fh, content_type_for_path(ref_path)))) if mask_refs: mask_path = output_file_from_url(mask_refs[0].get("url", "")) if mask_path: @@ -10720,6 +10779,11 @@ async def post_openai_edits(edit_files=None): finally: for fh in opened: fh.close() + for tp in temp_paths: + try: + os.unlink(tp) + except Exception: + pass # 2) edits 失败 → 非 GPT-Image-2 可回退到 /images/generations + JSON image:[urls/base64](grsai 风格) if response is None: if is_gpt2: @@ -10728,7 +10792,8 @@ async def post_openai_edits(edit_files=None): detail=f"GPT-Image-2 编辑接口 /images/edits 调用失败:{edit_failed_text[:300] or edit_failed_status}。已停止自动重试,避免上游可能已扣费后再次请求。" ) print(f"/images/edits failed ({edit_failed_status}): {edit_failed_text[:200]} → 回退到 /images/generations + image:[] JSON") - image_payload = [reference_to_data_url(ref, max_size=1536) for ref in image_refs[:ONLINE_IMAGE_REFERENCE_MAX]] + _fallback_refs = image_refs[:ONLINE_IMAGE_REFERENCE_MAX] + image_payload = [reference_to_data_url(ref, max_size=ref_image_max_size(len(_fallback_refs))) for ref in _fallback_refs] body = { "model": model, "prompt": prompt, "size": size, "response_format": "url", "n": 1, @@ -14484,7 +14549,9 @@ def append_volcengine_image(url: str, role: str): # enable_upsample / aspect_ratio(仅 16:9、9:16)。无 duration 字段, # 时长由模型本身决定,所以这里不传 duration/seconds。 yuli_images = [] - for ref in payload.images[:3]: + _veo_refs = payload.images[:3] + _veo_max = ref_image_max_size(len(_veo_refs)) + for ref in _veo_refs: ref_url = str(getattr(ref, "url", "") or "").strip() if not ref_url: continue @@ -14492,7 +14559,7 @@ def append_volcengine_image(url: str, role: str): yuli_images.append(ref_url) else: # 本地/dataURL 图片转成 data URL 兜底传递 - data_url = reference_to_data_url(ref.dict(), max_size=1536) + data_url = reference_to_data_url(ref.dict(), max_size=_veo_max) if data_url: yuli_images.append(data_url) prompt_text = str(payload.prompt or "") @@ -14512,10 +14579,12 @@ def append_volcengine_image(url: str, role: str): if payload.enable_upsample: body["enable_upsample"] = True else: + _veo3_refs = payload.images[:4] + _veo3_max = ref_image_max_size(len(_veo3_refs)) image_payload = [] - for ref in payload.images[:4]: + for ref in _veo3_refs: if ref.url: - image_payload.append(reference_to_data_url(ref.dict(), max_size=1536)) + image_payload.append(reference_to_data_url(ref.dict(), max_size=_veo3_max)) body = { "prompt": payload.prompt, "model": selected_model(payload.model, "veo3-fast"), @@ -16564,7 +16633,7 @@ async def generate_angle_cloud(req: CloudGenRequest): payload = { "model": model, "prompt": req.prompt.strip(), - "image_url": [modelscope_image_url(url, max_size=1536) for url in req.image_urls] + "image_url": [modelscope_image_url(url, max_size=ref_image_max_size(len(req.image_urls))) for url in req.image_urls] } if req.resolution: payload["size"] = modelscope_size(req.resolution) @@ -16754,7 +16823,8 @@ async def ms_generate(req: MsGenerateRequest): elif req.size: payload["size"] = modelscope_size(req.size) if req.image_urls: - payload["image_url"] = [modelscope_image_url(url, max_size=1536) for url in req.image_urls] + _ms_urls = req.image_urls + payload["image_url"] = [modelscope_image_url(url, max_size=ref_image_max_size(len(_ms_urls))) for url in _ms_urls] if req.loras is not None: payload["loras"] = req.loras diff --git a/static/angle.html b/static/angle.html index 961619a03..b1d6de375 100644 --- a/static/angle.html +++ b/static/angle.html @@ -17,21 +17,21 @@ } catch(e) {} })(); - - - - - - + + + + + + - + diff --git a/static/api-settings.html b/static/api-settings.html index ebf8eeba1..3a3026c41 100644 --- a/static/api-settings.html +++ b/static/api-settings.html @@ -16,12 +16,12 @@ } catch(e) {} })(); - - - - - - + + + + + +
@@ -565,6 +565,6 @@
- + diff --git a/static/asset-manager.html b/static/asset-manager.html index a66cc7ee7..1b8b6a087 100644 --- a/static/asset-manager.html +++ b/static/asset-manager.html @@ -16,10 +16,10 @@ } catch(e) {} })(); - - - - + + + +
@@ -47,6 +47,6 @@
- + diff --git a/static/canvas-list.html b/static/canvas-list.html index 884849589..8e1735a82 100644 --- a/static/canvas-list.html +++ b/static/canvas-list.html @@ -16,13 +16,13 @@ } catch(e) {} })(); - - - - - - - + + + + + + +
@@ -90,6 +90,6 @@
- + diff --git a/static/canvas.html b/static/canvas.html index c39a9aaf7..caef2a193 100644 --- a/static/canvas.html +++ b/static/canvas.html @@ -16,13 +16,13 @@ } catch(e) {} })(); - - - - - - - + + + + + + +
@@ -350,7 +350,7 @@
- - + + diff --git a/static/comfyui-settings.html b/static/comfyui-settings.html index 0495a0f70..e23df7d6b 100644 --- a/static/comfyui-settings.html +++ b/static/comfyui-settings.html @@ -15,12 +15,12 @@ } catch(e) {} })(); - - - - - - + + + + + +
@@ -122,6 +122,6 @@ preview
- + diff --git a/static/css/smart-canvas.css b/static/css/smart-canvas.css index bb97bc04c..6807de974 100644 --- a/static/css/smart-canvas.css +++ b/static/css/smart-canvas.css @@ -1325,11 +1325,35 @@ input[type=range].smart-range::-webkit-slider-thumb { -webkit-appearance:none; w .agent-attach-chip { position:relative; width:44px; height:44px; border-radius:10px; overflow:hidden; border:1px solid var(--line); cursor:pointer; transition:border-color .14s ease; } .agent-attach-chip:hover { border-color:var(--strong); } .agent-attach-chip img { width:100%; height:100%; object-fit:cover; display:block; } +.agent-att-num { position:absolute; top:2px; left:2px; min-width:14px; height:14px; border-radius:7px; background:rgba(15,23,42,.82); color:#fff; font-size:9px; font-weight:600; line-height:14px; text-align:center; padding:0 3px; z-index:2; pointer-events:none; } .agent-attach-chip button { position:absolute; top:2px; right:2px; width:15px; height:15px; border-radius:50%; background:rgba(15,23,42,.78); color:#fff; display:flex; align-items:center; justify-content:center; border:none; cursor:pointer; padding:0; } .agent-attach-chip button i,.agent-attach-chip button svg { width:9px; height:9px; } .agent-attach-chip.dragging { opacity:.4; } .agent-attach-chip.drop-before { box-shadow:-2px 0 0 0 var(--strong); } .agent-attach-chip.drop-after { box-shadow:2px 0 0 0 var(--strong); } +.agent-ref-confirm { position:absolute; bottom:100%; left:0; right:0; margin-bottom:6px; background:var(--card); border:1px solid var(--line); border-radius:14px; box-shadow:0 -4px 24px rgba(0,0,0,.12); z-index:50; max-height:280px; display:flex; flex-direction:column; } +.agent-ref-confirm-head { display:flex; align-items:center; justify-content:space-between; padding:10px 14px 6px; border-bottom:1px solid var(--line); } +.agent-ref-confirm-title { font-size:12px; font-weight:600; color:var(--text); } +.agent-ref-confirm-close { width:22px; height:22px; border-radius:6px; border:none; background:none; color:var(--muted); cursor:pointer; display:flex; align-items:center; justify-content:center; } +.agent-ref-confirm-close:hover { color:var(--text); background:var(--soft); } +.agent-ref-confirm-close i,.agent-ref-confirm-close svg { width:13px; height:13px; } +.agent-ref-confirm-body { flex:1; overflow-y:auto; padding:8px 14px; } +.agent-ref-confirm-task { display:flex; align-items:center; gap:8px; padding:6px 0; border-bottom:1px solid var(--line); } +.agent-ref-confirm-task:last-child { border-bottom:none; } +.agent-ref-confirm-task-num { min-width:20px; height:20px; border-radius:10px; background:var(--soft); color:var(--strong); font-size:10px; font-weight:600; line-height:20px; text-align:center; } +.agent-ref-confirm-task-refs { display:flex; gap:3px; flex-wrap:wrap; } +.agent-ref-confirm-task-ref { min-width:18px; height:18px; border-radius:9px; background:rgba(59,130,246,.12); color:#3b82f6; font-size:9px; font-weight:600; line-height:18px; text-align:center; padding:0 4px; } +.agent-ref-confirm-task-prompt { flex:1; font-size:10px; color:var(--muted); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } +.agent-ref-confirm-task-mode { font-size:9px; padding:1px 6px; border-radius:4px; font-weight:500; } +.agent-ref-confirm-task-mode.split { background:rgba(34,197,94,.12); color:#22c55e; } +.agent-ref-confirm-task-mode.combine { background:rgba(249,115,22,.12); color:#f97316; } +.agent-ref-confirm-task-mode.single { background:rgba(99,102,241,.12); color:#6366f1; } +.agent-ref-confirm-actions { display:flex; gap:8px; padding:8px 14px 10px; border-top:1px solid var(--line); } +.agent-ref-confirm-btn { flex:1; height:32px; border-radius:8px; border:none; font-size:12px; font-weight:500; cursor:pointer; transition:opacity .14s ease; } +.agent-ref-confirm-btn.primary { background:var(--strong); color:#fff; } +.agent-ref-confirm-btn.primary:hover { opacity:.88; } +.agent-ref-confirm-btn.secondary { background:var(--soft); color:var(--text); } +.agent-ref-confirm-btn.secondary:hover { opacity:.88; } .agent-onebox textarea { display:block; width:100%; min-width:0; resize:none; max-height:200px; border:none; background:transparent; color:var(--text); padding:8px 12px; font-size:11.5px; line-height:1.5; outline:none; font-family:inherit; } .agent-toolbar { display:flex; align-items:center; gap:4px; padding:4px 8px 6px; } .agent-toolbar-icon { width:28px; height:28px; border-radius:8px; display:inline-flex; align-items:center; justify-content:center; border:none; background:none; color:var(--muted); cursor:pointer; transition:color .14s ease, background .14s ease; } diff --git a/static/enhance.html b/static/enhance.html index 29cd8eb5a..5bdd2ddb8 100644 --- a/static/enhance.html +++ b/static/enhance.html @@ -17,14 +17,14 @@ } catch(e) {} })(); - - - - - - + + + + + + - + diff --git a/static/gpt-chat.html b/static/gpt-chat.html index c0f88b843..8e262772a 100644 --- a/static/gpt-chat.html +++ b/static/gpt-chat.html @@ -16,13 +16,13 @@ } catch(e) {} })(); - - - - - + + + + + - + diff --git a/static/online.html b/static/online.html index 1491f14ec..c76de242d 100644 --- a/static/online.html +++ b/static/online.html @@ -16,14 +16,14 @@ } catch(e) {} })(); - - - - - - + + + + + + - + - + From 1d301984db53282678244a7f3a643f74faa0afce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Mon, 20 Jul 2026 23:18:47 +0800 Subject: [PATCH 58/67] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=80=9D?= =?UTF-8?q?=E7=BB=B4=E6=A8=A1=E5=BC=8FJSON=E8=A7=A3=E6=9E=90=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=E5=92=8C=E9=9D=9E=E6=80=9D=E7=BB=B4=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E5=8F=91=E9=80=81=E6=8C=89=E9=92=AE=E4=B8=8D=E5=8F=AF=E7=82=B9?= =?UTF-8?q?=E5=87=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/smart-canvas.css | 1 + static/js/smart-canvas.js | 137 +++++++++++++++++++++++++++++++++--- static/smart-canvas.html | 4 +- 3 files changed, 132 insertions(+), 10 deletions(-) diff --git a/static/css/smart-canvas.css b/static/css/smart-canvas.css index 6807de974..cc5fb0e1e 100644 --- a/static/css/smart-canvas.css +++ b/static/css/smart-canvas.css @@ -1332,6 +1332,7 @@ input[type=range].smart-range::-webkit-slider-thumb { -webkit-appearance:none; w .agent-attach-chip.drop-before { box-shadow:-2px 0 0 0 var(--strong); } .agent-attach-chip.drop-after { box-shadow:2px 0 0 0 var(--strong); } .agent-ref-confirm { position:absolute; bottom:100%; left:0; right:0; margin-bottom:6px; background:var(--card); border:1px solid var(--line); border-radius:14px; box-shadow:0 -4px 24px rgba(0,0,0,.12); z-index:50; max-height:280px; display:flex; flex-direction:column; } +.agent-ref-confirm[hidden] { display:none; } .agent-ref-confirm-head { display:flex; align-items:center; justify-content:space-between; padding:10px 14px 6px; border-bottom:1px solid var(--line); } .agent-ref-confirm-title { font-size:12px; font-weight:600; color:var(--text); } .agent-ref-confirm-close { width:22px; height:22px; border-radius:6px; border:none; background:none; color:var(--muted); cursor:pointer; display:flex; align-items:center; justify-content:center; } diff --git a/static/js/smart-canvas.js b/static/js/smart-canvas.js index 7e0fe4446..137a4f700 100644 --- a/static/js/smart-canvas.js +++ b/static/js/smart-canvas.js @@ -17912,7 +17912,7 @@ Reply with raw JSON only (no markdown, no extra text): Fields: "reply"=对话回复; "options"=[{label,value}]按钮选项; "collected"=已确认的维度字典; "next_dimension"=下一轮维度; "remaining_dimensions"=剩余维度数组; "prompts"=待确认的中文提示词(仅最终轮返回); "generations"=立即生成的图片(思维模式下始终为空). 所有prompt必须中文,包含主体/风格/构图/光线/色彩/细节/氛围 -文字规则:默认情况下prompt不要包含文字内容(标题、对白、台词、旁白、字幕),只描述画面视觉元素"); +文字规则:默认情况下prompt不要包含文字内容(标题、对白、台词、旁白、字幕),只描述画面视觉元素`); } else { parts.push(AGENT_FORMAT_INSTRUCTION); } @@ -18130,6 +18130,97 @@ function extractJsonBlocks(text){ } return blocks; } +// ★ 修复 LLM 返回的常见 JSON 格式问题 +// 处理:尾随逗号、单引号、未加引号的键、注释、智能引号等 +function repairJsonString(str){ + if(!str || typeof str !== 'string') return str; + let s = str; + // 1. 移除行注释 // ... 和块注释 /* ... */ + s = s.replace(/\/\*[\s\S]*?\*\//g, ''); + // 行注释:只在字符串外移除(简单处理:不匹配引号内的 //) + s = s.replace(/(^|[^:\\])\/\/.*$/gm, '$1'); + // 2. 智能引号 → 普通双引号 + s = s.replace(/[\u201c\u201d\u201e\u201f]/g, '"'); + s = s.replace(/[\u2018\u2019\u201a\u201b]/g, "'"); + // 3. 单引号字符串 → 双引号字符串(仅对键值对中的值) + // 匹配 : '...' 或 : '...,' 模式 + s = s.replace(/:\s*'([^']*)'/g, ': "$1"'); + // 4. 未加引号的键 → 加引号(匹配 { key: 或 , key: 模式,key 为字母/数字/下划线) + s = s.replace(/([{,]\s*)([a-zA-Z_][a-zA-Z0-9_]*)\s*:/g, '$1"$2":'); + // 5. 尾随逗号(} 或 ] 前的逗号) + s = s.replace(/,(\s*[}\]])/g, '$1'); + // 6. 处理字符串内未转义的换行符(JSON 标准不允许字符串内有 literal newline) + // 将字符串值中的 literal \n \r \t 替换为转义形式 + s = s.replace(/"((?:[^"\\]|\\.)*)"/g, (match, inner) => { + // inner 是字符串内容(已处理转义) + // 如果包含 literal newline,替换为 \n + const fixed = inner.replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t'); + return '"' + fixed + '"'; + }); + return s; +} +// ★ 用正则从原始文本中提取 JSON 字段(最后兜底) +function extractFieldsWithRegex(text){ + const result = { reply:'', options:[], prompts:[], generations:[], collected:{}, next_dimension:'', remaining_dimensions:[] }; + // 提取 reply + const replyMatch = text.match(/"reply"\s*:\s*"((?:[^"\\]|\\.)*)"/); + if(replyMatch){ + try { result.reply = JSON.parse('"' + replyMatch[1] + '"'); } catch(e){ result.reply = replyMatch[1]; } + } + // 提取 options(简单提取 label/value 对) + const optionsMatch = text.match(/"options"\s*:\s*\[([\s\S]*?)\]/); + if(optionsMatch){ + const optRe = /"label"\s*:\s*"((?:[^"\\]|\\.)*)"\s*,\s*"value"\s*:\s*"((?:[^"\\]|\\.)*)"/g; + let m; + while((m = optRe.exec(optionsMatch[1])) !== null){ + try { + const label = JSON.parse('"' + m[1] + '"'); + const value = JSON.parse('"' + m[2] + '"'); + result.options.push({label, value}); + } catch(e){ + result.options.push({label:m[1], value:m[2]}); + } + } + } + // 提取 prompts + const promptsMatch = text.match(/"prompts"\s*:\s*\[([\s\S]*?)\]/); + if(promptsMatch){ + const promptRe = /"prompt"\s*:\s*"((?:[^"\\]|\\.)*)"/g; + let m; + while((m = promptRe.exec(promptsMatch[1])) !== null){ + try { + const prompt = JSON.parse('"' + m[1] + '"'); + result.prompts.push({prompt, count:1, use_last_outputs:false, use_attachments:false, status:'pending'}); + } catch(e){ + result.prompts.push({prompt:m[1], count:1, use_last_outputs:false, use_attachments:false, status:'pending'}); + } + } + } + // 提取 generations + const gensMatch = text.match(/"generations"\s*:\s*\[([\s\S]*?)\]/); + if(gensMatch){ + const genRe = /"prompt"\s*:\s*"((?:[^"\\]|\\.)*)"/g; + let m; + while((m = genRe.exec(gensMatch[1])) !== null){ + try { + const prompt = JSON.parse('"' + m[1] + '"'); + result.generations.push({prompt, count:1, use_last_outputs:false, use_attachments:false, results:[], status:'running'}); + } catch(e){ + result.generations.push({prompt:m[1], count:1, use_last_outputs:false, use_attachments:false, results:[], status:'running'}); + } + } + } + // 提取 next_dimension + const ndMatch = text.match(/"next_dimension"\s*:\s*"((?:[^"\\]|\\.)*)"/); + if(ndMatch) result.next_dimension = ndMatch[1]; + // 提取 remaining_dimensions + const rdMatch = text.match(/"remaining_dimensions"\s*:\s*\[([\s\S]*?)\]/); + if(rdMatch){ + const items = rdMatch[1].match(/"([^"]+)"/g); + if(items) result.remaining_dimensions = items.map(s => s.replace(/^"|"$/g, '')); + } + return result; +} function parseAgentResponse(raw, lastUserText){ const text = String(raw || '').trim(); const candidates = [text]; @@ -18150,9 +18241,13 @@ function parseAgentResponse(raw, lastUserText){ // 先解析所有可成功的 JSON 候选,再按优先级选择最合适的一个 const parsedCandidates = []; for(const candidate of candidates){ + // 先尝试直接解析,失败后尝试修复再解析 + let data = null; + try { data = JSON.parse(candidate); } catch(e1) { + try { data = JSON.parse(repairJsonString(candidate)); } catch(e2) { /* 尝试下一个候选 */ } + } + if(!data || typeof data !== 'object') continue; try { - const data = JSON.parse(candidate); - if(!data || typeof data !== 'object') continue; const reply = typeof data.reply === 'string' ? data.reply : (typeof data.text === 'string' ? data.text : ''); let options = (Array.isArray(data.options) ? data.options : []) .filter(o => o && typeof o.label === 'string' && typeof o.value === 'string') @@ -18201,7 +18296,18 @@ function parseAgentResponse(raw, lastUserText){ return parsedCandidates[0]; } // JSON 解析失败时的 fallback 链 - console.error('[parseAgentResponse] JSON 解析失败,原始文本:', text.slice(0, 500)); + console.warn('[parseAgentResponse] JSON.parse 失败,尝试 fallback 提取,原始文本:', text.slice(0, 500)); + // ★ 先尝试用正则从原始文本中提取 JSON 字段(兜底) + const regexResult = extractFieldsWithRegex(text); + const hasRegexContent = regexResult.reply || regexResult.options.length > 0 || regexResult.prompts.length > 0 || regexResult.generations.length > 0; + if(hasRegexContent){ + // 自动追加自定义输入选项 + if(regexResult.options.length > 0 && regexResult.options.length < 8 && !regexResult.options.some(o => o.value === 'CUSTOM_INPUT')){ + regexResult.options.push({label:'自定义输入', value:'CUSTOM_INPUT'}); + } + console.info('[parseAgentResponse] 正则提取成功:', {options:regexResult.options.length, prompts:regexResult.prompts.length, generations:regexResult.generations.length}); + return regexResult; + } const numberedFallback = extractNumberedOptions(text); if(numberedFallback){ const fallbackOptions = numberedFallback.options || []; @@ -18391,10 +18497,25 @@ if(!Array.isArray(parsed.generations)) parsed.generations = []; // 检查 reply 是否包含 JSON 标记(说明解析失败了,但 LLM 确实返回了结构化数据) const replyLooksLikeJson = parsed.reply && (parsed.reply.includes('"reply"') || parsed.reply.includes('"options"') || parsed.reply.trim().startsWith('{')); if(replyLooksLikeJson){ - // 解析失败但 LLM 返回了 JSON:不创建 prompt,显示错误让用户重试 - console.error('[thinkingMode] LLM 返回了 JSON 但解析失败,reply:', parsed.reply?.slice(0, 200)); - parsed.reply = '⚠️ AI 返回了结构化数据但格式异常,请重试或换个描述。'; - parsed.options = []; + // 解析失败但 LLM 返回了 JSON:尝试从 reply 文本中提取有用信息 + console.warn('[thinkingMode] LLM 返回了 JSON 但 JSON.parse 和正则提取均失败,尝试最后兜底,reply:', parsed.reply?.slice(0, 200)); + // 尝试从 raw reply 中提取 reply 字段的值 + const replyValMatch = parsed.reply.match(/"reply"\s*:\s*"((?:[^"\\]|\\.)*)"/); + if(replyValMatch){ + try { parsed.reply = JSON.parse('"' + replyValMatch[1] + '"'); } catch(e){ parsed.reply = replyValMatch[1]; } + // 提取到了 reply,继续走正常流程创建 prompt + parsed.prompts = [{prompt:text, count:1, use_last_outputs:isModifyRequest, use_attachments:false, status:'pending'}]; + } else { + // 彻底无法提取:给用户友好的提示 + 默认风格选项 + parsed.reply = '抱歉,AI 回复格式异常。请重新描述你的需求,或者选择一个风格方向开始:'; + parsed.options = [ + {label:'水墨风', value:'水墨风'}, + {label:'油画风', value:'油画风'}, + {label:'赛博朋克', value:'赛博朋克'}, + {label:'Q版卡通', value:'Q版卡通'}, + {label:'自定义输入', value:'CUSTOM_INPUT'} + ]; + } } else if(isVagueImageRequest(text) && !isModifyRequest){ // 模糊请求:强制走维度选择 parsed.options = [ diff --git a/static/smart-canvas.html b/static/smart-canvas.html index efd682e29..c5f88f489 100644 --- a/static/smart-canvas.html +++ b/static/smart-canvas.html @@ -20,7 +20,7 @@ - +
@@ -575,6 +575,6 @@
preview
- + From 225453f81204a7b6cfc7d572c5e0c23a87d8126a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Tue, 21 Jul 2026 11:58:18 +0800 Subject: [PATCH 59/67] =?UTF-8?q?feat:=20=E7=94=9F=E6=88=90=E5=9B=BE?= =?UTF-8?q?=E7=BC=96=E5=8F=B7=E5=BC=95=E7=94=A8+=E9=9D=9E=E6=80=9D?= =?UTF-8?q?=E7=BB=B4=E6=A8=A1=E5=BC=8F=E7=9B=B4=E8=BF=9E=E7=94=9F=E5=9B=BE?= =?UTF-8?q?+=E7=BC=96=E5=8F=B7=E7=BF=BB=E8=AF=91=E5=B1=82+UI=E9=87=8D?= =?UTF-8?q?=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 生成图每轮编号(跨卡片连续编号,蓝色方块标记) - 附件编号显示统一索引(生成图数量+附件序号) - 非思维模式下「图X」引用解析→直连生图模型 - parseImageRefTasks 重写:中文数字+底图检测+默认不拆分 - 编号翻译层:图N→第X张参考图+角色说明注入 - 参考图上限 20→10 + 超限 toast 提示 - UI:理解模型移到模型面板(始终可见) - UI:思维按钮改纯开关(ON=深度创作 OFF=快速执行) - 附件编号改蓝色方块+删除按钮hover显示 --- main.py | 2 +- static/css/smart-canvas.css | 7 +- static/index.html | 20 +-- static/js/smart-canvas.js | 247 ++++++++++++++++++++++-------------- static/smart-canvas.html | 24 ++-- 5 files changed, 176 insertions(+), 124 deletions(-) diff --git a/main.py b/main.py index db9cd0638..2b52f3a7c 100755 --- a/main.py +++ b/main.py @@ -632,7 +632,7 @@ def load_env_file(): VIDEO_PROMPT_MAX_LENGTH = int(os.getenv("VIDEO_PROMPT_MAX_LENGTH", "4000")) LLM_MESSAGE_MAX_LENGTH = int(os.getenv("LLM_MESSAGE_MAX_LENGTH", "20000")) CHAT_ATTACHMENT_MAX = int(os.getenv("CHAT_ATTACHMENT_MAX", "20")) -ONLINE_IMAGE_REFERENCE_MAX = int(os.getenv("ONLINE_IMAGE_REFERENCE_MAX", "20")) +ONLINE_IMAGE_REFERENCE_MAX = int(os.getenv("ONLINE_IMAGE_REFERENCE_MAX", "10")) def provider_max_reference_images(provider): """返回当前 provider 的生图参考图上限。 diff --git a/static/css/smart-canvas.css b/static/css/smart-canvas.css index cc5fb0e1e..08aa6de05 100644 --- a/static/css/smart-canvas.css +++ b/static/css/smart-canvas.css @@ -1306,6 +1306,8 @@ input[type=range].smart-range::-webkit-slider-thumb { -webkit-appearance:none; w .agent-msg.assistant .agent-msg-bubble { background:var(--card); color:var(--text); border:1px solid var(--line); border-bottom-left-radius:5px; } .agent-msg-thumbs { display:grid; grid-template-columns:repeat(3, 1fr); gap:5px; max-width:240px; } .agent-msg-thumbs img { width:100%; aspect-ratio:1/1; object-fit:cover; border-radius:9px; border:1px solid var(--line); cursor:pointer; display:block; } +.agent-gen-thumb-wrap { position:relative; } +.agent-gen-img-num { position:absolute; top:3px; left:3px; min-width:15px; height:15px; border-radius:4px; background:var(--strong); color:#fff; font-size:9px; font-weight:600; line-height:15px; text-align:center; padding:0 3px; z-index:2; pointer-events:none; } .agent-gen-card { width:250px; max-width:100%; border-radius:12px; border:1px solid var(--line); background:var(--card); padding:8px 10px; display:flex; flex-direction:column; gap:6px; } .agent-gen-prompt { font-size:10px; color:var(--muted); line-height:1.45; position:relative; } .agent-gen-prompt-collapsed { display:-webkit-box; -webkit-line-clamp:5; -webkit-box-orient:vertical; overflow:hidden; } @@ -1325,8 +1327,9 @@ input[type=range].smart-range::-webkit-slider-thumb { -webkit-appearance:none; w .agent-attach-chip { position:relative; width:44px; height:44px; border-radius:10px; overflow:hidden; border:1px solid var(--line); cursor:pointer; transition:border-color .14s ease; } .agent-attach-chip:hover { border-color:var(--strong); } .agent-attach-chip img { width:100%; height:100%; object-fit:cover; display:block; } -.agent-att-num { position:absolute; top:2px; left:2px; min-width:14px; height:14px; border-radius:7px; background:rgba(15,23,42,.82); color:#fff; font-size:9px; font-weight:600; line-height:14px; text-align:center; padding:0 3px; z-index:2; pointer-events:none; } -.agent-attach-chip button { position:absolute; top:2px; right:2px; width:15px; height:15px; border-radius:50%; background:rgba(15,23,42,.78); color:#fff; display:flex; align-items:center; justify-content:center; border:none; cursor:pointer; padding:0; } +.agent-att-num { position:absolute; top:2px; left:2px; min-width:14px; height:14px; border-radius:4px; background:var(--strong); color:#fff; font-size:9px; font-weight:600; line-height:14px; text-align:center; padding:0 3px; z-index:2; pointer-events:none; } +.agent-attach-chip button { position:absolute; top:2px; right:2px; width:15px; height:15px; border-radius:50%; background:rgba(15,23,42,.78); color:#fff; display:flex; align-items:center; justify-content:center; border:none; cursor:pointer; padding:0; opacity:0; transition:opacity .12s ease; } +.agent-attach-chip:hover button { opacity:1; } .agent-attach-chip button i,.agent-attach-chip button svg { width:9px; height:9px; } .agent-attach-chip.dragging { opacity:.4; } .agent-attach-chip.drop-before { box-shadow:-2px 0 0 0 var(--strong); } diff --git a/static/index.html b/static/index.html index f43a3a67f..ca5bd8e11 100644 --- a/static/index.html +++ b/static/index.html @@ -1672,16 +1672,16 @@
- - - - - - - - - - + + + + + + + + + +
diff --git a/static/js/smart-canvas.js b/static/js/smart-canvas.js index 137a4f700..cc71725c8 100644 --- a/static/js/smart-canvas.js +++ b/static/js/smart-canvas.js @@ -17,7 +17,7 @@ const fileInput = document.getElementById('fileInput'); const apiKindToggle = document.getElementById('apiKindToggle'); const inputThumbsRow = document.getElementById('inputThumbsRow'); const SMART_UPLOAD_MAX = 20; -const SMART_REFERENCE_IMAGE_MAX = 20; +const SMART_REFERENCE_IMAGE_MAX = 10; function providerMaxReferenceImages(providerId){ const p = apiProviderById(providerId); return Number(p?.max_reference_images) > 0 ? Number(p.max_reference_images) : SMART_REFERENCE_IMAGE_MAX; @@ -17142,71 +17142,93 @@ function isVagueImageRequest(text){ } // ★ 纯文字引导解析器:从用户输入中识别图N引用,判断拆分/组合模式 // 返回 { mode, tasks } 或 null(无图引用时) -// mode: 'split'(每张独立出图) | 'combine'(多图合成一张) | 'single'(单任务) +// mode: 'split'(每张独立出图) | 'single'(单任务,全发) // tasks: [{ prompt, attachment_indices(0-based) }] +// 核心原则:默认不拆分(全发给模型理解),只有明确的批量独立操作才拆分 function parseImageRefTasks(text, attachCount){ if(!text || !attachCount || attachCount === 0) return null; + // 0. 中文数字转阿拉伯(图一→图1,图十→图10) + const cnNumMap = {'一':'1','二':'2','三':'3','四':'4','五':'5','六':'6','七':'7','八':'8','九':'9','十':'10'}; + let t = text.replace(/图\s*([一二三四五六七八九十])/g, (match, cn) => `图${cnNumMap[cn] || cn}`); + const uniqueRefs = new Set(); - const consumedRanges = []; // {start, end} 文本索引范围,避免重复匹配 + const refOccurrences = {}; // 每张图被引用的次数 + const consumedRanges = []; + function addRef(num){ if(num >= 1 && num <= attachCount){ uniqueRefs.add(num); refOccurrences[num] = (refOccurrences[num] || 0) + 1; } } + // 1. 范围:图1到图7, 图1至7, 图1-4, 图1~4 const rangeRe = /图\s*(\d+)\s*[到至\-~]\s*图?\s*(\d+)/g; let m; - while((m = rangeRe.exec(text)) !== null){ + while((m = rangeRe.exec(t)) !== null){ const lo = Math.min(parseInt(m[1]), parseInt(m[2])); const hi = Math.max(parseInt(m[1]), parseInt(m[2])); - for(let i = lo; i <= hi; i++){ if(i >= 1 && i <= attachCount) uniqueRefs.add(i); } + for(let i = lo; i <= hi; i++) addRef(i); consumedRanges.push({start:m.index, end:m.index + m[0].length}); } // 2. 列表和单个:图1、2、3 或 图1 const listRe = /图\s*(\d+)((?:\s*[、,,和与]\s*\d+)*)/g; - while((m = listRe.exec(text)) !== null){ + while((m = listRe.exec(t)) !== null){ const ms = m.index, me = m.index + m[0].length; if(consumedRanges.some(r => ms >= r.start && me <= r.end)) continue; - const first = parseInt(m[1]); - if(first >= 1 && first <= attachCount) uniqueRefs.add(first); - if(m[2]){ - const restNums = m[2].match(/\d+/g); - if(restNums) restNums.forEach(n => { const num = parseInt(n); if(num >= 1 && num <= attachCount) uniqueRefs.add(num); }); - } - } - // "前面N张" / "前N张" - const frontRe = /前(?:面)?\s*(\d+)\s*张/; - const frontMatch = text.match(frontRe); - if(frontMatch){ - const n = parseInt(frontMatch[1]); - for(let i = 1; i <= Math.min(n, attachCount); i++) uniqueRefs.add(i); + addRef(parseInt(m[1])); + if(m[2]){ const restNums = m[2].match(/\d+/g); if(restNums) restNums.forEach(n => addRef(parseInt(n))); } } + // 3. "前面N张" / "前N张" + const frontMatch = t.match(/前(?:面)?\s*(\d+)\s*张/); + if(frontMatch){ const n = parseInt(frontMatch[1]); for(let i = 1; i <= Math.min(n, attachCount); i++) addRef(i); } + if(uniqueRefs.size === 0) return null; const allRefs = Array.from(uniqueRefs).sort((a, b) => a - b); - // 3. 识别公共参考图:按照图N, 用图N, 图N的方式/风格/版式 - const commonRefs = new Set(); - const commonRe1 = /(?:按照|用|参考|参照)\s*图\s*(\d+)/g; - while((m = commonRe1.exec(text)) !== null){ const num = parseInt(m[1]); if(num >= 1 && num <= attachCount) commonRefs.add(num); } - const commonRe2 = /图\s*(\d+)\s*的\s*(?:方式|风格|版式|构图|布局|模板)/g; - while((m = commonRe2.exec(text)) !== null){ const num = parseInt(m[1]); if(num >= 1 && num <= attachCount) commonRefs.add(num); } - // 4. 独立图 = 全部 - 公共 - const independentRefs = allRefs.filter(r => !commonRefs.has(r)); - // 5. 模式判断 - const hasReplace = /替换/.test(text); - if(hasReplace){ - // 组合模式:所有图合成一张 - return { mode:'combine', tasks:[{ prompt:text, attachment_indices:allRefs.map(r => r - 1) }] }; - } - if(commonRefs.size > 0 && independentRefs.length > 0){ - // 拆分模式:每张独立图 + 公共图 - const commonArr = Array.from(commonRefs).sort((a, b) => a - b).map(r => r - 1); - const tasks = independentRefs.map(ref => ({ prompt:text, attachment_indices:[ref - 1, ...commonArr] })); - return { mode:'split', tasks }; + + // 4. 底图检测:仅靠位置/结构模式判断(不靠引用次数,避免公共参考图被误判) + const baseImages = new Set(); + // "图N中/里/的XX位置" 或 "保持图N的XX不变" → 底图 + const baseRe1 = /图\s*(\d+)\s*(?:中|里)\s*(?:的)?(?:左|右|上|下|中间|旁边)/g; + while((m = baseRe1.exec(t)) !== null){ const num = parseInt(m[1]); if(num >= 1 && num <= attachCount) baseImages.add(num); } + const baseRe2 = /保持\s*图\s*(\d+)\s*的/g; + while((m = baseRe2.exec(t)) !== null){ const num = parseInt(m[1]); if(num >= 1 && num <= attachCount) baseImages.add(num); } + const baseRe3 = /图\s*(\d+)\s*的(?:背景|构图|版式|布局|场景|底色)/g; + while((m = baseRe3.exec(t)) !== null){ const num = parseInt(m[1]); if(num >= 1 && num <= attachCount) baseImages.add(num); } + + // 5. 模式判断(默认不拆分,交给模型理解) + const hasCombineHint = /合成一张|合并|拼在一起|组合成|拼接|融合/.test(t); + const hasSplitKeyword = /各出一张|各出|分别|各一张|每张|逐一|逐个|全部重新/.test(t); + + // 规则 1:有底图 → 不拆分(单任务编辑) + if(baseImages.size > 0){ + return { mode:'single', tasks:[{ prompt:text, attachment_indices:allRefs.map(r => r - 1) }] }; + } + // 规则 2:合成关键词 → 不拆分 + if(hasCombineHint){ + return { mode:'single', tasks:[{ prompt:text, attachment_indices:allRefs.map(r => r - 1) }] }; + } + // 规则 3:只有 1 张引用 → 不拆分 + if(allRefs.length <= 1){ + return { mode:'single', tasks:[{ prompt:text, attachment_indices:allRefs.map(r => r - 1) }] }; + } + // 规则 4:多张独立引用 + (有拆分关键词 或 范围引用) → 拆分 + // 识别公共参考图:不在范围引用内的单独编号 = 公共图 + const rangeRefs = new Set(); + const rangeRe2 = /图\s*(\d+)\s*[到至\-~]\s*图?\s*(\d+)/g; + while((m = rangeRe2.exec(t)) !== null){ + const lo = Math.min(parseInt(m[1]), parseInt(m[2])); + const hi = Math.max(parseInt(m[1]), parseInt(m[2])); + for(let i = lo; i <= hi; i++){ if(i >= 1 && i <= attachCount) rangeRefs.add(i); } } - const hasSplitKeyword = /各出一张|各出|分别|各一张|每张/.test(text); - if(hasSplitKeyword && independentRefs.length > 0){ - // 拆分模式(无公共图) - const tasks = independentRefs.map(ref => ({ prompt:text, attachment_indices:[ref - 1] })); + // 独立图 = 范围引用内的图;公共图 = 不在范围内的单独引用 + const independentRefs = allRefs.filter(r => rangeRefs.has(r)); + const commonRefs = allRefs.filter(r => !rangeRefs.has(r)); + + const shouldSplit = (hasSplitKeyword || rangeRefs.size > 1) && independentRefs.length > 1; + if(shouldSplit){ + const commonArr = commonRefs.map(r => r - 1); + const tasks = independentRefs.map(ref => ({ prompt:text, attachment_indices:[ref - 1, ...commonArr] })); return { mode:'split', tasks }; } - // 默认:单任务带所有引用图 + // 默认:不拆分,全发(让模型理解意图) return { mode:'single', tasks:[{ prompt:text, attachment_indices:allRefs.map(r => r - 1) }] }; } + // ★ 确认面板:显示解析后的参考图任务列表,等待用户确认/取消 let _agentRefConfirmResolver = null; function showImageRefConfirmPanel(refTasks){ @@ -17367,7 +17389,7 @@ function renderAgentAttachments(){ html += `
${escapeHtml(skill.name || 'skill.md')}
`; }); attachments.forEach((att, i) => { - html += `
${i + 1}
`; + html += `
${agentLastResults().length + i + 1}
`; }); agentAttachRow.innerHTML = html; if(window.lucide) lucide.createIcons(); @@ -17494,7 +17516,7 @@ async function agentAttachFiles(files){ toast(String(e.message || e).slice(0, 120)); } } -function agentGenCardHtml(gen){ +function agentGenCardHtml(gen, numOffset){ const status = gen.status || 'running'; const statusText = status === 'done' ? tr('smart.agentGenDone') : status === 'error' ? tr('smart.agentGenFail') : tr('smart.agentGenerating'); const refTags = [gen.use_last_outputs ? tr('smart.agentRefLast') : '', gen.use_attachments ? tr('smart.agentRefAttach') : ''].filter(Boolean).join(' · '); @@ -17502,14 +17524,15 @@ function agentGenCardHtml(gen){ const attachIdxTag = (Array.isArray(gen.attachment_indices) && gen.attachment_indices.length > 0) ? `参考图#${gen.attachment_indices.map(i => i + 1).join(',')}` : ''; const fullRefTags = [refTags, attachIdxTag].filter(Boolean).join(' · '); - const thumbs = (gen.results || []).filter(r => r?.url).map((r, i) => ``).join(''); + const thumbs = (gen.results || []).filter(r => r?.url).map((r, i) => `${(numOffset || 0) + i + 1}`).join(''); const promptText = escapeHtml(gen.prompt || ''); const promptHtml = promptText ? `
${promptText}
` : ''; return `
${promptHtml}
${status === 'running' ? '' : ''}${escapeHtml(statusText)}${fullRefTags ? ' · ' + escapeHtml(fullRefTags) : ''}
${status === 'error' && gen.error ? `
${escapeHtml(String(gen.error).slice(0, 160))}
` : ''}${thumbs ? `
${thumbs}
` : ''}
`; } function agentMessageHtml(msg){ const imgs = (msg.images || []).filter(i => i?.url).map(i => ``).join(''); - const gens = (msg.generations || []).map(agentGenCardHtml).join(''); + let _genNumOffset = 0; + const gens = (msg.generations || []).map(g => { const html = agentGenCardHtml(g, _genNumOffset); _genNumOffset += (g.results || []).filter(r => r?.url).length; return html; }).join(''); const actions = msg.text ? `
${msg.role === 'assistant' ? `` : ''}
` : ''; // 结构化 options 字段(仅 assistant 消息,且 generations 为空时显示) const hasGenerations = Array.isArray(msg.generations) && msg.generations.length > 0; @@ -17767,6 +17790,16 @@ function agentLastUserAttachments(){ } return []; } +function agentCurrentImageMap(){ + // 统一编号映射:上一轮生成图(图1~图M) + 当前附件(图M+1~图M+N) + const genResults = agentLastResults(); + const attachments = (agentState?.attachments || []).filter(a => a?.url); + const map = []; + genResults.forEach((r, i) => map.push({num: i + 1, url: r.url, name: r.name || `图${i + 1}`, source: 'gen'})); + const offset = genResults.length; + attachments.forEach((a, i) => map.push({num: offset + i + 1, url: a.url, name: a.name || `图${offset + i + 1}`, source: 'att'})); + return map; +} function agentNewChat(){ if(!agentState) return; // 保存当前对话 @@ -18659,8 +18692,10 @@ async function sendAgentMessage(){ const resolution = agentState.genResolution || '1k'; const count = Math.max(1, Math.min(8, Number(agentState.genCount) || 1)); - // ★ 图片引用解析:检测用户输入中的"图N"引用,构建精确的 attachment_indices - const refTasks = parseImageRefTasks(text, attachments.length); + // ★ 图片引用解析:统一编号(生成图在前 + 附件在后),检测"图N"引用 + const imageMap = agentCurrentImageMap(); + const totalCount = imageMap.length; + const refTasks = totalCount > 0 ? parseImageRefTasks(text, totalCount) : null; let generations = []; let requestedCount = count; const _finalCount = resolveFinalGenCount(text); @@ -18669,27 +18704,25 @@ async function sendAgentMessage(){ // 判断是否是修改请求 const userModifyRe = /改成|换成|转换成|修改为|变成|转为|改为|转成|调整为|修改成|变回|调成|重新画|重画|重新生成|修改一下|改一下|调整一下/i; const isModifyRequest = userModifyRe.test(text); - const useLastOutputs = isModifyRequest; - const useAttachments = attachments.length > 0; + const useLastOutputs = isModifyRequest && !refTasks; + const useAttachments = attachments.length > 0 && !refTasks; if(refTasks && refTasks.tasks.length > 0){ - // ★ 检测到图片引用:弹确认面板 - const confirmed = await showImageRefConfirmPanel(refTasks); - if(!confirmed){ - hideImageRefConfirmPanel(false); - return; // 用户取消 - } - hideImageRefConfirmPanel(true); - // 从确认的任务构建 generations - generations = refTasks.tasks.map(task => ({ - prompt: task.prompt, - count: 1, - use_last_outputs: useLastOutputs, - use_attachments: true, - attachment_indices: task.attachment_indices, - results: [], - status: 'running' - })); + // ★ 检测到图片引用:解析编号对应的实际 URL,直接发给生图模型 + generations = refTasks.tasks.map(task => { + const resolvedRefs = task.attachment_indices + .filter(idx => idx >= 0 && idx < imageMap.length) + .map(idx => ({url: imageMap[idx].url, name: imageMap[idx].name || `图${idx + 1}`})); + return { + prompt: text, + count: 1, + use_last_outputs: false, + use_attachments: false, + direct_refs: resolvedRefs, + results: [], + status: 'running' + }; + }); requestedCount = generations.length; } else { // 正常模式:根据数量构建 generations @@ -19187,8 +19220,12 @@ async function runAgentGenerations(assistantMsg, userMsg){ placeholderNode.runStartedAt = nowMs(); placeholderNode.runTimerHidden = false; let refsForBox = []; - if(gen.use_last_outputs) refsForBox = refsForBox.concat(lastResults); - if(gen.use_attachments) refsForBox = refsForBox.concat(attachRefs); + if(Array.isArray(gen.direct_refs) && gen.direct_refs.length > 0){ + refsForBox = gen.direct_refs.filter(r => r?.url); + } else { + if(gen.use_last_outputs) refsForBox = refsForBox.concat(lastResults); + if(gen.use_attachments) refsForBox = refsForBox.concat(attachRefs); + } refsForBox = imageRefsOnly(refsForBox).slice(0, providerMaxReferenceImages(providerId)); const pendingBox = agentPendingBoxSize(gen.count, {refs: refsForBox}); placeholderNode.w = pendingBox.w; @@ -19211,22 +19248,51 @@ async function runAgentGenerations(assistantMsg, userMsg){ const placeholderId = placeholderNode?.id || null; try { let refs = []; - if(gen.use_last_outputs) refs = refs.concat(lastResults); - if(gen.use_attachments){ - // 如果指定了 attachment_indices,只取对应的附件(0-based 索引) - if(Array.isArray(gen.attachment_indices) && gen.attachment_indices.length > 0){ - const filtered = gen.attachment_indices - .filter(i => i >= 0 && i < attachRefs.length) - .map(i => attachRefs[i]) - .filter(Boolean); - refs = refs.concat(filtered); - } else { - refs = refs.concat(attachRefs); + if(Array.isArray(gen.direct_refs) && gen.direct_refs.length > 0){ + // ★ 统一编号引用:直接使用预解析的参考图 URL + refs = gen.direct_refs.filter(r => r?.url); + } else { + if(gen.use_last_outputs) refs = refs.concat(lastResults); + if(gen.use_attachments){ + // 如果指定了 attachment_indices,只取对应的附件(0-based 索引) + if(Array.isArray(gen.attachment_indices) && gen.attachment_indices.length > 0){ + const filtered = gen.attachment_indices + .filter(i => i >= 0 && i < attachRefs.length) + .map(i => attachRefs[i]) + .filter(Boolean); + refs = refs.concat(filtered); + } else { + refs = refs.concat(attachRefs); + } } } const _agentRefMax = providerMaxReferenceImages(providerId); - refs = imageRefsOnly(refs).slice(0, _agentRefMax).map(r => ({url:r.url, name:r.name || 'ref'})); - const payload = {prompt:gen.prompt, provider_id:providerId, model:genModel, size, quality:agentState.genQuality || 'auto', n:1, reference_images:refs}; + const _allImageRefs = imageRefsOnly(refs); + if(_allImageRefs.length > _agentRefMax){ toast(`参考图超出上限(最多${_agentRefMax}张),已截取前${_agentRefMax}张`); } + refs = _allImageRefs.slice(0, _agentRefMax).map(r => ({url:r.url, name:r.name || 'ref'})); + // ★ 编号翻译层:将 prompt 中的"图N/图一"替换为"第X张参考图",注入角色说明 + let _finalPrompt = gen.prompt; + if(Array.isArray(gen.direct_refs) && gen.direct_refs.length > 0){ + // 先做中文数字→阿拉伯转换(与 parseImageRefTasks 一致) + const _cnMap = {'一':'1','二':'2','三':'3','四':'4','五':'5','六':'6','七':'7','八':'8','九':'9','十':'10'}; + _finalPrompt = _finalPrompt.replace(/图\s*([一二三四五六七八九十])/g, (m, cn) => `图${_cnMap[cn] || cn}`); + const _imgMap = agentCurrentImageMap(); + const _roleDescs = []; + gen.direct_refs.forEach((ref, idx) => { + const _entry = _imgMap.find(m => m.url === ref.url); + const _origNum = _entry ? _entry.num : null; + if(_origNum){ + const _re = new RegExp('图\\s*' + _origNum + '(?![0-9])', 'g'); + _finalPrompt = _finalPrompt.replace(_re, `第${idx + 1}张参考图`); + } + _roleDescs.push(`第${idx + 1}张`); + }); + // 注入角色说明头(让模型知道参考图数组的顺序含义) + if(_roleDescs.length > 1){ + _finalPrompt = `[参考图顺序:${_roleDescs.join('、')},与下方参考图数组一一对应]\n${_finalPrompt}`; + } + } + const payload = {prompt:_finalPrompt, provider_id:providerId, model:genModel, size, quality:agentState.genQuality || 'auto', n:1, reference_images:refs}; const tasks = await Promise.all(Array.from({length:gen.count}, () => fetch('/api/canvas-image-tasks', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload)}).then(async r => { if(!r.ok) throw new Error(await responseErrorMessage(r, tr('smart.agentGenFail'))); return r.json(); @@ -19638,11 +19704,6 @@ function syncAgentThinkingBtn(){ if(agentThinkingBtn){ agentThinkingBtn.classList.toggle('active', !!agentState?.thinkingMode); } - // 思维模式关闭时收起理解模型下拉面板 - if(!agentState?.thinkingMode){ - const chatModelPanel = document.getElementById('agentChatModelPanel'); - if(chatModelPanel) chatModelPanel.hidden = true; - } } syncAgentThinkingBtn(); agentThinkingBtn?.addEventListener('click', () => { @@ -19650,15 +19711,7 @@ agentThinkingBtn?.addEventListener('click', () => { agentState.thinkingMode = !agentState.thinkingMode; syncAgentThinkingBtn(); saveAgentState(); - toast(agentState.thinkingMode ? '思维模式已开启:扩写/优化提示词' : '思维模式已关闭:直接生成'); - // 开启思维模式时,展开理解模型选择面板(右对齐,避免覆盖发送按钮) - if(agentState.thinkingMode){ - const chatModelPanel = document.getElementById('agentChatModelPanel'); - const thinkingBtn = document.getElementById('agentThinkingBtn'); - if(chatModelPanel && thinkingBtn && window.__agentShowDropdown){ - window.__agentShowDropdown(thinkingBtn, chatModelPanel, {alignRight:true}); - } - } + toast(agentState.thinkingMode ? '思维模式 ON:深度创作(多轮扩写)' : '思维模式 OFF:快速执行(直传生图)'); }); agentInput?.addEventListener('input', () => { const val = agentInput.value; diff --git a/static/smart-canvas.html b/static/smart-canvas.html index c5f88f489..699fcf11b 100644 --- a/static/smart-canvas.html +++ b/static/smart-canvas.html @@ -20,7 +20,7 @@ - +
@@ -246,6 +246,7 @@
@@ -299,19 +300,14 @@
-
- - +
@@ -575,6 +571,6 @@
preview
- + From e4d299aa990d4053fee851232ab7ed8bb6f884b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Tue, 21 Jul 2026 16:00:46 +0800 Subject: [PATCH 60/67] =?UTF-8?q?feat:=20OFF=E6=A8=A1=E5=BC=8F=E9=87=8D?= =?UTF-8?q?=E5=86=99=E4=B8=BALLM=E5=BF=AB=E9=80=9F=E6=89=A7=E8=A1=8C?= =?UTF-8?q?=E5=99=A8+=E5=88=86=E6=9E=90=E9=98=B2=E6=8A=A4+=E4=BB=A3?= =?UTF-8?q?=E7=90=86=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - OFF模式架构重写:删除前端规则引擎,改为1次LLM调用理解意图 - 新增AGENT_OFF_MODE_INSTRUCTION系统提示词(快速执行器角色) - 分析请求防护:前端拦截+系统提示词加强(防止分析时误生图) - 自定义输入按钮bug修复(点击不再直接发送) - CLI子进程代理环境变量继承(env=os.environ+大写补全) - 启动脚本加载.zshrc确保代理变量可用 --- ...5\212\250\346\234\215\345\212\241.command" | 5 + main.py | 10 + static/js/smart-canvas.js | 223 ++++++++++-------- static/smart-canvas.html | 2 +- 4 files changed, 146 insertions(+), 94 deletions(-) diff --git "a/mac-\345\220\257\345\212\250\346\234\215\345\212\241.command" "b/mac-\345\220\257\345\212\250\346\234\215\345\212\241.command" index 23b3971c8..d1fc1fec6 100755 --- "a/mac-\345\220\257\345\212\250\346\234\215\345\212\241.command" +++ "b/mac-\345\220\257\345\212\250\346\234\215\345\212\241.command" @@ -4,6 +4,11 @@ cd "$(dirname "$0")" +# 加载用户 shell 配置(确保代理等环境变量可用) +[ -f "$HOME/.zshrc" ] && source "$HOME/.zshrc" 2>/dev/null +[ -f "$HOME/.bash_profile" ] && source "$HOME/.bash_profile" 2>/dev/null +[ -f "$HOME/.profile" ] && source "$HOME/.profile" 2>/dev/null + echo "============================================" echo " ComfyUI-API-Modelscope" echo "============================================" diff --git a/main.py b/main.py index 2b52f3a7c..16f41dcd2 100755 --- a/main.py +++ b/main.py @@ -4460,6 +4460,7 @@ async def run_codex_cli(prompt, model="", image_paths=None, timeout=None, output proc = await asyncio.create_subprocess_exec( *args, cwd=BASE_DIR, + env=os.environ, stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, @@ -5157,9 +5158,18 @@ async def run_gemini_cli(prompt, model="", timeout=None, allow_tools=False): args.extend(["--prompt", str(prompt or "")]) proc = None try: + # 补全大写代理变量(Go CLI 工具通常只认大写 HTTP_PROXY/HTTPS_PROXY) + _env = dict(os.environ) + if not _env.get('HTTP_PROXY') and _env.get('http_proxy'): + _env['HTTP_PROXY'] = _env['http_proxy'] + if not _env.get('HTTPS_PROXY') and _env.get('https_proxy'): + _env['HTTPS_PROXY'] = _env['https_proxy'] + if not _env.get('ALL_PROXY') and _env.get('all_proxy'): + _env['ALL_PROXY'] = _env['all_proxy'] proc = await asyncio.create_subprocess_exec( *args, cwd=BASE_DIR, + env=_env, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) diff --git a/static/js/smart-canvas.js b/static/js/smart-canvas.js index cc71725c8..520d7ae9f 100644 --- a/static/js/smart-canvas.js +++ b/static/js/smart-canvas.js @@ -16839,6 +16839,29 @@ B. 主体更换(use_last_outputs必须为true): 1. 绝对不要添加用户没要求的内容,特别是:动画帧、序列帧、多角度视图、动作分解、分镜 2. 判断依据:如果新主体和原主体是不同的物体/生物/人物 → 主体更换(B);如果只是改变同一个主体的呈现方式 → 风格修改(A) 3. 无论A还是B,都使用use_last_outputs: true引用原图,区别在于prompt的写法不同`; +// OFF模式系统提示词:快速执行器(理解意图但不改写prompt) +const AGENT_OFF_MODE_INSTRUCTION = `You are a fast image-generation executor. Reply with raw JSON only (no markdown, no extra text): +{"reply":"简短回复","options":[],"generations":[{"prompt":"用户的原始指令或基于上下文的完整指令","count":1,"use_last_outputs":false,"use_attachments":false}]} + +★★★ 核心规则 ★★★ +1. prompt字段:如果用户给了具体的视觉描述,必须原文保留,禁止改写/扩写/翻译/优化。如果用户给的是抽象指令(如"按照分析结果做""按上次风格"),你可以结合上下文生成完整prompt。 +2. 不要提问,不要确认,直接执行。 +3. 理解上下文:记住之前对话中的skill、参考图、分析结果、参数设定。 +4. 图片引用:用户说"图N"时,N对应当前可用图片列表中的编号(系统会在消息中告诉你映射关系)。 +5. 修改/编辑请求("改成""换成""背景换"等)→ 默认1张,use_last_outputs=true。 +6. 新需求没说数量 → 继承上轮数量;上轮也没有 → 用工具栏设置。 +7. ★★★ 分析/描述/反推/看图/识别类请求 → generations必须为空数组[],只在reply中给出文字分析结果。判断标准:用户没有要求"生成/画/做/出图",而是要求"分析/描述/看看/识别/反推/总结/提取"时,绝对不要返回generations。★★★ +8. 所有prompt必须中文,包含主体/风格/构图/光线/色彩/细节/氛围。 +9. 文字规则:默认prompt不含文字内容,除非skill要求或用户明确要求加文字。 +10. 用户上传角色参考图 → 后续保持角色一致性。 +11. 有skill文档时遵循其风格规则,每条prompt完整保留skill描述。 +12. 绝对不要添加用户没要求的内容(动画帧、序列帧、多角度视图等)。 + +修改场景: +A. 风格/属性修改(use_last_outputs=true):只描述要改的+保持其他不变 +B. 主体更换(use_last_outputs=true):"将原图中的[原主体]替换为[新主体],保持场景/背景/构图/光影不变" +通用:不同物体→B;同一主体不同呈现→A`; + let agentOpen = false; let agentSending = false; let agentThinking = false; @@ -17674,6 +17697,12 @@ function renderAgentMessages(){ // 发送文本给 LLM,补全原始请求上下文(带数量信息) const lastUserMsg = [...(agentState.messages || [])].reverse().find(m => m.role === 'user'); const originalRequest = lastUserMsg ? String(lastUserMsg.text || '').trim() : ''; + // 检测"自定义输入"类选项 → 只focus输入框,不发送 + const isCustomInput = /自定义|其他|custom|other|手动/i.test(value) || /自定义|其他|custom|other|手动/i.test(btn.textContent || ''); + if(isCustomInput){ + if(agentInput){ agentInput.value = ''; agentInput.focus(); } + return; + } const sendText = originalRequest && originalRequest !== value ? `${originalRequest},选择:${value}` : value; if(agentInput){ agentInput.value = sendText; @@ -18671,121 +18700,129 @@ async function sendAgentMessage(){ const attachments = (Array.isArray(agentState.attachments) ? agentState.attachments : []).slice(); if(!text && !attachments.length) return; - // ============ 改造:思维模式 OFF 时跳过 LLM,直接生图 ============ + // ============ OFF模式:1次LLM调用(快速执行器) ============ const thinkingModeOn = agentState?.thinkingMode; if(!thinkingModeOn){ - // 思维模式关闭:直接构建 generations 并生图 - const genProviderId = agentState.genProvider; - const genModel = agentState.genModel; - - // 检查生图模型是否配置 - const genProviders = agentGenProviders(); - const genProvider = genProviders.find(p => p.id === genProviderId) || genProviders[0]; - if(!genProvider){ toast(tr('smart.agentNoProviders') || '未配置生图模型'); return; } - - const models = providerImageModels(genProvider.id) || []; - const model = genModel && models.includes(genModel) ? genModel : (models[0] || ''); - if(!model){ toast(tr('smart.agentNoProviders') || '生图模型不可用'); return; } - - // 计算生图参数 - const ratio = agentState.genRatio || 'square'; - const resolution = agentState.genResolution || '1k'; - const count = Math.max(1, Math.min(8, Number(agentState.genCount) || 1)); - - // ★ 图片引用解析:统一编号(生成图在前 + 附件在后),检测"图N"引用 - const imageMap = agentCurrentImageMap(); - const totalCount = imageMap.length; - const refTasks = totalCount > 0 ? parseImageRefTasks(text, totalCount) : null; - let generations = []; - let requestedCount = count; - const _finalCount = resolveFinalGenCount(text); - if(_finalCount.count > 1) requestedCount = _finalCount.count; - - // 判断是否是修改请求 - const userModifyRe = /改成|换成|转换成|修改为|变成|转为|改为|转成|调整为|修改成|变回|调成|重新画|重画|重新生成|修改一下|改一下|调整一下/i; - const isModifyRequest = userModifyRe.test(text); - const useLastOutputs = isModifyRequest && !refTasks; - const useAttachments = attachments.length > 0 && !refTasks; - - if(refTasks && refTasks.tasks.length > 0){ - // ★ 检测到图片引用:解析编号对应的实际 URL,直接发给生图模型 - generations = refTasks.tasks.map(task => { - const resolvedRefs = task.attachment_indices - .filter(idx => idx >= 0 && idx < imageMap.length) - .map(idx => ({url: imageMap[idx].url, name: imageMap[idx].name || `图${idx + 1}`})); - return { - prompt: text, - count: 1, - use_last_outputs: false, - use_attachments: false, - direct_refs: resolvedRefs, - results: [], - status: 'running' - }; + // 检查理解模型是否配置(OFF模式也需要LLM理解意图) + if(!chatApiProviders().length){ toast(tr('smart.agentNeedChatModel') || '请先配置理解模型'); return; } + const chatProvider = resolveChatProviderId(agentState.chatProvider); + const chatModel = resolveChatModel(agentState.chatModel, chatProvider); + agentState.chatProvider = chatProvider; + agentState.chatModel = chatModel; + + // 构建上下文图片(附件 + 上轮生成图 + 上轮附件) + const contextImages = attachments.slice(); + if(agentState.autoContext !== false){ + [...agentLastResults(), ...agentLastUserAttachments()].forEach(item => { + if(item?.url && contextImages.length < AGENT_LLM_IMAGE_MAX && !contextImages.some(i => i.url === item.url)) contextImages.push(item); }); - requestedCount = generations.length; - } else { - // 正常模式:根据数量构建 generations - for(let i = 0; i < Math.max(requestedCount, 1); i++){ - const promptText = requestedCount > 1 ? `${text}(变体${i + 1},不同构图/角度/场景)` : text; - generations.push({ - prompt: promptText, - count: 1, - use_last_outputs: useLastOutputs, - use_attachments: useAttachments, - results: [], - status: 'running' - }); - } } - - // 创建 user 消息 - const userMsg = { - id: uid('am'), - role: 'user', - text, - images: attachments, - ts: Date.now() + + // 构建图片编号映射说明(让LLM知道"图N"对应什么) + const imageMap = agentCurrentImageMap(); + let imageMapDesc = ''; + if(imageMap.length > 0){ + imageMapDesc = '\n\n【当前可用图片编号】\n' + imageMap.map(m => `图${m.num} = ${m.source === 'gen' ? '上一轮生成图' : '用户上传附件'}(${m.name || ''})`).join('\n'); + } + + // 构建消息文本(注入图片映射 + skill提醒) + let messageText = text || '(请分析这些图片)'; + messageText += imageMapDesc; + const _skills = Array.isArray(agentState?.skills) ? agentState.skills : []; + if(_skills.length > 0){ + const skillNames = _skills.map(s => s?.name).filter(Boolean).join('、'); + messageText += `\n\n【Skill提醒】遵循 Skill 文档(${skillNames})的所有样式描述。`; + } + + // 构建LLM请求 + const llmPayload = { + message: messageText, + messages: agentHistoryMessages().slice(0, -1), + images: contextImages.slice(0, AGENT_LLM_IMAGE_MAX).map(i => i.url), + videos: [], + model: chatModel, + provider: chatProvider, + ms_model: chatProvider === 'modelscope' ? chatModel : '', + system_prompt: AGENT_OFF_MODE_INSTRUCTION + (_skills.length > 0 ? '\n\n' + _skills.map(s => `===== Skill: ${s.name} =====\n${s.content || ''}\n===== End =====`).join('\n') : '') }; - if(requestedCount > 1) userMsg.requestedCount = requestedCount; + + // 创建user消息 + const userMsg = {id:uid('am'), role:'user', text, images:attachments, ts:Date.now()}; agentState.messages.push(userMsg); agentState.messages = agentState.messages.slice(-AGENT_MSG_MAX); agentState.attachments = []; if(agentInput) agentInput.value = ''; renderAgentAttachments(); - - // 创建 assistant 消息(直接生图模式) - const assistantMsg = { - id: uid('am'), - role: 'assistant', - text: '', - options: [], - prompts: [], - generations: generations, - ts: Date.now(), - requestedCount: requestedCount - }; - agentState.messages.push(assistantMsg); - agentState.messages = agentState.messages.slice(-AGENT_MSG_MAX); agentSending = true; - saveAgentState(); + agentThinking = true; renderAgentMessages(); - - // 直接执行生图 + saveAgentState(); + try { - await runAgentGenerations(assistantMsg, userMsg); + // 调用LLM(1次,快速理解意图) + const taskRes = await fetch('/api/agent-llm-task', { + method:'POST', headers:{'Content-Type':'application/json'}, + body:JSON.stringify(llmPayload) + }).then(async r => { + if(!r.ok) throw new Error(await responseErrorMessage(r, 'LLM调用失败')); + return r.json(); + }); + const taskId = taskRes.task_id; + if(!taskId) throw new Error('LLM任务创建失败'); + // 轮询LLM结果 + const llmResult = await pollAgentLlmTask(taskId); + agentThinking = false; + + // 解析LLM返回 + const parsed = parseAgentResponse(llmResult.text || '', text); + if(!Array.isArray(parsed.generations)) parsed.generations = []; + if(!Array.isArray(parsed.options)) parsed.options = []; + // ★ 前端防护:分析/描述类请求强制清空generations(防止LLM不遵守规则) + const _analysisRe = /^(分析|描述|看看|识别|反推|总结|提取|解读|评价|对比|比较|说说|告诉我|这是什么|里面有什么|什么风格|什么特点)/i; + const _noGenRe = /(不要生成|不用画|不需要出图|只分析|只描述|只看|别画|别生成)/i; + if((_analysisRe.test(text.trim()) || _noGenRe.test(text)) && !/生成|画一|做一|出一|来一|帮我画|帮我做|帮我生/.test(text)){ + parsed.generations = []; + } + + // 如果LLM返回了generations → 直接执行生图 + if(parsed.generations.length > 0){ + const assistantMsg = {id:uid('am'), role:'assistant', text:parsed.reply || '', options:[], prompts:[], generations:parsed.generations, ts:Date.now()}; + agentState.messages.push(assistantMsg); + agentState.messages = agentState.messages.slice(-AGENT_MSG_MAX); + saveAgentState(); + renderAgentMessages(); + await runAgentGenerations(assistantMsg, userMsg); + } else { + // 纯文字回复(分析/描述/问答) + const assistantMsg = {id:uid('am'), role:'assistant', text:parsed.reply || llmResult.text || '', options:parsed.options || [], prompts:[], generations:[], ts:Date.now()}; + agentState.messages.push(assistantMsg); + agentState.messages = agentState.messages.slice(-AGENT_MSG_MAX); + saveAgentState(); + renderAgentMessages(); + } } catch(e) { - assistantMsg.generations.forEach(g => { g.status = 'failed'; g.error = String(e.message || e); }); - renderAgentMessages(); + agentThinking = false; + // Fallback:LLM失败时直接透传用户原文+全部参考图 + console.warn('[OFF mode] LLM failed, fallback to direct:', e.message); + const fallbackGens = [{prompt:text, count:1, use_last_outputs:false, use_attachments:attachments.length > 0, results:[], status:'running'}]; + const assistantMsg = {id:uid('am'), role:'assistant', text:'', options:[], prompts:[], generations:fallbackGens, ts:Date.now()}; + agentState.messages.push(assistantMsg); + agentState.messages = agentState.messages.slice(-AGENT_MSG_MAX); saveAgentState(); + renderAgentMessages(); + try { await runAgentGenerations(assistantMsg, userMsg); } catch(e2) { + assistantMsg.generations.forEach(g => { g.status = 'failed'; g.error = String(e2.message || e2); }); + renderAgentMessages(); saveAgentState(); + } } finally { agentSending = false; + agentThinking = false; renderAgentMessages(); saveAgentState(); } return; } - // ============ 改造结束 ============ + // ============ OFF模式结束 ============ // 思维模式开启:走原有 LLM 流程 if(!chatApiProviders().length){ toast(tr('smart.agentNeedChatModel')); return; } diff --git a/static/smart-canvas.html b/static/smart-canvas.html index 699fcf11b..8d4294064 100644 --- a/static/smart-canvas.html +++ b/static/smart-canvas.html @@ -571,6 +571,6 @@
preview
- + From 49593abad3276e1e3bf2c66decdd5162fd0cf000 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Tue, 21 Jul 2026 18:44:41 +0800 Subject: [PATCH 61/67] =?UTF-8?q?feat(agent):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E9=9D=9E=E6=80=9D=E7=BB=B4=E6=A8=A1=E5=BC=8F=E6=84=8F=E5=9B=BE?= =?UTF-8?q?=E8=B7=AF=E7=94=B1=E6=9E=B6=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重写 AGENT_OFF_MODE_INSTRUCTION: 支持8种意图(generate/edit/analyze/refine/composite/clarify/meta/cancel) - 新增快速路径: 纯文生图和简单修改跳过LLM直接执行 - 新增 parseAgentIntentRoute: 意图路由JSON解析(含正则回退) - 分析/反推区分: 反推只输出prompt, 分析输出结果+行动引导选项 - 新增 clarify 意图: 意图不明时主动问用户并提供选项 - 新增生成后快捷操作栏: 修改/变体/反推 - 新增分析卡片和提示词建议卡片UI - 流式输出: 后端canvas_llm_stream + WebSocket逐token推送 - 思维模式接入流式(stream=true) - 流式气泡: max-height 200px + 滚动 + 停止按钮 - 最小安全网: 剥离@mention后分析动词强制text_only - mac启动脚本: 添加no_proxy避免Clash劫持本地请求 - 后端: 新增broadcast_agent_llm_token, canvas_llm_stream, stream参数 --- ...57\345\212\250\346\234\215\345\212\241.sh" | 4 + main.py | 84 +++- static/css/smart-canvas.css | 28 ++ static/js/smart-canvas.js | 406 +++++++++++++++--- static/smart-canvas.html | 4 +- 5 files changed, 459 insertions(+), 67 deletions(-) diff --git "a/mac-\345\220\257\345\212\250\346\234\215\345\212\241.sh" "b/mac-\345\220\257\345\212\250\346\234\215\345\212\241.sh" index f57bc1d9d..30f0bfebe 100644 --- "a/mac-\345\220\257\345\212\250\346\234\215\345\212\241.sh" +++ "b/mac-\345\220\257\345\212\250\346\234\215\345\212\241.sh" @@ -18,6 +18,10 @@ echo "" # Open browser after 3 seconds sleep 3 && open "${APP_URL}" & +# 本地代理地址不走系统代理,避免 Clash 等工具劫持 127.0.0.1 请求 +export no_proxy="127.0.0.1,localhost,0.0.0.0" +export NO_PROXY="127.0.0.1,localhost,0.0.0.0" + python3 main.py echo "" diff --git a/main.py b/main.py index 16f41dcd2..7afb313db 100755 --- a/main.py +++ b/main.py @@ -178,6 +178,14 @@ async def broadcast_agent_llm_done(self, task_id: str, status: str): print(f"Broadcast agent llm done error: {e}") self.active_connections.remove(connection) + async def broadcast_agent_llm_token(self, task_id: str, token: str): + data = json.dumps({"type": "agent_llm_token", "task_id": task_id, "token": token}) + for connection in self.active_connections[:]: + try: + await connection.send_text(data) + except Exception: + self.active_connections.remove(connection) + async def send_personal_message(self, message: dict, client_id: str): ws = self.user_connections.get(client_id) if ws: @@ -13552,13 +13560,16 @@ async def run_canvas_image_task(task_id: str, payload: OnlineImageRequest): except Exception: pass -async def run_agent_llm_task(task_id: str, payload: CanvasLLMRequest): +async def run_agent_llm_task(task_id: str, payload: CanvasLLMRequest, stream: bool = False): with AGENT_LLM_TASK_LOCK: if task_id in AGENT_LLM_TASKS: AGENT_LLM_TASKS[task_id]["status"] = "running" AGENT_LLM_TASKS[task_id]["updated_at"] = time.time() try: - result = await canvas_llm(payload) + if stream: + result = await canvas_llm_stream(task_id, payload) + else: + result = await canvas_llm(payload) with AGENT_LLM_TASK_LOCK: AGENT_LLM_TASKS[task_id].update({ "status": "succeeded", @@ -13586,7 +13597,7 @@ async def run_agent_llm_task(task_id: str, payload: CanvasLLMRequest): pass @app.post("/api/agent-llm-task") -async def create_agent_llm_task(payload: CanvasLLMRequest): +async def create_agent_llm_task(payload: CanvasLLMRequest, stream: bool = False): task_id = f"agent_llm_{uuid.uuid4().hex}" with AGENT_LLM_TASK_LOCK: AGENT_LLM_TASKS[task_id] = { @@ -13598,7 +13609,7 @@ async def create_agent_llm_task(payload: CanvasLLMRequest): "result": None, "error": "", } - asyncio.create_task(run_agent_llm_task(task_id, payload)) + asyncio.create_task(run_agent_llm_task(task_id, payload, stream=stream)) return {"task_id": task_id, "status": "queued"} @app.get("/api/agent-llm-task/{task_id}") @@ -14742,6 +14753,71 @@ def append_volcengine_image(url: str, role: str): # --- Canvas LLM --- +async def canvas_llm_stream(task_id: str, payload: CanvasLLMRequest): + """流式调用 LLM,通过 WebSocket 逐 token 广播,最终返回完整结果。""" + _provider = get_api_provider(payload.provider) + # CLI 协议不支持流式,回退为普通调用 + if is_codex_provider(_provider) or is_gemini_cli_provider(_provider): + return await canvas_llm(payload) + chat_base, chat_hdrs, model = resolve_chat_provider(payload.provider, payload.model, payload.ms_model) + _llm_provider = get_api_provider(payload.provider) if payload.provider not in ("modelscope",) else {} + _is_apimart = is_apimart_provider(_llm_provider) + # APIMart 不支持流式,回退 + if _is_apimart: + return await canvas_llm(payload) + system_prompt = (payload.system_prompt or "").strip() + upstream_messages = [{"role": "system", "content": system_prompt}] if system_prompt else [] + for item in payload.messages[-MAX_HISTORY_MESSAGES:]: + role = item.get("role") + content = item.get("content") + if role in {"user", "assistant"} and content: + upstream_messages.append({"role": role, "content": content}) + image_inputs = [img for img in (payload.images or []) if is_image_reference_value(img)] + if image_inputs: + content_parts = [{"type": "text", "text": payload.message}] + for img in image_inputs[:8]: + if not img or not isinstance(img, str): + continue + ref_url = media_reference_to_url(img, max_image_size=1024) + if ref_url: + content_parts.append({"type": "image_url", "image_url": {"url": ref_url}}) + upstream_messages.append({"role": "user", "content": content_parts}) + else: + upstream_messages.append({"role": "user", "content": payload.message}) + # 流式请求 + full_text = "" + try: + async with httpx.AsyncClient(timeout=AI_REQUEST_TIMEOUT) as client: + req_body = {"model": model, "messages": upstream_messages, "stream": True} + async with client.stream("POST", f"{chat_base}/chat/completions", headers=chat_hdrs, json=req_body) as response: + response.raise_for_status() + async for line in response.aiter_lines(): + if not line or not line.startswith("data: "): + continue + data_str = line[6:].strip() + if data_str == "[DONE]": + break + try: + chunk = json.loads(data_str) + delta = (chunk.get("choices") or [{}])[0].get("delta") or {} + token = delta.get("content") or "" + if token: + full_text += token + try: + await manager.broadcast_agent_llm_token(task_id, token) + except Exception: + pass + except (json.JSONDecodeError, IndexError, KeyError): + continue + except httpx.HTTPStatusError as exc: + body = exc.response.text or "" if hasattr(exc.response, 'text') else "" + friendly = friendly_chat_error_detail(body, model, _llm_provider) + raise HTTPException(status_code=exc.response.status_code, detail=friendly or f"上游接口错误:{body[:300]}") from exc + except httpx.HTTPError as exc: + raise HTTPException(status_code=502, detail=f"请求上游接口失败:{exc}") from exc + text = full_text.strip() or "接口返回了空回复。" + return {"text": text, "model": model, "raw_usage": None, "raw": None} + @app.post("/api/canvas-llm") async def canvas_llm(payload: CanvasLLMRequest): _provider = get_api_provider(payload.provider) diff --git a/static/css/smart-canvas.css b/static/css/smart-canvas.css index 08aa6de05..3ba32ed56 100644 --- a/static/css/smart-canvas.css +++ b/static/css/smart-canvas.css @@ -1319,6 +1319,34 @@ input[type=range].smart-range::-webkit-slider-thumb { -webkit-appearance:none; w .agent-gen-status.done { color:#059669; } .agent-gen-spinner { width:12px; height:12px; border-radius:50%; border:2px solid var(--line); border-top-color:var(--strong); animation:agentSpin .8s linear infinite; flex:0 0 auto; } @keyframes agentSpin { to { transform:rotate(360deg); } } + +/* 生成后快捷操作栏 */ +.agent-gen-quick-actions { display:flex; gap:4px; padding-top:4px; border-top:1px solid var(--line); } +.agent-quick-btn { display:inline-flex; align-items:center; gap:3px; height:24px; padding:0 8px; border-radius:6px; border:1px solid var(--line); background:var(--card); color:var(--muted); font-size:10px; font-weight:600; cursor:pointer; transition:all .14s ease; white-space:nowrap; } +.agent-quick-btn:hover { color:var(--text); border-color:var(--strong); background:var(--soft); } +.agent-quick-btn.primary { background:var(--strong); color:var(--strong-text); border-color:var(--strong); } +.agent-quick-btn.primary:hover { opacity:.85; } +.agent-quick-btn i,.agent-quick-btn svg { width:11px; height:11px; } + +/* 分析结果卡片 */ +.agent-analysis-card { width:100%; max-width:320px; border-radius:12px; border:1px solid var(--line); background:var(--card); overflow:hidden; } +.agent-analysis-body { padding:10px 12px; font-size:11px; line-height:1.6; color:var(--text); white-space:pre-wrap; word-break:break-word; max-height:200px; overflow-y:auto; } +.agent-analysis-actions { display:flex; gap:6px; padding:8px 10px; border-top:1px solid var(--line); background:var(--soft); } + +/* 提示词建议卡片 */ +.agent-prompt-suggest-card { width:100%; max-width:320px; border-radius:12px; border:1px solid var(--line); background:var(--card); overflow:hidden; } +.agent-prompt-suggest-body { padding:10px 12px; font-size:11px; line-height:1.6; color:var(--text); white-space:pre-wrap; word-break:break-word; font-family:'SF Mono',Menlo,monospace; max-height:200px; overflow-y:auto; background:var(--soft); } + +/* 降级提示条 */ +.agent-degrade-notice { display:flex; align-items:center; gap:6px; padding:6px 10px; border-radius:8px; background:#fef3c7; border:1px solid #f59e0b; font-size:10px; color:#92400e; margin-bottom:6px; } +.agent-degrade-notice i,.agent-degrade-notice svg { width:12px; height:12px; flex-shrink:0; } + +/* 流式输出气泡 */ +.agent-stream-bubble { position:relative; } +.agent-stream-body { font-size:11.5px; line-height:1.6; white-space:pre-wrap; word-break:break-word; max-height:200px; overflow-y:auto; scroll-behavior:smooth; } +.agent-stream-stop { position:absolute; top:4px; right:4px; width:20px; height:20px; border-radius:5px; border:1px solid var(--line); background:var(--card); color:var(--muted); font-size:9px; cursor:pointer; display:flex; align-items:center; justify-content:center; transition:all .14s ease; } +.agent-stream-stop:hover { color:#e11d48; border-color:#e11d48; background:#fef2f2; } + .agent-input-area { display:flex; flex-direction:column; gap:0; position:relative; } .agent-onebox { border:1px solid var(--line); border-radius:16px; background:var(--card); transition:border-color .14s ease; } .agent-onebox:focus-within { border-color:var(--strong); } diff --git a/static/js/smart-canvas.js b/static/js/smart-canvas.js index 520d7ae9f..214dd77a9 100644 --- a/static/js/smart-canvas.js +++ b/static/js/smart-canvas.js @@ -5290,6 +5290,7 @@ function connectAssetLibrarySyncSocket(){ if(data?.type === 'canvas_updated') handleCanvasUpdatedMessage(data); if(data?.type === 'canvas_task_done') handleCanvasTaskDoneMessage(data); if(data?.type === 'agent_llm_done') handleAgentLlmDoneMessage(data); + if(data?.type === 'agent_llm_token') handleAgentLlmTokenMessage(data); } catch(e) {} }; socket.onclose = () => { @@ -15242,13 +15243,68 @@ function handleCanvasTaskDoneMessage(data){ } // Agent LLM 任务 WebSocket 通知 const _agentLlmWaiters = new Map(); +// 流式输出状态 +let _agentStreamTaskId = null; +let _agentStreamText = ''; +let _agentStreamCancelled = false; function handleAgentLlmDoneMessage(data){ const taskId = data?.task_id; const status = data?.status; if(!taskId) return; + // 流式完成:清除流式状态 + if(taskId === _agentStreamTaskId){ + _agentStreamTaskId = null; + } const waiter = _agentLlmWaiters.get(taskId); if(waiter) waiter(status || 'done'); } +function handleAgentLlmTokenMessage(data){ + const taskId = data?.task_id; + const token = data?.token; + if(!taskId || !token) return; + if(taskId !== _agentStreamTaskId) return; + if(_agentStreamCancelled) return; + _agentStreamText += token; + // 更新流式气泡 UI + renderAgentStreamBubble(); +} +function renderAgentStreamBubble(){ + if(!agentMessages) return; + let bubble = agentMessages.querySelector('.agent-stream-bubble'); + if(!bubble){ + // 创建流式气泡 + const msgDiv = document.createElement('div'); + msgDiv.className = 'agent-msg assistant'; + msgDiv.innerHTML = `
`; + agentMessages.appendChild(msgDiv); + bubble = msgDiv.querySelector('.agent-stream-bubble'); + // 停止按钮 + bubble.querySelector('.agent-stream-stop').onclick = () => { + _agentStreamCancelled = true; + }; + } + const body = bubble.querySelector('.agent-stream-body'); + if(body){ + body.textContent = _agentStreamText; + body.scrollTop = body.scrollHeight; + } + agentMessages.scrollTop = agentMessages.scrollHeight; +} +function startAgentStream(taskId){ + _agentStreamTaskId = taskId; + _agentStreamText = ''; + _agentStreamCancelled = false; +} +function endAgentStream(){ + // 移除流式气泡(最终结果由正常流程渲染) + if(agentMessages){ + const streamMsg = agentMessages.querySelector('.agent-stream-bubble')?.closest('.agent-msg'); + if(streamMsg) streamMsg.remove(); + } + _agentStreamTaskId = null; + _agentStreamText = ''; + _agentStreamCancelled = false; +} async function pollAgentLlmTask(taskId){ if(!taskId) throw new Error('Invalid task ID'); const startTime = Date.now(); @@ -16840,27 +16896,73 @@ B. 主体更换(use_last_outputs必须为true): 2. 判断依据:如果新主体和原主体是不同的物体/生物/人物 → 主体更换(B);如果只是改变同一个主体的呈现方式 → 风格修改(A) 3. 无论A还是B,都使用use_last_outputs: true引用原图,区别在于prompt的写法不同`; // OFF模式系统提示词:快速执行器(理解意图但不改写prompt) -const AGENT_OFF_MODE_INSTRUCTION = `You are a fast image-generation executor. Reply with raw JSON only (no markdown, no extra text): -{"reply":"简短回复","options":[],"generations":[{"prompt":"用户的原始指令或基于上下文的完整指令","count":1,"use_last_outputs":false,"use_attachments":false}]} +const AGENT_OFF_MODE_INSTRUCTION = `你是一个图像生成 Agent 的意图决策器。分析用户输入+上下文,决定下一步行动。仅返回原始 JSON,不要 markdown。 + +【输出格式】 +{"intent":"...","reply":"...","prompts":[],"use_attachments":false,"attachment_roles":{},"use_last_outputs":false,"text_only":false,"analysis":"","options":[]} + +【intent 类型】 +- generate: 用户想生成新图(给了视觉描述或生图指令) +- edit: 用户想修改已有的图("改成/换成/调整/加/去掉") +- analyze: 用户想分析/反推/描述/识别图片(不生图) +- refine: 用户想扩写/优化/翻译提示词(不生图) +- composite: 多参考图精确分配("用图1换左边,图2换右边") +- clarify: 意图不明,需要问用户确认(用户只发了图没说要干什么,或表述模糊) +- meta: 用户询问 Agent 自身信息("你用了什么prompt""支持什么模型""你能做什么") +- cancel: 用户想取消/不要了("算了""取消""不要了""停") -★★★ 核心规则 ★★★ -1. prompt字段:如果用户给了具体的视觉描述,必须原文保留,禁止改写/扩写/翻译/优化。如果用户给的是抽象指令(如"按照分析结果做""按上次风格"),你可以结合上下文生成完整prompt。 -2. 不要提问,不要确认,直接执行。 -3. 理解上下文:记住之前对话中的skill、参考图、分析结果、参数设定。 -4. 图片引用:用户说"图N"时,N对应当前可用图片列表中的编号(系统会在消息中告诉你映射关系)。 -5. 修改/编辑请求("改成""换成""背景换"等)→ 默认1张,use_last_outputs=true。 -6. 新需求没说数量 → 继承上轮数量;上轮也没有 → 用工具栏设置。 -7. ★★★ 分析/描述/反推/看图/识别类请求 → generations必须为空数组[],只在reply中给出文字分析结果。判断标准:用户没有要求"生成/画/做/出图",而是要求"分析/描述/看看/识别/反推/总结/提取"时,绝对不要返回generations。★★★ -8. 所有prompt必须中文,包含主体/风格/构图/光线/色彩/细节/氛围。 -9. 文字规则:默认prompt不含文字内容,除非skill要求或用户明确要求加文字。 -10. 用户上传角色参考图 → 后续保持角色一致性。 -11. 有skill文档时遵循其风格规则,每条prompt完整保留skill描述。 -12. 绝对不要添加用户没要求的内容(动画帧、序列帧、多角度视图等)。 +【字段说明】 +- reply: 给用户的简短回复 +- prompts: 生图 prompt 数组。空[]=用用户原文直接生图(你不改写) +- use_attachments: 是否携带参考图 +- attachment_roles: 每张参考图角色 {"0":"style","1":"content","2":"target"} +- use_last_outputs: 是否携带上一轮生成结果 +- text_only: true=纯文本回复,不生图 +- analysis: 分析/扩写结果(text_only=true 时填写) +- options: clarify 时给用户的选项 [{"label":"...","value":"..."}] + +【核心规则】 +1. ★ 不要改写用户的视觉描述。用户给了具体描述时 prompts=[](用原文生图)。只有抽象指令("按分析结果做""按上次风格")时才生成 prompt。 +2. ★ 分析/反推/描述/识别/对比类 → intent=analyze, text_only=true。判断标准:用户要求"分析/描述/反推/识别/总结/提取/对比/看看/这是什么"且没有要求生图。 +3. ★ 提示词扩写/优化/翻译 → intent=refine, text_only=true。 +4. 修改/编辑 → intent=edit, use_last_outputs=true, prompts只写修改指令。 +5. 意图不明(用户只发了图没说要干什么,或表述模糊无法判断)→ intent=clarify,在 reply 中提问,在 options 中给 2-4 个选项。 +6. 用户询问 Agent 信息 → intent=meta, text_only=true。 +7. 用户取消 → intent=cancel, text_only=true, reply="好的,已取消。" +8. 图片引用:"图N" 对应系统提供的图片编号。 +9. 多参考图精确分配 → intent=composite, prompts写完整结构化指令。 +10. 所有 prompt 必须中文。默认不含文字内容。 +11. 有 skill 文档时遵循其风格规则。 +12. 绝对不要添加用户没要求的内容。 +13. 角色参考图 → 后续保持角色一致性。 + +【修改场景】 +A. 风格/属性修改:use_last_outputs=true,prompt只描述要改的+保持其他不变 +B. 主体更换:use_last_outputs=true,"将原图中的[原主体]替换为[新主体],保持场景/背景/构图/光影不变" + +【analyze 输出规则】 +★ 区分"反推"和"分析": +- 用户说"反推/反推提示词" → 只输出 prompt,options=[],不引导后续操作 +- 用户说"分析/看看/对比" → 输出分析 + options(行动引导) + +反推提示词格式(options=[]): +主体:... +风格:... +构图:... +配色:... +光线:... +氛围:... +完整Prompt:(可直接用于生图的一段完整描述) + +分析格式(options非空,引导用户下一步): +analysis 中写分析结果,同时在 options 中给出 2-4 个后续行动建议。 +示例: +{"intent":"analyze","reply":"分析完成","text_only":true,"analysis":"图1:水墨风格,留白构图,冷色调...\n图2:油画风格,居中构图,暖色调...","options":[{"label":"用图1的风格画新内容","value":"用图1的风格画一张新图"},{"label":"融合两张图的优点","value":"融合图1和图2的优点生成新图"},{"label":"改进某张图","value":"帮我改进图2的构图"}]} + +【clarify 示例】 +用户发了一张图但没说要干什么: +{"intent":"clarify","reply":"你想让我对这张图做什么?","prompts":[],"text_only":true,"options":[{"label":"分析/反推提示词","value":"分析这张图"},{"label":"基于此图生成新图","value":"按这个风格生成"},{"label":"修改这张图","value":"修改这张图"}]}`; -修改场景: -A. 风格/属性修改(use_last_outputs=true):只描述要改的+保持其他不变 -B. 主体更换(use_last_outputs=true):"将原图中的[原主体]替换为[新主体],保持场景/背景/构图/光影不变" -通用:不同物体→B;同一主体不同呈现→A`; let agentOpen = false; let agentSending = false; @@ -17550,7 +17652,9 @@ function agentGenCardHtml(gen, numOffset){ const thumbs = (gen.results || []).filter(r => r?.url).map((r, i) => `${(numOffset || 0) + i + 1}`).join(''); const promptText = escapeHtml(gen.prompt || ''); const promptHtml = promptText ? `
${promptText}
` : ''; - return `
${promptHtml}
${status === 'running' ? '' : ''}${escapeHtml(statusText)}${fullRefTags ? ' · ' + escapeHtml(fullRefTags) : ''}
${status === 'error' && gen.error ? `
${escapeHtml(String(gen.error).slice(0, 160))}
` : ''}${thumbs ? `
${thumbs}
` : ''}
`; + // 生成完成后显示快捷操作栏 + const quickActions = status === 'done' && thumbs ? `
` : ''; + return `
${promptHtml}
${status === 'running' ? '' : ''}${escapeHtml(statusText)}${fullRefTags ? ' · ' + escapeHtml(fullRefTags) : ''}
${status === 'error' && gen.error ? `
${escapeHtml(String(gen.error).slice(0, 160))}
` : ''}${thumbs ? `
${thumbs}
` : ''}${quickActions}
`; } function agentMessageHtml(msg){ const imgs = (msg.images || []).filter(i => i?.url).map(i => ``).join(''); @@ -17605,7 +17709,15 @@ function agentMessageHtml(msg){ const footerHtml = (confirmAllHtml || cancelAllHtml) ? `` : ''; promptCardHtml = `
📝 提示词确认${countHint}${progress}
${listHtml}
${footerHtml}
`; } - return `
${msg.text ? `
${escapeHtml(msg.text)}
` : ''}${imgs ? `
${imgs}
` : ''}${gens}${promptCardHtml}${optionsHtml}${actions}
`; + // 分析/提示词建议卡片(非思维模式 text_only 回复) + let cardHtml = ''; + if(msg.role === 'assistant' && msg.cardType === 'analysis' && msg.text){ + cardHtml = `
${escapeHtml(msg.text)}
`; + } else if(msg.role === 'assistant' && msg.cardType === 'prompt_suggestion' && msg.text){ + cardHtml = `
${escapeHtml(msg.text)}
`; + } + const bubbleHtml = (msg.text && !cardHtml) ? `
${escapeHtml(msg.text)}
` : ''; + return `
${bubbleHtml}${cardHtml}${imgs ? `
${imgs}
` : ''}${gens}${promptCardHtml}${optionsHtml}${actions}
`; } function renderAgentMessages(){ if(!agentMessages || !agentState) return; @@ -17648,6 +17760,48 @@ function renderAgentMessages(){ if(!agentSending) agentRetryMessage(btn.dataset.agentRetry); }; }); + // 快捷操作栏事件(修改/变体/反推) + agentMessages.querySelectorAll('[data-agent-quick]').forEach(btn => { + btn.onclick = e => { + e.stopPropagation(); + if(agentSending) return; + const action = btn.dataset.agentQuick; + const card = btn.closest('.agent-gen-card'); + const msgEl = btn.closest('.agent-msg'); + const msg = (agentState?.messages || []).find(m => m.id === msgEl?.querySelector('[data-agent-copy]')?.dataset?.agentCopy) || [...(agentState?.messages || [])].reverse().find(m => m.role === 'assistant' && m.generations?.length); + if(action === 'edit'){ + // 修改:聚焦输入框,预填"把这张图" + if(agentInput){ agentInput.value = '把这张图'; agentInput.focus(); } + } else if(action === 'variant'){ + // 变体:用同一 prompt 再生一张 + const gen = msg?.generations?.find(g => (g.results || []).some(r => r?.url)); + if(gen?.prompt){ agentSendWithText(gen.prompt); } + } else if(action === 'describe'){ + // 反推:取最后一张生成图,发送"分析这张图,反推prompt" + const lastUrl = msg?.generations?.flatMap(g => (g.results || []).filter(r => r?.url)).pop()?.url; + if(lastUrl){ + agentState.attachments = [{url:lastUrl, name:'生成图'}]; + renderAgentAttachments(); + if(agentInput){ agentInput.value = '分析这张图,反推提示词'; agentInput.focus(); } + } + } + }; + }); + // 分析/提示词卡片的"用这个生图"按钮 + agentMessages.querySelectorAll('[data-agent-card-gen]').forEach(btn => { + btn.onclick = e => { + e.stopPropagation(); + if(agentSending) return; + const cardEl = btn.closest('.agent-analysis-card, .agent-prompt-suggest-card'); + if(!cardEl) return; + const bodyEl = cardEl.querySelector('.agent-analysis-body, .agent-prompt-suggest-body'); + const text = bodyEl?.textContent || ''; + // 提取"完整Prompt:"后的内容,或用全文 + const promptMatch = text.match(/完整Prompt[::]\s*([\s\S]+)/i); + const prompt = promptMatch ? promptMatch[1].trim() : text.trim(); + if(prompt) agentSendWithText(prompt); + }; + }); // 运行过程中锁定输入框和顶部按钮 if(agentInput) agentInput.disabled = agentSending; const newChatBtn = document.getElementById('agentNewChatBtn'); @@ -18283,6 +18437,55 @@ function extractFieldsWithRegex(text){ } return result; } +// 非思维模式意图路由解析:从 LLM 返回中提取结构化意图 JSON +function parseAgentIntentRoute(raw, lastUserText){ + const text = String(raw || '').trim(); + const fallback = {intent:'generate', reply:'', prompts:[], use_attachments:false, attachment_roles:{}, use_last_outputs:false, text_only:false, analysis:''}; + if(!text) return fallback; + // 尝试提取 JSON + let jsonStr = text; + // 去掉可能的 markdown 代码块 + const fenceMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/); + if(fenceMatch) jsonStr = fenceMatch[1].trim(); + // 尝试找到最外层的 { } + const braceStart = jsonStr.indexOf('{'); + const braceEnd = jsonStr.lastIndexOf('}'); + if(braceStart >= 0 && braceEnd > braceStart){ + jsonStr = jsonStr.slice(braceStart, braceEnd + 1); + } + try { + const data = JSON.parse(jsonStr); + const intent = String(data.intent || 'generate').toLowerCase(); + const validIntents = ['generate','edit','analyze','refine','composite','reference_generate','clarify','meta','cancel']; + return { + intent: validIntents.includes(intent) ? intent : 'generate', + reply: String(data.reply || '').slice(0, 500), + prompts: Array.isArray(data.prompts) ? data.prompts.filter(p => typeof p === 'string' && p.trim()).map(p => p.trim()) : [], + use_attachments: !!data.use_attachments, + attachment_roles: (data.attachment_roles && typeof data.attachment_roles === 'object') ? data.attachment_roles : {}, + use_last_outputs: !!data.use_last_outputs, + text_only: !!data.text_only || intent === 'analyze' || intent === 'refine' || intent === 'clarify' || intent === 'meta' || intent === 'cancel', + analysis: String(data.analysis || '').slice(0, 5000), + options: Array.isArray(data.options) ? data.options.filter(o => o && o.label).slice(0, 6) : [] + }; + } catch(e) { + // JSON 解析失败,尝试正则提取关键字段 + console.warn('[IntentRoute] JSON parse failed, regex fallback:', e.message); + const textOnlyMatch = text.match(/"text_only"\s*:\s*(true|false)/i); + const intentMatch = text.match(/"intent"\s*:\s*"(\w+)"/i); + const analysisMatch = text.match(/"analysis"\s*:\s*"((?:[^"\\]|\\.)*)"/s); + const replyMatch = text.match(/"reply"\s*:\s*"((?:[^"\\]|\\.)*)"/s); + const isTextOnly = textOnlyMatch ? textOnlyMatch[1] === 'true' : false; + const intent = intentMatch ? intentMatch[1].toLowerCase() : 'generate'; + if(isTextOnly || intent === 'analyze' || intent === 'refine'){ + let analysis = ''; + if(analysisMatch){ try { analysis = JSON.parse('"' + analysisMatch[1] + '"'); } catch(e2){ analysis = analysisMatch[1]; } } + return {...fallback, intent, text_only:true, analysis: analysis || text}; + } + // 生图类回退:用用户原文 + return {...fallback, intent:'generate', prompts:[]}; + } +} function parseAgentResponse(raw, lastUserText){ const text = String(raw || '').trim(); const candidates = [text]; @@ -18677,6 +18880,12 @@ const assistantMsg = { saveAgentState(); if(assistantMsg.generations.length && assistantMsg.prompts.length === 0) await runAgentGenerations(assistantMsg, userMsg); } +// 快捷操作辅助:设置输入框文本并触发发送 +function agentSendWithText(text){ + if(!text || agentSending) return; + if(agentInput) agentInput.value = text; + sendAgentMessage(); +} async function sendAgentMessage(){ if(agentSending || !agentState) return; // P2-14: 确认中发送新消息拦截 —— 检测有未完成的 prompts 时弹 toast @@ -18700,17 +18909,75 @@ async function sendAgentMessage(){ const attachments = (Array.isArray(agentState.attachments) ? agentState.attachments : []).slice(); if(!text && !attachments.length) return; - // ============ OFF模式:1次LLM调用(快速执行器) ============ + // ============ OFF模式:意图路由 + 快速路径 ============ const thinkingModeOn = agentState?.thinkingMode; if(!thinkingModeOn){ - // 检查理解模型是否配置(OFF模式也需要LLM理解意图) - if(!chatApiProviders().length){ toast(tr('smart.agentNeedChatModel') || '请先配置理解模型'); return; } + const _skills = Array.isArray(agentState?.skills) ? agentState.skills : []; + const hasLastOutputs = agentLastResults().length > 0; + const modifyRe = /改成|换成|变成|转为|改为|转成|调整为|修改成|修改一下|改一下|调整一下|重新画|重画|重新生成|加一个|去掉|删除|移除/i; + const analyzeRe = /^(分析|描述|看看|识别|反推|总结|提取|解读|评价|对比|比较|说说|告诉我|这是什么|里面有什么|什么风格|什么特点|什么构图)/i; + const refineRe = /(扩写|优化|翻译|改写|写prompt|写提示词|帮我写|生成prompt)/i; + const noGenRe = /(不要生成|不用画|不需要出图|只分析|只描述|只看|别画|别生成)/i; + const genIntentRe = /生成|画一|做一|出一|来一|帮我画|帮我做|帮我生|设计|创作/i; + + // 创建user消息(所有路径共用) + const userMsg = {id:uid('am'), role:'user', text, images:attachments, ts:Date.now()}; + agentState.messages.push(userMsg); + agentState.messages = agentState.messages.slice(-AGENT_MSG_MAX); + agentState.attachments = []; + if(agentInput) agentInput.value = ''; + renderAgentAttachments(); + agentSending = true; + saveAgentState(); + + // ===== 快速路径:跳过LLM直接生图 ===== + const isFastPath = text && !attachments.length && !hasLastOutputs + && _skills.length === 0 + && !analyzeRe.test(text.trim()) && !refineRe.test(text) && !noGenRe.test(text) + && !modifyRe.test(text) + && !/图\d|参考图/.test(text); + if(isFastPath){ + // 直接用原文生图,零延迟 + const gens = [{prompt:text, count:1, use_last_outputs:false, use_attachments:false, results:[], status:'running'}]; + const assistantMsg = {id:uid('am'), role:'assistant', text:'', options:[], prompts:[], generations:gens, ts:Date.now()}; + agentState.messages.push(assistantMsg); + agentState.messages = agentState.messages.slice(-AGENT_MSG_MAX); + saveAgentState(); + renderAgentMessages(); + agentSending = false; + await runAgentGenerations(assistantMsg, userMsg); + agentSending = false; + renderAgentMessages(); + saveAgentState(); + return; + } + + // ===== 修改快速路径:有last_outputs + 修改意图,跳过LLM ===== + const isEditFastPath = text && hasLastOutputs && modifyRe.test(text) && !attachments.length + && !analyzeRe.test(text.trim()) && !refineRe.test(text); + if(isEditFastPath){ + const gens = [{prompt:text, count:1, use_last_outputs:true, use_attachments:false, results:[], status:'running'}]; + const assistantMsg = {id:uid('am'), role:'assistant', text:'', options:[], prompts:[], generations:gens, ts:Date.now()}; + agentState.messages.push(assistantMsg); + agentState.messages = agentState.messages.slice(-AGENT_MSG_MAX); + saveAgentState(); + renderAgentMessages(); + agentSending = false; + await runAgentGenerations(assistantMsg, userMsg); + agentSending = false; + renderAgentMessages(); + saveAgentState(); + return; + } + + // ===== 需要LLM意图路由 ===== + if(!chatApiProviders().length){ toast(tr('smart.agentNeedChatModel') || '请先配置理解模型'); agentSending = false; renderAgentMessages(); return; } const chatProvider = resolveChatProviderId(agentState.chatProvider); const chatModel = resolveChatModel(agentState.chatModel, chatProvider); agentState.chatProvider = chatProvider; agentState.chatModel = chatModel; - // 构建上下文图片(附件 + 上轮生成图 + 上轮附件) + // 构建上下文图片 const contextImages = attachments.slice(); if(agentState.autoContext !== false){ [...agentLastResults(), ...agentLastUserAttachments()].forEach(item => { @@ -18718,23 +18985,21 @@ async function sendAgentMessage(){ }); } - // 构建图片编号映射说明(让LLM知道"图N"对应什么) + // 图片编号映射 const imageMap = agentCurrentImageMap(); let imageMapDesc = ''; if(imageMap.length > 0){ imageMapDesc = '\n\n【当前可用图片编号】\n' + imageMap.map(m => `图${m.num} = ${m.source === 'gen' ? '上一轮生成图' : '用户上传附件'}(${m.name || ''})`).join('\n'); } - // 构建消息文本(注入图片映射 + skill提醒) + // 构建消息文本 let messageText = text || '(请分析这些图片)'; messageText += imageMapDesc; - const _skills = Array.isArray(agentState?.skills) ? agentState.skills : []; if(_skills.length > 0){ const skillNames = _skills.map(s => s?.name).filter(Boolean).join('、'); messageText += `\n\n【Skill提醒】遵循 Skill 文档(${skillNames})的所有样式描述。`; } - // 构建LLM请求 const llmPayload = { message: messageText, messages: agentHistoryMessages().slice(0, -1), @@ -18746,20 +19011,11 @@ async function sendAgentMessage(){ system_prompt: AGENT_OFF_MODE_INSTRUCTION + (_skills.length > 0 ? '\n\n' + _skills.map(s => `===== Skill: ${s.name} =====\n${s.content || ''}\n===== End =====`).join('\n') : '') }; - // 创建user消息 - const userMsg = {id:uid('am'), role:'user', text, images:attachments, ts:Date.now()}; - agentState.messages.push(userMsg); - agentState.messages = agentState.messages.slice(-AGENT_MSG_MAX); - agentState.attachments = []; - if(agentInput) agentInput.value = ''; - renderAgentAttachments(); - agentSending = true; agentThinking = true; renderAgentMessages(); saveAgentState(); try { - // 调用LLM(1次,快速理解意图) const taskRes = await fetch('/api/agent-llm-task', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(llmPayload) @@ -18769,42 +19025,67 @@ async function sendAgentMessage(){ }); const taskId = taskRes.task_id; if(!taskId) throw new Error('LLM任务创建失败'); - // 轮询LLM结果 const llmResult = await pollAgentLlmTask(taskId); agentThinking = false; - // 解析LLM返回 - const parsed = parseAgentResponse(llmResult.text || '', text); - if(!Array.isArray(parsed.generations)) parsed.generations = []; - if(!Array.isArray(parsed.options)) parsed.options = []; - // ★ 前端防护:分析/描述类请求强制清空generations(防止LLM不遵守规则) - const _analysisRe = /^(分析|描述|看看|识别|反推|总结|提取|解读|评价|对比|比较|说说|告诉我|这是什么|里面有什么|什么风格|什么特点)/i; - const _noGenRe = /(不要生成|不用画|不需要出图|只分析|只描述|只看|别画|别生成)/i; - if((_analysisRe.test(text.trim()) || _noGenRe.test(text)) && !/生成|画一|做一|出一|来一|帮我画|帮我做|帮我生/.test(text)){ - parsed.generations = []; + // 解析意图路由 JSON + const routed = parseAgentIntentRoute(llmResult.text || '', text); + + // 最小安全网:剥离@mention后,如果文本以分析动词开头且无生图动词,强制 text_only + // (仅针对 LLM 反复误判的最明显场景,不是旧的多层正则) + const _cleanText = text.replace(/@[^\s]+/g, '').trim(); + const _isObviousAnalysis = /^(分析|描述|反推|识别|总结|提取|解读|对比|比较|看看这|这是什么|什么风格|什么特点|什么构图)/.test(_cleanText); + const _hasExplicitGen = /生成|画一|做一|出一|来一|帮我画|帮我做|帮我生|设计一|创作一/.test(_cleanText); + if(_isObviousAnalysis && !_hasExplicitGen && !routed.text_only){ + routed.text_only = true; + routed.intent = 'analyze'; + if(!routed.analysis && routed.reply) routed.analysis = routed.reply; } - // 如果LLM返回了generations → 直接执行生图 - if(parsed.generations.length > 0){ - const assistantMsg = {id:uid('am'), role:'assistant', text:parsed.reply || '', options:[], prompts:[], generations:parsed.generations, ts:Date.now()}; + // 意图分发(信任 LLM 判断,不再用正则覆盖) + if(routed.intent === 'cancel'){ + // 取消 + const assistantMsg = {id:uid('am'), role:'assistant', text:routed.reply || '好的,已取消。', options:[], prompts:[], generations:[], ts:Date.now()}; agentState.messages.push(assistantMsg); agentState.messages = agentState.messages.slice(-AGENT_MSG_MAX); - saveAgentState(); - renderAgentMessages(); - await runAgentGenerations(assistantMsg, userMsg); + saveAgentState(); renderAgentMessages(); + } else if(routed.intent === 'clarify'){ + // 意图不明,问用户 + const assistantMsg = {id:uid('am'), role:'assistant', text:routed.reply || '你想让我做什么?', options:routed.options || [], prompts:[], generations:[], ts:Date.now()}; + agentState.messages.push(assistantMsg); + agentState.messages = agentState.messages.slice(-AGENT_MSG_MAX); + saveAgentState(); renderAgentMessages(); + } else if(routed.text_only){ + // 纯文本回复(分析/反推/提示词扩写/meta) + const analysisText = routed.analysis || routed.reply || llmResult.text || ''; + const cardType = routed.intent === 'refine' ? 'prompt_suggestion' : routed.intent === 'meta' ? '' : 'analysis'; + const assistantMsg = {id:uid('am'), role:'assistant', text:analysisText, options:routed.options || [], prompts:[], generations:[], ts:Date.now(), cardType:cardType || undefined}; + agentState.messages.push(assistantMsg); + agentState.messages = agentState.messages.slice(-AGENT_MSG_MAX); + saveAgentState(); renderAgentMessages(); } else { - // 纯文字回复(分析/描述/问答) - const assistantMsg = {id:uid('am'), role:'assistant', text:parsed.reply || llmResult.text || '', options:parsed.options || [], prompts:[], generations:[], ts:Date.now()}; + // 生图类意图(generate/edit/composite/reference_generate) + const prompts = (Array.isArray(routed.prompts) && routed.prompts.length > 0) ? routed.prompts : [text]; + const gens = prompts.map(p => ({ + prompt: p, + count: 1, + use_last_outputs: !!routed.use_last_outputs, + use_attachments: !!routed.use_attachments || attachments.length > 0, + results: [], + status: 'running' + })); + const assistantMsg = {id:uid('am'), role:'assistant', text:routed.reply || '', options:[], prompts:[], generations:gens, ts:Date.now()}; agentState.messages.push(assistantMsg); agentState.messages = agentState.messages.slice(-AGENT_MSG_MAX); saveAgentState(); renderAgentMessages(); + await runAgentGenerations(assistantMsg, userMsg); } } catch(e) { agentThinking = false; - // Fallback:LLM失败时直接透传用户原文+全部参考图 - console.warn('[OFF mode] LLM failed, fallback to direct:', e.message); - const fallbackGens = [{prompt:text, count:1, use_last_outputs:false, use_attachments:attachments.length > 0, results:[], status:'running'}]; + // Fallback:LLM失败时直接用原文生图 + console.warn('[OFF mode] LLM intent route failed, fallback:', e.message); + const fallbackGens = [{prompt:text, count:1, use_last_outputs:hasLastOutputs && modifyRe.test(text), use_attachments:attachments.length > 0, results:[], status:'running'}]; const assistantMsg = {id:uid('am'), role:'assistant', text:'', options:[], prompts:[], generations:fallbackGens, ts:Date.now()}; agentState.messages.push(assistantMsg); agentState.messages = agentState.messages.slice(-AGENT_MSG_MAX); @@ -18896,8 +19177,8 @@ async function sendAgentMessage(){ system_prompt:agentSystemPrompt(bypassThinking, _finalCount.count) }; try { - // 创建后端 LLM 任务(息屏/刷新不会丢失) - const taskRes = await fetch('/api/agent-llm-task', { + // 创建后端 LLM 任务(流式输出) + const taskRes = await fetch('/api/agent-llm-task?stream=true', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(llmPayload) @@ -18907,12 +19188,15 @@ async function sendAgentMessage(){ }); const llmTaskId = taskRes.task_id; if(!llmTaskId) throw new Error('Failed to create LLM task'); + // 启动流式渲染 + startAgentStream(llmTaskId); // 保存 LLM task ID,刷新后可恢复 agentState._pendingLlmTaskId = llmTaskId; agentState._pendingLlmTaskTs = Date.now(); saveAgentState(); // 等待 LLM 结果(WebSocket 实时通知 + 轮询保底) const result = await pollAgentLlmTask(llmTaskId); + endAgentStream(); delete agentState._pendingLlmTaskId; delete agentState._pendingLlmTaskTs; // 处理结果 diff --git a/static/smart-canvas.html b/static/smart-canvas.html index 8d4294064..11abdc531 100644 --- a/static/smart-canvas.html +++ b/static/smart-canvas.html @@ -20,7 +20,7 @@ - +
@@ -571,6 +571,6 @@
preview
- + From 8b595eefaabd7248e08fa16955d675a5902ee78b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Tue, 21 Jul 2026 18:46:43 +0800 Subject: [PATCH 62/67] =?UTF-8?q?docs:=20=E7=89=88=E6=9C=AC=E5=8F=B7?= =?UTF-8?q?=E5=8D=87=E7=BA=A7=E8=87=B3=20v2.0=EF=BC=8C=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=20README=20=E7=89=88=E6=9C=AC=E8=A1=A8=E6=A0=BC=E5=92=8C=20CHA?= =?UTF-8?q?NGELOG=20=E6=94=B9=E7=89=88=E5=A4=A7=E7=BA=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 32 ++++++++++++++++++++++++++++++++ README.md | 17 ++++++++++++++++- VERSION | 2 +- 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09aadea8e..571ac92a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,37 @@ # 版本更新大纲 +#### v2.0 — Agent 架构大改版(LLM-first 意图决策) + +**架构重构:** +- 废弃旧版多层正则防护,改为 LLM 统一意图决策 +- 8 种意图类型:generate / edit / analyze / refine / composite / clarify / meta / cancel +- 极窄快速路径:纯文生图、简单修改跳过 LLM 零延迟执行 +- 最小安全网:剥离 @mention 后分析动词强制 text_only + +**流式输出:** +- 后端 `canvas_llm_stream`:流式调用 LLM,通过 WebSocket 逐 token 推送 +- 思维模式接入流式(`?stream=true`) +- 流式气泡:max-height 200px + 滚动 + 停止按钮 + +**意图分发:** +- analyze:分析图片 + 行动引导选项(点击即可继续操作) +- refine:反推/扩写提示词,只输出 prompt 不引导 +- clarify:意图不明时主动提问 + 可点击选项 +- cancel:取消操作 +- meta:回答 Agent 自身信息 + +**新 UI 组件:** +- 生成后快捷操作栏(修改 / 变体 / 反推) +- 分析结果卡片(结构化标签 + “用这个Prompt生图”按钮) +- 提示词建议卡片(等宽字体 + “直接生图”按钮) +- 降级提示条(模型能力不足时显示) +- 流式气泡(逐字显示 + 停止按钮) + +**其他:** +- 模型无关设计:不硬编码任何模型名称 +- LLM 失败回退策略:明确生图意图→直接生图,明确分析意图→提示重试 +- mac 启动脚本添加 no_proxy 避免 Clash 劫持本地请求 + #### v1.0 — AI Agent 面板基线 - AI Agent 侧边聊天面板(OneBox 风格),画布联动生图 - `@` 引用画布图片,附件统一管理(Skill 文档 + 图片) diff --git a/README.md b/README.md index 0a654a388..1f024f9f6 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Supports comfyui/API calls/modelscope calls > 本仓库是基于原项目 [hero8152/Infinite-Canvas](https://github.com/hero8152/Infinite-Canvas) 的二次开发 fork,新增了智能画布 AI Agent 面板。 > -> **当前版本:v1.6**(查看 [版本更新大纲](./CHANGELOG.md)) +> **当前版本:v2.0**(查看 [版本更新大纲](./CHANGELOG.md)) ### 重要提示 - **已关闭自动更新**:导航栏版本号显示为「Agent版(无自动更新)」。若从上游拉取/合并最新代码,可能覆盖本分支的 AI Agent 功能,请谨慎操作并先备份。 @@ -28,6 +28,21 @@ Supports comfyui/API calls/modelscope calls | v1.5 | 2026-07-19 | 硬软参数分层(数量=软参数输入框>工具栏,比例/分辨率=硬参数工具栏)、Skill=单张图样式不决定数量、统一数量决策函数 | | v1.5.1 | 2026-07-19 | 单 generation 多图泄漏修复(API 返回多图限制+参考图 URL 过滤)、count=1 强制范围扩大到所有直接模式 | | v1.6 | 2026-07-20 | LLM 与生图解耦(思维模式 OFF 跳过 LLM 直接生图)、思维模式多轮维度采集(渐进式问答+自定义输入)、LLM 模型选择移入思维模式面板、框选批量发送至 Agent、附件拖拽排序、发送按钮 bug 修复 | +| **v2.0** | **2026-07-21** | **Agent 架构大改版**:意图路由重构(8种意图智能分发)、流式输出、分析/反推分离、生成后快捷操作、clarify 主动澄清机制(详见下方大纲) | + +### v2.0 改版大纲 + +本次为 Agent 核心架构的大版本重构,从“补丁式正则”演进为“LLM-first 意图决策”架构: + +1. **意图路由重构**:废弃旧版多层正则防护,改为 LLM 统一意图决策(generate/edit/analyze/refine/composite/clarify/meta/cancel) +2. **快速路径**:纯文生图、简单修改等明确场景跳过 LLM,零延迟直接执行 +3. **流式输出**:思维模式 LLM 回复逐字显示(WebSocket 推送),带停止按钮和高度限制 +4. **分析/反推分离**:反推只输出 prompt;分析输出结果 + 行动引导选项(点击即可继续操作) +5. **意图澄清(clarify)**:用户意图不明时主动提问并提供可点击选项 +6. **生成后快捷操作**:每张生成图下方显示「修改 / 变体 / 反推」快捷按钮 +7. **新 UI 组件**:分析结果卡片、提示词建议卡片、快捷操作栏、降级提示条、流式气泡 +8. **模型无关设计**:不硬编码任何模型名称,所有能力通过用户配置动态获取 +9. **安全网机制**:LLM 失败时智能回退(明确生图意图→直接生图,明确分析意图→提示重试) ---- diff --git a/VERSION b/VERSION index 1d81d23b2..c0ec837a4 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v1.6 +v2.0 From b9dd48c08e782e979cfe530c2cc14bc2b8d90a87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Tue, 21 Jul 2026 18:51:14 +0800 Subject: [PATCH 63/67] =?UTF-8?q?fix(agent):=20=E5=BF=AB=E9=80=9F=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=E5=8A=A0=E5=85=A5=E6=AD=A3=E5=90=91=E4=BF=A1=E5=8F=B7?= =?UTF-8?q?=EF=BC=88=E7=94=9F=E5=9B=BE=E5=8A=A8=E8=AF=8D=E6=88=96>10?= =?UTF-8?q?=E5=AD=97=EF=BC=89=EF=BC=8C=E9=98=B2=E6=AD=A2=E5=AF=92=E6=9A=84?= =?UTF-8?q?=E8=AF=AF=E7=94=9F=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/js/smart-canvas.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/static/js/smart-canvas.js b/static/js/smart-canvas.js index 214dd77a9..6830165d1 100644 --- a/static/js/smart-canvas.js +++ b/static/js/smart-canvas.js @@ -18931,11 +18931,13 @@ async function sendAgentMessage(){ saveAgentState(); // ===== 快速路径:跳过LLM直接生图 ===== + const _hasGenVerb = /画|生成|设计|创作|做一张|出一张|来一张|帮我画|帮我做|帮我生/.test(text); const isFastPath = text && !attachments.length && !hasLastOutputs && _skills.length === 0 && !analyzeRe.test(text.trim()) && !refineRe.test(text) && !noGenRe.test(text) && !modifyRe.test(text) - && !/图\d|参考图/.test(text); + && !/图\d|参考图/.test(text) + && (_hasGenVerb || text.trim().length > 10); if(isFastPath){ // 直接用原文生图,零延迟 const gens = [{prompt:text, count:1, use_last_outputs:false, use_attachments:false, results:[], status:'running'}]; From 03276c8cee5cf3da3f07bbe71c0f7441cbfc1bb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Wed, 22 Jul 2026 11:38:00 +0800 Subject: [PATCH 64/67] =?UTF-8?q?feat(agent):=20=E5=8F=98=E4=BD=93?= =?UTF-8?q?=E6=8C=89=E9=92=AE=E6=94=AF=E6=8C=81=E6=95=B0=E9=87=8F=E9=80=89?= =?UTF-8?q?=E6=8B=A9=20+=20=E6=80=9D=E7=BB=B4=E6=A8=A1=E5=BC=8F=E6=95=B0?= =?UTF-8?q?=E9=87=8F=E4=BF=AE=E5=A4=8D=20+=20=E6=96=87=E6=A1=88=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 变体按钮改为气泡弹出选择数量(×1 ×2 ×3 ×4),支持差异化生成 - 修复思维模式 genCount=1 时 LLM 返回多条 prompts 的 bug(前端截断+提示词约束) - 思维模式 tooltip/toast 文案更新:强调适合灵感探索,明确需求建议关闭 - tooltip 样式优化:限宽200px、文字折行、左对齐 - 页面版本号更新 --- .../160331-infinite-canvas/canvas.json | 123 ++ .../160331-infinite-canvas/findings.json | 191 ++ .../160331-infinite-canvas/report.canvas.tsx | 1825 +++++++++++++++++ static/angle.html | 18 +- static/api-settings.html | 14 +- static/asset-manager.html | 10 +- static/canvas-list.html | 16 +- static/canvas.html | 18 +- static/comfyui-settings.html | 14 +- static/css/smart-canvas.css | 11 +- static/enhance.html | 16 +- static/gpt-chat.html | 12 +- static/index.html | 28 +- static/js/smart-canvas.js | 67 +- static/klein.html | 16 +- static/online.html | 16 +- static/smart-canvas.html | 14 +- static/zimage.html | 14 +- 18 files changed, 2314 insertions(+), 109 deletions(-) create mode 100644 .qoder/better-loop/2026-07-21/160331-infinite-canvas/canvas.json create mode 100644 .qoder/better-loop/2026-07-21/160331-infinite-canvas/findings.json create mode 100644 .qoder/better-loop/2026-07-21/160331-infinite-canvas/report.canvas.tsx diff --git a/.qoder/better-loop/2026-07-21/160331-infinite-canvas/canvas.json b/.qoder/better-loop/2026-07-21/160331-infinite-canvas/canvas.json new file mode 100644 index 000000000..9e4924ab3 --- /dev/null +++ b/.qoder/better-loop/2026-07-21/160331-infinite-canvas/canvas.json @@ -0,0 +1,123 @@ +{ + "schemaVersion": 1, + "summary": { + "evidenceMode": "session-limited", + "evidenceBoundary": { + "manifest": { + "schemaVersion": 2, + "sourceFingerprint": "7254844991a82cce", + "adapterVersion": "qoder-task-loop-source-v2", + "platform": "qoder", + "selection": { + "strategy": "all-eligible", + "eligibleCount": 0, + "analyzedCount": 0, + "confidence": "Low" + } + }, + "deliveryEvidenceLevels": [], + "sourceGaps": [] + }, + "semanticFacets": { + "schemaVersion": 1, + "status": "supplementary", + "entries": [ + { + "id": "session-insight:source-coverage", + "kind": "redacted-summary", + "episodeRef": null, + "status": "candidate", + "labels": [ + "source-coverage", + "Low" + ], + "summary": "Analyzed 0 of 0 sessions; 0/5 enabled source roots exist. Use this as the current workspace evidence boundary for final insight cards.", + "evidenceRefs": [], + "modelVersion": "session-insights-v1" + }, + { + "id": "session-insight:validation-behavior", + "kind": "redacted-summary", + "episodeRef": null, + "status": "candidate", + "labels": [ + "validation-behavior", + "Low" + ], + "summary": "No validation command category was observed in the analyzed session sample. Inspect more sessions or add explicit validation guidance before claiming validation-after-edit behavior.", + "evidenceRefs": [], + "modelVersion": "session-insights-v1" + }, + { + "id": "session-insight:post-edit-validation", + "kind": "rework-correction", + "episodeRef": null, + "status": "candidate", + "labels": [ + "post-edit-validation", + "Low" + ], + "summary": "No edit event was observed in the analyzed sample. Inspect more sessions before making claims about edit or validation habits.", + "evidenceRefs": [], + "modelVersion": "session-insights-v1" + }, + { + "id": "session-insight:execution-friction", + "kind": "friction-taxonomy", + "episodeRef": null, + "status": "candidate", + "labels": [ + "execution-friction", + "Low" + ], + "summary": "No strong execution friction signal in the analyzed sample. Keep friction claims narrow unless additional failed commands, rejected actions, or warnings are inspected.", + "evidenceRefs": [], + "modelVersion": "session-insights-v1" + } + ] + }, + "learningCapture": { + "schemaVersion": 1, + "state": "N/A", + "summary": "暂不适用——需要两个可比的观察窗口和一次改进对比。", + "interventions": [] + } + }, + "dimensions": [ + { + "id": "task-understanding" + }, + { + "id": "controlled-execution" + }, + { + "id": "change-validation" + }, + { + "id": "reliable-delivery" + }, + { + "id": "learning-capture" + } + ], + "findings": [ + { + "id": "agent-context-entrypoint-missing" + }, + { + "id": "monolith-scope-localization" + }, + { + "id": "no-automated-validation" + }, + { + "id": "no-quality-enforcement-gate" + }, + { + "id": "dependency-not-pinned" + }, + { + "id": "no-change-acceptance-path" + } + ] +} diff --git a/.qoder/better-loop/2026-07-21/160331-infinite-canvas/findings.json b/.qoder/better-loop/2026-07-21/160331-infinite-canvas/findings.json new file mode 100644 index 000000000..aefc1b7d1 --- /dev/null +++ b/.qoder/better-loop/2026-07-21/160331-infinite-canvas/findings.json @@ -0,0 +1,191 @@ +{ + "summary": { + "projectName": "Infinite-Canvas", + "locale": "zh-CN", + "modelId": "agent-work-loop-v4", + "reportContractVersion": 24, + "overview": "Infinite-Canvas 是一个 FastAPI 单体项目,具备基本启动脚本和依赖声明,但缺乏 agent 上下文指引、自动化验证、质量门禁和变更验收机制。本次审查为 session-limited 模式(无合格会话),结论仅基于项目静态证据。", + "aiAgentPractice": { + "inspectedSurfaces": [ + "Workflows", + "Plugins", + "Memories" + ], + "coverageRows": [ + { + "surface": "Workflows", + "scopes": [ + "Project" + ], + "count": 7, + "paths": [ + "workflows/2511.json", + "workflows/Flux2-Klein.json", + "workflows/LTXDirectorv2-API.json", + "workflows/upscale.json", + "workflows/Z-Image.json", + "workflows/Z-Image-Enhance.json", + "workflows/LTXDirectorv2-API.config.json" + ] + }, + { + "surface": "Plugins", + "scopes": [ + "Plugin" + ], + "count": 2, + "paths": [ + "better-loop", + "qoder-create-plugin" + ] + }, + { + "surface": "Memories", + "scopes": [ + "Project" + ], + "count": 2, + "paths": [ + "~/.qoder/memories/019f6f6d/projects/Users-yangfan-Desktop-Infinite-Canvas/project_introduction/Infinite-Canvas_AI_Agent项目概述.md", + "~/.qoder/memories/019f6f6d/projects/Users-yangfan-Desktop-Infinite-Canvas/project_tech_stack/Python后端技术栈与协议支持.md" + ] + } + ] + }, + "suggestions": [], + "assignmentSummaries": [], + "dimensions": [ + { + "id": "task-understanding", + "label": "任务理解", + "score": 25, + "summary": "README 提供了 fork 约束等基本信息,但无 agent 指令文件或模块化结构,agent 无法高效定位任务边界和风险区域。", + "findingRefs": [ + "agent-context-entrypoint-missing", + "monolith-scope-localization" + ] + }, + { + "id": "controlled-execution", + "label": "可控执行", + "score": 30, + "summary": "存在一键启动脚本和基本依赖声明,但依赖未锁定版本,无 doctor/reset 路径,环境可复现性不足。", + "findingRefs": [ + "dependency-not-pinned" + ] + }, + { + "id": "change-validation", + "label": "改动验证", + "score": 5, + "summary": "项目中不存在任何测试、lint、typecheck 或 CI 配置,agent 变更后无任何自动化反馈信号。", + "findingRefs": [ + "no-automated-validation" + ] + }, + { + "id": "reliable-delivery", + "label": "可靠交付", + "score": 10, + "summary": "无 CI 流水线、分支保护、PR 验收路径或回滚机制,变更可直接合入主分支而无机械化检查。", + "findingRefs": [ + "no-quality-enforcement-gate", + "no-change-acceptance-path" + ] + }, + { + "id": "learning-capture", + "label": "经验沉淀", + "score": 35, + "summary": "存在 2 条项目记忆和 7 个工作流资产,但无可观察的循环检测、复用改进或后续验证证据。", + "findingRefs": [] + } + ] + }, + "findings": [ + { + "id": "agent-context-entrypoint-missing", + "title": "缺少 Agent 上下文入口文件", + "severity": "Medium", + "reason": "项目无 AGENTS.md、CLAUDE.md 或等效的 agent 指令文件。agent 无法从项目结构中获得任务边界、高风险区域、模块职责或下一步指引,只能依赖通读 README 和遍历目录推断上下文。", + "expectedOutput": [ + "创建一个结构化的 agent 入口文件,覆盖项目定位、目录说明、启动路径、高风险区域和任务路由。" + ], + "expectedArtifact": "AGENTS.md", + "aiFixPrompt": "/better-loop fix this issue\n\n为 Infinite-Canvas 创建一个简洁的 AGENTS.md 入口文件(80-150 行),包含:项目定位、目录职责、启动命令、高风险区域、常见任务路径。\n\n## Validation\n\n- 确认文件存在且内容覆盖上述五个部分\n- 确认命令与 mac-启动服务.sh 和 main.py 一致", + "dimensionRefs": [ + "task-understanding" + ] + }, + { + "id": "monolith-scope-localization", + "title": "17,787 行单文件单体阻碍变更范围定位", + "severity": "High", + "reason": "main.py 包含全部路由、WebSocket 管理、LLM 调用、生图逻辑和文件处理(17,787 行),无模块或包拆分。agent 无法通过目录结构定位变更范围,任何修改都需要在巨大单文件中搜索目标,增加误改风险。", + "expectedOutput": [ + "将 main.py 按功能域拆分为独立模块,每个模块职责单一,agent 可通过目录结构定位变更范围。" + ], + "expectedArtifact": "模块拆分方案", + "aiFixPrompt": "/better-loop fix this issue\n\n制定 main.py 的模块拆分方案:按功能域(路由、WebSocket、LLM、生图、文件处理)拆分为独立模块,保持现有 API 兼容。\n\n## Validation\n\n- 服务正常启动且端口 3000 可访问\n- 各模块可独立导入无循环依赖", + "dimensionRefs": [ + "task-understanding" + ] + }, + { + "id": "no-automated-validation", + "title": "无任何自动化验证手段", + "severity": "High", + "reason": "项目中不存在测试文件、lint 配置(ruff/flake8)、类型检查(mypy/pyright)或 CI 流水线。agent 变更后唯一的反馈是「服务是否启动不报错」,无法检测逻辑回归、类型错误或代码风格问题。", + "expectedOutput": [ + "配置 ruff lint 和 pytest 冒烟测试,agent 变更后可在 2 分钟内获得可操作的反馈信号。" + ], + "expectedArtifact": "验证配置", + "aiFixPrompt": "/better-loop fix this issue\n\n为项目添加最小验证层:1) 配置 ruff 作为 linter;2) 添加 pytest 及至少覆盖核心路由的冒烟测试;3) 在 requirements.txt 中声明开发依赖。\n\n## Validation\n\n- ruff check 可运行并输出可操作的诊断\n- pytest 至少有一个通过的冒烟测试\n- 验证命令可在 2 分钟内完成", + "dimensionRefs": [ + "change-validation" + ] + }, + { + "id": "no-quality-enforcement-gate", + "title": "无质量执行门禁", + "severity": "Medium", + "reason": "无 pre-commit hooks、CI pipeline、schema 校验或架构边界检查。质量完全依赖人工审查,agent 产出的代码无机械化拦截路径,违反约束的变更可无阻碍地进入代码库。", + "expectedOutput": [ + "配置 pre-commit hook,在提交前自动运行 lint 检查,违规时阻止提交并给出修复方向。" + ], + "expectedArtifact": "质量门禁配置", + "aiFixPrompt": "/better-loop fix this issue\n\n添加最小质量门禁:1) 配置 pre-commit hook 运行 ruff check;2) 添加基本的 .pre-commit-config.yaml。\n\n## Validation\n\n- pre-commit run --all-files 可执行\n- 违规时输出包含文件和规则信息", + "dimensionRefs": [ + "reliable-delivery" + ] + }, + { + "id": "dependency-not-pinned", + "title": "依赖未锁定版本,环境不可复现", + "severity": "Medium", + "reason": "requirements.txt 中 7 个依赖均无版本约束(无 == 或 >= 限定),无 pip-tools/poetry lockfile。不同时间或环境安装可能产生不兼容版本,agent 无法保证确定性环境复现。", + "expectedOutput": [ + "生成带版本约束的依赖文件,确保不同环境安装结果一致。" + ], + "expectedArtifact": "锁定的依赖文件", + "aiFixPrompt": "/better-loop fix this issue\n\n锁定依赖版本:1) 使用 pip freeze 或 pip-compile 生成带版本约束的 requirements.txt;2) 或迁移到 poetry/pip-tools 管理 lockfile。\n\n## Validation\n\n- requirements.txt 中每个依赖有明确版本约束\n- 从 lockfile 安装后服务正常启动", + "dimensionRefs": [ + "controlled-execution" + ] + }, + { + "id": "no-change-acceptance-path", + "title": "无变更验收路径和安全边界", + "severity": "Medium", + "reason": "无分支保护、PR 模板、CODEOWNERS 或合并前检查。agent 产出的变更可直接合入主分支而无验收步骤。同时 data/ 目录下的运行时数据虽被 .gitignore 排除但存在于工作目录,agent 操作可能意外修改用户数据而无感知。", + "expectedOutput": [ + "声明变更验收路径和运行时数据操作边界,agent 产出变更有明确的验收步骤。" + ], + "expectedArtifact": "验收流程文档", + "aiFixPrompt": "/better-loop fix this issue\n\n建立最小变更验收路径:1) 在 README 或 AGENTS.md 中声明变更需通过分支+PR 流程;2) 对 data/ 目录添加操作边界说明。\n\n## Validation\n\n- 文档中明确了变更验收流程\n- data/ 目录的操作边界有明确说明", + "dimensionRefs": [ + "reliable-delivery" + ] + } + ] +} diff --git a/.qoder/better-loop/2026-07-21/160331-infinite-canvas/report.canvas.tsx b/.qoder/better-loop/2026-07-21/160331-infinite-canvas/report.canvas.tsx new file mode 100644 index 000000000..2036a5510 --- /dev/null +++ b/.qoder/better-loop/2026-07-21/160331-infinite-canvas/report.canvas.tsx @@ -0,0 +1,1825 @@ +import { + AreaChart, + Button, + Callout, + Card, + CardBody, + CardHeader, + CollapsibleSection, + Dialog, + Divider, + Fluency, + Grid, + H1, + H2, + IconButton, + MetricsGrid, + Row, + RiskHeatmap, + SendToChatButton, + Stack, + Table, + Tag, + Text, +} from "qoder/canvas"; +import hostReportData from "./findings.json"; +import canvasData from "./canvas.json"; + +function mergeCanvasRows(hostRows, canvasRows) { + const detailById = new Map( + (Array.isArray(canvasRows) ? canvasRows : []) + .filter((row) => row && typeof row === "object" && typeof row.id === "string") + .map((row) => [row.id, row]), + ); + return (Array.isArray(hostRows) ? hostRows : []).map((row) => ({ ...detailById.get(row?.id), ...row })); +} + +function mergeCanvasObjects(host, detail) { + if (!host || typeof host !== "object" || Array.isArray(host)) return detail; + if (!detail || typeof detail !== "object" || Array.isArray(detail)) return host; + const merged = { ...host }; + for (const [key, value] of Object.entries(detail)) { + merged[key] = value && typeof value === "object" && !Array.isArray(value) + ? mergeCanvasObjects(host[key], value) + : value; + } + return merged; +} + +function mergeCanvasReport(host, detail) { + const summary = host?.summary ?? {}; + if (!detail || typeof detail !== "object" || Array.isArray(detail)) return host; + return { + summary: { + ...(detail?.summary ?? {}), + ...summary, + atAGlance: mergeCanvasObjects(detail?.summary?.atAGlance, summary.atAGlance), + dimensions: mergeCanvasRows(summary.dimensions, detail?.dimensions), + }, + findings: mergeCanvasRows(host?.findings, detail?.findings), + }; +} + +const report = mergeCanvasReport(hostReportData, canvasData); + +const pageStyle = { maxWidth: 960, margin: "0 auto", padding: 16, boxSizing: "border-box" }; +const taskLoopPageStyle = { ...pageStyle, maxWidth: 1200, padding: 20 }; +const taskLoopReaderCopyStyle = { maxWidth: 680 }; + +const DIMENSION_SUMMARY_EXAMPLE = "Example: project guidance makes the main workflow clear, but ownership for cross-cutting changes is not documented."; + +function list(value) { + return Array.isArray(value) ? value : []; +} + +function clampScore(value) { + const score = Number(value); + if (!Number.isFinite(score)) return 0; + return Math.max(0, Math.min(100, score)); +} + +function projectName() { + return report.summary?.projectName ?? "Qoder Harness Report"; +} + +function textValue(value) { + return typeof value === "string" ? value.trim() : ""; +} + +function openingStrengths() { + const explicit = list(report.summary?.strengths).map(textValue).filter(Boolean); + return explicit.length ? explicit.slice(0, 3) : ["Reviewed project signals are organized into dimensions and issue findings."]; +} + +function averageScore(dimensions) { + if (dimensions.length === 0) return 0; + return Math.round(dimensions.reduce((sum, row) => sum + clampScore(row.score), 0) / dimensions.length); +} + +function scoreTone(score) { + if (score >= 70) return "success"; + if (score >= 40) return "warning"; + return "danger"; +} + +function stageStatus(score) { + if (score >= 70) return "high"; + if (score >= 40) return "medium"; + if (score > 0) return "low"; + return "blocked"; +} + +function fluencyReason(row) { + return textValue(row?.summary) || taskLoopCopy( + "No reviewed score explanation is available for this dimension.", + "这个维度暂时没有经过复核的评分说明。", + ); +} + +function splitFluencyTooltipReason(value) { + let remaining = textValue(value).replace(/\s+/g, " "); + const chunks = []; + const limits = /[\u3400-\u9fff]/.test(remaining) ? [20, 20, 20, 20] : [34, 30, 30, 30]; + for (const limit of limits) { + if (!remaining) break; + if (remaining.length <= limit) { + chunks.push(remaining); + remaining = ""; + break; + } + let cut = remaining.lastIndexOf(" ", limit); + if (cut < Math.floor(limit * 0.55)) cut = limit; + chunks.push(remaining.slice(0, cut).trim()); + remaining = remaining.slice(cut).trim(); + } + if (remaining && chunks.length) { + chunks[chunks.length - 1] = `${chunks[chunks.length - 1].slice(0, 29)}…`; + } + return chunks; +} + +function dimensionFluencyStages(dimensions) { + return dimensions.map((row) => { + const score = clampScore(row.score); + const usesGenericBand = row.id !== "learning-capture"; + return { + id: row.id, + name: taskLoopDimensionLabel(row.id), + score, + ...(usesGenericBand ? { status: stageStatus(score), blocker: score <= 20 } : {}), + }; + }); +} + +function dimensionFluencyTooltip(row) { + const [title, ...rows] = splitFluencyTooltipReason(fluencyReason(row)); + return { + title, + rows: rows.map((value) => ({ value })), + }; +} + +function severityTone(value) { + if (value === "Critical" || value === "High") return "danger"; + if (value === "Medium") return "warning"; + if (value === "Low") return "success"; + return "neutral"; +} + +function severityRank(value) { + if (value === "Critical") return 0; + if (value === "High") return 1; + if (value === "Medium") return 2; + if (value === "Low") return 3; + return 4; +} + +function dimensionLabel(id, dimensions) { + const match = dimensions.find((row) => row.id === id); + return match?.label ?? id.replace(/-/g, " "); +} + +function aiAgentPractice() { + return report.summary?.aiAgentPractice ?? {}; +} + +function practiceRows() { + const rows = aiAgentPractice().coverageRows; + return Array.isArray(rows) ? rows : []; +} + +function inspectedSurfaces() { + const surfaces = aiAgentPractice().inspectedSurfaces; + return Array.isArray(surfaces) ? surfaces : []; +} + +function visiblePracticePaths(value) { + return list(value).map(textValue).filter((candidate) => candidate + && !candidate.includes("SharedClientCache/projects/") + && !candidate.startsWith("/") + && !/^[A-Za-z]:[\\/]/.test(candidate) + && !candidate.split(/[\\/]/).includes("..")); +} + +function practiceDescription(surface) { + const descriptions = { + Rules: ["Standing project guidance and task-routing instructions.", "项目常驻指引与任务路由说明。"], + Skills: ["Reusable agent workflows available to the project.", "项目可用的可复用 Agent 工作流。"], + "Custom Agents": ["Specialized agent profiles available for delegated work.", "可用于委派工作的专用 Agent 配置。"], + Hooks: ["Lifecycle automation around agent and delivery events.", "围绕 Agent 与交付事件的生命周期自动化。"], + MCP: ["External tools and resources exposed through MCP.", "通过 MCP 暴露的外部工具与资源。"], + Commands: ["Named command entry points for repeatable agent work.", "可重复 Agent 工作的命令入口。"], + Workflows: ["Reusable multi-step project workflows.", "可复用的多步骤项目工作流。"], + Plugins: ["Installed packages that contribute agent capabilities.", "提供 Agent 能力的已安装插件。"], + "Session Insights": ["Task-session evidence available for report analysis.", "可用于报告分析的任务会话证据。"], + Memories: ["Representative project or global Memory note files.", "项目级或全局 Memory 的代表性笔记文件。"], + }; + const copy = descriptions[surface] ?? ["Recorded agent capability sources.", "已记录的 Agent 能力来源。"]; + return taskLoopCopy(copy[0], copy[1]); +} + +function TaskLoopPracticePaths({ paths }) { + if (paths.length === 0) return null; + const preview = paths.slice(0, 2); + const remaining = paths.slice(2); + return ( + + {preview.map((path, index) => ( + {path} + ))} + {remaining.length ? ( + + {taskLoopCopy(`View ${remaining.length} more locations`, `查看其余 ${remaining.length} 个位置`)} + + )} + bodyStyle={{ padding: "4px 0 0 16px" }} + headerStyle={{ borderBottom: "none", minHeight: 24 }} + > + + {remaining.map((path, index) => ( + {path} + ))} + + + ) : null} + + ); +} + +function PracticeSourceCard({ row }) { + const paths = visiblePracticePaths(row.paths); + const scopes = list(row.scopes); + return ( + + + + {row.surface ?? taskLoopCopy("Surface", "能力面")} + + )} + trailing={Number.isInteger(Number(row.count)) ? {row.count} : undefined} + /> + + + {practiceDescription(row.surface)} + {scopes.length || paths.length ? ( + + {scopes.length ? ( + + {scopes.map((scope) => {scope})} + + ) : null} + {paths.length ? ( + + {taskLoopCopy("Sources", "来源")} + + + ) : null} + + ) : null} + + + + ); +} + +function OpeningStrengths() { + const strengths = openingStrengths(); + return ( + + + {strengths.map((strength, index) => ( + {strength} + ))} + + + ); +} + +function DimensionSummary({ dimensions }) { + return ( + + {dimensions.map((row) => { + const score = clampScore(row.score); + return ( + + {taskLoopDimensionLabel(row.id)}} + trailing={{score}%} + /> + + + {textValue(row.summary) || DIMENSION_SUMMARY_EXAMPLE} + {list(row.findingRefs).length ? ( + Linked findings: {row.findingRefs.join(", ")} + ) : null} + + + + ); + })} + + ); +} + +function FindingItem({ row, dimensions }) { + return ( + + {row.title ?? row.id}} + trailing={{row.severity ?? "Unrated"}} + /> + + + + {list(row.dimensionRefs).slice(0, 1).map((ref) => ( + {dimensionLabel(ref, dimensions)} + ))} + + + + AI Fix + + + + + + ); +} + +function PracticeCoverage() { + const rows = practiceRows(); + const surfaces = inspectedSurfaces(); + + return ( + + + AI Agent Practices + {surfaces.length ? {surfaces.length} surfaces : null} + + {rows.length ? ( + + {rows.map((row, index) => )} + + ) : No AI Agent practice rows recorded.} + + ); +} + +function usesChineseReaderCopy() { + const locale = textValue(report.summary?.locale); + if (locale) return locale.toLowerCase().startsWith("zh"); + const readerSample = [ + ...list(report.summary?.strengths), + ...list(report.findings).slice(0, 3).flatMap((row) => [row?.title, row?.reason, row?.reader]), + ].map(textValue).join(" "); + return /[\u3400-\u9fff]/.test(readerSample); +} + +function taskLoopCopy(english, chinese) { + return usesChineseReaderCopy() ? chinese : english; +} + +function taskLoopDimensionLabel(id) { + if (!id) return taskLoopCopy("not observed", "未观察到"); + return dimensionLabel(id, list(report.summary?.dimensions)); +} + +function learningStateLabel(value) { + const labels = { + "N/A": ["Needs a comparison", "需要比较"], + pending: ["Comparison planned", "已计划比较"], + improving: ["Improving", "正在改善"], + unchanged: ["No clear change", "没有明显变化"], + regressing: ["Worse — stop or revert", "变差——停止或回退"], + "outcome-supported": ["A later result supports it", "后续结果支持它"], + }[value]; + return labels ? taskLoopCopy(labels[0], labels[1]) : taskLoopCopy("Not observed", "未观察到"); +} + +function taskLoopSummary() { + return report.summary?.atAGlance ?? {}; +} + +function taskLoopSessionOverview() { + const entries = list(report.summary?.semanticFacets?.entries); + const usage = entries.find((entry) => entry?.id === "session-insight:session-usage-efficiency"); + return textValue(usage?.summary); +} + +function taskLoopUsageActivity() { + const activity = report.summary?.usageActivity; + return activity && list(activity.dates).length ? activity : null; +} + +function taskLoopUsageEfficiency() { + const usage = report.summary?.usageEfficiency; + if (!usage || typeof usage !== "object" || Array.isArray(usage)) return null; + return usage.selection || usage.accounting || usage.longSessions || usage.modelUsage || usage.reviewLead + ? usage + : null; +} + +function taskLoopUsageCoverageInfo(activity, usage, sessionOverview) { + if (textValue(sessionOverview)) return sessionOverview; + const analyzed = Number(usage?.selection?.analyzedSessionCount); + const eligible = Number(usage?.selection?.eligibleSessionCount); + if (Number.isFinite(analyzed) && analyzed >= 0 && Number.isFinite(eligible) && eligible >= 0) { + return analyzed === eligible + ? taskLoopCopy(`This analysis covered ${formatUsageNumber(analyzed)} sessions.`, `本次分析覆盖 ${formatUsageNumber(analyzed)} 个会话。`) + : taskLoopCopy( + `This analysis covered ${formatUsageNumber(analyzed)} of ${formatUsageNumber(eligible)} eligible sessions.`, + `本次分析覆盖 ${formatUsageNumber(eligible)} 个候选会话中的 ${formatUsageNumber(analyzed)} 个。`, + ); + } + const total = Number(activity?.sessions?.total); + return Number.isFinite(total) && total >= 0 + ? taskLoopCopy(`This analysis covered ${formatUsageNumber(total)} sessions.`, `本次分析覆盖 ${formatUsageNumber(total)} 个会话。`) + : ""; +} + +function usageSeriesTotal(series) { + return list(series).reduce((sum, row) => sum + Number(row?.total ?? 0), 0); +} + +function usageActivityMatrix(activity) { + const sourceDates = list(activity?.dates); + if (!sourceDates.length) return null; + const last = new Date(`${sourceDates.at(-1)}T00:00:00.000Z`); + const windowStart = new Date(last.getTime() - (364 * 86_400_000)); + const start = new Date(windowStart); + start.setUTCDate(start.getUTCDate() - start.getUTCDay()); + const weekCount = 53; + const columns = Array.from({ length: weekCount }, (_, index) => { + const date = new Date(start.getTime() + (index * 7 * 86_400_000)); + const previous = index > 0 ? new Date(start.getTime() + ((index - 1) * 7 * 86_400_000)) : null; + if (!previous || previous.getUTCMonth() === date.getUTCMonth()) return ""; + return usesChineseReaderCopy() + ? `${date.getUTCMonth() + 1}月` + : date.toLocaleDateString("en-US", { month: "short", timeZone: "UTC" }); + }); + const values = Array.from({ length: 7 }, () => Array(weekCount).fill(null)); + for (let dayOffset = 0; dayOffset < 365; dayOffset += 1) { + const date = new Date(windowStart.getTime() + (dayOffset * 86_400_000)); + const week = Math.floor((date.getTime() - start.getTime()) / (7 * 86_400_000)); + const dateKey = date.toISOString().slice(0, 10); + values[date.getUTCDay()][week] = { + id: dateKey, + value: 0, + ariaLabel: taskLoopCopy(`${dateKey}: no observed activity`, `${dateKey}:未观察到活动`), + }; + } + sourceDates.forEach((date, index) => { + const parsed = new Date(`${date}T00:00:00.000Z`); + const week = Math.floor((parsed.getTime() - start.getTime()) / (7 * 86_400_000)); + if (week < 0 || week >= weekCount) return; + const active = Number(activity?.sessions?.activeMinutes?.[index] ?? 0); + values[parsed.getUTCDay()][week] = { + id: date, + value: active, + ariaLabel: taskLoopCopy(`${date}: ${formatActivityMinutes(active)}`, `${date}:${formatActivityMinutes(active)}`), + }; + }); + return { columns, values, start: windowStart.toISOString().slice(0, 10), end: sourceDates.at(-1) }; +} + +function usageChartWindow(activity) { + const dates = list(activity?.dates); + const offset = Math.max(0, dates.length - 30); + return { offset, categories: dates.slice(offset).map((date) => String(date).slice(5)) }; +} + +function visibleUsageSeries(series, offset, limit = 5) { + const rows = list(series); + const named = rows.filter((row) => row?.name !== "Other"); + const primary = named.slice(0, limit).map((row) => ({ name: usageSeriesLabel(row.name), data: list(row.daily).slice(offset), total: Number(row.total ?? 0) })); + const remainder = [...named.slice(limit), ...rows.filter((row) => row?.name === "Other")]; + if (remainder.length > 0) { + const length = primary[0]?.data.length ?? list(remainder[0]?.daily).slice(offset).length; + primary.push({ + name: "Other", + total: remainder.reduce((sum, row) => sum + Number(row?.total ?? 0), 0), + data: Array.from({ length }, (_, index) => remainder.reduce((sum, row) => sum + Number(list(row?.daily).slice(offset)[index] ?? 0), 0)), + }); + } + return primary; +} + +function formatUsageNumber(value) { + return Math.round(Number(value ?? 0)).toLocaleString(usesChineseReaderCopy() ? "zh-CN" : "en-US"); +} + +function usageSeriesLabel(value) { + if (value === "Unknown model") return taskLoopCopy("Unattributed model", "未归属模型"); + if (value === "Unknown Skill") return taskLoopCopy("Unattributed Skill", "未归属 Skill"); + return value; +} + +function formatActivityMinutes(value) { + const formatted = Number(value ?? 0).toLocaleString(usesChineseReaderCopy() ? "zh-CN" : "en-US", { + maximumFractionDigits: 1, + }); + return `${formatted} ${taskLoopCopy("min", "分钟")}`; +} + +function taskLoopCoverage() { + return taskLoopSummary().coverage ?? {}; +} + +function confidenceLabel(value) { + const normalized = textValue(value).toLowerCase(); + const labels = { + high: ["High confidence", "高可信度"], + medium: ["Medium confidence", "中等可信度"], + low: ["Low confidence", "低可信度"], + }[normalized]; + return labels ? taskLoopCopy(labels[0], labels[1]) : taskLoopCopy("Confidence not recorded", "未记录可信度"); +} + +function confidenceTone(value) { + const normalized = textValue(value).toLowerCase(); + if (normalized === "high") return "success"; + if (normalized === "medium") return "warning"; + return "neutral"; +} + +function taskLoopStateLabel(value) { + const labels = { + Wired: ["Wired", "机制已接入"], + Present: ["Present", "已发现机制"], + Unobserved: ["Unobserved", "未观察到"], + observed: ["Observed", "已观察到"], + "Not applicable": ["Not applicable", "暂不适用"], + "N/A": ["Needs a comparison", "需要比较"], + }[value]; + return labels ? taskLoopCopy(labels[0], labels[1]) : textValue(value) || "—"; +} + +function taskLoopSubdimensionLabel(id) { + for (const dimension of list(report.summary?.dimensions)) { + const match = list(dimension?.subdimensions).find((row) => row?.id === id); + if (match) return match.label ?? id; + } + return id; +} + +function evidenceReferenceLabel(item) { + return textValue(item?.label) || textValue(item?.id) || taskLoopCopy("Unnamed evidence", "未命名证据"); +} + +function evidenceReferenceMeta(item) { + return [ + textValue(item?.status), + textValue(item?.type), + Number.isFinite(Number(item?.line)) ? `${taskLoopCopy("line", "行")} ${item.line}` : "", + ].filter(Boolean).join(" · "); +} + +function EvidenceReferenceList({ items }) { + return ( + + {items.map((item, index) => ( + + + {item?.group ? {item.group} : null} + {item?.kind ? {item.kind} : null} + {evidenceReferenceLabel(item)} + + {evidenceReferenceMeta(item) ? {evidenceReferenceMeta(item)} : null} + + ))} + + ); +} + +function severityLabel(value) { + const labels = { + Critical: ["Critical", "紧急"], + High: ["High", "高"], + Medium: ["Medium", "中"], + Low: ["Low", "低"], + }[value]; + return labels ? taskLoopCopy(labels[0], labels[1]) : value ?? "—"; +} + +function practiceSurfaceGlyph(surface) { + return ({ Rules: "R", Skills: "S", "Custom Agents": "A", Hooks: "H", MCP: "M" })[surface] ?? textValue(surface).slice(0, 1).toUpperCase() ?? "?"; +} + +function PracticeSurfaceIcon({ row }) { + return {practiceSurfaceGlyph(row?.surface)}; +} + +function TaskLoopReportHeader({ findings }) { + const sources = practiceRows().filter((row) => Number(row?.count) > 0); + const overview = textValue(report.summary?.overview); + return ( + +

{projectName()}

+ {overview ? {overview} : null} + + + {taskLoopCopy(`${findings.length} prioritized improvements`, `${findings.length} 项优先优化`)} + + {sources.length ? ( + + {taskLoopCopy(`${sources.length} practice source types`, `${sources.length} 类实践来源`)} + + ) : null} + +
+ ); +} + +function TaskLoopFluency({ dimensions }) { + if (dimensions.length === 0) return null; + return ( + + +

{taskLoopCopy("Agent Work Loop", "Agent 工作流")}

+ {dimensions.length} {taskLoopCopy("dimensions", "个维度")} +
+ + dimensionFluencyTooltip(dimensions[index])} + height={180} + highThreshold={70} + mediumThreshold={40} + showStageLabels + /> + +
+ ); +} + +function practiceCount(row) { + const value = Number(row?.count); + return Number.isInteger(value) ? value : "—"; +} + +function practiceScopeCell(row) { + const scopes = list(row?.scopes).map(textValue).filter(Boolean); + if (!scopes.length) return ; + return ( + + {scopes.map((scope) => {scope})} + + ); +} + +function practiceSourceCell(row) { + const [firstPath] = visiblePracticePaths(row?.paths); + return firstPath + ? {firstPath} + : {taskLoopCopy("No source location recorded", "未记录来源位置")}; +} + +function practiceSourceDetail(row) { + const remaining = visiblePracticePaths(row?.paths).slice(1); + if (!remaining.length) return null; + const pathListStyle = remaining.length > 8 + ? { maxHeight: 220, overflowY: "auto", paddingRight: 4 } + : { paddingRight: 4 }; + return ( + + {taskLoopCopy(`View ${remaining.length} more locations`, `查看其余 ${remaining.length} 个位置`)} + + )} + bodyStyle={{ padding: "6px 0 2px 16px" }} + headerStyle={{ borderBottom: "none", minHeight: 24 }} + > + + {remaining.map((path, index) => ( + {path} + ))} + + + ); +} + +function taskLoopPracticeColumns() { + return [ + { + key: "surface", + title: taskLoopCopy("Asset", "资产"), + minWidth: "300px", + render: (row) => ( + + + + {row.surface ?? taskLoopCopy("Surface", "能力面")} + + {practiceDescription(row.surface)} + + ), + }, + { + key: "coverage", + title: taskLoopCopy("Coverage", "覆盖范围"), + width: "170px", + minWidth: "150px", + render: (row) => ( + + + {taskLoopCopy(`${practiceCount(row)} sources`, `${practiceCount(row)} 个来源`)} + + {practiceScopeCell(row)} + + ), + }, + { + key: "source", + title: taskLoopCopy("Representative source", "代表来源"), + minWidth: "260px", + render: practiceSourceCell, + }, + ]; +} + +function TaskLoopPracticeTable({ rows = practiceRows() }) { + if (rows.length === 0) { + return {taskLoopCopy("No Agent assets recorded.", "未记录 Agent 工程资产。")}; + } + return ( + row.surface ?? "surface"} + density="compact" + renderDetail={practiceSourceDetail} + emptyText={taskLoopCopy("No Agent asset coverage recorded", "未记录 Agent 工程资产覆盖")} + /> + ); +} + +function TaskLoopActivityHeatmap({ activity }) { + const matrix = usageActivityMatrix(activity); + if (!matrix) return {taskLoopCopy("No dated session activity was observed.", "没有观察到带日期的会话活动。")}; + return ( + + + {taskLoopCopy("Daily activity (active minutes)", "每日活动(活跃分钟)")} + {matrix.start} — {matrix.end} + + formatActivityMinutes(value)} + cellSize={16} + columnWidth={16} + rowLabelWidth={34} + responsive + minCellSize={8} + minGap={2} + initialScrollPosition="end" + colorTemplate={{ + none: { background: "rgba(127, 127, 127, 0.1)", border: "transparent" }, + low: { background: "rgba(64, 166, 103, 0.22)", border: "transparent" }, + medium: { background: "rgba(54, 158, 94, 0.42)", border: "transparent" }, + high: { background: "rgba(38, 139, 78, 0.66)", border: "transparent" }, + critical: { background: "rgba(24, 115, 63, 0.9)", border: "transparent" }, + }} + maxHeight={190} + labels={{ ariaLabel: taskLoopCopy("Daily session activity", "每日会话活动") }} + /> + + ); +} + +function UsageStatRow({ label, value }) { + if (value === undefined || value === null || value === "") return null; + return ( + + {label} + {value} + + ); +} + +function UsageRankList({ series, limit = 5 }) { + const rows = list(series).slice(0, limit); + if (!rows.length) return {taskLoopCopy("No usage observed.", "未观察到用量。")}; + return ( + + {rows.map((row, index) => ( + + + {index + 1} + {usageSeriesLabel(row.name)} + + {formatUsageNumber(row.total)} + + ))} + + ); +} + +function taskLoopModelUsageColumns() { + return [ + { + key: "model", + title: taskLoopCopy("Model", "模型"), + minWidth: "180px", + render: (row) => {usageSeriesLabel(row.model)}, + }, + { + key: "responseCount", + title: taskLoopCopy("Responses", "响应数"), + width: "110px", + align: "right", + render: (row) => {formatUsageNumber(row.responseCount)}, + }, + { + key: "usageFieldObservedCount", + title: taskLoopCopy("Usage fields observed", "观察到用量字段"), + minWidth: "160px", + align: "right", + render: (row) => {formatUsageNumber(row.usageFieldObservedCount)}, + }, + { + key: "nonZeroUsageCount", + title: taskLoopCopy("Non-zero usage", "非零用量记录"), + minWidth: "140px", + align: "right", + render: (row) => {formatUsageNumber(row.nonZeroUsageCount)}, + }, + ]; +} + +function TaskLoopModelUsageTable({ rows }) { + if (!rows.length) return null; + return ( + + + {taskLoopCopy("Model response accounting", "模型响应明细")} + {rows.length} {taskLoopCopy("models", "个模型")} + + + {taskLoopCopy( + "These are response counts, not model-active session counts or a quality comparison.", + "这里统计的是响应次数,不是模型活跃会话数,也不代表模型质量对比。", + )} + +
row.model} + density="compact" + /> + + ); +} + +function TaskLoopLongSessionReview({ usage }) { + const lead = usage?.reviewLead; + const samples = list(usage?.longSessions?.samples); + if (!lead || !samples.length) return null; + const estimate = usage.longSessions?.estimate; + const coverage = lead.sampleCoverage; + const pendingCount = coverage?.shown ?? samples.length; + const analyzedCount = usage.selection?.analyzedSessionCount ?? 0; + const longestActiveMinutes = usage.longSessions?.longestActiveMinutes ?? Math.max(...samples.map((sample) => Number(sample.activeMinutes ?? 0))); + return ( + + + + +

{taskLoopCopy(`${pendingCount} long sessions need review`, `${pendingCount} 个长会话待复核`)}

+ {pendingCount} {taskLoopCopy("pending", "待复核")} +
+ + {taskLoopCopy( + `${pendingCount} of ${formatUsageNumber(analyzedCount)} analyzed sessions crossed the ${estimate?.activeThresholdMinutes ?? 45}-minute estimate threshold; the longest estimate is ${formatActivityMinutes(longestActiveMinutes)}. Treat them as investigation leads until reviewed.`, + `${formatUsageNumber(analyzedCount)} 个已分析会话中有 ${pendingCount} 个超过 ${estimate?.activeThresholdMinutes ?? 45} 分钟估算阈值,最长估算为 ${formatActivityMinutes(longestActiveMinutes)}。在人工复核前,只将其视为调查线索。`, + )} + +
+ + {taskLoopCopy(`Review ${pendingCount} sessions`, `复核 ${pendingCount} 个会话`)} + +
+ + {samples.map((sample, index) => { + const failureCount = Number(sample.failureCount ?? 0); + const roleLabel = sample.role === "user-thread-candidate" + ? taskLoopCopy("Main-thread candidate", "主线程候选") + : sample.role === "child-agent-candidate" + ? taskLoopCopy("Child-Agent candidate", "子 Agent 候选") + : sample.role; + return ( + + + + {sample.alias} + + {sample.userInputSummary} + {taskLoopCopy("Role", "角色")}: {roleLabel} + + + + {taskLoopCopy("Estimated active time", "估算活跃时长")} + {formatActivityMinutes(sample.activeMinutes)} + + + {formatUsageNumber(failureCount)} {taskLoopCopy("failures", "失败事件")} + + + {index < samples.length - 1 ? : null} + + ); + })} + + {estimate ? ( + + {taskLoopCopy( + `Estimate boundary: event gaps are capped at ${estimate.gapCapMinutes} minutes and gaps over ${estimate.idleGapMinutes} minutes are treated as idle.`, + `估算边界:事件间隔最多计 ${estimate.gapCapMinutes} 分钟,超过 ${estimate.idleGapMinutes} 分钟按空闲处理。`, + )} + + ) : null} +
+ ); +} + +function TaskLoopProjectUsage({ activity, usage }) { + if (!activity && !usage) return null; + const activeMinutes = list(activity?.sessions?.activeMinutes).reduce((sum, value) => sum + Number(value ?? 0), 0); + const census = usage?.selection; + const longSessions = usage?.longSessions; + const skillUses = usageSeriesTotal(activity?.skills); + const analyzedSessions = census + ? `${formatUsageNumber(census.analyzedSessionCount)} / ${formatUsageNumber(census.eligibleSessionCount)}` + : activity ? formatUsageNumber(activity.sessions?.total) : null; + return ( + + {activity ? : null} + {activity ? : null} + + + {taskLoopCopy("Activity insights", "使用概览")} + + {activity ? : null} + {activity ? : null} + {longSessions ? : null} + + + {taskLoopCopy("Most used Skills", "最常使用的 Skills")} + + + + + ); +} + +function TaskLoopUsageMethodology({ usage }) { + if (!usage) return null; + const census = usage.selection; + const accounting = usage.accounting; + const roles = usage.roles; + const outcomeReview = usage.outcomeReview; + const taskSelection = taskLoopCoverage().selection ?? {}; + const modelUsage = list(usage.modelUsage); + const hasCoverage = census || Object.keys(taskSelection).length || roles || accounting; + if (!hasCoverage && !modelUsage.length && !usage.reviewLead) return null; + + return ( + + {census || Object.keys(taskSelection).length ? ( + + {taskLoopMeasurementBoundaryText(taskSelection, census)} + + ) : null} + {roles || accounting ? ( + + {roles ? ( + + {taskLoopCopy("Session composition", "会话构成")} + + + + ) : null} + {accounting ? ( + + {taskLoopCopy("Measurement coverage", "计量覆盖")} + + + + + + + ) : null} + + ) : null} + {modelUsage.length ? : null} + + {accounting?.mode === "effort-proxy" + ? taskLoopCopy("Active time and model-session counts are effort proxies; exact token or credit savings are unavailable.", "活跃时间和模型会话数仅代表投入;目前无法精确计算 token 或 credit 节省。") + : taskLoopCopy("Usage totals describe observed activity, not counterfactual savings.", "用量只描述已观察活动,不代表反事实节省。")} + {outcomeReview && !outcomeReview.comparableModelOutcomeEvidence + ? taskLoopCopy(" Model outcomes need a controlled A/B before comparison.", " 模型效果需要通过受控 A/B 后才能比较。") + : ""} + + + ); +} + +function usageTrendLeader(series) { + return series.reduce((leader, row) => Number(row.total ?? 0) > Number(leader?.total ?? -1) ? row : leader, null); +} + +function usageTrendRange(categories) { + if (!categories.length) return taskLoopCopy("Latest observations", "最近观测"); + if (categories.length === 1) return categories[0]; + return `${categories[0]} – ${categories[categories.length - 1]}`; +} + +function TaskLoopUsageTrend({ title, totalLabel, leaderDescription, series, categories }) { + if (!series.length) return null; + const total = series.reduce((sum, row) => sum + Number(row.total ?? 0), 0); + const leader = usageTrendLeader(series); + const range = usageTrendRange(categories); + return ( + + + + + {title} + + {taskLoopCopy( + `${range} · ${formatUsageNumber(total)} ${totalLabel}`, + `${range} · 共 ${formatUsageNumber(total)} ${totalLabel}`, + )} + + + + {leader ? ( + + {usageSeriesLabel(leader.name)} · {formatUsageNumber(leader.total)} {totalLabel} + {leaderDescription} + + ) : null} + + + + ); +} + +function TaskLoopUsageTrends({ activity }) { + if (!activity) return null; + const chartWindow = usageChartWindow(activity); + const categories = chartWindow.categories; + const modelSeries = visibleUsageSeries(activity.models, chartWindow.offset); + const skillSeries = visibleUsageSeries(activity.skills, chartWindow.offset); + if (!modelSeries.length && !skillSeries.length) return null; + return ( + +

{taskLoopCopy("Usage trends", "用量趋势")}

+ + + + +
+ ); +} + +function taskLoopSessionInsightTitle(id) { + const labels = { + "session-insight:source-coverage": ["Source coverage", "数据覆盖"], + "session-insight:validation-behavior": ["Validation behavior", "验证行为"], + "session-insight:post-edit-validation": ["Post-edit validation", "改动后验证"], + "session-insight:execution-friction": ["Execution friction", "执行摩擦"], + "session-insight:tool-mix": ["Tool mix", "工具使用"], + "session-insight:observed-hooks": ["Observed hooks", "Hook 执行"], + "session-insight:planning-workflow": ["Planning workflow", "规划工作流"], + "session-insight:session-complexity": ["Session complexity", "会话复杂度"], + "session-insight:session-usage-efficiency": ["Session effort", "会话投入"], + }[id]; + return labels ? taskLoopCopy(labels[0], labels[1]) : id; +} + +function taskLoopSessionInsightConfidence(row) { + return list(row?.labels).map(textValue).find((value) => ["High", "Medium", "Low"].includes(value)) ?? ""; +} + +function taskLoopSessionInsightColumns() { + return [ + { + key: "id", + title: taskLoopCopy("Observation", "观察主题"), + minWidth: "150px", + render: (row) => {taskLoopSessionInsightTitle(row.id)}, + }, + { + key: "summary", + title: taskLoopCopy("What was observed", "观察说明"), + minWidth: "420px", + render: (row) => {row.summary ?? "—"}, + }, + { + key: "confidence", + title: taskLoopCopy("Confidence", "可信度"), + minWidth: "120px", + render: (row) => { + const confidence = taskLoopSessionInsightConfidence(row); + return confidence ? {confidenceLabel(confidence)} : ; + }, + }, + { + key: "evidenceRefs", + title: taskLoopCopy("Evidence", "证据"), + width: "80px", + align: "right", + render: (row) => {list(row.evidenceRefs).length}, + }, + ]; +} + +function taskLoopSessionInsightDetail(row) { + const evidenceRefs = list(row?.evidenceRefs); + return ( + {taskLoopCopy("View raw observation metadata", "查看原始观察元数据")}} + bodyStyle={{ padding: "6px 0 2px 16px" }} + headerStyle={{ borderBottom: "none", minHeight: 24 }} + > + + + {taskLoopCopy("Insight ID", "洞察 ID")}: {row.id} · {taskLoopCopy("Status", "状态")}: {row.status ?? "—"} · {taskLoopCopy("Kind", "类型")}: {row.kind ?? "—"} · {taskLoopCopy("Model", "模型版本")}: {row.modelVersion ?? "—"} + + {list(row.labels).length ? ( + {row.labels.map((label) => {label})} + ) : null} + {evidenceRefs.length ? : {taskLoopCopy("No raw evidence references recorded.", "未记录原始证据引用。")}} + + + ); +} + +function taskLoopRepresentativeSessionInsights(entries) { + const preferredIds = [ + "session-insight:post-edit-validation", + "session-insight:execution-friction", + "session-insight:tool-mix", + ]; + const preferred = preferredIds + .map((id) => entries.find((entry) => entry?.id === id)) + .filter((entry) => entry && list(entry.evidenceRefs).length > 0); + const remaining = entries + .filter((entry) => list(entry?.evidenceRefs).length > 0 && !preferred.includes(entry)) + .sort((left, right) => list(right.evidenceRefs).length - list(left.evidenceRefs).length); + return [...preferred, ...remaining].slice(0, 3); +} + +function TaskLoopSessionInsightsDialog({ entries }) { + return ( + + {taskLoopCopy(`View all ${entries.length}`, `查看全部 ${entries.length} 条`)} + + )} + title={taskLoopCopy("All session observations", "全部会话观察")} + closeLabel={taskLoopCopy("Close", "关闭")} + maxWidth={1040} + > + + + {taskLoopCopy( + "These are candidate observations projected from session evidence. Read them as investigation leads, not confirmed user intent or outcome claims.", + "这些是从会话证据投影出的候选观察,用于指引后续调查,不等同于已确认的用户意图或结果结论。", + )} + +
row.id} + density="compact" + renderDetail={taskLoopSessionInsightDetail} + /> + + + ); +} + +function TaskLoopSessionInsights() { + const entries = list(report.summary?.semanticFacets?.entries); + if (!entries.length) return null; + const representativeEntries = taskLoopRepresentativeSessionInsights(entries); + const rowTones = ["success", "warning", "info"]; + return ( + + +

{taskLoopCopy("Session observations", "会话观察")}

+ {entries.length} {taskLoopCopy("observations", "条观察")} +
+ + {taskLoopCopy( + "Representative evidence-bearing observations for follow-up investigation and priority judgment.", + "按主题呈现的代表性观察,用于指引后续调查与优先级判断。", + )} + + + {representativeEntries.map((row, index) => { + const confidence = taskLoopSessionInsightConfidence(row); + const evidenceCount = list(row.evidenceRefs).length; + return ( + + + + + {index + 1} + {taskLoopSessionInsightTitle(row.id)} + + + {row.summary ?? "—"} + + + {confidence ? {confidenceLabel(confidence)} : null} + + {evidenceCount} {taskLoopCopy(evidenceCount === 1 ? "evidence item" : "evidence items", "条证据")} + + + + + + ); + })} + + + + +
+ ); +} + +function TaskLoopFindingDialog({ row }) { + const expectedOutput = list(row.expectedOutput).filter((item) => textValue(item)); + const dimensionRefs = list(row.dimensionRefs); + return ( + {taskLoopCopy("View details", "查看详情")} + )} + title={row.title ?? row.id} + closeLabel={taskLoopCopy("Close", "关闭")} + maxWidth={880} + footer={( + + + {taskLoopCopy("Plan AI Fix", "规划 AI 修复")} + + + )} + > + + + {severityLabel(row.severity)} + {dimensionRefs.map((dimensionRef) => ( + {taskLoopDimensionLabel(dimensionRef)} + ))} + + + {taskLoopCopy("Cause", "原因")} + + {textValue(row.reason) || taskLoopCopy("No cause was recorded.", "未记录原因。")} + + + + + {taskLoopCopy("Expected Output", "预期结果")} + {expectedOutput.length ? expectedOutput.map((output, index) => ( + + {index + 1} + {output} + + )) : ( + {taskLoopCopy("No expected output was recorded.", "未记录预期结果。")} + )} + + + + ); +} + +function TaskLoopFindingCard({ row }) { + const [dimensionRef] = list(row.dimensionRefs); + return ( + + + + + + {severityLabel(row.severity)} + {dimensionRef ? {taskLoopDimensionLabel(dimensionRef)} : null} + + + {row.title ?? row.id} + + {textValue(row.reason) ? ( + + {row.reason} + + ) : null} + + + + + + {taskLoopCopy("Plan AI Fix", "规划 AI 修复")} + + + + + + + + ); +} + +function TaskLoopFindingCards({ findings }) { + return ( + + {findings.map((row) => )} + + ); +} + +function taskLoopSuggestionKindLabel(kind) { + const labels = { + "try-existing": ["Try existing", "试用已有能力"], + "working-pattern": ["Working pattern", "有效模式"], + "loop-candidate": ["Loop candidate", "循环候选"], + horizon: ["Horizon", "中长期"], + }[kind] ?? ["Suggestion", "建议"]; + return taskLoopCopy(labels[0], labels[1]); +} + +function TaskLoopSuggestionDialog({ row }) { + const prerequisites = list(row.prerequisites).map(textValue).filter(Boolean); + const blockedBy = list(row.blockedBy).map(textValue).filter(Boolean); + return ( + {taskLoopCopy("Review suggestion", "查看建议")}} + title={row.title ?? row.id} + closeLabel={taskLoopCopy("Close", "关闭")} + maxWidth={760} + > + + + {taskLoopSuggestionKindLabel(row.kind)} + {confidenceLabel(row.confidence)} + + + {taskLoopCopy("Why this is worth trying", "为什么值得尝试")} + {row.reason} + + + + + {taskLoopCopy("Owner", "负责人")} + {row.owner} + + + {taskLoopCopy("Validation", "验证")} + {row.validation} + + + + {taskLoopCopy("Next step", "下一步")} + {row.nextStep} + + {prerequisites.length ? ( + + {taskLoopCopy("Prerequisites", "前置条件")} + {prerequisites.map((item, index) => ( + + {index + 1} + {item} + + ))} + + ) : null} + {blockedBy.length ? ( + + {taskLoopCopy("Blocked by", "阻塞项")} + {blockedBy.map((item, index) => ( + + {index + 1} + {item} + + ))} + + ) : null} + + + ); +} + +function TaskLoopSuggestionCard({ row }) { + return ( + + + + + {taskLoopSuggestionKindLabel(row.kind)} + {confidenceLabel(row.confidence)} + + + {row.title ?? row.id} + + + {row.reason} + + + {taskLoopCopy("Next step", "下一步")} + {row.nextStep} + + + + {row.owner} + + + + + + ); +} + +function TaskLoopSuggestionCards({ suggestions }) { + return ( + + {suggestions.map((row) => )} + + ); +} + +function taskLoopDeliveryOutcomeLabel(boundary) { + if (!boundary || !Object.hasOwn(boundary, "deliveryEvidenceLevels")) { + return taskLoopCopy("Not supplied", "未提供"); + } + const levels = list(boundary?.deliveryEvidenceLevels).map(textValue).filter(Boolean); + if (!levels.length || levels.every((value) => ["none", "unobserved", "not-observed"].includes(value.toLowerCase()))) { + return taskLoopCopy("Not observed", "未观察到"); + } + return levels.join(", "); +} + +function taskLoopCoverageFraction(value, analyzedField, eligibleField) { + if (!value || !Object.hasOwn(value, analyzedField) || !Object.hasOwn(value, eligibleField)) return null; + const analyzed = Number(value[analyzedField]); + const eligible = Number(value[eligibleField]); + return Number.isInteger(analyzed) && analyzed >= 0 && Number.isInteger(eligible) && eligible >= 0 + ? `${formatUsageNumber(analyzed)}/${formatUsageNumber(eligible)}` + : null; +} + +function taskLoopMeasurementBoundaryText(selection, usageSelection) { + const sample = taskLoopCoverageFraction(selection, "analyzedCount", "eligibleCount"); + const activity = taskLoopCoverageFraction(usageSelection, "analyzedSessionCount", "eligibleSessionCount"); + if (sample && activity) { + return taskLoopCopy( + `Work-stage conclusions use ${sample} stratified sample sessions; activity accounting covers ${activity}. Use this evidence to locate review leads, not to prove efficiency or model quality.`, + `工作环节结论来自 ${sample} 个分层抽样会话;活动统计覆盖 ${activity}。当前证据可用于定位线索,不足以证明效率或模型质量。`, + ); + } + if (sample) { + return taskLoopCopy( + `Work-stage conclusions use ${sample} stratified sample sessions. Activity and model accounting were not supplied, so no usage conclusion is available.`, + `工作环节结论来自 ${sample} 个分层抽样会话。未提供活动与模型统计,因此无法形成用量结论。`, + ); + } + if (activity) { + return taskLoopCopy( + `Activity accounting covers ${activity}. Sampling provenance was not supplied, so usage is shown without a work-stage sampling conclusion.`, + `活动统计覆盖 ${activity}。未提供抽样来源,因此这里只展示用量,不形成工作环节抽样结论。`, + ); + } + return taskLoopCopy( + "Session measurement context was not supplied, so no sampling, usage, or model conclusion is available.", + "未提供会话计量上下文,因此无法形成抽样、用量或模型结论。", + ); +} + +function taskLoopMeasurementModelLabel(usage, modelUsage) { + if (!usage) return taskLoopCopy("Usage unavailable", "用量不可用"); + return `${modelUsage.length} ${taskLoopCopy("models", "个模型")}`; +} + +function taskLoopSelectionDetailText(selection) { + const coverage = taskLoopCoverageFraction(selection, "analyzedCount", "eligibleCount"); + if (!coverage) return taskLoopCopy("Not supplied", "未提供"); + return taskLoopCopy( + `${textValue(selection.strategy) || "unknown"}; ${coverage} eligible sessions analyzed.`, + `${textValue(selection.strategy) || "未知"};已分析 ${coverage} 个符合条件的会话。`, + ); +} + +function taskLoopTaskEvidenceText(coverage) { + const fields = ["episodeCount", "editedEpisodeCount", "closedEpisodeCount", "recoveredEpisodeCount"]; + if (!fields.every((field) => Object.hasOwn(coverage, field))) return taskLoopCopy("Not supplied", "未提供"); + const [episodes, edited, closed, recovered] = fields.map((field) => Number(coverage[field])); + if (![episodes, edited, closed, recovered].every((value) => Number.isInteger(value) && value >= 0)) { + return taskLoopCopy("Not supplied", "未提供"); + } + return taskLoopCopy( + `${episodes} episodes; ${edited} with changes; ${closed} closed; ${recovered} repaired and passed.`, + `${episodes} 个任务片段;${edited} 个包含改动;${closed} 个已闭环;${recovered} 个修复并通过。`, + ); +} + +function taskLoopLearningDetailText(learning) { + if (!Object.hasOwn(learning, "state") && !Array.isArray(learning.interventions)) { + return taskLoopCopy("Not supplied", "未提供"); + } + return taskLoopCopy( + `${learningStateLabel(learning.state)}; ${list(learning.interventions).length} declared intervention(s).`, + `${learningStateLabel(learning.state)};${list(learning.interventions).length} 项已声明的改进。`, + ); +} + +function TaskLoopEvidenceFact({ label, value, tone = "neutral" }) { + return ( + + + + {label} + + {value} + + + + + ); +} + +function TaskLoopEvidenceDetails({ usage, boundary, manifest, selection, learning, coverage }) { + const hasDetailedBoundary = Object.keys(boundary).length > 0 + || Object.keys(coverage).length > 0 + || Object.keys(learning).length > 0; + return ( + + {hasDetailedBoundary ? ( + + {taskLoopCopy("Sampling and provenance", "抽样与来源")} + {taskLoopCopy("Session selection", "会话抽样")}: {taskLoopSelectionDetailText(selection)} + {taskLoopCopy("Task evidence", "任务证据")}: {taskLoopTaskEvidenceText(coverage)} + {taskLoopCopy("Delivery outcomes", "交付结果")}: {taskLoopDeliveryOutcomeLabel(boundary)}. + {taskLoopCopy("Learning comparison", "学习循环比较")}: {taskLoopLearningDetailText(learning)} + {textValue(learning.summary) ? {learning.summary} : null} + + ) : ( + + {taskLoopCopy( + "This run did not supply task-episode or delivery-outcome evidence, so the report only shows repository signals it could verify.", + "本次运行没有提供任务片段或交付结果证据,因此报告只展示能够验证的仓库信号。", + )} + + )} + + + + + + + + + {usage ? ( + <> + + + + ) : null} + + ); +} + +function TaskLoopEvidenceBoundary({ usage }) { + const boundary = report.summary?.evidenceBoundary ?? {}; + const manifest = boundary.manifest ?? {}; + const selection = manifest.selection ?? {}; + const learning = report.summary?.learningCapture ?? {}; + const coverage = taskLoopCoverage(); + const modelUsage = list(usage?.modelUsage); + const longSessionSamples = list(usage?.longSessions?.samples); + const sessionInsights = list(report.summary?.semanticFacets?.entries); + const activitySelection = usage?.selection ?? {}; + const samplingConfidence = textValue(selection.confidence) + || taskLoopCopy("Not supplied", "未提供"); + const sourceGapValue = Array.isArray(boundary.sourceGaps) ? boundary.sourceGaps.length : "—"; + + return ( + +

{taskLoopCopy("Evidence and methodology", "证据与方法")}

+ + + {taskLoopMeasurementBoundaryText(selection, activitySelection)} + + + + + + + + {usage?.reviewLead && longSessionSamples.length ? ( + <> + + + + ) : null} + {sessionInsights.length ? ( + <> + + + + ) : null} + + + {taskLoopCopy("View measurement and model details", "查看计量与模型明细")} + + {taskLoopCopy("Response accounting, model detail, and sampling method", "响应计量、模型明细与抽样方法")} + + + )} + trailing={{taskLoopMeasurementModelLabel(usage, modelUsage)}} + bodyStyle={{ padding: "12px 0 2px 16px" }} + headerStyle={{ minHeight: 44 }} + > + + +
+ ); +} + +function TaskLoopReport() { + const dimensions = list(report.summary?.dimensions); + const findings = list(report.findings).sort((left, right) => severityRank(left.severity) - severityRank(right.severity)); + const suggestions = list(report.summary?.suggestions); + const sessionOverview = taskLoopSessionOverview(); + const activity = taskLoopUsageActivity(); + const usage = taskLoopUsageEfficiency(); + const usageCoverageInfo = taskLoopUsageCoverageInfo(activity, usage, sessionOverview); + const highFindings = findings.filter((row) => row.severity === "Critical" || row.severity === "High"); + const mediumFindings = findings.filter((row) => row.severity === "Medium"); + return ( + + + + + + {activity || usage ? ( + + +

{taskLoopCopy("Project usage", "项目用量")}

+ {usageCoverageInfo ? ( + + i + + ) : null} +
+ +
+ ) : null} + + + +

{taskLoopCopy("Prioritized improvements", "优先优化项")}

+ + {taskLoopCopy( + `${findings.length} total · ${highFindings.length} high · ${mediumFindings.length} medium`, + `共 ${findings.length} 项 · ${highFindings.length} 个高优先级 · ${mediumFindings.length} 个中优先级`, + )} + +
+ + {suggestions.length ? ( + <> + + + + {taskLoopCopy("Suggestions", "建议")} + + {taskLoopCopy( + "Evidence-bound capabilities and patterns worth trying next. Suggestions are advisory and do not include an AI Fix action.", + "基于证据、值得下一步尝试的能力与模式。建议仅供参考,不包含 AI 修复动作。", + )} + + + {suggestions.length} {taskLoopCopy("suggestions", "条建议")} + + + + ) : null} +
+ + +

{taskLoopCopy("Agent Customize", "Agent 自定义")}

+ + {taskLoopCopy( + "Discovered sources, not a quality or maturity score.", + "这里只展示已发现来源,不代表质量或成熟度评分。", + )} + + +
+ + + + +
+ ); +} + +export default function QoderHarnessReport() { + return ; +} diff --git a/static/angle.html b/static/angle.html index b1d6de375..fb62e674f 100644 --- a/static/angle.html +++ b/static/angle.html @@ -17,21 +17,21 @@ } catch(e) {} })(); - - - - - - + + + + + + - + diff --git a/static/api-settings.html b/static/api-settings.html index 3a3026c41..c4f7d1a7b 100644 --- a/static/api-settings.html +++ b/static/api-settings.html @@ -16,12 +16,12 @@ } catch(e) {} })(); - - - - - - + + + + + +
@@ -565,6 +565,6 @@
- + diff --git a/static/asset-manager.html b/static/asset-manager.html index 1b8b6a087..574a3441a 100644 --- a/static/asset-manager.html +++ b/static/asset-manager.html @@ -16,10 +16,10 @@ } catch(e) {} })(); - - - - + + + +
@@ -47,6 +47,6 @@
- + diff --git a/static/canvas-list.html b/static/canvas-list.html index 8e1735a82..06d09047b 100644 --- a/static/canvas-list.html +++ b/static/canvas-list.html @@ -16,13 +16,13 @@ } catch(e) {} })(); - - - - - - - + + + + + + +
@@ -90,6 +90,6 @@
- + diff --git a/static/canvas.html b/static/canvas.html index caef2a193..21557fabe 100644 --- a/static/canvas.html +++ b/static/canvas.html @@ -16,13 +16,13 @@ } catch(e) {} })(); - - - - - - - + + + + + + +
@@ -350,7 +350,7 @@
- - + + diff --git a/static/comfyui-settings.html b/static/comfyui-settings.html index e23df7d6b..82ac6fbe2 100644 --- a/static/comfyui-settings.html +++ b/static/comfyui-settings.html @@ -15,12 +15,12 @@ } catch(e) {} })(); - - - - - - + + + + + +
@@ -122,6 +122,6 @@ preview
- + diff --git a/static/css/smart-canvas.css b/static/css/smart-canvas.css index 3ba32ed56..41f32673e 100644 --- a/static/css/smart-canvas.css +++ b/static/css/smart-canvas.css @@ -1220,8 +1220,10 @@ input[type=range].smart-range::-webkit-slider-thumb { -webkit-appearance:none; w .agent-thinking-btn.active:hover { opacity:.88; } .agent-thinking-tooltip { position:absolute; bottom:calc(100% + 8px); right:0; + width:200px; background:#1e293b; color:#f1f5f9; - padding:8px 12px; border-radius:8px; white-space:nowrap; + padding:8px 12px; border-radius:8px; + text-align:left; white-space:normal; line-height:1.5; pointer-events:none; opacity:0; visibility:hidden; transform:translateY(4px); transition:opacity .14s ease, transform .14s ease, visibility .14s ease; @@ -1328,6 +1330,13 @@ input[type=range].smart-range::-webkit-slider-thumb { -webkit-appearance:none; w .agent-quick-btn.primary:hover { opacity:.85; } .agent-quick-btn i,.agent-quick-btn svg { width:11px; height:11px; } +/* 变体数量选择气泡 */ +.agent-variant-wrap { position:relative; display:inline-flex; } +.agent-variant-pop { position:absolute; bottom:calc(100% + 6px); left:50%; transform:translateX(-50%); display:none; gap:2px; padding:4px 6px; border-radius:8px; border:1px solid var(--line); background:var(--card); box-shadow:0 4px 12px rgba(0,0,0,.12); z-index:20; white-space:nowrap; } +.agent-variant-pop.open { display:flex; } +.agent-variant-opt { display:inline-flex; align-items:center; justify-content:center; min-width:28px; height:24px; padding:0 6px; border-radius:6px; border:1px solid var(--line); background:var(--soft); color:var(--text); font-size:11px; font-weight:700; cursor:pointer; transition:all .12s ease; } +.agent-variant-opt:hover { background:var(--strong); color:var(--strong-text); border-color:var(--strong); } + /* 分析结果卡片 */ .agent-analysis-card { width:100%; max-width:320px; border-radius:12px; border:1px solid var(--line); background:var(--card); overflow:hidden; } .agent-analysis-body { padding:10px 12px; font-size:11px; line-height:1.6; color:var(--text); white-space:pre-wrap; word-break:break-word; max-height:200px; overflow-y:auto; } diff --git a/static/enhance.html b/static/enhance.html index 5bdd2ddb8..59b9bd455 100644 --- a/static/enhance.html +++ b/static/enhance.html @@ -17,14 +17,14 @@ } catch(e) {} })(); - - - - - - + + + + + + - + diff --git a/static/gpt-chat.html b/static/gpt-chat.html index 8e262772a..be0ca47d7 100644 --- a/static/gpt-chat.html +++ b/static/gpt-chat.html @@ -16,13 +16,13 @@ } catch(e) {} })(); - - - - - + + + + + - + diff --git a/static/online.html b/static/online.html index c76de242d..0d3a6a846 100644 --- a/static/online.html +++ b/static/online.html @@ -16,14 +16,14 @@ } catch(e) {} })(); - - - - - - + + + + + + - + - + From 197ee0960a5ed665e0ce7b2f156f09ef009238c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=96=E4=BC=A0=E7=9A=84=E8=8B=B9=E6=9E=9C=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E6=9C=AC?= Date: Thu, 23 Jul 2026 02:40:49 +0800 Subject: [PATCH 65/67] =?UTF-8?q?v2.1=20=E7=81=B5=E6=84=9F=E5=BA=93?= =?UTF-8?q?=EF=BC=9ACivitai=20=E5=9B=BE=E6=BA=90=20+=20=E7=9C=9F=E5=85=B3?= =?UTF-8?q?=E9=94=AE=E8=AF=8D=E6=90=9C=E7=B4=A2=20+=20=E6=A0=87=E7=AD=BE?= =?UTF-8?q?=E7=AD=9B=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增灵感库面板:Civitai 海量 AI 图片排序浏览(热门/最新/点赞/收藏) - 真关键词搜索:接入 Civitai Meilisearch 搜索服务,中英文均可搜 - 中文搜索词典:常用中文词自动映射 Civitai 英文标签 - 分类标签筛选:chips 换行布局 + 更多标签分组下拉面板(6组58标签) - 一键导入画布:图片本地化(刷新不丢)+ 双重去重 + 来源署名 - 体验优化:按真实宽高比预留高度修复布局跳动、加载重试、429降级、TTL缓存 - README 版本表格改为大白话并新增 v2.1 说明 --- CHANGELOG.md | 19 ++ README.md | 17 +- VERSION | 2 +- main.py | 218 ++++++++++++++++++ static/css/smart-canvas.css | 69 ++++++ static/index.html | 20 +- static/js/smart-canvas.js | 443 +++++++++++++++++++++++++++++++++++- static/smart-canvas.html | 38 +++- 8 files changed, 796 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 571ac92a8..7ed1a29e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # 版本更新大纲 +#### v2.1 — 灵感库(Civitai 图源 + 真搜索 + 标签筛选) + +**灵感库:** +- 新增“灵感库”面板:内置 Civitai 海量 AI 图片,排序浏览(热门/最新/点赞/收藏) +- 真关键词搜索:接入 Civitai 的 Meilisearch 搜索服务,中文/英文都能搜(如“猫”“海报”) +- 中文搜索词典:常用中文词自动映射成 Civitai 英文标签 +- 分类标签筛选:chips 换行布局 + “更多标签”分组下拉面板(6 组 58 个标签) + +**导入画布:** +- 一键导入喜欢的图当参考,自动下载到本地(刷新不丢、加载秒开) +- 双重去重:按图片 ID 命名 + 画布节点查重,不重复导入 +- 来源署名:保留 Civitai 链接与作者 + +**体验优化:** +- 图片按真实宽高比预留高度,懒加载时不再上下跳动 +- 图片加载失败自动重试 + 点击重试 +- 429 限流友好提示、TTL 缓存、浏览去重 +- API Key 强制前置配置(无 Key 进配置页) + #### v2.0 — Agent 架构大改版(LLM-first 意图决策) **架构重构:** diff --git a/README.md b/README.md index 1f024f9f6..9a9d662c0 100644 --- a/README.md +++ b/README.md @@ -21,14 +21,15 @@ Supports comfyui/API calls/modelscope calls | 版本 | 日期 | 说明 | |------|------|------| -| v1.0 | 2026-07-18 | AI Agent 面板基线:聊天面板、画布联动生图、多对话管理、参数面板、结构化确认流程、生图占位 | -| v1.1 | 2026-07-19 | 系统提示词重写、修改场景智能区分、LLM 任务后端化、刷新恢复、WebSocket 实时通知、占位比例修复、并发多图不叠加、提示词展开收起 | -| v1.3 | 2026-07-19 | 思维模式开关(扩写/确认/重新生成/修改)、全模型模糊需求统一触发选择、发送按钮失效修复、LLM 任务 5 分钟超时保护、恢复逻辑超时清理 | -| v1.4 | 2026-07-19 | 多图确认流程重构(全部确认后统一生图)、prompts 状态机、内联编辑、Skill 完整保留(首因近因双语)、思维模式兜底 bug 修复 | -| v1.5 | 2026-07-19 | 硬软参数分层(数量=软参数输入框>工具栏,比例/分辨率=硬参数工具栏)、Skill=单张图样式不决定数量、统一数量决策函数 | -| v1.5.1 | 2026-07-19 | 单 generation 多图泄漏修复(API 返回多图限制+参考图 URL 过滤)、count=1 强制范围扩大到所有直接模式 | -| v1.6 | 2026-07-20 | LLM 与生图解耦(思维模式 OFF 跳过 LLM 直接生图)、思维模式多轮维度采集(渐进式问答+自定义输入)、LLM 模型选择移入思维模式面板、框选批量发送至 Agent、附件拖拽排序、发送按钮 bug 修复 | -| **v2.0** | **2026-07-21** | **Agent 架构大改版**:意图路由重构(8种意图智能分发)、流式输出、分析/反推分离、生成后快捷操作、clarify 主动澄清机制(详见下方大纲) | +| v1.0 | 2026-07-18 | 首发:右侧加了 AI 聊天框,能边聊边在画布上画图;可建多个对话;能调图片的比例、清晰度、数量 | +| v1.1 | 2026-07-19 | AI 更懂你的话(自动分清“改风格”还是“换主体”);刷新网页不丢正在画的图;画图有实时提醒 | +| v1.3 | 2026-07-19 | 新增“思维模式”:AI 先帮你把想法补全再画;需求说不清时自动弹选项让你选;修复发送按钮失灵 | +| v1.4 | 2026-07-19 | 多张图全部确认后再一起生成;提示词可直接在卡片上改;AI 记忆更稳 | +| v1.5 | 2026-07-19 | “数量”用输入框填、“比例/清晰度”用按钮选,分工更清楚 | +| v1.5.1 | 2026-07-19 | 修复“只要 1 张却给好几张”的问题 | +| v1.6 | 2026-07-20 | 关掉思维模式直接画图、不等 AI;思维模式会一步步追问帮你补细节;可框选多张图一起发给 AI;附件能拖动排序 | +| v2.0 | 2026-07-21 | AI 大脑升级:自动判断你想“画图/修改/分析/反推提示词”;回复逐字显示;生成后有“修改/变体/反推”快捷按钮;听不懂会主动问你(详见下方大纲) | +| **v2.1** | **2026-07-22** | **新增“灵感库”**:内置海量 AI 图片,支持中文/英文搜索(如搜“猫”“海报”)和分类标签筛选;喜欢的图一键导入画布当参考;图片自动存本地、刷新不丢;浏览更顺滑、不再上下跳动 | ### v2.0 改版大纲 diff --git a/VERSION b/VERSION index c0ec837a4..28c213927 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v2.0 +v2.1 diff --git a/main.py b/main.py index 7afb313db..6d0045b77 100755 --- a/main.py +++ b/main.py @@ -1800,6 +1800,224 @@ def parse_prompt_template_markdown(text: str): }) return templates +YOUMIND_PROMPTS_API = "https://youmind.com/youmarketing-api/prompts" + +@app.get("/api/inspire-prompts") +async def inspire_prompts( + q: str = "", + category: str = "", + page: int = 1, + limit: int = 20, + sort_by: str = "views", +): + """灵感库提示词数据代理:转发 YouMind 公开提示词 API(提示词 + 参考图 + 分类 + 分页)。 + YouMind API 有简单防盗链(校验 Origin/Referer),此处伪造请求头绕过;图片无防盗链,前端直连。""" + payload = { + "model": "gpt-image-2", + "sortBy": sort_by if sort_by in ("views", "latest", "likes") else "views", + "sortOrder": "desc", + "page": max(1, int(page)), + "limit": min(60, max(1, int(limit))), + "locale": "zh-CN", + } + if q and q.strip(): + payload["q"] = q.strip() + if category and category.strip(): + payload["categories"] = category.strip() + headers = { + "Content-Type": "application/json", + "Origin": "https://youmind.com", + "Referer": "https://youmind.com/zh-CN/gpt-image-2-prompts/explore", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36", + } + try: + async with httpx.AsyncClient(timeout=httpx.Timeout(connect=15.0, read=40.0, write=20.0, pool=15.0)) as client: + resp = await client.post(YOUMIND_PROMPTS_API, json=payload, headers=headers) + except httpx.HTTPError as e: + raise HTTPException(status_code=502, detail=f"提示词库请求失败:{e}") + if resp.status_code != 200: + raise HTTPException(status_code=502, detail=f"提示词库上游错误:{resp.status_code}") + try: + data = resp.json() + except Exception: + raise HTTPException(status_code=502, detail="提示词库响应解析失败") + items = [] + for p in data.get("prompts", []) or []: + media = p.get("media") or [] + thumbs = p.get("mediaThumbnails") or [] + refs = p.get("referenceImages") or [] + cats = p.get("promptCategories") or [] + cat_slugs = [c.get("slug") if isinstance(c, dict) else c for c in cats] + items.append({ + "id": p.get("id"), + "title": p.get("title") or "", + "description": p.get("description") or "", + "prompt": p.get("content") or "", + "promptZh": p.get("translatedContent") or "", + "image": media[0] if media else "", + "thumb": thumbs[0] if thumbs else (media[0] if media else ""), + "refs": refs, + "needRefs": bool(p.get("needReferenceImages")), + "categories": cat_slugs, + "likes": p.get("likes") or 0, + }) + return { + "items": items, + "total": data.get("total") or 0, + "page": data.get("page") or page, + "totalPages": data.get("totalPages") or 0, + "hasMore": bool(data.get("hasMore")), + } + +CIVITAI_IMAGES_API = "https://civitai.com/api/v1/images" +CIVITAI_SEARCH_API = "https://search-new.civitai.com/multi-search" +# Civitai 前端内置的 Meilisearch 公开搜索 key(仅搜索、无写入权限,免登录可用) +CIVITAI_SEARCH_KEY = "8c46eb2508e21db1e9828a97968d91ab1ca1caa5f70a00e88a2ba1e286603b61" +CIVITAI_IMG_CDN = "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA" + +async def civitai_meili_search(query: str, limit: int, cursor: str): + """通过 Civitai 的 Meilisearch 搜索图片(真正的关键词搜索,返回 prompt+tags)。""" + offset = int(cursor) if str(cursor).isdigit() else 0 + body = {"queries": [{ + "q": query, + "indexUid": "images_v6", + "limit": limit, + "offset": offset, + "filter": ["(poi != true) AND (combinedNsfwLevel=1)"] + }]} + headers = {"Content-Type": "application/json", "Authorization": f"Bearer {CIVITAI_SEARCH_KEY}"} + try: + async with httpx.AsyncClient(timeout=httpx.Timeout(connect=15.0, read=40.0, write=20.0, pool=15.0)) as client: + resp = await client.post(CIVITAI_SEARCH_API, json=body, headers=headers) + except httpx.HTTPError as e: + raise HTTPException(status_code=502, detail=f"Civitai 搜索失败:{e}") + if resp.status_code == 429: + raise HTTPException(status_code=429, detail="Civitai 限流:请稍后再试") + if resp.status_code != 200: + raise HTTPException(status_code=502, detail=f"Civitai 搜索错误:{resp.status_code}") + data = resp.json() + result = (data.get("results") or [{}])[0] + hits = result.get("hits") or [] + total = result.get("estimatedTotalHits") or 0 + items = [] + for h in hits: + url = h.get("url") or "" + if not url: continue + prompt = h.get("prompt") or "" + items.append({ + "id": h.get("id"), + "image": f"{CIVITAI_IMG_CDN}/{url}/original=true/{url}.jpeg", + "thumb": f"{CIVITAI_IMG_CDN}/{url}/width=500/{url}.jpeg", + "prompt": prompt, + "promptZh": "", + "title": "", + "description": prompt[:100], + "width": h.get("width"), + "height": h.get("height"), + "model": "", + "username": (h.get("user") or {}).get("username") or "", + "tags": h.get("tagNames") or [], + }) + next_offset = offset + limit + has_more = bool(hits) and next_offset < total + return {"items": items, "cursor": str(next_offset) if has_more else "", "hasMore": has_more, "total": total} + +@app.get("/api/inspire-civitai") +async def inspire_civitai(request: Request, cursor: str = "", limit: int = 24, sort: str = "Most Reactions", tag: str = ""): + """灵感库 Civitai 数据源:用户生成的 AI 图片(参考性强)。 + Civitai 接口必须带 User-Agent 头,否则返回空。游标分页。 + 可选 API Key(前端经 X-Civitai-Key 头传入),带 Key 限额更高、更稳定。 + tag 非空时走 Meilisearch 真搜索;否则走 /api/v1/images 排序浏览。""" + limit = min(100, max(1, int(limit))) + search_query = (tag or "").strip() + # 搜索模式:tag 非空 → Meilisearch 关键词搜索 + if search_query: + return await civitai_meili_search(search_query, limit, cursor) + # 浏览模式:/api/v1/images + sort + params = {"limit": str(limit), "nsfw": "false"} + if sort: params["sort"] = sort + if cursor: params["cursor"] = cursor + headers = { + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36", + "Content-Type": "application/json", + } + civitai_key = (request.headers.get("x-civitai-key") or "").strip() + if civitai_key: + headers["Authorization"] = f"Bearer {civitai_key}" + try: + async with httpx.AsyncClient(timeout=httpx.Timeout(connect=15.0, read=40.0, write=20.0, pool=15.0)) as client: + resp = await client.get(CIVITAI_IMAGES_API, params=params, headers=headers) + except httpx.HTTPError as e: + raise HTTPException(status_code=502, detail=f"Civitai 请求失败:{e}") + if resp.status_code == 429: + raise HTTPException(status_code=429, detail="Civitai 限流:请求太频繁,请稍后再试(配置 API Key 可提高限额)") + if resp.status_code != 200: + raise HTTPException(status_code=502, detail=f"Civitai 上游错误:{resp.status_code}") + try: + data = resp.json() + except Exception: + raise HTTPException(status_code=502, detail="Civitai 响应解析失败") + items = [] + for it in data.get("items", []) or []: + url = it.get("url") or "" + if not url: continue + thumb = url.replace("original=true", "width=400") # 缩略图(宽400,兼顾清晰与加载速度) + meta = it.get("meta") or {} + prompt = meta.get("prompt") or "" + tags = [t.get("name") if isinstance(t, dict) else t for t in (it.get("tags") or [])] + items.append({ + "id": it.get("id"), + "image": url, + "thumb": thumb, + "prompt": prompt, + "promptZh": "", + "title": "", + "description": prompt[:100] if prompt else "", + "width": it.get("width"), + "height": it.get("height"), + "model": it.get("baseModel") or "", + "username": it.get("username") or "", + "tags": tags, + }) + next_cursor = (data.get("metadata") or {}).get("nextCursor") or "" + return { + "items": items, + "cursor": next_cursor, + "hasMore": bool(next_cursor), + } + +class InspireLocalizeRequest(BaseModel): + url: str + id: str = "" + +@app.post("/api/inspire-localize") +async def inspire_localize(payload: InspireLocalizeRequest): + """把 Civitai 图片下载保存到本地(中等高清 width=1024),按 civitaiId 命名实现去重。 + 已本地化过的直接返回本地地址,不重复下载。""" + url = (payload.url or "").strip() + cid = str(payload.id or "").strip() + if not url: + raise HTTPException(status_code=400, detail="缺少图片地址") + safe_id = re.sub(r"[^0-9a-zA-Z_-]", "", cid) or uuid.uuid4().hex[:10] + filename = f"civitai_{safe_id}.jpg" + path = output_path_for(filename, "output") + # 去重:已本地化过直接返回本地地址 + if os.path.isfile(path): + return {"url": output_url_for(filename, "output"), "cached": True} + # 中等高清:width=1024 + dl_url = url.replace("original=true", "width=1024") + try: + timeout = httpx.Timeout(connect=20.0, read=120.0, write=60.0, pool=20.0) + async with httpx.AsyncClient(timeout=timeout, follow_redirects=True) as client: + resp = await client.get(dl_url, headers={"User-Agent": "Mozilla/5.0 Chrome/124.0 Safari/537.36"}) + resp.raise_for_status() + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, "wb") as f: + f.write(resp.content) + except Exception as e: + raise HTTPException(status_code=502, detail=f"图片本地化失败:{e}") + return {"url": output_url_for(filename, "output"), "cached": False} + @app.get("/api/app-info") def app_info(): version = current_app_version() diff --git a/static/css/smart-canvas.css b/static/css/smart-canvas.css index 41f32673e..82c9f711c 100644 --- a/static/css/smart-canvas.css +++ b/static/css/smart-canvas.css @@ -20,6 +20,8 @@ html[data-studio-scale="off"].studio-scale-managed .smart-log-toggle, html[data-studio-scale="off"].studio-scale-managed .smart-shortcut-toggle, html[data-studio-scale="off"].studio-scale-managed .smart-workflow-toggle, html[data-studio-scale="off"].studio-scale-managed .asset-toggle, +html[data-studio-scale="off"].studio-scale-managed .agent-toggle, +html[data-studio-scale="off"].studio-scale-managed .inspire-toggle, /* 注意:.composer 不能加进这份 zoom 列表——它在 .world 内用世界坐标定位, 单独 zoom 会让坐标按 scale 重算,离原点越远偏移越大(小屏下直接飞出屏幕外) */ html[data-studio-scale="off"].studio-scale-managed .smart-minimap, @@ -27,6 +29,8 @@ html[data-studio-scale="off"].studio-scale-managed .smart-log-panel, html[data-studio-scale="off"].studio-scale-managed .shortcut-panel, html[data-studio-scale="off"].studio-scale-managed .workflow-panel, html[data-studio-scale="off"].studio-scale-managed .asset-panel, +html[data-studio-scale="off"].studio-scale-managed .agent-panel, +html[data-studio-scale="off"].studio-scale-managed .inspire-panel, html[data-studio-scale="off"].studio-scale-managed .asset-dock, html[data-studio-scale="off"].studio-scale-managed .asset-dialog, html[data-studio-scale="off"].studio-scale-managed .asset-hover-preview, @@ -1165,6 +1169,71 @@ input[type=range].smart-range::-webkit-slider-thumb { -webkit-appearance:none; w .agent-toggle i,.agent-toggle svg { width:16px; height:16px; } .agent-panel { position:absolute; right:22px; top:66px; bottom:22px; z-index:55; width:360px; max-width:calc(100vw - 44px); min-height:0; display:flex; flex-direction:column; gap:10px; padding:12px; border-radius:18px; background:var(--panel); border:1px solid var(--line); box-shadow:0 22px 58px var(--shadow); backdrop-filter:blur(20px); transform:translateX(16px); opacity:0; visibility:hidden; pointer-events:none; transition:opacity .16s ease, transform .16s ease, visibility .16s ease; } .agent-panel.open { opacity:1; visibility:visible; pointer-events:auto; transform:translateX(0); } + +/* --- 灵感库(画布左侧提示词库) --- */ +.inspire-toggle { position:absolute; right:574px; top:22px; z-index:57; height:40px; padding:0 16px; border-radius:999px; background:var(--panel); color:var(--text); border:1px solid var(--line); box-shadow:0 14px 34px var(--shadow); backdrop-filter:blur(16px); display:flex; align-items:center; justify-content:center; gap:8px; font-size:12px; font-weight:750; white-space:nowrap; transition:transform .14s ease, border-color .14s ease; } +.inspire-toggle:hover { border-color:var(--text); transform:translateY(-1px); } +.inspire-toggle.active { background:var(--strong); color:var(--strong-text); border-color:var(--strong); box-shadow:0 16px 38px var(--shadow); } +.inspire-toggle i,.inspire-toggle svg { width:16px; height:16px; } +.inspire-panel { position:absolute; left:0; top:0; bottom:0; z-index:58; width:40vw; max-width:680px; min-width:340px; display:flex; flex-direction:column; gap:10px; padding:14px; background:var(--panel); border-right:1px solid var(--line); box-shadow:18px 0 50px var(--shadow); backdrop-filter:blur(20px); transform:translateX(-102%); opacity:0; visibility:hidden; pointer-events:none; transition:opacity .2s ease, transform .22s cubic-bezier(.4,0,.2,1), visibility .2s ease; } +.inspire-panel.open { opacity:1; visibility:visible; pointer-events:auto; transform:translateX(0); } +.inspire-head { display:flex; align-items:center; gap:8px; } +.inspire-head .asset-title { flex:0 0 auto; } +.inspire-count { flex:1 1 auto; font-size:11px; color:var(--faint); font-weight:600; } +.inspire-search-wrap { position:relative; display:flex; align-items:center; } +.inspire-search-btn { position:absolute; left:6px; width:26px; height:26px; display:flex; align-items:center; justify-content:center; border:none; background:transparent; color:var(--faint); cursor:pointer; border-radius:7px; transition:all .14s ease; z-index:1; } +.inspire-search-btn:hover { color:var(--strong); background:var(--soft); } +.inspire-search-btn i,.inspire-search-btn svg { width:15px; height:15px; } +.inspire-search { width:100%; height:38px; padding:0 12px 0 34px; border-radius:11px; border:1px solid var(--line); background:var(--card); color:var(--text); font-size:12.5px; outline:none; transition:border-color .14s ease; } +.inspire-search:focus { border-color:var(--strong); } +.inspire-cats { display:flex; flex-wrap:wrap; gap:6px; padding-bottom:2px; flex:0 0 auto; } +.inspire-cats::-webkit-scrollbar { display:none; } +.inspire-cat { flex:0 0 auto; height:27px; padding:0 12px; border-radius:999px; border:1px solid var(--line); background:var(--card); color:var(--muted); font-size:11px; font-weight:700; cursor:pointer; white-space:nowrap; transition:all .13s ease; } +.inspire-cat:hover { color:var(--text); border-color:var(--strong); } +.inspire-cat.active { background:var(--strong); color:var(--strong-text); border-color:var(--strong); } +.inspire-cat.more { border-style:dashed; color:var(--faint); } +.inspire-more-tags { flex:0 0 auto; border:1px solid var(--line); background:var(--card); border-radius:12px; padding:10px 10px 4px; margin-top:2px; max-height:264px; overflow-y:auto; scrollbar-width:thin; } +.inspire-more-tags[hidden] { display:none; } +.inspire-mt-group { margin-bottom:9px; } +.inspire-mt-name { font-size:10px; font-weight:800; color:var(--faint); margin-bottom:6px; letter-spacing:.5px; } +.inspire-mt-tags { display:flex; flex-wrap:wrap; gap:5px; } +.inspire-mt-tag { height:24px; padding:0 10px; border-radius:999px; border:1px solid var(--line); background:var(--soft); color:var(--muted); font-size:11px; font-weight:600; cursor:pointer; white-space:nowrap; transition:all .13s ease; } +.inspire-mt-tag:hover { color:var(--text); border-color:var(--strong); } +.inspire-mt-tag.active { background:var(--strong); color:var(--strong-text); border-color:var(--strong); } +.inspire-scroll { flex:1 1 auto; min-height:0; overflow-y:auto; overscroll-behavior:contain; margin:0 -6px; padding:0 6px; scrollbar-width:thin; scrollbar-color:rgba(148,163,184,.42) transparent; } +.inspire-grid { column-count:3; column-gap:4px; } +@media (max-width:1100px){ .inspire-grid { column-count:2; } } +.inspire-card { position:relative; break-inside:avoid; margin-bottom:4px; overflow:hidden; background:var(--soft); cursor:pointer; line-height:0; } +.inspire-card img { width:100%; display:block; opacity:0; transition:opacity .3s ease; } +.inspire-card img.loaded { opacity:1; } +.inspire-card-ph { width:100%; background:var(--soft); animation:inspirePulse 1.2s ease-in-out infinite; } +@keyframes inspirePulse { 0%,100%{opacity:.55} 50%{opacity:.9} } +.inspire-overlay { position:absolute; inset:0; display:flex; flex-direction:column; justify-content:flex-end; gap:8px; padding:10px; background:linear-gradient(to top, rgba(0,0,0,.86) 0%, rgba(0,0,0,.62) 45%, rgba(0,0,0,.28) 100%); opacity:0; transition:opacity .18s ease; line-height:1.5; } +.inspire-card:hover .inspire-overlay { opacity:1; } +.inspire-overlay-title { color:#fff; font-size:12px; font-weight:800; line-height:1.4; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; } +.inspire-overlay-author { color:rgba(255,255,255,.7); font-size:9.5px; font-weight:700; margin-bottom:2px; } +.inspire-overlay-prompt { color:rgba(255,255,255,.92); font-size:10.5px; line-height:1.55; display:-webkit-box; -webkit-line-clamp:5; -webkit-box-orient:vertical; overflow:hidden; } +.inspire-overlay-btn { align-self:flex-start; display:inline-flex; align-items:center; gap:5px; height:28px; padding:0 12px; border-radius:8px; border:none; background:#fff; color:#111; font-size:11px; font-weight:800; cursor:pointer; transition:transform .12s ease, background .12s ease; } +.inspire-overlay-btn:hover { transform:translateY(-1px); background:#f1f5f9; } +.inspire-overlay-btn i,.inspire-overlay-btn svg { width:13px; height:13px; } +.inspire-loader { display:flex; align-items:center; justify-content:center; gap:8px; padding:18px 0; color:var(--muted); font-size:11px; font-weight:600; } +.inspire-empty { padding:40px 0; text-align:center; color:var(--faint); font-size:12px; } +.inspire-gallery { display:flex; flex-direction:column; gap:10px; flex:1 1 auto; min-height:0; } +.inspire-config { flex:1 1 auto; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:14px; padding:28px 24px; text-align:center; } +.inspire-config[hidden] { display:none; } +.inspire-config-icon { width:56px; height:56px; border-radius:16px; background:var(--soft); border:1px solid var(--line); display:flex; align-items:center; justify-content:center; color:var(--strong); } +.inspire-config-icon i,.inspire-config-icon svg { width:26px; height:26px; } +.inspire-config-title { font-size:15px; font-weight:800; color:var(--text); } +.inspire-config-desc { font-size:11.5px; line-height:1.7; color:var(--muted); max-width:300px; } +.inspire-key-input { width:100%; max-width:300px; height:40px; padding:0 14px; border-radius:11px; border:1px solid var(--line); background:var(--card); color:var(--text); font-size:12.5px; outline:none; transition:border-color .14s ease; } +.inspire-key-input:focus { border-color:var(--strong); } +.inspire-getkey-btn { display:inline-flex; align-items:center; gap:6px; height:34px; padding:0 16px; border-radius:999px; border:1px solid var(--line); background:var(--card); color:var(--text); font-size:12px; font-weight:700; cursor:pointer; transition:all .14s ease; } +.inspire-getkey-btn:hover { border-color:var(--strong); color:var(--strong); } +.inspire-getkey-btn i,.inspire-getkey-btn svg { width:13px; height:13px; } +.inspire-savekey-btn { height:38px; padding:0 28px; border-radius:999px; border:none; background:var(--strong); color:var(--strong-text); font-size:13px; font-weight:800; cursor:pointer; transition:opacity .14s ease, transform .14s ease; } +.inspire-savekey-btn:hover { opacity:.88; transform:translateY(-1px); } +.inspire-config-tip { font-size:10px; color:var(--faint); } +.inspire-end { padding:18px 0; text-align:center; color:var(--faint); font-size:11px; } .agent-skill-zone { display:flex; flex-direction:column; gap:6px; } .agent-skill-drop { min-height:44px; border:1px dashed var(--line); border-radius:13px; display:flex; align-items:center; justify-content:center; gap:7px; text-align:center; padding:8px; color:var(--faint); font-size:10.5px; font-weight:600; background:var(--soft); cursor:pointer; transition:border-color .14s ease, color .14s ease, background .14s ease; } .agent-skill-drop:hover { border-color:var(--strong); color:var(--text); } diff --git a/static/index.html b/static/index.html index b1c33c9bb..6da991a39 100644 --- a/static/index.html +++ b/static/index.html @@ -1672,16 +1672,16 @@
- - - - - - - - - - + + + + + + + + + +
diff --git a/static/js/smart-canvas.js b/static/js/smart-canvas.js index 187fa78d0..128a5aefa 100644 --- a/static/js/smart-canvas.js +++ b/static/js/smart-canvas.js @@ -8077,7 +8077,7 @@ function handlePortDrop(drag, e){ return; } if(!drag.moved){ discardPendingUndo(); render(); return; } - if(hit?.closest?.('.composer,.smart-back,.asset-panel,.asset-toggle,.agent-panel,.agent-toggle,.smart-log-toggle,.smart-shortcut-toggle,.smart-workflow-toggle,.log-modal,.shortcut-modal,.image-edit-modal,.smart-minimap')){ + if(hit?.closest?.('.composer,.smart-back,.asset-panel,.asset-toggle,.agent-panel,.agent-toggle,.inspire-panel,.smart-log-toggle,.smart-shortcut-toggle,.smart-workflow-toggle,.log-modal,.shortcut-modal,.image-edit-modal,.smart-minimap')){ discardPendingUndo(); render(); return; } const p = screenToWorld(e); @@ -15725,14 +15725,14 @@ function createNodeFromMenu(type){ shell.addEventListener('mousedown', e => { if(!zoomPreviewState) return; if(e.button !== 0) return; - if(e.target.closest('.composer,.smart-back,.asset-panel,.asset-toggle,.agent-panel,.agent-toggle,.smart-log-toggle,.smart-shortcut-toggle,.smart-workflow-toggle,.log-modal,.shortcut-modal,.image-edit-modal,.create-menu,.smart-minimap')) return; + if(e.target.closest('.composer,.smart-back,.asset-panel,.asset-toggle,.agent-panel,.agent-toggle,.inspire-panel,.smart-log-toggle,.smart-shortcut-toggle,.smart-workflow-toggle,.log-modal,.shortcut-modal,.image-edit-modal,.create-menu,.smart-minimap')) return; e.preventDefault(); e.stopPropagation(); }, true); shell.addEventListener('click', e => { if(!zoomPreviewState) return; if(e.button !== 0) return; - if(e.target.closest('.composer,.smart-back,.asset-panel,.asset-toggle,.agent-panel,.agent-toggle,.smart-log-toggle,.smart-shortcut-toggle,.smart-workflow-toggle,.log-modal,.shortcut-modal,.image-edit-modal,.create-menu,.smart-minimap')) return; + if(e.target.closest('.composer,.smart-back,.asset-panel,.asset-toggle,.agent-panel,.agent-toggle,.inspire-panel,.smart-log-toggle,.smart-shortcut-toggle,.smart-workflow-toggle,.log-modal,.shortcut-modal,.image-edit-modal,.create-menu,.smart-minimap')) return; e.preventDefault(); e.stopPropagation(); const nodeEl = e.target.closest('.image-node'); @@ -15740,8 +15740,8 @@ shell.addEventListener('click', e => { else exitZoomPreview(screenToWorld(e)); }, true); shell.onmousedown = e => { - if(zoomPreviewState && e.button === 0 && !e.target.closest('.composer,.smart-back,.asset-panel,.asset-toggle,.agent-panel,.agent-toggle,.smart-log-toggle,.smart-shortcut-toggle,.smart-workflow-toggle,.log-modal,.shortcut-modal,.image-edit-modal,.create-menu,.smart-minimap')) return; - if(e.target.closest('.image-node,.composer,.smart-back,.asset-panel,.asset-toggle,.agent-panel,.agent-toggle,.smart-log-toggle,.smart-shortcut-toggle,.smart-workflow-toggle,.log-modal,.shortcut-modal,.create-menu,.smart-minimap')) return; + if(zoomPreviewState && e.button === 0 && !e.target.closest('.composer,.smart-back,.asset-panel,.asset-toggle,.agent-panel,.agent-toggle,.inspire-panel,.smart-log-toggle,.smart-shortcut-toggle,.smart-workflow-toggle,.log-modal,.shortcut-modal,.image-edit-modal,.create-menu,.smart-minimap')) return; + if(e.target.closest('.image-node,.composer,.smart-back,.asset-panel,.asset-toggle,.agent-panel,.agent-toggle,.inspire-panel,.smart-log-toggle,.smart-shortcut-toggle,.smart-workflow-toggle,.log-modal,.shortcut-modal,.create-menu,.smart-minimap')) return; closeCreateMenu(); if(e.button === 0 && e.shiftKey){ e.preventDefault(); @@ -15778,7 +15778,7 @@ shell.oncontextmenu = e => { e.stopPropagation(); return; } - if(didPan || e.target.closest('.composer,.smart-back,.asset-panel,.asset-toggle,.agent-panel,.agent-toggle,.smart-log-toggle,.smart-shortcut-toggle,.smart-workflow-toggle,.log-modal,.shortcut-modal,.image-edit-modal,.create-menu,.smart-minimap')) return; + if(didPan || e.target.closest('.composer,.smart-back,.asset-panel,.asset-toggle,.agent-panel,.agent-toggle,.inspire-panel,.smart-log-toggle,.smart-shortcut-toggle,.smart-workflow-toggle,.log-modal,.shortcut-modal,.image-edit-modal,.create-menu,.smart-minimap')) return; if(document.getElementById('imageEditModal')?.classList.contains('open')) return; e.preventDefault(); e.stopPropagation(); @@ -15794,14 +15794,14 @@ shell.oncontextmenu = e => { openCreateMenu(e); }; shell.ondblclick = e => { - if(didPan || e.target.closest('.image-node,.composer,.smart-back,.asset-panel,.asset-toggle,.agent-panel,.agent-toggle,.smart-log-toggle,.smart-shortcut-toggle,.smart-workflow-toggle,.log-modal,.shortcut-modal,.image-edit-modal,.create-menu')) return; + if(didPan || e.target.closest('.image-node,.composer,.smart-back,.asset-panel,.asset-toggle,.agent-panel,.agent-toggle,.inspire-panel,.smart-log-toggle,.smart-shortcut-toggle,.smart-workflow-toggle,.log-modal,.shortcut-modal,.image-edit-modal,.create-menu')) return; if(document.getElementById('imageEditModal')?.classList.contains('open')) return; e.preventDefault(); openCreateMenu(e); }; shell.onclick = e => { if(selectionJustFinished) return; - if(didPan || e.target.closest('.image-node,.composer,.smart-back,.asset-panel,.asset-toggle,.agent-panel,.agent-toggle,.smart-log-toggle,.smart-shortcut-toggle,.smart-workflow-toggle,.log-modal,.shortcut-modal,.image-edit-modal,.create-menu')) return; + if(didPan || e.target.closest('.image-node,.composer,.smart-back,.asset-panel,.asset-toggle,.agent-panel,.agent-toggle,.inspire-panel,.smart-log-toggle,.smart-shortcut-toggle,.smart-workflow-toggle,.log-modal,.shortcut-modal,.image-edit-modal,.create-menu')) return; if(document.getElementById('imageEditModal')?.classList.contains('open')) return; closeCreateMenu(); clearSelection(); @@ -16310,7 +16310,7 @@ window.onmouseup = e => { } }; shell.addEventListener('wheel', e => { - if(e.target.closest('.composer,.smart-back,.image-edit-modal,.asset-panel,.asset-toggle,.agent-panel,.agent-toggle,.smart-log-toggle,.smart-shortcut-toggle,.smart-workflow-toggle,.workflow-transfer-panel,.log-modal,.shortcut-modal,.prompt-node-segments,.prompt-node-text,.prompt-node-llm,.smart-group-list,[data-thumb-scroll]')) return; + if(e.target.closest('.composer,.smart-back,.image-edit-modal,.asset-panel,.asset-toggle,.agent-panel,.agent-toggle,.inspire-panel,.smart-log-toggle,.smart-shortcut-toggle,.smart-workflow-toggle,.workflow-transfer-panel,.log-modal,.shortcut-modal,.prompt-node-segments,.prompt-node-text,.prompt-node-llm,.smart-group-list,[data-thumb-scroll]')) return; e.preventDefault(); const rect = shell.getBoundingClientRect(); const sx = e.clientX - rect.left; @@ -16328,6 +16328,18 @@ shell.ondrop = async e => { e.preventDefault(); if(e.target.closest('.image-node')) return; const p = screenToWorld(e); + // 灵感库拖拽:内容脚本已通过 postMessage 暂存图片+提示词(绕开跨域 dataTransfer 限制) + if(pendingInspireDrag && pendingInspireDrag.imageUrl){ + const drag = pendingInspireDrag; + pendingInspireDrag = null; + pushUndo(); + createImageNodeAt(p, [{url:drag.imageUrl, name:'inspire-' + Date.now() + '.png', kind:'image'}], {skipUndo:true}); + if(drag.prompt) setPromptText(drag.prompt); + render(); + scheduleSave(); + toast(drag.prompt ? '已拖入灵感图 + 提示词' : '已拖入灵感图'); + return; + } const assetRaw = e.dataTransfer.getData('application/x-smart-asset'); if(assetRaw){ try { @@ -17227,6 +17239,383 @@ function toggleAgentPanel(open=!agentOpen){ renderAgentMessages(); } } +// ============ 灵感库(Civitai AI 图片源 · 用户生成 AI 图) ============ +const INSPIRE_SORTS = [ + {id:'Most Reactions', name:'热门'}, + {id:'Newest', name:'最新'}, + {id:'Most Liked', name:'点赞'}, + {id:'Most Collected', name:'收藏'} +]; +// 搜索方案A:中文分类 chips(映射 Civitai 英文 tag) +const INSPIRE_TAGS = [ + {zh:'全部', tag:''}, + {zh:'人物', tag:'1girl'}, + {zh:'风景', tag:'landscape'}, + {zh:'动漫', tag:'anime'}, + {zh:'写实', tag:'photorealistic'}, + {zh:'赛博朋克', tag:'cyberpunk'}, + {zh:'奇幻', tag:'fantasy'}, + {zh:'机甲', tag:'mecha'}, + {zh:'建筑', tag:'architecture'}, + {zh:'城市', tag:'city'}, + {zh:'肖像', tag:'portrait'}, + {zh:'概念设计', tag:'concept art'}, + {zh:'插画', tag:'illustration'}, + {zh:'水墨国风', tag:'chinese style'}, + {zh:'可爱', tag:'cute'}, + {zh:'科幻', tag:'sci-fi'} +]; +// 搜索方案B:中文→英文搜索词典(常见美术词) +const INSPIRE_ZH_EN = { + '女孩':'1girl','男孩':'1boy','人物':'1girl','美女':'1girl','风景':'landscape','景色':'landscape','动漫':'anime','漫画':'anime', + '写实':'photorealistic','真实':'photorealistic','照片':'photorealistic','摄影':'photorealistic','赛博朋克':'cyberpunk','奇幻':'fantasy','魔法':'magic', + '机甲':'mecha','机器人':'robot','建筑':'architecture','房子':'architecture','城市':'city','街景':'cityscape','夜景':'night','夜晚':'night', + '肖像':'portrait','头像':'portrait','概念设计':'concept art','概念':'concept art','插画':'illustration','水墨':'chinese style','国风':'chinese style','中国风':'chinese style', + '可爱':'cute','萌':'cute','科幻':'sci-fi','未来':'futuristic','水彩':'watercolor','油画':'oil painting','素描':'sketch','像素':'pixel art', + '吉卜力':'ghibli','极简':'minimalism','复古':'retro','蒸汽朋克':'steampunk','龙':'dragon','猫':'cat','狗':'dog','花':'flower', + '植物':'plant','海洋':'ocean','大海':'ocean','山':'mountain','森林':'forest','雪':'snow','沙漠':'desert','战士':'warrior', + '骑士':'knight','巫师':'wizard','飞船':'spaceship','汽车':'car','美食':'food','食物':'food','甜点':'food','少女':'1girl','男神':'1boy', + '海报':'poster','宣传海报':'poster','设计':'design','logo':'logo','图标':'icon','壁纸':'wallpaper','头像':'avatar','封面':'cover', + '产品':'product','商品':'product','电商':'e-commerce','包装':'packaging','名片':'business card','插画':'illustration', + '二次元':'anime','国风':'chinese style','古风':'chinese style','像素风':'pixel art','蒸汽波':'vaporwave','暗黑':'dark','唯美':'aesthetic' +}; +// 「更多标签」分组面板数据(中文标签 → Civitai 英文 tag) +const INSPIRE_TAG_GROUPS = [ + { name:'人物', tags:[['少女','1girl'],['男孩','1boy'],['肖像','portrait'],['战士','warrior'],['骑士','knight'],['巫师','wizard'],['美女','1girl']] }, + { name:'风格', tags:[['动漫','anime'],['写实','photorealistic'],['赛博朋克','cyberpunk'],['奇幻','fantasy'],['水墨国风','chinese style'],['像素风','pixel art'],['蒸汽朋克','steampunk'],['吉卜力','ghibli'],['复古','retro'],['蒸汽波','vaporwave'],['唯美','aesthetic'],['暗黑','dark'],['水彩','watercolor'],['油画','oil painting'],['素描','sketch']] }, + { name:'场景', tags:[['风景','landscape'],['城市','city'],['街景','cityscape'],['夜景','night'],['森林','forest'],['海洋','ocean'],['山','mountain'],['沙漠','desert'],['建筑','architecture'],['雪','snow']] }, + { name:'生物·物体', tags:[['猫','cat'],['狗','dog'],['龙','dragon'],['机甲','mecha'],['机器人','robot'],['汽车','car'],['飞船','spaceship'],['花','flower'],['植物','plant'],['美食','food']] }, + { name:'设计', tags:[['海报','poster'],['logo','logo'],['图标','icon'],['壁纸','wallpaper'],['封面','cover'],['产品','product'],['包装','packaging'],['名片','business card'],['插画','illustration'],['设计','design']] }, + { name:'概念', tags:[['概念设计','concept art'],['科幻','sci-fi'],['未来','futuristic'],['可爱','cute'],['极简','minimalism'],['魔法','magic']] }, +]; +let inspireOpen = false; +let inspireCursor = ''; +let inspireLoading = false; +let inspireHasMore = true; +let inspireSort = 'Most Reactions'; +let inspireTag = ''; +let inspireLoadedCount = 0; +let inspireSeenIds = new Set(); // 浏览去重 +const _inspireCache = {}; // TTL 缓存 +const INSPIRE_CACHE_TTL = 15 * 60 * 1000; // 15 分钟 +// ---- API Key 管理 ---- +function getCivitaiKey(){ try { return localStorage.getItem('civitai_api_key') || ''; } catch(e){ return ''; } } +function saveCivitaiKey(k){ try { if(k) localStorage.setItem('civitai_api_key', k); else localStorage.removeItem('civitai_api_key'); } catch(e){} } +// ---- 配置页 / 图库 切换 ---- +function showInspireConfig(show){ + const config = document.getElementById('inspireConfig'); + const gallery = document.getElementById('inspireGallery'); + if(config) config.hidden = !show; + if(gallery) gallery.style.display = show ? 'none' : 'flex'; + if(show){ + const input = document.getElementById('inspireKeyInput'); + if(input) input.value = getCivitaiKey(); + } +} +function toggleInspirePanel(open=!inspireOpen){ + const panel = document.getElementById('inspirePanel'); + const toggle = document.getElementById('inspireToggle'); + if(!panel || !toggle) return; + inspireOpen = !!open; + if(inspireOpen){ toggleAgentPanel(false); toggleAssetLibrary(false); } + panel.classList.toggle('open', inspireOpen); + toggle.classList.toggle('active', inspireOpen); + if(inspireOpen){ + const hasKey = !!getCivitaiKey(); + showInspireConfig(!hasKey); // 无 Key → 配置页,不加载图库 + if(hasKey){ + renderInspireCats(); + const grid = document.getElementById('inspireGrid'); + if(grid && !grid.dataset.loaded) loadInspirePage(true); + } + } +} +// ---- 分类 chips(中文) ---- +function renderInspireCats(){ + const wrap = document.getElementById('inspireCats'); + if(!wrap) return; + const chips = INSPIRE_TAGS.map(c => ``).join(''); + wrap.innerHTML = chips + ''; + wrap.querySelectorAll('[data-inspire-tag]').forEach(btn => { + btn.onclick = () => { + const tag = btn.dataset.inspireTag || ''; + if(tag === inspireTag) return; + inspireTag = tag; + const search = document.getElementById('inspireSearch'); + if(search) search.value = ''; + closeInspireMoreTags(); + renderInspireCats(); + resetInspireAndLoad(); + }; + }); + const moreBtn = document.getElementById('inspireMoreBtn'); + if(moreBtn) moreBtn.onclick = () => toggleInspireMoreTags(); +} +// ---- 「更多标签」分组下拉面板 ---- +function renderInspireMoreTags(){ + const panel = document.getElementById('inspireMoreTags'); + if(!panel || panel.dataset.rendered) return; + panel.dataset.rendered = '1'; + panel.innerHTML = INSPIRE_TAG_GROUPS.map(g => + '
' + escapeHtml(g.name) + '
' + + g.tags.map(t => '').join('') + + '
' + ).join(''); + panel.querySelectorAll('[data-mt-tag]').forEach(btn => { + btn.onclick = () => { + inspireTag = btn.dataset.mtTag || ''; + const search = document.getElementById('inspireSearch'); + if(search) search.value = btn.dataset.mtZh || ''; + closeInspireMoreTags(); + renderInspireCats(); + resetInspireAndLoad(); + }; + }); +} +function toggleInspireMoreTags(){ + const panel = document.getElementById('inspireMoreTags'); + if(!panel) return; + if(panel.hidden){ renderInspireMoreTags(); updateInspireMoreActive(); panel.hidden = false; } + else { panel.hidden = true; } +} +function closeInspireMoreTags(){ + const panel = document.getElementById('inspireMoreTags'); + if(panel) panel.hidden = true; +} +function updateInspireMoreActive(){ + const panel = document.getElementById('inspireMoreTags'); + if(!panel) return; + panel.querySelectorAll('[data-mt-tag]').forEach(btn => { + btn.classList.toggle('active', (btn.dataset.mtTag || '') === inspireTag); + }); +} +function resetInspireAndLoad(){ + inspireCursor = ''; + inspireHasMore = true; + inspireLoadedCount = 0; + inspireSeenIds = new Set(); + const grid = document.getElementById('inspireGrid'); + if(grid){ grid.innerHTML = ''; delete grid.dataset.loaded; } + const end = document.getElementById('inspireEnd'); + if(end) end.hidden = true; + const sc = document.getElementById('inspireScroll'); + if(sc) sc.scrollTop = 0; + loadInspirePage(true); +} +// ---- TTL 缓存 ---- +function _inspireCacheKey(){ return inspireSort + '|' + inspireTag + '|' + inspireCursor; } +function _inspireCacheGet(key){ + const c = _inspireCache[key]; + if(c && (Date.now() - c.ts < INSPIRE_CACHE_TTL)) return c.data; + if(c) delete _inspireCache[key]; + return null; +} +function _inspireCacheSet(key, data){ _inspireCache[key] = {data, ts:Date.now()}; } +// ---- 数据加载(带 Key + 429 降级 + 缓存 + 去重) ---- +async function loadInspirePage(reset=false){ + if(inspireLoading || (!inspireHasMore && !reset)) return; + inspireLoading = true; + const loader = document.getElementById('inspireLoader'); + const empty = document.getElementById('inspireEmpty'); + const end = document.getElementById('inspireEnd'); + const grid = document.getElementById('inspireGrid'); + if(loader) loader.hidden = false; + if(empty) empty.hidden = true; + if(end) end.hidden = true; + const cacheKey = _inspireCacheKey(); + let data = _inspireCacheGet(cacheKey); + if(!data){ + const params = new URLSearchParams({ limit:'24', sort:inspireSort }); + if(inspireTag) params.set('tag', inspireTag); + if(inspireCursor) params.set('cursor', inspireCursor); + try { + const resp = await fetch('/api/inspire-civitai?' + params.toString(), { headers:{ 'X-Civitai-Key': getCivitaiKey() } }); + if(resp.status === 429){ + if(loader) loader.hidden = true; + inspireLoading = false; + toast('Civitai 限流:请稍后再试(配置 Key 可提高限额)'); + if(reset && grid && !grid.children.length) grid.innerHTML = '
限流了,请稍后再试
'; + return; + } + if(!resp.ok) throw new Error('HTTP ' + resp.status); + data = await resp.json(); + _inspireCacheSet(cacheKey, data); + } catch(e){ + console.warn('[inspire] Civitai 加载失败:', e); + if(loader) loader.hidden = true; + inspireLoading = false; + if(reset && grid) grid.innerHTML = '
加载失败,请检查网络后重试
'; + else if(!reset) toast('加载更多失败'); + return; + } + } + const rawItems = Array.isArray(data.items) ? data.items : []; + inspireCursor = data.cursor || ''; + inspireHasMore = !!data.hasMore; + // 浏览去重:跳过已展示的 ID + const items = rawItems.filter(it => { + const id = it.id; + if(id != null && inspireSeenIds.has(id)) return false; + if(id != null) inspireSeenIds.add(id); + return true; + }); + inspireLoadedCount += items.length; + const cnt = document.getElementById('inspireCount'); + if(cnt) cnt.textContent = '已加载 ' + inspireLoadedCount + ' 张'; + if(reset && items.length === 0){ + if(grid) grid.innerHTML = ''; + if(empty) empty.hidden = false; + } else { + appendInspireCards(items); + if(grid) grid.dataset.loaded = '1'; + } + if(!inspireHasMore && end) end.hidden = false; // 到底提示 + if(loader) loader.hidden = true; + inspireLoading = false; +} +function appendInspireCards(items){ + const grid = document.getElementById('inspireGrid'); + if(!grid) return; + items.forEach(it => { + const imgUrl = it.thumb || it.image; + if(!imgUrl) return; + const card = document.createElement('div'); + card.className = 'inspire-card'; + const promptText = it.prompt || ''; + const authorLine = it.username + ? '
@' + escapeHtml(it.username) + (it.model ? ' · ' + escapeHtml(it.model) : '') + '
' + : (it.model ? '
' + escapeHtml(it.model) + '
' : ''); + const overlayBody = promptText + ? '
' + escapeHtml(promptText) + '
' + : '
AI 生成图(作者未共享提示词)
'; + card.innerHTML = '' + + '
' + + authorLine + + overlayBody + + '' + + '
'; + const img = card.querySelector('img'); + // 按真实宽高预留比例,避免懒加载时布局上下跳动 + if(it.width && it.height){ img.style.aspectRatio = it.width + ' / ' + it.height; } + img.src = imgUrl; + img.addEventListener('load', () => img.classList.add('loaded')); + // 加载失败重试:自动重试1次 + 点击重试 + img.addEventListener('error', () => { + if(img.dataset.retried){ img.classList.add('loaded'); img.style.minHeight = '90px'; img.style.cursor = 'pointer'; img.title = '加载失败,点击重试'; } + else { img.dataset.retried = '1'; setTimeout(() => { const s = img.src; img.src = ''; img.src = s; }, 1500); } + }); + img.addEventListener('click', () => { + if(img.dataset.retried && img.naturalWidth === 0){ delete img.dataset.retried; img.classList.remove('loaded'); img.style.minHeight=''; const s = img.src; img.src=''; img.src=s; } + }); + card.querySelector('.inspire-overlay-btn').addEventListener('click', e => { e.stopPropagation(); importInspireToCanvas(it); }); + grid.appendChild(card); + }); + if(window.lucide) lucide.createIcons(); +} +// ---- 导入:本地化 + 节点去重 + 来源署名 + 反馈 ---- +function findInspireNode(civitaiId){ + if(civitaiId == null || civitaiId === '') return null; + return (typeof nodes !== 'undefined' ? nodes : []).find(n => (n.images || []).some(img => String(img.civitaiId || '') === String(civitaiId))); +} +async function importInspireToCanvas(it){ + const url = it.image || it.thumb; + if(!url){ toast('该图片无法导入'); return; } + // 节点去重:画布已有同图 → 选中它 + const existing = findInspireNode(it.id); + if(existing){ + selectedId = existing.id; selectedIds = [existing.id]; + render(); + const r = nodeRect(existing); + if(r) agentCenterOnPoint(r.x + r.width/2, r.y + r.height/2); + toast('这张图已在画布中'); + return; + } + toast('正在导入到画布…'); + try { + const locResp = await fetch('/api/inspire-localize', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ url, id:String(it.id || '') }) }); + if(!locResp.ok) throw new Error('localize failed'); + const loc = await locResp.json(); + const localUrl = loc.url || url; + const prompt = it.prompt || ''; + const imgObj = { + url: localUrl, + name: 'civitai_' + (it.id || Date.now()) + '.jpg', + kind: 'image', + civitaiId: String(it.id || ''), + civitaiSource: it.id ? ('https://civitai.com/images/' + it.id) : '', + civitaiAuthor: it.username || '' + }; + const hadNode = !!selectedNode(); + try { addManualReferenceToSelectedNode(imgObj); } catch(e){ console.warn('[inspire] 挂参考图失败', e); } + const pos = agentFindEmptyPosition(1); + const node = createImageNodeAt(pos, [{ ...imgObj }]); + if(node) node.runFinishedAt = nowMs(); + render(); + scheduleSave(); + if(prompt) setPromptText(prompt); + toast(prompt ? '已导入:参考图 + 提示词(来源 Civitai @' + (it.username || '匿名') + ')' : '已导入参考图(来源 Civitai)'); + } catch(e){ + console.warn('[inspire] 导入失败', e); + toast('导入失败,请重试'); + } +} +function initInspireScroll(){ + const sc = document.getElementById('inspireScroll'); + if(!sc || sc.dataset.inspireScrollInit) return; + sc.dataset.inspireScrollInit = '1'; + sc.addEventListener('scroll', () => { + if(!inspireOpen || inspireLoading || !inspireHasMore) return; + if(sc.scrollTop + sc.clientHeight >= sc.scrollHeight - 500) loadInspirePage(false); + }); +} +// ---- 搜索 A+B:中文词典映射 + 回车/按钮触发 ---- +function initInspireSearch(){ + const input = document.getElementById('inspireSearch'); + const btn = document.getElementById('inspireSearchBtn'); + if(!input || input.dataset.inspireSearchInit) return; + input.dataset.inspireSearchInit = '1'; + const doSearch = () => { + const raw = input.value.trim(); + const tag = raw ? (INSPIRE_ZH_EN[raw] || raw) : ''; // 中文→英文,未命中则原样 + inspireTag = tag; + renderInspireCats(); + resetInspireAndLoad(); + }; + // 回车触发(兼容中文输入法:isComposing 时不触发) + input.addEventListener('keydown', e => { + if(e.key === 'Enter' && !e.isComposing && e.keyCode !== 229){ e.preventDefault(); doSearch(); } + }); + // 点击放大镜按钮触发(避免输入法回车问题) + if(btn) btn.addEventListener('click', () => doSearch()); +} +// ---- 配置页事件 ---- +function initInspireConfig(){ + const getKeyBtn = document.getElementById('inspireGetKeyBtn'); + const saveBtn = document.getElementById('inspireSaveKeyBtn'); + const settingsBtn = document.getElementById('inspireSettingsBtn'); + if(getKeyBtn && !getKeyBtn.dataset.init){ + getKeyBtn.dataset.init = '1'; + getKeyBtn.addEventListener('click', () => window.open('https://civitai.com/user/account', '_blank', 'noopener')); + } + if(saveBtn && !saveBtn.dataset.init){ + saveBtn.dataset.init = '1'; + saveBtn.addEventListener('click', () => { + const input = document.getElementById('inspireKeyInput'); + const key = (input?.value || '').trim(); + if(!key){ toast('请先输入 API Key'); return; } + saveCivitaiKey(key); + toast('已保存,正在加载灵感库…'); + showInspireConfig(false); + renderInspireCats(); + const grid = document.getElementById('inspireGrid'); + if(grid && !grid.dataset.loaded) loadInspirePage(true); + }); + } + if(settingsBtn && !settingsBtn.dataset.init){ + settingsBtn.dataset.init = '1'; + settingsBtn.addEventListener('click', () => showInspireConfig(true)); // 回配置页改 Key + } +} function chatRequestedImageCount(text){ const t = String(text || ''); // 阿拉伯数字 1-8:3张/5个/8条/画4张/生成6个 @@ -19993,6 +20382,42 @@ function initAgentPanel(){ if(!e.target.closest('.agent-toolbar-dropdown-wrap') && !e.target.closest('.agent-dropdown-panel')) closeAllDropdowns(); }, true); agentToggle?.addEventListener('click', () => toggleAgentPanel()); +document.getElementById('inspireToggle')?.addEventListener('click', () => toggleInspirePanel()); +document.getElementById('inspireCloseBtn')?.addEventListener('click', () => toggleInspirePanel(false)); +initInspireScroll(); +initInspireSearch(); +initInspireConfig(); +// ============ 灵感库 ← 内容脚本通信(图片+提示词送回画布) ============ +let pendingInspireDrag = null; +let _pendingInspireDragTimer = null; +function handleInspireImageToCanvas(imageUrl, prompt){ + const url = String(imageUrl || ''); + if(!url) return; + const pos = agentFindEmptyPosition(1); + const node = createImageNodeAt(pos, [{url, name:'inspire-' + Date.now() + '.png', kind:'image'}]); + if(node) node.runFinishedAt = nowMs(); + render(); + scheduleSave(); + if(prompt) setPromptText(prompt); + toast(prompt ? '已接收灵感图 + 提示词(填入下方输入框)' : '已接收灵感图'); +} +window.addEventListener('message', e => { + const d = e.data; + if(!d || typeof d !== 'object') return; + if(d.type === 'inspire-to-canvas'){ + // 「发送到画布」按钮:直接创建节点 + 填提示词 + handleInspireImageToCanvas(d.imageUrl, d.prompt); + } else if(d.type === 'inspire-drag-payload'){ + // 拖拽开始:暂存图片+提示词,等画布 ondrop 取用 + pendingInspireDrag = {imageUrl:String(d.imageUrl || ''), prompt:String(d.prompt || '')}; + clearTimeout(_pendingInspireDragTimer); + // 兜底:6秒内没有 drop 就清空(防止拖拽取消后残留) + _pendingInspireDragTimer = setTimeout(() => { pendingInspireDrag = null; }, 6000); + } else if(d.type === 'inspire-drag-end'){ + // 拖拽结束(未 drop):稍延迟清空,给 ondrop 留处理时间 + setTimeout(() => { pendingInspireDrag = null; }, 100); + } +}); agentCloseBtn?.addEventListener('click', () => toggleAgentPanel(false)); document.getElementById('agentNewChatBtn')?.addEventListener('click', () => agentNewChat()); document.getElementById('agentDeleteChatBtn')?.addEventListener('click', () => agentDeleteChat()); diff --git a/static/smart-canvas.html b/static/smart-canvas.html index 88828aa52..674ff1841 100644 --- a/static/smart-canvas.html +++ b/static/smart-canvas.html @@ -20,7 +20,7 @@ - +
@@ -144,6 +144,7 @@ + + - + diff --git a/static/asset-manager.html b/static/asset-manager.html index 574a3441a..fd3a9ba42 100644 --- a/static/asset-manager.html +++ b/static/asset-manager.html @@ -16,10 +16,10 @@ } catch(e) {} })(); - - - - + + + +
@@ -47,6 +47,6 @@
- + diff --git a/static/canvas-list.html b/static/canvas-list.html index 06d09047b..90583ab03 100644 --- a/static/canvas-list.html +++ b/static/canvas-list.html @@ -16,13 +16,13 @@ } catch(e) {} })(); - - - - - - - + + + + + + +
@@ -90,6 +90,6 @@
- + diff --git a/static/canvas.html b/static/canvas.html index 21557fabe..fb6d5ebd4 100644 --- a/static/canvas.html +++ b/static/canvas.html @@ -16,13 +16,13 @@ } catch(e) {} })(); - - - - - - - + + + + + + +
@@ -350,7 +350,7 @@
- - + + diff --git a/static/comfyui-settings.html b/static/comfyui-settings.html index 82ac6fbe2..e0fde2ff7 100644 --- a/static/comfyui-settings.html +++ b/static/comfyui-settings.html @@ -15,12 +15,12 @@ } catch(e) {} })(); - - - - - - + + + + + +
@@ -122,6 +122,6 @@ preview
- + diff --git a/static/enhance.html b/static/enhance.html index 59b9bd455..216523159 100644 --- a/static/enhance.html +++ b/static/enhance.html @@ -17,14 +17,14 @@ } catch(e) {} })(); - - - - - - + + + + + + - + diff --git a/static/gpt-chat.html b/static/gpt-chat.html index be0ca47d7..58ba997db 100644 --- a/static/gpt-chat.html +++ b/static/gpt-chat.html @@ -16,13 +16,13 @@ } catch(e) {} })(); - - - - - + + + + + - + diff --git a/static/online.html b/static/online.html index 0d3a6a846..cb8b835c1 100644 --- a/static/online.html +++ b/static/online.html @@ -16,14 +16,14 @@ } catch(e) {} })(); - - - - - - + + + + + + - + - +