Merge pull request #1235 from trheyi/main

Update member model and robot member handling
This commit is contained in:
Max 2025-10-25 17:41:56 +08:00 committed by GitHub
commit 56110d29cd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 260 additions and 255 deletions

File diff suppressed because one or more lines are too long

View file

@ -157,18 +157,17 @@ var (
// DefaultMemberFields contains basic member fields
DefaultMemberFields = []interface{}{
"team_id", "user_id", "member_type", "display_name", "bio", "email", "role_id", "status",
"team_id", "user_id", "member_type", "display_name", "bio", "avatar", "email", "role_id", "status",
"invitation_id", "invited_by", "invited_at", "joined_at", "invitation_token", "invitation_expires_at",
"last_active_at", "login_count", "created_at", "updated_at",
}
// DefaultMemberDetailFields contains all member fields including robot config and permissions
DefaultMemberDetailFields = []interface{}{
"team_id", "user_id", "member_type", "display_name", "bio", "email", "role_id", "status",
"robot_name", "robot_description", "robot_avatar", "robot_config", "agents", "tools",
"mcp_servers", "data_access_permissions", "system_prompt", "is_active_robot",
"schedule_config", "random_activity", "activity_frequency", "last_robot_activity",
"robot_status", "invitation_id", "invited_by", "invited_at", "joined_at", "invitation_token",
"team_id", "user_id", "member_type", "display_name", "bio", "avatar", "email", "role_id", "status",
"system_prompt", "manager_id", "robot_config", "agents", "mcp_servers",
"language_model", "cost_limit", "autonomous_mode", "last_robot_activity", "robot_status",
"invitation_id", "invited_by", "invited_at", "joined_at", "invitation_token",
"invitation_expires_at", "permissions", "restrictions", "last_active_at",
"login_count", "notes", "metadata", "created_at", "updated_at",
}

View file

@ -186,8 +186,8 @@ func (u *DefaultUser) CreateMember(ctx context.Context, memberData maps.MapStrAn
// CreateRobotMember creates a new robot member
func (u *DefaultUser) CreateRobotMember(ctx context.Context, teamID string, robotData maps.MapStrAny) (int64, error) {
// Validate required fields for robot members
if _, exists := robotData["robot_name"]; !exists {
return 0, fmt.Errorf("robot_name is required for robot members")
if _, exists := robotData["display_name"]; !exists {
return 0, fmt.Errorf("display_name is required for robot members")
}
if _, exists := robotData["role_id"]; !exists {
return 0, fmt.Errorf("role_id is required for robot members")
@ -200,12 +200,20 @@ func (u *DefaultUser) CreateRobotMember(ctx context.Context, teamID string, robo
"user_id": nil, // Robots don't have user_id
}
// Copy shared profile fields (used by both users and robots)
profileFields := []string{
"display_name", "bio", "avatar",
}
for _, field := range profileFields {
if value, exists := robotData[field]; exists {
memberData[field] = value
}
}
// Copy robot-specific fields
robotFields := []string{
"role_id", "robot_name", "robot_description", "robot_avatar",
"robot_config", "agents", "tools", "mcp_servers", "data_access_permissions",
"system_prompt", "is_active_robot", "schedule_config", "random_activity",
"activity_frequency", "robot_status",
"role_id", "system_prompt", "manager_id", "robot_config", "agents", "mcp_servers",
"language_model", "cost_limit", "autonomous_mode", "robot_status",
}
for _, field := range robotFields {
@ -219,6 +227,11 @@ func (u *DefaultUser) CreateRobotMember(ctx context.Context, teamID string, robo
memberData["robot_status"] = "idle"
}
// Set default autonomous mode if not provided
if _, exists := memberData["autonomous_mode"]; !exists {
memberData["autonomous_mode"] = false
}
return u.CreateMember(ctx, memberData)
}
@ -511,7 +524,7 @@ func (u *DefaultUser) GetTeamRobotMembers(ctx context.Context, teamID string) ([
{Column: "member_type", Value: "robot"},
},
Orders: []model.QueryOrder{
{Column: "robot_name", Option: "asc"},
{Column: "display_name", Option: "asc"},
},
}
@ -530,7 +543,7 @@ func (u *DefaultUser) GetActiveRobotMembers(ctx context.Context) ([]maps.MapStr,
Select: u.memberDetailFields,
Wheres: []model.QueryWhere{
{Column: "member_type", Value: "robot"},
{Column: "is_active_robot", Value: true},
{Column: "autonomous_mode", Value: true},
{Column: "status", Value: "active"},
},
Orders: []model.QueryOrder{
@ -669,13 +682,13 @@ func (u *DefaultUser) PaginateMembers(ctx context.Context, param model.QueryPara
}
// copyMemberProfileFromUser copies member profile fields from user if not set in updateData
// Fields: display_name (from user.name), bio (n/a), email (from user.email)
// Fields: display_name (from user.name), bio (n/a), avatar (from user.picture), email (from user.email)
// Only copies if the field is nil or empty in updateData
// Removes fields with nil or empty string values from updateData
func (u *DefaultUser) copyMemberProfileFromUser(ctx context.Context, userID string, updateData maps.MapStrAny) {
if userID == "" {
// Remove empty fields if no user_id
for _, field := range []string{"display_name", "bio", "email"} {
for _, field := range []string{"display_name", "bio", "avatar", "email"} {
if updateData[field] == nil || updateData[field] == "" {
delete(updateData, field)
}
@ -688,6 +701,9 @@ func (u *DefaultUser) copyMemberProfileFromUser(ctx context.Context, userID stri
if updateData["display_name"] == nil || updateData["display_name"] == "" {
needsCopy = true
}
if updateData["avatar"] == nil || updateData["avatar"] == "" {
needsCopy = true
}
if updateData["email"] == nil || updateData["email"] == "" {
needsCopy = true
}
@ -702,6 +718,11 @@ func (u *DefaultUser) copyMemberProfileFromUser(ctx context.Context, userID stri
updateData["display_name"] = user["name"]
}
// Copy avatar from user.picture if not set
if (updateData["avatar"] == nil || updateData["avatar"] == "") && user["picture"] != nil && user["picture"] != "" {
updateData["avatar"] = user["picture"]
}
// Copy email from user.email if not set
if (updateData["email"] == nil || updateData["email"] == "") && user["email"] != nil && user["email"] != "" {
updateData["email"] = user["email"]
@ -710,7 +731,7 @@ func (u *DefaultUser) copyMemberProfileFromUser(ctx context.Context, userID stri
}
// Remove fields with nil or empty string values (should not be inserted to database)
for _, field := range []string{"display_name", "bio", "email"} {
for _, field := range []string{"display_name", "bio", "avatar", "email"} {
if updateData[field] == nil || updateData[field] == "" {
delete(updateData, field)
}

View file

@ -340,16 +340,18 @@ func TestRobotMemberOperations(t *testing.T) {
// Test CreateRobotMember
t.Run("CreateRobotMember", func(t *testing.T) {
robotData := maps.MapStrAny{
"robot_name": "TestBot" + testUUID,
"robot_description": "A test robot for unit testing",
"robot_avatar": "https://example.com/robot.png",
"role_id": "bot",
"is_active_robot": true,
"robot_status": "idle",
"system_prompt": "You are a helpful test robot",
"display_name": "TestBot" + testUUID,
"bio": "A test robot for unit testing",
"avatar": "https://example.com/robot.png",
"role_id": "bot",
"autonomous_mode": true,
"robot_status": "idle",
"system_prompt": "You are a helpful test robot",
"language_model": "gpt-4",
"cost_limit": 100.00,
"manager_id": ownerUser,
"robot_config": map[string]interface{}{
"max_tokens": 1000,
"model": "gpt-4",
},
}
@ -372,8 +374,8 @@ func TestRobotMemberOperations(t *testing.T) {
assert.NoError(t, err)
assert.Len(t, robots, 1)
assert.Equal(t, "robot", robots[0]["member_type"])
assert.Equal(t, "TestBot"+testUUID, robots[0]["robot_name"])
assert.Equal(t, "A test robot for unit testing", robots[0]["robot_description"])
assert.Equal(t, "TestBot"+testUUID, robots[0]["display_name"])
assert.Equal(t, "A test robot for unit testing", robots[0]["bio"])
})
// Test UpdateRobotActivity
@ -400,7 +402,7 @@ func TestRobotMemberOperations(t *testing.T) {
t.Run("GetActiveRobotMembers", func(t *testing.T) {
// First make sure our robot is active
err := testProvider.UpdateMemberByID(ctx, robotMemberID, maps.MapStrAny{
"is_active_robot": true,
"autonomous_mode": true,
"status": "active",
})
if err != nil {
@ -417,12 +419,12 @@ func TestRobotMemberOperations(t *testing.T) {
// Find our test robot in the results
found := false
for _, robot := range robots {
if robot["robot_name"] == "TestBot"+testUUID {
if robot["display_name"] == "TestBot"+testUUID {
found = true
assert.Equal(t, "robot", robot["member_type"])
// Handle different boolean types from database
isActive := robot["is_active_robot"]
assert.True(t, isActive == true || isActive == int64(1) || isActive == 1, "Robot should be active")
autonomousMode := robot["autonomous_mode"]
assert.True(t, autonomousMode == true || autonomousMode == int64(1) || autonomousMode == 1, "Robot should be autonomous")
break
}
}
@ -431,17 +433,17 @@ func TestRobotMemberOperations(t *testing.T) {
// Test robot member validation
t.Run("CreateRobotMember_ValidationErrors", func(t *testing.T) {
// Missing robot_name
// Missing display_name
robotData := maps.MapStrAny{
"role_id": "bot",
}
_, err := testProvider.CreateRobotMember(ctx, teamID, robotData)
assert.Error(t, err)
assert.Contains(t, err.Error(), "robot_name is required")
assert.Contains(t, err.Error(), "display_name is required")
// Missing role_id
robotData = maps.MapStrAny{
"robot_name": "TestBot2",
"display_name": "TestBot2",
}
_, err = testProvider.CreateRobotMember(ctx, teamID, robotData)
assert.Error(t, err)

View file

@ -455,11 +455,11 @@ func TestTeamMemberOperations(t *testing.T) {
// Test CreateRobotMember
t.Run("CreateRobotMember", func(t *testing.T) {
robotData := maps.MapStrAny{
"robot_name": "TestBot" + testUUID,
"robot_description": "A test robot for unit testing",
"role_id": "bot",
"is_active_robot": true,
"robot_status": "idle",
"display_name": "TestBot" + testUUID,
"bio": "A test robot for unit testing",
"role_id": "bot",
"autonomous_mode": true,
"robot_status": "idle",
}
robotID, err := testProvider.CreateRobotMember(ctx, teamID, robotData)
@ -473,7 +473,7 @@ func TestTeamMemberOperations(t *testing.T) {
assert.NoError(t, err)
assert.Len(t, robots, 1) // Our test robot
assert.Equal(t, "robot", robots[0]["member_type"])
assert.Equal(t, "TestBot"+testUUID, robots[0]["robot_name"])
assert.Equal(t, "TestBot"+testUUID, robots[0]["display_name"])
})
// Test RemoveMember (at the end)

View file

@ -56,12 +56,13 @@
// ============================================================================
// Member Profile Fields (Team-specific identity)
// Shared by both user and robot members
// ============================================================================
{
"name": "display_name",
"type": "string",
"label": "Display Name",
"comment": "Display name for this member within the team (can differ from user.name)",
"comment": "Display name for this member within the team (can differ from user.name for users, robot name for robots)",
"length": 200,
"nullable": true,
"index": true
@ -70,14 +71,22 @@
"name": "bio",
"type": "text",
"label": "Bio",
"comment": "Personal bio/description for this member within the team",
"comment": "Personal bio/description for this member within the team (robot description for robots)",
"nullable": true
},
{
"name": "avatar",
"type": "string",
"label": "Avatar",
"comment": "Avatar/icon URL for this member (shared by both users and robots)",
"length": 500,
"nullable": true
},
{
"name": "email",
"type": "string",
"label": "Email",
"comment": "Email for this member within the team (can differ from user.email)",
"comment": "Email for this member within the team (can differ from user.email, used for communication including robots)",
"length": 255,
"nullable": true,
"index": true
@ -112,30 +121,23 @@
},
// ============================================================================
// Robot Identity Fields (only for robot members)
// Robot Identity & Role Fields (only for robot members)
// ============================================================================
{
"name": "robot_name",
"type": "string",
"label": "Robot Name",
"comment": "Display name for robot member (only used when member_type is robot)",
"length": 100,
"nullable": true
},
{
"name": "robot_description",
"name": "system_prompt",
"type": "text",
"label": "Robot Description",
"comment": "Description of robot's purpose and capabilities (only used when member_type is robot)",
"label": "System Prompt",
"comment": "Identity & role prompt/instructions for robot member (defines robot personality and behavior)",
"nullable": true
},
{
"name": "robot_avatar",
"name": "manager_id",
"type": "string",
"label": "Robot Avatar",
"comment": "Avatar/icon URL for robot member (only used when member_type is robot)",
"length": 500,
"nullable": true
"label": "Direct Manager",
"comment": "User ID of the direct manager/supervisor for this robot member",
"length": 255,
"nullable": true,
"index": true
},
// ============================================================================
@ -145,7 +147,7 @@
"name": "robot_config",
"type": "json",
"label": "Robot Configuration",
"comment": "Robot-specific configuration including behavior settings and capabilities",
"comment": "Robot-specific configuration including behavior settings, scheduling, activity patterns, and capabilities",
"nullable": true
},
{
@ -155,13 +157,6 @@
"comment": "List of accessible AI agents (public service NPCs) that robot can interact with",
"nullable": true
},
{
"name": "tools",
"type": "json",
"label": "Available Tools",
"comment": "List of available tools and functions that robot can use",
"nullable": true
},
{
"name": "mcp_servers",
"type": "json",
@ -170,52 +165,34 @@
"nullable": true
},
{
"name": "data_access_permissions",
"type": "json",
"label": "Data Access Permissions",
"comment": "Specific data access permissions for robot member",
"name": "language_model",
"type": "string",
"label": "Language Model",
"comment": "Language model used by the robot (e.g., gpt-4, claude-3-opus)",
"length": 100,
"nullable": true
},
{
"name": "system_prompt",
"type": "text",
"label": "System Prompt",
"comment": "System prompt/instructions for robot member",
"name": "cost_limit",
"type": "decimal",
"label": "Cost Limit (USD/month)",
"comment": "Monthly cost limit in USD for robot operations",
"precision": 10,
"scale": 2,
"nullable": true
},
// ============================================================================
// Robot Scheduling & Automation
// ============================================================================
{
"name": "is_active_robot",
"name": "autonomous_mode",
"type": "boolean",
"label": "Is Active Robot",
"comment": "Whether robot member is currently active and can perform automated tasks",
"label": "Autonomous Mode",
"comment": "Whether robot can operate autonomously and perform automated tasks",
"default": false,
"index": true
},
{
"name": "schedule_config",
"type": "json",
"label": "Schedule Configuration",
"comment": "Cron-like scheduling configuration for automated robot tasks",
"nullable": true
},
{
"name": "random_activity",
"type": "boolean",
"label": "Random Activity",
"comment": "Whether robot can perform random activities to find tasks",
"default": false
},
{
"name": "activity_frequency",
"type": "json",
"label": "Activity Frequency",
"comment": "Configuration for random activity frequency and patterns",
"nullable": true
},
// ============================================================================
// Robot Activity & Status Tracking
// ============================================================================
{
"name": "last_robot_activity",
"type": "timestamp",
@ -360,10 +337,10 @@
"comment": "Unique constraint: one user can have only one membership per team, with unique invitation_id for pending invitations"
},
{
"name": "idx_team_robot_unique",
"columns": ["team_id", "robot_name", "member_type"],
"name": "idx_team_email_unique",
"columns": ["team_id", "email"],
"type": "unique",
"comment": "Unique constraint: one robot name per team (only applies to robot members)"
"comment": "Unique constraint: one email per team (used for communication, applies to both users and robots)"
},
{
"name": "idx_team_member_type_role",
@ -379,27 +356,33 @@
},
{
"name": "idx_robot_members",
"columns": ["member_type", "is_active_robot", "robot_status"],
"columns": ["member_type", "autonomous_mode", "robot_status"],
"type": "index",
"comment": "Index for finding active robot members"
},
{
"name": "idx_robot_activity",
"columns": ["member_type", "last_robot_activity", "random_activity"],
"columns": ["member_type", "last_robot_activity", "autonomous_mode"],
"type": "index",
"comment": "Index for robot activity scheduling"
},
{
"name": "idx_robot_type_status",
"columns": ["member_type", "robot_status", "is_active_robot"],
"columns": ["member_type", "robot_status", "autonomous_mode"],
"type": "index",
"comment": "Index for finding robot members by status"
},
{
"name": "idx_robot_name",
"columns": ["member_type", "robot_name"],
"name": "idx_robot_display_name",
"columns": ["member_type", "display_name"],
"type": "index",
"comment": "Index for finding robot members by name"
"comment": "Index for finding robot members by display name"
},
{
"name": "idx_robot_manager",
"columns": ["manager_id", "member_type"],
"type": "index",
"comment": "Index for finding robots by manager"
},
{
"name": "idx_team_invitations",