chore: initialize qiming workspace repository

This commit is contained in:
Codex
2026-05-29 14:22:48 +08:00
commit bfd67a0f2c
10750 changed files with 1885711 additions and 0 deletions

View File

@@ -0,0 +1,149 @@
#!/bin/bash
# 检查 sidecar 二进制是否齐全
#
# 用法:
# ./scripts/check-sidecars.sh
# ./scripts/check-sidecars.sh --target x86_64-pc-windows-msvc
# ./scripts/check-sidecars.sh --all-common
# ./scripts/check-sidecars.sh --component mcp-proxy --component node-runtime
# ./scripts/check-sidecars.sh --downloaded-only
# ./scripts/check-sidecars.sh --dir .cache/sidecars --downloaded-only --all-common
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
CHECK_DIR="${CHECK_DIR:-.cache/sidecars}"
TARGET=""
TARGETS=()
COMPONENTS=()
DOWNLOADED_ONLY=0
ALL_COMMON=0
COMMON_TARGETS=(
"x86_64-pc-windows-msvc"
"aarch64-pc-windows-msvc"
"x86_64-apple-darwin"
"aarch64-apple-darwin"
"x86_64-unknown-linux-gnu"
"aarch64-unknown-linux-gnu"
)
while [[ $# -gt 0 ]]; do
case "$1" in
--target)
TARGETS+=("${2:-}")
shift 2
;;
--all-common)
ALL_COMMON=1
shift 1
;;
--component)
COMPONENTS+=("${2:-}")
shift 2
;;
--downloaded-only)
DOWNLOADED_ONLY=1
shift 1
;;
--dir)
CHECK_DIR="${2:-}"
shift 2
;;
-h|--help)
sed -n '1,12p' "$0"
exit 0
;;
*)
echo "未知参数: $1" >&2
exit 2
;;
esac
done
if [[ $ALL_COMMON -eq 1 ]]; then
TARGETS=("${COMMON_TARGETS[@]}")
fi
if [[ ${#TARGETS[@]} -eq 0 ]]; then
# Default to current platform
case "$(uname -s)" in
Darwin)
case "$(uname -m)" in
arm64) TARGETS=("aarch64-apple-darwin") ;;
*) TARGETS=("x86_64-apple-darwin") ;;
esac
;;
Linux)
case "$(uname -m)" in
aarch64) TARGETS=("aarch64-unknown-linux-gnu") ;;
armv7l) TARGETS=("aarch64-unknown-linux-gnu") ;;
*) TARGETS=("x86_64-unknown-linux-gnu") ;;
esac
;;
MINGW*|MSYS*|CYGWIN*)
TARGETS=("x86_64-pc-windows-msvc")
;;
esac
fi
if [[ ${#COMPONENTS[@]} -eq 0 && $DOWNLOADED_ONLY -eq 1 ]]; then
COMPONENTS=("mcp-proxy" "node-runtime")
fi
if [[ ${#COMPONENTS[@]} -eq 0 ]]; then
COMPONENTS=("qiming-lanproxy" "mcp-proxy" "node-runtime")
fi
if [[ ! -d "$CHECK_DIR" ]]; then
echo "目录不存在: $CHECK_DIR" >&2
exit 1
fi
expected_filename() {
local component="$1"
local target="$2"
local ext=""
if [[ "$target" == *windows* ]]; then
ext=".exe"
fi
echo "${component}-${target}${ext}"
}
total_missing=0
for TARGET in "${TARGETS[@]}"; do
echo "检查目标: $TARGET"
echo "检查目录: $CHECK_DIR"
echo "组件: ${COMPONENTS[*]}"
echo ""
missing=0
for component in "${COMPONENTS[@]}"; do
fname="$(expected_filename "$component" "$TARGET")"
fpath="$CHECK_DIR/$fname"
if [[ -f "$fpath" ]]; then
echo "OK $fname"
else
echo "MISS $fname"
missing=$((missing + 1))
total_missing=$((total_missing + 1))
fi
done
echo ""
if [[ $missing -gt 0 ]]; then
echo "目标 $TARGET 缺失 sidecar 文件数量: $missing"
else
echo "目标 $TARGET sidecar 文件检查通过"
fi
echo ""
done
if [[ $total_missing -gt 0 ]]; then
echo "总缺失 sidecar 文件数量: $total_missing"
exit 1
fi
echo "所有目标 sidecar 文件检查通过"

View File

@@ -0,0 +1,59 @@
#!/bin/bash
# 清理本地下载的 sidecar二进制缓存
#
# 用法:
# ./scripts/clean-downloaded-sidecars.sh
# ./scripts/clean-downloaded-sidecars.sh --dry-run
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
CACHE_DIR="$ROOT_DIR/.cache/sidecars"
DRY_RUN=0
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run)
DRY_RUN=1
shift 1
;;
-h|--help)
sed -n '1,12p' "$0"
exit 0
;;
*)
echo "未知参数: $1" >&2
exit 2
;;
esac
done
targets=()
if [[ -d "$CACHE_DIR" ]]; then
targets+=(
"$CACHE_DIR"/mcp-proxy-*
"$CACHE_DIR"/node-runtime-*
"$CACHE_DIR"/sidecar-download-manifest.txt
)
fi
echo "清理目录: $CACHE_DIR"
removed=0
for f in "${targets[@]}"; do
if [[ -e "$f" ]]; then
if [[ $DRY_RUN -eq 1 ]]; then
echo "DRY rm -f $f"
else
rm -f "$f"
echo "REMOVED $f"
fi
removed=$((removed + 1))
fi
done
if [[ $removed -eq 0 ]]; then
echo "没有可清理的下载产物"
else
echo "完成,处理文件数: $removed"
fi

View File

@@ -0,0 +1,201 @@
#!/usr/bin/env bash
#
# download-node.sh — 下载 Node.js 到 Electron 资源目录
#
# 用法:
# ./scripts/download-node.sh # 自动检测当前平台
# ./scripts/download-node.sh darwin arm64 # 指定平台和架构
# ./scripts/download-node.sh linux x64
# ./scripts/download-node.sh win x64
#
# 支持平台/架构:
# darwin arm64 | x64
# linux x64 | arm64
# win x64
#
set -euo pipefail
NODE_VERSION="22.14.0"
RESOURCE_DIR="crates/agent-electron-client/resources/node"
# 项目根目录
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
TARGET_DIR="${PROJECT_ROOT}/${RESOURCE_DIR}"
# === 平台检测 ===
detect_platform() {
local uname_s
uname_s="$(uname -s)"
case "${uname_s}" in
Darwin) echo "darwin" ;;
Linux) echo "linux" ;;
MINGW*|MSYS*|CYGWIN*) echo "win" ;;
*) echo "unsupported"; return 1 ;;
esac
}
detect_arch() {
local uname_m
uname_m="$(uname -m)"
case "${uname_m}" in
x86_64|amd64) echo "x64" ;;
arm64|aarch64) echo "arm64" ;;
*) echo "unsupported"; return 1 ;;
esac
}
PLATFORM="${1:-$(detect_platform)}"
ARCH="${2:-$(detect_arch)}"
echo "==> Node.js v${NODE_VERSION} for ${PLATFORM}-${ARCH}"
# === 构建下载 URL ===
if [ "${PLATFORM}" = "win" ]; then
FILENAME="node-v${NODE_VERSION}-win-${ARCH}.zip"
URL="https://nodejs.org/dist/v${NODE_VERSION}/${FILENAME}"
else
FILENAME="node-v${NODE_VERSION}-${PLATFORM}-${ARCH}.tar.xz"
URL="https://nodejs.org/dist/v${NODE_VERSION}/${FILENAME}"
fi
# === 检查是否已下载 ===
NODE_BIN="${TARGET_DIR}/bin/node"
if [ "${PLATFORM}" = "win" ]; then
NODE_BIN="${TARGET_DIR}/node.exe"
fi
if [ -f "${NODE_BIN}" ]; then
EXISTING_VERSION="$("${NODE_BIN}" --version 2>/dev/null || true)"
if [ "${EXISTING_VERSION}" = "v${NODE_VERSION}" ]; then
# macOS: 还需验证架构匹配,避免 universal 构建时跳过不同架构的下载
if [ "${PLATFORM}" = "darwin" ]; then
case "${ARCH}" in
arm64) EXPECTED_ARCH="arm64" ;;
x64) EXPECTED_ARCH="x86_64" ;;
esac
ACTUAL_ARCH="$(file "${NODE_BIN}" | grep -oE 'arm64|x86_64' | head -1 || true)"
if [ "${ACTUAL_ARCH}" != "${EXPECTED_ARCH}" ]; then
echo "==> Node.js v${NODE_VERSION} exists but architecture mismatch (have ${ACTUAL_ARCH}, need ${EXPECTED_ARCH}), re-downloading..."
else
echo "==> Node.js v${NODE_VERSION} (${ARCH}) already exists at ${TARGET_DIR}, skipping download"
exit 0
fi
else
echo "==> Node.js v${NODE_VERSION} already exists at ${TARGET_DIR}, skipping download"
exit 0
fi
fi
fi
# === 下载 ===
TMPDIR_DL="$(mktemp -d)"
trap 'rm -rf "${TMPDIR_DL}"' EXIT
echo "==> Downloading ${URL}"
if command -v curl &>/dev/null; then
curl -fSL --progress-bar -o "${TMPDIR_DL}/${FILENAME}" "${URL}"
elif command -v wget &>/dev/null; then
wget -q --show-progress -O "${TMPDIR_DL}/${FILENAME}" "${URL}"
else
echo "Error: curl or wget required" >&2
exit 1
fi
# === 解压 ===
echo "==> Extracting..."
EXTRACT_DIR="${TMPDIR_DL}/extracted"
mkdir -p "${EXTRACT_DIR}"
if [ "${PLATFORM}" = "win" ]; then
# Windows: 尝试多种解压方式
if command -v unzip &>/dev/null; then
unzip -q "${TMPDIR_DL}/${FILENAME}" -d "${EXTRACT_DIR}"
elif command -v pwsh &>/dev/null || command -v powershell &>/dev/null; then
# PowerShell 7+ 支持 tar 格式
pwsh -Command "Expand-Archive -Path '${TMPDIR_DL}/${FILENAME}' -DestinationPath '${EXTRACT_DIR}' -Force" 2>/dev/null || \
powershell -Command "Expand-Archive -Path '${TMPDIR_DL}/${FILENAME}' -DestinationPath '${EXTRACT_DIR}' -Force"
elif command -v tar &>/dev/null; then
# 某些 Windows 环境下的 tar 也可以处理 zip
tar -xf "${TMPDIR_DL}/${FILENAME}" -C "${EXTRACT_DIR}"
else
echo "Error: No extraction tool available (unzip, powershell, or tar)" >&2
exit 1
fi
INNER_DIR="${EXTRACT_DIR}/node-v${NODE_VERSION}-win-${ARCH}"
else
tar -xf "${TMPDIR_DL}/${FILENAME}" -C "${EXTRACT_DIR}"
INNER_DIR="${EXTRACT_DIR}/node-v${NODE_VERSION}-${PLATFORM}-${ARCH}"
fi
if [ ! -d "${INNER_DIR}" ]; then
echo "Error: Expected directory ${INNER_DIR} not found after extraction" >&2
exit 1
fi
# === 复制到资源目录(仅 bin/ 和 lib/ ===
echo "==> Installing to ${TARGET_DIR}"
rm -rf "${TARGET_DIR}"
mkdir -p "${TARGET_DIR}"
if [ "${PLATFORM}" = "win" ]; then
# Windows: Node.js 发行包没有 bin/ 子目录,需要手动创建
mkdir -p "${TARGET_DIR}/bin"
# 复制可执行文件到 bin/ 目录
cp "${INNER_DIR}/node.exe" "${TARGET_DIR}/bin/"
for exe in npm npm.cmd npx npx.cmd corepack corepack.cmd; do
if [ -f "${INNER_DIR}/${exe}" ]; then
cp "${INNER_DIR}/${exe}" "${TARGET_DIR}/bin/"
fi
done
# 复制 node_modulesnpm 等工具需要)
if [ -d "${INNER_DIR}/node_modules" ]; then
mkdir -p "${TARGET_DIR}/lib"
cp -r "${INNER_DIR}/node_modules" "${TARGET_DIR}/lib/node_modules"
fi
else
# Unix: 只保留 bin/ 和 lib/(使用 cp -a 保留符号链接)
cp -a "${INNER_DIR}/bin" "${TARGET_DIR}/bin"
if [ -d "${INNER_DIR}/lib" ]; then
cp -a "${INNER_DIR}/lib" "${TARGET_DIR}/lib"
fi
# 确保可执行权限
chmod 755 "${TARGET_DIR}/bin/node"
for bin in npm npx corepack; do
if [ -f "${TARGET_DIR}/bin/${bin}" ]; then
chmod 755 "${TARGET_DIR}/bin/${bin}"
fi
done
fi
# === 验证 ===
echo "==> Verifying installation..."
if [ "${PLATFORM}" = "win" ]; then
echo " (skipping verification on Windows — cross-platform build)"
else
INSTALLED_VERSION="$("${TARGET_DIR}/bin/node" --version)"
if [ "${INSTALLED_VERSION}" = "v${NODE_VERSION}" ]; then
echo "==> OK: Node.js ${INSTALLED_VERSION}"
else
echo "Error: Expected v${NODE_VERSION}, got ${INSTALLED_VERSION}" >&2
exit 1
fi
fi
# === 输出大小统计 ===
echo "==> Size:"
du -sh "${TARGET_DIR}/bin" 2>/dev/null || true
du -sh "${TARGET_DIR}/lib" 2>/dev/null || true
du -sh "${TARGET_DIR}" | sed 's/\t/ /' | awk '{print " Total: " $1}'
echo "==> Done"

