接入创作参考图原生导入
Creation Agent参考图上传优先复用file.importImage宿主能力 补充原生导入取消和浏览器回退测试 更新宿主壳协议、方案文档和共享记忆
This commit is contained in:
@@ -1015,6 +1015,154 @@ test('creation agent workspace renders selected reference image with shared prev
|
||||
expect(onClearReferenceImage).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('creation agent workspace imports reference image through native HostBridge', async () => {
|
||||
ensureScrollApis();
|
||||
|
||||
const inputClickSpy = vi
|
||||
.spyOn(HTMLInputElement.prototype, 'click')
|
||||
.mockImplementation(() => undefined);
|
||||
vi.spyOn(hostBridgeServices, 'canUseNativeHostCapability').mockImplementation(
|
||||
(capability) => capability === 'file.importImage',
|
||||
);
|
||||
const importImageSpy = vi
|
||||
.spyOn(hostBridgeServices, 'importHostImageFile')
|
||||
.mockResolvedValue({
|
||||
action: 'selected',
|
||||
fileName: '设定参考图.png',
|
||||
base64Data: 'aW1hZ2U=',
|
||||
mimeType: 'image/png',
|
||||
bytes: 5,
|
||||
});
|
||||
const onReferenceImageChange = vi.fn<[File], Promise<void>>(
|
||||
async () => undefined,
|
||||
);
|
||||
|
||||
try {
|
||||
render(
|
||||
<CreationAgentWorkspace
|
||||
session={{
|
||||
sessionId: 'creation-agent-session-1',
|
||||
title: null,
|
||||
currentTurn: 0,
|
||||
progressPercent: 0,
|
||||
anchors: [],
|
||||
messages: [],
|
||||
}}
|
||||
theme={testTheme}
|
||||
loadingText="正在准备"
|
||||
composerPlaceholder="输入消息"
|
||||
primaryActionLabel="生成结果页"
|
||||
onBack={() => {}}
|
||||
onSubmitText={() => {}}
|
||||
onPrimaryAction={() => {}}
|
||||
onReferenceImageChange={onReferenceImageChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '上传参考图' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onReferenceImageChange).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
const importedFile = onReferenceImageChange.mock.calls[0]?.[0] as File;
|
||||
expect(importImageSpy).toHaveBeenCalledTimes(1);
|
||||
expect(importedFile.name).toBe('设定参考图.png');
|
||||
expect(importedFile.type).toBe('image/png');
|
||||
expect(importedFile.size).toBe(5);
|
||||
expect(inputClickSpy).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
inputClickSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
test('creation agent workspace keeps native image cancellation inside shell flow', async () => {
|
||||
ensureScrollApis();
|
||||
|
||||
const inputClickSpy = vi
|
||||
.spyOn(HTMLInputElement.prototype, 'click')
|
||||
.mockImplementation(() => undefined);
|
||||
vi.spyOn(hostBridgeServices, 'canUseNativeHostCapability').mockImplementation(
|
||||
(capability) => capability === 'file.importImage',
|
||||
);
|
||||
vi.spyOn(hostBridgeServices, 'importHostImageFile').mockResolvedValue(false);
|
||||
const onReferenceImageChange = vi.fn();
|
||||
|
||||
try {
|
||||
render(
|
||||
<CreationAgentWorkspace
|
||||
session={{
|
||||
sessionId: 'creation-agent-session-1',
|
||||
title: null,
|
||||
currentTurn: 0,
|
||||
progressPercent: 0,
|
||||
anchors: [],
|
||||
messages: [],
|
||||
}}
|
||||
theme={testTheme}
|
||||
loadingText="正在准备"
|
||||
composerPlaceholder="输入消息"
|
||||
primaryActionLabel="生成结果页"
|
||||
onBack={() => {}}
|
||||
onSubmitText={() => {}}
|
||||
onPrimaryAction={() => {}}
|
||||
onReferenceImageChange={onReferenceImageChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '上传参考图' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(hostBridgeServices.importHostImageFile).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
expect(onReferenceImageChange).not.toHaveBeenCalled();
|
||||
expect(inputClickSpy).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
inputClickSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
test('creation agent workspace falls back to browser image picker without native image capability', () => {
|
||||
ensureScrollApis();
|
||||
|
||||
const inputClickSpy = vi
|
||||
.spyOn(HTMLInputElement.prototype, 'click')
|
||||
.mockImplementation(() => undefined);
|
||||
vi.spyOn(hostBridgeServices, 'canUseNativeHostCapability').mockReturnValue(
|
||||
false,
|
||||
);
|
||||
|
||||
try {
|
||||
render(
|
||||
<CreationAgentWorkspace
|
||||
session={{
|
||||
sessionId: 'creation-agent-session-1',
|
||||
title: null,
|
||||
currentTurn: 0,
|
||||
progressPercent: 0,
|
||||
anchors: [],
|
||||
messages: [],
|
||||
}}
|
||||
theme={testTheme}
|
||||
loadingText="正在准备"
|
||||
composerPlaceholder="输入消息"
|
||||
primaryActionLabel="生成结果页"
|
||||
onBack={() => {}}
|
||||
onSubmitText={() => {}}
|
||||
onPrimaryAction={() => {}}
|
||||
onReferenceImageChange={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '上传参考图' }));
|
||||
|
||||
expect(inputClickSpy).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
inputClickSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
test('creation agent workspace shows document parse error near composer', async () => {
|
||||
ensureScrollApis();
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
canUseNativeHostCapability,
|
||||
exportHostTextFile,
|
||||
importHostDocumentFile,
|
||||
importHostImageFile,
|
||||
importHostTextFile,
|
||||
} from '../../services/host-bridge/hostBridge';
|
||||
import { PlatformActionButton } from '../common/PlatformActionButton';
|
||||
@@ -489,6 +490,9 @@ export function CreationAgentWorkspace({
|
||||
const [documentInputError, setDocumentInputError] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [referenceImageInputError, setReferenceImageInputError] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [isParsingDocumentInput, setIsParsingDocumentInput] = useState(false);
|
||||
const [isReadingReferenceImage, setIsReadingReferenceImage] = useState(false);
|
||||
const [isExportingSessionText, setIsExportingSessionText] = useState(false);
|
||||
@@ -592,6 +596,7 @@ export function CreationAgentWorkspace({
|
||||
onSubmitText(text);
|
||||
setDraftText('');
|
||||
setDocumentInputError(null);
|
||||
setReferenceImageInputError(null);
|
||||
};
|
||||
|
||||
const appendDocumentInputText = (text: string) => {
|
||||
@@ -671,6 +676,27 @@ export function CreationAgentWorkspace({
|
||||
documentInputRef.current?.click();
|
||||
};
|
||||
|
||||
const runReferenceImageTask = async (task: () => Promise<void> | void) => {
|
||||
if (isBusy || isReadingReferenceImage || !onReferenceImageChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsReadingReferenceImage(true);
|
||||
setReferenceImageInputError(null);
|
||||
|
||||
try {
|
||||
await task();
|
||||
} catch (referenceImageReadError) {
|
||||
setReferenceImageInputError(
|
||||
referenceImageReadError instanceof Error
|
||||
? referenceImageReadError.message
|
||||
: '读取参考图失败,请重新选择图片。',
|
||||
);
|
||||
} finally {
|
||||
setIsReadingReferenceImage(false);
|
||||
}
|
||||
};
|
||||
|
||||
const exportSessionText = async () => {
|
||||
if (
|
||||
!sessionExportMarkdown ||
|
||||
@@ -708,6 +734,28 @@ export function CreationAgentWorkspace({
|
||||
};
|
||||
|
||||
const openReferenceImagePicker = () => {
|
||||
if (isBusy || isReadingReferenceImage || !onReferenceImageChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (canUseNativeHostCapability('file.importImage')) {
|
||||
void runReferenceImageTask(async () => {
|
||||
const importedImageFile = await importHostImageFile();
|
||||
if (!importedImageFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
await onReferenceImageChange(
|
||||
base64DataToFile(
|
||||
importedImageFile.base64Data,
|
||||
importedImageFile.fileName,
|
||||
importedImageFile.mimeType,
|
||||
),
|
||||
);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
referenceImageInputRef.current?.click();
|
||||
};
|
||||
|
||||
@@ -732,13 +780,12 @@ export function CreationAgentWorkspace({
|
||||
return;
|
||||
}
|
||||
|
||||
setIsReadingReferenceImage(true);
|
||||
await runReferenceImageTask(() => onReferenceImageChange(file));
|
||||
};
|
||||
|
||||
try {
|
||||
await onReferenceImageChange(file);
|
||||
} finally {
|
||||
setIsReadingReferenceImage(false);
|
||||
}
|
||||
const clearReferenceImage = () => {
|
||||
setReferenceImageInputError(null);
|
||||
onClearReferenceImage?.();
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -880,19 +927,25 @@ export function CreationAgentWorkspace({
|
||||
caption={referenceImageLabel || '已选择参考图'}
|
||||
removeLabel="移除参考图"
|
||||
disabled={isBusy || isReadingReferenceImage}
|
||||
onRemove={onClearReferenceImage}
|
||||
onRemove={clearReferenceImage}
|
||||
className="mx-4 mb-3 bg-white/70"
|
||||
removeButtonProps={{ title: '移除参考图' }}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{documentInputError || referenceImageError || error ? (
|
||||
{documentInputError ||
|
||||
referenceImageInputError ||
|
||||
referenceImageError ||
|
||||
error ? (
|
||||
<PlatformStatusMessage
|
||||
tone="error"
|
||||
surface="platform"
|
||||
className="mx-4 mb-3 rounded-[1rem]"
|
||||
>
|
||||
{documentInputError || referenceImageError || error}
|
||||
{documentInputError ||
|
||||
referenceImageInputError ||
|
||||
referenceImageError ||
|
||||
error}
|
||||
</PlatformStatusMessage>
|
||||
) : null}
|
||||
|
||||
@@ -973,6 +1026,7 @@ export function CreationAgentWorkspace({
|
||||
onChange={(event) => {
|
||||
setDraftText(event.target.value);
|
||||
setDocumentInputError(null);
|
||||
setReferenceImageInputError(null);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
|
||||
Reference in New Issue
Block a user