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
func TestAPIExecutionQueryWithData(t *testing.T) {
if testing.Short() {
@ -377,6 +446,44 @@ func TestAPITriggerWithData(t *testing.T) {
// ==================== 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
func setupAPITestRobot(t *testing.T, memberID, teamID string) {
m := model.Select("__yao.member")

View file

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

View file

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

View file

@ -16,9 +16,10 @@
Backend → SDK → Page Integration
└─ List, Get, Create, Update, Delete robots
🟡 Phase 1-FE: Frontend Integration ⬜ [Current]
└─ Implement SDK (openapi/robot.ts)
└─ Page Integration (Robot list, detail, create, edit, delete)
✅ Phase 1-FE: Frontend Integration ✅ [Completed]
└─ SDK (openapi/robot.ts) ✅
└─ Page Integration (Robot list, detail, create, edit, delete) ✅
└─ UI/UX (CreatureLoading, bubble animations) ✅
🟢 Phase 2: Execution Management
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
**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
- [ ] `listRobots(params)` - GET /v1/agent/robots
- [ ] `getRobot(id)` - GET /v1/agent/robots/:id
- [ ] `getRobotStatus(id)` - GET /v1/agent/robots/:id/status
- [ ] `createRobot(data)` - POST /v1/agent/robots
- [ ] `updateRobot(id, data)` - PUT /v1/agent/robots/:id
- [ ] `deleteRobot(id)` - DELETE /v1/agent/robots/:id
- [ ] Add TypeScript types for request/response
- [ ] Export from `openapi/index.ts`
- [x] Create `robot/types.ts` - TypeScript types for Robot API
- [x] `RobotFilter` - filter options for listing (including `autonomous_mode`)
- [x] `Robot` - robot data structure
- [x] `RobotStatusResponse` - runtime status
- [x] `RobotCreateRequest` / `RobotUpdateRequest` - CRUD requests
- [x] `RobotDeleteResponse` - delete response
- [x] Create `robot/robots.ts` - Robot API SDK class (`AgentRobots`)
- [x] `List(filter)` - GET /v1/agent/robots
- [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
- [ ] Replace mock data with `listRobots()` API
- [ ] Implement pagination
- [ ] Implement filters (status, keywords, team)
- [ ] Robot Detail Page
- [ ] Fetch robot via `getRobot(id)`
- [ ] Display robot status via `getRobotStatus(id)`
- [ ] Create Robot
- [ ] Form validation
- [ ] Call `createRobot()` API
- [ ] Handle success/error
- [ ] Edit Robot
- [ ] Pre-populate form with existing data
- [ ] Call `updateRobot()` API
- [ ] Delete Robot
- [ ] Confirmation dialog
- [ ] Call `deleteRobot()` API
- [ ] Handle running execution conflict (409)
- [x] Create `useRobots` hook for API calls
- [x] `listRobots(filter)` - list robots with pagination
- [x] `getRobot(id)` - get single robot
- [x] `getRobotStatus(id)` - get runtime status
- [x] `createRobot(data)` - create robot
- [x] `updateRobot(id, data)` - update robot
- [x] `deleteRobot(id)` - delete robot
- [x] Error handling and loading state
- [x] Robot List Page (`mission-control/index.tsx`)
- [x] Replace mock data with `listRobots()` API (fallback to mock)
- [x] Fetch status for each robot via `getRobotStatus()`
- [x] Refresh list after robot created/updated/deleted
- [x] Empty state with "Create Agent" button (with bubble animation)
- [ ] Implement pagination (TODO: Phase 2)
- [ ] Implement filters (status, keywords, team) (TODO: Phase 2)
- [x] Robot Detail Modal (`AgentModal`)
- [x] Real-time status refresh via `getRobotStatus(id)`
- [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
- [ ] Permission test: Personal user vs Team user
- [ ] Error handling: 400, 403, 404, 409, 500
- [x] `CreatureLoading` component with organic animations
- [x] Breathing aura, floating creature, orbit ring, particles
- [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 |
|-------|------|---------|----------|-------------|
| 1. Core CRUD | 🟢 | ✅ | | Robot CRUD endpoints |
| 1-FE Frontend Integration | 🟢 | - | 🟡 | **Current**: SDK + Page Integration |
| 1. Core CRUD | 🟢 | ✅ | | Robot CRUD endpoints |
| 1-FE Frontend Integration | 🟢 | - | ✅ | SDK ✅, Page Integration ✅, UI/UX ✅ |
| 2. Execution | 🟢 | ⬜ | ⬜ | Execution listing, control, trigger |
| 3. Results/Activities | 🟢 | ⬜ | ⬜ | Deliverables and activity feed |
| 4. i18n | 🟢 | ⬜ | ⬜ | Locale parameter support |
@ -719,21 +759,36 @@ import (
**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
3. **Page Integration** - Replace mock data, call real APIs
4. **E2E Verification** - Full flow testing
3. **Hook Implementation** - `cui/packages/cui/hooks/useRobots.ts`
4. **Page Integration** - Replace mock data, call real APIs
5. **E2E Verification** - Full flow testing
**File Locations:**
```
cui/packages/cui/
├── openapi/
│ ├── robot.ts # Robot API SDK
│ └── index.ts # Export all APIs
└── pages/robot/
├── index.tsx # Robot list page
├── [id].tsx # Robot detail page
└── components/ # Shared components
│ └── agent/
│ └── robot/
│ ├── types.ts # TypeScript types
│ ├── robots.ts # AgentRobots SDK class
│ └── index.ts # Exports
├── 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
@ -742,7 +797,7 @@ Each phase independently deliverable:
| Phase | Backend | Frontend | Verifiable Features |
|-------|---------|----------|---------------------|
| 1 | ✅ | 🟡 Current | Robot CRUD basic management |
| 1 | ✅ | | Robot CRUD basic management |
| 2 | ⬜ | ⬜ | Execution list/control/trigger |
| 3 | ⬜ | ⬜ | Results/Activities viewing |
| 4 | ⬜ | ⬜ | Multi-language support |

View file

@ -37,6 +37,7 @@ func ListRobots(c *gin.Context) {
requestedTeamID := strings.TrimSpace(c.Query("team_id"))
status := strings.TrimSpace(c.Query("status"))
keywords := strings.TrimSpace(c.Query("keywords"))
autonomousModeStr := strings.TrimSpace(c.Query("autonomous_mode"))
// Apply permission-based filtering
// This ensures users only see robots they have access to:
@ -55,6 +56,14 @@ func ListRobots(c *gin.Context) {
if 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
ctx := &robottypes.Context{}

View file

@ -74,6 +74,68 @@ func TestListRobots(t *testing.T) {
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) {
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots", nil)
require.NoError(t, err)