Implement Autonomous Mode Filtering in Robot API

- Added support for filtering robots by `autonomous_mode` in the ListRobots API.
- Enhanced ListQuery structure to include an optional `AutonomousMode` field.
- Updated listRobotsFromDB function to apply the autonomous mode filter based on the query.
- Created new test cases to validate the filtering functionality for both autonomous and on-demand robots.
- Revised related OpenAPI endpoints and frontend integration to accommodate the new filtering options.
This commit is contained in:
Max 2026-01-22 16:14:59 +08:00
parent 6f93175a46
commit bb7638f1e5
6 changed files with 296 additions and 60 deletions

View file

@ -205,6 +205,75 @@ func TestAPIRobotQueryWithData(t *testing.T) {
}) })
} }
// TestListRobotsAutonomousModeFilter tests the autonomous_mode filter
func TestListRobotsAutonomousModeFilter(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
testutils.Prepare(t)
defer testutils.Clean(t)
cleanupAPITestRobots(t)
defer cleanupAPITestRobots(t)
// Setup: Create robots with different autonomous_mode settings
setupAPITestRobotWithMode(t, "robot_api_auto_001", "team_api_mode", true) // autonomous
setupAPITestRobotWithMode(t, "robot_api_auto_002", "team_api_mode", true) // autonomous
setupAPITestRobotWithMode(t, "robot_api_demand_001", "team_api_mode", false) // on-demand
ctx := types.NewContext(context.Background(), nil)
t.Run("ListRobots returns all robots when autonomous_mode is nil", func(t *testing.T) {
result, err := api.ListRobots(ctx, &api.ListQuery{
TeamID: "team_api_mode",
Page: 1,
PageSize: 10,
})
require.NoError(t, err)
require.NotNil(t, result)
// Should have all 3 robots
assert.Equal(t, 3, result.Total)
})
t.Run("ListRobots filters by autonomous_mode=true", func(t *testing.T) {
autonomousMode := true
result, err := api.ListRobots(ctx, &api.ListQuery{
TeamID: "team_api_mode",
AutonomousMode: &autonomousMode,
Page: 1,
PageSize: 10,
})
require.NoError(t, err)
require.NotNil(t, result)
// Should have only 2 autonomous robots
assert.Equal(t, 2, result.Total)
for _, robot := range result.Data {
assert.True(t, robot.AutonomousMode, "All returned robots should be autonomous")
}
})
t.Run("ListRobots filters by autonomous_mode=false", func(t *testing.T) {
autonomousMode := false
result, err := api.ListRobots(ctx, &api.ListQuery{
TeamID: "team_api_mode",
AutonomousMode: &autonomousMode,
Page: 1,
PageSize: 10,
})
require.NoError(t, err)
require.NotNil(t, result)
// Should have only 1 on-demand robot
assert.Equal(t, 1, result.Total)
for _, robot := range result.Data {
assert.False(t, robot.AutonomousMode, "All returned robots should be on-demand")
}
})
}
// TestAPIExecutionQueryWithData tests execution query APIs with real data // TestAPIExecutionQueryWithData tests execution query APIs with real data
func TestAPIExecutionQueryWithData(t *testing.T) { func TestAPIExecutionQueryWithData(t *testing.T) {
if testing.Short() { if testing.Short() {
@ -377,6 +446,44 @@ func TestAPITriggerWithData(t *testing.T) {
// ==================== Helper Functions ==================== // ==================== Helper Functions ====================
// setupAPITestRobotWithMode creates a test robot with specific autonomous_mode setting
func setupAPITestRobotWithMode(t *testing.T, memberID, teamID string, autonomousMode bool) {
m := model.Select("__yao.member")
tableName := m.MetaData.Table.Name
qb := capsule.Query()
robotConfig := map[string]interface{}{
"identity": map[string]interface{}{
"role": "API Test Robot",
"duties": []string{"Testing API functions"},
},
"quota": map[string]interface{}{
"max": 5,
"queue": 20,
"priority": 5,
},
}
configJSON, _ := json.Marshal(robotConfig)
err := qb.Table(tableName).Insert([]map[string]interface{}{
{
"member_id": memberID,
"team_id": teamID,
"member_type": "robot",
"display_name": "API Test Robot " + memberID,
"system_prompt": "You are an API test robot.",
"status": "active",
"role_id": "member",
"autonomous_mode": autonomousMode,
"robot_status": "idle",
"robot_config": string(configJSON),
},
})
if err != nil {
t.Fatalf("Failed to insert robot %s: %v", memberID, err)
}
}
// setupAPITestRobot creates a test robot in the database // setupAPITestRobot creates a test robot in the database
func setupAPITestRobot(t *testing.T, memberID, teamID string) { func setupAPITestRobot(t *testing.T, memberID, teamID string) {
m := model.Select("__yao.member") m := model.Select("__yao.member")

View file

@ -168,7 +168,6 @@ func listRobotsFromDB(query *ListQuery) (*ListResult, error) {
// Build where conditions // Build where conditions
wheres := []model.QueryWhere{ wheres := []model.QueryWhere{
{Column: "member_type", Value: "robot"}, {Column: "member_type", Value: "robot"},
{Column: "autonomous_mode", Value: true},
{Column: "status", Value: "active"}, {Column: "status", Value: "active"},
} }
@ -185,6 +184,9 @@ func listRobotsFromDB(query *ListQuery) (*ListResult, error) {
Value: "%" + query.Keywords + "%", Value: "%" + query.Keywords + "%",
}) })
} }
if query.AutonomousMode != nil {
wheres = append(wheres, model.QueryWhere{Column: "autonomous_mode", Value: *query.AutonomousMode})
}
// Build order // Build order
orders := []model.QueryOrder{} orders := []model.QueryOrder{}

View file

@ -9,13 +9,14 @@ import (
// ListQuery - query options for List() // ListQuery - query options for List()
type ListQuery struct { type ListQuery struct {
TeamID string `json:"team_id,omitempty"` TeamID string `json:"team_id,omitempty"`
Status types.RobotStatus `json:"status,omitempty"` Status types.RobotStatus `json:"status,omitempty"`
Keywords string `json:"keywords,omitempty"` Keywords string `json:"keywords,omitempty"`
ClockMode types.ClockMode `json:"clock_mode,omitempty"` ClockMode types.ClockMode `json:"clock_mode,omitempty"`
Page int `json:"page,omitempty"` AutonomousMode *bool `json:"autonomous_mode,omitempty"` // nil=all, true=autonomous only, false=on-demand only
PageSize int `json:"pagesize,omitempty"` Page int `json:"page,omitempty"`
Order string `json:"order,omitempty"` PageSize int `json:"pagesize,omitempty"`
Order string `json:"order,omitempty"`
} }
// ListResult - result of List() // ListResult - result of List()

View file

@ -16,9 +16,10 @@
Backend → SDK → Page Integration Backend → SDK → Page Integration
└─ List, Get, Create, Update, Delete robots └─ List, Get, Create, Update, Delete robots
🟡 Phase 1-FE: Frontend Integration ⬜ [Current] ✅ Phase 1-FE: Frontend Integration ✅ [Completed]
└─ Implement SDK (openapi/robot.ts) └─ SDK (openapi/robot.ts) ✅
└─ Page Integration (Robot list, detail, create, edit, delete) └─ Page Integration (Robot list, detail, create, edit, delete) ✅
└─ UI/UX (CreatureLoading, bubble animations) ✅
🟢 Phase 2: Execution Management 🟢 Phase 2: Execution Management
Backend → SDK → Page Integration Backend → SDK → Page Integration
@ -177,53 +178,92 @@
--- ---
## 🟡 Phase 1-FE: Frontend Integration ⬜ [Current] ## ✅ Phase 1-FE: Frontend Integration ✅ [Completed]
**Goal:** Implement frontend SDK and integrate pages to validate Phase 1 deliverables **Goal:** Implement frontend SDK and integrate pages to validate Phase 1 deliverables
**Status:** ⬜ Not Started **Status:** ✅ Completed
### 1-FE.1 SDK Implementation ### 1-FE.1 SDK Implementation
> Location: `cui/packages/cui/openapi/robot.ts` > Location: `cui/packages/cui/openapi/agent/robot/`
- [ ] Create `robot.ts` - Robot API SDK - [x] Create `robot/types.ts` - TypeScript types for Robot API
- [ ] `listRobots(params)` - GET /v1/agent/robots - [x] `RobotFilter` - filter options for listing (including `autonomous_mode`)
- [ ] `getRobot(id)` - GET /v1/agent/robots/:id - [x] `Robot` - robot data structure
- [ ] `getRobotStatus(id)` - GET /v1/agent/robots/:id/status - [x] `RobotStatusResponse` - runtime status
- [ ] `createRobot(data)` - POST /v1/agent/robots - [x] `RobotCreateRequest` / `RobotUpdateRequest` - CRUD requests
- [ ] `updateRobot(id, data)` - PUT /v1/agent/robots/:id - [x] `RobotDeleteResponse` - delete response
- [ ] `deleteRobot(id)` - DELETE /v1/agent/robots/:id - [x] Create `robot/robots.ts` - Robot API SDK class (`AgentRobots`)
- [ ] Add TypeScript types for request/response - [x] `List(filter)` - GET /v1/agent/robots
- [ ] Export from `openapi/index.ts` - [x] `Get(id)` - GET /v1/agent/robots/:id
- [x] `GetStatus(id)` - GET /v1/agent/robots/:id/status
- [x] `Create(data)` - POST /v1/agent/robots
- [x] `Update(id, data)` - PUT /v1/agent/robots/:id
- [x] `Delete(id)` - DELETE /v1/agent/robots/:id
- [x] Create `robot/index.ts` - exports
- [x] Update `agent/api.ts` - add `robots` property to Agent class
- [x] Update `agent/index.ts` - export robot module
- [x] Linter check passed
### 1-FE.2 Page Integration ⬜ ### 1-FE.2 Page Integration
> Location: `cui/packages/cui/pages/robot/` > Location: `cui/packages/cui/pages/mission-control/`
- [ ] Robot List Page - [x] Create `useRobots` hook for API calls
- [ ] Replace mock data with `listRobots()` API - [x] `listRobots(filter)` - list robots with pagination
- [ ] Implement pagination - [x] `getRobot(id)` - get single robot
- [ ] Implement filters (status, keywords, team) - [x] `getRobotStatus(id)` - get runtime status
- [ ] Robot Detail Page - [x] `createRobot(data)` - create robot
- [ ] Fetch robot via `getRobot(id)` - [x] `updateRobot(id, data)` - update robot
- [ ] Display robot status via `getRobotStatus(id)` - [x] `deleteRobot(id)` - delete robot
- [ ] Create Robot - [x] Error handling and loading state
- [ ] Form validation - [x] Robot List Page (`mission-control/index.tsx`)
- [ ] Call `createRobot()` API - [x] Replace mock data with `listRobots()` API (fallback to mock)
- [ ] Handle success/error - [x] Fetch status for each robot via `getRobotStatus()`
- [ ] Edit Robot - [x] Refresh list after robot created/updated/deleted
- [ ] Pre-populate form with existing data - [x] Empty state with "Create Agent" button (with bubble animation)
- [ ] Call `updateRobot()` API - [ ] Implement pagination (TODO: Phase 2)
- [ ] Delete Robot - [ ] Implement filters (status, keywords, team) (TODO: Phase 2)
- [ ] Confirmation dialog - [x] Robot Detail Modal (`AgentModal`)
- [ ] Call `deleteRobot()` API - [x] Real-time status refresh via `getRobotStatus(id)`
- [ ] Handle running execution conflict (409) - [x] Auto-refresh every 10 seconds while modal open
- [x] Merge real-time status with robot data
- [x] Create Robot (`AddAgentModal`)
- [x] Call `createRobot()` API
- [x] Handle success/error messages
- [x] Form validation (existing)
- [x] Load email domains, managers, agents, MCP servers from API
- [x] Edit Robot (`ConfigTab` in `AgentModal`)
- [x] Load robot data from API (`getRobot()`)
- [x] Load email domains, managers, roles from Team API
- [x] Load agents and MCP servers from API
- [x] Pre-populate form with existing data
- [x] Call `updateRobot()` API with `robot_config.clock` for schedule
- [x] Handle success/error messages
- [x] Work Schedule panel saves correctly
- [x] Delete Robot (`AdvancedPanel` in `ConfigTab`)
- [x] Confirmation dialog with name input
- [x] Call `deleteRobot()` API
- [x] Handle running execution conflict (409)
- [x] Refresh list after deletion
### 1-FE.3 Verification ⬜ ### 1-FE.3 UI/UX Enhancements ✅
- [ ] E2E test: Create → List → Get → Update → Delete - [x] `CreatureLoading` component with organic animations
- [ ] Permission test: Personal user vs Team user - [x] Breathing aura, floating creature, orbit ring, particles
- [ ] Error handling: 400, 403, 404, 409, 500 - [x] Three sizes: small, medium, large
- [x] Used in ConfigTab, ResultsTab, HistoryTab
- [x] Empty state "Create Agent" button with bubble animation
- [x] Cyan, purple, pink glowing bubbles rising
- [x] CSS variable compliance (`--color_mission_button_text`)
- [x] Consistent loading animations across all tabs
### 1-FE.4 Verification ✅
- [x] Manual test: Create → List → Get → Update → Delete
- [ ] E2E automated test (TODO: Phase 3)
- [x] Permission test: Personal user vs Team user (manual tested)
- [x] Error handling: 400, 403, 404, 409, 500
--- ---
@ -579,8 +619,8 @@ yao/openapi/tests/robot/
| Phase | Risk | Backend | Frontend | Description | | Phase | Risk | Backend | Frontend | Description |
|-------|------|---------|----------|-------------| |-------|------|---------|----------|-------------|
| 1. Core CRUD | 🟢 | ✅ | | Robot CRUD endpoints | | 1. Core CRUD | 🟢 | ✅ | | Robot CRUD endpoints |
| 1-FE Frontend Integration | 🟢 | - | 🟡 | **Current**: SDK + Page Integration | | 1-FE Frontend Integration | 🟢 | - | ✅ | SDK ✅, Page Integration ✅, UI/UX ✅ |
| 2. Execution | 🟢 | ⬜ | ⬜ | Execution listing, control, trigger | | 2. Execution | 🟢 | ⬜ | ⬜ | Execution listing, control, trigger |
| 3. Results/Activities | 🟢 | ⬜ | ⬜ | Deliverables and activity feed | | 3. Results/Activities | 🟢 | ⬜ | ⬜ | Deliverables and activity feed |
| 4. i18n | 🟢 | ⬜ | ⬜ | Locale parameter support | | 4. i18n | 🟢 | ⬜ | ⬜ | Locale parameter support |
@ -719,21 +759,36 @@ import (
**Execute immediately after each phase backend completion:** **Execute immediately after each phase backend completion:**
1. **SDK Implementation** - `cui/packages/cui/openapi/robot.ts` 1. **SDK Implementation** - `cui/packages/cui/openapi/agent/robot/`
2. **Type Definitions** - TypeScript request/response types 2. **Type Definitions** - TypeScript request/response types
3. **Page Integration** - Replace mock data, call real APIs 3. **Hook Implementation** - `cui/packages/cui/hooks/useRobots.ts`
4. **E2E Verification** - Full flow testing 4. **Page Integration** - Replace mock data, call real APIs
5. **E2E Verification** - Full flow testing
**File Locations:** **File Locations:**
``` ```
cui/packages/cui/ cui/packages/cui/
├── openapi/ ├── openapi/
│ ├── robot.ts # Robot API SDK │ └── agent/
│ └── index.ts # Export all APIs │ └── robot/
└── pages/robot/ │ ├── types.ts # TypeScript types
├── index.tsx # Robot list page │ ├── robots.ts # AgentRobots SDK class
├── [id].tsx # Robot detail page │ └── index.ts # Exports
└── components/ # Shared components ├── hooks/
│ └── useRobots.ts # React hook for robot API calls
├── styles/
│ └── preset/
│ └── vars.less # CSS variables (--color_mission_button_text)
└── pages/
└── mission-control/
├── index.tsx # Robot list (grid) page
├── index.less # Styles with bubble animations
└── components/
├── AgentModal/ # Robot detail modal
├── AddAgentModal/ # Create robot modal
└── CreatureLoading/ # Branded loading component
├── index.tsx
└── index.less
``` ```
### Incremental Deployment ### Incremental Deployment
@ -742,7 +797,7 @@ Each phase independently deliverable:
| Phase | Backend | Frontend | Verifiable Features | | Phase | Backend | Frontend | Verifiable Features |
|-------|---------|----------|---------------------| |-------|---------|----------|---------------------|
| 1 | ✅ | 🟡 Current | Robot CRUD basic management | | 1 | ✅ | | Robot CRUD basic management |
| 2 | ⬜ | ⬜ | Execution list/control/trigger | | 2 | ⬜ | ⬜ | Execution list/control/trigger |
| 3 | ⬜ | ⬜ | Results/Activities viewing | | 3 | ⬜ | ⬜ | Results/Activities viewing |
| 4 | ⬜ | ⬜ | Multi-language support | | 4 | ⬜ | ⬜ | Multi-language support |

View file

@ -37,6 +37,7 @@ func ListRobots(c *gin.Context) {
requestedTeamID := strings.TrimSpace(c.Query("team_id")) requestedTeamID := strings.TrimSpace(c.Query("team_id"))
status := strings.TrimSpace(c.Query("status")) status := strings.TrimSpace(c.Query("status"))
keywords := strings.TrimSpace(c.Query("keywords")) keywords := strings.TrimSpace(c.Query("keywords"))
autonomousModeStr := strings.TrimSpace(c.Query("autonomous_mode"))
// Apply permission-based filtering // Apply permission-based filtering
// This ensures users only see robots they have access to: // This ensures users only see robots they have access to:
@ -55,6 +56,14 @@ func ListRobots(c *gin.Context) {
if status != "" { if status != "" {
query.Status = robottypes.RobotStatus(status) query.Status = robottypes.RobotStatus(status)
} }
// Parse autonomous_mode filter: "true" or "false" to filter, empty/other to show all
if autonomousModeStr == "true" {
autonomousMode := true
query.AutonomousMode = &autonomousMode
} else if autonomousModeStr == "false" {
autonomousMode := false
query.AutonomousMode = &autonomousMode
}
// Create robot context // Create robot context
ctx := &robottypes.Context{} ctx := &robottypes.Context{}

View file

@ -74,6 +74,68 @@ func TestListRobots(t *testing.T) {
assert.Equal(t, float64(5), response["pagesize"]) assert.Equal(t, float64(5), response["pagesize"])
}) })
t.Run("ListRobotsWithAutonomousModeFilter", func(t *testing.T) {
// Test with autonomous_mode=true
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots?autonomous_mode=true", nil)
require.NoError(t, err)
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
var response map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&response)
require.NoError(t, err)
// Verify response structure
assert.Contains(t, response, "data")
assert.Contains(t, response, "total")
// If there are robots, verify they are all autonomous
if data, ok := response["data"].([]interface{}); ok && len(data) > 0 {
for _, item := range data {
if robot, ok := item.(map[string]interface{}); ok {
assert.True(t, robot["autonomous_mode"].(bool), "All robots should have autonomous_mode=true")
}
}
}
})
t.Run("ListRobotsWithAutonomousModeFalse", func(t *testing.T) {
// Test with autonomous_mode=false
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots?autonomous_mode=false", nil)
require.NoError(t, err)
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
var response map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&response)
require.NoError(t, err)
// Verify response structure
assert.Contains(t, response, "data")
assert.Contains(t, response, "total")
// If there are robots, verify they are all on-demand (not autonomous)
if data, ok := response["data"].([]interface{}); ok && len(data) > 0 {
for _, item := range data {
if robot, ok := item.(map[string]interface{}); ok {
assert.False(t, robot["autonomous_mode"].(bool), "All robots should have autonomous_mode=false")
}
}
}
})
t.Run("ListRobotsUnauthorized", func(t *testing.T) { t.Run("ListRobotsUnauthorized", func(t *testing.T) {
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots", nil) req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots", nil)
require.NoError(t, err) require.NoError(t, err)