Major changes: - Add HandList.tsx component for left sidebar - Add HandTaskPanel.tsx for middle content area - Restructure Sidebar tabs: 分身/HANDS/Workflow - Remove Hands tab from RightPanel - Localize all UI text to Chinese - Archive legacy OpenClaw documentation - Add Hands integration lessons document - Update feature checklist with new components UI improvements: - Left sidebar now shows Hands list with status icons - Middle area shows selected Hand's tasks and results - Consistent styling with Tailwind CSS - Chinese status labels and buttons Documentation: - Create docs/archive/openclaw-legacy/ for old docs - Add docs/knowledge-base/hands-integration-lessons.md - Update docs/knowledge-base/feature-checklist.md - Update docs/knowledge-base/README.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
297 lines
10 KiB
JavaScript
297 lines
10 KiB
JavaScript
import { mkdtempSync, rmSync, existsSync, cpSync, mkdirSync, readdirSync, statSync } from 'node:fs';
|
||
import os from 'node:os';
|
||
import path from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { spawnSync } from 'node:child_process';
|
||
|
||
const __filename = fileURLToPath(import.meta.url);
|
||
const __dirname = path.dirname(__filename);
|
||
const desktopRoot = path.resolve(__dirname, '..');
|
||
const localToolsRoot = path.join(desktopRoot, 'local-tools');
|
||
const args = new Set(process.argv.slice(2));
|
||
const dryRun = args.has('--dry-run');
|
||
const showHelp = args.has('--help') || args.has('-h');
|
||
const projectCacheRoot = path.join(desktopRoot, 'src-tauri', 'target', '.tauri');
|
||
const userCacheRoot = process.env.LOCALAPPDATA ? path.join(process.env.LOCALAPPDATA, 'tauri') : null;
|
||
const cacheRoots = [projectCacheRoot, userCacheRoot].filter(Boolean);
|
||
const nsisUtilsDllName = 'nsis_tauri_utils.dll';
|
||
|
||
function log(message) {
|
||
console.log(`[preseed-tauri-tools] ${message}`);
|
||
}
|
||
|
||
function fail(message) {
|
||
console.error(`[preseed-tauri-tools] ${message}`);
|
||
process.exit(1);
|
||
}
|
||
|
||
function ensureDir(dirPath) {
|
||
mkdirSync(dirPath, { recursive: true });
|
||
}
|
||
|
||
function findNsisRoot(dirPath) {
|
||
return findDirectoryContaining(dirPath, (current, entries) => {
|
||
const names = new Set(entries.map((entry) => entry.name));
|
||
return names.has('makensis.exe') || names.has('Bin');
|
||
});
|
||
}
|
||
|
||
function findWixRoot(dirPath) {
|
||
return findDirectoryContaining(dirPath, (current, entries) => {
|
||
const names = new Set(entries.map((entry) => entry.name));
|
||
return names.has('candle.exe') || names.has('light.exe');
|
||
});
|
||
}
|
||
|
||
function directoryHasToolSignature(toolName, dirPath) {
|
||
if (!existsSync(dirPath) || !statSync(dirPath).isDirectory()) {
|
||
return false;
|
||
}
|
||
|
||
const match = toolName === 'NSIS' ? findNsisRoot(dirPath) : findWixRoot(dirPath);
|
||
|
||
return Boolean(match);
|
||
}
|
||
|
||
function directoryHasReadyNsisLayout(dirPath) {
|
||
const root = findNsisRoot(dirPath);
|
||
if (!root) {
|
||
return false;
|
||
}
|
||
|
||
return existsSync(path.join(root, 'Plugins', 'x86-unicode', nsisUtilsDllName))
|
||
|| existsSync(path.join(root, 'Plugins', 'x86-unicode', 'additional', nsisUtilsDllName));
|
||
}
|
||
|
||
function copyDirectoryContents(sourceDir, destinationDir) {
|
||
ensureDir(destinationDir);
|
||
for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
|
||
const sourcePath = path.join(sourceDir, entry.name);
|
||
const destinationPath = path.join(destinationDir, entry.name);
|
||
cpSync(sourcePath, destinationPath, { recursive: true, force: true });
|
||
}
|
||
}
|
||
|
||
function expandZip(zipPath, destinationDir) {
|
||
const command = [
|
||
'-NoProfile',
|
||
'-Command',
|
||
`Expand-Archive -LiteralPath '${zipPath.replace(/'/g, "''")}' -DestinationPath '${destinationDir.replace(/'/g, "''")}' -Force`,
|
||
];
|
||
const result = spawnSync('powershell', command, {
|
||
stdio: 'inherit',
|
||
shell: process.platform === 'win32',
|
||
});
|
||
if (typeof result.status === 'number' && result.status !== 0) {
|
||
process.exit(result.status);
|
||
}
|
||
if (result.error) {
|
||
throw result.error;
|
||
}
|
||
}
|
||
|
||
function findDirectoryContaining(rootDir, predicate) {
|
||
const queue = [rootDir];
|
||
while (queue.length > 0) {
|
||
const current = queue.shift();
|
||
const entries = readdirSync(current, { withFileTypes: true });
|
||
if (predicate(current, entries)) {
|
||
return current;
|
||
}
|
||
for (const entry of entries) {
|
||
if (entry.isDirectory()) {
|
||
queue.push(path.join(current, entry.name));
|
||
}
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function firstExistingFile(candidates) {
|
||
for (const candidate of candidates.filter(Boolean).map((value) => path.resolve(value))) {
|
||
if (existsSync(candidate) && statSync(candidate).isFile()) {
|
||
return candidate;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function resolveNsisSupportDll() {
|
||
return firstExistingFile([
|
||
process.env.ZCLAW_TAURI_NSIS_TAURI_UTILS_DLL,
|
||
path.join(localToolsRoot, nsisUtilsDllName),
|
||
path.join(localToolsRoot, 'nsis_tauri_utils-v0.5.3', nsisUtilsDllName),
|
||
path.join(localToolsRoot, 'nsis_tauri_utils-v0.5.2', nsisUtilsDllName),
|
||
]);
|
||
}
|
||
|
||
function resolveSource(toolName) {
|
||
if (toolName === 'NSIS') {
|
||
const dirCandidates = [
|
||
process.env.ZCLAW_TAURI_NSIS_DIR,
|
||
path.join(localToolsRoot, 'NSIS'),
|
||
].filter(Boolean).map((value) => path.resolve(value));
|
||
for (const candidate of dirCandidates) {
|
||
if (directoryHasReadyNsisLayout(candidate)) {
|
||
return { kind: 'dir', path: candidate };
|
||
}
|
||
}
|
||
|
||
const supportDll = resolveNsisSupportDll();
|
||
|
||
for (const candidate of dirCandidates) {
|
||
if (directoryHasToolSignature('NSIS', candidate)) {
|
||
return { kind: 'nsis-base-dir', path: candidate, supportDll };
|
||
}
|
||
}
|
||
|
||
const zipCandidates = [
|
||
process.env.ZCLAW_TAURI_NSIS_ZIP,
|
||
path.join(localToolsRoot, 'nsis.zip'),
|
||
path.join(localToolsRoot, 'nsis-3.11.zip'),
|
||
path.join(localToolsRoot, 'nsis-3.08.zip'),
|
||
].filter(Boolean).map((value) => path.resolve(value));
|
||
for (const candidate of zipCandidates) {
|
||
if (existsSync(candidate) && statSync(candidate).isFile()) {
|
||
return { kind: 'nsis-base-zip', path: candidate, supportDll };
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
const envDirKey = toolName === 'NSIS' ? 'ZCLAW_TAURI_NSIS_DIR' : 'ZCLAW_TAURI_WIX_DIR';
|
||
const envZipKey = toolName === 'NSIS' ? 'ZCLAW_TAURI_NSIS_ZIP' : 'ZCLAW_TAURI_WIX_ZIP';
|
||
const localZipCandidates = toolName === 'NSIS'
|
||
? [path.join(localToolsRoot, 'nsis.zip'), path.join(localToolsRoot, 'nsis-3.11.zip')]
|
||
: [
|
||
path.join(localToolsRoot, 'wix.zip'),
|
||
path.join(localToolsRoot, 'wix314-binaries.zip'),
|
||
path.join(localToolsRoot, 'wix311-binaries.zip'),
|
||
];
|
||
|
||
const localDirCandidates = toolName === 'NSIS'
|
||
? [path.join(localToolsRoot, toolName)]
|
||
: [path.join(localToolsRoot, 'WixTools314'), path.join(localToolsRoot, 'WixTools')];
|
||
const dirCandidates = [process.env[envDirKey], ...localDirCandidates].filter(Boolean).map((value) => path.resolve(value));
|
||
for (const candidate of dirCandidates) {
|
||
if (directoryHasToolSignature(toolName, candidate)) {
|
||
return { kind: 'dir', path: candidate };
|
||
}
|
||
}
|
||
|
||
const zipCandidates = [process.env[envZipKey], ...localZipCandidates].filter(Boolean).map((value) => path.resolve(value));
|
||
for (const candidate of zipCandidates) {
|
||
if (existsSync(candidate) && statSync(candidate).isFile()) {
|
||
return { kind: 'zip', path: candidate };
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function normalizeToolSource(toolName, source) {
|
||
if (toolName === 'NSIS' && source.kind !== 'dir') {
|
||
const tempRoot = mkdtempSync(path.join(os.tmpdir(), 'zclaw-tauri-tool-'));
|
||
const assembledRoot = path.join(tempRoot, 'NSIS');
|
||
ensureDir(assembledRoot);
|
||
|
||
if (source.kind === 'nsis-base-dir') {
|
||
const baseRoot = findNsisRoot(source.path);
|
||
if (!baseRoot) {
|
||
fail(`NSIS 目录未找到 makensis:${source.path}`);
|
||
}
|
||
copyDirectoryContents(baseRoot, assembledRoot);
|
||
} else if (source.kind === 'nsis-base-zip') {
|
||
const extractedRoot = path.join(tempRoot, 'extract');
|
||
ensureDir(extractedRoot);
|
||
expandZip(source.path, extractedRoot);
|
||
const baseRoot = findNsisRoot(extractedRoot);
|
||
if (!baseRoot) {
|
||
fail(`NSIS zip 解压后未找到 makensis:${source.path}`);
|
||
}
|
||
copyDirectoryContents(baseRoot, assembledRoot);
|
||
}
|
||
|
||
if (!source.supportDll) {
|
||
fail(`检测到 NSIS 基础包,但缺少 ${nsisUtilsDllName}。请放到 desktop/local-tools/${nsisUtilsDllName} 或设置 ZCLAW_TAURI_NSIS_TAURI_UTILS_DLL。`);
|
||
}
|
||
|
||
const pluginsDir = path.join(assembledRoot, 'Plugins', 'x86-unicode');
|
||
const additionalPluginsDir = path.join(pluginsDir, 'additional');
|
||
ensureDir(pluginsDir);
|
||
ensureDir(additionalPluginsDir);
|
||
cpSync(source.supportDll, path.join(pluginsDir, nsisUtilsDllName), { force: true });
|
||
cpSync(source.supportDll, path.join(additionalPluginsDir, nsisUtilsDllName), { force: true });
|
||
|
||
return { tempRoot, path: assembledRoot };
|
||
}
|
||
|
||
if (source.kind === 'dir') {
|
||
return source.path;
|
||
}
|
||
|
||
const tempRoot = mkdtempSync(path.join(os.tmpdir(), 'zclaw-tauri-tool-'));
|
||
const extractedRoot = path.join(tempRoot, 'extract');
|
||
ensureDir(extractedRoot);
|
||
expandZip(source.path, extractedRoot);
|
||
const normalized = toolName === 'NSIS'
|
||
? findNsisRoot(extractedRoot)
|
||
: findWixRoot(extractedRoot);
|
||
|
||
if (!normalized) {
|
||
fail(`${toolName} zip 解压后未找到有效工具目录:${source.path}`);
|
||
}
|
||
|
||
return { tempRoot, path: normalized };
|
||
}
|
||
|
||
function printUsage() {
|
||
console.log('Usage: node scripts/preseed-tauri-tools.mjs [--dry-run]');
|
||
console.log('Sources:');
|
||
console.log(' ZCLAW_TAURI_NSIS_DIR / desktop/local-tools/NSIS');
|
||
console.log(' ZCLAW_TAURI_NSIS_ZIP / desktop/local-tools/nsis.zip or nsis-3.11.zip');
|
||
console.log(` ZCLAW_TAURI_NSIS_TAURI_UTILS_DLL / desktop/local-tools/${nsisUtilsDllName}`);
|
||
console.log(' ZCLAW_TAURI_WIX_DIR / desktop/local-tools/WixTools314 or WixTools');
|
||
console.log(' ZCLAW_TAURI_WIX_ZIP / desktop/local-tools/wix.zip or wix314-binaries.zip');
|
||
}
|
||
|
||
if (showHelp) {
|
||
printUsage();
|
||
process.exit(0);
|
||
}
|
||
|
||
for (const toolName of ['NSIS', 'WixTools']) {
|
||
const source = resolveSource(toolName);
|
||
if (!source) {
|
||
log(`${toolName} 未提供本地预置源,跳过。`);
|
||
continue;
|
||
}
|
||
|
||
let normalized = null;
|
||
try {
|
||
normalized = normalizeToolSource(toolName, source);
|
||
const sourcePath = typeof normalized === 'string' ? normalized : normalized.path;
|
||
for (const cacheRoot of cacheRoots) {
|
||
const destinationNames = toolName === 'WixTools' ? ['WixTools314', 'WixTools'] : [toolName];
|
||
for (const destinationName of destinationNames) {
|
||
const destination = path.join(cacheRoot, destinationName);
|
||
log(`${toolName}: ${source.path} -> ${destination}`);
|
||
if (!dryRun) {
|
||
ensureDir(cacheRoot);
|
||
rmSync(destination, { recursive: true, force: true });
|
||
copyDirectoryContents(sourcePath, destination);
|
||
}
|
||
}
|
||
}
|
||
} finally {
|
||
if (normalized && typeof normalized !== 'string' && normalized.tempRoot) {
|
||
rmSync(normalized.tempRoot, { recursive: true, force: true });
|
||
}
|
||
}
|
||
}
|
||
|
||
if (dryRun) {
|
||
log('Dry run 完成,未写入任何文件。');
|
||
}
|