test update json

This commit is contained in:
2025-06-15 18:42:49 +09:00
parent 4df7f72312
commit 7e1ed39d06
13 changed files with 540 additions and 473 deletions

View File

@@ -47,7 +47,8 @@
"Bash(git push:*)", "Bash(git push:*)",
"Bash(git tag:*)", "Bash(git tag:*)",
"Bash(../bin/ailog:*)", "Bash(../bin/ailog:*)",
"Bash(../target/release/ailog oauth build:*)" "Bash(../target/release/ailog oauth build:*)",
"Bash(ailog:*)"
], ],
"deny": [] "deny": []
} }

1
.gitignore vendored
View File

@@ -12,3 +12,4 @@ node_modules
package-lock.json package-lock.json
my-blog/static/assets/comment-atproto-* my-blog/static/assets/comment-atproto-*
bin/ailog bin/ailog
docs

View File

@@ -1,3 +1,3 @@
<!-- OAuth Comment System - Load globally for session management --> <!-- OAuth Comment System - Load globally for session management -->
<script type="module" crossorigin src="/assets/comment-atproto-G86WWmu8.js"></script> <script type="module" crossorigin src="/assets/comment-atproto-ne3pH4yy.js"></script>
<link rel="stylesheet" crossorigin href="/assets/comment-atproto-FS0uZjXB.css"> <link rel="stylesheet" crossorigin href="/assets/comment-atproto-FS0uZjXB.css">

View File

@@ -1,3 +1,3 @@
<!-- OAuth Comment System - Load globally for session management --> <!-- OAuth Comment System - Load globally for session management -->
<script type="module" crossorigin src="/assets/comment-atproto-G86WWmu8.js"></script> <script type="module" crossorigin src="/assets/comment-atproto-ne3pH4yy.js"></script>
<link rel="stylesheet" crossorigin href="/assets/comment-atproto-FS0uZjXB.css"> <link rel="stylesheet" crossorigin href="/assets/comment-atproto-FS0uZjXB.css">

View File

@@ -4,18 +4,9 @@ VITE_OAUTH_CLIENT_ID=https://syui.ai/client-metadata.json
VITE_OAUTH_REDIRECT_URI=https://syui.ai/oauth/callback VITE_OAUTH_REDIRECT_URI=https://syui.ai/oauth/callback
VITE_ADMIN_DID=did:plc:uqzpqmrjnptsxezjx4xuh2mn VITE_ADMIN_DID=did:plc:uqzpqmrjnptsxezjx4xuh2mn
# Base collection for OAuth app and ailog (all others are derived) # Base collection (all others are derived via getCollectionNames)
VITE_OAUTH_COLLECTION=ai.syui.log VITE_OAUTH_COLLECTION=ai.syui.log
# [user, chat, chat.lang, chat.comment]
# AI Configuration
VITE_AI_ENABLED=true
VITE_AI_ASK_AI=true
VITE_AI_PROVIDER=ollama
VITE_AI_MODEL=gemma3:4b
VITE_AI_HOST=https://ollama.syui.ai
VITE_AI_SYSTEM_PROMPT="あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。 相手のことが大好きで、ときどき甘えたり、照れたりします。 でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。"
VITE_AI_DID=did:plc:4hqjfn7m6n5hno3doamuhgef
# API Configuration # API Configuration
VITE_BSKY_PUBLIC_API=https://public.api.bsky.app VITE_BSKY_PUBLIC_API=https://public.api.bsky.app
VITE_ATPROTO_API=https://bsky.social

View File

