# Saber 与 Saber-AI 部署指南

Saber-AI 作为独立产物与宿主 Saber 部署在同一个站点下，由宿主在运行时按注册表加载。部署涉及三样东西：宿主产物、AI 产物、注册表 `remotes.json`。宿主与 AI 各自构建、各自上传，注册表把两者关联起来；升级、回滚、下线都只改注册表，不重新构建宿主，不重启 Nginx。

## 1. 整体结构

```text
/srv/saber/                              宿主产物（Saber 的 dist/）
/srv/saber/remotes.json                  注册表
/srv/micro/ai/releases/2.0.0/            AI 产物（Saber-AI 的 dist/，整个目录）
/srv/micro/share/ai  →  ../ai/releases/2.0.0/share    分享页面的软链接，指向线上版本
```

AI 产物按版本号分目录，旧版本保留：切换版本后尚未刷新页面的用户仍能从旧目录加载资源，回滚只需把注册表指回去。

文中出现的路径分两类。磁盘路径（如 `/srv/micro/`）只有服务器和 Nginx 知道，放在哪里可以自行决定；URL 路径（如 `/micro/ai/releases/2.0.0/mf-manifest.json`）是浏览器请求的地址，写在注册表和 AI 的构建配置里。两者由 Nginx 的 `alias` 对应：URL `/micro/...` 落到磁盘 `/srv/micro/...`。更换磁盘目录只需改 `alias`，注册表与 AI 配置不受影响。

## 2. 宿主 Saber

宿主的配置不需要为 AI 修改，`.env.production` 保持 `VITE_APP_API=/api`、`VITE_APP_BASE=/`、`VITE_APP_REMOTES=remotes.json`。

1. 构建并上传：

   ```bash
   cd Saber
   pnpm run build
   rsync -a --exclude remotes.json dist/ user@host:/srv/saber/
   ```

   构建产物里带有一份空清单的 `remotes.json`，上传时必须排除，否则会覆盖服务器上已配置的注册表。

2. 在服务器上写 `/srv/saber/remotes.json`：

   ```json
   {
     "schema": 1,
     "contract": "1",
     "remotes": [
       {
         "code": "ai",
         "name": "AI 大模型",
         "routePrefix": "/ai",
         "version": "2.0.0",
         "entry": "/micro/ai/releases/2.0.0/mf-manifest.json",
         "enabled": true,
         "sort": 1
       }
     ]
   }
   ```

   `entry` 是 URL 路径，须以 `/` 开头并与 AI 的上传目录对应。宿主只接受同源地址，写成 `http://...` 的条目会被丢弃。

3. 后端执行 AI 的菜单脚本 `doc/sql/03-bladex.mysql.ai.ext.create.sql` 并给账号授权。宿主只在菜单中出现 `/ai/**` 路径时才加载 AI，未授权的用户不会下载 AI 代码。

## 3. Saber-AI

1. `.env.production`：

   - `VITE_APP_BASE=/micro/ai/releases/2.0.0/`，URL 路径，决定产物内部所有资源的引用地址，须与上传目录对应，升版本号时同步修改。
   - `VITE_APP_PROXY_TARGET` 留空。它只用于后端回调本平台的地址，不是接口代理；接口一律经站点 `/api`。

2. 构建：

   ```bash
   cd Saber-AI
   GIT_COMMIT=$(git rev-parse --short HEAD) pnpm run build
   ```

   `GIT_COMMIT` 写入产物的构建标识，排障时可对应到提交。

3. 上传整个 `dist/`（含 `share/` 与 `fonts/`）：

   ```bash
   rsync -a dist/ user@host:/srv/micro/ai/releases/2.0.0/
   ```

   版本目录只新建不覆盖，同一版本号需要重发时改版本号再发。

4. 建立分享页面的软链接：

   ```bash
   ssh user@host 'mkdir -p /srv/micro/share && ln -sfn ../ai/releases/2.0.0/share /srv/micro/share/ai'
   ```

   分享页面与嵌入脚本的地址固定为 `/share/ai/...`，第三方站点引用后不随版本变化，软链接把固定地址指向当前版本。

## 4. Nginx

以下配置可直接使用，替换域名与后端地址即可。

