@talex-touch/utils 1.0.32 → 1.0.34

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,196 @@
1
+ import type { IStorageChannel } from './base-storage'
2
+
3
+ /**
4
+ * Storage subscription callback type
5
+ */
6
+ export type StorageSubscriptionCallback = (data: any) => void
7
+
8
+ /**
9
+ * Storage subscription manager for renderer process
10
+ * Provides easy subscription to storage updates via channel events
11
+ */
12
+ class StorageSubscriptionManager {
13
+ private channel: IStorageChannel | null = null
14
+ private subscribers = new Map<string, Set<StorageSubscriptionCallback>>()
15
+ private channelListenerRegistered = false
16
+ private pendingUpdates = new Map<string, NodeJS.Timeout>()
17
+
18
+ /**
19
+ * Initialize the subscription manager with a channel
20
+ */
21
+ init(channel: IStorageChannel): void {
22
+ this.channel = channel
23
+
24
+ if (!this.channelListenerRegistered) {
25
+ // Listen to storage:update events from main process
26
+ this.channel.regChannel('storage:update', ({ data }) => {
27
+ const { name } = data as { name: string }
28
+ this.handleStorageUpdate(name)
29
+ })
30
+ this.channelListenerRegistered = true
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Subscribe to storage changes for a specific config
36
+ * @param configName - The configuration file name (e.g., 'app-setting.ini')
37
+ * @param callback - Callback function to receive updates
38
+ * @returns Unsubscribe function
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * const unsubscribe = subscribeStorage('app-setting.ini', (data) => {
43
+ * console.log('Config updated:', data)
44
+ * })
45
+ *
46
+ * // Later:
47
+ * unsubscribe()
48
+ * ```
49
+ */
50
+ subscribe(configName: string, callback: StorageSubscriptionCallback): () => void {
51
+ if (!this.subscribers.has(configName)) {
52
+ this.subscribers.set(configName, new Set())
53
+ }
54
+
55
+ this.subscribers.get(configName)!.add(callback)
56
+
57
+ // Immediately load and call with current data
58
+ if (this.channel) {
59
+ const currentData = this.channel.sendSync('storage:get', configName)
60
+ if (currentData) {
61
+ try {
62
+ callback(currentData)
63
+ }
64
+ catch (error) {
65
+ console.error(`[StorageSubscription] Callback error for "${configName}":`, error)
66
+ }
67
+ }
68
+ }
69
+
70
+ // Return unsubscribe function
71
+ return () => {
72
+ this.unsubscribe(configName, callback)
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Unsubscribe from storage changes
78
+ * @param configName - The configuration file name
79
+ * @param callback - The same callback function used in subscribe
80
+ */
81
+ unsubscribe(configName: string, callback: StorageSubscriptionCallback): void {
82
+ const callbacks = this.subscribers.get(configName)
83
+ if (callbacks) {
84
+ callbacks.delete(callback)
85
+ if (callbacks.size === 0) {
86
+ this.subscribers.delete(configName)
87
+ }
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Handle storage update events from main process
93
+ * @private
94
+ */
95
+ private async handleStorageUpdate(configName: string): Promise<void> {
96
+ const callbacks = this.subscribers.get(configName)
97
+ if (!callbacks || callbacks.size === 0) {
98
+ return
99
+ }
100
+
101
+ if (!this.channel) {
102
+ return
103
+ }
104
+
105
+ // Debounce updates to avoid excessive IPC and callback invocations
106
+ const existing = this.pendingUpdates.get(configName)
107
+ if (existing) {
108
+ clearTimeout(existing)
109
+ }
110
+
111
+ const timer = setTimeout(async () => {
112
+ // Fetch latest data
113
+ const data = await this.channel!.send('storage:get', configName)
114
+ if (!data) {
115
+ this.pendingUpdates.delete(configName)
116
+ return
117
+ }
118
+
119
+ // Notify all subscribers
120
+ callbacks.forEach((callback) => {
121
+ try {
122
+ callback(data)
123
+ }
124
+ catch (error) {
125
+ console.error(`[StorageSubscription] Callback error for "${configName}":`, error)
126
+ }
127
+ })
128
+
129
+ this.pendingUpdates.delete(configName)
130
+ }, 50) // 50ms debounce window
131
+
132
+ this.pendingUpdates.set(configName, timer)
133
+ }
134
+
135
+ /**
136
+ * Get the number of active subscriptions for a config
137
+ * @param configName - The configuration file name
138
+ * @returns Number of active callbacks subscribed to this config
139
+ */
140
+ getSubscriberCount(configName: string): number {
141
+ return this.subscribers.get(configName)?.size ?? 0
142
+ }
143
+
144
+ /**
145
+ * Clear all subscriptions
146
+ */
147
+ clear(): void {
148
+ this.subscribers.clear()
149
+ }
150
+ }
151
+
152
+ // Global singleton instance
153
+ const subscriptionManager = new StorageSubscriptionManager()
154
+
155
+ /**
156
+ * Initialize storage subscription system with channel
157
+ * Must be called before using subscribeStorage
158
+ *
159
+ * @param channel - The storage channel
160
+ */
161
+ export function initStorageSubscription(channel: IStorageChannel): void {
162
+ subscriptionManager.init(channel)
163
+ }
164
+
165
+ /**
166
+ * Subscribe to storage configuration changes
167
+ *
168
+ * @param configName - Configuration file name (e.g., 'app-setting.ini')
169
+ * @param callback - Callback function that receives updated data
170
+ * @returns Unsubscribe function
171
+ *
172
+ * @example
173
+ * ```typescript
174
+ * import { subscribeStorage } from '@talex-touch/utils/renderer/storage/storage-subscription'
175
+ *
176
+ * const unsubscribe = subscribeStorage('app-setting.ini', (data) => {
177
+ * console.log('Settings updated:', data)
178
+ * })
179
+ *
180
+ * // Clean up when no longer needed
181
+ * unsubscribe()
182
+ * ```
183
+ */
184
+ export function subscribeStorage(
185
+ configName: string,
186
+ callback: StorageSubscriptionCallback,
187
+ ): () => void {
188
+ return subscriptionManager.subscribe(configName, callback)
189
+ }
190
+
191
+ /**
192
+ * Get subscription manager instance (for debugging)
193
+ */
194
+ export function getSubscriptionManager(): StorageSubscriptionManager {
195
+ return subscriptionManager
196
+ }
package/types/icon.ts CHANGED
@@ -37,6 +37,13 @@ export interface ITuffIcon {
37
37
  /** Icon value */
38
38
  value: string
39
39
 
40
+ /**
41
+ * Icon Colorful (Only for URL type)
42
+ * @desc This prop defines whether a URL icon should be rendered in colorful mode.
43
+ * It is only applicable when the icon type is 'url'.
44
+ */
45
+ colorful?: boolean
46
+
40
47
  /** Icon status (optional) */
41
48
  status?: TuffIconStatus
42
49
 
@@ -382,7 +382,7 @@ export const DEFAULT_CAPABILITIES: Record<string, AISDKCapabilityConfig> = {
382
382
  },
383
383
  'embedding.generate': {
384
384
  id: 'embedding.generate',
385
- label: 'Embedding',
385
+ label: 'Embedding / 向量生成',
386
386
  description: '为向量检索/摘要生成 embedding',
387
387
  providers: [
388
388
  {
@@ -391,11 +391,17 @@ export const DEFAULT_CAPABILITIES: Record<string, AISDKCapabilityConfig> = {
391
391
  priority: 1,
392
392
  enabled: true,
393
393
  },
394
+ {
395
+ providerId: 'openai-default',
396
+ models: ['text-embedding-3-small', 'text-embedding-3-large'],
397
+ priority: 2,
398
+ enabled: false,
399
+ },
394
400
  ],
395
401
  },
396
402
  'vision.ocr': {
397
403
  id: 'vision.ocr',
398
- label: '图像 OCR',
404
+ label: '图像 OCR / Image Recognition',
399
405
  description: '识别截图、图片中的文字并生成关键词',
400
406
  promptTemplate:
401
407
  '你是 OCR 助手,识别图片所有文本,生成 keywords 数组(5 个以内)辅助搜索。',
@@ -403,10 +409,198 @@ export const DEFAULT_CAPABILITIES: Record<string, AISDKCapabilityConfig> = {
403
409
  providers: [
404
410
  {
405
411
  providerId: 'siliconflow-default',
406
- models: ['deepseek-ai/DeepSeek-OCR', 'deepseek-ai/DeepSeek-R1-0528-Qwen3-8B'],
412
+ models: ['deepseek-ai/DeepSeek-OCR', 'THUDM/GLM-4.1V-9B-Thinking'],
407
413
  priority: 1,
408
414
  enabled: true,
409
- metadata: { defaultVisionModel: 'deepseek-ai/DeepSeek-OCR' },
415
+ },
416
+ {
417
+ providerId: 'openai-default',
418
+ models: ['gpt-4o', 'gpt-4o-mini'],
419
+ priority: 2,
420
+ enabled: false,
421
+ },
422
+ {
423
+ providerId: 'anthropic-default',
424
+ models: ['claude-3-5-sonnet-20241022'],
425
+ priority: 3,
426
+ enabled: false,
427
+ },
428
+ ],
429
+ },
430
+ 'text.translate': {
431
+ id: 'text.translate',
432
+ label: '翻译 / Translation',
433
+ description: '多语言文本翻译',
434
+ promptTemplate: '你是专业翻译助手。请将以下文本翻译成 {{targetLang}},只返回译文,不要解释。',
435
+ providers: [
436
+ { providerId: 'deepseek-default', priority: 1, enabled: true },
437
+ { providerId: 'openai-default', priority: 2, enabled: false },
438
+ { providerId: 'anthropic-default', priority: 3, enabled: false },
439
+ ],
440
+ },
441
+ 'text.summarize': {
442
+ id: 'text.summarize',
443
+ label: '摘要 / Summarization',
444
+ description: '生成文本内容的简洁摘要',
445
+ promptTemplate: '请用简洁的语言总结以下内容的核心要点,不超过 {{maxLength}} 字。',
446
+ providers: [
447
+ { providerId: 'deepseek-default', priority: 1, enabled: true },
448
+ { providerId: 'openai-default', priority: 2, enabled: false },
449
+ { providerId: 'anthropic-default', priority: 3, enabled: false },
450
+ ],
451
+ },
452
+ 'intent.detect': {
453
+ id: 'intent.detect',
454
+ label: '意图识别 / Intent Detection',
455
+ description: '识别用户查询的意图类型(搜索、打开、计算等)',
456
+ promptTemplate: '分析用户输入的意图,返回 JSON 格式:{intent: string, confidence: number, entities: string[]}',
457
+ providers: [
458
+ { providerId: 'deepseek-default', priority: 1, enabled: true },
459
+ { providerId: 'openai-default', priority: 2, enabled: false },
460
+ ],
461
+ },
462
+ 'code.generate': {
463
+ id: 'code.generate',
464
+ label: '代码生成 / Code Generation',
465
+ description: '根据需求生成代码片段',
466
+ promptTemplate: '你是编程助手。根据需求生成 {{language}} 代码,包含注释说明。',
467
+ providers: [
468
+ { providerId: 'deepseek-default', models: ['deepseek-coder'], priority: 1, enabled: true },
469
+ { providerId: 'openai-default', models: ['gpt-4o'], priority: 2, enabled: false },
470
+ ],
471
+ },
472
+ 'code.explain': {
473
+ id: 'code.explain',
474
+ label: '代码解释 / Code Explanation',
475
+ description: '解释代码的功能和逻辑',
476
+ promptTemplate: '你是编程导师。用通俗易懂的语言解释这段代码的功能、逻辑和关键点。',
477
+ providers: [
478
+ { providerId: 'deepseek-default', models: ['deepseek-coder'], priority: 1, enabled: true },
479
+ { providerId: 'anthropic-default', priority: 2, enabled: false },
480
+ ],
481
+ },
482
+ 'content.extract': {
483
+ id: 'content.extract',
484
+ label: '内容提取 / Content Extraction',
485
+ description: '从文本中提取关键信息(日期、人名、地点等)',
486
+ promptTemplate: '从文本中提取关键信息,返回 JSON 格式:{dates: [], people: [], locations: [], keywords: []}',
487
+ providers: [
488
+ { providerId: 'deepseek-default', priority: 1, enabled: true },
489
+ { providerId: 'openai-default', priority: 2, enabled: false },
490
+ ],
491
+ },
492
+ 'sentiment.analyze': {
493
+ id: 'sentiment.analyze',
494
+ label: '情感分析 / Sentiment Analysis',
495
+ description: '分析文本的情感倾向(积极/消极/中性)',
496
+ promptTemplate: '分析文本情感倾向,返回 JSON:{sentiment: "positive"|"negative"|"neutral", score: 0-1, keywords: []}',
497
+ providers: [
498
+ { providerId: 'deepseek-default', priority: 1, enabled: true },
499
+ { providerId: 'openai-default', priority: 2, enabled: false },
500
+ ],
501
+ },
502
+ 'code.review': {
503
+ id: 'code.review',
504
+ label: '代码审查 / Code Review',
505
+ description: '审查代码,发现潜在问题、最佳实践和改进建议',
506
+ promptTemplate: '作为资深代码审查员,审查以下代码。关注:1) 潜在bug 2) 性能问题 3) 安全隐患 4) 最佳实践 5) 可读性',
507
+ providers: [
508
+ { providerId: 'deepseek-default', models: ['deepseek-coder'], priority: 1, enabled: true },
509
+ { providerId: 'anthropic-default', priority: 2, enabled: false },
510
+ { providerId: 'openai-default', models: ['gpt-4o'], priority: 3, enabled: false },
511
+ ],
512
+ },
513
+ 'keywords.extract': {
514
+ id: 'keywords.extract',
515
+ label: '关键词提取 / Keyword Extraction',
516
+ description: '从文本中提取关键词和短语',
517
+ promptTemplate: '从文本中提取最重要的关键词,返回 JSON 数组:{keywords: [{term: string, relevance: number}]}',
518
+ providers: [
519
+ { providerId: 'deepseek-default', priority: 1, enabled: true },
520
+ { providerId: 'openai-default', priority: 2, enabled: false },
521
+ ],
522
+ },
523
+ 'audio.transcribe': {
524
+ id: 'audio.transcribe',
525
+ label: '音频转录 / Audio Transcription',
526
+ description: '将语音转换为文字(支持多语言)',
527
+ providers: [
528
+ {
529
+ providerId: 'openai-default',
530
+ models: ['whisper-1'],
531
+ priority: 1,
532
+ enabled: false,
533
+ },
534
+ {
535
+ providerId: 'siliconflow-default',
536
+ models: ['TeleAI/TeleSpeechASR'],
537
+ priority: 2,
538
+ enabled: true,
539
+ },
540
+ ],
541
+ },
542
+ 'audio.tts': {
543
+ id: 'audio.tts',
544
+ label: '语音合成 / Text-to-Speech',
545
+ description: '将文字转换为自然语音',
546
+ providers: [
547
+ {
548
+ providerId: 'openai-default',
549
+ models: ['tts-1', 'tts-1-hd'],
550
+ priority: 1,
551
+ enabled: false,
552
+ },
553
+ ],
554
+ },
555
+ 'image.caption': {
556
+ id: 'image.caption',
557
+ label: '图像标题 / Image Captioning',
558
+ description: '为图片生成描述性标题',
559
+ promptTemplate: '生成简洁准确的图片描述(中英文),捕捉主要内容和氛围。',
560
+ providers: [
561
+ {
562
+ providerId: 'siliconflow-default',
563
+ models: ['THUDM/GLM-4.1V-9B-Thinking'],
564
+ priority: 1,
565
+ enabled: true,
566
+ },
567
+ {
568
+ providerId: 'openai-default',
569
+ models: ['gpt-4o', 'gpt-4o-mini'],
570
+ priority: 2,
571
+ enabled: false,
572
+ },
573
+ {
574
+ providerId: 'anthropic-default',
575
+ models: ['claude-3-5-sonnet-20241022'],
576
+ priority: 3,
577
+ enabled: false,
578
+ },
579
+ ],
580
+ },
581
+ 'image.analyze': {
582
+ id: 'image.analyze',
583
+ label: '图像分析 / Image Analysis',
584
+ description: '深度分析图像内容、物体、场景和上下文',
585
+ promptTemplate: '详细分析图片,包括:物体识别、场景理解、颜色分析、构图评估。返回结构化结果。',
586
+ providers: [
587
+ {
588
+ providerId: 'siliconflow-default',
589
+ models: ['THUDM/GLM-4.1V-9B-Thinking'],
590
+ priority: 1,
591
+ enabled: true,
592
+ },
593
+ {
594
+ providerId: 'openai-default',
595
+ models: ['gpt-4o'],
596
+ priority: 2,
597
+ enabled: false,
598
+ },
599
+ {
600
+ providerId: 'anthropic-default',
601
+ models: ['claude-3-5-sonnet-20241022'],
602
+ priority: 3,
603
+ enabled: false,
410
604
  },
411
605
  ],
412
606
  },
package/types/update.ts CHANGED
@@ -51,6 +51,18 @@ export interface UpdateCheckResult {
51
51
  source: string
52
52
  }
53
53
 
54
+ export type UpdateUserAction = 'update-now' | 'skip' | 'remind-later'
55
+
56
+ export interface CachedUpdateRecord {
57
+ release: GitHubRelease
58
+ channel: AppPreviewChannel
59
+ status: 'pending' | 'skipped' | 'snoozed' | 'acknowledged'
60
+ fetchedAt: number
61
+ snoozeUntil?: number | null
62
+ tag: string
63
+ source: string
64
+ }
65
+
54
66
  /**
55
67
  * Custom provider definition used to extend the updater beyond the built-ins.
56
68
  */
@@ -1,207 +0,0 @@
1
- import { clipboard } from 'electron'
2
-
3
- /**
4
- * Represents a file object from the clipboard
5
- */
6
- interface ClipboardFileInfo {
7
- /** File data as a buffer */
8
- buffer: Buffer
9
- /** MIME type of the file */
10
- mimetype: string
11
- /** Original filename */
12
- originalname: string
13
- }
14
-
15
- /**
16
- * Possible return types from clipboard file operations
17
- */
18
- export type ClipboardFileResult = string | ClipboardFileInfo
19
-
20
- /**
21
- * Utility class for handling clipboard file operations across different platforms
22
- */
23
- export class ClipboardHelper {
24
- /**
25
- * Check if the current platform is macOS
26
- * @private
27
- */
28
- private get isMac(): boolean {
29
- return process.platform === 'darwin'
30
- }
31
-
32
- /**
33
- * Generate a unique identifier with specified length and radix
34
- * @param length - Length of the generated string
35
- * @param radix - Radix for number conversion (default: 16)
36
- * @returns Generated unique identifier
37
- * @private
38
- */
39
- private generateUuid(length: number, radix: number = 16): string {
40
- const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('')
41
- const uuid: string[] = []
42
- let i: number
43
-
44
- radix = radix || chars.length
45
-
46
- if (length) {
47
- for (i = 0; i < length; i++) {
48
- uuid[i] = chars[0 | (Math.random() * radix)]
49
- }
50
- }
51
- else {
52
- let r: number
53
- uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-'
54
- uuid[14] = '4'
55
-
56
- for (i = 0; i < 36; i++) {
57
- if (!uuid[i]) {
58
- r = 0 | (Math.random() * 16)
59
- uuid[i] = chars[i === 19 ? (r & 0x3) | 0x8 : r]
60
- }
61
- }
62
- }
63
-
64
- return uuid.join('')
65
- }
66
-
67
- /**
68
- * Process clipboard files on macOS platform
69
- * @returns Array of file paths or file info objects
70
- * @private
71
- */
72
- private getClipboardFilesMac(): ClipboardFileResult[] {
73
- let filePath: ClipboardFileResult[] = []
74
-
75
- if (clipboard.has('NSFilenamesPboardType')) {
76
- // Handle multiple files
77
- const tagContent = clipboard.read('NSFilenamesPboardType').match(/<string>.*<\/string>/g)
78
- filePath = tagContent ? tagContent.map(item => item.replace(/<string>|<\/string>/g, '')) : []
79
- }
80
- else {
81
- // Handle single file
82
- const clipboardImage = clipboard.readImage('clipboard')
83
- if (!clipboardImage.isEmpty()) {
84
- // Handle image from clipboard
85
- const png = clipboardImage.toPNG()
86
- const fileInfo: ClipboardFileInfo = {
87
- buffer: png,
88
- mimetype: 'image/png',
89
- originalname: `${this.generateUuid(8, 16)}.png`,
90
- }
91
- filePath = [fileInfo]
92
- }
93
- else {
94
- // Handle single file path
95
- const fileUrl = clipboard.read('public.file-url')
96
- if (fileUrl) {
97
- filePath = [fileUrl.replace('file://', '')].filter(item => item)
98
- }
99
- }
100
- }
101
-
102
- return filePath
103
- }
104
-
105
- /**
106
- * Process clipboard files on Windows platform
107
- * @returns Array of file paths or file info objects
108
- * @private
109
- */
110
- private getClipboardFilesWindows(): ClipboardFileResult[] {
111
- let filePath: ClipboardFileResult[] = []
112
-
113
- if (clipboard.has('CF_HDROP')) {
114
- // Handle multiple files
115
- const rawFilePathStr = clipboard.readBuffer('CF_HDROP').toString('ucs2') || ''
116
- let formatFilePathStr = [...rawFilePathStr]
117
- .filter((_, index) => rawFilePathStr.charCodeAt(index) !== 0)
118
- .join('')
119
- .replace(/\\/g, '\\\\')
120
-
121
- const drivePrefix = formatFilePathStr.match(/[a-z]:\\/i)
122
-
123
- if (drivePrefix) {
124
- const drivePrefixIndex = formatFilePathStr.indexOf(drivePrefix[0])
125
- if (drivePrefixIndex !== 0) {
126
- formatFilePathStr = formatFilePathStr.substring(drivePrefixIndex)
127
- }
128
- filePath = formatFilePathStr
129
- .split(drivePrefix[0])
130
- .filter(item => item)
131
- .map(item => drivePrefix[0] + item)
132
- }
133
- }
134
- else {
135
- // Handle single file
136
- const clipboardImage = clipboard.readImage('clipboard')
137
- if (!clipboardImage.isEmpty()) {
138
- // Handle image from clipboard
139
- const png = clipboardImage.toPNG()
140
- const fileInfo: ClipboardFileInfo = {
141
- buffer: png,
142
- mimetype: 'image/png',
143
- originalname: `${this.generateUuid(8, 16)}.png`,
144
- }
145
- filePath = [fileInfo]
146
- }
147
- else {
148
- // Handle single file path
149
- try {
150
- const fileName = clipboard.readBuffer('FileNameW').toString('ucs2').replace(new RegExp(String.fromCharCode(0), 'g'), '')
151
- if (fileName) {
152
- filePath = [fileName]
153
- }
154
- }
155
- catch (error) {
156
- // Ignore read errors for non-file clipboard content
157
- }
158
- }
159
- }
160
-
161
- return filePath
162
- }
163
-
164
- /**
165
- * Retrieves file paths or file information from the system clipboard
166
- *
167
- * This method handles both file paths and images from the clipboard across different platforms.
168
- * On macOS, it uses NSFilenamesPboardType for multiple files and public.file-url for single files.
169
- * On Windows, it uses CF_HDROP for multiple files and FileNameW for single files.
170
- *
171
- * @returns Array of file paths (strings) or file info objects containing buffer data for images
172
- * @throws {Error} When clipboard access fails or platform is unsupported
173
- *
174
- * @example
175
- * ```typescript
176
- * const clipboardHelper = new ClipboardHelper()
177
- * const files = clipboardHelper.getClipboardFiles()
178
- *
179
- * files.forEach(file => {
180
- * if (typeof file === 'string') {
181
- * console.log('File path:', file)
182
- * } else {
183
- * console.log('Image file:', file.originalname, 'Size:', file.buffer.length)
184
- * }
185
- * })
186
- * ```
187
- */
188
- public getClipboardFiles(): ClipboardFileResult[] {
189
- try {
190
- if (this.isMac) {
191
- return this.getClipboardFilesMac()
192
- }
193
- else {
194
- return this.getClipboardFilesWindows()
195
- }
196
- }
197
- catch (error) {
198
- console.error('Failed to read clipboard files:', error)
199
- return []
200
- }
201
- }
202
- }
203
-
204
- /**
205
- * Default export of ClipboardHelper class for convenience
206
- */
207
- export default ClipboardHelper