@talex-touch/utils 1.0.33 → 1.0.35

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.
@@ -8,7 +8,6 @@
8
8
  */
9
9
 
10
10
  import type { FileScanOptions } from './file-scan-constants'
11
- // eslint-disable-next-line @typescript-eslint/no-var-requires
12
11
  import {
13
12
  BASE_BLACKLISTED_DIRS,
14
13
  BLACKLISTED_EXTENSIONS,
@@ -3,6 +3,7 @@ export enum StorageList {
3
3
  SHORTCUT_SETTING = 'shortcut-setting.ini',
4
4
  OPENERS = 'openers.json',
5
5
  IntelligenceConfig = 'aisdk-config',
6
+ MARKET_SOURCES = 'market-sources.json',
6
7
  }
7
8
 
8
9
  /**
package/index.ts CHANGED
@@ -11,3 +11,4 @@ export * from './types'
11
11
  export * from './types/download'
12
12
  export * from './types/icon'
13
13
  export * from './types/update'
14
+ export * from './market'
@@ -0,0 +1,95 @@
1
+ import { StorageList } from '../common/storage/constants'
2
+ import type {
3
+ MarketProviderDefinition,
4
+ MarketSourcesPayload,
5
+ MarketSourcesStorageInfo,
6
+ MarketProviderTrustLevel,
7
+ } from './types'
8
+
9
+ export const MARKET_SOURCES_STORAGE_KEY = StorageList.MARKET_SOURCES
10
+ export const MARKET_SOURCES_STORAGE_VERSION = 1
11
+
12
+ function defineProvider(
13
+ provider: Omit<MarketProviderDefinition, 'trustLevel'> & {
14
+ trustLevel?: MarketProviderTrustLevel
15
+ },
16
+ ): MarketProviderDefinition {
17
+ return {
18
+ trustLevel: provider.trustLevel ?? 'unverified',
19
+ ...provider,
20
+ }
21
+ }
22
+
23
+ export const DEFAULT_MARKET_PROVIDERS: MarketProviderDefinition[] = [
24
+ defineProvider({
25
+ id: 'talex-official',
26
+ name: 'Talex Official',
27
+ type: 'nexusStore',
28
+ url: 'https://raw.githubusercontent.com/talex-touch/tuff-official-plugins/main/plugins.json',
29
+ description: '官方插件市场,提供经过审核的核心插件。',
30
+ enabled: true,
31
+ priority: 100,
32
+ trustLevel: 'official',
33
+ readOnly: true,
34
+ config: {
35
+ manifestUrl:
36
+ 'https://raw.githubusercontent.com/talex-touch/tuff-official-plugins/main/plugins.json',
37
+ baseUrl: 'https://raw.githubusercontent.com/talex-touch/tuff-official-plugins/main/',
38
+ },
39
+ }),
40
+ defineProvider({
41
+ id: 'github-releases',
42
+ name: 'GitHub Releases',
43
+ type: 'repository',
44
+ description: '从 GitHub 仓库 releases / manifest 中读取插件。',
45
+ enabled: false,
46
+ priority: 80,
47
+ trustLevel: 'unverified',
48
+ config: {
49
+ platform: 'github',
50
+ apiBase: 'https://api.github.com',
51
+ },
52
+ }),
53
+ defineProvider({
54
+ id: 'gitee-repos',
55
+ name: 'Gitee 仓库',
56
+ type: 'repository',
57
+ description: 'Gitee 平台插件仓库,适合国内网络。',
58
+ enabled: false,
59
+ priority: 70,
60
+ trustLevel: 'unverified',
61
+ config: {
62
+ platform: 'gitee',
63
+ apiBase: 'https://gitee.com/api/v5',
64
+ },
65
+ }),
66
+ defineProvider({
67
+ id: 'npm-scope',
68
+ name: 'NPM 包',
69
+ type: 'npmPackage',
70
+ description: '基于 NPM 关键字或 scope 的插件发布渠道。',
71
+ enabled: false,
72
+ priority: 60,
73
+ trustLevel: 'unverified',
74
+ config: {
75
+ registryUrl: 'https://registry.npmjs.org',
76
+ keyword: 'talex-touch-plugin',
77
+ },
78
+ }),
79
+ ]
80
+
81
+ export const MARKET_SOURCES_STORAGE_INFO: MarketSourcesStorageInfo = {
82
+ storageKey: MARKET_SOURCES_STORAGE_KEY,
83
+ version: MARKET_SOURCES_STORAGE_VERSION,
84
+ }
85
+
86
+ export function createDefaultMarketSourcesPayload(): MarketSourcesPayload {
87
+ const clone = typeof structuredClone === 'function'
88
+ ? structuredClone(DEFAULT_MARKET_PROVIDERS)
89
+ : JSON.parse(JSON.stringify(DEFAULT_MARKET_PROVIDERS))
90
+
91
+ return {
92
+ version: MARKET_SOURCES_STORAGE_VERSION,
93
+ sources: clone,
94
+ }
95
+ }
@@ -0,0 +1,2 @@
1
+ export * from './types'
2
+ export * from './constants'
@@ -0,0 +1,118 @@
1
+ import type { StorageList } from '../common/storage/constants'
2
+
3
+ export type MarketProviderType = 'repository' | 'nexusStore' | 'npmPackage'
4
+
5
+ export type MarketProviderTrustLevel = 'official' | 'verified' | 'unverified'
6
+
7
+ export interface MarketProviderDefinition {
8
+ id: string
9
+ name: string
10
+ type: MarketProviderType
11
+ /**
12
+ * Base URL or identifier for the provider.
13
+ * Individual provider implementations can interpret this differently.
14
+ */
15
+ url?: string
16
+ /**
17
+ * Additional configuration object for provider specific options.
18
+ */
19
+ config?: Record<string, any>
20
+ description?: string
21
+ enabled: boolean
22
+ priority: number
23
+ trustLevel?: MarketProviderTrustLevel
24
+ tags?: string[]
25
+ /**
26
+ * Whether this provider should be treated as read-only (no install)
27
+ */
28
+ readOnly?: boolean
29
+ }
30
+
31
+ export interface MarketSourcesPayload {
32
+ /**
33
+ * Schema version, used for migrations.
34
+ */
35
+ version: number
36
+ sources: MarketProviderDefinition[]
37
+ }
38
+
39
+ export type MarketInstallInstruction =
40
+ | {
41
+ type: 'url'
42
+ url: string
43
+ format?: 'zip' | 'tar' | 'tgz' | 'tpex'
44
+ integrity?: string
45
+ }
46
+ | {
47
+ type: 'npm'
48
+ packageName: string
49
+ version?: string
50
+ registry?: string
51
+ }
52
+ | {
53
+ type: 'git'
54
+ repo: string
55
+ ref?: string
56
+ sparse?: boolean
57
+ }
58
+
59
+ export interface MarketPlugin {
60
+ id: string
61
+ name: string
62
+ version?: string
63
+ description?: string
64
+ category?: string
65
+ tags?: string[]
66
+ author?: string
67
+ icon?: string
68
+ metadata?: Record<string, unknown>
69
+ readmeUrl?: string
70
+ homepage?: string
71
+ downloadUrl?: string
72
+ install?: MarketInstallInstruction
73
+ providerId: string
74
+ providerName: string
75
+ providerType: MarketProviderType
76
+ providerTrustLevel: MarketProviderTrustLevel
77
+ trusted: boolean
78
+ official?: boolean
79
+ timestamp?: number | string
80
+ }
81
+
82
+ export interface MarketProviderResultMeta {
83
+ providerId: string
84
+ providerName: string
85
+ providerType: MarketProviderType
86
+ success: boolean
87
+ error?: string
88
+ fetchedAt: number
89
+ itemCount: number
90
+ }
91
+
92
+ export interface MarketProviderListOptions {
93
+ keyword?: string
94
+ force?: boolean
95
+ }
96
+
97
+ export interface MarketHttpRequestOptions {
98
+ url: string
99
+ method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
100
+ headers?: Record<string, string>
101
+ params?: Record<string, any>
102
+ data?: any
103
+ timeout?: number
104
+ responseType?: 'json' | 'text' | 'arraybuffer'
105
+ }
106
+
107
+ export interface MarketHttpResponse<T = unknown> {
108
+ status: number
109
+ statusText: string
110
+ headers: Record<string, string>
111
+ data: T
112
+ url: string
113
+ }
114
+
115
+ export interface MarketSourcesStorageInfo {
116
+ storageKey: StorageList
117
+ version: number
118
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@talex-touch/utils",
3
- "version": "1.0.33",
3
+ "version": "1.0.35",
4
4
  "private": false,
5
5
  "description": "Tuff series utils",
6
6
  "author": "TalexDreamSoul",
package/plugin/channel.ts CHANGED
@@ -10,6 +10,8 @@ import {
10
10
  DataCode,
11
11
  } from '../channel'
12
12
 
13
+ const CHANNEL_DEFAULT_TIMEOUT = 10_000
14
+
13
15
  let cachedIpcRenderer: IpcRenderer | null = null
14
16
 
15
17
  // 使用惰性解析避免在打包阶段静态引入 electron
@@ -197,6 +199,19 @@ class TouchChannel implements ITouchClientChannel {
197
199
  return true
198
200
  }
199
201
 
202
+ private formatPayloadPreview(payload: unknown): string {
203
+ if (payload === null || payload === undefined)
204
+ return String(payload)
205
+ if (typeof payload === 'string')
206
+ return payload.length > 200 ? `${payload.slice(0, 200)}…` : payload
207
+ try {
208
+ return JSON.stringify(payload)
209
+ }
210
+ catch {
211
+ return '[unserializable]'
212
+ }
213
+ }
214
+
200
215
  send(eventName: string, arg: any): Promise<any> {
201
216
  const uniqueId = `${new Date().getTime()}#${eventName}@${Math.random().toString(
202
217
  12,
@@ -207,7 +222,7 @@ class TouchChannel implements ITouchClientChannel {
207
222
  data: arg,
208
223
  sync: {
209
224
  timeStamp: new Date().getTime(),
210
- timeout: 10000,
225
+ timeout: CHANNEL_DEFAULT_TIMEOUT,
211
226
  id: uniqueId,
212
227
  },
213
228
  name: eventName,
@@ -218,10 +233,40 @@ class TouchChannel implements ITouchClientChannel {
218
233
  },
219
234
  } as RawStandardChannelData
220
235
 
221
- return new Promise((resolve) => {
222
- this.ipcRenderer.send('@plugin-process-message', data)
236
+ return new Promise((resolve, reject) => {
237
+ try {
238
+ this.ipcRenderer.send('@plugin-process-message', data)
239
+ }
240
+ catch (error) {
241
+ const errorMessage = error instanceof Error ? error.message : String(error)
242
+ console.error(
243
+ `[PluginChannel] Failed to send "${eventName}": ${errorMessage}`,
244
+ { payloadPreview: this.formatPayloadPreview(arg) },
245
+ )
246
+ reject(
247
+ Object.assign(
248
+ new Error(`Failed to send plugin channel message "${eventName}": ${errorMessage}`),
249
+ { code: 'plugin_channel_send_failed' },
250
+ ),
251
+ )
252
+ return
253
+ }
254
+
255
+ const timeoutMs = data.sync?.timeout ?? CHANNEL_DEFAULT_TIMEOUT
256
+ const timeoutHandle = setTimeout(() => {
257
+ if (!this.pendingMap.has(uniqueId))
258
+ return
259
+ this.pendingMap.delete(uniqueId)
260
+ const timeoutError = Object.assign(
261
+ new Error(`Plugin channel request "${eventName}" timed out after ${timeoutMs}ms`),
262
+ { code: 'plugin_channel_timeout' },
263
+ )
264
+ console.warn(timeoutError.message)
265
+ reject(timeoutError)
266
+ }, timeoutMs)
223
267
 
224
268
  this.pendingMap.set(uniqueId, (res: any) => {
269
+ clearTimeout(timeoutHandle)
225
270
  this.pendingMap.delete(uniqueId)
226
271
 
227
272
  resolve(res.data)
@@ -241,12 +286,26 @@ class TouchChannel implements ITouchClientChannel {
241
286
  },
242
287
  } as RawStandardChannelData
243
288
 
244
- const res = this.__parse_raw_data(void 0, this.ipcRenderer.sendSync('@plugin-process-message', data))!
289
+ try {
290
+ const res = this.__parse_raw_data(
291
+ void 0,
292
+ this.ipcRenderer.sendSync('@plugin-process-message', data),
293
+ )!
245
294
 
246
- if (res.header.status === 'reply')
247
- return res.data
295
+ if (res.header.status === 'reply')
296
+ return res.data
248
297
 
249
- return res
298
+ return res
299
+ }
300
+ catch (error) {
301
+ const errorMessage = error instanceof Error ? error.message : String(error)
302
+ console.error('[PluginChannel] Failed to sendSync message', {
303
+ eventName,
304
+ error: errorMessage,
305
+ payloadPreview: this.formatPayloadPreview(arg),
306
+ })
307
+ throw new Error(`Failed to sendSync plugin channel message "${eventName}": ${errorMessage}`)
308
+ }
250
309
  }
251
310
  }
252
311
 
package/plugin/index.ts CHANGED
@@ -498,3 +498,4 @@ export type { IPluginLogger, LogDataType, LogItem, LogLevel } from './log/types'
498
498
  export * from './providers'
499
499
  export * from './risk'
500
500
  export * from './sdk/index'
501
+ export * from './widget'
@@ -26,8 +26,10 @@ plugin.box.shrink()
26
26
  plugin.box.hideInput()
27
27
  plugin.box.showInput()
28
28
 
29
- // 获取当前输入
29
+ // 获取与设置输入
30
30
  const input = await plugin.box.getInput()
31
+ await plugin.box.setInput('Hello Touch!')
32
+ await plugin.box.clearInput()
31
33
  ```
32
34
 
33
35
  ### 2. FeatureSDK - 搜索结果管理
@@ -203,6 +205,8 @@ export default {
203
205
  - `core-box:hide-input` - 隐藏输入框
204
206
  - `core-box:show-input` - 显示输入框
205
207
  - `core-box:get-input` - 获取当前输入值
208
+ - `core-box:set-input` - 设置输入框内容
209
+ - `core-box:clear-input` - 清空输入框
206
210
  - `core-box:input-changed` - 输入变化广播(主进程 → 插件)
207
211
  - `core-box:set-input-visibility` - 设置输入框可见性(主进程 → 渲染进程)
208
212
  - `core-box:request-input-value` - 请求输入值(主进程 → 渲染进程)
@@ -1,9 +1,28 @@
1
1
  /**
2
2
  * Box SDK for Plugin Development
3
- *
3
+ *
4
4
  * Provides a unified API for plugins to control the CoreBox window behavior,
5
5
  * including visibility, size, input field control, and input value access.
6
6
  */
7
+ import { ensureRendererChannel } from './channel'
8
+
9
+ /**
10
+ * Clipboard content type flags for binary combination
11
+ */
12
+ export enum ClipboardType {
13
+ TEXT = 0b0001,
14
+ IMAGE = 0b0010,
15
+ FILE = 0b0100,
16
+ }
17
+
18
+ /**
19
+ * Preset clipboard type combinations
20
+ */
21
+ export const ClipboardTypePresets = {
22
+ TEXT_ONLY: ClipboardType.TEXT,
23
+ TEXT_AND_IMAGE: ClipboardType.TEXT | ClipboardType.IMAGE,
24
+ ALL: ClipboardType.TEXT | ClipboardType.IMAGE | ClipboardType.FILE,
25
+ } as const
7
26
 
8
27
  /**
9
28
  * Expand options for CoreBox window
@@ -17,18 +36,18 @@ export interface BoxExpandOptions {
17
36
 
18
37
  /**
19
38
  * Box SDK interface for plugins
20
- *
39
+ *
21
40
  * @example
22
41
  * ```typescript
23
42
  * // Hide CoreBox
24
43
  * plugin.box.hide()
25
- *
44
+ *
26
45
  * // Show CoreBox
27
46
  * plugin.box.show()
28
- *
47
+ *
29
48
  * // Expand to show 10 items
30
49
  * plugin.box.expand({ length: 10 })
31
- *
50
+ *
32
51
  * // Get current input
33
52
  * const input = plugin.box.getInput()
34
53
  * ```
@@ -36,93 +55,142 @@ export interface BoxExpandOptions {
36
55
  export interface BoxSDK {
37
56
  /**
38
57
  * Hides the CoreBox window
39
- *
58
+ *
40
59
  * @example
41
60
  * ```typescript
42
61
  * plugin.box.hide()
43
62
  * ```
44
63
  */
45
- hide(): void
64
+ hide: () => void
46
65
 
47
66
  /**
48
67
  * Shows the CoreBox window
49
- *
68
+ *
50
69
  * @example
51
70
  * ```typescript
52
71
  * plugin.box.show()
53
72
  * ```
54
73
  */
55
- show(): void
74
+ show: () => void
56
75
 
57
76
  /**
58
77
  * Expands the CoreBox window
59
- *
78
+ *
60
79
  * @param options - Optional expansion configuration
61
- *
80
+ *
62
81
  * @example
63
82
  * ```typescript
64
83
  * // Expand to show 10 items
65
84
  * plugin.box.expand({ length: 10 })
66
- *
85
+ *
67
86
  * // Force maximum expansion
68
87
  * plugin.box.expand({ forceMax: true })
69
- *
88
+ *
70
89
  * // Default expansion
71
90
  * plugin.box.expand()
72
91
  * ```
73
92
  */
74
- expand(options?: BoxExpandOptions): Promise<void>
93
+ expand: (options?: BoxExpandOptions) => Promise<void>
75
94
 
76
95
  /**
77
96
  * Shrinks the CoreBox window to compact size
78
- *
97
+ *
79
98
  * @example
80
99
  * ```typescript
81
100
  * plugin.box.shrink()
82
101
  * ```
83
102
  */
84
- shrink(): Promise<void>
103
+ shrink: () => Promise<void>
85
104
 
86
105
  /**
87
106
  * Hides the input field in CoreBox
88
- *
107
+ *
89
108
  * @example
90
109
  * ```typescript
91
110
  * plugin.box.hideInput()
92
111
  * ```
93
112
  */
94
- hideInput(): Promise<void>
113
+ hideInput: () => Promise<void>
95
114
 
96
115
  /**
97
116
  * Shows the input field in CoreBox
98
- *
117
+ *
99
118
  * @example
100
119
  * ```typescript
101
120
  * plugin.box.showInput()
102
121
  * ```
103
122
  */
104
- showInput(): Promise<void>
123
+ showInput: () => Promise<void>
105
124
 
106
125
  /**
107
126
  * Gets the current input value from CoreBox search field
108
- *
127
+ *
109
128
  * @returns Promise resolving to the current input string
110
- *
129
+ *
111
130
  * @example
112
131
  * ```typescript
113
132
  * const input = await plugin.box.getInput()
114
133
  * console.log('Current input:', input)
115
134
  * ```
116
135
  */
117
- getInput(): Promise<string>
136
+ getInput: () => Promise<string>
137
+
138
+ /**
139
+ * Sets the CoreBox search input to the specified value
140
+ *
141
+ * @example
142
+ * ```typescript
143
+ * await plugin.box.setInput('hello world')
144
+ * ```
145
+ */
146
+ setInput: (value: string) => Promise<void>
147
+
148
+ /**
149
+ * Clears the CoreBox search input
150
+ *
151
+ * @example
152
+ * ```typescript
153
+ * await plugin.box.clearInput()
154
+ * ```
155
+ */
156
+ clearInput: () => Promise<void>
157
+
158
+ /**
159
+ * Enable input monitoring for attached UI view
160
+ *
161
+ * @example
162
+ * ```typescript
163
+ * await plugin.box.allowInput()
164
+ * plugin.channel.regChannel('core-box:input-change', ({ data }) => {
165
+ * console.log('Input changed:', data.input)
166
+ * })
167
+ * ```
168
+ */
169
+ allowInput: () => Promise<void>
170
+
171
+ /**
172
+ * Enable clipboard monitoring for specified type combination
173
+ *
174
+ * @param types - Binary combination of ClipboardType flags
175
+ *
176
+ * @example
177
+ * ```typescript
178
+ * // Allow text and images
179
+ * await plugin.box.allowClipboard(ClipboardType.TEXT | ClipboardType.IMAGE)
180
+ *
181
+ * // Or use presets
182
+ * await plugin.box.allowClipboard(ClipboardTypePresets.TEXT_AND_IMAGE)
183
+ * ```
184
+ */
185
+ allowClipboard: (types: number) => Promise<void>
118
186
  }
119
187
 
120
188
  /**
121
189
  * Creates a Box SDK instance for plugin use
122
- *
190
+ *
123
191
  * @param channel - The plugin channel bridge for IPC communication
124
192
  * @returns Configured Box SDK instance
125
- *
193
+ *
126
194
  * @internal
127
195
  */
128
196
  export function createBoxSDK(channel: any): BoxSDK {
@@ -148,7 +216,8 @@ export function createBoxSDK(channel: any): BoxSDK {
148
216
  async expand(options?: BoxExpandOptions): Promise<void> {
149
217
  try {
150
218
  await sendFn('core-box:expand', options || {})
151
- } catch (error) {
219
+ }
220
+ catch (error) {
152
221
  console.error('[Box SDK] Failed to expand CoreBox:', error)
153
222
  throw error
154
223
  }
@@ -157,7 +226,8 @@ export function createBoxSDK(channel: any): BoxSDK {
157
226
  async shrink(): Promise<void> {
158
227
  try {
159
228
  await sendFn('core-box:expand', { mode: 'collapse' })
160
- } catch (error) {
229
+ }
230
+ catch (error) {
161
231
  console.error('[Box SDK] Failed to shrink CoreBox:', error)
162
232
  throw error
163
233
  }
@@ -166,7 +236,8 @@ export function createBoxSDK(channel: any): BoxSDK {
166
236
  async hideInput(): Promise<void> {
167
237
  try {
168
238
  await sendFn('core-box:hide-input')
169
- } catch (error) {
239
+ }
240
+ catch (error) {
170
241
  console.error('[Box SDK] Failed to hide input:', error)
171
242
  throw error
172
243
  }
@@ -175,7 +246,8 @@ export function createBoxSDK(channel: any): BoxSDK {
175
246
  async showInput(): Promise<void> {
176
247
  try {
177
248
  await sendFn('core-box:show-input')
178
- } catch (error) {
249
+ }
250
+ catch (error) {
179
251
  console.error('[Box SDK] Failed to show input:', error)
180
252
  throw error
181
253
  }
@@ -185,35 +257,70 @@ export function createBoxSDK(channel: any): BoxSDK {
185
257
  try {
186
258
  const result = await sendFn('core-box:get-input')
187
259
  return result?.data?.input || result?.input || ''
188
- } catch (error) {
260
+ }
261
+ catch (error) {
189
262
  console.error('[Box SDK] Failed to get input:', error)
190
263
  throw error
191
264
  }
192
- }
265
+ },
266
+
267
+ async setInput(value: string): Promise<void> {
268
+ try {
269
+ await sendFn('core-box:set-input', { value })
270
+ }
271
+ catch (error) {
272
+ console.error('[Box SDK] Failed to set input:', error)
273
+ throw error
274
+ }
275
+ },
276
+
277
+ async clearInput(): Promise<void> {
278
+ try {
279
+ await sendFn('core-box:clear-input')
280
+ }
281
+ catch (error) {
282
+ console.error('[Box SDK] Failed to clear input:', error)
283
+ throw error
284
+ }
285
+ },
286
+
287
+ async allowInput(): Promise<void> {
288
+ try {
289
+ await sendFn('core-box:allow-input')
290
+ }
291
+ catch (error) {
292
+ console.error('[Box SDK] Failed to enable input monitoring:', error)
293
+ throw error
294
+ }
295
+ },
296
+
297
+ async allowClipboard(types: number): Promise<void> {
298
+ try {
299
+ await sendFn('core-box:allow-clipboard', types)
300
+ }
301
+ catch (error) {
302
+ console.error('[Box SDK] Failed to enable clipboard monitoring:', error)
303
+ throw error
304
+ }
305
+ },
193
306
  }
194
307
  }
195
308
 
196
309
  /**
197
310
  * Hook for using Box SDK in plugin context
198
- *
311
+ *
199
312
  * @returns Box SDK instance
200
- *
313
+ *
201
314
  * @example
202
315
  * ```typescript
203
316
  * const box = useBox()
204
- *
317
+ *
205
318
  * box.hide()
206
319
  * box.expand({ length: 10 })
207
320
  * const input = await box.getInput()
208
321
  * ```
209
322
  */
210
323
  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
-
324
+ const channel = ensureRendererChannel('[Box SDK] Channel not available. Make sure this is called in a plugin context.')
218
325
  return createBoxSDK(channel)
219
326
  }
@@ -4,6 +4,34 @@ import { genChannel } from '../channel'
4
4
 
5
5
  const ensureClientChannel = (): ITouchClientChannel => genChannel()
6
6
 
7
+ const DEFAULT_CHANNEL_ERROR = '[Plugin SDK] Channel not available. Make sure this code runs inside a plugin renderer context.'
8
+
9
+ let cachedWindowChannel: ITouchClientChannel | null = null
10
+
11
+ /**
12
+ * Ensures that the renderer-side plugin channel (window.$channel) exists and returns it.
13
+ *
14
+ * @param errorMessage - Optional custom error message when the channel is unavailable
15
+ */
16
+ export function ensureRendererChannel(errorMessage = DEFAULT_CHANNEL_ERROR): ITouchClientChannel {
17
+ const globalWindow = typeof window === 'undefined' ? undefined : window
18
+ const channel = globalWindow?.$channel ?? cachedWindowChannel
19
+
20
+ if (!channel) {
21
+ throw new Error(errorMessage)
22
+ }
23
+
24
+ cachedWindowChannel = channel
25
+ return channel
26
+ }
27
+
28
+ /**
29
+ * Convenience hook for accessing window.$channel in plugin renderers.
30
+ */
31
+ export function useChannel(errorMessage?: string): ITouchClientChannel {
32
+ return ensureRendererChannel(errorMessage)
33
+ }
34
+
7
35
  export function createPluginRendererChannel(): IPluginRendererChannel {
8
36
  const client = ensureClientChannel()
9
37
 
@@ -46,3 +74,9 @@ export function usePluginRendererChannel(): IPluginRendererChannel {
46
74
 
47
75
  return cachedRendererChannel
48
76
  }
77
+
78
+ declare global {
79
+ interface Window {
80
+ $channel: ITouchClientChannel
81
+ }
82
+ }
@@ -11,6 +11,7 @@ import type {
11
11
  DivisionBoxState,
12
12
  SessionInfo,
13
13
  } from '../../types/division-box'
14
+ import { ensureRendererChannel } from './channel'
14
15
 
15
16
  /**
16
17
  * State change event handler
@@ -255,12 +256,6 @@ export function createDivisionBoxSDK(channel: any): DivisionBoxSDK {
255
256
  * ```
256
257
  */
257
258
  export function useDivisionBox(): DivisionBoxSDK {
258
- // @ts-ignore - window.$channel is injected by the plugin system
259
- const channel = window.$channel
260
-
261
- if (!channel) {
262
- throw new Error('[DivisionBox SDK] Channel not available. Make sure this is called in a plugin context.')
263
- }
264
-
259
+ const channel = ensureRendererChannel('[DivisionBox SDK] Channel not available. Make sure this is called in a plugin context.')
265
260
  return createDivisionBoxSDK(channel)
266
261
  }
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import type { TuffItem } from '../../core-box/tuff'
10
+ import { ensureRendererChannel } from './channel'
10
11
 
11
12
  /**
12
13
  * Input change event handler
@@ -218,18 +219,13 @@ export function createFeatureSDK(boxItemsAPI: any, channel: any): FeatureSDK {
218
219
  * ```
219
220
  */
220
221
  export function useFeature(): FeatureSDK {
221
- // @ts-ignore - window.$boxItems and window.$channel are injected by the plugin system
222
+ // @ts-ignore - window.$boxItems is injected by the plugin system
222
223
  const boxItemsAPI = window.$boxItems
223
- // @ts-ignore
224
- const channel = window.$channel
224
+ const channel = ensureRendererChannel('[Feature SDK] Channel not available. Make sure this is called in a plugin context.')
225
225
 
226
226
  if (!boxItemsAPI) {
227
227
  throw new Error('[Feature SDK] boxItems API not available. Make sure this is called in a plugin context.')
228
228
  }
229
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
230
  return createFeatureSDK(boxItemsAPI, channel)
235
231
  }
@@ -1,4 +1,5 @@
1
1
  import { BridgeEventForCoreBox } from '../enum/bridge-event'
2
+ import { ensureRendererChannel } from '../channel'
2
3
 
3
4
  export type BridgeEvent = BridgeEventForCoreBox
4
5
 
@@ -26,7 +27,8 @@ export function injectBridgeEvent<T>(type: BridgeEvent, hook: BridgeHook<T>) {
26
27
 
27
28
  // Only register the channel listener once per event type
28
29
  if (hooks.length === 0) {
29
- window.$channel.regChannel(type, ({ data }) => {
30
+ const channel = ensureRendererChannel('[TouchSDK] Bridge channel not available. Make sure hooks run in plugin renderer context.')
31
+ channel.regChannel(type, ({ data }) => {
30
32
  console.debug(`[TouchSDK] ${type} event received: `, data)
31
33
  // When the event is received, call all registered hooks for this type
32
34
  const registeredHooks = __hooks[type]
@@ -1,3 +1,5 @@
1
+ import { ensureRendererChannel } from '../channel'
2
+
1
3
  export enum LifecycleHooks {
2
4
  ENABLE = 'en',
3
5
  DISABLE = 'di',
@@ -22,7 +24,8 @@ export function injectHook(type: LifecycleHooks, hook: Function, processFunc = (
22
24
  const hooks: Array<Function> = __hooks[type] || (__hooks[type] = [])
23
25
 
24
26
  if (hooks.length === 0) {
25
- window.$channel.regChannel(`@lifecycle:${type}`, (obj: any) => {
27
+ const channel = ensureRendererChannel('[Lifecycle Hook] Channel not available. Make sure hooks run in plugin renderer context.')
28
+ channel.regChannel(`@lifecycle:${type}`, (obj: any) => {
26
29
  processFunc(obj)
27
30
 
28
31
  // @ts-ignore
@@ -1,4 +1,5 @@
1
1
  import type { FileDetails, StorageStats, StorageTreeNode } from '../../types/storage'
2
+ import { ensureRendererChannel } from './channel'
2
3
 
3
4
  /**
4
5
  * Get the storage for the current plugin.
@@ -15,7 +16,7 @@ export function usePluginStorage() {
15
16
  throw new Error('[Plugin SDK] Cannot determine plugin name. Make sure this is called in a plugin context.')
16
17
  }
17
18
 
18
- const channel = window.$channel
19
+ const channel = ensureRendererChannel('[Plugin Storage] Channel not available. Make sure this is called in a plugin context.')
19
20
 
20
21
  return {
21
22
  /**
@@ -0,0 +1,25 @@
1
+ export const DEFAULT_WIDGET_RENDERERS = {
2
+ CORE_PREVIEW_CARD: 'core-preview-card',
3
+ CORE_INTELLIGENCE_ANSWER: 'core-intelligence-answer',
4
+ } as const
5
+
6
+ const values = Object.values(DEFAULT_WIDGET_RENDERERS)
7
+ export const DEFAULT_WIDGET_RENDERER_IDS = new Set<string>(values)
8
+
9
+ export function isDefaultWidgetRenderer(id: string | undefined): boolean {
10
+ return Boolean(id) && DEFAULT_WIDGET_RENDERER_IDS.has(id!)
11
+ }
12
+
13
+ export interface WidgetRegistrationPayload {
14
+ widgetId: string
15
+ pluginName: string
16
+ featureId: string
17
+ filePath: string
18
+ code: string
19
+ styles: string
20
+ hash: string
21
+ }
22
+
23
+ export function makeWidgetId(pluginName: string, featureId: string): string {
24
+ return `${pluginName}::${featureId}`
25
+ }
@@ -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