use crate::rt::PluginError;
use base64::Engine as _;
use serde_json::{json, Value};
use std::cell::{Cell, RefCell};
use std::collections::BTreeMap;
use std::io::Read as _;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
thread_local! {
static SETTINGS: RefCell<BTreeMap<String, Value>> = RefCell::new(BTreeMap::new());
static LOCALE: RefCell<String> = RefCell::new(String::from("en"));
static NOTES: RefCell<BTreeMap<String, Value>> = RefCell::new(BTreeMap::new());
static FOLDERS: RefCell<Vec<Value>> = RefCell::new(Vec::new());
static AI_REPLY: RefCell<Option<String>> = RefCell::new(None);
static AI_REPLY_QUEUE: RefCell<std::collections::VecDeque<String>> = RefCell::new(std::collections::VecDeque::new());
static AI_LAST_MESSAGES: RefCell<Option<Value>> = RefCell::new(None);
static WRITE_PENDING: Cell<bool> = const { Cell::new(false) };
}
pub const FAKE_CREATED_ID: &str = "0123456789abcdef0123456789abcdef";
pub fn set_setting(key: &str, value: Value) {
SETTINGS.with(|s| s.borrow_mut().insert(key.to_string(), value));
}
pub fn clear_settings() {
SETTINGS.with(|s| s.borrow_mut().clear());
}
pub fn set_locale(code: &str) {
LOCALE.with(|l| *l.borrow_mut() = code.to_string());
}
pub fn make_note(id: &str, parent_id: &str, title: &str, body: &str) -> Value {
json!({
"id": id,
"parent_id": parent_id,
"title": title,
"body": body,
"created_time": 1_700_000_000_000_i64,
"updated_time": 1_700_000_000_000_i64,
"markup_language": 1,
"is_todo": false,
"todo_completed": false,
"is_conflict": false,
"source_url": "",
"order": 0,
})
}
pub fn put_note(note: Value) {
let id = note.get("id").and_then(Value::as_str).unwrap_or_default().to_string();
NOTES.with(|n| n.borrow_mut().insert(id, note));
}
pub fn put_folder(id: &str, title: &str, parent_id: &str) {
FOLDERS.with(|f| {
f.borrow_mut().push(json!({ "id": id, "title": title, "parent_id": parent_id }));
});
}
pub fn clear_notes() {
NOTES.with(|n| n.borrow_mut().clear());
FOLDERS.with(|f| f.borrow_mut().clear());
}
pub fn set_ai_reply(reply: &str) {
AI_REPLY.with(|r| *r.borrow_mut() = Some(reply.to_string()));
}
pub fn push_ai_reply(reply: &str) {
AI_REPLY_QUEUE.with(|q| q.borrow_mut().push_back(reply.to_string()));
}
pub fn last_ai_messages() -> Option<Value> {
AI_LAST_MESSAGES.with(|m| m.borrow().clone())
}
pub fn set_write_pending(pending: bool) {
WRITE_PENDING.with(|w| w.set(pending));
}
pub fn call(method: &str, params: Value) -> Result<Value, PluginError> {
match method {
"log" => {
let level = params.get("level").and_then(Value::as_str).unwrap_or("info");
let message = params.get("message").and_then(Value::as_str).unwrap_or("");
eprintln!("[plugin log/{level}] {message}");
Ok(Value::Null)
}
"time.now" => {
let ms = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| PluginError::internal(format!("系统时钟异常: {e}")))?
.as_millis() as i64;
Ok(json!({ "unix_ms": ms }))
}
"system.locale" => Ok(json!({ "locale": LOCALE.with(|l| l.borrow().clone()) })),
"settings.get" => {
let key = params
.get("key")
.and_then(Value::as_str)
.ok_or_else(|| PluginError::invalid("settings.get 缺 key"))?;
let value = SETTINGS.with(|s| s.borrow().get(key).cloned()).unwrap_or(Value::Null);
Ok(json!({ "value": value }))
}
"settings.set" => {
let key = params
.get("key")
.and_then(Value::as_str)
.ok_or_else(|| PluginError::invalid("settings.set 缺 key"))?;
let value = params.get("value").cloned().unwrap_or(Value::Null);
set_setting(key, value);
Ok(json!({}))
}
"http.request" => http_request(¶ms),
"notes.get" => {
let id = params
.get("id")
.and_then(Value::as_str)
.ok_or_else(|| PluginError::invalid("notes.get 缺 id"))?;
NOTES
.with(|n| n.borrow().get(id).cloned())
.map(|note| json!({ "note": note }))
.ok_or_else(|| PluginError::not_found(format!("笔记不存在: {id}")))
}
"notes.search" => {
let q = params
.get("query")
.and_then(Value::as_str)
.ok_or_else(|| PluginError::invalid("notes.search 缺 query"))?
.to_lowercase();
let limit = params
.get("limit")
.and_then(Value::as_u64)
.unwrap_or(20)
.clamp(1, 100) as usize;
let notes: Vec<Value> = NOTES.with(|n| {
n.borrow()
.values()
.filter(|note| {
let title = note.get("title").and_then(Value::as_str).unwrap_or("");
let body = note.get("body").and_then(Value::as_str).unwrap_or("");
title.to_lowercase().contains(&q) || body.to_lowercase().contains(&q)
})
.take(limit)
.map(|note| {
json!({
"id": note["id"],
"title": note["title"],
"parent_id": note["parent_id"],
})
})
.collect()
});
Ok(json!({ "notes": notes }))
}
"notes.list_folders" => {
let mut folders = FOLDERS.with(|f| f.borrow().clone());
folders.sort_by(|a, b| {
let key = |v: &Value| {
(
v.get("title").and_then(Value::as_str).unwrap_or("").to_string(),
v.get("id").and_then(Value::as_str).unwrap_or("").to_string(),
)
};
key(a).cmp(&key(b))
});
Ok(json!({ "folders": folders }))
}
"notes.upsert" => {
let id = params
.get("id")
.and_then(Value::as_str)
.ok_or_else(|| PluginError::invalid("notes.upsert 缺 id"))?;
let mut note = NOTES
.with(|n| n.borrow().get(id).cloned())
.ok_or_else(|| PluginError::not_found(format!("笔记不存在: {id}")))?;
if let Some(t) = params.get("title").and_then(Value::as_str) {
note["title"] = json!(t);
}
if let Some(b) = params.get("body").and_then(Value::as_str) {
note["body"] = json!(b);
}
let pending = WRITE_PENDING.with(Cell::get);
if !pending {
put_note(note.clone());
}
Ok(json!({ "note": note, "pending": pending }))
}
"notes.create" => {
let parent_id = params
.get("parent_id")
.and_then(Value::as_str)
.ok_or_else(|| PluginError::invalid("notes.create 缺 parent_id"))?;
let parent_ok = FOLDERS.with(|f| {
f.borrow().iter().any(|v| v.get("id").and_then(Value::as_str) == Some(parent_id))
});
if !parent_ok {
return Err(PluginError::invalid(format!("笔记本不存在: {parent_id}")));
}
let title = params.get("title").and_then(Value::as_str).unwrap_or("");
let body = params.get("body").and_then(Value::as_str).unwrap_or("");
let pending = WRITE_PENDING.with(Cell::get);
let note = make_note(if pending { "" } else { FAKE_CREATED_ID }, parent_id, title, body);
if !pending {
put_note(note.clone());
}
Ok(json!({ "note": note, "pending": pending }))
}
"ai.complete" => {
let ok = params
.get("messages")
.and_then(Value::as_array)
.map(|m| !m.is_empty())
.unwrap_or(false);
if !ok {
return Err(PluginError::invalid("ai.complete 需要非空 messages"));
}
AI_LAST_MESSAGES.with(|m| *m.borrow_mut() = params.get("messages").cloned());
let reply = AI_REPLY_QUEUE
.with(|q| q.borrow_mut().pop_front())
.or_else(|| AI_REPLY.with(|r| r.borrow().clone()))
.unwrap_or_else(|| "(stub reply)".to_string());
Ok(json!({ "content": reply }))
}
other => Err(PluginError::unsupported(format!("native-host 未实现方法 {other}"))),
}
}
fn http_request(params: &Value) -> Result<Value, PluginError> {
let method = params
.get("method")
.and_then(Value::as_str)
.ok_or_else(|| PluginError::invalid("http.request 缺 method"))?;
let url = params
.get("url")
.and_then(Value::as_str)
.ok_or_else(|| PluginError::invalid("http.request 缺 url"))?;
let b64 = base64::engine::general_purpose::STANDARD;
let mut req = ureq::request(method, url);
if let Some(headers) = params.get("headers").and_then(Value::as_object) {
for (k, v) in headers {
if let Some(v) = v.as_str() {
req = req.set(k, v);
}
}
}
if let Some(t) = params.get("timeout_ms").and_then(Value::as_u64) {
req = req.timeout(Duration::from_millis(t));
}
let body = match params.get("body_b64").and_then(Value::as_str) {
Some(s) => Some(
b64.decode(s)
.map_err(|e| PluginError::invalid(format!("body_b64 解码失败: {e}")))?,
),
None => None,
};
let result = match &body {
Some(bytes) => req.send_bytes(bytes),
None => req.call(),
};
let resp = match result {
Ok(resp) => resp,
Err(ureq::Error::Status(_, resp)) => resp,
Err(e) => return Err(PluginError::internal(format!("http 请求失败: {e}"))),
};
let status = resp.status();
let mut headers = BTreeMap::new();
for name in resp.headers_names() {
if let Some(v) = resp.header(&name) {
headers.insert(name, v.to_string());
}
}
let mut bytes = Vec::new();
resp.into_reader()
.read_to_end(&mut bytes)
.map_err(|e| PluginError::internal(format!("读响应体失败: {e}")))?;
Ok(json!({ "status": status, "headers": headers, "body_b64": b64.encode(&bytes) }))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn settings_round_trip_through_host_wrappers() {
clear_settings();
crate::host::settings_set("k", json!("v")).unwrap();
assert_eq!(crate::host::settings_get("k").unwrap(), json!("v"));
assert_eq!(crate::host::settings_get("missing").unwrap(), Value::Null);
}
#[test]
fn now_ms_is_sane() {
let ms = crate::host::now_ms().unwrap();
assert!(ms > 1_600_000_000_000, "unix 毫秒应在 2020 年之后: {ms}");
}
#[test]
fn system_locale_through_host_wrapper() {
assert_eq!(crate::host::system_locale().unwrap(), "en"); set_locale("fr");
assert_eq!(crate::host::system_locale().unwrap(), "fr");
set_locale("en"); }
#[test]
fn unknown_method_is_unsupported() {
assert_eq!(call("nope", json!({})).unwrap_err().code, "unsupported");
}
#[test]
fn notes_read_paths_through_host_wrappers() {
clear_notes();
put_folder("f".repeat(32).as_str(), "收件箱", "");
put_note(make_note(&"a".repeat(32), &"f".repeat(32), "购物清单", "牛奶 面包"));
put_note(make_note(&"b".repeat(32), &"f".repeat(32), "旅行", "机票"));
let note = crate::host::notes_get(&"a".repeat(32)).unwrap();
assert_eq!(note.title, "购物清单");
assert_eq!(crate::host::notes_get(&"9".repeat(32)).unwrap_err().code, "not_found");
let hits = crate::host::notes_search("牛奶", None).unwrap();
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].title, "购物清单");
let folders = crate::host::notes_list_folders().unwrap();
assert_eq!(folders.len(), 1);
assert_eq!(folders[0].title, "收件箱");
}
#[test]
fn notes_write_respects_pending_switch() {
clear_notes();
put_folder(&"f".repeat(32), "收件箱", "");
put_note(make_note(&"a".repeat(32), &"f".repeat(32), "t", "old"));
set_write_pending(false);
let r = crate::host::notes_upsert(&"a".repeat(32), None, Some("new")).unwrap();
assert!(!r.pending);
assert_eq!(crate::host::notes_get(&"a".repeat(32)).unwrap().body, "new");
let created = crate::host::notes_create(&"f".repeat(32), Some("新笔记"), None).unwrap();
assert!(!created.pending);
assert_eq!(created.note.id, FAKE_CREATED_ID);
set_write_pending(true);
let r = crate::host::notes_upsert(&"a".repeat(32), None, Some("newer")).unwrap();
assert!(r.pending);
assert_eq!(crate::host::notes_get(&"a".repeat(32)).unwrap().body, "new");
let created = crate::host::notes_create(&"f".repeat(32), Some("x"), None).unwrap();
assert!(created.pending);
assert_eq!(created.note.id, "");
set_write_pending(false);
assert_eq!(crate::host::notes_create(&"9".repeat(32), None, None).unwrap_err().code, "invalid");
}
#[test]
fn ai_complete_returns_injected_reply() {
set_ai_reply("你好,这是替身回复");
let msgs = [crate::host::Message::user("hi")];
assert_eq!(crate::host::ai_complete(&msgs, None).unwrap(), "你好,这是替身回复");
assert_eq!(
crate::host::ai_complete(&[], None).unwrap_err().code,
"invalid",
"空 messages 应报 invalid"
);
}
#[test]
fn ai_reply_queue_pops_fifo_then_falls_back() {
set_ai_reply("固定回复");
push_ai_reply("第一轮");
push_ai_reply("第二轮");
let msgs = [crate::host::Message::user("hi")];
assert_eq!(crate::host::ai_complete(&msgs, None).unwrap(), "第一轮");
assert_eq!(crate::host::ai_complete(&msgs, None).unwrap(), "第二轮");
assert_eq!(crate::host::ai_complete(&msgs, None).unwrap(), "固定回复", "队列耗尽回落固定回复");
let recorded = last_ai_messages().expect("应记录最近一次 messages");
assert_eq!(recorded[0]["content"], "hi");
}
}