Skip to content

Commit d1ab2fc

Browse files
authored
refactor(codeagent): enhance worksace (#170)
1 parent bfd59d6 commit d1ab2fc

File tree

6 files changed

+578
-208
lines changed

6 files changed

+578
-208
lines changed

internal/agent/agent.go

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,12 @@ func (a *Agent) StartCleanupRoutine() {
5252
defer ticker.Stop()
5353

5454
for range ticker.C {
55-
a.cleanupExpiredResouces()
55+
a.cleanupExpiredResources()
5656
}
5757
}
5858

59-
// cleanupExpiredResouces 清理过期的工作空间
60-
func (a *Agent) cleanupExpiredResouces() {
59+
// cleanupExpiredResources 清理过期的工作空间
60+
func (a *Agent) cleanupExpiredResources() {
6161
m := a.workspace
6262

6363
// 先收集过期的工作空间,避免在持有锁时调用可能获取锁的方法
@@ -141,10 +141,7 @@ func (a *Agent) ProcessIssueCommentWithAI(ctx context.Context, event *github.Iss
141141
// 5. 创建 session 目录
142142
// 从PR目录名中提取suffix
143143
prDirName := filepath.Base(ws.Path)
144-
var suffix string
145-
// 目录格式:{aiModel}-{repo}-pr-{prNumber}-{timestamp}
146-
expectedPrefix := fmt.Sprintf("%s-%s-pr-%d-", ws.AIModel, ws.Repo, pr.GetNumber())
147-
suffix = strings.TrimPrefix(prDirName, expectedPrefix)
144+
suffix := a.workspace.ExtractSuffixFromPRDir(ws.AIModel, ws.Repo, pr.GetNumber(), prDirName)
148145

149146
sessionPath, err := a.workspace.CreateSessionPath(filepath.Dir(ws.Path), ws.AIModel, ws.Repo, pr.GetNumber(), suffix)
150147
if err != nil {

internal/workspace/README.md

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# Workspace 管理
2+
3+
本模块负责管理代码代理的工作空间,包括 Issue、PR 和 Session 目录的创建、移动和清理。
4+
5+
## 目录格式规范
6+
7+
所有目录都遵循统一的命名格式,包含 AI 模型信息以便区分不同的 AI 处理会话。
8+
9+
### Issue 目录格式
10+
11+
- **格式**: `{aiModel}-{repo}-issue-{issueNumber}-{timestamp}`
12+
- **示例**: `gemini-codeagent-issue-123-1752829201`
13+
14+
### PR 目录格式
15+
16+
- **格式**: `{aiModel}-{repo}-pr-{prNumber}-{timestamp}`
17+
- **示例**: `gemini-codeagent-pr-161-1752829201`
18+
19+
### Session 目录格式
20+
21+
- **格式**: `{aiModel}-{repo}-session-{prNumber}-{timestamp}`
22+
- **示例**: `gemini-codeagent-session-161-1752829201`
23+
24+
## 核心功能
25+
26+
### 1. 目录格式管理 (`format.go`)
27+
28+
提供统一的目录格式生成和解析功能,作为 `Manager` 的内部组件:
29+
30+
- `generateIssueDirName()` - 生成 Issue 目录名
31+
- `generatePRDirName()` - 生成 PR 目录名
32+
- `generateSessionDirName()` - 生成 Session 目录名
33+
- `parsePRDirName()` - 解析 PR 目录名
34+
- `extractSuffixFromPRDir()` - 从 PR 目录名提取后缀
35+
36+
### 2. 工作空间管理 (`manager.go`)
37+
38+
负责工作空间的完整生命周期管理,并提供目录格式的公共接口:
39+
40+
#### 目录格式公共方法
41+
42+
- `GenerateIssueDirName()` - 生成 Issue 目录名
43+
- `GeneratePRDirName()` - 生成 PR 目录名
44+
- `GenerateSessionDirName()` - 生成 Session 目录名
45+
- `ParsePRDirName()` - 解析 PR 目录名
46+
- `ExtractSuffixFromPRDir()` - 从 PR 目录名提取后缀
47+
- `ExtractSuffixFromIssueDir()` - 从 Issue 目录名提取后缀
48+
49+
#### 工作空间生命周期管理
50+
51+
- **创建**: 从 Issue 或 PR 创建工作空间
52+
- **移动**: 将 Issue 工作空间移动到 PR 工作空间
53+
- **清理**: 清理过期的工作空间和资源
54+
- **Session 管理**: 创建和管理 AI 会话目录
55+
56+
#### 主要方法
57+
58+
##### 工作空间创建
59+
60+
- `CreateWorkspaceFromIssueWithAI()` - 从 Issue 创建工作空间
61+
- `GetOrCreateWorkspaceForPRWithAI()` - 获取或创建 PR 工作空间
62+
63+
##### 工作空间操作
64+
65+
- `MoveIssueToPR()` - 将 Issue 工作空间移动到 PR
66+
- `CreateSessionPath()` - 创建 Session 目录
67+
- `CleanupWorkspace()` - 清理工作空间
68+
69+
##### 工作空间查询
70+
71+
- `GetAllWorkspacesByPR()` - 获取 PR 的所有工作空间
72+
- `GetExpiredWorkspaces()` - 获取过期的工作空间
73+
74+
## 使用示例
75+
76+
```go
77+
// 创建工作空间管理器
78+
manager := NewManager(config)
79+
80+
// 通过 Manager 调用目录格式功能
81+
prDirName := manager.GeneratePRDirName("gemini", "codeagent", 161, 1752829201)
82+
// 结果: "gemini-codeagent-pr-161-1752829201"
83+
84+
// 解析 PR 目录名
85+
prInfo, err := manager.ParsePRDirName("gemini-codeagent-pr-161-1752829201")
86+
if err == nil {
87+
fmt.Printf("AI Model: %s, Repo: %s, PR: %d\n",
88+
prInfo.AIModel, prInfo.Repo, prInfo.PRNumber)
89+
}
90+
91+
// 从 Issue 创建工作空间
92+
ws := manager.CreateWorkspaceFromIssueWithAI(issue, "gemini")
93+
94+
// 移动到 PR
95+
err = manager.MoveIssueToPR(ws, prNumber)
96+
97+
// 创建 Session 目录
98+
sessionPath, err := manager.CreateSessionPath(ws.Path, "gemini", "codeagent", prNumber, "1752829201")
99+
```
100+
101+
## 设计原则
102+
103+
1. **封装性**: `dirFormatter` 作为 `Manager` 的内部组件,不直接暴露给外部
104+
2. **统一接口**: 所有目录格式功能通过 `Manager` 的公共方法调用
105+
3. **统一格式**: 所有目录都遵循相同的命名规范
106+
4. **AI 模型区分**: 通过 AI 模型信息区分不同的处理会话
107+
5. **时间戳标识**: 使用时间戳确保目录名唯一性
108+
6. **生命周期管理**: 完整的工作空间创建、移动、清理流程
109+
7. **错误处理**: 完善的错误处理和日志记录
110+
111+
## 测试
112+
113+
运行测试确保功能正确:
114+
115+
```bash
116+
go test ./internal/workspace -v
117+
```
118+
119+
测试覆盖了以下功能:
120+
121+
- 目录名生成
122+
- 目录名解析(包括错误处理)
123+
- 后缀提取
124+
- 工作空间创建和移动
125+
- Session 目录管理

internal/workspace/format.go

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package workspace
2+
3+
import (
4+
"fmt"
5+
"strconv"
6+
"strings"
7+
)
8+
9+
// dirFormatter 目录格式管理器
10+
type dirFormatter struct{}
11+
12+
// newDirFormatter 创建目录格式管理器
13+
func newDirFormatter() *dirFormatter {
14+
return &dirFormatter{}
15+
}
16+
17+
// generateIssueDirName 生成Issue目录名
18+
func (f *dirFormatter) generateIssueDirName(aiModel, repo string, issueNumber int, timestamp int64) string {
19+
return fmt.Sprintf("%s-%s-issue-%d-%d", aiModel, repo, issueNumber, timestamp)
20+
}
21+
22+
// generatePRDirName 生成PR目录名
23+
func (f *dirFormatter) generatePRDirName(aiModel, repo string, prNumber int, timestamp int64) string {
24+
return fmt.Sprintf("%s-%s-pr-%d-%d", aiModel, repo, prNumber, timestamp)
25+
}
26+
27+
// generateSessionDirName 生成Session目录名
28+
func (f *dirFormatter) generateSessionDirName(aiModel, repo string, prNumber int, timestamp int64) string {
29+
return fmt.Sprintf("%s-%s-session-%d-%d", aiModel, repo, prNumber, timestamp)
30+
}
31+
32+
// generateSessionDirNameWithSuffix 生成Session目录名(使用suffix)
33+
func (f *dirFormatter) generateSessionDirNameWithSuffix(aiModel, repo string, prNumber int, suffix string) string {
34+
return fmt.Sprintf("%s-%s-session-%d-%s", aiModel, repo, prNumber, suffix)
35+
}
36+
37+
// extractSuffixFromPRDir 从PR目录名中提取suffix(时间戳)
38+
func (f *dirFormatter) extractSuffixFromPRDir(aiModel, repo string, prNumber int, dirName string) string {
39+
expectedPrefix := fmt.Sprintf("%s-%s-pr-%d-", aiModel, repo, prNumber)
40+
return strings.TrimPrefix(dirName, expectedPrefix)
41+
}
42+
43+
// extractSuffixFromIssueDir 从Issue目录名中提取suffix(时间戳)
44+
func (f *dirFormatter) extractSuffixFromIssueDir(aiModel, repo string, issueNumber int, dirName string) string {
45+
expectedPrefix := fmt.Sprintf("%s-%s-issue-%d-", aiModel, repo, issueNumber)
46+
return strings.TrimPrefix(dirName, expectedPrefix)
47+
}
48+
49+
// createSessionPath 创建Session目录路径
50+
func (f *dirFormatter) createSessionPath(underPath, aiModel, repo string, prNumber int, suffix string) string {
51+
dirName := f.generateSessionDirNameWithSuffix(aiModel, repo, prNumber, suffix)
52+
return fmt.Sprintf("%s/%s", underPath, dirName)
53+
}
54+
55+
// createSessionPathWithTimestamp 创建Session目录路径(使用时间戳)
56+
func (f *dirFormatter) createSessionPathWithTimestamp(underPath, aiModel, repo string, prNumber int, timestamp int64) string {
57+
dirName := f.generateSessionDirName(aiModel, repo, prNumber, timestamp)
58+
return fmt.Sprintf("%s/%s", underPath, dirName)
59+
}
60+
61+
// IssueDirFormat Issue目录格式
62+
type IssueDirFormat struct {
63+
AIModel string
64+
Repo string
65+
IssueNumber int
66+
Timestamp int64
67+
}
68+
69+
// PRDirFormat PR目录格式
70+
type PRDirFormat struct {
71+
AIModel string
72+
Repo string
73+
PRNumber int
74+
Timestamp int64
75+
}
76+
77+
// SessionDirFormat Session目录格式
78+
type SessionDirFormat struct {
79+
AIModel string
80+
Repo string
81+
PRNumber int
82+
Timestamp int64
83+
}
84+
85+
// parsePRDirName 解析PR目录名
86+
func (f *dirFormatter) parsePRDirName(dirName string) (*PRDirFormat, error) {
87+
parts := strings.Split(dirName, "-")
88+
if len(parts) < 5 {
89+
return nil, fmt.Errorf("invalid PR directory format: %s", dirName)
90+
}
91+
92+
// 格式: {aiModel}-{repo}-pr-{prNumber}-{timestamp}
93+
// 找到 "pr" 的位置
94+
prIndex := -1
95+
for i, part := range parts {
96+
if part == "pr" {
97+
prIndex = i
98+
break
99+
}
100+
}
101+
102+
if prIndex == -1 || prIndex < 2 || prIndex >= len(parts)-2 {
103+
return nil, fmt.Errorf("invalid PR directory format: %s", dirName)
104+
}
105+
106+
// 提取AI模型和仓库名
107+
aiModel := strings.Join(parts[:prIndex-1], "-")
108+
repo := parts[prIndex-1]
109+
110+
// 提取PR编号
111+
prNumber, err := strconv.Atoi(parts[prIndex+1])
112+
if err != nil {
113+
return nil, fmt.Errorf("invalid PR number: %s", parts[prIndex+1])
114+
}
115+
116+
// 提取时间戳
117+
timestamp, err := strconv.ParseInt(parts[prIndex+2], 10, 64)
118+
if err != nil {
119+
return nil, fmt.Errorf("invalid timestamp: %s", parts[prIndex+2])
120+
}
121+
122+
return &PRDirFormat{
123+
AIModel: aiModel,
124+
Repo: repo,
125+
PRNumber: prNumber,
126+
Timestamp: timestamp,
127+
}, nil
128+
}

0 commit comments

Comments
 (0)