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,143 @@
#!/usr/bin/env node
/**
* 构建 Windows 专用 Sandbox helperqiming-sandbox-helper.exe
* 产物仅被 Windows 版 Electron 客户端加载;建议在 Windows 宿主上执行,或在 macOS/Linux 上使用 --win 交叉编译。
*
* Usage:
* npm run build:sandbox-helper # Windows 宿主:本机构建
* npm run build:sandbox-helper -- --win # 非 Windows交叉编译 Windows x64
*
* Output: resources/sandbox-helper/qiming-sandbox-helper.exe
*/
const { execSync } = require("child_process");
const fs = require("fs");
const path = require("path");
const { getProjectRoot, resolveFromProject } = require("../utils/project-paths");
const projectRoot = getProjectRoot();
/**
* crate 位于 monorepo crates/ 下,与 agent-electron-client 同级。
* getProjectRoot() 返回 crates/agent-electron-client故 crate 路径为
* path.resolve(projectRoot, '..', 'windows-sandbox-helper')。
*/
const crateDir = path.resolve(projectRoot, "..", "windows-sandbox-helper");
const outputDir = resolveFromProject("resources", "sandbox-helper");
function ensureDir(dirPath) {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
}
function main() {
const args = process.argv.slice(2);
const isWinTarget = args.includes("--win");
// Check if Rust is available
try {
execSync("rustc --version", { stdio: "pipe" });
} catch {
console.warn(
"[build-sandbox-helper] Rust 未安装跳过Windows Sandbox helper 仅在 Windows 上需要)",
);
return;
}
// Check if we're on Windows or targeting Windows
const onWindows = process.platform === "win32";
if (!onWindows && !isWinTarget) {
console.warn(
"[build-sandbox-helper] 非 Windows 环境;如需交叉编译请使用: npm run build:sandbox-helper -- --win",
);
return;
}
if (!fs.existsSync(path.join(crateDir, "Cargo.toml"))) {
console.error(
`[build-sandbox-helper] 未找到 crate 目录: ${crateDir}(请确认 monorepo 中存在 crates/windows-sandbox-helper`,
);
process.exit(1);
}
ensureDir(outputDir);
const buildArgs = ["cargo", "build", "--release", "--bin", "qiming-sandbox-helper"];
if (isWinTarget) {
if (process.platform === "win32") {
console.warn("[build-sandbox-helper] --win 在 Windows 上无效(已是 Windows");
} else {
// Cross-compile for Windows x64
const target = "x86_64-pc-windows-msvc";
console.log(`[build-sandbox-helper] 交叉编译 Windows x64target: ${target}`);
buildArgs.push("--target", target);
}
}
console.log(`[build-sandbox-helper] 构建: ${buildArgs.join(" ")}`);
console.log(`[build-sandbox-helper] crate: ${crateDir}`);
try {
execSync(buildArgs.join(" "), {
cwd: crateDir,
stdio: "inherit",
});
// Determine source path
let srcPath;
if (isWinTarget && process.platform !== "win32") {
srcPath = path.join(
crateDir,
"target",
"x86_64-pc-windows-msvc",
"release",
"qiming-sandbox-helper.exe",
);
} else {
srcPath = path.join(crateDir, "target", "release", "qiming-sandbox-helper.exe");
}
if (!fs.existsSync(srcPath)) {
if (isWinTarget && process.platform !== "win32") {
srcPath = path.join(
crateDir,
"target",
"x86_64-pc-windows-msvc",
"debug",
"qiming-sandbox-helper.exe",
);
} else {
srcPath = path.join(crateDir, "target", "debug", "qiming-sandbox-helper.exe");
}
}
if (!fs.existsSync(srcPath)) {
throw new Error(`构建产物未找到: ${srcPath}`);
}
const destPath = path.join(outputDir, "qiming-sandbox-helper.exe");
fs.copyFileSync(srcPath, destPath);
const stats = fs.statSync(destPath);
console.log(
`[build-sandbox-helper] 完成: ${destPath} (${(stats.size / 1024).toFixed(1)} KB)`,
);
} catch (error) {
if (error instanceof Error && error.message.includes("build产物未找到")) {
throw error;
}
console.error(
"[build-sandbox-helper] 构建失败(可能是因为非 Windows 平台):",
error instanceof Error ? error.message : String(error),
);
process.exit(1);
}
}
try {
main();
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
}

View File

@@ -0,0 +1,105 @@
#!/usr/bin/env node
/**
* 准备 claude-code-acp-ts 源码并复制到 resources/
*
* 逻辑:
* 1. 若 sources/claude-code-acp-ts 不存在 → git clone + npm install + npm run build
* 2. 若存在但无 node_modules → npm install + npm run build
* 3. 否则跳过构建(认为已就绪)
* 4. 复制到 resources/claude-code-acp-ts/
*
* 产物:
* resources/claude-code-acp-ts/
* ├── dist/
* ├── node_modules/
* └── package.json
*/
const path = require('path');
const fs = require('fs');
const { execSync } = require('child_process');
const { getProjectRoot } = require('../utils/project-paths');
const projectRoot = getProjectRoot();
const electronClientRoot = projectRoot;
// 从 package.json 读取源码地址
const pkgJson = JSON.parse(fs.readFileSync(path.join(electronClientRoot, 'package.json'), 'utf8'));
const { url: GIT_REPO, branch: GIT_BRANCH } = pkgJson.bundledSources['claude-code-acp-ts'];
const SOURCE_DIR = path.join(electronClientRoot, 'sources', 'claude-code-acp-ts');
const destDir = path.join(electronClientRoot, 'resources', 'claude-code-acp-ts');
function exec(cmd, opts = {}) {
console.log(` $ ${cmd}`);
execSync(cmd, { stdio: 'inherit', ...opts });
}
function main() {
// 1. 克隆或更新源码
if (!fs.existsSync(path.join(SOURCE_DIR, '.git'))) {
console.log('[prepare-claude-code-acp-ts] 克隆源码...');
exec(`git clone --branch ${GIT_BRANCH} ${GIT_REPO} "${SOURCE_DIR}"`);
} else {
console.log('[prepare-claude-code-acp-ts] 更新源码...');
exec(`cd "${SOURCE_DIR}" && git checkout ${GIT_BRANCH} && git pull`);
}
// 2. 检查构建产物是否存在
const hasBuild = fs.existsSync(path.join(SOURCE_DIR, 'dist'));
const hasNodeModules = fs.existsSync(path.join(SOURCE_DIR, 'node_modules'));
if (!hasBuild || !hasNodeModules) {
// 清理旧的 node_modules若有
if (fs.existsSync(path.join(SOURCE_DIR, 'node_modules'))) {
console.log('[prepare-claude-code-acp-ts] 清理旧的 node_modules...');
exec(`rm -rf "${path.join(SOURCE_DIR, 'node_modules')}"`);
}
// 3. 安装依赖
console.log('[prepare-claude-code-acp-ts] 安装依赖...');
exec(`cd "${SOURCE_DIR}" && npm install --ignore-scripts`);
// 4. 构建
// 注意:不用 npm run build因为其 build 脚本可能是 ./node_modules/.bin/tscUnix 风格Windows 不认识
//改用 npx tsc 替代,可跨平台
console.log('[prepare-claude-code-acp-ts] 构建项目...');
exec(`cd "${SOURCE_DIR}" && npx tsc`);
} else {
console.log('[prepare-claude-code-acp-ts] 构建产物已就绪,跳过构建');
}
// 5. 读取版本
const srcPkg = JSON.parse(fs.readFileSync(path.join(SOURCE_DIR, 'package.json'), 'utf8'));
console.log(`[prepare-claude-code-acp-ts] 源码版本: ${srcPkg.name}@${srcPkg.version}`);
// 6. 清理并创建目标目录
if (fs.existsSync(destDir)) {
fs.rmSync(destDir, { recursive: true });
}
fs.mkdirSync(destDir, { recursive: true });
// 7. 复制 dist/
console.log('[prepare-claude-code-acp-ts] 复制 dist/...');
exec(`cp -R "${path.join(SOURCE_DIR, 'dist')}" "${destDir}/"`);
// 8. 复制 package.json
fs.copyFileSync(
path.join(SOURCE_DIR, 'package.json'),
path.join(destDir, 'package.json')
);
// 9. 复制 node_modules/
console.log('[prepare-claude-code-acp-ts] 复制 node_modules/...');
exec(`cp -R "${path.join(SOURCE_DIR, 'node_modules')}" "${destDir}/"`);
// 10. 复制 LICENSE
const licenseSrc = path.join(SOURCE_DIR, 'LICENSE');
if (fs.existsSync(licenseSrc)) {
fs.copyFileSync(licenseSrc, path.join(destDir, 'LICENSE'));
}
console.log(`[prepare-claude-code-acp-ts] ✓ resources/claude-code-acp-ts/ (${srcPkg.version})`);
}
main();

View File

