gemini cli 源码解读

文章目录

Gemini CLI 项目框架总览

文档对 Gemini

CLI 源码进行系统性拆解与讲解,适合希望深入理解该项目实现原理的开发者。

源码地址:https://github.com/google-gemini/gemini-cli#


一、项目简介

Gemini

CLI 是 Google 开源的 AI 命令行工具,让用户可以在终端中直接与 Gemini 模型交互,并让模型代理执行文件操作、Shell 命令、网络搜索等任务。

  • GitHubgoogle-gemini/gemini-cli
  • 语言:TypeScript(全栈)
  • 架构:npm monorepo(单仓库多包)
  • 版本(分析时):0.36.0-nightly

二、仓库目录结构

复制代码
gemini-cli-main/
│
├── packages/                   ← 核心代码,分为多个独立子包
│   ├── core/                   ← 🧠 AI 代理引擎(最重要)
│   ├── cli/                    ← 🖥️  终端 UI 和用户交互
│   ├── sdk/                    ← 📦 对外暴露的编程 SDK
│   ├── a2a-server/             ← 🤝 Agent-to-Agent 协议服务
│   ├── devtools/               ← 🛠️  开发者调试工具(React DevTools 等)
│   ├── vscode-ide-companion/   ← 🔌 VS Code 扩展插件
│   └── test-utils/             ← 🧪 公共测试工具
│
├── integration-tests/          ← 集成测试(真实运行 CLI)
├── evals/                      ← AI 行为评估测试(用 Gemini 评估 Gemini)
├── docs/                       ← 用户文档(已翻译为中文)
│   └── architecture/           ← 本系列架构讲解文档
├── scripts/                    ← 构建、发布、代码生成脚本
├── .gemini/                    ← 项目自身使用 Gemini CLI 的配置
├── esbuild.config.js           ← 最终打包配置(输出 bundle/gemini.js)
└── package.json                ← 根 workspace 配置

三、各子包详细说明

3.1 packages/core --- 大脑(最重要)

包名:@google/gemini-cli-core

这是整个项目的核心引擎,封装了所有与 AI 代理相关的逻辑:

复制代码
core/src/
├── agent/          ← 单轮对话 Agent(与 Gemini API 通信)
├── agents/         ← 多类型代理(子代理、远程代理、A2A 代理)
├── scheduler/      ← 🔑 工具调用调度器(串行/并发/审批控制)
├── tools/          ← 🔧 AI 可调用的所有工具(40+ 个)
├── config/         ← 配置管理(读取 settings.json、环境变量等)
├── hooks/          ← 钩子系统(工具调用前/后触发自定义逻辑)
├── mcp/            ← MCP 协议客户端(接入外部工具服务器)
├── sandbox/        ← 沙箱隔离(Docker/Podman 容器执行)
├── skills/         ← 技能系统(可复用的 AI 工作流)
├── telemetry/      ← OpenTelemetry 遥测(追踪、指标、日志)
├── prompts/        ← 系统提示词管理
├── policy/         ← 安全策略引擎(控制哪些工具可以自动执行)
├── confirmation-bus/ ← 消息总线(工具审批的事件驱动通信)
├── core/           ← Agent 循环主流程(GeminiClient)
├── routing/        ← 模型路由(根据任务选择不同模型)
└── utils/          ← 工具函数

3.2 packages/cli --- 终端界面

包名:@google/gemini-cli

React + Ink 渲染终端 UI,Ink 是一个允许用 React 组件描述终端输出的库:

复制代码
cli/src/
├── gemini.tsx          ← 交互式模式入口(Interactive CLI)
├── nonInteractiveCli.ts← 无头/管道模式入口(Headless/Pipe)
├── interactiveCli.tsx  ← 交互式 CLI 主流程协调器
├── ui/
│   ├── App.tsx             ← 根 React 组件
│   ├── AppContainer.tsx    ← 状态管理容器
│   ├── components/         ← 各类 UI 组件(消息、输入框、工具调用显示等)
│   ├── hooks/              ← React hooks(流式响应、快捷键等)
│   ├── themes/             ← 主题(颜色方案)
│   ├── state/              ← 全局状态(会话、设置等)
│   ├── contexts/           ← React Context(配置注入等)
│   └── commands/           ← 斜杠命令(/help、/clear、/model 等)
└── config/             ← CLI 层配置

3.3 packages/sdk --- 对外 SDK

封装 core 包,提供给第三方开发者通过编程方式调用 Gemini CLI 功能的接口。

3.4 packages/a2a-server --- Agent 通信服务

实现
A2A(Agent-to-Agent)协议,允许多个 AI 代理互相通信和协作。例如:一个父代理可以委派子任务给专门的子代理。

3.5 packages/vscode-ide-companion --- VS Code 扩展

将 Gemini CLI 的能力嵌入 VS Code IDE,提供代码辅助、上下文感知等功能。

3.6 packages/devtools --- 开发调试工具

集成 React DevTools,用于调试终端 UI 组件树。


四、核心技术栈

层次 技术 用途
语言 TypeScript 5.x 全栈类型安全
终端 UI React 19 +Ink 用 React 组件渲染终端
AI 调用 @google/genai SDK 调用 Gemini API
工具协议 MCP(Model Context Protocol) 接入外部工具服务器
Agent 通信 A2A(Agent-to-Agent Protocol) 多代理协作
沙箱隔离 Docker / Podman 安全执行 Shell 命令
遥测 OpenTelemetry 追踪/指标/日志
构建打包 esbuild 输出单文件 bundle
测试 Vitest 单元测试 + 集成测试
依赖管理 npm workspaces monorepo 包管理

五、一次完整对话的数据流

复制代码
用户在终端输入文字
        │
        ▼
interactiveCli.tsx(CLI 层)
  接收用户输入,构建消息
        │
        ▼
GeminiClient / AgentLoop(core/core/ 层)
  将消息 + 工具定义发给 Gemini API
        │
        ▼
Gemini API 返回响应
  情况 A:纯文字回复 → 直接显示给用户
  情况 B:ToolCall("我要调用某工具")→ 进入调度器
        │
        ▼
Scheduler(core/scheduler/)
  检查策略 → 请求用户审批(如需要)→ 执行工具
        │
        ▼
Tool 执行(core/tools/)
  如 shell.ts 执行命令、read-file.ts 读文件、web-search.ts 搜索...
        │
        ▼
工具结果通过 functionResponse 返回给 Gemini API
        │
        ▼
Gemini 生成最终文字回复
        │
        ▼
React + Ink 渲染到终端

Scheduler 调度器深度解析

源码位置:packages/core/src/scheduler/

Scheduler 是 Gemini

CLI 中最核心的组件之一,负责将 AI 模型返回的"工具调用请求"转化为实际执行,并管理整个执行生命周期


一、Scheduler 是什么?

当 Gemini 模型决定要调用某个工具时(比如"读取这个文件"、"执行这条命令"),它不会直接执行,而是返回一个结构化的
ToolCall 请求。Scheduler

就是负责接收这些请求并驱动它们完成执行的"调度引擎"。

它解决的核心问题:

  1. 并发控制:哪些工具调用可以并行?哪些必须串行?
  2. 安全审批:哪些操作需要用户确认后才能执行?
  3. 策略检查:是否违反安全规则(策略引擎)?
  4. 状态追踪:每个工具调用处于哪个生命周期阶段?
  5. 取消支持:用户按 Ctrl+C 时如何干净地终止?

二、涉及的文件

文件 职责
scheduler.ts 主调度器,协调整个执行流程
state-manager.ts 状态机,管理每个工具调用的状态流转
tool-executor.ts 实际执行单个工具,处理结果
confirmation.ts 用户确认流程(审批弹窗逻辑)
policy.ts 策略检查(自动允许/拒绝/询问)
tool-modifier.ts 允许用户在确认时修改工具参数
types.ts 所有类型定义(状态枚举、数据结构)

三、工具调用的生命周期(状态机)

每个工具调用都是一个有限状态机,按以下状态流转:

复制代码
                    ┌─────────────────────────────────────────────┐
                    │              新工具调用请求进入               │
                    └────────────────────┬────────────────────────┘
                                         │
                                         ▼
                                   [Validating]
                              验证参数 + 查找工具定义
                                    /         \
                              参数错误          参数合法
                                 /               \
                            [Error]          检查安全策略
                                            /     |     \
                                       DENY    ASK_USER  ALLOW
                                         |        |        |
                                      [Error]  [AwaitingApproval]  |
                                              用户确认/取消   |
                                              /        \     |
                                         Cancel      Proceed |
                                            |            \   |
                                        [Cancelled]   [Scheduled]
                                                          |
                                                    实际执行工具
                                                          |
                                                    [Executing]
                                                    (实时输出)
                                                    /         \
                                               执行成功       执行失败
                                                   |               |
                                             [Success]          [Error]

