initialize();
use app\controller\api\Chat;
use app\model\AiModel;
use app\service\AgentCatalog;
use app\service\ComfyUIService;
$failures = [];
$checks = 0;
$assert = static function (bool $condition, string $name, string $detail = '') use (&$failures, &$checks): void {
$checks++;
if ($condition) {
printf("CHECK %02d PASS %s\n", $checks, $name);
return;
}
$failures[] = [$name, $detail];
printf("CHECK %02d FAIL %s%s\n", $checks, $name, $detail === '' ? '' : ' - ' . $detail);
};
$chatReflection = new ReflectionClass(Chat::class);
$chat = $chatReflection->newInstanceWithoutConstructor();
$resolveTurn = $chatReflection->getMethod('resolveAgentImageTurn');
$resolveTurn->setAccessible(true);
$buildAgentPrompt = $chatReflection->getMethod('buildAgentActionImagePrompt');
$buildAgentPrompt->setAccessible(true);
$shouldRecoverAction = $chatReflection->getMethod('shouldRecoverAllowedAgentImageAction');
$shouldRecoverAction->setAccessible(true);
$parseActionForTurn = $chatReflection->getMethod('parseAgentImageActionForTurn');
$parseActionForTurn->setAccessible(true);
$takeSafeStreamPrefix = $chatReflection->getMethod('takeSafeAgentStreamPrefix');
$takeSafeStreamPrefix->setAccessible(true);
$plainStream = 'Agent streaming text.';
$plainBuffer = $plainStream;
$plainArgs = [&$plainBuffer, false];
$plainPrefix = $takeSafeStreamPrefix->invokeArgs($chat, $plainArgs);
$assert($plainPrefix === $plainStream, 'Agent prose streams from its first chunk');
$assert($plainBuffer === '', 'Normal Agent prose has no fixed safety delay');
$plainFlushArgs = [&$plainBuffer, true];
$plainTail = $takeSafeStreamPrefix->invokeArgs($chat, $plainFlushArgs);
$assert($plainBuffer === '', 'Agent stream flush clears its safety tail');
$assert($plainPrefix . $plainTail === $plainStream, 'Agent prose stream preserves every character');
$actionBuffer = str_repeat('Visible explanation. ', 5)
. "```json\n{\"action\":\"generate_image\",\"prompt\":\"A cat\"}";
$actionArgs = [&$actionBuffer, false];
$visiblePrefix = $takeSafeStreamPrefix->invokeArgs($chat, $actionArgs);
$assert(!str_contains($visiblePrefix, 'generate_image'), 'Internal image action never reaches streamed prose');
$assert(str_contains($actionBuffer, 'generate_image'), 'Internal image action remains buffered for routing');
$splitActionBuffer = '{"';
$splitArgs = [&$splitActionBuffer, false];
$splitPrefix = $takeSafeStreamPrefix->invokeArgs($chat, $splitArgs);
$splitActionBuffer .= 'action":"generate_image","prompt":"A dog"}';
$splitArgs = [&$splitActionBuffer, false];
$splitSecondPrefix = $takeSafeStreamPrefix->invokeArgs($chat, $splitArgs);
$assert(
!str_contains($splitPrefix . $splitSecondPrefix, 'generate_image'),
'Image action split across chunks stays hidden'
);
$assert(str_contains($splitActionBuffer, 'generate_image'), 'Split image action is retained intact');
$normalJsonBuffer = '{"answer":"ordinary JSON"}';
$normalJsonArgs = [&$normalJsonBuffer, false];
$normalJsonPrefix = $takeSafeStreamPrefix->invokeArgs($chat, $normalJsonArgs);
$assert($normalJsonPrefix === '{"answer":"ordinary JSON"}', 'Ordinary JSON is not mistaken for an image action');
$assert($normalJsonBuffer === '', 'Ordinary JSON does not remain buffered');
$markdownActionBuffer = '```j';
$markdownFirstArgs = [&$markdownActionBuffer, false];
$markdownFirst = $takeSafeStreamPrefix->invokeArgs($chat, $markdownFirstArgs);
$markdownActionBuffer .= "son\n{\"action\":\"generate_image\",\"prompt\":\"A fox\"}";
$markdownSecondArgs = [&$markdownActionBuffer, false];
$markdownSecond = $takeSafeStreamPrefix->invokeArgs($chat, $markdownSecondArgs);
$assert($markdownFirst . $markdownSecond === '', 'Markdown image action split across chunks stays hidden');
$assert(str_contains($markdownActionBuffer, 'generate_image'), 'Markdown image action remains buffered for routing');
$image = [[
'type' => 'image',
'url' => '/api/uploads/source.png',
'name' => 'source.png',
'mime' => 'image/png',
]];
$editHistory = [[
'role' => 'user',
'content' => '把右下角水印去掉',
'attachments' => $image,
]];
$editTurn = $resolveTurn->invoke($chat, '把右下角水印去掉', $editHistory);
$assert(($editTurn['allowed'] ?? false) === true, '上传原图去水印允许图片动作');
$assert(($editTurn['revision'] ?? false) === true, '上传原图去水印标记为修改');
$assert(($editTurn['uploaded_edit'] ?? false) === true, '上传原图使用本轮附件');
$assert(($editTurn['independent'] ?? true) === false, '图片编辑不作为独立新图');
$residualHistory = [
$editHistory[0],
[
'role' => 'assistant',
'content' => '已去除水印:',
'attachments' => $image,
],
[
'role' => 'user',
'content' => '去除的不彻底',
'attachments' => [],
],
];
$residualTurn = $resolveTurn->invoke($chat, '去除的不彻底', $residualHistory);
$assert(($residualTurn['allowed'] ?? false) === true, '残留反馈允许继续执行图片处理');
$assert(($residualTurn['revision'] ?? false) === true, '残留反馈保持图片修改语义');
$authorTurn = $resolveTurn->invoke($chat, '把作者也给去除了', [
[
'role' => 'assistant',
'content' => '已去除水印:',
'attachments' => $image,
],
[
'role' => 'user',
'content' => '把作者也给去除了',
'attachments' => [],
],
]);
$assert(($authorTurn['allowed'] ?? false) === true, '连续作者删除允许图片动作');
$assert(($authorTurn['revision'] ?? false) === true, '连续作者删除保持上一张图片状态');
$enhanceTurn = $resolveTurn->invoke($chat, '变清晰', [
[
'role' => 'assistant',
'content' => '已去除水印:',
'attachments' => $image,
],
[
'role' => 'user',
'content' => '变清晰',
'attachments' => [],
],
]);
$assert(($enhanceTurn['allowed'] ?? false) === true, '短句变清晰允许图片增强');
$assert(($enhanceTurn['revision'] ?? false) === true, '短句变清晰继承当前图片');
$interruptedEnhanceTurn = $resolveTurn->invoke($chat, '可以帮我把图片变清晰吗', [
[
'role' => 'assistant',
'content' => '已去除水印:',
'attachments' => $image,
],
[
'role' => 'user',
'content' => '变清晰',
'attachments' => [],
],
[
'role' => 'assistant',
'content' => '图片动作暂未执行。',
'attachments' => [],
],
[
'role' => 'user',
'content' => '可以帮我把图片变清晰吗',
'attachments' => [],
],
]);
$assert(($interruptedEnhanceTurn['allowed'] ?? false) === true, '失败说明后仍保留最近有效图片');
$assert(($interruptedEnhanceTurn['revision'] ?? false) === true, '中断后的清晰化仍作为连续编辑');
$interruptedAuthorTurn = $resolveTurn->invoke($chat, '那把图片上的作者去除了', [
[
'role' => 'assistant',
'content' => '已去除水印:',
'attachments' => $image,
],
[
'role' => 'user',
'content' => '变清晰',
'attachments' => [],
],
[
'role' => 'assistant',
'content' => '图片动作暂未执行。',
'attachments' => [],
],
[
'role' => 'user',
'content' => '可以帮我把图片变清晰吗',
'attachments' => [],
],
[
'role' => 'assistant',
'content' => '这里是被说明文字包裹的图片动作。',
'attachments' => [],
],
[
'role' => 'user',
'content' => '那把图片上的作者去除了',
'attachments' => [],
],
]);
$assert(($interruptedAuthorTurn['allowed'] ?? false) === true, '多次失败后去作者仍允许图片动作');
$assert(($interruptedAuthorTurn['revision'] ?? false) === true, '多次失败后去作者仍继承最近图片');
$questionHistory = [[
'role' => 'user',
'content' => '这张图里是谁',
'attachments' => $image,
]];
$questionTurn = $resolveTurn->invoke($chat, '这张图里是谁', $questionHistory);
$assert(($questionTurn['allowed'] ?? true) === false, '图片问答不误触发编辑');
$assert(AgentCatalog::requestsImageEditing('换成夜晚背景'), '换背景识别为图片编辑');
$assert(AgentCatalog::requestsImageEditing('修复这张老照片'), '照片修复识别为图片编辑');
$assert(!AgentCatalog::requestsImageEditing('分析一下这张照片'), '图片分析保持文字任务');
$assert(AgentCatalog::imageEditMode('把水印擦掉') === 'inpaint', '局部移除选择 inpaint');
$assert(AgentCatalog::imageEditMode('改成油画风格') === 'img2img', '整体风格转换选择 img2img');
$assert(AgentCatalog::requestsWatermarkRemoval('帮我去除下水印'), '自然表达识别为去水印任务');
$assert(AgentCatalog::requestsWatermarkRemoval('去水印'), '最短口语去水印进入专用处理');
$assert(AgentCatalog::requestsImageEditing('去除的不彻底'), '省略式残留反馈识别为图片编辑');
$assert(AgentCatalog::requestsWatermarkRemoval('去除的不彻底'), '省略式残留反馈延续去水印任务');
$assert(AgentCatalog::imageEditMode('去除的不彻底') === 'inpaint', '残留反馈继续使用局部处理');
$assert(AgentCatalog::requestsImageEditing('把作者也给去除了'), '连续作者删除识别为图片编辑');
$assert(AgentCatalog::requestsImageEditing('变清晰'), '最短清晰化口语识别为图片编辑');
$assert(AgentCatalog::requestsImageEditing('可以帮我吧图片变清晰吗'), '自然清晰化问法识别为图片编辑');
$assert(!AgentCatalog::requestsImageEditing('怎么让图片变清晰'), '清晰化教程问题保持文字任务');
$assert(AgentCatalog::imageTextRemovalTarget('把作者也给去除了') === 'author', '连续操作提取作者删除目标');
$assert(AgentCatalog::imageTextRemovalTarget('标题也去掉') === 'title', '连续操作提取标题删除目标');
$assert(!AgentCatalog::requestsWatermarkRemoval('给图片加个水印'), '添加水印不会误判为移除');
$wrappedAction = '我会为你处理这张图片,下面是动作:' . str_repeat('说明文字', 90)
. '{"action":"generate_image","prompt":"Remove only the lower-right watermark."}';
$parsedWrappedAction = AgentCatalog::parseImageAction($wrappedAction);
$assert($parsedWrappedAction === null, '长解释包裹动作仍被严格解析器拒绝');
$routedWrappedAction = AgentCatalog::parseRoutedImageAction($wrappedAction);
$assert(($routedWrappedAction['action'] ?? '') === 'generate_image', '路由确认后恢复说明文字中的动作');
$assert(
($parseActionForTurn->invoke($chat, $wrappedAction, true)['action'] ?? '') === 'generate_image',
'控制器仅在允许图片操作时恢复包裹动作'
);
$assert(
$parseActionForTurn->invoke($chat, $wrappedAction, false) === null,
'普通问答不会执行说明文字中的图片动作'
);
$assert(
$shouldRecoverAction->invoke($chat, true, $parsedWrappedAction) === true,
'已确认图片路由会恢复不规范动作'
);
$assert(
$shouldRecoverAction->invoke($chat, false, $parsedWrappedAction) === false,
'普通文字路由不会恢复图片动作'
);
$editPrompt = $buildAgentPrompt->invoke(
$chat,
1,
'Remove only the lower-right watermark from the uploaded book cover.',
true
);
$assert(str_contains($editPrompt, 'Preserve all existing text'), '编辑提示保留原图文字与排版');
$assert(!str_contains($editPrompt, 'No readable text'), '编辑提示不再全局禁止文字');
$generationPrompt = $buildAgentPrompt->invoke($chat, 1, 'A clean fantasy landscape.', false);
$assert(str_contains($generationPrompt, 'No readable text'), '新图生成继续默认禁止文字');
$prepareEditPrompt = $chatReflection->getMethod('prepareComfyEditPrompt');
$prepareEditPrompt->setAccessible(true);
$sanitizedWatermarkPrompt = $prepareEditPrompt->invoke(
$chat,
'The lower-right corner contains a Baidu AI watermark.',
['operation' => 'remove_watermark']
);
$assert(!str_contains($sanitizedWatermarkPrompt, 'Baidu'), '去水印任务不会复述并重建原水印');
$assert(str_contains($sanitizedWatermarkPrompt, 'Do not recreate'), '去水印提示明确禁止重建水印');
$fixturePath = sys_get_temp_dir() . DIRECTORY_SEPARATOR
. 'watermark_mask_fixture_' . bin2hex(random_bytes(6)) . '.png';
$fixture = imagecreatetruecolor(200, 300);
$fixtureDark = imagecolorallocate($fixture, 20, 24, 30);
$fixtureLight = imagecolorallocate($fixture, 220, 220, 220);
imagefill($fixture, 0, 0, $fixtureDark);
imagefilledrectangle($fixture, 150, 280, 195, 294, $fixtureLight);
imagepng($fixture, $fixturePath);
imagedestroy($fixture);
$autoMaskPath = ComfyUIService::createAutomaticWatermarkMask($fixturePath, '帮我去掉水印');
$autoMask = imagecreatefrompng($autoMaskPath);
$assert((imagecolorat($autoMask, 190, 290) & 0xFF) === 255, '无位置去水印自动覆盖右下角');
$assert((imagecolorat($autoMask, 10, 10) & 0xFF) === 0, '自动遮罩不修改画面主体');
imagedestroy($autoMask);
$topLeftMaskPath = ComfyUIService::createAutomaticWatermarkMask($fixturePath, '去掉左上角水印');
$topLeftMask = imagecreatefrompng($topLeftMaskPath);
$assert((imagecolorat($topLeftMask, 10, 10) & 0xFF) === 255, '明确位置时遮罩严格覆盖左上角');
$assert((imagecolorat($topLeftMask, 190, 290) & 0xFF) === 0, '明确位置不会误改右下角');
imagedestroy($topLeftMask);
$fillServiceReflection = new ReflectionClass(ComfyUIService::class);
$fillMaskedPixels = $fillServiceReflection->getMethod('fillMaskedPixelsFromBoundary');
$fillMaskedPixels->setAccessible(true);
$fillFixture = imagecreatetruecolor(40, 40);
$fillMask = imagecreatetruecolor(40, 40);
$fillBase = imagecolorallocate($fillFixture, 28, 38, 52);
$maskBlack = imagecolorallocate($fillMask, 0, 0, 0);
$maskWhite = imagecolorallocate($fillMask, 255, 255, 255);
imagefill($fillFixture, 0, 0, $fillBase);
imagefill($fillMask, 0, 0, $maskBlack);
imagefilledrectangle($fillMask, 14, 15, 26, 24, $maskWhite);
$fillMaskedPixels->invoke(null, $fillFixture, $fillMask, 40, 40);
$filledCenter = imagecolorat($fillFixture, 20, 20);
$assert((($filledCenter >> 16) & 0xFF) < 80, '确定性填充不会生成高亮伪文字');
$assert(($filledCenter & 0xFF) > 20, '确定性填充延续周围背景颜色');
imagedestroy($fillFixture);
imagedestroy($fillMask);
$authorFixturePath = sys_get_temp_dir() . DIRECTORY_SEPARATOR
. 'author_mask_fixture_' . bin2hex(random_bytes(6)) . '.png';
$authorFixture = imagecreatetruecolor(240, 320);
for ($y = 0; $y < 320; $y++) {
for ($x = 0; $x < 240; $x++) {
$red = 24 + (int) round($x * 0.16);
$green = 42 + (int) round($x * 0.12);
$blue = 70 + (int) round($x * 0.1);
imagesetpixel($authorFixture, $x, $y, ($red << 16) | ($green << 8) | $blue);
}
}
$authorGold = imagecolorallocate($authorFixture, 230, 185, 92);
imagefilledrectangle($authorFixture, 94, 112, 146, 122, $authorGold);
imagepng($authorFixture, $authorFixturePath);
imagedestroy($authorFixture);
$authorMaskPath = ComfyUIService::createAutomaticTextRemovalMask($authorFixturePath, 'author');
$authorMask = imagecreatefrompng($authorMaskPath);
$assert((imagecolorat($authorMask, 120, 117) & 0xFF) === 255, 'author mask detects the author credit');
$assert((imagecolorat($authorMask, 120, 75) & 0xFF) === 0, 'author mask does not touch the title band');
$authorResult = imagecreatefrompng($authorFixturePath);
$authorOriginalFar = imagecolorat($authorResult, 20, 250);
$fillTextBand = $fillServiceReflection->getMethod('fillTextBandFromHorizontalSurroundings');
$fillTextBand->setAccessible(true);
$fillTextBand->invoke(null, $authorResult, $authorMask, 240, 320);
$authorCenter = imagecolorat($authorResult, 120, 117);
$assert((($authorCenter >> 16) & 0xFF) < 120, 'author fill removes bright text and its shadow band');
$assert(imagecolorat($authorResult, 20, 250) === $authorOriginalFar, 'author fill preserves pixels outside the local band');
imagedestroy($authorResult);
imagedestroy($authorMask);
@unlink($authorMaskPath);
@unlink($authorFixturePath);
$maskPixelValue = $fillServiceReflection->getMethod('maskPixelValue');
$maskPixelValue->setAccessible(true);
$paletteMask = imagecreate(5, 5);
$paletteBlack = imagecolorallocate($paletteMask, 0, 0, 0);
$paletteWhite = imagecolorallocate($paletteMask, 255, 255, 255);
imagefill($paletteMask, 0, 0, $paletteBlack);
imagesetpixel($paletteMask, 2, 2, $paletteWhite);
$assert($maskPixelValue->invoke(null, $paletteMask, 2, 2) === 255, '调色板遮罩正确读取白色修改区');
$assert($maskPixelValue->invoke(null, $paletteMask, 0, 0) === 0, '调色板遮罩正确读取黑色保护区');
imagedestroy($paletteMask);
@unlink($autoMaskPath);
@unlink($topLeftMaskPath);
@unlink($fixturePath);
$serviceReflection = new ReflectionClass(ComfyUIService::class);
$enrichImagePrompt = $serviceReflection->getMethod('enrichUserPromptForImage');
$enrichImagePrompt->setAccessible(true);
$standaloneImagePrompt = $enrichImagePrompt->invoke(null, '梦幻森林游戏概念原画');
$assert(str_contains($standaloneImagePrompt, 'standalone full-bleed finished image'), '游戏素材提示强制输出单张满幅成品');
$assert(str_contains($standaloneImagePrompt, 'Never visualize, quote, label, or print'), '游戏素材提示禁止把用户描述画进图片');
$requestedImageCount = $chatReflection->getMethod('requestedImageCount');
$requestedImageCount->setAccessible(true);
$assert($requestedImageCount->invoke($chat, '制作小说封面') === 1, '未指定数量时默认只生成一张图');
$assert($requestedImageCount->invoke($chat, '请生成四张不同构图的小说封面') === 4, '中文数量可触发显式四图生成');
$assert($requestedImageCount->invoke($chat, '生成 2 个不同版本,比例 16:9') === 2, '阿拉伯数字数量不会被宽高比干扰');
$assert($requestedImageCount->invoke($chat, '给我十张方案') === 4, '显式批量数量按工作流上限收敛到四张');
$applyVisibleText = $chatReflection->getMethod('applyRequestedVisibleText');
$applyVisibleText->setAccessible(true);
$exactTitlePrompt = $applyVisibleText->invoke(
$chat,
"A Chinese fantasy novel cover titled 'Divine Luo'. A golden deity above cloud mountains. No text, no letters, no Chinese characters, no title, no caption, no watermark.",
'制作小说封面:神话大罗,我直通天地'
);
$assert(str_contains($exactTitlePrompt, 'EXACT_VISIBLE_TEXT: <<<神话大罗,我直通天地>>>'), '明确封面标题保留原始中文逐字标记');
$assert(!str_contains($exactTitlePrompt, "titled 'Divine Luo'"), '明确中文标题移除 Agent 擅自翻译的英文标题');
$assert(!preg_match('/no\s+(?:readable\s+)?text/iu', $exactTitlePrompt), '明确标题任务移除冲突的全局禁字限制');
$exactAgentPrompt = AgentCatalog::buildImagePrompt(AgentCatalog::find('auto'), $exactTitlePrompt);
$assert(!str_contains($exactAgentPrompt, 'no readable text'), 'Agent 画面方向不会向明确标题重新注入禁字规则');
$applyImageBatchSize = $serviceReflection->getMethod('applyImageBatchSize');
$applyImageBatchSize->setAccessible(true);
$batchWorkflow = [
'latent' => ['class_type' => 'EmptyLatentImage', 'inputs' => ['batch_size' => 1]],
'other' => ['class_type' => 'KSampler', 'inputs' => ['steps' => 20]],
];
$applyImageBatchSize->invokeArgs(null, [&$batchWorkflow, 4]);
$assert(($batchWorkflow['latent']['inputs']['batch_size'] ?? null) === 4, '四图生成将 latent 批次数设为 4');
$assert(!array_key_exists('batch_size', $batchWorkflow['other']['inputs']), '四图生成不污染无批次字段的节点');
$applyImageBatchSize->invokeArgs(null, [&$batchWorkflow, 99]);
$assert(($batchWorkflow['latent']['inputs']['batch_size'] ?? null) === 4, '四图生成批次数上限固定为 4');
$optimizeSampling = $serviceReflection->getMethod('optimizeTextToImageSampling');
$optimizeSampling->setAccessible(true);
$legacyTurboWorkflow = [
'loader' => ['class_type' => 'UNETLoaderMultiGPU', 'inputs' => ['unet_name' => 'z_image_turbo_bf16.safetensors']],
'sampler' => ['class_type' => 'KSampler', 'inputs' => ['steps' => 20, 'sampler_name' => 'euler', 'scheduler' => 'simple']],
'sampling' => ['class_type' => 'ModelSamplingAuraFlow', 'inputs' => ['shift' => 50]],
];
$optimizeSampling->invokeArgs(null, [&$legacyTurboWorkflow, []]);
$assert(($legacyTurboWorkflow['sampler']['inputs']['steps'] ?? null) === 8, '旧版 Turbo 工作流自动降到八步采样');
$assert(($legacyTurboWorkflow['sampler']['inputs']['sampler_name'] ?? null) === 'res_multistep', 'Z-Image Turbo 自动使用匹配的采样器');
$assert(abs((float) ($legacyTurboWorkflow['sampling']['inputs']['shift'] ?? 0) - 5.0) < 0.0001, 'Z-Image Turbo 修正过高的 AuraFlow shift');
$promptSystem = $serviceReflection->getReflectionConstant('PROMPT_ENGINEER_SYSTEM')->getValue();
$assert(str_contains($promptSystem, 'title-safe negative space'), '小说封面提示保留后期排字安全区但不生成伪文字');
$assert(str_starts_with($promptSystem, '/no_think'), '提示词扩写显式关闭慢速思考链');
$assert(str_contains($promptSystem, 'under 80 English words'), '提示词扩写限制冗余长度');
$buildEditWorkflow = $serviceReflection->getMethod('buildEditWorkflow');
$buildEditWorkflow->setAccessible(true);
$model = new AiModel();
$model->extra_config = [];
$model->model_id = '1:1 (Square)';
$buildWorkflow = $serviceReflection->getMethod('buildWorkflow');
$buildWorkflow->setAccessible(true);
$textToImageWorkflow = $buildWorkflow->invoke(null, '梦幻森林游戏场景素材', $model, 4);
$cleanupNodes = array_filter(
$textToImageWorkflow,
fn (array $node): bool => ($node['class_type'] ?? '') === 'RegexReplace'
&& ($node['_meta']['title'] ?? '') === 'Strip prompt reasoning'
);
$assert(count($cleanupNodes) === 1, '生图工作流在提示编码前插入思考内容清理节点');
$cleanupNodeId = (string) array_key_first($cleanupNodes);
$linkedToCleanup = false;
foreach ($textToImageWorkflow as $node) {
if (($node['class_type'] ?? '') === 'CLIPTextEncode'
&& (($node['inputs']['text'][0] ?? '') === $cleanupNodeId)) {
$linkedToCleanup = true;
break;
}
}
$assert($linkedToCleanup, '生图正向提示编码器只接收清理后的最终提示词');
$cleanupPattern = (string) ($cleanupNodes[$cleanupNodeId]['inputs']['regex_pattern'] ?? '');
$assert(
preg_replace('/' . str_replace('/', '\\/', $cleanupPattern) . '/is', '', '用户原始描述final scene') === 'final scene',
'提示清理节点会移除包含用户原文的 think 区块'
);
$assert(
preg_replace('/' . str_replace('/', '\\/', $cleanupPattern) . '/is', '', '未闭合的思考内容') === '',
'提示清理节点不会把未闭合思考链送进图像编码器'
);
$coverWorkflow = $buildWorkflow->invoke(null, '制作小说封面', $model, 1);
$coverAspect = null;
$coverSampler = null;
$coverTextGenerator = null;
foreach ($coverWorkflow as $node) {
$type = $node['class_type'] ?? '';
if ($type === 'ResolutionSelector') {
$coverAspect = $node['inputs']['aspect_ratio'] ?? null;
} elseif ($type === 'KSampler') {
$coverSampler = $node['inputs'];
} elseif ($type === 'TextGenerate') {
$coverTextGenerator = $node['inputs'];
}
}
$assert($coverAspect === '2:3 (Portrait Photo)', '小说封面自动采用竖版构图比例');
$assert(($coverTextGenerator['max_length'] ?? 0) === 128, '提示词扩写限制长度以减少无效等待');
$assert(abs((float) ($coverTextGenerator['sampling_mode.temperature'] ?? 1) - 0.25) < 0.0001, '提示词扩写使用低温度提高指令稳定性');
$exactTextWorkflow = $buildWorkflow->invoke(null, $exactAgentPrompt, $model, 1);
$exactPositive = '';
$exactNegative = '';
$exactRefine = null;
foreach ($exactTextWorkflow as $node) {
if (($node['class_type'] ?? '') === 'PrimitiveBoolean') {
$exactRefine = $node['inputs']['value'] ?? null;
}
if (($node['class_type'] ?? '') !== 'CLIPTextEncode' || !is_string($node['inputs']['text'] ?? null)) {
continue;
}
$title = (string) ($node['_meta']['title'] ?? '');
if (preg_match('/negative|负面|负向/i', $title)) {
$exactNegative .= ' ' . $node['inputs']['text'];
} else {
$exactPositive .= ' ' . $node['inputs']['text'];
}
}
$assert($exactRefine === false, '明确标题跳过可能改写原文的二次提示词扩写');
$assert(str_contains($exactPositive, '神话大罗,我直通天地'), '正向编码器收到完整准确中文标题');
$assert(str_contains($exactPositive, 'front-facing 2D fantasy key art'), '封面标题任务强制输出平面正封面而非书籍样机');
$assert(!str_contains($exactPositive, 'misspelled title'), '负面提示词不会再泄漏进正向编码器');
$exactTextNegativeBaseline = $serviceReflection->getReflectionConstant('EXACT_TEXT_NEGATIVE')->getValue();
$assert(str_contains($exactTextNegativeBaseline, 'misspelled title'), '明确标题使用专用错字负面提示');
$assert(str_contains($exactTextNegativeBaseline, 'book mockup'), '明确标题负面提示禁止书脊和立体样机产生额外文字');
$img2img = $buildEditWorkflow->invoke(
null,
'make the source image warmer',
$model,
'chat_edits/source.png',
null,
'img2img'
);
$img2imgTypes = array_column($img2img, 'class_type');
$assert(in_array('LoadImage', $img2imgTypes, true), 'img2img 包含 LoadImage');
$assert(in_array('VAEEncode', $img2imgTypes, true), 'img2img 包含 VAEEncode');
$imgSampler = null;
foreach ($img2img as $node) {
if (($node['class_type'] ?? '') === 'KSampler') {
$imgSampler = $node;
break;
}
}
$imgLatentNode = (string) ($imgSampler['inputs']['latent_image'][0] ?? '');
$assert(
$imgLatentNode !== '' && ($img2img[$imgLatentNode]['class_type'] ?? '') === 'VAEEncode',
'img2img 采样 latent 连接原图编码'
);
$assert(abs((float) ($imgSampler['inputs']['denoise'] ?? 0) - 0.45) < 0.0001, 'img2img 默认 denoise 为 0.45');
$img2imgStrings = [];
foreach ($img2img as $node) {
foreach (($node['inputs'] ?? []) as $value) {
if (is_string($value)) {
$img2imgStrings[] = $value;
}
}
}
$img2imgText = implode("\n", $img2imgStrings);
$assert(str_contains($img2imgText, 'Preserve all existing text'), 'img2img 工作流要求保留现有文字');
$editNegative = $serviceReflection->getReflectionConstant('EDIT_DEFAULT_NEGATIVE')->getValue();
$assert(str_contains($editNegative, 'altered existing typography'), '图片编辑负面提示保护原排版');
$buildDirectEditPrompt = $serviceReflection->getMethod('buildDirectEnglishEditPrompt');
$buildDirectEditPrompt->setAccessible(true);
$directEditPrompt = $buildDirectEditPrompt->invoke(
null,
'remove only the lower-right watermark',
'img2img'
);
$assert(str_contains($directEditPrompt, 'Preserve all existing text'), '无扩写节点时仍保留现有文字');
$inpaint = $buildEditWorkflow->invoke(
null,
'remove the marked watermark and restore the background',
$model,
'chat_edits/source.png',
'chat_edits/mask.png',
'inpaint'
);
$inpaintTypes = array_column($inpaint, 'class_type');
$assert(in_array('LoadImageMask', $inpaintTypes, true), 'inpaint 包含 LoadImageMask');
$assert(in_array('VAEEncodeForInpaint', $inpaintTypes, true), 'inpaint 包含 VAEEncodeForInpaint');
$inpaintSampler = null;
foreach ($inpaint as $node) {
if (($node['class_type'] ?? '') === 'KSampler') {
$inpaintSampler = $node;
break;
}
}
$inpaintLatentNode = (string) ($inpaintSampler['inputs']['latent_image'][0] ?? '');
$assert(
$inpaintLatentNode !== ''
&& ($inpaint[$inpaintLatentNode]['class_type'] ?? '') === 'VAEEncodeForInpaint',
'inpaint 采样 latent 连接局部重绘编码'
);
$assert(abs((float) ($inpaintSampler['inputs']['denoise'] ?? 0) - 0.72) < 0.0001, 'inpaint 默认 denoise 为 0.72');
$watermarkInpaint = $buildEditWorkflow->invoke(
null,
'remove the masked watermark',
$model,
'chat_edits/source.png',
'chat_edits/mask.png',
'inpaint',
0.58
);
$watermarkSampler = null;
foreach ($watermarkInpaint as $node) {
if (($node['class_type'] ?? '') === 'KSampler') {
$watermarkSampler = $node;
break;
}
}
$assert(
abs((float) ($watermarkSampler['inputs']['denoise'] ?? 0) - 0.58) < 0.0001,
'自动去水印使用受控局部重绘强度'
);
$prepareEditPrompt = $chatReflection->getMethod('prepareComfyEditPrompt');
$prepareEditPrompt->setAccessible(true);
$outpaintPrompt = $prepareEditPrompt->invoke(
$chat,
'扩展为 4:3',
['operation' => 'outpaint']
);
$assert(str_contains($outpaintPrompt, 'OUTPAINT_FULL_BLEED'), '扩图任务使用专用满幅补全提示');
$assert(!str_contains(strtolower($outpaintPrompt), 'mirror'), '扩图正向提示不再用禁用词反向激活镜像内容');
$assert(str_contains($outpaintPrompt, 'full-bleed'), '扩图提示要求无缝满幅画面');
$sceneAwareOutpaintPrompt = $prepareEditPrompt->invoke(
$chat,
'扩展为 4:3',
[
'operation' => 'outpaint',
'outpaint_scene_prompt' => 'A blue cosmic fantasy landscape with golden energy and floating rocks.',
]
);
$assert(
str_contains($sceneAwareOutpaintPrompt, 'blue cosmic fantasy landscape'),
'扩图提示注入原图视觉场景,避免生成无关环境'
);
$outpaintInpaint = $buildEditWorkflow->invoke(
null,
'OUTPAINT_FULL_BLEED: extend the scene',
$model,
'chat_edits/outpaint-source.png',
'chat_edits/outpaint-mask.png',
'inpaint',
1.0,
'outpaint',
135791113
);
$outpaintSampler = null;
$outpaintModel = null;
$outpaintFill = null;
$outpaintBlur = null;
$outpaintPatch = null;
$outpaintConditioning = null;
$outpaintComposite = null;
$outpaintCompositeMask = null;
foreach ($outpaintInpaint as $node) {
if (($node['class_type'] ?? '') === 'KSampler') {
$outpaintSampler = $node;
}
if (($node['class_type'] ?? '') === 'CheckpointLoaderSimple') $outpaintModel = $node;
if (($node['class_type'] ?? '') === 'INPAINT_MaskedFill') $outpaintFill = $node;
if (($node['class_type'] ?? '') === 'INPAINT_MaskedBlur') $outpaintBlur = $node;
if (($node['class_type'] ?? '') === 'INPAINT_ApplyFooocusInpaint') $outpaintPatch = $node;
if (($node['class_type'] ?? '') === 'INPAINT_VAEEncodeInpaintConditioning') $outpaintConditioning = $node;
if (($node['class_type'] ?? '') === 'ImageCompositeMasked') {
$outpaintComposite = $node;
$outpaintCompositeMask = $node['inputs']['mask'] ?? null;
}
}
$assert(
abs((float) ($outpaintSampler['inputs']['denoise'] ?? 0) - 1.0) < 0.0001,
'扩图边框完全重绘避免镜像残影'
);
$assert(
((int) ($outpaintSampler['inputs']['seed'] ?? 0)) === 135791113,
'扩图使用原图派生种子,不再让所有图片共用单一种子'
);
$assert(
($outpaintModel['inputs']['ckpt_name'] ?? '') === 'juggernautXL_version6Rundiffusion.safetensors',
'扩图使用 Fooocus 兼容的非蒸馏 SDXL 模型'
);
$assert(($outpaintFill['inputs']['fill'] ?? '') === 'navier-stokes', '扩图区使用边界传播预填而非镜像像素');
$assert(((int) ($outpaintBlur['inputs']['blur'] ?? 0)) >= 65, '扩图区在采样前生成低频颜色引导');
$assert($outpaintPatch !== null, '扩图采样器应用 Fooocus inpaint patch');
$assert(isset($outpaintConditioning['inputs']['pixels'], $outpaintConditioning['inputs']['mask']), '扩图条件编码器接收预处理画布和蒙版');
$assert(isset($outpaintComposite['inputs']['mask']), '扩图输出恢复受保护的原图中心');
$compositeDestination = $outpaintComposite['inputs']['destination'] ?? null;
$compositeBase = is_array($compositeDestination)
? ($outpaintInpaint[(string) ($compositeDestination[0] ?? '')] ?? null)
: null;
$assert(
($compositeBase['class_type'] ?? '') === 'LoadImage',
'扩图最终合成以原图为中心保护底图'
);
$finalMask = is_array($outpaintCompositeMask)
? ($outpaintInpaint[(string) ($outpaintCompositeMask[0] ?? '')] ?? null)
: null;
$assert(
($finalMask['class_type'] ?? '') === 'LoadImageMask',
'扩图最终合成直接使用单向羽化遮罩,避免二次模糊暴露灰底阴影'
);
$outpaintNegative = '';
$outpaintPositive = '';
foreach ($outpaintInpaint as $nodeId => $node) {
if (($node['class_type'] ?? '') !== 'CLIPTextEncode') {
continue;
}
$text = (string) ($node['inputs']['text'] ?? '');
if (str_contains($text, 'mirrored content') || str_contains($text, 'tiled image')) {
$outpaintNegative = $text;
} elseif (str_contains($text, 'OUTPAINT_FULL_BLEED')) {
$outpaintPositive = $text;
}
}
$assert($outpaintNegative !== '', '扩图负面提示禁止镜像、平铺和复制');
$assert(
!preg_match('/\b(?:plant|room|furniture|mockup|mirror|poster)\b/i', $outpaintPositive),
'扩图正向 CLIP 提示不含会反向激活无关场景的禁用名词'
);
$assert(
AgentCatalog::requestsBackgroundRemoval('抠出主要人物,保留发丝细节,背景透明'),
'抠图指令识别为背景移除任务'
);
$assert(
!AgentCatalog::requestsBackgroundRemoval('请介绍一下怎么做抠图'),
'抠图教程问题保持文字任务'
);
$assert(
!AgentCatalog::requestsBackgroundRemoval('去除图片中的水印并用相邻背景自然补全,只修改水印区域'),
'去水印默认提示不会误判为抠图'
);
$assert(
!AgentCatalog::requestsBackgroundRemoval('去掉右下角水印并自然补全背景'),
'去水印建议文案不会误判为抠图'
);
$assert(
AgentCatalog::requestsBackgroundRemoval('去掉图片背景'),
'明确去背景指令仍识别为抠图'
);
$assert(
AgentCatalog::requestsBackgroundRemoval('去除背景'),
'最短去背景口语仍识别为抠图'
);
$workbenchWatermarkPrompt = "去除图片中的水印并自然修复该区域纹理,只修改水印区域,保留人物、标题和其他内容。\n右下角水印去除";
$watermarkFlags = AgentCatalog::resolveImageEditFlags($workbenchWatermarkPrompt, 'watermark');
$assert(($watermarkFlags['is_watermark_removal'] ?? false) === true, '工作台去水印工具强制走去水印');
$assert(($watermarkFlags['is_background_removal'] ?? true) === false, '工作台去水印工具不会被内容误判为抠图');
$legacyWatermarkPrompt = "去除图片中的水印并用相邻背景自然补全,只修改水印区域,保留人物、标题和其他内容。\n右下角水印去除";
$legacyFlags = AgentCatalog::resolveImageEditFlags($legacyWatermarkPrompt, 'watermark');
$assert(($legacyFlags['is_watermark_removal'] ?? false) === true, '旧版去水印默认提示在 image_tool=watermark 时仍走去水印');
$assert(($legacyFlags['is_background_removal'] ?? true) === false, '旧版去水印默认提示在 image_tool=watermark 时不会抠图');
$chatWatermarkFlags = AgentCatalog::resolveImageEditFlags('去掉右下角水印并自然补全背景', '');
$assert(($chatWatermarkFlags['is_watermark_removal'] ?? false) === true, '对话去水印优先识别为去水印');
$assert(($chatWatermarkFlags['is_background_removal'] ?? true) === false, '对话去水印不会因补全背景误判为抠图');
$cutoutFlags = AgentCatalog::resolveImageEditFlags('任意无关文案', 'cutout');
$assert(($cutoutFlags['is_background_removal'] ?? false) === true, '工作台抠图工具强制走背景移除');
$assert(($cutoutFlags['is_watermark_removal'] ?? true) === false, '工作台抠图工具不会被误判为去水印');
$eraseFlags = AgentCatalog::resolveImageEditFlags('去掉涂抹区域的水印', 'erase');
$assert(($eraseFlags['is_background_removal'] ?? true) === false, '消除工具不会误判为抠图');
$assert(($eraseFlags['is_watermark_removal'] ?? false) === true, '消除工具提到水印时仍可走去水印强度');
$buildBackgroundRemovalWorkflow = $serviceReflection->getMethod('buildBackgroundRemovalWorkflow');
$buildBackgroundRemovalWorkflow->setAccessible(true);
$cutoutWorkflow = $buildBackgroundRemovalWorkflow->invoke(null, 'chat_edits/cutout-source.png');
$cutoutTypes = array_column($cutoutWorkflow, 'class_type');
$assert(in_array('easy imageRemBg', $cutoutTypes, true), '抠图工作流使用本地 RMBG 节点');
$cutoutNode = null;
foreach ($cutoutWorkflow as $node) {
if (($node['class_type'] ?? '') === 'easy imageRemBg') {
$cutoutNode = $node;
break;
}
}
$assert(($cutoutNode['inputs']['add_background'] ?? '') === 'none', '抠图工作流保留透明背景');
$assert(($cutoutNode['inputs']['image_output'] ?? '') === 'Save', '抠图工作流输出透明 PNG');
printf("RESULT %d checks; %d failed\n", $checks, count($failures));
exit($failures === [] ? 0 : 1);