import { parseSSEChunk, type StreamEvent } from './chatStream'; import { canonicalDocIdForTask } from '../lib/controlledDocHydrate'; import type { TaskRunContext } from '../types/workbench'; export type { TaskRunContext }; /** * streamTask 消费 POST /api/v1/tasks/run/stream 的 SSE 事件流。 * 复用 chatStream 的 parseSSEChunk 解析与中止语义。 */ export async function streamTask( body: Record, handlers: { onEvent: (ev: StreamEvent) => void; onRequestId?: (id: string) => void; }, signal?: AbortSignal, ): Promise { const resp = await fetch('/api/v1/tasks/run/stream', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream' }, body: JSON.stringify(body), signal, }); const rid = resp.headers.get('X-Request-Id') || resp.headers.get('x-request-id'); if (rid) handlers.onRequestId?.(rid); if (!resp.ok || !resp.body) { throw Object.assign(new Error(`HTTP ${resp.status}`), { httpStatus: resp.status, requestId: rid, }); } const reader = resp.body.getReader(); const decoder = new TextDecoder(); let buf = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buf += decoder.decode(value, { stream: true }); const parsed = parseSSEChunk(buf); buf = parsed.rest; for (const ev of parsed.events) handlers.onEvent(ev); } if (buf) { const flushed = parseSSEChunk(buf, { flush: true }); for (const ev of flushed.events) handlers.onEvent(ev); } } /** 构造新任务运行请求(五要素 + task_key + 用户/项目标识) */ export function buildTaskRequest(task: TaskRunContext): Record { const body: Record = { task_key: task.taskKey, project_name: task.projectName, customer_name: task.customerName, device_type: task.deviceType, }; if (task.intendedUse) body.intended_use = task.intendedUse; if (task.targetMarket) body.target_market = task.targetMarket; if (task.sessionId) body.session_id = task.sessionId; if (task.userId) body.user_id = task.userId; if (task.projectId) body.project_id = task.projectId; if (task.clientTurnId) body.client_turn_id = task.clientTurnId; if (task.materials && task.materials.length > 0) { body.materials = task.materials; } const canonicalDocId = canonicalDocIdForTask( task.projectId, task.taskKey, undefined, task.docId, ); if (canonicalDocId) body.doc_id = canonicalDocId; return body; } /** 构造 resume 请求:在任务请求基础上追加 work_dir(可选动态步骤),交由后端续跑既有工作区 */ export function buildResumeRequest( task: TaskRunContext, workDir: string, addSteps?: Array>, ): Record { const body = buildTaskRequest(task); body.work_dir = workDir; if (addSteps && addSteps.length > 0) { body.add_steps = addSteps; } return body; }