diff --git a/.gitignore b/.gitignore index 3b12e31..30d935a 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ my-blog/templates/oauth-assets.html cloudflared-config.yml .config oauth-server-example +atproto diff --git a/my-blog/config.toml b/my-blog/config.toml index 5ffdffe..bec225c 100644 --- a/my-blog/config.toml +++ b/my-blog/config.toml @@ -16,8 +16,10 @@ auto_translate = false comment_moderation = false ask_ai = true provider = "ollama" -model = "gemma3:4b" -host = "https://localhost:11434" +model = "qwen3" +model_translation = "llama3.2:1b" +model_technical = "phi3:mini" +host = "http://localhost:11434" system_prompt = "あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。 相手のことが大好きで、ときどき甘えたり、照れたりします。 でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。" handle = "ai.syui.ai" #num_predict = 200 diff --git a/my-blog/oauth/.env.production b/my-blog/oauth/.env.production index 5ab0fb0..e5e32d2 100644 --- a/my-blog/oauth/.env.production +++ b/my-blog/oauth/.env.production @@ -16,5 +16,5 @@ VITE_AI_ENABLED=true VITE_AI_ASK_AI=true VITE_AI_PROVIDER=ollama VITE_AI_MODEL=gemma3:4b -VITE_AI_HOST=https://localhost:11434 +VITE_AI_HOST=http://localhost:11434 VITE_AI_SYSTEM_PROMPT="あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。 相手のことが大好きで、ときどき甘えたり、照れたりします。 でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。" diff --git a/oauth/.env.production b/oauth/.env.production index e7e8431..89e9ede 100644 --- a/oauth/.env.production +++ b/oauth/.env.production @@ -15,7 +15,7 @@ VITE_ATPROTO_HANDLE_LIST=["syui.syui.ai","ai.syui.ai","ai.ai"] VITE_AI_ENABLED=true VITE_AI_ASK_AI=true VITE_AI_PROVIDER=ollama -VITE_AI_MODEL=gemma3:4b +VITE_AI_MODEL=gemma3:1b VITE_AI_HOST=https://ollama.syui.ai VITE_AI_SYSTEM_PROMPT="あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。 相手のことが大好きで、ときどき甘えたり、照れたりします。 でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。" diff --git a/oauth/src/App.tsx b/oauth/src/App.tsx index f97dce7..31311fe 100644 --- a/oauth/src/App.tsx +++ b/oauth/src/App.tsx @@ -224,13 +224,7 @@ function App() { // Ensure handle is not DID const handle = oauthResult.handle !== oauthResult.did ? oauthResult.handle : oauthResult.handle; - // Check if handle is allowed - if (appConfig.allowedHandles.length > 0 && !appConfig.allowedHandles.includes(handle)) { - // Handle not in allowed list - setError(`Access denied: ${handle} is not authorized for this application.`); - setIsLoading(false); - return; - } + // Note: appConfig.allowedHandles is used for PDS detection, not access control // Get user profile including avatar const userProfile = await getUserProfile(oauthResult.did, handle); @@ -1213,8 +1207,9 @@ function App() { // Extract content based on format const contentText = isNewFormat ? value.text : (value.content || value.body || ''); - // For AI comments, always use the loaded AI profile instead of record.value.author - const authorInfo = aiProfile; + // Use the author from the record if available, otherwise fall back to AI profile + const authorInfo = value.author || aiProfile; + const postInfo = isNewFormat ? value.post : null; const contentType = value.type || 'unknown'; const createdAt = value.createdAt || value.generated_at || ''; diff --git a/oauth/src/components/AIChat.tsx b/oauth/src/components/AIChat.tsx index 642b521..72b8bf3 100644 --- a/oauth/src/components/AIChat.tsx +++ b/oauth/src/components/AIChat.tsx @@ -191,6 +191,7 @@ Answer:`; method: 'POST', headers: { 'Content-Type': 'application/json', + 'Origin': 'https://syui.ai', }, body: JSON.stringify({ model: aiConfig.model, diff --git a/oauth/src/config/app.ts b/oauth/src/config/app.ts index 8b1b400..b73ff51 100644 --- a/oauth/src/config/app.ts +++ b/oauth/src/config/app.ts @@ -113,7 +113,7 @@ export function getAppConfig(): AppConfig { const aiEnabled = import.meta.env.VITE_AI_ENABLED === 'true'; const aiAskAi = import.meta.env.VITE_AI_ASK_AI === 'true'; const aiProvider = import.meta.env.VITE_AI_PROVIDER || 'ollama'; - const aiModel = import.meta.env.VITE_AI_MODEL || 'gemma2:2b'; + const aiModel = import.meta.env.VITE_AI_MODEL || 'gemma3:4b'; const aiHost = import.meta.env.VITE_AI_HOST || 'https://ollama.syui.ai'; const aiSystemPrompt = import.meta.env.VITE_AI_SYSTEM_PROMPT || 'You are a helpful AI assistant trained on this blog\'s content.'; const atprotoPds = import.meta.env.VITE_ATPROTO_PDS || 'syu.is'; diff --git a/oauth/src/services/atproto-oauth.ts b/oauth/src/services/atproto-oauth.ts index 21407da..615fa69 100644 --- a/oauth/src/services/atproto-oauth.ts +++ b/oauth/src/services/atproto-oauth.ts @@ -12,6 +12,7 @@ interface AtprotoSession { class AtprotoOAuthService { private oauthClient: BrowserOAuthClient | null = null; + private oauthClientSyuIs: BrowserOAuthClient | null = null; private agent: Agent | null = null; private initializePromise: Promise | null = null; @@ -31,22 +32,27 @@ class AtprotoOAuthService { private async _doInitialize(): Promise { try { - - // Generate client ID based on current origin const clientId = this.getClientId(); - - // Support multiple PDS hosts for OAuth + // Initialize both OAuth clients this.oauthClient = await BrowserOAuthClient.load({ clientId: clientId, - handleResolver: 'https://bsky.social', // Default resolver + handleResolver: 'https://bsky.social', + plcDirectoryUrl: 'https://plc.directory', + }); + + this.oauthClientSyuIs = await BrowserOAuthClient.load({ + clientId: clientId, + handleResolver: 'https://syu.is', + plcDirectoryUrl: 'https://plc.syu.is', }); - - - // Try to restore existing session - const result = await this.oauthClient.init(); + // Try to restore existing session from either client + let result = await this.oauthClient.init(); + if (!result?.session) { + result = await this.oauthClientSyuIs.init(); + } if (result?.session) { // Create Agent instance with proper configuration @@ -92,41 +98,13 @@ class AtprotoOAuthService { } private async processSession(session: any): Promise<{ did: string; handle: string }> { - - - // Log full session structure - - - - - - - - // Check if agent has properties we can access - if (session.agent) { - - - - } - const did = session.sub || session.did; let handle = session.handle || 'unknown'; // Create Agent directly with session (per official docs) try { this.agent = new Agent(session); - - - // Check if agent has session info after creation - - - - if (this.agent.session) { - - - } } catch (err) { - // Fallback to dpopFetch method this.agent = new Agent({ service: session.server?.serviceEndpoint || 'https://bsky.social', @@ -204,61 +182,15 @@ class AtprotoOAuthService { return `${origin}/client-metadata.json`; } - private async detectPDSFromHandle(handle: string): Promise { - // Handle detection for OAuth PDS routing - - // Check if handle ends with known PDS domains first - const pdsMapping = { - 'syu.is': 'https://syu.is', - 'bsky.social': 'https://bsky.social', - }; - - for (const [domain, pdsUrl] of Object.entries(pdsMapping)) { - if (handle.endsWith(`.${domain}`)) { - // Using PDS for domain match - return pdsUrl; - } - } - - // For handles that don't match domain patterns, resolve via API - try { - // Try to resolve handle to get the actual PDS - const endpoints = ['https://syu.is', 'https://bsky.social']; - - for (const endpoint of endpoints) { - try { - const response = await fetch(`${endpoint}/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`); - if (response.ok) { - const data = await response.json(); - if (data.did) { - console.log('[OAuth Debug] Resolved handle via', endpoint, '- using that PDS'); - return endpoint; - } - } - } catch (e) { - continue; - } - } - } catch (e) { - console.log('[OAuth Debug] Handle resolution failed, using default'); - } - - // Default to bsky.social - // Using default bsky.social - return 'https://bsky.social'; - } async initiateOAuthFlow(handle?: string): Promise { try { - - - if (!this.oauthClient) { - + if (!this.oauthClient || !this.oauthClientSyuIs) { await this.initialize(); } - if (!this.oauthClient) { - throw new Error('Failed to initialize OAuth client'); + if (!this.oauthClient || !this.oauthClientSyuIs) { + throw new Error('Failed to initialize OAuth clients'); } // If handle is not provided, prompt user @@ -269,61 +201,27 @@ class AtprotoOAuthService { } } - - - // Detect PDS based on handle - const pdsUrl = await this.detectPDSFromHandle(handle); - // Starting OAuth flow - - - // Always re-initialize OAuth client with detected PDS - // Re-initializing OAuth client - - // Clear existing client to force fresh initialization - this.oauthClient = null; - this.initializePromise = null; - - this.oauthClient = await BrowserOAuthClient.load({ - clientId: this.getClientId(), - handleResolver: pdsUrl, - }); - - // OAuth client initialized - - // Start OAuth authorization flow - - + // Determine which OAuth client to use + const allowedHandlesStr = import.meta.env.VITE_ATPROTO_HANDLE_LIST || '[]'; + let allowedHandles: string[] = []; try { - // Starting OAuth authorization - - // Try to authorize with DID instead of handle for syu.is PDS only - let authTarget = handle; - if (pdsUrl === 'https://syu.is') { - try { - const resolveResponse = await fetch(`${pdsUrl}/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`); - if (resolveResponse.ok) { - const resolveData = await resolveResponse.json(); - authTarget = resolveData.did; - // Using DID for syu.is OAuth workaround - } - } catch (e) { - // Could not resolve to DID, using handle - } - } - - const authUrl = await this.oauthClient.authorize(authTarget, { - scope: 'atproto transition:generic', - }); - - // Redirect to authorization server - window.location.href = authUrl.toString(); - } catch (authorizeError) { - // Authorization failed - throw authorizeError; + allowedHandles = JSON.parse(allowedHandlesStr); + } catch { + allowedHandles = []; } - } catch (error) { + const usesSyuIs = handle.endsWith('.syu.is') || allowedHandles.includes(handle); + const oauthClient = usesSyuIs ? this.oauthClientSyuIs : this.oauthClient; + // Start OAuth authorization flow + const authUrl = await oauthClient.authorize(handle, { + scope: 'atproto transition:generic', + }); + + // Redirect to authorization server + window.location.href = authUrl.toString(); + + } catch (error) { throw new Error(`OAuth認証の開始に失敗しました: ${error}`); } } @@ -379,21 +277,15 @@ class AtprotoOAuthService { async checkSession(): Promise<{ did: string; handle: string } | null> { try { - - if (!this.oauthClient) { - await this.initialize(); } if (!this.oauthClient) { - return null; } - const result = await this.oauthClient.init(); - if (result?.session) { // Use the common session processing method @@ -458,28 +350,20 @@ class AtprotoOAuthService { async logout(): Promise { try { - - // Clear Agent this.agent = null; - // Clear BrowserOAuthClient session if (this.oauthClient) { - try { // BrowserOAuthClient may have a revoke or signOut method if (typeof (this.oauthClient as any).signOut === 'function') { await (this.oauthClient as any).signOut(); - } else if (typeof (this.oauthClient as any).revoke === 'function') { await (this.oauthClient as any).revoke(); - - } else { - } } catch (oauthError) { - + // Ignore logout errors } // Reset the OAuth client to force re-initialization @@ -491,18 +375,16 @@ class AtprotoOAuthService { localStorage.removeItem('atproto_session'); sessionStorage.clear(); - // Clear all localStorage items that might be related to OAuth - const keysToRemove: string[] = []; + // Clear all OAuth-related storage for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && (key.includes('oauth') || key.includes('atproto') || key.includes('session'))) { - keysToRemove.push(key); + localStorage.removeItem(key); } } - keysToRemove.forEach(key => { - - localStorage.removeItem(key); - }); + + // Clear internal session info + (this as any)._sessionInfo = null; diff --git a/scpt/delete-chat-records.zsh b/scpt/delete-chat-records.zsh index c11ee93..ae8d297 100755 --- a/scpt/delete-chat-records.zsh +++ b/scpt/delete-chat-records.zsh @@ -3,10 +3,10 @@ set -e cb=ai.syui.log -cl=( $cb.user ) +cl=( $cb.chat.lang $cb.chat.comment) f=~/.config/syui/ai/log/config.json -default_collection="ai.syui.log.chat.comment" +default_collection="ai.syui.log.chat" default_pds="syu.is" default_did=`cat $f|jq -r .admin.did` default_token=`cat $f|jq -r .admin.access_jwt` diff --git a/src/commands/stream.rs b/src/commands/stream.rs index 5c370c8..26cde96 100644 --- a/src/commands/stream.rs +++ b/src/commands/stream.rs @@ -1426,21 +1426,8 @@ async fn generate_ai_content(content: &str, prompt_type: &str, ai_config: &AiCon .timeout(std::time::Duration::from_secs(120)) // 2 minute timeout .build()?; - // Try localhost first (for same-server deployment) - let localhost_url = "http://localhost:11434/api/generate"; - match client.post(localhost_url).json(&request).send().await { - Ok(response) if response.status().is_success() => { - let ollama_response: OllamaResponse = response.json().await?; - println!("{}", "✅ Used localhost Ollama".green()); - return Ok(ollama_response.response); - } - _ => { - println!("{}", "⚠️ Localhost Ollama not available, trying remote...".yellow()); - } - } - - // Fallback to remote host - let remote_url = format!("{}/api/generate", ai_config.ollama_host); + // Use configured Ollama host + let ollama_url = format!("{}/api/generate", ai_config.ollama_host); // Check if this is a local/private network connection (no CORS needed) // RFC 1918 private networks + localhost @@ -1461,13 +1448,13 @@ async fn generate_ai_content(content: &str, prompt_type: &str, ai_config: &AiCon } else { false } }); - let mut request_builder = client.post(&remote_url).json(&request); + let mut request_builder = client.post(&ollama_url).json(&request); if !is_local { - println!("{}", format!("🔗 Making request to: {} with Origin: {}", remote_url, ai_config.blog_host).blue()); + println!("{}", format!("🔗 Making request to: {} with Origin: {}", ollama_url, ai_config.blog_host).blue()); request_builder = request_builder.header("Origin", &ai_config.blog_host); } else { - println!("{}", format!("🔗 Making request to local network: {}", remote_url).blue()); + println!("{}", format!("🔗 Making request to local network: {}", ollama_url).blue()); } let response = request_builder.send().await?; @@ -1477,7 +1464,7 @@ async fn generate_ai_content(content: &str, prompt_type: &str, ai_config: &AiCon } let ollama_response: OllamaResponse = response.json().await?; - println!("{}", "✅ Used remote Ollama".green()); + println!("{}", "✅ Ollama request successful".green()); Ok(ollama_response.response) }