# KOC LOOP 私有化部署指南

本文适用于 `main` 分支。目标架构是运维提出的：Nginx 代理 + KOC 服务 + MySQL 数据库，并让后台、外部 KOC 领取页和 Agent MCP 都能通过一个公网域名访问。

## 1. 部署形态

| 组件 | 容器 | 作用 | 持久化 |
| --- | --- | --- | --- |
| Nginx | `nginx` | 公网入口、反向代理、托管 KOC 领取页 | 配置随镜像 |
| KOC 服务 | `app` | Next.js 后台、API、MCP、每天 09:00 自动采集 | 上传目录挂载卷 |
| MySQL 8 | `mysql` | 任务、笔记、领取、回填、账号、采集和用户数据 | MySQL 数据卷 |

访问路径：

- `https://你的域名/`：运营后台；
- `https://你的域名/koc/`：外部 KOC 领取和回填；
- `https://你的域名/api/mcp`：Agent MCP；
- `https://你的域名/api/health`：服务健康检查。

第一版按单实例 KOC 服务设计。MySQL 和上传目录均持久化。以后需要横向扩容时，可以把上传卷换成共享 NAS；对象存储接口已与业务代码分离。

## 2. 服务器要求

- Linux 服务器一台，建议至少 4 核、8 GB 内存、100 GB 数据盘；
- Docker Engine 24+；
- Docker Compose v2；
- 可解析到服务器或负载均衡的公网域名；
- HTTPS 证书由公司网关、负载均衡或 Nginx 统一终止；
- 服务器可以访问飞书 OpenAPI 和正式数据采集 MCP。

服务器只需对公网开放 80/443。MySQL 不映射公网端口。

## 3. 准备代码与环境变量

```bash
git clone ssh://git@gta.gbotai.cn:42001/wufengping/koc-loop.git
cd koc-loop
git checkout main
cp .env.self-hosted.example .env.self-hosted
```

编辑 `.env.self-hosted`。必须替换所有 `replace-with-*` 占位值：

| 变量 | 用途 |
| --- | --- |
| `MYSQL_ROOT_PASSWORD` | MySQL root 密码，仅数据库容器使用 |
| `MYSQL_PASSWORD` | KOC 服务的数据库密码 |
| `APP_ORIGIN` | 后台公网地址，如 `https://koc.example.com` |
| `KOC_PORTAL_URL` | 领取页完整地址，如 `https://koc.example.com/koc/` |
| `SUPER_ADMIN_USERNAME` | 首次启动创建唯一超级管理员 |
| `SUPER_ADMIN_PASSWORD` | 首次启动的超级管理员初始密码，至少 8 位 |
| `ADMIN_INTERNAL_TOKEN` | 内部管理调用密钥 |
| `KOC_MCP_API_KEY` | Agent 调用 KOC LOOP MCP 的独立 Bearer 密钥 |
| `FEISHU_APP_ID` / `FEISHU_APP_SECRET` | 读取飞书内容表和配图 |
| `AI_TOOL_CENTER_MCP_URL` / `AI_TOOL_CENTER_MCP_KEY` | 小红书公开数据采集服务 |
| `ENABLE_SCHEDULER` | 是否启用每天 09:00 自动采集，生产保持 `true` |

密钥必须由密码管理器生成，禁止提交到 Git、聊天、部署日志或 URL。三个业务密钥 `ADMIN_INTERNAL_TOKEN`、`KOC_MCP_API_KEY`、`AI_TOOL_CENTER_MCP_KEY` 不得复用。

`APP_ORIGIN` 必须填写用户实际访问的 HTTPS 公网地址，不能填写 `localhost`、`app:3000` 或其他容器内部地址。Excel 中的视频下载链接会优先使用这个地址；前置网关还必须把原始 `Host`、`X-Forwarded-Host` 和 `X-Forwarded-Proto` 传给仓库内的 Nginx。

## 4. 首次启动

```bash
docker compose --env-file .env.self-hosted \
  -f docker-compose.self-hosted.yml up -d --build
```

应用容器启动时会先执行同一套数据库迁移脚本，成功后才启动 KOC 服务。查看状态：

```bash
docker compose --env-file .env.self-hosted \
  -f docker-compose.self-hosted.yml ps

curl -fsS http://127.0.0.1:${HTTP_PORT:-80}/api/health
```

健康检查应返回 `status: ok` 和 `database: true`。

首次打开后台登录页时，系统会根据环境变量创建唯一超级管理员。创建成功后，可从运行环境移除 `SUPER_ADMIN_PASSWORD` 的明文值并重启应用；后续账号与密码统一在“用户管理”中维护。

## 5. Nginx 与 HTTPS

仓库内 `deploy/nginx/koc-loop.conf` 默认监听容器 80 端口，适合前置公司网关或负载均衡终止 HTTPS。

如果证书直接挂在本机 Nginx：

1. 把证书和私钥以只读卷挂载进 `nginx` 容器；
2. 增加 443 `listen ... ssl` 配置；
3. 80 端口只做 301 跳转；
4. 保留 `/koc/` 静态规则、`/api/mcp` 长连接规则和 `/` 反向代理规则。

MCP 路由已经关闭代理缓冲并将超时时间延长到 1 小时，避免 Agent 的长调用被 Nginx 提前截断。仓库内 Nginx 同时将上传限制设为 85 MB，用于接收最多 80 MB 的批量回填 Excel；公司网关或负载均衡的请求体限制也必须不低于 85 MB。

