@echofiles/echo-pdf 0.4.2 → 0.5.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/README.md +232 -16
- package/bin/echo-pdf.js +176 -8
- package/bin/lib/http.js +26 -1
- package/dist/auth.js +16 -4
- package/dist/local/index.d.ts +135 -0
- package/dist/local/index.js +555 -0
- package/dist/mcp-server.js +3 -6
- package/dist/node/pdfium-local.d.ts +8 -0
- package/dist/node/pdfium-local.js +147 -0
- package/dist/node/semantic-local.d.ts +16 -0
- package/dist/node/semantic-local.js +113 -0
- package/dist/pdf-config.js +10 -0
- package/dist/pdf-types.d.ts +4 -0
- package/dist/provider-client.d.ts +8 -0
- package/dist/provider-client.js +39 -0
- package/dist/worker.js +20 -0
- package/package.json +13 -2
package/dist/mcp-server.js
CHANGED
|
@@ -6,11 +6,11 @@ const ok = (id, result) => new Response(JSON.stringify({
|
|
|
6
6
|
id: id ?? null,
|
|
7
7
|
result,
|
|
8
8
|
}), { headers: { "Content-Type": "application/json" } });
|
|
9
|
-
const err = (id, code, message, data) => new Response(JSON.stringify({
|
|
9
|
+
const err = (id, code, message, data, httpStatus = 400) => new Response(JSON.stringify({
|
|
10
10
|
jsonrpc: "2.0",
|
|
11
11
|
id: id ?? null,
|
|
12
12
|
error: data ? { code, message, data } : { code, message },
|
|
13
|
-
}), { status:
|
|
13
|
+
}), { status: httpStatus, headers: { "Content-Type": "application/json" } });
|
|
14
14
|
const asObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? v : {};
|
|
15
15
|
const resolvePublicBaseUrl = (request, configured) => typeof configured === "string" && configured.length > 0 ? configured : request.url;
|
|
16
16
|
const prepareMcpToolArgs = (toolName, args) => {
|
|
@@ -32,10 +32,7 @@ export const handleMcpRequest = async (request, env, config, fileStore) => {
|
|
|
32
32
|
contextName: "MCP",
|
|
33
33
|
});
|
|
34
34
|
if (!auth.ok) {
|
|
35
|
-
return
|
|
36
|
-
status: auth.status,
|
|
37
|
-
headers: { "Content-Type": "application/json" },
|
|
38
|
-
});
|
|
35
|
+
return err(null, -32001, auth.message, { status: auth.status, code: auth.code }, 200);
|
|
39
36
|
}
|
|
40
37
|
let body;
|
|
41
38
|
try {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { EchoPdfConfig } from "../pdf-types.js";
|
|
2
|
+
export declare const getLocalPdfPageCount: (config: EchoPdfConfig, bytes: Uint8Array) => Promise<number>;
|
|
3
|
+
export declare const renderLocalPdfPageToPng: (config: EchoPdfConfig, bytes: Uint8Array, pageIndex: number, scale?: number) => Promise<{
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
png: Uint8Array;
|
|
7
|
+
}>;
|
|
8
|
+
export declare const extractLocalPdfPageText: (config: EchoPdfConfig, bytes: Uint8Array, pageIndex: number) => Promise<string>;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/// <reference path="./compat.d.ts" />
|
|
2
|
+
import { encode as encodePng } from "@cf-wasm/png";
|
|
3
|
+
import { init } from "@embedpdf/pdfium";
|
|
4
|
+
let moduleInstance = null;
|
|
5
|
+
let libraryInitialized = false;
|
|
6
|
+
const isNodeRuntime = () => typeof process !== "undefined" && Boolean(process.versions?.node);
|
|
7
|
+
const ensureWasmFunctionShim = () => {
|
|
8
|
+
const wasmApi = WebAssembly;
|
|
9
|
+
if (typeof wasmApi.Function === "function")
|
|
10
|
+
return;
|
|
11
|
+
wasmApi.Function = (_sig, fn) => fn;
|
|
12
|
+
};
|
|
13
|
+
const readLocalPdfiumWasm = async () => {
|
|
14
|
+
const [{ readFile }, { createRequire }] = await Promise.all([
|
|
15
|
+
import("node:fs/promises"),
|
|
16
|
+
import("node:module"),
|
|
17
|
+
]);
|
|
18
|
+
const require = createRequire(import.meta.url);
|
|
19
|
+
const bytes = await readFile(require.resolve("@embedpdf/pdfium/pdfium.wasm"));
|
|
20
|
+
return new Uint8Array(bytes).slice().buffer;
|
|
21
|
+
};
|
|
22
|
+
const ensureLocalPdfium = async (_config) => {
|
|
23
|
+
if (!isNodeRuntime()) {
|
|
24
|
+
throw new Error("local document APIs require a Node-compatible runtime");
|
|
25
|
+
}
|
|
26
|
+
ensureWasmFunctionShim();
|
|
27
|
+
if (!moduleInstance) {
|
|
28
|
+
moduleInstance = await init({ wasmBinary: await readLocalPdfiumWasm() });
|
|
29
|
+
}
|
|
30
|
+
if (!libraryInitialized) {
|
|
31
|
+
moduleInstance.FPDF_InitLibrary();
|
|
32
|
+
libraryInitialized = true;
|
|
33
|
+
}
|
|
34
|
+
return moduleInstance;
|
|
35
|
+
};
|
|
36
|
+
const makeDoc = (pdfium, bytes) => {
|
|
37
|
+
const memPtr = pdfium.pdfium.wasmExports.malloc(bytes.length);
|
|
38
|
+
pdfium.pdfium.HEAPU8.set(bytes, memPtr);
|
|
39
|
+
const doc = pdfium.FPDF_LoadMemDocument(memPtr, bytes.length, "");
|
|
40
|
+
if (!doc) {
|
|
41
|
+
pdfium.pdfium.wasmExports.free(memPtr);
|
|
42
|
+
throw new Error("Failed to load PDF document");
|
|
43
|
+
}
|
|
44
|
+
return { doc, memPtr };
|
|
45
|
+
};
|
|
46
|
+
const closeDoc = (pdfium, doc, memPtr) => {
|
|
47
|
+
pdfium.FPDF_CloseDocument(doc);
|
|
48
|
+
pdfium.pdfium.wasmExports.free(memPtr);
|
|
49
|
+
};
|
|
50
|
+
const decodeUtf16Le = (buf) => {
|
|
51
|
+
const view = new Uint16Array(buf.buffer, buf.byteOffset, Math.floor(buf.byteLength / 2));
|
|
52
|
+
const chars = [];
|
|
53
|
+
for (const code of view) {
|
|
54
|
+
if (code === 0)
|
|
55
|
+
break;
|
|
56
|
+
chars.push(code);
|
|
57
|
+
}
|
|
58
|
+
return String.fromCharCode(...chars);
|
|
59
|
+
};
|
|
60
|
+
const bgraToRgba = (bgra) => {
|
|
61
|
+
const rgba = new Uint8Array(bgra.length);
|
|
62
|
+
for (let i = 0; i < bgra.length; i += 4) {
|
|
63
|
+
rgba[i] = bgra[i + 2] ?? 0;
|
|
64
|
+
rgba[i + 1] = bgra[i + 1] ?? 0;
|
|
65
|
+
rgba[i + 2] = bgra[i] ?? 0;
|
|
66
|
+
rgba[i + 3] = bgra[i + 3] ?? 255;
|
|
67
|
+
}
|
|
68
|
+
return rgba;
|
|
69
|
+
};
|
|
70
|
+
export const getLocalPdfPageCount = async (config, bytes) => {
|
|
71
|
+
const pdfium = await ensureLocalPdfium(config);
|
|
72
|
+
const { doc, memPtr } = makeDoc(pdfium, bytes);
|
|
73
|
+
try {
|
|
74
|
+
return pdfium.FPDF_GetPageCount(doc);
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
closeDoc(pdfium, doc, memPtr);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
export const renderLocalPdfPageToPng = async (config, bytes, pageIndex, scale = config.service.defaultRenderScale) => {
|
|
81
|
+
const pdfium = await ensureLocalPdfium(config);
|
|
82
|
+
const { doc, memPtr } = makeDoc(pdfium, bytes);
|
|
83
|
+
let page = 0;
|
|
84
|
+
let bitmap = 0;
|
|
85
|
+
try {
|
|
86
|
+
page = pdfium.FPDF_LoadPage(doc, pageIndex);
|
|
87
|
+
if (!page) {
|
|
88
|
+
throw new Error(`Failed to load page ${pageIndex}`);
|
|
89
|
+
}
|
|
90
|
+
const width = Math.max(1, Math.round(pdfium.FPDF_GetPageWidthF(page) * scale));
|
|
91
|
+
const height = Math.max(1, Math.round(pdfium.FPDF_GetPageHeightF(page) * scale));
|
|
92
|
+
bitmap = pdfium.FPDFBitmap_Create(width, height, 1);
|
|
93
|
+
if (!bitmap) {
|
|
94
|
+
throw new Error("Failed to create bitmap");
|
|
95
|
+
}
|
|
96
|
+
pdfium.FPDFBitmap_FillRect(bitmap, 0, 0, width, height, 0xffffffff);
|
|
97
|
+
pdfium.FPDF_RenderPageBitmap(bitmap, page, 0, 0, width, height, 0, 0);
|
|
98
|
+
const stride = pdfium.FPDFBitmap_GetStride(bitmap);
|
|
99
|
+
const bufferPtr = pdfium.FPDFBitmap_GetBuffer(bitmap);
|
|
100
|
+
const heap = pdfium.pdfium.HEAPU8;
|
|
101
|
+
const bgra = heap.slice(bufferPtr, bufferPtr + stride * height);
|
|
102
|
+
const rgba = bgraToRgba(bgra);
|
|
103
|
+
const png = encodePng(rgba, width, height);
|
|
104
|
+
return { width, height, png };
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
if (bitmap)
|
|
108
|
+
pdfium.FPDFBitmap_Destroy(bitmap);
|
|
109
|
+
if (page)
|
|
110
|
+
pdfium.FPDF_ClosePage(page);
|
|
111
|
+
closeDoc(pdfium, doc, memPtr);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
export const extractLocalPdfPageText = async (config, bytes, pageIndex) => {
|
|
115
|
+
const pdfium = await ensureLocalPdfium(config);
|
|
116
|
+
const { doc, memPtr } = makeDoc(pdfium, bytes);
|
|
117
|
+
let page = 0;
|
|
118
|
+
let textPage = 0;
|
|
119
|
+
let outPtr = 0;
|
|
120
|
+
try {
|
|
121
|
+
page = pdfium.FPDF_LoadPage(doc, pageIndex);
|
|
122
|
+
if (!page) {
|
|
123
|
+
throw new Error(`Failed to load page ${pageIndex}`);
|
|
124
|
+
}
|
|
125
|
+
textPage = pdfium.FPDFText_LoadPage(page);
|
|
126
|
+
if (!textPage)
|
|
127
|
+
return "";
|
|
128
|
+
const chars = pdfium.FPDFText_CountChars(textPage);
|
|
129
|
+
if (chars <= 0)
|
|
130
|
+
return "";
|
|
131
|
+
const bytesLen = (chars + 1) * 2;
|
|
132
|
+
outPtr = pdfium.pdfium.wasmExports.malloc(bytesLen);
|
|
133
|
+
pdfium.FPDFText_GetText(textPage, 0, chars, outPtr);
|
|
134
|
+
const heap = pdfium.pdfium.HEAPU8;
|
|
135
|
+
const raw = heap.slice(outPtr, outPtr + bytesLen);
|
|
136
|
+
return decodeUtf16Le(raw).trim();
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
if (outPtr)
|
|
140
|
+
pdfium.pdfium.wasmExports.free(outPtr);
|
|
141
|
+
if (textPage)
|
|
142
|
+
pdfium.FPDFText_ClosePage(textPage);
|
|
143
|
+
if (page)
|
|
144
|
+
pdfium.FPDF_ClosePage(page);
|
|
145
|
+
closeDoc(pdfium, doc, memPtr);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface SemanticPageInput {
|
|
2
|
+
readonly pageNumber: number;
|
|
3
|
+
readonly text: string;
|
|
4
|
+
readonly artifactPath: string;
|
|
5
|
+
}
|
|
6
|
+
export interface SemanticSectionNode {
|
|
7
|
+
readonly id: string;
|
|
8
|
+
readonly type: "section";
|
|
9
|
+
readonly title: string;
|
|
10
|
+
readonly level: number;
|
|
11
|
+
readonly pageNumber: number;
|
|
12
|
+
readonly pageArtifactPath: string;
|
|
13
|
+
readonly excerpt: string;
|
|
14
|
+
readonly children: ReadonlyArray<SemanticSectionNode>;
|
|
15
|
+
}
|
|
16
|
+
export declare const buildSemanticSectionTree: (pages: ReadonlyArray<SemanticPageInput>) => ReadonlyArray<SemanticSectionNode>;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
const normalizeLine = (value) => value.replace(/\s+/g, " ").trim();
|
|
2
|
+
const excerptFor = (value) => normalizeLine(value).slice(0, 160);
|
|
3
|
+
const hasTocSuffix = (value) => /(?:\.{2,}|\s{2,}|\t)\d+$/.test(value);
|
|
4
|
+
const hasTrailingPageNumber = (value) => /\s\d+$/.test(value);
|
|
5
|
+
const isContentsHeading = (value) => {
|
|
6
|
+
const normalized = normalizeLine(value).toLowerCase();
|
|
7
|
+
return normalized === "contents" || normalized === "table of contents" || normalized === "目录";
|
|
8
|
+
};
|
|
9
|
+
const detectHeading = (line) => {
|
|
10
|
+
const normalized = normalizeLine(line);
|
|
11
|
+
if (!normalized || normalized.length > 120)
|
|
12
|
+
return null;
|
|
13
|
+
if (hasTocSuffix(normalized))
|
|
14
|
+
return null;
|
|
15
|
+
const numbered = normalized.match(/^(\d+(?:\.\d+){0,3})\s+(.+)$/);
|
|
16
|
+
if (numbered) {
|
|
17
|
+
const numberPath = numbered[1] || "";
|
|
18
|
+
const topLevelNumber = Number.parseInt(numberPath.split(".")[0] || "", 10);
|
|
19
|
+
const title = normalizeLine(numbered[2] || "");
|
|
20
|
+
const level = numberPath.split(".").length;
|
|
21
|
+
if (!title)
|
|
22
|
+
return null;
|
|
23
|
+
if (title.length < 2)
|
|
24
|
+
return null;
|
|
25
|
+
if (hasTrailingPageNumber(normalized))
|
|
26
|
+
return null;
|
|
27
|
+
if (!/^[A-Za-z\u4E00-\u9FFF第((]/.test(title))
|
|
28
|
+
return null;
|
|
29
|
+
if (/^(GHz|MHz|Kbps|Mbps|Hz|kHz|mA|V|W)\b/i.test(title))
|
|
30
|
+
return null;
|
|
31
|
+
if (/[。;;::]$/.test(title))
|
|
32
|
+
return null;
|
|
33
|
+
if (Number.isFinite(topLevelNumber) && topLevelNumber > 20)
|
|
34
|
+
return null;
|
|
35
|
+
if (/^[A-Z]+\d+$/.test(title))
|
|
36
|
+
return null;
|
|
37
|
+
if (level === 1 && title.length > 40)
|
|
38
|
+
return null;
|
|
39
|
+
if (level === 1 && /[,,×—]/.test(title))
|
|
40
|
+
return null;
|
|
41
|
+
return {
|
|
42
|
+
title: `${numberPath} ${title}`.trim(),
|
|
43
|
+
level,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const chinese = normalized.match(/^(第[0-9一二三四五六七八九十百]+)(章|节|部分)\s+(.+)$/);
|
|
47
|
+
if (chinese) {
|
|
48
|
+
const suffix = chinese[2] || "";
|
|
49
|
+
return {
|
|
50
|
+
title: normalized,
|
|
51
|
+
level: suffix === "节" ? 2 : 1,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const english = normalized.match(/^(Chapter|Section|Part|Appendix)\b[:\s-]*(.+)?$/i);
|
|
55
|
+
if (english) {
|
|
56
|
+
return {
|
|
57
|
+
title: normalized,
|
|
58
|
+
level: /section/i.test(english[1] || "") ? 2 : 1,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
};
|
|
63
|
+
const toReadonlyTree = (node) => ({
|
|
64
|
+
...node,
|
|
65
|
+
children: node.children.map(toReadonlyTree),
|
|
66
|
+
});
|
|
67
|
+
export const buildSemanticSectionTree = (pages) => {
|
|
68
|
+
const rootChildren = [];
|
|
69
|
+
const stack = [];
|
|
70
|
+
const emittedKeys = new Set();
|
|
71
|
+
let nextId = 1;
|
|
72
|
+
for (const page of pages) {
|
|
73
|
+
const lines = page.text
|
|
74
|
+
.split(/\r?\n/)
|
|
75
|
+
.map(normalizeLine)
|
|
76
|
+
.filter(Boolean);
|
|
77
|
+
if (lines.length === 0)
|
|
78
|
+
continue;
|
|
79
|
+
const contentsPage = isContentsHeading(lines[0] || "");
|
|
80
|
+
for (const line of lines) {
|
|
81
|
+
const heading = detectHeading(line);
|
|
82
|
+
if (!heading || contentsPage)
|
|
83
|
+
continue;
|
|
84
|
+
const emittedKey = `${heading.level}:${heading.title}`;
|
|
85
|
+
if (emittedKeys.has(emittedKey))
|
|
86
|
+
continue;
|
|
87
|
+
const node = {
|
|
88
|
+
id: `section-${nextId}`,
|
|
89
|
+
type: "section",
|
|
90
|
+
title: heading.title,
|
|
91
|
+
level: heading.level,
|
|
92
|
+
pageNumber: page.pageNumber,
|
|
93
|
+
pageArtifactPath: page.artifactPath,
|
|
94
|
+
excerpt: excerptFor(line),
|
|
95
|
+
children: [],
|
|
96
|
+
};
|
|
97
|
+
nextId += 1;
|
|
98
|
+
emittedKeys.add(emittedKey);
|
|
99
|
+
while (stack.length > 0 && (stack[stack.length - 1]?.level || 0) >= heading.level) {
|
|
100
|
+
stack.pop();
|
|
101
|
+
}
|
|
102
|
+
const parent = stack[stack.length - 1];
|
|
103
|
+
if (parent) {
|
|
104
|
+
parent.children.push(node);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
rootChildren.push(node);
|
|
108
|
+
}
|
|
109
|
+
stack.push(node);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return rootChildren.map(toReadonlyTree);
|
|
113
|
+
};
|
package/dist/pdf-config.js
CHANGED
|
@@ -69,6 +69,8 @@ export const loadEchoPdfConfig = (env) => {
|
|
|
69
69
|
const providerOverride = env.ECHO_PDF_DEFAULT_PROVIDER;
|
|
70
70
|
const modelOverride = env.ECHO_PDF_DEFAULT_MODEL;
|
|
71
71
|
const publicBaseUrlOverride = env.ECHO_PDF_PUBLIC_BASE_URL;
|
|
72
|
+
const computeAuthHeaderOverride = env.ECHO_PDF_COMPUTE_AUTH_HEADER;
|
|
73
|
+
const computeAuthEnvOverride = env.ECHO_PDF_COMPUTE_AUTH_ENV;
|
|
72
74
|
const fileGetAuthHeaderOverride = env.ECHO_PDF_FILE_GET_AUTH_HEADER;
|
|
73
75
|
const fileGetAuthEnvOverride = env.ECHO_PDF_FILE_GET_AUTH_ENV;
|
|
74
76
|
const fileGetCacheTtlOverride = env.ECHO_PDF_FILE_GET_CACHE_TTL_SECONDS;
|
|
@@ -79,6 +81,14 @@ export const loadEchoPdfConfig = (env) => {
|
|
|
79
81
|
publicBaseUrl: typeof publicBaseUrlOverride === "string" && publicBaseUrlOverride.trim().length > 0
|
|
80
82
|
? publicBaseUrlOverride.trim()
|
|
81
83
|
: resolved.service.publicBaseUrl,
|
|
84
|
+
computeAuth: {
|
|
85
|
+
authHeader: typeof computeAuthHeaderOverride === "string" && computeAuthHeaderOverride.trim().length > 0
|
|
86
|
+
? computeAuthHeaderOverride.trim()
|
|
87
|
+
: resolved.service.computeAuth?.authHeader,
|
|
88
|
+
authEnv: typeof computeAuthEnvOverride === "string" && computeAuthEnvOverride.trim().length > 0
|
|
89
|
+
? computeAuthEnvOverride.trim()
|
|
90
|
+
: resolved.service.computeAuth?.authEnv,
|
|
91
|
+
},
|
|
82
92
|
fileGet: {
|
|
83
93
|
authHeader: typeof fileGetAuthHeaderOverride === "string" && fileGetAuthHeaderOverride.trim().length > 0
|
|
84
94
|
? fileGetAuthHeaderOverride.trim()
|
package/dist/pdf-types.d.ts
CHANGED
|
@@ -20,6 +20,10 @@ export interface EchoPdfConfig {
|
|
|
20
20
|
readonly service: {
|
|
21
21
|
readonly name: string;
|
|
22
22
|
readonly publicBaseUrl?: string;
|
|
23
|
+
readonly computeAuth?: {
|
|
24
|
+
readonly authHeader?: string;
|
|
25
|
+
readonly authEnv?: string;
|
|
26
|
+
};
|
|
23
27
|
readonly fileGet?: {
|
|
24
28
|
readonly authHeader?: string;
|
|
25
29
|
readonly authEnv?: string;
|
|
@@ -10,3 +10,11 @@ export declare const visionRecognize: (input: {
|
|
|
10
10
|
imageDataUrl: string;
|
|
11
11
|
runtimeApiKeys?: Record<string, string>;
|
|
12
12
|
}) => Promise<string>;
|
|
13
|
+
export declare const generateText: (input: {
|
|
14
|
+
config: EchoPdfConfig;
|
|
15
|
+
env: Env;
|
|
16
|
+
providerAlias: string;
|
|
17
|
+
model: string;
|
|
18
|
+
prompt: string;
|
|
19
|
+
runtimeApiKeys?: Record<string, string>;
|
|
20
|
+
}) => Promise<string>;
|
package/dist/provider-client.js
CHANGED
|
@@ -132,3 +132,42 @@ export const visionRecognize = async (input) => {
|
|
|
132
132
|
}
|
|
133
133
|
return "";
|
|
134
134
|
};
|
|
135
|
+
export const generateText = async (input) => {
|
|
136
|
+
const provider = getProvider(input.config, input.providerAlias);
|
|
137
|
+
const url = resolveEndpoint(provider, "chatCompletionsPath");
|
|
138
|
+
const response = await withTimeout(url, {
|
|
139
|
+
method: "POST",
|
|
140
|
+
headers: {
|
|
141
|
+
"Content-Type": "application/json",
|
|
142
|
+
...toAuthHeader(input.config, input.providerAlias, provider, input.env, input.runtimeApiKeys),
|
|
143
|
+
...(provider.headers ?? {}),
|
|
144
|
+
},
|
|
145
|
+
body: JSON.stringify({
|
|
146
|
+
model: input.model,
|
|
147
|
+
messages: [
|
|
148
|
+
{
|
|
149
|
+
role: "user",
|
|
150
|
+
content: input.prompt,
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
}),
|
|
154
|
+
}, provider.timeoutMs ?? 30000);
|
|
155
|
+
if (!response.ok) {
|
|
156
|
+
throw new Error(`Text generation request failed: HTTP ${response.status} url=${url} detail=${await responseDetail(response)}`);
|
|
157
|
+
}
|
|
158
|
+
const payload = await response.json();
|
|
159
|
+
const message = payload.choices?.[0]?.message;
|
|
160
|
+
if (!message)
|
|
161
|
+
return "";
|
|
162
|
+
const content = message.content;
|
|
163
|
+
if (typeof content === "string")
|
|
164
|
+
return content;
|
|
165
|
+
if (Array.isArray(content)) {
|
|
166
|
+
return content
|
|
167
|
+
.map((part) => part)
|
|
168
|
+
.filter((part) => part.type === "text" && typeof part.text === "string")
|
|
169
|
+
.map((part) => part.text ?? "")
|
|
170
|
+
.join("");
|
|
171
|
+
}
|
|
172
|
+
return "";
|
|
173
|
+
};
|
package/dist/worker.js
CHANGED
|
@@ -111,6 +111,14 @@ const operationArgsFromRequest = (request) => {
|
|
|
111
111
|
args.prompt = request.prompt;
|
|
112
112
|
return args;
|
|
113
113
|
};
|
|
114
|
+
const checkComputeAuth = (request, env, config) => checkHeaderAuth(request, env, {
|
|
115
|
+
authHeader: config.service.computeAuth?.authHeader,
|
|
116
|
+
authEnv: config.service.computeAuth?.authEnv,
|
|
117
|
+
allowMissingSecret: false,
|
|
118
|
+
misconfiguredCode: "COMPUTE_AUTH_MISCONFIGURED",
|
|
119
|
+
unauthorizedCode: "UNAUTHORIZED",
|
|
120
|
+
contextName: "compute endpoint",
|
|
121
|
+
});
|
|
114
122
|
export default {
|
|
115
123
|
async fetch(request, env, ctx) {
|
|
116
124
|
const url = new URL(request.url);
|
|
@@ -149,6 +157,9 @@ export default {
|
|
|
149
157
|
return json({ tools: listToolSchemas() });
|
|
150
158
|
}
|
|
151
159
|
if (request.method === "POST" && url.pathname === "/tools/call") {
|
|
160
|
+
const auth = checkComputeAuth(request, env, config);
|
|
161
|
+
if (!auth.ok)
|
|
162
|
+
return json({ error: auth.message, code: auth.code }, auth.status);
|
|
152
163
|
const body = await readJson(request);
|
|
153
164
|
const name = typeof body.name === "string" ? body.name : "";
|
|
154
165
|
if (!name)
|
|
@@ -180,6 +191,9 @@ export default {
|
|
|
180
191
|
}
|
|
181
192
|
}
|
|
182
193
|
if (request.method === "POST" && url.pathname === "/providers/models") {
|
|
194
|
+
const auth = checkComputeAuth(request, env, config);
|
|
195
|
+
if (!auth.ok)
|
|
196
|
+
return json({ error: auth.message, code: auth.code }, auth.status);
|
|
183
197
|
const body = await readJson(request);
|
|
184
198
|
const provider = resolveProviderAlias(config, typeof body.provider === "string" ? body.provider : undefined);
|
|
185
199
|
const runtimeKeys = typeof body.providerApiKeys === "object" && body.providerApiKeys !== null
|
|
@@ -194,6 +208,9 @@ export default {
|
|
|
194
208
|
}
|
|
195
209
|
}
|
|
196
210
|
if (request.method === "POST" && url.pathname === "/api/agent/run") {
|
|
211
|
+
const auth = checkComputeAuth(request, env, config);
|
|
212
|
+
if (!auth.ok)
|
|
213
|
+
return json({ error: auth.message, code: auth.code }, auth.status);
|
|
197
214
|
const body = await readJson(request);
|
|
198
215
|
if (Object.hasOwn(body, "operation") && !isValidOperation(body.operation)) {
|
|
199
216
|
return json({ error: "Invalid operation. Must be one of: extract_pages, ocr_pages, tables_to_latex" }, 400);
|
|
@@ -213,6 +230,9 @@ export default {
|
|
|
213
230
|
}
|
|
214
231
|
}
|
|
215
232
|
if (request.method === "POST" && url.pathname === "/api/agent/stream") {
|
|
233
|
+
const auth = checkComputeAuth(request, env, config);
|
|
234
|
+
if (!auth.ok)
|
|
235
|
+
return json({ error: auth.message, code: auth.code }, auth.status);
|
|
216
236
|
const body = await readJson(request);
|
|
217
237
|
if (Object.hasOwn(body, "operation") && !isValidOperation(body.operation)) {
|
|
218
238
|
return json({ error: "Invalid operation. Must be one of: extract_pages, ocr_pages, tables_to_latex" }, 400);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@echofiles/echo-pdf",
|
|
3
|
-
"description": "
|
|
4
|
-
"version": "0.
|
|
3
|
+
"description": "Local-first PDF document component core with CLI, workspace artifacts, and reusable page primitives.",
|
|
4
|
+
"version": "0.5.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
"types": "./dist/core/index.d.ts",
|
|
21
21
|
"import": "./dist/core/index.js"
|
|
22
22
|
},
|
|
23
|
+
"./local": {
|
|
24
|
+
"types": "./dist/local/index.d.ts",
|
|
25
|
+
"import": "./dist/local/index.js"
|
|
26
|
+
},
|
|
23
27
|
"./worker": {
|
|
24
28
|
"types": "./dist/worker.d.ts",
|
|
25
29
|
"import": "./dist/worker.js"
|
|
@@ -38,6 +42,13 @@
|
|
|
38
42
|
"check:runtime": "bash ./scripts/check-runtime.sh",
|
|
39
43
|
"dev": "wrangler dev",
|
|
40
44
|
"deploy": "wrangler deploy",
|
|
45
|
+
"document:dev": "ECHO_PDF_SOURCE_DEV=1 bun ./bin/echo-pdf.js",
|
|
46
|
+
"eval": "node ./eval/run-local.mjs",
|
|
47
|
+
"eval:smoke": "node ./eval/run-local.mjs --suite smoke",
|
|
48
|
+
"eval:core": "node ./eval/run-local.mjs --suite core",
|
|
49
|
+
"eval:stress": "node ./eval/run-local.mjs --suite stress",
|
|
50
|
+
"eval:known-bad": "node ./eval/run-local.mjs --suite known-bad",
|
|
51
|
+
"eval:fetch-public-samples": "node ./eval/fetch-public-samples.mjs",
|
|
41
52
|
"typecheck": "npm run check:runtime && tsc --noEmit",
|
|
42
53
|
"test:unit": "npm run check:runtime && vitest run tests/unit",
|
|
43
54
|
"test:import-smoke": "npm run check:runtime && npm run build && vitest run tests/integration/npm-pack-import.integration.test.ts tests/integration/ts-nodenext-consumer.integration.test.ts",
|