diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 58baf54..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(cargo init:*)", - "Bash(cargo:*)", - "Bash(find:*)", - "Bash(mkdir:*)", - "Bash(../target/debug/ailog new:*)", - "Bash(../target/debug/ailog build)", - "Bash(/Users/syui/ai/log/target/debug/ailog build)", - "Bash(ls:*)", - "Bash(curl:*)", - "Bash(pkill:*)", - "WebFetch(domain:docs.anthropic.com)", - "WebFetch(domain:github.com)", - "Bash(rm:*)", - "Bash(mv:*)", - "Bash(cp:*)", - "Bash(timeout:*)", - "Bash(grep:*)", - "Bash(./target/debug/ailog:*)", - "Bash(cat:*)", - "Bash(npm install)", - "Bash(npm run build:*)", - "Bash(chmod:*)", - "Bash(./scripts/tunnel.sh:*)", - "Bash(PRODUCTION=true cargo run -- build)", - "Bash(cloudflared tunnel:*)", - "Bash(npm install:*)", - "Bash(./scripts/build-oauth-partial.zsh:*)", - "Bash(./scripts/quick-oauth-update.zsh:*)", - "Bash(../target/debug/ailog serve)", - "Bash(./scripts/test-oauth.sh:*)", - "Bash(./run.zsh:*)", - "Bash(npm run dev:*)", - "Bash(./target/release/ailog:*)", - "Bash(rg:*)", - "Bash(../target/release/ailog build)", - "Bash(zsh run.zsh:*)", - "Bash(hugo:*)", - "WebFetch(domain:docs.bsky.app)", - "WebFetch(domain:syui.ai)", - "Bash(rustup target list:*)", - "Bash(rustup target:*)", - "Bash(git add:*)", - "Bash(git commit:*)", - "Bash(git push:*)", - "Bash(git tag:*)", - "Bash(../bin/ailog:*)", - "Bash(../target/release/ailog oauth build:*)", - "Bash(ailog:*)", - "WebFetch(domain:plc.directory)", - "WebFetch(domain:atproto.com)", - "WebFetch(domain:syu.is)", - "Bash(sed:*)", - "Bash(./scpt/run.zsh:*)", - "Bash(RUST_LOG=debug cargo run -- stream status)", - "Bash(RUST_LOG=debug cargo run -- stream test-api)" - ], - "deny": [] - } -} \ No newline at end of file diff --git a/.gitignore b/.gitignore index a65a40a..1449b77 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ oauth_old oauth_example my-blog/static/oauth/assets/comment-atproto* *.lock +my-blog/config.toml +.claude/settings.local.json diff --git a/Cargo.toml b/Cargo.toml index 2fa8682..c37c183 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ailog" -version = "0.2.4" +version = "0.2.5" edition = "2021" authors = ["syui"] description = "A static blog generator with AI features" @@ -82,4 +82,4 @@ codegen-units = 1 [profile.dev.package."*"] # Optimize dependencies in dev builds -opt-level = 3 \ No newline at end of file +opt-level = 3 diff --git a/my-blog/config.toml b/my-blog/config.toml index bec225c..d6a8342 100644 --- a/my-blog/config.toml +++ b/my-blog/config.toml @@ -16,10 +16,10 @@ auto_translate = false comment_moderation = false ask_ai = true provider = "ollama" -model = "qwen3" +model = "gemma3" model_translation = "llama3.2:1b" model_technical = "phi3:mini" -host = "http://localhost:11434" +host = "http://192.168.11.95:11434" system_prompt = "あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。 相手のことが大好きで、ときどき甘えたり、照れたりします。 でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。" handle = "ai.syui.ai" #num_predict = 200 diff --git a/oauth/package.json b/oauth/package.json index 50f02d1..25c8b3d 100644 --- a/oauth/package.json +++ b/oauth/package.json @@ -1,6 +1,6 @@ { "name": "ailog-oauth", - "version": "0.2.4", + "version": "0.2.5", "type": "module", "scripts": { "dev": "vite", diff --git a/src/commands/auth.rs b/src/commands/auth.rs index 3aae4ef..2e1f3e4 100644 --- a/src/commands/auth.rs +++ b/src/commands/auth.rs @@ -537,19 +537,30 @@ fn migrate_config_if_needed(config_path: &std::path::Path, config_json: &str) -> // Load config with automatic token refresh pub async fn load_config_with_refresh() -> Result { let mut config = load_config()?; + let old_access_jwt = config.admin.access_jwt.clone(); - // Test if current access token is still valid - if let Err(_) = test_api_access_with_auth(&config).await { - println!("{}", "🔄 Access token expired, refreshing...".yellow()); - - // Try to refresh the token - match refresh_access_token(&mut config).await { - Ok(_) => { + // Always try to refresh token to avoid any expiration issues + println!("{}", "🔄 Refreshing access token...".yellow()); + println!("📍 Current access JWT: {}...", &old_access_jwt[..30.min(old_access_jwt.len())]); + + // Try to refresh the token + match refresh_access_token(&mut config).await { + Ok(_) => { + if config.admin.access_jwt != old_access_jwt { + println!("{}", "✅ Token refreshed with new JWT".green()); + println!("📍 New access JWT: {}...", &config.admin.access_jwt[..30.min(config.admin.access_jwt.len())]); save_config(&config)?; - println!("{}", "✅ Token refreshed successfully".green()); + println!("{}", "💾 Config saved to disk".green()); + } else { + println!("{}", "ℹ️ Token refresh returned same JWT (still valid)".cyan()); } - Err(e) => { - return Err(anyhow::anyhow!("Failed to refresh token: {}. Please run 'ailog auth init' again.", e)); + } + Err(e) => { + // If refresh fails, test if current token is still valid + if let Ok(_) = test_api_access_with_auth(&config).await { + println!("{}", "ℹ️ Refresh failed but current token is still valid".cyan()); + } else { + return Err(anyhow::anyhow!("Token expired and refresh failed: {}. Please run 'ailog auth init' again.", e)); } } } @@ -584,6 +595,9 @@ async fn refresh_access_token(config: &mut AuthConfig) -> Result<()> { let client = reqwest::Client::new(); let url = format!("{}/xrpc/com.atproto.server.refreshSession", config.admin.pds); + println!("🔑 Refreshing token at: {}", url); + println!("🔑 Using refresh JWT: {}...", &config.admin.refresh_jwt[..20.min(config.admin.refresh_jwt.len())]); + let response = client .post(&url) .header("Authorization", format!("Bearer {}", config.admin.refresh_jwt)) @@ -601,10 +615,16 @@ async fn refresh_access_token(config: &mut AuthConfig) -> Result<()> { // Update tokens if let Some(access_jwt) = refresh_response["accessJwt"].as_str() { config.admin.access_jwt = access_jwt.to_string(); + println!("✅ New access JWT: {}...", &access_jwt[..20.min(access_jwt.len())]); + } else { + println!("⚠️ No accessJwt in refresh response"); } if let Some(refresh_jwt) = refresh_response["refreshJwt"].as_str() { config.admin.refresh_jwt = refresh_jwt.to_string(); + println!("✅ New refresh JWT: {}...", &refresh_jwt[..20.min(refresh_jwt.len())]); + } else { + println!("⚠️ No refreshJwt in refresh response"); } Ok(()) @@ -612,8 +632,43 @@ async fn refresh_access_token(config: &mut AuthConfig) -> Result<()> { fn save_config(config: &AuthConfig) -> Result<()> { let config_path = get_config_path()?; + println!("💾 Saving config to: {}", config_path.display()); + + // Read old config to compare + let old_config = if config_path.exists() { + fs::read_to_string(&config_path).ok() + } else { + None + }; + let config_json = serde_json::to_string_pretty(config)?; - fs::write(&config_path, config_json)?; + fs::write(&config_path, &config_json)?; + + // Verify the write was successful + let saved_content = fs::read_to_string(&config_path)?; + if saved_content == config_json { + println!("✅ Config successfully saved to {}", config_path.display()); + + // Compare tokens if old config exists + if let Some(old) = old_config { + if let (Ok(old_json), Ok(new_json)) = ( + serde_json::from_str::(&old), + serde_json::from_str::(&config_json) + ) { + if old_json.admin.access_jwt != new_json.admin.access_jwt { + println!("📝 Access JWT was updated in file"); + println!(" Old: {}...", &old_json.admin.access_jwt[..30.min(old_json.admin.access_jwt.len())]); + println!(" New: {}...", &new_json.admin.access_jwt[..30.min(new_json.admin.access_jwt.len())]); + } + if old_json.admin.refresh_jwt != new_json.admin.refresh_jwt { + println!("📝 Refresh JWT was updated in file"); + } + } + } + } else { + println!("❌ Config save verification failed!"); + } + Ok(()) } diff --git a/src/commands/stream.rs b/src/commands/stream.rs index fc7ba11..aeb1e38 100644 --- a/src/commands/stream.rs +++ b/src/commands/stream.rs @@ -446,8 +446,13 @@ pub async fn init_user_list(project_dir: Option, handles: Option, daemon: bool, ai_generate: bool) -> Result<()> { + println!("{}", "🚀 Starting ailog stream server...".cyan()); + println!("{}", "📋 Step 1: Loading and refreshing authentication...".cyan()); + let mut config = load_config_with_refresh().await?; + println!("{}", "📋 Step 2: Configuration loaded successfully".green()); + // Load collection config with priority: env vars > project config > defaults let (collection_comment, _collection_user) = load_collection_config(project_dir.as_deref())?; @@ -2019,7 +2024,14 @@ async fn generate_and_store_comment( ) -> Result<()> { // Generate comment using limited post content for brevity let limited_contents = if post.contents.len() > 300 { - format!("{}...", &post.contents[..300]) + // Use char_indices to safely truncate at character boundaries + let truncate_pos = post.contents + .char_indices() + .take(100) // Take first 100 characters instead of bytes + .last() + .map(|(idx, ch)| idx + ch.len_utf8()) + .unwrap_or(post.contents.len()); + format!("{}...", &post.contents[..truncate_pos]) } else { post.contents.clone() }; @@ -2060,7 +2072,18 @@ async fn store_atproto_record( record_data: &serde_json::Value, ) -> Result<()> { // Always load fresh config to ensure we have valid tokens - let config = load_config_with_refresh().await?; + println!("{} Checking token before putRecord...", "🔄".yellow()); + let config = match load_config_with_refresh().await { + Ok(c) => { + println!("{} Token check/refresh completed for putRecord", "✅".green()); + println!("🔑 Using access JWT: {}...", &c.admin.access_jwt[..30.min(c.admin.access_jwt.len())]); + c + }, + Err(e) => { + println!("{} Failed to refresh token: {}", "❌".red(), e); + return Err(anyhow::anyhow!("Token refresh failed: {}", e)); + } + }; let url = format!("{}/xrpc/com.atproto.repo.putRecord", config.admin.pds);