@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.
- package/common/file-scan-utils.ts +0 -1
- package/common/storage/constants.ts +1 -0
- package/common/utils/polling.ts +2 -1
- package/core-box/index.ts +1 -0
- package/core-box/recommendation.ts +77 -0
- package/electron/index.ts +1 -1
- package/index.ts +1 -0
- package/market/constants.ts +95 -0
- package/market/index.ts +2 -0
- package/market/types.ts +118 -0
- package/package.json +1 -1
- package/plugin/index.ts +1 -0
- package/plugin/sdk/README.md +216 -0
- package/plugin/sdk/box-sdk.ts +219 -0
- package/plugin/sdk/feature-sdk.ts +235 -0
- package/plugin/sdk/index.ts +2 -0
- package/plugin/sdk/types.ts +21 -8
- package/plugin/widget.ts +25 -0
- package/renderer/storage/intelligence-storage.ts +12 -9
- package/renderer/storage/storage-subscription.ts +196 -0
- package/types/icon.ts +7 -0
- package/types/intelligence.ts +198 -4
- package/types/update.ts +12 -0
- package/electron/clipboard-helper.ts +0 -207
|
@@ -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
|
|
package/types/intelligence.ts
CHANGED
|
@@ -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', '
|
|
412
|
+
models: ['deepseek-ai/DeepSeek-OCR', 'THUDM/GLM-4.1V-9B-Thinking'],
|
|
407
413
|
priority: 1,
|
|
408
414
|
enabled: true,
|
|
409
|
-
|
|
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
|