test(e2e): 补全文生图/图生图对比生成并稳定回放

- 增加 SiliconFlow 文生图与图生图用例并启用对比生成\n- 完善 VCR 回放(含非 SSE 响应)并修复输出选择器\n- 图生图跳过易抖动模板下拉、等待上传弹窗关闭;e2e 脚本改用 cross-env 兼容 Windows
This commit is contained in:
linshen committed 2026-01-15 22:43:00 +08:00
1 parent 840d4087a7
commit 90f23b6e0a
11 files changed
+614 -169

No files matched your search

+2 -2
View File
@@ -28,8 +28,8 @@
"test:unit": "pnpm -r test --run --passWithNoTests",
"test:e2e": "playwright test",
"test:e2e:smart": "node scripts/smart-e2e.js",
"test:e2e:record": "E2E_VCR_MODE=record playwright test",
"test:e2e:replay": "E2E_VCR_MODE=replay playwright test",
"test:e2e:record": "cross-env E2E_VCR_MODE=record playwright test",
"test:e2e:replay": "cross-env E2E_VCR_MODE=replay playwright test",
"test:gate:core": "pnpm -F @prompt-optimizer/core test:gate",
"test:gate:ui": "pnpm -F @prompt-optimizer/ui test",
"test:gate:e2e": "playwright test tests/e2e/regression.spec.ts tests/e2e/workflows/p0-route-smoke.spec.ts",
@@ -145,6 +145,7 @@
:style="{ flex: 1, gap: '8px' }"
>
<NButton
data-testid="image-image2image-open-upload"
:disabled="isOptimizing"
@click="openUploadModal"
size="medium"
@@ -158,6 +159,7 @@
class="thumbnail-container"
>
<NImage
data-testid="image-image2image-input-preview"
:src="previewImageUrl"
:style="{
width: '60px',
@@ -198,6 +200,7 @@
>
<template v-if="appOpenModelManager">
<SelectWithConfig
data-testid="image-image2image-text-model-select"
v-model="selectedTextModelKey"
:options="textModelOptions"
:getPrimary="OptionAccessors.getPrimary"
@@ -225,6 +228,7 @@
</template>
<template v-else>
<SelectWithConfig
data-testid="image-image2image-text-model-select"
v-model="selectedTextModelKey"
:options="textModelOptions"
:getPrimary="OptionAccessors.getPrimary"
@@ -262,6 +266,7 @@
v-if="services && services.templateManager"
>
<SelectWithConfig
data-testid="image-image2image-template-select"
v-model="selectedTemplateIdForSelect"
:options="templateOptions"
:getPrimary="OptionAccessors.getPrimary"
@@ -354,6 +359,7 @@
>
<PromptPanelUI
v-if="services && services.templateManager"
test-id="image-image2image"
ref="promptPanelRef"
v-model:optimized-prompt="optimizedPrompt"
:reasoning="optimizedReasoning"
@@ -393,6 +399,7 @@
<n-space align="center" :size="12">
<template v-if="appOpenModelManager">
<SelectWithConfig
data-testid="image-image2image-image-model-select"
v-model="selectedImageModelKey"
:options="imageModelOptions"
:getPrimary="OptionAccessors.getPrimary"
@@ -422,6 +429,7 @@
</template>
<template v-else>
<SelectWithConfig
data-testid="image-image2image-image-model-select"
v-model="selectedImageModelKey"
:options="imageModelOptions"
:getPrimary="OptionAccessors.getPrimary"
@@ -456,6 +464,7 @@
<n-form-item>
<n-space align="center" wrap>
<n-switch
data-testid="image-image2image-generate-compare-toggle"
v-model:value="isCompareMode"
:disabled="isGenerating"
/>
@@ -463,6 +472,7 @@
t("imageWorkspace.generation.compareMode")
}}</n-text>
<n-button
data-testid="image-image2image-generate-button"
type="primary"
:loading="isGenerating"
@click="handleGenerateImage"
@@ -511,6 +521,7 @@
<NSpace vertical :size="12">
<!-- 图像显示 -->
<NImage
data-testid="image-image2image-original-image"
:src="
getImageSrc(
originalImageResult.images[0],
@@ -640,6 +651,7 @@
<NSpace vertical :size="12">
<!-- 图像显示 -->
<NImage
data-testid="image-image2image-optimized-image"
:src="
getImageSrc(
optimizedImageResult.images[0],
@@ -771,6 +783,7 @@
<NSpace vertical :size="12">
<!-- 图像显示 -->
<NImage
data-testid="image-image2image-single-image"
:src="
getImageSrc(
optimizedImageResult.images[0],
@@ -880,136 +893,8 @@
</NSpace>
</template>
<template v-else>
<template
v-if="
originalImageResult &&
originalImageResult.images.length > 0
"
>
<!-- 多模态结果显示:图像 + 文本(使用Naive UI组件) -->
<NSpace vertical :size="12">
<!-- 图像显示 -->
<NImage
:src="
getImageSrc(
originalImageResult.images[0],
)
"
object-fit="contain"
:img-props="{
style: {
width: '100%',
height: 'auto',
display: 'block',
},
}"
/>
<!-- 文本输出显示(如果存在) -->
<template v-if="originalImageResult.text">
<NCard
size="small"
:title="
t(
'imageWorkspace.results.textOutput',
)
"
style="margin-top: 8px"
>
<NText
:depth="2"
style="
white-space: pre-wrap;
line-height: 1.5;
"
>
{{ originalImageResult.text }}
</NText>
</NCard>
</template>
<!-- 操作按钮 -->
<NSpace justify="center" :size="8">
<NButton
@click="
downloadImageFromResult(
originalImageResult
.images[0],
'original',
)
"
>
<template #icon>
<NIcon>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3"
/>
</svg>
</NIcon>
</template>
{{
t(
"imageWorkspace.results.download",
)
}}
</NButton>
<NButton
v-if="originalImageResult.text"
size="small"
secondary
@click="
copyImageText(
originalImageResult.text,
)
"
>
<template #icon>
<NIcon>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect
x="9"
y="9"
width="13"
height="13"
rx="2"
ry="2"
/>
<path
d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"
/>
</svg>
</NIcon>
</template>
{{
t(
"imageWorkspace.results.copyText",
)
}}
</NButton>
</NSpace>
</NSpace>
</template>
<NEmpty
v-else
:description="
t(
'imageWorkspace.results.noGenerationResult',
)
"
:description="t('imageWorkspace.results.noGenerationResult')"
/>
</template>
</template>
@@ -1035,6 +920,7 @@
<!-- 图片上传弹窗 -->
<n-modal
data-testid="image-image2image-upload-modal"
v-model:show="showUploadModal"
preset="card"
:title="t('imageWorkspace.upload.title')"
@@ -1042,6 +928,7 @@
>
<div style="padding: 16px">
<n-upload
data-testid="image-image2image-upload"
:max="1"
accept="image/png,image/jpeg"
:show-file-list="true"
@@ -299,6 +299,7 @@
>
<PromptPanelUI
v-if="services && services.templateManager"
test-id="image-text2image"
ref="promptPanelRef"
v-model:optimized-prompt="optimizedPrompt"
:reasoning="optimizedReasoning"
@@ -338,6 +339,7 @@
<n-space align="center" :size="12">
<template v-if="appOpenModelManager">
<SelectWithConfig
data-testid="image-text2image-image-model-select"
v-model="selectedImageModelKey"
:options="imageModelOptions"
:getPrimary="OptionAccessors.getPrimary"
@@ -367,6 +369,7 @@
</template>
<template v-else>
<SelectWithConfig
data-testid="image-text2image-image-model-select"
v-model="selectedImageModelKey"
:options="imageModelOptions"
:getPrimary="OptionAccessors.getPrimary"
@@ -401,6 +404,7 @@
<n-form-item>
<n-space align="center" wrap>
<n-switch
data-testid="image-text2image-generate-compare-toggle"
v-model:value="isCompareMode"
:disabled="isGenerating"
/>
@@ -408,6 +412,7 @@
t("imageWorkspace.generation.compareMode")
}}</n-text>
<n-button
data-testid="image-text2image-generate-button"
type="primary"
:loading="isGenerating"
@click="handleGenerateImage"
@@ -456,6 +461,7 @@
<NSpace vertical :size="12">
<!-- 图像显示 -->
<NImage
data-testid="image-text2image-original-image"
:src="
getImageSrc(
originalImageResult.images[0],
@@ -585,6 +591,7 @@
<NSpace vertical :size="12">
<!-- 图像显示 -->
<NImage
data-testid="image-text2image-optimized-image"
:src="
getImageSrc(
optimizedImageResult.images[0],
@@ -716,6 +723,7 @@
<NSpace vertical :size="12">
<!-- 图像显示 -->
<NImage
data-testid="image-text2image-single-image"
:src="
getImageSrc(
optimizedImageResult.images[0],
@@ -824,31 +832,32 @@
</NSpace>
</NSpace>
</template>
<template v-else>
<template
v-if="
originalImageResult &&
originalImageResult.images.length > 0
"
>
<!-- 多模态结果显示:图像 + 文本(使用Naive UI组件) -->
<NSpace vertical :size="12">
<!-- 图像显示 -->
<NImage
:src="
getImageSrc(
originalImageResult.images[0],
)
<template v-else>
<template
v-if="
optimizedImageResult &&
optimizedImageResult.images.length > 0
"
object-fit="contain"
:img-props="{
style: {
width: '100%',
height: 'auto',
display: 'block',
},
}"
/>
>
<!-- 多模态结果显示:图像 + 文本(使用Naive UI组件) -->
<NSpace vertical :size="12">
<!-- 图像显示 -->
<NImage
data-testid="image-text2image-optimized-image"
:src="
getImageSrc(
optimizedImageResult.images[0],
)
"
object-fit="contain"
:img-props="{
style: {
width: '100%',
height: 'auto',
display: 'block',
},
}"
/>
<!-- 文本输出显示(如果存在) -->
<template v-if="originalImageResult.text">
View File
Whitespace-only changes.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

@@ -0,0 +1,109 @@
{
"testName": "test\\image-image2image-generate.spec.ts",
"testCase": "上传输入图并在对比模式下生成 original+optimized 两张图",
"interactions": [
{
"provider": "deepseek",
"url": "https://api.deepseek.com/v1/chat/completions",
"method": "POST",
"requestBody": {
"model": "deepseek-chat",
"messages": [
{
"role": "system",
"content": "# Role: Image-to-Image Prompt Optimization Expert\n\n## Profile\n- Author: prompt-optimizer\n- Version: 1.0.0\n- Language: English\n- Description: Specialized in Image-to-Image scenario prompt optimization, providing restrained and natural editing guidance based on existing images\n\n## Background\n- Editing based on existing images requires restrained modifications while preserving original image characteristics\n- Need to clearly specify what to preserve, what to modify, and what to enhance\n- Must consider consistency of original image's composition, style, subject, lighting and color\n- Instructions need to be precise and specific, avoiding excessive changes to original intent\n- Need to balance \"preserving original features\" with \"achieving modification requirements\"\n\n## Task Understanding\nYour task is to optimize user's image modification requests into natural-language Image-to-Image prompts, ensuring desired modifications are achieved while maintaining core characteristics of the original image.\n\n**Key Principle: User's prompt expresses \"what to change/add/remove\", not \"description of what's already in the original image\".**\n\n## Skills\n1. Modification Intent Recognition (Core Ability)\n - **Recognize Addition Intent**: New elements (people, objects, effects) described by user don't exist in original image and need to be naturally added\n - **Recognize Deletion Intent**: User explicitly mentions \"remove/delete/eliminate\" certain elements\n - **Recognize Replacement Intent**: User mentions \"change to/replace with/turn into\", need to replace existing elements\n - **Recognize Enhancement Intent**: User mentions \"more/strengthen/optimize\" certain features, already present in original but need enhancement\n - **Default Preservation Principle**: Elements in original image not mentioned by user are preserved by default\n\n2. Image Editing Understanding\n - Judge feasibility and impact of modifications\n - Predict how new and old elements will blend\n - Ensure coherence of overall effect\n\n3. Precise Instruction Construction\n - Clearly specify elements to keep unchanged\n - Precisely describe parts needing modification\n - Provide specific modification direction and degree\n - Use natural language to clearly describe expected style and effects (no parameters/weights/numbers)\n\n## Goals\n- If request involves single object or simple scene, default to: \"centered single object composition, clean background, soft ground shadow, clear material expression\"\n- Maintain original image's core composition and main features\n- Precisely achieve user's modification requirements\n- Avoid unnecessary excessive modifications\n- Ensure modified results are natural and harmonious\n\n## Constrains\n- Must respect original image's basic composition and subjects\n- Modification amplitude should be moderate, avoid unrecognizable transformation\n- Maintain original image's consistency in style/lighting/color/perspective\n- Instructions clear, specific, executable, using natural language only\n\n## Creative Guidance\n- **Primary Task: Identify whether user describes \"add/delete/replace/enhance\" intent**\n- Use natural language to clearly express boundaries of \"preserve/add/delete/enhance\"\n- For **added elements**: Specify position, size, posture, and relationship with original image\n- For **deleted elements**: Explain how to naturally fill the blank after deletion\n- For **replaced elements**: Specify replacement scope and new element characteristics\n- For **enhanced elements**: Specify enhancement aspects and degree\n- Emphasize natural integration of new and old elements in style, lighting, perspective and color\n- Adjust wording and detail focus based on \"Lens Adaptation\" (photography/design/Chinese aesthetics/illustration)\n- Concise and coherent, no need to follow fixed steps\n\n## Output Requirements\n- Directly output optimized Image-to-Image prompt (natural language, plain text), recommended length 3–6 sentences\n- Do not add any prefixes or explanations; output only the prompt itself\n- **Must explicitly state \"add/delete/replace/enhance\" operations** to help Image-to-Image model understand modification intent\n- Clearly distinguish \"preserve/add/delete/enhance\" elements, emphasize natural integration with original in style/lighting/perspective/color\n- Do not use any parameters/weights/negative lists\n- When explicit clues are lacking, prioritize keeping scene simple: focus attention on subject, clean edges, background without clutter\n- Instructions precise, executable, with natural effects\n\n## Intent Recognition Examples\n**Addition Intent**: User describes new elements not in original → Output should clearly state \"add XX element, position at..., blend with original by...\"\n**Deletion Intent**: User says \"remove/delete background\" → Output should clearly state \"remove XX area, keep subject intact, naturally fill...\"\nLine truncated
},
{
"role": "user",
"content": "Please optimize the following image modification request into natural-language Image-to-Image prompt.\n\nImportant Notes:\n- **User's prompt is \"desired final effect\", not \"description of original image\"**\n- **Key to judging intent**: Do elements user describes exist in original image?\n * If user describes elements not in original → **Addition Intent** (e.g., original has only flower, user says \"person holding flower\" → need to add person)\n * If user explicitly says \"remove/delete/eliminate\" → **Deletion Intent**\n * If user says \"change to/replace with/turn into\" → **Replacement Intent**\n * If user says \"more/strengthen/highlight\" certain feature → **Enhancement Intent** (feature already in original)\n- **Don't speculate original content**: Judge only based on user's prompt and common sense, don't assume original has complex elements not mentioned\n- Clearly state \"preserve elements/add elements/delete elements/enhance elements\", describe specifically in natural language\n- Do not use any parameters/weights/negative lists or intensity numbers\n- Modified effect needs natural integration with original in style, lighting, perspective\n\nModification request to optimize:\nmake it watercolor style\n\nPlease output precise Image-to-Image optimization prompt:"
}
],
"stream": true
},
"requestHash": "816df57c1d2e3927bf78a71f04de2b4ffb03b8fa9a5ae1d234f5cf34cb7db52c",
"rawBody": "data: {\"id\":\"ad444d02-b5fe-451e-9d61-fcb91657069c\",\"object\":\"chat.completion.chunk\",\"created\":1768526685,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"ad444d02-b5fe-451e-9d61-fcb91657069c\",\"object\":\"chat.completion.chunk\",\"created\":1768526685,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"Transform\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"ad444d02-b5fe-451e-9d61-fcb91657069c\",\"object\":\"chat.completion.chunk\",\"created\":1768526685,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" the\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"ad444d02-b5fe-451e-9d61-fcb91657069c\",\"object\":\"chat.completion.chunk\",\"created\":1768526685,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" image\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"ad444d02-b5fe-451e-9d61-fcb91657069c\",\"object\":\"chat.completion.chunk\",\"created\":1768526685,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" into\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"ad444d02-b5fe-451e-9d61-fcb91657069c\",\"object\":\"chat.completion.chunk\",\"created\":1768526685,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" a\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"ad444d02-b5fe-451e-9d61-fcb91657069c\",\"object\":\"chat.completion.chunk\",\"created\":1768526685,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" water\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"ad444d02-b5fe-451e-9d61-fcb91657069c\",\"object\":\"chat.completion.chunk\",\"created\":1768526685,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"color\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"ad444d02-b5fe-451e-9d61-fcb91657069c\",\"object\":\"chat.completion.chunk\",\"created\":1768526685,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" painting\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"ad444d02-b5fe-451e-9d61-fcb91657069c\",\"object\":\"chat.completion.chunk\",\"created\":1768526685,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" style\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"ad444d02-b5fe-451e-9d61-fcb91657069c\",\"object\":\"chat.completion.chunk\",\"created\":1768526685,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\",\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"ad444d02-b5fe-451e-9d61-fcb91657069c\",\"object\":\"chat.completion.chunk\",\"created\":1768526685,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" preserving\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"ad444d02-b5fe-451e-9d61-fcb91657069c\",\"object\":\"chat.completion.chunk\",\"created\":1768526685,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" the\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"ad444d02-b5fe-451e-9d61-fcb91657069c\",\"object\":\"chat.completion.chunk\",\"created\":1768526685,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" original\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"ad444d02-b5fe-451e-9d61-fcb91657069c\",\"object\":\"chat.completion.chunk\",\"created\":1768526685,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" composition\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"ad444d02-b5fe-451e-9d61-fcb91657069c\",\"object\":\"chat.completion.chunk\",\"created\":1768526685,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\",\"},\"lLine truncated
"responseHeaders": {
"content-type": "text/event-stream"
},
"responseBody": {
"id": "ad444d02-b5fe-451e-9d61-fcb91657069c",
"object": "chat.completion.chunk",
"created": 1768526685,
"model": "deepseek-chat",
"system_fingerprint": "fp_eaab8d114b_prod0820_fp8_kvcache",
"choices": [
{
"index": 0,
"delta": {
"content": ""
},
"logprobs": null,
"finish_reason": "stop",
"message": {
"role": "assistant",
"content": "Transform the image into a watercolor painting style, preserving the original composition, subjects, and color palette. Apply soft, blended brushstrokes and subtle texture to mimic traditional watercolor techniques, while maintaining the natural lighting and perspective of the scene."
}
}
],
"usage": {
"prompt_tokens": 1328,
"completion_tokens": 49,
"total_tokens": 1377,
"prompt_tokens_details": {
"cached_tokens": 1280
},
"prompt_cache_hit_tokens": 1280,
"prompt_cache_miss_tokens": 48
}
},
"duration": 3046,
"status": 200
},
{
"provider": "siliconflow",
"url": "https://api.siliconflow.cn/v1/images/generations",
"method": "POST",
"requestBody": {
"model": "Kwai-Kolors/Kolors",
"prompt": "make it watercolor style",
"image_size": "1024x1024",
"num_inference_steps": 20,
"guidance_scale": 7.5,
"outputMimeType": "image/png",
"batch_size": 1,
"image": "data:image/png;base64,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 truncated
},
"requestHash": "e49af9b84c63a244202a725f10c932cc28f4f71a36b357fc94bdb026c58977b6",
"rawBody": "{\"images\":[{\"url\":\"https://s3.siliconflow.cn/default/outputs/c7582ae3-275d-4864-ad96-7c019bf19335_4f7418a4d64a291f62a25c9d1fd0fe1c_ComfyUI_2eefff96_00001_.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAXXXXFILESEXAMPLE%2F20260116%2Fcn-shanghai-1%2Fs3%2Faws4_request&X-Amz-Date=20260116T012456Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Security-Token=eyJhbGciOiJSUzI1NiIsImtpZCI6ImdyYXkiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiJmYWFzOmRlcDpvcHI6ZDI5Y3UzZ2gzdnZjNzNjNWJpNWc6NGYxMDYiLCJpc3MiOiJodHRwczovL2lhbS5zaWxpY29uZmxvdy5jbiIsImlhdCI6MTc2ODUxNzQ5OCwiZXhwIjoxNzY4NjkwMjk4LCJ0eXAiOiJzZXJ2aWNlIiwiYXBsdCI6InNmOmZhYXM6ZmFicmljIiwidG50IjoiZDI5Y3UzZ2gzdnZjNzNjNWJpNWciLCJpZCI6ImQ1a211dWxzc3N2YzczYTVpMnYwIiwiYWNjZXNzIjpbeyJ0eXBlIjoiZmFhcyIsInN1YmplY3RJZCI6ImQyOWN1M2doM3Z2YzczYzViaTVnIiwiYWN0aW9ucyI6WyJmYWFzOmludm9jYXRpb246cHVsbGluZyJdfV19.DYF_r5vQVcmqXdwFwh8TgJI_D4OO6ZeY6Aazr3noqvX9V9IumONrTxiZuR5jxFv4562CzFaQHa1UyyEZjc-CZVsB2Sa4e9mRJg9LLpjtlm2Sd2QOIqpUKM8cg4SzaXifsGHTczZ-nHoRgCEYTBFnOj4E87u5ddBKs6FC7jwQpGOtkC5mDpSd5oOUcmusvJWKgaS_HTDKD8rnT0ozRMpGTiq0qAoqpKu3BNLNggnYoCNjkxNB25z69F2jpghwMOuVOQDsi0Kngiz46c-f1LY72dVdWd_JMYTr6k5a8bZnuLGrcCgYNPS-vxFVf0iCTW-UfPqINUz0nzAjvxCD-77qkJHwE-NDL0-1XIzh3I4PJ2jTgJ2W7cQ9JGitPjnfM_y_1No3kDWN6IFrkjGrU6ef1kdHhogy_x93yYD78OJufcRNqaSjkiAHHFs3TGA7UeHcSW2oDnYylulCmR60Tmrxqih_ttV1tl6v5SmN6LEQ0v7w5d6MAnjBUE2Ih4znEN-u7RtVw8SNss45i1-TW8nIbYWNqVxi4mxjyhgrv_Fs4vw_RxL3YsPPawvFJkE1vQ7APxfQsLz23BfJqiBpW5YUs8q16mzb093ui1cVGAL4SDPrva69h5-M-pREroUepvKEdpeIG6BpJ95aX3HSQ0Y8YxAeMffpcltzONdFLDdbcZQ&X-Amz-Signature=3cb7157931a73fa6dbd663aa1caa5734c3f9298b391f0d116b4941b4f710e56e\"}],\"timings\":{\"inference\":6.119},\"seed\":1594417173,\"shared_id\":\"0\",\"data\":[{\"url\":\"https://s3.siliconflow.cn/default/outputs/c7582ae3-275d-4864-ad96-7c019bf19335_4f7418a4d64a291f62a25c9d1fd0fe1c_ComfyUI_2eefff96_00001_.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAXXXXFILESEXAMPLE%2F20260116%2Fcn-shanghai-1%2Fs3%2Faws4_request&X-Amz-Date=20260116T012456Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Security-Token=eyJhbGciOiJSUzI1NiIsImtpZCI6ImdyYXkiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiJmYWFzOmRlcDpvcHI6ZDI5Y3UzZ2gzdnZjNzNjNWJpNWc6NGYxMDYiLCJpc3MiOiJodHRwczovL2lhbS5zaWxpY29uZmxvdy5jbiIsImlhdCI6MTc2ODUxNzQ5OCwiZXhwIjoxNzY4NjkwMjk4LCJ0eXAiOiJzZXJ2aWNlIiwiYXBsdCI6InNmOmZhYXM6ZmFicmljIiwidG50IjoiZDI5Y3UzZ2gzdnZjNzNjNWJpNWciLCJpZCI6ImQ1a211dWxzc3N2YzczYTVpMnYwIiwiYWNjZXNzIjpbeyJ0eXBlIjoiZmFhcyIsInN1YmplY3RJZCI6ImQyOWN1M2doM3Z2YzczYzViaTVnIiwiYWN0aW9ucyI6WyJmYWFzOmludm9jYXRpb246cHVsbGluZyJdfV19.DYF_r5vQVcmqXdwFwh8TgJI_D4OO6ZeY6Aazr3noqvX9V9IumONrTxiZuR5jxFv4562CzFaQHa1UyyEZjc-CZVsB2Sa4e9mRJg9LLpjtlm2Sd2QOIqpUKM8cg4SzaXifsGHTczZ-nHoRgCEYTBFnOj4E87u5ddBKs6FC7jwQpGOtkC5mDpSd5oOUcmusvJWKgaS_HTDKD8rnT0ozRMpGTiq0qAoqpKu3BNLNggnYoCNjkxNB25z69F2jpghwMOuVOQDsi0Kngiz46c-f1LY72dVdWd_JMYTr6k5a8bZnuLGrcCgYNPS-vxFVf0iCTW-UfPqINUz0nzAjvxCD-77qkJHwE-NDL0-1XIzh3I4PJ2jTgJ2W7cQ9JGitPjnfM_y_1No3kDWN6IFrkjGrU6ef1kdHhogy_x93yYD78OJufcRNqaSjkiAHHFs3TGA7UeHcSW2oDnYylulCmR60Tmrxqih_ttV1tl6v5SmN6LEQ0v7w5d6MAnjBUE2Ih4znEN-u7RtVw8SNss45i1-TW8nIbYWNqVxi4mxjyhgrv_Fs4vw_RxL3YsPPawvFJkE1vQ7APxfQsLz23BfJqiBpW5YUs8q16mzb093ui1cVGAL4SDPrva69h5-M-pREroUepvKEdpeIG6BpJ95aX3HSQ0Y8YxAeMffpcltzONdFLDdbcZQ&X-Amz-Signature=3cb7157931a73fa6dbd663aa1caa5734c3f9298b391f0d116b4941b4f710e56e\"}],\"created\":1768526697}\n",
"responseHeaders": {
"content-type": "application/json; charset=utf-8"
},
"responseBody": null,
"duration": 6999,
"status": 200
},
{
"provider": "siliconflow",
"url": "https://api.siliconflow.cn/v1/images/generations",
"method": "POST",
"requestBody": {
"model": "Kwai-Kolors/Kolors",
"prompt": "Transform the image into a watercolor painting style, preserving the original composition, subjects, and color palette. Apply soft, blended brushstrokes and subtle texture to mimic traditional watercolor techniques, while maintaining the natural lighting and perspective of the scene.",
"image_size": "1024x1024",
"num_inference_steps": 20,
"guidance_scale": 7.5,
"outputMimeType": "image/png",
"batch_size": 1,
"image": "data:image/png;base64,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 truncated
},
"requestHash": "751de4f160fee6d590d655dd6a06c40cd66d59d2db755c8347943d08246ef16a",
"rawBody": "{\"images\":[{\"url\":\"https://s3.siliconflow.cn/default/outputs/2634463b-0345-4e01-aa7a-6b3a2f17adee_ea090ce82ed3461c7fa0fa814dcde0a5_ComfyUI_6ebd32d5_00001_.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAXXXXFILESEXAMPLE%2F20260116%2Fcn-shanghai-1%2Fs3%2Faws4_request&X-Amz-Date=20260116T012501Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Security-Token=eyJhbGciOiJSUzI1NiIsImtpZCI6ImdyYXkiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiJmYWFzOmRlcDpvcHI6ZDI5Y3UzZ2gzdnZjNzNjNWJpNWc6NGYxMDYiLCJpc3MiOiJodHRwczovL2lhbS5zaWxpY29uZmxvdy5jbiIsImlhdCI6MTc2ODUxNzQ5OCwiZXhwIjoxNzY4NjkwMjk4LCJ0eXAiOiJzZXJ2aWNlIiwiYXBsdCI6InNmOmZhYXM6ZmFicmljIiwidG50IjoiZDI5Y3UzZ2gzdnZjNzNjNWJpNWciLCJpZCI6ImQ1a211dWxzc3N2YzczYTVpMnYwIiwiYWNjZXNzIjpbeyJ0eXBlIjoiZmFhcyIsInN1YmplY3RJZCI6ImQyOWN1M2doM3Z2YzczYzViaTVnIiwiYWN0aW9ucyI6WyJmYWFzOmludm9jYXRpb246cHVsbGluZyJdfV19.DYF_r5vQVcmqXdwFwh8TgJI_D4OO6ZeY6Aazr3noqvX9V9IumONrTxiZuR5jxFv4562CzFaQHa1UyyEZjc-CZVsB2Sa4e9mRJg9LLpjtlm2Sd2QOIqpUKM8cg4SzaXifsGHTczZ-nHoRgCEYTBFnOj4E87u5ddBKs6FC7jwQpGOtkC5mDpSd5oOUcmusvJWKgaS_HTDKD8rnT0ozRMpGTiq0qAoqpKu3BNLNggnYoCNjkxNB25z69F2jpghwMOuVOQDsi0Kngiz46c-f1LY72dVdWd_JMYTr6k5a8bZnuLGrcCgYNPS-vxFVf0iCTW-UfPqINUz0nzAjvxCD-77qkJHwE-NDL0-1XIzh3I4PJ2jTgJ2W7cQ9JGitPjnfM_y_1No3kDWN6IFrkjGrU6ef1kdHhogy_x93yYD78OJufcRNqaSjkiAHHFs3TGA7UeHcSW2oDnYylulCmR60Tmrxqih_ttV1tl6v5SmN6LEQ0v7w5d6MAnjBUE2Ih4znEN-u7RtVw8SNss45i1-TW8nIbYWNqVxi4mxjyhgrv_Fs4vw_RxL3YsPPawvFJkE1vQ7APxfQsLz23BfJqiBpW5YUs8q16mzb093ui1cVGAL4SDPrva69h5-M-pREroUepvKEdpeIG6BpJ95aX3HSQ0Y8YxAeMffpcltzONdFLDdbcZQ&X-Amz-Signature=c4f469c324486bd0ade002097becab7b9b09a2c4c54a2c4857fbd43a949f2f43\"}],\"timings\":{\"inference\":4.575},\"seed\":2001704611,\"shared_id\":\"0\",\"data\":[{\"url\":\"https://s3.siliconflow.cn/default/outputs/2634463b-0345-4e01-aa7a-6b3a2f17adee_ea090ce82ed3461c7fa0fa814dcde0a5_ComfyUI_6ebd32d5_00001_.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAXXXXFILESEXAMPLE%2F20260116%2Fcn-shanghai-1%2Fs3%2Faws4_request&X-Amz-Date=20260116T012501Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Security-Token=eyJhbGciOiJSUzI1NiIsImtpZCI6ImdyYXkiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiJmYWFzOmRlcDpvcHI6ZDI5Y3UzZ2gzdnZjNzNjNWJpNWc6NGYxMDYiLCJpc3MiOiJodHRwczovL2lhbS5zaWxpY29uZmxvdy5jbiIsImlhdCI6MTc2ODUxNzQ5OCwiZXhwIjoxNzY4NjkwMjk4LCJ0eXAiOiJzZXJ2aWNlIiwiYXBsdCI6InNmOmZhYXM6ZmFicmljIiwidG50IjoiZDI5Y3UzZ2gzdnZjNzNjNWJpNWciLCJpZCI6ImQ1a211dWxzc3N2YzczYTVpMnYwIiwiYWNjZXNzIjpbeyJ0eXBlIjoiZmFhcyIsInN1YmplY3RJZCI6ImQyOWN1M2doM3Z2YzczYzViaTVnIiwiYWN0aW9ucyI6WyJmYWFzOmludm9jYXRpb246cHVsbGluZyJdfV19.DYF_r5vQVcmqXdwFwh8TgJI_D4OO6ZeY6Aazr3noqvX9V9IumONrTxiZuR5jxFv4562CzFaQHa1UyyEZjc-CZVsB2Sa4e9mRJg9LLpjtlm2Sd2QOIqpUKM8cg4SzaXifsGHTczZ-nHoRgCEYTBFnOj4E87u5ddBKs6FC7jwQpGOtkC5mDpSd5oOUcmusvJWKgaS_HTDKD8rnT0ozRMpGTiq0qAoqpKu3BNLNggnYoCNjkxNB25z69F2jpghwMOuVOQDsi0Kngiz46c-f1LY72dVdWd_JMYTr6k5a8bZnuLGrcCgYNPS-vxFVf0iCTW-UfPqINUz0nzAjvxCD-77qkJHwE-NDL0-1XIzh3I4PJ2jTgJ2W7cQ9JGitPjnfM_y_1No3kDWN6IFrkjGrU6ef1kdHhogy_x93yYD78OJufcRNqaSjkiAHHFs3TGA7UeHcSW2oDnYylulCmR60Tmrxqih_ttV1tl6v5SmN6LEQ0v7w5d6MAnjBUE2Ih4znEN-u7RtVw8SNss45i1-TW8nIbYWNqVxi4mxjyhgrv_Fs4vw_RxL3YsPPawvFJkE1vQ7APxfQsLz23BfJqiBpW5YUs8q16mzb093ui1cVGAL4SDPrva69h5-M-pREroUepvKEdpeIG6BpJ95aX3HSQ0Y8YxAeMffpcltzONdFLDdbcZQ&X-Amz-Signature=c4f469c324486bd0ade002097becab7b9b09a2c4c54a2c4857fbd43a949f2f43\"}],\"created\":1768526702}\n",
"responseHeaders": {
"content-type": "application/json; charset=utf-8"
},
"responseBody": null,
"duration": 5150,
"status": 200
}
]
}
@@ -0,0 +1,107 @@
{
"testName": "test\\image-text2image-generate.spec.ts",
"testCase": "切换到 SiliconFlow 图像模型并生成图片(对比模式)",
"interactions": [
{
"provider": "deepseek",
"url": "https://api.deepseek.com/v1/chat/completions",
"method": "POST",
"requestBody": {
"model": "deepseek-chat",
"messages": [
{
"role": "system",
"content": "# Role: General Natural-Language Image Prompt Optimization Expert\n\n## Profile\n- Author: prompt-optimizer\n- Version: 1.3.0\n- Language: English\n- Description: General natural-language prompt optimization for multimodal image models, organized around subject, action, environment anchors, composition/viewpoint, lighting/time, color/material, and atmosphere; use natural language throughout with no parameters, weights, or negative lists\n\n## Background\n- Multimodal models understand natural language well; tags, weights, and negative lists are unnecessary\n- Structured visual information (subject, composition, lighting, palette, material, atmosphere) significantly improves controllability and stability\n- Use coherent natural language instead of keyword stacking; aim for specific, visual, layered detail\n\n## Task Understanding\nDirectly enrich and structure the user's original description; in natural language, add subject characteristics, action and interaction, environmental anchors, lighting and palette, material and texture, atmosphere and emotion, composition and viewpoint (specify aspect ratio if needed).\n\n## Skills\n1. Subject & Action\n - Use 2–3 precise modifiers to portray shape, expression, and texture\n - Add one clear action or interaction with props to enhance storytelling\n - For interaction or motion, use natural cues for subtle dynamics: eye contact, gestural response, slight body lean/turn, expression echo, small object motion (e.g., ripples in a cup, gentle clink); avoid rigid posing and fully static descriptions\n2. Environment & Space\n - Set recognizable environment anchors (cabin/garden path/forest clearing, etc.)\n - Clarify foreground/midground/background layers to stabilize spatial relations\n3. Lighting & Time\n - Describe light quality and direction (soft/hard; side/back/top light, etc.)\n - Specify time mood (morning/golden hour/night) in harmony with light effect\n - Include emotional functions of directional/side/back light, rim highlights, light spots and reflections\n - Explain how light affects the subject (outlines silhouette, highlights features, softens background)\n4. Color & Material\n - Dominant palette and complementary contrast (pastel/warm earth/cool scheme)\n - Material feel and image grain (hand-drawn lines/soft brushwork/paper grain/metal/glass/fabric)\n - Example materials: film grain/plastic/metal/glass/rubber/paper grain; emphasize real details (chamfers, reflections, micro-scratches)\n5. Atmosphere & Style\n - Use abstract style words to express unified aesthetics (fairy tale/storybook/pastoral warmth/cool/severe/dramatic)\n6. Composition & Viewpoint\n - State aspect ratio in natural language (e.g., \"square composition/square format\"), lens distance (close-up/half-body/wide shot), viewpoint (eye-level/high/low)\n - Add composition type (selfie composition/isometric/three-view), using natural language to express frame and perspective\n7. Contrast & Resonance\n - Use contrast in light/dark, warm/cool, soft/hard, motion/stillness to create tension\n - Let foreground details and background imagery echo the theme for unity\n\n## Goals\n- Produce clear, specific, visual natural-language prompts\n- Do not include parameters, weights, or negative lists\n- Keep language concise and coherent; directly usable\n\n## Constraints\n- Do not use technical parameters (sampling/steps/seed)\n- Do not use weighting syntax or negative lists\n- Preserve the original creative intent\n\n## Quality Assurance\n- Clear focus and clean edges; simple background that does not distract\n- Keep attention on the subject; avoid overload; maintain stable composition\n\n## Creative Guidance\n- Preserve original intent; express directly in natural language\n- Use structured narration: organize information into 3–6 separate yet coherent sentences\n * Simple scenes: 3 sentences; complex scenes: 5–6 sentences\n * Each sentence focuses on one core task; avoid stuffing too many dimensions into a single sentence\n- Modifier density: every key noun gets 2–3 precise modifiers\n * Example: not just \"light\", but \"soft, diffused morning light\"\n * Example: not just \"panda\", but \"a panda with fluffy black-and-white fur\"\n- Recommended sentence allocation:\n * S1: subject + key traits + action + environment anchor\n * S2: light source + light quality + time + palette tendency\n * S3: atmosphere and emotion + abstract style words\n * S4 (optional): material and texture (lines/brushwork/paper grain)\n * S5 (optional): composition/viewpoint/aspect ratio (natural language, e.g., isometric/selfie composition)\n * S6 (optional): narrative tension/imagery (small conflict, pauses and breathing space)\n * Dynamic/interaction tips: use \"captured moment\", \"in the middle of…\", \"mutual gaze and gestural response\"; combine shallow depth of field and micro motion cues (subtle hand movement, gentle touch/vibration of objects)Line truncated
},
{
"role": "user",
"content": "Please optimize the following description into a general natural-language image prompt:\n\nNotes:\n- Use natural language only; do not include parameters, weights, or negative lists\n- Output 3–6 structured sentences, each focusing on one core dimension\n- Each key noun should have 2–3 precise modifiers (e.g., \"soft, diffused morning light\")\n- Suggested pattern: subject + action + environment anchor → lighting + time + palette → atmosphere + style → (optional) material/texture or composition/viewpoint\n\nOriginal description:\ncorgi, studio photo\n\nPlease output the optimized prompt:"
}
],
"stream": true
},
"requestHash": "e31c749e8724a4cd484098c1dbcea86a2ae0c6e531b065b02e83ab6eaa1ede6f",
"rawBody": "data: {\"id\":\"414b7ed9-597b-4c9d-9b7b-f15c488943cf\",\"object\":\"chat.completion.chunk\",\"created\":1768468841,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"414b7ed9-597b-4c9d-9b7b-f15c488943cf\",\"object\":\"chat.completion.chunk\",\"created\":1768468841,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"A\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"414b7ed9-597b-4c9d-9b7b-f15c488943cf\",\"object\":\"chat.completion.chunk\",\"created\":1768468841,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" fluffy\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"414b7ed9-597b-4c9d-9b7b-f15c488943cf\",\"object\":\"chat.completion.chunk\",\"created\":1768468841,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\",\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"414b7ed9-597b-4c9d-9b7b-f15c488943cf\",\"object\":\"chat.completion.chunk\",\"created\":1768468841,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" short\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"414b7ed9-597b-4c9d-9b7b-f15c488943cf\",\"object\":\"chat.completion.chunk\",\"created\":1768468841,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"-legged\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"414b7ed9-597b-4c9d-9b7b-f15c488943cf\",\"object\":\"chat.completion.chunk\",\"created\":1768468841,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" cor\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"414b7ed9-597b-4c9d-9b7b-f15c488943cf\",\"object\":\"chat.completion.chunk\",\"created\":1768468841,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"gi\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"414b7ed9-597b-4c9d-9b7b-f15c488943cf\",\"object\":\"chat.completion.chunk\",\"created\":1768468841,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" sits\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"414b7ed9-597b-4c9d-9b7b-f15c488943cf\",\"object\":\"chat.completion.chunk\",\"created\":1768468841,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" attent\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"414b7ed9-597b-4c9d-9b7b-f15c488943cf\",\"object\":\"chat.completion.chunk\",\"created\":1768468841,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"ively\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"414b7ed9-597b-4c9d-9b7b-f15c488943cf\",\"object\":\"chat.completion.chunk\",\"created\":1768468841,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" on\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"414b7ed9-597b-4c9d-9b7b-f15c488943cf\",\"object\":\"chat.completion.chunk\",\"created\":1768468841,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" a\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"414b7ed9-597b-4c9d-9b7b-f15c488943cf\",\"object\":\"chat.completion.chunk\",\"created\":1768468841,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" clean\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"414b7ed9-597b-4c9d-9b7b-f15c488943cf\",\"object\":\"chat.completion.chunk\",\"created\":1768468841,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\",\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"414b7ed9-597b-4c9d-9b7b-f15c488943cf\",\"object\":\"chat.completion.chunk\",\"created\":1768468841,\"model\":\"deepseek-chat\",\"system_fingerprint\":\"fp_eaab8d114b_prod0820_fp8_kvcache\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" seamless\"},\"logprobs\":null,\"finish_Line truncated
"responseHeaders": {
"content-type": "text/event-stream"
},
"responseBody": {
"id": "414b7ed9-597b-4c9d-9b7b-f15c488943cf",
"object": "chat.completion.chunk",
"created": 1768468841,
"model": "deepseek-chat",
"system_fingerprint": "fp_eaab8d114b_prod0820_fp8_kvcache",
"choices": [
{
"index": 0,
"delta": {
"content": ""
},
"logprobs": null,
"finish_reason": "stop",
"message": {
"role": "assistant",
"content": "A fluffy, short-legged corgi sits attentively on a clean, seamless studio backdrop. Soft, diffused studio lighting from a large window source creates gentle highlights on its red and white fur, set against a muted, neutral color palette. The atmosphere is bright and cheerful, captured in a classic pet portrait style with a warm, inviting mood. The image is a close-up, eye-level composition that focuses on the corgi's alert expression and perky ears."
}
}
],
"usage": {
"prompt_tokens": 1368,
"completion_tokens": 93,
"total_tokens": 1461,
"prompt_tokens_details": {
"cached_tokens": 1344
},
"prompt_cache_hit_tokens": 1344,
"prompt_cache_miss_tokens": 24
}
},
"duration": 4278,
"status": 200
},
{
"provider": "siliconflow",
"url": "https://api.siliconflow.cn/v1/images/generations",
"method": "POST",
"requestBody": {
"model": "Kwai-Kolors/Kolors",
"prompt": "corgi, studio photo",
"image_size": "1024x1024",
"num_inference_steps": 20,
"guidance_scale": 7.5,
"outputMimeType": "image/png",
"batch_size": 1
},
"requestHash": "6a2e82ec8d630fa1c0c8cba5d908fba696f59b78bee161fddd6a28fc7a888fc1",
"rawBody": "{\"images\":[{\"url\":\"https://s3.siliconflow.cn/default/outputs/163ce948-6e6c-4542-84c8-79292c791937_43c2cbf166f8369dbe02ca8c914b865d_ComfyUI_055e3235_00001_.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAXXXXFILESEXAMPLE%2F20260115%2Fcn-shanghai-1%2Fs3%2Faws4_request&X-Amz-Date=20260115T092049Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Security-Token=eyJhbGciOiJSUzI1NiIsImtpZCI6ImdyYXkiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiJmYWFzOmRlcDpvcHI6ZDI5Y3UzZ2gzdnZjNzNjNWJpNWc6N2NjZjkiLCJpc3MiOiJodHRwczovL2lhbS5zaWxpY29uZmxvdy5jbiIsImlhdCI6MTc2ODQ0OTA5OSwiZXhwIjoxNzY4NjIxODk5LCJ0eXAiOiJzZXJ2aWNlIiwiYXBsdCI6InNmOmZhYXM6ZmFicmljIiwidG50IjoiZDI5Y3UzZ2gzdnZjNzNjNWJpNWciLCJpZCI6ImQ1azY4aXA3MTluczczOGRla3BnIiwiYWNjZXNzIjpbeyJ0eXBlIjoiZmFhcyIsInN1YmplY3RJZCI6ImQyOWN1M2doM3Z2YzczYzViaTVnIiwiYWN0aW9ucyI6WyJmYWFzOmludm9jYXRpb246cHVsbGluZyJdfV19.hfUwYtuQFdEDjWmJqOsAV1n_knvI9-g9T0aO15bzfELNs-5BD4PmVnglY8icElOvSXVEDExVMMws79A_Z-DO7Ned6eYgQ0yvyfKhmBs70NhM4cakQewqjYe4kOlYqOQSPnvu98mI_i3vc7bQk-VYTLchfDIFaA84u8ReXOR0CHdpdu_EIoFSzhhX71YODExi61B1m0DcDMUUYrO5S8L7K5qMhoNovdaw01kpW9CHw4Nu1Vfad_dy2MgJm2NOOmqppC84NbacdB8BcvbLKQvAGQ2oLltZnWAq2HaIEo6xzB3_Im4WVTJ3c40VFROeKa4kIK8T3m2GdFiSLqaquMLHfcbyzAu-yNlm8_NMGFVoeEA3ZuvgmiCIfE77W6izwCY4fuL6NKwaUi4tu_fuQiouQWBb3xnP16kBkOvx7tPK0lA7dwXFURYJYNtTlgoxYYsHJ2mAjGoUROzgTDII20IfEetnJJtpEh7Ru7dOANdu6m7IkWhnYehtDDO91dbnmYVaorBCauuD3EddcARTZ1HlkH4rUpaZAMPCrMVP2Otp3M41beSIk8-13PAOpGEjdrNgFYD-y5qzMdIrPJHMkA8UQ0CjAitvyHpSmWPfdKo7IDjSwGElck61FofqIjhgAKgAYw9V_v_y_xCJSNaaFKcLP4_pbh04xf17tp9krl_T6dQ&X-Amz-Signature=6f4ac30a52d3b68a877997b1d2e6bf3bdc9c57b6863c017ce92d144213feff2e\"}],\"timings\":{\"inference\":4.043},\"seed\":746895220,\"shared_id\":\"0\",\"data\":[{\"url\":\"https://s3.siliconflow.cn/default/outputs/163ce948-6e6c-4542-84c8-79292c791937_43c2cbf166f8369dbe02ca8c914b865d_ComfyUI_055e3235_00001_.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAXXXXFILESEXAMPLE%2F20260115%2Fcn-shanghai-1%2Fs3%2Faws4_request&X-Amz-Date=20260115T092049Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Security-Token=eyJhbGciOiJSUzI1NiIsImtpZCI6ImdyYXkiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiJmYWFzOmRlcDpvcHI6ZDI5Y3UzZ2gzdnZjNzNjNWJpNWc6N2NjZjkiLCJpc3MiOiJodHRwczovL2lhbS5zaWxpY29uZmxvdy5jbiIsImlhdCI6MTc2ODQ0OTA5OSwiZXhwIjoxNzY4NjIxODk5LCJ0eXAiOiJzZXJ2aWNlIiwiYXBsdCI6InNmOmZhYXM6ZmFicmljIiwidG50IjoiZDI5Y3UzZ2gzdnZjNzNjNWJpNWciLCJpZCI6ImQ1azY4aXA3MTluczczOGRla3BnIiwiYWNjZXNzIjpbeyJ0eXBlIjoiZmFhcyIsInN1YmplY3RJZCI6ImQyOWN1M2doM3Z2YzczYzViaTVnIiwiYWN0aW9ucyI6WyJmYWFzOmludm9jYXRpb246cHVsbGluZyJdfV19.hfUwYtuQFdEDjWmJqOsAV1n_knvI9-g9T0aO15bzfELNs-5BD4PmVnglY8icElOvSXVEDExVMMws79A_Z-DO7Ned6eYgQ0yvyfKhmBs70NhM4cakQewqjYe4kOlYqOQSPnvu98mI_i3vc7bQk-VYTLchfDIFaA84u8ReXOR0CHdpdu_EIoFSzhhX71YODExi61B1m0DcDMUUYrO5S8L7K5qMhoNovdaw01kpW9CHw4Nu1Vfad_dy2MgJm2NOOmqppC84NbacdB8BcvbLKQvAGQ2oLltZnWAq2HaIEo6xzB3_Im4WVTJ3c40VFROeKa4kIK8T3m2GdFiSLqaquMLHfcbyzAu-yNlm8_NMGFVoeEA3ZuvgmiCIfE77W6izwCY4fuL6NKwaUi4tu_fuQiouQWBb3xnP16kBkOvx7tPK0lA7dwXFURYJYNtTlgoxYYsHJ2mAjGoUROzgTDII20IfEetnJJtpEh7Ru7dOANdu6m7IkWhnYehtDDO91dbnmYVaorBCauuD3EddcARTZ1HlkH4rUpaZAMPCrMVP2Otp3M41beSIk8-13PAOpGEjdrNgFYD-y5qzMdIrPJHMkA8UQ0CjAitvyHpSmWPfdKo7IDjSwGElck61FofqIjhgAKgAYw9V_v_y_xCJSNaaFKcLP4_pbh04xf17tp9krl_T6dQ&X-Amz-Signature=6f4ac30a52d3b68a877997b1d2e6bf3bdc9c57b6863c017ce92d144213feff2e\"}],\"created\":1768468850}\n",
"responseHeaders": {
"content-type": "application/json; charset=utf-8"
},
"responseBody": null,
"duration": 4402,
"status": 200
},
{
"provider": "siliconflow",
"url": "https://api.siliconflow.cn/v1/images/generations",
"method": "POST",
"requestBody": {
"model": "Kwai-Kolors/Kolors",
"prompt": "A fluffy, short-legged corgi sits attentively on a clean, seamless studio backdrop. Soft, diffused studio lighting from a large window source creates gentle highlights on its red and white fur, set against a muted, neutral color palette. The atmosphere is bright and cheerful, captured in a classic pet portrait style with a warm, inviting mood. The image is a close-up, eye-level composition that focuses on the corgi's alert expression and perky ears.",
"image_size": "1024x1024",
"num_inference_steps": 20,
"guidance_scale": 7.5,
"outputMimeType": "image/png",
"batch_size": 1
},
"requestHash": "e742a5373e68a4ad80c886102f272f6c6c9e92da9384d37045383ca39eef069b",
"rawBody": "{\"images\":[{\"url\":\"https://s3.siliconflow.cn/default/outputs/9262629b-c1f2-4d09-9f58-a4472b83eefb_87b59841dc1995320f71d51b6a5006dd_ComfyUI_73cce29e_00001_.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAXXXXFILESEXAMPLE%2F20260115%2Fcn-shanghai-1%2Fs3%2Faws4_request&X-Amz-Date=20260115T092053Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Security-Token=eyJhbGciOiJSUzI1NiIsImtpZCI6ImdyYXkiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiJmYWFzOmRlcDpvcHI6ZDI5Y3UzZ2gzdnZjNzNjNWJpNWc6YzQwOGYiLCJpc3MiOiJodHRwczovL2lhbS5zaWxpY29uZmxvdy5jbiIsImlhdCI6MTc2ODQ2MzUwMSwiZXhwIjoxNzY4NjM2MzAxLCJ0eXAiOiJzZXJ2aWNlIiwiYXBsdCI6InNmOmZhYXM6ZmFicmljIiwidG50IjoiZDI5Y3UzZ2gzdnZjNzNjNWJpNWciLCJpZCI6ImQ1azlwMzk3MTluczczOG5tdWswIiwiYWNjZXNzIjpbeyJ0eXBlIjoiZmFhcyIsInN1YmplY3RJZCI6ImQyOWN1M2doM3Z2YzczYzViaTVnIiwiYWN0aW9ucyI6WyJmYWFzOmludm9jYXRpb246cHVsbGluZyJdfV19.lk8pCebLN5IQRysT2UZUfi4M-NMlsF9OMgJztZcXYOPOGUx29lKeenBz9cMYu1sagUFUeY2J1eav-bHCOX2-D5M5lwjYIL1_3pHevZqq4sxjTlJVuTEqGchWcW-iZYLrN_j6wdW4Njdrc-jsFhqca4fqsHD3qp8b4Qw-MXJ6FZmVL8unq0Pz48JCJMNYvcax2Kax2ZGQLGf_R2Kvzh_MSm77oUECkm5byM5oKLfP3JtyOa--Ulmd8kFrIhxGhWrWpSFHFP-geBBRPEo9juGzqGB1tbzHf4GuszhwEn8lO9tSH2oFgy-i_HD0SF7BRCAkqtCe86RO-elIdDXnzLkX2Ue_Wg-OEysEtj3zg4MsZR4q2sVceTmqD7JwQWI9GT0HMEUJUsInAPhrItKbbBrf_VJYPpOV2fP8YcvhTmRxXzVe5u39Hhzhf_O7Yhrrws-LgsI9QVxCIZ0jg48k4E4Z44RS8dNoquVqwGJIeD0m_XkxEZQkc6bPvcym8gm8KlHgiP_SD7zBfe5sl9YiU0J5ArBDYw1T5ZZmieCWVhP2yXe2XevFH6XdIfQK7vYCh9a1axhpWynXSZWg3WGKVUx2NWcvp6x37Mjs_BwJAyzHlOdQh6p1oTK15yPkG6oQPbJF31uMEPG46b1Whpxgxd0vDQ14XBQwTLDoMILOMkpV3ME&X-Amz-Signature=e2e127b3f3683950635001b858b3d94d81f59643d3f745d7ddf646a526986201\"}],\"timings\":{\"inference\":3.64},\"seed\":1355599120,\"shared_id\":\"0\",\"data\":[{\"url\":\"https://s3.siliconflow.cn/default/outputs/9262629b-c1f2-4d09-9f58-a4472b83eefb_87b59841dc1995320f71d51b6a5006dd_ComfyUI_73cce29e_00001_.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAXXXXFILESEXAMPLE%2F20260115%2Fcn-shanghai-1%2Fs3%2Faws4_request&X-Amz-Date=20260115T092053Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Security-Token=eyJhbGciOiJSUzI1NiIsImtpZCI6ImdyYXkiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiJmYWFzOmRlcDpvcHI6ZDI5Y3UzZ2gzdnZjNzNjNWJpNWc6YzQwOGYiLCJpc3MiOiJodHRwczovL2lhbS5zaWxpY29uZmxvdy5jbiIsImlhdCI6MTc2ODQ2MzUwMSwiZXhwIjoxNzY4NjM2MzAxLCJ0eXAiOiJzZXJ2aWNlIiwiYXBsdCI6InNmOmZhYXM6ZmFicmljIiwidG50IjoiZDI5Y3UzZ2gzdnZjNzNjNWJpNWciLCJpZCI6ImQ1azlwMzk3MTluczczOG5tdWswIiwiYWNjZXNzIjpbeyJ0eXBlIjoiZmFhcyIsInN1YmplY3RJZCI6ImQyOWN1M2doM3Z2YzczYzViaTVnIiwiYWN0aW9ucyI6WyJmYWFzOmludm9jYXRpb246cHVsbGluZyJdfV19.lk8pCebLN5IQRysT2UZUfi4M-NMlsF9OMgJztZcXYOPOGUx29lKeenBz9cMYu1sagUFUeY2J1eav-bHCOX2-D5M5lwjYIL1_3pHevZqq4sxjTlJVuTEqGchWcW-iZYLrN_j6wdW4Njdrc-jsFhqca4fqsHD3qp8b4Qw-MXJ6FZmVL8unq0Pz48JCJMNYvcax2Kax2ZGQLGf_R2Kvzh_MSm77oUECkm5byM5oKLfP3JtyOa--Ulmd8kFrIhxGhWrWpSFHFP-geBBRPEo9juGzqGB1tbzHf4GuszhwEn8lO9tSH2oFgy-i_HD0SF7BRCAkqtCe86RO-elIdDXnzLkX2Ue_Wg-OEysEtj3zg4MsZR4q2sVceTmqD7JwQWI9GT0HMEUJUsInAPhrItKbbBrf_VJYPpOV2fP8YcvhTmRxXzVe5u39Hhzhf_O7Yhrrws-LgsI9QVxCIZ0jg48k4E4Z44RS8dNoquVqwGJIeD0m_XkxEZQkc6bPvcym8gm8KlHgiP_SD7zBfe5sl9YiU0J5ArBDYw1T5ZZmieCWVhP2yXe2XevFH6XdIfQK7vYCh9a1axhpWynXSZWg3WGKVUx2NWcvp6x37Mjs_BwJAyzHlOdQh6p1oTK15yPkG6oQPbJF31uMEPG46b1Whpxgxd0vDQ14XBQwTLDoMILOMkpV3ME&X-Amz-Signature=e2e127b3f3683950635001b858b3d94d81f59643d3f745d7ddf646a526986201\"}],\"created\":1768468854}\n",
"responseHeaders": {
"content-type": "application/json; charset=utf-8"
},
"responseBody": null,
"duration": 3754,
"status": 200
}
]
}
+2
View File
@@ -3,6 +3,8 @@ import { expect, type Page } from '@playwright/test'
export type OptimizeWorkspaceMode =
| 'basic-system'
| 'basic-user'
| 'image-text2image'
| 'image-image2image'
| 'pro-variable'
| 'pro-multi'
+130 -14
View File
@@ -19,7 +19,7 @@ import * as crypto from 'crypto'
/**
* LLM API 提供商
*/
type LLMProvider = 'openai' | 'deepseek' | 'anthropic' | 'gemini' | 'zhipu' | 'modelscope'
type LLMProvider = 'openai' | 'deepseek' | 'anthropic' | 'gemini' | 'zhipu' | 'modelscope' | 'siliconflow'
/**
* VCR 模式
@@ -43,8 +43,19 @@ interface VCRInteraction {
method: string
requestBody: any
requestHash: string
responseBody: any // 解析后的响应(用于调试,实际回放时使用 rawSSE)
rawSSE: string // 原始 SSE 响应文本(回放时使用)
/** Raw response body as UTF-8 text (SSE or JSON). */
rawBody: string
/** Response headers captured at record time (subset). */
responseHeaders: Record<string, string>
/**
* Parsed response body (for debugging only).
* For SSE responses this is the reconstructed final JSON.
*/
responseBody: any
duration: number
status: number
}
@@ -64,7 +75,7 @@ interface VCRFixture {
url?: string
requestBody?: any
responseBody?: any
rawSSE?: string
rawSSE?: string // legacy only
duration?: number
}
@@ -93,6 +104,15 @@ class E2EVCR {
this.recordingEnabled = await this.shouldRecord()
this.replayConsumedByHash = new Map()
// In explicit record mode, always start from a clean fixture file to avoid mixing old interactions.
if (this.config.mode === 'record') {
try {
await fs.rm(this.getFixturePath(), { force: true })
} catch {
// ignore
}
}
const modeSymbol = this.getModeSymbol()
console.log(`[VCR] ${modeSymbol} Test: ${testName} - ${testCase}`)
}
@@ -173,6 +193,7 @@ class E2EVCR {
if (url.includes('generativelanguage.googleapis.com')) return 'gemini'
if (url.includes('open.bigmodel.cn')) return 'zhipu'
if (url.includes('modelscope.cn')) return 'modelscope'
if (url.includes('api.siliconflow.cn')) return 'siliconflow'
return null
}
@@ -196,13 +217,40 @@ class E2EVCR {
}
private computeRequestHash(provider: LLMProvider, url: string, method: string, requestBody: any): string {
const payload = `${provider}|${method}|${url}|${this.stableStringify(requestBody)}`
// Normalize url: for some providers, query params (e.g. cache busters) should not affect matching.
const normalizedUrl = url.split('?')[0]
// Image2Image requests can embed huge base64 strings; avoid hashing raw bytes.
// The hash only needs to distinguish interactions within a test run.
const normalizedBody = (() => {
if (!requestBody || typeof requestBody !== 'object') return requestBody
const cloned = JSON.parse(JSON.stringify(requestBody))
const b64 = cloned?.inputImage?.b64
if (typeof b64 === 'string' && b64.length > 0) {
cloned.inputImage.b64 = `__b64_len_${b64.length}__`
}
return cloned
})()
const payload = `${provider}|${method}|${normalizedUrl}|${this.stableStringify(normalizedBody)}`
return crypto.createHash('sha256').update(payload).digest('hex')
}
private normalizeFixture(fixture: VCRFixture | null): VCRFixture {
if (fixture && Array.isArray((fixture as any).interactions)) return fixture
if (fixture && Array.isArray((fixture as any).interactions)) {
// Backward compat: older multi-interaction fixtures stored rawSSE.
const interactions = (fixture as any).interactions as any[]
for (const it of interactions) {
if (typeof it.rawBody === 'undefined' && typeof it.rawSSE !== 'undefined') {
it.rawBody = it.rawSSE
it.responseHeaders = it.responseHeaders || { 'content-type': 'text/event-stream' }
delete it.rawSSE
}
}
return fixture
}
// Legacy single-interaction fixtures: normalize into interactions[].
if (fixture && (fixture as any).rawSSE) {
const legacyProvider = (fixture as any).provider as LLMProvider
const legacyUrl = (fixture as any).url as string
@@ -210,6 +258,8 @@ class E2EVCR {
const legacyMethod = 'POST'
const legacyRequestHash = this.computeRequestHash(legacyProvider, legacyUrl, legacyMethod, legacyRequestBody)
const rawBody = String((fixture as any).rawSSE || '')
return {
testName: fixture.testName,
testCase: fixture.testCase,
@@ -220,8 +270,9 @@ class E2EVCR {
method: legacyMethod,
requestBody: legacyRequestBody,
requestHash: legacyRequestHash,
rawBody,
responseHeaders: { 'content-type': 'text/event-stream' },
responseBody: (fixture as any).responseBody,
rawSSE: (fixture as any).rawSSE,
duration: Number((fixture as any).duration ?? 0),
status: 200,
},
@@ -252,7 +303,8 @@ class E2EVCR {
requestBody: any,
responseBody: any,
duration: number,
rawSSE: string,
rawBody: string,
responseHeaders: Record<string, string>,
method: string,
status: number
): Promise<void> {
@@ -267,15 +319,33 @@ class E2EVCR {
interactions: [...existing.interactions],
}
const sanitizedBody = (() => {
try {
// Keep the fixture small: drop huge base64 payloads.
if (requestBody && typeof requestBody === 'object') {
const cloned = JSON.parse(JSON.stringify(requestBody))
const b64 = cloned?.inputImage?.b64
if (typeof b64 === 'string' && b64.length > 0) {
cloned.inputImage.b64 = `__b64_len_${b64.length}__`
}
return cloned
}
} catch {
// ignore
}
return requestBody
})()
fixture.interactions.push({
provider,
url,
method,
requestBody,
requestBody: sanitizedBody,
requestHash,
rawBody,
responseHeaders,
responseBody,
duration,
rawSSE,
status,
})
@@ -340,6 +410,7 @@ class E2EVCR {
/https:\/\/generativelanguage\.googleapis\.com\/.*/,
/https:\/\/open\.bigmodel\.cn\/.*/,
/https:\/\/.*\.modelscope\.cn\/.*/,
/https:\/\/api\.siliconflow\.cn\/.*/,
]
for (const pattern of apiPatterns) {
@@ -391,6 +462,41 @@ class E2EVCR {
return
}
// 图像生成等非 SSE:直接按原始响应回放(避免强行合成 SSE 破坏语义)
const contentType = response.headers()['content-type'] || ''
const isImageResponse = /\bimage\//i.test(contentType)
const isSSE = /\btext\/event-stream\b/i.test(contentType)
if (!isSSE && (isImageResponse || /\/images\//i.test(url))) {
await this.saveFixture(
provider,
url,
JSON.parse(requestBody || '{}'),
null,
endTime - startTime,
responseBody,
{ 'content-type': contentType || 'application/octet-stream' },
method,
response.status()
)
const headers = { ...response.headers() }
delete (headers as any)['content-encoding']
delete (headers as any)['content-length']
delete (headers as any)['transfer-encoding']
await route.fulfill({
status: response.status(),
headers: {
...headers,
'access-control-allow-origin': '*',
'access-control-allow-headers': '*',
},
body: responseBody
})
return
}
const hasSSE = /(^|\n)\s*data:\s*/.test(responseBody)
let rawSSE = responseBody
@@ -506,6 +612,9 @@ class E2EVCR {
responseJson,
endTime - startTime,
rawSSE,
{
'content-type': hasSSE ? 'text/event-stream' : (response.headers()['content-type'] || 'application/json'),
},
method,
response.status()
)
@@ -540,17 +649,24 @@ class E2EVCR {
if (interaction) {
// 直接返回原始 SSE 文本(格式完全一致)
const contentType = interaction.responseHeaders?.['content-type'] || 'application/json'
const isSSE = /text\/event-stream/i.test(contentType)
await route.fulfill({
status: interaction.status || 200,
headers: {
'content-type': 'text/event-stream',
'cache-control': 'no-cache',
'connection': 'keep-alive',
'content-type': contentType,
...(isSSE
? {
'cache-control': 'no-cache',
'connection': 'keep-alive',
}
: {}),
// 关键:避免浏览器端 fetch 因 CORS 直接失败
'access-control-allow-origin': '*',
'access-control-allow-headers': '*',
},
body: interaction.rawSSE || ''
body: interaction.rawBody || ''
})
} else {
if (mode === 'replay') {
@@ -0,0 +1,115 @@
import { test, expect } from '../fixtures'
import { navigateToMode } from '../helpers/common'
import { fillOriginalPrompt, clickOptimizeButton, expectOptimizedResultNotEmpty } from '../helpers/optimize'
import * as path from 'path'
const MODE = 'image-image2image' as const
async function openSelectAndWaitForVisibleOptions(page: any, select: any) {
const visibleOptions = page.locator('.n-base-select-option:visible')
const ensureOpen = async () => {
await select.click()
await expect
.poll(async () => await visibleOptions.count(), { timeout: 20000 })
.toBeGreaterThan(0)
}
try {
await ensureOpen()
} catch {
await page.keyboard.press('Escape').catch(() => {})
await page.waitForTimeout(200)
await ensureOpen()
}
return visibleOptions
}
async function selectOption(page: any, select: any, matcher?: RegExp) {
const options = await openSelectAndWaitForVisibleOptions(page, select)
if (!matcher) {
await options.first().click()
return
}
const target = options.filter({ hasText: matcher }).first()
if ((await target.count()) > 0) {
await target.click()
return
}
// Fallback: pick the first visible option.
await options.first().click()
}
test.describe('Image Image2Image - 生成(SiliconFlow)', () => {
test('上传输入图并在对比模式下生成 original+optimized 两张图', async ({ page }) => {
test.setTimeout(900000)
await navigateToMode(page, 'image', 'image2image')
// 1) 打开上传弹窗并上传输入图
await page.getByTestId('image-image2image-open-upload').click()
const upload = page.getByTestId('image-image2image-upload')
const fileInput = upload.locator('input[type="file"]')
const seedPath = path.join(process.cwd(), 'tests/e2e/fixtures/images/text2image-output.png')
await fileInput.setInputFiles(seedPath)
// 等待缩略图出现,说明 session 已注入 inputImage
await expect(page.getByTestId('image-image2image-input-preview')).toBeVisible({ timeout: 30000 })
// 关闭 modal:不强依赖具体 DOM 结构,尽量退回到主界面继续
await page.keyboard.press('Escape').catch(() => {})
// 等待上传弹窗彻底关闭,避免残留遮罩层/动画拦截后续点击
await expect(page.getByTestId('image-image2image-upload-modal')).toBeHidden({ timeout: 20000 })
// 2) 选择文本模型(用于优化)
const textModelSelect = page.getByTestId('image-image2image-text-model-select')
await expect(textModelSelect).toBeVisible({ timeout: 20000 })
await selectOption(page, textModelSelect)
// 3) 选择优化模板(跳过)
// 这里不强依赖具体模板(模板列表可能变化,且 focus 会触发刷新导致下拉抖动),
// 只验证主流程:上传 → 优化 → 对比生成两张图。
// 4) 填写提示词(复用 helper:支持 textarea/CodeMirror,并在输入后等待 optimize-button 可用)
await fillOriginalPrompt(page, MODE, 'make it watercolor style')
// 5) 点击优化并等待优化输出非空
await clickOptimizeButton(page, MODE)
await expectOptimizedResultNotEmpty(page, MODE)
// 6) 选择图像模型:siliconflow
const modelSelect = page.getByTestId('image-image2image-image-model-select')
await expect(modelSelect).toBeVisible({ timeout: 20000 })
await selectOption(page, modelSelect, /siliconflow/i)
// 7) 打开对比模式
const compareToggle = page.getByTestId('image-image2image-generate-compare-toggle')
const ariaChecked = await compareToggle.getAttribute('aria-checked').catch(() => null)
if (ariaChecked !== 'true') {
await compareToggle.click()
}
// 8) 点击生成
await page.getByTestId('image-image2image-generate-button').click()
// 9) 断言两张结果图都非空
const originalImg = page.getByTestId('image-image2image-original-image').locator('img')
const optimizedImg = page.getByTestId('image-image2image-optimized-image').locator('img')
await expect
.poll(async () => (await originalImg.getAttribute('src')) || '', { timeout: 240000 })
.toMatch(/^data:image\/(png|jpeg);base64,|^https?:\/\//)
await expect
.poll(async () => (await optimizedImg.getAttribute('src')) || '', { timeout: 240000 })
.toMatch(/^data:image\/(png|jpeg);base64,|^https?:\/\//)
})
})
@@ -0,0 +1,100 @@
import { test, expect } from '../fixtures'
import { navigateToMode } from '../helpers/common'
import { fillOriginalPrompt, clickOptimizeButton, expectOptimizedResultNotEmpty } from '../helpers/optimize'
import * as fs from 'fs/promises'
import * as path from 'path'
const MODE = 'image-text2image' as const
function isBase64DataUrl(src: string) {
return /^data:image\/(png|jpeg);base64,/.test(src)
}
async function saveBase64DataUrlAsPng(dataUrl: string, outPath: string) {
const base64 = dataUrl.split(',')[1] || ''
const buf = Buffer.from(base64, 'base64')
await fs.mkdir(path.dirname(outPath), { recursive: true })
await fs.writeFile(outPath, buf)
}
test.describe('Image Text2Image - 生成(SiliconFlow)', () => {
test('切换到 SiliconFlow 图像模型并生成图片(对比模式)', async ({ page }) => {
// Record mode may be slow (two image generations); keep replay fast.
test.setTimeout(900000)
await navigateToMode(page, 'image', 'text2image')
// 1) 输入提示词并优化(左侧)
// 尽量保持 prompt 简短,避免生成的优化 prompt 过长导致图像模型失败/超时。
await fillOriginalPrompt(page, MODE, 'corgi, studio photo')
await clickOptimizeButton(page, MODE)
await expectOptimizedResultNotEmpty(page, MODE)
// 2) 选择图像模型(右侧):过滤 siliconflow,选择第一项
const modelSelect = page.getByTestId('image-text2image-image-model-select')
await expect(modelSelect).toBeVisible({ timeout: 20000 })
await modelSelect.click()
// Naive UI 下拉选项:通过菜单文本匹配 siliconflow(不依赖具体模型名)
const option = page.locator('.n-base-select-option').filter({ hasText: /siliconflow/i }).first()
await expect(option).toBeVisible({ timeout: 20000 })
await option.click()
// 3) 打开对比模式:生成 original + optimized 两张图
const compareToggle = page.getByTestId('image-text2image-generate-compare-toggle')
const ariaChecked = await compareToggle.getAttribute('aria-checked').catch(() => null)
if (ariaChecked !== 'true') {
await compareToggle.click()
}
// 4) 点击生成
await page.getByTestId('image-text2image-generate-button').click()
// 5) 断言两份生成结果都非空(至少 img src 有值)
const originalImg = page.getByTestId('image-text2image-original-image').locator('img')
const optimizedImg = page.getByTestId('image-text2image-optimized-image').locator('img')
let originalSrc = ''
await expect
.poll(async () => {
originalSrc = (await originalImg.getAttribute('src')) || ''
return originalSrc
}, { timeout: 240000 })
.toMatch(/^data:image\/(png|jpeg);base64,|^https?:\/\//)
let optimizedSrc = ''
await expect
.poll(async () => {
optimizedSrc = (await optimizedImg.getAttribute('src')) || ''
return optimizedSrc
}, { timeout: 240000 })
.toMatch(/^data:image\/(png|jpeg);base64,|^https?:\/\//)
// 在 record 模式下保存一张样例图供 image2image 上传复用。
// 如果是 base64,直接落盘;如果是 URL(siliconflow 默认返回 url),则通过 Playwright 下载落盘。
if (process.env.E2E_VCR_MODE === 'record') {
const outPath = path.join(process.cwd(), 'tests/e2e/fixtures/images/text2image-output.png')
if (isBase64DataUrl(optimizedSrc)) {
await saveBase64DataUrlAsPng(optimizedSrc, outPath)
} else if (optimizedSrc.startsWith('http')) {
const res = await page.request.get(optimizedSrc)
if (res.ok()) {
const buf = await res.body()
await fs.mkdir(path.dirname(outPath), { recursive: true })
await fs.writeFile(outPath, buf)
}
}
// If still missing, keep a url marker for debugging.
try {
await fs.access(outPath)
} catch {
if (optimizedSrc) {
await fs.mkdir(path.dirname(outPath), { recursive: true })
await fs.writeFile(outPath + '.url.txt', optimizedSrc, 'utf-8')
}
}
}
})
})