Merge branch 'codex/20260818-video-confirm-media-9b7c-video-confirm-media'
Some checks failed
Electron E2E / Electron E2E (macos-latest) (push) Has been cancelled
Electron E2E / Electron E2E (ubuntu-latest) (push) Has been cancelled
Electron E2E / Electron E2E (windows-latest) (push) Has been cancelled

This commit is contained in:
2026-08-18 13:26:15 +08:00
3 changed files with 129 additions and 8 deletions

View File

@@ -0,0 +1,47 @@
# Task: Fix video intent rendering as image confirmation
## Identity
- Task ID: 20260818-video-confirm-media-9b7c
- Mode: Feature
- Branch: codex/20260818-video-confirm-media-9b7c-video-confirm-media
- Worktree: D:\Datas\OthersProjects\makelore-video-confirm-media-9b7c
- Base commit: 11b19832a35477d2136c6ea953dd9c408fd84816
- Owner: codex
- Status: Ready for Integration
## Scope
- Normalize the server's public generation-option payload for the Electron/Main-to-Renderer shared design contract.
- Support both canonical client options (`value`/`disabled`) and the current server configuration shape (`id` or `seconds`/`enabled`) without changing the Renderer API.
- Add a regression proving a video Quote retains its medium and exposes selectable 2/6 second durations.
## Intent And Constraints
- The raw trace proves this run reached `medium=video` and `bailian_video_direct_v1`; the UI failure was caused by option-shape loss, not by a server image decision.
- Preserve existing image and legacy conversation compatibility.
- Do not expose provider credentials or change the HTTP API; normalization belongs at the Electron adapter boundary.
## Outcome
- Added a typed adapter normalizer that maps `value`/`id`/`seconds` to the shared `value` field and maps `enabled=false` to `disabled=true` (while retaining canonical payloads).
- The Renderer now receives video durations as numeric selectable options, so the duration control and video confirmation validation can work with the live server response.
- Added a regression test using the server-shaped video options and asserting the mapped video medium, model, and durations.
## Verification
- `pnpm exec vitest run tests/unit/works-square-design-workspace.test.ts` — 32 passed.
- `pnpm exec vitest run tests/unit/image-canvas-page.test.tsx tests/unit/works-square-design-workspace.test.ts` — 79 passed.
- `pnpm run typecheck` — passed.
- `pnpm exec eslint electron/image-workspace/works-square-workspace.ts tests/unit/works-square-design-workspace.test.ts` — passed.
- `pnpm run build:vite` — passed (existing chunk-size and dynamic-import warnings only).
- `git diff --check` — passed (only the repository's CRLF conversion warnings).
## Follow-ups
- Ship this client commit with the server intent-guard commit; run the packaged desktop smoke test against a live video Quote.
- A future API contract cleanup may make the server publish canonical option names directly, but the adapter normalizer is the compatibility boundary for now.
## Promotion Candidates
- None recorded.

View File

@@ -45,6 +45,17 @@ type ServerBrief = {
missing_decision: string | null;
};
type ServerGenerationOption = {
value?: string | number;
id?: string | number;
seconds?: number;
label?: string;
default?: boolean;
disabled?: boolean;
enabled?: boolean;
multiplier?: number;
};
type ServerQuote = {
quote_id: string;
status: DesignGenerationQuote['status'];
@@ -60,10 +71,10 @@ type ServerQuote = {
duration_seconds: number | null;
};
generation_options: {
models: Array<DesignGenerationOption<string>>;
resolutions: Array<DesignGenerationOption<string>>;
durations: Array<DesignGenerationOption<number>>;
aspect_ratios: Array<DesignGenerationOption<string>>;
models: ServerGenerationOption[];
resolutions: ServerGenerationOption[];
durations: ServerGenerationOption[];
aspect_ratios: ServerGenerationOption[];
};
pricing: {
schema: string;
@@ -281,6 +292,22 @@ function mapBrief(brief: ServerBrief): DesignBrief {
};
}
function mapGenerationOptions<TValue extends string | number>(
options: ServerGenerationOption[] | undefined,
): DesignGenerationOption<TValue>[] {
return (options ?? []).flatMap((option) => {
const value = option.value ?? option.id ?? option.seconds;
if (typeof value !== 'string' && typeof value !== 'number') return [];
return [{
value: value as TValue,
label: option.label ?? String(value),
default: option.default ?? false,
disabled: option.disabled ?? option.enabled === false,
multiplier: option.multiplier ?? 1,
}];
});
}
function mapQuote(quote: ServerQuote | null): DesignGenerationQuote | null {
if (!quote) return null;
return {
@@ -298,10 +325,10 @@ function mapQuote(quote: ServerQuote | null): DesignGenerationQuote | null {
durationSeconds: quote.generation_parameters.duration_seconds,
},
generationOptions: {
models: quote.generation_options.models,
resolutions: quote.generation_options.resolutions,
durations: quote.generation_options.durations,
aspectRatios: quote.generation_options.aspect_ratios,
models: mapGenerationOptions<string>(quote.generation_options.models),
resolutions: mapGenerationOptions<string>(quote.generation_options.resolutions),
durations: mapGenerationOptions<number>(quote.generation_options.durations),
aspectRatios: mapGenerationOptions<string>(quote.generation_options.aspect_ratios),
},
pricing: {
schema: quote.pricing.schema,

View File

@@ -592,6 +592,53 @@ describe('Works Square AI design adapter', () => {
);
});
it('normalizes the server video option shape so the duration picker stays available', async () => {
const videoConversation = {
...serverConversation,
brief: { ...serverConversation.brief, medium: 'video' },
messages: serverConversation.messages.map((message) => ({
...message,
generation_quote: {
...message.generation_quote!,
medium: 'video',
generation_parameters: {
...message.generation_quote!.generation_parameters,
model: 'wan2.7-i2v-2026-04-25',
resolution: '720P',
aspect_ratio: '4:5',
duration_seconds: 4,
},
generation_options: {
models: [{ id: 'wan2.7-i2v-2026-04-25', label: '万相视频', enabled: true, multiplier: 1 }],
resolutions: [{ value: '720P', label: '720P', enabled: true, multiplier: 1 }],
durations: [
{ seconds: 2, label: '2 秒', enabled: true, multiplier: 1 },
{ seconds: 6, label: '6 秒', enabled: true, multiplier: 1 },
],
aspect_ratios: [{ value: '4:5', label: '4:5', default: true, enabled: true, multiplier: 1 }],
},
},
})),
};
const fetchMock = vi.fn<typeof fetch>().mockResolvedValueOnce(jsonResponse(videoConversation));
const adapter = new WorksSquareDesignWorkspace({
apiBaseUrl: 'https://square.example',
fetchImpl: fetchMock,
});
const mapped = await adapter.getConversation('workspace-one', 'conversation-one');
const quote = mapped.messages[0].generationQuote!;
expect(mapped.brief.medium).toBe('video');
expect(quote.medium).toBe('video');
expect(quote.generationOptions.models[0]).toMatchObject({
value: 'wan2.7-i2v-2026-04-25',
disabled: false,
});
expect(quote.generationOptions.durations.map((option) => option.value)).toEqual([2, 6]);
expect(quote.generationOptions.durations.every((option) => !option.disabled)).toBe(true);
});
it('uploads a local reference image as multipart data and maps the returned Asset', async () => {
const fetchMock = vi.fn<typeof fetch>().mockResolvedValueOnce(jsonResponse({
asset_id: 'asset-uploaded',