feat: editor agent include attachment human readable label to context

This commit is contained in:
2026-07-22 14:01:21 +08:00
parent 53a66a3650
commit 2540c13ddb
6 changed files with 67 additions and 31 deletions
+24 -1
View File
@@ -1,6 +1,7 @@
// 画布Agent对话契约:会话元数据存 SpacetimeDB,消息正文整体存 OSSeditor-agent/{conversationId}.json)。
export const EDITOR_AGENT_MAX_ATTACHMENTS = 9;
export const EDITOR_AGENT_ATTACHMENT_LABEL_MAX_CODE_POINTS = 10;
export const EDITOR_AGENT_ERROR_MESSAGE_PREFIX = 'ERROR ';
export type EditorAgentMessageRole = 'user' | 'assistant' | 'system';
@@ -13,17 +14,39 @@ export type EditorAgentToolCallStatus =
export type EditorAgentAttachmentSource = 'canvas_resource' | 'library_asset';
function normalizeEditorAgentAttachmentLabel(
label: string | null | undefined,
fallback: string,
) {
const normalized = label?.trim() || fallback.trim() || '图片';
return Array.from(normalized)
.slice(0, EDITOR_AGENT_ATTACHMENT_LABEL_MAX_CODE_POINTS)
.join('');
}
export interface EditorAgentAttachmentRef {
source: EditorAgentAttachmentSource;
referenceId: string;
objectKey?: string | null;
imageSrc: string;
thumbnailSrc?: string | null;
label?: string | null;
label: string;
width?: number | null;
height?: number | null;
}
export function createEditorAgentAttachmentRef(
input: Omit<EditorAgentAttachmentRef, 'label'> & {
label?: string | null;
},
fallbackLabel: string = input.referenceId,
): EditorAgentAttachmentRef {
return {
...input,
label: normalizeEditorAgentAttachmentLabel(input.label, fallbackLabel),
};
}
export interface EditorAgentGeneratedImage {
resourceId?: string | null;
objectKey?: string | null;
@@ -159,8 +159,14 @@ pub async fn editor_agent_message(
let mut attachment_info = String::new();
attachment_info.push_str("user added these image ids to context: ");
for (i, attachment) in attachments.iter().enumerate() {
attachment_info
.push_str(&format!("[{i}]: {}, ", attachment.clone().into_image_id()));
let image_label_str = match &attachment.label {
None => "".to_string(),
Some(label) => {
format!(" {label}")
}
};
let image_id = attachment.clone().into_image_id();
attachment_info.push_str(&format!("({i}{image_label_str}): {image_id}, "));
}
document.messages.push(EditorAgentMessage {
id: document.messages.len(),
@@ -10,7 +10,7 @@ function AttachmentChip({
attachment: EditorAgentAttachmentRef;
onRemove?: () => void;
}) {
const label = attachment.label?.trim() || attachment.referenceId;
const label = attachment.label;
return (
<span className="group relative inline-flex max-w-full items-center gap-1.5 rounded-full border border-slate-200 bg-white px-2.5 py-1 text-xs text-slate-600 shadow-sm">
<ImageIcon className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
@@ -218,7 +218,7 @@ describe('EditorAgentConversationPanelView', () => {
{
id: 'layer-1',
resourceId: 'resource-1',
title: '角色图层',
title: '一二三四五六七八九十甲乙',
src: '/generated/role.png',
x: 0,
y: 0,
@@ -284,13 +284,13 @@ describe('EditorAgentConversationPanelView', () => {
).toBeNull();
fireEvent.click(
within(attachmentDialog).getByRole('checkbox', {
name: '选择画布图片 角色图层',
name: '选择画布图片 一二三四五六七八九十',
}),
);
fireEvent.click(
within(attachmentDialog).getByRole('button', { name: '应用' }),
);
expect(screen.getByText('角色图层')).toBeTruthy();
expect(screen.getByText('一二三四五六七八九十')).toBeTruthy();
fireEvent.change(screen.getByLabelText('发送给画布 Agent'), {
target: { value: '参考附件做像素风' },
@@ -308,6 +308,7 @@ describe('EditorAgentConversationPanelView', () => {
expect.objectContaining({
source: 'canvas_resource',
referenceId: 'resource-1',
label: '一二三四五六七八九十',
}),
],
}),
@@ -18,6 +18,7 @@ import {
} from 'react';
import {
createEditorAgentAttachmentRef,
EDITOR_AGENT_MAX_ATTACHMENTS,
type EditorAgentAttachmentRef,
} from '@/packages/shared/src/contracts';
@@ -84,16 +85,17 @@ function createCanvasAttachmentOptions(
layers: CanvasLayer[] = [],
): EditorAgentAttachmentOption[] {
return layers.filter(isImageLayer).map((layer) => {
const attachment: EditorAgentAttachmentRef = {
const referenceId = layer.resourceId || layer.id;
const attachment = createEditorAgentAttachmentRef({
source: 'canvas_resource',
referenceId: layer.resourceId || layer.id,
referenceId,
objectKey: layer.objectKey ?? null,
imageSrc: layer.src,
thumbnailSrc: layer.thumbnailSrc ?? null,
label: layer.title,
width: layer.width,
height: layer.height,
};
});
return {
key: attachmentKey(attachment),
sourceLabel: '画布',
@@ -106,7 +108,7 @@ function createLibraryAttachmentOptions(
assets: EditorAsset[] = [],
): EditorAgentAttachmentOption[] {
return assets.filter(isImageAsset).map((asset) => {
const attachment: EditorAgentAttachmentRef = {
const attachment = createEditorAgentAttachmentRef({
source: 'library_asset',
referenceId: asset.id,
objectKey: asset.objectKey ?? null,
@@ -115,7 +117,7 @@ function createLibraryAttachmentOptions(
label: asset.label,
width: asset.width,
height: asset.height,
};
});
return {
key: attachmentKey(attachment),
sourceLabel: '素材库',
@@ -198,8 +200,7 @@ function AttachmentPickerModal({
) : null}
<div className="grid grid-cols-2 gap-2 overflow-y-auto sm:grid-cols-3">
{visibleOptions.map((option) => {
const label =
option.attachment.label?.trim() || option.attachment.referenceId;
const label = option.attachment.label;
return (
<label
key={option.key}
@@ -414,16 +415,19 @@ export function EditorAgentConversationPanelView({
height,
sourceType: 'uploaded',
});
return {
source: 'canvas_resource',
referenceId: resource.resourceId,
objectKey: resource.objectKey ?? upload.objectKey,
imageSrc: resource.imageSrc,
thumbnailSrc: null,
label: resource.label ?? '粘贴图片',
width: resource.width,
height: resource.height,
};
return createEditorAgentAttachmentRef(
{
source: 'canvas_resource',
referenceId: resource.resourceId,
objectKey: resource.objectKey ?? upload.objectKey,
imageSrc: resource.imageSrc,
thumbnailSrc: null,
label: resource.label ?? file.name,
width: resource.width,
height: resource.height,
},
'粘贴图片',
);
};
function extractClipboardImageFiles(
clipboardData: DataTransfer | null,
@@ -3,10 +3,11 @@
import { act, renderHook, waitFor } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type {
EditorAgentConversationDetail,
EditorAgentMessage,
EditorAgentMessageResponse,
import {
createEditorAgentAttachmentRef,
type EditorAgentConversationDetail,
type EditorAgentMessage,
type EditorAgentMessageResponse,
} from '../../../../packages/shared/src/contracts/editorAgent.ts';
import {
EDITOR_AGENT_PATIENCE_NOTICE_DELAY_MS,
@@ -545,13 +546,13 @@ describe('useEditorAgentConversation', () => {
await act(async () => {
await result.current.sendMessage('', [
{
createEditorAgentAttachmentRef({
source: 'canvas_resource',
referenceId: 'resource-1',
objectKey: 'generated-editor-assets/resource-1.png',
imageSrc: '/resource-1.png',
label: '参考图',
},
label: '一二三四五六七八九十甲乙',
}),
]);
});
@@ -563,6 +564,7 @@ describe('useEditorAgentConversation', () => {
expect.objectContaining({
source: 'canvas_resource',
referenceId: 'resource-1',
label: '一二三四五六七八九十',
}),
],
}),