Update chatwoot docker

This commit is contained in:
ChenKaiLiuG
2025-12-21 16:49:44 +08:00
parent 13a6b1ed0e
commit ccb0202579
5 changed files with 152 additions and 141 deletions

5
.env.example Normal file
View 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

View File

@@ -54,11 +54,13 @@
```powershell ```powershell
# 複製主服務環境變數 # 複製主服務環境變數
Copy-Item .env.example .env Copy-Item .env.example .env
notepad .env # 設定 DB_PASSWORD notepad .env # 設定 DB_PASSWORD 和 CHATWOOT_SECRET_KEY
# 如需代理服務 # 生成 Chatwoot Secret Key (在 Linux/macOS)
Copy-Item .env.proxy .env.proxy openssl rand -hex 64
notepad .env.proxy # 填入 CLOUDFLARE_TUNNEL_TOKEN
# 或在 PowerShell 生成
-join ((1..128) | ForEach-Object { '{0:x}' -f (Get-Random -Maximum 16) })
``` ```
### 2. 啟動主要服務 ### 2. 啟動主要服務
@@ -90,9 +92,9 @@ docker-compose -f docker-compose.proxy.yml ps
| 服務 | 網址 | 說明 | | 服務 | 網址 | 說明 |
|------|------|------| |------|------|------|
| **Open WebUI** | http://localhost:3000 | 用戶對話介面 | | **Open WebUI** | http://localhost:10060 | 用戶對話介面 |
| **API 伺服器** | http://localhost:8000 | OpenAI API 相容端點 | | **API 伺服器** | http://localhost:18000 | OpenAI API 相容端點 |
| **NocoDB** | http://localhost:8080 | 管理員回覆介面 | | **Chatwoot** | http://localhost:13500 | 管理員對話介面 |
| **PostgreSQL** | localhost:5432 | 資料庫 | | **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 ### 設定 Open WebUI
1. 開啟 http://localhost:3000 1. 開啟 http://localhost:10060
2. 進入 **Settings****Connections** 2. 進入 **Settings****Connections**
3. 新增 OpenAI Connection: 3. 新增 OpenAI Connection:
- **API Base URL**: `http://api:8000/v1` - **API Base URL**: `http://tobiichiGPT-api:8000/v1`
- **API Key**: `sk-human` (任意值) - **API Key**: `sk-human` (任意值)
4. 模型列表會出現 **human-admin** 4. 模型列表會出現 **human-admin**
### 管理員回覆 ### 管理員回覆流程
1. 訪問 NocoDB: http://localhost:8080 1. 登入 Chatwoot: http://localhost:13500
2. 連接到 PostgreSQL: 2. 在對話列表查看新訊息
- Host: `postgres` 3. 直接回覆即可,用戶會在 3 秒內收到
- 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
用戶收到回覆
```
## 🔧 技術架構 ## 🔧 技術架構

View File

@@ -1,5 +1,4 @@
fastapi==0.104.1 fastapi==0.104.1
uvicorn==0.24.0 uvicorn==0.24.0
pydantic==2.5.0 pydantic==2.5.0
psycopg2-binary==2.9.9
asyncpg==0.29.0 asyncpg==0.29.0

View File

@@ -2,7 +2,8 @@
API 轉接層 - 偽裝 OpenAI API將請求轉為人工回覆隊列 API 轉接層 - 偽裝 OpenAI API將請求轉為人工回覆隊列
""" """
from fastapi import FastAPI from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from pydantic import BaseModel from pydantic import BaseModel
import asyncpg import asyncpg
import asyncio import asyncio
@@ -74,134 +75,130 @@ async def startup():
async def shutdown(): async def shutdown():
if db_pool: if db_pool:
await db_pool.close() await db_pool.close()
print("👋 資料庫連接已關閉")
@app.get("/") @app.get("/")
async def root(): async def root():
return { """根路徑"""
"status": "TobiichiGPT API Running", return {"status": "ok", "service": "TobiichiGPT API"}
"database": "Connected" if db_pool else "Disconnected"
}
@app.get("/v1/models") @app.get("/v1/models")
async def list_models(): async def list_models():
"""模擬 OpenAI 模型列表""" """模擬 OpenAI 的 /v1/models 端點"""
return { return {
"object": "list", "object": "list",
"data": [{ "data": [
"id": "human-admin", {
"object": "model", "id": "human-admin",
"created": int(time.time()), "object": "model",
"owned_by": "tobiichi" "created": int(time.time()),
}] "owned_by": "tobiichi",
"permission": [],
"root": "human-admin",
"parent": None
}
]
} }
@app.post("/v1/chat/completions") @app.post("/v1/chat/completions")
async def chat_completions(request: ChatRequest): async def chat_completions(request: ChatRequest):
"""接收用戶訊息,等待管理員回覆""" """
模擬 OpenAI Chat Completions API
# 生成對話 ID 將用戶訊息寫入資料庫,等待管理員回覆
conv_id = str(uuid.uuid4())[:12] """
# 取得最後一則用戶訊息
# 提取用戶訊息 user_message = None
user_message = ""
for msg in reversed(request.messages): for msg in reversed(request.messages):
if msg.role == "user": if msg.role == "user":
user_message = msg.content user_message = msg.content
break break
if not user_message: if not user_message:
return { return JSONResponse(
"id": f"chatcmpl-{conv_id}", status_code=400,
"object": "chat.completion", content={"error": "No user message found"}
"created": int(time.time()), )
"model": request.model,
"choices": [{ # 生成對話 ID
"index": 0, conversation_id = str(uuid.uuid4())
"message": {
"role": "assistant",
"content": "無法識別您的訊息,請重新輸入。"
},
"finish_reason": "stop"
}]
}
# 寫入資料庫 # 寫入資料庫
async with db_pool.acquire() as conn: async with db_pool.acquire() as conn:
await conn.execute(""" await conn.execute(
"""
INSERT INTO reply_queue (conversation_id, user_message, status) INSERT INTO reply_queue (conversation_id, user_message, status)
VALUES ($1, $2, 'pending') 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 分鐘 # 等待管理員回覆 (最多 15 分鐘)
timeout = 900 max_wait = 900 # 15 分鐘
start_time = time.time() check_interval = 3 # 每 3 秒檢查一次
reply_content = None waited = 0
while time.time() - start_time < timeout: while waited < max_wait:
await asyncio.sleep(check_interval)
waited += check_interval
async with db_pool.acquire() as conn: async with db_pool.acquire() as conn:
row = await conn.fetchrow(""" row = await conn.fetchrow(
SELECT admin_reply, status "SELECT admin_reply, status FROM reply_queue WHERE conversation_id = $1",
FROM reply_queue conversation_id
WHERE conversation_id = $1 )
""", conv_id)
if row and row['status'] == 'replied' and row['admin_reply']: if row and row['status'] == 'replied' and row['admin_reply']:
reply_content = row['admin_reply'] admin_reply = row['admin_reply']
print(f"[已回覆 {conv_id}] {reply_content[:50]}...") print(f"✅ 管理員已回覆 [{conversation_id}]")
break
# 回傳 OpenAI 格式的回應
await asyncio.sleep(3) # 每 3 秒檢查一次 return {
"id": f"chatcmpl-{conversation_id}",
# 如果超時 "object": "chat.completion",
if not reply_content: "created": int(time.time()),
reply_content = "抱歉,管理員暫時無法回覆,請稍後再試。" "model": request.model,
async with db_pool.acquire() as conn: "choices": [
await conn.execute(""" {
UPDATE reply_queue "index": 0,
SET status = 'timeout' "message": {
WHERE conversation_id = $1 "role": "assistant",
""", conv_id) "content": admin_reply
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": len(user_message),
"completion_tokens": len(admin_reply),
"total_tokens": len(user_message) + len(admin_reply)
}
}
# 超時回應
print(f"⏰ 等待超時 [{conversation_id}]")
return { return {
"id": f"chatcmpl-{conv_id}", "id": f"chatcmpl-{conversation_id}",
"object": "chat.completion", "object": "chat.completion",
"created": int(time.time()), "created": int(time.time()),
"model": request.model, "model": request.model,
"choices": [{ "choices": [
"index": 0, {
"message": { "index": 0,
"role": "assistant", "message": {
"content": reply_content "role": "assistant",
}, "content": "抱歉,管理員目前忙碌中,請稍後再試。"
"finish_reason": "stop" },
}], "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__": if __name__ == "__main__":
import uvicorn 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) uvicorn.run(app, host="0.0.0.0", port=8000)

View File

@@ -53,7 +53,7 @@ services:
container_name: tobiichiGPT-ui container_name: tobiichiGPT-ui
restart: unless-stopped restart: unless-stopped
ports: ports:
- "13000:3000" - "10060:8080"
environment: environment:
- DATABASE_URL=postgresql://tobiichi3227:${DB_PASSWORD}@postgres:5432/tobiichiGPT - DATABASE_URL=postgresql://tobiichi3227:${DB_PASSWORD}@postgres:5432/tobiichiGPT
- WEBUI_AUTH=True - WEBUI_AUTH=True
@@ -65,22 +65,47 @@ services:
postgres: postgres:
condition: service_healthy condition: service_healthy
# NocoDB - 管理員回覆介面 # Redis - Chatwoot 依賴
nocodb: redis:
image: nocodb/nocodb:latest image: redis:7-alpine
container_name: tobiichiGPT-nocodb 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 restart: unless-stopped
ports: ports:
- "18080:8080" - "13000:3000"
environment: 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: volumes:
- nocodb-data:/usr/app/data - chatwoot-data:/app/storage
networks: networks:
- tobiichiGPT-network - tobiichiGPT-network
depends_on: depends_on:
postgres: - postgres
condition: service_healthy - redis
command: >
sh -c "
bundle exec rails db:chatwoot_prepare &&
bundle exec rails s -b 0.0.0.0 -p 3000
"
networks: networks:
tobiichiGPT-network: tobiichiGPT-network:
@@ -88,4 +113,5 @@ networks:
name: tobiichiGPT-network # 固定網路名稱,讓 proxy stack 可以連接 name: tobiichiGPT-network # 固定網路名稱,讓 proxy stack 可以連接
volumes: volumes:
nocodb-data: redis-data:
chatwoot-data: