chore: initialize qiming workspace repository
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 构建 Windows 专用 Sandbox helper(qiming-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 x64(target: ${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);
|
||||
}
|
||||
@@ -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/tsc(Unix 风格),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();
|
||||
@@ -0,0 +1,343 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Git 集成(仅 Windows 客户端)
|
||||
*
|
||||
* 仅在 Windows 构建时执行:准备 resources/git/(PortableGit + bash),供端口检查等脚本使用。
|
||||
* macOS/Linux 使用系统 git/bash,不打包内置 Git。
|
||||
*
|
||||
* 参考 LobsterAI 方案:scripts/setup-mingit.js(workspace/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,
|
||||
};
|
||||
@@ -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 bundle(ESM,原生依赖标记为 external)
|
||||
* ├── dist/lib.bundle.cjs — esbuild SDK bundle(CJS,供 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();
|
||||
@@ -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();
|
||||
@@ -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 bundle(esbuild 单文件,含 shebang)
|
||||
* ├── dist/lib.bundle.js — 库 bundle(PersistentMcpBridge 等导出)
|
||||
* └── 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();
|
||||
@@ -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.js,monaco 可能无法加载');
|
||||
}
|
||||
|
||||
console.log('[prepare-monaco] ✓ public/monaco/vs 已准备完成');
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
@@ -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 校验和
|
||||
*
|
||||
* 平台 key:darwin-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();
|
||||
@@ -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();
|
||||
@@ -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`];
|
||||
|
||||
// Windows:PATH 里常见的是 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();
|
||||
@@ -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" });
|
||||
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 准备三端沙箱运行时资源
|
||||
*
|
||||
* 约定:
|
||||
* - 子模块目录:../agent-sandbox-runtime(相对 crates/agent-electron-client)
|
||||
* - 清单文件:manifest.json
|
||||
* - 目标目录:resources/sandbox-runtime/bin
|
||||
*
|
||||
* (仅 Windows 客户端)Sandbox helper:manifest 的 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);
|
||||
}
|
||||
@@ -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
|
||||
*
|
||||
* 平台 key:darwin-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 | linux;x64 | 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 解压 zip(Windows 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();
|
||||
@@ -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();
|
||||
Reference in New Issue
Block a user