Skip to main content

jasper_plugin_sdk/
host.rs

1//! 宿主能力的类型化封装(spec §6.5「插件 → host」方法)。
2//! 每个函数对应一个 host_call 方法;所需能力见 spec §7,未授权时返回 code="forbidden"。
3
4use crate::rt::{call_host, PluginError};
5use base64::Engine as _;
6use jasper_core::model::Note;
7use serde::{Deserialize, Serialize};
8use serde_json::{json, Value};
9use std::collections::BTreeMap;
10
11/// `log`:宿主日志(无需能力)。失败静默忽略——日志不该让业务失败。
12pub fn log(level: &str, message: &str) {
13    let _ = call_host("log", json!({ "level": level, "message": message }));
14}
15
16/// `time.now`:当前时间 Unix 毫秒(无需能力,spec 0.2)。
17/// 沙箱内没有时钟(`SystemTime::now()` 在 wasm32 会 panic)——签名协议(S3 SigV4 等)用这个。
18pub fn now_ms() -> Result<i64, PluginError> {
19    let r = call_host("time.now", json!({}))?;
20    r.get("unix_ms")
21        .and_then(Value::as_i64)
22        .ok_or_else(|| PluginError::internal("time.now 响应缺 unix_ms"))
23}
24
25/// `system.locale`:当前 UI 语言代码(如 `en`/`zh`/`fr`;无需能力,spec 0.4)。
26/// 供插件本地化**自己运行时产出的文字**(chat 回复 / 动态 UI 文案等)。宿主把 UI 语言
27/// 持久化在配置里,前端切换时同步;未设置时回落 `"en"`(总返回有效代码)。
28pub fn system_locale() -> Result<String, PluginError> {
29    let r = call_host("system.locale", json!({}))?;
30    r.get("locale")
31        .and_then(Value::as_str)
32        .map(str::to_string)
33        .ok_or_else(|| PluginError::internal("system.locale 响应缺 locale"))
34}
35
36/// `settings.get`:读插件作用域 KV(能力 `settings`)。键不存在返回 Null。
37pub fn settings_get(key: &str) -> Result<Value, PluginError> {
38    let result = call_host("settings.get", json!({ "key": key }))?;
39    Ok(result.get("value").cloned().unwrap_or(Value::Null))
40}
41
42/// `settings.set`:写插件作用域 KV(能力 `settings`)。
43pub fn settings_set(key: &str, value: Value) -> Result<(), PluginError> {
44    call_host("settings.set", json!({ "key": key, "value": value }))?;
45    Ok(())
46}
47
48/// HTTP 请求(能力 `host:http`,spec 0.2)。二进制体在这里做 base64 编解码。
49#[derive(Debug, Clone, Default)]
50pub struct HttpRequest {
51    /// GET/PUT/PROPFIND/…(原样透传给宿主)
52    pub method: String,
53    pub url: String,
54    pub headers: BTreeMap<String, String>,
55    pub body: Option<Vec<u8>>,
56    pub timeout_ms: Option<u64>,
57}
58
59#[derive(Debug, Clone)]
60pub struct HttpResponse {
61    /// 非 2xx 也照常返回(spec §6.5:错误语义留给插件判断)
62    pub status: u16,
63    pub headers: BTreeMap<String, String>,
64    pub body: Vec<u8>,
65}
66
67impl HttpResponse {
68    pub fn is_success(&self) -> bool {
69        (200..300).contains(&self.status)
70    }
71    pub fn body_text(&self) -> String {
72        String::from_utf8_lossy(&self.body).into_owned()
73    }
74}
75
76/// `http.request`:宿主代理执行 HTTP(S)。网络失败(连接/超时)→ Err(internal);
77/// 拿到响应(含 4xx/5xx)→ Ok(HttpResponse)。
78pub fn http_request(req: &HttpRequest) -> Result<HttpResponse, PluginError> {
79    let b64 = base64::engine::general_purpose::STANDARD;
80    let mut params = json!({
81        "method": req.method,
82        "url": req.url,
83        "headers": req.headers,
84    });
85    if let Some(body) = &req.body {
86        params["body_b64"] = Value::String(b64.encode(body));
87    }
88    if let Some(t) = req.timeout_ms {
89        params["timeout_ms"] = json!(t);
90    }
91    let result = call_host("http.request", params)?;
92    let status = result
93        .get("status")
94        .and_then(Value::as_u64)
95        .ok_or_else(|| PluginError::internal("http.request 响应缺 status"))? as u16;
96    let headers = result
97        .get("headers")
98        .and_then(Value::as_object)
99        .map(|m| {
100            m.iter()
101                .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
102                .collect()
103        })
104        .unwrap_or_default();
105    let body = match result.get("body_b64").and_then(Value::as_str) {
106        Some(s) => b64
107            .decode(s)
108            .map_err(|e| PluginError::internal(format!("body_b64 解码失败: {e}")))?,
109        None => Vec::new(),
110    };
111    Ok(HttpResponse { status, headers, body })
112}
113
114// ---- notes.*(能力 notes:read / notes:write,spec 0.3)----
115// 仅在 command / ui 分发上下文可用;hooks/storage 分发内调用返回 code="unsupported"(spec §6.5)。
116
117/// 搜索结果条目(spec §6.5 NoteRef)。
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct NoteRef {
120    pub id: String,
121    pub title: String,
122    pub parent_id: String,
123}
124
125/// 笔记本条目(spec §6.5 FolderRef)。
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct FolderRef {
128    pub id: String,
129    pub title: String,
130    pub parent_id: String,
131}
132
133/// `notes.get`:按 id 取完整笔记(能力 `notes:read`)。不存在 → code="not_found"。
134pub fn notes_get(id: &str) -> Result<Note, PluginError> {
135    let r = call_host("notes.get", json!({ "id": id }))?;
136    parse_note(&r)
137}
138
139/// `notes.search`:标题/正文全文搜索(能力 `notes:read`)。`limit` 缺省宿主取 20、上限 100。
140pub fn notes_search(query: &str, limit: Option<u32>) -> Result<Vec<NoteRef>, PluginError> {
141    let mut params = json!({ "query": query });
142    if let Some(l) = limit {
143        params["limit"] = json!(l);
144    }
145    let r = call_host("notes.search", params)?;
146    serde_json::from_value(r.get("notes").cloned().unwrap_or(Value::Null))
147        .map_err(|e| PluginError::internal(format!("notes.search 响应解析失败: {e}")))
148}
149
150/// `notes.list_folders`:全部笔记本(能力 `notes:read`),宿主按 (title, id) 排序。
151pub fn notes_list_folders() -> Result<Vec<FolderRef>, PluginError> {
152    let r = call_host("notes.list_folders", json!({}))?;
153    serde_json::from_value(r.get("folders").cloned().unwrap_or(Value::Null))
154        .map_err(|e| PluginError::internal(format!("notes.list_folders 响应解析失败: {e}")))
155}
156
157/// `notes.upsert`/`notes.create` 的结果。`pending=true` = 提案已交宿主 UI 确认、
158/// **尚未落盘**(spec §6.5 写确认=提案回传);插件不应把它当成写入成功。
159#[derive(Debug, Clone)]
160pub struct UpsertResult {
161    pub note: Note,
162    pub pending: bool,
163}
164
165/// `notes.upsert`:改标题/正文(能力 `notes:write`)。`None` 字段保持原值;
166/// 其余元数据宿主逐字保留。默认走提案回传(见 [`UpsertResult`])。
167pub fn notes_upsert(id: &str, title: Option<&str>, body: Option<&str>) -> Result<UpsertResult, PluginError> {
168    let mut params = json!({ "id": id });
169    if let Some(t) = title {
170        params["title"] = json!(t);
171    }
172    if let Some(b) = body {
173        params["body"] = json!(b);
174    }
175    let r = call_host("notes.upsert", params)?;
176    parse_upsert(&r)
177}
178
179/// `notes.create`:新建笔记(能力 `notes:write`)。`parent_id` 必须是已存在笔记本(否则 invalid)。
180/// `pending=true` 时 `note.id` 为空串——id 由宿主在用户批准时生成,无法链式写入。
181pub fn notes_create(parent_id: &str, title: Option<&str>, body: Option<&str>) -> Result<UpsertResult, PluginError> {
182    let mut params = json!({ "parent_id": parent_id });
183    if let Some(t) = title {
184        params["title"] = json!(t);
185    }
186    if let Some(b) = body {
187        params["body"] = json!(b);
188    }
189    let r = call_host("notes.create", params)?;
190    parse_upsert(&r)
191}
192
193fn parse_note(r: &Value) -> Result<Note, PluginError> {
194    serde_json::from_value(r.get("note").cloned().unwrap_or(Value::Null))
195        .map_err(|e| PluginError::internal(format!("note 解析失败: {e}")))
196}
197
198fn parse_upsert(r: &Value) -> Result<UpsertResult, PluginError> {
199    Ok(UpsertResult {
200        note: parse_note(r)?,
201        pending: r.get("pending").and_then(Value::as_bool).unwrap_or(false),
202    })
203}
204
205// ---- ai.complete(能力 host:ai,spec 0.3)----
206
207/// 对话消息(spec §6.5 Message);`role` ∈ system|user|assistant。
208#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct Message {
210    pub role: String,
211    pub content: String,
212}
213
214impl Message {
215    pub fn system(content: impl Into<String>) -> Self {
216        Self { role: "system".into(), content: content.into() }
217    }
218    pub fn user(content: impl Into<String>) -> Self {
219        Self { role: "user".into(), content: content.into() }
220    }
221    pub fn assistant(content: impl Into<String>) -> Self {
222        Self { role: "assistant".into(), content: content.into() }
223    }
224}
225
226/// `ai.complete` 的可选项;`model` 缺省用宿主配置。宿主钳制 temperature 0..=2、max_tokens 1..=32768。
227#[derive(Debug, Clone, Default)]
228pub struct AiOptions {
229    pub model: Option<String>,
230    pub temperature: Option<f64>,
231    pub max_tokens: Option<u64>,
232}
233
234/// `ai.complete`:宿主代理的一次性补全(能力 `host:ai`)。密钥/端点在宿主,插件不可见;
235/// 宿主未配置 AI → code="internal"(message 指明去设置页配置)。网络等待不计插件 CPU 墙钟。
236pub fn ai_complete(messages: &[Message], options: Option<&AiOptions>) -> Result<String, PluginError> {
237    let mut params = json!({ "messages": messages });
238    if let Some(o) = options {
239        let mut opts = json!({});
240        if let Some(m) = &o.model {
241            opts["model"] = json!(m);
242        }
243        if let Some(t) = o.temperature {
244            opts["temperature"] = json!(t);
245        }
246        if let Some(mt) = o.max_tokens {
247            opts["max_tokens"] = json!(mt);
248        }
249        params["options"] = opts;
250    }
251    let r = call_host("ai.complete", params)?;
252    r.get("content")
253        .and_then(Value::as_str)
254        .map(|s| s.to_string())
255        .ok_or_else(|| PluginError::internal("ai.complete 响应缺 content"))
256}