Skip to main content

aiway_plugin/
plugin_ctx.rs

1//! 插件上下文接口定义
2//!
3//! 定义插件可访问的上下文操作,宿主侧和 WASM 侧分别提供实现。
4
5#[cfg(feature = "model")]
6use aiway_protocol::model::Provider;
7use serde::{Deserialize, Serialize};
8use std::any::Any;
9use std::collections::HashMap;
10
11use crate::PluginError;
12use aiway_protocol::context::HttpContext;
13
14/// 日志级别常量,与 WASM 侧和 Host 侧保持一致
15pub const LOG_ERROR: i32 = 1;
16pub const LOG_WARN: i32 = 2;
17pub const LOG_INFO: i32 = 3;
18pub const LOG_DEBUG: i32 = 4;
19pub const LOG_TRACE: i32 = 5;
20
21/// HTTP 请求参数
22#[derive(Serialize, Deserialize)]
23pub struct HttpRequest {
24    pub method: String,
25    pub url: String,
26    pub headers: Vec<(String, String)>,
27    pub body: Option<Vec<u8>>,
28    /// URL 编码表单(与 body/multipart 互斥,优先级: multipart > form > body)
29    pub form: Option<HashMap<String, String>>,
30    /// Multipart 表单(与 body/form 互斥,优先级最高)
31    pub multipart: Option<Vec<FormPart>>,
32    pub timeout_ms: u64,
33}
34
35/// Multipart 表单字段
36#[derive(Serialize, Deserialize)]
37pub struct FormPart {
38    pub key: String,
39    /// 字段值(文本或文件内容)
40    pub value: Vec<u8>,
41    /// 文件名(文件上传时设置)
42    pub file_name: Option<String>,
43    /// MIME 类型(如 "text/plain"、"image/png")
44    pub mime_type: Option<String>,
45}
46
47/// HTTP 请求构建器
48pub struct HttpRequestBuilder {
49    method: String,
50    url: String,
51    headers: Vec<(String, String)>,
52    body: Option<Vec<u8>>,
53    form: Option<HashMap<String, String>>,
54    multipart: Option<Vec<FormPart>>,
55    timeout_ms: u64,
56}
57
58impl HttpRequestBuilder {
59    /// 创建构建器,`method` 和 `url` 为必填项
60    pub fn new(method: impl Into<String>, url: impl Into<String>) -> Self {
61        Self {
62            method: method.into(),
63            url: url.into(),
64            headers: Vec::new(),
65            body: None,
66            form: None,
67            multipart: None,
68            timeout_ms: 10_000,
69        }
70    }
71
72    /// 添加请求头
73    pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
74        self.headers.push((key.into(), value.into()));
75        self
76    }
77
78    /// 设置原始请求体(与 form/multipart 互斥)
79    pub fn body(mut self, body: Vec<u8>) -> Self {
80        self.body = Some(body);
81        self
82    }
83
84    /// 设置 URL 编码表单(与 body/multipart 互斥)
85    pub fn form(mut self, form: HashMap<String, String>) -> Self {
86        self.form = Some(form);
87        self
88    }
89
90    /// 添加单个表单字段
91    pub fn add_form_field(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
92        self.form.get_or_insert_with(HashMap::new).insert(key.into(), value.into());
93        self
94    }
95
96    /// 设置 Multipart 表单字段列表(与 body/form 互斥)
97    pub fn multipart(mut self, parts: Vec<FormPart>) -> Self {
98        self.multipart = Some(parts);
99        self
100    }
101
102    /// 添加单个 Multipart 字段
103    pub fn add_multipart_part(mut self, part: FormPart) -> Self {
104        self.multipart.get_or_insert_with(Vec::new).push(part);
105        self
106    }
107
108    /// 设置超时时间(毫秒),默认 10000
109    pub fn timeout_ms(mut self, timeout_ms: u64) -> Self {
110        self.timeout_ms = timeout_ms;
111        self
112    }
113
114    /// 构建 HttpRequest
115    pub fn build(self) -> HttpRequest {
116        HttpRequest {
117            method: self.method,
118            url: self.url,
119            headers: self.headers,
120            body: self.body,
121            form: self.form,
122            multipart: self.multipart,
123            timeout_ms: self.timeout_ms,
124        }
125    }
126}
127
128/// HTTP 响应结果
129#[derive(Serialize, Deserialize)]
130pub struct HttpResponse {
131    pub status: u16,
132    pub headers: Vec<(String, String)>,
133    pub body: Vec<u8>,
134}
135
136impl HttpResponse {
137    /// 将响应体作为 UTF-8 文本返回
138    pub fn text(&self) -> Result<String, PluginError> {
139        String::from_utf8(self.body.clone())
140            .map_err(|e| PluginError::HttpError(format!("invalid UTF-8 response: {e}")))
141    }
142
143    /// 将响应体作为 JSON 反序列化
144    pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T, PluginError> {
145        serde_json::from_slice(&self.body)
146            .map_err(|e| PluginError::SerializeError(format!("JSON deserialize failed: {e}")))
147    }
148}
149
150/// 插件上下文接口
151///
152/// 宿主侧通过 `HttpContext` 实现,WASM 侧通过 `WasmHttpContext` 实现。
153/// 插件开发者面向此 trait 编程,不依赖具体实现。
154pub trait PluginContext: Send {
155    /// 请求 ID
156    fn request_id(&self) -> String;
157    /// 请求时间戳(毫秒)
158    fn request_ts(&self) -> i64;
159    /// 是否为 SSE 连接
160    fn is_sse(&self) -> bool;
161    /// 是否为 WebSocket 连接
162    fn is_websocket(&self) -> bool;
163    /// 路由名称
164    fn get_route_name(&self) -> Option<String>;
165    /// 路由目标地址
166    fn get_routing_url(&self) -> Option<String>;
167    /// 响应体大小
168    fn get_response_body_size(&self) -> Option<i64>;
169    /// 设置响应体大小
170    fn set_response_body_size(&mut self, size: i64);
171    /// 模型名称(仅模型插件可用)
172    #[cfg(feature = "model")]
173    fn get_model_name(&self) -> Option<String>;
174    /// 模型提供商(仅模型插件可用)
175    #[cfg(feature = "model")]
176    fn get_model_provider(&self) -> Option<Provider>;
177
178    /// 输出日志(底层接口,level 使用 LOG_* 常量)
179    fn log(&self, level: i32, msg: &str);
180    /// 输出 ERROR 级别日志
181    fn log_error(&self, msg: &str) { self.log(LOG_ERROR, msg); }
182    /// 输出 WARN 级别日志
183    fn log_warn(&self, msg: &str) { self.log(LOG_WARN, msg); }
184    /// 输出 INFO 级别日志
185    fn log_info(&self, msg: &str) { self.log(LOG_INFO, msg); }
186    /// 输出 DEBUG 级别日志
187    fn log_debug(&self, msg: &str) { self.log(LOG_DEBUG, msg); }
188    /// 输出 TRACE 级别日志
189    fn log_trace(&self, msg: &str) { self.log(LOG_TRACE, msg); }
190
191    /// 发起 HTTP 请求(默认实现返回错误,WASM 侧通过宿主函数重写)
192    fn http_request(&self, _req: &HttpRequest) -> Result<HttpResponse, PluginError> {
193        Err(PluginError::HttpError("http_request not supported in this context".into()))
194    }
195
196    /// 类型擦除,供宿主侧 downcast 获取 `HttpContext`
197    fn as_any_mut(&mut self) -> &mut dyn Any;
198}
199
200impl PluginContext for HttpContext {
201    fn request_id(&self) -> String {
202        HttpContext::request_id(self)
203    }
204
205    fn request_ts(&self) -> i64 {
206        HttpContext::request_ts(self)
207    }
208
209    fn is_sse(&self) -> bool {
210        HttpContext::is_sse(self)
211    }
212
213    fn is_websocket(&self) -> bool {
214        HttpContext::is_websocket(self)
215    }
216
217    fn get_route_name(&self) -> Option<String> {
218        self.get_route().map(|r| r.name.clone())
219    }
220
221    fn get_routing_url(&self) -> Option<String> {
222        HttpContext::get_routing_url(self).cloned()
223    }
224
225    fn get_response_body_size(&self) -> Option<i64> {
226        self.get_state::<i64>(Self::RESPONSE_BODY_SIZE)
227    }
228
229    fn set_response_body_size(&mut self, size: i64) {
230        self.insert_state(Self::RESPONSE_BODY_SIZE, size);
231    }
232
233    #[cfg(feature = "model")]
234    fn get_model_name(&self) -> Option<String> {
235        self.get_proxy_model_name()
236    }
237
238    #[cfg(feature = "model")]
239    fn get_model_provider(&self) -> Option<Provider> {
240        self.get_proxy_model_provider()
241    }
242
243    fn log(&self, level: i32, msg: &str) {
244        match level {
245            LOG_ERROR => log::error!("{}", msg),
246            LOG_WARN => log::warn!("{}", msg),
247            LOG_INFO => log::info!("{}", msg),
248            LOG_DEBUG => log::debug!("{}", msg),
249            LOG_TRACE => log::trace!("{}", msg),
250            _ => log::info!("{}", msg),
251        }
252    }
253
254    fn as_any_mut(&mut self) -> &mut dyn Any {
255        self
256    }
257}