@@ -0,0 +1,343 @@
#!/usr/bin/env node
/**
* Git 集成(仅 Windows 客户端)
*
* 仅在 Windows 构建时执行:准备 resources/git/PortableGit + bash供端口检查等脚本使用。
* macOS/Linux 使用系统 git/bash不打包内置 Git。
*
* 参考 LobsterAI 方案scripts/setup-mingit.jsworkspace/lobsterAI
*
* - Windows下载 PortableGit.7z.exe并用 7zip-bin 解压,或使用本地归档/缓存
* - 非 Windows默认跳过可通过 --required 或 QIMING_SETUP_GIT_FORCE=1 在 macOS 上为 Windows 打包做准备
*
* 环境变量:
* QIMING_PORTABLE_GIT_ARCHIVE - 本地离线归档路径CI 推荐预先下载后设置)
* QIMING_GIT_URL - 下载地址覆盖(镜像等)
* QIMING_SETUP_GIT_FORCE=1 - 非 Windows 主机也执行准备(用于在 macOS 上打 Windows 包)
*
* CLI
* --required - 若无法准备则失败CI 中建议配合 QIMING_PORTABLE_GIT_ARCHIVE 使用)
*/
'use strict';
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
const { Readable } = require('stream');
const { pipeline } = require('stream/promises');
const { getProjectRoot } = require('../utils/project-paths');
// 与 LobsterAI 保持一致
const GIT_VERSION = '2.47.1';
const PORTABLE_GIT_FILE = `PortableGit-${GIT_VERSION}-64-bit.7z.exe`;
const DEFAULT_PORTABLE_GIT_URL =
`https://github.com/git-for-windows/git/releases/download/v${GIT_VERSION}.windows.1/${PORTABLE_GIT_FILE}`;
const PROJECT_ROOT = getProjectRoot();
const GIT_ROOT = path.join(PROJECT_ROOT, 'resources', 'git');
const CACHE_DIR = path.join(GIT_ROOT, '.cache');
const DEFAULT_ARCHIVE_PATH = path.join(CACHE_DIR, PORTABLE_GIT_FILE);
// 需要删除的目录(与 LobsterAI 一致,减小体积)
const DIRS_TO_PRUNE = [
'doc',
'ReleaseNotes.html',
'README.portable',
path.join('mingw64', 'doc'),
path.join('mingw64', 'share', 'doc'),
path.join('mingw64', 'share', 'gtk-doc'),
path.join('mingw64', 'share', 'man'),
path.join('mingw64', 'share', 'gitweb'),
path.join('mingw64', 'share', 'git-gui'),
path.join('mingw64', 'libexec', 'git-core', 'git-gui'),
path.join('mingw64', 'libexec', 'git-core', 'git-gui--askpass'),
path.join('usr', 'share', 'doc'),
path.join('usr', 'share', 'man'),
path.join('usr', 'share', 'vim'),
path.join('usr', 'share', 'perl5'),
path.join('usr', 'lib', 'perl5'),
];
function parseArgs(argv) {
return {
required: argv.includes('--required'),
};
}
function resolveInputPath(input) {
if (typeof input !== 'string') return null;
const trimmed = input.trim();
if (!trimmed) return null;
return path.isAbsolute(trimmed) ? trimmed : path.resolve(process.cwd(), trimmed);
}
function isNonEmptyFile(filePath) {
try {
return fs.statSync(filePath).isFile() && fs.statSync(filePath).size > 0;
} catch {
return false;
}
}
function getDirSize(dir) {
let size = 0;
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
size += getDirSize(full);
} else {
size += fs.statSync(full).size;
}
}
return size;
}
/** 解析 7zip-bin 可执行路径(与 LobsterAI 一致) */
function resolve7zaPath() {
let path7za;
try {
({ path7za } = require('7zip-bin'));
} catch (error) {
throw new Error(
'缺少依赖 "7zip-bin",请先 npm install 后重试。' +
`原始错误: ${error instanceof Error ? error.message : String(error)}`
);
}
if (!path7za || !fs.existsSync(path7za)) {
throw new Error(`7zip-bin 可执行文件未找到: ${path7za || '(空路径)'}`);
}
return path7za;
}
/**
* 查找 PortableGit 的 bash.exe 路径(与 LobsterAI findPortableGitBash 一致)
* @param {string} [baseDir=GIT_ROOT]
* @returns {string|null}
*/
function findPortableGitBash(baseDir = GIT_ROOT) {
const candidates = [
path.join(baseDir, 'bin', 'bash.exe'),
path.join(baseDir, 'usr', 'bin', 'bash.exe'),
path.join(baseDir, 'mingw64', 'bin', 'bash.exe'),
path.join(baseDir, 'mingw64', 'usr', 'bin', 'bash.exe'),
];
for (const candidate of candidates) {
if (fs.existsSync(candidate)) {
return candidate;
}
}
return null;
}
/** 使用 fetch + stream 下载归档(与 LobsterAI 一致Node 18+ */
async function downloadArchive(url, destination) {
const response = await fetch(url, { redirect: 'follow' });
if (!response.ok || !response.body) {
throw new Error(`下载失败 (${response.status} ${response.statusText}): ${url}`);
}
fs.mkdirSync(path.dirname(destination), { recursive: true });
const tmpFile = `${destination}.download`;
try {
const stream = fs.createWriteStream(tmpFile);
await pipeline(Readable.fromWeb(response.body), stream);
if (!isNonEmptyFile(tmpFile)) {
throw new Error('下载的归档为空。');
}
fs.renameSync(tmpFile, destination);
} catch (error) {
try {
fs.rmSync(tmpFile, { force: true });
} catch {
// ignore
}
throw error;
}
}
function pruneUnneededFiles() {
let prunedCount = 0;
for (const relPath of DIRS_TO_PRUNE) {
const fullPath = path.join(GIT_ROOT, relPath);
if (!fs.existsSync(fullPath)) continue;
try {
fs.rmSync(fullPath, { recursive: true, force: true });
prunedCount++;
} catch (error) {
console.warn(
`[prepare-git] 删除失败: ${relPath}: ${error instanceof Error ? error.message : String(error)}`
);
}
}
console.log(`[prepare-git] 已删除 ${prunedCount} 个不需要的目录/文件`);
}
/**
* 使用 7zip-bin 解压 .7z.exe 到临时目录,再将内容移动到 GIT_ROOT
* 归档内通常有一层根目录(如 PortableGit 或 mingw64需把其内容移到 GIT_ROOT
*/
function extractArchive(archivePath) {
const sevenZip = resolve7zaPath();
const extractDir = path.join(GIT_ROOT, '.extract');
if (fs.existsSync(extractDir)) {
fs.rmSync(extractDir, { recursive: true, force: true });
}
fs.mkdirSync(extractDir, { recursive: true });
console.log(`[prepare-git] 使用 7zip-bin 解压: ${archivePath}`);
const result = spawnSync(sevenZip, ['x', archivePath, `-o${extractDir}`, '-y'], {
stdio: 'inherit',
});
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
throw new Error(`7z 解压失败,退出码 ${result.status}`);
}
// 将解压出的第一层目录内容移到 GIT_ROOT归档根可能是 PortableGit 或 mingw64 等)
const entries = fs.readdirSync(extractDir);
if (entries.length === 1) {
const single = path.join(extractDir, entries[0]);
if (fs.statSync(single).isDirectory()) {
const subEntries = fs.readdirSync(single);
for (const e of subEntries) {
const src = path.join(single, e);
const dest = path.join(GIT_ROOT, e);
if (fs.existsSync(dest)) {
fs.rmSync(dest, { recursive: true, force: true });
}
fs.renameSync(src, dest);
}
} else {
fs.renameSync(single, path.join(GIT_ROOT, entries[0]));
}
} else {
for (const entry of entries) {
const src = path.join(extractDir, entry);
const dest = path.join(GIT_ROOT, entry);
if (fs.existsSync(dest)) {
fs.rmSync(dest, { recursive: true, force: true });
}
fs.renameSync(src, dest);
}
}
fs.rmSync(extractDir, { recursive: true, force: true });
}
/**
* 解析归档来源:环境变量本地文件 → 缓存文件 → 下载
* @param {boolean} required - 若为 true 且无法得到归档则抛错
*/
async function resolveArchive(required) {
const envArchive = resolveInputPath(process.env.QIMING_PORTABLE_GIT_ARCHIVE);
if (envArchive) {
if (!isNonEmptyFile(envArchive)) {
throw new Error(
`QIMING_PORTABLE_GIT_ARCHIVE 指向无效文件: ${envArchive}`
);
}
console.log(`[prepare-git] 使用本地归档 QIMING_PORTABLE_GIT_ARCHIVE: ${envArchive}`);
return { archivePath: envArchive, source: 'env-archive' };
}
if (isNonEmptyFile(DEFAULT_ARCHIVE_PATH)) {
console.log(`[prepare-git] 使用缓存: ${DEFAULT_ARCHIVE_PATH}`);
return { archivePath: DEFAULT_ARCHIVE_PATH, source: 'cache' };
}
const urlFromEnv =
typeof process.env.QIMING_GIT_URL === 'string'
? process.env.QIMING_GIT_URL.trim()
: '';
const downloadUrl = urlFromEnv || DEFAULT_PORTABLE_GIT_URL;
try {
console.log(`[prepare-git] 下载 PortableGit: ${downloadUrl}`);
await downloadArchive(downloadUrl, DEFAULT_ARCHIVE_PATH);
const fileSizeMB = (fs.statSync(DEFAULT_ARCHIVE_PATH).size / 1024 / 1024).toFixed(1);
console.log(`[prepare-git] 已下载 (${fileSizeMB} MB): ${DEFAULT_ARCHIVE_PATH}`);
return { archivePath: DEFAULT_ARCHIVE_PATH, source: 'download' };
} catch (error) {
if (required) {
throw new Error(
'无法获取 PortableGit 归档。' +
'可设置 QIMING_PORTABLE_GIT_ARCHIVE 为本地离线包路径,或 QIMING_GIT_URL 为可访问镜像。' +
`原始错误: ${error instanceof Error ? error.message : String(error)}`
);
}
console.warn(
'[prepare-git] PortableGit 归档不可用,已跳过(未使用 --required。' +
`原因: ${error instanceof Error ? error.message : String(error)}`
);
return null;
}
}
/**
* 确保 PortableGit 已准备(与 LobsterAI ensurePortableGit 逻辑一致)
* @param {{ required?: boolean }} [options]
*/
async function ensurePortableGit(options = {}) {
const required = Boolean(options.required);
const isWindows = process.platform === 'win32';
const force = process.env.QIMING_SETUP_GIT_FORCE === '1';
const shouldRun = isWindows || required || force;
if (!shouldRun) {
console.log(
'[prepare-git] 非 Windows 主机跳过(可用 --required 或 QIMING_SETUP_GIT_FORCE=1 强制为 Windows 打包做准备)'
);
return { ok: true, skipped: true, bashPath: null };
}
const existingBash = findPortableGitBash();
if (existingBash) {
console.log(`[prepare-git] PortableGit 已就绪: ${existingBash}`);
return { ok: true, skipped: false, bashPath: existingBash };
}
const archive = await resolveArchive(required);
if (!archive) {
return { ok: true, skipped: true, bashPath: null };
}
extractArchive(archive.archivePath);
const resolvedBash = findPortableGitBash();
if (!resolvedBash) {
throw new Error(
'PortableGit 解压完成但未找到 bash.exe。' +
`已检查: ${path.join(GIT_ROOT, 'bin', 'bash.exe')} 与 usr/bin、mingw64 等。`
);
}
pruneUnneededFiles();
const finalSize = getDirSize(GIT_ROOT);
console.log(`[prepare-git] PortableGit 准备完成: ${resolvedBash}`);
console.log(`[prepare-git] 总大小: ~${(finalSize / 1024 / 1024).toFixed(1)} MB`);
return { ok: true, skipped: false, bashPath: resolvedBash };
}
async function main() {
const args = parseArgs(process.argv.slice(2));
await ensurePortableGit({ required: args.required });
}
if (require.main === module) {
main().catch((error) => {
console.error('[prepare-git] 错误:', error instanceof Error ? error.message : String(error));
process.exit(1);
});
}
module.exports = {
ensurePortableGit,
findPortableGitBash,
GIT_VERSION,
GIT_ROOT,
};

View File

@@ -0,0 +1,145 @@
#!/usr/bin/env node
/**
* 从 node_modules 复制 agent-gui-server 到 resources/
*
* 前提:
* 1. pnpm install 已执行workspace 链接生效)
* 2. agent-gui-server 已构建npm run build
*
* 产物:
* resources/agent-gui-server/
* ├── dist/index.js — esbuild CLI bundleESM原生依赖标记为 external
* ├── dist/lib.bundle.cjs — esbuild SDK bundleCJS供 Electron runtime require
* ├── node_modules/ — external 原生依赖sharp, @nut-tree-fork/*, clipboardy
* └── package.json — 精简版name/version/main/bin
*
* 打包时 electron-builder extraResources 会打包到
* .app/Contents/Resources/agent-gui-server/
*/
const path = require('path');
const fs = require('fs');
const { execSync } = require('child_process');
const { getProjectRoot } = require('../utils/project-paths');
const projectRoot = getProjectRoot();
const srcDir = path.join(projectRoot, 'node_modules', 'agent-gui-server');
const destDir = path.join(projectRoot, 'resources', 'agent-gui-server');
// esbuild 中标记为 external 的原生依赖(需要在运行时可用)
const NATIVE_EXTERNALS = ['sharp', '@nut-tree-fork/nut-js', 'clipboardy'];
function main() {
// 1. 验证 node_modules 中存在 agent-gui-server
if (!fs.existsSync(path.join(srcDir, 'package.json'))) {
console.error('[prepare-gui-server] node_modules 中未找到 agent-gui-server');
console.error('[prepare-gui-server] 请先执行 pnpm install');
process.exit(1);
}
const srcPkg = JSON.parse(fs.readFileSync(path.join(srcDir, 'package.json'), 'utf8'));
console.log(`[prepare-gui-server] 源码版本: ${srcPkg.name}@${srcPkg.version}`);
// 2. 验证必要文件存在
const srcIndexJs = path.join(srcDir, 'dist', 'index.js');
if (!fs.existsSync(srcIndexJs)) {
console.error(`[prepare-gui-server] CLI 入口不存在: ${srcIndexJs}`);
console.error('[prepare-gui-server] 请先在 crates/agent-gui-server 中执行 npm run build');
process.exit(1);
}
// 3. 检查目标是否已是最新版本
const destPkgPath = path.join(destDir, 'package.json');
const destLibBundle = path.join(destDir, 'dist', 'lib.bundle.cjs');
if (fs.existsSync(destPkgPath) && fs.existsSync(destLibBundle)) {
try {
const destPkg = JSON.parse(fs.readFileSync(destPkgPath, 'utf8'));
if (destPkg.version === srcPkg.version) {
const destIndexJs = path.join(destDir, 'dist', 'index.js');
if (fs.existsSync(destIndexJs)) {
const srcSize = fs.statSync(srcIndexJs).size;
const destSize = fs.statSync(destIndexJs).size;
if (srcSize === destSize) {
console.log(`[prepare-gui-server] ${srcPkg.version} 已是最新,跳过`);
return;
}
}
}
} catch {
// 目标损坏,重新复制
}
}
// 4. 清理并创建目标目录
console.log('[prepare-gui-server] 复制到 resources/agent-gui-server/...');
if (fs.existsSync(destDir)) {
fs.rmSync(destDir, { recursive: true });
}
fs.mkdirSync(path.join(destDir, 'dist'), { recursive: true });
// 5. 复制 dist/index.js (esbuild CLI bundle)
const destIndexJs = path.join(destDir, 'dist', 'index.js');
fs.copyFileSync(srcIndexJs, destIndexJs);
fs.chmodSync(destIndexJs, 0o755);
console.log(` dist/index.js (${(fs.statSync(destIndexJs).size / 1024).toFixed(0)} KB)`);
// 5b. 复制 dist/lib.bundle.cjs (esbuild CJS SDK bundle, for Electron runtime require)
const srcLibBundle = path.join(srcDir, 'dist', 'lib.bundle.cjs');
if (fs.existsSync(srcLibBundle)) {
fs.copyFileSync(srcLibBundle, destLibBundle);
console.log(` dist/lib.bundle.cjs (${(fs.statSync(destLibBundle).size / 1024).toFixed(0)} KB)`);
} else {
console.warn('[prepare-gui-server] dist/lib.bundle.cjs not found — CJS SDK bundle will be unavailable');
}
// 6. 安装 external 原生依赖到 resources/agent-gui-server/node_modules/
// 使用 npm install 确保原生模块为当前平台编译
const depsToInstall = [];
for (const dep of NATIVE_EXTERNALS) {
const ver = srcPkg.dependencies?.[dep];
if (ver) {
depsToInstall.push(`${dep}@${ver.replace(/^[\^~]/, '')}`);
}
}
if (depsToInstall.length > 0) {
// 先写一个临时 package.json 让 npm install 工作
const tmpPkg = { name: 'agent-gui-server-runtime', version: '0.0.0', private: true };
fs.writeFileSync(destPkgPath, JSON.stringify(tmpPkg, null, 2) + '\n');
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
// CI 交叉编译支持TARGET_ARCH 环境变量指定目标架构(如 macOS arm64 runner 构建 x64 包)
// npm install 通过 --cpu 标志安装目标架构的原生模块
const targetArch = process.env.TARGET_ARCH;
const archFlag = targetArch ? ` --cpu=${targetArch}` : '';
const archLabel = targetArch ? ` (target: ${targetArch})` : '';
console.log(` 安装原生依赖${archLabel}: ${depsToInstall.join(', ')}...`);
try {
execSync(`${npmCmd} install --no-save${archFlag} ${depsToInstall.join(' ')}`, {
cwd: destDir,
stdio: 'pipe',
});
console.log(' node_modules/ (原生依赖安装完成)');
} catch (e) {
console.error('[prepare-gui-server] 原生依赖安装失败:', e.stderr?.toString() || e.message);
process.exit(1);
}
}
// 7. 生成最终精简版 package.json
const slimPkg = {
name: srcPkg.name,
version: srcPkg.version,
type: 'module',
main: './dist/lib.bundle.cjs',
bin: { 'agent-gui-server': './dist/index.js' },
};
fs.writeFileSync(destPkgPath, JSON.stringify(slimPkg, null, 2) + '\n');
console.log(' package.json (slim)');
console.log(`[prepare-gui-server] ✓ resources/agent-gui-server/ (${srcPkg.version})`);
}
main();

