Skip to content

Commit 54861b9

Browse files
authored
refactor(codeagent): reorganize the directory structure of workspace (#93)
* refactor(codeagent): reorganize the directory structure of workspace * Initial plan for Issue #11: 测试任务1 * Initial plan for Issue #11: 测试任务1 * Initial plan for Issue #11: 测试任务1 * refactor(codeagent): reorganize the directory structure of workspace
1 parent 6446817 commit 54861b9

File tree

12 files changed

+1273
-398
lines changed

12 files changed

+1273
-398
lines changed

cmd/server/config.yaml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,18 @@ github:
77
webhook_url: "http://localhost:8888/hook"
88

99
workspace:
10-
base_dir: "/tmp/codeagent"
10+
base_dir: "/codeagent"
1111
cleanup_after: "24h"
1212

1313
claude:
1414
# api_key: 通过 --claude-api-key 参数或 CLAUDE_API_KEY 环境变量设置
15-
container_image: "anthropic/claude-code:latest"
15+
container_image: "goplusorg/codeagent:v0.4"
1616
timeout: "30m"
1717

1818
docker:
1919
socket: "unix:///var/run/docker.sock"
2020
network: "bridge"
21+
22+
gemini:
23+
container_image: "goplusorg/codeagent:v0.4"
24+
timeout: "30m"

docs/codeagent-v0.4.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# CodeAgent v0.4 - 基于 Git Worktree 的工作空间管理设计
2+
3+
## 概述
4+
5+
CodeAgent v0.4 采用极简的工作空间管理方案,完全基于 Git worktree 机制。所有工作空间仅通过目录名唯一标识,无需任何额外的映射文件或持久化元数据,系统可在重启后自动恢复所有状态。
6+
7+
## 设计要点
8+
9+
1. **目录唯一性**:每个工作空间目录名唯一,包含关键信息(如 repo、issue/pr 编号、时间戳)。
10+
2. **无映射/无额外元数据**:所有状态仅靠目录名表达,无需任何映射文件或数据库。
11+
3. **极简恢复**:系统启动时只需扫描目录名即可恢复全部工作空间。
12+
4. **目录隔离**:所有 worktree 目录与仓库目录同级,仓库内部结构始终整洁。
13+
14+
## 工作空间生命周期
15+
16+
### 1. Issue 工作空间
17+
18+
- 创建时目录名格式:`{repo}-issue-{issue-number}-{timestamp}`
19+
- 例如:`codeagent-issue-123-1703123456789`
20+
21+
### 2. PR 工作空间
22+
23+
- PR 创建后,目录名格式:`{repo}-pr-{pr-number}-{timestamp}`
24+
- 例如:`codeagent-pr-91-1703123456789`
25+
26+
Session 目录统一为:`{repo}-session-{pr-number}-{timestamp}`
27+
28+
### 3. 目录结构示例
29+
30+
```
31+
basedir/
32+
├── qbox/
33+
│ ├── codeagent/ # 仓库目录
34+
│ │ ├── .git/ # 共享的 Git 仓库
35+
│ ├── codeagent-issue-124-1703123456790/ # Issue 工作空间
36+
│ ├── codeagent-pr-91-1703123456789/ # PR 工作空间
37+
│ ├── codeagent-session-issue-124-1703123456790/ # Issue session 目录
38+
│ ├── codeagent-session-pr-91-1703123456789/ # PR session 目录
39+
```
40+
41+
## 恢复与清理机制
42+
43+
- **恢复**:系统启动时递归扫描所有组织/仓库目录下的 worktree 目录(`{repo}-issue-*``{repo}-pr-*`)和 session 目录(`{repo}-session-issue-*``{repo}-session-pr-*`),通过目录名解析出 issue/pr 编号、时间戳,自动恢复所有工作空间。
44+
- **清理**:只需根据目录名和业务逻辑判断是否过期,直接删除对应 worktree 目录和 session 目录。
45+
46+
## 主要优势
47+
48+
- **极简**:无任何多余元数据,目录即状态。
49+
- **健壮**:即使异常重启,目录结构不变,所有工作空间都能恢复。
50+
- **高性能**:充分利用 Git worktree 的原生能力,无需重复 clone。
51+
- **易维护**:目录结构清晰,便于人工排查和自动化脚本处理。
52+
53+
## 总结
54+
55+
CodeAgent v0.4 的工作空间管理方案,彻底抛弃了映射、move、数据库等复杂机制,完全依赖 Git worktree 和目录名唯一性,实现了极致简洁、健壮、可恢复的多工作空间管理。

internal/agent/agent.go

Lines changed: 68 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package agent
33
import (
44
"fmt"
55
"io"
6+
"path/filepath"
67
"strings"
78
"time"
89

@@ -39,119 +40,56 @@ func New(cfg *config.Config, workspaceManager *workspace.Manager) *Agent {
3940
}
4041
}
4142

42-
// ProcessIssue 处理 Issue 事件,生成代码(保留向后兼容)
43-
func (a *Agent) ProcessIssue(issue *github.Issue) error {
44-
// 1. 准备临时工作空间
45-
ws := a.workspace.Prepare(issue)
46-
if ws.ID == "" {
47-
return fmt.Errorf("failed to prepare workspace")
43+
// ProcessIssueComment 处理 Issue 评论事件,包含完整的仓库信息
44+
func (a *Agent) ProcessIssueComment(event *github.IssueCommentEvent) error {
45+
// 1. 创建 Issue 工作空间
46+
ws := a.workspace.CreateWorkspaceFromIssue(event.Issue)
47+
if ws == nil {
48+
return fmt.Errorf("failed to create workspace from issue")
4849
}
4950

5051
// 2. 创建分支并推送
51-
if err := a.github.CreateBranch(&ws); err != nil {
52+
if err := a.github.CreateBranch(ws); err != nil {
5253
log.Errorf("Failed to create branch: %v", err)
5354
return err
5455
}
5556

5657
// 3. 创建初始 PR
57-
pr, err := a.github.CreatePullRequest(&ws)
58+
pr, err := a.github.CreatePullRequest(ws)
5859
if err != nil {
5960
log.Errorf("Failed to create PR: %v", err)
6061
return err
6162
}
6263

63-
// 4. 初始化 code client
64-
code, err := a.sessionManager.GetSession(&ws)
65-
if err != nil {
66-
log.Errorf("failed to get code client: %v", err)
67-
return err
64+
// 4. 移动工作空间从 Issue 到 PR
65+
if err := a.workspace.MoveIssueToPR(ws, pr.GetNumber()); err != nil {
66+
log.Errorf("Failed to move workspace: %v", err)
6867
}
68+
ws.PRNumber = pr.GetNumber()
6969

70-
// 5. 执行代码修改,使用更明确的 prompt
71-
codePrompt := fmt.Sprintf(`请根据以下 Issue 内容直接修改代码:
72-
73-
标题:%s
74-
描述:%s
75-
76-
要求:
77-
1. 仔细分析 Issue 需求,理解要解决的问题
78-
2. 查看现有代码结构,找到需要修改的文件
79-
3. 直接实现代码修改,确保功能完整
80-
4. 遵循项目的代码风格和最佳实践
81-
5. 添加必要的测试用例(如果需要)
82-
6. 确保代码能够正常运行
83-
84-
请开始分析和修改代码。`, issue.GetTitle(), issue.GetBody())
85-
codeResp, err := a.promptWithRetry(code, codePrompt, 3)
70+
// 5. 创建 session 目录
71+
suffix := strings.TrimPrefix(filepath.Base(ws.Path), fmt.Sprintf("%s-pr-%d-", ws.Repo, pr.GetNumber()))
72+
sessionPath, err := a.workspace.CreateSessionPath(filepath.Dir(ws.Path), ws.Repo, pr.GetNumber(), suffix)
8673
if err != nil {
87-
log.Errorf("failed to prompt for code modification: %v", err)
88-
return err
89-
}
90-
91-
codeOutput, err := io.ReadAll(codeResp.Out)
92-
if err != nil {
93-
log.Errorf("failed to read code modification output: %v", err)
94-
return err
95-
}
96-
97-
log.Infof("Code Modification Output: %s", string(codeOutput))
98-
99-
// 6. 更新 PR Body 为执行结果
100-
if err = a.github.UpdatePullRequest(pr, string(codeOutput)); err != nil {
101-
log.Errorf("failed to update PR body with execution result: %v", err)
102-
return err
103-
}
104-
105-
// 7. 评论到 PR
106-
commentBody := fmt.Sprintf("<details><summary>Code Modification Session</summary>%s</details>", string(codeOutput))
107-
if err = a.github.CreatePullRequestComment(pr, commentBody); err != nil {
108-
log.Errorf("failed to create PR comment for code modification: %v", err)
109-
return err
110-
}
111-
112-
// 8. 提交变更并推送到远程
113-
result := &models.ExecutionResult{
114-
Output: string(codeOutput),
115-
}
116-
if err = a.github.CommitAndPush(&ws, result, code); err != nil {
117-
log.Errorf("Failed to commit and push: %v", err)
74+
log.Errorf("Failed to create session directory: %v", err)
11875
return err
11976
}
77+
ws.SessionPath = sessionPath
12078

121-
log.Infof("Successfully processed Issue #%d, PR: %s", issue.GetNumber(), pr.GetHTMLURL())
122-
return nil
123-
}
79+
// 6. 注册工作空间到 PR 映射
80+
ws.PullRequest = pr
81+
a.workspace.RegisterWorkspace(ws, pr)
12482

125-
// ProcessIssueComment 处理 Issue 评论事件,包含完整的仓库信息
126-
func (a *Agent) ProcessIssueComment(event *github.IssueCommentEvent) error {
127-
// 1. 准备临时工作空间,传递完整事件
128-
ws := a.workspace.PrepareFromEvent(event)
129-
if ws.ID == "" {
130-
return fmt.Errorf("failed to prepare workspace")
131-
}
83+
log.Infof("process issue #%d, workspace: %s, session: %s", event.Issue.GetNumber(), ws.Path, ws.SessionPath)
13284

133-
// 2. 创建分支并推送
134-
if err := a.github.CreateBranch(&ws); err != nil {
135-
log.Errorf("Failed to create branch: %v", err)
136-
return err
137-
}
138-
139-
// 3. 创建初始 PR
140-
pr, err := a.github.CreatePullRequest(&ws)
141-
if err != nil {
142-
log.Errorf("Failed to create PR: %v", err)
143-
return err
144-
}
145-
a.workspace.RegisterWorkspace(&ws, pr)
146-
147-
// 4. 初始化 code client
148-
code, err := a.sessionManager.GetSession(&ws)
85+
// 7. 初始化 code client
86+
code, err := a.sessionManager.GetSession(ws)
14987
if err != nil {
15088
log.Errorf("failed to get code client: %v", err)
15189
return err
15290
}
15391

154-
// 5. 执行代码修改,规范 prompt,要求 AI 输出结构化摘要
92+
// 8. 执行代码修改,规范 prompt,要求 AI 输出结构化摘要
15593
codePrompt := fmt.Sprintf(`请根据以下 Issue 内容修改代码:
15694
15795
标题:%s
@@ -182,7 +120,7 @@ func (a *Agent) ProcessIssueComment(event *github.IssueCommentEvent) error {
182120

183121
log.Infof("LLM Output: %s", string(codeOutput))
184122

185-
// 6. 组织结构化 PR Body(解析三段式输出)
123+
// 9. 组织结构化 PR Body(解析三段式输出)
186124
aiStr := string(codeOutput)
187125

188126
// 解析三段式输出
@@ -219,11 +157,11 @@ func (a *Agent) ProcessIssueComment(event *github.IssueCommentEvent) error {
219157
return err
220158
}
221159

222-
// 8. 提交变更并推送到远程
160+
// 10. 提交变更并推送到远程
223161
result := &models.ExecutionResult{
224162
Output: string(codeOutput),
225163
}
226-
if err = a.github.CommitAndPush(&ws, result, code); err != nil {
164+
if err = a.github.CommitAndPush(ws, result, code); err != nil {
227165
log.Errorf("Failed to commit and push: %v", err)
228166
return err
229167
}
@@ -314,13 +252,19 @@ func (a *Agent) ContinuePRWithArgs(event *github.IssueCommentEvent, args string)
314252
HTMLURL: event.Issue.HTMLURL,
315253
}
316254

317-
// 2. 准备临时工作空间
318-
ws := a.workspace.Getworkspace(pr)
255+
// 2. 获取或创建 PR 工作空间
256+
ws := a.workspace.GetOrCreateWorkspaceForPR(pr)
319257
if ws == nil {
320-
return fmt.Errorf("failed to prepare workspace for PR continue")
258+
return fmt.Errorf("failed to get or create workspace for PR continue")
321259
}
322260

323-
// 3. 初始化 code client
261+
// 3. 拉取远端最新代码
262+
if err := a.github.PullLatestChanges(ws); err != nil {
263+
log.Errorf("Failed to pull latest changes: %v", err)
264+
// 不返回错误,继续执行,因为可能是网络问题
265+
}
266+
267+
// 4. 初始化 code client
324268
code, err := a.sessionManager.GetSession(ws)
325269
if err != nil {
326270
log.Errorf("failed to get code client for PR continue: %v", err)
@@ -390,13 +334,19 @@ func (a *Agent) FixPRWithArgs(event *github.IssueCommentEvent, args string) erro
390334
HTMLURL: event.Issue.HTMLURL,
391335
}
392336

393-
// 2. 准备临时工作空间
394-
ws := a.workspace.Getworkspace(pr)
337+
// 2. 获取或创建 PR 工作空间
338+
ws := a.workspace.GetOrCreateWorkspaceForPR(pr)
395339
if ws == nil {
396-
return fmt.Errorf("failed to prepare workspace for PR fix")
340+
return fmt.Errorf("failed to get or create workspace for PR fix")
341+
}
342+
343+
// 3. 拉取远端最新代码
344+
if err := a.github.PullLatestChanges(ws); err != nil {
345+
log.Errorf("Failed to pull latest changes: %v", err)
346+
// 不返回错误,继续执行,因为可能是网络问题
397347
}
398348

399-
// 3. 初始化 code client
349+
// 4. 初始化 code client
400350
code, err := a.sessionManager.GetSession(ws)
401351
if err != nil {
402352
log.Errorf("failed to get code client for PR fix: %v", err)
@@ -452,13 +402,19 @@ func (a *Agent) ContinuePRFromReviewComment(event *github.PullRequestReviewComme
452402
// 1. 从工作空间管理器获取 PR 信息
453403
pr := event.PullRequest
454404

455-
// 2. 准备临时工作空间
456-
ws := a.workspace.Getworkspace(pr)
405+
// 2. 获取或创建 PR 工作空间
406+
ws := a.workspace.GetOrCreateWorkspaceForPR(pr)
457407
if ws == nil {
458-
return fmt.Errorf("failed to prepare workspace for PR continue from review comment")
408+
return fmt.Errorf("failed to get or create workspace for PR continue from review comment")
409+
}
410+
411+
// 3. 拉取远端最新代码
412+
if err := a.github.PullLatestChanges(ws); err != nil {
413+
log.Errorf("Failed to pull latest changes: %v", err)
414+
// 不返回错误,继续执行,因为可能是网络问题
459415
}
460416

461-
// 3. 初始化 code client
417+
// 4. 初始化 code client
462418
code, err := a.sessionManager.GetSession(ws)
463419
if err != nil {
464420
log.Errorf("failed to get code client for PR continue from review comment: %v", err)
@@ -533,13 +489,19 @@ func (a *Agent) FixPRFromReviewComment(event *github.PullRequestReviewCommentEve
533489
// 1. 从工作空间管理器获取 PR 信息
534490
pr := event.PullRequest
535491

536-
// 2. 准备临时工作空间
537-
ws := a.workspace.Getworkspace(pr)
492+
// 2. 获取或创建 PR 工作空间
493+
ws := a.workspace.GetOrCreateWorkspaceForPR(pr)
538494
if ws == nil {
539-
return fmt.Errorf("failed to prepare workspace for PR fix from review comment")
495+
return fmt.Errorf("failed to get or create workspace for PR fix from review comment")
496+
}
497+
498+
// 3. 拉取远端最新代码
499+
if err := a.github.PullLatestChanges(ws); err != nil {
500+
log.Errorf("Failed to pull latest changes: %v", err)
501+
// 不返回错误,继续执行,因为可能是网络问题
540502
}
541503

542-
// 3. 初始化 code client
504+
// 4. 初始化 code client
543505
code, err := a.sessionManager.GetSession(ws)
544506
if err != nil {
545507
log.Errorf("failed to get code client for PR fix from review comment: %v", err)

internal/code/claude.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,21 @@ import (
1515

1616
// claudeCode Docker 实现
1717
type claudeCode struct {
18-
cmd *exec.Cmd
1918
containerName string
2019
}
2120

2221
func NewClaudeDocker(workspace *models.Workspace, cfg *config.Config) (Code, error) {
2322
// 解析仓库信息,只获取仓库名,不包含完整URL
2423
repoName := extractRepoName(workspace.Repository)
25-
containerName := fmt.Sprintf("claude-%s-%d", repoName, workspace.PullRequest.GetNumber())
24+
containerName := fmt.Sprintf("claude-%s-%d", repoName, workspace.PRNumber)
25+
26+
// 检查是否已经有对应的容器在运行
27+
if isContainerRunning(containerName) {
28+
log.Infof("Found existing container: %s, reusing it", containerName)
29+
return &claudeCode{
30+
containerName: containerName,
31+
}, nil
32+
}
2633

2734
// 确保路径存在
2835
workspacePath, _ := filepath.Abs(workspace.Path)
@@ -86,7 +93,6 @@ func NewClaudeDocker(workspace *models.Workspace, cfg *config.Config) (Code, err
8693
log.Infof("docker container started successfully")
8794

8895
return &claudeCode{
89-
cmd: cmd,
9096
containerName: containerName,
9197
}, nil
9298
}

0 commit comments

Comments
 (0)