diff --git a/openapi/tests/user/member_test.go b/openapi/tests/user/member_test.go index 39dc7f8a..cb223789 100644 --- a/openapi/tests/user/member_test.go +++ b/openapi/tests/user/member_test.go @@ -1137,6 +1137,7 @@ func TestMemberCreateRobot(t *testing.T) { teamID, map[string]interface{}{ "name": "AI Assistant Full", + "avatar": fmt.Sprintf("https://example.com/avatars/ai-full-%s.png", testUUID), "email": fmt.Sprintf("ai-full-%s@test.com", testUUID), "robot_email": fmt.Sprintf("robot-full-%s@robot.test.com", testUUID), "authorized_senders": []string{"user1@test.com", "user2@test.com"}, @@ -1637,6 +1638,7 @@ func TestMemberUpdateRobot(t *testing.T) { func() (string, string) { return createTestRobot("2") }, map[string]interface{}{ "name": "Updated Robot Full", + "avatar": fmt.Sprintf("https://example.com/avatars/full-%s.png", testUUID), "email": fmt.Sprintf("updated-display-%s@test.com", testUUID), "robot_email": fmt.Sprintf("updated-robot-%s@robot.test.com", testUUID), "bio": "Updated comprehensive description", @@ -1673,6 +1675,7 @@ func TestMemberUpdateRobot(t *testing.T) { body, _ := io.ReadAll(getResp.Body) json.Unmarshal(body, &member) assert.Equal(t, "Updated Robot Full", member["display_name"]) + assert.Equal(t, fmt.Sprintf("https://example.com/avatars/full-%s.png", testUUID), member["avatar"]) assert.Equal(t, "Updated system prompt", member["system_prompt"]) assert.Equal(t, "gpt-4", member["language_model"]) } @@ -1902,6 +1905,73 @@ func TestMemberUpdateRobot(t *testing.T) { "should handle empty update (no-op)", nil, }, + { + "update robot avatar", + func() (string, string) { return createTestRobot("13") }, + map[string]interface{}{ + "name": "Robot with Avatar", + "robot_email": fmt.Sprintf("robot-avatar-%s@robot.test.com", testUUID), + "avatar": fmt.Sprintf("https://example.com/avatars/robot-%s.png", testUUID), + }, + map[string]string{ + "Authorization": "Bearer " + tokenInfo.AccessToken, + }, + 200, + "should update robot avatar successfully", + func(t *testing.T, memberID string) { + // Verify the avatar was updated + 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, "Robot with Avatar", member["display_name"]) + assert.Equal(t, fmt.Sprintf("https://example.com/avatars/robot-%s.png", testUUID), member["avatar"], "Should have correct avatar URL") + } + } + }, + }, + { + "update only robot avatar", + func() (string, string) { return createTestRobot("14") }, + map[string]interface{}{ + "avatar": fmt.Sprintf("https://example.com/avatars/updated-%s.png", testUUID), + }, + map[string]string{ + "Authorization": "Bearer " + tokenInfo.AccessToken, + }, + 200, + "should update only avatar without affecting other fields", + func(t *testing.T, memberID string) { + // Verify only avatar was updated + 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) + // Avatar should be updated + assert.Equal(t, fmt.Sprintf("https://example.com/avatars/updated-%s.png", testUUID), member["avatar"], "Should have updated avatar URL") + // Original fields should remain + assert.Equal(t, "Test Robot 14", member["display_name"], "Name should remain unchanged") + assert.Equal(t, "gpt-3.5-turbo", member["language_model"], "LLM should remain unchanged") + } + } + }, + }, } for _, tc := range testCases { diff --git a/openapi/tests/user/team_config_robot_test.go b/openapi/tests/user/team_config_robot_test.go index a9f5ae6a..f1492831 100644 --- a/openapi/tests/user/team_config_robot_test.go +++ b/openapi/tests/user/team_config_robot_test.go @@ -105,6 +105,8 @@ func TestGetTeamConfigPublic(t *testing.T) { assert.Equal(t, originalConfig.Role, publicConfig.Role, "Role should be preserved") assert.Equal(t, originalConfig.Roles, publicConfig.Roles, "Roles should be preserved") assert.Equal(t, originalConfig.Invite, publicConfig.Invite, "Invite config should be preserved") + assert.Equal(t, originalConfig.Uploader, publicConfig.Uploader, "Uploader should be preserved (public field)") + assert.Equal(t, originalConfig.AvatarAgent, publicConfig.AvatarAgent, "AvatarAgent should be preserved (public field)") // Test robot configuration if originalConfig.Robot != nil { diff --git a/openapi/tests/user/team_config_test.go b/openapi/tests/user/team_config_test.go index 0f745d2a..b55f283b 100644 --- a/openapi/tests/user/team_config_test.go +++ b/openapi/tests/user/team_config_test.go @@ -51,6 +51,19 @@ func TestTeamConfigStructure(t *testing.T) { // Verify team config structure is valid assert.IsType(t, &user.TeamConfig{}, teamConfig, "Should return correct team config type") + // Test uploader field (public field) + if teamConfig.Uploader != "" { + t.Logf("Uploader configured: %s", teamConfig.Uploader) + assert.NotEmpty(t, teamConfig.Uploader, "Uploader should not be empty if set") + } + + // Test avatar_agent field (public field, optional) + if teamConfig.AvatarAgent != "" { + t.Logf("Avatar agent configured: %s", teamConfig.AvatarAgent) + } else { + t.Log("Avatar agent not configured (optional field)") + } + // Test roles configuration if teamConfig.Roles != nil { assert.IsType(t, []*user.TeamRole{}, teamConfig.Roles, "Roles should be slice of TeamRole pointers") @@ -163,6 +176,19 @@ func TestTeamConfigAPI(t *testing.T) { // Verify team config structure assert.IsType(t, &user.TeamConfig{}, &teamConfig, "Should return correct team config type") + // Test uploader field (public field) + if teamConfig.Uploader != "" { + t.Logf("API returned uploader: %s", teamConfig.Uploader) + assert.NotEmpty(t, teamConfig.Uploader, "Uploader should not be empty if set") + } + + // Test avatar_agent field (public field, optional) + if teamConfig.AvatarAgent != "" { + t.Logf("API returned avatar_agent: %s", teamConfig.AvatarAgent) + } else { + t.Log("API returned no avatar_agent (optional field)") + } + // Test roles if present if teamConfig.Roles != nil { assert.IsType(t, []*user.TeamRole{}, teamConfig.Roles, "Roles should be slice of TeamRole pointers") diff --git a/openapi/user/config.go b/openapi/user/config.go index 162437e8..4770252f 100644 --- a/openapi/user/config.go +++ b/openapi/user/config.go @@ -344,10 +344,12 @@ func GetTeamConfigPublic(locale string) *TeamConfig { // Create a deep copy of the config to avoid modifying the original publicConfig := &TeamConfig{ - Type: originalConfig.Type, - Role: originalConfig.Role, - Roles: originalConfig.Roles, // Shallow copy is OK for roles (read-only) - Invite: originalConfig.Invite, // Shallow copy is OK for invite config (read-only) + Type: originalConfig.Type, + Role: originalConfig.Role, + Roles: originalConfig.Roles, // Shallow copy is OK for roles (read-only) + Invite: originalConfig.Invite, // Shallow copy is OK for invite config (read-only) + Uploader: originalConfig.Uploader, // Public information + AvatarAgent: originalConfig.AvatarAgent, // Public information } // Handle robot config - create a copy without sensitive fields diff --git a/openapi/user/member.go b/openapi/user/member.go index 7743f284..da70c96b 100644 --- a/openapi/user/member.go +++ b/openapi/user/member.go @@ -287,6 +287,9 @@ func GinMemberCreateRobot(c *gin.Context) { } // Add optional fields + if req.Avatar != "" { + baseData["avatar"] = req.Avatar + } if req.Email != "" { baseData["email"] = req.Email // Optional: display-only email } @@ -394,6 +397,9 @@ func GinMemberUpdateRobot(c *gin.Context) { if req.Name != "" { updateData["display_name"] = req.Name } + if req.Avatar != "" { + updateData["avatar"] = req.Avatar + } if req.Email != "" { updateData["email"] = req.Email } diff --git a/openapi/user/types.go b/openapi/user/types.go index e1c05fcd..5882feb4 100644 --- a/openapi/user/types.go +++ b/openapi/user/types.go @@ -445,6 +445,7 @@ type MemberDetailResponse struct { // CreateRobotMemberRequest represents the request to create a new robot member type CreateRobotMemberRequest struct { Name string `json:"name" binding:"required"` // Display name + Avatar string `json:"avatar,omitempty"` // Avatar URL or file ID Email string `json:"email,omitempty"` // Email address (optional, for display only) RobotEmail string `json:"robot_email" binding:"required"` // Robot's globally unique email address (required) AuthorizedSenders []string `json:"authorized_senders,omitempty"` // Whitelist of emails authorized to send commands @@ -463,6 +464,7 @@ type CreateRobotMemberRequest struct { // UpdateRobotMemberRequest represents the request to update a robot member type UpdateRobotMemberRequest struct { Name string `json:"name,omitempty"` // Display name + Avatar string `json:"avatar,omitempty"` // Avatar URL or file ID 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 @@ -617,11 +619,13 @@ type RobotDefaults struct { // TeamConfig represents the team configuration loaded from DSL files type TeamConfig struct { - Roles []*TeamRole `json:"roles,omitempty"` - Robot *RobotConfig `json:"robot,omitempty"` - Invite *InviteConfig `json:"invite,omitempty"` - Type string `json:"type,omitempty"` // Default subscription type for new teams - Role string `json:"role,omitempty"` // Default user role for team creator + Roles []*TeamRole `json:"roles,omitempty"` + Robot *RobotConfig `json:"robot,omitempty"` + Invite *InviteConfig `json:"invite,omitempty"` + Type string `json:"type,omitempty"` // Default subscription type for new teams + Role string `json:"role,omitempty"` // Default user role for team creator + Uploader string `json:"uploader,omitempty"` // Uploader for avatar and attachments (default: __yao.attachment) + AvatarAgent string `json:"avatar_agent,omitempty"` // Agent ID for avatar generation (optional) } // TeamRole represents a team role configuration