cc工作前备份

This commit is contained in:
iven
2026-03-12 00:23:42 +08:00
parent f75a2b798b
commit ef849c62ab
98 changed files with 12110 additions and 568 deletions

View File

@@ -0,0 +1,408 @@
/**
* ZCLAW Gateway Client (Browser/Tauri side)
*
* WebSocket client for OpenClaw Gateway protocol, designed to run
* in the Tauri React frontend. Uses native browser WebSocket API.
*/
// === Protocol Types ===
export interface GatewayRequest {
type: 'req';
id: string;
method: string;
params?: Record<string, any>;
}
export interface GatewayResponse {
type: 'res';
id: string;
ok: boolean;
payload?: any;
error?: any;
}
export interface GatewayEvent {
type: 'event';
event: string;
payload?: any;
seq?: number;
}
export type GatewayFrame = GatewayRequest | GatewayResponse | GatewayEvent;
export interface AgentStreamDelta {
stream: 'assistant' | 'tool' | 'lifecycle';
delta?: string;
content?: string;
tool?: string;
toolInput?: string;
toolOutput?: string;
phase?: 'start' | 'end' | 'error';
runId?: string;
error?: string;
}
export type ConnectionState = 'disconnected' | 'connecting' | 'handshaking' | 'connected' | 'reconnecting';
type EventCallback = (payload: any) => void;
// === Client ===
export class GatewayClient {
private ws: WebSocket | null = null;
private state: ConnectionState = 'disconnected';
private requestId = 0;
private pendingRequests = new Map<string, {
resolve: (value: any) => void;
reject: (reason: any) => void;
timer: number;
}>();
private eventListeners = new Map<string, Set<EventCallback>>();
private reconnectAttempts = 0;
private reconnectTimer: number | null = null;
private deviceId: string;
// Options
private url: string;
private token: string;
private autoReconnect: boolean;
private reconnectInterval: number;
private requestTimeout: number;
// State change callbacks
onStateChange?: (state: ConnectionState) => void;
onLog?: (level: string, message: string) => void;
constructor(opts?: {
url?: string;
token?: string;
autoReconnect?: boolean;
reconnectInterval?: number;
requestTimeout?: number;
}) {
this.url = opts?.url || 'ws://127.0.0.1:18789';
this.token = opts?.token || '';
this.autoReconnect = opts?.autoReconnect ?? true;
this.reconnectInterval = opts?.reconnectInterval || 3000;
this.requestTimeout = opts?.requestTimeout || 30000;
this.deviceId = crypto.randomUUID?.() || `zclaw_${Date.now().toString(36)}`;
}
getState(): ConnectionState {
return this.state;
}
// === Connection ===
connect(): Promise<void> {
if (this.state === 'connected' || this.state === 'connecting') {
return Promise.resolve();
}
this.setState('connecting');
return new Promise((resolve, reject) => {
try {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
this.setState('handshaking');
};
this.ws.onmessage = (evt) => {
try {
const frame: GatewayFrame = JSON.parse(evt.data);
this.handleFrame(frame, resolve);
} catch (err: any) {
this.log('error', `Parse error: ${err.message}`);
}
};
this.ws.onclose = (evt) => {
const wasConnected = this.state === 'connected';
this.cleanup();
if (wasConnected && this.autoReconnect) {
this.scheduleReconnect();
}
this.emitEvent('close', { code: evt.code, reason: evt.reason });
};
this.ws.onerror = () => {
if (this.state === 'connecting') {
this.cleanup();
reject(new Error('WebSocket connection failed'));
}
};
} catch (err) {
this.cleanup();
reject(err);
}
});
}
disconnect() {
this.autoReconnect = false;
this.cancelReconnect();
if (this.ws) {
this.ws.close(1000, 'Client disconnect');
}
this.cleanup();
}
// === Request/Response ===
async request(method: string, params?: Record<string, any>): Promise<any> {
if (this.state !== 'connected') {
throw new Error(`Not connected (state: ${this.state})`);
}
const id = `req_${++this.requestId}`;
const frame: GatewayRequest = { type: 'req', id, method, params };
return new Promise((resolve, reject) => {
const timer = window.setTimeout(() => {
this.pendingRequests.delete(id);
reject(new Error(`Request ${method} timed out`));
}, this.requestTimeout);
this.pendingRequests.set(id, { resolve, reject, timer });
this.send(frame);
});
}
// === High-level API ===
/** Send message to agent, returns { runId, acceptedAt } */
async chat(message: string, opts?: { sessionKey?: string; model?: string }): Promise<{ runId: string; acceptedAt: string }> {
return this.request('agent', {
message,
sessionKey: opts?.sessionKey,
model: opts?.model,
});
}
/** Get Gateway health info */
async health(): Promise<any> {
return this.request('health');
}
/** Get Gateway status */
async status(): Promise<any> {
return this.request('status');
}
// ZCLAW custom methods
async listClones(): Promise<any> { return this.request('zclaw.clones.list'); }
async createClone(opts: { name: string; role?: string; scenarios?: string[] }): Promise<any> { return this.request('zclaw.clones.create', opts); }
async updateClone(id: string, updates: Record<string, any>): Promise<any> { return this.request('zclaw.clones.update', { id, updates }); }
async deleteClone(id: string): Promise<any> { return this.request('zclaw.clones.delete', { id }); }
async getUsageStats(): Promise<any> { return this.request('zclaw.stats.usage'); }
async getSessionStats(): Promise<any> { return this.request('zclaw.stats.sessions'); }
async getWorkspaceInfo(): Promise<any> { return this.request('zclaw.workspace.info'); }
async getPluginStatus(): Promise<any> { return this.request('zclaw.plugins.status'); }
async getQuickConfig(): Promise<any> { return this.request('zclaw.config.quick', { get: true }); }
async saveQuickConfig(config: Record<string, any>): Promise<any> { return this.request('zclaw.config.quick', config); }
async listChannels(): Promise<any> { return this.request('channels.list'); }
async getFeishuStatus(): Promise<any> { return this.request('feishu.status'); }
async listScheduledTasks(): Promise<any> { return this.request('heartbeat.tasks'); }
// === Event Subscription ===
/** Subscribe to a Gateway event (e.g., 'agent', 'chat', 'heartbeat') */
on(event: string, callback: EventCallback): () => void {
if (!this.eventListeners.has(event)) {
this.eventListeners.set(event, new Set());
}
this.eventListeners.get(event)!.add(callback);
// Return unsubscribe function
return () => {
this.eventListeners.get(event)?.delete(callback);
};
}
/** Subscribe to agent stream events */
onAgentStream(callback: (delta: AgentStreamDelta) => void): () => void {
return this.on('agent', callback);
}
// === Internal ===
private handleFrame(frame: GatewayFrame, connectResolve?: (value: void) => void) {
if (frame.type === 'event') {
this.handleEvent(frame, connectResolve);
} else if (frame.type === 'res') {
this.handleResponse(frame);
}
}
private handleEvent(event: GatewayEvent, connectResolve?: (value: void) => void) {
// Handle connect challenge
if (event.event === 'connect.challenge' && this.state === 'handshaking') {
this.performHandshake(event.payload?.nonce, connectResolve);
return;
}
// Dispatch to listeners
this.emitEvent(event.event, event.payload);
}
private performHandshake(_nonce: string, connectResolve?: (value: void) => void) {
const connectId = `connect_${Date.now()}`;
const connectReq: GatewayRequest = {
type: 'req',
id: connectId,
method: 'connect',
params: {
minProtocol: 3,
maxProtocol: 3,
client: {
id: 'zclaw-tauri',
version: '0.2.0',
platform: this.detectPlatform(),
mode: 'operator',
},
role: 'operator',
scopes: ['operator.read', 'operator.write'],
auth: this.token ? { token: this.token } : {},
locale: 'zh-CN',
userAgent: 'zclaw-tauri/0.2.0',
device: { id: this.deviceId },
},
};
// Temporarily intercept the connect response
const originalHandler = this.ws!.onmessage;
this.ws!.onmessage = (evt) => {
try {
const frame = JSON.parse(evt.data);
if (frame.type === 'res' && frame.id === connectId) {
// Restore normal message handler
this.ws!.onmessage = originalHandler;
if (frame.ok) {
this.setState('connected');
this.reconnectAttempts = 0;
this.emitEvent('connected', frame.payload);
this.log('info', 'Connected to Gateway');
connectResolve?.();
} else {
this.log('error', `Handshake failed: ${JSON.stringify(frame.error)}`);
this.cleanup();
}
} else {
// Pass through non-connect frames
originalHandler?.call(this.ws!, evt);
}
} catch { /* ignore */ }
};
this.send(connectReq);
}
private handleResponse(res: GatewayResponse) {
const pending = this.pendingRequests.get(res.id);
if (pending) {
clearTimeout(pending.timer);
this.pendingRequests.delete(res.id);
if (res.ok) {
pending.resolve(res.payload);
} else {
pending.reject(new Error(JSON.stringify(res.error)));
}
}
}
private send(frame: GatewayFrame) {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(frame));
}
}
private emitEvent(event: string, payload: any) {
const listeners = this.eventListeners.get(event);
if (listeners) {
for (const cb of listeners) {
try { cb(payload); } catch { /* ignore listener errors */ }
}
}
// Also emit wildcard
const wildcardListeners = this.eventListeners.get('*');
if (wildcardListeners) {
for (const cb of wildcardListeners) {
try { cb({ event, payload }); } catch { /* ignore */ }
}
}
}
private setState(state: ConnectionState) {
this.state = state;
this.onStateChange?.(state);
this.emitEvent('state', state);
}
private cleanup() {
for (const [, pending] of this.pendingRequests) {
clearTimeout(pending.timer);
pending.reject(new Error('Connection closed'));
}
this.pendingRequests.clear();
if (this.ws) {
this.ws.onopen = null;
this.ws.onmessage = null;
this.ws.onclose = null;
this.ws.onerror = null;
if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
try { this.ws.close(); } catch { /* ignore */ }
}
this.ws = null;
}
this.setState('disconnected');
}
private scheduleReconnect() {
this.reconnectAttempts++;
this.setState('reconnecting');
const delay = Math.min(this.reconnectInterval * Math.pow(1.5, this.reconnectAttempts - 1), 30000);
this.reconnectTimer = window.setTimeout(async () => {
try {
await this.connect();
} catch { /* close handler will trigger another reconnect */ }
}, delay);
}
private cancelReconnect() {
if (this.reconnectTimer !== null) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
}
private detectPlatform(): string {
const ua = navigator.userAgent.toLowerCase();
if (ua.includes('win')) return 'windows';
if (ua.includes('mac')) return 'macos';
return 'linux';
}
private log(level: string, message: string) {
this.onLog?.(level, message);
}
}
// Singleton instance
let _client: GatewayClient | null = null;
export function getGatewayClient(opts?: ConstructorParameters<typeof GatewayClient>[0]): GatewayClient {
if (!_client) {
_client = new GatewayClient(opts);
}
return _client;
}