diff --git a/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/api/weixin_public_account_delivery.go b/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/api/weixin_public_account_delivery.go index 5e907ae..2ad0300 100644 --- a/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/api/weixin_public_account_delivery.go +++ b/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/api/weixin_public_account_delivery.go @@ -27,8 +27,8 @@ type weixinPublicAccountSection struct { } func executeWeixinPublicAccountImagePromptStep(workflow *weixinPublicAccountWorkflowState, article *weixinpublicaccountmodel.WeixinPublicAccountArticle, user *model.User, now time.Time) weixinPublicAccountStepExecution { - prompts := buildWeixinPublicAccountImagePrompts(workflow) - contentWithPrompts := buildWeixinPublicAccountContentWithPrompts(workflow.Shared.Content, prompts) + prompts := buildWeixinPublicAccountImagePromptsV2(workflow) + contentWithPrompts := buildWeixinPublicAccountContentWithPromptsFromJSON(workflow.Shared.Content, prompts) workflow.Shared.ContentWithPrompts = contentWithPrompts workflow.Shared.ImagePrompts = prompts @@ -55,9 +55,9 @@ func executeWeixinPublicAccountImagePromptStep(workflow *weixinPublicAccountWork func executeWeixinPublicAccountImageGenerationStep(workflow *weixinPublicAccountWorkflowState, article *weixinpublicaccountmodel.WeixinPublicAccountArticle, user *model.User, now time.Time) (weixinPublicAccountStepExecution, *web.AppError) { prompts := workflow.Shared.ImagePrompts if len(prompts) == 0 { - prompts = buildWeixinPublicAccountImagePrompts(workflow) + prompts = buildWeixinPublicAccountImagePromptsV2(workflow) workflow.Shared.ImagePrompts = prompts - workflow.Shared.ContentWithPrompts = buildWeixinPublicAccountContentWithPrompts(workflow.Shared.Content, prompts) + workflow.Shared.ContentWithPrompts = buildWeixinPublicAccountContentWithPromptsFromJSON(workflow.Shared.Content, prompts) } aiRoute, refs, appErr := generateWeixinPublicAccountImages(article.TaskID, workflow, prompts, user) if appErr != nil { @@ -181,6 +181,11 @@ func buildWeixinPublicAccountContentWithPrompts(content string, prompts []weixin return strings.Join(blocks, "\n") } +// buildWeixinPublicAccountContentWithPromptsFromJSON 基于 JSON 配置渲染带括号标记的正文。 +func buildWeixinPublicAccountContentWithPromptsFromJSON(content string, prompts []weixinPublicAccountImagePrompt) string { + return renderWeixinPublicAccountImagePromptFromJSON(content, prompts) +} + func buildWeixinPublicAccountVisualPrompt(workflow *weixinPublicAccountWorkflowState, sectionTitle, context string) string { keyword := firstNonEmpty(workflow.Form.Keyword, "业务主题") tone := firstNonEmpty(workflow.Form.Tone, "专业但好懂") diff --git a/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/api/weixin_public_account_image_prompt_config.go b/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/api/weixin_public_account_image_prompt_config.go index 50b17cf..6fc1710 100644 --- a/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/api/weixin_public_account_image_prompt_config.go +++ b/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/api/weixin_public_account_image_prompt_config.go @@ -217,18 +217,20 @@ func buildWeixinPublicAccountImagePromptsV2(workflow *weixinPublicAccountWorkflo func buildCoverImagePrompt(workflow *weixinPublicAccountWorkflowState, rule *imagePromptRule) string { title := firstNonEmpty(workflow.Shared.SelectedTitle, workflow.Form.Keyword, "公众号文章") - // 从 JSON 模板构建(当前回退到硬编码模板) + // 从 JSON 模板构建 template := rule.Template.Template - if template == "" { - // 回退到当前硬编码的英文模板 - return fmt.Sprintf("realistic editorial illustration, Chinese business article cover, %s, focus on %s, %s, clean composition, natural lighting, brand-safe, no text, no watermark", - weixinPublicAccountBusinessDomainLabel(workflow.Form.BusinessDomain), - title, - "公众号封面主视觉", - ) + if template != "" { + // 使用字符串替换,而非 fmt.Sprintf + prompt := strings.ReplaceAll(template, "{index}", "1") + prompt = strings.ReplaceAll(prompt, "{section_summary}", fmt.Sprintf("%s(截断至120字)", title)) + return prompt } - - return fmt.Sprintf(template, 1, title, fmt.Sprintf("%s(截断至120字)", title)) + // 回退到硬编码模板 + return fmt.Sprintf("realistic editorial illustration, Chinese business article cover, %s, focus on %s, %s, clean composition, natural lighting, brand-safe, no text, no watermark", + weixinPublicAccountBusinessDomainLabel(workflow.Form.BusinessDomain), + title, + "公众号封面主视觉", + ) } // buildContentImagePrompt 生成正文配图提示词。 @@ -237,16 +239,18 @@ func buildContentImagePrompt(workflow *weixinPublicAccountWorkflowState, section // 从 JSON 模板构建 template := rule.Template.Template - if template == "" { - // 回退到当前硬编码的英文模板 - return fmt.Sprintf("realistic editorial illustration, Chinese business article cover, %s, focus on %s, %s, clean composition, natural lighting, brand-safe, no text, no watermark", - weixinPublicAccountBusinessDomainLabel(workflow.Form.BusinessDomain), - firstNonEmpty(section.Title, workflow.Form.Keyword), - firstNonEmpty(summary, workflow.Form.Tone), - ) + if template != "" { + // 使用字符串替换,而非 fmt.Sprintf + prompt := strings.ReplaceAll(template, "{index}", fmt.Sprintf("%d", index)) + prompt = strings.ReplaceAll(prompt, "{section_summary}", summary) + return prompt } - - return fmt.Sprintf(template, index, firstNonEmpty(section.Title, workflow.Form.Keyword), summary) + // 回退到硬编码模板 + return fmt.Sprintf("realistic editorial illustration, Chinese business article cover, %s, focus on %s, %s, clean composition, natural lighting, brand-safe, no text, no watermark", + weixinPublicAccountBusinessDomainLabel(workflow.Form.BusinessDomain), + firstNonEmpty(section.Title, workflow.Form.Keyword), + firstNonEmpty(summary, workflow.Form.Tone), + ) } // countChineseWords 计算内容的中文字数。 diff --git a/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/api/weixin_public_account_workflow.go b/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/api/weixin_public_account_workflow.go index 49542ec..c809515 100644 --- a/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/api/weixin_public_account_workflow.go +++ b/eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/api/weixin_public_account_workflow.go @@ -792,7 +792,7 @@ func respondWeixinPublicAccountWorkflow(c *gin.Context, task model.TaskRecord) { func findWeixinPublicAccountImagePrompt(workflow weixinPublicAccountWorkflowState, imageKey string) (weixinPublicAccountImagePrompt, int, bool) { prompts := workflow.Shared.ImagePrompts if len(prompts) == 0 { - prompts = buildWeixinPublicAccountImagePrompts(&workflow) + prompts = buildWeixinPublicAccountImagePromptsV2(&workflow) workflow.Shared.ImagePrompts = prompts } for idx, item := range prompts { diff --git a/eai_agentplatform/frontend/src/components/chat/WeixinPublicAccountSpecialistPanel.vue b/eai_agentplatform/frontend/src/components/chat/WeixinPublicAccountSpecialistPanel.vue index 0ea13a0..113cdcc 100644 --- a/eai_agentplatform/frontend/src/components/chat/WeixinPublicAccountSpecialistPanel.vue +++ b/eai_agentplatform/frontend/src/components/chat/WeixinPublicAccountSpecialistPanel.vue @@ -148,20 +148,51 @@
-
- {{ file.kindLabel }} -
- {{ file.name }} - {{ file.meta }} +
+
{{ group.summary }}
+
+ + 加载中 +
+ {{ file.meta }} + {{ file.size }} +
+ +
+
+
+
+ {{ file.kindLabel }} +
+ {{ file.name }} + {{ file.meta }} +
+
-
@@ -695,10 +726,49 @@ const artifactStepGroups = computed(() => orderedSteps.value.map((step, index) = } })) const artifactItems = computed(() => artifactStepGroups.value.map((group) => group.artifact).filter(Boolean)) -// 一份产物都没有时不再列 8 个「这一步还没有产物」的空壳,只留一句提示。 -// 跑过几步的任务则八步全列,缺哪一步一眼能看出来。 -const visibleArtifactGroups = computed(() => (artifactItems.value.length ? artifactStepGroups.value : [])) +const visibleArtifactGroups = computed(() => (artifactItems.value.length ? artifactStepGroupsWithThumbnails.value : [])) const previewTitle = computed(() => (previewTarget.value?.name ? `预览 · ${previewTarget.value.name}` : '预览')) +const artifactImageUrls = computed(() => { + // Collect all image URLs from artifact files so we can load thumbnails + const urls = [] + artifactStepGroups.value.forEach((group) => { + if (group.isImageGroup && group.files) { + group.files.forEach((file) => { + if (file.url) urls.push(String(file.url)) + }) + } + }) + return urls +}) +const artifactImageObjectUrls = computed(() => { + const result = {} + artifactImageUrls.value.forEach((url) => { + result[url] = imageObjectUrls.value[url] || '' + }) + return result +}) + +// Augment artifact step groups with thumbnail URLs +const artifactStepGroupsWithThumbnails = computed(() => { + return artifactStepGroups.value.map((group) => { + if (group.isImageGroup && group.files) { + return { + ...group, + files: group.files.map((file) => { + if (file.kind === 'image' && file.url) { + return { + ...file, + thumbnailUrl: artifactImageObjectUrls.value[String(file.url)] || imageObjectUrls.value[String(file.url)] || '', + } + } + return file + }), + } + } + return group + }) +}) + const uploadItems = computed(() => { const rawList = collectUploadItems(parseTaskContext(workflowTask.value)) return rawList.map((item, index) => normalizeUploadItem(item, index)).filter(Boolean) @@ -1335,10 +1405,17 @@ function buildArtifactView(item, stepKey) { } if (base.kind === 'files') { - const files = buildArtifactFiles(payload, view.type) - if (!files.length) return degradeArtifactView(base, stepKey) - base.files = files - base.summary = artifactFilesSummary(files, payload, view.type) + const filesOrGroup = buildArtifactFiles(payload, view.type) + if (!filesOrGroup || (Array.isArray(filesOrGroup) && !filesOrGroup.length)) return degradeArtifactView(base, stepKey) + // images 返回的是 { isImageGroup: true, summary, files } 对象 + if (filesOrGroup.isImageGroup) { + base.isImageGroup = true + base.summary = filesOrGroup.summary + base.files = filesOrGroup.files + } else { + base.files = filesOrGroup + base.summary = artifactFilesSummary(filesOrGroup, payload, view.type) + } base.body = '' return base } @@ -1444,17 +1521,22 @@ function artifactRowsSummary(payload, artifactType, rows) { function buildArtifactFiles(payload, artifactType) { if (artifactType === 'images') { const refs = Array.isArray(payload?.image_refs) ? payload.image_refs : [] - return refs.map((ref, idx) => { - const url = String(ref?.url || '').trim() - return { - key: `image-${ref?.key || idx}`, - kind: 'image', - kindLabel: '图片', - name: fileNameOfUrl(url) || `生成图 ${idx + 1}.png`, - meta: String(ref?.section || ref?.key || '').trim(), - url, - } - }).filter((file) => file.url) + return { + isImageGroup: true, + summary: `计划生成 ${refs.length} 张配图`, + files: refs.map((ref, idx) => { + const url = String(ref?.url || '').trim() + return { + key: `image-${ref?.key || idx}`, + kind: 'image', + kindLabel: '图片', + name: fileNameOfUrl(url) || `生成图 ${idx + 1}.png`, + meta: String(ref?.section || ref?.key || '').trim(), + size: ref?.image_size ? sizeLabel(ref.image_size) : '', + url, + } + }).filter((file) => file.url), + } } if (artifactType === 'preview') { const html = String(payload?.preview_html || '').trim() @@ -1551,12 +1633,31 @@ function formatFileSize(value) { return `${(size / (1024 * 1024)).toFixed(1)} MB` } +// 图片尺寸标签 +function sizeLabel(imageSize) { + const map = { + 'landscape_16_9': '16:9 横图', + 'landscape_4_3': '4:3 横图', + 'portrait_4_3': '4:3 竖图', + 'square_hd': '1:1 方形', + } + return map[imageSize] || imageSize +} + watch( () => [currentSpecialistKey.value, currentTaskId.value], refresh, { immediate: true } ) +watch( + artifactImageUrls, + (urls) => { + urls.forEach((url) => ensureImageObjectUrl(url)) + }, + { immediate: true, deep: true } +) + watch( imageRefs, (list) => { @@ -2088,6 +2189,67 @@ onBeforeUnmount(() => { margin-top: 4px; } +/* 产物 Tab 中图片生成步骤的缩略图行 */ +.oa-image-group-summary { + font-size: 12px; + color: #8c8c8c; + margin-bottom: 8px; + font-weight: 500; +} + +.oa-image-row { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 0; + border-bottom: 1px solid #f0f0f0; +} + +.oa-image-row:last-child { + border-bottom: none; +} + +.oa-image-thumb { + width: 48px; + height: 36px; + object-fit: cover; + border-radius: 4px; + flex-shrink: 0; + background: #f5f5f5; +} + +.oa-image-thumb-loading { + opacity: 0; + transition: opacity 0.2s; +} + +.oa-image-row-info { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; + flex: 1; +} + +.oa-image-row-section { + font-size: 13px; + color: #333; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.oa-image-row-size { + font-size: 11px; + color: #999; +} + +.oa-file-list-plain { + display: flex; + flex-direction: column; + gap: 6px; +} + .oa-image-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));