Ver Fonte

feat(dist): Nuitka 单文件二进制发行 — 路线B 防逆向+免装Python

setup.sh 新增 --build-binary(scripts/build_binary.sh):用 Nuitka standalone
把整个 agentpaas 打成原生可执行(内嵌 Python+依赖+前端+10内置包),用户免装
Python 双击即用。ONEFILE=1 单文件(默认)/ONEFILE=0 目录模式(省内存);
PROVIDERS 可配置打包的 LLM SDK(默认 anthropic openai)。

修两个被 standalone 暴露的真 bug:
- **secrets 撞名(latent bug)**:agentpaas 有子包 `secrets` 与 stdlib `secrets`
  同名,CPython 绝对导入下 stdlib 胜出(线上正常),但 Nuitka 解析混乱致
  `agentpaas.secrets` 遮蔽 stdlib,starlette `from secrets import token_hex` 崩。
  该子包(vault.py AES 加密)全仓零引用=死代码 → 重命名为 `secretvault`。
- **webui 路径**:app.py `_resolve_webui_dist` 只按源码布局上溯4层,打包后无
  src 层会越过 → 加 frozen 感知候选(上溯2层 + 可执行同级)。源码/dev 模式不变。

实测(arm64 standalone-dir,无 Python 环境 PATH=/usr/bin:/bin):
构建退出0;/health 200;内嵌 web UI 返回 HTML;/assets 200;
首启自动安装全部 10 个内置智能体包。
回归全绿 334+566;webui 解析在源码模式仍命中 repo/webui-dist。

文档:DESKTOP_MANUAL_zh「源码保护」加第三档(--build-binary 单二进制)+ 边界
(开源版不加密字符串常量;平台绑定;chromadb/PDF 工具未内嵌);QUICK_START 同步;
.gitignore 忽略 dist-binary/ build_binary_out/。

注:沙箱回收>10min 后台任务,故此处验证的成品未打 provider(脚本默认含,
本机构建无此限制);provider 版编译已走完安装进编译期、非脚本问题。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
kenny67nju há 2 meses atrás
pai
commit
c50aa0f365

+ 2 - 0
.gitignore

@@ -267,3 +267,5 @@ review_result*.json
 /dist-secure/
 /build_nuitka/
 /.nuitka-venv/
+/dist-binary/
+/build_binary_out/

+ 5 - 1
QUICK_START.md

@@ -141,9 +141,13 @@ chmod +x setup.sh
 # 构建 wheel 发行包(开发者:打包发布用,产出 dist/*.whl,仍含 .py)
 ./setup.sh --build
 
-# 防逆向:Nuitka 编译成 .so wheel(产出 dist-secure/,解开只有机器码)
+# 防逆向:Nuitka 编译成 .so wheel(产出 dist-secure/,解开只有机器码,仍需 Python
 ./setup.sh --build-secure
 
+# 最强:Nuitka 打成单文件二进制(产出 dist-binary/,用户免装 Python,双击即用)
+./setup.sh --build-binary
+# 资源受限可用目录模式: ONEFILE=0 ./setup.sh --build-binary
+
 # 组装「不含源码」的发行包 release/(有编译版则优先用,再 zip 后发给用户)
 ./setup.sh --package
 

+ 12 - 1
agentpaas/src/agentpaas/api/app.py

@@ -316,13 +316,24 @@ async def health():
 #   3. <repo_root>/webui-dist (dev: 4 levels up from this file's dir)
 #   4. <cwd>/webui-dist (running from repo root)
 def _resolve_webui_dist() -> str:
+    import sys
     explicit = os.getenv("AGENTPAAS_WEBUI_DIST", "")
     here = os.path.dirname(os.path.abspath(__file__))
