Merge pull request #1236 from trheyi/main
Implement robot member creation and validation
This commit is contained in:
commit
ec8650b59e
6 changed files with 397 additions and 93 deletions
|
|
@ -193,6 +193,26 @@ func (u *DefaultUser) CreateRobotMember(ctx context.Context, teamID string, robo
|
|||
return 0, fmt.Errorf("role_id is required for robot members")
|
||||
}
|
||||
|
||||
// Check if email already exists in this team
|
||||
if email, exists := robotData["email"]; exists && email != nil && email != "" {
|
||||
emailStr := fmt.Sprintf("%v", email)
|
||||
m := model.Select(u.memberModel)
|
||||
existingMembers, err := m.Get(model.QueryParam{
|
||||
Select: []interface{}{"id"},
|
||||
Wheres: []model.QueryWhere{
|
||||
{Column: "team_id", Value: teamID},
|
||||
{Column: "email", Value: emailStr},
|
||||
},
|
||||
Limit: 1,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to check email uniqueness: %w", err)
|
||||
}
|
||||
if len(existingMembers) > 0 {
|
||||
return 0, fmt.Errorf("email %s already exists in this team", emailStr)
|
||||
}
|
||||
}
|
||||
|
||||
memberData := maps.MapStrAny{
|
||||
"team_id": teamID,
|
||||
"member_type": "robot",
|
||||
|
|
@ -202,7 +222,7 @@ func (u *DefaultUser) CreateRobotMember(ctx context.Context, teamID string, robo
|
|||
|
||||
// Copy shared profile fields (used by both users and robots)
|
||||
profileFields := []string{
|
||||
"display_name", "bio", "avatar",
|
||||
"display_name", "bio", "avatar", "email",
|
||||
}
|
||||
for _, field := range profileFields {
|
||||
if value, exists := robotData[field]; exists {
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@ package user_test
|
|||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/yaoapp/yao/openapi"
|
||||
"github.com/yaoapp/yao/openapi/tests/testutils"
|
||||
|
|
@ -1022,4 +1024,313 @@ func getOwnerMemberID(t *testing.T, serverURL, baseURL, teamID, accessToken stri
|
|||
return ""
|
||||
}
|
||||
|
||||
// TestMemberCreateRobot tests the POST /user/teams/:team_id/members/robots endpoint
|
||||
func TestMemberCreateRobot(t *testing.T) {
|
||||
// Initialize test environment
|
||||
serverURL := testutils.Prepare(t)
|
||||
defer testutils.Clean()
|
||||
|
||||
// Get base URL from server config
|
||||
baseURL := ""
|
||||
if openapi.Server != nil && openapi.Server.Config != nil {
|
||||
baseURL = openapi.Server.Config.BaseURL
|
||||
}
|
||||
|
||||
// Register a test client for OAuth authentication
|
||||
testClient := testutils.RegisterTestClient(t, "Robot Member Test Client", []string{"https://localhost/callback"})
|
||||
defer testutils.CleanupTestClient(t, testClient.ClientID)
|
||||
|
||||
// Obtain access token with root permissions (required for creating robot members)
|
||||
tokenInfo := testutils.ObtainAccessTokenWithRootPermission(t, serverURL, testClient.ClientID, testClient.ClientSecret, "https://localhost/callback", "openid profile")
|
||||
|
||||
// Use UUID to ensure unique team name
|
||||
testUUID := strings.ReplaceAll(uuid.New().String(), "-", "")[:8]
|
||||
|
||||
// Create a test team
|
||||
createdTeam := createTestTeam(t, serverURL, baseURL, tokenInfo.AccessToken, "Robot Member Test Team "+testUUID)
|
||||
teamID := getTeamID(createdTeam)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
teamID string
|
||||
body map[string]interface{}
|
||||
headers map[string]string
|
||||
expectCode int
|
||||
expectMsg string
|
||||
}{
|
||||
{
|
||||
"create robot without authentication",
|
||||
teamID,
|
||||
map[string]interface{}{
|
||||
"name": "Test Robot",
|
||||
"email": "robot@test.com",
|
||||
"role": "member",
|
||||
"prompt": "You are a helpful assistant",
|
||||
},
|
||||
map[string]string{},
|
||||
401,
|
||||
"should require authentication",
|
||||
},
|
||||
{
|
||||
"create robot with all fields",
|
||||
teamID,
|
||||
map[string]interface{}{
|
||||
"name": "AI Assistant Full",
|
||||
"email": fmt.Sprintf("ai-full-%s@test.com", testUUID),
|
||||
"bio": "A comprehensive AI assistant",
|
||||
"role": "member",
|
||||
"report_to": tokenInfo.UserID,
|
||||
"prompt": "You are a helpful AI assistant with full capabilities",
|
||||
"llm": "gpt-4",
|
||||
"agents": []string{"data-analyst", "code-reviewer"},
|
||||
"mcp_tools": []string{"filesystem", "database"},
|
||||
"autonomous_mode": "enabled",
|
||||
"cost_limit": 100.50,
|
||||
},
|
||||
map[string]string{
|
||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||
},
|
||||
201,
|
||||
"should create robot with all fields successfully",
|
||||
},
|
||||
{
|
||||
"create robot with required fields only",
|
||||
teamID,
|
||||
map[string]interface{}{
|
||||
"name": "AI Assistant Min",
|
||||
"email": fmt.Sprintf("ai-min-%s@test.com", testUUID),
|
||||
"role": "member",
|
||||
"prompt": "You are a basic assistant",
|
||||
},
|
||||
map[string]string{
|
||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||
},
|
||||
201,
|
||||
"should create robot with required fields only",
|
||||
},
|
||||
{
|
||||
"create robot with autonomous_mode variations",
|
||||
teamID,
|
||||
map[string]interface{}{
|
||||
"name": "AI Assistant Auto",
|
||||
"email": fmt.Sprintf("ai-auto-%s@test.com", testUUID),
|
||||
"role": "member",
|
||||
"prompt": "You are an autonomous assistant",
|
||||
"autonomous_mode": "1", // Test numeric string
|
||||
},
|
||||
map[string]string{
|
||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||
},
|
||||
201,
|
||||
"should handle autonomous_mode=1",
|
||||
},
|
||||
{
|
||||
"create robot with disabled autonomous_mode",
|
||||
teamID,
|
||||
map[string]interface{}{
|
||||
"name": "AI Assistant Manual",
|
||||
"email": fmt.Sprintf("ai-manual-%s@test.com", testUUID),
|
||||
"role": "member",
|
||||
"prompt": "You are a manual assistant",
|
||||
"autonomous_mode": "disabled",
|
||||
},
|
||||
map[string]string{
|
||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||
},
|
||||
201,
|
||||
"should handle autonomous_mode=disabled",
|
||||
},
|
||||
{
|
||||
"create robot without name",
|
||||
teamID,
|
||||
map[string]interface{}{
|
||||
"email": "no-name@test.com",
|
||||
"role": "member",
|
||||
"prompt": "You are an assistant",
|
||||
},
|
||||
map[string]string{
|
||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||
},
|
||||
400,
|
||||
"should require name",
|
||||
},
|
||||
{
|
||||
"create robot without email",
|
||||
teamID,
|
||||
map[string]interface{}{
|
||||
"name": "No Email Robot",
|
||||
"role": "member",
|
||||
"prompt": "You are an assistant",
|
||||
},
|
||||
map[string]string{
|
||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||
},
|
||||
400,
|
||||
"should require email",
|
||||
},
|
||||
{
|
||||
"create robot without role",
|
||||
teamID,
|
||||
map[string]interface{}{
|
||||
"name": "No Role Robot",
|
||||
"email": "no-role@test.com",
|
||||
"prompt": "You are an assistant",
|
||||
},
|
||||
map[string]string{
|
||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||
},
|
||||
400,
|
||||
"should require role",
|
||||
},
|
||||
{
|
||||
"create robot without prompt",
|
||||
teamID,
|
||||
map[string]interface{}{
|
||||
"name": "No Prompt Robot",
|
||||
"email": "no-prompt@test.com",
|
||||
"role": "member",
|
||||
},
|
||||
map[string]string{
|
||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||
},
|
||||
400,
|
||||
"should require prompt",
|
||||
},
|
||||
{
|
||||
"create robot with duplicate email",
|
||||
teamID,
|
||||
map[string]interface{}{
|
||||
"name": "Duplicate Email Robot",
|
||||
"email": fmt.Sprintf("ai-full-%s@test.com", testUUID), // Same as first successful case
|
||||
"role": "member",
|
||||
"prompt": "You are an assistant",
|
||||
},
|
||||
map[string]string{
|
||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||
},
|
||||
409,
|
||||
"should reject duplicate email in same team",
|
||||
},
|
||||
{
|
||||
"create robot in non-existent team",
|
||||
"non-existent-team-id",
|
||||
map[string]interface{}{
|
||||
"name": "Robot in Void",
|
||||
"email": "void@test.com",
|
||||
"role": "member",
|
||||
"prompt": "You are lost",
|
||||
},
|
||||
map[string]string{
|
||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||
},
|
||||
404,
|
||||
"should return not found for non-existent team",
|
||||
},
|
||||
{
|
||||
"create robot with invalid JSON",
|
||||
teamID,
|
||||
nil, // Will send invalid JSON
|
||||
map[string]string{
|
||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||
},
|
||||
400,
|
||||
"should handle invalid JSON",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
requestURL := serverURL + baseURL + "/user/teams/" + tc.teamID + "/members/robots"
|
||||
|
||||
var req *http.Request
|
||||
var err error
|
||||
|
||||
if tc.body == nil {
|
||||
// Send invalid JSON for invalid JSON test case
|
||||
req, err = http.NewRequest("POST", requestURL, bytes.NewBufferString("invalid json"))
|
||||
} else {
|
||||
bodyBytes, _ := json.Marshal(tc.body)
|
||||
req, err = http.NewRequest("POST", requestURL, bytes.NewBuffer(bodyBytes))
|
||||
}
|
||||
assert.NoError(t, err, "Should create HTTP request")
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// Add headers
|
||||
for key, value := range tc.headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
assert.NoError(t, err, "HTTP request should succeed")
|
||||
|
||||
if resp != nil {
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, tc.expectCode, resp.StatusCode, "Expected status code %d for %s", tc.expectCode, tc.name)
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err, "Should read response body")
|
||||
|
||||
if resp.StatusCode == 201 {
|
||||
// Parse response as created member
|
||||
var response map[string]interface{}
|
||||
err = json.Unmarshal(body, &response)
|
||||
assert.NoError(t, err, "Should parse JSON response")
|
||||
|
||||
// Verify response structure
|
||||
assert.Contains(t, response, "member_id", "Should have member_id")
|
||||
assert.NotEmpty(t, response["member_id"], "Member ID should not be empty")
|
||||
|
||||
// Verify the member was created with correct type
|
||||
memberID := toString(response["member_id"])
|
||||
getMemberURL := serverURL + baseURL + "/user/teams/" + tc.teamID + "/members/" + memberID
|
||||
getReq, _ := http.NewRequest("GET", getMemberURL, nil)
|
||||
getReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
getResp, err := client.Do(getReq)
|
||||
if err == nil && getResp != nil {
|
||||
defer getResp.Body.Close()
|
||||
if getResp.StatusCode == 200 {
|
||||
var member map[string]interface{}
|
||||
getBody, _ := io.ReadAll(getResp.Body)
|
||||
json.Unmarshal(getBody, &member)
|
||||
|
||||
// Verify robot member fields
|
||||
assert.Equal(t, "robot", member["member_type"], "Should be robot member type")
|
||||
if tc.body["name"] != nil {
|
||||
assert.Equal(t, tc.body["name"], member["display_name"], "Should have correct display_name")
|
||||
}
|
||||
if tc.body["email"] != nil {
|
||||
assert.Equal(t, tc.body["email"], member["email"], "Should have correct email")
|
||||
}
|
||||
if tc.body["prompt"] != nil {
|
||||
assert.Equal(t, tc.body["prompt"], member["system_prompt"], "Should have correct system_prompt")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Robot member create test %s: status=%d, body=%s", tc.name, resp.StatusCode, string(body))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// toString converts interface{} to string for test assertions
|
||||
func toString(v interface{}) string {
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
return val
|
||||
case float64:
|
||||
return fmt.Sprintf("%.0f", val)
|
||||
case int:
|
||||
return fmt.Sprintf("%d", val)
|
||||
case int64:
|
||||
return fmt.Sprintf("%d", val)
|
||||
default:
|
||||
return fmt.Sprintf("%v", val)
|
||||
}
|
||||
}
|
||||
|
||||
// Note: getTeamID function is already defined in team_test.go
|
||||
|
|
|
|||
|
|
@ -147,10 +147,10 @@ func GinMemberGet(c *gin.Context) {
|
|||
response.RespondWithSuccess(c, http.StatusOK, member)
|
||||
}
|
||||
|
||||
// GinMemberCreateDirect handles POST /teams/:team_id/members - Add member directly to team
|
||||
func GinMemberCreateDirect(c *gin.Context) {
|
||||
// GinMemberCreateRobot handles POST /teams/:team_id/members/robots - Add robot member to team
|
||||
func GinMemberCreateRobot(c *gin.Context) {
|
||||
// Get authorized user info
|
||||
authInfo := oauth.GetAuthorizedInfo(c)
|
||||
authInfo := authorized.GetInfo(c)
|
||||
if authInfo == nil || authInfo.UserID == "" {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrInvalidClient.Code,
|
||||
|
|
@ -171,7 +171,7 @@ func GinMemberCreateDirect(c *gin.Context) {
|
|||
}
|
||||
|
||||
// Parse request body
|
||||
var req CreateMemberRequest
|
||||
var req CreateRobotMemberRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrInvalidRequest.Code,
|
||||
|
|
@ -181,22 +181,40 @@ func GinMemberCreateDirect(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
// Prepare member data
|
||||
memberData := maps.MapStrAny{
|
||||
"user_id": req.UserID,
|
||||
"member_type": req.MemberType,
|
||||
"role_id": req.RoleID,
|
||||
// Prepare base robot member data
|
||||
baseData := maps.MapStrAny{
|
||||
"display_name": req.Name,
|
||||
"email": req.Email,
|
||||
"bio": req.Bio,
|
||||
"role_id": req.RoleID,
|
||||
"system_prompt": req.SystemPrompt,
|
||||
"autonomous_mode": toBool(req.AutonomousMode),
|
||||
}
|
||||
|
||||
// Add settings if provided
|
||||
if req.Settings != nil {
|
||||
memberData["settings"] = req.Settings
|
||||
// Add optional fields
|
||||
if req.ManagerID != "" {
|
||||
baseData["manager_id"] = req.ManagerID
|
||||
}
|
||||
if req.LanguageModel != "" {
|
||||
baseData["language_model"] = req.LanguageModel
|
||||
}
|
||||
if len(req.Agents) > 0 {
|
||||
baseData["agents"] = req.Agents
|
||||
}
|
||||
if len(req.MCPServers) > 0 {
|
||||
baseData["mcp_servers"] = req.MCPServers
|
||||
}
|
||||
if req.CostLimit > 0 {
|
||||
baseData["cost_limit"] = req.CostLimit
|
||||
}
|
||||
|
||||
// Wrap with create scope for permission tracking
|
||||
robotData := authInfo.WithCreateScope(baseData)
|
||||
|
||||
// Call business logic
|
||||
memberID, err := memberCreateDirect(c.Request.Context(), authInfo.UserID, teamID, memberData)
|
||||
memberID, err := memberCreateRobot(c.Request.Context(), authInfo.UserID, teamID, robotData)
|
||||
if err != nil {
|
||||
log.Error("Failed to create member: %v", err)
|
||||
log.Error("Failed to create robot member: %v", err)
|
||||
// Check error type for appropriate response
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
errorResp := &response.ErrorResponse{
|
||||
|
|
@ -210,7 +228,7 @@ func GinMemberCreateDirect(c *gin.Context) {
|
|||
ErrorDescription: err.Error(),
|
||||
}
|
||||
response.RespondWithError(c, response.StatusForbidden, errorResp)
|
||||
} else if strings.Contains(err.Error(), "already exists") {
|
||||
} else if strings.Contains(err.Error(), "already exists") || strings.Contains(err.Error(), "duplicate") {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrInvalidRequest.Code,
|
||||
ErrorDescription: err.Error(),
|
||||
|
|
@ -219,7 +237,7 @@ func GinMemberCreateDirect(c *gin.Context) {
|
|||
} else {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrServerError.Code,
|
||||
ErrorDescription: "Failed to create member",
|
||||
ErrorDescription: "Failed to create robot member",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||
}
|
||||
|
|
@ -454,48 +472,6 @@ func ProcessMemberGet(process *process.Process) interface{} {
|
|||
return result
|
||||
}
|
||||
|
||||
// ProcessMemberCreateDirect user.member.create Member create processor
|
||||
// Args[0] string: team_id
|
||||
// Args[1] map: Member data {"user_id": "user123", "member_type": "user", "role_id": "member", "settings": {...}}
|
||||
// Return: map: {"member_id": "created_member_id"}
|
||||
func ProcessMemberCreateDirect(process *process.Process) interface{} {
|
||||
process.ValidateArgNums(2)
|
||||
|
||||
// Get user_id from session
|
||||
userIDStr := GetUserIDFromSession(process)
|
||||
|
||||
teamID := process.ArgsString(0)
|
||||
memberData := maps.MapStrAny(process.ArgsMap(1))
|
||||
|
||||
if teamID == "" {
|
||||
exception.New("team_id is required", 400).Throw()
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if _, ok := memberData["user_id"]; !ok {
|
||||
exception.New("user_id is required", 400).Throw()
|
||||
}
|
||||
if _, ok := memberData["role_id"]; !ok {
|
||||
exception.New("role_id is required", 400).Throw()
|
||||
}
|
||||
|
||||
// Get context
|
||||
ctx := process.Context
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
// Call business logic
|
||||
memberID, err := memberCreateDirect(ctx, userIDStr, teamID, memberData)
|
||||
if err != nil {
|
||||
exception.New("failed to create member: %s", 500, err.Error()).Throw()
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"member_id": memberID,
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessMemberUpdate user.member.update Member update processor
|
||||
// Args[0] string: team_id
|
||||
// Args[1] string: member_id
|
||||
|
|
@ -644,8 +620,8 @@ func memberGet(ctx context.Context, userID, teamID, memberID string) (maps.MapSt
|
|||
return memberData, nil
|
||||
}
|
||||
|
||||
// memberCreateDirect handles the business logic for creating a team member directly
|
||||
func memberCreateDirect(ctx context.Context, userID, teamID string, memberData maps.MapStrAny) (int64, error) {
|
||||
// memberCreateRobot handles the business logic for creating a robot member
|
||||
func memberCreateRobot(ctx context.Context, userID, teamID string, robotData maps.MapStrAny) (int64, error) {
|
||||
// Check if user has access to the team (write permission: owner only)
|
||||
isOwner, _, err := checkTeamAccess(ctx, teamID, userID)
|
||||
if err != nil {
|
||||
|
|
@ -654,7 +630,7 @@ func memberCreateDirect(ctx context.Context, userID, teamID string, memberData m
|
|||
|
||||
// Only allow access if user is owner
|
||||
if !isOwner {
|
||||
return 0, fmt.Errorf("access denied: only team owner can add members")
|
||||
return 0, fmt.Errorf("access denied: only team owner can add robot members")
|
||||
}
|
||||
|
||||
// Get user provider instance
|
||||
|
|
@ -663,30 +639,10 @@ func memberCreateDirect(ctx context.Context, userID, teamID string, memberData m
|
|||
return 0, fmt.Errorf("failed to get user provider: %w", err)
|
||||
}
|
||||
|
||||
// Check if member already exists
|
||||
memberUserID := toString(memberData["user_id"])
|
||||
exists, err := provider.MemberExists(ctx, teamID, memberUserID)
|
||||
// Use CreateRobotMember method which handles robot-specific logic
|
||||
memberID, err := provider.CreateRobotMember(ctx, teamID, robotData)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to check member existence: %w", err)
|
||||
}
|
||||
if exists {
|
||||
return 0, fmt.Errorf("member already exists in this team")
|
||||
}
|
||||
|
||||
// Set team ID and default values
|
||||
memberData["team_id"] = teamID
|
||||
if memberData["member_type"] == nil || memberData["member_type"] == "" {
|
||||
memberData["member_type"] = "user"
|
||||
}
|
||||
memberData["status"] = "active"
|
||||
memberData["joined_at"] = time.Now()
|
||||
memberData["created_at"] = time.Now()
|
||||
memberData["updated_at"] = time.Now()
|
||||
|
||||
// Create member
|
||||
memberID, err := provider.CreateMember(ctx, memberData)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to create member: %w", err)
|
||||
return 0, fmt.Errorf("failed to create robot member: %w", err)
|
||||
}
|
||||
|
||||
return memberID, nil
|
||||
|
|
|
|||
|
|
@ -416,12 +416,19 @@ type MemberDetailResponse struct {
|
|||
UserInfo map[string]interface{} `json:"user_info,omitempty"`
|
||||
}
|
||||
|
||||
// CreateMemberRequest represents the request to add a member directly
|
||||
type CreateMemberRequest struct {
|
||||
UserID string `json:"user_id" binding:"required"`
|
||||
MemberType string `json:"member_type,omitempty"` // "user" or "robot"
|
||||
RoleID string `json:"role_id" binding:"required"`
|
||||
Settings *MemberSettings `json:"settings,omitempty"`
|
||||
// CreateRobotMemberRequest represents the request to create a new robot member
|
||||
type CreateRobotMemberRequest struct {
|
||||
Name string `json:"name" binding:"required"` // Display name
|
||||
Email string `json:"email" binding:"required"` // Email address
|
||||
Bio string `json:"bio,omitempty"` // Bio/description
|
||||
RoleID string `json:"role" binding:"required"` // Role ID
|
||||
ManagerID string `json:"report_to,omitempty"` // Direct manager user ID
|
||||
SystemPrompt string `json:"prompt" binding:"required"` // Identity & role prompt
|
||||
LanguageModel string `json:"llm,omitempty"` // Language model (e.g., "gpt-4")
|
||||
Agents []string `json:"agents,omitempty"` // Accessible agents
|
||||
MCPServers []string `json:"mcp_tools,omitempty"` // MCP servers/tools
|
||||
AutonomousMode string `json:"autonomous_mode,omitempty"` // "enabled" or "disabled"
|
||||
CostLimit float64 `json:"cost_limit,omitempty"` // Monthly cost limit in USD
|
||||
}
|
||||
|
||||
// UpdateMemberRequest represents the request to update a member
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ func attachTeam(group *gin.RouterGroup, oauth types.OAuth) {
|
|||
|
||||
// Team Members - Nested resource endpoints
|
||||
team.GET("/:id/members", GinMemberList) // GET /teams/:id/members - List team members
|
||||
team.POST("/:id/members", GinMemberCreateDirect) // POST /teams/:id/members - Add team member
|
||||
team.POST("/:id/members/robots", GinMemberCreateRobot) // POST /teams/:id/members/robots - Add robot member
|
||||
team.GET("/:id/members/:member_id", GinMemberGet) // GET /teams/:id/members/:member_id - Get member details
|
||||
team.PUT("/:id/members/:member_id", GinMemberUpdate) // PUT /teams/:id/members/:member_id - Update member
|
||||
team.DELETE("/:id/members/:member_id", GinMemberDelete) // DELETE /teams/:id/members/:member_id - Remove member
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ func GetUserIDFromSession(process *process.Process) string {
|
|||
|
||||
// toBool converts various types to boolean
|
||||
// Supports: bool, int, int64, float64, string
|
||||
// String values: "true", "false", "1", "0", "enabled", "disabled", "yes", "no", "on", "off"
|
||||
// Returns false for nil or unsupported types
|
||||
func toBool(v interface{}) bool {
|
||||
if v == nil {
|
||||
|
|
@ -51,7 +52,16 @@ func toBool(v interface{}) bool {
|
|||
case float64:
|
||||
return val != 0
|
||||
case string:
|
||||
return val == "true" || val == "1"
|
||||
// Normalize string to lowercase for case-insensitive comparison
|
||||
normalized := strings.ToLower(strings.TrimSpace(val))
|
||||
switch normalized {
|
||||
case "true", "1", "enabled", "yes", "on":
|
||||
return true
|
||||
case "false", "0", "disabled", "no", "off", "":
|
||||
return false
|
||||
default:
|
||||
return false
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue