feat: prepare Zhinian desktop pilot
This commit is contained in:
@@ -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 ? '✓' : '✗'}`;
|
||||
|
||||
Reference in New Issue
Block a user