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:
@@ -17,7 +17,6 @@ import {
|
||||
canAutoExecute,
|
||||
executeWithAutonomy,
|
||||
DEFAULT_AUTONOMY_CONFIGS,
|
||||
type ActionType,
|
||||
type AutonomyLevel,
|
||||
} from '../autonomy-manager';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
354
desktop/src/lib/active-learning.ts
Normal file
354
desktop/src/lib/active-learning.ts
Normal 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;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
460
desktop/src/lib/browser-client.ts
Normal file
460
desktop/src/lib/browser-client.ts
Normal 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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
82
desktop/src/lib/error-utils.ts
Normal file
82
desktop/src/lib/error-utils.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
361
desktop/src/lib/personality-presets.ts
Normal file
361
desktop/src/lib/personality-presets.ts
Normal 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');
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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 [];
|
||||
|
||||
Reference in New Issue
Block a user