Files
zclaw_openfang/desktop/src/lib/config-parser.ts
iven 3e81bd3e50 feat(ui): Phase 8 UI/UX optimization and system documentation update
## Sidebar Enhancement
- Change tabs to icon + small label layout for better space utilization
- Add Teams tab with team collaboration entry point

## Settings Page Improvements
- Connect theme toggle to gatewayStore.saveQuickConfig for persistence
- Remove OpenFang backend download section, simplify UI
- Add time range filter to UsageStats (7d/30d/all)
- Add stat cards with icons (sessions, messages, input/output tokens)
- Add token usage overview bar chart
- Add 8 ZCLAW system skill definitions with categories

## Bug Fixes
- Fix ChannelList duplicate content with deduplication logic
- Integrate CreateTriggerModal in TriggersPanel
- Add independent SecurityStatusPanel with 12 default enabled layers
- Change workflow view to use SchedulerPanel as unified entry

## New Components
- CreateTriggerModal: Event trigger creation modal
- HandApprovalModal: Hand approval workflow dialog
- HandParamsForm: Enhanced Hand parameter form
- SecurityLayersPanel: 16-layer security status display

## Architecture
- Add TOML config parsing support (toml-utils.ts, config-parser.ts)
- Add request timeout and retry mechanism (request-helper.ts)
- Add secure token storage (secure-storage.ts, secure_storage.rs)

## Tests
- Add unit tests for config-parser, toml-utils, request-helper
- Add team-client and teamStore tests

## Documentation
- Update SYSTEM_ANALYSIS.md with Phase 8 completion
- UI completion: 100% (30/30 components)
- API coverage: 93% (63/68 endpoints)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 14:12:11 +08:00

378 lines
11 KiB
TypeScript

