Compare commits
2 Commits
1ed3184000
...
dd4426ded7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd4426ded7 | ||
|
|
3ebe7db37d |
@ -863,6 +863,8 @@ type AiModel = {
|
||||
healthLastSuccessAt?: string | null;
|
||||
healthAutoDisabledAt?: string | null;
|
||||
healthAutoDisabledReason?: string | null;
|
||||
healthRecoverySuccessStreak?: number;
|
||||
healthObservationStartedAt?: string | null;
|
||||
isAgentModel?: boolean;
|
||||
agentCodingModelId?: string;
|
||||
isMultimodal?: boolean;
|
||||
@ -15677,6 +15679,19 @@ export function AdminSettingsClient() {
|
||||
supportsImageGen: model.supportsImageGen,
|
||||
});
|
||||
const healthStatus = model.healthStatus || "healthy";
|
||||
const isObservationPool =
|
||||
healthStatus === "degraded" &&
|
||||
Boolean(model.healthObservationStartedAt);
|
||||
const recoverySuccessStreak = Math.max(
|
||||
0,
|
||||
Number(model.healthRecoverySuccessStreak || 0)
|
||||
);
|
||||
const autoDisableCooldownUntil = model.healthAutoDisabledAt
|
||||
? new Date(
|
||||
new Date(model.healthAutoDisabledAt).getTime() +
|
||||
2 * 60 * 60 * 1000
|
||||
)
|
||||
: null;
|
||||
const modelHealthEvents =
|
||||
modelHealthEventsByModelId.get(model.id) || [];
|
||||
const latestHealthEvent = modelHealthEvents[0];
|
||||
@ -15756,7 +15771,9 @@ export function AdminSettingsClient() {
|
||||
)}
|
||||
{healthStatus === "degraded" && (
|
||||
<span className="text-xs bg-orange-100 text-orange-700 px-2 py-0.5 rounded">
|
||||
健康观察中
|
||||
{isObservationPool
|
||||
? `观察池 ${recoverySuccessStreak}/3`
|
||||
: "健康观察中"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@ -15784,6 +15801,38 @@ export function AdminSettingsClient() {
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
{healthStatus === "auto_disabled" &&
|
||||
autoDisableCooldownUntil && (
|
||||
<span>
|
||||
解冻时间:
|
||||
{formatBeijingTime(autoDisableCooldownUntil, {
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
{isObservationPool && (
|
||||
<span>
|
||||
观察成功:{recoverySuccessStreak}/3
|
||||
</span>
|
||||
)}
|
||||
{isObservationPool &&
|
||||
model.healthObservationStartedAt && (
|
||||
<span>
|
||||
观察开始:
|
||||
{formatBeijingTime(
|
||||
model.healthObservationStartedAt,
|
||||
{
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{(model.healthLastError || latestHealthEvent?.errorMessage) && (
|
||||
<div className="line-clamp-2 max-w-[760px]">
|
||||
|
||||
@ -6463,6 +6463,7 @@ export async function POST(request: Request) {
|
||||
})
|
||||
.filter(Boolean) as Array<{ role: "user" | "assistant"; content: string }>,
|
||||
context: effectiveAutoContextText || undefined,
|
||||
preferredModelId: preferredAgentModelId,
|
||||
}).catch(() =>
|
||||
optimizePrompt({
|
||||
originalPrompt: promptSeed,
|
||||
|
||||
@ -284,6 +284,30 @@ function defaultToolForAction(action: AgentPlanNextAction): AgentProjectToolName
|
||||
}
|
||||
}
|
||||
|
||||
function candidateToolsForAction(action: AgentPlanNextAction): AgentProjectToolName[] {
|
||||
switch (action) {
|
||||
case "analyze_image":
|
||||
return ["analyzeTargetImage", "analyzeImage"];
|
||||
case "edit_image":
|
||||
return ["editTargetImage", "editImage"];
|
||||
case "generate_image":
|
||||
return ["generateNewImage", "generateImage"];
|
||||
case "generate_video":
|
||||
return ["generateVideo"];
|
||||
case "clarify":
|
||||
case "respond":
|
||||
default:
|
||||
return ["respondDirect"];
|
||||
}
|
||||
}
|
||||
|
||||
function pickAvailableToolForAction(
|
||||
action: AgentPlanNextAction,
|
||||
availableNames: Set<AgentProjectToolName>,
|
||||
): AgentProjectToolName | undefined {
|
||||
return candidateToolsForAction(action).find((name) => availableNames.has(name));
|
||||
}
|
||||
|
||||
function mapIntentToAction(intent: AnalysisResult["intent"]): AgentPlanNextAction {
|
||||
switch (intent) {
|
||||
case "vision":
|
||||
@ -395,9 +419,26 @@ function sanitizePlannedToolCalls(
|
||||
});
|
||||
}
|
||||
|
||||
if (sanitized.length > 0) return sanitized;
|
||||
if (sanitized.length > 0) {
|
||||
const hasPrimaryActionTool = sanitized.some((tool) =>
|
||||
candidateToolsForAction(fallbackAction).includes(tool.name),
|
||||
);
|
||||
const fallbackTool = pickAvailableToolForAction(fallbackAction, availableNames);
|
||||
if (
|
||||
!hasPrimaryActionTool &&
|
||||
fallbackTool &&
|
||||
fallbackAction !== "respond" &&
|
||||
fallbackAction !== "clarify"
|
||||
) {
|
||||
sanitized.push({
|
||||
name: fallbackTool,
|
||||
reason: "补齐本轮主执行工具,避免只解析目标图后停住。",
|
||||
});
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
const fallbackTool = defaultToolForAction(fallbackAction);
|
||||
const fallbackTool = pickAvailableToolForAction(fallbackAction, availableNames) || defaultToolForAction(fallbackAction);
|
||||
if (
|
||||
(fallbackAction === "analyze_image" || fallbackAction === "edit_image") &&
|
||||
options?.targetSource &&
|
||||
@ -698,6 +739,59 @@ function normalizeAspectRatio(value?: string): string | undefined {
|
||||
return allowed.has(raw) ? raw : undefined;
|
||||
}
|
||||
|
||||
function parseChineseCountToken(token?: string): number | undefined {
|
||||
const normalized = String(token || "").trim().toLowerCase();
|
||||
if (!normalized) return undefined;
|
||||
const numeric = Number(normalized);
|
||||
if (Number.isFinite(numeric) && numeric >= 1) return Math.round(numeric);
|
||||
const map: Record<string, number> = {
|
||||
一: 1,
|
||||
二: 2,
|
||||
两: 2,
|
||||
三: 3,
|
||||
四: 4,
|
||||
五: 5,
|
||||
六: 6,
|
||||
七: 7,
|
||||
八: 8,
|
||||
九: 9,
|
||||
十: 10,
|
||||
};
|
||||
return map[normalized];
|
||||
}
|
||||
|
||||
function inferExplicitImageCountFromText(text: string): number | undefined {
|
||||
const normalized = String(text || "").toLowerCase().replace(/\s+/g, "");
|
||||
const match =
|
||||
normalized.match(/([1-9]\d?|一|二|两|三|四|五|六|七|八|九|十)张/) ||
|
||||
normalized.match(/([1-9]\d?|一|二|两|三|四|五|六|七|八|九|十)(?:个|组)?(?:版本|方案|变体)/);
|
||||
const parsed = parseChineseCountToken(match?.[1]);
|
||||
if (parsed && parsed >= 1) return Math.min(parsed, 12);
|
||||
if (/多张/.test(normalized)) return 3;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function inferExplicitAspectRatioFromText(text: string): string | undefined {
|
||||
const normalized = String(text || "").toLowerCase().replace(/\s+/g, "");
|
||||
const explicitRatio = normalized.match(/(21:9|16:9|9:16|5:4|4:5|4:3|3:4|3:2|2:3|1:1)/)?.[1];
|
||||
if (explicitRatio) return explicitRatio;
|
||||
if (/竖图|竖版|竖屏|手机壁纸|9比16/.test(normalized)) return "9:16";
|
||||
if (/横图|横版|横屏|宽屏|banner|16比9/.test(normalized)) return "16:9";
|
||||
if (/方图|方形|头像|logo|电商主图|1比1/.test(normalized)) return "1:1";
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function extractExplicitImageParamsFromText(
|
||||
text: string,
|
||||
): Pick<AgentRecommendedImageParams, "aspectRatio" | "imageCount"> {
|
||||
const output: Pick<AgentRecommendedImageParams, "aspectRatio" | "imageCount"> = {};
|
||||
const aspectRatio = inferExplicitAspectRatioFromText(text);
|
||||
const imageCount = inferExplicitImageCountFromText(text);
|
||||
if (aspectRatio) output.aspectRatio = aspectRatio;
|
||||
if (imageCount) output.imageCount = imageCount;
|
||||
return output;
|
||||
}
|
||||
|
||||
function normalizeImageSize(value?: string): string | undefined {
|
||||
if (!value) return undefined;
|
||||
const normalized = value.trim().toUpperCase();
|
||||
@ -964,9 +1058,8 @@ function heuristicImageParams(
|
||||
if (analysis.intent === "image_edit") {
|
||||
params.imageCount = 1;
|
||||
} else {
|
||||
const explicitCountMatch = normalized.match(/([1-9]\d?)张/);
|
||||
const explicitCount = explicitCountMatch ? Number(explicitCountMatch[1]) : 0;
|
||||
if (explicitCount >= 1) {
|
||||
const explicitCount = inferExplicitImageCountFromText(text);
|
||||
if (explicitCount && explicitCount >= 1) {
|
||||
params.imageCount = Math.min(explicitCount, 12);
|
||||
} else if (/多张|四张|4张|三张|3张/.test(normalized)) {
|
||||
params.imageCount = /四张|4张/.test(normalized) ? 4 : 3;
|
||||
@ -977,7 +1070,10 @@ function heuristicImageParams(
|
||||
}
|
||||
}
|
||||
|
||||
if (/横图|横版|宽屏|封面|banner|海报/.test(normalized)) {
|
||||
const explicitAspectRatio = inferExplicitAspectRatioFromText(text);
|
||||
if (explicitAspectRatio) {
|
||||
params.aspectRatio = explicitAspectRatio;
|
||||
} else if (/横图|横版|宽屏|封面|banner|海报/.test(normalized)) {
|
||||
params.aspectRatio = "16:9";
|
||||
} else if (/竖图|竖版|手机壁纸|全身|海报竖版/.test(normalized)) {
|
||||
params.aspectRatio = "9:16";
|
||||
@ -1313,6 +1409,59 @@ function sanitizeAgentPlan(
|
||||
};
|
||||
}
|
||||
|
||||
async function refinePlanWithSkillRoute(params: {
|
||||
text: string;
|
||||
analysis: AnalysisResult;
|
||||
plan: AgentExecutionPlan;
|
||||
availableTools: AgentProjectToolDefinition[];
|
||||
skillRoute?: AgentSkillRouteResult;
|
||||
hasImage: boolean;
|
||||
hasCanvasSelection: boolean;
|
||||
targetResolutionMode: AgentTargetResolutionMode;
|
||||
mediaSource: "current_message" | "canvas_selection" | "history" | "none";
|
||||
selectedImageCount: number;
|
||||
threadState: AgentThreadState | null;
|
||||
}): Promise<AgentExecutionPlan> {
|
||||
if (!params.skillRoute) return params.plan;
|
||||
|
||||
const { refinePlanWithSpecialist } = await import("./specialists");
|
||||
let refined = params.plan;
|
||||
for (const specialist of [params.skillRoute.primary, ...params.skillRoute.overlays]) {
|
||||
refined = refinePlanWithSpecialist(specialist, {
|
||||
text: params.text,
|
||||
analysis: params.analysis,
|
||||
plan: refined,
|
||||
availableTools: params.availableTools,
|
||||
hasImage: params.hasImage,
|
||||
hasCanvasSelection: params.hasCanvasSelection,
|
||||
targetResolutionMode: params.targetResolutionMode,
|
||||
mediaSource: params.mediaSource,
|
||||
selectedImageCount: params.selectedImageCount,
|
||||
threadState: params.threadState,
|
||||
});
|
||||
}
|
||||
return refined;
|
||||
}
|
||||
|
||||
function shouldUseLightweightPlanner(
|
||||
state: AutoAgentStateType,
|
||||
analysis: AnalysisResult,
|
||||
): boolean {
|
||||
if (process.env.AGENT_FORCE_LLM_PLANNER === "true") return false;
|
||||
if (analysis.needsClarification) return false;
|
||||
if (analysis.confidence < 0.9) return false;
|
||||
if (analysis.intent === "image_gen") return true;
|
||||
if (analysis.intent === "video_gen") return true;
|
||||
if (
|
||||
analysis.intent === "image_edit" &&
|
||||
state.targetResolutionMode !== "clarification" &&
|
||||
(state.hasImage || state.imageFromHistory || state.historyImageCount > 0 || state.selectedImageCount > 0)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function buildExecutionPlanWithAgent(
|
||||
state: AutoAgentStateType,
|
||||
analysis: AnalysisResult,
|
||||
@ -1328,6 +1477,28 @@ async function buildExecutionPlanWithAgent(
|
||||
availableTools,
|
||||
});
|
||||
|
||||
if (shouldUseLightweightPlanner(state, analysis)) {
|
||||
const refinedFallbackPlan = await refinePlanWithSkillRoute({
|
||||
text: state.text,
|
||||
analysis,
|
||||
plan: fallbackPlan,
|
||||
availableTools,
|
||||
skillRoute,
|
||||
hasImage: state.hasImage,
|
||||
hasCanvasSelection: state.hasCanvasSelection || state.selectedImageCount > 0,
|
||||
targetResolutionMode: state.targetResolutionMode,
|
||||
mediaSource: state.mediaSource,
|
||||
selectedImageCount: state.selectedImageCount,
|
||||
threadState: state.threadState,
|
||||
});
|
||||
console.log("[Auto Agent] Lightweight planner used:", {
|
||||
intent: analysis.intent,
|
||||
confidence: analysis.confidence,
|
||||
nextAction: refinedFallbackPlan.nextAction,
|
||||
});
|
||||
return refinedFallbackPlan;
|
||||
}
|
||||
|
||||
try {
|
||||
const {
|
||||
getEnabledAiModelsWithProvider,
|
||||
@ -1445,23 +1616,19 @@ async function buildExecutionPlanWithAgent(
|
||||
|
||||
const parsed = parseJsonBlock<Record<string, unknown>>(text);
|
||||
let planned = sanitizeAgentPlan(parsed, fallbackPlan, availableTools);
|
||||
if (skillRoute) {
|
||||
const { refinePlanWithSpecialist } = await import("./specialists");
|
||||
for (const specialist of [skillRoute.primary, ...skillRoute.overlays]) {
|
||||
planned = refinePlanWithSpecialist(specialist, {
|
||||
text: state.text,
|
||||
analysis,
|
||||
plan: planned,
|
||||
availableTools,
|
||||
hasImage: state.hasImage,
|
||||
hasCanvasSelection: state.hasCanvasSelection || state.selectedImageCount > 0,
|
||||
targetResolutionMode: state.targetResolutionMode,
|
||||
mediaSource: state.mediaSource,
|
||||
selectedImageCount: state.selectedImageCount,
|
||||
threadState: state.threadState,
|
||||
});
|
||||
}
|
||||
}
|
||||
planned = await refinePlanWithSkillRoute({
|
||||
text: state.text,
|
||||
analysis,
|
||||
plan: planned,
|
||||
availableTools,
|
||||
skillRoute,
|
||||
hasImage: state.hasImage,
|
||||
hasCanvasSelection: state.hasCanvasSelection || state.selectedImageCount > 0,
|
||||
targetResolutionMode: state.targetResolutionMode,
|
||||
mediaSource: state.mediaSource,
|
||||
selectedImageCount: state.selectedImageCount,
|
||||
threadState: state.threadState,
|
||||
});
|
||||
console.log(
|
||||
`[Auto Agent] Planner model: ${plannerModelConfig.model.displayName} (${plannerModelConfig.provider.type})`,
|
||||
);
|
||||
@ -2017,6 +2184,18 @@ const shouldOptimize = (state: AutoAgentStateType) => {
|
||||
return state.execution.shouldOptimizePrompt ? "optimize" : "route";
|
||||
};
|
||||
|
||||
function shouldUseLlmPromptOptimizer(state: AutoAgentStateType): boolean {
|
||||
if (!state.analysis || !state.execution?.shouldApplyImagePipeline) return false;
|
||||
if (process.env.AGENT_LLM_PROMPT_OPTIMIZER_ENABLED === "false") return false;
|
||||
if (
|
||||
state.analysis.intent === "image_gen" &&
|
||||
process.env.AGENT_IMAGE_GEN_LLM_OPTIMIZER_ENABLED !== "true"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const optimizeNode = async (state: AutoAgentStateType) => {
|
||||
if (!state.analysis || !state.execution) {
|
||||
return {};
|
||||
@ -2053,19 +2232,20 @@ const optimizeNode = async (state: AutoAgentStateType) => {
|
||||
return {};
|
||||
}
|
||||
const { optimizePrompt, optimizePromptWithLLM } = await import("./prompt-optimizer");
|
||||
const optimizedFromLlm = await withTimeoutFallback<
|
||||
Awaited<ReturnType<typeof optimizePromptWithLLM>> | null
|
||||
>(
|
||||
optimizePromptWithLLM({
|
||||
originalPrompt: state.text,
|
||||
intent: state.analysis.intent,
|
||||
subIntent: state.analysis.subIntent,
|
||||
historyMessages: state.historyMessages,
|
||||
context: state.context || undefined,
|
||||
}),
|
||||
AGENT_OPERATION_TIMEOUT_MS,
|
||||
null,
|
||||
);
|
||||
const optimizedFromLlm = shouldUseLlmPromptOptimizer(state)
|
||||
? await withTimeoutFallback<Awaited<ReturnType<typeof optimizePromptWithLLM>> | null>(
|
||||
optimizePromptWithLLM({
|
||||
originalPrompt: state.text,
|
||||
intent: state.analysis.intent,
|
||||
subIntent: state.analysis.subIntent,
|
||||
historyMessages: state.historyMessages,
|
||||
context: state.context || undefined,
|
||||
preferredModelId: state.preferredModelId || undefined,
|
||||
}),
|
||||
AGENT_OPERATION_TIMEOUT_MS,
|
||||
null,
|
||||
)
|
||||
: null;
|
||||
const optimized =
|
||||
optimizedFromLlm ||
|
||||
optimizePrompt({
|
||||
@ -2073,16 +2253,20 @@ const optimizeNode = async (state: AutoAgentStateType) => {
|
||||
intent: state.analysis.intent,
|
||||
subIntent: state.analysis.subIntent,
|
||||
});
|
||||
const heuristicParams = heuristicImageParams(state.text, state.analysis);
|
||||
const explicitTextParams = extractExplicitImageParamsFromText(state.text);
|
||||
const mergedParams = sanitizeRecommendedImageParams(
|
||||
{
|
||||
...heuristicParams,
|
||||
...optimized.recommendedImageParams,
|
||||
...explicitTextParams,
|
||||
negativePrompt:
|
||||
optimized.negativePrompt && optimized.negativePrompt.trim().length > 0
|
||||
? optimized.negativePrompt
|
||||
: optimized.recommendedImageParams?.negativePrompt,
|
||||
},
|
||||
state.analysis.intent,
|
||||
) ?? heuristicImageParams(state.text, state.analysis);
|
||||
) ?? heuristicParams;
|
||||
|
||||
return {
|
||||
optimizedPrompt: optimized.optimizedPrompt,
|
||||
|
||||
@ -139,6 +139,7 @@ export function resolveConversationAction(params: {
|
||||
confirmedPrompt?: string;
|
||||
hasImageInputForPromptCarryForward: boolean;
|
||||
}) {
|
||||
const normalizedText = String(params.text || "").toLowerCase().replace(/\s+/g, "");
|
||||
const requestedGrid = detectRequestedStoryboardGrid(params.text);
|
||||
const shouldContinuePendingGrid =
|
||||
Boolean(params.pendingGridConfirmation) &&
|
||||
@ -163,8 +164,16 @@ export function resolveConversationAction(params: {
|
||||
} satisfies ConversationActionResolution;
|
||||
}
|
||||
|
||||
const mentionsGenerationFollowup = mentionsImageGenerationFollowup(params.text);
|
||||
const explicitFreshImageGeneration =
|
||||
!/不要生图|先别生图|不需要生图|不要生成|别生成/.test(normalizedText) &&
|
||||
/生图|出图|生成.*(?:图|图片|海报|头像|插画|场景|主图|产品图)|画.*(?:图|图片|海报|头像|插画|小猫|猫咪|人物|角色)|做.*(?:图|图片|海报|头像|主图|产品图)/.test(
|
||||
normalizedText,
|
||||
);
|
||||
const hasImageEditFollowup =
|
||||
params.imageWorkflowMemory?.lastIntent === "image_edit" &&
|
||||
!mentionsGenerationFollowup &&
|
||||
!explicitFreshImageGeneration &&
|
||||
(mentionsImageEditFollowup(params.text) ||
|
||||
Boolean(params.imageWorkflowMemory?.lastResultImageUrls?.length));
|
||||
|
||||
@ -187,7 +196,7 @@ export function resolveConversationAction(params: {
|
||||
|
||||
const hasImageGenerationFollowup =
|
||||
params.imageWorkflowMemory?.lastIntent === "image_gen" &&
|
||||
(mentionsImageGenerationFollowup(params.text) || promptCarryForwardRequested);
|
||||
(mentionsGenerationFollowup || promptCarryForwardRequested);
|
||||
|
||||
if (hasImageGenerationFollowup) {
|
||||
return {
|
||||
|
||||
@ -62,6 +62,8 @@ export function buildImageEditSpecialistRoute(params: {
|
||||
planningHints,
|
||||
toolAllowlist: [
|
||||
"respondDirect",
|
||||
"resolveTargetImage",
|
||||
"editTargetImage",
|
||||
"editImage",
|
||||
"removeBackground",
|
||||
"enhanceImage",
|
||||
@ -71,6 +73,7 @@ export function buildImageEditSpecialistRoute(params: {
|
||||
"useSelectedCanvasImage",
|
||||
"useLatestResultImage",
|
||||
"useOriginalUploadImage",
|
||||
"publishResultToCanvas",
|
||||
"publishToCanvas",
|
||||
],
|
||||
};
|
||||
@ -111,7 +114,14 @@ export function refinePlanWithImageEditSubagent(params: {
|
||||
}
|
||||
|
||||
let toolCalls = [...params.plan.toolCalls];
|
||||
if (hasTool(params.availableTools, "useLatestResultImage")) {
|
||||
if (hasTool(params.availableTools, "resolveTargetImage")) {
|
||||
toolCalls = upsertTool(
|
||||
toolCalls,
|
||||
"resolveTargetImage",
|
||||
"image-edit 子代理优先绑定上一轮结果图作为连续修图目标",
|
||||
{ prepend: true }
|
||||
);
|
||||
} else if (hasTool(params.availableTools, "useLatestResultImage")) {
|
||||
toolCalls = upsertTool(
|
||||
toolCalls,
|
||||
"useLatestResultImage",
|
||||
@ -119,15 +129,26 @@ export function refinePlanWithImageEditSubagent(params: {
|
||||
{ prepend: true }
|
||||
);
|
||||
}
|
||||
if (hasTool(params.availableTools, "editImage")) {
|
||||
if (hasTool(params.availableTools, "editTargetImage")) {
|
||||
toolCalls = upsertTool(
|
||||
toolCalls,
|
||||
"editTargetImage",
|
||||
"image-edit 子代理要求优先走连续修图执行链路"
|
||||
);
|
||||
} else if (hasTool(params.availableTools, "editImage")) {
|
||||
toolCalls = upsertTool(
|
||||
toolCalls,
|
||||
"editImage",
|
||||
"image-edit 子代理要求优先走连续修图执行链路",
|
||||
{ prepend: true }
|
||||
"image-edit 子代理要求优先走连续修图执行链路"
|
||||
);
|
||||
}
|
||||
if (hasTool(params.availableTools, "publishToCanvas")) {
|
||||
if (hasTool(params.availableTools, "publishResultToCanvas")) {
|
||||
toolCalls = upsertTool(
|
||||
toolCalls,
|
||||
"publishResultToCanvas",
|
||||
"image-edit 子代理建议把修图结果同步回画布"
|
||||
);
|
||||
} else if (hasTool(params.availableTools, "publishToCanvas")) {
|
||||
toolCalls = upsertTool(
|
||||
toolCalls,
|
||||
"publishToCanvas",
|
||||
|
||||
@ -85,10 +85,13 @@ export function buildImageGenerationSpecialistRoute(params: {
|
||||
planningHints,
|
||||
toolAllowlist: [
|
||||
"respondDirect",
|
||||
"resolveTargetImage",
|
||||
"generateNewImage",
|
||||
"generateImage",
|
||||
"useSelectedCanvasImage",
|
||||
"useLatestResultImage",
|
||||
"useOriginalUploadImage",
|
||||
"publishResultToCanvas",
|
||||
"publishToCanvas",
|
||||
],
|
||||
};
|
||||
@ -137,10 +140,29 @@ export function refinePlanWithImageGenerationSubagent(params: {
|
||||
}
|
||||
|
||||
let toolCalls = [...params.plan.toolCalls];
|
||||
if (hasTool(params.availableTools, "generateImage")) {
|
||||
toolCalls = [{ name: "generateImage", reason: "image-generation 子代理要求优先走连续生图执行链路" }];
|
||||
const targetResolverTools = toolCalls.filter((tool) =>
|
||||
["resolveTargetImage", "useSelectedCanvasImage", "useLatestResultImage", "useOriginalUploadImage"].includes(
|
||||
tool.name,
|
||||
),
|
||||
);
|
||||
if (hasTool(params.availableTools, "generateNewImage")) {
|
||||
toolCalls = [
|
||||
...targetResolverTools,
|
||||
{ name: "generateNewImage", reason: "image-generation 子代理要求优先走连续生图执行链路" },
|
||||
];
|
||||
} else if (hasTool(params.availableTools, "generateImage")) {
|
||||
toolCalls = [
|
||||
...targetResolverTools,
|
||||
{ name: "generateImage", reason: "image-generation 子代理要求优先走连续生图执行链路" },
|
||||
];
|
||||
}
|
||||
if (hasTool(params.availableTools, "publishToCanvas")) {
|
||||
if (hasTool(params.availableTools, "publishResultToCanvas")) {
|
||||
toolCalls = upsertTool(
|
||||
toolCalls,
|
||||
"publishResultToCanvas",
|
||||
"image-generation 子代理建议把连续生图结果同步到画布"
|
||||
);
|
||||
} else if (hasTool(params.availableTools, "publishToCanvas")) {
|
||||
toolCalls = upsertTool(
|
||||
toolCalls,
|
||||
"publishToCanvas",
|
||||
|
||||
@ -579,8 +579,9 @@ export async function optimizePromptWithLLM(params: {
|
||||
subIntent?: string;
|
||||
historyMessages?: HistoryMessage[];
|
||||
context?: string;
|
||||
preferredModelId?: string;
|
||||
}): Promise<OptimizeResult> {
|
||||
const { originalPrompt, intent, subIntent, historyMessages, context } = params;
|
||||
const { originalPrompt, intent, subIntent, historyMessages, context, preferredModelId } = params;
|
||||
|
||||
console.log("[Prompt Optimizer LLM] Input:", {
|
||||
originalPrompt,
|
||||
@ -611,6 +612,16 @@ export async function optimizePromptWithLLM(params: {
|
||||
const agentModel = await getAgentAiModelWithProvider();
|
||||
const isReasoningCompatibleProvider = (providerType: string) =>
|
||||
["zhipu", "alibaba", "openai", "custom", "local", "volcano-multimodal", "volcano-vision"].includes(providerType);
|
||||
const normalizedPreferredModelId = String(preferredModelId || "").trim();
|
||||
const preferredOptimizerModel = normalizedPreferredModelId
|
||||
? allModels.find(
|
||||
(m) =>
|
||||
(m.model.id === normalizedPreferredModelId || m.model.modelId === normalizedPreferredModelId) &&
|
||||
m.model.isEnabled &&
|
||||
!m.model.isSubModel &&
|
||||
isReasoningCompatibleProvider(m.provider.type)
|
||||
)
|
||||
: undefined;
|
||||
const usableAgentModel =
|
||||
agentModel && isReasoningCompatibleProvider(agentModel.provider.type)
|
||||
? agentModel
|
||||
@ -621,7 +632,7 @@ export async function optimizePromptWithLLM(params: {
|
||||
const qwenModel = allModels.find(
|
||||
(m) => m.provider.type === "alibaba" && m.model.supportsVision
|
||||
);
|
||||
const selectedOptimizerModel = usableAgentModel || zhipuModel || qwenModel;
|
||||
const selectedOptimizerModel = preferredOptimizerModel || usableAgentModel || zhipuModel || qwenModel;
|
||||
const agentCodingModelId =
|
||||
selectedOptimizerModel &&
|
||||
usableAgentModel &&
|
||||
@ -677,6 +688,7 @@ export async function optimizePromptWithLLM(params: {
|
||||
prompt: finalUserMessage,
|
||||
temperature: getCompatibleTextGenerationTemperature(selectedOptimizerModel, 0.2),
|
||||
maxTokens: 200,
|
||||
abortSignal: AbortSignal.timeout(12_000),
|
||||
} as any);
|
||||
|
||||
// 解析 JSON 响应
|
||||
|
||||
@ -78,14 +78,42 @@ function isExplicitVideoGenerationRequest(text: string): boolean {
|
||||
function isExplicitImageGenerationRequest(text: string): boolean {
|
||||
const normalizedText = normalizeCompactText(text);
|
||||
if (!normalizedText || isLikelyAdvisoryOrPlanningRequest(text)) return false;
|
||||
return /重新生成|再来一张|生成新图|新建图片|创作一张|生成一张|画一张|给我画|帮我画|出一张|做一张图|做一张海报|创建一张图片|参考.*生成|按这个风格生成|以这张图为灵感生成|仿照.*生成|生成一只|生成一个|生成一幅|生成一版|生成一套|生成一组|帮我生成.*(?:图|图片|海报|头像|插画|场景|小猫|猫咪|人物|产品图)|给我生成.*(?:图|图片|海报|头像|插画|场景|小猫|猫咪|人物|产品图)/.test(
|
||||
normalizedText,
|
||||
if (/不要生图|先别生图|不需要生图|不要生成|别生成|只要提示词|仅给提示词|先给提示词/.test(normalizedText)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const countToken = "(?:一|两|二|三|四|五|六|七|八|九|十|[1-9]\\d?)";
|
||||
const visualTarget =
|
||||
"(?:图|图片|海报|头像|插画|场景|主图|产品图|商品图|电商图|壁纸|封面|logo|图标)";
|
||||
const createVisualTarget = new RegExp(
|
||||
`(?:生成|创作|创建|制作|设计|做|出|画|绘制)${countToken}?张?.{0,32}${visualTarget}`,
|
||||
);
|
||||
const drawSubject = new RegExp(
|
||||
`(?:画|绘制|生成|创作|创建)${countToken}?(?:只|个|位|幅|张)?.{0,24}(?:小猫|猫咪|猫|狗|人物|角色|ip|头像|logo|图标|插画)`,
|
||||
);
|
||||
const continueVariants = new RegExp(
|
||||
`(?:再来|再出|再做|再给我|补|多来|继续生成|继续出)${countToken}?张`,
|
||||
);
|
||||
|
||||
return (
|
||||
/重新生成|生成新图|新建图片|创作一张|生成一张|画一张|给我画|帮我画|出一张|做一张图|做一张海报|创建一张图片|生成一只|生成一个|生成一幅|生成一版|生成一套|生成一组|生图|出图/.test(
|
||||
normalizedText,
|
||||
) ||
|
||||
createVisualTarget.test(normalizedText) ||
|
||||
drawSubject.test(normalizedText) ||
|
||||
continueVariants.test(normalizedText) ||
|
||||
/参考.*生成|按这个风格生成|以这张图为灵感生成|仿照.*生成|同风格再来|同样风格再来/.test(
|
||||
normalizedText,
|
||||
) ||
|
||||
/帮我生成.*(?:图|图片|海报|头像|插画|场景|小猫|猫咪|人物|产品图)|给我生成.*(?:图|图片|海报|头像|插画|场景|小猫|猫咪|人物|产品图)/.test(
|
||||
normalizedText,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function isLikelyImageEditRequest(text: string): boolean {
|
||||
const normalizedText = String(text || "").toLowerCase();
|
||||
return /抠图|去背景|透明背景|去背|扣图|移除背景|换背景|改背景|背景换成|背景改成|白底|纯色背景|替换.*背景|变清晰|高清|锐化|降噪|提升质量|修复|清晰化|扩展|拓展|延伸|扩图|outpainting|拓宽|扩大|去除|消除|擦掉|删除|移除|p掉|重绘|风格化|卡通化|动漫风|油画风|转.*风格|变.*风格|改.*风格|调色|调整颜色|亮度|对比度|饱和度|色调|曝光|滤镜|改成.*色|换成.*色|变成.*色|颜色改成|颜色换成|颜色变成|缩放|放大|缩小|调整尺寸|裁剪|resize|crop|修图|p图|改图|调图|编辑.*图|优化.*图|重做|再调整|继续改|继续调/.test(
|
||||
return /抠图|去背景|透明背景|去背|扣图|移除背景|换背景|改背景|背景换成|背景改成|白底|纯色背景|替换.*背景|变清晰|高清|锐化|降噪|提升质量|修复|清晰化|扩展|拓展|延伸|扩图|outpainting|拓宽|扩大|换成.*(?:比例|竖版|横版|竖图|横图|9:16|16:9|1:1)|改成.*(?:比例|竖版|横版|竖图|横图|9:16|16:9|1:1)|比例|画幅|竖版|横版|去除|消除|擦掉|删除|移除|p掉|重绘|风格化|卡通化|动漫风|油画风|转.*风格|变.*风格|改.*风格|调色|调整颜色|亮度|对比度|饱和度|色调|曝光|滤镜|改成.*色|换成.*色|变成.*色|颜色改成|颜色换成|颜色变成|缩放|放大|缩小|调整尺寸|裁剪|resize|crop|修图|p图|改图|调图|编辑.*图|优化.*图|重做|再调整|继续改|继续调/.test(
|
||||
normalizedText,
|
||||
);
|
||||
}
|
||||
@ -94,6 +122,7 @@ function inferDefaultImageEditSubIntent(text: string): string {
|
||||
const lowerText = String(text || "").toLowerCase();
|
||||
if (/抠图|去背景|透明背景|去背|扣图|移除背景/.test(lowerText)) return "remove_background";
|
||||
if (/换背景|改背景|背景换成|背景改成|白底|透明底/.test(lowerText)) return "replace_background";
|
||||
if (/扩展|拓展|延伸|扩图|outpainting|拓宽|扩大|比例|画幅|竖版|横版|竖图|横图|9:16|16:9|1:1/.test(lowerText)) return "extend";
|
||||
if (/风格|卡通|动漫|油画|像素风|写实|插画/.test(lowerText)) return "style_transfer";
|
||||
if (/颜色|调色|亮度|对比度|饱和度|改成.*色|换成.*色/.test(lowerText)) return "color_adjust";
|
||||
if (/去除|消除|擦掉|删除|移除|p掉|重绘/.test(lowerText)) return "inpaint";
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import "server-only";
|
||||
|
||||
import { and, desc, eq, sql } from "drizzle-orm";
|
||||
import { and, desc, eq, inArray, lte, sql } from "drizzle-orm";
|
||||
import { db } from "@/lib/db";
|
||||
import {
|
||||
aiModel,
|
||||
@ -20,6 +20,8 @@ export type AiModelHealthEventType =
|
||||
export type AiModelHealthSeverity = "info" | "warning" | "error" | "critical";
|
||||
|
||||
const AUTO_DISABLE_FAILURE_THRESHOLD = 3;
|
||||
const AUTO_DISABLE_COOLDOWN_MS = 2 * 60 * 60 * 1000;
|
||||
const OBSERVATION_RECOVERY_SUCCESS_THRESHOLD = 3;
|
||||
const RECENT_EVENT_LIMIT = 80;
|
||||
|
||||
function normalizeText(value: unknown) {
|
||||
@ -162,23 +164,157 @@ function buildMetadata(value?: Record<string, unknown>) {
|
||||
}
|
||||
}
|
||||
|
||||
function isObservationPoolModel(
|
||||
model:
|
||||
| Pick<
|
||||
AiModel,
|
||||
| "healthStatus"
|
||||
| "healthObservationStartedAt"
|
||||
>
|
||||
| null
|
||||
| undefined
|
||||
) {
|
||||
return (
|
||||
model?.healthStatus === "degraded" &&
|
||||
Boolean(model.healthObservationStartedAt)
|
||||
);
|
||||
}
|
||||
|
||||
function getAutoDisableCooldownCutoff(now = new Date()) {
|
||||
return new Date(now.getTime() - AUTO_DISABLE_COOLDOWN_MS);
|
||||
}
|
||||
|
||||
export async function reviveAiModelsFromCooldown(params?: { modelId?: string }) {
|
||||
const now = new Date();
|
||||
const cooldownCutoff = getAutoDisableCooldownCutoff(now);
|
||||
const conditions = [
|
||||
eq(aiModel.healthStatus, "auto_disabled"),
|
||||
eq(aiModel.isEnabled, false),
|
||||
eq(aiModel.excludeFromFailover, false),
|
||||
lte(aiModel.healthAutoDisabledAt, cooldownCutoff),
|
||||
];
|
||||
|
||||
if (params?.modelId) {
|
||||
conditions.push(eq(aiModel.id, normalizeText(params.modelId)));
|
||||
}
|
||||
|
||||
const dueModels = await db
|
||||
.select({
|
||||
id: aiModel.id,
|
||||
modelId: aiModel.modelId,
|
||||
displayName: aiModel.displayName,
|
||||
})
|
||||
.from(aiModel)
|
||||
.where(and(...conditions));
|
||||
|
||||
if (dueModels.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const revived = await db
|
||||
.update(aiModel)
|
||||
.set({
|
||||
isEnabled: true,
|
||||
healthStatus: "degraded",
|
||||
healthConsecutiveFailures: 0,
|
||||
healthRecoverySuccessStreak: 0,
|
||||
healthObservationStartedAt: now,
|
||||
healthUpdatedAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
.where(
|
||||
inArray(
|
||||
aiModel.id,
|
||||
dueModels.map((item) => item.id)
|
||||
)
|
||||
)
|
||||
.returning();
|
||||
|
||||
if (revived.length > 0) {
|
||||
console.info("[AI Model Health] Revived cooled-down models into observation pool:", {
|
||||
count: revived.length,
|
||||
models: revived.map((item) => ({
|
||||
id: item.id,
|
||||
modelId: item.modelId,
|
||||
displayName: item.displayName,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
return revived;
|
||||
}
|
||||
|
||||
export async function recordAiModelHealthSuccess(params: {
|
||||
model?: Pick<AiModel, "id" | "healthStatus"> | null;
|
||||
model?:
|
||||
| Pick<
|
||||
AiModel,
|
||||
| "id"
|
||||
| "healthStatus"
|
||||
| "healthRecoverySuccessStreak"
|
||||
| "healthObservationStartedAt"
|
||||
>
|
||||
| null;
|
||||
}) {
|
||||
const modelId = normalizeText(params.model?.id);
|
||||
if (!modelId) return;
|
||||
|
||||
const [existing] = await db
|
||||
.select({
|
||||
id: aiModel.id,
|
||||
healthStatus: aiModel.healthStatus,
|
||||
healthRecoverySuccessStreak: aiModel.healthRecoverySuccessStreak,
|
||||
healthObservationStartedAt: aiModel.healthObservationStartedAt,
|
||||
})
|
||||
.from(aiModel)
|
||||
.where(eq(aiModel.id, modelId))
|
||||
.limit(1);
|
||||
|
||||
if (!existing) return;
|
||||
|
||||
const now = new Date();
|
||||
const isObservation = isObservationPoolModel(existing);
|
||||
const nextRecoverySuccessStreak = isObservation
|
||||
? (existing.healthRecoverySuccessStreak || 0) + 1
|
||||
: 0;
|
||||
const recoveredToHealthy =
|
||||
isObservation &&
|
||||
nextRecoverySuccessStreak >= OBSERVATION_RECOVERY_SUCCESS_THRESHOLD;
|
||||
|
||||
const updateData: Partial<AiModel> = {
|
||||
healthStatus:
|
||||
existing.healthStatus === "auto_disabled"
|
||||
? "auto_disabled"
|
||||
: recoveredToHealthy
|
||||
? "healthy"
|
||||
: isObservation
|
||||
? "degraded"
|
||||
: "healthy",
|
||||
healthConsecutiveFailures: 0,
|
||||
healthLastEventType: "success",
|
||||
healthLastError: null,
|
||||
healthLastSuccessAt: now,
|
||||
healthRecoverySuccessStreak: recoveredToHealthy
|
||||
? 0
|
||||
: isObservation
|
||||
? nextRecoverySuccessStreak
|
||||
: 0,
|
||||
healthObservationStartedAt: recoveredToHealthy
|
||||
? null
|
||||
: isObservation
|
||||
? existing.healthObservationStartedAt
|
||||
: null,
|
||||
healthUpdatedAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
if (recoveredToHealthy) {
|
||||
updateData.healthAutoDisabledAt = null;
|
||||
updateData.healthAutoDisabledReason = null;
|
||||
}
|
||||
|
||||
await db
|
||||
.update(aiModel)
|
||||
.set({
|
||||
healthStatus:
|
||||
params.model?.healthStatus === "auto_disabled" ? "auto_disabled" : "healthy",
|
||||
healthConsecutiveFailures: 0,
|
||||
healthLastEventType: "success",
|
||||
healthLastError: null,
|
||||
healthLastSuccessAt: new Date(),
|
||||
healthUpdatedAt: new Date(),
|
||||
})
|
||||
.set(updateData)
|
||||
.where(eq(aiModel.id, modelId));
|
||||
}
|
||||
|
||||
@ -205,10 +341,15 @@ export async function recordAiModelHealthFailure(params: {
|
||||
const [existing] = await db
|
||||
.select({
|
||||
id: aiModel.id,
|
||||
modelId: aiModel.modelId,
|
||||
displayName: aiModel.displayName,
|
||||
isEnabled: aiModel.isEnabled,
|
||||
excludeFromFailover: aiModel.excludeFromFailover,
|
||||
healthStatus: aiModel.healthStatus,
|
||||
healthConsecutiveFailures: aiModel.healthConsecutiveFailures,
|
||||
healthRecoverySuccessStreak: aiModel.healthRecoverySuccessStreak,
|
||||
healthObservationStartedAt: aiModel.healthObservationStartedAt,
|
||||
healthAutoDisabledAt: aiModel.healthAutoDisabledAt,
|
||||
})
|
||||
.from(aiModel)
|
||||
.where(eq(aiModel.id, modelId))
|
||||
@ -216,14 +357,19 @@ export async function recordAiModelHealthFailure(params: {
|
||||
|
||||
if (!existing) return;
|
||||
|
||||
const isObservation = isObservationPoolModel(existing);
|
||||
const nextConsecutiveFailures = classification.counted
|
||||
? Math.max(1, (existing.healthConsecutiveFailures || 0) + 1)
|
||||
? Math.max(
|
||||
1,
|
||||
(isObservation ? 0 : existing.healthConsecutiveFailures || 0) + 1
|
||||
)
|
||||
: existing.healthConsecutiveFailures || 0;
|
||||
const shouldAutoDisable =
|
||||
classification.counted &&
|
||||
!existing.excludeFromFailover &&
|
||||
existing.healthStatus !== "auto_disabled" &&
|
||||
nextConsecutiveFailures >= AUTO_DISABLE_FAILURE_THRESHOLD;
|
||||
((isObservation && existing.isEnabled) ||
|
||||
(existing.healthStatus !== "auto_disabled" &&
|
||||
nextConsecutiveFailures >= AUTO_DISABLE_FAILURE_THRESHOLD));
|
||||
const nextStatus = shouldAutoDisable
|
||||
? "auto_disabled"
|
||||
: classification.counted
|
||||
@ -264,13 +410,23 @@ export async function recordAiModelHealthFailure(params: {
|
||||
healthLastEventType: classification.eventType,
|
||||
healthLastError: errorMessage,
|
||||
healthLastFailureAt: now,
|
||||
healthRecoverySuccessStreak: shouldAutoDisable
|
||||
? 0
|
||||
: classification.counted
|
||||
? 0
|
||||
: existing.healthRecoverySuccessStreak || 0,
|
||||
healthObservationStartedAt: shouldAutoDisable
|
||||
? null
|
||||
: isObservation
|
||||
? existing.healthObservationStartedAt
|
||||
: null,
|
||||
healthUpdatedAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
if (shouldAutoDisable) {
|
||||
updateData.healthAutoDisabledAt = now;
|
||||
updateData.healthAutoDisabledReason = autoDisabledReason || errorMessage;
|
||||
} else if (existing.healthStatus !== "auto_disabled") {
|
||||
} else if (existing.healthStatus !== "auto_disabled" && !isObservation) {
|
||||
updateData.healthAutoDisabledReason = null;
|
||||
}
|
||||
|
||||
@ -304,6 +460,8 @@ export async function restoreAiModelHealth(modelId: string) {
|
||||
healthLastFailureAt: null,
|
||||
healthAutoDisabledAt: null,
|
||||
healthAutoDisabledReason: null,
|
||||
healthRecoverySuccessStreak: 0,
|
||||
healthObservationStartedAt: null,
|
||||
healthUpdatedAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
|
||||
@ -54,9 +54,33 @@ function isLikelyVideoExecutionRequest(text: string): boolean {
|
||||
function isLikelyImageExecutionRequest(text: string): boolean {
|
||||
const normalized = normalizeCompactText(text);
|
||||
if (!normalized) return false;
|
||||
if (/不要生图|先别生图|不需要生图|不要生成|别生成|只要提示词|仅给提示词|先给提示词/.test(normalized)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return /生图|生成图片|出图|画一张|帮我画|做图|做一张图|重新生成|再来一张|执行生成|开始生成|生成出来|生成一只|生成一个|生成一张|生成一幅|生成一版|帮我生成.*(?:图|图片|海报|头像|插画|场景|小猫|猫咪|人物|产品图)|给我生成.*(?:图|图片|海报|头像|插画|场景|小猫|猫咪|人物|产品图)/.test(
|
||||
normalized,
|
||||
const countToken = "(?:一|两|二|三|四|五|六|七|八|九|十|[1-9]\\d?)";
|
||||
const visualTarget =
|
||||
"(?:图|图片|海报|头像|插画|场景|主图|产品图|商品图|电商图|壁纸|封面|logo|图标)";
|
||||
const createVisualTarget = new RegExp(
|
||||
`(?:生成|创作|创建|制作|设计|做|出|画|绘制)${countToken}?张?.{0,32}${visualTarget}`,
|
||||
);
|
||||
const drawSubject = new RegExp(
|
||||
`(?:画|绘制|生成|创作|创建)${countToken}?(?:只|个|位|幅|张)?.{0,24}(?:小猫|猫咪|猫|狗|人物|角色|ip|头像|logo|图标|插画)`,
|
||||
);
|
||||
const continueVariants = new RegExp(
|
||||
`(?:再来|再出|再做|再给我|补|多来|继续生成|继续出)${countToken}?张`,
|
||||
);
|
||||
|
||||
return (
|
||||
/生图|生成图片|出图|画一张|帮我画|做图|做一张图|重新生成|再来一张|执行生成|开始生成|生成出来|生成一只|生成一个|生成一张|生成一幅|生成一版|帮我生成.*(?:图|图片|海报|头像|插画|场景|小猫|猫咪|人物|产品图)|给我生成.*(?:图|图片|海报|头像|插画|场景|小猫|猫咪|人物|产品图)/.test(
|
||||
normalized,
|
||||
) ||
|
||||
createVisualTarget.test(normalized) ||
|
||||
drawSubject.test(normalized) ||
|
||||
continueVariants.test(normalized) ||
|
||||
/参考.*生成|按这个风格生成|以这张图为灵感生成|仿照.*生成|同风格再来|同样风格再来/.test(
|
||||
normalized,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -33,6 +33,7 @@ function inferDefaultImageEditSubIntent(text: string): AnalysisResult["subIntent
|
||||
const lowerText = text.toLowerCase();
|
||||
if (/抠图|去背景|透明背景|去背|扣图|移除背景/.test(lowerText)) return "remove_background";
|
||||
if (/换背景|改背景|背景换成|背景改成|白底|透明底/.test(lowerText)) return "replace_background";
|
||||
if (/扩展|拓展|延伸|扩图|outpainting|拓宽|扩大|比例|画幅|竖版|横版|竖图|横图|9:16|16:9|1:1/.test(lowerText)) return "extend";
|
||||
if (/风格|卡通|动漫|油画|像素风|写实|插画/.test(lowerText)) return "style_transfer";
|
||||
if (/颜色|调色|亮度|对比度|饱和度|改成.*色|换成.*色/.test(lowerText)) return "color_adjust";
|
||||
if (/去除|消除|擦掉|删除|移除|p掉|重绘/.test(lowerText)) return "inpaint";
|
||||
@ -65,11 +66,11 @@ export function isExplicitPromptOptimizationRequest(text: string): boolean {
|
||||
if (alsoRequestsExecution) return false;
|
||||
|
||||
const asksAnalysis =
|
||||
/分析|看图|看看|识别|拆解|提炼|整理|归纳|总结|描述|写出|给我|输出|优化|改写/.test(
|
||||
/分析|看图|看看|识别|拆解|提炼|整理|归纳|总结|描述|写|撰写|写出|给我|输出|优化|改写/.test(
|
||||
normalizedText,
|
||||
);
|
||||
const asksPromptQuestion =
|
||||
/提示词应该是什么|什么提示词|提示词是什么|怎么写提示词|如何写提示词|怎么描述|如何描述|怎么表达|如何表达|给我一版提示词|给我一套提示词|适合.*提示词|用于生图的提示词|生成这样的图片.*提示词/.test(
|
||||
/提示词应该是什么|什么提示词|提示词是什么|怎么写提示词|如何写提示词|怎么描述|如何描述|怎么表达|如何表达|给我一版提示词|给我一套提示词|帮我写.*提示词|写一版.*提示词|适合.*提示词|用于生图.*提示词|生成这样的图片.*提示词/.test(
|
||||
normalizedText,
|
||||
);
|
||||
const blocksExecution =
|
||||
@ -95,17 +96,48 @@ function isExplicitVideoGenerationRequest(text: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function isExplicitImageGenerationRequest(text: string): boolean {
|
||||
export function isExplicitImageGenerationRequest(text: string): boolean {
|
||||
const normalizedText = normalizeCompactText(text);
|
||||
if (isLikelyAdvisoryOrPlanningRequest(text)) return false;
|
||||
return /重新生成|再来一张|生成新图|新建图片|创作一张|生成一张|画一张|给我画|帮我画|出一张|做一张图|做一张海报|创建一张图片|参考.*生成|按这个风格生成|以这张图为灵感生成|仿照.*生成|生成一只|生成一个|生成一幅|生成一版|生成一套|生成一组|帮我生成.*(?:图|图片|海报|头像|插画|场景|小猫|猫咪|人物|产品图)|给我生成.*(?:图|图片|海报|头像|插画|场景|小猫|猫咪|人物|产品图)/.test(
|
||||
normalizedText,
|
||||
if (isExplicitPromptOptimizationRequest(text)) return false;
|
||||
if (/不要生图|先别生图|不需要生图|不要生成|别生成|只要提示词|仅给提示词|先给提示词/.test(normalizedText)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const countToken = "(?:一|两|二|三|四|五|六|七|八|九|十|[1-9]\\d?)";
|
||||
const visualTarget =
|
||||
"(?:图|图片|海报|头像|插画|场景|主图|产品图|商品图|电商图|壁纸|封面|logo|图标)";
|
||||
const createVisualTarget = new RegExp(
|
||||
`(?:生成|创作|创建|制作|设计|做|出|画|绘制)${countToken}?张?.{0,32}${visualTarget}`,
|
||||
);
|
||||
const drawSubject = new RegExp(
|
||||
`(?:画|绘制|生成|创作|创建)${countToken}?(?:只|个|位|幅|张)?.{0,24}(?:小猫|猫咪|猫|狗|人物|角色|ip|头像|logo|图标|插画)`,
|
||||
);
|
||||
const continueVariants = new RegExp(
|
||||
`(?:再来|再出|再做|再给我|补|多来|继续生成|继续出)${countToken}?张`,
|
||||
);
|
||||
const referenceGeneration =
|
||||
/参考.*生成|按这个风格生成|以这张图为灵感生成|仿照.*生成|同风格再来|同样风格再来/.test(
|
||||
normalizedText,
|
||||
);
|
||||
|
||||
return (
|
||||
/重新生成|生成新图|新建图片|创作一张|生成一张|画一张|给我画|帮我画|出一张|做一张图|做一张海报|创建一张图片|生成一只|生成一个|生成一幅|生成一版|生成一套|生成一组|生图|出图/.test(
|
||||
normalizedText,
|
||||
) ||
|
||||
createVisualTarget.test(normalizedText) ||
|
||||
drawSubject.test(normalizedText) ||
|
||||
continueVariants.test(normalizedText) ||
|
||||
referenceGeneration ||
|
||||
/帮我生成.*(?:图|图片|海报|头像|插画|场景|小猫|猫咪|人物|产品图)|给我生成.*(?:图|图片|海报|头像|插画|场景|小猫|猫咪|人物|产品图)/.test(
|
||||
normalizedText,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function isLikelyImageEditRequest(text: string): boolean {
|
||||
const normalizedText = text.toLowerCase();
|
||||
return /抠图|去背景|透明背景|去背|扣图|移除背景|换背景|改背景|背景换成|背景改成|白底|纯色背景|替换.*背景|变清晰|高清|锐化|降噪|提升质量|修复|清晰化|扩展|拓展|延伸|扩图|outpainting|拓宽|扩大|去除|消除|擦掉|删除|移除|p掉|重绘|风格化|卡通化|动漫风|油画风|转.*风格|变.*风格|改.*风格|调色|调整颜色|亮度|对比度|饱和度|色调|曝光|滤镜|改成.*色|换成.*色|变成.*色|颜色改成|颜色换成|颜色变成|缩放|放大|缩小|调整尺寸|裁剪|resize|crop|修图|p图|改图|调图|编辑.*图|优化.*图|重做|再调整|继续改|继续调/.test(
|
||||
return /抠图|去背景|透明背景|去背|扣图|移除背景|换背景|改背景|背景换成|背景改成|白底|纯色背景|替换.*背景|变清晰|高清|锐化|降噪|提升质量|修复|清晰化|扩展|拓展|延伸|扩图|outpainting|拓宽|扩大|换成.*(?:比例|竖版|横版|竖图|横图|9:16|16:9|1:1)|改成.*(?:比例|竖版|横版|竖图|横图|9:16|16:9|1:1)|比例|画幅|竖版|横版|去除|消除|擦掉|删除|移除|p掉|重绘|风格化|卡通化|动漫风|油画风|转.*风格|变.*风格|改.*风格|调色|调整颜色|亮度|对比度|饱和度|色调|曝光|滤镜|改成.*色|换成.*色|变成.*色|颜色改成|颜色换成|颜色变成|缩放|放大|缩小|调整尺寸|裁剪|resize|crop|修图|p图|改图|调图|编辑.*图|优化.*图|重做|再调整|继续改|继续调/.test(
|
||||
normalizedText,
|
||||
);
|
||||
}
|
||||
@ -1859,6 +1891,16 @@ export async function analyzeIntent(params: {
|
||||
* 🆕 现在返回低置信度,强制走LLM分析以获取自然对话回复
|
||||
*/
|
||||
function quickKeywordMatch(text: string, hasImage: boolean): AnalysisResult {
|
||||
if (isExplicitPromptOptimizationRequest(text)) {
|
||||
return {
|
||||
intent: hasImage ? "vision" : "chat",
|
||||
confidence: 0.95,
|
||||
conversationalReply: hasImage
|
||||
? "收到,我先分析这张图并整理可直接使用的优化提示词,不会直接生图。"
|
||||
: "收到,我先按你的要求整理提示词,不直接执行生成。",
|
||||
};
|
||||
}
|
||||
|
||||
if (isExplicitVideoGenerationRequest(text)) {
|
||||
return {
|
||||
intent: "video_gen",
|
||||
@ -1876,6 +1918,15 @@ function quickKeywordMatch(text: string, hasImage: boolean): AnalysisResult {
|
||||
conversationalReply: hasImage
|
||||
? "收到,我会结合参考图直接帮你生成。"
|
||||
: "好的,我来直接帮你生成这张图。",
|
||||
};
|
||||
}
|
||||
|
||||
if (hasImage && isLikelyImageEditRequest(text) && !isLikelyAdvisoryOrPlanningRequest(text)) {
|
||||
return {
|
||||
intent: "image_edit",
|
||||
subIntent: inferDefaultImageEditSubIntent(text),
|
||||
confidence: 0.92,
|
||||
conversationalReply: "收到,我会按你的目标直接处理这张图。",
|
||||
};
|
||||
}
|
||||
|
||||
@ -2306,7 +2357,7 @@ export function fallbackKeywordAnalysis(
|
||||
}
|
||||
|
||||
// 图像拓展
|
||||
if (/扩展|拓展|延伸|扩图|outpainting|拓宽|扩大/.test(lowerText)) {
|
||||
if (/扩展|拓展|延伸|扩图|outpainting|拓宽|扩大|换成.*(?:比例|竖版|横版|竖图|横图|9:16|16:9|1:1)|改成.*(?:比例|竖版|横版|竖图|横图|9:16|16:9|1:1)|比例|画幅|竖版|横版/.test(lowerText)) {
|
||||
return {
|
||||
intent: "image_edit",
|
||||
subIntent: "extend",
|
||||
|
||||
@ -110,6 +110,14 @@ const runMigrate = async () => {
|
||||
ALTER TABLE "AiModel"
|
||||
ADD COLUMN IF NOT EXISTS "isDefaultTextEditModel" boolean NOT NULL DEFAULT false;
|
||||
`);
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "AiModel"
|
||||
ADD COLUMN IF NOT EXISTS "healthRecoverySuccessStreak" integer NOT NULL DEFAULT 0;
|
||||
`);
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "AiModel"
|
||||
ADD COLUMN IF NOT EXISTS "healthObservationStartedAt" timestamp;
|
||||
`);
|
||||
for (const item of providerBaseUrlDefaults) {
|
||||
await db.execute(sql`
|
||||
UPDATE "AiProvider"
|
||||
|
||||
5
lib/db/migrations/0080_add_ai_model_health_recovery.sql
Normal file
5
lib/db/migrations/0080_add_ai_model_health_recovery.sql
Normal file
@ -0,0 +1,5 @@
|
||||
ALTER TABLE "AiModel"
|
||||
ADD COLUMN IF NOT EXISTS "healthRecoverySuccessStreak" integer NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE "AiModel"
|
||||
ADD COLUMN IF NOT EXISTS "healthObservationStartedAt" timestamp;
|
||||
@ -463,6 +463,13 @@
|
||||
"when": 1778252400000,
|
||||
"tag": "0079_add_ai_model_health",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 66,
|
||||
"version": "7",
|
||||
"when": 1778461200000,
|
||||
"tag": "0080_add_ai_model_health_recovery",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ import { drizzle } from "drizzle-orm/postgres-js";
|
||||
import postgres from "postgres";
|
||||
import type { ArtifactKind } from "@/components/artifact";
|
||||
import type { VisibilityType } from "@/components/visibility-selector";
|
||||
import { reviveAiModelsFromCooldown } from "@/lib/ai/runtime/model-health";
|
||||
import { ChatSDKError } from "../errors";
|
||||
import type { AppUsage } from "../usage";
|
||||
import { generateUUID } from "../utils";
|
||||
@ -3912,6 +3913,7 @@ export async function deleteAiProvider({ id }: { id: string }) {
|
||||
|
||||
export async function getAllAiModels(): Promise<AiModel[]> {
|
||||
try {
|
||||
await reviveAiModelsFromCooldown();
|
||||
// 暂时移除排序以避免 sortOrder 类型问题
|
||||
return await db.select().from(aiModel);
|
||||
} catch (_error) {
|
||||
@ -3925,6 +3927,7 @@ export async function getAllAiModels(): Promise<AiModel[]> {
|
||||
|
||||
export async function getEnabledAiModels(): Promise<AiModel[]> {
|
||||
try {
|
||||
await reviveAiModelsFromCooldown();
|
||||
return await db
|
||||
.select()
|
||||
.from(aiModel)
|
||||
@ -3943,6 +3946,7 @@ export async function getAiModelsByProviderId({
|
||||
providerId: string;
|
||||
}): Promise<AiModel[]> {
|
||||
try {
|
||||
await reviveAiModelsFromCooldown();
|
||||
return await db
|
||||
.select()
|
||||
.from(aiModel)
|
||||
@ -3961,6 +3965,7 @@ export async function getAiModelById({
|
||||
id: string;
|
||||
}): Promise<AiModel | undefined> {
|
||||
try {
|
||||
await reviveAiModelsFromCooldown({ modelId: id });
|
||||
const models = await db
|
||||
.select()
|
||||
.from(aiModel)
|
||||
@ -3977,6 +3982,7 @@ export async function getAiModelById({
|
||||
|
||||
export async function getDefaultAiModel(): Promise<AiModel | undefined> {
|
||||
try {
|
||||
await reviveAiModelsFromCooldown();
|
||||
const models = await db
|
||||
.select()
|
||||
.from(aiModel)
|
||||
@ -4194,6 +4200,8 @@ export async function updateAiModel({
|
||||
updateData.healthLastFailureAt = null;
|
||||
updateData.healthAutoDisabledAt = null;
|
||||
updateData.healthAutoDisabledReason = null;
|
||||
updateData.healthRecoverySuccessStreak = 0;
|
||||
updateData.healthObservationStartedAt = null;
|
||||
updateData.healthUpdatedAt = new Date();
|
||||
}
|
||||
}
|
||||
@ -4477,6 +4485,7 @@ export async function getEnabledAiModelsWithProvider(
|
||||
includeHidden: boolean = true
|
||||
) {
|
||||
try {
|
||||
await reviveAiModelsFromCooldown();
|
||||
const conditions = [
|
||||
eq(aiModel.isEnabled, true),
|
||||
eq(aiProvider.isEnabled, true),
|
||||
@ -4499,7 +4508,23 @@ export async function getEnabledAiModelsWithProvider(
|
||||
.innerJoin(aiProvider, eq(aiModel.providerId, aiProvider.id))
|
||||
.where(and(...conditions));
|
||||
|
||||
return models;
|
||||
const healthPriority = (status?: string | null) => {
|
||||
if (status === "healthy") return 0;
|
||||
if (status === "degraded") return 1;
|
||||
if (status === "auto_disabled") return 2;
|
||||
return 3;
|
||||
};
|
||||
|
||||
return [...models].sort((left, right) => {
|
||||
const byHealth =
|
||||
healthPriority(left.model.healthStatus) -
|
||||
healthPriority(right.model.healthStatus);
|
||||
if (byHealth !== 0) return byHealth;
|
||||
if (left.model.isDefault !== right.model.isDefault) {
|
||||
return Number(right.model.isDefault) - Number(left.model.isDefault);
|
||||
}
|
||||
return (left.model.sortOrder ?? 0) - (right.model.sortOrder ?? 0);
|
||||
});
|
||||
} catch (_error) {
|
||||
throw new ChatSDKError(
|
||||
"bad_request:database",
|
||||
@ -4528,6 +4553,7 @@ export async function getAgentAiModelWithProvider() {
|
||||
*/
|
||||
export async function getSubModelsByParentModelId(parentModelId: string) {
|
||||
try {
|
||||
await reviveAiModelsFromCooldown();
|
||||
const subModels = await db
|
||||
.select({
|
||||
model: aiModel,
|
||||
|
||||
@ -349,6 +349,10 @@ export const aiModel = pgTable("AiModel", {
|
||||
healthLastSuccessAt: timestamp("healthLastSuccessAt"),
|
||||
healthAutoDisabledAt: timestamp("healthAutoDisabledAt"),
|
||||
healthAutoDisabledReason: text("healthAutoDisabledReason"),
|
||||
healthRecoverySuccessStreak: integer("healthRecoverySuccessStreak")
|
||||
.notNull()
|
||||
.default(0),
|
||||
healthObservationStartedAt: timestamp("healthObservationStartedAt"),
|
||||
healthUpdatedAt: timestamp("healthUpdatedAt").notNull().defaultNow(),
|
||||
// 🆕 图片生成模型特性配置(用于阿里生图、Gemini等)
|
||||
imageGenConfig: jsonb("imageGenConfig"), // { maxImages: 6, supportsSize: true, supportsPromptExtend: true, supportsNegativePrompt: true, enableLoadingPlaceholder: true }
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user