intools-cli 1.0.8 → 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
@@ -156,6 +156,7 @@ Preload 脚本是一个特殊的 JavaScript 文件,在**渲染进程加载之
156
156
  | 🖥️ **Electron API 访问** | 可以调用 Electron 渲染进程 API |
157
157
  | 🌉 **桥接能力** | 通过 `window.xxx` 将原生能力暴露给前端 React/Vue 组件 |
158
158
  | ⚡ **同步执行** | 在页面 DOM 加载前执行,确保 API 可用 |
159
+ | 📄 **使用 .cjs 扩展名** | 由于项目使用 `type: module`,preload 必须命名为 `*.cjs` |
159
160
 
160
161
  ### 适用场景
161
162
 
@@ -180,17 +181,23 @@ Preload 脚本是一个特殊的 JavaScript 文件,在**渲染进程加载之
180
181
  "displayName": "我的插件",
181
182
  "main": "dist/main.js",
182
183
  "ui": "ui/index.html",
183
- "preload": "preload.js", // 👈 指定预加载脚本
184
+ "preload": "preload.cjs", // 👈 使用 .cjs 扩展名,放在根目录
184
185
  "features": [...]
185
186
  }
186
187
  ```
187
188
 
189
+ > [!WARNING]
190
+ > **必须使用 `.cjs` 扩展名!** 由于模板使用 `"type": "module"`,所有 `.js` 文件会被当作 ES Module 处理。使用 `.cjs` 扩展名可确保文件始终被视为 CommonJS。
191
+
192
+ > [!IMPORTANT]
193
+ > **preload 不需要打包!** 直接使用源码文件,放在项目根目录。这样 `node_modules` 中的依赖可以正常解析。
194
+
188
195
  ---
189
196
 
190
- ### preload.js 编写规范
197
+ ### preload.cjs 编写规范
191
198
 
192
199
  ```javascript
193
- // preload.js - 必须使用 CommonJS 规范
200
+ // preload.cjs - 必须使用 CommonJS 规范和 .cjs 扩展名
194
201
  const fs = require('fs')
195
202
  const os = require('os')
196
203
  const path = require('path')
@@ -278,6 +285,122 @@ export function MyComponent() {
278
285
 
279
286
  ---
280
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') // 上级目录
318
+
319
+ window.myApi = {
320
+ format: utils.formatData,
321
+ constants: shared.APP_NAME
322
+ }
323
+ ```
324
+
325
+ > [!NOTE]
326
+ > 自编写模块也必须使用 CommonJS 格式(`module.exports`),路径相对于 `preload.cjs` 文件位置。
327
+
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
369
+
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
+ }
397
+ ```
398
+
399
+ > [!TIP]
400
+ > 虽然可以直接使用 Electron API,但建议优先使用 `window.intools` 封装的 API,它们提供了更好的跨平台兼容性和错误处理。
401
+
402
+ ---
403
+
281
404
  ### 与 Main 后端的区别
282
405
 
283
406
  | 对比项 | Preload 脚本 | Main 后端 (main.js) |
@@ -297,7 +420,9 @@ export function MyComponent() {
297
420
 
298
421
  | 项目 | 说明 |
299
422
  |------|------|
423
+ | 📄 文件扩展名 | **必须使用 `.cjs`** 扩展名,放在项目根目录 |
300
424
  | 📝 文件格式 | **必须是 CommonJS** 格式,使用 `require()` 导入模块 |
425
+ | 📦 不需要打包 | 直接使用源码文件,不要用 esbuild/webpack 打包 |
301
426
  | 🔍 代码规范 | 必须是清晰可读的源码,**禁止压缩/混淆**(安全审查需要) |
302
427
  | 📦 可用模块 | Node.js 原生模块 + 已安装的 npm 包 |
303
428
  | 🌐 API 暴露 | 通过 `window.xxx` 暴露,建议使用 `window.{插件名}Api` 命名 |
@@ -310,6 +435,216 @@ export function MyComponent() {
310
435
 
311
436
  ---
312
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` 不会包含在内。
645
+
646
+ ---
647
+
313
648
  # 第二部分:API 参考
314
649
 
315
650
  ## 1. 剪贴板 (clipboard)
@@ -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.8",
3
+ "version": "1.0.9",
4
4
  "description": "InTools 插件开发 CLI 工具",
5
5
  "main": "dist/index.js",
6
6
  "files": [