Implement robot member update functionality with comprehensive validation
- Added the `UpdateRobotMember` method to handle updates for robot members, including validation for member existence, type checks, and email uniqueness. - Introduced a new `UpdateRobotMemberRequest` structure to facilitate updates via the API, allowing for modifications to various fields such as `robot_email`, `status`, and `autonomous_mode`. - Enhanced the `GinMemberUpdateRobot` handler to process update requests, ensuring proper authentication and error handling for various scenarios. - Expanded test coverage with new test cases for the robot member update functionality, validating successful updates, error conditions, and edge cases. - Refactored existing member management logic to integrate the new update capabilities, improving overall system robustness and user experience.
This commit is contained in:
parent
7de562f80d
commit
1e01ed768e
7 changed files with 902 additions and 0 deletions
|
|
@ -386,6 +386,83 @@ func (u *DefaultUser) CreateRobotMember(ctx context.Context, teamID string, robo
|
||||||
return u.CreateMember(ctx, memberData)
|
return u.CreateMember(ctx, memberData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateRobotMember updates a robot member by member_id
|
||||||
|
func (u *DefaultUser) UpdateRobotMember(ctx context.Context, memberID string, robotData maps.MapStrAny) error {
|
||||||
|
// First, verify the member exists and is a robot
|
||||||
|
existingMember, err := u.GetMemberByMemberID(ctx, memberID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get member: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify this is a robot member
|
||||||
|
memberType, exists := existingMember["member_type"]
|
||||||
|
if !exists || memberType != "robot" {
|
||||||
|
return fmt.Errorf("member %s is not a robot member", memberID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if robot_email already exists globally (if updating robot_email)
|
||||||
|
if robotEmail, exists := robotData["robot_email"]; exists && robotEmail != nil && robotEmail != "" {
|
||||||
|
robotEmailStr := fmt.Sprintf("%v", robotEmail)
|
||||||
|
|
||||||
|
// Only check uniqueness if the email is actually changing
|
||||||
|
currentEmail, _ := existingMember["robot_email"]
|
||||||
|
if currentEmail != robotEmailStr {
|
||||||
|
m := model.Select(u.memberModel)
|
||||||
|
existingMembers, err := m.Get(model.QueryParam{
|
||||||
|
Select: []interface{}{"id", "member_id"},
|
||||||
|
Wheres: []model.QueryWhere{
|
||||||
|
{Column: "robot_email", Value: robotEmailStr},
|
||||||
|
},
|
||||||
|
Limit: 1,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check robot_email uniqueness: %w", err)
|
||||||
|
}
|
||||||
|
if len(existingMembers) > 0 {
|
||||||
|
// Check if it's not the same member
|
||||||
|
existingMemberID, _ := existingMembers[0]["member_id"]
|
||||||
|
if existingMemberID != memberID {
|
||||||
|
return fmt.Errorf("robot_email %s already exists", robotEmailStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
memberData := maps.MapStrAny{}
|
||||||
|
|
||||||
|
// Copy shared profile fields (used by both users and robots)
|
||||||
|
profileFields := []string{
|
||||||
|
"display_name", "bio", "avatar", "email",
|
||||||
|
}
|
||||||
|
for _, field := range profileFields {
|
||||||
|
if value, exists := robotData[field]; exists {
|
||||||
|
memberData[field] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy robot-specific fields
|
||||||
|
robotFields := []string{
|
||||||
|
"role_id", "system_prompt", "manager_id", "robot_email", "authorized_senders", "email_filter_rules",
|
||||||
|
"robot_config", "agents", "mcp_servers",
|
||||||
|
"language_model", "cost_limit", "autonomous_mode", "robot_status",
|
||||||
|
"notes", "metadata", "status",
|
||||||
|
"__yao_updated_by", "__yao_team_id", "__yao_tenant_id",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, field := range robotFields {
|
||||||
|
if value, exists := robotData[field]; exists {
|
||||||
|
memberData[field] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip update if no valid fields to update
|
||||||
|
if len(memberData) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.UpdateMemberByMemberID(ctx, memberID, memberData)
|
||||||
|
}
|
||||||
|
|
||||||
// AddMember adds a user to a team (invitation-based)
|
// AddMember adds a user to a team (invitation-based)
|
||||||
func (u *DefaultUser) AddMember(ctx context.Context, teamID string, userID string, roleID string, invitedBy string) (string, error) {
|
func (u *DefaultUser) AddMember(ctx context.Context, teamID string, userID string, roleID string, invitedBy string) (string, error) {
|
||||||
// Check if member already exists
|
// Check if member already exists
|
||||||
|
|
|
||||||
|
|
@ -1240,6 +1240,233 @@ func TestMemberExistsByRobotEmail(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUpdateRobotMember(t *testing.T) {
|
||||||
|
prepare(t)
|
||||||
|
defer clean()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Use UUID to ensure unique identifiers
|
||||||
|
testUUID := strings.ReplaceAll(uuid.New().String(), "-", "")[:8]
|
||||||
|
|
||||||
|
// Create test user (team owner)
|
||||||
|
ownerUser := createTestUser(ctx, t, "owner"+testUUID)
|
||||||
|
regularUser := createTestUser(ctx, t, "user"+testUUID)
|
||||||
|
|
||||||
|
// Create test team
|
||||||
|
teamMap := maps.MapStrAny{
|
||||||
|
"name": "Update Robot Test Team " + testUUID,
|
||||||
|
"display_name": "Update Robot Test " + testUUID,
|
||||||
|
"description": "A test team for robot update testing",
|
||||||
|
"owner_id": ownerUser,
|
||||||
|
"status": "active",
|
||||||
|
}
|
||||||
|
|
||||||
|
teamID, err := testProvider.CreateTeam(ctx, teamMap)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
var robotMemberID string
|
||||||
|
var regularMemberID string
|
||||||
|
|
||||||
|
// Create a robot member
|
||||||
|
t.Run("Setup_CreateRobotMember", func(t *testing.T) {
|
||||||
|
robotData := maps.MapStrAny{
|
||||||
|
"display_name": "TestBot" + testUUID,
|
||||||
|
"bio": "Original bio",
|
||||||
|
"role_id": "bot",
|
||||||
|
"autonomous_mode": false,
|
||||||
|
"robot_status": "idle",
|
||||||
|
"system_prompt": "Original prompt",
|
||||||
|
"language_model": "gpt-3.5-turbo",
|
||||||
|
"cost_limit": 50.00,
|
||||||
|
"robot_email": "testbot" + testUUID + "@robot.example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
memberID, err := testProvider.CreateRobotMember(ctx, teamID, robotData)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, memberID)
|
||||||
|
robotMemberID = memberID
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create a regular user member for testing
|
||||||
|
t.Run("Setup_CreateRegularMember", func(t *testing.T) {
|
||||||
|
memberData := maps.MapStrAny{
|
||||||
|
"team_id": teamID,
|
||||||
|
"user_id": regularUser,
|
||||||
|
"member_type": "user",
|
||||||
|
"role_id": "user",
|
||||||
|
"status": "active",
|
||||||
|
}
|
||||||
|
|
||||||
|
memberID, err := testProvider.CreateMember(ctx, memberData)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, memberID)
|
||||||
|
regularMemberID = memberID
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test successful update of robot member
|
||||||
|
t.Run("UpdateRobotMember_Success", func(t *testing.T) {
|
||||||
|
updateData := maps.MapStrAny{
|
||||||
|
"display_name": "UpdatedBot" + testUUID,
|
||||||
|
"bio": "Updated bio",
|
||||||
|
"autonomous_mode": true,
|
||||||
|
"robot_status": "working",
|
||||||
|
"system_prompt": "Updated prompt",
|
||||||
|
"language_model": "gpt-4",
|
||||||
|
"cost_limit": 100.00,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := testProvider.UpdateRobotMember(ctx, robotMemberID, updateData)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify updates
|
||||||
|
member, err := testProvider.GetMemberDetailByMemberID(ctx, robotMemberID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "UpdatedBot"+testUUID, member["display_name"])
|
||||||
|
assert.Equal(t, "Updated bio", member["bio"])
|
||||||
|
assert.Equal(t, "working", member["robot_status"])
|
||||||
|
assert.Equal(t, "Updated prompt", member["system_prompt"])
|
||||||
|
assert.Equal(t, "gpt-4", member["language_model"])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test updating robot_email
|
||||||
|
t.Run("UpdateRobotMember_UpdateEmail", func(t *testing.T) {
|
||||||
|
newEmail := "updated-testbot" + testUUID + "@robot.example.com"
|
||||||
|
updateData := maps.MapStrAny{
|
||||||
|
"robot_email": newEmail,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := testProvider.UpdateRobotMember(ctx, robotMemberID, updateData)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify update
|
||||||
|
member, err := testProvider.GetMemberDetailByMemberID(ctx, robotMemberID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, newEmail, member["robot_email"])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test updating robot configuration fields
|
||||||
|
t.Run("UpdateRobotMember_UpdateConfiguration", func(t *testing.T) {
|
||||||
|
updateData := maps.MapStrAny{
|
||||||
|
"authorized_senders": []string{
|
||||||
|
"admin@example.com",
|
||||||
|
"manager@example.com",
|
||||||
|
},
|
||||||
|
"email_filter_rules": []string{
|
||||||
|
".*@example\\.com$",
|
||||||
|
".*@test\\.com$",
|
||||||
|
},
|
||||||
|
"robot_config": map[string]interface{}{
|
||||||
|
"max_tokens": 2000,
|
||||||
|
"temperature": 0.7,
|
||||||
|
},
|
||||||
|
"agents": []string{
|
||||||
|
"agent1",
|
||||||
|
"agent2",
|
||||||
|
},
|
||||||
|
"mcp_servers": []string{
|
||||||
|
"mcp://server1",
|
||||||
|
"mcp://server2",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := testProvider.UpdateRobotMember(ctx, robotMemberID, updateData)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify update
|
||||||
|
member, err := testProvider.GetMemberDetailByMemberID(ctx, robotMemberID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, member["authorized_senders"])
|
||||||
|
assert.NotNil(t, member["email_filter_rules"])
|
||||||
|
assert.NotNil(t, member["robot_config"])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test error when trying to update non-robot member
|
||||||
|
t.Run("UpdateRobotMember_NotRobotMember", func(t *testing.T) {
|
||||||
|
updateData := maps.MapStrAny{
|
||||||
|
"display_name": "Should Fail",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := testProvider.UpdateRobotMember(ctx, regularMemberID, updateData)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "not a robot member")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test error when member_id doesn't exist
|
||||||
|
t.Run("UpdateRobotMember_NotFound", func(t *testing.T) {
|
||||||
|
updateData := maps.MapStrAny{
|
||||||
|
"display_name": "Should Fail",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := testProvider.UpdateRobotMember(ctx, "non-existent-member-id", updateData)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "failed to get member")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test robot_email uniqueness validation during update
|
||||||
|
t.Run("UpdateRobotMember_EmailUniqueness", func(t *testing.T) {
|
||||||
|
// Create another robot with a different email
|
||||||
|
anotherRobotData := maps.MapStrAny{
|
||||||
|
"display_name": "AnotherBot" + testUUID,
|
||||||
|
"role_id": "bot",
|
||||||
|
"robot_email": "anotherbot" + testUUID + "@robot.example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := testProvider.CreateRobotMember(ctx, teamID, anotherRobotData)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Try to update the first robot's email to match the second robot's email
|
||||||
|
updateData := maps.MapStrAny{
|
||||||
|
"robot_email": "anotherbot" + testUUID + "@robot.example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
err = testProvider.UpdateRobotMember(ctx, robotMemberID, updateData)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "already exists")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test updating with same email (should succeed - no actual change)
|
||||||
|
t.Run("UpdateRobotMember_SameEmail", func(t *testing.T) {
|
||||||
|
// Get current email
|
||||||
|
member, err := testProvider.GetMemberDetailByMemberID(ctx, robotMemberID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
currentEmail := member["robot_email"]
|
||||||
|
|
||||||
|
// Update with same email
|
||||||
|
updateData := maps.MapStrAny{
|
||||||
|
"robot_email": currentEmail,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = testProvider.UpdateRobotMember(ctx, robotMemberID, updateData)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test update with empty data (should not error)
|
||||||
|
t.Run("UpdateRobotMember_EmptyData", func(t *testing.T) {
|
||||||
|
err := testProvider.UpdateRobotMember(ctx, robotMemberID, maps.MapStrAny{})
|
||||||
|
assert.NoError(t, err) // Should not error, just do nothing
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test updating status
|
||||||
|
t.Run("UpdateRobotMember_UpdateStatus", func(t *testing.T) {
|
||||||
|
updateData := maps.MapStrAny{
|
||||||
|
"status": "inactive",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := testProvider.UpdateRobotMember(ctx, robotMemberID, updateData)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify update
|
||||||
|
member, err := testProvider.GetMemberByMemberID(ctx, robotMemberID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "inactive", member["status"])
|
||||||
|
|
||||||
|
// Restore to active
|
||||||
|
err = testProvider.UpdateRobotMember(ctx, robotMemberID, maps.MapStrAny{"status": "active"})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestRobotEmailUniqueness(t *testing.T) {
|
func TestRobotEmailUniqueness(t *testing.T) {
|
||||||
prepare(t)
|
prepare(t)
|
||||||
defer clean()
|
defer clean()
|
||||||
|
|
|
||||||
|
|
@ -317,6 +317,7 @@ type UserProvider interface {
|
||||||
|
|
||||||
// Robot Member Operations
|
// Robot Member Operations
|
||||||
CreateRobotMember(ctx context.Context, teamID string, robotData maps.MapStrAny) (string, error)
|
CreateRobotMember(ctx context.Context, teamID string, robotData maps.MapStrAny) (string, error)
|
||||||
|
UpdateRobotMember(ctx context.Context, memberID string, robotData maps.MapStrAny) error
|
||||||
UpdateRobotActivity(ctx context.Context, memberID int64, robotStatus string) error
|
UpdateRobotActivity(ctx context.Context, memberID int64, robotStatus string) error
|
||||||
GetActiveRobotMembers(ctx context.Context) ([]maps.MapStr, error)
|
GetActiveRobotMembers(ctx context.Context) ([]maps.MapStr, error)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1555,4 +1555,416 @@ func TestMemberCheckRobotEmail(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestMemberUpdateRobot tests the PUT /user/teams/:team_id/members/robots/:member_id endpoint
|
||||||
|
func TestMemberUpdateRobot(t *testing.T) {
|
||||||
|
// Initialize test environment
|
||||||
|
serverURL := testutils.Prepare(t)
|
||||||
|
defer testutils.Clean()
|
||||||
|
|
||||||
|
// Get base URL from server config
|
||||||
|
baseURL := ""
|
||||||
|
if openapi.Server != nil && openapi.Server.Config != nil {
|
||||||
|
baseURL = openapi.Server.Config.BaseURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register a test client for OAuth authentication
|
||||||
|
testClient := testutils.RegisterTestClient(t, "Robot Member Update Test Client", []string{"https://localhost/callback"})
|
||||||
|
defer testutils.CleanupTestClient(t, testClient.ClientID)
|
||||||
|
|
||||||
|
// Obtain access token with root permissions (required for robot operations)
|
||||||
|
tokenInfo := testutils.ObtainAccessTokenWithRootPermission(t, serverURL, testClient.ClientID, testClient.ClientSecret, "https://localhost/callback", "openid profile")
|
||||||
|
|
||||||
|
// Use UUID to ensure unique test data
|
||||||
|
testUUID := strings.ReplaceAll(uuid.New().String(), "-", "")[:8]
|
||||||
|
|
||||||
|
// Create a test team
|
||||||
|
createdTeam := createTestTeam(t, serverURL, baseURL, tokenInfo.AccessToken, "Robot Update Test Team "+testUUID)
|
||||||
|
teamID := getTeamID(createdTeam)
|
||||||
|
|
||||||
|
// Helper function to create a robot member for testing
|
||||||
|
createTestRobot := func(suffix string) (string, string) {
|
||||||
|
robotEmail := fmt.Sprintf("test-robot-%s-%s@robot.test.com", testUUID, suffix)
|
||||||
|
robotBody := map[string]interface{}{
|
||||||
|
"name": "Test Robot " + suffix,
|
||||||
|
"robot_email": robotEmail,
|
||||||
|
"email": fmt.Sprintf("display-%s-%s@test.com", testUUID, suffix),
|
||||||
|
"role": "member",
|
||||||
|
"prompt": "Original prompt for " + suffix,
|
||||||
|
"llm": "gpt-3.5-turbo",
|
||||||
|
"autonomous_mode": "disabled",
|
||||||
|
"cost_limit": 50.0,
|
||||||
|
}
|
||||||
|
robotBodyBytes, _ := json.Marshal(robotBody)
|
||||||
|
robotReq, _ := http.NewRequest("POST", serverURL+baseURL+"/user/teams/"+teamID+"/members/robots", bytes.NewBuffer(robotBodyBytes))
|
||||||
|
robotReq.Header.Set("Content-Type", "application/json")
|
||||||
|
robotReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||||
|
client := &http.Client{}
|
||||||
|
robotResp, err := client.Do(robotReq)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if robotResp != nil {
|
||||||
|
defer robotResp.Body.Close()
|
||||||
|
assert.Equal(t, 201, robotResp.StatusCode, "Should create robot member successfully")
|
||||||
|
body, _ := io.ReadAll(robotResp.Body)
|
||||||
|
var response map[string]interface{}
|
||||||
|
json.Unmarshal(body, &response)
|
||||||
|
return toString(response["member_id"]), robotEmail
|
||||||
|
}
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
setupFunc func() (string, string) // Returns (memberID, originalRobotEmail)
|
||||||
|
body map[string]interface{}
|
||||||
|
headers map[string]string
|
||||||
|
expectCode int
|
||||||
|
expectMsg string
|
||||||
|
validateFn func(*testing.T, string) // Optional validation function with memberID
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"update robot without authentication",
|
||||||
|
func() (string, string) { return createTestRobot("1") },
|
||||||
|
map[string]interface{}{
|
||||||
|
"name": "Updated Name",
|
||||||
|
},
|
||||||
|
map[string]string{},
|
||||||
|
401,
|
||||||
|
"should require authentication",
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"update robot with all fields",
|
||||||
|
func() (string, string) { return createTestRobot("2") },
|
||||||
|
map[string]interface{}{
|
||||||
|
"name": "Updated Robot Full",
|
||||||
|
"email": fmt.Sprintf("updated-display-%s@test.com", testUUID),
|
||||||
|
"robot_email": fmt.Sprintf("updated-robot-%s@robot.test.com", testUUID),
|
||||||
|
"bio": "Updated comprehensive description",
|
||||||
|
"role": "admin",
|
||||||
|
"report_to": tokenInfo.UserID,
|
||||||
|
"prompt": "Updated system prompt",
|
||||||
|
"llm": "gpt-4",
|
||||||
|
"agents": []string{"agent1", "agent2"},
|
||||||
|
"mcp_tools": []string{"tool1", "tool2"},
|
||||||
|
"authorized_senders": []string{"admin@test.com"},
|
||||||
|
"email_filter_rules": []string{".*@test\\.com$"},
|
||||||
|
"autonomous_mode": "enabled",
|
||||||
|
"cost_limit": 100.0,
|
||||||
|
"status": "active",
|
||||||
|
"robot_status": "working",
|
||||||
|
},
|
||||||
|
map[string]string{
|
||||||
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
|
},
|
||||||
|
200,
|
||||||
|
"should update robot with all fields successfully",
|
||||||
|
func(t *testing.T, memberID string) {
|
||||||
|
// Verify the update
|
||||||
|
getMemberURL := serverURL + baseURL + "/user/teams/" + teamID + "/members/" + memberID
|
||||||
|
getReq, _ := http.NewRequest("GET", getMemberURL, nil)
|
||||||
|
getReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||||
|
client := &http.Client{}
|
||||||
|
getResp, err := client.Do(getReq)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if getResp != nil {
|
||||||
|
defer getResp.Body.Close()
|
||||||
|
if getResp.StatusCode == 200 {
|
||||||
|
var member map[string]interface{}
|
||||||
|
body, _ := io.ReadAll(getResp.Body)
|
||||||
|
json.Unmarshal(body, &member)
|
||||||
|
assert.Equal(t, "Updated Robot Full", member["display_name"])
|
||||||
|
assert.Equal(t, "Updated system prompt", member["system_prompt"])
|
||||||
|
assert.Equal(t, "gpt-4", member["language_model"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"update robot with partial fields",
|
||||||
|
func() (string, string) { return createTestRobot("3") },
|
||||||
|
map[string]interface{}{
|
||||||
|
"name": "Partially Updated Robot",
|
||||||
|
"prompt": "Partially updated prompt",
|
||||||
|
},
|
||||||
|
map[string]string{
|
||||||
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
|
},
|
||||||
|
200,
|
||||||
|
"should update robot with partial fields",
|
||||||
|
func(t *testing.T, memberID string) {
|
||||||
|
getMemberURL := serverURL + baseURL + "/user/teams/" + teamID + "/members/" + memberID
|
||||||
|
getReq, _ := http.NewRequest("GET", getMemberURL, nil)
|
||||||
|
getReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||||
|
client := &http.Client{}
|
||||||
|
getResp, err := client.Do(getReq)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if getResp != nil {
|
||||||
|
defer getResp.Body.Close()
|
||||||
|
if getResp.StatusCode == 200 {
|
||||||
|
var member map[string]interface{}
|
||||||
|
body, _ := io.ReadAll(getResp.Body)
|
||||||
|
json.Unmarshal(body, &member)
|
||||||
|
assert.Equal(t, "Partially Updated Robot", member["display_name"])
|
||||||
|
assert.Equal(t, "Partially updated prompt", member["system_prompt"])
|
||||||
|
// Original fields should remain
|
||||||
|
assert.Equal(t, "gpt-3.5-turbo", member["language_model"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"update robot_email to new unique email",
|
||||||
|
func() (string, string) { return createTestRobot("4") },
|
||||||
|
map[string]interface{}{
|
||||||
|
"robot_email": fmt.Sprintf("new-unique-%s@robot.test.com", testUUID),
|
||||||
|
},
|
||||||
|
map[string]string{
|
||||||
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
|
},
|
||||||
|
200,
|
||||||
|
"should update robot_email to new unique email",
|
||||||
|
func(t *testing.T, memberID string) {
|
||||||
|
getMemberURL := serverURL + baseURL + "/user/teams/" + teamID + "/members/" + memberID
|
||||||
|
getReq, _ := http.NewRequest("GET", getMemberURL, nil)
|
||||||
|
getReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||||
|
client := &http.Client{}
|
||||||
|
getResp, err := client.Do(getReq)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if getResp != nil {
|
||||||
|
defer getResp.Body.Close()
|
||||||
|
if getResp.StatusCode == 200 {
|
||||||
|
var member map[string]interface{}
|
||||||
|
body, _ := io.ReadAll(getResp.Body)
|
||||||
|
json.Unmarshal(body, &member)
|
||||||
|
assert.Equal(t, fmt.Sprintf("new-unique-%s@robot.test.com", testUUID), member["robot_email"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"update robot_email to duplicate email",
|
||||||
|
func() (string, string) {
|
||||||
|
// Create two robots
|
||||||
|
memberID1, email1 := createTestRobot("5a")
|
||||||
|
_, _ = createTestRobot("5b")
|
||||||
|
return memberID1, email1
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"robot_email": fmt.Sprintf("test-robot-%s-5b@robot.test.com", testUUID),
|
||||||
|
},
|
||||||
|
map[string]string{
|
||||||
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
|
},
|
||||||
|
409,
|
||||||
|
"should reject duplicate robot_email",
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"update autonomous_mode variations",
|
||||||
|
func() (string, string) { return createTestRobot("6") },
|
||||||
|
map[string]interface{}{
|
||||||
|
"autonomous_mode": "1",
|
||||||
|
},
|
||||||
|
map[string]string{
|
||||||
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
|
},
|
||||||
|
200,
|
||||||
|
"should handle autonomous_mode=1",
|
||||||
|
func(t *testing.T, memberID string) {
|
||||||
|
getMemberURL := serverURL + baseURL + "/user/teams/" + teamID + "/members/" + memberID
|
||||||
|
getReq, _ := http.NewRequest("GET", getMemberURL, nil)
|
||||||
|
getReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||||
|
client := &http.Client{}
|
||||||
|
getResp, err := client.Do(getReq)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if getResp != nil {
|
||||||
|
defer getResp.Body.Close()
|
||||||
|
if getResp.StatusCode == 200 {
|
||||||
|
var member map[string]interface{}
|
||||||
|
body, _ := io.ReadAll(getResp.Body)
|
||||||
|
json.Unmarshal(body, &member)
|
||||||
|
// autonomous_mode should be enabled
|
||||||
|
autonomousMode := member["autonomous_mode"]
|
||||||
|
assert.True(t, autonomousMode == true || autonomousMode == float64(1) || autonomousMode == int64(1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"update robot status",
|
||||||
|
func() (string, string) { return createTestRobot("7") },
|
||||||
|
map[string]interface{}{
|
||||||
|
"status": "inactive",
|
||||||
|
"robot_status": "error",
|
||||||
|
},
|
||||||
|
map[string]string{
|
||||||
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
|
},
|
||||||
|
200,
|
||||||
|
"should update robot status fields",
|
||||||
|
func(t *testing.T, memberID string) {
|
||||||
|
getMemberURL := serverURL + baseURL + "/user/teams/" + teamID + "/members/" + memberID
|
||||||
|
getReq, _ := http.NewRequest("GET", getMemberURL, nil)
|
||||||
|
getReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||||
|
client := &http.Client{}
|
||||||
|
getResp, err := client.Do(getReq)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if getResp != nil {
|
||||||
|
defer getResp.Body.Close()
|
||||||
|
if getResp.StatusCode == 200 {
|
||||||
|
var member map[string]interface{}
|
||||||
|
body, _ := io.ReadAll(getResp.Body)
|
||||||
|
json.Unmarshal(body, &member)
|
||||||
|
assert.Equal(t, "inactive", member["status"])
|
||||||
|
assert.Equal(t, "error", member["robot_status"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"update array fields",
|
||||||
|
func() (string, string) { return createTestRobot("8") },
|
||||||
|
map[string]interface{}{
|
||||||
|
"agents": []string{"new-agent1", "new-agent2", "new-agent3"},
|
||||||
|
"mcp_tools": []string{"new-tool1"},
|
||||||
|
"authorized_senders": []string{"sender1@test.com", "sender2@test.com"},
|
||||||
|
"email_filter_rules": []string{".*@allowed\\.com$"},
|
||||||
|
},
|
||||||
|
map[string]string{
|
||||||
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
|
},
|
||||||
|
200,
|
||||||
|
"should update array fields",
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"update non-existent robot",
|
||||||
|
func() (string, string) { return "non-existent-member-id", "" },
|
||||||
|
map[string]interface{}{
|
||||||
|
"name": "Should Fail",
|
||||||
|
},
|
||||||
|
map[string]string{
|
||||||
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
|
},
|
||||||
|
404,
|
||||||
|
"should return not found for non-existent robot",
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"update regular user member as robot",
|
||||||
|
func() (string, string) {
|
||||||
|
// Create a regular user member instead of robot
|
||||||
|
memberID := createTestMember(t, serverURL, baseURL, teamID, tokenInfo.AccessToken, "regular-user-"+testUUID)
|
||||||
|
return memberID, ""
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"name": "Should Fail",
|
||||||
|
},
|
||||||
|
map[string]string{
|
||||||
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
|
},
|
||||||
|
400,
|
||||||
|
"should reject updating non-robot member",
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"update robot in non-existent team",
|
||||||
|
func() (string, string) { return createTestRobot("10") },
|
||||||
|
map[string]interface{}{
|
||||||
|
"name": "Should Fail",
|
||||||
|
},
|
||||||
|
map[string]string{
|
||||||
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
|
},
|
||||||
|
404,
|
||||||
|
"should return not found for non-existent team",
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"update robot with invalid JSON",
|
||||||
|
func() (string, string) { return createTestRobot("11") },
|
||||||
|
nil, // Will send invalid JSON
|
||||||
|
map[string]string{
|
||||||
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
|
},
|
||||||
|
400,
|
||||||
|
"should handle invalid JSON",
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"update robot with empty body",
|
||||||
|
func() (string, string) { return createTestRobot("12") },
|
||||||
|
map[string]interface{}{},
|
||||||
|
map[string]string{
|
||||||
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
|
},
|
||||||
|
200,
|
||||||
|
"should handle empty update (no-op)",
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
memberID, _ := tc.setupFunc()
|
||||||
|
|
||||||
|
// Use non-existent team ID for the specific test case
|
||||||
|
targetTeamID := teamID
|
||||||
|
if tc.name == "update robot in non-existent team" {
|
||||||
|
targetTeamID = "non-existent-team-id"
|
||||||
|
}
|
||||||
|
|
||||||
|
requestURL := serverURL + baseURL + "/user/teams/" + targetTeamID + "/members/robots/" + memberID
|
||||||
|
|
||||||
|
var req *http.Request
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if tc.body == nil {
|
||||||
|
// Send invalid JSON for invalid JSON test case
|
||||||
|
req, err = http.NewRequest("PUT", requestURL, bytes.NewBufferString("invalid json"))
|
||||||
|
} else {
|
||||||
|
bodyBytes, _ := json.Marshal(tc.body)
|
||||||
|
req, err = http.NewRequest("PUT", requestURL, bytes.NewBuffer(bodyBytes))
|
||||||
|
}
|
||||||
|
assert.NoError(t, err, "Should create HTTP request")
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
// Add headers
|
||||||
|
for key, value := range tc.headers {
|
||||||
|
req.Header.Set(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
assert.NoError(t, err, "HTTP request should succeed")
|
||||||
|
|
||||||
|
if resp != nil {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
assert.Equal(t, tc.expectCode, resp.StatusCode, "Expected status code %d for %s", tc.expectCode, tc.name)
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
assert.NoError(t, err, "Should read response body")
|
||||||
|
|
||||||
|
if resp.StatusCode == 200 {
|
||||||
|
// Parse response as success message
|
||||||
|
var response map[string]interface{}
|
||||||
|
err = json.Unmarshal(body, &response)
|
||||||
|
assert.NoError(t, err, "Should parse JSON response")
|
||||||
|
|
||||||
|
assert.Contains(t, response, "message", "Should have success message")
|
||||||
|
assert.Equal(t, "Robot member updated successfully", response["message"], "Should have correct success message")
|
||||||
|
|
||||||
|
// Run custom validation if provided
|
||||||
|
if tc.validateFn != nil {
|
||||||
|
tc.validateFn(t, memberID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Robot member update test %s: status=%d, body=%s", tc.name, resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Note: getTeamID function is already defined in team_test.go
|
// Note: getTeamID function is already defined in team_test.go
|
||||||
|
|
|
||||||
|
|
@ -352,6 +352,142 @@ func GinMemberCreateRobot(c *gin.Context) {
|
||||||
response.RespondWithSuccess(c, http.StatusCreated, gin.H{"member_id": memberID})
|
response.RespondWithSuccess(c, http.StatusCreated, gin.H{"member_id": memberID})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GinMemberUpdateRobot handles PUT /teams/:team_id/members/robots/:member_id - Update robot member
|
||||||
|
func GinMemberUpdateRobot(c *gin.Context) {
|
||||||
|
// Get authorized user info
|
||||||
|
authInfo := authorized.GetInfo(c)
|
||||||
|
if authInfo == nil || authInfo.UserID == "" {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrInvalidClient.Code,
|
||||||
|
ErrorDescription: "User not authenticated",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusUnauthorized, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
teamID := c.Param("id")
|
||||||
|
memberID := c.Param("member_id")
|
||||||
|
if teamID == "" || memberID == "" {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrInvalidRequest.Code,
|
||||||
|
ErrorDescription: "Team ID and Member ID are required",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse request body
|
||||||
|
var req UpdateRobotMemberRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrInvalidRequest.Code,
|
||||||
|
ErrorDescription: "Invalid request body: " + err.Error(),
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare update data
|
||||||
|
updateData := maps.MapStrAny{}
|
||||||
|
|
||||||
|
// Add fields if provided
|
||||||
|
if req.Name != "" {
|
||||||
|
updateData["display_name"] = req.Name
|
||||||
|
}
|
||||||
|
if req.Email != "" {
|
||||||
|
updateData["email"] = req.Email
|
||||||
|
}
|
||||||
|
if req.RobotEmail != "" {
|
||||||
|
updateData["robot_email"] = req.RobotEmail
|
||||||
|
}
|
||||||
|
if req.Bio != "" {
|
||||||
|
updateData["bio"] = req.Bio
|
||||||
|
}
|
||||||
|
if req.RoleID != "" {
|
||||||
|
updateData["role_id"] = req.RoleID
|
||||||
|
}
|
||||||
|
if req.ManagerID != "" {
|
||||||
|
updateData["manager_id"] = req.ManagerID
|
||||||
|
}
|
||||||
|
if req.SystemPrompt != "" {
|
||||||
|
updateData["system_prompt"] = req.SystemPrompt
|
||||||
|
}
|
||||||
|
if req.LanguageModel != "" {
|
||||||
|
updateData["language_model"] = req.LanguageModel
|
||||||
|
}
|
||||||
|
if req.Status != "" {
|
||||||
|
updateData["status"] = req.Status
|
||||||
|
}
|
||||||
|
if req.RobotStatus != "" {
|
||||||
|
updateData["robot_status"] = req.RobotStatus
|
||||||
|
}
|
||||||
|
if req.AutonomousMode != "" {
|
||||||
|
updateData["autonomous_mode"] = toBool(req.AutonomousMode)
|
||||||
|
}
|
||||||
|
if req.CostLimit > 0 {
|
||||||
|
updateData["cost_limit"] = req.CostLimit
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle array fields (they can be empty arrays)
|
||||||
|
if req.AuthorizedSenders != nil {
|
||||||
|
updateData["authorized_senders"] = req.AuthorizedSenders
|
||||||
|
}
|
||||||
|
if req.EmailFilterRules != nil {
|
||||||
|
updateData["email_filter_rules"] = req.EmailFilterRules
|
||||||
|
}
|
||||||
|
if req.Agents != nil {
|
||||||
|
updateData["agents"] = req.Agents
|
||||||
|
}
|
||||||
|
if req.MCPServers != nil {
|
||||||
|
updateData["mcp_servers"] = req.MCPServers
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap with update scope for permission tracking
|
||||||
|
robotData := authInfo.WithUpdateScope(updateData)
|
||||||
|
|
||||||
|
// Call business logic
|
||||||
|
err := memberUpdateRobot(c.Request.Context(), authInfo.UserID, teamID, memberID, robotData)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to update robot member: %v", err)
|
||||||
|
// Check error type for appropriate response
|
||||||
|
if strings.Contains(err.Error(), "not found") {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrInvalidRequest.Code,
|
||||||
|
ErrorDescription: err.Error(),
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
||||||
|
} else if strings.Contains(err.Error(), "access denied") {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrAccessDenied.Code,
|
||||||
|
ErrorDescription: err.Error(),
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusForbidden, errorResp)
|
||||||
|
} else if strings.Contains(err.Error(), "not a robot member") {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrInvalidRequest.Code,
|
||||||
|
ErrorDescription: err.Error(),
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
||||||
|
} else if strings.Contains(err.Error(), "already exists") || strings.Contains(err.Error(), "duplicate") {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrInvalidRequest.Code,
|
||||||
|
ErrorDescription: err.Error(),
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusConflict, errorResp)
|
||||||
|
} else {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrServerError.Code,
|
||||||
|
ErrorDescription: "Failed to update robot member",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return success
|
||||||
|
response.RespondWithSuccess(c, http.StatusOK, gin.H{"message": "Robot member updated successfully"})
|
||||||
|
}
|
||||||
|
|
||||||
// GinMemberUpdate handles PUT /teams/:team_id/members/:member_id - Update team member
|
// GinMemberUpdate handles PUT /teams/:team_id/members/:member_id - Update team member
|
||||||
func GinMemberUpdate(c *gin.Context) {
|
func GinMemberUpdate(c *gin.Context) {
|
||||||
// Get authorized user info
|
// Get authorized user info
|
||||||
|
|
@ -921,6 +1057,34 @@ func memberCreateRobot(ctx context.Context, userID, teamID string, robotData map
|
||||||
return memberID, nil
|
return memberID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// memberUpdateRobot handles the business logic for updating a robot member
|
||||||
|
func memberUpdateRobot(ctx context.Context, userID, teamID, memberID string, robotData maps.MapStrAny) error {
|
||||||
|
// Check if user has access to the team (write permission: owner only)
|
||||||
|
isOwner, _, err := checkTeamAccess(ctx, teamID, userID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only allow access if user is owner
|
||||||
|
if !isOwner {
|
||||||
|
return fmt.Errorf("access denied: only team owner can update robot members")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user provider instance
|
||||||
|
provider, err := getUserProvider()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get user provider: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use UpdateRobotMember method which handles robot-specific logic and validation
|
||||||
|
err = provider.UpdateRobotMember(ctx, memberID, robotData)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to update robot member: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// memberUpdate handles the business logic for updating a team member
|
// memberUpdate handles the business logic for updating a team member
|
||||||
func memberUpdate(ctx context.Context, userID, teamID, memberID string, updateData maps.MapStrAny) error {
|
func memberUpdate(ctx context.Context, userID, teamID, memberID string, updateData maps.MapStrAny) error {
|
||||||
// Check if user has access to the team (write permission: owner only)
|
// Check if user has access to the team (write permission: owner only)
|
||||||
|
|
|
||||||
|
|
@ -460,6 +460,26 @@ type CreateRobotMemberRequest struct {
|
||||||
CostLimit float64 `json:"cost_limit,omitempty"` // Monthly cost limit in USD
|
CostLimit float64 `json:"cost_limit,omitempty"` // Monthly cost limit in USD
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateRobotMemberRequest represents the request to update a robot member
|
||||||
|
type UpdateRobotMemberRequest struct {
|
||||||
|
Name string `json:"name,omitempty"` // Display name
|
||||||
|
Email string `json:"email,omitempty"` // Email address (optional, for display only)
|
||||||
|
RobotEmail string `json:"robot_email,omitempty"` // Robot's globally unique email address
|
||||||
|
AuthorizedSenders []string `json:"authorized_senders,omitempty"` // Whitelist of emails authorized to send commands
|
||||||
|
EmailFilterRules []string `json:"email_filter_rules,omitempty"` // Email filtering rules (supports regex patterns)
|
||||||
|
Bio string `json:"bio,omitempty"` // Bio/description
|
||||||
|
RoleID string `json:"role,omitempty"` // Role ID
|
||||||
|
ManagerID string `json:"report_to,omitempty"` // Direct manager user ID
|
||||||
|
SystemPrompt string `json:"prompt,omitempty"` // Identity & role prompt
|
||||||
|
LanguageModel string `json:"llm,omitempty"` // Language model (e.g., "gpt-4")
|
||||||
|
Agents []string `json:"agents,omitempty"` // Accessible agents
|
||||||
|
MCPServers []string `json:"mcp_tools,omitempty"` // MCP servers/tools
|
||||||
|
AutonomousMode string `json:"autonomous_mode,omitempty"` // "enabled" or "disabled"
|
||||||
|
CostLimit float64 `json:"cost_limit,omitempty"` // Monthly cost limit in USD
|
||||||
|
Status string `json:"status,omitempty"` // Status: active, inactive
|
||||||
|
RobotStatus string `json:"robot_status,omitempty"` // Robot status: idle, working, error
|
||||||
|
}
|
||||||
|
|
||||||
// MemberListRequest represents the request to list team members with advanced filtering
|
// MemberListRequest represents the request to list team members with advanced filtering
|
||||||
type MemberListRequest struct {
|
type MemberListRequest struct {
|
||||||
// Pagination
|
// Pagination
|
||||||
|
|
|
||||||
|
|
@ -136,6 +136,7 @@ func attachTeam(group *gin.RouterGroup, oauth types.OAuth) {
|
||||||
team.GET("/:id/members", GinMemberList) // GET /api/user/teams/:id/members - List team members
|
team.GET("/:id/members", GinMemberList) // GET /api/user/teams/:id/members - List team members
|
||||||
team.GET("/:id/members/check-robot-email", GinMemberCheckRobotEmail) // GET /api/user/teams/:id/members/check-robot-email?robot_email=xxx - Check if robot email exists globally
|
team.GET("/:id/members/check-robot-email", GinMemberCheckRobotEmail) // GET /api/user/teams/:id/members/check-robot-email?robot_email=xxx - Check if robot email exists globally
|
||||||
team.POST("/:id/members/robots", GinMemberCreateRobot) // POST /api/user/teams/:id/members/robots - Add robot member
|
team.POST("/:id/members/robots", GinMemberCreateRobot) // POST /api/user/teams/:id/members/robots - Add robot member
|
||||||
|
team.PUT("/:id/members/robots/:member_id", GinMemberUpdateRobot) // PUT /api/user/teams/:id/members/robots/:member_id - Update robot member
|
||||||
team.GET("/:id/members/:member_id", GinMemberGet) // GET /api/user/teams/:id/members/:member_id - Get member details
|
team.GET("/:id/members/:member_id", GinMemberGet) // GET /api/user/teams/:id/members/:member_id - Get member details
|
||||||
team.PUT("/:id/members/:member_id", GinMemberUpdate) // PUT /api/user/teams/:id/members/:member_id - Update member
|
team.PUT("/:id/members/:member_id", GinMemberUpdate) // PUT /api/user/teams/:id/members/:member_id - Update member
|
||||||
team.DELETE("/:id/members/:member_id", GinMemberDelete) // DELETE /api/user/teams/:id/members/:member_id - Remove member
|
team.DELETE("/:id/members/:member_id", GinMemberDelete) // DELETE /api/user/teams/:id/members/:member_id - Remove member
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue