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 contains basic member fields
DefaultMemberFields = []interface{}{ 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", "invitation_id", "invited_by", "invited_at", "joined_at", "invitation_token", "invitation_expires_at",
"last_active_at", "login_count", "created_at", "updated_at", "last_active_at", "login_count", "created_at", "updated_at",
} }
// DefaultMemberDetailFields contains all member fields including robot config and permissions // DefaultMemberDetailFields contains all member fields including robot config and permissions
DefaultMemberDetailFields = []interface{}{ DefaultMemberDetailFields = []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",
"robot_name", "robot_description", "robot_avatar", "robot_config", "agents", "tools", "system_prompt", "manager_id", "robot_config", "agents", "mcp_servers",
"mcp_servers", "data_access_permissions", "system_prompt", "is_active_robot", "language_model", "cost_limit", "autonomous_mode", "last_robot_activity", "robot_status",
"schedule_config", "random_activity", "activity_frequency", "last_robot_activity", "invitation_id", "invited_by", "invited_at", "joined_at", "invitation_token",
"robot_status", "invitation_id", "invited_by", "invited_at", "joined_at", "invitation_token",
"invitation_expires_at", "permissions", "restrictions", "last_active_at", "invitation_expires_at", "permissions", "restrictions", "last_active_at",
"login_count", "notes", "metadata", "created_at", "updated_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 // CreateRobotMember creates a new robot member
func (u *DefaultUser) CreateRobotMember(ctx context.Context, teamID string, robotData maps.MapStrAny) (int64, error) { func (u *DefaultUser) CreateRobotMember(ctx context.Context, teamID string, robotData maps.MapStrAny) (int64, error) {
// Validate required fields for robot members // Validate required fields for robot members
if _, exists := robotData["robot_name"]; !exists { if _, exists := robotData["display_name"]; !exists {
return 0, fmt.Errorf("robot_name is required for robot members") return 0, fmt.Errorf("display_name is required for robot members")
} }
if _, exists := robotData["role_id"]; !exists { if _, exists := robotData["role_id"]; !exists {
return 0, fmt.Errorf("role_id is required for robot members") 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 "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 // Copy robot-specific fields
robotFields := []string{ robotFields := []string{
"role_id", "robot_name", "robot_description", "robot_avatar", "role_id", "system_prompt", "manager_id", "robot_config", "agents", "mcp_servers",
"robot_config", "agents", "tools", "mcp_servers", "data_access_permissions", "language_model", "cost_limit", "autonomous_mode", "robot_status",
"system_prompt", "is_active_robot", "schedule_config", "random_activity",
"activity_frequency", "robot_status",
} }
for _, field := range robotFields { for _, field := range robotFields {
@ -219,6 +227,11 @@ func (u *DefaultUser) CreateRobotMember(ctx context.Context, teamID string, robo
memberData["robot_status"] = "idle" 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) return u.CreateMember(ctx, memberData)
} }
@ -511,7 +524,7 @@ func (u *DefaultUser) GetTeamRobotMembers(ctx context.Context, teamID string) ([
{Column: "member_type", Value: "robot"}, {Column: "member_type", Value: "robot"},
}, },
Orders: []model.QueryOrder{ 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, Select: u.memberDetailFields,
Wheres: []model.QueryWhere{ Wheres: []model.QueryWhere{
{Column: "member_type", Value: "robot"}, {Column: "member_type", Value: "robot"},
{Column: "is_active_robot", Value: true}, {Column: "autonomous_mode", Value: true},
{Column: "status", Value: "active"}, {Column: "status", Value: "active"},
}, },
Orders: []model.QueryOrder{ 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 // 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 // Only copies if the field is nil or empty in updateData
// Removes fields with nil or empty string values from updateData // Removes fields with nil or empty string values from updateData
func (u *DefaultUser) copyMemberProfileFromUser(ctx context.Context, userID string, updateData maps.MapStrAny) { func (u *DefaultUser) copyMemberProfileFromUser(ctx context.Context, userID string, updateData maps.MapStrAny) {
if userID == "" { if userID == "" {
// Remove empty fields if no user_id // 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] == "" { if updateData[field] == nil || updateData[field] == "" {
delete(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"] == "" { if updateData["display_name"] == nil || updateData["display_name"] == "" {
needsCopy = true needsCopy = true
} }
if updateData["avatar"] == nil || updateData["avatar"] == "" {
needsCopy = true
}
if updateData["email"] == nil || updateData["email"] == "" { if updateData["email"] == nil || updateData["email"] == "" {
needsCopy = true needsCopy = true
} }
@ -702,6 +718,11 @@ func (u *DefaultUser) copyMemberProfileFromUser(ctx context.Context, userID stri
updateData["display_name"] = user["name"] 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 // Copy email from user.email if not set
if (updateData["email"] == nil || updateData["email"] == "") && user["email"] != nil && user["email"] != "" { if (updateData["email"] == nil || updateData["email"] == "") && user["email"] != nil && user["email"] != "" {
updateData["email"] = 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) // 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] == "" { if updateData[field] == nil || updateData[field] == "" {
delete(updateData, field) delete(updateData, field)
} }

View file

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

View file

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

View file

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