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 CHANGED
@@ -141,56 +141,507 @@
141
141
  "icon": { "type": "svg", "value": "<svg>...</svg>" }
142
142
  ```
143
143
 
144
- ## Preload 配置(自定义 Node.js 能力)
144
+ ## Preload 预加载脚本 核心概念
145
145
 
146
- 配置自定义 preload 脚本,可在渲染进程中直接使用 Node.js 能力。
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
- "preload": "preload.js"
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
- ### preload.js 示例
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.js - 遵循 CommonJS 规范
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
- getHomeDir: () => os.homedir(),
167
- readFile: (filePath) => fs.readFileSync(filePath, 'utf-8'),
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
- ```typescript
175
- // 在 UI 组件中使用
176
- const homeDir = window.myApi?.getHomeDir()
177
- const content = window.myApi?.readFile('/path/to/file.txt')
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
- // 核心 API 仍然可用
180
- const text = await window.intools.clipboard.readText()
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
- | 文件格式 | CommonJS 格式,使用 `require()` 导入模块 |
188
- | 代码规范 | 必须是清晰可读的源码,**不能压缩/混淆** |
189
- | 可用模块 | Node.js 原生模块 + 第三方 npm 模块 |
190
- | API 暴露 | 通过 `window.xxx` 暴露自定义 API |
191
- | 核心 API | `window.intools` 核心 API 仍然可用 |
192
- | 安全性 | 有完整 Node.js 权限,需注意安全风险 |
193
- | 打包 | 运行 `intools pack` 会自动包含 preload 文件 |
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: #1e1e1e;
275
- color: #e0e0e0;
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: #2d2d2d;
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: #999;
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: #999;
408
+ color: var(--text-secondary);
317
409
  }
318
410
 
319
411
  .field textarea {
320
412
  flex: 1;
321
- background: #2d2d2d;
322
- border: 1px solid #3d3d3d;
413
+ background: var(--bg-secondary);
414
+ border: 1px solid var(--border-color);
323
415
  border-radius: 6px;
324
416
  padding: 12px;
325
- color: #fff;
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: #0078d4;
427
+ border-color: var(--accent-color);
335
428
  }
336
429
 
337
430
  .field textarea::placeholder {
338
- color: #666;
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: #0078d4;
450
+ background: var(--accent-color);
358
451
  color: #fff;
359
452
  }
360
453
 
361
454
  .btn-primary:hover {
362
- background: #1084d8;
455
+ background: var(--accent-hover);
363
456
  }
364
457
 
365
458
  .btn-secondary {
366
- background: #3d3d3d;
367
- color: #fff;
459
+ background: var(--bg-tertiary);
460
+ color: var(--text-primary);
368
461
  }
369
462
 
370
463
  .btn-secondary:hover {
371
- background: #4d4d4d;
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 {
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "intools-cli",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "InTools 插件开发 CLI 工具",
5
5
  "main": "dist/index.js",
6
6
  "files": [