mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-13 16:14:57 +00:00
Replace direct sentinel error comparisons (==, !=) with errors.Is()
across the codebase. Direct comparison breaks when errors are wrapped
with fmt.Errorf("...: %w", err), which can cause missed matches for
sql.ErrNoRows, io.EOF, context.DeadlineExceeded, and context.Canceled.
Affected packages:
- store/pg: sql.ErrNoRows in secure_cli, agents_export_team_*, mcp_export_queries
- store/sqlitestore: sql.ErrNoRows in schema migration
- tools: context.DeadlineExceeded in shell, credentialed_exec
- mcp: context.DeadlineExceeded in bridge_tool
- providers: context.Canceled in acp_provider, anthropic_stream_test
- updater: io.EOF in tar extraction
Also removes unused @xyflow/react dependency from web UI — it was
replaced by react-force-graph-2d but never cleaned up from package.json.
Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
302 lines
9.4 KiB
Go
302 lines
9.4 KiB
Go
package pg
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"log/slog"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// ExportTeamByID fetches team metadata by team_id directly (not via lead_agent_id).
|
|
func ExportTeamByID(ctx context.Context, db *sql.DB, teamID uuid.UUID) (*TeamExport, error) {
|
|
tc, tcArgs, _, err := scopeClause(ctx, 2)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var t TeamExport
|
|
err = db.QueryRowContext(ctx,
|
|
"SELECT name, COALESCE(description,''), status, COALESCE(settings,'{}')"+
|
|
" FROM agent_teams WHERE id = $1"+tc,
|
|
append([]any{teamID}, tcArgs...)...,
|
|
).Scan(&t.Name, &t.Description, &t.Status, &t.Settings)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &t, nil
|
|
}
|
|
|
|
// GetTeamMemberAgents returns all agents (id + agent_key) in a team.
|
|
func GetTeamMemberAgents(ctx context.Context, db *sql.DB, teamID uuid.UUID) ([]struct {
|
|
ID uuid.UUID
|
|
AgentKey string
|
|
}, error) {
|
|
tc, tcArgs, _, err := scopeClauseAlias(ctx, 2, "m")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rows, err := db.QueryContext(ctx,
|
|
"SELECT a.id, a.agent_key"+
|
|
" FROM agent_team_members m"+
|
|
" JOIN agents a ON a.id = m.agent_id"+
|
|
" WHERE m.team_id = $1"+tc,
|
|
append([]any{teamID}, tcArgs...)...,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var out []struct {
|
|
ID uuid.UUID
|
|
AgentKey string
|
|
}
|
|
for rows.Next() {
|
|
var item struct {
|
|
ID uuid.UUID
|
|
AgentKey string
|
|
}
|
|
if err := rows.Scan(&item.ID, &item.AgentKey); err != nil {
|
|
slog.Warn("export.team.member_agents.scan", "error", err)
|
|
continue
|
|
}
|
|
out = append(out, item)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// ExportTeamLinksForTeam returns agent_links where both source and target are members of the team.
|
|
func ExportTeamLinksForTeam(ctx context.Context, db *sql.DB, teamID uuid.UUID) ([]AgentLinkExport, error) {
|
|
tc, tcArgs, _, err := scopeClauseAlias(ctx, 2, "l")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rows, err := db.QueryContext(ctx,
|
|
"SELECT sa.agent_key, ta.agent_key, l.direction, COALESCE(l.description,'')"+
|
|
" FROM agent_links l"+
|
|
" JOIN agents sa ON sa.id = l.source_agent_id"+
|
|
" JOIN agents ta ON ta.id = l.target_agent_id"+
|
|
" WHERE l.source_agent_id IN (SELECT agent_id FROM agent_team_members WHERE team_id = $1)"+
|
|
" AND l.target_agent_id IN (SELECT agent_id FROM agent_team_members WHERE team_id = $1)"+tc,
|
|
append([]any{teamID}, tcArgs...)...,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var out []AgentLinkExport
|
|
for rows.Next() {
|
|
var l AgentLinkExport
|
|
if err := rows.Scan(&l.SourceAgentKey, &l.TargetAgentKey, &l.Direction, &l.Description); err != nil {
|
|
slog.Warn("export.team.links.scan", "error", err)
|
|
continue
|
|
}
|
|
out = append(out, l)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// ExportTeamPreviewCountsByID returns task/member/link counts for a team by team_id.
|
|
func ExportTeamPreviewCountsByID(ctx context.Context, db *sql.DB, teamID uuid.UUID) (tasks, members, links int, err error) {
|
|
tc, tcArgs, _, err := scopeClause(ctx, 2)
|
|
if err != nil {
|
|
return
|
|
}
|
|
args := append([]any{teamID}, tcArgs...)
|
|
|
|
_ = db.QueryRowContext(ctx,
|
|
"SELECT"+
|
|
" (SELECT COUNT(*) FROM team_tasks WHERE team_id = $1"+tc+"),"+
|
|
" (SELECT COUNT(*) FROM agent_team_members WHERE team_id = $1"+tc+"),"+
|
|
" (SELECT COUNT(*) FROM agent_links"+
|
|
" WHERE source_agent_id IN (SELECT agent_id FROM agent_team_members WHERE team_id = $1"+tc+")"+
|
|
" AND target_agent_id IN (SELECT agent_id FROM agent_team_members WHERE team_id = $1"+tc+")"+tc+")",
|
|
args...,
|
|
).Scan(&tasks, &members, &links)
|
|
return
|
|
}
|
|
|
|
// ExportTeamMembersNonLead returns all non-lead members by team_id (no lead exclusion needed when doing standalone team export).
|
|
func ExportTeamMembersNonLead(ctx context.Context, db *sql.DB, teamID uuid.UUID, leadAgentID uuid.UUID) ([]TeamMemberExport, error) {
|
|
tc, tcArgs, _, err := scopeClauseAlias(ctx, 3, "m")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rows, err := db.QueryContext(ctx,
|
|
"SELECT a.agent_key, m.role"+
|
|
" FROM agent_team_members m"+
|
|
" JOIN agents a ON a.id = m.agent_id"+
|
|
" WHERE m.team_id = $1 AND m.agent_id != $2"+tc,
|
|
append([]any{teamID, leadAgentID}, tcArgs...)...,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var out []TeamMemberExport
|
|
for rows.Next() {
|
|
var m TeamMemberExport
|
|
if err := rows.Scan(&m.AgentKey, &m.Role); err != nil {
|
|
slog.Warn("export.team.members_non_lead.scan", "error", err)
|
|
continue
|
|
}
|
|
out = append(out, m)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// ExportTeamLeadAgentID returns the lead agent_id for a team, or uuid.Nil if not found.
|
|
func ExportTeamLeadAgentID(ctx context.Context, db *sql.DB, teamID uuid.UUID) (uuid.UUID, error) {
|
|
tc, tcArgs, _, err := scopeClause(ctx, 2)
|
|
if err != nil {
|
|
return uuid.Nil, err
|
|
}
|
|
var leadID uuid.UUID
|
|
err = db.QueryRowContext(ctx,
|
|
"SELECT lead_agent_id FROM agent_teams WHERE id = $1"+tc,
|
|
append([]any{teamID}, tcArgs...)...,
|
|
).Scan(&leadID)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return uuid.Nil, nil
|
|
}
|
|
return leadID, err
|
|
}
|
|
|
|
// ExportTeamAgentConfig returns agent metadata (id, agent_key, display_name, provider, model, etc.)
|
|
// as a raw JSON-serializable map using marshalAgentConfigExport helper. Returned as json.RawMessage.
|
|
func ExportTeamAgentBasicInfo(ctx context.Context, db *sql.DB, agentID uuid.UUID) (agentKey string, err error) {
|
|
tc, tcArgs, _, err := scopeClause(ctx, 2)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
err = db.QueryRowContext(ctx,
|
|
"SELECT agent_key FROM agents WHERE id = $1"+tc,
|
|
append([]any{agentID}, tcArgs...)...,
|
|
).Scan(&agentKey)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return "", nil
|
|
}
|
|
return agentKey, err
|
|
}
|
|
|
|
// ExportTeamMembersAll returns ALL members of a team (including lead) with agent_key resolution.
|
|
func ExportTeamMembersAll(ctx context.Context, db *sql.DB, teamID uuid.UUID) ([]TeamMemberExport, error) {
|
|
tc, tcArgs, _, err := scopeClauseAlias(ctx, 2, "m")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rows, err := db.QueryContext(ctx,
|
|
"SELECT a.agent_key, m.role"+
|
|
" FROM agent_team_members m"+
|
|
" JOIN agents a ON a.id = m.agent_id"+
|
|
" WHERE m.team_id = $1"+tc,
|
|
append([]any{teamID}, tcArgs...)...,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var out []TeamMemberExport
|
|
for rows.Next() {
|
|
var m TeamMemberExport
|
|
if err := rows.Scan(&m.AgentKey, &m.Role); err != nil {
|
|
slog.Warn("export.team.all_members.scan", "error", err)
|
|
continue
|
|
}
|
|
out = append(out, m)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// exportTeamAgentFullData returns a fully-serializable agent config for team export.
|
|
func ExportTeamAgentJSON(ctx context.Context, db *sql.DB, agentID uuid.UUID) (json.RawMessage, error) {
|
|
tc, tcArgs, _, err := scopeClause(ctx, 2)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var (
|
|
agentKey string
|
|
displayName string
|
|
frontmatter string
|
|
provider string
|
|
model string
|
|
contextWindow int
|
|
maxToolIterations int
|
|
agentType string
|
|
status string
|
|
toolsConfig []byte
|
|
sandboxConfig []byte
|
|
subagentsConfig []byte
|
|
memoryConfig []byte
|
|
compactionConfig []byte
|
|
contextPruning []byte
|
|
otherConfig []byte
|
|
)
|
|
|
|
err = db.QueryRowContext(ctx,
|
|
"SELECT agent_key, COALESCE(display_name,''), COALESCE(frontmatter,''),"+
|
|
" COALESCE(provider,''), COALESCE(model,''),"+
|
|
" context_window, max_tool_iterations, COALESCE(agent_type,'open'), COALESCE(status,'active'),"+
|
|
" tools_config, sandbox_config, subagents_config, memory_config, compaction_config,"+
|
|
" context_pruning, other_config"+
|
|
" FROM agents WHERE id = $1"+tc,
|
|
append([]any{agentID}, tcArgs...)...,
|
|
).Scan(
|
|
&agentKey, &displayName, &frontmatter,
|
|
&provider, &model,
|
|
&contextWindow, &maxToolIterations, &agentType, &status,
|
|
&toolsConfig, &sandboxConfig, &subagentsConfig, &memoryConfig, &compactionConfig,
|
|
&contextPruning, &otherConfig,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
type exportableAgent struct {
|
|
AgentKey string `json:"agent_key"`
|
|
DisplayName string `json:"display_name,omitempty"`
|
|
Frontmatter string `json:"frontmatter,omitempty"`
|
|
Provider string `json:"provider"`
|
|
Model string `json:"model"`
|
|
ContextWindow int `json:"context_window"`
|
|
MaxToolIterations int `json:"max_tool_iterations"`
|
|
AgentType string `json:"agent_type"`
|
|
Status string `json:"status"`
|
|
ToolsConfig json.RawMessage `json:"tools_config,omitempty"`
|
|
SandboxConfig json.RawMessage `json:"sandbox_config,omitempty"`
|
|
SubagentsConfig json.RawMessage `json:"subagents_config,omitempty"`
|
|
MemoryConfig json.RawMessage `json:"memory_config,omitempty"`
|
|
CompactionConfig json.RawMessage `json:"compaction_config,omitempty"`
|
|
ContextPruning json.RawMessage `json:"context_pruning,omitempty"`
|
|
OtherConfig json.RawMessage `json:"other_config,omitempty"`
|
|
}
|
|
raw, err := json.Marshal(exportableAgent{
|
|
AgentKey: agentKey,
|
|
DisplayName: displayName,
|
|
Frontmatter: frontmatter,
|
|
Provider: provider,
|
|
Model: model,
|
|
ContextWindow: contextWindow,
|
|
MaxToolIterations: maxToolIterations,
|
|
AgentType: agentType,
|
|
Status: status,
|
|
ToolsConfig: toolsConfig,
|
|
SandboxConfig: sandboxConfig,
|
|
SubagentsConfig: subagentsConfig,
|
|
MemoryConfig: memoryConfig,
|
|
CompactionConfig: compactionConfig,
|
|
ContextPruning: contextPruning,
|
|
OtherConfig: otherConfig,
|
|
})
|
|
return raw, err
|
|
}
|