@larkup/workspace-server 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +176 -0
- package/README.md +2 -2
- package/dist-types/app/api/chat/feedback/route.d.ts +4 -0
- package/dist-types/app/api/chat/feedback/route.d.ts.map +1 -0
- package/dist-types/app/api/chat/route.d.ts +3 -0
- package/dist-types/app/api/chat/route.d.ts.map +1 -0
- package/dist-types/app/api/chat/tools.d.ts +67 -0
- package/dist-types/app/api/chat/tools.d.ts.map +1 -0
- package/dist-types/app/api/config/test-embedding/route.d.ts +3 -0
- package/dist-types/app/api/config/test-embedding/route.d.ts.map +1 -0
- package/dist-types/app/api/config/verify/route.d.ts +3 -0
- package/dist-types/app/api/config/verify/route.d.ts.map +1 -0
- package/dist-types/app/api/describe-image/route.d.ts +4 -0
- package/dist-types/app/api/describe-image/route.d.ts.map +1 -0
- package/dist-types/app/api/doc-editor/edit/route.d.ts +10 -0
- package/dist-types/app/api/doc-editor/edit/route.d.ts.map +1 -0
- package/dist-types/app/api/doc-editor/export/route.d.ts +9 -0
- package/dist-types/app/api/doc-editor/export/route.d.ts.map +1 -0
- package/dist-types/app/api/doc-editor/parse/route.d.ts +10 -0
- package/dist-types/app/api/doc-editor/parse/route.d.ts.map +1 -0
- package/dist-types/app/api/doc-editor/restore/route.d.ts +2 -0
- package/dist-types/app/api/doc-editor/restore/route.d.ts.map +1 -0
- package/dist-types/app/api/document/sign/route.d.ts +2 -0
- package/dist-types/app/api/document/sign/route.d.ts.map +1 -0
- package/dist-types/app/api/files/remote/route.d.ts +5 -0
- package/dist-types/app/api/files/remote/route.d.ts.map +1 -0
- package/dist-types/app/api/mcp/[connectionId]/route.d.ts +9 -0
- package/dist-types/app/api/mcp/[connectionId]/route.d.ts.map +1 -0
- package/dist-types/app/api/mcp/[connectionId]/test/route.d.ts +8 -0
- package/dist-types/app/api/mcp/[connectionId]/test/route.d.ts.map +1 -0
- package/dist-types/app/api/mcp/route.d.ts +4 -0
- package/dist-types/app/api/mcp/route.d.ts.map +1 -0
- package/dist-types/app/api/parse-file/route.d.ts +6 -0
- package/dist-types/app/api/parse-file/route.d.ts.map +1 -0
- package/dist-types/app/api/pdf-page/route.d.ts +4 -0
- package/dist-types/app/api/pdf-page/route.d.ts.map +1 -0
- package/dist-types/app/api/proxy/verify/route.d.ts +4 -0
- package/dist-types/app/api/proxy/verify/route.d.ts.map +1 -0
- package/dist-types/app/api/search/bing/route.d.ts +5 -0
- package/dist-types/app/api/search/bing/route.d.ts.map +1 -0
- package/dist-types/app/api/search/brave/route.d.ts +5 -0
- package/dist-types/app/api/search/brave/route.d.ts.map +1 -0
- package/dist-types/app/api/search/exa/route.d.ts +5 -0
- package/dist-types/app/api/search/exa/route.d.ts.map +1 -0
- package/dist-types/app/api/search/google/route.d.ts +30 -0
- package/dist-types/app/api/search/google/route.d.ts.map +1 -0
- package/dist-types/app/api/search/route.d.ts +7 -0
- package/dist-types/app/api/search/route.d.ts.map +1 -0
- package/dist-types/app/api/search/tavily/route.d.ts +5 -0
- package/dist-types/app/api/search/tavily/route.d.ts.map +1 -0
- package/dist-types/app/api/search/verify/route.d.ts +4 -0
- package/dist-types/app/api/search/verify/route.d.ts.map +1 -0
- package/dist-types/app/api/tabular/route.d.ts +9 -0
- package/dist-types/app/api/tabular/route.d.ts.map +1 -0
- package/dist-types/lib/chat/assistant-source-scope.d.ts +10 -0
- package/dist-types/lib/chat/assistant-source-scope.d.ts.map +1 -0
- package/dist-types/lib/chat/conversation-memory.d.ts +80 -0
- package/dist-types/lib/chat/conversation-memory.d.ts.map +1 -0
- package/dist-types/lib/chat/gateway-fallbacks.d.ts +7 -0
- package/dist-types/lib/chat/gateway-fallbacks.d.ts.map +1 -0
- package/dist-types/lib/chat/image-description.d.ts +10 -0
- package/dist-types/lib/chat/image-description.d.ts.map +1 -0
- package/dist-types/lib/chat/media-retrieval-routing.d.ts +14 -0
- package/dist-types/lib/chat/media-retrieval-routing.d.ts.map +1 -0
- package/dist-types/lib/chat/media-source-routing.d.ts +53 -0
- package/dist-types/lib/chat/media-source-routing.d.ts.map +1 -0
- package/dist-types/lib/chat/message-input.d.ts +8 -0
- package/dist-types/lib/chat/message-input.d.ts.map +1 -0
- package/dist-types/lib/chat/model-provider.d.ts +23 -0
- package/dist-types/lib/chat/model-provider.d.ts.map +1 -0
- package/dist-types/lib/chat/response-style.d.ts +2 -0
- package/dist-types/lib/chat/response-style.d.ts.map +1 -0
- package/dist-types/lib/chat/retrieval-ranking.d.ts +32 -0
- package/dist-types/lib/chat/retrieval-ranking.d.ts.map +1 -0
- package/dist-types/lib/chat/retrieval-routing.d.ts +39 -0
- package/dist-types/lib/chat/retrieval-routing.d.ts.map +1 -0
- package/dist-types/lib/chat/tabular-dataset-routing.d.ts +15 -0
- package/dist-types/lib/chat/tabular-dataset-routing.d.ts.map +1 -0
- package/dist-types/lib/chat/tabular-query-plan.d.ts +12 -0
- package/dist-types/lib/chat/tabular-query-plan.d.ts.map +1 -0
- package/dist-types/lib/chat/tabular-routing.d.ts +41 -0
- package/dist-types/lib/chat/tabular-routing.d.ts.map +1 -0
- package/dist-types/lib/chat/tabular-sandbox-files.d.ts +33 -0
- package/dist-types/lib/chat/tabular-sandbox-files.d.ts.map +1 -0
- package/dist-types/lib/chat/tabular-visualization.d.ts +31 -0
- package/dist-types/lib/chat/tabular-visualization.d.ts.map +1 -0
- package/dist-types/lib/chat/tool-call-repair.d.ts +11 -0
- package/dist-types/lib/chat/tool-call-repair.d.ts.map +1 -0
- package/dist-types/lib/chat/tool-context.d.ts +72 -0
- package/dist-types/lib/chat/tool-context.d.ts.map +1 -0
- package/dist-types/lib/chat/tool-registry.d.ts +3 -0
- package/dist-types/lib/chat/tool-registry.d.ts.map +1 -0
- package/dist-types/lib/chat/video-rag-routing.d.ts +33 -0
- package/dist-types/lib/chat/video-rag-routing.d.ts.map +1 -0
- package/dist-types/lib/chat/visual-routing.d.ts +53 -0
- package/dist-types/lib/chat/visual-routing.d.ts.map +1 -0
- package/dist-types/lib/enterprise-client.d.ts +31 -0
- package/dist-types/lib/enterprise-client.d.ts.map +1 -0
- package/dist-types/lib/marketplace/tool-runtime-config.d.ts +7 -0
- package/dist-types/lib/marketplace/tool-runtime-config.d.ts.map +1 -0
- package/dist-types/lib/media/knowledge.d.ts +47 -0
- package/dist-types/lib/media/knowledge.d.ts.map +1 -0
- package/dist-types/lib/media/pdf-inspection.d.ts +30 -0
- package/dist-types/lib/media/pdf-inspection.d.ts.map +1 -0
- package/dist-types/lib/media/source-identity.d.ts +12 -0
- package/dist-types/lib/media/source-identity.d.ts.map +1 -0
- package/dist-types/lib/media/source-utils.d.ts +157 -0
- package/dist-types/lib/media/source-utils.d.ts.map +1 -0
- package/dist-types/lib/media/video/quick-look.d.ts +60 -0
- package/dist-types/lib/media/video/quick-look.d.ts.map +1 -0
- package/dist-types/lib/media/video-intelligence-adapter.d.ts +521 -0
- package/dist-types/lib/media/video-intelligence-adapter.d.ts.map +1 -0
- package/dist-types/lib/media/video-intelligence-connection.d.ts +32 -0
- package/dist-types/lib/media/video-intelligence-connection.d.ts.map +1 -0
- package/dist-types/lib/remote-files.d.ts +12 -0
- package/dist-types/lib/remote-files.d.ts.map +1 -0
- package/dist-types/lib/search/provider-verification.d.ts +5 -0
- package/dist-types/lib/search/provider-verification.d.ts.map +1 -0
- package/dist-types/runtime-host.d.ts +20 -0
- package/dist-types/runtime-host.d.ts.map +1 -0
- package/package.json +338 -3
- package/src/app/api/chat/feedback/route.ts +77 -0
- package/src/app/api/chat/route.ts +2277 -0
- package/src/app/api/chat/tools.ts +3563 -0
- package/src/app/api/config/test-embedding/route.ts +39 -0
- package/src/app/api/config/verify/route.ts +309 -0
- package/src/app/api/describe-image/route.ts +108 -0
- package/src/app/api/doc-editor/edit/route.ts +87 -0
- package/src/app/api/doc-editor/export/route.ts +32 -0
- package/src/app/api/doc-editor/parse/route.ts +74 -0
- package/src/app/api/doc-editor/restore/route.ts +70 -0
- package/src/app/api/document/sign/route.ts +55 -0
- package/src/app/api/files/remote/route.ts +32 -0
- package/src/app/api/mcp/[connectionId]/route.ts +29 -0
- package/src/app/api/mcp/[connectionId]/test/route.ts +13 -0
- package/src/app/api/mcp/route.ts +22 -0
- package/src/app/api/parse-file/route.ts +86 -0
- package/src/app/api/pdf-page/route.ts +40 -0
- package/src/app/api/proxy/verify/route.ts +78 -0
- package/src/app/api/search/bing/route.ts +86 -0
- package/src/app/api/search/brave/route.ts +88 -0
- package/src/app/api/search/exa/route.ts +100 -0
- package/src/app/api/search/google/route.ts +147 -0
- package/src/app/api/search/route.ts +28 -0
- package/src/app/api/search/tavily/route.ts +93 -0
- package/src/app/api/search/verify/route.ts +27 -0
- package/src/app/api/tabular/route.ts +68 -0
- package/src/lib/chat/assistant-source-scope.ts +32 -0
- package/src/lib/chat/conversation-memory.ts +621 -0
- package/src/lib/chat/gateway-fallbacks.ts +12 -0
- package/src/lib/chat/image-description.ts +21 -0
- package/src/lib/chat/media-retrieval-routing.ts +37 -0
- package/src/lib/chat/media-source-routing.ts +100 -0
- package/src/lib/chat/message-input.ts +48 -0
- package/src/lib/chat/model-provider.ts +157 -0
- package/src/lib/chat/response-style.ts +3 -0
- package/src/lib/chat/retrieval-ranking.ts +126 -0
- package/src/lib/chat/retrieval-routing.ts +221 -0
- package/src/lib/chat/tabular-dataset-routing.ts +93 -0
- package/src/lib/chat/tabular-query-plan.ts +195 -0
- package/src/lib/chat/tabular-routing.ts +122 -0
- package/src/lib/chat/tabular-sandbox-files.ts +78 -0
- package/src/lib/chat/tabular-visualization.ts +165 -0
- package/src/lib/chat/tool-call-repair.ts +84 -0
- package/src/lib/chat/tool-context.ts +1222 -0
- package/src/lib/chat/tool-registry.ts +6 -0
- package/src/lib/chat/video-rag-routing.ts +149 -0
- package/src/lib/chat/visual-routing.ts +225 -0
- package/src/lib/enterprise-client.ts +96 -0
- package/src/lib/marketplace/tool-runtime-config.ts +155 -0
- package/src/lib/media/knowledge.ts +489 -0
- package/src/lib/media/pdf-inspection.ts +280 -0
- package/src/lib/media/source-identity.ts +50 -0
- package/src/lib/media/source-utils.ts +1156 -0
- package/src/lib/media/video/quick-look.ts +368 -0
- package/src/lib/media/video-intelligence-adapter.ts +1946 -0
- package/src/lib/media/video-intelligence-connection.ts +151 -0
- package/src/lib/remote-files.ts +182 -0
- package/src/lib/search/provider-verification.ts +95 -0
- package/src/runtime-host.ts +31 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { restoreSession, parseDocument } from '@larkup/tool-doc-editor';
|
|
2
|
+
import type { EditorSession } from '@larkup/tool-doc-editor';
|
|
3
|
+
|
|
4
|
+
export async function POST(req: Request) {
|
|
5
|
+
try {
|
|
6
|
+
const { sessionId, fileBase64, parsedDocument, fileName, mimeType, activeFileType } =
|
|
7
|
+
await req.json();
|
|
8
|
+
|
|
9
|
+
if (!sessionId || !fileBase64) {
|
|
10
|
+
return Response.json({ error: 'Missing sessionId or fileBase64' }, { status: 400 });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const buffer = Buffer.from(fileBase64, 'base64');
|
|
14
|
+
|
|
15
|
+
// Re-parse the actual file to get proper fields and pages.
|
|
16
|
+
// This is critical: if we don't have a parsedDocument with fields,
|
|
17
|
+
// any subsequent fill attempts will fail because field ID resolution
|
|
18
|
+
// relies on session.parsed.fields being populated.
|
|
19
|
+
let parsed;
|
|
20
|
+
if (parsedDocument && parsedDocument.fields && parsedDocument.fields.length > 0) {
|
|
21
|
+
parsed = parsedDocument;
|
|
22
|
+
} else {
|
|
23
|
+
try {
|
|
24
|
+
const resolvedMime = mimeType || 'application/pdf';
|
|
25
|
+
const resolvedName = fileName || 'Document';
|
|
26
|
+
parsed = await parseDocument(buffer, resolvedName, resolvedMime);
|
|
27
|
+
parsed.sessionId = sessionId;
|
|
28
|
+
} catch (parseErr: any) {
|
|
29
|
+
console.warn('[doc-editor/restore] Re-parse failed, using empty parsed:', parseErr.message);
|
|
30
|
+
parsed = {
|
|
31
|
+
sessionId,
|
|
32
|
+
fileName: fileName || 'Document',
|
|
33
|
+
type: activeFileType || 'pdf',
|
|
34
|
+
mimeType: mimeType || 'application/pdf',
|
|
35
|
+
fileSize: buffer.length,
|
|
36
|
+
pages: [],
|
|
37
|
+
fields: [],
|
|
38
|
+
rawText: '',
|
|
39
|
+
metadata: {},
|
|
40
|
+
totalPages: 1,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const session: EditorSession = {
|
|
46
|
+
id: sessionId,
|
|
47
|
+
fileName: fileName || parsed.fileName || 'Document',
|
|
48
|
+
type: activeFileType || parsed.type || 'pdf',
|
|
49
|
+
mimeType: mimeType || parsed.mimeType || 'application/pdf',
|
|
50
|
+
originalFileBase64: fileBase64,
|
|
51
|
+
currentFileBase64: fileBase64,
|
|
52
|
+
parsed: parsed,
|
|
53
|
+
edits: [],
|
|
54
|
+
createdAt: new Date().toISOString(),
|
|
55
|
+
updatedAt: new Date().toISOString(),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
restoreSession(session);
|
|
59
|
+
console.log(
|
|
60
|
+
`[doc-editor/restore] Session ${sessionId} restored with ${
|
|
61
|
+
parsed.fields?.length ?? 0
|
|
62
|
+
} fields`,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
return Response.json({ success: true, message: 'Session restored in backend' });
|
|
66
|
+
} catch (error: any) {
|
|
67
|
+
console.error('Error restoring session:', error);
|
|
68
|
+
return Response.json({ error: error.message || 'Failed to restore session' }, { status: 500 });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { applySignature } from '@larkup/tool-doc-editor';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
const requestSchema = z.object({
|
|
5
|
+
sessionId: z.string(),
|
|
6
|
+
signatureData: z.object({
|
|
7
|
+
mode: z.enum(['type', 'draw', 'upload']),
|
|
8
|
+
imagePayload: z.string().optional(),
|
|
9
|
+
text: z.string().optional(),
|
|
10
|
+
font: z.string().optional(),
|
|
11
|
+
color: z.string().optional(),
|
|
12
|
+
date: z.string().optional(),
|
|
13
|
+
extraText: z.string().optional(),
|
|
14
|
+
pageIndex: z.number(),
|
|
15
|
+
x: z.number().optional(),
|
|
16
|
+
y: z.number().optional(),
|
|
17
|
+
scale: z.number().optional(),
|
|
18
|
+
base64Override: z.string().optional(),
|
|
19
|
+
detectedContext: z.string().optional(),
|
|
20
|
+
placementNote: z.string().optional(),
|
|
21
|
+
}),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export async function POST(req: Request) {
|
|
25
|
+
try {
|
|
26
|
+
const body = await req.json();
|
|
27
|
+
const result = requestSchema.safeParse(body);
|
|
28
|
+
|
|
29
|
+
if (!result.success) {
|
|
30
|
+
return Response.json(
|
|
31
|
+
{ error: 'Invalid request data', details: result.error.issues },
|
|
32
|
+
{ status: 400 },
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { sessionId, signatureData } = result.data;
|
|
37
|
+
const editResult = await applySignature(sessionId, signatureData as any);
|
|
38
|
+
|
|
39
|
+
if (!editResult.success) {
|
|
40
|
+
return Response.json({ error: editResult.error }, { status: 500 });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Since EditResult no longer returns `document` but `parsed` and `fileBase64`, we return those.
|
|
44
|
+
return Response.json({
|
|
45
|
+
document: {
|
|
46
|
+
parsed: (editResult as any).parsed,
|
|
47
|
+
fileBase64: (editResult as any).fileBase64,
|
|
48
|
+
updatedFields: (editResult as any).updatedFields,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
} catch (err: any) {
|
|
52
|
+
console.error('Sign doc error:', err);
|
|
53
|
+
return Response.json({ error: 'Internal Server Error' }, { status: 500 });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { downloadRemoteFile } from '@larkup/workspace-server/lib/remote-files';
|
|
2
|
+
|
|
3
|
+
export const runtime = 'nodejs';
|
|
4
|
+
export const dynamic = 'force-dynamic';
|
|
5
|
+
export const maxDuration = 60;
|
|
6
|
+
|
|
7
|
+
export async function POST(request: Request) {
|
|
8
|
+
try {
|
|
9
|
+
const body = (await request.json()) as { url?: unknown };
|
|
10
|
+
if (typeof body.url !== 'string' || !body.url.trim()) {
|
|
11
|
+
return Response.json({ error: 'Enter a file URL to continue.' }, { status: 400 });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const file = await downloadRemoteFile(body.url);
|
|
15
|
+
const responseBody = file.bytes.buffer.slice(
|
|
16
|
+
file.bytes.byteOffset,
|
|
17
|
+
file.bytes.byteOffset + file.bytes.byteLength,
|
|
18
|
+
) as ArrayBuffer;
|
|
19
|
+
return new Response(responseBody, {
|
|
20
|
+
headers: {
|
|
21
|
+
'Content-Type': file.contentType,
|
|
22
|
+
'Content-Length': String(file.bytes.byteLength),
|
|
23
|
+
'Cache-Control': 'no-store',
|
|
24
|
+
'X-Larkup-File-Name': encodeURIComponent(file.fileName),
|
|
25
|
+
'X-Larkup-Source-Url': encodeURIComponent(file.sourceUrl),
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
} catch (error) {
|
|
29
|
+
const message = error instanceof Error ? error.message : 'Could not download the remote file.';
|
|
30
|
+
return Response.json({ error: message }, { status: 422 });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { deleteMcpConnection, updateMcpConnection } from '@larkup/core/mcp-store';
|
|
2
|
+
|
|
3
|
+
type Params = { params: Promise<{ connectionId: string }> };
|
|
4
|
+
|
|
5
|
+
export async function PUT(req: Request, { params }: Params) {
|
|
6
|
+
try {
|
|
7
|
+
const { connectionId } = await params;
|
|
8
|
+
const updated = await updateMcpConnection(connectionId, await req.json());
|
|
9
|
+
if (!updated) return Response.json({ error: 'MCP connection not found' }, { status: 404 });
|
|
10
|
+
return Response.json(updated);
|
|
11
|
+
} catch (error) {
|
|
12
|
+
return Response.json(
|
|
13
|
+
{ error: error instanceof Error ? error.message : String(error) },
|
|
14
|
+
{ status: 400 },
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function DELETE(_req: Request, { params }: Params) {
|
|
20
|
+
try {
|
|
21
|
+
const { connectionId } = await params;
|
|
22
|
+
if (!(await deleteMcpConnection(connectionId))) {
|
|
23
|
+
return Response.json({ error: 'MCP connection not found' }, { status: 404 });
|
|
24
|
+
}
|
|
25
|
+
return Response.json({ success: true });
|
|
26
|
+
} catch (error) {
|
|
27
|
+
return Response.json({ error: String(error) }, { status: 500 });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { testMcpConnection } from '@larkup/core/mcp-store';
|
|
2
|
+
|
|
3
|
+
type Params = { params: Promise<{ connectionId: string }> };
|
|
4
|
+
|
|
5
|
+
export async function POST(_req: Request, { params }: Params) {
|
|
6
|
+
try {
|
|
7
|
+
const { connectionId } = await params;
|
|
8
|
+
return Response.json(await testMcpConnection(connectionId));
|
|
9
|
+
} catch (error) {
|
|
10
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
11
|
+
return Response.json({ error: message }, { status: message.includes('not found') ? 404 : 502 });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createMcpConnection, listMcpConnections } from '@larkup/core/mcp-store';
|
|
2
|
+
|
|
3
|
+
/** Workspace MCP connections. Credentials are redacted by the core store. */
|
|
4
|
+
export async function GET() {
|
|
5
|
+
try {
|
|
6
|
+
return Response.json({ connections: await listMcpConnections() });
|
|
7
|
+
} catch (error) {
|
|
8
|
+
return Response.json({ error: String(error) }, { status: 500 });
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function POST(req: Request) {
|
|
13
|
+
try {
|
|
14
|
+
const connection = await createMcpConnection(await req.json());
|
|
15
|
+
return Response.json(connection, { status: 201 });
|
|
16
|
+
} catch (error) {
|
|
17
|
+
return Response.json(
|
|
18
|
+
{ error: error instanceof Error ? error.message : String(error) },
|
|
19
|
+
{ status: 400 },
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import mammoth from 'mammoth';
|
|
2
|
+
import { getData } from 'pdf-parse/worker';
|
|
3
|
+
|
|
4
|
+
export const runtime = 'nodejs';
|
|
5
|
+
export const dynamic = 'force-dynamic';
|
|
6
|
+
|
|
7
|
+
// Use pdf-parse's packaged worker payload. Unlike a filesystem path, it is
|
|
8
|
+
// preserved in Next's standalone output and always matches the parser version.
|
|
9
|
+
let pdfParserPromise: Promise<(typeof import('pdf-parse'))['PDFParse']> | undefined;
|
|
10
|
+
|
|
11
|
+
async function getPdfParser() {
|
|
12
|
+
if (!pdfParserPromise) {
|
|
13
|
+
pdfParserPromise = (async () => {
|
|
14
|
+
// Let PDF.js load its own compatible Node canvas implementation. Mixing
|
|
15
|
+
// it with the app-level canvas package creates incompatible Path2D
|
|
16
|
+
// instances and prevents the same PDF from being rendered later.
|
|
17
|
+
const { PDFParse } = await import('pdf-parse');
|
|
18
|
+
PDFParse.setWorker(getData());
|
|
19
|
+
return PDFParse;
|
|
20
|
+
})();
|
|
21
|
+
}
|
|
22
|
+
return pdfParserPromise;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** pdf-parse adds page counters even when a scanned PDF has no text layer. */
|
|
26
|
+
export function hasReadablePdfText(text: string): boolean {
|
|
27
|
+
return text.replace(/^\s*--\s*\d+\s+of\s+\d+\s*--\s*$/gim, '').replace(/[\s\f]+/g, '').length > 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function POST(req: Request) {
|
|
31
|
+
try {
|
|
32
|
+
const formData = await req.formData();
|
|
33
|
+
const file = formData.get('file') as File | null;
|
|
34
|
+
|
|
35
|
+
if (!file) {
|
|
36
|
+
return Response.json({ error: 'No file provided' }, { status: 400 });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const buffer = Buffer.from(await file.arrayBuffer());
|
|
40
|
+
let text = '';
|
|
41
|
+
|
|
42
|
+
if (file.type === 'application/pdf' || file.name.endsWith('.pdf')) {
|
|
43
|
+
// pdf-parse accepts typed-array input consistently across Next's Node
|
|
44
|
+
// runtime and standalone builds. Passing Buffer directly can fail for
|
|
45
|
+
// otherwise-valid PDFs after bundling.
|
|
46
|
+
const PDFParse = await getPdfParser();
|
|
47
|
+
const parser = new PDFParse({ data: new Uint8Array(buffer) });
|
|
48
|
+
try {
|
|
49
|
+
text = (await parser.getText()).text;
|
|
50
|
+
} finally {
|
|
51
|
+
await parser.destroy();
|
|
52
|
+
}
|
|
53
|
+
} else if (
|
|
54
|
+
file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ||
|
|
55
|
+
file.name.endsWith('.docx')
|
|
56
|
+
) {
|
|
57
|
+
const result = await mammoth.extractRawText({ buffer });
|
|
58
|
+
text = result.value;
|
|
59
|
+
} else {
|
|
60
|
+
return Response.json({ error: 'Unsupported file type' }, { status: 400 });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const hasReadableText =
|
|
64
|
+
file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')
|
|
65
|
+
? hasReadablePdfText(text)
|
|
66
|
+
: Boolean(text.trim());
|
|
67
|
+
|
|
68
|
+
// Image-only PDFs are still valid sources. The shared upload UI renders
|
|
69
|
+
// their pages and sends them through the configured vision model.
|
|
70
|
+
if (
|
|
71
|
+
!hasReadableText &&
|
|
72
|
+
(file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf'))
|
|
73
|
+
) {
|
|
74
|
+
return Response.json({ text: '', hasReadableText: false });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!hasReadableText) {
|
|
78
|
+
return Response.json({ error: 'No readable text was found in this file.' }, { status: 422 });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return Response.json({ text, hasReadableText: true });
|
|
82
|
+
} catch (err) {
|
|
83
|
+
const message = err instanceof Error ? err.message : 'Failed to parse file.';
|
|
84
|
+
return Response.json({ error: message }, { status: 500 });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { readDocuments } from '@larkup/core/documents-store';
|
|
2
|
+
import { runWithProject } from '@larkup/core/project-store';
|
|
3
|
+
import { renderStoredPdfPage } from '@larkup/workspace-server/lib/media/pdf-inspection';
|
|
4
|
+
|
|
5
|
+
export const runtime = 'nodejs';
|
|
6
|
+
export const dynamic = 'force-dynamic';
|
|
7
|
+
|
|
8
|
+
export async function GET(request: Request) {
|
|
9
|
+
const url = new URL(request.url);
|
|
10
|
+
const documentId = url.searchParams.get('documentId');
|
|
11
|
+
const pageNumber = Number(url.searchParams.get('page'));
|
|
12
|
+
const projectId = url.searchParams.get('projectId');
|
|
13
|
+
if (!documentId || !Number.isInteger(pageNumber) || pageNumber < 1) {
|
|
14
|
+
return Response.json(
|
|
15
|
+
{ error: 'documentId and a positive page are required.' },
|
|
16
|
+
{ status: 400 },
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const findDocument = async () =>
|
|
22
|
+
(await readDocuments()).find((document) => document.id === documentId);
|
|
23
|
+
const document = projectId
|
|
24
|
+
? await runWithProject(projectId, findDocument)
|
|
25
|
+
: await findDocument();
|
|
26
|
+
if (!document) return Response.json({ error: 'PDF document not found.' }, { status: 404 });
|
|
27
|
+
const rendered = await renderStoredPdfPage(document, pageNumber);
|
|
28
|
+
return new Response(new Uint8Array(rendered.data), {
|
|
29
|
+
headers: {
|
|
30
|
+
'Content-Type': rendered.contentType,
|
|
31
|
+
'Cache-Control': 'private, max-age=300',
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
} catch (error: any) {
|
|
35
|
+
return Response.json(
|
|
36
|
+
{ error: error.message ?? 'Could not render the PDF page.' },
|
|
37
|
+
{ status: 422 },
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import * as http from 'node:http';
|
|
2
|
+
import * as https from 'node:https';
|
|
3
|
+
|
|
4
|
+
export const runtime = 'nodejs';
|
|
5
|
+
export const dynamic = 'force-dynamic';
|
|
6
|
+
|
|
7
|
+
export async function POST(req: Request) {
|
|
8
|
+
try {
|
|
9
|
+
const { server, username, password } = await req.json();
|
|
10
|
+
|
|
11
|
+
if (!server) {
|
|
12
|
+
return Response.json({ error: 'Proxy server URL is required' }, { status: 400 });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let proxyUrl: URL;
|
|
16
|
+
try {
|
|
17
|
+
proxyUrl = new URL(server);
|
|
18
|
+
} catch {
|
|
19
|
+
return Response.json({ error: 'Invalid proxy server URL' }, { status: 400 });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
await new Promise((resolve, reject) => {
|
|
23
|
+
const headers: Record<string, string> = {
|
|
24
|
+
Host: 'example.com',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
if (username && password) {
|
|
28
|
+
const auth = Buffer.from(`${username}:${password}`).toString('base64');
|
|
29
|
+
headers['Proxy-Authorization'] = `Basic ${auth}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const options: http.RequestOptions = {
|
|
33
|
+
hostname: proxyUrl.hostname,
|
|
34
|
+
port: proxyUrl.port || (proxyUrl.protocol === 'https:' ? 443 : 80),
|
|
35
|
+
path: 'http://example.com/',
|
|
36
|
+
method: 'GET',
|
|
37
|
+
headers,
|
|
38
|
+
timeout: 10000,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const request = (proxyUrl.protocol === 'https:' ? https : http).request(options, (res) => {
|
|
42
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 400) {
|
|
43
|
+
let data = '';
|
|
44
|
+
res.on('data', (chunk) => {
|
|
45
|
+
data += chunk;
|
|
46
|
+
});
|
|
47
|
+
res.on('end', () => {
|
|
48
|
+
if (data.includes('Example Domain')) {
|
|
49
|
+
resolve(true);
|
|
50
|
+
} else {
|
|
51
|
+
reject(
|
|
52
|
+
new Error('Proxy connection succeeded, but received invalid response from target'),
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
} else {
|
|
57
|
+
reject(new Error(`Proxy returned status code ${res.statusCode}`));
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
request.on('error', (err) => {
|
|
62
|
+
reject(err);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
request.on('timeout', () => {
|
|
66
|
+
request.destroy();
|
|
67
|
+
reject(new Error('Proxy verification timed out'));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
request.end();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return Response.json({ success: true });
|
|
74
|
+
} catch (err) {
|
|
75
|
+
const message = err instanceof Error ? err.message : 'Verification failed';
|
|
76
|
+
return Response.json({ error: message }, { status: 400 });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { readConfig } from '@larkup/core/config-store';
|
|
2
|
+
import { SerperSearchResponse, SerperSearchItem } from '../google/route';
|
|
3
|
+
|
|
4
|
+
export const runtime = 'nodejs';
|
|
5
|
+
export const dynamic = 'force-dynamic';
|
|
6
|
+
|
|
7
|
+
export async function POST(req: Request) {
|
|
8
|
+
const config = await readConfig();
|
|
9
|
+
const apiKey = config.bingApiKey;
|
|
10
|
+
|
|
11
|
+
if (!apiKey) {
|
|
12
|
+
return Response.json(
|
|
13
|
+
{
|
|
14
|
+
error: 'Bing search is not configured. Set BING_API_KEY in your environment.',
|
|
15
|
+
configured: false,
|
|
16
|
+
},
|
|
17
|
+
{ status: 503 },
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let body: { query?: string; page?: number };
|
|
22
|
+
try {
|
|
23
|
+
body = await req.json();
|
|
24
|
+
} catch {
|
|
25
|
+
return Response.json({ error: 'Invalid request body.' }, { status: 400 });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const { query, page = 1 } = body;
|
|
29
|
+
if (!query?.trim()) {
|
|
30
|
+
return Response.json({ error: 'A search query is required.' }, { status: 400 });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const clampedPage = Math.max(1, Math.min(page, 10));
|
|
34
|
+
const offset = (clampedPage - 1) * 10;
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const res = await fetch(
|
|
38
|
+
`https://serpapi.com/search.json?engine=bing&q=${encodeURIComponent(query.trim())}&first=${
|
|
39
|
+
offset + 1
|
|
40
|
+
}&api_key=${apiKey}`,
|
|
41
|
+
{
|
|
42
|
+
method: 'GET',
|
|
43
|
+
cache: 'no-store',
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const json = await res.json();
|
|
48
|
+
|
|
49
|
+
if (!res.ok || json.error) {
|
|
50
|
+
const msg = json.error ?? `Bing (SerpApi) error (${res.status})`;
|
|
51
|
+
return Response.json({ error: msg }, { status: res.status });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const items: SerperSearchItem[] = (json.organic_results ?? []).map((r: any) => ({
|
|
55
|
+
url: r.link,
|
|
56
|
+
title: r.title,
|
|
57
|
+
description: r.snippet,
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
// SerpApi doesn't easily expose total results for Bing sometimes, but we can check if there's a next page or just estimate
|
|
61
|
+
const rawTotal = 100; // Fake a large number for pagination
|
|
62
|
+
const gotFullPage = items.length >= 10; // Assuming 10 results per page default
|
|
63
|
+
const hasMore = gotFullPage && clampedPage < 10;
|
|
64
|
+
|
|
65
|
+
const totalPages = hasMore ? clampedPage + 1 : clampedPage;
|
|
66
|
+
|
|
67
|
+
return Response.json({
|
|
68
|
+
items,
|
|
69
|
+
totalResults: rawTotal,
|
|
70
|
+
totalResultsIsEstimate: true,
|
|
71
|
+
currentPage: clampedPage,
|
|
72
|
+
totalPages,
|
|
73
|
+
hasMore,
|
|
74
|
+
query: query.trim(),
|
|
75
|
+
} satisfies SerperSearchResponse & { totalResultsIsEstimate: boolean });
|
|
76
|
+
} catch (err) {
|
|
77
|
+
const msg = err instanceof Error ? err.message : 'Bing search failed.';
|
|
78
|
+
return Response.json({ error: msg }, { status: 500 });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function GET() {
|
|
83
|
+
const config = await readConfig();
|
|
84
|
+
const configured = Boolean(config.bingApiKey);
|
|
85
|
+
return Response.json({ configured });
|
|
86
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { readConfig } from '@larkup/core/config-store';
|
|
2
|
+
import { SerperSearchResponse, SerperSearchItem } from '../google/route';
|
|
3
|
+
|
|
4
|
+
export const runtime = 'nodejs';
|
|
5
|
+
export const dynamic = 'force-dynamic';
|
|
6
|
+
|
|
7
|
+
export async function POST(req: Request) {
|
|
8
|
+
const config = await readConfig();
|
|
9
|
+
const apiKey = config.braveApiKey;
|
|
10
|
+
|
|
11
|
+
if (!apiKey) {
|
|
12
|
+
return Response.json(
|
|
13
|
+
{
|
|
14
|
+
error: 'Brave search is not configured. Set BRAVE_API_KEY in your environment.',
|
|
15
|
+
configured: false,
|
|
16
|
+
},
|
|
17
|
+
{ status: 503 },
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let body: { query?: string; page?: number };
|
|
22
|
+
try {
|
|
23
|
+
body = await req.json();
|
|
24
|
+
} catch {
|
|
25
|
+
return Response.json({ error: 'Invalid request body.' }, { status: 400 });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const { query, page = 1 } = body;
|
|
29
|
+
if (!query?.trim()) {
|
|
30
|
+
return Response.json({ error: 'A search query is required.' }, { status: 400 });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const clampedPage = Math.max(1, Math.min(page, 10)); // Brave offset logic can be complex, just map page to offset
|
|
34
|
+
const offset = (clampedPage - 1) * 10;
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const res = await fetch(
|
|
38
|
+
`https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(
|
|
39
|
+
query.trim(),
|
|
40
|
+
)}&count=10&offset=${offset}`,
|
|
41
|
+
{
|
|
42
|
+
method: 'GET',
|
|
43
|
+
headers: {
|
|
44
|
+
Accept: 'application/json',
|
|
45
|
+
'Accept-Encoding': 'gzip',
|
|
46
|
+
'X-Subscription-Token': apiKey,
|
|
47
|
+
},
|
|
48
|
+
cache: 'no-store',
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const json = await res.json();
|
|
53
|
+
|
|
54
|
+
if (!res.ok) {
|
|
55
|
+
const msg = json.message ?? `Brave API error (${res.status})`;
|
|
56
|
+
return Response.json({ error: msg }, { status: res.status });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const items: SerperSearchItem[] = (json.web?.results ?? []).map((r: any) => ({
|
|
60
|
+
url: r.url,
|
|
61
|
+
title: r.title,
|
|
62
|
+
description: r.description,
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
const rawTotal = 100; // Brave doesn't give a total results count easily, cap at 100 for pagination
|
|
66
|
+
const hasMore = items.length === 10 && clampedPage < 10;
|
|
67
|
+
const totalPages = hasMore ? 10 : clampedPage;
|
|
68
|
+
|
|
69
|
+
return Response.json({
|
|
70
|
+
items,
|
|
71
|
+
totalResults: rawTotal,
|
|
72
|
+
totalResultsIsEstimate: true,
|
|
73
|
+
currentPage: clampedPage,
|
|
74
|
+
totalPages,
|
|
75
|
+
hasMore,
|
|
76
|
+
query: query.trim(),
|
|
77
|
+
} satisfies SerperSearchResponse & { totalResultsIsEstimate: boolean });
|
|
78
|
+
} catch (err) {
|
|
79
|
+
const msg = err instanceof Error ? err.message : 'Brave search failed.';
|
|
80
|
+
return Response.json({ error: msg }, { status: 500 });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function GET() {
|
|
85
|
+
const config = await readConfig();
|
|
86
|
+
const configured = Boolean(config.braveApiKey);
|
|
87
|
+
return Response.json({ configured });
|
|
88
|
+
}
|