clean data structure

This commit is contained in:
2026-07-10 10:45:20 +08:00
parent 37b4329fee
commit 2709563c60
2 changed files with 11 additions and 250 deletions
@@ -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());
}
}