diff --git a/.github/workflows/fnos-build-fpk.yml b/.github/workflows/fnos-build-fpk.yml new file mode 100644 index 00000000..d7176b12 --- /dev/null +++ b/.github/workflows/fnos-build-fpk.yml @@ -0,0 +1,367 @@ +name: Build Octop FPK (template) + +# TEMPLATE — FnOS (飞牛 NAS) app packaging pipeline for fork maintainers. +# # This workflow builds the Octop Docker image, the Docker .fpk and the native .fpk, +# # then publishes GitHub releases (fixed version + rolling latest). +# # +# # To enable on your fork: +# # 1. Replace ghcr.io/TencentCloud/octop with your own image namespace (or fork a copy). +# # 2. Optionally add a push trigger below (e.g. branches: [main]) so a rebuild happens +# # automatically on every upstream sync. It is intentionally workflow_dispatch-only +# # here so it never runs on the upstream repository. +# # +# image job:从仓库源码构建 Octop Docker 镜像,推送到 GHCR(ghcr.io/TencentCloud/octop:vanilla) +# fpk job:构建「Docker 版」飞牛安装包 Fnos-octop-vanilla--.fpk(依赖 image,确保镜像已就绪) +# native job:构建「本地版(非 Docker)」飞牛安装包 Fnos-octop-vanilla-native--.fpk。 +# 不内置 Python 运行时(依赖飞牛应用商店 Python 3.12), +# 仅内置 Octop 核心 whl + 核心依赖 site-packages,不含浏览器二进制与预装技能。 +# release job: +# - 创建固定版本 release(如 fnos-0.9.16-01),保留历史、带更新日志; +# - 同步更新滚动 release fnos-vanilla-latest,方便用户始终从同一 URL 下载最新版。 +# +# 镜像包随公开仓库默认即为 public,无需额外设置可见性。 + +on: + workflow_dispatch: + +permissions: + contents: write + packages: write + +concurrency: + group: build-octop-vanilla + cancel-in-progress: false + +jobs: + version: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.ver.outputs.version }} + iter: ${{ steps.iter.outputs.iter }} + tag: ${{ steps.iter.outputs.tag }} + pkg_ver: ${{ steps.iter.outputs.pkg_ver }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Read version + id: ver + run: | + VER=$(grep -m1 '^version' pyproject.toml | sed -E 's/.*"([0-9][0-9.]*[0-9])".*/\1/') + echo "version=$VER" >> "$GITHUB_OUTPUT" + + - name: Compute iteration + id: iter + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + VER=${{ steps.ver.outputs.version }} + # 查询已存在的 fnos--NN releases,取最大序号 +1 作为本次迭代号。 + # 之前用 GitHub API / git ls-remote / git tag -l 都在 CI 中因认证、 + # 限流或本地 tag 未拉取而回退成 0,导致每次都覆盖 fnos--01。 + # GitHub CLI 在 Actions runner 中已预装且自动使用 GITHUB_TOKEN,最稳。 + echo "[version] existing releases matching fnos-vanilla-${VER}-NN:" + gh release list --repo "${{ github.repository }}" --limit 100 --json tagName | python3 -c 'import sys,json; [print(" "+r.get("tagName","")) for r in json.load(sys.stdin)]' || true + EXISTING=$(gh release list --repo "${{ github.repository }}" --limit 100 --json tagName | python3 -c 'import sys,json,re; d=json.load(sys.stdin); pat=re.compile(r"fnos-vanilla-[0-9]+\.[0-9]+\.[0-9]+-([0-9]+)$"); nums=[int(pat.match(r.get("tagName","")).group(1)) for r in d if pat.match(r.get("tagName",""))]; print(max(nums) if nums else 0)') + MAX=${EXISTING:-0} + NEXT=$((MAX + 1)) + ITER=$(printf "%02d" "$NEXT") + echo "iter=$ITER" >> "$GITHUB_OUTPUT" + echo "tag=fnos-vanilla-${VER}-${ITER}" >> "$GITHUB_OUTPUT" + echo "pkg_ver=${VER}-${ITER}" >> "$GITHUB_OUTPUT" + echo "[version] VER=$VER, EXISTING_MAX=$MAX, ITER=$ITER, TAG=fnos-${VER}-${ITER}" + + image: + needs: version + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push image + uses: docker/build-push-action@v6 + with: + context: . + file: fnos/Dockerfile + push: true + tags: | + ghcr.io/TencentCloud/octop:vanilla + ghcr.io/TencentCloud/octop:vanilla-v${{ needs.version.outputs.version }} + cache-from: type=gha + cache-to: type=gha,mode=max + + fpk: + needs: [image, version] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Build Docker FPK + env: + FPK_NAME_PREFIX: Fnos-octop-vanilla + FPK_ITER: ${{ needs.version.outputs.iter }} + run: bash scripts/build-fpk.sh docker + + - name: Upload docker fpk artifact + uses: actions/upload-artifact@v4 + with: + name: docker-fpk + path: dist/Fnos-octop-vanilla-*.fpk + if-no-files-found: error + + native: + # 即使原生版构建失败也不阻塞整体发布:Docker 版(fpk)始终会发布; + # 原生版成功则一并附带。continue-on-error 让 needs 视为通过。 + continue-on-error: true + needs: version + runs-on: ubuntu-latest + outputs: + native_ok: ${{ steps.build_native.outcome == 'success' && steps.upload_native.outcome == 'success' }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Set up Python 3.12 (build-only) + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Set up Node.js 20 (frontend build) + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: dashboard/package-lock.json + + # native 包必须包含当前 fork 的源码修改(如 /admin/providers/fetch-models)。 + # 上游 TencentCloud/Octop 的 release wheel 不包含这些修改,因此改为从当前 + # 仓库源码构建 wheel:先构建前端产物到 src/octop/dashboard,再打包 wheel。 + - name: Build Octop wheel from current source + run: | + set -e + echo "=== build frontend ===" + cd dashboard + npm ci --prefer-offline --no-audit + NODE_ENV=production NODE_OPTIONS="--max-old-space-size=4096" npm run build + cd .. + echo "=== frontend output ===" + ls -la src/octop/dashboard | head -20 + echo "=== build wheel ===" + python3 -m pip install --quiet build + python3 -m build --wheel + echo "=== built wheel ===" + ls -lh dist/octop-*.whl + + - name: Install Octop core into app/site-packages (no Chromium, no preinstalled skills) + id: install_core + timeout-minutes: 15 + env: + UV_PYTHON_INSTALL_DIR: ${{ github.workspace }}/.uv-python + run: | + set -x + LOG="${{ github.workspace }}/native-install.log" + : > "$LOG" + mkdir -p "${{ github.workspace }}/fnos-native/app/site-packages" + SP="${{ github.workspace }}/fnos-native/app/site-packages" + { + # 用 uv 安装核心 whl 及其依赖到包内 site-packages。 + # pip 对 orcakit-harness-agent[all] 的复杂依赖树会报 resolution-too-deep, + # uv 的回溯能力强得多(旧方案已验证)。 + python3 -m pip install --quiet uv + # 第一步:让 uv 自由解析并安装核心 whl(含其完整依赖树)。 + # 自由解析会拉到最新的 mcp==2.0.0 + langchain-mcp-adapters==0.3.1,这两者在结构上 + # 不兼容(见第二步),但本步只负责把依赖装齐,不要求版本正确。 + # 注意:不能把钉版与 octop.whl 放在同一条 uv 命令里——会和 whl 的传递依赖约束 + # 发生 resolution 冲突,导致整个 native 构建失败(fdca4c6 的教训)。 + python3 -m uv pip install --python python3.12 \ + --target "$SP" --no-cache "$(ls dist/octop-*.whl | head -1)" + # 第二步:强制覆盖为仓库 uv.lock 锁定的兼容组合 mcp==1.28.1 + langchain-mcp-adapters==0.3.0。 + # mcp 2.0 移除了 mcp.server.fastmcp / mcp.shared.context.RequestContext 等符号, + # 而 langchain-mcp-adapters 0.3.x 仍硬导入它们,导致 octop 启动即 ImportError。 + # 先清掉旧版目录,防止多版本共存导致 Python 仍加载坏的 mcp 2.0。 + rm -rf "$SP/mcp" "$SP"/mcp-*.dist-info \ + "$SP/langchain_mcp_adapters" "$SP"/langchain_mcp_adapters-*.dist-info + # 用 pip download 拿到钉版 whl,再 unzip 进 site-packages。 + # 这比 pip install --target --force-reinstall 更可控,也不会触发 resolution。 + mkdir -p /tmp/octop-pinned + python3.12 -m pip download --no-deps --no-cache-dir -d /tmp/octop-pinned \ + "mcp==1.28.1" "langchain-mcp-adapters==0.3.0" + for whl in /tmp/octop-pinned/*.whl; do + echo "Extracting pinned wheel: $whl" + python3.12 -m zipfile -e "$whl" "$SP" + done + # 把核心 whl 也内置进去,供飞牛主机离线重装/回滚使用。 + cp dist/octop-*.whl "$SP/octop.whl" + # 校验:必须能走通真正的启动导入链(octop.launch → octop.infra.server → + # harness_agent.mcp → langchain_mcp_adapters)。仅 import octop 不会触发该链路, + # 故这里显式导入启动入口,确保 mcp/langchain-mcp-adapters 组合确实可用。 + PYTHONPATH="$SP" python3.12 \ + -c "from octop.launch import run_foreground_blocking; import mcp, langchain_mcp_adapters; print('octop', octop.__version__, '| mcp', mcp.__version__, '| lcma', langchain_mcp_adapters.__version__)" + } 2>&1 | tee -a "$LOG" + + - name: Build native FPK + id: build_native + env: + FPK_NAME_PREFIX: Fnos-octop-vanilla + FPK_ITER: ${{ needs.version.outputs.iter }} + run: bash scripts/build-fpk.sh native + + - name: Show native fpk size + run: ls -lh dist/Fnos-octop-vanilla-native-*.fpk + + - name: Upload native fpk artifact + id: upload_native + uses: actions/upload-artifact@v4 + with: + name: native-fpk + path: dist/Fnos-octop-vanilla-native-*.fpk + if-no-files-found: error + + - name: Upload native install log (for debugging) + if: always() + uses: actions/upload-artifact@v4 + with: + name: native-install-log + path: native-install.log + if-no-files-found: ignore + + release: + # 即使原生版失败(continue-on-error)也要发布 Docker 版; + # 原生版成功时 artifacts/native-fpk 存在,会被一并打包进发布。 + if: always() + needs: [fpk, native, version] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + + # 分别按名称下载,单个大产物(native-fpk ~560MB)单独下载比「下载全部」更稳定, + # 避免 GitHub artifact 服务对“全部”模式偶发 5 次重试失败。 + - name: Download docker fpk artifact (required) + uses: actions/download-artifact@v4 + with: + name: docker-fpk + path: artifacts/docker-fpk + if-no-files-found: error + + - name: Download native fpk artifact (best-effort) + # 原生版即使下载/解压偶发失败也不阻塞发布:Docker 版始终会发布。 + continue-on-error: true + uses: actions/download-artifact@v4 + with: + name: native-fpk + path: artifacts/native-fpk + if-no-files-found: ignore + + - name: Stage dist + run: | + mkdir -p dist + cp artifacts/docker-fpk/*.fpk dist/ || true + cp artifacts/native-fpk/*.fpk dist/ || true + ls -lh dist + + - name: Generate changelog + id: changelog + run: | + VER="${{ needs.version.outputs.version }}" + ITER="${{ needs.version.outputs.iter }}" + TAG="fnos-vanilla-${VER}-${ITER}" + # 找本分支历史中可达的最近固定版本 tag(排除滚动 tag)。 + # 不能直接 git describe:若历史 tag 误指向其它分支(如 main)的 commit, + # describe 会失败并回退到仓库根,导致更新说明把整个历史都列出来。 + PREV_TAG="" + for t in $(git tag --list "fnos-vanilla-*" --sort=-creatordate); do + [ "$t" = "fnos-vanilla-latest" ] && continue + if git merge-base --is-ancestor "$t" HEAD 2>/dev/null; then + PREV_TAG="$t" + break + fi + done + echo "prev_tag=$PREV_TAG" >> "$GITHUB_OUTPUT" + { + # 安装说明置顶,用户一眼看到下载文件 + echo "## 安装说明" + echo "" + echo "1. 飞牛应用中心 → 设置 → 手动安装应用,选择对应 .fpk。" + echo "2. Docker 版 **Fnos-octop-vanilla-${VER}-${ITER}.fpk**(约 80KB):飞牛自动从 GHCR 拉取镜像运行,镜像为 \\"ghcr.io/TencentCloud/octop:vanilla\\"。" + echo "3. 本地版 **Fnos-octop-vanilla-native-${VER}-${ITER}.fpk**(约 200MB):非 Docker,原生运行在飞牛主机;安装时自动关联系统 Python 3.12 开发工具;浏览器、远程桌面等附加组件在 Octop 应用内按需安装。" + echo "" + echo "---" + echo "固定版本请从本 Release 附件下载;最新滚动版见 [fnos-vanilla-latest](https://github.com/${{ github.repository }}/releases/tag/fnos-vanilla-latest)。" + echo "" + echo "## 本次更新(v${VER}-${ITER})" + echo "" + # 只列相对上一版的非 merge commit,最多 30 条,避免说明冗长 + if [ -n "$PREV_TAG" ]; then + git log --oneline --no-merges "$PREV_TAG"..HEAD | head -30 + else + git log --oneline --no-merges -30 HEAD + fi + } > /tmp/release-body.md + cat /tmp/release-body.md + + # --- 固定版本 release:保留历史 --- + - name: Publish fixed release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ needs.version.outputs.tag }} + # 显式指向构建所用 commit,避免 GitHub 默认指向 main 分支 HEAD + # (此前 fnos-vanilla-* tag 全部错误指向 main 的 commit) + target_commitish: ${{ github.sha }} + name: Octop (FnOS Vanilla) v${{ needs.version.outputs.pkg_ver }} + body_path: /tmp/release-body.md + files: dist/*.fpk + draft: false + prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # --- 滚动 release fnos-vanilla-latest:始终指向最新版,只保留最新 assets --- + - name: Delete rolling release if exists + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + TAG=fnos-vanilla-latest + REL_ID=$(curl -sS -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \ + https://api.github.com/repos/${{ github.repository }}/releases/tags/$TAG \ + | python3 -c "import sys,json;print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) + if [ -n "$REL_ID" ]; then + curl -sS -X DELETE -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \ + https://api.github.com/repos/${{ github.repository }}/releases/$REL_ID || true + fi + curl -sS -X DELETE -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \ + https://api.github.com/repos/${{ github.repository }}/git/refs/tags/$TAG || true + + - name: Publish rolling release + uses: softprops/action-gh-release@v2 + with: + tag_name: fnos-vanilla-latest + target_commitish: ${{ github.sha }} + name: Octop (FnOS Vanilla) latest + body_path: /tmp/release-body.md + files: dist/*.fpk + draft: false + prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index c2d43c75..062e8013 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,12 @@ Here are our mid-to-long term plans: This roadmap may shift as the community grows; treat it as indicative only. + +## 🖥️ FnOS (飞牛 NAS) Installation + +Octop can be installed on FnOS (飞牛 NAS) devices via the App Center as a `.fpk` package, either as a Docker-backed app or as a native (non-Docker) app. See [`fnos/README.md`](fnos/README.md) for the full packaging guide, the FPK build helper (`scripts/build-fpk.sh`), and the CI pipeline template (`.github/workflows/fnos-build-fpk.yml`). + +Initial admin credentials: `admin` / `Octop123` (change after first login). For non-root FnOS installs, the WeCom/Feishu connector CLIs require the fix in [PR #406](https://github.com/TencentCloud/Octop/pull/406) (user-level npm fallback). ## 🚀 Quick Start ### Prerequisites diff --git a/fnos-native/ICON.PNG b/fnos-native/ICON.PNG new file mode 100644 index 00000000..cc5ca51d Binary files /dev/null and b/fnos-native/ICON.PNG differ diff --git a/fnos-native/ICON_256.PNG b/fnos-native/ICON_256.PNG new file mode 100644 index 00000000..4c85d8c9 Binary files /dev/null and b/fnos-native/ICON_256.PNG differ diff --git a/fnos-native/LICENSE b/fnos-native/LICENSE new file mode 100644 index 00000000..10baa764 --- /dev/null +++ b/fnos-native/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Octop + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/fnos-native/app/bin/octop b/fnos-native/app/bin/octop new file mode 100644 index 00000000..e282f6b4 --- /dev/null +++ b/fnos-native/app/bin/octop @@ -0,0 +1,80 @@ +#!/bin/bash +# +# Octop 本地版 — 启动器 +# 运行在飞牛主机上,依赖飞牛应用商店提供的 Python 3.12(不内置 Python 运行时)。 +# 包内仅含 Octop 核心 whl + 核心依赖 site-packages;浏览器/专家/技能由用户按需安装。 +# 首次启动用向导配置的 .env 初始化管理员账号,然后启动服务。 +# +set -euo pipefail + +# 共享函数库(find_python312 / fix_ownership_and_perms / free_octop_ports) +# 打包时由 scripts/build-fpk.sh 注入;仓库内运行无此文件时跳过(函数缺失时再报错)。 +_OCTOP_COMMON=""$(dirname "$0")/../cmd/common.sh"" +[ -f "$_OCTOP_COMMON" ] && source "$_OCTOP_COMMON" + +APPDEST="${TRIM_APPDEST:-/var/apps/octop-native}" +PKGVAR="${TRIM_PKGVAR:-/var/apps/octop-native/var}" + +# 数据落在 FnOS 的 data-share 共享目录,升级时保留。 +if [ -n "${TRIM_DATA_SHARE_PATHS:-}" ]; then + DATA_DIR="${TRIM_DATA_SHARE_PATHS%%:*}" +else + DATA_DIR="${APPDEST}/shares/octop-native/data" +fi + +mkdir -p "$DATA_DIR" "$PKGVAR" + +# 加载向导配置(install_callback / config_callback 写入) +if [ -f "$PKGVAR/.env" ]; then + set -a + . "$PKGVAR/.env" + set +a +fi + +export HOME="$DATA_DIR" +export OCTOP_HOME="${DATA_DIR}/.octop" +export OCTOP_DATA_DIR="$DATA_DIR" + +# 查找飞牛系统上的 Python 3.12(优先应用商店安装路径,再 PATH) + + +PY="$(find_python312)" || { + echo "[octop-native] 错误:未找到 Python 3.12。" >&2 + echo "请先前往飞牛应用中心安装「Python 3.12」开发工具,再重新安装本应用。" >&2 + exit 1 +} + +SITE_PACKAGES="$APPDEST/site-packages" +export PYTHONPATH="${SITE_PACKAGES}${PYTHONPATH:+:${PYTHONPATH}}" + +# 兜底:若包内 site-packages 中未检测到 octop(升级异常 / 损坏等), +# 用内置核心 whl 就地离线安装,避免用户联网下载。 +if ! "$PY" -c "import octop" >/dev/null 2>&1; then + echo "[octop-native] 未检测到已安装的 Octop,正在用内置核心 whl 离线安装 ..." + "$PY" -m pip install --no-cache --no-index --find-links "$SITE_PACKAGES" "$SITE_PACKAGES/octop.whl" >/dev/null 2>&1 || { + echo "[octop-native] 离线安装失败,请检查网络或手动执行:$PY -m pip install $SITE_PACKAGES/octop.whl" >&2 + exit 1 + } +fi + +OCTOP_PORT="${OCTOP_PORT:-8089}" +OCTOP_LOG_LEVEL="${OCTOP_LOG_LEVEL:-info}" +OCTOP_BIND_HOST="${OCTOP_BIND_HOST:-0.0.0.0}" + +mkdir -p "$OCTOP_HOME" + +octop_cmd() { + "$PY" -m octop.cli.main "$@" +} + +# 首次初始化管理员账号(仅当数据库尚未建立) +if [ ! -f "$OCTOP_HOME/octop.db" ]; then + echo "[octop-native] 首次启动,正在初始化管理员账号 ..." + octop_cmd init --yes \ + --admin-username "${OCTOP_ADMIN_USERNAME:-admin}" \ + --admin-password "${OCTOP_DEFAULT_PASSWORD:-Octop123}" \ + ${OCTOP_ADMIN_DISPLAY_NAME:+--admin-display-name "$OCTOP_ADMIN_DISPLAY_NAME"} +fi + +echo "[octop-native] 正在启动 Octop,$OCTOP_BIND_HOST:$OCTOP_PORT ..." +exec "$PY" -m octop.cli.main run --host "$OCTOP_BIND_HOST" --port "$OCTOP_PORT" --log-level "${OCTOP_LOG_LEVEL}" diff --git a/fnos-native/app/bin/octop-cli b/fnos-native/app/bin/octop-cli new file mode 100644 index 00000000..ffa781b5 --- /dev/null +++ b/fnos-native/app/bin/octop-cli @@ -0,0 +1,78 @@ +#!/bin/bash +# +# octop — 飞牛本地版「官方 CLI」包装 +# +# 复刻 octop 运行环境后调用 `python -m octop.cli.main`,因此可使用全部官方子命令: +# octop --help +# octop version +# octop provider list +# octop agent list +# octop user list +# ...(还包括 init / run / config / backup / skills / update / clean 等) +# +# 注意:本机 Web 服务由飞牛应用中心托管(cmd/main 拉起 bin/octop), +# 请勿手动执行 `octop run`(会与飞牛管理的实例抢 8089 端口)。 +# +# 本包装以 octop-native 身份运行,避免产生 root 所有的数据文件 +# (Web 服务本身也以 octop-native 运行,混用 root 会导致后续权限问题)。 +# root 调用 sudo 免密;其他用户调用需输入自身 sudo 密码。 +# +set -euo pipefail + +# 共享函数库(find_python312 / fix_ownership_and_perms / free_octop_ports) +# 打包时由 scripts/build-fpk.sh 注入;仓库内运行无此文件时跳过(函数缺失时再报错)。 +_OCTOP_COMMON=""$(dirname "$0")/../cmd/common.sh"" +[ -f "$_OCTOP_COMMON" ] && source "$_OCTOP_COMMON" + +APPDEST="${TRIM_APPDEST:-/var/apps/octop-native}" +PKGVAR="${TRIM_PKGVAR:-/var/apps/octop-native/var}" + +# 数据目录:优先飞牛 data-share 共享目录,否则回退包内目录 +if [ -n "${TRIM_DATA_SHARE_PATHS:-}" ]; then + DATA_DIR="${TRIM_DATA_SHARE_PATHS%%:*}" +else + DATA_DIR="$APPDEST/shares/octop-native/data" +fi + +mkdir -p "$DATA_DIR" "$PKGVAR" + +# 加载安装向导写入的配置(OCTOP_PORT / 管理员账号 等) +if [ -f "$PKGVAR/.env" ]; then + set -a; . "$PKGVAR/.env"; set +a +fi + +# 查找飞牛应用商店提供的 Python 3.12 + + +PY="$(find_python312)" || { + echo "错误:未找到 Python 3.12,请先在飞牛应用中心安装「Python 3.12」开发工具。" >&2 + exit 1 +} + +# 关键环境变量:让 octop 找到包本体(site-packages)与你的数据目录 +SITE_PACKAGES="$APPDEST/site-packages" +export PYTHONPATH="${SITE_PACKAGES}${PYTHONPATH:+:$PYTHONPATH}" +export HOME="$DATA_DIR" +export OCTOP_HOME="$DATA_DIR/.octop" +export OCTOP_DATA_DIR="$DATA_DIR" + +# 兜底:若包内 octop 未就绪,用内置核心 whl 离线安装(避免联网) +if ! "$PY" -c "import octop" >/dev/null 2>&1; then + echo "[octop] 未检测到已安装的 Octop,正在用内置核心 whl 离线安装 ..." + "$PY" -m pip install --no-cache --no-index --find-links "$SITE_PACKAGES" "$SITE_PACKAGES/octop.whl" >/dev/null 2>&1 || { + echo "[octop] 离线安装失败,请检查网络或手动执行:$PY -m pip install $SITE_PACKAGES/octop.whl" >&2 + exit 1 + } +fi + +# 以 octop-native 身份运行,避免产生 root 所有的数据文件 +if id octop-native >/dev/null 2>&1 && command -v sudo >/dev/null 2>&1; then + exec sudo -u octop-native \ + PYTHONPATH="$PYTHONPATH" \ + HOME="$HOME" \ + OCTOP_HOME="$OCTOP_HOME" \ + OCTOP_DATA_DIR="$OCTOP_DATA_DIR" \ + "$PY" -m octop.cli.main "$@" +else + exec "$PY" -m octop.cli.main "$@" +fi diff --git a/fnos-native/app/ui/config b/fnos-native/app/ui/config new file mode 100644 index 00000000..7e9a915f --- /dev/null +++ b/fnos-native/app/ui/config @@ -0,0 +1,16 @@ +{ + ".url": { + "octop-native.Application": { + "title": "OCTOP", + "icon": "images/icon_{0}.png", + "type": "iframe", + "protocol": "http", + "port": "8089", + "url": "/", + "allUsers": true, + "control": { + "accessPerm": "readonly" + } + } + } +} diff --git a/fnos-native/app/ui/images/icon_256.png b/fnos-native/app/ui/images/icon_256.png new file mode 100644 index 00000000..4c85d8c9 Binary files /dev/null and b/fnos-native/app/ui/images/icon_256.png differ diff --git a/fnos-native/app/ui/images/icon_64.png b/fnos-native/app/ui/images/icon_64.png new file mode 100644 index 00000000..84a23221 Binary files /dev/null and b/fnos-native/app/ui/images/icon_64.png differ diff --git a/fnos-native/cmd/config_callback b/fnos-native/cmd/config_callback new file mode 100644 index 00000000..1ee2ccbe --- /dev/null +++ b/fnos-native/cmd/config_callback @@ -0,0 +1,58 @@ +#!/bin/bash +# +# Octop 本地版 — 重新配置回调 +# 与安装回调逻辑一致:将向导配置写回 PKGVAR/.env。 +# 注意:管理员账号/密码仅对尚未初始化的数据库生效; +# 密钥与日志级别在应用重启后生效。 +# +set -euo pipefail + +# 共享函数库(find_python312 / fix_ownership_and_perms / free_octop_ports) +# 打包时由 scripts/build-fpk.sh 注入;仓库内运行无此文件时跳过(函数缺失时再报错)。 +_OCTOP_COMMON=""$(dirname "$0")/common.sh"" +[ -f "$_OCTOP_COMMON" ] && source "$_OCTOP_COMMON" + +# --------------------------------------------------------------------------- +# 修正数据目录与 .env 属主/权限(与 install_callback 同一坑): +# 重新配置回调以 root 身份写 .env,必须 chown 给 octop-native,否则服务 +# 以 octop-native 启动读 .env 时 Permission denied。含 setfacl -b 防 ACL 把 +# mask 压成 ---。路径用 TRIM_* 变量,兼容 /vol3 等其它卷。 +# --------------------------------------------------------------------------- + + +PKGVAR="${TRIM_PKGVAR:-/var/apps/octop-native/var}" +mkdir -p "$PKGVAR" +ENV_FILE="$PKGVAR/.env" + +ADMIN_USER="${wizard_admin_username:-admin}" +ADMIN_PASS="${wizard_admin_password:-octop}" +LOG_LEVEL="${wizard_log_level:-info}" +OPENAI="${wizard_openai_api_key:-}" +DASHSCOPE="${wizard_dashscope_api_key:-}" + +sanitize() { printf '%s' "$1" | tr -d '\n\r"'"'"''; } + +ADMIN_USER=$(sanitize "$ADMIN_USER") +ADMIN_PASS=$(sanitize "$ADMIN_PASS") +LOG_LEVEL=$(sanitize "$LOG_LEVEL") +OPENAI=$(sanitize "$OPENAI") +DASHSCOPE=$(sanitize "$DASHSCOPE") + +{ + echo "OCTOP_PORT=8089" + echo "OCTOP_BIND_HOST=0.0.0.0" + echo "OCTOP_LOG_LEVEL=${LOG_LEVEL}" + echo "OCTOP_ADMIN_USERNAME=${ADMIN_USER}" + echo "OCTOP_DEFAULT_PASSWORD=${ADMIN_PASS}" + echo "OCTOP_ADMIN_DISPLAY_NAME=Admin" + echo "OPENAI_API_KEY=${OPENAI}" + echo "DASHSCOPE_API_KEY=${DASHSCOPE}" +} > "$ENV_FILE" + +chmod 600 "$ENV_FILE" +echo "[octop-native] 已更新配置: $ENV_FILE" > "${TRIM_TEMP_LOGFILE:-/dev/null}" 2>/dev/null || true + +# 修正属主/权限:.env 与 appdata 改回 octop-native(root 写 root 属主会让 +# 以 octop-native 启动的服务读取 .env 时 Permission denied)。 +fix_ownership_and_perms "$PKGVAR" "$ENV_FILE" +exit 0 diff --git a/fnos-native/cmd/config_init b/fnos-native/cmd/config_init new file mode 100644 index 00000000..06bd9865 --- /dev/null +++ b/fnos-native/cmd/config_init @@ -0,0 +1,2 @@ +#!/bin/bash +exit 0 diff --git a/fnos-native/cmd/install_callback b/fnos-native/cmd/install_callback new file mode 100644 index 00000000..4bbc07a0 --- /dev/null +++ b/fnos-native/cmd/install_callback @@ -0,0 +1,145 @@ +#!/bin/bash +# +# Octop 本地版 — 安装后回调 +# 使用固定默认配置写入 PKGVAR/.env(安装向导已移除,避免用户输入无效字段)。 +# 并校验飞牛系统上是否已安装 Python 3.12(应用商店提供)。 +# +set -uo pipefail + +# 共享函数库(find_python312 / fix_ownership_and_perms / free_octop_ports) +# 打包时由 scripts/build-fpk.sh 注入;仓库内运行无此文件时跳过(函数缺失时再报错)。 +_OCTOP_COMMON=""$(dirname "$0")/common.sh"" +[ -f "$_OCTOP_COMMON" ] && source "$_OCTOP_COMMON" + +# 释放 Octop 端口(8088=Docker 版,8089=本地版),杀掉残留/孤儿进程,避免重装端口冲突。 + + +# --------------------------------------------------------------------------- +# 修正数据目录与 .env 的属主/权限(致命坑,曾导致服务启动即崩): +# install_callback/config_callback 以 root 身份写 .env,若不显式 chown 给 +# octop-native,文件保持 root:root、权限 600,服务(octop-native)启动时 +# `bin/octop` 第 25 行 `. "$PKGVAR/.env"` 会 Permission denied 直接崩溃。 +# 另外若目录曾带 ACL,单纯 chmod 会把 mask 压成 ---,需 setfacl -b 清除。 +# 路径一律用 TRIM_* 变量(PKGVAR / TRIM_DATA_SHARE_PATHS),不固定 /vol1, +# 兼容用户在 /vol3 等其它卷安装(地址会变)。 +# --------------------------------------------------------------------------- + + +free_octop_ports + +PKGVAR="${TRIM_PKGVAR:-/var/apps/octop-native/var}" +APPDEST="${TRIM_APPDEST:-/var/apps/octop-native}" +mkdir -p "$PKGVAR" +ENV_FILE="$PKGVAR/.env" + +# 安装向导已移除,使用固定默认值(用户首次登录后可在控制台「设置」中修改)。 +ADMIN_USER="admin" +ADMIN_PASS="Octop123" +LOG_LEVEL="info" +OPENAI="" +DASHSCOPE="" + +sanitize() { printf '%s' "$1" | tr -d '\n\r"'"'"''; } + +ADMIN_USER=$(sanitize "$ADMIN_USER") +ADMIN_PASS=$(sanitize "$ADMIN_PASS") +LOG_LEVEL=$(sanitize "$LOG_LEVEL") +OPENAI=$(sanitize "$OPENAI") +DASHSCOPE=$(sanitize "$DASHSCOPE") + +{ + echo "OCTOP_PORT=8089" + echo "OCTOP_BIND_HOST=0.0.0.0" + echo "OCTOP_LOG_LEVEL=${LOG_LEVEL}" + echo "OCTOP_ADMIN_USERNAME=${ADMIN_USER}" + echo "OCTOP_DEFAULT_PASSWORD=${ADMIN_PASS}" + echo "OCTOP_ADMIN_DISPLAY_NAME=Admin" + echo "OPENAI_API_KEY=${OPENAI}" + echo "DASHSCOPE_API_KEY=${DASHSCOPE}" +} > "$ENV_FILE" + +chmod 600 "$ENV_FILE" +echo "[octop-native] 已写入配置: $ENV_FILE" > "${TRIM_TEMP_LOGFILE:-/dev/null}" 2>/dev/null || true + +# 修正属主/权限:.env 与 appdata 改回 octop-native,避免服务以 octop-native 启动 +# 时 . "$PKGVAR/.env" 报 Permission denied(root 写 root 属主,低权限用户读不了)。 +fix_ownership_and_perms "$PKGVAR" "$ENV_FILE" + +# 校验 Python 3.12 是否存在 + + +if ! PY="$(find_python312)"; then + echo "[octop-native] 错误:飞牛系统中未找到 Python 3.12。" > "${TRIM_TEMP_LOGFILE:-/dev/null}" 2>&1 || true + echo "请先前往飞牛应用中心 → 开发工具 → 安装「Python 3.12」,然后重新安装 Octop 本地版。" > "${TRIM_TEMP_LOGFILE:-/dev/null}" 2>&1 || true + exit 1 +fi + +echo "[octop-native] 已检测到 Python 3.12: $PY" > "${TRIM_TEMP_LOGFILE:-/dev/null}" 2>&1 || true + +# --------------------------------------------------------------------------- +# 让 Octop 本地版“开箱即用”: +# 1) 给 octop-native 系统账户设置已知密码(= 向导里的管理员密码), +# 便于工作台终端等场景使用。 +# 2) 授予 octop-native 免密码 sudo(NOPASSWD)。 +# Octop 的远程桌面/浏览器等组件在运行时会以 octop-native 身份执行 +# `sudo -n ...`(例如 desktop/setup.py 的 install/start/resize/uninstall), +# 若 sudo 需要密码就会失败报 “a password is required”。 +# 这里在安装回调(root 权限)阶段一次性配好,之后用户在界面里安装 +# 远程桌面 / 在工作台终端里执行 sudo 都不再需要输入密码。 +# --------------------------------------------------------------------------- +OCTOP_USER="octop-native" + +# 设置 octop-native 登录密码为向导管理员密码(若账户不存在则跳过) +if id "$OCTOP_USER" >/dev/null 2>&1; then + echo "${OCTOP_USER}:${ADMIN_PASS}" | chpasswd 2>/dev/null || true + echo "[octop-native] 已为 ${OCTOP_USER} 设置登录密码(与管理员密码一致)" > "${TRIM_TEMP_LOGFILE:-/dev/null}" 2>&1 || true +fi + +# 写入免密码 sudo 规则并做语法校验。 +# 这是本地版“远程桌面/浏览器/工作台终端”能正常工作的关键,必须成功; +# 因此这里不使用 || true 静默失败,若写入或校验失败则让安装回调直接返回非零, +# 避免飞牛显示“安装成功”但实际功能不可用(之前 printf+||true 导致文件被清空)。 +SUDOERS_FILE="/etc/sudoers.d/octop-native" +cat > "$SUDOERS_FILE" <<'EOF' +# 允许 octop-native 免密码执行任意命令(Octop 远程桌面/浏览器/工作台终端需要) +# !requiretty:Octop 以内嵌子进程方式调用 `sudo -n ...`,没有 controlling tty; +# 若系统 sudoers 默认开启 requiretty,这里必须显式关闭,否则会报 +# "sorry, you must have a tty to run sudo" 而让远程桌面/浏览器安装失败。 +Defaults:octop-native !requiretty +octop-native ALL=(ALL) NOPASSWD: ALL +EOF +chmod 0440 "$SUDOERS_FILE" +if command -v visudo >/dev/null 2>&1; then + if ! visudo -c -f "$SUDOERS_FILE" >/dev/null 2>&1; then + echo "[octop-native] 错误:写入的 sudoers 语法校验失败,请检查 ${SUDOERS_FILE}" > "${TRIM_TEMP_LOGFILE:-/dev/null}" 2>&1 || true + exit 1 + fi +fi +echo "[octop-native] 已配置 ${OCTOP_USER} 免密码 sudo" > "${TRIM_TEMP_LOGFILE:-/dev/null}" 2>&1 || true + +# 整体校验:必须“真实执行”一次 sudo,而不仅是 `sudo -n -l -U`(后者不需要 tty, +# 会掩盖 requiretty 问题)。以 octop-native 身份真实运行 `sudo -n id -u`, +# 期望 stdout 为 0(uid=0)。若失败,说明 sudoers 未被包含或 requiretty 未关闭, +# 用户点“安装虚拟桌面”时才会暴露,这里提前暴露便于排查。 +if id "$OCTOP_USER" >/dev/null 2>&1 && command -v sudo >/dev/null 2>&1; then + if su -s /bin/bash "$OCTOP_USER" -c 'sudo -n id -u' 2>/dev/null | grep -qx 0; then + echo "[octop-native] 校验通过:${OCTOP_USER} 可免密 sudo(真实执行 id -u 返回 0)" > "${TRIM_TEMP_LOGFILE:-/dev/null}" 2>&1 || true + else + echo "[octop-native] 警告:${OCTOP_USER} 免密 sudo 真实执行未通过(可能 /etc/sudoers.d 未包含或 requiretty 未关闭);若后续远程桌面报 sudo 错误,请在飞牛终端以 root 执行 visudo -c 并检查 /etc/sudoers 是否 #includedir /etc/sudoers.d。" > "${TRIM_TEMP_LOGFILE:-/dev/null}" 2>&1 || true + fi +fi + +# --------------------------------------------------------------------------- +# 将官方 octop CLI 暴露到 PATH,让用户安装后像官方一样直接用 `octop` 命令 +# (例如 `octop --help` / `octop provider list` / `octop agent list`)。 +# 真正的服务由飞牛应用中心托管(cmd/main 拉起 bin/octop),本 CLI 仅作命令行入口。 +# --------------------------------------------------------------------------- +CLI_SRC="$APPDEST/bin/octop-cli" +if [ -f "$CLI_SRC" ]; then + chmod +x "$CLI_SRC" 2>/dev/null || true + ln -sf "$CLI_SRC" /usr/local/bin/octop-cli + ln -sf "$CLI_SRC" /usr/local/bin/octop + echo "[octop-native] 已注册命令:octop / octop-cli -> $CLI_SRC" > "${TRIM_TEMP_LOGFILE:-/dev/null}" 2>&1 || true +fi + +exit 0 diff --git a/fnos-native/cmd/install_init b/fnos-native/cmd/install_init new file mode 100644 index 00000000..06bd9865 --- /dev/null +++ b/fnos-native/cmd/install_init @@ -0,0 +1,2 @@ +#!/bin/bash +exit 0 diff --git a/fnos-native/cmd/main b/fnos-native/cmd/main new file mode 100644 index 00000000..ac446f39 --- /dev/null +++ b/fnos-native/cmd/main @@ -0,0 +1,94 @@ +#!/bin/bash +# +# Octop 本地版(原生 app)— 生命周期管理 +# 直接以进程方式运行在飞牛主机上(非 Docker),监听 0.0.0.0:8089。 +# start: 启动 Octop(后台,写 PID 文件) +# stop: 发送 SIGTERM,超时 SIGKILL +# status: 查询进程状态 +# + +set -uo pipefail + +APPDEST="${TRIM_APPDEST:-/var/apps/octop-native}" +PKGVAR="${TRIM_PKGVAR:-/var/apps/octop-native/var}" +LOG_FILE="${PKGVAR}/info.log" +PID_FILE="${PKGVAR}/octop.pid" +PORT="${OCTOP_PORT:-8089}" +BIN="$APPDEST/bin/octop" + +log_msg() { echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "${LOG_FILE}"; } + +start_process() { + if status; then return 0; fi + mkdir -p "${PKGVAR}" + log_msg "Starting Octop (native) on port ${PORT} ..." + + # 以最低权限用户 octop-native 运行 Octop 服务(安全:web 服务与工作台终端 + # 不以 root 身份暴露)。若该用户不存在(个别 FnOS 版本在 run-as=root 时不 + # 创建包用户),则回退以当前身份(root)运行,保证服务一定起得来。 + # 注意:本包 run-as=root 是为了让 install_callback 能以 root 写入 + # /etc/sudoers.d/octop-native(授予 octop-native 免密 sudo),从而使远程桌面 + # /浏览器安装这类需要 root 的操作可用;服务本身仍尽量降权运行。 + local -a RUN_AS=() + if id octop-native >/dev/null 2>&1 && command -v runuser >/dev/null 2>&1; then + RUN_AS=(runuser -u octop-native --) + log_msg "将以 octop-native 低权限用户运行 Octop 服务" + else + log_msg "octop-native 用户不可用,回退以当前身份运行 Octop 服务" + fi + + # 启动 Octop 监听所有接口(供飞牛直接通过端口访问) + OCTOP_PORT="$PORT" OCTOP_BIND_HOST=0.0.0.0 ${RUN_AS[@]+"${RUN_AS[@]}"} "$BIN" --host 0.0.0.0 --port "$PORT" >> "${LOG_FILE}" 2>&1 & + octop_pid=$! + + # 等待 Octop TCP 端口就绪(最多 30 秒) + for i in $(seq 1 30); do + if nc -z 127.0.0.1 "$PORT" 2>/dev/null || (exec 3<>/dev/tcp/127.0.0.1/"$PORT") 2>/dev/null; then + break + fi + if ! kill -0 "$octop_pid" 2>/dev/null; then + log_msg "Octop process exited early" + return 1 + fi + sleep 1 + done + + printf "%s" "$octop_pid" > "${PID_FILE}" + log_msg "Octop pid=$octop_pid" + return 0 +} + +stop_process() { + log_msg "Stopping Octop (native) ..." + + if [ -r "${PID_FILE}" ]; then + pid=$(head -n 1 "${PID_FILE}" | tr -d '[:space:]') + if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then + # 杀整个进程组(包括可能的子进程) + kill -TERM "-$pid" 2>/dev/null || kill -TERM "$pid" 2>/dev/null || true + for i in $(seq 1 10); do + sleep 1 + if ! kill -0 "$pid" 2>/dev/null; then + break + fi + done + if kill -0 "$pid" 2>/dev/null; then + kill -KILL "-$pid" 2>/dev/null || kill -KILL "$pid" 2>/dev/null || true + fi + fi + rm -f "${PID_FILE}" + fi + + return 0 +} + +status() { + [ -f "${PID_FILE}" ] && pid=$(head -n 1 "${PID_FILE}" | tr -d '[:space:]') && [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null +} + +case $1 in + start) start_process ;; + stop) stop_process ;; + status) status && exit 0 || exit 3 ;; + *) exit 1 ;; +esac diff --git a/fnos-native/cmd/uninstall_callback b/fnos-native/cmd/uninstall_callback new file mode 100644 index 00000000..08bc1fd1 --- /dev/null +++ b/fnos-native/cmd/uninstall_callback @@ -0,0 +1,33 @@ +#!/bin/bash +# Octop 本地版 — 卸载后回调 +# 根据卸载向导选择:完全删除时清理 data-share 用户数据;保留数据时不作处理。 +# 无论是否删除数据,都清理本包安装时写入的特权(sudoers)与可能残留的桌面服务。 +set -uo pipefail + +# 清理安装时写入的免密码 sudo 规则(install_callback 以 root 写入,这里同样以 +# root 删除;卸载后不应再保留对 octop-native 的 NOPASSWD 授权)。 +rm -f /etc/sudoers.d/octop-native 2>/dev/null || true +echo "[octop-native] 已移除免密码 sudo 规则" > "${TRIM_TEMP_LOGFILE:-/dev/null}" 2>&1 || true + +# 移除安装时注册到 PATH 的 octop / octop-cli 命令 +rm -f /usr/local/bin/octop /usr/local/bin/octop-cli 2>/dev/null || true +echo "[octop-native] 已移除命令:octop / octop-cli" > "${TRIM_TEMP_LOGFILE:-/dev/null}" 2>&1 || true + +# 停止并禁用可能残留的虚拟桌面 systemd 单元(若用户卸载前未在 UI 内卸载桌面)。 +if command -v systemctl >/dev/null 2>&1 && [ -d /run/systemd/system ]; then + systemctl disable octop-desktop-xvnc octop-desktop-openbox octop-desktop-session 2>/dev/null || true + systemctl stop octop-desktop-xvnc octop-desktop-openbox octop-desktop-session 2>/dev/null || true + systemctl reset-failed octop-desktop-xvnc octop-desktop-openbox octop-desktop-session 2>/dev/null || true + systemctl daemon-reload 2>/dev/null || true +fi + +if [ "${wizard_delete_data:-no}" = "yes" ]; then + DATA_DIR="${TRIM_DATA_SHARE_PATHS:-}" + DATA_DIR="${DATA_DIR%%:*}" + if [ -n "$DATA_DIR" ] && [ -d "$DATA_DIR" ]; then + rm -rf "$DATA_DIR" 2>/dev/null || true + echo "[octop-native] 已删除用户数据: $DATA_DIR" > "${TRIM_TEMP_LOGFILE:-/dev/null}" 2>&1 || true + fi +fi + +exit 0 diff --git a/fnos-native/cmd/uninstall_init b/fnos-native/cmd/uninstall_init new file mode 100644 index 00000000..0986c983 --- /dev/null +++ b/fnos-native/cmd/uninstall_init @@ -0,0 +1,30 @@ +#!/bin/bash +# +# Octop 本地版 — 卸载前回调 +# 根据卸载向导 wizard_delete_data 的选择: +# - 保留数据:不清理 /vol1/@appshare/octop-native/data,由飞牛保留。 +# - 完全删除:在 uninstall_callback 中清理 data-share。 +# 同时释放 Octop 端口(8088=Docker 版,8089=本地版),避免卸载后残留进程占端口。 +# +set -uo pipefail + +# 共享函数库(find_python312 / fix_ownership_and_perms / free_octop_ports) +# 打包时由 scripts/build-fpk.sh 注入;仓库内运行无此文件时跳过(函数缺失时再报错)。 +_OCTOP_COMMON=""$(dirname "$0")/common.sh"" +[ -f "$_OCTOP_COMMON" ] && source "$_OCTOP_COMMON" + +LOG="${TRIM_TEMP_LOGFILE:-/dev/null}" +log() { echo "[octop-native] $1" > "$LOG" 2>/dev/null || true; } + +# 卸载时一并释放 Octop 端口(8088=Docker 版,8089=本地版),杀掉残留/孤儿进程,避免卸载不干净。 + + +free_octop_ports + +if [ "${wizard_delete_data:-no}" = "yes" ]; then + log "用户选择完全删除数据,将在 uninstall_callback 中清理 /vol1/@appshare/octop-native/data" +else + log "用户选择保留数据,/vol1/@appshare/octop-native/data 将保留" +fi + +exit 0 diff --git a/fnos-native/cmd/upgrade_callback b/fnos-native/cmd/upgrade_callback new file mode 100644 index 00000000..06bd9865 --- /dev/null +++ b/fnos-native/cmd/upgrade_callback @@ -0,0 +1,2 @@ +#!/bin/bash +exit 0 diff --git a/fnos-native/cmd/upgrade_init b/fnos-native/cmd/upgrade_init new file mode 100644 index 00000000..06bd9865 --- /dev/null +++ b/fnos-native/cmd/upgrade_init @@ -0,0 +1,2 @@ +#!/bin/bash +exit 0 diff --git a/fnos-native/config/privilege b/fnos-native/config/privilege new file mode 100644 index 00000000..b7a0b102 --- /dev/null +++ b/fnos-native/config/privilege @@ -0,0 +1,7 @@ +{ + "defaults": { + "run-as": "root" + }, + "username": "octop-native", + "groupname": "octop-native" +} diff --git a/fnos-native/config/resource b/fnos-native/config/resource new file mode 100644 index 00000000..df00ed8e --- /dev/null +++ b/fnos-native/config/resource @@ -0,0 +1,9 @@ +{ + "data-share": { + "shares": [ + { + "name": "octop-native/data" + } + ] + } +} diff --git a/fnos-native/manifest b/fnos-native/manifest new file mode 100644 index 00000000..d8aa6627 --- /dev/null +++ b/fnos-native/manifest @@ -0,0 +1,14 @@ +appname=octop-native +version=0.9.15 +display_name=OCTOP +desc=腾讯开源的多用户多智能体 AI 助手平台,支持 Web 控制台、CLI、IM 渠道与定时任务,数据全部本地存储。

