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
+215
View File
@@ -0,0 +1,215 @@
import * as fs from 'fs/promises';
import * as path from 'path';
import * as os from 'os';
import type { SessionData, SessionSummary } from './types.js';
/**
* 获取默认存储目录
* 遵循 XDG 规范:~/.local/share/ai-assist/
*/
function getDefaultStorageDir(): string {
const xdgDataHome = process.env.XDG_DATA_HOME;
if (xdgDataHome) {
return path.join(xdgDataHome, 'ai-assist');
}
return path.join(os.homedir(), '.local', 'share', 'ai-assist');
}
/**
* 会话存储类
* 负责会话数据的读写操作
*/
export class SessionStorage {
private storageDir: string;
private sessionsDir: string;
private currentSessionFile: string;
constructor(storageDir?: string) {
this.storageDir = storageDir || getDefaultStorageDir();
this.sessionsDir = path.join(this.storageDir, 'sessions');
this.currentSessionFile = path.join(this.storageDir, 'current-session.json');
}
/**
* 确保存储目录存在
*/
async ensureDir(): Promise<void> {
await fs.mkdir(this.sessionsDir, { recursive: true });
}
/**
* 生成会话 ID
*/
generateSessionId(): string {
const now = new Date();
const timestamp = now.toISOString().slice(0, 10); // YYYY-MM-DD
const random = Math.random().toString(36).substring(2, 8);
return `${timestamp}_${random}`;
}
/**
* 保存当前会话
*/
async saveCurrentSession(session: SessionData): Promise<void> {
await this.ensureDir();
session.updatedAt = new Date().toISOString();
await fs.writeFile(
this.currentSessionFile,
JSON.stringify(session, null, 2),
'utf-8'
);
}
/**
* 加载当前会话
*/
async loadCurrentSession(): Promise<SessionData | null> {
try {
const content = await fs.readFile(this.currentSessionFile, 'utf-8');
return JSON.parse(content) as SessionData;
} catch {
return null;
}
}
/**
* 归档当前会话到历史
*/
async archiveCurrentSession(): Promise<void> {
const current = await this.loadCurrentSession();
if (!current || current.messages.length === 0) {
return;
}
await this.ensureDir();
const archivePath = path.join(this.sessionsDir, `${current.id}.json`);
await fs.writeFile(archivePath, JSON.stringify(current, null, 2), 'utf-8');
}
/**
* 删除当前会话文件
*/
async clearCurrentSession(): Promise<void> {
try {
await fs.unlink(this.currentSessionFile);
} catch {
// 文件不存在,忽略
}
}
/**
* 列出历史会话
*/
async listSessions(): Promise<SessionSummary[]> {
await this.ensureDir();
const files = await fs.readdir(this.sessionsDir);
const summaries: SessionSummary[] = [];
for (const file of files) {
if (!file.endsWith('.json')) continue;
try {
const filePath = path.join(this.sessionsDir, file);
const content = await fs.readFile(filePath, 'utf-8');
const session = JSON.parse(content) as SessionData;
summaries.push({
id: session.id,
title: session.title || this.generateTitle(session),
workdir: session.workdir,
messageCount: session.messages.length,
createdAt: session.createdAt,
updatedAt: session.updatedAt,
});
} catch {
// 跳过无法解析的文件
}
}
// 按更新时间降序排列
return summaries.sort(
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
);
}
/**
* 加载指定会话
*/
async loadSession(sessionId: string): Promise<SessionData | null> {
try {
const filePath = path.join(this.sessionsDir, `${sessionId}.json`);
const content = await fs.readFile(filePath, 'utf-8');
return JSON.parse(content) as SessionData;
} catch {
return null;
}
}
/**
* 保存指定会话(用于子会话)
*/
async saveSession(session: SessionData): Promise<void> {
await this.ensureDir();
session.updatedAt = new Date().toISOString();
const filePath = path.join(this.sessionsDir, `${session.id}.json`);
await fs.writeFile(filePath, JSON.stringify(session, null, 2), 'utf-8');
}
/**
* 删除指定会话
*/
async deleteSession(sessionId: string): Promise<boolean> {
try {
const filePath = path.join(this.sessionsDir, `${sessionId}.json`);
await fs.unlink(filePath);
return true;
} catch {
return false;
}
}
/**
* 清理旧会话(保留最近 N 个)
*/
async cleanupOldSessions(keepCount: number = 50): Promise<number> {
const sessions = await this.listSessions();
if (sessions.length <= keepCount) {
return 0;
}
const toDelete = sessions.slice(keepCount);
let deletedCount = 0;
for (const session of toDelete) {
if (await this.deleteSession(session.id)) {
deletedCount++;
}
}
return deletedCount;
}
/**
* 从会话生成标题
*/
private generateTitle(session: SessionData): string {
// 从第一条用户消息生成标题
const firstUserMessage = session.messages.find((m) => m.role === 'user');
if (firstUserMessage && typeof firstUserMessage.content === 'string') {
const content = firstUserMessage.content;
// 取前 50 个字符
return content.length > 50 ? content.substring(0, 50) + '...' : content;
}
return `会话 ${session.id}`;
}
/**
* 获取存储目录路径
*/
getStorageDir(): string {
return this.storageDir;
}
}
// 导出默认实例
export const sessionStorage = new SessionStorage();