文章目录
- [Gemini CLI 项目框架总览](#Gemini CLI 项目框架总览)
-
- 一、项目简介
- 二、仓库目录结构
- 三、各子包详细说明
-
- [3.1 `packages/core` --- 大脑(最重要)](#3.1
packages/core— 大脑(最重要)) - [3.2 `packages/cli` --- 终端界面](#3.2
packages/cli— 终端界面) - [3.3 `packages/sdk` --- 对外 SDK](#3.3
packages/sdk— 对外 SDK) - [3.4 `packages/a2a-server` --- Agent 通信服务](#3.4
packages/a2a-server— Agent 通信服务) - [3.5 `packages/vscode-ide-companion` --- VS Code 扩展](#3.5
packages/vscode-ide-companion— VS Code 扩展) - [3.6 `packages/devtools` --- 开发调试工具](#3.6
packages/devtools— 开发调试工具)
- [3.1 `packages/core` --- 大脑(最重要)](#3.1
- 四、核心技术栈
- 五、一次完整对话的数据流
- [Scheduler 调度器深度解析](#Scheduler 调度器深度解析)
-
- [一、Scheduler 是什么?](#一、Scheduler 是什么?)
- 二、涉及的文件
- 三、工具调用的生命周期(状态机)
- 四、核心类:`Scheduler`
-
- [4.1 构造函数与组成](#4.1 构造函数与组成)
- [4.2 主入口:`schedule()` 方法](#4.2 主入口:
schedule()方法) - [4.3 批处理:`_startBatch()` 方法](#4.3 批处理:
_startBatch()方法) - [4.4 主处理循环:`_processQueue()` / `_processNextItem()`](#4.4 主处理循环:
_processQueue()/_processNextItem()) - [4.5 并行化逻辑](#4.5 并行化逻辑)
- 五、状态管理:`SchedulerStateManager`
- 六、工具执行:`ToolExecutor`
- 七、策略与审批流程
-
- [7.1 三种策略决策](#7.1 三种策略决策)
- [7.2 审批模式(`ApprovalMode`)](#7.2 审批模式(
ApprovalMode)) - [7.3 用户审批流程](#7.3 用户审批流程)
- 八、取消机制
- 九、关键设计模式总结
- 十、与其他模块的关系
- [Tools 工具系统深度解析](#Tools 工具系统深度解析)
-
- 一、工具系统的整体架构
- 二、核心抽象:两层设计模式
-
- [2.1 继承体系](#2.1 继承体系)
- [2.2 工具种类(`Kind` 枚举)](#2.2 工具种类(
Kind枚举))
- 三、`ToolResult`:工具的返回值格式
- 四、内置工具详解
-
- [4.1 文件系统类工具](#4.1 文件系统类工具)
-
- [`read_file` --- 读取文件](#
read_file— 读取文件) - [`write_file` --- 写入/创建文件](#
write_file— 写入/创建文件) - [`edit` --- 精确字符串替换](#
edit— 精确字符串替换) - [`glob` --- 文件模式匹配](#
glob— 文件模式匹配) - [`grep` --- 文件内容搜索](#
grep— 文件内容搜索) - [`ls` --- 列出目录](#
ls— 列出目录) - [`read_many_files` --- 批量读取](#
read_many_files— 批量读取)
- [`read_file` --- 读取文件](#
- [4.2 执行类工具](#4.2 执行类工具)
-
- [`run_shell_command` --- Shell 命令执行](#
run_shell_command— Shell 命令执行)
- [`run_shell_command` --- Shell 命令执行](#
- [4.3 网络类工具](#4.3 网络类工具)
-
- [`web_search` --- 网络搜索](#
web_search— 网络搜索) - [`web_fetch` --- 网页抓取](#
web_fetch— 网页抓取)
- [`web_search` --- 网络搜索](#
- [4.4 AI 辅助工具](#4.4 AI 辅助工具)
-
- [`save_memory` --- 持久化记忆](#
save_memory— 持久化记忆) - [`ask_user` --- 向用户提问](#
ask_user— 向用户提问) - [`write_todos` --- 任务清单](#
write_todos— 任务清单) - [`enter_plan_mode` / `exit_plan_mode` --- 计划模式](#
enter_plan_mode/exit_plan_mode— 计划模式)
- [`save_memory` --- 持久化记忆](#
- 五、`ToolRegistry`:工具注册表
-
- 工具排序优先级
- 动态工具发现(`DiscoveredTool`)
- [Plan Mode 下的工具限制](#Plan Mode 下的工具限制)
- [六、`wait_for_previous` 参数:并发控制的桥梁](#六、
wait_for_previous参数:并发控制的桥梁) - 七、`ToolCallConfirmationDetails`:确认弹窗系统
- 八、工具开发模式:如何实现一个新工具
- 九、工具系统与其他模块的关系
- [CLI 终端 UI 深度解析](#CLI 终端 UI 深度解析)
-
- [一、为什么用 React 写终端 UI?](#一、为什么用 React 写终端 UI?)
- 二、整体目录结构
- 三、启动流程
-
- [3.1 程序入口:`gemini.tsx`](#3.1 程序入口:
gemini.tsx) - [3.2 交互模式:`interactiveCli.tsx`](#3.2 交互模式:
interactiveCli.tsx)
- [3.1 程序入口:`gemini.tsx`](#3.1 程序入口:
- 四、组件层级与职责分工
-
- [Static 区域 vs Dynamic 区域](#Static 区域 vs Dynamic 区域)
- 五、核心状态管理
-
- [5.1 `AppContainer`:唯一真相来源](#5.1
AppContainer:唯一真相来源) - [5.2 Context 体系:状态的分发](#5.2 Context 体系:状态的分发)
- [5.1 `AppContainer`:唯一真相来源](#5.1
- [六、最重要的 Hook:`useGeminiStream`](#六、最重要的 Hook:
useGeminiStream) -
- [6.1 它管理的状态](#6.1 它管理的状态)
- [6.2 主流程:`submitQuery()`](#6.2 主流程:
submitQuery()) - [6.3 流式输出的渲染方式](#6.3 流式输出的渲染方式)
- [七、输入系统:`useTextBuffer` + `Composer`](#七、输入系统:
useTextBuffer+Composer) -
- [7.1 文本缓冲区](#7.1 文本缓冲区)
- [7.2 输入的处理路径](#7.2 输入的处理路径)
- 八、键盘事件系统
-
- [8.1 `useKeypress` 与优先级](#8.1
useKeypress与优先级) - [8.2 主要快捷键映射](#8.2 主要快捷键映射)
- [8.1 `useKeypress` 与优先级](#8.1
- 九、历史管理:`useHistoryManager`
- [十、工具审批的 UI 流程](#十、工具审批的 UI 流程)
- [十一、特殊 UI 特性](#十一、特殊 UI 特性)
-
- [11.1 交替缓冲区(Alternate Buffer)](#11.1 交替缓冲区(Alternate Buffer))
- [11.2 背景 Shell](#11.2 背景 Shell)
- [11.3 主题系统](#11.3 主题系统)
- [11.4 Vim 模式](#11.4 Vim 模式)
- [十二、`AppContainer` 的核心数据流总结](#十二、
AppContainer的核心数据流总结) - 十三、关键设计模式总结
- [Hooks 钩子系统深度解析](#Hooks 钩子系统深度解析)
-
- [一、Hooks 解决什么问题?](#一、Hooks 解决什么问题?)
- [二、11 种 Hook 事件](#二、11 种 Hook 事件)
- [三、Hook 的两种类型](#三、Hook 的两种类型)
-
- [3.1 Command Hook(命令钩子)](#3.1 Command Hook(命令钩子))
- [3.2 Runtime Hook(运行时钩子)](#3.2 Runtime Hook(运行时钩子))
- 四、架构:五大组件
-
- [4.1 HookRegistry --- 注册表](#4.1 HookRegistry — 注册表)
- [4.2 HookPlanner --- 规划器](#4.2 HookPlanner — 规划器)
- [4.3 HookRunner --- 执行器](#4.3 HookRunner — 执行器)
- [4.4 HookAggregator --- 聚合器](#4.4 HookAggregator — 聚合器)
- [4.5 HookEventHandler --- 事件处理器](#4.5 HookEventHandler — 事件处理器)
- 五、与工具执行的集成点
-
- [BeforeTool 的参数修改](#BeforeTool 的参数修改)
- [AfterTool 的上下文注入](#AfterTool 的上下文注入)
- [AfterTool 触发链式调用(Tail Call)](#AfterTool 触发链式调用(Tail Call))
- [六、与 LLM 调用的集成点](#六、与 LLM 调用的集成点)
-
- [BeforeModel Hook](#BeforeModel Hook)
- [BeforeToolSelection Hook](#BeforeToolSelection Hook)
- [AfterModel Hook](#AfterModel Hook)
- [七、Hook Output 的决策类型](#七、Hook Output 的决策类型)
- [八、Session 级钩子](#八、Session 级钩子)
- 九、安全模型
-
- [9.1 受信任文件夹(Trusted Folder)](#9.1 受信任文件夹(Trusted Folder))
- [9.2 环境变量净化](#9.2 环境变量净化)
- [9.3 Hook 执行不影响主流程](#9.3 Hook 执行不影响主流程)
- 十、配置完整示例
- 十一、关键设计模式总结
- 十二、与其他模块的关系
- [MCP 协议集成深度解析](#MCP 协议集成深度解析)
-
- [一、MCP 解决什么问题?](#一、MCP 解决什么问题?)
- 二、三种传输方式
- 三、配置方式
- 四、核心类:`McpClient`
- 五、连接与发现流程
- 六、工具命名约定
- [七、MCP 工具如何融入现有工具体系](#七、MCP 工具如何融入现有工具体系)
- 八、用户确认机制
- 九、动态更新(热重载)
- 十、认证机制
-
- [10.1 OAuth 认证](#10.1 OAuth 认证)
- [10.2 Google 凭证认证](#10.2 Google 凭证认证)
- [10.3 服务账号模拟](#10.3 服务账号模拟)
- 十一、安全模型
- 十二、进度通知
- [十三、Prompts 系统](#十三、Prompts 系统)
- [十四、MCP 客户端管理器](#十四、MCP 客户端管理器)
- 十五、关键设计模式总结
- 十六、与其他模块的关系
- 十七、完整数据流
- [十八、如何添加一个 MCP 服务器(实操示例)](#十八、如何添加一个 MCP 服务器(实操示例))
Gemini CLI 项目框架总览
文档对 Gemini
CLI 源码进行系统性拆解与讲解,适合希望深入理解该项目实现原理的开发者。
一、项目简介
Gemini
CLI 是 Google 开源的 AI 命令行工具,让用户可以在终端中直接与 Gemini 模型交互,并让模型代理执行文件操作、Shell 命令、网络搜索等任务。
- GitHub :google-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
就是负责接收这些请求并驱动它们完成执行的"调度引擎"。
它解决的核心问题:
- 并发控制:哪些工具调用可以并行?哪些必须串行?
- 安全审批:哪些操作需要用户确认后才能执行?
- 策略检查:是否违反安全规则(策略引擎)?
- 状态追踪:每个工具调用处于哪个生命周期阶段?
- 取消支持:用户按 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[]>
处理步骤:
- 对每个请求,在
toolRegistry中查找对应工具 - 调用
tool.build(args)验证参数,构建invocation对象 - 将所有工具调用加入状态队列(初始状态:
Validating) - 启动主处理循环
_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:正在处理中(Validating→AwaitingApproval→Scheduled→
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>
执行流程:
- 调用
executeToolWithHooks(invocation, ...)--- 这里会触发钩子(hooks) - 等待工具返回
ToolResult - 检查是否需要截断超长输出(
truncateOutputIfNeeded) - 将结果转换为标准的
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.json 的 approvalMode 字段控制:
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,最敏感的工具,必须经过用户审批 -
审批弹窗显示:解析后的命令根、完整命令、工作目录
-
策略记忆 :用户选择"始终允许"时,记录的是命令前缀(如
git、npm),而不是完整命令 -
支持后台执行 :
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 网络类工具
web_search --- 网络搜索
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,只读- 底层使用
undici或node-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 列表
- 状态:
pending、in_progress、completed、cancelled、blocked
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"
}
发现流程:
- 执行
toolDiscoveryCommand,解析 JSON 输出(FunctionDeclaration[]格式) - 将每个工具包装为
DiscoveredTool,注册到 registry - 调用时执行
toolCallCommand <tool-name>,通过 stdin 传递参数,读取 stdout 作为结果
Plan Mode 下的工具限制
当 approvalMode === 'plan' 时,write_file 和 edit
的 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/
中)。
八、工具开发模式:如何实现一个新工具
- 定义参数类型
typescript
interface MyToolParams {
input: string;
options?: { verbose: boolean };
}
- 创建 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, // 给用户看
};
}
}
- 创建 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);
}
}
- 注册到 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 映射到终端的"重绘哪些行"
这样就能用 useState、useEffect、useMemo 等熟悉的工具来管理终端 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 模式(
useVimhook) - 历史导航(上下箭头,类似 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),让外部逻辑可以:
- 拦截并修改工具参数(BeforeTool)
- 阻止不安全的工具调用(BeforeTool 返回 deny)
- 增强工具结果(AfterTool 附加上下文)
- 监控模型的每次 LLM 调用(BeforeModel / AfterModel)
- 响应会话生命周期(SessionStart / SessionEnd)
- 发送通知(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,处理后将
HookOutputJSON 写入
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() 传递修改
串行执行时,输出到输入的传递规则:
BeforeAgent:additionalContext追加到 promptBeforeModel:llm_request合并到下一个请求BeforeTool:tool_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 时,框架会:
- 用新参数覆盖原始参数(
Object.assign) - 调用
tool.build(newParams)重建 invocation(重新验证参数,确保派生状态如
resolvedPath更新) - 在工具结果中附加修改通知
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 的目录时:
TrustedHooksManager检查该目录是否已被信任- 如果未信任,发出警告,列出所有待执行的 Hook 命令
- 用户确认后,将信任状态记录在
~/.gemini/trusted-hooks.json - 后续访问该目录时直接信任
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.ts、mcp-tool.ts、mcp-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_issues、create_branch) - Prompts(提示词) :预定义的对话模板(如
code_review、debug_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()):
- 强制添加
mcp_前缀 - 将非法字符(不在
[a-zA-Z0-9_\-.]中的)替换为_ - 若名称超过 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
}
同样的合并模式也适用于 ResourceListChanged 和 PromptListChanged。
十、认证机制
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 斜杠命令显示所有服务器的连接状态。