feat: rebuild as multi-user web agent
This commit is contained in:
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
project_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "${project_dir}"
|
||||
|
||||
./scripts/audit-web-image.sh
|
||||
|
||||
runtime_image="${RUNTIME_AUDIT_IMAGE:-k1412-agent-runtime:audit}"
|
||||
gateway_image="${GATEWAY_AUDIT_IMAGE:-k1412-agent-gateway:audit}"
|
||||
workspace_image="${WORKSPACE_AUDIT_IMAGE:-k1412-agent-workspace:audit}"
|
||||
trivy_image="aquasec/trivy@sha256:cffe3f5161a47a6823fbd23d985795b3ed72a4c806da4c4df16266c02accdd6f"
|
||||
trivy_cache="k1412-trivy-cache"
|
||||
|
||||
docker build --platform linux/amd64 -f docker/runtime.Dockerfile -t "${runtime_image}" .
|
||||
docker build --platform linux/amd64 -f docker/gateway.Dockerfile -t "${gateway_image}" .
|
||||
docker build --platform linux/amd64 -f docker/workspace.Dockerfile -t "${workspace_image}" .
|
||||
|
||||
for image in "${runtime_image}" "${gateway_image}" "${workspace_image}"; do
|
||||
docker run --rm "${image}" python -m pip check
|
||||
docker run --rm \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v "${trivy_cache}:/root/.cache" \
|
||||
"${trivy_image}" \
|
||||
image \
|
||||
--quiet \
|
||||
--scanners vuln \
|
||||
--severity HIGH,CRITICAL \
|
||||
--ignore-unfixed \
|
||||
--exit-code 1 \
|
||||
--format json \
|
||||
"${image}" >/dev/null
|
||||
done
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
project_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "${project_dir}"
|
||||
|
||||
image="${WEB_AUDIT_IMAGE:-k1412-agent-web:audit}"
|
||||
trivy_image="aquasec/trivy@sha256:cffe3f5161a47a6823fbd23d985795b3ed72a4c806da4c4df16266c02accdd6f"
|
||||
trivy_cache="k1412-trivy-cache"
|
||||
docker build -f docker/web.Dockerfile -t "${image}" .
|
||||
|
||||
docker run --rm --entrypoint sh "${image}" -lc '
|
||||
python -m pip check &&
|
||||
python -m pip install -q pip-audit &&
|
||||
python -m pip_audit
|
||||
'
|
||||
|
||||
docker volume inspect "${trivy_cache}" >/dev/null 2>&1 \
|
||||
|| docker volume create "${trivy_cache}" >/dev/null
|
||||
trivy=(docker run --rm \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v "${trivy_cache}:/root/.cache" \
|
||||
"${trivy_image}")
|
||||
scan_args=(image --quiet --scanners vuln --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1)
|
||||
if ! "${trivy[@]}" "${scan_args[@]}" --format json "${image}" >/dev/null; then
|
||||
"${trivy[@]}" "${scan_args[@]}" "${image}" || true
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,28 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# 配置线上 nginx 上传体积限制。
|
||||
# 需要 sudo 权限;应用服务本身仍然用普通用户运行。
|
||||
|
||||
LIMIT="${CLAW_NGINX_CLIENT_MAX_BODY_SIZE:-300m}"
|
||||
CONF_PATH="${CLAW_NGINX_UPLOAD_CONF:-/etc/nginx/conf.d/zk-data-agent-upload.conf}"
|
||||
|
||||
if ! command -v nginx >/dev/null 2>&1; then
|
||||
echo "WARN 未发现 nginx,跳过上传限制配置。"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! command -v sudo >/dev/null 2>&1; then
|
||||
echo "ERROR 配置 nginx 需要 sudo。" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sudo tee "${CONF_PATH}" >/dev/null <<EOF
|
||||
# Managed by zk-data-agent.
|
||||
# 放开 Web 文件上传体积,避免大文件上传被 nginx 直接 413。
|
||||
client_max_body_size ${LIMIT};
|
||||
EOF
|
||||
|
||||
sudo nginx -t
|
||||
sudo systemctl reload nginx
|
||||
echo "已配置 nginx client_max_body_size=${LIMIT} (${CONF_PATH})"
|
||||
@@ -1,468 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Ubuntu 一键部署/更新脚本。
|
||||
# - 首次执行会交互式生成 .env.deploy,本机保存,不提交 git。
|
||||
# - 后续执行会拉取代码、更新依赖、构建前端、安装/重启 systemd 服务。
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
ENV_FILE="${ROOT_DIR}/.env.deploy"
|
||||
PYTHON_VERSION="${PYTHON_VERSION:-3.10.14}"
|
||||
DEPLOY_INSTANCE="${CLAW_DEPLOY_INSTANCE:-zk-data-agent}"
|
||||
BACKEND_SERVICE="${CLAW_BACKEND_SERVICE:-${DEPLOY_INSTANCE}-backend}"
|
||||
FRONTEND_SERVICE="${CLAW_FRONTEND_SERVICE:-${DEPLOY_INSTANCE}-frontend}"
|
||||
USER_SYSTEMD_DIR="${HOME}/.config/systemd/user"
|
||||
SYSTEM_SYSTEMD_DIR="/etc/systemd/system"
|
||||
BRANCH=""
|
||||
FORCE=0
|
||||
SKIP_GIT=0
|
||||
BOOTSTRAP_SYSTEM=0
|
||||
SERVICE_SCOPE="${CLAW_SERVICE_SCOPE:-}"
|
||||
ENABLE_LINUX_ACCOUNTS="${CLAW_ENABLE_LINUX_ACCOUNTS:-}"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: bash scripts/deploy-ubuntu.sh [branch] [--force] [--skip-git] [--bootstrap-system] [--system-service] [--user-service] [--enable-linux-accounts]
|
||||
|
||||
Options:
|
||||
branch 要部署的 git 分支;不传则使用当前分支
|
||||
--force 使用 git reset --hard origin/<branch> 覆盖本地改动
|
||||
--skip-git 跳过 git 拉取,只部署当前工作区
|
||||
--bootstrap-system
|
||||
强制使用 sudo 安装 Ubuntu 系统依赖;首次安装会默认执行
|
||||
--system-service
|
||||
安装 root/system 级 systemd 服务;以 root 执行时默认启用
|
||||
--user-service
|
||||
安装当前用户级 systemd 服务
|
||||
--enable-linux-accounts
|
||||
启用平台账号同名 Linux 用户工作区;要求 system 服务和 root 权限
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--force)
|
||||
FORCE=1
|
||||
shift
|
||||
;;
|
||||
--skip-git)
|
||||
SKIP_GIT=1
|
||||
shift
|
||||
;;
|
||||
--bootstrap-system)
|
||||
BOOTSTRAP_SYSTEM=1
|
||||
shift
|
||||
;;
|
||||
--system-service)
|
||||
SERVICE_SCOPE="system"
|
||||
shift
|
||||
;;
|
||||
--user-service)
|
||||
SERVICE_SCOPE="user"
|
||||
shift
|
||||
;;
|
||||
--enable-linux-accounts)
|
||||
ENABLE_LINUX_ACCOUNTS="1"
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
if [[ -n "${BRANCH}" ]]; then
|
||||
echo "Unexpected argument: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
BRANCH="$1"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
REQUESTED_SERVICE_SCOPE="${SERVICE_SCOPE}"
|
||||
REQUESTED_ENABLE_LINUX_ACCOUNTS="${ENABLE_LINUX_ACCOUNTS}"
|
||||
|
||||
log() {
|
||||
printf "\n\033[1;34m==>\033[0m %s\n" "$1"
|
||||
}
|
||||
|
||||
warn() {
|
||||
printf "\033[1;33mWARN\033[0m %s\n" "$1" >&2
|
||||
}
|
||||
|
||||
fail() {
|
||||
printf "\033[1;31mERROR\033[0m %s\n" "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
configure_instance_names() {
|
||||
if [[ -z "${SERVICE_SCOPE}" && -n "${CLAW_SERVICE_SCOPE:-}" ]]; then
|
||||
SERVICE_SCOPE="${CLAW_SERVICE_SCOPE}"
|
||||
fi
|
||||
DEPLOY_INSTANCE="${CLAW_DEPLOY_INSTANCE:-${DEPLOY_INSTANCE:-zk-data-agent}}"
|
||||
BACKEND_SERVICE="${CLAW_BACKEND_SERVICE:-${DEPLOY_INSTANCE}-backend}"
|
||||
FRONTEND_SERVICE="${CLAW_FRONTEND_SERVICE:-${DEPLOY_INSTANCE}-frontend}"
|
||||
if [[ -z "${SERVICE_SCOPE}" ]]; then
|
||||
if [[ "${EUID}" == "0" ]]; then
|
||||
SERVICE_SCOPE="system"
|
||||
else
|
||||
SERVICE_SCOPE="user"
|
||||
fi
|
||||
fi
|
||||
if [[ -z "${ENABLE_LINUX_ACCOUNTS}" ]]; then
|
||||
ENABLE_LINUX_ACCOUNTS="${CLAW_ENABLE_LINUX_ACCOUNTS:-0}"
|
||||
fi
|
||||
}
|
||||
|
||||
require_command() {
|
||||
local name="$1"
|
||||
local hint="$2"
|
||||
if ! command -v "${name}" >/dev/null 2>&1; then
|
||||
fail "缺少命令:${name}。${hint}"
|
||||
fi
|
||||
}
|
||||
|
||||
resolve_npm_bin() {
|
||||
if [[ -n "${CLAW_NODE_BIN_DIR:-}" ]]; then
|
||||
export PATH="${CLAW_NODE_BIN_DIR}:${PATH}"
|
||||
fi
|
||||
local npm_bin="${CLAW_NPM_BIN:-$(command -v npm || true)}"
|
||||
if [[ -z "${npm_bin}" || ! -x "${npm_bin}" ]]; then
|
||||
fail "缺少命令:npm。请先安装 Node.js 和 npm,建议 Node.js 20 或 22。"
|
||||
fi
|
||||
local node_bin_dir
|
||||
node_bin_dir="$(dirname "${npm_bin}")"
|
||||
export PATH="${node_bin_dir}:${PATH}"
|
||||
if ! "${npm_bin}" --version >/dev/null 2>&1; then
|
||||
fail "npm 不可用:${npm_bin}。请确认同目录下存在 node,或重新安装 Node.js/npm。"
|
||||
fi
|
||||
printf "%s\n" "${npm_bin}"
|
||||
}
|
||||
|
||||
bootstrap_system_packages() {
|
||||
if [[ "${BOOTSTRAP_SYSTEM}" != "1" && -f "${ENV_FILE}" ]]; then
|
||||
return
|
||||
fi
|
||||
require_command sudo "首次安装系统依赖需要 sudo。"
|
||||
if ! command -v apt-get >/dev/null 2>&1; then
|
||||
warn "未发现 apt-get,跳过系统依赖安装。请手工安装 git、curl、pyenv、Node.js、npm 和 Python 编译依赖。"
|
||||
return
|
||||
fi
|
||||
log "首次初始化 Ubuntu 系统依赖"
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
git curl ca-certificates build-essential \
|
||||
libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev \
|
||||
libffi-dev liblzma-dev libncursesw5-dev xz-utils tk-dev \
|
||||
systemd
|
||||
if [[ "${ENABLE_LINUX_ACCOUNTS}" == "1" ]]; then
|
||||
sudo apt-get install -y passwd python3 python3-venv
|
||||
fi
|
||||
}
|
||||
|
||||
configure_nginx_upload_limit() {
|
||||
if [[ "${BOOTSTRAP_SYSTEM}" != "1" && -f "${ENV_FILE}" ]]; then
|
||||
return
|
||||
fi
|
||||
local script="${ROOT_DIR}/scripts/configure-nginx-upload-limit.sh"
|
||||
if [[ -x "${script}" ]]; then
|
||||
log "配置 nginx 上传体积限制"
|
||||
"${script}" || warn "nginx 上传体积限制配置失败,可稍后手动执行:bash ${script}"
|
||||
fi
|
||||
}
|
||||
|
||||
prompt_value() {
|
||||
local var_name="$1"
|
||||
local prompt="$2"
|
||||
local default_value="${3:-}"
|
||||
local secret="${4:-0}"
|
||||
local value
|
||||
|
||||
if [[ "${secret}" == "1" ]]; then
|
||||
read -r -s -p "${prompt}" value
|
||||
printf "\n"
|
||||
else
|
||||
if [[ -n "${default_value}" ]]; then
|
||||
read -r -p "${prompt} [${default_value}]: " value
|
||||
else
|
||||
read -r -p "${prompt}: " value
|
||||
fi
|
||||
fi
|
||||
value="${value:-${default_value}}"
|
||||
printf -v "${var_name}" "%s" "${value}"
|
||||
}
|
||||
|
||||
write_env_file() {
|
||||
local api_key="$1"
|
||||
local base_url="$2"
|
||||
local model="$3"
|
||||
local backend_host="$4"
|
||||
local backend_port="$5"
|
||||
local frontend_host="$6"
|
||||
local frontend_port="$7"
|
||||
local claw_api_url="http://${backend_host}:${backend_port}"
|
||||
|
||||
umask 077
|
||||
cat >"${ENV_FILE}" <<EOF
|
||||
# ZK Data Agent 本机部署配置。
|
||||
# 该文件包含敏感信息,只保存在部署机器本地,不要提交到 git。
|
||||
|
||||
export OPENAI_API_KEY=$(printf "%q" "${api_key}")
|
||||
export OPENAI_BASE_URL=$(printf "%q" "${base_url}")
|
||||
export OPENAI_MODEL=$(printf "%q" "${model}")
|
||||
export OPENAI_TIMEOUT_SECONDS="${OPENAI_TIMEOUT_SECONDS:-3600}"
|
||||
|
||||
export CLAW_DEPLOY_INSTANCE=$(printf "%q" "${DEPLOY_INSTANCE}")
|
||||
export CLAW_BACKEND_HOST=$(printf "%q" "${backend_host}")
|
||||
export CLAW_BACKEND_PORT=$(printf "%q" "${backend_port}")
|
||||
export CLAW_FRONTEND_HOST=$(printf "%q" "${frontend_host}")
|
||||
export CLAW_FRONTEND_PORT=$(printf "%q" "${frontend_port}")
|
||||
export CLAW_API_URL=$(printf "%q" "${claw_api_url}")
|
||||
export CLAW_SERVICE_SCOPE=$(printf "%q" "${SERVICE_SCOPE}")
|
||||
export CLAW_ENABLE_LINUX_ACCOUNTS=$(printf "%q" "${ENABLE_LINUX_ACCOUNTS:-0}")
|
||||
EOF
|
||||
chmod 600 "${ENV_FILE}"
|
||||
}
|
||||
|
||||
upsert_env_export() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
local tmp_file
|
||||
tmp_file="$(mktemp)"
|
||||
grep -v -E "^export ${key}=" "${ENV_FILE}" >"${tmp_file}" || true
|
||||
printf 'export %s=%s\n' "${key}" "$(printf "%q" "${value}")" >>"${tmp_file}"
|
||||
install -m 0600 "${tmp_file}" "${ENV_FILE}"
|
||||
rm -f "${tmp_file}"
|
||||
}
|
||||
|
||||
ensure_env_file() {
|
||||
if [[ -f "${ENV_FILE}" ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
source "${ENV_FILE}"
|
||||
if [[ -n "${REQUESTED_SERVICE_SCOPE}" ]]; then
|
||||
SERVICE_SCOPE="${REQUESTED_SERVICE_SCOPE}"
|
||||
export CLAW_SERVICE_SCOPE="${REQUESTED_SERVICE_SCOPE}"
|
||||
fi
|
||||
if [[ -n "${REQUESTED_ENABLE_LINUX_ACCOUNTS}" ]]; then
|
||||
ENABLE_LINUX_ACCOUNTS="${REQUESTED_ENABLE_LINUX_ACCOUNTS}"
|
||||
export CLAW_ENABLE_LINUX_ACCOUNTS="${REQUESTED_ENABLE_LINUX_ACCOUNTS}"
|
||||
fi
|
||||
configure_instance_names
|
||||
if [[ -n "${REQUESTED_SERVICE_SCOPE}" ]]; then
|
||||
upsert_env_export CLAW_SERVICE_SCOPE "${SERVICE_SCOPE}"
|
||||
fi
|
||||
if [[ -n "${REQUESTED_ENABLE_LINUX_ACCOUNTS}" ]]; then
|
||||
upsert_env_export CLAW_ENABLE_LINUX_ACCOUNTS "${ENABLE_LINUX_ACCOUNTS}"
|
||||
fi
|
||||
if [[ -z "${OPENAI_API_KEY:-}" ]]; then
|
||||
fail "${ENV_FILE} 缺少 OPENAI_API_KEY,请编辑该文件补齐。"
|
||||
fi
|
||||
log "使用本机部署配置:${ENV_FILE}"
|
||||
echo "CLAW_DEPLOY_INSTANCE=${DEPLOY_INSTANCE}"
|
||||
echo "OPENAI_BASE_URL=${OPENAI_BASE_URL:-}"
|
||||
echo "OPENAI_MODEL=${OPENAI_MODEL:-}"
|
||||
echo "OPENAI_TIMEOUT_SECONDS=${OPENAI_TIMEOUT_SECONDS:-3600}"
|
||||
echo "CLAW_SERVICE_SCOPE=${SERVICE_SCOPE}"
|
||||
echo "CLAW_ENABLE_LINUX_ACCOUNTS=${CLAW_ENABLE_LINUX_ACCOUNTS:-0}"
|
||||
echo "OPENAI_API_KEY=已配置"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ ! -t 0 ]]; then
|
||||
fail "首次部署需要交互式输入 OPENAI_API_KEY。请登录机器后执行:bash scripts/deploy-ubuntu.sh"
|
||||
fi
|
||||
|
||||
log "首次部署:生成本机配置 ${ENV_FILE}"
|
||||
echo "配置会写入 ${ENV_FILE},该文件已被 .gitignore 忽略。"
|
||||
|
||||
local api_key base_url model backend_host backend_port frontend_host frontend_port
|
||||
prompt_value api_key "请输入 OPENAI_API_KEY: " "" 1
|
||||
[[ -n "${api_key}" ]] || fail "OPENAI_API_KEY 不能为空。"
|
||||
prompt_value base_url "请输入 OPENAI_BASE_URL" "http://model.mify.ai.srv/v1"
|
||||
prompt_value model "请输入 OPENAI_MODEL" "tongyi/deepseek-v4-pro"
|
||||
configure_instance_names
|
||||
if [[ "${ENABLE_LINUX_ACCOUNTS}" == "1" && "${SERVICE_SCOPE}" != "system" ]]; then
|
||||
fail "--enable-linux-accounts 需要 --system-service,并建议使用 sudo/root 执行部署。"
|
||||
fi
|
||||
prompt_value backend_host "请输入后端监听地址" "127.0.0.1"
|
||||
prompt_value backend_port "请输入后端端口" "8765"
|
||||
prompt_value frontend_host "请输入前端监听地址" "0.0.0.0"
|
||||
prompt_value frontend_port "请输入前端端口" "3000"
|
||||
write_env_file "${api_key}" "${base_url}" "${model}" "${backend_host}" "${backend_port}" "${frontend_host}" "${frontend_port}"
|
||||
echo "已写入 ${ENV_FILE},后续修改配置请编辑这个文件。"
|
||||
}
|
||||
|
||||
update_git() {
|
||||
if [[ "${SKIP_GIT}" == "1" ]]; then
|
||||
log "跳过 git 拉取,使用当前工作区"
|
||||
return
|
||||
fi
|
||||
require_command git "请先安装 git。"
|
||||
cd "${ROOT_DIR}"
|
||||
[[ -d .git ]] || fail "${ROOT_DIR} 不是 git 仓库。"
|
||||
|
||||
if [[ -z "${BRANCH}" ]]; then
|
||||
BRANCH="$(git branch --show-current)"
|
||||
fi
|
||||
[[ -n "${BRANCH}" ]] || fail "无法判断当前分支,请显式传入分支名。"
|
||||
|
||||
log "更新代码:origin/${BRANCH}"
|
||||
git fetch origin
|
||||
git checkout "${BRANCH}"
|
||||
if [[ "${FORCE}" == "1" ]]; then
|
||||
git reset --hard "origin/${BRANCH}"
|
||||
else
|
||||
if [[ -n "$(git status --porcelain)" ]]; then
|
||||
fail "工作区有未提交改动。请先处理,或使用 --force 覆盖。"
|
||||
fi
|
||||
git pull --ff-only origin "${BRANCH}"
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_python() {
|
||||
require_command pyenv "请先安装 pyenv,并安装 Python ${PYTHON_VERSION}。"
|
||||
log "准备 Python 环境:${PYTHON_VERSION}"
|
||||
cd "${ROOT_DIR}"
|
||||
if ! pyenv versions --bare | grep -Fx "${PYTHON_VERSION}" >/dev/null 2>&1; then
|
||||
echo "pyenv 未发现 Python ${PYTHON_VERSION},开始安装。"
|
||||
pyenv install "${PYTHON_VERSION}"
|
||||
fi
|
||||
PYENV_VERSION="${PYTHON_VERSION}" pyenv exec python -m venv "${ROOT_DIR}/.venv"
|
||||
"${ROOT_DIR}/.venv/bin/python" -m pip install --upgrade pip setuptools wheel
|
||||
"${ROOT_DIR}/.venv/bin/python" -m pip install -e .
|
||||
}
|
||||
|
||||
build_frontend() {
|
||||
local npm_bin
|
||||
npm_bin="$(resolve_npm_bin)"
|
||||
export PATH="$(dirname "${npm_bin}"):${PATH}"
|
||||
log "安装并构建前端"
|
||||
cd "${ROOT_DIR}/frontend/app"
|
||||
if [[ -f package-lock.json ]]; then
|
||||
"${npm_bin}" ci
|
||||
else
|
||||
"${npm_bin}" install
|
||||
fi
|
||||
"${npm_bin}" run build
|
||||
}
|
||||
|
||||
remember_node_runtime() {
|
||||
local npm_bin npx_bin node_bin node_bin_dir tmp_file
|
||||
npm_bin="$(resolve_npm_bin)"
|
||||
node_bin_dir="$(dirname "${npm_bin}")"
|
||||
export PATH="${node_bin_dir}:${PATH}"
|
||||
node_bin="$(PATH="${node_bin_dir}:${PATH}" command -v node || true)"
|
||||
npx_bin="$(PATH="${node_bin_dir}:${PATH}" command -v npx || true)"
|
||||
tmp_file="$(mktemp)"
|
||||
grep -v -E '^(# systemd 用户服务不会读取交互式 shell 配置|export CLAW_(NPM_BIN|NPX_BIN|NODE_BIN|NODE_BIN_DIR)=)' "${ENV_FILE}" >"${tmp_file}" || true
|
||||
cat >>"${tmp_file}" <<EOF
|
||||
|
||||
# systemd 用户服务不会读取交互式 shell 配置,这里记录部署时可用的 Node.js/npm/npx 路径。
|
||||
export CLAW_NPM_BIN=$(printf "%q" "${npm_bin}")
|
||||
export CLAW_NPX_BIN=$(printf "%q" "${npx_bin}")
|
||||
export CLAW_NODE_BIN=$(printf "%q" "${node_bin}")
|
||||
export CLAW_NODE_BIN_DIR=$(printf "%q" "${node_bin_dir}")
|
||||
EOF
|
||||
install -m 0600 "${tmp_file}" "${ENV_FILE}"
|
||||
rm -f "${tmp_file}"
|
||||
log "记录 Node.js 运行路径"
|
||||
echo "npm=${npm_bin}"
|
||||
echo "npx=${npx_bin:-未检测到}"
|
||||
echo "node=${node_bin:-未检测到}"
|
||||
}
|
||||
|
||||
install_systemd_service() {
|
||||
local service_name="$1"
|
||||
local template_path="$2"
|
||||
local target_dir="${USER_SYSTEMD_DIR}"
|
||||
local wanted_by="default.target"
|
||||
if [[ "${SERVICE_SCOPE}" == "system" ]]; then
|
||||
target_dir="${SYSTEM_SYSTEMD_DIR}"
|
||||
wanted_by="multi-user.target"
|
||||
fi
|
||||
local target_path="${target_dir}/${service_name}.service"
|
||||
local tmp_path
|
||||
tmp_path="$(mktemp)"
|
||||
mkdir -p "${target_dir}"
|
||||
sed \
|
||||
-e "s#__PROJECT_ROOT__#${ROOT_DIR}#g" \
|
||||
-e "s#__BACKEND_SERVICE__#${BACKEND_SERVICE}#g" \
|
||||
-e "s#__FRONTEND_SERVICE__#${FRONTEND_SERVICE}#g" \
|
||||
-e "s#WantedBy=default.target#WantedBy=${wanted_by}#g" \
|
||||
"${template_path}" >"${tmp_path}"
|
||||
install -m 0644 "${tmp_path}" "${target_path}"
|
||||
rm -f "${tmp_path}"
|
||||
}
|
||||
|
||||
install_services() {
|
||||
require_command systemctl "当前系统不支持 systemd,无法安装服务。"
|
||||
if [[ "${ENABLE_LINUX_ACCOUNTS}" == "1" && "${SERVICE_SCOPE}" != "system" ]]; then
|
||||
fail "CLAW_ENABLE_LINUX_ACCOUNTS=1 需要 system 服务。请用 sudo bash scripts/deploy-ubuntu.sh --system-service --enable-linux-accounts。"
|
||||
fi
|
||||
log "安装/更新 ${SERVICE_SCOPE} systemd 服务"
|
||||
install_systemd_service "${BACKEND_SERVICE}" "${ROOT_DIR}/deploy/systemd/zk-data-agent-backend.service.template"
|
||||
install_systemd_service "${FRONTEND_SERVICE}" "${ROOT_DIR}/deploy/systemd/zk-data-agent-frontend.service.template"
|
||||
if [[ "${SERVICE_SCOPE}" == "system" ]]; then
|
||||
systemctl daemon-reload
|
||||
systemctl enable "${BACKEND_SERVICE}" "${FRONTEND_SERVICE}"
|
||||
else
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable "${BACKEND_SERVICE}" "${FRONTEND_SERVICE}"
|
||||
fi
|
||||
}
|
||||
|
||||
restart_services() {
|
||||
log "重启服务"
|
||||
if [[ "${SERVICE_SCOPE}" == "system" ]]; then
|
||||
systemctl restart "${BACKEND_SERVICE}"
|
||||
systemctl restart "${FRONTEND_SERVICE}"
|
||||
else
|
||||
systemctl --user restart "${BACKEND_SERVICE}"
|
||||
systemctl --user restart "${FRONTEND_SERVICE}"
|
||||
fi
|
||||
}
|
||||
|
||||
health_check() {
|
||||
# shellcheck disable=SC1090
|
||||
source "${ENV_FILE}"
|
||||
local backend_url="http://${CLAW_BACKEND_HOST:-127.0.0.1}:${CLAW_BACKEND_PORT:-8765}/api/state"
|
||||
local frontend_url="http://127.0.0.1:${CLAW_FRONTEND_PORT:-3000}/"
|
||||
log "健康检查"
|
||||
for _ in {1..40}; do
|
||||
if curl -fsS "${backend_url}" >/dev/null 2>&1 && curl -fsS "${frontend_url}" >/dev/null 2>&1; then
|
||||
echo "backend: ${backend_url}"
|
||||
echo "frontend: http://<机器IP>:${CLAW_FRONTEND_PORT:-3000}/"
|
||||
echo "服务已启动。"
|
||||
return
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
warn "健康检查失败,输出最近日志。"
|
||||
if [[ "${SERVICE_SCOPE}" == "system" ]]; then
|
||||
journalctl -u "${BACKEND_SERVICE}" -n 80 --no-pager || true
|
||||
journalctl -u "${FRONTEND_SERVICE}" -n 80 --no-pager || true
|
||||
else
|
||||
journalctl --user -u "${BACKEND_SERVICE}" -n 80 --no-pager || true
|
||||
journalctl --user -u "${FRONTEND_SERVICE}" -n 80 --no-pager || true
|
||||
fi
|
||||
exit 1
|
||||
}
|
||||
|
||||
main() {
|
||||
log "ZK Data Agent Ubuntu 部署"
|
||||
bootstrap_system_packages
|
||||
configure_nginx_upload_limit
|
||||
update_git
|
||||
ensure_env_file
|
||||
configure_instance_names
|
||||
ensure_python
|
||||
build_frontend
|
||||
remember_node_runtime
|
||||
install_services
|
||||
restart_services
|
||||
health_check
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+55
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
project_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
env_file="${project_dir}/.env"
|
||||
|
||||
if [[ -e "${env_file}" ]]; then
|
||||
echo "${env_file} already exists; refusing to overwrite it." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
umask 077
|
||||
read -r -p "Bootstrap admin email: " admin_email
|
||||
read -r -s -p "Rotated model API key: " model_api_key
|
||||
echo
|
||||
|
||||
if [[ -z "${admin_email}" || -z "${model_api_key}" ]]; then
|
||||
echo "Admin email and model API key are required." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
random_hex() {
|
||||
openssl rand -hex "$1"
|
||||
}
|
||||
|
||||
admin_password="$(random_hex 18)"
|
||||
postgres_password="$(random_hex 18)"
|
||||
|
||||
{
|
||||
echo "WEBUI_URL=http://localhost:3000"
|
||||
echo "CORS_ALLOW_ORIGIN=http://localhost:3000"
|
||||
echo "WEBUI_COOKIE_SECURE=false"
|
||||
echo "WEBUI_SECRET_KEY=$(random_hex 32)"
|
||||
echo "WEBUI_ADMIN_EMAIL=${admin_email}"
|
||||
echo "WEBUI_ADMIN_PASSWORD=${admin_password}"
|
||||
echo "OPENWEBUI_FORWARD_JWT_SECRET=$(random_hex 32)"
|
||||
echo "INTERNAL_PROVIDER_KEY=$(random_hex 32)"
|
||||
echo "INTERNAL_GATEWAY_KEY=$(random_hex 32)"
|
||||
echo "MODEL_API_KEY=${model_api_key}"
|
||||
echo "MODEL_API_BASE_URL=https://api.k1412.top"
|
||||
echo "POSTGRES_USER=agent"
|
||||
echo "POSTGRES_PASSWORD=${postgres_password}"
|
||||
echo "POSTGRES_DB=agent"
|
||||
echo "EXECUTION_PROVIDER=local-docker"
|
||||
echo "WORKSPACE_IMAGE=k1412-agent-workspace:dev"
|
||||
echo "WORKSPACE_NETWORK_ENABLED=true"
|
||||
echo "WORKSPACE_MEMORY_LIMIT=2g"
|
||||
echo "WORKSPACE_CPU_LIMIT=2"
|
||||
echo "WORKSPACE_PIDS_LIMIT=512"
|
||||
} > "${env_file}"
|
||||
|
||||
chmod 600 "${env_file}"
|
||||
echo "Created ${env_file} with mode 600."
|
||||
echo "Bootstrap admin password: ${admin_password}"
|
||||
echo "Store that password in a password manager; it is not printed again."
|
||||
@@ -1,84 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# 从 git 拉取/更新仓库,然后调用仓库内 scripts/deploy-ubuntu.sh。
|
||||
|
||||
REPO="${REPO:-git@git.n.xiaomi.com:wuyang6/zk-data-agent.git}"
|
||||
BRANCH="${BRANCH:-main}"
|
||||
APP_DIR="${APP_DIR:-${HOME}/zk-data-agent}"
|
||||
FORCE=0
|
||||
BOOTSTRAP_SYSTEM="${BOOTSTRAP_SYSTEM:-0}"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: bash install-from-git.sh [--repo REPO] [--branch BRANCH] [--dir APP_DIR] [--force] [--bootstrap-system]
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--repo)
|
||||
REPO="$2"
|
||||
shift 2
|
||||
;;
|
||||
--branch)
|
||||
BRANCH="$2"
|
||||
shift 2
|
||||
;;
|
||||
--dir)
|
||||
APP_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--force)
|
||||
FORCE=1
|
||||
shift
|
||||
;;
|
||||
--bootstrap-system)
|
||||
BOOTSTRAP_SYSTEM=1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unexpected argument: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
command -v git >/dev/null 2>&1 || {
|
||||
echo "Missing git. Please install git first." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [[ -e "${APP_DIR}" && ! -d "${APP_DIR}/.git" ]]; then
|
||||
echo "${APP_DIR} already exists but is not a git repository." >&2
|
||||
echo "Please move it away, or rerun with --force to replace it." >&2
|
||||
if [[ "${FORCE}" != "1" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
rm -rf "${APP_DIR}"
|
||||
fi
|
||||
|
||||
if [[ -d "${APP_DIR}/.git" ]]; then
|
||||
cd "${APP_DIR}"
|
||||
git fetch origin
|
||||
git checkout "${BRANCH}"
|
||||
if [[ "${FORCE}" == "1" ]]; then
|
||||
git reset --hard "origin/${BRANCH}"
|
||||
else
|
||||
git pull --ff-only origin "${BRANCH}"
|
||||
fi
|
||||
else
|
||||
git clone --branch "${BRANCH}" "${REPO}" "${APP_DIR}"
|
||||
cd "${APP_DIR}"
|
||||
fi
|
||||
|
||||
deploy_args=("${BRANCH}" "--skip-git")
|
||||
if [[ "${BOOTSTRAP_SYSTEM}" == "1" ]]; then
|
||||
deploy_args+=("--bootstrap-system")
|
||||
fi
|
||||
bash scripts/deploy-ubuntu.sh "${deploy_args[@]}"
|
||||
@@ -1,47 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Ubuntu/systemd 后端启动入口。
|
||||
# 配置从项目根目录的 .env.deploy 读取;该文件只保存在部署机器本地,不提交到 git。
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
ENV_FILE="${ROOT_DIR}/.env.deploy"
|
||||
|
||||
if [[ -f "${ENV_FILE}" ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
source "${ENV_FILE}"
|
||||
else
|
||||
echo "Missing deploy config: ${ENV_FILE}" >&2
|
||||
echo "Run: bash scripts/deploy-ubuntu.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export OPENAI_API_KEY
|
||||
export OPENAI_BASE_URL="${OPENAI_BASE_URL:-http://model.mify.ai.srv/v1}"
|
||||
export OPENAI_MODEL="${OPENAI_MODEL:-tongyi/deepseek-v4-pro}"
|
||||
export OPENAI_TIMEOUT_SECONDS="${OPENAI_TIMEOUT_SECONDS:-3600}"
|
||||
export CLAW_MODEL_IDLE_TIMEOUT_SECONDS="${CLAW_MODEL_IDLE_TIMEOUT_SECONDS:-${OPENAI_TIMEOUT_SECONDS}}"
|
||||
|
||||
# 后端也会调用 Node 生态工具,例如 feishu-mcp-pro。systemd 不读取交互式
|
||||
# shell 的 nvm 配置,所以需要把部署时记录的 Node 路径显式传给 Python 进程。
|
||||
if [[ -n "${CLAW_NODE_BIN_DIR:-}" ]]; then
|
||||
export PATH="${CLAW_NODE_BIN_DIR}:${PATH}"
|
||||
fi
|
||||
export CLAW_NPM_BIN="${CLAW_NPM_BIN:-}"
|
||||
export CLAW_NPX_BIN="${CLAW_NPX_BIN:-}"
|
||||
export CLAW_NODE_BIN="${CLAW_NODE_BIN:-}"
|
||||
export CLAW_NODE_BIN_DIR="${CLAW_NODE_BIN_DIR:-}"
|
||||
|
||||
BACKEND_HOST="${CLAW_BACKEND_HOST:-127.0.0.1}"
|
||||
BACKEND_PORT="${CLAW_BACKEND_PORT:-8765}"
|
||||
PYTHON_BIN="${CLAW_PYTHON_BIN:-${ROOT_DIR}/.venv/bin/python}"
|
||||
|
||||
exec "${PYTHON_BIN}" -m src.gui \
|
||||
--host "${BACKEND_HOST}" \
|
||||
--port "${BACKEND_PORT}" \
|
||||
--cwd "${ROOT_DIR}" \
|
||||
--timeout-seconds "${OPENAI_TIMEOUT_SECONDS}" \
|
||||
--session-dir "${ROOT_DIR}/.port_sessions/agent" \
|
||||
--allow-write \
|
||||
--allow-shell \
|
||||
--no-browser
|
||||
@@ -1,38 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Ubuntu/systemd 前端启动入口。
|
||||
# 运行生产构建后的 Next.js 服务;开发调试继续使用 scripts/start-webui.sh。
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
ENV_FILE="${ROOT_DIR}/.env.deploy"
|
||||
|
||||
if [[ -f "${ENV_FILE}" ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
source "${ENV_FILE}"
|
||||
else
|
||||
echo "Missing deploy config: ${ENV_FILE}" >&2
|
||||
echo "Run: bash scripts/deploy-ubuntu.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BACKEND_HOST="${CLAW_BACKEND_HOST:-127.0.0.1}"
|
||||
BACKEND_PORT="${CLAW_BACKEND_PORT:-8765}"
|
||||
FRONTEND_HOST="${CLAW_FRONTEND_HOST:-0.0.0.0}"
|
||||
FRONTEND_PORT="${CLAW_FRONTEND_PORT:-3000}"
|
||||
NPM_BIN="${CLAW_NPM_BIN:-$(command -v npm || true)}"
|
||||
|
||||
if [[ -n "${CLAW_NODE_BIN_DIR:-}" ]]; then
|
||||
export PATH="${CLAW_NODE_BIN_DIR}:${PATH}"
|
||||
fi
|
||||
|
||||
if [[ -z "${NPM_BIN}" || ! -x "${NPM_BIN}" ]]; then
|
||||
echo "npm not found for frontend service." >&2
|
||||
echo "Run deploy again from a shell where npm is available: bash scripts/deploy-ubuntu.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export CLAW_API_URL="${CLAW_API_URL:-http://${BACKEND_HOST}:${BACKEND_PORT}}"
|
||||
|
||||
cd "${ROOT_DIR}/frontend/app"
|
||||
exec "${NPM_BIN}" run start -- --hostname "${FRONTEND_HOST}" --port "${FRONTEND_PORT}"
|
||||
@@ -1,182 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# 一键启动本地 WebUI:
|
||||
# - 后端 FastAPI: 127.0.0.1:8765
|
||||
# - 前端 Next.js: 127.0.0.1:3000
|
||||
# - PID 和日志统一写入 .port_sessions,重复执行会清理上一次由本脚本启动的进程。
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
RUN_DIR="${ROOT_DIR}/.port_sessions"
|
||||
ENV_FILE="${ROOT_DIR}/.env.deploy"
|
||||
if [[ -f "${ENV_FILE}" ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
source "${ENV_FILE}"
|
||||
fi
|
||||
BACKEND_HOST="${CLAW_BACKEND_HOST:-0.0.0.0}"
|
||||
BACKEND_PORT="${CLAW_BACKEND_PORT:-8765}"
|
||||
FRONTEND_HOST="${CLAW_FRONTEND_HOST:-0.0.0.0}"
|
||||
FRONTEND_PORT="${CLAW_FRONTEND_PORT:-3000}"
|
||||
BACKEND_URL="http://${BACKEND_HOST}:${BACKEND_PORT}"
|
||||
FRONTEND_URL="http://${FRONTEND_HOST}:${FRONTEND_PORT}"
|
||||
PYTHON_BIN="${CLAW_PYTHON_BIN:-${ROOT_DIR}/.venv/bin/python}"
|
||||
if [[ ! -x "${PYTHON_BIN}" ]]; then
|
||||
PYTHON_BIN="python3"
|
||||
fi
|
||||
LOCAL_IPV4_ORIGINS="$(ifconfig 2>/dev/null | awk '/inet / && $2 !~ /^127\\./ {print $2}' | awk '!seen[$0]++' | paste -sd, -)"
|
||||
DEFAULT_ALLOWED_DEV_ORIGINS="localhost,127.0.0.1,0.0.0.0"
|
||||
if [[ -n "${LOCAL_IPV4_ORIGINS}" ]]; then
|
||||
DEFAULT_ALLOWED_DEV_ORIGINS="${DEFAULT_ALLOWED_DEV_ORIGINS},${LOCAL_IPV4_ORIGINS}"
|
||||
while IFS= read -r local_ip; do
|
||||
[[ -z "${local_ip}" ]] && continue
|
||||
DEFAULT_ALLOWED_DEV_ORIGINS="${DEFAULT_ALLOWED_DEV_ORIGINS},${local_ip}:${FRONTEND_PORT}"
|
||||
done < <(printf "%s\n" "${LOCAL_IPV4_ORIGINS}" | tr "," "\n")
|
||||
fi
|
||||
|
||||
mkdir -p "${RUN_DIR}"
|
||||
|
||||
stop_pid_file() {
|
||||
local pid_file="$1"
|
||||
local name="$2"
|
||||
if [[ ! -f "${pid_file}" ]]; then
|
||||
return
|
||||
fi
|
||||
local pid
|
||||
pid="$(cat "${pid_file}" 2>/dev/null || true)"
|
||||
if [[ -n "${pid}" ]] && kill -0 "${pid}" 2>/dev/null; then
|
||||
echo "Stopping previous ${name} (${pid})..."
|
||||
kill "${pid}" 2>/dev/null || true
|
||||
for _ in {1..20}; do
|
||||
if ! kill -0 "${pid}" 2>/dev/null; then
|
||||
break
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
if kill -0 "${pid}" 2>/dev/null; then
|
||||
kill -9 "${pid}" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
rm -f "${pid_file}"
|
||||
}
|
||||
|
||||
stop_port_listener() {
|
||||
local port="$1"
|
||||
local name="$2"
|
||||
local pids
|
||||
pids="$(lsof -tiTCP:"${port}" -sTCP:LISTEN 2>/dev/null || true)"
|
||||
if [[ -z "${pids}" ]]; then
|
||||
return
|
||||
fi
|
||||
echo "Stopping ${name} listener(s) on port ${port}: ${pids//$'\n'/ }..."
|
||||
while IFS= read -r pid; do
|
||||
[[ -z "${pid}" ]] && continue
|
||||
kill "${pid}" 2>/dev/null || true
|
||||
done <<<"${pids}"
|
||||
for _ in {1..20}; do
|
||||
if ! lsof -tiTCP:"${port}" -sTCP:LISTEN >/dev/null 2>&1; then
|
||||
return
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
while IFS= read -r pid; do
|
||||
[[ -z "${pid}" ]] && continue
|
||||
kill -9 "${pid}" 2>/dev/null || true
|
||||
done <<<"${pids}"
|
||||
}
|
||||
|
||||
wait_for_url() {
|
||||
local url="$1"
|
||||
local name="$2"
|
||||
local log_file="$3"
|
||||
for _ in {1..80}; do
|
||||
if curl -fsS "${url}" >/dev/null 2>&1; then
|
||||
echo "${name} ready: ${url}"
|
||||
return 0
|
||||
fi
|
||||
sleep 0.25
|
||||
done
|
||||
echo "${name} did not become ready: ${url}" >&2
|
||||
echo "---- ${name} log tail ----" >&2
|
||||
tail -80 "${log_file}" >&2 || true
|
||||
return 1
|
||||
}
|
||||
|
||||
shell_quote() {
|
||||
printf "%q" "$1"
|
||||
}
|
||||
|
||||
start_detached_process() {
|
||||
local name="$1"
|
||||
local pid_file="$2"
|
||||
local log_file="$3"
|
||||
local work_dir="$4"
|
||||
shift 4
|
||||
local command="cd $(shell_quote "${work_dir}") && echo \$\$ > $(shell_quote "${pid_file}") && exec"
|
||||
local arg
|
||||
for arg in "$@"; do
|
||||
command+=" $(shell_quote "${arg}")"
|
||||
done
|
||||
command+=" >$(shell_quote "${log_file}") 2>&1"
|
||||
|
||||
if command -v screen >/dev/null 2>&1; then
|
||||
screen -dmS "claw-${name}" bash -lc "${command}"
|
||||
else
|
||||
(
|
||||
cd "${work_dir}"
|
||||
nohup "$@" >"${log_file}" 2>&1 &
|
||||
echo "$!" >"${pid_file}"
|
||||
)
|
||||
fi
|
||||
}
|
||||
|
||||
stop_pid_file "${RUN_DIR}/webui-frontend.pid" "frontend"
|
||||
stop_pid_file "${RUN_DIR}/webui-backend.pid" "backend"
|
||||
stop_port_listener "${FRONTEND_PORT}" "frontend"
|
||||
stop_port_listener "${BACKEND_PORT}" "backend"
|
||||
|
||||
if [[ -z "${OPENAI_API_KEY:-}" ]]; then
|
||||
echo "OPENAI_API_KEY is not configured." >&2
|
||||
echo "Run scripts/deploy-ubuntu.sh once, or create ${ENV_FILE} from .env.deploy.example." >&2
|
||||
fi
|
||||
export OPENAI_API_KEY="${OPENAI_API_KEY:-}"
|
||||
export OPENAI_BASE_URL="${OPENAI_BASE_URL:-http://model.mify.ai.srv/v1}"
|
||||
export OPENAI_MODEL="${OPENAI_MODEL:-tongyi/deepseek-v4-pro}"
|
||||
export OPENAI_TIMEOUT_SECONDS="${OPENAI_TIMEOUT_SECONDS:-3600}"
|
||||
|
||||
BACKEND_LOG="${RUN_DIR}/webui-backend.log"
|
||||
FRONTEND_LOG="${RUN_DIR}/webui-frontend.log"
|
||||
|
||||
echo "Starting backend on ${BACKEND_URL}..."
|
||||
start_detached_process "backend" "${RUN_DIR}/webui-backend.pid" "${BACKEND_LOG}" "${ROOT_DIR}" \
|
||||
"${PYTHON_BIN}" -m src.gui \
|
||||
--host "${BACKEND_HOST}" \
|
||||
--port "${BACKEND_PORT}" \
|
||||
--cwd "${ROOT_DIR}" \
|
||||
--timeout-seconds "${OPENAI_TIMEOUT_SECONDS}" \
|
||||
--session-dir "${RUN_DIR}/agent" \
|
||||
--allow-write \
|
||||
--allow-shell \
|
||||
--no-browser
|
||||
|
||||
wait_for_url "${BACKEND_URL}/api/state" "backend" "${BACKEND_LOG}"
|
||||
|
||||
echo "Starting frontend on ${FRONTEND_URL}..."
|
||||
export CLAW_API_URL="${BACKEND_URL}"
|
||||
export CLAW_ALLOWED_DEV_ORIGINS="${CLAW_ALLOWED_DEV_ORIGINS:-${DEFAULT_ALLOWED_DEV_ORIGINS}}"
|
||||
start_detached_process "frontend" "${RUN_DIR}/webui-frontend.pid" "${FRONTEND_LOG}" "${ROOT_DIR}/frontend/app" \
|
||||
npm run dev -- --hostname "${FRONTEND_HOST}" --port "${FRONTEND_PORT}"
|
||||
|
||||
wait_for_url "${FRONTEND_URL}" "frontend" "${FRONTEND_LOG}"
|
||||
|
||||
cat <<EOF
|
||||
|
||||
WebUI started.
|
||||
Frontend: ${FRONTEND_URL}
|
||||
Backend : ${BACKEND_URL}
|
||||
Logs : ${RUN_DIR}/webui-frontend.log
|
||||
${RUN_DIR}/webui-backend.log
|
||||
|
||||
Stop:
|
||||
kill \$(cat "${RUN_DIR}/webui-frontend.pid") \$(cat "${RUN_DIR}/webui-backend.pid")
|
||||
|
||||
EOF
|
||||
@@ -1,96 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Subprocess-safe remote program-state sync.
|
||||
|
||||
Reads jupyter binding JSON, fetches program-state.jsonl via jupyter
|
||||
Contents API (HTTP GET), writes to local path. Designed to be called
|
||||
via asyncio.create_subprocess_exec with a hard timeout — if the pod is
|
||||
unreachable, the parent kills this process cleanly.
|
||||
|
||||
Usage: python3 sync_remote_state.py <binding_json_path> <local_state_path>
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import ssl
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 3:
|
||||
sys.exit(1)
|
||||
binding_path = sys.argv[1]
|
||||
local_path = sys.argv[2]
|
||||
|
||||
try:
|
||||
with open(binding_path, encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
except (OSError, json.JSONDecodeError):
|
||||
sys.exit(1)
|
||||
|
||||
binding = data.get('binding', {})
|
||||
base_url = binding.get('base_url', '').rstrip('/')
|
||||
api_path = binding.get('workspace_api_path', '')
|
||||
if not base_url or not api_path:
|
||||
sys.exit(1)
|
||||
|
||||
cookies_list = data.get('cookies', [])
|
||||
cookie_str = '; '.join(f"{c['name']}={c['value']}" for c in cookies_list)
|
||||
xsrf = data.get('xsrf_token', '')
|
||||
|
||||
file_api_path = f"{api_path}/output/program-state.jsonl"
|
||||
url = f"{base_url}/api/contents/{file_api_path}?content=1&type=file"
|
||||
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_peer = False
|
||||
|
||||
req = urllib.request.Request(url)
|
||||
req.add_header('Cookie', cookie_str)
|
||||
if xsrf:
|
||||
req.add_header('X-XSRFToken', xsrf)
|
||||
|
||||
try:
|
||||
resp = urllib.request.urlopen(req, timeout=8, context=ctx)
|
||||
body = json.loads(resp.read())
|
||||
except (urllib.error.URLError, OSError, json.JSONDecodeError, TimeoutError):
|
||||
sys.exit(1)
|
||||
|
||||
content = body.get('content', '')
|
||||
if not content or not content.strip():
|
||||
sys.exit(0)
|
||||
|
||||
# Preserve local-only _synthetic entries
|
||||
local_synthetics = []
|
||||
try:
|
||||
with open(local_path, encoding='utf-8') as f:
|
||||
for line in f:
|
||||
if '"_synthetic"' in line:
|
||||
try:
|
||||
obj = json.loads(line)
|
||||
if obj.get('_synthetic'):
|
||||
local_synthetics.append(line.rstrip('\n'))
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
pass
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
merged = content.rstrip('\n')
|
||||
if local_synthetics:
|
||||
merged += '\n' + '\n'.join(local_synthetics)
|
||||
merged += '\n'
|
||||
|
||||
# Skip rewrite if unchanged
|
||||
try:
|
||||
with open(local_path, encoding='utf-8') as f:
|
||||
if f.read() == merged:
|
||||
sys.exit(0)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
import os
|
||||
os.makedirs(os.path.dirname(local_path), exist_ok=True)
|
||||
with open(local_path, 'w', encoding='utf-8') as f:
|
||||
f.write(merged)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,133 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# 快速更新脚本:适合已经完成首次部署后的日常代码更新。
|
||||
# 只做 git 更新、前端 build、重启 systemd 服务;不检查/安装系统依赖。
|
||||
# 如果 pyproject.toml 在本次更新中变化,会额外同步一次 Python 依赖。
|
||||
# 如果本次改动包含依赖、systemd 模板或部署脚本变化,请使用 scripts/deploy-ubuntu.sh。
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
ENV_FILE="${ROOT_DIR}/.env.deploy"
|
||||
DEPLOY_INSTANCE="${CLAW_DEPLOY_INSTANCE:-zk-data-agent}"
|
||||
BACKEND_SERVICE="${CLAW_BACKEND_SERVICE:-${DEPLOY_INSTANCE}-backend}"
|
||||
FRONTEND_SERVICE="${CLAW_FRONTEND_SERVICE:-${DEPLOY_INSTANCE}-frontend}"
|
||||
SERVICE_SCOPE="${CLAW_SERVICE_SCOPE:-}"
|
||||
BRANCH="${1:-}"
|
||||
|
||||
log() {
|
||||
printf "\n\033[1;34m==>\033[0m %s\n" "$1"
|
||||
}
|
||||
|
||||
fail() {
|
||||
printf "\033[1;31mERROR\033[0m %s\n" "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
configure_instance_names() {
|
||||
if [[ -z "${SERVICE_SCOPE}" && -n "${CLAW_SERVICE_SCOPE:-}" ]]; then
|
||||
SERVICE_SCOPE="${CLAW_SERVICE_SCOPE}"
|
||||
fi
|
||||
DEPLOY_INSTANCE="${CLAW_DEPLOY_INSTANCE:-${DEPLOY_INSTANCE:-zk-data-agent}}"
|
||||
BACKEND_SERVICE="${CLAW_BACKEND_SERVICE:-${DEPLOY_INSTANCE}-backend}"
|
||||
FRONTEND_SERVICE="${CLAW_FRONTEND_SERVICE:-${DEPLOY_INSTANCE}-frontend}"
|
||||
if [[ -z "${SERVICE_SCOPE}" ]]; then
|
||||
if [[ "${EUID}" == "0" ]]; then
|
||||
SERVICE_SCOPE="system"
|
||||
else
|
||||
SERVICE_SCOPE="user"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
resolve_npm_bin() {
|
||||
if [[ -f "${ENV_FILE}" ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
source "${ENV_FILE}"
|
||||
configure_instance_names
|
||||
fi
|
||||
if [[ -n "${CLAW_NODE_BIN_DIR:-}" ]]; then
|
||||
export PATH="${CLAW_NODE_BIN_DIR}:${PATH}"
|
||||
fi
|
||||
local npm_bin="${CLAW_NPM_BIN:-$(command -v npm || true)}"
|
||||
if [[ -z "${npm_bin}" || ! -x "${npm_bin}" ]]; then
|
||||
fail "缺少命令:npm。请先安装 Node.js 和 npm,或执行 bash scripts/deploy-ubuntu.sh 重新记录 Node.js 路径。"
|
||||
fi
|
||||
local node_bin_dir
|
||||
node_bin_dir="$(dirname "${npm_bin}")"
|
||||
export PATH="${node_bin_dir}:${PATH}"
|
||||
if ! "${npm_bin}" --version >/dev/null 2>&1; then
|
||||
fail "npm 不可用:${npm_bin}。请确认同目录下存在 node,或重新安装 Node.js/npm。"
|
||||
fi
|
||||
printf "%s\n" "${npm_bin}"
|
||||
}
|
||||
|
||||
sync_python_dependencies_if_needed() {
|
||||
if [[ "${PYTHON_DEPS_CHANGED}" != "1" ]]; then
|
||||
return
|
||||
fi
|
||||
local python_bin="${ROOT_DIR}/.venv/bin/python"
|
||||
if [[ ! -x "${python_bin}" ]]; then
|
||||
fail "缺少项目 Python 虚拟环境:${python_bin}。请执行 bash scripts/deploy-ubuntu.sh。"
|
||||
fi
|
||||
log "检测到 pyproject.toml 变化,同步 Python 依赖"
|
||||
"${python_bin}" -m pip install -e .
|
||||
}
|
||||
|
||||
systemctl_for_scope() {
|
||||
if [[ "${SERVICE_SCOPE}" == "system" ]]; then
|
||||
if [[ "${EUID}" == "0" ]]; then
|
||||
systemctl "$@"
|
||||
else
|
||||
sudo systemctl "$@"
|
||||
fi
|
||||
else
|
||||
systemctl --user "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
cd "${ROOT_DIR}"
|
||||
[[ -d .git ]] || fail "${ROOT_DIR} 不是 git 仓库。"
|
||||
|
||||
if [[ -f "${ENV_FILE}" ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
source "${ENV_FILE}"
|
||||
fi
|
||||
configure_instance_names
|
||||
|
||||
if [[ -z "${BRANCH}" ]]; then
|
||||
BRANCH="$(git branch --show-current)"
|
||||
fi
|
||||
[[ -n "${BRANCH}" ]] || fail "无法判断当前分支,请显式传入分支名。"
|
||||
|
||||
if [[ -n "$(git status --porcelain)" ]]; then
|
||||
fail "工作区有未提交改动。请先处理后再快速更新。"
|
||||
fi
|
||||
|
||||
PYTHON_DEPS_CHANGED=0
|
||||
before_rev="$(git rev-parse HEAD)"
|
||||
log "拉取代码:origin/${BRANCH}"
|
||||
git fetch origin
|
||||
git checkout "${BRANCH}"
|
||||
git pull --ff-only origin "${BRANCH}"
|
||||
after_rev="$(git rev-parse HEAD)"
|
||||
if [[ "${before_rev}" != "${after_rev}" ]] && ! git diff --quiet "${before_rev}" "${after_rev}" -- pyproject.toml; then
|
||||
PYTHON_DEPS_CHANGED=1
|
||||
fi
|
||||
|
||||
sync_python_dependencies_if_needed
|
||||
|
||||
log "构建前端"
|
||||
cd "${ROOT_DIR}/frontend/app"
|
||||
npm_bin="$(resolve_npm_bin)"
|
||||
export PATH="$(dirname "${npm_bin}"):${PATH}"
|
||||
"${npm_bin}" run build
|
||||
|
||||
log "重启 ${SERVICE_SCOPE} 服务"
|
||||
systemctl_for_scope restart "${BACKEND_SERVICE}" "${FRONTEND_SERVICE}"
|
||||
|
||||
log "健康检查"
|
||||
sleep 1
|
||||
systemctl_for_scope --no-pager --lines=0 status "${BACKEND_SERVICE}" "${FRONTEND_SERVICE}"
|
||||
|
||||
echo
|
||||
echo "快速更新完成。"
|
||||
Executable
+59
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
project_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "${project_dir}"
|
||||
|
||||
project_name="k1412-agent-e2e"
|
||||
web_port="${E2E_WEB_PORT:-3000}"
|
||||
compose=(docker compose -p "${project_name}" -f compose.yaml -f compose.e2e.yaml)
|
||||
|
||||
export WEB_PORT="${web_port}"
|
||||
export WEBUI_URL="http://127.0.0.1:${web_port}"
|
||||
export CORS_ALLOW_ORIGIN="${WEBUI_URL}"
|
||||
export WEBUI_SECRET_KEY="e2e-webui-secret-at-least-32-bytes"
|
||||
export WEBUI_ADMIN_EMAIL="admin-e2e@example.invalid"
|
||||
export WEBUI_ADMIN_PASSWORD="e2e-admin-password"
|
||||
export OPENWEBUI_FORWARD_JWT_SECRET="e2e-forward-jwt-secret-at-least-32-bytes"
|
||||
export INTERNAL_PROVIDER_KEY="e2e-provider-secret-at-least-32-bytes"
|
||||
export INTERNAL_GATEWAY_KEY="e2e-gateway-secret-at-least-32-bytes"
|
||||
export MODEL_API_KEY="e2e-model-key"
|
||||
export POSTGRES_PASSWORD="e2e-postgres-password"
|
||||
export WEB_IMAGE="k1412-agent-web:e2e"
|
||||
export RUNTIME_IMAGE="k1412-agent-runtime:e2e"
|
||||
export GATEWAY_IMAGE="k1412-agent-gateway:e2e"
|
||||
export WORKSPACE_IMAGE="k1412-agent-workspace:e2e"
|
||||
export E2E_BASE_URL="${WEBUI_URL}"
|
||||
export E2E_ADMIN_EMAIL="${WEBUI_ADMIN_EMAIL}"
|
||||
export E2E_ADMIN_PASSWORD="${WEBUI_ADMIN_PASSWORD}"
|
||||
export E2E_FORWARD_JWT_SECRET="${OPENWEBUI_FORWARD_JWT_SECRET}"
|
||||
export E2E_INTERNAL_PROVIDER_KEY="${INTERNAL_PROVIDER_KEY}"
|
||||
|
||||
cleanup() {
|
||||
if [[ "${E2E_KEEP_STACK:-0}" != "1" ]]; then
|
||||
"${compose[@]}" down --volumes --remove-orphans
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
"${compose[@]}" down --volumes --remove-orphans
|
||||
"${compose[@]}" --profile build-only build workspace-image
|
||||
"${compose[@]}" up --detach --build
|
||||
|
||||
for _ in $(seq 1 120); do
|
||||
web_health="$("${compose[@]}" ps --format json web 2>/dev/null || true)"
|
||||
bootstrap_state="$("${compose[@]}" ps --all --format json bootstrap 2>/dev/null || true)"
|
||||
if [[ "${web_health}" == *'"Health":"healthy"'* \
|
||||
&& "${bootstrap_state}" == *'"State":"exited"'* \
|
||||
&& "${bootstrap_state}" == *'"ExitCode":0'* ]]; then
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
"${compose[@]}" ps
|
||||
bootstrap_state="$("${compose[@]}" ps --all --format json bootstrap)"
|
||||
[[ "${bootstrap_state}" == *'"State":"exited"'* ]]
|
||||
[[ "${bootstrap_state}" == *'"ExitCode":0'* ]]
|
||||
curl --silent --show-error --fail "${WEBUI_URL}/health" >/dev/null
|
||||
.venv/bin/python e2e/verify_stack.py
|
||||
Executable
+39
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
project_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "${project_dir}"
|
||||
|
||||
python3 -m compileall -q agent_platform
|
||||
|
||||
if [[ ! -x .venv/bin/pytest ]]; then
|
||||
python3 -m venv .venv
|
||||
.venv/bin/python -m pip install -q --upgrade pip==26.1.2
|
||||
.venv/bin/pip install -q -e '.[dev]'
|
||||
fi
|
||||
|
||||
.venv/bin/ruff check agent_platform tests e2e
|
||||
.venv/bin/ruff format --check agent_platform tests e2e
|
||||
.venv/bin/pytest
|
||||
.venv/bin/pip-audit
|
||||
|
||||
export WEBUI_SECRET_KEY=verify-webui-secret
|
||||
export WEBUI_ADMIN_EMAIL=admin@example.invalid
|
||||
export WEBUI_ADMIN_PASSWORD=verify-admin-password
|
||||
export OPENWEBUI_FORWARD_JWT_SECRET=verify-forward-jwt-secret-at-least-32-bytes
|
||||
export INTERNAL_PROVIDER_KEY=verify-provider-secret-at-least-32-bytes
|
||||
export INTERNAL_GATEWAY_KEY=verify-gateway-secret-at-least-32-bytes
|
||||
export MODEL_API_KEY=verify-model-key
|
||||
export POSTGRES_PASSWORD=verify-postgres-password
|
||||
export WEB_IMAGE=docker.k1412.top/wuyang/k1412-agent-web:verify
|
||||
export RUNTIME_IMAGE=docker.k1412.top/wuyang/k1412-agent-runtime:verify
|
||||
export GATEWAY_IMAGE=docker.k1412.top/wuyang/k1412-agent-gateway:verify
|
||||
export WORKSPACE_IMAGE=k1412-agent-workspace:test
|
||||
|
||||
docker compose config --quiet
|
||||
docker compose \
|
||||
-f deploy/docker-compose.yml \
|
||||
-f deploy/docker-compose.override.yml \
|
||||
config --quiet
|
||||
docker build -f docker/workspace.Dockerfile -t k1412-agent-workspace:test .
|
||||
RUN_DOCKER_INTEGRATION=1 .venv/bin/pytest tests/test_docker_workspace.py
|
||||
Reference in New Issue
Block a user