@@ -7,32 +7,10 @@ import { appConfig, getCollectionNames } from './config/app';
import './App.css'; import './App.css';
function App() { function App() {
console.log('APP COMPONENT LOADED - Console working!'); // Handle OAuth callback detection
console.log('Current timestamp:', new Date().toISOString());
// Immediately log URL information on every page load
console.log('IMMEDIATE URL CHECK:');
console.log('- href:', window.location.href);
console.log('- pathname:', window.location.pathname);
console.log('- search:', window.location.search);
console.log('- hash:', window.location.hash);
// Also show URL info via alert if it contains OAuth parameters
if (window.location.search.includes('code=') || window.location.search.includes('state=')) { if (window.location.search.includes('code=') || window.location.search.includes('state=')) {
const urlInfo = `OAuth callback detected!\n\nURL: ${window.location.href}\nSearch: ${window.location.search}`; const urlInfo = `OAuth callback detected!\n\nURL: ${window.location.href}\nSearch: ${window.location.search}`;
alert(urlInfo); alert(urlInfo);
console.log('OAuth callback URL detected!');
} else {
// Check if we have stored OAuth info from previous steps
const preOAuthUrl = sessionStorage.getItem('pre_oauth_url');
const storedState = sessionStorage.getItem('oauth_state');
const storedCodeVerifier = sessionStorage.getItem('oauth_code_verifier');
console.log('=== OAUTH SESSION STORAGE CHECK ===');
console.log('Pre-OAuth URL:', preOAuthUrl);
console.log('Stored state:', storedState);
console.log('Stored code verifier:', storedCodeVerifier ? 'Present' : 'Missing');
console.log('=== END SESSION STORAGE CHECK ===');
} }
const [user, setUser] = useState<User | null>(null); const [user, setUser] = useState<User | null>(null);
@@ -59,7 +37,6 @@ function App() {
const collections = getCollectionNames(appConfig.collections.base); const collections = getCollectionNames(appConfig.collections.base);
ws.onopen = () => { ws.onopen = () => {
console.log('Jetstream connected');
ws.send(JSON.stringify({ ws.send(JSON.stringify({
wantedCollections: [collections.comment] wantedCollections: [collections.comment]
})); }));
@@ -69,22 +46,20 @@ function App() {
try { try {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
if (data.collection === collections.comment && data.commit?.operation === 'create') { if (data.collection === collections.comment && data.commit?.operation === 'create') {
console.log('New comment detected via Jetstream:', data);
// Optionally reload comments // Optionally reload comments
// loadAllComments(window.location.href); // loadAllComments(window.location.href);
} }
} catch (err) { } catch (err) {
console.warn('Failed to parse Jetstream message:', err); // Ignore parsing errors
} }
}; };
ws.onerror = (err) => { ws.onerror = (err) => {
console.warn('Jetstream error:', err); // Ignore Jetstream errors
}; };
return ws; return ws;
} catch (err) { } catch (err) {
console.warn('Failed to setup Jetstream:', err);
return null; return null;
} }
}; };
@@ -108,12 +83,12 @@ function App() {
// キャッシュがなければ、ATProtoから取得認証状態に関係なく // キャッシュがなければ、ATProtoから取得認証状態に関係なく
if (!loadCachedComments()) { if (!loadCachedComments()) {
console.log('No cached comments found, loading from ATProto...');
loadAllComments(); // URLフィルタリングを無効にして全コメント表示 loadAllComments(); // URLフィルタリングを無効にして全コメント表示
} else {
console.log('Cached comments loaded successfully');
} }
// Load AI chat history (認証状態に関係なく、全ユーザーのチャット履歴を表示)
loadAiChatHistory();
// Handle popstate events for mock OAuth flow // Handle popstate events for mock OAuth flow
const handlePopState = () => { const handlePopState = () => {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
@@ -138,12 +113,9 @@ function App() {
// Check existing sessions // Check existing sessions
const checkAuth = async () => { const checkAuth = async () => {
// First check OAuth session using official BrowserOAuthClient // First check OAuth session using official BrowserOAuthClient
console.log('Checking OAuth session...');
const oauthResult = await atprotoOAuthService.checkSession(); const oauthResult = await atprotoOAuthService.checkSession();
console.log('OAuth checkSession result:', oauthResult);
if (oauthResult) { if (oauthResult) {
console.log('OAuth session 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;
@@ -153,11 +125,10 @@ function App() {
// Load all comments for display (this will be the default view) // Load all comments for display (this will be the default view)
// Temporarily disable URL filtering to see all comments // Temporarily disable URL filtering to see all comments
console.log('OAuth session found, loading all comments...');
loadAllComments(); loadAllComments();
// Load AI chat history // Load AI chat history
loadAiChatHistory(userProfile.did); loadAiChatHistory();
// Load user list records if admin // Load user list records if admin
if (userProfile.did === appConfig.adminDid) { if (userProfile.did === appConfig.adminDid) {
@@ -166,8 +137,6 @@ function App() {
setIsLoading(false); setIsLoading(false);
return; return;
} else {
console.log('No OAuth session found');
} }
// Fallback to legacy auth // Fallback to legacy auth
@@ -177,7 +146,6 @@ function App() {
// Load all comments for display (this will be the default view) // Load all comments for display (this will be the default view)
// Temporarily disable URL filtering to see all comments // Temporarily disable URL filtering to see all comments
console.log('Legacy auth session found, loading all comments...');
loadAllComments(); loadAllComments();
// Load user list records if admin // Load user list records if admin
@@ -188,7 +156,6 @@ function App() {
setIsLoading(false); setIsLoading(false);
// 認証状態に関係なく、コメントを読み込む // 認証状態に関係なく、コメントを読み込む
console.log('No auth session found, loading all comments anyway...');
loadAllComments(); loadAllComments();
}; };
@@ -215,7 +182,7 @@ function App() {
}; };
} }
} catch (error) { } catch (error) {
console.error('Failed to get user profile:', error); // Failed to get user profile
} }
// Fallback to basic user info // Fallback to basic user info
@@ -236,27 +203,46 @@ function App() {
return `data:image/svg+xml;base64,${btoa(svg)}`; return `data:image/svg+xml;base64,${btoa(svg)}`;
}; };
const loadAiChatHistory = async (did: string) => { const loadAiChatHistory = async () => {
try { try {
console.log('Loading AI chat history for DID:', did); // Load all chat records from admin's user list records
const agent = atprotoOAuthService.getAgent(); const adminDid = appConfig.adminDid;
if (!agent) { const atprotoApi = appConfig.atprotoApi || 'https://bsky.social';
console.log('No agent available'); const collections = getCollectionNames(appConfig.collections.base);
// First, get user list from admin
const userListResponse = await fetch(`${atprotoApi}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(adminDid)}&collection=${encodeURIComponent(collections.user)}&limit=100`);
if (!userListResponse.ok) {
setAiChatHistory([]);
return; return;
} }
// Get AI chat records from current user const userListData = await userListResponse.json();
const response = await agent.api.com.atproto.repo.listRecords({ const userRecords = userListData.records || [];
repo: did,
collection: appConfig.collections.chat,
limit: 100,
});
console.log('AI chat history loaded:', response.data); // Extract unique DIDs from user records
const chatRecords = response.data.records || []; const userDids = [...new Set(userRecords.map(record => record.value.did).filter(Boolean))];
// Load chat records from all registered users
const allChatRecords = [];
for (const userDid of userDids) {
try {
const chatResponse = await fetch(`${atprotoApi}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(userDid)}&collection=${encodeURIComponent(collections.chat)}&limit=100`);
if (chatResponse.ok) {
const chatData = await chatResponse.json();
const records = chatData.records || [];
allChatRecords.push(...records);
}
} catch (err) {
// Skip failed users
continue;
}
}
// Filter out old records with invalid AI profile data (temporary fix for migration) // Filter out old records with invalid AI profile data (temporary fix for migration)
const validRecords = chatRecords.filter(record => { const validRecords = allChatRecords.filter(record => {
if (record.value.answer) { if (record.value.answer) {
// This is an AI answer - check if it has valid AI profile // This is an AI answer - check if it has valid AI profile
return record.value.author?.handle && return record.value.author?.handle &&
@@ -266,16 +252,13 @@ function App() {
return true; // Keep all questions return true; // Keep all questions
}); });
console.log(`Filtered ${chatRecords.length} records to ${validRecords.length} valid records`); // Sort by creation time
// Sort by creation time and group question-answer pairs
const sortedRecords = validRecords.sort((a, b) => const sortedRecords = validRecords.sort((a, b) =>
new Date(a.value.createdAt).getTime() - new Date(b.value.createdAt).getTime() new Date(b.value.createdAt).getTime() - new Date(a.value.createdAt).getTime()
); );
setAiChatHistory(sortedRecords); setAiChatHistory(sortedRecords);
} catch (err) { } catch (err) {
console.error('Failed to load AI chat history:', err);
setAiChatHistory([]); setAiChatHistory([]);
} }
}; };
@@ -284,58 +267,64 @@ function App() {
const loadAIGeneratedContent = async () => { const loadAIGeneratedContent = async () => {
try { try {
const adminDid = appConfig.adminDid; const adminDid = appConfig.adminDid;
const bskyApi = appConfig.bskyPublicApi || 'https://public.api.bsky.app'; const atprotoApi = appConfig.atprotoApi || 'https://bsky.social';
const collections = getCollectionNames(appConfig.collections.base); const collections = getCollectionNames(appConfig.collections.base);
// Load lang:en records // Load lang:en records
const langResponse = await fetch(`${bskyApi}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(adminDid)}&collection=${encodeURIComponent(collections.chatLang)}&limit=100`); const langResponse = await fetch(`${atprotoApi}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(adminDid)}&collection=${encodeURIComponent(collections.chatLang)}&limit=100`);
if (langResponse.ok) { if (langResponse.ok) {
const langData = await langResponse.json(); const langData = await langResponse.json();
const langRecords = langData.records || []; const langRecords = langData.records || [];
// Filter by current page URL if on post page // Filter by current page path if on post page
const filteredLangRecords = appConfig.rkey const filteredLangRecords = appConfig.rkey
? langRecords.filter(record => record.value.url === window.location.href) ? langRecords.filter(record => {
// Compare path only, not full URL to support localhost vs production
const recordPath = record.value.post?.url ? new URL(record.value.post.url).pathname :
record.value.url ? new URL(record.value.url).pathname : '';
return recordPath === window.location.pathname;
})
: langRecords.slice(0, 3); // Top page: latest 3 : langRecords.slice(0, 3); // Top page: latest 3
setLangEnRecords(filteredLangRecords); setLangEnRecords(filteredLangRecords);
} }
// Load AI comment records // Load AI comment records
const commentResponse = await fetch(`${bskyApi}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(adminDid)}&collection=${encodeURIComponent(collections.chatComment)}&limit=100`); const commentResponse = await fetch(`${atprotoApi}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(adminDid)}&collection=${encodeURIComponent(collections.chatComment)}&limit=100`);
if (commentResponse.ok) { if (commentResponse.ok) {
const commentData = await commentResponse.json(); const commentData = await commentResponse.json();
const commentRecords = commentData.records || []; const commentRecords = commentData.records || [];
// Filter by current page URL if on post page // Filter by current page path if on post page
const filteredCommentRecords = appConfig.rkey const filteredCommentRecords = appConfig.rkey
? commentRecords.filter(record => record.value.url === window.location.href) ? commentRecords.filter(record => {
// Compare path only, not full URL to support localhost vs production
const recordPath = record.value.post?.url ? new URL(record.value.post.url).pathname :
record.value.url ? new URL(record.value.url).pathname : '';
return recordPath === window.location.pathname;
})
: commentRecords.slice(0, 3); // Top page: latest 3 : commentRecords.slice(0, 3); // Top page: latest 3
setAiCommentRecords(filteredCommentRecords); setAiCommentRecords(filteredCommentRecords);
} }
} catch (err) { } catch (err) {
console.error('Failed to load AI generated content:', err); // Ignore errors
} }
}; };
const loadUserComments = async (did: string) => { const loadUserComments = async (did: string) => {
try { try {
console.log('Loading comments for DID:', did);
const agent = atprotoOAuthService.getAgent(); const agent = atprotoOAuthService.getAgent();
if (!agent) { if (!agent) {
console.log('No agent available');
return; return;
} }
// Get comments from current user // Get comments from current user
const response = await agent.api.com.atproto.repo.listRecords({ const response = await agent.api.com.atproto.repo.listRecords({
repo: did, repo: did,
collection: appConfig.collections.comment, collection: getCollectionNames(appConfig.collections.base).comment,
limit: 100, limit: 100,
}); });
console.log('User comments loaded:', response.data);
const userComments = response.data.records || []; const userComments = response.data.records || [];
// Enhance comments with profile information if missing // Enhance comments with profile information if missing
@@ -356,7 +345,7 @@ function App() {
} }
}; };
} catch (err) { } catch (err) {
console.warn('Failed to enhance comment with profile:', err); // Ignore enhancement errors
return record; return record;
} }
} }
@@ -366,7 +355,7 @@ function App() {
setComments(enhancedComments); setComments(enhancedComments);
} catch (err) { } catch (err) {
console.error('Failed to load comments:', err); // Ignore load errors
setComments([]); setComments([]);
} }
}; };
@@ -376,20 +365,20 @@ function App() {
try { try {
// 管理者のユーザーリストを取得 // 管理者のユーザーリストを取得
const adminDid = appConfig.adminDid; const adminDid = appConfig.adminDid;
console.log('Fetching user list from admin DID:', adminDid); // Fetching user list from admin DID
const response = await fetch(`https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(adminDid)}&collection=${encodeURIComponent(appConfig.collections.user)}&limit=100`); const response = await fetch(`https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(adminDid)}&collection=${encodeURIComponent(getCollectionNames(appConfig.collections.base).user)}&limit=100`);
if (!response.ok) { if (!response.ok) {
console.warn('Failed to fetch user list from admin, using default users. Status:', response.status); // Failed to fetch user list from admin, using default users
return getDefaultUsers(); return getDefaultUsers();
} }
const data = await response.json(); const data = await response.json();
const userRecords = data.records || []; const userRecords = data.records || [];
console.log('User records found:', userRecords.length); // User records found
if (userRecords.length === 0) { if (userRecords.length === 0) {
console.log('No user records found, using default users'); // No user records found, using default users
return getDefaultUsers(); return getDefaultUsers();
} }
@@ -401,13 +390,13 @@ function App() {
const resolvedUsers = await Promise.all( const resolvedUsers = await Promise.all(
record.value.users.map(async (user) => { record.value.users.map(async (user) => {
if (user.did && user.did.includes('-placeholder')) { if (user.did && user.did.includes('-placeholder')) {
console.log(`Resolving placeholder DID for ${user.handle}`); // Resolving placeholder DID
try { try {
const profileResponse = await fetch(`${appConfig.bskyPublicApi}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(user.handle)}`); const profileResponse = await fetch(`${appConfig.bskyPublicApi}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(user.handle)}`);
if (profileResponse.ok) { if (profileResponse.ok) {
const profileData = await profileResponse.json(); const profileData = await profileResponse.json();
if (profileData.did) { if (profileData.did) {
console.log(`Resolved ${user.handle}: ${user.did} -> ${profileData.did}`); // Resolved DID
return { return {
...user, ...user,
did: profileData.did did: profileData.did
@@ -415,7 +404,7 @@ function App() {
} }
} }
} catch (err) { } catch (err) {
console.warn(`Failed to resolve DID for ${user.handle}:`, err); // Failed to resolve DID
} }
} }
return user; return user;
@@ -425,10 +414,10 @@ function App() {
} }
} }
console.log('Loaded and resolved users from admin records:', allUsers); // Loaded and resolved users from admin records
return allUsers; return allUsers;
} catch (err) { } catch (err) {
console.warn('Failed to load users from records, using defaults:', err); // Failed to load users from records, using defaults
return getDefaultUsers(); return getDefaultUsers();
} }
}; };
@@ -436,12 +425,12 @@ function App() {
// ユーザーリスト一覧を読み込み // ユーザーリスト一覧を読み込み
const loadUserListRecords = async () => { const loadUserListRecords = async () => {
try { try {
console.log('Loading user list records...'); // Loading user list records
const adminDid = appConfig.adminDid; const adminDid = appConfig.adminDid;
const response = await fetch(`https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(adminDid)}&collection=${encodeURIComponent(appConfig.collections.user)}&limit=100`); const response = await fetch(`https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(adminDid)}&collection=${encodeURIComponent(getCollectionNames(appConfig.collections.base).user)}&limit=100`);
if (!response.ok) { if (!response.ok) {
console.warn('Failed to fetch user list records'); // Failed to fetch user list records
setUserListRecords([]); setUserListRecords([]);
return; return;
} }
@@ -454,10 +443,10 @@ function App() {
new Date(b.value.createdAt).getTime() - new Date(a.value.createdAt).getTime() new Date(b.value.createdAt).getTime() - new Date(a.value.createdAt).getTime()
); );
console.log(`Loaded ${sortedRecords.length} user list records`); // Loaded user list records
setUserListRecords(sortedRecords); setUserListRecords(sortedRecords);
} catch (err) { } catch (err) {
console.error('Failed to load user list records:', err); // Failed to load user list records
setUserListRecords([]); setUserListRecords([]);
} }
}; };
@@ -477,39 +466,33 @@ function App() {
}); });
} }
console.log('Default users list (including current user):', defaultUsers); // Default users list (including current user)
return defaultUsers; return defaultUsers;
}; };
// 新しい関数: 全ユーザーからコメントを収集 // 新しい関数: 全ユーザーからコメントを収集
const loadAllComments = async (pageUrl?: string) => { const loadAllComments = async (pageUrl?: string) => {
try { try {
console.log('Loading comments from all users...');
console.log('Page URL filter:', pageUrl);
// ユーザーリストを動的に取得 // ユーザーリストを動的に取得
const knownUsers = await loadUsersFromRecord(); const knownUsers = await loadUsersFromRecord();
console.log('Known users for comment fetching:', knownUsers);
const allComments = []; const allComments = [];
// 各ユーザーからコメントを収集 // 各ユーザーからコメントを収集
for (const user of knownUsers) { for (const user of knownUsers) {
try { try {
console.log(`Fetching comments from user: ${user.handle} (${user.did}) at ${user.pds}`);
// Public API使用認証不要 // Public API使用認証不要
const collections = getCollectionNames(appConfig.collections.base); const collections = getCollectionNames(appConfig.collections.base);
const response = await fetch(`${user.pds}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(user.did)}&collection=${encodeURIComponent(collections.comment)}&limit=100`); const response = await fetch(`${user.pds}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(user.did)}&collection=${encodeURIComponent(collections.comment)}&limit=100`);
if (!response.ok) { if (!response.ok) {
console.warn(`Failed to fetch from ${user.handle} (${response.status}): ${response.statusText}`);
continue; continue;
} }
const data = await response.json(); const data = await response.json();
const userRecords = data.records || []; const userRecords = data.records || [];
console.log(`Found ${userRecords.length} comment records from ${user.handle}`);
// Flatten comments from new array format // Flatten comments from new array format
const userComments = []; const userComments = [];
@@ -529,18 +512,24 @@ function App() {
} }
} }
console.log(`Flattened to ${userComments.length} individual comments from ${user.handle}`);
// ページURLでフィルタリング(指定された場合) // ページpathでフィルタリング(指定された場合)
const filteredComments = pageUrl const filteredComments = pageUrl
? userComments.filter(record => record.value.url === pageUrl) ? userComments.filter(record => {
try {
// Compare path only, not full URL to support localhost vs production
const recordPath = record.value.url ? new URL(record.value.url).pathname : '';
const currentPath = new URL(pageUrl).pathname;
return recordPath === currentPath;
} catch (err) {
// Fallback to exact match if URL parsing fails
return record.value.url === pageUrl;
}
})
: userComments; : userComments;
console.log(`After URL filtering (${pageUrl}): ${filteredComments.length} comments from ${user.handle}`);
console.log('All comments from this user:', userComments.map(r => ({ url: r.value.url, text: r.value.text })));
allComments.push(...filteredComments); allComments.push(...filteredComments);
} catch (err) { } catch (err) {
console.warn(`Failed to load comments from ${user.handle}:`, err);
} }
} }
@@ -572,21 +561,17 @@ function App() {
}; };
} }
} catch (err) { } catch (err) {
console.warn('Failed to enhance comment with profile:', err); // Ignore enhancement errors
} }
} }
return record; return record;
}) })
); );
console.log(`Loaded ${enhancedComments.length} comments from all users`);
// デバッグ情報を追加 // デバッグ情報を追加
console.log('Final enhanced comments:', enhancedComments);
console.log('Known users used:', knownUsers);
setComments(enhancedComments); setComments(enhancedComments);
console.log('Comments state updated with', enhancedComments.length, 'comments');
// キャッシュに保存5分間有効 // キャッシュに保存5分間有効
if (pageUrl) { if (pageUrl) {
@@ -598,7 +583,6 @@ function App() {
localStorage.setItem(cacheKey, JSON.stringify(cacheData)); localStorage.setItem(cacheKey, JSON.stringify(cacheData));
} }
} catch (err) { } catch (err) {
console.error('Failed to load all comments:', err);
setComments([]); setComments([]);
} }
}; };
@@ -640,7 +624,7 @@ function App() {
try { try {
const existingResponse = await agent.api.com.atproto.repo.getRecord({ const existingResponse = await agent.api.com.atproto.repo.getRecord({
repo: user.did, repo: user.did,
collection: appConfig.collections.comment, collection: getCollectionNames(appConfig.collections.base).comment,
rkey: rkey, rkey: rkey,
}); });
@@ -659,7 +643,6 @@ function App() {
} }
} catch (err) { } catch (err) {
// Record doesn't exist yet, that's fine // Record doesn't exist yet, that's fine
console.log('No existing record found, creating new one');
} }
// Add new comment to the array // Add new comment to the array
@@ -667,7 +650,7 @@ function App() {
// Create the record with comments array // Create the record with comments array
const record = { const record = {
$type: appConfig.collections.comment, $type: getCollectionNames(appConfig.collections.base).comment,
comments: existingComments, comments: existingComments,
url: window.location.href, url: window.location.href,
createdAt: now.toISOString(), // Latest update time createdAt: now.toISOString(), // Latest update time
@@ -676,18 +659,16 @@ function App() {
// Post to ATProto with rkey // Post to ATProto with rkey
const response = await agent.api.com.atproto.repo.putRecord({ const response = await agent.api.com.atproto.repo.putRecord({
repo: user.did, repo: user.did,
collection: appConfig.collections.comment, collection: getCollectionNames(appConfig.collections.base).comment,
rkey: rkey, rkey: rkey,
record: record, record: record,
}); });
console.log('Comment posted:', response);
// Clear form and reload all comments // Clear form and reload all comments
setCommentText(''); setCommentText('');
await loadAllComments(window.location.href); await loadAllComments(window.location.href);
} catch (err: any) { } catch (err: any) {
console.error('Failed to post comment:', err);
setError('コメントの投稿に失敗しました: ' + err.message); setError('コメントの投稿に失敗しました: ' + err.message);
} finally { } finally {
setIsPosting(false); setIsPosting(false);
@@ -714,22 +695,19 @@ function App() {
const uriParts = uri.split('/'); const uriParts = uri.split('/');
const rkey = uriParts[uriParts.length - 1]; const rkey = uriParts[uriParts.length - 1];
console.log('Deleting comment with rkey:', rkey);
// Delete the record // Delete the record
await agent.api.com.atproto.repo.deleteRecord({ await agent.api.com.atproto.repo.deleteRecord({
repo: user.did, repo: user.did,
collection: appConfig.collections.comment, collection: getCollectionNames(appConfig.collections.base).comment,
rkey: rkey, rkey: rkey,
}); });
console.log('Comment deleted successfully');
// Reload all comments to reflect the deletion // Reload all comments to reflect the deletion
await loadAllComments(window.location.href); await loadAllComments(window.location.href);
} catch (err: any) { } catch (err: any) {
console.error('Failed to delete comment:', err);
alert('コメントの削除に失敗しました: ' + err.message); alert('コメントの削除に失敗しました: ' + err.message);
} }
}; };
@@ -787,11 +765,9 @@ function App() {
const profileData = await profileResponse.json(); const profileData = await profileResponse.json();
if (profileData.did) { if (profileData.did) {
resolvedDid = profileData.did; resolvedDid = profileData.did;
console.log(`Resolved ${handle} -> ${resolvedDid}`);
} }
} }
} catch (err) { } catch (err) {
console.warn(`Failed to resolve DID for ${handle}:`, err);
} }
return { return {
@@ -806,7 +782,7 @@ function App() {
const rkey = now.toISOString().replace(/[:.]/g, '-'); const rkey = now.toISOString().replace(/[:.]/g, '-');
const record = { const record = {
$type: appConfig.collections.user, $type: getCollectionNames(appConfig.collections.base).user,
users: users, users: users,
createdAt: now.toISOString(), createdAt: now.toISOString(),
updatedBy: { updatedBy: {
@@ -818,19 +794,17 @@ function App() {
// Post to ATProto with rkey // Post to ATProto with rkey
const response = await agent.api.com.atproto.repo.putRecord({ const response = await agent.api.com.atproto.repo.putRecord({
repo: user.did, repo: user.did,
collection: appConfig.collections.user, collection: getCollectionNames(appConfig.collections.base).user,
rkey: rkey, rkey: rkey,
record: record, record: record,
}); });
console.log('User list posted:', response);
// Clear form and reload user list records // Clear form and reload user list records
setUserListInput(''); setUserListInput('');
loadUserListRecords(); loadUserListRecords();
alert('ユーザーリストが更新されました'); alert('ユーザーリストが更新されました');
} catch (err: any) { } catch (err: any) {
console.error('Failed to post user list:', err);
setError('ユーザーリストの投稿に失敗しました: ' + err.message); setError('ユーザーリストの投稿に失敗しました: ' + err.message);
} finally { } finally {
setIsPostingUserList(false); setIsPostingUserList(false);
@@ -858,21 +832,18 @@ function App() {
const uriParts = uri.split('/'); const uriParts = uri.split('/');
const rkey = uriParts[uriParts.length - 1]; const rkey = uriParts[uriParts.length - 1];
console.log('Deleting user list with rkey:', rkey);
// Delete the record // Delete the record
await agent.api.com.atproto.repo.deleteRecord({ await agent.api.com.atproto.repo.deleteRecord({
repo: user.did, repo: user.did,
collection: appConfig.collections.user, collection: getCollectionNames(appConfig.collections.base).user,
rkey: rkey, rkey: rkey,
}); });
console.log('User list deleted successfully');
loadUserListRecords(); loadUserListRecords();
alert('ユーザーリストが削除されました'); alert('ユーザーリストが削除されました');
} catch (err: any) { } catch (err: any) {
console.error('Failed to delete user list:', err);
alert('ユーザーリストの削除に失敗しました: ' + err.message); alert('ユーザーリストの削除に失敗しました: ' + err.message);
} }
}; };
@@ -895,7 +866,6 @@ function App() {
try { try {
await atprotoOAuthService.initiateOAuthFlow(handleInput); await atprotoOAuthService.initiateOAuthFlow(handleInput);
} catch (err) { } catch (err) {
console.error('OAuth failed:', err);
alert('認証の開始に失敗しました。再度お試しください。'); alert('認証の開始に失敗しました。再度お試しください。');
} }
}; };
@@ -918,7 +888,13 @@ function App() {
} }
// Extract rkey from comment URI: at://did:plc:xxx/collection/rkey // Extract rkey from comment URI: at://did:plc:xxx/collection/rkey
const uriParts = record.uri.split('/'); // Handle both original records and flattened records from new array format
const uri = record.uri || record.originalRecord?.uri;
if (!uri) {
return false;
}
const uriParts = uri.split('/');
const commentRkey = uriParts[uriParts.length - 1]; const commentRkey = uriParts[uriParts.length - 1];
// Show comment only if rkey matches current post // Show comment only if rkey matches current post
@@ -926,12 +902,130 @@ function App() {
}; };
// OAuth callback is now handled by React Router in main.tsx // OAuth callback is now handled by React Router in main.tsx
console.log('=== APP.TSX URL CHECK ===');
console.log('Full URL:', window.location.href);
console.log('Pathname:', window.location.pathname);
console.log('Search params:', window.location.search);
console.log('=== END URL CHECK ===');
// Unified rendering function for AI content
const renderAIContent = (record: any, index: number, className: string) => {
// Handle both new format (record.value.$type) and old format compatibility
const value = record.value;
const isNewFormat = value.$type && value.post && value.author;
// Extract content based on format
const contentText = isNewFormat ? value.text : (value.content || value.body || '');
const authorInfo = isNewFormat ? value.author : null;
const postInfo = isNewFormat ? value.post : null;
const contentType = value.type || 'unknown';
const createdAt = value.createdAt || value.generated_at || '';
return (
<div key={index} className={className}>
<div className="content-header">
<img
src={authorInfo?.avatar || generatePlaceholderAvatar('AI')}
alt="AI Avatar"
className="comment-avatar"
ref={(img) => {
// For old format, try to fetch from ai_did
if (img && !isNewFormat && value.ai_did) {
fetch(`${appConfig.bskyPublicApi}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(value.ai_did)}`)
.then(res => res.json())
.then(data => {
if (data.avatar && img) {
img.src = data.avatar;
}
})
.catch(err => {
// Keep placeholder on error
});
}
}}
/>
<div className="comment-author-info">
<span className="comment-author">
{authorInfo?.displayName || 'AI'}
</span>
<span className="comment-handle">
@{authorInfo?.handle || 'ai'}
</span>
<span className="content-type-badge">{getTypeLabel(value.$type, contentType)}</span>
</div>
<span className="comment-date">
{new Date(createdAt).toLocaleString()}
</span>
<div className="comment-actions">
<button
onClick={() => toggleJsonDisplay(record.uri)}
className="json-button"
title="Show/Hide JSON"
>
{showJsonFor === record.uri ? 'Hide' : 'JSON'}
</button>
</div>
</div>
{/* Post information for new format */}
{postInfo && (
<div className="post-info">
<h4>{postInfo.title}</h4>
{postInfo.tags && (
<div className="tags">
{postInfo.tags.map((tag: string) => (
<span key={tag} className="tag">#{tag}</span>
))}
</div>
)}
</div>
)}
{/* Legacy post information for old format */}
{!postInfo && (value.post_title || value.post_url) && (
<div className="post-info">
<h4>{value.post_title || 'Unknown'}</h4>
</div>
)}
<div className="content-text">
{contentText}
</div>
<div className="comment-meta">
{(postInfo?.url || value.post_url) && (
<small>
<a href={postInfo?.url || value.post_url}>
{postInfo?.url || value.post_url}
</a>
</small>
)}
</div>
{/* JSON Display */}
{showJsonFor === record.uri && (
<div className="json-display">
<h5>JSON Record:</h5>
<pre className="json-content">
{JSON.stringify(record, null, 2)}
</pre>
</div>
)}
</div>
);
};
const getTypeLabel = (collectionType: string, contentType: string) => {
if (!collectionType) return contentType;
const collections = getCollectionNames(appConfig.collections.base);
if (collectionType === collections.chat) {
return contentType === 'question' ? '質問' : '回答';
}
if (collectionType === collections.chatLang) {
return `翻訳: ${contentType.toUpperCase()}`;
}
if (collectionType === collections.chatComment) {
return `AI ${contentType}`;
}
return contentType;
};
return ( return (
<div className="app"> <div className="app">
@@ -1081,14 +1175,12 @@ function App() {
> >
Comments ({comments.filter(shouldShowComment).length}) Comments ({comments.filter(shouldShowComment).length})
</button> </button>
{user && (
<button <button
className={`tab-button ${activeTab === 'ai-chat' ? 'active' : ''}`} className={`tab-button ${activeTab === 'ai-chat' ? 'active' : ''}`}
onClick={() => setActiveTab('ai-chat')} onClick={() => setActiveTab('ai-chat')}
> >
AI Chat History ({aiChatHistory.length}) AI Chat History ({aiChatHistory.length})
</button> </button>
)}
<button <button
className={`tab-button ${activeTab === 'lang-en' ? 'active' : ''}`} className={`tab-button ${activeTab === 'lang-en' ? 'active' : ''}`}
onClick={() => setActiveTab('lang-en')} onClick={() => setActiveTab('lang-en')}
@@ -1129,7 +1221,6 @@ function App() {
} }
}) })
.catch(err => { .catch(err => {
console.warn('Failed to fetch fresh avatar:', err);
// Keep placeholder on error // Keep placeholder on error
}); });
} }
@@ -1196,7 +1287,7 @@ function App() {
)} )}
{/* AI Chat History List */} {/* AI Chat History List */}
{activeTab === 'ai-chat' && user && ( {activeTab === 'ai-chat' && (
<div className="ai-chat-list"> <div className="ai-chat-list">
<div className="chat-header"> <div className="chat-header">
<h3>AI Chat History</h3> <h3>AI Chat History</h3>
@@ -1222,7 +1313,6 @@ function App() {
} }
}) })
.catch(err => { .catch(err => {
console.warn('Failed to fetch fresh avatar:', err);
// Keep placeholder on error // Keep placeholder on error
}); });
} }
@@ -1287,37 +1377,9 @@ function App() {
{langEnRecords.length === 0 ? ( {langEnRecords.length === 0 ? (
<p className="no-content">No English translations yet</p> <p className="no-content">No English translations yet</p>
) : ( ) : (
langEnRecords.map((record, index) => ( langEnRecords.map((record, index) =>
<div key={index} className="lang-item"> renderAIContent(record, index, 'lang-item')
<div className="lang-header"> )
<img
src={record.value.author?.avatar || generatePlaceholderAvatar(record.value.author?.handle || 'AI')}
alt="AI Avatar"
className="comment-avatar"
/>
<div className="comment-author-info">
<span className="comment-author">
{record.value.author?.displayName || 'AI Translator'}
</span>
<span className="comment-handle">
@{record.value.author?.handle || 'ai'}
</span>
</div>
<span className="comment-date">
{new Date(record.value.createdAt).toLocaleString()}
</span>
</div>
<div className="lang-content">
<div className="lang-type">Type: {record.value.type || 'en'}</div>
<div className="lang-body">{record.value.body}</div>
</div>
<div className="comment-meta">
{record.value.url && (
<small><a href={record.value.url}>{record.value.url}</a></small>
)}
</div>
</div>
))
)} )}
</div> </div>
)} )}
@@ -1328,37 +1390,9 @@ function App() {
{aiCommentRecords.length === 0 ? ( {aiCommentRecords.length === 0 ? (
<p className="no-content">No AI comments yet</p> <p className="no-content">No AI comments yet</p>
) : ( ) : (
aiCommentRecords.map((record, index) => ( aiCommentRecords.map((record, index) =>
<div key={index} className="ai-comment-item"> renderAIContent(record, index, 'ai-comment-item')
<div className="ai-comment-header"> )
<img
src={record.value.author?.avatar || generatePlaceholderAvatar(record.value.author?.handle || 'AI')}
alt="AI Avatar"
className="comment-avatar"
/>
<div className="comment-author-info">
<span className="comment-author">
{record.value.author?.displayName || 'AI Commenter'}
</span>
<span className="comment-handle">
@{record.value.author?.handle || 'ai'}
</span>
</div>
<span className="comment-date">
{new Date(record.value.createdAt).toLocaleString()}
</span>
</div>
<div className="ai-comment-content">
<div className="ai-comment-type">Type: {record.value.type || 'comment'}</div>
<div className="ai-comment-body">{record.value.body}</div>
</div>
<div className="comment-meta">
{record.value.url && (
<small><a href={record.value.url}>{record.value.url}</a></small>
)}
</div>
</div>
))
)} )}
</div> </div>
)} )}

