clean data structure
This commit is contained in:
@@ -15,11 +15,11 @@ use serde::Serialize;
|
||||
use serde_json::{Value, json};
|
||||
use module_editor_agent::agent::tool::Tool;
|
||||
use shared_contracts::editor_agent::{
|
||||
CreateEditorAgentConversationRequest, EditorAgentConversationListResponse,
|
||||
EditorAgentConversationMessagesDocument, EditorAgentConversationResponse,
|
||||
EditorAgentConversationSummary, EditorAgentGeneratedImage, EditorAgentMessage,
|
||||
EditorAgentMessageRole, EditorAgentToolCall, EditorAgentToolCallStatus,
|
||||
StreamEditorAgentMessageRequest,
|
||||
CreateEditorAgentConversationRequest, EditorAgentConversationListResponse,
|
||||
EditorAgentConversationMessagesDocument, EditorAgentConversationResponse,
|
||||
EditorAgentConversationSummary, EditorAgentGeneratedImage, EditorAgentMessage,
|
||||
EditorAgentMessageRole, EditorAgentToolCall, EditorAgentToolCallStatus,
|
||||
EditorAgentMessageRequest,
|
||||
};
|
||||
use spacetime_client::{
|
||||
EditorAgentConversationCreateRecordInput, EditorAgentConversationDeleteRecordInput,
|
||||
@@ -45,11 +45,11 @@ use crate::state::AppState;
|
||||
use shared_kernel::{build_prefixed_uuid_id, normalize_optional_string};
|
||||
|
||||
pub async fn editor_agent_message(
|
||||
State(state): State<AppState>,
|
||||
Path(conversation_id): Path<String>,
|
||||
Extension(_request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Json(payload): Json<StreamEditorAgentMessageRequest>,
|
||||
State(state): State<AppState>,
|
||||
Path(conversation_id): Path<String>,
|
||||
Extension(_request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Json(payload): Json<EditorAgentMessageRequest>,
|
||||
) -> Result<Json<EditorAgentMessageResponse>, AppError> {
|
||||
let owner_user_id = authenticated.claims().user_id().to_string();
|
||||
require_editor_agent_sidebar_enabled(&state, owner_user_id.as_str()).await?;
|
||||
|
||||
@@ -8,17 +8,6 @@ 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 {
|
||||
@@ -27,23 +16,6 @@ pub enum EditorAgentMessageRole {
|
||||
System
|
||||
}
|
||||
|
||||
#[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")]
|
||||
@@ -52,16 +24,6 @@ pub enum EditorAgentAttachmentSource {
|
||||
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 EditorAgentToolCallStatus {
|
||||
@@ -183,209 +145,8 @@ pub struct EditorAgentConversationResponse {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct StreamEditorAgentMessageRequest {
|
||||
pub client_message_id: String,
|
||||
pub struct EditorAgentMessageRequest {
|
||||
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<EditorAgentToolCallStatus>,
|
||||
#[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),
|
||||
}],
|
||||
tool_calls: 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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user