# Triển khai chatbot lên VPS

Máy chủ web: `tradingauto.org`, đăng nhập `quang` bằng khoá
`~/.ssh/tradingauto_vps`. **Luôn SSH bằng tên miền, đừng ghi IP** — IP của
máy này là IP động.

Tệp trong web root thuộc `admin:admin` và để `644`, còn `quang` chỉ cùng
nhóm, nên mọi thao tác ghi đè đều phải qua `sudo`.

---

## 0. Điều kiện trước

| Thứ | Trạng thái lúc viết tài liệu (22/09/2026) |
|---|---|
| Python trên VPS | 3.12.3 — **chưa có** fastapi, uvicorn, httpx, pymysql, redis |
| Docker | chưa cài, và **không cần** |
| RAM | 7,7 GB, còn trống ~6,2 GB |
| Đĩa | còn trống 25 GB |
| Cổng 8787 | chưa có gì nghe |

Máy local là Python 3.10, VPS là 3.12. Mã viết theo cú pháp 3.10 nên chạy
được cả hai, nhưng **đừng dùng cú pháp mới của 3.12** khi sửa về sau.

---

## 1. Chép mã lên

Làm như mọi lần deploy khác của kho này: đóng gói ở máy local, `scp` sang
`/tmp`, rồi đặt vào chỗ bằng `sudo`.

```bash
cd /g/Xampp/htdocs
tar -czf /tmp/chatbot.tar.gz private/services/chatbot
scp -i ~/.ssh/tradingauto_vps /tmp/chatbot.tar.gz quang@tradingauto.org:/tmp/
```

Trên VPS:

```bash
cd /tmp && rm -rf chatbot_bung && mkdir chatbot_bung
tar -xzf chatbot.tar.gz -C chatbot_bung
W=/home/admin/web/tradingauto.org
sudo -n mkdir -p $W/private/services/chatbot
sudo -n cp -a chatbot_bung/private/services/chatbot/. $W/private/services/chatbot/
sudo -n chown -R admin:admin $W/private/services/chatbot
```

**Tệp local là LF hay CRLF?** Mã trong thư mục này viết bằng LF. Nếu về sau
sửa trên Windows rồi thấy Python báo lỗi cú pháp lạ, kiểm bằng
`file app.py` trước khi nghi ngờ mã.

---

## 2. Môi trường ảo

Cài vào **trong thư mục dịch vụ**, đúng đường dẫn mà `chatbot.service` trỏ tới:

```bash
cd /home/admin/web/tradingauto.org/private/services/chatbot
sudo -n -u admin python3 -m venv moitruong
sudo -n -u admin ./moitruong/bin/pip install --upgrade pip
sudo -n -u admin ./moitruong/bin/pip install -r requirements.txt
```

Giai đoạn 1 **không cài** `numpy` và `sentence-transformers`. Tìm kiếm chạy
thuần BM25 và như thế là đủ cho ~500 đoạn. Bật vector thì mới cài, nhưng
cân nhắc: `bge-m3` tải về khoảng 2 GB.

---

## 3. Dựng chỉ mục

```bash
cd /home/admin/web/tradingauto.org/private/services/chatbot
sudo -n -u admin ./moitruong/bin/python kho/chi_muc.py
```

Phải in ra số đoạn (dự kiến 400–700) và tệp `chi_muc.sqlite` phải xuất hiện.

> **Bước này phải lặp lại mỗi lần deploy nội dung website.** Sửa trang tài
> liệu mà quên dựng lại chỉ mục thì bot vẫn trả lời theo bản cũ — mà không
> có dấu hiệu gì báo là nó cũ. Ghép luôn vào kịch bản deploy của web.

---

## 4. Biến môi trường

Không viết khoá vào tệp unit — `systemctl cat` thì ai đọc cũng thấy.

```bash
sudo -n install -m 600 -o root -g root /dev/null /etc/tradingauto-chatbot.env
sudo -n tee /etc/tradingauto-chatbot.env >/dev/null <<'EOF'
CHATBOT_CONG=8787
CHATBOT_KHOA_NOI_BO=          # phải khớp CHAT_KHOA trong chat_config.php
CHATBOT_NHA_CUNG_CAP=rong     # 'rong' | 'anthropic' | 'openai'
NGAN_SACH_NGAY_USD=3.0
# ANTHROPIC_API_KEY=...
# LLM_GOC=...  LLM_KHOA=...  LLM_MODEL_DIEN_GIAI=...
# LLM_MODEL_PHAN_LOAI=...    (TUỲ CHỌN — xem ghi chú dưới)
# LLM_MODEL_DU_PHONG=...     (TUỲ CHỌN — model thử lại khi model chính hỏng hạ tầng)
EOF
```

Biến `LLM_GOC`/`LLM_KHOA`/`LLM_MODEL_DIEN_GIAI` chỉ bắt buộc khi
`CHATBOT_NHA_CUNG_CAP=openai`. `LLM_MODEL_PHAN_LOAI` là TUỲ CHỌN — không
đặt thì tự dùng chung giá trị của `LLM_MODEL_DIEN_GIAI` (nhánh gọi mô hình
để phân loại ý định chưa được bật ở giai đoạn này, `nao/tra_loi.py` phân
loại hoàn toàn bằng luật, xem `nao/dinh_tuyen.py`). `LLM_MODEL_DU_PHONG`
cũng TUỲ CHỌN — đặt thì khi model chính gặp lỗi HẠ TẦNG (timeout, lỗi
mạng, HTTP 5xx/429/403, hoặc phản hồi rỗng) lớp sẽ tự thử lại ĐÚNG MỘT LẦN
bằng model này; không đặt thì hỏng là chuyển người ngay như trước.