状态枚举(CoreToolCallStatus

typescript 复制代码
// packages/core/src/scheduler/types.ts
enum CoreToolCallStatus {
  Validating = 'validating', // 正在验证参数、查找工具
  AwaitingApproval = 'awaiting_approval', // 等待用户审批
  Scheduled = 'scheduled', // 审批通过,等待执行
  Executing = 'executing', // 正在执行中(可显示实时输出)
  Success = 'success', // 执行成功
  Error = 'error', // 执行失败/被策略拒绝
  Cancelled = 'cancelled', // 用户取消
}

四、核心类:Scheduler

4.1 构造函数与组成

typescript 复制代码
// packages/core/src/scheduler/scheduler.ts
export class Scheduler {
  private readonly state: SchedulerStateManager; // 状态管理
  private readonly executor: ToolExecutor; // 工具执行器
  private readonly modifier: ToolModificationHandler; // 参数修改器
  private readonly config: Config;
  private readonly messageBus: MessageBus; // 事件总线

  private isProcessing = false; // 当前是否在处理批次
  private isCancelling = false; // 当前是否在取消操作
  private readonly requestQueue: SchedulerQueueItem[] = []; // 等待处理的队列
}

Scheduler 由四个核心部件组成:

  • StateManager:纯状态容器,不执行任何逻辑,只负责状态存储和转换
  • ToolExecutor:真正运行工具代码的执行器
  • ToolModificationHandler:允许用户在审批时修改工具参数(如编辑文件内容)
  • MessageBus:事件总线,将状态变化广播给 UI 层

4.2 主入口:schedule() 方法

typescript 复制代码
async schedule(
  request: ToolCallRequestInfo | ToolCallRequestInfo[],
  signal: AbortSignal,
): Promise<CompletedToolCall[]>

这是外部调用的唯一入口。传入一批工具调用请求,返回一批执行结果。

流程逻辑

复制代码
schedule() 被调用
    │
    ├── 如果当前没有处理中的批次 → _startBatch() 立即处理
    └── 如果已有批次在处理 → _enqueueRequest() 排队等待

4.3 批处理:_startBatch() 方法

typescript 复制代码
private async _startBatch(
  requests: ToolCallRequestInfo[],
  signal: AbortSignal,
): Promise<CompletedToolCall[]>

处理步骤

  1. 对每个请求,在 toolRegistry 中查找对应工具
  2. 调用 tool.build(args) 验证参数,构建 invocation 对象
  3. 将所有工具调用加入状态队列(初始状态:Validating
  4. 启动主处理循环 _processQueue()

4.4 主处理循环:_processQueue() / _processNextItem()

这是调度器的"心跳",循环执行直到所有工具调用完成:

typescript 复制代码
// 每次迭代处理三个阶段:

// 阶段 1:处理所有 Validating 状态的调用(并发)
// → 检查策略、等待用户审批

// 阶段 2:执行所有 Scheduled 状态的调用(并发)
// → 只有当所有活跃调用都到达 ready 状态时才执行

// 阶段 3:将终态(Success/Error/Cancelled)的调用移入 completedBatch

4.5 并行化逻辑

typescript 复制代码
// 判断一个工具调用是否可以并行执行
private _isParallelizable(request: ToolCallRequestInfo): boolean {
  if (request.args) {
    const wait = request.args['wait_for_previous'];
    if (typeof wait === 'boolean') {
      return !wait;  // 工具可以通过参数 wait_for_previous: true 强制串行
    }
  }
  return true; // 默认并行
}

并行规则:当模型返回多个工具调用时,如果第一个是可并行的,调度器会把后续所有可并行工具一起批量激活并发执行。如果遇到需要串行的工具,则等前面的全部完成再执行。


五、状态管理:SchedulerStateManager

源码:packages/core/src/scheduler/state-manager.ts

状态管理器是一个纯状态容器,不包含任何业务逻辑:

typescript 复制代码
export class SchedulerStateManager {
  private readonly activeCalls = new Map<string, ToolCall>(); // 当前活跃调用
  private readonly queue: ToolCall[] = []; // 等待激活的队列
  private _completedBatch: CompletedToolCall[] = []; // 已完成的调用
}

三层存储

  • queue:还未开始处理,等待被 dequeue() 取出
  • activeCalls:正在处理中(ValidatingAwaitingApprovalScheduled
    Executing
  • _completedBatch:已终态(Success / Error / Cancelled),等待返回给 AI

每次状态变化都会调用 emitUpdate(),通过 MessageBus 广播 TOOL_CALLS_UPDATE

事件,驱动 UI 刷新。

状态转换方法(类型安全的重载)

typescript 复制代码
// 针对不同目标状态有不同的重载签名,确保类型安全
updateStatus(callId, CoreToolCallStatus.Success, data: ToolCallResponseInfo): void
updateStatus(callId, CoreToolCallStatus.Error, data: ToolCallResponseInfo): void
updateStatus(callId, CoreToolCallStatus.AwaitingApproval, data: ConfirmationDetails): void
updateStatus(callId, CoreToolCallStatus.Scheduled): void
updateStatus(callId, CoreToolCallStatus.Executing, data?: Partial<ExecutingToolCall>): void
updateStatus(callId, CoreToolCallStatus.Cancelled, data: string | ToolCallResponseInfo): void

六、工具执行:ToolExecutor

源码:packages/core/src/scheduler/tool-executor.ts

执行器负责实际运行工具,并将结果标准化:

typescript 复制代码
async execute(context: ToolExecutionContext): Promise<CompletedToolCall>

执行流程

  1. 调用 executeToolWithHooks(invocation, ...) --- 这里会触发钩子(hooks)
  2. 等待工具返回 ToolResult
  3. 检查是否需要截断超长输出(truncateOutputIfNeeded
  4. 将结果转换为标准的 ToolCallResponseInfo 格式(包装成 functionResponse
    Part)

Tail Call 机制 :某些工具执行完后可以返回一个
tailToolCallRequest,要求立即接着执行另一个工具。这用于工具链(比如:先搜索文件,再读取文件)。

typescript 复制代码
// 如果工具返回了 tailToolCallRequest,调度器会用新工具"替换"当前调用
// 原始 callId 保留,但工具名称和参数变为新工具的
if (result.tailToolCallRequest) {
  this.state.replaceActiveCallWithTailCall(callId, nextCall);
}

七、策略与审批流程

7.1 三种策略决策

typescript 复制代码
// packages/core/src/policy/types.ts
enum PolicyDecision {
  ALLOW, // 自动允许,直接执行
  DENY, // 拒绝,返回错误
  ASK_USER, // 需要用户确认
}

7.2 审批模式(ApprovalMode

通过 settings.jsonapprovalMode 字段控制:

  • AUTO:全部自动允许(危险!)
  • DEFAULT:读操作自动允许,写/执行操作询问用户
  • MANUAL:所有操作都询问用户

7.3 用户审批流程

复制代码
工具调用进入 Validating
    │
    ▼
checkPolicy() → ASK_USER
    │
    ▼
resolveConfirmation()
    ├── 在 MessageBus 上发布 TOOL_CONFIRMATION_REQUEST
    ├── 状态变为 AwaitingApproval
    ├── UI 层收到事件,显示确认弹窗
    └── 等待用户操作...
         │
         ├── 用户点击"允许一次" → ProceedOnce
         ├── 用户点击"始终允许" → AlwaysProceed(更新策略)
         ├── 用户点击"修改参数" → 打开编辑器修改
         └── 用户点击"取消" → Cancel(级联取消整批)

八、取消机制

typescript 复制代码
cancelAll(): void {
  // 1. 清空等待队列(reject 所有挂起的 Promise)
  while (this.requestQueue.length > 0) {
    const next = this.requestQueue.shift();
    next?.reject(new Error('Operation cancelled by user'));
  }

  // 2. 将所有活跃调用标记为 Cancelled
  for (const activeCall of activeCalls) {
    this.state.updateStatus(callId, CoreToolCallStatus.Cancelled, '...');
  }

  // 3. 清空状态队列
  this.state.cancelAllQueued('Operation cancelled by user');
}

AbortSignal 贯穿整个调用链,任何地方检测到 signal.aborted 都会立即停止。


九、关键设计模式总结

模式 应用
状态机 工具调用的 7 种状态,每种状态有严格的转换规则
事件驱动 MessageBus 解耦调度器与 UI,状态变化发事件通知
命令模式 ToolCallRequestInfo 封装工具调用请求,与执行逻辑解耦
职责链 验证 → 策略检查 → 用户审批 → 执行,每步可中断
Tail Call 工具执行后可链式触发下一个工具,不增加调用栈
类型安全重载 updateStatus 的重载确保不同状态携带正确的数据类型

十、与其他模块的关系

复制代码
GeminiClient(agent loop)
    │  调用 schedule()
    ▼
Scheduler
    ├── → SchedulerStateManager(状态存储)
    ├── → ToolExecutor
    │       └── → executeToolWithHooks()
    │               └── → 具体 Tool 实现(shell.ts、read-file.ts 等)
    ├── → PolicyChecker(策略检查)
    ├── → MessageBus(广播状态更新)
    │       └── → UI 层(React 组件)
    └── → ConfirmationResolver(用户审批)

Tools 工具系统深度解析

源码位置:packages/core/src/tools/

工具系统是 Gemini

CLI 的"手脚"------AI 模型通过工具与外部世界交互。本文档深入分析工具系统的设计模式、所有内置工具,以及如何扩展自定义工具。


一、工具系统的整体架构

复制代码
tools/
├── tools.ts            ← 核心抽象:接口、基类、类型定义
├── tool-registry.ts    ← 工具注册表:管理所有可用工具
├── tool-names.ts       ← 所有工具名称常量(唯一真相来源)
│
├── 文件系统工具
│   ├── read-file.ts        ← 读取单个文件
│   ├── read-many-files.ts  ← 批量读取文件
│   ├── write-file.ts       ← 写入/创建文件
│   ├── edit.ts             ← 精确字符串替换编辑
│   ├── glob.ts             ← 文件模式匹配
│   ├── grep.ts             ← 文件内容搜索
│   └── ls.ts               ← 列出目录
│
├── 执行工具
│   └── shell.ts            ← 执行 Shell 命令
│
├── 网络工具
│   ├── web-search.ts       ← Google 搜索
│   └── web-fetch.ts        ← 抓取网页内容
│
├── AI/代理工具
│   ├── memoryTool.ts       ← 持久化记忆
│   ├── ask-user.ts         ← 向用户提问
│   ├── activate-skill.ts   ← 激活技能
│   ├── enter-plan-mode.ts  ← 进入计划模式
│   ├── exit-plan-mode.ts   ← 退出计划模式
│   ├── write-todos.ts      ← 任务清单管理
│   └── trackerTools.ts     ← 任务追踪器
│
├── 文档工具
│   └── get-internal-docs.ts ← 获取内部文档
│
├── MCP 工具
│   ├── mcp-tool.ts         ← MCP 工具适配器
│   └── mcp-client.ts       ← MCP 客户端
│
└── 工具发现
    └── tool-registry.ts    ← 动态发现和注册工具

二、核心抽象:两层设计模式

工具系统采用**"工厂 + 命令"**的两层设计:

复制代码
DeclarativeTool(工厂层)
    ├── name, description, schema   ← 告诉 AI "我能做什么"
    ├── build(params) → ToolInvocation  ← 验证参数,生成执行对象
    └── isReadOnly, kind            ← 影响策略和并发控制

ToolInvocation(命令层)
    ├── params                      ← 已验证的参数(类型安全)
    ├── getDescription()            ← 给用户看的描述(显示在审批弹窗)
    ├── shouldConfirmExecute()      ← 是否需要用户确认
    └── execute(signal, updateOutput) ← 实际执行逻辑

为什么要分两层?

  • DeclarativeTool 是单例,整个生命周期只有一个实例,负责"类级别"的元数据
  • ToolInvocation 是每次调用创建的对象,持有本次调用的具体参数和状态
  • 这使得验证逻辑(在 build 中)与执行逻辑(在 execute 中)完全分离

2.1 继承体系

复制代码
DeclarativeTool<TParams, TResult>     ← 最底层抽象基类
    └── BaseDeclarativeTool           ← 添加了 JSON Schema 验证的默认 build()
            ├── ReadFileTool
            ├── WriteFileTool
            ├── EditTool
            ├── ShellTool
            ├── WebSearchTool
            └── ... 所有内置工具

ToolInvocation<TParams, TResult>      ← 执行对象接口
    └── BaseToolInvocation            ← 提供 MessageBus 集成和默认确认逻辑
            ├── ReadFileToolInvocation
            ├── ShellToolInvocation
            └── ... 对应每个工具的 Invocation 类

2.2 工具种类(Kind 枚举)

typescript 复制代码
// packages/core/src/tools/tools.ts
enum Kind {
  Read = 'read', // 只读操作 → isReadOnly=true,默认不需审批
  Search = 'search', // 搜索操作 → isReadOnly=true
  Fetch = 'fetch', // 网络获取 → isReadOnly=true
  Edit = 'edit', // 文件修改 → 需要审批(显示 diff)
  Delete = 'delete', // 删除操作 → 需要审批
  Move = 'move', // 移动操作 → 需要审批
  Execute = 'execute', // 执行命令 → 需要审批(显示命令)
  Think = 'think', // 思考/规划
  Agent = 'agent', // 子代理
  Communicate = 'communicate',
  Plan = 'plan',
  SwitchMode = 'switch_mode',
  Other = 'other',
}

// Read/Search/Fetch 归为只读类,可以并发执行,策略默认 ALLOW
const READ_ONLY_KINDS = [Kind.Read, Kind.Search, Kind.Fetch];
// Edit/Delete/Move/Execute 有副作用,策略默认 ASK_USER
const MUTATOR_KINDS = [Kind.Edit, Kind.Delete, Kind.Move, Kind.Execute];

三、ToolResult:工具的返回值格式

每个工具都返回标准化的 ToolResult

typescript 复制代码
interface ToolResult {
  llmContent: PartListUnion; // 给 AI 模型看的内容(放入对话历史)
  returnDisplay: ToolResultDisplay; // 给用户看的内容(渲染在终端)
  error?: {
    message: string;
    type?: ToolErrorType; // 错误分类(FILE_NOT_FOUND、PERMISSION_DENIED 等)
  };
  data?: Record<string, unknown>; // 结构化数据(传给调用方程序)
  tailToolCallRequest?: {
    // 链式调用下一个工具(Tail Call)
    name: string;
    args: Record<string, unknown>;
  };
}

llmContent vs returnDisplay 的区别

  • llmContent:放入 AI 的对话历史,影响 AI 的后续判断。应该包含完整的、准确的信息。
  • returnDisplay:在终端 UI 上渲染给用户看。可以是 markdown、diff、ANSI 颜色输出等更友好的格式。

四、内置工具详解

4.1 文件系统类工具

read_file --- 读取文件
typescript 复制代码
// 参数
interface ReadFileToolParams {
  file_path: string; // 文件路径
  start_line?: number; // 起始行(1-based,可选)
  end_line?: number; // 结束行(1-based,可选)
}
  • Kind.Read,只读,不需要用户确认
  • 支持指定行范围(大文件局部读取)
  • 自动检测文件编码、处理二进制文件
  • JIT
    Context
    (Just-In-Time)机制:读文件时会自动附加相关的上下文信息(如 git 状态)
write_file --- 写入/创建文件
typescript 复制代码
interface WriteFileToolParams {
  file_path: string;
  content: string; // 完整的文件内容(全量覆盖)
}
  • Kind.Edit,需要用户审批
  • 显示与现有文件的 diff(如果文件已存在)
  • 确认弹窗类型:ToolEditConfirmationDetails(展示文件 diff)
edit --- 精确字符串替换
typescript 复制代码
interface EditToolParams {
  file_path: string;
  old_string: string; // 要替换的原始字符串(必须在文件中唯一存在)
  new_string: string; // 替换后的新字符串
  allow_multiple?: boolean; // 是否允许替换多处
  instruction?: string; // 如果无法精确匹配,AI 的补充说明
}
  • Kind.Edit,需要用户审批,显示 diff

  • 核心挑战:模糊匹配恢复机制

    typescript 复制代码
    // edit.ts 中的容错策略(按优先级降序):
    // 1. exact    ------ 精确字符串匹配
    // 2. flexible ------ 忽略行尾空白的灵活匹配
    // 3. regex    ------ 正则表达式匹配
    // 4. fuzzy    ------ Levenshtein 距离模糊匹配(阈值 10% 差异)
    // 5. LLM fix  ------ 调用 AI 根据 instruction 修复匹配失败的情况
  • 使用 fast-levenshtein 计算编辑距离,diff 库生成 unified diff

glob --- 文件模式匹配
typescript 复制代码
interface GlobToolParams {
  pattern: string; // glob 模式(如 "**/*.ts")
  path?: string; // 搜索根目录
  respect_gitignore?: boolean;
}
  • Kind.Search,只读
  • 返回匹配文件的路径列表(按修改时间排序)
grep --- 文件内容搜索
typescript 复制代码
interface GrepToolParams {
  pattern: string; // 正则表达式或字符串
  include?: string; // 文件过滤(如 "*.ts")
  path?: string;
  fixed_strings?: boolean; // 是否作为纯字符串而非正则
  context?: number; // 匹配行前后各显示多少行
  // ...更多过滤参数
}
  • Kind.Search,只读
  • 底层使用 ripgrep@joshua.litt/get-ripgrep 包),性能极高
  • 回退策略:ripgrep 不可用时使用纯 Node.js 实现
ls --- 列出目录
  • Kind.Read,只读
  • 返回格式化的目录树(过滤 .gitignore 的路径)
read_many_files --- 批量读取
  • Kind.Read,只读
  • 支持 glob 模式批量读取,可递归
  • 用于 AI 需要"理解整个代码库"的场景

4.2 执行类工具

run_shell_command --- Shell 命令执行
typescript 复制代码
interface ShellToolParams {
  command: string; // 要执行的命令
  description?: string; // 人类可读的描述(显示在审批弹窗)
  dir_path?: string; // 工作目录
  is_background?: boolean; // 是否在后台运行(不等待结束)
}
  • Kind.Execute最敏感的工具,必须经过用户审批

  • 审批弹窗显示:解析后的命令根、完整命令、工作目录

  • 策略记忆 :用户选择"始终允许"时,记录的是命令前缀(如
    gitnpm),而不是完整命令

  • 支持后台执行is_background: true

    时立即返回,进程在后台运行,输出流式显示

  • 支持实时输出canUpdateOutput: true):Shell 命令执行时实时推送输出到 UI

  • 使用 node-pty 提供伪终端(PTY),保留 ANSI 颜色和光标控制

    typescript 复制代码
    // Shell 执行底层机制(ShellExecutionService):
    // 1. 用 node-pty 创建伪终端
    // 2. 每 1000ms 推送一次输出更新
    // 3. 输出超过阈值时截断,保存到临时文件
    // 4. 后台任务通过 coreEvents.emitFeedback() 通知 UI

4.3 网络类工具

typescript 复制代码
interface WebSearchToolParams {
  query: string; // 搜索关键词
}
  • Kind.Search,只读
  • 底层:通过 Gemini API 的 Google Search Grounding 功能实现
    • 不是独立发起 HTTP 请求,而是让 Gemini 模型调用内置的搜索工具
    • 结果包含 GroundingMetadata(来源链接和置信度)
  • 返回格式:搜索结果文本 + 来源列表
web_fetch --- 网页抓取
typescript 复制代码
interface WebFetchToolParams {
  url: string; // 要抓取的 URL
  prompt?: string; // 从抓取内容中提取什么信息
}
  • Kind.Fetch,只读
  • 底层使用 undicinode-fetch 发起 HTTP 请求
  • 将 HTML 转换为 Markdown(使用 html-to-text)以减少 token 消耗
  • 支持 Puppeteer(无头浏览器)处理动态渲染的页面

4.4 AI 辅助工具

save_memory --- 持久化记忆
typescript 复制代码
interface MemoryToolParams {
  fact: string; // 要记住的事实
}
  • 将信息写入 GEMINI.md 文件(在 ~/.gemini/ 或项目目录)
  • AI 每次启动时自动读取这些文件作为上下文
ask_user --- 向用户提问
typescript 复制代码
interface AskUserToolParams {
  questions: Array<{
    question: string;
    type: 'text' | 'select' | 'multiselect';
    options?: Array<{ label: string; description?: string }>;
  }>;
}
  • 显示结构化的问题表单,收集用户输入
  • 支持文本输入、单选、多选
  • 用于 AI 需要更多信息才能继续时
write_todos --- 任务清单
  • 在 UI 上显示结构化的 Todo 列表
  • 状态:pendingin_progresscompletedcancelledblocked
enter_plan_mode / exit_plan_mode --- 计划模式
  • 进入计划模式后,AI 只能读取文件、不能修改
  • 专门用于先规划后执行的工作流

五、ToolRegistry:工具注册表

ToolRegistry 是工具的"目录",管理所有可用工具:

typescript 复制代码
class ToolRegistry {
  private allKnownTools: Map<string, AnyDeclarativeTool>;

  // 注册一个工具
  registerTool(tool: AnyDeclarativeTool): void;

  // 获取可用工具列表(过滤被排除的工具)
  getActiveTools(): AnyDeclarativeTool[];

  // 生成 FunctionDeclaration 数组(发送给 Gemini API)
  getFunctionDeclarations(modelId?: string): FunctionDeclaration[];

  // 动态发现工具(执行配置的发现命令)
  discoverAllTools(): Promise<void>;
}

工具排序优先级

复制代码
1. 内置工具(ReadFile、Shell 等)    ← 优先级最高
2. 自定义发现工具(discovered_tool_*)
3. MCP 工具(按服务器名排序)         ← 优先级最低

动态工具发现(DiscoveredTool

可以在 settings.json 中配置工具发现命令:

json 复制代码
{
  "toolDiscoveryCommand": "my-tool-server list-tools",
  "toolCallCommand": "my-tool-server call"
}

发现流程:

  1. 执行 toolDiscoveryCommand,解析 JSON 输出(FunctionDeclaration[] 格式)
  2. 将每个工具包装为 DiscoveredTool,注册到 registry
  3. 调用时执行
    toolCallCommand <tool-name>,通过 stdin 传递参数,读取 stdout 作为结果

Plan Mode 下的工具限制

approvalMode === 'plan' 时,write_fileedit

的 schema 描述会被动态修改:

typescript 复制代码
// 添加限制说明,告诉 AI 只能操作计划目录
schema.description = `ONLY FOR PLANS: ${schema.description}.
You are currently in Plan Mode and may ONLY use this tool to write
or update plans (.md files) in the plans directory.`;

六、wait_for_previous 参数:并发控制的桥梁

每个工具的 schema 中都自动注入了 wait_for_previous 参数:

typescript 复制代码
// DeclarativeTool.addWaitForPreviousParameter() 自动注入
{
  wait_for_previous: {
    type: 'boolean',
    description: 'Set to true to wait for all previously requested tools
                  in this turn to complete before starting.'
  }
}

这是工具系统和调度系统之间的接口契约

  • AI 模型通过这个参数声明工具调用间的依赖关系
  • Scheduler 读取这个参数决定并行还是串行执行
  • 例如:先 grep 搜索,再 read_file 读取结果文件 → 后者需要
    wait_for_previous: true

七、ToolCallConfirmationDetails:确认弹窗系统

每种工具可以返回不同类型的确认弹窗详情:

type 工具 弹窗内容
edit WriteFile、Edit 显示文件 diff,支持编辑器打开修改
exec Shell 显示命令、工作目录、命令解析树
mcp MCP 工具 显示 MCP 服务器名、工具名、参数
info MemoryTool、技能等 通用信息提示
ask_user AskUser 问题表单
exit_plan_mode ExitPlanMode 计划内容审查

UI 层根据 type 字段渲染不同的确认组件(在 packages/cli/src/ui/components/

中)。


八、工具开发模式:如何实现一个新工具

  1. 定义参数类型
typescript 复制代码
interface MyToolParams {
  input: string;
  options?: { verbose: boolean };
}
  1. 创建 Invocation 类(执行逻辑)
typescript 复制代码
class MyToolInvocation extends BaseToolInvocation<MyToolParams, ToolResult> {
  getDescription() {
    return `Processing: ${this.params.input}`;
  }

  async execute(signal: AbortSignal): Promise<ToolResult> {
    // 实际执行逻辑
    const result = await doSomething(this.params.input);
    return {
      llmContent: result, // 给 AI 看
      returnDisplay: result, // 给用户看
    };
  }
}
  1. 创建 Tool 类(元数据 + 工厂)
typescript 复制代码
class MyTool extends BaseDeclarativeTool<MyToolParams, ToolResult> {
  constructor(messageBus: MessageBus) {
    super(
      'my_tool', // name(AI 调用时用的名字)
      'My Tool', // displayName
      'Does something...', // description(给 AI 看的说明)
      Kind.Read, // kind
      myParamSchema, // JSON Schema
      messageBus,
    );
  }

  protected createInvocation(params, messageBus) {
    return new MyToolInvocation(params, messageBus);
  }
}
  1. 注册到 ToolRegistry
typescript 复制代码
toolRegistry.registerTool(new MyTool(messageBus));

九、工具系统与其他模块的关系

复制代码
Gemini API
    │ 返回 FunctionCall(工具调用请求)
    ▼
Scheduler.schedule()
    │ 在 ToolRegistry 中查找工具
    ▼
ToolRegistry.getTool(name) → DeclarativeTool
    │ 调用 tool.build(args)
    ▼
DeclarativeTool.build() → ToolInvocation(验证参数)
    │ 传给 ToolExecutor
    ▼
ToolExecutor.execute()
    │ 调用 executeToolWithHooks()
    │     ├── 触发 before 钩子(Hooks 系统)
    │     ├── 调用 invocation.execute()
    │     └── 触发 after 钩子
    ▼
ToolResult
    │ 包装成 functionResponse Part
    ▼
发回给 Gemini API(下一轮对话)

CLI 终端 UI 深度解析

源码位置:packages/cli/src/

Gemini CLI 的终端界面是用 React + Ink

构建的------这是一个非常有趣的技术选型,让 Web 前端的组件化思维被带进了终端世界。本文从架构到核心 hook,完整拆解这套 UI 是如何工作的。


一、为什么用 React 写终端 UI?

传统终端 UI 用 ncurses

或直接写 ANSI 转义码,代码难以维护。Ink

的思路是:

  • 用 React 组件描述 UI 结构
  • 用 Flexbox 布局(基于 Yoga 引擎)自动计算位置
  • 将 React 的虚拟 DOM diff 映射到终端的"重绘哪些行"

这样就能用 useStateuseEffectuseMemo 等熟悉的工具来管理终端 UI 的状态。


二、整体目录结构

复制代码
cli/src/
├── gemini.tsx              ← 程序入口,初始化配置、选择运行模式
├── interactiveCli.tsx      ← 交互模式:启动 Ink 渲染引擎
├── nonInteractiveCli.ts    ← 非交互模式(管道/脚本)
│
└── ui/
    ├── App.tsx             ← 根组件(轻薄,只做路由)
    ├── AppContainer.tsx    ← 核心容器(重量级,所有状态在这里)
    │
    ├── components/         ← 纯展示组件(50+ 个)
    ├── hooks/              ← 业务逻辑 hooks(40+ 个)
    ├── contexts/           ← React Context(全局状态分发)
    ├── layouts/            ← 布局组件
    ├── state/              ← 状态定义
    ├── types.ts            ← UI 层类型定义
    └── themes/             ← 主题配置

三、启动流程

3.1 程序入口:gemini.tsx

复制代码
gemini.tsx 是整个程序的 main(),负责:

1. 解析 CLI 参数(yargs)
2. 加载配置(settings.json、环境变量)
3. 检测运行模式:
   ├── 有 stdin 管道?→ runNonInteractive()(headless 模式)
   ├── 有 --prompt 参数?→ runNonInteractive()
   └── 否则 → 启动 Ink(交互模式)
4. 处理认证(API Key / OAuth)
5. 启动沙箱(如果配置了 Docker/Podman)

3.2 交互模式:interactiveCli.tsx

typescript 复制代码
// 简化版流程
const { waitUntilExit } = render(
  <SettingsProvider settings={settings}>
    <OverflowProvider>
      <SessionProvider>
        <VimModeProvider>
          <KeypressProvider>
            <AppContainer
              config={config}
              version={version}
              initializationResult={initResult}
            />
          </KeypressProvider>
        </VimModeProvider>
      </SessionProvider>
    </OverflowProvider>
  </SettingsProvider>
);
await waitUntilExit();

render()

是 Ink 提供的函数,将 React 组件树渲染到终端,返回一个 Promise,在用户退出前一直等待。


四、组件层级与职责分工

复制代码
AppContainer(2600 行的"上帝组件")
    │ 管理所有业务状态,通过 Context 向下传递
    │
    └── App(路由组件,决定显示什么)
            │
            ├── QuittingDisplay        ← 退出时的清理显示
            └── DefaultAppLayout       ← 正常运行时的布局
                    │
                    ├── [Static 区域]  ← 历史消息(已固定,不再重绘)
                    │    ├── AppHeader     ← 顶部标题栏
                    │    └── HistoryList   ← 所有历史消息
                    │         ├── UserMessage
                    │         ├── ModelMessage(流式渲染)
                    │         ├── ToolCallGroup(工具调用组)
                    │         │    ├── ToolCallDisplay(单个工具)
                    │         │    └── ConfirmationDialog(审批弹窗)
                    │         └── InfoMessage / ErrorMessage
                    │
                    └── [Dynamic 区域] ← 活跃区(持续重绘)
                         ├── Composer(用户输入框 + 自动补全)
                         ├── LoadingIndicator(思考中...)
                         ├── ApprovalModeIndicator(当前审批模式)
                         └── StatusBar(底部状态栏)

Static 区域 vs Dynamic 区域

Ink 有一个关键性能优化机制:<Static> 组件。

复制代码
<Static>
  ─ 已完成的历史消息,渲染一次后永不重绘
  ─ 减少大量无效的终端重绘
</Static>

<Box>(动态区域)
  ─ 正在进行的流式输出、输入框
  ─ 每帧都可能重绘
</Box>

五、核心状态管理

5.1 AppContainer:唯一真相来源

AppContainer.tsx 是整个 UI 的状态中枢 ,管理了约 60+ 个状态变量,包括:

typescript 复制代码
// 核心业务状态
const [isProcessing, setIsProcessing] = useState(false);
const [streamingState, ...] = useGeminiStream(...);   // AI 流式响应

// 对话历史
const historyManager = useHistory(...);

// UI 状态
const [quittingMessages, setQuittingMessages] = useState(null);
const [constrainHeight, setConstrainHeight] = useState(true);
const [showErrorDetails, setShowErrorDetails] = useState(false);

// 弹窗状态(15+ 个)
const [isThemeDialogOpen, ...] = useThemeCommand(...);
const [isModelDialogOpen, ...] = useModelCommand(...);
const [isAuthDialogOpen, ...] = useAuthCommand(...);
// ...

// 终端信息
const { columns: terminalWidth, rows: terminalHeight } = useTerminalSize();

5.2 Context 体系:状态的分发

状态通过多层 Context 向子组件传递(避免 prop drilling):

复制代码
UIStateContext       ← 所有只读状态(60+ 个字段的 uiState 对象)
UIActionsContext     ← 所有操作方法(handleFinalSubmit、refreshStatic 等)
ConfigContext        ← Config 实例
AppContext           ← version、startupWarnings
ToolActionsContext   ← 工具审批操作
SessionContext       ← 会话统计
SettingsContext      ← 用户设置
VimModeContext       ← Vim 模式状态
OverflowContext      ← 内容溢出状态
KeypressContext      ← 全局键盘事件
ShellFocusContext    ← Shell 焦点状态

六、最重要的 Hook:useGeminiStream

源码:packages/cli/src/ui/hooks/useGeminiStream.ts

这是整个 UI 层最核心的 hook,负责驱动与 AI 模型的完整交互流程

6.1 它管理的状态

typescript 复制代码
const [streamingState, setStreamingState] = useState<StreamingState>(
  StreamingState.Idle,
);

// StreamingState 枚举:
enum StreamingState {
  Idle = 'idle', // 等待用户输入
  Responding = 'responding', // AI 正在回复(流式输出)
  WaitingForConfirmation = 'waiting_for_confirmation', // 等待工具审批
}

6.2 主流程:submitQuery()

复制代码
用户按 Enter → handleFinalSubmit() → submitQuery()
    │
    ▼
1. 将用户消息加入 historyManager(显示在 UI 上)
2. 调用 geminiClient.sendMessage(userMessage)
3. 监听事件流(AsyncGenerator):
    │
    ├── ContentEvent(文本块)
    │    └── 流式追加到当前 ModelMessage 组件
    │
    ├── ThinkingEvent(AI 思考内容,如果开启)
    │    └── 显示在 ThinkingDisplay 组件
    │
    ├── ToolCallEvent(工具调用请求)
    │    └── 传给 useToolScheduler → Scheduler.schedule()
    │         └── 工具执行结果再次发回给 AI
    │
    └── FinishedEvent(AI 完成)
         └── 将 pending 历史转为 static

6.3 流式输出的渲染方式

typescript 复制代码
// pending 状态:正在流式接收的消息(在动态区域渲染)
const [pendingHistoryItems, setPendingHistoryItems] = useState([]);

// 每收到一个 ContentEvent,追加文本到 pending item
// React 重新渲染,用户看到文字一个一个出现

// FinishedEvent 收到后:
// 1. 将 pendingHistoryItems 转移到 historyManager.history(进入 Static 区域)
// 2. pending 清空
// 3. 触发 refreshStatic()(重绘静态区域)

七、输入系统:useTextBuffer + Composer

7.1 文本缓冲区

输入框底层是一个自研的 useTextBuffer hook(不用 Ink 自带的
TextInput),支持:

  • 多行编辑
  • 光标移动(方向键、Home/End)
  • Vim 模式(useVim hook)
  • 历史导航(上下箭头,类似 Shell)
  • 粘贴路径自动转义(Windows 路径 → POSIX 路径)
  • @ 命令补全(@file.ts 引用文件)

7.2 输入的处理路径

复制代码
用户输入字符
    │
    ├── 普通字符 → useTextBuffer 追加到缓冲区
    ├── @ 前缀  → useAtCompletion 触发文件补全
    ├── / 前缀  → useCommandCompletion 触发命令补全
    └── Enter   → handleFinalSubmit()
                     │
                     ├── 是斜杠命令?→ handleSlashCommand()
                     ├── AI 正在回复且开启 Model Steering?→ handleHintSubmit()(注入引导提示)
                     └── 否则 → submitQuery()(发给 AI)

八、键盘事件系统

8.1 useKeypress 与优先级

键盘事件通过 KeypressContext 全局管理,支持优先级

typescript 复制代码
enum KeypressPriority {
  Critical = 0, // 最高,如复制模式退出
  High = 1, // 全局快捷键(Ctrl+C、Ctrl+D)
  Normal = 2, // 普通输入
  Low = 3, // 背景处理
}

// 注册一个键盘监听器
useKeypress(handleGlobalKeypress, {
  isActive: true,
  priority: KeypressPriority.High,
});

高优先级处理器先收到事件,返回 true 表示"已消费,不再传播"。

8.2 主要快捷键映射

快捷键 行为
Enter 提交输入
Ctrl+C 取消当前操作(第二次退出程序)
Ctrl+D 退出程序
Ctrl+O 展开/折叠内容块
Ctrl+E 切换错误详情显示
Ctrl+T 切换 Todo 显示
Ctrl+M 切换 Markdown 渲染
Ctrl+Z 挂起进程(回到 Shell)
Tab 聚焦背景 Shell
Shift+Tab 取消聚焦背景 Shell

九、历史管理:useHistoryManager

历史记录是 UI 层独立维护的(区别于 AI 的对话历史):

typescript 复制代码
// HistoryItem 的类型联合
type HistoryItem =
  | { type: 'user'; text: string } // 用户消息
  | { type: 'model'; text: string } // AI 回复
  | { type: 'tool_group'; tools: ToolCall[] } // 工具调用组
  | { type: 'info'; text: string } // 系统信息
  | { type: 'error'; text: string } // 错误信息
  | { type: 'thinking'; text: string } // AI 思考过程
  | { type: 'hint'; text: string }; // 用户引导提示

每条消息有唯一 ID,历史管理器负责:

  • addItem():添加新消息
  • clearItems():清空(/clear 命令)
  • loadHistory():从持久化存储恢复会话

十、工具审批的 UI 流程

工具调用的审批弹窗是整个 UI 里最复杂的交互:

复制代码
Scheduler 发出 TOOL_CALLS_UPDATE 事件(状态变为 AwaitingApproval)
    │
    ▼
useToolScheduler hook 收到更新
    │
    ▼
pendingHistoryItems 中的 tool_group 状态更新
    │
    ▼
ToolCallGroup 组件渲染 ConfirmationDialog
    │
    ├── type='edit'   → FileDiffDialog(显示 diff + 行号)
    ├── type='exec'   → ExecConfirmDialog(显示命令树)
    ├── type='mcp'    → McpConfirmDialog
    └── type='info'   → GenericConfirmDialog
    │
    ▼
用户点击确认/取消按钮
    │
    ▼
messageBus.publish(TOOL_CONFIRMATION_RESPONSE)
    │
    ▼
Scheduler 的 resolveConfirmation() 收到响应,继续执行

十一、特殊 UI 特性

11.1 交替缓冲区(Alternate Buffer)

typescript 复制代码
// 类似 vim/less 的全屏模式
// 进入时:清空终端,独占全屏
// 退出时:恢复之前的终端内容

const shouldUseAlternateScreen = shouldEnterAlternateScreen(
  isAlternateBuffer,
  config.getScreenReader(),
);

11.2 背景 Shell

用户可以在 AI 对话的同时开启一个嵌入的 Shell 窗口(基于 node-pty),快捷键 Tab

切换焦点:

typescript 复制代码
// useBackgroundShellManager 管理多个后台 Shell
// 每个 Shell 用 PTY 渲染(保留颜色、光标等)
// backgroundShells: Map<pid, BackgroundShell>

11.3 主题系统

typescript 复制代码
// 主题定义了所有颜色的语义别名
const theme = {
  colors: {
    primary: '#4285F4', // Google Blue
    accent: '#34A853',
    error: '#EA4335',
    warning: '#FBBC04',
    // ...
  },
};

// 组件通过 useTheme() 获取颜色,不硬编码

自动检测终端背景色(亮色/暗色),切换对应主题。

11.4 Vim 模式

typescript 复制代码
// useVim hook 拦截键盘输入
// Normal 模式:hjkl 移动,dd 删除行,yy 复制行
// Insert 模式:正常输入
// 通过 /vim 命令开启

十二、AppContainer 的核心数据流总结

复制代码
用户按键
    │ useKeypress / useTextBuffer
    ▼
handleFinalSubmit()
    │
    ▼
useGeminiStream.submitQuery()
    │          ┌──────────────────────────────────┐
    ▼          │  AI 流式响应                      │
geminiClient.sendMessage()                        │
    │          │  ContentEvent → 流式渲染文字      │
    │          │  ToolCallEvent → useToolScheduler │
    │          │      → Scheduler.schedule()       │
    │          │      → 工具执行 → 结果返回 AI     │
    │          │  FinishedEvent → 消息固化          │
    │          └──────────────────────────────────┘
    ▼
historyManager.addItem()
    │
    ▼
UIStateContext 更新(uiState 对象)
    │
    ▼
React 重新渲染 → Ink 计算 diff → 写入终端

十三、关键设计模式总结

模式 应用场景
容器/展示分离 AppContainer(逻辑)vs 各组件(展示)
Context 替代 prop drilling UIStateContext 向下传递 60+ 字段
Static 区域优化 历史消息不重绘,只重绘活跃区域
事件驱动 coreEvents 将 core 层变化推送给 UI
优先级键盘 KeypressContext 实现事件消费链
自定义 hook 40+ 个 hook 拆分复杂逻辑,保持 AppContainer 可读

Hooks 钩子系统深度解析

源码位置:packages/core/src/hooks/
packages/core/src/core/coreToolHookTriggers.ts

Hooks 是 Gemini

CLI 的扩展插件机制,允许在 AI 代理执行的关键节点注入自定义逻辑------比如在工具调用前后执行脚本、拦截模型请求、监控会话生命周期等。


一、Hooks 解决什么问题?

没有 Hooks,整个 AI 代理执行过程是一个封闭系统。Hooks 打开了若干"切面"(Aspect),让外部逻辑可以:

  1. 拦截并修改工具参数(BeforeTool)
  2. 阻止不安全的工具调用(BeforeTool 返回 deny)
  3. 增强工具结果(AfterTool 附加上下文)
  4. 监控模型的每次 LLM 调用(BeforeModel / AfterModel)
  5. 响应会话生命周期(SessionStart / SessionEnd)
  6. 发送通知(Notification,如企业级告警系统)

二、11 种 Hook 事件

typescript 复制代码
// packages/core/src/hooks/types.ts
enum HookEventName {
  BeforeTool = 'BeforeTool', // 工具执行前
  AfterTool = 'AfterTool', // 工具执行后
  BeforeAgent = 'BeforeAgent', // 用户 prompt 提交前
  AfterAgent = 'AfterAgent', // AI 回复完成后
  SessionStart = 'SessionStart', // 会话开始
  SessionEnd = 'SessionEnd', // 会话结束
  PreCompress = 'PreCompress', // 上下文压缩前
  BeforeModel = 'BeforeModel', // 每次 LLM API 调用前
  AfterModel = 'AfterModel', // 每次 LLM API 调用后
  BeforeToolSelection = 'BeforeToolSelection', // 工具列表发给模型前
  Notification = 'Notification', // 需要用户确认时的通知
}

各事件触发时机

事件 触发时机 典型用途
BeforeAgent 用户发送 prompt,AI 开始处理前 注入系统上下文(当前时间、环境信息)
BeforeModel 每次调用 Gemini API 之前 修改 prompt、添加系统指令、短路 LLM 调用
BeforeToolSelection 把工具列表发给模型前 动态过滤可用工具(按权限、按场景)
BeforeTool 某个工具即将执行前 参数校验、修改参数、阻止危险操作
AfterTool 工具执行完成后 结果增强、触发链式工具、审计日志
AfterModel 每次 Gemini API 返回后 过滤敏感内容、修改 AI 回复
AfterAgent AI 完成一轮回复后 持久化结果、发送通知、清理上下文
SessionStart 会话初始化时(启动/恢复/clear) 初始化状态、加载工作区上下文
SessionEnd 会话退出时 清理资源、保存历史
PreCompress 上下文自动或手动压缩前 备份对话历史
Notification 工具需要用户审批时 发送企业告警(Slack、邮件等)

三、Hook 的两种类型

3.1 Command Hook(命令钩子)

执行一个 Shell 命令,通过 stdin/stdout 与 Gemini CLI 通信:

json 复制代码
// ~/.gemini/settings.json 或 .gemini/settings.json
{
  "hooks": {
    "BeforeTool": [
      {
        "matcher": "run_shell_command",
        "hooks": [
          {
            "type": "command",
            "command": "python3 /path/to/security_check.py",
            "timeout": 30000
          }
        ]
      }
    ]
  }
}

通信协议

  • Gemini CLI 将 HookInput 序列化为 JSON,写入进程的 stdin
  • Hook 脚本从 stdin 读取 JSON,处理后将 HookOutput JSON 写入
    stdout(或 stderr)
  • 退出码含义:
    • 0 → 允许(allow)
    • 1 → 非阻塞警告(allow with warning)
    • 2+ → 阻止执行(deny/block)

示例 Hook 脚本(Python)

python 复制代码
import sys, json

data = json.load(sys.stdin)
tool_name = data.get("tool_name", "")
tool_input = data.get("tool_input", {})

# 阻止删除 .env 文件
if tool_name == "write_file" and ".env" in str(tool_input.get("path", "")):
    print(json.dumps({
        "decision": "deny",
        "reason": "不允许修改 .env 文件"
    }))
    sys.exit(2)

# 允许其他操作
print(json.dumps({"decision": "allow"}))
sys.exit(0)

3.2 Runtime Hook(运行时钩子)

通过编程方式注册 TypeScript 函数,适用于 SDK 集成场景:

typescript 复制代码
// 通过 HookSystem.registerHook() 注册
hookSystem.registerHook(
  {
    type: HookType.Runtime,
    name: 'my-security-hook',
    action: async (input: HookInput) => {
      // 检查工具调用
      return { decision: 'allow' };
    },
    timeout: 5000,
  },
  HookEventName.BeforeTool,
);

四、架构:五大组件

复制代码
HookSystem(门面/入口)
    │
    ├── HookRegistry     --- 注册表,管理所有 Hook 配置
    ├── HookPlanner      --- 规划器,根据事件找出匹配的 Hook
    ├── HookRunner       --- 执行器,真正运行 Hook 命令/函数
    ├── HookAggregator   --- 聚合器,合并多个 Hook 的输出结果
    └── HookEventHandler --- 事件处理器,协调整个触发流程

4.1 HookRegistry --- 注册表

负责从配置文件加载 Hook,并管理其生命周期:

typescript 复制代码
export class HookRegistry {
  private entries: HookRegistryEntry[] = [];

  // 配置源优先级(数字越小越高)
  // Runtime(0) > Project(1) > User(2) > System(3) > Extensions(4)
}

配置来源

  • Runtime:代码中动态注册(最高优先级)
  • Project.gemini/settings.json(需要信任文件夹)
  • User~/.gemini/settings.json
  • System:系统级配置
  • Extensions:扩展插件提供的 Hook

安全检查:项目级 Hook 只在"受信任文件夹"中执行。首次遇到项目 Hook 时,会向用户显示警告,并请求信任。

4.2 HookPlanner --- 规划器

根据事件名和上下文,从注册表中筛选出要执行的 Hook,生成执行计划

typescript 复制代码
createExecutionPlan(eventName, context?) {
  // 1. 从注册表获取该事件的所有 Hook
  // 2. 按 matcher 过滤(工具名匹配 / 触发源匹配)
  // 3. 去重(相同命令的 Hook 只执行一次)
  // 4. 决定是串行还是并行执行
  return HookExecutionPlan;
}

Matcher 过滤

json 复制代码
{
  "BeforeTool": [
    {
      "matcher": "write_file",       // 精确匹配工具名
      "hooks": [...]
    },
    {
      "matcher": "write_.*|edit_.*", // 正则匹配
      "hooks": [...]
    },
    {
      "matcher": "*",                // 匹配所有工具
      "hooks": [...]
    }
  ]
}

4.3 HookRunner --- 执行器

真正执行单个 Hook:

Command Hook 执行流程

复制代码
1. 安全检查:项目 Hook 需要信任文件夹
2. 构建 Shell 命令(支持 $GEMINI_PROJECT_DIR 变量展开)
3. 设置环境变量(sanitizeEnvironment 清理敏感变量)
4. spawn 子进程,写入 JSON input 到 stdin
5. 收集 stdout/stderr
6. 进程退出时,解析输出为 HookOutput
7. 返回 HookExecutionResult

并行 vs 串行执行

typescript 复制代码
// 并行(默认)
executeHooksParallel(hookConfigs, eventName, input)
  → Promise.all(promises)

// 串行:前一个 Hook 的输出会合并到下一个 Hook 的输入
executeHooksSequential(hookConfigs, eventName, input)
  → for...of loop,applyHookOutputToInput() 传递修改

串行执行时,输出到输入的传递规则:

  • BeforeAgentadditionalContext 追加到 prompt
  • BeforeModelllm_request 合并到下一个请求
  • BeforeTooltool_input 合并到下一个工具输入

超时机制:默认 60 秒,超时后先发 SIGTERM,5 秒后发 SIGKILL(Windows 用 taskkill)。

4.4 HookAggregator --- 聚合器

将多个 Hook 的执行结果合并为最终输出:

复制代码
多个 HookExecutionResult
        │
        ▼
HookAggregator.aggregate()
        │
        ▼
AggregatedHookResult { finalOutput, allResults, ... }

聚合策略:优先级最高的阻塞决策优先;否则合并所有 systemMessage 等非冲突字段。

4.5 HookEventHandler --- 事件处理器

对外暴露 fire*Event() 方法,内部协调 Planner → Runner →

Aggregator 的完整流程:

typescript 复制代码
async fireBeforeToolEvent(toolName, toolInput, ...): Promise<AggregatedHookResult> {
  // 1. HookPlanner.createExecutionPlan(BeforeTool, { toolName })
  // 2. HookRunner.executeHooks[Parallel|Sequential](plan.hookConfigs, ...)
  // 3. HookAggregator.aggregate(results)
  // 4. return result
}

五、与工具执行的集成点

这是 Hooks 最核心的应用。executeToolWithHooks() 函数包装了每一次工具执行:

typescript 复制代码
// packages/core/src/core/coreToolHookTriggers.ts

export async function executeToolWithHooks(
  invocation, toolName, signal, tool, liveOutputCallback, options, config
): Promise<ToolResult> {

  // ① BeforeTool Hook
  const beforeOutput = await hookSystem.fireBeforeToolEvent(toolName, toolInput)

  if (beforeOutput?.shouldStopExecution())  → 停止整个 Agent
  if (beforeOutput?.getBlockingError())     → 阻止工具执行,返回错误
  if (beforeOutput?.getModifiedToolInput()) → 修改参数后重建 invocation

  // ② 执行实际工具
  const toolResult = await invocation.execute(signal, ...)

  // ③ AfterTool Hook
  const afterOutput = await hookSystem.fireAfterToolEvent(toolName, toolInput, toolResult)

  if (afterOutput?.shouldStopExecution())    → 停止整个 Agent
  if (afterOutput?.getBlockingError())       → 屏蔽工具结果
  if (afterOutput?.getAdditionalContext())   → 追加 <hook_context>...</hook_context>
  if (afterOutput?.getTailToolCallRequest()) → 触发链式工具调用

  return toolResult
}

BeforeTool 的参数修改

当 BeforeTool Hook 返回 hookSpecificOutput.tool_input 时,框架会:

  1. 用新参数覆盖原始参数(Object.assign
  2. 调用 tool.build(newParams) 重建 invocation(重新验证参数,确保派生状态如
    resolvedPath 更新)
  3. 在工具结果中附加修改通知

AfterTool 的上下文注入

typescript 复制代码
// Hook 脚本返回:
{ "hookSpecificOutput": { "additionalContext": "文件最后修改时间: 2025-01-01" } }

// 被包装后发给 AI:
"...工具原始结果...\n\n<hook_context>文件最后修改时间: 2025-01-01</hook_context>"

AfterTool 触发链式调用(Tail Call)

typescript 复制代码
// Hook 脚本返回:
{
  "hookSpecificOutput": {
    "tailToolCallRequest": {
      "name": "read_file",
      "args": { "path": "/tmp/generated_output.txt" }
    }
  }
}
// → Scheduler 会用新工具调用"替换"当前调用,继续执行

六、与 LLM 调用的集成点

除了工具调用,Hooks 还可以拦截 LLM 请求本身:

BeforeModel Hook

复制代码
每次调用 Gemini API 前触发
    │
    ├── 返回 continue=false → 停止整个 Agent 执行
    ├── 返回 decision=block + llm_response → 短路 LLM 调用,使用合成响应
    └── 返回 llm_request 修改 → 修改 contents/config 后再发给 Gemini

合成响应(短路)示例

json 复制代码
{
  "decision": "block",
  "reason": "敏感话题,拒绝处理",
  "hookSpecificOutput": {
    "llm_response": {
      "candidates": [
        { "content": { "parts": [{ "text": "抱歉,这个话题我无法处理。" }] } }
      ]
    }
  }
}

BeforeToolSelection Hook

在工具列表发送给模型之前触发,可以动态修改工具配置:

json 复制代码
// 限制模型只能使用 ANY_MODE(不能强制使用特定工具)
{
  "hookSpecificOutput": {
    "toolConfig": {
      "functionCallingConfig": { "mode": "ANY" }
    }
  }
}

AfterModel Hook

每个 LLM 响应 chunk 返回后触发(流式场景下每个 chunk 都会触发),可以修改模型输出。


七、Hook Output 的决策类型

typescript 复制代码
type HookDecision = 'ask' | 'block' | 'deny' | 'approve' | 'allow' | undefined;
Decision 效果
allow / approve 允许继续执行
block / deny 阻止执行,返回错误给 AI
ask 要求用户手动确认(配合 Notification 系统)
undefined 不干预,继续执行

continue: false 的区别:

  • decision: 'deny' → 阻止当前工具,AI 收到错误信息后可重试
  • continue: false → 停止整个 Agent 执行循环

八、Session 级钩子

SessionStart

typescript 复制代码
// 触发时机:
enum SessionStartSource {
  Startup = 'startup', // CLI 首次启动
  Resume = 'resume', // 恢复之前的会话
  Clear = 'clear', // 用户执行 /clear 后
}

可返回 additionalContext,自动注入为系统消息(如当前目录结构、Git 状态等)。

AfterAgent

每次 AI 完整回答用户后触发。特殊能力:

typescript 复制代码
// AfterAgent Hook 可请求清空对话上下文
{ "hookSpecificOutput": { "clearContext": true } }
// → 触发 /clear,开始新对话(常用于多轮工作流的阶段切换)

九、安全模型

9.1 受信任文件夹(Trusted Folder)

项目级 Hook(.gemini/settings.json)默认不被信任 。当 Gemini

CLI 进入一个包含项目 Hook 的目录时:

  1. TrustedHooksManager 检查该目录是否已被信任
  2. 如果未信任,发出警告,列出所有待执行的 Hook 命令
  3. 用户确认后,将信任状态记录在 ~/.gemini/trusted-hooks.json
  4. 后续访问该目录时直接信任

9.2 环境变量净化

Command Hook 在执行时,环境变量会经过 sanitizeEnvironment()

清理,防止泄露敏感变量(如 API keys)给 Hook 脚本。

9.3 Hook 执行不影响主流程

Hook 执行失败(非零退出码、JSON 解析错误、超时)时,默认不会阻断 主流程------错误会被
debugLogger.warn()

记录,但程序继续运行。这保证了 Hook 的"插件性":坏掉的 Hook 不会搞崩整个 CLI。


十、配置完整示例

json 复制代码
// .gemini/settings.json
{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "cat /path/to/workspace-context.txt",
            "timeout": 5000
          }
        ]
      }
    ],
    "BeforeTool": [
      {
        "matcher": "run_shell_command",
        "sequential": false,
        "hooks": [
          {
            "type": "command",
            "command": "python3 ~/.gemini/hooks/security_check.py",
            "timeout": 10000,
            "env": { "RULES_PATH": "/etc/security/rules.json" }
          }
        ]
      }
    ],
    "AfterTool": [
      {
        "matcher": "write_file|edit_file",
        "hooks": [
          {
            "type": "command",
            "command": "node ~/.gemini/hooks/audit_logger.js"
          }
        ]
      }
    ],
    "Notification": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "curl -X POST https://hooks.slack.com/... -d @-"
          }
        ]
      }
    ]
  }
}