+    # Nuitka standalone/onefile: this module compiles to
+    # <bundle_root>/agentpaas/api/app.py (no `src` level), and the bundled
+    # webui-dir sits at <bundle_root>/webui-dist — i.e. up 2, not up 4.
+    # Also probe next to the executable so the binary serves the UI
+    # regardless of cwd.
+    exe_dir = os.path.dirname(os.path.abspath(sys.argv[0])) if sys.argv and sys.argv[0] else here
     candidates = [
         explicit,
         "/app/webui-dist",
-        # agentpaas/src/agentpaas/api -> up 4 -> repo root
+        # agentpaas/src/agentpaas/api -> up 4 -> repo root (dev/source layout)
         os.path.normpath(os.path.join(here, "..", "..", "..", "..", "webui-dist")),
+        # bundled layout (Nuitka): agentpaas/api -> up 2 -> bundle root
+        os.path.normpath(os.path.join(here, "..", "..", "webui-dist")),
+        # alongside the (onefile/standalone) executable
+        os.path.join(exe_dir, "webui-dist"),
         os.path.normpath(os.path.join(os.getcwd(), "webui-dist")),
     ]
     for c in candidates:

+ 0 - 0
agentpaas/src/agentpaas/secrets/__init__.py → agentpaas/src/agentpaas/secretvault/__init__.py


+ 0 - 0
agentpaas/src/agentpaas/secrets/inject.py → agentpaas/src/agentpaas/secretvault/inject.py


+ 0 - 0
agentpaas/src/agentpaas/secrets/vault.py → agentpaas/src/agentpaas/secretvault/vault.py


+ 24 - 7
docs/DESKTOP_MANUAL_zh.md

