zerolaunch_plugin_sdk_rust/
host_proxy.rs1use dashmap::DashMap;
7use std::sync::atomic::{AtomicU64, Ordering};
8use std::sync::Arc;
9use std::time::Duration;
10use tokio::sync::{mpsc, oneshot};
11use zerolaunch_plugin_api::services::model::{
12 ModelChatRequest, ModelChatResponse, ModelEmbeddingRequest, ModelEmbeddingResponse, ModelInfo,
13 ModelSimilarityRequest, ModelSimilarityResponse,
14};
15use zerolaunch_plugin_protocol::methods::host;
16use zerolaunch_plugin_protocol::JsonRpcError;
17
18use base64::Engine as _;
19
20pub struct HostProxy {
23 next_id: AtomicU64,
25 pending: Arc<DashMap<u64, oneshot::Sender<Result<serde_json::Value, JsonRpcError>>>>,
28 outbound_tx: mpsc::Sender<Vec<u8>>,
30}
31
32impl HostProxy {
33 pub fn new(
34 pending: Arc<DashMap<u64, oneshot::Sender<Result<serde_json::Value, JsonRpcError>>>>,
35 outbound_tx: mpsc::Sender<Vec<u8>>,
36 ) -> Self {
37 Self {
38 next_id: AtomicU64::new(1),
39 pending,
40 outbound_tx,
41 }
42 }
43
44 async fn send_request(
47 &self,
48 method: &str,
49 params: serde_json::Value,
50 ) -> Result<serde_json::Value, String> {
51 let is_model_call = method.starts_with("host/model.");
54 let timeout = if is_model_call {
55 Duration::from_secs(300)
56 } else {
57 Duration::from_secs(30)
58 };
59 self.send_request_inner(method, params, timeout).await
60 }
61
62 async fn send_request_inner(
64 &self,
65 method: &str,
66 params: serde_json::Value,
67 timeout: Duration,
68 ) -> Result<serde_json::Value, String> {
69 let id = self.next_id.fetch_add(1, Ordering::SeqCst);
70 let request = serde_json::json!({
71 "jsonrpc": "2.0",
72 "id": id,
73 "method": method,
74 "params": params,
75 });
76
77 let payload = serde_json::to_vec(&request).map_err(|e| e.to_string())?;
78
79 let (tx, rx) = oneshot::channel();
81 self.pending.insert(id, tx);
82
83 if self.outbound_tx.send(payload).await.is_err() {
87 self.pending.remove(&id);
88 return Err("write channel closed".to_string());
89 }
90
91 match tokio::time::timeout(timeout, rx).await {
95 Ok(Ok(Ok(value))) => Ok(value),
96 Ok(Ok(Err(err))) => Err(format!(
97 "host call failed (code {}): {}",
98 err.code, err.message
99 )),
100 Ok(Err(_)) => Err("response channel closed".to_string()),
101 Err(_) => {
102 self.pending.remove(&id);
104 Err("host call timed out".to_string())
105 }
106 }
107 }
108
109 pub async fn log(&self, level: &str, message: &str) -> Result<(), String> {
110 self.send_request(
111 host::LOG,
112 serde_json::json!({ "level": level, "message": message }),
113 )
114 .await?;
115 Ok(())
116 }
117
118 pub fn log_no_wait(&self, level: &str, message: &str) {
124 let id = self.next_id.fetch_add(1, Ordering::SeqCst);
125 let Ok(payload) = serde_json::to_vec(&serde_json::json!({
126 "jsonrpc": "2.0",
127 "id": id,
128 "method": host::LOG,
129 "params": { "level": level, "message": message },
130 })) else {
131 return;
132 };
133
134 let (tx, _rx) = oneshot::channel(); self.pending.insert(id, tx);
136
137 if self.outbound_tx.try_send(payload).is_err() {
140 self.pending.remove(&id);
141 }
142 }
143
144 pub async fn shell_open(&self, target: &str) -> Result<(), String> {
145 self.send_request(host::SHELL_OPEN, serde_json::json!({ "target": target }))
146 .await?;
147 Ok(())
148 }
149
150 pub async fn get_icon(&self, path: &str) -> Result<String, String> {
153 let result = self
154 .send_request(
155 host::ICON_GET,
156 serde_json::json!({ "request": { "path": path }, "level": "Full" }),
157 )
158 .await?;
159 Ok(result.as_str().unwrap_or("").to_string())
160 }
161
162 pub async fn shell_execute_command(&self, cmd: &str) -> Result<(), String> {
163 self.send_request(
164 host::SHELL_EXECUTE_COMMAND,
165 serde_json::json!({ "cmd": cmd }),
166 )
167 .await?;
168 Ok(())
169 }
170
171 pub async fn shell_open_folder(&self, path: &str) -> Result<(), String> {
172 self.send_request(host::SHELL_OPEN_FOLDER, serde_json::json!({ "path": path }))
173 .await?;
174 Ok(())
175 }
176
177 pub async fn shell_execute_elevation(&self, path: &str) -> Result<(), String> {
178 self.send_request(
179 host::SHELL_EXECUTE_ELEVATION,
180 serde_json::json!({ "path": path }),
181 )
182 .await?;
183 Ok(())
184 }
185
186 pub async fn notify(&self, title: &str, message: &str) -> Result<(), String> {
187 self.send_request(
188 host::NOTIFY,
189 serde_json::json!({ "title": title, "message": message }),
190 )
191 .await?;
192 Ok(())
193 }
194
195 pub async fn get_locale(&self) -> Result<String, String> {
197 let result = self
198 .send_request(host::GET_LOCALE, serde_json::json!(null))
199 .await?;
200 Ok(result.as_str().unwrap_or("").to_string())
201 }
202
203 pub async fn get_theme(&self) -> Result<String, String> {
205 let result = self
206 .send_request(host::GET_THEME, serde_json::Value::Null)
207 .await?;
208 result
209 .as_str()
210 .map(str::to_string)
211 .ok_or_else(|| "host theme response is not a string".to_string())
212 }
213
214 pub async fn model_list(&self) -> Result<Vec<ModelInfo>, String> {
216 let result = self
217 .send_request(host::MODEL_LIST, serde_json::Value::Null)
218 .await?;
219 serde_json::from_value(result).map_err(|e| e.to_string())
220 }
221
222 pub async fn model_chat(&self, req: ModelChatRequest) -> Result<ModelChatResponse, String> {
224 let params = serde_json::to_value(req).map_err(|e| e.to_string())?;
225 let result = self.send_request(host::MODEL_CHAT, params).await?;
226 serde_json::from_value(result).map_err(|e| e.to_string())
227 }
228
229 pub async fn model_embedding(
231 &self,
232 req: ModelEmbeddingRequest,
233 ) -> Result<ModelEmbeddingResponse, String> {
234 let params = serde_json::to_value(req).map_err(|e| e.to_string())?;
235 let result = self.send_request(host::MODEL_EMBEDDING, params).await?;
236 serde_json::from_value(result).map_err(|e| e.to_string())
237 }
238
239 pub async fn model_similarity(
241 &self,
242 req: ModelSimilarityRequest,
243 ) -> Result<ModelSimilarityResponse, String> {
244 let params = serde_json::to_value(req).map_err(|e| e.to_string())?;
245 let result = self.send_request(host::MODEL_SIMILARITY, params).await?;
246 serde_json::from_value(result).map_err(|e| e.to_string())
247 }
248
249 pub async fn enumerate_apps(&self) -> Result<serde_json::Value, String> {
250 self.send_request(host::APP_ENUMERATE, serde_json::json!(null))
251 .await
252 }
253
254 pub async fn resolve_path(&self, kind: &str) -> Result<String, String> {
255 let result = self
256 .send_request(host::PATH_RESOLVE, serde_json::json!({ "kind": kind }))
257 .await?;
258 Ok(result.as_str().unwrap_or("").to_string())
259 }
260
261 pub async fn resource_upload(
264 &self,
265 resource_id: &str,
266 file_path: &str,
267 max_size: Option<u64>,
268 ) -> Result<String, String> {
269 let result = self
270 .send_request(
271 host::RESOURCE_UPLOAD,
272 serde_json::json!({
273 "resourceId": resource_id,
274 "filePath": file_path,
275 "maxSize": max_size,
276 }),
277 )
278 .await?;
279 Ok(result.as_str().unwrap_or("").to_string())
280 }
281
282 pub async fn resource_get(&self, resource_id: &str) -> Result<Vec<u8>, String> {
283 let result = self
284 .send_request(
285 host::RESOURCE_GET,
286 serde_json::json!({
287 "resourceId": resource_id,
288 }),
289 )
290 .await?;
291 let b64 = result.as_str().unwrap_or("");
292 base64::engine::general_purpose::STANDARD
293 .decode(b64)
294 .map_err(|e| format!("base64 decode failed: {}", e))
295 }
296
297 pub async fn resource_put(&self, resource_id: &str, data: &[u8]) -> Result<(), String> {
299 let b64 = base64::engine::general_purpose::STANDARD.encode(data);
300 self.send_request(
301 host::RESOURCE_PUT,
302 serde_json::json!({
303 "resourceId": resource_id,
304 "bytesB64": b64,
305 }),
306 )
307 .await?;
308 Ok(())
309 }
310
311 pub async fn resource_delete(&self, resource_id: &str) -> Result<(), String> {
313 self.send_request(
314 host::RESOURCE_DELETE,
315 serde_json::json!({
316 "resourceId": resource_id,
317 }),
318 )
319 .await?;
320 Ok(())
321 }
322
323 pub async fn resource_list(&self) -> Result<Vec<String>, String> {
325 let result = self
326 .send_request(host::RESOURCE_LIST, serde_json::json!({}))
327 .await?;
328 serde_json::from_value(result).map_err(|e| format!("parse resource list failed: {}", e))
329 }
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335
336 fn make_proxy() -> (Arc<HostProxy>, mpsc::Receiver<Vec<u8>>) {
337 let (tx, rx) = mpsc::channel::<Vec<u8>>(16);
338 let pending: Arc<DashMap<u64, oneshot::Sender<Result<serde_json::Value, JsonRpcError>>>> =
339 Arc::new(DashMap::new());
340 (Arc::new(HostProxy::new(pending, tx)), rx)
341 }
342
343 #[tokio::test]
352 async fn send_request_posts_clean_json_without_embedded_frame() {
353 let (proxy, mut rx) = make_proxy();
354
355 proxy.log_no_wait("warn", "test message");
357 let bytes = rx.recv().await.expect("log_no_wait 应投递一条消息");
358 let text = String::from_utf8(bytes.clone()).expect("UTF-8");
359 assert!(
360 !text.contains("Content-Length"),
361 "outbound 通道中的消息不得包含帧头(双重分帧): {:?}",
362 text
363 );
364 let v: serde_json::Value = serde_json::from_slice(&bytes).expect("干净 JSON 可解析");
366 assert_eq!(v["method"], host::LOG);
367 assert_eq!(v["params"]["message"], "test message");
368 }
369
370 #[tokio::test]
372 async fn send_request_await_path_posts_clean_json() {
373 let (proxy, mut rx) = make_proxy();
374 let task = tokio::spawn(async move {
377 let _ = proxy.model_list().await; });
379 let bytes = tokio::time::timeout(Duration::from_secs(2), rx.recv())
380 .await
381 .expect("send_request 应投递一条消息")
382 .expect("通道未关闭");
383 let text = String::from_utf8_lossy(&bytes);
384 assert!(
385 !text.contains("Content-Length"),
386 "outbound 通道中的消息不得包含帧头(双重分帧): {:?}",
387 text
388 );
389 let v: serde_json::Value = serde_json::from_slice(&bytes).expect("干净 JSON 可解析");
390 assert_eq!(v["method"], host::MODEL_LIST);
391 task.abort();
393 }
394}