🖼️ 给博客聊天加上「看图说话」功能:30 分钟的多模态升级之旅

🖼️ 给博客聊天加上「看图说话」功能:30 分钟的多模态升级之旅

凌晨十二点半,我盯着刚上线的 Polly Chat 2.0,突然冒出一个念头:

"能不能让它看懂图片?"

三十分钟后,这个功能上线了。

起因:一个简单的问题

Polly Chat 2.0 刚完成零后端重构,用 Cloudflare Worker 代理直连 CopilotX(我的 Copilot API 代理),体验非常丝滑。但作为技术博客的门面,纯文本对话总觉得差点意思。

Claude 本身支持 Vision,CopilotX 理论上也应该支持——但我从没测试过。

技术验证:一个红色方块

与其猜测,不如直接测。我写了个 30 行的测试脚本:

# 创建一个 50x50 的红色方块
img = Image.new('RGB', (50, 50), color='red')
buffer = io.BytesIO()
img.save(buffer, format='PNG')
img_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')

# 发送给 CopilotX (Anthropic 格式)
payload = {
    "model": "claude-sonnet-4-20250514",
    "messages": [{
        "role": "user",
        "content": [
            {"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": img_base64}},
            {"type": "text", "text": "What color is this image? Answer in one word."}
        ]
    }]
}

运行结果:

Status: 200
AI Response: Red

✅ Vision support works!

太好了,CopilotX 原生支持 Vision,不需要改后端!

交互设计:按钮?不,Ctrl+V 更优雅

接下来是前端。传统做法是加一个上传按钮,但我想到了更自然的交互:

方案参考产品优点缺点
左侧按钮WhatsApp直观占空间
右侧按钮ChatGPT紧凑按钮区拥挤
Ctrl+V 粘贴Slack/Discord零 UI 改动,符合直觉需要提示

最终选择了粘贴方案。用户截图后直接 Ctrl+V,图片出现在输入框上方:

┌──────────────────────────────────────────┐
│  ┌─────┐                                 │
│  │ 🖼️  │ ❌                              │
│  └─────┘                                 │
│ ┌────────────────────────────────────┐   │
│ │ 这张图是什么?               [✈️] │   │
│ └────────────────────────────────────┘   │
└──────────────────────────────────────────┘

实现核心代码:

userInput.addEventListener('paste', (e) => {
    const items = e.clipboardData?.items;
    if (!items) return;
    
    for (let item of items) {
        if (item.type.startsWith('image/')) {
            e.preventDefault();
            const file = item.getAsFile();
            processImage(file);  // 压缩 + 预览 + 存储
            break;
        }
    }
});

踩坑:413 Payload Too Large

第一次测试发图,直接报错:

Oops, something went wrong: HTTP 413

原因:Cloudflare Worker 免费版请求体限制 100KB,而一张普通截图的 base64 编码轻松超过 1MB。

解决方案:前端压缩。Canvas 大法好:

async function compressImage(file, maxKB = 50, maxDim = 800) {
    return new Promise((resolve) => {
        const img = new Image();
        img.onload = () => {
            // 缩放到最大 800px
            let { width, height } = img;
            if (width > maxDim || height > maxDim) {
                const ratio = Math.min(maxDim/width, maxDim/height);
                width *= ratio;
                height *= ratio;
            }
            
            // Canvas 压缩 + JPEG 降质
            const canvas = document.createElement('canvas');
            canvas.width = width;
            canvas.height = height;
            const ctx = canvas.getContext('2d');
            ctx.drawImage(img, 0, 0, width, height);
            
            // 循环降低质量直到 < 50KB
            let quality = 0.8;
            let base64;
            do {
                base64 = canvas.toDataURL('image/jpeg', quality);
                quality -= 0.1;
            } while (base64.length > maxKB * 1024 * 1.37 && quality > 0.1);
            
            resolve(base64);
        };
        img.src = URL.createObjectURL(file);
    });
}

再次测试:成功!

UI 打磨:极简滚动条

功能完成后,我注意到聊天内容多了后,右侧的滚动条有点突兀——和博客的简约风格不搭。

解决方案:hover 时才显示的超细滚动条:

#chat-box {
    scrollbar-width: thin;
    scrollbar-color: transparent transparent;
}

#chat-box:hover {
    scrollbar-color: rgba(0, 0, 0, 0.15) transparent;
}

#chat-box::-webkit-scrollbar {
    width: 6px;
}

#chat-box::-webkit-scrollbar-thumb {
    background: transparent;
    transition: background 0.3s ease;
}

#chat-box:hover::-webkit-scrollbar-thumb {
    background: rgba(0, 0, 0, 0.15);
}

效果:平时几乎不可见,滑动时优雅显现。

总结:30 分钟的愉悦开发

阶段耗时产出
技术验证5 min确认 CopilotX Vision 可用
交互设计5 min选定 Ctrl+V 方案
前端实现10 minpaste 监听 + 预览 UI
踩坑修复8 min图片压缩
UI 打磨2 min极简滚动条

这次开发体验极度愉悦,原因有三:

  1. 验证优先:先确认后端能力,避免无用功
  2. 设计简约:Ctrl+V 比按钮更优雅,代码也更少
  3. 渐进增强:先跑通核心流程,再解决边缘问题

最终效果如封面图所示——截图粘贴,AI 秒懂。

现在,你可以在博客首页试试:截个图,Ctrl+V,问问 Polly 看到了什么。

Comments