feat: 重构为 Monorepo 架构并实现 HTTP Server
架构变更: - 采用 pnpm workspaces 实现 Monorepo 结构 - 将现有代码迁移到 packages/core - 新增 packages/server HTTP 服务层 Server 功能: - REST API: 会话管理、工具管理、配置管理 - WebSocket: 实时双向通信支持 - SSE: 服务端事件推送 - Hono + Bun 作为运行时 API 端点: - GET/POST /api/sessions - 会话 CRUD - GET/POST /api/sessions/:id/messages - 消息管理 - GET /api/sessions/:id/events - SSE 事件流 - WS /api/ws/:sessionId - WebSocket 连接 - GET/POST /api/tools - 工具管理 - GET/PUT /api/config - 配置管理
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* 图片处理工具
|
||||
*
|
||||
* 提供图片文件读取、格式检测、base64 编码等功能
|
||||
*/
|
||||
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
|
||||
/** 支持的图片扩展名 */
|
||||
export const IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.webp'];
|
||||
|
||||
/** 图片 MIME 类型映射 */
|
||||
const MIME_TYPES: Record<string, string> = {
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.gif': 'image/gif',
|
||||
'.webp': 'image/webp',
|
||||
};
|
||||
|
||||
/** 图片信息 */
|
||||
export interface ImageInfo {
|
||||
/** 原始文件路径 */
|
||||
path: string;
|
||||
/** 文件名 */
|
||||
filename: string;
|
||||
/** 扩展名 */
|
||||
extension: string;
|
||||
/** MIME 类型 */
|
||||
mimeType: string;
|
||||
/** 文件大小(字节) */
|
||||
size: number;
|
||||
/** base64 编码的数据 */
|
||||
base64: string;
|
||||
/** 完整的 data URL */
|
||||
dataUrl: string;
|
||||
}
|
||||
|
||||
/** 图片加载结果 */
|
||||
export interface ImageLoadResult {
|
||||
success: boolean;
|
||||
image?: ImageInfo;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断文件路径是否为图片
|
||||
*/
|
||||
export function isImagePath(filePath: string): boolean {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
return IMAGE_EXTENSIONS.includes(ext);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从输入文本中提取图片引用
|
||||
* 支持多种格式:
|
||||
* 1. @path/to/image.png(不带空格的路径)
|
||||
* 2. @"path/to/image with spaces.png"(带空格的路径用引号包裹)
|
||||
* 3. @/path/to/image.png(绝对路径,自动匹配到图片扩展名结束)
|
||||
*
|
||||
* @param input 用户输入
|
||||
* @returns 图片路径列表和去除图片引用后的文本
|
||||
*/
|
||||
export function extractImageReferences(input: string): {
|
||||
imagePaths: string[];
|
||||
textContent: string;
|
||||
} {
|
||||
const imagePaths: string[] = [];
|
||||
let textContent = input;
|
||||
|
||||
// 模式1: 带引号的路径 @"path/to/image.png" 或 @'path/to/image.png'
|
||||
const quotedMatches = [...input.matchAll(/@["']([^"']+\.(?:png|jpg|jpeg|gif|webp))["']/gi)];
|
||||
for (const match of quotedMatches) {
|
||||
imagePaths.push(match[1]);
|
||||
textContent = textContent.replace(match[0], ' ');
|
||||
}
|
||||
|
||||
// 模式2: 绝对路径(以 / 或 ~ 开头,匹配到图片扩展名结束)
|
||||
// 支持路径中包含空格
|
||||
const absoluteMatches = [...textContent.matchAll(/@([/~][^\n]*?\.(?:png|jpg|jpeg|gif|webp))(?=\s|$)/gi)];
|
||||
for (const match of absoluteMatches) {
|
||||
if (!imagePaths.includes(match[1])) {
|
||||
imagePaths.push(match[1]);
|
||||
textContent = textContent.replace(match[0], ' ');
|
||||
}
|
||||
}
|
||||
|
||||
// 模式3: 相对路径(不以 / 开头,不包含空格)
|
||||
const relativeMatches = [...textContent.matchAll(/@((?:\.\/|\.\.\/)?[^\s@"'/][^\s@"']*\.(?:png|jpg|jpeg|gif|webp))/gi)];
|
||||
for (const match of relativeMatches) {
|
||||
if (!imagePaths.includes(match[1])) {
|
||||
imagePaths.push(match[1]);
|
||||
textContent = textContent.replace(match[0], ' ');
|
||||
}
|
||||
}
|
||||
|
||||
// 清理多余空格
|
||||
textContent = textContent.replace(/\s+/g, ' ').trim();
|
||||
|
||||
return { imagePaths, textContent };
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载图片文件
|
||||
* @param filePath 图片路径(相对或绝对)
|
||||
* @param workdir 工作目录(用于解析相对路径)
|
||||
*/
|
||||
export async function loadImage(
|
||||
filePath: string,
|
||||
workdir: string = process.cwd()
|
||||
): Promise<ImageLoadResult> {
|
||||
try {
|
||||
// 解析路径
|
||||
const absolutePath = path.isAbsolute(filePath)
|
||||
? filePath
|
||||
: path.resolve(workdir, filePath);
|
||||
|
||||
// 检查扩展名
|
||||
const ext = path.extname(absolutePath).toLowerCase();
|
||||
if (!IMAGE_EXTENSIONS.includes(ext)) {
|
||||
return {
|
||||
success: false,
|
||||
error: `不支持的图片格式: ${ext}。支持的格式: ${IMAGE_EXTENSIONS.join(', ')}`,
|
||||
};
|
||||
}
|
||||
|
||||
// 读取文件
|
||||
const buffer = await fs.readFile(absolutePath);
|
||||
const stats = await fs.stat(absolutePath);
|
||||
|
||||
// 转换为 base64
|
||||
const base64 = buffer.toString('base64');
|
||||
const mimeType = MIME_TYPES[ext] || 'application/octet-stream';
|
||||
const dataUrl = `data:${mimeType};base64,${base64}`;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
image: {
|
||||
path: absolutePath,
|
||||
filename: path.basename(absolutePath),
|
||||
extension: ext,
|
||||
mimeType,
|
||||
size: stats.size,
|
||||
base64,
|
||||
dataUrl,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
return {
|
||||
success: false,
|
||||
error: `图片文件不存在: ${filePath}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: `加载图片失败: ${error instanceof Error ? error.message : String(error)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量加载图片
|
||||
* @param filePaths 图片路径列表
|
||||
* @param workdir 工作目录
|
||||
*/
|
||||
export async function loadImages(
|
||||
filePaths: string[],
|
||||
workdir: string = process.cwd()
|
||||
): Promise<{
|
||||
images: ImageInfo[];
|
||||
errors: Array<{ path: string; error: string }>;
|
||||
}> {
|
||||
const images: ImageInfo[] = [];
|
||||
const errors: Array<{ path: string; error: string }> = [];
|
||||
|
||||
for (const filePath of filePaths) {
|
||||
const result = await loadImage(filePath, workdir);
|
||||
if (result.success && result.image) {
|
||||
images.push(result.image);
|
||||
} else {
|
||||
errors.push({ path: filePath, error: result.error || '未知错误' });
|
||||
}
|
||||
}
|
||||
|
||||
return { images, errors };
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化文件大小
|
||||
*/
|
||||
export function formatFileSize(bytes: number): string {
|
||||
if (bytes < 1024) {
|
||||
return `${bytes}B`;
|
||||
} else if (bytes < 1024 * 1024) {
|
||||
return `${(bytes / 1024).toFixed(1)}KB`;
|
||||
} else {
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user