**Bật `CHATBOT_NHA_CUNG_CAP=rong` cho lần chạy đầu.** Nó trả lời bằng chính
đoạn tài liệu tìm được, không gọi mô hình, không tốn tiền. Xác nhận đường
đi thông suốt rồi hẵng cắm khoá thật.

**Trước khi đổi sang `CHATBOT_NHA_CUNG_CAP=openai`, PHẢI chạy
`thu/kiem_cong.py` bằng tay trước** (tốn tiền thật, không chạy tự động
trong CI):

```bash
cd /home/admin/web/tradingauto.org/private/services/chatbot
export LLM_GOC=...  LLM_KHOA=...  LLM_MODEL_DIEN_GIAI=...  LLM_MODEL_DU_PHONG=...
sudo -n -u admin ./moitruong/bin/python thu/kiem_cong.py
```

Lý do bắt buộc bước này: `GET /v1/models` của cổng liệt kê được rất nhiều
model, nhưng đó KHÔNG PHẢI danh sách được phép gọi — bảng giá mới là danh
sách thật, và một model bị chặn chỉ lộ ra là HTTP 403 lúc GỌI THẬT, không
lộ ra lúc soát danh sách model. `thu/kiem_cong.py` để lỗi 403 đó lộ ra khi
người triển khai đang ngồi đọc terminal, không phải lúc khách đang chat.

Số đo thật trên cổng `hhtechapi.net` (đo ngày 22/09/2026, prompt ~2.400
token vào): `deepseek-v4-flash` 3.582 ms, `deepseek-v4-pro` 4.617 ms,
`claude-haiku-4.5` 6.190 ms, `glm-5.3-flash` 6.157 ms — cả bốn trả đúng
field `model` khớp cái gửi đi, không thấy tráo model. Toàn bộ dòng Gemini
text bị chặn kiểu 403 "không có trong bảng giá hiện hành" trên khoá đã đo.
Từ số đo này, KHUYẾN NGHỊ cấu hình:

```
LLM_MODEL_DIEN_GIAI=deepseek-v4-flash
LLM_MODEL_DU_PHONG=claude-haiku-4.5
```

(nhanh nhất làm model chính, một model khác hãng hẳn làm dự phòng để một
sự cố ở nhà cung cấp gốc của model chính không kéo sập luôn cả model dự
phòng). Số đo trên có thể đổi theo thời gian — `thu/kiem_cong.py` mới là
nguồn xác nhận tại THỜI ĐIỂM triển khai, số đo ở đây chỉ để tham khảo.

---

## 5. Dịch vụ

```bash
sudo -n cp /home/admin/web/tradingauto.org/private/services/chatbot/trienkhai/chatbot.service \
           /etc/systemd/system/chatbot.service
sudo -n systemctl daemon-reload
sudo -n systemctl enable --now chatbot
sudo -n systemctl is-active chatbot
sudo -n journalctl -u chatbot --since "-60 s" -o cat
```

Kiểm nhanh:

```bash
curl -s http://127.0.0.1:8787/khoe | head -c 400
curl -s -X POST http://127.0.0.1:8787/chat \
     -H 'Content-Type: application/json' \
     -d '{"session_id":"thu","message":"cài EA vào MT5 thế nào","ngon_ngu":"vi"}'
```

---

## 6. Sửa `chat.php` — hai trường mới

`public/api/chat.php` hiện chỉ gửi `session_id` và `message`. Bot cần thêm:

- `nguoi_dung` — không có thì **không tra được key của khách**; biến
  `$nguoi_dung` đã tính sẵn ngay phía trên chỗ gọi curl, chỉ là chưa gửi đi.
- `ngon_ngu` — không có thì bot luôn trả lời tiếng Việt kể cả khi khách
  đang đọc bản tiếng Anh.

Cả hai đều **tuỳ chọn** ở phía Python (có mặc định), nên thứ tự deploy
không quan trọng: đẩy bên nào trước cũng không gãy.

---

## 7. Bật cho khách

`private/app/lib/chat_config.php` đang để `CHAT_BAT = true`, tức widget đã
hiện trên mọi trang và đang trả câu dự phòng Zalo vì chưa có gì nghe ở
8787. Khi dịch vụ chạy là khách dùng được ngay, **không phải bật gì thêm** —
nhớ điều này, nếu không sẽ vô tình cho khách dùng bản chưa kiểm.

Muốn giữ kín trong lúc thử thì đặt `CHAT_BAT = false` trước, kiểm bằng
`curl` nội bộ, xong mới bật lại.

---

## 8. Lùi lại khi hỏng

```bash
sudo -n systemctl stop chatbot
```

Chỉ vậy. Widget tự quay về câu mời nhắn Zalo — `chat.php` đã xử lý sẵn
trường hợp không gọi được cổng. Khách không thấy trang vỡ, không thấy lỗi.

Đây là lý do đáng giá nhất của việc giữ `chat.php` làm lớp đệm thay vì cho
trình duyệt gọi thẳng dịch vụ Python.
