Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9871760bc0 | |||
| f533c385ea | |||
| bcb2f303ff | |||
| a31156dc4e |
+2
-1
@@ -2,7 +2,8 @@ FROM golang:1.26.5-bookworm AS build
|
|||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
ARG GOPROXY=https://goproxy.cn,direct
|
||||||
|
RUN GOPROXY=$GOPROXY go mod download
|
||||||
COPY . ./
|
COPY . ./
|
||||||
|
|
||||||
ARG VERSION=dev
|
ARG VERSION=dev
|
||||||
|
|||||||
@@ -76,6 +76,39 @@ DobotAI 是 WeKnora 的企业外部渠道集成层,负责钉钉协议、身份
|
|||||||
|
|
||||||
目录职责规则:`cmd/`只装配进程,`internal/`承载业务模块,`internal/store/postgres/`只实现数据库持久化,`db/migrations/`只放Schema变更,`deploy/`只放容器运行资产。历史计划、备份、镜像归档和真实环境文件均不进入Git。
|
目录职责规则:`cmd/`只装配进程,`internal/`承载业务模块,`internal/store/postgres/`只实现数据库持久化,`db/migrations/`只放Schema变更,`deploy/`只放容器运行资产。历史计划、备份、镜像归档和真实环境文件均不进入Git。
|
||||||
|
|
||||||
|
## 部署方式
|
||||||
|
|
||||||
|
| 方式 | 适用场景 | 入口 | 数据库与迁移 | 推荐程度 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| 源码/原生二进制 | 本地开发、快速调试、临时问题定位 | `go run ./cmd/dobotai` 或受控构建的二进制 | 操作人员准备独立PostgreSQL并显式执行迁移 | 仅开发 |
|
||||||
|
| Docker Compose | 测试、预发布、生产候选 | 仓库根目录`install-docker.sh` | 脚本构建镜像、创建`dobotai-postgres`和`dobotai-postgres-data`,并按序执行迁移 | 推荐 |
|
||||||
|
|
||||||
|
### 源码/原生部署
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 开发机:使用Git外的环境文件注入真实配置
|
||||||
|
go run ./cmd/dobotai
|
||||||
|
curl http://127.0.0.1:8090/healthz
|
||||||
|
```
|
||||||
|
|
||||||
|
源码方式不自动创建数据库、不管理密钥目录、不自动执行SQL迁移,也不适合作为长期测试或生产运行方式。
|
||||||
|
|
||||||
|
### Docker Compose 部署
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 克隆后进入仓库根目录执行:自动安装Docker/Compose(如缺失)、构建镜像、
|
||||||
|
# 准备目录、独立PostgreSQL、命名卷、env模板和迁移。
|
||||||
|
sudo bash ./install-docker.sh
|
||||||
|
|
||||||
|
# 配置WeKnora和钉钉后自动启动应用
|
||||||
|
sudo dobotai-dingtalk-configure
|
||||||
|
|
||||||
|
# 仅首次且没有有效管理员时执行
|
||||||
|
sudo dobotai-dingtalk-bind-admin
|
||||||
|
```
|
||||||
|
|
||||||
|
完整的安装、现有数据库迁移、备份、回滚和验收说明见 [Docker 部署手册](deploy/dobotai-dingtalk/README.md)。
|
||||||
|
|
||||||
## 钉钉 Stream
|
## 钉钉 Stream
|
||||||
|
|
||||||
使用官方 `dingtalk-stream-sdk-go`,不暴露公网回调入口。
|
使用官方 `dingtalk-stream-sdk-go`,不暴露公网回调入口。
|
||||||
|
|||||||
+178
-196
@@ -1,280 +1,262 @@
|
|||||||
# DobotAI DingTalk 容器部署手册
|
# DobotAI DingTalk Docker 部署手册
|
||||||
|
|
||||||
> 适用范围:DobotAI 测试环境的容器化运行。容器和 Compose 服务名固定为 `dobotai-dingtalk`。
|
> 适用范围:DobotAI 的 Docker Compose 部署。容器名固定为 `dobotai-dingtalk`,数据库容器名固定为 `dobotai-postgres`。
|
||||||
> 本文不授权生产部署;生产必须使用已验收的同一镜像、独立备份和独立变更审批。
|
>
|
||||||
|
> 本文面向测试/预发布。生产必须使用已验收的同一镜像、独立备份和独立审批,禁止在生产重新编译。
|
||||||
|
|
||||||
## 1. 定位
|
## 1. 定位
|
||||||
|
|
||||||
DobotAI 是钉钉渠道的对话编排与交付层,不是知识库或文档解析平台。
|
DobotAI 是钉钉渠道的对话编排与交付层,不是知识库、文档解析器或模型平台。
|
||||||
|
|
||||||
它负责:
|
```text
|
||||||
|
DobotAI 负责:
|
||||||
|
钉钉 Stream、富文本/图片、会话映射、WeKnora Agent 调用、卡片交付、
|
||||||
|
/think、/setting、覆盖判定、Support Case、反馈、渠道审计。
|
||||||
|
|
||||||
- 接收钉钉 Stream 消息、富文本和图片;
|
DobotAI 不负责:
|
||||||
- 将请求路由到 WeKnora Agent,并维护渠道会话;
|
WeKnora 文档/Chunk/向量/图谱/解析/模型;钉钉应用创建;知识库治理。
|
||||||
- 通过钉钉 AI 卡片或 Markdown 流式交付回答;
|
```
|
||||||
- 处理 `/think`、`/setting` 等渠道命令;
|
|
||||||
- 保存 Turn、会话映射、覆盖判定、Support Case、反馈和审计数据;
|
|
||||||
- 对“真实检索过但没有引用”的回答创建未覆盖提醒;
|
|
||||||
- 将 `resource://` 图片按私有 OSS 规则安全外发。
|
|
||||||
|
|
||||||
它不负责:
|
## 2. Docker 架构
|
||||||
|
|
||||||
- WeKnora 文档、Chunk、向量、图谱、解析或模型运行;
|
|
||||||
- WeKnora Agent 的知识检索策略;
|
|
||||||
- 直接暴露 PostgreSQL、WeKnora 或对象存储到公网;
|
|
||||||
- 替代钉钉应用、机器人或群权限配置。
|
|
||||||
|
|
||||||
## 2. 架构
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
钉钉用户/群
|
钉钉用户/群
|
||||||
│ DingTalk Stream
|
│ DingTalk Stream(出站长连接)
|
||||||
▼
|
▼
|
||||||
┌──────────────────────────────────────┐
|
┌────────────────────────────────────────┐
|
||||||
│ dobotai-dingtalk 容器 │
|
│ dobotai-dingtalk │
|
||||||
│ - 消息解析、会话编排、卡片流式交付 │
|
│ 消息、会话、卡片、覆盖与支持流程 │
|
||||||
│ - /think、/setting、首位管理员绑定 │
|
│ 仅映射到宿主机 127.0.0.1:8090 │
|
||||||
│ - 覆盖判定、Support Case、审计 │
|
└───────┬──────────────────────┬───────────┘
|
||||||
└───────┬──────────────────┬───────────┘
|
│ Compose 网络 │ host.docker.internal
|
||||||
│ │
|
|
||||||
│ HTTP Agent Chat │ PostgreSQL
|
|
||||||
▼ ▼
|
▼ ▼
|
||||||
WeKnora 服务 DobotAI 独立数据库
|
┌────────────────────┐ 已有 WeKnora 服务
|
||||||
- Agent/检索 - Turn / 会话 / 覆盖判定
|
│ dobotai-postgres │ Agent / 检索 / 知识 / 模型
|
||||||
- 文档/Chunk - Support Case / 反馈 / 设置
|
│ dobotai 独立数据库 │
|
||||||
- 模型与解析
|
│ 命名卷持久化 │
|
||||||
|
└────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2.1 容器边界
|
Compose 管理的资源:
|
||||||
|
|
||||||
| 资源 | 归属 | 是否由本容器创建/删除 |
|
| 资源 | 名称 | 数据策略 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `dobotai-dingtalk` | DobotAI应用进程 | 是,容器可重建 |
|
| 应用容器 | `dobotai-dingtalk` | 可重建,不保存业务数据卷 |
|
||||||
| DobotAI PostgreSQL | 独立数据服务 | 否,保留现有数据卷 |
|
| PostgreSQL容器 | `dobotai-postgres` | 可重建,数据在命名卷 |
|
||||||
| WeKnora | 独立知识服务 | 否 |
|
| PostgreSQL命名卷 | `dobotai-postgres-data` | 持久数据,禁止常规删除 |
|
||||||
| `/var/lib/dobotai-test/secrets` | 宿主机密钥目录 | 仅挂载读写,不迁移、不输出 |
|
| Compose网络 | `dobotai-network` | 应用与数据库内部通信 |
|
||||||
| `/etc/dobotai-test/dobotai.env` | 受保护运行配置 | Docker读取,脚本原子更新 |
|
| 宿主机密钥目录 | `<DOBOTAI_HOME>/secrets` | bind mount,不进入Git |
|
||||||
|
|
||||||
测试环境的 PostgreSQL、WeKnora 已通过宿主机回环地址提供服务,因此 Compose 使用 `network_mode: host`。这不是新开公网端口:DobotAI仍按 `DOBOTAI_LISTEN_ADDR` 监听,默认是 `127.0.0.1:8090`。
|
关键Compose属性:
|
||||||
|
|
||||||
### 2.2 容器安全属性
|
|
||||||
|
|
||||||
Compose 固定启用:
|
|
||||||
|
|
||||||
```text
|
|
||||||
容器名:dobotai-dingtalk
|
|
||||||
网络:host(仅兼容现有测试宿主机回环依赖)
|
|
||||||
运行身份:与原 dobotai-test 服务用户相同的 UID:GID
|
|
||||||
根文件系统:只读
|
|
||||||
临时写入:仅 /tmp tmpfs
|
|
||||||
额外权限:no-new-privileges
|
|
||||||
重启策略:unless-stopped
|
|
||||||
```
|
|
||||||
|
|
||||||
对应的关键 Compose 配置如下;不要为避免端口冲突额外添加 `ports:` 映射:
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
|
dobotai-postgres:
|
||||||
|
container_name: dobotai-postgres
|
||||||
|
volumes:
|
||||||
|
- dobotai-postgres-data:/var/lib/postgresql/data
|
||||||
|
|
||||||
dobotai-dingtalk:
|
dobotai-dingtalk:
|
||||||
container_name: dobotai-dingtalk
|
container_name: dobotai-dingtalk
|
||||||
network_mode: host
|
depends_on:
|
||||||
read_only: true
|
dobotai-postgres:
|
||||||
restart: unless-stopped
|
condition: service_healthy
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8090:8090"
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
```
|
```
|
||||||
|
|
||||||
## 3. 安装前检查
|
应用不使用 `network_mode: host`:它以`dobotai-postgres:5432`访问Compose数据库,以`http://host.docker.internal:8080`访问宿主机的WeKnora。
|
||||||
|
|
||||||
安装操作人员需要具备测试服务器的 `sudo` 和 Docker 权限。开始前确认:
|
## 3. 自动安装准备
|
||||||
|
|
||||||
|
`install-docker.sh`自动完成:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
1. 已构建或导入经审查的 DobotAI 镜像;
|
检查 Docker、Docker Compose 和已导入的候选镜像
|
||||||
2. 现有 /etc/dobotai-test/dobotai.env 存在且权限为 0600;
|
创建 <DOBOTAI_HOME>/container、config、secrets、backups、migrations
|
||||||
3. 现有密钥目录 /var/lib/dobotai-test/secrets 存在;
|
生成权限0600的 config/postgres.env 与 config/dobotai.env
|
||||||
4. DobotAI PostgreSQL 与 WeKnora 已健康;
|
生成独立数据库密码、Ingress Token、Admin Token
|
||||||
5. 已记录当前原生服务二进制SHA和运行状态;
|
创建并启动 dobotai-postgres
|
||||||
6. 如本版本包含 SQL 迁移,已先完成逻辑备份与迁移演练。
|
等待 PostgreSQL healthcheck
|
||||||
|
按顺序应用 db/migrations/*.up.sql
|
||||||
|
在 dobotai_schema_migrations 记录版本、名称和时间
|
||||||
|
安装 configure / start / bind-admin 三条短命令
|
||||||
```
|
```
|
||||||
|
|
||||||
不要执行:
|
它不会:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
docker compose down -v
|
启动尚未配置的钉钉应用容器
|
||||||
docker volume rm / prune
|
覆盖已有配置或数据库数据
|
||||||
删除 PostgreSQL / WeKnora 数据卷
|
删除任何 Docker 卷
|
||||||
将密钥、数据库连接串或 API Key 写入命令行、日志或 Git
|
输出密码、API Key、Token 或连接串
|
||||||
```
|
```
|
||||||
|
|
||||||
## 4. 构建和导入镜像
|
默认安装根目录:
|
||||||
|
|
||||||
在已审查且工作树干净的源码目录构建。示例中的值均为占位符:
|
```text
|
||||||
|
/opt/dobotai
|
||||||
|
├── container/ Compose、脚本、SQL迁移、运行参数
|
||||||
|
├── config/ dobotai.env、postgres.env(0600)
|
||||||
|
├── secrets/ DobotAI SecretStore(0700)
|
||||||
|
└── backups/ 逻辑备份和SHA清单
|
||||||
|
```
|
||||||
|
|
||||||
|
可以用`DOBOTAI_HOME=/绝对路径`改安装位置。测试环境建议使用独立目录,例如`/opt/dobotai-test-compose`。
|
||||||
|
|
||||||
|
## 4. 前置条件
|
||||||
|
|
||||||
|
```text
|
||||||
|
部署用户具有 sudo 和 Docker daemon 访问权限
|
||||||
|
宿主机 WeKnora 已健康,容器可经 host.docker.internal:8080 访问
|
||||||
|
```
|
||||||
|
|
||||||
|
Docker与Docker Compose缺失时由根安装脚本自动安装(当前支持APT系统)。不必手工构建、导入或命名镜像。
|
||||||
|
|
||||||
|
## 5. 全新安装
|
||||||
|
|
||||||
|
从源码仓库克隆后,进入仓库根目录执行唯一安装入口:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build \
|
sudo bash ./install-docker.sh
|
||||||
--build-arg VERSION=test-<commit-short> \
|
|
||||||
--build-arg COMMIT_SHA=<完整提交SHA> \
|
|
||||||
--build-arg BUILD_TIME=<UTC时间> \
|
|
||||||
-t dobotai-dingtalk:test-<commit-short> .
|
|
||||||
```
|
```
|
||||||
|
|
||||||
跨主机传输时先保存镜像归档并记录 SHA-256;测试服务器导入后必须复核归档哈希和镜像标签。生产不得在目标机重新编译,应使用已测试的同一镜像归档或不可变镜像摘要。
|
脚本内部以当前Git提交构建固定内部镜像名:
|
||||||
|
|
||||||
## 5. 安装容器资产(不切换服务)
|
|
||||||
|
|
||||||
将本目录、候选镜像和已核验的归档放到测试服务器的受控发布目录。确认镜像已经 `docker load` 后执行:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo DOBOTAI_IMAGE=dobotai-dingtalk:test-<commit-short> \
|
|
||||||
bash ./install-test.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
该脚本会:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
1. 检查候选镜像、现有 env 与密钥目录;
|
dobotai-dingtalk:local-<当前Git短SHA>
|
||||||
2. 备份原生 DobotAI 二进制和 systemd Unit,并写入 SHA-256;
|
|
||||||
3. 安装 Compose、配置器、启动器和绑定脚本;
|
|
||||||
4. 生成权限 0600 的 /opt/dobotai-test/container/runtime.env;
|
|
||||||
5. 保持原生 dobotai-test.service 继续运行。
|
|
||||||
```
|
```
|
||||||
|
|
||||||
它不会读取或打印环境变量值、不会修改数据库、不会启动容器。
|
镜像名、PostgreSQL镜像和构建元数据只由根目录`install-docker.sh`维护,用户不传`DOBOTAI_IMAGE`。脚本完成后数据库和迁移已就绪,应用尚未启动。然后执行:
|
||||||
|
|
||||||
## 6. 配置并启动
|
构建期Go依赖使用脚本内置的公开模块代理`https://goproxy.cn,direct`;它只存在于镜像构建命令,不写入DobotAI运行配置。
|
||||||
|
|
||||||
日常只需要执行:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo dobotai-dingtalk-configure
|
sudo dobotai-dingtalk-configure
|
||||||
```
|
```
|
||||||
|
|
||||||
该命令交互更新 `/etc/dobotai-test/dobotai.env`。每项都会显示用途和脱敏格式示例;已配置的值永不回显,直接回车表示保留。
|
配置器逐项显示用途和脱敏格式示例;已有值不会回显,直接回车表示保留。数据库连接串、数据库密码、Ingress Token和Admin Token由安装脚本生成,交互配置器不要求输入。
|
||||||
|
|
||||||
| 环境变量 | 用途 | 格式示例 |
|
主要业务连接配置:
|
||||||
|
|
||||||
|
| 变量 | 用途 | 示例 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `DATABASE_URL` | DobotAI独立PostgreSQL连接串,不得填写WeKnora数据库 | `postgres://USER:<数据库密码>@127.0.0.1:55432/dobotai_test?sslmode=disable` |
|
| `WEKNORA_BASE_URL` | Docker内访问宿主机WeKnora | `http://host.docker.internal:8080` |
|
||||||
| `WEKNORA_BASE_URL` | 测试WeKnora地址 | `http://127.0.0.1:8080` |
|
| `WEKNORA_API_KEY` | DobotAI专用WeKnora API Key | `<你的WeKnora API Key>` |
|
||||||
| `WEKNORA_API_KEY` | DobotAI使用的WeKnora API密钥 | `<你的WeKnora API Key>` |
|
| `DOBOTAI_TENANT_KEY` | DobotAI租户隔离键 | `dobotai` |
|
||||||
| `DOBOTAI_INGRESS_TOKEN` | `/v1/qa`调用令牌 | `<随机长字符串>` |
|
| `DOBOTAI_AGENT_BINDINGS` | 渠道到WeKnora Agent映射 | `{"dingtalk:*":"你的Agent ID"}` |
|
||||||
| `DOBOTAI_TENANT_KEY` | 与既有DobotAI数据一致的租户键 | `dobotai-test` |
|
|
||||||
| `DOBOTAI_AGENT_BINDINGS` | 钉钉会话到WeKnora Agent的映射 | `{"dingtalk:*":"你的Agent ID"}` |
|
|
||||||
| `DINGTALK_CLIENT_ID` | 钉钉应用Client ID/AppKey | `dingxxxxxxxx` |
|
| `DINGTALK_CLIENT_ID` | 钉钉应用Client ID/AppKey | `dingxxxxxxxx` |
|
||||||
| `DINGTALK_CLIENT_SECRET` | 钉钉应用Client Secret/AppSecret | `<你的钉钉 AppSecret>` |
|
| `DINGTALK_CLIENT_SECRET` | 钉钉应用Secret/AppSecret | `<你的钉钉 AppSecret>` |
|
||||||
|
|
||||||
保存成功后,配置器自动调用启动器:
|
配置器成功后自动启动`dobotai-dingtalk`,并检查PostgreSQL健康、`http://127.0.0.1:8090/healthz`和容器运行状态。
|
||||||
|
|
||||||
```text
|
|
||||||
停止原生 dobotai-test.service
|
|
||||||
→ 启动 dobotai-dingtalk
|
|
||||||
→ 检查 http://127.0.0.1:8090/healthz
|
|
||||||
→ 健康成功后禁用原生服务
|
|
||||||
```
|
|
||||||
|
|
||||||
首次容器启动失败会恢复原生服务;容器版本升级失败会恢复上一容器镜像。
|
|
||||||
|
|
||||||
## 7. 首位管理员绑定
|
## 7. 首位管理员绑定
|
||||||
|
|
||||||
仅当该租户不存在有效管理员时执行:
|
仅在租户没有有效管理员时执行:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo dobotai-dingtalk-bind-admin
|
sudo dobotai-dingtalk-bind-admin
|
||||||
```
|
```
|
||||||
|
|
||||||
终端会显示一个10分钟有效的随机六码。候选管理员必须私聊钉钉机器人发送:
|
终端显示10分钟有效的一次性六码。候选管理员私聊钉钉机器人:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
/setting <六码绑定码>
|
/setting <六码绑定码>
|
||||||
```
|
```
|
||||||
|
|
||||||
验证成功后,终端自动写入 `super_admin/active` 并退出。已有有效管理员时命令拒绝执行,防止借由该入口追加管理员或意外提权。
|
终端自动确认后写入`super_admin/active`。已有管理员时命令拒绝执行,防止意外提权。
|
||||||
|
|
||||||
## 8. 覆盖判定与未覆盖提醒
|
## 8. 迁移现有DobotAI实例
|
||||||
|
|
||||||
覆盖判定基于真实的 `knowledge_searched` 工具事件和结构化引用,不按关键词猜测。
|
旧实例已有Turn、会话、Support Case、管理员或反馈时,不能直接新建空库后切换。先安装目标Compose基础设施,再运行:
|
||||||
|
|
||||||
| 场景 | 覆盖结果 | 是否创建 Support Case |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| 有引用 | `covered_has_references` | 否 |
|
|
||||||
| 实际检索过但无引用 | `uncovered_no_references` | 是 |
|
|
||||||
| 明确无权威知识/无答案 | `uncovered_explicit_no_answer` | 是 |
|
|
||||||
| 图片未检索、纯视觉识别 | `covered_visual_recognition` | 否 |
|
|
||||||
| 文本未检索、闲聊或普通非知识响应 | `covered_non_knowledge_response` | 否 |
|
|
||||||
|
|
||||||
因此“在吗”“你好”“谢谢”等未触发知识检索的文本不会产生未覆盖提醒;真正检索过但没有权威引用的问题仍会进入人工支持流程。
|
|
||||||
|
|
||||||
## 9. 数据库迁移门禁
|
|
||||||
|
|
||||||
DobotAI进程只连接数据库,**不会自动发现或执行 SQL 迁移**。包含新迁移的版本必须在容器替换前由发布流程显式完成:
|
|
||||||
|
|
||||||
```text
|
|
||||||
1. 对 DobotAI PostgreSQL 做逻辑备份;
|
|
||||||
2. 使用 pg_restore --list 验证备份非空;
|
|
||||||
3. 在事务中执行已审查的 .up.sql;
|
|
||||||
4. 向 dobotai_schema_migrations 写入对应 version/name/applied_at;
|
|
||||||
5. 查询约束、列或索引确认生效;
|
|
||||||
6. 再替换容器镜像并进行健康与业务验收。
|
|
||||||
```
|
|
||||||
|
|
||||||
`000021_coverage_non_knowledge_response_reason` 是一个示例:它只扩展 `coverage_decisions.reason_code` 的 CHECK 约束,不回写历史Turn、覆盖判定或Support Case。
|
|
||||||
|
|
||||||
## 10. 日常检查
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 健康检查
|
sudo DOBOTAI_HOME=/opt/dobotai-compose \
|
||||||
curl --noproxy '*' -fsS http://127.0.0.1:8090/healthz
|
SOURCE_POSTGRES_CONTAINER=dobotai-test-postgres \
|
||||||
|
SOURCE_APP_CONTAINER=dobotai-dingtalk \
|
||||||
|
SOURCE_DOBOTAI_ENV=/etc/dobotai-test/dobotai.env \
|
||||||
|
/opt/dobotai-compose/container/migrate-existing-database.sh
|
||||||
|
```
|
||||||
|
|
||||||
# 运行版本
|
迁移脚本固定顺序:
|
||||||
|
|
||||||
|
```text
|
||||||
|
启动目标 dobotai-postgres 并等待健康
|
||||||
|
→ 停止并改名旧应用容器(不删除)
|
||||||
|
→ pg_dump -Fc 导出旧数据库
|
||||||
|
→ pg_restore --list 验证备份可读
|
||||||
|
→ 写入备份SHA-256
|
||||||
|
→ 复制旧应用配置,只替换数据库、监听和WeKnora地址
|
||||||
|
→ 恢复到 dobotai-postgres-data
|
||||||
|
→ 应用缺失迁移
|
||||||
|
→ 启动新 dobotai-dingtalk
|
||||||
|
→ 健康失败则恢复旧容器名称和旧容器
|
||||||
|
```
|
||||||
|
|
||||||
|
源数据库不会删除。成功后旧应用容器会以`*-legacy-*`保留,确认稳定前不得删除旧容器或旧数据库。
|
||||||
|
|
||||||
|
## 9. 数据库迁移
|
||||||
|
|
||||||
|
DobotAI应用本身不扫描SQL。安装/迁移流程由:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo /opt/dobotai/container/apply-migrations.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
执行`migrations/*.up.sql`,每个成功迁移在同一事务写入:
|
||||||
|
|
||||||
|
```text
|
||||||
|
dobotai_schema_migrations(version, name, applied_at)
|
||||||
|
```
|
||||||
|
|
||||||
|
任何数据库升级前都必须有经`pg_restore --list`验证的逻辑备份。`.down.sql`只能在审查通过后用于数据库回滚,不能用应用镜像回滚替代数据库回滚。
|
||||||
|
|
||||||
|
## 10. 日常运行与升级
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 健康与版本
|
||||||
|
curl --noproxy '*' -fsS http://127.0.0.1:8090/healthz
|
||||||
curl --noproxy '*' -fsS http://127.0.0.1:8090/version
|
curl --noproxy '*' -fsS http://127.0.0.1:8090/version
|
||||||
|
|
||||||
# 容器状态与重启次数
|
# Compose状态
|
||||||
sudo docker inspect -f '{{.Config.Image}}|{{.State.Running}}|{{.RestartCount}}' dobotai-dingtalk
|
sudo docker compose --env-file /opt/dobotai/container/runtime.env \
|
||||||
|
-f /opt/dobotai/container/compose.yaml ps
|
||||||
|
|
||||||
# 最近日志(不要复制含敏感内容的原始上下文)
|
# 日志:不要转发含用户内容或凭据的原始上下文
|
||||||
sudo docker logs --since 10m dobotai-dingtalk
|
sudo docker logs --since 10m dobotai-dingtalk
|
||||||
|
|
||||||
|
# 数据库状态
|
||||||
|
sudo docker inspect -f '{{.State.Health.Status}}|{{.RestartCount}}' dobotai-postgres
|
||||||
```
|
```
|
||||||
|
|
||||||
验收至少应同时满足:
|
升级顺序:导入候选镜像 → 备份并应用必要迁移 → 更新`runtime.env`中的`DOBOTAI_IMAGE` → 执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo dobotai-dingtalk-start
|
||||||
|
```
|
||||||
|
|
||||||
|
应用健康失败时启动器恢复上一应用镜像;数据库迁移仍必须按审查过的SQL回滚流程处理。
|
||||||
|
|
||||||
|
## 11. 覆盖判定边界
|
||||||
|
|
||||||
```text
|
```text
|
||||||
容器 running,重启次数为0;
|
有结构化引用 → covered_has_references
|
||||||
/healthz 返回 ok;
|
实际检索过但没有引用 → uncovered_no_references → Support Case
|
||||||
/version 的 commit_sha 与发布记录一致;
|
明确无权威知识/无答案 → uncovered_explicit_no_answer → Support Case
|
||||||
钉钉 Stream 仅启动一个消费者;
|
图片未检索、纯视觉识别 → covered_visual_recognition
|
||||||
旧 dobotai-test.service 为 inactive + disabled;
|
文本未检索、闲聊/普通非知识响应 → covered_non_knowledge_response
|
||||||
目标业务路径已由真实钉钉消息验证。
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 11. 升级与回滚
|
因此“在吗”“你好”“谢谢”等未触发知识检索的文本不会创建Support Case或发送未覆盖提醒。
|
||||||
|
|
||||||
### 升级
|
## 12. 禁止操作
|
||||||
|
|
||||||
```text
|
```text
|
||||||
构建/导入经审查的新镜像
|
docker compose down -v
|
||||||
→ 备份DobotAI PostgreSQL
|
docker volume rm dobotai-postgres-data
|
||||||
→ 应用并记录必要迁移
|
docker system prune --volumes
|
||||||
→ 将 runtime.env 中的 DOBOTAI_IMAGE 更新为新标签
|
把 config/*.env、secrets/、备份Dump、真实Token或凭据提交Git
|
||||||
→ 执行 sudo dobotai-dingtalk-start
|
在生产重新编译或使用未经测试的镜像
|
||||||
→ 健康、版本、日志、钉钉真实消息验收
|
|
||||||
```
|
|
||||||
|
|
||||||
注意拼写:数据库和应用名称是 `DobotAI`;上述流程中的镜像名为 `dobotai-dingtalk`。
|
|
||||||
|
|
||||||
### 自动回滚
|
|
||||||
|
|
||||||
启动器会保存替换前的容器镜像:
|
|
||||||
|
|
||||||
```text
|
|
||||||
首次容器化失败:停止候选容器,恢复原生服务;
|
|
||||||
后续容器升级失败:停止候选容器,恢复上一容器镜像。
|
|
||||||
```
|
|
||||||
|
|
||||||
数据库迁移不能仅靠容器回滚撤销。只有经过审查且已验证的 `.down.sql` 才可用于数据库回滚;任何数据恢复都必须先确认备份可用。
|
|
||||||
|
|
||||||
## 12. 明确非目标
|
|
||||||
|
|
||||||
```text
|
|
||||||
不容器化或重建 WeKnora;
|
|
||||||
不容器化或删除 DobotAI PostgreSQL;
|
|
||||||
不重新启用已删除的网页管理面;
|
|
||||||
不在生产环境复用测试凭据、测试镜像或未验收的迁移;
|
|
||||||
不把密钥、Token、连接串、钉钉凭据写入 Git、命令行或日志。
|
|
||||||
```
|
```
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
home="${DOBOTAI_HOME:-/opt/dobotai}"
|
||||||
|
container_dir="$home/container"
|
||||||
|
compose_file="$container_dir/compose.yaml"
|
||||||
|
runtime_env="$container_dir/runtime.env"
|
||||||
|
migrations_dir="$container_dir/migrations"
|
||||||
|
|
||||||
|
if [[ ! -r "$compose_file" || ! -r "$runtime_env" || ! -d "$migrations_dir" ]]; then
|
||||||
|
echo "迁移运行文件未安装。请先执行 install-docker.sh。" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
compose=(docker compose --env-file "$runtime_env" -f "$compose_file")
|
||||||
|
"${compose[@]}" up -d --no-build dobotai-postgres
|
||||||
|
for _ in $(seq 1 30); do
|
||||||
|
status=$(docker inspect -f '{{.State.Health.Status}}' dobotai-postgres 2>/dev/null || true)
|
||||||
|
[[ "$status" == "healthy" ]] && break
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
[[ "${status:-}" == "healthy" ]] || { echo "PostgreSQL 未健康,拒绝迁移。" >&2; exit 1; }
|
||||||
|
|
||||||
|
psql_db() {
|
||||||
|
"${compose[@]}" exec -T dobotai-postgres psql -v ON_ERROR_STOP=1 -U dobotai -d dobotai "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
psql_db -c 'CREATE TABLE IF NOT EXISTS dobotai_schema_migrations (version INTEGER PRIMARY KEY, name TEXT NOT NULL, applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW())'
|
||||||
|
|
||||||
|
for file in "$migrations_dir"/*.up.sql; do
|
||||||
|
[[ -f "$file" ]] || continue
|
||||||
|
base="$(basename "$file")"
|
||||||
|
version_text="${base%%_*}"
|
||||||
|
version=$((10#$version_text))
|
||||||
|
name="${base#*_}"
|
||||||
|
name="${name%.up.sql}"
|
||||||
|
case "$name" in *[!a-z0-9_]* ) echo "非法迁移文件名:$base" >&2; exit 1;; esac
|
||||||
|
applied=$(psql_db -At -c "SELECT EXISTS(SELECT 1 FROM dobotai_schema_migrations WHERE version=$version)")
|
||||||
|
[[ "$applied" == "t" ]] && continue
|
||||||
|
{
|
||||||
|
printf 'BEGIN;\n'
|
||||||
|
cat "$file"
|
||||||
|
printf "\nINSERT INTO dobotai_schema_migrations(version,name) VALUES (%s,'%s');\nCOMMIT;\n" "$version" "$name"
|
||||||
|
} | "${compose[@]}" exec -T dobotai-postgres psql -v ON_ERROR_STOP=1 -U dobotai -d dobotai
|
||||||
|
echo "已应用迁移:$base"
|
||||||
|
done
|
||||||
@@ -1,9 +1,28 @@
|
|||||||
services:
|
services:
|
||||||
|
dobotai-postgres:
|
||||||
|
image: ${DOBOTAI_POSTGRES_IMAGE:-postgres:16-alpine}
|
||||||
|
container_name: dobotai-postgres
|
||||||
|
env_file:
|
||||||
|
- ${DOBOTAI_POSTGRES_ENV_FILE:?set DOBOTAI_POSTGRES_ENV_FILE in runtime.env}
|
||||||
|
volumes:
|
||||||
|
- dobotai-postgres-data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 30
|
||||||
|
restart: unless-stopped
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
networks:
|
||||||
|
- dobotai-network
|
||||||
|
|
||||||
dobotai-dingtalk:
|
dobotai-dingtalk:
|
||||||
image: ${DOBOTAI_IMAGE:?set DOBOTAI_IMAGE in runtime.env}
|
image: ${DOBOTAI_IMAGE:?set DOBOTAI_IMAGE in runtime.env}
|
||||||
container_name: dobotai-dingtalk
|
container_name: dobotai-dingtalk
|
||||||
network_mode: host
|
depends_on:
|
||||||
user: "${DOBOTAI_RUNTIME_UID:?set DOBOTAI_RUNTIME_UID}:${DOBOTAI_RUNTIME_GID:?set DOBOTAI_RUNTIME_GID}"
|
dobotai-postgres:
|
||||||
|
condition: service_healthy
|
||||||
env_file:
|
env_file:
|
||||||
- ${DOBOTAI_ENV_FILE:?set DOBOTAI_ENV_FILE in runtime.env}
|
- ${DOBOTAI_ENV_FILE:?set DOBOTAI_ENV_FILE in runtime.env}
|
||||||
environment:
|
environment:
|
||||||
@@ -13,6 +32,10 @@ services:
|
|||||||
source: ${DOBOTAI_SECRET_DIR_HOST:?set DOBOTAI_SECRET_DIR_HOST in runtime.env}
|
source: ${DOBOTAI_SECRET_DIR_HOST:?set DOBOTAI_SECRET_DIR_HOST in runtime.env}
|
||||||
target: /var/lib/dobotai/secrets
|
target: /var/lib/dobotai/secrets
|
||||||
read_only: false
|
read_only: false
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8090:8090"
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
init: true
|
init: true
|
||||||
read_only: true
|
read_only: true
|
||||||
@@ -20,3 +43,14 @@ services:
|
|||||||
- /tmp:rw,noexec,nosuid,size=32m
|
- /tmp:rw,noexec,nosuid,size=32m
|
||||||
security_opt:
|
security_opt:
|
||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
|
networks:
|
||||||
|
- dobotai-network
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
dobotai-postgres-data:
|
||||||
|
name: dobotai-postgres-data
|
||||||
|
|
||||||
|
networks:
|
||||||
|
dobotai-network:
|
||||||
|
name: dobotai-network
|
||||||
|
driver: bridge
|
||||||
|
|||||||
@@ -6,11 +6,10 @@ from pathlib import Path
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
FIELDS = (
|
FIELDS = (
|
||||||
("DATABASE_URL", "DobotAI PostgreSQL 连接串", "DobotAI独立数据库,不得填写WeKnora数据库。", "postgres://USER:<数据库密码>@127.0.0.1:55432/dobotai_test?sslmode=disable", True),
|
("WEKNORA_BASE_URL", "WeKnora 地址", "Docker 容器访问宿主机 WeKnora 时使用 host.docker.internal。", "http://host.docker.internal:8080", False),
|
||||||
("WEKNORA_BASE_URL", "WeKnora 地址", "容器通过宿主网络访问测试WeKnora。", "http://127.0.0.1:8080", False),
|
("WEKNORA_API_KEY", "WeKnora API Key", "在 WeKnora 中为 DobotAI 创建的独立 API Key。", "<你的WeKnora API Key>", True),
|
||||||
("WEKNORA_API_KEY", "WeKnora API Key", "在WeKnora中为DobotAI创建的API密钥。", "<你的WeKnora API Key>", True),
|
|
||||||
("DOBOTAI_INGRESS_TOKEN", "DobotAI Ingress Token", "调用 /v1/qa 时使用的独立长随机令牌。", "<随机长字符串>", True),
|
("DOBOTAI_INGRESS_TOKEN", "DobotAI Ingress Token", "调用 /v1/qa 时使用的独立长随机令牌。", "<随机长字符串>", True),
|
||||||
("DOBOTAI_TENANT_KEY", "DobotAI 租户键", "必须与现有DobotAI数据和Agent绑定使用同一个租户键。", "dobotai-test", False),
|
("DOBOTAI_TENANT_KEY", "DobotAI 租户键", "同一 DobotAI 数据库内的渠道隔离键。", "dobotai", False),
|
||||||
("DOBOTAI_AGENT_BINDINGS", "DobotAI Agent Bindings JSON", "钉钉会话匹配到 WeKnora Agent 的映射。", '{"dingtalk:*":"你的Agent ID"}', False),
|
("DOBOTAI_AGENT_BINDINGS", "DobotAI Agent Bindings JSON", "钉钉会话匹配到 WeKnora Agent 的映射。", '{"dingtalk:*":"你的Agent ID"}', False),
|
||||||
("DINGTALK_CLIENT_ID", "钉钉 Client ID", "钉钉应用的 Client ID/AppKey。", "dingxxxxxxxx", False),
|
("DINGTALK_CLIENT_ID", "钉钉 Client ID", "钉钉应用的 Client ID/AppKey。", "dingxxxxxxxx", False),
|
||||||
("DINGTALK_CLIENT_SECRET", "钉钉 Client Secret", "钉钉应用的 Client Secret/AppSecret。", "<你的钉钉 AppSecret>", True),
|
("DINGTALK_CLIENT_SECRET", "钉钉 Client Secret", "钉钉应用的 Client Secret/AppSecret。", "<你的钉钉 AppSecret>", True),
|
||||||
@@ -56,14 +55,13 @@ def write_env(path: Path, lines, values):
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description="配置 DobotAI 容器运行环境")
|
parser = argparse.ArgumentParser(description="配置 DobotAI Docker 应用连接")
|
||||||
parser.add_argument("--env-file", required=True)
|
parser.add_argument("--env-file", required=True)
|
||||||
parser.add_argument("--start-script", required=True)
|
parser.add_argument("--start-script", required=True)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
env_file = Path(args.env_file)
|
env_file = Path(args.env_file)
|
||||||
if not env_file.is_file():
|
if not env_file.is_file():
|
||||||
raise SystemExit("DobotAI 环境文件不存在,请先完成部署。")
|
raise SystemExit("DobotAI 环境文件不存在,请先完成 Docker 安装。")
|
||||||
lines, values = parse_env(env_file)
|
lines, values = parse_env(env_file)
|
||||||
for key, label, description, example, secret in FIELDS:
|
for key, label, description, example, secret in FIELDS:
|
||||||
entered = prompt(label, description, example, secret, bool(values.get(key, "").strip()))
|
entered = prompt(label, description, example, secret, bool(values.get(key, "").strip()))
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
container="dobotai-dingtalk"
|
home="${DOBOTAI_HOME:-/opt/dobotai}"
|
||||||
if [[ "$(docker inspect --format '{{.State.Running}}' "$container" 2>/dev/null || true)" != "true" ]]; then
|
container_dir="$home/container"
|
||||||
|
if [[ "$(docker inspect --format '{{.State.Running}}' dobotai-dingtalk 2>/dev/null || true)" != "true" ]]; then
|
||||||
echo "DobotAI 容器未运行。请先执行:sudo dobotai-dingtalk-configure" >&2
|
echo "DobotAI 容器未运行。请先执行:sudo dobotai-dingtalk-configure" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
exec docker exec -it dobotai-dingtalk /usr/local/bin/dobotai bootstrap-admin
|
exec docker exec -it dobotai-dingtalk /usr/local/bin/dobotai bootstrap-admin
|
||||||
|
|||||||
@@ -1,23 +1,18 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
container_dir="/opt/dobotai-test/container"
|
home="${DOBOTAI_HOME:-/opt/dobotai}"
|
||||||
|
container_dir="$home/container"
|
||||||
compose_file="$container_dir/compose.yaml"
|
compose_file="$container_dir/compose.yaml"
|
||||||
runtime_env="$container_dir/runtime.env"
|
runtime_env="$container_dir/runtime.env"
|
||||||
legacy_service="dobotai-test.service"
|
|
||||||
health_url="http://127.0.0.1:8090/healthz"
|
health_url="http://127.0.0.1:8090/healthz"
|
||||||
|
|
||||||
if [[ ! -r "$compose_file" || ! -r "$runtime_env" ]]; then
|
if [[ ! -r "$compose_file" || ! -r "$runtime_env" ]]; then
|
||||||
echo "容器运行文件未安装。请先部署 dobotai-dingtalk。" >&2
|
echo "Docker 运行文件未安装。请先执行 install-docker.sh。" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
legacy_was_active=false
|
compose=(docker compose --env-file "$runtime_env" -f "$compose_file")
|
||||||
if systemctl is-active --quiet "$legacy_service"; then
|
|
||||||
legacy_was_active=true
|
|
||||||
systemctl stop "$legacy_service"
|
|
||||||
fi
|
|
||||||
|
|
||||||
previous_image="$(docker inspect --format '{{.Config.Image}}' dobotai-dingtalk 2>/dev/null || true)"
|
previous_image="$(docker inspect --format '{{.Config.Image}}' dobotai-dingtalk 2>/dev/null || true)"
|
||||||
|
|
||||||
set_runtime_image() {
|
set_runtime_image() {
|
||||||
@@ -26,19 +21,11 @@ set_runtime_image() {
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
path = Path(sys.argv[1])
|
path = Path(sys.argv[1])
|
||||||
image = sys.argv[2]
|
image = sys.argv[2]
|
||||||
lines = path.read_text(encoding="utf-8").splitlines()
|
lines = path.read_text(encoding="utf-8").splitlines()
|
||||||
updated = False
|
rendered = ["DOBOTAI_IMAGE=" + image if line.startswith("DOBOTAI_IMAGE=") else line for line in lines]
|
||||||
rendered = []
|
if not any(line.startswith("DOBOTAI_IMAGE=") for line in lines):
|
||||||
for line in lines:
|
|
||||||
if line.startswith("DOBOTAI_IMAGE="):
|
|
||||||
rendered.append("DOBOTAI_IMAGE=" + image)
|
|
||||||
updated = True
|
|
||||||
else:
|
|
||||||
rendered.append(line)
|
|
||||||
if not updated:
|
|
||||||
rendered.append("DOBOTAI_IMAGE=" + image)
|
rendered.append("DOBOTAI_IMAGE=" + image)
|
||||||
temporary = path.with_name(path.name + ".new")
|
temporary = path.with_name(path.name + ".new")
|
||||||
temporary.write_text("\n".join(rendered) + "\n", encoding="utf-8")
|
temporary.write_text("\n".join(rendered) + "\n", encoding="utf-8")
|
||||||
@@ -48,20 +35,18 @@ PY
|
|||||||
}
|
}
|
||||||
|
|
||||||
rollback() {
|
rollback() {
|
||||||
docker compose --env-file "$runtime_env" -f "$compose_file" down --remove-orphans >/dev/null 2>&1 || true
|
"${compose[@]}" rm -sf dobotai-dingtalk >/dev/null 2>&1 || true
|
||||||
if [[ -n "$previous_image" ]]; then
|
if [[ -n "$previous_image" ]]; then
|
||||||
set_runtime_image "$previous_image"
|
set_runtime_image "$previous_image"
|
||||||
docker compose --env-file "$runtime_env" -f "$compose_file" up -d --no-build --force-recreate >/dev/null 2>&1 || true
|
"${compose[@]}" up -d --no-build --force-recreate dobotai-dingtalk >/dev/null 2>&1 || true
|
||||||
elif [[ "$legacy_was_active" == "true" ]]; then
|
|
||||||
systemctl start "$legacy_service" || true
|
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
trap rollback ERR
|
trap rollback ERR
|
||||||
|
|
||||||
docker compose --env-file "$runtime_env" -f "$compose_file" up -d --no-build --force-recreate
|
"${compose[@]}" up -d --no-build dobotai-postgres
|
||||||
|
"${compose[@]}" up -d --no-build --force-recreate dobotai-dingtalk
|
||||||
for _ in $(seq 1 30); do
|
for _ in $(seq 1 30); do
|
||||||
if curl --noproxy '*' --fail --silent --max-time 2 "$health_url" >/dev/null; then
|
if curl --noproxy '*' --fail --silent --max-time 2 "$health_url" >/dev/null; then
|
||||||
systemctl disable "$legacy_service" >/dev/null
|
|
||||||
trap - ERR
|
trap - ERR
|
||||||
echo "DobotAI 容器已启动:dobotai-dingtalk"
|
echo "DobotAI 容器已启动:dobotai-dingtalk"
|
||||||
exit 0
|
exit 0
|
||||||
@@ -69,9 +54,5 @@ for _ in $(seq 1 30); do
|
|||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
|
|
||||||
if [[ -n "$previous_image" ]]; then
|
echo "DobotAI 容器健康检查失败,已回滚到上一容器镜像(如存在)。" >&2
|
||||||
echo "DobotAI 容器健康检查失败,已回滚到上一容器镜像。" >&2
|
|
||||||
else
|
|
||||||
echo "DobotAI 容器健康检查失败,已回滚到原生服务。" >&2
|
|
||||||
fi
|
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
source_root="${DOBOTAI_SOURCE_ROOT:-$(cd "$script_dir/../.." && pwd)}"
|
||||||
|
home="${DOBOTAI_HOME:-/opt/dobotai}"
|
||||||
|
container_dir="$home/container"
|
||||||
|
config_dir="$home/config"
|
||||||
|
secret_dir="$home/secrets"
|
||||||
|
backup_dir="$home/backups"
|
||||||
|
app_env="$config_dir/dobotai.env"
|
||||||
|
postgres_env="$config_dir/postgres.env"
|
||||||
|
runtime_env="$container_dir/runtime.env"
|
||||||
|
image="${DOBOTAI_IMAGE:?set DOBOTAI_IMAGE to a reviewed DobotAI image tag}"
|
||||||
|
postgres_image="${DOBOTAI_POSTGRES_IMAGE:-postgres:16-alpine}"
|
||||||
|
bin_dir="${DOBOTAI_BIN_DIR:-/usr/local/sbin}"
|
||||||
|
|
||||||
|
if [[ ${EUID} -ne 0 ]]; then
|
||||||
|
echo "请使用 sudo 执行 Docker 安装脚本。" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
command -v docker >/dev/null || { echo "Docker 未安装。" >&2; exit 1; }
|
||||||
|
docker compose version >/dev/null || { echo "Docker Compose 未安装。" >&2; exit 1; }
|
||||||
|
docker image inspect "$image" >/dev/null 2>&1 || { echo "候选应用镜像不存在:$image" >&2; exit 1; }
|
||||||
|
|
||||||
|
install -d -m 0750 "$home" "$container_dir" "$config_dir" "$backup_dir"
|
||||||
|
install -d -m 0700 -o 10001 -g 10001 "$secret_dir"
|
||||||
|
install -d -m 0750 "$container_dir/migrations"
|
||||||
|
install -m 0644 "$script_dir/compose.yaml" "$container_dir/compose.yaml"
|
||||||
|
install -m 0700 "$script_dir/configure_env.py" "$container_dir/configure_env.py"
|
||||||
|
install -m 0750 "$script_dir/dobotai-dingtalk-start" "$container_dir/dobotai-dingtalk-start"
|
||||||
|
install -m 0750 "$script_dir/dobotai-dingtalk-bind-admin" "$container_dir/dobotai-dingtalk-bind-admin"
|
||||||
|
install -m 0750 "$script_dir/apply-migrations.sh" "$container_dir/apply-migrations.sh"
|
||||||
|
install -m 0750 "$script_dir/migrate-existing-database.sh" "$container_dir/migrate-existing-database.sh"
|
||||||
|
install -m 0644 "$source_root"/db/migrations/*.up.sql "$container_dir/migrations/"
|
||||||
|
install -m 0644 "$source_root"/db/migrations/*.down.sql "$container_dir/migrations/"
|
||||||
|
|
||||||
|
if [[ ! -f "$postgres_env" || ! -f "$app_env" ]]; then
|
||||||
|
umask 077
|
||||||
|
python3 - "$postgres_env" "$app_env" <<'PY'
|
||||||
|
from pathlib import Path
|
||||||
|
import os
|
||||||
|
import secrets
|
||||||
|
import sys
|
||||||
|
|
||||||
|
postgres_env = Path(sys.argv[1])
|
||||||
|
app_env = Path(sys.argv[2])
|
||||||
|
password = secrets.token_urlsafe(32)
|
||||||
|
ingress = secrets.token_urlsafe(32)
|
||||||
|
admin = secrets.token_urlsafe(32)
|
||||||
|
postgres_env.write_text(
|
||||||
|
"POSTGRES_DB=dobotai\n"
|
||||||
|
"POSTGRES_USER=dobotai\n"
|
||||||
|
f"POSTGRES_PASSWORD={password}\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
os.chmod(postgres_env, 0o600)
|
||||||
|
app_env.write_text(
|
||||||
|
"DOBOTAI_LISTEN_ADDR=0.0.0.0:8090\n"
|
||||||
|
f"DATABASE_URL=postgres://dobotai:{password}@dobotai-postgres:5432/dobotai?sslmode=disable\n"
|
||||||
|
f"DOBOTAI_INGRESS_TOKEN={ingress}\n"
|
||||||
|
f"DOBOTAI_ADMIN_TOKEN={admin}\n"
|
||||||
|
"DOBOTAI_GROUP_SESSION_POLICY=shared\n"
|
||||||
|
"DOBOTAI_TENANT_KEY=dobotai\n"
|
||||||
|
"DOBOTAI_AGENT_BINDINGS=\n"
|
||||||
|
"DOBOTAI_QUICK_AGENT_ID=builtin-quick-answer\n"
|
||||||
|
"DOBOTAI_REASONING_AGENT_ID=builtin-smart-reasoning\n"
|
||||||
|
"WEKNORA_BASE_URL=http://host.docker.internal:8080\n"
|
||||||
|
"WEKNORA_API_KEY=\n"
|
||||||
|
"DINGTALK_CLIENT_ID=\n"
|
||||||
|
"DINGTALK_CLIENT_SECRET=\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
os.chmod(app_env, 0o600)
|
||||||
|
PY
|
||||||
|
fi
|
||||||
|
|
||||||
|
umask 077
|
||||||
|
cat > "$runtime_env" <<EOF
|
||||||
|
DOBOTAI_IMAGE=$image
|
||||||
|
DOBOTAI_POSTGRES_IMAGE=$postgres_image
|
||||||
|
DOBOTAI_ENV_FILE=$app_env
|
||||||
|
DOBOTAI_POSTGRES_ENV_FILE=$postgres_env
|
||||||
|
DOBOTAI_SECRET_DIR_HOST=$secret_dir
|
||||||
|
DOBOTAI_MIGRATIONS_DIR=$container_dir/migrations
|
||||||
|
EOF
|
||||||
|
chmod 0600 "$runtime_env"
|
||||||
|
|
||||||
|
install -d -m 0755 "$bin_dir"
|
||||||
|
cat > "$bin_dir/dobotai-dingtalk-configure" <<EOF
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
export DOBOTAI_HOME='$home'
|
||||||
|
exec python3 '$container_dir/configure_env.py' --env-file '$app_env' --start-script '$bin_dir/dobotai-dingtalk-start'
|
||||||
|
EOF
|
||||||
|
cat > "$bin_dir/dobotai-dingtalk-start" <<EOF
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
export DOBOTAI_HOME='$home'
|
||||||
|
exec '$container_dir/dobotai-dingtalk-start'
|
||||||
|
EOF
|
||||||
|
cat > "$bin_dir/dobotai-dingtalk-bind-admin" <<EOF
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
export DOBOTAI_HOME='$home'
|
||||||
|
exec '$container_dir/dobotai-dingtalk-bind-admin'
|
||||||
|
EOF
|
||||||
|
chmod 0750 "$bin_dir/dobotai-dingtalk-configure" "$bin_dir/dobotai-dingtalk-start" "$bin_dir/dobotai-dingtalk-bind-admin"
|
||||||
|
|
||||||
|
compose=(docker compose --env-file "$runtime_env" -f "$container_dir/compose.yaml")
|
||||||
|
"${compose[@]}" up -d dobotai-postgres
|
||||||
|
for _ in $(seq 1 30); do
|
||||||
|
status=$(docker inspect -f '{{.State.Health.Status}}' dobotai-postgres 2>/dev/null || true)
|
||||||
|
[[ "$status" == "healthy" ]] && break
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
[[ "${status:-}" == "healthy" ]] || { echo "PostgreSQL 初始化失败。" >&2; exit 1; }
|
||||||
|
DOBOTAI_HOME="$home" "$container_dir/apply-migrations.sh"
|
||||||
|
|
||||||
|
echo "Docker 基础设施已准备完成:数据库与迁移已就绪,应用尚未启动。"
|
||||||
|
echo "下一步执行:sudo dobotai-dingtalk-configure"
|
||||||
@@ -1,54 +1,4 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
container_dir="${DOBOTAI_CONTAINER_DIR:-/opt/dobotai-test/container}"
|
exec "$script_dir/install-docker.sh" "$@"
|
||||||
service="${DOBOTAI_LEGACY_SERVICE:-dobotai-test.service}"
|
|
||||||
runtime_user="${DOBOTAI_RUNTIME_USER:-dobotai-test}"
|
|
||||||
env_file="${DOBOTAI_ENV_FILE:-/etc/dobotai-test/dobotai.env}"
|
|
||||||
secret_dir="${DOBOTAI_SECRET_DIR_HOST:-/var/lib/dobotai-test/secrets}"
|
|
||||||
image="${DOBOTAI_IMAGE:?set DOBOTAI_IMAGE to the reviewed image tag before installation}"
|
|
||||||
backup_root="${DOBOTAI_BACKUP_ROOT:-/opt/dobotai-test/backups}"
|
|
||||||
|
|
||||||
if [[ ${EUID} -ne 0 ]]; then
|
|
||||||
echo "请使用 sudo 执行安装脚本。" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if ! docker image inspect "$image" >/dev/null 2>&1; then
|
|
||||||
echo "候选镜像不存在:$image" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ ! -r "$env_file" || ! -d "$secret_dir" ]]; then
|
|
||||||
echo "现有 DobotAI 环境文件或密钥目录不存在,拒绝安装。" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
uid="$(id -u "$runtime_user")"
|
|
||||||
gid="$(id -g "$runtime_user")"
|
|
||||||
stamp="$(date -u '+%Y%m%dT%H%M%SZ')"
|
|
||||||
backup_dir="$backup_root/pre-container-$stamp"
|
|
||||||
install -d -m 0700 "$backup_dir" "$container_dir"
|
|
||||||
cp -p /opt/dobotai-test/bin/dobotai "$backup_dir/dobotai.native"
|
|
||||||
cp -p /etc/systemd/system/dobotai-test.service "$backup_dir/dobotai-test.service"
|
|
||||||
sha256sum "$backup_dir/dobotai.native" > "$backup_dir/SHA256SUMS"
|
|
||||||
|
|
||||||
install -m 0644 "$script_dir/compose.yaml" "$container_dir/compose.yaml"
|
|
||||||
install -m 0750 "$script_dir/dobotai-dingtalk-start" /usr/local/sbin/dobotai-dingtalk-start
|
|
||||||
install -m 0750 "$script_dir/dobotai-dingtalk-configure" /usr/local/sbin/dobotai-dingtalk-configure
|
|
||||||
install -m 0750 "$script_dir/dobotai-dingtalk-bind-admin" /usr/local/sbin/dobotai-dingtalk-bind-admin
|
|
||||||
install -m 0700 "$script_dir/configure_env.py" "$container_dir/configure_env.py"
|
|
||||||
|
|
||||||
runtime_tmp="$container_dir/runtime.env.new"
|
|
||||||
umask 077
|
|
||||||
cat > "$runtime_tmp" <<EOF
|
|
||||||
DOBOTAI_IMAGE=$image
|
|
||||||
DOBOTAI_RUNTIME_UID=$uid
|
|
||||||
DOBOTAI_RUNTIME_GID=$gid
|
|
||||||
DOBOTAI_ENV_FILE=$env_file
|
|
||||||
DOBOTAI_SECRET_DIR_HOST=$secret_dir
|
|
||||||
EOF
|
|
||||||
chmod 0600 "$runtime_tmp"
|
|
||||||
mv -f "$runtime_tmp" "$container_dir/runtime.env"
|
|
||||||
|
|
||||||
echo "容器资产已安装,旧服务仍在运行。"
|
|
||||||
echo "下一步执行:sudo dobotai-dingtalk-configure"
|
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
home="${DOBOTAI_HOME:-/opt/dobotai}"
|
||||||
|
container_dir="$home/container"
|
||||||
|
config_dir="$home/config"
|
||||||
|
runtime_env="$container_dir/runtime.env"
|
||||||
|
compose_file="$container_dir/compose.yaml"
|
||||||
|
source_container="${SOURCE_POSTGRES_CONTAINER:-dobotai-test-postgres}"
|
||||||
|
source_app_container="${SOURCE_APP_CONTAINER:-dobotai-dingtalk}"
|
||||||
|
source_env="${SOURCE_DOBOTAI_ENV:-/etc/dobotai-test/dobotai.env}"
|
||||||
|
app_env="$config_dir/dobotai.env"
|
||||||
|
backup_dir="$home/backups"
|
||||||
|
stamp="$(date -u '+%Y%m%dT%H%M%SZ')"
|
||||||
|
legacy_app="${source_app_container}-legacy-${stamp}"
|
||||||
|
|
||||||
|
[[ ${EUID} -eq 0 ]] || { echo "请使用 sudo 执行现有数据库迁移脚本。" >&2; exit 1; }
|
||||||
|
[[ -r "$runtime_env" && -r "$compose_file" && -r "$source_env" ]] || { echo "运行文件或旧环境文件不存在。请先执行 install-docker.sh。" >&2; exit 1; }
|
||||||
|
docker inspect "$source_container" >/dev/null 2>&1 || { echo "源 PostgreSQL 容器不存在:$source_container" >&2; exit 1; }
|
||||||
|
docker inspect "$source_app_container" >/dev/null 2>&1 || { echo "源 DobotAI 容器不存在:$source_app_container" >&2; exit 1; }
|
||||||
|
|
||||||
|
source_pg_user=$(docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' "$source_container" | python3 -c "import sys; print(next((x.strip().split('=',1)[1] for x in sys.stdin if x.startswith('POSTGRES_USER=')), ''))")
|
||||||
|
source_pg_db=$(docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' "$source_container" | python3 -c "import sys; print(next((x.strip().split('=',1)[1] for x in sys.stdin if x.startswith('POSTGRES_DB=')), ''))")
|
||||||
|
test -n "$source_pg_user" && test -n "$source_pg_db"
|
||||||
|
install -d -m 0700 "$backup_dir"
|
||||||
|
dump="$backup_dir/pre-compose-adoption-${stamp}.dump"
|
||||||
|
|
||||||
|
compose=(docker compose --env-file "$runtime_env" -f "$compose_file")
|
||||||
|
"${compose[@]}" up -d dobotai-postgres
|
||||||
|
for _ in $(seq 1 30); do
|
||||||
|
status=$(docker inspect -f '{{.State.Health.Status}}' dobotai-postgres 2>/dev/null || true)
|
||||||
|
[[ "$status" == "healthy" ]] && break
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
[[ "${status:-}" == "healthy" ]] || { echo "目标 PostgreSQL 未健康。" >&2; exit 1; }
|
||||||
|
|
||||||
|
old_running=$(docker inspect -f '{{.State.Running}}' "$source_app_container")
|
||||||
|
rollback() {
|
||||||
|
docker compose --env-file "$runtime_env" -f "$compose_file" rm -sf dobotai-dingtalk >/dev/null 2>&1 || true
|
||||||
|
if docker inspect "$legacy_app" >/dev/null 2>&1; then
|
||||||
|
docker rename "$legacy_app" "$source_app_container" || true
|
||||||
|
[[ "$old_running" == "true" ]] && docker start "$source_app_container" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap rollback ERR
|
||||||
|
|
||||||
|
[[ "$old_running" == "true" ]] && docker stop "$source_app_container" >/dev/null
|
||||||
|
docker rename "$source_app_container" "$legacy_app"
|
||||||
|
docker exec -e PGUSER="$source_pg_user" "$source_container" pg_dump -Fc --no-owner -d "$source_pg_db" > "$dump"
|
||||||
|
test -s "$dump"
|
||||||
|
docker exec -i dobotai-postgres pg_restore --list < "$dump" >/dev/null
|
||||||
|
sha256sum "$dump" > "$dump.sha256"
|
||||||
|
|
||||||
|
# Replace only deployment-managed endpoints; preserve all existing application credentials.
|
||||||
|
python3 - "$source_env" "$app_env" "$config_dir/postgres.env" <<'PY'
|
||||||
|
from pathlib import Path
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
source, target, postgres_env = map(Path, sys.argv[1:])
|
||||||
|
password = next(line.split('=', 1)[1].strip() for line in postgres_env.read_text().splitlines() if line.startswith('POSTGRES_PASSWORD='))
|
||||||
|
values = {}
|
||||||
|
for line in source.read_text().splitlines():
|
||||||
|
if line and not line.startswith('#') and '=' in line:
|
||||||
|
key, value = line.split('=', 1)
|
||||||
|
values[key] = value
|
||||||
|
values['DOBOTAI_LISTEN_ADDR'] = '0.0.0.0:8090'
|
||||||
|
values['DATABASE_URL'] = f'postgres://dobotai:{password}@dobotai-postgres:5432/dobotai?sslmode=disable'
|
||||||
|
values['WEKNORA_BASE_URL'] = 'http://host.docker.internal:8080'
|
||||||
|
temporary = target.with_name(target.name + '.new')
|
||||||
|
temporary.write_text('\n'.join(f'{k}={v}' for k, v in values.items()) + '\n', encoding='utf-8')
|
||||||
|
os.chmod(temporary, 0o600)
|
||||||
|
os.replace(temporary, target)
|
||||||
|
PY
|
||||||
|
|
||||||
|
"${compose[@]}" exec -T dobotai-postgres psql -v ON_ERROR_STOP=1 -U dobotai -d dobotai -c 'DROP SCHEMA public CASCADE; CREATE SCHEMA public;'
|
||||||
|
docker exec -i dobotai-postgres pg_restore -U dobotai -d dobotai --no-owner < "$dump"
|
||||||
|
DOBOTAI_HOME="$home" "$container_dir/apply-migrations.sh"
|
||||||
|
"${compose[@]}" up -d --no-build dobotai-dingtalk
|
||||||
|
for _ in $(seq 1 30); do
|
||||||
|
if curl --noproxy '*' --fail --silent --max-time 2 http://127.0.0.1:8090/healthz >/dev/null; then
|
||||||
|
trap - ERR
|
||||||
|
echo "现有DobotAI数据库已迁移到 dobotai-postgres-data;旧应用容器保留为 $legacy_app。"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "迁移后的应用健康检查失败,正在恢复旧容器。" >&2
|
||||||
|
exit 1
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
# 非敏感容器运行参数;install-test.sh 会根据测试服务器实际用户和目录生成此文件。
|
# 非敏感运行参数;由 install-docker.sh 生成实际文件。
|
||||||
DOBOTAI_IMAGE=dobotai-dingtalk:test
|
DOBOTAI_IMAGE=dobotai-dingtalk:test
|
||||||
DOBOTAI_RUNTIME_UID=996
|
DOBOTAI_POSTGRES_IMAGE=postgres:16-alpine
|
||||||
DOBOTAI_RUNTIME_GID=988
|
DOBOTAI_ENV_FILE=/opt/dobotai/config/dobotai.env
|
||||||
DOBOTAI_ENV_FILE=/etc/dobotai-test/dobotai.env
|
DOBOTAI_POSTGRES_ENV_FILE=/opt/dobotai/config/postgres.env
|
||||||
DOBOTAI_SECRET_DIR_HOST=/var/lib/dobotai-test/secrets
|
DOBOTAI_SECRET_DIR_HOST=/opt/dobotai/secrets
|
||||||
|
DOBOTAI_MIGRATIONS_DIR=/opt/dobotai/container/migrations
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# 唯一安装入口:镜像仓库名与标签策略只在本脚本维护。
|
||||||
|
postgres_image="postgres:16-alpine"
|
||||||
|
|
||||||
|
if [[ ${EUID} -ne 0 ]]; then
|
||||||
|
echo "请在克隆仓库根目录执行:sudo bash ./install-docker.sh" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cd "$repo_root"
|
||||||
|
[[ -f Dockerfile && -f deploy/dobotai-dingtalk/install-docker.sh ]] || {
|
||||||
|
echo "安装脚本必须从完整DobotAI源码仓库根目录运行。" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
command -v git >/dev/null || { echo "未找到 git,无法确定当前源码版本。" >&2; exit 1; }
|
||||||
|
git -C "$repo_root" rev-parse --is-inside-work-tree >/dev/null || {
|
||||||
|
echo "请从 git clone 得到的仓库中执行本脚本。" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
install_docker_if_missing() {
|
||||||
|
if command -v docker >/dev/null && docker compose version >/dev/null 2>&1; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
command -v apt-get >/dev/null || {
|
||||||
|
echo "本脚本仅自动安装基于 apt 的 Docker;请先安装 Docker Engine 和 Docker Compose v2。" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
apt-get update
|
||||||
|
if apt-cache show docker-compose-v2 >/dev/null 2>&1; then
|
||||||
|
apt-get install -y docker.io docker-compose-v2
|
||||||
|
elif apt-cache show docker-compose-plugin >/dev/null 2>&1; then
|
||||||
|
apt-get install -y docker.io docker-compose-plugin
|
||||||
|
else
|
||||||
|
echo "当前APT源没有 Docker Compose v2 插件,无法安全继续。" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
systemctl enable --now docker
|
||||||
|
docker compose version >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
install_docker_if_missing
|
||||||
|
|
||||||
|
commit_sha="$(git -C "$repo_root" rev-parse HEAD)"
|
||||||
|
commit_short="$(git -C "$repo_root" rev-parse --short=12 HEAD)"
|
||||||
|
build_time="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
|
image="dobotai-dingtalk:local-${commit_short}"
|
||||||
|
|
||||||
|
echo "[1/3] 构建 DobotAI 镜像:$image"
|
||||||
|
docker build \
|
||||||
|
--build-arg "GOPROXY=https://goproxy.cn,direct" \
|
||||||
|
--build-arg "VERSION=local-${commit_short}" \
|
||||||
|
--build-arg "COMMIT_SHA=${commit_sha}" \
|
||||||
|
--build-arg "BUILD_TIME=${build_time}" \
|
||||||
|
--label "org.opencontainers.image.revision=${commit_sha}" \
|
||||||
|
--tag "$image" \
|
||||||
|
"$repo_root"
|
||||||
|
|
||||||
|
echo "[2/3] 准备独立PostgreSQL、目录、配置文件和迁移"
|
||||||
|
export DOBOTAI_IMAGE="$image"
|
||||||
|
export DOBOTAI_POSTGRES_IMAGE="$postgres_image"
|
||||||
|
export DOBOTAI_SOURCE_ROOT="$repo_root"
|
||||||
|
exec bash "$repo_root/deploy/dobotai-dingtalk/install-docker.sh"
|
||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDobotAIDingTalkContainerAssetsProvideSafeShortOperatorCommands(t *testing.T) {
|
func TestDobotAIDingTalkComposeOwnsApplicationAndPostgres(t *testing.T) {
|
||||||
compose, err := os.ReadFile("../deploy/dobotai-dingtalk/compose.yaml")
|
compose, err := os.ReadFile("../deploy/dobotai-dingtalk/compose.yaml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -14,7 +14,13 @@ func TestDobotAIDingTalkContainerAssetsProvideSafeShortOperatorCommands(t *testi
|
|||||||
for _, required := range []string{
|
for _, required := range []string{
|
||||||
"dobotai-dingtalk:",
|
"dobotai-dingtalk:",
|
||||||
"container_name: dobotai-dingtalk",
|
"container_name: dobotai-dingtalk",
|
||||||
"network_mode: host",
|
"dobotai-postgres:",
|
||||||
|
"container_name: dobotai-postgres",
|
||||||
|
"dobotai-postgres-data:",
|
||||||
|
"depends_on:",
|
||||||
|
"condition: service_healthy",
|
||||||
|
"host.docker.internal:host-gateway",
|
||||||
|
"127.0.0.1:8090:8090",
|
||||||
"env_file:",
|
"env_file:",
|
||||||
"read_only: true",
|
"read_only: true",
|
||||||
"/var/lib/dobotai/secrets",
|
"/var/lib/dobotai/secrets",
|
||||||
@@ -28,11 +34,7 @@ func TestDobotAIDingTalkContainerAssetsProvideSafeShortOperatorCommands(t *testi
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
for _, required := range []string{
|
for _, required := range []string{"configure_env.py", "dobotai-dingtalk-start"} {
|
||||||
"configure_env.py",
|
|
||||||
"/etc/dobotai-test/dobotai.env",
|
|
||||||
"dobotai-dingtalk-start",
|
|
||||||
} {
|
|
||||||
if !strings.Contains(string(configure), required) {
|
if !strings.Contains(string(configure), required) {
|
||||||
t.Errorf("configure command missing %q", required)
|
t.Errorf("configure command missing %q", required)
|
||||||
}
|
}
|
||||||
@@ -42,10 +44,7 @@ func TestDobotAIDingTalkContainerAssetsProvideSafeShortOperatorCommands(t *testi
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
for _, required := range []string{
|
for _, required := range []string{"docker exec -it dobotai-dingtalk", "/usr/local/bin/dobotai bootstrap-admin"} {
|
||||||
"docker exec -it dobotai-dingtalk",
|
|
||||||
"/usr/local/bin/dobotai bootstrap-admin",
|
|
||||||
} {
|
|
||||||
if !strings.Contains(string(bind), required) {
|
if !strings.Contains(string(bind), required) {
|
||||||
t.Errorf("bind command missing %q", required)
|
t.Errorf("bind command missing %q", required)
|
||||||
}
|
}
|
||||||
@@ -83,8 +82,7 @@ func TestDobotAIDingTalkCredentialConfigurationExplainsEveryInputWithRedactedExa
|
|||||||
t.Fatal("configuration guidance must label every example")
|
t.Fatal("configuration guidance must label every example")
|
||||||
}
|
}
|
||||||
for _, example := range []string{
|
for _, example := range []string{
|
||||||
"postgres://USER:<数据库密码>@127.0.0.1:55432/dobotai_test?sslmode=disable",
|
"http://host.docker.internal:8080",
|
||||||
"http://127.0.0.1:8080",
|
|
||||||
`{"dingtalk:*":"你的Agent ID"}`,
|
`{"dingtalk:*":"你的Agent ID"}`,
|
||||||
"DINGTALK_CLIENT_SECRET",
|
"DINGTALK_CLIENT_SECRET",
|
||||||
} {
|
} {
|
||||||
@@ -95,6 +93,12 @@ func TestDobotAIDingTalkCredentialConfigurationExplainsEveryInputWithRedactedExa
|
|||||||
t.Errorf("documentation missing redacted example %q", example)
|
t.Errorf("documentation missing redacted example %q", example)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if strings.Contains(string(configurator), "DATABASE_URL") {
|
||||||
|
t.Fatal("database URL must be deployment-managed, not requested interactively")
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(documentation), "dobotai-postgres") {
|
||||||
|
t.Fatal("documentation must explain the Compose-managed database")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDobotAIDingTalkStartRollsBackToPreviousContainerImage(t *testing.T) {
|
func TestDobotAIDingTalkStartRollsBackToPreviousContainerImage(t *testing.T) {
|
||||||
@@ -108,10 +112,85 @@ func TestDobotAIDingTalkStartRollsBackToPreviousContainerImage(t *testing.T) {
|
|||||||
"set_runtime_image",
|
"set_runtime_image",
|
||||||
"docker inspect --format",
|
"docker inspect --format",
|
||||||
"set_runtime_image \"$previous_image\"",
|
"set_runtime_image \"$previous_image\"",
|
||||||
"docker compose --env-file \"$runtime_env\" -f \"$compose_file\" up -d --no-build --force-recreate",
|
"\"${compose[@]}\" up -d --no-build --force-recreate dobotai-dingtalk",
|
||||||
} {
|
} {
|
||||||
if !strings.Contains(source, required) {
|
if !strings.Contains(source, required) {
|
||||||
t.Errorf("start script missing container upgrade rollback %q", required)
|
t.Errorf("start script missing container upgrade rollback %q", required)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDobotAIDingTalkDockerInstallerPreparesDirectoriesDatabaseAndMigrations(t *testing.T) {
|
||||||
|
data, err := os.ReadFile("../deploy/dobotai-dingtalk/install-docker.sh")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
source := string(data)
|
||||||
|
for _, required := range []string{
|
||||||
|
"container", "secrets", "backups", "migrations", "postgres.env",
|
||||||
|
"dobotai-postgres", "apply-migrations.sh", "docker compose", "umask 077",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(source, required) {
|
||||||
|
t.Errorf("docker installer missing %q", required)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(source, "dobotai-dingtalk up -d") {
|
||||||
|
t.Fatal("installer must prepare database but must not start the unconfigured application")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDobotAIDingTalkExistingDatabaseMigrationHasBackupAndRollbackBoundary(t *testing.T) {
|
||||||
|
data, err := os.ReadFile("../deploy/dobotai-dingtalk/migrate-existing-database.sh")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
source := string(data)
|
||||||
|
for _, required := range []string{
|
||||||
|
"pg_dump -Fc", "pg_restore --list", "dobotai-postgres-data",
|
||||||
|
"dobotai-dingtalk", "rollback", "apply-migrations.sh",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(source, required) {
|
||||||
|
t.Errorf("database adoption script missing %q", required)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRootDockerInstallerIsCloneAndRunEntryPoint(t *testing.T) {
|
||||||
|
data, err := os.ReadFile("../install-docker.sh")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
source := string(data)
|
||||||
|
for _, required := range []string{
|
||||||
|
"docker build",
|
||||||
|
"dobotai-dingtalk:local-",
|
||||||
|
"install_docker_if_missing",
|
||||||
|
"docker compose version",
|
||||||
|
"DOBOTAI_SOURCE_ROOT",
|
||||||
|
"deploy/dobotai-dingtalk/install-docker.sh",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(source, required) {
|
||||||
|
t.Errorf("root installer missing %q", required)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(source, "${DOBOTAI_IMAGE") || strings.Contains(source, "DOBOTAI_IMAGE=?") {
|
||||||
|
t.Fatal("root installer must not require an operator supplied DOBOTAI_IMAGE")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRootDockerInstallerBuildsWithInternalGoModuleProxy(t *testing.T) {
|
||||||
|
installer, err := os.ReadFile("../install-docker.sh")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
dockerfile, err := os.ReadFile("../Dockerfile")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(installer), "GOPROXY=https://goproxy.cn,direct") {
|
||||||
|
t.Fatal("root installer must pass an internal default GOPROXY to docker build")
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(dockerfile), "ARG GOPROXY") || !strings.Contains(string(dockerfile), "GOPROXY=$GOPROXY go mod download") {
|
||||||
|
t.Fatal("Dockerfile must accept and use the root installer GOPROXY")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user