/**
* OpenFang Configuration Parser
*
* Provides configuration parsing, validation, and serialization for OpenFang TOML files.
*
* @module lib/config-parser
*/
import { tomlUtils, TomlParseError } from './toml-utils';
import type {
OpenFangConfig,
ConfigValidationResult,
ConfigValidationError,
ConfigValidationWarning,
ConfigFileMetadata,
ServerConfig,
AgentSectionConfig,
LLMConfig,
} from '../types/config';
/**
* Error class for configuration parsing errors
*/
export class ConfigParseError extends Error {
constructor(
message: string,
public readonly cause?: unknown
) {
super(message);
this.name = 'ConfigParseError';
}
}
/**
* Error class for configuration validation errors (thrown when validation fails)
*/
export class ConfigValidationFailedError extends Error {
constructor(
message: string,
public readonly errors: ConfigValidationError[]
) {
super(message);
this.name = 'ConfigValidationFailedError';
}
}
/**
* Required configuration fields with their paths
*/
const REQUIRED_FIELDS: Array<{ path: string; description: string }> = [
{ path: 'server', description: 'Server configuration' },
{ path: 'server.host', description: 'Server host address' },
{ path: 'server.port', description: 'Server port number' },
{ path: 'agent', description: 'Agent configuration' },
{ path: 'agent.defaults', description: 'Agent defaults' },
{ path: 'agent.defaults.workspace', description: 'Default workspace path' },
{ path: 'agent.defaults.default_model', description: 'Default model name' },
{ path: 'llm', description: 'LLM configuration' },
{ path: 'llm.default_provider', description: 'Default LLM provider' },
{ path: 'llm.default_model', description: 'Default LLM model' },
];
/**
* Default configuration values
*/
const DEFAULT_CONFIG: Partial<OpenFangConfig> = {
server: {
host: '127.0.0.1',
port: 50051,
websocket_port: 50051,
websocket_path: '/ws',
api_version: 'v1',
},
agent: {
defaults: {
workspace: '~/.openfang/workspace',
default_model: 'gpt-4',
},
},
llm: {
default_provider: 'openai',
default_model: 'gpt-4',
},
};
/**
* Configuration parser and validator
*/
export const configParser = {
/**
* Parse TOML content into an OpenFang configuration object
*
* @param content - The TOML content to parse
* @param envVars - Optional environment variables for resolution
* @returns The parsed configuration object
* @throws ConfigParseError if parsing fails
*
* @example
* ```typescript
* const config = configParser.parseConfig(tomlContent, { OPENAI_API_KEY: 'sk-...' });
* ```
*/
parseConfig: (content: string, envVars?: Record<string, string | undefined>): OpenFangConfig => {
try {
// First resolve environment variables
const resolved = tomlUtils.resolveEnvVars(content, envVars);
// Parse TOML
const parsed = tomlUtils.parse<OpenFangConfig>(resolved);
return parsed;
} catch (error) {
if (error instanceof TomlParseError) {
throw new ConfigParseError(`Failed to parse configuration: ${error.message}`, error);
}
throw new ConfigParseError(
`Unexpected error parsing configuration: ${error instanceof Error ? error.message : String(error)}`,
error
);
}
},
/**
* Validate an OpenFang configuration object
*
* @param config - The configuration object to validate
* @returns Validation result with errors and warnings
*
* @example
* ```typescript
* const result = configParser.validateConfig(parsedConfig);
* if (!result.valid) {
* console.error('Config errors:', result.errors);
* }
* ```
*/
validateConfig: (config: unknown): ConfigValidationResult => {
const errors: ConfigValidationError[] = [];
const warnings: ConfigValidationWarning[] = [];
// Basic type check
if (typeof config !== 'object' || config === null) {
errors.push({
path: '',
message: 'Configuration must be a non-null object',
severity: 'error',
});
return { valid: false, errors, warnings };
}
const cfg = config as Record<string, unknown>;
// Check required fields
for (const { path, description } of REQUIRED_FIELDS) {
const value = getNestedValue(cfg, path);
if (value === undefined) {
errors.push({
path,
message: `Missing required field: ${description}`,
severity: 'error',
});
}
}
// Validate server configuration
if (cfg.server && typeof cfg.server === 'object') {
const server = cfg.server as ServerConfig;
if (typeof server.port === 'number' && (server.port < 1 || server.port > 65535)) {
errors.push({
path: 'server.port',
message: 'Port must be between 1 and 65535',
severity: 'error',
});
}
if (typeof server.host === 'string' && server.host.length === 0) {
errors.push({
path: 'server.host',
message: 'Host cannot be empty',
severity: 'error',
});
}
}
// Validate agent configuration
if (cfg.agent && typeof cfg.agent === 'object') {
const agent = cfg.agent as AgentSectionConfig;
if (agent.defaults) {
if (typeof agent.defaults.workspace === 'string' && agent.defaults.workspace.length === 0) {
warnings.push({
path: 'agent.defaults.workspace',
message: 'Workspace path is empty',
severity: 'warning',
});
}
if (typeof agent.defaults.default_model === 'string' && agent.defaults.default_model.length === 0) {
errors.push({
path: 'agent.defaults.default_model',
message: 'Default model cannot be empty',
severity: 'error',
});
}
}
}
// Validate LLM configuration
if (cfg.llm && typeof cfg.llm === 'object') {
const llm = cfg.llm as LLMConfig;
if (typeof llm.default_provider === 'string' && llm.default_provider.length === 0) {
errors.push({
path: 'llm.default_provider',
message: 'Default provider cannot be empty',
severity: 'error',
});
}
if (typeof llm.default_model === 'string' && llm.default_model.length === 0) {
errors.push({
path: 'llm.default_model',
message: 'Default model cannot be empty',
severity: 'error',
});
}
}
return {
valid: errors.length === 0,
errors,
warnings,
};
},
/**
* Parse and validate configuration in one step
*
* @param content - The TOML content to parse
* @param envVars - Optional environment variables for resolution
* @returns The parsed and validated configuration
* @throws ConfigParseError if parsing fails
* @throws ConfigValidationFailedError if validation fails
*
* @example
* ```typescript
* const config = configParser.parseAndValidate(tomlContent);
* ```
*/
parseAndValidate: (
content: string,
envVars?: Record<string, string | undefined>
): OpenFangConfig => {
const config = configParser.parseConfig(content, envVars);
const result = configParser.validateConfig(config);
if (!result.valid) {
throw new ConfigValidationFailedError(
`Configuration validation failed: ${result.errors.map((e) => e.message).join(', ')}`,
result.errors
);
}
return config;
},
/**
* Serialize a configuration object to TOML format
*
* @param config - The configuration object to serialize
* @returns The TOML string
*
* @example
* ```typescript
* const toml = configParser.stringifyConfig(config);
* ```
*/
stringifyConfig: (config: OpenFangConfig): string => {
return tomlUtils.stringify(config as unknown as Record<string, unknown>);
},
/**
* Merge partial configuration with defaults
*
* @param config - Partial configuration to merge
* @returns Complete configuration with defaults applied
*
* @example
* ```typescript
* const fullConfig = configParser.mergeWithDefaults(partialConfig);
* ```
*/
mergeWithDefaults: (config: Partial<OpenFangConfig>): OpenFangConfig => {
return deepMerge(DEFAULT_CONFIG, config) as unknown as OpenFangConfig;
},
/**
* Extract metadata from a TOML configuration file
*
* @param content - The TOML content
* @param filePath - The file path
* @returns Configuration file metadata
*
* @example
* ```typescript
* const metadata = configParser.extractMetadata(tomlContent, '/path/to/config.toml');
* console.log('Env vars needed:', metadata.envVars);
* ```
*/
extractMetadata: (content: string, filePath: string): ConfigFileMetadata => {
const envVars = tomlUtils.extractEnvVarNames(content);
const hasUnresolvedEnvVars = tomlUtils.hasUnresolvedEnvVars(content);
return {
path: filePath,
name: filePath.split('/').pop() || filePath,
envVars,
hasUnresolvedEnvVars,
};
},
/**
* Get default configuration
*
* @returns Default OpenFang configuration
*/
getDefaults: (): OpenFangConfig => {
return JSON.parse(JSON.stringify(DEFAULT_CONFIG)) as OpenFangConfig;
},
/**
* Check if a configuration object is valid
*
* @param config - The configuration to check
* @returns Type guard for OpenFangConfig
*/
isOpenFangConfig: (config: unknown): config is OpenFangConfig => {
const result = configParser.validateConfig(config);
return result.valid;
},
};
/**
* Helper function to get a nested value from an object using dot-notation path
*/
function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
const parts = path.split('.');
let current: unknown = obj;
for (const part of parts) {
if (current === null || current === undefined) {
return undefined;
}
if (typeof current !== 'object') {
return undefined;
}
current = (current as Record<string, unknown>)[part];
}
return current;
}
/**
* Helper function to deep merge two objects
*/
function deepMerge<T extends Record<string, unknown>>(
target: Partial<T>,
source: Partial<T>
): Partial<T> {
const result = { ...target };
for (const key of Object.keys(source) as (keyof T)[]) {
const sourceValue = source[key];
const targetValue = target[key];
if (
sourceValue !== undefined &&
typeof sourceValue === 'object' &&
sourceValue !== null &&
!Array.isArray(sourceValue) &&
targetValue !== undefined &&
typeof targetValue === 'object' &&
targetValue !== null &&
!Array.isArray(targetValue)
) {
result[key] = deepMerge(
targetValue as Record<string, unknown>,
sourceValue as Record<string, unknown>
) as T[keyof T];
} else if (sourceValue !== undefined) {
// Safe assignment: sourceValue is typed as Partial<T>[keyof T], result[key] expects T[keyof T]
result[key] = sourceValue as T[keyof T];
}
}
return result;
}
export default configParser;