@talex-touch/utils 1.0.34 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@talex-touch/utils",
3
- "version": "1.0.34",
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
 
@@ -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
  /**