use crate::rt::{call_host, PluginError};
use base64::Engine as _;
use jasper_core::model::Note;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::collections::BTreeMap;
pub fn log(level: &str, message: &str) {
let _ = call_host("log", json!({ "level": level, "message": message }));
}
pub fn now_ms() -> Result<i64, PluginError> {
let r = call_host("time.now", json!({}))?;
r.get("unix_ms")
.and_then(Value::as_i64)
.ok_or_else(|| PluginError::internal("time.now 响应缺 unix_ms"))
}
pub fn system_locale() -> Result<String, PluginError> {
let r = call_host("system.locale", json!({}))?;
r.get("locale")
.and_then(Value::as_str)
.map(str::to_string)
.ok_or_else(|| PluginError::internal("system.locale 响应缺 locale"))
}
pub fn settings_get(key: &str) -> Result<Value, PluginError> {
let result = call_host("settings.get", json!({ "key": key }))?;
Ok(result.get("value").cloned().unwrap_or(Value::Null))
}
pub fn settings_set(key: &str, value: Value) -> Result<(), PluginError> {
call_host("settings.set", json!({ "key": key, "value": value }))?;
Ok(())
}
#[derive(Debug, Clone, Default)]
pub struct HttpRequest {
pub method: String,
pub url: String,
pub headers: BTreeMap<String, String>,
pub body: Option<Vec<u8>>,
pub timeout_ms: Option<u64>,
}
#[derive(Debug, Clone)]
pub struct HttpResponse {
pub status: u16,
pub headers: BTreeMap<String, String>,
pub body: Vec<u8>,
}
impl HttpResponse {
pub fn is_success(&self) -> bool {
(200..300).contains(&self.status)
}
pub fn body_text(&self) -> String {
String::from_utf8_lossy(&self.body).into_owned()
}
}
pub fn http_request(req: &HttpRequest) -> Result<HttpResponse, PluginError> {
let b64 = base64::engine::general_purpose::STANDARD;
let mut params = json!({
"method": req.method,
"url": req.url,
"headers": req.headers,
});
if let Some(body) = &req.body {
params["body_b64"] = Value::String(b64.encode(body));
}
if let Some(t) = req.timeout_ms {
params["timeout_ms"] = json!(t);
}
let result = call_host("http.request", params)?;
let status = result
.get("status")
.and_then(Value::as_u64)
.ok_or_else(|| PluginError::internal("http.request 响应缺 status"))? as u16;
let headers = result
.get("headers")
.and_then(Value::as_object)
.map(|m| {
m.iter()
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
.collect()
})
.unwrap_or_default();
let body = match result.get("body_b64").and_then(Value::as_str) {
Some(s) => b64
.decode(s)
.map_err(|e| PluginError::internal(format!("body_b64 解码失败: {e}")))?,
None => Vec::new(),
};
Ok(HttpResponse { status, headers, body })
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NoteRef {
pub id: String,
pub title: String,
pub parent_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FolderRef {
pub id: String,
pub title: String,
pub parent_id: String,
}
pub fn notes_get(id: &str) -> Result<Note, PluginError> {
let r = call_host("notes.get", json!({ "id": id }))?;
parse_note(&r)
}
pub fn notes_search(query: &str, limit: Option<u32>) -> Result<Vec<NoteRef>, PluginError> {
let mut params = json!({ "query": query });
if let Some(l) = limit {
params["limit"] = json!(l);
}
let r = call_host("notes.search", params)?;
serde_json::from_value(r.get("notes").cloned().unwrap_or(Value::Null))
.map_err(|e| PluginError::internal(format!("notes.search 响应解析失败: {e}")))
}
pub fn notes_list_folders() -> Result<Vec<FolderRef>, PluginError> {
let r = call_host("notes.list_folders", json!({}))?;
serde_json::from_value(r.get("folders").cloned().unwrap_or(Value::Null))
.map_err(|e| PluginError::internal(format!("notes.list_folders 响应解析失败: {e}")))
}
#[derive(Debug, Clone)]
pub struct UpsertResult {
pub note: Note,
pub pending: bool,
}
pub fn notes_upsert(id: &str, title: Option<&str>, body: Option<&str>) -> Result<UpsertResult, PluginError> {
let mut params = json!({ "id": id });
if let Some(t) = title {
params["title"] = json!(t);
}
if let Some(b) = body {
params["body"] = json!(b);
}
let r = call_host("notes.upsert", params)?;
parse_upsert(&r)
}
pub fn notes_create(parent_id: &str, title: Option<&str>, body: Option<&str>) -> Result<UpsertResult, PluginError> {
let mut params = json!({ "parent_id": parent_id });
if let Some(t) = title {
params["title"] = json!(t);
}
if let Some(b) = body {
params["body"] = json!(b);
}
let r = call_host("notes.create", params)?;
parse_upsert(&r)
}
fn parse_note(r: &Value) -> Result<Note, PluginError> {
serde_json::from_value(r.get("note").cloned().unwrap_or(Value::Null))
.map_err(|e| PluginError::internal(format!("note 解析失败: {e}")))
}
fn parse_upsert(r: &Value) -> Result<UpsertResult, PluginError> {
Ok(UpsertResult {
note: parse_note(r)?,
pending: r.get("pending").and_then(Value::as_bool).unwrap_or(false),
})
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message {
pub role: String,
pub content: String,
}
impl Message {
pub fn system(content: impl Into<String>) -> Self {
Self { role: "system".into(), content: content.into() }
}
pub fn user(content: impl Into<String>) -> Self {
Self { role: "user".into(), content: content.into() }
}
pub fn assistant(content: impl Into<String>) -> Self {
Self { role: "assistant".into(), content: content.into() }
}
}
#[derive(Debug, Clone, Default)]
pub struct AiOptions {
pub model: Option<String>,
pub temperature: Option<f64>,
pub max_tokens: Option<u64>,
}
pub fn ai_complete(messages: &[Message], options: Option<&AiOptions>) -> Result<String, PluginError> {
let mut params = json!({ "messages": messages });
if let Some(o) = options {
let mut opts = json!({});
if let Some(m) = &o.model {
opts["model"] = json!(m);
}
if let Some(t) = o.temperature {
opts["temperature"] = json!(t);
}
if let Some(mt) = o.max_tokens {
opts["max_tokens"] = json!(mt);
}
params["options"] = opts;
}
let r = call_host("ai.complete", params)?;
r.get("content")
.and_then(Value::as_str)
.map(|s| s.to_string())
.ok_or_else(|| PluginError::internal("ai.complete 响应缺 content"))
}