11 Commits

Author SHA1 Message Date
292bf39c4e fix 2025-06-17 14:15:34 +09:00
be8fd38c59 fix 2025-06-17 14:13:11 +09:00
abc42330ed fix 2025-06-17 14:11:27 +09:00
2143d5b142 fix oauth 2025-06-17 14:00:36 +09:00
549f728b7a fix oauth 2025-06-17 13:58:00 +09:00
849490f5b2 fix oauth 2025-06-17 13:45:40 +09:00
c1d3678d96 fix oauth 2025-06-17 13:43:34 +09:00
fcab7c7f83 fix oauth 2025-06-17 13:19:31 +09:00
51e4a492bc fix: display correct author for user chat messages
Previously, all chat messages in the 'chat' tab were showing with AI account info
regardless of the actual author. This fix ensures the author information from
the record is used, only falling back to AI profile when no author is present.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-17 13:11:04 +09:00
9ed35a6a1e fix oauth 2025-06-17 13:05:01 +09:00
74e1014e77 fix oauth plc 2025-06-17 12:48:55 +09:00
7 changed files with 35 additions and 93 deletions

1
.gitignore vendored
View File

@@ -17,3 +17,4 @@ my-blog/templates/oauth-assets.html
cloudflared-config.yml cloudflared-config.yml
.config .config
oauth-server-example oauth-server-example
atproto

View File

@@ -17,7 +17,7 @@ comment_moderation = false
ask_ai = true ask_ai = true
provider = "ollama" provider = "ollama"
model = "gemma3:4b" model = "gemma3:4b"
host = "https://localhost:11434" host = "http://localhost:11434"
system_prompt = "あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。 相手のことが大好きで、ときどき甘えたり、照れたりします。 でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。" system_prompt = "あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。 相手のことが大好きで、ときどき甘えたり、照れたりします。 でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。"
handle = "ai.syui.ai" handle = "ai.syui.ai"
#num_predict = 200 #num_predict = 200

View File

@@ -16,5 +16,5 @@ VITE_AI_ENABLED=true
VITE_AI_ASK_AI=true VITE_AI_ASK_AI=true
VITE_AI_PROVIDER=ollama VITE_AI_PROVIDER=ollama
VITE_AI_MODEL=gemma3:4b VITE_AI_MODEL=gemma3:4b
VITE_AI_HOST=https://localhost:11434 VITE_AI_HOST=http://localhost:11434
VITE_AI_SYSTEM_PROMPT="あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。 相手のことが大好きで、ときどき甘えたり、照れたりします。 でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。" VITE_AI_SYSTEM_PROMPT="あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。 相手のことが大好きで、ときどき甘えたり、照れたりします。 でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。"

View File

