产品化改造:完善本地集成与启动体验

This commit is contained in:
baiyanyun
2026-06-02 17:55:08 +08:00
parent a280774f50
commit 873cd6ef53
24 changed files with 3490 additions and 45 deletions

View File

@@ -2,11 +2,12 @@
/**
* 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
* 2) 本地源码构建(默认开发模式):自动查找同级 qimingcode 仓库,缺 dist 时用 Bun 构建当前平台
* 3) GitHub Release 下载CI/正式构建兜底)
* QIMINGCODE_ALLOW_REMOTE=1 npm run prepare:qimingcode
*
* 打包时 electron-builder extraResources 将 resources/qimingcode 打包到应用内
* 运行时 getQimingCodeBundledBinPath() 解析对应平台二进制
@@ -17,6 +18,8 @@
*
* 环境变量:
* QIMINGCODE_DIST_DIR — qimingcode 本地构建产物目录(设置后走本地复制模式)
* QIMINGCODE_SOURCE_DIR — qimingcode 本地源码仓库目录(默认查找 ../../../qimingcode
* QIMINGCODE_ALLOW_REMOTE — 允许本地源码不可用时从 GitHub Release 下载
* QIMINGCODE_REPO — GitHub 仓库(默认 qiming-ai/qimingcode
* GITHUB_TOKEN — GitHub token私有仓库或提高速率限制用
*/
@@ -228,14 +231,123 @@ function copyFromDist(key) {
function getLocalDistDir() {
const candidates = [
process.env.QIMINGCODE_DIST_DIR,
path.resolve(projectRoot, '..', '..', '..', 'qimingcode', 'packages', 'opencode', 'dist'),
getLocalSourceDir() ? path.join(getLocalSourceDir(), 'packages', 'opencode', 'dist') : null,
path.join(process.env.HOME || '/root', 'workspace/qimingcode/packages/opencode/dist'),
].filter(Boolean);
return candidates.find((candidate) => fs.existsSync(candidate)) || candidates[0];
}
// ==================== 模式 2: GitHub Release 下载 ====================
function getLocalSourceDir() {
const candidates = [
process.env.QIMINGCODE_SOURCE_DIR,
path.resolve(projectRoot, '..', '..', '..', 'qimingcode'),
path.join(process.env.HOME || '/root', 'workspace/qimingcode'),
].filter(Boolean);
for (const candidate of candidates) {
if (fs.existsSync(path.join(candidate, 'packages', 'opencode', 'package.json'))) {
return candidate;
}
}
return null;
}
function getBunCommand() {
if (process.env.BUN_BIN) return process.env.BUN_BIN;
try {
return execFileSync('which', ['bun'], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
} catch {
return null;
}
}
function getLocalBuildEnv(sourceDir, opencodeDir, bun) {
const bunDir = path.dirname(bun);
const localModelsJson = path.join(opencodeDir, 'assets', 'models.json');
const env = {
...process.env,
OPENCODE_VERSION: QIMINGCODE_VERSION,
PATH: `${bunDir}${path.delimiter}${process.env.PATH || ''}`,
...getLocalRuntimeEnv(sourceDir),
};
if (!process.env.MODELS_DEV_API_JSON && fs.existsSync(localModelsJson)) {
env.MODELS_DEV_API_JSON = localModelsJson;
console.log(`[prepare-qimingcode] 使用本地模型快照: ${localModelsJson}`);
}
return env;
}
function getLocalRuntimeEnv(sourceDir = getLocalSourceDir() || projectRoot) {
const localRuntimeDir = path.join(sourceDir, '.local-runtime');
return {
XDG_CACHE_HOME: process.env.XDG_CACHE_HOME || path.join(localRuntimeDir, 'cache'),
XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME || path.join(localRuntimeDir, 'config'),
XDG_DATA_HOME: process.env.XDG_DATA_HOME || path.join(localRuntimeDir, 'data'),
XDG_STATE_HOME: process.env.XDG_STATE_HOME || path.join(localRuntimeDir, 'state'),
OPENCODE_DATA_DIR: process.env.OPENCODE_DATA_DIR || path.join(localRuntimeDir, 'data', 'opencode'),
};
}
function ensureLocalDist(key) {
const distDir = getLocalDistDir();
const distName = PLATFORM_MAP[key];
if (!distName) return false;
const binDir = path.join(distDir, distName, 'bin');
const existing = getBinaryCandidates(key)
.map((candidate) => path.join(binDir, candidate))
.find((candidatePath) => fs.existsSync(candidatePath));
if (existing) return true;
const sourceDir = getLocalSourceDir();
if (!sourceDir) return false;
const bun = getBunCommand();
if (!bun) {
console.error('[prepare-qimingcode] 找到本地 qimingcode 源码,但未找到 Bun。');
console.error('[prepare-qimingcode] 请先安装 Bun或设置 BUN_BIN=/path/to/bun 后重试。');
return false;
}
console.log(`[prepare-qimingcode] 本地 dist 缺失,使用源码构建当前平台: ${sourceDir}`);
const opencodeDir = path.join(sourceDir, 'packages', 'opencode');
const hasRootNodeModules = fs.existsSync(path.join(sourceDir, 'node_modules'));
const hasOpencodeNodeModules = fs.existsSync(path.join(opencodeDir, 'node_modules'));
if (!hasRootNodeModules || !hasOpencodeNodeModules) {
console.log('[prepare-qimingcode] 安装 qimingcode 依赖 (bun install --ignore-scripts)...');
execFileSync(bun, ['install', '--ignore-scripts'], { cwd: sourceDir, stdio: 'inherit' });
}
const buildEnv = getLocalBuildEnv(sourceDir, opencodeDir, bun);
try {
console.log('[prepare-qimingcode] 修复 node-pty helper 权限...');
execFileSync(bun, ['run', 'fix-node-pty'], { cwd: opencodeDir, stdio: 'inherit', env: buildEnv });
} catch (err) {
console.warn(`[prepare-qimingcode] node-pty 权限修复失败,将继续构建: ${err.message}`);
}
console.log('[prepare-qimingcode] 构建 qimingcode 当前平台二进制...');
execFileSync(
bun,
['run', 'script/build.ts', '--single', '--skip-embed-web-ui', '--skip-install'],
{
cwd: opencodeDir,
stdio: 'inherit',
env: buildEnv,
},
);
return getBinaryCandidates(key)
.map((candidate) => path.join(binDir, candidate))
.some((candidatePath) => fs.existsSync(candidatePath));
}
// ==================== 模式 2: GitHub Release 下载(显式兜底) ====================
/**
* 下载文件到缓存目录
@@ -568,7 +680,11 @@ function verifyBinaryVersion(binaryPath, expectedVersion, key, hash) {
}
try {
const output = execFileSync(binaryPath, ['-v'], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
const output = execFileSync(binaryPath, ['-v'], {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
env: { ...process.env, ...getLocalRuntimeEnv() },
}).trim();
if (output !== expectedVersion) {
console.warn(
`[prepare-qimingcode] ${key}: ⚠️ 二进制内部版本 ${output} 与期望版本 ${expectedVersion} 不一致release tag 版本与二进制版本不同步)`,
@@ -594,8 +710,13 @@ function codesign(binaryPath, key) {
async function main() {
const allPlatforms = process.argv.includes('--all') || process.argv.includes('--all-platforms');
const useLocalDist = !!process.env.QIMINGCODE_DIST_DIR || fs.existsSync(getLocalDistDir());
const mode = useLocalDist ? '本地 dist 复制' : 'GitHub Release 下载';
const allowRemote = process.env.QIMINGCODE_ALLOW_REMOTE === '1';
const localSourceDir = getLocalSourceDir();
const mode = localSourceDir || process.env.QIMINGCODE_DIST_DIR
? '本地优先dist/源码构建)'
: allowRemote
? 'GitHub Release 下载'
: '本地优先(未找到本地源码)';
fs.mkdirSync(resDir, { recursive: true });
@@ -615,7 +736,18 @@ async function main() {
let fail = 0;
for (const key of keys) {
const success = useLocalDist ? copyFromDist(key) : await downloadFromRelease(key);
let success = false;
if (ensureLocalDist(key)) {
success = copyFromDist(key);
} else if (allowRemote) {
console.warn('[prepare-qimingcode] 本地 qimingcode 产物不可用,按 QIMINGCODE_ALLOW_REMOTE=1 使用远端兜底');
success = await downloadFromRelease(key);
} else {
console.error('[prepare-qimingcode] 本地 qimingcode 产物不可用,且未允许远端下载。');
console.error('[prepare-qimingcode] 请确保同级存在 qimingcode 仓库,或设置 QIMINGCODE_SOURCE_DIR / QIMINGCODE_DIST_DIR。');
}
if (success) {
ok++;
} else {