5e32375f0e
架构变更: - 采用 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 - 配置管理
197 lines
4.5 KiB
TypeScript
197 lines
4.5 KiB
TypeScript
/**
|
|
* 自动提交管理器
|
|
*
|
|
* 参考 aider 的 auto_commit 实现
|
|
* 支持 immediate、batch、manual 三种模式
|
|
*/
|
|
|
|
import { minimatch } from 'minimatch';
|
|
import type { GitRepo } from './repo.js';
|
|
import type { AutoCommitConfig, CommitResult } from './types.js';
|
|
import { MessageGenerator } from './message-generator.js';
|
|
|
|
export class AutoCommitManager {
|
|
private repo: GitRepo;
|
|
private config: AutoCommitConfig;
|
|
private messageGenerator: MessageGenerator;
|
|
|
|
/** 待提交的文件 */
|
|
private pendingFiles: Set<string> = new Set();
|
|
|
|
/** 批量提交定时器 */
|
|
private batchTimer: NodeJS.Timeout | null = null;
|
|
|
|
/** 提交回调 */
|
|
private onCommitCallback?: (result: CommitResult) => void;
|
|
|
|
constructor(repo: GitRepo, config: AutoCommitConfig, messageGenerator: MessageGenerator) {
|
|
this.repo = repo;
|
|
this.config = config;
|
|
this.messageGenerator = messageGenerator;
|
|
}
|
|
|
|
/**
|
|
* 设置提交回调
|
|
*/
|
|
setOnCommit(callback: (result: CommitResult) => void): void {
|
|
this.onCommitCallback = callback;
|
|
}
|
|
|
|
/**
|
|
* 文件变更后调用
|
|
*/
|
|
async onFileChanged(filePath: string, changeType: 'create' | 'modify' | 'delete'): Promise<void> {
|
|
if (!this.config.enabled) {
|
|
return;
|
|
}
|
|
|
|
// 检查是否应该排除
|
|
if (this.shouldExclude(filePath)) {
|
|
return;
|
|
}
|
|
|
|
// 如果启用脏文件提交,检查文件是否为脏状态
|
|
if (this.config.dirtyCommits && changeType !== 'create') {
|
|
const isDirty = await this.repo.isDirty(filePath);
|
|
if (isDirty) {
|
|
// 在新的编辑之前,先提交脏文件
|
|
await this.commitDirtyFile(filePath);
|
|
}
|
|
}
|
|
|
|
// 添加到待提交列表
|
|
this.pendingFiles.add(filePath);
|
|
|
|
// 根据模式处理
|
|
switch (this.config.mode) {
|
|
case 'immediate':
|
|
await this.executeCommit();
|
|
break;
|
|
|
|
case 'batch':
|
|
this.scheduleBatchCommit();
|
|
break;
|
|
|
|
case 'manual':
|
|
// 不自动提交,只记录
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 计划批量提交
|
|
*/
|
|
private scheduleBatchCommit(): void {
|
|
if (this.batchTimer) {
|
|
clearTimeout(this.batchTimer);
|
|
}
|
|
|
|
this.batchTimer = setTimeout(async () => {
|
|
this.batchTimer = null;
|
|
await this.executeCommit();
|
|
}, this.config.batchDelay);
|
|
}
|
|
|
|
/**
|
|
* 执行提交
|
|
*/
|
|
private async executeCommit(): Promise<CommitResult | null> {
|
|
if (this.pendingFiles.size === 0) {
|
|
return null;
|
|
}
|
|
|
|
const files = Array.from(this.pendingFiles);
|
|
this.pendingFiles.clear();
|
|
|
|
try {
|
|
// 获取差异用于生成消息
|
|
const diff = await this.repo.getDiff({ staged: false });
|
|
|
|
// 生成提交消息
|
|
const message = this.messageGenerator.generate(diff, files);
|
|
|
|
// 执行提交
|
|
const result = await this.repo.commit({
|
|
files,
|
|
message,
|
|
aiEdits: true,
|
|
});
|
|
|
|
// 调用回调
|
|
if (result.success && this.onCommitCallback) {
|
|
this.onCommitCallback(result);
|
|
}
|
|
|
|
return result;
|
|
} catch (error) {
|
|
// 恢复 pending 状态
|
|
files.forEach((f) => this.pendingFiles.add(f));
|
|
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 提交脏文件
|
|
*/
|
|
private async commitDirtyFile(filePath: string): Promise<void> {
|
|
try {
|
|
await this.repo.commit({
|
|
files: [filePath],
|
|
message: `chore: save changes to ${filePath} before AI edit`,
|
|
aiEdits: false, // 用户的脏文件,不标记为 AI 编辑
|
|
});
|
|
} catch {
|
|
// 脏文件提交失败不影响主流程
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 检查文件是否应该排除
|
|
*/
|
|
private shouldExclude(filePath: string): boolean {
|
|
return this.config.excludePatterns.some((pattern) =>
|
|
minimatch(filePath, pattern, { matchBase: true })
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 强制立即提交
|
|
*/
|
|
async flush(): Promise<CommitResult | null> {
|
|
if (this.batchTimer) {
|
|
clearTimeout(this.batchTimer);
|
|
this.batchTimer = null;
|
|
}
|
|
return this.executeCommit();
|
|
}
|
|
|
|
/**
|
|
* 获取待提交文件
|
|
*/
|
|
getPendingFiles(): string[] {
|
|
return Array.from(this.pendingFiles);
|
|
}
|
|
|
|
/**
|
|
* 清除待提交文件
|
|
*/
|
|
clearPending(): void {
|
|
if (this.batchTimer) {
|
|
clearTimeout(this.batchTimer);
|
|
this.batchTimer = null;
|
|
}
|
|
this.pendingFiles.clear();
|
|
}
|
|
|
|
/**
|
|
* 是否有待提交文件
|
|
*/
|
|
hasPendingFiles(): boolean {
|
|
return this.pendingFiles.size > 0;
|
|
}
|
|
}
|