View File

@@ -0,0 +1,127 @@
#!/usr/bin/env node
/**
* 多平台 qiming-lanproxy 集成:构建前按当前平台准备 resources/lanproxy/bin/
*
* 源resources/lanproxy/binaries/ 下的多平台二进制
* 文件名格式qiming-lanproxy-<rust-target>[.exe]
*
* 目标resources/lanproxy/bin/qiming-lanproxy (或 .exe)
*
* 打包时 electron-builder 的 extraResources 会打包
* resources/lanproxy → .app/Contents/Resources/lanproxy
* 运行时 getLanproxyBinPath() 优先用 lanproxy/binaries/<平台名>,其次 bin/qiming-lanproxy[.exe]
*
* 平台映射 (Node → Rust target)
* darwin-arm64 → aarch64-apple-darwin
* darwin-x64 → x86_64-apple-darwin
* win32-x64 → x86_64-pc-windows-msvc
* linux-x64 → x86_64-unknown-linux-gnu
* linux-arm64 → aarch64-unknown-linux-gnu
*/
const path = require('path');
const fs = require('fs');
const { getProjectRoot } = require('../utils/project-paths');
const projectRoot = getProjectRoot();
const srcBinDir = path.join(projectRoot, 'resources', 'lanproxy', 'binaries');
const destBinDir = path.join(projectRoot, 'resources', 'lanproxy', 'bin');
// Node platform-arch → Rust target triple
const PLATFORM_MAP = {
'darwin-arm64': 'aarch64-apple-darwin',
'darwin-x64': 'x86_64-apple-darwin',
'win32-x64': 'x86_64-pc-windows-msvc',
'win32-ia32': 'i686-pc-windows-msvc',
'linux-x64': 'x86_64-unknown-linux-gnu',
'linux-arm64': 'aarch64-unknown-linux-gnu',
};
// Fallback for macOS arm64 (universal binary)
function getFallbackPath(key) {
if (key === 'darwin-arm64') {
return 'qiming-lanproxy-universal-apple-darwin';
}
return null;
}
function getPlatformKey() {
const a = process.env.TARGET_ARCH || process.arch;
return `${process.platform}-${a}`;
}
function main() {
const key = getPlatformKey();
const target = PLATFORM_MAP[key];
if (!target) {
console.error(`[prepare-lanproxy] 不支持的平台: ${key}`);
console.error(`[prepare-lanproxy] 支持的平台: ${Object.keys(PLATFORM_MAP).join(', ')}`);
process.exit(1);
}
const isWin = process.platform === 'win32';
const srcName = `qiming-lanproxy-${target}${isWin ? '.exe' : ''}`;
const destName = `qiming-lanproxy${isWin ? '.exe' : ''}`;
let srcPath = path.join(srcBinDir, srcName);
const destPath = path.join(destBinDir, destName);
// macOS arm64: 尝试 universal binary 作为 fallback
if (!fs.existsSync(srcPath)) {
const fallbackName = getFallbackPath(key);
if (fallbackName) {
const fallbackPath = path.join(srcBinDir, fallbackName);
if (fs.existsSync(fallbackPath)) {
console.log(`[prepare-lanproxy] ${key}${fallbackName} (fallback)`);
srcPath = fallbackPath;
}
}
}
// 如果源文件仍不存在,创建空目录并继续
if (!fs.existsSync(srcPath)) {
console.warn(`[prepare-lanproxy] 源文件不存在: ${srcPath},将跳过 lanproxy 二进制(安装包可正常产出,运行时内网穿透不可用)`);
fs.mkdirSync(destBinDir, { recursive: true });
return;
}
// 如果目标已存在且大小一致,检查架构匹配
const platformKeyFile = path.join(destBinDir, '.platform-key');
if (fs.existsSync(destPath)) {
if (fs.existsSync(platformKeyFile)) {
const existingKey = fs.readFileSync(platformKeyFile, 'utf-8').trim();
if (existingKey !== key) {
console.log(`[prepare-lanproxy] 架构不匹配: 已有 ${existingKey}, 需要 ${key}, 清理并重新复制`);
fs.rmSync(destBinDir, { recursive: true, force: true });
} else {
const srcStat = fs.statSync(srcPath);
const destStat = fs.statSync(destPath);
if (srcStat.size === destStat.size) {
console.log(`[prepare-lanproxy] ${key}${destName} (已是最新,跳过)`);
return;
}
}
} else {
// No .platform-key — legacy check by size only
const srcStat = fs.statSync(srcPath);
const destStat = fs.statSync(destPath);
if (srcStat.size === destStat.size) {
console.log(`[prepare-lanproxy] ${key}${destName} (已是最新,跳过)`);
return;
}
}
}
// 复制
const srcBasename = path.basename(srcPath);
console.log(`[prepare-lanproxy] ${key}${srcBasename}`);
fs.mkdirSync(destBinDir, { recursive: true });
fs.copyFileSync(srcPath, destPath);
fs.chmodSync(destPath, 0o755);
fs.writeFileSync(platformKeyFile, key, 'utf-8');
console.log(`[prepare-lanproxy] ✓ ${destPath} (${(fs.statSync(destPath).size / 1024 / 1024).toFixed(1)} MB)`);
console.log(`[prepare-lanproxy] 已写入 .platform-key: ${key}`);
}
main();

View File

@@ -0,0 +1,139 @@
#!/usr/bin/env node
/**
* 从 node_modules 复制 qiming-mcp-stdio-proxy 到 resources/
*
* 前提:
* 1. pnpm install 已执行workspace 链接生效)
* 2. qiming-mcp-stdio-proxy 已构建npm run build
*
* 产物3 个文件):
* resources/qiming-mcp-stdio-proxy/
* ├── dist/index.js — CLI bundleesbuild 单文件,含 shebang
* ├── dist/lib.bundle.js — 库 bundlePersistentMcpBridge 等导出)
* └── package.json — 精简版name/version/bin/main
*
* 打包时 electron-builder extraResources 会打包到
* .app/Contents/Resources/qiming-mcp-stdio-proxy/
*/
const path = require('path');
const fs = require('fs');
const { getProjectRoot } = require('../utils/project-paths');
const projectRoot = getProjectRoot();
const srcDir = path.join(projectRoot, 'node_modules', 'qiming-mcp-stdio-proxy');
const destDir = path.join(projectRoot, 'resources', 'qiming-mcp-stdio-proxy');
function main() {
// 1. 验证 node_modules 中存在 qiming-mcp-stdio-proxy
if (!fs.existsSync(path.join(srcDir, 'package.json'))) {
console.error(`[prepare-mcp-proxy] node_modules 中未找到 qiming-mcp-stdio-proxy`);
console.error('[prepare-mcp-proxy] 请先执行 pnpm install');
process.exit(1);
}
const srcPkg = JSON.parse(fs.readFileSync(path.join(srcDir, 'package.json'), 'utf8'));
console.log(`[prepare-mcp-proxy] 源码版本: ${srcPkg.name}@${srcPkg.version}`);
// 2. 验证必要文件存在
const srcIndexJs = path.join(srcDir, 'dist', 'index.js');
const srcLibJs = path.join(srcDir, 'dist', 'lib.js');
if (!fs.existsSync(srcIndexJs)) {
console.error(`[prepare-mcp-proxy] CLI 入口不存在: ${srcIndexJs}`);
console.error('[prepare-mcp-proxy] 请先在 crates/qiming-mcp-stdio-proxy 中执行 npm run build');
process.exit(1);
}
if (!fs.existsSync(srcLibJs)) {
console.error(`[prepare-mcp-proxy] 库入口不存在: ${srcLibJs}`);
console.error('[prepare-mcp-proxy] 请先在 crates/qiming-mcp-stdio-proxy 中执行 npm run build');
process.exit(1);
}
// 3. 检查目标是否已是最新版本
const destPkgPath = path.join(destDir, 'package.json');
if (fs.existsSync(destPkgPath)) {
try {
const destPkg = JSON.parse(fs.readFileSync(destPkgPath, 'utf8'));
if (destPkg.version === srcPkg.version) {
const destIndexJs = path.join(destDir, 'dist', 'index.js');
const destLibJs = path.join(destDir, 'dist', 'lib.bundle.js');
if (fs.existsSync(destIndexJs) && fs.existsSync(destLibJs)) {
console.log(`[prepare-mcp-proxy] ${srcPkg.version} 已是最新,跳过`);
return;
}
}
} catch {
// 目标损坏,重新复制
}
}
// 4. 构建 lib bundle使用 esbuild
console.log('[prepare-mcp-proxy] 构建 lib bundle (esbuild)...');
const esbuildBin = path.join(srcDir, 'node_modules', '.bin', 'esbuild');
// 如果 proxy 包中没有 esbuild使用项目根目录的 esbuild
const esbuildToUse = fs.existsSync(esbuildBin)
? esbuildBin
: path.join(projectRoot, 'node_modules', '.bin', 'esbuild');
// Windows 兼容
const esbuildCmd = process.platform === 'win32' && !esbuildToUse.endsWith('.cmd')
? esbuildToUse + '.cmd'
: esbuildToUse;
const libEntry = srcLibJs;
const libBundlePath = path.join(srcDir, 'dist', 'lib.bundle.js');
const { execSync } = require('child_process');
try {
execSync(
`"${esbuildCmd}" "${libEntry}" --bundle --platform=node --target=node22 --format=cjs --outfile="${libBundlePath}" --legal-comments=none`,
{ cwd: srcDir, stdio: 'inherit' },
);
} catch (e) {
console.error('[prepare-mcp-proxy] esbuild 打包失败,尝试直接复制 lib.js');
// 如果 esbuild 失败,直接使用 lib.js 作为 lib.bundle.js
if (fs.existsSync(libEntry)) {
fs.copyFileSync(libEntry, libBundlePath);
} else {
console.error('[prepare-mcp-proxy] lib.js 也不存在,无法继续');
process.exit(1);
}
}
// 5. 复制到 resources/qiming-mcp-stdio-proxy/
console.log('[prepare-mcp-proxy] 复制到 resources/qiming-mcp-stdio-proxy/...');
// 清理目标目录
if (fs.existsSync(destDir)) {
fs.rmSync(destDir, { recursive: true });
}
fs.mkdirSync(path.join(destDir, 'dist'), { recursive: true });
// 复制 dist/index.js (CLI bundle)
const destIndexJs = path.join(destDir, 'dist', 'index.js');
fs.copyFileSync(srcIndexJs, destIndexJs);
fs.chmodSync(destIndexJs, 0o755);
console.log(` dist/index.js (${(fs.statSync(destIndexJs).size / 1024).toFixed(0)} KB)`);
// 复制 dist/lib.bundle.js (library bundle)
const destLibJs = path.join(destDir, 'dist', 'lib.bundle.js');
fs.copyFileSync(libBundlePath, destLibJs);
console.log(` dist/lib.bundle.js (${(fs.statSync(destLibJs).size / 1024).toFixed(0)} KB)`);
// 6. 生成精简版 package.json
const slimPkg = {
name: srcPkg.name,
version: srcPkg.version,
bin: { 'qiming-mcp-stdio-proxy': './dist/index.js' },
main: './dist/lib.bundle.js',
};
fs.writeFileSync(destPkgPath, JSON.stringify(slimPkg, null, 2) + '\n');
console.log(' package.json (slim)');
console.log(`[prepare-mcp-proxy] ✓ resources/qiming-mcp-stdio-proxy/ (${srcPkg.version})`);
}
main();

