Refine desktop setup and remove bundled app center apps

This commit is contained in:
inman
2026-06-04 09:58:58 +08:00
parent 6153579b90
commit 84128dbe23
73 changed files with 3888 additions and 2024 deletions

View File

@@ -19,8 +19,9 @@
* @mariozechner/clipboard).
*/
const { cpSync, existsSync, readdirSync, rmSync, statSync, mkdirSync, realpathSync } = require('fs');
const { cpSync, existsSync, readdirSync, rmSync, statSync, mkdirSync, realpathSync, readFileSync, writeFileSync } = require('fs');
const { join, dirname, basename, relative } = require('path');
const ts = require('typescript');
// On Windows, paths in pnpm's virtual store can exceed the default MAX_PATH
// limit (260 chars). Node.js 18.17+ respects the system LongPathsEnabled
@@ -280,30 +281,6 @@ function removeOptionalNativeClipboard(nodeModulesDir) {
return removed;
}
function copyNianxxPlayNodeModules(resourcesDir, platform, arch) {
const src = join(__dirname, '..', 'build', 'apps', 'nianxx-play', 'node_modules');
const nianxxPlayRoot = join(resourcesDir, 'resources', 'nianxx-play');
const dest = join(nianxxPlayRoot, 'node_modules');
if (!existsSync(nianxxPlayRoot)) return;
if (!existsSync(src)) {
console.warn('[after-pack] ⚠️ build/apps/nianxx-play/node_modules not found. Run prepare:nianxx-play first.');
return;
}
const depCount = readdirSync(src, { withFileTypes: true })
.filter(d => d.isDirectory() && d.name !== '.bin')
.length;
console.log(`[after-pack] Copying ${depCount} NianxxPlay dependencies to ${dest} ...`);
rmSync(dest, { recursive: true, force: true });
cpSync(src, dest, { recursive: true, dereference: true });
cleanupUnnecessaryFiles(dest);
cleanupKoffi(dest, platform, arch);
cleanupNativePlatformPackages(dest, platform, arch);
console.log('[after-pack] ✅ NianxxPlay node_modules copied.');
}
// ── Broken module patcher ─────────────────────────────────────────────────────
// Some bundled packages have transpiled CJS that sets `module.exports = exports.default`
// without ever assigning `exports.default`, leaving module.exports === undefined.
@@ -488,7 +465,8 @@ function patchPluginIds(pluginDir, expectedId) {
if (!existsSync(pkgJsonPath)) return;
const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8'));
const entryFiles = [pkg.main, pkg.module].filter(Boolean);
const extensionEntries = Array.isArray(pkg.openclaw?.extensions) ? pkg.openclaw.extensions : [];
const entryFiles = [...new Set([pkg.main, pkg.module, ...extensionEntries].filter(Boolean))];
for (const entry of entryFiles) {
const entryPath = join(pluginDir, entry);
@@ -520,6 +498,194 @@ function patchPluginIds(pluginDir, expectedId) {
// bundle-openclaw-plugins.mjs so the packaged app is self-contained even when
// build/openclaw-plugins/ was not pre-generated.
function readJsonFile(filePath) {
return JSON.parse(readFileSync(filePath, 'utf8'));
}
function writeJsonFile(filePath, value) {
writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
}
function normalizeEntryPath(entry) {
if (typeof entry !== 'string') return null;
const trimmed = entry.trim();
if (!trimmed || require('path').isAbsolute(trimmed)) return null;
return trimmed.replace(/^\.\//, '');
}
function toPackageEntry(entry) {
return entry.startsWith('.') ? entry : `./${entry}`;
}
function isJavaScriptEntry(entry) {
return /\.(?:cjs|mjs|js)$/i.test(entry);
}
function entryExists(pluginDir, entry) {
const normalized = normalizeEntryPath(entry);
return Boolean(normalized) && existsSync(join(pluginDir, normalized));
}
function collectRuntimeEntryHints(pkg) {
const hints = [];
const extensions = pkg.openclaw?.extensions;
if (Array.isArray(extensions)) hints.push(...extensions);
if (typeof pkg.main === 'string') hints.push(pkg.main);
if (typeof pkg.module === 'string') hints.push(pkg.module);
hints.push('./dist/index.js', './index.js');
return [...new Set(hints.map(normalizeEntryPath).filter(Boolean))];
}
function findExistingRuntimeEntry(pluginDir, pkg) {
for (const hint of collectRuntimeEntryHints(pkg)) {
if (isJavaScriptEntry(hint) && existsSync(join(pluginDir, hint))) {
return hint;
}
}
return null;
}
function patchRuntimeEntryHints(pluginDir) {
const pkgJsonPath = join(pluginDir, 'package.json');
if (!existsSync(pkgJsonPath)) return null;
const pkg = readJsonFile(pkgJsonPath);
let modified = false;
const extensions = pkg.openclaw?.extensions;
if (Array.isArray(extensions)) {
const patchedExtensions = extensions.map((entry) => {
const normalized = normalizeEntryPath(entry);
if (!normalized?.endsWith('.ts')) return entry;
const jsEntry = `dist/${normalized.replace(/\.ts$/i, '.js')}`;
return existsSync(join(pluginDir, jsEntry)) ? toPackageEntry(jsEntry) : entry;
});
if (JSON.stringify(patchedExtensions) !== JSON.stringify(extensions)) {
pkg.openclaw.extensions = patchedExtensions;
modified = true;
}
}
const existingRuntimeEntry = findExistingRuntimeEntry(pluginDir, pkg);
if (existingRuntimeEntry) {
if (typeof pkg.main !== 'string' || !entryExists(pluginDir, pkg.main)) {
pkg.main = toPackageEntry(existingRuntimeEntry);
modified = true;
}
if (typeof pkg.module === 'string' && !entryExists(pluginDir, pkg.module)) {
pkg.module = toPackageEntry(existingRuntimeEntry);
modified = true;
}
}
if (modified) {
writeJsonFile(pkgJsonPath, pkg);
}
return existingRuntimeEntry;
}
function patchManifestChannelConfigs(pluginDir) {
const manifestPath = join(pluginDir, 'openclaw.plugin.json');
if (!existsSync(manifestPath)) return;
const manifest = readJsonFile(manifestPath);
if (manifest.channelConfigs || !Array.isArray(manifest.channels)) return;
const schema = { type: 'object' };
manifest.channelConfigs = Object.fromEntries(
manifest.channels
.filter((channelId) => typeof channelId === 'string' && channelId.trim().length > 0)
.map((channelId) => [channelId, { schema }]),
);
writeJsonFile(manifestPath, manifest);
}
function collectTypeScriptFiles(pluginDir) {
const result = [];
const skipDirs = new Set(['node_modules', 'dist', '.git']);
function walk(currentDir) {
for (const entry of readdirSync(currentDir, { withFileTypes: true })) {
if (skipDirs.has(entry.name)) continue;
const fullPath = join(currentDir, entry.name);
if (entry.isDirectory()) {
walk(fullPath);
continue;
}
if (!entry.isFile()) continue;
if (!entry.name.endsWith('.ts')) continue;
if (entry.name.endsWith('.d.ts') || entry.name.endsWith('.test.ts')) continue;
result.push(fullPath);
}
}
walk(pluginDir);
return result;
}
function compileTypeScriptPluginIfNeeded(pluginDir, pluginId) {
const pkgJsonPath = join(pluginDir, 'package.json');
if (!existsSync(pkgJsonPath)) return;
const pkg = readJsonFile(pkgJsonPath);
const extensionEntries = Array.isArray(pkg.openclaw?.extensions) ? pkg.openclaw.extensions : [];
const hasTypeScriptEntry = extensionEntries.some((entry) => normalizeEntryPath(entry)?.endsWith('.ts'));
if (!hasTypeScriptEntry) {
const runtimeEntry = patchRuntimeEntryHints(pluginDir);
if (runtimeEntry) {
console.log(`[after-pack] Runtime entry: ${runtimeEntry}`);
}
return;
}
const tsFiles = collectTypeScriptFiles(pluginDir);
if (tsFiles.length === 0) {
throw new Error(`Plugin ${pluginId} declares TypeScript entries but no .ts source files were found.`);
}
const distDir = join(pluginDir, 'dist');
rmSync(distDir, { recursive: true, force: true });
for (const sourcePath of tsFiles) {
const source = readFileSync(sourcePath, 'utf8');
const output = ts.transpileModule(source, {
compilerOptions: {
target: ts.ScriptTarget.ES2022,
module: ts.ModuleKind.ES2022,
esModuleInterop: true,
importsNotUsedAsValues: ts.ImportsNotUsedAsValues.Remove,
sourceMap: false,
inlineSources: false,
},
fileName: sourcePath,
reportDiagnostics: true,
});
const diagnostics = output.diagnostics ?? [];
const blocking = diagnostics.filter((diag) => diag.category === ts.DiagnosticCategory.Error);
if (blocking.length > 0) {
const message = blocking
.map((diag) => ts.flattenDiagnosticMessageText(diag.messageText, '\n'))
.join('\n');
throw new Error(`Failed to transpile ${relative(pluginDir, sourcePath)}:\n${message}`);
}
const rel = relative(pluginDir, sourcePath).replace(/\.ts$/i, '.js');
const outputPath = join(distDir, rel);
mkdirSync(dirname(outputPath), { recursive: true });
writeFileSync(outputPath, output.outputText, 'utf8');
}
patchRuntimeEntryHints(pluginDir);
const runtimeEntry = findExistingRuntimeEntry(pluginDir, readJsonFile(pkgJsonPath));
if (!runtimeEntry) {
throw new Error(`Plugin ${pluginId} did not produce a loadable JavaScript runtime entry.`);
}
console.log(`[after-pack] Compiled ${tsFiles.length} TypeScript files -> dist/ (${runtimeEntry})`);
}
function getVirtualStoreNodeModules(realPkgPath) {
let dir = realPkgPath;
while (dir !== dirname(dir)) {
@@ -670,10 +836,6 @@ exports.default = async function afterPack(context) {
console.log(`[after-pack] ✅ Removed optional native clipboard packages (${clipboardRemoved}) to avoid macOS Gatekeeper prompts.`);
}
// 1.0 Copy bundled large-app runtime deps that electron-builder skips because
// node_modules/ is ignored globally.
copyNianxxPlayNodeModules(resourcesDir, platform, arch);
// Patch broken modules whose CJS transpiled output sets module.exports = undefined,
// causing TypeError in Node.js 22+ ESM interop.
patchBrokenModules(dest);
@@ -685,6 +847,7 @@ exports.default = async function afterPack(context) {
// - node_modules/ is excluded by .gitignore so the deps copy must be manual
const BUNDLED_PLUGINS = [
{ npmName: '@tencent-weixin/openclaw-weixin', pluginId: 'openclaw-weixin' },
{ npmName: '@larksuite/openclaw-lark', pluginId: 'openclaw-lark' },
];
mkdirSync(pluginsDestRoot, { recursive: true });
@@ -700,6 +863,8 @@ exports.default = async function afterPack(context) {
cleanupNativePlatformPackages(pluginNM, platform, arch);
}
// Fix hardcoded plugin ID mismatches in compiled JS
patchManifestChannelConfigs(pluginDestDir);
compileTypeScriptPluginIfNeeded(pluginDestDir, pluginId);
patchPluginIds(pluginDestDir, pluginId);
}
}
@@ -715,6 +880,7 @@ exports.default = async function afterPack(context) {
rmSync(pluginDestDir, { recursive: true, force: true });
cpSync(sourceDir, pluginDestDir, { recursive: true, dereference: true });
cleanupUnnecessaryFiles(pluginDestDir);
patchManifestChannelConfigs(pluginDestDir);
patchPluginIds(pluginDestDir, entry.name);
}
}

View File

@@ -6,9 +6,10 @@
* Build a self-contained mirror of OpenClaw third-party plugins for packaging.
* Current plugins:
* - @tencent-weixin/openclaw-weixin -> build/openclaw-plugins/openclaw-weixin
* - @larksuite/openclaw-lark -> build/openclaw-plugins/openclaw-lark
*
* The output plugin directory contains:
* - plugin source files (index.ts, openclaw.plugin.json, package.json, ...)
* - plugin runtime files (dist/index.js or index.js, openclaw.plugin.json, package.json, ...)
* - plugin runtime node_modules/ (flattened direct + transitive deps)
*/
@@ -16,6 +17,7 @@ import 'zx/globals';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import ts from 'typescript';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, '..');
@@ -36,8 +38,197 @@ function normWin(p) {
const PLUGINS = [
{ npmName: '@tencent-weixin/openclaw-weixin', pluginId: 'openclaw-weixin' },
{ npmName: '@larksuite/openclaw-lark', pluginId: 'openclaw-lark' },
];
function readJsonFile(filePath) {
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
}
function writeJsonFile(filePath, value) {
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
}
function normalizeEntryPath(entry) {
if (typeof entry !== 'string') return null;
const trimmed = entry.trim();
if (!trimmed || path.isAbsolute(trimmed)) return null;
return trimmed.replace(/^\.\//, '');
}
function toPackageEntry(entry) {
return entry.startsWith('.') ? entry : `./${entry}`;
}
function isJavaScriptEntry(entry) {
return /\.(?:cjs|mjs|js)$/i.test(entry);
}
function entryExists(pluginDir, entry) {
const normalized = normalizeEntryPath(entry);
return Boolean(normalized) && fs.existsSync(path.join(pluginDir, normalized));
}
function collectRuntimeEntryHints(pkg) {
const hints = [];
const extensions = pkg.openclaw?.extensions;
if (Array.isArray(extensions)) hints.push(...extensions);
if (typeof pkg.main === 'string') hints.push(pkg.main);
if (typeof pkg.module === 'string') hints.push(pkg.module);
hints.push('./dist/index.js', './index.js');
return [...new Set(hints.map(normalizeEntryPath).filter(Boolean))];
}
function findExistingRuntimeEntry(pluginDir, pkg) {
for (const hint of collectRuntimeEntryHints(pkg)) {
if (isJavaScriptEntry(hint) && fs.existsSync(path.join(pluginDir, hint))) {
return hint;
}
}
return null;
}
function patchRuntimeEntryHints(pluginDir) {
const pkgJsonPath = path.join(pluginDir, 'package.json');
if (!fs.existsSync(pkgJsonPath)) return null;
const pkg = readJsonFile(pkgJsonPath);
let modified = false;
const extensions = pkg.openclaw?.extensions;
if (Array.isArray(extensions)) {
const patchedExtensions = extensions.map((entry) => {
const normalized = normalizeEntryPath(entry);
if (!normalized?.endsWith('.ts')) return entry;
const jsEntry = `dist/${normalized.replace(/\.ts$/i, '.js')}`;
return fs.existsSync(path.join(pluginDir, jsEntry)) ? toPackageEntry(jsEntry) : entry;
});
if (JSON.stringify(patchedExtensions) !== JSON.stringify(extensions)) {
pkg.openclaw.extensions = patchedExtensions;
modified = true;
}
}
const existingRuntimeEntry = findExistingRuntimeEntry(pluginDir, pkg);
if (existingRuntimeEntry) {
if (typeof pkg.main !== 'string' || !entryExists(pluginDir, pkg.main)) {
pkg.main = toPackageEntry(existingRuntimeEntry);
modified = true;
}
if (typeof pkg.module === 'string' && !entryExists(pluginDir, pkg.module)) {
pkg.module = toPackageEntry(existingRuntimeEntry);
modified = true;
}
}
if (modified) {
writeJsonFile(pkgJsonPath, pkg);
}
return existingRuntimeEntry;
}
function patchManifestChannelConfigs(pluginDir) {
const manifestPath = path.join(pluginDir, 'openclaw.plugin.json');
if (!fs.existsSync(manifestPath)) return;
const manifest = readJsonFile(manifestPath);
if (manifest.channelConfigs || !Array.isArray(manifest.channels)) return;
const schema = { type: 'object' };
manifest.channelConfigs = Object.fromEntries(
manifest.channels
.filter((channelId) => typeof channelId === 'string' && channelId.trim().length > 0)
.map((channelId) => [channelId, { schema }]),
);
writeJsonFile(manifestPath, manifest);
}
function collectTypeScriptFiles(pluginDir) {
const result = [];
const skipDirs = new Set(['node_modules', 'dist', '.git']);
function walk(currentDir) {
for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
if (skipDirs.has(entry.name)) continue;
const fullPath = path.join(currentDir, entry.name);
if (entry.isDirectory()) {
walk(fullPath);
continue;
}
if (!entry.isFile()) continue;
if (!entry.name.endsWith('.ts')) continue;
if (entry.name.endsWith('.d.ts') || entry.name.endsWith('.test.ts')) continue;
result.push(fullPath);
}
}
walk(pluginDir);
return result;
}
function compileTypeScriptPluginIfNeeded(pluginDir, pluginId) {
const pkgJsonPath = path.join(pluginDir, 'package.json');
if (!fs.existsSync(pkgJsonPath)) return;
const pkg = readJsonFile(pkgJsonPath);
const extensionEntries = Array.isArray(pkg.openclaw?.extensions) ? pkg.openclaw.extensions : [];
const hasTypeScriptEntry = extensionEntries.some((entry) => normalizeEntryPath(entry)?.endsWith('.ts'));
if (!hasTypeScriptEntry) {
const runtimeEntry = patchRuntimeEntryHints(pluginDir);
if (runtimeEntry) {
echo` 🔗 Runtime entry: ${runtimeEntry}`;
}
return;
}
const tsFiles = collectTypeScriptFiles(pluginDir);
if (tsFiles.length === 0) {
throw new Error(`Plugin ${pluginId} declares TypeScript entries but no .ts source files were found.`);
}
const distDir = path.join(pluginDir, 'dist');
fs.rmSync(distDir, { recursive: true, force: true });
for (const sourcePath of tsFiles) {
const source = fs.readFileSync(sourcePath, 'utf8');
const output = ts.transpileModule(source, {
compilerOptions: {
target: ts.ScriptTarget.ES2022,
module: ts.ModuleKind.ES2022,
esModuleInterop: true,
importsNotUsedAsValues: ts.ImportsNotUsedAsValues.Remove,
sourceMap: false,
inlineSources: false,
},
fileName: sourcePath,
reportDiagnostics: true,
});
const diagnostics = output.diagnostics ?? [];
const blocking = diagnostics.filter((diag) => diag.category === ts.DiagnosticCategory.Error);
if (blocking.length > 0) {
const message = blocking
.map((diag) => ts.flattenDiagnosticMessageText(diag.messageText, '\n'))
.join('\n');
throw new Error(`Failed to transpile ${path.relative(pluginDir, sourcePath)}:\n${message}`);
}
const rel = path.relative(pluginDir, sourcePath).replace(/\.ts$/i, '.js');
const outputPath = path.join(distDir, rel);
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, output.outputText, 'utf8');
}
patchRuntimeEntryHints(pluginDir);
const runtimeEntry = findExistingRuntimeEntry(pluginDir, readJsonFile(pkgJsonPath));
if (!runtimeEntry) {
throw new Error(`Plugin ${pluginId} did not produce a loadable JavaScript runtime entry.`);
}
echo` 🛠️ Compiled ${tsFiles.length} TypeScript files -> dist/ (${runtimeEntry})`;
}
function getVirtualStoreNodeModules(realPkgPath) {
let dir = realPkgPath;
while (dir !== path.dirname(dir)) {
@@ -171,6 +362,8 @@ function bundleOnePlugin({ npmName, pluginId }) {
// 4) Patch plugin ID mismatch: some npm packages hardcode a different ID in
// their JS output than what openclaw.plugin.json declares. The Gateway
// validates that these match, so we fix it post-copy.
patchManifestChannelConfigs(outputDir);
compileTypeScriptPluginIfNeeded(outputDir, pluginId);
patchPluginId(outputDir, pluginId);
echo`${pluginId}: copied ${copiedCount} deps (skipped dupes: ${skippedDupes})`;
@@ -196,7 +389,8 @@ function patchPluginId(pluginDir, expectedId) {
if (!fs.existsSync(pkgJsonPath)) return;
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
const entryFiles = [pkg.main, pkg.module].filter(Boolean);
const extensionEntries = Array.isArray(pkg.openclaw?.extensions) ? pkg.openclaw.extensions : [];
const entryFiles = [...new Set([pkg.main, pkg.module, ...extensionEntries].filter(Boolean))];
// Known ID mismatches to patch. Keys are the wrong ID found in compiled JS,
// values are the correct ID (must match openclaw.plugin.json).

View File

@@ -1315,7 +1315,7 @@ echo` 🧭 Wrote Yinian runtime patch marker ${YINIAN_RUNTIME_PATCH_MARKER.ver
// 8. Verify the bundle
const entryExists = fs.existsSync(path.join(OUTPUT, 'openclaw.mjs'));
const distExists = fs.existsSync(path.join(OUTPUT, 'dist', 'entry.js'));
const requiredTemplateFiles = ['AGENTS.md', 'TOOLS.md', 'HEARTBEAT.md'];
const requiredTemplateFiles = ['SOUL.md', 'IDENTITY.md', 'USER.md', 'AGENTS.md', 'TOOLS.md', 'HEARTBEAT.md', 'BOOT.md'];
const missingTemplateFiles = requiredTemplateFiles.filter((fileName) => (
!fs.existsSync(path.join(OUTPUT, 'docs', 'reference', 'templates', fileName))
));

View File

@@ -13,7 +13,18 @@ const SHOULD_BUNDLE = process.env.YINIAN_BUNDLE_MODEL_AUTH === '1';
const SOURCE_AUTH_PROFILES = process.env.YINIAN_MODEL_AUTH_SOURCE
? resolve(process.env.YINIAN_MODEL_AUTH_SOURCE)
: join(homedir(), '.openclaw', 'agents', 'main', 'agent', 'auth-profiles.json');
const PROFILE_IDS = ['minimax:default', 'minimax:cn'];
const MODEL_PROVIDER_KEY = (process.env.YINIAN_MODEL_PROVIDER_KEY || 'yinian-model').trim();
const MODEL_ID = (process.env.YINIAN_MODEL_ID || '').trim();
const MODEL_NAME = (process.env.YINIAN_MODEL_NAME || MODEL_ID).trim();
const MODEL_BASE_URL = (process.env.YINIAN_MODEL_BASE_URL || '').trim();
const MODEL_API = (process.env.YINIAN_MODEL_API || 'openai-completions').trim();
const MODEL_AUTH_PROFILE_ID = (process.env.YINIAN_MODEL_AUTH_PROFILE_ID || `${MODEL_PROVIDER_KEY}:default`).trim();
const SOURCE_PROFILE_ID = (process.env.YINIAN_MODEL_AUTH_SOURCE_PROFILE_ID || '').trim();
const MODEL_FALLBACKS = (process.env.YINIAN_MODEL_FALLBACKS || '')
.split(',')
.map((value) => value.trim())
.filter(Boolean);
const LEGACY_SOURCE_PROFILE_IDS = ['minimax:default', 'minimax:cn'];
function log(message) {
console.log(`[yinian-model-auth] ${message}`);
@@ -44,7 +55,7 @@ if (!SHOULD_BUNDLE) {
bundled: false,
reason: 'YINIAN_BUNDLE_MODEL_AUTH is not enabled',
});
log('pilot model auth bundling disabled.');
log('pilot model auth bundling disabled. Customer pilot installers must use package:pilot or set YINIAN_BUNDLE_MODEL_AUTH=1.');
process.exit(0);
}
@@ -52,26 +63,54 @@ if (!existsSync(SOURCE_AUTH_PROFILES)) {
fail(`source auth profiles not found: ${SOURCE_AUTH_PROFILES}`);
}
if (!MODEL_PROVIDER_KEY) {
fail('YINIAN_MODEL_PROVIDER_KEY is required for model auth bundling.');
}
if (!MODEL_ID) {
fail('YINIAN_MODEL_ID is required for model auth bundling.');
}
if (!MODEL_BASE_URL) {
fail('YINIAN_MODEL_BASE_URL is required for model auth bundling.');
}
const source = readJson(SOURCE_AUTH_PROFILES);
const sourceProfiles = source && typeof source === 'object' && source.profiles && typeof source.profiles === 'object'
? source.profiles
: {};
const profiles = {};
const sourceProfileIds = [
SOURCE_PROFILE_ID,
MODEL_AUTH_PROFILE_ID,
`${MODEL_PROVIDER_KEY}:default`,
...LEGACY_SOURCE_PROFILE_IDS,
].filter(Boolean);
const candidateProfileIds = Array.from(new Set([
...sourceProfileIds,
...Object.keys(sourceProfiles),
]));
let bundledKey = '';
for (const profileId of PROFILE_IDS) {
for (const profileId of candidateProfileIds) {
const profile = sourceProfiles[profileId];
if (!profile || typeof profile !== 'object') continue;
if (profile.type !== 'api_key' || profile.provider !== 'minimax') continue;
if (profile.type !== 'api_key') continue;
if (typeof profile.key !== 'string' || profile.key.trim().length < 8) continue;
profiles[profileId] = {
bundledKey = profile.key.trim();
break;
}
if (bundledKey) {
profiles[MODEL_AUTH_PROFILE_ID] = {
type: 'api_key',
provider: 'minimax',
key: profile.key,
provider: MODEL_PROVIDER_KEY,
key: bundledKey,
};
}
if (!profiles['minimax:default']) {
fail('minimax:default API key profile is required for pilot model auth bundling.');
if (!profiles[MODEL_AUTH_PROFILE_ID]) {
fail(`API key profile is required for pilot model auth bundling. Set YINIAN_MODEL_AUTH_SOURCE_PROFILE_ID or create ${MODEL_AUTH_PROFILE_ID}.`);
}
writeManifest({
@@ -79,14 +118,23 @@ writeManifest({
purpose: 'internal-pilot-only',
source: 'local-openclaw-auth-profiles',
profileIds: Object.keys(profiles),
model: {
providerKey: MODEL_PROVIDER_KEY,
modelId: MODEL_ID,
modelName: MODEL_NAME || MODEL_ID,
baseUrl: MODEL_BASE_URL,
api: MODEL_API,
authProfileId: MODEL_AUTH_PROFILE_ID,
fallbackModelRefs: MODEL_FALLBACKS,
},
store: {
version: 1,
profiles,
order: {
minimax: Object.keys(profiles),
[MODEL_PROVIDER_KEY]: Object.keys(profiles),
},
lastGood: {
minimax: 'minimax:default',
[MODEL_PROVIDER_KEY]: MODEL_AUTH_PROFILE_ID,
},
},
});

View File

@@ -1,341 +0,0 @@
#!/usr/bin/env node
import { spawnSync } from 'node:child_process';
import {
chmodSync,
cpSync,
existsSync,
mkdirSync,
readdirSync,
readFileSync,
rmSync,
statSync,
writeFileSync,
} from 'node:fs';
import { basename, dirname, join, resolve, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = resolve(__dirname, '..');
const DEFAULT_SOURCE = resolve(ROOT, '..', '..', 'NianxxPlay');
const SOURCE_DIR = resolve(process.env.NIANXX_PLAY_DIR || DEFAULT_SOURCE);
const OUTPUT_DIR = resolve(ROOT, 'build', 'apps', 'nianxx-play');
const BUNDLE_RUNTIME_ENV = process.env.NIANXX_PLAY_BUNDLE_ENV === '1';
const RUNTIME_ENV_FILE_NAME = '.env.runtime';
function log(message) {
console.log(`[nianxx-play-bundle] ${message}`);
}
function fail(message) {
console.error(`[nianxx-play-bundle] ${message}`);
process.exit(1);
}
function run(command, args, cwd) {
const result = spawnSync(command, args, {
cwd,
env: {
...process.env,
NEXT_TELEMETRY_DISABLED: '1',
},
stdio: 'inherit',
shell: process.platform === 'win32',
});
if (result.status !== 0) {
fail(`${command} ${args.join(' ')} failed with exit code ${result.status ?? 'unknown'}`);
}
}
function readPackageVersion(packageJsonPath) {
try {
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
return typeof pkg.version === 'string' ? pkg.version : '0.0.0';
} catch {
return '0.0.0';
}
}
function parseEnvFile(envPath) {
if (!existsSync(envPath)) return [];
const entries = [];
const raw = readFileSync(envPath, 'utf8');
for (const line of raw.split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const match = trimmed.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
if (!match) continue;
const key = match[1];
let value = match[2].trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
entries.push({ key, value });
}
return entries;
}
function selectRuntimeEnvFile(sourceDir) {
const explicit = process.env.NIANXX_PLAY_ENV_FILE?.trim();
if (explicit) {
const resolved = resolve(explicit);
return existsSync(resolved) ? resolved : undefined;
}
for (const fileName of ['.env.local', '.env.production.local', '.env.production', '.env']) {
const candidate = join(sourceDir, fileName);
if (existsSync(candidate)) return candidate;
}
return undefined;
}
function encodeEnvValue(value) {
return JSON.stringify(value);
}
function writeRuntimeEnvFile(sourceDir, outputDir) {
if (!BUNDLE_RUNTIME_ENV) return { bundled: false, values: 0 };
const envFile = selectRuntimeEnvFile(sourceDir);
if (!envFile) {
fail('NIANXX_PLAY_BUNDLE_ENV=1 set, but no NianxxPlay env file was found.');
}
const entries = parseEnvFile(envFile);
if (!entries.length) {
fail(`NIANXX_PLAY_BUNDLE_ENV=1 set, but env file has no usable entries: ${envFile}`);
}
const runtimeEnvPath = join(outputDir, RUNTIME_ENV_FILE_NAME);
const text = [
'# Bundled only for internal testing. Do not use in production builds.',
...entries.map((entry) => `${entry.key}=${encodeEnvValue(entry.value)}`),
'',
].join('\n');
writeFileSync(runtimeEnvPath, text, 'utf8');
return { bundled: true, values: entries.length };
}
function collectSecretLikeEnvValues(sourceDir) {
const envFileNames = [
'.env',
'.env.local',
'.env.production',
'.env.production.local',
];
const sensitiveKeyPattern = /(SECRET|TOKEN|PASSWORD|PRIVATE|AUTH|API_KEY|ACCESS_KEY|KEY_ID|KEY_SECRET|SERVICE_ROLE)/i;
const ignoredValues = new Set(['true', 'false', 'null', 'undefined', 'development', 'production']);
const values = [];
for (const fileName of envFileNames) {
for (const entry of parseEnvFile(join(sourceDir, fileName))) {
if (!sensitiveKeyPattern.test(entry.key)) continue;
if (!entry.value || entry.value.length < 8) continue;
if (ignoredValues.has(entry.value.toLowerCase())) continue;
values.push(entry);
}
}
return values;
}
function shouldCopyPublic(src) {
const name = basename(src);
if (name === 'uploads') return false;
const publicRelative = relative(publicDir, src).split('\\').join('/');
if (publicRelative === 'generated-results' || publicRelative.startsWith('generated-results/')) return false;
if (name.startsWith('.env')) return false;
return true;
}
function shouldCopyRuntime(src) {
const name = basename(src);
if (name.startsWith('.env')) return false;
if (name === '.data' || name === '.git' || name === '.next-cache') return false;
const runtimeRelative = relative(standaloneDir, src).split('\\').join('/');
if (runtimeRelative === 'public/uploads' || runtimeRelative.startsWith('public/uploads/')) return false;
if (runtimeRelative === 'public/generated-results' || runtimeRelative.startsWith('public/generated-results/')) return false;
if (runtimeRelative === 'uploads' || runtimeRelative.startsWith('uploads/')) return false;
if (runtimeRelative === 'generated-results' || runtimeRelative.startsWith('generated-results/')) return false;
return true;
}
function copyDir(from, to, filter = () => true) {
if (!existsSync(from)) return false;
mkdirSync(dirname(to), { recursive: true });
cpSync(from, to, {
recursive: true,
dereference: true,
filter,
});
return true;
}
function normalizeBundlePermissions(dir) {
if (!existsSync(dir)) return;
const stats = statSync(dir);
try {
if (stats.isDirectory()) {
chmodSync(dir, 0o755);
for (const entry of readdirSync(dir)) {
normalizeBundlePermissions(join(dir, entry));
}
return;
}
if (stats.isFile()) {
const executable = (stats.mode & 0o111) !== 0;
chmodSync(dir, executable ? 0o755 : 0o644);
}
} catch (error) {
fail(`Unable to normalize bundle permissions for ${relative(ROOT, dir)}: ${error.message}`);
}
}
function dirSizeBytes(dir) {
if (!existsSync(dir)) return 0;
const stats = statSync(dir);
if (stats.isFile()) return stats.size;
let size = 0;
for (const entry of readdirSync(dir)) {
size += dirSizeBytes(join(dir, entry));
}
return size;
}
function assertNoEnvFiles(dir) {
const allowedRuntimeEnvPath = BUNDLE_RUNTIME_ENV
? join(OUTPUT_DIR, RUNTIME_ENV_FILE_NAME)
: undefined;
const stack = [dir];
while (stack.length) {
const current = stack.pop();
for (const entry of readdirSync(current)) {
const fullPath = join(current, entry);
if (allowedRuntimeEnvPath && fullPath === allowedRuntimeEnvPath) {
continue;
}
if (basename(entry).startsWith('.env')) {
fail(`Refusing to ship env file: ${relative(ROOT, fullPath)}`);
}
if (statSync(fullPath).isDirectory()) stack.push(fullPath);
}
}
}
function assertNoForbiddenBundlePaths(outputDir) {
const forbidden = [
join(outputDir, 'public', 'uploads'),
join(outputDir, 'public', 'generated-results'),
join(outputDir, '.next', 'cache'),
];
for (const target of forbidden) {
if (existsSync(target)) {
fail(`Refusing to ship development/user data path: ${relative(ROOT, target)}`);
}
}
}
function shouldScanForSecrets(filePath) {
const stats = statSync(filePath);
if (!stats.isFile()) return false;
if (stats.size > 5 * 1024 * 1024) return false;
return /\.(?:js|json|html|css|txt|mjs|cjs|map)$/i.test(filePath);
}
function assertNoSecretValues(dir, secretEntries) {
if (!secretEntries.length) return;
const stack = [dir];
while (stack.length) {
const current = stack.pop();
for (const entry of readdirSync(current)) {
const fullPath = join(current, entry);
const stats = statSync(fullPath);
if (stats.isDirectory()) {
stack.push(fullPath);
continue;
}
if (BUNDLE_RUNTIME_ENV && fullPath === join(OUTPUT_DIR, RUNTIME_ENV_FILE_NAME)) continue;
if (!shouldScanForSecrets(fullPath)) continue;
const content = readFileSync(fullPath, 'utf8');
for (const secret of secretEntries) {
if (content.includes(secret.value)) {
fail(`Refusing to ship bundle: secret-like env value "${secret.key}" appears in ${relative(ROOT, fullPath)}`);
}
}
}
}
}
if (process.env.SKIP_NIANXX_PLAY_BUNDLE === '1') {
log('SKIP_NIANXX_PLAY_BUNDLE=1 set, skipping.');
process.exit(0);
}
if (!existsSync(join(SOURCE_DIR, 'package.json'))) {
fail(`NianxxPlay source not found: ${SOURCE_DIR}`);
}
const sourceVersion = readPackageVersion(join(SOURCE_DIR, 'package.json'));
const secretLikeEnvValues = collectSecretLikeEnvValues(SOURCE_DIR);
log(`source: ${SOURCE_DIR}`);
log(`version: ${sourceVersion}`);
if (process.env.NIANXX_PLAY_SKIP_BUILD !== '1') {
log('building Next.js standalone runtime...');
run(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', 'build'], SOURCE_DIR);
} else {
log('NIANXX_PLAY_SKIP_BUILD=1 set, reusing existing .next output.');
}
const standaloneDir = join(SOURCE_DIR, '.next', 'standalone');
const staticDir = join(SOURCE_DIR, '.next', 'static');
const publicDir = join(SOURCE_DIR, 'public');
const contentDir = join(SOURCE_DIR, 'content');
if (!existsSync(join(standaloneDir, 'server.js'))) {
fail(`Missing Next.js standalone server: ${join(standaloneDir, 'server.js')}`);
}
if (!existsSync(staticDir)) {
fail(`Missing Next.js static output: ${staticDir}`);
}
rmSync(OUTPUT_DIR, { recursive: true, force: true });
mkdirSync(OUTPUT_DIR, { recursive: true });
log(`copying standalone runtime -> ${OUTPUT_DIR}`);
copyDir(standaloneDir, OUTPUT_DIR, shouldCopyRuntime);
copyDir(staticDir, join(OUTPUT_DIR, '.next', 'static'), shouldCopyRuntime);
copyDir(publicDir, join(OUTPUT_DIR, 'public'), shouldCopyPublic);
copyDir(contentDir, join(OUTPUT_DIR, 'content'), shouldCopyRuntime);
const runtimeEnv = writeRuntimeEnvFile(SOURCE_DIR, OUTPUT_DIR);
normalizeBundlePermissions(OUTPUT_DIR);
assertNoEnvFiles(OUTPUT_DIR);
assertNoForbiddenBundlePaths(OUTPUT_DIR);
assertNoSecretValues(OUTPUT_DIR, secretLikeEnvValues);
const manifest = {
appId: 'nianxx-play',
name: 'NianxxPlay',
version: sourceVersion,
bundledAt: new Date().toISOString(),
runtime: 'next-standalone',
entry: 'server.js',
excludes: ['.env*', '.data', 'public/uploads', 'public/generated-results', 'development caches'],
secretScan: {
checked: true,
sourceEnvValues: secretLikeEnvValues.length,
},
runtimeEnv: runtimeEnv.bundled
? {
bundled: true,
file: RUNTIME_ENV_FILE_NAME,
values: runtimeEnv.values,
purpose: 'internal-testing-only',
}
: {
bundled: false,
},
sizeBytes: dirSizeBytes(OUTPUT_DIR),
};
writeFileSync(join(OUTPUT_DIR, 'bundle-manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
log(`ready: ${OUTPUT_DIR}`);
log(`size: ${(manifest.sizeBytes / 1024 / 1024).toFixed(1)} MB`);