feat: enhance after-pack script to copy OpenClaw runtime dependencies

- Added a new script `bundle-openclaw.mjs` to bundle OpenClaw runtime dependencies.
- Updated `after-pack.cjs` to copy bundled OpenClaw runtime and its node_modules.
- Improved cleanup of unnecessary development files in node_modules.
- Adjusted paths for resources in the packaging process.

style: update loading indicator styles in ChatHistoryPanel

- Changed the border radius and padding for the loading indicator in ChatHistoryPanel.

fix: improve ProvidersSection to handle provider account syncing

- Added logic to sync model configuration to provider accounts.
- Introduced error handling and loading states during the sync process.
- Enhanced vendor resolution and account management logic.

fix: fallback session handling in chat store

- Implemented fallback session logic in loadSessions to ensure a valid session is always available on error.
This commit is contained in:
duanshuwen
2026-04-22 21:56:37 +08:00
parent 2f675afe47
commit ea1fd18e6f
22 changed files with 8947 additions and 94 deletions

View File

@@ -0,0 +1,172 @@
import crypto from 'crypto';
import { access, chmod, constants, mkdir, readFile, writeFile } from 'fs/promises';
import path from 'path';
export interface DeviceIdentity {
deviceId: string;
publicKeyPem: string;
privateKeyPem: string;
}
export interface DeviceAuthPayloadParams {
deviceId: string;
clientId: string;
clientMode: string;
role: string;
scopes: string[];
signedAtMs: number;
token?: string | null;
nonce?: string | null;
version?: 'v1' | 'v2';
}
const ED25519_SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');
function base64UrlEncode(buffer: Buffer): string {
return buffer
.toString('base64')
.replaceAll('+', '-')
.replaceAll('/', '_')
.replace(/=+$/g, '');
}
function derivePublicKeyRaw(publicKeyPem: string): Buffer {
const spki = crypto.createPublicKey(publicKeyPem).export({
type: 'spki',
format: 'der',
}) as Buffer;
if (
spki.length === ED25519_SPKI_PREFIX.length + 32
&& spki.subarray(0, ED25519_SPKI_PREFIX.length).equals(ED25519_SPKI_PREFIX)
) {
return spki.subarray(ED25519_SPKI_PREFIX.length);
}
return spki;
}
function fingerprintPublicKey(publicKeyPem: string): string {
const raw = derivePublicKeyRaw(publicKeyPem);
return crypto.createHash('sha256').update(raw).digest('hex');
}
async function fileExists(filePath: string): Promise<boolean> {
try {
await access(filePath, constants.F_OK);
return true;
} catch {
return false;
}
}
async function generateIdentity(): Promise<DeviceIdentity> {
const { publicKey, privateKey } = await new Promise<crypto.KeyPairKeyObjectResult>(
(resolve, reject) => {
crypto.generateKeyPair('ed25519', (error, nextPublicKey, nextPrivateKey) => {
if (error) {
reject(error);
return;
}
resolve({
publicKey: nextPublicKey,
privateKey: nextPrivateKey,
});
});
},
);
return {
deviceId: fingerprintPublicKey(publicKey.export({ type: 'spki', format: 'pem' }).toString()),
publicKeyPem: publicKey.export({ type: 'spki', format: 'pem' }).toString(),
privateKeyPem: privateKey.export({ type: 'pkcs8', format: 'pem' }).toString(),
};
}
export async function loadOrCreateDeviceIdentity(filePath: string): Promise<DeviceIdentity> {
try {
if (await fileExists(filePath)) {
const raw = await readFile(filePath, 'utf-8');
const parsed = JSON.parse(raw);
if (
parsed?.version === 1
&& typeof parsed.deviceId === 'string'
&& typeof parsed.publicKeyPem === 'string'
&& typeof parsed.privateKeyPem === 'string'
) {
const derivedId = fingerprintPublicKey(parsed.publicKeyPem);
if (derivedId && derivedId !== parsed.deviceId) {
const updated = { ...parsed, deviceId: derivedId };
await writeFile(filePath, `${JSON.stringify(updated, null, 2)}\n`, { mode: 0o600 });
return {
deviceId: derivedId,
publicKeyPem: parsed.publicKeyPem,
privateKeyPem: parsed.privateKeyPem,
};
}
return {
deviceId: parsed.deviceId,
publicKeyPem: parsed.publicKeyPem,
privateKeyPem: parsed.privateKeyPem,
};
}
}
} catch {
// Fall through and create a fresh identity.
}
const identity = await generateIdentity();
const dir = path.dirname(filePath);
if (!(await fileExists(dir))) {
await mkdir(dir, { recursive: true });
}
await writeFile(filePath, `${JSON.stringify({
version: 1,
...identity,
createdAtMs: Date.now(),
}, null, 2)}\n`, { mode: 0o600 });
try {
await chmod(filePath, 0o600);
} catch {
// Ignore chmod failures on unsupported filesystems.
}
return identity;
}
export function signDevicePayload(privateKeyPem: string, payload: string): string {
const key = crypto.createPrivateKey(privateKeyPem);
return base64UrlEncode(crypto.sign(null, Buffer.from(payload, 'utf-8'), key));
}
export function publicKeyRawBase64UrlFromPem(publicKeyPem: string): string {
return base64UrlEncode(derivePublicKeyRaw(publicKeyPem));
}
export function buildDeviceAuthPayload(params: DeviceAuthPayloadParams): string {
const version = params.version ?? (params.nonce ? 'v2' : 'v1');
const scopes = params.scopes.join(',');
const token = params.token ?? '';
const payload = [
version,
params.deviceId,
params.clientId,
params.clientMode,
params.role,
scopes,
String(params.signedAtMs),
token,
];
if (version === 'v2') {
payload.push(params.nonce ?? '');
}
return payload.join('|');
}

