Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 13 additions & 89 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -1,101 +1,25 @@
# 自动生成的镜像配置 (by setup-mirror.mjs)
# 生成时间: 2026-06-07T08:50:30.745Z
# --- begin auto-generated by setup-mirror.mjs ---
# 生成时间: 2026-06-24T19:18:59.425Z

# npm registry
registry=https://mirrors.huaweicloud.com/repository/npm/

# 用户自定义配置
# npm registry

# 用户自定义配置
# npm registry

# 用户自定义配置
# npm registry

# 用户自定义配置
# npm registry

# 用户自定义配置
# npm registry

# 用户自定义配置
# npm registry

# 用户自定义配置
# npm registry

# 用户自定义配置
# npm registry

# 用户自定义配置
# npm registry

# 用户自定义配置
# npm registry

# 用户自定义配置
# npm registry

# 用户自定义配置
# npm registry

# 用户自定义配置
# npm registry

# sharp 二进制镜像

# better-sqlite3 二进制镜像

# puppeteer chromium 镜像

# 用户自定义配置
# npm registry

# sharp 二进制镜像

# better-sqlite3 二进制镜像

# puppeteer chromium 镜像

# 用户自定义配置
# npm registry

# sharp 二进制镜像

# better-sqlite3 二进制镜像

# puppeteer chromium 镜像

# 用户自定义配置
# npm registry

# sharp 二进制镜像

# better-sqlite3 二进制镜像

# puppeteer chromium 镜像

# 用户自定义配置
# npm registry

# sharp 二进制镜像
# 网络超时与重试(避免 GitHub 不稳定时无限等待)
fetch-timeout=60000
fetch-retries=3

# better-sqlite3 二进制镜像
# electron 二进制镜像
electron_mirror=https://mirrors.huaweicloud.com/repository/npm/-/binary/electron/
electron_builder_binaries_mirror=https://mirrors.huaweicloud.com/repository/npm/-/binary/electron-builder-binaries/

# puppeteer chromium 镜像

# 用户自定义配置
# npm registry
puppeteer_download_base_url=https://cdn.npmmirror.com/binaries/chrome-for-testing

# sharp 二进制镜像
sharp_binary_host=https://mirrors.huaweicloud.com/repository/npm/-/binary/sharp
sharp_libvips_binary_host=https://mirrors.huaweicloud.com/repository/npm/-/binary/sharp-libvips

# better-sqlite3 二进制镜像
better_sqlite3_binary_host=https://mirrors.huaweicloud.com/repository/npm/-/binary/better-sqlite3

# puppeteer chromium 镜像

# 用户自定义配置
# 强制 pnpm 生成 npm 兼容的扁平 node_modules(避免 .pnpm/ 符号链接结构)。
# 原因:Electron 打包后 backend 以 ELECTRON_RUN_AS_NODE 模式从 resources/app/ 下运行,
# 通过 extraResources 拷贝的 node_modules 必须是扁平结构才能被 Node 的模块解析器正常向上查找;
# pnpm 的默认 isolated 链接器会让传递依赖丢失,导致 Cannot find module 报错。
# --- end auto-generated by setup-mirror.mjs ---
3 changes: 3 additions & 0 deletions .puppeteerrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
chromeDownloadBaseUrl: 'https://cdn.npmmirror.com/binaries/chrome-for-testing',
};
40 changes: 37 additions & 3 deletions scripts/deploy.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -848,7 +848,13 @@ async function deployProject(options = {}) {
}

