Skip to content

Commit 3f9a91b

Browse files
feat(blog): add blog post on chat image upload and AI recognition
Add new blog post covering the complete solution for implementing image upload and AI recognition in chat systems, from design to implementation. Co-Authored-By: Hagicode <noreply@hagicode.com> Signed-off-by: newbe36524 <newbe36524@qq.com>
1 parent 4f3fd6e commit 3f9a91b

1 file changed

Lines changed: 284 additions & 0 deletions

File tree

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
---
2+
title: 在聊天中实现图片上传与 AI 识别:从设计到落地的完整方案
3+
date: 2026-05-07
4+
tags: [AI, 图片上传, 多模态, 系统设计, HagiCode]
5+
---
6+
7+
## 在聊天中实现图片上传与 AI 识别:从设计到落地的完整方案
8+
9+
> 在 AI 交互系统中,如何让用户上传图片并让 AI 直接识别?其实这个问题我也纠结了很久,好在 HagiCode 的实践中摸索出了一些门道。今天就聊聊这套图片上传与识别方案,从自定义协议设计到文件系统存储,再到前后端分离预览,也算是个完整的技术笔记了。
10+
11+
## 背景
12+
13+
在这个 AI 聊天盛行的时代,视觉信息其实是用户表达意图的重要载体。只是呢,传统聊天系统大多只支持纯文本输入,这就导致用户没法把视觉上下文直接传递给 AI 分析,多少有点遗憾。
14+
15+
HagiCode 在开发过程中也遇到了类似的困境:用户无法在聊天或主意见创建时上传图片,AI 无法访问用户本地的视觉信息,缺少图片从输入、存储、渲染到 AI 上下文传递的完整闭环。
16+
17+
其实这些问题也没什么大不了的,只是需要一点时间和耐心去解决罢了。我们设计并实现了一套完整的图片上传与识别流程,让 Claude 等 AI 能够直接识别和分析用户上传的截图。接下来我会慢慢细说这个方案的实现细节。
18+
19+
## 关于 HagiCode
20+
21+
本文分享的方案来自我们在 [HagiCode](https://hagicode.com) 项目中的实践经验。HagiCode 是一个开源的 AI 代码助手项目,采用基于 OpenSpec 的工作流设计,致力于提供更智能的代码编写体验。
22+
23+
## 分析
24+
25+
### 技术挑战
26+
27+
在开始实现之前,我们还是得先理清楚面临的主要挑战,毕竟磨刀不误砍柴工嘛。
28+
29+
**跨模块协作**:图片上传涉及前端 UI、上传服务、后端 API、文件存储、消息持久化和 AI 执行映射等多个模块。每个模块都有自己的职责和接口,需要设计一个协调一致的整体方案。
30+
31+
**存储策略选择**:图片应该存储在数据库还是文件系统?如果选择文件系统,目录结构如何设计?如何与现有的 OpenSpec 工作流集成?这些都需要仔细权衡。
32+
33+
**引用协议设计**:需要一种标准的图片引用方式,既能被前端渲染显示,又能被 AI 执行链路正确解析。直接使用文件路径?HTTP URL?还是设计专门的协议?
34+
35+
**AI 能力兼容**:不同 AI 执行器对多模态的支持程度不同。有些执行器原生支持图片输入,有些只能处理文本。如何设计统一的适配层,确保所有执行器都能正确处理图片信息?
36+
37+
### 设计决策
38+
39+
经过充分的讨论和权衡,我们做出了以下关键设计决策。
40+
41+
**决策 1:文件系统存储**
42+
43+
我们选择将图片存储在文件系统而非数据库中。目录结构设计如下:
44+
45+
```
46+
<系统根目录>/images/<sessionId>/
47+
├── <timestamp>-<uuid>.jpg
48+
└── <timestamp>-<uuid>.png
49+
```
50+
51+
理由其实也挺明确的:简化实现,避免数据库膨胀,文件可以直接被 AI 读取。而且图片文件本质上就不适合放在数据库里,文件系统才是更自然的选择。这就像把书放在书架上,而不是塞进笔记本里,一个道理罢了。
52+
53+
**决策 2:自定义协议 `hagiimag://`**
54+
55+
为了避免与 HTTP URL 冲突,同时让引用语义更清晰,我们设计了一个自定义图片引用协议:
56+
57+
```
58+
hagiimag://session-abc123/20260301-143022-a1b2c3d4
59+
```
60+
61+
这个协议的格式是 `hagiimag://<sessionId>/<imageId>`,语义清晰,便于解析和路由。看到这个格式,开发者立刻就能明白这是一个图片引用,而不是普通的 URL。这种设计上的小心思,有时候还是挺有用的。
62+
63+
**决策 3:前端预览与 AI 访问分离**
64+
65+
在实现过程中,我们发现前端和 AI 对图片的访问需求不同:前端需要通过 HTTP API 进行预览,而 AI 需要直接读取本地文件路径。因此我们设计了分离的访问方式:
66+
67+
- 前端使用 `/api/Images/{sessionId}/{imageId}/content` 进行预览
68+
- AI 使用服务端解析的本地文件路径
69+
70+
这样既保证了安全性(不暴露服务器路径),又兼顾了可用性(浏览器可直接访问)。毕竟安全性和可用性这两件事,总是需要平衡的。
71+
72+
**决策 4:立即上传策略**
73+
74+
另一个关键决策是上传时机。我们选择用户选择或粘贴图片时立即触发上传,发送消息时只引用已经上传成功的图片。
75+
76+
这样做的好处是错误处理前置,避免消息发送 API 变复杂,保持 JSON 契约的简洁性。用户在发送前就能知道图片是否上传成功,体验也更好。这种"未雨绸缪"的设计思路,或许在很多时候都适用。
77+
78+
## 解决
79+
80+
### 架构设计
81+
82+
基于上述决策,我们设计了如下整体架构:
83+
84+
```
85+
前端层
86+
├── ConversationInputArea ◄─────── useImageAttachmentManager
87+
│ │ │
88+
│ ├── 文件选择 ├── 附件状态管理
89+
│ ├── 剪贴板粘贴 ├── 上传/重试/删除
90+
│ └── 附件预览 └── 图片引用生成
91+
92+
服务层
93+
├── ImageUploadService
94+
│ ├── uploadImage() ◄─────── ImagesController
95+
│ ├── deleteImage() │
96+
│ ├── parseHagiImageUrl() ◄─────── 解析协议链接
97+
│ └── buildPreviewUrl() │
98+
99+
后端层
100+
├── ImagesController ◄─────── ImagesDomainService
101+
│ │ │
102+
│ ├── POST /upload ├── 文件验证
103+
│ ├── GET /{sessionId}/{imageId} ├── 图片保存
104+
│ ├── DELETE ├── 图片压缩
105+
│ └── GET /content └── 引用解析
106+
107+
AI 执行层
108+
├── ImageContentBlock ◄─────── StructuredMessageDomainService
109+
│ │ │
110+
│ ├── 多模态执行器 ├── 图片块解析
111+
│ └── 文本执行器降级 └── 路径提示生成
112+
```
113+
114+
这个架构清晰地展示了从前端到 AI 的完整数据流。每一层都有明确的职责,通过标准接口进行交互。其实好的架构就是这样,各司其职,互不干扰,沟通顺畅。
115+
116+
### 关键流程
117+
118+
**图片上传流程**
119+
120+
1. 用户通过文件选择或剪贴板粘贴选择图片
121+
2. 前端验证文件类型和大小(支持 JPEG/PNG/WEBP/GIF,单文件 10MB)
122+
3. 调用上传 API,图片保存到 `/images/{sessionId}/` 目录
123+
4. API 返回 `hagiimag://` 引用和预览 URL
124+
5. 前端在附件条中显示预览缩略图,用户可以在发送前预览
125+
126+
**AI 识别流程**
127+
128+
1. 用户发送包含图片引用的消息
129+
2. 后端解析 `hagiimag://` 协议链接,提取 sessionId 和 imageId
130+
3. 将图片引用映射为 `ImageContentBlock`
131+
4. 根据执行器能力选择处理方式:
132+
- 多模态执行器:传递结构化图片输入
133+
- 文本执行器:降级为图片路径提示
134+
135+
这样就完成了一个完整的闭环:用户上传图片 → AI 识别图片 → AI 返回分析结果。这种流程上的顺畅,往往能给用户带来更好的体验。
136+
137+
## 实践
138+
139+
### 前端实现
140+
141+
在前端,我们提供了一个专门的 Hook 来管理图片附件状态:
142+
143+
```typescript
144+
import { useImageAttachmentManager } from '@/hooks/useImageAttachmentManager';
145+
146+
function ChatInput() {
147+
const {
148+
attachments,
149+
uploadedImages,
150+
hasBlockingAttachments,
151+
isUploading,
152+
selectFiles,
153+
removeAttachment,
154+
clearAttachments,
155+
} = useImageAttachmentManager({
156+
ownerId: sessionId,
157+
mapUploadedImage: (response) => response,
158+
uploadOptions: { compress: false },
159+
});
160+
161+
const handleFileSelect = (files: File[]) => {
162+
selectFiles(files);
163+
};
164+
165+
const handlePaste = (e: ClipboardEvent) => {
166+
const files = Array.from(e.clipboardData?.files || [])
167+
.filter(f => f.type.startsWith('image/'));
168+
if (files.length > 0) {
169+
handleFileSelect(files);
170+
}
171+
};
172+
173+
return (
174+
<div>
175+
{/* 附件条 */}
176+
{attachments.map(att => (
177+
<AttachmentItem
178+
key={att.localId}
179+
file={att.file}
180+
status={att.status}
181+
onRemove={() => removeAttachment(att.localId)}
182+
/>
183+
))}
184+
185+
{/* 输入框 */}
186+
<textarea onPaste={handlePaste} />
187+
188+
{/* 上传按钮 */}
189+
<button onClick={() => fileInputRef.current?.click()}>
190+
上传图片
191+
</button>
192+
</div>
193+
);
194+
}
195+
```
196+
197+
这个 Hook 封装了所有附件管理的逻辑,包括上传状态跟踪、失败重试、附件删除等。使用起来非常简单,只需要调用几个方法就能完成整个流程。其实好的 API 设计就是这样,简单易用,又不失灵活性。
198+
199+
**解析自定义协议**
200+
201+
```typescript
202+
// 从自定义协议中提取 sessionId 和 imageId
203+
const parsed = parseHagiImageUrl("hagiimag://session-abc123/20260301-143022-uuid");
204+
// 返回: { sessionId: "session-abc123", imageId: "20260301-143022-uuid" }
205+
206+
// 构建预览 URL
207+
const previewUrl = buildPreviewUrl(parsed.sessionId, parsed.imageId);
208+
// 返回: "/api/Images/session-abc123/20260301-143022-uuid/content"
209+
```
210+
211+
通过这两个工具函数,前端可以轻松地在 `hagiimag://` 协议和 HTTP URL 之间进行转换。这种转换逻辑封装好了,使用起来就方便多了。
212+
213+
### 后端实现
214+
215+
后端使用 ASP.NET Core 实现,核心是 `ImagesController``ImagesDomainService`
216+
217+
```csharp
218+
[HttpPost("upload")]
219+
[RequestSizeLimit(50 * 1024 * 1024)]
220+
public async Task<ActionResult<ImageUploadResponseDto>> Upload(
221+
[FromForm] UploadImageFormRequest input)
222+
{
223+
// 1. 验证请求
224+
if (file == null || file.Length == 0)
225+
throw new UserFriendlyException("No file provided");
226+
227+
// 2. 验证文件类型和大小
228+
var (isValid, errorMessage) = _imagesDomainService.ValidateImage(
229+
file.FileName, file.ContentType, file.Length);
230+
if (!isValid)
231+
throw new UserFriendlyException(errorMessage);
232+
233+
// 3. 保存到文件系统
234+
await using var stream = file.OpenReadStream();
235+
var result = await _imagesDomainService.UploadImageAsync(
236+
stream,
237+
sessionId,
238+
file.FileName,
239+
file.ContentType,
240+
CurrentUserId,
241+
compress: input.Compress);
242+
243+
// 4. 返回结果
244+
return Ok(result);
245+
}
246+
```
247+
248+
这个实现遵循了典型的 Web API 开发模式:验证、处理、返回。需要注意的是,我们设置了 50MB 的请求大小限制,防止恶意的大文件上传。毕竟在网络世界里,小心一点总是没错的。
249+
250+
### 注意事项
251+
252+
在实现过程中,有一些细节需要特别注意:
253+
254+
**权限校验**:图片访问必须验证用户身份,确保只能访问自己会话的图片。这是一个基本的安全要求,不能省略。安全这东西,不怕一万就怕万一。
255+
256+
**路径安全**:严格验证 `sessionId``imageId`,防止路径遍历攻击。比如要拒绝包含 `../` 的路径,防止用户访问系统中的任意文件。这种边界条件处理好了,系统才能更稳健。
257+
258+
**文件清理**:会话删除时需同步清理关联图片,避免孤儿文件堆积。长期运行后,这些文件可能会占用大量磁盘空间。及时清理,也是一种好的习惯。
259+
260+
**压缩策略**:对于截图类文件名(如 `screenshot.png`),自动启用压缩以节省空间。这个策略可以根据实际需求调整。存储空间嘛,能省一点是一点。
261+
262+
**降级处理**:不支持多模态的执行器必须收到图片路径提示,不能静默丢弃图片信息。这一点很重要,否则用户会以为 AI 忽略了他的图片。用户体验这种事,细节决定成败。
263+
264+
**状态管理**:上传中的附件会阻止消息发送,失败附件允许重试或删除。这个设计保证了用户体验的连贯性。状态管理清晰了,用户就不会感到困惑。
265+
266+
## 总结
267+
268+
通过这套完整的图片上传与识别方案,HagiCode 实现了从用户输入到 AI 识别的完整闭环。整个方案的核心亮点包括:
269+
270+
- 自定义 `hagiimag://` 协议实现了图片引用的标准化
271+
- 文件系统存储简化了实现并提高了性能
272+
- 前端预览与 AI 访问分离兼顾了安全性和可用性
273+
- 立即上传策略优化了用户体验
274+
- 多模态与文本降级的兼容设计确保了灵活性
275+
276+
这个方案在 HagiCode 中运行稳定,用户反馈良好。如果你也在实现类似的功能,希望这些经验对你有帮助。
277+
278+
其实技术方案这东西,没有绝对的对错,只有适合不适合罢了。找到适合自己项目的路,才是最重要的。
279+
280+
## 参考资料
281+
282+
- HagiCode GitHub: [github.com/HagiCode-org/site](https://github.com/HagiCode-org/site)
283+
- HagiCode 官网: [hagicode.com](https://hagicode.com)
284+
- OpenSpec 工作流文档: [docs.hagicode.com](https://docs.hagicode.com)

0 commit comments

Comments
 (0)