安装后使用:
在应用中心点击「打开」按钮即可进入 Web 控制台,默认地址 http://设备IP:8089。
初始管理员账号:admin,初始密码:Octop123(首次登录后请在头像菜单修改密码)。

首次登录后:
请在控制台「设置」中配置 LLM 模型、IM 渠道、远程桌面、浏览器等组件。

注意:本地版直接运行在飞牛主机上,安装前会自动关联安装「Python 3.12」开发工具。 +source=thirdparty +platform=all +maintainer=TencentCloud OrcaKit +install_dep_apps=python312 +desktop_uidir=ui +desktop_applaunchname=octop-native.Application +service_port=8089 +checkport=true +ctl_stop=true +micro_app=true diff --git a/fnos-native/wizard/uninstall b/fnos-native/wizard/uninstall new file mode 100644 index 00000000..0273f1bf --- /dev/null +++ b/fnos-native/wizard/uninstall @@ -0,0 +1,29 @@ +[ + { + "stepTitle": "卸载 OCTOP 本地版", + "items": [ + { + "type": "radio", + "field": "wizard_delete_data", + "label": "请选择数据清理方式。", + "initValue": "no", + "options": [ + { + "label": "保留数据(推荐 — 下次安装时可继续使用)", + "value": "no" + }, + { + "label": "完全删除(包括对话历史、文件和配置)", + "value": "yes" + } + ], + "rules": [ + { + "required": true, + "message": "请选择是否删除应用数据" + } + ] + } + ] + } +] diff --git a/fnos/Dockerfile b/fnos/Dockerfile new file mode 100644 index 00000000..d0e5fd96 --- /dev/null +++ b/fnos/Dockerfile @@ -0,0 +1,111 @@ +# syntax=docker/dockerfile:1 +# ============================================================================= +# Octop — FnOS 专用构建(基于仓库源码,安装全部附加组件) +# +# 与上游 docker/Dockerfile 一致,但显式安装全部 extras: +# - browser (Playwright 浏览器自动化,已进核心依赖) +# - desktop (mss / pynput / pillow,桌面控制附加组件) +# +# 构建上下文必须为仓库根目录(fnpack / CI 中以仓库根为 context): +# docker build -f fnos/Dockerfile -t ghcr.io/TencentCloud/octop:latest . +# +# 国内加速(可选 build-arg): +# PIP_INDEX_URL / PIP_TRUSTED_HOST / NPM_REGISTRY / APT_MIRROR +# ============================================================================= + +# --------------------------------------------------------------------------- +# 阶段 1 — 前端构建(React / TypeScript → src/octop/dashboard) +# --------------------------------------------------------------------------- +FROM node:20-slim AS frontend-builder + +ARG NODE_MAX_OLD_SPACE_SIZE=2048 +ARG NPM_REGISTRY= +WORKDIR /build/dashboard + +COPY dashboard/package.json dashboard/package-lock.json ./ +RUN --mount=type=cache,target=/root/.npm \ + if [ -n "$NPM_REGISTRY" ]; then npm config set registry "$NPM_REGISTRY"; fi \ + && npm ci --prefer-offline --no-audit + +COPY dashboard/ ./ + +RUN mkdir -p ../src/octop/dashboard +RUN NODE_ENV=production NODE_OPTIONS="--max-old-space-size=${NODE_MAX_OLD_SPACE_SIZE}" npx vite build + +# --------------------------------------------------------------------------- +# 阶段 2 — Python 运行时 + 预构建前端 + 全部附加组件 +# --------------------------------------------------------------------------- +FROM python:3.12-slim AS runtime + +LABEL maintainer="TencentCloud OrcaKit" +LABEL org.opencontainers.image.title="Octop" +LABEL org.opencontainers.image.description="Smarter self-hosted AI assistant — multi-user, multi-agent (FnOS build with all addons)." +LABEL org.opencontainers.image.source="https://github.com/TencentCloud/Octop" +LABEL org.opencontainers.image.licenses="MIT" + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + HOME=/data \ + OCTOP_BIND_HOST=0.0.0.0 \ + OCTOP_PORT=8088 \ + OCTOP_LOG_LEVEL=info \ + UV_COMPILE_BYTECODE=1 \ + UV_LINK_MODE=copy \ + UV_PYTHON_DOWNLOADS=never \ + PIP_INDEX_URL=https://mirrors.aliyun.com/pypi/simple \ + PIP_TRUSTED_HOST=mirrors.aliyun.com + +ARG APT_MIRROR= + +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + if [ -n "$APT_MIRROR" ]; then \ + printf 'Types: deb\nURIs: https://%s/debian\nSuites: trixie trixie-updates\nComponents: main contrib non-free non-free-firmware\n\nTypes: deb\nURIs: https://%s/debian-security\nSuites: trixie-security\nComponents: main contrib non-free non-free-firmware\n' \ + "$APT_MIRROR" "$APT_MIRROR" > /etc/apt/sources.list.d/debian.sources; \ + fi \ + && apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + libffi-dev \ + curl \ + git \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=ghcr.io/astral-sh/uv:0.7 /uv /uvx /bin/ + +WORKDIR /app + +# 仅改源码时可复用此层缓存(用 uv pip 安装,避免 uv.lock 冻结冲突) +COPY pyproject.toml README.md LICENSE ./ +COPY src/ ./src/ +COPY --from=frontend-builder /build/src/octop/dashboard/ ./src/octop/dashboard/ + +COPY docker/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + +# 安装项目 + 全部附加组件(browser 已在核心依赖,desktop 显式加入) +# UV_INDEX_URL 首选阿里云镜像加速;UV_DEFAULT_INDEX 保留 PyPI 作为兜底, +# 避免新发布的包(如 orcakit-harness-agent 0.9.16)尚未同步到镜像时构建失败。 +RUN --mount=type=cache,target=/root/.cache/uv \ + uv venv /app/.venv \ + && UV_DEFAULT_INDEX=https://pypi.org/simple \ + UV_INDEX_URL=https://mirrors.aliyun.com/pypi/simple \ + UV_INSECURE_HOST=mirrors.aliyun.com \ + uv pip install --python /app/.venv ".[desktop]" \ + && PLAYWRIGHT_BROWSERS_PATH=/root/.cache/ms-playwright \ + /app/.venv/bin/python -m playwright install --with-deps chromium \ + && apt-get update && apt-get install -y --no-install-recommends fonts-noto-cjk \ + && apt-get purge -y --auto-remove build-essential \ + && rm -rf /var/lib/apt/lists/* /tmp/* + +ENV PATH="/app/.venv/bin:$PATH" \ + PLAYWRIGHT_BROWSERS_PATH=/root/.cache/ms-playwright + +RUN mkdir -p /data/.octop + +EXPOSE 8088 + +HEALTHCHECK --interval=30s --timeout=10s --start-period=120s --retries=3 \ + CMD ["sh", "-c", "curl -f http://localhost:${OCTOP_PORT:-8088}/api/health || exit 1"] + +ENTRYPOINT ["docker-entrypoint.sh"] +CMD [] diff --git a/fnos/ICON.PNG b/fnos/ICON.PNG new file mode 100644 index 00000000..cc5ca51d Binary files /dev/null and b/fnos/ICON.PNG differ diff --git a/fnos/ICON_256.PNG b/fnos/ICON_256.PNG new file mode 100644 index 00000000..4c85d8c9 Binary files /dev/null and b/fnos/ICON_256.PNG differ diff --git a/fnos/LICENSE b/fnos/LICENSE new file mode 100644 index 00000000..10baa764 --- /dev/null +++ b/fnos/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Octop + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/fnos/README.md b/fnos/README.md new file mode 100644 index 00000000..23d16658 --- /dev/null +++ b/fnos/README.md @@ -0,0 +1,88 @@ +# Octop — 飞牛 FnOS 安装包 + +本目录包含将 [TencentCloud/Octop](https://github.com/TencentCloud/Octop) 打包为飞牛 fnOS `.fpk` 安装包所需的全部文件,并附带自动同步上游 + 自动构建的 GitHub Actions。 + +## First use (initial account) + +After install, open the app (Docker: `http://:8088`, native: `http://:8089`) and sign in with the **initial admin account**: + +| Field | Value | +|-------|-------| +| Username | `admin` | +| Password | `Octop123` | + +> The password is a fixed initial value (same as the official Docker image default). **Change it right after first login** via the avatar menu → "Change password". If `OCTOP_ADMIN_USERNAME` / `OCTOP_DEFAULT_PASSWORD` were customized via env during install, those take precedence. + +## 两种安装包 + +仓库同时产出 **两款** `.fpk`,用于满足不同部署偏好: + +| 版本 | 包名 | 体积 | 运行方式 | 依赖 | +|------|------|------|----------|------| +| **Docker 版** | `octop-.fpk` | ~8 KB | 飞牛自动从 `ghcr.io/TencentCloud/octop:latest` 拉镜像运行 | 宿主需有 Docker 运行时 | +| **本地版(非 Docker)** | `octop-native-.fpk` | ~560 MB | 自带 Python 3.12 运行时 + 前端 + 全部附加组件 + Chromium,原生运行在飞牛主机 | 无需 Docker | + +- **Docker 版**实现为 FnOS `docker-project`:包体只含 `docker-compose.yaml` 与向导配置,运行时由飞牛拉取 GHCR 镜像。镜像已内置全部附加组件(`browser` 浏览器自动化 + `desktop` 桌面控制)与前端。 +- **本地版**实现为 FnOS 原生 `app`:包内自带独立 Python 3.12 运行时、Octop 全部依赖、前端构建产物、Playwright Chromium,以及 `data-share` 共享数据目录,直接以进程方式运行,不依赖 Docker。 + +> 两款包都在滚动发布 **`fnos-latest`**: + +## 目录结构 + +``` +fnos/ # Docker 版(docker-project) +├── manifest # 应用元信息(platform=all / 名称/版本/桌面入口等) +├── ICON.PNG / ICON_256.PNG +├── LICENSE # 复用仓库根 LICENSE(MIT) +├── cmd/ # 生命周期脚本(main / install_callback / config_callback 等) +├── config/ +│ ├── privilege # 权限声明(docker-octop 用户) +│ └── resource # 资源声明(docker-project + 数据共享目录) +├── wizard/ +│ └── install # 安装向导(可配置管理员账号/密码、日志级别、LLM 密钥) +├── app/ +│ ├── docker/ +│ │ └── docker-compose.yaml # 引用 ghcr.io/TencentCloud/octop:latest +│ └── ui/ +│ ├── config # 桌面图标入口 +│ └── images/icon-{64,256}.png +└── Dockerfile # 从仓库源码构建镜像,安装全部附加组件(browser + desktop) + +fnos-native/ # 本地版(非 Docker 的 FnOS 原生 app) +├── manifest # platform=all + 原生 app 元信息 +├── cmd/ # 生命周期脚本(main / install_callback / config_callback) +├── config/ +│ ├── privilege # 权限声明(root,用于补装 Chromium 系统库) +│ └── resource # data-share + usr-local-linker +├── app/ +│ ├── bin/octop # 启动器(用自带 Python 运行时启动 octop init/run) +│ └── ui/ # 桌面图标入口 +└── wizard/ # 安装/配置/卸载/升级向导 +``` + +## 工作机制 + +1. **源码同步**:`.github/workflows/zz-sync-upstream.yml` 每 6 小时把上游 `TencentCloud/Octop` 的更新合并进本仓 `main` 分支(使用仓库自带 `GITHUB_TOKEN`,无需 PAT)。 +2. **镜像构建**:`.github/workflows/zz-build-fpk.yml` 的 `image` job 在 `main` 更新时,用 `fnos/Dockerfile` 从仓库源码构建 Octop 镜像并推送到 `ghcr.io/TencentCloud/octop:latest`(含全部附加组件)。 +3. **安装包构建**:同一 workflow 的 `fpk` job 用 `scripts/build-fpk.sh` 把 `fnos/` 打成 Docker 版 `.fpk`;`native` job 用 python-build-standalone 构建 Python 3.12 运行时、安装全部依赖与 Playwright Chromium,打成本地版 `.fpk`(`continue-on-error`,失败不阻塞 Docker 版)。两者均以滚动发布 `fnos-latest` 提供下载。 + +## 本地构建 .fpk(无需 Docker) + +```bash +bash scripts/build-fpk.sh # 仅 Docker 版 → dist/octop-.fpk +bash scripts/build-fpk.sh docker # 仅 Docker 版 +bash scripts/build-fpk.sh native # 仅本地版 → dist/octop-native-.fpk +``` + +`.fpk` 为「双层 gzip tar」:外层含 `app.tgz / cmd / config / wizard / ICON.PNG / ICON_256.PNG / LICENSE / manifest`,内层 `app.tgz` 含 `app/` 内容。 + +## 在飞牛上安装 + +1. 飞牛「应用中心 → 设置 → 手动安装应用」选择对应 `.fpk`: + - 想用 Docker 跑、主机已装 Docker → 选 `octop-.fpk` + - 不想依赖 Docker、希望自带运行时原生运行 → 选 `octop-native-.fpk` +2. 安装向导中设置管理员账号/密码、日志级别、LLM 密钥(可选)。 +3. 安装完成后桌面出现「Octop AI 助手」图标,浏览器打开 `http://<设备IP>:8088`。 +4. Docker 版镜像首次会从 `ghcr.io/TencentCloud/octop:latest` 拉取;请确保该镜像为公开(workflow 已自动设为 public)。本地版无需联网拉镜像,首次启动会按需要补装 Chromium 系统库(需 root 权限,已尽力处理)。 + +> 服务端口固定为 `8088`(飞牛端口映射与桌面图标均据此)。附加组件(browser 浏览器自动化 + desktop 桌面控制)已在镜像中默认安装。 diff --git a/fnos/app/docker/docker-compose.yaml b/fnos/app/docker/docker-compose.yaml new file mode 100644 index 00000000..82e39a92 --- /dev/null +++ b/fnos/app/docker/docker-compose.yaml @@ -0,0 +1,20 @@ +services: + octop: + image: ghcr.io/TencentCloud/octop:latest + container_name: octop + restart: unless-stopped + pull_policy: always + ports: + - "8088:8088" + volumes: + - "/var/apps/octop/share/octop/data:/data/.octop" + environment: + - HOME=/data + - OCTOP_BIND_HOST=0.0.0.0 + - OCTOP_PORT=${OCTOP_PORT:-8088} + - OCTOP_LOG_LEVEL=${OCTOP_LOG_LEVEL:-info} + - OCTOP_DEFAULT_PASSWORD=${OCTOP_DEFAULT_PASSWORD:-Octop123} + - OCTOP_ADMIN_USERNAME=${OCTOP_ADMIN_USERNAME:-admin} + - OCTOP_ADMIN_DISPLAY_NAME=${OCTOP_ADMIN_DISPLAY_NAME:-Admin} + - OPENAI_API_KEY=${OPENAI_API_KEY:-} + - DASHSCOPE_API_KEY=${DASHSCOPE_API_KEY:-} diff --git a/fnos/app/ui/config b/fnos/app/ui/config new file mode 100644 index 00000000..a8260225 --- /dev/null +++ b/fnos/app/ui/config @@ -0,0 +1,16 @@ +{ + ".url": { + "octop.Application": { + "title": "OCTOP", + "icon": "images/icon_{0}.png", + "type": "iframe", + "protocol": "http", + "port": "8088", + "url": "/", + "allUsers": true, + "control": { + "accessPerm": "readonly" + } + } + } +} diff --git a/fnos/app/ui/images/icon_256.png b/fnos/app/ui/images/icon_256.png new file mode 100644 index 00000000..4c85d8c9 Binary files /dev/null and b/fnos/app/ui/images/icon_256.png differ diff --git a/fnos/app/ui/images/icon_64.png b/fnos/app/ui/images/icon_64.png new file mode 100644 index 00000000..84a23221 Binary files /dev/null and b/fnos/app/ui/images/icon_64.png differ diff --git a/fnos/cmd/config_callback b/fnos/cmd/config_callback new file mode 100644 index 00000000..b5d6dd7b --- /dev/null +++ b/fnos/cmd/config_callback @@ -0,0 +1,40 @@ +#!/bin/bash +# +# Octop — 配置后回调(重新配置时调用) +# 逻辑与 install_callback 一致:用新配置重写 .env。 +# 注意:修改管理员密码/账号不会改动已初始化的数据库(仅首次启动生效); +# 修改 LLM 密钥与日志级别会在容器重启后生效。 +# +set -euo pipefail + +ENV_FILE="${TRIM_APPDEST:-/var/apps/octop/target}/docker/.env" +mkdir -p "$(dirname "$ENV_FILE")" + +ADMIN_USER="${wizard_admin_username:-admin}" +ADMIN_PASS="${wizard_admin_password:-octop}" +LOG_LEVEL="${wizard_log_level:-info}" +OPENAI="${wizard_openai_api_key:-}" +DASHSCOPE="${wizard_dashscope_api_key:-}" + +sanitize() { printf '%s' "$1" | tr -d '\n\r"'"'"''; } + +ADMIN_USER=$(sanitize "$ADMIN_USER") +ADMIN_PASS=$(sanitize "$ADMIN_PASS") +LOG_LEVEL=$(sanitize "$LOG_LEVEL") +OPENAI=$(sanitize "$OPENAI") +DASHSCOPE=$(sanitize "$DASHSCOPE") + +{ + echo "OCTOP_PORT=8088" + echo "OCTOP_BIND_HOST=0.0.0.0" + echo "OCTOP_LOG_LEVEL=${LOG_LEVEL}" + echo "OCTOP_ADMIN_USERNAME=${ADMIN_USER}" + echo "OCTOP_DEFAULT_PASSWORD=${ADMIN_PASS}" + echo "OCTOP_ADMIN_DISPLAY_NAME=Admin" + echo "OPENAI_API_KEY=${OPENAI}" + echo "DASHSCOPE_API_KEY=${DASHSCOPE}" +} > "$ENV_FILE" + +chmod 600 "$ENV_FILE" +echo "[octop] 配置已更新: $ENV_FILE" > "${TRIM_TEMP_LOGFILE:-/dev/null}" 2>/dev/null || true +exit 0 diff --git a/fnos/cmd/config_init b/fnos/cmd/config_init new file mode 100644 index 00000000..18380b2c --- /dev/null +++ b/fnos/cmd/config_init @@ -0,0 +1,3 @@ +#!/bin/bash +# Octop — 配置前回调(无特殊处理) +exit 0 diff --git a/fnos/cmd/install_callback b/fnos/cmd/install_callback new file mode 100644 index 00000000..5b0bf121 --- /dev/null +++ b/fnos/cmd/install_callback @@ -0,0 +1,51 @@ +#!/bin/bash +# +# Octop — 安装后回调 +# 使用固定默认配置写入 docker-compose 同目录的 .env(安装向导已移除)。 +# docker compose 会自动加载该 .env,从而完成前后端参数配置。 +# +set -euo pipefail + +# 共享函数库(find_python312 / fix_ownership_and_perms / free_octop_ports) +# 打包时由 scripts/build-fpk.sh 注入;仓库内运行无此文件时跳过(函数缺失时再报错)。 +_OCTOP_COMMON=""$(dirname "$0")/common.sh"" +[ -f "$_OCTOP_COMMON" ] && source "$_OCTOP_COMMON" + +# 释放 Octop 端口(8088=Docker 版,8089=本地版),杀掉残留/孤儿进程,避免重装端口冲突。 + + +free_octop_ports + +ENV_FILE="${TRIM_APPDEST:-/var/apps/octop/target}/docker/.env" +mkdir -p "$(dirname "$ENV_FILE")" + +# 安装向导已移除,使用固定默认值(用户首次登录后可在控制台「设置」中修改)。 +ADMIN_USER="admin" +ADMIN_PASS="Octop123" +LOG_LEVEL="info" +OPENAI="" +DASHSCOPE="" + +# 去除可能破坏 .env 格式的换行/引号(安全起见) +sanitize() { printf '%s' "$1" | tr -d '\n\r"'"'"''; } + +ADMIN_USER=$(sanitize "$ADMIN_USER") +ADMIN_PASS=$(sanitize "$ADMIN_PASS") +LOG_LEVEL=$(sanitize "$LOG_LEVEL") +OPENAI=$(sanitize "$OPENAI") +DASHSCOPE=$(sanitize "$DASHSCOPE") + +{ + echo "OCTOP_PORT=8088" + echo "OCTOP_BIND_HOST=0.0.0.0" + echo "OCTOP_LOG_LEVEL=${LOG_LEVEL}" + echo "OCTOP_ADMIN_USERNAME=${ADMIN_USER}" + echo "OCTOP_DEFAULT_PASSWORD=${ADMIN_PASS}" + echo "OCTOP_ADMIN_DISPLAY_NAME=Admin" + echo "OPENAI_API_KEY=${OPENAI}" + echo "DASHSCOPE_API_KEY=${DASHSCOPE}" +} > "$ENV_FILE" + +chmod 600 "$ENV_FILE" +echo "[octop] 已写入配置: $ENV_FILE" > "${TRIM_TEMP_LOGFILE:-/dev/null}" 2>/dev/null || true +exit 0 diff --git a/fnos/cmd/install_init b/fnos/cmd/install_init new file mode 100644 index 00000000..5ac6eb60 --- /dev/null +++ b/fnos/cmd/install_init @@ -0,0 +1,3 @@ +#!/bin/bash +# Octop — 安装前回调(无特殊处理;docker-project 由应用中心管理) +exit 0 diff --git a/fnos/cmd/main b/fnos/cmd/main new file mode 100644 index 00000000..ec7cc62c --- /dev/null +++ b/fnos/cmd/main @@ -0,0 +1,41 @@ +#!/bin/bash +# +# Octop — FnOS 生命周期管理(docker-project) +# start/stop 由 fnOS 应用中心根据 config/resource 的 docker-project 自动管理; +# 本脚本仅负责状态检查(供系统健康检查轮询)。 +# + +FILE_PATH="${TRIM_APPDEST}/docker/docker-compose.yaml" + +is_docker_running() { + DOCKER_NAME="" + if [ -f "$FILE_PATH" ]; then + DOCKER_NAME=$(grep "container_name" "$FILE_PATH" | awk -F ':' '{print $2}' | xargs) + fi + if [ -n "$DOCKER_NAME" ]; then + docker inspect "$DOCKER_NAME" 2>/dev/null | grep -q "\"Status\": \"running\"," || return 1 + return 0 + fi + return 1 +} + +case $1 in +start) + # docker 应用由 appcenter 启动 + exit 0 + ;; +stop) + # docker 应用由 appcenter 停止 + exit 0 + ;; +status) + if is_docker_running; then + exit 0 + else + exit 3 + fi + ;; +*) + exit 1 + ;; +esac diff --git a/fnos/cmd/uninstall_callback b/fnos/cmd/uninstall_callback new file mode 100644 index 00000000..3f94e8b3 --- /dev/null +++ b/fnos/cmd/uninstall_callback @@ -0,0 +1,14 @@ +#!/bin/bash +# Octop — 卸载后回调 +# 根据卸载向导选择:完全删除时清理 data-share 用户数据;保留数据时不作处理。 +set -euo pipefail + +if [ "${wizard_delete_data:-no}" = "yes" ]; then + DATA_DIR="/vol1/@appshare/octop/data" + if [ -d "$DATA_DIR" ]; then + rm -rf "$DATA_DIR" 2>/dev/null || true + echo "[octop] 已删除用户数据: $DATA_DIR" > "${TRIM_TEMP_LOGFILE:-/dev/null}" 2>/dev/null || true + fi +fi + +exit 0 diff --git a/fnos/cmd/uninstall_init b/fnos/cmd/uninstall_init new file mode 100644 index 00000000..e77ba25e --- /dev/null +++ b/fnos/cmd/uninstall_init @@ -0,0 +1,26 @@ +#!/bin/bash +# Octop — 卸载前回调 +# 根据卸载向导 wizard_delete_data 的选择: +# - 保留数据:不清理 /vol1/@appshare/octop/data。 +# - 完全删除:在 uninstall_callback 中清理 data-share。 +# 同时释放 Octop 端口(8088=Docker 版,8089=本地版),避免卸载后残留进程占端口。 +set -euo pipefail + +# 共享函数库(find_python312 / fix_ownership_and_perms / free_octop_ports) +# 打包时由 scripts/build-fpk.sh 注入;仓库内运行无此文件时跳过(函数缺失时再报错)。 +_OCTOP_COMMON=""$(dirname "$0")/common.sh"" +[ -f "$_OCTOP_COMMON" ] && source "$_OCTOP_COMMON" + +log() { echo "[octop] $1" > "${TRIM_TEMP_LOGFILE:-/dev/null}" 2>/dev/null || true; } + + + +free_octop_ports + +if [ "${wizard_delete_data:-no}" = "yes" ]; then + log "用户选择完全删除数据,将在 uninstall_callback 中清理 /vol1/@appshare/octop/data" +else + log "用户选择保留数据,/vol1/@appshare/octop/data 将保留" +fi + +exit 0 diff --git a/fnos/cmd/upgrade_callback b/fnos/cmd/upgrade_callback new file mode 100644 index 00000000..8ad2c5b1 --- /dev/null +++ b/fnos/cmd/upgrade_callback @@ -0,0 +1,3 @@ +#!/bin/bash +# Octop — 更新后回调(无特殊处理;新版镜像由应用中心拉取并重建容器) +exit 0 diff --git a/fnos/cmd/upgrade_init b/fnos/cmd/upgrade_init new file mode 100644 index 00000000..d713779a --- /dev/null +++ b/fnos/cmd/upgrade_init @@ -0,0 +1,3 @@ +#!/bin/bash +# Octop — 更新前回调(无特殊处理;数据卷由 FnOS 托管,升级不丢数据) +exit 0 diff --git a/fnos/config/privilege b/fnos/config/privilege new file mode 100644 index 00000000..869ffb66 --- /dev/null +++ b/fnos/config/privilege @@ -0,0 +1,7 @@ +{ + "defaults": { + "run-as": "package" + }, + "username": "octop", + "groupname": "octop" +} diff --git a/fnos/config/resource b/fnos/config/resource new file mode 100644 index 00000000..98582818 --- /dev/null +++ b/fnos/config/resource @@ -0,0 +1,17 @@ +{ + "docker-project": { + "projects": [ + { + "name": "octop", + "path": "docker" + } + ] + }, + "data-share": { + "shares": [ + { + "name": "octop/data" + } + ] + } +} diff --git a/fnos/manifest b/fnos/manifest new file mode 100644 index 00000000..6c7a3daf --- /dev/null +++ b/fnos/manifest @@ -0,0 +1,13 @@ +appname=octop +version=0.9.15 +display_name=OCTOP +desc=腾讯开源的多用户多智能体 AI 助手平台,支持 Web 控制台、CLI、IM 渠道与定时任务,数据全部本地存储。

