Перейти к основному содержимому

Добавление IPC-маршрутов

Это руководство описывает полный процесс разработки IPC-маршрута — от определения типов до фронтенд-хука — на примере гипотетической функции «Избранное».

Обзор архитектуры коммуникации

Renderer Process (React)
| window.api.favorites.list()
v
Preload Script (contextBridge)
| ipcRenderer.invoke('favorites:list', authToken)
v
Main Process Router (BaseRouter.secureHandle)
| auth validation → param validation (Zod)
v
Main Process Service
| business logic → database operations
v
Return result → Preload → Renderer

Каждый IPC-вызов автоматически передаёт authToken, который secureHandle проверяет централизованно — вам никогда не нужно обрабатывать аутентификацию вручную.


Шаг 1: Определение общих типов

Файл: packages/desktop/app/shared/contracts/favorites-types.ts

export interface Favorite {
id: string;
sessionId: string;
messageId: string;
note?: string;
createdAt: string;
}

export interface CreateFavoriteInput {
sessionId: string;
messageId: string;
note?: string;
}

export interface UpdateFavoriteInput {
note?: string;
}

Общие типы размещаются в @shared/contracts/. Как фронтенд, так и бэкенд могут импортировать их через @shared/contracts/favorites-types, что обеспечивает согласованность типов на обеих сторонах границы.


Шаг 2: Создание сервиса бэкенда

Файл: packages/desktop/app/main/services/content/favorites/FavoritesService.ts

import type { DbRpc } from '../../workers/types';
import type {
CreateFavoriteInput,
Favorite,
UpdateFavoriteInput,
} from '@shared/contracts/favorites-types';

export class FavoritesService {
constructor(private readonly db: DbRpc) {}

async list(sessionId?: string): Promise<Favorite[]> {
return this.db.favorites_list({ sessionId });
}

async create(input: CreateFavoriteInput): Promise<Favorite> {
return this.db.favorites_create(input);
}

async update(id: string, input: UpdateFavoriteInput): Promise<Favorite> {
return this.db.favorites_update({ id, ...input });
}

async delete(id: string): Promise<void> {
return this.db.favorites_delete({ id });
}
}

:::warning Важно Все обращения к внешним API, операции с файловой системой и доступ к базе данных должны выполняться в слое Service. Фронтенд не должен обращаться к ним напрямую. :::


Шаг 3: Создание Router

Файл: packages/desktop/app/main/services/routers/FavoritesRouter.ts

import { z } from 'zod';
import { secureHandle } from '../../ipc/safe-handle';
import type { AuthService } from '../platform/auth';
import type { FavoritesService } from '../../content/favorites/FavoritesService';

export class FavoritesRouter {
constructor(
private readonly auth: AuthService,
private readonly favorites: FavoritesService,
) {}

register(): void {
const validate = (token: string) => this.auth.validate(token);

secureHandle(
'favorites:list',
async (_event, params: unknown) => {
const { sessionId } = z.object({
sessionId: z.string().optional(),
}).parse(params ?? {});
return this.favorites.list(sessionId);
},
validate,
);

secureHandle(
'favorites:create',
async (_event, params: unknown) => {
const input = z.object({
sessionId: z.string(),
messageId: z.string(),
note: z.string().optional(),
}).parse(params);
return this.favorites.create(input);
},
validate,
);

secureHandle(
'favorites:update',
async (_event, params: unknown) => {
const { id, note } = z.object({
id: z.string(),
note: z.string().optional(),
}).parse(params);
return this.favorites.update(id, { note });
},
validate,
);

secureHandle(
'favorites:delete',
async (_event, params: unknown) => {
const { id } = z.object({
id: z.string(),
}).parse(params);
return this.favorites.delete(id);
},
validate,
);
}
}

Ключевые особенности паттерна:

  • secureHandle(channel, handler, validate) — единый безопасный IPC-обработчик
    • channel — имя IPC-канала (формат: домен:действие)
    • handler — асинхронная функция-обработчик; первый аргумент — event, второй — params с извлечённым токеном
    • validate — функция проверки токена
  • Валидация через Zod: все параметры должны пройти валидацию Zod для защиты от вредоносных входных данных

Шаг 4: Регистрация Router

Файл: packages/desktop/app/main/services/routers/index.ts

// 1. Импортируем Router и Service
import { FavoritesRouter } from './FavoritesRouter';
import { FavoritesService } from '../../content/favorites/FavoritesService';

// 2. Внутри registerAllRouters() создаём экземпляры и регистрируем
export function registerAllRouters(deps: RouterDependencies) {
// ... существующие регистрации роутеров ...

// Создаём FavoritesService и Router
const favoritesService = new FavoritesService(deps.db);
const favoritesRouter = new FavoritesRouter(deps.auth, favoritesService);
favoritesRouter.register();

// ... другой код ...
}

Если Service требует новых зависимостей, одновременно обновите интерфейс RouterDependencies.


