feat(hands): restructure Hands UI with Chinese localization

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>
This commit is contained in:
iven
2026-03-14 23:16:32 +08:00
parent 67e1da635d
commit 07079293f4
126 changed files with 36229 additions and 1035 deletions

View File

@@ -0,0 +1,150 @@
#!/usr/bin/env node
/**
* OpenFang Binary Downloader
* Automatically downloads the correct OpenFang binary for the current platform
* Run during Tauri build process
*/
import { execSync } from 'child_process';
import { existsSync, mkdirSync, writeFileSync, renameSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { platform, arch } from 'os';
const __dirname = dirname(fileURLToPath(import.meta.url));
const RESOURCES_DIR = join(__dirname, '../src-tauri/resources/openfang-runtime');
// OpenFang release info
const OPENFANG_REPO = 'RightNow-AI/openfang';
const OPENFANG_VERSION = process.env.OPENFANG_VERSION || 'latest';
interface PlatformConfig {
binaryName: string;
downloadName: string;
}
function getPlatformConfig(): PlatformConfig {
const currentPlatform = platform();
const currentArch = arch();
switch (currentPlatform) {
case 'win32':
return {
binaryName: 'openfang.exe',
downloadName: currentArch === 'x64'
? 'openfang-x86_64-pc-windows-msvc.exe'
: 'openfang-aarch64-pc-windows-msvc.exe',
};
case 'darwin':
return {
binaryName: currentArch === 'arm64'
? 'openfang-aarch64-apple-darwin'
: 'openfang-x86_64-apple-darwin',
downloadName: currentArch === 'arm64'
? 'openfang-aarch64-apple-darwin'
: 'openfang-x86_64-apple-darwin',
};
case 'linux':
return {
binaryName: currentArch === 'arm64'
? 'openfang-aarch64-unknown-linux-gnu'
: 'openfang-x86_64-unknown-linux-gnu',
downloadName: currentArch === 'arm64'
? 'openfang-aarch64-unknown-linux-gnu'
: 'openfang-x86_64-unknown-linux-gnu',
};
default:
throw new Error(`Unsupported platform: ${currentPlatform}`);
}
}
function downloadBinary(): void {
const config = getPlatformConfig();
const baseUrl = `https://github.com/${OPENFANG_REPO}/releases`;
const downloadUrl = OPENFANG_VERSION === 'latest'
? `${baseUrl}/latest/download/${config.downloadName}`
: `${baseUrl}/download/${OPENFANG_VERSION}/${config.downloadName}`;
const outputPath = join(RESOURCES_DIR, config.binaryName);
console.log('='.repeat(60));
console.log('OpenFang Binary Downloader');
console.log('='.repeat(60));
console.log(`Platform: ${platform()} (${arch()})`);
console.log(`Binary: ${config.binaryName}`);
console.log(`Version: ${OPENFANG_VERSION}`);
console.log(`URL: ${downloadUrl}`);
console.log('='.repeat(60));
// Ensure directory exists
if (!existsSync(RESOURCES_DIR)) {
mkdirSync(RESOURCES_DIR, { recursive: true });
}
// Check if already downloaded
if (existsSync(outputPath)) {
console.log('✓ Binary already exists, skipping download.');
return;
}
// Download using curl (cross-platform via Node.js)
console.log('Downloading...');
try {
// Use curl for download (available on all platforms with Git/WSL)
const tempPath = `${outputPath}.tmp`;
if (platform() === 'win32') {
// Windows: use PowerShell
execSync(
`powershell -Command "Invoke-WebRequest -Uri '${downloadUrl}' -OutFile '${tempPath}'"`,
{ stdio: 'inherit' }
);
} else {
// Unix: use curl
execSync(`curl -fsSL -o "${tempPath}" "${downloadUrl}"`, { stdio: 'inherit' });
}
// Rename temp file to final name
renameSync(tempPath, outputPath);
// Make executable on Unix
if (platform() !== 'win32') {
execSync(`chmod +x "${outputPath}"`);
}
console.log('✓ Download complete!');
} catch (error) {
console.error('✗ Download failed:', error);
console.log('\nPlease download manually from:');
console.log(` ${baseUrl}/${OPENFANG_VERSION === 'latest' ? 'latest' : 'tag/' + OPENFANG_VERSION}`);
process.exit(1);
}
}
function updateManifest(): void {
const manifestPath = join(RESOURCES_DIR, 'runtime-manifest.json');
const manifest = {
source: {
binPath: platform() === 'win32' ? 'openfang.exe' : `openfang-${arch()}-${platform()}`,
},
stagedAt: new Date().toISOString(),
version: OPENFANG_VERSION === 'latest' ? new Date().toISOString().split('T')[0].replace(/-/g, '.') : OPENFANG_VERSION,
runtimeType: 'openfang',
description: 'OpenFang Agent OS - Single binary runtime (~32MB)',
endpoints: {
websocket: 'ws://127.0.0.1:4200/ws',
rest: 'http://127.0.0.1:4200/api',
},
};
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
console.log('✓ Manifest updated');
}
// Run
downloadBinary();
updateManifest();
console.log('\n✓ OpenFang runtime ready for build!');

View File

@@ -0,0 +1,167 @@
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const desktopRoot = path.resolve(__dirname, '..');
const outputDir = path.join(desktopRoot, 'src-tauri', 'resources', 'openclaw-runtime');
const dryRun = process.argv.includes('--dry-run');
function log(message) {
console.log(`[prepare-openclaw-runtime] ${message}`);
}
function readFirstExistingPath(commandNames) {
for (const commandName of commandNames) {
try {
const stdout = execFileSync('where.exe', [commandName], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
});
const firstMatch = stdout
.split(/\r?\n/)
.map((line) => line.trim())
.find(Boolean);
if (firstMatch) {
return firstMatch;
}
} catch {
continue;
}
}
return null;
}
function ensureFileExists(filePath, label) {
if (!filePath || !fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
throw new Error(`${label} 不存在:${filePath || '(empty)'}`);
}
}
function ensureDirExists(dirPath, label) {
if (!dirPath || !fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory()) {
throw new Error(`${label} 不存在:${dirPath || '(empty)'}`);
}
}
function resolveOpenClawBin() {
const override = process.env.OPENCLAW_BIN;
if (override) {
return path.resolve(override);
}
const resolved = readFirstExistingPath(['openclaw.cmd', 'openclaw']);
if (!resolved) {
throw new Error('未找到 openclaw 入口。请先安装 OpenClaw或设置 OPENCLAW_BIN。');
}
return resolved;
}
function resolvePackageDir(openclawBinPath) {
const override = process.env.OPENCLAW_PACKAGE_DIR;
if (override) {
return path.resolve(override);
}
return path.join(path.dirname(openclawBinPath), 'node_modules', 'openclaw');
}
function resolveNodeExe(openclawBinPath) {
const override = process.env.OPENCLAW_NODE_EXE;
if (override) {
return path.resolve(override);
}
const bundledNode = path.join(path.dirname(openclawBinPath), 'node.exe');
if (fs.existsSync(bundledNode)) {
return bundledNode;
}
const resolved = readFirstExistingPath(['node.exe', 'node']);
if (!resolved) {
throw new Error('未找到 node.exe。请先安装 Node.js或设置 OPENCLAW_NODE_EXE。');
}
return resolved;
}
function cleanOutputDirectory(dirPath) {
if (!fs.existsSync(dirPath)) {
return;
}
for (const entry of fs.readdirSync(dirPath)) {
fs.rmSync(path.join(dirPath, entry), { recursive: true, force: true });
}
}
function writeCmdLauncher(dirPath) {
const launcher = [
'@ECHO off',
'SETLOCAL',
'SET "_prog=%~dp0\\node.exe"',
'"%_prog%" "%~dp0\\node_modules\\openclaw\\openclaw.mjs" %*',
'',
].join('\r\n');
fs.writeFileSync(path.join(dirPath, 'openclaw.cmd'), launcher, 'utf8');
}
function stageRuntime() {
const openclawBinPath = resolveOpenClawBin();
const packageDir = resolvePackageDir(openclawBinPath);
const nodeExePath = resolveNodeExe(openclawBinPath);
const packageJsonPath = path.join(packageDir, 'package.json');
const entryPath = path.join(packageDir, 'openclaw.mjs');
ensureFileExists(openclawBinPath, 'OpenClaw 入口');
ensureDirExists(packageDir, 'OpenClaw 包目录');
ensureFileExists(packageJsonPath, 'OpenClaw package.json');
ensureFileExists(entryPath, 'OpenClaw 入口脚本');
ensureFileExists(nodeExePath, 'Node.js 可执行文件');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const destinationPackageDir = path.join(outputDir, 'node_modules', 'openclaw');
const manifest = {
source: {
openclawBinPath,
packageDir,
nodeExePath,
},
stagedAt: new Date().toISOString(),
version: packageJson.version ?? null,
};
log(`OpenClaw version: ${packageJson.version || 'unknown'}`);
log(`Source bin: ${openclawBinPath}`);
log(`Source package: ${packageDir}`);
log(`Source node.exe: ${nodeExePath}`);
log(`Target dir: ${outputDir}`);
if (dryRun) {
log('Dry run 完成,未写入任何文件。');
return;
}
fs.mkdirSync(outputDir, { recursive: true });
cleanOutputDirectory(outputDir);
fs.mkdirSync(path.join(outputDir, 'node_modules'), { recursive: true });
fs.copyFileSync(nodeExePath, path.join(outputDir, 'node.exe'));
fs.cpSync(packageDir, destinationPackageDir, { recursive: true, force: true });
writeCmdLauncher(outputDir);
fs.writeFileSync(path.join(outputDir, 'runtime-manifest.json'), JSON.stringify(manifest, null, 2), 'utf8');
log('OpenClaw runtime 已写入 src-tauri/resources/openclaw-runtime');
}
try {
stageRuntime();
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(`[prepare-openclaw-runtime] ${message}`);
process.exit(1);
}

