PicoBot/src/command/adapters/websocket.rs
oudecheng cda14360af chore: 建立工程化基线(rustfmt + clippy + CI + eslint + prettier)
配置:
- rustfmt.toml: 固化 max_width=100 / 4 空格缩进,cargo fmt 全量格式化
- Cargo.toml: 配置 [lints.rust] 与 [lints.clippy] 渐进式规则
- .github/workflows/ci.yml: Rust(fmt+clippy+test) + 前端(eslint+tsc+test) 双平台 CI
- Makefile: 新增 check/fmt/fix 目标,clippy 对齐 --all-targets --all-features
- web: eslint flat config + prettier 配置 + package.json 脚本与依赖
- src/main.rs: loop→while 修复 clippy::never_loop

对抗性审查发现并修复:
- eslint 缺 caughtErrorsIgnorePattern 导致 catch(_) 误报为 error
- 前端 lint 未接入 CI,现已补上 Lint 步骤
- Makefile 与 CI 的 clippy flags 不一致,已对齐
2026-08-03 23:24:02 +08:00

335 lines
14 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.

use crate::command::Command;
use crate::command::adapter::{AdapterError, InputAdapter, OutputAdapter};
use crate::command::context::AdapterContext;
use crate::command::response::{CommandResponse, MessageKind};
use crate::protocol::WsOutbound;
/// WebSocket 输入适配器
///
/// 将 WebSocket 的 JSON 输入直接反序列化为 Command
pub struct WebSocketInputAdapter;
impl WebSocketInputAdapter {
/// 创建新的 WebSocket 输入适配器
pub fn new() -> Self {
Self
}
}
impl Default for WebSocketInputAdapter {
fn default() -> Self {
Self::new()
}
}
impl InputAdapter for WebSocketInputAdapter {
fn try_parse(
&self,
input: &str,
_ctx: AdapterContext,
) -> Result<Option<Command>, AdapterError> {
// 尝试将 JSON 反序列化为 Command
// 如果失败,说明不是 Command 消息,返回 None
match serde_json::from_str(input) {
Ok(cmd) => Ok(Some(cmd)),
Err(_) => Ok(None),
}
}
}
/// WebSocket 输出适配器
///
/// 将 CommandResponse 转换为 WsOutbound 消息列表
pub struct WebSocketOutputAdapter;
impl WebSocketOutputAdapter {
/// 创建新的 WebSocket 输出适配器
pub fn new() -> Self {
Self
}
}
impl Default for WebSocketOutputAdapter {
fn default() -> Self {
Self::new()
}
}
impl OutputAdapter for WebSocketOutputAdapter {
type Output = Vec<WsOutbound>;
fn adapt(&self, response: CommandResponse) -> Vec<WsOutbound> {
let mut outbounds = Vec::new();
// 如果出错,返回错误消息
if let Some(error) = response.error {
outbounds.push(WsOutbound::Error {
code: error.code,
message: error.message,
timestamp: Some(crate::protocol::now_timestamp()),
subagent_task_id: None,
});
return outbounds;
}
// 转换响应消息为 WsOutbound
for msg in &response.messages {
let outbound = match msg.kind {
MessageKind::Text => WsOutbound::AssistantResponse {
id: response.request_id.to_string(),
content: msg.content.clone(),
role: "assistant".to_string(),
attachments: Vec::new(),
subagent_task_id: None,
topic_id: None,
timestamp: Some(crate::protocol::now_timestamp()),
reasoning_content: None,
user_message_id: None,
},
MessageKind::Notification => {
// 根据元数据判断具体类型
// 优先识别话题重命名(同时含 topics + topic_id + title
if let (Some(topics_json), Some(topic_id), Some(title)) = (
response.metadata.get("topics"),
response.metadata.get("topic_id"),
response.metadata.get("title"),
) {
match serde_json::from_str::<Vec<crate::protocol::TopicSummary>>(
topics_json,
) {
Ok(topics) => {
let session_id = response
.metadata
.get("session_id")
.cloned()
.unwrap_or_default();
WsOutbound::TopicRenamed {
topics,
session_id,
topic_id: topic_id.clone(),
title: title.clone(),
}
}
Err(_) => WsOutbound::AssistantResponse {
id: response.request_id.to_string(),
content: msg.content.clone(),
role: "assistant".to_string(),
attachments: Vec::new(),
subagent_task_id: None,
topic_id: None,
timestamp: Some(crate::protocol::now_timestamp()),
reasoning_content: None,
user_message_id: None,
},
}
} else if let Some(topics_json) = response.metadata.get("topics") {
// Topic 列表响应 - 优先检查 topics
match serde_json::from_str::<Vec<crate::protocol::TopicSummary>>(
topics_json,
) {
Ok(topics) => {
let session_id = response
.metadata
.get("session_id")
.cloned()
.unwrap_or_default();
WsOutbound::TopicList { topics, session_id }
}
Err(_) => WsOutbound::AssistantResponse {
id: response.request_id.to_string(),
content: msg.content.clone(),
role: "assistant".to_string(),
attachments: Vec::new(),
subagent_task_id: None,
topic_id: None,
timestamp: Some(crate::protocol::now_timestamp()),
reasoning_content: None,
user_message_id: None,
},
}
} else if let Some(session_id) = response.metadata.get("session_id") {
// 有 session_id 但没有 topic_id 的是创建会话
if response.metadata.get("topic_id").is_none() {
WsOutbound::SessionCreated {
session_id: session_id.clone(),
title: msg.content.clone(),
}
} else {
// 加载会话
let message_count = response
.metadata
.get("message_count")
.and_then(|s| s.parse().ok())
.unwrap_or(0);
WsOutbound::SessionLoaded {
session_id: session_id.clone(),
title: msg.content.clone(),
message_count,
}
}
} else if let Some(topic_id) = response.metadata.get("topic_id") {
// 只有 topic_id可能是加载话题
let message_count = response
.metadata
.get("message_count")
.and_then(|s| s.parse().ok())
.unwrap_or(0);
WsOutbound::SessionLoaded {
session_id: topic_id.clone(),
title: msg.content.clone(),
message_count,
}
} else if let Some(channels_json) = response.metadata.get("channels") {
// 通道列表响应
match serde_json::from_str::<Vec<crate::protocol::Channel>>(channels_json) {
Ok(channels) => WsOutbound::ChannelList { channels },
Err(_) => WsOutbound::AssistantResponse {
id: response.request_id.to_string(),
content: msg.content.clone(),
role: "assistant".to_string(),
attachments: Vec::new(),
subagent_task_id: None,
topic_id: None,
timestamp: Some(crate::protocol::now_timestamp()),
reasoning_content: None,
user_message_id: None,
},
}
} else if let Some(sessions_json) = response.metadata.get("sessions") {
// 会话列表响应
match serde_json::from_str::<Vec<crate::protocol::SessionSummary>>(
sessions_json,
) {
Ok(sessions) => {
let channel_name = response.metadata.get("channel_name").cloned();
WsOutbound::SessionList {
sessions,
current_session_id: None,
channel_name,
}
}
Err(_) => WsOutbound::AssistantResponse {
id: response.request_id.to_string(),
content: msg.content.clone(),
role: "assistant".to_string(),
attachments: Vec::new(),
subagent_task_id: None,
topic_id: None,
timestamp: Some(crate::protocol::now_timestamp()),
reasoning_content: None,
user_message_id: None,
},
}
} else if let Some(topics_json) = response.metadata.get("topics") {
// Topic 列表响应
match serde_json::from_str::<Vec<crate::protocol::TopicSummary>>(
topics_json,
) {
Ok(topics) => {
let session_id = response
.metadata
.get("session_id")
.cloned()
.unwrap_or_default();
WsOutbound::TopicList { topics, session_id }
}
Err(_) => WsOutbound::AssistantResponse {
id: response.request_id.to_string(),
content: msg.content.clone(),
role: "assistant".to_string(),
attachments: Vec::new(),
subagent_task_id: None,
topic_id: None,
timestamp: Some(crate::protocol::now_timestamp()),
reasoning_content: None,
user_message_id: None,
},
}
} else {
// 默认通知
WsOutbound::AssistantResponse {
id: response.request_id.to_string(),
content: msg.content.clone(),
role: "assistant".to_string(),
attachments: Vec::new(),
subagent_task_id: None,
topic_id: None,
timestamp: Some(crate::protocol::now_timestamp()),
reasoning_content: None,
user_message_id: None,
}
}
}
MessageKind::Error => WsOutbound::Error {
code: "RESPONSE_ERROR".to_string(),
message: msg.content.clone(),
timestamp: Some(crate::protocol::now_timestamp()),
subagent_task_id: None,
},
_ => WsOutbound::AssistantResponse {
id: response.request_id.to_string(),
content: msg.content.clone(),
role: "assistant".to_string(),
attachments: Vec::new(),
subagent_task_id: None,
topic_id: None,
timestamp: Some(crate::protocol::now_timestamp()),
reasoning_content: None,
user_message_id: None,
},
};
outbounds.push(outbound);
}
outbounds
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_websocket_input_adapter_valid_command() {
let adapter = WebSocketInputAdapter::new();
let ctx = AdapterContext::new("test");
let json = r#"{"type":"create_session","title":"my session"}"#;
let result = adapter.try_parse(json, ctx).unwrap();
assert!(result.is_some());
let cmd = result.unwrap();
assert!(matches!(
cmd,
Command::CreateSession {
title: Some(ref t)
} if t == "my session"
));
}
#[test]
fn test_websocket_input_adapter_invalid_json() {
let adapter = WebSocketInputAdapter::new();
let ctx = AdapterContext::new("test");
let json = "not a command";
let result = adapter.try_parse(json, ctx).unwrap();
assert!(result.is_none());
}
#[test]
fn test_websocket_output_adapter_session_created() {
let adapter = WebSocketOutputAdapter::new();
let request_id = uuid::Uuid::new_v4();
let response = CommandResponse::success(request_id)
.with_message(MessageKind::Notification, "My Session")
.with_metadata("session_id", "abc123");
let outbounds = adapter.adapt(response);
assert_eq!(outbounds.len(), 1);
assert!(matches!(outbounds[0], WsOutbound::SessionCreated { .. }));
}
}