主流程

This commit is contained in:
2026-06-10 17:53:09 +08:00
parent c6f239f424
commit b20435c027
66 changed files with 2779 additions and 1181 deletions
+2 -2
View File
@@ -89,10 +89,10 @@ func (f *fakeAgentKindRepo) InsertSession(context.Context, *acp.Session) (*acp.S
func (f *fakeAgentKindRepo) GetSessionByID(context.Context, uuid.UUID) (*acp.Session, error) {
panic("n/a")
}
func (f *fakeAgentKindRepo) ListSessionsByUser(context.Context, uuid.UUID) ([]*acp.Session, error) {
func (f *fakeAgentKindRepo) ListSessionsByUser(context.Context, uuid.UUID, *uuid.UUID) ([]*acp.Session, error) {
panic("n/a")
}
func (f *fakeAgentKindRepo) ListAllSessions(context.Context) ([]*acp.Session, error) {
func (f *fakeAgentKindRepo) ListAllSessions(context.Context, *uuid.UUID) ([]*acp.Session, error) {
panic("n/a")
}
func (f *fakeAgentKindRepo) UpdateSessionRunning(context.Context, uuid.UUID, string, int32) error {
+8 -1
View File
@@ -120,10 +120,17 @@ type AgentKindService interface {
type SessionService interface {
Create(ctx context.Context, c Caller, in CreateSessionInput) (*Session, error)
Get(ctx context.Context, c Caller, id uuid.UUID) (*Session, error)
List(ctx context.Context, c Caller, all bool) ([]*Session, error)
List(ctx context.Context, c Caller, f SessionListFilter) ([]*Session, error)
Terminate(ctx context.Context, c Caller, id uuid.UUID) error
}
// SessionListFilter 控制 sessions 列表的过滤维度。
// All 仅 admin 可用(查看全部用户);RequirementID nil 表示不按需求过滤。
type SessionListFilter struct {
All bool
RequirementID *uuid.UUID
}
// CreateSessionInput 是创建 session 的入参。
// branch / issue_number / requirement_number 三选一或全空(spec §8.1)。
type CreateSessionInput struct {
+10 -2
View File
@@ -300,8 +300,16 @@ func (h *Handler) listSessions(w http.ResponseWriter, r *http.Request) {
writeErr(w, r, err)
return
}
all := r.URL.Query().Get("all") == "true"
list, err := h.sessSvc.List(r.Context(), c, all)
f := SessionListFilter{All: r.URL.Query().Get("all") == "true"}
if v := r.URL.Query().Get("requirement_id"); v != "" {
id, err := uuid.Parse(v)
if err != nil {
writeErr(w, r, errs.New(errs.CodeInvalidInput, "requirement_id 非法"))
return
}
f.RequirementID = &id
}
list, err := h.sessSvc.List(r.Context(), c, f)
if err != nil {
writeErr(w, r, err)
return
+2
View File
@@ -23,6 +23,7 @@ SELECT id, workspace_id, project_id, agent_kind_id, user_id,
pid, exit_code, last_error, started_at, ended_at
FROM acp_sessions
WHERE user_id = $1
AND ($2::uuid IS NULL OR requirement_id = $2)
ORDER BY started_at DESC;
-- name: ListAllSessions :many
@@ -31,6 +32,7 @@ SELECT id, workspace_id, project_id, agent_kind_id, user_id,
branch, cwd_path, is_main_worktree, status,
pid, exit_code, last_error, started_at, ended_at
FROM acp_sessions
WHERE ($1::uuid IS NULL OR requirement_id = $1)
ORDER BY started_at DESC;
-- name: UpdateSessionRunning :exec
+2 -2
View File
@@ -81,10 +81,10 @@ func (f *fakeEventRepo) InsertSession(context.Context, *acp.Session) (*acp.Sessi
func (f *fakeEventRepo) GetSessionByID(context.Context, uuid.UUID) (*acp.Session, error) {
panic("n/a")
}
func (f *fakeEventRepo) ListSessionsByUser(context.Context, uuid.UUID) ([]*acp.Session, error) {
func (f *fakeEventRepo) ListSessionsByUser(context.Context, uuid.UUID, *uuid.UUID) ([]*acp.Session, error) {
panic("n/a")
}
func (f *fakeEventRepo) ListAllSessions(context.Context) ([]*acp.Session, error) {
func (f *fakeEventRepo) ListAllSessions(context.Context, *uuid.UUID) ([]*acp.Session, error) {
panic("n/a")
}
func (f *fakeEventRepo) UpdateSessionRunning(context.Context, uuid.UUID, string, int32) error {
+10 -6
View File
@@ -31,8 +31,9 @@ type Repository interface {
// Session
InsertSession(ctx context.Context, s *Session) (*Session, error)
GetSessionByID(ctx context.Context, id uuid.UUID) (*Session, error)
ListSessionsByUser(ctx context.Context, userID uuid.UUID) ([]*Session, error)
ListAllSessions(ctx context.Context) ([]*Session, error)
// requirementID 非 nil 时仅返回关联该需求的 sessions。
ListSessionsByUser(ctx context.Context, userID uuid.UUID, requirementID *uuid.UUID) ([]*Session, error)
ListAllSessions(ctx context.Context, requirementID *uuid.UUID) ([]*Session, error)
UpdateSessionRunning(ctx context.Context, id uuid.UUID, agentSessionID string, pid int32) error
UpdateSessionFinished(ctx context.Context, id uuid.UUID, status SessionStatus, exitCode *int32, lastErr *string) error
CountActiveSessions(ctx context.Context) (int64, error)
@@ -288,8 +289,11 @@ func (r *pgRepo) GetSessionByID(ctx context.Context, id uuid.UUID) (*Session, er
return rowToSession(row), nil
}
func (r *pgRepo) ListSessionsByUser(ctx context.Context, userID uuid.UUID) ([]*Session, error) {
rows, err := r.q.ListSessionsByUser(ctx, toPgUUID(userID))
func (r *pgRepo) ListSessionsByUser(ctx context.Context, userID uuid.UUID, requirementID *uuid.UUID) ([]*Session, error) {
rows, err := r.q.ListSessionsByUser(ctx, acpsqlc.ListSessionsByUserParams{
UserID: toPgUUID(userID),
Column2: toPgUUIDPtr(requirementID),
})
if err != nil {
return nil, errs.Wrap(err, errs.CodeInternal, "list sessions by user")
}
@@ -300,8 +304,8 @@ func (r *pgRepo) ListSessionsByUser(ctx context.Context, userID uuid.UUID) ([]*S
return out, nil
}
func (r *pgRepo) ListAllSessions(ctx context.Context) ([]*Session, error) {
rows, err := r.q.ListAllSessions(ctx)
func (r *pgRepo) ListAllSessions(ctx context.Context, requirementID *uuid.UUID) ([]*Session, error) {
rows, err := r.q.ListAllSessions(ctx, toPgUUIDPtr(requirementID))
if err != nil {
return nil, errs.Wrap(err, errs.CodeInternal, "list all sessions")
}
+5 -5
View File
@@ -335,14 +335,14 @@ func (s *sessionService) Get(ctx context.Context, c Caller, id uuid.UUID) (*Sess
return sess, nil
}
func (s *sessionService) List(ctx context.Context, c Caller, all bool) ([]*Session, error) {
if all && !c.IsAdmin {
func (s *sessionService) List(ctx context.Context, c Caller, f SessionListFilter) ([]*Session, error) {
if f.All && !c.IsAdmin {
return nil, errs.New(errs.CodeForbidden, "admin only")
}
if all {
return s.repo.ListAllSessions(ctx)
if f.All {
return s.repo.ListAllSessions(ctx, f.RequirementID)
}
return s.repo.ListSessionsByUser(ctx, c.UserID)
return s.repo.ListSessionsByUser(ctx, c.UserID, f.RequirementID)
}
func (s *sessionService) Terminate(ctx context.Context, c Caller, id uuid.UUID) error {
+58 -3
View File
@@ -212,10 +212,33 @@ func (r *fakeAcpRepo) DeleteAgentKind(_ context.Context, _ uuid.UUID) error { re
func (r *fakeAcpRepo) CountAgentKindUsage(_ context.Context, _ uuid.UUID) (int64, error) {
return 0, nil
}
func (r *fakeAcpRepo) ListSessionsByUser(_ context.Context, _ uuid.UUID) ([]*acp.Session, error) {
return nil, nil
func (r *fakeAcpRepo) ListSessionsByUser(_ context.Context, userID uuid.UUID, reqID *uuid.UUID) ([]*acp.Session, error) {
r.mu.Lock()
defer r.mu.Unlock()
var out []*acp.Session
for _, s := range r.sessions {
if s.UserID != userID {
continue
}
if reqID != nil && (s.RequirementID == nil || *s.RequirementID != *reqID) {
continue
}
out = append(out, s)
}
return out, nil
}
func (r *fakeAcpRepo) ListAllSessions(_ context.Context, reqID *uuid.UUID) ([]*acp.Session, error) {
r.mu.Lock()
defer r.mu.Unlock()
var out []*acp.Session
for _, s := range r.sessions {
if reqID != nil && (s.RequirementID == nil || *s.RequirementID != *reqID) {
continue
}
out = append(out, s)
}
return out, nil
}
func (r *fakeAcpRepo) ListAllSessions(_ context.Context) ([]*acp.Session, error) { return nil, nil }
func (r *fakeAcpRepo) UpdateSessionRunning(_ context.Context, _ uuid.UUID, _ string, _ int32) error {
return nil
}
@@ -440,3 +463,35 @@ func (f *fakeAcpRepo) ExpirePendingPermissionRequestsBySession(context.Context,
func (f *fakeAcpRepo) ExpireStalePermissionRequests(context.Context, time.Time) (int64, error) {
return 0, nil
}
func TestSessionService_List_RequirementFilter(t *testing.T) {
t.Parallel()
uid := uuid.New()
reqID := uuid.New()
repo := &fakeAcpRepo{}
repo.sessions = []*acp.Session{
{ID: uuid.New(), UserID: uid, RequirementID: &reqID},
{ID: uuid.New(), UserID: uid},
{ID: uuid.New(), UserID: uuid.New(), RequirementID: &reqID},
}
svc := acp.NewSessionService(
repo, nil, nil, nil, nil, nil, nil,
acp.SessionServiceConfig{}, nil,
)
// 普通用户:仅本人 + requirement 过滤
list, err := svc.List(context.Background(), acp.Caller{UserID: uid}, acp.SessionListFilter{RequirementID: &reqID})
require.NoError(t, err)
require.Len(t, list, 1)
// 非 admin 请求 all → 403
_, err = svc.List(context.Background(), acp.Caller{UserID: uid}, acp.SessionListFilter{All: true})
require.Error(t, err)
// admin all + requirement 过滤:跨用户 2 条
list, err = svc.List(context.Background(), acp.Caller{UserID: uid, IsAdmin: true}, acp.SessionListFilter{All: true, RequirementID: &reqID})
require.NoError(t, err)
require.Len(t, list, 2)
}
+13
View File
@@ -95,6 +95,7 @@ type Conversation struct {
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
RequirementID pgtype.UUID `json:"requirement_id"`
}
type GitCredential struct {
@@ -276,6 +277,18 @@ type Requirement struct {
WorkspaceID pgtype.UUID `json:"workspace_id"`
}
type RequirementArtifact struct {
ID pgtype.UUID `json:"id"`
RequirementID pgtype.UUID `json:"requirement_id"`
Phase string `json:"phase"`
Version int32 `json:"version"`
Content string `json:"content"`
Note string `json:"note"`
SourceMessageID *int64 `json:"source_message_id"`
CreatedBy pgtype.UUID `json:"created_by"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
type User struct {
ID pgtype.UUID `json:"id"`
Email string `json:"email"`
+11 -4
View File
@@ -154,11 +154,12 @@ SELECT id, workspace_id, project_id, agent_kind_id, user_id,
branch, cwd_path, is_main_worktree, status,
pid, exit_code, last_error, started_at, ended_at
FROM acp_sessions
WHERE ($1::uuid IS NULL OR requirement_id = $1)
ORDER BY started_at DESC
`
func (q *Queries) ListAllSessions(ctx context.Context) ([]AcpSession, error) {
rows, err := q.db.Query(ctx, listAllSessions)
func (q *Queries) ListAllSessions(ctx context.Context, dollar_1 pgtype.UUID) ([]AcpSession, error) {
rows, err := q.db.Query(ctx, listAllSessions, dollar_1)
if err != nil {
return nil, err
}
@@ -202,11 +203,17 @@ SELECT id, workspace_id, project_id, agent_kind_id, user_id,
pid, exit_code, last_error, started_at, ended_at
FROM acp_sessions
WHERE user_id = $1
AND ($2::uuid IS NULL OR requirement_id = $2)
ORDER BY started_at DESC
`
func (q *Queries) ListSessionsByUser(ctx context.Context, userID pgtype.UUID) ([]AcpSession, error) {
rows, err := q.db.Query(ctx, listSessionsByUser, userID)
type ListSessionsByUserParams struct {
UserID pgtype.UUID `json:"user_id"`
Column2 pgtype.UUID `json:"column_2"`
}
func (q *Queries) ListSessionsByUser(ctx context.Context, arg ListSessionsByUserParams) ([]AcpSession, error) {
rows, err := q.db.Query(ctx, listSessionsByUser, arg.UserID, arg.Column2)
if err != nil {
return nil, err
}