Gemini CLI DevTools:终端 Agent 的本地 Network 与 Console 检查器实现解析
当在 Gemini CLI 中启用 general.devtools 设置后,CLI 会自动探测或启动一个监听在 25417 端口的本地 DevTools 服务,实时捕获 Agent 会话发出的所有网络请求(含流式响应分块与耗时)和控制台日志(log/warn/error/debug/info),并通过一个类似 Chrome DevTools 的网页界面进行检视。本文以 packages/devtools/GEMINI.md 为主线,结合 devtools 服务端实现、CLI 侧编排逻辑 与日志捕获层 的源码,完整讲解其功能全景、端口竞争协调机制、WebSocket/SSE 数据协议、日志脱敏策略以及构建开发流程。
功能全景:一个“Chrome DevTools 式”的终端检视器
DevTools 包提供四个核心能力(与 GEMINI.md 的 Features 一节一一对应):
- Network Inspector(网络检查器):实时记录请求/响应,支持流式响应分块(streaming chunks)的逐块可视化与整体耗时(duration)追踪。对于大模型流式 API 这类“响应体分多次到达”的场景,可以逐块观察 SSE 数据块何时到达。
- Console Inspector(控制台检查器):实时查看五种级别的控制台日志:
log/warn/error/debug/info。 - Session Management(会话管理):支持多个并发的 CLI 会话接入同一 DevTools 实例,并显示实时连接状态;UI 中可按会话过滤日志。
- Import/Export(导入/导出):可导入 JSONL 格式的日志文件(即 CLI 文件日志模式的产物),也可将当前会话日志导出为文件离线分析。
如何启用与触发
DevTools 由配置项 general.devtools 控制。从 settingsSchema.ts 可以看到其定义:布尔类型、默认 false、requiresRestart: false,描述为 “Enable DevTools inspector on launch.”。
启动链路分两条:
- 日志捕获初始化:gemini.tsx 在交互式模式下检测到
settings.merged.general.devtools为真时,动态导入utils/devtoolsService.js并调用setupInitialActivityLogger(config)——注意这一步只是开始缓冲日志,并不启动服务器。 - F12 触发检查器:AppContainer.tsx 中,
Command.SHOW_ERROR_DETAILS键(F12)绑定到toggleDevToolsPanel,它负责真正启动 DevTools 服务并尝试打开浏览器。文档中所述gemini.tsx / nonInteractiveCli.ts经由 dynamic import 接入devtoolsService的架构即来源于此。
toggleDevToolsPanel 的行为策略值得注意(见 devtoolsService.ts):
export async function toggleDevToolsPanel(
config: Config,
isOpen: boolean,
toggle: () => void,
setOpen: () => void,
): Promise<void> {
if (isOpen) {
toggle(); // 面板已打开 → 直接关闭
return;
}
try {
const { openBrowserSecurely, shouldLaunchBrowser } = await import(
'@google/gemini-cli-core'
);
const url = await startDevToolsServer(config);
if (shouldLaunchBrowser()) {
try {
await openBrowserSecurely(url);
return; // 浏览器打开成功 → 不再弹出内嵌面板
} catch (e) { /* ... */ }
}
setOpen(); // 无法打开浏览器时 → 打开 TUI 内嵌面板
} catch (e) {
setOpen();
}
}
即:优先在外部浏览器中打开 http://localhost:<port>;在无图形环境或打开失败时回退到 TUI 内嵌抽屉。
工作原理:端口探测、竞争协调与“晋升”
文档 “How It Works” 一节描述的机制,在源码中对应 startOrJoinDevTools 与 handlePromotion:
- 探测:CLI 先对
127.0.0.1:25417的/ws路径发起一次 WebSocket 握手,500ms 内成功打开即认为已有 DevTools 实例存活(probeDevTools,见 devtoolsService.ts)。 - 复用:探测到实例则直接作为 WebSocket 客户端接入,不重复起服务。
- 自启:探测失败则动态
import('@google/gemini-cli-devtools'),取DevTools.getInstance()单例并start()。 - 竞争裁决:若两个 CLI 进程同时抢端口,输的一方会检测到自身实际绑定端口不等于 25417(服务端在
EADDRINUSE时自动递增端口重试,最多 +10),随后探测胜者是否存活——存活则停止自己的服务、转为客户端连向胜者;若“胜者”无响应,则保留自己实际绑定的端口继续服务(见 devtoolsService.ts 与 index.ts)。
if (actualPort === defaultPort) {
// We won the port — we are the server
return { host: defaultHost, port: actualPort };
}
// Lost the race — someone else has the default port.
const winnerAlive = await probeDevTools(defaultHost, defaultPort);
if (winnerAlive) {
await devtools.stop();
return { host: defaultHost, port: defaultPort };
}
// Winner isn't responding — keep ours
此外还有一条“晋升”路径:会话启动时日志以 buffering 模式拦截(暂存内存),F12 或 general.devtools 触发时才挂接网络传输;若 WebSocket 断开且 2 次重连失败,handlePromotion 会尝试重新“start or join”一个服务并挂上新的网络传输,最多尝试 3 次(MAX_PROMOTION_ATTEMPTS = 3)。整个过程无需任何环境变量——文档中 “No environment variables needed for normal use” 即指此。
架构:五层数据流
原文档给出的架构分层如下,每一层都能在当前仓库中找到对应实现:
gemini.tsx / nonInteractiveCli.ts
│ (dynamic import)
▼
devtoolsService.ts ← orchestration + DevTools lifecycle
│ (imports)
▼
activityLogger.ts ← pure logging (capture, file, WebSocket transport)
│ (events)
▼
DevTools server (:25417) ← this package (HTTP + WebSocket + SSE)
│ (SSE /events)
▼
DevTools UI (React) ← client/ compiled by esbuild
- 入口层:
gemini.tsx(交互式)与nonInteractiveCli.ts(headless)通过 dynamic import 延迟加载 DevTools,避免未启用时的启动开销。 - 编排层:devtoolsService.ts 负责生命周期:探测、竞争裁决、并发调用去重(
startDevToolsServer对 in-flight 调用返回同一个 Promise)、晋升重试。 - 捕获层:activityLogger.ts 是“纯日志”层,负责拦截、文件落盘、WebSocket 传输,对外以事件(
network/console/network-logging-enabled)驱动下游。 - 服务端:packages/devtools/src/index.ts 中的
DevTools类,同时承担 HTTP 静态资源、WebSocket 接入、SSE 推送三种职责。 - UI 层:client/src/App.tsx 的 React 应用,由 esbuild 编译后内嵌进服务端(见下文构建部分)。
服务端实现:HTTP、WebSocket 与 SSE 三合一
DevTools 类是单例(getInstance()),默认端口 25417,只绑定 127.0.0.1,从源码结构看这是一个纯本地组件。它内置了若干工程细节:
同源防护。HTTP 处理器对带 Origin 头的请求只放行 http://127.0.0.1:<port>,源码注释说明原因:日志可能包含 API 密钥与请求头,不能允许任意网页跨域窃取(index.ts)。
SSE 快照 + 增量推送。/events 端点在连接建立时立即写出一条 event: snapshot,内容为 { networkLogs, consoleLogs, sessions } 全量快照,之后订阅三个内部事件做增量推送:
| 事件名 | 触发 | 内容 |
|---|---|---|
snapshot |
SSE 连接建立时 | 网络日志 + 控制台日志 + 会话列表全量 |
network |
update 事件 |
单条 NetworkLog(新建或按 id 合并后的更新) |
console |
console-update 事件 |
单条 InspectorConsoleLog |
session |
session-update 事件 |
当前全部会话 id 数组 |
UI 断线重连后无需轮询,靠 snapshot 事件即可恢复完整状态。
会话注册与心跳。WebSocket 挂在 /ws 路径,第一条消息必须是 register(携带 CLI 的 sessionId),服务端回 registered 确认并广播 session-update;随后每 10 秒向各会话发 ping,超过 30 秒未回 pong 的会话被关闭并从会话表移除(index.ts)。
内存上限。控制台日志在服务端保留上限为 5000 条,超出后丢弃最旧(index.ts)。从源码结构看,服务端内存中的网络日志列表同样做了最旧项驱逐以约束内存;UI 端依赖增量 network 事件自行累积展示,二者分工互补。
类型契约。共享的数据结构定义在 types.ts:
export interface NetworkLog {
id: string;
sessionId?: string;
timestamp: number;
method: string;
url: string;
headers: Record<string, string | string[] | undefined>;
body?: string;
pending?: boolean;
chunks?: Array<{ index: number; data: string; timestamp: number }>;
response?: {
status: number;
headers: Record<string, string | string[] | undefined>;
body?: string;
durationMs: number;
};
error?: string;
}
export interface ConsoleLogPayload {
type: 'log' | 'warn' | 'error' | 'debug' | 'info';
content: string;
}
chunks 字段对应文档所说的 streaming chunks:响应体流式到达时,每个块带 index 与 timestamp 单独上报(pending: true),流结束后合并出完整 response(含 durationMs)。服务端在收到完整 response.body 后会丢弃冗余的 chunks,源码注释指出这是为了避免快照序列化时超出 V8 字符串上限(index.ts)。
API 端点一览
继承原文档的端点表,并补充源码中可验证的行为细节:
| Endpoint | Method | Description |
|---|---|---|
/ws |
WebSocket | Log ingestion from CLI sessions(register / network / console,另有 ping / pong 心跳与 trigger-debugger 下行消息) |
/events |
SSE | Pushes snapshot on connect, then incremental network/console/session events |
/api/trigger-debugger |
POST | Triggers the Node.js debugger for a specific CLI session via WebSocket |
/api/trigger-debugger 的完整链路是:HTTP 层校验 { sessionId: string } 请求体(非法返回 400,会话不存在返回 404,见 index.ts)→ 向目标会话的 WebSocket 下发 { type: 'trigger-debugger' } → CLI 侧 activityLogger.ts 收到后调用 node:inspector 的 inspector.open(),在调试器附加时提示用户在 Chrome 中打开 chrome://inspect。也就是说,UI 端可以按会话“远程”挂上 Node.js 调试器。
此外还有两个静态资源路由:/(或 /index.html)返回内嵌的 INDEX_HTML,/assets/main.js 返回内嵌的 CLIENT_JS,其余路径 404。
CLI 侧捕获:fetch 与 http/https 双重拦截
ActivityLogger(activityLogger.ts)是单例,enable() 后通过 monkey-patch 两类出口:
global.fetch拦截:为每个请求生成随机 id,并注入x-activity-request-id请求头(ACTIVITY_ID_HEADER常量);先以pending: true上报请求信息,再response.clone()后用 reader 逐块读取响应体,每块触发一次带chunk的增量上报,最后合并出完整响应与durationMs。http.request/https.request拦截:用Object.defineProperty替换模块导出,包装write/end收集请求体,监听response的data/end事件收集响应流;对content-encoding: gzip / deflate会用zlib.gunzip / inflate解压后再记录,保证 UI 里看到的是可读明文。
两条拦截路径共享三个重要策略:
- 本机流量豁免:URL 包含
127.0.0.1或localhost的请求直接放行不拦截——DevTools 自身通信与本地服务不进入检查器。 - 敏感头脱敏:
sanitizeNetworkLog将请求头中的authorization、cookie、x-goog-api-key以及响应头中的set-cookie统一替换为[REDACTED](activityLogger.ts)。这与服务端只绑回环地址、同源 CORS 两道防线叠加,构成对凭据泄露的纵深防护。 - 缓冲与背压:网络事件按请求 id 分组缓冲(
bufferLimit = 10组),控制台日志缓冲 10 条;WebSocket 未连接或网络日志未开启时,消息进入传输缓冲(上限 100 条),注册成功后按时间戳排序一次性flushBuffer()补齐。
三种传输模式与环境变量
文档的 Environment Variables 一节列出了唯一的变量:
| Variable | Description |
|---|---|
GEMINI_CLI_ACTIVITY_LOG_TARGET |
File path for JSONL mode (optional, fallback) |
它对应 setupInitialActivityLogger 的模式选择逻辑(devtoolsService.ts):
export function setupInitialActivityLogger(config: Config) {
const target = process.env['GEMINI_CLI_ACTIVITY_LOG_TARGET'];
if (target) {
if (!config.storage) return;
initActivityLogger(config, { mode: 'file', filePath: target });
} else {
// Start in buffering mode — transport attached later on F12
initActivityLogger(config, { mode: 'buffer' });
}
}
- file 模式:JSONL 落盘,默认路径为项目临时日志目录下的
session-<sessionId>.jsonl,设置GEMINI_CLI_ACTIVITY_LOG_TARGET可覆盖路径。每行格式为{ type: 'console' | 'network', payload, sessionId, timestamp }。 - buffer 模式:默认路径,只拦截 + 内存缓冲,F12 触发时才挂网络传输并 flush 缓冲——这让未打开检查器的日常使用几乎零开销。
- 控制台日志来源:
bridgeCoreEvents将 core 包的CoreEvent.ConsoleLog事件桥接进ActivityLogger.logConsole,因此 UI 里看到的正是 CLI 内部各级别日志。
客户端 UI 与构建管线
client/src/App.tsx 是一个约 2000 行的单文件 React 应用(另有 hooks.ts 提供 SSE 数据获取)。从源码可见的功能包括:Console / Network 两个标签页、会话过滤(selectedSessionId)、JSONL 导入(按行解析 { type, payload } 结构——与 file 模式产物完全兼容)、以及持久化到 localStorage 的深浅色主题。数据获取依赖 /events 的 SSE 流:snapshot 建立基线,增量事件实时追加。
构建管线的设计很有意味(esbuild.client.js):
await esbuild.build({
entryPoints: ['client/src/main.tsx'],
bundle: true,
minify: true,
format: 'esm',
target: 'es2020',
jsx: 'automatic',
outfile: 'dist/client/main.js',
define: { 'process.env.NODE_ENV': '"production"' },
});
// Embed client assets as string constants so the devtools server can be
// bundled into the CLI without needing readFileSync + __dirname at runtime.
const indexHtml = readFileSync('client/index.html', 'utf-8');
const clientJs = readFileSync('dist/client/main.js', 'utf-8');
writeFileSync(
'src/_client-assets.ts',
`... export const INDEX_HTML = ${JSON.stringify(indexHtml)};
export const CLIENT_JS = ${JSON.stringify(clientJs)};`,
);
即:构建时把 index.html 与压缩后的 main.js 作为字符串常量生成到 src/_client-assets.ts,服务端直接 import { INDEX_HTML, CLIENT_JS } 引用(index.ts)。这样 DevTools 服务端可以整体 bundle 进 CLI 发行包,运行时不需要 __dirname 或文件读取,天然适配 SEA/打包场景。
package.json 中可确认的关键信息:包名 @google/gemini-cli-devtools、入口 dist/src/index.js、运行时依赖仅 ws@8.16.0(React 19 仅为客户端构建期依赖)、engines.node >= 20。
开发构建与项目结构
继承原文档 Development 一节的命令(对应 package.json 的 scripts 字段,build 实际为 build:client && tsc -p tsconfig.build.json):
# Build everything (client + server)
npm run build
# Rebuild client only after UI changes
npm run build:client
项目结构(与 GEMINI.md 的 Project Structure 一节一致):
packages/devtools/
├── src/
│ └── index.ts # DevTools server (HTTP, WebSocket, SSE)
├── client/
│ ├── index.html
│ └── src/
│ ├── main.tsx # React entry
│ ├── App.tsx # DevTools UI
│ └── hooks.ts # Data fetching hooks
├── esbuild.client.js # Client build script
└── dist/ # Build output
├── src/index.js # Compiled server
└── client/ # Bundled client assets
小结:设计取舍回顾
把文档与源码对照后可以提炼出 DevTools 包的几个核心设计决策:
- “先探测、后竞争、再裁决”:25417 默认端口 + 探测 + 端口递增重试(最多 10 次)+ 竞争败者转客户端,保证多 CLI 会话共享同一个检查器,且对“胜者假死”有兜底。
- 零环境变量、渐进开销:日常运行只有内存缓冲;F12 才挂传输;
GEMINI_CLI_ACTIVITY_LOG_TARGET仅作 JSONL 文件模式的可选旁路。 - 安全纵深:仅绑定回环地址、同源 CORS、敏感头
[REDACTED]、本机流量豁免,四条防线共同约束可能含凭据的日志。 - 资产内嵌:客户端以字符串常量形式编译进服务端模块,使整个 DevTools 可以无运行时文件依赖地随 CLI 分发。
关键源码入口:服务端 packages/devtools/src/index.ts、类型 packages/devtools/src/types.ts、CLI 编排 packages/cli/src/utils/devtoolsService.ts、捕获层 packages/cli/src/utils/activityLogger.ts、配置定义 packages/cli/src/config/settingsSchema.ts。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00