Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

364 lines
11 KiB
Go

package workspaceissues
import (
"context"
"strings"
)
func (s Service) ListTasks(ctx context.Context, filter TaskListFilter) (TaskList, error) {
store, err := s.store()
if err != nil {
return TaskList{}, err
}
filter.WorkspaceID = strings.TrimSpace(filter.WorkspaceID)
filter.IssueID = strings.TrimSpace(filter.IssueID)
if filter.WorkspaceID == "" || filter.IssueID == "" {
return TaskList{}, ErrInvalidArgument
}
if filter.StatusFilter != "" {
if _, ok := NormalizeStatus(string(filter.StatusFilter)); !ok {
return TaskList{}, ErrInvalidArgument
}
}
filter.SearchQuery = strings.TrimSpace(filter.SearchQuery)
if _, err := store.GetIssue(ctx, filter.WorkspaceID, filter.IssueID); err != nil {
return TaskList{}, err
}
list, err := store.ListTasks(ctx, filter)
if err != nil {
return TaskList{}, err
}
list.NextPageToken = EncodeTaskListCursorToken(list.NextCursor)
return list, nil
}
func (s Service) CreateTask(ctx context.Context, input CreateTaskInput) (Task, error) {
tasks, err := s.CreateTasks(ctx, CreateTasksInput{
IssueID: input.IssueID,
WorkspaceID: input.WorkspaceID,
ActorUserID: input.ActorUserID,
Tasks: []CreateTaskItemInput{{
TaskID: input.TaskID,
Title: input.Title,
Content: input.Content,
Priority: input.Priority,
DueAtUnixMS: input.DueAtUnixMS,
AgentTargetID: input.AgentTargetID,
ModelPlanID: input.ModelPlanID,
Model: input.Model,
PermissionModeID: input.PermissionModeID,
ReasoningEffort: input.ReasoningEffort,
ExecutionDirectory: input.ExecutionDirectory,
DependencyTaskIDs: input.DependencyTaskIDs,
Parallelizable: input.Parallelizable,
AutoAccept: input.AutoAccept,
}},
})
if err != nil {
return Task{}, err
}
if len(tasks) != 1 {
return Task{}, ErrInvalidArgument
}
return tasks[0], nil
}
func (s Service) CreateTasks(ctx context.Context, input CreateTasksInput) ([]Task, error) {
store, err := s.store()
if err != nil {
return nil, err
}
workspaceID := strings.TrimSpace(input.WorkspaceID)
issueID := strings.TrimSpace(input.IssueID)
actorUserID := strings.TrimSpace(input.ActorUserID)
if workspaceID == "" || issueID == "" || actorUserID == "" || len(input.Tasks) == 0 {
return nil, ErrInvalidArgument
}
issue, err := store.GetIssue(ctx, workspaceID, issueID)
if err != nil {
return nil, err
}
if err := RejectManagedIssueMutation(issue); err != nil {
return nil, err
}
tasks, err := s.buildTasks(issue, actorUserID, input.Tasks)
if err != nil {
return nil, err
}
existing, err := store.ListTasks(ctx, TaskListFilter{WorkspaceID: workspaceID, IssueID: issueID, ReturnAll: true})
if err != nil {
return nil, err
}
graph := append(append([]Task(nil), existing.Items...), tasks...)
if !ValidateTaskDependencyGraph(graph) {
return nil, ErrInvalidArgument
}
created, err := store.AppendTasks(ctx, tasks)
if err != nil {
return nil, err
}
projected, err := store.RecalculateIssueProjection(ctx, workspaceID, issueID)
if err != nil {
return nil, err
}
if projected.Budget.Mode == BudgetModeAuto {
projected.Budget.TokenLimit = CompileAutoTokenBudget(projected.TaskCount, projected.ExecutionProfile)
if _, err := store.UpdateIssue(ctx, projected); err != nil {
return nil, err
}
}
if err := store.TouchTopicActivity(ctx, workspaceID, issue.TopicID, s.nowUnixMS()); err != nil {
return nil, err
}
return created, nil
}
func (s Service) buildTasks(issue Issue, actorUserID string, items []CreateTaskItemInput) ([]Task, error) {
now := s.nowUnixMS()
tasks := make([]Task, 0, len(items))
for _, item := range items {
title := strings.TrimSpace(item.Title)
if title == "" {
return nil, ErrInvalidArgument
}
agentTargetID := strings.TrimSpace(item.AgentTargetID)
modelPlanID := strings.TrimSpace(item.ModelPlanID)
model := strings.TrimSpace(item.Model)
if agentTargetID == "" && (modelPlanID != "" || model != "") {
return nil, ErrInvalidArgument
}
task := Task{
TaskID: s.resolveID(IDKindTask, item.TaskID),
IssueID: issue.IssueID,
WorkspaceID: issue.WorkspaceID,
Title: title,
Content: strings.TrimSpace(item.Content),
SearchText: TrimSearchText(item.Content),
Status: StatusNotStarted,
Priority: NormalizePriority(item.Priority),
DueAtUnixMS: maxInt64(item.DueAtUnixMS, 0),
AgentTargetID: agentTargetID,
ModelPlanID: modelPlanID,
Model: model,
PermissionModeID: strings.TrimSpace(item.PermissionModeID),
ReasoningEffort: strings.TrimSpace(item.ReasoningEffort),
ExecutionDirectory: strings.TrimSpace(item.ExecutionDirectory),
DependencyTaskIDs: NormalizeDependencyTaskIDs(item.DependencyTaskIDs),
Parallelizable: item.Parallelizable,
AutoAccept: item.AutoAccept,
AcceptanceState: AcceptanceAgentClaimed,
CreatorUserID: actorUserID,
CreatedAtUnixMS: now,
UpdatedAtUnixMS: now,
}
if task.TaskID == "" {
return nil, ErrInvalidArgument
}
tasks = append(tasks, task)
}
return tasks, nil
}
func (s Service) UpdateTask(ctx context.Context, input UpdateTaskInput) (Task, error) {
store, err := s.store()
if err != nil {
return Task{}, err
}
workspaceID := strings.TrimSpace(input.WorkspaceID)
issueID := strings.TrimSpace(input.IssueID)
taskID := strings.TrimSpace(input.TaskID)
if workspaceID == "" || issueID == "" || taskID == "" || strings.TrimSpace(input.ActorUserID) == "" {
return Task{}, ErrInvalidArgument
}
issue, err := store.GetIssue(ctx, workspaceID, issueID)
if err != nil {
return Task{}, err
}
if err := RejectManagedIssueMutation(issue); err != nil {
return Task{}, err
}
task, err := store.GetTask(ctx, workspaceID, issueID, taskID)
if err != nil {
return Task{}, err
}
if input.HasTitle {
title := strings.TrimSpace(input.Title)
if title == "" {
return Task{}, ErrInvalidArgument
}
task.Title = title
}
if input.HasContent {
task.Content = strings.TrimSpace(input.Content)
task.SearchText = TrimSearchText(input.Content)
}
if input.HasStatus {
status, ok := NormalizeStatus(input.Status)
if !ok {
return Task{}, ErrInvalidArgument
}
task.Status = status
}
if input.HasPriority {
task.Priority = NormalizePriority(input.Priority)
}
if input.HasDueAt {
task.DueAtUnixMS = input.DueAtUnixMS
}
if input.HasSortIndex {
if input.SortIndex < 0 {
return Task{}, ErrInvalidArgument
}
task.SortIndex = input.SortIndex
}
if input.HasAgentTargetID {
task.AgentTargetID = strings.TrimSpace(input.AgentTargetID)
}
if input.HasModelPlanID {
task.ModelPlanID = strings.TrimSpace(input.ModelPlanID)
}
if input.HasModel {
task.Model = strings.TrimSpace(input.Model)
}
if task.AgentTargetID == "" && (task.ModelPlanID != "" || task.Model != "") {
return Task{}, ErrInvalidArgument
}
if input.HasExecutionDirectory {
task.ExecutionDirectory = strings.TrimSpace(input.ExecutionDirectory)
}
if input.HasDependencyTaskIDs {
task.DependencyTaskIDs = NormalizeDependencyTaskIDs(input.DependencyTaskIDs)
}
if input.HasParallelizable {
task.Parallelizable = input.Parallelizable
}
if input.HasAutoAccept {
task.AutoAccept = input.AutoAccept
}
if task.AcceptanceState == "" {
if task.Status == StatusCompleted || task.Status == StatusPendingAcceptance {
task.AcceptanceState = AcceptanceAutoChecked
} else {
task.AcceptanceState = AcceptanceAgentClaimed
}
}
if input.HasAcceptanceState {
next, ok := NormalizeAcceptanceState(input.AcceptanceState)
if !ok || !CanTransitionAcceptance(task.AcceptanceState, next) {
return Task{}, ErrInvalidArgument
}
task.AcceptanceState = next
}
if input.HasAcceptanceSummary {
task.AcceptanceSummary = strings.TrimSpace(input.AcceptanceSummary)
}
if input.HasStatus {
switch task.Status {
case StatusCompleted:
if !input.HasAcceptanceState && CanTransitionAcceptance(task.AcceptanceState, AcceptanceUserAccepted) {
task.AcceptanceState = AcceptanceUserAccepted
}
if task.AcceptanceState != AcceptanceUserAccepted {
return Task{}, ErrInvalidArgument
}
case StatusNotStarted:
task.AcceptanceState = AcceptanceAgentClaimed
task.AcceptanceSummary = ""
case StatusPendingAcceptance:
// An accepted task cannot be pushed back to pending acceptance:
// that write would keep user_accepted on a non-completed task and
// wedge the dispatch frontier. Rework goes through not_started.
if task.AcceptanceState == AcceptanceUserAccepted {
return Task{}, ErrInvalidArgument
}
}
}
task.UpdatedAtUnixMS = s.nowUnixMS()
updated, err := store.UpdateTask(ctx, task)
if err != nil {
return Task{}, err
}
if _, err := store.RecalculateIssueProjection(ctx, workspaceID, issueID); err != nil {
return Task{}, err
}
if err := store.TouchTopicActivity(ctx, workspaceID, issue.TopicID, task.UpdatedAtUnixMS); err != nil {
return Task{}, err
}
return updated, nil
}
func (s Service) GetTaskDetail(ctx context.Context, workspaceID string, issueID string, taskID string) (TaskDetail, error) {
store, err := s.store()
if err != nil {
return TaskDetail{}, err
}
workspaceID = strings.TrimSpace(workspaceID)
issueID = strings.TrimSpace(issueID)
taskID = strings.TrimSpace(taskID)
if workspaceID == "" || issueID == "" || taskID == "" {
return TaskDetail{}, ErrInvalidArgument
}
task, err := store.GetTask(ctx, workspaceID, issueID, taskID)
if err != nil {
return TaskDetail{}, err
}
refs, err := store.ListContextRefs(ctx, workspaceID, issueID, taskID, ContextRefParentTask)
if err != nil {
return TaskDetail{}, err
}
runs, err := store.ListRuns(ctx, workspaceID, issueID, taskID)
if err != nil {
return TaskDetail{}, err
}
var latestRun *Run
if len(runs) > 0 {
latestRun = &runs[0]
}
outputs, err := store.ListLatestRunOutputs(ctx, workspaceID, issueID, taskID)
if err != nil {
return TaskDetail{}, err
}
return TaskDetail{
Task: task,
ContextRefs: refs,
LatestRun: latestRun,
RecentRuns: runs,
LatestOutputs: outputs,
}, nil
}
func (s Service) DeleteTask(ctx context.Context, workspaceID string, issueID string, taskID string, actorUserID string) (bool, error) {
store, err := s.store()
if err != nil {
return false, err
}
workspaceID = strings.TrimSpace(workspaceID)
issueID = strings.TrimSpace(issueID)
taskID = strings.TrimSpace(taskID)
actorUserID = strings.TrimSpace(actorUserID)
if workspaceID == "" || issueID == "" || taskID == "" || actorUserID == "" {
return false, ErrInvalidArgument
}
issue, err := store.GetIssue(ctx, workspaceID, issueID)
if err != nil {
return false, err
}
if err := RejectManagedIssueMutation(issue); err != nil {
return false, err
}
removed, err := store.DeleteTask(ctx, workspaceID, issueID, taskID, actorUserID)
if err != nil {
return false, err
}
if !removed {
return false, ErrTaskNotFound
}
if _, err := store.RecalculateIssueProjection(ctx, workspaceID, issueID); err != nil {
return false, err
}
if err := store.TouchTopicActivity(ctx, workspaceID, issue.TopicID, s.nowUnixMS()); err != nil {
return false, err
}
return true, nil
}