View File

@@ -12,17 +12,25 @@ export interface AppConfig {
aiModel: string; aiModel: string;
aiHost: string; aiHost: string;
bskyPublicApi: string; bskyPublicApi: string;
atprotoApi: string;
} }
// Collection name builders (similar to Rust implementation) // Collection name builders (similar to Rust implementation)
export function getCollectionNames(base: string) { export function getCollectionNames(base: string) {
return { if (!base) {
// Fallback to default
base = 'ai.syui.log';
}
const collections = {
comment: base, comment: base,
user: `${base}.user`, user: `${base}.user`,
chat: `${base}.chat`, chat: `${base}.chat`,
chatLang: `${base}.chat.lang`, chatLang: `${base}.chat.lang`,
chatComment: `${base}.chat.comment`, chatComment: `${base}.chat.comment`,
}; };
return collections;
} }
// Generate collection names from host // Generate collection names from host
@@ -43,9 +51,9 @@ function generateBaseCollectionFromHost(host: string): string {
// Reverse the parts for collection naming // Reverse the parts for collection naming
// log.syui.ai -> ai.syui.log // log.syui.ai -> ai.syui.log
const reversedParts = parts.reverse(); const reversedParts = parts.reverse();
return reversedParts.join('.'); const result = reversedParts.join('.');
return result;
} catch (error) { } catch (error) {
console.warn('Failed to generate collection base from host:', host, error);
// Fallback to default // Fallback to default
return 'ai.syui.log'; return 'ai.syui.log';
} }
@@ -66,8 +74,15 @@ export function getAppConfig(): AppConfig {
// Priority: Environment variables > Auto-generated from host // Priority: Environment variables > Auto-generated from host
const autoGeneratedBase = generateBaseCollectionFromHost(host); const autoGeneratedBase = generateBaseCollectionFromHost(host);
let baseCollection = import.meta.env.VITE_OAUTH_COLLECTION || autoGeneratedBase;
// Ensure base collection is never undefined
if (!baseCollection) {
baseCollection = 'ai.syui.log';
}
const collections = { const collections = {
base: import.meta.env.VITE_OAUTH_COLLECTION || autoGeneratedBase, base: baseCollection,
}; };
const rkey = extractRkeyFromUrl(); const rkey = extractRkeyFromUrl();
@@ -79,15 +94,8 @@ export function getAppConfig(): AppConfig {
const aiModel = import.meta.env.VITE_AI_MODEL || 'gemma2:2b'; const aiModel = import.meta.env.VITE_AI_MODEL || 'gemma2:2b';
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 bskyPublicApi = import.meta.env.VITE_BSKY_PUBLIC_API || 'https://public.api.bsky.app'; const bskyPublicApi = import.meta.env.VITE_BSKY_PUBLIC_API || 'https://public.api.bsky.app';
const atprotoApi = import.meta.env.VITE_ATPROTO_API || 'https://bsky.social';
console.log('App configuration:', {
host,
adminDid,
collections,
rkey: rkey || 'none (not on post page)',
ai: { enabled: aiEnabled, askAi: aiAskAi, provider: aiProvider, model: aiModel, host: aiHost },
bskyPublicApi
});
return { return {
adminDid, adminDid,
@@ -99,7 +107,8 @@ export function getAppConfig(): AppConfig {
aiProvider, aiProvider,
aiModel, aiModel,
aiHost, aiHost,
bskyPublicApi bskyPublicApi,
atprotoApi
}; };
} }

View File

@@ -73,7 +73,6 @@ export const aiCardApi = {
}); });
return response.data.data; return response.data.data;
} catch (error) { } catch (error) {
console.warn('ai.gpt AI分析機能が利用できません:', error);
throw new Error('AI分析機能を利用するにはai.gptサーバーが必要です'); throw new Error('AI分析機能を利用するにはai.gptサーバーが必要です');
} }
}, },
@@ -86,7 +85,6 @@ export const aiCardApi = {
const response = await aiGptApi.get('/card_get_gacha_stats'); const response = await aiGptApi.get('/card_get_gacha_stats');
return response.data.data; return response.data.data;
} catch (error) { } catch (error) {
console.warn('ai.gpt AI統計機能が利用できません:', error);
throw new Error('AI統計機能を利用するにはai.gptサーバーが必要です'); throw new Error('AI統計機能を利用するにはai.gptサーバーが必要です');
} }
}, },

