Update
This commit is contained in:
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
.env
|
||||
*.log
|
||||
__pycache__
|
||||
*.pyc
|
||||
cloudflared-config.yml
|
||||
credentials.json
|
||||
npm-data/
|
||||
open-webui-data/
|
||||
122
QUICKSTART.md
Normal file
122
QUICKSTART.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# TobiichiGPT - 快速設定指南
|
||||
|
||||
## 📦 一鍵啟動
|
||||
|
||||
```powershell
|
||||
# 進入 docker-stack 目錄
|
||||
cd docker-stack
|
||||
|
||||
# 啟動所有服務
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## 🌐 服務清單
|
||||
|
||||
| 服務 | 網址 | 用途 |
|
||||
|------|------|------|
|
||||
| Open WebUI | http://localhost:3000 | 用戶對話介面 |
|
||||
| 管理員後台 | http://localhost:8000/admin | 真人回覆訊息 |
|
||||
| NPM 管理面板 | http://localhost:82 | 反向代理設定 |
|
||||
| Cloudflare Tunnel | 自動連線 | 公開訪問(需設定) |
|
||||
|
||||
## ⚙️ Open WebUI 設定步驟
|
||||
|
||||
1. 開啟 http://localhost:3000
|
||||
2. 進入 **Settings** → **Connections**
|
||||
3. 點擊 **+ Add OpenAI Connection**
|
||||
4. 填入:
|
||||
- **API Base URL**: `http://human-reply-server:8000/v1`
|
||||
- **API Key**: `sk-human` (隨便填)
|
||||
5. 模型列表會出現 **human-admin**
|
||||
|
||||
## 💬 使用流程
|
||||
|
||||
### 用戶端
|
||||
1. 在 Open WebUI 選擇 `human-admin` 模型
|
||||
2. 輸入訊息後送出
|
||||
3. 等待管理員回覆(畫面會轉圈)
|
||||
|
||||
### 管理員端
|
||||
1. 訪問 http://localhost:8000/admin
|
||||
2. 看到新訊息通知
|
||||
3. 在文字框輸入回覆
|
||||
4. 點擊「送出回覆」
|
||||
|
||||
## 🔒 設定 HTTPS
|
||||
|
||||
### 方法 1: 使用 NPM(本機 NPM)
|
||||
|
||||
1. 訪問 http://localhost:82
|
||||
2. 登入(預設帳密:admin@example.com / changeme)
|
||||
3. 首次登入會要求更改密碼
|
||||
4. 新增 Proxy Host:
|
||||
- **Domain Names**: 你的網域 (例如: chat.example.com)
|
||||
- **Forward Hostname**: `open-webui`
|
||||
- **Forward Port**: `8080`
|
||||
- 勾選 **SSL** → 申請 Let's Encrypt 憑證
|
||||
|
||||
### 方法 2: 使用 Cloudflare Tunnel(推薦)
|
||||
|
||||
1. 前往 https://one.dash.cloudflare.com/
|
||||
2. 建立 Tunnel 並複製 Token
|
||||
3. 編輯 `docker-stack/.env`,填入 Token
|
||||
4. 在 Cloudflare 設定 Public Hostname:
|
||||
- `chat.yourdomain.com` → `open-webui:8080`
|
||||
- `admin.yourdomain.com` → `human-reply-server:8000`
|
||||
5. 自動獲得 HTTPS + DDoS 保護
|
||||
|
||||
## 🛠️ 常用指令
|
||||
|
||||
```powershell
|
||||
# 查看服務狀態
|
||||
docker-compose ps
|
||||
|
||||
# 查看日誌
|
||||
docker-compose logs -f
|
||||
|
||||
# 重啟服務
|
||||
docker-compose restart
|
||||
|
||||
# 停止服務
|
||||
docker-compose down
|
||||
|
||||
# 完全移除(包含資料)
|
||||
docker-compose down -v
|
||||
```
|
||||
|
||||
## 🐛 疑難排解
|
||||
|
||||
### Open WebUI 連不到人工回覆伺服器
|
||||
- 確認 API URL 使用 `http://human-reply-server:8000/v1`(容器名稱)
|
||||
- 不要使用 `localhost:8000`
|
||||
|
||||
### 管理員後台打不開
|
||||
- 確認容器是否正常運行:`docker ps`
|
||||
- 查看錯誤日誌:`docker logs tobiichi-gpt`
|
||||
|
||||
### NPM 無法訪問
|
||||
- 確認 port 81 沒有被佔用
|
||||
- Windows 防火牆可能需要開放 port
|
||||
|
||||
## 📝 Port 對應
|
||||
|
||||
### 本機訪問
|
||||
|
||||
| 容器內 Port | 本機 Port | 服務 |
|
||||
|------------|----------|------|
|
||||
| 8080 | 3000 | Open WebUI |
|
||||
| 8000 | 8000 | 人工回覆 API |
|
||||
| 80 | 8080 | 本專案 NPM HTTP |
|
||||
| 443 | 8443 | 本專案 NPM HTTPS |
|
||||
| 81 | 82 | 本專案 NPM 管理 |
|
||||
|
||||
### 與現有 NPM 共存
|
||||
|
||||
| 服務 | Port | 說明 |
|
||||
|------|------|------|
|
||||
| 現有 NPM | 80/443/81 | 原有的 NPM 實例 |
|
||||
| 本專案 NPM | 8080/8443/82 | TobiichiGPT 專用 NPM |
|
||||
|
||||
### 透過 Cloudflare Tunnel
|
||||
|
||||
無需開放 Port,直接使用網域訪問(需設定 `.env` 檔案)
|
||||
251
README.md
251
README.md
@@ -1,2 +1,251 @@
|
||||
# tobiichiGPT
|
||||
# TobiichiGPT - 人工回覆系統
|
||||
|
||||
將 AI 對話系統改造成由真人管理員回覆的極簡方案。
|
||||
|
||||
## 🎯 特色
|
||||
|
||||
- ✅ **零修改** - 不需要修改 Open WebUI 程式碼
|
||||
- ✅ **極簡化** - 單一 Python 檔案即可運行
|
||||
- ✅ **即插即用** - 偽裝成 OpenAI API,直接在 Open WebUI 設定中使用
|
||||
- ✅ **視覺化後台** - 美觀的網頁介面供管理員回覆
|
||||
|
||||
## 🚀 快速開始
|
||||
|
||||
### 方法 1: Docker (推薦)
|
||||
|
||||
```powershell
|
||||
# 進入 docker-stack 目錄
|
||||
cd docker-stack
|
||||
|
||||
# 設定環境變數(首次使用)
|
||||
Copy-Item .env.example .env
|
||||
notepad .env # 填入 Cloudflare Tunnel Token(若要使用)
|
||||
|
||||
# 使用 Docker Compose 啟動
|
||||
docker-compose up -d
|
||||
|
||||
# 查看日誌
|
||||
docker-compose logs -f
|
||||
|
||||
# 停止服務
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
### 方法 2: 直接執行
|
||||
|
||||
```powershell
|
||||
# 1. 進入 docker 目錄
|
||||
cd docker
|
||||
|
||||
# 2. 安裝依賴
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 3. 啟動伺服器
|
||||
python human_reply_server.py
|
||||
```
|
||||
|
||||
啟動後會看到:
|
||||
```
|
||||
🚀 人工回覆伺服器啟動中...
|
||||
📌 管理員後台: http://localhost:8000/admin
|
||||
📌 API 端點: http://localhost:8000/v1
|
||||
```
|
||||
|
||||
## 🌐 服務訪問
|
||||
|
||||
啟動後可訪問以下服務:
|
||||
|
||||
| 服務 | 網址 | 說明 |
|
||||
|------|------|------|
|
||||
| **Open WebUI** | http://localhost:3000 | 用戶對話介面 |
|
||||
| **管理員後台** | http://localhost:8000/admin | 真人回覆訊息 |
|
||||
| **NPM 管理面板** | http://localhost:82 | Nginx Proxy Manager |
|
||||
| **Cloudflare Tunnel** | 自動連線 | 無需手動訪問 |
|
||||
|
||||
預設帳號密碼(NPM):
|
||||
- Email: `admin@example.com`
|
||||
- Password: `changeme`
|
||||
|
||||
### 在 Open WebUI 中設定
|
||||
|
||||
1. 開啟 http://localhost:3000
|
||||
2. 進入 **Settings** → **Connections**
|
||||
3. 點擊 **+ Add OpenAI Connection**
|
||||
4. 填入設定:
|
||||
- **API Base URL**: `http://human-reply-server:8000/v1`
|
||||
- **API Key**: 隨便填 (例如: `sk-human`)
|
||||
5. 儲存後,模型列表會出現 **human-admin**
|
||||
|
||||
### 開始使用
|
||||
|
||||
1. **用戶端**: 在 Open WebUI (http://localhost:3000) 選擇 `human-admin` 模型,發送訊息
|
||||
2. **管理員**: 訪問 http://localhost:8000/admin,查看並回覆訊息
|
||||
3. **自動刷新**: 後台每 5 秒自動刷新,顯示新訊息
|
||||
|
||||
## 📋 運作流程
|
||||
|
||||
```
|
||||
用戶發送訊息
|
||||
↓
|
||||
Open WebUI 調用 API (轉圈圈等待)
|
||||
↓
|
||||
訊息進入待處理隊列
|
||||
↓
|
||||
管理員在後台看到訊息
|
||||
↓
|
||||
管理員輸入並送出回覆
|
||||
↓
|
||||
API 回傳給 Open WebUI
|
||||
↓
|
||||
用戶收到回覆
|
||||
```
|
||||
|
||||
## 🔧 技術架構
|
||||
|
||||
- **FastAPI**: 輕量級 Python Web 框架
|
||||
- **AsyncIO**: 異步等待管理員回覆
|
||||
- **偽裝 OpenAI API**:
|
||||
- `/v1/models` - 模型列表
|
||||
- `/v1/chat/completions` - 聊天完成端點
|
||||
|
||||
## 🐳 Docker 部署
|
||||
|
||||
### 檔案結構
|
||||
```
|
||||
tobiichiGPT/
|
||||
├── docker/ # Docker 相關檔案
|
||||
│ ├── human_reply_server.py # 主程式
|
||||
│ ├── requirements.txt # Python 依賴
|
||||
│ ├── Dockerfile # Docker 映像檔
|
||||
│ └── .dockerignore # Docker 忽略檔案
|
||||
├── docker-stack/ # Docker Compose 配置
|
||||
│ ├── docker-compose.yml # 服務編排檔案
|
||||
│ └── .env.example # 環境變數範例
|
||||
├── README.md # 專案說明
|
||||
├── QUICKSTART.md # 快速開始指南
|
||||
└── LICENSE # 授權檔案
|
||||
```
|
||||
|
||||
### Docker 指令
|
||||
|
||||
```powershell
|
||||
# 建立映像檔
|
||||
cd docker
|
||||
docker build -t tobiichi-gpt .
|
||||
|
||||
# 直接執行容器
|
||||
docker run -d -p 8000:8000 --name tobiichi-gpt tobiichi-gpt
|
||||
|
||||
# 使用 Docker Compose(推薦)
|
||||
cd .\docker-stack
|
||||
docker-compose up -d
|
||||
|
||||
# 查看容器狀態
|
||||
docker ps
|
||||
|
||||
# 查看日誌
|
||||
docker logs -f tobiichi-gpt
|
||||
|
||||
# 停止並移除
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
### 與 Open WebUI 整合
|
||||
|
||||
如果 Open WebUI 也在 Docker 中運行,將兩者加入同一網路:
|
||||
|
||||
```yaml
|
||||
# 在 docker-compose.yml 中加入 Open WebUI
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
human-reply-server:
|
||||
build: .
|
||||
container_name: tobiichi-gpt
|
||||
ports:
|
||||
- "8000:8000"
|
||||
networks:
|
||||
- tobiichi-network
|
||||
|
||||
open-webui:
|
||||
image: ghcr.io/open-webui/open-webui:main
|
||||
container_name: open-webui
|
||||
ports:
|
||||
- "3000:8080"
|
||||
volumes:
|
||||
- open-webui:/app/backend/data
|
||||
networks:
|
||||
- tobiichi-network
|
||||
|
||||
networks:
|
||||
tobiichi-network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
open-webui:
|
||||
```
|
||||
|
||||
然後在 Open WebUI 中使用:
|
||||
- **API Base URL**: `http://human-reply-server:8000/v1`
|
||||
|
||||
## 📝 進階設定
|
||||
|
||||
### 修改等待超時時間
|
||||
|
||||
編輯 `docker/human_reply_server.py` 第 79 行:
|
||||
|
||||
```python
|
||||
timeout = 600 # 預設 10 分鐘,可改為其他秒數
|
||||
```
|
||||
|
||||
### 修改伺服器埠號
|
||||
|
||||
**方法 1: 修改 docker-compose.yml**
|
||||
```yaml
|
||||
# 編輯 docker-stack/docker-compose.yml
|
||||
ports:
|
||||
- "9000:8000" # 本機 9000 對應容器 8000
|
||||
```
|
||||
|
||||
**方法 2: 修改程式碼**
|
||||
```python
|
||||
# 編輯 docker/human_reply_server.py
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000) # 改為其他埠號
|
||||
```
|
||||
|
||||
### 設定 Cloudflare Tunnel
|
||||
|
||||
詳細設定步驟請參考專案根目錄的說明文件,或參考 `docker-stack/.env.example`:
|
||||
|
||||
```powershell
|
||||
cd docker-stack
|
||||
Copy-Item .env.example .env
|
||||
notepad .env # 填入您的 CLOUDFLARE_TUNNEL_TOKEN
|
||||
```
|
||||
|
||||
### 支援多管理員
|
||||
|
||||
目前版本採「先搶先贏」機制,任何管理員都可以回覆任何訊息。若需要分配機制,可以加入:
|
||||
- 管理員登入系統
|
||||
- 訊息認領機制
|
||||
- 管理員負載平衡
|
||||
|
||||
## ⚠️ 注意事項
|
||||
|
||||
- 此為**最簡化版本**,適合小規模使用
|
||||
- 訊息儲存在記憶體中,重啟會遺失
|
||||
- 沒有身份驗證,建議僅在內網使用
|
||||
- 若需生產環境,建議加入:
|
||||
- 資料庫持久化
|
||||
- 身份驗證
|
||||
- WebSocket 即時推送
|
||||
- 訊息隊列系統
|
||||
|
||||
## 🎨 管理員後台截圖
|
||||
|
||||
後台提供:
|
||||
- 待處理訊息列表
|
||||
- 用戶訊息顯示
|
||||
- 回覆文字框
|
||||
- 一鍵送出功能
|
||||
- 自動刷新
|
||||
|
||||
4
docker-stack/.env.example
Normal file
4
docker-stack/.env.example
Normal file
@@ -0,0 +1,4 @@
|
||||
# Cloudflare Tunnel Token
|
||||
# 請到 Cloudflare Zero Trust Dashboard 建立 Tunnel 並取得 Token
|
||||
# https://one.dash.cloudflare.com/
|
||||
CLOUDFLARE_TUNNEL_TOKEN=your-tunnel-token-here
|
||||
69
docker-stack/docker-compose.yml
Normal file
69
docker-stack/docker-compose.yml
Normal file
@@ -0,0 +1,69 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# 人工回覆伺服器
|
||||
human-reply-server:
|
||||
build: .
|
||||
container_name: tobiichi-gpt
|
||||
ports:
|
||||
- "8000:8000"
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- TZ=Asia/Taipei
|
||||
networks:
|
||||
- tobiichi-network
|
||||
|
||||
# Open WebUI
|
||||
open-webui:
|
||||
image: ghcr.io/open-webui/open-webui:main
|
||||
container_name: open-webui
|
||||
ports:
|
||||
- "3000:8080"
|
||||
volumes:
|
||||
- open-webui-data:/app/backend/data
|
||||
environment:
|
||||
- WEBUI_AUTH=False # 關閉登入驗證(可選)
|
||||
- TZ=Asia/Taipei
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- tobiichi-network
|
||||
|
||||
# Nginx Proxy Manager
|
||||
nginx-proxy-manager:
|
||||
image: jc21/nginx-proxy-manager:latest
|
||||
container_name: nginx-proxy-manager
|
||||
ports:
|
||||
- "8080:80" # HTTP (避免與現有 NPM 衝突)
|
||||
- "8443:443" # HTTPS (避免與現有 NPM 衝突)
|
||||
- "82:81" # NPM 管理介面 (避免與現有 NPM 衝突)
|
||||
volumes:
|
||||
- npm-data:/data
|
||||
- npm-letsencrypt:/etc/letsencrypt
|
||||
environment:
|
||||
- TZ=Asia/Taipei
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- tobiichi-network
|
||||
|
||||
# Cloudflare Tunnel
|
||||
cloudflared:
|
||||
image: cloudflare/cloudflared:latest
|
||||
container_name: cloudflared-tunnel
|
||||
command: tunnel run
|
||||
environment:
|
||||
- TUNNEL_TOKEN=${CLOUDFLARE_TUNNEL_TOKEN}
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- tobiichi-network
|
||||
depends_on:
|
||||
- open-webui
|
||||
- human-reply-server
|
||||
|
||||
networks:
|
||||
tobiichi-network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
open-webui-data:
|
||||
npm-data:
|
||||
npm-letsencrypt:
|
||||
17
docker/.dockerignore
Normal file
17
docker/.dockerignore
Normal file
@@ -0,0 +1,17 @@
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.Python
|
||||
*.so
|
||||
*.egg
|
||||
*.egg-info
|
||||
dist
|
||||
build
|
||||
.git
|
||||
.gitignore
|
||||
*.md
|
||||
LICENSE
|
||||
.vscode
|
||||
.idea
|
||||
*.log
|
||||
18
docker/Dockerfile
Normal file
18
docker/Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 複製依賴檔案
|
||||
COPY requirements.txt .
|
||||
|
||||
# 安裝依賴
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# 複製程式檔案
|
||||
COPY human_reply_server.py .
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8000
|
||||
|
||||
# 啟動伺服器
|
||||
CMD ["python", "human_reply_server.py"]
|
||||
384
docker/human_reply_server.py
Normal file
384
docker/human_reply_server.py
Normal file
@@ -0,0 +1,384 @@
|
||||
"""
|
||||
人工回覆伺服器 - 偽裝成 OpenAI API
|
||||
使用方式:
|
||||
1. 執行此程式:python human_reply_server.py
|
||||
2. 在 Open WebUI 設定中加入模型:http://localhost:8000/v1
|
||||
3. 用戶發送訊息後,訪問 http://localhost:8000/admin 查看並回覆
|
||||
"""
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import HTMLResponse, StreamingResponse
|
||||
from pydantic import BaseModel
|
||||
import asyncio
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
import uuid
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# 儲存待處理的對話
|
||||
pending_conversations = {}
|
||||
# 儲存管理員的回覆
|
||||
admin_replies = {}
|
||||
|
||||
|
||||
class Message(BaseModel):
|
||||
role: str
|
||||
content: str
|
||||
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
model: str
|
||||
messages: list[Message]
|
||||
stream: Optional[bool] = False
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"status": "Human Reply Server Running", "admin_panel": "/admin"}
|
||||
|
||||
|
||||
@app.get("/v1/models")
|
||||
async def list_models():
|
||||
"""模擬 OpenAI 的模型列表 API"""
|
||||
return {
|
||||
"object": "list",
|
||||
"data": [
|
||||
{
|
||||
"id": "human-admin",
|
||||
"object": "model",
|
||||
"created": int(time.time()),
|
||||
"owned_by": "human-admin"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@app.post("/v1/chat/completions")
|
||||
async def chat_completions(request: ChatRequest):
|
||||
"""模擬 OpenAI 的聊天完成 API - 等待真人回覆"""
|
||||
|
||||
# 生成對話 ID
|
||||
conversation_id = str(uuid.uuid4())[:8]
|
||||
|
||||
# 取得最後一則用戶訊息
|
||||
user_message = ""
|
||||
for msg in reversed(request.messages):
|
||||
if msg.role == "user":
|
||||
user_message = msg.content
|
||||
break
|
||||
|
||||
# 儲存對話資訊
|
||||
pending_conversations[conversation_id] = {
|
||||
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"user_message": user_message,
|
||||
"full_history": [{"role": msg.role, "content": msg.content} for msg in request.messages]
|
||||
}
|
||||
|
||||
print(f"\n[新訊息 {conversation_id}] {user_message}")
|
||||
print(f"→ 等待管理員回覆,請訪問: http://localhost:8000/admin")
|
||||
|
||||
# 等待管理員回覆 (最多等待 10 分鐘)
|
||||
timeout = 600
|
||||
elapsed = 0
|
||||
while conversation_id not in admin_replies and elapsed < timeout:
|
||||
await asyncio.sleep(1)
|
||||
elapsed += 1
|
||||
|
||||
# 取得管理員回覆
|
||||
if conversation_id in admin_replies:
|
||||
reply_content = admin_replies[conversation_id]
|
||||
del admin_replies[conversation_id]
|
||||
del pending_conversations[conversation_id]
|
||||
else:
|
||||
reply_content = "抱歉,管理員暫時無法回覆,請稍後再試。"
|
||||
|
||||
# 根據是否需要串流回傳
|
||||
if request.stream:
|
||||
return StreamingResponse(
|
||||
stream_response(reply_content),
|
||||
media_type="text/event-stream"
|
||||
)
|
||||
else:
|
||||
return {
|
||||
"id": f"chatcmpl-{conversation_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": 0,
|
||||
"completion_tokens": 0,
|
||||
"total_tokens": 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async def stream_response(content: str):
|
||||
"""串流方式回傳回覆"""
|
||||
# 開始標記
|
||||
yield f"data: {json.dumps({'choices': [{'delta': {'role': 'assistant'}, 'index': 0}]})}\n\n"
|
||||
|
||||
# 逐字回傳
|
||||
for char in content:
|
||||
chunk = {
|
||||
"choices": [{
|
||||
"delta": {"content": char},
|
||||
"index": 0
|
||||
}]
|
||||
}
|
||||
yield f"data: {json.dumps(chunk)}\n\n"
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
# 結束標記
|
||||
yield f"data: {json.dumps({'choices': [{'delta': {}, 'index': 0, 'finish_reason': 'stop'}]})}\n\n"
|
||||
yield "data: [DONE]\n\n"
|
||||
|
||||
|
||||
@app.get("/admin", response_class=HTMLResponse)
|
||||
async def admin_panel():
|
||||
"""管理員後台 - 查看並回覆訊息"""
|
||||
|
||||
# 生成待處理對話的 HTML
|
||||
conversations_html = ""
|
||||
if pending_conversations:
|
||||
for conv_id, conv_data in pending_conversations.items():
|
||||
conversations_html += f"""
|
||||
<div class="conversation" id="conv-{conv_id}">
|
||||
<div class="conv-header">
|
||||
<strong>對話 ID:</strong> {conv_id}
|
||||
<span class="timestamp">{conv_data['timestamp']}</span>
|
||||
</div>
|
||||
<div class="user-message">
|
||||
<strong>用戶:</strong> {conv_data['user_message']}
|
||||
</div>
|
||||
<textarea id="reply-{conv_id}" placeholder="輸入您的回覆..."></textarea>
|
||||
<button onclick="sendReply('{conv_id}')">送出回覆</button>
|
||||
</div>
|
||||
"""
|
||||
else:
|
||||
conversations_html = '<p class="no-messages">目前沒有待處理的訊息</p>'
|
||||
|
||||
html = f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>管理員後台 - 人工回覆系統</title>
|
||||
<style>
|
||||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||||
body {{
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}}
|
||||
.container {{
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
||||
padding: 30px;
|
||||
}}
|
||||
h1 {{
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
font-size: 28px;
|
||||
}}
|
||||
.subtitle {{
|
||||
color: #666;
|
||||
margin-bottom: 30px;
|
||||
font-size: 14px;
|
||||
}}
|
||||
.conversation {{
|
||||
background: #f8f9fa;
|
||||
border-left: 4px solid #667eea;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 8px;
|
||||
animation: slideIn 0.3s ease;
|
||||
}}
|
||||
@keyframes slideIn {{
|
||||
from {{ opacity: 0; transform: translateY(-10px); }}
|
||||
to {{ opacity: 1; transform: translateY(0); }}
|
||||
}}
|
||||
.conv-header {{
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}}
|
||||
.timestamp {{
|
||||
float: right;
|
||||
color: #6c757d;
|
||||
font-size: 12px;
|
||||
}}
|
||||
.user-message {{
|
||||
background: white;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 15px;
|
||||
border: 1px solid #e0e0e0;
|
||||
}}
|
||||
textarea {{
|
||||
width: 100%;
|
||||
min-height: 100px;
|
||||
padding: 12px;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
resize: vertical;
|
||||
transition: border-color 0.3s;
|
||||
}}
|
||||
textarea:focus {{
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}}
|
||||
button {{
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 30px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-top: 10px;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}}
|
||||
button:hover {{
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||
}}
|
||||
button:active {{
|
||||
transform: translateY(0);
|
||||
}}
|
||||
.no-messages {{
|
||||
text-align: center;
|
||||
color: #6c757d;
|
||||
padding: 40px;
|
||||
font-style: italic;
|
||||
}}
|
||||
.status {{
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: white;
|
||||
padding: 10px 20px;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
display: none;
|
||||
}}
|
||||
.status.show {{
|
||||
display: block;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}}
|
||||
@keyframes fadeIn {{
|
||||
from {{ opacity: 0; }}
|
||||
to {{ opacity: 1; }}
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="status" id="status"></div>
|
||||
<div class="container">
|
||||
<h1>🎯 管理員後台</h1>
|
||||
<p class="subtitle">人工回覆系統 - 待處理訊息數量: {len(pending_conversations)}</p>
|
||||
|
||||
<div id="conversations">
|
||||
{conversations_html}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function sendReply(convId) {{
|
||||
const textarea = document.getElementById('reply-' + convId);
|
||||
const reply = textarea.value.trim();
|
||||
|
||||
if (!reply) {{
|
||||
alert('請輸入回覆內容');
|
||||
return;
|
||||
}}
|
||||
|
||||
fetch('/admin/reply', {{
|
||||
method: 'POST',
|
||||
headers: {{ 'Content-Type': 'application/json' }},
|
||||
body: JSON.stringify({{ conversation_id: convId, reply: reply }})
|
||||
}})
|
||||
.then(response => response.json())
|
||||
.then(data => {{
|
||||
if (data.status === 'success') {{
|
||||
showStatus('✅ 回覆已送出!');
|
||||
document.getElementById('conv-' + convId).remove();
|
||||
|
||||
// 如果沒有待處理訊息,顯示提示
|
||||
if (document.querySelectorAll('.conversation').length === 0) {{
|
||||
document.getElementById('conversations').innerHTML =
|
||||
'<p class="no-messages">目前沒有待處理的訊息</p>';
|
||||
}}
|
||||
}}
|
||||
}})
|
||||
.catch(error => {{
|
||||
showStatus('❌ 送出失敗: ' + error);
|
||||
}});
|
||||
}}
|
||||
|
||||
function showStatus(message) {{
|
||||
const status = document.getElementById('status');
|
||||
status.textContent = message;
|
||||
status.classList.add('show');
|
||||
setTimeout(() => status.classList.remove('show'), 3000);
|
||||
}}
|
||||
|
||||
// 每 5 秒自動重新整理
|
||||
setInterval(() => location.reload(), 5000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
return html
|
||||
|
||||
|
||||
@app.post("/admin/reply")
|
||||
async def admin_reply(request: Request):
|
||||
"""接收管理員的回覆"""
|
||||
data = await request.json()
|
||||
conversation_id = data.get("conversation_id")
|
||||
reply = data.get("reply")
|
||||
|
||||
if conversation_id and reply:
|
||||
admin_replies[conversation_id] = reply
|
||||
print(f"[管理員回覆 {conversation_id}] {reply}")
|
||||
return {"status": "success"}
|
||||
|
||||
return {"status": "error", "message": "Missing conversation_id or reply"}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
print("=" * 60)
|
||||
print("🚀 人工回覆伺服器啟動中...")
|
||||
print("=" * 60)
|
||||
print("📌 管理員後台: http://localhost:8000/admin")
|
||||
print("📌 API 端點: http://localhost:8000/v1")
|
||||
print("=" * 60)
|
||||
print("\n在 Open WebUI 中設定:")
|
||||
print("1. 進入 Settings → Connections")
|
||||
print("2. 添加 OpenAI API:")
|
||||
print(" - API Base URL: http://localhost:8000/v1")
|
||||
print(" - API Key: 隨便填 (例如: sk-human)")
|
||||
print("3. 模型名稱會顯示為: human-admin")
|
||||
print("=" * 60)
|
||||
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
3
docker/requirements.txt
Normal file
3
docker/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
fastapi==0.104.1
|
||||
uvicorn==0.24.0
|
||||
pydantic==2.5.0
|
||||
Reference in New Issue
Block a user