cc-haha/src/server/api/agents.ts
程序员阿江(Relakkes) 16d716e773 fix: keep completed task lists from leaking into the next turn
Completed desktop task bars were only being dismissed locally, which let the
persisted task list resurface on refresh or bleed into the next user turn.
This wires the existing server-side reset path into the desktop stores and
session flow so a finished task cycle is summarized once, cleared locally,
and removed remotely before the next round starts.

Constraint: Existing task persistence already lives behind the server task-list API and must stay compatible with persisted JSON task files
Rejected: Only hide the completed task bar in UI state | left stale persisted tasks behind and reintroduced them on reload
Rejected: Clear desktop state without a task summary | dropped useful completion context from the chat transcript
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Keep desktop task dismissal and task-list persistence behavior aligned; do not reintroduce local-only clearing without covering reload and next-turn flows
Tested: `cd desktop && bun run lint`
Tested: `cd desktop && bun run test src/stores/cliTaskStore.test.ts src/stores/chatStore.test.ts src/components/chat/SessionTaskBar.test.tsx src/components/chat/ComputerUsePermissionModal.test.tsx`
Tested: `bun test src/server/__tests__/e2e/business-flow.test.ts --test-name-pattern "Task Lists API"`
Not-tested: Full `bun test src/server/__tests__/e2e/business-flow.test.ts` suite still has unrelated pre-existing failures in Models and Sessions sections
2026-04-21 00:46:29 +08:00

235 lines
7.9 KiB
TypeScript

