Skip to main content
Glama
user_handler.py24.6 kB
""" App Store Connect 用户管理处理器 - 负责团队成员和邀请管理 """ import threading import time from datetime import datetime, timedelta from typing import Optional, Any, List, Callable, Dict from ..models import TeamMember, UserInvitation, ROLE_MAPPING from ...mcp_handler_interface import IMCPHandler class UserHandler(IMCPHandler): """用户管理处理器 - 负责团队成员、邀请等用户相关操作""" def __init__(self, client): self.client = client self._polling_tasks = {} # 存储正在进行的轮询任务 self._status_callbacks = {} # 存储状态回调函数 def register_tools(self, mcp: Any) -> None: """注册用户管理相关工具""" @mcp.tool("get_team_members") def get_team_members_tool() -> str: """ 获取App Store Connect团队成员列表 Returns: str: 团队成员列表,包括成员邮箱、全名等信息 """ try: members = self.get_team_members() return f"团队共有 {len(members)} 名成员:\n" + "\n".join( [f"- {m.email} ({m.full_name})" for m in members]) except Exception as e: return f"获取团队成员失败: {str(e)}" @mcp.tool("check_user_invitations") def check_invitations_tool() -> str: """ 检查App Store Connect中待处理的用户邀请 Returns: str: 待处理邀请列表,包括被邀请者邮箱、角色、状态、过期时间等信息 """ try: invitations = self.get_user_invitations() if not invitations: return "当前没有待处理的邀请" result = f"待处理邀请 ({len(invitations)} 个):\n" for inv in invitations: status = "已过期" if inv.is_expired else "有效" result += f"- {inv.email} ({inv.full_name}) - 角色: {', '.join(inv.roles)} - 状态: {status} - 过期时间: {inv.expires}\n" return result except Exception as e: return f"获取邀请列表失败: {str(e)}" @mcp.tool("invite_user_with_polling") def invite_user_with_polling_tool(email: str, app_name: str, role: str = "CUSTOMER_SUPPORT") -> str: """ App Store Connect 邀请用户加入团队并异步等待其接受邀请后添加到TestFlight(带轮询监控) Args: email (str): 被邀请用户的邮箱地址 app_name (str): 要添加TestFlight权限的应用名称 role (str, optional): 用户在团队中的角色,默认为"CUSTOMER_SUPPORT" 可选值: "ADMIN", "APP_MANAGER", "DEVELOPER", "MARKETING", "SALES", "CUSTOMER_SUPPORT" Returns: str: 邀请操作的结果信息,包括邀请状态和后续操作说明 """ # 从邮箱自动提取用户名 first_name = email.split('@')[0] # 使用角色映射 mapped_role = ROLE_MAPPING.get(role, role.upper()) roles = [mapped_role] # 定义状态回调函数(简单打印,实际使用中可以记录到日志) def status_callback(user_email: str, status: str): print(f"[轮询状态] {user_email}: {status}") return self.invite_user_and_wait_for_testflight(email, first_name, app_name, roles, status_callback) @mcp.tool("get_polling_status") def get_polling_status_tool(email: Optional[str] = None) -> str: """ App Store Connect 获取用户邀请轮询任务的状态 Args: email (str, optional): 要查询状态的用户邮箱,默认为None(获取所有任务状态) Returns: str: 轮询任务状态信息,包括任务进度、当前状态等 """ try: status = self.get_polling_status(email) if email: # 单个用户状态 if 'error' in status: return status['error'] else: return f"用户 {status['email']} 的轮询状态:\n" + \ f"- 任务ID: {status['task_id']}\n" + \ f"- 应用: {status['app_name']}\n" + \ f"- 状态: {status['status']}\n" + \ f"- 已运行: {status['elapsed_minutes']:.0f} 分钟\n" + \ f"- 线程活跃: {'是' if status['thread_alive'] else '否'}" else: # 所有任务状态 if status['total_count'] == 0: return "当前没有运行中的轮询任务" result = f"运行中的轮询任务 ({status['total_count']} 个):\n" for task_id, task_info in status['tasks'].items(): result += f"- {task_info['email']} ({task_info['app_name']}) - " + \ f"状态: {task_info['status']} - " + \ f"运行: {task_info['elapsed_minutes']:.0f}分钟 - " + \ f"活跃: {'是' if task_info['thread_alive'] else '否'}\n" return result except Exception as e: return f"获取轮询状态失败: {str(e)}" @mcp.tool("cancel_polling_task") def cancel_polling_task_tool(email: str) -> str: """ App Store Connect 取消指定用户的轮询任务 Args: email (str): 要取消轮询任务的用户邮箱地址 Returns: str: 取消操作的结果信息 """ try: return self.cancel_polling_task(email) except Exception as e: return f"取消轮询任务失败: {str(e)}" @mcp.tool("remove_team_member") def remove_team_member_tool(email: str) -> str: """ 从 App Store Connect 团队中移除指定成员 Args: email (str): 要移除的团队成员邮箱地址 Returns: str: 移除操作的结果信息 """ try: self.remove_team_member(email) return f"已成功从团队中移除用户 {email}" except ValueError as e: return str(e) except Exception as e: return f"移除团队成员失败: {str(e)}" @mcp.tool("remove_user_completely") def remove_user_completely_tool(email: str, app_name: str) -> str: """ App Store Connect 完全移除用户(同时从团队和TestFlight中移除) Args: email (str): 要移除的用户邮箱地址 app_name (str): 要从TestFlight中移除的应用名称 Returns: str: 完全移除操作的详细结果信息,包括团队和TestFlight的移除状态 """ try: return self.remove_user_completely(email, app_name) except Exception as e: return f"完全移除用户失败: {str(e)}" def register_resources(self, mcp: Any) -> None: """注册用户管理相关资源""" @mcp.resource("appstore://team-members") def get_team_members_resource() -> str: """App Store Connect 获取团队成员资源""" try: members = self.get_team_members() return f"团队成员列表:\n" + "\n".join([f"- {m.email} ({m.full_name})" for m in members]) except Exception as e: return f"获取团队成员失败: {str(e)}" @mcp.resource("appstore://invitations") def get_invitations_resource() -> str: """App Store Connect 获取邀请列表资源""" try: invitations = self.get_user_invitations() if not invitations: return "当前没有待处理的邀请" return f"待处理邀请:\n" + "\n".join([f"- {inv.email} ({inv.full_name})" for inv in invitations]) except Exception as e: return f"获取邀请列表失败: {str(e)}" def register_prompts(self, mcp: Any) -> None: """注册用户管理相关提示""" @mcp.prompt("appstore_invite_user") def appstore_invite_user_prompt(email: str = "", name: str = "", app_name: str = "", role: str = "") -> str: """App Store Connect邀请用户提示""" return f"""App Store Connect 用户邀请助手 邀请信息: - 邮箱: {email} - 姓名: {name} - 应用: {app_name} - 角色: {role} 支持的角色类型: - 管理员 (ADMIN): 完全访问权限 - 财务 (FINANCE): 财务和销售报告访问 - 开发者 (DEVELOPER): 开发和测试访问 - 营销 (MARKETING): 营销和应用商店访问 - 客服 (CUSTOMER_SUPPORT): 客户支持访问 使用步骤: 1. 确认用户信息正确 2. 选择合适的角色 3. 使用 invite_user_to_team 工具发送邀请 4. 用户会自动添加到TestFlight内部测试组 注意事项: - 邀请邮件会发送到用户邮箱 - 用户需要接受邀请才能加入团队 - TestFlight会自动添加到第一个内部测试组 """ @mcp.prompt("appstore_remove_user") def appstore_remove_user_prompt(email: str = "", app_name: str = "", operation: str = "") -> str: """App Store Connect移除用户提示""" return f"""App Store Connect 用户移除助手 移除信息: - 用户邮箱: {email} - 应用名称: {app_name} - 操作类型: {operation} 支持的移除操作: - remove_team_member: 仅从团队中移除用户 - remove_testflight_tester: 仅从TestFlight测试组中移除用户 - remove_user_completely: 完全移除用户(从团队和TestFlight中移除) 使用步骤: 1. 确认用户邮箱正确 2. 选择合适的移除操作 3. 如果是TestFlight相关操作,确认应用名称 4. 使用相应的工具执行移除操作 注意事项: - 移除操作不可逆,请谨慎操作 - 从团队移除用户会自动撤销其所有权限 - 从TestFlight移除仅影响测试访问权限 - 完全移除会同时处理团队和TestFlight权限 安全提示: ⚠️ 移除用户前请确认: - 用户确实不再需要访问权限 - 已通知相关人员此操作 - 备份重要的用户相关数据(如测试反馈) 移除后果: - 用户将无法访问App Store Connect - 用户将无法下载TestFlight构建版本 - 用户的测试数据和反馈将被保留 """ # ============================================================================= # 业务逻辑方法 # ============================================================================= def get_team_members(self) -> List[TeamMember]: """获取团队成员""" response = self.client.make_api_request("users") members = [] for user_data in response.get("data", []): member = TeamMember.from_api_response(user_data) members.append(member) return members def get_user_invitations(self) -> List[UserInvitation]: """获取用户邀请列表""" response = self.client.make_api_request("userInvitations") invitations = [] for invitation_data in response.get("data", []): invitation = UserInvitation.from_api_response(invitation_data) invitations.append(invitation) return invitations def check_user_in_team(self, email: str) -> Optional[TeamMember]: """检查用户是否已在团队中""" members = self.get_team_members() for member in members: if member.email.lower() == email.lower(): return member return None def invite_user_to_team(self, email: str, first_name: str, roles: List[str], apps: List[str]) -> Dict[str, Any]: """邀请用户加入团队""" last_name = "peropero" # 默认姓氏 data = { "data": { "type": "userInvitations", "attributes": { "email": email, "firstName": first_name, "lastName": last_name, "roles": roles, "allAppsVisible": False }, "relationships": { "visibleApps": { "data": [{"type": "apps", "id": app_id} for app_id in apps] } } } } return self.client.make_api_request("userInvitations", method="POST", data=data) def remove_team_member(self, email: str) -> Dict[str, Any]: """从团队中移除成员""" # 首先获取用户信息 member = self.check_user_in_team(email) if not member: raise ValueError(f"用户 {email} 不在团队中") user_id = member.id # 调用删除API return self.client.make_api_request(f"users/{user_id}", method="DELETE") def remove_user_completely(self, email: str, app_name: str) -> str: """完全移除用户(从团队和TestFlight中移除)""" results = [] try: # 1. 从TestFlight中移除 try: self.client.handlers["TestFlightHandler"].remove_beta_tester(email, app_name) results.append(f"已从应用 {app_name} 的TestFlight测试组中移除用户 {email}") except ValueError as e: if "不在应用" in str(e): results.append(f"用户 {email} 未在应用 {app_name} 的TestFlight测试组中") else: results.append(f"从TestFlight移除用户失败: {str(e)}") except Exception as e: results.append(f"从TestFlight移除用户失败: {str(e)}") # 2. 从团队中移除 try: self.remove_team_member(email) results.append(f"已从团队中移除用户 {email}") except ValueError as e: if "不在团队中" in str(e): results.append(f"用户 {email} 未在团队中") else: results.append(f"从团队移除用户失败: {str(e)}") except Exception as e: results.append(f"从团队移除用户失败: {str(e)}") return "\n".join(results) except Exception as e: return f"移除用户过程中发生错误: {str(e)}" def invite_user_and_wait_for_testflight(self, email: str, first_name: str, app_name: str, roles: List[str], status_callback: Optional[Callable[[str, str], None]] = None) -> str: """邀请用户加入团队,并异步等待其接受邀请后添加到TestFlight""" try: # 1. 检查用户是否已在团队中 existing_member = self.check_user_in_team(email) if existing_member: # 用户已在团队中,直接添加到TestFlight app = self.client.handlers["AppHandler"].get_app_by_name(app_name) if not app: return f"未找到应用: {app_name}" beta_groups = self.client.handlers["TestFlightHandler"].get_beta_groups_for_app(app.id) internal_groups = [group for group in beta_groups if group.is_internal_group] if not internal_groups: return f"警告: 应用 {app_name} 没有内部测试组" default_group = internal_groups[0] try: self.client.handlers["TestFlightHandler"].add_beta_tester(email, first_name, [default_group.id]) return f"用户 {email} 已在团队中,已直接添加到TestFlight测试组: {default_group.name}" except Exception as e: if "already exists" in str(e).lower(): return f"用户 {email} 已在团队和TestFlight测试组中" else: return f"添加TestFlight测试者失败: {str(e)}" # 2. 获取应用信息 app = self.client.handlers["AppHandler"].get_app_by_name(app_name) if not app: return f"未找到应用: {app_name}" # 3. 邀请用户到团队 try: self.invite_user_to_team(email, first_name, roles, [app.id]) except Exception as e: return f"邀请用户失败: {str(e)}" # 4. 启动异步轮询监控 self._poll_user_acceptance(email, first_name, app_name, status_callback=status_callback) return f"已邀请用户 {email} 加入团队(角色: {', '.join(roles)}),正在后台监控用户接受邀请状态。监控时间最长2小时,每5分钟检查一次。" except Exception as e: return f"操作失败: {str(e)}" def get_polling_status(self, email: Optional[str] = None) -> Dict[str, Any]: """获取轮询任务状态""" if email: # 查找指定用户的任务 for task_id, task_info in self._polling_tasks.items(): if task_info['email'].lower() == email.lower(): elapsed = datetime.now() - task_info['start_time'] return { 'task_id': task_id, 'email': task_info['email'], 'app_name': task_info['app_name'], 'status': task_info['status'], 'elapsed_minutes': elapsed.total_seconds() // 60, 'thread_alive': task_info['thread'].is_alive() } return {'error': f'未找到用户 {email} 的轮询任务'} else: # 返回所有任务状态 tasks = {} for task_id, task_info in self._polling_tasks.items(): elapsed = datetime.now() - task_info['start_time'] tasks[task_id] = { 'email': task_info['email'], 'app_name': task_info['app_name'], 'status': task_info['status'], 'elapsed_minutes': elapsed.total_seconds() // 60, 'thread_alive': task_info['thread'].is_alive() } return {'tasks': tasks, 'total_count': len(tasks)} def cancel_polling_task(self, email: str) -> str: """取消指定用户的轮询任务""" task_to_remove = None for task_id, task_info in self._polling_tasks.items(): if task_info['email'].lower() == email.lower(): task_to_remove = task_id break if task_to_remove: del self._polling_tasks[task_to_remove] if email in self._status_callbacks: del self._status_callbacks[email] return f"已取消用户 {email} 的轮询任务" else: return f"未找到用户 {email} 的轮询任务" def _poll_user_acceptance(self, email: str, first_name: str, app_name: str, max_duration_hours: int = 2, poll_interval_minutes: int = 5, status_callback: Optional[Callable[[str, str], None]] = None) -> None: """轮询检查用户是否接受邀请并加入团队""" def polling_thread(): start_time = datetime.now() end_time = start_time + timedelta(hours=max_duration_hours) poll_interval = timedelta(minutes=poll_interval_minutes) pool_task_id = f"{email}_{int(time.time())}" try: if status_callback: status_callback(email, f"开始轮询检查用户 {email} 是否接受邀请") while datetime.now() < end_time: try: # 检查用户是否已在团队中 member = self.check_user_in_team(email) if member: if status_callback: status_callback(email, f"用户 {email} 已接受邀请并加入团队") # 用户已加入团队,现在添加到TestFlight try: app = self.client.handlers["AppHandler"].get_app_by_name(app_name) if not app: if status_callback: status_callback(email, f"错误: 未找到应用 {app_name}") return beta_groups = self.client.handlers["TestFlightHandler"].get_beta_groups_for_app(app.id) internal_groups = [group for group in beta_groups if group.is_internal_group] if not internal_groups: if status_callback: status_callback(email, f"警告: 应用 {app_name} 没有内部测试组") return default_group = internal_groups[0] self.client.handlers["TestFlightHandler"].add_beta_tester(email, first_name, [default_group.id]) if status_callback: status_callback(email, f"成功: 用户 {email} 已添加到TestFlight测试组 {default_group.name}") except Exception as e: if "already exists" in str(e).lower(): if status_callback: status_callback(email, f"用户 {email} 已在TestFlight测试组中") else: if status_callback: status_callback(email, f"添加TestFlight测试者失败: {str(e)}") # 任务完成,清理 if pool_task_id in self._polling_tasks: del self._polling_tasks[pool_task_id] return # 用户还未接受邀请,继续等待 elapsed = datetime.now() - start_time remaining = end_time - datetime.now() if status_callback: status_callback(email, f"用户 {email} 尚未接受邀请。已等待: {elapsed.seconds // 60}分钟,剩余: {remaining.seconds // 60}分钟") # 等待下一次轮询 time.sleep(poll_interval.total_seconds()) except Exception as e: if status_callback: status_callback(email, f"轮询检查时发生错误: {str(e)}") time.sleep(poll_interval.total_seconds()) # 超时 if status_callback: status_callback(email, f"超时: 用户 {email} 在 {max_duration_hours} 小时内未接受邀请") finally: # 清理任务 if pool_task_id in self._polling_tasks: del self._polling_tasks[pool_task_id] if email in self._status_callbacks: del self._status_callbacks[email] # 启动轮询线程 task_id = f"{email}_{int(time.time())}" thread = threading.Thread(target=polling_thread, daemon=True) thread.start() self._polling_tasks[task_id] = { 'thread': thread, 'email': email, 'app_name': app_name, 'start_time': datetime.now(), 'status': 'polling' } if status_callback: self._status_callbacks[email] = status_callback

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/peroperogames/pero-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server