- Dashboard Overview with real-time status display - Live Log Viewer (scrollable, filterable) - Config Editor with JSON syntax highlighting - Model Switcher for provider management - Provider Manager for API key configuration - Quick Actions for common tasks - API Routes: status, logs, config, actions, models, providers Tech Stack: - Next.js 14 (App Router) - TypeScript - Tailwind CSS - shadcn/ui components - CodeMirror for JSON editing - Docker support with docker-compose
388 lines
9.4 KiB
TypeScript
388 lines
9.4 KiB
TypeScript
import { exec } from 'child_process';
|
|
import { promisify } from 'util';
|
|
import fs from 'fs/promises';
|
|
import path from 'path';
|
|
|
|
const execAsync = promisify(exec);
|
|
|
|
const OPENCLAW_CONFIG_PATH = '/home/ai/.openclaw/openclaw.json';
|
|
const OPENCLAW_BIN = 'openclaw';
|
|
|
|
export interface OpenClawConfig {
|
|
meta?: {
|
|
lastTouchedVersion?: string;
|
|
lastTouchedAt?: string;
|
|
};
|
|
auth?: {
|
|
profiles?: Record<string, any>;
|
|
};
|
|
models?: {
|
|
mode?: string;
|
|
providers?: Record<string, any>;
|
|
defaults?: {
|
|
model?: {
|
|
primary?: string;
|
|
};
|
|
};
|
|
};
|
|
gateway?: {
|
|
port?: number;
|
|
mode?: string;
|
|
bind?: string;
|
|
controlUi?: {
|
|
enabled?: boolean;
|
|
};
|
|
auth?: {
|
|
token?: string;
|
|
mode?: string;
|
|
};
|
|
};
|
|
plugins?: {
|
|
entries?: Record<string, any>;
|
|
};
|
|
tools?: {
|
|
web?: {
|
|
search?: {
|
|
apiKey?: string;
|
|
};
|
|
};
|
|
};
|
|
[key: string]: any;
|
|
}
|
|
|
|
export interface GatewayStatus {
|
|
running: boolean;
|
|
pid?: number;
|
|
port?: number;
|
|
uptime?: number;
|
|
mode?: string;
|
|
}
|
|
|
|
export interface InstanceStatus {
|
|
id: string;
|
|
name: string;
|
|
type: 'openclaw' | 'opencode' | 'gateway';
|
|
status: 'online' | 'offline' | 'unknown' | 'starting' | 'stopping';
|
|
endpoint?: string;
|
|
model?: string;
|
|
lastCheck: number;
|
|
}
|
|
|
|
/**
|
|
* Get the OpenClaw configuration
|
|
*/
|
|
export async function getOpenClawConfig(): Promise<OpenClawConfig> {
|
|
try {
|
|
const content = await fs.readFile(OPENCLAW_CONFIG_PATH, 'utf-8');
|
|
return JSON.parse(content);
|
|
} catch (error) {
|
|
throw new Error(`Failed to read OpenClaw config: ${(error as Error).message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save the OpenClaw configuration
|
|
*/
|
|
export async function saveOpenClawConfig(config: OpenClawConfig): Promise<void> {
|
|
try {
|
|
await fs.writeFile(
|
|
OPENCLAW_CONFIG_PATH,
|
|
JSON.stringify(config, null, 2),
|
|
'utf-8'
|
|
);
|
|
} catch (error) {
|
|
throw new Error(`Failed to save OpenClaw config: ${(error as Error).message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get Gateway status
|
|
*/
|
|
export async function getGatewayStatus(): Promise<GatewayStatus> {
|
|
try {
|
|
const { stdout } = await execAsync('ps aux | grep "[o]penclaw gateway"');
|
|
const running = stdout.trim().length > 0;
|
|
|
|
if (!running) {
|
|
return { running: false };
|
|
}
|
|
|
|
// Try to get more details from the running process
|
|
try {
|
|
const { stdout: statusOutput } = await execAsync('openclaw gateway status --json', {
|
|
timeout: 5000,
|
|
});
|
|
const statusData = JSON.parse(statusOutput);
|
|
|
|
return {
|
|
running: true,
|
|
pid: statusData.pid,
|
|
port: statusData.port,
|
|
uptime: statusData.uptime,
|
|
mode: statusData.mode,
|
|
};
|
|
} catch {
|
|
return { running: true };
|
|
}
|
|
} catch {
|
|
return { running: false };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Start the Gateway
|
|
*/
|
|
export async function startGateway(): Promise<{ success: boolean; message: string }> {
|
|
try {
|
|
await execAsync('openclaw gateway start', { timeout: 10000 });
|
|
return { success: true, message: 'Gateway started successfully' };
|
|
} catch (error) {
|
|
return { success: false, message: `Failed to start Gateway: ${(error as Error).message}` };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stop the Gateway
|
|
*/
|
|
export async function stopGateway(): Promise<{ success: boolean; message: string }> {
|
|
try {
|
|
await execAsync('openclaw gateway stop', { timeout: 10000 });
|
|
return { success: true, message: 'Gateway stopped successfully' };
|
|
} catch (error) {
|
|
return { success: false, message: `Failed to stop Gateway: ${(error as Error).message}` };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Restart the Gateway
|
|
*/
|
|
export async function restartGateway(): Promise<{ success: boolean; message: string }> {
|
|
try {
|
|
await execAsync('openclaw gateway restart', { timeout: 15000 });
|
|
return { success: true, message: 'Gateway restarted successfully' };
|
|
} catch (error) {
|
|
return { success: false, message: `Failed to restart Gateway: ${(error as Error).message}` };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get recent logs from Gateway
|
|
*/
|
|
export async function getGatewayLogs(lines: number = 100): Promise<string[]> {
|
|
try {
|
|
const { stdout } = await execAsync(`journalctl -u openclaw -n ${lines} --no-pager --output=cat`, {
|
|
timeout: 5000,
|
|
});
|
|
return stdout.trim().split('\n');
|
|
} catch {
|
|
// Fallback: Try to read from common log locations
|
|
const logPaths = [
|
|
'/var/log/openclaw/gateway.log',
|
|
'/root/.openclaw/logs/gateway.log',
|
|
'/tmp/openclaw-gateway.log',
|
|
];
|
|
|
|
for (const logPath of logPaths) {
|
|
try {
|
|
const content = await fs.readFile(logPath, 'utf-8');
|
|
return content.trim().split('\n').slice(-lines);
|
|
} catch {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get current active model
|
|
*/
|
|
export async function getActiveModel(): Promise<string | null> {
|
|
try {
|
|
const config = await getOpenClawConfig();
|
|
return config.models?.defaults?.model?.primary || null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set active model
|
|
*/
|
|
export async function setActiveModel(modelId: string): Promise<{ success: boolean; message: string }> {
|
|
try {
|
|
const config = await getOpenClawConfig();
|
|
if (!config.models) config.models = {};
|
|
if (!config.models.defaults) config.models.defaults = {};
|
|
if (!config.models.defaults.model) config.models.defaults.model = {};
|
|
|
|
config.models.defaults.model.primary = modelId;
|
|
|
|
await saveOpenClawConfig(config);
|
|
|
|
// Restart gateway to apply changes
|
|
await restartGateway();
|
|
|
|
return { success: true, message: `Model set to ${modelId}` };
|
|
} catch (error) {
|
|
return { success: false, message: `Failed to set model: ${(error as Error).message}` };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get available models
|
|
*/
|
|
export async function getAvailableModels(): Promise<Array<{ id: string; name: string }>> {
|
|
try {
|
|
const config = await getOpenClawConfig();
|
|
const models: Array<{ id: string; name: string }> = [];
|
|
|
|
// Collect models from all providers
|
|
if (config.models?.providers) {
|
|
for (const [providerName, providerData] of Object.entries(config.models.providers)) {
|
|
if (providerData && typeof providerData === 'object' && 'models' in providerData) {
|
|
const providerModels = (providerData as any).models || [];
|
|
for (const model of providerModels) {
|
|
models.push({
|
|
id: `${providerName}/${model.id}`,
|
|
name: model.name || model.id,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return models;
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get all providers with their API keys
|
|
*/
|
|
export async function getProviders(): Promise<Array<{
|
|
id: string;
|
|
name: string;
|
|
type: string;
|
|
hasKey: boolean;
|
|
key?: string;
|
|
}>> {
|
|
try {
|
|
const config = await getOpenClawConfig();
|
|
const providers: Array<{
|
|
id: string;
|
|
name: string;
|
|
type: string;
|
|
hasKey: boolean;
|
|
key?: string;
|
|
}> = [];
|
|
|
|
if (config.auth?.profiles) {
|
|
for (const [id, profile] of Object.entries(config.auth.profiles)) {
|
|
const providerData = profile as any;
|
|
providers.push({
|
|
id,
|
|
name: providerData.email || id,
|
|
type: providerData.provider || id.split(':')[0],
|
|
hasKey: !!providerData.apiKey,
|
|
key: providerData.apiKey,
|
|
});
|
|
}
|
|
}
|
|
|
|
return providers;
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add or update a provider's API key
|
|
*/
|
|
export async function setProviderApiKey(
|
|
providerId: string,
|
|
apiKey: string
|
|
): Promise<{ success: boolean; message: string }> {
|
|
try {
|
|
const config = await getOpenClawConfig();
|
|
if (!config.auth) config.auth = {};
|
|
if (!config.auth.profiles) config.auth.profiles = {};
|
|
|
|
if (!config.auth.profiles[providerId]) {
|
|
// Create new provider profile
|
|
const [providerName, ...rest] = providerId.split(':');
|
|
config.auth.profiles[providerId] = {
|
|
provider: providerName,
|
|
mode: 'api_key',
|
|
apiKey,
|
|
};
|
|
} else {
|
|
config.auth.profiles[providerId].apiKey = apiKey;
|
|
}
|
|
|
|
await saveOpenClawConfig(config);
|
|
|
|
return { success: true, message: `API key updated for ${providerId}` };
|
|
} catch (error) {
|
|
return { success: false, message: `Failed to update API key: ${(error as Error).message}` };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get system status summary
|
|
*/
|
|
export async function getSystemStatus(): Promise<{
|
|
gateway: GatewayStatus;
|
|
activeModel: string | null;
|
|
instanceCount: number;
|
|
uptime: number;
|
|
}> {
|
|
const [gatewayStatus, activeModel] = await Promise.all([
|
|
getGatewayStatus(),
|
|
getActiveModel(),
|
|
]);
|
|
|
|
// Get system uptime
|
|
const uptime = process.uptime();
|
|
|
|
return {
|
|
gateway: gatewayStatus,
|
|
activeModel,
|
|
instanceCount: 1, // Will be expanded for multiple instances
|
|
uptime,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Apply config changes and restart gateway
|
|
*/
|
|
export async function applyConfig(config: OpenClawConfig): Promise<{
|
|
success: boolean;
|
|
message: string;
|
|
}> {
|
|
try {
|
|
// Validate config (basic)
|
|
if (!config || typeof config !== 'object') {
|
|
throw new Error('Invalid configuration');
|
|
}
|
|
|
|
await saveOpenClawConfig(config);
|
|
|
|
// Restart gateway to apply changes
|
|
const restartResult = await restartGateway();
|
|
|
|
if (!restartResult.success) {
|
|
return {
|
|
success: false,
|
|
message: `Config saved but failed to restart Gateway: ${restartResult.message}`,
|
|
};
|
|
}
|
|
|
|
return { success: true, message: 'Configuration applied and Gateway restarted' };
|
|
} catch (error) {
|
|
return { success: false, message: `Failed to apply config: ${(error as Error).message}` };
|
|
}
|
|
}
|