diff --git a/apps/api/src/backend.test.ts b/apps/api/src/backend.test.ts index 4085bff..8ee1a53 100644 --- a/apps/api/src/backend.test.ts +++ b/apps/api/src/backend.test.ts @@ -106,6 +106,26 @@ test('user job DTO applies final prompt visibility without changing the original assert.equal(visible.finalPrompt, 'optimized'); assert.equal(visible.prompt, 'original') }) +test('user job DTO exposes only the truncated provider failure detail', async () => { + const base = { id: 'job', created_by: 'user', model_id: 'model', model_name: 'Video', prompt: 'p', size: '16:9', count: 1, status: 'failed', error_code: 'PROVIDER_REJECTED', created_at: new Date() } + const rejected = await jobDto({ ...base, provider_error: { status: 400, detail: ' Input image resolution is unsupported ', providerReferenceId: 'nested-ref' } }) + assert.equal(rejected.errorCode, 'PROVIDER_REJECTED') + assert.equal(rejected.errorMessage, 'Input image resolution is unsupported') + assert.equal(JSON.stringify(rejected).includes('nested-ref'), false) + assert.equal(JSON.stringify(rejected).includes('"status":400'), false) + const truncated = await jobDto({ ...base, provider_error: { detail: 'x'.repeat(400) } }) + assert.equal(truncated.errorMessage?.length, 300) + for (const row of [ + base, + { ...base, provider_error: null }, + { ...base, provider_error: { status: 502 } }, + { ...base, provider_error: { detail: ' ' } }, + { ...base, provider_error: 'not-an-object' }, + ]) { + assert.equal((await jobDto(row)).errorMessage, undefined) + } +}) + test('manual retry resumes from the correct generation phase', () => { assert.deepEqual(retryPreparation({ optimization_mode: 'disabled' }), { phase: 'image_generating', resetOptimization: false }) assert.deepEqual(retryPreparation({ optimization_mode: 'enabled', prompt_optimization_id: 'po', final_prompt: 'optimized prompt', template_instruction_snapshot: 'template' }), { phase: 'image_generating', resetOptimization: false }) diff --git a/apps/api/src/shared/dto.ts b/apps/api/src/shared/dto.ts index 998677e..b2a9f9d 100644 --- a/apps/api/src/shared/dto.ts +++ b/apps/api/src/shared/dto.ts @@ -209,6 +209,11 @@ export async function jobDto(row: Record, outputs: Record } + // Only the already-sanitised provider detail is user-visible; never spread the object. + const providerError = row.provider_error && typeof row.provider_error === 'object' + ? row.provider_error as { detail?: unknown } + : undefined + const providerDetail = typeof providerError?.detail === 'string' ? providerError.detail.trim() : '' return { id: row.id, createdBy: row.created_by, @@ -240,6 +245,7 @@ export async function jobDto(row: Record, outputs: Record + // The console mirrors its 图像/视频 tab into `?tab=`, so it reads search params + // and needs a boundary like every other `useSearchParams` consumer. + return ( + 正在加载创作台...}> + + + ) } diff --git a/apps/web-next/src/app/admin/plugins/page.tsx b/apps/web-next/src/app/admin/plugins/page.tsx new file mode 100644 index 0000000..9ca4d22 --- /dev/null +++ b/apps/web-next/src/app/admin/plugins/page.tsx @@ -0,0 +1,11 @@ +import { AdminPluginsView } from '@/features/admin/components/admin-plugins-view' + +export const dynamic = 'force-dynamic' + +export const metadata = { + title: '媒体插件 - MuseCanvas 管理后台', +} + +export default function AdminPluginsPage() { + return +} diff --git a/apps/web-next/src/app/admin/settings/page.tsx b/apps/web-next/src/app/admin/settings/page.tsx new file mode 100644 index 0000000..a4af435 --- /dev/null +++ b/apps/web-next/src/app/admin/settings/page.tsx @@ -0,0 +1,11 @@ +import { AdminSettingsView } from '@/features/admin/components/admin-settings-view' + +export const dynamic = 'force-dynamic' + +export const metadata = { + title: '系统配置 - MuseCanvas 管理后台', +} + +export default function AdminSettingsPage() { + return +} diff --git a/apps/web-next/src/app/page.tsx b/apps/web-next/src/app/page.tsx index 3de0c14..5febd45 100644 --- a/apps/web-next/src/app/page.tsx +++ b/apps/web-next/src/app/page.tsx @@ -1,92 +1,109 @@ import Link from 'next/link' -import { - ArrowRight, - Brush, - Check, - Layers3, - Library, - PanelTop, - Sparkles, - WandSparkles, - Zap, -} from 'lucide-react' +import { ArrowRight, Layers3, Library, Mail, Repeat, ShieldCheck, WandSparkles } from 'lucide-react' export const dynamic = 'force-static' export const metadata = { title: 'MuseCanvas - 把灵感整理成可生成的画面', - description: '面向创作者的 AI 图像与视频工作台', + description: '面向创作者的 AI 图像生成工作台:提示词、画幅与参考图、任务队列和作品图库保持在同一条创作路径上。', } -const canvasTiles = [ - { title: 'Prompt', text: '雨后城市屋顶,薄雾,胶片质感' }, - { title: 'Thinking', text: '模型前置思考,自动选择提示词模板' }, - { title: 'Queue', text: '生成中 68%' }, +const consoleSpecs: { label: string; value: string; mono?: boolean }[] = [ + { label: '生成模型', value: '由管理端配置的图像模型,可切换' }, + { label: '画幅', value: '1:1 · 16:9 · 9:16 · 4:3 · 3:4', mono: true }, + { label: '单次张数', value: '1 / 2 / 4 张', mono: true }, + { label: '参考图', value: '最多 4 张,PNG / JPEG', mono: true }, ] -const featureCards = [ +const capabilities = [ { - title: '从一句话开始', - description: '把灵感、构图和情绪写成提示词,MuseCanvas 会通过前置思考把它整理成可生成的视觉方向。', + title: '提示词不需要套模板', + description: + '写下主体、场景、材质、光线,以及想避开的元素就够了。风格模板由系统侧统一维护,提交前不必手动挑选。', icon: WandSparkles, }, { - title: '模板自动适配', - description: '用户不需要手动挑选风格模板,模型会根据提示词意图自动匹配合适的前处理模板。', - icon: PanelTop, + title: '模型、画幅与参考图', + description: + '在同一个面板里切换模型、5 种画幅和单次张数,并上传 PNG 或 JPEG 参考图作为构图与风格依据。', + icon: Layers3, + }, + { + title: '排队、重试与取消', + description: + '任务提交后进入队列按序执行,失败会自动重试;也可以只对失败的任务手动重跑,或在结束前取消。', + icon: Repeat, }, { - title: '作品自然沉淀', - description: '生成结果、任务状态和历史记录自动进入图库,方便回看、下载和继续迭代。', + title: '作品自动沉淀', + description: + '生成结果自动进入图库。调整网格密度、放大比对细节、下载原图,或者单张、批量删除不要的版本。', icon: Library, }, + { + title: '验证码登录', + description: + '用邮箱接收 6 位验证码直接进入,不需要再记一个密码;也支持 GitHub 与 Google 账号。部署方可切换为邀请码注册。', + icon: Mail, + }, + { + title: '自托管与后台', + description: + '通过初始化向导完成部署配置,任务、用户、模型与提供商、提示词模板、OAuth 都有各自的管理页面。', + icon: ShieldCheck, + }, ] const workflowSteps = [ { - title: '描述画面', - description: '用自然语言写下主体、场景、材质、光线和想要避开的元素。', + title: '写下画面', + description: '用自然语言描述构图与氛围,需要延续某张图的感觉时,直接把它作为参考图上传。', }, { title: '确定输出', - description: '选择模型、画幅和生成数量,前置思考会在提交后自动完成模板匹配。', + description: '选择模型、画幅和单次张数,提交后任务进入队列,每条任务的状态都可以在创作台里跟着看。', }, { - title: '归档复用', - description: '保存作品和最终提示词,把一次灵感变成之后可以继续使用的创作资产。', + title: '回看与下载', + description: '完成后结果自动出现在图库,按时间回看、放大比对、下载原图,或删除不满意的版本。', }, ] -const capabilities = [ - { icon: WandSparkles, label: '提示词前置思考' }, - { icon: Layers3, label: '多模型与画幅' }, - { icon: Zap, label: '任务队列与重试' }, - { icon: Library, label: '图库与历史沉淀' }, -] - export default function HomePage() { return (
{/* Top navigation */} -