chore: initialize qiming workspace repository
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
#!/usr/bin/env node
|
||||
/* eslint-disable no-console */
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const PROJECT_ROOT = process.env.QIMING_BOUNDARY_PROJECT_ROOT
|
||||
? path.resolve(process.env.QIMING_BOUNDARY_PROJECT_ROOT)
|
||||
: path.resolve(__dirname, '..', '..');
|
||||
const SRC_ROOT = path.join(PROJECT_ROOT, 'src');
|
||||
|
||||
const MAIN_ROOT = path.join(SRC_ROOT, 'main');
|
||||
const RENDERER_ROOT = path.join(SRC_ROOT, 'renderer');
|
||||
const PRELOAD_ROOT = path.join(SRC_ROOT, 'preload');
|
||||
|
||||
const BANNED_BRIDGE_FILES = [
|
||||
'src/main/startup.ts',
|
||||
'src/main/logConfig.ts',
|
||||
'src/main/serviceManager.ts',
|
||||
'src/main/trayManager.ts',
|
||||
'src/main/autoLaunchManager.ts',
|
||||
'src/main/preload.ts',
|
||||
'src/renderer/services/api.ts',
|
||||
'src/renderer/services/setup.ts',
|
||||
'src/renderer/services/auth.ts',
|
||||
'src/renderer/services/ai.ts',
|
||||
'src/renderer/services/fileServer.ts',
|
||||
'src/renderer/services/lanproxy.ts',
|
||||
'src/renderer/services/agentRunner.ts',
|
||||
'src/renderer/services/sandbox.ts',
|
||||
'src/renderer/services/permissions.ts',
|
||||
'src/renderer/services/skills.ts',
|
||||
'src/renderer/services/im.ts',
|
||||
'src/renderer/services/scheduler.ts',
|
||||
'src/renderer/services/logService.ts',
|
||||
'src/renderer/components/ClientPage.tsx',
|
||||
'src/renderer/components/SettingsPage.tsx',
|
||||
'src/renderer/components/DependenciesPage.tsx',
|
||||
'src/renderer/components/AboutPage.tsx',
|
||||
'src/renderer/components/PermissionsPage.tsx',
|
||||
'src/renderer/components/LogViewer.tsx',
|
||||
'src/renderer/components/MCPSettings.tsx',
|
||||
'src/renderer/components/AgentSettings.tsx',
|
||||
'src/renderer/components/AgentRunnerSettings.tsx',
|
||||
'src/renderer/components/LanproxySettings.tsx',
|
||||
'src/renderer/components/SkillsSync.tsx',
|
||||
'src/renderer/components/IMSettings.tsx',
|
||||
'src/renderer/components/TaskSettings.tsx',
|
||||
'src/renderer/components/SetupWizard.tsx',
|
||||
'src/renderer/components/SetupDependencies.tsx',
|
||||
'src/renderer/components/PermissionModal.tsx',
|
||||
];
|
||||
|
||||
const BANNED_BRIDGE_PATHS = new Set(
|
||||
BANNED_BRIDGE_FILES.map((p) => path.normalize(path.join(PROJECT_ROOT, p)))
|
||||
);
|
||||
|
||||
function isSourceFile(filePath) {
|
||||
return filePath.endsWith('.ts') || filePath.endsWith('.tsx');
|
||||
}
|
||||
|
||||
function walk(dirPath) {
|
||||
const out = [];
|
||||
for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
|
||||
const fullPath = path.join(dirPath, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
if (entry.name === 'node_modules' || entry.name === 'dist') continue;
|
||||
out.push(...walk(fullPath));
|
||||
continue;
|
||||
}
|
||||
if (isSourceFile(fullPath)) out.push(fullPath);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function existsFile(candidate) {
|
||||
try {
|
||||
return fs.statSync(candidate).isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveImport(specifier, fromFile) {
|
||||
let basePath = null;
|
||||
|
||||
if (specifier.startsWith('.')) {
|
||||
basePath = path.resolve(path.dirname(fromFile), specifier);
|
||||
} else if (specifier.startsWith('@/')) {
|
||||
basePath = path.join(SRC_ROOT, specifier.slice(2));
|
||||
} else if (specifier.startsWith('@main/')) {
|
||||
basePath = path.join(MAIN_ROOT, specifier.slice('@main/'.length));
|
||||
} else if (specifier.startsWith('@renderer/')) {
|
||||
basePath = path.join(RENDERER_ROOT, specifier.slice('@renderer/'.length));
|
||||
} else if (specifier.startsWith('@preload/')) {
|
||||
basePath = path.join(PRELOAD_ROOT, specifier.slice('@preload/'.length));
|
||||
} else if (specifier.startsWith('@shared/')) {
|
||||
basePath = path.join(SRC_ROOT, 'shared', specifier.slice('@shared/'.length));
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
const candidates = [
|
||||
basePath,
|
||||
`${basePath}.ts`,
|
||||
`${basePath}.tsx`,
|
||||
`${basePath}.js`,
|
||||
path.join(basePath, 'index.ts'),
|
||||
path.join(basePath, 'index.tsx'),
|
||||
path.join(basePath, 'index.js'),
|
||||
];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (existsFile(candidate)) return path.normalize(candidate);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function lineFromIndex(content, index) {
|
||||
let line = 1;
|
||||
for (let i = 0; i < index; i += 1) {
|
||||
if (content.charCodeAt(i) === 10) line += 1;
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
function collectImports(fileContent) {
|
||||
const specs = [];
|
||||
const patterns = [
|
||||
/\bimport\s+(?:[^'"]+from\s+)?['"]([^'"]+)['"]/g,
|
||||
/\bexport\s+[^'"]*from\s+['"]([^'"]+)['"]/g,
|
||||
/\brequire\(\s*['"]([^'"]+)['"]\s*\)/g,
|
||||
/\bimport\(\s*['"]([^'"]+)['"]\s*\)/g,
|
||||
];
|
||||
for (const pattern of patterns) {
|
||||
let match;
|
||||
while ((match = pattern.exec(fileContent)) !== null) {
|
||||
specs.push({ specifier: match[1], index: match.index });
|
||||
}
|
||||
}
|
||||
return specs;
|
||||
}
|
||||
|
||||
function isWithin(filePath, rootPath) {
|
||||
const rel = path.relative(rootPath, filePath);
|
||||
return rel && !rel.startsWith('..') && !path.isAbsolute(rel);
|
||||
}
|
||||
|
||||
function checkFile(filePath) {
|
||||
const violations = [];
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const imports = collectImports(content);
|
||||
|
||||
for (const imp of imports) {
|
||||
const resolved = resolveImport(imp.specifier, filePath);
|
||||
const line = lineFromIndex(content, imp.index);
|
||||
|
||||
if (resolved && BANNED_BRIDGE_PATHS.has(resolved)) {
|
||||
violations.push({
|
||||
line,
|
||||
rule: 'no-bridge-imports',
|
||||
message: `Do not import bridge file '${imp.specifier}'. Import the new grouped path directly.`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const inRenderer = isWithin(filePath, RENDERER_ROOT);
|
||||
const inMain = isWithin(filePath, MAIN_ROOT);
|
||||
|
||||
if (inRenderer) {
|
||||
const directAliasViolation = imp.specifier.startsWith('@main/') || imp.specifier.startsWith('@preload/');
|
||||
const resolvedViolation = resolved && (isWithin(resolved, MAIN_ROOT) || isWithin(resolved, PRELOAD_ROOT));
|
||||
if (directAliasViolation || resolvedViolation) {
|
||||
violations.push({
|
||||
line,
|
||||
rule: 'renderer-process-boundary',
|
||||
message: `Renderer must not import main/preload code directly ('${imp.specifier}'). Use IPC bridge only.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (inMain) {
|
||||
const directAliasViolation = imp.specifier.startsWith('@renderer/');
|
||||
const resolvedViolation = resolved && isWithin(resolved, RENDERER_ROOT);
|
||||
if (directAliasViolation || resolvedViolation) {
|
||||
violations.push({
|
||||
line,
|
||||
rule: 'main-process-boundary',
|
||||
message: `Main process must not import renderer code ('${imp.specifier}').`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return violations;
|
||||
}
|
||||
|
||||
function runBoundaryCheck() {
|
||||
const files = walk(SRC_ROOT);
|
||||
const allViolations = [];
|
||||
|
||||
for (const file of files) {
|
||||
const violations = checkFile(file);
|
||||
for (const v of violations) {
|
||||
allViolations.push({
|
||||
file: path.relative(PROJECT_ROOT, file),
|
||||
...v,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return allViolations;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const allViolations = runBoundaryCheck();
|
||||
|
||||
if (allViolations.length > 0) {
|
||||
console.error('Import boundary check failed:\n');
|
||||
for (const v of allViolations) {
|
||||
console.error(`- ${v.file}:${v.line} [${v.rule}] ${v.message}`);
|
||||
}
|
||||
console.error(`\nTotal violations: ${allViolations.length}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('Import boundary check passed.');
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
runBoundaryCheck,
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
# 检查端口是否被占用,输出占用进程 PID(若有),exit 0=占用 exit 1=未占用
|
||||
# 与 main 进程 startupPorts.ts 内嵌逻辑一致;Windows 依赖集成 Git Bash (prepare-git) 或系统 PATH 中的 netstat/findstr
|
||||
# 用法: bash check-port.sh PORT
|
||||
|
||||
port="$1"
|
||||
if [[ -z "$port" ]]; then
|
||||
echo "usage: check-port.sh PORT" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ "$OSTYPE" == msys* ]] || [[ "$OSTYPE" == cygwin* ]]; then
|
||||
# Windows (Git Bash / MSYS2): 使用 cmd 的 netstat + findstr
|
||||
out=$(cmd //c "netstat -ano 2>nul | findstr \":${port} \"" 2>/dev/null)
|
||||
else
|
||||
# macOS / Linux
|
||||
out=$(lsof -t -i ":${port}" 2>/dev/null)
|
||||
fi
|
||||
|
||||
if [[ -n "$out" ]]; then
|
||||
# 取首行最后一列作为 PID(netstat 最后一列是 PID;lsof -t 整行即 PID)
|
||||
pid=$(echo "$out" | head -1 | awk '{print $NF}')
|
||||
if [[ -n "$pid" && "$pid" =~ ^[0-9]+$ ]]; then
|
||||
echo "$pid"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
exit 1
|
||||
@@ -0,0 +1,178 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 启动前端口检查脚本(聚合逻辑的 CLI 入口)
|
||||
*
|
||||
* 端口默认值与解析规则与 src/shared/startupPorts.ts 保持一致,请同步修改。
|
||||
* 从 ~/.qimingclaw/qimingclaw.db(Windows: %USERPROFILE%\.qimingclaw\)读取配置。
|
||||
* 端口占用检查:优先统一用 bash 执行 scripts/tools/check-port.sh(Windows 需先 npm run prepare:git 集成 Git Bash),
|
||||
* 无 bash 或脚本时回退到 Node 内联 netstat/lsof。
|
||||
* 依赖:Node;读取 DB 需系统 sqlite3(Windows 上多数未预装,无则用默认端口)。
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { execSync, spawnSync } = require('child_process');
|
||||
const { getProjectRoot } = require('../utils/project-paths');
|
||||
|
||||
const isWin = process.platform === 'win32';
|
||||
const home = process.env.HOME || process.env.USERPROFILE;
|
||||
const dbPath = path.join(home, '.qimingclaw', 'qimingclaw.db');
|
||||
const projectRoot = getProjectRoot();
|
||||
const checkPortShPath = path.join(__dirname, 'check-port.sh');
|
||||
const winBashPath = path.join(projectRoot, 'resources', 'git', 'bin', 'bash.exe');
|
||||
|
||||
// 与 shared/constants + shared/startupPorts 保持一致
|
||||
const DEFAULTS = {
|
||||
agent: 60001,
|
||||
fileServer: 60000,
|
||||
mcp: 18099,
|
||||
lanproxyLocal: 60002,
|
||||
vite: 60173,
|
||||
};
|
||||
|
||||
const LABELS = {
|
||||
agent: 'Agent(ComputerServer)',
|
||||
fileServer: 'FileServer',
|
||||
mcp: 'MCP Proxy',
|
||||
lanproxyLocal: 'Lanproxy',
|
||||
vite: 'Vite',
|
||||
};
|
||||
|
||||
function getSetting(dbPath, key) {
|
||||
try {
|
||||
// key 仅来自内部常量,仍做引号转义以防将来扩展;Windows 路径含反斜杠,双引号包裹路径即可
|
||||
const safeKey = String(key).replace(/'/g, "''");
|
||||
const quotedPath = dbPath.replace(/\\/g, '/'); // sqlite3 在 Windows 上通常也接受正斜杠
|
||||
const out = execSync(`sqlite3 "${quotedPath}" "SELECT value FROM settings WHERE key='${safeKey}';"`, {
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
...(isWin && { shell: true }), // Windows 下若 sqlite3 为 .cmd 或路径含空格,需 shell
|
||||
}).trim();
|
||||
if (!out) return null;
|
||||
try {
|
||||
return JSON.parse(out);
|
||||
} catch {
|
||||
return out;
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePortsFromSettings(getSettingFn) {
|
||||
const step1 = getSettingFn('step1_config');
|
||||
const agent = (step1 && step1.agentPort != null) ? step1.agentPort : DEFAULTS.agent;
|
||||
const fileServer = (step1 && step1.fileServerPort != null) ? step1.fileServerPort : DEFAULTS.fileServer;
|
||||
|
||||
const mcpRaw = getSettingFn('mcp_proxy_port');
|
||||
let mcp = DEFAULTS.mcp;
|
||||
if (typeof mcpRaw === 'number' && Number.isInteger(mcpRaw)) mcp = mcpRaw;
|
||||
else if (typeof mcpRaw === 'string') { const n = parseInt(mcpRaw, 10); if (!Number.isNaN(n)) mcp = n; }
|
||||
|
||||
return {
|
||||
agent,
|
||||
fileServer,
|
||||
mcp,
|
||||
lanproxyLocal: DEFAULTS.lanproxyLocal,
|
||||
vite: DEFAULTS.vite,
|
||||
};
|
||||
}
|
||||
|
||||
function getPortsToCheck(ports, includeVite) {
|
||||
const list = [
|
||||
{ name: 'agent', label: LABELS.agent, port: ports.agent },
|
||||
{ name: 'fileServer', label: LABELS.fileServer, port: ports.fileServer },
|
||||
{ name: 'mcp', label: LABELS.mcp, port: ports.mcp },
|
||||
{ name: 'lanproxyLocal', label: LABELS.lanproxyLocal, port: ports.lanproxyLocal },
|
||||
];
|
||||
if (includeVite) list.push({ name: 'vite', label: LABELS.vite, port: ports.vite });
|
||||
return list;
|
||||
}
|
||||
|
||||
/** 优先用 bash 执行 scripts/tools/check-port.sh(与 main 进程一致),否则回退 Node 内联逻辑 */
|
||||
function isPortInUse(port) {
|
||||
const bashPath = isWin ? (fs.existsSync(winBashPath) ? winBashPath : null) : 'bash';
|
||||
const scriptContent = fs.existsSync(checkPortShPath)
|
||||
? fs.readFileSync(checkPortShPath, 'utf8')
|
||||
: null;
|
||||
|
||||
if (bashPath && scriptContent) {
|
||||
const result = spawnSync(bashPath, ['-c', scriptContent, '_', String(port)], {
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
if (result.status === 0 && result.stdout) {
|
||||
const text = (result.stdout && String(result.stdout)).trim();
|
||||
const pid = text && /^\d+$/.test(text) ? parseInt(text, 10) : undefined;
|
||||
return { inUse: true, pid };
|
||||
}
|
||||
return { inUse: false };
|
||||
}
|
||||
|
||||
// 回退:无 bash 或脚本时用 Node 内联 netstat/lsof
|
||||
try {
|
||||
if (isWin) {
|
||||
const out = execSync(`netstat -ano 2>nul | findstr ":${port} "`, {
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
shell: true,
|
||||
});
|
||||
const text = (out && String(out)).trim();
|
||||
const inUse = text.length > 0;
|
||||
let pid;
|
||||
if (inUse) {
|
||||
const firstLine = text.split(/\r?\n/)[0] || '';
|
||||
const lastCol = firstLine.split(/\s+/).filter(Boolean).pop();
|
||||
if (lastCol && /^\d+$/.test(lastCol)) pid = parseInt(lastCol, 10);
|
||||
}
|
||||
return { inUse, pid };
|
||||
}
|
||||
const out = execSync(`lsof -t -i :${port} 2>/dev/null`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
|
||||
const text = (out && String(out)).trim();
|
||||
const inUse = text.length > 0;
|
||||
const pid = inUse && /^\d+$/.test(text) ? parseInt(text.split('\n')[0], 10) : undefined;
|
||||
return { inUse, pid };
|
||||
} catch {
|
||||
return { inUse: false };
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
const includeVite = process.argv.includes('--vite');
|
||||
const getSettingFn = (key) => getSetting(dbPath, key);
|
||||
|
||||
let ports;
|
||||
let source = '';
|
||||
if (!fs.existsSync(dbPath)) {
|
||||
ports = { ...DEFAULTS };
|
||||
source = '未找到 DB,使用默认端口';
|
||||
} else {
|
||||
try {
|
||||
ports = resolvePortsFromSettings(getSettingFn);
|
||||
source = `已从 ${dbPath} 读取配置`;
|
||||
} catch (err) {
|
||||
ports = { ...DEFAULTS };
|
||||
source = '读取 DB 失败(如未安装 sqlite3),使用默认端口';
|
||||
}
|
||||
}
|
||||
console.log('端口来源:', source);
|
||||
|
||||
const list = getPortsToCheck(ports, includeVite);
|
||||
console.log('检查端口:', list.map(({ label, port }) => `${label}=${port}`).join(', '));
|
||||
console.log('');
|
||||
|
||||
let hasInUse = false;
|
||||
for (const { label, port } of list) {
|
||||
const { inUse, pid } = isPortInUse(port);
|
||||
if (inUse) {
|
||||
hasInUse = true;
|
||||
console.log(` [占用] ${label} (${port}) ${pid ? `PID=${pid}` : ''}`);
|
||||
} else {
|
||||
console.log(` [空闲] ${label} (${port})`);
|
||||
}
|
||||
}
|
||||
|
||||
process.exit(hasInUse ? 1 : 0);
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 清理 Electron / electron-builder 缓存,用于修复「zip: not a valid zip file」等因缓存损坏导致的打包失败。
|
||||
* 清理后重新执行 build:electron 会重新下载对应平台的 Electron。
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
const home = os.homedir();
|
||||
const dirs =
|
||||
process.platform === 'darwin'
|
||||
? [
|
||||
path.join(home, 'Library', 'Caches', 'electron'),
|
||||
path.join(home, 'Library', 'Caches', 'electron-builder'),
|
||||
]
|
||||
: process.platform === 'win32'
|
||||
? [
|
||||
path.join(process.env.LOCALAPPDATA || home, 'electron', 'Cache'),
|
||||
path.join(process.env.LOCALAPPDATA || home, 'electron-builder', 'Cache'),
|
||||
]
|
||||
: [
|
||||
path.join(home, '.cache', 'electron'),
|
||||
path.join(home, '.cache', 'electron-builder'),
|
||||
];
|
||||
|
||||
for (const dir of dirs) {
|
||||
try {
|
||||
if (fs.existsSync(dir)) {
|
||||
fs.rmSync(dir, { recursive: true });
|
||||
console.log('[clean-electron-cache] 已删除:', dir);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[clean-electron-cache] 删除失败:', dir, e.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[clean-electron-cache] 完成,可重新执行 npm run build:electron -- --win');
|
||||
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 生成 / 检查托盘图标
|
||||
*
|
||||
* public/tray/ 下的文件说明:
|
||||
*
|
||||
* macOS(已手工制作,提交在 git 中):
|
||||
* trayTemplate.png — 22x22 黑色剪影 + alpha(macOS Template Image @1x)
|
||||
* trayTemplate@2x.png — 44x44 黑色剪影 + alpha(macOS Template Image @2x)
|
||||
*
|
||||
* Windows / Linux(此脚本自动从 64x64.png 生成):
|
||||
* tray.png — 32x32 彩色
|
||||
* tray@2x.png — 64x64 彩色
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/tools/generate-tray-icons.js
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { getProjectRoot } = require('../utils/project-paths');
|
||||
|
||||
const projectRoot = getProjectRoot();
|
||||
const LOGO_ICON = path.join(projectRoot, 'public', '64x64.png');
|
||||
const OUTPUT_DIR = path.join(projectRoot, 'public', 'tray');
|
||||
|
||||
async function generateTrayIcons() {
|
||||
const sharp = require('sharp');
|
||||
|
||||
if (!fs.existsSync(LOGO_ICON)) {
|
||||
console.error('Source icon not found:', LOGO_ICON);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(OUTPUT_DIR)) {
|
||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
// ---- macOS: 检查手工模板图标是否存在 ----
|
||||
for (const f of ['trayTemplate.png', 'trayTemplate@2x.png']) {
|
||||
const p = path.join(OUTPUT_DIR, f);
|
||||
if (!fs.existsSync(p)) {
|
||||
console.warn(` WARNING: macOS template icon missing: ${f} — please craft manually (black silhouette + alpha)`);
|
||||
} else {
|
||||
console.log(` macOS template: ${f} (exists)`);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Windows / Linux: 从 64x64 logo 生成彩色图标 ----
|
||||
// Logo 占画布 ~75%,四周留透明 padding
|
||||
for (const { size, suffix } of [
|
||||
{ size: 32, suffix: 'tray.png' },
|
||||
{ size: 64, suffix: 'tray@2x.png' },
|
||||
]) {
|
||||
const logoSize = Math.round(size * 0.75);
|
||||
const padding = Math.round((size - logoSize) / 2);
|
||||
|
||||
const outPath = path.join(OUTPUT_DIR, suffix);
|
||||
await sharp(LOGO_ICON)
|
||||
.resize(logoSize, logoSize, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })
|
||||
.extend({
|
||||
top: padding,
|
||||
bottom: size - logoSize - padding,
|
||||
left: padding,
|
||||
right: size - logoSize - padding,
|
||||
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
||||
})
|
||||
.png()
|
||||
.toFile(outPath);
|
||||
|
||||
console.log(` colored: ${suffix} (${size}x${size})`);
|
||||
}
|
||||
|
||||
console.log('Done! Tray dir:', OUTPUT_DIR);
|
||||
}
|
||||
|
||||
generateTrayIcons().catch(err => {
|
||||
console.error('Error generating tray icons:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 测试集成的 Node.js 是否正确配置
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { execSync } = require('child_process');
|
||||
const { getProjectRoot } = require('../utils/project-paths');
|
||||
|
||||
const projectRoot = getProjectRoot();
|
||||
const distMain = path.join(projectRoot, 'dist/main');
|
||||
|
||||
console.log('Testing integrated Node.js 24 configuration...\n');
|
||||
|
||||
// 1. 检查资源目录
|
||||
const platform = `${process.platform}-${process.arch}`;
|
||||
console.log(`Platform: ${platform}`);
|
||||
|
||||
const nodeDir = path.join(projectRoot, 'resources', 'node', platform);
|
||||
const nodeBin = path.join(nodeDir, 'bin', process.platform === 'win32' ? 'node.exe' : 'node');
|
||||
|
||||
console.log(`Node directory: ${nodeDir}`);
|
||||
console.log(`Node binary: ${nodeBin}\n`);
|
||||
|
||||
// 2. 检查目录存在
|
||||
if (!fs.existsSync(nodeDir)) {
|
||||
console.error(`❌ Node.js directory not found: ${nodeDir}`);
|
||||
console.log('💡 Run: npm run prepare:node');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('✅ Node.js directory exists');
|
||||
|
||||
// 3. 检查二进制文件
|
||||
if (!fs.existsSync(nodeBin)) {
|
||||
console.error(`❌ Node binary not found: ${nodeBin}`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('✅ Node binary exists\n');
|
||||
|
||||
// 4. 测试版本
|
||||
try {
|
||||
const result = execSync(`"${nodeBin}" --version`, { encoding: 'utf8' }).trim();
|
||||
console.log(`Node version: ${result}`);
|
||||
// 读取 prepare-node.js 中定义的 NODE_VERSION
|
||||
const prepareScript = fs.readFileSync(path.join(projectRoot, 'scripts', 'prepare', 'prepare-node.js'), 'utf8');
|
||||
const versionMatch = prepareScript.match(/const NODE_VERSION = '([^']+)'/);
|
||||
const expectedVersion = versionMatch ? versionMatch[1] : null;
|
||||
if (expectedVersion && result === `v${expectedVersion}`) {
|
||||
console.log(`✅ Version is v${expectedVersion} (correct)\n`);
|
||||
} else if (expectedVersion) {
|
||||
console.log(`⚠️ Expected v${expectedVersion}, got ${result}\n`);
|
||||
} else {
|
||||
console.log(`⚠️ Could not determine expected version from prepare-node.js\n`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`❌ Failed to run node: ${err.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 5. 测试编译后的 getNodeBinPath
|
||||
console.log('Testing getNodeBinPath() function...');
|
||||
|
||||
try {
|
||||
// 模拟 Electron 环境
|
||||
const oldApp = global.app;
|
||||
global.app = {
|
||||
getPath: (name) => {
|
||||
if (name === 'home') return '/mock/home';
|
||||
return projectRoot;
|
||||
},
|
||||
};
|
||||
|
||||
const dependenciesPath = path.join(distMain, 'services/system/dependencies.js');
|
||||
|
||||
if (!fs.existsSync(dependenciesPath)) {
|
||||
console.log('⚠️ Compiled dependencies.js not found (need to run: npm run build:main)');
|
||||
} else {
|
||||
// 清除缓存重新加载
|
||||
delete require.cache[require.resolve(dependenciesPath)];
|
||||
const { getNodeBinPath } = require(dependenciesPath);
|
||||
|
||||
const resolvedPath = getNodeBinPath();
|
||||
console.log(`getNodeBinPath() returned: ${resolvedPath}`);
|
||||
|
||||
if (resolvedPath) {
|
||||
console.log('✅ getNodeBinPath() returns correct path\n');
|
||||
} else {
|
||||
console.log('❌ getNodeBinPath() returned null\n');
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复 app
|
||||
global.app = oldApp;
|
||||
} catch (err) {
|
||||
console.log(`⚠️ Could not test getNodeBinPath(): ${err.message}\n`);
|
||||
}
|
||||
|
||||
console.log('✅ Integrated Node.js 24 is ready!');
|
||||
console.log('\n💡 When MCP proxy starts, it will use this integrated node.');
|
||||
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# 本地测试自动更新流程
|
||||
#
|
||||
# 步骤:
|
||||
# 1. 打包当前版本作为「旧版」客户端
|
||||
# 2. 临时 bump 到高版本,打包作为「新版」更新源
|
||||
# 3. 用 HTTP 服务托管新版产物
|
||||
# 4. 启动旧版客户端,指向本地更新源
|
||||
#
|
||||
# 用法:
|
||||
# bash scripts/tools/test-update.sh
|
||||
#
|
||||
# 前置条件:
|
||||
# - npm install 已完成
|
||||
# - npx serve 可用(或全局安装 serve)
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
# ---------- 配置 ----------
|
||||
OLD_VERSION=$(node -p "require('./package.json').version")
|
||||
NEW_VERSION="99.0.0" # 足够高,保证触发更新
|
||||
SERVE_PORT=8080
|
||||
|
||||
echo "========================================="
|
||||
echo " 本地更新测试"
|
||||
echo "========================================="
|
||||
echo " 当前版本 (旧版): $OLD_VERSION"
|
||||
echo " 模拟版本 (新版): $NEW_VERSION"
|
||||
echo " 更新源端口: $SERVE_PORT"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
|
||||
# ---------- Step 1: 打包旧版 ----------
|
||||
echo "[1/4] 打包旧版 v${OLD_VERSION} ..."
|
||||
OLD_RELEASE_DIR="$PROJECT_DIR/release/$OLD_VERSION"
|
||||
if [ -d "$OLD_RELEASE_DIR" ] && ls "$OLD_RELEASE_DIR"/*.dmg "$OLD_RELEASE_DIR"/*.exe "$OLD_RELEASE_DIR"/*.AppImage 2>/dev/null | head -1 > /dev/null; then
|
||||
echo " -> 已存在 $OLD_RELEASE_DIR,跳过打包"
|
||||
else
|
||||
npm run dist:unsigned:local
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ---------- Step 2: 临时 bump 版本,打包新版 ----------
|
||||
echo "[2/4] 打包新版 v${NEW_VERSION} ..."
|
||||
NEW_RELEASE_DIR="$PROJECT_DIR/release/$NEW_VERSION"
|
||||
if [ -d "$NEW_RELEASE_DIR" ] && ls "$NEW_RELEASE_DIR"/latest*.yml 2>/dev/null | head -1 > /dev/null; then
|
||||
echo " -> 已存在 $NEW_RELEASE_DIR,跳过打包"
|
||||
else
|
||||
# 临时修改版本号
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
||||
pkg.version = '$NEW_VERSION';
|
||||
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
|
||||
"
|
||||
echo " -> package.json version 临时改为 $NEW_VERSION"
|
||||
|
||||
npm run dist:unsigned:local || true
|
||||
|
||||
# 恢复版本号
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
||||
pkg.version = '$OLD_VERSION';
|
||||
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
|
||||
"
|
||||
echo " -> package.json version 已恢复为 $OLD_VERSION"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ---------- Step 3: 启动本地更新服务器 ----------
|
||||
echo "[3/4] 启动本地更新服务器 ..."
|
||||
echo " -> 托管目录: $NEW_RELEASE_DIR"
|
||||
echo " -> 地址: http://localhost:$SERVE_PORT"
|
||||
echo ""
|
||||
|
||||
# 检查 latest*.yml 是否存在
|
||||
if ! ls "$NEW_RELEASE_DIR"/latest*.yml 2>/dev/null | head -1 > /dev/null; then
|
||||
echo "❌ 错误: $NEW_RELEASE_DIR 中缺少 latest*.yml 文件"
|
||||
echo " electron-updater 需要此文件来检测更新"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo " latest*.yml 内容:"
|
||||
echo " ---"
|
||||
cat "$NEW_RELEASE_DIR"/latest*.yml | head -20 | sed 's/^/ /'
|
||||
echo " ---"
|
||||
echo ""
|
||||
|
||||
# 后台启动 HTTP 服务
|
||||
npx serve "$NEW_RELEASE_DIR" -p $SERVE_PORT --no-clipboard &
|
||||
SERVE_PID=$!
|
||||
sleep 2
|
||||
|
||||
# 验证服务启动
|
||||
if ! curl -s "http://localhost:$SERVE_PORT/" > /dev/null 2>&1; then
|
||||
echo "❌ HTTP 服务启动失败"
|
||||
kill $SERVE_PID 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
echo " -> HTTP 服务已启动 (PID: $SERVE_PID)"
|
||||
echo ""
|
||||
|
||||
# ---------- Step 4: 启动旧版客户端 ----------
|
||||
echo "[4/4] 启动旧版客户端 ..."
|
||||
echo ""
|
||||
|
||||
APP_PATH=""
|
||||
if [ "$(uname)" = "Darwin" ]; then
|
||||
# macOS: 找 .app
|
||||
APP_PATH=$(find "$OLD_RELEASE_DIR" -name "*.app" -maxdepth 2 | head -1)
|
||||
if [ -z "$APP_PATH" ]; then
|
||||
# 可能在 mac-arm64 或 mac 子目录
|
||||
APP_PATH=$(find "$OLD_RELEASE_DIR" -name "*.app" -maxdepth 3 | head -1)
|
||||
fi
|
||||
elif [ "$(uname)" = "Linux" ]; then
|
||||
APP_PATH=$(find "$OLD_RELEASE_DIR" -name "*.AppImage" | head -1)
|
||||
fi
|
||||
|
||||
if [ -z "$APP_PATH" ]; then
|
||||
echo "⚠️ 未找到旧版安装包,请手动启动:"
|
||||
echo ""
|
||||
echo " macOS:"
|
||||
echo " QIMING_UPDATE_SERVER=http://localhost:$SERVE_PORT open \"$OLD_RELEASE_DIR/mac-arm64/QimingClaw.app\""
|
||||
echo ""
|
||||
echo " 或直接运行二进制:"
|
||||
echo " QIMING_UPDATE_SERVER=http://localhost:$SERVE_PORT \"$OLD_RELEASE_DIR/mac-arm64/QimingClaw.app/Contents/MacOS/QimingClaw\""
|
||||
echo ""
|
||||
else
|
||||
echo " 找到旧版应用: $APP_PATH"
|
||||
echo ""
|
||||
if [ "$(uname)" = "Darwin" ]; then
|
||||
BINARY_PATH="$APP_PATH/Contents/MacOS/QimingClaw"
|
||||
echo " 启动命令:"
|
||||
echo " QIMING_UPDATE_SERVER=http://localhost:$SERVE_PORT \"$BINARY_PATH\""
|
||||
echo ""
|
||||
echo " 按 Enter 启动,或 Ctrl+C 取消 ..."
|
||||
read -r
|
||||
QIMING_UPDATE_SERVER="http://localhost:$SERVE_PORT" "$BINARY_PATH" &
|
||||
else
|
||||
echo " 启动命令:"
|
||||
echo " QIMING_UPDATE_SERVER=http://localhost:$SERVE_PORT \"$APP_PATH\""
|
||||
echo ""
|
||||
echo " 按 Enter 启动,或 Ctrl+C 取消 ..."
|
||||
read -r
|
||||
QIMING_UPDATE_SERVER="http://localhost:$SERVE_PORT" "$APP_PATH" &
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo " 测试指引"
|
||||
echo "========================================="
|
||||
echo " 1. 打开「关于」页面"
|
||||
echo " 2. 点「检查更新」或等 10 秒自动检查"
|
||||
echo " 3. 应显示「发现新版本: v$NEW_VERSION」"
|
||||
echo " 4. 点「下载更新」,观察进度条"
|
||||
echo " 5. 下载完成后点「重启安装」"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
echo " 测试完成后按 Ctrl+C 停止更新服务器"
|
||||
|
||||
# 等待 Ctrl+C
|
||||
trap "echo ''; echo '正在清理...'; kill $SERVE_PID 2>/dev/null; echo '完成'; exit 0" INT TERM
|
||||
wait $SERVE_PID 2>/dev/null
|
||||
Reference in New Issue
Block a user