test update json
This commit is contained in:
@@ -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
1
.gitignore
vendored
@@ -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
|
||||||
|
@@ -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">
|
@@ -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">
|
@@ -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
|
||||||
|
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
@@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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サーバーが必要です');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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)
|
||||||
|
@@ -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!({
|
||||||
|
Reference in New Issue
Block a user