View File

@@ -0,0 +1,376 @@
#!/bin/bash
# 下载 sidecar 二进制到本地缓存目录(多平台)
#
# 支持 target:
# - x86_64-pc-windows-msvc
# - aarch64-pc-windows-msvc
# - x86_64-apple-darwin
# - aarch64-apple-darwin
# - x86_64-unknown-linux-gnu
# - aarch64-unknown-linux-gnu
#
# 用法:
# ./scripts/download-sidecars.sh # 自动从 GitHub 获取最新 mcp-proxy 版本
# ./scripts/download-sidecars.sh --target x86_64-pc-windows-msvc
# ./scripts/download-sidecars.sh --all-common
# ./scripts/download-sidecars.sh --all-common
# ./scripts/download-sidecars.sh --cache-dir .cache/sidecars
# ./scripts/download-sidecars.sh --mcp-version 0.1.48 # 指定 mcp-proxy 版本
# ./scripts/download-sidecars.sh --force # 强制重新下载
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
CACHE_DIR="$ROOT_DIR/.cache/sidecars"
TMP_DIR="/tmp/qiming-sidecar-download"
MCP_VERSION="0.1.53"
NODE_VERSION="22.14.0"
FORCE=0
DRY_RUN=0
ALL_COMMON=0
NO_CHECK=0
TARGETS=()
LAST_SUCCESS_URL=""
MCP_GITHUB_REPO="qiming-ai/mcp-proxy"
# 从 GitHub releases 获取最新版本号
get_latest_mcp_version() {
local api_url="https://api.github.com/repos/${MCP_GITHUB_REPO}/releases/latest"
local response
response=$(curl -fsSL "$api_url" 2>/dev/null) || {
echo "ERROR: 无法从 GitHub 获取最新版本" >&2
return 1
}
local tag_name
tag_name=$(echo "$response" | grep -m1 '"tag_name"' | sed 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')
if [[ -z "$tag_name" ]]; then
echo "ERROR: 无法解析 GitHub response 中的 tag_name" >&2
return 1
fi
# 移除前缀 'v' (如 v0.1.48 -> 0.1.48)
echo "${tag_name#v}"
}
COMMON_TARGETS=(
"x86_64-pc-windows-msvc"
"aarch64-pc-windows-msvc"
"x86_64-apple-darwin"
"aarch64-apple-darwin"
"x86_64-unknown-linux-gnu"
"aarch64-unknown-linux-gnu"
)
while [[ $# -gt 0 ]]; do
case "$1" in
--target)
TARGETS+=("${2:-}")
shift 2
;;
--all-common)
ALL_COMMON=1
shift 1
;;
--cache-dir)
CACHE_DIR="${2:-}"
shift 2
;;
--mcp-version)
MCP_VERSION="${2:-}"
shift 2
;;
--node-version)
NODE_VERSION="${2:-}"
shift 2
;;
--force)
FORCE=1
shift 1
;;
--dry-run)
DRY_RUN=1
shift 1
;;
--no-check)
NO_CHECK=1
shift 1
;;
-h|--help)
sed -n '1,28p' "$0"
exit 0
;;
*)
echo "未知参数: $1" >&2
exit 2
;;
esac
done
if [[ $ALL_COMMON -eq 1 ]]; then
TARGETS=("${COMMON_TARGETS[@]}")
fi
if [[ ${#TARGETS[@]} -eq 0 ]]; then
# Default to current platform
case "$(uname -s)" in
Darwin)
case "$(uname -m)" in
arm64) TARGETS=("aarch64-apple-darwin") ;;
*) TARGETS=("x86_64-apple-darwin") ;;
esac
;;
Linux)
case "$(uname -m)" in
aarch64) TARGETS=("aarch64-unknown-linux-gnu") ;;
armv7l) TARGETS=("aarch64-unknown-linux-gnu") ;;
*) TARGETS=("x86_64-unknown-linux-gnu") ;;
esac
;;
MINGW*|MSYS*|CYGWIN*)
TARGETS=("x86_64-pc-windows-msvc")
;;
esac
fi
# 如果未指定 MCP 版本,从 GitHub 获取最新版本
if [[ -z "$MCP_VERSION" ]]; then
echo "正在从 GitHub 获取 mcp-stdio-proxy 最新版本..."
MCP_VERSION=$(get_latest_mcp_version) || exit 1
echo "检测到最新版本: $MCP_VERSION"
fi
mkdir -p "$CACHE_DIR" "$TMP_DIR"
MANIFEST_FILE="$CACHE_DIR/sidecar-download-manifest.txt"
download_file() {
local url="$1"
local out="$2"
if [[ $DRY_RUN -eq 1 ]]; then
echo "DRY curl -fL \"$url\" -o \"$out\""
return 0
fi
curl -fL "$url" -o "$out"
}
download_first_success() {
local out="$1"
shift
local urls=("$@")
local url
local i=0
LAST_SUCCESS_URL=""
for url in "${urls[@]}"; do
i=$((i + 1))
if [[ $DRY_RUN -eq 1 ]]; then
echo "DRY candidate[$i] -> $url"
download_file "$url" "$out"
LAST_SUCCESS_URL="$url"
return 0
fi
if curl -fL "$url" -o "$out"; then
LAST_SUCCESS_URL="$url"
return 0
fi
echo " WARN 下载失败,尝试下一个候选: $url" >&2
done
return 1
}
extract_archive() {
local archive="$1"
local out_dir="$2"
rm -rf "$out_dir"
mkdir -p "$out_dir"
# 按扩展名选择解压方式Windows 下 MCP 为 .zip必须用 unzip若保存时无扩展名会误走 tar 导致失败)
if [[ "$archive" == *.zip ]]; then
unzip -q -o "$archive" -d "$out_dir"
elif [[ "$archive" == *.tar.xz || "$archive" == *.tar.gz || "$archive" == *.tgz ]]; then
tar -xf "$archive" -C "$out_dir"
else
# 无扩展名或未知格式时用 file 检测
if command -v file &>/dev/null; then
case "$(file -b "$archive")" in
*[Zz]ip*)
unzip -q -o "$archive" -d "$out_dir"
;;
*)
tar -xf "$archive" -C "$out_dir"
;;
esac
else
tar -xf "$archive" -C "$out_dir"
fi
fi
}
if [[ $DRY_RUN -eq 0 ]]; then
{
echo "# sidecar download manifest"
echo "# generated_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "# mcp_version=$MCP_VERSION node_version=$NODE_VERSION"
echo "# cache_dir=$CACHE_DIR"
echo "# format: target component filename source"
} > "$MANIFEST_FILE"
fi
for TARGET in "${TARGETS[@]}"; do
MCP_ARTIFACT_CANDIDATES=()
case "$TARGET" in
x86_64-pc-windows-msvc)
MCP_ARTIFACT_CANDIDATES=("mcp-stdio-proxy-x86_64-pc-windows-msvc.zip")
MCP_BIN_NAME="mcp-proxy.exe"
NODE_ARTIFACT="node-v${NODE_VERSION}-win-x64.zip"
NODE_BIN_NAME="node.exe"
DEST_EXT=".exe"
;;
aarch64-pc-windows-msvc)
MCP_ARTIFACT_CANDIDATES=(
"mcp-stdio-proxy-aarch64-pc-windows-msvc.zip"
"mcp-stdio-proxy-x86_64-pc-windows-msvc.zip"
)
MCP_BIN_NAME="mcp-proxy.exe"
NODE_ARTIFACT="node-v${NODE_VERSION}-win-arm64.zip"
NODE_BIN_NAME="node.exe"
DEST_EXT=".exe"
;;
x86_64-apple-darwin)
MCP_ARTIFACT_CANDIDATES=("mcp-stdio-proxy-x86_64-apple-darwin.tar.xz")
MCP_BIN_NAME="mcp-proxy"
NODE_ARTIFACT="node-v${NODE_VERSION}-darwin-x64.tar.gz"
NODE_BIN_NAME="node"
DEST_EXT=""
;;
aarch64-apple-darwin)
MCP_ARTIFACT_CANDIDATES=("mcp-stdio-proxy-aarch64-apple-darwin.tar.xz")
MCP_BIN_NAME="mcp-proxy"
NODE_ARTIFACT="node-v${NODE_VERSION}-darwin-arm64.tar.gz"
NODE_BIN_NAME="node"
DEST_EXT=""
;;
x86_64-unknown-linux-gnu)
MCP_ARTIFACT_CANDIDATES=("mcp-stdio-proxy-x86_64-unknown-linux-gnu.tar.xz")
MCP_BIN_NAME="mcp-proxy"
NODE_ARTIFACT="node-v${NODE_VERSION}-linux-x64.tar.xz"
NODE_BIN_NAME="node"
DEST_EXT=""
;;
aarch64-unknown-linux-gnu)
MCP_ARTIFACT_CANDIDATES=("mcp-stdio-proxy-aarch64-unknown-linux-gnu.tar.xz")
MCP_BIN_NAME="mcp-proxy"
NODE_ARTIFACT="node-v${NODE_VERSION}-linux-arm64.tar.xz"
NODE_BIN_NAME="node"
DEST_EXT=""
;;
*)
echo "不支持的 target: $TARGET" >&2
exit 2
;;
esac
MCP_CACHE="$CACHE_DIR/mcp-proxy-${TARGET}${DEST_EXT}"
NODE_CACHE="$CACHE_DIR/node-runtime-${TARGET}${DEST_EXT}"
# 从第一个候选 artifact 得到压缩包后缀(.zip 或 .tar.xz保证 extract_archive 能正确选择 unzip/tar
MCP_ARCHIVE_SUFFIX="${MCP_ARTIFACT_CANDIDATES[0]#*${TARGET}}"
MCP_PKG="$TMP_DIR/mcp-stdio-proxy-${TARGET}${MCP_ARCHIVE_SUFFIX}"
# 缓存版本记录文件
MCP_VERSION_FILE="$CACHE_DIR/mcp-proxy-${TARGET}.version"
echo "target: $TARGET"
echo " cache dir : $CACHE_DIR"
echo " mcp version : $MCP_VERSION"
echo " node version: $NODE_VERSION"
# 检查是否需要重新下载force标志、缓存文件不存在、或版本不一致
NEED_DOWNLOAD=0
if [[ $FORCE -eq 1 ]]; then
NEED_DOWNLOAD=1
rm -f "$MCP_VERSION_FILE"
echo " FORCE 强制重新下载"
elif [[ ! -f "$MCP_CACHE" || ! -f "$NODE_CACHE" ]]; then
NEED_DOWNLOAD=1
echo " 缓存文件不存在,需要下载"
elif [[ -f "$MCP_VERSION_FILE" ]]; then
CACHED_VERSION=$(cat "$MCP_VERSION_FILE" 2>/dev/null || echo "")
if [[ "$CACHED_VERSION" != "$MCP_VERSION" ]]; then
NEED_DOWNLOAD=1
echo " 版本不一致 (缓存: $CACHED_VERSION, 最新: $MCP_VERSION),需要更新"
fi
else
# 缓存文件存在但没有版本记录,尝试从二进制获取版本
CACHED_VERSION=$("$MCP_CACHE" -V 2>/dev/null | awk '{print $2}' || echo "")
if [[ -z "$CACHED_VERSION" ]]; then
# 无法获取版本,保守起见重新下载
NEED_DOWNLOAD=1
echo " 无法获取缓存版本,需要重新下载"
elif [[ "$CACHED_VERSION" != "$MCP_VERSION" ]]; then
NEED_DOWNLOAD=1
echo " 版本不一致 (缓存: $CACHED_VERSION, 最新: $MCP_VERSION),需要更新"
else
# 版本一致,补写版本记录文件
echo "$MCP_VERSION" > "$MCP_VERSION_FILE"
fi
fi
if [[ $NEED_DOWNLOAD -eq 0 ]]; then
echo " SKIP 已存在缓存: $(basename "$MCP_CACHE"), $(basename "$NODE_CACHE")"
else
NODE_URL="https://nodejs.org/dist/v${NODE_VERSION}/${NODE_ARTIFACT}"
NODE_PKG="$TMP_DIR/${NODE_ARTIFACT}"
MCP_URLS=()
for artifact in "${MCP_ARTIFACT_CANDIDATES[@]}"; do
MCP_URLS+=("https://qiming-packages.oss-rg-china-mainland.aliyuncs.com/mcp-stdio-proxy/v${MCP_VERSION}/${artifact}")
done
if ! download_first_success "$MCP_PKG" "${MCP_URLS[@]}"; then
echo "下载 mcp-proxy 失败 (target=$TARGET, version=$MCP_VERSION)" >&2
exit 1
fi
MCP_SELECTED_URL="$LAST_SUCCESS_URL"
download_file "$NODE_URL" "$NODE_PKG"
if [[ $DRY_RUN -eq 1 ]]; then
echo " DRY copy -> $MCP_CACHE"
echo " DRY copy -> $NODE_CACHE"
echo ""
continue
fi
MCP_EXTRACT_DIR="$TMP_DIR/extract-mcp-${TARGET}"
NODE_EXTRACT_DIR="$TMP_DIR/extract-node-${TARGET}"
extract_archive "$MCP_PKG" "$MCP_EXTRACT_DIR"
extract_archive "$NODE_PKG" "$NODE_EXTRACT_DIR"
MCP_SRC="$(find "$MCP_EXTRACT_DIR" -type f -name "$MCP_BIN_NAME" | head -n 1)"
NODE_SRC="$(find "$NODE_EXTRACT_DIR" -type f -name "$NODE_BIN_NAME" | head -n 1)"
if [[ -z "$MCP_SRC" || -z "$NODE_SRC" ]]; then
echo "解压后未找到目标二进制 (target=$TARGET)" >&2
exit 1
fi
cp -f "$MCP_SRC" "$MCP_CACHE"
cp -f "$NODE_SRC" "$NODE_CACHE"
chmod +x "$MCP_CACHE" "$NODE_CACHE" || true
# 保存版本号到版本文件
echo "$MCP_VERSION" > "$MCP_VERSION_FILE"
echo " DONE cache $(basename "$MCP_CACHE")"
echo " DONE cache $(basename "$NODE_CACHE")"
echo "$TARGET mcp-proxy $(basename "$MCP_CACHE") $MCP_SELECTED_URL" >> "$MANIFEST_FILE"
echo "$TARGET node-runtime $(basename "$NODE_CACHE") $NODE_URL" >> "$MANIFEST_FILE"
fi
echo ""
done
echo "完成。"
echo " cache: $CACHE_DIR"
if [[ $NO_CHECK -eq 0 && $DRY_RUN -eq 0 ]]; then
echo ""
echo "开始自动校验下载项 sidecar..."
for t in "${TARGETS[@]}"; do
"$SCRIPT_DIR/check-sidecars.sh" --downloaded-only --target "$t" --dir "$CACHE_DIR"
done
fi

