feat: add guarded image generation controls

This commit is contained in:
john
2026-07-20 07:37:43 +08:00
parent 72ed4dde83
commit 105a72c1f1
19 changed files with 482 additions and 23 deletions
+87 -5
View File
@@ -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,