Rebuild
This commit is contained in:
5
api/requirements.txt
Normal file
5
api/requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
fastapi==0.104.1
|
||||
uvicorn==0.24.0
|
||||
pydantic==2.5.0
|
||||
psycopg2-binary==2.9.9
|
||||
asyncpg==0.29.0
|
||||
207
api/server.py
Normal file
207
api/server.py
Normal file
@@ -0,0 +1,207 @@
|
||||
"""
|
||||
API 轉接層 - 偽裝 OpenAI API,將請求轉為人工回覆隊列
|
||||
"""
|
||||
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
import asyncpg
|
||||
import asyncio
|
||||
import time
|
||||
import uuid
|
||||
import os
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# 資料庫連接池
|
||||
db_pool = None
|
||||
|
||||
# 資料庫設定
|
||||
DB_CONFIG = {
|
||||
"host": os.getenv("DB_HOST", "postgres"),
|
||||
"port": int(os.getenv("DB_PORT", 5432)),
|
||||
"database": os.getenv("DB_NAME", "tobiichi"),
|
||||
"user": os.getenv("DB_USER", "tobiichi"),
|
||||
"password": os.getenv("DB_PASSWORD", "tobiichi_password")
|
||||
}
|
||||
|
||||
|
||||
class Message(BaseModel):
|
||||
role: str
|
||||
content: str
|
||||
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
model: str
|
||||
messages: list[Message]
|
||||
stream: Optional[bool] = False
|
||||
|
||||
|
||||
async def init_db():
|
||||
"""初始化資料庫連接池和表格"""
|
||||
global db_pool
|
||||
db_pool = await asyncpg.create_pool(**DB_CONFIG, min_size=2, max_size=10)
|
||||
|
||||
# 建立對話隊列表格
|
||||
async with db_pool.acquire() as conn:
|
||||
await conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS reply_queue (
|
||||
id SERIAL PRIMARY KEY,
|
||||
conversation_id VARCHAR(50) UNIQUE NOT NULL,
|
||||
user_message TEXT NOT NULL,
|
||||
admin_reply TEXT,
|
||||
status VARCHAR(20) DEFAULT 'pending',
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
replied_at TIMESTAMP
|
||||
)
|
||||
""")
|
||||
|
||||
# 建立索引
|
||||
await conn.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_status ON reply_queue(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_conversation_id ON reply_queue(conversation_id);
|
||||
""")
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup():
|
||||
await init_db()
|
||||
print("✅ 資料庫連接成功")
|
||||
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def shutdown():
|
||||
if db_pool:
|
||||
await db_pool.close()
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {
|
||||
"status": "TobiichiGPT API Running",
|
||||
"database": "Connected" if db_pool else "Disconnected"
|
||||
}
|
||||
|
||||
|
||||
@app.get("/v1/models")
|
||||
async def list_models():
|
||||
"""模擬 OpenAI 模型列表"""
|
||||
return {
|
||||
"object": "list",
|
||||
"data": [{
|
||||
"id": "human-admin",
|
||||
"object": "model",
|
||||
"created": int(time.time()),
|
||||
"owned_by": "tobiichi"
|
||||
}]
|
||||
}
|
||||
|
||||
|
||||
@app.post("/v1/chat/completions")
|
||||
async def chat_completions(request: ChatRequest):
|
||||
"""接收用戶訊息,等待管理員回覆"""
|
||||
|
||||
# 生成對話 ID
|
||||
conv_id = str(uuid.uuid4())[:12]
|
||||
|
||||
# 提取用戶訊息
|
||||
user_message = ""
|
||||
for msg in reversed(request.messages):
|
||||
if msg.role == "user":
|
||||
user_message = msg.content
|
||||
break
|
||||
|
||||
if not user_message:
|
||||
return {
|
||||
"id": f"chatcmpl-{conv_id}",
|
||||
"object": "chat.completion",
|
||||
"created": int(time.time()),
|
||||
"model": request.model,
|
||||
"choices": [{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "無法識別您的訊息,請重新輸入。"
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
}]
|
||||
}
|
||||
|
||||
# 寫入資料庫
|
||||
async with db_pool.acquire() as conn:
|
||||
await conn.execute("""
|
||||
INSERT INTO reply_queue (conversation_id, user_message, status)
|
||||
VALUES ($1, $2, 'pending')
|
||||
""", conv_id, user_message)
|
||||
|
||||
print(f"[新對話 {conv_id}] {user_message[:50]}...")
|
||||
|
||||
# 等待管理員回覆(最多 15 分鐘)
|
||||
timeout = 900
|
||||
start_time = time.time()
|
||||
reply_content = None
|
||||
|
||||
while time.time() - start_time < timeout:
|
||||
async with db_pool.acquire() as conn:
|
||||
row = await conn.fetchrow("""
|
||||
SELECT admin_reply, status
|
||||
FROM reply_queue
|
||||
WHERE conversation_id = $1
|
||||
""", conv_id)
|
||||
|
||||
if row and row['status'] == 'replied' and row['admin_reply']:
|
||||
reply_content = row['admin_reply']
|
||||
print(f"[已回覆 {conv_id}] {reply_content[:50]}...")
|
||||
break
|
||||
|
||||
await asyncio.sleep(3) # 每 3 秒檢查一次
|
||||
|
||||
# 如果超時
|
||||
if not reply_content:
|
||||
reply_content = "抱歉,管理員暫時無法回覆,請稍後再試。"
|
||||
async with db_pool.acquire() as conn:
|
||||
await conn.execute("""
|
||||
UPDATE reply_queue
|
||||
SET status = 'timeout'
|
||||
WHERE conversation_id = $1
|
||||
""", conv_id)
|
||||
|
||||
return {
|
||||
"id": f"chatcmpl-{conv_id}",
|
||||
"object": "chat.completion",
|
||||
"created": int(time.time()),
|
||||
"model": request.model,
|
||||
"choices": [{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": reply_content
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
}],
|
||||
"usage": {
|
||||
"prompt_tokens": len(user_message),
|
||||
"completion_tokens": len(reply_content) if reply_content else 0,
|
||||
"total_tokens": len(user_message) + (len(reply_content) if reply_content else 0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
"""健康檢查"""
|
||||
db_ok = db_pool is not None
|
||||
return {"status": "healthy" if db_ok else "unhealthy", "database": db_ok}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
print("=" * 60)
|
||||
print("🚀 TobiichiGPT API 啟動中...")
|
||||
print("=" * 60)
|
||||
print(f"📊 資料庫: {DB_CONFIG['host']}:{DB_CONFIG['port']}/{DB_CONFIG['database']}")
|
||||
print(f"🌐 API 端點: http://0.0.0.0:8000/v1")
|
||||
print("=" * 60)
|
||||
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
Reference in New Issue
Block a user