命令设计
CLI 客户端使用特殊的 image: 命令来发送图像:
1
| > image: /path/to/image.png
|
这个命令设计简洁直观,易于记忆和使用。
命令格式
示例:
1 2 3 4 5 6 7 8
| > image: /Users/zhenl/Pictures/screenshot.png
> image: ./images/chart.jpg
> image: ~/Downloads/photo.png
|
实现解析
命令检测
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
function isImageCommand(input: string): boolean { return input.trim().startsWith("image:"); }
function extractImagePath(input: string): string { const match = input.match(/image:\s*(.+)/); if (!match) { throw new Error("无效的图像命令格式"); } return match[1].trim(); }
|
路径展开
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
import * as path from "path"; import * as os from "os";
function expandPath(filePath: string): string { if (filePath.startsWith("~")) { return path.join(os.homedir(), filePath.slice(1)); }
return path.resolve(filePath); }
|
图像加载流程
完整实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
|
import * as fs from "fs/promises"; import { readImageAsBase64 } from "../types/multodal.js";
const MAX_IMAGE_SIZE = 10 * 1024 * 1024;
async function handleImageCommand( input: string ): Promise<MultimodalMessage> { const filePath = extractImagePath(input);
const expandedPath = expandPath(filePath);
try { await fs.access(expandedPath); } catch { throw new Error(`文件不存在: ${expandedPath}`); }
const stats = await fs.stat(expandedPath); if (stats.size > MAX_IMAGE_SIZE) { const sizeMB = (stats.size / 1024 / 1024).toFixed(2); const maxMB = (MAX_IMAGE_SIZE / 1024 / 1024).toFixed(0); throw new Error( `图像文件过大 (${sizeMB}MB),最大支持 ${maxMB}MB` ); }
const imageContent = await readImageAsBase64(expandedPath);
return { role: "user", content: imageContent, }; }
|
错误处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
async function safeHandleImageCommand( input: string, onImage: (message: MultimodalMessage) => void, onError: (error: string) => void ): Promise<void> { try { const message = await handleImageCommand(input); onImage(message); } catch (error) { const errorMessage = error instanceof Error ? error.message : "未知错误"; onError(errorMessage); } }
|
CLI 主循环集成
修改后的输入处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
|
import * as readline from "readline";
export class CLIClient { private rl: readline.Interface; private pendingImage: MultimodalMessage | null = null;
async start() { this.rl = readline.createInterface({ input: process.stdin, output: process.stdout, });
this.setPrompt();
this.rl.on("line", async (input) => { await this.handleInput(input.trim()); this.setPrompt(); }); }
private async handleInput(input: string): Promise<void> { if (!input) { return; }
if (input === "/exit" || input === "/quit") { this.shutdown(); return; }
if (isImageCommand(input)) { await safeHandleImageCommand( input, (message) => { this.pendingImage = message; console.log("✓ 图像已加载,可以添加文本描述或直接按回车发送"); }, (error) => { console.error(`✗ ${error}`); } ); return; }
let message: MultimodalMessage;
if (this.pendingImage) { message = { role: "user", content: [ this.pendingImage.content, { type: "text", text: input }, ], }; this.pendingImage = null; } else { message = { role: "user", content: { type: "text", text: input }, }; }
await this.sendMessage(message); }
private setPrompt(): void { if (this.pendingImage) { this.rl.setPrompt("🖼️ > "); } else { this.rl.setPrompt("> "); } this.rl.prompt(); } }
|
使用示例
场景 1:发送纯图像
1 2 3 4 5
| > image: ./screenshot.png ✓ 图像已加载,可以添加文本描述或直接按回车发送 🖼️ > [按回车] Agent: 我看到这是一个代码编辑器的截图...
|
场景 2:发送图像 + 文本
1 2 3 4
| > image: ~/Downloads/chart.png ✓ 图像已加载,可以添加文本描述或直接按回车发送 🖼️ > 请解释这个图表 Agent: 这个折线图显示了 2024 年的销售趋势...
|
场景 3:取消图像
1 2 3 4
| > image: ./photo.jpg ✓ 图像已加载,可以添加文本描述或直接按回车发送 🖼️ > /cancel > _图像已取消
|
交互增强
进度提示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
async function loadImageWithProgress( filePath: string ): Promise<ImageContent> { console.log(`正在加载图像: ${filePath}`);
const expandedPath = expandPath(filePath);
const stats = await fs.stat(expandedPath); const sizeMB = (stats.size / 1024 / 1024).toFixed(2); console.log(` 文件大小: ${sizeMB}MB`);
const imageContent = await readImageAsBase64(expandedPath);
console.log(` 格式: ${imageContent.mediaType}`); console.log("✓ 图像加载完成");
return imageContent; }
|
颜色输出
1 2 3 4 5
|
console.log("\x1b[32m%s\x1b[0m", "✓ 图像已加载"); console.log("\x1b[31m%s\x1b[0m", "✗ 加载失败"); console.log("\x1b[33m%s\x1b[0m", "⚠ 文件过大");
|
自动完成(可选)
使用 readline 实现简单的路径补全:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| function completer(line: string): [string[], string] { const completions: string[] = []; const hits: string[] = [];
if (line.startsWith("image:")) { const prefix = line.slice(6).trim(); const dir = path.dirname(prefix) || ".";
try { const files = await fs.readdir(dir); files.forEach((file) => { if (file.startsWith(path.basename(prefix))) { hits.push(`image: ${path.join(dir, file)}`); } }); } catch { } }
return [hits, line]; }
this.rl = readline.createInterface({ input: process.stdin, output: process.stdout, completer: completer, });
|
命令历史
图像命令会保存在命令历史中,方便重复使用:
1 2 3 4 5 6 7 8
| > history 1 image: ./screenshot.png 2 请描述这个 3 image: ~/Downloads/chart.png 4 分析数据
> !1 > image: ./screenshot.png
|
错误场景处理
文件不存在
1 2
| > image: ./nonexistent.png ✗ 文件不存在: /path/to/nonexistent.png
|
文件过大
1 2
| > image: ./large-photo.tif ✗ 图像文件过大 (15.23MB),最大支持 10MB
|
不支持的格式
1 2
| > image: ./document.pdf ✗ 不支持的文件格式: .pdf(仅支持 png, jpg, gif, webp)
|
读取失败
1 2
| > image: /protected/photo.png ✗ 无法读取文件: Permission denied
|
小结
本节介绍了 CLI 图像命令的完整实现:
image: 命令格式和解析
- 文件路径展开和验证
- 图像加载和转换流程
- CLI 主循环集成
- 交互增强功能
- 错误处理和场景
第十一章总结
本章详细介绍了多模态 AI Agent 的实现:
11.1 多模态 AI 概述
- 多模态 AI 的定义和优势
- 视觉能力在 Agent 中的重要性
- 主流视觉模型介绍
11.2 视觉模型集成
- LLMClient 视觉模型支持
- 自动模型选择逻辑
- 消息格式转换
11.3 图像处理与传输
- 多模态类型定义
- 图像格式支持
- Base64 编码传输
- 文件大小限制
11.4 多模态消息协议
- WebSocket 协议扩展
- 消息验证
- 服务器端处理
11.5 Web UI 图像功能
11.6 CLI 图像命令
通过本章的学习,你已经掌握了如何在 AI Agent 中集成视觉能力,使 Agent 能够理解和分析图像内容。
下一章: 第十二章:联网搜索
导航
上一篇: 11.5 Web UI 图像功能
下一篇: 12.1 搜索能力的重要性