Files
tutti/services/tuttid/data/workspace/sqlite_issue_run_outputs.go
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

76 lines
2.6 KiB
Go

package workspace
import (
"context"
"fmt"
"strings"
workspaceissues "github.com/tutti-os/tutti/packages/workspace/issues"
)
// SearchRunOutputs matches output files by display name across one workspace,
// joining the owning issue for its title (and for topic scoping). Results are
// recency-ordered and deduplicated by path, mirroring GetIssueDetail outputs.
func (s *SQLiteStore) SearchRunOutputs(ctx context.Context, params workspaceissues.RunOutputSearchParams) ([]workspaceissues.RunOutputSearchHit, error) {
if err := s.ensureIssueDatabase(); err != nil {
return nil, err
}
if err := s.ensureIssueWorkspace(ctx, params.WorkspaceID); err != nil {
return nil, err
}
// query 为空时 LIKE '%%' 匹配全部 —— 即「仅按类型筛选」的 list-all(由下方 filters 收窄)。
where := []string{"o.workspace_id = ?", "LOWER(o.display_name) LIKE ?"}
args := []any{params.WorkspaceID, "%" + strings.ToLower(params.Query) + "%"}
if params.IssueID != "" {
where = append(where, "o.issue_id = ?")
args = append(args, params.IssueID)
} else if params.TopicID != "" {
where = append(where, "i.topic_id = ?")
args = append(args, params.TopicID)
}
// 文件类型筛选(全局统一口径):按 display_name 扩展名收窄,见 reference_filter_categories.go。
if clause, clauseArgs := referenceFilterDisplayNameClause("o.display_name", params.Filters); clause != "" {
where = append(where, clause)
args = append(args, clauseArgs...)
}
query := fmt.Sprintf(`
SELECT o.id, o.output_id, o.run_id, o.task_id, o.issue_id, o.workspace_id,
o.path, o.display_name, o.media_type, o.size_bytes, o.created_at_unix_ms,
i.title
FROM workspace_issue_run_outputs o
JOIN workspace_issues i
ON i.workspace_id = o.workspace_id AND i.issue_id = o.issue_id
WHERE %s
ORDER BY o.created_at_unix_ms DESC, o.id DESC
LIMIT ?
`, strings.Join(where, " AND "))
args = append(args, params.Limit)
rows, err := s.readDB.QueryContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("search workspace issue run outputs: %w", err)
}
defer rows.Close()
hits := make([]workspaceissues.RunOutputSearchHit, 0)
seenPaths := map[string]struct{}{}
for rows.Next() {
hit, err := scanWorkspaceIssueRunOutputSearchHit(rows)
if err != nil {
return nil, err
}
dedupKey := strings.TrimSpace(hit.Output.Path)
if _, exists := seenPaths[dedupKey]; exists {
continue
}
seenPaths[dedupKey] = struct{}{}
hits = append(hits, hit)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate workspace issue run output search: %w", err)
}
return hits, nil
}