安装后使用:
在应用中心点击「打开」按钮即可进入 Web 控制台,默认地址 http://设备IP:8088。
初始管理员账号:admin,初始密码:Octop123(首次登录后请在头像菜单修改密码)。

首次登录后:
请在控制台「设置」中配置 LLM 模型、IM 渠道、远程桌面、浏览器等组件。

注意:Docker 版通过容器部署,会自动从 ghcr.io/TencentCloud/octop:latest 拉取镜像运行,已预装 browser / desktop 附加组件。 +source=thirdparty +platform=all +maintainer=TencentCloud OrcaKit +desktop_uidir=ui +desktop_applaunchname=octop.Application +service_port=8088 +checkport=true +ctl_stop=true +micro_app=true diff --git a/fnos/wizard/uninstall b/fnos/wizard/uninstall new file mode 100644 index 00000000..5f86ace4 --- /dev/null +++ b/fnos/wizard/uninstall @@ -0,0 +1,29 @@ +[ + { + "stepTitle": "卸载 OCTOP", + "items": [ + { + "type": "radio", + "field": "wizard_delete_data", + "label": "请选择数据清理方式。", + "initValue": "no", + "options": [ + { + "label": "保留数据(推荐 — 下次安装时可继续使用)", + "value": "no" + }, + { + "label": "完全删除(包括对话历史、文件和配置)", + "value": "yes" + } + ], + "rules": [ + { + "required": true, + "message": "请选择是否删除应用数据" + } + ] + } + ] + } +] diff --git a/scripts/build-fpk.sh b/scripts/build-fpk.sh new file mode 100644 index 00000000..6712f93f --- /dev/null +++ b/scripts/build-fpk.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +# ============================================================================= +# 构建 Octop 飞牛 FnOS 安装包 (.fpk) +# +# 直接使用官方 fnpack CLI 打包(fnpack 在生成 .fpk 前会校验 manifest / cmd / +# config / wizard / app 等结构,确保产物与飞牛 fnOS 安装校验完全一致)。 +# +# 用法(在仓库根目录执行): +# bash scripts/build-fpk.sh docker # 构建 Docker 版 -> dist/octop-.fpk +# bash scripts/build-fpk.sh native # 构建本地版(非Docker) -> dist/octop-native-.fpk +# bash scripts/build-fpk.sh # 两个都构建 +# +# 环境变量: +# FPK_NAME_PREFIX 输出文件名前缀,默认 "octop" +# 例如 FPK_NAME_PREFIX=Fnos-octop 会生成 Fnos-octop-.fpk +# FPK_ITER 迭代号,默认空 +# 例如 FPK_ITER=01 会生成 ...--01.fpk +# +# 说明: +# - Linux CI 下会自动下载 fnpack-1.2.3-linux-amd64; +# - 本地若已存在 .verify/fnpack(.exe) 则直接复用,不再联网下载。 +# - 版本号同时来自仓库根 pyproject.toml,并注入到 manifest 的 version 字段 +# (manifest 采用 key=value 无空格格式,故用 `^version=` 匹配)。 +# ============================================================================= +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +OUT="$ROOT/dist" + +# 使用仓库内的临时目录,避免 Windows 风格 TMPDIR 在 Git Bash 下被错误解析 +TMP="$(mktemp -d "$ROOT/.buildtmp.XXXXXX")" +cleanup() { rm -rf "$TMP" >/dev/null 2>&1 || true; } +trap cleanup EXIT + +VER="$(grep -m1 '^version' "$ROOT/pyproject.toml" | sed -E 's/.*"([0-9][0-9.]*[0-9])".*/\1/')" +[ -n "$VER" ] || { echo "无法从 pyproject.toml 解析版本"; exit 1; } +echo "[build-fpk] Octop 版本: $VER" + +# 输出文件名前缀与迭代号(由 CI 传入,实现 Fnos-octop-0.9.16-01.fpk 风格) +PREFIX="${FPK_NAME_PREFIX:-octop}" +ITER_SUFFIX="" +if [ -n "${FPK_ITER:-}" ]; then + ITER_SUFFIX="-$FPK_ITER" +fi +echo "[build-fpk] 包名前缀: $PREFIX, 迭代后缀: ${ITER_SUFFIX:-}" + +# --- 获取 fnpack CLI --- +FNPACK="" +if [ -x "$ROOT/.verify/fnpack.exe" ]; then + FNPACK="$ROOT/.verify/fnpack.exe" +elif [ -x "$ROOT/.verify/fnpack" ]; then + FNPACK="$ROOT/.verify/fnpack" +else + OS="$(uname -s)" + case "$OS" in + Linux) FNPACK_URL="https://static2.fnnas.com/fnpack/fnpack-1.2.3-linux-amd64" ;; + Darwin) FNPACK_URL="https://static2.fnnas.com/fnpack/fnpack-1.2.3-darwin-amd64" ;; + *) FNPACK_URL="https://static2.fnnas.com/fnpack/fnpack-1.2.3-windows-amd64" ;; + esac + FNPACK="$TMP/fnpack" + echo "[build-fpk] 下载 fnpack: $FNPACK_URL" + curl -fsSL -o "$FNPACK" "$FNPACK_URL" + chmod +x "$FNPACK" +fi +echo "[build-fpk] 使用 fnpack: $FNPACK" + +mkdir -p "$OUT" + +build_one() { + local KIND="$1" PKG OUTNAME + case "$KIND" in + docker) + PKG="$ROOT/fnos" + OUTNAME="${PREFIX}-${VER}${ITER_SUFFIX}.fpk" + ;; + native) + PKG="$ROOT/fnos-native" + OUTNAME="${PREFIX}-native-${VER}${ITER_SUFFIX}.fpk" + ;; + *) echo "未知类型: $KIND"; return 1 ;; + esac + + local BUILD="$TMP/$KIND" + rm -rf "$BUILD"; mkdir -p "$BUILD" + + # 复制整个包目录(manifest / cmd / config / wizard / app / ICON* / LICENSE) + cp -r "$PKG/." "$BUILD/" + + # 注入共享函数库(find_python312 / fix_ownership_and_perms / free_octop_ports), + # 仓库单一来源 scripts/fnos/common.sh,cmd/bin 脚本统一 source cmd/common.sh。 + if [ -f "$ROOT/scripts/fnos/common.sh" ]; then + cp "$ROOT/scripts/fnos/common.sh" "$BUILD/cmd/common.sh" + fi + + # 注入版本号到 manifest(manifest 为 key=value 无空格格式) + sed -i.bak "s/^version=.*/version=$VER/" "$BUILD/manifest" && rm -f "$BUILD/manifest.bak" + + echo "[build-fpk] fnpack 校验并打包 $KIND ..." + # fnpack 校验 manifest/cmd/config/wizard/app 后在当前目录生成 .fpk + # Windows 版 fnpack.exe 无法解析 Git-Bash 的 /c/... 路径,需转换为 Windows 原生路径。 + local DIR="$BUILD" + case "$FNPACK" in + *.exe) DIR="$(cygpath -w -m "$BUILD")" ;; + esac + ( cd "$BUILD" && "$FNPACK" build --directory "$DIR" ) + + local SRC + SRC="$(ls "$BUILD"/*.fpk 2>/dev/null | head -1)" + [ -n "$SRC" ] || { echo "[build-fpk] 未找到 fnpack 产物"; return 1; } + + mv "$SRC" "$OUT/$OUTNAME" + echo "[build-fpk] 产物: $OUT/$OUTNAME ($(stat -c%s "$OUT/$OUTNAME") bytes)" + echo "[build-fpk] 外层内容:" + tar -tzf "$OUT/$OUTNAME" +} + +if [ $# -eq 0 ]; then + build_one docker + build_one native +else + for k in "$@"; do build_one "$k"; done +fi diff --git a/scripts/fnos/common.sh b/scripts/fnos/common.sh new file mode 100644 index 00000000..b7749829 --- /dev/null +++ b/scripts/fnos/common.sh @@ -0,0 +1,126 @@ +#!/bin/bash +# +# Octop FnOS 打包共享函数库。 +# 仓库唯一来源:scripts/fnos/common.sh +# 打包时由 scripts/build-fpk.sh 注入到包内 cmd/common.sh; +# fnos/ 与 fnos-native/ 的 cmd 脚本及 app/bin 脚本统一 source 本文件, +# 避免 find_python312 / fix_ownership_and_perms / free_octop_ports 重复维护。 +# +set -u + +# --------------------------------------------------------------------------- +# 释放 Octop 端口(8088=Docker 版,8089=本地版)并清理本应用残留进程。 +# 仅清理:(1) 占用 Octop 端口的进程;(2) 本安装目录(TRIM_APPDEST)下的 +# octop 服务进程。不使用宽泛的 `pgrep -f octop`,避免误杀其它用户/其它 +# 安装路径下的同名进程。 +# --------------------------------------------------------------------------- +free_octop_ports() { + local port pid pids pat appdir + for port in 8088 8089; do + pids="$(ss -ltnp 2>/dev/null | grep -E "[:.]${port}([[:space:]]|$)" | sed -n 's/.*pid=\([0-9]*\).*/\1/p' | sort -u)" || true + if [ -z "$pids" ] && command -v fuser >/dev/null 2>&1; then + pids="$(fuser "${port}/tcp" 2>/dev/null | tr -cs '[:digit:]' ' ')" || true + fi + if [ -z "$pids" ] && command -v lsof >/dev/null 2>&1; then + pids="$(lsof -ti tcp:"$port" 2>/dev/null)" || true + fi + for pid in $pids; do + [ -n "$pid" ] || continue + if kill -TERM "$pid" 2>/dev/null; then + echo "[octop] 已发送 TERM 给占用 ${port} 的进程 ${pid}" > "${TRIM_TEMP_LOGFILE:-/dev/null}" 2>/dev/null || true + fi + done + sleep 1 + for pid in $pids; do + [ -n "$pid" ] || continue + if kill -0 "$pid" 2>/dev/null; then + kill -KILL "$pid" 2>/dev/null || true + echo "[octop] 已强制 KILL 占用 ${port} 的进程 ${pid}" > "${TRIM_TEMP_LOGFILE:-/dev/null}" 2>/dev/null || true + fi + done + done + + # 兜底:只按本安装目录精确匹配,防止误杀其它实例。 + appdir="${TRIM_APPDEST:-/var/apps/octop-native}" + for pat in "$appdir/bin/octop" "$appdir/app/bin/octop"; do + pids="$(pgrep -f -- "$pat" 2>/dev/null | tr '\n' ' ')" || true + [ -z "$pids" ] && continue + echo "[octop] 发现本应用残留服务进程($pat): $pids,准备清理" > "${TRIM_TEMP_LOGFILE:-/dev/null}" 2>&1 || true + for pid in $pids; do + kill -TERM "$pid" 2>/dev/null || true + done + sleep 1 + for pid in $pids; do + if kill -0 "$pid" 2>/dev/null; then + kill -KILL "$pid" 2>/dev/null || true + echo "[octop] 已强制 KILL 残留服务进程 ${pid}" > "${TRIM_TEMP_LOGFILE:-/dev/null}" 2>&1 || true + fi + done + done +} + +# --------------------------------------------------------------------------- +# 修正数据目录与 .env 的属主/权限。 +# install_callback/config_callback 以 root 写 .env,若不 chown 给运行用户, +# 服务(octop-native)启动时 `. "$PKGVAR/.env"` 会 Permission denied。 +# 若目录曾带 ACL,单纯 chmod 会把 mask 压成 ---,需 setfacl -b 清除。 +# --------------------------------------------------------------------------- +fix_ownership_and_perms() { + local pkgvar="$1" envfile="$2" + local octop_user="octop-native" + id "$octop_user" >/dev/null 2>&1 || { + echo "[octop] 警告:${octop_user} 账户不存在,跳过数据目录 chown(服务将回退以 root 运行)" > "${TRIM_TEMP_LOGFILE:-/dev/null}" 2>&1 || true + return 0 + } + + # 1) 应用数据目录与 .env 改属主为运行用户 + chown -R "$octop_user:$octop_user" "$pkgvar" 2>/dev/null || true + chmod 700 "$pkgvar" 2>/dev/null || true + [ -f "$envfile" ] && chmod 600 "$envfile" 2>/dev/null || true + + # 2) 清除 ACL,避免 chmod 把 mask 压成 --- 导致仍读不到 + if command -v setfacl >/dev/null 2>&1; then + setfacl -b "$pkgvar" 2>/dev/null || true + [ -f "$envfile" ] && setfacl -b "$envfile" 2>/dev/null || true + fi + + # 3) 共享数据目录(@appshare):确保属主正确且可进入 + if [ -n "${TRIM_DATA_SHARE_PATHS:-}" ]; then + local ds="${TRIM_DATA_SHARE_PATHS%%:*}" + chown -R "$octop_user:$octop_user" "$ds" 2>/dev/null || true + local share_root="$ds" + while [ "$share_root" != "/" ] && [ "$(basename "$(dirname "$share_root")")" != "@appshare" ]; do + share_root="$(dirname "$share_root")" + done + chown "$octop_user:$octop_user" "$share_root" 2>/dev/null || true + chmod 755 "$share_root" 2>/dev/null || true + if command -v setfacl >/dev/null 2>&1; then + setfacl -b "$share_root" 2>/dev/null || true + setfacl -b "$ds" 2>/dev/null || true + fi + fi + + echo "[octop] 已修正数据目录/.env 属主与权限(${octop_user}:${octop_user})" > "${TRIM_TEMP_LOGFILE:-/dev/null}" 2>&1 || true +} + +# --------------------------------------------------------------------------- +# 查找飞牛系统上的 Python 3.12(应用商店提供)。 +# --------------------------------------------------------------------------- +find_python312() { + local cand py + for cand in \ + /var/apps/python312/target/bin/python3.12 \ + /usr/local/bin/python3.12 \ + /var/apps/python3.12/bin/python3.12 \ + python3.12 + do + if command -v "$cand" >/dev/null 2>&1; then + py="$(command -v "$cand")" + if "$py" -c 'import sys; assert sys.version_info[:2] == (3,12)' >/dev/null 2>&1; then + printf '%s' "$py" + return 0 + fi + fi + done + return 1 +}