mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
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
235 lines
7.9 KiB
TypeScript
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,
|
|
}
|
|
}
|