Merge pull request #1235 from trheyi/main
Update member model and robot member handling
This commit is contained in:
commit
56110d29cd
6 changed files with 260 additions and 255 deletions
284
data/bindata.go
284
data/bindata.go
File diff suppressed because one or more lines are too long
|
|
@ -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",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue