feat(wxpa): 配图提示词规则从 Go 硬编码迁移到 JSON 配置驱动

- 新增 spec/06-image_prompt_generation/rule.json 定义配图分布规则(500 字间隔、封面图、正文图尺寸)
- 新增 prompt_template、bracket_format 等 JSON 配置,支持中文提示词模板
- 修改 buildCoverImagePrompt/buildContentImagePrompt 使用 strings.ReplaceAll 替换 {index}/{section_summary} 占位符
- 修改 executeWeixinPublicAccountImagePromptStep 调用 buildWeixinPublicAccountImagePromptsV2
- 新增 buildWeixinPublicAccountContentWithPromptsFromJSON 包装 renderWeixinPublicAccountImagePromptFromJSON
- 前端产物 Tab 新增配图缩略图行展示
This commit is contained in:
eaiadmin
2026-09-25 00:19:15 +08:00
parent d1a737d448
commit e73169df50
4 changed files with 226 additions and 55 deletions
@@ -148,20 +148,51 @@
</div>
<div v-else-if="group.kind === 'files'" class="oa-file-list">
<div v-for="file in group.files" :key="file.key" class="oa-file-row">
<span class="oa-file-kind">{{ file.kindLabel }}</span>
<div class="oa-file-main">
<span class="oa-file-name" :title="file.name">{{ file.name }}</span>
<span v-if="file.meta" class="oa-file-meta">{{ file.meta }}</span>
<div v-if="group.isImageGroup" class="oa-image-group">
<div class="oa-image-group-summary">{{ group.summary }}</div>
<div v-for="file in group.files" :key="file.key" class="oa-image-row">
<img
v-if="file.thumbnailUrl"
:src="file.thumbnailUrl"
:alt="file.name"
class="oa-image-thumb"
/>
<img
v-else
src="data:image/svg+xml;charset=utf-8,"
alt="加载中"
class="oa-image-thumb oa-image-thumb-loading"
/>
<div class="oa-image-row-info">
<span class="oa-image-row-section">{{ file.meta }}</span>
<span v-if="file.size" class="oa-image-row-size">{{ file.size }}</span>
</div>
<button
class="oa-btn oa-btn-tiny"
type="button"
:disabled="previewLoading"
@click="downloadFile(file)"
>
下载
</button>
</div>
</div>
<div v-else class="oa-file-list-plain">
<div v-for="file in group.files" :key="file.key" class="oa-file-row">
<span class="oa-file-kind">{{ file.kindLabel }}</span>
<div class="oa-file-main">
<span class="oa-file-name" :title="file.name">{{ file.name }}</span>
<span v-if="file.meta" class="oa-file-meta">{{ file.meta }}</span>
</div>
<button
class="oa-btn oa-btn-tiny"
type="button"
:disabled="previewLoading"
@click="downloadFile(file)"
>
下载
</button>
</div>
<button
class="oa-btn oa-btn-tiny"
type="button"
:disabled="previewLoading"
@click="downloadFile(file)"
>
下载
</button>
</div>
</div>
@@ -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));