Files
Genarrative/server-rs/crates/shared-contracts/src/editor_agent.rs
T
kdletters d7d98acfd4 接入VectorEngine画布Agent规划
切换画布 Agent 与通用 LLM 代理到 VectorEngine gpt-5.4-mini。

为画布 Agent 注入上一轮生成结果并默认承接上一张图编辑。

补齐规范图工具分流、规范展板 prompt 要求和 JSON 残片回复兜底。

同步前后端契约、测试脚本、环境示例和项目文档。
2026-07-05 17:42:27 +08:00

397 lines
13 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 画布Agent对话契约:与 packages/shared/src/contracts/editorAgent.ts 保持同形。
//! 会话元数据存 SpacetimeDB,消息正文整体存 OSS(editor-agent/{conversationId}.json)。
use serde::{Deserialize, Serialize};
pub const EDITOR_AGENT_MAX_ATTACHMENTS: usize = 9;
pub const EDITOR_AGENT_TITLE_MAX_CHARS: usize = 20;
pub const EDITOR_AGENT_DEFAULT_CONVERSATION_TITLE: &str = "新对话";
pub const EDITOR_AGENT_MESSAGES_DOCUMENT_VERSION: u32 = 1;
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum EditorAgentStage {
Idle,
Thinking,
Responding,
Generating,
Completed,
Failed,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum EditorAgentMessageRole {
User,
Assistant,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum EditorAgentMessageKind {
Chat,
Stage,
Error,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum EditorAgentMessageStatus {
Streaming,
Generating,
Completed,
Failed,
Stopped,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum EditorAgentAttachmentSource {
CanvasResource,
LibraryAsset,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum EditorAgentToolName {
GenerateImage,
EditImage,
GenerateCharacter,
GenerateIconSpritesheet,
GenerateUiDesign,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum EditorAgentGenerationStatus {
Generating,
Completed,
Failed,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EditorAgentAttachmentRef {
pub source: EditorAgentAttachmentSource,
pub reference_id: String,
#[serde(default)]
pub object_key: Option<String>,
pub image_src: String,
#[serde(default)]
pub thumbnail_src: Option<String>,
#[serde(default)]
pub label: Option<String>,
#[serde(default)]
pub width: Option<u32>,
#[serde(default)]
pub height: Option<u32>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EditorAgentGeneratedImage {
#[serde(default)]
pub resource_id: Option<String>,
#[serde(default)]
pub object_key: Option<String>,
#[serde(default)]
pub asset_object_id: Option<String>,
pub image_src: String,
#[serde(default)]
pub thumbnail_src: Option<String>,
#[serde(default)]
pub width: Option<u32>,
#[serde(default)]
pub height: Option<u32>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EditorAgentGenerationRecord {
pub tool_call_id: String,
pub tool_name: EditorAgentToolName,
#[serde(default)]
pub summary: Option<String>,
#[serde(default)]
pub task_id: Option<String>,
pub status: EditorAgentGenerationStatus,
#[serde(default)]
pub model: Option<String>,
#[serde(default)]
pub images: Vec<EditorAgentGeneratedImage>,
#[serde(default)]
pub error: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EditorAgentMessage {
pub id: String,
pub role: EditorAgentMessageRole,
pub kind: EditorAgentMessageKind,
pub text: String,
#[serde(default)]
pub attachments: Vec<EditorAgentAttachmentRef>,
#[serde(default)]
pub generations: Vec<EditorAgentGenerationRecord>,
pub status: EditorAgentMessageStatus,
pub created_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EditorAgentConversationSummary {
pub conversation_id: String,
pub project_id: String,
pub title: String,
pub created_at: String,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EditorAgentConversationDetail {
pub conversation_id: String,
pub project_id: String,
pub title: String,
pub created_at: String,
pub updated_at: String,
pub messages: Vec<EditorAgentMessage>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EditorAgentConversationMessagesDocument {
pub version: u32,
pub conversation_id: String,
pub messages: Vec<EditorAgentMessage>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreateEditorAgentConversationRequest {
#[serde(default)]
pub title: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EditorAgentConversationListResponse {
pub conversations: Vec<EditorAgentConversationSummary>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EditorAgentConversationResponse {
pub conversation: EditorAgentConversationDetail,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct StreamEditorAgentMessageRequest {
pub client_message_id: String,
pub text: String,
#[serde(default)]
pub attachments: Vec<EditorAgentAttachmentRef>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum EditorAgentSseEventType {
Stage,
MessageDelta,
ToolStarted,
ToolCompleted,
GenerationResult,
Error,
Done,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EditorAgentSseEnvelope {
pub event: EditorAgentSseEventType,
pub data: serde_json::Value,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EditorAgentStageEvent {
pub conversation_id: String,
pub stage: EditorAgentStage,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EditorAgentMessageDeltaEvent {
pub conversation_id: String,
pub message_id: String,
pub role: EditorAgentMessageRole,
pub kind: EditorAgentMessageKind,
pub text_delta: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EditorAgentToolEvent {
pub conversation_id: String,
pub message_id: String,
pub tool_call_id: String,
pub tool_name: EditorAgentToolName,
#[serde(default)]
pub summary: Option<String>,
#[serde(default)]
pub task_id: Option<String>,
#[serde(default)]
pub model: Option<String>,
#[serde(default)]
pub status: Option<EditorAgentGenerationStatus>,
#[serde(default)]
pub error: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EditorAgentGenerationResultEvent {
pub conversation_id: String,
pub message_id: String,
pub tool_call_id: String,
pub tool_name: EditorAgentToolName,
#[serde(default)]
pub model: Option<String>,
pub images: Vec<EditorAgentGeneratedImage>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EditorAgentErrorEvent {
#[serde(default)]
pub conversation_id: Option<String>,
pub code: String,
pub message: String,
pub recoverable: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EditorAgentDoneEvent {
pub conversation_id: String,
#[serde(default)]
pub title: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn editor_agent_conversation_detail_uses_camel_case() {
let detail = EditorAgentConversationDetail {
conversation_id: "editor-agent-conv-1".to_string(),
project_id: "project-1".to_string(),
title: "新对话".to_string(),
created_at: "2026-07-03T00:00:00Z".to_string(),
updated_at: "2026-07-03T00:00:01Z".to_string(),
messages: vec![EditorAgentMessage {
id: "message-1".to_string(),
role: EditorAgentMessageRole::User,
kind: EditorAgentMessageKind::Chat,
text: "帮我生成一张森林背景".to_string(),
attachments: vec![EditorAgentAttachmentRef {
source: EditorAgentAttachmentSource::CanvasResource,
reference_id: "resource-1".to_string(),
object_key: Some("editor/resource-1.png".to_string()),
image_src: "https://example.test/resource-1.png".to_string(),
thumbnail_src: None,
label: Some("森林参考".to_string()),
width: Some(1024),
height: Some(768),
}],
generations: vec![],
status: EditorAgentMessageStatus::Completed,
created_at: "2026-07-03T00:00:00Z".to_string(),
}],
};
let payload = serde_json::to_value(&detail).expect("detail should serialize");
assert_eq!(payload["conversationId"], json!("editor-agent-conv-1"));
assert_eq!(payload["messages"][0]["role"], json!("user"));
assert_eq!(
payload["messages"][0]["attachments"][0]["source"],
json!("canvas_resource")
);
assert_eq!(
payload["messages"][0]["attachments"][0]["referenceId"],
json!("resource-1")
);
assert_eq!(payload["messages"][0]["status"], json!("completed"));
let decoded: EditorAgentConversationDetail =
serde_json::from_value(payload).expect("detail should deserialize");
assert_eq!(decoded, detail);
}
#[test]
fn editor_agent_sse_events_serialize_event_names() {
let envelope = EditorAgentSseEnvelope {
event: EditorAgentSseEventType::GenerationResult,
data: serde_json::to_value(EditorAgentGenerationResultEvent {
conversation_id: "editor-agent-conv-1".to_string(),
message_id: "message-2".to_string(),
tool_call_id: "tool-call-1".to_string(),
tool_name: EditorAgentToolName::GenerateImage,
model: Some("z-image".to_string()),
images: vec![EditorAgentGeneratedImage {
resource_id: Some("resource-2".to_string()),
object_key: Some("generated/result.png".to_string()),
asset_object_id: Some("asset-object-2".to_string()),
image_src: "https://example.test/generated.png".to_string(),
thumbnail_src: None,
width: Some(1024),
height: Some(1024),
}],
})
.expect("event data should serialize"),
};
let payload = serde_json::to_value(envelope).expect("envelope should serialize");
assert_eq!(payload["event"], json!("generation_result"));
assert_eq!(payload["data"]["toolName"], json!("generate_image"));
assert_eq!(
payload["data"]["images"][0]["resourceId"],
json!("resource-2")
);
assert_eq!(
payload["data"]["images"][0]["objectKey"],
json!("generated/result.png")
);
let error_envelope = EditorAgentSseEnvelope {
event: EditorAgentSseEventType::Error,
data: serde_json::to_value(EditorAgentErrorEvent {
conversation_id: Some("editor-agent-conv-1".to_string()),
code: "INSUFFICIENT_BALANCE".to_string(),
message: "泥点余额不足,本次生成未执行".to_string(),
recoverable: true,
})
.expect("event data should serialize"),
};
let error_payload =
serde_json::to_value(error_envelope).expect("envelope should serialize");
assert_eq!(error_payload["event"], json!("error"));
assert_eq!(error_payload["data"]["recoverable"], json!(true));
}
#[test]
fn stream_request_defaults_attachments_to_empty() {
let payload = json!({
"clientMessageId": "client-message-1",
"text": "随便聊聊"
});
let decoded: StreamEditorAgentMessageRequest =
serde_json::from_value(payload).expect("request should deserialize");
assert!(decoded.attachments.is_empty());
}
}