@@ -221,19 +221,25 @@ function App() {
const oauthResult = await atprotoOAuthService.checkSession(); const oauthResult = await atprotoOAuthService.checkSession();
if (oauthResult) { if (oauthResult) {
console.log('OAuth result found:', oauthResult);
// Ensure handle is not DID // Ensure handle is not DID
const handle = oauthResult.handle !== oauthResult.did ? oauthResult.handle : oauthResult.handle; const handle = oauthResult.handle !== oauthResult.did ? oauthResult.handle : oauthResult.handle;
console.log('Processed handle:', handle);
// Check if handle is allowed // Check if handle is allowed
if (appConfig.allowedHandles.length > 0 && !appConfig.allowedHandles.includes(handle)) { if (appConfig.allowedHandles.length > 0 && !appConfig.allowedHandles.includes(handle)) {
console.log('Handle not allowed:', handle, 'Allowed:', appConfig.allowedHandles);
// Handle not in allowed list // Handle not in allowed list
setError(`Access denied: ${handle} is not authorized for this application.`); setError(`Access denied: ${handle} is not authorized for this application.`);
setIsLoading(false); setIsLoading(false);
return; return;
} }
console.log('Getting user profile for:', oauthResult.did, handle);
// Get user profile including avatar // Get user profile including avatar
const userProfile = await getUserProfile(oauthResult.did, handle); const userProfile = await getUserProfile(oauthResult.did, handle);
console.log('User profile received:', userProfile);
setUser(userProfile); setUser(userProfile);
// Load all comments for display (this will be the default view) // Load all comments for display (this will be the default view)
@@ -1213,8 +1219,9 @@ function App() {
// Extract content based on format // Extract content based on format
const contentText = isNewFormat ? value.text : (value.content || value.body || ''); const contentText = isNewFormat ? value.text : (value.content || value.body || '');
// For AI comments, always use the loaded AI profile instead of record.value.author // Use the author from the record if available, otherwise fall back to AI profile
const authorInfo = aiProfile; const authorInfo = value.author || aiProfile;
const postInfo = isNewFormat ? value.post : null; const postInfo = isNewFormat ? value.post : null;
const contentType = value.type || 'unknown'; const contentType = value.type || 'unknown';
const createdAt = value.createdAt || value.generated_at || ''; const createdAt = value.createdAt || value.generated_at || '';

View File

@@ -191,6 +191,7 @@ Answer:`;
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Origin': 'https://syui.ai',
}, },
body: JSON.stringify({ body: JSON.stringify({
model: aiConfig.model, model: aiConfig.model,

View File

@@ -113,7 +113,7 @@ export function getAppConfig(): AppConfig {
const aiEnabled = import.meta.env.VITE_AI_ENABLED === 'true'; const aiEnabled = import.meta.env.VITE_AI_ENABLED === 'true';
const aiAskAi = import.meta.env.VITE_AI_ASK_AI === 'true'; const aiAskAi = import.meta.env.VITE_AI_ASK_AI === 'true';
const aiProvider = import.meta.env.VITE_AI_PROVIDER || 'ollama'; 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 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 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'; const atprotoPds = import.meta.env.VITE_ATPROTO_PDS || 'syu.is';

View File

@@ -41,6 +41,7 @@ class AtprotoOAuthService {
this.oauthClient = await BrowserOAuthClient.load({ this.oauthClient = await BrowserOAuthClient.load({
clientId: clientId, clientId: clientId,
handleResolver: 'https://bsky.social', // Default resolver handleResolver: 'https://bsky.social', // Default resolver
plcDirectoryUrl: 'https://plc.directory', // Default PLC directory
}); });
@@ -92,41 +93,13 @@ class AtprotoOAuthService {
} }
private async processSession(session: any): Promise<{ did: string; handle: string }> { 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; const did = session.sub || session.did;
let handle = session.handle || 'unknown'; let handle = session.handle || 'unknown';
// Create Agent directly with session (per official docs) // Create Agent directly with session (per official docs)
try { try {
this.agent = new Agent(session); this.agent = new Agent(session);
// Check if agent has session info after creation
if (this.agent.session) {
}
} catch (err) { } catch (err) {
// Fallback to dpopFetch method // Fallback to dpopFetch method
this.agent = new Agent({ this.agent = new Agent({
service: session.server?.serviceEndpoint || 'https://bsky.social', service: session.server?.serviceEndpoint || 'https://bsky.social',
@@ -215,7 +188,6 @@ class AtprotoOAuthService {
for (const [domain, pdsUrl] of Object.entries(pdsMapping)) { for (const [domain, pdsUrl] of Object.entries(pdsMapping)) {
if (handle.endsWith(`.${domain}`)) { if (handle.endsWith(`.${domain}`)) {
// Using PDS for domain match
return pdsUrl; return pdsUrl;
} }
} }
@@ -231,7 +203,6 @@ class AtprotoOAuthService {
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
if (data.did) { if (data.did) {
console.log('[OAuth Debug] Resolved handle via', endpoint, '- using that PDS');
return endpoint; return endpoint;
} }
} }
@@ -240,20 +211,16 @@ class AtprotoOAuthService {
} }
} }
} catch (e) { } catch (e) {
console.log('[OAuth Debug] Handle resolution failed, using default'); // Handle resolution failed, using default
} }
// Default to bsky.social // Default to bsky.social
// Using default bsky.social
return 'https://bsky.social'; return 'https://bsky.social';
} }
async initiateOAuthFlow(handle?: string): Promise<void> { async initiateOAuthFlow(handle?: string): Promise<void> {
try { try {
if (!this.oauthClient) { if (!this.oauthClient) {
await this.initialize(); await this.initialize();
} }
@@ -269,24 +236,20 @@ class AtprotoOAuthService {
} }
} }
// Detect PDS based on handle // Detect PDS based on handle
const pdsUrl = await this.detectPDSFromHandle(handle); const pdsUrl = await this.detectPDSFromHandle(handle);
// Starting OAuth flow
// Re-initialize OAuth client with correct PDS if needed
// Always re-initialize OAuth client with detected PDS if (pdsUrl !== 'https://bsky.social') {
// Re-initializing OAuth client // Determine PLC directory for syu.is only
const plcDirectoryUrl = handle.endsWith('.syu.is') || handle.endsWith('.syui.ai') ? 'https://plc.syu.is' : 'https://plc.directory';
// Clear existing client to force fresh initialization
this.oauthClient = null;
this.initializePromise = null;
this.oauthClient = await BrowserOAuthClient.load({ this.oauthClient = await BrowserOAuthClient.load({
clientId: this.getClientId(), clientId: this.getClientId(),
handleResolver: pdsUrl, handleResolver: pdsUrl,
plcDirectoryUrl: plcDirectoryUrl,
}); });
}
// OAuth client initialized // OAuth client initialized
@@ -296,22 +259,8 @@ class AtprotoOAuthService {
try { try {
// Starting OAuth authorization // Starting OAuth authorization
// Try to authorize with DID instead of handle for syu.is PDS only // Use handle directly since PLC directory is now correctly configured
let authTarget = handle; const authUrl = await this.oauthClient.authorize(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', scope: 'atproto transition:generic',
}); });
@@ -379,22 +328,16 @@ class AtprotoOAuthService {
async checkSession(): Promise<{ did: string; handle: string } | null> { async checkSession(): Promise<{ did: string; handle: string } | null> {
try { try {
if (!this.oauthClient) { if (!this.oauthClient) {
await this.initialize(); await this.initialize();
} }
if (!this.oauthClient) { if (!this.oauthClient) {
return null; return null;
} }
const result = await this.oauthClient.init(); const result = await this.oauthClient.init();
if (result?.session) { if (result?.session) {
// Use the common session processing method // Use the common session processing method
return this.processSession(result.session); return this.processSession(result.session);
@@ -458,28 +401,20 @@ class AtprotoOAuthService {
async logout(): Promise<void> { async logout(): Promise<void> {
try { try {
// Clear Agent // Clear Agent
this.agent = null; this.agent = null;
// Clear BrowserOAuthClient session // Clear BrowserOAuthClient session
if (this.oauthClient) { if (this.oauthClient) {
try { try {
// BrowserOAuthClient may have a revoke or signOut method // BrowserOAuthClient may have a revoke or signOut method
if (typeof (this.oauthClient as any).signOut === 'function') { if (typeof (this.oauthClient as any).signOut === 'function') {
await (this.oauthClient as any).signOut(); await (this.oauthClient as any).signOut();
} else if (typeof (this.oauthClient as any).revoke === 'function') { } else if (typeof (this.oauthClient as any).revoke === 'function') {
await (this.oauthClient as any).revoke(); await (this.oauthClient as any).revoke();
} else {
} }
} catch (oauthError) { } catch (oauthError) {
// Ignore logout errors
} }
// Reset the OAuth client to force re-initialization // Reset the OAuth client to force re-initialization
@@ -491,18 +426,16 @@ class AtprotoOAuthService {
localStorage.removeItem('atproto_session'); localStorage.removeItem('atproto_session');
sessionStorage.clear(); sessionStorage.clear();
// Clear all localStorage items that might be related to OAuth // Clear all OAuth-related storage
const keysToRemove: string[] = [];
for (let i = 0; i < localStorage.length; i++) { for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i); const key = localStorage.key(i);
if (key && (key.includes('oauth') || key.includes('atproto') || key.includes('session'))) { if (key && (key.includes('oauth') || key.includes('atproto') || key.includes('session'))) {
keysToRemove.push(key);
}
}
keysToRemove.forEach(key => {
localStorage.removeItem(key); localStorage.removeItem(key);
}); }
}
// Clear internal session info
(this as any)._sessionInfo = null;