@talex-touch/utils 1.0.23 → 1.0.25

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,8 +8,44 @@ export interface TimingRecord {
8
8
  iteration?: number
9
9
  meta?: TimingMeta
10
10
  error?: unknown
11
+ logLevel?: TimingLogLevel
11
12
  }
12
13
 
14
+ export type TimingLogLevel = 'none' | 'info' | 'warn' | 'error'
15
+
16
+ export type TimingLogThresholdOverrides = Partial<Record<Exclude<TimingLogLevel, 'error'>, number>>
17
+
18
+ type ResolvedTimingLogThresholds = Record<Exclude<TimingLogLevel, 'error'>, number>
19
+
20
+ const DEFAULT_AUTO_LOG = true
21
+ const DEFAULT_STORE_HISTORY = true
22
+ const DEFAULT_HISTORY_LIMIT = 50
23
+
24
+ export const DEFAULT_TIMING_LOG_THRESHOLDS: Readonly<ResolvedTimingLogThresholds> = Object.freeze({
25
+ none: 16.7,
26
+ info: 200,
27
+ warn: 500
28
+ })
29
+
30
+ export const DEFAULT_TIMING_OPTIONS: Readonly<Required<Pick<TimingOptions, 'autoLog' | 'storeHistory'>> & {
31
+ logThresholds: Readonly<ResolvedTimingLogThresholds>
32
+ }> = Object.freeze({
33
+ autoLog: DEFAULT_AUTO_LOG,
34
+ storeHistory: DEFAULT_STORE_HISTORY,
35
+ logThresholds: DEFAULT_TIMING_LOG_THRESHOLDS
36
+ })
37
+
38
+ export const DEFAULT_TIMING_MANAGER_CONFIG: Readonly<
39
+ Required<Pick<TimingManagerConfig, 'autoLog' | 'storeHistory' | 'historyLimit'>> & {
40
+ logThresholds: Readonly<ResolvedTimingLogThresholds>
41
+ }
42
+ > = Object.freeze({
43
+ autoLog: DEFAULT_AUTO_LOG,
44
+ storeHistory: DEFAULT_STORE_HISTORY,
45
+ historyLimit: DEFAULT_HISTORY_LIMIT,
46
+ logThresholds: DEFAULT_TIMING_LOG_THRESHOLDS
47
+ })
48
+
13
49
  export interface TimingStats {
14
50
  label: string
15
51
  count: number
@@ -22,6 +58,7 @@ export interface TimingStats {
22
58
  lastEndedAt?: number
23
59
  errorCount: number
24
60
  lastError?: unknown
61
+ lastLogLevel?: TimingLogLevel
25
62
  }
26
63
 
27
64
  export interface TimingSummary extends TimingStats {
@@ -30,9 +67,11 @@ export interface TimingSummary extends TimingStats {
30
67
 
31
68
  export interface TimingManagerConfig {
32
69
  autoLog?: boolean
70
+ storeHistory?: boolean
33
71
  historyLimit?: number
34
72
  logger?: (message: string, entry: TimingRecord, stats: TimingStats) => void
35
73
  formatter?: (entry: TimingRecord, stats: TimingStats) => string
74
+ logThresholds?: TimingLogThresholdOverrides
36
75
  }
37
76
 
38
77
  export interface TimingOptions {
@@ -41,39 +80,63 @@ export interface TimingOptions {
41
80
  logger?: (message: string, entry: TimingRecord, stats: TimingStats) => void
42
81
  formatter?: (entry: TimingRecord, stats: TimingStats) => string
43
82
  historyLimit?: number
83
+ logThresholds?: TimingLogThresholdOverrides
84
+ }
85
+
86
+ type ResolvedTimingOptions = {
87
+ autoLog: boolean
88
+ storeHistory: boolean
89
+ historyLimit: number
90
+ logThresholds: ResolvedTimingLogThresholds
91
+ formatter: (entry: TimingRecord, stats: TimingStats) => string
92
+ logger: (message: string, entry: TimingRecord, stats: TimingStats) => void
44
93
  }
45
94
 
46
95
  export class TimingManager {
47
96
  private readonly stats = new Map<string, TimingStats>()
48
97
  private readonly history = new Map<string, TimingRecord[]>()
49
98
  private readonly moduleStats = new Map<string, TimingStats>()
99
+ private readonly config: TimingManagerConfig
100
+
101
+ constructor(config: TimingManagerConfig = {}) {
102
+ const mergedThresholds: ResolvedTimingLogThresholds = {
103
+ ...DEFAULT_TIMING_LOG_THRESHOLDS,
104
+ ...(config.logThresholds ?? {})
105
+ }
50
106
 
51
- constructor(private readonly config: TimingManagerConfig = {}) {}
107
+ this.config = {
108
+ ...DEFAULT_TIMING_MANAGER_CONFIG,
109
+ ...config,
110
+ logThresholds: mergedThresholds
111
+ }
112
+ }
52
113
 
53
114
  createTiming(label: string, options: TimingOptions = {}): TimingScope {
54
115
  return new TimingScope(this, label, options)
55
116
  }
56
117
 
57
118
  record(label: string, record: TimingRecord, options: TimingOptions = {}): void {
58
- const stats = this.updateStats(this.stats, label, record)
119
+ const resolved = this.resolveOptions(options)
120
+ const logLevel = determineLogLevel(record.durationMs, resolved.logThresholds)
121
+ const recordWithLevel: TimingRecord = { ...record, logLevel }
122
+
123
+ const stats = this.updateStats(this.stats, label, recordWithLevel)
59
124
  const moduleKey = this.extractModuleKey(label)
60
- this.updateStats(this.moduleStats, moduleKey, { ...record, label: moduleKey })
125
+ this.updateStats(this.moduleStats, moduleKey, { ...recordWithLevel, label: moduleKey })
61
126
 
62
- if (options.storeHistory ?? true) {
63
- const limit = options.historyLimit ?? this.config.historyLimit ?? 50
127
+ if (resolved.storeHistory) {
128
+ const limit = resolved.historyLimit
64
129
  const list = this.history.get(label) ?? []
65
- list.push(record)
130
+ list.push(recordWithLevel)
66
131
  if (list.length > limit) {
67
132
  list.splice(0, list.length - limit)
68
133
  }
69
134
  this.history.set(label, list)
70
135
  }
71
136
 
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)
137
+ if (resolved.autoLog && logLevel !== 'none') {
138
+ const message = resolved.formatter(recordWithLevel, stats)
139
+ resolved.logger(message, recordWithLevel, stats)
77
140
  }
78
141
  }
79
142
 
@@ -131,6 +194,7 @@ export class TimingManager {
131
194
  next.lastMs = durationMs
132
195
  next.lastStartedAt = record.startedAt
133
196
  next.lastEndedAt = record.endedAt
197
+ next.lastLogLevel = record.logLevel
134
198
 
135
199
  if (error) {
136
200
  next.errorCount += 1
@@ -145,6 +209,23 @@ export class TimingManager {
145
209
  const [moduleKey] = label.split(':')
146
210
  return moduleKey || label
147
211
  }
212
+
213
+ private resolveOptions(options: TimingOptions = {}): ResolvedTimingOptions {
214
+ const logThresholds: ResolvedTimingLogThresholds = {
215
+ ...DEFAULT_TIMING_LOG_THRESHOLDS,
216
+ ...(this.config.logThresholds ?? {}),
217
+ ...(options.logThresholds ?? {})
218
+ }
219
+
220
+ return {
221
+ autoLog: options.autoLog ?? this.config.autoLog ?? DEFAULT_AUTO_LOG,
222
+ storeHistory: options.storeHistory ?? this.config.storeHistory ?? DEFAULT_STORE_HISTORY,
223
+ historyLimit: options.historyLimit ?? this.config.historyLimit ?? DEFAULT_HISTORY_LIMIT,
224
+ logThresholds,
225
+ formatter: options.formatter ?? this.config.formatter ?? defaultFormatter,
226
+ logger: options.logger ?? this.config.logger ?? defaultLogger
227
+ }
228
+ }
148
229
  }
149
230
 
150
231
  export class TimingScope {
@@ -224,13 +305,49 @@ export class TimingScope {
224
305
 
225
306
  function defaultFormatter(record: TimingRecord, stats: TimingStats): string {
226
307
  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})`
308
+ const levelTag =
309
+ record.logLevel && record.logLevel !== 'info' ? ` [${record.logLevel.toUpperCase()}]` : ''
310
+ return `⏱ [${record.label}] ${duration} ms${levelTag} (avg: ${stats.avgMs.toFixed(
311
+ 2
312
+ )} ms, max: ${stats.maxMs.toFixed(2)} ms, count: ${stats.count})`
228
313
  }
229
314
 
230
- function defaultLogger(message: string): void {
315
+ function defaultLogger(message: string, entry: TimingRecord, _stats: TimingStats): void {
316
+ if (entry.logLevel === 'none') {
317
+ return
318
+ }
319
+
320
+ if (entry.logLevel === 'warn') {
321
+ console.warn(message)
322
+ return
323
+ }
324
+
325
+ if (entry.logLevel === 'error') {
326
+ console.error(message)
327
+ return
328
+ }
329
+
330
+ if (typeof console.info === 'function') {
331
+ console.info(message)
332
+ return
333
+ }
334
+
231
335
  console.log(message)
232
336
  }
233
337
 
338
+ function determineLogLevel(durationMs: number, thresholds: ResolvedTimingLogThresholds): TimingLogLevel {
339
+ if (durationMs <= thresholds.none) {
340
+ return 'none'
341
+ }
342
+ if (durationMs <= thresholds.info) {
343
+ return 'info'
344
+ }
345
+ if (durationMs <= thresholds.warn) {
346
+ return 'warn'
347
+ }
348
+ return 'error'
349
+ }
350
+
234
351
  const now = (() => {
235
352
  if (typeof globalThis !== 'undefined') {
236
353
  const perf = (globalThis as typeof globalThis & { performance?: { now?: () => number } }).performance
@@ -245,6 +362,133 @@ const timingManagerInstance = new TimingManager()
245
362
 
246
363
  export const timingManager = timingManagerInstance
247
364
 
365
+ export function startTiming(): number {
366
+ return now()
367
+ }
368
+
369
+ export function completeTiming(
370
+ label: string,
371
+ startedAt: number,
372
+ meta: TimingMeta = {},
373
+ options: TimingOptions = {}
374
+ ): number {
375
+ const endedAt = now()
376
+ const durationMs = endedAt - startedAt
377
+ timingManagerInstance.record(
378
+ label,
379
+ {
380
+ label,
381
+ durationMs,
382
+ startedAt,
383
+ endedAt,
384
+ meta
385
+ },
386
+ options
387
+ )
388
+ return durationMs
389
+ }
390
+
391
+ export function logTiming(
392
+ label: string,
393
+ durationMs: number,
394
+ meta: TimingMeta = {},
395
+ options: TimingOptions = {}
396
+ ): void {
397
+ const endedAt = now()
398
+ const startedAt = endedAt - durationMs
399
+ timingManagerInstance.record(
400
+ label,
401
+ {
402
+ label,
403
+ durationMs,
404
+ startedAt,
405
+ endedAt,
406
+ meta
407
+ },
408
+ options
409
+ )
410
+ }
411
+
412
+ export interface TimingLoggerToken {
413
+ label: string
414
+ startedAt: number
415
+ meta: TimingMeta
416
+ options: TimingOptions
417
+ }
418
+
419
+ export const timingLogger = {
420
+ start(label: string, meta: TimingMeta = {}, options: TimingOptions = {}): TimingLoggerToken {
421
+ return {
422
+ label,
423
+ startedAt: startTiming(),
424
+ meta,
425
+ options
426
+ }
427
+ },
428
+
429
+ finish(
430
+ token: TimingLoggerToken,
431
+ meta: TimingMeta = {},
432
+ overrides: TimingOptions = {}
433
+ ): number {
434
+ const mergedMeta = { ...token.meta, ...meta }
435
+ const mergedOptions = mergeTimingOptions(token.options, overrides)
436
+ return completeTiming(token.label, token.startedAt, mergedMeta, mergedOptions)
437
+ },
438
+
439
+ print(
440
+ label: string,
441
+ durationMs: number,
442
+ meta: TimingMeta = {},
443
+ options: TimingOptions = {}
444
+ ): number {
445
+ logTiming(label, durationMs, meta, options)
446
+ return durationMs
447
+ },
448
+
449
+ async cost<T>(
450
+ label: string,
451
+ fn: () => Promise<T> | T,
452
+ meta: TimingMeta = {},
453
+ options: TimingOptions = {}
454
+ ): Promise<T> {
455
+ const scope = createTiming(label, options)
456
+ return scope.cost(fn, meta)
457
+ },
458
+
459
+ mark(
460
+ label: string,
461
+ durationMs: number,
462
+ meta: TimingMeta = {},
463
+ options: TimingOptions = {}
464
+ ): number {
465
+ logTiming(label, durationMs, meta, options)
466
+ return durationMs
467
+ }
468
+ }
469
+
248
470
  export function createTiming(label: string, options: TimingOptions = {}): TimingScope {
249
471
  return timingManagerInstance.createTiming(label, options)
250
472
  }
473
+
474
+ function mergeTimingOptions(
475
+ base: TimingOptions = {},
476
+ override: TimingOptions = {}
477
+ ): TimingOptions {
478
+ if (!base && !override) return {}
479
+ if (!override || Object.keys(override).length === 0) {
480
+ return { ...base }
481
+ }
482
+ if (!base || Object.keys(base).length === 0) {
483
+ return { ...override }
484
+ }
485
+
486
+ return {
487
+ ...base,
488
+ ...override,
489
+ logThresholds: {
490
+ ...(base.logThresholds ?? {}),
491
+ ...(override.logThresholds ?? {})
492
+ }
493
+ }
494
+ }
@@ -863,12 +863,29 @@ export interface TuffMeta {
863
863
  * @description The ID of the feature.
864
864
  */
865
865
  featureId?: string
866
+
867
+ /**
868
+ * For plugin feature items, this holds the interaction configuration.
869
+ * @description Defines how the feature should be rendered (widget, webcontent, or index).
870
+ */
871
+ interaction?: {
872
+ type: 'webcontent' | 'widget' | 'index'
873
+ path?: string
874
+ }
875
+
866
876
  /**
867
877
  * Defines the default action to be taken when the item is executed (e.g., by pressing Enter).
868
878
  * This is used to distinguish simple actions (like 'copy') from feature activations.
869
879
  * @description The default action type.
870
880
  */
871
881
  defaultAction?: string;
882
+
883
+ /**
884
+ * Priority of the item for sorting in search results
885
+ * Higher numbers have higher priority (displayed first)
886
+ * @description Priority value for search result ordering
887
+ */
888
+ priority?: number;
872
889
  /**
873
890
  * 原始数据
874
891
  * @description 项目的原始数据对象,用于特殊处理
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "module": "./index.ts",
6
6
  "license": "MPL-2.0",
7
7
  "private": false,
8
- "version": "1.0.23",
8
+ "version": "1.0.25",
9
9
  "scripts": {
10
10
  "publish": "npm publish --access public"
11
11
  },
package/plugin/index.ts CHANGED
@@ -59,7 +59,7 @@ export interface IPluginBaseInfo {
59
59
  export interface IPluginDev {
60
60
  enable: boolean
61
61
  address: string
62
- source?: string // 修改此处,允许 source 为字符串或 undefined
62
+ source?: boolean
63
63
  }
64
64
 
65
65
  export interface ITouchPlugin extends IPluginBaseInfo {
@@ -81,6 +81,47 @@ export interface ITouchPlugin extends IPluginBaseInfo {
81
81
 
82
82
  enable(): Promise<boolean>
83
83
  disable(): Promise<boolean>
84
+
85
+ /**
86
+ * Get the plugin file.
87
+ * @param fileName The name of the file.
88
+ * @returns The content of the file.
89
+ */
90
+ getPluginFile(fileName: string): object
91
+
92
+ /**
93
+ * Save the plugin file.
94
+ * @param fileName The name of the file.
95
+ * @param content The content of the file.
96
+ * @returns The result of the save operation.
97
+ */
98
+ savePluginFile(fileName: string, content: object): { success: boolean; error?: string }
99
+
100
+ /**
101
+ * Delete the plugin file.
102
+ * @param fileName The name of the file.
103
+ * @returns The result of the delete operation.
104
+ */
105
+ deletePluginFile(fileName: string): { success: boolean; error?: string }
106
+
107
+ /**
108
+ * List all files in the plugin.
109
+ * @returns The list of files.
110
+ */
111
+ listPluginFiles(): string[]
112
+
113
+ /**
114
+ * Get the plugin configuration.
115
+ * @returns The configuration content.
116
+ */
117
+ getPluginConfig(): object
118
+
119
+ /**
120
+ * Save the plugin configuration.
121
+ * @param content The configuration content.
122
+ * @returns The result of the save operation.
123
+ */
124
+ savePluginConfig(content: object): { success: boolean; error?: string }
84
125
  }
85
126
 
86
127
  export interface IFeatureCommand {
@@ -98,6 +139,12 @@ export interface IPluginFeature {
98
139
  platform: IPlatform
99
140
  commands: IFeatureCommand[]
100
141
  interaction?: IFeatureInteraction
142
+ /**
143
+ * Priority of the feature for sorting in search results
144
+ * Higher numbers have higher priority (displayed first)
145
+ * Default is 0
146
+ */
147
+ priority?: number
101
148
  }
102
149
 
103
150
  export type IFeatureInteraction = {
@@ -113,6 +160,18 @@ export type IFeatureInteraction = {
113
160
  * These hooks are triggered based on real user interaction and system events.
114
161
  */
115
162
  export interface IFeatureLifeCycle {
163
+ /**
164
+ * onInit is called when the feature is initialized.
165
+ * Can be used to prepare data or UI specific to this session.
166
+ */
167
+ onInit?(): void
168
+
169
+ /**
170
+ * Called when a message is received from the main application.
171
+ * @param key - The key of the message
172
+ * @param info - The information of the message
173
+ */
174
+ onMessage?(key: string, info: any): void
116
175
  /**
117
176
  * Called when a feature is actively launched from the launcher.
118
177
  * Can be used to prepare data or UI specific to this session.
@@ -156,8 +215,16 @@ export interface IFeatureLifeCycle {
156
215
  * This is used for handling actions on the items themselves,
157
216
  * rather than triggering a new feature.
158
217
  * @param item The TuffItem that was executed.
218
+ * @returns Object indicating whether to activate the feature and any activation data
159
219
  */
160
- onItemAction?(item: any): Promise<any>
220
+ onItemAction?(item: any): Promise<{
221
+ /** Whether the action executed an external operation (e.g., opened browser) */
222
+ externalAction?: boolean
223
+ /** Whether the feature should be activated after this action */
224
+ shouldActivate?: boolean
225
+ /** Activation data if shouldActivate is true */
226
+ activation?: any
227
+ } | void>
161
228
  }
162
229
 
163
230
  /**
@@ -206,8 +273,16 @@ export interface ITargetFeatureLifeCycle {
206
273
  * This is used for handling actions on the items themselves,
207
274
  * rather than triggering a new feature.
208
275
  * @param item The TuffItem that was executed.
276
+ * @returns Object indicating whether to activate the feature and any activation data
209
277
  */
210
- onItemAction?(item: any): Promise<any>
278
+ onItemAction?(item: any): Promise<{
279
+ /** Whether the action executed an external operation (e.g., opened browser) */
280
+ externalAction?: boolean
281
+ /** Whether the feature should be activated after this action */
282
+ shouldActivate?: boolean
283
+ /** Activation data if shouldActivate is true */
284
+ activation?: any
285
+ } | void>
211
286
  }
212
287
 
213
288
  /**
@@ -1,28 +1,77 @@
1
- import { genChannel } from '../channel';
2
- import { IPluginFeature } from '../index'
1
+ /**
2
+ * Plugin SDK Common Utilities
3
+ *
4
+ * @description
5
+ * 提供插件SDK的通用功能,包括通信、快捷键等
6
+ */
3
7
 
4
- export function regShortcut(key: string, func: Function) {
5
- const channel = genChannel()
8
+ import { genChannel } from '../channel'
6
9
 
7
- const res = channel.sendSync('shortcon:reg', { key })
8
- if ( res instanceof String ) throw new Error(String(res))
9
- if ( res === false ) return false;
10
+ /**
11
+ * Register a shortcut
12
+ * @param key - The shortcut combination
13
+ * @param func - The trigger function
14
+ * @returns Whether the shortcut is registered successfully
15
+ */
16
+ export function regShortcut(key: string, func: Function): boolean {
17
+ const channel = genChannel()
18
+
19
+ const res = channel.sendSync('shortcon:reg', { key })
20
+ if (res instanceof String) throw new Error(String(res))
21
+ if (res === false) return false
10
22
 
11
- channel.regChannel('shortcon:trigger', ({ data }) => key === data.key && func())
23
+ channel.regChannel('shortcon:trigger', ({ data }) => key === data.key && func())
12
24
 
13
- return true;
25
+ return true
14
26
  }
15
27
 
16
- export function regFeature(feature: IPluginFeature): boolean {
28
+ /**
29
+ * Communicate with other plugins via the index:communicate channel
30
+ * @param key - The message key
31
+ * @param info - The message data
32
+ * @returns Promise<any> The communication result
33
+ */
34
+ export async function communicateWithPlugin(
35
+ key: string,
36
+ info: any = {}
37
+ ): Promise<any> {
17
38
  const channel = genChannel()
18
39
 
19
- return channel.sendSync('feature:reg', { feature })
40
+ try {
41
+ return await channel.send('index:communicate', {
42
+ key,
43
+ info
44
+ })
45
+ } catch (error) {
46
+ console.error(`[Plugin SDK] Failed to communicate`, error)
47
+ throw error
48
+ }
20
49
  }
21
50
 
22
- export function unRegFeature(id: string): boolean {
51
+ /**
52
+ * Send a message to the main application
53
+ * @param message - The message type
54
+ * @param data - The message data
55
+ * @returns Promise<any> The message result
56
+ */
57
+ export async function sendMessage(message: string, data: any = {}): Promise<any> {
23
58
  const channel = genChannel()
24
59
 
25
- return channel.sendSync('feature:unreg', { feature: id })
60
+ try {
61
+ return await channel.send(`plugin:${message}`, data)
62
+ } catch (error) {
63
+ console.error(`[Plugin SDK] Failed to send message: ${message}`, error)
64
+ throw error
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Get the channel object for the plugin
70
+ * Attention: Using this function make sure you know what you are doing.
71
+ * @returns The channel object for the plugin
72
+ */
73
+ export function getChannel() {
74
+ return genChannel()
26
75
  }
27
76
 
28
- export * from './window';
77
+ export * from './window'
@@ -0,0 +1,27 @@
1
+ import type { IPluginRendererChannel } from './types'
2
+
3
+ const ensurePluginContext = (): { channel: IPluginRendererChannel; pluginName: string } => {
4
+ const plugin = (window as any)?.$plugin
5
+ if (!plugin?.name) {
6
+ throw new Error('[TouchSDK] Unable to resolve plugin name inside renderer context.')
7
+ }
8
+
9
+ const channel = (window as any)?.$channel as IPluginRendererChannel | undefined
10
+ if (!channel) {
11
+ throw new Error('[TouchSDK] Channel bridge is not available for the current plugin renderer.')
12
+ }
13
+
14
+ return {
15
+ channel,
16
+ pluginName: plugin.name as string
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Clears all CoreBox items associated with the current plugin.
22
+ */
23
+ export async function clearCoreBoxItems(): Promise<void> {
24
+ const { channel, pluginName } = ensurePluginContext()
25
+ await channel.send('core-box:clear-items', { pluginName })
26
+ }
27
+