添加 IPC 接口
本指南以一个假设的「收藏夹」功能为例,逐步演示从类型定义到前端 Hook 的完整 IPC 接口开发流程。
通信架构回顾
渲染进程 (React)
| window.api.favorites.list()
v
Preload 脚本 (contextBridge)
| ipcRenderer.invoke('favorites:list', authToken)
v
主进程 Router (BaseRouter.secureHandle)
| 认证验证 → 参数校验 (Zod)
v
主进程 Service
| 业务逻辑 → 数据库操作
v
返回结果 → Preload → 渲染进程
每次 IPC 调用都会自动附带 authToken,由 secureHandle 统一验证,开发者无需手动处理认证。
第一步:定义共享类型
文件: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 导入,保证类型一致性。
第二步:创建后端 Service
文件: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 层完成,前端绝不直接访问。 :::
第三步:创建 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,第二个是已去除 token 的paramsvalidate— Token 验证函数
- Zod 校验:所有参数都必须通过 Zod 校验,防止恶意输入
第四步:注册 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) {
// ... 现有 router 注册代码 ...
// 创建 FavoritesService 和 Router
const favoritesService = new FavoritesService(deps.db);
const favoritesRouter = new FavoritesRouter(deps.auth, favoritesService);
favoritesRouter.register();
// ... 其他代码 ...
}
如果 Service 需要新的依赖项,需同步更新 RouterDependencies 接口。
第五步:暴露到 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 }),
},
};
同时在 @shared/contracts.ts 的 DesktopApi 接口中添加类型声明(确保前端 window.api 有类型提示)。
第六步:创建前端 Hook
文件: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 };
}
第七步:创建 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">加载中...</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>暂无收藏</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>
);
}
第八步:添加 i18n
为 en、zh、ja 三种语言创建翻译文件:
文件:packages/renderer/src/locales/{en,zh,ja}/favorites.json
{
"favorites.title": "收藏夹",
"favorites.empty": "暂无收藏",
"favorites.add": "添加收藏",
"favorites.remove": "取消收藏",
"favorites.note": "备注"
}
检查清单
在提交 PR 前,确认以下各项:
- 前端只传递用户输入和配置参数(不包含 API Key 等敏感信息)
- 后端负责所有外部调用(API、文件系统、数据库)
- 返回给前端的数据是最终形态(前端不需再处理后回传)
- 避免在前端处理 base64 等大数据后再发回后端
- Router 中的参数经过 Zod 校验
- 已在
registerAllRouters()中注册新 Router - Preload API 中的方法签名与
DesktopApi类型定义一致 - i18n 三语言同步更新
- 运行
npm run lint无 error
相关文件速查
| 层级 | 路径 |
|---|---|
| 共享类型 | packages/desktop/app/shared/contracts/ |
| 后端服务 | packages/desktop/app/main/services/ |
| IPC Router | packages/desktop/app/main/services/routers/ |
| Router 注册 | packages/desktop/app/main/services/routers/index.ts |
| Preload 脚本 | packages/desktop/app/preload/index.ts |
| 前端 Hook | packages/renderer/src/features/<feature>/hooks/(跨功能放 shared/hooks/) |
| 前端组件 | packages/renderer/src/features/<feature>/components/(共享 UI 在 components/ui/) |
| i18n | packages/renderer/src/locales/ |