视频下载接口必须经过 `/api/partner-image` 反向代理，正常响应应包含 `Content-Type: video/mp4` 和带 `.mp4` 文件名的 `Content-Disposition: attachment`。不要在网关层改写该响应类型或移除附件响应头。

## 6. 迁移原 Sites 数据

迁移分为数据库与图片两部分。先在原生产站点保持只读窗口，完成导出后再切换域名，避免新旧系统同时写入。

### 6.1 D1 数据导入 MySQL

把 D1 各表导出成一个 JSON 文件，结构如下：

```json
{
  "tables": {
    "partners": [{ "id": "...", "name": "..." }],
    "tasks": [{ "id": "...", "name": "..." }],
    "contents": [],
    "accounts": [],
    "claims": [],
    "delegation_bundles": [],
    "distributions": [],
    "collection_runs": [],
    "users": [],
    "auth_sessions": [],
    "mcp_export_tokens": []
  }
}
```

先确保 MySQL 迁移已完成，再在应用环境中运行：

```bash
npm run db:import-json -- /backup/koc-d1-export.json
```

导入脚本按业务依赖顺序写入，并使用主键/唯一键安全更新已有记录。正式迁移前先在测试库演练并核对任务数、笔记数、领取数、发布数和账号数。

### 6.2 R2 媒体文件导入

把 R2 按原对象 key 导出到一个目录，目录层级必须保留，例如：

```text
content-assets/...
content-videos/...
publish-evidence/...
creator-center/...
```

运行：

```bash
UPLOAD_DIR=/data/koc/uploads \
  npm run storage:import -- /backup/koc-r2-export
```

脚本会复制文件，并为缺少元数据的媒体文件生成 Content-Type 元数据。容器部署时也可以在宿主机临时挂载 `upload_data` 卷后执行。

## 7. 上线验收

必须逐项验证：

1. 超级管理员可以登录，普通用户看不到 KOC 资源模块；
2. 用真实飞书表格创建一个 1 篇测试任务；
3. 返回的领取链接以 `/koc/?task=` 开头；
4. 手机公网打开领取页，能查看正文与配图；
5. 回填短链/长链、上传发布截图、刷新后记录仍在；
6. 上传创作者截图并填写曝光量、阅读量；
7. 后台立即采集一篇笔记成功；
8. 保存次日采集计划，确认数据库产生 `collection_runs`；
9. 图文任务导出的 Excel 内能直接看到原图和截图；
10. 视频任务导出的 Excel 不含“图片”列，包含“视频”列，点击链接能下载扩展名为 `.mp4` 且可正常播放的文件；
11. 批量回填 Excel 可以上传，发布链接、笔记截图和单篇笔记数据分析截图均能正确回写；
12. Agent 用 `KOC_MCP_API_KEY` 调用 `/api/mcp` 能发现全部工具；
13. 重启全部容器后数据、图片和视频不丢失。

## 8. 备份与恢复

每天至少备份：

- MySQL：`mysqldump --single-transaction`；
- `mysql_data` 卷；
- `upload_data` 卷；
- 当前 Git 提交号和脱敏后的环境变量清单。

备份必须复制到另一台机器或对象存储，不能只保存在部署服务器。恢复演练至少每季度一次。

## 9. 升级与回滚

升级前先备份数据库和上传卷：

```bash
git pull
docker compose --env-file .env.self-hosted \
  -f docker-compose.self-hosted.yml up -d --build
```

应用容器每次启动都会按文件名顺序执行尚未应用的 `mysql/*.sql`。本次版本包含平台/视频字段、账号性别/简介/标签以及“当前联系人”字段的增量迁移；升级后应检查容器日志确认 `0005`、`0006`、`0007` 已执行或已被识别为历史迁移。

数据库迁移只允许向前追加新的 `mysql/*.sql` 文件，禁止修改已经在生产执行过的迁移。应用回滚到旧镜像前，要确认旧代码兼容当前数据库结构；涉及不可逆结构变化时，必须同时准备数据库恢复方案。

## 10. 运维排查

| 现象 | 处理 |
| --- | --- |
| `/api/health` 返回 503 | 查看 MySQL 容器健康状态与应用数据库变量 |
| 登录页可开但登录失败 | 确认迁移完成、超级管理员变量仅用于初始化 |
| 配图或截图 404 | 检查 `upload_data` 卷和 `UPLOAD_DIR=/data/koc/uploads` |
| Excel 视频链接出现 localhost 或无法访问 | 检查 `APP_ORIGIN`、公网域名和网关转发的 Host/Proto 请求头 |
| 视频下载后不是 MP4 或无法播放 | 检查 `/api/partner-image` 是否经过应用代理、文件是否完整，以及网关是否保留 Content-Type/Content-Disposition |
| 批量回填表上传返回 413 | 将公司网关、负载均衡和 Nginx 的请求体限制统一提高到至少 85 MB |
| 09:00 未自动采集 | 检查 `ENABLE_SCHEDULER=true`、服务器日志和采集 MCP 网络 |
| MCP 401 | 检查请求头是否为 `Authorization: Bearer <KOC_MCP_API_KEY>` |
| 飞书读取失败 | 检查应用权限、文档授权和服务器到飞书 OpenAPI 的网络 |

生产日志不得打印数据库密码、飞书 Secret、MCP key 或完整带 key 的采集服务 URL。
