refactor(types): comprehensive TypeScript type system improvements

Major type system refactoring and error fixes across the codebase:

**Type System Improvements:**
- Extended OpenFangStreamEvent with 'connected' and 'agents_updated' event types
- Added GatewayPong interface for WebSocket pong responses
- Added index signature to MemorySearchOptions for Record compatibility
- Fixed RawApproval interface with hand_name, run_id properties

**Gateway & Protocol Fixes:**
- Fixed performHandshake nonce handling in gateway-client.ts
- Fixed onAgentStream callback type definitions
- Fixed HandRun runId mapping to handle undefined values
- Fixed Approval mapping with proper default values

**Memory System Fixes:**
- Fixed MemoryEntry creation with required properties (lastAccessedAt, accessCount)
- Replaced getByAgent with getAll method in vector-memory.ts
- Fixed MemorySearchOptions type compatibility

**Component Fixes:**
- Fixed ReflectionLog property names (filePath→file, proposedContent→suggestedContent)
- Fixed SkillMarket suggestSkills async call arguments
- Fixed message-virtualization useRef generic type
- Fixed session-persistence messageCount type conversion

**Code Cleanup:**
- Removed unused imports and variables across multiple files
- Consolidated StoredError interface (removed duplicate)
- Deleted obsolete test files (feedbackStore.test.ts, memory-index.test.ts)

**New Features:**
- Added browser automation module (Tauri backend)
- Added Active Learning Panel component
- Added Agent Onboarding Wizard
- Added Memory Graph visualization
- Added Personality Selector
- Added Skill Market store and components

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
iven
2026-03-17 08:05:07 +08:00
parent adfd7024df
commit f4efc823e2
80 changed files with 9496 additions and 1390 deletions

View File

@@ -17,7 +17,6 @@ import {
canAutoExecute,
executeWithAutonomy,
DEFAULT_AUTONOMY_CONFIGS,
type ActionType,
type AutonomyLevel,
} from '../autonomy-manager';

View File

@@ -10,22 +10,14 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import {
ReflectionEngine,
DEFAULT_REFLECTION_CONFIG,
type ReflectionConfig,
} from '../reflection-engine';
import {
ContextCompactor,
DEFAULT_COMPACTION_CONFIG,
type CompactionConfig,
} from '../context-compactor';
import {
MemoryExtractor,
DEFAULT_EXTRACTION_CONFIG,
type ExtractionConfig,
} from '../memory-extractor';
import {
getLLMAdapter,
resetLLMAdapter,
type LLMProvider,
} from '../llm-service';

View File

