refactor: 移除 Team 和 Swarm 协作功能
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

功能论证结论:Team(团队)和 Swarm(协作)为零后端支持的
纯前端 localStorage 空壳,Pipeline 系统已完全覆盖其全部能力。

删除 16 个文件,约 7,950 行代码:
- 5 个组件:TeamCollaborationView, TeamOrchestrator, TeamList, DevQALoop, SwarmDashboard
- 1 个 Store:teamStore.ts
- 3 个 Client/库:team-client.ts, useTeamEvents.ts, agent-swarm.ts
- 1 个类型文件:team.ts
- 4 个测试文件
- 1 个文档(归档 swarm-coordination.md)

修改 4 个文件:
- Sidebar.tsx:移除"团队"和"协作"导航项
- App.tsx:移除 team/swarm 视图路由
- types/index.ts:移除 team 类型导出
- chatStore.ts:移除 dispatchSwarmTask 方法

更新 CHANGELOG.md 和功能文档 README.md
This commit is contained in:
iven
2026-03-26 20:27:19 +08:00
parent 978dc5cdd8
commit c3996573aa
22 changed files with 11 additions and 7689 deletions

View File

@@ -1,549 +0,0 @@
/**
* Agent Swarm - Multi-Agent collaboration framework for ZCLAW
*
* Enables multiple agents (clones) to collaborate on complex tasks through:
* - Sequential: Agents process in chain, each building on the previous
* - Parallel: Agents work simultaneously on different subtasks
* - Debate: Agents provide competing perspectives, coordinator synthesizes
*
* Integrates with existing Clone/Agent infrastructure via agentStore/gatewayStore.
*
* Reference: ZCLAW_AGENT_INTELLIGENCE_EVOLUTION.md §6.5.1
*/
import { intelligenceClient } from './intelligence-client';
// === Types ===
export type CommunicationStyle = 'sequential' | 'parallel' | 'debate';
export type TaskDecomposition = 'auto' | 'manual';
export type SubtaskStatus = 'pending' | 'running' | 'done' | 'failed';
export type SwarmTaskStatus = 'planning' | 'executing' | 'aggregating' | 'done' | 'failed';
export interface SwarmSpecialist {
agentId: string;
role: string;
capabilities: string[];
model?: string;
}
export interface SwarmConfig {
coordinator: string;
specialists: SwarmSpecialist[];
taskDecomposition: TaskDecomposition;
communicationStyle: CommunicationStyle;
maxRoundsDebate: number;
timeoutPerSubtaskMs: number;
}
export interface Subtask {
id: string;
assignedTo: string;
description: string;
status: SubtaskStatus;
result?: string;
error?: string;
startedAt?: string;
completedAt?: string;
round?: number;
}
export interface SwarmTask {
id: string;
description: string;
subtasks: Subtask[];
status: SwarmTaskStatus;
communicationStyle: CommunicationStyle;
finalResult?: string;
createdAt: string;
completedAt?: string;
metadata?: Record<string, unknown>;
}
export interface SwarmExecutionResult {
task: SwarmTask;
summary: string;
participantCount: number;
totalDurationMs: number;
}
export type AgentExecutor = (
agentId: string,
prompt: string,
context?: string
) => Promise<string>;
// === Default Config ===
export const DEFAULT_SWARM_CONFIG: SwarmConfig = {
coordinator: 'zclaw-main',
specialists: [],
taskDecomposition: 'auto',
communicationStyle: 'sequential',
maxRoundsDebate: 3,
timeoutPerSubtaskMs: 60_000,
};
// === Storage ===
const SWARM_HISTORY_KEY = 'zclaw-swarm-history';
// === Swarm Engine ===
export class AgentSwarm {
private config: SwarmConfig;
private history: SwarmTask[] = [];
private executor: AgentExecutor | null = null;
constructor(config?: Partial<SwarmConfig>) {
this.config = { ...DEFAULT_SWARM_CONFIG, ...config };
this.loadHistory();
}
/**
* Set the executor function used to dispatch prompts to individual agents.
* This decouples the swarm from the gateway transport layer.
*/
setExecutor(executor: AgentExecutor): void {
this.executor = executor;
}
// === Task Creation ===
/**
* Create a new swarm task. If taskDecomposition is 'auto', subtasks are
* generated based on specialist capabilities and the task description.
*/
createTask(
description: string,
options?: {
subtasks?: Array<{ assignedTo: string; description: string }>;
communicationStyle?: CommunicationStyle;
metadata?: Record<string, unknown>;
}
): SwarmTask {
const style = options?.communicationStyle || this.config.communicationStyle;
const taskId = `swarm_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
let subtasks: Subtask[];
if (options?.subtasks && options.subtasks.length > 0) {
// Manual decomposition
subtasks = options.subtasks.map((st, i) => ({
id: `${taskId}_sub_${i}`,
assignedTo: st.assignedTo,
description: st.description,
status: 'pending' as SubtaskStatus,
}));
} else {
// Auto decomposition based on specialists
subtasks = this.autoDecompose(taskId, description, style);
}
const task: SwarmTask = {
id: taskId,
description,
subtasks,
status: 'planning',
communicationStyle: style,
createdAt: new Date().toISOString(),
metadata: options?.metadata,
};
return task;
}
/**
* Execute a swarm task using the configured communication style.
*/
async execute(task: SwarmTask): Promise<SwarmExecutionResult> {
if (!this.executor) {
throw new Error('[AgentSwarm] No executor set. Call setExecutor() first.');
}
const startTime = Date.now();
task.status = 'executing';
try {
switch (task.communicationStyle) {
case 'sequential':
await this.executeSequential(task);
break;
case 'parallel':
await this.executeParallel(task);
break;
case 'debate':
await this.executeDebate(task);
break;
}
// Aggregation phase
task.status = 'aggregating';
task.finalResult = await this.aggregate(task);
task.status = 'done';
task.completedAt = new Date().toISOString();
} catch (err) {
task.status = 'failed';
task.finalResult = `执行失败: ${err instanceof Error ? err.message : String(err)}`;
task.completedAt = new Date().toISOString();
}
const totalDurationMs = Date.now() - startTime;
// Store in history
this.history.push(task);
if (this.history.length > 50) {
this.history = this.history.slice(-25);
}
this.saveHistory();
// Save task result as memory
try {
await intelligenceClient.memory.store({
agent_id: this.config.coordinator,
memory_type: 'lesson',
content: `协作任务完成: "${task.description}" — ${task.subtasks.length}个子任务, 模式: ${task.communicationStyle}, 结果: ${(task.finalResult || '').slice(0, 200)}`,
importance: 6,
source: 'auto',
tags: ['swarm', task.communicationStyle],
});
} catch { /* non-critical */ }
const result: SwarmExecutionResult = {
task,
summary: task.finalResult || '',
participantCount: new Set(task.subtasks.map(s => s.assignedTo)).size,
totalDurationMs,
};
console.log(
`[AgentSwarm] Task "${task.description}" completed in ${totalDurationMs}ms, ` +
`${result.participantCount} participants, ${task.subtasks.length} subtasks`
);
return result;
}
// === Execution Strategies ===
/**
* Sequential: Each agent runs in order, receiving the previous agent's output as context.
*/
private async executeSequential(task: SwarmTask): Promise<void> {
let previousResult = '';
for (const subtask of task.subtasks) {
subtask.status = 'running';
subtask.startedAt = new Date().toISOString();
try {
const context = previousResult
? `前一个Agent的输出:\n${previousResult}\n\n请基于以上内容继续完成你的部分。`
: '';
const result = await this.executeSubtask(subtask, context);
subtask.result = result;
subtask.status = 'done';
previousResult = result;
} catch (err) {
subtask.status = 'failed';
subtask.error = err instanceof Error ? err.message : String(err);
}
subtask.completedAt = new Date().toISOString();
}
}
/**
* Parallel: All agents run simultaneously, results collected independently.
*/
private async executeParallel(task: SwarmTask): Promise<void> {
const promises = task.subtasks.map(async (subtask) => {
subtask.status = 'running';
subtask.startedAt = new Date().toISOString();
try {
const result = await this.executeSubtask(subtask);
subtask.result = result;
subtask.status = 'done';
} catch (err) {
subtask.status = 'failed';
subtask.error = err instanceof Error ? err.message : String(err);
}
subtask.completedAt = new Date().toISOString();
});
await Promise.allSettled(promises);
}
/**
* Debate: All agents respond to the same prompt in multiple rounds.
* Each round, agents can see previous round results.
*/
private async executeDebate(task: SwarmTask): Promise<void> {
const maxRounds = this.config.maxRoundsDebate;
const agents = [...new Set(task.subtasks.map(s => s.assignedTo))];
for (let round = 1; round <= maxRounds; round++) {
const roundContext = round > 1
? this.buildDebateContext(task, round - 1)
: '';
const roundPromises = agents.map(async (agentId) => {
const subtaskId = `${task.id}_debate_r${round}_${agentId}`;
const subtask: Subtask = {
id: subtaskId,
assignedTo: agentId,
description: task.description,
status: 'running',
startedAt: new Date().toISOString(),
round,
};
try {
const prompt = round === 1
? task.description
: `这是第${round}轮讨论。请参考其他Agent的观点给出你的更新后的分析。\n\n${roundContext}`;
const result = await this.executeSubtask(subtask, '', prompt);
subtask.result = result;
subtask.status = 'done';
} catch (err) {
subtask.status = 'failed';
subtask.error = err instanceof Error ? err.message : String(err);
}
subtask.completedAt = new Date().toISOString();
task.subtasks.push(subtask);
});
await Promise.allSettled(roundPromises);
// Check if consensus reached (all agents give similar answers)
if (round < maxRounds && this.checkConsensus(task, round)) {
console.log(`[AgentSwarm] Debate consensus reached at round ${round}`);
break;
}
}
}
// === Helpers ===
private async executeSubtask(
subtask: Subtask,
context?: string,
promptOverride?: string
): Promise<string> {
if (!this.executor) throw new Error('No executor');
const prompt = promptOverride || subtask.description;
return this.executor(subtask.assignedTo, prompt, context);
}
/**
* Auto-decompose a task into subtasks based on specialist roles.
*/
private autoDecompose(
taskId: string,
description: string,
style: CommunicationStyle
): Subtask[] {
const specialists = this.config.specialists;
if (specialists.length === 0) {
// Single agent fallback
return [{
id: `${taskId}_sub_0`,
assignedTo: this.config.coordinator,
description,
status: 'pending',
}];
}
if (style === 'debate') {
// In debate mode, all specialists get the same task
return specialists.map((spec, i) => ({
id: `${taskId}_sub_${i}`,
assignedTo: spec.agentId,
description: `作为${spec.role},请分析以下问题: ${description}`,
status: 'pending' as SubtaskStatus,
round: 1,
}));
}
if (style === 'parallel') {
// In parallel, assign based on capabilities
return specialists.map((spec, i) => ({
id: `${taskId}_sub_${i}`,
assignedTo: spec.agentId,
description: `${spec.role}的角度完成: ${description}`,
status: 'pending' as SubtaskStatus,
}));
}
// Sequential: chain through specialists
return specialists.map((spec, i) => ({
id: `${taskId}_sub_${i}`,
assignedTo: spec.agentId,
description: i === 0
? `作为${spec.role},请首先分析: ${description}`
: `作为${spec.role},请基于前一位同事的分析继续深化: ${description}`,
status: 'pending' as SubtaskStatus,
}));
}
private buildDebateContext(task: SwarmTask, upToRound: number): string {
const roundSubtasks = task.subtasks.filter(
s => s.round && s.round <= upToRound && s.status === 'done'
);
if (roundSubtasks.length === 0) return '';
const lines = roundSubtasks.map(s => {
const specialist = this.config.specialists.find(sp => sp.agentId === s.assignedTo);
const role = specialist?.role || s.assignedTo;
return `**${role}** (第${s.round}轮):\n${s.result || '(无输出)'}`;
});
return `其他Agent的观点:\n\n${lines.join('\n\n---\n\n')}`;
}
private checkConsensus(task: SwarmTask, round: number): boolean {
const roundResults = task.subtasks
.filter(s => s.round === round && s.status === 'done' && s.result)
.map(s => s.result!);
if (roundResults.length < 2) return false;
// Simple heuristic: if all results share > 60% keywords, consider consensus
const tokenSets = roundResults.map(r =>
new Set(r.toLowerCase().split(/[\s,;.!?。,;!?]+/).filter(t => t.length > 1))
);
for (let i = 1; i < tokenSets.length; i++) {
const common = [...tokenSets[0]].filter(t => tokenSets[i].has(t));
const similarity = (2 * common.length) / (tokenSets[0].size + tokenSets[i].size);
if (similarity < 0.6) return false;
}
return true;
}
/**
* Aggregate subtask results into a final summary.
* Phase 4: rule-based. Future: LLM-powered synthesis.
*/
private async aggregate(task: SwarmTask): Promise<string> {
const completedSubtasks = task.subtasks.filter(s => s.status === 'done' && s.result);
const failedSubtasks = task.subtasks.filter(s => s.status === 'failed');
if (completedSubtasks.length === 0) {
return failedSubtasks.length > 0
? `所有子任务失败: ${failedSubtasks.map(s => s.error).join('; ')}`
: '无结果';
}
const sections: string[] = [];
sections.push(`## 协作任务: ${task.description}`);
sections.push(`**模式**: ${task.communicationStyle} | **参与者**: ${new Set(completedSubtasks.map(s => s.assignedTo)).size}`);
if (task.communicationStyle === 'debate') {
// Group by round
const maxRound = Math.max(...completedSubtasks.map(s => s.round || 1));
for (let r = 1; r <= maxRound; r++) {
const roundResults = completedSubtasks.filter(s => s.round === r);
if (roundResults.length > 0) {
sections.push(`\n### 第${r}轮讨论`);
for (const s of roundResults) {
const spec = this.config.specialists.find(sp => sp.agentId === s.assignedTo);
sections.push(`**${spec?.role || s.assignedTo}**:\n${s.result}`);
}
}
}
} else {
for (const s of completedSubtasks) {
const spec = this.config.specialists.find(sp => sp.agentId === s.assignedTo);
sections.push(`\n### ${spec?.role || s.assignedTo}\n${s.result}`);
}
}
if (failedSubtasks.length > 0) {
sections.push(`\n### 失败的子任务 (${failedSubtasks.length})`);
for (const s of failedSubtasks) {
sections.push(`- ${s.assignedTo}: ${s.error}`);
}
}
return sections.join('\n');
}
// === Query ===
getHistory(limit: number = 20): SwarmTask[] {
return this.history.slice(-limit);
}
getTask(taskId: string): SwarmTask | undefined {
return this.history.find(t => t.id === taskId);
}
getSpecialists(): SwarmSpecialist[] {
return [...this.config.specialists];
}
addSpecialist(specialist: SwarmSpecialist): void {
const existing = this.config.specialists.findIndex(s => s.agentId === specialist.agentId);
if (existing >= 0) {
this.config.specialists[existing] = specialist;
} else {
this.config.specialists.push(specialist);
}
}
removeSpecialist(agentId: string): void {
this.config.specialists = this.config.specialists.filter(s => s.agentId !== agentId);
}
// === Config ===
getConfig(): SwarmConfig {
return { ...this.config, specialists: [...this.config.specialists] };
}
updateConfig(updates: Partial<SwarmConfig>): void {
this.config = { ...this.config, ...updates };
}
// === Persistence ===
private loadHistory(): void {
try {
const raw = localStorage.getItem(SWARM_HISTORY_KEY);
if (raw) this.history = JSON.parse(raw);
} catch {
this.history = [];
}
}
private saveHistory(): void {
try {
localStorage.setItem(SWARM_HISTORY_KEY, JSON.stringify(this.history.slice(-25)));
} catch { /* silent */ }
}
}
// === Singleton ===
let _instance: AgentSwarm | null = null;
export function getAgentSwarm(config?: Partial<SwarmConfig>): AgentSwarm {
if (!_instance) {
_instance = new AgentSwarm(config);
}
return _instance;
}
export function resetAgentSwarm(): void {
_instance = null;
}

View File

@@ -1,440 +0,0 @@
/**
* 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';
// Re-export types for consumers
export type { CollaborationEvent } 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;

View File

@@ -1,202 +0,0 @@
/**
* 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, CollaborationEvent } from '../lib/team-client';
import { silentErrorHandler } from './error-utils';
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 } = options;
const unsubscribeRef = useRef<(() => void) | null>(null);
const {
addEvent,
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(silentErrorHandler('useTeamEvents'));
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 { getClient } = require('../store/connectionStore');
return getClient();
} catch {
return null;
}
}
export default useTeamEvents;