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
+576
View File
@@ -0,0 +1,576 @@
/**
* Shadow Git 存储实现
* 使用隔离的 Git 仓库存储检查点,不影响用户的主仓库
*/
import * as fs from 'fs/promises';
import * as path from 'path';
import { execFile } from 'child_process';
import { promisify } from 'util';
import type { FileChange, DiffInfo, FileDiff } from './types.js';
const execFileAsync = promisify(execFile);
/**
* 计算工作目录哈希
* 使用 31 进制哈希算法,与 Cline 保持一致
*/
export function hashWorkingDir(workingDir: string): string {
let hash = 0;
for (let i = 0; i < workingDir.length; i++) {
hash = ((hash * 31 + workingDir.charCodeAt(i)) >>> 0) % 2147483647;
}
return hash.toString().slice(0, 13).padStart(13, '0');
}
/**
* 需要排除的目录列表
*/
const EXCLUDED_DIRS = [
'node_modules',
'.git',
'dist',
'build',
'.next',
'__pycache__',
'.pytest_cache',
'coverage',
'.nyc_output',
'.ai-assist',
];
/**
* Shadow Git 管理器
*/
export class ShadowGit {
private workDir: string;
private shadowGitDir: string;
private initialized = false;
private cwdHash: string;
constructor(workDir: string, storageBaseDir: string) {
this.workDir = path.resolve(workDir);
this.cwdHash = hashWorkingDir(this.workDir);
this.shadowGitDir = path.join(storageBaseDir, this.cwdHash);
}
/**
* 获取工作目录哈希
*/
getCwdHash(): string {
return this.cwdHash;
}
/**
* 获取 Shadow Git 目录
*/
getShadowGitDir(): string {
return this.shadowGitDir;
}
/**
* 初始化 Shadow Git 仓库
*/
async initialize(): Promise<void> {
if (this.initialized) return;
const gitDir = path.join(this.shadowGitDir, '.git');
try {
await fs.access(gitDir);
// 已存在,验证配置
await this.verifyConfig();
} catch {
// 不存在,创建新仓库
await this.createRepository();
}
this.initialized = true;
}
/**
* 创建新的 Shadow Git 仓库
*/
private async createRepository(): Promise<void> {
// 创建目录
await fs.mkdir(this.shadowGitDir, { recursive: true });
// 初始化 Git 仓库
await this.git(['init']);
// 配置用户信息
await this.git(['config', 'user.name', 'AI Assistant Checkpoint']);
await this.git(['config', 'user.email', 'checkpoint@ai-assist.local']);
// 配置工作目录
await this.git(['config', 'core.worktree', this.workDir]);
// 禁用 GPG 签名
await this.git(['config', 'commit.gpgsign', 'false']);
// 创建 .gitignore
const gitignoreContent = EXCLUDED_DIRS.map((d) => `${d}/`).join('\n') + '\n';
await fs.writeFile(
path.join(this.shadowGitDir, '.gitignore'),
gitignoreContent
);
// 创建初始提交
await this.git(['add', '.gitignore']);
await this.git([
'commit',
'--allow-empty',
'-m',
'Initial checkpoint repository',
]);
}
/**
* 验证现有配置
*/
private async verifyConfig(): Promise<void> {
try {
const { stdout } = await this.git(['config', 'core.worktree']);
const configuredWorkDir = stdout.trim();
if (configuredWorkDir !== this.workDir) {
// 更新工作目录配置
await this.git(['config', 'core.worktree', this.workDir]);
}
} catch {
// 配置不存在,添加
await this.git(['config', 'core.worktree', this.workDir]);
}
}
/**
* 执行 Git 命令
*/
private async git(
args: string[],
options: { cwd?: string } = {}
): Promise<{ stdout: string; stderr: string }> {
const cwd = options.cwd || this.shadowGitDir;
const gitDir = path.join(this.shadowGitDir, '.git');
try {
const result = await execFileAsync(
'git',
['--git-dir', gitDir, '--work-tree', this.workDir, ...args],
{
cwd,
maxBuffer: 50 * 1024 * 1024, // 50MB
env: {
...process.env,
GIT_TERMINAL_PROMPT: '0',
},
}
);
return result;
} catch (error: any) {
// 某些 git 命令失败是正常的 (如空提交)
if (error.stdout !== undefined) {
return { stdout: error.stdout || '', stderr: error.stderr || '' };
}
throw error;
}
}
/**
* 创建检查点提交
*/
async createCommit(message: string): Promise<string> {
await this.initialize();
// 暂时禁用嵌套 .git 目录
const nestedGitDirs = await this.findNestedGitDirs();
await this.renameNestedGitDirs(nestedGitDirs, true);
try {
// 添加所有文件
await this.git(['add', '.', '--ignore-errors']);
// 创建提交
await this.git([
'commit',
'--allow-empty',
'--no-verify',
'-m',
message,
]);
// 获取 commit hash
const { stdout } = await this.git(['rev-parse', 'HEAD']);
return stdout.trim();
} finally {
// 恢复嵌套 .git 目录
await this.renameNestedGitDirs(nestedGitDirs, false);
}
}
/**
* 查找嵌套的 .git 目录
*/
private async findNestedGitDirs(): Promise<string[]> {
const nestedDirs: string[] = [];
const walk = async (dir: string, depth = 0): Promise<void> => {
if (depth > 5) return; // 限制深度
try {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const fullPath = path.join(dir, entry.name);
// 跳过排除的目录
if (EXCLUDED_DIRS.includes(entry.name)) continue;
if (entry.name === '.git') {
nestedDirs.push(fullPath);
} else if (!entry.name.startsWith('.')) {
await walk(fullPath, depth + 1);
}
}
} catch {
// 忽略无法访问的目录
}
};
await walk(this.workDir);
return nestedDirs;
}
/**
* 重命名嵌套 .git 目录
*/
private async renameNestedGitDirs(
dirs: string[],
disable: boolean
): Promise<void> {
for (const dir of dirs) {
const disabledName = dir + '_disabled';
try {
if (disable) {
await fs.rename(dir, disabledName);
} else {
await fs.rename(disabledName, dir);
}
} catch {
// 忽略错误
}
}
}
/**
* 获取当前 HEAD commit hash
*/
async getHead(): Promise<string> {
await this.initialize();
const { stdout } = await this.git(['rev-parse', 'HEAD']);
return stdout.trim();
}
/**
* 重置到指定 commit
*/
async resetHard(commitHash: string): Promise<void> {
await this.initialize();
// 暂时禁用嵌套 .git 目录
const nestedGitDirs = await this.findNestedGitDirs();
await this.renameNestedGitDirs(nestedGitDirs, true);
try {
await this.git(['reset', '--hard', commitHash]);
} finally {
await this.renameNestedGitDirs(nestedGitDirs, false);
}
}
/**
* 获取 commit 列表
*/
async getCommits(limit = 100): Promise<
Array<{
hash: string;
message: string;
timestamp: number;
}>
> {
await this.initialize();
const { stdout } = await this.git([
'log',
`--max-count=${limit}`,
'--format=%H|%s|%ct',
]);
if (!stdout.trim()) return [];
return stdout
.trim()
.split('\n')
.map((line) => {
const [hash, message, timestamp] = line.split('|');
return {
hash,
message,
timestamp: parseInt(timestamp, 10) * 1000,
};
});
}
/**
* 获取两个 commit 之间的差异摘要
*/
async getDiffSummary(fromCommit: string, toCommit = 'HEAD'): Promise<DiffInfo> {
await this.initialize();
const { stdout } = await this.git([
'diff',
'--stat',
'--numstat',
fromCommit,
toCommit,
]);
const files: FileChange[] = [];
let totalInsertions = 0;
let totalDeletions = 0;
// 解析 numstat 输出
const lines = stdout.trim().split('\n');
for (const line of lines) {
const match = line.match(/^(\d+|-)\t(\d+|-)\t(.+)$/);
if (match) {
const insertions = match[1] === '-' ? 0 : parseInt(match[1], 10);
const deletions = match[2] === '-' ? 0 : parseInt(match[2], 10);
const filePath = match[3];
// 检测重命名
const renameMatch = filePath.match(/^(.+)\{(.+) => (.+)\}(.*)$/);
if (renameMatch) {
const prefix = renameMatch[1];
const oldName = renameMatch[2];
const newName = renameMatch[3];
const suffix = renameMatch[4];
files.push({
path: prefix + newName + suffix,
oldPath: prefix + oldName + suffix,
type: 'renamed',
insertions,
deletions,
});
} else {
// 获取文件状态
const type = await this.getFileChangeType(fromCommit, toCommit, filePath);
files.push({
path: filePath,
type,
insertions,
deletions,
});
}
totalInsertions += insertions;
totalDeletions += deletions;
}
}
return {
from: fromCommit,
to: toCommit,
files,
totalInsertions,
totalDeletions,
};
}
/**
* 获取文件变更类型
*/
private async getFileChangeType(
fromCommit: string,
toCommit: string,
filePath: string
): Promise<FileChange['type']> {
try {
const { stdout } = await this.git([
'diff',
'--name-status',
fromCommit,
toCommit,
'--',
filePath,
]);
const status = stdout.trim().charAt(0);
switch (status) {
case 'A':
return 'added';
case 'D':
return 'deleted';
case 'R':
return 'renamed';
default:
return 'modified';
}
} catch {
return 'modified';
}
}
/**
* 获取文件内容差异
*/
async getFileDiff(
fromCommit: string,
toCommit: string,
filePath: string
): Promise<FileDiff> {
await this.initialize();
const type = await this.getFileChangeType(fromCommit, toCommit, filePath);
let oldContent: string | undefined;
let newContent: string | undefined;
let patch: string | undefined;
try {
if (type !== 'added') {
const { stdout } = await this.git(['show', `${fromCommit}:${filePath}`]);
oldContent = stdout;
}
} catch {
// 文件在旧 commit 中不存在
}
try {
if (type !== 'deleted') {
const { stdout } = await this.git(['show', `${toCommit}:${filePath}`]);
newContent = stdout;
}
} catch {
// 文件在新 commit 中不存在
}
try {
const { stdout } = await this.git([
'diff',
fromCommit,
toCommit,
'--',
filePath,
]);
patch = stdout;
} catch {
// 无法生成 diff
}
return {
path: filePath,
type,
oldContent,
newContent,
patch,
};
}
/**
* 检出指定 commit 的特定文件
*/
async checkoutFiles(commitHash: string, files: string[]): Promise<void> {
await this.initialize();
for (const file of files) {
try {
await this.git(['checkout', commitHash, '--', file]);
} catch (error) {
// 文件可能不存在于该 commit
console.warn(`Failed to checkout ${file} from ${commitHash}:`, error);
}
}
}
/**
* 获取指定 commit 中文件的内容
*/
async getFileContent(commitHash: string, filePath: string): Promise<string | null> {
await this.initialize();
try {
const { stdout } = await this.git(['show', `${commitHash}:${filePath}`]);
return stdout;
} catch {
return null;
}
}
/**
* 清理旧的 commit(保留最近 N 个)
*/
async cleanup(keepCount: number): Promise<number> {
await this.initialize();
const commits = await this.getCommits(keepCount + 100);
if (commits.length <= keepCount) {
return 0;
}
// 使用 git gc 清理
try {
await this.git(['gc', '--aggressive', '--prune=now']);
} catch {
// gc 可能失败,忽略
}
return commits.length - keepCount;
}
/**
* 检查是否有未提交的变更
*/
async hasChanges(): Promise<boolean> {
await this.initialize();
const { stdout } = await this.git(['status', '--porcelain']);
return stdout.trim().length > 0;
}
/**
* 获取工作目录与 HEAD 的差异
*/
async getWorkingDirDiff(): Promise<DiffInfo> {
await this.initialize();
// 先添加所有文件到暂存区以检测新文件
const nestedGitDirs = await this.findNestedGitDirs();
await this.renameNestedGitDirs(nestedGitDirs, true);
try {
await this.git(['add', '.', '--ignore-errors']);
const result = await this.getDiffSummary('HEAD', '--staged');
// 重置暂存区
await this.git(['reset', 'HEAD']);
return result;
} finally {
await this.renameNestedGitDirs(nestedGitDirs, false);
}
}
}
/**
* 创建 Shadow Git 实例
*/
export function createShadowGit(
workDir: string,
storageBaseDir: string
): ShadowGit {
return new ShadowGit(workDir, storageBaseDir);
}