feat: 添加 OpenAI 兼容 API 支持和独立 Vision 服务
- 添加 OpenAI AI SDK provider 支持 (@ai-sdk/openai) - 支持 OpenAI 兼容服务的 baseUrl 配置(如阿里云百炼) - 添加独立的 Vision 配置(visionProvider/visionApiKey/visionBaseUrl/visionModel) - 实现图片引用语法 @path/to/image.png,支持带空格的路径 - 当主模型不支持 vision 时,自动调用配置的 Vision 服务分析图片 - 添加图片处理工具函数和单元测试
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
isImagePath,
|
||||
extractImageReferences,
|
||||
formatFileSize,
|
||||
IMAGE_EXTENSIONS,
|
||||
} from '../../../src/utils/image.js';
|
||||
|
||||
describe('Image Utils - 图片处理工具', () => {
|
||||
describe('IMAGE_EXTENSIONS', () => {
|
||||
it('包含常见图片扩展名', () => {
|
||||
expect(IMAGE_EXTENSIONS).toContain('.png');
|
||||
expect(IMAGE_EXTENSIONS).toContain('.jpg');
|
||||
expect(IMAGE_EXTENSIONS).toContain('.jpeg');
|
||||
expect(IMAGE_EXTENSIONS).toContain('.gif');
|
||||
expect(IMAGE_EXTENSIONS).toContain('.webp');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isImagePath - 判断是否为图片路径', () => {
|
||||
it('识别 PNG 文件', () => {
|
||||
expect(isImagePath('screenshot.png')).toBe(true);
|
||||
expect(isImagePath('path/to/image.PNG')).toBe(true);
|
||||
});
|
||||
|
||||
it('识别 JPG/JPEG 文件', () => {
|
||||
expect(isImagePath('photo.jpg')).toBe(true);
|
||||
expect(isImagePath('photo.jpeg')).toBe(true);
|
||||
expect(isImagePath('photo.JPG')).toBe(true);
|
||||
});
|
||||
|
||||
it('识别 GIF 文件', () => {
|
||||
expect(isImagePath('animation.gif')).toBe(true);
|
||||
});
|
||||
|
||||
it('识别 WebP 文件', () => {
|
||||
expect(isImagePath('modern.webp')).toBe(true);
|
||||
});
|
||||
|
||||
it('不识别非图片文件', () => {
|
||||
expect(isImagePath('document.txt')).toBe(false);
|
||||
expect(isImagePath('script.ts')).toBe(false);
|
||||
expect(isImagePath('data.json')).toBe(false);
|
||||
expect(isImagePath('readme.md')).toBe(false);
|
||||
});
|
||||
|
||||
it('处理没有扩展名的文件', () => {
|
||||
expect(isImagePath('noextension')).toBe(false);
|
||||
expect(isImagePath('Makefile')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractImageReferences - 提取图片引用', () => {
|
||||
it('提取单个 @ 引用', () => {
|
||||
const result = extractImageReferences('请分析这张图片 @screenshot.png');
|
||||
expect(result.imagePaths).toEqual(['screenshot.png']);
|
||||
expect(result.textContent).toBe('请分析这张图片');
|
||||
});
|
||||
|
||||
it('提取多个 @ 引用', () => {
|
||||
const result = extractImageReferences('对比 @before.png 和 @after.jpg');
|
||||
expect(result.imagePaths).toEqual(['before.png', 'after.jpg']);
|
||||
expect(result.textContent).toBe('对比 和');
|
||||
});
|
||||
|
||||
it('提取带路径的图片引用', () => {
|
||||
const result = extractImageReferences('分析 @./images/test.png');
|
||||
expect(result.imagePaths).toEqual(['./images/test.png']);
|
||||
});
|
||||
|
||||
it('提取绝对路径图片引用', () => {
|
||||
const result = extractImageReferences('查看 @/tmp/screenshot.png');
|
||||
expect(result.imagePaths).toEqual(['/tmp/screenshot.png']);
|
||||
});
|
||||
|
||||
it('提取带空格的绝对路径(自动匹配到扩展名)', () => {
|
||||
const result = extractImageReferences('这张图片内容是什么?@/Users/xd/Adobe Express - file.png');
|
||||
expect(result.imagePaths).toEqual(['/Users/xd/Adobe Express - file.png']);
|
||||
expect(result.textContent).toBe('这张图片内容是什么?');
|
||||
});
|
||||
|
||||
it('提取带引号的路径(支持空格)', () => {
|
||||
const result = extractImageReferences('分析 @"./my images/test photo.png"');
|
||||
expect(result.imagePaths).toEqual(['./my images/test photo.png']);
|
||||
expect(result.textContent).toBe('分析');
|
||||
});
|
||||
|
||||
it('提取带单引号的路径', () => {
|
||||
const result = extractImageReferences("查看 @'./path with spaces/image.jpg'");
|
||||
expect(result.imagePaths).toEqual(['./path with spaces/image.jpg']);
|
||||
expect(result.textContent).toBe('查看');
|
||||
});
|
||||
|
||||
it('忽略非图片的 @ 引用', () => {
|
||||
const result = extractImageReferences('请查看 @readme.md 文件');
|
||||
expect(result.imagePaths).toEqual([]);
|
||||
expect(result.textContent).toBe('请查看 @readme.md 文件');
|
||||
});
|
||||
|
||||
it('忽略邮箱地址', () => {
|
||||
const result = extractImageReferences('联系 user@example.com 了解详情');
|
||||
expect(result.imagePaths).toEqual([]);
|
||||
expect(result.textContent).toBe('联系 user@example.com 了解详情');
|
||||
});
|
||||
|
||||
it('混合图片和非图片引用', () => {
|
||||
const result = extractImageReferences(
|
||||
'查看 @screenshot.png 和 @config.json'
|
||||
);
|
||||
expect(result.imagePaths).toEqual(['screenshot.png']);
|
||||
expect(result.textContent).toBe('查看 和 @config.json');
|
||||
});
|
||||
|
||||
it('没有引用时返回原文本', () => {
|
||||
const result = extractImageReferences('这是一段普通文本');
|
||||
expect(result.imagePaths).toEqual([]);
|
||||
expect(result.textContent).toBe('这是一段普通文本');
|
||||
});
|
||||
|
||||
it('只有图片引用时文本内容为空', () => {
|
||||
const result = extractImageReferences('@image.png');
|
||||
expect(result.imagePaths).toEqual(['image.png']);
|
||||
expect(result.textContent).toBe('');
|
||||
});
|
||||
|
||||
it('处理多个空格', () => {
|
||||
const result = extractImageReferences(' @test.png 描述 ');
|
||||
expect(result.imagePaths).toEqual(['test.png']);
|
||||
expect(result.textContent.trim()).toBe('描述');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatFileSize - 格式化文件大小', () => {
|
||||
it('格式化字节', () => {
|
||||
expect(formatFileSize(0)).toBe('0B');
|
||||
expect(formatFileSize(100)).toBe('100B');
|
||||
expect(formatFileSize(1023)).toBe('1023B');
|
||||
});
|
||||
|
||||
it('格式化 KB', () => {
|
||||
expect(formatFileSize(1024)).toBe('1.0KB');
|
||||
expect(formatFileSize(1536)).toBe('1.5KB');
|
||||
expect(formatFileSize(10240)).toBe('10.0KB');
|
||||
});
|
||||
|
||||
it('格式化 MB', () => {
|
||||
expect(formatFileSize(1024 * 1024)).toBe('1.0MB');
|
||||
expect(formatFileSize(1024 * 1024 * 2.5)).toBe('2.5MB');
|
||||
});
|
||||
|
||||
it('大文件以 MB 为单位', () => {
|
||||
expect(formatFileSize(1024 * 1024 * 1024)).toBe('1024.0MB');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user