File size: 4,833 Bytes
1f21206 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | /**
* Conversation API Routes
*
* 提供对话交互的 REST 端点。实际的流式对话通过 WebSocket 处理,
* 此处的 REST API 用于非流式操作与状态查询。
*
* Routes:
* POST /api/sessions/:id/chat — 发送消息(入队)
* GET /api/sessions/:id/chat/status — 查询对话状态
* POST /api/sessions/:id/chat/stop — 停止生成
*/
import { ApiError, errorResponse } from '../middleware/errorHandler.js'
import { sessionService } from '../services/sessionService.js'
// In-memory conversation state per session
const sessionStates = new Map<string, 'idle' | 'thinking' | 'compacting' | 'tool_executing'>()
export async function handleConversationsApi(
req: Request,
url: URL,
segments: string[]
): Promise<Response> {
try {
// segments: ['api', 'sessions', ':id', 'chat', ...rest]
// or: ['api', 'conversations', ...]
//
// When routed through the sessions handler:
// segments = ['api', 'sessions', sessionId, 'chat', subAction?]
// When routed directly via /api/conversations:
// segments = ['api', 'conversations', sessionId, subAction?]
let sessionId: string | undefined
let subAction: string | undefined
if (segments[1] === 'sessions') {
// /api/sessions/:id/chat[/status|/stop]
sessionId = segments[2]
// segments[3] === 'chat'
subAction = segments[4]
} else {
// /api/conversations/:id[/status|/stop]
sessionId = segments[2]
subAction = segments[3]
}
if (!sessionId) {
throw ApiError.badRequest('Session ID is required')
}
// -----------------------------------------------------------------------
// GET /chat/status
// -----------------------------------------------------------------------
if (subAction === 'status' && req.method === 'GET') {
return getChatStatus(sessionId)
}
// -----------------------------------------------------------------------
// POST /chat/stop
// -----------------------------------------------------------------------
if (subAction === 'stop' && req.method === 'POST') {
return stopChat(sessionId)
}
// -----------------------------------------------------------------------
// POST /chat (send message)
// -----------------------------------------------------------------------
if (!subAction) {
if (req.method !== 'POST') {
return Response.json(
{ error: 'METHOD_NOT_ALLOWED', message: `Method ${req.method} not allowed` },
{ status: 405 }
)
}
return await sendMessage(req, sessionId)
}
return Response.json(
{ error: 'NOT_FOUND', message: `Unknown chat sub-resource: ${subAction}` },
{ status: 404 }
)
} catch (error) {
return errorResponse(error)
}
}
// ============================================================================
// Handler implementations
// ============================================================================
async function sendMessage(req: Request, sessionId: string): Promise<Response> {
// Validate session exists
const session = await sessionService.getSession(sessionId)
if (!session) {
throw ApiError.notFound(`Session not found: ${sessionId}`)
}
let body: { content?: string }
try {
body = (await req.json()) as { content?: string }
} catch {
throw ApiError.badRequest('Invalid JSON body')
}
if (!body.content || typeof body.content !== 'string') {
throw ApiError.badRequest('content (string) is required in request body')
}
const messageId = crypto.randomUUID()
// Mark session as thinking — actual processing happens through WebSocket
sessionStates.set(sessionId, 'thinking')
return Response.json(
{ messageId, status: 'queued' as const },
{ status: 202 }
)
}
function getChatStatus(sessionId: string): Response {
const state = sessionStates.get(sessionId) || 'idle'
return Response.json({ state })
}
function stopChat(sessionId: string): Response {
// Reset to idle — in a full implementation this would signal the
// WebSocket handler / subprocess to abort the current generation.
sessionStates.set(sessionId, 'idle')
return Response.json({ ok: true })
}
// ============================================================================
// Helpers for WebSocket integration (exported for use by ws/handler)
// ============================================================================
export function setSessionChatState(
sessionId: string,
state: 'idle' | 'thinking' | 'compacting' | 'tool_executing'
): void {
sessionStates.set(sessionId, state)
}
export function getSessionChatState(
sessionId: string
): 'idle' | 'thinking' | 'compacting' | 'tool_executing' {
return sessionStates.get(sessionId) || 'idle'
}
|