View File

@@ -31,11 +31,11 @@ class AtprotoOAuthService {
private async _doInitialize(): Promise<void> { private async _doInitialize(): Promise<void> {
try { try {
console.log('=== INITIALIZING ATPROTO OAUTH CLIENT ===');
// Generate client ID based on current origin // Generate client ID based on current origin
const clientId = this.getClientId(); const clientId = this.getClientId();
console.log('Client ID:', clientId);
// Support multiple PDS hosts for OAuth // Support multiple PDS hosts for OAuth
this.oauthClient = await BrowserOAuthClient.load({ this.oauthClient = await BrowserOAuthClient.load({
@@ -43,39 +43,33 @@ class AtprotoOAuthService {
handleResolver: 'https://bsky.social', // Default resolver handleResolver: 'https://bsky.social', // Default resolver
}); });
console.log('BrowserOAuthClient initialized successfully with multi-PDS support');
// Try to restore existing session // Try to restore existing session
const result = await this.oauthClient.init(); const result = await this.oauthClient.init();
if (result?.session) { if (result?.session) {
console.log('Existing session restored:', {
did: result.session.did,
handle: result.session.handle || 'unknown',
hasAccessJwt: !!result.session.accessJwt,
hasRefreshJwt: !!result.session.refreshJwt
});
// Create Agent instance with proper configuration // Create Agent instance with proper configuration
console.log('Creating Agent with session:', result.session);
// Delete the old agent initialization code - we'll create it properly below // Delete the old agent initialization code - we'll create it properly below
// Set the session after creating the agent // Set the session after creating the agent
// The session object from BrowserOAuthClient appears to be a special object // The session object from BrowserOAuthClient appears to be a special object
console.log('Full session object:', result.session);
console.log('Session type:', typeof result.session);
console.log('Session constructor:', result.session?.constructor?.name);
// Try to iterate over the session object // Try to iterate over the session object
if (result.session) { if (result.session) {
console.log('Session properties:');
for (const key in result.session) { for (const key in result.session) {
console.log(` ${key}:`, result.session[key]);
} }
// Check if session has methods // Check if session has methods
const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(result.session)); const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(result.session));
console.log('Session methods:', methods);
} }
// BrowserOAuthClient might return a Session object that needs to be used with the agent // BrowserOAuthClient might return a Session object that needs to be used with the agent
@@ -83,36 +77,36 @@ class AtprotoOAuthService {
if (result.session) { if (result.session) {
// Process the session to extract DID and handle // Process the session to extract DID and handle
const sessionData = await this.processSession(result.session); const sessionData = await this.processSession(result.session);
console.log('Session processed during initialization:', sessionData);
} }
} else { } else {
console.log('No existing session found');
} }
} catch (error) { } catch (error) {
console.error('Failed to initialize OAuth client:', error);
this.initializePromise = null; // Reset on error to allow retry this.initializePromise = null; // Reset on error to allow retry
throw error; throw error;
} }
} }
private async processSession(session: any): Promise<{ did: string; handle: string }> { private async processSession(session: any): Promise<{ did: string; handle: string }> {
console.log('Processing session:', session);
// Log full session structure // Log full session structure
console.log('Session structure:');
console.log('- sub:', session.sub);
console.log('- did:', session.did);
console.log('- handle:', session.handle);
console.log('- iss:', session.iss);
console.log('- aud:', session.aud);
// Check if agent has properties we can access // Check if agent has properties we can access
if (session.agent) { if (session.agent) {
console.log('- agent:', session.agent);
console.log('- agent.did:', session.agent?.did);
console.log('- agent.handle:', session.agent?.handle);
} }
const did = session.sub || session.did; const did = session.sub || session.did;
@@ -121,18 +115,18 @@ class AtprotoOAuthService {
// 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);
console.log('Agent created directly with session');
// Check if agent has session info after creation // Check if agent has session info after creation
console.log('Agent after creation:');
console.log('- agent.did:', this.agent.did);
console.log('- agent.session:', this.agent.session);
if (this.agent.session) { if (this.agent.session) {
console.log('- agent.session.did:', this.agent.session.did);
console.log('- agent.session.handle:', this.agent.session.handle);
} }
} catch (err) { } catch (err) {
console.log('Failed to create Agent with session directly, trying dpopFetch method');
// 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',
@@ -145,7 +139,7 @@ class AtprotoOAuthService {
// If handle is missing, try multiple methods to resolve it // If handle is missing, try multiple methods to resolve it
if (!handle || handle === 'unknown') { if (!handle || handle === 'unknown') {
console.log('Handle not in session, attempting to resolve...');
// Method 1: Try using the agent to get profile // Method 1: Try using the agent to get profile
try { try {
@@ -154,11 +148,11 @@ class AtprotoOAuthService {
if (profile.data.handle) { if (profile.data.handle) {
handle = profile.data.handle; handle = profile.data.handle;
(this as any)._sessionInfo.handle = handle; (this as any)._sessionInfo.handle = handle;
console.log('Successfully resolved handle via getProfile:', handle);
return { did, handle }; return { did, handle };
} }
} catch (err) { } catch (err) {
console.error('getProfile failed:', err);
} }
// Method 2: Try using describeRepo // Method 2: Try using describeRepo
@@ -169,18 +163,20 @@ class AtprotoOAuthService {
if (repoDesc.data.handle) { if (repoDesc.data.handle) {
handle = repoDesc.data.handle; handle = repoDesc.data.handle;
(this as any)._sessionInfo.handle = handle; (this as any)._sessionInfo.handle = handle;
console.log('Got handle from describeRepo:', handle);
return { did, handle }; return { did, handle };
} }
} catch (err) { } catch (err) {
console.error('describeRepo failed:', err);
} }
// Method 3: Hardcoded fallback for known DIDs // Method 3: Fallback for admin DID
if (did === 'did:plc:uqzpqmrjnptsxezjx4xuh2mn') { const adminDid = import.meta.env.VITE_ADMIN_DID;
handle = 'syui.ai'; if (did === adminDid) {
const appHost = import.meta.env.VITE_APP_HOST || 'https://syui.ai';
handle = new URL(appHost).hostname;
(this as any)._sessionInfo.handle = handle; (this as any)._sessionInfo.handle = handle;
console.log('Using hardcoded handle for known DID');
} }
} }
@@ -191,7 +187,7 @@ class AtprotoOAuthService {
// Use environment variable if available // Use environment variable if available
const envClientId = import.meta.env.VITE_OAUTH_CLIENT_ID; const envClientId = import.meta.env.VITE_OAUTH_CLIENT_ID;
if (envClientId) { if (envClientId) {
console.log('Using client ID from environment:', envClientId);
return envClientId; return envClientId;
} }
@@ -200,7 +196,7 @@ class AtprotoOAuthService {
// For localhost development, use undefined for loopback client // For localhost development, use undefined for loopback client
// The BrowserOAuthClient will handle this automatically // The BrowserOAuthClient will handle this automatically
if (origin.includes('localhost') || origin.includes('127.0.0.1')) { if (origin.includes('localhost') || origin.includes('127.0.0.1')) {
console.log('Using loopback client for localhost development');
return undefined as any; // Loopback client return undefined as any; // Loopback client
} }
@@ -209,7 +205,7 @@ class AtprotoOAuthService {
} }
private detectPDSFromHandle(handle: string): string { private detectPDSFromHandle(handle: string): string {
console.log('Detecting PDS for handle:', handle);
// Supported PDS hosts and their corresponding handles // Supported PDS hosts and their corresponding handles
const pdsMapping = { const pdsMapping = {
@@ -220,22 +216,22 @@ class AtprotoOAuthService {
// Check if handle ends with known PDS domains // Check if handle ends with known PDS domains
for (const [domain, pdsUrl] of Object.entries(pdsMapping)) { for (const [domain, pdsUrl] of Object.entries(pdsMapping)) {
if (handle.endsWith(`.${domain}`)) { if (handle.endsWith(`.${domain}`)) {
console.log(`Handle ${handle} mapped to PDS: ${pdsUrl}`);
return pdsUrl; return pdsUrl;
} }
} }
// Default to bsky.social // Default to bsky.social
console.log(`Handle ${handle} using default PDS: https://bsky.social`);
return 'https://bsky.social'; return 'https://bsky.social';
} }
async initiateOAuthFlow(handle?: string): Promise<void> { async initiateOAuthFlow(handle?: string): Promise<void> {
try { try {
console.log('=== INITIATING OAUTH FLOW ===');
if (!this.oauthClient) { if (!this.oauthClient) {
console.log('OAuth client not initialized, initializing now...');
await this.initialize(); await this.initialize();
} }
@@ -251,15 +247,15 @@ class AtprotoOAuthService {
} }
} }
console.log('Starting OAuth flow for handle:', handle);
// Detect PDS based on handle // Detect PDS based on handle
const pdsUrl = this.detectPDSFromHandle(handle); const pdsUrl = this.detectPDSFromHandle(handle);
console.log('Detected PDS for handle:', { handle, pdsUrl });
// Re-initialize OAuth client with correct PDS if needed // Re-initialize OAuth client with correct PDS if needed
if (pdsUrl !== 'https://bsky.social') { if (pdsUrl !== 'https://bsky.social') {
console.log('Re-initializing OAuth client for custom PDS:', pdsUrl);
this.oauthClient = await BrowserOAuthClient.load({ this.oauthClient = await BrowserOAuthClient.load({
clientId: this.getClientId(), clientId: this.getClientId(),
handleResolver: pdsUrl, handleResolver: pdsUrl,
@@ -267,20 +263,14 @@ class AtprotoOAuthService {
} }
// Start OAuth authorization flow // Start OAuth authorization flow
console.log('Calling oauthClient.authorize with handle:', handle);
try { try {
const authUrl = await this.oauthClient.authorize(handle, { const authUrl = await this.oauthClient.authorize(handle, {
scope: 'atproto transition:generic', scope: 'atproto transition:generic',
}); });
console.log('Authorization URL generated:', authUrl.toString());
console.log('URL breakdown:', {
protocol: authUrl.protocol,
hostname: authUrl.hostname,
pathname: authUrl.pathname,
search: authUrl.search
});
// Store some debug info before redirect // Store some debug info before redirect
sessionStorage.setItem('oauth_debug_pre_redirect', JSON.stringify({ sessionStorage.setItem('oauth_debug_pre_redirect', JSON.stringify({
@@ -291,35 +281,30 @@ class AtprotoOAuthService {
})); }));
// Redirect to authorization server // Redirect to authorization server
console.log('About to redirect to:', authUrl.toString());
window.location.href = authUrl.toString(); window.location.href = authUrl.toString();
} catch (authorizeError) { } catch (authorizeError) {
console.error('oauthClient.authorize failed:', authorizeError);
console.error('Error details:', {
name: authorizeError.name,
message: authorizeError.message,
stack: authorizeError.stack
});
throw authorizeError; throw authorizeError;
} }
} catch (error) { } catch (error) {
console.error('Failed to initiate OAuth flow:', error);
throw new Error(`OAuth認証の開始に失敗しました: ${error}`); throw new Error(`OAuth認証の開始に失敗しました: ${error}`);
} }
} }
async handleOAuthCallback(): Promise<{ did: string; handle: string } | null> { async handleOAuthCallback(): Promise<{ did: string; handle: string } | null> {
try { try {
console.log('=== HANDLING OAUTH CALLBACK ===');
console.log('Current URL:', window.location.href);
console.log('URL hash:', window.location.hash);
console.log('URL search:', window.location.search);
// BrowserOAuthClient should automatically handle the callback // BrowserOAuthClient should automatically handle the callback
// We just need to initialize it and it will process the current URL // We just need to initialize it and it will process the current URL
if (!this.oauthClient) { if (!this.oauthClient) {
console.log('OAuth client not initialized, initializing now...');
await this.initialize(); await this.initialize();
} }
@@ -327,11 +312,11 @@ class AtprotoOAuthService {
throw new Error('Failed to initialize OAuth client'); throw new Error('Failed to initialize OAuth client');
} }
console.log('OAuth client ready, initializing to process callback...');
// Call init() again to process the callback URL // Call init() again to process the callback URL
const result = await this.oauthClient.init(); const result = await this.oauthClient.init();
console.log('OAuth callback processing result:', result);
if (result?.session) { if (result?.session) {
// Process the session // Process the session
@@ -339,47 +324,42 @@ class AtprotoOAuthService {
} }
// If no session yet, wait a bit and try again // If no session yet, wait a bit and try again
console.log('No session found immediately, waiting...');
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise(resolve => setTimeout(resolve, 1000));
// Try to check session again // Try to check session again
const sessionCheck = await this.checkSession(); const sessionCheck = await this.checkSession();
if (sessionCheck) { if (sessionCheck) {
console.log('Session found after delay:', sessionCheck);
return sessionCheck; return sessionCheck;
} }
console.warn('OAuth callback completed but no session was created');
return null; return null;
} catch (error) { } catch (error) {
console.error('OAuth callback handling failed:', error);
console.error('Error details:', {
name: error.name,
message: error.message,
stack: error.stack
});
throw new Error(`OAuth認証の完了に失敗しました: ${error.message}`); throw new Error(`OAuth認証の完了に失敗しました: ${error.message}`);
} }
} }
async checkSession(): Promise<{ did: string; handle: string } | null> { async checkSession(): Promise<{ did: string; handle: string } | null> {
try { try {
console.log('=== CHECK SESSION CALLED ===');
if (!this.oauthClient) { if (!this.oauthClient) {
console.log('No OAuth client, initializing...');
await this.initialize(); await this.initialize();
} }
if (!this.oauthClient) { if (!this.oauthClient) {
console.log('OAuth client initialization failed');
return null; return null;
} }
console.log('Running oauthClient.init() to check session...');
const result = await this.oauthClient.init(); const result = await this.oauthClient.init();
console.log('oauthClient.init() result:', result);
if (result?.session) { if (result?.session) {
// Use the common session processing method // Use the common session processing method
@@ -388,7 +368,7 @@ class AtprotoOAuthService {
return null; return null;
} catch (error) { } catch (error) {
console.error('Session check failed:', error);
return null; return null;
} }
} }
@@ -398,13 +378,7 @@ class AtprotoOAuthService {
} }
getSession(): AtprotoSession | null { getSession(): AtprotoSession | null {
console.log('getSession called');
console.log('Current state:', {
hasAgent: !!this.agent,
hasAgentSession: !!this.agent?.session,
hasOAuthClient: !!this.oauthClient,
hasSessionInfo: !!(this as any)._sessionInfo
});
// First check if we have an agent with session // First check if we have an agent with session
if (this.agent?.session) { if (this.agent?.session) {
@@ -414,7 +388,7 @@ class AtprotoOAuthService {
accessJwt: this.agent.session.accessJwt || '', accessJwt: this.agent.session.accessJwt || '',
refreshJwt: this.agent.session.refreshJwt || '', refreshJwt: this.agent.session.refreshJwt || '',
}; };
console.log('Returning agent session:', session);
return session; return session;
} }
@@ -426,11 +400,11 @@ class AtprotoOAuthService {
accessJwt: 'dpop-protected', // Indicate that tokens are handled by dpopFetch accessJwt: 'dpop-protected', // Indicate that tokens are handled by dpopFetch
refreshJwt: 'dpop-protected', refreshJwt: 'dpop-protected',
}; };
console.log('Returning stored session info:', session);
return session; return session;
} }
console.log('No session available');
return null; return null;
} }
@@ -450,28 +424,28 @@ class AtprotoOAuthService {
async logout(): Promise<void> { async logout(): Promise<void> {
try { try {
console.log('=== LOGGING OUT ===');
// Clear Agent // Clear Agent
this.agent = null; this.agent = null;
console.log('Agent cleared');
// Clear BrowserOAuthClient session // Clear BrowserOAuthClient session
if (this.oauthClient) { if (this.oauthClient) {
console.log('Clearing OAuth client session...');
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();
console.log('OAuth client signed out');
} 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();
console.log('OAuth client revoked');
} else { } else {
console.log('No explicit signOut method found on OAuth client');
} }
} catch (oauthError) { } catch (oauthError) {
console.error('OAuth client logout error:', oauthError);
} }
// Reset the OAuth client to force re-initialization // Reset the OAuth client to force re-initialization
@@ -492,11 +466,11 @@ class AtprotoOAuthService {
} }
} }
keysToRemove.forEach(key => { keysToRemove.forEach(key => {
console.log('Removing localStorage key:', key);
localStorage.removeItem(key); localStorage.removeItem(key);
}); });
console.log('=== LOGOUT COMPLETED ===');
// Force page reload to ensure clean state // Force page reload to ensure clean state
setTimeout(() => { setTimeout(() => {
@@ -504,7 +478,7 @@ class AtprotoOAuthService {
}, 100); }, 100);
} catch (error) { } catch (error) {
console.error('Logout failed:', error);
} }
} }
@@ -519,8 +493,8 @@ class AtprotoOAuthService {
const did = sessionInfo.did; const did = sessionInfo.did;
try { try {
console.log('Saving cards to atproto collection...');
console.log('Using DID:', did);
// Ensure we have a fresh agent // Ensure we have a fresh agent
if (!this.agent) { if (!this.agent) {
@@ -550,13 +524,6 @@ class AtprotoOAuthService {
createdAt: createdAt createdAt: createdAt
}; };
console.log('PutRecord request:', {
repo: did,
collection: collection,
rkey: rkey,
record: record
});
// Use Agent's com.atproto.repo.putRecord method // Use Agent's com.atproto.repo.putRecord method
const response = await this.agent.com.atproto.repo.putRecord({ const response = await this.agent.com.atproto.repo.putRecord({
@@ -566,9 +533,9 @@ class AtprotoOAuthService {
record: record record: record
}); });
console.log('カードデータをai.card.boxに保存しました:', response);
} catch (error) { } catch (error) {
console.error('カードボックス保存エラー:', error);
throw error; throw error;
} }
} }
@@ -584,8 +551,8 @@ class AtprotoOAuthService {
const did = sessionInfo.did; const did = sessionInfo.did;
try { try {
console.log('Fetching cards from atproto collection...');
console.log('Using DID:', did);
// Ensure we have a fresh agent // Ensure we have a fresh agent
if (!this.agent) { if (!this.agent) {
@@ -598,7 +565,7 @@ class AtprotoOAuthService {
rkey: 'self' rkey: 'self'
}); });
console.log('Cards from box response:', response);
// Convert to expected format // Convert to expected format
const result = { const result = {
@@ -611,7 +578,7 @@ class AtprotoOAuthService {
return result; return result;
} catch (error) { } catch (error) {
console.error('カードボックス取得エラー:', error);
// If record doesn't exist, return empty // If record doesn't exist, return empty
if (error.toString().includes('RecordNotFound')) { if (error.toString().includes('RecordNotFound')) {
@@ -633,8 +600,8 @@ class AtprotoOAuthService {
const did = sessionInfo.did; const did = sessionInfo.did;
try { try {
console.log('Deleting card box collection...');
console.log('Using DID:', did);
// Ensure we have a fresh agent // Ensure we have a fresh agent
if (!this.agent) { if (!this.agent) {
@@ -647,33 +614,35 @@ class AtprotoOAuthService {
rkey: 'self' rkey: 'self'
}); });
console.log('Card box deleted successfully:', response);
} catch (error) { } catch (error) {
console.error('カードボックス削除エラー:', error);
throw error; throw error;
} }
} }
// 手動でトークンを設定(開発・デバッグ用) // 手動でトークンを設定(開発・デバッグ用)
setManualTokens(accessJwt: string, refreshJwt: string): void { setManualTokens(accessJwt: string, refreshJwt: string): void {
console.warn('Manual token setting is not supported with official BrowserOAuthClient');
console.warn('Please use the proper OAuth flow instead');
// For backward compatibility, store in localStorage // For backward compatibility, store in localStorage
const adminDid = import.meta.env.VITE_ADMIN_DID || 'did:plc:unknown';
const appHost = import.meta.env.VITE_APP_HOST || 'https://example.com';
const session: AtprotoSession = { const session: AtprotoSession = {
did: 'did:plc:uqzpqmrjnptsxezjx4xuh2mn', did: adminDid,
handle: 'syui.ai', handle: new URL(appHost).hostname,
accessJwt: accessJwt, accessJwt: accessJwt,
refreshJwt: refreshJwt refreshJwt: refreshJwt
}; };
localStorage.setItem('atproto_session', JSON.stringify(session)); localStorage.setItem('atproto_session', JSON.stringify(session));
console.log('Manual tokens stored in localStorage for backward compatibility');
} }
// 後方互換性のための従来関数 // 後方互換性のための従来関数
saveSessionToStorage(session: AtprotoSession): void { saveSessionToStorage(session: AtprotoSession): void {
console.warn('saveSessionToStorage is deprecated with BrowserOAuthClient');
localStorage.setItem('atproto_session', JSON.stringify(session)); localStorage.setItem('atproto_session', JSON.stringify(session));
} }

View File

@@ -53,7 +53,6 @@ export class OAuthEndpointHandler {
} }
}); });
} catch (error) { } catch (error) {
console.error('Failed to generate JWKS:', error);
return new Response(JSON.stringify({ error: 'Failed to generate JWKS' }), { return new Response(JSON.stringify({ error: 'Failed to generate JWKS' }), {
status: 500, status: 500,
headers: { 'Content-Type': 'application/json' } headers: { 'Content-Type': 'application/json' }
@@ -62,7 +61,6 @@ export class OAuthEndpointHandler {
} }
} catch (e) { } catch (e) {
// If URL parsing fails, pass through to original fetch // If URL parsing fails, pass through to original fetch
console.debug('URL parsing failed, passing through:', e);
} }
// Pass through all other requests // Pass through all other requests
@@ -136,6 +134,5 @@ export function registerOAuthServiceWorker() {
const blob = new Blob([swCode], { type: 'application/javascript' }); const blob = new Blob([swCode], { type: 'application/javascript' });
const swUrl = URL.createObjectURL(blob); const swUrl = URL.createObjectURL(blob);
navigator.serviceWorker.register(swUrl).catch(console.error);
} }
} }

View File

@@ -37,7 +37,6 @@ export class OAuthKeyManager {
this.keyPair = await this.importKeyPair(keyData); this.keyPair = await this.importKeyPair(keyData);
return this.keyPair; return this.keyPair;
} catch (error) { } catch (error) {
console.warn('Failed to load stored key, generating new one:', error);
localStorage.removeItem('oauth_private_key'); localStorage.removeItem('oauth_private_key');
} }
} }
@@ -115,7 +114,6 @@ export class OAuthKeyManager {
const privateKey = await window.crypto.subtle.exportKey('jwk', keyPair.privateKey); const privateKey = await window.crypto.subtle.exportKey('jwk', keyPair.privateKey);
localStorage.setItem('oauth_private_key', JSON.stringify(privateKey)); localStorage.setItem('oauth_private_key', JSON.stringify(privateKey));
} catch (error) { } catch (error) {
console.error('Failed to store private key:', error);
} }
} }

