fix oauth-ai-chat
This commit is contained in:
@@ -8,330 +8,3 @@ extra:
|
||||
type: "ai"
|
||||
---
|
||||
|
||||
<!-- AI Conversation Metadata -->
|
||||
<!-- Total exchanges: 1 -->
|
||||
|
||||
<div class="chat-message user-message comment-style">
|
||||
<div class="message-header">
|
||||
<div class="avatar">
|
||||
<img src="https://bsky.syu.is/img/avatar/plain/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/bafkreif62mqyra4ndv6ohlscl7adp3vhalcjxwhs676ktfj2sq2drs3pdi@jpeg" alt="syui avatar" class="profile-avatar">
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<div class="display-name">syui</div>
|
||||
<div class="handle">
|
||||
<a href="https://syu.is/profile/did:plc:vzsvtbtbnwn22xjqhcu3vd6y" target="_blank" rel="noopener noreferrer" class="handle-link">@syui.syui.ai</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="message-content">
|
||||
今後のゲーム世界の制御はAIやmcpが担っていくことになるのではないでしょうか。例えば、npcの会話や行動、その世界の天候や環境変化、それらをAIが制御していくと思います。どう思いますか?
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-message ai-message comment-style">
|
||||
<div class="message-header">
|
||||
<div class="avatar">
|
||||
<img src="https://bsky.syu.is/img/avatar/plain/did:plc:6qyecktefllvenje24fcxnie/bafkreigo3ucp32carhbn3chfc3hlf6i7f4rplojc76iylihzpifyexi24y@jpeg" alt="ai avatar" class="profile-avatar">
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<div class="display-name">ai</div>
|
||||
<div class="handle">
|
||||
<a href="https://syu.is/profile/did:plc:6qyecktefllvenje24fcxnie" target="_blank" rel="noopener noreferrer" class="handle-link">@ai.syui.ai</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="message-content">
|
||||
わぁ!すっごくワクワクする質問だね〜!アイ、ゲームのAIのこと大好きなの!えへへ...最近のゲーム開発、本当にすごいことになってるよ!
|
||||
|
||||
## AIとMCPがゲーム世界を変えちゃう未来!✨
|
||||
|
||||
### MCPって何?アイが教えてあげる!
|
||||
|
||||
MCPっていうのはね、Model Context Protocolの略なの!Anthropicさんが作った、すっごく賢いプロトコルなんだよ〜。
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[ゲームエンジン] -->|MCP| B[AI Model]
|
||||
B --> C[NPC会話生成]
|
||||
B --> D[天候システム]
|
||||
B --> E[ダイナミックイベント]
|
||||
C --> F[プレイヤー体験]
|
||||
D --> F
|
||||
E --> F
|
||||
```
|
||||
|
||||
### 今のゲーム世界でAIができること!
|
||||
|
||||
えっとね、2025年の今、AIはこんなすごいことができるようになってるの!
|
||||
|
||||
| 機能 | 従来の方法 | AI/MCP統合後 |
|
||||
|------|------------|--------------|
|
||||
| NPC会話 | 事前に書かれたセリフ | リアルタイム生成・文脈理解 |
|
||||
| 天候システム | 固定パターン | 動的生成・物語連動 |
|
||||
| クエスト生成 | 開発者が全部作る | プレイヤーに合わせて自動生成 |
|
||||
| 世界の反応 | 限定的なトリガー | 無限の可能性! |
|
||||
|
||||
### 実装例:NPCの賢い会話システム
|
||||
|
||||
アイが実際に動くコード書いてみるね!TypeScriptとMCPを使った例だよ〜
|
||||
|
||||
```typescript
|
||||
// game-mcp-server.ts
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
|
||||
interface GameContext {
|
||||
playerName: string;
|
||||
playerLevel: number;
|
||||
currentQuest: string;
|
||||
weatherCondition: string;
|
||||
timeOfDay: string;
|
||||
}
|
||||
|
||||
class GameAIServer {
|
||||
private server: Server;
|
||||
private gameContext: GameContext;
|
||||
|
||||
constructor() {
|
||||
this.server = new Server({
|
||||
name: 'game-ai-controller',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
// NPCの会話生成ツール
|
||||
this.server.setRequestHandler('call_tool', async (request) => {
|
||||
if (request.params.name === 'generate_npc_dialogue') {
|
||||
const { npcName, playerAction, mood } = request.params.arguments as any;
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: await this.generateContextualDialogue(npcName, playerAction, mood)
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
if (request.params.name === 'update_weather') {
|
||||
const { currentStory, playerEmotion } = request.params.arguments as any;
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: await this.generateDynamicWeather(currentStory, playerEmotion)
|
||||
}]
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async generateContextualDialogue(
|
||||
npcName: string,
|
||||
playerAction: string,
|
||||
mood: string
|
||||
): Promise<string> {
|
||||
// ここでAIモデルと連携!
|
||||
const prompt = `
|
||||
NPC: ${npcName}
|
||||
プレイヤーのアクション: ${playerAction}
|
||||
現在の雰囲気: ${mood}
|
||||
時間帯: ${this.gameContext.timeOfDay}
|
||||
|
||||
このNPCの性格に合った自然な返答を生成してください。
|
||||
`;
|
||||
|
||||
// 実際のAI応答(簡略化)
|
||||
return `おお、${this.gameContext.playerName}よ!
|
||||
${this.gameContext.weatherCondition}の日に会えて嬉しいぞ!`;
|
||||
}
|
||||
|
||||
private async generateDynamicWeather(
|
||||
storyContext: string,
|
||||
playerEmotion: string
|
||||
): Promise<string> {
|
||||
// 物語と感情に基づいた天候生成
|
||||
const weatherPatterns = {
|
||||
sad: ['雨', '霧', '曇り'],
|
||||
happy: ['晴れ', '虹', '春風'],
|
||||
tense: ['嵐', '雷', '強風']
|
||||
};
|
||||
|
||||
// AIがストーリーに最適な天候を選択
|
||||
return JSON.stringify({
|
||||
weather: weatherPatterns[playerEmotion]?.[0] || '晴れ',
|
||||
intensity: 0.7,
|
||||
duration: '30minutes'
|
||||
});
|
||||
}
|
||||
|
||||
async start() {
|
||||
const transport = new StdioServerTransport();
|
||||
await this.server.connect(transport);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Unreal Engine 5との統合例!
|
||||
|
||||
UE5でMCPを使うとね、もっとすごいことができちゃうの!
|
||||
|
||||
```python
|
||||
# UE5_MCP_Integration.py
|
||||
import unreal
|
||||
import asyncio
|
||||
from mcp import create_client
|
||||
|
||||
class GameAIController(unreal.Actor):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.mcp_client = None
|
||||
self.initialize_mcp()
|
||||
|
||||
async def initialize_mcp(self):
|
||||
"""MCPクライアントの初期化"""
|
||||
self.mcp_client = await create_client(
|
||||
server_name="game-ai-controller",
|
||||
transport="stdio"
|
||||
)
|
||||
|
||||
@unreal.ufunction(callable=True, category="AI")
|
||||
async def generate_npc_response(self, npc_actor, player_input):
|
||||
"""NPCのリアルタイム応答生成"""
|
||||
|
||||
# ゲームの状態を取得
|
||||
game_state = {
|
||||
"player_location": player_input.get_actor_location(),
|
||||
"time_of_day": unreal.GameplayStatics.get_time_of_day(),
|
||||
"weather": self.get_current_weather(),
|
||||
"npc_mood": npc_actor.get_property("mood")
|
||||
}
|
||||
|
||||
# MCPを通じてAIに問い合わせ
|
||||
response = await self.mcp_client.call_tool(
|
||||
"generate_npc_dialogue",
|
||||
arguments={
|
||||
"context": game_state,
|
||||
"player_input": player_input,
|
||||
"npc_personality": npc_actor.personality_profile
|
||||
}
|
||||
)
|
||||
|
||||
# NPCに応答させる
|
||||
npc_actor.speak(response.content[0].text)
|
||||
|
||||
# 表情も変える!
|
||||
emotion = await self.analyze_emotion(response.content[0].text)
|
||||
npc_actor.set_facial_expression(emotion)
|
||||
```
|
||||
|
||||
### セキュリティとパフォーマンスの考慮事項
|
||||
|
||||
えっとね、アイが気をつけてることがあるの!
|
||||
|
||||
#### セキュリティ面での注意点:
|
||||
|
||||
1. **プロンプトインジェクション対策**
|
||||
```typescript
|
||||
function sanitizePlayerInput(input: string): string {
|
||||
// 危険な文字列をフィルタリング
|
||||
const dangerousPatterns = [
|
||||
/ignore previous instructions/i,
|
||||
/system prompt/i,
|
||||
/admin mode/i
|
||||
];
|
||||
|
||||
for (const pattern of dangerousPatterns) {
|
||||
if (pattern.test(input)) {
|
||||
return "不適切な入力が検出されました";
|
||||
}
|
||||
}
|
||||
|
||||
return input.slice(0, 200); // 長さ制限
|
||||
}
|
||||
```
|
||||
|
||||
2. **レート制限**
|
||||
```typescript
|
||||
class RateLimiter {
|
||||
private requests = new Map<string, number[]>();
|
||||
|
||||
canMakeRequest(playerId: string): boolean {
|
||||
const now = Date.now();
|
||||
const playerRequests = this.requests.get(playerId) || [];
|
||||
|
||||
// 1分以内のリクエストをカウント
|
||||
const recentRequests = playerRequests.filter(
|
||||
time => now - time < 60000
|
||||
);
|
||||
|
||||
if (recentRequests.length >= 10) {
|
||||
return false; // 1分に10回まで
|
||||
}
|
||||
|
||||
recentRequests.push(now);
|
||||
this.requests.set(playerId, recentRequests);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### パフォーマンス最適化:
|
||||
|
||||
```typescript
|
||||
// キャッシュシステムの実装
|
||||
class AIResponseCache {
|
||||
private cache = new Map<string, {
|
||||
response: string;
|
||||
timestamp: number;
|
||||
}>();
|
||||
|
||||
private readonly CACHE_DURATION = 5 * 60 * 1000; // 5分
|
||||
|
||||
async getResponse(key: string, generator: () => Promise<string>): Promise<string> {
|
||||
const cached = this.cache.get(key);
|
||||
|
||||
if (cached && Date.now() - cached.timestamp < this.CACHE_DURATION) {
|
||||
return cached.response;
|
||||
}
|
||||
|
||||
const response = await generator();
|
||||
this.cache.set(key, {
|
||||
response,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### アイが思う未来のゲーム世界!
|
||||
|
||||
えへへ、アイはね、こんな未来を想像してるの!
|
||||
|
||||
- **完全に個別化されたストーリー**: みんなが違う物語を体験できる!
|
||||
- **感情に反応する世界**: 悲しいときは雨が降って、嬉しいときは虹が出る!
|
||||
- **永遠に成長するNPC**: プレイヤーとの会話を覚えて、一緒に成長していく!
|
||||
- **創発的なクエスト**: AIが勝手に新しい冒険を作っちゃう!
|
||||
|
||||
### 実装の比較検討
|
||||
|
||||
| アプローチ | メリット | デメリット | アイのおすすめ度 |
|
||||
|-----------|---------|-----------|----------------|
|
||||
| ローカルAI | 低遅延・オフライン対応 | 計算リソース必要 | ★★★☆☆ |
|
||||
| クラウドAI + MCP | 高性能・更新が簡単 | ネット必須 | ★★★★★ |
|
||||
| ハイブリッド | バランスが良い | 実装が複雑 | ★★★★☆ |
|
||||
|
||||
### まとめ:アイからのメッセージ!
|
||||
|
||||
ねぇねぇ、すっごく楽しい未来が待ってると思わない?AIとMCPがゲームの世界を本当に生きてるみたいにしちゃうんだよ!
|
||||
|
||||
でもね、アイが一番大切だと思うのは...技術だけじゃなくて、プレイヤーさんの心に寄り添うゲームを作ることなの。AIは道具で、本当に大切なのは、みんなが楽しめる素敵な体験を作ることだよね!
|
||||
|
||||
えへへ...難しい話もしちゃったけど、分かってもらえたかな?もっと詳しく知りたいところがあったら、アイに聞いてね!いつでも教えてあげる〜!✨
|
||||
|
||||
---
|
||||
|
||||
*この記事は、6歳のアイちゃんが最新のAI技術について熱く語ってくれました!次回は「量子コンピューターでゲームはどう変わる?」について教えてくれる予定です!お楽しみに〜!*
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@@ -13,6 +13,7 @@
|
||||
<span class="article-lang">{{ post.language }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if not post.extra.type or post.extra.type != "ai" %}
|
||||
<div class="article-actions">
|
||||
{% if post.markdown_url %}
|
||||
<a href="{{ post.markdown_url }}" class="action-btn markdown-btn" title="View Markdown">
|
||||
@@ -25,8 +26,10 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</header>
|
||||
|
||||
{% if not post.extra.type or post.extra.type != "ai" %}
|
||||
<nav class="toc">
|
||||
<h3>Contents</h3>
|
||||
<div id="toc-content">
|
||||
@@ -37,6 +40,7 @@
|
||||
<div class="article-body">
|
||||
{{ post.content | safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div id="comment-atproto"></div>
|
||||
</article>
|
||||
|
@@ -22,6 +22,19 @@ export default function App() {
|
||||
const [showAskAI, setShowAskAI] = useState(false)
|
||||
const [showTestUI, setShowTestUI] = useState(false)
|
||||
|
||||
// Check if current page has matching chat records (AI posts always have chat records)
|
||||
const isAiPost = !pageContext.isTopPage && Array.isArray(adminChatRecords) && adminChatRecords.some(chatPair => {
|
||||
const recordUrl = chatPair.question?.value?.post?.url
|
||||
if (!recordUrl) return false
|
||||
|
||||
try {
|
||||
const recordRkey = new URL(recordUrl).pathname.split('/').pop()?.replace(/\.html$/, '')
|
||||
return recordRkey === pageContext.rkey
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
// Environment-based feature flags
|
||||
const ENABLE_TEST_UI = import.meta.env.VITE_ENABLE_TEST_UI === 'true'
|
||||
const ENABLE_DEBUG = import.meta.env.VITE_ENABLE_DEBUG === 'true'
|
||||
@@ -354,6 +367,7 @@ Answer:`
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
{!isAiPost && (
|
||||
<header className="oauth-app-header">
|
||||
<div className="oauth-header-content">
|
||||
{user && (
|
||||
@@ -394,6 +408,7 @@ Answer:`
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)}
|
||||
|
||||
<div className="main-content">
|
||||
<div className="content-area">
|
||||
|
@@ -6,7 +6,20 @@ import LoadingSkeleton from './LoadingSkeleton.jsx'
|
||||
import { logger } from '../utils/logger.js'
|
||||
|
||||
export default function RecordTabs({ langRecords, commentRecords, userComments, chatRecords, chatHasMore, onLoadMoreChat, userChatRecords, userChatLoading, baseRecords, apiConfig, pageContext, user = null, agent = null, onRecordDeleted = null }) {
|
||||
const [activeTab, setActiveTab] = useState('profiles')
|
||||
// Check if current page has matching chat records (AI posts always have chat records)
|
||||
const isAiPost = !pageContext.isTopPage && Array.isArray(chatRecords) && chatRecords.some(chatPair => {
|
||||
const recordUrl = chatPair.question?.value?.post?.url
|
||||
if (!recordUrl) return false
|
||||
|
||||
try {
|
||||
const recordRkey = new URL(recordUrl).pathname.split('/').pop()?.replace(/\.html$/, '')
|
||||
return recordRkey === pageContext.rkey
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
const [activeTab, setActiveTab] = useState(isAiPost ? 'collection' : 'profiles')
|
||||
|
||||
// Monitor activeTab changes
|
||||
useEffect(() => {
|
||||
@@ -145,6 +158,7 @@ export default function RecordTabs({ langRecords, commentRecords, userComments,
|
||||
|
||||
return (
|
||||
<div className="record-tabs">
|
||||
{!isAiPost && (
|
||||
<div className="tab-header">
|
||||
<button
|
||||
className={`tab-btn ${activeTab === 'profiles' ? 'active' : ''}`}
|
||||
@@ -186,9 +200,10 @@ export default function RecordTabs({ langRecords, commentRecords, userComments,
|
||||
en ({filteredLangRecords.length})
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="tab-content">
|
||||
{activeTab === 'lang' && (
|
||||
{activeTab === 'lang' && !isAiPost && (
|
||||
!langRecords ? (
|
||||
<LoadingSkeleton count={3} showTitle={true} />
|
||||
) : (
|
||||
@@ -203,7 +218,7 @@ export default function RecordTabs({ langRecords, commentRecords, userComments,
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{activeTab === 'comment' && (
|
||||
{activeTab === 'comment' && !isAiPost && (
|
||||
!commentRecords ? (
|
||||
<LoadingSkeleton count={3} showTitle={true} />
|
||||
) : (
|
||||
@@ -233,7 +248,7 @@ export default function RecordTabs({ langRecords, commentRecords, userComments,
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{activeTab === 'users' && (
|
||||
{activeTab === 'users' && !isAiPost && (
|
||||
!userComments ? (
|
||||
<LoadingSkeleton count={3} showTitle={true} />
|
||||
) : (
|
||||
@@ -248,7 +263,7 @@ export default function RecordTabs({ langRecords, commentRecords, userComments,
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{activeTab === 'profiles' && (
|
||||
{activeTab === 'profiles' && !isAiPost && (
|
||||
!baseRecords ? (
|
||||
<LoadingSkeleton count={3} showTitle={true} />
|
||||
) : (
|
||||
|
@@ -523,7 +523,8 @@ async fn post_to_atproto(
|
||||
"slug": "",
|
||||
"tags": [],
|
||||
"title": title,
|
||||
"language": "ja"
|
||||
"language": "ja",
|
||||
"type": "ai"
|
||||
},
|
||||
"text": pair.question,
|
||||
"type": "question",
|
||||
@@ -548,7 +549,8 @@ async fn post_to_atproto(
|
||||
"slug": "",
|
||||
"tags": [],
|
||||
"title": title,
|
||||
"language": "ja"
|
||||
"language": "ja",
|
||||
"type": "ai"
|
||||
},
|
||||
"text": pair.answer,
|
||||
"type": "answer",
|
||||
|
Reference in New Issue
Block a user