```nginx
server {
    listen 80;
    server_name saber.example.com;
    root /srv/saber;

    # 知识库文档、创作素材经 /api 上传，Nginx 默认 1m 会直接拒绝
    client_max_body_size 100m;

    # 宿主页面：前端路由刷新时回落到 index.html
    location / {
        try_files $uri $uri/ /index.html;
    }
    location = /index.html {
        add_header Cache-Control "no-cache";
    }

    # 运行期配置：部署时会修改，禁止缓存
    location = /remotes.json {
        add_header Cache-Control "no-cache";
    }
    location = /compatibility-matrix.json {
        add_header Cache-Control "no-cache";
    }

    # AI 产物：缺失返回 404，不能回落到 index.html，
    # 否则浏览器把 HTML 当作 JS 执行，报出的语法错误会误导排查
    location /micro/ {
        alias /srv/micro/;
        try_files $uri =404;

        location ~ /(mf-manifest\.json|mf-stats\.json|remoteEntry\.js)$ {
            add_header Cache-Control "no-cache";
        }
        location ~ /assets/ {
            add_header Cache-Control "public, max-age=31536000, immutable";
        }
    }

    # AI 分享页面：页面路由回落到 share.html，静态资源缺失同样 404
    location ~ ^/share/(?<share_code>[a-z][a-z0-9]*)/ {
        root /srv/micro;
        try_files $uri /share/$share_code/share.html;

        location ~ /(share\.html|embed\.js)$ {
            add_header Cache-Control "no-cache";
        }
        location ~ /assets/ {
            add_header Cache-Control "public, max-age=31536000, immutable";
        }
    }

    # AI 接口：对话、运行、生成为 SSE 流式输出，须关闭代理缓冲并放宽读超时，
    # 否则用户看到的是整段一次出现，长时间生成会被 504 断开
    location /api/blade-ai/ {
        proxy_pass http://127.0.0.1:80/blade-ai/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_buffering off;
        proxy_read_timeout 600s;
    }

    # 其余后端接口：去掉 /api 前缀后转给网关（Cloud）或 Boot 单体
    location /api/ {
        proxy_pass http://127.0.0.1:80/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
```

HTTPS 按常规加 `listen 443 ssl` 与证书，其余不变。

宿主仓库的 `deploy/nginx/saber.conf.template` 与上述配置基本一致，缺少 `client_max_body_size` 与 `/api/blade-ai/` 两段。这两段对 AI 是必要的，建议补进模板，原生 Nginx 与 Docker 两种形态共用这一份。

## 5. 上线核对

```bash
curl -s https://<站点>/remotes.json                                # 含 ai 条目，enabled 为 true
curl -I https://<站点>/micro/ai/releases/2.0.0/mf-manifest.json    # 200
curl -I https://<站点>/micro/ai/releases/2.0.0/nope.js             # 404；若为 200 说明回落到了 index.html
curl -I https://<站点>/share/ai/embed.js                           # 200
```

浏览器登录宿主：侧栏出现大模型菜单，进入 `/ai/dashboard` 后刷新页面仍在原处，发起对话可见逐字输出。

## 6. 升级、回滚与下线

| 操作 | 做法 |
| --- | --- |
| 升级 AI | 改版本号重新构建，上传到新的版本目录，注册表的 `version` 与 `entry` 改为新版本，软链接指向新目录 |
| 回滚 | 注册表改回旧版本，软链接指回旧目录；旧目录一直保留，回滚即时生效 |
| 临时下线 | 注册表中 `enabled` 改为 `false` |
| 升级宿主 | 重新构建并上传 `/srv/saber/`，同样排除 `remotes.json` |

以上操作均不需要重新构建另一方，也不需要重启 Nginx，用户刷新页面即生效。

## 7. 部署脚本

宿主仓库的 `deploy/saber.sh` 把构建、上传、软链接、健康检查、修改注册表封装为子命令。在 `deploy/.env` 配好服务器地址后，日常发版一条命令：

```bash
deploy/saber.sh release ai ../Saber-AI
```

它依次完成构建、上传到版本目录、健康检查，确认后切换注册表并下发。原理与本文一致，手工操作与脚本可以混用。