View File

@@ -0,0 +1,64 @@
#!/usr/bin/env node
/**
* 将 monaco-editor 的静态资源min/vs复制到 public/ 下,确保 Electron 打包后可离线加载。
*
* 目标产物:
* public/monaco/vs/**
*
* 运行时:
* 渲染进程通过 @monaco-editor/react 的 loader.config({ paths: { vs: "./monaco/vs" } })
* 指向该目录。
*/
/* eslint-disable no-console */
'use strict';
const path = require('path');
const fs = require('fs');
const { getProjectRoot } = require('../utils/project-paths');
const projectRoot = getProjectRoot();
const srcVsDir = path.join(projectRoot, 'node_modules', 'monaco-editor', 'min', 'vs');
const destVsDir = path.join(projectRoot, 'public', 'monaco', 'vs');
function copyDirRecursive(src, dest) {
if (!fs.existsSync(src)) return;
fs.mkdirSync(dest, { recursive: true });
for (const name of fs.readdirSync(src)) {
const s = path.join(src, name);
const d = path.join(dest, name);
const stat = fs.statSync(s);
if (stat.isDirectory()) {
copyDirRecursive(s, d);
} else {
fs.copyFileSync(s, d);
}
}
}
function main() {
if (!fs.existsSync(srcVsDir)) {
console.error('[prepare-monaco] 未找到 monaco-editor 资源目录:');
console.error(` ${srcVsDir}`);
console.error('[prepare-monaco] 请先执行 npm install');
process.exit(1);
}
// 清理旧产物,避免残留导致加载异常
if (fs.existsSync(destVsDir)) {
fs.rmSync(destVsDir, { recursive: true, force: true });
}
console.log('[prepare-monaco] 复制 monaco 静态资源...');
copyDirRecursive(srcVsDir, destVsDir);
// 基本校验loader 需要 loader.js
const loaderJs = path.join(destVsDir, 'loader.js');
if (!fs.existsSync(loaderJs)) {
console.warn('[prepare-monaco] ⚠️ 缺少 vs/loader.jsmonaco 可能无法加载');
}
console.log('[prepare-monaco] ✓ public/monaco/vs 已准备完成');
}
main();

View File

