Merge pull request #2552 from sheetung/fnos-privilege

Fnos privilege
This commit is contained in:
sheetung
2026-09-19 09:16:15 +08:00
committed by GitHub
12 changed files with 240 additions and 130 deletions
+6 -2
View File
@@ -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
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"title": "LangBot", "title": "LangBot",
"icon": "images/icon-256.png", "icon": "images/icon-256.png",
"type": "url", "type": "iframe",
"protocol": "http", "protocol": "http",
"port": "${wizard_port}", "port": "${wizard_port}",
"url": "/", "url": "/",
+1 -1
View File
@@ -3,7 +3,7 @@
"langbot.main": { "langbot.main": {
"title": "LangBot", "title": "LangBot",
"icon": "images/icon-{0}.png", "icon": "images/icon-{0}.png",
"type": "url", "type": "iframe",
"protocol": "http", "protocol": "http",
"port": "${wizard_port}", "port": "${wizard_port}",
"url": "/", "url": "/",
+10 -9
View File
@@ -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
+71 -51
View File
@@ -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
+9 -2
View File
@@ -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
View File
@@ -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
+1 -2
View File
@@ -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
+50 -32
View File
@@ -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
}
export PATH="${HOME}/.local/bin:${PATH}"
UV_BIN="uv"
fi
"${UV_BIN}" venv .venv --python "${PYTHON_BIN}" || {
echo "Failed to recreate virtual environment" > "${TRIM_TEMP_LOGFILE}"
exit 1 exit 1
} }
"${UV_BIN}" sync --extra seekdb || { UV_PROJECT_ENVIRONMENT="${VENV_DIR}" "${UV_BIN}" sync --extra seekdb >> "${DEBUG_LOG}" 2>&1 || {
echo "Dependency sync failed" > "${TRIM_TEMP_LOGFILE}" echo "Dependency sync failed. See ${DEBUG_LOG}" > "${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
+20 -4
View File
@@ -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 -1
View File
@@ -1,5 +1,5 @@
{ {
"defaults": { "defaults": {
"run-as": "root" "run-as": "package"
} }
} }
+1 -1
View File
@@ -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