@echofiles/echo-pdf 0.3.1 → 0.4.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 +39 -0
- package/bin/echo-pdf.js +227 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,6 +48,13 @@ npm i -g @echofiles/echo-pdf
|
|
|
48
48
|
echo-pdf init --service-url https://echo-pdf.echofilesai.workers.dev
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
+
本地一键启动服务(daemon):
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
echo-pdf dev --port 8788
|
|
55
|
+
echo-pdf init --service-url http://127.0.0.1:8788
|
|
56
|
+
```
|
|
57
|
+
|
|
51
58
|
配置 API Key(仅保存在本机 CLI 配置,不会上报到服务端存储):
|
|
52
59
|
|
|
53
60
|
```bash
|
|
@@ -115,6 +122,20 @@ echo-pdf mcp call --tool pdf_extract_pages --args '{
|
|
|
115
122
|
}'
|
|
116
123
|
```
|
|
117
124
|
|
|
125
|
+
### 3.1.3 stdio MCP(支持本地文件路径)
|
|
126
|
+
|
|
127
|
+
stdio 模式会把本地 `path/filePath` 自动上传为 `fileId` 后再调用远端工具。
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
echo-pdf mcp stdio
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
生成 Claude Desktop/Cursor 等可用的 stdio 配置片段:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
echo-pdf setup add claude-desktop --mode stdio
|
|
137
|
+
```
|
|
138
|
+
|
|
118
139
|
### 3.2 给客户端生成 MCP 配置片段
|
|
119
140
|
|
|
120
141
|
```bash
|
|
@@ -172,6 +193,12 @@ curl -sS -X POST https://echo-pdf.echofilesai.workers.dev/api/files/upload \
|
|
|
172
193
|
|
|
173
194
|
返回中会拿到 `file.id`。
|
|
174
195
|
|
|
196
|
+
CLI 等价命令:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
echo-pdf file upload ./sample.pdf
|
|
200
|
+
```
|
|
201
|
+
|
|
175
202
|
### 5.2 提取页面图片
|
|
176
203
|
|
|
177
204
|
```bash
|
|
@@ -185,6 +212,18 @@ curl -sS -X POST https://echo-pdf.echofilesai.workers.dev/tools/call \
|
|
|
185
212
|
}'
|
|
186
213
|
```
|
|
187
214
|
|
|
215
|
+
CLI(支持直接传本地路径):
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
echo-pdf call --tool pdf_extract_pages --args '{"path":"./sample.pdf","pages":[1],"returnMode":"url"}'
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
下载产物:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
echo-pdf file get --file-id <FILE_ID> --out ./output.bin
|
|
225
|
+
```
|
|
226
|
+
|
|
188
227
|
### 5.3 OCR
|
|
189
228
|
|
|
190
229
|
```bash
|
package/bin/echo-pdf.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process"
|
|
2
3
|
import fs from "node:fs"
|
|
3
4
|
import os from "node:os"
|
|
4
5
|
import path from "node:path"
|
|
@@ -208,6 +209,170 @@ const buildMcpRequest = (id, method, params = {}) => ({
|
|
|
208
209
|
params,
|
|
209
210
|
})
|
|
210
211
|
|
|
212
|
+
const uploadFile = async (serviceUrl, filePath) => {
|
|
213
|
+
const absPath = path.resolve(process.cwd(), filePath)
|
|
214
|
+
const bytes = fs.readFileSync(absPath)
|
|
215
|
+
const filename = path.basename(absPath)
|
|
216
|
+
const form = new FormData()
|
|
217
|
+
form.append("file", new Blob([bytes]), filename)
|
|
218
|
+
const response = await fetch(`${serviceUrl}/api/files/upload`, { method: "POST", body: form })
|
|
219
|
+
const text = await response.text()
|
|
220
|
+
let data
|
|
221
|
+
try {
|
|
222
|
+
data = JSON.parse(text)
|
|
223
|
+
} catch {
|
|
224
|
+
data = { raw: text }
|
|
225
|
+
}
|
|
226
|
+
if (!response.ok) {
|
|
227
|
+
throw new Error(`${response.status} ${JSON.stringify(data)}`)
|
|
228
|
+
}
|
|
229
|
+
return data
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const downloadFile = async (serviceUrl, fileId, outputPath) => {
|
|
233
|
+
const response = await fetch(`${serviceUrl}/api/files/get?fileId=${encodeURIComponent(fileId)}&download=1`)
|
|
234
|
+
if (!response.ok) {
|
|
235
|
+
const text = await response.text()
|
|
236
|
+
throw new Error(`${response.status} ${text}`)
|
|
237
|
+
}
|
|
238
|
+
const bytes = Buffer.from(await response.arrayBuffer())
|
|
239
|
+
const absOut = path.resolve(process.cwd(), outputPath)
|
|
240
|
+
fs.mkdirSync(path.dirname(absOut), { recursive: true })
|
|
241
|
+
fs.writeFileSync(absOut, bytes)
|
|
242
|
+
return absOut
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const withUploadedLocalFile = async (serviceUrl, tool, args) => {
|
|
246
|
+
const nextArgs = { ...(args || {}) }
|
|
247
|
+
if (tool.startsWith("pdf_")) {
|
|
248
|
+
const localPath = typeof nextArgs.path === "string"
|
|
249
|
+
? nextArgs.path
|
|
250
|
+
: (typeof nextArgs.filePath === "string" ? nextArgs.filePath : "")
|
|
251
|
+
if (localPath && !nextArgs.fileId && !nextArgs.url && !nextArgs.base64) {
|
|
252
|
+
const upload = await uploadFile(serviceUrl, localPath)
|
|
253
|
+
const fileId = upload?.file?.id
|
|
254
|
+
if (!fileId) throw new Error(`upload failed for local path: ${localPath}`)
|
|
255
|
+
nextArgs.fileId = fileId
|
|
256
|
+
delete nextArgs.path
|
|
257
|
+
delete nextArgs.filePath
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return nextArgs
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const runDevServer = (port, host) => {
|
|
264
|
+
const wranglerBin = path.resolve(__dirname, "../node_modules/.bin/wrangler")
|
|
265
|
+
const wranglerArgs = ["dev", "--port", String(port), "--ip", host]
|
|
266
|
+
const cmd = fs.existsSync(wranglerBin) ? wranglerBin : "npx"
|
|
267
|
+
const args = fs.existsSync(wranglerBin) ? wranglerArgs : ["-y", "wrangler", ...wranglerArgs]
|
|
268
|
+
const child = spawn(cmd, args, {
|
|
269
|
+
stdio: "inherit",
|
|
270
|
+
env: process.env,
|
|
271
|
+
cwd: process.cwd(),
|
|
272
|
+
})
|
|
273
|
+
child.on("exit", (code, signal) => {
|
|
274
|
+
if (signal) process.kill(process.pid, signal)
|
|
275
|
+
process.exit(code ?? 0)
|
|
276
|
+
})
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const mcpReadLoop = (onMessage, onError) => {
|
|
280
|
+
let buffer = Buffer.alloc(0)
|
|
281
|
+
let expectedLength = null
|
|
282
|
+
process.stdin.on("data", (chunk) => {
|
|
283
|
+
buffer = Buffer.concat([buffer, chunk])
|
|
284
|
+
while (true) {
|
|
285
|
+
if (expectedLength === null) {
|
|
286
|
+
const headerEnd = buffer.indexOf("\r\n\r\n")
|
|
287
|
+
if (headerEnd === -1) break
|
|
288
|
+
const headerRaw = buffer.slice(0, headerEnd).toString("utf-8")
|
|
289
|
+
const lines = headerRaw.split("\r\n")
|
|
290
|
+
const cl = lines.find((line) => line.toLowerCase().startsWith("content-length:"))
|
|
291
|
+
if (!cl) {
|
|
292
|
+
onError(new Error("Missing Content-Length"))
|
|
293
|
+
buffer = buffer.slice(headerEnd + 4)
|
|
294
|
+
continue
|
|
295
|
+
}
|
|
296
|
+
expectedLength = Number(cl.split(":")[1]?.trim() || "0")
|
|
297
|
+
buffer = buffer.slice(headerEnd + 4)
|
|
298
|
+
}
|
|
299
|
+
if (!Number.isFinite(expectedLength) || expectedLength < 0) {
|
|
300
|
+
onError(new Error("Invalid Content-Length"))
|
|
301
|
+
expectedLength = null
|
|
302
|
+
continue
|
|
303
|
+
}
|
|
304
|
+
if (buffer.length < expectedLength) break
|
|
305
|
+
const body = buffer.slice(0, expectedLength).toString("utf-8")
|
|
306
|
+
buffer = buffer.slice(expectedLength)
|
|
307
|
+
expectedLength = null
|
|
308
|
+
try {
|
|
309
|
+
const maybePromise = onMessage(JSON.parse(body))
|
|
310
|
+
if (maybePromise && typeof maybePromise.then === "function") {
|
|
311
|
+
maybePromise.catch(onError)
|
|
312
|
+
}
|
|
313
|
+
} catch (error) {
|
|
314
|
+
onError(error)
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
})
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const mcpWrite = (obj) => {
|
|
321
|
+
const body = Buffer.from(JSON.stringify(obj))
|
|
322
|
+
const header = Buffer.from(`Content-Length: ${body.length}\r\n\r\n`)
|
|
323
|
+
process.stdout.write(header)
|
|
324
|
+
process.stdout.write(body)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const runMcpStdio = async () => {
|
|
328
|
+
const config = loadConfig()
|
|
329
|
+
const serviceUrl = config.serviceUrl
|
|
330
|
+
const headers = buildMcpHeaders()
|
|
331
|
+
mcpReadLoop(async (msg) => {
|
|
332
|
+
const method = msg?.method
|
|
333
|
+
const id = Object.hasOwn(msg || {}, "id") ? msg.id : null
|
|
334
|
+
if (msg?.jsonrpc !== "2.0" || typeof method !== "string") {
|
|
335
|
+
mcpWrite({ jsonrpc: "2.0", id, error: { code: -32600, message: "Invalid Request" } })
|
|
336
|
+
return
|
|
337
|
+
}
|
|
338
|
+
if (method === "notifications/initialized") return
|
|
339
|
+
if (method === "initialize" || method === "tools/list") {
|
|
340
|
+
const data = await postJson(`${serviceUrl}/mcp`, msg, headers)
|
|
341
|
+
mcpWrite(data)
|
|
342
|
+
return
|
|
343
|
+
}
|
|
344
|
+
if (method === "tools/call") {
|
|
345
|
+
try {
|
|
346
|
+
const tool = String(msg?.params?.name || "")
|
|
347
|
+
const args = (msg?.params?.arguments && typeof msg.params.arguments === "object")
|
|
348
|
+
? msg.params.arguments
|
|
349
|
+
: {}
|
|
350
|
+
const preparedArgs = await withUploadedLocalFile(serviceUrl, tool, args)
|
|
351
|
+
const payload = {
|
|
352
|
+
...msg,
|
|
353
|
+
params: {
|
|
354
|
+
...(msg.params || {}),
|
|
355
|
+
arguments: preparedArgs,
|
|
356
|
+
},
|
|
357
|
+
}
|
|
358
|
+
const data = await postJson(`${serviceUrl}/mcp`, payload, headers)
|
|
359
|
+
mcpWrite(data)
|
|
360
|
+
} catch (error) {
|
|
361
|
+
mcpWrite({
|
|
362
|
+
jsonrpc: "2.0",
|
|
363
|
+
id,
|
|
364
|
+
error: { code: -32603, message: error instanceof Error ? error.message : String(error) },
|
|
365
|
+
})
|
|
366
|
+
}
|
|
367
|
+
return
|
|
368
|
+
}
|
|
369
|
+
const data = await postJson(`${serviceUrl}/mcp`, msg, headers)
|
|
370
|
+
mcpWrite(data)
|
|
371
|
+
}, (error) => {
|
|
372
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
|
|
373
|
+
})
|
|
374
|
+
}
|
|
375
|
+
|
|
211
376
|
const parseConfigValue = (raw, type = "auto") => {
|
|
212
377
|
if (type === "string") return String(raw)
|
|
213
378
|
if (type === "number") {
|
|
@@ -302,6 +467,7 @@ const usage = () => {
|
|
|
302
467
|
process.stdout.write(`echo-pdf CLI\n\n`)
|
|
303
468
|
process.stdout.write(`Commands:\n`)
|
|
304
469
|
process.stdout.write(` init [--service-url URL]\n`)
|
|
470
|
+
process.stdout.write(` dev [--port 8788] [--host 127.0.0.1]\n`)
|
|
305
471
|
process.stdout.write(` provider set --provider <${PROVIDER_SET_NAMES.join("|")}> --api-key <KEY> [--profile name]\n`)
|
|
306
472
|
process.stdout.write(` provider use --provider <${PROVIDER_ALIASES.join("|")}> [--profile name]\n`)
|
|
307
473
|
process.stdout.write(` provider list [--profile name]\n`)
|
|
@@ -312,13 +478,29 @@ const usage = () => {
|
|
|
312
478
|
process.stdout.write(` model list [--profile name]\n`)
|
|
313
479
|
process.stdout.write(` tools\n`)
|
|
314
480
|
process.stdout.write(` call --tool <name> --args '<json>' [--provider alias] [--model model] [--profile name]\n`)
|
|
481
|
+
process.stdout.write(` file upload <local.pdf>\n`)
|
|
482
|
+
process.stdout.write(` file get --file-id <id> --out <path>\n`)
|
|
315
483
|
process.stdout.write(` mcp initialize\n`)
|
|
316
484
|
process.stdout.write(` mcp tools\n`)
|
|
317
485
|
process.stdout.write(` mcp call --tool <name> --args '<json>'\n`)
|
|
486
|
+
process.stdout.write(` mcp stdio\n`)
|
|
318
487
|
process.stdout.write(` setup add <claude-desktop|claude-code|cursor|cline|windsurf|gemini|json>\n`)
|
|
319
488
|
}
|
|
320
489
|
|
|
321
|
-
const setupSnippet = (tool, serviceUrl) => {
|
|
490
|
+
const setupSnippet = (tool, serviceUrl, mode = "http") => {
|
|
491
|
+
if (mode === "stdio") {
|
|
492
|
+
return {
|
|
493
|
+
mcpServers: {
|
|
494
|
+
"echo-pdf": {
|
|
495
|
+
command: "echo-pdf",
|
|
496
|
+
args: ["mcp", "stdio"],
|
|
497
|
+
env: {
|
|
498
|
+
ECHO_PDF_SERVICE_URL: serviceUrl,
|
|
499
|
+
},
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
}
|
|
503
|
+
}
|
|
322
504
|
const transport = {
|
|
323
505
|
type: "streamable-http",
|
|
324
506
|
url: `${serviceUrl}/mcp`,
|
|
@@ -405,6 +587,14 @@ const main = async () => {
|
|
|
405
587
|
return
|
|
406
588
|
}
|
|
407
589
|
|
|
590
|
+
if (command === "dev") {
|
|
591
|
+
const port = typeof flags.port === "string" ? Number(flags.port) : 8788
|
|
592
|
+
const host = typeof flags.host === "string" ? flags.host : "127.0.0.1"
|
|
593
|
+
if (!Number.isFinite(port) || port <= 0) throw new Error("dev --port must be positive number")
|
|
594
|
+
runDevServer(Math.floor(port), host)
|
|
595
|
+
return
|
|
596
|
+
}
|
|
597
|
+
|
|
408
598
|
if (command === "provider" && subcommand === "set") {
|
|
409
599
|
const providerAlias = resolveProviderAliasInput(flags.provider)
|
|
410
600
|
const apiKey = flags["api-key"]
|
|
@@ -538,15 +728,42 @@ const main = async () => {
|
|
|
538
728
|
const tool = flags.tool
|
|
539
729
|
if (typeof tool !== "string") throw new Error("call requires --tool")
|
|
540
730
|
const args = typeof flags.args === "string" ? JSON.parse(flags.args) : {}
|
|
731
|
+
const preparedArgs = await withUploadedLocalFile(config.serviceUrl, tool, args)
|
|
541
732
|
const provider = resolveProviderAlias(profile, flags.provider)
|
|
542
733
|
const model = typeof flags.model === "string" ? flags.model : resolveDefaultModel(profile, provider)
|
|
543
734
|
const providerApiKeys = buildProviderApiKeys(config, profileName)
|
|
544
|
-
const payload = buildToolCallRequest({ tool, args, provider, model, providerApiKeys })
|
|
735
|
+
const payload = buildToolCallRequest({ tool, args: preparedArgs, provider, model, providerApiKeys })
|
|
545
736
|
const data = await postJson(`${config.serviceUrl}/tools/call`, payload)
|
|
546
737
|
print(data)
|
|
547
738
|
return
|
|
548
739
|
}
|
|
549
740
|
|
|
741
|
+
if (command === "file") {
|
|
742
|
+
const action = rest[0] || ""
|
|
743
|
+
const config = loadConfig()
|
|
744
|
+
if (action === "upload") {
|
|
745
|
+
const filePath = rest[1]
|
|
746
|
+
if (!filePath) throw new Error("file upload requires a path")
|
|
747
|
+
const data = await uploadFile(config.serviceUrl, filePath)
|
|
748
|
+
print({
|
|
749
|
+
fileId: data?.file?.id || "",
|
|
750
|
+
filename: data?.file?.filename || path.basename(filePath),
|
|
751
|
+
sizeBytes: data?.file?.sizeBytes || 0,
|
|
752
|
+
file: data?.file || null,
|
|
753
|
+
})
|
|
754
|
+
return
|
|
755
|
+
}
|
|
756
|
+
if (action === "get") {
|
|
757
|
+
const fileId = typeof flags["file-id"] === "string" ? flags["file-id"] : ""
|
|
758
|
+
const out = typeof flags.out === "string" ? flags.out : ""
|
|
759
|
+
if (!fileId || !out) throw new Error("file get requires --file-id and --out")
|
|
760
|
+
const savedTo = await downloadFile(config.serviceUrl, fileId, out)
|
|
761
|
+
print({ ok: true, fileId, savedTo })
|
|
762
|
+
return
|
|
763
|
+
}
|
|
764
|
+
throw new Error("file command supports: upload|get")
|
|
765
|
+
}
|
|
766
|
+
|
|
550
767
|
if (command === "mcp" && subcommand === "initialize") {
|
|
551
768
|
const config = loadConfig()
|
|
552
769
|
const data = await postJson(`${config.serviceUrl}/mcp`, buildMcpRequest(1, "initialize"), buildMcpHeaders())
|
|
@@ -575,11 +792,18 @@ const main = async () => {
|
|
|
575
792
|
return
|
|
576
793
|
}
|
|
577
794
|
|
|
795
|
+
if (command === "mcp" && subcommand === "stdio") {
|
|
796
|
+
await runMcpStdio()
|
|
797
|
+
return
|
|
798
|
+
}
|
|
799
|
+
|
|
578
800
|
if (command === "setup" && subcommand === "add") {
|
|
579
801
|
const tool = rest[0]
|
|
580
802
|
if (!tool) throw new Error("setup add requires tool name")
|
|
581
803
|
const config = loadConfig()
|
|
582
|
-
|
|
804
|
+
const mode = typeof flags.mode === "string" ? flags.mode : "http"
|
|
805
|
+
if (!["http", "stdio"].includes(mode)) throw new Error("setup add --mode must be http|stdio")
|
|
806
|
+
print(setupSnippet(tool, config.serviceUrl, mode))
|
|
583
807
|
return
|
|
584
808
|
}
|
|
585
809
|
|