首页
/ LobeHub Desktop 本地工具开发指南:从 Manifest 到 IPC Controller 的完整实现路径

LobeHub Desktop 本地工具开发指南:从 Manifest 到 IPC Controller 的完整实现路径

2026-09-06 11:57:34作者:幸俭卉

在 LobeHub 的桌面端(Electron 应用)中,Agent 之所以能够读写、搜索、移动本地文件并执行 shell 命令,依赖的是一套分层清晰的"本地工具"(Local Tools)实现体系。本文基于仓库内的开发参考文档 local-tools.md,完整讲解新增一个桌面本地工具的六步标准流程:定义工具清单(Manifest)→ 定义类型 → 实现 Store Action → 实现 Service 层 → 实现主进程 Controller(IPC Handler)→ 更新 Agent 文档,并结合当前仓库中的真实实现(LocalFileCtrlocalFileService 等)剖析每层的调用关系与安全边界,读完即可在 Desktop 应用中完整落地一个可被 Agent 调用的本地工具。

一、总体工作流:六步分层架构

参考文档给出的标准流程如下:

  1. Define tool interface (Manifest) —— 定义工具接口(工具清单)
  2. Define related types —— 定义相关类型
  3. Implement Store Action —— 实现渲染进程的 Store Action
  4. Implement Service Layer —— 实现 IPC Service 层
  5. Implement Controller (IPC Handler) —— 实现主进程 Controller
  6. Update Agent documentation —— 更新 Agent 的 systemRole 文档

这条流程的本质是 Electron 典型的渲染进程 ↔ IPC ↔ 主进程分层:渲染进程里的 AI 对话 Store 只负责状态流转与消息更新,Service 层负责把调用桥接到 Electron IPC,真正接触文件系统、shell、dialog 等系统能力的逻辑全部收敛在主进程 Controller 中。这样做的好处是:敏感操作不暴露在渲染进程、类型贯穿三层可被 TypeScript 静态校验、同一能力可以复用给不同入口(例如文档与代码中提到的渲染进程 IPC 路径和 gateway RPC 路径可以共享同一套缓存与实现)。

二、Step 1:定义工具接口(Manifest)

文档要求在 src/tools/[tool_category]/index.ts 中定义工具的 API 名称常量与 Manifest。以 local-files 分类为例:

// src/tools/local-files/index.ts
export const LocalFilesApiName = {
  RenameFile: 'renameFile',
  MoveFile: 'moveFile',
} as const;

export const LocalFilesManifest = {
  api: [
    {
      name: LocalFilesApiName.RenameFile,
      description: 'Rename a local file',
      parameters: {
        type: 'object',
        properties: {
          oldPath: { type: 'string', description: 'Current file path' },
          newName: { type: 'string', description: 'New file name' },
        },
        required: ['oldPath', 'newName'],
      },
    },
  ],
};

要点:

  • LocalFilesApiNameas const 固化 API 名,避免散落字符串;Manifest 的 parameters 直接采用 JSON Schema 风格(type / properties / required),这是给模型做 function calling 的参数契约,description 字段会直接进入工具描述,写清楚参数语义能显著影响模型调用正确率;
  • 一个"分类"(tool_category)对应一个目录,目录内 index.ts 集中导出该分类下所有 API 的 Manifest。

需要说明的是,文档中的 src/tools/local-files/ 是该流程的示例位置;在当前仓库中,工具参数与状态类型大多沉淀在共享包内(如 electron-client-ipc 类型定义),Manifest 与 systemRole 的组织以各 builtin-tool-* 包(如 packages/builtin-tools/packages/builtin-tool-local-system/)为准,新增工具时应先查看相邻工具包的实际组织方式再落位。

三、Step 2:定义相关类型

类型分两处定义,一处放共享包、一处放工具自身:

// packages/electron-client-ipc/src/types.ts
export interface RenameLocalFileParams {
  oldPath: string;
  newName: string;
}

// src/tools/local-files/type.ts
export interface LocalRenameFileState {
  success: boolean;
  error?: string;
  oldPath: string;
  newPath: string;
}
  • RenameLocalFileParams 放在 electron-client-ipc 包中,是因为它要跨渲染/主进程边界传输,两边都从同一包 import,保证参数结构一致;
  • LocalRenameFileState 是渲染进程内部的工具执行状态,供 Store 与消息卡片渲染使用。

