Fix Picture Agent loading fallback

This commit is contained in:
boos
2026-06-02 17:23:36 +08:00
parent 4ff6a89587
commit c876370a22
2 changed files with 133 additions and 37 deletions
+7
View File
@@ -3221,6 +3221,13 @@ textarea {
margin-top: auto; margin-top: auto;
} }
.picture-agent-submit-hint {
margin-top: -4px;
color: var(--color-text-muted);
font-size: 12px;
line-height: 1.5;
}
.picture-agent-job-summary { .picture-agent-job-summary {
display: flex; display: flex;
align-items: center; align-items: center;
+126 -37
View File
@@ -105,6 +105,50 @@ const STYLE_PRESETS = [
prompt: 'modern Chinese illustration style, elegant linework, vivid but balanced colors, poster composition', prompt: 'modern Chinese illustration style, elegant linework, vivid but balanced colors, poster composition',
}, },
]; ];
const FALLBACK_TEMPLATES: PromptTemplate[] = [
{
id: 'local-product-glass-robot',
title: '玻璃机器人产品照',
tag: '商业',
size: '1024x1024',
prompt: 'A precise editorial product photo of a translucent glass robot painting a small neon city, sharp focus, clean background, controlled studio lighting, premium advertising composition, ultra detailed',
},
{
id: 'local-hanfu-cat-golden-hour',
title: '汉服与白猫',
tag: '国风',
size: '1024x1536',
prompt: 'A breathtakingly beautiful ancient Chinese woman in flowing jade-green and blush pink hanfu, golden peony embroidery, sitting by a moon-shaped window in warm afternoon sunlight, gently teasing a fluffy white cat with a silk tassel, celadon tea set and ink calligraphy nearby, serene romantic dreamlike Song Dynasty aesthetics, cinematic depth of field, ultra detailed digital art',
},
{
id: 'local-cyberpunk-street',
title: '赛博街景海报',
tag: '海报',
size: '1536x1024',
prompt: 'A cinematic cyberpunk street market after rain, neon signs reflected on wet pavement, a courier in a translucent raincoat walking through steam, layered storefronts and holographic menus, dramatic teal and amber lighting, editorial poster composition, high detail',
},
{
id: 'local-character-turnaround',
title: '角色四视图',
tag: '角色',
size: '1536x1024',
prompt: 'Character design sheet, same original fantasy explorer shown in front view, side view, back view and three-quarter view, consistent outfit and proportions, clean light background, readable silhouette, detailed fabric and accessories, professional concept art',
},
{
id: 'local-food-photography',
title: '高级餐饮摄影',
tag: '商业',
size: '1024x1024',
prompt: 'Premium food photography of a porcelain bowl of hand-pulled noodles with chili oil, scallions and steam, dark stone tabletop, soft side lighting, shallow depth of field, appetizing texture, refined restaurant campaign style',
},
{
id: 'local-tea-room',
title: '现代茶室空间',
tag: '空间',
size: '1536x1024',
prompt: 'A quiet contemporary Chinese tea room interior with warm timber lattice, low stone tea table, linen cushions, bamboo courtyard visible through sliding doors, natural morning light, calm luxury hospitality design, realistic architectural visualization',
},
];
function makeId() { function makeId() {
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
@@ -331,6 +375,13 @@ export default function PictureAgent() {
() => jobs.filter((job) => job.status === 'succeeded' || job.status === 'failed' || job.status === 'cancelled'), () => jobs.filter((job) => job.status === 'succeeded' || job.status === 'failed' || job.status === 'cancelled'),
[jobs], [jobs],
); );
const submitDisabledHint = useMemo(() => {
if (loading) return '任务正在提交';
if (!model.trim()) return '正在加载或等待选择生图模型';
if (!prompt.trim()) return '请输入提示词';
if (mode === 'edit' && !sourceFile) return '图像编辑需要先上传参考图';
return '';
}, [loading, mode, model, prompt, sourceFile]);
const authedFetch = useCallback((url: string, options: RequestInit & { timeoutMs?: number } = {}) => { const authedFetch = useCallback((url: string, options: RequestInit & { timeoutMs?: number } = {}) => {
return fetchSessionResponse(audience, url, options); return fetchSessionResponse(audience, url, options);
@@ -372,49 +423,84 @@ export default function PictureAgent() {
return; return;
} }
setBootError(''); setBootError('');
setTemplatesLoading(true); setStatus((current) => current || {
success: true,
mode: 'local',
upstreamLabel: 'BoosAPI 本地图片路由',
keyName: '加载密钥中',
});
setModel((current) => current || 'gpt-image-2');
setTemplates(FALLBACK_TEMPLATES);
setTemplatesError(''); setTemplatesError('');
setTemplatesLoading(true);
try { try {
const [statusRes, modelsRes, templatesRes, jobsRes] = await Promise.all([ const [statusResult, modelsResult, jobsResult] = await Promise.allSettled([
authedFetch('/api/picture-agent/status', { timeoutMs: 15_000 }), authedFetch('/api/picture-agent/status', { timeoutMs: 8_000 }),
authedFetch('/api/picture-agent/models', { timeoutMs: 15_000 }), authedFetch('/api/picture-agent/models', { timeoutMs: 8_000 }),
authedFetch(`/api/picture-agent/templates?limit=${TEMPLATE_LIMIT}`, { timeoutMs: 30_000 }), authedFetch('/api/picture-agent/jobs?limit=20', { timeoutMs: 8_000 }),
authedFetch('/api/picture-agent/jobs?limit=20', { timeoutMs: 15_000 }),
]); ]);
if (!statusRes.ok) throw new Error(await parseError(statusRes));
if (!modelsRes.ok) throw new Error(await parseError(modelsRes));
const nextStatus = await statusRes.json();
const nextModels = await modelsRes.json();
let nextTemplates: PromptTemplate[] = [];
if (templatesRes.ok) {
const templatePayload = await templatesRes.json();
nextTemplates = Array.isArray(templatePayload?.templates) ? templatePayload.templates : [];
} else {
setTemplatesError(await parseError(templatesRes));
}
let nextJobs: PictureJob[] = [];
if (jobsRes.ok) {
const jobsPayload = await jobsRes.json();
nextJobs = Array.isArray(jobsPayload?.jobs) ? jobsPayload.jobs : [];
}
if (ignore) return; if (ignore) return;
const availableModels = Array.isArray(nextModels?.models)
? nextModels.models.filter((item: string) => isPictureModel(item)) if (statusResult.status === 'fulfilled') {
: []; if (!statusResult.value.ok) {
setStatus(nextStatus); throw new Error(await parseError(statusResult.value));
setModels(availableModels); }
setTemplates(nextTemplates); setStatus(await statusResult.value.json());
setJobs(nextJobs); } else {
hydrateImagesFromJobs(nextJobs); throw new Error(statusResult.reason?.message || '生图助手状态加载失败');
setModel((current) => { }
if (current && isPictureModel(current)) return current;
const fallback = nextModels?.defaultModel && isPictureModel(nextModels.defaultModel) if (modelsResult.status === 'fulfilled') {
? nextModels.defaultModel if (!modelsResult.value.ok) {
: availableModels[0]; throw new Error(await parseError(modelsResult.value));
return fallback || 'gpt-image-2'; }
}); const nextModels = await modelsResult.value.json();
if (ignore) return;
const availableModels = Array.isArray(nextModels?.models)
? nextModels.models.filter((item: string) => isPictureModel(item))
: [];
setModels(availableModels);
setModel((current) => {
if (current && isPictureModel(current)) return current;
const fallback = nextModels?.defaultModel && isPictureModel(nextModels.defaultModel)
? nextModels.defaultModel
: availableModels[0];
return fallback || 'gpt-image-2';
});
} else {
throw new Error(modelsResult.reason?.message || '生图模型加载失败');
}
if (jobsResult.status === 'fulfilled' && jobsResult.value.ok) {
const jobsPayload = await jobsResult.value.json();
if (ignore) return;
const nextJobs = Array.isArray(jobsPayload?.jobs) ? jobsPayload.jobs : [];
setJobs(nextJobs);
hydrateImagesFromJobs(nextJobs);
}
} catch (err: any) { } catch (err: any) {
if (!ignore) setBootError(err?.message || '生图助手初始化失败'); if (!ignore) setBootError(err?.message || '生图助手初始化失败');
}
try {
const templatesRes = await authedFetch(`/api/picture-agent/templates?limit=${TEMPLATE_LIMIT}`, { timeoutMs: 10_000 });
if (ignore) return;
if (!templatesRes.ok) {
setTemplatesError(await parseError(templatesRes));
return;
}
const templatePayload = await templatesRes.json();
if (ignore) return;
const nextTemplates = Array.isArray(templatePayload?.templates) ? templatePayload.templates : [];
if (nextTemplates.length) {
setTemplates(nextTemplates);
}
setTemplatesError(templatePayload?.fallback && templatePayload?.remote?.message
? String(templatePayload.remote.message)
: '');
} catch (err: any) {
if (!ignore) setTemplatesError(err?.message || '远程模板加载失败,已使用本地模板');
} finally { } finally {
if (!ignore) setTemplatesLoading(false); if (!ignore) setTemplatesLoading(false);
} }
@@ -818,6 +904,9 @@ export default function PictureAgent() {
<button type="button" className="btn btn-primary picture-agent-submit" disabled={!canSubmit} onClick={handleSubmit}> <button type="button" className="btn btn-primary picture-agent-submit" disabled={!canSubmit} onClick={handleSubmit}>
{loading ? '提交中...' : mode === 'edit' ? '编辑图片' : '生成图片'} {loading ? '提交中...' : mode === 'edit' ? '编辑图片' : '生成图片'}
</button> </button>
{!canSubmit && submitDisabledHint ? (
<div className="picture-agent-submit-hint">{submitDisabledHint}</div>
) : null}
<div className="picture-agent-job-summary"> <div className="picture-agent-job-summary">
<div> <div>