repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
The Living OS cockpit
stars
latest
clone command
git clone gitlawb://did:key:z6Mku78K...XywC/living-os-cockp...git clone gitlawb://did:key:z6Mku78K.../living-os-cockp...59751530feat: surface worker supervisor health in live work8h ago| #1 | import { NextRequest, NextResponse } from 'next/server'; |
| #2 | import { authErrorResponse, getUserContext } from '@/lib/user-context'; |
| #3 | import { writeFile, mkdir } from 'fs/promises'; |
| #4 | import { join } from 'path'; |
| #5 | import { request as httpRequest } from 'http'; |
| #6 | import { request as httpsRequest } from 'https'; |
| #7 | |
| #8 | export const runtime = 'nodejs'; |
| #9 | export const maxDuration = 600; |
| #10 | |
| #11 | const MAX_PREVIEW_CHARS = 60000; |
| #12 | const PDF_TEXT_LAYER_MIN_CHARS = 20; |
| #13 | |
| #14 | function meaningfulChars(text: string) { |
| #15 | return (text || '').replace(/\s+/g, '').length; |
| #16 | } |
| #17 | |
| #18 | function postGatewayTool(token: string, payload: any, timeoutMs = 580000): Promise<{ status: number; data: any; text: string }> { |
| #19 | return new Promise((resolve, reject) => { |
| #20 | const target = new URL(`${process.env.GATEWAY_URL}/api/v1/tools/execute`); |
| #21 | const body = JSON.stringify(payload); |
| #22 | const requestImpl = target.protocol === 'https:' ? httpsRequest : httpRequest; |
| #23 | |
| #24 | const req = requestImpl({ |
| #25 | hostname: target.hostname, |
| #26 | port: target.port, |
| #27 | path: `${target.pathname}${target.search}`, |
| #28 | method: 'POST', |
| #29 | headers: { |
| #30 | 'Authorization': `Bearer ${token}`, |
| #31 | 'Content-Type': 'application/json', |
| #32 | 'Content-Length': Buffer.byteLength(body), |
| #33 | }, |
| #34 | }, (res) => { |
| #35 | let text = ''; |
| #36 | res.setEncoding('utf8'); |
| #37 | res.on('data', (chunk) => { text += chunk; }); |
| #38 | res.on('end', () => { |
| #39 | let data: any = null; |
| #40 | try { data = JSON.parse(text); } catch {} |
| #41 | resolve({ status: res.statusCode ?? 0, data, text }); |
| #42 | }); |
| #43 | }); |
| #44 | |
| #45 | req.setTimeout(timeoutMs, () => { |
| #46 | req.destroy(new Error(`Gateway tool call timed out after ${timeoutMs}ms`)); |
| #47 | }); |
| #48 | req.on('error', reject); |
| #49 | req.write(body); |
| #50 | req.end(); |
| #51 | }); |
| #52 | } |
| #53 | |
| #54 | export async function POST(req: NextRequest) { |
| #55 | try { |
| #56 | const ctx = await getUserContext(); |
| #57 | const formData = await req.formData(); |
| #58 | const file = formData.get('file') as File | null; |
| #59 | |
| #60 | if (!file) { |
| #61 | return NextResponse.json({ error: 'No file provided' }, { status: 400 }); |
| #62 | } |
| #63 | |
| #64 | const uploadDir = join(ctx.vaultPath, 'pending_uploads'); |
| #65 | await mkdir(uploadDir, { recursive: true }); |
| #66 | |
| #67 | const safeName = `${Date.now()}_${file.name.replace(/[^a-zA-Z0-9._-]/g, '_')}`; |
| #68 | const filePath = join(uploadDir, safeName); |
| #69 | |
| #70 | const buffer = Buffer.from(await file.arrayBuffer()); |
| #71 | await writeFile(filePath, buffer); |
| #72 | |
| #73 | let preview = ''; |
| #74 | let extractionMethod: string | undefined; |
| #75 | let extractionPages: number | undefined; |
| #76 | let extractionWarning: string | undefined; |
| #77 | let truncated = false; |
| #78 | let fullLength = 0; |
| #79 | |
| #80 | const lower = safeName.toLowerCase(); |
| #81 | const isPdf = file.type === 'application/pdf' || lower.endsWith('.pdf'); |
| #82 | const isText = file.type.startsWith('text/') || /\.(txt|md|json|csv|log|jsonl)$/i.test(safeName); |
| #83 | |
| #84 | if (isPdf) { |
| #85 | try { |
| #86 | const textLayerCheck = await postGatewayTool(ctx.token, { |
| #87 | tool: 'ocr_parse', |
| #88 | args: { file_path: filePath, engine: 'pdfplumber' }, |
| #89 | }, |
| #90 | 120000, |
| #91 | ); |
| #92 | |
| #93 | if (textLayerCheck.status < 200 || textLayerCheck.status >= 300) { |
| #94 | extractionMethod = 'failed'; |
| #95 | const detail = typeof textLayerCheck.data?.detail === 'string' |
| #96 | ? textLayerCheck.data.detail |
| #97 | : (textLayerCheck.text || JSON.stringify(textLayerCheck.data)).slice(0, 200); |
| #98 | extractionWarning = `Gateway OCR HTTP ${textLayerCheck.status}: ${detail}`; |
| #99 | return NextResponse.json({ |
| #100 | filename: safeName, |
| #101 | path: filePath, |
| #102 | size: buffer.length, |
| #103 | type: file.type, |
| #104 | preview, |
| #105 | extractionMethod, |
| #106 | extractionPages, |
| #107 | extractionWarning, |
| #108 | truncated, |
| #109 | fullLength, |
| #110 | }); |
| #111 | } |
| #112 | |
| #113 | const textResult = textLayerCheck.data?.result ?? textLayerCheck.data; |
| #114 | const textLayerContent = textResult?.content ?? ''; |
| #115 | let result = textResult; |
| #116 | |
| #117 | if (!textResult?.success || meaningfulChars(textLayerContent) <= PDF_TEXT_LAYER_MIN_CHARS) { |
| #118 | const chandraResult = await postGatewayTool(ctx.token, { |
| #119 | tool: 'ocr_parse', |
| #120 | args: { file_path: filePath, engine: 'chandra' }, |
| #121 | }, |
| #122 | 580000, |
| #123 | ); |
| #124 | |
| #125 | if (chandraResult.status < 200 || chandraResult.status >= 300) { |
| #126 | extractionMethod = 'failed'; |
| #127 | const detail = typeof chandraResult.data?.detail === 'string' |
| #128 | ? chandraResult.data.detail |
| #129 | : (chandraResult.text || JSON.stringify(chandraResult.data)).slice(0, 200); |
| #130 | extractionWarning = `Gateway Chandra OCR HTTP ${chandraResult.status}: ${detail}`; |
| #131 | return NextResponse.json({ |
| #132 | filename: safeName, |
| #133 | path: filePath, |
| #134 | size: buffer.length, |
| #135 | type: file.type, |
| #136 | preview, |
| #137 | extractionMethod, |
| #138 | extractionPages, |
| #139 | extractionWarning, |
| #140 | truncated, |
| #141 | fullLength, |
| #142 | }); |
| #143 | } |
| #144 | |
| #145 | result = chandraResult.data?.result ?? chandraResult.data; |
| #146 | if (textResult?.error && result && typeof result === 'object') { |
| #147 | result.pdfplumber_error = textResult.error; |
| #148 | } |
| #149 | } |
| #150 | |
| #151 | if (result?.success) { |
| #152 | preview = result.content ?? ''; |
| #153 | fullLength = preview.length; |
| #154 | extractionMethod = result.engine_used ?? 'chandra'; |
| #155 | extractionPages = result.page_count; |
| #156 | if (preview.length > MAX_PREVIEW_CHARS) { |
| #157 | preview = preview.slice(0, MAX_PREVIEW_CHARS) + |
| #158 | `\n\n[... truncated. Full length: ${fullLength} chars. File at ${filePath}]`; |
| #159 | truncated = true; |
| #160 | } |
| #161 | |
| #162 | // Fire-and-forget vault ingestion (non-blocking) |
| #163 | if (result.content && fullLength > 100) { |
| #164 | postGatewayTool(ctx.token, { |
| #165 | tool: 'vault_ingest', |
| #166 | args: { |
| #167 | file_path: filePath, |
| #168 | extracted_text: result.content, |
| #169 | user_id: ctx.vaultUserKey, |
| #170 | canonical_member_id: ctx.canonicalMemberId, |
| #171 | }, |
| #172 | }, |
| #173 | 600000, |
| #174 | ).catch((e) => console.error('vault_ingest fire-and-forget failed:', e?.message)); |
| #175 | } |
| #176 | } else { |
| #177 | extractionMethod = 'failed'; |
| #178 | extractionWarning = result?.error?.slice(0, 200) ?? 'OCR parse returned no content'; |
| #179 | } |
| #180 | } catch (e: any) { |
| #181 | extractionMethod = 'failed'; |
| #182 | extractionWarning = `Gateway tool call failed: ${e?.message?.slice(0, 200) ?? 'unknown'}`; |
| #183 | } |
| #184 | } else if (isText) { |
| #185 | preview = buffer.toString('utf-8'); |
| #186 | fullLength = preview.length; |
| #187 | if (preview.length > MAX_PREVIEW_CHARS) { |
| #188 | preview = preview.slice(0, MAX_PREVIEW_CHARS) + `\n\n[... truncated]`; |
| #189 | truncated = true; |
| #190 | } |
| #191 | extractionMethod = 'native'; |
| #192 | } |
| #193 | |
| #194 | return NextResponse.json({ |
| #195 | filename: safeName, |
| #196 | path: filePath, |
| #197 | size: buffer.length, |
| #198 | type: file.type, |
| #199 | preview, |
| #200 | extractionMethod, |
| #201 | extractionPages, |
| #202 | extractionWarning, |
| #203 | truncated, |
| #204 | fullLength, |
| #205 | }); |
| #206 | } catch (error) { |
| #207 | return authErrorResponse(error); |
| #208 | } |
| #209 | } |
| #210 |