@talex-touch/utils 1.0.21 → 1.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/common/utils/index.ts +2 -0
- package/common/utils/timing.ts +250 -0
- package/package.json +1 -1
- package/plugin/index.ts +1 -0
- package/plugin/install.ts +43 -0
- package/plugin/node/logger-manager.ts +27 -9
- package/plugin/providers/types.ts +2 -0
- package/plugin/sdk/channel.ts +48 -0
- package/plugin/sdk/clipboard.ts +99 -0
- package/plugin/sdk/hooks/bridge.ts +3 -0
- package/plugin/sdk/index.ts +4 -1
- package/plugin/sdk/system.ts +14 -0
- package/plugin/sdk/types.ts +124 -0
package/common/utils/index.ts
CHANGED
|
@@ -166,6 +166,8 @@ export function structuredStrictStringify(value: unknown): string {
|
|
|
166
166
|
return JSON.stringify(serialize(value, 'root'));
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
export * from './timing'
|
|
170
|
+
|
|
169
171
|
export { runAdaptiveTaskQueue, type AdaptiveTaskQueueOptions } from './task-queue'
|
|
170
172
|
|
|
171
173
|
export * from './time'
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
export type TimingMeta = Record<string, unknown>
|
|
2
|
+
|
|
3
|
+
export interface TimingRecord {
|
|
4
|
+
label: string
|
|
5
|
+
durationMs: number
|
|
6
|
+
startedAt: number
|
|
7
|
+
endedAt: number
|
|
8
|
+
iteration?: number
|
|
9
|
+
meta?: TimingMeta
|
|
10
|
+
error?: unknown
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface TimingStats {
|
|
14
|
+
label: string
|
|
15
|
+
count: number
|
|
16
|
+
totalMs: number
|
|
17
|
+
avgMs: number
|
|
18
|
+
maxMs: number
|
|
19
|
+
minMs: number
|
|
20
|
+
lastMs: number
|
|
21
|
+
lastStartedAt?: number
|
|
22
|
+
lastEndedAt?: number
|
|
23
|
+
errorCount: number
|
|
24
|
+
lastError?: unknown
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface TimingSummary extends TimingStats {
|
|
28
|
+
history: TimingRecord[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface TimingManagerConfig {
|
|
32
|
+
autoLog?: boolean
|
|
33
|
+
historyLimit?: number
|
|
34
|
+
logger?: (message: string, entry: TimingRecord, stats: TimingStats) => void
|
|
35
|
+
formatter?: (entry: TimingRecord, stats: TimingStats) => string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface TimingOptions {
|
|
39
|
+
autoLog?: boolean
|
|
40
|
+
storeHistory?: boolean
|
|
41
|
+
logger?: (message: string, entry: TimingRecord, stats: TimingStats) => void
|
|
42
|
+
formatter?: (entry: TimingRecord, stats: TimingStats) => string
|
|
43
|
+
historyLimit?: number
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class TimingManager {
|
|
47
|
+
private readonly stats = new Map<string, TimingStats>()
|
|
48
|
+
private readonly history = new Map<string, TimingRecord[]>()
|
|
49
|
+
private readonly moduleStats = new Map<string, TimingStats>()
|
|
50
|
+
|
|
51
|
+
constructor(private readonly config: TimingManagerConfig = {}) {}
|
|
52
|
+
|
|
53
|
+
createTiming(label: string, options: TimingOptions = {}): TimingScope {
|
|
54
|
+
return new TimingScope(this, label, options)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
record(label: string, record: TimingRecord, options: TimingOptions = {}): void {
|
|
58
|
+
const stats = this.updateStats(this.stats, label, record)
|
|
59
|
+
const moduleKey = this.extractModuleKey(label)
|
|
60
|
+
this.updateStats(this.moduleStats, moduleKey, { ...record, label: moduleKey })
|
|
61
|
+
|
|
62
|
+
if (options.storeHistory ?? true) {
|
|
63
|
+
const limit = options.historyLimit ?? this.config.historyLimit ?? 50
|
|
64
|
+
const list = this.history.get(label) ?? []
|
|
65
|
+
list.push(record)
|
|
66
|
+
if (list.length > limit) {
|
|
67
|
+
list.splice(0, list.length - limit)
|
|
68
|
+
}
|
|
69
|
+
this.history.set(label, list)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const shouldLog = options.autoLog ?? this.config.autoLog ?? true
|
|
73
|
+
if (shouldLog) {
|
|
74
|
+
const formatter = options.formatter ?? this.config.formatter ?? defaultFormatter
|
|
75
|
+
const logger = options.logger ?? this.config.logger ?? defaultLogger
|
|
76
|
+
logger(formatter(record, stats), record, stats)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
getStats(label: string): TimingStats | undefined {
|
|
81
|
+
const stats = this.stats.get(label)
|
|
82
|
+
if (!stats) return undefined
|
|
83
|
+
return { ...stats }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
getAllStats(): TimingStats[] {
|
|
87
|
+
return Array.from(this.stats.values()).map((s) => ({ ...s }))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
getHistory(label: string): TimingRecord[] {
|
|
91
|
+
return [...(this.history.get(label) ?? [])]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
getModuleStats(moduleKey?: string): TimingStats[] | TimingStats | undefined {
|
|
95
|
+
if (moduleKey) {
|
|
96
|
+
const stats = this.moduleStats.get(moduleKey)
|
|
97
|
+
return stats ? { ...stats } : undefined
|
|
98
|
+
}
|
|
99
|
+
return Array.from(this.moduleStats.values()).map((s) => ({ ...s }))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
reset(label?: string): void {
|
|
103
|
+
if (!label) {
|
|
104
|
+
this.stats.clear()
|
|
105
|
+
this.history.clear()
|
|
106
|
+
this.moduleStats.clear()
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
this.stats.delete(label)
|
|
110
|
+
this.history.delete(label)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private updateStats(target: Map<string, TimingStats>, label: string, record: TimingRecord): TimingStats {
|
|
114
|
+
const { durationMs, error } = record
|
|
115
|
+
const next = target.get(label) ?? {
|
|
116
|
+
label,
|
|
117
|
+
count: 0,
|
|
118
|
+
totalMs: 0,
|
|
119
|
+
avgMs: 0,
|
|
120
|
+
maxMs: Number.NEGATIVE_INFINITY,
|
|
121
|
+
minMs: Number.POSITIVE_INFINITY,
|
|
122
|
+
lastMs: 0,
|
|
123
|
+
errorCount: 0
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
next.count += 1
|
|
127
|
+
next.totalMs += durationMs
|
|
128
|
+
next.avgMs = next.totalMs / next.count
|
|
129
|
+
next.maxMs = Math.max(next.maxMs, durationMs)
|
|
130
|
+
next.minMs = Math.min(next.minMs, durationMs)
|
|
131
|
+
next.lastMs = durationMs
|
|
132
|
+
next.lastStartedAt = record.startedAt
|
|
133
|
+
next.lastEndedAt = record.endedAt
|
|
134
|
+
|
|
135
|
+
if (error) {
|
|
136
|
+
next.errorCount += 1
|
|
137
|
+
next.lastError = error
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
target.set(label, next)
|
|
141
|
+
return next
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private extractModuleKey(label: string): string {
|
|
145
|
+
const [moduleKey] = label.split(':')
|
|
146
|
+
return moduleKey || label
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export class TimingScope {
|
|
151
|
+
constructor(
|
|
152
|
+
private readonly manager: TimingManager,
|
|
153
|
+
private readonly label: string,
|
|
154
|
+
private readonly options: TimingOptions
|
|
155
|
+
) {}
|
|
156
|
+
|
|
157
|
+
async cost<T>(fn: () => Promise<T> | T, meta: TimingMeta = {}, overrides: TimingOptions = {}): Promise<T> {
|
|
158
|
+
const startedAt = now()
|
|
159
|
+
try {
|
|
160
|
+
const result = await fn()
|
|
161
|
+
this.finish(startedAt, meta, undefined, overrides)
|
|
162
|
+
return result
|
|
163
|
+
} catch (error) {
|
|
164
|
+
this.finish(startedAt, meta, error, overrides)
|
|
165
|
+
throw error
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async count<T>(iterations: number, fn: (iteration: number) => Promise<T> | T, meta: TimingMeta | ((iteration: number) => TimingMeta) = {}, overrides: TimingOptions = {}): Promise<T[]> {
|
|
170
|
+
const results: T[] = []
|
|
171
|
+
for (let index = 0; index < iterations; index++) {
|
|
172
|
+
const iterationMeta = typeof meta === 'function' ? meta(index) : meta
|
|
173
|
+
const startedAt = now()
|
|
174
|
+
try {
|
|
175
|
+
const value = await fn(index)
|
|
176
|
+
this.finish(startedAt, { ...iterationMeta, iteration: index }, undefined, overrides)
|
|
177
|
+
results.push(value)
|
|
178
|
+
} catch (error) {
|
|
179
|
+
this.finish(startedAt, { ...iterationMeta, iteration: index }, error, overrides)
|
|
180
|
+
throw error
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return results
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
mark(durationMs: number, meta: TimingMeta = {}, overrides: TimingOptions = {}): void {
|
|
187
|
+
const endedAt = now()
|
|
188
|
+
const startedAt = endedAt - durationMs
|
|
189
|
+
this.manager.record(
|
|
190
|
+
this.label,
|
|
191
|
+
{
|
|
192
|
+
label: this.label,
|
|
193
|
+
durationMs,
|
|
194
|
+
startedAt,
|
|
195
|
+
endedAt,
|
|
196
|
+
meta
|
|
197
|
+
},
|
|
198
|
+
{ ...this.options, ...overrides }
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
getStats(): TimingStats | undefined {
|
|
203
|
+
return this.manager.getStats(this.label)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
getHistory(): TimingRecord[] {
|
|
207
|
+
return this.manager.getHistory(this.label)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private finish(startedAt: number, meta: TimingMeta, error: unknown, overrides: TimingOptions): void {
|
|
211
|
+
const endedAt = now()
|
|
212
|
+
const record: TimingRecord = {
|
|
213
|
+
label: this.label,
|
|
214
|
+
durationMs: endedAt - startedAt,
|
|
215
|
+
startedAt,
|
|
216
|
+
endedAt,
|
|
217
|
+
meta,
|
|
218
|
+
error
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
this.manager.record(this.label, record, { ...this.options, ...overrides })
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function defaultFormatter(record: TimingRecord, stats: TimingStats): string {
|
|
226
|
+
const duration = record.durationMs.toFixed(2)
|
|
227
|
+
return `⏱ [${record.label}] ${duration} ms (avg: ${stats.avgMs.toFixed(2)} ms, max: ${stats.maxMs.toFixed(2)} ms, count: ${stats.count})`
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function defaultLogger(message: string): void {
|
|
231
|
+
console.log(message)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const now = (() => {
|
|
235
|
+
if (typeof globalThis !== 'undefined') {
|
|
236
|
+
const perf = (globalThis as typeof globalThis & { performance?: { now?: () => number } }).performance
|
|
237
|
+
if (perf?.now) {
|
|
238
|
+
return () => perf.now()
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return () => Date.now()
|
|
242
|
+
})()
|
|
243
|
+
|
|
244
|
+
const timingManagerInstance = new TimingManager()
|
|
245
|
+
|
|
246
|
+
export const timingManager = timingManagerInstance
|
|
247
|
+
|
|
248
|
+
export function createTiming(label: string, options: TimingOptions = {}): TimingScope {
|
|
249
|
+
return timingManagerInstance.createTiming(label, options)
|
|
250
|
+
}
|
package/package.json
CHANGED
package/plugin/index.ts
CHANGED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type PluginInstallTaskStage =
|
|
2
|
+
| 'queued'
|
|
3
|
+
| 'downloading'
|
|
4
|
+
| 'awaiting-confirmation'
|
|
5
|
+
| 'installing'
|
|
6
|
+
| 'completed'
|
|
7
|
+
| 'failed'
|
|
8
|
+
| 'cancelled'
|
|
9
|
+
|
|
10
|
+
export interface PluginInstallProgressEvent {
|
|
11
|
+
taskId: string
|
|
12
|
+
stage: PluginInstallTaskStage
|
|
13
|
+
progress?: number
|
|
14
|
+
message?: string
|
|
15
|
+
error?: string
|
|
16
|
+
/** 安装源标识,例如下载 URL。 */
|
|
17
|
+
source?: string
|
|
18
|
+
/** 官方插件标记。 */
|
|
19
|
+
official?: boolean
|
|
20
|
+
/** 插件唯一标识或名称(由客户端提供)。 */
|
|
21
|
+
pluginId?: string
|
|
22
|
+
pluginName?: string
|
|
23
|
+
/** 队列中的剩余任务数量(包含当前任务)。 */
|
|
24
|
+
remaining?: number
|
|
25
|
+
/** 当前任务在队列中的位置(0 表示正在处理)。 */
|
|
26
|
+
position?: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface PluginInstallConfirmRequest {
|
|
30
|
+
taskId: string
|
|
31
|
+
pluginName?: string
|
|
32
|
+
pluginId?: string
|
|
33
|
+
source?: string
|
|
34
|
+
official?: boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type PluginInstallConfirmDecision = 'accept' | 'reject'
|
|
38
|
+
|
|
39
|
+
export interface PluginInstallConfirmResponse {
|
|
40
|
+
taskId: string
|
|
41
|
+
decision: PluginInstallConfirmDecision
|
|
42
|
+
reason?: string
|
|
43
|
+
}
|
|
@@ -12,6 +12,7 @@ export class PluginLoggerManager {
|
|
|
12
12
|
private readonly pluginLogDir: string
|
|
13
13
|
private readonly sessionLogPath: string
|
|
14
14
|
private readonly pluginInfoPath: string
|
|
15
|
+
private readonly sessionStart: string
|
|
15
16
|
private buffer: LogItem[] = []
|
|
16
17
|
private flushInterval: NodeJS.Timeout
|
|
17
18
|
private onLogAppend?: (log: LogItem) => void
|
|
@@ -25,15 +26,15 @@ export class PluginLoggerManager {
|
|
|
25
26
|
constructor(baseDir: string, pluginInfo: ITouchPlugin, onLogAppend?: (log: LogItem) => void) {
|
|
26
27
|
this.pluginInfo = pluginInfo
|
|
27
28
|
this.onLogAppend = onLogAppend
|
|
28
|
-
|
|
29
|
+
this.sessionStart = new Date().toISOString()
|
|
30
|
+
const timestamp = this.sessionStart.replace(/[:.]/g, '-')
|
|
29
31
|
const sessionFolder = `${timestamp}_${pluginInfo.name.replace(/[^a-zA-Z0-9-_]/g, '_')}`
|
|
30
32
|
|
|
31
33
|
this.pluginLogDir = path.resolve(baseDir, 'logs', sessionFolder)
|
|
32
34
|
this.sessionLogPath = path.resolve(this.pluginLogDir, 'session.log')
|
|
33
35
|
this.pluginInfoPath = path.resolve(this.pluginLogDir, 'touch-plugin.info')
|
|
34
36
|
|
|
35
|
-
this.
|
|
36
|
-
this.createPluginInfoFile()
|
|
37
|
+
this.ensureLogEnvironment(true)
|
|
37
38
|
this.flushInterval = setInterval(() => this.flush(), 5000)
|
|
38
39
|
}
|
|
39
40
|
|
|
@@ -52,8 +53,17 @@ export class PluginLoggerManager {
|
|
|
52
53
|
flush(): void {
|
|
53
54
|
if (this.buffer.length === 0) return
|
|
54
55
|
const lines = this.buffer.map((item) => structuredStrictStringify(item)).join('\n') + '\n'
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
try {
|
|
57
|
+
this.ensureLogEnvironment()
|
|
58
|
+
fs.appendFileSync(this.sessionLogPath, lines)
|
|
59
|
+
this.buffer = []
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if ((error as NodeJS.ErrnoException)?.code !== 'ENOENT') throw error
|
|
62
|
+
// Directory or file was removed; rebuild and retry once.
|
|
63
|
+
this.ensureLogEnvironment(true)
|
|
64
|
+
fs.appendFileSync(this.sessionLogPath, lines)
|
|
65
|
+
this.buffer = []
|
|
66
|
+
}
|
|
57
67
|
}
|
|
58
68
|
|
|
59
69
|
/**
|
|
@@ -83,12 +93,12 @@ export class PluginLoggerManager {
|
|
|
83
93
|
/**
|
|
84
94
|
* Creates the touch-plugin.info file with plugin information.
|
|
85
95
|
*/
|
|
86
|
-
private
|
|
96
|
+
private writePluginInfoFile(): void {
|
|
87
97
|
const pluginInfo = {
|
|
88
98
|
name: this.pluginInfo.name,
|
|
89
99
|
version: this.pluginInfo.version,
|
|
90
100
|
description: this.pluginInfo.desc,
|
|
91
|
-
sessionStart:
|
|
101
|
+
sessionStart: this.sessionStart,
|
|
92
102
|
icon: this.pluginInfo.icon,
|
|
93
103
|
platforms: this.pluginInfo.platforms,
|
|
94
104
|
status: this.pluginInfo.status,
|
|
@@ -103,11 +113,19 @@ export class PluginLoggerManager {
|
|
|
103
113
|
}
|
|
104
114
|
|
|
105
115
|
/**
|
|
106
|
-
* Ensures the log directory
|
|
116
|
+
* Ensures the log directory and related files exist.
|
|
107
117
|
*/
|
|
108
|
-
private
|
|
118
|
+
private ensureLogEnvironment(forceInfo = false): void {
|
|
109
119
|
if (!fs.existsSync(this.pluginLogDir)) {
|
|
110
120
|
fs.mkdirSync(this.pluginLogDir, { recursive: true })
|
|
111
121
|
}
|
|
122
|
+
|
|
123
|
+
if (!fs.existsSync(this.sessionLogPath)) {
|
|
124
|
+
fs.writeFileSync(this.sessionLogPath, '')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (forceInfo || !fs.existsSync(this.pluginInfoPath)) {
|
|
128
|
+
this.writePluginInfoFile()
|
|
129
|
+
}
|
|
112
130
|
}
|
|
113
131
|
}
|
|
@@ -17,6 +17,8 @@ export interface PluginInstallRequest {
|
|
|
17
17
|
hintType?: PluginProviderType
|
|
18
18
|
/** 可选元数据,在调用链中透传。 */
|
|
19
19
|
metadata?: Record<string, unknown>
|
|
20
|
+
/** 客户端附加信息,仅用于 UI 状态同步。 */
|
|
21
|
+
clientMetadata?: Record<string, unknown>
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
export interface PluginProviderContext {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ITouchClientChannel } from '@talex-touch/utils/channel';
|
|
2
|
+
import { genChannel } from '../channel';
|
|
3
|
+
import type { IPluginRendererChannel, PluginChannelHandler } from './types';
|
|
4
|
+
|
|
5
|
+
const ensureClientChannel = (): ITouchClientChannel => genChannel();
|
|
6
|
+
|
|
7
|
+
export function createPluginRendererChannel(): IPluginRendererChannel {
|
|
8
|
+
const client = ensureClientChannel();
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
send(eventName, payload) {
|
|
12
|
+
return client.send(eventName, payload);
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
sendSync(eventName, payload) {
|
|
16
|
+
return client.sendSync(eventName, payload);
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
on(eventName, handler) {
|
|
20
|
+
return client.regChannel(eventName, handler);
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
once(eventName, handler) {
|
|
24
|
+
let dispose: () => void = () => void 0;
|
|
25
|
+
const wrapped: PluginChannelHandler = (event) => {
|
|
26
|
+
dispose();
|
|
27
|
+
handler(event);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
dispose = client.regChannel(eventName, wrapped);
|
|
31
|
+
return dispose;
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
get raw() {
|
|
35
|
+
return client;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let cachedRendererChannel: IPluginRendererChannel | null = null;
|
|
41
|
+
|
|
42
|
+
export function usePluginRendererChannel(): IPluginRendererChannel {
|
|
43
|
+
if (!cachedRendererChannel) {
|
|
44
|
+
cachedRendererChannel = createPluginRendererChannel();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return cachedRendererChannel;
|
|
48
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { PluginClipboardHistoryResponse, PluginClipboardItem } from './types'
|
|
2
|
+
|
|
3
|
+
const ensurePluginChannel = () => {
|
|
4
|
+
const channel = (window as any)?.$channel
|
|
5
|
+
if (!channel) {
|
|
6
|
+
throw new Error('[Plugin SDK] Clipboard channel requires plugin renderer context with $channel available.')
|
|
7
|
+
}
|
|
8
|
+
return channel
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const normalizeItem = (item: PluginClipboardItem | null): PluginClipboardItem | null => {
|
|
12
|
+
if (!item) return item
|
|
13
|
+
if (!item.meta && typeof item.metadata === 'string') {
|
|
14
|
+
try {
|
|
15
|
+
const parsed = JSON.parse(item.metadata)
|
|
16
|
+
return { ...item, meta: parsed }
|
|
17
|
+
} catch {
|
|
18
|
+
return { ...item, meta: null }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return item
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ClipboardHistoryOptions {
|
|
25
|
+
page?: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ClipboardFavoriteOptions {
|
|
29
|
+
id: number
|
|
30
|
+
isFavorite: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ClipboardDeleteOptions {
|
|
34
|
+
id: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ClipboardApplyOptions {
|
|
38
|
+
item?: PluginClipboardItem
|
|
39
|
+
text?: string
|
|
40
|
+
html?: string | null
|
|
41
|
+
files?: string[]
|
|
42
|
+
delayMs?: number
|
|
43
|
+
hideCoreBox?: boolean
|
|
44
|
+
type?: PluginClipboardItem['type']
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function useClipboardHistory() {
|
|
48
|
+
const channel = ensurePluginChannel()
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
async getLatest(): Promise<PluginClipboardItem | null> {
|
|
52
|
+
const result = await channel.send('clipboard:get-latest')
|
|
53
|
+
return normalizeItem(result)
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
async getHistory(options: ClipboardHistoryOptions = {}): Promise<PluginClipboardHistoryResponse> {
|
|
57
|
+
const { page = 1 } = options
|
|
58
|
+
const response = await channel.send('clipboard:get-history', { page })
|
|
59
|
+
const history = Array.isArray(response?.history)
|
|
60
|
+
? response.history.map((item: PluginClipboardItem) => normalizeItem(item) ?? item)
|
|
61
|
+
: []
|
|
62
|
+
return {
|
|
63
|
+
...response,
|
|
64
|
+
history
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
async setFavorite(options: ClipboardFavoriteOptions): Promise<void> {
|
|
69
|
+
await channel.send('clipboard:set-favorite', options)
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
async deleteItem(options: ClipboardDeleteOptions): Promise<void> {
|
|
73
|
+
await channel.send('clipboard:delete-item', options)
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
async clearHistory(): Promise<void> {
|
|
77
|
+
await channel.send('clipboard:clear-history')
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
onDidChange(callback: (item: PluginClipboardItem) => void): () => void {
|
|
81
|
+
return channel.regChannel('core-box:clipboard-change', ({ data }) => {
|
|
82
|
+
const item = (data && 'item' in data ? data.item : data) as PluginClipboardItem
|
|
83
|
+
callback(normalizeItem(item) ?? item)
|
|
84
|
+
})
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Writes the provided clipboard payload to the system clipboard and issues a paste command
|
|
89
|
+
* to the foreground application.
|
|
90
|
+
*/
|
|
91
|
+
async applyToActiveApp(options: ClipboardApplyOptions = {}): Promise<boolean> {
|
|
92
|
+
const response = await channel.send('clipboard:apply-to-active-app', options)
|
|
93
|
+
if (typeof response === 'object' && response) {
|
|
94
|
+
return Boolean((response as any).success)
|
|
95
|
+
}
|
|
96
|
+
return true
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -10,6 +10,7 @@ export type BridgeHook<T = any> = (data: T) => void
|
|
|
10
10
|
|
|
11
11
|
const __hooks: Record<BridgeEvent, Array<BridgeHook>> = {
|
|
12
12
|
[BridgeEventForCoreBox.CORE_BOX_INPUT_CHANGE]: [],
|
|
13
|
+
[BridgeEventForCoreBox.CORE_BOX_CLIPBOARD_CHANGE]: []
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -66,3 +67,5 @@ export const createBridgeHook = <T>(type: BridgeEvent) => (hook: BridgeHook<T>)
|
|
|
66
67
|
* @param data The input change data (string).
|
|
67
68
|
*/
|
|
68
69
|
export const onCoreBoxInputChange = createBridgeHook<{ query: string }>(BridgeEventForCoreBox.CORE_BOX_INPUT_CHANGE)
|
|
70
|
+
|
|
71
|
+
export const onCoreBoxClipboardChange = createBridgeHook<{ item: any }>(BridgeEventForCoreBox.CORE_BOX_CLIPBOARD_CHANGE)
|
package/plugin/sdk/index.ts
CHANGED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ActiveAppSnapshot } from './types'
|
|
2
|
+
|
|
3
|
+
const ensurePluginChannel = () => {
|
|
4
|
+
const channel = (window as any)?.$channel
|
|
5
|
+
if (!channel) {
|
|
6
|
+
throw new Error('[Plugin SDK] System channel requires plugin renderer context with $channel available.')
|
|
7
|
+
}
|
|
8
|
+
return channel
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function getActiveAppSnapshot(options: { forceRefresh?: boolean } = {}): Promise<ActiveAppSnapshot | null> {
|
|
12
|
+
const channel = ensurePluginChannel()
|
|
13
|
+
return channel.send('system:get-active-app', options)
|
|
14
|
+
}
|
package/plugin/sdk/types.ts
CHANGED
|
@@ -4,6 +4,124 @@
|
|
|
4
4
|
* @version 1.0.0
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import type { ITouchChannel, ITouchClientChannel, StandardChannelData } from '@talex-touch/utils/channel';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Handler signature for plugin channel events.
|
|
11
|
+
*/
|
|
12
|
+
export type PluginChannelHandler = (event: StandardChannelData) => any;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Bridge exposed to plugin backends for channel-based communication.
|
|
16
|
+
*/
|
|
17
|
+
export interface IPluginChannelBridge {
|
|
18
|
+
/**
|
|
19
|
+
* Sends a payload to the main renderer process.
|
|
20
|
+
* @param eventName - Channel event name.
|
|
21
|
+
* @param payload - Optional data payload.
|
|
22
|
+
*/
|
|
23
|
+
sendToMain<T = any>(eventName: string, payload?: any): Promise<T>;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Sends a payload to this plugin's renderer view.
|
|
27
|
+
* @param eventName - Channel event name.
|
|
28
|
+
* @param payload - Optional data payload.
|
|
29
|
+
*/
|
|
30
|
+
sendToRenderer<T = any>(eventName: string, payload?: any): Promise<T>;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Registers a handler for main renderer messages.
|
|
34
|
+
* @param eventName - Channel event name to listen for.
|
|
35
|
+
* @param handler - Handler invoked with the raw channel event.
|
|
36
|
+
* @returns Unsubscribe function.
|
|
37
|
+
*/
|
|
38
|
+
onMain(eventName: string, handler: PluginChannelHandler): () => void;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Registers a handler for renderer-originated messages scoped to this plugin.
|
|
42
|
+
* @param eventName - Channel event name to listen for.
|
|
43
|
+
* @param handler - Handler invoked with the raw channel event.
|
|
44
|
+
* @returns Unsubscribe function.
|
|
45
|
+
*/
|
|
46
|
+
onRenderer(eventName: string, handler: PluginChannelHandler): () => void;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Access to the underlying channel implementation for advanced scenarios.
|
|
50
|
+
*/
|
|
51
|
+
readonly raw: ITouchChannel;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Renderer-side helper for plugin webviews to interact with the bridge channel.
|
|
56
|
+
*/
|
|
57
|
+
export interface IPluginRendererChannel {
|
|
58
|
+
/**
|
|
59
|
+
* Sends a message asynchronously and resolves with the reply payload.
|
|
60
|
+
*/
|
|
61
|
+
send<T = any>(eventName: string, payload?: any): Promise<T>;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Sends a message synchronously and returns the reply payload.
|
|
65
|
+
*/
|
|
66
|
+
sendSync<T = any>(eventName: string, payload?: any): T;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Registers a handler for renderer channel events.
|
|
70
|
+
* @returns Unsubscribe function.
|
|
71
|
+
*/
|
|
72
|
+
on(eventName: string, handler: PluginChannelHandler): () => void;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Registers a one-off handler for a renderer channel event.
|
|
76
|
+
* @returns Unsubscribe function (no-op after invocation).
|
|
77
|
+
*/
|
|
78
|
+
once(eventName: string, handler: PluginChannelHandler): () => void;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Provides access to the raw client channel.
|
|
82
|
+
*/
|
|
83
|
+
readonly raw: ITouchClientChannel;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Clipboard history item shared with plugin renderers.
|
|
88
|
+
*/
|
|
89
|
+
export interface PluginClipboardItem {
|
|
90
|
+
id?: number
|
|
91
|
+
type: 'text' | 'image' | 'files'
|
|
92
|
+
content: string
|
|
93
|
+
thumbnail?: string | null
|
|
94
|
+
rawContent?: string | null
|
|
95
|
+
sourceApp?: string | null
|
|
96
|
+
timestamp?: string | number | Date | null
|
|
97
|
+
isFavorite?: boolean | null
|
|
98
|
+
metadata?: string | null
|
|
99
|
+
meta?: Record<string, unknown> | null
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Clipboard history pagination response structure.
|
|
104
|
+
*/
|
|
105
|
+
export interface PluginClipboardHistoryResponse {
|
|
106
|
+
history: PluginClipboardItem[]
|
|
107
|
+
total: number
|
|
108
|
+
page: number
|
|
109
|
+
pageSize: number
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface ActiveAppSnapshot {
|
|
113
|
+
identifier: string | null
|
|
114
|
+
displayName: string | null
|
|
115
|
+
bundleId: string | null
|
|
116
|
+
processId: number | null
|
|
117
|
+
executablePath: string | null
|
|
118
|
+
platform: 'macos' | 'windows' | 'linux' | null
|
|
119
|
+
windowTitle: string | null
|
|
120
|
+
url?: string | null
|
|
121
|
+
icon?: string | null
|
|
122
|
+
lastUpdated: number
|
|
123
|
+
}
|
|
124
|
+
|
|
7
125
|
/**
|
|
8
126
|
* Plugin utilities interface providing core functionality for plugin development
|
|
9
127
|
*
|
|
@@ -28,6 +146,12 @@ export interface IPluginUtils {
|
|
|
28
146
|
*/
|
|
29
147
|
clipboard: IClipboardManager;
|
|
30
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Channel bridge for communicating with renderer and main processes
|
|
151
|
+
* @see {@link IPluginChannelBridge}
|
|
152
|
+
*/
|
|
153
|
+
channel: IPluginChannelBridge;
|
|
154
|
+
|
|
31
155
|
/**
|
|
32
156
|
* Search result manager for handling search operations
|
|
33
157
|
* @see {@link ISearchManager}
|