markpdfdown 0.1.3 → 0.1.6

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.
@@ -0,0 +1,140 @@
1
+ # MarkPDFdown
2
+
3
+ [English](./README.md) | [中文](./README.zh-CN.md) | [日本語](./README.ja.md) | [Русский](./README.ru.md) | [العربية](./README.ar.md) | [فارسی](./README.fa.md)
4
+
5
+ 一款使用大语言模型(LLM)视觉识别功能将 PDF 文档转换为 Markdown 格式的桌面应用程序。
6
+
7
+ ## 功能特性
8
+
9
+ - **多 LLM 支持**:OpenAI、Anthropic Claude、Google Gemini、Ollama(本地模型)和 OpenAI Responses API
10
+ - **高质量转换**:利用 LLM 视觉能力实现精准的 PDF 到 Markdown 转换
11
+ - **并排预览**:同时查看原始 PDF 页面和生成的 Markdown
12
+ - **数学公式和代码支持**:完整支持 LaTeX 公式(KaTeX)和语法高亮代码块
13
+ - **多语言界面**:英语、中文、日语、俄语、阿拉伯语和波斯语
14
+ - **并行处理**:可配置工作线程数以加快转换速度
15
+ - **进度追踪**:实时状态更新和单页重试支持
16
+ - **本地存储**:使用 SQLite 数据库持久化任务
17
+
18
+ ## 截图
19
+
20
+ <img width="1264" height="848" alt="1769311168213_download" src="https://github.com/user-attachments/assets/15b5a801-6729-492a-a979-1fc4dba6853a" />
21
+
22
+ ## 安装
23
+
24
+ ### 快速开始(推荐)
25
+
26
+ 使用 npx 直接运行(需要 Node.js 18+):
27
+
28
+ ```bash
29
+ npx -y markpdfdown
30
+ ```
31
+
32
+ ### 下载安装包
33
+
34
+ 从 [Releases](https://github.com/MarkPDFdown/markpdfdown-desktop/releases) 页面下载适合您平台的最新版本:
35
+
36
+ - **Windows**:`MarkPDFdown-{version}-x64.exe`
37
+ - **macOS**:`MarkPDFdown-{version}-arm64.dmg` / `MarkPDFdown-{version}-x64.dmg`
38
+ - **Linux**:`MarkPDFdown-{version}-x86_64.AppImage`
39
+
40
+ ## 使用方法
41
+
42
+ 1. **配置提供商**:进入设置,添加您的 LLM 提供商凭据(API 密钥、基础 URL)
43
+ 2. **添加模型**:配置您想要用于转换的模型
44
+ 3. **上传 PDF**:拖放或点击选择 PDF 文件
45
+ 4. **选择模型**:选择用于转换的 LLM 模型
46
+ 5. **转换**:开始转换过程
47
+ 6. **预览**:逐页查看结果,支持并排对比
48
+ 7. **下载**:导出合并后的 Markdown 文件
49
+
50
+ ## 开发
51
+
52
+ ### 前置要求
53
+
54
+ - Node.js 18+
55
+ - npm 8+
56
+
57
+ ### 设置
58
+
59
+ ```bash
60
+ # 安装依赖
61
+ npm install
62
+
63
+ # 生成 Prisma 客户端
64
+ npm run generate
65
+
66
+ # 运行数据库迁移
67
+ npm run migrate:dev
68
+
69
+ # 启动开发服务器
70
+ npm run dev
71
+ ```
72
+
73
+ ### 构建
74
+
75
+ ```bash
76
+ # 生产构建
77
+ npm run build
78
+
79
+ # 平台特定安装包
80
+ npm run build:win # Windows NSIS 安装程序
81
+ npm run build:mac # macOS DMG
82
+ npm run build:linux # Linux AppImage
83
+ ```
84
+
85
+ ### 测试
86
+
87
+ ```bash
88
+ npm test # 运行所有测试
89
+ npm run test:unit # 仅单元测试
90
+ npm run test:renderer # 仅组件测试
91
+ npm run test:coverage # 生成覆盖率报告
92
+ ```
93
+
94
+ ### 项目结构
95
+
96
+ ```
97
+ src/
98
+ ├── main/ # Electron 主进程
99
+ │ ├── index.ts # 应用入口、窗口创建、IPC 设置
100
+ │ └── ipc/ # IPC 处理器
101
+ ├── preload/ # 预加载脚本(window.api)
102
+ ├── renderer/ # React 前端
103
+ │ ├── components/ # UI 组件
104
+ │ ├── pages/ # 路由页面
105
+ │ └── locales/ # 国际化翻译
106
+ ├── core/ # 业务逻辑(整洁架构)
107
+ │ ├── infrastructure/ # 数据库、外部服务
108
+ │ ├── application/ # 工作线程、编排
109
+ │ ├── domain/ # 接口、领域类型
110
+ │ └── shared/ # 事件总线、工具函数
111
+ └── shared/ # 主进程/渲染进程共享类型
112
+ ```
113
+
114
+ ## 技术栈
115
+
116
+ - **框架**:Electron 35 + React 18 + TypeScript
117
+ - **构建工具**:Vite 6
118
+ - **UI**:Ant Design 5
119
+ - **数据库**:Prisma ORM + SQLite
120
+ - **PDF 处理**:pdf-lib、pdf-to-png-converter、Sharp
121
+ - **Markdown**:react-markdown、remark-gfm、remark-math、rehype-katex、rehype-prism-plus
122
+ - **测试**:Vitest + Testing Library
123
+
124
+ ## 支持的 LLM 提供商
125
+
126
+ | 提供商 | 模型 | 备注 |
127
+ |--------|------|------|
128
+ | OpenAI | GPT-4o、GPT-4-turbo 等 | 需要 API 密钥 |
129
+ | Anthropic | Claude 3.5、Claude 3 等 | 需要 API 密钥 |
130
+ | Google Gemini | Gemini Pro、Gemini Flash 等 | 需要 API 密钥 |
131
+ | Ollama | LLaVA、Llama 3.2 Vision 等 | 本地运行,无需 API 密钥 |
132
+ | OpenAI Responses | 任何 OpenAI 兼容模型 | 支持自定义端点 |
133
+
134
+ ## 许可证
135
+
136
+ [Apache-2.0](./LICENSE)
137
+
138
+ ## 贡献
139
+
140
+ 欢迎贡献!请阅读 [AGENTS.md](./AGENTS.md) 文件了解开发指南。
package/bin/cli.js CHANGED
@@ -21,9 +21,23 @@ async function ensurePrismaClient() {
21
21
  if (!existsSync(prismaClientPath)) {
22
22
  console.log('🔧 Prisma client not found. Generating...');
23
23
  try {
24
- execSync('npx prisma generate --schema=./src/core/infrastructure/db/schema.prisma', {
24
+ // 使用项目本地的 prisma CLI
25
+ const prismaBin = process.platform === 'win32' ? 'prisma.cmd' : 'prisma';
26
+ const prismaPath = join(projectRoot, 'node_modules', '.bin', prismaBin);
27
+ const prismaCmd = existsSync(prismaPath)
28
+ ? `"${prismaPath}"`
29
+ : 'npx prisma';
30
+
31
+ // 设置临时的 DATABASE_URL,prisma generate 需要此变量存在
32
+ // 实际的数据库路径在运行时由 db/index.ts 动态决定
33
+ execSync(`${prismaCmd} generate --schema=./src/core/infrastructure/db/schema.prisma`, {
25
34
  cwd: projectRoot,
26
- stdio: 'inherit'
35
+ stdio: 'inherit',
36
+ shell: true,
37
+ env: {
38
+ ...process.env,
39
+ DATABASE_URL: 'file:./placeholder.db'
40
+ }
27
41
  });
28
42
  console.log('✅ Prisma client generated successfully.');
29
43
  } catch (error) {
@@ -33,14 +47,26 @@ async function ensurePrismaClient() {
33
47
  }
34
48
  }
35
49
 
50
+ // 获取 Electron 可执行文件路径
51
+ function getElectronPath() {
52
+ const require = createRequire(import.meta.url);
53
+ try {
54
+ // 使用 electron 包导出的路径(最可靠的方式)
55
+ return require('electron');
56
+ } catch (e) {
57
+ console.error('❌ Electron not found. Please ensure the package is installed correctly.');
58
+ process.exit(1);
59
+ }
60
+ }
61
+
36
62
  // 启动 Electron
37
63
  function launchElectron(args) {
38
- const electronBin = process.platform === 'win32' ? 'electron.cmd' : 'electron';
39
- const electronPath = join(projectRoot, 'node_modules', '.bin', electronBin);
64
+ const electronPath = getElectronPath();
40
65
  const mainPath = join(projectRoot, 'dist', 'main', 'index.js');
41
66
 
42
67
  if (!existsSync(mainPath)) {
43
- console.error('❌ Application not built. Please run: npm run build');
68
+ console.error('❌ Application not built. Main file not found at:', mainPath);
69
+ console.error(' Please run: npm run build');
44
70
  process.exit(1);
45
71
  }
46
72
 
@@ -793,7 +793,7 @@ class PDFSplitter {
793
793
  /**
794
794
  * Wrap errors with friendly, actionable messages.
795
795
  */
796
- wrapError(error, taskId, filename) {
796
+ wrapError(error, _taskId, filename) {
797
797
  const err = error;
798
798
  const message = err.message.toLowerCase();
799
799
  if (message.includes("password") || message.includes("encrypted")) {
@@ -881,7 +881,7 @@ class ImageSplitter {
881
881
  /**
882
882
  * Wrap errors with friendly, actionable messages.
883
883
  */
884
- wrapError(error, taskId, filename) {
884
+ wrapError(error, _taskId, filename) {
885
885
  const err = error;
886
886
  const message = err.message.toLowerCase();
887
887
  if (message.includes("enoent") || message.includes("no such file")) {
@@ -30,7 +30,8 @@ electron.contextBridge.exposeInMainWorld("api", {
30
30
  taskDetail: {
31
31
  getByPage: (taskId, page) => electron.ipcRenderer.invoke("taskDetail:getByPage", taskId, page),
32
32
  getAllByTask: (taskId) => electron.ipcRenderer.invoke("taskDetail:getAllByTask", taskId),
33
- retry: (pageId) => electron.ipcRenderer.invoke("taskDetail:retry", pageId)
33
+ retry: (pageId) => electron.ipcRenderer.invoke("taskDetail:retry", pageId),
34
+ retryFailed: (taskId) => electron.ipcRenderer.invoke("taskDetail:retryFailed", taskId)
34
35
  },
35
36
  // ==================== File APIs ====================
36
37
  file: {