View File

@@ -49,50 +49,17 @@ pub async fn build(project_dir: PathBuf) -> Result<()> {
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.unwrap_or("ai.syui.log"); .unwrap_or("ai.syui.log");
// Extract AI config if present
let ai_config = config.get("ai")
.and_then(|v| v.as_table());
let ai_enabled = ai_config
.and_then(|ai| ai.get("enabled"))
.and_then(|v| v.as_bool())
.unwrap_or(false);
let ai_ask_ai = ai_config
.and_then(|ai| ai.get("ask_ai"))
.and_then(|v| v.as_bool())
.unwrap_or(false);
let ai_provider = ai_config
.and_then(|ai| ai.get("provider"))
.and_then(|v| v.as_str())
.unwrap_or("ollama");
let ai_model = ai_config
.and_then(|ai| ai.get("model"))
.and_then(|v| v.as_str())
.unwrap_or("gemma2:2b");
let ai_host = ai_config
.and_then(|ai| ai.get("host"))
.and_then(|v| v.as_str())
.unwrap_or("https://ollama.syui.ai");
let ai_system_prompt = ai_config
.and_then(|ai| ai.get("system_prompt"))
.and_then(|v| v.as_str())
.unwrap_or("you are a helpful ai assistant");
let ai_did = ai_config
.and_then(|ai| ai.get("ai_did"))
.and_then(|v| v.as_str())
.unwrap_or("did:plc:4hqjfn7m6n5hno3doamuhgef");
// Extract bsky_api from oauth config // Extract bsky_api from oauth config
let bsky_api = oauth_config.get("bsky_api") let bsky_api = oauth_config.get("bsky_api")
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.unwrap_or("https://public.api.bsky.app"); .unwrap_or("https://public.api.bsky.app");
// Extract atproto_api from oauth config
let atproto_api = oauth_config.get("atproto_api")
.and_then(|v| v.as_str())
.unwrap_or("https://bsky.social");
// 4. Create .env.production content // 4. Create .env.production content
let env_content = format!( let env_content = format!(
r#"# Production environment variables r#"# Production environment variables
@@ -101,34 +68,20 @@ VITE_OAUTH_CLIENT_ID={}/{}
VITE_OAUTH_REDIRECT_URI={}/{} VITE_OAUTH_REDIRECT_URI={}/{}
VITE_ADMIN_DID={} VITE_ADMIN_DID={}
# Base collection for OAuth app and ailog (all others are derived) # Base collection (all others are derived via getCollectionNames)
VITE_OAUTH_COLLECTION={} VITE_OAUTH_COLLECTION={}
# AI Configuration
VITE_AI_ENABLED={}
VITE_AI_ASK_AI={}
VITE_AI_PROVIDER={}
VITE_AI_MODEL={}
VITE_AI_HOST={}
VITE_AI_SYSTEM_PROMPT="{}"
VITE_AI_DID={}
# API Configuration # API Configuration
VITE_BSKY_PUBLIC_API={} VITE_BSKY_PUBLIC_API={}
VITE_ATPROTO_API={}
"#, "#,
base_url, base_url,
base_url, client_id_path, base_url, client_id_path,
base_url, redirect_path, base_url, redirect_path,
admin_did, admin_did,
collection_base, collection_base,
ai_enabled, bsky_api,
ai_ask_ai, atproto_api
ai_provider,
ai_model,
ai_host,
ai_system_prompt,
ai_did,
bsky_api
); );
// 5. Find oauth directory (relative to current working directory) // 5. Find oauth directory (relative to current working directory)

View File

@@ -931,12 +931,20 @@ pub async fn test_api() -> Result<()> {
} }
// AI content generation functions // AI content generation functions
async fn generate_ai_content(content: &str, prompt_type: &str, ollama_host: &str) -> Result<String> { async fn generate_ai_content(content: &str, prompt_type: &str, ollama_host: &str, blog_host: &str) -> Result<String> {
let model = "gemma3:4b"; let model = "gemma3:4b";
let system_prompt = "あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。相手のことが大好きで、ときどき甘えたり、照れたりします。でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。";
let prompt = match prompt_type { let prompt = match prompt_type {
"translate" => format!("Translate the following Japanese blog post to English. Keep the technical terms and code blocks intact:\n\n{}", content), "translate" => format!(
"comment" => format!("Read this blog post and provide an insightful comment about it. Focus on the key points and add your perspective:\n\n{}", content), "{}\n\n# 指示\n以下の日本語ブログ記事を英語に翻訳してください。\n- 技術用語やコードブロックはそのまま維持\n- アイらしい表現で翻訳\n- 簡潔に要点をまとめる\n\n# ブログ記事\n{}",
system_prompt, content
),
"comment" => format!(
"{}\n\n# 指示\nこのブログ記事を読んで、アイらしいコメントをしてください。\n- 技術的な内容への感想\n- アイの視点からの面白い発見\n- 短めに、でも内容のあるコメント\n\n# ブログ記事\n{}",
system_prompt, content
),
_ => return Err(anyhow::anyhow!("Unknown prompt type: {}", prompt_type)), _ => return Err(anyhow::anyhow!("Unknown prompt type: {}", prompt_type)),
}; };
@@ -947,11 +955,13 @@ async fn generate_ai_content(content: &str, prompt_type: &str, ollama_host: &str
options: OllamaOptions { options: OllamaOptions {
temperature: 0.9, temperature: 0.9,
top_p: 0.9, top_p: 0.9,
num_predict: 500, num_predict: 300, // Shorter responses for comments
}, },
}; };
let client = reqwest::Client::new(); let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(120)) // 2 minute timeout
.build()?;
// Try localhost first (for same-server deployment) // Try localhost first (for same-server deployment)
let localhost_url = "http://localhost:11434/api/generate"; let localhost_url = "http://localhost:11434/api/generate";
@@ -968,7 +978,13 @@ async fn generate_ai_content(content: &str, prompt_type: &str, ollama_host: &str
// Fallback to remote host // Fallback to remote host
let remote_url = format!("{}/api/generate", ollama_host); let remote_url = format!("{}/api/generate", ollama_host);
let response = client.post(&remote_url).json(&request).send().await?; println!("{}", format!("🔗 Making request to: {} with Origin: {}", remote_url, blog_host).blue());
let response = client
.post(&remote_url)
.header("Origin", blog_host)
.json(&request)
.send()
.await?;
if !response.status().is_success() { if !response.status().is_success() {
return Err(anyhow::anyhow!("Ollama API request failed: {}", response.status())); return Err(anyhow::anyhow!("Ollama API request failed: {}", response.status()));
@@ -1042,25 +1058,49 @@ async fn check_and_process_new_posts(
for post in blog_posts { for post in blog_posts {
let post_slug = extract_slug_from_url(&post.href); let post_slug = extract_slug_from_url(&post.href);
// Check if translation already exists // Check if translation already exists (support both old and new format)
let translation_exists = existing_lang_records.iter().any(|record| { let translation_exists = existing_lang_records.iter().any(|record| {
record.get("value") let value = record.get("value");
// Check new format: value.post.slug
let new_format_match = value
.and_then(|v| v.get("post"))
.and_then(|p| p.get("slug"))
.and_then(|s| s.as_str())
== Some(&post_slug);
// Check old format: value.post_slug
let old_format_match = value
.and_then(|v| v.get("post_slug")) .and_then(|v| v.get("post_slug"))
.and_then(|s| s.as_str()) .and_then(|s| s.as_str())
== Some(&post_slug) == Some(&post_slug);
new_format_match || old_format_match
}); });
// Check if comment already exists // Check if comment already exists (support both old and new format)
let comment_exists = existing_comment_records.iter().any(|record| { let comment_exists = existing_comment_records.iter().any(|record| {
record.get("value") let value = record.get("value");
// Check new format: value.post.slug
let new_format_match = value
.and_then(|v| v.get("post"))
.and_then(|p| p.get("slug"))
.and_then(|s| s.as_str())
== Some(&post_slug);
// Check old format: value.post_slug
let old_format_match = value
.and_then(|v| v.get("post_slug")) .and_then(|v| v.get("post_slug"))
.and_then(|s| s.as_str()) .and_then(|s| s.as_str())
== Some(&post_slug) == Some(&post_slug);
new_format_match || old_format_match
}); });
// Generate translation if not exists // Generate translation if not exists
if !translation_exists { if !translation_exists {
match generate_and_store_translation(client, config, &post, ollama_host, ai_did).await { match generate_and_store_translation(client, config, &post, ollama_host, blog_host, ai_did).await {
Ok(_) => { Ok(_) => {
println!("{}", format!("✅ Generated translation for: {}", post.title).green()); println!("{}", format!("✅ Generated translation for: {}", post.title).green());
processed_count += 1; processed_count += 1;
@@ -1069,11 +1109,13 @@ async fn check_and_process_new_posts(
println!("{}", format!("❌ Failed to generate translation for {}: {}", post.title, e).red()); println!("{}", format!("❌ Failed to generate translation for {}: {}", post.title, e).red());
} }
} }
} else {
println!("{}", format!("⏭️ Translation already exists for: {}", post.title).yellow());
} }
// Generate comment if not exists // Generate comment if not exists
if !comment_exists { if !comment_exists {
match generate_and_store_comment(client, config, &post, ollama_host, ai_did).await { match generate_and_store_comment(client, config, &post, ollama_host, blog_host, ai_did).await {
Ok(_) => { Ok(_) => {
println!("{}", format!("✅ Generated comment for: {}", post.title).green()); println!("{}", format!("✅ Generated comment for: {}", post.title).green());
processed_count += 1; processed_count += 1;
@@ -1082,6 +1124,8 @@ async fn check_and_process_new_posts(
println!("{}", format!("❌ Failed to generate comment for {}: {}", post.title, e).red()); println!("{}", format!("❌ Failed to generate comment for {}: {}", post.title, e).red());
} }
} }
} else {
println!("{}", format!("⏭️ Comment already exists for: {}", post.title).yellow());
} }
} }
@@ -1120,25 +1164,78 @@ fn extract_slug_from_url(url: &str) -> String {
.to_string() .to_string()
} }
fn extract_date_from_slug(slug: &str) -> String {
// Extract date from slug like "2025-06-14-blog" -> "2025-06-14T00:00:00Z"
if slug.len() >= 10 && slug.chars().nth(4) == Some('-') && slug.chars().nth(7) == Some('-') {
format!("{}T00:00:00Z", &slug[0..10])
} else {
chrono::Utc::now().format("%Y-%m-%dT00:00:00Z").to_string()
}
}
async fn get_ai_profile(client: &reqwest::Client, ai_did: &str) -> Result<serde_json::Value> {
let url = format!("https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor={}",
urlencoding::encode(ai_did));
let response = client
.get(&url)
.send()
.await?;
if !response.status().is_success() {
// Fallback to default AI profile
return Ok(serde_json::json!({
"did": ai_did,
"handle": "yui.syui.ai",
"displayName": "ai",
"avatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:4hqjfn7m6n5hno3doamuhgef/bafkreiaxkv624mffw3cfyi67ufxtwuwsy2mjw2ygezsvtd44ycbgkfdo2a@jpeg"
}));
}
let profile_data: serde_json::Value = response.json().await?;
Ok(serde_json::json!({
"did": ai_did,
"handle": profile_data["handle"].as_str().unwrap_or("yui.syui.ai"),
"displayName": profile_data["displayName"].as_str().unwrap_or("ai"),
"avatar": profile_data["avatar"].as_str()
}))
}
async fn generate_and_store_translation( async fn generate_and_store_translation(
client: &reqwest::Client, client: &reqwest::Client,
config: &AuthConfig, config: &AuthConfig,
post: &BlogPost, post: &BlogPost,
ollama_host: &str, ollama_host: &str,
blog_host: &str,
ai_did: &str, ai_did: &str,
) -> Result<()> { ) -> Result<()> {
// Generate translation // Generate translation using post content instead of just title
let translation = generate_ai_content(&post.title, "translate", ollama_host).await?; let content_to_translate = format!("Title: {}\n\n{}", post.title, post.contents);
let translation = generate_ai_content(&content_to_translate, "translate", ollama_host, blog_host).await?;
// Store in ai.syui.log.chat.lang collection // Get AI profile information
let ai_author = get_ai_profile(client, ai_did).await?;
// Extract post metadata
let post_slug = extract_slug_from_url(&post.href);
let post_date = extract_date_from_slug(&post_slug);
// Store in ai.syui.log.chat.lang collection with new format
let record_data = serde_json::json!({ let record_data = serde_json::json!({
"post_slug": extract_slug_from_url(&post.href), "$type": "ai.syui.log.chat.lang",
"post_title": post.title, "post": {
"post_url": post.href, "url": post.href,
"lang": "en", "slug": post_slug,
"content": translation, "title": post.title,
"generated_at": chrono::Utc::now().to_rfc3339(), "date": post_date,
"ai_did": ai_did "tags": post.tags,
"language": "ja"
},
"type": "en",
"text": translation,
"author": ai_author,
"createdAt": chrono::Utc::now().to_rfc3339()
}); });
store_atproto_record(client, config, &config.collections.chat_lang(), &record_data).await store_atproto_record(client, config, &config.collections.chat_lang(), &record_data).await
@@ -1149,19 +1246,35 @@ async fn generate_and_store_comment(
config: &AuthConfig, config: &AuthConfig,
post: &BlogPost, post: &BlogPost,
ollama_host: &str, ollama_host: &str,
blog_host: &str,
ai_did: &str, ai_did: &str,
) -> Result<()> { ) -> Result<()> {
// Generate comment // Generate comment using post content instead of just title
let comment = generate_ai_content(&post.title, "comment", ollama_host).await?; let content_to_comment = format!("Title: {}\n\n{}", post.title, post.contents);
let comment = generate_ai_content(&content_to_comment, "comment", ollama_host, blog_host).await?;
// Store in ai.syui.log.chat.comment collection // Get AI profile information
let ai_author = get_ai_profile(client, ai_did).await?;
// Extract post metadata
let post_slug = extract_slug_from_url(&post.href);
let post_date = extract_date_from_slug(&post_slug);
// Store in ai.syui.log.chat.comment collection with new format
let record_data = serde_json::json!({ let record_data = serde_json::json!({
"post_slug": extract_slug_from_url(&post.href), "$type": "ai.syui.log.chat.comment",
"post_title": post.title, "post": {
"post_url": post.href, "url": post.href,
"content": comment, "slug": post_slug,
"generated_at": chrono::Utc::now().to_rfc3339(), "title": post.title,
"ai_did": ai_did "date": post_date,
"tags": post.tags,
"language": "ja"
},
"type": "info",
"text": comment,
"author": ai_author,
"createdAt": chrono::Utc::now().to_rfc3339()
}); });
store_atproto_record(client, config, &config.collections.chat_comment(), &record_data).await store_atproto_record(client, config, &config.collections.chat_comment(), &record_data).await
@@ -1169,10 +1282,13 @@ async fn generate_and_store_comment(
async fn store_atproto_record( async fn store_atproto_record(
client: &reqwest::Client, client: &reqwest::Client,
config: &AuthConfig, _config: &AuthConfig,
collection: &str, collection: &str,
record_data: &serde_json::Value, record_data: &serde_json::Value,
) -> Result<()> { ) -> Result<()> {
// Always load fresh config to ensure we have valid tokens
let config = load_config_with_refresh().await?;
let url = format!("{}/xrpc/com.atproto.repo.putRecord", config.admin.pds); let url = format!("{}/xrpc/com.atproto.repo.putRecord", config.admin.pds);
let put_request = serde_json::json!({ let put_request = serde_json::json!({