当前仓库中,localFileService.ts@lobechat/electron-client-ipc 一次性导入了几十个 Params/Result 类型对(LocalReadFileParamsRenameLocalFileParamsEditLocalFileParamsRunCommandParams 等),每个能力都遵循"XxxParams + XxxResult"的成对约定,这套约定正是本步骤的直接产物。

四、Step 3:实现 Store Action

Store Action 位于渲染进程(示例位置 src/store/chat/slices/builtinTool/actions/localFile.ts),职责是:切换 loading 态 → 调用 Service → 把结果写回插件状态与消息内容:

// src/store/chat/slices/builtinTool/actions/localFile.ts
renameLocalFile: async (id: string, params: RenameLocalFileParams) => {
  const { toggleLocalFileLoading, updatePluginState, internal_updateMessageContent } = get();

  toggleLocalFileLoading(id, true);

  try {
    const result = await localFileService.renameFile(params);

    if (result.success) {
      updatePluginState(id, { success: true, ...result });
      internal_updateMessageContent(id, JSON.stringify({ success: true }));
    } else {
      updatePluginState(id, { success: false, error: result.error });
      internal_updateMessageContent(id, JSON.stringify({ error: result.error }));
    }

    return result.success;
  } catch (e) {
    console.error(e);
    updatePluginState(id, { success: false, error: e.message });
    return false;
  } finally {
    toggleLocalFileLoading(id, false);
  }
},

三个关键动作:

  • toggleLocalFileLoading(id, true/false):在 finally 中必然复位,保证 UI loading 状态不悬挂;
  • updatePluginState(id, ...):更新该消息对应的工具卡片状态,成功时展开完整结果(如 newPath),失败时只带 error
  • internal_updateMessageContent(id, ...):把结构化结果({"success":true}{"error":"..."})写回消息内容,供模型后续轮次读取工具执行结果。

当前仓库中,builtinTool 的 actions 实际收敛在 builtinTool/actions 目录 下(含 search.tsinterpreter.ts 等),新增本地工具动作时可参考同目录现有切片的写法保持一致。

五、Step 4:实现 Service 层

Service 层是渲染进程里最薄的一层,只负责把参数通过 IPC 转发给主进程:

// src/services/electron/localFileService.ts
import { ensureElectronIpc } from '@/utils/electron/ipc';

const ipc = ensureElectronIpc();

export const localFileService = {
  renameFile: (params: RenameLocalFileParams) => ipc.localFiles.renameFile(params),
};
  • ensureElectronIpc() 返回 preload 注入的 typed IPC 客户端,ipc.localFiles / ipc.localSystem 等命名空间与主进程 Controller 的 groupName 一一对应;
  • 这一层禁止写业务逻辑,业务逻辑要么在主进程 Controller,要么在渲染进程的 UI 组件里。

当前仓库的 LocalFileService 已经长成一个覆盖完整能力面的 Service 类,按功能分组:

分组 代表方法 对应 IPC 命名空间
文件操作 listLocalFiles / readLocalFile / moveLocalFiles / renameLocalFile / writeFile / editLocalFile localSystem
文件搜索 searchLocalFiles / grepContent / globFiles / searchProjectFiles localSystem
打开与对话框 openLocalFile / openLocalFolder / showSaveDialog localSystem
Shell 命令 runCommand / getCommandOutput / killCommand / installSandbox shellCommand
预览 getLocalFilePreview / readLocalFileBytes localSystem

一个值得注意的细节:listProjectSkills 走的是 ipc.workspace 命名空间而非 localSystem,源码注释明确说明"项目技能扫描位于主进程 WorkspaceCtrworkspace group),从 LocalFileCtr 中拆分出去"——这说明 Controller 的职责划分是按领域归属而非"所有文件操作都放一个类"来维护的。

六、Step 5:实现主进程 Controller(IPC Handler)

文档给出的最小 Controller 示例:

// apps/desktop/src/main/controllers/LocalFileCtr.ts
import * as fs from 'fs/promises';
import * as path from 'path';
import { ControllerModule, IpcMethod } from '@/controllers';