View File

@@ -0,0 +1,201 @@
#!/usr/bin/env bash
#
# download-uv.sh — 下载 uv 到 Electron 资源目录
#
# 用法:
# ./scripts/download-uv.sh # 自动检测当前平台
# ./scripts/download-uv.sh darwin arm64 # 指定平台和架构
# ./scripts/download-uv.sh linux x64
# ./scripts/download-uv.sh win x64
#
# 支持平台/架构:
# darwin arm64 | x64
# linux x64 | arm64
# win x64
#
set -euo pipefail
UV_VERSION="0.10.0"
RESOURCE_DIR="crates/agent-electron-client/resources/uv"
# 项目根目录
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
TARGET_DIR="${PROJECT_ROOT}/${RESOURCE_DIR}"
# === 平台检测 ===
detect_platform() {
local uname_s
uname_s="$(uname -s)"
case "${uname_s}" in
Darwin) echo "darwin" ;;
Linux) echo "linux" ;;
MINGW*|MSYS*|CYGWIN*) echo "win" ;;
*) echo "unsupported"; return 1 ;;
esac
}
detect_arch() {
local uname_m
uname_m="$(uname -m)"
case "${uname_m}" in
x86_64|amd64) echo "x64" ;;
arm64|aarch64) echo "arm64" ;;
*) echo "unsupported"; return 1 ;;
esac
}
PLATFORM="${1:-$(detect_platform)}"
ARCH="${2:-$(detect_arch)}"
echo "==> uv v${UV_VERSION} for ${PLATFORM}-${ARCH}"
# === 构建下载 URL ===
# 映射到 uv release target 名称
case "${PLATFORM}-${ARCH}" in
darwin-arm64) UV_TARGET="uv-aarch64-apple-darwin" ;;
darwin-x64) UV_TARGET="uv-x86_64-apple-darwin" ;;
linux-x64) UV_TARGET="uv-x86_64-unknown-linux-gnu" ;;
linux-arm64) UV_TARGET="uv-aarch64-unknown-linux-gnu" ;;
win-x64) UV_TARGET="uv-x86_64-pc-windows-msvc" ;;
*) echo "Error: unsupported platform-arch: ${PLATFORM}-${ARCH}" >&2; exit 1 ;;
esac
if [ "${PLATFORM}" = "win" ]; then
FILENAME="${UV_TARGET}.zip"
else
FILENAME="${UV_TARGET}.tar.gz"
fi
URL="https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/${FILENAME}"
# === 检查是否已下载 ===
UV_BIN="${TARGET_DIR}/bin/uv"
if [ "${PLATFORM}" = "win" ]; then
UV_BIN="${TARGET_DIR}/bin/uv.exe"
fi
if [ -f "${UV_BIN}" ]; then
EXISTING_VERSION="$("${UV_BIN}" --version 2>/dev/null | awk '{print $2}' || true)"
if [ "${EXISTING_VERSION}" = "${UV_VERSION}" ]; then
if [ "${PLATFORM}" = "darwin" ]; then
case "${ARCH}" in
arm64) EXPECTED_ARCH="arm64" ;;
x64) EXPECTED_ARCH="x86_64" ;;
esac
ACTUAL_ARCH="$(file "${UV_BIN}" | grep -oE 'arm64|x86_64' | head -1 || true)"
if [ "${ACTUAL_ARCH}" != "${EXPECTED_ARCH}" ]; then
echo "==> uv v${UV_VERSION} exists but architecture mismatch (have ${ACTUAL_ARCH}, need ${EXPECTED_ARCH}), re-downloading..."
else
echo "==> uv v${UV_VERSION} (${ARCH}) already exists at ${TARGET_DIR}, skipping download"
exit 0
fi
else
echo "==> uv v${UV_VERSION} already exists at ${TARGET_DIR}, skipping download"
exit 0
fi
fi
fi
# === 下载 ===
TMPDIR_DL="$(mktemp -d)"
trap 'rm -rf "${TMPDIR_DL}"' EXIT
echo "==> Downloading ${URL}"
if command -v curl &>/dev/null; then
curl -fSL --progress-bar -o "${TMPDIR_DL}/${FILENAME}" "${URL}"
elif command -v wget &>/dev/null; then
wget -q --show-progress -O "${TMPDIR_DL}/${FILENAME}" "${URL}"
else
echo "Error: curl or wget required" >&2
exit 1
fi
# === 解压 ===
echo "==> Extracting..."
EXTRACT_DIR="${TMPDIR_DL}/extracted"
mkdir -p "${EXTRACT_DIR}"
if [ "${PLATFORM}" = "win" ]; then
# Windows: 尝试多种解压方式
if command -v unzip &>/dev/null; then
unzip -q "${TMPDIR_DL}/${FILENAME}" -d "${EXTRACT_DIR}"
elif command -v pwsh &>/dev/null || command -v powershell &>/dev/null; then
# PowerShell 7+ 支持 tar 格式
pwsh -Command "Expand-Archive -Path '${TMPDIR_DL}/${FILENAME}' -DestinationPath '${EXTRACT_DIR}' -Force" 2>/dev/null || \
powershell -Command "Expand-Archive -Path '${TMPDIR_DL}/${FILENAME}' -DestinationPath '${EXTRACT_DIR}' -Force"
elif command -v tar &>/dev/null; then
# 某些 Windows 环境下的 tar 也可以处理 zip
tar -xf "${TMPDIR_DL}/${FILENAME}" -C "${EXTRACT_DIR}"
else
echo "Error: No extraction tool available (unzip, powershell, or tar)" >&2
exit 1
fi
else
tar -xzf "${TMPDIR_DL}/${FILENAME}" -C "${EXTRACT_DIR}"
fi
# 定位 uv 二进制所在目录(不同平台/版本的归档结构可能不同:有或无外层子目录)
if [ "${PLATFORM}" = "win" ]; then
UV_FOUND="$(find "${EXTRACT_DIR}" -name "uv.exe" -type f | head -1)"
else
UV_FOUND="$(find "${EXTRACT_DIR}" -name "uv" -type f | head -1)"
fi
if [ -z "${UV_FOUND}" ]; then
echo "Error: uv binary not found after extraction" >&2
echo "==> Contents of extract dir:" >&2
find "${EXTRACT_DIR}" -type f >&2
exit 1
fi
INNER_DIR="$(dirname "${UV_FOUND}")"
# === 复制到资源目录 ===
echo "==> Installing to ${TARGET_DIR}"
rm -rf "${TARGET_DIR}"
mkdir -p "${TARGET_DIR}/bin"
if [ "${PLATFORM}" = "win" ]; then
cp "${INNER_DIR}/uv.exe" "${TARGET_DIR}/bin/"
if [ -f "${INNER_DIR}/uvx.exe" ]; then
cp "${INNER_DIR}/uvx.exe" "${TARGET_DIR}/bin/"
fi
else
cp "${INNER_DIR}/uv" "${TARGET_DIR}/bin/"
if [ -f "${INNER_DIR}/uvx" ]; then
cp "${INNER_DIR}/uvx" "${TARGET_DIR}/bin/"
fi
chmod 755 "${TARGET_DIR}/bin/uv"
[ -f "${TARGET_DIR}/bin/uvx" ] && chmod 755 "${TARGET_DIR}/bin/uvx"
fi
# === 验证 ===
echo "==> Verifying installation..."
if [ "${PLATFORM}" = "win" ]; then
echo " (skipping verification on Windows — cross-platform build)"
else
INSTALLED_VERSION="$("${TARGET_DIR}/bin/uv" --version | awk '{print $2}')"
if [ "${INSTALLED_VERSION}" = "${UV_VERSION}" ]; then
echo "==> OK: uv ${INSTALLED_VERSION}"
else
echo "Error: Expected ${UV_VERSION}, got ${INSTALLED_VERSION}" >&2
exit 1
fi
fi
# === 输出大小统计 ===
echo "==> Size:"
du -sh "${TARGET_DIR}/bin" 2>/dev/null || true
du -sh "${TARGET_DIR}" | sed 's/\t/ /' | awk '{print " Total: " $1}'
echo "==> Done"

