yao/agent/robot/api/robot.go
Max c6ca68876d Refactor API Structure and Update TODO for MVP Integration
- Updated the API documentation in TODO.md to reflect the completion of the Go API implementation and end-to-end tests for the main flow (P0 → P1 → P2 → P3 → P4).
- Clarified that Process handlers and JSAPI are optional features, moving them to Phase 12, while emphasizing that the Go API is sufficient for MVP integration.
- Removed obsolete API files (`api.go`, `jsapi.go`, `process.go`) as they were stubs and not implemented in Phase 10, streamlining the codebase.
- Revised the structure of the TODO.md to better outline the goals and tasks for Phases 11 and 12, ensuring clear tracking of progress and dependencies.
2026-01-19 09:17:33 +08:00

258 lines
6.4 KiB
Go

package api
import (
"fmt"
"github.com/yaoapp/gou/model"
"github.com/yaoapp/kun/maps"
"github.com/yaoapp/yao/agent/robot/types"
)
// ==================== Robot Query API ====================
// These functions query robot information
// memberModel is the model name for member table
const memberModel = "__yao.member"
// GetRobot returns a robot by member ID
// Returns the robot from cache if available, otherwise loads from database
func GetRobot(ctx *types.Context, memberID string) (*types.Robot, error) {
if memberID == "" {
return nil, fmt.Errorf("member_id is required")
}
mgr, err := getManager()
if err != nil {
// Manager not started, try to load directly from database
return loadRobotFromDB(memberID)
}
// Try cache first
robot := mgr.Cache().Get(memberID)
if robot != nil {
return robot, nil
}
// Not in cache, try to load from database
robot, err = mgr.Cache().LoadByID(ctx, memberID)
if err != nil {
return nil, err
}
return robot, nil
}
// ListRobots returns robots with pagination and filtering
func ListRobots(ctx *types.Context, query *ListQuery) (*ListResult, error) {
if query == nil {
query = &ListQuery{}
}
query.applyDefaults()
mgr, err := getManager()
if err != nil {
// Manager not started, load directly from database
return listRobotsFromDB(query)
}
// If only teamID specified (no other filters), use cache for faster lookup
// Note: Cache only contains autonomous_mode=true robots, so this is safe
if query.TeamID != "" && query.Status == "" && query.Keywords == "" && query.ClockMode == "" {
robots := mgr.Cache().List(query.TeamID)
return paginateRobots(robots, query), nil
}
// For complex queries, load from database
return listRobotsFromDB(query)
}
// GetRobotStatus returns the runtime status of a robot
func GetRobotStatus(ctx *types.Context, memberID string) (*RobotState, error) {
if memberID == "" {
return nil, fmt.Errorf("member_id is required")
}
robot, err := GetRobot(ctx, memberID)
if err != nil {
return nil, err
}
state := &RobotState{
MemberID: robot.MemberID,
TeamID: robot.TeamID,
DisplayName: robot.DisplayName,
Status: robot.Status,
Running: robot.RunningCount(),
MaxRunning: 2, // default
}
if robot.Config != nil && robot.Config.Quota != nil {
state.MaxRunning = robot.Config.Quota.GetMax()
}
// Get running execution IDs
executions := robot.GetExecutions()
state.RunningIDs = make([]string, 0, len(executions))
for _, exec := range executions {
state.RunningIDs = append(state.RunningIDs, exec.ID)
}
// Set last run time
if !robot.LastRun.IsZero() {
state.LastRun = &robot.LastRun
}
// Set next run time
if !robot.NextRun.IsZero() {
state.NextRun = &robot.NextRun
}
return state, nil
}
// ==================== Helper Functions ====================
// loadRobotFromDB loads a robot directly from database
func loadRobotFromDB(memberID string) (*types.Robot, error) {
m := model.Select(memberModel)
if m == nil {
return nil, fmt.Errorf("model %s not found", memberModel)
}
records, err := m.Get(model.QueryParam{
Select: []interface{}{
"id", "member_id", "team_id", "display_name",
"system_prompt", "robot_status", "autonomous_mode",
"robot_config", "robot_email",
},
Wheres: []model.QueryWhere{
{Column: "member_id", Value: memberID},
{Column: "member_type", Value: "robot"},
},
Limit: 1,
})
if err != nil {
return nil, fmt.Errorf("failed to load robot: %w", err)
}
if len(records) == 0 {
return nil, types.ErrRobotNotFound
}
return types.NewRobotFromMap(map[string]interface{}(records[0]))
}
// listRobotsFromDB loads robots from database with filtering
func listRobotsFromDB(query *ListQuery) (*ListResult, error) {
m := model.Select(memberModel)
if m == nil {
return nil, fmt.Errorf("model %s not found", memberModel)
}
// Build where conditions
wheres := []model.QueryWhere{
{Column: "member_type", Value: "robot"},
{Column: "autonomous_mode", Value: true},
{Column: "status", Value: "active"},
}
if query.TeamID != "" {
wheres = append(wheres, model.QueryWhere{Column: "team_id", Value: query.TeamID})
}
if query.Status != "" {
wheres = append(wheres, model.QueryWhere{Column: "robot_status", Value: string(query.Status)})
}
if query.Keywords != "" {
wheres = append(wheres, model.QueryWhere{
Column: "display_name",
OP: "like",
Value: "%" + query.Keywords + "%",
})
}
// Build order
orders := []model.QueryOrder{}
if query.Order != "" {
orders = append(orders, model.QueryOrder{Column: query.Order})
} else {
orders = append(orders, model.QueryOrder{Column: "created_at", Option: "desc"})
}
// Execute paginated query
result, err := m.Paginate(model.QueryParam{
Select: []interface{}{
"id", "member_id", "team_id", "display_name",
"system_prompt", "robot_status", "autonomous_mode",
"robot_config", "robot_email",
},
Wheres: wheres,
Orders: orders,
}, query.Page, query.PageSize)
if err != nil {
return nil, fmt.Errorf("failed to list robots: %w", err)
}
// Parse result
listResult := &ListResult{
Data: []*types.Robot{},
Page: query.Page,
PageSize: query.PageSize,
}
// Get total count
if total, ok := result.Get("total").(int); ok {
listResult.Total = total
}
// Parse robot records - handle both []maps.MapStr and []map[string]interface{}
data := result.Get("data")
switch records := data.(type) {
case []maps.MapStr:
for _, record := range records {
robot, err := types.NewRobotFromMap(map[string]interface{}(record))
if err != nil {
continue // skip invalid records
}
listResult.Data = append(listResult.Data, robot)
}
case []map[string]interface{}:
for _, record := range records {
robot, err := types.NewRobotFromMap(record)
if err != nil {
continue // skip invalid records
}
listResult.Data = append(listResult.Data, robot)
}
}
return listResult, nil
}
// paginateRobots applies pagination to a slice of robots
func paginateRobots(robots []*types.Robot, query *ListQuery) *ListResult {
total := len(robots)
// Calculate offset
offset := (query.Page - 1) * query.PageSize
if offset >= total {
return &ListResult{
Data: []*types.Robot{},
Total: total,
Page: query.Page,
PageSize: query.PageSize,
}
}
// Calculate end index
end := offset + query.PageSize
if end > total {
end = total
}
return &ListResult{
Data: robots[offset:end],
Total: total,
Page: query.Page,
PageSize: query.PageSize,
}
}