feat: refactor HomePage to integrate agents store and update related components

feat: add runtime event handling for providers in ProvidersSection

feat: update routing to include Channels and Agents pages

feat: extend route types and navigation items for Channels and Agents

feat: implement agents store for managing agent data and interactions

fix: update chat store to utilize agents store for agent-related functionality

chore: export agents store from index

fix: enhance runtime types for better event handling

fix: update Vite config to handle dev server URL correctly
This commit is contained in:
duanshuwen
2026-04-18 14:56:32 +08:00
parent dfa4388087
commit ee72cf7261
52 changed files with 6626 additions and 189 deletions

View File

@@ -4,6 +4,7 @@ import { CONFIG_KEYS, IPC_EVENTS } from '@runtime/lib/constants'
import { debounce } from '@runtime/lib/utils'
import logManager from '@electron/service/logger'
import { getUserDataDir } from '@electron/utils/paths'
const DEFAULT_CONFIG: IConfig = {
[CONFIG_KEYS.THEME_MODE]: 'system',
@@ -35,6 +36,7 @@ export class ConfigService {
const { default: Store } = await import('electron-store');
this._store = new Store<IConfig>({
name: 'config',
cwd: getUserDataDir(),
defaults: DEFAULT_CONFIG,
});
this._setupIpcEvents();

View File

@@ -1,9 +1,10 @@
import { IPC_EVENTS } from '@runtime/lib/constants';
import { promisify } from 'util';
import { app, ipcMain } from 'electron';
import { ipcMain } from 'electron';
import log from 'electron-log';
import * as path from 'path';
import * as fs from 'fs';
import { getUserDataDir } from '@electron/utils/paths';
// 转换为Promise形式的fs方法
const readdirAsync = promisify(fs.readdir);
@@ -20,7 +21,7 @@ class LogService {
private readonly CLEANUP_INTERVAL_MS = 24 * 60 * 60 * 1000;
private constructor() {
const logPath = path.join(app.getPath('userData'), 'logs');
const logPath = path.join(getUserDataDir(), 'logs');
// c:users/{username}/AppData/Roaming/{appName}/logs
// 创建日志目录
@@ -81,7 +82,7 @@ class LogService {
private async _cleanupOldLogs() {
try {
const logPath = path.join(app.getPath('userData'), 'logs');
const logPath = path.join(getUserDataDir(), 'logs');
if (!fs.existsSync(logPath)) return;

View File

@@ -1,4 +1,3 @@
import { app } from 'electron';
import * as fs from 'fs';
import * as path from 'path';
import logManager from '@electron/service/logger';
@@ -8,6 +7,7 @@ import type {
ProviderVendorInfo,
ProviderWithKeyInfo,
} from '@runtime/lib/providers';
import { getUserDataDir } from '@electron/utils/paths';
interface ProviderStore {
accounts: ProviderAccount[];
@@ -19,8 +19,8 @@ const defaultStore: ProviderStore = {
defaultAccountId: null,
};
const storePath = path.join(app.getPath('userData'), 'provider-accounts.json');
const keysPath = path.join(app.getPath('userData'), 'provider-keys.json');
const storePath = path.join(getUserDataDir(), 'provider-accounts.json');
const keysPath = path.join(getUserDataDir(), 'provider-keys.json');
function readJson<T>(filePath: string, defaultValue: T): T {
try {

View File

@@ -0,0 +1,231 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import logManager from '@electron/service/logger';
import { providerApiService } from '@electron/service/provider-api-service';
import { listAgentsSnapshot } from '@electron/utils/agent-config';
import { ensureDir, ensureOpenClawRuntimeLayout, getOpenClawRuntimePaths } from '@electron/utils/paths';
import type { AgentSummary, AgentsSnapshot } from '@runtime/lib/agents';
import type { ProviderAccount } from '@runtime/lib/providers';
interface AgentRuntimeSyncMeta {
agentId: string;
providerAccountId: string | null;
modelRef: string | null;
inheritedModel: boolean;
runtimeDir: string;
}
export interface ProviderRuntimeSyncResult {
syncedAt: string;
agentCount: number;
defaultAccountId: string | null;
globalRegistryPath: string;
agents: AgentRuntimeSyncMeta[];
warnings: string[];
}
interface RuntimeProviderProfile {
id: string;
vendorId: string;
label: string;
authMode: string;
apiKey: string | null;
hasKey: boolean;
baseUrl?: string;
apiProtocol?: string;
headers?: Record<string, string>;
metadata?: ProviderAccount['metadata'];
}
const AGENT_RUNTIME_DIR_NAME = 'runtime';
const AUTH_PROFILES_FILE_NAME = 'auth-profiles.json';
const MODELS_FILE_NAME = 'models.json';
const OPENCLAW_FILE_NAME = 'openclaw.json';
const GLOBAL_REGISTRY_FILE_NAME = 'agents-runtime.json';
function writeJson(filePath: string, data: unknown): void {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
}
function getEnabledAccounts(accounts?: ProviderAccount[]): ProviderAccount[] {
const source = Array.isArray(accounts) ? accounts : providerApiService.getAccounts();
return source.filter((account) => account.enabled !== false);
}
function resolveEffectiveAccount(
agent: AgentSummary,
accounts: ProviderAccount[],
defaultAccountId: string | null,
): ProviderAccount | null {
if (agent.providerAccountId) {
return accounts.find((account) => account.id === agent.providerAccountId) ?? null;
}
if (defaultAccountId) {
return accounts.find((account) => account.id === defaultAccountId) ?? null;
}
return accounts[0] ?? null;
}
function buildRuntimeProfile(account: ProviderAccount | null): RuntimeProviderProfile[] {
if (!account) return [];
const apiKey = providerApiService.getApiKey(account.id).apiKey;
return [{
id: account.id,
vendorId: account.vendorId,
label: account.label,
authMode: account.authMode,
apiKey: apiKey || null,
hasKey: Boolean(apiKey),
baseUrl: account.baseUrl,
apiProtocol: account.apiProtocol,
headers: account.headers,
metadata: account.metadata,
}];
}
function buildRuntimeModelEntry(
agent: AgentSummary,
effectiveAccount: ProviderAccount | null,
defaultModelRef: string | null,
) {
const effectiveModelRef = agent.modelRef ?? effectiveAccount?.model ?? defaultModelRef ?? null;
if (!effectiveModelRef) {
return [];
}
return [{
id: effectiveModelRef,
modelRef: effectiveModelRef,
providerAccountId: effectiveAccount?.id ?? null,
vendorId: effectiveAccount?.vendorId ?? agent.vendorId ?? null,
baseUrl: effectiveAccount?.baseUrl ?? null,
apiProtocol: effectiveAccount?.apiProtocol ?? null,
headers: effectiveAccount?.headers ?? {},
fallbackModels: effectiveAccount?.fallbackModels ?? [],
fallbackAccountIds: effectiveAccount?.fallbackAccountIds ?? [],
inheritedModel: Boolean(agent.inheritedModel),
source: agent.overrideModelRef ? 'agent-override' : 'provider-default',
}];
}
function ensureAgentRuntimeDir(agent: AgentSummary): string {
const agentDir = agent.agentDir?.trim();
if (!agentDir) {
throw new Error(`Agent "${agent.id}" is missing agentDir`);
}
return ensureDir(path.join(agentDir, AGENT_RUNTIME_DIR_NAME));
}
function writeAgentRuntimeFiles(
agent: AgentSummary,
snapshot: AgentsSnapshot,
accounts: ProviderAccount[],
defaultAccountId: string | null,
syncedAt: string,
warnings: string[],
): AgentRuntimeSyncMeta {
const runtimeDir = ensureAgentRuntimeDir(agent);
const effectiveAccount = resolveEffectiveAccount(agent, accounts, defaultAccountId);
const profiles = buildRuntimeProfile(effectiveAccount);
const models = buildRuntimeModelEntry(agent, effectiveAccount, snapshot.defaultModelRef);
if (agent.providerAccountId && !effectiveAccount) {
warnings.push(`Agent "${agent.id}" references missing provider account "${agent.providerAccountId}"`);
}
if (!effectiveAccount) {
warnings.push(`Agent "${agent.id}" has no enabled provider account available`);
}
if (models.length === 0) {
warnings.push(`Agent "${agent.id}" has no model configured after runtime sync`);
}
writeJson(path.join(runtimeDir, AUTH_PROFILES_FILE_NAME), {
version: 1,
syncedAt,
agentId: agent.id,
defaultProfileId: effectiveAccount?.id ?? null,
profiles,
});
writeJson(path.join(runtimeDir, MODELS_FILE_NAME), {
version: 1,
syncedAt,
agentId: agent.id,
defaultModelRef: models[0]?.modelRef ?? null,
models,
});
writeJson(path.join(runtimeDir, OPENCLAW_FILE_NAME), {
version: 1,
syncedAt,
agentId: agent.id,
agentName: agent.name,
mainSessionKey: agent.mainSessionKey,
workspace: agent.workspace ?? null,
agentDir: agent.agentDir ?? null,
providerAccountId: effectiveAccount?.id ?? null,
defaultProviderAccountId: snapshot.defaultProviderAccountId,
modelRef: models[0]?.modelRef ?? null,
defaultModelRef: snapshot.defaultModelRef,
inheritedModel: Boolean(agent.inheritedModel),
});
return {
agentId: agent.id,
providerAccountId: effectiveAccount?.id ?? null,
modelRef: models[0]?.modelRef ?? null,
inheritedModel: Boolean(agent.inheritedModel),
runtimeDir,
};
}
export function syncProviderRuntimeSnapshot(options?: {
accounts?: ProviderAccount[];
defaultAccountId?: string | null;
snapshot?: AgentsSnapshot;
}): ProviderRuntimeSyncResult {
const syncedAt = new Date().toISOString();
const accounts = getEnabledAccounts(options?.accounts);
const defaultAccountId = options?.defaultAccountId ?? providerApiService.getDefault().accountId;
const snapshot = options?.snapshot ?? listAgentsSnapshot(accounts, defaultAccountId);
const warnings: string[] = [];
const runtimePaths = ensureOpenClawRuntimeLayout(getOpenClawRuntimePaths());
const agents = snapshot.agents.map((agent) =>
writeAgentRuntimeFiles(agent, snapshot, accounts, defaultAccountId, syncedAt, warnings),
);
const globalRegistryPath = path.join(runtimePaths.runtimeDir, GLOBAL_REGISTRY_FILE_NAME);
writeJson(globalRegistryPath, {
version: 1,
syncedAt,
defaultAccountId,
defaultModelRef: snapshot.defaultModelRef,
agents,
});
if (warnings.length > 0) {
logManager.warn('Provider runtime sync completed with warnings', warnings);
} else {
logManager.info('Provider runtime sync completed', {
agentCount: agents.length,
globalRegistryPath,
});
}
return {
syncedAt,
agentCount: agents.length,
defaultAccountId,
globalRegistryPath,
agents,
warnings,
};
}

View File

@@ -9,6 +9,9 @@ type TabId = string
type TabInfo = { id: TabId; url: string; title: string; isLoading: boolean; canGoBack: boolean; canGoForward: boolean }
const UI_HEIGHT = 88
const preloadEntryPath = MAIN_WINDOW_VITE_DEV_SERVER_URL
? path.join(process.cwd(), 'dist-electron', 'preload', 'preload.js')
: path.join(__dirname, '..', 'preload', 'preload.js')
export class TabManager {
private win: BrowserWindow
@@ -99,9 +102,7 @@ export class TabManager {
nodeIntegration: false,
contextIsolation: true,
sandbox: true,
preload: MAIN_WINDOW_VITE_DEV_SERVER_URL
? path.join(process.cwd(), 'dist-electron/preload/preload.js')
: path.join(__dirname, 'preload.js'),
preload: preloadEntryPath,
},
})
this.views.set(id, view)

View File

@@ -30,6 +30,9 @@ interface SizeOptions {
const isMac = process.platform === 'darwin';
const isWindows = process.platform === 'win32';
const useCustomTitleBar = isWindows;
const preloadEntryPath = MAIN_WINDOW_VITE_DEV_SERVER_URL
? path.join(process.cwd(), 'dist-electron', 'preload', 'preload.js')
: path.join(__dirname, '..', 'preload', 'preload.js');
function getSharedWindowOptions(): BrowserWindowConstructorOptions {
return {
@@ -45,9 +48,7 @@ function getSharedWindowOptions(): BrowserWindowConstructorOptions {
contextIsolation: true, // 启用上下文隔离,防止渲染进程访问主进程 API
sandbox: true, // 启用沙箱模式,进一步增强安全性
backgroundThrottling: false,
preload: MAIN_WINDOW_VITE_DEV_SERVER_URL
? path.join(process.cwd(), 'dist-electron/preload/preload.js')
: path.join(__dirname, 'preload.js'),
preload: preloadEntryPath,
},
};
}