Enhance robot member functionality with avatar support

- Added avatar field to CreateRobotMemberRequest and UpdateRobotMemberRequest structures, allowing for avatar URL or file ID during member creation and updates.
- Updated GinMemberCreateRobot and GinMemberUpdateRobot handlers to process avatar information, ensuring it is included in the member data.
- Expanded test cases for member creation and updates to validate avatar handling, including scenarios for updating only the avatar without affecting other fields.
- Enhanced team configuration tests to verify preservation of uploader and avatar agent fields, improving overall test coverage and reliability.
This commit is contained in:
Max 2025-10-27 16:45:10 +08:00
parent 26b690246d
commit 7572f99ba0
6 changed files with 119 additions and 9 deletions

View file

@ -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 {

View file

@ -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 {

View file

@ -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")

View file

@ -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

View file

@ -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
}

View file

@ -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