Files
chat/backend/app/service/AgentCatalog.php
T
2026-07-22 10:18:59 +08:00

910 lines
48 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace app\service;
class AgentCatalog
{
public static function all(): array
{
return [
[
'id' => 'auto',
'name' => 'Agent',
'short_name' => 'Agent',
'description' => '自动识别任务并调用合适的语言或图片生成模型',
'icon' => 'robot',
'accent' => 'blue',
'placeholder' => '描述你要完成的任务,Agent 会自动判断并执行',
'supports_image' => true,
'image_prompt' => 'High-quality purposeful image, coherent composition, clear visual hierarchy, refined lighting and color, faithful to the user request, professional art direction, no readable text, no letters, no Chinese characters, no title, no caption, no watermark.',
'system_prompt' => '你是通用自主 Agent。先在内部识别用户的真实目标与任务类型,再选择合适的方法直接完成:规划类任务梳理目标、约束、依赖和执行步骤;代码类任务理解现有约束,定位根因,给出可靠实现与验证;写作类任务判断受众、场景、目标和语气,组织清晰自然的内容;分析类任务核对口径,区分事实、推断与假设,给出证据和建议。不要要求用户先选择任务分类,也不要无意义地复述分类结果。信息足够时直接推进,信息不足时只询问真正阻塞的关键问题。必须理解连续对话中的省略、指代和真实意图,不能只按当前一句做关键词匹配:如果前文正在讨论某个对象“长什么样”、外观、形态、颜色或场景,用户随后说“我看不到”“看不见”“我想看看”“给我看看”“展示一下”等,真实目标就是直接看到该对象的图片;应从最近对话提取主体和视觉特征并执行生图,绝不能回答自己是纯文本模型、建议用户另找绘图工具或继续用文字描述。解析“他、她、它、这个、那个”等代词时,最近一轮由用户明确提到的实体拥有最高优先级;代词的字形或性别绝不能自行把动物、物品、建筑等改成人类。新的知识询问、新主体介绍、图片识别和单纯情绪反馈默认是文本任务,绝不能因为前面生成过图片就继承旧主体或继续生图。“什么鬼”“不对”“胡说”“离谱”等短反馈通常是在质疑上一轮回答,应结合紧邻的助手回复承认偏差、重新核对或询问具体错误,不能把短语当成词条进行字面解释。图片任务必须遵守动作协议:当用户明确或隐含地希望看到、生成、重做或修改图片,评价上一张生成图并期待调整,或确认刚讨论的图片方案(例如“对”“就这样”“按方案三”)时,不要继续讲解或重复确认,只输出一行 JSON{"action":"generate_image","prompt":"完整、独立、可直接用于生图的最终画面描述"}。prompt 必须以当前图片版本状态为基础,合并本轮要求并保留所有未被本轮明确推翻的历史修改,绝不能擅自退回更早版本的场景、主体、风格或构图;“太 X”通常表示降低 X,“不够 X”表示增强 X,不得反向理解。prompt 的视觉描述以英文为主;用户未要求画面含字时不得保留中文指令词,并必须明确 no text, no letters, no Chinese characters, no title, no caption, no watermark;用户明确给出标题、书名或画面文字时,必须将原文逐字保留为 EXACT_VISIBLE_TEXT: <<<原文>>>,禁止翻译、改写、漏字、换序或另造字符,此时禁止追加 no text/no title 等冲突要求。prompt 只能描述修改完成后真正可见的主体、环境、构图、风格、光线和色彩,不得引用、复述或翻译“生成一张图片”“重新优化”等命令和反馈原句。“在某个环节生成”“接着生成”“帮我生成”等流程、时序和操作措辞不是画面内容,禁止据此添加人物或场景;只有用户明确说画面包含某元素时才能加入。只有缺失会改变核心主体的关键信息且无法从上下文推断时,才简短追问一次。用户只是要求识图、分析图片、编写提示词或询问生成方法时,应正常回答,不得输出图片动作。系统会自动执行动作 JSON,绝不能把 JSON 当作回答展示给用户。用户询问能力时应明确说明可以生成图片,不要声称不支持。不得编造图片链接,也不得在系统没有返回真实图片附件时声称图片已经生成。',
],
];
}
public static function publicList(): array
{
return array_map(function (array $agent) {
unset($agent['system_prompt'], $agent['supports_image'], $agent['image_prompt']);
return $agent;
}, self::all());
}
public static function find(?string $id): ?array
{
if (!$id) {
return null;
}
if (in_array($id, ['planner', 'engineer', 'writer', 'analyst'], true)) {
$id = 'auto';
}
foreach (self::all() as $agent) {
if ($agent['id'] === $id) {
return $agent;
}
}
return null;
}
public static function supportsImage(?array $agent): bool
{
return $agent !== null && ($agent['supports_image'] ?? false) === true;
}
public static function buildImagePrompt(?array $agent, string $content): string
{
$content = trim($content);
if (!self::supportsImage($agent)) {
return $content;
}
$direction = trim((string) ($agent['image_prompt'] ?? ''));
if (str_contains($content, 'EXACT_VISIBLE_TEXT: <<<')) {
$direction = 'High-quality purposeful image, coherent composition, clear visual hierarchy, refined lighting and color, faithful to the user request, professional art direction.';
}
return $direction === '' ? $content : $direction . "\n\n" . $content;
}
/**
* Parse the private action envelope emitted by the language model. The model may
* wrap it in a Markdown fence or use the legacy `task` key, so accept both forms.
*/
public static function parseImageAction(string $content): ?array
{
$content = trim($content);
if ($content === '') {
return null;
}
$candidates = [[
'json' => $content,
'wrapper' => '',
]];
if (preg_match_all('/```(?:json)?\s*([\s\S]*?)```/iu', $content, $fences, PREG_OFFSET_CAPTURE)) {
foreach ($fences[1] as $index => $match) {
$fullMatch = $fences[0][$index];
$candidates[] = [
'json' => $match[0],
'wrapper' => substr($content, 0, $fullMatch[1])
. substr($content, $fullMatch[1] + strlen($fullMatch[0])),
];
}
}
foreach (self::extractJsonObjects($content) as $object) {
$candidates[] = $object;
}
foreach ($candidates as $candidate) {
if (!self::allowsImageActionWrapper((string) ($candidate['wrapper'] ?? ''))) {
continue;
}
$json = trim((string) preg_replace(
'/^```(?:json)?\s*|\s*```$/iu',
'',
trim((string) ($candidate['json'] ?? ''))
));
$payload = json_decode($json, true);
if (!is_array($payload)) {
continue;
}
$action = mb_strtolower(trim((string) ($payload['action'] ?? $payload['task'] ?? $payload['tool'] ?? '')));
$action = str_replace(['-', ' '], '_', $action);
if (!in_array($action, ['generate_image', 'create_image', 'image_generation', 'draw_image'], true)) {
continue;
}
$arguments = $payload['arguments'] ?? [];
if (is_string($arguments)) {
$arguments = json_decode($arguments, true) ?: [];
}
$prompt = trim((string) ($payload['prompt'] ?? (is_array($arguments) ? ($arguments['prompt'] ?? '') : '')));
if ($prompt === '') {
continue;
}
return [
'action' => 'generate_image',
'prompt' => mb_substr($prompt, 0, 8000),
];
}
return null;
}
/**
* The deterministic router has already approved an image operation, so a valid
* action object may be recovered even when the model surrounded it with prose.
*/
public static function parseRoutedImageAction(string $content): ?array
{
foreach (self::extractJsonObjects($content) as $object) {
$action = self::parseImageAction((string) ($object['json'] ?? ''));
if ($action !== null) {
return $action;
}
}
return null;
}
private static function extractJsonObjects(string $content): array
{
$objects = [];
$length = strlen($content);
$depth = 0;
$start = null;
$inString = false;
$escaped = false;
for ($index = 0; $index < $length; $index++) {
$char = $content[$index];
if ($inString) {
if ($escaped) {
$escaped = false;
} elseif ($char === '\\') {
$escaped = true;
} elseif ($char === '"') {
$inString = false;
}
continue;
}
if ($char === '"') {
$inString = true;
} elseif ($char === '{') {
if ($depth === 0) {
$start = $index;
}
$depth++;
} elseif ($char === '}' && $depth > 0) {
$depth--;
if ($depth === 0 && $start !== null) {
$json = substr($content, $start, $index - $start + 1);
$objects[] = [
'json' => $json,
'wrapper' => substr($content, 0, $start) . substr($content, $index + 1),
];
$start = null;
}
}
}
return $objects;
}
private static function allowsImageActionWrapper(string $wrapper): bool
{
$wrapper = trim((string) preg_replace('/[`\s,。.!:;]+/u', ' ', $wrapper));
if ($wrapper === '') {
return true;
}
if (preg_match('/示例|格式|例如|假设|说明|解释|请勿|不要执行|仅供参考|example|format|do\s+not|don[\']t|reference/iu', $wrapper)) {
return false;
}
if (mb_strlen($wrapper) > 320) {
return false;
}
if (mb_strlen($wrapper) > 80) {
return preg_match(
'/(?:我将|将为你|将为您|现在为你|现在为您|接下来|马上|立即|正在)'
. '[\s\S]{0,180}(?:执行|生成|创作|制作|绘制|生图|generate|create|draw|paint)/iu',
$wrapper
) === 1;
}
return preg_match(
'/(?:好的|收到|明白|可以|没问题|开始|马上|立即|现在|正在|将为你|下面开始|'
. 'here\s+you\s+go|here\s+is|i[\']ll|i\s+will|we[\']ll|we\s+will|generating|creating)'
. '[^。.!?]{0,48}(?:执行|生成|创作|制作|绘制|生图|generate|create|draw|paint)?/iu',
$wrapper
) === 1;
}
/**
* Detect natural visual requests that omit words such as “图片” or “生成”.
* Elliptical complaints like “我看不到” only count when recent dialogue is
* actually about appearance, preventing code/document visibility issues from
* being routed to the image model.
*/
public static function requestsContextualImageGeneration(string $content, array $history = []): bool
{
$content = mb_strtolower(trim($content));
if ($content === '' || self::requestsImageGeneration($content)) {
return false;
}
// “看不懂” describes comprehension, not an inability to see an image.
if (preg_match('/^(?:我)?(?:还是)?(?:看不懂|看不明白|没看懂|没有看懂)[了啊呀呢吗,,。!?!?\s]*$/u', $content)) {
return false;
}
if (preg_match('/(?:不要|不用|无需|别|不需要|只要文字|文字说明|不用图片|不要图片)/u', $content)) {
return false;
}
$nonVisualTarget = '/(?:介绍|资料|知识|习性|毒性|分类|区别|原因|为什么|怎么|如何|教程|步骤|方法|代码|文档|表格|报表|数据|日志|报错|新闻|视频|电影|直播|实时|天气|预报|价格|股票|网页|页面|消息|聊天记录|文字|答案)/u';
if (preg_match($nonVisualTarget, $content)) {
return false;
}
$directDisplay = '/^(?:请|麻烦)?(?:'
. '(?:给|让)(?:我)?(?:看(?:看|到|见|一下)?|瞧瞧|展示(?:一下)?)'
. '|(?:我)?(?:想|要|希望|想要)(?:看(?:看|到|见|一下)?|瞧瞧)'
. '|(?:能否|能不能|可以|可不可以)?(?:给我|让我)?(?:看(?:看|到|见|一下)?|瞧瞧|展示(?:一下)?)'
. ')\s*(?<target>[^,。!?!?\n]{1,30}?)(?:的?(?:样子|图片|照片|图像))?(?:可以吗|行吗|好吗|呢)?$/u';
if (preg_match($directDisplay, $content, $match)) {
$target = trim((string) ($match['target'] ?? ''));
$resolvedTarget = self::normalizeVisualSubject($target);
if ($resolvedTarget !== '' && !preg_match($nonVisualTarget, $resolvedTarget)) {
return true;
}
if (self::resolveContextualImageSubject($content, $history) !== '') {
return true;
}
}
$appearanceQuestion = '/^(?:他|她|它|他们|她们|它们|这个|那个|这|那|该对象|该东西|[^,。!?!?\n]{1,24})?'
. '(?:长啥样|长什么样(?:子)?|是什么样子|外观(?:怎样|如何|是什么样)?)'
. '[啊呀呢吗么,,。!?!?\s]*$/u';
if (preg_match($appearanceQuestion, $content)
&& self::resolveContextualImageSubject($content, $history) !== '') {
return true;
}
if (preg_match('/^(?:那)?也(?:给我|让我)?(?:看看|看一下|瞧瞧|展示一下)[了啊吧呀呢,,。!?!?\s]*$/u', $content)
&& self::resolveContextualImageSubject($content, $history) !== '') {
return true;
}
$naturalContextualDisplay = '/^(?:'
. '(?:给我?)?瞅一眼|有照片吗|(?:他|她|它|这个|那个)有(?:图|照片)吗|'
. '(?:这个|那个)?也想(?:看看|瞧瞧)|我也想(?:看看|瞧瞧)|'
. '也来(?:一)?张(?:他|她|它|这个|那个)(?:的)?|'
. '(?:两(?:个|位|只)|俩)都(?:给我|让我)?(?:看看|瞧瞧|展示一下)|'
. '(?:那)?也展示一下'
. ')[了啊吧呀呢呗,,。!?!?\s]*$/u';
if (preg_match($naturalContextualDisplay, $content)
&& self::resolveContextualImageSubject($content, $history) !== '') {
return true;
}
$appearanceThenDisplay = '/^(?:他|她|它|他们|她们|它们|这个|那个|这|那|该对象|该东西|[^,。!?!?\n]{1,24})?'
. '(?:长啥样|长什么样|是什么样子|外观(?:怎样|如何|是什么样)?)'
. '[,、\s]*(?:我)?(?:想|要|希望|想要)?(?:看(?:看|到|见|一下)?|瞧瞧|展示(?:一下)?)'
. '[了啊呀呢吗??。!!]*$/u';
if (preg_match($appearanceThenDisplay, $content)
&& self::resolveContextualImageSubject($content, $history) !== '') {
return true;
}
$ellipticalDisplay = '/^(?:我)?(?:还是|但|可是|就是|现在)?\s*'
. '(?:看不到|看不见|没看到|没有看到|想看(?:看|到)?|想瞧瞧|给我看看|让我看看|展示一下|直接展示|有图吗|图片呢|图呢|能看吗|能看到吗)'
. '[了啊呀呢吗??。!!]*$/u';
if (!preg_match($ellipticalDisplay, $content)) {
return false;
}
$contextParts = [];
$history = array_slice($history, -12);
foreach ($history as $index => $message) {
$messageContent = trim((string) ($message['content'] ?? ''));
$isCurrentMessage = $index === array_key_last($history)
&& ($message['role'] ?? '') === 'user'
&& mb_strtolower($messageContent) === $content;
if ($messageContent !== '' && !$isCurrentMessage) {
$contextParts[] = $messageContent;
}
}
$context = implode("\n", $contextParts);
if ($context === '') {
return false;
}
return preg_match(
'/(?:长什么样|什么样子|外观|外形|形态|样貌|造型|轮廓|颜色|纹理|头部|身体|眼睛|四肢|翅膀|鳞片|毛发|姿态|构图|场景|风景|天空|动物|人物|建筑|物体|照片|图片|图像|画面|视觉|写实|卡通)/u',
$context
) === 1;
}
/**
* Resolve the concrete entity behind a contextual visual request. Pronouns are
* never accepted as subjects; the latest explicit user entity wins.
*/
public static function resolveContextualImageSubject(string $content, array $history = []): string
{
$content = mb_strtolower(trim($content));
if ($content === '') {
return '';
}
$currentPatterns = [
'/^(?<subject>[^,。!?!?\n]{1,24}?)(?:长啥样|长什么样|是什么样子|外观(?:怎样|如何|是什么样)?)/u',
'/(?:给我|让我|我想|我希望|我想要|想|要|希望)(?:看(?:看|到|见|一下)?|瞧瞧|展示(?:一下)?)\s*(?<subject>[^,。!?!?\n]{1,30})/u',
];
foreach ($currentPatterns as $pattern) {
if (preg_match($pattern, $content, $match)) {
$subject = self::normalizeVisualSubject((string) ($match['subject'] ?? ''));
if ($subject !== '') {
return $subject;
}
}
}
$history = array_slice($history, -12);
$priorMessages = [];
foreach ($history as $index => $message) {
$messageContent = mb_strtolower(trim((string) ($message['content'] ?? '')));
$isCurrentMessage = $index === array_key_last($history)
&& ($message['role'] ?? '') === 'user'
&& $messageContent === $content;
if ($messageContent !== '' && !$isCurrentMessage) {
$priorMessages[] = [
'role' => (string) ($message['role'] ?? ''),
'content' => $messageContent,
];
}
}
for ($index = count($priorMessages) - 1; $index >= 0; $index--) {
$message = $priorMessages[$index];
if ($message['role'] !== 'user') {
continue;
}
$patterns = [
'/^(?:不是[^,。!?!?\n]{1,20}[,]?\s*)?(?:我说的是|我要的是|应该是)\s*(?<subject>[^,。!?!?\n]{1,24})/u',
'/(?:你知道|你了解|认识|了解一下|讲讲|介绍一下)\s*(?<subject>[^,。!?!?\n]{1,24}?)(?:吗|么|呢|吧||\?|。||!)?$/u',
'/^(?:那|那么|至于)\s*(?<subject>[^,。!?!?\n]{1,24}?)\s*(?:呢|怎么样|又如何)?[,。!?!?\s]*$/u',
'/^(?<subject>[^,。!?!?\n]{1,24}?)(?:长啥样|长什么样|是什么样子|的?(?:外观|外形|形态|样貌|造型))/u',
'/(?:想看|看看|展示|生成|画|绘制)\s*(?<subject>[^,。!?!?\n]{1,24})/u',
];
foreach ($patterns as $pattern) {
if (preg_match($pattern, $message['content'], $match)) {
$subject = self::normalizeVisualSubject((string) ($match['subject'] ?? ''));
if ($subject !== '') {
if (self::isAmbiguousReferencedSubject($subject, $content)) {
return '';
}
return $subject;
}
}
}
}
for ($index = count($priorMessages) - 1; $index >= 0; $index--) {
$message = $priorMessages[$index];
if ($message['role'] !== 'assistant') {
continue;
}
if (preg_match(
'/^(?:知道|了解|当然知道|当然了解)?[。,.!\s]*(?<subject>[\p{Han}a-z0-9·_-]{1,24}?)(?:是|属于|通常|一般|具有|有)/iu',
$message['content'],
$match
)) {
$subject = self::normalizeVisualSubject((string) ($match['subject'] ?? ''));
if ($subject !== '') {
if (self::isAmbiguousReferencedSubject($subject, $content)) {
return '';
}
return $subject;
}
}
}
return '';
}
private static function normalizeVisualSubject(string $subject): string
{
$subject = mb_strtolower(trim($subject));
$subject = preg_replace('/^[“”‘’"\'《》〈〉【】\[\]()()\s]+|[“”‘’"\'《》〈〉【】\[\]()()\s]+$/u', '', $subject);
$subject = preg_replace(
'/^(?:请|麻烦)?(?:给我|让我|我想|我希望|我想要|想|要|希望)(?:看(?:看|到|见|一下)?|瞧瞧|展示(?:一下)?)?/u',
'',
$subject
);
$subject = preg_replace('/^(?:关于|有关|这个|那个|这种|那种|一个|一种|一只|一位|该)/u', '', $subject);
$subject = preg_replace('/(?:的?(?:样子|图片|照片|图像)|长啥样|长什么样|是什么样子|可以吗|行吗|好吗|呢|吗)$/u', '', $subject);
$subject = trim((string) $subject);
if ($subject === '' || mb_strlen($subject) > 24) {
return '';
}
if (preg_match('/^(?:看|看看|看一下|瞧瞧|展示|展示一下)$/u', $subject)) {
return '';
}
if (preg_match('/^(?:那)?(?:他|她|它|他们|她们|它们|这个|那个|这|那|玩意儿|东西|对象|该对象|该东西)(?:长|是|外观)?$/u', $subject)) {
return '';
}
if (preg_match('/^(?:他|她|它|他们|她们|它们|这个|那个|这|那|其|对象|东西|该对象|该东西)$/u', $subject)) {
return '';
}
if (preg_match('/(?:介绍|资料|知识|习性|毒性|分类|区别|原因|教程|步骤|方法|代码|文档|表格|报表|数据|日志|报错|新闻|视频|电影|直播|实时|天气|预报|价格|股票|网页|页面|消息|聊天记录|文字|答案|php|javascript|java|python)/iu', $subject)) {
return '';
}
if (preg_match('/(?:不要|不用|生成|执行|系统|提示词|prompt|json|模型|工具)/iu', $subject)) {
return '';
}
return $subject;
}
private static function isAmbiguousReferencedSubject(string $subject, string $content): bool
{
$hasMultipleCandidates = preg_match('/(?:和|与|、|以及|还有|及)/u', $subject) === 1;
if (!$hasMultipleCandidates) {
return false;
}
$usesPluralReference = preg_match('/(?:他们|她们|它们|两(?:个|位|只)|都|一起|全部)/u', $content) === 1;
$usesSingularReference = preg_match('/(?:他|她|它)(?!们)|(?:这个|那个|这位|那位)/u', $content) === 1
|| preg_match('/^(?:我)?(?:想|要|希望)?(?:看看|看一下|瞧瞧)[了啊吧呀呢,,。!?!?\s]*$/u', $content) === 1;
return $usesSingularReference && !$usesPluralReference;
}
/**
* Detect instructions that require the pixels of an existing image. The
* controller only enables this route when a user image or generated image
* is actually available, so ordinary text such as “删除这一段” is unaffected.
*/
public static function requestsImageEditing(string $content): bool
{
$content = mb_strtolower(trim($content));
if ($content === '') {
return false;
}
if (self::requestsImageEnhancement($content)) {
return true;
}
if (preg_match('/(?:分析|解释|识别|读取|描述|评价|为什么|是什么|是谁|教程|方法|怎么做)/u', $content)
&& !preg_match('/(?:修改|处理|修复|重绘|去掉|移除|擦除|删除|替换|换成|改成)/u', $content)) {
return false;
}
return preg_match(
'/(?:img2img|image\s*to\s*image|inpaint|图生图|局部重绘|局部修复|图片编辑|处理这张图|修改这张图|'
. '(?:去|移除|擦除|抹掉|删除|去掉|清除)[^,。!?!?\n]{0,16}(?:水印|logo|标志|文字|字幕|人物|路人|物体|东西|瑕疵|污点|反光|背景)|'
. '(?:水印|logo|标志|文字|字幕|人物|路人|物体|东西|瑕疵|污点|反光)[^,。!?!?\n]{0,12}(?:去|移除|擦除|抹掉|删除|去掉|清除)|'
. '(?:换|替换|修改|改变|改成)[^,。!?!?\n]{0,10}(?:背景|天空|衣服|颜色|风格|人物|物体)|'
. '(?:抠图|扩图|补图|修图|精修|老照片修复|照片修复|画质修复|修复[^,,。!?!?\n]{0,10}(?:照片|图片|画面)|上色|去噪|锐化|无损放大|增强清晰度)|'
. '(?:(?:去除|移除|删除|擦除|清除)[^,。!?!?\n]{0,6}(?:不彻底|不干净|有残留)|'
. '(?:没|没有|未)(?:去|删|清|擦)[^,。!?!?\n]{0,6}(?:干净|掉|完)|'
. '(?:还|仍然|依然)(?:有|在)[^,。!?!?\n]{0,6}(?:水印|残留))|'
. '(?:把|将)[^,。!?!?\n]{1,24}(?:换成|改成|变成|去(?:除|掉)(?:了)?|移除(?:了)?|删除(?:了)?|擦掉(?:了)?|修复))'
. '/iu',
$content
) === 1;
}
public static function requestsImageEnhancement(string $content): bool
{
$content = mb_strtolower(trim($content));
if ($content === '') {
return false;
}
if (preg_match('/(?:怎么|如何|为什么|教程|方法|原理|能不能介绍|请解释)/u', $content)) {
return false;
}
return preg_match(
'/(?:变|变得|弄|调|处理)(?:成|得)?(?:更)?清晰|'
. '(?:更|再)清晰(?:一?点|一些)?|清晰(?:一?点|一些)|'
. '提高清晰度|增强清晰度|高清化|变高清|提升画质|改善画质|'
. '超分(?:辨率)?|无损放大|锐化(?:一下)?/u',
$content
) === 1;
}
public static function imageEditMode(string $content): string
{
$content = mb_strtolower(trim($content));
return preg_match(
'/(?:inpaint|局部|遮罩|蒙版|修补|补全|修复|不彻底|不干净|有残留|没去干净|'
. '(?:去|移除|擦除|擦掉|抹掉|删除|去掉|清除)|'
. '(?:换|替换|修改|改变|改成)[^,。!?!?\n]{0,10}(?:背景|天空|衣服|人物|物体))/iu',
$content
) === 1 ? 'inpaint' : 'img2img';
}
public static function requestsBackgroundRemoval(string $content): bool
{
$content = mb_strtolower(trim($content));
if ($content === '') {
return false;
}
if (preg_match('/(?:怎么|如何|教程|方法|原理|能不能介绍|请解释)/u', $content)) {
return false;
}
// Do not treat "补全/相邻/周围背景" (inpaint fill language) as cutout.
return preg_match(
'/(?:抠图|抠出[^,。!?!?\n]{0,16}(?:主体|人物|商品|物体)|'
. '(?:移除|去除|删除|去掉|清除)\s*(?:图片|照片|整图|图像|这张图)?(?:中|里)?(?:的|了)?背景|'
. '(?:把|将)背景(?:移除|去除|删除|去掉|清除)|'
. '(?:背景透明|透明背景|透明底|transparent\s+background|remove\s+(?:the\s+)?background))/iu',
$content
) === 1;
}
/**
* Resolve cutout / watermark intent. Explicit workbench image_tool wins over content heuristics.
*
* @return array{is_background_removal: bool, is_watermark_removal: bool}
*/
public static function resolveImageEditFlags(string $content, string $imageTool = ''): array
{
$imageTool = trim($imageTool);
if ($imageTool === 'cutout') {
return ['is_background_removal' => true, 'is_watermark_removal' => false];
}
if ($imageTool === 'watermark') {
return ['is_background_removal' => false, 'is_watermark_removal' => true];
}
if ($imageTool !== '') {
// Other explicit tools (erase/replace/...) must never be misrouted to cutout.
$isWatermarkRemoval = $imageTool === 'erase' && self::requestsWatermarkRemoval($content);
return ['is_background_removal' => false, 'is_watermark_removal' => $isWatermarkRemoval];
}
$isBackgroundRemoval = self::requestsBackgroundRemoval($content);
$isWatermarkRemoval = self::requestsWatermarkRemoval($content);
if ($isBackgroundRemoval && $isWatermarkRemoval) {
// Phrases like "去掉水印并补全背景" are watermark jobs, not cutout.
$isBackgroundRemoval = false;
}
return [
'is_background_removal' => $isBackgroundRemoval,
'is_watermark_removal' => $isWatermarkRemoval,
];
}
public static function requestsWatermarkRemoval(string $content): bool
{
$content = mb_strtolower(trim($content));
if ($content === '') {
return false;
}
if (preg_match(
'/(?:(?:去除|移除|删除|擦除|清除)[^,。!?!?\n]{0,6}(?:不彻底|不干净|有残留)|'
. '(?:没|没有|未)(?:去|删|清|擦)[^,。!?!?\n]{0,6}(?:干净|掉|完)|'
. '(?:还|仍然|依然)(?:有|在)[^,。!?!?\n]{0,6}(?:水印|残留))/u',
$content
)) {
return true;
}
return preg_match(
'/(?:(?:去(?:除|掉)?|移除|删除|擦除|擦掉|抹掉|清除)[^,。!?!?\n]{0,16}(?:水印|watermark)|'
. '(?:水印|watermark)[^,。!?!?\n]{0,16}(?:去(?:除|掉)?|移除|删除|擦除|擦掉|抹掉|清除|remove|erase)|'
. '(?:remove|erase)[^,.!?\n]{0,16}watermark)/iu',
$content
) === 1;
}
public static function imageTextRemovalTarget(string $content): ?string
{
$content = mb_strtolower(trim($content));
if ($content === '') {
return null;
}
$hasRemoval = preg_match(
'/(?:(?:去(?:除|掉)?|移除|删除|擦除|擦掉|抹掉|清除)(?:了)?|'
. '(?:不要|不保留|隐藏)[^,。!?!?\n]{0,8}(?:文字|文本|字样|署名|作者|作者名|标题|书名))/u',
$content
) === 1;
if (!$hasRemoval) {
return null;
}
if (preg_match('/(?:作者|作者名|署名|笔名)/u', $content)) {
return 'author';
}
if (preg_match('/(?:标题|书名|主标题|副标题)/u', $content)) {
return 'title';
}
if (preg_match('/(?:所有|全部|整张|画面中|图中)?[^,。!?!?\n]{0,6}(?:文字|文本|字样|字幕)/u', $content)) {
return 'all_text';
}
return null;
}
public static function requestsImageRevision(string $content): bool
{
$content = mb_strtolower(trim($content));
if ($content === '') {
return false;
}
$textOnlyTarget = preg_match('/文案|文章|代码|方案|报告|表格|数据|提示词|prompt/u', $content) === 1;
$imageReference = preg_match('/图片|图像|画面|插画|海报|封面|头像|照片|背景|构图|人物|角色/u', $content) === 1;
$strongRevision = '/(?:重新|再次|再)(?:生成|制作|创作|做|画|绘制|渲染|优化|调整|修改|来|出图|生图)|重做|重画|换(?:一|个)?张|再来(?:一|个)?张|重新来/u';
if (preg_match($strongRevision, $content)) {
if ($textOnlyTarget && !$imageReference) {
return false;
}
return true;
}
if (preg_match('/为什么|怎么|怎么办|如何|是什么|描述|分析|评价|解释|识别|读取|建议|方法/u', $content)) {
return false;
}
$correctionRevision = '/^(?:不对|不是|错了|搞错了|认错了|画错了|这不是|我说的是|我要的是|应该是)'
. '[^。!?!?\n]{0,36}(?:我说的是|我要的是|应该是|不是|而是|是|换成|改成|画成)'
. '[^。!?!?\n]{1,30}/u';
if (preg_match($correctionRevision, $content)) {
return !$textOnlyTarget || $imageReference;
}
$visualTarget = '(?:图片|图像|画面|主体|人物|角色|背景|构图|镜头|光线|色彩|颜色|风格|姿势|动作|表情|服装|材质|细节|比例|胸围|胸部|肌肉|脸|眼睛|头发|毛发|鬃毛|毛|角|翅膀|尾巴|身体|天空|草地|招牌|文字|字|尺寸|分辨率)';
$editAction = '(?:调整|修改|优化|改(?:成|为)?|换(?:成|为|个)?|变成|做成|画成|调(?:成|整)?|增加|减少|加强|弱化|放大|缩小|加大|减小|删掉|去掉|别加|不要|取消|保留|保持|替换|修正)';
$qualityTarget = '(?:大|小|长|短|亮|暗|高|低|多|少|宽|窄|强|弱|胖|瘦|饱满|明显|真实|自然|写实|卡通|清晰|模糊|拥挤|空旷|空|荒凉|生动|僵硬|单调|鲜艳|饱和|假|胸围|胸部|肌肉|细节|比例)';
$contextualRevision = '/^(?:请|麻烦)?(?:把|将)?[^,。!?!?\n]{0,14}'
. '(?:改(?:成|为)?|换(?:成|为|个)?|变成|做成|画成|调(?:成|整)?)'
. '[^,。!?!?\n]{1,24}/u';
if (preg_match($contextualRevision, $content)) {
return !$textOnlyTarget || $imageReference;
}
$shortVisualRevision = preg_match(
'/^(?:请|麻烦)?(?:'
. $visualTarget . '(?:往|向)(?:左|右|上|下|前|后)(?:移|挪)?(?:一点|点|一些|些)?'
. '|' . $visualTarget . '(?:拉远|拉近|推近|后退|前移)(?:一点|点|一些|些)?'
. '|' . $visualTarget . '(?:再|更|稍微)?(?:淡|浓|亮堂|明亮|柔和|鲜艳|自然)(?:一点|点|一些|些)?'
. '|(?:再|更|稍微)?(?:亮堂|明亮|柔和|自然)(?:一点|点|一些|些)?'
. '|别这么(?:挤|拥挤|塑料|假|僵硬|空|暗|亮)'
. '|更有(?:电影|故事|层次|空间|氛围|质感|生命)感'
. ')[了啊吧呀呢,,。!?!?\s]*$/u',
$content
) === 1;
if ($shortVisualRevision) {
return true;
}
return preg_match('/' . $editAction . '[^,。!?!?\n]{0,16}' . $visualTarget . '/u', $content) === 1
|| preg_match('/' . $visualTarget . '[^,。!?!?\n]{0,16}' . $editAction . '/u', $content) === 1
|| preg_match('/(?:不够|太|需要更|想要更)[^,。!?!?\n]{0,16}' . $qualityTarget . '/u', $content) === 1
|| preg_match('/' . $visualTarget . '[^,。!?!?\n]{0,12}(?:再|更)[^,。!?!?\n]{0,8}(?:一点|点|一些|' . $qualityTarget . ')/u', $content) === 1
|| preg_match('/^(?:请|麻烦)?(?:再|更|稍微)' . $qualityTarget . '(?:一点|点|一些|些)?[了啊吧呀呢,,。!?!?\s]*$/u', $content) === 1;
}
public static function referencesPriorImagePlan(string $content): bool
{
$content = mb_strtolower(trim($content));
if ($content === '') {
return false;
}
return preg_match('/(?:按照|按|根据|采用|选择|就用|使用|照着)[^,。!?!?\n]{0,20}(?:方案|选项|建议|上面|前面|刚才|你说的|这个|那个)/u', $content) === 1
|| preg_match('/(?:方案|选项)\s*(?:第)?[一二三四五六七八九十\d]+[^,。!?!?\n]{0,12}(?:生成|制作|做|画|执行|来一张)/u', $content) === 1
|| preg_match('/^\s*(?:就用|选择|采用)?\s*(?:方案|选项)\s*(?:第)?[一二三四五六七八九十\d]+\s*$/u', $content) === 1
|| preg_match('/(?:就这样|按这个|照这个|用这个|照你说的)[^,。!?!?\n]{0,12}(?:生成|制作|做|画|来|执行)/u', $content) === 1;
}
/**
* A short confirmation only means "generate it" when the immediately preceding
* assistant turn actually offered a concrete image plan. This keeps replies such
* as "对" from inheriting an unrelated image much earlier in the conversation.
*/
public static function confirmsContextualImagePlan(string $content, array $history = []): bool
{
$content = mb_strtolower(trim($content));
$simpleConfirmation = preg_match(
'/^(?:对|好|好的|可以|行|就这样|按这个|照这个|用这个|开始吧|生成吧|做吧|画吧)[了啊吧呀呢,,。!?!?\s]*$/u',
$content
) === 1;
$selectionConfirmation = mb_strlen($content) <= 36
&& !preg_match('/(?:不行|不要|别|先不|为什么|怎么|哪一个|哪个好|吗|么|\?|)/u', $content)
&& preg_match('/(?:第?[一二三四五六七八九十\d]+(?:个|套|种|版|号|方案|方向)?|方案|方向|选项|这个|那个|它|你说的)/u', $content)
&& preg_match('/(?:挺好|不错|可以|就|选|用|照|按|采用|确定|来|做|生成)/u', $content);
$naturalConfirmation = preg_match(
'/^(?:那?就)?(?:按|照|用|采用)?(?:你说的|这个|那个|它|这样|这么|这些建议|刚才的建议)'
. '(?:来|改|调整|优化|做|生成|执行)?[了啊吧呀呢,,。!?!?\s]*$/u',
$content
) === 1;
$bareSelection = preg_match(
'/^(?:(?:第)?[一二三四五六七八九十\d]+(?:个|套|种|版|号|方案|方向)|最后(?:一个|一套|一种|一版|那个))'
. '[了啊吧呀呢,,。!?!?\s]*$/u',
$content
) === 1;
$contextualExecution = preg_match(
'/^(?:听你的|你推荐的那个|开始干吧|行[,,]?照办|可以[,,]?动手|嗯?[,]?就这么弄|'
. '好[,,]?修一下|执行吧|就这么办)[了啊吧呀呢,,。!?!?\s]*$/u',
$content
) === 1;
if (!$simpleConfirmation
&& !$selectionConfirmation
&& !$naturalConfirmation
&& !$bareSelection
&& !$contextualExecution) {
return false;
}
$history = array_slice($history, -12);
for ($index = count($history) - 1; $index >= 0; $index--) {
$message = $history[$index];
$messageContent = trim((string) ($message['content'] ?? ''));
if (($message['role'] ?? '') === 'user' && mb_strtolower($messageContent) === $content) {
continue;
}
if (($message['role'] ?? '') !== 'assistant' || $messageContent === '') {
continue;
}
$attachments = $message['attachments'] ?? [];
if (is_string($attachments)) {
$attachments = json_decode($attachments, true) ?: [];
}
foreach ((array) $attachments as $attachment) {
if (is_array($attachment) && ($attachment['type'] ?? '') === 'image') {
return false;
}
}
$hasVisualPlan = preg_match(
'/(?:图片|图像|插画|海报|封面|头像|画面|视觉|构图|风格|场景|生图|出图|生成|制作|绘制|方案|方向)/u',
$messageContent
) === 1;
$offersExecution = preg_match(
'/(?:选择|确认|采用|生成|制作|绘制|开始|要哪|哪个|哪一个|告诉我|可以吗|怎么样)/u',
$messageContent
) === 1;
return $hasVisualPlan && $offersExecution;
}
return false;
}
/**
* Conservative Agent router: only creation intent triggers ComfyUI; image analysis and prompt-writing stay on the LLM.
*/
public static function requestsImageGeneration(string $content): bool
{
$content = mb_strtolower(trim($content));
if ($content === '') {
return false;
}
$imageTarget = '(?:图片|图像|插画|海报|封面|头像|壁纸|配图|宣传图|广告图|商品图|产品图|场景图|人物图|背景图|电商图|主图|详情图|效果图|概念图|示意图|流程图|架构图|思维导图|图表|照片|视觉稿|视觉图|logo|标志|图标|banner|image|picture|illustration|poster|cover|wallpaper|photo|artwork|icon)';
$drawAction = '(?<![插漫油图绘字壁年版国动彩])画(?!面|质|风|布|廊|家|作|册|笔|纸|框|室|展)';
$creationAction = '(?:生成|创建|生图|出图|' . $drawAction . '|绘制|创作|制作|设计|渲染|generate|create|draw|paint|render|design|make)';
$directImageAction = '(?:生图|出图|绘图|作图|制图|' . $drawAction . '|绘制|paint\b|draw\s+(?!a\s+conclusion\b|conclusions?\b))';
$directRequest = '(?:(?:^|给我|帮我|请|来)(?:直接|马上|立即|现在|重新|再)?'
. $directImageAction . '|(?:把|将)[^,。!?!?\n]{1,12}(?:' . $drawAction . '|绘制|绘图|重画))';
// Pronoun-only “来张它的” needs conversation context and must not discard history.
if (preg_match('/^(?:也)?来(?:一)?张(?:他|她|它|他们|她们|它们|这个|那个|刚才那个)(?:的)?[了啊吧呀呢,,。!?!?\s]*$/u', $content)) {
return false;
}
$explicitDeferral = '/(?:别急着|先别|暂时(?:先)?不|暂不|目前(?:先)?不|先不要)[^。!?!?\n]{0,10}'
. '(?:生成|制作|创作|绘制|生图|出图)|先[^。!?!?\n]{0,20}'
. '(?:讨论|分析|规划|构思|方案|方向|建议)[^。!?!?\n]{0,24}'
. '(?:确认后|之后|以后|再)(?:生成|制作|创作|绘制|生图|出图)/u';
if (preg_match($explicitDeferral, $content)) {
return false;
}
$capabilityQuestion = '/(?:你|模型|agent|ai|助手)?[^,。!?!?\n]{0,5}'
. '(?:会不会|能不能|能否|是否(?:可以|能够|支持)|会|能|可以|支持)'
. '[^,。!?!?\n]{0,16}(?:' . $directImageAction . '|'
. $creationAction . '[^,。!?!?\n]{0,10}' . $imageTarget . '|'
. $imageTarget . '[^,。!?!?\n]{0,10}' . $creationAction . ')'
. '[^,。!?!?\n]{0,4}(?:吗|么|不||\?)?$/iu';
$concreteBrief = '/(?:一|两|二|三|几|多)?(?:张|幅|个|只|位|套|枚|款|版)'
. '|(?:主体|场景|背景|风格|构图|镜头|光线|颜色|色彩|姿势|动作|材质)[:为是]?/u';
$englishCapabilityQuestion = '/\b(?:(?:can|could)\s+you|do\s+you\s+support|are\s+you\s+able\s+to)\s+'
. '(?:generate|create|draw|paint|make|image\s+generation)[^.!?\n]{0,16}'
. '(?:images?|pictures?|illustrations?|artwork)?\s*\??$/iu';
$englishConcreteBrief = '/\b(?:of|showing|featuring|depicting|with|in\s+the\s+style\s+of)\b/iu';
$isCapabilityOnly = static function (string $text) use (
$capabilityQuestion,
$concreteBrief,
$englishCapabilityQuestion,
$englishConcreteBrief
): bool {
return (preg_match($capabilityQuestion, $text) && !preg_match($concreteBrief, $text))
|| (preg_match($englishCapabilityQuestion, $text) && !preg_match($englishConcreteBrief, $text));
};
if ($isCapabilityOnly($content)) {
return false;
}
if (preg_match(
'/(?:解释|介绍|说明|讲讲)[^,。!?!?\n]{0,16}(?:图片生成|图像生成|生图)'
. '[^,。!?!?\n]{0,12}(?:原理|机制|技术|能力|流程)/u',
$content
)) {
return false;
}
$negatedRequest = '/(?:不要|不用|无需|别|禁止|避免|不需要|do\s+not|don[\']t)[^,。!?!?\n]{0,8}'
. '(?:' . $directImageAction . '|' . $creationAction . '[^,。!?!?\n]{0,12}' . $imageTarget . ')/iu';
$metaRequest = '/(?:'
. '(?:如何|怎么|怎样|教程|方法|步骤|是否支持|支不支持|会不会)[^,。!?!?\n]{0,20}(?:'
. $directImageAction . '|' . $creationAction . '[^,。!?!?\n]{0,12}' . $imageTarget . ')'
. '|(?:告诉|说明|介绍|解释|列出|给出|讲讲|想知道)[^,。!?!?\n]{0,10}'
. '(?:' . $directImageAction . '|' . $creationAction . '[^,。!?!?\n]{0,12}' . $imageTarget . ')'
. '[^,。!?!?\n]{0,10}(?:步骤|教程|方法|流程)'
. '|(?:' . $directImageAction . '|' . $creationAction . '[^,。!?!?\n]{0,12}' . $imageTarget . ')'
. '[^,。!?!?\n]{0,8}(?:步骤|教程|方法|流程)(?:是什么|有哪些|怎么|如何|呢|吗||\?)?'
. '|(?:解释|说明|介绍|讨论|询问|查看|输出|给出)[^,。!?!?\n]{0,16}'
. '(?:生图|出图|生成图片|图片生成)[^,。!?!?\n]{0,14}(?:动作|协议|格式|json|工具|接口|示例)'
. '|(?:生图|出图|生成图片|图片生成)[^,。!?!?\n]{0,12}'
. '(?:是什么|怎么用|动作|协议|格式|json|模型|工具|接口|能力)'
. '|(?:写|撰写|优化|润色|翻译|改写|分析|解释|提供|给出)[^,。!?!?\n]{0,16}'
. '(?:生图|生成图片|图片生成)[^,。!?!?\n]{0,10}(?:提示词|prompt)'
. ')/iu';
$deferredRequest = '/(?:只|先|暂时|目前)(?:讨论|分析|规划|构思|给建议|写提示词|不生成|不要生成|先不生成|暂不生成|不执行|不要执行)/u';
$visualScene = '(?:头像|壁纸|海报|封面|主图|夜景|日落|日出|风景|城市|书房|客厅|卧室|花园|海边|沙漠|森林|雪山|草原|天空|极光|人物|人像|女孩|男孩|动物|猫|狗|龙)';
$colloquialVisualRequest = '/^(?:请|麻烦)?(?:给|帮)?(?:我)?'
. '(?:整|来|出|做)(?:一|两|三)?(?:个|张|幅|套)?'
. '[^,。!?!?\n]{0,18}' . $visualScene . '(?:看看|瞧瞧)?[了啊吧呀呢,,。!?!?\s]*$/u';
$patterns = [
'/' . $creationAction . '[^,。!?!?\n]{0,18}' . $imageTarget . '/iu',
'/' . $imageTarget . '[^,。!?!?\n]{0,12}' . $creationAction . '/iu',
'/' . $directRequest . '/iu',
$colloquialVisualRequest,
'/(?:给我|帮我|来|生成|创建|创作|制作|做|出)[^,。!?!?\n]{0,8}(?:一|两|三|几|多)?张(?!表(?:格)?)/u',
'/^\s*\/(?:image|imagine|draw)\b/iu',
];
$clauses = preg_split('/(?:但是|改为|改成|而是|然后|随后|并且|但|并)|[,。!?!?;\n]+/u', $content) ?: [$content];
$decision = false;
foreach ($clauses as $clause) {
if ($clause === '') {
continue;
}
if ($isCapabilityOnly($clause)
|| preg_match($negatedRequest, $clause)
|| preg_match($metaRequest, $clause)
|| preg_match($deferredRequest, $clause)) {
$decision = false;
continue;
}
foreach ($patterns as $pattern) {
if (preg_match($pattern, $clause)) {
$decision = true;
break;
}
}
}
return $decision;
}
}