@clazic/kordoc 2.4.17 → 2.4.19
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/dist/{auto-detect-2YGFYQCN.js → auto-detect-CBYICI6B.js} +4 -4
- package/dist/{chunk-WM3XI23V.js → chunk-463YQ2WL.js} +38 -25
- package/dist/chunk-463YQ2WL.js.map +1 -0
- package/dist/{chunk-7NOZFYH6.js → chunk-CLK4PNZ7.js} +7 -8
- package/dist/chunk-CLK4PNZ7.js.map +1 -0
- package/dist/{chunk-W2KDIKDF.js → chunk-MZN7PLTZ.js} +2 -2
- package/dist/{chunk-34WIGIQC.js → chunk-Y4WFKJ5P.js} +1 -1
- package/dist/chunk-Y4WFKJ5P.js.map +1 -0
- package/dist/cli.js +9 -13
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +49 -191
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -6
- package/dist/index.d.ts +5 -6
- package/dist/index.js +49 -190
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +5 -6
- package/dist/mcp.js.map +1 -1
- package/dist/{resolve-673XFZQ6.js → resolve-XWYJYKKH.js} +15 -36
- package/dist/resolve-XWYJYKKH.js.map +1 -0
- package/dist/{utils-DHOODYKU.js → utils-YUAT7LFD.js} +2 -2
- package/dist/{watch-RM4VNOL4.js → watch-WEOFVVDO.js} +5 -6
- package/dist/{watch-RM4VNOL4.js.map → watch-WEOFVVDO.js.map} +1 -1
- package/package.json +1 -2
- package/dist/chunk-34WIGIQC.js.map +0 -1
- package/dist/chunk-7FMKAV4P.js +0 -56
- package/dist/chunk-7FMKAV4P.js.map +0 -1
- package/dist/chunk-7NOZFYH6.js.map +0 -1
- package/dist/chunk-WM3XI23V.js.map +0 -1
- package/dist/resolve-673XFZQ6.js.map +0 -1
- package/dist/tesseract-provider-MNMZPSGF.js +0 -11
- package/dist/utils-DHOODYKU.js.map +0 -1
- /package/dist/{auto-detect-2YGFYQCN.js.map → auto-detect-CBYICI6B.js.map} +0 -0
- /package/dist/{chunk-W2KDIKDF.js.map → chunk-MZN7PLTZ.js.map} +0 -0
- /package/dist/{tesseract-provider-MNMZPSGF.js.map → utils-YUAT7LFD.js.map} +0 -0
package/dist/mcp.js
CHANGED
|
@@ -10,17 +10,16 @@ import {
|
|
|
10
10
|
markdownToHwpx,
|
|
11
11
|
markdownToXlsx,
|
|
12
12
|
parse
|
|
13
|
-
} from "./chunk-
|
|
14
|
-
import "./chunk-YW5G6BCJ.js";
|
|
13
|
+
} from "./chunk-463YQ2WL.js";
|
|
15
14
|
import {
|
|
16
15
|
KordocError,
|
|
17
16
|
VERSION,
|
|
18
17
|
sanitizeError,
|
|
19
18
|
toArrayBuffer
|
|
20
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-MZN7PLTZ.js";
|
|
21
20
|
import "./chunk-MOL7MDBG.js";
|
|
22
|
-
import "./chunk-
|
|
23
|
-
import "./chunk-
|
|
21
|
+
import "./chunk-Y4WFKJ5P.js";
|
|
22
|
+
import "./chunk-YW5G6BCJ.js";
|
|
24
23
|
import {
|
|
25
24
|
createLoggerFromEnv,
|
|
26
25
|
generateRunId
|
|
@@ -75,7 +74,7 @@ server.tool(
|
|
|
75
74
|
{
|
|
76
75
|
file_path: z.string().min(1).describe("\uD30C\uC2F1\uD560 \uBB38\uC11C \uD30C\uC77C\uC758 \uC808\uB300 \uACBD\uB85C (HWP, HWPX, PDF, XLSX, DOCX)"),
|
|
77
76
|
image_dir: z.string().optional().describe("\uC774\uBBF8\uC9C0 \uC800\uC7A5 \uD3F4\uB354 \uACBD\uB85C (\uAE30\uBCF8: \uD30C\uC77C\uBA85\uACFC \uAC19\uC740 \uC774\uB984\uC758 \uD3F4\uB354)"),
|
|
78
|
-
ocr: z.enum(["auto", "gemini", "claude", "codex", "ollama", "
|
|
77
|
+
ocr: z.enum(["auto", "gemini", "claude", "codex", "ollama", "off"]).optional().describe("OCR \uBAA8\uB4DC (\uC774\uBBF8\uC9C0 \uAE30\uBC18 PDF\uC6A9): auto, gemini, claude, codex, ollama, off")
|
|
79
78
|
},
|
|
80
79
|
async ({ file_path, image_dir, ocr }) => {
|
|
81
80
|
mcpLogger.log({ level: "info", stage: "detect", event: "start", message: "MCP parse_document \uC2DC\uC791", meta: { file_path } });
|
package/dist/mcp.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/mcp.ts"],"sourcesContent":["/** kordoc MCP 서버 — Claude/Cursor에서 문서 파싱 도구로 사용 */\n\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\"\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\"\nimport { z } from \"zod\"\nimport { readFileSync, realpathSync, openSync, readSync, closeSync, statSync, mkdirSync, writeFileSync, readdirSync, existsSync } from \"fs\"\nimport { resolve, isAbsolute, extname, basename } from \"path\"\nimport { parse, detectFormat, blocksToMarkdown, compare, extractFormFields, markdownToHwpx, markdownToXlsx } from \"./index.js\"\nimport type { ExtractedImage } from \"./types.js\"\nimport { VERSION, toArrayBuffer, sanitizeError, KordocError } from \"./utils.js\"\nimport { createLoggerFromEnv, generateRunId } from \"./logging/logger.js\"\nimport { extractHwp5MetadataOnly } from \"./hwp5/parser.js\"\nimport { extractHwpxMetadataOnly } from \"./hwpx/parser.js\"\nimport { extractPdfMetadataOnly } from \"./pdf/parser.js\"\n\n/** 허용 파일 확장자 */\nconst ALLOWED_EXTENSIONS = new Set([\".hwp\", \".hwpx\", \".pdf\", \".xlsx\", \".docx\"])\n/** 최대 파일 크기 (500MB) */\nconst MAX_FILE_SIZE = 500 * 1024 * 1024\n\n/** 경로 정규화 및 보안 검증 */\nfunction safePath(filePath: string): string {\n if (!filePath) throw new KordocError(\"파일 경로가 비어있습니다\")\n const resolved = resolve(filePath)\n const real = realpathSync(resolved)\n if (!isAbsolute(real)) throw new KordocError(\"절대 경로만 허용됩니다\")\n const ext = extname(real).toLowerCase()\n if (!ALLOWED_EXTENSIONS.has(ext)) throw new KordocError(`지원하지 않는 확장자입니다: ${ext} (허용: ${[...ALLOWED_EXTENSIONS].join(\", \")})`)\n return real\n}\n\n/** 최대 파일 크기 — metadata 전용 (50MB, 전체 파싱보다 보수적) */\nconst MAX_METADATA_FILE_SIZE = 50 * 1024 * 1024\n\n/** 파일 읽기 + 크기 검증 공통 로직 */\nfunction readValidatedFile(filePath: string, maxSize = MAX_FILE_SIZE): { buffer: ArrayBuffer; resolved: string } {\n const resolved = safePath(filePath)\n const fileSize = statSync(resolved).size\n if (fileSize > maxSize) {\n throw new KordocError(`파일이 너무 큽니다: ${(fileSize / 1024 / 1024).toFixed(1)}MB (최대 ${maxSize / 1024 / 1024}MB)`)\n }\n const raw = readFileSync(resolved)\n return { buffer: toArrayBuffer(raw), resolved }\n}\n\n/** 파일 헤더(16바이트)만 읽어 포맷 감지 — 전체 파일 로드 불필요 */\nfunction detectFormatFromHeader(resolved: string): ReturnType<typeof detectFormat> {\n const fd = openSync(resolved, \"r\")\n try {\n const headerBuf = Buffer.alloc(16)\n readSync(fd, headerBuf, 0, 16, 0)\n return detectFormat(toArrayBuffer(headerBuf))\n } finally {\n closeSync(fd)\n }\n}\n\nconst server = new McpServer({\n name: \"kordoc\",\n version: VERSION,\n})\n\nconst mcpLogger = createLoggerFromEnv().withRun(generateRunId(\"mcp\")).child({ component: \"mcp.ts\" })\n\n// ─── 도구: parse_document ────────────────────────────\n\nserver.tool(\n \"parse_document\",\n \"한국 문서 파일(HWP, HWPX, PDF, XLSX, DOCX)을 마크다운으로 변환합니다. 파일 경로를 입력하면 포맷을 자동 감지하여 텍스트를 추출합니다.\",\n {\n file_path: z.string().min(1).describe(\"파싱할 문서 파일의 절대 경로 (HWP, HWPX, PDF, XLSX, DOCX)\"),\n image_dir: z.string().optional().describe(\"이미지 저장 폴더 경로 (기본: 파일명과 같은 이름의 폴더)\"),\n ocr: z.enum([\"auto\", \"gemini\", \"claude\", \"codex\", \"ollama\", \"tesseract\", \"off\"]).optional().describe(\"OCR 모드 (이미지 기반 PDF용): auto, gemini, claude, codex, ollama, tesseract, off\"),\n },\n async ({ file_path, image_dir, ocr }) => {\n mcpLogger.log({ level: \"info\", stage: \"detect\", event: \"start\", message: \"MCP parse_document 시작\", meta: { file_path } })\n try {\n const { buffer, resolved: resolvedFilePath } = readValidatedFile(file_path)\n const format = detectFormat(buffer)\n\n if (format === \"unknown\") {\n return {\n content: [{ type: \"text\", text: `지원하지 않는 파일 형식입니다: ${file_path}` }],\n isError: true,\n }\n }\n\n const result = await parse(buffer, { ocrMode: ocr as import(\"./types.js\").OcrMode | undefined })\n\n if (!result.success) {\n mcpLogger.log({ level: \"error\", stage: \"finalize\", event: \"error\", message: \"MCP parse_document 실패\", meta: { file_path, error: result.error, code: result.code } })\n return {\n content: [{ type: \"text\", text: `파싱 실패 (${result.fileType}): ${result.error}` }],\n isError: true,\n }\n }\n\n const meta = [\n `포맷: ${result.fileType.toUpperCase()}`,\n result.pageCount ? `페이지: ${result.pageCount}` : null,\n result.metadata?.title ? `제목: ${result.metadata.title}` : null,\n result.metadata?.author ? `작성자: ${result.metadata.author}` : null,\n result.isImageBased ? \"이미지 기반 PDF (텍스트 추출 불가)\" : null,\n ].filter(Boolean).join(\" | \")\n\n // outline/warnings 부가 정보 추가\n const parts: string[] = [`[${meta}]`]\n\n if (result.outline && result.outline.length > 0) {\n const outlineText = result.outline.map(o => `${\" \".repeat(o.level - 1)}- ${o.text}`).join(\"\\n\")\n parts.push(`\\n📑 문서 구조:\\n${outlineText}`)\n }\n\n if (result.warnings && result.warnings.length > 0) {\n const warnText = result.warnings.map(w => `- [p${w.page || \"?\"}] ${w.message}`).join(\"\\n\")\n parts.push(`\\n⚠️ 경고:\\n${warnText}`)\n }\n\n // 이미지 저장\n const savedImages: string[] = []\n if (result.images?.length) {\n const defaultDir = resolve(resolvedFilePath, \"..\", basename(resolvedFilePath).replace(/\\.[^.]+$/, \"\"))\n const imgDir = image_dir ? resolve(image_dir) : defaultDir\n mkdirSync(imgDir, { recursive: true })\n for (const img of result.images) {\n const imgPath = resolve(imgDir, img.filename)\n writeFileSync(imgPath, img.data)\n savedImages.push(imgPath)\n }\n parts.push(`\\n💾 저장된 이미지 (${savedImages.length}개):\\n${savedImages.map(p => ` ${p}`).join(\"\\n\")}`)\n }\n\n parts.push(`\\n\\n${result.markdown}`)\n mcpLogger.log({ level: \"info\", stage: \"finalize\", event: \"done\", message: \"MCP parse_document 완료\", meta: { file_path, fileType: result.fileType } })\n\n return {\n content: [{ type: \"text\", text: parts.join(\"\") }],\n }\n } catch (err) {\n mcpLogger.log({ level: \"error\", stage: \"finalize\", event: \"error\", message: \"MCP parse_document 예외\", error: { message: sanitizeError(err), name: err instanceof Error ? err.name : \"Error\", stack: err instanceof Error ? err.stack : undefined } })\n return {\n content: [{ type: \"text\", text: `오류: ${sanitizeError(err)}` }],\n isError: true,\n }\n }\n }\n)\n\n// ─── 도구: convert_document ──────────────────────────\n\nserver.tool(\n \"convert_document\",\n \"마크다운 텍스트를 HWPX 또는 XLSX 파일로 변환하여 저장합니다.\",\n {\n markdown: z.string().min(1).describe(\"변환할 마크다운 텍스트\"),\n output_path: z.string().min(1).describe(\"저장할 파일의 절대 경로 (.hwpx 또는 .xlsx)\"),\n format: z.enum([\"hwpx\", \"xlsx\"]).optional().default(\"hwpx\").describe(\"출력 포맷 (기본: hwpx)\"),\n image_dir: z.string().optional().describe(\"이미지 폴더 경로 (기본: output_path 파일명 폴더)\"),\n template_path: z.string().optional().describe(\"HWPX 템플릿 파일 경로 (hwpx 전용)\"),\n },\n async ({ markdown, output_path, format, image_dir, template_path }) => {\n try {\n const resolvedOutput = resolve(output_path)\n const outputExt = extname(resolvedOutput).toLowerCase()\n if (outputExt !== \".hwpx\" && outputExt !== \".xlsx\") {\n return {\n content: [{ type: \"text\", text: `오류: 출력 파일 확장자는 .hwpx 또는 .xlsx만 허용됩니다: ${outputExt || \"(없음)\"}` }],\n isError: true,\n }\n }\n const warnings: string[] = []\n\n // 이미지 폴더에서 이미지 로드\n const stem = basename(resolvedOutput).replace(/\\.[^.]+$/, \"\")\n const defaultImgDir = resolve(resolvedOutput, \"..\", stem)\n const imgDir = image_dir ? resolve(image_dir) : defaultImgDir\n const images: ExtractedImage[] = []\n\n if (existsSync(imgDir)) {\n const mimeMap: Record<string, string> = {\n png: \"image/png\", jpg: \"image/jpeg\", jpeg: \"image/jpeg\",\n gif: \"image/gif\", bmp: \"image/bmp\",\n }\n for (const entry of readdirSync(imgDir, { withFileTypes: true })) {\n if (!entry.isFile()) continue\n const fname = entry.name\n const ext = extname(fname).slice(1).toLowerCase()\n if (!mimeMap[ext]) continue\n const data = readFileSync(resolve(imgDir, fname))\n images.push({ filename: fname, data: new Uint8Array(data), mimeType: mimeMap[ext] })\n }\n }\n\n let buf: ArrayBuffer\n if (format === \"xlsx\") {\n if (template_path) warnings.push(\"[warn] --template은 hwpx 전용입니다. 무시됩니다.\")\n buf = await markdownToXlsx(markdown, { warnings, images: images.length ? images : undefined })\n } else {\n let templateArrayBuffer: ArrayBuffer | undefined\n if (template_path) {\n const tmpl = readFileSync(safePath(template_path))\n templateArrayBuffer = tmpl.buffer.slice(tmpl.byteOffset, tmpl.byteOffset + tmpl.byteLength)\n }\n buf = await markdownToHwpx(markdown, {\n warnings,\n images: images.length ? images : undefined,\n templateArrayBuffer,\n })\n }\n\n writeFileSync(resolvedOutput, Buffer.from(buf))\n\n const parts = [\n `✅ 변환 완료: ${resolvedOutput}`,\n `포맷: ${format.toUpperCase()}, 크기: ${(buf.byteLength / 1024).toFixed(1)}KB`,\n ]\n if (images.length) parts.push(`포함된 이미지: ${images.length}개 (${imgDir})`)\n if (warnings.length) parts.push(`경고:\\n${warnings.map(w => ` ${w}`).join(\"\\n\")}`)\n\n return { content: [{ type: \"text\", text: parts.join(\"\\n\") }] }\n } catch (err) {\n return {\n content: [{ type: \"text\", text: `오류: ${sanitizeError(err)}` }],\n isError: true,\n }\n }\n }\n)\n\n// ─── 도구: detect_format ─────────────────────────────\n\nserver.tool(\n \"detect_format\",\n \"파일의 포맷을 매직 바이트로 감지합니다 (hwpx, hwp, pdf, unknown).\",\n {\n file_path: z.string().min(1).describe(\"감지할 파일의 절대 경로\"),\n },\n async ({ file_path }) => {\n try {\n const resolved = safePath(file_path)\n const format = detectFormatFromHeader(resolved)\n return {\n content: [{ type: \"text\", text: `${file_path}: ${format}` }],\n }\n } catch (err) {\n return {\n content: [{ type: \"text\", text: `오류: ${sanitizeError(err)}` }],\n isError: true,\n }\n }\n }\n)\n\n// ─── 도구: parse_metadata ────────────────────────────\n\nserver.tool(\n \"parse_metadata\",\n \"문서의 메타데이터(제목, 작성자, 날짜 등)만 빠르게 추출합니다. 전체 파싱 없이 헤더/매니페스트만 읽습니다.\",\n {\n file_path: z.string().min(1).describe(\"메타데이터를 추출할 문서 파일의 절대 경로\"),\n },\n async ({ file_path }) => {\n try {\n const resolved = safePath(file_path)\n const format = detectFormatFromHeader(resolved)\n\n if (format === \"unknown\") {\n return {\n content: [{ type: \"text\", text: `지원하지 않는 파일 형식입니다: ${file_path}` }],\n isError: true,\n }\n }\n\n // metadata 전용 크기 제한 (50MB)\n const { buffer } = readValidatedFile(file_path, MAX_METADATA_FILE_SIZE)\n\n let metadata\n switch (format) {\n case \"hwp\":\n metadata = extractHwp5MetadataOnly(Buffer.from(buffer))\n break\n case \"hwpx\":\n metadata = await extractHwpxMetadataOnly(buffer)\n break\n case \"pdf\":\n metadata = await extractPdfMetadataOnly(buffer)\n break\n }\n\n return {\n content: [{ type: \"text\", text: JSON.stringify({ format, ...metadata }, null, 2) }],\n }\n } catch (err) {\n return {\n content: [{ type: \"text\", text: `오류: ${sanitizeError(err)}` }],\n isError: true,\n }\n }\n }\n)\n\n// ─── 도구: parse_pages ──────────────────────────────\n\nserver.tool(\n \"parse_pages\",\n \"문서의 특정 페이지/섹션 범위만 파싱합니다. PDF는 정확한 페이지, HWP/HWPX는 섹션 단위 근사치입니다.\",\n {\n file_path: z.string().min(1).describe(\"파싱할 문서 파일의 절대 경로\"),\n pages: z.string().min(1).describe(\"페이지 범위 (예: '1-3', '1,3,5-7')\"),\n },\n async ({ file_path, pages }) => {\n try {\n const { buffer } = readValidatedFile(file_path)\n const format = detectFormat(buffer)\n\n if (format === \"unknown\") {\n return {\n content: [{ type: \"text\", text: `지원하지 않는 파일 형식입니다: ${file_path}` }],\n isError: true,\n }\n }\n\n const result = await parse(buffer, { pages })\n\n if (!result.success) {\n return {\n content: [{ type: \"text\", text: `파싱 실패 (${result.fileType}): ${result.error}` }],\n isError: true,\n }\n }\n\n const meta = [\n `포맷: ${result.fileType.toUpperCase()}`,\n `범위: ${pages}`,\n result.pageCount ? `페이지: ${result.pageCount}` : null,\n ].filter(Boolean).join(\" | \")\n\n return {\n content: [{ type: \"text\", text: `[${meta}]\\n\\n${result.markdown}` }],\n }\n } catch (err) {\n return {\n content: [{ type: \"text\", text: `오류: ${sanitizeError(err)}` }],\n isError: true,\n }\n }\n }\n)\n\n// ─── 도구: parse_table ──────────────────────────────\n\nserver.tool(\n \"parse_table\",\n \"문서에서 N번째 테이블만 추출합니다 (0-based index). 테이블이 없거나 인덱스 범위를 초과하면 오류를 반환합니다.\",\n {\n file_path: z.string().min(1).describe(\"파싱할 문서 파일의 절대 경로\"),\n table_index: z.number().int().min(0).describe(\"추출할 테이블 인덱스 (0부터 시작)\"),\n },\n async ({ file_path, table_index }) => {\n try {\n const { buffer } = readValidatedFile(file_path)\n const format = detectFormat(buffer)\n\n if (format === \"unknown\") {\n return {\n content: [{ type: \"text\", text: `지원하지 않는 파일 형식입니다: ${file_path}` }],\n isError: true,\n }\n }\n\n const result = await parse(buffer)\n\n if (!result.success) {\n return {\n content: [{ type: \"text\", text: `파싱 실패 (${result.fileType}): ${result.error}` }],\n isError: true,\n }\n }\n\n const tableBlocks = result.blocks.filter(b => b.type === \"table\" && b.table)\n if (tableBlocks.length === 0) {\n return {\n content: [{ type: \"text\", text: `문서에 테이블이 없습니다.` }],\n isError: true,\n }\n }\n\n if (table_index >= tableBlocks.length) {\n return {\n content: [{ type: \"text\", text: `테이블 인덱스 초과: ${table_index} (총 ${tableBlocks.length}개 테이블)` }],\n isError: true,\n }\n }\n\n const tableBlock = tableBlocks[table_index]\n const tableMarkdown = blocksToMarkdown([tableBlock])\n\n return {\n content: [{ type: \"text\", text: `[테이블 #${table_index} / 총 ${tableBlocks.length}개]\\n\\n${tableMarkdown}` }],\n }\n } catch (err) {\n return {\n content: [{ type: \"text\", text: `오류: ${sanitizeError(err)}` }],\n isError: true,\n }\n }\n }\n)\n\n// ─── 도구: compare_documents ─────────────────────────\n\nserver.tool(\n \"compare_documents\",\n \"두 한국 문서 파일을 비교하여 추가/삭제/변경된 블록을 표시합니다. 신구대조표 생성에 활용됩니다. 크로스 포맷(HWP↔HWPX) 비교 가능.\",\n {\n file_path_a: z.string().min(1).describe(\"비교 원본 문서의 절대 경로\"),\n file_path_b: z.string().min(1).describe(\"비교 대상 문서의 절대 경로\"),\n },\n async ({ file_path_a, file_path_b }) => {\n try {\n const { buffer: bufA } = readValidatedFile(file_path_a)\n const { buffer: bufB } = readValidatedFile(file_path_b)\n\n const result = await compare(bufA, bufB)\n const { stats, diffs } = result\n\n const lines: string[] = [\n `## 문서 비교 결과`,\n `추가: ${stats.added} | 삭제: ${stats.removed} | 변경: ${stats.modified} | 동일: ${stats.unchanged}`,\n \"\",\n ]\n\n for (const d of diffs) {\n const prefix = d.type === \"added\" ? \"+\" : d.type === \"removed\" ? \"-\" : d.type === \"modified\" ? \"~\" : \" \"\n const text = d.after?.text || d.before?.text || (d.after?.table ? \"[테이블]\" : d.before?.table ? \"[테이블]\" : \"\")\n const sim = d.similarity !== undefined ? ` (${(d.similarity * 100).toFixed(0)}%)` : \"\"\n lines.push(`${prefix} ${text.substring(0, 200)}${sim}`)\n }\n\n return {\n content: [{ type: \"text\", text: lines.join(\"\\n\") }],\n }\n } catch (err) {\n return {\n content: [{ type: \"text\", text: `오류: ${sanitizeError(err)}` }],\n isError: true,\n }\n }\n }\n)\n\n// ─── 도구: parse_form ───────────────────────────────\n\nserver.tool(\n \"parse_form\",\n \"한국 서식 문서에서 레이블-값 쌍을 구조화된 JSON으로 추출합니다. 양식/서식 문서에 최적화.\",\n {\n file_path: z.string().min(1).describe(\"서식 문서 파일의 절대 경로\"),\n },\n async ({ file_path }) => {\n try {\n const { buffer } = readValidatedFile(file_path)\n const result = await parse(buffer)\n\n if (!result.success) {\n return {\n content: [{ type: \"text\", text: `파싱 실패: ${result.error}` }],\n isError: true,\n }\n }\n\n const form = extractFormFields(result.blocks)\n return {\n content: [{ type: \"text\", text: JSON.stringify(form, null, 2) }],\n }\n } catch (err) {\n return {\n content: [{ type: \"text\", text: `오류: ${sanitizeError(err)}` }],\n isError: true,\n }\n }\n }\n)\n\n// ─── 서버 시작 ───────────────────────────────────────\n\nasync function main() {\n const transport = new StdioServerTransport()\n await server.connect(transport)\n}\n\nmain().catch((err) => { console.error(err); process.exit(1) })\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/mcp.ts"],"sourcesContent":["/** kordoc MCP 서버 — Claude/Cursor에서 문서 파싱 도구로 사용 */\n\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\"\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\"\nimport { z } from \"zod\"\nimport { readFileSync, realpathSync, openSync, readSync, closeSync, statSync, mkdirSync, writeFileSync, readdirSync, existsSync } from \"fs\"\nimport { resolve, isAbsolute, extname, basename } from \"path\"\nimport { parse, detectFormat, blocksToMarkdown, compare, extractFormFields, markdownToHwpx, markdownToXlsx } from \"./index.js\"\nimport type { ExtractedImage } from \"./types.js\"\nimport { VERSION, toArrayBuffer, sanitizeError, KordocError } from \"./utils.js\"\nimport { createLoggerFromEnv, generateRunId } from \"./logging/logger.js\"\nimport { extractHwp5MetadataOnly } from \"./hwp5/parser.js\"\nimport { extractHwpxMetadataOnly } from \"./hwpx/parser.js\"\nimport { extractPdfMetadataOnly } from \"./pdf/parser.js\"\n\n/** 허용 파일 확장자 */\nconst ALLOWED_EXTENSIONS = new Set([\".hwp\", \".hwpx\", \".pdf\", \".xlsx\", \".docx\"])\n/** 최대 파일 크기 (500MB) */\nconst MAX_FILE_SIZE = 500 * 1024 * 1024\n\n/** 경로 정규화 및 보안 검증 */\nfunction safePath(filePath: string): string {\n if (!filePath) throw new KordocError(\"파일 경로가 비어있습니다\")\n const resolved = resolve(filePath)\n const real = realpathSync(resolved)\n if (!isAbsolute(real)) throw new KordocError(\"절대 경로만 허용됩니다\")\n const ext = extname(real).toLowerCase()\n if (!ALLOWED_EXTENSIONS.has(ext)) throw new KordocError(`지원하지 않는 확장자입니다: ${ext} (허용: ${[...ALLOWED_EXTENSIONS].join(\", \")})`)\n return real\n}\n\n/** 최대 파일 크기 — metadata 전용 (50MB, 전체 파싱보다 보수적) */\nconst MAX_METADATA_FILE_SIZE = 50 * 1024 * 1024\n\n/** 파일 읽기 + 크기 검증 공통 로직 */\nfunction readValidatedFile(filePath: string, maxSize = MAX_FILE_SIZE): { buffer: ArrayBuffer; resolved: string } {\n const resolved = safePath(filePath)\n const fileSize = statSync(resolved).size\n if (fileSize > maxSize) {\n throw new KordocError(`파일이 너무 큽니다: ${(fileSize / 1024 / 1024).toFixed(1)}MB (최대 ${maxSize / 1024 / 1024}MB)`)\n }\n const raw = readFileSync(resolved)\n return { buffer: toArrayBuffer(raw), resolved }\n}\n\n/** 파일 헤더(16바이트)만 읽어 포맷 감지 — 전체 파일 로드 불필요 */\nfunction detectFormatFromHeader(resolved: string): ReturnType<typeof detectFormat> {\n const fd = openSync(resolved, \"r\")\n try {\n const headerBuf = Buffer.alloc(16)\n readSync(fd, headerBuf, 0, 16, 0)\n return detectFormat(toArrayBuffer(headerBuf))\n } finally {\n closeSync(fd)\n }\n}\n\nconst server = new McpServer({\n name: \"kordoc\",\n version: VERSION,\n})\n\nconst mcpLogger = createLoggerFromEnv().withRun(generateRunId(\"mcp\")).child({ component: \"mcp.ts\" })\n\n// ─── 도구: parse_document ────────────────────────────\n\nserver.tool(\n \"parse_document\",\n \"한국 문서 파일(HWP, HWPX, PDF, XLSX, DOCX)을 마크다운으로 변환합니다. 파일 경로를 입력하면 포맷을 자동 감지하여 텍스트를 추출합니다.\",\n {\n file_path: z.string().min(1).describe(\"파싱할 문서 파일의 절대 경로 (HWP, HWPX, PDF, XLSX, DOCX)\"),\n image_dir: z.string().optional().describe(\"이미지 저장 폴더 경로 (기본: 파일명과 같은 이름의 폴더)\"),\n ocr: z.enum([\"auto\", \"gemini\", \"claude\", \"codex\", \"ollama\", \"off\"]).optional().describe(\"OCR 모드 (이미지 기반 PDF용): auto, gemini, claude, codex, ollama, off\"),\n },\n async ({ file_path, image_dir, ocr }) => {\n mcpLogger.log({ level: \"info\", stage: \"detect\", event: \"start\", message: \"MCP parse_document 시작\", meta: { file_path } })\n try {\n const { buffer, resolved: resolvedFilePath } = readValidatedFile(file_path)\n const format = detectFormat(buffer)\n\n if (format === \"unknown\") {\n return {\n content: [{ type: \"text\", text: `지원하지 않는 파일 형식입니다: ${file_path}` }],\n isError: true,\n }\n }\n\n const result = await parse(buffer, { ocrMode: ocr as import(\"./types.js\").OcrMode | undefined })\n\n if (!result.success) {\n mcpLogger.log({ level: \"error\", stage: \"finalize\", event: \"error\", message: \"MCP parse_document 실패\", meta: { file_path, error: result.error, code: result.code } })\n return {\n content: [{ type: \"text\", text: `파싱 실패 (${result.fileType}): ${result.error}` }],\n isError: true,\n }\n }\n\n const meta = [\n `포맷: ${result.fileType.toUpperCase()}`,\n result.pageCount ? `페이지: ${result.pageCount}` : null,\n result.metadata?.title ? `제목: ${result.metadata.title}` : null,\n result.metadata?.author ? `작성자: ${result.metadata.author}` : null,\n result.isImageBased ? \"이미지 기반 PDF (텍스트 추출 불가)\" : null,\n ].filter(Boolean).join(\" | \")\n\n // outline/warnings 부가 정보 추가\n const parts: string[] = [`[${meta}]`]\n\n if (result.outline && result.outline.length > 0) {\n const outlineText = result.outline.map(o => `${\" \".repeat(o.level - 1)}- ${o.text}`).join(\"\\n\")\n parts.push(`\\n📑 문서 구조:\\n${outlineText}`)\n }\n\n if (result.warnings && result.warnings.length > 0) {\n const warnText = result.warnings.map(w => `- [p${w.page || \"?\"}] ${w.message}`).join(\"\\n\")\n parts.push(`\\n⚠️ 경고:\\n${warnText}`)\n }\n\n // 이미지 저장\n const savedImages: string[] = []\n if (result.images?.length) {\n const defaultDir = resolve(resolvedFilePath, \"..\", basename(resolvedFilePath).replace(/\\.[^.]+$/, \"\"))\n const imgDir = image_dir ? resolve(image_dir) : defaultDir\n mkdirSync(imgDir, { recursive: true })\n for (const img of result.images) {\n const imgPath = resolve(imgDir, img.filename)\n writeFileSync(imgPath, img.data)\n savedImages.push(imgPath)\n }\n parts.push(`\\n💾 저장된 이미지 (${savedImages.length}개):\\n${savedImages.map(p => ` ${p}`).join(\"\\n\")}`)\n }\n\n parts.push(`\\n\\n${result.markdown}`)\n mcpLogger.log({ level: \"info\", stage: \"finalize\", event: \"done\", message: \"MCP parse_document 완료\", meta: { file_path, fileType: result.fileType } })\n\n return {\n content: [{ type: \"text\", text: parts.join(\"\") }],\n }\n } catch (err) {\n mcpLogger.log({ level: \"error\", stage: \"finalize\", event: \"error\", message: \"MCP parse_document 예외\", error: { message: sanitizeError(err), name: err instanceof Error ? err.name : \"Error\", stack: err instanceof Error ? err.stack : undefined } })\n return {\n content: [{ type: \"text\", text: `오류: ${sanitizeError(err)}` }],\n isError: true,\n }\n }\n }\n)\n\n// ─── 도구: convert_document ──────────────────────────\n\nserver.tool(\n \"convert_document\",\n \"마크다운 텍스트를 HWPX 또는 XLSX 파일로 변환하여 저장합니다.\",\n {\n markdown: z.string().min(1).describe(\"변환할 마크다운 텍스트\"),\n output_path: z.string().min(1).describe(\"저장할 파일의 절대 경로 (.hwpx 또는 .xlsx)\"),\n format: z.enum([\"hwpx\", \"xlsx\"]).optional().default(\"hwpx\").describe(\"출력 포맷 (기본: hwpx)\"),\n image_dir: z.string().optional().describe(\"이미지 폴더 경로 (기본: output_path 파일명 폴더)\"),\n template_path: z.string().optional().describe(\"HWPX 템플릿 파일 경로 (hwpx 전용)\"),\n },\n async ({ markdown, output_path, format, image_dir, template_path }) => {\n try {\n const resolvedOutput = resolve(output_path)\n const outputExt = extname(resolvedOutput).toLowerCase()\n if (outputExt !== \".hwpx\" && outputExt !== \".xlsx\") {\n return {\n content: [{ type: \"text\", text: `오류: 출력 파일 확장자는 .hwpx 또는 .xlsx만 허용됩니다: ${outputExt || \"(없음)\"}` }],\n isError: true,\n }\n }\n const warnings: string[] = []\n\n // 이미지 폴더에서 이미지 로드\n const stem = basename(resolvedOutput).replace(/\\.[^.]+$/, \"\")\n const defaultImgDir = resolve(resolvedOutput, \"..\", stem)\n const imgDir = image_dir ? resolve(image_dir) : defaultImgDir\n const images: ExtractedImage[] = []\n\n if (existsSync(imgDir)) {\n const mimeMap: Record<string, string> = {\n png: \"image/png\", jpg: \"image/jpeg\", jpeg: \"image/jpeg\",\n gif: \"image/gif\", bmp: \"image/bmp\",\n }\n for (const entry of readdirSync(imgDir, { withFileTypes: true })) {\n if (!entry.isFile()) continue\n const fname = entry.name\n const ext = extname(fname).slice(1).toLowerCase()\n if (!mimeMap[ext]) continue\n const data = readFileSync(resolve(imgDir, fname))\n images.push({ filename: fname, data: new Uint8Array(data), mimeType: mimeMap[ext] })\n }\n }\n\n let buf: ArrayBuffer\n if (format === \"xlsx\") {\n if (template_path) warnings.push(\"[warn] --template은 hwpx 전용입니다. 무시됩니다.\")\n buf = await markdownToXlsx(markdown, { warnings, images: images.length ? images : undefined })\n } else {\n let templateArrayBuffer: ArrayBuffer | undefined\n if (template_path) {\n const tmpl = readFileSync(safePath(template_path))\n templateArrayBuffer = tmpl.buffer.slice(tmpl.byteOffset, tmpl.byteOffset + tmpl.byteLength)\n }\n buf = await markdownToHwpx(markdown, {\n warnings,\n images: images.length ? images : undefined,\n templateArrayBuffer,\n })\n }\n\n writeFileSync(resolvedOutput, Buffer.from(buf))\n\n const parts = [\n `✅ 변환 완료: ${resolvedOutput}`,\n `포맷: ${format.toUpperCase()}, 크기: ${(buf.byteLength / 1024).toFixed(1)}KB`,\n ]\n if (images.length) parts.push(`포함된 이미지: ${images.length}개 (${imgDir})`)\n if (warnings.length) parts.push(`경고:\\n${warnings.map(w => ` ${w}`).join(\"\\n\")}`)\n\n return { content: [{ type: \"text\", text: parts.join(\"\\n\") }] }\n } catch (err) {\n return {\n content: [{ type: \"text\", text: `오류: ${sanitizeError(err)}` }],\n isError: true,\n }\n }\n }\n)\n\n// ─── 도구: detect_format ─────────────────────────────\n\nserver.tool(\n \"detect_format\",\n \"파일의 포맷을 매직 바이트로 감지합니다 (hwpx, hwp, pdf, unknown).\",\n {\n file_path: z.string().min(1).describe(\"감지할 파일의 절대 경로\"),\n },\n async ({ file_path }) => {\n try {\n const resolved = safePath(file_path)\n const format = detectFormatFromHeader(resolved)\n return {\n content: [{ type: \"text\", text: `${file_path}: ${format}` }],\n }\n } catch (err) {\n return {\n content: [{ type: \"text\", text: `오류: ${sanitizeError(err)}` }],\n isError: true,\n }\n }\n }\n)\n\n// ─── 도구: parse_metadata ────────────────────────────\n\nserver.tool(\n \"parse_metadata\",\n \"문서의 메타데이터(제목, 작성자, 날짜 등)만 빠르게 추출합니다. 전체 파싱 없이 헤더/매니페스트만 읽습니다.\",\n {\n file_path: z.string().min(1).describe(\"메타데이터를 추출할 문서 파일의 절대 경로\"),\n },\n async ({ file_path }) => {\n try {\n const resolved = safePath(file_path)\n const format = detectFormatFromHeader(resolved)\n\n if (format === \"unknown\") {\n return {\n content: [{ type: \"text\", text: `지원하지 않는 파일 형식입니다: ${file_path}` }],\n isError: true,\n }\n }\n\n // metadata 전용 크기 제한 (50MB)\n const { buffer } = readValidatedFile(file_path, MAX_METADATA_FILE_SIZE)\n\n let metadata\n switch (format) {\n case \"hwp\":\n metadata = extractHwp5MetadataOnly(Buffer.from(buffer))\n break\n case \"hwpx\":\n metadata = await extractHwpxMetadataOnly(buffer)\n break\n case \"pdf\":\n metadata = await extractPdfMetadataOnly(buffer)\n break\n }\n\n return {\n content: [{ type: \"text\", text: JSON.stringify({ format, ...metadata }, null, 2) }],\n }\n } catch (err) {\n return {\n content: [{ type: \"text\", text: `오류: ${sanitizeError(err)}` }],\n isError: true,\n }\n }\n }\n)\n\n// ─── 도구: parse_pages ──────────────────────────────\n\nserver.tool(\n \"parse_pages\",\n \"문서의 특정 페이지/섹션 범위만 파싱합니다. PDF는 정확한 페이지, HWP/HWPX는 섹션 단위 근사치입니다.\",\n {\n file_path: z.string().min(1).describe(\"파싱할 문서 파일의 절대 경로\"),\n pages: z.string().min(1).describe(\"페이지 범위 (예: '1-3', '1,3,5-7')\"),\n },\n async ({ file_path, pages }) => {\n try {\n const { buffer } = readValidatedFile(file_path)\n const format = detectFormat(buffer)\n\n if (format === \"unknown\") {\n return {\n content: [{ type: \"text\", text: `지원하지 않는 파일 형식입니다: ${file_path}` }],\n isError: true,\n }\n }\n\n const result = await parse(buffer, { pages })\n\n if (!result.success) {\n return {\n content: [{ type: \"text\", text: `파싱 실패 (${result.fileType}): ${result.error}` }],\n isError: true,\n }\n }\n\n const meta = [\n `포맷: ${result.fileType.toUpperCase()}`,\n `범위: ${pages}`,\n result.pageCount ? `페이지: ${result.pageCount}` : null,\n ].filter(Boolean).join(\" | \")\n\n return {\n content: [{ type: \"text\", text: `[${meta}]\\n\\n${result.markdown}` }],\n }\n } catch (err) {\n return {\n content: [{ type: \"text\", text: `오류: ${sanitizeError(err)}` }],\n isError: true,\n }\n }\n }\n)\n\n// ─── 도구: parse_table ──────────────────────────────\n\nserver.tool(\n \"parse_table\",\n \"문서에서 N번째 테이블만 추출합니다 (0-based index). 테이블이 없거나 인덱스 범위를 초과하면 오류를 반환합니다.\",\n {\n file_path: z.string().min(1).describe(\"파싱할 문서 파일의 절대 경로\"),\n table_index: z.number().int().min(0).describe(\"추출할 테이블 인덱스 (0부터 시작)\"),\n },\n async ({ file_path, table_index }) => {\n try {\n const { buffer } = readValidatedFile(file_path)\n const format = detectFormat(buffer)\n\n if (format === \"unknown\") {\n return {\n content: [{ type: \"text\", text: `지원하지 않는 파일 형식입니다: ${file_path}` }],\n isError: true,\n }\n }\n\n const result = await parse(buffer)\n\n if (!result.success) {\n return {\n content: [{ type: \"text\", text: `파싱 실패 (${result.fileType}): ${result.error}` }],\n isError: true,\n }\n }\n\n const tableBlocks = result.blocks.filter(b => b.type === \"table\" && b.table)\n if (tableBlocks.length === 0) {\n return {\n content: [{ type: \"text\", text: `문서에 테이블이 없습니다.` }],\n isError: true,\n }\n }\n\n if (table_index >= tableBlocks.length) {\n return {\n content: [{ type: \"text\", text: `테이블 인덱스 초과: ${table_index} (총 ${tableBlocks.length}개 테이블)` }],\n isError: true,\n }\n }\n\n const tableBlock = tableBlocks[table_index]\n const tableMarkdown = blocksToMarkdown([tableBlock])\n\n return {\n content: [{ type: \"text\", text: `[테이블 #${table_index} / 총 ${tableBlocks.length}개]\\n\\n${tableMarkdown}` }],\n }\n } catch (err) {\n return {\n content: [{ type: \"text\", text: `오류: ${sanitizeError(err)}` }],\n isError: true,\n }\n }\n }\n)\n\n// ─── 도구: compare_documents ─────────────────────────\n\nserver.tool(\n \"compare_documents\",\n \"두 한국 문서 파일을 비교하여 추가/삭제/변경된 블록을 표시합니다. 신구대조표 생성에 활용됩니다. 크로스 포맷(HWP↔HWPX) 비교 가능.\",\n {\n file_path_a: z.string().min(1).describe(\"비교 원본 문서의 절대 경로\"),\n file_path_b: z.string().min(1).describe(\"비교 대상 문서의 절대 경로\"),\n },\n async ({ file_path_a, file_path_b }) => {\n try {\n const { buffer: bufA } = readValidatedFile(file_path_a)\n const { buffer: bufB } = readValidatedFile(file_path_b)\n\n const result = await compare(bufA, bufB)\n const { stats, diffs } = result\n\n const lines: string[] = [\n `## 문서 비교 결과`,\n `추가: ${stats.added} | 삭제: ${stats.removed} | 변경: ${stats.modified} | 동일: ${stats.unchanged}`,\n \"\",\n ]\n\n for (const d of diffs) {\n const prefix = d.type === \"added\" ? \"+\" : d.type === \"removed\" ? \"-\" : d.type === \"modified\" ? \"~\" : \" \"\n const text = d.after?.text || d.before?.text || (d.after?.table ? \"[테이블]\" : d.before?.table ? \"[테이블]\" : \"\")\n const sim = d.similarity !== undefined ? ` (${(d.similarity * 100).toFixed(0)}%)` : \"\"\n lines.push(`${prefix} ${text.substring(0, 200)}${sim}`)\n }\n\n return {\n content: [{ type: \"text\", text: lines.join(\"\\n\") }],\n }\n } catch (err) {\n return {\n content: [{ type: \"text\", text: `오류: ${sanitizeError(err)}` }],\n isError: true,\n }\n }\n }\n)\n\n// ─── 도구: parse_form ───────────────────────────────\n\nserver.tool(\n \"parse_form\",\n \"한국 서식 문서에서 레이블-값 쌍을 구조화된 JSON으로 추출합니다. 양식/서식 문서에 최적화.\",\n {\n file_path: z.string().min(1).describe(\"서식 문서 파일의 절대 경로\"),\n },\n async ({ file_path }) => {\n try {\n const { buffer } = readValidatedFile(file_path)\n const result = await parse(buffer)\n\n if (!result.success) {\n return {\n content: [{ type: \"text\", text: `파싱 실패: ${result.error}` }],\n isError: true,\n }\n }\n\n const form = extractFormFields(result.blocks)\n return {\n content: [{ type: \"text\", text: JSON.stringify(form, null, 2) }],\n }\n } catch (err) {\n return {\n content: [{ type: \"text\", text: `오류: ${sanitizeError(err)}` }],\n isError: true,\n }\n }\n }\n)\n\n// ─── 서버 시작 ───────────────────────────────────────\n\nasync function main() {\n const transport = new StdioServerTransport()\n await server.connect(transport)\n}\n\nmain().catch((err) => { console.error(err); process.exit(1) })\n"],"mappings":"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","names":[]}
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
createCliOcrProvider
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import {
|
|
6
|
-
createLoggerFromEnv
|
|
7
|
-
} from "./chunk-I6YC6ZGK.js";
|
|
4
|
+
} from "./chunk-Y4WFKJ5P.js";
|
|
8
5
|
import {
|
|
9
6
|
detectAvailableOcr,
|
|
10
|
-
getTesseractFallbackMessage,
|
|
11
7
|
validateOcrMode
|
|
12
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-CLK4PNZ7.js";
|
|
9
|
+
import {
|
|
10
|
+
createLoggerFromEnv
|
|
11
|
+
} from "./chunk-I6YC6ZGK.js";
|
|
13
12
|
import "./chunk-ZWE3DS7E.js";
|
|
14
13
|
|
|
15
14
|
// src/ocr/resolve.ts
|
|
@@ -22,15 +21,6 @@ async function resolveOcrProvider(mode, warnings, concurrency, batchSize) {
|
|
|
22
21
|
}
|
|
23
22
|
if (mode !== "auto") {
|
|
24
23
|
validateOcrMode(mode);
|
|
25
|
-
if (mode === "tesseract") {
|
|
26
|
-
const { createTesseractProvider, createTesseractPoolProvider } = await import("./tesseract-provider-MNMZPSGF.js");
|
|
27
|
-
if (concurrency && concurrency > 1) {
|
|
28
|
-
logger.log({ level: "info", event: "done", message: "Tesseract pool provider \uC120\uD0DD", meta: { concurrency } });
|
|
29
|
-
return createTesseractPoolProvider(concurrency);
|
|
30
|
-
}
|
|
31
|
-
logger.log({ level: "info", event: "done", message: "Tesseract single provider \uC120\uD0DD" });
|
|
32
|
-
return createTesseractProvider();
|
|
33
|
-
}
|
|
34
24
|
if (mode === "gemini" || mode === "claude" || mode === "codex") {
|
|
35
25
|
const { createBatchCliProvider, DEFAULT_BATCH_SIZES } = await import("./batch-provider-5BFJRKAZ.js");
|
|
36
26
|
const effectiveBatch = batchSize ?? DEFAULT_BATCH_SIZES[mode];
|
|
@@ -46,27 +36,16 @@ async function resolveOcrProvider(mode, warnings, concurrency, batchSize) {
|
|
|
46
36
|
}
|
|
47
37
|
const detected = detectAvailableOcr();
|
|
48
38
|
logger.log({ level: "info", event: "progress", message: "OCR auto \uAC10\uC9C0 \uACB0\uACFC", meta: { detected } });
|
|
49
|
-
if (detected
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
code: "OCR_CLI_FALLBACK"
|
|
54
|
-
});
|
|
55
|
-
} else {
|
|
56
|
-
warnings?.push({
|
|
57
|
-
message: `OCR: '${detected}' \uC0AC\uC6A9 \uC911 (codex CLI\uAC00 \uC5C6\uC5B4 fallback). \uB354 \uB098\uC740 \uD488\uC9C8\uC744 \uC704\uD574 codex CLI \uC124\uCE58\uB97C \uAD8C\uC7A5\uD569\uB2C8\uB2E4.`,
|
|
58
|
-
code: "OCR_CLI_FALLBACK"
|
|
59
|
-
});
|
|
60
|
-
}
|
|
39
|
+
if (!detected) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
"\uC0AC\uC6A9 \uAC00\uB2A5\uD55C OCR CLI\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. \uB2E4\uC74C \uC911 \uD558\uB098\uB97C \uC124\uCE58\uD558\uC138\uC694:\n Codex CLI: npm install -g @openai/codex\n Claude CLI: npm install -g @anthropic-ai/claude-code\n Gemini CLI: https://ai.google.dev/gemini-api/docs/cli"
|
|
42
|
+
);
|
|
61
43
|
}
|
|
62
|
-
if (detected
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
logger.log({ level: "info", event: "done", message: "AUTO: Tesseract single provider \uC120\uD0DD" });
|
|
69
|
-
return createTesseractProvider();
|
|
44
|
+
if (detected !== "codex") {
|
|
45
|
+
warnings?.push({
|
|
46
|
+
message: `OCR: '${detected}' \uC0AC\uC6A9 \uC911 (codex CLI\uAC00 \uC5C6\uC5B4 fallback). \uB354 \uB098\uC740 \uD488\uC9C8\uC744 \uC704\uD574 codex CLI \uC124\uCE58\uB97C \uAD8C\uC7A5\uD569\uB2C8\uB2E4.`,
|
|
47
|
+
code: "OCR_CLI_FALLBACK"
|
|
48
|
+
});
|
|
70
49
|
}
|
|
71
50
|
if (detected === "gemini" || detected === "codex" || detected === "claude") {
|
|
72
51
|
const { createBatchCliProvider, DEFAULT_BATCH_SIZES } = await import("./batch-provider-5BFJRKAZ.js");
|
|
@@ -84,4 +63,4 @@ async function resolveOcrProvider(mode, warnings, concurrency, batchSize) {
|
|
|
84
63
|
export {
|
|
85
64
|
resolveOcrProvider
|
|
86
65
|
};
|
|
87
|
-
//# sourceMappingURL=resolve-
|
|
66
|
+
//# sourceMappingURL=resolve-XWYJYKKH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/ocr/resolve.ts"],"sourcesContent":["/**\n * OCR 프로바이더 팩토리\n *\n * ocrMode에 따라 적절한 OcrProvider를 생성하여 반환.\n * - \"auto\": 설치된 CLI 자동 탐색 (codex → gemini → claude → ollama)\n * - 특정 CLI: 해당 CLI 사용 (미설치 시 에러)\n * - \"off\": 에러 throw\n */\n\nimport type { OcrMode, OcrProvider, ParseWarning, BatchOcrProvider } from \"../types.js\"\nimport { detectAvailableOcr, validateOcrMode } from \"./auto-detect.js\"\nimport { createCliOcrProvider } from \"./cli-provider.js\"\nimport { createLoggerFromEnv } from \"../logging/logger.js\"\n\n/**\n * ocrMode에 따라 OcrProvider를 생성.\n *\n * @param mode - OCR 모드\n * @param warnings - 경고 수집 배열 (fallback 발생 시 경고 추가)\n * @param concurrency - 병렬 처리 수 (기본: 1=순차)\n * @returns OcrProvider 함수\n * @throws mode=\"off\"이거나 지정 CLI 미설치 시 Error\n */\nexport async function resolveOcrProvider(\n mode: OcrMode,\n warnings?: ParseWarning[],\n concurrency?: number,\n batchSize?: number\n): Promise<OcrProvider | BatchOcrProvider> {\n const logger = createLoggerFromEnv().child({ component: \"ocr/resolve.ts\", stage: \"ocr\" })\n logger.log({ level: \"debug\", event: \"start\", message: \"OCR provider resolve 시작\", meta: { mode, concurrency, batchSize } })\n\n if (mode === \"off\") {\n logger.log({ level: \"warn\", event: \"error\", message: \"OCR 비활성화 모드 요청\" })\n throw new Error(\"OCR이 비활성화되어 있습니다 (--ocr off).\")\n }\n\n // ── 수동 지정 모드 ──────────────────────────────────\n if (mode !== \"auto\") {\n validateOcrMode(mode)\n\n // gemini/claude/codex: 배치 크기 > 1이면 배치 프로바이더 사용\n if (mode === \"gemini\" || mode === \"claude\" || mode === \"codex\") {\n const { createBatchCliProvider, DEFAULT_BATCH_SIZES } = await import(\"./batch-provider.js\")\n const effectiveBatch = batchSize ?? DEFAULT_BATCH_SIZES[mode]\n if (effectiveBatch > 1) {\n logger.log({ level: \"info\", event: \"done\", message: \"Batch CLI provider 선택\", meta: { mode, batchSize: effectiveBatch } })\n return createBatchCliProvider(mode, effectiveBatch)\n }\n logger.log({ level: \"info\", event: \"done\", message: \"CLI provider 선택\", meta: { mode } })\n return createCliOcrProvider(mode)\n }\n\n logger.log({ level: \"info\", event: \"done\", message: \"CLI provider 선택\", meta: { mode } })\n return createCliOcrProvider(mode)\n }\n\n // ── 자동 탐색 모드 ───────────────────────────────────\n const detected = detectAvailableOcr()\n logger.log({ level: \"info\", event: \"progress\", message: \"OCR auto 감지 결과\", meta: { detected } })\n\n if (!detected) {\n throw new Error(\n \"사용 가능한 OCR CLI가 없습니다. 다음 중 하나를 설치하세요:\\n\" +\n \" Codex CLI: npm install -g @openai/codex\\n\" +\n \" Claude CLI: npm install -g @anthropic-ai/claude-code\\n\" +\n \" Gemini CLI: https://ai.google.dev/gemini-api/docs/cli\"\n )\n }\n\n if (detected !== \"codex\") {\n warnings?.push({\n message: `OCR: '${detected}' 사용 중 (codex CLI가 없어 fallback). 더 나은 품질을 위해 codex CLI 설치를 권장합니다.`,\n code: \"OCR_CLI_FALLBACK\",\n })\n }\n\n // gemini/claude/codex: 배치 크기 > 1이면 배치 프로바이더 사용\n if (detected === \"gemini\" || detected === \"codex\" || detected === \"claude\") {\n const { createBatchCliProvider, DEFAULT_BATCH_SIZES } = await import(\"./batch-provider.js\")\n const effectiveBatch = batchSize ?? DEFAULT_BATCH_SIZES[detected]\n if (effectiveBatch > 1) {\n logger.log({ level: \"info\", event: \"done\", message: \"AUTO: Batch CLI provider 선택\", meta: { mode: detected, batchSize: effectiveBatch } })\n return createBatchCliProvider(detected, effectiveBatch)\n }\n logger.log({ level: \"info\", event: \"done\", message: \"AUTO: CLI provider 선택\", meta: { mode: detected } })\n return createCliOcrProvider(detected)\n }\n\n logger.log({ level: \"info\", event: \"done\", message: \"AUTO: CLI provider 선택\", meta: { mode: detected } })\n return createCliOcrProvider(detected)\n}\n"],"mappings":";;;;;;;;;;;;;;AAuBA,eAAsB,mBACpB,MACA,UACA,aACA,WACyC;AACzC,QAAM,SAAS,oBAAoB,EAAE,MAAM,EAAE,WAAW,kBAAkB,OAAO,MAAM,CAAC;AACxF,SAAO,IAAI,EAAE,OAAO,SAAS,OAAO,SAAS,SAAS,qCAA2B,MAAM,EAAE,MAAM,aAAa,UAAU,EAAE,CAAC;AAEzH,MAAI,SAAS,OAAO;AAClB,WAAO,IAAI,EAAE,OAAO,QAAQ,OAAO,SAAS,SAAS,yDAAiB,CAAC;AACvE,UAAM,IAAI,MAAM,sFAA+B;AAAA,EACjD;AAGA,MAAI,SAAS,QAAQ;AACnB,oBAAgB,IAAI;AAGpB,QAAI,SAAS,YAAY,SAAS,YAAY,SAAS,SAAS;AAC9D,YAAM,EAAE,wBAAwB,oBAAoB,IAAI,MAAM,OAAO,8BAAqB;AAC1F,YAAM,iBAAiB,aAAa,oBAAoB,IAAI;AAC5D,UAAI,iBAAiB,GAAG;AACtB,eAAO,IAAI,EAAE,OAAO,QAAQ,OAAO,QAAQ,SAAS,mCAAyB,MAAM,EAAE,MAAM,WAAW,eAAe,EAAE,CAAC;AACxH,eAAO,uBAAuB,MAAM,cAAc;AAAA,MACpD;AACA,aAAO,IAAI,EAAE,OAAO,QAAQ,OAAO,QAAQ,SAAS,6BAAmB,MAAM,EAAE,KAAK,EAAE,CAAC;AACvF,aAAO,qBAAqB,IAAI;AAAA,IAClC;AAEA,WAAO,IAAI,EAAE,OAAO,QAAQ,OAAO,QAAQ,SAAS,6BAAmB,MAAM,EAAE,KAAK,EAAE,CAAC;AACvF,WAAO,qBAAqB,IAAI;AAAA,EAClC;AAGA,QAAM,WAAW,mBAAmB;AACpC,SAAO,IAAI,EAAE,OAAO,QAAQ,OAAO,YAAY,SAAS,sCAAkB,MAAM,EAAE,SAAS,EAAE,CAAC;AAE9F,MAAI,CAAC,UAAU;AACb,UAAM,IAAI;AAAA,MACR;AAAA,IAIF;AAAA,EACF;AAEA,MAAI,aAAa,SAAS;AACxB,cAAU,KAAK;AAAA,MACb,SAAS,SAAS,QAAQ;AAAA,MAC1B,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AAGA,MAAI,aAAa,YAAY,aAAa,WAAW,aAAa,UAAU;AAC1E,UAAM,EAAE,wBAAwB,oBAAoB,IAAI,MAAM,OAAO,8BAAqB;AAC1F,UAAM,iBAAiB,aAAa,oBAAoB,QAAQ;AAChE,QAAI,iBAAiB,GAAG;AACtB,aAAO,IAAI,EAAE,OAAO,QAAQ,OAAO,QAAQ,SAAS,yCAA+B,MAAM,EAAE,MAAM,UAAU,WAAW,eAAe,EAAE,CAAC;AACxI,aAAO,uBAAuB,UAAU,cAAc;AAAA,IACxD;AACA,WAAO,IAAI,EAAE,OAAO,QAAQ,OAAO,QAAQ,SAAS,mCAAyB,MAAM,EAAE,MAAM,SAAS,EAAE,CAAC;AACvG,WAAO,qBAAqB,QAAQ;AAAA,EACtC;AAEA,SAAO,IAAI,EAAE,OAAO,QAAQ,OAAO,QAAQ,SAAS,mCAAyB,MAAM,EAAE,MAAM,SAAS,EAAE,CAAC;AACvG,SAAO,qBAAqB,QAAQ;AACtC;","names":[]}
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
sanitizeError,
|
|
10
10
|
sanitizeHref,
|
|
11
11
|
toArrayBuffer
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-MZN7PLTZ.js";
|
|
13
13
|
import "./chunk-ZWE3DS7E.js";
|
|
14
14
|
export {
|
|
15
15
|
KordocError,
|
|
@@ -22,4 +22,4 @@ export {
|
|
|
22
22
|
sanitizeHref,
|
|
23
23
|
toArrayBuffer
|
|
24
24
|
};
|
|
25
|
-
//# sourceMappingURL=utils-
|
|
25
|
+
//# sourceMappingURL=utils-YUAT7LFD.js.map
|
|
@@ -2,14 +2,13 @@
|
|
|
2
2
|
import {
|
|
3
3
|
detectFormat,
|
|
4
4
|
parse
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import "./chunk-YW5G6BCJ.js";
|
|
5
|
+
} from "./chunk-463YQ2WL.js";
|
|
7
6
|
import {
|
|
8
7
|
toArrayBuffer
|
|
9
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-MZN7PLTZ.js";
|
|
10
9
|
import "./chunk-MOL7MDBG.js";
|
|
11
|
-
import "./chunk-
|
|
12
|
-
import "./chunk-
|
|
10
|
+
import "./chunk-Y4WFKJ5P.js";
|
|
11
|
+
import "./chunk-YW5G6BCJ.js";
|
|
13
12
|
import "./chunk-I6YC6ZGK.js";
|
|
14
13
|
import "./chunk-ZWE3DS7E.js";
|
|
15
14
|
|
|
@@ -137,4 +136,4 @@ async function sendWebhook(url, payload) {
|
|
|
137
136
|
export {
|
|
138
137
|
watchDirectory
|
|
139
138
|
};
|
|
140
|
-
//# sourceMappingURL=watch-
|
|
139
|
+
//# sourceMappingURL=watch-WEOFVVDO.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/watch.ts"],"sourcesContent":["/** 디렉토리 감시 모드 — 새 문서 자동 변환 + Webhook 알림 */\n\nimport { watch, readFileSync, writeFileSync, mkdirSync, statSync, existsSync } from \"fs\"\nimport { basename, resolve, extname, sep } from \"path\"\nimport { parse, detectFormat } from \"./index.js\"\nimport { toArrayBuffer } from \"./utils.js\"\nimport type { WatchOptions } from \"./types.js\"\n\nconst SUPPORTED_EXTENSIONS = new Set([\".hwp\", \".hwpx\", \".pdf\", \".xlsx\", \".docx\"])\nconst DEBOUNCE_MS = 1000\n/** 파일 쓰기 완료 판정: 연속 2회 동일 크기 확인 간격 */\nconst STABLE_CHECK_MS = 300\nconst MAX_FILE_SIZE = 500 * 1024 * 1024\n\n/**\n * 디렉토리를 감시하여 새 문서 파일을 자동 변환.\n *\n * @example\n * ```bash\n * kordoc watch ./incoming -d ./output --webhook https://api.example.com/docs\n * ```\n */\nexport async function watchDirectory(options: WatchOptions): Promise<void> {\n const { dir, outDir, webhook, format = \"markdown\", pages, silent } = options\n\n if (!existsSync(dir)) throw new Error(`디렉토리를 찾을 수 없습니다: ${dir}`)\n if (webhook) validateWebhookUrl(webhook)\n if (outDir) mkdirSync(outDir, { recursive: true })\n\n const log = silent ? () => {} : (msg: string) => process.stderr.write(msg + \"\\n\")\n log(`[kordoc watch] 감시 시작: ${resolve(dir)}`)\n if (outDir) log(`[kordoc watch] 출력: ${resolve(outDir)}`)\n if (webhook) log(`[kordoc watch] 웹훅: ${webhook}`)\n\n // 디바운스 맵\n const pending = new Map<string, ReturnType<typeof setTimeout>>()\n\n /** 파일 크기가 안정화될 때까지 대기 (쓰기 완료 감지) */\n const waitForStableSize = async (absPath: string): Promise<number> => {\n let prevSize = statSync(absPath).size\n await new Promise(r => setTimeout(r, STABLE_CHECK_MS))\n if (!existsSync(absPath)) return 0\n const currSize = statSync(absPath).size\n if (currSize !== prevSize) {\n // 크기가 변했으면 한 번 더 대기\n await new Promise(r => setTimeout(r, STABLE_CHECK_MS))\n if (!existsSync(absPath)) return 0\n return statSync(absPath).size\n }\n return currSize\n }\n\n const processFile = async (filePath: string) => {\n const ext = extname(filePath).toLowerCase()\n if (!SUPPORTED_EXTENSIONS.has(ext)) return\n\n const fileName = basename(filePath)\n try {\n const absPath = resolve(dir, filePath)\n // 경로 순회 방지 — 감시 디렉토리 외부 파일 차단\n const realDir = resolve(dir)\n const dirWithSep = realDir.endsWith(sep) ? realDir : realDir + sep\n if (!absPath.startsWith(dirWithSep) && absPath !== realDir) return\n if (!existsSync(absPath)) return\n\n const fileSize = await waitForStableSize(absPath)\n if (fileSize > MAX_FILE_SIZE || fileSize === 0) return\n\n log(`[kordoc watch] 변환 중: ${fileName}`)\n\n const buffer = readFileSync(absPath)\n const arrayBuffer = toArrayBuffer(buffer)\n const parseOptions = pages ? { pages } : undefined\n const result = await parse(arrayBuffer, parseOptions)\n\n if (!result.success) {\n log(`[kordoc watch] 실패: ${fileName} — ${result.error}`)\n await sendWebhook(webhook, { file: fileName, format: detectFormat(arrayBuffer), success: false, error: result.error })\n return\n }\n\n const output = format === \"json\" ? JSON.stringify(result, null, 2) : result.markdown\n\n if (outDir) {\n const outExt = format === \"json\" ? \".json\" : \".md\"\n const outPath = resolve(outDir, fileName.replace(/\\.[^.]+$/, outExt))\n writeFileSync(outPath, output, \"utf-8\")\n log(`[kordoc watch] 완료: ${fileName} → ${basename(outPath)}`)\n } else {\n process.stdout.write(output + \"\\n\")\n }\n\n await sendWebhook(webhook, {\n file: fileName,\n format: result.fileType,\n success: true,\n markdown: format === \"markdown\" ? output.substring(0, 1000) : undefined,\n })\n } catch (err) {\n log(`[kordoc watch] 에러: ${fileName} — ${err instanceof Error ? err.message : err}`)\n }\n }\n\n const handler = (_event: string | null, filename: string | Buffer | null) => {\n if (!filename) return\n const filePath = filename.toString()\n const existing = pending.get(filePath)\n if (existing) clearTimeout(existing)\n pending.set(filePath, setTimeout(() => {\n pending.delete(filePath)\n processFile(filePath).catch(() => {})\n }, DEBOUNCE_MS))\n }\n\n // fs.watch recursive: Node 18+ macOS/Windows, Node 22+ Linux\n // Linux Node 22 미만은 recursive 미지원 → graceful degradation (최상위 디렉토리만 감시)\n try {\n watch(dir, { recursive: true }, handler)\n } catch {\n process.stderr.write(\"[kordoc watch] 경고: 하위 디렉토리 감시 미지원 (Node.js 22+ 또는 macOS/Windows 필요) — 최상위만 감시\\n\")\n watch(dir, handler)\n }\n\n // 프로세스 종료 방지 (Ctrl+C로 종료)\n return new Promise(() => {})\n}\n\n/** Webhook URL 검증 — SSRF 방지: http/https만 허용, localhost/private IP 차단 */\nfunction validateWebhookUrl(url: string): void {\n let parsed: URL\n try {\n parsed = new URL(url)\n } catch {\n throw new Error(`유효하지 않은 webhook URL: ${url}`)\n }\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n throw new Error(`허용되지 않는 webhook 프로토콜: ${parsed.protocol}`)\n }\n const hostname = parsed.hostname.toLowerCase()\n if (\n hostname === \"localhost\" ||\n hostname === \"[::1]\" ||\n hostname.startsWith(\"127.\") ||\n hostname.startsWith(\"10.\") ||\n hostname.startsWith(\"192.168.\") ||\n /^172\\.(1[6-9]|2\\d|3[01])\\./.test(hostname) ||\n hostname === \"0.0.0.0\" ||\n hostname.startsWith(\"169.254.\") ||\n hostname.endsWith(\".local\") ||\n // IPv6 사설 대역\n hostname.startsWith(\"[fc\") ||\n hostname.startsWith(\"[fd\") ||\n hostname.startsWith(\"[fe80:\") ||\n hostname === \"[::0]\" ||\n hostname === \"[::]\" ||\n // 클라우드 메타데이터 엔드포인트\n hostname === \"metadata.google.internal\" ||\n hostname === \"metadata.google\" ||\n // 16진수/8진수 IP 인코딩 우회 방지\n /^0x[0-9a-f]+$/i.test(hostname) ||\n /^0[0-7]+$/.test(hostname)\n ) {\n throw new Error(`내부 네트워크 대상 webhook은 허용되지 않습니다: ${hostname}`)\n }\n}\n\nasync function sendWebhook(url: string | undefined, payload: Record<string, unknown>): Promise<void> {\n if (!url) return\n try {\n validateWebhookUrl(url)\n await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ ...payload, timestamp: new Date().toISOString() }),\n })\n } catch {\n // webhook 실패는 조용히 무시\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/watch.ts"],"sourcesContent":["/** 디렉토리 감시 모드 — 새 문서 자동 변환 + Webhook 알림 */\n\nimport { watch, readFileSync, writeFileSync, mkdirSync, statSync, existsSync } from \"fs\"\nimport { basename, resolve, extname, sep } from \"path\"\nimport { parse, detectFormat } from \"./index.js\"\nimport { toArrayBuffer } from \"./utils.js\"\nimport type { WatchOptions } from \"./types.js\"\n\nconst SUPPORTED_EXTENSIONS = new Set([\".hwp\", \".hwpx\", \".pdf\", \".xlsx\", \".docx\"])\nconst DEBOUNCE_MS = 1000\n/** 파일 쓰기 완료 판정: 연속 2회 동일 크기 확인 간격 */\nconst STABLE_CHECK_MS = 300\nconst MAX_FILE_SIZE = 500 * 1024 * 1024\n\n/**\n * 디렉토리를 감시하여 새 문서 파일을 자동 변환.\n *\n * @example\n * ```bash\n * kordoc watch ./incoming -d ./output --webhook https://api.example.com/docs\n * ```\n */\nexport async function watchDirectory(options: WatchOptions): Promise<void> {\n const { dir, outDir, webhook, format = \"markdown\", pages, silent } = options\n\n if (!existsSync(dir)) throw new Error(`디렉토리를 찾을 수 없습니다: ${dir}`)\n if (webhook) validateWebhookUrl(webhook)\n if (outDir) mkdirSync(outDir, { recursive: true })\n\n const log = silent ? () => {} : (msg: string) => process.stderr.write(msg + \"\\n\")\n log(`[kordoc watch] 감시 시작: ${resolve(dir)}`)\n if (outDir) log(`[kordoc watch] 출력: ${resolve(outDir)}`)\n if (webhook) log(`[kordoc watch] 웹훅: ${webhook}`)\n\n // 디바운스 맵\n const pending = new Map<string, ReturnType<typeof setTimeout>>()\n\n /** 파일 크기가 안정화될 때까지 대기 (쓰기 완료 감지) */\n const waitForStableSize = async (absPath: string): Promise<number> => {\n let prevSize = statSync(absPath).size\n await new Promise(r => setTimeout(r, STABLE_CHECK_MS))\n if (!existsSync(absPath)) return 0\n const currSize = statSync(absPath).size\n if (currSize !== prevSize) {\n // 크기가 변했으면 한 번 더 대기\n await new Promise(r => setTimeout(r, STABLE_CHECK_MS))\n if (!existsSync(absPath)) return 0\n return statSync(absPath).size\n }\n return currSize\n }\n\n const processFile = async (filePath: string) => {\n const ext = extname(filePath).toLowerCase()\n if (!SUPPORTED_EXTENSIONS.has(ext)) return\n\n const fileName = basename(filePath)\n try {\n const absPath = resolve(dir, filePath)\n // 경로 순회 방지 — 감시 디렉토리 외부 파일 차단\n const realDir = resolve(dir)\n const dirWithSep = realDir.endsWith(sep) ? realDir : realDir + sep\n if (!absPath.startsWith(dirWithSep) && absPath !== realDir) return\n if (!existsSync(absPath)) return\n\n const fileSize = await waitForStableSize(absPath)\n if (fileSize > MAX_FILE_SIZE || fileSize === 0) return\n\n log(`[kordoc watch] 변환 중: ${fileName}`)\n\n const buffer = readFileSync(absPath)\n const arrayBuffer = toArrayBuffer(buffer)\n const parseOptions = pages ? { pages } : undefined\n const result = await parse(arrayBuffer, parseOptions)\n\n if (!result.success) {\n log(`[kordoc watch] 실패: ${fileName} — ${result.error}`)\n await sendWebhook(webhook, { file: fileName, format: detectFormat(arrayBuffer), success: false, error: result.error })\n return\n }\n\n const output = format === \"json\" ? JSON.stringify(result, null, 2) : result.markdown\n\n if (outDir) {\n const outExt = format === \"json\" ? \".json\" : \".md\"\n const outPath = resolve(outDir, fileName.replace(/\\.[^.]+$/, outExt))\n writeFileSync(outPath, output, \"utf-8\")\n log(`[kordoc watch] 완료: ${fileName} → ${basename(outPath)}`)\n } else {\n process.stdout.write(output + \"\\n\")\n }\n\n await sendWebhook(webhook, {\n file: fileName,\n format: result.fileType,\n success: true,\n markdown: format === \"markdown\" ? output.substring(0, 1000) : undefined,\n })\n } catch (err) {\n log(`[kordoc watch] 에러: ${fileName} — ${err instanceof Error ? err.message : err}`)\n }\n }\n\n const handler = (_event: string | null, filename: string | Buffer | null) => {\n if (!filename) return\n const filePath = filename.toString()\n const existing = pending.get(filePath)\n if (existing) clearTimeout(existing)\n pending.set(filePath, setTimeout(() => {\n pending.delete(filePath)\n processFile(filePath).catch(() => {})\n }, DEBOUNCE_MS))\n }\n\n // fs.watch recursive: Node 18+ macOS/Windows, Node 22+ Linux\n // Linux Node 22 미만은 recursive 미지원 → graceful degradation (최상위 디렉토리만 감시)\n try {\n watch(dir, { recursive: true }, handler)\n } catch {\n process.stderr.write(\"[kordoc watch] 경고: 하위 디렉토리 감시 미지원 (Node.js 22+ 또는 macOS/Windows 필요) — 최상위만 감시\\n\")\n watch(dir, handler)\n }\n\n // 프로세스 종료 방지 (Ctrl+C로 종료)\n return new Promise(() => {})\n}\n\n/** Webhook URL 검증 — SSRF 방지: http/https만 허용, localhost/private IP 차단 */\nfunction validateWebhookUrl(url: string): void {\n let parsed: URL\n try {\n parsed = new URL(url)\n } catch {\n throw new Error(`유효하지 않은 webhook URL: ${url}`)\n }\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n throw new Error(`허용되지 않는 webhook 프로토콜: ${parsed.protocol}`)\n }\n const hostname = parsed.hostname.toLowerCase()\n if (\n hostname === \"localhost\" ||\n hostname === \"[::1]\" ||\n hostname.startsWith(\"127.\") ||\n hostname.startsWith(\"10.\") ||\n hostname.startsWith(\"192.168.\") ||\n /^172\\.(1[6-9]|2\\d|3[01])\\./.test(hostname) ||\n hostname === \"0.0.0.0\" ||\n hostname.startsWith(\"169.254.\") ||\n hostname.endsWith(\".local\") ||\n // IPv6 사설 대역\n hostname.startsWith(\"[fc\") ||\n hostname.startsWith(\"[fd\") ||\n hostname.startsWith(\"[fe80:\") ||\n hostname === \"[::0]\" ||\n hostname === \"[::]\" ||\n // 클라우드 메타데이터 엔드포인트\n hostname === \"metadata.google.internal\" ||\n hostname === \"metadata.google\" ||\n // 16진수/8진수 IP 인코딩 우회 방지\n /^0x[0-9a-f]+$/i.test(hostname) ||\n /^0[0-7]+$/.test(hostname)\n ) {\n throw new Error(`내부 네트워크 대상 webhook은 허용되지 않습니다: ${hostname}`)\n }\n}\n\nasync function sendWebhook(url: string | undefined, payload: Record<string, unknown>): Promise<void> {\n if (!url) return\n try {\n validateWebhookUrl(url)\n await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ ...payload, timestamp: new Date().toISOString() }),\n })\n } catch {\n // webhook 실패는 조용히 무시\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;AAEA,SAAS,OAAO,cAAc,eAAe,WAAW,UAAU,kBAAkB;AACpF,SAAS,UAAU,SAAS,SAAS,WAAW;AAKhD,IAAM,uBAAuB,oBAAI,IAAI,CAAC,QAAQ,SAAS,QAAQ,SAAS,OAAO,CAAC;AAChF,IAAM,cAAc;AAEpB,IAAM,kBAAkB;AACxB,IAAM,gBAAgB,MAAM,OAAO;AAUnC,eAAsB,eAAe,SAAsC;AACzE,QAAM,EAAE,KAAK,QAAQ,SAAS,SAAS,YAAY,OAAO,OAAO,IAAI;AAErE,MAAI,CAAC,WAAW,GAAG,EAAG,OAAM,IAAI,MAAM,gFAAoB,GAAG,EAAE;AAC/D,MAAI,QAAS,oBAAmB,OAAO;AACvC,MAAI,OAAQ,WAAU,QAAQ,EAAE,WAAW,KAAK,CAAC;AAEjD,QAAM,MAAM,SAAS,MAAM;AAAA,EAAC,IAAI,CAAC,QAAgB,QAAQ,OAAO,MAAM,MAAM,IAAI;AAChF,MAAI,6CAAyB,QAAQ,GAAG,CAAC,EAAE;AAC3C,MAAI,OAAQ,KAAI,gCAAsB,QAAQ,MAAM,CAAC,EAAE;AACvD,MAAI,QAAS,KAAI,gCAAsB,OAAO,EAAE;AAGhD,QAAM,UAAU,oBAAI,IAA2C;AAG/D,QAAM,oBAAoB,OAAO,YAAqC;AACpE,QAAI,WAAW,SAAS,OAAO,EAAE;AACjC,UAAM,IAAI,QAAQ,OAAK,WAAW,GAAG,eAAe,CAAC;AACrD,QAAI,CAAC,WAAW,OAAO,EAAG,QAAO;AACjC,UAAM,WAAW,SAAS,OAAO,EAAE;AACnC,QAAI,aAAa,UAAU;AAEzB,YAAM,IAAI,QAAQ,OAAK,WAAW,GAAG,eAAe,CAAC;AACrD,UAAI,CAAC,WAAW,OAAO,EAAG,QAAO;AACjC,aAAO,SAAS,OAAO,EAAE;AAAA,IAC3B;AACA,WAAO;AAAA,EACT;AAEA,QAAM,cAAc,OAAO,aAAqB;AAC9C,UAAM,MAAM,QAAQ,QAAQ,EAAE,YAAY;AAC1C,QAAI,CAAC,qBAAqB,IAAI,GAAG,EAAG;AAEpC,UAAM,WAAW,SAAS,QAAQ;AAClC,QAAI;AACF,YAAM,UAAU,QAAQ,KAAK,QAAQ;AAErC,YAAM,UAAU,QAAQ,GAAG;AAC3B,YAAM,aAAa,QAAQ,SAAS,GAAG,IAAI,UAAU,UAAU;AAC/D,UAAI,CAAC,QAAQ,WAAW,UAAU,KAAK,YAAY,QAAS;AAC5D,UAAI,CAAC,WAAW,OAAO,EAAG;AAE1B,YAAM,WAAW,MAAM,kBAAkB,OAAO;AAChD,UAAI,WAAW,iBAAiB,aAAa,EAAG;AAEhD,UAAI,uCAAwB,QAAQ,EAAE;AAEtC,YAAM,SAAS,aAAa,OAAO;AACnC,YAAM,cAAc,cAAc,MAAM;AACxC,YAAM,eAAe,QAAQ,EAAE,MAAM,IAAI;AACzC,YAAM,SAAS,MAAM,MAAM,aAAa,YAAY;AAEpD,UAAI,CAAC,OAAO,SAAS;AACnB,YAAI,gCAAsB,QAAQ,WAAM,OAAO,KAAK,EAAE;AACtD,cAAM,YAAY,SAAS,EAAE,MAAM,UAAU,QAAQ,aAAa,WAAW,GAAG,SAAS,OAAO,OAAO,OAAO,MAAM,CAAC;AACrH;AAAA,MACF;AAEA,YAAM,SAAS,WAAW,SAAS,KAAK,UAAU,QAAQ,MAAM,CAAC,IAAI,OAAO;AAE5E,UAAI,QAAQ;AACV,cAAM,SAAS,WAAW,SAAS,UAAU;AAC7C,cAAM,UAAU,QAAQ,QAAQ,SAAS,QAAQ,YAAY,MAAM,CAAC;AACpE,sBAAc,SAAS,QAAQ,OAAO;AACtC,YAAI,gCAAsB,QAAQ,WAAM,SAAS,OAAO,CAAC,EAAE;AAAA,MAC7D,OAAO;AACL,gBAAQ,OAAO,MAAM,SAAS,IAAI;AAAA,MACpC;AAEA,YAAM,YAAY,SAAS;AAAA,QACzB,MAAM;AAAA,QACN,QAAQ,OAAO;AAAA,QACf,SAAS;AAAA,QACT,UAAU,WAAW,aAAa,OAAO,UAAU,GAAG,GAAI,IAAI;AAAA,MAChE,CAAC;AAAA,IACH,SAAS,KAAK;AACZ,UAAI,gCAAsB,QAAQ,WAAM,eAAe,QAAQ,IAAI,UAAU,GAAG,EAAE;AAAA,IACpF;AAAA,EACF;AAEA,QAAM,UAAU,CAAC,QAAuB,aAAqC;AAC3E,QAAI,CAAC,SAAU;AACf,UAAM,WAAW,SAAS,SAAS;AACnC,UAAM,WAAW,QAAQ,IAAI,QAAQ;AACrC,QAAI,SAAU,cAAa,QAAQ;AACnC,YAAQ,IAAI,UAAU,WAAW,MAAM;AACrC,cAAQ,OAAO,QAAQ;AACvB,kBAAY,QAAQ,EAAE,MAAM,MAAM;AAAA,MAAC,CAAC;AAAA,IACtC,GAAG,WAAW,CAAC;AAAA,EACjB;AAIA,MAAI;AACF,UAAM,KAAK,EAAE,WAAW,KAAK,GAAG,OAAO;AAAA,EACzC,QAAQ;AACN,YAAQ,OAAO,MAAM,yMAAiF;AACtG,UAAM,KAAK,OAAO;AAAA,EACpB;AAGA,SAAO,IAAI,QAAQ,MAAM;AAAA,EAAC,CAAC;AAC7B;AAGA,SAAS,mBAAmB,KAAmB;AAC7C,MAAI;AACJ,MAAI;AACF,aAAS,IAAI,IAAI,GAAG;AAAA,EACtB,QAAQ;AACN,UAAM,IAAI,MAAM,sDAAwB,GAAG,EAAE;AAAA,EAC/C;AACA,MAAI,OAAO,aAAa,WAAW,OAAO,aAAa,UAAU;AAC/D,UAAM,IAAI,MAAM,2EAAyB,OAAO,QAAQ,EAAE;AAAA,EAC5D;AACA,QAAM,WAAW,OAAO,SAAS,YAAY;AAC7C,MACE,aAAa,eACb,aAAa,WACb,SAAS,WAAW,MAAM,KAC1B,SAAS,WAAW,KAAK,KACzB,SAAS,WAAW,UAAU,KAC9B,6BAA6B,KAAK,QAAQ,KAC1C,aAAa,aACb,SAAS,WAAW,UAAU,KAC9B,SAAS,SAAS,QAAQ;AAAA,EAE1B,SAAS,WAAW,KAAK,KACzB,SAAS,WAAW,KAAK,KACzB,SAAS,WAAW,QAAQ,KAC5B,aAAa,WACb,aAAa;AAAA,EAEb,aAAa,8BACb,aAAa;AAAA,EAEb,iBAAiB,KAAK,QAAQ,KAC9B,YAAY,KAAK,QAAQ,GACzB;AACA,UAAM,IAAI,MAAM,uHAAkC,QAAQ,EAAE;AAAA,EAC9D;AACF;AAEA,eAAe,YAAY,KAAyB,SAAiD;AACnG,MAAI,CAAC,IAAK;AACV,MAAI;AACF,uBAAmB,GAAG;AACtB,UAAM,MAAM,KAAK;AAAA,MACf,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,MAC9C,MAAM,KAAK,UAAU,EAAE,GAAG,SAAS,YAAW,oBAAI,KAAK,GAAE,YAAY,EAAE,CAAC;AAAA,IAC1E,CAAC;AAAA,EACH,QAAQ;AAAA,EAER;AACF;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clazic/kordoc",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.19",
|
|
4
4
|
"description": "Parse Korean documents (HWP, HWPX, PDF, XLSX, DOCX) to Markdown",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -64,7 +64,6 @@
|
|
|
64
64
|
"jszip": "^3.10.1",
|
|
65
65
|
"libreoffice-convert": "^1.8.1",
|
|
66
66
|
"pdfjs-dist": "^5.6.205",
|
|
67
|
-
"tesseract.js": "^7.0.0",
|
|
68
67
|
"zod": "^4.3.6"
|
|
69
68
|
},
|
|
70
69
|
"devDependencies": {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/ocr/cli-provider.ts"],"sourcesContent":["/**\n * CLI 기반 OCR 프로바이더\n *\n * gemini / claude / codex / ollama CLI를 subprocess로 호출하여\n * PDF 페이지 이미지를 Markdown으로 변환.\n *\n * 이미지 전달 방식:\n * - gemini: -p \"프롬프트 @이미지경로\" (@ 파일 참조)\n * - claude: -p \"프롬프트 @이미지경로\" (@ 파일 참조, --print 모드)\n * - codex: exec -i 이미지경로 \"프롬프트\" (-i/--image 플래그)\n * - ollama: REST API (localhost:11434) — CLI는 이미지 입력 미지원\n */\n\nimport { spawnSync } from \"child_process\"\nimport { writeFileSync, readFileSync, unlinkSync, mkdirSync } from \"fs\"\nimport { join } from \"path\"\nimport { tmpdir } from \"os\"\nimport type { OcrMode, StructuredOcrResult } from \"../types.js\"\n\n/** OCR 프롬프트 — 모든 CLI 공통 */\nconst OCR_PROMPT = `이 PDF 페이지 이미지에서 텍스트와 테이블을 추출하여 순수 Markdown으로 변환하세요.\n규칙:\n- 테이블은 Markdown 테이블 문법 사용 (| 구분, |---|---| 헤더 구분선 포함)\n- 병합된 셀은 해당 위치에 내용 기재\n- 헤딩은 글자 크기에 따라 ## ~ ###### 사용\n- 리스트는 - 또는 1. 사용\n- 이미지, 도형 등 비텍스트 요소는 무시\n- 원문의 읽기 순서와 구조를 유지\n- \\`\\`\\`로 감싸지 말고 순수 Markdown만 출력`\n\n/** 임시 디렉토리 (프로세스당 1회 생성)\n *\n * gemini CLI는 /tmp/ 등 시스템 임시 디렉토리를 워크스페이스 외부로 간주하여\n * @파일참조 시 접근을 거부할 수 있음. cwd 하위 폴더를 사용하면 모든 CLI에서 접근 가능.\n *\n * ⚠️ .gitignore에 포함된 경로(예: .kordoc-tmp/)는 gemini CLI가 무시하므로\n * 반드시 gitignore되지 않는 이름 사용. 파일은 try/finally로 즉시 정리.\n */\nlet _tempDir: string | null = null\nfunction getTempDir(): string {\n if (!_tempDir) {\n _tempDir = join(process.cwd(), \".kordoc_ocr_tmp\")\n mkdirSync(_tempDir, { recursive: true })\n }\n return _tempDir\n}\n\n/**\n * CLI OcrProvider 생성.\n *\n * @param mode - 사용할 CLI (gemini, claude, codex, ollama)\n * @returns OcrProvider 함수 (StructuredOcrResult 반환)\n */\nexport function createCliOcrProvider(\n mode: Exclude<OcrMode, \"auto\" | \"off\" | \"tesseract\">\n): (pageImage: Uint8Array, pageNumber: number, mimeType: \"image/png\") => Promise<StructuredOcrResult> {\n return async (pageImage: Uint8Array, pageNumber: number): Promise<StructuredOcrResult> => {\n const tempPath = join(getTempDir(), `page-${pageNumber}.png`)\n\n try {\n writeFileSync(tempPath, pageImage)\n\n let output: string\n if (mode === \"ollama\") {\n output = await callOllamaApi(tempPath)\n } else {\n output = callCli(mode, tempPath)\n }\n\n return { markdown: stripCodeFence(output.trim()) }\n } finally {\n try { unlinkSync(tempPath) } catch { /* 임시 파일 정리 실패 무시 */ }\n }\n }\n}\n\n/**\n * 출력 텍스트에서 사용량·속도 제한 에러 감지.\n * 해당 메시지가 포함된 경우 throw하여 다음 엔진으로 fallback 트리거.\n */\nfunction checkForLimitError(output: string, mode: string): void {\n const lower = output.toLowerCase()\n if (lower.includes(\"usage limit\") || lower.includes(\"rate limit\")) {\n throw new Error(`${mode} 사용량/속도 제한: ${output.trim().slice(0, 200)}`)\n }\n}\n\n/**\n * CLI 실행 — gemini / claude / codex\n *\n * @throws CLI 실행 실패 또는 타임아웃(180초) 시 Error\n */\nfunction callCli(mode: string, imagePath: string): string {\n // codex는 --output-last-message로 대화 헤더 없는 깔끔한 출력 사용\n if (mode === \"codex\") {\n return callCodexCli(imagePath)\n }\n\n const args = buildCliArgs(mode, imagePath)\n\n const result = spawnSync(mode, args, {\n encoding: \"utf-8\",\n timeout: 600_000,\n maxBuffer: 10 * 1024 * 1024,\n shell: process.platform === \"win32\",\n // claude: /tmp에서 실행하여 프로젝트 CLAUDE.md의 규칙 간섭 방지\n ...(mode === \"claude\" ? { cwd: tmpdir() } : {}),\n })\n\n if (result.error) {\n throw new Error(`${mode} CLI 실행 실패: ${result.error.message}`)\n }\n if (result.status !== 0) {\n const errMsg = result.stderr?.trim() || `exit code ${result.status}`\n throw new Error(`${mode} OCR 실패: ${errMsg}`)\n }\n\n const output = result.stdout || \"\"\n checkForLimitError(output, mode)\n return output\n}\n\n/**\n * codex exec 실행 — --output-last-message로 대화 헤더 없는 깔끔한 출력.\n * 인자 순서: `codex exec <prompt> --image <file> --output-last-message <outfile>`\n */\nfunction callCodexCli(imagePath: string): string {\n // 출력 파일은 /tmp/ 사용 — codex sandbox는 cwd 내 쓰기를 막을 수 있음\n const outPath = join(tmpdir(), `kordoc-codex-out-${Date.now()}.txt`)\n try {\n const args = [\"exec\", OCR_PROMPT, \"--image\", imagePath, \"--output-last-message\", outPath]\n const model = process.env.KORDOC_CODEX_MODEL\n if (model) args.push(\"--model\", model)\n\n const result = spawnSync(\"codex\", args, {\n encoding: \"utf-8\",\n timeout: 180_000,\n maxBuffer: 10 * 1024 * 1024,\n input: \"\", // stdin EOF 즉시 전달 (대화형 입력 차단)\n shell: process.platform === \"win32\",\n })\n\n if (result.error) {\n throw new Error(`codex CLI 실행 실패: ${result.error.message}`)\n }\n if (result.status !== 0) {\n const errMsg = result.stderr?.trim() || `exit code ${result.status}`\n throw new Error(`codex OCR 실패: ${errMsg}`)\n }\n\n // --output-last-message 파일에서 읽기 (없으면 stdout 폴백)\n let text: string\n try {\n text = readFileSync(outPath, \"utf-8\")\n } catch {\n text = result.stdout || \"\"\n }\n checkForLimitError(text, \"codex\")\n return text\n } finally {\n try { unlinkSync(outPath) } catch { /* 무시 */ }\n }\n}\n\n/**\n * CLI별 인자 배열 생성.\n *\n * gemini: [\"--prompt\", \"프롬프트 @이미지경로\", \"--yolo\"]\n * - -y/--yolo: 자동 승인 (OCR은 도구 사용 없으므로 실질적 영향 없음)\n * - @ 파일 참조로 이미지를 컨텍스트에 포함\n *\n * claude: [\"--print\", \"프롬프트 @이미지경로\"]\n * - --print(-p): 비대화형 출력 모드\n * - @ 파일 참조로 이미지를 컨텍스트에 포함\n *\n * codex: callCodexCli()에서 별도 처리\n * - `codex exec <prompt> --image <file> --output-last-message <outfile>`\n * - 프롬프트가 --image보다 앞에 위치해야 함 (인자 순서 중요)\n *\n * ⚠️ CLI 버전에 따라 문법이 다를 수 있음. 업데이트 시 --help 재확인 필요.\n */\nfunction buildCliArgs(mode: string, imagePath: string): string[] {\n const normalizedPath = imagePath.replace(/\\\\/g, \"/\")\n const promptWithImage = `${OCR_PROMPT}\n\n이미지: @${normalizedPath}`\n\n switch (mode) {\n case \"gemini\": {\n const args = [\"--prompt\", promptWithImage, \"--yolo\"]\n const model = process.env.KORDOC_GEMINI_MODEL\n if (model) args.push(\"--model\", model)\n return args\n }\n\n case \"claude\": {\n const args = [\"--print\", promptWithImage]\n const model = process.env.KORDOC_CLAUDE_MODEL\n if (model) args.push(\"--model\", model)\n return args\n }\n\n default:\n throw new Error(`지원하지 않는 CLI: ${mode}`)\n }\n}\n\n/**\n * Ollama REST API 호출 — CLI는 이미지 입력을 지원하지 않으므로 API 직접 사용.\n *\n * 기본 모델: KORDOC_OLLAMA_MODEL 환경변수 또는 \"gemma4:27b\"\n * 기본 호스트: KORDOC_OLLAMA_HOST 환경변수 또는 \"http://localhost:11434\"\n *\n * @throws Ollama 서버 미실행 또는 응답 오류 시 Error\n */\nasync function callOllamaApi(imagePath: string): Promise<string> {\n const { readFileSync } = await import(\"fs\")\n const imageBase64 = readFileSync(imagePath).toString(\"base64\")\n\n const model = process.env.KORDOC_OLLAMA_MODEL || \"qwen3-vl:8b\"\n const host = process.env.KORDOC_OLLAMA_HOST || \"http://localhost:11434\"\n const timeoutMs = Number(process.env.KORDOC_OLLAMA_TIMEOUT) || 120_000\n\n const response = await fetch(`${host}/api/chat`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n model,\n messages: [{\n role: \"user\",\n content: OCR_PROMPT,\n images: [imageBase64],\n }],\n stream: false,\n }),\n signal: AbortSignal.timeout(timeoutMs),\n })\n\n if (!response.ok) {\n throw new Error(`Ollama API 오류: ${response.status} ${response.statusText}`)\n }\n\n const data = await response.json() as { message?: { content?: string } }\n return data.message?.content || \"\"\n}\n\n/**\n * LLM 출력에서 코드 펜스 제거.\n * LLM이 가끔 결과를 ```markdown ... ``` 으로 감싸는 경우 처리.\n */\nfunction stripCodeFence(text: string): string {\n const match = text.match(/^```(?:markdown|md)?\\s*([\\s\\S]*?)```\\s*$/m)\n return match ? match[1].trim() : text\n}\n"],"mappings":";;;AAaA,SAAS,iBAAiB;AAC1B,SAAS,eAAe,cAAc,YAAY,iBAAiB;AACnE,SAAS,YAAY;AACrB,SAAS,cAAc;AAIvB,IAAM,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkBnB,IAAI,WAA0B;AAC9B,SAAS,aAAqB;AAC5B,MAAI,CAAC,UAAU;AACb,eAAW,KAAK,QAAQ,IAAI,GAAG,iBAAiB;AAChD,cAAU,UAAU,EAAE,WAAW,KAAK,CAAC;AAAA,EACzC;AACA,SAAO;AACT;AAQO,SAAS,qBACd,MACoG;AACpG,SAAO,OAAO,WAAuB,eAAqD;AACxF,UAAM,WAAW,KAAK,WAAW,GAAG,QAAQ,UAAU,MAAM;AAE5D,QAAI;AACF,oBAAc,UAAU,SAAS;AAEjC,UAAI;AACJ,UAAI,SAAS,UAAU;AACrB,iBAAS,MAAM,cAAc,QAAQ;AAAA,MACvC,OAAO;AACL,iBAAS,QAAQ,MAAM,QAAQ;AAAA,MACjC;AAEA,aAAO,EAAE,UAAU,eAAe,OAAO,KAAK,CAAC,EAAE;AAAA,IACnD,UAAE;AACA,UAAI;AAAE,mBAAW,QAAQ;AAAA,MAAE,QAAQ;AAAA,MAAuB;AAAA,IAC5D;AAAA,EACF;AACF;AAMA,SAAS,mBAAmB,QAAgB,MAAoB;AAC9D,QAAM,QAAQ,OAAO,YAAY;AACjC,MAAI,MAAM,SAAS,aAAa,KAAK,MAAM,SAAS,YAAY,GAAG;AACjE,UAAM,IAAI,MAAM,GAAG,IAAI,kDAAe,OAAO,KAAK,EAAE,MAAM,GAAG,GAAG,CAAC,EAAE;AAAA,EACrE;AACF;AAOA,SAAS,QAAQ,MAAc,WAA2B;AAExD,MAAI,SAAS,SAAS;AACpB,WAAO,aAAa,SAAS;AAAA,EAC/B;AAEA,QAAM,OAAO,aAAa,MAAM,SAAS;AAEzC,QAAM,SAAS,UAAU,MAAM,MAAM;AAAA,IACnC,UAAU;AAAA,IACV,SAAS;AAAA,IACT,WAAW,KAAK,OAAO;AAAA,IACvB,OAAO,QAAQ,aAAa;AAAA;AAAA,IAE5B,GAAI,SAAS,WAAW,EAAE,KAAK,OAAO,EAAE,IAAI,CAAC;AAAA,EAC/C,CAAC;AAED,MAAI,OAAO,OAAO;AAChB,UAAM,IAAI,MAAM,GAAG,IAAI,mCAAe,OAAO,MAAM,OAAO,EAAE;AAAA,EAC9D;AACA,MAAI,OAAO,WAAW,GAAG;AACvB,UAAM,SAAS,OAAO,QAAQ,KAAK,KAAK,aAAa,OAAO,MAAM;AAClE,UAAM,IAAI,MAAM,GAAG,IAAI,sBAAY,MAAM,EAAE;AAAA,EAC7C;AAEA,QAAM,SAAS,OAAO,UAAU;AAChC,qBAAmB,QAAQ,IAAI;AAC/B,SAAO;AACT;AAMA,SAAS,aAAa,WAA2B;AAE/C,QAAM,UAAU,KAAK,OAAO,GAAG,oBAAoB,KAAK,IAAI,CAAC,MAAM;AACnE,MAAI;AACF,UAAM,OAAO,CAAC,QAAQ,YAAY,WAAW,WAAW,yBAAyB,OAAO;AACxF,UAAM,QAAQ,QAAQ,IAAI;AAC1B,QAAI,MAAO,MAAK,KAAK,WAAW,KAAK;AAErC,UAAM,SAAS,UAAU,SAAS,MAAM;AAAA,MACtC,UAAU;AAAA,MACV,SAAS;AAAA,MACT,WAAW,KAAK,OAAO;AAAA,MACvB,OAAO;AAAA;AAAA,MACP,OAAO,QAAQ,aAAa;AAAA,IAC9B,CAAC;AAED,QAAI,OAAO,OAAO;AAChB,YAAM,IAAI,MAAM,wCAAoB,OAAO,MAAM,OAAO,EAAE;AAAA,IAC5D;AACA,QAAI,OAAO,WAAW,GAAG;AACvB,YAAM,SAAS,OAAO,QAAQ,KAAK,KAAK,aAAa,OAAO,MAAM;AAClE,YAAM,IAAI,MAAM,2BAAiB,MAAM,EAAE;AAAA,IAC3C;AAGA,QAAI;AACJ,QAAI;AACF,aAAO,aAAa,SAAS,OAAO;AAAA,IACtC,QAAQ;AACN,aAAO,OAAO,UAAU;AAAA,IAC1B;AACA,uBAAmB,MAAM,OAAO;AAChC,WAAO;AAAA,EACT,UAAE;AACA,QAAI;AAAE,iBAAW,OAAO;AAAA,IAAE,QAAQ;AAAA,IAAW;AAAA,EAC/C;AACF;AAmBA,SAAS,aAAa,MAAc,WAA6B;AAC/D,QAAM,iBAAiB,UAAU,QAAQ,OAAO,GAAG;AACnD,QAAM,kBAAkB,GAAG,UAAU;AAAA;AAAA,uBAE/B,cAAc;AAEpB,UAAQ,MAAM;AAAA,IACZ,KAAK,UAAU;AACb,YAAM,OAAO,CAAC,YAAY,iBAAiB,QAAQ;AACnD,YAAM,QAAQ,QAAQ,IAAI;AAC1B,UAAI,MAAO,MAAK,KAAK,WAAW,KAAK;AACrC,aAAO;AAAA,IACT;AAAA,IAEA,KAAK,UAAU;AACb,YAAM,OAAO,CAAC,WAAW,eAAe;AACxC,YAAM,QAAQ,QAAQ,IAAI;AAC1B,UAAI,MAAO,MAAK,KAAK,WAAW,KAAK;AACrC,aAAO;AAAA,IACT;AAAA,IAEA;AACE,YAAM,IAAI,MAAM,8CAAgB,IAAI,EAAE;AAAA,EAC1C;AACF;AAUA,eAAe,cAAc,WAAoC;AAC/D,QAAM,EAAE,cAAAA,cAAa,IAAI,MAAM,OAAO,IAAI;AAC1C,QAAM,cAAcA,cAAa,SAAS,EAAE,SAAS,QAAQ;AAE7D,QAAM,QAAQ,QAAQ,IAAI,uBAAuB;AACjD,QAAM,OAAO,QAAQ,IAAI,sBAAsB;AAC/C,QAAM,YAAY,OAAO,QAAQ,IAAI,qBAAqB,KAAK;AAE/D,QAAM,WAAW,MAAM,MAAM,GAAG,IAAI,aAAa;AAAA,IAC/C,QAAQ;AAAA,IACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,IAC9C,MAAM,KAAK,UAAU;AAAA,MACnB;AAAA,MACA,UAAU,CAAC;AAAA,QACT,MAAM;AAAA,QACN,SAAS;AAAA,QACT,QAAQ,CAAC,WAAW;AAAA,MACtB,CAAC;AAAA,MACD,QAAQ;AAAA,IACV,CAAC;AAAA,IACD,QAAQ,YAAY,QAAQ,SAAS;AAAA,EACvC,CAAC;AAED,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI,MAAM,4BAAkB,SAAS,MAAM,IAAI,SAAS,UAAU,EAAE;AAAA,EAC5E;AAEA,QAAM,OAAO,MAAM,SAAS,KAAK;AACjC,SAAO,KAAK,SAAS,WAAW;AAClC;AAMA,SAAS,eAAe,MAAsB;AAC5C,QAAM,QAAQ,KAAK,MAAM,2CAA2C;AACpE,SAAO,QAAQ,MAAM,CAAC,EAAE,KAAK,IAAI;AACnC;","names":["readFileSync"]}
|
package/dist/chunk-7FMKAV4P.js
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// src/ocr/tesseract-provider.ts
|
|
4
|
-
import { createWorker } from "tesseract.js";
|
|
5
|
-
async function createTesseractProvider() {
|
|
6
|
-
const worker = await createWorker("kor+eng");
|
|
7
|
-
let terminated = false;
|
|
8
|
-
const provider = async (pageImage, _pageNumber, _mimeType) => {
|
|
9
|
-
const { data } = await worker.recognize(pageImage);
|
|
10
|
-
return data.text;
|
|
11
|
-
};
|
|
12
|
-
provider.terminate = async () => {
|
|
13
|
-
if (!terminated) {
|
|
14
|
-
await worker.terminate();
|
|
15
|
-
terminated = true;
|
|
16
|
-
}
|
|
17
|
-
};
|
|
18
|
-
return provider;
|
|
19
|
-
}
|
|
20
|
-
async function createTesseractPoolProvider(concurrency) {
|
|
21
|
-
const workers = await Promise.all(
|
|
22
|
-
Array.from({ length: concurrency }, () => createWorker("kor+eng"))
|
|
23
|
-
);
|
|
24
|
-
const idle = [...workers];
|
|
25
|
-
const waitQueue = [];
|
|
26
|
-
function acquire() {
|
|
27
|
-
if (idle.length > 0) return Promise.resolve(idle.pop());
|
|
28
|
-
return new Promise((resolve) => waitQueue.push(resolve));
|
|
29
|
-
}
|
|
30
|
-
function release(w) {
|
|
31
|
-
if (waitQueue.length > 0) {
|
|
32
|
-
waitQueue.shift()(w);
|
|
33
|
-
} else {
|
|
34
|
-
idle.push(w);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
const provider = async (pageImage, _pageNumber, _mimeType) => {
|
|
38
|
-
const w = await acquire();
|
|
39
|
-
try {
|
|
40
|
-
const { data } = await w.recognize(pageImage);
|
|
41
|
-
return data.text;
|
|
42
|
-
} finally {
|
|
43
|
-
release(w);
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
|
-
provider.terminate = async () => {
|
|
47
|
-
await Promise.all(workers.map((w) => w.terminate()));
|
|
48
|
-
};
|
|
49
|
-
return provider;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export {
|
|
53
|
-
createTesseractProvider,
|
|
54
|
-
createTesseractPoolProvider
|
|
55
|
-
};
|
|
56
|
-
//# sourceMappingURL=chunk-7FMKAV4P.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/ocr/tesseract-provider.ts"],"sourcesContent":["/**\n * Tesseract.js 기반 OCR 프로바이더\n *\n * tesseract.js는 bundled 의존성으로 별도 설치 불필요.\n * Vision LLM CLI가 없을 때의 최후 fallback으로 자동 사용.\n *\n * 특성:\n * - 순수 텍스트만 반환 (테이블/헤딩 구조 복원 불가)\n * - 한글 인식률 약 85-90% (깨끗한 이미지 기준)\n * - 완전 오프라인 동작 (API 키 불필요)\n */\n\nimport { createWorker } from \"tesseract.js\"\nimport type { OcrProvider } from \"../types.js\"\n\n/**\n * Tesseract.js OcrProvider 생성.\n *\n * 워커를 1회 생성하여 재사용 (매 페이지마다 초기화 방지).\n * 문서 처리 완료 후 terminate()로 워커 정리.\n *\n * @returns OcrProvider 함수 (+ terminate 메서드)\n */\nexport async function createTesseractProvider(): Promise<OcrProvider & { terminate: () => Promise<void> }> {\n // kor+eng: 한글 + 영문 동시 인식 (한국 공문서 특성)\n const worker = await createWorker(\"kor+eng\")\n let terminated = false\n\n const provider = async (\n pageImage: Uint8Array,\n _pageNumber: number,\n _mimeType: \"image/png\"\n ): Promise<string> => {\n const { data } = await worker.recognize(pageImage)\n return data.text\n }\n\n ;(provider as OcrProvider & { terminate: () => Promise<void> }).terminate = async () => {\n if (!terminated) {\n await worker.terminate()\n terminated = true\n }\n }\n\n return provider as OcrProvider & { terminate: () => Promise<void> }\n}\n\n/**\n * Tesseract.js 워커 풀 기반 병렬 OcrProvider 생성.\n *\n * N개 워커를 동시 생성하여 여러 페이지를 병렬로 OCR 처리.\n * acquire/release 패턴으로 동시 실행 안전성 보장.\n * 대량 이미지 기반 PDF(예: 300페이지) 처리 속도 향상에 효과적.\n *\n * @param concurrency - 동시 워커 수 (권장: CPU 코어 수)\n * @returns OcrProvider 함수 (+ terminate 메서드)\n */\nexport async function createTesseractPoolProvider(\n concurrency: number\n): Promise<OcrProvider & { terminate: () => Promise<void> }> {\n // N개 워커를 동시 초기화 (Promise.all로 병렬 생성)\n const workers = await Promise.all(\n Array.from({ length: concurrency }, () => createWorker(\"kor+eng\"))\n )\n\n // 유휴 워커 큐 (초기에는 모든 워커가 유휴 상태)\n const idle: typeof workers = [...workers]\n // 워커를 기다리는 콜백 큐\n const waitQueue: ((w: (typeof workers)[0]) => void)[] = []\n\n /** 유휴 워커를 획득 (없으면 대기) */\n function acquire(): Promise<(typeof workers)[0]> {\n if (idle.length > 0) return Promise.resolve(idle.pop()!)\n return new Promise(resolve => waitQueue.push(resolve))\n }\n\n /** 사용 완료된 워커를 반환 (대기 중인 호출자에게 우선 전달) */\n function release(w: (typeof workers)[0]): void {\n if (waitQueue.length > 0) {\n // 대기 중인 요청자에게 즉시 전달\n waitQueue.shift()!(w)\n } else {\n idle.push(w)\n }\n }\n\n // 병렬 호출 안전 프로바이더 — 각 호출이 acquire/release로 워커를 독점 사용\n const provider = async (\n pageImage: Uint8Array,\n _pageNumber: number,\n _mimeType: \"image/png\"\n ): Promise<string> => {\n const w = await acquire()\n try {\n const { data } = await w.recognize(pageImage)\n return data.text\n } finally {\n // 성공/실패 무관하게 워커 반환\n release(w)\n }\n }\n\n // 모든 워커 정리 메서드\n ;(provider as OcrProvider & { terminate: () => Promise<void> }).terminate = async () => {\n await Promise.all(workers.map(w => w.terminate()))\n }\n\n return provider as OcrProvider & { terminate: () => Promise<void> }\n}\n"],"mappings":";;;AAYA,SAAS,oBAAoB;AAW7B,eAAsB,0BAAqF;AAEzG,QAAM,SAAS,MAAM,aAAa,SAAS;AAC3C,MAAI,aAAa;AAEjB,QAAM,WAAW,OACf,WACA,aACA,cACoB;AACpB,UAAM,EAAE,KAAK,IAAI,MAAM,OAAO,UAAU,SAAS;AACjD,WAAO,KAAK;AAAA,EACd;AAEC,EAAC,SAA8D,YAAY,YAAY;AACtF,QAAI,CAAC,YAAY;AACf,YAAM,OAAO,UAAU;AACvB,mBAAa;AAAA,IACf;AAAA,EACF;AAEA,SAAO;AACT;AAYA,eAAsB,4BACpB,aAC2D;AAE3D,QAAM,UAAU,MAAM,QAAQ;AAAA,IAC5B,MAAM,KAAK,EAAE,QAAQ,YAAY,GAAG,MAAM,aAAa,SAAS,CAAC;AAAA,EACnE;AAGA,QAAM,OAAuB,CAAC,GAAG,OAAO;AAExC,QAAM,YAAkD,CAAC;AAGzD,WAAS,UAAwC;AAC/C,QAAI,KAAK,SAAS,EAAG,QAAO,QAAQ,QAAQ,KAAK,IAAI,CAAE;AACvD,WAAO,IAAI,QAAQ,aAAW,UAAU,KAAK,OAAO,CAAC;AAAA,EACvD;AAGA,WAAS,QAAQ,GAA8B;AAC7C,QAAI,UAAU,SAAS,GAAG;AAExB,gBAAU,MAAM,EAAG,CAAC;AAAA,IACtB,OAAO;AACL,WAAK,KAAK,CAAC;AAAA,IACb;AAAA,EACF;AAGA,QAAM,WAAW,OACf,WACA,aACA,cACoB;AACpB,UAAM,IAAI,MAAM,QAAQ;AACxB,QAAI;AACF,YAAM,EAAE,KAAK,IAAI,MAAM,EAAE,UAAU,SAAS;AAC5C,aAAO,KAAK;AAAA,IACd,UAAE;AAEA,cAAQ,CAAC;AAAA,IACX;AAAA,EACF;AAGC,EAAC,SAA8D,YAAY,YAAY;AACtF,UAAM,QAAQ,IAAI,QAAQ,IAAI,OAAK,EAAE,UAAU,CAAC,CAAC;AAAA,EACnD;AAEA,SAAO;AACT;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/ocr/auto-detect.ts"],"sourcesContent":["/**\n * OCR CLI 자동 탐색\n *\n * 탐색 순서: codex → gemini → claude → ollama → tesseract.js\n * CLI는 which(unix) / where(win) 명령어로 PATH 존재 확인.\n * tesseract.js는 bundled 의존성이므로 항상 사용 가능 (최후 fallback).\n */\n\nimport { execSync } from \"child_process\"\nimport type { OcrMode } from \"../types.js\"\n\n/** CLI 탐색 우선순위 */\nconst CLI_PRIORITY = [\"codex\", \"gemini\", \"claude\", \"ollama\"] as const\n\n/**\n * 시스템에 설치된 OCR 도구를 우선순위대로 탐색.\n * tesseract.js는 bundled 의존성이므로 CLI를 찾지 못해도 항상 \"tesseract\" 반환.\n * @returns 사용 가능한 OcrMode (null 반환 없음)\n */\nexport function detectAvailableOcr(): OcrMode {\n // 1. CLI 프로그램 탐색 (codex → gemini → claude → ollama)\n for (const cli of CLI_PRIORITY) {\n if (isCliInstalled(cli)) return cli\n }\n\n // 2. tesseract.js — bundled 의존성, 항상 사용 가능\n return \"tesseract\"\n}\n\n/**\n * 특정 CLI가 시스템 PATH에 있는지 확인.\n * which(unix) 또는 where(win32) 사용.\n */\nfunction isCliInstalled(name: string): boolean {\n try {\n const cmd = process.platform === \"win32\" ? \"where\" : \"which\"\n execSync(`${cmd} ${name}`, { stdio: \"ignore\", timeout: 3000 })\n return true\n } catch {\n return false\n }\n}\n\n/**\n * auto 모드에서 시도할 fallback 체인 반환.\n * 설치된 CLI만 포함하며, tesseract는 항상 마지막에 추가.\n */\nexport function getAutoFallbackChain(): OcrMode[] {\n const chain: OcrMode[] = []\n for (const cli of CLI_PRIORITY) {\n if (isCliInstalled(cli)) chain.push(cli)\n }\n chain.push(\"tesseract\")\n return chain\n}\n\n/**\n * 수동 지정된 OcrMode 유효성 검증.\n * --ocr gemini 등 강제 지정 시 호출.\n * @throws 해당 CLI가 설치되지 않은 경우 Error (tesseract는 항상 통과)\n */\nexport function validateOcrMode(mode: OcrMode): void {\n if (mode === \"auto\" || mode === \"off\" || mode === \"tesseract\") return\n\n if (!isCliInstalled(mode)) {\n throw new Error(`'${mode}' CLI가 설치되지 않았습니다.\\n${getInstallGuide(mode)}`)\n }\n}\n\n/** CLI별 설치 안내 메시지 */\nfunction getInstallGuide(mode: string): string {\n const guides: Record<string, string> = {\n gemini: \"설치: https://ai.google.dev/gemini-api/docs/cli\",\n claude: \"설치: npm install -g @anthropic-ai/claude-code 또는 https://claude.ai/code\",\n codex: \"설치: npm install -g @openai/codex 또는 https://github.com/openai/codex\",\n ollama: \"설치: brew install ollama 또는 https://ollama.com/download\",\n }\n return guides[mode] || `'${mode}'을(를) 설치해주세요.`\n}\n\n/**\n * AI CLI가 없어 tesseract.js로 fallback할 때 표시할 안내 메시지.\n */\nexport function getTesseractFallbackMessage(): string {\n return [\n \"설치된 AI CLI가 없어 내장 tesseract.js로 OCR을 수행합니다.\",\n \"더 나은 품질(테이블/헤딩 구조 보존)을 위해 AI CLI 설치를 권장합니다:\",\n \"\",\n \" [권장] Codex CLI: npm install -g @openai/codex\",\n \" Gemini CLI: https://ai.google.dev/gemini-api/docs/cli\",\n \" Claude CLI: npm install -g @anthropic-ai/claude-code\",\n \" Ollama: brew install ollama (+ ollama pull gemma4:27b)\",\n ].join(\"\\n\")\n}\n"],"mappings":";;;AAQA,SAAS,gBAAgB;AAIzB,IAAM,eAAe,CAAC,SAAS,UAAU,UAAU,QAAQ;AAOpD,SAAS,qBAA8B;AAE5C,aAAW,OAAO,cAAc;AAC9B,QAAI,eAAe,GAAG,EAAG,QAAO;AAAA,EAClC;AAGA,SAAO;AACT;AAMA,SAAS,eAAe,MAAuB;AAC7C,MAAI;AACF,UAAM,MAAM,QAAQ,aAAa,UAAU,UAAU;AACrD,aAAS,GAAG,GAAG,IAAI,IAAI,IAAI,EAAE,OAAO,UAAU,SAAS,IAAK,CAAC;AAC7D,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMO,SAAS,uBAAkC;AAChD,QAAM,QAAmB,CAAC;AAC1B,aAAW,OAAO,cAAc;AAC9B,QAAI,eAAe,GAAG,EAAG,OAAM,KAAK,GAAG;AAAA,EACzC;AACA,QAAM,KAAK,WAAW;AACtB,SAAO;AACT;AAOO,SAAS,gBAAgB,MAAqB;AACnD,MAAI,SAAS,UAAU,SAAS,SAAS,SAAS,YAAa;AAE/D,MAAI,CAAC,eAAe,IAAI,GAAG;AACzB,UAAM,IAAI,MAAM,IAAI,IAAI;AAAA,EAAuB,gBAAgB,IAAI,CAAC,EAAE;AAAA,EACxE;AACF;AAGA,SAAS,gBAAgB,MAAsB;AAC7C,QAAM,SAAiC;AAAA,IACrC,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,OAAQ;AAAA,IACR,QAAQ;AAAA,EACV;AACA,SAAO,OAAO,IAAI,KAAK,IAAI,IAAI;AACjC;AAKO,SAAS,8BAAsC;AACpD,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,EAAE,KAAK,IAAI;AACb;","names":[]}
|