cc-haha/src/server/api/teams.ts
程序员阿江(Relakkes) 23b31b397a Prevent local team sessions from dropping members and stalling adapters
This bundles the pending desktop/server team-session fixes with the local adapter recovery changes already in the worktree. The team path now keeps teammate membership stable under concurrent spawns, surfaces real teammate identities in the desktop UI, and allows direct interaction with member transcripts. The adapter changes recover automatically when stale thinking signatures invalidate an existing session.

Constraint: Team config writes can happen concurrently while multiple reviewers spawn in parallel

Constraint: Desktop member views must follow mailbox/transcript semantics rather than hijacking teammate runtime sessions

Rejected: Keep relying on config.json alone for member discovery | in-process teammates can be lost after concurrent writes

Rejected: Open teammate sessionIds as normal desktop sessions | would attach a second CLI instead of the running teammate

Confidence: medium

Scope-risk: moderate

Reversibility: clean

Directive: Preserve locked team-file mutation for any future teammate registration path and keep teammate labels sourced from member names before agent types

Tested: bun test src/server/__tests__/teams.test.ts src/server/__tests__/team-watcher.test.ts

Tested: cd desktop && bun run test --run src/stores/chatStore.test.ts src/pages/ActiveSession.test.tsx

Tested: cd desktop && bun run lint

Tested: cd desktop && bun run build

Not-tested: Manual end-to-end validation against a live Agent Teams run in the desktop app
2026-04-14 17:27:07 +08:00

83 lines
3.0 KiB
TypeScript

/**
* Teams REST API
*
* GET /api/teams — 列出所有团队
* GET /api/teams/:name — 获取团队详情
* GET /api/teams/:name/members/:id/transcript — 获取成员 transcript
* POST /api/teams/:name/members/:id/messages — 给成员发送消息
* DELETE /api/teams/:name — 删除团队
*/
import { teamService } from '../services/teamService.js'
import { ApiError, errorResponse } from '../middleware/errorHandler.js'
export async function handleTeamsApi(
req: Request,
_url: URL,
segments: string[],
): Promise<Response> {
try {
const method = req.method
const teamName = segments[2] ? decodeURIComponent(segments[2]) : undefined
// ── GET /api/teams ────────────────────────────────────────────────────
if (method === 'GET' && !teamName) {
const teams = await teamService.listTeams()
return Response.json({ teams })
}
// ── GET /api/teams/:name/members/:id/transcript ───────────────────────
if (
method === 'GET' &&
teamName &&
segments[3] === 'members' &&
segments[4] &&
segments[5] === 'transcript'
) {
const agentId = decodeURIComponent(segments[4])
const messages = await teamService.getMemberTranscript(teamName, agentId)
return Response.json({ messages })
}
// ── POST /api/teams/:name/members/:id/messages ─────────────────────────
if (
method === 'POST' &&
teamName &&
segments[3] === 'members' &&
segments[4] &&
segments[5] === 'messages'
) {
const agentId = decodeURIComponent(segments[4])
let body: { content?: string }
try {
body = (await req.json()) as { content?: string }
} catch {
throw ApiError.badRequest('Invalid JSON body')
}
await teamService.sendMemberMessage(teamName, agentId, body.content ?? '')
return Response.json({ ok: true })
}
// ── GET /api/teams/:name ──────────────────────────────────────────────
if (method === 'GET' && teamName) {
const team = await teamService.getTeam(teamName)
return Response.json(team)
}
// ── DELETE /api/teams/:name ───────────────────────────────────────────
if (method === 'DELETE' && teamName) {
await teamService.deleteTeam(teamName)
return Response.json({ ok: true })
}
throw new ApiError(
405,
`Method ${method} not allowed on /api/teams${teamName ? `/${teamName}` : ''}`,
'METHOD_NOT_ALLOWED',
)
} catch (error) {
return errorResponse(error)
}
}