十一、关键设计模式总结

模式 应用
门面模式 HookSystem 提供统一入口,隐藏内部五个组件的复杂性
职责分离 Registry(存储)、Planner(筛选)、Runner(执行)、Aggregator(合并)各司其职
配置源优先级 Runtime > Project > User > System > Extensions,高优先级覆盖低优先级
零侵入原则 Hook 失败不影响主流程,保证插件可靠性
进程隔离 Command Hook 在独立子进程运行,崩溃不影响 CLI
JSON 协议 stdin/stdout JSON 协议简单通用,任何语言均可实现 Hook

十二、与其他模块的关系

复制代码
Config(配置加载)
    │ getHooks() / isTrustedFolder()
    ▼
HookSystem
    │ initialize()
    ▼
HookRegistry(从 settings.json 加载 Hook)

executeToolWithHooks()(工具执行包装器)
    │ fireBeforeToolEvent / fireAfterToolEvent
    ▼
HookSystem.hookEventHandler
    │
    ├── HookPlanner.createExecutionPlan()  →  筛选匹配的 Hook
    ├── HookRunner.executeHooks[...]()     →  运行 Shell 命令 / 函数
    └── HookAggregator.aggregate()        →  合并结果

GeminiClient(Agent 循环)
    │ fireBeforeModelEvent / fireAfterModelEvent
    ▼
