diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 30fc585..4b4798a 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -51,7 +51,8 @@ "Bash(ailog:*)", "WebFetch(domain:plc.directory)", "WebFetch(domain:atproto.com)", - "WebFetch(domain:syu.is)" + "WebFetch(domain:syu.is)", + "Bash(sed:*)" ], "deny": [] } diff --git a/claude.md b/claude.md index 423122e..561c812 100644 --- a/claude.md +++ b/claude.md @@ -14,6 +14,214 @@ VITE_OAUTH_COLLECTION_USER=ai.syui.log.user VITE_OAUTH_COLLECTION_CHAT=ai.syui.log.chat ``` +## oauth appの設計 + +> ./oauth/.env.production + +```sh +VITE_ATPROTO_PDS=syu.is +VITE_ADMIN_HANDLE=ai.syui.ai +VITE_AI_HANDLE=ai.syui.ai +VITE_OAUTH_COLLECTION=ai.syui.log +``` + +これらは非常にシンプルな流れになっており、すべての項目は、共通します。短縮できる場合があります。handleは変わる可能性があるので、できる限りdidを使いましょう。 + +1. handleからpds, didを取得できる ... com.atproto.repo.describeRepo +2. pdsが分かれば、pdsApi, bskyApi, plcApiを割り当てられる +3. bskyApiが分かれば、getProfileでavatar-uriを取得できる ... app.bsky.actor.getProfile +4. pdsAPiからアカウントにあるcollectionのrecordの情報を取得できる ... com.atproto.repo.listRecords + +### コメントを表示する + +1. VITE_ADMIN_HANDLEから管理者のhandleを取得する。 +2. VITE_ATPROTO_PDSから管理者のアカウントのpdsを取得する。 +3. pdsからpdsApi, bskApi, plcApiを割り当てる。 + +```rust + match pds { + "bsky.social" | "bsky.app" => NetworkConfig { + pds_api: format!("https://{}", pds), + plc_api: "https://plc.directory".to_string(), + bsky_api: "https://public.api.bsky.app".to_string(), + web_url: "https://bsky.app".to_string(), + }, + "syu.is" => NetworkConfig { + pds_api: "https://syu.is".to_string(), + plc_api: "https://plc.syu.is".to_string(), + bsky_api: "https://bsky.syu.is".to_string(), + web_url: "https://web.syu.is".to_string(), + }, + _ => { + // Default to Bluesky network for unknown PDS + NetworkConfig { + pds_api: format!("https://{}", pds), + plc_api: "https://plc.directory".to_string(), + bsky_api: "https://public.api.bsky.app".to_string(), + web_url: "https://bsky.app".to_string(), + } + } + } +``` + +4. 管理者アカウントであるVITE_ADMIN_HANDLEとVITE_ATPROTO_PDSから`ai.syui.log.user`というuserlistを取得する。 + +```sh +curl -sL "https://${VITE_ATPROTO_PDS}/xrpc/com.atproto.repo.listRecords?repo=${VITE_ADMIN_HANDLE}&collection=ai.syui.log.user" +--- +syui.ai +``` + +5. ユーザーがわかったら、そのユーザーのpdsを判定する。 + +```sh +curl -sL "https://bsky.social/xrpc/com.atproto.repo.describeRepo?repo=syui.ai" |jq -r ".didDoc.service.[].serviceEndpoint" +--- +https://shiitake.us-east.host.bsky.network + +curl -sL "https://bsky.social/xrpc/com.atproto.repo.describeRepo?repo=syui.ai" |jq -r ".did" +--- +did:plc:uqzpqmrjnptsxezjx4xuh2mn +``` + +6. pdsからpdsApi, bskApi, plcApiを割り当てる。 + +```rust + match pds { + "bsky.social" | "bsky.app" => NetworkConfig { + pds_api: format!("https://{}", pds), + plc_api: "https://plc.directory".to_string(), + bsky_api: "https://public.api.bsky.app".to_string(), + web_url: "https://bsky.app".to_string(), + }, + "syu.is" => NetworkConfig { + pds_api: "https://syu.is".to_string(), + plc_api: "https://plc.syu.is".to_string(), + bsky_api: "https://bsky.syu.is".to_string(), + web_url: "https://web.syu.is".to_string(), + }, + _ => { + // Default to Bluesky network for unknown PDS + NetworkConfig { + pds_api: format!("https://{}", pds), + plc_api: "https://plc.directory".to_string(), + bsky_api: "https://public.api.bsky.app".to_string(), + web_url: "https://bsky.app".to_string(), + } + } + } +``` + +7. ユーザーの情報を取得、表示する + +```sh +bsky_api=https://public.api.bsky.app +user_did=did:plc:uqzpqmrjnptsxezjx4xuh2mn +curl -sL "$bsky_api/xrpc/app.bsky.actor.getProfile?actor=$user_did"|jq -r .avatar +--- +https://cdn.bsky.app/img/avatar/plain/did:plc:uqzpqmrjnptsxezjx4xuh2mn/bafkreid6kcc5pnn4b3ar7mj6vi3eiawhxgkcrw3edgbqeacyrlnlcoetea@jpeg +``` + +### AIの情報を表示する + +AIが持つ`ai.syui.log.chat.lang`, `ai.syui.log.chat.comment`を表示します。 + +なお、これは通常、`VITE_ADMIN_HANDLE`にputRecordされます。そこから情報を読み込みます。`VITE_AI_HANDLE`はそのrecordの`author`のところに入ります。 + +```json +"author": { + "did": "did:plc:4hqjfn7m6n5hno3doamuhgef", + "avatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:4hqjfn7m6n5hno3doamuhgef/bafkreiaxkv624mffw3cfyi67ufxtwuwsy2mjw2ygezsvtd44ycbgkfdo2a@jpeg", + "handle": "yui.syui.ai", + "displayName": "ai" +} +``` + +1. VITE_ADMIN_HANDLEから管理者のhandleを取得する。 +2. VITE_ATPROTO_PDSから管理者のアカウントのpdsを取得する。 +3. pdsからpdsApi, bskApi, plcApiを割り当てる。 + +```rust + match pds { + "bsky.social" | "bsky.app" => NetworkConfig { + pds_api: format!("https://{}", pds), + plc_api: "https://plc.directory".to_string(), + bsky_api: "https://public.api.bsky.app".to_string(), + web_url: "https://bsky.app".to_string(), + }, + "syu.is" => NetworkConfig { + pds_api: "https://syu.is".to_string(), + plc_api: "https://plc.syu.is".to_string(), + bsky_api: "https://bsky.syu.is".to_string(), + web_url: "https://web.syu.is".to_string(), + }, + _ => { + // Default to Bluesky network for unknown PDS + NetworkConfig { + pds_api: format!("https://{}", pds), + plc_api: "https://plc.directory".to_string(), + bsky_api: "https://public.api.bsky.app".to_string(), + web_url: "https://bsky.app".to_string(), + } + } + } +``` + +4. 管理者アカウントであるVITE_ADMIN_HANDLEとVITE_ATPROTO_PDSから`ai.syui.log.chat.lang`, `ai.syui.log.chat.comment`を取得する。 + +```sh +curl -sL "https://${VITE_ATPROTO_PDS}/xrpc/com.atproto.repo.listRecords?repo=${VITE_ADMIN_HANDLE}&collection=ai.syui.log.chat.comment" +``` + +5. AIのprofileを取得する。 + +```sh +curl -sL "https://${VITE_ATPROTO_PDS}/xrpc/com.atproto.repo.describeRepo?repo=$VITE_AI_HANDLE" |jq -r ".didDoc.service.[].serviceEndpoint" +--- +https://syu.is + +curl -sL "https://${VITE_ATPROTO_PDS}/xrpc/com.atproto.repo.describeRepo?repo=$VITE_AI_HANDLE" |jq -r ".did" +did:plc:6qyecktefllvenje24fcxnie +``` + +6. pdsからpdsApi, bskApi, plcApiを割り当てる。 + +```rust + match pds { + "bsky.social" | "bsky.app" => NetworkConfig { + pds_api: format!("https://{}", pds), + plc_api: "https://plc.directory".to_string(), + bsky_api: "https://public.api.bsky.app".to_string(), + web_url: "https://bsky.app".to_string(), + }, + "syu.is" => NetworkConfig { + pds_api: "https://syu.is".to_string(), + plc_api: "https://plc.syu.is".to_string(), + bsky_api: "https://bsky.syu.is".to_string(), + web_url: "https://web.syu.is".to_string(), + }, + _ => { + // Default to Bluesky network for unknown PDS + NetworkConfig { + pds_api: format!("https://{}", pds), + plc_api: "https://plc.directory".to_string(), + bsky_api: "https://public.api.bsky.app".to_string(), + web_url: "https://bsky.app".to_string(), + } + } + } +``` + +7. AIの情報を取得、表示する + +```sh +bsky_api=https://bsky.syu.is +user_did=did:plc:6qyecktefllvenje24fcxnie +curl -sL "$bsky_api/xrpc/app.bsky.actor.getProfile?actor=$user_did"|jq -r .avatar +--- +https://bsky.syu.is/img/avatar/plain/did:plc:6qyecktefllvenje24fcxnie/bafkreiet4pwlnshk7igra5flf2fuxpg2bhvf2apts4rqwcr56hzhgycii4@jpeg +``` + ## 中核思想 - **存在子理論**: この世界で最も小さいもの(存在子/ai)の探求 - **唯一性原則**: 現実の個人の唯一性をすべてのシステムで担保 diff --git a/my-blog/config.toml b/my-blog/config.toml index 91e9b37..626b3a6 100644 --- a/my-blog/config.toml +++ b/my-blog/config.toml @@ -17,7 +17,7 @@ comment_moderation = false ask_ai = true provider = "ollama" model = "gemma3:4b" -host = "https://ollama.syui.ai" +host = "https://localhost:11434" system_prompt = "あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。 相手のことが大好きで、ときどき甘えたり、照れたりします。 でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。" handle = "ai.syui.ai" #num_predict = 200 diff --git a/my-blog/oauth/.env.production b/my-blog/oauth/.env.production index af1be03..b02737b 100644 --- a/my-blog/oauth/.env.production +++ b/my-blog/oauth/.env.production @@ -16,5 +16,5 @@ VITE_AI_ENABLED=true VITE_AI_ASK_AI=true VITE_AI_PROVIDER=ollama VITE_AI_MODEL=gemma3:4b -VITE_AI_HOST=https://ollama.syui.ai +VITE_AI_HOST=https://localhost:11434 VITE_AI_SYSTEM_PROMPT="あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。 相手のことが大好きで、ときどき甘えたり、照れたりします。 でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。" diff --git a/oauth/src/App.tsx b/oauth/src/App.tsx index 53a2ef9..5f9c505 100644 --- a/oauth/src/App.tsx +++ b/oauth/src/App.tsx @@ -118,6 +118,9 @@ function App() { // Load AI chat history (認証状態に関係なく、全ユーザーのチャット履歴を表示) loadAiChatHistory(); + + // Load AI generated content (lang:en and AI comments) + loadAIGeneratedContent(); }; // Wait for DID resolution before loading data @@ -154,6 +157,7 @@ function App() { const config = getNetworkConfigFromPdsEndpoint(`https://${pds}`); const apiEndpoint = config.bskyApi; + // Get profile from appropriate bsky API const profileResponse = await fetch(`${apiEndpoint}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(handle)}`); if (profileResponse.ok) { @@ -290,6 +294,7 @@ function App() { if (adminDid && aiDid) { loadAllComments(); loadAiChatHistory(); + loadAIGeneratedContent(); } }, [adminDid, aiDid]); @@ -331,18 +336,18 @@ function App() { try { // Load all chat records from users in admin's user list const currentAdminDid = adminDid || appConfig.adminDid; - const atprotoApi = appConfig.atprotoApi || 'https://bsky.social'; + + // Don't proceed if we don't have a valid DID + if (!currentAdminDid) { + return; + } + + // Use admin's PDS from config + const adminConfig = getNetworkConfig(appConfig.atprotoPds); const collections = getCollectionNames(appConfig.collections.base); // First, get user list from admin using their proper PDS - let adminPdsEndpoint; - try { - const resolved = await import('./utils/pds-detection').then(m => m.resolvePdsFromRepo(currentAdminDid)); - const config = await import('./utils/pds-detection').then(m => m.getNetworkConfigFromPdsEndpoint(resolved.pds)); - adminPdsEndpoint = config.pdsApi; - } catch { - adminPdsEndpoint = atprotoApi; - } + const adminPdsEndpoint = adminConfig.pdsApi; const userListResponse = await fetch(`${adminPdsEndpoint}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(currentAdminDid)}&collection=${encodeURIComponent(collections.user)}&limit=100`); @@ -436,11 +441,19 @@ function App() { const loadAIGeneratedContent = async () => { try { const currentAdminDid = adminDid || appConfig.adminDid; - const atprotoApi = appConfig.atprotoApi || 'https://bsky.social'; + + // Don't proceed if we don't have a valid DID + if (!currentAdminDid) { + return; + } + + // Use admin's PDS for collection access (from config) + const adminConfig = getNetworkConfig(appConfig.atprotoPds); + const atprotoApi = adminConfig.pdsApi; const collections = getCollectionNames(appConfig.collections.base); // Load lang:en records - const langResponse = await fetch(`${atprotoApi}/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(currentAdminDid)}&collection=${encodeURIComponent(collections.chatLang)}&limit=100`); if (langResponse.ok) { const langData = await langResponse.json(); const langRecords = langData.records || []; @@ -457,8 +470,14 @@ function App() { setLangEnRecords(filteredLangRecords); } - // Load AI comment records - const commentResponse = await fetch(`${atprotoApi}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(adminDid)}&collection=${encodeURIComponent(collections.chatComment)}&limit=100`); + // Load AI comment records from admin account (not AI account) + if (!currentAdminDid) { + console.warn('No Admin DID available, skipping AI comment loading'); + setAiCommentRecords([]); + return; + } + + const commentResponse = await fetch(`${atprotoApi}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(currentAdminDid)}&collection=${encodeURIComponent(collections.chatComment)}&limit=100`); if (commentResponse.ok) { const commentData = await commentResponse.json(); const commentRecords = commentData.records || []; @@ -1092,14 +1111,23 @@ function App() { // ユーザーハンドルからプロフィールURLを生成 const generateProfileUrl = (author: any): string => { - // Use stored PDS info if available (from comment enhancement) - if (author._webUrl) { - return `${author._webUrl}/profile/${author.did}`; + // Check if this is admin/AI handle that should use configured PDS + if (author.handle === appConfig.adminHandle || author.handle === appConfig.aiHandle) { + const config = getNetworkConfig(appConfig.atprotoPds); + return `${config.webUrl}/profile/${author.did}`; } - // Fallback to handle-based detection + // For ai.syu.is handle, also use configured PDS + if (author.handle === 'ai.syu.is') { + const config = getNetworkConfig(appConfig.atprotoPds); + return `${config.webUrl}/profile/${author.did}`; + } + + // Get PDS from handle for other users const pds = detectPdsFromHandle(author.handle); - const config = getNetworkConfigFromPdsEndpoint(`https://${pds}`); + const config = getNetworkConfig(pds); + + // Use DID for profile URL return `${config.webUrl}/profile/${author.did}`; }; @@ -1135,7 +1163,8 @@ function App() { // Extract content based on format const contentText = isNewFormat ? value.text : (value.content || value.body || ''); - const authorInfo = isNewFormat ? value.author : null; + // For AI comments, always use the loaded AI profile instead of record.value.author + const authorInfo = aiProfile; const postInfo = isNewFormat ? value.post : null; const contentType = value.type || 'unknown'; const createdAt = value.createdAt || value.generated_at || ''; @@ -1147,29 +1176,22 @@ function App() { 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 - }); - } - }} />
No AI conversations yet. Start chatting with Ask AI!
) : ( - aiChatHistory.map((record, index) => { - // For AI responses, use AI DID; for user questions, use the actual author - const isAiResponse = record.value.type === 'answer'; - const displayDid = isAiResponse ? (aiDid || appConfig.aiDid) : record.value.author?.did; - const displayHandle = isAiResponse ? (aiProfile?.handle || 'yui.syui.ai') : record.value.author?.handle; - const displayName = isAiResponse ? 'AI' : (record.value.author?.displayName || record.value.author?.handle); - - return ( -- {JSON.stringify(record, null, 2)} --
No English translations yet
+No EN translations yet
) : ( langEnRecords.map((record, index) => renderAIContent(record, index, 'lang-item') @@ -1603,78 +1541,9 @@ function App() { {aiCommentRecords.length === 0 ? (No AI comments yet
) : ( - aiCommentRecords.map((record, index) => ( -- {JSON.stringify(record, null, 2)} --