项目地址:https://github.com/lovelyqun/gemini-cli-web.git
在AI应用开发领域,如何将强大的命令行工具转化为易用的Web应用是一个常见挑战。本文将深入分析 packages/web-simple
的实现,这是一个基于 Gemini CLI Core 包构建的Web扩展,展示了如何优雅地复用现有核心逻辑,快速构建功能完整的Web AI助手。
web-simple
采用了"薄Web层 + 重核心复用"的架构设计:
┌─────────────────────────────────────────┐
│ 前端层 │
│ ┌─────────────┐ ┌─────────────────┐ │
│ │ HTML/CSS │ │ WebSocket客户端 │ │
│ └─────────────┘ └─────────────────┘ │
└─────────────────────────────────────────┘
│
│ WebSocket/HTTP
▼
┌─────────────────────────────────────────┐
│ Express服务层 │
│ ┌─────────────┐ ┌─────────────────┐ │
│ │ REST API │ │ WebSocket服务 │ │
│ └─────────────┘ └─────────────────┘ │
└─────────────────────────────────────────┘
│
│ 直接调用
▼
┌─────────────────────────────────────────┐
│ @google/gemini-cli-core │
│ ┌─────────────┐ ┌─────────────────┐ │
│ │ Config │ │ GeminiClient │ │
│ │ ToolRegistry│ │ 工具执行引擎 │ │
│ └─────────────┘ └─────────────────┘ │
└─────────────────────────────────────────┘
这种设计的优势在于:
服务器启动时,最关键的步骤是初始化Gemini配置:
async function initializeGeminiConfig() {
try {
const cwdDir = process.env.CWD || process.cwd();
// 检查环境变量
if (!process.env.GEMINI_API_KEY) {
throw new Error('GEMINI_API_KEY 环境变量未设置');
}
// 创建会话ID
currentSessionId = generateSessionId();
// 创建文件发现服务
const fileService = new FileDiscoveryService(cwdDir);
// 核心配置参数
const configParams = {
sessionId: currentSessionId,
embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL,
targetDir: cwdDir,
debugMode: process.env.DEBUG === 'true',
// 启用核心工具集合
coreTools: ['LSTool','ReadFileTool','ReadManyFilesTool',
'WriteFileTool', 'EditTool', 'GrepTool','GlobTool',
'ShellTool','WebFetchTool','WebSearchTool','MemoryTool'],
approvalMode: ApprovalMode.YOLO, // Web环境自动执行
fileDiscoveryService: fileService,
model: process.env.GEMINI_MODEL || DEFAULT_GEMINI_MODEL,
maxSessionTurns: 100,
noBrowser: true
};
// 创建并初始化配置实例
geminiConfig = new Config(configParams);
await geminiConfig.initialize();
await geminiConfig.refreshAuth(AuthType.USE_GEMINI);
return true;
} catch (error) {
console.error('❌ 初始化Gemini配置失败:', error.message);
return false;
}
}
核心要点分析:
@google/gemini-cli-core
的 Config
类,Web应用获得了与CLI完全相同的配置能力coreTools
参数启用文件操作、Shell执行、Web搜索等工具approvalMode: ApprovalMode.YOLO
实现自动执行,避免Web环境中的交互提示Web版本的核心亮点是支持实时流式响应,让用户能看到AI的思考过程:
async function handleWebSocketChat(ws, connectionId, data) {
const { message } = data;
// 获取核心组件
const client = geminiConfig.getGeminiClient();
const toolRegistry = await geminiConfig.getToolRegistry();
try {
const messageContent = [{ text: message }];
const prompt_id = `ws-${connectionId.slice(0, 8)}-${Date.now()}`;
// 创建流式响应
const messageStream = client.sendMessageStream(
messageContent,
abortController.signal,
prompt_id
);
let fullResponse = '';
const pendingToolCalls = [];
// 实时处理事件流
for await (const event of messageStream) {
switch (event.type) {
case GeminiEventType.Content:
fullResponse += event.value;
// 实时推送内容片段
ws.send(JSON.stringify({
type: 'content',
data: { content: event.value, isComplete: false }
}));
break;
case GeminiEventType.Thought:
// 推送AI思考过程
ws.send(JSON.stringify({
type: 'thought',
data: {
subject: event.value.subject,
description: event.value.description
}
}));
break;
case GeminiEventType.ToolCallRequest:
// 收集工具调用请求
pendingToolCalls.push(event.value);
ws.send(JSON.stringify({
type: 'tool_call',
data: { toolInfo: event.value, status: 'pending' }
}));
break;
}
}
// 处理工具调用
if (pendingToolCalls.length > 0) {
await executeToolCalls(pendingToolCalls, client, toolRegistry, ws, prompt_id);
}
} catch (error) {
// 错误处理
ws.send(JSON.stringify({
type: 'chat_error',
error: formatApiError(error.message)
}));
}
}
技术创新点:
Content
、Thought
、ToolCallRequest
等多种事件类型工具调用是Gemini CLI最强大的功能之一,Web版本完全复用了这一能力:
// 执行工具调用的核心逻辑
for (const toolCallRequest of pendingToolCalls) {
try {
// 获取工具实例
const tool = toolRegistry.getTool(toolCallRequest.name);
if (!tool) {
throw new Error(`工具 "${toolCallRequest.name}" 未找到`);
}
// 执行工具
const toolResult = await tool.execute(
toolCallRequest.args,
abortController.signal
);
// 推送执行结果
ws.send(JSON.stringify({
type: 'tool_call',
data: {
toolInfo: toolCallRequest,
toolResult: toolResult,
status: 'completed'
}
}));
// 构建工具响应给AI
const toolResponsePart = {
functionResponse: {
name: toolCallRequest.name,
response: {
output: typeof toolResult.llmContent === 'string'
? toolResult.llmContent
: JSON.stringify(toolResult.llmContent)
}
}
};
toolResponseParts.push(toolResponsePart);
} catch (toolError) {
// 工具执行错误处理
console.error(`工具调用失败: ${toolCallRequest.name}`, toolError);
}
}
// 将工具结果发送给AI继续对话
if (toolResponseParts.length > 0) {
const continueStream = client.sendMessageStream(
toolResponseParts,
abortController.signal,
prompt_id
);
// 处理AI的后续响应
for await (const continueEvent of continueStream) {
if (continueEvent.type === GeminiEventType.Content) {
fullResponse += continueEvent.value;
ws.send(JSON.stringify({
type: 'content',
data: { content: continueEvent.value, isComplete: false }
}));
}
}
}
关键特性:
前端采用现代WebSocket架构,支持实时双向通信:
class GeminiWebSocketClient {
constructor() {
this.ws = null;
this.isConnected = false;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
}
// 建立连接
connect() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}`;
this.ws = new WebSocket(wsUrl);
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
this.processMessage(data);
};
}
// 处理服务器消息
processMessage(data) {
switch (data.type) {
case 'content':
this.handleContent(data.data);
break;
case 'thought':
this.showThinking(data.data.subject, data.data.description);
break;
case 'tool_call':
this.handleToolCall(data.data);
break;
}
}
// 实时内容更新
handleContent(data) {
let aiMessage = this.getCurrentAIMessage();
if (!aiMessage) {
aiMessage = this.addMessage('ai', '');
aiMessage.dataset.rawContent = '';
}
// 累积内容并格式化显示
aiMessage.dataset.rawContent += data.content;
const contentElement = aiMessage.querySelector('.message-text');
const formattedContent = this.formatMessageContent(aiMessage.dataset.rawContent);
contentElement.innerHTML = formattedContent;
// 添加打字机效果
contentElement.classList.add('typing-content');
this.scrollToBottom();
}
}
前端支持丰富的Markdown渲染和代码高亮:
formatMessageContent(content) {
// 1. 处理工具调用标记
content = this.formatToolCalls(content);
// 2. 处理代码块
content = this.formatCodeBlocks(content);
// 3. 处理行内代码
content = this.formatInlineCode(content);
// 4. 处理基本Markdown
content = this.formatBasicMarkdown(content);
return content;
}
formatCodeBlocks(content) {
return content.replace(/```(\w*)\n?([\s\S]*?)```/g, (match, language, code) => {
const lang = language || 'text';
const cleanCode = code.trim();
return `
${lang}
${lang}">${this.escapeHtml(cleanCode)}
`;
});
}
web-simple
巧妙地支持两种通信协议:
WebSocket协议(推荐):
REST API协议(兼容性):
// REST API实现
app.post('/api/chat', async (req, res) => {
const { message } = req.body;
// 复用相同的core逻辑
const client = geminiConfig.getGeminiClient();
const messageContent = [{ text: message }];
let fullResponse = '';
const messageStream = client.sendMessageStream(messageContent, signal, prompt_id);
// 收集完整响应
for await (const event of messageStream) {
if (event.type === GeminiEventType.Content) {
fullResponse += event.value;
}
}
// 返回完整结果
res.json({
response: fullResponse.trim(),
sessionId: currentSessionId
});
});
function formatApiError(error) {
// 结构化错误处理
if (error && typeof error === 'object' && 'message' in error) {
let text = `[API Error: ${error.message}]`;
if (error.status === 429) {
text += '\n可能的原因:API配额已用完或请求过于频繁,请稍后重试。';
}
return text;
}
// 解析JSON错误
const jsonStart = error.indexOf('{');
if (jsonStart !== -1) {
try {
const parsedError = JSON.parse(error.substring(jsonStart));
if (parsedError.error && parsedError.error.message) {
return `[API Error: ${parsedError.error.message}]`;
}
} catch (e) {
// 忽略解析错误
}
}
return `[API Error: ${error}]`;
}
// 心跳检测机制
setInterval(() => {
wss.clients.forEach((ws) => {
const connection = [...connections.values()].find(conn => conn.ws === ws);
if (connection) {
if (!connection.isAlive) {
ws.terminate();
connections.delete(connection.connectionId);
return;
}
connection.isAlive = false;
ws.ping();
}
});
}, 30000);
# 必需环境变量
GEMINI_API_KEY=your_gemini_api_key
# 可选配置
GEMINI_MODEL=gemini-pro
DEBUG=true
CWD=/path/to/working/directory
PORT=3000
@google/gemini-cli-core
,Web版本获得了与CLI相同的强大功能