mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-22 17:36:59 +08:00
feat(fnos): run entirely without root and harden process lifecycle
- Switch privilege model to run-as: package (zero root, per fnOS guide) - Borrow App Store python312 instead of bundling CPython; keep bundled uv - Move venv, HOME and caches onto the persistent data share - Start service via setsid; stop/upgrade kill the whole process group - Sweep stray runtime/box orphans in install/upgrade init hooks - Track LangBot 4.10.11 (manifest baseline + upstream merge)
This commit is contained in:
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
This directory packages LangBot as a `.fpk` app for the fnOS App Store. It is a native deployment: no Docker involved — uv creates a Python virtual environment directly on the NAS, and Node.js v22 from the fnOS App Store provides the Box sandbox and npx MCP capabilities.
|
This directory packages LangBot as a `.fpk` app for the fnOS App Store. It is a native deployment: no Docker involved — uv creates a Python virtual environment directly on the NAS, and Node.js v22 from the fnOS App Store provides the Box sandbox and npx MCP capabilities.
|
||||||
|
|
||||||
|
## Privilege Model
|
||||||
|
|
||||||
|
Per the [fnOS privilege docs](https://developer.fnnas.com/docs/core-concepts/privilege/), the whole app runs as the dedicated package user (`run-as: package`; username auto-generated by fnOS from `appname`) — no root anywhere. The official App Store apps `nodejs_v22` and `python312` (declared in `manifest` via `install_dep_apps`) are borrowed from `/var/apps/` cross-app. `HOME` is pointed at `<data>/.home` inside the persistent share so tool caches (uv, npm/npx MCP) stay writable regardless of the generated user's system home.
|
||||||
|
|
||||||
## Directory Structure
|
## Directory Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -10,10 +14,10 @@ packaging/fnos/
|
|||||||
├── build.sh # One-shot build script (shared by local and CI)
|
├── build.sh # One-shot build script (shared by local and CI)
|
||||||
├── LICENSE
|
├── LICENSE
|
||||||
├── config/
|
├── config/
|
||||||
│ ├── privilege # Privilege config (run-as: root)
|
│ ├── privilege # Privilege config (run-as: package, no root)
|
||||||
│ └── resource # Persistent data share declaration (langbot/data)
|
│ └── resource # Persistent data share declaration (langbot/data)
|
||||||
├── cmd/ # Lifecycle scripts (fnOS invokes them with TRIM_* env vars)
|
├── cmd/ # Lifecycle scripts (fnOS invokes them with TRIM_* env vars)
|
||||||
│ ├── main # Service start/stop manager (start/stop/status, owns PID/log)
|
│ ├── main # Service start/stop manager (start/stop/status, owns PID/log); started via setsid, stop kills the whole process group
|
||||||
│ ├── install_init # Pre-install hook
|
│ ├── install_init # Pre-install hook
|
||||||
│ ├── install_callback # Post-install hook: create venv, uv sync deps, seed config.yaml port
|
│ ├── install_callback # Post-install hook: create venv, uv sync deps, seed config.yaml port
|
||||||
│ ├── upgrade_init # Pre-upgrade hook
|
│ ├── upgrade_init # Pre-upgrade hook
|
||||||
|
|||||||
+10
-9
@@ -73,8 +73,11 @@ rsync -a \
|
|||||||
[ -d "${FPK_DIR}/app/langbot/web/dist" ] || { echo "ERROR: web/dist missing after rsync!" >&2; exit 1; }
|
[ -d "${FPK_DIR}/app/langbot/web/dist" ] || { echo "ERROR: web/dist missing after rsync!" >&2; exit 1; }
|
||||||
echo " Source synced ($(du -sh "${FPK_DIR}/app/langbot" | cut -f1))"
|
echo " Source synced ($(du -sh "${FPK_DIR}/app/langbot" | cut -f1))"
|
||||||
|
|
||||||
# --- 2.5 Download bundled uv binaries (offline install on NAS) ---
|
# --- 2.5 Download bundled uv binary (offline install on NAS) ---
|
||||||
echo "[2.5/5] Downloading bundled uv binaries..."
|
# Python comes from the official python312 App Store app (see manifest
|
||||||
|
# install_dep_apps); only uv is carried in the package. The download is a
|
||||||
|
# hard requirement — the build fails without it (no fallback installs).
|
||||||
|
echo "[2.5/5] Downloading bundled uv binary..."
|
||||||
UV_VERSION="0.12.9"
|
UV_VERSION="0.12.9"
|
||||||
mkdir -p "${FPK_DIR}/app/bin"
|
mkdir -p "${FPK_DIR}/app/bin"
|
||||||
for arch in x86_64 aarch64; do
|
for arch in x86_64 aarch64; do
|
||||||
@@ -84,15 +87,13 @@ for arch in x86_64 aarch64; do
|
|||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
tmp="$(mktemp -d)"
|
tmp="$(mktemp -d)"
|
||||||
if curl -sSL -o "${tmp}/uv.tar.gz" \
|
curl -fsSL -o "${tmp}/uv.tar.gz" \
|
||||||
"https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-${arch}-unknown-linux-gnu.tar.gz" \
|
"https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-${arch}-unknown-linux-gnu.tar.gz" \
|
||||||
&& tar xzf "${tmp}/uv.tar.gz" -C "${tmp}" \
|
&& tar xzf "${tmp}/uv.tar.gz" -C "${tmp}" \
|
||||||
&& cp "${tmp}/uv-${arch}-unknown-linux-gnu/uv" "${out}"; then
|
&& cp "${tmp}/uv-${arch}-unknown-linux-gnu/uv" "${out}" \
|
||||||
chmod +x "${out}"
|
&& chmod +x "${out}" \
|
||||||
echo " uv-${arch} downloaded (${UV_VERSION})"
|
&& echo " uv-${arch} downloaded (${UV_VERSION})" \
|
||||||
else
|
|| { echo "ERROR: failed to download uv for ${arch}" >&2; rm -rf "${tmp}"; exit 1; }
|
||||||
echo " WARNING: failed to download uv for ${arch}, install will fall back to online install" >&2
|
|
||||||
fi
|
|
||||||
rm -rf "${tmp}"
|
rm -rf "${tmp}"
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,30 @@ cd "${APP_DIR}" || {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- Ensure data directory exists ---
|
# --- Ensure data directory exists ---
|
||||||
mkdir -p "${DATA_DIR}/plugins" "${DATA_DIR}/box" "${DATA_DIR}/logs" 2>/dev/null || true
|
mkdir -p "${DATA_DIR}/plugins" "${DATA_DIR}/box" "${DATA_DIR}/logs" || {
|
||||||
|
echo "Data share not writable: ${DATA_DIR}" > "${TRIM_TEMP_LOGFILE}"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Writable HOME and tool caches ---
|
||||||
|
# Under run-as: package the generated user's system HOME and the app install
|
||||||
|
# dir (TRIM_APPDEST) can be read-only; uv aborts if it cannot initialise its
|
||||||
|
# cache. Point HOME / UV_CACHE_DIR at the persistent data share and keep the
|
||||||
|
# venv there too (UV_PROJECT_ENVIRONMENT), instead of inside APP_DIR.
|
||||||
|
VENV_DIR="${DATA_DIR}/.venv"
|
||||||
|
export HOME="${DATA_DIR}/.home"
|
||||||
|
export UV_CACHE_DIR="${DATA_DIR}/.cache/uv"
|
||||||
|
# Never download a managed CPython (the download host is unreachable on many
|
||||||
|
# NAS networks); use the distro Python only — fail loudly if it is missing.
|
||||||
|
export UV_PYTHON_DOWNLOADS=never
|
||||||
|
mkdir -p "${HOME}" "${UV_CACHE_DIR}" || {
|
||||||
|
echo "Data share not writable: ${DATA_DIR}" > "${TRIM_TEMP_LOGFILE}"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Real uv/pip errors are captured here for diagnosis (the installer popup
|
||||||
|
# only shows the short message we write to TRIM_TEMP_LOGFILE).
|
||||||
|
DEBUG_LOG="${DATA_DIR}/logs/install-debug.log"
|
||||||
|
|
||||||
# --- Pre-seed config.yaml with the user-selected web port ---
|
# --- Pre-seed config.yaml with the user-selected web port ---
|
||||||
# Data root points at the persistent share (LANGBOT_DATA_ROOT is exported by
|
# Data root points at the persistent share (LANGBOT_DATA_ROOT is exported by
|
||||||
@@ -35,7 +58,10 @@ TEMPLATE_FILE="${APP_DIR}/src/langbot/templates/config.yaml"
|
|||||||
_patch_config() {
|
_patch_config() {
|
||||||
local cfg_dir="$1"
|
local cfg_dir="$1"
|
||||||
local cfg_file="${cfg_dir}/config.yaml"
|
local cfg_file="${cfg_dir}/config.yaml"
|
||||||
mkdir -p "${cfg_dir}" 2>/dev/null || true
|
mkdir -p "${cfg_dir}" || {
|
||||||
|
echo "Cannot create config directory: ${cfg_dir}" > "${TRIM_TEMP_LOGFILE}"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
if [ ! -f "${cfg_file}" ] && [ -f "${TEMPLATE_FILE}" ]; then
|
if [ ! -f "${cfg_file}" ] && [ -f "${TEMPLATE_FILE}" ]; then
|
||||||
cp "${TEMPLATE_FILE}" "${cfg_file}"
|
cp "${TEMPLATE_FILE}" "${cfg_file}"
|
||||||
fi
|
fi
|
||||||
@@ -68,15 +94,27 @@ if [ ! -d "/var/apps/nodejs_v${NODE_VERSION}" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- Python check ---
|
# --- CPU architecture (must be resolved before locating bundled binaries) ---
|
||||||
PYTHON_BIN="python3"
|
ARCH=$(uname -m)
|
||||||
if ! command -v "${PYTHON_BIN}" >/dev/null 2>&1; then
|
case "${ARCH}" in
|
||||||
PYTHON_BIN="python"
|
x86_64|aarch64) ;;
|
||||||
fi
|
*)
|
||||||
if ! command -v "${PYTHON_BIN}" >/dev/null 2>&1; then
|
echo "Unsupported CPU architecture: ${ARCH}" > "${TRIM_TEMP_LOGFILE}"
|
||||||
echo "Python not found on this system" > "${TRIM_TEMP_LOGFILE}"
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# --- Python: official python312 App Store app (same borrow pattern as Node.js) ---
|
||||||
|
PYTHON_APP="python312"
|
||||||
|
PYTHON_BIN="/var/apps/${PYTHON_APP}/target/bin/python3"
|
||||||
|
if [ ! -d "/var/apps/${PYTHON_APP}" ]; then
|
||||||
|
echo "未找到官方 Python 环境:请先在应用中心安装 ${PYTHON_APP},再重新安装本应用。" > "${TRIM_TEMP_LOGFILE}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
[ -x "${PYTHON_BIN}" ] || {
|
||||||
|
echo "Python 解释器缺失或不可执行:${PYTHON_BIN}" > "${TRIM_TEMP_LOGFILE}"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
PY_VER=$("${PYTHON_BIN}" -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null)
|
PY_VER=$("${PYTHON_BIN}" -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null)
|
||||||
if [ -z "${PY_VER}" ]; then
|
if [ -z "${PY_VER}" ]; then
|
||||||
@@ -90,55 +128,32 @@ if [ "${PY_MAJOR}" -lt 3 ] || { [ "${PY_MAJOR}" -eq 3 ] && [ "${PY_MINOR}" -lt 1
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- Resolve uv: bundled binary first, then online fallbacks ---
|
# --- Resolve uv: run the bundled binary in place (single canonical path) ---
|
||||||
UV_BIN=""
|
UV_BIN="${TRIM_APPDEST}/bin/uv-${ARCH}"
|
||||||
ARCH=$(uname -m)
|
[ -x "${UV_BIN}" ] || {
|
||||||
case "${ARCH}" in
|
echo "Bundled uv binary missing or not executable: ${UV_BIN}" > "${TRIM_TEMP_LOGFILE}"
|
||||||
x86_64) BUNDLED_UV="${TRIM_APPDEST}/bin/uv-x86_64" ;;
|
|
||||||
aarch64) BUNDLED_UV="${TRIM_APPDEST}/bin/uv-aarch64" ;;
|
|
||||||
*) BUNDLED_UV="" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
if [ -n "${BUNDLED_UV}" ] && [ -x "${BUNDLED_UV}" ]; then
|
|
||||||
mkdir -p "${TRIM_PKGVAR}/bin"
|
|
||||||
cp "${BUNDLED_UV}" "${TRIM_PKGVAR}/bin/uv" && chmod +x "${TRIM_PKGVAR}/bin/uv"
|
|
||||||
UV_BIN="${TRIM_PKGVAR}/bin/uv"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "${UV_BIN}" ] && command -v uv >/dev/null 2>&1; then
|
|
||||||
UV_BIN="uv"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "${UV_BIN}" ]; then
|
|
||||||
"${PYTHON_BIN}" -m pip install --user --no-cache-dir uv 2>/dev/null || \
|
|
||||||
"${PYTHON_BIN}" -m pip install --no-cache-dir uv 2>/dev/null || \
|
|
||||||
curl -LsSf https://astral.sh/uv/install.sh | sh 2>/dev/null || true
|
|
||||||
export PATH="${HOME}/.local/bin:${PATH}"
|
|
||||||
if command -v uv >/dev/null 2>&1; then
|
|
||||||
UV_BIN="uv"
|
|
||||||
elif [ -x "${HOME}/.local/bin/uv" ]; then
|
|
||||||
UV_BIN="${HOME}/.local/bin/uv"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "${UV_BIN}" ]; then
|
|
||||||
echo "无法获取 uv:内置二进制缺失且在线安装失败。请检查网络后重新安装。" > "${TRIM_TEMP_LOGFILE}"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
}
|
||||||
|
|
||||||
# --- Create venv via uv ---
|
# --- Create venv via uv (on the writable data share, not APP_DIR) ---
|
||||||
if [ ! -d ".venv" ]; then
|
if [ ! -d "${VENV_DIR}" ]; then
|
||||||
"${UV_BIN}" venv .venv --python "${PYTHON_BIN}" || {
|
{
|
||||||
echo "Failed to create Python virtual environment via uv" > "${TRIM_TEMP_LOGFILE}"
|
echo "== whoami: $(id 2>&1)"
|
||||||
|
echo "== APP_DIR perms: $(ls -ld "${APP_DIR}" 2>&1)"
|
||||||
|
echo "== DATA_DIR perms: $(ls -ld "${DATA_DIR}" 2>&1)"
|
||||||
|
echo "== HOME=${HOME} UV_CACHE_DIR=${UV_CACHE_DIR}"
|
||||||
|
"${UV_BIN}" venv "${VENV_DIR}" --python "${PYTHON_BIN}"
|
||||||
|
} >> "${DEBUG_LOG}" 2>&1 || {
|
||||||
|
echo "Failed to create Python virtual environment via uv. See ${DEBUG_LOG}" > "${TRIM_TEMP_LOGFILE}"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- Sync dependencies ---
|
# --- Sync dependencies (install into the relocated venv) ---
|
||||||
"${UV_BIN}" sync --extra seekdb || {
|
if ! UV_PROJECT_ENVIRONMENT="${VENV_DIR}" "${UV_BIN}" sync --extra seekdb >> "${DEBUG_LOG}" 2>&1; then
|
||||||
echo "Dependency sync failed. Check network connectivity." > "${TRIM_TEMP_LOGFILE}"
|
echo "Dependency sync failed. Check network connectivity. See ${DEBUG_LOG}" > "${TRIM_TEMP_LOGFILE}"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
fi
|
||||||
|
|
||||||
# --- Verify frontend dist ---
|
# --- Verify frontend dist ---
|
||||||
if [ ! -d "web/dist" ] || [ -z "$(ls -A web/dist 2>/dev/null)" ]; then
|
if [ ! -d "web/dist" ] || [ -z "$(ls -A web/dist 2>/dev/null)" ]; then
|
||||||
@@ -146,4 +161,9 @@ if [ ! -d "web/dist" ] || [ -z "$(ls -A web/dist 2>/dev/null)" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# --- Prepare a writable HOME inside the data dir for tool caches (uv,
|
||||||
|
# npm/npx MCP). Everything already runs as the package user (run-as:
|
||||||
|
# package), so no chown is needed — files created here belong to it.
|
||||||
|
mkdir -p "${DATA_DIR}/.home" 2>/dev/null || true
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# cmd/install_init - pre-install hook
|
# cmd/install_init - pre-install hook (runs before files are applied)
|
||||||
# Nothing special to do before extraction.
|
# Sweep stray processes from a previous failed/killed install: orphans whose
|
||||||
|
# parent was hard-killed keep running and hold the runtime/box ws ports,
|
||||||
|
# which breaks the new instance. No match is the normal case on a clean
|
||||||
|
# install — pkill exits 1.
|
||||||
|
|
||||||
|
RUN_USER=$(id -un)
|
||||||
|
pkill -KILL -u "${RUN_USER}" -f "appcenter/langbot" 2>/dev/null || true
|
||||||
|
pkill -KILL -u "${RUN_USER}" -f "appshare/langbot" 2>/dev/null || true
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
+69
-24
@@ -27,27 +27,48 @@ if [ -z "${DATA_DIR}" ]; then
|
|||||||
DATA_DIR="${TRIM_PKGVAR}/data"
|
DATA_DIR="${TRIM_PKGVAR}/data"
|
||||||
fi
|
fi
|
||||||
export LANGBOT_DATA_ROOT="${DATA_DIR}"
|
export LANGBOT_DATA_ROOT="${DATA_DIR}"
|
||||||
mkdir -p "${DATA_DIR}" 2>/dev/null || true
|
mkdir -p "${DATA_DIR}" || {
|
||||||
|
echo "Data share not writable: ${DATA_DIR}" > "${TRIM_TEMP_LOGFILE}"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
# --- Locate Python ---
|
# --- Writable HOME / tool caches / venv on the data share ---
|
||||||
PYTHON_BIN="python3"
|
# Must match cmd/install_callback: under run-as: package neither the system
|
||||||
! command -v "${PYTHON_BIN}" >/dev/null 2>&1 && PYTHON_BIN="python"
|
# HOME nor TRIM_APPDEST are guaranteed writable, and the venv lives at
|
||||||
|
# ${DATA_DIR}/.venv instead of inside the app dir.
|
||||||
|
export HOME="${DATA_DIR}/.home"
|
||||||
|
export UV_CACHE_DIR="${DATA_DIR}/.cache/uv"
|
||||||
|
# Never download a managed CPython — distro Python only (see install_callback)
|
||||||
|
export UV_PYTHON_DOWNLOADS=never
|
||||||
|
export UV_PROJECT_ENVIRONMENT="${DATA_DIR}/.venv"
|
||||||
|
mkdir -p "${HOME}" "${UV_CACHE_DIR}" || {
|
||||||
|
echo "Data share not writable: ${DATA_DIR}" > "${TRIM_TEMP_LOGFILE}"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
# --- Locate uv ---
|
# --- CPU architecture (must be resolved before locating bundled binaries) ---
|
||||||
# install_callback puts the bundled uv binary at ${TRIM_PKGVAR}/bin/uv
|
ARCH=$(uname -m)
|
||||||
UV_BIN="${TRIM_PKGVAR}/bin/uv"
|
case "${ARCH}" in
|
||||||
if [ ! -x "${UV_BIN}" ]; then
|
x86_64|aarch64) ;;
|
||||||
UV_BIN="uv"
|
*)
|
||||||
fi
|
echo "Unsupported CPU architecture: ${ARCH}" > "${TRIM_TEMP_LOGFILE}"
|
||||||
if ! command -v "${UV_BIN}" >/dev/null 2>&1; then
|
exit 1
|
||||||
UV_BIN="${HOME}/.local/bin/uv"
|
;;
|
||||||
fi
|
esac
|
||||||
if ! command -v "${UV_BIN}" >/dev/null 2>&1 && [ ! -x "${UV_BIN}" ]; then
|
|
||||||
UV_BIN="${HOME}/.cargo/bin/uv"
|
# --- Locate Python: official python312 App Store app (same borrow pattern as Node.js) ---
|
||||||
fi
|
PYTHON_BIN="/var/apps/python312/target/bin/python3"
|
||||||
if ! command -v "${UV_BIN}" >/dev/null 2>&1 && [ ! -x "${UV_BIN}" ]; then
|
[ -x "${PYTHON_BIN}" ] || {
|
||||||
UV_BIN="${APP_DIR}/.venv/bin/uv"
|
echo "Python interpreter missing or not executable: ${PYTHON_BIN} (install the python312 app)" > "${TRIM_TEMP_LOGFILE}"
|
||||||
fi
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Locate uv: bundled binary at its single canonical path in the app dir ---
|
||||||
|
UV_BIN="${TRIM_APPDEST}/bin/uv-${ARCH}"
|
||||||
|
[ -x "${UV_BIN}" ] || {
|
||||||
|
echo "Bundled uv binary missing or not executable: ${UV_BIN}" > "${TRIM_TEMP_LOGFILE}"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
case $1 in
|
case $1 in
|
||||||
start)
|
start)
|
||||||
@@ -69,7 +90,7 @@ case $1 in
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if [ ! -d ".venv" ]; then
|
if [ ! -d "${DATA_DIR}/.venv" ]; then
|
||||||
echo "Python virtual environment not found. Please reinstall LangBot." > "${TRIM_TEMP_LOGFILE}"
|
echo "Python virtual environment not found. Please reinstall LangBot." > "${TRIM_TEMP_LOGFILE}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -79,6 +100,9 @@ case $1 in
|
|||||||
# fresh data/ with a default 5300 config gets recreated inside target/ on
|
# fresh data/ with a default 5300 config gets recreated inside target/ on
|
||||||
# every install/upgrade. The symlink keeps everything on the persistent
|
# every install/upgrade. The symlink keeps everything on the persistent
|
||||||
# share; it is recreated here on each start (upgrades wipe target/).
|
# share; it is recreated here on each start (upgrades wipe target/).
|
||||||
|
# Requires the package user to have write permission on the app dir — if
|
||||||
|
# fnOS ever mounts it read-only, this fails loudly instead of silently
|
||||||
|
# running against an ephemeral data directory.
|
||||||
APP_DATA="${APP_DIR}/data"
|
APP_DATA="${APP_DIR}/data"
|
||||||
if [ -L "${APP_DATA}" ]; then
|
if [ -L "${APP_DATA}" ]; then
|
||||||
# already a symlink; re-point if the persistent dir changed
|
# already a symlink; re-point if the persistent dir changed
|
||||||
@@ -87,7 +111,7 @@ case $1 in
|
|||||||
# legacy real dir (created by LangBot before this fix): merge into the
|
# legacy real dir (created by LangBot before this fix): merge into the
|
||||||
# persistent dir without overwriting newer files already there
|
# persistent dir without overwriting newer files already there
|
||||||
mkdir -p "${DATA_DIR}"
|
mkdir -p "${DATA_DIR}"
|
||||||
cp -an "${APP_DATA}/." "${DATA_DIR}/" 2>/dev/null || cp -a "${APP_DATA}/." "${DATA_DIR}/"
|
cp -an "${APP_DATA}/." "${DATA_DIR}/." || cp -a "${APP_DATA}/." "${DATA_DIR}/"
|
||||||
rm -rf "${APP_DATA}"
|
rm -rf "${APP_DATA}"
|
||||||
ln -s "${DATA_DIR}" "${APP_DATA}"
|
ln -s "${DATA_DIR}" "${APP_DATA}"
|
||||||
else
|
else
|
||||||
@@ -146,7 +170,22 @@ except Exception:
|
|||||||
# (--standalone-runtime would require an external runtime at
|
# (--standalone-runtime would require an external runtime at
|
||||||
# ws://langbot_plugin_runtime:5400, which only exists in Docker Compose.)
|
# ws://langbot_plugin_runtime:5400, which only exists in Docker Compose.)
|
||||||
# --standalone-box omitted: Box sandbox defaults off, users enable via Web UI
|
# --standalone-box omitted: Box sandbox defaults off, users enable via Web UI
|
||||||
nohup "${UV_BIN}" run --no-sync main.py \
|
#
|
||||||
|
# Privilege model: the whole app runs as the generated package user
|
||||||
|
# (run-as: package, see config/privilege) — no root anywhere. HOME /
|
||||||
|
# UV_CACHE_DIR / UV_PROJECT_ENVIRONMENT are exported at the top and point
|
||||||
|
# at the persistent share so tool caches (uv, npm/npx) and the relocated
|
||||||
|
# venv stay writable regardless of the generated user's system home.
|
||||||
|
#
|
||||||
|
# Process-group lifecycle: setsid makes the main process a session/group
|
||||||
|
# leader, so PID == PGID. "stop" kills the whole group — stdio children
|
||||||
|
# (plugin runtime, Box) die with the parent and can never survive as
|
||||||
|
# orphans holding their ws ports after a crash, stop or upgrade.
|
||||||
|
command -v setsid >/dev/null 2>&1 || {
|
||||||
|
echo "setsid not found (util-linux required for process-group lifecycle)" > "${TRIM_TEMP_LOGFILE}"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
setsid nohup "${UV_BIN}" run --no-sync main.py \
|
||||||
> "${LOG_FILE}" 2>&1 &
|
> "${LOG_FILE}" 2>&1 &
|
||||||
echo $! > "${PID_FILE}"
|
echo $! > "${PID_FILE}"
|
||||||
|
|
||||||
@@ -165,12 +204,18 @@ except Exception:
|
|||||||
if [ -f "${PID_FILE}" ]; then
|
if [ -f "${PID_FILE}" ]; then
|
||||||
PID=$(cat "${PID_FILE}" | tr -d '[:space:]')
|
PID=$(cat "${PID_FILE}" | tr -d '[:space:]')
|
||||||
if [ -n "${PID}" ]; then
|
if [ -n "${PID}" ]; then
|
||||||
kill "${PID}" 2>/dev/null
|
# Started with setsid, so PID == PGID: kill the whole group so
|
||||||
|
# stdio children (plugin runtime, Box) die with the parent. The
|
||||||
|
# plain-PID kill covers instances started before the setsid
|
||||||
|
# change (group kill is a no-op for them).
|
||||||
|
kill -TERM -- "-${PID}" 2>/dev/null
|
||||||
|
kill -TERM "${PID}" 2>/dev/null
|
||||||
for _ in 1 2 3 4 5 6 7 8 9 10; do
|
for _ in 1 2 3 4 5 6 7 8 9 10; do
|
||||||
kill -0 "${PID}" 2>/dev/null || break
|
kill -0 "${PID}" 2>/dev/null || break
|
||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
kill -9 "${PID}" 2>/dev/null
|
kill -KILL -- "-${PID}" 2>/dev/null
|
||||||
|
kill -KILL "${PID}" 2>/dev/null
|
||||||
fi
|
fi
|
||||||
rm -f "${PID_FILE}"
|
rm -f "${PID_FILE}"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ if [ "${wizard_keep_data:-yes}" = "no" ]; then
|
|||||||
# 应用运行数据(pid、日志等)
|
# 应用运行数据(pid、日志等)
|
||||||
if [ -n "${TRIM_PKGVAR}" ]; then
|
if [ -n "${TRIM_PKGVAR}" ]; then
|
||||||
rm -rf "${TRIM_PKGVAR:?}"/langbot.pid \
|
rm -rf "${TRIM_PKGVAR:?}"/langbot.pid \
|
||||||
"${TRIM_PKGVAR:?}"/langbot.log \
|
"${TRIM_PKGVAR:?}"/langbot.log 2>/dev/null || true
|
||||||
"${TRIM_PKGVAR:?}"/bin 2>/dev/null || true
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 共享数据目录(langbot/data)
|
# 共享数据目录(langbot/data)
|
||||||
|
|||||||
@@ -8,6 +8,20 @@ APP_DIR="${TRIM_APPDEST}/langbot"
|
|||||||
DATA_DIR="${TRIM_DATA_SHARE_PATHS%%:*}"
|
DATA_DIR="${TRIM_DATA_SHARE_PATHS%%:*}"
|
||||||
[ -z "${DATA_DIR}" ] && DATA_DIR="${TRIM_PKGVAR}/data"
|
[ -z "${DATA_DIR}" ] && DATA_DIR="${TRIM_PKGVAR}/data"
|
||||||
|
|
||||||
|
# Writable HOME / caches / venv on the data share (must match
|
||||||
|
# cmd/install_callback — venv is relocated under DATA_DIR).
|
||||||
|
VENV_DIR="${DATA_DIR}/.venv"
|
||||||
|
export HOME="${DATA_DIR}/.home"
|
||||||
|
export UV_CACHE_DIR="${DATA_DIR}/.cache/uv"
|
||||||
|
# Never download a managed CPython — distro Python only (see install_callback)
|
||||||
|
export UV_PYTHON_DOWNLOADS=never
|
||||||
|
export UV_PROJECT_ENVIRONMENT="${VENV_DIR}"
|
||||||
|
mkdir -p "${HOME}" "${UV_CACHE_DIR}" "${DATA_DIR}/logs" || {
|
||||||
|
echo "Data share not writable: ${DATA_DIR}" > "${TRIM_TEMP_LOGFILE}"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
DEBUG_LOG="${DATA_DIR}/logs/upgrade-debug.log"
|
||||||
|
|
||||||
# Apply port from upgrade wizard (config persists across upgrades; this
|
# Apply port from upgrade wizard (config persists across upgrades; this
|
||||||
# only rewrites it when the user changed the value in the upgrade wizard)
|
# only rewrites it when the user changed the value in the upgrade wizard)
|
||||||
CONFIG_FILE="${DATA_DIR}/config.yaml"
|
CONFIG_FILE="${DATA_DIR}/config.yaml"
|
||||||
@@ -26,52 +40,56 @@ cd "${APP_DIR}" || {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Find uv (bundled first, then PATH / ~/.local/bin / ~/.cargo/bin)
|
# --- CPU architecture (must be resolved before locating bundled binaries) ---
|
||||||
UV_BIN="${TRIM_PKGVAR}/bin/uv"
|
ARCH=$(uname -m)
|
||||||
if [ ! -x "${UV_BIN}" ]; then
|
case "${ARCH}" in
|
||||||
UV_BIN="uv"
|
x86_64|aarch64) ;;
|
||||||
fi
|
*)
|
||||||
if ! command -v "${UV_BIN}" >/dev/null 2>&1; then
|
echo "Unsupported CPU architecture: ${ARCH}" > "${TRIM_TEMP_LOGFILE}"
|
||||||
UV_BIN="${HOME}/.local/bin/uv"
|
exit 1
|
||||||
fi
|
;;
|
||||||
if ! command -v "${UV_BIN}" >/dev/null 2>&1 && [ ! -x "${UV_BIN}" ]; then
|
esac
|
||||||
UV_BIN="${HOME}/.cargo/bin/uv"
|
|
||||||
fi
|
|
||||||
|
|
||||||
PYTHON_BIN="python3"
|
# uv: bundled binary at its single canonical path in the app dir
|
||||||
! command -v "${PYTHON_BIN}" >/dev/null 2>&1 && PYTHON_BIN="python"
|
UV_BIN="${TRIM_APPDEST}/bin/uv-${ARCH}"
|
||||||
|
[ -x "${UV_BIN}" ] || {
|
||||||
|
echo "Bundled uv binary missing or not executable: ${UV_BIN}" > "${TRIM_TEMP_LOGFILE}"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
# Re-sync deps
|
# Python: official python312 App Store app (same borrow pattern as Node.js)
|
||||||
if [ -d ".venv" ]; then
|
PYTHON_BIN="/var/apps/python312/target/bin/python3"
|
||||||
"${UV_BIN}" sync --extra seekdb 2>/dev/null || {
|
[ -x "${PYTHON_BIN}" ] || {
|
||||||
echo "Dependency sync failed after upgrade" > "${TRIM_TEMP_LOGFILE}"
|
echo "Python interpreter missing or not executable: ${PYTHON_BIN} (install the python312 app)" > "${TRIM_TEMP_LOGFILE}"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Re-sync deps (venv lives at ${DATA_DIR}/.venv, see install_callback)
|
||||||
|
if [ -d "${VENV_DIR}" ]; then
|
||||||
|
UV_PROJECT_ENVIRONMENT="${VENV_DIR}" "${UV_BIN}" sync --extra seekdb >> "${DEBUG_LOG}" 2>&1 || {
|
||||||
|
echo "Dependency sync failed after upgrade. See ${DEBUG_LOG}" > "${TRIM_TEMP_LOGFILE}"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
# Venv was lost, recreate via uv
|
# Venv was lost, recreate it with the bundled uv on the data share
|
||||||
if ! command -v "${UV_BIN}" >/dev/null 2>&1 && [ ! -x "${UV_BIN}" ]; then
|
"${UV_BIN}" venv "${VENV_DIR}" --python "${PYTHON_BIN}" >> "${DEBUG_LOG}" 2>&1 || {
|
||||||
"${PYTHON_BIN}" -m pip install --user --no-cache-dir uv 2>/dev/null || \
|
echo "Failed to recreate virtual environment. See ${DEBUG_LOG}" > "${TRIM_TEMP_LOGFILE}"
|
||||||
"${PYTHON_BIN}" -m pip install --no-cache-dir uv 2>/dev/null || {
|
|
||||||
echo "Failed to install uv" > "${TRIM_TEMP_LOGFILE}"
|
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
export PATH="${HOME}/.local/bin:${PATH}"
|
UV_PROJECT_ENVIRONMENT="${VENV_DIR}" "${UV_BIN}" sync --extra seekdb >> "${DEBUG_LOG}" 2>&1 || {
|
||||||
UV_BIN="uv"
|
echo "Dependency sync failed. See ${DEBUG_LOG}" > "${TRIM_TEMP_LOGFILE}"
|
||||||
fi
|
|
||||||
"${UV_BIN}" venv .venv --python "${PYTHON_BIN}" || {
|
|
||||||
echo "Failed to recreate virtual environment" > "${TRIM_TEMP_LOGFILE}"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
"${UV_BIN}" sync --extra seekdb || {
|
|
||||||
echo "Dependency sync failed" > "${TRIM_TEMP_LOGFILE}"
|
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Verify frontend dist still present
|
# Verify frontend dist still present
|
||||||
if [ ! -d "web/dist" ] || [ -z "$(ls -A web/dist 2>/dev/null)" ]; then
|
if [ ! -d "web/dist" ] || [ -z "$(ls -A web/dist 2>/dev/null)" ]; then
|
||||||
echo "Frontend dist missing after upgrade! Web UI will not be available." > "${TRIM_TEMP_LOGFILE}"
|
echo "Frontend dist missing! Web UI will not be available." > "${TRIM_TEMP_LOGFILE}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Everything runs as the package user (run-as: package); the venv recreated
|
||||||
|
# above and the config rewritten via sed already belong to it. Cache HOME is
|
||||||
|
# prepared at the top of this script (see cmd/install_callback).
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
@@ -1,20 +1,36 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# cmd/upgrade_init - pre-upgrade hook
|
# cmd/upgrade_init - pre-upgrade hook (runs before files are replaced)
|
||||||
# Stop the running LangBot process before files are replaced.
|
# 1. Stop the running LangBot instance — whole process group, so stdio
|
||||||
|
# children (plugin runtime, Box) die with the parent.
|
||||||
|
# 2. Sweep stray processes from a previous crash/upgrade: orphans whose
|
||||||
|
# parent was hard-killed keep running and hold the runtime/box ws ports,
|
||||||
|
# which breaks the next start.
|
||||||
|
|
||||||
PID_FILE="${TRIM_PKGVAR}/langbot.pid"
|
PID_FILE="${TRIM_PKGVAR}/langbot.pid"
|
||||||
|
|
||||||
if [ -f "${PID_FILE}" ]; then
|
if [ -f "${PID_FILE}" ]; then
|
||||||
PID=$(cat "${PID_FILE}" | tr -d '[:space:]')
|
PID=$(cat "${PID_FILE}" | tr -d '[:space:]')
|
||||||
if [ -n "${PID}" ] && kill -0 "${PID}" 2>/dev/null; then
|
if [ -n "${PID}" ] && kill -0 "${PID}" 2>/dev/null; then
|
||||||
kill "${PID}" 2>/dev/null
|
# Instances started with setsid have PID == PGID; the plain-PID kill
|
||||||
|
# covers instances started before that change.
|
||||||
|
kill -TERM -- "-${PID}" 2>/dev/null
|
||||||
|
kill -TERM "${PID}" 2>/dev/null
|
||||||
for _ in 1 2 3 4 5 6 7 8 9 10; do
|
for _ in 1 2 3 4 5 6 7 8 9 10; do
|
||||||
kill -0 "${PID}" 2>/dev/null || break
|
kill -0 "${PID}" 2>/dev/null || break
|
||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
kill -9 "${PID}" 2>/dev/null
|
kill -KILL -- "-${PID}" 2>/dev/null
|
||||||
|
kill -KILL "${PID}" 2>/dev/null
|
||||||
fi
|
fi
|
||||||
rm -f "${PID_FILE}"
|
rm -f "${PID_FILE}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Stray sweep: match only our volume paths (venv/uv under the app dir on
|
||||||
|
# @appcenter, venv/HOME/caches under the data share on @appshare). This
|
||||||
|
# script itself runs from /var/apps/<appname>/cmd/, so it never matches
|
||||||
|
# itself. No match is the normal case on a clean upgrade — pkill exits 1.
|
||||||
|
RUN_USER=$(id -un)
|
||||||
|
pkill -KILL -u "${RUN_USER}" -f "appcenter/langbot" 2>/dev/null || true
|
||||||
|
pkill -KILL -u "${RUN_USER}" -f "appshare/langbot" 2>/dev/null || true
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"run-as": "root"
|
"run-as": "package"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
appname=langbot
|
appname=langbot
|
||||||
version=4.10.10
|
version=4.10.11
|
||||||
display_name=LangBot
|
display_name=LangBot
|
||||||
desc=基于 LLM 的多平台智能对话机器人,支持 QQ、微信、飞书、钉钉、Telegram 等十余种即时通讯平台,内置 Web 管理界面和 AI Agent 能力。
|
desc=基于 LLM 的多平台智能对话机器人,支持 QQ、微信、飞书、钉钉、Telegram 等十余种即时通讯平台,内置 Web 管理界面和 AI Agent 能力。
|
||||||
platform=all
|
platform=all
|
||||||
|
|||||||
Reference in New Issue
Block a user