test update json
This commit is contained in:
@@ -47,7 +47,8 @@
|
||||
"Bash(git push:*)",
|
||||
"Bash(git tag:*)",
|
||||
"Bash(../bin/ailog:*)",
|
||||
"Bash(../target/release/ailog oauth build:*)"
|
||||
"Bash(../target/release/ailog oauth build:*)",
|
||||
"Bash(ailog:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -12,3 +12,4 @@ node_modules
|
||||
package-lock.json
|
||||
my-blog/static/assets/comment-atproto-*
|
||||
bin/ailog
|
||||
docs
|
||||
|
@@ -1,3 +1,3 @@
|
||||
<!-- 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">
|
@@ -1,3 +1,3 @@
|
||||
<!-- 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">
|
@@ -4,18 +4,9 @@ VITE_OAUTH_CLIENT_ID=https://syui.ai/client-metadata.json
|
||||
VITE_OAUTH_REDIRECT_URI=https://syui.ai/oauth/callback
|
||||
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
|
||||
# [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
|
||||
VITE_BSKY_PUBLIC_API=https://public.api.bsky.app
|
||||
VITE_ATPROTO_API=https://bsky.social
|
||||
|
@@ -7,32 +7,10 @@ import { appConfig, getCollectionNames } from './config/app';
|
||||
import './App.css';
|
||||
|
||||
function App() {
|
||||
console.log('APP COMPONENT LOADED - Console working!');
|
||||
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
|
||||
// Handle OAuth callback detection
|
||||
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}`;
|
||||
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);
|
||||
@@ -59,7 +37,6 @@ function App() {
|
||||
|
||||
const collections = getCollectionNames(appConfig.collections.base);
|
||||
ws.onopen = () => {
|
||||
console.log('Jetstream connected');
|
||||
ws.send(JSON.stringify({
|
||||
wantedCollections: [collections.comment]
|
||||
}));
|
||||
@@ -69,22 +46,20 @@ function App() {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.collection === collections.comment && data.commit?.operation === 'create') {
|
||||
console.log('New comment detected via Jetstream:', data);
|
||||
// Optionally reload comments
|
||||
// loadAllComments(window.location.href);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to parse Jetstream message:', err);
|
||||
// Ignore parsing errors
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (err) => {
|
||||
console.warn('Jetstream error:', err);
|
||||
// Ignore Jetstream errors
|
||||
};
|
||||
|
||||
return ws;
|
||||
} catch (err) {
|
||||
console.warn('Failed to setup Jetstream:', err);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -108,11 +83,11 @@ function App() {
|
||||
|
||||
// キャッシュがなければ、ATProtoから取得(認証状態に関係なく)
|
||||
if (!loadCachedComments()) {
|
||||
console.log('No cached comments found, loading from ATProto...');
|
||||
loadAllComments(); // URLフィルタリングを無効にして全コメント表示
|
||||
} else {
|
||||
console.log('Cached comments loaded successfully');
|
||||
}
|
||||
|
||||
// Load AI chat history (認証状態に関係なく、全ユーザーのチャット履歴を表示)
|
||||
loadAiChatHistory();
|
||||
|
||||
// Handle popstate events for mock OAuth flow
|
||||
const handlePopState = () => {
|
||||
@@ -138,12 +113,9 @@ function App() {
|
||||
// Check existing sessions
|
||||
const checkAuth = async () => {
|
||||
// First check OAuth session using official BrowserOAuthClient
|
||||
console.log('Checking OAuth session...');
|
||||
const oauthResult = await atprotoOAuthService.checkSession();
|
||||
console.log('OAuth checkSession result:', oauthResult);
|
||||
|
||||
if (oauthResult) {
|
||||
console.log('OAuth session found:', oauthResult);
|
||||
// Ensure handle is not DID
|
||||
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)
|
||||
// Temporarily disable URL filtering to see all comments
|
||||
console.log('OAuth session found, loading all comments...');
|
||||
loadAllComments();
|
||||
|
||||
// Load AI chat history
|
||||
loadAiChatHistory(userProfile.did);
|
||||
loadAiChatHistory();
|
||||
|
||||
// Load user list records if admin
|
||||
if (userProfile.did === appConfig.adminDid) {
|
||||
@@ -166,8 +137,6 @@ function App() {
|
||||
|
||||
setIsLoading(false);
|
||||
return;
|
||||
} else {
|
||||
console.log('No OAuth session found');
|
||||
}
|
||||
|
||||
// Fallback to legacy auth
|
||||
@@ -177,7 +146,6 @@ function App() {
|
||||
|
||||
// Load all comments for display (this will be the default view)
|
||||
// Temporarily disable URL filtering to see all comments
|
||||
console.log('Legacy auth session found, loading all comments...');
|
||||
loadAllComments();
|
||||
|
||||
// Load user list records if admin
|
||||
@@ -188,7 +156,6 @@ function App() {
|
||||
setIsLoading(false);
|
||||
|
||||
// 認証状態に関係なく、コメントを読み込む
|
||||
console.log('No auth session found, loading all comments anyway...');
|
||||
loadAllComments();
|
||||
};
|
||||
|
||||
@@ -215,7 +182,7 @@ function App() {
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to get user profile:', error);
|
||||
// Failed to get user profile
|
||||
}
|
||||
|
||||
// Fallback to basic user info
|
||||
@@ -236,27 +203,46 @@ function App() {
|
||||
return `data:image/svg+xml;base64,${btoa(svg)}`;
|
||||
};
|
||||
|
||||
const loadAiChatHistory = async (did: string) => {
|
||||
const loadAiChatHistory = async () => {
|
||||
try {
|
||||
console.log('Loading AI chat history for DID:', did);
|
||||
const agent = atprotoOAuthService.getAgent();
|
||||
if (!agent) {
|
||||
console.log('No agent available');
|
||||
// Load all chat records from admin's user list records
|
||||
const adminDid = appConfig.adminDid;
|
||||
const atprotoApi = appConfig.atprotoApi || 'https://bsky.social';
|
||||
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;
|
||||
}
|
||||
|
||||
// Get AI chat records from current user
|
||||
const response = await agent.api.com.atproto.repo.listRecords({
|
||||
repo: did,
|
||||
collection: appConfig.collections.chat,
|
||||
limit: 100,
|
||||
});
|
||||
|
||||
console.log('AI chat history loaded:', response.data);
|
||||
const chatRecords = response.data.records || [];
|
||||
|
||||
const userListData = await userListResponse.json();
|
||||
const userRecords = userListData.records || [];
|
||||
|
||||
// Extract unique DIDs from user 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)
|
||||
const validRecords = chatRecords.filter(record => {
|
||||
const validRecords = allChatRecords.filter(record => {
|
||||
if (record.value.answer) {
|
||||
// This is an AI answer - check if it has valid AI profile
|
||||
return record.value.author?.handle &&
|
||||
@@ -266,16 +252,13 @@ function App() {
|
||||
return true; // Keep all questions
|
||||
});
|
||||
|
||||
console.log(`Filtered ${chatRecords.length} records to ${validRecords.length} valid records`);
|
||||
|
||||
// Sort by creation time and group question-answer pairs
|
||||
// Sort by creation time
|
||||
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);
|
||||
} catch (err) {
|
||||
console.error('Failed to load AI chat history:', err);
|
||||
setAiChatHistory([]);
|
||||
}
|
||||
};
|
||||
@@ -284,58 +267,64 @@ function App() {
|
||||
const loadAIGeneratedContent = async () => {
|
||||
try {
|
||||
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);
|
||||
|
||||
// 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) {
|
||||
const langData = await langResponse.json();
|
||||
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
|
||||
? 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
|
||||
|
||||
setLangEnRecords(filteredLangRecords);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
const commentData = await commentResponse.json();
|
||||
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
|
||||
? 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
|
||||
|
||||
setAiCommentRecords(filteredCommentRecords);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load AI generated content:', err);
|
||||
// Ignore errors
|
||||
}
|
||||
};
|
||||
|
||||
const loadUserComments = async (did: string) => {
|
||||
try {
|
||||
console.log('Loading comments for DID:', did);
|
||||
const agent = atprotoOAuthService.getAgent();
|
||||
if (!agent) {
|
||||
console.log('No agent available');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get comments from current user
|
||||
const response = await agent.api.com.atproto.repo.listRecords({
|
||||
repo: did,
|
||||
collection: appConfig.collections.comment,
|
||||
collection: getCollectionNames(appConfig.collections.base).comment,
|
||||
limit: 100,
|
||||
});
|
||||
|
||||
console.log('User comments loaded:', response.data);
|
||||
const userComments = response.data.records || [];
|
||||
|
||||
// Enhance comments with profile information if missing
|
||||
@@ -356,7 +345,7 @@ function App() {
|
||||
}
|
||||
};
|
||||
} catch (err) {
|
||||
console.warn('Failed to enhance comment with profile:', err);
|
||||
// Ignore enhancement errors
|
||||
return record;
|
||||
}
|
||||
}
|
||||
@@ -366,7 +355,7 @@ function App() {
|
||||
|
||||
setComments(enhancedComments);
|
||||
} catch (err) {
|
||||
console.error('Failed to load comments:', err);
|
||||
// Ignore load errors
|
||||
setComments([]);
|
||||
}
|
||||
};
|
||||
@@ -376,20 +365,20 @@ function App() {
|
||||
try {
|
||||
// 管理者のユーザーリストを取得
|
||||
const adminDid = appConfig.adminDid;
|
||||
console.log('Fetching user list from admin DID:', adminDid);
|
||||
const response = await fetch(`https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(adminDid)}&collection=${encodeURIComponent(appConfig.collections.user)}&limit=100`);
|
||||
// Fetching user list from admin DID
|
||||
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) {
|
||||
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();
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const userRecords = data.records || [];
|
||||
console.log('User records found:', userRecords.length);
|
||||
// User records found
|
||||
|
||||
if (userRecords.length === 0) {
|
||||
console.log('No user records found, using default users');
|
||||
// No user records found, using default users
|
||||
return getDefaultUsers();
|
||||
}
|
||||
|
||||
@@ -401,13 +390,13 @@ function App() {
|
||||
const resolvedUsers = await Promise.all(
|
||||
record.value.users.map(async (user) => {
|
||||
if (user.did && user.did.includes('-placeholder')) {
|
||||
console.log(`Resolving placeholder DID for ${user.handle}`);
|
||||
// Resolving placeholder DID
|
||||
try {
|
||||
const profileResponse = await fetch(`${appConfig.bskyPublicApi}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(user.handle)}`);
|
||||
if (profileResponse.ok) {
|
||||
const profileData = await profileResponse.json();
|
||||
if (profileData.did) {
|
||||
console.log(`Resolved ${user.handle}: ${user.did} -> ${profileData.did}`);
|
||||
// Resolved DID
|
||||
return {
|
||||
...user,
|
||||
did: profileData.did
|
||||
@@ -415,7 +404,7 @@ function App() {
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`Failed to resolve DID for ${user.handle}:`, err);
|
||||
// Failed to resolve DID
|
||||
}
|
||||
}
|
||||
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;
|
||||
} catch (err) {
|
||||
console.warn('Failed to load users from records, using defaults:', err);
|
||||
// Failed to load users from records, using defaults
|
||||
return getDefaultUsers();
|
||||
}
|
||||
};
|
||||
@@ -436,12 +425,12 @@ function App() {
|
||||
// ユーザーリスト一覧を読み込み
|
||||
const loadUserListRecords = async () => {
|
||||
try {
|
||||
console.log('Loading user list records...');
|
||||
// Loading user list records
|
||||
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) {
|
||||
console.warn('Failed to fetch user list records');
|
||||
// Failed to fetch user list records
|
||||
setUserListRecords([]);
|
||||
return;
|
||||
}
|
||||
@@ -454,10 +443,10 @@ function App() {
|
||||
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);
|
||||
} catch (err) {
|
||||
console.error('Failed to load user list records:', err);
|
||||
// Failed to load user list records
|
||||
setUserListRecords([]);
|
||||
}
|
||||
};
|
||||
@@ -477,39 +466,33 @@ function App() {
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Default users list (including current user):', defaultUsers);
|
||||
// Default users list (including current user)
|
||||
return defaultUsers;
|
||||
};
|
||||
|
||||
// 新しい関数: 全ユーザーからコメントを収集
|
||||
const loadAllComments = async (pageUrl?: string) => {
|
||||
try {
|
||||
console.log('Loading comments from all users...');
|
||||
console.log('Page URL filter:', pageUrl);
|
||||
|
||||
// ユーザーリストを動的に取得
|
||||
const knownUsers = await loadUsersFromRecord();
|
||||
console.log('Known users for comment fetching:', knownUsers);
|
||||
|
||||
const allComments = [];
|
||||
|
||||
// 各ユーザーからコメントを収集
|
||||
for (const user of knownUsers) {
|
||||
try {
|
||||
console.log(`Fetching comments from user: ${user.handle} (${user.did}) at ${user.pds}`);
|
||||
|
||||
// Public API使用(認証不要)
|
||||
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`);
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn(`Failed to fetch from ${user.handle} (${response.status}): ${response.statusText}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const userRecords = data.records || [];
|
||||
console.log(`Found ${userRecords.length} comment records from ${user.handle}`);
|
||||
|
||||
// Flatten comments from new array format
|
||||
const userComments = [];
|
||||
@@ -529,18 +512,24 @@ function App() {
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Flattened to ${userComments.length} individual comments from ${user.handle}`);
|
||||
|
||||
// ページURLでフィルタリング(指定された場合)
|
||||
// ページpathでフィルタリング(指定された場合)
|
||||
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;
|
||||
|
||||
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);
|
||||
} catch (err) {
|
||||
console.warn(`Failed to load comments from ${user.handle}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -572,21 +561,17 @@ function App() {
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to enhance comment with profile:', err);
|
||||
// Ignore enhancement errors
|
||||
}
|
||||
}
|
||||
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);
|
||||
console.log('Comments state updated with', enhancedComments.length, 'comments');
|
||||
|
||||
// キャッシュに保存(5分間有効)
|
||||
if (pageUrl) {
|
||||
@@ -598,7 +583,6 @@ function App() {
|
||||
localStorage.setItem(cacheKey, JSON.stringify(cacheData));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load all comments:', err);
|
||||
setComments([]);
|
||||
}
|
||||
};
|
||||
@@ -640,7 +624,7 @@ function App() {
|
||||
try {
|
||||
const existingResponse = await agent.api.com.atproto.repo.getRecord({
|
||||
repo: user.did,
|
||||
collection: appConfig.collections.comment,
|
||||
collection: getCollectionNames(appConfig.collections.base).comment,
|
||||
rkey: rkey,
|
||||
});
|
||||
|
||||
@@ -659,7 +643,6 @@ function App() {
|
||||
}
|
||||
} catch (err) {
|
||||
// Record doesn't exist yet, that's fine
|
||||
console.log('No existing record found, creating new one');
|
||||
}
|
||||
|
||||
// Add new comment to the array
|
||||
@@ -667,7 +650,7 @@ function App() {
|
||||
|
||||
// Create the record with comments array
|
||||
const record = {
|
||||
$type: appConfig.collections.comment,
|
||||
$type: getCollectionNames(appConfig.collections.base).comment,
|
||||
comments: existingComments,
|
||||
url: window.location.href,
|
||||
createdAt: now.toISOString(), // Latest update time
|
||||
@@ -676,18 +659,16 @@ function App() {
|
||||
// Post to ATProto with rkey
|
||||
const response = await agent.api.com.atproto.repo.putRecord({
|
||||
repo: user.did,
|
||||
collection: appConfig.collections.comment,
|
||||
collection: getCollectionNames(appConfig.collections.base).comment,
|
||||
rkey: rkey,
|
||||
record: record,
|
||||
});
|
||||
|
||||
console.log('Comment posted:', response);
|
||||
|
||||
// Clear form and reload all comments
|
||||
setCommentText('');
|
||||
await loadAllComments(window.location.href);
|
||||
} catch (err: any) {
|
||||
console.error('Failed to post comment:', err);
|
||||
setError('コメントの投稿に失敗しました: ' + err.message);
|
||||
} finally {
|
||||
setIsPosting(false);
|
||||
@@ -714,22 +695,19 @@ function App() {
|
||||
const uriParts = uri.split('/');
|
||||
const rkey = uriParts[uriParts.length - 1];
|
||||
|
||||
console.log('Deleting comment with rkey:', rkey);
|
||||
|
||||
// Delete the record
|
||||
await agent.api.com.atproto.repo.deleteRecord({
|
||||
repo: user.did,
|
||||
collection: appConfig.collections.comment,
|
||||
collection: getCollectionNames(appConfig.collections.base).comment,
|
||||
rkey: rkey,
|
||||
});
|
||||
|
||||
console.log('Comment deleted successfully');
|
||||
|
||||
// Reload all comments to reflect the deletion
|
||||
await loadAllComments(window.location.href);
|
||||
|
||||
} catch (err: any) {
|
||||
console.error('Failed to delete comment:', err);
|
||||
alert('コメントの削除に失敗しました: ' + err.message);
|
||||
}
|
||||
};
|
||||
@@ -787,11 +765,9 @@ function App() {
|
||||
const profileData = await profileResponse.json();
|
||||
if (profileData.did) {
|
||||
resolvedDid = profileData.did;
|
||||
console.log(`Resolved ${handle} -> ${resolvedDid}`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`Failed to resolve DID for ${handle}:`, err);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -806,7 +782,7 @@ function App() {
|
||||
const rkey = now.toISOString().replace(/[:.]/g, '-');
|
||||
|
||||
const record = {
|
||||
$type: appConfig.collections.user,
|
||||
$type: getCollectionNames(appConfig.collections.base).user,
|
||||
users: users,
|
||||
createdAt: now.toISOString(),
|
||||
updatedBy: {
|
||||
@@ -818,19 +794,17 @@ function App() {
|
||||
// Post to ATProto with rkey
|
||||
const response = await agent.api.com.atproto.repo.putRecord({
|
||||
repo: user.did,
|
||||
collection: appConfig.collections.user,
|
||||
collection: getCollectionNames(appConfig.collections.base).user,
|
||||
rkey: rkey,
|
||||
record: record,
|
||||
});
|
||||
|
||||
console.log('User list posted:', response);
|
||||
|
||||
// Clear form and reload user list records
|
||||
setUserListInput('');
|
||||
loadUserListRecords();
|
||||
alert('ユーザーリストが更新されました');
|
||||
} catch (err: any) {
|
||||
console.error('Failed to post user list:', err);
|
||||
setError('ユーザーリストの投稿に失敗しました: ' + err.message);
|
||||
} finally {
|
||||
setIsPostingUserList(false);
|
||||
@@ -858,21 +832,18 @@ function App() {
|
||||
const uriParts = uri.split('/');
|
||||
const rkey = uriParts[uriParts.length - 1];
|
||||
|
||||
console.log('Deleting user list with rkey:', rkey);
|
||||
|
||||
// Delete the record
|
||||
await agent.api.com.atproto.repo.deleteRecord({
|
||||
repo: user.did,
|
||||
collection: appConfig.collections.user,
|
||||
collection: getCollectionNames(appConfig.collections.base).user,
|
||||
rkey: rkey,
|
||||
});
|
||||
|
||||
console.log('User list deleted successfully');
|
||||
loadUserListRecords();
|
||||
alert('ユーザーリストが削除されました');
|
||||
|
||||
} catch (err: any) {
|
||||
console.error('Failed to delete user list:', err);
|
||||
alert('ユーザーリストの削除に失敗しました: ' + err.message);
|
||||
}
|
||||
};
|
||||
@@ -895,7 +866,6 @@ function App() {
|
||||
try {
|
||||
await atprotoOAuthService.initiateOAuthFlow(handleInput);
|
||||
} catch (err) {
|
||||
console.error('OAuth failed:', err);
|
||||
alert('認証の開始に失敗しました。再度お試しください。');
|
||||
}
|
||||
};
|
||||
@@ -918,7 +888,13 @@ function App() {
|
||||
}
|
||||
|
||||
// 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];
|
||||
|
||||
// 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
|
||||
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 (
|
||||
<div className="app">
|
||||
@@ -1081,14 +1175,12 @@ function App() {
|
||||
>
|
||||
Comments ({comments.filter(shouldShowComment).length})
|
||||
</button>
|
||||
{user && (
|
||||
<button
|
||||
className={`tab-button ${activeTab === 'ai-chat' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('ai-chat')}
|
||||
>
|
||||
AI Chat History ({aiChatHistory.length})
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className={`tab-button ${activeTab === 'ai-chat' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('ai-chat')}
|
||||
>
|
||||
AI Chat History ({aiChatHistory.length})
|
||||
</button>
|
||||
<button
|
||||
className={`tab-button ${activeTab === 'lang-en' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('lang-en')}
|
||||
@@ -1129,7 +1221,6 @@ function App() {
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.warn('Failed to fetch fresh avatar:', err);
|
||||
// Keep placeholder on error
|
||||
});
|
||||
}
|
||||
@@ -1196,7 +1287,7 @@ function App() {
|
||||
)}
|
||||
|
||||
{/* AI Chat History List */}
|
||||
{activeTab === 'ai-chat' && user && (
|
||||
{activeTab === 'ai-chat' && (
|
||||
<div className="ai-chat-list">
|
||||
<div className="chat-header">
|
||||
<h3>AI Chat History</h3>
|
||||
@@ -1222,7 +1313,6 @@ function App() {
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.warn('Failed to fetch fresh avatar:', err);
|
||||
// Keep placeholder on error
|
||||
});
|
||||
}
|
||||
@@ -1287,37 +1377,9 @@ function App() {
|
||||
{langEnRecords.length === 0 ? (
|
||||
<p className="no-content">No English translations yet</p>
|
||||
) : (
|
||||
langEnRecords.map((record, index) => (
|
||||
<div key={index} className="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>
|
||||
))
|
||||
langEnRecords.map((record, index) =>
|
||||
renderAIContent(record, index, 'lang-item')
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -1328,37 +1390,9 @@ function App() {
|
||||
{aiCommentRecords.length === 0 ? (
|
||||
<p className="no-content">No AI comments yet</p>
|
||||
) : (
|
||||
aiCommentRecords.map((record, index) => (
|
||||
<div key={index} className="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>
|
||||
))
|
||||
aiCommentRecords.map((record, index) =>
|
||||
renderAIContent(record, index, 'ai-comment-item')
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
@@ -12,17 +12,25 @@ export interface AppConfig {
|
||||
aiModel: string;
|
||||
aiHost: string;
|
||||
bskyPublicApi: string;
|
||||
atprotoApi: string;
|
||||
}
|
||||
|
||||
// Collection name builders (similar to Rust implementation)
|
||||
export function getCollectionNames(base: string) {
|
||||
return {
|
||||
if (!base) {
|
||||
// Fallback to default
|
||||
base = 'ai.syui.log';
|
||||
}
|
||||
|
||||
const collections = {
|
||||
comment: base,
|
||||
user: `${base}.user`,
|
||||
chat: `${base}.chat`,
|
||||
chatLang: `${base}.chat.lang`,
|
||||
chatComment: `${base}.chat.comment`,
|
||||
};
|
||||
|
||||
return collections;
|
||||
}
|
||||
|
||||
// Generate collection names from host
|
||||
@@ -43,9 +51,9 @@ function generateBaseCollectionFromHost(host: string): string {
|
||||
// Reverse the parts for collection naming
|
||||
// log.syui.ai -> ai.syui.log
|
||||
const reversedParts = parts.reverse();
|
||||
return reversedParts.join('.');
|
||||
const result = reversedParts.join('.');
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.warn('Failed to generate collection base from host:', host, error);
|
||||
// Fallback to default
|
||||
return 'ai.syui.log';
|
||||
}
|
||||
@@ -66,8 +74,15 @@ export function getAppConfig(): AppConfig {
|
||||
|
||||
// Priority: Environment variables > Auto-generated from 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 = {
|
||||
base: import.meta.env.VITE_OAUTH_COLLECTION || autoGeneratedBase,
|
||||
base: baseCollection,
|
||||
};
|
||||
|
||||
const rkey = extractRkeyFromUrl();
|
||||
@@ -79,15 +94,8 @@ export function getAppConfig(): AppConfig {
|
||||
const aiModel = import.meta.env.VITE_AI_MODEL || 'gemma2:2b';
|
||||
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 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 {
|
||||
adminDid,
|
||||
@@ -99,7 +107,8 @@ export function getAppConfig(): AppConfig {
|
||||
aiProvider,
|
||||
aiModel,
|
||||
aiHost,
|
||||
bskyPublicApi
|
||||
bskyPublicApi,
|
||||
atprotoApi
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -73,7 +73,6 @@ export const aiCardApi = {
|
||||
});
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
console.warn('ai.gpt AI分析機能が利用できません:', error);
|
||||
throw new Error('AI分析機能を利用するにはai.gptサーバーが必要です');
|
||||
}
|
||||
},
|
||||
@@ -86,7 +85,6 @@ export const aiCardApi = {
|
||||
const response = await aiGptApi.get('/card_get_gacha_stats');
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
console.warn('ai.gpt AI統計機能が利用できません:', error);
|
||||
throw new Error('AI統計機能を利用するにはai.gptサーバーが必要です');
|
||||
}
|
||||
},
|
||||
|
@@ -31,11 +31,11 @@ class AtprotoOAuthService {
|
||||
|
||||
private async _doInitialize(): Promise<void> {
|
||||
try {
|
||||
console.log('=== INITIALIZING ATPROTO OAUTH CLIENT ===');
|
||||
|
||||
|
||||
// Generate client ID based on current origin
|
||||
const clientId = this.getClientId();
|
||||
console.log('Client ID:', clientId);
|
||||
|
||||
|
||||
// Support multiple PDS hosts for OAuth
|
||||
this.oauthClient = await BrowserOAuthClient.load({
|
||||
@@ -43,39 +43,33 @@ class AtprotoOAuthService {
|
||||
handleResolver: 'https://bsky.social', // Default resolver
|
||||
});
|
||||
|
||||
console.log('BrowserOAuthClient initialized successfully with multi-PDS support');
|
||||
|
||||
|
||||
// Try to restore existing session
|
||||
const result = await this.oauthClient.init();
|
||||
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
|
||||
console.log('Creating Agent with session:', result.session);
|
||||
|
||||
|
||||
// Delete the old agent initialization code - we'll create it properly below
|
||||
|
||||
// Set the session after creating the agent
|
||||
// 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
|
||||
if (result.session) {
|
||||
console.log('Session properties:');
|
||||
|
||||
for (const key in result.session) {
|
||||
console.log(` ${key}:`, result.session[key]);
|
||||
|
||||
}
|
||||
|
||||
// Check if session has methods
|
||||
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
|
||||
@@ -83,36 +77,36 @@ class AtprotoOAuthService {
|
||||
if (result.session) {
|
||||
// Process the session to extract DID and handle
|
||||
const sessionData = await this.processSession(result.session);
|
||||
console.log('Session processed during initialization:', sessionData);
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
console.log('No existing session found');
|
||||
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize OAuth client:', error);
|
||||
|
||||
this.initializePromise = null; // Reset on error to allow retry
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async processSession(session: any): Promise<{ did: string; handle: string }> {
|
||||
console.log('Processing session:', session);
|
||||
|
||||
|
||||
// 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
|
||||
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;
|
||||
@@ -121,18 +115,18 @@ class AtprotoOAuthService {
|
||||
// Create Agent directly with session (per official docs)
|
||||
try {
|
||||
this.agent = new Agent(session);
|
||||
console.log('Agent created directly with session');
|
||||
|
||||
|
||||
// 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) {
|
||||
console.log('- agent.session.did:', this.agent.session.did);
|
||||
console.log('- agent.session.handle:', this.agent.session.handle);
|
||||
|
||||
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('Failed to create Agent with session directly, trying dpopFetch method');
|
||||
|
||||
// Fallback to dpopFetch method
|
||||
this.agent = new Agent({
|
||||
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 || handle === 'unknown') {
|
||||
console.log('Handle not in session, attempting to resolve...');
|
||||
|
||||
|
||||
// Method 1: Try using the agent to get profile
|
||||
try {
|
||||
@@ -154,11 +148,11 @@ class AtprotoOAuthService {
|
||||
if (profile.data.handle) {
|
||||
handle = profile.data.handle;
|
||||
(this as any)._sessionInfo.handle = handle;
|
||||
console.log('Successfully resolved handle via getProfile:', handle);
|
||||
|
||||
return { did, handle };
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('getProfile failed:', err);
|
||||
|
||||
}
|
||||
|
||||
// Method 2: Try using describeRepo
|
||||
@@ -169,18 +163,20 @@ class AtprotoOAuthService {
|
||||
if (repoDesc.data.handle) {
|
||||
handle = repoDesc.data.handle;
|
||||
(this as any)._sessionInfo.handle = handle;
|
||||
console.log('Got handle from describeRepo:', handle);
|
||||
|
||||
return { did, handle };
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('describeRepo failed:', err);
|
||||
|
||||
}
|
||||
|
||||
// Method 3: Hardcoded fallback for known DIDs
|
||||
if (did === 'did:plc:uqzpqmrjnptsxezjx4xuh2mn') {
|
||||
handle = 'syui.ai';
|
||||
// Method 3: Fallback for admin DID
|
||||
const adminDid = import.meta.env.VITE_ADMIN_DID;
|
||||
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;
|
||||
console.log('Using hardcoded handle for known DID');
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,7 +187,7 @@ class AtprotoOAuthService {
|
||||
// Use environment variable if available
|
||||
const envClientId = import.meta.env.VITE_OAUTH_CLIENT_ID;
|
||||
if (envClientId) {
|
||||
console.log('Using client ID from environment:', envClientId);
|
||||
|
||||
return envClientId;
|
||||
}
|
||||
|
||||
@@ -200,7 +196,7 @@ class AtprotoOAuthService {
|
||||
// For localhost development, use undefined for loopback client
|
||||
// The BrowserOAuthClient will handle this automatically
|
||||
if (origin.includes('localhost') || origin.includes('127.0.0.1')) {
|
||||
console.log('Using loopback client for localhost development');
|
||||
|
||||
return undefined as any; // Loopback client
|
||||
}
|
||||
|
||||
@@ -209,7 +205,7 @@ class AtprotoOAuthService {
|
||||
}
|
||||
|
||||
private detectPDSFromHandle(handle: string): string {
|
||||
console.log('Detecting PDS for handle:', handle);
|
||||
|
||||
|
||||
// Supported PDS hosts and their corresponding handles
|
||||
const pdsMapping = {
|
||||
@@ -220,22 +216,22 @@ class AtprotoOAuthService {
|
||||
// Check if handle ends with known PDS domains
|
||||
for (const [domain, pdsUrl] of Object.entries(pdsMapping)) {
|
||||
if (handle.endsWith(`.${domain}`)) {
|
||||
console.log(`Handle ${handle} mapped to PDS: ${pdsUrl}`);
|
||||
|
||||
return pdsUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// Default to bsky.social
|
||||
console.log(`Handle ${handle} using default PDS: https://bsky.social`);
|
||||
|
||||
return 'https://bsky.social';
|
||||
}
|
||||
|
||||
async initiateOAuthFlow(handle?: string): Promise<void> {
|
||||
try {
|
||||
console.log('=== INITIATING OAUTH FLOW ===');
|
||||
|
||||
|
||||
if (!this.oauthClient) {
|
||||
console.log('OAuth client not initialized, initializing now...');
|
||||
|
||||
await this.initialize();
|
||||
}
|
||||
|
||||
@@ -251,15 +247,15 @@ class AtprotoOAuthService {
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Starting OAuth flow for handle:', handle);
|
||||
|
||||
|
||||
// Detect PDS based on handle
|
||||
const pdsUrl = this.detectPDSFromHandle(handle);
|
||||
console.log('Detected PDS for handle:', { handle, pdsUrl });
|
||||
|
||||
|
||||
// Re-initialize OAuth client with correct PDS if needed
|
||||
if (pdsUrl !== 'https://bsky.social') {
|
||||
console.log('Re-initializing OAuth client for custom PDS:', pdsUrl);
|
||||
|
||||
this.oauthClient = await BrowserOAuthClient.load({
|
||||
clientId: this.getClientId(),
|
||||
handleResolver: pdsUrl,
|
||||
@@ -267,20 +263,14 @@ class AtprotoOAuthService {
|
||||
}
|
||||
|
||||
// Start OAuth authorization flow
|
||||
console.log('Calling oauthClient.authorize with handle:', handle);
|
||||
|
||||
|
||||
try {
|
||||
const authUrl = await this.oauthClient.authorize(handle, {
|
||||
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
|
||||
sessionStorage.setItem('oauth_debug_pre_redirect', JSON.stringify({
|
||||
@@ -291,35 +281,30 @@ class AtprotoOAuthService {
|
||||
}));
|
||||
|
||||
// Redirect to authorization server
|
||||
console.log('About to redirect to:', authUrl.toString());
|
||||
|
||||
window.location.href = authUrl.toString();
|
||||
} catch (authorizeError) {
|
||||
console.error('oauthClient.authorize failed:', authorizeError);
|
||||
console.error('Error details:', {
|
||||
name: authorizeError.name,
|
||||
message: authorizeError.message,
|
||||
stack: authorizeError.stack
|
||||
});
|
||||
|
||||
throw authorizeError;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to initiate OAuth flow:', error);
|
||||
|
||||
throw new Error(`OAuth認証の開始に失敗しました: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async handleOAuthCallback(): Promise<{ did: string; handle: string } | null> {
|
||||
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
|
||||
// We just need to initialize it and it will process the current URL
|
||||
if (!this.oauthClient) {
|
||||
console.log('OAuth client not initialized, initializing now...');
|
||||
|
||||
await this.initialize();
|
||||
}
|
||||
|
||||
@@ -327,11 +312,11 @@ class AtprotoOAuthService {
|
||||
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
|
||||
const result = await this.oauthClient.init();
|
||||
console.log('OAuth callback processing result:', result);
|
||||
|
||||
|
||||
if (result?.session) {
|
||||
// Process the session
|
||||
@@ -339,47 +324,42 @@ class AtprotoOAuthService {
|
||||
}
|
||||
|
||||
// If no session yet, wait a bit and try again
|
||||
console.log('No session found immediately, waiting...');
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Try to check session again
|
||||
const sessionCheck = await this.checkSession();
|
||||
if (sessionCheck) {
|
||||
console.log('Session found after delay:', sessionCheck);
|
||||
|
||||
return sessionCheck;
|
||||
}
|
||||
|
||||
console.warn('OAuth callback completed but no session was created');
|
||||
|
||||
return null;
|
||||
|
||||
} 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}`);
|
||||
}
|
||||
}
|
||||
|
||||
async checkSession(): Promise<{ did: string; handle: string } | null> {
|
||||
try {
|
||||
console.log('=== CHECK SESSION CALLED ===');
|
||||
|
||||
|
||||
if (!this.oauthClient) {
|
||||
console.log('No OAuth client, initializing...');
|
||||
|
||||
await this.initialize();
|
||||
}
|
||||
|
||||
if (!this.oauthClient) {
|
||||
console.log('OAuth client initialization failed');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('Running oauthClient.init() to check session...');
|
||||
|
||||
const result = await this.oauthClient.init();
|
||||
console.log('oauthClient.init() result:', result);
|
||||
|
||||
|
||||
if (result?.session) {
|
||||
// Use the common session processing method
|
||||
@@ -388,7 +368,7 @@ class AtprotoOAuthService {
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Session check failed:', error);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -398,13 +378,7 @@ class AtprotoOAuthService {
|
||||
}
|
||||
|
||||
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
|
||||
if (this.agent?.session) {
|
||||
@@ -414,7 +388,7 @@ class AtprotoOAuthService {
|
||||
accessJwt: this.agent.session.accessJwt || '',
|
||||
refreshJwt: this.agent.session.refreshJwt || '',
|
||||
};
|
||||
console.log('Returning agent session:', session);
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
@@ -426,11 +400,11 @@ class AtprotoOAuthService {
|
||||
accessJwt: 'dpop-protected', // Indicate that tokens are handled by dpopFetch
|
||||
refreshJwt: 'dpop-protected',
|
||||
};
|
||||
console.log('Returning stored session info:', session);
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
console.log('No session available');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -450,28 +424,28 @@ class AtprotoOAuthService {
|
||||
|
||||
async logout(): Promise<void> {
|
||||
try {
|
||||
console.log('=== LOGGING OUT ===');
|
||||
|
||||
|
||||
// Clear Agent
|
||||
this.agent = null;
|
||||
console.log('Agent cleared');
|
||||
|
||||
|
||||
// Clear BrowserOAuthClient session
|
||||
if (this.oauthClient) {
|
||||
console.log('Clearing OAuth client session...');
|
||||
|
||||
try {
|
||||
// BrowserOAuthClient may have a revoke or signOut method
|
||||
if (typeof (this.oauthClient as any).signOut === 'function') {
|
||||
await (this.oauthClient as any).signOut();
|
||||
console.log('OAuth client signed out');
|
||||
|
||||
} else if (typeof (this.oauthClient as any).revoke === 'function') {
|
||||
await (this.oauthClient as any).revoke();
|
||||
console.log('OAuth client revoked');
|
||||
|
||||
} else {
|
||||
console.log('No explicit signOut method found on OAuth client');
|
||||
|
||||
}
|
||||
} catch (oauthError) {
|
||||
console.error('OAuth client logout error:', oauthError);
|
||||
|
||||
}
|
||||
|
||||
// Reset the OAuth client to force re-initialization
|
||||
@@ -492,11 +466,11 @@ class AtprotoOAuthService {
|
||||
}
|
||||
}
|
||||
keysToRemove.forEach(key => {
|
||||
console.log('Removing localStorage key:', key);
|
||||
|
||||
localStorage.removeItem(key);
|
||||
});
|
||||
|
||||
console.log('=== LOGOUT COMPLETED ===');
|
||||
|
||||
|
||||
// Force page reload to ensure clean state
|
||||
setTimeout(() => {
|
||||
@@ -504,7 +478,7 @@ class AtprotoOAuthService {
|
||||
}, 100);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Logout failed:', error);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -519,8 +493,8 @@ class AtprotoOAuthService {
|
||||
const did = sessionInfo.did;
|
||||
|
||||
try {
|
||||
console.log('Saving cards to atproto collection...');
|
||||
console.log('Using DID:', did);
|
||||
|
||||
|
||||
|
||||
// Ensure we have a fresh agent
|
||||
if (!this.agent) {
|
||||
@@ -550,13 +524,6 @@ class AtprotoOAuthService {
|
||||
createdAt: createdAt
|
||||
};
|
||||
|
||||
console.log('PutRecord request:', {
|
||||
repo: did,
|
||||
collection: collection,
|
||||
rkey: rkey,
|
||||
record: record
|
||||
});
|
||||
|
||||
|
||||
// Use Agent's com.atproto.repo.putRecord method
|
||||
const response = await this.agent.com.atproto.repo.putRecord({
|
||||
@@ -566,9 +533,9 @@ class AtprotoOAuthService {
|
||||
record: record
|
||||
});
|
||||
|
||||
console.log('カードデータをai.card.boxに保存しました:', response);
|
||||
|
||||
} catch (error) {
|
||||
console.error('カードボックス保存エラー:', error);
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -584,8 +551,8 @@ class AtprotoOAuthService {
|
||||
const did = sessionInfo.did;
|
||||
|
||||
try {
|
||||
console.log('Fetching cards from atproto collection...');
|
||||
console.log('Using DID:', did);
|
||||
|
||||
|
||||
|
||||
// Ensure we have a fresh agent
|
||||
if (!this.agent) {
|
||||
@@ -598,7 +565,7 @@ class AtprotoOAuthService {
|
||||
rkey: 'self'
|
||||
});
|
||||
|
||||
console.log('Cards from box response:', response);
|
||||
|
||||
|
||||
// Convert to expected format
|
||||
const result = {
|
||||
@@ -611,7 +578,7 @@ class AtprotoOAuthService {
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('カードボックス取得エラー:', error);
|
||||
|
||||
|
||||
// If record doesn't exist, return empty
|
||||
if (error.toString().includes('RecordNotFound')) {
|
||||
@@ -633,8 +600,8 @@ class AtprotoOAuthService {
|
||||
const did = sessionInfo.did;
|
||||
|
||||
try {
|
||||
console.log('Deleting card box collection...');
|
||||
console.log('Using DID:', did);
|
||||
|
||||
|
||||
|
||||
// Ensure we have a fresh agent
|
||||
if (!this.agent) {
|
||||
@@ -647,33 +614,35 @@ class AtprotoOAuthService {
|
||||
rkey: 'self'
|
||||
});
|
||||
|
||||
console.log('Card box deleted successfully:', response);
|
||||
|
||||
} catch (error) {
|
||||
console.error('カードボックス削除エラー:', error);
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 手動でトークンを設定(開発・デバッグ用)
|
||||
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
|
||||
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 = {
|
||||
did: 'did:plc:uqzpqmrjnptsxezjx4xuh2mn',
|
||||
handle: 'syui.ai',
|
||||
did: adminDid,
|
||||
handle: new URL(appHost).hostname,
|
||||
accessJwt: accessJwt,
|
||||
refreshJwt: refreshJwt
|
||||
};
|
||||
|
||||
localStorage.setItem('atproto_session', JSON.stringify(session));
|
||||
console.log('Manual tokens stored in localStorage for backward compatibility');
|
||||
|
||||
}
|
||||
|
||||
// 後方互換性のための従来関数
|
||||
saveSessionToStorage(session: AtprotoSession): void {
|
||||
console.warn('saveSessionToStorage is deprecated with BrowserOAuthClient');
|
||||
|
||||
localStorage.setItem('atproto_session', JSON.stringify(session));
|
||||
}
|
||||
|
||||
|
@@ -53,7 +53,6 @@ export class OAuthEndpointHandler {
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to generate JWKS:', error);
|
||||
return new Response(JSON.stringify({ error: 'Failed to generate JWKS' }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
@@ -62,7 +61,6 @@ export class OAuthEndpointHandler {
|
||||
}
|
||||
} catch (e) {
|
||||
// If URL parsing fails, pass through to original fetch
|
||||
console.debug('URL parsing failed, passing through:', e);
|
||||
}
|
||||
|
||||
// Pass through all other requests
|
||||
@@ -136,6 +134,5 @@ export function registerOAuthServiceWorker() {
|
||||
const blob = new Blob([swCode], { type: 'application/javascript' });
|
||||
const swUrl = URL.createObjectURL(blob);
|
||||
|
||||
navigator.serviceWorker.register(swUrl).catch(console.error);
|
||||
}
|
||||
}
|
@@ -37,7 +37,6 @@ export class OAuthKeyManager {
|
||||
this.keyPair = await this.importKeyPair(keyData);
|
||||
return this.keyPair;
|
||||
} catch (error) {
|
||||
console.warn('Failed to load stored key, generating new one:', error);
|
||||
localStorage.removeItem('oauth_private_key');
|
||||
}
|
||||
}
|
||||
@@ -115,7 +114,6 @@ export class OAuthKeyManager {
|
||||
const privateKey = await window.crypto.subtle.exportKey('jwk', keyPair.privateKey);
|
||||
localStorage.setItem('oauth_private_key', JSON.stringify(privateKey));
|
||||
} catch (error) {
|
||||
console.error('Failed to store private key:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -49,49 +49,16 @@ pub async fn build(project_dir: PathBuf) -> Result<()> {
|
||||
.and_then(|v| v.as_str())
|
||||
.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
|
||||
let bsky_api = oauth_config.get("bsky_api")
|
||||
.and_then(|v| v.as_str())
|
||||
.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
|
||||
let env_content = format!(
|
||||
@@ -101,34 +68,20 @@ VITE_OAUTH_CLIENT_ID={}/{}
|
||||
VITE_OAUTH_REDIRECT_URI={}/{}
|
||||
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={}
|
||||
|
||||
# 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
|
||||
VITE_BSKY_PUBLIC_API={}
|
||||
VITE_ATPROTO_API={}
|
||||
"#,
|
||||
base_url,
|
||||
base_url, client_id_path,
|
||||
base_url, redirect_path,
|
||||
admin_did,
|
||||
collection_base,
|
||||
ai_enabled,
|
||||
ai_ask_ai,
|
||||
ai_provider,
|
||||
ai_model,
|
||||
ai_host,
|
||||
ai_system_prompt,
|
||||
ai_did,
|
||||
bsky_api
|
||||
bsky_api,
|
||||
atproto_api
|
||||
);
|
||||
|
||||
// 5. Find oauth directory (relative to current working directory)
|
||||
|
@@ -931,12 +931,20 @@ pub async fn test_api() -> Result<()> {
|
||||
}
|
||||
|
||||
// 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 system_prompt = "あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。相手のことが大好きで、ときどき甘えたり、照れたりします。でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。";
|
||||
|
||||
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),
|
||||
"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),
|
||||
"translate" => format!(
|
||||
"{}\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)),
|
||||
};
|
||||
|
||||
@@ -947,11 +955,13 @@ async fn generate_ai_content(content: &str, prompt_type: &str, ollama_host: &str
|
||||
options: OllamaOptions {
|
||||
temperature: 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)
|
||||
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
|
||||
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() {
|
||||
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 {
|
||||
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| {
|
||||
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(|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| {
|
||||
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(|s| s.as_str())
|
||||
== Some(&post_slug)
|
||||
== Some(&post_slug);
|
||||
|
||||
new_format_match || old_format_match
|
||||
});
|
||||
|
||||
// Generate translation if not 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(_) => {
|
||||
println!("{}", format!("✅ Generated translation for: {}", post.title).green());
|
||||
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());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("{}", format!("⏭️ Translation already exists for: {}", post.title).yellow());
|
||||
}
|
||||
|
||||
// Generate comment if not 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(_) => {
|
||||
println!("{}", format!("✅ Generated comment for: {}", post.title).green());
|
||||
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());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("{}", format!("⏭️ Comment already exists for: {}", post.title).yellow());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1120,25 +1164,78 @@ fn extract_slug_from_url(url: &str) -> 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(
|
||||
client: &reqwest::Client,
|
||||
config: &AuthConfig,
|
||||
post: &BlogPost,
|
||||
ollama_host: &str,
|
||||
blog_host: &str,
|
||||
ai_did: &str,
|
||||
) -> Result<()> {
|
||||
// Generate translation
|
||||
let translation = generate_ai_content(&post.title, "translate", ollama_host).await?;
|
||||
// Generate translation using post content instead of just title
|
||||
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!({
|
||||
"post_slug": extract_slug_from_url(&post.href),
|
||||
"post_title": post.title,
|
||||
"post_url": post.href,
|
||||
"lang": "en",
|
||||
"content": translation,
|
||||
"generated_at": chrono::Utc::now().to_rfc3339(),
|
||||
"ai_did": ai_did
|
||||
"$type": "ai.syui.log.chat.lang",
|
||||
"post": {
|
||||
"url": post.href,
|
||||
"slug": post_slug,
|
||||
"title": post.title,
|
||||
"date": post_date,
|
||||
"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
|
||||
@@ -1149,19 +1246,35 @@ async fn generate_and_store_comment(
|
||||
config: &AuthConfig,
|
||||
post: &BlogPost,
|
||||
ollama_host: &str,
|
||||
blog_host: &str,
|
||||
ai_did: &str,
|
||||
) -> Result<()> {
|
||||
// Generate comment
|
||||
let comment = generate_ai_content(&post.title, "comment", ollama_host).await?;
|
||||
// Generate comment using post content instead of just title
|
||||
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!({
|
||||
"post_slug": extract_slug_from_url(&post.href),
|
||||
"post_title": post.title,
|
||||
"post_url": post.href,
|
||||
"content": comment,
|
||||
"generated_at": chrono::Utc::now().to_rfc3339(),
|
||||
"ai_did": ai_did
|
||||
"$type": "ai.syui.log.chat.comment",
|
||||
"post": {
|
||||
"url": post.href,
|
||||
"slug": post_slug,
|
||||
"title": post.title,
|
||||
"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
|
||||
@@ -1169,10 +1282,13 @@ async fn generate_and_store_comment(
|
||||
|
||||
async fn store_atproto_record(
|
||||
client: &reqwest::Client,
|
||||
config: &AuthConfig,
|
||||
_config: &AuthConfig,
|
||||
collection: &str,
|
||||
record_data: &serde_json::Value,
|
||||
) -> 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 put_request = serde_json::json!({
|
||||
|
Reference in New Issue
Block a user