export default class LocalFileCtr extends ControllerModule {
  static override readonly groupName = 'localFiles';

  @IpcMethod()
  async renameFile(params: RenameLocalFileParams) {
    const { oldPath, newName } = params;
    const newPath = path.join(path.dirname(oldPath), newName);

    try {
      await fs.rename(oldPath, newPath);
      return { success: true, newPath };
    } catch (error) {
      return { success: false, error: error.message };
    }
  }
}

框架机制:ControllerModule 与 IpcMethod

controllers/index.ts 可以看到:ControllerModule 继承自 IpcService,构造时注入全局 App 实例;IpcMethod 装饰器(从 @/utils/ipc 导出)负责把方法注册到 IPC 路由。groupName 静态属性决定该 Controller 在 IPC 客户端上暴露的命名空间,因此groupName 必须同步改 Service 层的 ipc.xxx 字段,否则运行时才暴露断链。

当前实现:LocalFileCtr 的纵深细节

当前仓库的 LocalFileCtr.ts 已经远超"rename 一个文件"的雏形,其 groupName = 'localSystem',可从中读出若干生产级实践:

  • 底层能力下沉到共享包readLocalFilewriteLocalFilerenameLocalFilemoveLocalFileseditLocalFileexpandTilderesolveAgainstCwd 等函数统一从 local-file-shell 包 导入,Controller 只做日志与分支编排,便于单测与复用;
  • 图片读取的特殊通道readFilepng/jpeg/webp/gif/avif 等扩展名不走文本读取,而是校验大小(超过 MAX_IMAGE_READ_BYTES = 10MB 直接拒绝)、经 RemoteFileUploadService 上传后返回 [Image: filename] 占位与 imageUrl,让 Agent 能"看到"图片;上传失败时降级为明确的占位提示而不是抛错;
  • 安全路径审计auditSafePaths 只允许落在 SAFE_PATH_PREFIXES = ['/tmp', '/var/tmp'] 之下的路径,且会用 realpath 解析符号链接后二次校验,防止通过软链逃逸沙箱;
  • 预览大小闸门MAX_DOCUMENT_PREVIEW_BYTES = 20MB,超限文档走无内容的 binary/pdf 变体(base64 会让 payload 膨胀约 4/3,必须能装进单条 IPC/RPC 响应);
  • 项目文件索引getProjectFileIndex 优先用 git rev-parse --show-toplevel + git ls-files(含 tracked/untracked/ignored 三路并行,各带超时)构建索引,非 git 目录回退到 glob 引擎(上限 PROJECT_FILE_GLOB_LIMIT = 5000),并顺带把项目根加入预览白名单(approveProjectRootForPreview);
  • 统一错误形态:每个 handler 都返回 { success, error? } 结构而非抛异常,与 Store Action 中 result.success 的分支处理正好闭环。

七、Step 6:更新 Agent 文档

流程的最后一步在 src/tools/[tool_category]/systemRole.ts:把新工具的描述加入 <core_capabilities>,使用指引加入 <tool_usage_guidelines>。这一步决定了模型"知道有这把锤子、并且知道什么时候该用"——Manifest 里的 description 与 systemRole 中的指引应当语义一致,避免两处描述漂移。当前仓库中各工具包(如 packages/builtin-tool-local-system/)内均有自己的 systemRole 组织,可直接参照其章节结构补充。

八、落地检查清单

按六步流程新增一个 Desktop 本地工具时,建议对照以下清单逐项确认:

  1. Manifest 中 API 名、参数 JSON Schema、required 列表与类型定义一致;
  2. 跨进程参数类型放在 electron-client-ipc 共享包,两端 import 同一份;
  3. Store Action 有 loading 开关且 finally 复位,成功/失败都调用 updatePluginState 并回写消息内容;
  4. Service 层只做 ipc.<groupName>.<method>(params) 透传,无业务逻辑;
  5. Controller 的 groupName 与 Service 端命名空间匹配,handler 返回 { success, error? } 统一形态,系统级能力(fs、dialog、shell)只出现在主进程;
  6. systemRole 文档同步更新 <core_capabilities><tool_usage_guidelines>

关键文件索引:

登录后查看全文
热门项目推荐
相关项目推荐