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 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
36pub 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
42pub fn settings_set(key: &str, value: Value) -> Result<(), PluginError> {
44 call_host("settings.set", json!({ "key": key, "value": value }))?;
45 Ok(())
46}
47
48#[derive(Debug, Clone, Default)]
50pub struct HttpRequest {
51 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 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
76pub 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#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct NoteRef {
120 pub id: String,
121 pub title: String,
122 pub parent_id: String,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct FolderRef {
128 pub id: String,
129 pub title: String,
130 pub parent_id: String,
131}
132
133pub fn notes_get(id: &str) -> Result<Note, PluginError> {
135 let r = call_host("notes.get", json!({ "id": id }))?;
136 parse_note(&r)
137}
138
139pub 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
150pub 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#[derive(Debug, Clone)]
160pub struct UpsertResult {
161 pub note: Note,
162 pub pending: bool,
163}
164
165pub 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
179pub 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#[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#[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
234pub 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}