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

246 lines
8.8 KiB
Go

package workspace
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"strings"
workspaceissues "github.com/tutti-os/tutti/packages/workspace/issues"
)
func (s *SQLiteStore) ListTasks(ctx context.Context, filter workspaceissues.TaskListFilter) (workspaceissues.TaskList, error) {
if err := s.ensureIssueDatabase(); err != nil {
return workspaceissues.TaskList{}, err
}
if _, err := s.GetIssue(ctx, filter.WorkspaceID, filter.IssueID); err != nil {
return workspaceissues.TaskList{}, err
}
where, args := taskListWhere(filter)
countWhere := strings.Join(where, " AND ")
countArgs := append([]any(nil), args...)
totalCount, err := s.countIssueRows(ctx, "workspace_issue_tasks", countWhere, countArgs)
if err != nil {
return workspaceissues.TaskList{}, err
}
statusCountWhere, statusCountArgs := taskListStatusCountWhere(filter)
counts, err := s.countIssueStatuses(
ctx,
"workspace_issue_tasks",
strings.Join(statusCountWhere, " AND "),
statusCountArgs,
)
if err != nil {
return workspaceissues.TaskList{}, err
}
if filter.Cursor != nil {
where = append(where, "(sort_index > ? OR (sort_index = ? AND id > ?))")
args = append(args, filter.Cursor.SortIndex, filter.Cursor.SortIndex, filter.Cursor.ID)
}
limit := issueListLimit(filter.PageSize, filter.ReturnAll)
query := fmt.Sprintf(`
SELECT %s
FROM workspace_issue_tasks
WHERE %s
ORDER BY sort_index ASC, id ASC%s
`, taskSelectColumns, strings.Join(where, " AND "), issueLimitClause(limit))
if limit > 0 {
args = append(args, limit)
}
rows, err := s.readDB.QueryContext(ctx, query, args...)
if err != nil {
return workspaceissues.TaskList{}, fmt.Errorf("list workspace issue tasks: %w", err)
}
defer rows.Close()
items, err := scanWorkspaceIssueTasks(rows)
if err != nil {
return workspaceissues.TaskList{}, err
}
var nextCursor *workspaceissues.TaskListCursor
pageSize := normalizedIssuePageSize(filter.PageSize)
if !filter.ReturnAll && len(items) > pageSize {
last := items[pageSize-1]
nextCursor = &workspaceissues.TaskListCursor{SortIndex: last.SortIndex, ID: last.ID}
items = items[:pageSize]
}
return workspaceissues.TaskList{
Items: items,
NextCursor: nextCursor,
TotalCount: totalCount,
StatusCounts: counts,
}, nil
}
func (s *SQLiteStore) AppendTasks(ctx context.Context, tasks []workspaceissues.Task) ([]workspaceissues.Task, error) {
if err := s.ensureIssueDatabase(); err != nil {
return nil, err
}
if len(tasks) == 0 {
return []workspaceissues.Task{}, nil
}
workspaceID := tasks[0].WorkspaceID
issueID := tasks[0].IssueID
tx, err := s.writeDB.BeginTx(ctx, nil)
if err != nil {
return nil, fmt.Errorf("begin append workspace issue tasks: %w", err)
}
defer func() {
_ = tx.Rollback()
}()
var exists int
if err := tx.QueryRowContext(ctx, `
SELECT 1
FROM workspace_issues
WHERE workspace_id = ? AND issue_id = ?
`, workspaceID, issueID).Scan(&exists); errors.Is(err, sql.ErrNoRows) {
return nil, workspaceissues.ErrIssueNotFound
} else if err != nil {
return nil, fmt.Errorf("get workspace issue for task append: %w", err)
}
var nextSortIndex int
if err := tx.QueryRowContext(ctx, `
SELECT COALESCE(MAX(sort_index), 0) + 1
FROM workspace_issue_tasks
WHERE workspace_id = ? AND issue_id = ?
`, workspaceID, issueID).Scan(&nextSortIndex); err != nil {
return nil, fmt.Errorf("get next workspace issue task sort index: %w", err)
}
created := make([]workspaceissues.Task, 0, len(tasks))
for index, task := range tasks {
if task.WorkspaceID != workspaceID || task.IssueID != issueID {
return nil, workspaceissues.ErrInvalidArgument
}
task.SortIndex = nextSortIndex + index
createdTask, err := insertWorkspaceIssueTask(ctx, tx, task)
if err != nil {
return nil, err
}
created = append(created, createdTask)
}
if err := tx.Commit(); err != nil {
return nil, fmt.Errorf("commit append workspace issue tasks: %w", err)
}
return created, nil
}
func insertWorkspaceIssueTask(ctx context.Context, execer workspaceIssueExecer, task workspaceissues.Task) (workspaceissues.Task, error) {
dependencyTaskIDsJSON, err := json.Marshal(task.DependencyTaskIDs)
if err != nil {
return workspaceissues.Task{}, fmt.Errorf("encode workspace issue task dependencies: %w", err)
}
result, err := execer.ExecContext(ctx, `
INSERT INTO workspace_issue_tasks (
task_id, issue_id, workspace_id, title, content, search_text, status,
priority, sort_index, due_at_unix_ms, agent_target_id, model_plan_id, model,
permission_mode_id, reasoning_effort,
execution_directory, dependency_task_ids_json, parallelizable, auto_accept,
acceptance_state, acceptance_summary, creator_user_id, creator_display_name,
creator_avatar_url, latest_run_id, superseded_at_unix_ms, superseded_by_task_id,
created_at_unix_ms, updated_at_unix_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, task.TaskID, task.IssueID, task.WorkspaceID, task.Title, task.Content, task.SearchText,
string(task.Status), string(task.Priority), task.SortIndex, task.DueAtUnixMS, task.AgentTargetID,
task.ModelPlanID, task.Model, task.PermissionModeID, task.ReasoningEffort,
task.ExecutionDirectory, string(dependencyTaskIDsJSON), task.Parallelizable, task.AutoAccept,
string(task.AcceptanceState), task.AcceptanceSummary, task.CreatorUserID,
task.CreatorDisplayName, task.CreatorAvatarURL, task.LatestRunID,
task.SupersededAtUnixMS, task.SupersededByTaskID,
task.CreatedAtUnixMS, task.UpdatedAtUnixMS)
if err != nil {
if isSQLiteUniqueConstraintError(err) {
return workspaceissues.Task{}, workspaceissues.ErrTaskAlreadyExists
}
return workspaceissues.Task{}, fmt.Errorf("append workspace issue task: %w", err)
}
id, err := result.LastInsertId()
if err != nil {
return workspaceissues.Task{}, fmt.Errorf("read appended workspace issue task id: %w", err)
}
task.ID = uint64(id)
return task, nil
}
func (s *SQLiteStore) GetTask(ctx context.Context, workspaceID string, issueID string, taskID string) (workspaceissues.Task, error) {
if err := s.ensureIssueDatabase(); err != nil {
return workspaceissues.Task{}, err
}
row := s.readDB.QueryRowContext(ctx, `
SELECT `+taskSelectColumns+`
FROM workspace_issue_tasks
WHERE workspace_id = ? AND issue_id = ? AND task_id = ?
`, workspaceID, issueID, taskID)
task, err := scanWorkspaceIssueTask(row)
if errors.Is(err, sql.ErrNoRows) {
return workspaceissues.Task{}, workspaceissues.ErrTaskNotFound
}
if err != nil {
return workspaceissues.Task{}, fmt.Errorf("get workspace issue task: %w", err)
}
return task, nil
}
func (s *SQLiteStore) UpdateTask(ctx context.Context, task workspaceissues.Task) (workspaceissues.Task, error) {
if err := s.ensureIssueDatabase(); err != nil {
return workspaceissues.Task{}, err
}
dependencyTaskIDsJSON, err := json.Marshal(task.DependencyTaskIDs)
if err != nil {
return workspaceissues.Task{}, fmt.Errorf("encode workspace issue task dependencies: %w", err)
}
result, err := s.writeDB.ExecContext(ctx, `
UPDATE workspace_issue_tasks
SET title = ?, content = ?, search_text = ?, status = ?, priority = ?,
sort_index = ?, due_at_unix_ms = ?, agent_target_id = ?, model_plan_id = ?,
model = ?, permission_mode_id = ?, reasoning_effort = ?, execution_directory = ?,
dependency_task_ids_json = ?, parallelizable = ?, auto_accept = ?, acceptance_state = ?, acceptance_summary = ?,
latest_run_id = ?, superseded_at_unix_ms = ?, superseded_by_task_id = ?,
updated_at_unix_ms = ?
WHERE workspace_id = ? AND issue_id = ? AND task_id = ?
`, task.Title, task.Content, task.SearchText, string(task.Status), string(task.Priority),
task.SortIndex, task.DueAtUnixMS, task.AgentTargetID, task.ModelPlanID, task.Model,
task.PermissionModeID, task.ReasoningEffort,
task.ExecutionDirectory, string(dependencyTaskIDsJSON), task.Parallelizable, task.AutoAccept,
string(task.AcceptanceState), task.AcceptanceSummary, task.LatestRunID,
task.SupersededAtUnixMS, task.SupersededByTaskID, task.UpdatedAtUnixMS,
task.WorkspaceID, task.IssueID, task.TaskID)
if err != nil {
return workspaceissues.Task{}, fmt.Errorf("update workspace issue task: %w", err)
}
if err := requireRowsAffected(result, workspaceissues.ErrTaskNotFound, "update workspace issue task"); err != nil {
return workspaceissues.Task{}, err
}
return s.GetTask(ctx, task.WorkspaceID, task.IssueID, task.TaskID)
}
func (s *SQLiteStore) DeleteTask(ctx context.Context, workspaceID string, issueID string, taskID string, _ string) (bool, error) {
if err := s.ensureIssueDatabase(); err != nil {
return false, err
}
result, err := s.writeDB.ExecContext(ctx, `
DELETE FROM workspace_issue_tasks
WHERE workspace_id = ? AND issue_id = ? AND task_id = ?
`, workspaceID, issueID, taskID)
if err != nil {
return false, fmt.Errorf("delete workspace issue task: %w", err)
}
return rowsWereAffected(result, "delete workspace issue task")
}