321 lines
19 KiB
Markdown
321 lines
19 KiB
Markdown
# 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;对象存储接口已与业务代码分离。
|
||
|
||
仓库提供两套编排文件,按数据库归属选用,共用同一份环境文件 `.env.app`(模板 `.env.app.example`):
|
||
|
||
- `docker-compose.self-hosted.yml`:自带 MySQL 容器与数据卷,适合独立服务器或没有公共数据库的环境;
|
||
- `docker-compose.production.yml`:只包含 KOC 服务与入口 Nginx,直连公司公共 MySQL(要求 MySQL 8 + utf8mb4,账号需对目标库有建表与读写权限)。
|
||
|
||
两套文件的入口 Nginx 都建议保留:它除了反向代理,还托管 `/koc/` 领取页静态文件、`/api/mcp` 长连接规则与 85 MB 上传限制,公司网关只需把域名转发到容器的 `HTTP_PORT`。
|
||
|
||
## 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.app.example .env.app
|
||
```
|
||
|
||
编辑 `.env.app`。必须替换所有 `replace-with-*` 占位值(公共 MySQL 模式还需把 `MYSQL_HOST` 指向公司实例):
|
||
|
||
| 变量 | 用途 |
|
||
| --- | --- |
|
||
| `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` |
|
||
| `WECOM_CORP_ID` / `WECOM_AGENT_ID` / `WECOM_SECRET` | 企业微信自建应用,用于临期催办的应用消息推送;三项需同时填写,全部留空则只走群机器人 |
|
||
| `WECOM_ROBOT_WEBHOOK` | 企业微信群机器人 webhook,用于当日催办汇总 |
|
||
| `WECOM_NOTIFY_DUE_DAYS` | 临期阈值,默认 3,超过则不在催办;可填 1–30 |
|
||
| `WECOM_NOTIFY_ENABLED` | 企业微信通知总开关,默认 `true`;临时关闭设为 `false` |
|
||
| `WECOM_FEEDBACK_ENABLED` | 数据回流通知,默认 `false`;开启后 KOC 提交链接/截图与每日采集摘要推送运营群机器人 |
|
||
| `WECOM_ESCALATION_ENABLED` | 超期升级催办,默认 `false`;开启后超期任务升级运营群机器人 + 项目总群交互卡片 |
|
||
| `WECOM_AUTO_RELEASE_DAYS` | 超期 N 天自动释放内容回可领取池,从 `due_at` 起算,`0` = 关闭(默认),最大 90 |
|
||
| `WECOM_CALLBACK_ENABLED` | 企微回调开关,默认 `false`;开启后支持交互卡片「已处理」回执落库,需配置回调 URL 与密钥 |
|
||
| `WECOM_CALLBACK_TOKEN` / `WECOM_CALLBACK_ENCODING_AES_KEY` | 企微后台「接收消息」里生成的回调 Token 与 EncodingAESKey |
|
||
|
||
企业微信变量全部选填:不填则临期催办与汇总都跳过,不影响其他功能。所有变量都可以在「合作方」页面顶部「企业微信连通性」面板查看就绪状态,并用「测试群机器人」「测试发送」按钮验证。
|
||
|
||
密钥必须由密码管理器生成,禁止提交到 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.app \
|
||
-f docker-compose.self-hosted.yml up -d --build
|
||
```
|
||
|
||
生产环境使用公司公共 MySQL 时改用:
|
||
|
||
```bash
|
||
docker compose --env-file .env.app \
|
||
-f docker-compose.production.yml up -d --build
|
||
```
|
||
|
||
应用容器启动时会先执行同一套数据库迁移脚本,成功后才启动 KOC 服务。查看状态(生产模式把 `-f` 换成对应编排文件即可):
|
||
|
||
```bash
|
||
docker compose --env-file .env.app \
|
||
-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.app \
|
||
-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。
|
||
|
||
## 11. 企业微信自建应用与群机器人配置
|
||
|
||
KOC LOOP 的企业微信通知默认是**单向外发**:每天 09:00 与定时采集一同触发,给合作方绑定的外部联系人推送临期催办,再向群机器人发送当日汇总。基础催办不需要回调,也不需要拉通讯录;交互卡片回执等进阶能力见第 12 章。
|
||
|
||
### 11.1 群机器人(用于当日催办汇总)
|
||
|
||
1. 在企业微信里建立一个用于催办的群;
|
||
2. 群设置 → 群机器人 → 添加机器人 → 复制 webhook 地址,形如 `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=...`;
|
||
3. 把整个 webhook 写入 `WECOM_ROBOT_WEBHOOK`。
|
||
|
||
只要这一项就绪,每天 09:00 的催办汇总就会推到该群。
|
||
|
||
### 11.2 自建应用(用于给单个合作方发应用消息)
|
||
|
||
1. 登录企业微信管理后台 → 应用管理 → 自建 → 创建应用,名称建议 `KOC LOOP 催办`;
|
||
2. 应用详情页记下 `AgentId`,填入 `WECOM_AGENT_ID`;
|
||
3. 我的企业 → 企业信息 → 企业 ID,填入 `WECOM_CORP_ID`;
|
||
4. 应用详情页 → Secret → 发送 Secret 到管理端,填入 `WECOM_SECRET`;
|
||
5. 应用「可见范围」必须包含合作方对应的企业微信成员,否则推送时会返回 `invaliduser`。
|
||
|
||
### 11.3 绑定外部联系人
|
||
|
||
企业微信应用消息推送需要 `external_userid`(外部联系人 ID),KOC LOOP 不自动拉取,需要手动绑定:
|
||
|
||
1. 在企业微信「客户联系 → 外部联系人」里找到合作方对应的客户;
|
||
2. 复制其 external_userid(形如 `woxxxxxx`);
|
||
3. 在 KOC LOOP 后台「合作方」页面,找到对应合作方行,点「绑定」粘贴 ID → 「保存」;
|
||
4. 点「测试发送」验证;如果返回 `应用消息发送失败`,多半是应用可见范围未包含该外部联系人,或 external_userid 复制错了。
|
||
|
||
外部联系人 ID 不入库后不会自动同步企业微信端的变更;如果合作方的外部联系人在企业微信里被删除或转移,需要回来更新这一列。
|
||
|
||
### 11.4 验证
|
||
|
||
部署后到后台「合作方」页面,顶部「企业微信连通性」面板应显示三项就绪状态:群机器人、自建应用消息、临期阈值。逐项点「测试」:
|
||
|
||
- 「测试群机器人」:群内收到 `[KOC LOOP 测试] 群机器人连通性正常,时间 ...` 即配置成功;
|
||
- 合作方行的「测试发送」:对应外部联系人收到同样格式的测试消息即绑定正确。
|
||
|
||
如果面板显示「未配置」但已填环境变量,先确认容器加载了新的 `.env.app`(重启服务),再回到该页面点「刷新状态」。
|
||
|
||
### 11.5 任务发布到企微客户群(企业群发)
|
||
|
||
平台支持把任务以「企业群发」方式推送到多个外部客户群。**企业微信不允许外部群添加群机器人 webhook**,官方路径是半自动群发:平台创建群发任务 → 各群的群主在企业微信客户端「群发助手」里点击发送 → 消息才送达客户群。依赖 11.2 的同一个自建应用,无需新增环境变量;数据库表 `wecom_group_chats` / `wecom_group_pushes` 由迁移 `mysql/0009_wecom_group_push.sql` 创建(`npm run db:migrate` 自动执行)。
|
||
|
||
使用前需在企业微信管理后台完成三项配置:
|
||
|
||
1. **客户联系 → 配置 → 可调用应用**:把该自建应用加入可调用列表,否则客户群相关接口会报权限错误;
|
||
2. **应用可见范围**:必须包含所有目标群的群主(群主是群发任务的确认人);
|
||
3. **应用可信 IP**(应用详情页 → 开发者接口 → 企业可信 IP):必须包含服务器出口 IP,否则接口返回 `60020 not allow to access from your ip`。
|
||
|
||
使用流程:后台「任务中心」→ 任务行「发布到企微群」→ 弹窗内「同步群列表」(拉取客户联系下的正常状态客户群)→ 勾选目标群、确认文案 → 「创建群发」。创建成功后平台返回 msgid 并落库 `wecom_group_pushes`,各群主在企微客户端收到群发助手提醒,**点击发送后**消息(含 KOC 领取链接)才出现在群里。
|
||
|
||
限制与注意:每个客户群每月最多接收「当月天数」条企业群发;群主使用的企业微信客户端需 ≥4.1.10 才能免选群直接发送;无效的 chat_id 会进入失败列表但不影响其他群。
|
||
|
||
## 12. 以企微为中心的业务流程(一键推送 / 回流通知 / 分层催办 / 回调卡片 / 复盘报表)
|
||
|
||
在 11.2 自建应用与 11.5 企业群发的基础上,以下能力全部由环境变量独立控制,默认关闭,不配置则对现有流程零影响。推送记录统一落库 `wecom_push_logs`(迁移 `mysql/0011_wecom_push_logs.sql`,`npm run db:migrate` 自动执行)。
|
||
|
||
### 12.1 任务一键推送
|
||
|
||
后台「任务中心」→ 任务行「一键推送」:一次把任务送达三类对象 —— ①项目总群成员应用消息(复用项目推送)②已绑定企微的 KOC 个人直达(弹层默认全选,可搜索/取消勾选)③可选创建任务群企业群发。同一任务当日重复推送会先提示,确认后可强制再推。KOC 消息里的领取链接在企微内置浏览器打开时自动完成 OAuth 静默登录,KOC 无需输任何身份。
|
||
|
||
### 12.2 数据回流通知(`WECOM_FEEDBACK_ENABLED`)
|
||
|
||
开启后:KOC 提交发布链接 / 截图 → 运营群机器人收到提醒(合作方 / 任务 / 标题 / 链接);每天 09:00 定时采集结束后推送批次摘要,采集失败会附告警清单。通知失败不影响提交与采集主流程。
|
||
|
||
### 12.3 超期升级与自动释放(`WECOM_ESCALATION_ENABLED` / `WECOM_AUTO_RELEASE_DAYS`)
|
||
|
||
催办分三层:临期(默认 3 天内)直达合作方本人(原有逻辑不变)→ 超期(`due_at` 已过)升级运营群机器人汇总 + 项目总群成员收到 template_card 交互卡片(按钮「查看任务」跳转、「已处理」回执落库)→ 超期 N 天(`WECOM_AUTO_RELEASE_DAYS`,从 `due_at` 起算)自动把仍未发布的 distribution 释放回可领取池,已提交截图或已回填链接的不释放。两层开关独立;也可在「合作方」页点「跑一次超期升级」手动触发。
|
||
|
||
### 12.4 企微回调配置(`WECOM_CALLBACK_ENABLED`)
|
||
|
||
交互卡片「已处理」按钮需要公网回调才能回执。步骤:
|
||
|
||
1. 登录企业微信管理后台 → 应用管理 → 自建 → 打开 11.2 的应用;
|
||
2. 「接收消息」→ 设置 API 接收 → 回调 URL 填 `https://<APP_ORIGIN>/api/wecom-callback`(必须公网 80/443 可达,与 APP_ORIGIN 一致);
|
||
3. Token 与 EncodingAESKey 随机生成后,分别填入 `WECOM_CALLBACK_TOKEN`、`WECOM_CALLBACK_ENCODING_AES_KEY`,保存时企微会向该 URL 发 GET 验证,服务端自动应答;
|
||
4. 「企业可信 IP」(开发者接口)加入服务器出口 IP,否则 access_token 类接口全部返回 `60020`;
|
||
5. 应用「可见范围」包含接收卡片的运营成员;
|
||
6. `WECOM_CALLBACK_ENABLED=true` 并重启服务;关闭时回调路由直接 403,可随时回退。
|
||
|
||
回执写入 `wecom_push_logs`(`push_kind = card_ack`),在任务推送记录里可见「卡片确认」条目。回调地址、Token 或密钥配错时企微保存验证不通过,逐项核对第 2、3 步。
|
||
|
||
### 12.5 复盘报表
|
||
|
||
后台「复盘报表」(管理员可见):按任务汇总领取 / 发布 / 完成数、完成率、曝光阅读与 D2/D5/D7/最新互动数据,支持状态、类型、关键词筛选,一键导出带 BOM 的 CSV(Excel 直接打开不乱码)。无需环境变量。
|