@@ -0,0 +1,343 @@
#!/usr/bin/env node
/**
* Node.js 24 集成(所有平台)
*
* 为所有平台准备 resources/node/<platform-arch>/,供运行时使用。
*
* 参考 LobsterAI 方案https://github.com/netease-youdao/LobsterAI
*
* 1) 若已存在 resources/node/<platform-arch>/,则跳过
* 2) 否则从 nodejs.org 下载对应平台的包,解压到 <platform-arch>/
* 3) 可选:验证下载文件的 SHA256 校验和
*
* 平台 keydarwin-x64, darwin-arm64, win32-x64, win32-arm64, linux-x64, linux-arm64
*/
const path = require('path');
const fs = require('fs');
const https = require('https');
const crypto = require('crypto');
const { execSync } = require('child_process');
const { getProjectRoot } = require('../utils/project-paths');
const projectRoot = getProjectRoot();
const nodeRoot = path.join(projectRoot, 'resources', 'node');
// Node.js 版本
const NODE_VERSION = '24.14.0';
// 是否验证 SHA256可通过环境变量禁用SKIP_SHA256=1 npm run prepare:node
const SKIP_SHA256 = process.env.SKIP_SHA256 === '1';
// 平台 key
function getPlatformKey() {
const p = process.platform;
const a = process.env.TARGET_ARCH || process.arch;
return `${p}-${a}`;
}
// Node.js 官方 release 资源文件名
const NODE_ASSET_SUFFIX = {
'darwin-arm64': 'node-v${VERSION}-darwin-arm64.tar.xz',
'darwin-x64': 'node-v${VERSION}-darwin-x64.tar.xz',
'win32-x64': 'node-v${VERSION}-win-x64.zip',
'win32-arm64': 'node-v${VERSION}-win-arm64.zip',
'linux-x64': 'node-v${VERSION}-linux-x64.tar.xz',
'linux-arm64': 'node-v${VERSION}-linux-arm64.tar.xz',
};
/**
* 下载文件
*/
function download(url, filename) {
return new Promise((resolve, reject) => {
const file = path.join(nodeRoot, '.cache', filename);
if (fs.existsSync(file)) {
console.log(`[prepare-node] 使用缓存: ${filename}`);
resolve(file);
return;
}
fs.mkdirSync(path.dirname(file), { recursive: true });
const stream = fs.createWriteStream(file);
https.get(url, (res) => {
if (res.statusCode === 302 || res.statusCode === 301) {
const loc = res.headers.location;
const fullUrl = loc.startsWith('http') ? loc : new URL(loc, url).href;
stream.close();
fs.unlink(file, () => {});
return download(fullUrl, filename).then(resolve).catch(reject);
}
if (res.statusCode !== 200) {
stream.close();
fs.unlink(file, () => {});
return reject(new Error(`HTTP ${res.statusCode}`));
}
res.pipe(stream);
stream.on('finish', () => { stream.close(); resolve(file); });
stream.on('error', reject);
}).on('error', reject);
});
}
/**
* 计算文件的 SHA256 哈希值
*/
function computeSha256(filePath) {
return new Promise((resolve, reject) => {
const hash = crypto.createHash('sha256');
const stream = fs.createReadStream(filePath);
stream.on('data', (chunk) => hash.update(chunk));
stream.on('end', () => resolve(hash.digest('hex')));
stream.on('error', reject);
});
}
/**
* 获取 Node.js 官方 SHASUMS256.txt 内容
*/
async function fetchShasums(urlOverride) {
const url = urlOverride || `https://nodejs.org/dist/v${NODE_VERSION}/SHASUMS256.txt`;
return new Promise((resolve, reject) => {
https.get(url, (res) => {
if (res.statusCode === 302 || res.statusCode === 301) {
const loc = res.headers.location;
const fullUrl = loc.startsWith('http') ? loc : new URL(loc, url).href;
return fetchShasums(fullUrl).then(resolve).catch(reject);
}
if (res.statusCode !== 200) {
return reject(new Error(`HTTP ${res.statusCode}`));
}
let data = '';
res.on('data', (chunk) => data += chunk);
res.on('end', () => resolve(data));
res.on('error', reject);
}).on('error', reject);
});
}
/**
* 验证下载文件的 SHA256
*/
async function verifySha256(filePath, filename) {
if (SKIP_SHA256) {
console.log('[prepare-node] SHA256 校验已跳过 (SKIP_SHA256=1)');
return true;
}
try {
console.log('[prepare-node] 验证 SHA256 校验和...');
const shasums = await fetchShasums();
const expectedHash = shasums
.split('\n')
.find(line => line.endsWith(filename))
?.split(/\s+/)[0];
if (!expectedHash) {
console.warn('[prepare-node] ⚠️ 未找到对应的 SHA256 校验和,跳过验证');
return true;
}
const actualHash = await computeSha256(filePath);
if (actualHash !== expectedHash) {
console.error(`[prepare-node] ❌ SHA256 校验失败!`);
console.error(`[prepare-node] 期望: ${expectedHash}`);
console.error(`[prepare-node] 实际: ${actualHash}`);
return false;
}
console.log('[prepare-node] ✅ SHA256 校验通过');
return true;
} catch (err) {
console.warn('[prepare-node] ⚠️ SHA256 校验失败(网络问题?:', err.message);
// 网络问题时允许继续
return true;
}
}
/**
* 解压
*/
function extractArchive(archivePath, outDir) {
fs.mkdirSync(outDir, { recursive: true });
const ext = path.extname(archivePath).toLowerCase();
if (ext === '.zip') {
if (process.platform === 'win32') {
const psPath = archivePath.replace(/'/g, "''");
const psDest = outDir.replace(/'/g, "''");
execSync(
`powershell -NoProfile -Command "Expand-Archive -LiteralPath '${psPath}' -DestinationPath '${psDest}' -Force"`,
{ stdio: 'inherit' }
);
} else {
execSync(`unzip -o -q "${archivePath}" -d "${outDir}"`, { stdio: 'inherit' });
}
} else {
execSync(`tar -xJf "${archivePath}" -C "${outDir}"`, { stdio: 'inherit' });
}
}
/**
* 复制目录,保留符号链接
*/
function copyDirRecursiveWithSymlinks(src, dest) {
if (!fs.existsSync(src)) return;
fs.mkdirSync(dest, { recursive: true });
for (const name of fs.readdirSync(src)) {
const s = path.join(src, name);
const d = path.join(dest, name);
const stat = fs.lstatSync(s);
if (stat.isSymbolicLink()) {
const linkTarget = fs.readlinkSync(s);
fs.symlinkSync(linkTarget, d);
} else if (stat.isDirectory()) {
copyDirRecursiveWithSymlinks(s, d);
} else {
fs.copyFileSync(s, d);
}
}
}
async function prepareNode(key, suffix) {
const cacheDir = path.join(nodeRoot, '.cache');
const platformDir = path.join(nodeRoot, key);
// 检查是否已存在
const platformKeyFile = path.join(platformDir, '.platform-key');
if (fs.existsSync(platformDir)) {
if (fs.existsSync(platformKeyFile)) {
const existingKey = fs.readFileSync(platformKeyFile, 'utf-8').trim();
if (existingKey === key) {
console.log(`[prepare-node] Node.js ${NODE_VERSION} (${key}) 已存在且架构匹配,跳过`);
return;
}
console.log(`[prepare-node] 架构不匹配: 已有 ${existingKey}, 需要 ${key}, 清理并重新下载`);
fs.rmSync(platformDir, { recursive: true, force: true });
} else {
// No .platform-key — legacy, treat as matching
console.log(`[prepare-node] Node.js ${NODE_VERSION} (${key}) 已存在,跳过`);
return;
}
}
const filename = suffix.replace('${VERSION}', NODE_VERSION);
const downloadUrl = `https://nodejs.org/dist/v${NODE_VERSION}/${filename}`;
console.log(`[prepare-node] 下载 Node.js ${NODE_VERSION} (${filename})...`);
try {
const archivePath = await download(downloadUrl, filename);
// 验证 SHA256
const sha256Valid = await verifySha256(archivePath, filename);
if (!sha256Valid) {
fs.unlinkSync(archivePath);
console.error('[prepare-node] 下载文件已删除,请重试');
process.exit(1);
}
const extractDir = path.join(cacheDir, `node-extract-${key}`);
if (fs.existsSync(extractDir)) {
fs.rmSync(extractDir, { recursive: true, force: true });
}
console.log(`[prepare-node] 解压...`);
extractArchive(archivePath, extractDir);
// 移动到目标目录(保留符号链接)
const entries = fs.readdirSync(extractDir);
if (entries.length === 1) {
const inner = path.join(extractDir, entries[0]);
if (fs.statSync(inner).isDirectory()) {
copyDirRecursiveWithSymlinks(inner, platformDir);
}
}
// 清理不需要的文件以减小包体积
// 1. 删除 include 目录C++ 头文件,运行时不需要)
const includeDir = path.join(platformDir, 'include');
if (fs.existsSync(includeDir)) {
fs.rmSync(includeDir, { recursive: true, force: true });
console.log(`[prepare-node] 已删除 include/ 目录(运行时不需要)`);
}
// 2. 删除 CHANGELOG.md, LICENSE, README.md可选减小体积
['CHANGELOG.md', 'LICENSE', 'README.md'].forEach(file => {
const filePath = path.join(platformDir, file);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
});
// Windows: 统一目录结构,将 node.exe 移动到 bin/ 目录
// 这样所有平台都使用 resources/node/<platform>/bin/node 结构
if (key.startsWith('win32-')) {
const nodeExe = path.join(platformDir, 'node.exe');
const binDir = path.join(platformDir, 'bin');
if (fs.existsSync(nodeExe) && !fs.existsSync(binDir)) {
fs.mkdirSync(binDir, { recursive: true });
fs.renameSync(nodeExe, path.join(binDir, 'node.exe'));
// 移动相关文件到 bin/
['npm.cmd', 'npx.cmd', 'corepack.cmd', 'npm', 'npx', 'corepack'].forEach(cmd => {
const src = path.join(platformDir, cmd);
if (fs.existsSync(src)) {
fs.renameSync(src, path.join(binDir, cmd));
}
});
// 移动 node_modules 到 bin/ 目录npm.cmd 期望 node_modules 在同一目录)
const nodeModulesSrc = path.join(platformDir, 'node_modules');
const nodeModulesDest = path.join(binDir, 'node_modules');
if (fs.existsSync(nodeModulesSrc) && !fs.existsSync(nodeModulesDest)) {
fs.renameSync(nodeModulesSrc, nodeModulesDest);
}
console.log(`[prepare-node] 已将 Windows Node.js 重组为 bin/ 结构`);
}
}
// macOS: 对 Node.js 二进制进行 ad-hoc 签名,使其能继承主应用的辅助功能权限
// 避免用户需要单独授权 node 子进程
if (key.startsWith('darwin-')) {
const nodeBin = path.join(platformDir, 'bin', 'node');
if (fs.existsSync(nodeBin)) {
try {
execSync(`codesign --force --sign - "${nodeBin}"`, { stdio: 'inherit' });
console.log(`[prepare-node] 已对 Node.js 二进制进行 ad-hoc 签名`);
} catch (signErr) {
console.warn(`[prepare-node] ⚠️ 签名失败(不影响功能):`, signErr.message);
}
}
}
console.log(`[prepare-node] Node.js ${NODE_VERSION} (${key}) 准备完成!`);
// Write .platform-key marker
fs.writeFileSync(platformKeyFile, key, 'utf-8');
console.log(`[prepare-node] 已写入 .platform-key: ${key}`);
} catch (err) {
console.error(`[prepare-node] 下载或解压失败:`, err.message);
process.exit(1);
}
}
async function main() {
// 所有平台都需要内置 Node.js 24不依赖用户系统安装
const key = getPlatformKey();
const suffix = NODE_ASSET_SUFFIX[key];
if (!suffix) {
console.warn(`[prepare-node] 未支持的平台: ${key}`);
return;
}
// 创建目录
fs.mkdirSync(nodeRoot, { recursive: true });
await prepareNode(key, suffix);
}
main();

View File

@@ -0,0 +1,103 @@
#!/usr/bin/env node
/**
* 准备 qiming-file-server 源码并复制到 resources/
*
* 逻辑:
* 1. 若 sources/qiming-file-server 不存在 → git clone + npm install + npm run build
* 2. 若存在但无 node_modules → npm install + npm run build
* 3. 否则跳过构建(认为已就绪)
* 4. 复制到 resources/qiming-file-server/
*
* 产物:
* resources/qiming-file-server/
* ├── dist/
* ├── node_modules/
* └── package.json
*/
const path = require('path');
const fs = require('fs');
const { execSync } = require('child_process');
const { getProjectRoot } = require('../utils/project-paths');
const projectRoot = getProjectRoot();
const electronClientRoot = projectRoot;
// 从 package.json 读取源码地址
const pkgJson = JSON.parse(fs.readFileSync(path.join(electronClientRoot, 'package.json'), 'utf8'));
const { url: GIT_REPO, branch: GIT_BRANCH } = pkgJson.bundledSources['qiming-file-server'];
const SOURCE_DIR = path.join(electronClientRoot, 'sources', 'qiming-file-server');
const destDir = path.join(electronClientRoot, 'resources', 'qiming-file-server');
function exec(cmd, opts = {}) {
console.log(` $ ${cmd}`);
execSync(cmd, { stdio: 'inherit', ...opts });
}
function main() {
// 1. 克隆或更新源码
if (!fs.existsSync(path.join(SOURCE_DIR, '.git'))) {
console.log('[prepare-qiming-file-server] 克隆源码...');
exec(`git clone --branch ${GIT_BRANCH} ${GIT_REPO} "${SOURCE_DIR}"`);
} else {
console.log('[prepare-qiming-file-server] 更新源码...');
exec(`cd "${SOURCE_DIR}" && git checkout ${GIT_BRANCH} && git pull`);
}
// 2. 检查构建产物是否存在
const hasBuild = fs.existsSync(path.join(SOURCE_DIR, 'dist'));
const hasNodeModules = fs.existsSync(path.join(SOURCE_DIR, 'node_modules'));
if (!hasBuild || !hasNodeModules) {
// 清理旧的 node_modules若有
if (fs.existsSync(path.join(SOURCE_DIR, 'node_modules'))) {
console.log('[prepare-qiming-file-server] 清理旧的 node_modules...');
exec(`rm -rf "${path.join(SOURCE_DIR, 'node_modules')}"`);
}
// 3. 安装依赖
console.log('[prepare-qiming-file-server] 安装依赖...');
exec(`cd "${SOURCE_DIR}" && npm install --ignore-scripts`);
// 4. 构建
console.log('[prepare-qiming-file-server] 构建项目...');
exec(`cd "${SOURCE_DIR}" && npm run build`);
} else {
console.log('[prepare-qiming-file-server] 构建产物已就绪,跳过构建');
}
// 5. 读取版本
const srcPkg = JSON.parse(fs.readFileSync(path.join(SOURCE_DIR, 'package.json'), 'utf8'));
console.log(`[prepare-qiming-file-server] 源码版本: ${srcPkg.name}@${srcPkg.version}`);
// 6. 清理并创建目标目录
if (fs.existsSync(destDir)) {
fs.rmSync(destDir, { recursive: true });
}
fs.mkdirSync(destDir, { recursive: true });
// 7. 复制 dist/
console.log('[prepare-qiming-file-server] 复制 dist/...');
exec(`cp -R "${path.join(SOURCE_DIR, 'dist')}" "${destDir}/"`);
// 8. 复制 package.json
fs.copyFileSync(
path.join(SOURCE_DIR, 'package.json'),
path.join(destDir, 'package.json')
);
// 9. 复制 node_modules/
console.log('[prepare-qiming-file-server] 复制 node_modules/...');
exec(`cp -R "${path.join(SOURCE_DIR, 'node_modules')}" "${destDir}/"`);
// 10. 复制 LICENSE
const licenseSrc = path.join(SOURCE_DIR, 'LICENSE');
if (fs.existsSync(licenseSrc)) {
fs.copyFileSync(licenseSrc, path.join(destDir, 'LICENSE'));
}
console.log(`[prepare-qiming-file-server] ✓ resources/qiming-file-server/ (${srcPkg.version})`);
}
main();

View File

@@ -0,0 +1,622 @@
#!/usr/bin/env node
/**
* qimingcode 多平台集成:准备 resources/qimingcode/{platform}/bin/
*
* 两种模式:
* 1) 本地 dist 复制(设置 QIMINGCODE_DIST_DIR 环境变量,开发调试用)
* QIMINGCODE_DIST_DIR=~/workspace/qimingcode/packages/opencode/dist npm run prepare:qimingcode
* 2) GitHub Release 下载默认CI/正式构建用)
* npm run prepare:qimingcode
*
* 打包时 electron-builder extraResources 将 resources/qimingcode 打包到应用内
* 运行时 getQimingCodeBundledBinPath() 解析对应平台二进制
*
* 用法:
* node scripts/prepare/prepare-qimingcode.js # 当前平台
* node scripts/prepare/prepare-qimingcode.js --all # 全平台
*
* 环境变量:
* QIMINGCODE_DIST_DIR — qimingcode 本地构建产物目录(设置后走本地复制模式)
* QIMINGCODE_REPO — GitHub 仓库(默认 qiming-ai/qimingcode
* GITHUB_TOKEN — GitHub token私有仓库或提高速率限制用
*/
const path = require('path');
const fs = require('fs');
const https = require('https');
const { URL } = require('url');
const { execSync, execFileSync } = require('child_process');
const { getProjectRoot } = require('../utils/project-paths');
const QIMINGCODE_VERSION = '1.1.97';
const QIMINGCODE_REPO = process.env.QIMINGCODE_REPO || 'qiming-ai/qimingcode';
const projectRoot = getProjectRoot();
const resDir = path.join(projectRoot, 'resources', 'qimingcode');
const cacheDir = path.join(projectRoot, 'scripts', 'resources', 'qimingcode-cache');
// Node platform-arch → dist 文件夹名 / Release asset 名
const PLATFORM_MAP = {
'darwin-arm64': 'qimingcode-darwin-arm64',
'darwin-x64': 'qimingcode-darwin-x64',
'linux-arm64': 'qimingcode-linux-arm64',
'linux-arm64-musl': 'qimingcode-linux-arm64-musl',
'linux-x64': 'qimingcode-linux-x64',
'linux-x64-musl': 'qimingcode-linux-x64-musl',
'win32-x64': 'qimingcode-windows-x64',
};
// 资源目录名需与运行时 getQimingCodeBundledBinPath() 一致
const RESOURCE_PLATFORM_KEY_MAP = {
'win32-x64': 'windows-x64',
};
function getPlatformKey() {
const a = process.env.TARGET_ARCH || process.arch;
return `${process.platform}-${a}`;
}
function getResourcePlatformKey(key) {
return RESOURCE_PLATFORM_KEY_MAP[key] || key;
}
function isWindows(key) {
return key.startsWith('win32');
}
function getBinaryName(key) {
return isWindows(key) ? 'qimingcode.exe' : 'qimingcode';
}
/**
* 兼容 release 二进制命名差异:
* - 历史格式: qimingcode / qimingcode.exe
* - 新格式: opencode / opencode.exe
*
* 说明:
* 1) 运行时入口仍统一为 resources/.../bin/qimingcode(.exe)
* 2) 这里仅放宽“解压后查找源二进制”的候选名,不改变运行时对外约定
*/
function getBinaryCandidates(key) {
const preferred = getBinaryName(key);
const fallback = isWindows(key) ? 'opencode.exe' : 'opencode';
return [preferred, fallback];
}
/**
* 清理目标 bin 目录,避免旧版本资源文件残留。
*
* 背景:
* - 历史上这里是“增量覆盖复制”,当新包删除了某些文件(例如 assets/models.json
* 而旧包中仍存在时,旧文件会继续留在目标目录,造成“看似升级成功但实际混入旧资源”。
*
* 规则:
* - 每次准备二进制前先删后建,保证目标目录仅包含当前包内容。
* - 使用 force:true确保目录不存在时也不会抛错。
*/
function resetDestBinDir(destDir) {
fs.rmSync(destDir, { recursive: true, force: true });
fs.mkdirSync(destDir, { recursive: true });
}
/**
* 确保目标目录存在 assets/model.json。
*
* 背景:
* - 新版 release 有时只包含单二进制,不再附带 assets/model.json。
* - 业务侧仍有路径会读取该文件,因此这里统一兜底创建最小占位文件。
*
* 约束:
* - 若上游已提供 model.json则保持原样不覆盖。
* - 仅在缺失时创建,内容保持最小且可 JSON.parse。
*/
function ensureModelJson(destDir, version) {
const assetsDir = path.join(destDir, 'assets');
const modelJsonPath = path.join(assetsDir, 'model.json');
if (fs.existsSync(modelJsonPath)) return;
fs.mkdirSync(assetsDir, { recursive: true });
const fallback = {
models: [],
source: 'generated-fallback',
version,
};
fs.writeFileSync(modelJsonPath, `${JSON.stringify(fallback, null, 2)}\n`, 'utf-8');
}
/**
* 即便命中版本与 SHA也执行一次“目录重铺”。
*
* 原因:
* - 过去采用增量覆盖复制,可能在目标目录留下旧版本 assets 残留。
* - 仅靠二进制 SHA 命中无法发现“额外残留文件”。
*
* 处理:
* - 命中后不提前 return而是继续走解压/复制流程(优先使用本地缓存包),
* 通过 resetDestBinDir() 达到“先清理后落盘”的确定性结果。
*/
const FORCE_REFRESH_ON_MATCH = true;
// ==================== 模式 1: 本地 dist 复制 ====================
function copyFromDist(key) {
const qimingcodeDist = process.env.QIMINGCODE_DIST_DIR || path.join(
process.env.HOME || '/root',
'workspace/qimingcode/packages/opencode/dist',
);
const distName = PLATFORM_MAP[key];
if (!distName) {
console.error(`[prepare-qimingcode] 不支持的平台: ${key}`);
return false;
}
const resourceKey = getResourcePlatformKey(key);
const binary = getBinaryName(key);
const srcPath = path.join(qimingcodeDist, distName, 'bin', binary);
const destDir = path.join(resDir, resourceKey, 'bin');
const destPath = path.join(destDir, binary);
if (!fs.existsSync(srcPath)) {
console.warn(`[prepare-qimingcode] ${key}: 构建产物不存在 ${srcPath}`);
return false;
}
// 检查是否已是最新SHA256 一致 + 版本匹配)
// 注意codesign 会修改二进制,所以用保存的 .sha256 记录比对 dest签名后
// 而非比对 src未签名vs dest已签名
if (fs.existsSync(destPath)) {
const versionFile = path.join(resDir, '.version');
if (fs.existsSync(versionFile) && fs.readFileSync(versionFile, 'utf-8').trim() === QIMINGCODE_VERSION) {
const shaFile = path.join(resDir, `.sha256-${resourceKey}`);
if (fs.existsSync(shaFile)) {
const expectedHash = fs.readFileSync(shaFile, 'utf-8').trim();
const currentHash = sha256File(destPath);
if (currentHash === expectedHash) {
// 额外校验:二进制内部版本号可能与 .version 不一致(曾发生过标记更新但二进制未更新)
const innerVersion = verifyBinaryVersion(destPath, QIMINGCODE_VERSION, key, currentHash);
if (innerVersion && innerVersion !== QIMINGCODE_VERSION) {
console.warn(
`[prepare-qimingcode] ${key}: 版本标记为 ${QIMINGCODE_VERSION} 但二进制为 ${innerVersion},将重新复制覆盖`,
);
} else {
console.log(
`[prepare-qimingcode] ${key} ✓ (已是最新, SHA256=${currentHash.slice(0, 16)}...)`
+ (FORCE_REFRESH_ON_MATCH ? ',将执行目录重铺以清理残留文件' : ''),
);
if (!FORCE_REFRESH_ON_MATCH) return true;
}
}
console.warn(`[prepare-qimingcode] ${key}: SHA256 不匹配,需重新复制 (saved=${expectedHash.slice(0, 16)}... current=${currentHash.slice(0, 16)}...)`);
}
}
}
// 复制前先清理目标目录,确保不会夹带旧版本 assets 残留文件。
resetDestBinDir(destDir);
// 复制整个 bin 目录(包含二进制 + assets 等)
const srcBinDir = path.join(qimingcodeDist, distName, 'bin');
fs.cpSync(srcBinDir, destDir, { recursive: true });
ensureModelJson(destDir, QIMINGCODE_VERSION);
fs.chmodSync(destPath, 0o755);
const sizeMB = (fs.statSync(destPath).size / 1024 / 1024).toFixed(1);
console.log(`[prepare-qimingcode] ${key} ✓ 从本地 dist 复制 (${sizeMB} MB)`);
// macOS ad-hoc 签名
codesign(destPath, key);
// 计算 SHA256签名后用于打印 + 保存
const hash = sha256File(destPath);
// 验证二进制内部版本号 + 打印 SHA256
verifyBinaryVersion(destPath, QIMINGCODE_VERSION, key, hash);
// 保存 SHA256 记录,下次可精确跳过
fs.writeFileSync(path.join(resDir, `.sha256-${resourceKey}`), hash, 'utf-8');
return true;
}
// ==================== 模式 2: GitHub Release 下载 ====================
/**
* 下载文件到缓存目录
* @param {{ force?: boolean }} [options] force=true 时忽略已有缓存(解压失败/缓存截断时用)
*/
function download(url, preferredFilename, options = {}) {
const force = !!options.force;
return new Promise((resolve, reject) => {
const filename = preferredFilename || path.basename(url.split('?')[0]) || 'download';
const file = path.join(cacheDir, filename);
// 缓存检查(仅看大小,不校验 gzip损坏时需 force 重下)
if (!force && fs.existsSync(file)) {
try {
const stats = fs.statSync(file);
if (stats.size > 100 * 1024) {
console.log(`[prepare-qimingcode] 使用缓存: ${filename} (${Math.round(stats.size / 1024 / 1024)} MB)`);
resolve(file);
return;
}
} catch (_) {}
try { fs.unlinkSync(file); } catch (_) {}
}
if (force && fs.existsSync(file)) {
try { fs.unlinkSync(file); } catch (_) {}
}
const headers = { 'User-Agent': 'QimingClaw-Build' };
if (process.env.GITHUB_TOKEN) {
headers['Authorization'] = `Bearer ${process.env.GITHUB_TOKEN}`;
}
fs.mkdirSync(cacheDir, { recursive: true });
const doRequest = (reqUrl, redirects) => {
if (redirects > 10) return reject(new Error('Too many redirects'));
https.get(reqUrl, { headers }, (res) => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
res.resume();
const loc = res.headers.location;
const nextUrl = loc.startsWith('http') ? loc : new URL(loc, reqUrl).href;
doRequest(nextUrl, redirects + 1);
return;
}
if (res.statusCode !== 200) {
res.resume();
try { fs.unlinkSync(file); } catch (_) {}
return reject(new Error(`HTTP ${res.statusCode} for ${reqUrl}`));
}
const stream = fs.createWriteStream(file);
res.pipe(stream);
stream.on('finish', () => { stream.close(); resolve(file); });
stream.on('error', (e) => {
stream.close();
try { fs.unlinkSync(file); } catch (_) {}
reject(e);
});
}).on('error', reject);
};
doRequest(url, 0);
});
}
/**
* 从 GitHub Release 下载并解压
*/
async function downloadFromRelease(key) {
const distName = PLATFORM_MAP[key];
if (!distName) {
console.error(`[prepare-qimingcode] 不支持的平台: ${key}`);
return false;
}
const resourceKey = getResourcePlatformKey(key);
const binary = getBinaryName(key);
const destDir = path.join(resDir, resourceKey, 'bin');
const destPath = path.join(destDir, binary);
// 检查是否已是最新(版本匹配 + SHA256 一致)
const versionFile = path.join(resDir, '.version');
if (fs.existsSync(destPath) && fs.existsSync(versionFile)) {
if (fs.readFileSync(versionFile, 'utf-8').trim() === QIMINGCODE_VERSION) {
// 额外校验:比对已缓存 tar.gz 与当前 dest 的 SHA256 是否记录一致
// 防止 release 被 force-push 后 .version 匹配但二进制已过期
const shaFile = path.join(resDir, `.sha256-${getResourcePlatformKey(key)}`);
if (fs.existsSync(shaFile)) {
const expectedHash = fs.readFileSync(shaFile, 'utf-8').trim();
const currentHash = sha256File(destPath);
if (currentHash === expectedHash) {
// 关键:即使命中 SHA256 + .version也必须校验二进制内部版本号
// 否则一旦资源目录里的二进制未更新但标记文件被更新,会导致永远跳过下载。
const innerVersion = verifyBinaryVersion(destPath, QIMINGCODE_VERSION, key, currentHash);
if (innerVersion && innerVersion !== QIMINGCODE_VERSION) {
console.warn(
`[prepare-qimingcode] ${key}: 版本标记为 ${QIMINGCODE_VERSION} 但二进制为 ${innerVersion},将强制重新下载覆盖`,
);
} else {
const sizeMB = (fs.statSync(destPath).size / 1024 / 1024).toFixed(1);
console.log(
`[prepare-qimingcode] ${key} ✓ (已是最新 ${sizeMB} MB, SHA256=${currentHash.slice(0, 16)}...)`
+ (FORCE_REFRESH_ON_MATCH ? ',将执行目录重铺以清理残留文件' : ''),
);
if (!FORCE_REFRESH_ON_MATCH) return true;
}
}
console.warn(`[prepare-qimingcode] ${key}: SHA256 不匹配,需重新下载 (expected=${expectedHash.slice(0, 16)}... current=${currentHash.slice(0, 16)}...)`);
} else {
// 无 SHA256 记录时也做一次内部版本校验:避免“标记已更新、二进制未更新”被掩盖
const innerVersion = verifyBinaryVersion(destPath, QIMINGCODE_VERSION, key, null);
if (innerVersion && innerVersion !== QIMINGCODE_VERSION) {
console.warn(
`[prepare-qimingcode] ${key}: 版本标记为 ${QIMINGCODE_VERSION} 但二进制为 ${innerVersion},将重新下载覆盖`,
);
} else {
const sizeMB = (fs.statSync(destPath).size / 1024 / 1024).toFixed(1);
console.log(
`[prepare-qimingcode] ${key} ✓ (版本匹配 ${sizeMB} MB无 SHA256 记录)`
+ (FORCE_REFRESH_ON_MATCH ? ',将执行目录重铺以清理残留文件' : ',跳过下载'),
);
if (!FORCE_REFRESH_ON_MATCH) return true;
}
}
}
}
// Release 资产命名兼容策略:
// 1) 新命名带版本后缀qimingcode-xxx-v1.1.96.tar.gz
// 2) 旧命名不带版本后缀qimingcode-xxx.tar.gz
// 先尝试新命名404 再回退旧命名,兼容历史 release 与新 CI 命名规则。
const assetCandidates = [`${distName}-v${QIMINGCODE_VERSION}.tar.gz`, `${distName}.tar.gz`];
// WindowsPATH 里常见的是 System32 的 bsdtar它不认 MSYS 的 /d/a/... 路径,
// 只认盘符路径D:\... 或 D:/...。Git for Windows 的 GNU tar 也接受 D:/...。
const toTarPath = (p) => {
if (process.platform !== 'win32') return p;
const match = /^([A-Za-z]):[\\/](.*)$/.exec(p);
if (!match) return p.replace(/\\/g, '/');
const drive = match[1];
const rest = match[2].replace(/\\/g, '/');
return `${drive}:/${rest}`;
};
let lastErr = null;
for (const assetName of assetCandidates) {
const downloadUrl = `https://github.com/${QIMINGCODE_REPO}/releases/download/v${QIMINGCODE_VERSION}/${assetName}`;
console.log(`[prepare-qimingcode] ${key}: 尝试下载 ${assetName} ...`);
try {
for (let attempt = 0; attempt < 2; attempt++) {
const force = attempt > 0;
if (force) {
console.warn(
`[prepare-qimingcode] ${key}: 归档解压失败(常见于缓存被截断),将删除缓存并重新下载 ${assetName}`,
);
}
const archivePath = await download(downloadUrl, assetName, { force });
// 解压到临时目录
const extractDir = path.join(cacheDir, `extract-${key}`);
if (fs.existsSync(extractDir)) {
try { fs.rmSync(extractDir, { recursive: true }); } catch (_) {}
}
fs.mkdirSync(extractDir, { recursive: true });
const tarArchivePath = toTarPath(archivePath);
const tarExtractDir = toTarPath(extractDir);
// 使用参数数组调用 tar避免在 Windows/MSYS 下对 C:\ 路径的错误解析。
// --force-local 仅在 win32 且 tar 支持时使用macOS BSD tar 不支持该选项)。
const tarArgs = ['-xzf', tarArchivePath, '-C', tarExtractDir];
if (process.platform === 'win32') {
try {
const tarHelp = execFileSync('tar', ['--help'], { encoding: 'utf-8', stdio: 'pipe' });
if (typeof tarHelp === 'string' && tarHelp.includes('--force-local')) {
tarArgs.unshift('--force-local');
}
} catch (_) {}
}
try {
execFileSync('tar', tarArgs, { stdio: 'pipe' });
} catch (tarErr) {
if (attempt === 1) throw tarErr;
continue;
}
// 查找二进制文件:优先新名称 qimingcode其次兼容旧名称 opencode。
// 背景:部分历史 release 资产仍产出 opencode 可执行文件。
const binaryCandidates = getBinaryCandidates(key);
const binaryPath = findBinary(extractDir, binaryCandidates);
if (!binaryPath) {
if (attempt === 1) {
throw new Error(`解压后未找到可执行文件(候选: ${binaryCandidates.join(', ')}`);
}
continue;
}
// 复制前先清理目标目录,避免旧版本 assets 文件被“增量复制”保留下来。
resetDestBinDir(destDir);
const extractedBinDir = path.dirname(binaryPath);
const extractedBaseName = path.basename(binaryPath);
// 复制策略:
// A. 命中标准名qimingcode复制整个 bin 目录,尽量保留同目录 assets
// B. 命中别名opencode按目标标准名落盘保证后续路径稳定
if (extractedBaseName === binary) {
fs.cpSync(extractedBinDir, destDir, { recursive: true });
} else {
fs.copyFileSync(binaryPath, destPath);
// 复制同目录 assets如 models.json
const assetsDir = path.join(extractedBinDir, 'assets');
if (fs.existsSync(assetsDir)) {
const destAssetsDir = path.join(destDir, 'assets');
fs.mkdirSync(destAssetsDir, { recursive: true });
fs.cpSync(assetsDir, destAssetsDir, { recursive: true });
}
}
ensureModelJson(destDir, QIMINGCODE_VERSION);
fs.chmodSync(destPath, 0o755);
const sizeMB = (fs.statSync(destPath).size / 1024 / 1024).toFixed(1);
console.log(`[prepare-qimingcode] ${key} ✓ 从 GitHub Release 下载 (${sizeMB} MB)`);
// macOS ad-hoc 签名
codesign(destPath, key);
// 计算 SHA256签名后用于打印 + 保存
const hash = sha256File(destPath);
// 验证二进制内部版本号 + 打印 SHA256
const innerVersion = verifyBinaryVersion(destPath, QIMINGCODE_VERSION, key, hash);
if (innerVersion && innerVersion !== QIMINGCODE_VERSION) {
// 常见于:本地缓存的 tar.gz 仍是旧内容(例如同名资产被替换、或缓存命中导致一直用旧包)
// 第一次发现不一致时,删除缓存并强制重新下载再试一次。
if (attempt === 0) {
console.warn(
`[prepare-qimingcode] ${key}: 检测到二进制版本不一致,将删除缓存并强制重新下载 ${assetName} 再验证一次`,
);
continue;
}
}
// 保存 SHA256 记录,下次可精确跳过
fs.writeFileSync(path.join(resDir, `.sha256-${resourceKey}`), hash, 'utf-8');
return true;
}
} catch (err) {
lastErr = err;
// 如果新命名不存在404自动回退旧命名继续尝试其他错误也继续尝试下一候选。
console.warn(`[prepare-qimingcode] ${key}: 资产 ${assetName} 失败 (${err.message}),尝试下一个命名...`);
}
}
console.error(`[prepare-qimingcode] ${key}: 下载失败: ${lastErr ? lastErr.message : 'unknown error'}`);
console.error(`[prepare-qimingcode] 请确认 GitHub Release 存在: https://github.com/${QIMINGCODE_REPO}/releases/tag/v${QIMINGCODE_VERSION}`);
return false;
}
/**
* 在解压目录中递归查找二进制文件
*/
function findBinary(dir, binaryNames) {
const names = Array.isArray(binaryNames) ? binaryNames : [binaryNames];
// 先走最常见目录bin/ 与 package/bin/
for (const name of names) {
const direct = path.join(dir, 'bin', name);
if (fs.existsSync(direct)) return direct;
const pkgBin = path.join(dir, 'package', 'bin', name);
if (fs.existsSync(pkgBin)) return pkgBin;
}
// 新 release 可能是“根目录单文件”
for (const name of names) {
const rootFile = path.join(dir, name);
if (fs.existsSync(rootFile)) return rootFile;
}
// 最后兜底递归搜索(最多 3 层)
for (const name of names) {
const found = _findRecursive(dir, name, 3);
if (found) return found;
}
return null;
}
function _findRecursive(dir, binaryName, maxDepth) {
if (maxDepth <= 0) return null;
try {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const fullPath = path.join(dir, entry.name);
if (entry.isFile() && entry.name === binaryName) return fullPath;
if (entry.isDirectory()) {
const found = _findRecursive(fullPath, binaryName, maxDepth - 1);
if (found) return found;
}
}
} catch (_) {}
return null;
}
// ==================== 通用 ====================
/**
* 计算文件 SHA256
*/
function sha256File(filePath) {
try {
return execFileSync('shasum', ['-a', '256', filePath], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim().split(/\s+/)[0];
} catch {
// Windows 或无 shasum 时,用 Node.js crypto
const crypto = require('crypto');
const data = fs.readFileSync(filePath);
return crypto.createHash('sha256').update(data).digest('hex');
}
}
/**
* 验证二进制内部版本号是否与期望版本匹配
* qimingcode 的 release tag 可能与二进制内部版本不一致,
* 需要检测以避免 .version 标记与实际二进制不符。
*/
function verifyBinaryVersion(binaryPath, expectedVersion, key, hash) {
// 打印 SHA256由调用方传入避免重复计算
if (hash) {
console.log(`[prepare-qimingcode] ${key}: SHA256=${hash}`);
}
try {
const output = execFileSync(binaryPath, ['-v'], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
if (output !== expectedVersion) {
console.warn(
`[prepare-qimingcode] ${key}: ⚠️ 二进制内部版本 ${output} 与期望版本 ${expectedVersion} 不一致release tag 版本与二进制版本不同步)`,
);
}
return output;
} catch (e) {
// 二进制可能不支持 -v 或无法在本平台执行(交叉编译场景),跳过校验
console.warn(`[prepare-qimingcode] ${key}: 无法验证二进制版本(${e.message}),跳过校验`);
return null;
}
}
function codesign(binaryPath, key) {
if (process.platform === 'darwin') {
try {
execSync(`codesign --force --sign - "${binaryPath}"`, { stdio: 'pipe' });
} catch {
console.warn(`[prepare-qimingcode] ${key} 签名失败(不影响功能)`);
}
}
}
async function main() {
const allPlatforms = process.argv.includes('--all') || process.argv.includes('--all-platforms');
const useLocalDist = !!process.env.QIMINGCODE_DIST_DIR;
const mode = useLocalDist ? '本地 dist 复制' : 'GitHub Release 下载';
fs.mkdirSync(resDir, { recursive: true });
const keys = allPlatforms ? Object.keys(PLATFORM_MAP) : [getPlatformKey()];
console.log(`[prepare-qimingcode] 模式: ${mode}`);
console.log(`[prepare-qimingcode] 版本: v${QIMINGCODE_VERSION}`);
console.log(`[prepare-qimingcode] 平台: ${keys.join(', ')}`);
if (!allPlatforms && !PLATFORM_MAP[keys[0]]) {
console.error(`[prepare-qimingcode] 不支持的平台: ${keys[0]}`);
console.error(`[prepare-qimingcode] 支持的平台: ${Object.keys(PLATFORM_MAP).join(', ')}`);
process.exit(1);
}
let ok = 0;
let fail = 0;
for (const key of keys) {
const success = useLocalDist ? copyFromDist(key) : await downloadFromRelease(key);
if (success) {
ok++;
} else {
fail++;
}
}
if (ok > 0) {
// 写入版本标记
fs.writeFileSync(path.join(resDir, '.version'), QIMINGCODE_VERSION, 'utf-8');
console.log(`[prepare-qimingcode] ✓ 版本: ${QIMINGCODE_VERSION}`);
}
console.log(`[prepare-qimingcode] 完成: ${ok} 成功, ${fail} 失败`);
if (fail > 0 && !allPlatforms) {
process.exit(1);
}
}
main();

View File

@@ -0,0 +1,15 @@
#!/usr/bin/env node
/**
* 场景仅限 Windows 客户端沙箱:仅在 win32 宿主上构建 qiming-sandbox-helper.exe
* 供 prepare:all 串联。macOS/Linux 上立即退出 0不执行 cargo。
*/
const { execSync } = require("child_process");
const path = require("path");
if (process.platform !== "win32") {
console.log("[prepare-sandbox-helper-win] 非 Windows跳过 build:sandbox-helper");
process.exit(0);
}
const script = path.join(__dirname, "build-sandbox-helper.js");
execSync(`node "${script}"`, { stdio: "inherit" });

View File

@@ -0,0 +1,142 @@
#!/usr/bin/env node
/**
* 准备三端沙箱运行时资源
*
* 约定:
* - 子模块目录:../agent-sandbox-runtime相对 crates/agent-electron-client
* - 清单文件manifest.json
* - 目标目录resources/sandbox-runtime/bin
*
* (仅 Windows 客户端Sandbox helpermanifest 的 win32 产物或另行构建的
* qiming-sandbox-helper.exe亦可由 Rust crate crates/windows-sandbox-helper
* 通过 npm run build:sandbox-helper 生成到 resources/sandbox-helper。
*/
const fs = require("fs");
const path = require("path");
const crypto = require("crypto");
const { getProjectRoot } = require("../utils/project-paths");
const projectRoot = getProjectRoot();
const cratesDir = path.resolve(projectRoot, "..");
const sandboxRuntimeSubmodule = path.join(cratesDir, "agent-sandbox-runtime");
const manifestPath = path.join(sandboxRuntimeSubmodule, "manifest.json");
const targetRoot = path.join(projectRoot, "resources", "sandbox-runtime");
const targetBin = path.join(targetRoot, "bin");
function getPlatformKey() {
const arch = process.env.TARGET_ARCH || process.arch;
return `${process.platform}-${arch}`;
}
function sha256(filePath) {
const hash = crypto.createHash("sha256");
const data = fs.readFileSync(filePath);
hash.update(data);
return hash.digest("hex");
}
function ensureDir(dirPath) {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
}
function normalizeEntry(entry) {
if (!entry) return null;
if (typeof entry === "string") {
return { source: entry };
}
if (typeof entry !== "object") return null;
const source = entry.source || entry.path || entry.file;
if (!source || typeof source !== "string") return null;
return {
source,
sha256: typeof entry.sha256 === "string" ? entry.sha256.toLowerCase() : null,
targetName: typeof entry.targetName === "string" ? entry.targetName : null,
};
}
function loadManifest() {
if (!fs.existsSync(manifestPath)) {
console.warn(
`[prepare-sandbox-runtime] 未找到 manifest: ${manifestPath},跳过(子模块未初始化或未接入)`,
);
return null;
}
const raw = fs.readFileSync(manifestPath, "utf-8");
return JSON.parse(raw);
}
function resolvePlatformArtifact(manifest, key) {
const table = manifest.platforms || manifest.artifacts || {};
return normalizeEntry(table[key]);
}
function main() {
const key = getPlatformKey();
const manifest = loadManifest();
if (!manifest) return;
const artifact = resolvePlatformArtifact(manifest, key);
if (!artifact) {
console.warn(
`[prepare-sandbox-runtime] manifest 未提供平台 ${key} 的产物定义,跳过`,
);
return;
}
const sourcePath = path.isAbsolute(artifact.source)
? artifact.source
: path.join(sandboxRuntimeSubmodule, artifact.source);
if (!fs.existsSync(sourcePath)) {
throw new Error(
`[prepare-sandbox-runtime] 产物不存在: ${sourcePath}platform=${key}`,
);
}
if (artifact.sha256) {
const current = sha256(sourcePath);
if (current !== artifact.sha256) {
throw new Error(
`[prepare-sandbox-runtime] 校验失败: expected=${artifact.sha256}, actual=${current}, file=${sourcePath}`,
);
}
}
ensureDir(targetBin);
const targetName = artifact.targetName || path.basename(sourcePath);
const targetPath = path.join(targetBin, targetName);
fs.copyFileSync(sourcePath, targetPath);
if (process.platform !== "win32") {
try {
fs.chmodSync(targetPath, 0o755);
} catch (_) {}
}
const resolved = {
version: manifest.version || "unknown",
platform: key,
source: sourcePath,
target: targetPath,
sha256: artifact.sha256 || null,
preparedAt: new Date().toISOString(),
};
fs.writeFileSync(
path.join(targetRoot, "resolved-manifest.json"),
JSON.stringify(resolved, null, 2),
"utf-8",
);
fs.writeFileSync(path.join(targetBin, ".platform-key"), key, "utf-8");
console.log(`[prepare-sandbox-runtime] ${key} -> ${targetName}`);
}
try {
main();
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
}

View File

@@ -0,0 +1,261 @@
#!/usr/bin/env node
/**
* 多平台 uv 集成:构建前按当前平台准备 resources/uv/bin/
*
* 1) 若已存在 resources/uv/<platform-arch>/bin/,则复制到 resources/uv/bin/
* 2) 否则从 GitHub Releases (astral-sh/uv) 下载当前平台包,解压到 <platform-arch>/ 再复制到 bin/
*
* 打包时 electron-builder 的 extraResources 会打包 resources/uv → .app/Contents/Resources/uv
* 运行时 getUvBinPath() 使用 Resources/uv/bin/uv 或 uv.exe
*
* 平台 keydarwin-arm64 | darwin-x64 | win32-x64 | win32-arm64 | linux-x64 | linux-arm64
*/
const path = require('path');
const fs = require('fs');
const https = require('https');
const { execSync } = require('child_process');
const { getProjectRoot } = require('../utils/project-paths');
const UV_VERSION = '0.10.8';
const projectRoot = getProjectRoot();
const uvRoot = path.join(projectRoot, 'resources', 'uv');
const cacheDir = path.join(uvRoot, '.cache');
// Node 与 Electron 一致darwin | win32 | linuxx64 | arm64
function getPlatformKey() {
const p = process.platform;
const a = process.env.TARGET_ARCH || process.arch;
return `${p}-${a}`;
}
// 当前平台对应的 uv 官方 release 资源文件名(不含 .tar.gz / .zip
const UV_ASSET_SUFFIX = {
'darwin-arm64': 'uv-aarch64-apple-darwin',
'darwin-x64': 'uv-x86_64-apple-darwin',
'win32-x64': 'uv-x86_64-pc-windows-msvc',
'win32-arm64': 'uv-aarch64-pc-windows-msvc',
'linux-x64': 'uv-x86_64-unknown-linux-gnu',
'linux-arm64': 'uv-aarch64-unknown-linux-gnu',
};
function copyDirRecursive(src, dest) {
if (!fs.existsSync(src)) return;
fs.mkdirSync(dest, { recursive: true });
for (const name of fs.readdirSync(src)) {
const s = path.join(src, name);
const d = path.join(dest, name);
if (fs.statSync(s).isDirectory()) {
copyDirRecursive(s, d);
} else {
fs.copyFileSync(s, d);
}
}
}
function copyToDestBin(key) {
const srcBin = path.join(uvRoot, key, 'bin');
const destBin = path.join(uvRoot, 'bin');
if (!fs.existsSync(srcBin)) return false;
if (!fs.existsSync(destBin)) fs.mkdirSync(destBin, { recursive: true });
for (const f of fs.readdirSync(srcBin)) {
const src = path.join(srcBin, f);
if (fs.statSync(src).isFile()) {
fs.copyFileSync(src, path.join(destBin, f));
console.log(`[prepare-uv] ${key} -> bin/${f}`);
}
}
return true;
}
/**
* 下载文件到 cache 目录。使用 preferredFilename 避免重定向后 URL 过长导致 ENAMETOOLONG。
* @param {string} url - 下载地址
* @param {string} [preferredFilename] - 保存文件名(建议传入,如 uv-aarch64-apple-darwin.tar.gz
*/
function download(url, preferredFilename) {
return new Promise((resolve, reject) => {
const filename = preferredFilename || path.basename(url.split('?')[0]) || 'download';
const file = path.join(cacheDir, filename);
// 检查缓存是否存在且有效Windows: > 1MB
if (fs.existsSync(file)) {
try {
const stats = fs.statSync(file);
const minSize = process.platform === 'win32' ? 1024 * 1024 : 100 * 1024;
if (stats.size > minSize) {
console.log(`[prepare-uv] 使用缓存: ${filename} (${Math.round(stats.size / 1024 / 1024)} MB)`);
resolve(file);
return;
}
} catch (e) {
// 文件可能被锁定,删除后重新下载
console.log(`[prepare-uv] 缓存文件异常,删除后重新下载`);
try { fs.unlinkSync(file); } catch (_) {}
}
}
const stream = fs.createWriteStream(file);
https.get(url, { headers: { 'User-Agent': 'Qiming-Agent-Build' } }, (res) => {
if (res.statusCode === 302 || res.statusCode === 301) {
stream.close();
fs.unlink(file, () => {});
const loc = res.headers.location;
return download(loc.startsWith('http') ? loc : new URL(loc, url).href, preferredFilename).then(resolve).catch(reject);
}
if (res.statusCode !== 200) {
stream.close();
fs.unlink(file, () => {});
return reject(new Error(`HTTP ${res.statusCode} ${url}`));
}
res.pipe(stream);
stream.on('finish', () => { stream.close(); resolve(file); });
stream.on('error', (e) => { stream.close(); reject(e); });
}).on('error', (e) => { stream.close(); reject(e); });
});
}
function extractArchive(archivePath, outDir) {
fs.mkdirSync(outDir, { recursive: true });
const ext = path.extname(archivePath).toLowerCase();
const isZip = ext === '.zip';
if (isZip) {
if (process.platform === 'win32') {
// Windows: 使用 tar 解压 zipWindows 10+ 内置 tar
// 注意:需要等待文件流完全关闭,避免 "being used by another process" 错误
try {
execSync(`tar -xf "${archivePath}" -C "${outDir}"`, { stdio: 'inherit' });
} catch (e) {
// 如果 tar 失败,尝试使用 PowerShell可能遇到文件锁定问题
console.log('[prepare-uv] tar 解压失败,尝试 PowerShell...');
const psPath = archivePath.replace(/'/g, "''");
const psDest = outDir.replace(/'/g, "''");
execSync(
`powershell -NoProfile -Command "Expand-Archive -LiteralPath '${psPath}' -DestinationPath '${psDest}' -Force"`,
{ stdio: 'inherit' },
);
}
} else {
try {
execSync(`unzip -o -q "${archivePath}" -d "${outDir}"`, { stdio: 'inherit' });
} catch (e) {
execSync(`tar -xf "${archivePath}" -C "${outDir}"`, { stdio: 'inherit' });
}
}
} else {
execSync(`tar -xzf "${archivePath}" -C "${outDir}"`, { stdio: 'inherit' });
}
}
function moveExtractedToKey(extractDir, key) {
const entries = fs.readdirSync(extractDir);
const targetRoot = path.join(uvRoot, key);
if (entries.length === 1) {
const inner = path.join(extractDir, entries[0]);
if (fs.statSync(inner).isDirectory()) {
copyDirRecursive(inner, targetRoot);
} else {
fs.mkdirSync(path.join(targetRoot, 'bin'), { recursive: true });
fs.copyFileSync(inner, path.join(targetRoot, 'bin', entries[0]));
}
} else {
copyDirRecursive(extractDir, targetRoot);
}
// Windows zip 解压后 uv.exe 在根目录,需要移动到 bin/
const binDir = path.join(targetRoot, 'bin');
const uvExe = path.join(targetRoot, 'uv.exe');
const uvBin = path.join(targetRoot, 'uv');
const uvxExe = path.join(targetRoot, 'uvx.exe');
const uvxBin = path.join(targetRoot, 'uvx');
const uvwExe = path.join(targetRoot, 'uvw.exe');
// 确保 bin 目录存在
if (!fs.existsSync(binDir)) {
fs.mkdirSync(binDir, { recursive: true });
}
// 将根目录的 uv 相关文件移动到 bin/
if (fs.existsSync(uvExe)) fs.renameSync(uvExe, path.join(binDir, 'uv.exe'));
if (fs.existsSync(uvBin)) fs.renameSync(uvBin, path.join(binDir, 'uv'));
if (fs.existsSync(uvxExe)) fs.renameSync(uvxExe, path.join(binDir, 'uvx.exe'));
if (fs.existsSync(uvxBin)) fs.renameSync(uvxBin, path.join(binDir, 'uvx'));
if (fs.existsSync(uvwExe)) fs.renameSync(uvwExe, path.join(binDir, 'uvw.exe'));
}
async function downloadAndPrepare(key, suffix, version) {
const isZip = process.platform === 'win32';
const ext = isZip ? '.zip' : '.tar.gz';
const assetName = suffix + ext;
const downloadUrl = `https://github.com/astral-sh/uv/releases/download/${version}/${assetName}`;
console.log(`[prepare-uv] 下载 uv ${version} (${assetName}) ...`);
if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true });
const archivePath = await download(downloadUrl, assetName);
const extractDir = path.join(cacheDir, `uv-extract-${key}`);
if (fs.existsSync(extractDir)) {
try { fs.rmSync(extractDir, { recursive: true }); } catch (_) {}
}
extractArchive(archivePath, extractDir);
moveExtractedToKey(extractDir, key);
if (!copyToDestBin(key)) {
console.warn('[prepare-uv] 解压后未找到 bin 目录,请检查包结构');
}
}
async function main() {
const key = getPlatformKey();
const srcDir = path.join(uvRoot, key);
const destBin = path.join(uvRoot, 'bin');
const uvName = process.platform === 'win32' ? 'uv.exe' : 'uv';
const destUv = path.join(destBin, uvName);
const platformKeyFile = path.join(destBin, '.platform-key');
console.log(`[prepare-uv] 平台: ${key}, 源码目录: ${srcDir}, 目标目录: ${destBin}`);
// 检查 .platform-key 是否匹配,不匹配则清理并重新下载
if (fs.existsSync(destUv)) {
if (fs.existsSync(platformKeyFile)) {
const existingKey = fs.readFileSync(platformKeyFile, 'utf-8').trim();
if (existingKey === key) {
console.log(`[prepare-uv] uv 已存在且架构匹配 (${key}), 跳过下载`);
return;
}
console.log(`[prepare-uv] 架构不匹配: 已有 ${existingKey}, 需要 ${key}, 清理并重新下载`);
} else {
console.log(`[prepare-uv] uv 已存在但缺少 .platform-key, 无法确认架构, 清理并重新下载`);
}
fs.rmSync(destBin, { recursive: true, force: true });
}
// 如果源码目录存在但 bin 不完整,尝试复制
if (fs.existsSync(srcDir) && copyToDestBin(key)) {
if (fs.existsSync(destUv)) {
fs.writeFileSync(platformKeyFile, key, 'utf-8');
console.log(`[prepare-uv] 使用已有 uv (${key}), 已复制到 bin/ 并写入 .platform-key`);
return;
}
}
const suffix = UV_ASSET_SUFFIX[key];
if (!suffix) {
console.warn(`[prepare-uv] 未支持的平台: ${key}`);
return;
}
const version = UV_VERSION;
console.log(`[prepare-uv] 使用 uv 版本: ${version}`);
try {
await downloadAndPrepare(key, suffix, version);
// Write .platform-key marker after successful download
if (fs.existsSync(destBin)) {
fs.writeFileSync(platformKeyFile, key, 'utf-8');
console.log(`[prepare-uv] 已写入 .platform-key: ${key}`);
}
} catch (err) {
console.error('[prepare-uv] 下载或解压失败:', err.message);
process.exit(1);
}
}
main();

View File

@@ -0,0 +1,133 @@
#!/usr/bin/env node
/**
* Prepare offline wheels for windows-mcp under resources/windows-mcp/wheels.
*
* Runtime installation will happen on end-user machine to avoid non-relocatable
* uv tool launcher issues when app is installed in a different path.
*
* Note: Bundled uv no longer provides `uv pip download`; we use
* `uv run ... python -m pip download` with a pinned Python (3.13+) so wheel tags
* match package `Requires-Python` and typical `uv tool install` on Windows.
*/
const path = require('path');
const fs = require('fs');
const { execFileSync } = require('child_process');
const { getProjectRoot, resolveFromProject } = require('../utils/project-paths');
const projectRoot = getProjectRoot();
const resDir = path.join(projectRoot, 'resources', 'windows-mcp');
const wheelsDir = path.join(resDir, 'wheels');
const manifestPath = path.join(resDir, 'manifest.json');
const PACKAGE_NAME = 'windows-mcp';
/** Interpreter for pip download (windows-mcp currently requires Python >= 3.13). 若修改须同步 windowsMcp.ts 的 WINDOWS_MCP_UV_PYTHON。 */
const PIP_DOWNLOAD_PYTHON = '3.13';
function getUvBinPath() {
const uvBinName = process.platform === 'win32' ? 'uv.exe' : 'uv';
return resolveFromProject('resources', 'uv', 'bin', uvBinName);
}
function runPipDownload(uvBin, destDir, packageName, env) {
execFileSync(
uvBin,
[
'run',
'--no-project',
'--isolated',
'--python',
PIP_DOWNLOAD_PYTHON,
'-w',
'pip',
'python',
'-m',
'pip',
'download',
'--dest',
destDir,
'--only-binary',
':all:',
packageName,
],
{ stdio: 'inherit', env },
);
}
function resolvePinnedVersion(files) {
const wheel = files.find((name) => /^windows_mcp-([^-]+)-.*\.whl$/i.test(name));
if (!wheel) {
return null;
}
const match = wheel.match(/^windows_mcp-([^-]+)-.*\.whl$/i);
return match ? match[1] : null;
}
function main() {
if (process.platform !== 'win32') {
console.log('[prepare-windows-mcp] Skipped: not Windows platform');
return;
}
const uvBin = getUvBinPath();
if (!fs.existsSync(uvBin)) {
console.error('[prepare-windows-mcp] uv not found at:', uvBin);
process.exit(1);
}
console.log('[prepare-windows-mcp] Using uv:', uvBin);
if (fs.existsSync(resDir)) {
console.log('[prepare-windows-mcp] Removing old resources/windows-mcp ...');
fs.rmSync(resDir, { recursive: true, force: true });
}
fs.mkdirSync(wheelsDir, { recursive: true });
console.log('[prepare-windows-mcp] Downloading wheels for offline installation ...');
try {
runPipDownload(uvBin, wheelsDir, PACKAGE_NAME, {
...process.env,
UV_PYTHON_DOWNLOADS: process.env.UV_PYTHON_DOWNLOADS ?? 'automatic',
});
} catch (error) {
console.error('[prepare-windows-mcp] Failed to download windows-mcp wheels:', error.message);
process.exit(1);
}
const files = fs
.readdirSync(wheelsDir)
.filter((name) => {
const full = path.join(wheelsDir, name);
return fs.statSync(full).isFile() && name.toLowerCase().endsWith('.whl');
})
.sort();
if (files.length === 0) {
console.error('[prepare-windows-mcp] No files downloaded into wheels directory');
process.exit(1);
}
const version = resolvePinnedVersion(files);
if (!version) {
console.error('[prepare-windows-mcp] Could not resolve pinned windows-mcp version from downloaded wheels');
console.error('[prepare-windows-mcp] Downloaded files:', files.join(', '));
process.exit(1);
}
const manifest = {
packageName: PACKAGE_NAME,
version,
resolvedSpec: `${PACKAGE_NAME}==${version}`,
generatedAt: new Date().toISOString(),
files,
};
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
console.log(`[prepare-windows-mcp] ✓ Downloaded ${files.length} files`);
console.log(`[prepare-windows-mcp] ✓ Pinned ${manifest.resolvedSpec}`);
console.log(`[prepare-windows-mcp] ✓ Wrote manifest: ${manifestPath}`);
console.log('[prepare-windows-mcp] ✓ resources/windows-mcp ready');
}
main();