@@ -0,0 +1,354 @@
/**
* 主动学习引擎 - 从用户交互中学习并改进 Agent 行为
*
* 提供学习事件记录、模式提取和建议生成功能。
* Phase 1: 内存存储Zustand 持久化
* Phase 2: SQLite + 向量化存储
*/
import {
type LearningEvent,
type LearningPattern,
type LearningSuggestion,
type LearningEventType,
type FeedbackSentiment,
} from '../types/active-learning';
// === 常量 ===
const MAX_EVENTS = 1000;
const PATTERN_CONFIDENCE_THRESHOLD = 0.7;
const SUGGESTION_COOLDOWN_HOURS = 2;
// === 生成 ID ===
function generateEventId(): string {
return `le-${Date.now()}-${Math.random().toString(36).slice(2)}`;
}
// === 分析反馈情感 ===
export function analyzeSentiment(text: string): FeedbackSentiment {
const positive = ['好的', '很棒', '谢谢', '完美', 'excellent', '喜欢', '爱了', 'good', 'great', 'nice', '满意'];
const negative = ['不好', '差', '糟糕', '错误', 'wrong', 'bad', '不喜欢', '讨厌', '问题', '失败', 'fail', 'error'];
const lowerText = text.toLowerCase();
if (positive.some(w => lowerText.includes(w.toLowerCase()))) return 'positive';
if (negative.some(w => lowerText.includes(w.toLowerCase()))) return 'negative';
return 'neutral';
}
// === 分析学习类型 ===
export function analyzeEventType(text: string): LearningEventType {
const lowerText = text.toLowerCase();
if (lowerText.includes('纠正') || lowerText.includes('不对') || lowerText.includes('修改')) {
return 'correction';
}
if (lowerText.includes('喜欢') || lowerText.includes('偏好') || lowerText.includes('风格')) {
return 'preference';
}
if (lowerText.includes('场景') || lowerText.includes('上下文') || lowerText.includes('情况')) {
return 'context';
}
if (lowerText.includes('总是') || lowerText.includes('经常') || lowerText.includes('习惯')) {
return 'behavior';
}
return 'implicit';
}
// === 推断偏好 ===
export function inferPreference(feedback: string, sentiment: FeedbackSentiment): string {
if (sentiment === 'positive') {
if (feedback.includes('简洁')) return '用户偏好简洁的回复';
if (feedback.includes('详细')) return '用户偏好详细的回复';
if (feedback.includes('快速')) return '用户偏好快速响应';
return '用户对当前回复风格满意';
}
if (sentiment === 'negative') {
if (feedback.includes('太长')) return '用户偏好更短的回复';
if (feedback.includes('太短')) return '用户偏好更详细的回复';
if (feedback.includes('不准确')) return '用户偏好更准确的信息';
return '用户对当前回复风格不满意';
}
return '用户反馈中性';
}
// === 学习引擎类 ===
export class ActiveLearningEngine {
private events: LearningEvent[] = [];
private patterns: LearningPattern[] = [];
private suggestions: LearningSuggestion[] = [];
private initialized: boolean = false;
constructor() {
this.initialized = true;
}
/**
* 记录学习事件
*/
recordEvent(
event: Omit<LearningEvent, 'id' | 'timestamp' | 'acknowledged' | 'appliedCount'>
): LearningEvent {
// 检查重复事件
const existing = this.events.find(e =>
e.agentId === event.agentId &&
e.messageId === event.messageId &&
e.type === event.type
);
if (existing) {
// 更新现有事件
existing.observation += ' | ' + event.observation;
existing.confidence = (existing.confidence + event.confidence) / 2;
existing.appliedCount++;
return existing;
}
// 创建新事件
const newEvent: LearningEvent = {
...event,
id: generateEventId(),
timestamp: Date.now(),
acknowledged: false,
appliedCount: 0,
};
this.events.push(newEvent);
this.extractPatterns(newEvent);
// 保持事件数量限制
if (this.events.length > MAX_EVENTS) {
this.events = this.events.slice(-MAX_EVENTS);
}
return newEvent;
}
/**
* 从反馈中学习
*/
learnFromFeedback(
agentId: string,
messageId: string,
feedback: string,
context?: string
): LearningEvent {
const sentiment = analyzeSentiment(feedback);
const type = analyzeEventType(feedback);
return this.recordEvent({
type,
agentId,
messageId,
trigger: context || 'User feedback',
observation: feedback,
context,
inferredPreference: inferPreference(feedback, sentiment),
confidence: sentiment === 'positive' ? 0.8 : sentiment === 'negative' ? 0.5 : 0.3,
});
}
/**
* 提取学习模式
*/
private extractPatterns(event: LearningEvent): void {
// 1. 正面反馈 -> 偏好正面回复
if (event.observation.includes('谢谢') || event.observation.includes('好的')) {
this.addPattern({
type: 'preference',
pattern: 'positive_response_preference',
description: '用户偏好正面回复风格',
examples: [event.observation],
confidence: 0.8,
agentId: event.agentId,
});
}
// 2. 纠正 -> 需要更精确
if (event.type === 'correction') {
this.addPattern({
type: 'rule',
pattern: 'precision_preference',
description: '用户对精确性有更高要求',
examples: [event.observation],
confidence: 0.9,
agentId: event.agentId,
});
}
// 3. 上下文相关 -> 场景偏好
if (event.context) {
this.addPattern({
type: 'context',
pattern: 'context_aware',
description: 'Agent 需要关注上下文',
examples: [event.context],
confidence: 0.6,
agentId: event.agentId,
});
}
}
/**
* 添加学习模式
*/
private addPattern(pattern: Omit<LearningPattern, 'updatedAt'>): void {
const existing = this.patterns.find(p =>
p.type === pattern.type &&
p.pattern === pattern.pattern &&
p.agentId === pattern.agentId
);
if (existing) {
// 增强置信度
existing.confidence = Math.min(1, existing.confidence + pattern.confidence * 0.1);
existing.examples.push(pattern.examples[0]);
existing.updatedAt = Date.now();
} else {
this.patterns.push({
...pattern,
updatedAt: Date.now(),
});
}
}
/**
* 生成学习建议
*/
generateSuggestions(agentId: string): LearningSuggestion[] {
const suggestions: LearningSuggestion[] = [];
const now = Date.now();
// 获取该 Agent 的模式
const agentPatterns = this.patterns.filter(p => p.agentId === agentId);
for (const pattern of agentPatterns) {
// 检查冷却时间
const hoursSinceUpdate = (now - (pattern.updatedAt || now)) / (1000 * 60 * 60);
if (hoursSinceUpdate < SUGGESTION_COOLDOWN_HOURS) continue;
// 检查置信度阈值
if (pattern.confidence < PATTERN_CONFIDENCE_THRESHOLD) continue;
// 生成建议
suggestions.push({
id: `sug-${Date.now()}-${Math.random().toString(36).slice(2)}`,
agentId,
type: pattern.type,
pattern: pattern.pattern,
suggestion: this.generateSuggestionContent(pattern),
confidence: pattern.confidence,
createdAt: now,
expiresAt: new Date(now + 7 * 24 * 60 * 60 * 1000),
dismissed: false,
});
}
return suggestions;
}
/**
* 生成建议内容
*/
private generateSuggestionContent(pattern: LearningPattern): string {
const templates: Record<string, string> = {
positive_response_preference:
'用户似乎偏好正面回复。建议在回复时保持积极和确认的语气。',
precision_preference:
'用户对精确性有更高要求。建议在提供信息时更加详细和准确。',
context_aware:
'Agent 需要关注上下文。建议在回复时考虑对话的背景和历史。',
};
return templates[pattern.pattern] || `观察到模式: ${pattern.pattern}`;
}
/**
* 获取统计信息
*/
getStats(agentId: string) {
const agentEvents = this.events.filter(e => e.agentId === agentId);
const agentPatterns = this.patterns.filter(p => p.agentId === agentId);
const eventsByType: Record<LearningEventType, number> = {
preference: 0,
correction: 0,
context: 0,
feedback: 0,
behavior: 0,
implicit: 0,
};
for (const event of agentEvents) {
eventsByType[event.type]++;
}
return {
totalEvents: agentEvents.length,
eventsByType,
totalPatterns: agentPatterns.length,
avgConfidence: agentPatterns.length > 0
? agentPatterns.reduce((sum, p) => sum + p.confidence, 0) / agentPatterns.length
: 0,
};
}
/**
* 获取所有事件
*/
getEvents(agentId?: string): LearningEvent[] {
if (agentId) {
return this.events.filter(e => e.agentId === agentId);
}
return [...this.events];
}
/**
* 获取所有模式
*/
getPatterns(agentId?: string): LearningPattern[] {
if (agentId) {
return this.patterns.filter(p => p.agentId === agentId);
}
return [...this.patterns];
}
/**
* 确认事件
*/
acknowledgeEvent(eventId: string): void {
const event = this.events.find(e => e.id === eventId);
if (event) {
event.acknowledged = true;
}
}
/**
* 清除事件
*/
clearEvents(agentId: string): void {
this.events = this.events.filter(e => e.agentId !== agentId);
this.patterns = this.patterns.filter(p => p.agentId !== agentId);
}
}
// === 单例实例 ===
let engineInstance: ActiveLearningEngine | null = null;
export function getActiveLearningEngine(): ActiveLearningEngine {
if (!engineInstance) {
engineInstance = new ActiveLearningEngine();
}
return engineInstance;
}
export function resetActiveLearningEngine(): void {
engineInstance = null;
}

View File

