This commit is contained in:
ChenKaiLiuG
2025-12-20 23:25:42 +08:00
parent 263de7f16a
commit 76a15ecabb
11 changed files with 620 additions and 744 deletions

View File

@@ -1,122 +0,0 @@
# 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` 檔案)

374
README.md
View File

@@ -5,161 +5,341 @@
## 🎯 特色
-**零修改** - 不需要修改 Open WebUI 程式碼
-**極簡化** - 單一 Python 檔案即可運行
-**即插即用** - 偽裝成 OpenAI API直接在 Open WebUI 設定中使用
-**視覺化後台** - 美觀的網頁介面供管理員回覆
-**極簡化** - 4 個容器完成所有功能
-**共享資料庫** - Open WebUI 和管理後台使用同一個 PostgreSQL
-**視覺化後台** - NocoDB 提供專業的資料庫管理介面
-**可編輯代碼** - API 程式碼透過 bind mount 可直接修改
## 🏗️ 架構
### 主要服務(必須)
```
┌─────────────────┐
│ PostgreSQL │ ← 共享資料庫
└────────┬────────┘
┌────┴─────┬─────────────┬─────────────┐
│ │ │ │
┌───▼────┐ ┌──▼─────┐ ┌────▼─────┐ ┌───▼────┐
│ API │ │ Open │ │ NocoDB │ │ 用戶 │
│ 中間層 │ │ WebUI │ │ 管理介面 │ │ 瀏覽器 │
└────────┘ └────────┘ └──────────┘ └────────┘
```
### 代理服務(選用)
```
┌───────────┐
│ 用戶 │
└─────┬─────┘
│ 公網
┌─────▼──────────────┐
│ Cloudflare Tunnel │
└─────┬──────────────┘
│ 內網
┌─────▼──────────────┐
│ Nginx Proxy Mgr │
└─────┬──────────────┘
┌─────▼──────────────┐
│ 主要服務 (上方) │
└────────────────────┘
```
## 🚀 快速開始
### 方法 1: Docker (推薦)
### 1. 準備環境檔案
```powershell
# 進入 docker-stack 目錄
cd docker-stack
# 設定環境變數(首次使用)
# 複製主服務環境變數
Copy-Item .env.example .env
notepad .env # 填入 Cloudflare Tunnel Token若要使用
notepad .env # 設定 DB_PASSWORD
# 使用 Docker Compose 啟動
# 如需代理服務
Copy-Item .env.proxy .env.proxy
notepad .env.proxy # 填入 CLOUDFLARE_TUNNEL_TOKEN
```
### 2. 啟動主要服務
```powershell
# 啟動 PostgreSQL + API + Open WebUI + NocoDB
docker-compose up -d
# 查看狀態
docker-compose ps
# 查看日誌
docker-compose logs -f
# 停止服務
docker-compose down
```
### 方法 2: 直接執行
### 3. 啟動代理服務(選用)
```powershell
# 1. 進入 docker 目錄
cd docker
# 啟動 Cloudflare Tunnel + NPM
docker-compose -f docker-compose.proxy.yml --env-file .env.proxy up -d
# 2. 安裝依賴
pip install -r requirements.txt
# 3. 啟動伺服器
python human_reply_server.py
```
啟動後會看到:
```
🚀 人工回覆伺服器啟動中...
📌 管理員後台: http://localhost:8000/admin
📌 API 端點: http://localhost:8000/v1
# 查看狀態
docker-compose -f docker-compose.proxy.yml ps
```
## 🌐 服務訪問
啟動後可訪問以下服務
### 主要服務
| 服務 | 網址 | 說明 |
|------|------|------|
| **Open WebUI** | http://localhost:3000 | 用戶對話介面 |
| **管理員後台** | http://localhost:8000/admin | 真人回覆訊息 |
| **NPM 管理面板** | http://localhost:82 | Nginx Proxy Manager |
| **Cloudflare Tunnel** | 自動連線 | 無需手動訪問 |
| **API 伺服器** | http://localhost:8000 | OpenAI API 相容端點 |
| **NocoDB** | http://localhost:8080 | 管理員回覆介面 |
| **PostgreSQL** | localhost:5432 | 資料庫 |
預設帳號密碼NPM
### 代理服務(如已啟動)
| 服務 | 網址 | 說明 |
|------|------|------|
| **NPM 管理面板** | http://localhost:81 | Nginx Proxy Manager |
| **HTTP 代理** | Port 80 | HTTP 流量 |
| **HTTPS 代理** | Port 443 | HTTPS 流量 |
**NPM 預設帳號**:
- Email: `admin@example.com`
- Password: `changeme`
### 在 Open WebUI 中設定
## 📋 使用流程
### 設定 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**
3. 新增 OpenAI Connection:
- **API Base URL**: `http://api:8000/v1`
- **API Key**: `sk-human` (任意值)
4. 模型列表會出現 **human-admin**
### 開始使用
### 管理員回覆
1. **用戶端**: 在 Open WebUI (http://localhost:3000) 選擇 `human-admin` 模型,發送訊息
2. **管理員**: 訪問 http://localhost:8000/admin查看並回覆訊息
3. **自動刷新**: 後台每 5 秒自動刷新,顯示新訊息
1. 訪問 NocoDB: http://localhost:8080
2. 連接到 PostgreSQL:
- Host: `postgres`
- Port: `5432`
- Database: `tobiichi`
- Username: `tobiichi`
- Password: (你在 .env 設定的密碼)
3. 開啟 `reply_queue` 表格
4. 查看 `status='pending'` 的訊息
5. 填入 `admin_reply` 欄位
6.`status` 改為 `replied`
7. 用戶會在 3 秒內收到回覆
## 📋 運作流程
### 完整流程
```
用戶發送訊息
用戶在 Open WebUI 發送訊息
Open WebUI 調用 API (轉圈圈等待)
API 收到請求,寫入 reply_queue (status='pending')
訊息進入待處理隊列
API 每 3 秒檢查該訊息的 status
管理員在後台看到訊息
管理員在 NocoDB 看到訊息,填入回覆並改 status='replied'
管理員輸入並送出回覆
API 讀取 admin_reply 欄位
API 回傳給 Open WebUI
回傳給 Open WebUI
用戶收到回覆
```
## 🔧 技術架構
- **FastAPI**: 輕量級 Python Web 框架
- **AsyncIO**: 異步等待管理員回覆
- **偽裝 OpenAI API**:
- `/v1/models` - 模型列表
- `/v1/chat/completions` - 聊天完成端點
### 主要服務
## 🐳 Docker 部署
- **PostgreSQL 15**: 共享關聯式資料庫
- **FastAPI**: Python Web 框架,提供 OpenAI API 相容端點
- **Open WebUI**: 對話前端介面
- **NocoDB**: 視覺化資料庫管理工具
### 檔案結構
```
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 指令
- **Nginx Proxy Manager**: 反向代理 + SSL 憑證管理
- **Cloudflare Tunnel**: 安全的公網訪問通道
### API 端點
- `/v1/models` - 模型列表(回傳 human-admin
- `/v1/chat/completions` - 聊天完成端點OpenAI 相容)
## 🌍 Cloudflare Tunnel 設定
### 1. 建立 Tunnel
1. 登入 [Cloudflare Zero Trust](https://one.dash.cloudflare.com/)
2. 前往 **Networks****Tunnels**
3. 點擊 **Create a tunnel**
4. 選擇 **Cloudflared**
5. 輸入 Tunnel 名稱(例如: `tobiichi-tunnel`
6. 複製顯示的 Token
7. 將 Token 貼到 `.env.proxy``CLOUDFLARE_TUNNEL_TOKEN`
### 2. 設定 Public Hostname
#### 選項 A: 透過 NPM 代理(推薦)
在 Cloudflare Tunnel 設定:
- **Public hostname**: `chat.yourdomain.com`
- **Service Type**: HTTP
- **URL**: `http://npm:80`
然後在 NPM (http://localhost:81) 設定:
- **Domain**: `chat.yourdomain.com`
- **Forward to**: `openwebui:3000`
#### 選項 B: 直接指向服務
在 Cloudflare Tunnel 設定:
- **Public hostname**: `chat.yourdomain.com`
- **Service Type**: HTTP
- **URL**: `http://openwebui:3000`
### 3. 啟動 Tunnel
```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
docker-compose -f docker-compose.proxy.yml --env-file .env.proxy up -d
```
### 與 Open WebUI 整合
訪問你設定的網域即可從公網訪問服務。
如果 Open WebUI 也在 Docker 中運行,將兩者加入同一網路:
## 📦 資料持久化
所有服務資料都會持久化保存:
```yaml
# 在 docker-compose.yml 中加入 Open WebUI
version: '3.8'
volumes:
postgres-data: # PostgreSQL 資料
openwebui-data: # Open WebUI 配置和對話記錄
nocodb-data: # NocoDB 配置
npm-data: # NPM 配置(代理服務)
npm-letsencrypt: # SSL 憑證(代理服務)
```
services:
human-reply-server:
## 🛠️ 修改 API 程式碼
API 程式碼位於 `./api/` 目錄,透過 bind mount 掛載到容器:
```powershell
# 編輯 API 程式碼
notepad api\server.py
# 重啟 API 服務套用修改
docker-compose restart api
# 查看 API 日誌
docker-compose logs -f api
```
## 🐳 檔案結構
```
tobiichiGPT/
├── api/ # API 服務程式碼
│ ├── server.py # FastAPI 主程式
│ └── requirements.txt # Python 依賴
├── docker-compose.yml # 主要服務編排
├── docker-compose.proxy.yml # 代理服務編排
├── .env.example # 主服務環境變數範例
├── .env.proxy # 代理服務環境變數範例
└── README.md # 本文件
```
## 🔍 故障排除
### API 無法連接資料庫
```powershell
# 檢查資料庫是否啟動
docker-compose ps postgres
# 查看資料庫日誌
docker-compose logs postgres
# 檢查網路連接
docker network inspect tobiichi-network
```
### Open WebUI 無法連接 API
1. 確認 API URL 使用容器名稱: `http://api:8000/v1`
2. 檢查兩個容器是否在同一網路:
```powershell
docker network inspect tobiichi-network
```
### NocoDB 無法連接資料庫
1. 檢查 PostgreSQL 是否健康:
```powershell
docker-compose ps
```
2. 確認資料庫密碼正確(`.env` 檔案)
### 代理服務無法啟動
```powershell
# 檢查主網路是否已建立
docker network ls | Select-String "tobiichi-network"
# 先啟動主服務
docker-compose up -d
# 再啟動代理服務
docker-compose -f docker-compose.proxy.yml up -d
```
## 📝 開發筆記
### 資料庫 Schema
`reply_queue` 表格結構:
```sql
CREATE TABLE reply_queue (
id SERIAL PRIMARY KEY,
user_message TEXT NOT NULL,
admin_reply TEXT,
status VARCHAR(20) DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
replied_at TIMESTAMP
);
CREATE INDEX idx_status ON reply_queue(status);
CREATE INDEX idx_created_at ON reply_queue(created_at);
```
### 環境變數
**主服務** (`.env`):
- `DB_PASSWORD`: PostgreSQL 密碼
**代理服務** (`.env.proxy`):
- `CLOUDFLARE_TUNNEL_TOKEN`: Cloudflare Tunnel 認證 Token
### Port 映射
**主服務**:
- 3000: Open WebUI
- 5432: PostgreSQL
- 8000: API
- 8080: NocoDB
**代理服務**:
- 80: HTTP
- 443: HTTPS
- 81: NPM 管理介面
## 📄 授權
MIT License
build: .
container_name: tobiichi-gpt
ports:

View File

@@ -1,3 +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
View 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)

90
docker-compose.yml Normal file
View File

@@ -0,0 +1,90 @@
version: '3.8'
services:
# PostgreSQL - 共用資料庫
postgres:
image: postgres:15-alpine
container_name: tobiichiGPT-postgres
restart: unless-stopped
environment:
POSTGRES_DB: tobiichiGPT
POSTGRES_USER: tobiichi3227
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- postgres-data:/var/lib/postgresql/data
networks:
- tobiichiGPT-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U tobiichi3227"]
interval: 10s
timeout: 5s
retries: 5
# API 轉接層 - 偽裝 OpenAI API
api:
image: python:3.11-slim
container_name: tobiichiGPT-api
restart: unless-stopped
ports:
- "18000:8000"
environment:
- DB_HOST=postgres
- DB_PORT=5432
- DB_NAME=tobiichiGPT
- DB_USER=tobiichi3227
- DB_PASSWORD=${DB_PASSWORD}
volumes:
- ./api:/app
working_dir: /app
command: >
sh -c "
pip install --no-cache-dir -r requirements.txt &&
python server.py
"
networks:
- tobiichiGPT-network
depends_on:
postgres:
condition: service_healthy
# Open WebUI - 用戶對話介面
openwebui:
image: ghcr.io/open-webui/open-webui:main
container_name: tobiichiGPT-ui
restart: unless-stopped
ports:
- "13000:3000"
environment:
- DATABASE_URL=postgresql://tobiichi3227:${DB_PASSWORD}@postgres:5432/tobiichiGPT
- WEBUI_AUTH=True
volumes:
- openwebui-data:/app/backend/data
networks:
- tobiichiGPT-network
depends_on:
postgres:
condition: service_healthy
# NocoDB - 管理員回覆介面
nocodb:
image: nocodb/nocodb:latest
container_name: tobiichiGPT-nocodb
restart: unless-stopped
ports:
- "18080:8080"
environment:
- NC_DB=pg://postgres:5432?u=tobiichi3227&p=${DB_PASSWORD}&d=tobiichiGPT
networks:
- tobiichiGPT-network
depends_on:
postgres:
condition: service_healthy
networks:
tobiichiGPT-network:
driver: bridge
name: tobiichiGPT-network # 固定網路名稱,讓 proxy stack 可以連接
volumes:
postgres-data:
openwebui-data:

View File

@@ -1,34 +0,0 @@
# Gitea Repository URL (替換為您的 Gitea 伺服器)
# 格式: http://gitea.example.com/username/tobiichiGPT.git
GIT_REPO_URL=http://your-gitea-server/your-username/tobiichiGPT.git
# Git Branch (預設使用 main)
GIT_BRANCH=main
# Container Registry (可選 - 如果要使用預建映像)
# 例如: gitea.example.com:5000
REGISTRY_URL=
# ========================================
# Gitea Repository 設定
# ========================================
# Gitea Repository URL (格式: http://gitea.example.com/username/tobiichiGPT.git)
# 如需認證可使用: http://username:token@gitea.example.com/username/tobiichiGPT.git
GIT_REPO_URL=http://your-gitea-server/your-username/tobiichiGPT.git
# Git Branch (預設: main)
GIT_BRANCH=main
# ========================================
# Container Registry (可選)
# ========================================
# 如果已預先建置映像,填入 Registry 前綴 (例如: gitea.example.com:5000/username/)
# 留空則每次從 Gitea 即時建置
REGISTRY_URL=
# ========================================
# Cloudflare Tunnel (可選)
# ========================================
# 請到 Cloudflare Zero Trust Dashboard 建立 Tunnel 並取得 Token
# https://one.dash.cloudflare.com/
CLOUDFLARE_TUNNEL_TOKEN=

View File

@@ -1,72 +0,0 @@
version: '3.8'
services:
# 人工回覆伺服器 (從 Gitea 建置)
human-reply-server:
image: ${REGISTRY_URL}tobiichi-gpt:latest
build:
context: https://git.karylab.com/ChenKaiLiuG/tobiichiGPT.git#${GIT_BRANCH:-main}
dockerfile: human-gpt/Dockerfile
container_name: tobiichi-gpt
ports:
- "11000: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:
- "10000: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:
- "10080:80" # HTTP (避免與現有 NPM 衝突)
- "10443:443" # HTTPS (避免與現有 NPM 衝突)
- "10081: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:

View File

@@ -1,17 +0,0 @@
__pycache__
*.pyc
*.pyo
*.pyd
.Python
*.so
*.egg
*.egg-info
dist
build
.git
.gitignore
*.md
LICENSE
.vscode
.idea
*.log

View File

@@ -1,18 +0,0 @@
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"]

View File

@@ -1,384 +0,0 @@
"""
人工回覆伺服器 - 偽裝成 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)

44
proxy-docker-compose.yml Normal file
View File

@@ -0,0 +1,44 @@
version: '3.8'
services:
# Nginx Proxy Manager
npm:
image: jc21/nginx-proxy-manager:latest
container_name: tobiichiGPT-npm
restart: unless-stopped
ports:
- "10080:80" # HTTP
- "10443:443" # HTTPS
- "10081:81" # 管理介面
volumes:
- npm-data:/data
- npm-letsencrypt:/etc/letsencrypt
environment:
- TZ=Asia/Taipei
networks:
- proxy-network
- tobiichiGPT-network
# Cloudflare Tunnel
cloudflared:
image: cloudflare/cloudflared:latest
container_name: tobiichiGPT-cloudflared
restart: unless-stopped
command: tunnel run
environment:
- TUNNEL_TOKEN=${CLOUDFLARE_TUNNEL_TOKEN}
networks:
- proxy-network
- tobiichiGPT-network
depends_on:
- npm
networks:
proxy-network:
driver: bridge
tobiichiGPT-network:
external: true # 連接到主 stack 的網路
volumes:
npm-data:
npm-letsencrypt: