Update chatwoot docker
This commit is contained in:
5
.env.example
Normal file
5
.env.example
Normal file
@@ -0,0 +1,5 @@
|
||||
# PostgreSQL 密碼
|
||||
DB_PASSWORD=your_secure_password_here
|
||||
|
||||
# Chatwoot Secret Key (使用以下命令生成: openssl rand -hex 64)
|
||||
CHATWOOT_SECRET_KEY=your_chatwoot_secret_key_here
|
||||
64
README.md
64
README.md
@@ -54,11 +54,13 @@
|
||||
```powershell
|
||||
# 複製主服務環境變數
|
||||
Copy-Item .env.example .env
|
||||
notepad .env # 設定 DB_PASSWORD
|
||||
notepad .env # 設定 DB_PASSWORD 和 CHATWOOT_SECRET_KEY
|
||||
|
||||
# 如需代理服務
|
||||
Copy-Item .env.proxy .env.proxy
|
||||
notepad .env.proxy # 填入 CLOUDFLARE_TUNNEL_TOKEN
|
||||
# 生成 Chatwoot Secret Key (在 Linux/macOS)
|
||||
openssl rand -hex 64
|
||||
|
||||
# 或在 PowerShell 生成
|
||||
-join ((1..128) | ForEach-Object { '{0:x}' -f (Get-Random -Maximum 16) })
|
||||
```
|
||||
|
||||
### 2. 啟動主要服務
|
||||
@@ -90,9 +92,9 @@ docker-compose -f docker-compose.proxy.yml ps
|
||||
|
||||
| 服務 | 網址 | 說明 |
|
||||
|------|------|------|
|
||||
| **Open WebUI** | http://localhost:3000 | 用戶對話介面 |
|
||||
| **API 伺服器** | http://localhost:8000 | OpenAI API 相容端點 |
|
||||
| **NocoDB** | http://localhost:8080 | 管理員回覆介面 |
|
||||
| **Open WebUI** | http://localhost:10060 | 用戶對話介面 |
|
||||
| **API 伺服器** | http://localhost:18000 | OpenAI API 相容端點 |
|
||||
| **Chatwoot** | http://localhost:13500 | 管理員對話介面 |
|
||||
| **PostgreSQL** | localhost:5432 | 資料庫 |
|
||||
|
||||
### 代理服務(如已啟動)
|
||||
@@ -109,47 +111,29 @@ docker-compose -f docker-compose.proxy.yml ps
|
||||
|
||||
## 📋 使用流程
|
||||
|
||||
### 首次設定 Chatwoot
|
||||
|
||||
1. 訪問 http://localhost:13500
|
||||
2. 建立管理員帳號
|
||||
3. 建立新的 Inbox(收件匣):
|
||||
- 名稱: TobiichiGPT
|
||||
- 類型: API
|
||||
4. 取得 Inbox ID(稍後需要)
|
||||
|
||||
### 設定 Open WebUI
|
||||
|
||||
1. 開啟 http://localhost:3000
|
||||
1. 開啟 http://localhost:10060
|
||||
2. 進入 **Settings** → **Connections**
|
||||
3. 新增 OpenAI Connection:
|
||||
- **API Base URL**: `http://api:8000/v1`
|
||||
- **API Base URL**: `http://tobiichiGPT-api:8000/v1`
|
||||
- **API Key**: `sk-human` (任意值)
|
||||
4. 模型列表會出現 **human-admin**
|
||||
|
||||
### 管理員回覆
|
||||
### 管理員回覆流程
|
||||
|
||||
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 發送訊息
|
||||
↓
|
||||
API 收到請求,寫入 reply_queue (status='pending')
|
||||
↓
|
||||
API 每 3 秒檢查該訊息的 status
|
||||
↓
|
||||
管理員在 NocoDB 看到訊息,填入回覆並改 status='replied'
|
||||
↓
|
||||
API 讀取 admin_reply 欄位
|
||||
↓
|
||||
回傳給 Open WebUI
|
||||
↓
|
||||
用戶收到回覆
|
||||
```
|
||||
1. 登入 Chatwoot: http://localhost:13500
|
||||
2. 在對話列表查看新訊息
|
||||
3. 直接回覆即可,用戶會在 3 秒內收到
|
||||
|
||||
## 🔧 技術架構
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
fastapi==0.104.1
|
||||
uvicorn==0.24.0
|
||||
pydantic==2.5.0
|
||||
psycopg2-binary==2.9.9
|
||||
asyncpg==0.29.0
|
||||
|
||||
151
api/server.py
151
api/server.py
@@ -2,7 +2,8 @@
|
||||
API 轉接層 - 偽裝 OpenAI API,將請求轉為人工回覆隊列
|
||||
"""
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel
|
||||
import asyncpg
|
||||
import asyncio
|
||||
@@ -74,134 +75,130 @@ async def startup():
|
||||
async def shutdown():
|
||||
if db_pool:
|
||||
await db_pool.close()
|
||||
print("👋 資料庫連接已關閉")
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {
|
||||
"status": "TobiichiGPT API Running",
|
||||
"database": "Connected" if db_pool else "Disconnected"
|
||||
}
|
||||
"""根路徑"""
|
||||
return {"status": "ok", "service": "TobiichiGPT API"}
|
||||
|
||||
|
||||
@app.get("/v1/models")
|
||||
async def list_models():
|
||||
"""模擬 OpenAI 模型列表"""
|
||||
"""模擬 OpenAI 的 /v1/models 端點"""
|
||||
return {
|
||||
"object": "list",
|
||||
"data": [{
|
||||
"data": [
|
||||
{
|
||||
"id": "human-admin",
|
||||
"object": "model",
|
||||
"created": int(time.time()),
|
||||
"owned_by": "tobiichi"
|
||||
}]
|
||||
"owned_by": "tobiichi",
|
||||
"permission": [],
|
||||
"root": "human-admin",
|
||||
"parent": None
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@app.post("/v1/chat/completions")
|
||||
async def chat_completions(request: ChatRequest):
|
||||
"""接收用戶訊息,等待管理員回覆"""
|
||||
|
||||
# 生成對話 ID
|
||||
conv_id = str(uuid.uuid4())[:12]
|
||||
|
||||
# 提取用戶訊息
|
||||
user_message = ""
|
||||
"""
|
||||
模擬 OpenAI Chat Completions API
|
||||
將用戶訊息寫入資料庫,等待管理員回覆
|
||||
"""
|
||||
# 取得最後一則用戶訊息
|
||||
user_message = None
|
||||
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"
|
||||
}]
|
||||
}
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={"error": "No user message found"}
|
||||
)
|
||||
|
||||
# 生成對話 ID
|
||||
conversation_id = str(uuid.uuid4())
|
||||
|
||||
# 寫入資料庫
|
||||
async with db_pool.acquire() as conn:
|
||||
await conn.execute("""
|
||||
await conn.execute(
|
||||
"""
|
||||
INSERT INTO reply_queue (conversation_id, user_message, status)
|
||||
VALUES ($1, $2, 'pending')
|
||||
""", conv_id, user_message)
|
||||
""",
|
||||
conversation_id, user_message
|
||||
)
|
||||
|
||||
print(f"[新對話 {conv_id}] {user_message[:50]}...")
|
||||
print(f"📝 收到訊息 [{conversation_id}]: {user_message[:50]}...")
|
||||
|
||||
# 等待管理員回覆(最多 15 分鐘)
|
||||
timeout = 900
|
||||
start_time = time.time()
|
||||
reply_content = None
|
||||
# 等待管理員回覆 (最多 15 分鐘)
|
||||
max_wait = 900 # 15 分鐘
|
||||
check_interval = 3 # 每 3 秒檢查一次
|
||||
waited = 0
|
||||
|
||||
while waited < max_wait:
|
||||
await asyncio.sleep(check_interval)
|
||||
waited += check_interval
|
||||
|
||||
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)
|
||||
row = await conn.fetchrow(
|
||||
"SELECT admin_reply, status FROM reply_queue WHERE conversation_id = $1",
|
||||
conversation_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)
|
||||
admin_reply = row['admin_reply']
|
||||
print(f"✅ 管理員已回覆 [{conversation_id}]")
|
||||
|
||||
# 回傳 OpenAI 格式的回應
|
||||
return {
|
||||
"id": f"chatcmpl-{conv_id}",
|
||||
"id": f"chatcmpl-{conversation_id}",
|
||||
"object": "chat.completion",
|
||||
"created": int(time.time()),
|
||||
"model": request.model,
|
||||
"choices": [{
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": reply_content
|
||||
"content": admin_reply
|
||||
},
|
||||
"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)
|
||||
"completion_tokens": len(admin_reply),
|
||||
"total_tokens": len(user_message) + len(admin_reply)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
"""健康檢查"""
|
||||
db_ok = db_pool is not None
|
||||
return {"status": "healthy" if db_ok else "unhealthy", "database": db_ok}
|
||||
# 超時回應
|
||||
print(f"⏰ 等待超時 [{conversation_id}]")
|
||||
return {
|
||||
"id": f"chatcmpl-{conversation_id}",
|
||||
"object": "chat.completion",
|
||||
"created": int(time.time()),
|
||||
"model": request.model,
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "抱歉,管理員目前忙碌中,請稍後再試。"
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
|
||||
@@ -53,7 +53,7 @@ services:
|
||||
container_name: tobiichiGPT-ui
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "13000:3000"
|
||||
- "10060:8080"
|
||||
environment:
|
||||
- DATABASE_URL=postgresql://tobiichi3227:${DB_PASSWORD}@postgres:5432/tobiichiGPT
|
||||
- WEBUI_AUTH=True
|
||||
@@ -65,22 +65,47 @@ services:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
||||
# NocoDB - 管理員回覆介面
|
||||
nocodb:
|
||||
image: nocodb/nocodb:latest
|
||||
container_name: tobiichiGPT-nocodb
|
||||
# Redis - Chatwoot 依賴
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: tobiichiGPT-redis
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
networks:
|
||||
- tobiichiGPT-network
|
||||
|
||||
# Chatwoot - 管理員對話介面
|
||||
chatwoot:
|
||||
image: chatwoot/chatwoot:latest
|
||||
container_name: tobiichiGPT-chatwoot
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "18080:8080"
|
||||
- "13000:3000"
|
||||
environment:
|
||||
- NC_DB=pg://postgres:5432?u=tobiichi3227&p=${DB_PASSWORD}&d=tobiichiGPT
|
||||
- NODE_ENV=production
|
||||
- REDIS_URL=redis://redis:6379
|
||||
- POSTGRES_HOST=postgres
|
||||
- POSTGRES_PORT=5432
|
||||
- POSTGRES_DATABASE=chatwoot
|
||||
- POSTGRES_USERNAME=tobiichi3227
|
||||
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
||||
- SECRET_KEY_BASE=${CHATWOOT_SECRET_KEY}
|
||||
- INSTALLATION_NAME=TobiichiGPT
|
||||
- FORCE_SSL=false
|
||||
- RAILS_LOG_TO_STDOUT=true
|
||||
volumes:
|
||||
- nocodb-data:/usr/app/data
|
||||
- chatwoot-data:/app/storage
|
||||
networks:
|
||||
- tobiichiGPT-network
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
- postgres
|
||||
- redis
|
||||
command: >
|
||||
sh -c "
|
||||
bundle exec rails db:chatwoot_prepare &&
|
||||
bundle exec rails s -b 0.0.0.0 -p 3000
|
||||
"
|
||||
|
||||
networks:
|
||||
tobiichiGPT-network:
|
||||
@@ -88,4 +113,5 @@ networks:
|
||||
name: tobiichiGPT-network # 固定網路名稱,讓 proxy stack 可以連接
|
||||
|
||||
volumes:
|
||||
nocodb-data:
|
||||
redis-data:
|
||||
chatwoot-data:
|
||||
|
||||
Reference in New Issue
Block a user