Шаг 5: Открытие доступа через Preload

Файл: packages/desktop/app/preload/index.ts

Добавьте новое пространство имён в объект api:

const api = {
// ... существующие пространства имён ...

favorites: {
list: (sessionId?: string) =>
invoke('favorites:list', sessionId ? { sessionId } : undefined),
create: (input: { sessionId: string; messageId: string; note?: string }) =>
invoke('favorites:create', input),
update: (id: string, note?: string) =>
invoke('favorites:update', { id, note }),
delete: (id: string) =>
invoke('favorites:delete', { id }),
},
};

Также добавьте объявление типа в интерфейс DesktopApi в файле @shared/contracts.ts, чтобы window.api на фронтенде получал корректные подсказки типов.


Шаг 6: Создание фронтенд-хука

Файл: packages/renderer/src/features/favorites/hooks/useFavorites.ts

import { useCallback, useEffect, useState } from 'react';

import type { Favorite } from '@shared/contracts/favorites-types';

export function useFavorites(sessionId?: string) {
const [favorites, setFavorites] = useState<Favorite[]>([]);
const [loading, setLoading] = useState(true);

const refresh = useCallback(async () => {
setLoading(true);
try {
const result = await window.api.favorites.list(sessionId);
setFavorites(result);
} finally {
setLoading(false);
}
}, [sessionId]);

useEffect(() => {
refresh();
}, [refresh]);

const addFavorite = useCallback(
async (messageId: string, note?: string) => {
if (!sessionId) return;
await window.api.favorites.create({ sessionId, messageId, note });
await refresh();
},
[sessionId, refresh],
);

const removeFavorite = useCallback(
async (id: string) => {
await window.api.favorites.delete(id);
await refresh();
},
[refresh],
);

return { favorites, loading, addFavorite, removeFavorite, refresh };
}

Шаг 7: Создание UI-компонента

Файл: packages/renderer/src/features/favorites/components/FavoritesList.tsx

import { Star, Trash2 } from 'lucide-react';

import { Button } from '@/components/ui/button';
import { useFavorites } from '@/features/favorites/hooks/useFavorites';

interface FavoritesListProps {
sessionId: string;
}

export function FavoritesList({ sessionId }: FavoritesListProps) {
const { favorites, loading, removeFavorite } = useFavorites(sessionId);

if (loading) {
return <div className="animate-pulse p-4">Loading...</div>;
}

if (favorites.length === 0) {
return (
<div className="flex flex-col items-center gap-2 p-8 text-muted-foreground">
<Star className="h-8 w-8" />
<p>No favorites yet</p>
</div>
);
}

return (
<div className="space-y-2 p-4">
{favorites.map((fav) => (
<div
key={fav.id}
className="flex items-center justify-between rounded-lg bg-surface-1 p-3"
>
<div className="min-w-0 flex-1">
<p className="truncate text-sm text-foreground">{fav.note}</p>
<p className="text-xs text-muted-foreground">{fav.createdAt}</p>
</div>
<Button
variant="ghost"
size="icon"
onClick={() => removeFavorite(fav.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
))}
</div>
);
}

Шаг 8: Добавление i18n

Создайте файлы перевода для en, zh и ja:

Файл: packages/renderer/src/locales/{en,zh,ja}/favorites.json

{
"favorites.title": "Favorites",
"favorites.empty": "No favorites yet",
"favorites.add": "Add to Favorites",
"favorites.remove": "Remove from Favorites",
"favorites.note": "Note"
}

Чеклист

Перед отправкой PR убедитесь в следующем:

  • Фронтенд передаёт только пользовательский ввод и параметры конфигурации (никаких чувствительных данных, например API-ключей)
  • Бэкенд обрабатывает все внешние вызовы (API, файловая система, база данных)
  • Данные, возвращаемые фронтенду, находятся в окончательном виде (фронтенд не должен их обрабатывать и отправлять обратно)
  • Крупные данные, такие как base64, не обрабатываются на фронтенде и не отправляются обратно на бэкенд
  • Параметры в Router валидируются с помощью Zod
  • Новый Router зарегистрирован в registerAllRouters()
  • Сигнатуры методов в Preload API соответствуют определению типа DesktopApi
  • Файлы i18n обновлены для всех трёх языков
  • npm run lint завершается без ошибок

Справочник файлов

СлойПуть
Общие типыpackages/desktop/app/shared/contracts/
Сервис бэкендаpackages/desktop/app/main/services/
IPC Routerpackages/desktop/app/main/services/routers/
Регистрация Routerpackages/desktop/app/main/services/routers/index.ts
Preload-скриптpackages/desktop/app/preload/index.ts
Фронтенд-хукpackages/renderer/src/features/<feature>/hooks/ (межфункциональные хуки — в shared/hooks/)
Фронтенд-компонентыpackages/renderer/src/features/<feature>/components/ (общий UI — в components/ui/)
i18npackages/renderer/src/locales/