diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3ae6f18 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.env +*.log +__pycache__ +*.pyc +cloudflared-config.yml +credentials.json +npm-data/ +open-webui-data/ diff --git a/QUICKSTART.md b/QUICKSTART.md new file mode 100644 index 0000000..0742bba --- /dev/null +++ b/QUICKSTART.md @@ -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` 檔案) diff --git a/README.md b/README.md index d6be013..17fcd6c 100644 --- a/README.md +++ b/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 即時推送 + - 訊息隊列系統 + +## 🎨 管理員後台截圖 + +後台提供: +- 待處理訊息列表 +- 用戶訊息顯示 +- 回覆文字框 +- 一鍵送出功能 +- 自動刷新 diff --git a/docker-stack/.env.example b/docker-stack/.env.example new file mode 100644 index 0000000..05ae533 --- /dev/null +++ b/docker-stack/.env.example @@ -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 diff --git a/docker-stack/docker-compose.yml b/docker-stack/docker-compose.yml new file mode 100644 index 0000000..46be8ae --- /dev/null +++ b/docker-stack/docker-compose.yml @@ -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: diff --git a/docker/.dockerignore b/docker/.dockerignore new file mode 100644 index 0000000..f7aa7bd --- /dev/null +++ b/docker/.dockerignore @@ -0,0 +1,17 @@ +__pycache__ +*.pyc +*.pyo +*.pyd +.Python +*.so +*.egg +*.egg-info +dist +build +.git +.gitignore +*.md +LICENSE +.vscode +.idea +*.log diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..41c1707 --- /dev/null +++ b/docker/Dockerfile @@ -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"] diff --git a/docker/human_reply_server.py b/docker/human_reply_server.py new file mode 100644 index 0000000..1d0609c --- /dev/null +++ b/docker/human_reply_server.py @@ -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""" +
+
+ 對話 ID: {conv_id} + {conv_data['timestamp']} +
+
+ 用戶: {conv_data['user_message']} +
+ + +
+ """ + else: + conversations_html = '

目前沒有待處理的訊息

' + + html = f""" + + + + + + 管理員後台 - 人工回覆系統 + + + +
+
+

🎯 管理員後台

+

人工回覆系統 - 待處理訊息數量: {len(pending_conversations)}

+ +
+ {conversations_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) diff --git a/docker/requirements.txt b/docker/requirements.txt new file mode 100644 index 0000000..8e26b4d --- /dev/null +++ b/docker/requirements.txt @@ -0,0 +1,3 @@ +fastapi==0.104.1 +uvicorn==0.24.0 +pydantic==2.5.0