mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-07-17 11:07:11 +08:00
fix(ci): track desktop backend packaging sources
The global build/ gitignore rule was silently excluding the PyInstaller spec, desktop requirements, and build script under desktop/build/, so the release workflow failed at "pip install -r requirements-desktop.txt" with a missing-file error. Re-include just those source files while keeping the build outputs (dist/, build-work/) and local venv ignored. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
9
.gitignore
vendored
9
.gitignore
vendored
@@ -45,3 +45,12 @@ dist/
|
||||
build/
|
||||
*.egg-info/
|
||||
.cow.pid
|
||||
|
||||
# Desktop backend packaging: keep the source files (spec/requirements/script)
|
||||
# tracked even though the generic build/ rule above ignores them, but never
|
||||
# track the build outputs or local venv.
|
||||
!desktop/build/
|
||||
desktop/build/*
|
||||
!desktop/build/cowagent-backend.spec
|
||||
!desktop/build/requirements-desktop.txt
|
||||
!desktop/build/build-backend.sh
|
||||
|
||||
79
desktop/build/build-backend.sh
Executable file
79
desktop/build/build-backend.sh
Executable file
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Build the desktop backend into a self-contained onedir bundle via PyInstaller.
|
||||
# Run from anywhere; paths are resolved relative to the repo root.
|
||||
#
|
||||
# Usage:
|
||||
# bash desktop/build/build-backend.sh # build
|
||||
# PYTHON=python3.11 bash desktop/build/build-backend.sh # pick interpreter
|
||||
#
|
||||
# Output: desktop/build/dist/cowagent-backend/ (folder with the executable)
|
||||
set -euo pipefail
|
||||
|
||||
# --- resolve paths --------------------------------------------------------
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
BUILD_DIR="$SCRIPT_DIR"
|
||||
VENV_DIR="$BUILD_DIR/.venv-build"
|
||||
|
||||
# Prefer Python 3.11 when available: on 3.13+ web.py must be installed from a
|
||||
# GitHub git source (the PyPI build fails), which is flaky on some networks.
|
||||
# 3.11 installs web.py straight from PyPI and has the best PyInstaller support.
|
||||
if [ -z "${PYTHON:-}" ]; then
|
||||
for cand in \
|
||||
"/Library/Frameworks/Python.framework/Versions/3.11/bin/python3.11" \
|
||||
"python3.11" \
|
||||
"python3.12" \
|
||||
"python3"; do
|
||||
if command -v "$cand" >/dev/null 2>&1; then
|
||||
PYTHON="$cand"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
# Prefer Python 3.11: it installs web.py from PyPI (no GitHub clone) and avoids
|
||||
# 3.13's removed-cgi compatibility shims. Override with PYTHON=... if needed.
|
||||
pick_python() {
|
||||
if [ -n "${PYTHON:-}" ]; then echo "$PYTHON"; return; fi
|
||||
for c in python3.11 python3.12 python3.10 python3; do
|
||||
if command -v "$c" >/dev/null 2>&1; then echo "$c"; return; fi
|
||||
done
|
||||
echo python3
|
||||
}
|
||||
PYTHON="$(pick_python)"
|
||||
|
||||
echo "==> Repo root: $ROOT"
|
||||
echo "==> Using Python: $($PYTHON --version 2>&1) ($PYTHON)"
|
||||
|
||||
# --- isolated build venv --------------------------------------------------
|
||||
if [ ! -d "$VENV_DIR" ]; then
|
||||
echo "==> Creating build venv at $VENV_DIR"
|
||||
"$PYTHON" -m venv "$VENV_DIR"
|
||||
fi
|
||||
# shellcheck disable=SC1091
|
||||
source "$VENV_DIR/bin/activate"
|
||||
|
||||
echo "==> Installing build dependencies"
|
||||
pip install -q --upgrade pip
|
||||
# Don't leave a half-populated venv behind if deps fail (e.g. flaky network):
|
||||
# the next run would otherwise reuse a broken venv.
|
||||
if ! pip install -q -r "$BUILD_DIR/requirements-desktop.txt"; then
|
||||
echo "!! Dependency install failed. Removing the build venv so a retry starts clean." >&2
|
||||
deactivate || true
|
||||
rm -rf "$VENV_DIR"
|
||||
exit 1
|
||||
fi
|
||||
pip install -q pyinstaller
|
||||
|
||||
# --- run pyinstaller from repo root so relative datas resolve -------------
|
||||
cd "$ROOT"
|
||||
echo "==> Running PyInstaller (onedir)"
|
||||
pyinstaller "$BUILD_DIR/cowagent-backend.spec" \
|
||||
--noconfirm \
|
||||
--distpath "$BUILD_DIR/dist" \
|
||||
--workpath "$BUILD_DIR/build-work"
|
||||
|
||||
echo ""
|
||||
echo "==> Done. Bundle at: $BUILD_DIR/dist/cowagent-backend/"
|
||||
du -sh "$BUILD_DIR/dist/cowagent-backend/" 2>/dev/null || true
|
||||
echo "==> Smoke test: COW_DESKTOP=1 \"$BUILD_DIR/dist/cowagent-backend/cowagent-backend\""
|
||||
145
desktop/build/cowagent-backend.spec
Normal file
145
desktop/build/cowagent-backend.spec
Normal file
@@ -0,0 +1,145 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
"""
|
||||
PyInstaller spec for the CowAgent desktop backend (onedir).
|
||||
|
||||
Produces a self-contained `cowagent-backend` folder that the Electron app
|
||||
spawns directly, so end users don't need Python installed.
|
||||
|
||||
onedir is chosen over onefile because the backend reads data files via paths
|
||||
relative to the source tree (e.g. config-template.json, skills/, chat.html);
|
||||
onedir preserves that layout, while onefile's temp-extraction would break it.
|
||||
|
||||
Build from the repo root:
|
||||
pyinstaller desktop/build/cowagent-backend.spec --noconfirm
|
||||
"""
|
||||
import os
|
||||
from PyInstaller.utils.hooks import collect_submodules, collect_data_files
|
||||
|
||||
# Resolve the repo root from the spec's own location (desktop/build/ -> root),
|
||||
# independent of the current working directory. PyInstaller exposes SPECPATH.
|
||||
ROOT = os.path.abspath(os.path.join(SPECPATH, '..', '..'))
|
||||
|
||||
|
||||
def rp(*parts):
|
||||
"""Absolute path under the repo root."""
|
||||
return os.path.join(ROOT, *parts)
|
||||
|
||||
# --- Hidden imports -------------------------------------------------------
|
||||
# Channels are imported dynamically by channel_factory via string names, so
|
||||
# PyInstaller's static analysis can't see them. List every channel we ship
|
||||
# (Feishu is intentionally excluded — lark-oapi is dropped from the desktop
|
||||
# build to save ~116MB).
|
||||
hiddenimports = [
|
||||
# channels (dynamic import in channel/channel_factory.py)
|
||||
'channel.web.web_channel',
|
||||
'channel.terminal.terminal_channel',
|
||||
'channel.weixin.weixin_channel',
|
||||
'channel.wechatmp.wechatmp_channel',
|
||||
'channel.wechatcom.wechatcomapp_channel',
|
||||
'channel.wechat_kf.wechat_kf_channel',
|
||||
'channel.dingtalk.dingtalk_channel',
|
||||
'channel.wecom_bot.wecom_bot_channel',
|
||||
'channel.qq.qq_channel',
|
||||
'channel.telegram.telegram_channel',
|
||||
'channel.slack.slack_channel',
|
||||
'channel.discord.discord_channel',
|
||||
]
|
||||
|
||||
# Agent tools and model providers are imported lazily in places; collect their
|
||||
# submodules so nothing is missed at runtime.
|
||||
hiddenimports += collect_submodules('agent.tools')
|
||||
hiddenimports += collect_submodules('models')
|
||||
hiddenimports += collect_submodules('voice')
|
||||
hiddenimports += collect_submodules('bridge')
|
||||
|
||||
# Plugin framework: WebChannel -> ChatChannel imports `from plugins import *`,
|
||||
# so the framework package must be present even though desktop mode never loads
|
||||
# actual plugins (it's only ~tens of KB of code).
|
||||
hiddenimports += [
|
||||
'plugins',
|
||||
'plugins.event',
|
||||
'plugins.plugin',
|
||||
'plugins.plugin_manager',
|
||||
]
|
||||
|
||||
# Third-party SDKs that use lazy/conditional imports internally.
|
||||
hiddenimports += collect_submodules('dashscope')
|
||||
hiddenimports += [
|
||||
'tiktoken_ext',
|
||||
'tiktoken_ext.openai_public',
|
||||
]
|
||||
|
||||
# --- Data files -----------------------------------------------------------
|
||||
# Runtime-read files/dirs that must travel with the executable. Paths are
|
||||
# (source, dest_dir_in_bundle).
|
||||
datas = [
|
||||
(rp('config-template.json'), '.'),
|
||||
(rp('skills'), 'skills'),
|
||||
# Web console served on the backend port: ship chat.html plus its static
|
||||
# assets (~1.9MB) so the browser-based console works as a debug/fallback
|
||||
# entry alongside the Electron UI.
|
||||
(rp('channel', 'web', 'chat.html'), 'channel/web'),
|
||||
(rp('channel', 'web', 'static'), 'channel/web/static'),
|
||||
]
|
||||
|
||||
# Some libraries (tiktoken encodings, etc.) ship data files.
|
||||
datas += collect_data_files('tiktoken_ext', include_py_files=False)
|
||||
|
||||
# --- Excludes -------------------------------------------------------------
|
||||
# Keep the bundle lean: drop Feishu's heavy SDK, plugins (disabled in desktop
|
||||
# mode), tests/docs, and dev-only packages.
|
||||
excludes = [
|
||||
'lark_oapi', # Feishu — ~116MB, excluded from desktop build
|
||||
'tests',
|
||||
'pip',
|
||||
'wheel',
|
||||
'pytest',
|
||||
'playwright', # browser tool is opt-in, not bundled
|
||||
]
|
||||
|
||||
block_cipher = None
|
||||
|
||||
a = Analysis(
|
||||
[rp('app.py')],
|
||||
pathex=[ROOT],
|
||||
binaries=[],
|
||||
datas=datas,
|
||||
hiddenimports=hiddenimports,
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=excludes,
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False,
|
||||
)
|
||||
|
||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name='cowagent-backend',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=False,
|
||||
console=True,
|
||||
disable_windowed_traceback=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
)
|
||||
|
||||
coll = COLLECT(
|
||||
exe,
|
||||
a.binaries,
|
||||
a.datas,
|
||||
strip=False,
|
||||
upx=False,
|
||||
upx_exclude=[],
|
||||
name='cowagent-backend',
|
||||
)
|
||||
52
desktop/build/requirements-desktop.txt
Normal file
52
desktop/build/requirements-desktop.txt
Normal file
@@ -0,0 +1,52 @@
|
||||
# Desktop backend dependencies (slimmed down from the full requirements).
|
||||
#
|
||||
# Goal: keep the package light. The desktop client only needs the web channel
|
||||
# (which Electron talks to) plus the agent core; the remaining IM channels are
|
||||
# cheap (~27MB total) so we keep them, but Feishu's `lark-oapi` (~116MB) is
|
||||
# dropped — it is by far the heaviest dependency and not needed for a C-end
|
||||
# desktop app. Feishu is hidden in desktop mode (see COW_DESKTOP in app.py).
|
||||
|
||||
# ---- core ----
|
||||
numpy>=1.21
|
||||
aiohttp>=3.8.6,<3.10
|
||||
requests>=2.28.2
|
||||
chardet>=5.1.0
|
||||
Pillow
|
||||
python-dotenv>=1.0.0
|
||||
PyYAML>=6.0
|
||||
croniter>=2.0.0
|
||||
click>=8.0
|
||||
qrcode
|
||||
json-repair
|
||||
|
||||
# ---- web framework (web channel) ----
|
||||
# web.py 0.62 fails to build on Python 3.13+ (cgi removed); use the GitHub fix.
|
||||
web.py; python_version < "3.13"
|
||||
web.py @ git+https://github.com/webpy/webpy.git ; python_version >= "3.13"
|
||||
legacy-cgi; python_version >= "3.13"
|
||||
|
||||
# ---- AI model SDKs ----
|
||||
zai-sdk
|
||||
dashscope
|
||||
tenacity # used by some dashscope submodules (retry logic)
|
||||
google-generativeai
|
||||
tiktoken>=0.3.2
|
||||
|
||||
# ---- voice (TTS/ASR) — kept per product decision ----
|
||||
pydub>=0.25.1
|
||||
gTTS>=2.3.1
|
||||
|
||||
# ---- document parsing (web_fetch / knowledge) ----
|
||||
pypdf
|
||||
python-docx
|
||||
openpyxl
|
||||
python-pptx
|
||||
|
||||
# ---- IM channels (kept; lightweight). Feishu/lark-oapi intentionally excluded. ----
|
||||
wechatpy
|
||||
pycryptodome
|
||||
dingtalk_stream
|
||||
websocket-client>=1.4.0
|
||||
python-telegram-bot
|
||||
slack_bolt
|
||||
discord.py
|
||||
Reference in New Issue
Block a user