mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-29 22:23:01 +00:00
* Enhance E2E build process and backend URL handling - Updated `e2e-build.sh` to set CI environment variable correctly for compatibility with various CI runners. - Refactored `backendUrl.ts` to support dynamic backend URL resolution from Vite environment variables, improving flexibility in different deployment contexts. * Implement web channel functionality and enhance chat features - Introduced new web channel commands for sending and canceling chat messages, improving user interaction with the chat system. - Added support for server-sent events (SSE) to handle real-time updates for chat interactions, enhancing responsiveness. - Refactored existing code to improve readability and maintainability, including updates to the core HTTP router and JSON-RPC handling. - Implemented comprehensive end-to-end tests for the web channel flow, ensuring robust functionality and user experience. - Enhanced the overall structure of the web channel module, including event publishing and subscription mechanisms for better scalability. * Refactor socket handling to unify communication across environments - Removed Tauri-specific socket handling from various components, streamlining the codebase to use a single Socket.IO client for both web and desktop environments. - Updated the `useIntelligenceSocket` hook to eliminate Tauri checks and directly utilize the socket service for message sending and chat initialization. - Refactored metadata synchronization in Gmail and Notion services to use the unified socket service, enhancing consistency in socket communication. - Improved error handling and logging across socket interactions to provide clearer feedback during connection issues. - Enhanced the Conversations component to manage socket connection status, ensuring a more robust user experience during chat interactions. * Refactor socket service and enhance core URL resolution - Replaced the deprecated `getBackendUrl` function with `resolveCoreSocketBaseUrl` to improve backend URL resolution for socket connections, accommodating both Tauri and non-Tauri environments. - Introduced `coreSocketBaseFromRpcUrl` to streamline the processing of RPC URLs. - Removed the `tauriSocket.ts` file, consolidating socket handling into a unified service for better maintainability and clarity. - Updated the socket connection logging to provide more informative messages during connection attempts. * Add engineioxide and socketioxide packages to Cargo.lock and update Cargo.toml - Introduced new packages: `engineioxide` and `socketioxide`, both at version 0.15.2, enhancing the project's capabilities. - Updated `Cargo.toml` to include `socketioxide` with the "extensions" feature, ensuring proper integration with the existing codebase. - Added dependencies for both packages, improving modularity and functionality across the application. * Refactor Conversations component and streamline chat handling - Removed unused imports and functions related to Notion context and socket handling, simplifying the Conversations component. - Enhanced error handling during chat interactions, ensuring clearer feedback for users. - Updated chatSend function parameters to eliminate unnecessary context, improving the clarity of the API call. - Consolidated logic for managing chat message history and context, enhancing maintainability and readability of the code. * Enhance core RPC client with improved logging and error handling - Introduced debug logging for socket RPC requests and responses, providing better visibility into the communication process. - Enhanced error handling in the core RPC client to log detailed error messages, improving debugging capabilities. - Updated socket service to log inbound events, ensuring comprehensive tracking of socket interactions. - Removed deprecated MCP handlers from the socket service, streamlining the codebase and improving maintainability. * Refactor apiClient and socketService for improved maintainability - Changed the declaration of `_getToken` from `let` to `var` in `apiClient.ts` to enhance variable scoping. - Removed unused imports in `socketService.ts`, streamlining the code and improving readability. * Refactor deep link handling and enhance E2E testing capabilities - Updated the deep link configuration in `lib.rs` to use a more generalized `#[cfg(desktop)]` directive, improving compatibility across desktop platforms. - Enhanced the `triggerDeepLink` function in `deep-link-helpers.ts` to include macOS-specific app activation and launching logic, ensuring better integration with the macOS environment. - Introduced a new `buildBypassJwt` function to generate bypass JWT tokens for E2E testing, improving the flexibility of authentication flows in tests. - Updated E2E tests in `conversations-web-channel-flow.spec.ts` to utilize the new `triggerAuthDeepLinkBypass` function, enhancing the testing of authentication scenarios. * Enhance deep link handling and E2E test configuration - Updated `triggerAuthDeepLink` in `deep-link-helpers.ts` to support environment-based bypass tokens, improving flexibility in authentication flows during E2E tests. - Modified the `serve_on_ephemeral` function call in `json_rpc_e2e.rs` to disable the core HTTP router's default behavior, enhancing test isolation and control over the testing environment. * Refactor deep link handling and enhance E2E test utilities - Updated `authFlow.e2e.test.tsx` to utilize `window.__simulateDeepLink` for simulating deep links, improving test accuracy and alignment with real-world scenarios. - Modified `lib.rs` to restrict deep link registration to Windows and Linux platforms, clarifying platform-specific behavior. - Enhanced `deep-link-helpers.ts` with a new function to simulate deep links in the WebView, providing a fallback mechanism for E2E tests. - Updated `conversations-web-channel-flow.spec.ts` to reflect changes in deep link triggering, improving logging and test clarity. * Update dependencies in Cargo.lock and Cargo.toml for improved project stability - Removed several unused dependencies from `Cargo.lock` and `Cargo.toml`, including `aes-gcm`, `argon2`, and `async-imap`, streamlining the project and reducing potential security vulnerabilities. - Updated existing dependencies to their latest versions, ensuring compatibility and leveraging improvements in performance and security. - Simplified the dependency structure by consolidating features and removing unnecessary comments, enhancing clarity and maintainability of the configuration files. * Refactor App component and remove unused providers - Removed `AIProvider` and `SkillProvider` from the `App` component, streamlining the component structure and improving readability. - Updated the `UserProvider` to focus solely on bootstrapping the authentication token, enhancing its clarity and purpose. - Adjusted the layout within the `App` component to maintain functionality without the removed providers, ensuring a smooth user experience. * Remove deprecated AI components and types - Deleted the `index.ts`, `loader.ts`, `types.ts`, and related test files from the AI module, streamlining the codebase by removing unused and outdated components. - Eliminated the `default-constitution.md` and associated constitution files, which were no longer relevant to the current architecture. - This cleanup enhances maintainability and reduces complexity within the AI system. * Refactor user data fetching logic and remove legacy tools cache watcher - Enhanced the `useUser` hook to prevent infinite retry loops by implementing a token check with a reference to track the last auto-fetch token. - Removed the legacy file watcher for `TOOLS.md`, transitioning to a core RPC and socket event-based refresh mechanism for tool/config updates. - Updated test state structure to include `isAuthBootstrapComplete` and `channelConnections`, improving test coverage and state management. * Refactor channel module structure and add new providers - Consolidated channel modules under a new `providers` directory, improving organization and clarity. - Introduced new channel providers for DingTalk, Discord, Email, iMessage, IRC, and Lark, expanding the messaging capabilities of the application. - Updated the `mod.rs` file to reflect the new structure and ensure proper module exports, enhancing maintainability and ease of use. * refactor(ui): remove legacy providers/lib logic and stabilize auth bootstrap * chore: apply pre-push auto-fixes
306 lines
8.8 KiB
TypeScript
306 lines
8.8 KiB
TypeScript
import { isTauri as coreIsTauri, invoke } from '@tauri-apps/api/core';
|
|
import debug from 'debug';
|
|
import { io, Socket } from 'socket.io-client';
|
|
|
|
import { SocketIOMCPTransportImpl } from '../lib/mcp';
|
|
import { syncToolsToBackend } from '../lib/skills';
|
|
import { store } from '../store';
|
|
import { upsertChannelConnection } from '../store/channelConnectionsSlice';
|
|
import { resetForUser, setSocketIdForUser, setStatusForUser } from '../store/socketSlice';
|
|
import type { ChannelAuthMode, ChannelConnectionStatus, ChannelType } from '../types/channels';
|
|
import { CORE_RPC_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*');
|
|
}
|
|
|
|
function coreSocketBaseFromRpcUrl(rpcUrl: string): string {
|
|
const trimmed = rpcUrl.trim().replace(/\/+$/, '');
|
|
return trimmed.endsWith('/rpc') ? trimmed.slice(0, -4) : trimmed;
|
|
}
|
|
|
|
async function resolveCoreSocketBaseUrl(): Promise<string> {
|
|
if (!coreIsTauri()) {
|
|
return coreSocketBaseFromRpcUrl(CORE_RPC_URL);
|
|
}
|
|
|
|
try {
|
|
const rpcUrl = await invoke<string>('core_rpc_url');
|
|
return coreSocketBaseFromRpcUrl(String(rpcUrl || CORE_RPC_URL));
|
|
} catch {
|
|
return coreSocketBaseFromRpcUrl(CORE_RPC_URL);
|
|
}
|
|
}
|
|
|
|
interface JwtPayload {
|
|
tgUserId?: string;
|
|
userId?: string;
|
|
sub?: string;
|
|
}
|
|
|
|
interface ChannelConnectionUpdatedEvent {
|
|
channel: ChannelType;
|
|
authMode: ChannelAuthMode;
|
|
status: ChannelConnectionStatus;
|
|
lastError?: string;
|
|
capabilities?: string[];
|
|
}
|
|
|
|
function isChannelConnectionUpdatePayload(value: unknown): value is ChannelConnectionUpdatedEvent {
|
|
if (!value || typeof value !== 'object') return false;
|
|
const obj = value as Record<string, unknown>;
|
|
const channel = obj.channel;
|
|
const authMode = obj.authMode;
|
|
const status = obj.status;
|
|
return (
|
|
(channel === 'telegram' || channel === 'discord') &&
|
|
(authMode === 'managed_dm' ||
|
|
authMode === 'oauth' ||
|
|
authMode === 'bot_token' ||
|
|
authMode === 'api_key') &&
|
|
(status === 'connected' ||
|
|
status === 'connecting' ||
|
|
status === 'disconnected' ||
|
|
status === 'error')
|
|
);
|
|
}
|
|
|
|
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__';
|
|
}
|
|
}
|
|
|
|
class SocketService {
|
|
private socket: Socket | null = null;
|
|
private token: string | null = null;
|
|
private mcpTransport: SocketIOMCPTransportImpl | null = null;
|
|
|
|
/**
|
|
* Connect to the socket server with authentication.
|
|
*/
|
|
connect(token: string): void {
|
|
void this.connectAsync(token);
|
|
}
|
|
|
|
private async connectAsync(token: string): Promise<void> {
|
|
if (!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();
|
|
store.dispatch(setStatusForUser({ userId: uid, status: 'connecting' }));
|
|
|
|
const backendUrl = await resolveCoreSocketBaseUrl();
|
|
socketLog('Connecting to core socket', { userId: uid, backendUrl });
|
|
|
|
// 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);
|
|
this.socket.onAny((event, ...args) => {
|
|
const firstArg = args.length > 0 ? args[0] : undefined;
|
|
socketLog(
|
|
'Inbound event',
|
|
createSafeLogData({ event, argsCount: args.length, hasData: args.length > 0 }, firstArg)
|
|
);
|
|
});
|
|
|
|
// 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' }));
|
|
});
|
|
|
|
this.socket.on('channel:connection-updated', data => {
|
|
if (!isChannelConnectionUpdatePayload(data)) return;
|
|
store.dispatch(
|
|
upsertChannelConnection({
|
|
channel: data.channel,
|
|
authMode: data.authMode,
|
|
patch: {
|
|
status: data.status,
|
|
lastError: data.lastError,
|
|
capabilities: data.capabilities ?? [],
|
|
},
|
|
})
|
|
);
|
|
});
|
|
|
|
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();
|