View File

@@ -0,0 +1,296 @@
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 完成,未写入任何文件。');
}

View File

@@ -0,0 +1,40 @@
import { spawnSync } from 'node:child_process';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const desktopRoot = path.resolve(__dirname, '..');
const forwardArgs = process.argv.slice(2);
function run(command, args, extraEnv = {}) {
const result = spawnSync(command, args, {
cwd: desktopRoot,
stdio: 'inherit',
shell: process.platform === 'win32',
env: {
...process.env,
...extraEnv,
},
});
if (typeof result.status === 'number' && result.status !== 0) {
process.exit(result.status);
}
if (result.error) {
throw result.error;
}
}
const env = {};
if (!process.env.TAURI_BUNDLER_TOOLS_GITHUB_MIRROR && process.env.ZCLAW_TAURI_TOOLS_GITHUB_MIRROR) {
env.TAURI_BUNDLER_TOOLS_GITHUB_MIRROR = process.env.ZCLAW_TAURI_TOOLS_GITHUB_MIRROR;
}
if (!process.env.TAURI_BUNDLER_TOOLS_GITHUB_MIRROR_TEMPLATE && process.env.ZCLAW_TAURI_TOOLS_GITHUB_MIRROR_TEMPLATE) {
env.TAURI_BUNDLER_TOOLS_GITHUB_MIRROR_TEMPLATE = process.env.ZCLAW_TAURI_TOOLS_GITHUB_MIRROR_TEMPLATE;
}
run('node', ['scripts/prepare-openfang-runtime.mjs']);
run('node', ['scripts/preseed-tauri-tools.mjs']);
run('pnpm', ['exec', 'tauri', 'build', ...forwardArgs], env);