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/
|
build/
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
.cow.pid
|
.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