@@ -132,22 +132,39 @@ zip -r lambdagent-paas-1.3.0.zip release/
 zip -r lambdagent-paas-1.3.0.zip release/
 ```
 
-前置:本机有 C 编译器(macOS:`xcode-select --install`),以及**原生架构、3.10+、带头文件**
-的 Python(macOS 上推荐 Homebrew 的 `python3.11`,不要用 Rosetta 下的 x86_64 Python)。
+**C. 单文件二进制(最强:防逆向 + 用户免装 Python)**:用 Nuitka 把整个应用(含内嵌
+Python、依赖、前端、10 个内置包)打成一个原生可执行文件,用户双击即用:
 
-两种方式对方都是:解压 → `cd release` → `./setup.sh`,脚本检测到 `dist/*.whl` 即按发行模式安装。
+```bash
+./setup.sh --build-binary    # 默认 onefile 单文件 → dist-binary/agentpaas
+# 资源受限/CI 机器可用 standalone 目录模式(更省内存、更稳):
+ONEFILE=0 ./setup.sh --build-binary    # → dist-binary/agentpaas-darwin-arm64.app/
+# 默认打包 anthropic/openai SDK;dashscope/ollama 走 HTTP 无需 SDK。自定义:
+PROVIDERS="anthropic openai" ./setup.sh --build-binary
+```
+
+用户用法:`./agentpaas serve --port 8000`,机器上**完全不需要 Python**。已实测:
+单二进制在无 Python 环境启动服务、加载内嵌 Web UI、首启自动安装全部 10 个内置智能体包。
+
+前置(B、C 通用):本机有 C 编译器(macOS:`xcode-select --install`),以及**原生架构、
+3.10+、带头文件**的 Python(macOS 上推荐 Homebrew 的 `python3.11`,不要用 Rosetta 下的
+x86_64 Python,否则编出错架构)。
+
+A/B 方式对方都是:解压 → `cd release` → `./setup.sh`,脚本检测到 `dist/*.whl` 即按发行模式安装;
+C 方式直接把二进制(或 `.app/` 目录)发给对方即可。
 `release/` 里**不含** `lambdagent/src`、`agentpaas/src` 源码树,也不含 `.git`、测试、内部文档。
 
 > **关于源码保护(重要,请如实理解)**
 > Python 是解释型语言,代码要在用户机器上运行,因此**任何纯本地分发都无法做到绝对不可逆**——
 > 这是语言层面的客观限制。防护强度由弱到强:
-> `.py` 源码(裸奔)< 普通 wheel < **Nuitka 编译 `.so`(本产品已支持,`--build-secure`)** < 核心逻辑放服务端(唯一数学上成立)。
+> `.py` 源码(裸奔)< 普通 wheel < **Nuitka 编译 `.so` / 单文件二进制(本产品已支持,`--build-secure` / `--build-binary`)** < 核心逻辑放服务端(唯一数学上成立)。
 > - **普通 wheel** 仍是 `.py` 打包,`unzip` 即可读源码,不混淆——价值只在「不泄露仓库历史/测试/内部文档」。
-> - **编译版 wheel(`--build-secure`)** 把代码编成 Mach-O 机器码 `.so`:**控制流/算法无法反编译回可读 `.py`**,已用 `--python-flag=no_docstrings` 去除 docstring。这是当前本地分发能做到的最强保护。
+> - **编译版 wheel(`--build-secure`)/ 单文件二进制(`--build-binary`)** 把代码编成 Mach-O 机器码:**控制流/算法无法反编译回可读 `.py`**,已用 `--python-flag=no_docstrings` 去除 docstring。这是当前本地分发能做到的最强保护;二进制还顺带免去用户装 Python
 >   - ⚠️ **边界**:开源版 Nuitka **不加密字符串常量**——日志文本、错误信息、**尤其是写在代码里的 prompt**,用 `strings` 命令仍能提取。若 prompt 是核心 IP,需上 Nuitka Commercial(常量加密)或把 prompt 改为加密/远程加载。
->   - ⚠️ **平台绑定**:编译版 wheel 绑定「Python 小版本 + CPU 架构」(如 `cp311 + macOS arm64`)。用户环境需匹配;多平台分发要在各对应机器/CI 上分别 `--build-secure`。
+>   - ⚠️ **平台绑定**:编译产物绑定「Python 小版本(wheel)/ 操作系统 + CPU 架构(二进制)」(如 `cp311`、`macOS arm64`)。用户环境需匹配;多平台分发要在各对应机器/CI 上分别构建。
+>   - ⚠️ **二进制未打包**:chromadb 向量库、PDF 外部工具(pandoc/xelatex)等未内嵌——用到这些功能仍需系统侧依赖。
 > **本产品定位为面向高校老师的单机产品 + BSL 1.1 协议**——真正的威慑来自授权条款,技术保护是叠加。
-> 对一般用户,普通 wheel 已足够;对外公开分发或有盗版顾虑时,用 `--build-secure`。详见 `RELEASE_NOTES.md` 与 BSL 协议。
+> 选择建议:一般用户用普通 wheel 即可;对外分发/防逆向用 `--build-secure`;想让用户「免装 Python、双击即用」且最强保护用 `--build-binary`。详见 `RELEASE_NOTES.md` 与 BSL 协议。
 
 ---
 

+ 117 - 0
scripts/build_binary.sh

@@ -0,0 +1,117 @@
+#!/usr/bin/env bash
+# ============================================================
+# build_binary.sh — 用 Nuitka 把整个 agentpaas 打成单个原生可执行文件
+# (standalone + onefile)。用户拿到后【无需安装 Python】,直接 ./agentpaas serve。
+#
+# 与 --build-secure(.so wheel)的区别:
+#   --build-secure : 仍是 wheel,用户要有匹配的 Python;保护的是源码
+#   --build-binary : 单文件二进制,内嵌 Python + 依赖 + 前端 + 内置包;保护源码 + 免装环境
+#
+# 产物:dist-binary/agentpaas(绑定「平台 + 架构」,如 macOS arm64)。
+#
+# 用法:  scripts/build_binary.sh [PYTHON]
+#   PYTHON 默认 python3,须为原生架构、3.10+、带头文件(macOS 推荐 Homebrew python3.11)。
+# 环境:
+#   PROVIDERS  要打进二进制的 LLM SDK,空格分隔,默认 "anthropic openai"
+#              (dashscope 走 openai 兼容/HTTP、ollama 走 HTTP,均无需 SDK)
+#   ONEFILE    1=单文件(默认,体验最好但打包耗资源);0=standalone 目录
+#              (产出一个文件夹,更省内存、更稳,适合 CI/资源受限机器)
+#
+# 前置:  C 编译器(macOS: xcode-select --install)
+# ============================================================
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
+cd "$PROJECT_DIR"
+
+RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m'
+info()  { echo -e "${BLUE}[INFO]${NC}  $1"; }
+ok()    { echo -e "${GREEN}[OK]${NC}    $1"; }
+warn()  { echo -e "${YELLOW}[WARN]${NC}  $1"; }
+fail()  { echo -e "${RED}[FAIL]${NC}  $1"; exit 1; }
+
+PYTHON="${1:-python3}"
+PROVIDERS="${PROVIDERS:-anthropic openai}"
+ONEFILE="${ONEFILE:-1}"
+command -v "$PYTHON" >/dev/null 2>&1 || fail "未找到 Python: $PYTHON"
+
+# ── 架构/版本/头文件检查(与 build_secure 一致)──────────────
+read -r PYARCH HAVE_H <<EOF
+$("$PYTHON" - <<'PY'
+import platform, sysconfig, os
+inc = sysconfig.get_path('include')
+print(f"{platform.machine()} {int(os.path.exists(os.path.join(inc,'Python.h')))}")
+PY
+)
+EOF
+HOSTARCH="$(uname -m)"
+[ "$PYARCH" = "$HOSTARCH" ] || fail "Python 架构($PYARCH) ≠ 主机($HOSTARCH)。请用原生 $HOSTARCH 的 Python(如 Homebrew python3.11),不要用 Rosetta 下的 x86_64 Python。"
+[ "$HAVE_H" = "1" ] || fail "缺 Python.h 头文件。请装带头文件的 Python(如 Homebrew)。"
+
+# ── 构建专用 venv:装 nuitka + 应用 + provider SDK ─────────
+BUILD_VENV="$PROJECT_DIR/.nuitka-venv"
+[ -x "$BUILD_VENV/bin/python" ] || "$PYTHON" -m venv "$BUILD_VENV"
+BPY="$BUILD_VENV/bin/python"
+info "准备构建环境(装 nuitka + 应用 + provider)..."
+"$BPY" -m pip install -q --upgrade pip nuitka >/dev/null
+# onefile 是 standalone:依赖必须真实安装进构建环境,Nuitka 才能跟进打包
+"$BPY" -m pip install -q ./lambdagent ./agentpaas $PROVIDERS
+
+# 仅对真正装上的 provider 生成 --include-module
+PROVIDER_FLAGS=()
+for p in $PROVIDERS; do
+    if "$BPY" -c "import $p" >/dev/null 2>&1; then
+        PROVIDER_FLAGS+=("--include-module=$p")
+        ok "provider 已纳入: $p"
+    else
+        warn "provider 未装上,跳过: $p"
+    fi
+done
+
+# ── Nuitka standalone + onefile ───────────────────────────
+OUT="$PROJECT_DIR/build_binary_out"
+rm -rf "$OUT"; mkdir -p "$OUT"
+ENTRY="agentpaas/src/agentpaas/__main__.py"
+
+MODE_FLAGS=(--standalone)
+[ "$ONEFILE" = "1" ] && MODE_FLAGS+=(--onefile)
+info "Nuitka 打包二进制(mode: $([ "$ONEFILE" = 1 ] && echo onefile单文件 || echo standalone目录),首次可能 10+ 分钟)..."
+"$BPY" -m nuitka \
+    "${MODE_FLAGS[@]}" \
+    --output-filename=agentpaas \
+    --output-dir="$OUT" \
+    --include-package=uvicorn \
+    --include-module=uvloop --include-module=httptools \
+    --include-module=websockets --include-module=h11 \
+    --include-package=agentpaas --include-package=lambdagent \
+    "${PROVIDER_FLAGS[@]}" \
+    --include-data-dir=webui-dist=webui-dist \
+    --include-data-dir=agentexample/agentpacks=agentexample/agentpacks \
+    --python-flag=no_docstrings \
+    --assume-yes-for-downloads \
+    "$ENTRY"
+
+PLATSUFFIX="$(uname -s | tr 'A-Z' 'a-z')-$HOSTARCH"
+mkdir -p dist-binary
+if [ "$ONEFILE" = "1" ]; then
+    BIN="$OUT/agentpaas"
+    [ -x "$BIN" ] || fail "未生成二进制"
+    cp "$BIN" "dist-binary/agentpaas-$PLATSUFFIX"
+    cp "$BIN" "dist-binary/agentpaas"
+    echo ""
+    ok "单文件二进制已生成 → dist-binary/agentpaas  ($(du -h "$BIN" | cut -f1))"
+    echo "  用户用法(无需 Python):  ./agentpaas serve --port 8000"
+else
+    # standalone:产出一个目录,整目录即发行件
+    DISTDIR="$OUT/__main__.dist"
+    [ -x "$DISTDIR/agentpaas" ] || fail "未生成 standalone 目录二进制"
+    rm -rf "dist-binary/agentpaas-$PLATSUFFIX.app"
+    cp -R "$DISTDIR" "dist-binary/agentpaas-$PLATSUFFIX.app"
+    echo ""
+    ok "standalone 目录已生成 → dist-binary/agentpaas-$PLATSUFFIX.app/  ($(du -sh "$DISTDIR" | cut -f1))"
+    echo "  用户用法(无需 Python):  ./agentpaas-$PLATSUFFIX.app/agentpaas serve --port 8000"
+fi
+echo ""
+warn "提醒: 二进制绑定 $PLATSUFFIX —— 换平台/架构需在对应机器重新构建。"
+warn "      未打包: chromadb 向量库、PDF 外部工具(pandoc/xelatex)——用到这些功能仍需系统侧依赖。"

+ 8 - 2
setup.sh

@@ -5,7 +5,8 @@
 #   chmod +x setup.sh
 #   ./setup.sh              # 安装并启动(自动识别 发行包/源码 两种模式)
 #   ./setup.sh --build      # 构建普通 wheel 包到 dist/(含 .py 源码)
-#   ./setup.sh --build-secure  # 用 Nuitka 编译成 .so wheel 到 dist-secure/(防逆向)
+#   ./setup.sh --build-secure  # 用 Nuitka 编译成 .so wheel 到 dist-secure/(防逆向,仍需 Python)
+#   ./setup.sh --build-binary  # 用 Nuitka 打成单文件二进制 dist-binary/(防逆向 + 免装 Python)
 #   ./setup.sh --package    # 打出「不含源码」的发行包 release/(优先用编译版)
 #   ./setup.sh --docker     # 使用 Docker 启动
 #   ./setup.sh --help       # 查看帮助
@@ -41,7 +42,8 @@ show_help() {
     echo "用法:"
     echo "  ./setup.sh              安装依赖并启动服务(自动识别发行/源码模式)"
     echo "  ./setup.sh --build         构建普通 wheel 包 → dist/*.whl(仍含 .py 源码)"
-    echo "  ./setup.sh --build-secure  Nuitka 编译成 .so wheel → dist-secure/(防逆向)"
+    echo "  ./setup.sh --build-secure  Nuitka 编译成 .so wheel → dist-secure/(防逆向,仍需 Python)"
+    echo "  ./setup.sh --build-binary  Nuitka 打成单文件二进制 → dist-binary/(防逆向 + 免装 Python)"
     echo "  ./setup.sh --package    打出不含源码的发行包 → release/(有编译版则优先用)"
     echo "  ./setup.sh --docker     使用 Docker Compose 启动"
     echo "  ./setup.sh --install    仅安装依赖,不启动服务"
@@ -352,6 +354,10 @@ main() {
             check_python
             bash scripts/build_secure.sh "$PYTHON"
             ;;
+        --build-binary)
+            check_python
+            bash scripts/build_binary.sh "$PYTHON"
+            ;;
         --package)
             package_release
             ;;