feat: prepare Zhinian desktop pilot
Some checks failed
Electron E2E / Electron E2E (macos-latest) (push) Has been cancelled
Electron E2E / Electron E2E (ubuntu-latest) (push) Has been cancelled
Electron E2E / Electron E2E (windows-latest) (push) Has been cancelled

This commit is contained in:
inman
2026-05-07 21:49:20 +08:00
parent cddaf37016
commit 0abc48189c
103 changed files with 10975 additions and 2049 deletions

View File

@@ -21,6 +21,16 @@ import 'zx/globals';
const ROOT = path.resolve(__dirname, '..');
const OUTPUT = path.join(ROOT, 'build', 'openclaw');
const NODE_MODULES = path.join(ROOT, 'node_modules');
const YINIAN_RUNTIME_PATCH_MARKER_FILE = '.yinian-runtime-patch.json';
const YINIAN_RUNTIME_PATCH_MARKER = {
id: 'yinian-openclaw-runtime',
version: '2026-05-07-desktop-fast-chat-v1',
patches: [
'bundled-npm-runner-env',
'optional-native-clipboard-removed',
'desktop-fast-chat-lightweight-tools',
],
};
// On Windows, pnpm virtual store paths can exceed MAX_PATH (260 chars).
function normWin(p) {
@@ -142,6 +152,12 @@ const SKIP_PACKAGES = new Set([
]);
const SKIP_SCOPES = ['@cloudflare/', '@types/'];
let skippedDevCount = 0;
let skippedOptionalNativeCount = 0;
function isOptionalNativeClipboardPackage(name) {
return name === '@mariozechner/clipboard'
|| name.startsWith('@mariozechner/clipboard-');
}
while (queue.length > 0) {
const { nodeModulesDir, skipPkg } = queue.shift();
@@ -151,6 +167,11 @@ while (queue.length > 0) {
// Skip the package that owns this virtual store entry (it's the package itself, not a dep)
if (name === skipPkg) continue;
if (isOptionalNativeClipboardPackage(name)) {
skippedOptionalNativeCount++;
continue;
}
if (SKIP_PACKAGES.has(name) || SKIP_SCOPES.some(s => name.startsWith(s))) {
skippedDevCount++;
continue;
@@ -178,6 +199,9 @@ while (queue.length > 0) {
echo` Found ${collected.size} total packages (direct + transitive)`;
echo` Skipped ${skippedDevCount} dev-only package references`;
if (skippedOptionalNativeCount > 0) {
echo` Skipped ${skippedOptionalNativeCount} optional native clipboard package references`;
}
// 4b. Collect extra packages required by ClawX's Electron main process that are
// NOT deps of openclaw. These are resolved from openclaw's context at runtime
@@ -186,8 +210,113 @@ echo` Skipped ${skippedDevCount} dev-only package references`;
//
// For each package we resolve it from the workspace's own node_modules,
// then BFS its transitive deps exactly like we did for openclaw above.
const OPENCLAW_RUNTIME_DEPS_PACKAGES = [
'@agentclientprotocol/claude-agent-acp',
'@agentclientprotocol/sdk',
'@anthropic-ai/sdk',
'@anthropic-ai/vertex-sdk',
'@aws-sdk/client-bedrock',
'@aws-sdk/client-bedrock-runtime',
'@aws-sdk/client-s3',
'@aws-sdk/credential-provider-node',
'@aws-sdk/s3-request-presigner',
'@aws/bedrock-token-generator',
'@azure/identity',
'@clack/prompts',
'@clawdbot/lobster',
'@discordjs/voice',
'@google/genai',
'@grammyjs/runner',
'@grammyjs/transformer-throttler',
'@homebridge/ciao',
'@lancedb/lancedb',
'@larksuiteoapi/node-sdk',
'@line/bot-sdk',
'@lydell/node-pty',
'@mariozechner/pi-agent-core',
'@mariozechner/pi-ai',
'@mariozechner/pi-coding-agent',
'@matrix-org/matrix-sdk-crypto-nodejs',
'@matrix-org/matrix-sdk-crypto-wasm',
'@microsoft/teams.api',
'@microsoft/teams.apps',
'@modelcontextprotocol/sdk',
'@mozilla/readability',
'@openai/codex',
'@opentelemetry/api',
'@opentelemetry/api-logs',
'@opentelemetry/exporter-logs-otlp-proto',
'@opentelemetry/exporter-metrics-otlp-proto',
'@opentelemetry/exporter-trace-otlp-proto',
'@opentelemetry/resources',
'@opentelemetry/sdk-logs',
'@opentelemetry/sdk-metrics',
'@opentelemetry/sdk-node',
'@opentelemetry/sdk-trace-base',
'@opentelemetry/semantic-conventions',
'@pierre/diffs',
'@pierre/theme',
'@slack/bolt',
'@slack/web-api',
'@tencent-connect/qqbot-connector',
'@tloncorp/tlon-skill',
'@twurple/api',
'@twurple/auth',
'@twurple/chat',
'@urbit/aura',
'@whiskeysockets/baileys',
'@zed-industries/codex-acp',
'acpx',
'ajv',
'chokidar',
'commander',
'croner',
'discord-api-types',
'dotenv',
'express',
'fake-indexeddb',
'gaxios',
'global-agent',
'google-auth-library',
'grammy',
'https-proxy-agent',
'jiti',
'jimp',
'json5',
'jsonwebtoken',
'jszip',
'jwks-rsa',
'linkedom',
'markdown-it',
'matrix-js-sdk',
'minimatch',
'mpg123-decoder',
'music-metadata',
'node-edge-tts',
'nostr-tools',
'openai',
'openshell',
'opusscript',
'pdfjs-dist',
'playwright-core',
'semver',
'sharp',
'silk-wasm',
'sqlite-vec',
'tar',
'tokenjuice',
'tslog',
'typebox',
'undici',
'web-push',
'ws',
'yaml',
'zca-js',
'zod',
];
const EXTRA_BUNDLED_PACKAGES = [
'@whiskeysockets/baileys', // WhatsApp channel (was a dep of old clawdbot, not openclaw)
...OPENCLAW_RUNTIME_DEPS_PACKAGES,
'qrcode-terminal', // QR rendering is loaded from OpenClaw context by channel login flows
];
@@ -215,6 +344,10 @@ for (const pkgName of EXTRA_BUNDLED_PACKAGES) {
const packages = listPackages(nodeModulesDir);
for (const { name, fullPath } of packages) {
if (name === skipPkg) continue;
if (isOptionalNativeClipboardPackage(name)) {
skippedOptionalNativeCount++;
continue;
}
if (SKIP_PACKAGES.has(name) || SKIP_SCOPES.some(s => name.startsWith(s))) continue;
let realPath;
try { realPath = fs.realpathSync(fullPath); } catch { continue; }
@@ -250,6 +383,11 @@ let copiedCount = 0;
let skippedDupes = 0;
for (const [realPath, pkgName] of collected) {
if (isOptionalNativeClipboardPackage(pkgName)) {
skippedOptionalNativeCount++;
continue;
}
if (copiedNames.has(pkgName)) {
skippedDupes++;
continue; // Keep the first version (closer to openclaw in dep tree)
@@ -299,6 +437,10 @@ if (fs.existsSync(extensionsDir)) {
for (const scopeEntry of scopeEntries) {
if (!scopeEntry.isDirectory()) continue;
const scopedName = `${pkgEntry.name}/${scopeEntry.name}`;
if (isOptionalNativeClipboardPackage(scopedName)) {
skippedOptionalNativeCount++;
continue;
}
if (copiedNames.has(scopedName)) continue;
const srcScoped = path.join(srcPkg, scopeEntry.name);
const destScoped = path.join(outputNodeModules, pkgEntry.name, scopeEntry.name);
@@ -310,6 +452,10 @@ if (fs.existsSync(extensionsDir)) {
} catch { /* skip on copy error */ }
}
} else {
if (isOptionalNativeClipboardPackage(pkgEntry.name)) {
skippedOptionalNativeCount++;
continue;
}
if (copiedNames.has(pkgEntry.name)) continue;
const destPkg = path.join(outputNodeModules, pkgEntry.name);
try {
@@ -326,6 +472,31 @@ if (mergedExtCount > 0) {
echo` Merged ${mergedExtCount} extension packages into top-level node_modules`;
}
let forcedRuntimeDepCount = 0;
for (const pkgName of OPENCLAW_RUNTIME_DEPS_PACKAGES) {
if (isOptionalNativeClipboardPackage(pkgName)) continue;
const pkgLink = path.join(NODE_MODULES, ...pkgName.split('/'));
if (!fs.existsSync(pkgLink)) continue;
let pkgReal;
try { pkgReal = fs.realpathSync(pkgLink); } catch { continue; }
const dest = path.join(outputNodeModules, pkgName);
try {
fs.rmSync(normWin(dest), { recursive: true, force: true });
fs.mkdirSync(normWin(path.dirname(dest)), { recursive: true });
fs.cpSync(normWin(pkgReal), normWin(dest), { recursive: true, dereference: true });
copiedNames.add(pkgName);
forcedRuntimeDepCount++;
} catch (err) {
echo` ⚠️ Failed to force-copy runtime dependency ${pkgName}: ${err.message}`;
}
}
if (forcedRuntimeDepCount > 0) {
echo` Forced ${forcedRuntimeDepCount} runtime dependency package(s) to workspace-resolved versions`;
}
// 6. Clean up the bundle to reduce package size
//
// This removes platform-agnostic waste: dev artifacts, docs, source maps,
@@ -480,12 +651,38 @@ function cleanupBundle(outputDir) {
return removedCount;
}
function removeOptionalNativeClipboard(nodeModulesDir) {
const scopeDir = path.join(nodeModulesDir, '@mariozechner');
let removedCount = 0;
let entries = [];
try {
entries = fs.readdirSync(scopeDir, { withFileTypes: true });
} catch {
return 0;
}
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const packageName = `@mariozechner/${entry.name}`;
if (!isOptionalNativeClipboardPackage(packageName)) continue;
if (rmSafe(path.join(scopeDir, entry.name))) {
removedCount++;
}
}
return removedCount;
}
echo``;
echo`🧹 Cleaning up bundle (removing dev artifacts, docs, source maps, type defs)...`;
const sizeBefore = getDirSize(OUTPUT);
const cleanedCount = cleanupBundle(OUTPUT);
const removedClipboardCount = removeOptionalNativeClipboard(outputNodeModules);
const sizeAfter = getDirSize(OUTPUT);
echo` Removed ${cleanedCount} files/directories`;
if (removedClipboardCount > 0) {
echo` Removed ${removedClipboardCount} optional native clipboard package(s)`;
}
echo` Size: ${formatSize(sizeBefore)}${formatSize(sizeAfter)} (saved ${formatSize(sizeBefore - sizeAfter)})`;
// 7. Patch known broken packages
@@ -826,11 +1023,142 @@ function patchBundledRuntime(outputDir) {
if (hintCount > 0) {
echo` 🩹 Patched ${hintCount} browser tool hint(s) to allow transient error retry`;
}
const desktopFastChatPatches = [
{
label: 'desktop chat raw prompt mode',
search: 'const contextInjectionMode = resolveContextInjectionMode(params.config);\n\t\tconst isRawModelRun = params.modelRun === true || params.promptMode === "none";',
replace: 'const contextInjectionMode = resolveContextInjectionMode(params.config);\n\t\tconst yinianDesktopFastModelRun = (!params.messageChannel && !params.messageProvider || params.messageChannel === "webchat" || params.messageProvider === "webchat") && (params.config?.tools?.profile === "messaging" || params.config?.tools?.profile === "minimal");\n\t\tconst isRawModelRun = params.modelRun === true || params.promptMode === "none" || yinianDesktopFastModelRun;',
},
{
label: 'desktop chat skip raw system prompt build',
search: 'const builtAppendPrompt = resolveSystemPromptOverride({',
replace: 'const builtAppendPrompt = isRawModelRun ? "" : resolveSystemPromptOverride({',
},
{
label: 'desktop chat disable tools',
search: 'const toolsRaw = params.disableTools || isRawModelRun ? [] : applyEmbeddedAttemptToolsAllow(createOpenClawCodingTools({',
replace: 'const yinianDesktopFastModelRun = (!params.messageChannel && !params.messageProvider || params.messageChannel === "webchat" || params.messageProvider === "webchat") && (params.config?.tools?.profile === "messaging" || params.config?.tools?.profile === "minimal");\n\t\tconst toolsRaw = params.disableTools || isRawModelRun || yinianDesktopFastModelRun ? [] : applyEmbeddedAttemptToolsAllow(createOpenClawCodingTools({',
},
{
label: 'desktop chat remove duplicate fast path flag',
search: 'const yinianDesktopFastModelRun = (!params.messageChannel && !params.messageProvider || params.messageChannel === "webchat" || params.messageProvider === "webchat") && (params.config?.tools?.profile === "messaging" || params.config?.tools?.profile === "minimal");\n\t\tconst toolsRaw = params.disableTools || isRawModelRun || yinianDesktopFastModelRun ? [] : applyEmbeddedAttemptToolsAllow(createOpenClawCodingTools({',
replace: 'const toolsRaw = params.disableTools || isRawModelRun || yinianDesktopFastModelRun ? [] : applyEmbeddedAttemptToolsAllow(createOpenClawCodingTools({',
},
{
label: 'desktop chat webchat fast path',
search: 'const yinianDesktopFastModelRun = !params.messageChannel && !params.messageProvider && (params.config?.tools?.profile === "messaging" || params.config?.tools?.profile === "minimal");',
replace: 'const yinianDesktopFastModelRun = (!params.messageChannel && !params.messageProvider || params.messageChannel === "webchat" || params.messageProvider === "webchat") && (params.config?.tools?.profile === "messaging" || params.config?.tools?.profile === "minimal");',
},
{
label: 'skip channel agent tools for lightweight profiles',
search: '...listChannelAgentTools({ cfg: options?.config }),',
replace: '...(profile === "messaging" || profile === "minimal" ? [] : listChannelAgentTools({ cfg: options?.config })),',
},
{
label: 'skip plugin tools for lightweight profiles',
search: 'allowGatewaySubagentBinding: options?.allowGatewaySubagentBinding\n\t\t})',
replace: 'allowGatewaySubagentBinding: options?.allowGatewaySubagentBinding,\n\t\t\tdisablePluginTools: options?.disablePluginTools ?? (profile === "messaging" || profile === "minimal")\n\t\t})',
},
];
let desktopFastChatCount = 0;
for (const patch of desktopFastChatPatches) {
let matchedAny = false;
if (fs.existsSync(distDir)) {
for (const file of fs.readdirSync(distDir)) {
if (!file.endsWith('.js')) continue;
const filePath = path.join(distDir, file);
const content = fs.readFileSync(filePath, 'utf8');
if (!content.includes(patch.search)) continue;
matchedAny = true;
const next = content.replace(patch.search, patch.replace);
if (next !== content) {
fs.writeFileSync(filePath, next, 'utf8');
desktopFastChatCount++;
}
}
}
if (!matchedAny) {
echo` ⚠️ Skipped patch for ${patch.label}: expected source snippet not found`;
}
}
if (desktopFastChatCount > 0) {
echo` 🩹 Patched ${desktopFastChatCount} desktop fast chat runtime site(s)`;
}
const runtimeDepsTargets = findFilesByName(
path.join(outputDir, 'dist'),
/^bundled-runtime-deps-.*\.js$/,
);
const npmRunnerSearch = `\tconst execPath = params.execPath ?? process.execPath;
\tconst existsSync = params.existsSync ?? fs.existsSync;
\tconst platform = params.platform ?? process.platform;
\tconst pathImpl = platform === "win32" ? path.win32 : path.posix;`;
const npmRunnerReplace = `\tconst execPath = params.execPath ?? process.execPath;
\tconst existsSync = params.existsSync ?? fs.existsSync;
\tconst platform = params.platform ?? process.platform;
\tconst pathImpl = platform === "win32" ? path.win32 : path.posix;
\tconst env = params.env ?? process.env;
\tconst explicitNodePath = env.YINIAN_NODE_EXEC_PATH;
\tconst explicitNpmCliPath = env.YINIAN_NPM_CLI_PATH;
\tif (explicitNodePath && explicitNpmCliPath && pathImpl.isAbsolute(explicitNodePath) && pathImpl.isAbsolute(explicitNpmCliPath) && existsSync(explicitNodePath) && existsSync(explicitNpmCliPath)) return {
\t\tcommand: explicitNodePath,
\t\targs: [explicitNpmCliPath, ...params.npmArgs]
\t};`;
let npmRunnerCount = 0;
let materializedCheckCount = 0;
for (const target of runtimeDepsTargets) {
const current = fs.readFileSync(target, 'utf8');
let next = current;
if (next.includes(npmRunnerSearch)) {
next = next.replace(npmRunnerSearch, npmRunnerReplace);
npmRunnerCount++;
}
const materializedSearch = `function isRuntimeDepsPlanMaterialized(installRoot, installSpecs) {
\tconst generatedManifestSpecs = readGeneratedInstallManifestSpecs(installRoot);
\tconst packageManifestSpecs = generatedManifestSpecs !== null ? null : readPackageRuntimeDepSpecs(installRoot);
\treturn (generatedManifestSpecs !== null && sameRuntimeDepSpecs(generatedManifestSpecs, installSpecs) || packageManifestSpecs !== null && sameRuntimeDepSpecs(packageManifestSpecs, installSpecs)) && hasSatisfiedInstallSpecPackages(installRoot, installSpecs);
}`;
const materializedReplace = `function isRuntimeDepsPlanMaterialized(installRoot, installSpecs) {
\tconst generatedManifestSpecs = readGeneratedInstallManifestSpecs(installRoot);
\tconst packageManifestSpecs = generatedManifestSpecs !== null ? null : readPackageRuntimeDepSpecs(installRoot);
\treturn (generatedManifestSpecs !== null || packageManifestSpecs !== null) && hasSatisfiedInstallSpecPackages(installRoot, installSpecs);
}`;
if (next.includes(materializedSearch)) {
next = next.replace(materializedSearch, materializedReplace);
materializedCheckCount++;
} else if (next.includes(materializedReplace)) {
materializedCheckCount++;
}
if (next !== current) {
fs.writeFileSync(target, next, 'utf8');
}
}
if (npmRunnerCount > 0) {
echo` 🩹 Patched ${npmRunnerCount} bundled npm runner resolver(s)`;
} else {
echo` ⚠️ Skipped patch for bundled npm runner resolver: expected source snippet not found`;
}
if (materializedCheckCount > 0) {
echo` 🩹 Patched ${materializedCheckCount} bundled runtime dependency materialization check(s)`;
} else {
echo` ⚠️ Skipped patch for bundled runtime dependency materialization check: expected source snippet not found`;
}
}
patchBrokenModules(outputNodeModules);
patchBundledRuntime(OUTPUT);
fs.writeFileSync(
path.join(OUTPUT, YINIAN_RUNTIME_PATCH_MARKER_FILE),
JSON.stringify(YINIAN_RUNTIME_PATCH_MARKER, null, 2) + '\n',
'utf8',
);
echo` 🧭 Wrote Yinian runtime patch marker ${YINIAN_RUNTIME_PATCH_MARKER.version}`;
// 8. Verify the bundle
const entryExists = fs.existsSync(path.join(OUTPUT, 'openclaw.mjs'));
const distExists = fs.existsSync(path.join(OUTPUT, 'dist', 'entry.js'));
@@ -839,6 +1167,7 @@ echo``;
echo`✅ Bundle complete: ${OUTPUT}`;
echo` Unique packages copied: ${copiedCount}`;
echo` Dev-only packages skipped: ${skippedDevCount}`;
echo` Optional native packages skipped: ${skippedOptionalNativeCount}`;
echo` Duplicate versions skipped: ${skippedDupes}`;
echo` Total discovered: ${collected.size}`;
echo` openclaw.mjs: ${entryExists ? '✓' : '✗'}`;