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:
2025-12-12 10:42:20 +08:00
parent 59dbed926e
commit 5e32375f0e
301 changed files with 3281 additions and 43 deletions
+201
View File
@@ -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`;
}
}