try {
run('npm', ['install', '--include=dev'], '安装后端依赖');
run('npm', ['install', '--include=dev'], '安装后端依赖', { timeout: 1800000 });
} catch (error) {
console.error('[deploy] npm install 失败,常见原因:');
console.error('[deploy] 1. 网络不稳定导致 electron/puppeteer 二进制下载卡死');
console.error('[deploy] 2. GitHub 在当前网络下不可达');
console.error('[deploy] 可尝试:配置代理后重试,或手动执行 npm install --ignore-scripts 后单独安装二进制');
throw error;
} finally {
if (puppeteerHostSet) {
delete process.env.PUPPETEER_DOWNLOAD_HOST;
Expand Down Expand Up @@ -1130,6 +1136,15 @@ function getServiceRunUser() {

function buildServiceContent() {
const serviceUser = getServiceRunUser();
// 构建 PATH:用户 bin 目录 + systemd 默认目录,确保 systemd 环境能找到 opencode/node
const userHome = resolveHomeDirForUser(serviceUser);
const pathDirs = [
path.join(userHome, '.local', 'bin'),
path.join(userHome, '.opencode', 'bin'),
'/usr/local/bin',
'/usr/bin',
'/bin',
];
return [
'[Unit]',
'Description=Feishu OpenCode Bridge',
Expand All @@ -1139,10 +1154,12 @@ function buildServiceContent() {
'Type=simple',
`User=${serviceUser}`,
`WorkingDirectory=${rootDir}`,
`ExecStart=${process.execPath} dist/index.js`,
`ExecStart=${process.execPath} dist/admin/index.js`,
'Restart=always',
'RestartSec=3',
`EnvironmentFile=-${envPath}`,
`Environment=PATH=${pathDirs.join(':')}`,
`Environment=HOME=${userHome}`,
`StandardOutput=append:${outLog}`,
`StandardError=append:${errLog}`,
'',
Expand All @@ -1161,13 +1178,30 @@ async function installSystemdService() {
console.log(`[deploy] 检测到 sudo 环境,将以 ${sudoUser} 身份执行部署...`);
const sudoUserHome = resolveHomeDirForUser(sudoUser);

// 构建普通用户的 PATH(sudo secure_path 会覆盖,需显式恢复)
const userPathDirs = [
path.join(sudoUserHome, '.local', 'bin'),
path.join(sudoUserHome, '.opencode', 'bin'),
'/usr/local/bin',
'/usr/bin',
'/bin',
];
const userPath = userPathDirs.join(':');

// 以普通用户身份执行部署
const deployResult = spawnSync('sudo', ['-u', sudoUser, 'node', path.join(scriptDir, 'deploy.mjs'), 'deploy'], {
// 用 env 命令显式注入 PATH,绕过 sudo secure_path 覆盖
// 用绝对路径的 node,避免 PATH 里仍找不到
const deployResult = spawnSync('sudo', [
'-u', sudoUser,
'env', `PATH=${userPath}`, `HOME=${sudoUserHome}`,
process.execPath, path.join(scriptDir, 'deploy.mjs'), 'deploy',
], {
cwd: rootDir,
stdio: 'inherit',
env: {
...process.env,
HOME: sudoUserHome,
PATH: userPath,
BRIDGE_SKIP_SUDO_WARNING: '1',
},
});
Expand Down
Empty file modified scripts/deploy.sh
100644 → 100755
Empty file.
63 changes: 57 additions & 6 deletions scripts/process-manager.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
*/

import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { spawn, spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
Expand Down Expand Up @@ -112,7 +113,7 @@ function findBridgeProcesses(excludeSelf = false, excludePid = null) {
* @param {number} excludePid - 要排除的指定 PID
* @returns {number[]} 进程 PID 列表
*/
function findOpenCodeProcesses(excludeSelf = false, excludePid = null) {
function findOpenCodeProcesses(excludeSelf = false, excludePid = null, serveOnly = false) {
const pids = [];
const currentPid = process.pid;

Expand All @@ -137,7 +138,11 @@ function findOpenCodeProcesses(excludeSelf = false, excludePid = null) {
}
// opencode.exe 直接就是目标进程
if (match[1] === 'opencode.exe') {
pids.push(pid);
// Windows 下 tasklist 不含命令行参数,serveOnly 无法精确判断
// 保守起见:serveOnly 时不通过 tasklist 匹配,改由 PID 文件判断
if (!serveOnly) {
pids.push(pid);
}
} else if (isOpenCodeProcessByCommand(pid)) {
pids.push(pid);
}
Expand All @@ -162,7 +167,11 @@ function findOpenCodeProcesses(excludeSelf = false, excludePid = null) {
const command = parts.slice(10).join(' ');

// 匹配 OpenCode 进程
if (isOpenCodeCommand(command)) {
if (serveOnly) {
if (isOpencodeServeCommand(command)) {
pids.push(pid);
}
} else if (isOpenCodeCommand(command)) {
pids.push(pid);
}
}
Expand Down Expand Up @@ -200,6 +209,22 @@ function isOpenCodeCommand(command) {
return /\bopencode\b/.test(normalizedCmd) || normalizedCmd.includes('opencode-cli');
}

/**
* 精确匹配 `opencode serve` 进程(用于 serve 幂等检查)
* 排除交互式 opencode、opencode debug、opencode attach 等非 serve 命令
*/
function isOpencodeServeCommand(command) {
const normalizedCmd = command.replace(/\\/g, '/');
if (isBridgeCommand(normalizedCmd)) {
return false;
}
if (normalizedCmd.includes('opencode-bridge')) {
return false;
}
// 匹配 opencode serve(允许前面有路径,后面有 --port 等参数)
return /\bopencode\b\s+serve\b/.test(normalizedCmd);
}

function getProcessCommandLine(pid) {
if (!isWindows()) {
return null;
Expand Down Expand Up @@ -510,7 +535,7 @@ function main() {
if (alivePid !== null) {
console.log(`[process-manager] opencode serve 运行中 (PID: ${alivePid})`);
} else {
const scanPids = findOpenCodeProcesses();
const scanPids = findOpenCodeProcesses(false, null, true);
if (scanPids.length > 0) {
console.log(`[process-manager] opencode serve 运行中(扫描到 PID: ${scanPids.join(', ')},但 PID 文件缺失)`);
} else {
Expand All @@ -536,7 +561,32 @@ function main() {
* 返回 { type: 'node-script', nodeExe, script } 或 { type: 'shell', cmd: 'opencode' }
*/
function resolveOpenCodeExecutable() {
// 优先读 OPENCODE_AUTO_START_CMD(用户自定义绝对路径)
const customCmd = process.env.OPENCODE_AUTO_START_CMD?.trim();
if (customCmd) {
const parts = customCmd.split(/\s+/);
const exePath = parts[0];
const restArgs = parts.slice(1);
if (fs.existsSync(exePath)) {
return { type: 'direct', exe: exePath, extraArgs: restArgs };
}
return { type: 'shell', cmd: customCmd };
}

if (!isWindows()) {
// Linux/macOS: 检查常见安装路径
const homeDir = os.homedir();
const candidates = [
path.join(homeDir, '.opencode', 'bin', 'opencode'), // 官方脚本安装
path.join(homeDir, '.local', 'bin', 'opencode'), // npm i -g (用户级 prefix)
'/usr/local/bin/opencode', // npm i -g (root)
'/usr/bin/opencode', // 包管理器安装
];
for (const candidate of candidates) {
if (fs.existsSync(candidate)) {
return { type: 'direct', exe: candidate };
}
}
return { type: 'shell', cmd: 'opencode' };
}

Expand Down Expand Up @@ -667,7 +717,8 @@ function startOpenCodeServe(options = {}) {
}

// 也通过进程扫描检查(防止 PID 文件丢失但进程还在的情况)
const scanPids = findOpenCodeProcesses();
// 仅匹配 `opencode serve` 进程,排除交互式 opencode 避免误判
const scanPids = findOpenCodeProcesses(false, null, true);
if (scanPids.length > 0) {
// 补写 PID 文件
try {
Expand Down Expand Up @@ -727,7 +778,7 @@ function startOpenCodeServe(options = {}) {
fs.closeSync(stdoutFd);
fs.closeSync(stderrFd);
} else if (exe.type === 'direct') {
const directArgs = ['serve'];
const directArgs = [...(exe.extraArgs || []), 'serve'];
if (servePort !== null) directArgs.push('--port', String(servePort));
child = spawn(exe.exe, directArgs, {
detached: true,
Expand Down
Loading