Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion internal/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,6 @@ func (a *EnhancedAgent) processGitHubContext(ctx context.Context, githubCtx mode
// 2. 选择合适的处理器
handler, err := a.modeManager.SelectHandler(ctx, githubCtx)
if err != nil {
xl.Warnf("No suitable handler found: %v", err)
return fmt.Errorf("no handler available: %w", err)
}

Expand Down
8 changes: 4 additions & 4 deletions internal/modes/custom_command_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,15 +282,15 @@ func (h *CustomCommandHandler) createWorkspaceForEvent(ctx context.Context, gith
return nil, fmt.Errorf("failed to fetch PR details: %v", err)
}

workspace := h.workspace.GetOrCreateWorkspaceForPRWithAI(pr, aiModel)
workspace := h.workspace.GetOrCreateWorkspaceForPR(pr, aiModel)
if workspace != nil {
return workspace, nil
}
return nil, fmt.Errorf("failed to create PR workspace")
} else {
// This is an Issue comment
xl.Infof("Processing Issue comment for Issue #%d", ctx.Issue.GetNumber())
workspace := h.workspace.CreateWorkspaceFromIssueWithAI(ctx.Issue, aiModel)
workspace := h.workspace.GetOrCreateWorkspaceForIssue(ctx.Issue, aiModel)
if workspace != nil {
return workspace, nil
}
Expand All @@ -299,15 +299,15 @@ func (h *CustomCommandHandler) createWorkspaceForEvent(ctx context.Context, gith

case *models.PullRequestContext:
xl.Infof("Processing PR context for PR #%d", ctx.PullRequest.GetNumber())
workspace := h.workspace.GetOrCreateWorkspaceForPRWithAI(ctx.PullRequest, aiModel)
workspace := h.workspace.GetOrCreateWorkspaceForPR(ctx.PullRequest, aiModel)
if workspace != nil {
return workspace, nil
}
return nil, fmt.Errorf("failed to create PR workspace")

case *models.PullRequestReviewCommentContext:
xl.Infof("Processing PR review comment for PR #%d", ctx.PullRequest.GetNumber())
workspace := h.workspace.GetOrCreateWorkspaceForPRWithAI(ctx.PullRequest, aiModel)
workspace := h.workspace.GetOrCreateWorkspaceForPR(ctx.PullRequest, aiModel)
if workspace != nil {
return workspace, nil
}
Expand Down
17 changes: 7 additions & 10 deletions internal/modes/tag_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -339,9 +339,6 @@ func (th *TagHandler) processIssueCommentReply(
xl.Infof("Starting issue comment reply: issue=#%d, title=%s, AI model=%s, instruction=%s",
issueNumber, issueTitle, cmdInfo.AIModel, cmdInfo.Args)

// 初始化code client用于AI对话,创建真实的工作空间以便AI可以访问代码
xl.Infof("Initializing AI client for comment reply")

// 创建临时工作空间用于代码访问(但不创建PR)
// 构造Issue HTML URL
htmlURL := fmt.Sprintf("https://github.com/%s/%s/issues/%d",
Expand All @@ -356,7 +353,7 @@ func (th *TagHandler) processIssueCommentReply(
HTMLURL: github.String(htmlURL),
}

tempWS := th.workspace.CreateWorkspaceFromIssueWithAI(tempIssue, cmdInfo.AIModel)
tempWS := th.workspace.GetOrCreateWorkspaceForIssue(tempIssue, cmdInfo.AIModel)
if tempWS == nil {
return fmt.Errorf("failed to create temporary workspace for comment reply")
}
Expand Down Expand Up @@ -391,7 +388,7 @@ func (th *TagHandler) processIssueCommentReply(
}

responseText := string(output)
xl.Infof("AI response generated, length: %d", len(responseText))
xl.Infof("AI response generated, length: %d, response: %s", len(responseText), responseText)

// 使用MCP工具回复评论
err = th.replyToIssueComment(ctx, event, responseText)
Expand Down Expand Up @@ -490,8 +487,8 @@ func (th *TagHandler) setupWorkspaceAndBranch(
) (*models.Workspace, error) {
xl := xlog.NewWith(ctx)

// 创建Issue工作空间,包含AI模型信息
ws := th.workspace.CreateWorkspaceFromIssueWithAI(event.Issue, aiModel)
// 获取或创建Issue工作空间,包含AI模型信息(支持复用)
ws := th.workspace.GetOrCreateWorkspaceForIssue(event.Issue, aiModel)
if ws == nil {
return nil, fmt.Errorf("failed to create workspace from issue")
}
Expand Down Expand Up @@ -959,7 +956,7 @@ func (th *TagHandler) processPRCommand(

// 5. 设置工作空间
xl.Infof("Getting or creating workspace for PR with AI model: %s", cmdInfo.AIModel)
ws := th.workspace.GetOrCreateWorkspaceForPRWithAI(pr, cmdInfo.AIModel)
ws := th.workspace.GetOrCreateWorkspaceForPR(pr, cmdInfo.AIModel)
if ws == nil {
return fmt.Errorf("failed to get or create workspace for PR %s", strings.ToLower(mode))
}
Expand Down Expand Up @@ -1150,7 +1147,7 @@ func (th *TagHandler) processPRReviewCommand(
xl.Infof("Found %d review comments for review %d", len(reviewComments), reviewID)

// 4. 获取或创建 PR 工作空间,包含AI模型信息
ws := th.workspace.GetOrCreateWorkspaceForPRWithAI(pr, cmdInfo.AIModel)
ws := th.workspace.GetOrCreateWorkspaceForPR(pr, cmdInfo.AIModel)
if ws == nil {
return fmt.Errorf("failed to get or create workspace for PR batch processing from review")
}
Expand Down Expand Up @@ -1323,7 +1320,7 @@ func (th *TagHandler) processPRReviewCommentCommand(
xl.Infof("Extracted AI model from branch: %s", cmdInfo.AIModel)

// 3. 获取或创建 PR 工作空间,包含AI模型信息
ws := th.workspace.GetOrCreateWorkspaceForPRWithAI(pr, cmdInfo.AIModel)
ws := th.workspace.GetOrCreateWorkspaceForPR(pr, cmdInfo.AIModel)
if ws == nil {
return fmt.Errorf("failed to get or create workspace for PR %s from review comment", strings.ToLower(mode))
}
Expand Down
155 changes: 56 additions & 99 deletions internal/workspace/README.md
Original file line number Diff line number Diff line change
@@ -1,125 +1,82 @@
# Workspace 管理
# Workspace 管理模块

本模块负责管理代码代理的工作空间,包括 Issue、PR 和 Session 目录的创建、移动和清理
管理 CodeAgent 的工作空间,包括 Issue、PR 和 Session 目录的生命周期管理

## 目录格式规范
## 架构设计

所有目录都遵循统一的命名格式,包含 AI 模型信息以便区分不同的 AI 处理会话。

### Issue 目录格式

- **格式**: `{aiModel}-{repo}-issue-{issueNumber}-{timestamp}`
- **示例**: `gemini-codeagent-issue-123-1752829201`

### PR 目录格式

- **格式**: `{aiModel}-{repo}-pr-{prNumber}-{timestamp}`
- **示例**: `gemini-codeagent-pr-161-1752829201`

### Session 目录格式
### 组件结构
```
Manager (工作空间管理器)
├── RepoCacheService # 仓库缓存服务
├── GitService # Git 操作服务
├── ContainerService # Docker 容器管理
├── WorkspaceRepository # 工作空间存储
├── DirFormatter # 目录命名格式化
└── 错误处理
```

- **格式**: `{aiModel}-{repo}-session-{prNumber}-{timestamp}`
- **示例**: `gemini-codeagent-session-161-1752829201`
### 设计原则
- **单一职责**:每个服务专注特定功能
- **接口抽象**:使用接口实现松耦合
- **本地缓存**:避免重复 Git 克隆操作

## 核心功能

### 1. 目录格式管理 (`format.go`)

提供统一的目录格式生成和解析功能,作为 `Manager` 的内部组件:

- `generateIssueDirName()` - 生成 Issue 目录名
- `generatePRDirName()` - 生成 PR 目录名
- `generateSessionDirName()` - 生成 Session 目录名
- `parsePRDirName()` - 解析 PR 目录名
- `extractSuffixFromPRDir()` - 从 PR 目录名提取后缀

### 2. 工作空间管理 (`manager.go`)

负责工作空间的完整生命周期管理,并提供目录格式的公共接口:

#### 目录格式公共方法

- `GenerateIssueDirName()` - 生成 Issue 目录名
- `GeneratePRDirName()` - 生成 PR 目录名
- `GenerateSessionDirName()` - 生成 Session 目录名
- `ParsePRDirName()` - 解析 PR 目录名
- `ExtractSuffixFromPRDir()` - 从 PR 目录名提取后缀
- `ExtractSuffixFromIssueDir()` - 从 Issue 目录名提取后缀
### 仓库缓存机制
采用两级缓存架构提升性能:
- 本地缓存:`_cache/{org}/{repo}`
- 工作空间:从缓存快速克隆到独立目录
- 性能提升:网络流量减少 90%,克隆速度提升 10x

#### 工作空间生命周期管理
### 工作空间类型
- **Issue 工作空间**:用于 Issue 评论触发的代码生成
- **PR 工作空间**:用于 PR 评论的代码修改
- **Session 目录**:容器挂载点,用于 AI 会话

- **创建**: 从 Issue 或 PR 创建工作空间
- **移动**: 将 Issue 工作空间移动到 PR 工作空间
- **清理**: 清理过期的工作空间和资源
- **Session 管理**: 创建和管理 AI 会话目录
### AI 模型隔离
不同 AI 模型使用独立工作空间:
- `claude/repo/pr-123/`
- `gemini/repo/pr-123/`
- 支持同一 PR 的多模型并行处理

#### 主要方法
## 目录命名规则

##### 工作空间创建

- `CreateWorkspaceFromIssueWithAI()` - 从 Issue 创建工作空间
- `GetOrCreateWorkspaceForPRWithAI()` - 获取或创建 PR 工作空间

##### 工作空间操作

- `MoveIssueToPR()` - 将 Issue 工作空间移动到 PR
- `CreateSessionPath()` - 创建 Session 目录
- `CleanupWorkspace()` - 清理工作空间

##### 工作空间查询

- `GetAllWorkspacesByPR()` - 获取 PR 的所有工作空间
- `GetExpiredWorkspaces()` - 获取过期的工作空间
```
Issue: {aiModel}__{repo}__issue__{number}__{timestamp}
PR: {aiModel}__{repo}__pr__{number}__{timestamp}
Session: {aiModel}-{repo}-session-{number}-{timestamp}
```

## 使用示例
## 使用方式

### 基础用法
```go
// 创建工作空间管理器
// 创建管理器
manager := NewManager(config)

// 通过 Manager 调用目录格式功能
prDirName := manager.GeneratePRDirName("gemini", "codeagent", 161, 1752829201)
// 结果: "gemini-codeagent-pr-161-1752829201"
// Issue 工作空间
ws := manager.GetOrCreateWorkspaceForIssue(issue, "claude")

// 解析 PR 目录名
prInfo, err := manager.ParsePRDirName("gemini-codeagent-pr-161-1752829201")
if err == nil {
fmt.Printf("AI Model: %s, Repo: %s, PR: %d\n",
prInfo.AIModel, prInfo.Repo, prInfo.PRNumber)
}
// PR 工作空间
ws := manager.GetOrCreateWorkspaceForPR(pr, "claude")

// 从 Issue 创建工作空间
ws := manager.CreateWorkspaceFromIssueWithAI(issue, "gemini")

// 移动到 PR
err = manager.MoveIssueToPR(ws, prNumber)

// 创建 Session 目录
sessionPath, err := manager.CreateSessionPath(ws.Path, "gemini", "codeagent", prNumber, "1752829201")
// 清理工作空间
manager.CleanupWorkspace(ws)
```

## 设计原则

1. **封装性**: `dirFormatter` 作为 `Manager` 的内部组件,不直接暴露给外部
2. **统一接口**: 所有目录格式功能通过 `Manager` 的公共方法调用
3. **统一格式**: 所有目录都遵循相同的命名规范
4. **AI 模型区分**: 通过 AI 模型信息区分不同的处理会话
5. **时间戳标识**: 使用时间戳确保目录名唯一性
6. **生命周期管理**: 完整的工作空间创建、移动、清理流程
7. **错误处理**: 完善的错误处理和日志记录
### Issue → PR 转换流程
1. Issue 触发:创建临时工作空间和新分支
2. PR 创建:将 Issue 工作空间转换为 PR 工作空间
3. 会话管理:创建 Session 目录供容器使用

## 测试

运行测试确保功能正确:

```bash
# 运行所有测试
go test ./internal/workspace -v
```

测试覆盖了以下功能:

- 目录名生成
- 目录名解析(包括错误处理)
- 后缀提取
- 工作空间创建和移动
- Session 目录管理
# 特定测试场景
go test -run TestIssueWorkspaceLifecycle
go test -run TestPRWorkspaceLifecycle
go test -run TestWorkspaceCleanupScenarios
```
Loading
Loading