View File

@@ -0,0 +1,80 @@
#!/bin/bash
# 预下载 npm 包 tarball 到本地目录,便于后续离线/半离线安装
#
# 用法:
# ./scripts/prefetch-npm-tarballs.sh
# ./scripts/prefetch-npm-tarballs.sh --out /tmp/qiming-npm-tarballs
# ./scripts/prefetch-npm-tarballs.sh --registry https://registry.npmmirror.com/
# ./scripts/prefetch-npm-tarballs.sh --package qiming-file-server --package mcp-stdio-proxy
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
DEFAULT_OUT="$ROOT_DIR/.cache/npm-tarballs"
OUT_DIR="$DEFAULT_OUT"
REGISTRY="${QIMING_NPM_REGISTRY:-https://registry.npmmirror.com/}"
DRY_RUN=0
PACKAGES=()
DEFAULT_PACKAGES=(
"mcp-stdio-proxy"
"qiming-file-server"
"qimingcode"
"claude-code-acp-ts"
)
while [[ $# -gt 0 ]]; do
case "$1" in
--out)
OUT_DIR="${2:-}"
shift 2
;;
--registry)
REGISTRY="${2:-}"
shift 2
;;
--package)
PACKAGES+=("${2:-}")
shift 2
;;
--dry-run)
DRY_RUN=1
shift 1
;;
-h|--help)
sed -n '1,20p' "$0"
exit 0
;;
*)
echo "未知参数: $1" >&2
exit 2
;;
esac
done
if [[ ${#PACKAGES[@]} -eq 0 ]]; then
PACKAGES=("${DEFAULT_PACKAGES[@]}")
fi
mkdir -p "$OUT_DIR"
echo "npm tarball 预下载目录: $OUT_DIR"
echo "registry: $REGISTRY"
echo "packages: ${PACKAGES[*]}"
echo ""
for pkg in "${PACKAGES[@]}"; do
if [[ $DRY_RUN -eq 1 ]]; then
echo "DRY npm pack \"$pkg\" --pack-destination \"$OUT_DIR\" --registry \"$REGISTRY\""
continue
fi
echo ">>> downloading $pkg"
npm pack "$pkg" --pack-destination "$OUT_DIR" --registry "$REGISTRY"
done
echo ""
echo "完成,已下载到: $OUT_DIR"
ls -1 "$OUT_DIR" | sed 's/^/ - /'

View File

@@ -0,0 +1,18 @@
#!/bin/bash
# prepare-sidecars.sh — 此脚本已弃用
#
# Electron 使用 npm 脚本准备 sidecar 二进制:
# cd crates/agent-electron-client
# npm run prepare # 准备所有
# npm run prepare:node # 仅 Node.js
# npm run prepare:uv # 仅 uv
# npm run prepare:mcp-proxy # 仅 mcp-proxy
# npm run prepare:lanproxy # 仅 lanproxy
# npm run prepare:qimingcode # 仅 qimingcode
echo "警告: 此脚本已弃用"
echo ""
echo "请使用 Electron 的 npm prepare 脚本:"
echo " cd crates/agent-electron-client"
echo " npm run prepare"
echo ""

View File

@@ -0,0 +1,117 @@
#!/usr/bin/env bash
#
# sign-macos-resource-bins.sh — 在 macOS 上对即将被打进 App 的第三方二进制做 codesign
#
# 背景:
# Electron 打包时会把 resources 中的文件拷贝到 .app/Contents/Resources/
# 但不会对这些可执行文件做签名。Apple 公证要求包内所有可执行文件必须:
# - 使用有效的 Developer ID 证书签名
# - 包含安全时间戳 (--timestamp)
# - 启用硬化运行时 (--options runtime)
# 因此需在 electron-builder 打包之前,对 resources 下的 node 与 uv/uvx
# 预先签名这样打进包后即可通过公证。node 与 uv 均不省略,一并签名。)
#
# 用法:
# 在项目根目录执行: ./scripts/sign-macos-resource-bins.sh
# 需设置环境变量 APPLE_SIGNING_IDENTITY例如:
# export APPLE_SIGNING_IDENTITY="Developer ID Application: Your Name (TEAM_ID)"
# make electron-bundle
#
# 仅在 macOS 上执行;非 macOS 或未设置 APPLE_SIGNING_IDENTITY 时直接退出 0。
#
set -euo pipefail
# 仅 macOS 需要此步骤
if [ "$(uname -s)" != "Darwin" ]; then
exit 0
fi
# 明确说明本步骤会对哪些资源签名
echo "==> [sign-macos-resource-bins] 将对 node、uv/uvx 做 codesign供 macOS 公证)"
# 未配置签名身份时跳过(例如 CI 未配置或非发布构建)
if [ -z "${APPLE_SIGNING_IDENTITY:-}" ]; then
echo "==> [sign-macos-resource-bins] 未设置 APPLE_SIGNING_IDENTITY跳过对 node/uv 的签名"
exit 0
fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
ELECTRON_RESOURCES="${PROJECT_ROOT}/crates/agent-electron-client/resources"
# node-runtime 为 V8/Node需 JIT 权限,否则 macOS 上会 SIGTRAP
NODE_RUNTIME_ENTITLEMENTS="${ELECTRON_RESOURCES}/node.entitlements"
# 使用一致的 identity 对单个二进制签名Developer ID + 时间戳 + 硬化运行时
sign_one() {
local bin_path="$1"
if [ ! -f "$bin_path" ]; then
return 0
fi
# 若已是同 identity 签名则可能跳过;--force 确保使用当前 identity 重签
codesign --force --timestamp --options runtime -s "${APPLE_SIGNING_IDENTITY}" -- "$bin_path"
echo " signed: $bin_path"
}
# 对 node-runtime 使用带 JIT entitlement 的签名(仅 macOS Mach-O
sign_node_runtime_with_jit() {
local bin_path="$1"
if [ ! -f "$bin_path" ]; then
return 0
fi
if ! file "$bin_path" | grep -q "Mach-O"; then
return 0
fi
codesign --force --timestamp --options runtime \
--entitlements "${NODE_RUNTIME_ENTITLEMENTS}" \
-s "${APPLE_SIGNING_IDENTITY}" -- "$bin_path"
echo " signed (with JIT entitlement): $bin_path"
}
echo "==> [sign-macos-resource-bins] 使用 identity: ${APPLE_SIGNING_IDENTITY}"
# 1. 签名 resources/uv/bin 下的 uv、uvx公证报错即为此二者
UV_BIN_DIR="${ELECTRON_RESOURCES}/uv/bin"
if [ -d "${UV_BIN_DIR}" ]; then
echo "==> [sign-macos-resource-bins] 签名 uv 资源 (uv, uvx)..."
for name in uv uvx; do
[ -f "${UV_BIN_DIR}/${name}" ] && sign_one "${UV_BIN_DIR}/${name}"
done
else
echo "==> [sign-macos-resource-bins] 未找到 ${UV_BIN_DIR},跳过 uv 签名"
fi
# 2. 签名 resources/node/bin 下的可执行文件
# 注意node 是 V8/Node.js 运行时,需要 JIT 权限
NODE_BIN_DIR="${ELECTRON_RESOURCES}/node/bin"
if [ -d "${NODE_BIN_DIR}" ]; then
echo "==> [sign-macos-resource-bins] 签名 node 资源 (带 JIT entitlement)..."
for f in "${NODE_BIN_DIR}"/*; do
if [ -f "$f" ] && [ -x "$f" ]; then
# 排除脚本或非 Mach-O如 .sh仅签名可执行二进制
if file "$f" | grep -q "Mach-O"; then
# node 是 V8 运行时,需要 JIT 权限
sign_node_runtime_with_jit "$f"
fi
fi
done
else
echo "==> [sign-macos-resource-bins] 未找到 ${NODE_BIN_DIR},跳过 node 签名"
fi
# 3. 签名 resources/lanproxy/bin 下的 lanproxy
LANPROXY_BIN_DIR="${ELECTRON_RESOURCES}/lanproxy/bin"
if [ -d "${LANPROXY_BIN_DIR}" ]; then
echo "==> [sign-macos-resource-bins] 签名 lanproxy 资源..."
for f in "${LANPROXY_BIN_DIR}"/*; do
if [ -f "$f" ] && [ -x "$f" ]; then
if file "$f" | grep -q "Mach-O"; then
sign_one "$f"
fi
fi
done
else
echo "==> [sign-macos-resource-bins] 未找到 ${LANPROXY_BIN_DIR},跳过 lanproxy 签名"
fi
echo "==> [sign-macos-resource-bins] node / uv / lanproxy 资源签名完成"