1
0
This commit is contained in:
2026-03-03 16:02:31 +09:00
parent dc31386cf2
commit eafd3429e7
5 changed files with 69 additions and 90 deletions

View File

@@ -6,6 +6,8 @@ use std::path::PathBuf;
use chrono::Utc; use chrono::Utc;
pub const DEFAULT_MEMORY: u64 = 100; pub const DEFAULT_MEMORY: u64 = 100;
pub const COLLECTION_CORE: &str = "ai.syui.gpt.core";
pub const COLLECTION_MEMORY: &str = "ai.syui.gpt.memory";
pub struct Config { pub struct Config {
pub path: Option<String>, pub path: Option<String>,
@@ -14,6 +16,20 @@ pub struct Config {
pub memory: u64, pub memory: u64,
} }
impl Config {
pub fn did(&self) -> &str {
self.did.as_deref().unwrap_or("self")
}
pub fn handle(&self) -> &str {
self.handle.as_deref().unwrap_or("self")
}
pub fn identity(&self) -> &str {
if cfg!(windows) { self.handle() } else { self.did() }
}
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct ConfigFile { struct ConfigFile {
bot: Option<BotConfig>, bot: Option<BotConfig>,
@@ -84,22 +100,20 @@ pub fn init() {
} }
let cfg = load(); let cfg = load();
let core_path = record_path(&cfg, "ai.syui.gpt.core", "self"); let core_path = record_path(&cfg, COLLECTION_CORE, "self");
if !core_path.exists() { if !core_path.exists() {
if let Some(parent) = core_path.parent() { if let Some(parent) = core_path.parent() {
let _ = fs::create_dir_all(parent); let _ = fs::create_dir_all(parent);
} }
let did = cfg.did.clone().unwrap_or_else(|| "self".to_string());
let handle = cfg.handle.clone().unwrap_or_else(|| "self".to_string());
let now = Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string(); let now = Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();
let core_record = json!({ let core_record = json!({
"uri": format!("at://{}/ai.syui.gpt.core/self", did), "uri": format!("at://{}/{}/self", cfg.did(), COLLECTION_CORE),
"value": { "value": {
"$type": "ai.syui.gpt.core", "$type": COLLECTION_CORE,
"did": did, "did": cfg.did(),
"handle": handle, "handle": cfg.handle(),
"content": { "content": {
"$type": "ai.syui.gpt.core#markdown", "$type": format!("{}#markdown", COLLECTION_CORE),
"text": "" "text": ""
}, },
"createdAt": now "createdAt": now
@@ -108,7 +122,7 @@ pub fn init() {
let _ = fs::write(&core_path, serde_json::to_string_pretty(&core_record).unwrap()); let _ = fs::write(&core_path, serde_json::to_string_pretty(&core_record).unwrap());
} }
let memory_dir = collection_dir(&cfg, "ai.syui.gpt.memory"); let memory_dir = collection_dir(&cfg, COLLECTION_MEMORY);
let _ = fs::create_dir_all(&memory_dir); let _ = fs::create_dir_all(&memory_dir);
} }
@@ -121,23 +135,15 @@ pub fn base_dir(cfg: &Config) -> PathBuf {
} }
} }
pub fn identity(cfg: &Config) -> String {
if cfg!(windows) {
cfg.handle.clone().unwrap_or_else(|| "self".to_string())
} else {
cfg.did.clone().unwrap_or_else(|| "self".to_string())
}
}
/// $cfg/{did|handle}/{collection}/{rkey}.json /// $cfg/{did|handle}/{collection}/{rkey}.json
pub fn record_path(cfg: &Config, collection: &str, rkey: &str) -> PathBuf { pub fn record_path(cfg: &Config, collection: &str, rkey: &str) -> PathBuf {
base_dir(cfg) base_dir(cfg)
.join(identity(cfg)) .join(cfg.identity())
.join(collection) .join(collection)
.join(format!("{}.json", rkey)) .join(format!("{}.json", rkey))
} }
/// $cfg/{did|handle}/{collection}/ /// $cfg/{did|handle}/{collection}/
pub fn collection_dir(cfg: &Config, collection: &str) -> PathBuf { pub fn collection_dir(cfg: &Config, collection: &str) -> PathBuf {
base_dir(cfg).join(identity(cfg)).join(collection) base_dir(cfg).join(cfg.identity()).join(collection)
} }

View File

@@ -2,11 +2,11 @@ use anyhow::{Context, Result};
use serde_json::Value; use serde_json::Value;
use std::fs; use std::fs;
use crate::core::config; use crate::core::config::{self, COLLECTION_CORE, COLLECTION_MEMORY};
pub fn read_core() -> Result<Value> { pub fn read_core() -> Result<Value> {
let cfg = config::load(); let cfg = config::load();
let path = config::record_path(&cfg, "ai.syui.gpt.core", "self"); let path = config::record_path(&cfg, COLLECTION_CORE, "self");
let content = fs::read_to_string(&path) let content = fs::read_to_string(&path)
.with_context(|| format!("Failed to read {}", path.display()))?; .with_context(|| format!("Failed to read {}", path.display()))?;
let record: Value = serde_json::from_str(&content) let record: Value = serde_json::from_str(&content)
@@ -16,17 +16,19 @@ pub fn read_core() -> Result<Value> {
pub fn read_memory_all() -> Result<Vec<Value>> { pub fn read_memory_all() -> Result<Vec<Value>> {
let cfg = config::load(); let cfg = config::load();
let dir = config::collection_dir(&cfg, "ai.syui.gpt.memory"); let dir = config::collection_dir(&cfg, COLLECTION_MEMORY);
if !dir.exists() { let entries = match fs::read_dir(&dir) {
return Ok(Vec::new()); Ok(entries) => entries,
} Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
let mut files: Vec<_> = fs::read_dir(&dir)? Err(e) => return Err(e).with_context(|| format!("Failed to read {}", dir.display())),
};
let mut files: Vec<_> = entries
.filter_map(|e| e.ok()) .filter_map(|e| e.ok())
.filter(|e| e.path().extension().is_some_and(|ext| ext == "json")) .filter(|e| e.path().extension().is_some_and(|ext| ext == "json"))
.collect(); .collect();
files.sort_by_key(|e| e.file_name()); files.sort_by_key(|e| e.file_name());
let mut records = Vec::new(); let mut records = Vec::with_capacity(files.len());
for entry in &files { for entry in &files {
let path = entry.path(); let path = entry.path();
let content = fs::read_to_string(&path) let content = fs::read_to_string(&path)
@@ -40,7 +42,7 @@ pub fn read_memory_all() -> Result<Vec<Value>> {
pub fn memory_count() -> usize { pub fn memory_count() -> usize {
let cfg = config::load(); let cfg = config::load();
let dir = config::collection_dir(&cfg, "ai.syui.gpt.memory"); let dir = config::collection_dir(&cfg, COLLECTION_MEMORY);
fs::read_dir(&dir) fs::read_dir(&dir)
.map(|entries| { .map(|entries| {
entries entries

View File

@@ -1,20 +1,22 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use chrono::Utc; use chrono::Utc;
use serde_json::json; use serde_json::{json, Value};
use std::fs; use std::fs;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use crate::core::config; use crate::core::config::{self, COLLECTION_MEMORY};
static TID_COUNTER: AtomicU64 = AtomicU64::new(0);
fn generate_tid() -> String { fn generate_tid() -> String {
// ATProto TID: 64-bit integer as 13 base32-sortable chars
// bit 63: always 0 (sign), bits 62..10: timestamp (microseconds), bits 9..0: clock_id
const CHARSET: &[u8] = b"234567abcdefghijklmnopqrstuvwxyz"; const CHARSET: &[u8] = b"234567abcdefghijklmnopqrstuvwxyz";
let micros = SystemTime::now() let micros = SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
.unwrap() .unwrap()
.as_micros() as u64; .as_micros() as u64;
let v = (micros << 10) & 0x7FFFFFFFFFFFFFFF; let clock_id = TID_COUNTER.fetch_add(1, Ordering::Relaxed) & 0x3FF;
let v = ((micros << 10) | clock_id) & 0x7FFFFFFFFFFFFFFF;
let mut tid = String::with_capacity(13); let mut tid = String::with_capacity(13);
for i in (0..13).rev() { for i in (0..13).rev() {
let idx = ((v >> (i * 5)) & 0x1f) as usize; let idx = ((v >> (i * 5)) & 0x1f) as usize;
@@ -23,27 +25,29 @@ fn generate_tid() -> String {
tid tid
} }
/// Save a single memory element as a new TID file fn build_memory_record(did: &str, tid: &str, text: &str) -> Value {
pub fn save_memory(content: &str) -> Result<()> {
let cfg = config::load();
let did = cfg.did.clone().unwrap_or_else(|| "self".to_string());
let tid = generate_tid();
let now = Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string(); let now = Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();
json!({
let record = json!({ "uri": format!("at://{}/{}/{}", did, COLLECTION_MEMORY, tid),
"uri": format!("at://{}/ai.syui.gpt.memory/{}", did, tid),
"value": { "value": {
"$type": "ai.syui.gpt.memory", "$type": COLLECTION_MEMORY,
"did": did, "did": did,
"content": { "content": {
"$type": "ai.syui.gpt.memory#markdown", "$type": format!("{}#markdown", COLLECTION_MEMORY),
"text": content "text": text
}, },
"createdAt": now "createdAt": now
} }
}); })
}
let dir = config::collection_dir(&cfg, "ai.syui.gpt.memory"); /// Save a single memory element as a new TID file
pub fn save_memory(content: &str) -> Result<()> {
let cfg = config::load();
let tid = generate_tid();
let record = build_memory_record(cfg.did(), &tid, content);
let dir = config::collection_dir(&cfg, COLLECTION_MEMORY);
fs::create_dir_all(&dir) fs::create_dir_all(&dir)
.with_context(|| format!("Failed to create {}", dir.display()))?; .with_context(|| format!("Failed to create {}", dir.display()))?;
let path = dir.join(format!("{}.json", tid)); let path = dir.join(format!("{}.json", tid));
@@ -55,15 +59,13 @@ pub fn save_memory(content: &str) -> Result<()> {
/// Delete all memory files, then write new ones from the given items /// Delete all memory files, then write new ones from the given items
pub fn compress_memory(items: &[String]) -> Result<()> { pub fn compress_memory(items: &[String]) -> Result<()> {
let cfg = config::load(); let cfg = config::load();
let did = cfg.did.clone().unwrap_or_else(|| "self".to_string()); let dir = config::collection_dir(&cfg, COLLECTION_MEMORY);
let dir = config::collection_dir(&cfg, "ai.syui.gpt.memory");
// delete all existing memory files // delete all existing memory files
if dir.exists() { if let Ok(entries) = fs::read_dir(&dir) {
for entry in fs::read_dir(&dir)? { for entry in entries.flatten() {
let entry = entry?;
if entry.path().extension().is_some_and(|ext| ext == "json") { if entry.path().extension().is_some_and(|ext| ext == "json") {
fs::remove_file(entry.path())?; let _ = fs::remove_file(entry.path());
} }
} }
} }
@@ -71,37 +73,9 @@ pub fn compress_memory(items: &[String]) -> Result<()> {
fs::create_dir_all(&dir) fs::create_dir_all(&dir)
.with_context(|| format!("Failed to create {}", dir.display()))?; .with_context(|| format!("Failed to create {}", dir.display()))?;
// write each item as a new TID file
for item in items { for item in items {
let micros = SystemTime::now() let tid = generate_tid();
.duration_since(UNIX_EPOCH) let record = build_memory_record(cfg.did(), &tid, item);
.unwrap()
.as_micros() as u64;
// small delay to ensure unique TIDs
std::thread::sleep(std::time::Duration::from_micros(1));
let v = (micros << 10) & 0x7FFFFFFFFFFFFFFF;
const CHARSET: &[u8] = b"234567abcdefghijklmnopqrstuvwxyz";
let mut tid = String::with_capacity(13);
for i in (0..13).rev() {
let idx = ((v >> (i * 5)) & 0x1f) as usize;
tid.push(CHARSET[idx] as char);
}
let now = Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();
let record = json!({
"uri": format!("at://{}/ai.syui.gpt.memory/{}", did, tid),
"value": {
"$type": "ai.syui.gpt.memory",
"did": did,
"content": {
"$type": "ai.syui.gpt.memory#markdown",
"text": item
},
"createdAt": now
}
});
let path = dir.join(format!("{}.json", tid)); let path = dir.join(format!("{}.json", tid));
let json_str = serde_json::to_string_pretty(&record)?; let json_str = serde_json::to_string_pretty(&record)?;
fs::write(&path, json_str) fs::write(&path, json_str)

View File

@@ -167,21 +167,18 @@ fn is_command_available(cmd: &str) -> bool {
fn print_status() { fn print_status() {
let cfg = config::load(); let cfg = config::load();
let did = cfg.did.clone().unwrap_or_else(|| "self".to_string());
let handle = cfg.handle.clone().unwrap_or_else(|| "self".to_string());
let base = config::base_dir(&cfg); let base = config::base_dir(&cfg);
let id = config::identity(&cfg);
let count = reader::memory_count(); let count = reader::memory_count();
println!("aigpt - AI memory MCP server\n"); println!("aigpt - AI memory MCP server\n");
println!("config: {}", config::config_file().display()); println!("config: {}", config::config_file().display());
println!("did: {}", did); println!("did: {}", cfg.did());
println!("handle: {}", handle); println!("handle: {}", cfg.handle());
println!("memory: {}", cfg.memory); println!("memory: {}", cfg.memory);
println!(); println!();
println!("path: {}/", base.display()); println!("path: {}/", base.display());
println!(" {}/{}", id, "ai.syui.gpt.core/self.json"); println!(" {}/{}/self.json", cfg.identity(), config::COLLECTION_CORE);
println!(" {}/{}", id, "ai.syui.gpt.memory/*.json"); println!(" {}/{}/*.json", cfg.identity(), config::COLLECTION_MEMORY);
println!(); println!();
println!("records: {}/{}", count, cfg.memory); println!("records: {}/{}", count, cfg.memory);
} }

View File

@@ -72,7 +72,7 @@ impl MCPServer {
}, },
"serverInfo": { "serverInfo": {
"name": "aigpt", "name": "aigpt",
"version": "0.3.0" "version": env!("CARGO_PKG_VERSION")
}, },
"instructions": instructions "instructions": instructions
} }