feat(apiproxy): host session-log download surface
Streams one ZIP of the root session artifact plus each subagent descendant verbatim (the persistence readRaw bytes) from GET /api/session.export as a host-only download — no wire envelope, absent from IApiClient. The downloads domain owns the query schema, the fetch handler answers the GET alongside the SSE routes, and compression runs on the host with fflate's streaming Zip API.
This commit is contained in:
@@ -41,6 +41,12 @@ import type {
|
||||
QueuedInboxItem, SessionSummary, SettingsNamespaceView, SubagentAddress, ToolEventView,
|
||||
WorkspaceId, WorkspaceView,
|
||||
} from './api/index.ts'
|
||||
import {
|
||||
sessionLogExportDeps,
|
||||
sessionLogZipFilename,
|
||||
streamSessionLogZip,
|
||||
} from './session-export.ts'
|
||||
import type { SessionRawArtifact } from '@deepseek-ai/dsh-session-persistence'
|
||||
import {
|
||||
SESSION_SEARCH_RESULT_LIMIT,
|
||||
SESSION_SEARCH_SNIPPET_MAX_CODE_POINTS,
|
||||
@@ -3348,6 +3354,39 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
|
||||
},
|
||||
},
|
||||
|
||||
downloads: {
|
||||
async sessionLog(request, signal) {
|
||||
// Clean error path first: missing services answer 500 and a missing
|
||||
// root artifact 404 before any zip byte is produced. The root content
|
||||
// read here is reused as the first zip entry, so nothing is read twice.
|
||||
const deps = sessionLogExportDeps(ctx)
|
||||
if (deps.sessionQuery === undefined || deps.sessionPersistence === undefined) {
|
||||
return new Response(
|
||||
'session log export is unavailable: missing session-query or session-persistence service',
|
||||
{ status: 500 },
|
||||
)
|
||||
}
|
||||
let root: SessionRawArtifact | undefined
|
||||
try {
|
||||
root = await deps.sessionPersistence.readRaw(request.sessionId)
|
||||
} catch (error: unknown) {
|
||||
return new Response(String(error), { status: 500 })
|
||||
}
|
||||
if (root === undefined) {
|
||||
return new Response('session not found', { status: 404 })
|
||||
}
|
||||
return new Response(
|
||||
streamSessionLogZip(deps, root, request.sessionId, request.includeDescendants === true, signal),
|
||||
{
|
||||
headers: {
|
||||
'content-type': 'application/zip',
|
||||
'content-disposition': `attachment; filename="${sessionLogZipFilename(request.sessionId)}"`,
|
||||
},
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
|
||||
respond(message: ClientResponse): Promise<RpcReceipt> {
|
||||
// Route by the echoed rpcId (the wire correlation): approvals first,
|
||||
// then questions — the two registries share one id space of UUIDs.
|
||||
|
||||
Reference in New Issue
Block a user