/**
* Agents REST API
*
* GET /api/agents — 获取 Agent 列表
* GET /api/agents/:name — 获取 Agent 详情
* POST /api/agents — 创建 Agent
* PUT /api/agents/:name — 更新 Agent
* DELETE /api/agents/:name — 删除 Agent
*
* GET /api/tasks — 获取后台任务列表
* GET /api/tasks/:id — 获取任务详情
*/
import { AgentService } from '../services/agentService.js'
import { taskService } from '../services/taskService.js'
import { ApiError, errorResponse } from '../middleware/errorHandler.js'
import { resetTaskList } from '../../utils/tasks.js'
import {
resolveAgentModelDisplay,
resolveAgentOverrides,
type ResolvedAgent,
} from '../../tools/AgentTool/agentDisplay.js'
import {
clearAgentDefinitionsCache,
getAgentDefinitionsWithOverrides,
type AgentDefinition as SharedAgentDefinition,
} from '../../tools/AgentTool/loadAgentsDir.js'
import { getCwd } from '../../utils/cwd.js'
const agentService = new AgentService()
export async function handleAgentsApi(
req: Request,
url: URL,
segments: string[],
): Promise<Response> {
try {
const resource = segments[1] // 'agents' | 'tasks'
if (resource === 'tasks') {
return await handleTasksApi(req, segments)
}
return await handleAgents(req, url, segments)
} catch (error) {
return errorResponse(error)
}
}
// ─── Agent CRUD ─────────────────────────────────────────────────────────────
async function handleAgents(
req: Request,
url: URL,
segments: string[],
): Promise<Response> {
const method = req.method
const agentName = segments[2] ? decodeURIComponent(segments[2]) : undefined
// ── GET /api/agents ──────────────────────────────────────────────────
if (method === 'GET' && !agentName) {
const cwd = url.searchParams.get('cwd') || getCwd()
const { activeAgents, allAgents } = await getAgentDefinitionsWithOverrides(cwd)
const resolvedAgents = resolveAgentOverrides(allAgents, activeAgents)
return Response.json({
activeAgents: activeAgents.map(agent => serializeActiveAgent(agent, true)),
allAgents: resolvedAgents.map(serializeResolvedAgent),
})
}
// ── GET /api/agents/:name ────────────────────────────────────────────
if (method === 'GET' && agentName) {
const agent = await agentService.getAgent(agentName)
if (!agent) {
throw ApiError.notFound(`Agent not found: ${agentName}`)
}
return Response.json({ agent })
}
// ── POST /api/agents ─────────────────────────────────────────────────
if (method === 'POST' && !agentName) {
const body = await parseJsonBody(req)
if (!body.name || typeof body.name !== 'string') {
throw ApiError.badRequest('Missing or invalid "name" in request body')
}
await agentService.createAgent({
name: body.name as string,
description: body.description as string | undefined,
model: body.model as string | undefined,
tools: body.tools as string[] | undefined,
systemPrompt: body.systemPrompt as string | undefined,
color: body.color as string | undefined,
})
clearAgentDefinitionsCache()
return Response.json({ ok: true }, { status: 201 })
}
// ── PUT /api/agents/:name ────────────────────────────────────────────
if (method === 'PUT' && agentName) {
const body = await parseJsonBody(req)
await agentService.updateAgent(agentName, body as Record<string, unknown>)
clearAgentDefinitionsCache()
const updated = await agentService.getAgent(agentName)
return Response.json({ agent: updated })
}
// ── DELETE /api/agents/:name ─────────────────────────────────────────
if (method === 'DELETE' && agentName) {
await agentService.deleteAgent(agentName)
clearAgentDefinitionsCache()
return Response.json({ ok: true })
}
throw new ApiError(
405,
`Method ${method} not allowed on /api/agents${agentName ? `/${agentName}` : ''}`,
'METHOD_NOT_ALLOWED',
)
}
// ─── Tasks API ─────────────────────────────────────────────────────────────
//
// GET /api/tasks → list all tasks (across all task lists)
// GET /api/tasks/lists → list all task lists with summaries
// GET /api/tasks/lists/:taskListId → get all tasks for a specific task list
// GET /api/tasks/lists/:taskListId/:id → get a single task
// POST /api/tasks/lists/:taskListId/reset → clear a completed task list
async function handleTasksApi(
req: Request,
segments: string[],
): Promise<Response> {
const sub = segments[2] // 'lists' or undefined
if (sub === 'lists') {
const taskListId = segments[3]
const taskId = segments[4]
if (req.method === 'POST' && taskListId && taskId === 'reset') {
await resetTaskList(taskListId)
return Response.json({ ok: true })
}
if (req.method !== 'GET') {
throw new ApiError(
405,
`Method ${req.method} not allowed on /api/tasks/lists`,
'METHOD_NOT_ALLOWED',
)
}
if (taskListId && taskId) {
// GET /api/tasks/lists/:taskListId/:taskId
const task = await taskService.getTask(taskListId, taskId)
if (!task) throw ApiError.notFound(`Task not found: ${taskListId}/${taskId}`)
return Response.json({ task })
}
if (taskListId) {
// GET /api/tasks/lists/:taskListId
const tasks = await taskService.getTasksForList(taskListId)
return Response.json({ tasks })
}
// GET /api/tasks/lists
const lists = await taskService.listTaskLists()
return Response.json({ lists })
}
if (req.method !== 'GET') {
throw new ApiError(
405,
`Method ${req.method} not allowed on /api/tasks`,
'METHOD_NOT_ALLOWED',
)
}
// GET /api/tasks — list all tasks
const tasks = await taskService.listTasks()
return Response.json({ tasks })
}
// ─── Helpers ────────────────────────────────────────────────────────────────
async function parseJsonBody(req: Request): Promise<Record<string, unknown>> {
try {
return (await req.json()) as Record<string, unknown>
} catch {
throw ApiError.badRequest('Invalid JSON body')
}
}
type ApiAgentDefinition = {
agentType: string
description?: string
model?: string
modelDisplay?: string
tools?: string[]
systemPrompt?: string
color?: string
source: SharedAgentDefinition['source']
baseDir?: string
isActive: boolean
}
type ApiResolvedAgentDefinition = ApiAgentDefinition & {
overriddenBy?: SharedAgentDefinition['source']
}
function serializeActiveAgent(
agent: SharedAgentDefinition,
isActive: boolean,
): ApiAgentDefinition {
return {
agentType: agent.agentType,
description: agent.whenToUse,
model: agent.model,
modelDisplay: resolveAgentModelDisplay(agent),
tools: agent.tools,
systemPrompt: agent.getSystemPrompt.length === 0 ? agent.getSystemPrompt() : undefined,
color: agent.color,
source: agent.source,
baseDir: agent.baseDir,
isActive,
}
}
function serializeResolvedAgent(agent: ResolvedAgent): ApiResolvedAgentDefinition {
return {
...serializeActiveAgent(agent, !agent.overriddenBy),
overriddenBy: agent.overriddenBy,
}
}