# Homelegance Chatbot 生产部署文档

**服务器**：AlmaLinux 8.10 | **Web 服务器**：Apache（与主站共用）  
**访问地址**：`https://www.homelegance.com/chat/`  
**部署路径**：`/redant/web/homelegance-chatbot`  
**数据库**：内网独立 PostgreSQL 服务器，Schema `chatbot`

---

## 目录

1. [环境要求](#1-环境要求)
2. [Node.js 安装](#2-nodejs-安装)
3. [代码部署](#3-代码部署)
4. [数据库初始化](#4-数据库初始化)
5. [环境变量配置](#5-环境变量配置)
6. [构建与启动](#6-构建与启动)
7. [PM2 进程管理](#7-pm2-进程管理)
8. [Apache 配置](#8-apache-配置)
9. [FastAPI ERP 桥接服务](#9-fastapi-erp-桥接服务)
10. [日常更新流程](#10-日常更新流程)
11. [部署验证清单](#11-部署验证清单)
12. [环境变量速查](#12-环境变量速查)

---

## 1. 环境要求

| 项目 | 详情 |
|------|------|
| 操作系统 | AlmaLinux 8.10 |
| Web 服务器 | Apache httpd（主站已有，共用） |
| 部署路径 | `/redant/web/homelegance-chatbot` |
| Node.js | v20.20.2 LTS |
| 包管理器 | pnpm v10.x |
| 进程管理 | PM2 |
| 数据库 | 内网独立 PostgreSQL 服务器，Schema `chatbot` |
| LLM API | Anthropic Claude API（`claude-sonnet-4-6`） |
| ERP 桥接 | Python FastAPI，端口 8080，仅本机访问 |

---

## 2. Node.js 安装

### 2.1 通过 NodeSource 安装 Node.js 20

```bash
# 需要服务器能访问外网（如有防火墙限制，先联系 IT 放行 nodesource.com）
curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash -
sudo dnf install -y nodejs --allowerasing
node --version   # 应输出 v20.x.x
```

> **注意**：安装前如有旧版 Node.js（v10），需先卸载：
> ```bash
> sudo dnf remove -y nodejs npm
> ```

### 2.2 安装 pnpm 和 PM2

```bash
npm install -g pnpm pm2
pnpm --version
pm2 --version
```

### 2.3 安装 Python 依赖（用于 ERP 桥接服务）

系统已有 Python 3.12，直接安装所需包：

```bash
python3.12 -V   # 确认 Python 3.12.x
pip3.12 install fastapi uvicorn asyncpg python-dotenv
```

---

## 3. 代码部署

### 3.1 克隆代码

```bash
sudo mkdir -p /redant/web/homelegance-chatbot
cd /redant/web
sudo git clone https://git.united-us.net/TonyT/chatbot.git homelegance-chatbot
sudo chown -R $USER:$USER homelegance-chatbot
```

### 3.2 安装依赖

```bash
cd /redant/web/homelegance-chatbot
pnpm install --frozen-lockfile
```

---

## 4. 数据库初始化

### 4.1 在内网 PostgreSQL 服务器上创建数据库和用户

```sql
-- 以管理员身份在 PostgreSQL 服务器执行
CREATE DATABASE homelegance_chat;
CREATE USER chatbot_user WITH PASSWORD '<强密码>';
GRANT ALL PRIVILEGES ON DATABASE homelegance_chat TO chatbot_user;

-- 连接到 homelegance_chat 库后创建 Schema
\c homelegance_chat
CREATE SCHEMA chatbot;
GRANT ALL ON SCHEMA chatbot TO chatbot_user;
```

### 4.2 执行 Drizzle 迁移

```bash
cd /redant/web/homelegance-chatbot

# 确保 .env.production 中 DATABASE_URL 已正确配置
export DATABASE_URL="postgresql://chatbot_user:<密码>@<内网PG地址>:5432/homelegance_chat"

pnpm db:push
```

> Drizzle 会自动在 `chatbot` schema 下创建所有表和枚举类型。

### 4.3 创建管理员账号

```bash
# 首次部署后，通过 /chat/register 页面注册第一个账号
# 然后在数据库中将其提升为 admin
psql -U chatbot_user -h <内网PG地址> -d homelegance_chat -c \
  "UPDATE chatbot.users SET role='admin' WHERE email='<管理员邮箱>';"
```

---

## 5. 环境变量配置

复制模板并填写真实值：

```bash
cp /redant/web/homelegance-chatbot/deploy/env.production.example \
   /redant/web/homelegance-chatbot/.env.production
nano /redant/web/homelegance-chatbot/.env.production
```

**`.env.production` 内容说明：**

```env
NODE_ENV=production
PORT=3000

# Chatbot 自身数据库（内网 PostgreSQL）
DATABASE_URL=postgresql://chatbot_user:<密码>@<内网PG地址>:5432/homelegance_chat

# JWT 签名密钥（随机生成64字符）
JWT_SECRET=<openssl rand -hex 32>

# Anthropic Claude API
ANTHROPIC_API_KEY=sk-ant-...

# ERP 桥接服务（内网，仅本机访问）
ERP_API_URL=http://127.0.0.1:8080
ERP_API_KEY=<内部随机密钥>

# Dealer Portal SSO 共享密钥
DEALER_PORTAL_SSO_SECRET=<与Portal约定的JWT签名密钥>
```

---

## 6. 构建与启动

```bash
cd /redant/web/homelegance-chatbot

# 构建前端（输出到 dist/public/）和后端（输出到 dist/index.js）
pnpm build

# 验证构建产物
ls dist/public/    # 应有 index.html、assets/ 等
ls dist/index.js   # 后端入口
```

---

## 7. PM2 进程管理

### 7.1 配置文件

`ecosystem.config.cjs` 已在项目根目录：

```js
module.exports = {
  apps: [{
    name: 'homelegance-chat',
    script: './dist/index.js',
    env_file: '.env.production',
    instances: 1,
    autorestart: true,
    max_memory_restart: '512M',
  }]
}
```

### 7.2 启动与自启

```bash
cd /redant/web/homelegance-chatbot
pm2 start ecosystem.config.cjs
pm2 save

# 配置系统启动时自动拉起（只需执行一次）
pm2 startup systemd
# 按提示执行输出的 sudo 命令
```

### 7.3 常用命令

```bash
pm2 list                        # 查看进程状态
pm2 logs homelegance-chat       # 查看实时日志
pm2 logs homelegance-chat --lines 100  # 查看最近100行日志
pm2 restart homelegance-chat    # 重启服务
pm2 stop homelegance-chat       # 停止服务
```

---

## 8. Apache 配置

### 8.1 说明

Chatbot 挂载在 **www.homelegance.com 主站的 `/chat/` 路径下**，无需新建 VirtualHost。将 `deploy/homelegance-chat.conf` 中的内容添加到主站配置文件的 `<VirtualHost *:443>` 块内即可。

### 8.2 主站配置文件位置

```bash
# 查找主站配置文件
grep -r "ServerName.*homelegance.com" /etc/httpd/conf.d/
```

### 8.3 添加配置

在主站 `<VirtualHost *:443>` 块内追加：

```apache
# ── Chatbot API：代理到 Node.js :3000 ─────────────────────────
# ProxyPass 必须在 Alias 之前声明
ProxyPreserveHost On
ProxyPass        /chat/api http://127.0.0.1:3000/api
ProxyPassReverse /chat/api http://127.0.0.1:3000/api

# ── Chatbot 静态文件：由 Apache 直接服务 ───────────────────────
Alias /chat /redant/web/homelegance-chatbot/dist/public
<Directory /redant/web/homelegance-chatbot/dist/public>
    Options -Indexes
    AllowOverride None
    Require all granted
</Directory>

# ── SPA fallback：/chat/* 非文件请求返回 index.html ───────────
RewriteEngine On
RewriteCond %{REQUEST_URI}       ^/chat/
RewriteCond %{REQUEST_URI}      !/chat/api/
RewriteCond %{REQUEST_FILENAME}  !-f
RewriteRule ^ /redant/web/homelegance-chatbot/dist/public/index.html [L]
```

### 8.4 重载 Apache

```bash
# 检查配置语法
sudo apachectl configtest

# 重载（不中断现有连接）
sudo systemctl reload httpd
```

### 8.5 确认所需模块已启用

```bash
httpd -M | grep -E "proxy|rewrite|alias"
# 应看到：proxy_module, proxy_http_module, rewrite_module, alias_module
```

如未启用：

```bash
sudo dnf install -y mod_proxy_http
# 在 /etc/httpd/conf.modules.d/ 中取消对应模块的注释
sudo systemctl restart httpd
```

---

## 9. FastAPI ERP 桥接服务

> **用途**：提供从 Chatbot 查询 ERP PostgreSQL 数据的只读接口，仅绑定 127.0.0.1，Apache 不代理此端口，外网不可达。

### 9.1 创建服务目录

```bash
sudo mkdir -p /redant/web/erp-bridge
sudo chown $USER:$USER /redant/web/erp-bridge
cd /redant/web/erp-bridge
```

### 9.2 创建 `.env` 文件

```bash
cat > /redant/web/erp-bridge/.env << 'EOF'
ERP_DATABASE_URL=postgresql://chatbot_readonly:<密码>@<ERP_PG地址>:5432/<ERP数据库名>
ERP_API_KEY=<与chatbot共享的内部随机密钥>
PORT=8080
EOF
```

### 9.3 创建 `main.py`

```python
from fastapi import FastAPI, HTTPException, Header, Depends
from contextlib import asynccontextmanager
from pydantic import BaseModel
from typing import Optional
import asyncpg, os
from dotenv import load_dotenv

load_dotenv()

DB_POOL = None
API_KEY = os.getenv("ERP_API_KEY")

@asynccontextmanager
async def lifespan(app: FastAPI):
    global DB_POOL
    DB_POOL = await asyncpg.create_pool(
        os.getenv("ERP_DATABASE_URL"), min_size=2, max_size=10
    )
    yield
    await DB_POOL.close()

app = FastAPI(lifespan=lifespan)

def verify_key(x_api_key: str = Header()):
    if x_api_key != API_KEY:
        raise HTTPException(status_code=401, detail="Unauthorized")

# 健康检查
@app.get("/health")
async def health():
    return {"status": "ok"}

# 产品目录查询
class CatalogQuery(BaseModel):
    category: Optional[str] = None
    model: Optional[str] = None
    description: Optional[str] = None
    limit: int = 20

@app.post("/catalog", dependencies=[Depends(verify_key)])
async def catalog(q: CatalogQuery):
    # 根据实际 ERP 表结构调整
    rows = await DB_POOL.fetch(
        "SELECT item_code, description, category FROM items WHERE "
        "($1::text IS NULL OR category ILIKE $1) AND "
        "($2::text IS NULL OR item_code ILIKE $2) "
        "LIMIT $3",
        f"%{q.category}%" if q.category else None,
        f"%{q.model}%" if q.model else None,
        q.limit
    )
    return [dict(r) for r in rows]

# 订单列表查询
class OrderQuery(BaseModel):
    so_id: Optional[str] = None
    customer_name: Optional[str] = None
    limit: int = 10

@app.post("/orders", dependencies=[Depends(verify_key)])
async def orders(q: OrderQuery):
    rows = await DB_POOL.fetch(
        "SELECT so_id, customer_name, status, order_date FROM sales_orders WHERE "
        "($1::text IS NULL OR so_id ILIKE $1) AND "
        "($2::text IS NULL OR customer_name ILIKE $2) "
        "ORDER BY order_date DESC LIMIT $3",
        f"%{q.so_id}%" if q.so_id else None,
        f"%{q.customer_name}%" if q.customer_name else None,
        q.limit
    )
    return [dict(r) for r in rows]

# 单个订单详情
@app.get("/orders/{so_id}", dependencies=[Depends(verify_key)])
async def order_detail(so_id: str):
    row = await DB_POOL.fetchrow(
        "SELECT * FROM sales_orders WHERE so_id = $1", so_id
    )
    if not row:
        raise HTTPException(status_code=404, detail="Order not found")
    items = await DB_POOL.fetch(
        "SELECT item_code, qty, price FROM sales_order_items WHERE so_id = $1", so_id
    )
    return {**dict(row), "items": [dict(i) for i in items]}

# 库存查询
class StockQuery(BaseModel):
    model: str
    limit: int = 20

@app.post("/stock", dependencies=[Depends(verify_key)])
async def stock(q: StockQuery):
    rows = await DB_POOL.fetch(
        "SELECT item_code, warehouse, qty_available FROM stock WHERE item_code ILIKE $1 LIMIT $2",
        f"%{q.model}%", q.limit
    )
    return [dict(r) for r in rows]
```

### 9.4 DBA 创建 ERP 只读账号

```sql
-- 在 ERP PostgreSQL 服务器执行
CREATE USER chatbot_readonly WITH PASSWORD '<密码>';
GRANT SELECT ON TABLE items, contacts, sales_orders, sales_order_items, stock
    TO chatbot_readonly;
```

### 9.5 Systemd 服务（`deploy/erp-bridge.service`）

```bash
sudo cp /redant/web/homelegance-chatbot/deploy/erp-bridge.service \
        /etc/systemd/system/erp-bridge.service
sudo systemctl daemon-reload
sudo systemctl enable --now erp-bridge
sudo systemctl status erp-bridge
```

**验证：**

```bash
curl http://127.0.0.1:8080/health
# 应返回：{"status":"ok"}
```

---

## 10. 日常更新流程

```bash
cd /redant/web/homelegance-chatbot

# 1. 拉取最新代码
git pull origin master

# 2. 更新依赖（仅新增包时需要）
pnpm install --frozen-lockfile

# 3. 重新构建
pnpm build

# 4. 重启 Node.js（Apache 静态文件无需重启）
pm2 restart homelegance-chat

# 5. 验证
pm2 logs homelegance-chat --lines 20
curl -I https://www.homelegance.com/chat/
```

---

## 11. 部署验证清单

| 检查项 | 命令/方式 | 预期结果 |
|--------|-----------|----------|
| Node.js 版本 | `node --version` | v20.x.x |
| pnpm 版本 | `pnpm --version` | v10.x.x |
| PM2 进程运行 | `pm2 list` | `homelegance-chat` online |
| Node.js 监听端口 | `ss -tlnp \| grep 3000` | 0.0.0.0:3000 |
| FastAPI 运行 | `curl http://127.0.0.1:8080/health` | `{"status":"ok"}` |
| Apache 配置语法 | `sudo apachectl configtest` | Syntax OK |
| 首页可访问 | `curl -I https://www.homelegance.com/chat/` | HTTP 200 |
| API 可访问 | `curl https://www.homelegance.com/chat/api/trpc` | JSON 响应 |
| 数据库连接 | `pm2 logs` 中无 DB 错误 | 无报错 |
| HTTPS 证书 | 浏览器访问 | 无证书警告 |

---

## 12. 环境变量速查

| 变量名 | 说明 | 示例 |
|--------|------|------|
| `NODE_ENV` | 运行环境 | `production` |
| `PORT` | Node.js 监听端口 | `3000` |
| `DATABASE_URL` | Chatbot 自身 PG 连接串 | `postgresql://chatbot_user:pwd@db:5432/homelegance_chat` |
| `JWT_SECRET` | JWT 签名密钥（64字符随机） | `openssl rand -hex 32` |
| `ANTHROPIC_API_KEY` | Claude API 密钥 | `sk-ant-...` |
| `ERP_API_URL` | ERP 桥接服务地址 | `http://127.0.0.1:8080` |
| `ERP_API_KEY` | ERP 桥接内部密钥 | 随机字符串 |
| `DEALER_PORTAL_SSO_SECRET` | Dealer Portal SSO 共享密钥 | HS256 签名密钥 |

---

## 附录

### Git 仓库

```
https://git.united-us.net/TonyT/chatbot.git
```

### deploy/ 目录文件说明

| 文件 | 用途 |
|------|------|
| `homelegance-chat.conf` | Apache 配置片段（添加到主站 VirtualHost） |
| `erp-bridge.service` | FastAPI ERP 桥接的 systemd 服务文件 |
| `env.production.example` | 环境变量模板 |
| `部署文档.md` | 本文档 |

### 目录结构

```
/redant/web/
├── homelegance-chatbot/      ← Chatbot 项目
│   ├── dist/
│   │   ├── public/           ← 前端静态文件（Apache 直接服务）
│   │   └── index.js          ← 后端入口（PM2 管理）
│   ├── .env.production       ← 环境变量（不提交 Git）
│   └── ecosystem.config.cjs  ← PM2 配置
└── erp-bridge/               ← FastAPI ERP 桥接服务
    ├── main.py
    └── .env
```
