1use 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
11pub fn log(level: &str, message: &str) {
13 let _ = call_host("log", json!({ "level": level, "message": message }));
14}
15
16pub 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
25pub fn settings_get(key: &str) -> Result<Value, PluginError> {
27 let result = call_host("settings.get", json!({ "key": key }))?;
28 Ok(result.get("value").cloned().unwrap_or(Value::Null))
29}
30
31pub fn settings_set(key: &str, value: Value) -> Result<(), PluginError> {
33 call_host("settings.set", json!({ "key": key, "value": value }))?;
34 Ok(())
35}
36
37#[derive(Debug, Clone, Default)]
39pub struct HttpRequest {
40 pub method: String,
42 pub url: String,
43 pub headers: BTreeMap<String, String>,
44 pub body: Option<Vec<u8>>,
45 pub timeout_ms: Option<u64>,
46}
47
48#[derive(Debug, Clone)]
49pub struct HttpResponse {
50 pub status: u16,
52 pub headers: BTreeMap<String, String>,
53 pub body: Vec<u8>,
54}
55
56impl HttpResponse {
57 pub fn is_success(&self) -> bool {
58 (200..300).contains(&self.status)
59 }
60 pub fn body_text(&self) -> String {
61 String::from_utf8_lossy(&self.body).into_owned()
62 }
63}
64
65pub fn http_request(req: &HttpRequest) -> Result<HttpResponse, PluginError> {
68 let b64 = base64::engine::general_purpose::STANDARD;
69 let mut params = json!({
70 "method": req.method,
71 "url": req.url,
72 "headers": req.headers,
73 });
74 if let Some(body) = &req.body {
75 params["body_b64"] = Value::String(b64.encode(body));
76 }
77 if let Some(t) = req.timeout_ms {
78 params["timeout_ms"] = json!(t);
79 }
80 let result = call_host("http.request", params)?;
81 let status = result
82 .get("status")
83 .and_then(Value::as_u64)
84 .ok_or_else(|| PluginError::internal("http.request 响应缺 status"))? as u16;
85 let headers = result
86 .get("headers")
87 .and_then(Value::as_object)
88 .map(|m| {
89 m.iter()
90 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
91 .collect()
92 })
93 .unwrap_or_default();
94 let body = match result.get("body_b64").and_then(Value::as_str) {
95 Some(s) => b64
96 .decode(s)
97 .map_err(|e| PluginError::internal(format!("body_b64 解码失败: {e}")))?,
98 None => Vec::new(),
99 };
100 Ok(HttpResponse { status, headers, body })
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct NoteRef {
109 pub id: String,
110 pub title: String,
111 pub parent_id: String,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct FolderRef {
117 pub id: String,
118 pub title: String,
119 pub parent_id: String,
120}
121
122pub fn notes_get(id: &str) -> Result<Note, PluginError> {
124 let r = call_host("notes.get", json!({ "id": id }))?;
125 parse_note(&r)
126}
127
128pub fn notes_search(query: &str, limit: Option<u32>) -> Result<Vec<NoteRef>, PluginError> {
130 let mut params = json!({ "query": query });
131 if let Some(l) = limit {
132 params["limit"] = json!(l);
133 }
134 let r = call_host("notes.search", params)?;
135 serde_json::from_value(r.get("notes").cloned().unwrap_or(Value::Null))
136 .map_err(|e| PluginError::internal(format!("notes.search 响应解析失败: {e}")))
137}
138
139pub fn notes_list_folders() -> Result<Vec<FolderRef>, PluginError> {
141 let r = call_host("notes.list_folders", json!({}))?;
142 serde_json::from_value(r.get("folders").cloned().unwrap_or(Value::Null))
143 .map_err(|e| PluginError::internal(format!("notes.list_folders 响应解析失败: {e}")))
144}
145
146#[derive(Debug, Clone)]
149pub struct UpsertResult {
150 pub note: Note,
151 pub pending: bool,
152}
153
154pub fn notes_upsert(id: &str, title: Option<&str>, body: Option<&str>) -> Result<UpsertResult, PluginError> {
157 let mut params = json!({ "id": id });
158 if let Some(t) = title {
159 params["title"] = json!(t);
160 }
161 if let Some(b) = body {
162 params["body"] = json!(b);
163 }
164 let r = call_host("notes.upsert", params)?;
165 parse_upsert(&r)
166}
167
168pub fn notes_create(parent_id: &str, title: Option<&str>, body: Option<&str>) -> Result<UpsertResult, PluginError> {
171 let mut params = json!({ "parent_id": parent_id });
172 if let Some(t) = title {
173 params["title"] = json!(t);
174 }
175 if let Some(b) = body {
176 params["body"] = json!(b);
177 }
178 let r = call_host("notes.create", params)?;
179 parse_upsert(&r)
180}
181
182fn parse_note(r: &Value) -> Result<Note, PluginError> {
183 serde_json::from_value(r.get("note").cloned().unwrap_or(Value::Null))
184 .map_err(|e| PluginError::internal(format!("note 解析失败: {e}")))
185}
186
187fn parse_upsert(r: &Value) -> Result<UpsertResult, PluginError> {
188 Ok(UpsertResult {
189 note: parse_note(r)?,
190 pending: r.get("pending").and_then(Value::as_bool).unwrap_or(false),
191 })
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct Message {
199 pub role: String,
200 pub content: String,
201}
202
203impl Message {
204 pub fn system(content: impl Into<String>) -> Self {
205 Self { role: "system".into(), content: content.into() }
206 }
207 pub fn user(content: impl Into<String>) -> Self {
208 Self { role: "user".into(), content: content.into() }
209 }
210 pub fn assistant(content: impl Into<String>) -> Self {
211 Self { role: "assistant".into(), content: content.into() }
212 }
213}
214
215#[derive(Debug, Clone, Default)]
217pub struct AiOptions {
218 pub model: Option<String>,
219 pub temperature: Option<f64>,
220 pub max_tokens: Option<u64>,
221}
222
223pub fn ai_complete(messages: &[Message], options: Option<&AiOptions>) -> Result<String, PluginError> {
226 let mut params = json!({ "messages": messages });
227 if let Some(o) = options {
228 let mut opts = json!({});
229 if let Some(m) = &o.model {
230 opts["model"] = json!(m);
231 }
232 if let Some(t) = o.temperature {
233 opts["temperature"] = json!(t);
234 }
235 if let Some(mt) = o.max_tokens {
236 opts["max_tokens"] = json!(mt);
237 }
238 params["options"] = opts;
239 }
240 let r = call_host("ai.complete", params)?;
241 r.get("content")
242 .and_then(Value::as_str)
243 .map(|s| s.to_string())
244 .ok_or_else(|| PluginError::internal("ai.complete 响应缺 content"))
245}