1- import { OpenAPIHono } from "@hono/zod-openapi" ;
2- import { jsonError } from "@/lib/http-error" ;
1+ import { createRoute , OpenAPIHono } from "@hono/zod-openapi" ;
2+ import { ErrorResponseSchema , errorResponses } from "@/schemas/common" ;
3+ import {
4+ FileDownloadResponseSchema ,
5+ FileParamsSchema ,
6+ FileStatusesBodySchema ,
7+ FileStatusesResponseSchema ,
8+ FilesResponseSchema ,
9+ ProviderFileInfoSchema ,
10+ UploadFileFormSchema ,
11+ } from "@/schemas/files" ;
312import {
413 deleteUserFile ,
514 getUserFileDownloadUrl ,
@@ -13,109 +22,135 @@ export const filesRoute = new OpenAPIHono();
1322// Max upload size enforced at the edge (the provider also caps via nacos cmaMaxFileSize).
1423const MAX_UPLOAD_BYTES = 100 * 1024 * 1024 ;
1524
16- /**
17- * POST /api/files — multipart/form-data with field `file`. Streams the bytes to the provider's
18- * Files API via the SDK and returns the file metadata ({ id, filename, mime_type, size_bytes, created_at }).
19- * Plain handler (not zod-openapi) so multipart parsing stays under our control.
20- */
21- filesRoute . post ( "/files" , async ( c ) => {
22- let body : Record < string , unknown > ;
23- try {
24- body = await c . req . parseBody ( ) ;
25- } catch ( error ) {
26- return jsonError ( error , 400 ) ;
27- }
28- const file = body . file ;
29- if ( ! ( file instanceof File ) || file . size === 0 ) {
30- return c . json ( { error : { message : "file is required" } } , 400 ) ;
31- }
32- if ( file . size > MAX_UPLOAD_BYTES ) {
33- return c . json ( { error : { message : "file too large" } } , 413 ) ;
34- }
25+ const uploadFileRoute = createRoute ( {
26+ method : "post" ,
27+ path : "/files" ,
28+ operationId : "uploadFile" ,
29+ request : {
30+ body : {
31+ required : true ,
32+ content : { "multipart/form-data" : { schema : UploadFileFormSchema } } ,
33+ } ,
34+ } ,
35+ responses : {
36+ 201 : {
37+ description : "Upload a workspace file" ,
38+ content : { "application/json" : { schema : ProviderFileInfoSchema } } ,
39+ } ,
40+ 413 : {
41+ description : "File exceeds the upload size limit" ,
42+ content : { "application/json" : { schema : ErrorResponseSchema } } ,
43+ } ,
44+ ...errorResponses ,
45+ } ,
46+ } ) ;
47+
48+ filesRoute . openapi (
49+ uploadFileRoute ,
50+ async ( c ) => {
51+ const { file } = c . req . valid ( "form" ) ;
52+ if ( file . size === 0 ) {
53+ return c . json ( { error : { message : "file is required" } } , 400 ) ;
54+ }
55+ if ( file . size > MAX_UPLOAD_BYTES ) {
56+ return c . json ( { error : { message : "file too large" } } , 413 ) ;
57+ }
3558
36- try {
3759 const content = new Uint8Array ( await file . arrayBuffer ( ) ) ;
3860 const info = await uploadUserFile ( {
3961 content,
4062 filename : file . name || "upload" ,
4163 mimeType : file . type || undefined ,
4264 } ) ;
4365 return c . json ( info , 201 ) ;
44- } catch ( error ) {
45- return jsonError ( error ) ;
46- }
66+ } ,
67+ ( result , c ) => {
68+ if ( ! result . success ) return c . json ( { error : { message : "file is required" } } , 400 ) ;
69+ } ,
70+ ) ;
71+
72+ const listFilesRoute = createRoute ( {
73+ method : "get" ,
74+ path : "/files" ,
75+ operationId : "listFiles" ,
76+ responses : {
77+ 200 : {
78+ description : "List workspace files" ,
79+ content : { "application/json" : { schema : FilesResponseSchema } } ,
80+ } ,
81+ ...errorResponses ,
82+ } ,
4783} ) ;
4884
49- /**
50- * GET /api/files — list workspace user-uploaded files (newest first), as
51- * `{ files: [{ id, filename, mime_type, size_bytes, created_at, downloadable, status?, available }] }`.
52- * The OpenAPI list omits `status` (@JsonIgnore) but carries `downloadable`; the server enriches
53- * `status` + `available` via @openagentpack/sdk/file-lifecycle before responding.
54- */
55- filesRoute . get ( "/files" , async ( c ) => {
56- try {
57- const files = await listUserFiles ( ) ;
58- return c . json ( { files } ) ;
59- } catch ( error ) {
60- return jsonError ( error ) ;
61- }
85+ filesRoute . openapi ( listFilesRoute , async ( c ) => {
86+ const files = await listUserFiles ( ) ;
87+ return c . json ( { files } , 200 ) ;
88+ } ) ;
89+
90+ const getFileStatusesRoute = createRoute ( {
91+ method : "post" ,
92+ path : "/files/status" ,
93+ operationId : "getFileStatuses" ,
94+ request : {
95+ body : {
96+ required : true ,
97+ content : { "application/json" : { schema : FileStatusesBodySchema } } ,
98+ } ,
99+ } ,
100+ responses : {
101+ 200 : {
102+ description : "Get file scan statuses" ,
103+ content : { "application/json" : { schema : FileStatusesResponseSchema } } ,
104+ } ,
105+ ...errorResponses ,
106+ } ,
107+ } ) ;
108+
109+ filesRoute . openapi (
110+ getFileStatusesRoute ,
111+ async ( c ) => {
112+ const { fileIds } = c . req . valid ( "json" ) ;
113+ const files = await getUserFileStatuses ( fileIds ) ;
114+ return c . json ( { files } , 200 ) ;
115+ } ,
116+ ( result , c ) => {
117+ if ( ! result . success ) return c . json ( { error : { message : "fileIds (string[]) is required" } } , 400 ) ;
118+ } ,
119+ ) ;
120+
121+ const downloadFileRoute = createRoute ( {
122+ method : "get" ,
123+ path : "/files/{id}/download" ,
124+ operationId : "downloadFile" ,
125+ request : { params : FileParamsSchema } ,
126+ responses : {
127+ 200 : {
128+ description : "Resolve a short-lived file download URL" ,
129+ content : { "application/json" : { schema : FileDownloadResponseSchema } } ,
130+ } ,
131+ ...errorResponses ,
132+ } ,
62133} ) ;
63134
64- /**
65- * POST /api/files/status — body `{ fileIds: string[] }`. Returns each file's normalized scan
66- * `status` and bindability (`{ files: [{ id, status, available }] }`).
67- */
68- filesRoute . post ( "/files/status" , async ( c ) => {
69- let body : { fileIds ?: unknown } ;
70- try {
71- body = await c . req . json ( ) ;
72- } catch ( error ) {
73- return jsonError ( error , 400 ) ;
74- }
75- const fileIds = body . fileIds ;
76- if ( ! Array . isArray ( fileIds ) || fileIds . some ( ( id ) => typeof id !== "string" ) ) {
77- return c . json ( { error : { message : "fileIds (string[]) is required" } } , 400 ) ;
78- }
79- try {
80- const files = await getUserFileStatuses ( fileIds as string [ ] ) ;
81- return c . json ( { files } ) ;
82- } catch ( error ) {
83- return jsonError ( error ) ;
84- }
135+ filesRoute . openapi ( downloadFileRoute , async ( c ) => {
136+ const { id } = c . req . valid ( "param" ) ;
137+ const result = await getUserFileDownloadUrl ( id ) ;
138+ return c . json ( result , 200 ) ;
85139} ) ;
86140
87- /**
88- * GET /api/files/:id/download — resolves a short-lived presigned download URL for a file
89- * (typically an agent-delivered artifact), as `{ url, expires_at }`. The webui fetches this on
90- * demand (each click), so the URL never goes stale in the UI. Errors if the provider has no
91- * download endpoint (bailian/claude).
92- */
93- filesRoute . get ( "/files/:id/download" , async ( c ) => {
94- const id = c . req . param ( "id" ) ;
95- if ( ! id ) {
96- return c . json ( { error : { message : "file id is required" } } , 400 ) ;
97- }
98- try {
99- const result = await getUserFileDownloadUrl ( id ) ;
100- return c . json ( result ) ;
101- } catch ( error ) {
102- return jsonError ( error ) ;
103- }
141+ const deleteFileRoute = createRoute ( {
142+ method : "delete" ,
143+ path : "/files/{id}" ,
144+ operationId : "deleteFile" ,
145+ request : { params : FileParamsSchema } ,
146+ responses : {
147+ 204 : { description : "File deleted" } ,
148+ ...errorResponses ,
149+ } ,
104150} ) ;
105151
106- /**
107- * DELETE /api/files/:id — removes a previously uploaded file from the provider's Files API.
108- * Used when the composer's upload chip is dismissed so the backend doesn't accumulate orphans.
109- */
110- filesRoute . delete ( "/files/:id" , async ( c ) => {
111- const id = c . req . param ( "id" ) ;
112- if ( ! id ) {
113- return c . json ( { error : { message : "file id is required" } } , 400 ) ;
114- }
115- try {
116- await deleteUserFile ( id ) ;
117- return c . body ( null , 204 ) ;
118- } catch ( error ) {
119- return jsonError ( error ) ;
120- }
152+ filesRoute . openapi ( deleteFileRoute , async ( c ) => {
153+ const { id } = c . req . valid ( "param" ) ;
154+ await deleteUserFile ( id ) ;
155+ return c . body ( null , 204 ) ;
121156} ) ;
0 commit comments