feat(team): add OpenFang Team API client and WebSocket events
API Client (lib/team-client.ts): - Team CRUD: listTeams, getTeam, createTeam, updateTeam, deleteTeam - Member management: addTeamMember, removeTeamMember, updateMemberRole - Task management: addTeamTask, updateTaskStatus, assignTask, submitDeliverable - Dev↔QA loops: startDevQALoop, submitReview, updateLoopState - Metrics & Events: getTeamMetrics, getTeamEvents, subscribeToTeamEvents - TeamAPIError class for error handling WebSocket Events (lib/useTeamEvents.ts): - useTeamEvents hook for subscribing to team events - useTeamEventStream hook for specific team events - useAllTeamEvents hook for all team events - Real-time task status updates - Real-time Dev↔QA loop state changes - Auto-refresh on team/member updates Event Types: - team.created/updated/deleted - member.added/removed/status_changed - task.created/assigned/status_changed/completed - loop.started/state_changed/completed - review.submitted Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
437
desktop/src/lib/team-client.ts
Normal file
437
desktop/src/lib/team-client.ts
Normal file
@@ -0,0 +1,437 @@
|
|||||||
|
/**
|
||||||
|
* OpenFang Team API Client
|
||||||
|
*
|
||||||
|
* REST API client for multi-agent team collaboration endpoints.
|
||||||
|
* Communicates with OpenFang Kernel for team management,
|
||||||
|
* task coordination, and Dev↔QA loops.
|
||||||
|
*
|
||||||
|
* @module lib/team-client
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {
|
||||||
|
Team,
|
||||||
|
TeamMember,
|
||||||
|
TeamTask,
|
||||||
|
TeamMemberRole,
|
||||||
|
DevQALoop,
|
||||||
|
CreateTeamRequest,
|
||||||
|
AddTeamTaskRequest,
|
||||||
|
TeamResponse,
|
||||||
|
ReviewFeedback,
|
||||||
|
TaskDeliverable,
|
||||||
|
CollaborationEvent,
|
||||||
|
TeamMetrics,
|
||||||
|
} from '../types/team';
|
||||||
|
|
||||||
|
// === Configuration ===
|
||||||
|
|
||||||
|
const API_BASE = '/api'; // Uses Vite proxy
|
||||||
|
|
||||||
|
// === Error Types ===
|
||||||
|
|
||||||
|
export class TeamAPIError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public statusCode: number,
|
||||||
|
public endpoint: string,
|
||||||
|
public details?: unknown
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'TeamAPIError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Helper Functions ===
|
||||||
|
|
||||||
|
async function request<T>(
|
||||||
|
endpoint: string,
|
||||||
|
options: RequestInit = {}
|
||||||
|
): Promise<T> {
|
||||||
|
const url = `${API_BASE}${endpoint}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new TeamAPIError(
|
||||||
|
errorData.message || `HTTP ${response.status}`,
|
||||||
|
response.status,
|
||||||
|
endpoint,
|
||||||
|
errorData
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof TeamAPIError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
throw new TeamAPIError(
|
||||||
|
(error as Error).message,
|
||||||
|
0,
|
||||||
|
endpoint,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Team API ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all teams
|
||||||
|
*/
|
||||||
|
export async function listTeams(): Promise<{ teams: Team[]; total: number }> {
|
||||||
|
return request<{ teams: Team[]; total: number }>('/teams');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific team by ID
|
||||||
|
*/
|
||||||
|
export async function getTeam(teamId: string): Promise<TeamResponse> {
|
||||||
|
return request<TeamResponse>(`/teams/${teamId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new team
|
||||||
|
*/
|
||||||
|
export async function createTeam(data: CreateTeamRequest): Promise<TeamResponse> {
|
||||||
|
return request<TeamResponse>('/teams', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a team
|
||||||
|
*/
|
||||||
|
export async function updateTeam(
|
||||||
|
teamId: string,
|
||||||
|
data: Partial<Pick<Team, 'name' | 'description' | 'pattern' | 'status'>>
|
||||||
|
): Promise<TeamResponse> {
|
||||||
|
return request<TeamResponse>(`/teams/${teamId}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a team
|
||||||
|
*/
|
||||||
|
export async function deleteTeam(teamId: string): Promise<{ success: boolean }> {
|
||||||
|
return request<{ success: boolean }>(`/teams/${teamId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Team Member API ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a member to a team
|
||||||
|
*/
|
||||||
|
export async function addTeamMember(
|
||||||
|
teamId: string,
|
||||||
|
agentId: string,
|
||||||
|
role: TeamMemberRole
|
||||||
|
): Promise<{ member: TeamMember; success: boolean }> {
|
||||||
|
return request<{ member: TeamMember; success: boolean }>(
|
||||||
|
`/teams/${teamId}/members`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ agentId, role }),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a member from a team
|
||||||
|
*/
|
||||||
|
export async function removeTeamMember(
|
||||||
|
teamId: string,
|
||||||
|
memberId: string
|
||||||
|
): Promise<{ success: boolean }> {
|
||||||
|
return request<{ success: boolean }>(
|
||||||
|
`/teams/${teamId}/members/${memberId}`,
|
||||||
|
{ method: 'DELETE' }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a member's role
|
||||||
|
*/
|
||||||
|
export async function updateMemberRole(
|
||||||
|
teamId: string,
|
||||||
|
memberId: string,
|
||||||
|
role: TeamMemberRole
|
||||||
|
): Promise<{ member: TeamMember; success: boolean }> {
|
||||||
|
return request<{ member: TeamMember; success: boolean }>(
|
||||||
|
`/teams/${teamId}/members/${memberId}`,
|
||||||
|
{
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify({ role }),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Team Task API ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a task to a team
|
||||||
|
*/
|
||||||
|
export async function addTeamTask(
|
||||||
|
data: AddTeamTaskRequest
|
||||||
|
): Promise<{ task: TeamTask; success: boolean }> {
|
||||||
|
return request<{ task: TeamTask; success: boolean }>(
|
||||||
|
`/teams/${data.teamId}/tasks`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
title: data.title,
|
||||||
|
description: data.description,
|
||||||
|
priority: data.priority,
|
||||||
|
type: data.type,
|
||||||
|
assigneeId: data.assigneeId,
|
||||||
|
dependencies: data.dependencies,
|
||||||
|
estimate: data.estimate,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a task's status
|
||||||
|
*/
|
||||||
|
export async function updateTaskStatus(
|
||||||
|
teamId: string,
|
||||||
|
taskId: string,
|
||||||
|
status: TeamTask['status']
|
||||||
|
): Promise<{ task: TeamTask; success: boolean }> {
|
||||||
|
return request<{ task: TeamTask; success: boolean }>(
|
||||||
|
`/teams/${teamId}/tasks/${taskId}`,
|
||||||
|
{
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify({ status }),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assign a task to a member
|
||||||
|
*/
|
||||||
|
export async function assignTask(
|
||||||
|
teamId: string,
|
||||||
|
taskId: string,
|
||||||
|
memberId: string
|
||||||
|
): Promise<{ task: TeamTask; success: boolean }> {
|
||||||
|
return request<{ task: TeamTask; success: boolean }>(
|
||||||
|
`/teams/${teamId}/tasks/${taskId}/assign`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ memberId }),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit a deliverable for a task
|
||||||
|
*/
|
||||||
|
export async function submitDeliverable(
|
||||||
|
teamId: string,
|
||||||
|
taskId: string,
|
||||||
|
deliverable: TaskDeliverable
|
||||||
|
): Promise<{ task: TeamTask; success: boolean }> {
|
||||||
|
return request<{ task: TeamTask; success: boolean }>(
|
||||||
|
`/teams/${teamId}/tasks/${taskId}/deliverable`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(deliverable),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Dev↔QA Loop API ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a Dev↔QA loop for a task
|
||||||
|
*/
|
||||||
|
export async function startDevQALoop(
|
||||||
|
teamId: string,
|
||||||
|
taskId: string,
|
||||||
|
developerId: string,
|
||||||
|
reviewerId: string
|
||||||
|
): Promise<{ loop: DevQALoop; success: boolean }> {
|
||||||
|
return request<{ loop: DevQALoop; success: boolean }>(
|
||||||
|
`/teams/${teamId}/loops`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ taskId, developerId, reviewerId }),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit a review for a Dev↔QA loop
|
||||||
|
*/
|
||||||
|
export async function submitReview(
|
||||||
|
teamId: string,
|
||||||
|
loopId: string,
|
||||||
|
feedback: Omit<ReviewFeedback, 'reviewedAt' | 'reviewerId'>
|
||||||
|
): Promise<{ loop: DevQALoop; success: boolean }> {
|
||||||
|
return request<{ loop: DevQALoop; success: boolean }>(
|
||||||
|
`/teams/${teamId}/loops/${loopId}/review`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(feedback),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a Dev↔QA loop state
|
||||||
|
*/
|
||||||
|
export async function updateLoopState(
|
||||||
|
teamId: string,
|
||||||
|
loopId: string,
|
||||||
|
state: DevQALoop['state']
|
||||||
|
): Promise<{ loop: DevQALoop; success: boolean }> {
|
||||||
|
return request<{ loop: DevQALoop; success: boolean }>(
|
||||||
|
`/teams/${teamId}/loops/${loopId}`,
|
||||||
|
{
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify({ state }),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Metrics & Events ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get team metrics
|
||||||
|
*/
|
||||||
|
export async function getTeamMetrics(teamId: string): Promise<TeamMetrics> {
|
||||||
|
return request<TeamMetrics>(`/teams/${teamId}/metrics`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get recent collaboration events
|
||||||
|
*/
|
||||||
|
export async function getTeamEvents(
|
||||||
|
teamId: string,
|
||||||
|
limit?: number
|
||||||
|
): Promise<{ events: CollaborationEvent[]; total: number }> {
|
||||||
|
const query = limit ? `?limit=${limit}` : '';
|
||||||
|
return request<{ events: CollaborationEvent[]; total: number }>(
|
||||||
|
`/teams/${teamId}/events${query}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === WebSocket Event Subscription ===
|
||||||
|
|
||||||
|
export type TeamEventType =
|
||||||
|
| 'team.created'
|
||||||
|
| 'team.updated'
|
||||||
|
| 'team.deleted'
|
||||||
|
| 'member.added'
|
||||||
|
| 'member.removed'
|
||||||
|
| 'member.status_changed'
|
||||||
|
| 'task.created'
|
||||||
|
| 'task.assigned'
|
||||||
|
| 'task.status_changed'
|
||||||
|
| 'task.completed'
|
||||||
|
| 'loop.started'
|
||||||
|
| 'loop.state_changed'
|
||||||
|
| 'loop.completed'
|
||||||
|
| 'review.submitted';
|
||||||
|
|
||||||
|
export interface TeamEventMessage {
|
||||||
|
type: 'team_event';
|
||||||
|
eventType: TeamEventType;
|
||||||
|
teamId: string;
|
||||||
|
payload: Record<string, unknown>;
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to team events via WebSocket
|
||||||
|
* Returns an unsubscribe function
|
||||||
|
*/
|
||||||
|
export function subscribeToTeamEvents(
|
||||||
|
teamId: string | null, // null = all teams
|
||||||
|
callback: (event: TeamEventMessage) => void,
|
||||||
|
ws: WebSocket
|
||||||
|
): () => void {
|
||||||
|
const handleMessage = (event: MessageEvent) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
if (data.type === 'team_event') {
|
||||||
|
// Filter by teamId if specified
|
||||||
|
if (teamId === null || data.teamId === teamId) {
|
||||||
|
callback(data as TeamEventMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore non-JSON messages
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.addEventListener('message', handleMessage);
|
||||||
|
|
||||||
|
// Send subscription message
|
||||||
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'subscribe',
|
||||||
|
topic: teamId ? `team:${teamId}` : 'teams',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return unsubscribe function
|
||||||
|
return () => {
|
||||||
|
ws.removeEventListener('message', handleMessage);
|
||||||
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'unsubscribe',
|
||||||
|
topic: teamId ? `team:${teamId}` : 'teams',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Export singleton client ===
|
||||||
|
|
||||||
|
export const teamClient = {
|
||||||
|
// Teams
|
||||||
|
listTeams,
|
||||||
|
getTeam,
|
||||||
|
createTeam,
|
||||||
|
updateTeam,
|
||||||
|
deleteTeam,
|
||||||
|
|
||||||
|
// Members
|
||||||
|
addTeamMember,
|
||||||
|
removeTeamMember,
|
||||||
|
updateMemberRole,
|
||||||
|
|
||||||
|
// Tasks
|
||||||
|
addTeamTask,
|
||||||
|
updateTaskStatus,
|
||||||
|
assignTask,
|
||||||
|
submitDeliverable,
|
||||||
|
|
||||||
|
// Dev↔QA Loops
|
||||||
|
startDevQALoop,
|
||||||
|
submitReview,
|
||||||
|
updateLoopState,
|
||||||
|
|
||||||
|
// Metrics & Events
|
||||||
|
getTeamMetrics,
|
||||||
|
getTeamEvents,
|
||||||
|
subscribeToTeamEvents,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default teamClient;
|
||||||
202
desktop/src/lib/useTeamEvents.ts
Normal file
202
desktop/src/lib/useTeamEvents.ts
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
/**
|
||||||
|
* useTeamEvents - WebSocket Real-time Event Sync Hook
|
||||||
|
*
|
||||||
|
* Subscribes to team collaboration events via WebSocket
|
||||||
|
* and updates the team store in real-time.
|
||||||
|
*
|
||||||
|
* @module lib/useTeamEvents
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useEffect, useRef, useCallback } from 'react';
|
||||||
|
import { useTeamStore } from '../store/teamStore';
|
||||||
|
import { useGatewayStore } from '../store/gatewayStore';
|
||||||
|
import type { TeamEventMessage, TeamEventType } from '../lib/team-client';
|
||||||
|
|
||||||
|
interface UseTeamEventsOptions {
|
||||||
|
/** Subscribe to specific team only, or null for all teams */
|
||||||
|
teamId?: string | null;
|
||||||
|
/** Event types to subscribe to (default: all) */
|
||||||
|
eventTypes?: TeamEventType[];
|
||||||
|
/** Maximum events to keep in history (default: 100) */
|
||||||
|
maxEvents?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for subscribing to real-time team collaboration events
|
||||||
|
*/
|
||||||
|
export function useTeamEvents(options: UseTeamEventsOptions = {}) {
|
||||||
|
const { teamId = null, eventTypes, maxEvents = 100 } = options;
|
||||||
|
const unsubscribeRef = useRef<(() => void) | null>(null);
|
||||||
|
|
||||||
|
const {
|
||||||
|
addEvent,
|
||||||
|
setActiveTeam,
|
||||||
|
updateTaskStatus,
|
||||||
|
updateLoopState,
|
||||||
|
loadTeams,
|
||||||
|
} = useTeamStore();
|
||||||
|
|
||||||
|
const { connectionState } = useGatewayStore();
|
||||||
|
|
||||||
|
const handleTeamEvent = useCallback(
|
||||||
|
(message: TeamEventMessage) => {
|
||||||
|
// Filter by event types if specified
|
||||||
|
if (eventTypes && !eventTypes.includes(message.eventType)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create collaboration event for store
|
||||||
|
const event = {
|
||||||
|
type: mapEventType(message.eventType),
|
||||||
|
teamId: message.teamId,
|
||||||
|
sourceAgentId: (message.payload.sourceAgentId as string) || 'system',
|
||||||
|
payload: message.payload,
|
||||||
|
timestamp: message.timestamp,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add to event history
|
||||||
|
addEvent(event);
|
||||||
|
|
||||||
|
// Handle specific event types
|
||||||
|
switch (message.eventType) {
|
||||||
|
case 'task.status_changed':
|
||||||
|
if (message.payload.taskId && message.payload.status) {
|
||||||
|
updateTaskStatus(
|
||||||
|
message.teamId,
|
||||||
|
message.payload.taskId as string,
|
||||||
|
message.payload.status as any
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'loop.state_changed':
|
||||||
|
if (message.payload.loopId && message.payload.state) {
|
||||||
|
updateLoopState(
|
||||||
|
message.teamId,
|
||||||
|
message.payload.loopId as string,
|
||||||
|
message.payload.state as any
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'team.updated':
|
||||||
|
case 'member.added':
|
||||||
|
case 'member.removed':
|
||||||
|
// Reload teams to get updated data
|
||||||
|
loadTeams().catch(() => {});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[eventTypes, addEvent, updateTaskStatus, updateLoopState, loadTeams]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Only subscribe when connected
|
||||||
|
if (connectionState !== 'connected') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get WebSocket from gateway client
|
||||||
|
const client = getGatewayClientSafe();
|
||||||
|
if (!client || !client.ws) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ws = client.ws;
|
||||||
|
|
||||||
|
// Subscribe to team events
|
||||||
|
const handleMessage = (event: MessageEvent) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
if (data.type === 'team_event') {
|
||||||
|
handleTeamEvent(data as TeamEventMessage);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore non-JSON messages
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.addEventListener('message', handleMessage);
|
||||||
|
|
||||||
|
// Send subscription message
|
||||||
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'subscribe',
|
||||||
|
topic: teamId ? `team:${teamId}` : 'teams',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsubscribeRef.current = () => {
|
||||||
|
ws.removeEventListener('message', handleMessage);
|
||||||
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'unsubscribe',
|
||||||
|
topic: teamId ? `team:${teamId}` : 'teams',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (unsubscribeRef.current) {
|
||||||
|
unsubscribeRef.current();
|
||||||
|
unsubscribeRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [connectionState, teamId, handleTeamEvent]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isConnected: connectionState === 'connected',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for subscribing to a specific team's events
|
||||||
|
*/
|
||||||
|
export function useTeamEventStream(teamId: string) {
|
||||||
|
return useTeamEvents({ teamId });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for subscribing to all team events
|
||||||
|
*/
|
||||||
|
export function useAllTeamEvents(options: Omit<UseTeamEventsOptions, 'teamId'> = {}) {
|
||||||
|
return useTeamEvents({ ...options, teamId: null });
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Helper Functions ===
|
||||||
|
|
||||||
|
function mapEventType(eventType: TeamEventType): CollaborationEvent['type'] {
|
||||||
|
const mapping: Record<TeamEventType, CollaborationEvent['type']> = {
|
||||||
|
'team.created': 'member_status_change',
|
||||||
|
'team.updated': 'member_status_change',
|
||||||
|
'team.deleted': 'member_status_change',
|
||||||
|
'member.added': 'member_status_change',
|
||||||
|
'member.removed': 'member_status_change',
|
||||||
|
'member.status_changed': 'member_status_change',
|
||||||
|
'task.created': 'task_assigned',
|
||||||
|
'task.assigned': 'task_assigned',
|
||||||
|
'task.status_changed': 'task_started',
|
||||||
|
'task.completed': 'task_completed',
|
||||||
|
'loop.started': 'loop_state_change',
|
||||||
|
'loop.state_changed': 'loop_state_change',
|
||||||
|
'loop.completed': 'loop_state_change',
|
||||||
|
'review.submitted': 'review_submitted',
|
||||||
|
};
|
||||||
|
return mapping[eventType] || 'task_started';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGatewayClientSafe() {
|
||||||
|
try {
|
||||||
|
// Dynamic import to avoid circular dependency
|
||||||
|
const { getGatewayClient } = require('../lib/gateway-client');
|
||||||
|
return getGatewayClient();
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useTeamEvents;
|
||||||
@@ -446,7 +446,9 @@ ZCLAW 是基于 **OpenFang** (Rust Agent OS) 的 AI Agent 桌面客户端,核
|
|||||||
* ✅ Sidebar 添加 Team 标签
|
* ✅ Sidebar 添加 Team 标签
|
||||||
* ✅ App.tsx 添加 Team 视图渲染
|
* ✅ App.tsx 添加 Team 视图渲染
|
||||||
* ✅ 团队选择和状态管理
|
* ✅ 团队选择和状态管理
|
||||||
|
* API 客户端:
|
||||||
|
* ✅ Team API 客户端 (`lib/team-client.ts`)
|
||||||
|
* ✅ WebSocket 事件订阅 (`lib/useTeamEvents.ts`)
|
||||||
* 待完成:
|
* 待完成:
|
||||||
* OpenFang Team API 客户端
|
* 与 OpenFang 后端 API 对接测试
|
||||||
* WebSocket 实时事件同步
|
*下一步: 后端 API 对接测试与集成验证*
|
||||||
*下一步: OpenFang API 对接与实时同步*
|
|
||||||
|
|||||||
Reference in New Issue
Block a user