@talex-touch/utils 1.0.39 → 1.0.42
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/core-box/tuff/tuff-dsl.ts +133 -54
- package/intelligence/client.ts +8 -8
- package/market/constants.ts +14 -0
- package/market/types.ts +1 -1
- package/package.json +1 -1
- package/plugin/index.ts +7 -1
- package/plugin/providers/index.ts +4 -0
- package/plugin/providers/market-client.ts +215 -0
- package/plugin/providers/npm-provider.ts +213 -0
- package/plugin/providers/tpex-provider.ts +283 -0
- package/plugin/providers/tpex-types.ts +34 -0
- package/plugin/sdk/README.md +54 -6
- package/plugin/sdk/clipboard.ts +196 -23
- package/plugin/sdk/enum/bridge-event.ts +1 -0
- package/plugin/sdk/feature-sdk.ts +85 -6
- package/plugin/sdk/flow.ts +246 -0
- package/plugin/sdk/hooks/bridge.ts +113 -39
- package/plugin/sdk/index.ts +2 -0
- package/plugin/sdk/performance.ts +186 -0
- package/plugin/widget.ts +5 -0
- package/renderer/hooks/arg-mapper.ts +20 -6
- package/renderer/hooks/use-intelligence.ts +291 -34
- package/renderer/storage/base-storage.ts +98 -15
- package/renderer/storage/intelligence-storage.ts +9 -9
- package/renderer/storage/storage-subscription.ts +17 -9
- package/search/fuzzy-match.ts +254 -0
- package/types/division-box.ts +20 -0
- package/types/flow.ts +283 -0
- package/types/index.ts +1 -0
- package/types/intelligence.ts +1496 -78
package/plugin/sdk/clipboard.ts
CHANGED
|
@@ -44,67 +44,141 @@ export interface ClipboardApplyOptions {
|
|
|
44
44
|
type?: PluginClipboardItem['type']
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
export interface ClipboardWriteOptions {
|
|
48
|
+
text?: string
|
|
49
|
+
html?: string
|
|
50
|
+
image?: string
|
|
51
|
+
files?: string[]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ClipboardReadResult {
|
|
55
|
+
text: string
|
|
56
|
+
html: string
|
|
57
|
+
hasImage: boolean
|
|
58
|
+
hasFiles: boolean
|
|
59
|
+
formats: string[]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ClipboardImageResult {
|
|
63
|
+
dataUrl: string
|
|
64
|
+
width: number
|
|
65
|
+
height: number
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface ClipboardCopyAndPasteOptions {
|
|
69
|
+
text?: string
|
|
70
|
+
html?: string
|
|
71
|
+
image?: string
|
|
72
|
+
files?: string[]
|
|
73
|
+
delayMs?: number
|
|
74
|
+
hideCoreBox?: boolean
|
|
75
|
+
}
|
|
76
|
+
|
|
47
77
|
export type ClipboardSearchOptions = PluginClipboardSearchOptions
|
|
48
78
|
export type ClipboardSearchResponse = PluginClipboardSearchResponse
|
|
49
79
|
|
|
80
|
+
/**
|
|
81
|
+
* @deprecated Use `useClipboard()` instead. This function will be removed in a future version.
|
|
82
|
+
*/
|
|
50
83
|
export function useClipboardHistory() {
|
|
84
|
+
return useClipboard().history
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Unified Clipboard SDK for plugin renderer context.
|
|
89
|
+
*
|
|
90
|
+
* Provides:
|
|
91
|
+
* - Basic clipboard operations (read/write) via IPC to main process
|
|
92
|
+
* - Clipboard history management
|
|
93
|
+
* - Copy and paste to active application
|
|
94
|
+
*
|
|
95
|
+
* All operations go through IPC to avoid WebContents focus issues.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```typescript
|
|
99
|
+
* const clipboard = useClipboard()
|
|
100
|
+
*
|
|
101
|
+
* // === Basic Operations ===
|
|
102
|
+
* await clipboard.writeText('Hello World')
|
|
103
|
+
* const content = await clipboard.read()
|
|
104
|
+
*
|
|
105
|
+
* // === Copy and Paste ===
|
|
106
|
+
* await clipboard.copyAndPaste({ text: 'Pasted content' })
|
|
107
|
+
*
|
|
108
|
+
* // === History Operations ===
|
|
109
|
+
* const latest = await clipboard.history.getLatest()
|
|
110
|
+
* const { history } = await clipboard.history.getHistory({ page: 1 })
|
|
111
|
+
*
|
|
112
|
+
* // === Listen to Changes ===
|
|
113
|
+
* // Note: Plugin must call box.allowClipboard(types) first
|
|
114
|
+
* const unsubscribe = clipboard.history.onDidChange((item) => {
|
|
115
|
+
* console.log('Clipboard changed:', item)
|
|
116
|
+
* })
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
export function useClipboard() {
|
|
51
120
|
const channel = ensurePluginChannel()
|
|
52
121
|
|
|
53
|
-
|
|
122
|
+
const history = {
|
|
123
|
+
/**
|
|
124
|
+
* Gets the most recent clipboard item
|
|
125
|
+
*/
|
|
54
126
|
async getLatest(): Promise<PluginClipboardItem | null> {
|
|
55
127
|
const result = await channel.send('clipboard:get-latest')
|
|
56
128
|
return normalizeItem(result)
|
|
57
129
|
},
|
|
58
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Gets clipboard history with pagination
|
|
133
|
+
*/
|
|
59
134
|
async getHistory(options: ClipboardHistoryOptions = {}): Promise<PluginClipboardHistoryResponse> {
|
|
60
135
|
const response = await channel.send('clipboard:get-history', options)
|
|
61
|
-
const
|
|
136
|
+
const items = Array.isArray(response?.history)
|
|
62
137
|
? response.history.map((item: PluginClipboardItem) => normalizeItem(item) ?? item)
|
|
63
138
|
: []
|
|
64
139
|
return {
|
|
65
140
|
...response,
|
|
66
|
-
history,
|
|
141
|
+
history: items,
|
|
67
142
|
}
|
|
68
143
|
},
|
|
69
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Sets favorite status for a clipboard item
|
|
147
|
+
*/
|
|
70
148
|
async setFavorite(options: ClipboardFavoriteOptions): Promise<void> {
|
|
71
149
|
await channel.send('clipboard:set-favorite', options)
|
|
72
150
|
},
|
|
73
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Deletes a clipboard item from history
|
|
154
|
+
*/
|
|
74
155
|
async deleteItem(options: ClipboardDeleteOptions): Promise<void> {
|
|
75
156
|
await channel.send('clipboard:delete-item', options)
|
|
76
157
|
},
|
|
77
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Clears all clipboard history
|
|
161
|
+
*/
|
|
78
162
|
async clearHistory(): Promise<void> {
|
|
79
163
|
await channel.send('clipboard:clear-history')
|
|
80
164
|
},
|
|
81
165
|
|
|
82
166
|
/**
|
|
83
|
-
* Search clipboard history with advanced filtering
|
|
84
|
-
* Supports keyword search, time-based filtering, and combined filters.
|
|
85
|
-
*
|
|
86
|
-
* @param options - Search options
|
|
87
|
-
* @returns Search results with pagination metadata
|
|
167
|
+
* Search clipboard history with advanced filtering
|
|
88
168
|
*
|
|
89
169
|
* @example
|
|
90
170
|
* ```typescript
|
|
91
171
|
* // Search by keyword
|
|
92
|
-
* const result = await searchHistory({ keyword: 'hello' })
|
|
93
|
-
*
|
|
94
|
-
* // Search by time range (last 24 hours)
|
|
95
|
-
* const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000
|
|
96
|
-
* const recent = await searchHistory({ startTime: oneDayAgo })
|
|
172
|
+
* const result = await clipboard.history.searchHistory({ keyword: 'hello' })
|
|
97
173
|
*
|
|
98
|
-
* //
|
|
99
|
-
* const
|
|
174
|
+
* // Filter by type and time
|
|
175
|
+
* const recent = await clipboard.history.searchHistory({
|
|
100
176
|
* type: 'text',
|
|
101
|
-
*
|
|
102
|
-
* sourceApp: 'com.apple.Safari'
|
|
177
|
+
* startTime: Date.now() - 24 * 60 * 60 * 1000
|
|
103
178
|
* })
|
|
104
179
|
* ```
|
|
105
180
|
*/
|
|
106
181
|
async searchHistory(options: ClipboardSearchOptions = {}): Promise<ClipboardSearchResponse> {
|
|
107
|
-
// Use the extended clipboard:get-history interface with search parameters
|
|
108
182
|
const response = await channel.send('clipboard:get-history', options)
|
|
109
183
|
const items = Array.isArray(response?.history)
|
|
110
184
|
? response.history.map((item: PluginClipboardItem) => normalizeItem(item) ?? item)
|
|
@@ -117,21 +191,120 @@ export function useClipboardHistory() {
|
|
|
117
191
|
}
|
|
118
192
|
},
|
|
119
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Listen to clipboard changes.
|
|
196
|
+
*
|
|
197
|
+
* **Important**: Plugin must call `box.allowClipboard(types)` first to enable monitoring.
|
|
198
|
+
* Only changes matching the allowed types will be received.
|
|
199
|
+
*
|
|
200
|
+
* @example
|
|
201
|
+
* ```typescript
|
|
202
|
+
* import { ClipboardType } from '@talex-touch/utils/plugin/sdk'
|
|
203
|
+
*
|
|
204
|
+
* // Enable monitoring for text and images
|
|
205
|
+
* await box.allowClipboard(ClipboardType.TEXT | ClipboardType.IMAGE)
|
|
206
|
+
*
|
|
207
|
+
* // Listen to changes
|
|
208
|
+
* const unsubscribe = clipboard.history.onDidChange((item) => {
|
|
209
|
+
* console.log('New clipboard item:', item.type, item.content)
|
|
210
|
+
* })
|
|
211
|
+
*
|
|
212
|
+
* // Later: stop listening
|
|
213
|
+
* unsubscribe()
|
|
214
|
+
* ```
|
|
215
|
+
*/
|
|
120
216
|
onDidChange(callback: (item: PluginClipboardItem) => void): () => void {
|
|
121
|
-
return channel.regChannel('core-box:clipboard-change', ({ data }) => {
|
|
122
|
-
const item = (data && 'item' in data ? data.item : data) as PluginClipboardItem
|
|
217
|
+
return channel.regChannel('core-box:clipboard-change', ({ data }: { data: unknown }) => {
|
|
218
|
+
const item = (data && typeof data === 'object' && 'item' in data ? (data as { item: PluginClipboardItem }).item : data) as PluginClipboardItem
|
|
123
219
|
callback(normalizeItem(item) ?? item)
|
|
124
220
|
})
|
|
125
221
|
},
|
|
126
222
|
|
|
127
223
|
/**
|
|
128
|
-
*
|
|
129
|
-
*
|
|
224
|
+
* Apply a clipboard item to the active application (write + paste)
|
|
225
|
+
* @deprecated Use `clipboard.copyAndPaste()` instead
|
|
130
226
|
*/
|
|
131
227
|
async applyToActiveApp(options: ClipboardApplyOptions = {}): Promise<boolean> {
|
|
132
228
|
const response = await channel.send('clipboard:apply-to-active-app', options)
|
|
133
229
|
if (typeof response === 'object' && response) {
|
|
134
|
-
return Boolean((response as
|
|
230
|
+
return Boolean((response as { success?: boolean }).success)
|
|
231
|
+
}
|
|
232
|
+
return true
|
|
233
|
+
},
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
/**
|
|
238
|
+
* Clipboard history operations
|
|
239
|
+
*/
|
|
240
|
+
history,
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Writes text to the system clipboard
|
|
244
|
+
*/
|
|
245
|
+
async writeText(text: string): Promise<void> {
|
|
246
|
+
await channel.send('clipboard:write-text', { text })
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Writes content to the system clipboard.
|
|
251
|
+
* Supports text, HTML, image (data URL), and files.
|
|
252
|
+
*/
|
|
253
|
+
async write(options: ClipboardWriteOptions): Promise<void> {
|
|
254
|
+
await channel.send('clipboard:write', options)
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Reads current clipboard content
|
|
259
|
+
*/
|
|
260
|
+
async read(): Promise<ClipboardReadResult> {
|
|
261
|
+
return await channel.send('clipboard:read')
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Reads image from clipboard as data URL
|
|
266
|
+
*/
|
|
267
|
+
async readImage(): Promise<ClipboardImageResult | null> {
|
|
268
|
+
return await channel.send('clipboard:read-image')
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Reads file paths from clipboard
|
|
273
|
+
*/
|
|
274
|
+
async readFiles(): Promise<string[]> {
|
|
275
|
+
return await channel.send('clipboard:read-files')
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Clears the system clipboard
|
|
280
|
+
*/
|
|
281
|
+
async clear(): Promise<void> {
|
|
282
|
+
await channel.send('clipboard:clear')
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Writes content to clipboard and simulates paste command to the active application.
|
|
287
|
+
* This is the recommended way to "paste" content from a plugin.
|
|
288
|
+
*
|
|
289
|
+
* @example
|
|
290
|
+
* ```typescript
|
|
291
|
+
* // Paste text
|
|
292
|
+
* await clipboard.copyAndPaste({ text: 'Hello' })
|
|
293
|
+
*
|
|
294
|
+
* // Paste with HTML formatting
|
|
295
|
+
* await clipboard.copyAndPaste({ text: 'Hello', html: '<b>Hello</b>' })
|
|
296
|
+
*
|
|
297
|
+
* // Paste image
|
|
298
|
+
* await clipboard.copyAndPaste({ image: 'data:image/png;base64,...' })
|
|
299
|
+
*
|
|
300
|
+
* // Paste files
|
|
301
|
+
* await clipboard.copyAndPaste({ files: ['/path/to/file.txt'] })
|
|
302
|
+
* ```
|
|
303
|
+
*/
|
|
304
|
+
async copyAndPaste(options: ClipboardCopyAndPasteOptions): Promise<boolean> {
|
|
305
|
+
const response = await channel.send('clipboard:copy-and-paste', options)
|
|
306
|
+
if (typeof response === 'object' && response) {
|
|
307
|
+
return Boolean((response as { success?: boolean }).success)
|
|
135
308
|
}
|
|
136
309
|
return true
|
|
137
310
|
},
|
|
@@ -14,6 +14,24 @@ import { ensureRendererChannel } from './channel'
|
|
|
14
14
|
*/
|
|
15
15
|
export type InputChangeHandler = (input: string) => void
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Keyboard event data forwarded from CoreBox
|
|
19
|
+
*/
|
|
20
|
+
export interface ForwardedKeyEvent {
|
|
21
|
+
key: string
|
|
22
|
+
code: string
|
|
23
|
+
metaKey: boolean
|
|
24
|
+
ctrlKey: boolean
|
|
25
|
+
altKey: boolean
|
|
26
|
+
shiftKey: boolean
|
|
27
|
+
repeat: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Key event handler
|
|
32
|
+
*/
|
|
33
|
+
export type KeyEventHandler = (event: ForwardedKeyEvent) => void
|
|
34
|
+
|
|
17
35
|
/**
|
|
18
36
|
* Feature SDK interface for plugins
|
|
19
37
|
*
|
|
@@ -125,6 +143,36 @@ export interface FeatureSDK {
|
|
|
125
143
|
* ```
|
|
126
144
|
*/
|
|
127
145
|
onInputChange(handler: InputChangeHandler): () => void
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Registers a listener for keyboard events forwarded from CoreBox
|
|
149
|
+
*
|
|
150
|
+
* When a plugin's UI view is attached to CoreBox, certain key events
|
|
151
|
+
* (Enter, Arrow keys, Meta+key combinations) are forwarded to the plugin.
|
|
152
|
+
*
|
|
153
|
+
* @param handler - Callback function invoked when a key event is forwarded
|
|
154
|
+
* @returns Unsubscribe function
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```typescript
|
|
158
|
+
* const unsubscribe = plugin.feature.onKeyEvent((event) => {
|
|
159
|
+
* if (event.key === 'Enter') {
|
|
160
|
+
* // Handle enter key
|
|
161
|
+
* submitSelection()
|
|
162
|
+
* } else if (event.key === 'ArrowDown') {
|
|
163
|
+
* // Navigate down in list
|
|
164
|
+
* selectNext()
|
|
165
|
+
* } else if (event.metaKey && event.key === 'k') {
|
|
166
|
+
* // Handle Cmd+K
|
|
167
|
+
* openSearch()
|
|
168
|
+
* }
|
|
169
|
+
* })
|
|
170
|
+
*
|
|
171
|
+
* // Later, unsubscribe
|
|
172
|
+
* unsubscribe()
|
|
173
|
+
* ```
|
|
174
|
+
*/
|
|
175
|
+
onKeyEvent(handler: KeyEventHandler): () => void
|
|
128
176
|
}
|
|
129
177
|
|
|
130
178
|
/**
|
|
@@ -138,25 +186,48 @@ export interface FeatureSDK {
|
|
|
138
186
|
*/
|
|
139
187
|
export function createFeatureSDK(boxItemsAPI: any, channel: any): FeatureSDK {
|
|
140
188
|
const inputChangeHandlers: Set<InputChangeHandler> = new Set()
|
|
189
|
+
const keyEventHandlers: Set<KeyEventHandler> = new Set()
|
|
141
190
|
|
|
142
191
|
// Register listener for input change events from main process
|
|
143
|
-
const
|
|
192
|
+
const registerInputListener = () => {
|
|
144
193
|
if (channel.onMain) {
|
|
145
194
|
// Main process plugin context
|
|
146
|
-
channel.onMain('core-box:input-
|
|
147
|
-
const input = event.data?.input || event.input || ''
|
|
195
|
+
channel.onMain('core-box:input-change', (event: any) => {
|
|
196
|
+
const input = event.data?.input || event.data?.query?.text || event.input || ''
|
|
148
197
|
inputChangeHandlers.forEach(handler => handler(input))
|
|
149
198
|
})
|
|
150
199
|
} else if (channel.on) {
|
|
151
200
|
// Renderer process context
|
|
152
|
-
channel.on('core-box:input-
|
|
153
|
-
const input = data?.input || data || ''
|
|
201
|
+
channel.on('core-box:input-change', (data: any) => {
|
|
202
|
+
const input = data?.input || data?.query?.text || data || ''
|
|
154
203
|
inputChangeHandlers.forEach(handler => handler(input))
|
|
155
204
|
})
|
|
156
205
|
}
|
|
157
206
|
}
|
|
158
207
|
|
|
159
|
-
|
|
208
|
+
// Register listener for key events from main process
|
|
209
|
+
const registerKeyListener = () => {
|
|
210
|
+
if (channel.onMain) {
|
|
211
|
+
// Main process plugin context
|
|
212
|
+
channel.onMain('core-box:key-event', (event: any) => {
|
|
213
|
+
const keyEvent = event.data as ForwardedKeyEvent
|
|
214
|
+
if (keyEvent) {
|
|
215
|
+
keyEventHandlers.forEach(handler => handler(keyEvent))
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
} else if (channel.on) {
|
|
219
|
+
// Renderer process context
|
|
220
|
+
channel.on('core-box:key-event', (data: any) => {
|
|
221
|
+
const keyEvent = data as ForwardedKeyEvent
|
|
222
|
+
if (keyEvent) {
|
|
223
|
+
keyEventHandlers.forEach(handler => handler(keyEvent))
|
|
224
|
+
}
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
registerInputListener()
|
|
230
|
+
registerKeyListener()
|
|
160
231
|
|
|
161
232
|
return {
|
|
162
233
|
pushItems(items: TuffItem[]): void {
|
|
@@ -200,6 +271,14 @@ export function createFeatureSDK(boxItemsAPI: any, channel: any): FeatureSDK {
|
|
|
200
271
|
return () => {
|
|
201
272
|
inputChangeHandlers.delete(handler)
|
|
202
273
|
}
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
onKeyEvent(handler: KeyEventHandler): () => void {
|
|
277
|
+
keyEventHandlers.add(handler)
|
|
278
|
+
|
|
279
|
+
return () => {
|
|
280
|
+
keyEventHandlers.delete(handler)
|
|
281
|
+
}
|
|
203
282
|
}
|
|
204
283
|
}
|
|
205
284
|
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flow SDK
|
|
3
|
+
*
|
|
4
|
+
* Plugin-side API for Flow Transfer operations.
|
|
5
|
+
* Allows plugins to dispatch flows and receive flow data.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
FlowPayload,
|
|
10
|
+
FlowDispatchOptions,
|
|
11
|
+
FlowDispatchResult,
|
|
12
|
+
FlowTargetInfo,
|
|
13
|
+
FlowSessionUpdate,
|
|
14
|
+
FlowPayloadType
|
|
15
|
+
} from '../../types/flow'
|
|
16
|
+
import { FlowIPCChannel } from '../../types/flow'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Flow SDK interface
|
|
20
|
+
*/
|
|
21
|
+
export interface IFlowSDK {
|
|
22
|
+
/**
|
|
23
|
+
* Dispatches a flow payload to another plugin
|
|
24
|
+
*
|
|
25
|
+
* @param payload - Data to transfer
|
|
26
|
+
* @param options - Dispatch options
|
|
27
|
+
* @returns Dispatch result
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* const result = await flow.dispatch(
|
|
32
|
+
* {
|
|
33
|
+
* type: 'text',
|
|
34
|
+
* data: 'Hello from plugin A',
|
|
35
|
+
* context: { sourcePluginId: 'plugin-a' }
|
|
36
|
+
* },
|
|
37
|
+
* {
|
|
38
|
+
* title: 'Share Text',
|
|
39
|
+
* preferredTarget: 'plugin-b.quick-note'
|
|
40
|
+
* }
|
|
41
|
+
* )
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
dispatch(payload: FlowPayload, options?: FlowDispatchOptions): Promise<FlowDispatchResult>
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Gets available flow targets
|
|
48
|
+
*
|
|
49
|
+
* @param payloadType - Filter by payload type (optional)
|
|
50
|
+
* @returns List of available targets
|
|
51
|
+
*/
|
|
52
|
+
getAvailableTargets(payloadType?: FlowPayloadType): Promise<FlowTargetInfo[]>
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Listens for session updates
|
|
56
|
+
*
|
|
57
|
+
* @param sessionId - Session to listen to
|
|
58
|
+
* @param handler - Update handler
|
|
59
|
+
* @returns Unsubscribe function
|
|
60
|
+
*/
|
|
61
|
+
onSessionUpdate(
|
|
62
|
+
sessionId: string,
|
|
63
|
+
handler: (update: FlowSessionUpdate) => void
|
|
64
|
+
): () => void
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Cancels a flow session
|
|
68
|
+
*
|
|
69
|
+
* @param sessionId - Session to cancel
|
|
70
|
+
*/
|
|
71
|
+
cancel(sessionId: string): Promise<void>
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Acknowledges a received flow (for target plugins)
|
|
75
|
+
*
|
|
76
|
+
* @param sessionId - Session to acknowledge
|
|
77
|
+
* @param ackPayload - Optional acknowledgment data
|
|
78
|
+
*/
|
|
79
|
+
acknowledge(sessionId: string, ackPayload?: any): Promise<void>
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Reports an error for a received flow (for target plugins)
|
|
83
|
+
*
|
|
84
|
+
* @param sessionId - Session to report error for
|
|
85
|
+
* @param message - Error message
|
|
86
|
+
*/
|
|
87
|
+
reportError(sessionId: string, message: string): Promise<void>
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Creates a Flow SDK instance
|
|
92
|
+
*
|
|
93
|
+
* @param channel - Channel for IPC communication
|
|
94
|
+
* @param pluginId - Current plugin ID
|
|
95
|
+
* @returns Flow SDK instance
|
|
96
|
+
*/
|
|
97
|
+
export function createFlowSDK(
|
|
98
|
+
channel: { send: (event: string, data?: any) => Promise<any> },
|
|
99
|
+
pluginId: string
|
|
100
|
+
): IFlowSDK {
|
|
101
|
+
const sessionListeners = new Map<string, Set<(update: FlowSessionUpdate) => void>>()
|
|
102
|
+
|
|
103
|
+
// Listen for session updates
|
|
104
|
+
if (typeof window !== 'undefined') {
|
|
105
|
+
window.addEventListener('message', (event) => {
|
|
106
|
+
if (event.data?.type === FlowIPCChannel.SESSION_UPDATE) {
|
|
107
|
+
const update = event.data.payload as FlowSessionUpdate
|
|
108
|
+
const listeners = sessionListeners.get(update.sessionId)
|
|
109
|
+
if (listeners) {
|
|
110
|
+
for (const listener of listeners) {
|
|
111
|
+
try {
|
|
112
|
+
listener(update)
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error('[FlowSDK] Error in session listener:', error)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
async dispatch(payload: FlowPayload, options?: FlowDispatchOptions): Promise<FlowDispatchResult> {
|
|
124
|
+
// Ensure context has sourcePluginId
|
|
125
|
+
const enrichedPayload: FlowPayload = {
|
|
126
|
+
...payload,
|
|
127
|
+
context: {
|
|
128
|
+
...payload.context,
|
|
129
|
+
sourcePluginId: payload.context?.sourcePluginId || pluginId
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const response = await channel.send(FlowIPCChannel.DISPATCH, {
|
|
134
|
+
senderId: pluginId,
|
|
135
|
+
payload: enrichedPayload,
|
|
136
|
+
options
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
if (!response?.success) {
|
|
140
|
+
throw new Error(response?.error?.message || 'Flow dispatch failed')
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return response.data
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
async getAvailableTargets(payloadType?: FlowPayloadType): Promise<FlowTargetInfo[]> {
|
|
147
|
+
const response = await channel.send(FlowIPCChannel.GET_TARGETS, {
|
|
148
|
+
payloadType
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
if (!response?.success) {
|
|
152
|
+
throw new Error(response?.error?.message || 'Failed to get targets')
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return response.data || []
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
onSessionUpdate(
|
|
159
|
+
sessionId: string,
|
|
160
|
+
handler: (update: FlowSessionUpdate) => void
|
|
161
|
+
): () => void {
|
|
162
|
+
if (!sessionListeners.has(sessionId)) {
|
|
163
|
+
sessionListeners.set(sessionId, new Set())
|
|
164
|
+
}
|
|
165
|
+
sessionListeners.get(sessionId)!.add(handler)
|
|
166
|
+
|
|
167
|
+
return () => {
|
|
168
|
+
const listeners = sessionListeners.get(sessionId)
|
|
169
|
+
if (listeners) {
|
|
170
|
+
listeners.delete(handler)
|
|
171
|
+
if (listeners.size === 0) {
|
|
172
|
+
sessionListeners.delete(sessionId)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
async cancel(sessionId: string): Promise<void> {
|
|
179
|
+
const response = await channel.send(FlowIPCChannel.CANCEL, {
|
|
180
|
+
sessionId
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
if (!response?.success) {
|
|
184
|
+
throw new Error(response?.error?.message || 'Failed to cancel session')
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
async acknowledge(sessionId: string, ackPayload?: any): Promise<void> {
|
|
189
|
+
const response = await channel.send(FlowIPCChannel.ACKNOWLEDGE, {
|
|
190
|
+
sessionId,
|
|
191
|
+
ackPayload
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
if (!response?.success) {
|
|
195
|
+
throw new Error(response?.error?.message || 'Failed to acknowledge session')
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
async reportError(sessionId: string, message: string): Promise<void> {
|
|
200
|
+
const response = await channel.send(FlowIPCChannel.REPORT_ERROR, {
|
|
201
|
+
sessionId,
|
|
202
|
+
message
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
if (!response?.success) {
|
|
206
|
+
throw new Error(response?.error?.message || 'Failed to report error')
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Helper to extract flow data from TuffQuery
|
|
214
|
+
*
|
|
215
|
+
* When a feature is triggered via Flow, the query will contain flow information.
|
|
216
|
+
*
|
|
217
|
+
* @param query - TuffQuery from feature trigger
|
|
218
|
+
* @returns Flow data if present, null otherwise
|
|
219
|
+
*/
|
|
220
|
+
export function extractFlowData(query: any): {
|
|
221
|
+
sessionId: string
|
|
222
|
+
payload: FlowPayload
|
|
223
|
+
senderId: string
|
|
224
|
+
senderName?: string
|
|
225
|
+
} | null {
|
|
226
|
+
if (!query?.flow) {
|
|
227
|
+
return null
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
sessionId: query.flow.sessionId,
|
|
232
|
+
payload: query.flow.payload,
|
|
233
|
+
senderId: query.flow.senderId,
|
|
234
|
+
senderName: query.flow.senderName
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Helper to check if a feature was triggered via Flow
|
|
240
|
+
*
|
|
241
|
+
* @param query - TuffQuery from feature trigger
|
|
242
|
+
* @returns True if triggered via Flow
|
|
243
|
+
*/
|
|
244
|
+
export function isFlowTriggered(query: any): boolean {
|
|
245
|
+
return !!query?.flow
|
|
246
|
+
}
|