intools-cli 1.0.7 → 1.0.9
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/PLUGIN_API.md +474 -23
- package/dist/commands/create/templates/react.js +170 -16
- package/dist/commands/pack.js +58 -0
- package/package.json +1 -1
package/PLUGIN_API.md
CHANGED
|
@@ -141,56 +141,507 @@
|
|
|
141
141
|
"icon": { "type": "svg", "value": "<svg>...</svg>" }
|
|
142
142
|
```
|
|
143
143
|
|
|
144
|
-
## Preload
|
|
144
|
+
## Preload 预加载脚本 ⭐ 核心概念
|
|
145
145
|
|
|
146
|
-
|
|
146
|
+
> [!IMPORTANT]
|
|
147
|
+
> **Preload 是 InTools 插件访问 Node.js 能力的核心机制。** 对于需要在渲染进程(UI)中使用 Node.js API、第三方 npm 模块或 Electron 渲染进程 API 的插件来说,Preload 是**必不可少**的。
|
|
148
|
+
|
|
149
|
+
### 什么是 Preload?
|
|
150
|
+
|
|
151
|
+
Preload 脚本是一个特殊的 JavaScript 文件,在**渲染进程加载之前**执行,具有以下特点:
|
|
152
|
+
|
|
153
|
+
| 特性 | 说明 |
|
|
154
|
+
|------|------|
|
|
155
|
+
| 🔧 **Node.js 完整支持** | 可以使用 `require()` 导入任何 Node.js 原生模块和 npm 包 |
|
|
156
|
+
| 🖥️ **Electron API 访问** | 可以调用 Electron 渲染进程 API |
|
|
157
|
+
| 🌉 **桥接能力** | 通过 `window.xxx` 将原生能力暴露给前端 React/Vue 组件 |
|
|
158
|
+
| ⚡ **同步执行** | 在页面 DOM 加载前执行,确保 API 可用 |
|
|
159
|
+
| 📄 **使用 .cjs 扩展名** | 由于项目使用 `type: module`,preload 必须命名为 `*.cjs` |
|
|
160
|
+
|
|
161
|
+
### 适用场景
|
|
162
|
+
|
|
163
|
+
以下场景**需要使用** Preload:
|
|
164
|
+
|
|
165
|
+
- 📂 使用 `pdf-lib`、`sharp`、`ffmpeg` 等需要 Node.js 环境的 npm 包
|
|
166
|
+
- 🔐 调用 Node.js 加密模块 (`crypto`)、子进程 (`child_process`)
|
|
167
|
+
- 📁 需要比 `window.intools.filesystem` 更底层的文件操作
|
|
168
|
+
- 🔗 与本地数据库交互 (SQLite、LevelDB 等)
|
|
169
|
+
- 🎯 任何需要原生能力但又想在前端统一调用的场景
|
|
170
|
+
|
|
171
|
+
---
|
|
147
172
|
|
|
148
173
|
### 配置方式
|
|
149
174
|
|
|
175
|
+
在 `manifest.json` 中添加 `preload` 字段,指定预加载脚本路径:
|
|
176
|
+
|
|
150
177
|
```json
|
|
151
178
|
{
|
|
152
|
-
"
|
|
179
|
+
"name": "my-plugin",
|
|
180
|
+
"version": "1.0.0",
|
|
181
|
+
"displayName": "我的插件",
|
|
182
|
+
"main": "dist/main.js",
|
|
183
|
+
"ui": "ui/index.html",
|
|
184
|
+
"preload": "preload.cjs", // 👈 使用 .cjs 扩展名,放在根目录
|
|
185
|
+
"features": [...]
|
|
153
186
|
}
|
|
154
187
|
```
|
|
155
188
|
|
|
156
|
-
|
|
189
|
+
> [!WARNING]
|
|
190
|
+
> **必须使用 `.cjs` 扩展名!** 由于模板使用 `"type": "module"`,所有 `.js` 文件会被当作 ES Module 处理。使用 `.cjs` 扩展名可确保文件始终被视为 CommonJS。
|
|
191
|
+
|
|
192
|
+
> [!IMPORTANT]
|
|
193
|
+
> **preload 不需要打包!** 直接使用源码文件,放在项目根目录。这样 `node_modules` 中的依赖可以正常解析。
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
### preload.cjs 编写规范
|
|
157
198
|
|
|
158
199
|
```javascript
|
|
159
|
-
// preload.
|
|
200
|
+
// preload.cjs - 必须使用 CommonJS 规范和 .cjs 扩展名
|
|
160
201
|
const fs = require('fs')
|
|
161
202
|
const os = require('os')
|
|
162
203
|
const path = require('path')
|
|
204
|
+
const { PDFDocument } = require('pdf-lib') // 可使用 npm 包
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* 通过 window 对象暴露 API 给前端
|
|
208
|
+
* 命名建议:window.{插件名}Api 或 window.{功能名}Api
|
|
209
|
+
*/
|
|
210
|
+
window.myPluginApi = {
|
|
211
|
+
// 同步方法
|
|
212
|
+
getHomeDir: () => os.homedir(),
|
|
213
|
+
getPlatform: () => process.platform,
|
|
214
|
+
|
|
215
|
+
// 异步方法
|
|
216
|
+
readFile: async (filePath) => {
|
|
217
|
+
return fs.promises.readFile(filePath, 'utf-8')
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
// 复杂功能封装
|
|
221
|
+
mergePDFs: async (pdfPaths, outputPath) => {
|
|
222
|
+
const mergedPdf = await PDFDocument.create()
|
|
223
|
+
for (const pdfPath of pdfPaths) {
|
|
224
|
+
const pdfBytes = fs.readFileSync(pdfPath)
|
|
225
|
+
const pdf = await PDFDocument.load(pdfBytes)
|
|
226
|
+
const pages = await mergedPdf.copyPages(pdf, pdf.getPageIndices())
|
|
227
|
+
pages.forEach(page => mergedPdf.addPage(page))
|
|
228
|
+
}
|
|
229
|
+
const bytes = await mergedPdf.save()
|
|
230
|
+
fs.writeFileSync(outputPath, bytes)
|
|
231
|
+
return outputPath
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
console.log('[Preload] API 已挂载到 window.myPluginApi')
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
### 前端调用示例
|
|
241
|
+
|
|
242
|
+
```tsx
|
|
243
|
+
// 在 React 组件中调用
|
|
244
|
+
import { useEffect, useState } from 'react'
|
|
245
|
+
|
|
246
|
+
// 类型声明(推荐单独放在 types.d.ts)
|
|
247
|
+
declare global {
|
|
248
|
+
interface Window {
|
|
249
|
+
myPluginApi?: {
|
|
250
|
+
getHomeDir: () => string
|
|
251
|
+
getPlatform: () => string
|
|
252
|
+
readFile: (path: string) => Promise<string>
|
|
253
|
+
mergePDFs: (paths: string[], output: string) => Promise<string>
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function MyComponent() {
|
|
259
|
+
const [homeDir, setHomeDir] = useState('')
|
|
260
|
+
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
// 使用可选链确保安全访问
|
|
263
|
+
if (window.myPluginApi) {
|
|
264
|
+
setHomeDir(window.myPluginApi.getHomeDir())
|
|
265
|
+
}
|
|
266
|
+
}, [])
|
|
267
|
+
|
|
268
|
+
const handleMerge = async () => {
|
|
269
|
+
const result = await window.myPluginApi?.mergePDFs(
|
|
270
|
+
['/path/to/1.pdf', '/path/to/2.pdf'],
|
|
271
|
+
'/path/to/merged.pdf'
|
|
272
|
+
)
|
|
273
|
+
console.log('合并完成:', result)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// 核心 API 仍然可用
|
|
277
|
+
const handleCopy = async () => {
|
|
278
|
+
const text = await window.intools.clipboard.readText()
|
|
279
|
+
console.log('剪贴板内容:', text)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return <div>Home: {homeDir}</div>
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
### 模块引入方法
|
|
289
|
+
|
|
290
|
+
Preload 脚本支持多种模块引入方式:
|
|
291
|
+
|
|
292
|
+
#### 1. 引入 Node.js 原生模块
|
|
293
|
+
|
|
294
|
+
```javascript
|
|
295
|
+
// preload.cjs
|
|
296
|
+
const fs = require('fs') // 文件系统
|
|
297
|
+
const os = require('os') // 操作系统信息
|
|
298
|
+
const path = require('path') // 路径操作
|
|
299
|
+
const crypto = require('crypto') // 加密
|
|
300
|
+
const { spawn } = require('child_process') // 子进程
|
|
301
|
+
|
|
302
|
+
window.nodeApi = {
|
|
303
|
+
homeDir: os.homedir(),
|
|
304
|
+
platform: process.platform,
|
|
305
|
+
cpus: os.cpus().length,
|
|
306
|
+
hash: (text) => crypto.createHash('md5').update(text).digest('hex')
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
#### 2. 引入自编写模块
|
|
311
|
+
|
|
312
|
+
```javascript
|
|
313
|
+
// preload.cjs
|
|
314
|
+
// 相对于 preload.js 文件的路径
|
|
315
|
+
const utils = require('./lib/utils') // 同级 lib 目录
|
|
316
|
+
const helpers = require('./helpers/format') // 同级 helpers 目录
|
|
317
|
+
const shared = require('../shared/constants') // 上级目录
|
|
163
318
|
|
|
164
|
-
// 通过 window 暴露给前端
|
|
165
319
|
window.myApi = {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
platform: process.platform
|
|
320
|
+
format: utils.formatData,
|
|
321
|
+
constants: shared.APP_NAME
|
|
169
322
|
}
|
|
170
323
|
```
|
|
171
324
|
|
|
172
|
-
|
|
325
|
+
> [!NOTE]
|
|
326
|
+
> 自编写模块也必须使用 CommonJS 格式(`module.exports`),路径相对于 `preload.cjs` 文件位置。
|
|
173
327
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
328
|
+
#### 3. 引入第三方模块
|
|
329
|
+
|
|
330
|
+
**方式 A:通过 npm 安装**
|
|
331
|
+
|
|
332
|
+
```bash
|
|
333
|
+
# 在插件目录安装依赖
|
|
334
|
+
cd my-plugin
|
|
335
|
+
npm install pdf-lib lodash dayjs
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
```javascript
|
|
339
|
+
// preload.cjs
|
|
340
|
+
const { PDFDocument } = require('pdf-lib')
|
|
341
|
+
const _ = require('lodash')
|
|
342
|
+
const dayjs = require('dayjs')
|
|
343
|
+
|
|
344
|
+
window.pdfApi = {
|
|
345
|
+
mergePDFs: async (paths) => { /* ... */ },
|
|
346
|
+
formatDate: (date) => dayjs(date).format('YYYY-MM-DD')
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
**方式 B:通过源码引入**
|
|
351
|
+
|
|
352
|
+
将第三方库源码放入插件目录:
|
|
353
|
+
|
|
354
|
+
```
|
|
355
|
+
my-plugin/
|
|
356
|
+
├── preload.cjs
|
|
357
|
+
├── vendor/
|
|
358
|
+
│ ├── lodash.min.js
|
|
359
|
+
│ └── crypto-js.js
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
```javascript
|
|
363
|
+
// preload.cjs
|
|
364
|
+
const _ = require('./vendor/lodash.min.js')
|
|
365
|
+
const CryptoJS = require('./vendor/crypto-js.js')
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
#### 4. 引入 Electron 渲染进程 API
|
|
178
369
|
|
|
179
|
-
|
|
180
|
-
|
|
370
|
+
```javascript
|
|
371
|
+
// preload.cjs
|
|
372
|
+
const {
|
|
373
|
+
ipcRenderer, // 进程通信
|
|
374
|
+
clipboard, // 剪贴板(直接访问,无需 IPC)
|
|
375
|
+
shell, // 打开外部链接/文件
|
|
376
|
+
nativeImage, // 图片处理
|
|
377
|
+
contextBridge // 上下文桥接(自定义 preload 模式下不需要)
|
|
378
|
+
} = require('electron')
|
|
379
|
+
|
|
380
|
+
window.electronApi = {
|
|
381
|
+
// 剪贴板操作
|
|
382
|
+
readClipboard: () => clipboard.readText(),
|
|
383
|
+
writeClipboard: (text) => clipboard.writeText(text),
|
|
384
|
+
readImage: () => clipboard.readImage().toDataURL(),
|
|
385
|
+
|
|
386
|
+
// 打开外部资源
|
|
387
|
+
openExternal: (url) => shell.openExternal(url),
|
|
388
|
+
showInFolder: (path) => shell.showItemInFolder(path),
|
|
389
|
+
|
|
390
|
+
// 自定义 IPC 通信(与主进程交互)
|
|
391
|
+
send: (channel, data) => ipcRenderer.send(channel, data),
|
|
392
|
+
invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args),
|
|
393
|
+
on: (channel, callback) => {
|
|
394
|
+
ipcRenderer.on(channel, (event, ...args) => callback(...args))
|
|
395
|
+
}
|
|
396
|
+
}
|
|
181
397
|
```
|
|
182
398
|
|
|
399
|
+
> [!TIP]
|
|
400
|
+
> 虽然可以直接使用 Electron API,但建议优先使用 `window.intools` 封装的 API,它们提供了更好的跨平台兼容性和错误处理。
|
|
401
|
+
|
|
402
|
+
---
|
|
403
|
+
|
|
404
|
+
### 与 Main 后端的区别
|
|
405
|
+
|
|
406
|
+
| 对比项 | Preload 脚本 | Main 后端 (main.js) |
|
|
407
|
+
|--------|--------------|---------------------|
|
|
408
|
+
| 执行环境 | 渲染进程(带 Node.js 权限) | 独立 Worker 进程 |
|
|
409
|
+
| 调用方式 | `window.xxxApi.method()` | `window.intools.host.invoke()` |
|
|
410
|
+
| 适合场景 | 同步操作、UI 紧密相关的原生功能 | 后台任务、长时间运行的操作 |
|
|
411
|
+
| 进程通信 | 无需 IPC,直接调用 | 需要 IPC,异步调用 |
|
|
412
|
+
| 生命周期 | 随 UI 窗口创建/销毁 | 独立管理,可持久化 |
|
|
413
|
+
|
|
414
|
+
> [!TIP]
|
|
415
|
+
> **选择建议**:如果功能与 UI 紧密相关且需要快速响应,使用 **Preload**;如果是后台任务或需要在无 UI 时运行,使用 **Main 后端**。
|
|
416
|
+
|
|
417
|
+
---
|
|
418
|
+
|
|
183
419
|
### 注意事项
|
|
184
420
|
|
|
185
421
|
| 项目 | 说明 |
|
|
186
422
|
|------|------|
|
|
187
|
-
|
|
|
188
|
-
|
|
|
189
|
-
|
|
|
190
|
-
|
|
|
191
|
-
|
|
|
192
|
-
|
|
|
193
|
-
|
|
|
423
|
+
| 📄 文件扩展名 | **必须使用 `.cjs`** 扩展名,放在项目根目录 |
|
|
424
|
+
| 📝 文件格式 | **必须是 CommonJS** 格式,使用 `require()` 导入模块 |
|
|
425
|
+
| 📦 不需要打包 | 直接使用源码文件,不要用 esbuild/webpack 打包 |
|
|
426
|
+
| 🔍 代码规范 | 必须是清晰可读的源码,**禁止压缩/混淆**(安全审查需要) |
|
|
427
|
+
| 📦 可用模块 | Node.js 原生模块 + 已安装的 npm 包 |
|
|
428
|
+
| 🌐 API 暴露 | 通过 `window.xxx` 暴露,建议使用 `window.{插件名}Api` 命名 |
|
|
429
|
+
| 🔧 核心 API | `window.intools` 核心 API 在 Preload 环境中**仍然可用** |
|
|
430
|
+
| ⚠️ 安全性 | 拥有完整 Node.js 权限,**请谨慎处理用户输入** |
|
|
431
|
+
| 📦 打包 | `intools pack` 会自动包含 preload 及其依赖 |
|
|
432
|
+
|
|
433
|
+
> [!CAUTION]
|
|
434
|
+
> Preload 脚本拥有完整的 Node.js 权限,可以访问文件系统、网络等敏感资源。请确保代码安全,避免执行不可信的用户输入。
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
### Preload vs 前端代码:适用场景区分 ⭐
|
|
439
|
+
|
|
440
|
+
> [!IMPORTANT]
|
|
441
|
+
> **Preload 并非万能!** 并非所有 Node.js 相关功能都应放在 Preload 中。以下是基于 pdf-tools 插件开发实践总结的经验。
|
|
442
|
+
|
|
443
|
+
#### 核心原则
|
|
444
|
+
|
|
445
|
+
| 放在 `preload.cjs` | 放在前端 React/Vue 项目 |
|
|
446
|
+
|-------------------|------------------------|
|
|
447
|
+
| 纯 Node.js 环境操作 | 需要 DOM/浏览器 API 的操作 |
|
|
448
|
+
| 无需渲染的数据处理 | 需要 `document`、`canvas` 等 |
|
|
449
|
+
| 第三方纯 JS 库 | 需要浏览器环境的第三方库 |
|
|
450
|
+
|
|
451
|
+
#### ✅ 适合放在 Preload 的功能
|
|
452
|
+
|
|
453
|
+
```javascript
|
|
454
|
+
// preload.cjs
|
|
455
|
+
const fs = require('fs');
|
|
456
|
+
const { PDFDocument } = require('pdf-lib'); // 纯 JS 库,不依赖 DOM
|
|
457
|
+
|
|
458
|
+
window.pdfApi = {
|
|
459
|
+
// ✅ 文件 I/O - Node.js 原生能力
|
|
460
|
+
readFile: async (path) => fs.promises.readFile(path),
|
|
461
|
+
saveFile: async (path, data) => fs.promises.writeFile(path, data),
|
|
462
|
+
|
|
463
|
+
// ✅ PDF 字节操作 - pdf-lib 是纯 JS 库,不需要渲染
|
|
464
|
+
getPDFInfo: async (pdfPath) => {
|
|
465
|
+
const bytes = await fs.promises.readFile(pdfPath);
|
|
466
|
+
const pdf = await PDFDocument.load(bytes);
|
|
467
|
+
return { pageCount: pdf.getPageCount() };
|
|
468
|
+
},
|
|
469
|
+
|
|
470
|
+
// ✅ 合并/拆分/水印 - 都是字节级操作,不涉及可视化
|
|
471
|
+
mergePDFs: async (files, output) => { /* ... */ },
|
|
472
|
+
splitPDF: async (file, outputDir) => { /* ... */ },
|
|
473
|
+
addWatermark: async (file, config) => { /* ... */ },
|
|
474
|
+
};
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
**适用场景**:
|
|
478
|
+
- 📂 文件读写 (`fs`)
|
|
479
|
+
- 🔧 PDF 结构操作(合并、拆分、提取元数据)
|
|
480
|
+
- 📦 纯 JS 第三方库(`pdf-lib`、`docx`、`xlsx`)
|
|
481
|
+
- 🔐 加密、压缩等纯数据处理
|
|
482
|
+
|
|
483
|
+
#### ❌ 不适合放在 Preload 的功能
|
|
484
|
+
|
|
485
|
+
以下功能**必须放在前端项目**(如 React 组件或 Service 类):
|
|
486
|
+
|
|
487
|
+
```typescript
|
|
488
|
+
// src/ui/services/PDFService.ts
|
|
489
|
+
import * as pdfjsLib from 'pdfjs-dist';
|
|
490
|
+
|
|
491
|
+
class PDFService {
|
|
492
|
+
// ❌ 需要 canvas 渲染 - 必须在浏览器环境
|
|
493
|
+
async convertPDFToImages(pdfPath: string) {
|
|
494
|
+
const pdf = await pdfjsLib.getDocument(data).promise;
|
|
495
|
+
|
|
496
|
+
for (let i = 1; i <= pdf.numPages; i++) {
|
|
497
|
+
const page = await pdf.getPage(i);
|
|
498
|
+
|
|
499
|
+
// 这些 API 只在浏览器中存在!
|
|
500
|
+
const canvas = document.createElement('canvas'); // ❌ 需要 DOM
|
|
501
|
+
const context = canvas.getContext('2d'); // ❌ 需要 Canvas API
|
|
502
|
+
|
|
503
|
+
await page.render({ canvasContext: context }).promise;
|
|
504
|
+
|
|
505
|
+
const blob = await new Promise(resolve =>
|
|
506
|
+
canvas.toBlob(resolve, 'image/png') // ❌ 需要 Blob API
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
**不适合的场景**:
|
|
514
|
+
- 🖼️ PDF 页面渲染为图片(需要 `canvas`)
|
|
515
|
+
- 📄 转换为 Word/PPT 时需要渲染图片(扫描件 PDF)
|
|
516
|
+
- 🎨 任何涉及 `document`、`canvas`、`Image` 的操作
|
|
517
|
+
|
|
518
|
+
#### 错误示例与报错
|
|
519
|
+
|
|
520
|
+
如果在 Preload 中使用浏览器 API:
|
|
521
|
+
|
|
522
|
+
```javascript
|
|
523
|
+
// preload.cjs - ❌ 错误做法!
|
|
524
|
+
window.pdfApi = {
|
|
525
|
+
renderToImage: async () => {
|
|
526
|
+
const canvas = document.createElement('canvas');
|
|
527
|
+
// 💥 报错: ReferenceError: document is not defined
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
#### 正确的架构模式
|
|
533
|
+
|
|
534
|
+
```
|
|
535
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
536
|
+
│ 前端 React 项目 │
|
|
537
|
+
│ ┌─────────────────────────────────────────────────────┐ │
|
|
538
|
+
│ │ PDFService.ts │ │
|
|
539
|
+
│ │ - convertPDFToImages() ← 使用 pdfjs-dist + canvas │ │
|
|
540
|
+
│ │ - convertToWord() ← 需要渲染扫描件 │ │
|
|
541
|
+
│ │ - compressPDF() ← 需要渲染后压缩 │ │
|
|
542
|
+
│ └──────────────────────────┬──────────────────────────┘ │
|
|
543
|
+
│ │ 调用 │
|
|
544
|
+
│ ▼ │
|
|
545
|
+
│ ┌─────────────────────────────────────────────────────┐ │
|
|
546
|
+
│ │ window.pdfApi (来自 preload.cjs) │ │
|
|
547
|
+
│ │ - readFile() / saveFile() ← 文件 I/O │ │
|
|
548
|
+
│ │ - getPDFInfo() ← 获取元数据 │ │
|
|
549
|
+
│ │ - mergePDFs() ← 字节操作 │ │
|
|
550
|
+
│ │ - splitPDF() ← 字节操作 │ │
|
|
551
|
+
│ └─────────────────────────────────────────────────────┘ │
|
|
552
|
+
└─────────────────────────────────────────────────────────────┘
|
|
553
|
+
│
|
|
554
|
+
│ require()
|
|
555
|
+
▼
|
|
556
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
557
|
+
│ preload.cjs │
|
|
558
|
+
│ - fs, path (Node.js 原生) │
|
|
559
|
+
│ - pdf-lib, docx, xlsx (纯 JS 第三方库) │
|
|
560
|
+
└─────────────────────────────────────────────────────────────┘
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
> [!TIP]
|
|
564
|
+
> **简单判断方法**:如果你的代码中出现了 `document`、`canvas`、`Image`、`Blob` 等关键字,那它就应该放在前端 React 项目中,而不是 Preload。
|
|
565
|
+
|
|
566
|
+
---
|
|
567
|
+
|
|
568
|
+
### 开发流程
|
|
569
|
+
|
|
570
|
+
使用 preload 的插件开发流程与普通插件略有不同:
|
|
571
|
+
|
|
572
|
+
#### 项目结构
|
|
573
|
+
|
|
574
|
+
```
|
|
575
|
+
my-plugin/
|
|
576
|
+
├── manifest.json # 配置 preload 字段
|
|
577
|
+
├── package.json # 依赖列表(含 type: module)
|
|
578
|
+
├── preload.cjs # 👈 Preload 脚本(根目录,不打包)
|
|
579
|
+
├── lib/ # 可选:自编写模块
|
|
580
|
+
│ └── utils.cjs
|
|
581
|
+
├── src/
|
|
582
|
+
│ ├── main.ts # 后端入口
|
|
583
|
+
│ └── ui/ # React UI
|
|
584
|
+
├── dist/ # 构建输出(main.js)
|
|
585
|
+
├── ui/ # UI 构建输出
|
|
586
|
+
└── node_modules/ # npm 依赖
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
#### CLI 命令说明
|
|
590
|
+
|
|
591
|
+
| 命令 | Preload 处理方式 |
|
|
592
|
+
|------|-----------------|
|
|
593
|
+
| `intools create` | 创建项目模板,手动添加 `preload.cjs` |
|
|
594
|
+
| `intools dev` | 无需处理,preload 使用源码直接运行 |
|
|
595
|
+
| `intools build` | 无需处理,preload **不需要打包** |
|
|
596
|
+
| `intools pack` | **自动打包** preload 文件 + node_modules 生产依赖 |
|
|
597
|
+
|
|
598
|
+
#### 开发步骤
|
|
599
|
+
|
|
600
|
+
```bash
|
|
601
|
+
# 1. 创建插件
|
|
602
|
+
cd plugins
|
|
603
|
+
npx intools create my-plugin --template react
|
|
604
|
+
|
|
605
|
+
# 2. 手动创建 preload.cjs
|
|
606
|
+
touch preload.cjs
|
|
607
|
+
|
|
608
|
+
# 3. 在 manifest.json 中配置
|
|
609
|
+
# "preload": "preload.cjs"
|
|
610
|
+
|
|
611
|
+
# 4. 安装第三方依赖(如需要)
|
|
612
|
+
npm install pdf-lib lodash
|
|
613
|
+
|
|
614
|
+
# 5. 编写 preload.cjs(使用 require)
|
|
615
|
+
|
|
616
|
+
# 6. 开发调试
|
|
617
|
+
npm run dev
|
|
618
|
+
|
|
619
|
+
# 7. 构建
|
|
620
|
+
npm run build
|
|
621
|
+
|
|
622
|
+
# 8. 打包发布
|
|
623
|
+
npm run pack
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
#### 打包后的结构
|
|
627
|
+
|
|
628
|
+
`intools pack` 会生成包含以下内容的 `.inplugin` 文件:
|
|
629
|
+
|
|
630
|
+
```
|
|
631
|
+
my-plugin-1.0.0.inplugin
|
|
632
|
+
├── manifest.json
|
|
633
|
+
├── main.js # 后端(打包后)
|
|
634
|
+
├── preload.cjs # Preload 源码
|
|
635
|
+
├── ui/ # UI 构建产物
|
|
636
|
+
├── node_modules/ # 👈 自动包含生产依赖
|
|
637
|
+
│ ├── pdf-lib/
|
|
638
|
+
│ └── (依赖的依赖...)
|
|
639
|
+
├── icon.png
|
|
640
|
+
└── README.md
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
> [!NOTE]
|
|
644
|
+
> 只有 `package.json` 中 `dependencies`(生产依赖)会被打包。`devDependencies` 不会包含在内。
|
|
194
645
|
|
|
195
646
|
---
|
|
196
647
|
|
|
@@ -199,25 +199,56 @@ function buildAppTsx(name) {
|
|
|
199
199
|
return `import { useEffect, useState } from 'react'
|
|
200
200
|
import { useIntools } from './hooks/useIntools'
|
|
201
201
|
|
|
202
|
+
// 附件类型定义
|
|
203
|
+
interface Attachment {
|
|
204
|
+
id: string
|
|
205
|
+
name: string
|
|
206
|
+
size: number
|
|
207
|
+
kind: 'file' | 'image'
|
|
208
|
+
mime?: string
|
|
209
|
+
ext?: string
|
|
210
|
+
path?: string
|
|
211
|
+
dataUrl?: string
|
|
212
|
+
}
|
|
213
|
+
|
|
202
214
|
interface PluginInitData {
|
|
203
215
|
pluginName: string
|
|
204
216
|
featureCode: string
|
|
205
217
|
input: string
|
|
206
218
|
mode?: string
|
|
207
219
|
route?: string
|
|
220
|
+
attachments?: Attachment[]
|
|
208
221
|
}
|
|
209
222
|
|
|
210
223
|
export default function App() {
|
|
211
224
|
const [input, setInput] = useState('')
|
|
212
225
|
const [output, setOutput] = useState('')
|
|
226
|
+
const [theme, setTheme] = useState<'light' | 'dark'>('light')
|
|
227
|
+
const [attachments, setAttachments] = useState<Attachment[]>([])
|
|
213
228
|
const { clipboard, notification } = useIntools('${name}')
|
|
214
229
|
|
|
215
230
|
useEffect(() => {
|
|
231
|
+
// 获取初始主题(从 URL 参数)
|
|
232
|
+
const params = new URLSearchParams(window.location.search)
|
|
233
|
+
const initialTheme = (params.get('theme') as 'light' | 'dark') || 'light'
|
|
234
|
+
setTheme(initialTheme)
|
|
235
|
+
document.documentElement.classList.toggle('dark', initialTheme === 'dark')
|
|
236
|
+
|
|
237
|
+
// 监听主题变化
|
|
238
|
+
window.intools?.onThemeChange?.((newTheme: 'light' | 'dark') => {
|
|
239
|
+
setTheme(newTheme)
|
|
240
|
+
document.documentElement.classList.toggle('dark', newTheme === 'dark')
|
|
241
|
+
})
|
|
242
|
+
|
|
216
243
|
// 接收插件初始化数据
|
|
217
244
|
window.intools?.onPluginInit?.((data: PluginInitData) => {
|
|
218
245
|
if (data.input) {
|
|
219
246
|
setInput(data.input)
|
|
220
247
|
}
|
|
248
|
+
// 接收附件数据
|
|
249
|
+
if (data.attachments) {
|
|
250
|
+
setAttachments(data.attachments)
|
|
251
|
+
}
|
|
221
252
|
})
|
|
222
253
|
}, [])
|
|
223
254
|
|
|
@@ -231,10 +262,44 @@ export default function App() {
|
|
|
231
262
|
notification.show('已复制到剪贴板')
|
|
232
263
|
}
|
|
233
264
|
|
|
265
|
+
// 格式化文件大小
|
|
266
|
+
const formatSize = (bytes: number) => {
|
|
267
|
+
if (bytes < 1024) return \`\${bytes} B\`
|
|
268
|
+
if (bytes < 1024 * 1024) return \`\${(bytes / 1024).toFixed(1)} KB\`
|
|
269
|
+
return \`\${(bytes / 1024 / 1024).toFixed(1)} MB\`
|
|
270
|
+
}
|
|
271
|
+
|
|
234
272
|
return (
|
|
235
273
|
<div className="app">
|
|
236
274
|
<div className="titlebar">${name}</div>
|
|
237
275
|
<div className="container">
|
|
276
|
+
{/* 附件展示区域 */}
|
|
277
|
+
{attachments.length > 0 && (
|
|
278
|
+
<div className="field">
|
|
279
|
+
<label>附件 ({attachments.length})</label>
|
|
280
|
+
<div className="attachments-list">
|
|
281
|
+
{attachments.map((item, index) => (
|
|
282
|
+
<div key={item.id || index} className="attachment-item">
|
|
283
|
+
<span className="attachment-icon">
|
|
284
|
+
{item.kind === 'image' ? '🖼️' : '📄'}
|
|
285
|
+
</span>
|
|
286
|
+
<div className="attachment-info">
|
|
287
|
+
<div className="attachment-name">{item.name}</div>
|
|
288
|
+
<div className="attachment-meta">{formatSize(item.size)}</div>
|
|
289
|
+
</div>
|
|
290
|
+
{item.kind === 'image' && (item.dataUrl || item.path) && (
|
|
291
|
+
<img
|
|
292
|
+
src={item.dataUrl || \`file://\${item.path}\`}
|
|
293
|
+
alt={item.name}
|
|
294
|
+
className="attachment-preview"
|
|
295
|
+
/>
|
|
296
|
+
)}
|
|
297
|
+
</div>
|
|
298
|
+
))}
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
)}
|
|
302
|
+
|
|
238
303
|
<div className="field">
|
|
239
304
|
<label>输入</label>
|
|
240
305
|
<textarea
|
|
@@ -263,7 +328,33 @@ export default function App() {
|
|
|
263
328
|
`;
|
|
264
329
|
}
|
|
265
330
|
function buildStylesCss() {
|
|
266
|
-
return
|
|
331
|
+
return `/* CSS 变量 - 亮色主题 */
|
|
332
|
+
:root {
|
|
333
|
+
--bg-primary: #ffffff;
|
|
334
|
+
--bg-secondary: #f5f5f5;
|
|
335
|
+
--bg-tertiary: #ebebeb;
|
|
336
|
+
--text-primary: #1e1e1e;
|
|
337
|
+
--text-secondary: #666666;
|
|
338
|
+
--text-tertiary: #999999;
|
|
339
|
+
--border-color: #e0e0e0;
|
|
340
|
+
--accent-color: #0078d4;
|
|
341
|
+
--accent-hover: #1084d8;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/* CSS 变量 - 暗色主题 */
|
|
345
|
+
:root.dark {
|
|
346
|
+
--bg-primary: #1e1e1e;
|
|
347
|
+
--bg-secondary: #2d2d2d;
|
|
348
|
+
--bg-tertiary: #3d3d3d;
|
|
349
|
+
--text-primary: #e0e0e0;
|
|
350
|
+
--text-secondary: #999999;
|
|
351
|
+
--text-tertiary: #666666;
|
|
352
|
+
--border-color: #3d3d3d;
|
|
353
|
+
--accent-color: #0078d4;
|
|
354
|
+
--accent-hover: #1084d8;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
* {
|
|
267
358
|
margin: 0;
|
|
268
359
|
padding: 0;
|
|
269
360
|
box-sizing: border-box;
|
|
@@ -271,9 +362,10 @@ function buildStylesCss() {
|
|
|
271
362
|
|
|
272
363
|
body {
|
|
273
364
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
274
|
-
background:
|
|
275
|
-
color:
|
|
365
|
+
background: var(--bg-primary);
|
|
366
|
+
color: var(--text-primary);
|
|
276
367
|
min-height: 100vh;
|
|
368
|
+
transition: background-color 0.2s, color 0.2s;
|
|
277
369
|
}
|
|
278
370
|
|
|
279
371
|
.app {
|
|
@@ -284,12 +376,12 @@ body {
|
|
|
284
376
|
|
|
285
377
|
.titlebar {
|
|
286
378
|
height: 32px;
|
|
287
|
-
background:
|
|
379
|
+
background: var(--bg-secondary);
|
|
288
380
|
display: flex;
|
|
289
381
|
align-items: center;
|
|
290
382
|
justify-content: center;
|
|
291
383
|
font-size: 13px;
|
|
292
|
-
color:
|
|
384
|
+
color: var(--text-secondary);
|
|
293
385
|
-webkit-app-region: drag;
|
|
294
386
|
flex-shrink: 0;
|
|
295
387
|
}
|
|
@@ -313,29 +405,30 @@ body {
|
|
|
313
405
|
|
|
314
406
|
.field label {
|
|
315
407
|
font-size: 12px;
|
|
316
|
-
color:
|
|
408
|
+
color: var(--text-secondary);
|
|
317
409
|
}
|
|
318
410
|
|
|
319
411
|
.field textarea {
|
|
320
412
|
flex: 1;
|
|
321
|
-
background:
|
|
322
|
-
border: 1px solid
|
|
413
|
+
background: var(--bg-secondary);
|
|
414
|
+
border: 1px solid var(--border-color);
|
|
323
415
|
border-radius: 6px;
|
|
324
416
|
padding: 12px;
|
|
325
|
-
color:
|
|
417
|
+
color: var(--text-primary);
|
|
326
418
|
font-family: 'Monaco', 'Consolas', monospace;
|
|
327
419
|
font-size: 13px;
|
|
328
420
|
resize: none;
|
|
329
421
|
outline: none;
|
|
330
422
|
min-height: 80px;
|
|
423
|
+
transition: background-color 0.2s, border-color 0.2s, color 0.2s;
|
|
331
424
|
}
|
|
332
425
|
|
|
333
426
|
.field textarea:focus {
|
|
334
|
-
border-color:
|
|
427
|
+
border-color: var(--accent-color);
|
|
335
428
|
}
|
|
336
429
|
|
|
337
430
|
.field textarea::placeholder {
|
|
338
|
-
color:
|
|
431
|
+
color: var(--text-tertiary);
|
|
339
432
|
}
|
|
340
433
|
|
|
341
434
|
.actions {
|
|
@@ -354,21 +447,70 @@ button {
|
|
|
354
447
|
}
|
|
355
448
|
|
|
356
449
|
.btn-primary {
|
|
357
|
-
background:
|
|
450
|
+
background: var(--accent-color);
|
|
358
451
|
color: #fff;
|
|
359
452
|
}
|
|
360
453
|
|
|
361
454
|
.btn-primary:hover {
|
|
362
|
-
background:
|
|
455
|
+
background: var(--accent-hover);
|
|
363
456
|
}
|
|
364
457
|
|
|
365
458
|
.btn-secondary {
|
|
366
|
-
background:
|
|
367
|
-
color:
|
|
459
|
+
background: var(--bg-tertiary);
|
|
460
|
+
color: var(--text-primary);
|
|
368
461
|
}
|
|
369
462
|
|
|
370
463
|
.btn-secondary:hover {
|
|
371
|
-
background:
|
|
464
|
+
background: var(--bg-secondary);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/* 附件列表样式 */
|
|
468
|
+
.attachments-list {
|
|
469
|
+
display: flex;
|
|
470
|
+
flex-direction: column;
|
|
471
|
+
gap: 8px;
|
|
472
|
+
max-height: 200px;
|
|
473
|
+
overflow-y: auto;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.attachment-item {
|
|
477
|
+
display: flex;
|
|
478
|
+
align-items: center;
|
|
479
|
+
padding: 10px 12px;
|
|
480
|
+
background: var(--bg-secondary);
|
|
481
|
+
border: 1px solid var(--border-color);
|
|
482
|
+
border-radius: 6px;
|
|
483
|
+
gap: 10px;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
.attachment-icon {
|
|
487
|
+
font-size: 20px;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
.attachment-info {
|
|
491
|
+
flex: 1;
|
|
492
|
+
min-width: 0;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
.attachment-name {
|
|
496
|
+
font-size: 13px;
|
|
497
|
+
font-weight: 500;
|
|
498
|
+
white-space: nowrap;
|
|
499
|
+
overflow: hidden;
|
|
500
|
+
text-overflow: ellipsis;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
.attachment-meta {
|
|
504
|
+
font-size: 11px;
|
|
505
|
+
color: var(--text-tertiary);
|
|
506
|
+
margin-top: 2px;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
.attachment-preview {
|
|
510
|
+
width: 40px;
|
|
511
|
+
height: 40px;
|
|
512
|
+
border-radius: 4px;
|
|
513
|
+
object-fit: cover;
|
|
372
514
|
}
|
|
373
515
|
`;
|
|
374
516
|
}
|
|
@@ -1145,6 +1287,17 @@ interface IntoolsFFmpeg {
|
|
|
1145
1287
|
run(args: string[], onProgress?: (progress: FFmpegRunProgress) => void): FFmpegTask
|
|
1146
1288
|
}
|
|
1147
1289
|
|
|
1290
|
+
interface Attachment {
|
|
1291
|
+
id: string
|
|
1292
|
+
name: string
|
|
1293
|
+
size: number
|
|
1294
|
+
kind: 'file' | 'image'
|
|
1295
|
+
mime?: string
|
|
1296
|
+
ext?: string
|
|
1297
|
+
path?: string
|
|
1298
|
+
dataUrl?: string
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1148
1301
|
interface PluginInitData {
|
|
1149
1302
|
pluginName: string
|
|
1150
1303
|
featureCode: string
|
|
@@ -1152,6 +1305,7 @@ interface PluginInitData {
|
|
|
1152
1305
|
input: string
|
|
1153
1306
|
mode?: string
|
|
1154
1307
|
route?: string
|
|
1308
|
+
attachments?: Attachment[]
|
|
1155
1309
|
}
|
|
1156
1310
|
|
|
1157
1311
|
interface IntoolsAPI {
|
package/dist/commands/pack.js
CHANGED
|
@@ -87,6 +87,33 @@ async function createArchive(cwd, outputPath, manifest) {
|
|
|
87
87
|
if (fs.existsSync(preloadPath)) {
|
|
88
88
|
archive.file(preloadPath, { name: manifest.preload });
|
|
89
89
|
console.log(chalk_1.default.gray(` + ${manifest.preload}`));
|
|
90
|
+
// 当有 preload 时,打包 node_modules 中的生产依赖
|
|
91
|
+
const nodeModulesDir = path.join(cwd, 'node_modules');
|
|
92
|
+
const pkgJsonPath = path.join(cwd, 'package.json');
|
|
93
|
+
if (fs.existsSync(nodeModulesDir) && fs.existsSync(pkgJsonPath)) {
|
|
94
|
+
const pkgJson = fs.readJsonSync(pkgJsonPath);
|
|
95
|
+
const dependencies = Object.keys(pkgJson.dependencies || {});
|
|
96
|
+
if (dependencies.length > 0) {
|
|
97
|
+
console.log(chalk_1.default.gray(' + node_modules/ (生产依赖)'));
|
|
98
|
+
// 打包每个生产依赖及其子依赖
|
|
99
|
+
for (const dep of dependencies) {
|
|
100
|
+
const depPath = path.join(nodeModulesDir, dep);
|
|
101
|
+
if (fs.existsSync(depPath)) {
|
|
102
|
+
archive.directory(depPath, `node_modules/${dep}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// 递归收集所有需要的依赖(包括依赖的依赖)
|
|
106
|
+
const allDeps = collectAllDependencies(cwd, dependencies);
|
|
107
|
+
for (const dep of allDeps) {
|
|
108
|
+
if (!dependencies.includes(dep)) {
|
|
109
|
+
const depPath = path.join(nodeModulesDir, dep);
|
|
110
|
+
if (fs.existsSync(depPath)) {
|
|
111
|
+
archive.directory(depPath, `node_modules/${dep}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
90
117
|
}
|
|
91
118
|
else {
|
|
92
119
|
console.log(chalk_1.default.yellow(`警告: preload 文件不存在: ${manifest.preload}`));
|
|
@@ -100,3 +127,34 @@ async function createArchive(cwd, outputPath, manifest) {
|
|
|
100
127
|
archive.finalize();
|
|
101
128
|
});
|
|
102
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* 递归收集所有依赖(包括依赖的依赖)
|
|
132
|
+
*/
|
|
133
|
+
function collectAllDependencies(cwd, dependencies) {
|
|
134
|
+
const nodeModulesDir = path.join(cwd, 'node_modules');
|
|
135
|
+
const collected = new Set();
|
|
136
|
+
const queue = [...dependencies];
|
|
137
|
+
while (queue.length > 0) {
|
|
138
|
+
const dep = queue.shift();
|
|
139
|
+
if (collected.has(dep))
|
|
140
|
+
continue;
|
|
141
|
+
collected.add(dep);
|
|
142
|
+
// 读取该依赖的 package.json 获取其依赖
|
|
143
|
+
const depPkgPath = path.join(nodeModulesDir, dep, 'package.json');
|
|
144
|
+
if (fs.existsSync(depPkgPath)) {
|
|
145
|
+
try {
|
|
146
|
+
const depPkg = fs.readJsonSync(depPkgPath);
|
|
147
|
+
const subDeps = Object.keys(depPkg.dependencies || {});
|
|
148
|
+
for (const subDep of subDeps) {
|
|
149
|
+
if (!collected.has(subDep)) {
|
|
150
|
+
queue.push(subDep);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// 忽略读取错误
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return Array.from(collected);
|
|
160
|
+
}
|