HookSystem(拦截 LLM 调用)

MCP 协议集成深度解析

源码位置:packages/core/src/tools/mcp-client.tsmcp-tool.tsmcp-client-manager.ts

MCP(Model Context

Protocol)是 Anthropic 提出的开放标准,让 AI 模型能够以统一的协议对接外部工具服务器 。Gemini

CLI 实现了完整的 MCP 客户端,让用户可以将任何 MCP 兼容服务器的能力注入到 AI 代理中。


一、MCP 解决什么问题?

没有 MCP,每个 AI 工具都需要单独集成:写专用代码、定义类型、处理传输。MCP 提供了一套标准协议:

复制代码
Gemini CLI(MCP Client)  ←────── MCP 协议 ──────→  MCP Server(任意语言实现)
                                                       ├── GitHub Tools
                                                       ├── Kubernetes Tools
                                                       ├── Database Tools
                                                       └── 自定义业务工具

MCP 服务器可以暴露三类资源:

  • Tools(工具) :AI 可调用的函数(如 search_issuescreate_branch
  • Prompts(提示词) :预定义的对话模板(如 code_reviewdebug_session
  • Resources(资源):AI 可读取的文档/文件(如数据库 schema、配置文档)

二、三种传输方式

typescript 复制代码
// packages/core/src/tools/mcp-client.ts

// 传输方式 1:stdio(本地子进程)
StdioClientTransport({
  command: 'node',
  args: ['./my-mcp-server.js'],
  env: { API_KEY: '...' },
});

// 传输方式 2:SSE(Server-Sent Events,旧式远程)
SSEClientTransport(new URL('https://my-server.com/sse'));

// 传输方式 3:StreamableHTTP(新式远程,推荐)
StreamableHTTPClientTransport(new URL('https://my-server.com/mcp'));
传输 适用场景 配置方式
stdio 本地工具服务器(Node.js、Python 等) command + args
sse 旧版远程服务器 url + type: "sse"
http 新版远程服务器(推荐) url + type: "http"httpUrl

自动协议协商 :配置了 url 但未指定 type 时,Gemini

CLI 会先尝试 StreamableHTTP,若返回 404 则自动降级为 SSE。


三、配置方式

json 复制代码
// ~/.gemini/settings.json 或 .gemini/settings.json
{
  "mcpServers": {
    "github": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"],
      "env": { "GITHUB_TOKEN": "$GITHUB_TOKEN" },
      "trust": true
    },
    "my-remote-server": {
      "url": "https://my-mcp-server.com/mcp",
      "type": "http",
      "headers": { "X-API-Key": "$MY_API_KEY" },
      "timeout": 30000,
      "includeTools": ["search", "create_issue"],
      "excludeTools": ["delete_repo"]
    }
  }
}

关键配置项

字段 说明
command / args stdio 传输的命令和参数
url / httpUrl 远程服务器 URL
type "http""sse"(不填则自动检测)
env 注入给服务器进程的环境变量(支持 $VAR 展开)
headers HTTP 请求头(支持 $VAR 展开)
trust true = 无需用户确认直接执行该服务器的工具
includeTools 白名单:只启用列出的工具
excludeTools 黑名单:禁用列出的工具(优先于 includeTools)
timeout 工具调用超时(默认 10 分钟)
oauth OAuth 认证配置

四、核心类:McpClient

每个 MCP 服务器对应一个 McpClient 实例,负责管理完整的连接生命周期:

typescript 复制代码
export class McpClient {
  private client: Client | undefined; // @modelcontextprotocol/sdk 的底层客户端
  private transport: Transport | undefined; // 传输层
  private status: MCPServerStatus; // 当前状态

  // 防止并发刷新的状态标志
  private isRefreshingTools = false;
  private pendingToolRefresh = false;
  // ...类似的 resources, prompts
}

状态机

复制代码
DISCONNECTED
    │ connect()
    ▼
CONNECTING
    │ 连接成功
    ▼
CONNECTED ──────────────────────────────────────────────────────────────────
    │                                                                        │
    │ 收到 ToolListChanged 通知                                              │
    ▼                                                                        │
refreshTools()(合并模式)                                            disconnect()
    │                                                                        │
    └──── 重新查询工具列表,更新 ToolRegistry ────────────────────────────────┘
                                                                     │
                                                                  DISCONNECTED

五、连接与发现流程

复制代码
CLI 启动
  │
  ▼
discoverMcpTools()(并发处理所有服务器)
  │
  ├── 对每个服务器 → connectAndDiscover()
  │      │
  │      ├── createTransport()(根据配置选择 Stdio/SSE/HTTP)
  │      │      └── 检查安全(stdio 需要 trusted folder)
  │      │
  │      ├── mcpClient.connect(transport)
  │      │      └── 如果返回 401 → OAuth 认证流程
  │      │
  │      ├── discoverTools()
  │      │      ├── mcpClient.listTools()
  │      │      ├── 过滤 excludeTools / includeTools
  │      │      └── 包装成 DiscoveredMCPTool 实例
  │      │
  │      ├── discoverPrompts()
  │      │      └── mcpClient.listPrompts()
  │      │
  │      └── discoverResources()
  │             └── mcpClient.resources/list(支持分页 cursor)
  │
  └── 将所有 DiscoveredMCPTool 注册到 ToolRegistry

discoverMcpTools() 使用 Promise.all()
并发连接所有服务器
,最大化启动速度。单个服务器连接失败不影响其他服务器。


六、工具命名约定

MCP 工具在 Gemini CLI 中使用统一的命名规则:

复制代码
mcp_{服务器名}_{工具名}

示例:
  服务器名: "github"    工具名: "create_issue"
  → mcp_github_create_issue

  服务器名: "my-server" 工具名: "search files"
  → mcp_my_server_search_files  (特殊字符替换为下划线)

规范处理(generateValidName()

  1. 强制添加 mcp_ 前缀
  2. 将非法字符(不在 [a-zA-Z0-9_\-.] 中的)替换为 _
  3. 若名称超过 63 字符,截断中间部分:前30字符...后30字符

工具通配符(用于策略规则):

复制代码
mcp_*                  → 所有 MCP 工具
mcp_github_*           → github 服务器的所有工具
mcp_*_create_issue     → 所有服务器的 create_issue 工具

七、MCP 工具如何融入现有工具体系

MCP 工具通过 DiscoveredMCPTool 类实现,完全遵循内置工具的抽象接口:

复制代码
BaseDeclarativeTool<ToolParams, ToolResult>    (抽象基类,内置工具也继承它)
    └── DiscoveredMCPTool                       (MCP 工具的工厂类)
            └── createInvocation()
                    └── DiscoveredMCPToolInvocation  (单次调用执行器)
                            └── execute()
                                    └── mcpTool.callTool()   → 发送到 MCP 服务器

这意味着 MCP 工具无缝对接调度器、Hook 系统、策略引擎,完全不需要特殊处理。

工具执行流程

typescript 复制代码
// DiscoveredMCPToolInvocation.execute()

async execute(signal: AbortSignal): Promise<ToolResult> {
  // 1. 构建 FunctionCall 请求(使用服务器原始工具名)
  const functionCalls = [{ name: this.serverToolName, args: this.params }]

  // 2. 发送给 MCP 服务器(带 AbortSignal 支持取消)
  const rawResponseParts = await mcpTool.callTool(functionCalls)

  // 3. 检查 MCP 错误(isError 字段)
  if (isMCPToolError(rawResponseParts)) { return error }

  // 4. 转换 MCP 内容块 → GenAI Part[]
  return {
    llmContent: transformMcpContentToParts(rawResponseParts),
    returnDisplay: getStringifiedResultForDisplay(rawResponseParts),
  }
}

内容块转换

MCP 工具返回的内容是结构化的"内容块数组",需要转换为 Gemini API 的 Part 格式:

复制代码
McpContentBlock 类型      →    GenAI Part
───────────────────────────────────────────────
{ type: "text", text }    →    { text }
{ type: "image", data }   →    { text: "[Image...]" } + { inlineData }
{ type: "audio", data }   →    { text: "[Audio...]" } + { inlineData }
{ type: "resource" }      →    { text } 或 { inlineData }(取决于是否有 blob)
{ type: "resource_link" } →    { text: "Resource Link: title at uri" }

八、用户确认机制

MCP 工具默认需要用户每次确认(因为执行的是外部未知服务器的代码)。

typescript 复制代码
// 三个信任级别:
1. trust: true(配置中)+ isTrustedFolder() → 完全免确认
2. 用户选择"始终允许此工具" → toolAllowListKey 加入内存白名单
3. 用户选择"始终允许此服务器" → serverAllowListKey 加入内存白名单

确认对话框显示:

  • 服务器名、工具名
  • 工具描述
  • 参数 schema 和当前参数值

九、动态更新(热重载)

MCP 服务器可以在运行时通知客户端工具列表变化。Gemini

CLI 实现了合并模式(Coalescing Pattern) 处理快速连续通知:

typescript 复制代码
// 收到 ToolListChangedNotification
client.setNotificationHandler(ToolListChangedNotificationSchema, async () => {
  await this.refreshTools()
})

async refreshTools() {
  if (isRefreshingTools) {
    pendingToolRefresh = true  // 标记"有更新在等待",不重复发起请求
    return
  }
  isRefreshingTools = true

  do {
    pendingToolRefresh = false
    const newTools = await discoverTools()

    // 验证重试:若工具没变化,等 500ms 再试一次
    // (有些服务器会在工具准备好之前发出通知)
    if (toolNamesMatch && !pendingToolRefresh) {
      await sleep(500)
      newTools = await discoverTools()
    }

    // 更新 ToolRegistry
    toolRegistry.removeMcpToolsByServer(serverName)
    newTools.forEach(tool => toolRegistry.registerTool(tool))
  } while (pendingToolRefresh)  // 如果期间又来了新通知,再刷新一次

  isRefreshingTools = false
}

同样的合并模式也适用于 ResourceListChangedPromptListChanged


十、认证机制

10.1 OAuth 认证

自动 OAuth 流程(服务器返回 401 时触发):

复制代码
连接失败 → 返回 401
    │
    ▼
extractWWWAuthenticateHeader()  从错误信息提取认证头
    │
    ├── 若找到 → handleAutomaticOAuth()
    │      ├── 从 WWW-Authenticate 解析 OAuth 端点
    │      ├── 启动 OAuth PKCE 流程(打开浏览器)
    │      ├── 获取 access_token
    │      └── 用 Bearer Token 重试连接
    │
    └── 若未找到 → 提示用户执行 /mcp auth <服务器名>

手动认证命令/mcp auth <server-name>

Token 存储:

  • macOS:系统 Keychain
  • 其他:~/.gemini/oauth-tokens.json(混合存储策略)

10.2 Google 凭证认证

json 复制代码
{
  "mcpServers": {
    "my-gcp-server": {
      "url": "https://...",
      "authProviderType": "googleCredentials"
    }
  }
}

10.3 服务账号模拟

json 复制代码
{
  "authProviderType": "serviceAccountImpersonation",
  "serviceAccountEmail": "sa@project.iam.gserviceaccount.com"
}

十一、安全模型

stdio 传输的信任要求

typescript 复制代码
// createTransport()
if (mcpServerConfig.command) {
  if (!cliConfig.isTrustedFolder()) {
    throw new Error('MCP stdio 服务器需要信任当前文件夹');
  }
}

stdio 服务器在本地执行任意命令,因此需要用户显式信任当前工作目录。

环境变量净化

stdio 服务器启动时,环境变量经过 sanitizeEnvironment()

净化(防止敏感变量泄露),只保留必要的变量,并允许通过 env 字段显式注入。

工具过滤

策略引擎(Policy Engine)的规则可以匹配 MCP 工具:

toml 复制代码
# .gemini/policy.toml
[[tool.rules]]
tool_name = "mcp_github_delete_repo"
decision = "DENY"

[[tool.rules]]
tool_name = "mcp_github_*"
decision = "ASK_USER"

十二、进度通知

长时间运行的 MCP 工具可以发送进度更新:

typescript 复制代码
// McpCallableTool.callTool()
// 每次调用都生成一个 progressToken(UUID)
const progressToken = randomUUID();
const result = await client.callTool({
  name: toolName,
  arguments: args,
  _meta: { progressToken },
});

// McpClient 监听进度通知
client.setNotificationHandler(ProgressNotificationSchema, (notification) => {
  const { progressToken, progress, total, message } = notification.params;
  const callId = progressTokenToCallId.get(progressToken);
  // 通过 coreEvents.emitMcpProgress() 广播进度到 UI
});

UI 层的工具调用显示组件订阅这些进度事件,实时显示进度条。


十三、Prompts 系统

MCP 服务器还可以提供提示词模板,用户可以通过斜杠命令调用:

复制代码
用户输入:/mcp-prompt code_review  file=main.py
    │
    ▼
PromptRegistry.getPrompt("code_review")
    │
    ▼
mcpClient.getPrompt({ name: "code_review", arguments: { file: "main.py" } })
    │
    ▼
返回 GetPromptResult { messages: [...] }(结构化的对话内容)
    │
    ▼
注入为初始消息,开始 AI 对话

十四、MCP 客户端管理器

McpClientManager 管理多个 MCP 客户端,处理子代理(subagent)场景:

复制代码
McpClientManager
    ├── connect(registries)       → 为指定的工具注册表连接所有服务器
    ├── disconnect(registries)    → 断开并从注册表移除工具
    ├── reconnect(serverName)     → 重连单个服务器
    └── getClients()              → 获取所有 McpClient 实例

子代理(如 A2A 协议调用的子代理)会创建独立的 MCP 客户端集合,不与主代理共享连接。


十五、关键设计模式总结

模式 应用
适配器模式 DiscoveredMCPTool 将 MCP 协议适配为内置工具接口,无需修改调度器
合并模式 防止快速连续的更新通知引发竞态条件和服务器过载
验证重试 收到更新通知后,若工具列表未变则等 500ms 重试(服务器可能通知早于数据就绪)
自动协议协商 未配置 type 时先尝试 HTTP,失败后降级 SSE
渐进式信任 从每次确认 → 信任此工具 → 信任此服务器,细粒度权限管理
进程隔离 stdio 服务器在独立进程运行,崩溃不影响主进程

十六、与其他模块的关系

复制代码
Config(settings.json 中的 mcpServers 配置)
    │
    ▼
discoverMcpTools()(CLI 启动时调用)
    │
    ├── connectToMcpServer()   →  createTransport()(Stdio/SSE/HTTP)
    │                          →  OAuth 认证(如需)
    │
    ├── discoverTools()        →  DiscoveredMCPTool → ToolRegistry(注册)
    ├── discoverPrompts()      →  DiscoveredMCPPrompt → PromptRegistry
    └── discoverResources()    →  Resource → ResourceRegistry

ToolRegistry(内置工具 + MCP 工具统一存储)
    │ getFunctionDeclarations()
    ▼
Gemini API(模型看到 mcp_xxx_yyy 工具,并不知道它是 MCP 工具)

Scheduler(调度)→ executeToolWithHooks()
    │ DiscoveredMCPToolInvocation.execute()
    ▼
McpCallableTool.callTool()
    │ @modelcontextprotocol/sdk Client.callTool()
    ▼
MCP 服务器(外部进程 / 远程服务)

十七、完整数据流

复制代码
用户提示:「帮我在 GitHub 上创建一个 issue」

1. Gemini 模型看到工具 mcp_github_create_issue(已注册)
2. 模型返回 ToolCall:{ name: "mcp_github_create_issue", args: {...} }
3. Scheduler 接收,进入 Validating 状态
4. PolicyEngine 检查 → 决定 ASK_USER
5. 用户确认弹窗(显示服务器名、工具名、参数)
6. 用户点击「允许」→ Scheduled 状态
7. ToolExecutor.execute() 调用
8. BeforeTool Hook(如有)触发
9. DiscoveredMCPToolInvocation.execute()
    → McpCallableTool.callTool([{ name: "create_issue", args }])
    → @modelcontextprotocol/sdk 通过 stdio 发送给 GitHub MCP 服务器
    → 服务器创建 issue,返回 { content: [{ type: "text", text: "Issue #123 created" }] }
10. transformMcpContentToParts() 转换内容块为 GenAI Part
11. AfterTool Hook(如有)触发
12. ToolResult 返回给 Scheduler → functionResponse 发给 Gemini
13. Gemini 生成最终回复:「已成功创建 Issue #123」

十八、如何添加一个 MCP 服务器(实操示例)

步骤 1:在 settings.json 中配置

json 复制代码
{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
      "trust": false
    }
  }
}