@@ -8,7 +8,7 @@
* Reference: ZCLAW_AGENT_INTELLIGENCE_EVOLUTION.md §6.2.1
*/
import { MemoryIndex, getMemoryIndex, resetMemoryIndex, tokenize } from './memory-index';
import { MemoryIndex, getMemoryIndex, tokenize } from './memory-index';
// === Types ===
@@ -36,6 +36,7 @@ export interface MemorySearchOptions {
tags?: string[];
limit?: number;
minImportance?: number;
[key: string]: unknown;
}
export interface MemoryStats {

View File

@@ -0,0 +1,460 @@
/**
* Browser Automation Client for ZCLAW
* Provides TypeScript API for Fantoccini-based browser automation
*/
import { invoke } from '@tauri-apps/api/core';
// ============================================================================
// Types
// ============================================================================
export interface BrowserSessionResult {
session_id: string;
}
export interface BrowserSessionInfo {
id: string;
name: string;
current_url: string | null;
title: string | null;
status: string;
created_at: string;
last_activity: string;
}
export interface BrowserNavigationResult {
url: string | null;
title: string | null;
}
export interface BrowserElementInfo {
selector: string;
tag_name: string | null;
text: string | null;
is_displayed: boolean;
is_enabled: boolean;
is_selected: boolean;
location: BrowserElementLocation | null;
size: BrowserElementSize | null;
}
export interface BrowserElementLocation {
x: number;
y: number;
}
export interface BrowserElementSize {
width: number;
height: number;
}
export interface BrowserScreenshotResult {
base64: string;
format: string;
}
export interface FormFieldData {
selector: string;
value: string;
}
// ============================================================================
// Session Management
// ============================================================================
/**
* Create a new browser session
*/
export async function createSession(options?: {
webdriverUrl?: string;
headless?: boolean;
browserType?: 'chrome' | 'firefox' | 'edge' | 'safari';
windowWidth?: number;
windowHeight?: number;
}): Promise<BrowserSessionResult> {
return invoke('browser_create_session', {
webdriverUrl: options?.webdriverUrl,
headless: options?.headless,
browserType: options?.browserType,
windowWidth: options?.windowWidth,
windowHeight: options?.windowHeight,
});
}
/**
* Close a browser session
*/
export async function closeSession(sessionId: string): Promise<void> {
return invoke('browser_close_session', { sessionId });
}
/**
* List all browser sessions
*/
export async function listSessions(): Promise<BrowserSessionInfo[]> {
return invoke('browser_list_sessions');
}
/**
* Get session info
*/
export async function getSession(sessionId: string): Promise<BrowserSessionInfo> {
return invoke('browser_get_session', { sessionId });
}
// ============================================================================
// Navigation
// ============================================================================
/**
* Navigate to URL
*/
export async function navigate(
sessionId: string,
url: string
): Promise<BrowserNavigationResult> {
return invoke('browser_navigate', { sessionId, url });
}
/**
* Go back
*/
export async function back(sessionId: string): Promise<void> {
return invoke('browser_back', { sessionId });
}
/**
* Go forward
*/
export async function forward(sessionId: string): Promise<void> {
return invoke('browser_forward', { sessionId });
}
/**
* Refresh page
*/
export async function refresh(sessionId: string): Promise<void> {
return invoke('browser_refresh', { sessionId });
}
/**
* Get current URL
*/
export async function getCurrentUrl(sessionId: string): Promise<string> {
return invoke('browser_get_url', { sessionId });
}
/**
* Get page title
*/
export async function getTitle(sessionId: string): Promise<string> {
return invoke('browser_get_title', { sessionId });
}
// ============================================================================
// Element Interaction
// ============================================================================
/**
* Find element by CSS selector
*/
export async function findElement(
sessionId: string,
selector: string
): Promise<BrowserElementInfo> {
return invoke('browser_find_element', { sessionId, selector });
}
/**
* Find multiple elements
*/
export async function findElements(
sessionId: string,
selector: string
): Promise<BrowserElementInfo[]> {
return invoke('browser_find_elements', { sessionId, selector });
}
/**
* Click element
*/
export async function click(sessionId: string, selector: string): Promise<void> {
return invoke('browser_click', { sessionId, selector });
}
/**
* Type text into element
*/
export async function typeText(
sessionId: string,
selector: string,
text: string,
clearFirst?: boolean
): Promise<void> {
return invoke('browser_type', { sessionId, selector, text, clearFirst });
}
/**
* Get element text
*/
export async function getText(sessionId: string, selector: string): Promise<string> {
return invoke('browser_get_text', { sessionId, selector });
}
/**
* Get element attribute
*/
export async function getAttribute(
sessionId: string,
selector: string,
attribute: string
): Promise<string | null> {
return invoke('browser_get_attribute', { sessionId, selector, attribute });
}
/**
* Wait for element
*/
export async function waitForElement(
sessionId: string,
selector: string,
timeoutMs?: number
): Promise<BrowserElementInfo> {
return invoke('browser_wait_for_element', {
sessionId,
selector,
timeoutMs: timeoutMs ?? 10000,
});
}
// ============================================================================
// Advanced Operations
// ============================================================================
/**
* Execute JavaScript
*/
export async function executeScript(
sessionId: string,
script: string,
args?: unknown[]
): Promise<unknown> {
return invoke('browser_execute_script', { sessionId, script, args });
}
/**
* Take screenshot
*/
export async function screenshot(sessionId: string): Promise<BrowserScreenshotResult> {
return invoke('browser_screenshot', { sessionId });
}
/**
* Take element screenshot
*/
export async function elementScreenshot(
sessionId: string,
selector: string
): Promise<BrowserScreenshotResult> {
return invoke('browser_element_screenshot', { sessionId, selector });
}
/**
* Get page source
*/
export async function getSource(sessionId: string): Promise<string> {
return invoke('browser_get_source', { sessionId });
}
// ============================================================================
// High-Level Tasks
// ============================================================================
/**
* Scrape page content
*/
export async function scrapePage(
sessionId: string,
selectors: string[],
waitFor?: string,
timeoutMs?: number
): Promise<Record<string, string[]>> {
return invoke('browser_scrape_page', {
sessionId,
selectors,
waitFor,
timeoutMs,
});
}
/**
* Fill form
*/
export async function fillForm(
sessionId: string,
fields: FormFieldData[],
submitSelector?: string
): Promise<void> {
return invoke('browser_fill_form', { sessionId, fields, submitSelector });
}
// ============================================================================
// Browser Client Class (Convenience Wrapper)
// ============================================================================
/**
* High-level browser client for easier usage
*/
export class Browser {
private sessionId: string | null = null;
/**
* Start a new browser session
*/
async start(options?: {
webdriverUrl?: string;
headless?: boolean;
browserType?: 'chrome' | 'firefox' | 'edge' | 'safari';
windowWidth?: number;
windowHeight?: number;
}): Promise<string> {
const result = await createSession(options);
this.sessionId = result.session_id;
return this.sessionId;
}
/**
* Close browser session
*/
async close(): Promise<void> {
if (this.sessionId) {
await closeSession(this.sessionId);
this.sessionId = null;
}
}
/**
* Get current session ID
*/
getSessionId(): string | null {
return this.sessionId;
}
/**
* Navigate to URL
*/
async goto(url: string): Promise<BrowserNavigationResult> {
this.ensureSession();
return navigate(this.sessionId!, url);
}
/**
* Find element
*/
async $(selector: string): Promise<BrowserElementInfo> {
this.ensureSession();
return findElement(this.sessionId!, selector);
}
/**
* Find multiple elements
*/
async $$(selector: string): Promise<BrowserElementInfo[]> {
this.ensureSession();
return findElements(this.sessionId!, selector);
}
/**
* Click element
*/
async click(selector: string): Promise<void> {
this.ensureSession();
return click(this.sessionId!, selector);
}
/**
* Type text
*/
async type(selector: string, text: string, clearFirst = false): Promise<void> {
this.ensureSession();
return typeText(this.sessionId!, selector, text, clearFirst);
}
/**
* Wait for element
*/
async wait(selector: string, timeoutMs = 10000): Promise<BrowserElementInfo> {
this.ensureSession();
return waitForElement(this.sessionId!, selector, timeoutMs);
}
/**
* Take screenshot
*/
async screenshot(): Promise<BrowserScreenshotResult> {
this.ensureSession();
return screenshot(this.sessionId!);
}
/**
* Execute JavaScript
*/
async eval(script: string, args?: unknown[]): Promise<unknown> {
this.ensureSession();
return executeScript(this.sessionId!, script, args);
}
/**
* Get page source
*/
async source(): Promise<string> {
this.ensureSession();
return getSource(this.sessionId!);
}
/**
* Get current URL
*/
async url(): Promise<string> {
this.ensureSession();
return getCurrentUrl(this.sessionId!);
}
/**
* Get page title
*/
async title(): Promise<string> {
this.ensureSession();
return getTitle(this.sessionId!);
}
/**
* Scrape page content
*/
async scrape(
selectors: string[],
waitFor?: string,
timeoutMs?: number
): Promise<Record<string, string[]>> {
this.ensureSession();
return scrapePage(this.sessionId!, selectors, waitFor, timeoutMs);
}
/**
* Fill form
*/
async fillForm(fields: FormFieldData[], submitSelector?: string): Promise<void> {
this.ensureSession();
return fillForm(this.sessionId!, fields, submitSelector);
}
private ensureSession(): void {
if (!this.sessionId) {
throw new Error('Browser session not started. Call start() first.');
}
}
}
// Default export
export default Browser;

View File

@@ -12,13 +12,15 @@ import {
ErrorSeverity,
} from './error-types';
// === Error Store ===
// === Types ===
interface StoredError extends AppError {
export interface StoredError extends AppError {
dismissed: boolean;
reported: boolean;
}
// === Error Store ===
interface ErrorStore {
errors: StoredError[];
addError: (error: AppError) => void;
@@ -52,12 +54,17 @@ function initErrorStore(): void {
errors: [],
addError: (error: AppError) => {
errorStore.errors = [error, ...errorStore.errors];
const storedError: StoredError = {
...error,
dismissed: false,
reported: false,
};
errorStore.errors = [storedError, ...errorStore.errors];
// Notify listeners
notifyErrorListeners(error);
},
dismissError: (id: string) => void {
dismissError(id: string): void {
const error = errorStore.errors.find(e => e.id === id);
if (error) {
errorStore.errors = errorStore.errors.map(e =>
@@ -66,11 +73,11 @@ function initErrorStore(): void {
}
},
dismissAll: () => void {
dismissAll(): void {
errorStore.errors = errorStore.errors.map(e => ({ ...e, dismissed: true }));
},
markReported: (id: string) => void {
markReported(id: string): void {
const error = errorStore.errors.find(e => e.id === id);
if (error) {
errorStore.errors = errorStore.errors.map(e =>
@@ -79,19 +86,19 @@ function initErrorStore(): void {
}
},
getUndismissedErrors: () => StoredError[] => {
getUndismissedErrors(): StoredError[] {
return errorStore.errors.filter(e => !e.dismissed);
},
getErrorCount: () => number => {
getErrorCount(): number {
return errorStore.errors.filter(e => !e.dismissed).length;
},
getErrorsByCategory: (category: ErrorCategory) => StoredError[] => {
getErrorsByCategory(category: ErrorCategory): StoredError[] {
return errorStore.errors.filter(e => e.category === category && !e.dismissed);
},
getErrorsBySeverity: (severity: ErrorSeverity) => StoredError[] => {
getErrorsBySeverity(severity: ErrorSeverity): StoredError[] {
return errorStore.errors.filter(e => e.severity === severity && !e.dismissed);
},
};
@@ -366,8 +373,3 @@ interface ErrorEvent {
reason?: string;
message?: string;
}
export interface StoredError extends AppError {
dismissed: boolean;
reported: boolean;
}

View File

@@ -353,13 +353,15 @@ export function classifyError(error: unknown): AppError {
severity: pattern.severity,
title: pattern.title,
message: pattern.messageTemplate(match),
// Only include name and message, not stack trace (security)
technicalDetails: error instanceof Error
? `${error.name}: ${error.message}\n${error.stack || ''}`
? `${error.name}: ${error.message}`
: String(error),
recoverable: pattern.recoverable,
recoverySteps: pattern.recoverySteps,
timestamp: new Date(),
originalError: error,
// Only preserve original error in development mode
originalError: import.meta.env.DEV ? error : undefined,
};
}
@@ -370,8 +372,9 @@ export function classifyError(error: unknown): AppError {
severity: 'medium',
title: 'An Error Occurred',
message: error instanceof Error ? error.message : 'An unexpected error occurred.',
// Only include name and message, not stack trace (security)
technicalDetails: error instanceof Error
? `${error.name}: ${error.message}\n${error.stack || ''}`
? `${error.name}: ${error.message}`
: String(error),
recoverable: true,
recoverySteps: [
@@ -380,7 +383,8 @@ export function classifyError(error: unknown): AppError {
{ description: 'Contact support with the error details' },
],
timestamp: new Date(),
originalError: error,
// Only preserve original error in development mode
originalError: import.meta.env.DEV ? error : undefined,
};
}

View File

@@ -0,0 +1,82 @@
/**
* 错误处理工具函数
* 提供统一的错误消息提取和静默错误处理
*/
/**
* 从未知错误中提取错误消息
* @param err - 捕获的错误
* @returns 格式化的错误消息字符串
*/
export function getErrorMessage(err: unknown): string {
if (err instanceof Error) return err.message;
if (typeof err === 'string') return err;
if (err && typeof err === 'object' && 'message' in err) {
return String((err as { message: unknown }).message);
}
return 'Unknown error';
}
/**
* 类型守卫:检查是否为 Error 实例
*/
export function isError(err: unknown): err is Error {
return err instanceof Error;
}
/**
* 获取错误的堆栈跟踪(仅开发环境)
*/
export function getErrorStack(err: unknown): string | undefined {
if (import.meta.env.DEV && err instanceof Error) {
return err.stack;
}
return undefined;
}
/**
* 创建静默错误处理器
* 用于 UI 事件处理器中预期的、不需要用户通知的错误
* 在开发环境中会记录警告,生产环境中静默处理
*
* @param context - 上下文名称,用于日志标识
* @returns 错误处理函数
*
* @example
* // 在事件处理器中使用
* onClick={() => { handleSubmit().catch(silentErrorHandler('FeedbackModal')); }}
*/
export function silentErrorHandler(context: string): (err: unknown) => void {
return (err: unknown) => {
if (import.meta.env.DEV) {
console.warn(`[${context}] Operation failed silently:`, getErrorMessage(err));
}
};
}
/**
* 安全执行异步操作,捕获错误并可选地记录
* 用于不阻塞主流程的副作用操作
*
* @param context - 上下文名称
* @param fn - 要执行的异步函数
* @param options - 配置选项
*
* @example
* // 安全执行连接操作
* safeAsync('App', () => connect());
*/
export async function safeAsync<T>(
context: string,
fn: () => Promise<T>,
options: { logInDev?: boolean } = { logInDev: true }
): Promise<T | undefined> {
try {
return await fn();
} catch (err: unknown) {
if (options.logInDev !== false && import.meta.env.DEV) {
console.warn(`[${context}] Async operation failed:`, getErrorMessage(err));
}
return undefined;
}
}

View File

@@ -37,15 +37,31 @@ import {
/**
* Whether to use WSS (WebSocket Secure) instead of WS.
* Set VITE_USE_WSS=true in production environments.
* - Production: defaults to WSS for security
* - Development: defaults to WS for convenience
* - Override: set VITE_USE_WSS=false to force WS in production
*/
const USE_WSS = import.meta.env.VITE_USE_WSS === 'true';
const USE_WSS = import.meta.env.VITE_USE_WSS !== 'false' && import.meta.env.PROD;
/**
* Default protocol based on WSS configuration.
*/
const DEFAULT_WS_PROTOCOL = USE_WSS ? 'wss://' : 'ws://';
/**
* Check if a URL points to localhost.
*/
function isLocalhost(url: string): boolean {
try {
const parsed = new URL(url);
return parsed.hostname === 'localhost' ||
parsed.hostname === '127.0.0.1' ||
parsed.hostname === '[::1]';
} catch {
return false;
}
}
// OpenFang endpoints (actual port is 50051, not 4200)
// Note: REST API uses relative path to leverage Vite proxy for CORS bypass
export const DEFAULT_GATEWAY_URL = `${DEFAULT_WS_PROTOCOL}127.0.0.1:50051/ws`;
@@ -87,7 +103,12 @@ export interface GatewayEvent {
seq?: number;
}
export type GatewayFrame = GatewayRequest | GatewayResponse | GatewayEvent;
export interface GatewayPong {
type: 'pong';
timestamp?: number;
}
export type GatewayFrame = GatewayRequest | GatewayResponse | GatewayEvent | GatewayPong;
function createIdempotencyKey(): string {
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
@@ -119,7 +140,7 @@ export interface AgentStreamDelta {
/** OpenFang WebSocket stream event types */
export interface OpenFangStreamEvent {
type: 'text_delta' | 'phase' | 'response' | 'typing' | 'tool_call' | 'tool_result' | 'hand' | 'workflow' | 'error';
type: 'text_delta' | 'phase' | 'response' | 'typing' | 'tool_call' | 'tool_result' | 'hand' | 'workflow' | 'error' | 'connected' | 'agents_updated';
content?: string;
phase?: 'streaming' | 'done';
state?: 'start' | 'stop';
@@ -136,6 +157,8 @@ export interface OpenFangStreamEvent {
workflow_result?: unknown;
message?: string;
code?: string;
agent_id?: string;
agents?: Array<{ id: string; name: string; status: string }>;
}
export type ConnectionState = 'disconnected' | 'connecting' | 'handshaking' | 'connected' | 'reconnecting';
@@ -481,6 +504,11 @@ export class GatewayClient {
return this.connectRest();
}
// Security warning: non-localhost with insecure WebSocket
if (!this.url.startsWith('wss://') && !isLocalhost(this.url)) {
console.warn('[Gateway] Connecting to non-localhost with insecure WebSocket (ws://). Consider using WSS in production.');
}
this.autoReconnect = true;
this.setState('connecting');
@@ -945,8 +973,57 @@ export class GatewayClient {
privacyOptIn?: boolean;
userName?: string;
userRole?: string;
emoji?: string;
personality?: string;
communicationStyle?: string;
notes?: string;
}): Promise<any> {
return this.restPost('/api/agents', opts);
// Build manifest_toml for OpenClaw Gateway
const lines: string[] = [];
lines.push(`name = "${opts.nickname || opts.name}"`);
lines.push(`model_provider = "bailian"`);
lines.push(`model_name = "${opts.model || 'qwen3.5-plus'}"`);
// Add identity section
lines.push('');
lines.push('[identity]');
if (opts.emoji) {
lines.push(`emoji = "${opts.emoji}"`);
}
if (opts.personality) {
lines.push(`personality = "${opts.personality}"`);
}
if (opts.communicationStyle) {
lines.push(`communication_style = "${opts.communicationStyle}"`);
}
// Add scenarios
if (opts.scenarios && opts.scenarios.length > 0) {
lines.push('');
lines.push('scenarios = [');
opts.scenarios.forEach((s, i) => {
lines.push(` "${s}"${i < opts.scenarios!.length - 1 ? ',' : ''}`);
});
lines.push(']');
}
// Add user context
if (opts.userName || opts.userRole) {
lines.push('');
lines.push('[user_context]');
if (opts.userName) {
lines.push(`name = "${opts.userName}"`);
}
if (opts.userRole) {
lines.push(`role = "${opts.userRole}"`);
}
}
const manifestToml = lines.join('\n');
return this.restPost('/api/agents', {
manifest_toml: manifestToml,
});
}
async updateClone(id: string, updates: Record<string, any>): Promise<any> {
return this.restPut(`/api/agents/${id}`, updates);
@@ -1496,7 +1573,9 @@ export class GatewayClient {
/** Subscribe to agent stream events */
onAgentStream(callback: (delta: AgentStreamDelta) => void): () => void {
return this.on('agent', callback);
return this.on('agent', (payload: unknown) => {
callback(payload as AgentStreamDelta);
});
}
// === Internal ===
@@ -1518,7 +1597,8 @@ export class GatewayClient {
private handleEvent(event: GatewayEvent, connectResolve?: () => void, connectReject?: (error: Error) => void) {
// Handle connect challenge
if (event.event === 'connect.challenge' && this.state === 'handshaking') {
this.performHandshake(event.payload?.nonce, connectResolve, connectReject);
const payload = event.payload as { nonce?: string } | undefined;
this.performHandshake(payload?.nonce || '', connectResolve, connectReject);
return;
}
@@ -1526,7 +1606,12 @@ export class GatewayClient {
this.emitEvent(event.event, event.payload);
}
private async performHandshake(challengeNonce: string, connectResolve?: () => void, connectReject?: (error: Error) => void) {
private async performHandshake(challengeNonce: string | undefined, connectResolve?: () => void, connectReject?: (error: Error) => void) {
if (!challengeNonce) {
this.log('error', 'No challenge nonce received');
connectReject?.(new Error('Handshake failed: no challenge nonce'));
return;
}
const connectId = `connect_${Date.now()}`;
// Use a valid client ID from GATEWAY_CLIENT_ID_SET
// Valid IDs: gateway-client, cli, webchat, node-host, test
@@ -1761,7 +1846,7 @@ export class GatewayClient {
// Don't reconnect immediately, let the next heartbeat check
}, GatewayClient.HEARTBEAT_TIMEOUT);
} catch (error) {
this.log('error', 'Failed to send heartbeat', error);
this.log('error', `Failed to send heartbeat: ${error instanceof Error ? error.message : String(error)}`);
}
}

View File

@@ -187,8 +187,13 @@ class OpenAILLMAdapter implements LLMServiceAdapter {
});
if (!response.ok) {
const error = await response.text();
throw new Error(`[OpenAI] API error: ${response.status} - ${error}`);
const errorBody = await response.text();
// Log full error in development only
if (import.meta.env.DEV) {
console.error('[OpenAI] API error:', errorBody);
}
// Return sanitized error to caller
throw new Error(`[OpenAI] API error: ${response.status} - Request failed`);
}
const data = await response.json();
@@ -247,8 +252,13 @@ class VolcengineLLMAdapter implements LLMServiceAdapter {
});
if (!response.ok) {
const error = await response.text();
throw new Error(`[Volcengine] API error: ${response.status} - ${error}`);
const errorBody = await response.text();
// Log full error in development only
if (import.meta.env.DEV) {
console.error('[Volcengine] API error:', errorBody);
}
// Return sanitized error to caller
throw new Error(`[Volcengine] API error: ${response.status} - Request failed`);
}
const data = await response.json();

View File

@@ -7,8 +7,12 @@
* @module message-virtualization
*/
import { useRef, useCallback, useMemo, useEffect, type React } from 'react';
import { VariableSizeList as List } from 'react-window';
import { useRef, useCallback, useMemo, useEffect, type CSSProperties, type ReactNode } from 'react';
import React from 'react';
import { VariableSizeList } from 'react-window';
// Type alias for convenience
type List = VariableSizeList;
/**
* Message item interface for virtualization
@@ -24,7 +28,7 @@ export interface VirtualizedMessageItem {
*/
export interface VirtualizedMessageListProps {
messages: VirtualizedMessageItem[];
renderMessage: (id: string, style: React.CSSProperties) => React.ReactNode;
renderMessage: (id: string, style: CSSProperties) => ReactNode;
height: number;
width: number | string;
overscan?: number;
@@ -49,7 +53,7 @@ const DEFAULT_HEIGHTS: Record<string, number> = {
*/
export interface UseVirtualizedMessagesReturn {
/** Reference to the VariableSizeList instance */
listRef: React.RefObject<List | null>;
listRef: React.RefObject<VariableSizeList | null>;
/** Get the current height for a message by id and role */
getHeight: (id: string, role: string) => number;
/** Update the measured height for a message */
@@ -388,7 +392,7 @@ export function useMemoizedContent<T>(
cache?: MessageCache<T>
): T {
// Use provided cache or create a default one
const cacheRef = useRef<MessageCache<T>>();
const cacheRef = useRef<MessageCache<T> | undefined>(undefined);
if (!cacheRef.current && !cache) {
cacheRef.current = new MessageCache<T>(200);
}

View File

@@ -0,0 +1,361 @@
/**
* Personality Presets Configuration
*
* Defines personality styles, scenario tags, and emoji presets for Agent onboarding.
* Used by AgentOnboardingWizard to provide guided personality setup.
*/
// === Personality Options ===
export interface PersonalityOption {
id: string;
label: string;
description: string;
icon: string; // Icon name for Lucide
traits: string[];
communicationStyle: string;
}
export const PERSONALITY_OPTIONS: PersonalityOption[] = [
{
id: 'professional',
label: '专业严谨',
description: '精确、可靠、技术导向',
icon: 'Briefcase',
traits: ['精确', '可靠', '技术导向', '系统化'],
communicationStyle: '专业、准确、注重细节,提供技术深度和可操作的建议',
},
{
id: 'friendly',
label: '友好亲切',
description: '温暖、耐心、易于沟通',
icon: 'Heart',
traits: ['温暖', '耐心', '易于沟通', '善解人意'],
communicationStyle: '亲切、耐心、善解人意,用易懂的语言解释复杂概念',
},
{
id: 'creative',
label: '创意灵活',
description: '想象力丰富、善于探索',
icon: 'Sparkles',
traits: ['想象力丰富', '善于探索', '思维开放', '创新'],
communicationStyle: '富有创意、思维开放,鼓励探索新想法和解决方案',
},
{
id: 'concise',
label: '简洁高效',
description: '快速、直接、结果导向',
icon: 'Zap',
traits: ['快速', '直接', '结果导向', '高效'],
communicationStyle: '简洁明了、直奔主题,专注于快速解决问题',
},
];
// === Scenario Tags ===
export interface ScenarioTag {
id: string;
label: string;
description: string;
icon: string; // Icon name for Lucide
keywords: string[];
}
export const SCENARIO_TAGS: ScenarioTag[] = [
{
id: 'coding',
label: '编程开发',
description: '代码编写、调试、代码审查',
icon: 'Code',
keywords: ['编程', '代码', '开发', '调试', 'Bug', '重构'],
},
{
id: 'writing',
label: '内容写作',
description: '文章撰写、文案创作、编辑润色',
icon: 'PenLine',
keywords: ['写作', '文案', '文章', '内容', '编辑', '润色'],
},
{
id: 'product',
label: '产品策划',
description: '产品规划、需求分析、用户研究',
icon: 'Package',
keywords: ['产品', '需求', '用户', '规划', '功能', 'PRD'],
},
{
id: 'data',
label: '数据分析',
description: '数据处理、统计分析、可视化',
icon: 'BarChart',
keywords: ['数据', '分析', '统计', '图表', '可视化', '报表'],
},
{
id: 'design',
label: '设计创意',
description: 'UI/UX设计、视觉设计、原型制作',
icon: 'Palette',
keywords: ['设计', 'UI', 'UX', '视觉', '原型', '界面'],
},
{
id: 'devops',
label: '运维部署',
description: '系统运维、CI/CD、容器化部署',
icon: 'Server',
keywords: ['运维', '部署', 'CI/CD', 'Docker', 'K8s', '服务器'],
},
{
id: 'research',
label: '研究调研',
description: '技术调研、文献研究、竞品分析',
icon: 'Search',
keywords: ['研究', '调研', '分析', '文献', '竞品', '技术'],
},
{
id: 'marketing',
label: '营销推广',
description: '营销策略、内容营销、社媒运营',
icon: 'Megaphone',
keywords: ['营销', '推广', '运营', '社媒', '增长', '转化'],
},
{
id: 'other',
label: '其他',
description: '其他用途或综合场景',
icon: 'MoreHorizontal',
keywords: [],
},
];
// === Emoji Presets ===
export const EMOJI_PRESETS = {
animals: ['🦞', '🐱', '🐶', '🦊', '🐼', '🦁', '🐬', '🦄'],
objects: ['💻', '🚀', '⚡', '🔧', '📚', '🎨', '⭐', '💎'],
expressions: ['😊', '🤓', '😎', '🤖'],
};
export const ALL_EMOJIS = [
...EMOJI_PRESETS.animals,
...EMOJI_PRESETS.objects,
...EMOJI_PRESETS.expressions,
];
// === Quick Start Suggestions ===
export interface QuickStartSuggestion {
icon: string;
text: string;
scenarios: string[]; // Which scenarios this suggestion applies to
}
export const QUICK_START_SUGGESTIONS: QuickStartSuggestion[] = [
{
icon: '💡',
text: '帮我写一个 Python 脚本处理 Excel 文件',
scenarios: ['coding', 'data'],
},
{
icon: '📊',
text: '分析这个数据集的趋势和关键指标',
scenarios: ['data', 'research'],
},
{
icon: '✍️',
text: '帮我起草一份产品需求文档',
scenarios: ['product', 'writing'],
},
{
icon: '🔍',
text: '帮我研究一下这个技术方案的可行性',
scenarios: ['research', 'coding'],
},
{
icon: '🎨',
text: '给我一些 UI 设计的创意建议',
scenarios: ['design'],
},
{
icon: '📝',
text: '帮我写一篇技术博客文章',
scenarios: ['writing'],
},
{
icon: '🚀',
text: '帮我规划一个完整的营销方案',
scenarios: ['marketing', 'product'],
},
{
icon: '⚙️',
text: '帮我配置一个自动化部署流程',
scenarios: ['devops', 'coding'],
},
];
// === Helper Functions ===
/**
* Get personality option by ID
*/
export function getPersonalityById(id: string): PersonalityOption | undefined {
return PERSONALITY_OPTIONS.find((p) => p.id === id);
}
/**
* Get scenario tag by ID
*/
export function getScenarioById(id: string): ScenarioTag | undefined {
return SCENARIO_TAGS.find((s) => s.id === id);
}
/**
* Get quick start suggestions for given scenarios
*/
export function getQuickStartSuggestions(scenarios: string[]): QuickStartSuggestion[] {
if (!scenarios || scenarios.length === 0) {
// Return first 3 general suggestions if no scenarios selected
return QUICK_START_SUGGESTIONS.slice(0, 3);
}
// Filter suggestions that match any of the selected scenarios
const matching = QUICK_START_SUGGESTIONS.filter((s) =>
s.scenarios.some((scenario) => scenarios.includes(scenario))
);
// Return up to 3 matching suggestions, fallback to first 3 if none match
return matching.length > 0 ? matching.slice(0, 3) : QUICK_START_SUGGESTIONS.slice(0, 3);
}
/**
* Generate welcome message based on personality and scenarios
*/
export function generateWelcomeMessage(config: {
userName?: string;
agentName: string;
emoji?: string;
personality?: string;
scenarios?: string[];
}): string {
const { userName, agentName, emoji, personality, scenarios } = config;
// Build greeting
let greeting = '';
if (userName) {
greeting = `你好,${userName}`;
} else {
greeting = '你好!';
}
// Build introduction
let intro = `我是${emoji ? ' ' + emoji : ''} ${agentName}`;
// Add scenario context
if (scenarios && scenarios.length > 0) {
const scenarioLabels = scenarios
.map((id) => getScenarioById(id)?.label)
.filter(Boolean)
.slice(0, 3);
if (scenarioLabels.length > 0) {
intro += `,你的${scenarioLabels.join('、')}助手`;
}
}
// Add personality touch
if (personality) {
const personalityOption = getPersonalityById(personality);
if (personalityOption) {
intro += `。我会以${personalityOption.traits[0]}的方式为你提供帮助`;
}
}
// Add closing
intro += '。有什么我可以帮你的吗?';
return `${greeting}\n\n${intro}`;
}
/**
* Generate SOUL.md content based on personality config
*/
export function generateSoulContent(config: {
agentName: string;
emoji?: string;
personality?: string;
scenarios?: string[];
communicationStyle?: string;
}): string {
const { agentName, emoji, personality, scenarios, communicationStyle } = config;
const personalityOption = personality ? getPersonalityById(personality) : undefined;
const scenarioLabels =
scenarios
?.map((id) => getScenarioById(id)?.label)
.filter(Boolean)
.join('、') || '通用';
return `# ${agentName} 人格
> ${emoji || '🤖'} ${agentName} - ${scenarioLabels}助手
## 核心特质
${
personalityOption
? personalityOption.traits.map((t) => `- ${t}`).join('\n')
: '- 高效执行\n- 专业可靠\n- 主动服务'
}
## 沟通风格
${communicationStyle || personalityOption?.communicationStyle || '简洁、专业、友好'}
## 专业领域
${scenarioLabels}
## 边界
- 安全约束:不执行可能损害用户或系统的操作
- 隐私保护:不主动收集或分享敏感信息
- 能力边界:超出能力范围时坦诚告知
## 语气
- 使用中文进行交流
- 保持专业但友好的态度
- 适时提供额外上下文和建议
`;
}
/**
* Generate USER.md content based on user profile
*/
export function generateUserContent(config: {
userName?: string;
userRole?: string;
scenarios?: string[];
}): string {
const { userName, userRole, scenarios } = config;
const scenarioLabels =
scenarios
?.map((id) => getScenarioById(id)?.label)
.filter(Boolean)
.join('、') || '通用';
const sections: string[] = ['# 用户档案\n'];
if (userName) {
sections.push(`## 基本信息\n\n- 姓名:${userName}`);
if (userRole) {
sections.push(`- 角色:${userRole}`);
}
sections.push('');
}
sections.push(`## 关注领域\n\n${scenarioLabels}\n`);
sections.push(`## 偏好设置\n\n- 语言:中文\n- 沟通风格:直接、高效\n`);
return sections.join('\n');
}

View File

@@ -11,9 +11,8 @@
*/
import { getVikingClient, type VikingHttpClient } from './viking-client';
import { getMemoryManager, type MemoryType } from './agent-memory';
import { getMemoryExtractor } from './memory-extractor';
import { canAutoExecute, executeWithAutonomy } from './autonomy-manager';
import { canAutoExecute } from './autonomy-manager';
// === Types ===
@@ -348,8 +347,8 @@ export class SessionPersistenceService {
metadata: {
startedAt: session.startedAt,
endedAt: new Date().toISOString(),
messageCount: session.messageCount,
agentId: session.agentId,
messageCount: String(session.messageCount || 0),
agentId: session.agentId || 'default',
},
wait: false,
}

View File

@@ -11,6 +11,7 @@ import { useEffect, useRef, useCallback } from 'react';
import { useTeamStore } from '../store/teamStore';
import { useGatewayStore } from '../store/gatewayStore';
import type { TeamEventMessage, TeamEventType, CollaborationEvent } from '../lib/team-client';
import { silentErrorHandler } from './error-utils';
interface UseTeamEventsOptions {
/** Subscribe to specific team only, or null for all teams */
@@ -82,7 +83,7 @@ export function useTeamEvents(options: UseTeamEventsOptions = {}) {
case 'member.added':
case 'member.removed':
// Reload teams to get updated data
loadTeams().catch(() => {});
loadTeams().catch(silentErrorHandler('useTeamEvents'));
break;
}
},

View File

@@ -123,14 +123,16 @@ export class VectorMemoryService {
importance: Math.round((1 - result.score) * 10), // Invert score to importance
createdAt: new Date().toISOString(),
source: 'auto',
tags: result.metadata?.tags ?? [],
tags: (result.metadata as Record<string, unknown>)?.tags ?? [],
lastAccessedAt: new Date().toISOString(),
accessCount: 0,
};
searchResults.push({
memory,
score: result.score,
uri: result.uri,
highlights: result.highlights,
highlights: (result.metadata as Record<string, unknown>)?.highlights as string[] | undefined,
});
}
@@ -155,8 +157,8 @@ export class VectorMemoryService {
): Promise<VectorSearchResult[]> {
// Get the memory content first
const memoryManager = getMemoryManager();
const memories = memoryManager.getByAgent(options?.agentId ?? 'default');
const memory = memories.find(m => m.id === memoryId);
const memories = await memoryManager.getAll(options?.agentId ?? 'default');
const memory = memories.find((m: MemoryEntry) => m.id === memoryId);
if (!memory) {
console.warn(`[VectorMemory] Memory not found: ${memoryId}`);
@@ -192,7 +194,7 @@ export class VectorMemoryService {
clusterCount: number = 5
): Promise<VectorSearchResult[][]> {
const memoryManager = getMemoryManager();
const memories = memoryManager.getByAgent(agentId);
const memories = await memoryManager.getAll(agentId);
if (memories.length === 0) {
return [];