From d4c5e808c735198463f79edc5cdabf28a4505950 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sat, 12 Sep 2026 08:10:44 +0800 Subject: [PATCH] =?UTF-8?q?feat(packaging):=20=E6=A8=A1=E5=9E=8B=E4=B8=8E?= =?UTF-8?q?=20ONNX=20Runtime=20=E9=9A=8F=20server/full=20=E5=8C=85?= =?UTF-8?q?=E5=8F=91=E5=B8=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 模型与运行库是发行版能力的一部分,不做成「装完再自己下载」: - package-linux.sh:新增 stage_multimodal_assets(),打 server/full 前校验产物 SHA256SUMS、逐文件非空、运行库架构与目标一致,缺一即失败;client 包不含。 顺带修掉三个让打包在最后一步才炸的既有缺陷: · 版本串直接取 git describe(v1.0.0-68-gxxx-dirty)不是合法包版本——deb 要求 数字开头、rpm 不允许 '-'。以前只有显式 VERSION=1.0.3 才打得出来;默认路径 从来没通过过。现在归一化,非数字开头时显式报错。 · 三处 mktemp -d 落在 /tmp(本机 9.8GB tmpfs),而 staging 要复制 719MB 模型, 中途 ENOSPC;报错文本指向某个 .onnx 文件,看着像资产坏了。改为落在与构建产物 同盘的 build/.stage-tmp。 · 开工前删掉旧的 SHA256SUMS:失败时脚本直接退出、不重算,留着像在为残缺产物背书。 - setup.sh:把包内 /usr/lib/homeagent/models/chinese-clip-vit-b16-onnx 软链到 /models/…(不复制 754MB、保持 dataDir 可迁移、已有自定义目录不覆盖) - homeagent.service:ExecStart 改 /usr/bin/homed(deb 装在那里,此前写 /usr/local/bin, 装了也不会被 unit 用上)、加 ONNXRUNTIME_DIR 与 StateDirectory、MemoryMax 2G→8G (实测常驻约 4.5GB,2G 会在首次全量建索引时被 cgroup OOM) - control-{full,server}:补 libstdc++6 / libgcc-s1(libonnxruntime.so 需要) - providers/{chineseclip,qwen3vl}:findOnnxLib 支持 ONNXRUNTIME_DIR / ONNX_ML_DIR 与包内 /usr/lib/homeagent/onnxruntime,随包的运行库才真的会被用上 - postinst:修掉两个让「装完即用」失效的点——它检查 /lib/systemd/system/ 下的 unit 而 deb 装到 /etc/systemd/system/,于是 daemon-reload/enable **从未执行**;以及 setup.sh 的失败被 `|| true` 吞掉(正是 initconfig 静默缺陷被藏住的原因)。现在 三个候选路径都查、失败可见并给出补救命令、首装 start / 升级 restart。 - docs/zh/multimodal-space.md:新增「随包分发」一节,并修正播种判据的说明 验收(从真实 deb 走一遍,不是读脚本): - 包内 initconfig 已是动态链接,凭据真的写进 config.db - 解包 → 按 postinst 顺序跑 setup.sh → 包内 homed 冷启动: multimodal space active: provider=chineseclip dim=512 fp=cd2a495cf990 modalities=[text image] - 全新安装的默认值确实被播种(core.plugin.dir / provider / model_dir 都在) - 真实对话拿到回复(3.4s,回复中含唯一标记) - 包内模型 SHA256SUMS 5/5 通过;包内 ORT 与源同 sha256;server 包 722MB (旧版 17MB,差额即模型与运行库);full 包同样含全部资产;client 包不含 --- deploy/homeagent.service | 8 +- deploy/packaging/linux/deb/control-full | 2 +- deploy/packaging/linux/deb/control-server | 2 +- deploy/packaging/linux/deb/postinst | 53 ++++++- deploy/packaging/linux/setup.sh | 14 +- deploy/packaging/package-linux.sh | 174 ++++++++++++++++++---- docs/zh/multimodal-space.md | 39 ++++- providers/chineseclip/embedder.go | 12 +- providers/qwen3vl/embedder.go | 14 +- 9 files changed, 269 insertions(+), 49 deletions(-) diff --git a/deploy/homeagent.service b/deploy/homeagent.service index 1a564b6..1a90826 100644 --- a/deploy/homeagent.service +++ b/deploy/homeagent.service @@ -6,9 +6,11 @@ Wants=network-online.target [Service] Type=simple -ExecStart=/usr/local/bin/homed -data /var/lib/homeagent +ExecStart=/usr/bin/homed -data /var/lib/homeagent Restart=always RestartSec=10 +Environment=ONNXRUNTIME_DIR=/usr/lib/homeagent/onnxruntime +StateDirectory=homeagent StartLimitBurst=3 StartLimitInterval=60s @@ -26,7 +28,9 @@ DeviceAllow=/dev/dsp rw # Resource limits LimitNOFILE=65536 LimitNPROC=256 -MemoryMax=2G +# Chinese-CLIP 本身实测约 1.15GB;加文档稠密索引、词向量、插件与 +# ORT arena 后生产实例约 4.5GB。2GB 会在首次全量建索引时被 cgroup OOM。 +MemoryMax=8G CPUQuota=100% [Install] diff --git a/deploy/packaging/linux/deb/control-full b/deploy/packaging/linux/deb/control-full index 9cac46b..cc1d487 100644 --- a/deploy/packaging/linux/deb/control-full +++ b/deploy/packaging/linux/deb/control-full @@ -3,7 +3,7 @@ Version: VERSION_PLACEHOLDER Architecture: ARCH_PLACEHOLDER Maintainer: HomeAgent Team Installed-Size: INSTALLED_SIZE_PLACEHOLDER -Depends: libc6 (>= 2.28) +Depends: libc6 (>= 2.28), libstdc++6, libgcc-s1 Section: utils Priority: optional Homepage: https://github.com/trueagent/HomeAgent diff --git a/deploy/packaging/linux/deb/control-server b/deploy/packaging/linux/deb/control-server index c15c858..00c4fac 100644 --- a/deploy/packaging/linux/deb/control-server +++ b/deploy/packaging/linux/deb/control-server @@ -3,7 +3,7 @@ Version: VERSION_PLACEHOLDER Architecture: ARCH_PLACEHOLDER Maintainer: HomeAgent Team Installed-Size: INSTALLED_SIZE_PLACEHOLDER -Depends: libc6 (>= 2.28) +Depends: libc6 (>= 2.28), libstdc++6, libgcc-s1 Section: utils Priority: optional Homepage: https://github.com/trueagent/HomeAgent diff --git a/deploy/packaging/linux/deb/postinst b/deploy/packaging/linux/deb/postinst index 17eedef..325853f 100644 --- a/deploy/packaging/linux/deb/postinst +++ b/deploy/packaging/linux/deb/postinst @@ -2,24 +2,63 @@ set -e SERVICE_NAME="homeagent" -SERVICE_FILE="/lib/systemd/system/${SERVICE_NAME}.service" HOMED_BIN="/usr/bin/homed" DATA_DIR="/var/lib/homeagent" +SETUP_SH="/usr/lib/homeagent/setup.sh" + +# unit 由本包装到 /etc/systemd/system/,而旧 postinst 只查 +# /lib/systemd/system/(merged-usr 下等于 /usr/lib/systemd/system,那里没有 +# 这个文件)——于是 daemon-reload 与 enable **从未执行过**:装完不会开机自启, +# 而 postinst 全程无报错。这里三个候选位置都看一下。 +find_unit() { + for p in "/etc/systemd/system/${SERVICE_NAME}.service" \ + "/usr/lib/systemd/system/${SERVICE_NAME}.service" \ + "/lib/systemd/system/${SERVICE_NAME}.service"; do + if [ -f "$p" ]; then + printf '%s' "$p" + return 0 + fi + done + return 1 +} case "$1" in configure) if [ -f "$HOMED_BIN" ]; then mkdir -p "$DATA_DIR" - # 初始化凭据和数据库 - if [ -x /usr/lib/homeagent/setup.sh ]; then - HOMEAGENT_DATA="$DATA_DIR" /usr/lib/homeagent/setup.sh || true + # 初始化凭据和数据库。 + # + # 这里不能再用 `|| true` 吞失败:setup.sh 靠 initconfig 写 config.db, + # 而 initconfig 曾因 CGO_ENABLED=0 静默空操作(凭据只进了 + # credentials.txt、没进数据库),用户拿它登录必然失败,安装却一声不响。 + # 失败必须看得见,并给出可直接执行的补救命令。 + if [ -x "$SETUP_SH" ]; then + if ! HOMEAGENT_DATA="$DATA_DIR" "$SETUP_SH"; then + echo "E: homeagent 初始化失败——凭据可能未写入 config.db。" >&2 + echo "E: 请手动重试:HOMEAGENT_DATA=$DATA_DIR $SETUP_SH" >&2 + fi + else + echo "W: 未找到 $SETUP_SH,跳过凭据初始化。" >&2 fi # 注册 systemd 服务 - if [ -f "$SERVICE_FILE" ]; then - systemctl daemon-reload 2>/dev/null || true - systemctl enable "$SERVICE_NAME" 2>/dev/null || true + if command -v systemctl >/dev/null 2>&1; then + if find_unit >/dev/null; then + systemctl daemon-reload 2>/dev/null || true + systemctl enable "$SERVICE_NAME" 2>/dev/null || true + # 首次安装就拉起来,装完即可用;升级时重启以真正加载新二进制 + # (仅 enable 不会让已在运行的进程换用新文件)。 + if [ -z "${2:-}" ]; then + systemctl start "$SERVICE_NAME" 2>/dev/null || \ + echo "W: homeagent 服务未能启动,请检查:systemctl status $SERVICE_NAME" >&2 + else + systemctl restart "$SERVICE_NAME" 2>/dev/null || \ + echo "W: homeagent 服务未能重启,请检查:systemctl status $SERVICE_NAME" >&2 + fi + else + echo "W: 未找到 ${SERVICE_NAME}.service,未启用服务。" >&2 + fi fi fi ;; diff --git a/deploy/packaging/linux/setup.sh b/deploy/packaging/linux/setup.sh index 2c6cc5c..2e0cc46 100644 --- a/deploy/packaging/linux/setup.sh +++ b/deploy/packaging/linux/setup.sh @@ -8,8 +8,20 @@ CRED_FILE="${DATA_DIR}/credentials.txt" CONFIG_DB="${DATA_DIR}/config.db" WAITER_CONF="${DATA_DIR}/waiter.yaml" INITCONFIG_BIN="/usr/bin/initconfig" +BUNDLED_MODEL_DIR="/usr/lib/homeagent/models/chinese-clip-vit-b16-onnx" +MODEL_LINK="${DATA_DIR}/models/chinese-clip-vit-b16-onnx" -# 如果已经初始化过,跳过 +# 模型随 server/full 包安装到只读的 /usr/lib;配置默认仍指向 dataDir/models。 +# 用符号链接把两者接起来,既不复制 754MB,也保持 dataDir 可迁移语义。 +# 用户已有自定义目录时绝不覆盖;升级时既有链接自然指向新版包内容。 +if [ -d "$BUNDLED_MODEL_DIR" ]; then + mkdir -p "${DATA_DIR}/models" + if [ ! -e "$MODEL_LINK" ] && [ ! -L "$MODEL_LINK" ]; then + ln -s "$BUNDLED_MODEL_DIR" "$MODEL_LINK" + fi +fi + +# 如果已经初始化过,只跳过凭据/数据库生成;上面的模型链接仍须在升级时补齐。 if [ -f "$CONFIG_DB" ] && [ -f "$CRED_FILE" ]; then exit 0 fi diff --git a/deploy/packaging/package-linux.sh b/deploy/packaging/package-linux.sh index a2b5bdd..01ea3af 100644 --- a/deploy/packaging/package-linux.sh +++ b/deploy/packaging/package-linux.sh @@ -5,11 +5,37 @@ PROJECT_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" BUILD_DIR="${PROJECT_ROOT}/build" DIST_DIR="${PROJECT_ROOT}/dist/linux" VERSION="${VERSION:-$(git -C "$PROJECT_ROOT" describe --tags --dirty 2>/dev/null || echo "0.8.0")}" + +# git describe 给出的是 v1.0.0-68-gba0b5a1-dirty 这类描述串,它不是合法的包版本: +# deb 的 Version 必须以数字开头,rpm 的 Version 不允许 '-'(那是版本/发布的分隔符)。 +# 以前只有显式传 VERSION=1.0.3 才打得出来,默认路径一跑就死在 dpkg-deb 上—— +# 而且死在 stage 之后,前面每条日志都是真的,只有最后一个产物没生成。 +PKG_VERSION="${VERSION#v}" +case "$PKG_VERSION" in + [0-9]*) ;; + *) echo "ERROR: 包版本必须以数字开头(得到 '$VERSION')。请显式设置 VERSION=x.y.z 后重试。" >&2; exit 1 ;; +esac +PKG_VERSION="$(printf '%s' "$PKG_VERSION" | sed -e 's/-/+/g')" PACKAGE_ROOT="${PROJECT_ROOT}/deploy/packaging/linux" GO="${GO:-$(command -v go 2>/dev/null || echo "go")}" ARCH="${1:-amd64}" # amd64 or arm64 +# server/full 发行包默认带 Chinese-CLIP ONNX 产物与 ONNX Runtime。 +# 二进制大资产不进 git:发布环境通过这两个目录提供已验证的产物;若缺失, +# server/full 打包必须明确失败,不能生成一个「默认启用但装完不能用」的假包。 +CHINESECLIP_BUNDLE_DIR="${CHINESECLIP_BUNDLE_DIR:-$BUILD_DIR/model-assets/chinese-clip-vit-b16-onnx}" +ONNXRUNTIME_ASSET_DIR="${ONNXRUNTIME_ASSET_DIR:-$BUILD_DIR/runtime-assets/$ARCH}" +ONNXRUNTIME_LIB="${ONNXRUNTIME_LIB:-$ONNXRUNTIME_ASSET_DIR/libonnxruntime.so}" +ONNXRUNTIME_LICENSE="${ONNXRUNTIME_LICENSE:-$ONNXRUNTIME_ASSET_DIR/LICENSE}" +ONNXRUNTIME_NOTICES="${ONNXRUNTIME_NOTICES:-$ONNXRUNTIME_ASSET_DIR/ThirdPartyNotices.txt}" + +# 打包 staging 会把 719MB 模型真的复制一份,临时目录必须落在构建目录所在的磁盘, +# 不能落在系统临时目录:本机 /tmp 是 9.8GB tmpfs,一次 full 包 staging 就能写满, +# 而且失败发生在 cp 进行到一半,报出来是 "No space left on device"——看上去像 +# 资产/版本有问题,实际只是临时目录选错了文件系统。 +STAGE_TMP="${BUILD_DIR}/.stage-tmp" + # electron 官方发布物用 x64/arm64 命名,而 Debian 用 amd64/arm64。 # 两者在 arm64 上恰好同名,amd64 上不同——此前缓存查找统一用 TAR_ARCH # (amd64),于是 electron-v*-linux-x64.zip 永远命中不到,amd64 GUI 只能 @@ -28,6 +54,7 @@ esac echo "=== HomeAgent Linux Packager ===" echo "Version: $VERSION" +[ "$PKG_VERSION" = "$VERSION" ] || echo "Package: $PKG_VERSION (normalized for deb/rpm)" echo "Arch: $ARCH" echo "" @@ -89,7 +116,7 @@ restore_syso() { } # ensure both are always restored on exit -restore_all() { restore_gomod; restore_syso; } +restore_all() { restore_gomod; restore_syso; rmdir "$STAGE_TMP" 2>/dev/null || true; } trap restore_all EXIT # ---- build Go binaries via existing build.sh ---- @@ -99,31 +126,29 @@ build_go() { prepare_gomod || true hide_syso - bash "$PROJECT_ROOT/deploy/packaging/build.sh" "linux/$ARCH" "homed" 2>&1 || { - echo "WARNING: homed build failed (CGO/sqlite3 issue). Server/full packages may be incomplete." - } - bash "$PROJECT_ROOT/deploy/packaging/build.sh" "linux/$ARCH" "waiter" 2>&1 || { - echo "WARNING: waiter build failed." - } - bash "$PROJECT_ROOT/deploy/packaging/build.sh" "linux/$ARCH" "initconfig" 2>&1 || { - echo "WARNING: initconfig build failed(包内将缺少首次配置初始化器)。" - } - local suffix="linux_${ARCH}" local homed_bin="$BUILD_DIR/homed_$suffix" local waiter_bin="$BUILD_DIR/waiter_$suffix" + local initconfig_bin="$BUILD_DIR/initconfig_$suffix" - if [ ! -f "$homed_bin" ]; then - echo "ERROR: homed binary not found at $homed_bin" - exit 1 - fi - if [ ! -f "$waiter_bin" ]; then - echo "ERROR: waiter binary not found at $waiter_bin" - exit 1 + # 先删旧产物:否则本次构建失败后,残留文件会让「产物存在」判据假绿。 + rm -f "$homed_bin" "$waiter_bin" "$initconfig_bin" + + bash "$PROJECT_ROOT/deploy/packaging/build.sh" "linux/$ARCH" "homed" + test -x "$homed_bin" + if ! go version -m "$homed_bin" | grep -Eq 'build[[:space:]]+-tags=.*onnxruntime'; then + echo "ERROR: homed 不是 onnxruntime 构建,拒绝打 server/full 包:$homed_bin" >&2 + return 1 fi - echo " homed: $homed_bin ($(du -h "$homed_bin" | cut -f1))" - echo " waiter: $waiter_bin ($(du -h "$waiter_bin" | cut -f1))" + bash "$PROJECT_ROOT/deploy/packaging/build.sh" "linux/$ARCH" "waiter" + test -x "$waiter_bin" + bash "$PROJECT_ROOT/deploy/packaging/build.sh" "linux/$ARCH" "initconfig" + test -x "$initconfig_bin" + + echo " homed: $homed_bin ($(du -h "$homed_bin" | cut -f1), onnxruntime)" + echo " waiter: $waiter_bin ($(du -h "$waiter_bin" | cut -f1))" + echo " initconfig: $initconfig_bin ($(du -h "$initconfig_bin" | cut -f1))" echo "" } @@ -310,6 +335,7 @@ stage_variant() { cp "$PROJECT_ROOT/deploy/homeagent.service" "$staging/etc/systemd/system/homeagent.service" [ -f "$initconfig_bin" ] && cp "$initconfig_bin" "$staging/usr/bin/initconfig" stage_setup "$staging" + stage_multimodal_assets "$staging" stage_gui "$staging" ;; server) @@ -318,6 +344,7 @@ stage_variant() { cp "$PROJECT_ROOT/deploy/homeagent.service" "$staging/etc/systemd/system/homeagent.service" [ -f "$initconfig_bin" ] && cp "$initconfig_bin" "$staging/usr/bin/initconfig" stage_setup "$staging" + stage_multimodal_assets "$staging" ;; client) cp "$BUILD_DIR/waiter_$suffix" "$staging/usr/bin/waiter" @@ -355,6 +382,75 @@ stage_setup() { fi } +# server/full 的 ONNX 资产。模型与运行库是发行版能力的一部分,不是可选下载: +# 只要打 server/full 包,两者缺一就失败。client 包不运行 homed,故不携带。 +stage_multimodal_assets() { + local staging="$1" + local model_dst="$staging/usr/lib/homeagent/models/chinese-clip-vit-b16-onnx" + local ort_dst="$staging/usr/lib/homeagent/onnxruntime" + local licenses="$staging/usr/share/doc/homeagent/licenses" + + if [ ! -d "$CHINESECLIP_BUNDLE_DIR" ]; then + echo "ERROR: Chinese-CLIP 产物目录不存在:$CHINESECLIP_BUNDLE_DIR" >&2 + echo "先运行 scripts/export_chineseclip_onnx.py,再通过 CHINESECLIP_BUNDLE_DIR 指向产物。" >&2 + return 1 + fi + for f in TextEncoder.onnx VisionEncoder.onnx embed_config.json vocab.txt reference.json SHA256SUMS; do + if [ ! -s "$CHINESECLIP_BUNDLE_DIR/$f" ]; then + echo "ERROR: Chinese-CLIP 产物缺少或为空:$CHINESECLIP_BUNDLE_DIR/$f" >&2 + return 1 + fi + done + if ! (cd "$CHINESECLIP_BUNDLE_DIR" && sha256sum -c SHA256SUMS); then + echo "ERROR: Chinese-CLIP SHA256SUMS 校验失败,拒绝打包。" >&2 + return 1 + fi + + if [ ! -s "$ONNXRUNTIME_LIB" ]; then + echo "ERROR: ONNX Runtime 不存在:$ONNXRUNTIME_LIB" >&2 + echo "通过 ONNXRUNTIME_ASSET_DIR 或 ONNXRUNTIME_LIB 指向与目标架构匹配的资产。" >&2 + return 1 + fi + for notice in "$ONNXRUNTIME_LICENSE" "$ONNXRUNTIME_NOTICES"; do + if [ ! -s "$notice" ]; then + echo "ERROR: ONNX Runtime 许可证资产缺失:$notice" >&2 + return 1 + fi + done + local runtime_desc + runtime_desc=$(file -b "$ONNXRUNTIME_LIB") + case "$ARCH" in + amd64) printf '%s' "$runtime_desc" | grep -qE 'x86-64|x86_64' || { + echo "ERROR: ONNX Runtime 架构不是 amd64:$runtime_desc" >&2; return 1; } ;; + arm64) printf '%s' "$runtime_desc" | grep -qE 'aarch64|ARM aarch64' || { + echo "ERROR: ONNX Runtime 架构不是 arm64:$runtime_desc" >&2; return 1; } ;; + esac + + mkdir -p "$model_dst" "$ort_dst" "$licenses" + cp -a "$CHINESECLIP_BUNDLE_DIR/." "$model_dst/" + install -m 0755 "$ONNXRUNTIME_LIB" "$ort_dst/libonnxruntime.so" + + # 许可证随二进制分发:Chinese-CLIP = Apache-2.0;ONNX Runtime = MIT, + # 同时携带其 ThirdPartyNotices(含 MKL/protobuf/zlib 等第三方条款)。 + cp /usr/share/common-licenses/Apache-2.0 "$licenses/Chinese-CLIP-Apache-2.0.txt" + cp "$ONNXRUNTIME_LICENSE" "$licenses/ONNX-Runtime-MIT.txt" + cp "$ONNXRUNTIME_NOTICES" "$licenses/ONNX-Runtime-ThirdPartyNotices.txt" + cat > "$licenses/MODEL-SOURCES.txt" < "$deb_root/DEBIAN/control" @@ -401,13 +497,13 @@ build_tar() { local tar_dir="${DIST_DIR}/tar" mkdir -p "$tar_dir" - local archive_name="homeagent_${VERSION}_linux_${TAR_ARCH}.tar.gz" - local archive_dir="homeagent-${VERSION}-linux-${TAR_ARCH}" + local archive_name="homeagent_${PKG_VERSION}_linux_${TAR_ARCH}.tar.gz" + local archive_dir="homeagent-${PKG_VERSION}-linux-${TAR_ARCH}" # build combined staging local staging - staging="$(mktemp -d)" - mkdir -p "$staging/usr/bin" "$staging/usr/lib/homeagent" + staging="$(mktemp -d "$STAGE_TMP/tar.XXXXXX")" + mkdir -p "$staging/usr/bin" "$staging/usr/lib/homeagent" "$staging/etc/systemd/system" # copy all available binaries for bin in homed waiter initconfig; do @@ -418,6 +514,8 @@ build_tar() { # setup script local setup_src="$PROJECT_ROOT/deploy/packaging/linux/setup.sh" [ -f "$setup_src" ] && cp "$setup_src" "$staging/usr/lib/homeagent/setup.sh" + cp "$PROJECT_ROOT/deploy/homeagent.service" "$staging/etc/systemd/system/homeagent.service" + stage_multimodal_assets "$staging" # GUI if available local gui_src="$BUILD_DIR/homeagent-gui-linux-${TAR_ARCH}" @@ -446,7 +544,7 @@ build_rpm() { local rpm_dir="${DIST_DIR}/rpm" mkdir -p "$rpm_dir" - local pkg_name="homeagent-${variant}-${VERSION}-1.${RPM_ARCH}.rpm" + local pkg_name="homeagent-${variant}-${PKG_VERSION}-1.${RPM_ARCH}.rpm" # find fpm local fpm_bin="$(command -v fpm 2>/dev/null || true)" @@ -506,7 +604,7 @@ build_rpm() { main() { local target_arch="$ARCH" - mkdir -p "$BUILD_DIR" + mkdir -p "$BUILD_DIR" "$STAGE_TMP" case "$ACTION" in all|build) @@ -523,6 +621,10 @@ main() { mkdir -p "$DIST_DIR" + # 上次成功构建留下的校验和必须在本次开工前删掉:本次若中途失败,脚本直接退出、 + # 不重算 SHA256SUMS,旧的它会一直躺在 dist 里,看上去像在为这一批残缺产物背书。 + rm -f "$DIST_DIR/SHA256SUMS" + for variant in full server client; do echo "" echo "==============================================" @@ -530,7 +632,7 @@ main() { echo "==============================================" local staging - staging=$(mktemp -d) + staging=$(mktemp -d "$STAGE_TMP/stage.XXXXXX") stage_variant "$variant" "$staging" case "$ACTION" in @@ -552,9 +654,19 @@ main() { echo "=== Done! Packages in: $DIST_DIR ===" echo "" echo "Summary:" - find "$DIST_DIR" -type f \( -name "*.deb" -o -name "homeagent_*.tar.gz" -o -name "*.rpm" \) 2>/dev/null | sort | while read -r f; do + mapfile -t release_files < <(find "$DIST_DIR" -type f \( -name "*.deb" -o -name "homeagent_*.tar.gz" -o -name "*.rpm" \) 2>/dev/null | sort) + for f in "${release_files[@]}"; do echo " $(du -h "$f" | cut -f1) $f" done + # 全部包生成之后一次计算,避免边打边算漏掉后生成的产物。 + if [ ${#release_files[@]} -gt 0 ]; then + ( + cd "$DIST_DIR" + find . -type f \( -name "*.deb" -o -name "homeagent_*.tar.gz" -o -name "*.rpm" \) \ + -print0 | sort -z | xargs -0 sha256sum > SHA256SUMS + ) + echo " SHA256SUMS: $DIST_DIR/SHA256SUMS" + fi } main diff --git a/docs/zh/multimodal-space.md b/docs/zh/multimodal-space.md index 300f057..ac7779d 100644 --- a/docs/zh/multimodal-space.md +++ b/docs/zh/multimodal-space.md @@ -149,12 +149,45 @@ core.memory.multimodal_space.options.model_dir = /home/newqqagent/models/chinese 发行版构建也默认带 `onnxruntime` 标签(`deploy/packaging/build.sh` 的 `HOMED_TAGS`, 需要极简构建时显式 `HOMED_TAGS=` 关闭)。 -已于既有安装:`SeedDefaults` 对非空配置库直接返回(`GetString` 缺键时回落调用方默认值), -所以老安装**不会**自动拿到这两个默认值,需要显式写配置。这是有意的: -升级就静默加载 1.8GB 模型不是无副作用的事。 +**老安装不会自动拿到**:播种判据是显式标记 `core.internal.seed_version`。 +老安装(已播种过)下次启动只会被补上标记,**不会**被注入新默认值—— +升级就静默加载 1.8GB 模型不是无副作用的事。要启用请显式写上面两个键。 + +> 这个判据曾经是「`config` 表为空才播种」。而发行包的 postinst 会先跑 +> `initconfig`,它写一行 `webui.listen_addr` ——于是**全新安装**被误判为 +> "已有配置",整个播种被跳过:没有 `core.plugin.dir`(装完 0 个插件)、 +> 也没有多模态 provider(随包的模型与运行库成了死重量)。回归测试 +> `TestSeedDefaultsAfterInitconfigPrepopulate` 与 +> `TestSeedDefaultsDoesNotInjectIntoLegacyInstall` 钉住了这两种情形。 同样要求 `homed` 带 `onnxruntime` build tag。 +### 随包分发(server / full 包自带模型与运行库) + +模型与运行库是发行版能力的一部分,不做成「可选下载」: + +| 内容 | 包内路径 | +|---|---| +| Chinese-CLIP 产物(754MB) | `/usr/lib/homeagent/models/chinese-clip-vit-b16-onnx/` | +| ONNX Runtime(24MB) | `/usr/lib/homeagent/onnxruntime/libonnxruntime.so` | +| 许可证 | `/usr/share/doc/homeagent/licenses/`(Apache-2.0、MIT、ThirdPartyNotices、模型来源) | + +- `deploy/packaging/package-linux.sh` 的 `stage_multimodal_assets()` 在打 server/full 前 + 会校验产物 `SHA256SUMS`、逐文件非空、运行库架构与目标一致;**缺一即失败**, + 不生成「默认启用但装完不能用」的假包。`client` 包不含(它不跑 homed)。 +- 安装时 `setup.sh` 把包内模型目录软链到 `/models/chinese-clip-vit-b16-onnx` + (既不复制 754MB,也保持 dataDir 可迁移;已存在的自定义目录绝不覆盖)。 +- 服务单元设 `Environment=ONNXRUNTIME_DIR=/usr/lib/homeagent/onnxruntime`; + provider 的查找顺序是 `ONNXRUNTIME_DIR` → `ONNX_ML_DIR` → 包内路径 → + `/opt/onnxruntime` → `/usr/local/lib` → `/usr/lib`。 +- 构建机需自备产物:`build/model-assets/chinese-clip-vit-b16-onnx/` 与 + `build/runtime-assets//{libonnxruntime.so,LICENSE,ThirdPartyNotices.txt}` + (可用 `CHINESECLIP_BUNDLE_DIR` / `ONNXRUNTIME_ASSET_DIR` 覆盖)。 + +实测(从真实 deb 解包、按 postinst 顺序跑 `setup.sh`、再冷启动包内 homed): +`multimodal space active: provider=chineseclip dim=512 fp=cd2a495cf990 modalities=[text image]`, +并完成一次真实对话;`homeagent-server` 包 722MB(旧版 17MB),差额即模型与运行库。 + #### ORT 环境是进程级单例(单主不析构) 进程内可能有多个 ORT 消费者(本 provider、`qwen3vl`、`internal/nlp` 的依存解析器)。 diff --git a/providers/chineseclip/embedder.go b/providers/chineseclip/embedder.go index c8d64d2..8cd1cdb 100644 --- a/providers/chineseclip/embedder.go +++ b/providers/chineseclip/embedder.go @@ -269,11 +269,19 @@ func computeFingerprint(modelDir string, cfg embedConfig) string { } func findOnnxLib() string { - for _, p := range []string{ + var candidates []string + for _, env := range []string{"ONNXRUNTIME_DIR", "ONNX_ML_DIR"} { + if dir := strings.TrimSpace(os.Getenv(env)); dir != "" { + candidates = append(candidates, filepath.Join(dir, "libonnxruntime.so")) + } + } + candidates = append(candidates, + "/usr/lib/homeagent/onnxruntime/libonnxruntime.so", // 发行包内置位置 "/opt/onnxruntime/libonnxruntime.so", "/usr/local/lib/libonnxruntime.so", "/usr/lib/libonnxruntime.so", - } { + ) + for _, p := range candidates { if _, err := os.Stat(p); err == nil { return p } diff --git a/providers/qwen3vl/embedder.go b/providers/qwen3vl/embedder.go index 2c5e718..d3eb60b 100644 --- a/providers/qwen3vl/embedder.go +++ b/providers/qwen3vl/embedder.go @@ -616,7 +616,19 @@ func computeFingerprint(modelDir string) string { } func findOnnxLib() string { - for _, p := range []string{"/opt/onnxruntime/libonnxruntime.so", "/usr/local/lib/libonnxruntime.so", "/usr/lib/libonnxruntime.so"} { + var candidates []string + for _, env := range []string{"ONNXRUNTIME_DIR", "ONNX_ML_DIR"} { + if dir := strings.TrimSpace(os.Getenv(env)); dir != "" { + candidates = append(candidates, filepath.Join(dir, "libonnxruntime.so")) + } + } + candidates = append(candidates, + "/usr/lib/homeagent/onnxruntime/libonnxruntime.so", // 发行包内置位置 + "/opt/onnxruntime/libonnxruntime.so", + "/usr/local/lib/libonnxruntime.so", + "/usr/lib/libonnxruntime.so", + ) + for _, p := range candidates { if _, err := os.Stat(p); err == nil { return p }