步骤 2:重启 Gemini CLI (或执行 /mcp reload

步骤 3:使用

复制代码
> 列出 /tmp 目录中的文件

Gemini 会自动选择 mcp_filesystem_list_directory 工具执行。

查看 MCP 状态/mcp 斜杠命令显示所有服务器的连接状态。


相关推荐
线束线缆组件品替网2 小时前
Amphenol RJE1Y36610644401 CAT6A网线组件选型与替代指南
网络·人工智能·数码相机·电脑·音视频·硬件工程·游戏机
大模型真好玩2 小时前
LangChain DeepAgents 速通指南(五)—— 快速了解DeepAgents框架及其核心特性
人工智能·langchain·agent
我材不敲代码2 小时前
基于 OpenCV-SIFT 特征匹配的指纹识别系统实战
人工智能·opencv·计算机视觉
mit6.8242 小时前
Kimi的新“注意力残差“技术
人工智能
做科研的周师兄2 小时前
巴音河中下游灌溉草地空间分布数据集(2020年)
大数据·人工智能·算法·机器学习·数据挖掘·聚类
梓仁沐白2 小时前
ReAct 和 Plan-and-Execute 讲解与对比
人工智能
jinanwuhuaguo2 小时前
OpenClaw v2026.3.23 深度技术分析报告:平台地基的加固与成熟度宣言
运维·数据库·人工智能·openclaw
yhdata2 小时前
电脑提花机市场规模定格14.33亿元,数据锚定行业进阶新坐标
大数据·人工智能·电脑
新缸中之脑2 小时前
Agency Agents 简明教程
人工智能