WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s

This commit is contained in:
yawan
2026-09-22 16:19:06 +08:00
commit f7fc992f57
3929 changed files with 1006607 additions and 0 deletions
+276
View File
@@ -0,0 +1,276 @@
# WeKnora 云镜像打包脚本(Cloud-Agnostic)
> **本文档面向「想把 WeKnora 打包成云镜像(AMI / 自定义镜像 / Snapshot)分发给其他人」的用户。**
> **如果你只是想自己用 WeKnora,请直接看主仓 [README](../../README.md),`docker compose up -d` 即可。**
## 这套脚本能做什么
帮你把任意一台「能跑 Docker 的 Linux 实例」变成一份**可分发的云镜像模板**:
- 别人基于这份镜像创建新实例后,**首次开机会自动**:
- 生成全新的随机密钥(DB / Redis / JWT / AES)
- 启动 WeKnora 全部默认容器
- 把生成的凭证写到 `/root/weknora-credentials.txt`
- 自删除一次性初始化脚本
- 实现「**开机即用、零私密泄漏、每实例独立密钥**」
适用平台(任意 systemd + Docker 的 Linux 都行):
- 腾讯云轻量应用服务器(Lighthouse) / 云服务器 CVM
- AWS EC2 AMI
- 阿里云 ECS 自定义镜像
- 火山引擎 / 华为云 / Vultr Snapshot
- 本地 KVM / Proxmox 模板
各平台具体的「制作镜像 / 共享 / 上架」操作步骤,请参考 [`docs/cloud-image/`](../../docs/cloud-image/) 下对应文档。
---
## 目录结构
```
scripts/cloud-image/
├── README.md # 本文档
├── prepare.sh # 步骤一: 装 Docker + 拉运行时 + 装 firstboot
├── cleanup.sh # 步骤二: 制作镜像前清理(执行后将锁定 SSH)
├── firstboot.sh # 新实例首次开机自动执行(用户无感)
└── systemd/
├── weknora.service # 开机自启 docker compose
└── weknora-firstboot.service # 首次启动 init(执行后自删)
```
## 不需要 clone 整个 WeKnora 仓库
WeKnora 所有容器都从 Docker Hub 拉镜像(`wechatopenai/weknora-*`),Go / Python / 前端源码都不需要带到宿主机。
`docker-compose.yml` 实际从宿主机挂载到容器的只有:
```
- ./config/config.yaml (单文件)
- ./skills/preloaded/ (目录)
```
所以镜像里需要的运行时文件**总共不到 100KB**:
| 文件 | 大小 | 用途 |
|---|---|---|
| `docker-compose.yml` | 12K | 容器编排 |
| `.env` | 12K | 环境变量 |
| `config/config.yaml` | 8K | 后端业务配置 |
| `skills/preloaded/` | 56K | Agent 预置技能 |
`prepare.sh` 用 `curl + tar` 只下载这 4 项,不 `git clone`。
## 镜像里启动哪些容器
WeKnora `docker-compose.yml` 大量服务是 **profile 限定**,本镜像只默认启动核心 5 个。
**默认启动(5 个常驻容器,开机自启):**
| 容器 | 角色 |
|---|---|
| `frontend` | Vue UI / NGINX 反代 |
| `app` | WeKnora Go 后端 |
| `docreader` | Python 文档解析 (gRPC) |
| `postgres` (ParadeDB) | 主库 + pgvector 向量检索 + BM25 |
| `redis` | 流式输出 / 缓存 / 异步队列 |
> ParadeDB 自带 pgvector,默认场景下不需额外起向量库。
**额外预拉但不常驻:**
- `sandbox` 镜像:Agent Skills 由 app 按需 `docker run`。提前 `pull` 避免新实例首次执行 Skill 卡在下载。
**Profile 限定,不预装(用户需要时自己 `pull`):**
| profile | 用途 |
|---|---|
| `minio` | 对象存储替代本地文件 |
| `qdrant` / `milvus` / `weaviate` / `doris` | 替代 pgvector |
| `neo4j` | GraphRAG 知识图谱 |
| `langfuse` | 自建 Langfuse 可观测平台 |
| `dex` | OIDC 登录 |
| `odl-hybrid` | OpenDataLoader Docling hybrid(体积大,无预发布镜像,需 `--build`) |
启用方式:
```bash
cd /opt/WeKnora
docker compose --profile neo4j up -d # 启用 GraphRAG
docker compose --profile langfuse up -d # 启用自建 Langfuse
docker compose --profile qdrant up -d # 切换到 Qdrant
docker compose --profile odl-hybrid up -d --build odl-hybrid # Docling hybrid(按需)
```
---
## 完整流程(云无关)
```
1) 在目标云上买/装一台干净 Linux 实例(建议 4C8G+,Ubuntu 22.04)
2) SSH 进去, 拷入本目录, 执行 prepare.sh
3) 浏览器验证功能
4) 执行 cleanup.sh (清掉私密 + SSH key, 自动关机)
5) 在云控制台「制作镜像 / 创建快照 / 创建 AMI」
6) 用新镜像创建测试实例, 验证 firstboot 工作正常
7) 共享 / 公开镜像(参考各平台文档)
```
### 步骤一:在干净实例上部署
要求:systemd + 联网 + sudo 权限。推荐 Ubuntu 22.04 / Debian 12 / CentOS Stream 9。
**1. 拷入脚本(任选一种,都不用 clone 整个 WeKnora 仓库)。**
> 命令需要写入 `/opt/`,最省心的做法是先 `sudo -i` 切到 root 再粘贴。
> 如果坚持每行加 `sudo`,注意 `>>` 重定向是在你当前 shell 执行的,必须改用 `sudo tee -a`。
> **中国大陆云主机请直接看方式 C (scp)**。实测腾讯云 / 阿里云轻量服务器经常连 `github.com`、`raw.githubusercontent.com`、`gh-proxy.com` 这类境外 / 公益代理都连不上 (TLS RST 或超时),方式 A/B 会全军覆没。本机已经有这份仓库,scp 上去最稳。
```bash
sudo -i # 切到 root, 后续命令直接执行
# === 方式 A: sparse checkout (~60KB) ===
# 不通时设 GH_PROXY=https://gh-proxy.com/ 或 https://ghfast.top/, 注意末尾斜杠。
GH_PROXY="${GH_PROXY:-}"
mkdir -p /opt/weknora-tools && cd /opt/weknora-tools
git init -q && git remote add origin "${GH_PROXY}https://github.com/Tencent/WeKnora.git"
git config core.sparseCheckout true
echo "scripts/cloud-image/" >> .git/info/sparse-checkout
git pull -q --depth=1 origin main
# === 方式 B: 直接 curl (无 git 时用这个) ===
# 不通时设 GH_PROXY=https://gh-proxy.com/ 或 https://ghfast.top/, 注意末尾斜杠。
GH_PROXY="${GH_PROXY:-}"
mkdir -p /opt/weknora-tools/scripts/cloud-image/systemd && cd /opt/weknora-tools
base="${GH_PROXY}https://raw.githubusercontent.com/Tencent/WeKnora/main/scripts/cloud-image"
for f in prepare.sh cleanup.sh firstboot.sh README.md; do
curl -fsSL "$base/$f" -o "scripts/cloud-image/$f"
done
for f in weknora.service weknora-firstboot.service; do
curl -fsSL "$base/systemd/$f" -o "scripts/cloud-image/systemd/$f"
done
chmod +x scripts/cloud-image/*.sh
# === 方式 C: 从本地 scp 上来 (推荐: 中国大陆云主机直接走这条) ===
# 在本机 (能正常访问 GitHub 的机器) 执行:
# scp -r scripts/cloud-image root@<实例IP>:/opt/weknora-tools/scripts/
```
> 不确定 VM 能不能访问代理时,先探一下:
> `for h in gh-proxy.com ghfast.top mirror.ghproxy.com github.moeyy.xyz kkgithub.com; do printf '%-25s' "$h"; curl -sS -o /dev/null -m 5 -w 'http=%{http_code} t=%{time_total}s\n' "https://$h/" 2>&1 || echo FAIL; done`
> 哪个返回 `http=200/301/302` 就把 `GH_PROXY` 设成它 (后面加 `/`)。一个都不通的话,认命走方式 C。
**2. 执行部署:**
```bash
sudo bash /opt/weknora-tools/scripts/cloud-image/prepare.sh
# 想 pin 特定版本(推荐, 保证镜像可复现)
sudo WEKNORA_REF=v0.5.0 bash /opt/weknora-tools/scripts/cloud-image/prepare.sh
# 中国大陆机器三件套: 同时绕开 GitHub / get.docker.com / Docker Hub 的境外 CDN
# (以腾讯云为例, 阿里云 / 华为云换对应镜像即可)
sudo \
WEKNORA_REF=v0.5.0 \
WEKNORA_GH_PROXY=https://gh-proxy.com/ \
DOCKER_INSTALL_MIRROR=https://mirrors.tencent.com/docker-ce/linux/ubuntu \
DOCKER_REGISTRY_MIRROR=https://mirror.ccs.tencentyun.com \
bash /opt/weknora-tools/scripts/cloud-image/prepare.sh
```
> 三个变量分别解决三个不同的境外 CDN 不可达问题:
> - `WEKNORA_GH_PROXY`:加速 **GitHub tarball** 下载(`prepare.sh` 步骤 2,运行时文件)
> - `DOCKER_INSTALL_MIRROR`:绕开 **`get.docker.com`**,改用 apt + docker-ce 镜像源装 Docker(步骤 1)
> - `DOCKER_REGISTRY_MIRROR`:加速 **Docker Hub** 镜像拉取(步骤 4,`wechatopenai/weknora-*`)
>
> 不同云厂商对应地址(按需替换 ubuntu/debian 部分以匹配实际发行版):
> | 厂商 | `DOCKER_INSTALL_MIRROR` | `DOCKER_REGISTRY_MIRROR` |
> |---|---|---|
> | 腾讯云 | `https://mirrors.tencent.com/docker-ce/linux/ubuntu` | `https://mirror.ccs.tencentyun.com` |
> | 阿里云 | `https://mirrors.aliyun.com/docker-ce/linux/ubuntu` | `https://<your-id>.mirror.aliyuncs.com` |
> | 华为云 | `https://mirrors.huaweicloud.com/docker-ce/linux/ubuntu` | `https://<id>.mirror.swr.myhuaweicloud.com` |
>
> `DOCKER_INSTALL_MIRROR` 目前仅支持 apt 系(Ubuntu / Debian / TencentOS-apt)。
> CentOS / Rocky 等 yum 系发行版 `get.docker.com` 一般能直连,没碰到再说。
`prepare.sh` 会:
1. 安装 Docker / Docker Compose plugin(已装则跳过)
2. 用 `curl + tar` 下载 4 个运行时文件到 `/opt/WeKnora`
3. 拉取并启动默认 5 个容器 + 预拉 sandbox 镜像
4. 安装 `weknora.service`(开机自启)+ `weknora-firstboot.service`(首启 init)
完成后访问 `http://<公网IP>`。
### 步骤二:验证
至少验证:
- 能注册管理员、能登录
- 能创建一个知识库
- 能上传一个文档并完成解析
- 能进行一次问答
```bash
sudo docker compose -f /opt/WeKnora/docker-compose.yml ps
curl -f http://localhost:8080/health
```
### 步骤三:清理并制作镜像
> **重要**:`cleanup.sh` 会删除所有 SSH 公钥、清空日志、清空数据库与 docker volume。执行后**不要再 SSH 进来**,直接去云控制台关机制作镜像。
```bash
sudo bash /opt/weknora-tools/scripts/cloud-image/cleanup.sh
```
执行完会自动 `poweroff`。然后到对应云控制台按其文档制作镜像。
### 新实例首次开机行为
用户用你的镜像创建实例后,第一次开机时 `weknora-firstboot.service` 会:
1. 生成随机的 `DB_PASSWORD` / `REDIS_PASSWORD` / `JWT_SECRET` / `SYSTEM_AES_KEY`
2. 写回 `/opt/WeKnora/.env`
3. `docker compose up -d` 启动全部服务
4. 把生成的凭证写到 `/root/weknora-credentials.txt`(仅 root 可读)
5. 把自己 disable + 删除自己(确保只跑一次)
之后每次开机都由 `weknora.service` 接管。
> **注意**:`firstboot.sh` 默认**不**禁用注册(`DISABLE_REGISTRATION=false`),第一个注册的人会成为管理员。
> 凭证文件里有「请尽快注册以防被抢注」的醒目提示。需要更严格控制可在 `firstboot.sh` 的 `replace` 调用列表中追加一行 `replace DISABLE_REGISTRATION true` 后再重制镜像。
---
## 升级镜像版本
镜像里没有 git 仓库,升级直接重跑 `prepare.sh`(会覆盖 4 个运行时文件,**不动 `.env` 和 docker volume 数据**):
```bash
sudo WEKNORA_REF=v0.6.0 bash /opt/weknora-tools/scripts/cloud-image/prepare.sh
sudo bash /opt/weknora-tools/scripts/cloud-image/cleanup.sh # 制作新镜像前
```
> **打新镜像时想顺便清掉旧版本镜像层**(每个旧 weknora-* tag 几百 MB,4 个镜像 ~2-4GB):
> ```bash
> sudo PRUNE_OLD_IMAGES=true WEKNORA_REF=v0.6.0 \
> bash /opt/weknora-tools/scripts/cloud-image/prepare.sh
> ```
> 默认 `false` 是为了保留回滚路径。打镜像前确认新版本稳定后再开。
## 安全注意事项
- 镜像里**不要**预置任何 LLM API Key、Langfuse Key、个人 SSH key
- 数据库 / Redis / MinIO 端口默认仅对 docker 网络可见,不要在云防火墙里对外开放
- `/root/weknora-credentials.txt` 用 `umask 077` 创建,仅 root 可读
- 每次重制镜像前必须执行 `cleanup.sh`,避免泄漏上一份测试数据 / SSH key / machine-id
## 各云平台具体操作
| 平台 | 文档 |
|---|---|
| 腾讯云轻量应用服务器 / CVM | [`docs/cloud-image/tencent-lighthouse.md`](../../docs/cloud-image/tencent-lighthouse.md) |
| AWS EC2 AMI | (欢迎贡献) |
| 阿里云 ECS | (欢迎贡献) |
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env bash
# cleanup.sh - 在制作云镜像前清理私密数据。
# 警告: 本脚本会删除 SSH 公钥、清空数据库与日志,最后会自动关机。
# 执行后请直接在云控制台「制作镜像 / 创建快照 / 创建 AMI」,不要再 SSH 进来。
set -euo pipefail
WEKNORA_DIR="${WEKNORA_DIR:-/opt/WeKnora}"
if [[ "${EUID}" -ne 0 ]]; then
echo "[cleanup] 请使用 sudo 或 root 运行" >&2
exit 1
fi
read -r -p "[cleanup] 该操作不可逆,确认继续? 输入 YES 继续: " ans
if [[ "${ans}" != "YES" ]]; then
echo "[cleanup] 已取消"
exit 0
fi
echo "[cleanup] 1/8 停止 WeKnora 容器"
COMPOSE_PROJECT=""
if [[ -d "${WEKNORA_DIR}" ]]; then
cd "${WEKNORA_DIR}"
# 优先用 compose ls 拿到真实 project 名 (默认是目录名小写, 如 weknora)
COMPOSE_PROJECT="$(docker compose ls --format json 2>/dev/null \
| grep -oE '"Name":"[^"]+"' | head -1 | cut -d'"' -f4 || true)"
docker compose down -v --remove-orphans || true
fi
echo "[cleanup] 2/8 清空 WeKnora 业务数据 + 首启 marker / 日志"
if [[ -d "${WEKNORA_DIR}" ]]; then
rm -rf "${WEKNORA_DIR}/data"/* "${WEKNORA_DIR}/logs"/* 2>/dev/null || true
# 故意不在这里重建 .env: 让镜像里 .env 缺失, 任何在 firstboot 之前
# 起来的 docker compose 都会因找不到 .env 而失败, 避免明文默认密码
# (postgres123!@# 之类) 把 postgres 数据卷初始化坏。
# firstboot.sh 自己会从 .env.example 拷贝并替换密钥。
rm -f "${WEKNORA_DIR}/.env" "${WEKNORA_DIR}/.firstboot.done"
fi
rm -f /root/weknora-credentials.txt /var/log/weknora-firstboot.log
echo "[cleanup] 3/8 清理残留 docker 卷与构建缓存"
# 严格按 compose project 名前缀匹配, 避免误伤同宿主上其它 postgres/redis 卷。
if [[ -n "${COMPOSE_PROJECT}" ]]; then
docker volume ls -q --filter "label=com.docker.compose.project=${COMPOSE_PROJECT}" \
| xargs -r docker volume rm -f || true
fi
# 注意: 这里只清"卷 / 已停容器 / 构建缓存", 绝对不能清镜像。
# 此前用过 `docker system prune -af --volumes`, 会把 prepare.sh 预拉的
# wechatopenai/weknora-* 等镜像一并清掉, 导致基于镜像建出来的新实例
# 在 firstboot 时还要重新从 Docker Hub 拉数 GB 镜像, 完全违背预装初衷。
docker container prune -f || true
docker builder prune -af || true
# 只清未挂载的 dangling 卷 (此时 compose down -v 已清掉业务卷)
docker volume prune -f || true
echo "[cleanup] 4/8 清空系统日志"
journalctl --rotate || true
journalctl --vacuum-time=1s || true
find /var/log -type f \( -name '*.log' -o -name '*.gz' -o -name '*.[0-9]' \) -print0 \
| xargs -0 -r truncate -s 0 || true
find /var/log -type f \( -name '*.gz' -o -name '*.[0-9]' \) -print0 \
| xargs -0 -r rm -f || true
echo "[cleanup] 5/8 清理 SSH 历史与授权 key(执行后将无法 SSH 进来)"
rm -f /root/.ssh/authorized_keys /root/.ssh/known_hosts /root/.bash_history
for d in /home/*; do
[[ -d "$d" ]] || continue
rm -f "$d/.ssh/authorized_keys" "$d/.ssh/known_hosts" "$d/.bash_history"
done
find / -xdev -type f \( -name 'id_rsa*' -o -name '*.pem' -o -name '*.key' \) \
-not -path '/etc/ssl/*' -not -path '/usr/*' -not -path '/var/lib/docker/*' 2>/dev/null \
| tee /tmp/cleanup-secrets-found.txt || true
echo "[cleanup] ↑ 上面是疑似遗留的密钥文件,必要时人工再核对"
echo "[cleanup] 6/8 重置 cloud-init / machine-id(让新实例拿到新 ID)"
cloud-init clean --logs --seed 2>/dev/null || true
truncate -s 0 /etc/machine-id || true
rm -f /var/lib/dbus/machine-id || true
echo "[cleanup] 7/8 清理 apt / tmp"
if command -v apt-get >/dev/null 2>&1; then
apt-get clean
rm -rf /var/lib/apt/lists/*
fi
rm -rf /tmp/* /var/tmp/* /root/.cache /home/*/.cache 2>/dev/null || true
echo "[cleanup] 8/8 同步磁盘并关机"
history -c || true
sync
echo
echo " 即将关机。关机完成后请到云控制台执行「制作镜像 / 创建快照 / 创建 AMI」。"
echo
sleep 3
poweroff
+139
View File
@@ -0,0 +1,139 @@
#!/usr/bin/env bash
# firstboot.sh - 由 weknora-firstboot.service 在新实例首次开机时自动执行。
# 任务: 生成随机密钥写入 .env -> 启动容器 -> 输出凭证 -> 标记完成 + 自禁用。
#
# 幂等性策略:
# 1) 一上来先写 ${MARKER} 标记 "已生成过密钥",
# 之后即便 docker compose 失败导致脚本中断, 重启后由 unit 的
# ConditionPathExists=!${MARKER} 拦住, 不会再生成新密钥覆盖 .env。
# (旧密钥已经写进 postgres 数据卷, 二次生成会导致数据库永久无法登录)
# 2) 失败时 unit 标记 failed, 用户可手动 docker compose up -d 恢复;
# 凭证仍可在 ${ENV_FILE} 中查到。
set -euo pipefail
WEKNORA_DIR="${WEKNORA_DIR:-/opt/WeKnora}"
ENV_FILE="${WEKNORA_DIR}/.env"
ENV_TEMPLATE="${WEKNORA_DIR}/.env.example"
CRED_FILE="/root/weknora-credentials.txt"
LOG_FILE="/var/log/weknora-firstboot.log"
MARKER="${WEKNORA_DIR}/.firstboot.done"
# 提前打开 LOG_FILE, 同时把 stderr 也复制一份到 systemd journal 方便调试
# (单纯 exec >> LOG_FILE 时, 脚本一开始的失败会因为 stderr 已被吞掉而看不到)
mkdir -p "$(dirname "${LOG_FILE}")"
exec > >(tee -a "${LOG_FILE}") 2>&1
echo "==== firstboot started at $(date -Iseconds) ===="
if [[ -f "${MARKER}" ]]; then
echo "marker ${MARKER} exists, skip (already initialized)"
exit 0
fi
# cleanup.sh 不再保留 .env, 这里从 .env.example 拷贝模板再做替换。
# 这样保证 firstboot 之前不会有任何含明文默认密码的 .env 让 weknora.service
# 抢先把 postgres 数据卷用错的密码初始化掉。
if [[ ! -f "${ENV_FILE}" ]]; then
if [[ -f "${ENV_TEMPLATE}" ]]; then
echo "creating ${ENV_FILE} from ${ENV_TEMPLATE}"
cp "${ENV_TEMPLATE}" "${ENV_FILE}"
else
echo "ERROR: neither ${ENV_FILE} nor ${ENV_TEMPLATE} found"
exit 1
fi
fi
DOCKER_BIN="$(command -v docker || true)"
if [[ -z "${DOCKER_BIN}" ]]; then
echo "ERROR: docker binary not found in PATH"
exit 1
fi
# 生成 32 字节强随机字符串(用于 AES-256 key, 必须刚好 32 字节)
# 用 `() ... ()` 启子 shell 并关闭 pipefail: head 只读 N 字节后关闭 stdin,
# tr 会收到 SIGPIPE (退出码 141), 在 `set -o pipefail` 下整条管道被判失败,
# 触发顶层 `set -e` 把 firstboot.sh 8ms exit 1 干掉。
gen32() ( set +o pipefail; LC_ALL=C tr -dc 'A-Za-z0-9' </dev/urandom | head -c 32 )
# 通用密码: 24 字符, 不含 / + = (避免出现在 URL / sed 替换时出问题)
genpw() ( set +o pipefail; LC_ALL=C tr -dc 'A-Za-z0-9' </dev/urandom | head -c 24 )
DB_PWD=$(genpw)
REDIS_PWD=$(genpw)
JWT=$(genpw)$(genpw)
SYS_AES=$(gen32)
# 用 | 作 sed 分隔符避免冲突; 仅替换以 KEY= 开头的行
replace() {
local key="$1" val="$2"
if grep -qE "^${key}=" "${ENV_FILE}"; then
sed -i "s|^${key}=.*|${key}=${val}|" "${ENV_FILE}"
else
echo "${key}=${val}" >>"${ENV_FILE}"
fi
}
replace DB_PASSWORD "${DB_PWD}"
replace REDIS_PASSWORD "${REDIS_PWD}"
replace JWT_SECRET "${JWT}"
replace SYSTEM_AES_KEY "${SYS_AES}"
replace GIN_MODE "release"
# 把 prepare.sh 阶段记录在 .cloud-image-meta 里的 WEKNORA_REF 还原为 .env 的
# WEKNORA_VERSION, 否则 docker compose 会落回 :latest 默认值, 导致镜像版本
# 与 prepare 时拉取的版本不一致。
META_FILE="${WEKNORA_DIR}/.cloud-image-meta"
if [[ -f "${META_FILE}" ]]; then
META_REF=$(grep -E '^WEKNORA_REF=' "${META_FILE}" | tail -1 | cut -d= -f2- || true)
if [[ -n "${META_REF}" ]]; then
replace WEKNORA_VERSION "${META_REF}"
echo "restored WEKNORA_VERSION=${META_REF} from ${META_FILE}"
fi
fi
# 关键: .env 改完立刻写 marker。
# 这之后即便 docker compose up 失败, 重启也不会再次重写 .env,
# 防止与 postgres 已经持久化的密码不一致。
umask 077
touch "${MARKER}"
chmod 0600 "${MARKER}"
echo "env updated, marker written, starting docker compose..."
cd "${WEKNORA_DIR}"
"${DOCKER_BIN}" compose up -d
# 尝试拿公网 IP, 失败就用内网
PUB_IP=$(curl -fsS --max-time 5 https://ifconfig.me 2>/dev/null \
|| curl -fsS --max-time 5 https://api.ipify.org 2>/dev/null \
|| hostname -I | awk '{print $1}')
cat >"${CRED_FILE}" <<INFO
========================================
WeKnora 实例初始化完成
生成时间: $(date -Iseconds)
========================================
访问地址 : http://${PUB_IP}
注册后如需关闭后续注册, 编辑 ${ENV_FILE}:
DISABLE_REGISTRATION=true
然后执行: cd ${WEKNORA_DIR} && docker compose up -d
下列随机凭证已写入 ${ENV_FILE}, 请妥善保存(仅 root 可读):
DB_PASSWORD = ${DB_PWD}
REDIS_PASSWORD = ${REDIS_PWD}
JWT_SECRET = ${JWT}
SYSTEM_AES_KEY = ${SYS_AES}
注意:
- 本文件只反映首启时刻的密钥, 后续以 ${ENV_FILE} 为准。
- 切勿直接对外暴露 5432 / 6379 / 9000 等基础设施端口。
- 仅用 80 / 443 对外服务, 必要时配置反向代理 + HTTPS。
INFO
echo "credentials written to ${CRED_FILE}"
# 仅停掉 unit, 不删 unit 文件 (否则当前正在运行的 oneshot 可能被 systemd 标记 failed)。
# 下次重启时, weknora-firstboot.service 通过 ConditionPathExists=!${MARKER} 自动跳过。
systemctl disable weknora-firstboot.service || true
echo "==== firstboot finished at $(date -Iseconds) ===="
+237
View File
@@ -0,0 +1,237 @@
#!/usr/bin/env bash
# prepare.sh - 在干净的 Linux 实例上部署 WeKnora 运行时, 用于制作云镜像模板。
# 不需要 clone 整个 WeKnora 仓库, 只下载 4 个运行时文件 (~100KB)。
# 兼容: Ubuntu / Debian / CentOS / Rocky / TencentOS 等带 systemd + Docker 的发行版。
# 使用方式: sudo bash prepare.sh
# 可调环境变量:
# WEKNORA_REF 要拉取的 git ref (tag / branch / commit), 默认 main
# WEKNORA_DIR 部署目录, 默认 /opt/WeKnora
# WEKNORA_REPO 仓库地址, 默认 https://github.com/Tencent/WeKnora
# WEKNORA_GH_PROXY GitHub 加速前缀, 默认空。中国大陆机器可设
# https://gh-proxy.com/ 或 https://ghfast.top/
# (实际下载地址变成 ${WEKNORA_GH_PROXY}${WEKNORA_REPO}/archive/...)
# DOCKER_INSTALL_MIRROR Docker 安装包镜像源, 默认空 (走 get.docker.com)。
# 中国大陆机器境外 CDN 不通时设为, 例如:
# https://mirrors.tencent.com/docker-ce/linux/ubuntu
# https://mirrors.aliyun.com/docker-ce/linux/ubuntu
# 会改用 apt + docker-ce 官方仓库镜像安装,
# 含 docker-ce / containerd.io / docker-compose-plugin,
# 完全不访问 get.docker.com。仅支持 apt 系发行版。
# DOCKER_REGISTRY_MIRROR Docker Hub 加速器, 默认空。腾讯云内网可设
# https://mirror.ccs.tencentyun.com
# (会写入 /etc/docker/daemon.json 并重启 docker)
# PRUNE_OLD_IMAGES 升级场景下是否清理 dangling / 旧版本 tag 镜像,
# 默认 false。设为 true 时在拉新镜像之后执行
# `docker image prune -af`, 把没有容器引用的镜像
# (含旧 WEKNORA_VERSION 的 wechatopenai/weknora-*)
# 一次性删掉, 减少要打进云镜像的体积。
set -euo pipefail
WEKNORA_REF="${WEKNORA_REF:-main}"
WEKNORA_DIR="${WEKNORA_DIR:-/opt/WeKnora}"
WEKNORA_REPO="${WEKNORA_REPO:-https://github.com/Tencent/WeKnora}"
WEKNORA_GH_PROXY="${WEKNORA_GH_PROXY:-}"
DOCKER_INSTALL_MIRROR="${DOCKER_INSTALL_MIRROR:-}"
DOCKER_REGISTRY_MIRROR="${DOCKER_REGISTRY_MIRROR:-}"
PRUNE_OLD_IMAGES="${PRUNE_OLD_IMAGES:-false}"
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
if [[ "${EUID}" -ne 0 ]]; then
echo "[prepare] 请使用 sudo 或 root 运行" >&2
exit 1
fi
# 通过镜像源 apt 安装 docker-ce 全家桶 (含 compose-plugin)。
# 用于中国大陆云主机直连 get.docker.com 被 RST 的场景。
install_docker_via_apt_mirror() {
local mirror="$1"
if ! command -v apt-get >/dev/null 2>&1; then
echo "[prepare] DOCKER_INSTALL_MIRROR 目前仅支持 apt 系发行版 (Ubuntu/Debian)" >&2
return 1
fi
apt-get update -y
apt-get install -y ca-certificates curl gnupg lsb-release
install -m 0755 -d /etc/apt/keyrings
curl -fsSL "${mirror%/}/gpg" | gpg --dearmor --yes -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
local arch codename
arch="$(dpkg --print-architecture)"
codename="$(lsb_release -cs)"
echo "deb [arch=${arch} signed-by=/etc/apt/keyrings/docker.gpg] ${mirror%/} ${codename} stable" \
> /etc/apt/sources.list.d/docker.list
apt-get update -y
apt-get install -y docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin curl tar
}
echo "[prepare] 1/6 安装 Docker 与依赖"
if ! command -v docker >/dev/null 2>&1; then
if [[ -n "${DOCKER_INSTALL_MIRROR}" ]]; then
echo "[prepare] 通过 ${DOCKER_INSTALL_MIRROR} 走 apt 安装 docker-ce (跳过 get.docker.com)"
install_docker_via_apt_mirror "${DOCKER_INSTALL_MIRROR}"
else
curl -fsSL https://get.docker.com | bash
fi
fi
systemctl enable --now docker
if ! docker compose version >/dev/null 2>&1; then
if command -v apt-get >/dev/null 2>&1; then
apt-get update -y
apt-get install -y docker-compose-plugin curl tar
elif command -v yum >/dev/null 2>&1; then
yum install -y docker-compose-plugin curl tar
fi
fi
# 可选: 配置 Docker Hub 加速器, 解决直连 registry-1.docker.io 超时的场景
# (典型: 中国大陆云主机 / 内网受限环境)。仅在用户显式传入时才动 daemon.json。
if [[ -n "${DOCKER_REGISTRY_MIRROR}" ]]; then
echo "[prepare] 1.5/6 配置 Docker Hub 加速器: ${DOCKER_REGISTRY_MIRROR}"
mkdir -p /etc/docker
# 已存在的 daemon.json 走 python 合并, 避免覆盖用户其它配置
if [[ -s /etc/docker/daemon.json ]] && command -v python3 >/dev/null 2>&1; then
python3 - "$DOCKER_REGISTRY_MIRROR" <<'PY'
import json, sys, pathlib
p = pathlib.Path("/etc/docker/daemon.json")
mirror = sys.argv[1]
try:
cfg = json.loads(p.read_text())
except Exception:
cfg = {}
mirrors = cfg.get("registry-mirrors") or []
if mirror not in mirrors:
mirrors.insert(0, mirror)
cfg["registry-mirrors"] = mirrors
p.write_text(json.dumps(cfg, indent=2) + "\n")
PY
else
cat >/etc/docker/daemon.json <<EOF
{
"registry-mirrors": ["${DOCKER_REGISTRY_MIRROR}"]
}
EOF
fi
systemctl restart docker
# 给 docker daemon 一点重启时间, 避免下面 docker compose pull 立刻拿到 EOF
for _ in 1 2 3 4 5; do
docker info >/dev/null 2>&1 && break
sleep 1
done
fi
echo "[prepare] 2/6 拉取 WeKnora 运行时文件 (ref=${WEKNORA_REF})"
# 只下载实际需要的 4 个文件, 不 clone 整个仓库 (~MB 级 -> ~KB 级)
mkdir -p "${WEKNORA_DIR}/config" "${WEKNORA_DIR}/skills"
tmp=$(mktemp -d)
trap 'rm -rf "${tmp}"' EXIT
tarball_url="${WEKNORA_GH_PROXY}${WEKNORA_REPO}/archive/${WEKNORA_REF}.tar.gz"
echo "[prepare] tarball: ${tarball_url}"
curl -fsSL "${tarball_url}" -o "${tmp}/repo.tar.gz"
# 仅解压需要的路径, 显著加速且省空间
tar -xzf "${tmp}/repo.tar.gz" -C "${tmp}" \
--wildcards \
'*/docker-compose.yml' \
'*/.env.example' \
'*/config/config.yaml' \
'*/skills/preloaded'
src=$(find "${tmp}" -maxdepth 1 -mindepth 1 -type d -name 'WeKnora-*' | head -1)
if [[ -z "${src}" ]]; then
echo "[prepare] 解压失败, 未找到 WeKnora-* 目录" >&2
exit 1
fi
cp "${src}/docker-compose.yml" "${WEKNORA_DIR}/"
cp "${src}/.env.example" "${WEKNORA_DIR}/"
cp "${src}/config/config.yaml" "${WEKNORA_DIR}/config/"
rm -rf "${WEKNORA_DIR}/skills/preloaded"
cp -r "${src}/skills/preloaded" "${WEKNORA_DIR}/skills/"
# 记录元信息, 供 firstboot / 升级时参考
cat >"${WEKNORA_DIR}/.cloud-image-meta" <<EOF
WEKNORA_REF=${WEKNORA_REF}
WEKNORA_REPO=${WEKNORA_REPO}
PREPARED_AT=$(date -Iseconds)
EOF
echo "[prepare] 3/6 准备 .env (默认值, firstboot 会替换为随机密钥)"
cd "${WEKNORA_DIR}"
[[ -f .env ]] || cp .env.example .env
sed -i 's/^GIN_MODE=.*/GIN_MODE=release/' .env || true
# 把 WEKNORA_VERSION 与 WEKNORA_REF 对齐, 让 docker compose 拉取与 ref 一致的
# 镜像 tag。无条件覆盖, 避免 .env 残留上一次 prepare 留下的旧版本号。
# Docker Hub 上 wechatopenai/weknora-* 的 tag 命名约定:
# - 浮动 tag:main(持续指向最新构建)
# - 固定 release tag:v 前缀 + semver(如 v0.7.2、v0.5.2)
# 因此这里不剥 v、也不映射到 latest。
WEKNORA_VERSION_VAL="${WEKNORA_REF}"
if grep -qE '^WEKNORA_VERSION=' .env; then
sed -i "s|^WEKNORA_VERSION=.*|WEKNORA_VERSION=${WEKNORA_VERSION_VAL}|" .env
else
echo "WEKNORA_VERSION=${WEKNORA_VERSION_VAL}" >>.env
fi
echo "[prepare] -> WEKNORA_VERSION=${WEKNORA_VERSION_VAL}"
echo "[prepare] 4/6 拉取并启动默认 5 个常驻容器 (frontend/app/docreader/postgres/redis)"
docker compose pull
docker compose up -d
# 提前 pull sandbox 镜像 (Agent Skills 运行时由 app 按需 docker run, 非常驻)
# 不预拉的话, 用户首次跑 Skill 会卡在下载
echo "[prepare] 4.5/6 预拉 sandbox 镜像 (Agent Skills 用, 非常驻)"
docker compose --profile full pull sandbox || true
# 其他向量库 / 可观测组件 (qdrant, milvus, weaviate, doris, neo4j, langfuse-*, minio, dex)
# 不预拉, 体积可省 5-15GB. 用户如需启用:
# cd /opt/WeKnora && docker compose --profile <name> up -d
# 升级场景: 清理旧版本 tag 的 wechatopenai/weknora-* 镜像。
# 默认关闭, 保留回滚路径; 制作镜像前显式打开以减小体积。
#
# 注意: 不用 `docker image prune -af`!
# sandbox 镜像在 compose 里只 pull 不 up (Agent Skills 由 app 按需 docker run),
# 没有任何容器引用它, 一旦 `prune -a` 会把当前版本的 sandbox 一起删掉,
# 反而违背 prepare.sh 4.5 步预拉 sandbox 的目的。
# 这里精确按 tag 比对, 只删 wechatopenai/weknora-* 仓库下、tag 不等于当前
# WEKNORA_VERSION 的镜像, 基础设施镜像 (paradedb / redis) 不动。
if [[ "${PRUNE_OLD_IMAGES,,}" == "true" || "${PRUNE_OLD_IMAGES}" == "1" ]]; then
echo "[prepare] 4.6/6 清理 wechatopenai/weknora-* 仓库下旧版本镜像 (PRUNE_OLD_IMAGES=true, keep=${WEKNORA_VERSION_VAL})"
docker image ls --format '{{.Repository}}:{{.Tag}}' \
| grep -E '^wechatopenai/weknora-' \
| grep -vE ":${WEKNORA_VERSION_VAL}\$" \
| xargs -r docker rmi -f 2>/dev/null || true
fi
echo "[prepare] 5/6 安装 systemd 单元"
# 探测 docker 二进制路径, 不同发行版可能在 /usr/bin 或 /usr/local/bin
DOCKER_BIN="$(command -v docker)"
if [[ -z "${DOCKER_BIN}" ]]; then
echo "[prepare] 未找到 docker 二进制" >&2
exit 1
fi
echo "[prepare] docker binary: ${DOCKER_BIN}"
install -m 0644 "${SCRIPT_DIR}/systemd/weknora.service" /etc/systemd/system/weknora.service
install -m 0644 "${SCRIPT_DIR}/systemd/weknora-firstboot.service" /etc/systemd/system/weknora-firstboot.service
install -m 0755 "${SCRIPT_DIR}/firstboot.sh" /usr/local/sbin/weknora-firstboot.sh
# 把 systemd 单元里的 docker 路径模板替换为实际路径
sed -i "s|@DOCKER_BIN@|${DOCKER_BIN}|g" /etc/systemd/system/weknora.service
systemctl daemon-reload
systemctl enable weknora.service
systemctl enable weknora-firstboot.service
echo "[prepare] 6/6 完成"
echo
echo " WeKnora 运行时已部署到 ${WEKNORA_DIR}"
echo " docker-compose.yml / config/config.yaml / skills/preloaded / .env"
echo " 版本: ${WEKNORA_REF} (见 ${WEKNORA_DIR}/.cloud-image-meta)"
echo
echo " 打开浏览器访问 http://<本机公网IP> 验证功能"
echo
echo " 验证通过后执行清理并制作镜像:"
echo " sudo bash ${SCRIPT_DIR}/cleanup.sh"
@@ -0,0 +1,21 @@
[Unit]
Description=WeKnora (docker compose)
Requires=docker.service weknora-firstboot.service
After=docker.service network-online.target weknora-firstboot.service
Wants=network-online.target
# 关键: Requires=weknora-firstboot.service 让首启 init 失败时本服务也不启动,
# 避免 firstboot 没成功生成随机密钥就用 .env.example 默认明文密码把 postgres
# 数据卷初始化掉。weknora-firstboot.service 在已初始化的实例上由
# ConditionPathExists=!/opt/WeKnora/.firstboot.done 跳过, 跳过 = 成功,
# 不会阻塞本服务后续重启。
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/WeKnora
ExecStart=@DOCKER_BIN@ compose up -d
ExecStop=@DOCKER_BIN@ compose down
TimeoutStartSec=600
[Install]
WantedBy=multi-user.target