mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
Replaces scattered environment checks with the centralized `IS_DEV` constant from `config.ts`. This improves consistency and simplifies maintenance by reducing redundancy across multiple files.
333 lines
9.8 KiB
TypeScript
333 lines
9.8 KiB
TypeScript
import { isTauri as coreIsTauri } from '@tauri-apps/api/core';
|
|
import debug from 'debug';
|
|
import { io, Socket } from 'socket.io-client';
|
|
|
|
import { MCPTool, MCPToolCall, SocketIOMCPTransportImpl } from '../lib/mcp';
|
|
import { skillManager, syncToolsToBackend } from '../lib/skills';
|
|
import { store } from '../store';
|
|
import { resetForUser, setSocketIdForUser, setStatusForUser } from '../store/socketSlice';
|
|
import { BACKEND_URL, IS_DEV } from '../utils/config';
|
|
import { createSafeLogData, sanitizeError } from '../utils/sanitize';
|
|
|
|
// Socket service logger using debug package
|
|
// Enable logging by setting DEBUG=socket* in environment or localStorage
|
|
const socketLog = debug('socket');
|
|
const socketWarn = debug('socket:warn');
|
|
const socketError = debug('socket:error');
|
|
|
|
// Enable socket logging in development by default
|
|
if (IS_DEV) {
|
|
debug.enable('socket*');
|
|
}
|
|
|
|
interface JwtPayload {
|
|
tgUserId?: string;
|
|
userId?: string;
|
|
sub?: string;
|
|
}
|
|
|
|
function getSocketUserId(): string {
|
|
const token = store.getState().auth.token;
|
|
if (!token) return '__pending__';
|
|
|
|
try {
|
|
const parts = token.split('.');
|
|
if (parts.length !== 3) return '__pending__';
|
|
|
|
const payloadBase64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
|
|
const payloadJson = atob(payloadBase64);
|
|
const payload = JSON.parse(payloadJson) as JwtPayload;
|
|
|
|
const id = payload.tgUserId || payload.userId || payload.sub;
|
|
return id || '__pending__';
|
|
} catch {
|
|
return '__pending__';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if running in Tauri (where Rust handles Socket.io).
|
|
* When true, this service should NOT create its own socket connection
|
|
* because the Rust-native SocketManager handles the connection and MCP.
|
|
*/
|
|
function isRustSocketMode(): boolean {
|
|
try {
|
|
return coreIsTauri();
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
class SocketService {
|
|
private socket: Socket | null = null;
|
|
private token: string | null = null;
|
|
private mcpTransport: SocketIOMCPTransportImpl | null = null;
|
|
|
|
/**
|
|
* Connect to the socket server with authentication.
|
|
*
|
|
* NOTE: In Tauri mode, this is a NO-OP. The Rust-native SocketManager
|
|
* handles the connection. The frontend calls `connectRustSocket()` instead.
|
|
*/
|
|
connect(token: string): void {
|
|
if (!token) return;
|
|
|
|
// In Tauri mode, Rust handles the socket connection.
|
|
// Don't create a duplicate frontend socket.
|
|
if (isRustSocketMode()) {
|
|
socketLog('Skipping frontend socket — Rust SocketManager handles connection');
|
|
this.token = token;
|
|
return;
|
|
}
|
|
|
|
// Don't connect if already connected with the same token
|
|
if (this.socket?.connected && this.token === token) return;
|
|
|
|
// Disconnect existing connection if token changed or socket exists
|
|
if (this.socket) {
|
|
if (this.token !== token) {
|
|
this.disconnect();
|
|
} else if (this.socket.connected) {
|
|
return;
|
|
} else if (!this.socket.disconnected) {
|
|
// Socket is connecting, wait for it
|
|
return;
|
|
}
|
|
}
|
|
|
|
this.token = token;
|
|
const uid = getSocketUserId();
|
|
|
|
socketLog('Connecting', { userId: uid, backendUrl: BACKEND_URL });
|
|
|
|
store.dispatch(setStatusForUser({ userId: uid, status: 'connecting' }));
|
|
|
|
const backendUrl = BACKEND_URL;
|
|
|
|
// Ensure we're not connecting to the wrong URL
|
|
if (backendUrl.includes('localhost:1420') || backendUrl.includes(':1420')) {
|
|
return;
|
|
}
|
|
|
|
const socketOptions = {
|
|
auth: { token },
|
|
path: '/socket.io/',
|
|
transports: ['websocket', 'polling'] as ('websocket' | 'polling')[],
|
|
reconnection: true,
|
|
reconnectionDelay: 1000,
|
|
reconnectionAttempts: 5,
|
|
forceNew: true,
|
|
timeout: 2000,
|
|
upgrade: true,
|
|
query: {},
|
|
};
|
|
|
|
this.socket = io(backendUrl, socketOptions);
|
|
|
|
// Initialize MCP transport for client→server MCP requests
|
|
this.mcpTransport = new SocketIOMCPTransportImpl(this.socket);
|
|
|
|
// Connection event handlers
|
|
this.socket.on('connect', () => {
|
|
const socketId = this.socket?.id || null;
|
|
const uid = getSocketUserId();
|
|
socketLog('Connected', { socketId, userId: uid });
|
|
store.dispatch(setStatusForUser({ userId: uid, status: 'connected' }));
|
|
store.dispatch(setSocketIdForUser({ userId: uid, socketId }));
|
|
syncToolsToBackend();
|
|
});
|
|
|
|
this.socket.on('ready', () => {
|
|
const uid = getSocketUserId();
|
|
socketLog('Server ready - authentication successful', { userId: uid });
|
|
});
|
|
|
|
this.socket.on('error', (error: unknown) => {
|
|
const uid = getSocketUserId();
|
|
socketError('Server error', { userId: uid, error: sanitizeError(error) });
|
|
});
|
|
|
|
this.socket.on('disconnect', (reason: string) => {
|
|
const uid = getSocketUserId();
|
|
socketLog('Disconnected', { userId: uid, reason });
|
|
store.dispatch(setStatusForUser({ userId: uid, status: 'disconnected' }));
|
|
store.dispatch(setSocketIdForUser({ userId: uid, socketId: null }));
|
|
});
|
|
|
|
this.socket.on('connect_error', (error: Error) => {
|
|
const uid = getSocketUserId();
|
|
socketError('Connection error', { userId: uid, error: sanitizeError(error) });
|
|
store.dispatch(setStatusForUser({ userId: uid, status: 'disconnected' }));
|
|
});
|
|
|
|
// MCP handlers — only in web mode (Rust handles MCP in Tauri mode)
|
|
this.socket.on('mcp:listTools', (data: { requestId: string }) => {
|
|
socketLog('MCP list tools request', { requestId: data.requestId });
|
|
|
|
// Aggregate tools from all ready skills
|
|
const skillsState = store.getState().skills.skills;
|
|
const allTools: MCPTool[] = [];
|
|
|
|
for (const [skillId, skill] of Object.entries(skillsState)) {
|
|
if (skill.status === 'ready' && skill.tools?.length) {
|
|
for (const tool of skill.tools) {
|
|
allTools.push({
|
|
name: `${skillId}__${tool.name}`,
|
|
description: tool.description,
|
|
inputSchema: tool.inputSchema,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
socketLog('MCP list tools response', {
|
|
requestId: data.requestId,
|
|
toolCount: allTools.length,
|
|
});
|
|
|
|
this.socket?.emit('mcp:listToolsResponse', { requestId: data.requestId, tools: allTools });
|
|
});
|
|
|
|
this.socket.on('mcp:toolCall', async (data: { requestId: string; toolCall: MCPToolCall }) => {
|
|
const { requestId, toolCall } = data;
|
|
socketLog('MCP tool call', createSafeLogData({ requestId, toolName: toolCall?.name }, data));
|
|
|
|
const separatorIdx = toolCall.name.indexOf('__');
|
|
if (separatorIdx === -1) {
|
|
socketError('MCP tool call - invalid tool name format', { requestId, name: toolCall.name });
|
|
this.socket?.emit('mcp:toolCallResponse', {
|
|
requestId,
|
|
result: {
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
text: `Invalid tool name: ${toolCall.name}. Expected format: skillId__toolName`,
|
|
},
|
|
],
|
|
isError: true,
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
const skillId = toolCall.name.substring(0, separatorIdx);
|
|
const toolName = toolCall.name.substring(separatorIdx + 2);
|
|
|
|
try {
|
|
const result = await skillManager.callTool(skillId, toolName, toolCall.arguments);
|
|
|
|
socketLog('MCP tool call success', { requestId, skillId, toolName });
|
|
|
|
this.socket?.emit('mcp:toolCallResponse', { requestId, result });
|
|
} catch (err) {
|
|
const msg = err instanceof Error ? err.message : String(err);
|
|
socketError('MCP tool call failed', {
|
|
requestId,
|
|
skillId,
|
|
toolName,
|
|
error: sanitizeError(err),
|
|
});
|
|
this.socket?.emit('mcp:toolCallResponse', {
|
|
requestId,
|
|
result: { content: [{ type: 'text', text: msg }], isError: true },
|
|
});
|
|
}
|
|
});
|
|
|
|
this.socket.connect();
|
|
}
|
|
|
|
/**
|
|
* Disconnect from the socket server
|
|
*/
|
|
disconnect(): void {
|
|
if (this.socket) {
|
|
const uid = getSocketUserId();
|
|
socketLog('Disconnecting', { userId: uid });
|
|
this.socket.disconnect();
|
|
this.socket = null;
|
|
this.token = null;
|
|
this.mcpTransport = null;
|
|
store.dispatch(resetForUser({ userId: uid }));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the current socket instance
|
|
*/
|
|
getSocket(): Socket | null {
|
|
return this.socket;
|
|
}
|
|
|
|
/**
|
|
* Get the MCP transport for making client→server MCP requests
|
|
*/
|
|
getMCPTransport(): SocketIOMCPTransportImpl | null {
|
|
return this.mcpTransport;
|
|
}
|
|
|
|
/**
|
|
* Check if socket is connected
|
|
*/
|
|
isConnected(): boolean {
|
|
return this.socket?.connected || false;
|
|
}
|
|
|
|
/**
|
|
* Emit an event to the server
|
|
*/
|
|
emit(event: string, data?: unknown): void {
|
|
if (this.socket?.connected) {
|
|
socketLog('Emitting event', createSafeLogData({ event }, data));
|
|
this.socket.emit(event, data);
|
|
} else {
|
|
socketWarn('Cannot emit event - socket not connected', { event });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Listen to an event from the server
|
|
*/
|
|
on(event: string, callback: (...args: unknown[]) => void): void {
|
|
if (this.socket) {
|
|
const wrappedCallback = (...args: unknown[]) => {
|
|
socketLog('Received event', { event, argsCount: args.length, hasData: args.length > 0 });
|
|
callback(...args);
|
|
};
|
|
this.socket.on(event, wrappedCallback);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove an event listener
|
|
*/
|
|
off(event: string, callback?: (...args: unknown[]) => void): void {
|
|
if (this.socket) {
|
|
if (callback) {
|
|
this.socket.off(event, callback);
|
|
} else {
|
|
this.socket.off(event);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Listen to an event once
|
|
*/
|
|
once(event: string, callback: (...args: unknown[]) => void): void {
|
|
if (this.socket) {
|
|
const wrappedCallback = (...args: unknown[]) => {
|
|
socketLog('Received event (once)', {
|
|
event,
|
|
argsCount: args.length,
|
|
hasData: args.length > 0,
|
|
});
|
|
callback(...args);
|
|
};
|
|
this.socket.once(event, wrappedCallback);
|
|
}
|
|
}
|
|
}
|
|
|
|
export const socketService = new SocketService();
|