mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-28 05:12:33 +00:00
* Add JSON-RPC schema definition and HTTP schema endpoint - Introduced a new `schema.json` file containing detailed definitions for various JSON-RPC methods, including their inputs, outputs, and descriptions. - Implemented a new HTTP endpoint `/schema` in the core server to serve the JSON-RPC schema, enhancing API documentation and accessibility. - Updated the core HTTP router to include the new schema route, improving the overall structure and usability of the API. - Enhanced error handling and response formatting in the server to ensure consistent feedback for schema requests. * Update TypeScript configuration and refactor core RPC client - Changed TypeScript target from ES2020 to ESNext and updated library references in `tsconfig.json` for improved compatibility with modern features. - Refactored `coreRpcClient.ts` to enhance JSON-RPC request handling, including the introduction of legacy method aliases and improved error handling. - Updated API service methods in `authApi.ts` and `channelConnectionsApi.ts` to utilize the new core RPC client structure, streamlining authentication and channel connection processes. - Added new utility functions for managing JSON-RPC requests and responses, improving code organization and maintainability. - Enhanced test coverage for the new RPC client methods and refactored existing tests to align with the updated structure. * Enhance Tauri configuration and refactor daemon program arguments - Updated `tauri.conf.json` to include additional macOS infoPlist settings for better application identification and icon management. - Refactored the `daemon_program_args` function in `common.rs` to improve clarity by renaming the parameter to `_exe`, indicating it is unused. This change enhances code readability and maintainability. * Refactor Tauri configuration by removing unused macOS infoPlist settings - Updated `tauri.conf.json` to streamline macOS configuration by removing unnecessary infoPlist entries while retaining essential settings for the application. - This change enhances clarity and maintainability of the Tauri configuration file. * Enhance authentication flow and testing documentation - Introduced a new `isAuthBootstrapComplete` state in the authentication slice to manage the completion of the authentication bootstrap process. - Updated the `UserProvider` to set the `isAuthBootstrapComplete` state based on the authentication status, improving session restoration logic. - Modified route components (`DefaultRedirect`, `ProtectedRoute`, `PublicRoute`) to conditionally render based on the `isAuthBootstrapComplete` state, enhancing user experience during the authentication process. - Added a comprehensive testing guide in `CLAUDE.md`, detailing unit and E2E testing practices, including setup, authoring rules, and a checklist for test coverage. - Updated the `SettingsHome` component to redirect to the home page instead of the login page upon logout, streamlining user navigation. - Enhanced the `LocalModelPanel` to track download progress and manage local AI assets more effectively, improving overall functionality. * Add resolutions for @tauri-apps/api dependency in package.json - Introduced a resolutions field in package.json to enforce the use of @tauri-apps/api version 2.10.1, ensuring compatibility across workspaces. - Updated dependencies in app/package.json to include @tauri-apps/api version 2.10.1, aligning with the new resolution. - Adjusted yarn.lock to reflect the updated version of @tauri-apps/api, enhancing dependency management and consistency. * Refactor Tauri configuration and enhance E2E build process - Updated `tauri.conf.json` to remove unused resource paths, streamlining the configuration for better maintainability. - Modified `wdio.conf.ts` to improve application path resolution for macOS, allowing for multiple bundle base checks to enhance compatibility. - Refactored `e2e-build.sh` to disable updater artifacts for E2E builds and introduced a conditional cargo clean mechanism, improving build efficiency and clarity. * Enhance E2E testing setup and documentation - Updated `CLAUDE.md` to clarify the default behavior of `OPENHUMAN_WORKSPACE` in `e2e-run-spec.sh`, emphasizing automatic creation and cleanup for reproducible E2E runs. - Modified `e2e-run-spec.sh` to implement automatic temporary workspace creation when `OPENHUMAN_WORKSPACE` is not set, improving usability for debugging and testing. - Enhanced cleanup logic in `e2e-run-spec.sh` to ensure proper removal of temporary workspaces after tests, contributing to a cleaner testing environment. * Implement shared mock backend for testing and enhance documentation - Introduced a shared mock backend for unit and integration tests, allowing for deterministic API behavior across app and Rust tests. - Updated `CLAUDE.md` to include detailed instructions on using the shared mock backend, including key admin endpoints and manual run commands. - Modified `package.json` to add scripts for running the mock API server and Rust tests with the mock backend. - Refactored test setup to utilize the new mock backend, improving test reliability and isolation. - Removed obsolete MSW handlers and server setup, streamlining the testing framework. * Enhance authentication state management and testing coverage - Introduced `isAuthBootstrapComplete` state in the authentication slice to track the completion of the authentication process. - Updated `ProtectedRoute` and `PublicRoute` components to utilize the new state, improving user experience during authentication. - Enhanced test cases for `ProtectedRoute` and `PublicRoute` to reflect the updated authentication state structure. - Added a new end-to-end test for the authentication flow, ensuring proper handling of OAuth tokens and session management. - Improved mock setup in tests to better simulate authentication scenarios, enhancing test reliability and coverage. * Enhance README.md with architecture overview and component roles - Added detailed descriptions of the OpenHuman architecture, highlighting the separation of business logic and UI components. - Explained the roles of Rust and the UI in the monorepo, including the use of JSON-RPC, QuickJS, Vite, React, and Tauri. - Documented the structure of controllers and the RPC surface, emphasizing the shared contract for automation and testing. - Provided links to further documentation for architecture, frontend structure, and Tauri commands. * Integrate ServiceBlockingGate component and enhance loading states - Added the `ServiceBlockingGate` component to manage service availability and display appropriate loading screens. - Updated `DefaultRedirect`, `ProtectedRoute`, and `PublicRoute` components to show a loading indicator while authentication bootstrap is in progress. - Implemented timeout handling in `UserProvider` for improved authentication state management. - Introduced tests for `ServiceBlockingGate` to ensure proper rendering and functionality under various service states. * Implement core RPC URL resolution and default hash route handling - Added a function to resolve the core RPC URL based on the environment, improving flexibility for Tauri and non-Tauri contexts. - Introduced a default hash route handler in the main application entry point to ensure proper navigation behavior. - Updated the core RPC command to expose the resolved RPC URL, enhancing the integration with the frontend. - Refactored the core RPC client to utilize the new URL resolution logic, ensuring consistent API calls. * Refactor backend URL usage to API_BASE_URL - Replaced all instances of BACKEND_URL with API_BASE_URL across various components and services to standardize API endpoint references. - Updated OAuth provider configurations, settings panels, hooks, and services to ensure consistent API calls. - Enhanced test setups to reflect the new API_BASE_URL, improving test reliability and alignment with the updated configuration. * Implement RouteLoadingScreen for improved loading states - Introduced a new `RouteLoadingScreen` component to provide a consistent loading experience across various routes. - Updated `DefaultRedirect`, `ProtectedRoute`, and `PublicRoute` components to utilize `RouteLoadingScreen` while waiting for authentication bootstrap completion. - Enhanced `MiniSidebar` to hide on additional public/setup routes, improving user navigation experience. - Refactored `UserProvider` to streamline authentication state management by removing unnecessary references. * Add feature design workflow section to CLAUDE.md - Introduced a comprehensive workflow for feature design, outlining steps from specification to UI implementation and testing. - Emphasized the importance of grounding designs in existing codebases and defined planning rules for E2E scenarios. - Provided detailed instructions for implementing features in Rust, conducting JSON-RPC tests, and building UI components in the Tauri app. * Refactor backend URL handling and improve OAuth flow - Replaced static API_BASE_URL references with dynamic backend URL resolution across various components and services, enhancing flexibility for Tauri and non-Tauri environments. - Updated OAuth provider configurations to utilize the new backend URL logic, ensuring consistent login URL generation. - Refactored API client and socket service to fetch the backend URL dynamically, improving reliability in different deployment contexts. - Introduced a new service for resolving the backend URL, streamlining the configuration and enhancing test setups. * Add debug logging guidelines to CLAUDE.md - Introduced comprehensive guidelines for implementing development-oriented debug logging in both Rust and the app. - Emphasized the importance of logging at appropriate levels (`debug`/`trace`) and following existing patterns for consistency. - Provided instructions on avoiding sensitive information in logs and ensuring terminal output is grep-friendly for easier debugging during development. * Enhance service management with new mock functionality and E2E tests - Added a mock service manager to facilitate deterministic service behavior during end-to-end tests, enabling better simulation of service states. - Introduced new buttons in the `ServiceBlockingGate` component for restarting and uninstalling services, improving user control over service management. - Implemented a comprehensive E2E test suite for the service connectivity flow, covering installation, starting, stopping, restarting, and uninstalling services. - Updated package scripts to include a new E2E test for service connectivity, enhancing testing coverage and reliability. - Refactored service operations to support mock functionality, ensuring consistent behavior across testing and production environments. * Enhance ServiceBlockingGate with improved logging and periodic health polling - Introduced periodic health polling in the `ServiceBlockingGate` component to refresh service status every 3 seconds, enhancing responsiveness to service state changes. - Added detailed logging for various operations, including service status checks and error handling, to improve traceability and debugging. - Updated E2E tests to include logging steps for better visibility during service connectivity flow tests. - Refactored error handling to ensure consistent logging of error messages across service operations. * Refactor E2E testing scripts and enhance CLAUDE.md documentation - Updated paths in CLAUDE.md to reflect the new location of the E2E run script, ensuring accurate instructions for running tests. - Removed outdated E2E test scripts from package.json and migrated relevant functionality to app/package.json for better organization. - Introduced new E2E testing scripts for specific flows (auth, login, payment, etc.) to streamline testing processes and improve modularity. - Added a script to run all E2E flows sequentially, enhancing test coverage and simplifying execution. - Improved documentation for E2E testing procedures in CLAUDE.md, providing clearer guidance for developers. * Refactor imports and enhance code readability - Removed duplicate import of `ServiceBlockingGate` in `App.tsx` for cleaner code. - Improved readability in `MiniSidebar.tsx` by formatting conditional statements. - Reordered imports in `PublicRoute.tsx` for consistency. - Enhanced formatting in `ServiceBlockingGate.tsx` for better clarity in asynchronous operations. - Streamlined import statements in various test files and components for improved organization. - Updated `LocalModelPanel.tsx` to enhance button disable logic readability. - Refactored CORS headers in `jsonrpc.rs` for better formatting. * ci: align build and test workflows with app workspace e2e * ci: align release and typecheck workflows with current workspace * Enhance ServiceBlockingGate functionality with improved refresh options - Introduced a new `RefreshOptions` type to customize the behavior of the `refreshStatus` function, allowing for conditional error clearing and status checking. - Updated the `refreshStatus` function to utilize the new options, enhancing control over service state updates. - Modified the button click handler to pass the new options, improving user experience during service refresh operations. - Adjusted state updates to prevent unnecessary re-renders and maintain consistency in service state management. * Refactor RefreshOptions type for improved clarity in ServiceBlockingGate - Consolidated the definition of the `RefreshOptions` type into a single line for better readability. - Maintained existing functionality while enhancing code clarity in the `ServiceBlockingGate` component.
380 lines
11 KiB
TypeScript
380 lines
11 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 { getBackendUrl } from '../services/backendUrl';
|
|
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 { 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;
|
|
}
|
|
|
|
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__';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 {
|
|
void this.connectAsync(token);
|
|
}
|
|
|
|
private async connectAsync(token: string): Promise<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();
|
|
store.dispatch(setStatusForUser({ userId: uid, status: 'connecting' }));
|
|
|
|
const backendUrl = await getBackendUrl();
|
|
socketLog('Connecting', { 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);
|
|
|
|
// 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 ?? [],
|
|
},
|
|
})
|
|
);
|
|
});
|
|
|
|
// 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();
|