test cleanup

This commit is contained in:
2025-06-17 01:48:30 +09:00
parent 5f0b09b555
commit db04af76ab
7 changed files with 311 additions and 210 deletions

View File

@ -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": []
}

208
claude.md
View File

@ -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の探求
- **唯一性原則**: 現実の個人の唯一性をすべてのシステムで担保

View File

@ -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

View File

@ -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とか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。"

View File

@ -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
});
}
}}
/>
<div className="comment-author-info">
<span className="comment-author">
{authorInfo?.displayName || 'AI'}
</span>
<span className="comment-handle">
@{authorInfo?.handle || aiProfile?.handle || 'yui.syui.ai'}
{authorInfo?.displayName || 'ai'}
</span>
<a
href={generateProfileUrl({
handle: authorInfo?.handle || aiProfile?.handle || appConfig.aiHandle,
did: authorInfo?.did || aiProfile?.did || appConfig.aiDid
})}
target="_blank"
rel="noopener noreferrer"
className="comment-handle"
>
@{authorInfo?.handle || aiProfile?.handle || appConfig.aiHandle}
</a>
</div>
<span className="comment-date">
{new Date(createdAt).toLocaleString()}
@ -1296,7 +1318,7 @@ function App() {
name="userList"
value={userListInput}
onChange={(e) => setUserListInput(e.target.value)}
placeholder="ユーザーハンドルをカンマ区切りで入力&#10;例: syui.ai, yui.syui.ai, user.bsky.social"
placeholder="ユーザーハンドルをカンマ区切りで入力&#10;例: syui.ai, ai.syui.ai, user.bsky.social"
rows={3}
disabled={isPostingUserList}
/>
@ -1493,93 +1515,9 @@ function App() {
{aiChatHistory.length === 0 ? (
<p className="no-chat">No AI conversations yet. Start chatting with Ask AI!</p>
) : (
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 (
<div key={index} className="comment-item">
<div className="comment-header">
<img
src={generatePlaceholderAvatar(displayHandle || 'unknown')}
alt={isAiResponse ? "AI Avatar" : "User Avatar"}
className="comment-avatar"
ref={(img) => {
// Fetch fresh avatar from API when component mounts
if (img && displayDid) {
fetch(`${appConfig.bskyPublicApi}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(displayDid)}`)
.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">
{displayName || 'unknown'}
</span>
<a
href={generateProfileUrl({ handle: displayHandle, did: displayDid })}
target="_blank"
rel="noopener noreferrer"
className="comment-handle"
>
@{displayHandle || 'unknown'}
</a>
</div>
<span className="comment-date">
{new Date(record.value.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>
<button className="chat-type-button">
{record.value.type === 'question' ? 'Question' : 'Answer'}
</button>
</div>
</div>
<div className="comment-meta">
{record.value.post?.url && (
<small><a href={record.value.post.url}>{record.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 className="comment-content">
{record.value.text?.split('\n').map((line: string, index: number) => (
<React.Fragment key={index}>
{line}
{index < record.value.text.split('\n').length - 1 && <br />}
</React.Fragment>
))}
</div>
</div>
);
})
aiChatHistory.map((record, index) =>
renderAIContent(record, index, 'comment-item')
)
)}
</div>
)}
@ -1588,7 +1526,7 @@ function App() {
{activeTab === 'lang-en' && (
<div className="comments-list">
{langEnRecords.length === 0 ? (
<p className="no-content">No English translations yet</p>
<p className="no-content">No EN translations yet</p>
) : (
langEnRecords.map((record, index) =>
renderAIContent(record, index, 'lang-item')
@ -1603,78 +1541,9 @@ function App() {
{aiCommentRecords.length === 0 ? (
<p className="no-content">No AI comments yet</p>
) : (
aiCommentRecords.map((record, index) => (
<div key={index} className="comment-item">
<div className="comment-header">
<img
src={generatePlaceholderAvatar('ai')}
alt="AI Avatar"
className="comment-avatar"
ref={(img) => {
// Fetch AI avatar
const currentAiDid = aiDid || appConfig.aiDid;
if (img && currentAiDid) {
fetch(`${appConfig.bskyPublicApi}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(currentAiDid)}`)
.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">
AI
</span>
<span className="comment-handle">
@{aiProfile?.handle || 'yui.syui.ai'}
</span>
</div>
<span className="comment-date">
{new Date(record.value.createdAt || record.value.generated_at).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>
<div className="comment-meta">
{(record.value.post?.url || record.value.post_url) && (
<small><a href={record.value.post?.url || record.value.post_url}>{record.value.post?.url || record.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 className="comment-content">
{(record.value.text || record.value.comment)?.split('\n').map((line: string, index: number) => (
<React.Fragment key={index}>
{line}
{index < (record.value.text || record.value.comment)?.split('\n').length - 1 && <br />}
</React.Fragment>
))}
</div>
</div>
))
aiCommentRecords.map((record, index) =>
renderAIContent(record, index, 'comment-item')
)
)}
</div>
)}

View File

@ -89,7 +89,7 @@ export function getAppConfig(): AppConfig {
// DIDsはハンドルから実行時に解決されるフォールバック用のみ保持
const adminDid = import.meta.env.VITE_ADMIN_DID || 'did:plc:uqzpqmrjnptsxezjx4xuh2mn';
const aiDid = import.meta.env.VITE_AI_DID || 'did:plc:4hqjfn7m6n5hno3doamuhgef';
const aiDid = import.meta.env.VITE_AI_DID || 'did:plc:6qyecktefllvenje24fcxnie';
const aiDisplayName = import.meta.env.VITE_AI_DISPLAY_NAME || 'ai';
const aiAvatar = import.meta.env.VITE_AI_AVATAR || '';
const aiDescription = import.meta.env.VITE_AI_DESCRIPTION || '';

View File

@ -1441,13 +1441,36 @@ async fn generate_ai_content(content: &str, prompt_type: &str, ai_config: &AiCon
// Fallback to remote host
let remote_url = format!("{}/api/generate", ai_config.ollama_host);
println!("{}", format!("🔗 Making request to: {} with Origin: {}", remote_url, ai_config.blog_host).blue());
let response = client
.post(&remote_url)
.header("Origin", &ai_config.blog_host)
.json(&request)
.send()
.await?;
// Check if this is a local/private network connection (no CORS needed)
// RFC 1918 private networks + localhost
let is_local = ai_config.ollama_host.contains("localhost") ||
ai_config.ollama_host.contains("127.0.0.1") ||
ai_config.ollama_host.contains("::1") ||
ai_config.ollama_host.contains("192.168.") || // 192.168.0.0/16
ai_config.ollama_host.contains("10.") || // 10.0.0.0/8
(ai_config.ollama_host.contains("172.") && { // 172.16.0.0/12
// Extract 172.x and check if x is 16-31
if let Some(start) = ai_config.ollama_host.find("172.") {
let after_172 = &ai_config.ollama_host[start + 4..];
if let Some(dot_pos) = after_172.find('.') {
if let Ok(second_octet) = after_172[..dot_pos].parse::<u8>() {
second_octet >= 16 && second_octet <= 31
} else { false }
} else { false }
} else { false }
});
let mut request_builder = client.post(&remote_url).json(&request);
if !is_local {
println!("{}", format!("🔗 Making request to: {} with Origin: {}", remote_url, ai_config.blog_host).blue());
request_builder = request_builder.header("Origin", &ai_config.blog_host);
} else {
println!("{}", format!("🔗 Making request to local network: {}", remote_url).blue());
}
let response = request_builder.send().await?;
if !response.status().is_success() {
return Err(anyhow::anyhow!("Ollama API request failed: {}", response.status()));