package acp import ( "encoding/json" "time" "github.com/google/uuid" ) // AgentKindAdminDTO 是 admin 视图的完整字段。EnvKeys 仅返回 key 列表(不返回值)。 type AgentKindAdminDTO struct { ID uuid.UUID `json:"id"` Name string `json:"name"` DisplayName string `json:"display_name"` Description string `json:"description"` BinaryPath string `json:"binary_path"` Args []string `json:"args"` EnvKeys []string `json:"env_keys"` Enabled bool `json:"enabled"` ToolAllowlist []string `json:"tool_allowlist"` ClientType string `json:"client_type"` // MCPServers 明文返回(admin only,编辑所需);条目可能含 header/env 密钥。 MCPServers []McpServerSpec `json:"mcp_servers"` // 成本核算:model 价格来源 + 默认预算上限(均可空)。 ModelID *string `json:"model_id"` MaxCostUSD *float64 `json:"max_cost_usd"` MaxTokens *int64 `json:"max_tokens"` MaxWallClockSeconds *int32 `json:"max_wall_clock_seconds"` CreatedBy uuid.UUID `json:"created_by"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } // AgentKindPublicDTO 是普通用户视图,不暴露 binary_path / args / env_keys。 type AgentKindPublicDTO struct { ID uuid.UUID `json:"id"` Name string `json:"name"` DisplayName string `json:"display_name"` Enabled bool `json:"enabled"` } // CreateAgentKindReq / UpdateAgentKindReq 是 HTTP 请求体。 type CreateAgentKindReq struct { Name string `json:"name"` DisplayName string `json:"display_name"` Description string `json:"description"` BinaryPath string `json:"binary_path"` Args []string `json:"args"` Env map[string]string `json:"env"` Enabled *bool `json:"enabled"` ToolAllowlist []string `json:"tool_allowlist"` ClientType string `json:"client_type"` MCPServers []McpServerSpec `json:"mcp_servers"` // 成本核算:model_id 为字符串 UUID(空/缺省 = 不设置);预算上限均可选。 ModelID *string `json:"model_id"` MaxCostUSD *float64 `json:"max_cost_usd"` MaxTokens *int64 `json:"max_tokens"` MaxWallClockSeconds *int32 `json:"max_wall_clock_seconds"` } type UpdateAgentKindReq struct { DisplayName *string `json:"display_name,omitempty"` Description *string `json:"description,omitempty"` BinaryPath *string `json:"binary_path,omitempty"` Args []string `json:"args,omitempty"` Env map[string]string `json:"env,omitempty"` Enabled *bool `json:"enabled,omitempty"` ToolAllowlist []string `json:"tool_allowlist,omitempty"` ClientType *string `json:"client_type,omitempty"` MCPServers []McpServerSpec `json:"mcp_servers,omitempty"` // nil = 不改;非 nil(含空)= 替换 // 成本核算三态:字段存在(非 nil)= 设置;缺省(nil)= 不改。 // ModelID 空串 = 清空;预算上限值 <=0 = 清空。 ModelID *string `json:"model_id,omitempty"` MaxCostUSD *float64 `json:"max_cost_usd,omitempty"` MaxTokens *int64 `json:"max_tokens,omitempty"` MaxWallClockSeconds *int32 `json:"max_wall_clock_seconds,omitempty"` } // ConfigFileDTO 是 AgentKind 配置文件的对外视图(admin only,content 明文)。 type ConfigFileDTO struct { RelPath string `json:"rel_path"` Content string `json:"content"` UpdatedAt string `json:"updated_at"` } // UpsertConfigFileReq 是 PUT config-files 请求体。 type UpsertConfigFileReq struct { RelPath string `json:"rel_path"` Content string `json:"content"` } func configFileToDTO(f *ConfigFileContent) ConfigFileDTO { return ConfigFileDTO{ RelPath: f.RelPath, Content: f.Content, UpdatedAt: f.UpdatedAt.UTC().Format("2006-01-02T15:04:05Z"), } } // PermissionRequestDTO 是权限请求的对外视图。 type PermissionRequestDTO struct { ID string `json:"id"` SessionID string `json:"session_id"` ToolName string `json:"tool_name"` ToolCall json.RawMessage `json:"tool_call"` Options json.RawMessage `json:"options"` Status string `json:"status"` CreatedAt string `json:"created_at"` } func permissionRequestToDTO(p *PermissionRequest) PermissionRequestDTO { return PermissionRequestDTO{ ID: p.ID.String(), SessionID: p.SessionID.String(), ToolName: p.ToolName, ToolCall: json.RawMessage(p.ToolCall), Options: json.RawMessage(p.Options), Status: string(p.Status), CreatedAt: p.CreatedAt.Format(time.RFC3339), } } // CreateSessionReq is the request body for POST /api/v1/acp/sessions. type CreateSessionReq struct { WorkspaceID string `json:"workspace_id"` AgentKindID string `json:"agent_kind_id"` Branch *string `json:"branch,omitempty"` IssueNumber *int `json:"issue_number,omitempty"` RequirementNumber *int `json:"requirement_number,omitempty"` InitialPrompt *string `json:"initial_prompt,omitempty"` } // SessionDTO is the response shape for sessions. type SessionDTO struct { ID string `json:"id"` WorkspaceID string `json:"workspace_id"` ProjectID string `json:"project_id"` AgentKindID string `json:"agent_kind_id"` UserID string `json:"user_id"` IssueID *string `json:"issue_id"` RequirementID *string `json:"requirement_id"` AgentSessionID *string `json:"agent_session_id"` Branch string `json:"branch"` CwdPath string `json:"cwd_path"` IsMainWorktree bool `json:"is_main_worktree"` Status string `json:"status"` PID *int32 `json:"pid"` ExitCode *int32 `json:"exit_code"` LastError *string `json:"last_error"` StartedAt string `json:"started_at"` EndedAt *string `json:"ended_at"` // 成本核算:运行时累计 + 有效预算上限 + 终止原因。 PromptTokens int64 `json:"prompt_tokens"` CompletionTokens int64 `json:"completion_tokens"` ThinkingTokens int64 `json:"thinking_tokens"` TotalCostUSD float64 `json:"total_cost_usd"` BudgetMaxCostUSD *float64 `json:"budget_max_cost_usd"` BudgetMaxTokens *int64 `json:"budget_max_tokens"` BudgetMaxWallClockSeconds *int32 `json:"budget_max_wall_clock_seconds"` TerminatedReason *string `json:"terminated_reason"` } func sessionToDTO(s *Session) SessionDTO { var issueID, reqID, ended *string if s.IssueID != nil { v := s.IssueID.String() issueID = &v } if s.RequirementID != nil { v := s.RequirementID.String() reqID = &v } if s.EndedAt != nil { v := s.EndedAt.UTC().Format("2006-01-02T15:04:05Z") ended = &v } return SessionDTO{ ID: s.ID.String(), WorkspaceID: s.WorkspaceID.String(), ProjectID: s.ProjectID.String(), AgentKindID: s.AgentKindID.String(), UserID: s.UserID.String(), IssueID: issueID, RequirementID: reqID, AgentSessionID: s.AgentSessionID, Branch: s.Branch, CwdPath: s.CwdPath, IsMainWorktree: s.IsMainWorktree, Status: string(s.Status), PID: s.PID, ExitCode: s.ExitCode, LastError: s.LastError, StartedAt: s.StartedAt.UTC().Format("2006-01-02T15:04:05Z"), EndedAt: ended, PromptTokens: s.PromptTokens, CompletionTokens: s.CompletionTokens, ThinkingTokens: s.ThinkingTokens, TotalCostUSD: s.TotalCostUSD, BudgetMaxCostUSD: s.BudgetMaxCostUSD, BudgetMaxTokens: s.BudgetMaxTokens, BudgetMaxWallClockSeconds: s.BudgetMaxWallClockSeconds, TerminatedReason: s.TerminatedReason, } } // SessionUsageDTO 是单条 per-turn 账目的对外视图。 type SessionUsageDTO struct { ID int64 `json:"id"` ModelID *string `json:"model_id"` PromptTokens int64 `json:"prompt_tokens"` CompletionTokens int64 `json:"completion_tokens"` ThinkingTokens int64 `json:"thinking_tokens"` CostUSD float64 `json:"cost_usd"` CreatedAt string `json:"created_at"` } // SessionUsageResponse 是 GET /sessions/{id}/usage 的响应:会话累计 + 最近账目。 type SessionUsageResponse struct { SessionID string `json:"session_id"` PromptTokens int64 `json:"prompt_tokens"` CompletionTokens int64 `json:"completion_tokens"` ThinkingTokens int64 `json:"thinking_tokens"` TotalCostUSD float64 `json:"total_cost_usd"` BudgetMaxCostUSD *float64 `json:"budget_max_cost_usd"` BudgetMaxTokens *int64 `json:"budget_max_tokens"` Recent []SessionUsageDTO `json:"recent"` } // AcpUsageItem 是 admin ACP 用量聚合的一行(镜像 chat AdminUsageItem)。 type AcpUsageItem struct { Key string `json:"key"` PromptTokens int64 `json:"prompt_tokens"` CompletionTokens int64 `json:"completion_tokens"` ThinkingTokens int64 `json:"thinking_tokens"` CostUSD float64 `json:"cost_usd"` } // AcpUsageResponse 包裹分组用量项。 type AcpUsageResponse struct { Items []AcpUsageItem `json:"items"` } // ===== run-history dashboard DTO ===== // DashboardResponse 是 GET /api/v1/acp/dashboard 的响应:总览 + 按天序列。 type DashboardResponse struct { From string `json:"from"` To string `json:"to"` Rollup SessionRollupDTO `json:"rollup"` ByDay []SessionDayDTO `json:"by_day"` } // SessionRollupDTO 是会话总览(计数 + 成功率 + 成本 + 平均时长)。 type SessionRollupDTO struct { Total int64 `json:"total"` Succeeded int64 `json:"succeeded"` Crashed int64 `json:"crashed"` Active int64 `json:"active"` SuccessRate float64 `json:"success_rate"` TotalCostUSD float64 `json:"total_cost_usd"` TotalTokens int64 `json:"total_tokens"` AvgDurationSeconds float64 `json:"avg_duration_seconds"` } // SessionDayDTO 是按天的时间序列一行。 type SessionDayDTO struct { Day string `json:"day"` Total int64 `json:"total"` Succeeded int64 `json:"succeeded"` Crashed int64 `json:"crashed"` TotalCostUSD float64 `json:"total_cost_usd"` } // SessionTimelineDTO 是单会话事件时间线一行(按 method/kind 分桶)。 type SessionTimelineDTO struct { Direction string `json:"direction"` Kind string `json:"kind"` Method string `json:"method"` EventCount int64 `json:"event_count"` FirstAt string `json:"first_at"` LastAt string `json:"last_at"` } func dashboardToResponse(r *DashboardResult) DashboardResponse { resp := DashboardResponse{ From: r.From.UTC().Format(time.RFC3339), To: r.To.UTC().Format(time.RFC3339), Rollup: SessionRollupDTO{ Total: r.Rollup.Total, Succeeded: r.Rollup.Succeeded, Crashed: r.Rollup.Crashed, Active: r.Rollup.Active, SuccessRate: r.Rollup.SuccessRate(), TotalCostUSD: r.Rollup.TotalCostUSD, TotalTokens: r.Rollup.TotalTokens, AvgDurationSeconds: r.Rollup.AvgDurationSeconds, }, ByDay: make([]SessionDayDTO, 0, len(r.ByDay)), } for _, d := range r.ByDay { resp.ByDay = append(resp.ByDay, SessionDayDTO{ Day: d.Day.UTC().Format(time.RFC3339), Total: d.Total, Succeeded: d.Succeeded, Crashed: d.Crashed, TotalCostUSD: d.TotalCostUSD, }) } return resp } func sessionTimelineToDTO(b SessionTimelineBucket) SessionTimelineDTO { return SessionTimelineDTO{ Direction: b.Direction, Kind: b.RPCKind, Method: b.Method, EventCount: b.EventCount, FirstAt: b.FirstAt.UTC().Format(time.RFC3339), LastAt: b.LastAt.UTC().Format(time.RFC3339), } } func sessionUsageToDTO(r *SessionUsageRecord) SessionUsageDTO { var modelID *string if r.ModelID != nil { v := r.ModelID.String() modelID = &v } return SessionUsageDTO{ ID: r.ID, ModelID: modelID, PromptTokens: r.PromptTokens, CompletionTokens: r.CompletionTokens, ThinkingTokens: r.ThinkingTokens, CostUSD: r.CostUSD, CreatedAt: r.CreatedAt.UTC().Format(time.RFC3339), } }