View File

@@ -0,0 +1,66 @@
import { readFile, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { ensureDir, getOpenClawConfigDir } from './paths';
type OpenClawConfig = Record<string, unknown>;
const OPENCLAW_CONFIG_FILE_NAME = 'openclaw.json';
export function getOpenClawConfigPath(): string {
return join(getOpenClawConfigDir(), OPENCLAW_CONFIG_FILE_NAME);
}
async function readOpenClawConfig(): Promise<OpenClawConfig> {
const configPath = getOpenClawConfigPath();
try {
const raw = await readFile(configPath, 'utf-8');
return JSON.parse(raw) as OpenClawConfig;
} catch {
return {};
}
}
async function writeOpenClawConfig(config: OpenClawConfig): Promise<void> {
ensureDir(getOpenClawConfigDir());
await writeFile(getOpenClawConfigPath(), JSON.stringify(config, null, 2), 'utf-8');
}
export async function syncBrowserConfigToOpenClaw(): Promise<void> {
const config = await readOpenClawConfig();
const browser = (
config.browser && typeof config.browser === 'object'
? { ...(config.browser as Record<string, unknown>) }
: {}
) as Record<string, unknown>;
let changed = false;
if (browser.enabled === undefined) {
browser.enabled = true;
changed = true;
}
if (browser.defaultProfile === undefined) {
browser.defaultProfile = 'openclaw';
changed = true;
}
if (browser.ssrfPolicy == null) {
browser.ssrfPolicy = { dangerouslyAllowPrivateNetwork: true };
changed = true;
} else if (
typeof browser.ssrfPolicy === 'object'
&& (browser.ssrfPolicy as Record<string, unknown>).dangerouslyAllowPrivateNetwork === undefined
) {
(browser.ssrfPolicy as Record<string, unknown>).dangerouslyAllowPrivateNetwork = true;
changed = true;
}
if (!changed) {
return;
}
config.browser = browser;
await writeOpenClawConfig(config);
}

View File

@@ -64,6 +64,49 @@ export function getOpenClawEntryPath(): string {
return join(getOpenClawDir(), OPENCLAW_ENTRY_FILE_NAME);
}
export function getOpenClawNodeModulesDir(): string {
return join(getOpenClawDir(), 'node_modules');
}
export function getOpenClawBuildDir(): string {
return join(app.getAppPath(), 'build', OPENCLAW_PACKAGE_DIR_NAME);
}
export function getOpenClawPackageStatus(): {
dir: string;
entryPath: string;
nodeModulesDir: string;
packageExists: boolean;
entryExists: boolean;
nodeModulesExists: boolean;
};
export function getOpenClawPackageStatus(
overrides?: Partial<Pick<OpenClawRuntimePaths, 'dir' | 'entryPath'>>,
): {
dir: string;
entryPath: string;
nodeModulesDir: string;
packageExists: boolean;
entryExists: boolean;
nodeModulesExists: boolean;
} {
const dir = overrides?.dir ?? getOpenClawDir();
const entryPath = overrides?.entryPath ?? join(dir, OPENCLAW_ENTRY_FILE_NAME);
const nodeModulesDir = join(dir, 'node_modules');
const entryExists = existsSync(entryPath);
const nodeModulesExists = existsSync(nodeModulesDir);
return {
dir,
entryPath,
nodeModulesDir,
packageExists: existsSync(dir),
entryExists,
nodeModulesExists,
};
}
export function getClawHubCliBinPath(): string {
const binName = process.platform === 'win32' ? 'clawhub.cmd' : 'clawhub';
return join(app.getAppPath(), 'node_modules', '.bin', binName);