test ai chat

This commit is contained in:
2026-01-20 13:46:28 +09:00
parent 6f5290753d
commit 1d3aa51fb6
27 changed files with 1463 additions and 45 deletions

View File

@@ -20,7 +20,7 @@ struct CreateSessionResponse {
}
/// Login to ATProto PDS
pub async fn login(handle: &str, password: &str, pds: &str) -> Result<()> {
pub async fn login(handle: &str, password: &str, pds: &str, is_bot: bool) -> Result<()> {
let client = reqwest::Client::new();
let url = lexicons::url(pds, &com_atproto_server::CREATE_SESSION);
@@ -29,7 +29,8 @@ pub async fn login(handle: &str, password: &str, pds: &str) -> Result<()> {
password: password.to_string(),
};
println!("Logging in to {} as {}...", pds, handle);
let account_type = if is_bot { "bot" } else { "user" };
println!("Logging in to {} as {} ({})...", pds, handle, account_type);
let res = client
.post(&url)
@@ -54,7 +55,11 @@ pub async fn login(handle: &str, password: &str, pds: &str) -> Result<()> {
pds: Some(pds.to_string()),
};
token::save_session(&session)?;
if is_bot {
token::save_bot_session(&session)?;
} else {
token::save_session(&session)?;
}
println!("Logged in as {} ({})", session.handle, session.did);
Ok(())
@@ -95,3 +100,39 @@ pub async fn refresh_session() -> Result<Session> {
Ok(session)
}
/// Refresh bot access token
pub async fn refresh_bot_session() -> Result<Session> {
let session = token::load_bot_session()?;
let pds = session.pds.as_deref().unwrap_or("bsky.social");
let client = reqwest::Client::new();
let url = lexicons::url(pds, &com_atproto_server::REFRESH_SESSION);
let res = client
.post(&url)
.header("Authorization", format!("Bearer {}", session.refresh_jwt))
.send()
.await
.context("Failed to refresh bot session")?;
if !res.status().is_success() {
let status = res.status();
let body = res.text().await.unwrap_or_default();
anyhow::bail!("Bot refresh failed: {} - {}. Try 'ailog login --bot' again.", status, body);
}
let new_session: CreateSessionResponse = res.json().await?;
let session = Session {
did: new_session.did,
handle: new_session.handle,
access_jwt: new_session.access_jwt,
refresh_jwt: new_session.refresh_jwt,
pds: Some(pds.to_string()),
};
token::save_bot_session(&session)?;
Ok(session)
}

View File

@@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::fs;
use super::auth;
use super::{auth, token};
use crate::lexicons::{self, com_atproto_repo, com_atproto_identity};
#[derive(Debug, Serialize)]
@@ -234,39 +234,55 @@ struct DescribeRepoResponse {
}
/// Sync PDS data to local content directory
pub async fn sync_to_local(output: &str) -> Result<()> {
let config_content = fs::read_to_string("public/config.json")
.context("config.json not found")?;
let config: Config = serde_json::from_str(&config_content)?;
println!("Syncing data for {}", config.handle);
pub async fn sync_to_local(output: &str, is_bot: bool, collection_override: Option<&str>) -> Result<()> {
let client = reqwest::Client::new();
// Resolve handle to DID
let resolve_url = format!(
"{}?handle={}",
lexicons::url("public.api.bsky.app", &com_atproto_identity::RESOLVE_HANDLE),
config.handle
);
let res = client.get(&resolve_url).send().await?;
let resolve: serde_json::Value = res.json().await?;
let did = resolve["did"].as_str().context("Could not resolve handle")?;
let (did, pds, _handle, collection) = if is_bot {
// Bot mode: use bot.json
let session = token::load_bot_session()?;
let pds = session.pds.as_deref().unwrap_or("bsky.social");
let collection = collection_override.unwrap_or("ai.syui.log.chat");
println!("Syncing bot data for {} ({})", session.handle, session.did);
(session.did.clone(), format!("https://{}", pds), session.handle.clone(), collection.to_string())
} else {
// User mode: use config.json
let config_content = fs::read_to_string("public/config.json")
.context("config.json not found")?;
let config: Config = serde_json::from_str(&config_content)?;
println!("Syncing data for {}", config.handle);
// Resolve handle to DID
let resolve_url = format!(
"{}?handle={}",
lexicons::url("public.api.bsky.app", &com_atproto_identity::RESOLVE_HANDLE),
config.handle
);
let res = client.get(&resolve_url).send().await?;
let resolve: serde_json::Value = res.json().await?;
let did = resolve["did"].as_str().context("Could not resolve handle")?.to_string();
// Get PDS from DID document
let plc_url = format!("https://plc.directory/{}", did);
let res = client.get(&plc_url).send().await?;
let did_doc: serde_json::Value = res.json().await?;
let pds = did_doc["service"]
.as_array()
.and_then(|services| {
services.iter().find(|s| s["type"] == "AtprotoPersonalDataServer")
})
.and_then(|s| s["serviceEndpoint"].as_str())
.context("Could not find PDS")?
.to_string();
let collection = collection_override
.map(|s| s.to_string())
.unwrap_or_else(|| config.collection.as_deref().unwrap_or("ai.syui.log.post").to_string());
(did, pds, config.handle.clone(), collection)
};
println!("DID: {}", did);
// Get PDS from DID document
let plc_url = format!("https://plc.directory/{}", did);
let res = client.get(&plc_url).send().await?;
let did_doc: serde_json::Value = res.json().await?;
let pds = did_doc["service"]
.as_array()
.and_then(|services| {
services.iter().find(|s| s["type"] == "AtprotoPersonalDataServer")
})
.and_then(|s| s["serviceEndpoint"].as_str())
.context("Could not find PDS")?;
println!("PDS: {}", pds);
// Remove https:// prefix for lexicons::url
@@ -332,7 +348,6 @@ pub async fn sync_to_local(output: &str) -> Result<()> {
}
// 3. Sync collection records
let collection = config.collection.as_deref().unwrap_or("ai.syui.log.post");
let records_url = format!(
"{}?repo={}&collection={}&limit=100",
lexicons::url(pds_host, &com_atproto_repo::LIST_RECORDS),
@@ -370,3 +385,82 @@ pub async fn sync_to_local(output: &str) -> Result<()> {
Ok(())
}
/// Push local content to PDS
pub async fn push_to_remote(input: &str, collection: &str, is_bot: bool) -> Result<()> {
let session = if is_bot {
auth::refresh_bot_session().await?
} else {
auth::refresh_session().await?
};
let pds = session.pds.as_deref().unwrap_or("bsky.social");
let did = &session.did;
// Build collection directory path
let collection_dir = format!("{}/{}/{}", input, did, collection);
if !std::path::Path::new(&collection_dir).exists() {
anyhow::bail!("Collection directory not found: {}", collection_dir);
}
println!("Pushing records from {} to {}", collection_dir, collection);
let client = reqwest::Client::new();
let url = lexicons::url(pds, &com_atproto_repo::PUT_RECORD);
let mut count = 0;
for entry in fs::read_dir(&collection_dir)? {
let entry = entry?;
let path = entry.path();
// Skip non-JSON files and index.json
if path.extension().map(|e| e != "json").unwrap_or(true) {
continue;
}
let filename = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
if filename == "index" {
continue;
}
let rkey = filename.to_string();
let content = fs::read_to_string(&path)?;
let record_data: Value = serde_json::from_str(&content)?;
// Extract value from record (sync saves as {uri, cid, value})
let record = if record_data.get("value").is_some() {
record_data["value"].clone()
} else {
record_data
};
let req = PutRecordRequest {
repo: did.clone(),
collection: collection.to_string(),
rkey: rkey.clone(),
record,
};
println!("Pushing: {}", rkey);
let res = client
.post(&url)
.header("Authorization", format!("Bearer {}", session.access_jwt))
.json(&req)
.send()
.await?;
if !res.status().is_success() {
let status = res.status();
let body = res.text().await.unwrap_or_default();
println!(" Failed: {} - {}", status, body);
} else {
let result: PutRecordResponse = res.json().await?;
println!(" OK: {}", result.uri);
count += 1;
}
}
println!("Pushed {} records to {}", count, collection);
Ok(())
}

View File

@@ -27,6 +27,16 @@ pub fn token_path() -> Result<PathBuf> {
Ok(config_dir.join("token.json"))
}
/// Get bot token file path: ~/Library/Application Support/ai.syui.log/bot.json
pub fn bot_token_path() -> Result<PathBuf> {
let config_dir = dirs::config_dir()
.context("Could not find config directory")?
.join(BUNDLE_ID);
fs::create_dir_all(&config_dir)?;
Ok(config_dir.join("bot.json"))
}
/// Load session from token file
pub fn load_session() -> Result<Session> {
let path = token_path()?;
@@ -44,3 +54,21 @@ pub fn save_session(session: &Session) -> Result<()> {
println!("Token saved to {:?}", path);
Ok(())
}
/// Load bot session from bot token file
pub fn load_bot_session() -> Result<Session> {
let path = bot_token_path()?;
let content = fs::read_to_string(&path)
.with_context(|| format!("Bot token file not found: {:?}. Run 'ailog login --bot' first.", path))?;
let session: Session = serde_json::from_str(&content)?;
Ok(session)
}
/// Save bot session to bot token file
pub fn save_bot_session(session: &Session) -> Result<()> {
let path = bot_token_path()?;
let content = serde_json::to_string_pretty(session)?;
fs::write(&path, content)?;
println!("Bot token saved to {:?}", path);
Ok(())
}