From 60bb67f0256d10320f2ce8900bf704eb2a8d3146 Mon Sep 17 00:00:00 2001 From: sheetung <30528385+sheetung@users.noreply.github.com> Date: Mon, 14 Sep 2026 00:31:19 +0800 Subject: [PATCH] Fnos packaging (#2524) * feat(packaging): add fnOS FPK packaging and CI workflow Add packaging/fnos/ shell (manifest, lifecycle cmd scripts, install/ upgrade/uninstall wizards, desktop entry, EULA) plus in-repo build script. A release now auto-builds langbot--fnos.fpk via .github/workflows/build-fnos-fpk.yaml, uploaded to the release assets. * ci(fnos): skip release upload on manual dispatch github.event.release.tag_name is empty when triggered via workflow_dispatch, causing 'gh release upload' to fail with 'requires at least 2 arg(s)'. Restrict the step to release events. * ci(fnos): normalize release asset name Strip a trailing -fnos from the tag-derived version before appending the suffix, avoiding langbot--fnos-fnos.fpk when the tag itself already carries -fnos. * ci(fnos): use release tag version for auto build, manifest for manual Auto build (release/tag) reads version from tag like other release workflows; build.sh strips the v prefix when injecting into manifest. Manual dispatch falls back to the version maintained in manifest. * feat(fnos): add post-install deployment notice to install wizard Last wizard step now informs users that first startup takes about 5-10 minutes for dependency setup before the web UI is ready. * docs(fnos): add packaging directory README * refactor(fnos): rename app from ai.langbot to langbot Rename appname, desktop entry, data share, build artifacts, and all references from ai.langbot to langbot across packaging files. --------- Co-authored-by: dadachann <185672915+dadachann@users.noreply.github.com> --- .github/workflows/build-fnos-fpk.yaml | 78 ++++++++ .gitignore | 9 + packaging/fnos/LICENSE | 145 +++++++++++++++ packaging/fnos/README.md | 78 ++++++++ packaging/fnos/app/desktop/langbot.main.url | 9 + packaging/fnos/app/ui/config | 13 ++ packaging/fnos/build.sh | 180 ++++++++++++++++++ packaging/fnos/cmd/config_callback | 6 + packaging/fnos/cmd/config_init | 4 + packaging/fnos/cmd/install_callback | 149 +++++++++++++++ packaging/fnos/cmd/install_init | 5 + packaging/fnos/cmd/main | 194 ++++++++++++++++++++ packaging/fnos/cmd/uninstall_callback | 21 +++ packaging/fnos/cmd/uninstall_init | 20 ++ packaging/fnos/cmd/upgrade_callback | 77 ++++++++ packaging/fnos/cmd/upgrade_init | 20 ++ packaging/fnos/config/privilege | 5 + packaging/fnos/config/resource | 9 + packaging/fnos/manifest | 16 ++ packaging/fnos/wizard/install | 47 +++++ packaging/fnos/wizard/uninstall | 17 ++ packaging/fnos/wizard/upgrade | 38 ++++ 22 files changed, 1140 insertions(+) create mode 100644 .github/workflows/build-fnos-fpk.yaml create mode 100644 packaging/fnos/LICENSE create mode 100644 packaging/fnos/README.md create mode 100644 packaging/fnos/app/desktop/langbot.main.url create mode 100644 packaging/fnos/app/ui/config create mode 100644 packaging/fnos/build.sh create mode 100755 packaging/fnos/cmd/config_callback create mode 100755 packaging/fnos/cmd/config_init create mode 100755 packaging/fnos/cmd/install_callback create mode 100755 packaging/fnos/cmd/install_init create mode 100755 packaging/fnos/cmd/main create mode 100755 packaging/fnos/cmd/uninstall_callback create mode 100755 packaging/fnos/cmd/uninstall_init create mode 100755 packaging/fnos/cmd/upgrade_callback create mode 100755 packaging/fnos/cmd/upgrade_init create mode 100644 packaging/fnos/config/privilege create mode 100644 packaging/fnos/config/resource create mode 100644 packaging/fnos/manifest create mode 100644 packaging/fnos/wizard/install create mode 100644 packaging/fnos/wizard/uninstall create mode 100644 packaging/fnos/wizard/upgrade diff --git a/.github/workflows/build-fnos-fpk.yaml b/.github/workflows/build-fnos-fpk.yaml new file mode 100644 index 000000000..e24906e39 --- /dev/null +++ b/.github/workflows/build-fnos-fpk.yaml @@ -0,0 +1,78 @@ +name: Build fnOS FPK + +on: + workflow_dispatch: + ## 发布release的时候会自动构建 + release: + types: [published] + +permissions: + contents: write + +jobs: + build-fnos-fpk: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + persist-credentials: false + + - name: Check version + id: check_version + run: | + echo $GITHUB_REF + # 如果是tag,则去掉refs/tags/前缀(与其他 release workflow 一致,版本号取 tag 名) + if [[ $GITHUB_REF == refs/tags/* ]]; then + echo "It's a tag" + echo "version=$(echo $GITHUB_REF | awk -F '/' '{print $3}')" >> $GITHUB_OUTPUT + else + # 手动触发(workflow_dispatch):读不到 tag,使用 manifest 内维护的版本 + echo "It's not a tag" + echo "version=$(grep '^version=' packaging/fnos/manifest | cut -d= -f2)" >> $GITHUB_OUTPUT + fi + + - name: Setup Node + uses: actions/setup-node@v2 + with: + node-version: '22' + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install build tools + run: | + pip install pillow + # fnpack:飞牛官方打包 CLI(静态二进制) + curl -fsSL -o /usr/local/bin/fnpack \ + https://static2.fnnas.com/fnpack/fnpack-1.2.3-linux-amd64 + chmod +x /usr/local/bin/fnpack + + - name: Build FPK + env: + FPK_VERSION: ${{ steps.check_version.outputs.version }} + run: | + bash packaging/fnos/build.sh + test -f packaging/fnos/langbot.fpk + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: langbot-${{ steps.check_version.outputs.version }}-fnos + path: packaging/fnos/langbot.fpk + + - name: Upload To Release + # 仅 release 触发时执行;手动/workflow_dispatch 触发时没有 release, + # 且 github.event.release.tag_name 为空(否则 gh release upload 缺参数报错) + if: github.event_name == 'release' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # tag 可能已带 -fnos 后缀(如 v4.10.9-fnos),产物名统一为 + # langbot-<基础版本>-fnos.fpk,避免出现 -fnos-fnos + VER="${{ steps.check_version.outputs.version }}" + BASE="${VER#v}"; BASE="${BASE%-fnos}" + cp packaging/fnos/langbot.fpk "langbot-${BASE}-fnos.fpk" + gh release upload ${{ github.event.release.tag_name }} "langbot-${BASE}-fnos.fpk" diff --git a/.gitignore b/.gitignore index db632fb19..459a22c82 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,12 @@ web/.next/ web/.pnpm-home .tmp Caddyfile + +# fnOS packaging build artifacts (packaging/fnos/build.sh) +packaging/fnos/app/langbot/ +packaging/fnos/app/bin/ +packaging/fnos/ICON.PNG +packaging/fnos/ICON_256.PNG +packaging/fnos/app/ui/images/ +packaging/fnos/app/desktop/images/ +packaging/fnos/*.fpk diff --git a/packaging/fnos/LICENSE b/packaging/fnos/LICENSE new file mode 100644 index 000000000..e297f1789 --- /dev/null +++ b/packaging/fnos/LICENSE @@ -0,0 +1,145 @@ + + LangBot 用户许可协议 + User License Agreement + + 飞牛 fnOS 平台发行版 | 最后更新:2026 年 8 月 + +感谢您使用 LangBot!本协议是您(用户)与 LangBot 开源项目(以下简称「LangBot」「我们」)之间,就您在飞牛 fnOS 平台(含飞牛 NAS 设备、飞牛 OS 及其应用中心,以下简称「平台」)上安装、运行、使用 LangBot 应用所订立的合法协议。 + +您一旦在飞牛应用中心勾选「我接受许可协议的条款」并继续安装、或以其他方式运行 LangBot,即表示您已阅读、理解并同意本协议的全部内容。如您不同意本协议,请不要安装或使用本软件。 + +--- + +一、软件性质 + +1.1 LangBot 是一款基于 LLM 的多平台智能对话机器人开源软件。主程序源代码按照 Apache License, Version 2.0 公开,您可在遵守开源协议的前提下自由使用、修改与再分发。 + +1.2 本发行版系 LangBot 社区为飞牛 fnOS 平台打包构建的自托管移植版本,与飞牛官方、飞牛硬件厂商不存在从属或关联关系。飞牛应用中心提供的分发渠道不构成对软件功能、可用性的任何担保。 + +--- + +二、使用授权 + +2.1 授予您一份有限的、非排他的、不可转让的个人使用许可:您可在一台或多台您合法拥有或管理的 fnOS 设备上安装、运行本软件,用于个人、家庭或组织内部合法用途。 + +2.2 您不得: + (a) 将本软件用于违反中国大陆地区法律法规或您所在司法管辖区法律的用途; + (b) 对机器人账号进行骚扰、诈骗、批量营销、发布违法违规内容等滥用行为; + (c) 逆向工程、反编译本软件所包含的第三方二进制(uv 等),但适用法律明确允许或对应开源许可证另作规定的除外; + (d) 试图干扰、过载或损害任何由本软件对外提供的服务或其基础设施。 + +--- + +三、服务可用性与「现状」提供 + +3.1 我们努力提供稳定可靠的软件,但**不保证运行无中断或无错误**。软件可能因维护、升级、不可抗力或其他原因暂时不可用。 + +3.2 本软件按「现状」「按可用」提供。我们不作任何明示或默示担保,包括但不限于对适销性、特定用途适用性与非侵权性的默示担保。我们不保证: + (a) 软件将满足您的具体需求; + (b) 运行不间断、及时、安全或无错误; + (c) 使用获得的结果准确或可靠; + (d) 任何错误都会被修复。 + +--- + +四、责任限制 + +在适用法律允许的最大范围内: +(a) 我们不对任何**间接、附带、特殊、惩罚性或后果性损失**承担责任,包括但不限于利润损失、数据丢失、商誉损失、业务中断或其他无形损失,无论是否已被告知该等损害发生的可能性; +(b) 无论基于合同、侵权、严格责任或其他任何理论,我们就本软件所引起的所有索赔,向您承担的**累计赔偿总额**不超过 15 美元(或等值当地货币)。 + +--- + +五、用户责任 + +5.1 您对通过本软件发送的所有内容和消息(包括您所配置并运行的机器人发出的消息)承担全部责任。 + +5.2 您必须遵守所有适用的法律法规,包括但不限于数据保护、隐私、消费者保护和反垃圾邮件法律。 + +5.3 您有责任保管好自己的账号凭据和各类 API Key,并**自行对重要数据进行备份**。我们对因服务中断、账号终止或系统故障等任何原因造成的数据丢失不承担责任。 + +5.4 您不得将本软件用于任何非法活动、发送垃圾信息、骚扰他人或侵害他人合法权益。 + +5.5 您使用机器人接入任何即时通讯平台(QQ、微信、飞书、钉钉、Telegram 等)前,须自行确认已获得该平台授权并遵守其开发者协议与社区规范;因违规接入导致的账号封禁、平台处罚,由您自行承担。 + +--- + +六、数据与隐私 + +6.1 本自托管版本默认情况下,所有配置、对话记录、知识库与插件数据均保存在您所安装的 fnOS 设备本地共享目录(langbot/data)中,不会被自动上传至除您显式配置的模型/服务提供商以外的任何第三方。您对自己的数据及备份负责。 + +6.2 LangBot 默认启用最少量的匿名遥测,用于帮助改进产品。详细政策见官方文档: + https://docs.langbot.app/zh/insight/data-collection-policy + +6.3 启用遥测时仅可能发送:查询事件(适配器类型、运行器类型、模型名称、处理耗时、版本号、匿名工作区 UUID、插件/功能使用计数、不含用户内容的错误追踪)、每日一次的工作区心跳(部署概况、资源对象数量)、完全自愿的问卷回答。 + +6.4 我们绝对不收集:消息内容、用户名/手机号/平台账号 ID、API 密钥或凭据、IP 地址、文件或媒体内容。 + +6.5 关闭方式:进入 LangBot Web 管理界面 → 设置 → Space 遥测,关闭开关;或在配置文件 data/config.yaml 中设置 `space.disable_telemetry: true`。关闭后所有功能照常运行。 + +--- + +七、不可抗力 + +因不可抗力事件导致的履约失败或延迟,我们不承担责任。不可抗力包括但不限于:自然灾害(地震、洪水、飓风等)、战争、恐怖主义或内乱、政府行为或法规、网络攻击(DDoS、勒索软件等)、第三方服务故障(AI 模型提供商、即时通讯平台、飞牛平台运行时等)、电力故障或互联网连接中断、流行病或公共卫生紧急事件。 + +--- + +八、第三方服务 + +本软件可能依赖或集成第三方服务,包括但不限于:即时通讯平台(Telegram、Discord、微信、QQ、Slack 等)、AI 模型提供商(OpenAI、Anthropic、Google、深度求索等)、飞牛平台的应用中心运行时与依赖应用(如 Node.js)。我们不对第三方服务的可用性、准确性、可靠性或安全性负责;使用第三方服务须受其各自条款约束,第三方服务的变更可能不经通知即影响本软件功能。 + +--- + +九、软件修改与终止 + +9.1 我们保留随时修改、暂停或终止软件或其中任何部分的权利,无论是否事先通知。 +9.2 我们保留随时修改本协议的权利。对重大变更我们会尽力在项目主页公告,继续使用软件即视为接受修订后的协议。 +9.3 您可以在飞牛应用中心卸载本软件。卸载时向导会询问是否保留数据,您可自主选择。终止后您继续使用软件的权利立即终止,我们无义务保留您的数据。 + +--- + +十、赔偿 + +您同意赔偿、抗辩并使 LangBot 团队及其关联贡献者、管理人员、代理人、员工免受任何及所有因以下事项引起或与之相关的索赔、损失、损害、责任、成本和费用(包括合理的律师费): +(a) 您对软件的使用; +(b) 您违反本协议; +(c) 您违反任何适用的法律或法规; +(d) 您侵犯任何第三方权利; +(e) 您或您所配置的机器人通过本软件传输的内容。 + +--- + +十一、知识产权 + +11.1 本软件及其设计、代码、文档和品牌归 LangBot 项目所有并受知识产权法保护。 +11.2 使用本软件并不授予您对软件的任何所有权。 +11.3 您保留通过本软件创建和传输内容的所有权。 + +--- + +十二、争议解决 + +因本协议引起或与之相关的任何争议,应首先通过友好协商解决。协商在 30 日内未达成一致的,任何一方均可依适用法律规定向有管辖权的法院提起诉讼。 + +--- + +十三、可分割性 + +如本协议的任何条款被认定为不可执行或无效,该条款应在最小必要范围内予以限制或剔除,其余条款继续完全有效。 + +--- + +十四、完整协议 + +本协议连同我们的数据收集政策(https://docs.langbot.app/zh/insight/data-collection-policy)构成您与我们之间关于本软件的完整协议,并取代所有先前的协议与谅解。 + +--- + +附录:开源许可证声明 +LangBot 主程序代码依照 Apache License 2.0 发布。详细条款见: +https://github.com/langbot-app/LangBot/blob/master/LICENSE +或 LangBot 源码包内的 LICENSE 文件。 + +数据收集政策:https://docs.langbot.app/zh/insight/data-collection-policy + diff --git a/packaging/fnos/README.md b/packaging/fnos/README.md new file mode 100644 index 000000000..1bd672b67 --- /dev/null +++ b/packaging/fnos/README.md @@ -0,0 +1,78 @@ +# LangBot fnOS Packaging + +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. + +## Directory Structure + +``` +packaging/fnos/ +├── manifest # App metadata (appname/version/port/dependency declarations) +├── build.sh # One-shot build script (shared by local and CI) +├── LICENSE +├── config/ +│ ├── privilege # Privilege config (run-as: root) +│ └── resource # Persistent data share declaration (langbot/data) +├── cmd/ # Lifecycle scripts (fnOS invokes them with TRIM_* env vars) +│ ├── main # Service start/stop manager (start/stop/status, owns PID/log) +│ ├── install_init # Pre-install hook +│ ├── install_callback # Post-install hook: create venv, uv sync deps, seed config.yaml port +│ ├── upgrade_init # Pre-upgrade hook +│ ├── upgrade_callback # Post-upgrade hook +│ ├── uninstall_init # Pre-uninstall hook +│ ├── uninstall_callback # Post-uninstall hook (keeps data per wizard choice) +│ ├── config_init # Pre-config-change hook +│ └── config_callback # Post-config-change hook (apply new port etc.) +├── wizard/ # Install wizards (JSON forms; values passed as wizard_* env vars) +│ ├── install # On install: Node version, web port, deployment-time notice +│ ├── upgrade # On upgrade: Node version confirmation +│ └── uninstall # On uninstall: whether to keep data +├── app/ +│ ├── ui/config # Desktop entry declaration (${wizard_port} placeholder, substituted by fnOS at install) +│ ├── desktop/langbot.main.url +│ ├── langbot/ # [generated] repo source synced via rsync (includes web/dist) +│ └── bin/ # [generated] offline uv binaries (x86_64/aarch64) +├── ICON.PNG / ICON_256.PNG # [generated] derived from res/logo-blue.png +└── langbot.fpk # [generated] final artifact +``` + +Paths marked `[generated]` are produced by `build.sh`, ignored via `.gitignore`; everything else is a git-tracked source file. + +## Building + +### Locally + +```bash +bash packaging/fnos/build.sh +``` + +Dependencies: python3 + Pillow, node + npm (or pnpm), fnpack (official fnOS packaging CLI, download from https://developer.fnnas.com/docs/cli/fnpack). + +The version comes from `version=` maintained in the manifest; it can also be injected: `FPK_VERSION=4.10.10-1 bash packaging/fnos/build.sh`. + +### CI + +[`.github/workflows/build-fnos-fpk.yaml`](../../.github/workflows/build-fnos-fpk.yaml) triggers automatically on Release publication and uploads `langbot--fnos.fpk` to the Release; it can also be triggered manually via workflow_dispatch. + +Version sources (consistent with the other release workflows): + +| Trigger | Version source | +|---|---| +| Release/tag auto build | Tag name (`v4.10.10-1` → in-package `4.10.10-1`; build.sh strips the `v` prefix on injection) | +| Manual workflow_dispatch / local build | Version maintained in manifest | + +## Final Artifact + +`langbot.fpk` (gzip + tar archive), containing: + +- `manifest` — realigned and appended with a `checksum` field by fnpack +- `app.tgz` — app payload (source, web/dist, uv binaries, entry configs) +- `cmd/`, `config/`, `wizard/` — lifecycle scripts and wizards +- `ICON.PNG`, `ICON_256.PNG`, `LICENSE` + +The first startup after installation takes about 5-10 minutes to finish dependency deployment (uv venv + sync); after that the web admin UI is reachable via the desktop icon or `http://:` (default port 5300). + +## References + +- fnOS developer docs: https://developer.fnnas.com/ +- fnOS app wizard: https://developer.fnnas.com/docs/core-concepts/wizard/ +- fnpack CLI: https://developer.fnnas.com/docs/cli/fnpack diff --git a/packaging/fnos/app/desktop/langbot.main.url b/packaging/fnos/app/desktop/langbot.main.url new file mode 100644 index 000000000..513870fe8 --- /dev/null +++ b/packaging/fnos/app/desktop/langbot.main.url @@ -0,0 +1,9 @@ +{ + "title": "LangBot", + "icon": "images/icon-256.png", + "type": "url", + "protocol": "http", + "port": "${wizard_port}", + "url": "/", + "allUsers": true +} diff --git a/packaging/fnos/app/ui/config b/packaging/fnos/app/ui/config new file mode 100644 index 000000000..d45765dd4 --- /dev/null +++ b/packaging/fnos/app/ui/config @@ -0,0 +1,13 @@ +{ + ".url": { + "langbot.main": { + "title": "LangBot", + "icon": "images/icon-{0}.png", + "type": "url", + "protocol": "http", + "port": "${wizard_port}", + "url": "/", + "allUsers": true + } + } +} diff --git a/packaging/fnos/build.sh b/packaging/fnos/build.sh new file mode 100644 index 000000000..74675e2a3 --- /dev/null +++ b/packaging/fnos/build.sh @@ -0,0 +1,180 @@ +#!/usr/bin/env bash +# packaging/fnos/build.sh - Build LangBot fnOS FPK package (in-repo version) +# 在 LangBot 仓库内直接打包飞牛 fnOS 应用 +# Usage: +# bash packaging/fnos/build.sh # 版本取 manifest 中 version= +# FPK_VERSION=4.10.9 bash packaging/fnos/build.sh # 注入版本(CI 用 release tag) +# Dependencies: python3+Pillow, node+npm, fnpack +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SRC_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" # LangBot 仓库根 +FPK_DIR="${SCRIPT_DIR}" + +echo "==> LangBot fnOS FPK builder (in-repo)" +echo " Source root: ${SRC_ROOT}" +echo " FPK dir: ${FPK_DIR}" + +# --- 0. Inject version (release tag) --- +if [ -n "${FPK_VERSION:-}" ]; then + sed -i "s/^version=.*/version=${FPK_VERSION#v}/" "${FPK_DIR}/manifest" +else + # 无注入版本时自动跟进仓库主版本(pyproject.toml) + PY_VER=$(grep -m1 '^version = ' "${SRC_ROOT}/pyproject.toml" | cut -d'"' -f2) + if [ -n "${PY_VER}" ]; then + sed -i "s/^version=.*/version=${PY_VER}/" "${FPK_DIR}/manifest" + fi +fi +MANIFEST_VER=$(grep '^version=' "${FPK_DIR}/manifest" | cut -d= -f2) +echo " FPK version: ${MANIFEST_VER}" + +# --- 1. Build frontend --- +echo "[1/5] Building frontend (web/dist)..." +cd "${SRC_ROOT}/web" +if command -v pnpm >/dev/null 2>&1; then + pnpm install --frozen-lockfile 2>/dev/null || pnpm install + pnpm build +else + npm install + npx vite build +fi +[ -d dist ] || { echo "ERROR: web/dist missing" >&2; exit 1; } +echo " Frontend built" + +# --- 2. Sync source into packaging/fnos/app/langbot/ --- +echo "[2/5] Syncing source to app/langbot/..." +rm -rf "${FPK_DIR}/app/langbot" +mkdir -p "${FPK_DIR}/app/langbot" +cd "${SRC_ROOT}" +rsync -a \ + --exclude='.git' \ + --exclude='.venv' \ + --exclude='__pycache__' \ + --exclude='*.pyc' \ + --exclude='web/node_modules' \ + --exclude='web/.vite' \ + --exclude='tests' \ + --exclude='packaging' \ + --exclude='.pytest_cache' \ + --exclude='.mypy_cache' \ + --exclude='.ruff_cache' \ + --exclude='data' \ + --exclude='*.log' \ + --exclude='.dockerignore' \ + --exclude='Dockerfile' \ + --exclude='docker/' \ + --exclude='kubernetes.yaml' \ + --exclude='.github/' \ + --exclude='docs/' \ + --exclude='examples/' \ + --exclude='res/' \ + ./ "${FPK_DIR}/app/langbot/" + +[ -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))" + +# --- 2.5 Download bundled uv binaries (offline install on NAS) --- +echo "[2.5/5] Downloading bundled uv binaries..." +UV_VERSION="0.12.9" +mkdir -p "${FPK_DIR}/app/bin" +for arch in x86_64 aarch64; do + out="${FPK_DIR}/app/bin/uv-${arch}" + if [ -x "${out}" ]; then + echo " uv-${arch} already present, skip" + continue + fi + tmp="$(mktemp -d)" + if curl -sSL -o "${tmp}/uv.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}" \ + && cp "${tmp}/uv-${arch}-unknown-linux-gnu/uv" "${out}"; then + chmod +x "${out}" + echo " uv-${arch} downloaded (${UV_VERSION})" + else + echo " WARNING: failed to download uv for ${arch}, install will fall back to online install" >&2 + fi + rm -rf "${tmp}" +done + +# --- 3. Regenerate icons from res/logo-blue.png --- +echo "[3/5] Generating icons from res/logo-blue.png..." +export LOGO_SRC="${SRC_ROOT}/res/logo-blue.png" +export OUT_DIR="${FPK_DIR}" +python3 << 'PYEOF' +from PIL import Image +import os, sys + +src = os.environ.get("LOGO_SRC") +out_dir = os.environ.get("OUT_DIR") +if not src or not out_dir: + print("ERROR: LOGO_SRC or OUT_DIR not set", file=sys.stderr) + sys.exit(1) + +if not os.path.isfile(src): + print(f"ERROR: logo source not found: {src}", file=sys.stderr) + sys.exit(1) + +img = Image.open(src).convert("RGBA") + +for size, name in [(64, "ICON.PNG"), (256, "ICON_256.PNG")]: + img.resize((size, size), Image.LANCZOS).save(os.path.join(out_dir, name)) + +ui_dir = os.path.join(out_dir, "app/ui/images") +os.makedirs(ui_dir, exist_ok=True) +for size in [64, 256]: + img.resize((size, size), Image.LANCZOS).save(os.path.join(ui_dir, f"icon-{size}.png")) + +desktop_dir = os.path.join(out_dir, "app/desktop/images") +os.makedirs(desktop_dir, exist_ok=True) +for size in [64, 256]: + img.resize((size, size), Image.LANCZOS).save(os.path.join(desktop_dir, f"icon-{size}.png")) + +print(" Icons generated") +PYEOF + +# --- 4. Validate structure --- +echo "[4/5] Validating FPK structure..." +ERRORS=0 +[ -f "${FPK_DIR}/manifest" ] || { echo " MISSING: manifest"; ERRORS=$((ERRORS+1)); } +[ -f "${FPK_DIR}/config/privilege" ] || { echo " MISSING: config/privilege"; ERRORS=$((ERRORS+1)); } +[ -f "${FPK_DIR}/config/resource" ] || { echo " MISSING: config/resource"; ERRORS=$((ERRORS+1)); } +[ -f "${FPK_DIR}/ICON.PNG" ] || { echo " MISSING: ICON.PNG"; ERRORS=$((ERRORS+1)); } +[ -f "${FPK_DIR}/ICON_256.PNG" ] || { echo " MISSING: ICON_256.PNG"; ERRORS=$((ERRORS+1)); } +[ -f "${FPK_DIR}/app/ui/config" ] || { echo " MISSING: app/ui/config"; ERRORS=$((ERRORS+1)); } +[ -f "${FPK_DIR}/app/ui/images/icon-64.png" ] || { echo " MISSING: app/ui/images/icon-64.png"; ERRORS=$((ERRORS+1)); } +[ -f "${FPK_DIR}/app/ui/images/icon-256.png" ] || { echo " MISSING: app/ui/images/icon-256.png"; ERRORS=$((ERRORS+1)); } +[ -f "${FPK_DIR}/app/desktop/langbot.main.url" ] || { echo " MISSING: app/desktop/langbot.main.url"; ERRORS=$((ERRORS+1)); } +[ -f "${FPK_DIR}/app/desktop/images/icon-64.png" ] || { echo " MISSING: app/desktop/images/icon-64.png"; ERRORS=$((ERRORS+1)); } +[ -f "${FPK_DIR}/app/desktop/images/icon-256.png" ] || { echo " MISSING: app/desktop/images/icon-256.png"; ERRORS=$((ERRORS+1)); } +[ -d "${FPK_DIR}/cmd" ] || { echo " MISSING: cmd/"; ERRORS=$((ERRORS+1)); } +[ -d "${FPK_DIR}/wizard" ] || { echo " MISSING: wizard/"; ERRORS=$((ERRORS+1)); } + +for script in "${FPK_DIR}/cmd/"*; do + [ -x "${script}" ] || { echo " NOT EXECUTABLE: cmd/$(basename "$script")"; ERRORS=$((ERRORS+1)); } +done + +if [ "${ERRORS}" -gt 0 ]; then + echo "FAILED: ${ERRORS} validation errors" >&2 + exit 1 +fi +echo " Structure OK" + +# --- 5. Build FPK --- +echo "[5/5] Building .fpk..." +if ! command -v fnpack >/dev/null 2>&1; then + echo "ERROR: fnpack not found in PATH." >&2 + echo " Download from https://developer.fnnas.com/docs/cli/fnpack" >&2 + exit 1 +fi + +cd "${FPK_DIR}" +fnpack build +FPK_FILE=$(ls -t *.fpk 2>/dev/null | head -1) +if [ -n "${FPK_FILE}" ]; then + echo "" + echo "==> Done! FPK: ${FPK_DIR}/${FPK_FILE}" + echo " Size: $(du -sh "${FPK_FILE}" | cut -f1)" +else + echo "WARNING: fnpack finished but no .fpk found in ${FPK_DIR}" >&2 + exit 1 +fi diff --git a/packaging/fnos/cmd/config_callback b/packaging/fnos/cmd/config_callback new file mode 100755 index 000000000..ccecdcdb8 --- /dev/null +++ b/packaging/fnos/cmd/config_callback @@ -0,0 +1,6 @@ +#!/bin/bash +# cmd/config_callback - post-config hook +# Applies wizard-provided settings (e.g. Node.js version) that cmd/main reads. +# Nothing to persist currently; cmd/main reads wizard_node_version at runtime. + +exit 0 diff --git a/packaging/fnos/cmd/config_init b/packaging/fnos/cmd/config_init new file mode 100755 index 000000000..0d366f7a1 --- /dev/null +++ b/packaging/fnos/cmd/config_init @@ -0,0 +1,4 @@ +#!/bin/bash +# cmd/config_init - pre-config hook + +exit 0 diff --git a/packaging/fnos/cmd/install_callback b/packaging/fnos/cmd/install_callback new file mode 100755 index 000000000..4eac06acc --- /dev/null +++ b/packaging/fnos/cmd/install_callback @@ -0,0 +1,149 @@ +#!/bin/bash +# cmd/install_callback - post-install hook +# Prefers bundled uv binary, creates Python venv, syncs deps, verifies dist. +# Also validates the user-selected Node.js version is actually installed. + +APP_DIR="${TRIM_APPDEST}/langbot" + +# --- Resolve data directory --- +# LangBot loads config CWD-relative (data/config.yaml); cmd/main replaces +# APP_DIR/data with a symlink to this persistent dir on every start. +DATA_DIR="${TRIM_DATA_SHARE_PATHS%%:*}" +if [ -z "${DATA_DIR}" ]; then + DATA_DIR="${TRIM_PKGVAR}/data" +fi + +cd "${APP_DIR}" || { + echo "App directory missing after install" > "${TRIM_TEMP_LOGFILE}" + exit 1 +} + +# --- Ensure data directory exists --- +mkdir -p "${DATA_DIR}/plugins" "${DATA_DIR}/box" "${DATA_DIR}/logs" 2>/dev/null || true + +# --- Pre-seed config.yaml with the user-selected web port --- +# Data root points at the persistent share (LANGBOT_DATA_ROOT is exported by +# cmd/main at start), so this config survives app upgrades. +# LangBot copies templates/config.yaml there on first boot only if missing, +# so we seed it ourselves with the chosen port. +PORT="${wizard_port:-5300}" +case "${PORT}" in + ''|*[!0-9]*) PORT="5300" ;; +esac +TEMPLATE_FILE="${APP_DIR}/src/langbot/templates/config.yaml" + +_patch_config() { + local cfg_dir="$1" + local cfg_file="${cfg_dir}/config.yaml" + mkdir -p "${cfg_dir}" 2>/dev/null || true + if [ ! -f "${cfg_file}" ] && [ -f "${TEMPLATE_FILE}" ]; then + cp "${TEMPLATE_FILE}" "${cfg_file}" + fi + if [ -f "${cfg_file}" ]; then + sed -i -E "/^api:/,/^[a-z_]+:/ s/^([[:space:]]*port:).*/\1 ${PORT}/" "${cfg_file}" + sed -i "s#webhook_prefix: 'http://127\.0\.0\.1:[0-9]*'#webhook_prefix: 'http://127.0.0.1:${PORT}'#" "${cfg_file}" + fi +} + +# Seed the persistent dir AND the CWD-relative APP_DIR/data (cmd/main merges +# the latter into the persistent dir via symlink on first start, so the port +# survives regardless of which path ends up being read). +_patch_config "${DATA_DIR}" +if [ "${APP_DIR}/data" != "${DATA_DIR}" ]; then + _patch_config "${APP_DIR}/data" +fi + +# --- Fallback patch for desktop entry port --- +# fnOS natively substitutes ${wizard_port} in ui/config at install time. +# This only kicks in if the placeholder somehow survived (e.g. CLI install). +UI_CONFIG="${TRIM_APPDEST}/ui/config" +if [ -f "${UI_CONFIG}" ] && grep -q 'wizard_port\|{port}' "${UI_CONFIG}"; then + sed -i "s/\${wizard_port}/${PORT}/g; s/{port}/${PORT}/g" "${UI_CONFIG}" +fi + +# --- Validate user-selected Node.js version is installed --- +NODE_VERSION="${wizard_node_version:-22}" +if [ ! -d "/var/apps/nodejs_v${NODE_VERSION}" ]; then + echo "Node.js v${NODE_VERSION} 未安装:请先在应用中心安装 nodejs_v${NODE_VERSION},再重新安装本应用。" > "${TRIM_TEMP_LOGFILE}" + exit 1 +fi + +# --- Python check --- +PYTHON_BIN="python3" +if ! command -v "${PYTHON_BIN}" >/dev/null 2>&1; then + PYTHON_BIN="python" +fi +if ! command -v "${PYTHON_BIN}" >/dev/null 2>&1; then + echo "Python not found on this system" > "${TRIM_TEMP_LOGFILE}" + exit 1 +fi + +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 + echo "Python 3.11+ required but not found on this system" > "${TRIM_TEMP_LOGFILE}" + exit 1 +fi +PY_MAJOR=$(echo "${PY_VER}" | cut -d. -f1) +PY_MINOR=$(echo "${PY_VER}" | cut -d. -f2) +if [ "${PY_MAJOR}" -lt 3 ] || { [ "${PY_MAJOR}" -eq 3 ] && [ "${PY_MINOR}" -lt 11 ]; }; then + echo "Python 3.11+ required, found ${PY_VER}" > "${TRIM_TEMP_LOGFILE}" + exit 1 +fi + +# --- Resolve uv: bundled binary first, then online fallbacks --- +UV_BIN="" +ARCH=$(uname -m) +case "${ARCH}" in + 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 +fi + +# --- Create venv via uv --- +if [ ! -d ".venv" ]; then + "${UV_BIN}" venv .venv --python "${PYTHON_BIN}" || { + echo "Failed to create Python virtual environment via uv" > "${TRIM_TEMP_LOGFILE}" + exit 1 + } +fi + +# --- Sync dependencies --- +"${UV_BIN}" sync --extra seekdb || { + echo "Dependency sync failed. Check network connectivity." > "${TRIM_TEMP_LOGFILE}" + exit 1 +} + +# --- Verify frontend dist --- +if [ ! -d "web/dist" ] || [ -z "$(ls -A web/dist 2>/dev/null)" ]; then + echo "Frontend dist missing! Web UI will not be available." > "${TRIM_TEMP_LOGFILE}" + exit 1 +fi + +exit 0 diff --git a/packaging/fnos/cmd/install_init b/packaging/fnos/cmd/install_init new file mode 100755 index 000000000..2e940ee03 --- /dev/null +++ b/packaging/fnos/cmd/install_init @@ -0,0 +1,5 @@ +#!/bin/bash +# cmd/install_init - pre-install hook +# Nothing special to do before extraction. + +exit 0 diff --git a/packaging/fnos/cmd/main b/packaging/fnos/cmd/main new file mode 100755 index 000000000..a3181c383 --- /dev/null +++ b/packaging/fnos/cmd/main @@ -0,0 +1,194 @@ +#!/bin/bash +# cmd/main - LangBot lifecycle manager for fnOS +# Handles start / stop / status via standalone-runtime Python process. +# Node.js path is injected into PATH so Box sandbox npx MCP servers can run. + +PID_FILE="${TRIM_PKGVAR}/langbot.pid" +APP_DIR="${TRIM_APPDEST}/langbot" +LOG_FILE="${TRIM_PKGVAR}/langbot.log" + +# --- Locate fnOS Node.js bin path --- +# fnOS appname-based path: /var/apps/nodejs_vXX/target/bin +# This is a stable symlink regardless of which volume the app is on. +NODE_VERSION="${wizard_node_version:-22}" +NODE_BIN_DIR="/var/apps/nodejs_v${NODE_VERSION}/target/bin" + +if [ -d "${NODE_BIN_DIR}" ]; then + export PATH="${NODE_BIN_DIR}:${PATH}" +fi + +# --- Persistent data root --- +# LangBot loads data/config.yaml CWD-RELATIVE (see core/stages/load_config.py: +# load_yaml_config('data/config.yaml', ...)) and resolves its data root to +# /data in source-install mode — it does NOT honour LANGBOT_DATA_ROOT for +# config.yaml. So the real fix is the symlink below: APP_DIR/data -> DATA_DIR. +DATA_DIR="${TRIM_DATA_SHARE_PATHS%%:*}" +if [ -z "${DATA_DIR}" ]; then + DATA_DIR="${TRIM_PKGVAR}/data" +fi +export LANGBOT_DATA_ROOT="${DATA_DIR}" +mkdir -p "${DATA_DIR}" 2>/dev/null || true + +# --- Locate Python --- +PYTHON_BIN="python3" +! command -v "${PYTHON_BIN}" >/dev/null 2>&1 && PYTHON_BIN="python" + +# --- Locate uv --- +# install_callback puts the bundled uv binary at ${TRIM_PKGVAR}/bin/uv +UV_BIN="${TRIM_PKGVAR}/bin/uv" +if [ ! -x "${UV_BIN}" ]; then + UV_BIN="uv" +fi +if ! command -v "${UV_BIN}" >/dev/null 2>&1; then + UV_BIN="${HOME}/.local/bin/uv" +fi +if ! command -v "${UV_BIN}" >/dev/null 2>&1 && [ ! -x "${UV_BIN}" ]; then + UV_BIN="${HOME}/.cargo/bin/uv" +fi +if ! command -v "${UV_BIN}" >/dev/null 2>&1 && [ ! -x "${UV_BIN}" ]; then + UV_BIN="${APP_DIR}/.venv/bin/uv" +fi + +case $1 in + start) + if [ -f "${PID_FILE}" ]; then + PID=$(cat "${PID_FILE}" | tr -d '[:space:]') + if [ -n "${PID}" ] && kill -0 "${PID}" 2>/dev/null; then + exit 0 + fi + rm -f "${PID_FILE}" + fi + + if [ ! -d "${APP_DIR}" ]; then + echo "LangBot app directory missing: ${APP_DIR}" > "${TRIM_TEMP_LOGFILE}" + exit 1 + fi + + cd "${APP_DIR}" || { + echo "Cannot enter app directory: ${APP_DIR}" > "${TRIM_TEMP_LOGFILE}" + exit 1 + } + + if [ ! -d ".venv" ]; then + echo "Python virtual environment not found. Please reinstall LangBot." > "${TRIM_TEMP_LOGFILE}" + exit 1 + fi + + # --- Unify data location: APP_DIR/data -> symlink to persistent DATA_DIR --- + # LangBot reads config CWD-relative (data/config.yaml), so without this a + # fresh data/ with a default 5300 config gets recreated inside target/ on + # every install/upgrade. The symlink keeps everything on the persistent + # share; it is recreated here on each start (upgrades wipe target/). + APP_DATA="${APP_DIR}/data" + if [ -L "${APP_DATA}" ]; then + # already a symlink; re-point if the persistent dir changed + [ "$(readlink "${APP_DATA}")" != "${DATA_DIR}" ] && ln -sfn "${DATA_DIR}" "${APP_DATA}" + elif [ -d "${APP_DATA}" ]; then + # legacy real dir (created by LangBot before this fix): merge into the + # persistent dir without overwriting newer files already there + mkdir -p "${DATA_DIR}" + cp -an "${APP_DATA}/." "${DATA_DIR}/" 2>/dev/null || cp -a "${APP_DATA}/." "${DATA_DIR}/" + rm -rf "${APP_DATA}" + ln -s "${DATA_DIR}" "${APP_DATA}" + else + ln -s "${DATA_DIR}" "${APP_DATA}" + fi + + # Ensure LangBot's actual listen port always matches what fnOS shows in + # "应用设置 → 访问端口" (which is the single source of truth from the user's + # POV). Two sources, checked in priority order: + # 1. ${wizard_port} — only set during install/upgrade callbacks (not on + # normal `start`; kept for completeness). + # 2. target/ui/config — read the "port" field written by fnOS after + # ${wizard_port} substitution AND any later edit the user made via + # "应用设置 → 自定义 URL" pencil button. + # Without this: user picks 5303 in wizard, LangBot still listens on its + # default 5300, desktop shortcut hits 5303 → connection refused. + CONFIG_FILE="${DATA_DIR}/config.yaml" + _patch_port() { + local _port="$1" + case "${_port}" in + ''|*[!0-9]*) return ;; + esac + if [ ! -f "${CONFIG_FILE}" ]; then + local _tmpl="${APP_DIR}/src/langbot/templates/config.yaml" + [ -f "${_tmpl}" ] && cp "${_tmpl}" "${CONFIG_FILE}" + fi + if [ -f "${CONFIG_FILE}" ]; then + sed -i -E "/^api:/,/^[a-z_]+:/ s/^([[:space:]]*port:).*/\1 ${_port}/" "${CONFIG_FILE}" + sed -i "s#webhook_prefix: 'http://127\.0\.0\.1:[0-9]*'#webhook_prefix: 'http://127.0.0.1:${_port}'#" "${CONFIG_FILE}" + fi + } + if [ -n "${wizard_port:-}" ]; then + _patch_port "${wizard_port}" + fi + if [ -f "${TRIM_APPDEST}/ui/config" ]; then + _port_from_ui=$(python3 -c 'import json,sys +try: + d = json.load(open(sys.argv[1])) + for _name, _entry in (d.get(".url") or {}).items(): + p = _entry.get("port") + if isinstance(p, (int, float)): + print(int(p)) + elif isinstance(p, str) and p.isdigit(): + print(int(p)) + break +except Exception: + pass +' "${TRIM_APPDEST}/ui/config" 2>/dev/null) + if [ -n "${_port_from_ui}" ]; then + _patch_port "${_port_from_ui}" + fi + fi + + # Native deployment: no --standalone-runtime flag, LangBot spawns the + # plugin runtime as a stdio subprocess (same as official `uv run main.py`). + # (--standalone-runtime would require an external runtime at + # ws://langbot_plugin_runtime:5400, which only exists in Docker Compose.) + # --standalone-box omitted: Box sandbox defaults off, users enable via Web UI + nohup "${UV_BIN}" run --no-sync main.py \ + > "${LOG_FILE}" 2>&1 & + echo $! > "${PID_FILE}" + + sleep 3 + if [ -f "${PID_FILE}" ]; then + PID=$(cat "${PID_FILE}" | tr -d '[:space:]') + if [ -n "${PID}" ] && kill -0 "${PID}" 2>/dev/null; then + exit 0 + fi + fi + echo "LangBot failed to start. Check ${LOG_FILE}" > "${TRIM_TEMP_LOGFILE}" + exit 1 + ;; + + stop) + if [ -f "${PID_FILE}" ]; then + PID=$(cat "${PID_FILE}" | tr -d '[:space:]') + if [ -n "${PID}" ]; then + kill "${PID}" 2>/dev/null + for _ in 1 2 3 4 5 6 7 8 9 10; do + kill -0 "${PID}" 2>/dev/null || break + sleep 1 + done + kill -9 "${PID}" 2>/dev/null + fi + rm -f "${PID_FILE}" + fi + exit 0 + ;; + + status) + if [ -f "${PID_FILE}" ]; then + PID=$(cat "${PID_FILE}" | tr -d '[:space:]') + if [ -n "${PID}" ] && kill -0 "${PID}" 2>/dev/null; then + exit 0 + fi + rm -f "${PID_FILE}" + fi + exit 3 + ;; + + *) + exit 1 + ;; +esac diff --git a/packaging/fnos/cmd/uninstall_callback b/packaging/fnos/cmd/uninstall_callback new file mode 100755 index 000000000..2baee90d9 --- /dev/null +++ b/packaging/fnos/cmd/uninstall_callback @@ -0,0 +1,21 @@ +#!/bin/bash +# cmd/uninstall_callback - post-uninstall hook +# The system preserves var/ and shares/ by default. Honor the user's +# wizard_keep_data choice: delete data only when explicitly requested. + +if [ "${wizard_keep_data:-yes}" = "no" ]; then + # 应用运行数据(pid、日志等) + if [ -n "${TRIM_PKGVAR}" ]; then + rm -rf "${TRIM_PKGVAR:?}"/langbot.pid \ + "${TRIM_PKGVAR:?}"/langbot.log \ + "${TRIM_PKGVAR:?}"/bin 2>/dev/null || true + fi + + # 共享数据目录(langbot/data) + DATA_DIR="${TRIM_DATA_SHARE_PATHS%%:*}" + if [ -n "${DATA_DIR}" ]; then + rm -rf "${DATA_DIR:?}" 2>/dev/null || true + fi +fi + +exit 0 diff --git a/packaging/fnos/cmd/uninstall_init b/packaging/fnos/cmd/uninstall_init new file mode 100755 index 000000000..800ced14c --- /dev/null +++ b/packaging/fnos/cmd/uninstall_init @@ -0,0 +1,20 @@ +#!/bin/bash +# cmd/uninstall_init - pre-uninstall hook +# Stop LangBot before files are removed. + +PID_FILE="${TRIM_PKGVAR}/langbot.pid" + +if [ -f "${PID_FILE}" ]; then + PID=$(cat "${PID_FILE}" | tr -d '[:space:]') + if [ -n "${PID}" ] && kill -0 "${PID}" 2>/dev/null; then + kill "${PID}" 2>/dev/null + for _ in 1 2 3 4 5 6 7 8 9 10; do + kill -0 "${PID}" 2>/dev/null || break + sleep 1 + done + kill -9 "${PID}" 2>/dev/null + fi + rm -f "${PID_FILE}" +fi + +exit 0 diff --git a/packaging/fnos/cmd/upgrade_callback b/packaging/fnos/cmd/upgrade_callback new file mode 100755 index 000000000..9288c866c --- /dev/null +++ b/packaging/fnos/cmd/upgrade_callback @@ -0,0 +1,77 @@ +#!/bin/bash +# cmd/upgrade_callback - post-upgrade hook +# Re-sync dependencies after code replacement using uv. + +APP_DIR="${TRIM_APPDEST}/langbot" + +# Persistent data root (must match cmd/main) +DATA_DIR="${TRIM_DATA_SHARE_PATHS%%:*}" +[ -z "${DATA_DIR}" ] && DATA_DIR="${TRIM_PKGVAR}/data" + +# Apply port from upgrade wizard (config persists across upgrades; this +# only rewrites it when the user changed the value in the upgrade wizard) +CONFIG_FILE="${DATA_DIR}/config.yaml" +if [ -n "${wizard_port:-}" ] && [ -f "${CONFIG_FILE}" ]; then + case "${wizard_port}" in + ''|*[!0-9]*) ;; + *) + sed -i -E "/^api:/,/^[a-z_]+:/ s/^([[:space:]]*port:).*/\1 ${wizard_port}/" "${CONFIG_FILE}" + sed -i "s#webhook_prefix: 'http://127\.0\.0\.1:[0-9]*'#webhook_prefix: 'http://127.0.0.1:${wizard_port}'#" "${CONFIG_FILE}" + ;; + esac +fi + +cd "${APP_DIR}" || { + echo "App directory missing after upgrade" > "${TRIM_TEMP_LOGFILE}" + exit 1 +} + +# Find uv (bundled first, then PATH / ~/.local/bin / ~/.cargo/bin) +UV_BIN="${TRIM_PKGVAR}/bin/uv" +if [ ! -x "${UV_BIN}" ]; then + UV_BIN="uv" +fi +if ! command -v "${UV_BIN}" >/dev/null 2>&1; then + UV_BIN="${HOME}/.local/bin/uv" +fi +if ! command -v "${UV_BIN}" >/dev/null 2>&1 && [ ! -x "${UV_BIN}" ]; then + UV_BIN="${HOME}/.cargo/bin/uv" +fi + +PYTHON_BIN="python3" +! command -v "${PYTHON_BIN}" >/dev/null 2>&1 && PYTHON_BIN="python" + +# Re-sync deps +if [ -d ".venv" ]; then + "${UV_BIN}" sync --extra seekdb 2>/dev/null || { + echo "Dependency sync failed after upgrade" > "${TRIM_TEMP_LOGFILE}" + exit 1 + } +else + # Venv was lost, recreate via uv + if ! command -v "${UV_BIN}" >/dev/null 2>&1 && [ ! -x "${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 || { + 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 + } + "${UV_BIN}" sync --extra seekdb || { + echo "Dependency sync failed" > "${TRIM_TEMP_LOGFILE}" + exit 1 + } +fi + +# Verify frontend dist still present +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}" + exit 1 +fi + +exit 0 diff --git a/packaging/fnos/cmd/upgrade_init b/packaging/fnos/cmd/upgrade_init new file mode 100755 index 000000000..fdac71831 --- /dev/null +++ b/packaging/fnos/cmd/upgrade_init @@ -0,0 +1,20 @@ +#!/bin/bash +# cmd/upgrade_init - pre-upgrade hook +# Stop the running LangBot process before files are replaced. + +PID_FILE="${TRIM_PKGVAR}/langbot.pid" + +if [ -f "${PID_FILE}" ]; then + PID=$(cat "${PID_FILE}" | tr -d '[:space:]') + if [ -n "${PID}" ] && kill -0 "${PID}" 2>/dev/null; then + kill "${PID}" 2>/dev/null + for _ in 1 2 3 4 5 6 7 8 9 10; do + kill -0 "${PID}" 2>/dev/null || break + sleep 1 + done + kill -9 "${PID}" 2>/dev/null + fi + rm -f "${PID_FILE}" +fi + +exit 0 diff --git a/packaging/fnos/config/privilege b/packaging/fnos/config/privilege new file mode 100644 index 000000000..e21db569b --- /dev/null +++ b/packaging/fnos/config/privilege @@ -0,0 +1,5 @@ +{ + "defaults": { + "run-as": "root" + } +} diff --git a/packaging/fnos/config/resource b/packaging/fnos/config/resource new file mode 100644 index 000000000..eef99d345 --- /dev/null +++ b/packaging/fnos/config/resource @@ -0,0 +1,9 @@ +{ + "data-share": { + "shares": [ + { + "name": "langbot/data" + } + ] + } +} diff --git a/packaging/fnos/manifest b/packaging/fnos/manifest new file mode 100644 index 000000000..98699b795 --- /dev/null +++ b/packaging/fnos/manifest @@ -0,0 +1,16 @@ +appname=langbot +version=4.10.10 +display_name=LangBot +desc=基于 LLM 的多平台智能对话机器人,支持 QQ、微信、飞书、钉钉、Telegram 等十余种即时通讯平台,内置 Web 管理界面和 AI Agent 能力。 +platform=all +source=thirdparty +maintainer=LangBot +maintainer_url=https://langbot.app +service_port=5300 +checkport=true +os_min_version=0.9.0 +desktop_uidir=ui +desktop_applaunchname=langbot.main +ctl_stop=true +install_dep_apps=nodejs_v22 +changelog=飞牛 fnOS 增强版首版:原生 Python 部署,依赖 Node.js v22 以启用 Box 沙箱 + npx MCP 能力 diff --git a/packaging/fnos/wizard/install b/packaging/fnos/wizard/install new file mode 100644 index 000000000..d9833facf --- /dev/null +++ b/packaging/fnos/wizard/install @@ -0,0 +1,47 @@ +[ + { + "stepTitle": "运行环境", + "items": [ + { + "type": "tips", + "helpText": "LangBot 依赖 Node.js v22 运行 Box 沙箱和 npx MCP。请先在应用中心安装 Node.js v22。" + }, + { + "type": "select", + "field": "wizard_node_version", + "label": "Node.js 版本", + "initValue": "22", + "options": [ + { "label": "Node.js v22 (推荐, LTS)", "value": "22" }, + { "label": "Node.js v24", "value": "24" }, + { "label": "Node.js v20", "value": "20" } + ] + } + ] + }, + { + "stepTitle": "访问配置", + "items": [ + { + "type": "text", + "field": "wizard_port", + "label": "Web 访问端口", + "initValue": "5300", + "rules": [ + { "required": true, "message": "请输入访问端口" }, + { "pattern": "^[0-9]+$", "message": "端口只能是数字" }, + { "min": 1, "max": 5, "message": "端口号长度不正确" } + ] + } + ] + }, + { + "stepTitle": "安装说明", + "items": [ + { + "type": "tips", + "helpText": "安装完成后,LangBot 首次启动约需 5-10 分钟完成依赖部署与初始化,部署完成后即可打开网页端使用。" + } + ] + } +] diff --git a/packaging/fnos/wizard/uninstall b/packaging/fnos/wizard/uninstall new file mode 100644 index 000000000..5b96987fb --- /dev/null +++ b/packaging/fnos/wizard/uninstall @@ -0,0 +1,17 @@ +[ + { + "stepTitle": "数据保留", + "items": [ + { + "type": "radio", + "field": "wizard_keep_data", + "label": "是否保留 LangBot 数据(插件、配置、日志)", + "initValue": "yes", + "options": [ + { "label": "保留数据(重新安装后可继续使用)", "value": "yes" }, + { "label": "彻底删除全部数据", "value": "no" } + ] + } + ] + } +] diff --git a/packaging/fnos/wizard/upgrade b/packaging/fnos/wizard/upgrade new file mode 100644 index 000000000..718264df7 --- /dev/null +++ b/packaging/fnos/wizard/upgrade @@ -0,0 +1,38 @@ +[ + { + "stepTitle": "运行环境", + "items": [ + { + "type": "tips", + "helpText": "LangBot 依赖 Node.js v22 运行 Box 沙箱和 npx MCP。如需更换版本,请先在应用中心安装对应版本。" + }, + { + "type": "select", + "field": "wizard_node_version", + "label": "Node.js 版本", + "initValue": "22", + "options": [ + { "label": "Node.js v22 (推荐, LTS)", "value": "22" }, + { "label": "Node.js v24", "value": "24" }, + { "label": "Node.js v20", "value": "20" } + ] + } + ] + }, + { + "stepTitle": "访问配置", + "items": [ + { + "type": "text", + "field": "wizard_port", + "label": "Web 访问端口", + "initValue": "5300", + "rules": [ + { "required": true, "message": "请输入访问端口" }, + { "pattern": "^[0-9]+$", "message": "端口只能是数字" }, + { "min": 1, "max": 5, "message": "端口号长度不正确" } + ] + } + ] + } +]