feat: add guarded image generation controls
This commit is contained in:
+87
-5
@@ -84,6 +84,83 @@ const OBVIOUS_DIRECT_CHAT_PATTERNS = [
|
||||
/(?:继续|再).{0,8}(?:写|讲|说|编).{0,10}(?:诗|故事|笑话|散文)/u,
|
||||
];
|
||||
|
||||
export const IMAGE_GENERATION_MODE = {
|
||||
AUTO: 'auto',
|
||||
REQUIRED: 'required',
|
||||
DISABLED: 'disabled',
|
||||
};
|
||||
|
||||
const IMAGE_GENERATION_REQUIRED_PATTERNS = [
|
||||
/(?:生成|制作|创建|画|绘制|配|添加|使用).{0,16}(?:AI\s*)?(?:图片|图像|背景图|封面图|插画|照片|主图|配图)/iu,
|
||||
/(?:图片|图像|背景图|封面图|插画|照片|主图|配图).{0,16}(?:生成|制作|创建|画|绘制|作为|用作)/iu,
|
||||
/(?:全屏|页面|网页|卡片|信息流).{0,12}(?:背景|封面|主图).{0,12}(?:图片|图像|照片)/iu,
|
||||
/(?:AI配图|AI图片|真图|真实图片|生图)/iu,
|
||||
];
|
||||
|
||||
const IMAGE_GENERATION_DISABLED_PATTERNS = [
|
||||
/(?:不要|无需|不需要|禁止|关闭|取消).{0,12}(?:AI\s*)?(?:生图|图片生成|生成图片|配图)/iu,
|
||||
/(?:纯文字|只要文字|不要图片|不要配图|不用图片|不用配图)/iu,
|
||||
];
|
||||
|
||||
const VISUAL_PAGE_IMAGE_HINT_PATTERNS = [
|
||||
/(?:精美|沉浸式|视觉化|视觉效果|全屏背景|大唐盛景|海报风|摄影风)/u,
|
||||
/(?:旅行|美食|活动|品牌|产品|促销|诗词|唐诗).{0,16}(?:页面|网页|H5|h5|封面|背景)/u,
|
||||
];
|
||||
|
||||
export function normalizeImageGenerationMode(value) {
|
||||
const normalized = String(value ?? '').trim().toLowerCase();
|
||||
if (normalized === IMAGE_GENERATION_MODE.REQUIRED) return IMAGE_GENERATION_MODE.REQUIRED;
|
||||
if (normalized === IMAGE_GENERATION_MODE.DISABLED) return IMAGE_GENERATION_MODE.DISABLED;
|
||||
return IMAGE_GENERATION_MODE.AUTO;
|
||||
}
|
||||
|
||||
export function resolveImageGenerationDecision({ text, requestedMode = IMAGE_GENERATION_MODE.AUTO } = {}) {
|
||||
const normalizedText = String(text ?? '').trim();
|
||||
const normalizedMode = normalizeImageGenerationMode(requestedMode);
|
||||
if (normalizedMode === IMAGE_GENERATION_MODE.REQUIRED) {
|
||||
return { mode: IMAGE_GENERATION_MODE.REQUIRED, source: 'user_override', reason: '用户在输入区强制开启 AI 配图' };
|
||||
}
|
||||
if (normalizedMode === IMAGE_GENERATION_MODE.DISABLED) {
|
||||
return { mode: IMAGE_GENERATION_MODE.DISABLED, source: 'user_override', reason: '用户在输入区关闭 AI 配图' };
|
||||
}
|
||||
if (IMAGE_GENERATION_DISABLED_PATTERNS.some((pattern) => pattern.test(normalizedText))) {
|
||||
return { mode: IMAGE_GENERATION_MODE.DISABLED, source: 'intent', reason: '用户明确要求不生成图片' };
|
||||
}
|
||||
if (IMAGE_GENERATION_REQUIRED_PATTERNS.some((pattern) => pattern.test(normalizedText))) {
|
||||
return { mode: IMAGE_GENERATION_MODE.REQUIRED, source: 'intent', reason: '检测到明确的图片生成需求' };
|
||||
}
|
||||
if (
|
||||
isPageGenerationIntent(normalizedText)
|
||||
&& VISUAL_PAGE_IMAGE_HINT_PATTERNS.some((pattern) => pattern.test(normalizedText))
|
||||
) {
|
||||
return { mode: IMAGE_GENERATION_MODE.REQUIRED, source: 'intent', reason: '视觉类页面需要真实主图和缩略图源图' };
|
||||
}
|
||||
return { mode: IMAGE_GENERATION_MODE.AUTO, source: 'default', reason: '未检测到必须生成或禁止生成图片的要求' };
|
||||
}
|
||||
|
||||
function requestedImageGenerationMode(userMessage) {
|
||||
const metadata = userMessage?.metadata ?? {};
|
||||
const runMetadata = metadata.memindRun ?? metadata.agentRun ?? {};
|
||||
return normalizeImageGenerationMode(
|
||||
runMetadata.imageGenerationMode ?? runMetadata.image_generation_mode ?? metadata.imageGenerationMode,
|
||||
);
|
||||
}
|
||||
|
||||
function buildImageGenerationInstruction(decision) {
|
||||
if (decision?.mode === IMAGE_GENERATION_MODE.REQUIRED) {
|
||||
return [
|
||||
'图片策略:强制生成。必须先调用 sandbox-fs__generate_image,并使用返回的 workspaceRelativePath/publicUrl 作为页面真实主图或背景图,再生成 HTML 与缩略图。',
|
||||
'性能规则:页面头图、卡片封面和信息流缩略图只能共享一张主图。优先只调用一次 purpose=hero;hero 成功后必须复用该资产并裁剪派生,禁止再调用 card_cover 或 feed_cover。只有 hero 用途被后台关闭、且确实只需要对应封面时,才允许改用一个 card_cover 或 feed_cover 请求。正文插图 inline_image 仅在后台开启且正文确有需要时单独生成。',
|
||||
'本轮证据要求:必须在当前这一次执行中发起新的 generate_image 工具请求;历史对话中的成功或失败都不能作为本轮结果。禁止在没有新 toolRequest/toolResponse 的情况下复述“已尝试”“Provider 未启用”或其他旧结论。',
|
||||
'完成条件:返回结果必须 ok=true,包含 jobId,且产物为 PNG/JPEG/WebP;不得用 SVG、CSS 绘图、旧素材或网页搜索图片冒充本次生图。生图失败时必须明确报告失败,不得静默降级后宣称完成。',
|
||||
].join('\n');
|
||||
}
|
||||
if (decision?.mode === IMAGE_GENERATION_MODE.DISABLED) {
|
||||
return '图片策略:关闭。本轮禁止调用 generate_image;如需页面视觉,只能使用现有合法资源或纯 CSS,并如实说明未生成新图片。';
|
||||
}
|
||||
return '图片策略:自动。仅在用户需求确实需要新主图、背景图、正文配图或封面时调用 generate_image;不要为了装饰无条件生图。';
|
||||
}
|
||||
|
||||
/** Realtime / web-search prompts — fast-path to agent even when LLM router is enabled. */
|
||||
const REALTIME_INFO_AGENT_PATTERNS = [
|
||||
/(?:世界杯|欧冠|NBA|英超|西甲|意甲|德甲|法甲|欧洲杯|亚洲杯|奥运会).{0,16}(?:赛况|赛程|比分|战况|结果|积分|排名|进展|情况)/u,
|
||||
@@ -719,6 +796,7 @@ export function buildAgentOrchestrationAgentText({
|
||||
`路由判定:${classification.reason}`,
|
||||
classification.agentBrief ? `执行要点:${classification.agentBrief}` : '',
|
||||
classification.suggestedSkill ? `建议 skill:${classification.suggestedSkill}` : '',
|
||||
buildImageGenerationInstruction(classification.imageGeneration),
|
||||
skillPrompt,
|
||||
memoryLines.length
|
||||
? [
|
||||
@@ -1114,8 +1192,7 @@ export function createChatIntentRouter(options = {}) {
|
||||
if (!classification) return classification;
|
||||
const base = { ...classification };
|
||||
delete base.decision;
|
||||
return finalizeRouterClassification(
|
||||
coercePageGenerationSkill(
|
||||
const coerced = coercePageGenerationSkill(
|
||||
coercePageDataSkill(
|
||||
coerceRealtimeWebSkill(base, text, { grantedSkills }),
|
||||
text,
|
||||
@@ -1123,9 +1200,14 @@ export function createChatIntentRouter(options = {}) {
|
||||
),
|
||||
text,
|
||||
{ grantedSkills },
|
||||
),
|
||||
decisionContext,
|
||||
);
|
||||
);
|
||||
return finalizeRouterClassification({
|
||||
...coerced,
|
||||
imageGeneration: resolveImageGenerationDecision({
|
||||
text,
|
||||
requestedMode: requestedImageGenerationMode(userMessage),
|
||||
}),
|
||||
}, decisionContext);
|
||||
};
|
||||
const ruleResult = classifyWithRules({
|
||||
text,
|
||||
|
||||
Reference in New Issue
Block a user