Add orchestrator types and error handling configurations

- Introduced new TypeScript types for orchestrator functionality, including `SessionStrategy`, `ErrorHandlingStrategy`, and `OrchestrationStep`.
- Defined interfaces for `OrchestrationPlan` and `ManualOrchestrationParams` to facilitate orchestration management.
- Added a new PNG image file for visual representation.
- Created a placeholder file named 'nul' for future use.
This commit is contained in:
catlog22
2026-02-14 12:54:08 +08:00
parent cdb240d2c2
commit 4d22ae4b2f
56 changed files with 4767 additions and 425 deletions

View File

@@ -1,61 +1,149 @@
/**
* Team Routes - REST API for team message visualization
* Team Routes - REST API for team message visualization & management
*
* Endpoints:
* - GET /api/teams - List all teams
* - GET /api/teams/:name/messages - Get messages (with filters)
* - GET /api/teams/:name/status - Get member status summary
* - GET /api/teams - List all teams (with ?location filter)
* - GET /api/teams/:name/messages - Get messages (with filters)
* - GET /api/teams/:name/status - Get member status summary
* - POST /api/teams/:name/archive - Archive a team
* - POST /api/teams/:name/unarchive - Unarchive a team
* - DELETE /api/teams/:name - Delete a team
*/
import { existsSync, readdirSync } from 'fs';
import { existsSync, readdirSync, rmSync } from 'fs';
import { join } from 'path';
import type { RouteContext } from './types.js';
import { readAllMessages, getLogDir } from '../../tools/team-msg.js';
import { readAllMessages, getLogDir, getEffectiveTeamMeta, readTeamMeta, writeTeamMeta } from '../../tools/team-msg.js';
import type { TeamMeta } from '../../tools/team-msg.js';
import { getProjectRoot } from '../../utils/path-validator.js';
function jsonResponse(res: import('http').ServerResponse, status: number, data: unknown): void {
res.writeHead(status, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(data));
}
export async function handleTeamRoutes(ctx: RouteContext): Promise<boolean> {
const { pathname, req, res, url } = ctx;
const { pathname, req, res, url, handlePostRequest } = ctx;
if (!pathname.startsWith('/api/teams')) return false;
if (req.method !== 'GET') return false;
// GET /api/teams - List all teams
if (pathname === '/api/teams') {
// ====== GET /api/teams - List all teams ======
if (pathname === '/api/teams' && req.method === 'GET') {
try {
const root = getProjectRoot();
const teamMsgDir = join(root, '.workflow', '.team-msg');
if (!existsSync(teamMsgDir)) {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ teams: [] }));
jsonResponse(res, 200, { teams: [] });
return true;
}
const locationFilter = url.searchParams.get('location') || 'active';
const entries = readdirSync(teamMsgDir, { withFileTypes: true });
const teams = entries
.filter(e => e.isDirectory())
.map(e => {
const messages = readAllMessages(e.name);
const lastMsg = messages[messages.length - 1];
const meta = getEffectiveTeamMeta(e.name);
// Count unique members from messages
const memberSet = new Set<string>();
for (const msg of messages) {
memberSet.add(msg.from);
memberSet.add(msg.to);
}
return {
name: e.name,
messageCount: messages.length,
lastActivity: lastMsg?.ts || '',
status: meta.status,
created_at: meta.created_at,
updated_at: meta.updated_at,
archived_at: meta.archived_at,
pipeline_mode: meta.pipeline_mode,
memberCount: memberSet.size,
members: Array.from(memberSet),
};
})
.filter(t => {
if (locationFilter === 'all') return true;
if (locationFilter === 'archived') return t.status === 'archived';
// 'active' = everything that's not archived
return t.status !== 'archived';
})
.sort((a, b) => b.lastActivity.localeCompare(a.lastActivity));
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ teams }));
jsonResponse(res, 200, { teams });
return true;
} catch (error) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: (error as Error).message }));
jsonResponse(res, 500, { error: (error as Error).message });
return true;
}
}
// Match /api/teams/:name/messages or /api/teams/:name/status
// ====== POST /api/teams/:name/archive ======
const archiveMatch = pathname.match(/^\/api\/teams\/([^/]+)\/archive$/);
if (archiveMatch && req.method === 'POST') {
const teamName = decodeURIComponent(archiveMatch[1]);
handlePostRequest(req, res, async () => {
const dir = getLogDir(teamName);
if (!existsSync(dir)) {
throw new Error(`Team "${teamName}" not found`);
}
const meta = getEffectiveTeamMeta(teamName);
meta.status = 'archived';
meta.archived_at = new Date().toISOString();
meta.updated_at = new Date().toISOString();
writeTeamMeta(teamName, meta);
return { success: true, team: teamName, status: 'archived' };
});
return true;
}
// ====== POST /api/teams/:name/unarchive ======
const unarchiveMatch = pathname.match(/^\/api\/teams\/([^/]+)\/unarchive$/);
if (unarchiveMatch && req.method === 'POST') {
const teamName = decodeURIComponent(unarchiveMatch[1]);
handlePostRequest(req, res, async () => {
const dir = getLogDir(teamName);
if (!existsSync(dir)) {
throw new Error(`Team "${teamName}" not found`);
}
const meta = getEffectiveTeamMeta(teamName);
meta.status = 'active';
delete meta.archived_at;
meta.updated_at = new Date().toISOString();
writeTeamMeta(teamName, meta);
return { success: true, team: teamName, status: 'active' };
});
return true;
}
// ====== DELETE /api/teams/:name ======
const deleteMatch = pathname.match(/^\/api\/teams\/([^/]+)$/);
if (deleteMatch && req.method === 'DELETE') {
const teamName = decodeURIComponent(deleteMatch[1]);
try {
const dir = getLogDir(teamName);
if (!existsSync(dir)) {
jsonResponse(res, 404, { error: `Team "${teamName}" not found` });
return true;
}
rmSync(dir, { recursive: true, force: true });
jsonResponse(res, 200, { success: true, team: teamName, deleted: true });
return true;
} catch (error) {
jsonResponse(res, 500, { error: (error as Error).message });
return true;
}
}
// ====== GET /api/teams/:name/messages or /api/teams/:name/status ======
if (req.method !== 'GET') return false;
const match = pathname.match(/^\/api\/teams\/([^/]+)\/(messages|status)$/);
if (!match) return false;
@@ -81,12 +169,10 @@ export async function handleTeamRoutes(ctx: RouteContext): Promise<boolean> {
const total = messages.length;
const sliced = messages.slice(Math.max(0, total - last - offset), total - offset);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ total, showing: sliced.length, messages: sliced }));
jsonResponse(res, 200, { total, showing: sliced.length, messages: sliced });
return true;
} catch (error) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: (error as Error).message }));
jsonResponse(res, 500, { error: (error as Error).message });
return true;
}
}
@@ -112,12 +198,10 @@ export async function handleTeamRoutes(ctx: RouteContext): Promise<boolean> {
const members = Array.from(memberMap.values()).sort((a, b) => b.lastSeen.localeCompare(a.lastSeen));
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ members, total_messages: messages.length }));
jsonResponse(res, 200, { members, total_messages: messages.length });
return true;
} catch (error) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: (error as Error).message }));
jsonResponse(res, 500, { error: (error as Error).message });
return true;
}
}