@talex-touch/utils 1.0.32 → 1.0.33

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.
@@ -1,3 +1,4 @@
1
+ /* eslint-disable no-console */
1
2
  /**
2
3
  * @module polling
3
4
  * A high-precision, efficient, singleton polling service for scheduling periodic tasks.
@@ -75,7 +76,7 @@ export class PollingService {
75
76
  nextRunMs,
76
77
  })
77
78
 
78
- console.log(`[PollingService] Task '${id}' registered to run every ${options.interval} ${options.unit}.`)
79
+ console.debug(`[PollingService] Task '${id}' registered to run every ${options.interval} ${options.unit}.`)
79
80
 
80
81
  if (this.isRunning) {
81
82
  this._reschedule()
package/core-box/index.ts CHANGED
@@ -5,3 +5,4 @@ export * from './preview/index'
5
5
  * Search box core functionality package
6
6
  */
7
7
  export * from './tuff/index'
8
+ export * from './recommendation'
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Time-based usage pattern context for recommendation matching.
3
+ */
4
+ export interface TimePattern {
5
+ /** Hour of day (0-23) */
6
+ hourOfDay: number
7
+ /** Day of week (0-6, 0=Sunday) */
8
+ dayOfWeek: number
9
+ /** Whether current time falls within working hours (9-18, weekdays) */
10
+ isWorkingHours: boolean
11
+ /** Broad time categorization */
12
+ timeSlot: 'morning' | 'afternoon' | 'evening' | 'night'
13
+ }
14
+
15
+ /**
16
+ * Complete contextual signal for recommendation matching.
17
+ * Gathered from system state, clipboard, and active applications.
18
+ */
19
+ export interface ContextSignal {
20
+ time: TimePattern
21
+ clipboard?: {
22
+ type: string
23
+ /** Hashed content for privacy (not original text) */
24
+ content: string
25
+ timestamp: number
26
+ contentType?: 'url' | 'text' | 'code' | 'file'
27
+ meta?: {
28
+ isUrl?: boolean
29
+ urlDomain?: string
30
+ textLength?: number
31
+ fileExtension?: string
32
+ fileType?: 'code' | 'text' | 'image' | 'document' | 'other'
33
+ language?: string
34
+ }
35
+ }
36
+ foregroundApp?: {
37
+ bundleId: string
38
+ name: string
39
+ }
40
+ systemState?: {
41
+ isOnline: boolean
42
+ batteryLevel: number
43
+ isDNDEnabled: boolean
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Scored recommendation item from recommendation engine.
49
+ */
50
+ export interface ScoredItem {
51
+ sourceId: string
52
+ itemId: string
53
+ score: number
54
+ source: 'frequent' | 'time-based' | 'recent' | 'trending' | 'context'
55
+ reason?: string
56
+ }
57
+
58
+ /**
59
+ * Recommendation badge display configuration for UI rendering.
60
+ */
61
+ export interface RecommendationBadge {
62
+ text: string
63
+ icon: string
64
+ variant: 'frequent' | 'intelligent' | 'recent' | 'trending'
65
+ }
66
+
67
+ /**
68
+ * Enhanced item metadata for intelligent recommendations.
69
+ * Attached to TuffItem.meta for rendering and filtering.
70
+ */
71
+ export interface RecommendationMetadata {
72
+ score: number
73
+ source: ScoredItem['source']
74
+ reason: string
75
+ isIntelligent: boolean
76
+ badge: RecommendationBadge
77
+ }
package/electron/index.ts CHANGED
@@ -1,3 +1,3 @@
1
- export * from './clipboard-helper'
2
1
  export * from './download-manager'
3
2
  export * from './env-tool'
3
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@talex-touch/utils",
3
- "version": "1.0.32",
3
+ "version": "1.0.33",
4
4
  "private": false,
5
5
  "description": "Tuff series utils",
6
6
  "author": "TalexDreamSoul",
@@ -0,0 +1,216 @@
1
+ # Plugin SDK 重构说明
2
+
3
+ ## 概述
4
+
5
+ Plugin SDK 已重构为统一的工厂函数模式,参考 `DivisionBoxSDK` 的设计。旧版本 API 已废弃,调用时会抛出错误。
6
+
7
+ ## 新的 SDK 结构
8
+
9
+ ### 1. BoxSDK - CoreBox 窗口控制
10
+
11
+ 控制 CoreBox 窗口的显示、大小和输入框状态。
12
+
13
+ ```typescript
14
+ // 隐藏/显示 CoreBox
15
+ plugin.box.hide()
16
+ plugin.box.show()
17
+
18
+ // 扩展窗口(显示更多结果)
19
+ plugin.box.expand({ length: 10 })
20
+ plugin.box.expand({ forceMax: true })
21
+
22
+ // 收缩窗口
23
+ plugin.box.shrink()
24
+
25
+ // 控制输入框
26
+ plugin.box.hideInput()
27
+ plugin.box.showInput()
28
+
29
+ // 获取当前输入
30
+ const input = await plugin.box.getInput()
31
+ ```
32
+
33
+ ### 2. FeatureSDK - 搜索结果管理
34
+
35
+ 管理插件推送的搜索结果项(TuffItems)。
36
+
37
+ ```typescript
38
+ // 推送多个结果
39
+ plugin.feature.pushItems([
40
+ { id: 'item-1', title: { text: 'Result 1' }, ... },
41
+ { id: 'item-2', title: { text: 'Result 2' }, ... }
42
+ ])
43
+
44
+ // 更新单个结果
45
+ plugin.feature.updateItem('item-1', {
46
+ title: { text: 'Updated Title' }
47
+ })
48
+
49
+ // 删除单个结果
50
+ plugin.feature.removeItem('item-1')
51
+
52
+ // 清空所有结果
53
+ plugin.feature.clearItems()
54
+
55
+ // 获取所有结果
56
+ const items = plugin.feature.getItems()
57
+
58
+ // 监听输入变化(实时搜索)
59
+ const unsubscribe = plugin.feature.onInputChange((input) => {
60
+ console.log('User typed:', input)
61
+ // 执行实时搜索
62
+ performSearch(input)
63
+ })
64
+
65
+ // 取消监听
66
+ unsubscribe()
67
+ ```
68
+
69
+ ## 废弃的 API
70
+
71
+ 以下 API 已废弃,调用时会抛出错误:
72
+
73
+ ### 旧的 Box API
74
+ ```typescript
75
+ // ❌ 废弃
76
+ plugin.$box.hide()
77
+ plugin.$box.show()
78
+
79
+ // ✅ 使用新 API
80
+ plugin.box.hide()
81
+ plugin.box.show()
82
+ ```
83
+
84
+ ### 旧的 Feature API
85
+ ```typescript
86
+ // ❌ 废弃
87
+ plugin.pushItems(items)
88
+ plugin.clearItems()
89
+ plugin.getItems()
90
+
91
+ // ✅ 使用新 API
92
+ plugin.feature.pushItems(items)
93
+ plugin.feature.clearItems()
94
+ plugin.feature.getItems()
95
+ ```
96
+
97
+ ## 迁移指南
98
+
99
+ ### 1. 更新 Box 控制代码
100
+
101
+ **旧代码:**
102
+ ```typescript
103
+ plugin.$box.hide()
104
+ plugin.$box.show()
105
+ ```
106
+
107
+ **新代码:**
108
+ ```typescript
109
+ plugin.box.hide()
110
+ plugin.box.show()
111
+ plugin.box.expand({ length: 10 })
112
+ plugin.box.shrink()
113
+ ```
114
+
115
+ ### 2. 更新搜索结果管理
116
+
117
+ **旧代码:**
118
+ ```typescript
119
+ plugin.pushItems([...])
120
+ plugin.clearItems()
121
+ const items = plugin.getItems()
122
+ ```
123
+
124
+ **新代码:**
125
+ ```typescript
126
+ plugin.feature.pushItems([...])
127
+ plugin.feature.updateItem('id', { ... })
128
+ plugin.feature.removeItem('id')
129
+ plugin.feature.clearItems()
130
+ const items = plugin.feature.getItems()
131
+ ```
132
+
133
+ ### 3. 添加实时搜索支持
134
+
135
+ **新功能:**
136
+ ```typescript
137
+ // 在插件初始化时注册监听器
138
+ onInit(context) {
139
+ context.utils.feature.onInputChange((input) => {
140
+ // 用户输入变化时触发
141
+ this.performRealTimeSearch(input)
142
+ })
143
+ }
144
+ ```
145
+
146
+ ## 完整示例
147
+
148
+ ```typescript
149
+ export default {
150
+ onInit(context) {
151
+ const { feature, box } = context.utils
152
+
153
+ // 监听输入变化
154
+ feature.onInputChange((input) => {
155
+ if (input.length > 2) {
156
+ // 执行搜索
157
+ const results = performSearch(input)
158
+ feature.pushItems(results)
159
+ } else {
160
+ feature.clearItems()
161
+ }
162
+ })
163
+ },
164
+
165
+ onFeatureTriggered(featureId, query, feature) {
166
+ const { feature: featureSDK, box } = this.context.utils
167
+
168
+ // 推送结果
169
+ featureSDK.pushItems([
170
+ {
171
+ id: 'result-1',
172
+ title: { text: 'Search Result' },
173
+ subtitle: { text: 'Description' },
174
+ source: { id: this.pluginName, name: this.pluginName }
175
+ }
176
+ ])
177
+
178
+ // 扩展窗口显示结果
179
+ box.expand({ length: 5 })
180
+
181
+ // 3秒后隐藏
182
+ setTimeout(() => {
183
+ box.hide()
184
+ }, 3000)
185
+ }
186
+ }
187
+ ```
188
+
189
+ ## 技术细节
190
+
191
+ ### SDK 工厂函数
192
+
193
+ 所有 SDK 都通过工厂函数创建:
194
+
195
+ - `createBoxSDK(channel)` - 创建 Box SDK 实例
196
+ - `createFeatureSDK(boxItems, channel)` - 创建 Feature SDK 实例
197
+ - `createDivisionBoxSDK(channel)` - 创建 DivisionBox SDK 实例
198
+
199
+ ### IPC 通道
200
+
201
+ 新增的 IPC 通道:
202
+
203
+ - `core-box:hide-input` - 隐藏输入框
204
+ - `core-box:show-input` - 显示输入框
205
+ - `core-box:get-input` - 获取当前输入值
206
+ - `core-box:input-changed` - 输入变化广播(主进程 → 插件)
207
+ - `core-box:set-input-visibility` - 设置输入框可见性(主进程 → 渲染进程)
208
+ - `core-box:request-input-value` - 请求输入值(主进程 → 渲染进程)
209
+
210
+ ### 架构优势
211
+
212
+ 1. **统一的 API 风格** - 所有 SDK 使用相同的工厂函数模式
213
+ 2. **更好的类型安全** - TypeScript 类型定义完整
214
+ 3. **功能分离** - Box 控制和 Feature 管理职责清晰
215
+ 4. **扩展性强** - 易于添加新功能
216
+ 5. **向后不兼容** - 强制迁移到新 API,避免技术债务
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Box SDK for Plugin Development
3
+ *
4
+ * Provides a unified API for plugins to control the CoreBox window behavior,
5
+ * including visibility, size, input field control, and input value access.
6
+ */
7
+
8
+ /**
9
+ * Expand options for CoreBox window
10
+ */
11
+ export interface BoxExpandOptions {
12
+ /** Number of items to show (affects window height) */
13
+ length?: number
14
+ /** Force maximum expansion */
15
+ forceMax?: boolean
16
+ }
17
+
18
+ /**
19
+ * Box SDK interface for plugins
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * // Hide CoreBox
24
+ * plugin.box.hide()
25
+ *
26
+ * // Show CoreBox
27
+ * plugin.box.show()
28
+ *
29
+ * // Expand to show 10 items
30
+ * plugin.box.expand({ length: 10 })
31
+ *
32
+ * // Get current input
33
+ * const input = plugin.box.getInput()
34
+ * ```
35
+ */
36
+ export interface BoxSDK {
37
+ /**
38
+ * Hides the CoreBox window
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * plugin.box.hide()
43
+ * ```
44
+ */
45
+ hide(): void
46
+
47
+ /**
48
+ * Shows the CoreBox window
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * plugin.box.show()
53
+ * ```
54
+ */
55
+ show(): void
56
+
57
+ /**
58
+ * Expands the CoreBox window
59
+ *
60
+ * @param options - Optional expansion configuration
61
+ *
62
+ * @example
63
+ * ```typescript
64
+ * // Expand to show 10 items
65
+ * plugin.box.expand({ length: 10 })
66
+ *
67
+ * // Force maximum expansion
68
+ * plugin.box.expand({ forceMax: true })
69
+ *
70
+ * // Default expansion
71
+ * plugin.box.expand()
72
+ * ```
73
+ */
74
+ expand(options?: BoxExpandOptions): Promise<void>
75
+
76
+ /**
77
+ * Shrinks the CoreBox window to compact size
78
+ *
79
+ * @example
80
+ * ```typescript
81
+ * plugin.box.shrink()
82
+ * ```
83
+ */
84
+ shrink(): Promise<void>
85
+
86
+ /**
87
+ * Hides the input field in CoreBox
88
+ *
89
+ * @example
90
+ * ```typescript
91
+ * plugin.box.hideInput()
92
+ * ```
93
+ */
94
+ hideInput(): Promise<void>
95
+
96
+ /**
97
+ * Shows the input field in CoreBox
98
+ *
99
+ * @example
100
+ * ```typescript
101
+ * plugin.box.showInput()
102
+ * ```
103
+ */
104
+ showInput(): Promise<void>
105
+
106
+ /**
107
+ * Gets the current input value from CoreBox search field
108
+ *
109
+ * @returns Promise resolving to the current input string
110
+ *
111
+ * @example
112
+ * ```typescript
113
+ * const input = await plugin.box.getInput()
114
+ * console.log('Current input:', input)
115
+ * ```
116
+ */
117
+ getInput(): Promise<string>
118
+ }
119
+
120
+ /**
121
+ * Creates a Box SDK instance for plugin use
122
+ *
123
+ * @param channel - The plugin channel bridge for IPC communication
124
+ * @returns Configured Box SDK instance
125
+ *
126
+ * @internal
127
+ */
128
+ export function createBoxSDK(channel: any): BoxSDK {
129
+ const sendFn = channel.sendToMain || channel.send
130
+
131
+ if (!sendFn) {
132
+ throw new Error('[Box SDK] Channel send function not available')
133
+ }
134
+
135
+ return {
136
+ hide(): void {
137
+ sendFn('core-box:hide').catch((error: any) => {
138
+ console.error('[Box SDK] Failed to hide CoreBox:', error)
139
+ })
140
+ },
141
+
142
+ show(): void {
143
+ sendFn('core-box:show').catch((error: any) => {
144
+ console.error('[Box SDK] Failed to show CoreBox:', error)
145
+ })
146
+ },
147
+
148
+ async expand(options?: BoxExpandOptions): Promise<void> {
149
+ try {
150
+ await sendFn('core-box:expand', options || {})
151
+ } catch (error) {
152
+ console.error('[Box SDK] Failed to expand CoreBox:', error)
153
+ throw error
154
+ }
155
+ },
156
+
157
+ async shrink(): Promise<void> {
158
+ try {
159
+ await sendFn('core-box:expand', { mode: 'collapse' })
160
+ } catch (error) {
161
+ console.error('[Box SDK] Failed to shrink CoreBox:', error)
162
+ throw error
163
+ }
164
+ },
165
+
166
+ async hideInput(): Promise<void> {
167
+ try {
168
+ await sendFn('core-box:hide-input')
169
+ } catch (error) {
170
+ console.error('[Box SDK] Failed to hide input:', error)
171
+ throw error
172
+ }
173
+ },
174
+
175
+ async showInput(): Promise<void> {
176
+ try {
177
+ await sendFn('core-box:show-input')
178
+ } catch (error) {
179
+ console.error('[Box SDK] Failed to show input:', error)
180
+ throw error
181
+ }
182
+ },
183
+
184
+ async getInput(): Promise<string> {
185
+ try {
186
+ const result = await sendFn('core-box:get-input')
187
+ return result?.data?.input || result?.input || ''
188
+ } catch (error) {
189
+ console.error('[Box SDK] Failed to get input:', error)
190
+ throw error
191
+ }
192
+ }
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Hook for using Box SDK in plugin context
198
+ *
199
+ * @returns Box SDK instance
200
+ *
201
+ * @example
202
+ * ```typescript
203
+ * const box = useBox()
204
+ *
205
+ * box.hide()
206
+ * box.expand({ length: 10 })
207
+ * const input = await box.getInput()
208
+ * ```
209
+ */
210
+ export function useBox(): BoxSDK {
211
+ // @ts-ignore - window.$channel is injected by the plugin system
212
+ const channel = window.$channel
213
+
214
+ if (!channel) {
215
+ throw new Error('[Box SDK] Channel not available. Make sure this is called in a plugin context.')
216
+ }
217
+
218
+ return createBoxSDK(channel)
219
+ }
@@ -0,0 +1,235 @@
1
+ /**
2
+ * Feature SDK for Plugin Development
3
+ *
4
+ * Provides a unified API for plugins to manage search result items (TuffItems)
5
+ * in the CoreBox interface. This SDK handles item lifecycle, updates, and
6
+ * input change notifications.
7
+ */
8
+
9
+ import type { TuffItem } from '../../core-box/tuff'
10
+
11
+ /**
12
+ * Input change event handler
13
+ */
14
+ export type InputChangeHandler = (input: string) => void
15
+
16
+ /**
17
+ * Feature SDK interface for plugins
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * // Push items to CoreBox
22
+ * plugin.feature.pushItems([
23
+ * { id: 'item-1', title: 'Result 1', ... },
24
+ * { id: 'item-2', title: 'Result 2', ... }
25
+ * ])
26
+ *
27
+ * // Update a specific item
28
+ * plugin.feature.updateItem('item-1', { title: 'Updated Title' })
29
+ *
30
+ * // Listen for input changes
31
+ * plugin.feature.onInputChange((input) => {
32
+ * console.log('User typed:', input)
33
+ * })
34
+ * ```
35
+ */
36
+ export interface FeatureSDK {
37
+ /**
38
+ * Pushes multiple items to the CoreBox search results
39
+ *
40
+ * @param items - Array of TuffItem objects to display
41
+ *
42
+ * @example
43
+ * ```typescript
44
+ * plugin.feature.pushItems([
45
+ * {
46
+ * id: 'calc-result',
47
+ * title: { text: '42' },
48
+ * subtitle: { text: 'Calculation result' },
49
+ * source: { id: 'calculator', name: 'Calculator' }
50
+ * }
51
+ * ])
52
+ * ```
53
+ */
54
+ pushItems(items: TuffItem[]): void
55
+
56
+ /**
57
+ * Updates a specific item by ID
58
+ *
59
+ * @param id - The unique ID of the item to update
60
+ * @param updates - Partial TuffItem with fields to update
61
+ *
62
+ * @example
63
+ * ```typescript
64
+ * // Update title and subtitle
65
+ * plugin.feature.updateItem('item-1', {
66
+ * title: { text: 'New Title' },
67
+ * subtitle: { text: 'Updated subtitle' }
68
+ * })
69
+ * ```
70
+ */
71
+ updateItem(id: string, updates: Partial<TuffItem>): void
72
+
73
+ /**
74
+ * Removes a specific item by ID
75
+ *
76
+ * @param id - The unique ID of the item to remove
77
+ *
78
+ * @example
79
+ * ```typescript
80
+ * plugin.feature.removeItem('item-1')
81
+ * ```
82
+ */
83
+ removeItem(id: string): void
84
+
85
+ /**
86
+ * Clears all items pushed by this plugin
87
+ *
88
+ * @example
89
+ * ```typescript
90
+ * plugin.feature.clearItems()
91
+ * ```
92
+ */
93
+ clearItems(): void
94
+
95
+ /**
96
+ * Gets all items currently pushed by this plugin
97
+ *
98
+ * @returns Array of TuffItem objects
99
+ *
100
+ * @example
101
+ * ```typescript
102
+ * const items = plugin.feature.getItems()
103
+ * console.log(`Currently showing ${items.length} items`)
104
+ * ```
105
+ */
106
+ getItems(): TuffItem[]
107
+
108
+ /**
109
+ * Registers a listener for input changes in the CoreBox search field
110
+ *
111
+ * @param handler - Callback function invoked when input changes
112
+ * @returns Unsubscribe function
113
+ *
114
+ * @example
115
+ * ```typescript
116
+ * const unsubscribe = plugin.feature.onInputChange((input) => {
117
+ * console.log('User typed:', input)
118
+ * // Perform real-time search
119
+ * performSearch(input)
120
+ * })
121
+ *
122
+ * // Later, unsubscribe
123
+ * unsubscribe()
124
+ * ```
125
+ */
126
+ onInputChange(handler: InputChangeHandler): () => void
127
+ }
128
+
129
+ /**
130
+ * Creates a Feature SDK instance for plugin use
131
+ *
132
+ * @param boxItemsAPI - The boxItems API object from plugin context
133
+ * @param channel - The plugin channel bridge for IPC communication
134
+ * @returns Configured Feature SDK instance
135
+ *
136
+ * @internal
137
+ */
138
+ export function createFeatureSDK(boxItemsAPI: any, channel: any): FeatureSDK {
139
+ const inputChangeHandlers: Set<InputChangeHandler> = new Set()
140
+
141
+ // Register listener for input change events from main process
142
+ const registerListener = () => {
143
+ if (channel.onMain) {
144
+ // Main process plugin context
145
+ channel.onMain('core-box:input-changed', (event: any) => {
146
+ const input = event.data?.input || event.input || ''
147
+ inputChangeHandlers.forEach(handler => handler(input))
148
+ })
149
+ } else if (channel.on) {
150
+ // Renderer process context
151
+ channel.on('core-box:input-changed', (data: any) => {
152
+ const input = data?.input || data || ''
153
+ inputChangeHandlers.forEach(handler => handler(input))
154
+ })
155
+ }
156
+ }
157
+
158
+ registerListener()
159
+
160
+ return {
161
+ pushItems(items: TuffItem[]): void {
162
+ if (!boxItemsAPI || !boxItemsAPI.pushItems) {
163
+ throw new Error('[Feature SDK] boxItems API not available')
164
+ }
165
+ boxItemsAPI.pushItems(items)
166
+ },
167
+
168
+ updateItem(id: string, updates: Partial<TuffItem>): void {
169
+ if (!boxItemsAPI || !boxItemsAPI.update) {
170
+ throw new Error('[Feature SDK] boxItems API not available')
171
+ }
172
+ boxItemsAPI.update(id, updates)
173
+ },
174
+
175
+ removeItem(id: string): void {
176
+ if (!boxItemsAPI || !boxItemsAPI.remove) {
177
+ throw new Error('[Feature SDK] boxItems API not available')
178
+ }
179
+ boxItemsAPI.remove(id)
180
+ },
181
+
182
+ clearItems(): void {
183
+ if (!boxItemsAPI || !boxItemsAPI.clear) {
184
+ throw new Error('[Feature SDK] boxItems API not available')
185
+ }
186
+ boxItemsAPI.clear()
187
+ },
188
+
189
+ getItems(): TuffItem[] {
190
+ if (!boxItemsAPI || !boxItemsAPI.getItems) {
191
+ throw new Error('[Feature SDK] boxItems API not available')
192
+ }
193
+ return boxItemsAPI.getItems()
194
+ },
195
+
196
+ onInputChange(handler: InputChangeHandler): () => void {
197
+ inputChangeHandlers.add(handler)
198
+
199
+ return () => {
200
+ inputChangeHandlers.delete(handler)
201
+ }
202
+ }
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Hook for using Feature SDK in plugin context
208
+ *
209
+ * @returns Feature SDK instance
210
+ *
211
+ * @example
212
+ * ```typescript
213
+ * const feature = useFeature()
214
+ *
215
+ * feature.pushItems([
216
+ * { id: '1', title: { text: 'Item 1' }, ... }
217
+ * ])
218
+ * ```
219
+ */
220
+ export function useFeature(): FeatureSDK {
221
+ // @ts-ignore - window.$boxItems and window.$channel are injected by the plugin system
222
+ const boxItemsAPI = window.$boxItems
223
+ // @ts-ignore
224
+ const channel = window.$channel
225
+
226
+ if (!boxItemsAPI) {
227
+ throw new Error('[Feature SDK] boxItems API not available. Make sure this is called in a plugin context.')
228
+ }
229
+
230
+ if (!channel) {
231
+ throw new Error('[Feature SDK] Channel not available. Make sure this is called in a plugin context.')
232
+ }
233
+
234
+ return createFeatureSDK(boxItemsAPI, channel)
235
+ }
@@ -7,10 +7,12 @@ export interface ITouchSDK {
7
7
 
8
8
  // Note: Window.$touchSDK is declared in ../preload.ts to avoid duplicate declarations
9
9
 
10
+ export * from './box-sdk'
10
11
  export * from './channel'
11
12
  export * from './clipboard'
12
13
  export * from './core-box'
13
14
  export * from './division-box'
15
+ export * from './feature-sdk'
14
16
  export { createFeaturesManager, useFeatures } from './features'
15
17
 
16
18
  export * from './hooks/index'
@@ -183,6 +183,18 @@ export interface IPluginUtils {
183
183
  */
184
184
  divisionBox: import('./division-box').DivisionBoxSDK
185
185
 
186
+ /**
187
+ * Box SDK for controlling CoreBox window behavior
188
+ * @see {@link BoxSDK}
189
+ */
190
+ box: import('./box-sdk').BoxSDK
191
+
192
+ /**
193
+ * Feature SDK for managing search result items
194
+ * @see {@link FeatureSDK}
195
+ */
196
+ feature: import('./feature-sdk').FeatureSDK
197
+
186
198
  /**
187
199
  * Opens a URL in the default browser
188
200
  * @param url - The URL to open
@@ -190,21 +202,22 @@ export interface IPluginUtils {
190
202
  openUrl: (url: string) => void
191
203
 
192
204
  /**
193
- * Pushes search result items to the search interface
194
- * @param items - Array of search result items to add
205
+ * @deprecated Use plugin.feature.pushItems() instead
206
+ * @throws Error indicating the API is deprecated
195
207
  */
196
- pushItems: (items: any[]) => void
208
+ pushItems: (items: any[]) => never
197
209
 
198
210
  /**
199
- * Clears all current search results
211
+ * @deprecated Use plugin.feature.clearItems() instead
212
+ * @throws Error indicating the API is deprecated
200
213
  */
201
- clearItems: () => void
214
+ clearItems: () => never
202
215
 
203
216
  /**
204
- * Gets all current search result items
205
- * @returns Array of current search result items
217
+ * @deprecated Use plugin.feature.getItems() instead
218
+ * @throws Error indicating the API is deprecated
206
219
  */
207
- getItems: () => any[]
220
+ getItems: () => never
208
221
 
209
222
  /**
210
223
  * Features manager for dynamic feature management
@@ -18,7 +18,7 @@ const defaultIntelligenceData: AISDKStorageData = {
18
18
  providers: [...DEFAULT_PROVIDERS],
19
19
  globalConfig: { ...DEFAULT_GLOBAL_CONFIG },
20
20
  capabilities: { ...DEFAULT_CAPABILITIES },
21
- version: 1,
21
+ version: 2,
22
22
  }
23
23
 
24
24
  const INTELLIGENCE_STORAGE_KEY = `storage:${StorageList.IntelligenceConfig}`
@@ -145,8 +145,9 @@ export async function migrateIntelligenceSettings(): Promise<void> {
145
145
  console.log('[Intelligence Storage] Starting migration check...')
146
146
  const currentData = intelligenceStorage.data
147
147
 
148
- if (!currentData.version || currentData.version < 1) {
149
- console.log('[Intelligence Storage] Migrating settings to version 1')
148
+ // Version 2: Force update capabilities to include all new ones
149
+ if (!currentData.version || currentData.version < 2) {
150
+ console.log('[Intelligence Storage] Migrating settings to version 2')
150
151
 
151
152
  const migratedProviders = currentData.providers.map(provider => ({
152
153
  ...provider,
@@ -169,24 +170,26 @@ export async function migrateIntelligenceSettings(): Promise<void> {
169
170
  cacheExpiration: currentData.globalConfig?.cacheExpiration ?? 3600,
170
171
  }
171
172
 
172
- const migratedCapabilities = currentData.capabilities ?? { ...DEFAULT_CAPABILITIES }
173
-
173
+ // Force update to latest DEFAULT_CAPABILITIES (version 2)
174
+ console.log('[Intelligence Storage] Updating capabilities to latest defaults')
175
+
174
176
  intelligenceStorage.applyData({
175
177
  providers: migratedProviders,
176
178
  globalConfig: migratedGlobalConfig,
177
- capabilities: migratedCapabilities,
178
- version: 1,
179
+ capabilities: { ...DEFAULT_CAPABILITIES },
180
+ version: 2,
179
181
  })
180
182
 
181
183
  await intelligenceStorage.saveToRemote({ force: true })
182
184
 
183
- console.log('[Intelligence Storage] Migration complete')
185
+ console.log('[Intelligence Storage] Migration to v2 complete, capabilities count:', Object.keys(DEFAULT_CAPABILITIES).length)
184
186
  }
185
187
  else {
186
188
  console.log('[Intelligence Storage] No migration needed, current version:', currentData.version)
187
189
  }
188
190
 
189
191
  console.log('[Intelligence Storage] Final providers count:', intelligenceStorage.data.providers.length)
192
+ console.log('[Intelligence Storage] Final capabilities count:', Object.keys(intelligenceStorage.data.capabilities).length)
190
193
  }
191
194
 
192
195
  /**
@@ -201,7 +204,7 @@ export async function resetIntelligenceConfig(): Promise<void> {
201
204
  providers: [...DEFAULT_PROVIDERS],
202
205
  globalConfig: { ...DEFAULT_GLOBAL_CONFIG },
203
206
  capabilities: { ...DEFAULT_CAPABILITIES },
204
- version: 1,
207
+ version: 2,
205
208
  })
206
209
 
207
210
  await intelligenceStorage.saveToRemote({ force: true })
@@ -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