@talex-touch/utils 1.0.42 → 1.0.45

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.
Files changed (234) hide show
  1. package/.eslintcache +1 -0
  2. package/__tests__/cloud-sync-sdk.test.ts +442 -0
  3. package/__tests__/icons/icons.test.ts +84 -0
  4. package/__tests__/plugin-sdk-lifecycle.test.ts +130 -0
  5. package/__tests__/power-sdk.test.ts +143 -0
  6. package/__tests__/preset-export-types.test.ts +108 -0
  7. package/__tests__/search/fuzzy-match.test.ts +137 -0
  8. package/__tests__/transport/port-policy.test.ts +44 -0
  9. package/__tests__/transport-domain-sdks.test.ts +152 -0
  10. package/__tests__/types/update.test.ts +67 -0
  11. package/account/account-sdk.ts +915 -0
  12. package/account/index.ts +2 -0
  13. package/account/types.ts +321 -0
  14. package/analytics/client.ts +136 -0
  15. package/analytics/index.ts +2 -0
  16. package/analytics/types.ts +156 -0
  17. package/animation/auto-resize.ts +322 -0
  18. package/animation/window-node.ts +26 -19
  19. package/auth/clerk-types.ts +12 -30
  20. package/auth/index.ts +0 -2
  21. package/auth/useAuthState.ts +6 -14
  22. package/base/index.ts +2 -0
  23. package/base/log-level.ts +105 -0
  24. package/channel/index.ts +170 -69
  25. package/cloud-sync/cloud-sync-sdk.ts +450 -0
  26. package/cloud-sync/index.ts +1 -0
  27. package/common/file-scan-utils.ts +17 -9
  28. package/common/index.ts +4 -0
  29. package/common/logger/index.ts +46 -0
  30. package/common/logger/logger-manager.ts +303 -0
  31. package/common/logger/module-logger.ts +270 -0
  32. package/common/logger/transport-logger.ts +234 -0
  33. package/common/logger/types.ts +93 -0
  34. package/common/search/gather.ts +48 -6
  35. package/common/search/index.ts +8 -0
  36. package/common/storage/constants.ts +13 -0
  37. package/common/storage/entity/app-settings.ts +245 -0
  38. package/common/storage/entity/index.ts +3 -0
  39. package/common/storage/entity/layout-atom-types.ts +147 -0
  40. package/common/storage/entity/openers.ts +1 -0
  41. package/common/storage/entity/preset-cloud-api.ts +132 -0
  42. package/common/storage/entity/preset-export-types.ts +256 -0
  43. package/common/storage/entity/shortcut-settings.ts +1 -0
  44. package/common/storage/shortcut-storage.ts +11 -0
  45. package/common/utils/clone-diagnostics.ts +105 -0
  46. package/common/utils/file.ts +16 -8
  47. package/common/utils/index.ts +6 -2
  48. package/common/utils/payload-preview.ts +173 -0
  49. package/common/utils/polling.ts +167 -13
  50. package/common/utils/safe-path.ts +103 -0
  51. package/common/utils/safe-shell.ts +115 -0
  52. package/common/utils/task-queue.ts +4 -1
  53. package/core-box/builder/tuff-builder.ts +0 -1
  54. package/core-box/index.ts +1 -1
  55. package/core-box/recommendation.ts +38 -1
  56. package/core-box/tuff/tuff-dsl.ts +32 -0
  57. package/electron/download-manager.ts +10 -7
  58. package/electron/env-tool.ts +42 -40
  59. package/electron/index.ts +0 -1
  60. package/env/index.ts +156 -0
  61. package/eslint.config.js +55 -0
  62. package/i18n/index.ts +62 -0
  63. package/i18n/locales/en.json +226 -0
  64. package/i18n/locales/zh.json +226 -0
  65. package/i18n/message-keys.ts +236 -0
  66. package/i18n/resolver.ts +181 -0
  67. package/icons/index.ts +257 -0
  68. package/icons/svg.ts +69 -0
  69. package/index.ts +9 -1
  70. package/intelligence/client.ts +72 -42
  71. package/market/constants.ts +9 -5
  72. package/market/index.ts +1 -1
  73. package/market/types.ts +19 -4
  74. package/package.json +15 -5
  75. package/permission/index.ts +143 -46
  76. package/permission/legacy.ts +26 -0
  77. package/permission/registry.ts +304 -0
  78. package/permission/types.ts +164 -0
  79. package/plugin/channel.ts +68 -39
  80. package/plugin/index.ts +80 -7
  81. package/plugin/install.ts +3 -0
  82. package/plugin/log/types.ts +22 -5
  83. package/plugin/node/logger-manager.ts +11 -3
  84. package/plugin/node/logger.ts +24 -17
  85. package/plugin/preload.ts +25 -2
  86. package/plugin/providers/index.ts +4 -4
  87. package/plugin/providers/market-client.ts +6 -3
  88. package/plugin/providers/npm-provider.ts +22 -7
  89. package/plugin/providers/tpex-provider.ts +22 -8
  90. package/plugin/sdk/box-items.ts +14 -0
  91. package/plugin/sdk/box-sdk.ts +64 -0
  92. package/plugin/sdk/channel.ts +119 -4
  93. package/plugin/sdk/clipboard.ts +26 -12
  94. package/plugin/sdk/cloud-sync.ts +113 -0
  95. package/plugin/sdk/common.ts +19 -11
  96. package/plugin/sdk/core-box.ts +6 -15
  97. package/plugin/sdk/division-box.ts +160 -65
  98. package/plugin/sdk/examples/storage-onDidChange-example.js +5 -2
  99. package/plugin/sdk/feature-sdk.ts +111 -76
  100. package/plugin/sdk/flow.ts +146 -45
  101. package/plugin/sdk/hooks/bridge.ts +13 -6
  102. package/plugin/sdk/hooks/life-cycle.ts +35 -16
  103. package/plugin/sdk/index.ts +14 -3
  104. package/plugin/sdk/intelligence.ts +87 -0
  105. package/plugin/sdk/meta/README.md +179 -0
  106. package/plugin/sdk/meta-sdk.ts +244 -0
  107. package/plugin/sdk/notification.ts +9 -0
  108. package/plugin/sdk/plugin-info.ts +64 -0
  109. package/plugin/sdk/power.ts +155 -0
  110. package/plugin/sdk/recommend.ts +21 -0
  111. package/plugin/sdk/service/index.ts +12 -8
  112. package/plugin/sdk/sqlite.ts +141 -0
  113. package/plugin/sdk/storage.ts +2 -6
  114. package/plugin/sdk/system.ts +2 -9
  115. package/plugin/sdk/temp-files.ts +41 -0
  116. package/plugin/sdk/touch-sdk.ts +18 -0
  117. package/plugin/sdk/types.ts +44 -4
  118. package/plugin/sdk/window/index.ts +12 -9
  119. package/plugin/sdk-version.ts +231 -0
  120. package/preload/renderer.ts +3 -2
  121. package/renderer/hooks/arg-mapper.ts +16 -2
  122. package/renderer/hooks/index.ts +13 -0
  123. package/renderer/hooks/initialize.ts +2 -1
  124. package/renderer/hooks/use-agent-market-sdk.ts +7 -0
  125. package/renderer/hooks/use-agent-market.ts +106 -0
  126. package/renderer/hooks/use-agents-sdk.ts +7 -0
  127. package/renderer/hooks/use-app-sdk.ts +7 -0
  128. package/renderer/hooks/use-channel.ts +33 -4
  129. package/renderer/hooks/use-download-sdk.ts +21 -0
  130. package/renderer/hooks/use-intelligence-sdk.ts +7 -0
  131. package/renderer/hooks/use-intelligence-stats.ts +290 -0
  132. package/renderer/hooks/use-intelligence.ts +55 -214
  133. package/renderer/hooks/use-market-sdk.ts +16 -0
  134. package/renderer/hooks/use-notification-sdk.ts +7 -0
  135. package/renderer/hooks/use-permission-sdk.ts +7 -0
  136. package/renderer/hooks/use-permission.ts +325 -0
  137. package/renderer/hooks/use-platform-sdk.ts +7 -0
  138. package/renderer/hooks/use-plugin-sdk.ts +16 -0
  139. package/renderer/hooks/use-settings-sdk.ts +7 -0
  140. package/renderer/hooks/use-update-sdk.ts +21 -0
  141. package/renderer/index.ts +1 -0
  142. package/renderer/ref.ts +19 -10
  143. package/renderer/shared/components/SharedPluginDetailContent.vue +84 -0
  144. package/renderer/shared/components/SharedPluginDetailHeader.vue +116 -0
  145. package/renderer/shared/components/SharedPluginDetailMetaList.vue +39 -0
  146. package/renderer/shared/components/SharedPluginDetailReadme.vue +45 -0
  147. package/renderer/shared/components/SharedPluginDetailVersions.vue +98 -0
  148. package/renderer/shared/components/index.ts +5 -0
  149. package/renderer/shared/components/shims-vue.d.ts +5 -0
  150. package/renderer/shared/index.ts +2 -0
  151. package/renderer/shared/plugin-detail.ts +62 -0
  152. package/renderer/storage/app-settings.ts +3 -1
  153. package/renderer/storage/base-storage.ts +508 -82
  154. package/renderer/storage/intelligence-storage.ts +31 -40
  155. package/renderer/storage/openers.ts +3 -1
  156. package/renderer/storage/storage-subscription.ts +126 -42
  157. package/renderer/touch-sdk/env.ts +10 -10
  158. package/renderer/touch-sdk/index.ts +114 -18
  159. package/renderer/touch-sdk/terminal.ts +24 -13
  160. package/search/feature-matcher.ts +279 -0
  161. package/search/fuzzy-match.ts +64 -34
  162. package/search/index.ts +10 -0
  163. package/search/levenshtein-utils.ts +17 -11
  164. package/transport/errors.ts +310 -0
  165. package/transport/event/builder.ts +378 -0
  166. package/transport/event/index.ts +7 -0
  167. package/transport/event/types.ts +292 -0
  168. package/transport/events/index.ts +2690 -0
  169. package/transport/events/meta-overlay.ts +79 -0
  170. package/transport/events/types/agents.ts +177 -0
  171. package/transport/events/types/app-index.ts +20 -0
  172. package/transport/events/types/app.ts +475 -0
  173. package/transport/events/types/box-item.ts +222 -0
  174. package/transport/events/types/clipboard.ts +80 -0
  175. package/transport/events/types/core-box.ts +534 -0
  176. package/transport/events/types/device-idle.ts +7 -0
  177. package/transport/events/types/division-box.ts +99 -0
  178. package/transport/events/types/download.ts +115 -0
  179. package/transport/events/types/file-index.ts +84 -0
  180. package/transport/events/types/flow.ts +149 -0
  181. package/transport/events/types/index.ts +70 -0
  182. package/transport/events/types/market.ts +39 -0
  183. package/transport/events/types/meta-overlay.ts +184 -0
  184. package/transport/events/types/notification.ts +140 -0
  185. package/transport/events/types/permission.ts +90 -0
  186. package/transport/events/types/platform.ts +8 -0
  187. package/transport/events/types/plugin.ts +631 -0
  188. package/transport/events/types/sentry.ts +20 -0
  189. package/transport/events/types/storage.ts +208 -0
  190. package/transport/events/types/transport.ts +60 -0
  191. package/transport/events/types/tray.ts +16 -0
  192. package/transport/events/types/update.ts +78 -0
  193. package/transport/index.ts +141 -0
  194. package/transport/main.ts +2 -0
  195. package/transport/prelude.ts +208 -0
  196. package/transport/sdk/constants.ts +29 -0
  197. package/transport/sdk/domains/agents-market.ts +47 -0
  198. package/transport/sdk/domains/agents.ts +62 -0
  199. package/transport/sdk/domains/app.ts +48 -0
  200. package/transport/sdk/domains/disposable.ts +35 -0
  201. package/transport/sdk/domains/download.ts +139 -0
  202. package/transport/sdk/domains/index.ts +13 -0
  203. package/transport/sdk/domains/intelligence.ts +616 -0
  204. package/transport/sdk/domains/market.ts +35 -0
  205. package/transport/sdk/domains/notification.ts +62 -0
  206. package/transport/sdk/domains/permission.ts +85 -0
  207. package/transport/sdk/domains/platform.ts +19 -0
  208. package/transport/sdk/domains/plugin.ts +144 -0
  209. package/transport/sdk/domains/settings.ts +102 -0
  210. package/transport/sdk/domains/update.ts +64 -0
  211. package/transport/sdk/index.ts +60 -0
  212. package/transport/sdk/main-transport.ts +710 -0
  213. package/transport/sdk/main.ts +9 -0
  214. package/transport/sdk/plugin-transport.ts +654 -0
  215. package/transport/sdk/port-policy.ts +38 -0
  216. package/transport/sdk/renderer-transport.ts +1165 -0
  217. package/transport/types.ts +605 -0
  218. package/types/agent.ts +399 -0
  219. package/types/cloud-sync.ts +157 -0
  220. package/types/division-box.ts +31 -31
  221. package/types/download.ts +1 -0
  222. package/types/flow.ts +63 -12
  223. package/types/icon.ts +2 -1
  224. package/types/index.ts +5 -0
  225. package/types/intelligence.ts +166 -173
  226. package/types/modules/base.ts +2 -0
  227. package/types/path-browserify.d.ts +5 -0
  228. package/types/platform.ts +12 -0
  229. package/types/startup-info.ts +32 -0
  230. package/types/touch-app-core.ts +8 -8
  231. package/types/update.ts +94 -1
  232. package/vitest.config.ts +25 -0
  233. package/auth/useClerkConfig.ts +0 -40
  234. package/auth/useClerkProvider.ts +0 -52
@@ -1,4 +1,3 @@
1
- /* eslint-disable no-console */
2
1
  /**
3
2
  * @module polling
4
3
  * A high-precision, efficient, singleton polling service for scheduling periodic tasks.
@@ -13,7 +12,16 @@ interface PollingTask {
13
12
  nextRunMs: number
14
13
  }
15
14
 
16
- type TimeUnit = 'seconds' | 'minutes' | 'hours'
15
+ type PollingTaskStats = {
16
+ count: number
17
+ lastStartAt: number
18
+ lastEndAt: number
19
+ lastDurationMs: number
20
+ maxDurationMs: number
21
+ lastMeta?: Record<string, unknown>
22
+ }
23
+
24
+ type TimeUnit = 'milliseconds' | 'seconds' | 'minutes' | 'hours'
17
25
 
18
26
  export class PollingService {
19
27
  private static instance: PollingService
@@ -21,6 +29,9 @@ export class PollingService {
21
29
  private timerId: NodeJS.Timeout | null = null
22
30
  private isRunning = false
23
31
  private quitListenerCleanup?: () => void
32
+ private activeTasks = new Map<string, number>()
33
+ private startAttempts = new Map<string, { count: number, lastAt: number }>()
34
+ private taskStats = new Map<string, PollingTaskStats>()
24
35
 
25
36
  private constructor() {
26
37
  // Private constructor to enforce singleton pattern
@@ -35,6 +46,8 @@ export class PollingService {
35
46
 
36
47
  private convertToMs(interval: number, unit: TimeUnit): number {
37
48
  switch (unit) {
49
+ case 'milliseconds':
50
+ return interval
38
51
  case 'seconds':
39
52
  return interval * 1000
40
53
  case 'minutes':
@@ -55,10 +68,17 @@ export class PollingService {
55
68
  public register(
56
69
  id: string,
57
70
  callback: () => void | Promise<void>,
58
- options: { interval: number, unit: TimeUnit, runImmediately?: boolean },
71
+ options: {
72
+ interval: number
73
+ unit: TimeUnit
74
+ runImmediately?: boolean
75
+ initialDelayMs?: number
76
+ },
59
77
  ): void {
60
78
  if (this.tasks.has(id)) {
61
- console.warn(`[PollingService] Task with ID '${id}' is already registered. Overwriting.`)
79
+ if (this.shouldVerboseLog()) {
80
+ console.warn(`[PollingService] Task with ID '${id}' is already registered. Overwriting.`)
81
+ }
62
82
  }
63
83
 
64
84
  const intervalMs = this.convertToMs(options.interval, options.unit)
@@ -67,7 +87,12 @@ export class PollingService {
67
87
  return
68
88
  }
69
89
 
70
- const nextRunMs = options.runImmediately ? Date.now() : Date.now() + intervalMs
90
+ const now = Date.now()
91
+ const nextRunMs = options.runImmediately
92
+ ? now
93
+ : typeof options.initialDelayMs === 'number'
94
+ ? now + Math.max(0, options.initialDelayMs)
95
+ : now + intervalMs
71
96
 
72
97
  this.tasks.set(id, {
73
98
  id,
@@ -76,26 +101,45 @@ export class PollingService {
76
101
  nextRunMs,
77
102
  })
78
103
 
79
- console.debug(`[PollingService] Task '${id}' registered to run every ${options.interval} ${options.unit}.`)
104
+ if (this.shouldVerboseLog()) {
105
+ console.debug(`[PollingService] Task '${id}' registered to run every ${options.interval} ${options.unit}.`)
106
+ }
80
107
 
81
108
  if (this.isRunning) {
82
109
  this._reschedule()
83
110
  }
84
111
  }
85
112
 
113
+ public setTaskMeta(id: string, meta: Record<string, unknown> | null): void {
114
+ if (!meta) return
115
+ const stats = this.taskStats.get(id) ?? {
116
+ count: 0,
117
+ lastStartAt: 0,
118
+ lastEndAt: 0,
119
+ lastDurationMs: 0,
120
+ maxDurationMs: 0,
121
+ }
122
+ stats.lastMeta = meta
123
+ this.taskStats.set(id, stats)
124
+ }
125
+
86
126
  /**
87
127
  * Unregisters a task, preventing it from being executed in the future.
88
128
  * @param id - The unique identifier of the task to remove.
89
129
  */
90
130
  public unregister(id: string): void {
91
131
  if (this.tasks.delete(id)) {
92
- console.log(`[PollingService] Task '${id}' unregistered.`)
132
+ if (this.shouldVerboseLog()) {
133
+ console.debug(`[PollingService] Task '${id}' unregistered.`)
134
+ }
93
135
  if (this.isRunning) {
94
136
  this._reschedule()
95
137
  }
96
138
  }
97
139
  else {
98
- console.warn(`[PollingService] Attempted to unregister a non-existent task with ID '${id}'.`)
140
+ if (this.shouldVerboseLog()) {
141
+ console.warn(`[PollingService] Attempted to unregister a non-existent task with ID '${id}'.`)
142
+ }
99
143
  }
100
144
  }
101
145
 
@@ -114,11 +158,13 @@ export class PollingService {
114
158
  */
115
159
  public start(): void {
116
160
  if (this.isRunning) {
117
- console.warn('[PollingService] Already running, skipping start.')
161
+ this.recordStartAttempt()
118
162
  return
119
163
  }
120
164
  this.isRunning = true
121
- console.log('[PollingService] Polling service started')
165
+ if (this.shouldVerboseLog()) {
166
+ console.log('[PollingService] Polling service started')
167
+ }
122
168
  this._setupQuitListener()
123
169
  this._reschedule()
124
170
  }
@@ -151,7 +197,7 @@ export class PollingService {
151
197
  }
152
198
  }
153
199
  }
154
- catch (error) {
200
+ catch {
155
201
  // Not in Electron environment or Electron not available
156
202
  // This is fine, just skip the quit listener setup
157
203
  }
@@ -178,10 +224,14 @@ export class PollingService {
178
224
  }
179
225
 
180
226
  if (reason) {
181
- console.log(`[PollingService] Stopping polling service: ${reason}`)
227
+ if (this.shouldVerboseLog()) {
228
+ console.log(`[PollingService] Stopping polling service: ${reason}`)
229
+ }
182
230
  }
183
231
  else {
184
- console.log('[PollingService] Polling service stopped')
232
+ if (this.shouldVerboseLog()) {
233
+ console.log('[PollingService] Polling service stopped')
234
+ }
185
235
  }
186
236
  }
187
237
 
@@ -217,12 +267,35 @@ export class PollingService {
217
267
  if (tasksToRun.length > 0) {
218
268
  // console.debug(`[PollingService] Executing ${tasksToRun.length} tasks.`);
219
269
  for (const task of tasksToRun) {
270
+ const startAt = Date.now()
271
+ this.activeTasks.set(task.id, startAt)
220
272
  try {
221
273
  await task.callback()
222
274
  }
223
275
  catch (error) {
224
276
  console.error(`[PollingService] Error executing task '${task.id}':`, error)
225
277
  }
278
+ finally {
279
+ const endAt = Date.now()
280
+ const durationMs = Math.max(0, endAt - startAt)
281
+ const stats = this.taskStats.get(task.id) ?? {
282
+ count: 0,
283
+ lastStartAt: 0,
284
+ lastEndAt: 0,
285
+ lastDurationMs: 0,
286
+ maxDurationMs: 0,
287
+ }
288
+ if (this.taskStats.get(task.id)?.lastMeta) {
289
+ stats.lastMeta = this.taskStats.get(task.id)?.lastMeta
290
+ }
291
+ stats.count += 1
292
+ stats.lastStartAt = startAt
293
+ stats.lastEndAt = endAt
294
+ stats.lastDurationMs = durationMs
295
+ stats.maxDurationMs = Math.max(stats.maxDurationMs, durationMs)
296
+ this.taskStats.set(task.id, stats)
297
+ this.activeTasks.delete(task.id)
298
+ }
226
299
  // Update next run time based on its last scheduled run time, not 'now'.
227
300
  // This prevents drift if a task takes a long time to execute.
228
301
  task.nextRunMs += task.intervalMs
@@ -231,6 +304,87 @@ export class PollingService {
231
304
 
232
305
  this._reschedule()
233
306
  }
307
+
308
+ public getDiagnostics(): {
309
+ activeTasks: Array<{
310
+ id: string
311
+ startedAt: number
312
+ ageMs: number
313
+ intervalMs?: number
314
+ nextRunMs?: number
315
+ lastDurationMs?: number
316
+ maxDurationMs?: number
317
+ count?: number
318
+ lastMeta?: Record<string, unknown>
319
+ }>
320
+ recentTasks: Array<{
321
+ id: string
322
+ lastDurationMs: number
323
+ lastEndAt: number
324
+ count: number
325
+ maxDurationMs: number
326
+ intervalMs?: number
327
+ nextRunMs?: number
328
+ lastMeta?: Record<string, unknown>
329
+ }>
330
+ startAttempts: Array<{ caller: string, count: number, ageMs: number }>
331
+ } {
332
+ const now = Date.now()
333
+ const activeTasks = Array.from(this.activeTasks.entries())
334
+ .map(([id, startedAt]) => ({
335
+ id,
336
+ startedAt,
337
+ ageMs: Math.max(0, now - startedAt),
338
+ intervalMs: this.tasks.get(id)?.intervalMs,
339
+ nextRunMs: this.tasks.get(id)?.nextRunMs,
340
+ lastDurationMs: this.taskStats.get(id)?.lastDurationMs,
341
+ maxDurationMs: this.taskStats.get(id)?.maxDurationMs,
342
+ count: this.taskStats.get(id)?.count,
343
+ lastMeta: this.taskStats.get(id)?.lastMeta,
344
+ }))
345
+ .sort((a, b) => b.ageMs - a.ageMs)
346
+ .slice(0, 6)
347
+ const recentTasks = Array.from(this.taskStats.entries())
348
+ .map(([id, stat]) => ({
349
+ id,
350
+ lastDurationMs: stat.lastDurationMs,
351
+ lastEndAt: stat.lastEndAt,
352
+ count: stat.count,
353
+ maxDurationMs: stat.maxDurationMs,
354
+ intervalMs: this.tasks.get(id)?.intervalMs,
355
+ nextRunMs: this.tasks.get(id)?.nextRunMs,
356
+ lastMeta: stat.lastMeta,
357
+ }))
358
+ .sort((a, b) => b.lastEndAt - a.lastEndAt)
359
+ .slice(0, 6)
360
+ const startAttempts = Array.from(this.startAttempts.entries())
361
+ .map(([caller, stat]) => ({
362
+ caller,
363
+ count: stat.count,
364
+ ageMs: Math.max(0, now - stat.lastAt),
365
+ }))
366
+ .sort((a, b) => b.count - a.count)
367
+ .slice(0, 5)
368
+
369
+ return {
370
+ activeTasks,
371
+ recentTasks,
372
+ startAttempts,
373
+ }
374
+ }
375
+
376
+ private recordStartAttempt(): void {
377
+ const stack = new Error().stack
378
+ const caller = stack?.split('\n')[2]?.trim() ?? 'unknown'
379
+ const entry = this.startAttempts.get(caller) ?? { count: 0, lastAt: 0 }
380
+ entry.count += 1
381
+ entry.lastAt = Date.now()
382
+ this.startAttempts.set(caller, entry)
383
+ }
384
+
385
+ private shouldVerboseLog(): boolean {
386
+ return (globalThis as any).__TALEX_VERBOSE_LOGS__ === true
387
+ }
234
388
  }
235
389
 
236
390
  export const pollingService = PollingService.getInstance()
@@ -0,0 +1,103 @@
1
+ import pathBrowserify from 'path-browserify'
2
+ import { hasWindow } from '../../env'
3
+
4
+ const path = (() => {
5
+ if (hasWindow()) {
6
+ return pathBrowserify
7
+ }
8
+
9
+ const nodeRequire = typeof require === 'function' ? require : null
10
+ if (nodeRequire) {
11
+ try {
12
+ return nodeRequire('node:path')
13
+ } catch {
14
+ return pathBrowserify
15
+ }
16
+ }
17
+
18
+ return pathBrowserify
19
+ })()
20
+
21
+ export interface SafePathResult {
22
+ resolvedPath: string | null
23
+ error?: string
24
+ }
25
+
26
+ export interface SafePathOptions {
27
+ allowAbsolute?: boolean
28
+ allowRoot?: boolean
29
+ }
30
+
31
+ const NULL_BYTE_PATTERN = /\0/
32
+
33
+ export function isAbsolutePath(value: string): boolean {
34
+ return path.isAbsolute(value) || path.win32.isAbsolute(value)
35
+ }
36
+
37
+ export function isSafePathSegment(value: string): boolean {
38
+ const trimmed = value.trim()
39
+ if (!trimmed || trimmed === '.' || trimmed === '..') return false
40
+ if (NULL_BYTE_PATTERN.test(trimmed)) return false
41
+ if (trimmed.includes('/') || trimmed.includes('\\')) return false
42
+ return true
43
+ }
44
+
45
+ export function normalizeAbsolutePath(value: string): string | null {
46
+ const trimmed = value.trim()
47
+ if (!trimmed) return null
48
+ if (!isAbsolutePath(trimmed)) return null
49
+ if (NULL_BYTE_PATTERN.test(trimmed)) return null
50
+ return path.normalize(trimmed)
51
+ }
52
+
53
+ export function resolveSafePath(
54
+ baseDir: string,
55
+ targetPath: string,
56
+ options: SafePathOptions = {}
57
+ ): SafePathResult {
58
+ const allowAbsolute = options.allowAbsolute ?? false
59
+ const allowRoot = options.allowRoot ?? false
60
+
61
+ const base = baseDir.trim()
62
+ const target = targetPath.trim()
63
+ if (!base || !target) {
64
+ return { resolvedPath: null, error: 'PATH_EMPTY' }
65
+ }
66
+
67
+ if (NULL_BYTE_PATTERN.test(target)) {
68
+ return { resolvedPath: null, error: 'PATH_NULL_BYTE' }
69
+ }
70
+
71
+ if (!allowAbsolute && isAbsolutePath(target)) {
72
+ return { resolvedPath: null, error: 'PATH_ABSOLUTE_NOT_ALLOWED' }
73
+ }
74
+
75
+ const resolvedBase = path.resolve(base)
76
+ const resolvedTarget = allowAbsolute && isAbsolutePath(target)
77
+ ? path.normalize(target)
78
+ : path.resolve(resolvedBase, target)
79
+
80
+ if (resolvedTarget === resolvedBase) {
81
+ return allowRoot
82
+ ? { resolvedPath: resolvedTarget }
83
+ : { resolvedPath: null, error: 'PATH_ROOT_NOT_ALLOWED' }
84
+ }
85
+
86
+ if (!resolvedTarget.startsWith(`${resolvedBase}${path.sep}`)) {
87
+ return { resolvedPath: null, error: 'PATH_TRAVERSAL' }
88
+ }
89
+
90
+ return { resolvedPath: resolvedTarget }
91
+ }
92
+
93
+ export function assertSafePath(
94
+ baseDir: string,
95
+ targetPath: string,
96
+ options: SafePathOptions = {}
97
+ ): string {
98
+ const result = resolveSafePath(baseDir, targetPath, options)
99
+ if (!result.resolvedPath) {
100
+ throw new Error(result.error ?? 'PATH_INVALID')
101
+ }
102
+ return result.resolvedPath
103
+ }
@@ -0,0 +1,115 @@
1
+ import type {
2
+ ChildProcess,
3
+ ExecFileOptions,
4
+ SpawnOptionsWithoutStdio,
5
+ } from 'node:child_process'
6
+ import { hasWindow } from '../../env'
7
+
8
+ type ExecFileFn = typeof import('node:child_process').execFile
9
+ type SpawnFn = typeof import('node:child_process').spawn
10
+
11
+ const NULL_BYTE_PATTERN = /\0/
12
+ const NEWLINE_PATTERN = /[\r\n]/
13
+
14
+ const childProcess = (() => {
15
+ if (hasWindow()) {
16
+ return null
17
+ }
18
+
19
+ const nodeRequire = typeof require === 'function' ? require : null
20
+ if (!nodeRequire) {
21
+ return null
22
+ }
23
+
24
+ try {
25
+ return nodeRequire('node:child_process') as typeof import('node:child_process')
26
+ } catch {
27
+ return null
28
+ }
29
+ })()
30
+
31
+ const execFileImpl = childProcess?.execFile
32
+ const spawnImpl = childProcess?.spawn
33
+
34
+ function assertShellValue(value: string, label: string): string {
35
+ const trimmed = value.trim()
36
+ if (!trimmed) {
37
+ throw new Error(`${label}_EMPTY`)
38
+ }
39
+ if (NULL_BYTE_PATTERN.test(trimmed)) {
40
+ throw new Error(`${label}_NULL_BYTE`)
41
+ }
42
+ if (NEWLINE_PATTERN.test(trimmed)) {
43
+ throw new Error(`${label}_NEWLINE`)
44
+ }
45
+ return trimmed
46
+ }
47
+
48
+ function assertShellArg(value: string): string {
49
+ if (NULL_BYTE_PATTERN.test(value)) {
50
+ throw new Error('ARG_NULL_BYTE')
51
+ }
52
+ return value
53
+ }
54
+
55
+ function requireChildProcess(): {
56
+ execFile: ExecFileFn
57
+ spawn: SpawnFn
58
+ } {
59
+ if (!execFileImpl || !spawnImpl) {
60
+ throw new Error('CHILD_PROCESS_UNAVAILABLE')
61
+ }
62
+
63
+ return {
64
+ execFile: execFileImpl,
65
+ spawn: spawnImpl,
66
+ }
67
+ }
68
+
69
+ export async function execFileSafe(
70
+ command: string,
71
+ args: string[] = [],
72
+ options: ExecFileOptions = {}
73
+ ): Promise<{ stdout: string; stderr: string }> {
74
+ const { execFile } = requireChildProcess()
75
+ const safeCommand = assertShellValue(command, 'COMMAND')
76
+ const safeArgs = args.map(assertShellArg)
77
+
78
+ return await new Promise((resolve, reject) => {
79
+ execFile(
80
+ safeCommand,
81
+ safeArgs,
82
+ {
83
+ ...options,
84
+ shell: false,
85
+ },
86
+ (error, stdout, stderr) => {
87
+ if (error) {
88
+ reject(error)
89
+ return
90
+ }
91
+ resolve({ stdout, stderr })
92
+ }
93
+ )
94
+ })
95
+ }
96
+
97
+ export function spawnSafe(
98
+ command: string,
99
+ args: string[] = [],
100
+ options: SpawnOptionsWithoutStdio = {}
101
+ ): ChildProcess {
102
+ const { spawn } = requireChildProcess()
103
+ const safeCommand = assertShellValue(command, 'COMMAND')
104
+ const safeArgs = args.map(assertShellArg)
105
+ return spawn(safeCommand, safeArgs, { ...options, shell: false })
106
+ }
107
+
108
+ export function spawnShellCommand(
109
+ command: string,
110
+ options: SpawnOptionsWithoutStdio = {}
111
+ ): ChildProcess {
112
+ const { spawn } = requireChildProcess()
113
+ const safeCommand = assertShellValue(command, 'COMMAND')
114
+ return spawn(safeCommand, [], { ...options, shell: true })
115
+ }
@@ -80,7 +80,10 @@ export async function runAdaptiveTaskQueue<T>(
80
80
  )
81
81
 
82
82
  for (let index = 0; index < total; index++) {
83
- await handler(items[index], index)
83
+ const item = items[index]
84
+ if (item === undefined)
85
+ continue
86
+ await handler(item, index)
84
87
 
85
88
  const processed = index + 1
86
89
  if (processed % batchSize === 0 && processed < total) {
@@ -578,7 +578,6 @@ class TuffSearchResultBuilder {
578
578
  }
579
579
 
580
580
  public setSortStats(stats: any[]): this {
581
- // @ts-ignore
582
581
  this.result.sort_stats = stats
583
582
  return this
584
583
  }
package/core-box/index.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  export * from './builder/index'
2
2
  export * from './preview/index'
3
+ export * from './recommendation'
3
4
  /**
4
5
  * Core Box Package
5
6
  * Search box core functionality package
6
7
  */
7
8
  export * from './tuff/index'
8
- export * from './recommendation'
@@ -51,10 +51,47 @@ export interface ScoredItem {
51
51
  sourceId: string
52
52
  itemId: string
53
53
  score: number
54
- source: 'frequent' | 'time-based' | 'recent' | 'trending' | 'context'
54
+ source: 'frequent' | 'time-based' | 'recent' | 'trending' | 'context' | 'plugin'
55
55
  reason?: string
56
56
  }
57
57
 
58
+ /**
59
+ * Candidate item returned by a plugin recommend provider.
60
+ * Unlike internal candidates, these do not require usageStats.
61
+ */
62
+ export interface PluginRecommendCandidate {
63
+ /** Provider ID (auto-filled from provider.id) */
64
+ providerId?: string
65
+ /** Unique item ID */
66
+ id: string
67
+ /** Display title */
68
+ title: string
69
+ /** Subtitle / description */
70
+ subtitle?: string
71
+ /** Icon configuration */
72
+ icon?: { type: string; value: string }
73
+ /** Priority 0-100, higher = more prominent */
74
+ priority?: number
75
+ /** Action key passed back to the plugin */
76
+ action: string
77
+ /** Additional data */
78
+ data?: Record<string, unknown>
79
+ }
80
+
81
+ /**
82
+ * Provider interface for plugins to supply custom recommendations.
83
+ */
84
+ export interface RecommendProvider {
85
+ /** Unique provider ID */
86
+ id: string
87
+ /** Display name */
88
+ name: string
89
+ /** Whether this provider can supply recommendations for the given context */
90
+ canProvide(context: ContextSignal): boolean
91
+ /** Return recommendation candidates */
92
+ getCandidates(context: ContextSignal): PluginRecommendCandidate[] | Promise<PluginRecommendCandidate[]>
93
+ }
94
+
58
95
  /**
59
96
  * Recommendation badge display configuration for UI rendering.
60
97
  */
@@ -380,6 +380,7 @@ export interface TuffCustomRender {
380
380
 
381
381
  /**
382
382
  * Icon definition
383
+ * @deprecated
383
384
  *
384
385
  * @description
385
386
  * Unified icon type supporting only ITuffIcon object format
@@ -1369,6 +1370,8 @@ export interface TuffContainerLayout {
1369
1370
  export interface TuffSectionMeta {
1370
1371
  /** 是否为智能推荐分组 */
1371
1372
  intelligence?: boolean
1373
+ /** 是否为置顶分组 */
1374
+ pinned?: boolean
1372
1375
  /** 扩展字段 */
1373
1376
  [key: string]: unknown
1374
1377
  }
@@ -1420,6 +1423,18 @@ export interface IProviderActivate {
1420
1423
  icon?: TuffIcon
1421
1424
  time?: number
1422
1425
  meta?: Record<string, any>
1426
+ /**
1427
+ * Whether the results area should be hidden when this provider is active.
1428
+ * - true: Hide results (webcontent mode - plugin UI view is attached)
1429
+ * - false/undefined: Show results (push mode - plugin pushes items to list)
1430
+ */
1431
+ hideResults?: boolean
1432
+ /**
1433
+ * Whether the input box should be shown when this provider is active.
1434
+ * - true: Show input (feature accepts input via acceptedInputTypes or allowInput)
1435
+ * - false/undefined: Hide input in webcontent mode, show in push mode
1436
+ */
1437
+ showInput?: boolean
1423
1438
  }
1424
1439
 
1425
1440
  /**
@@ -1482,6 +1497,23 @@ export interface ISearchProvider<C> {
1482
1497
  */
1483
1498
  readonly supportedInputTypes?: TuffInputType[]
1484
1499
 
1500
+ /**
1501
+ * Search priority layer
1502
+ * @description Determines when this provider's results are returned:
1503
+ * - 'fast': Results are returned immediately with the first batch (blocking)
1504
+ * - 'deferred': Results are appended asynchronously after fast layer completes
1505
+ * @default 'deferred'
1506
+ */
1507
+ readonly priority?: 'fast' | 'deferred'
1508
+
1509
+ /**
1510
+ * Expected search duration in milliseconds
1511
+ * @description Used for sorting providers within the same layer and timeout estimation.
1512
+ * Providers with lower expected duration run first within their layer.
1513
+ * @default 1000
1514
+ */
1515
+ readonly expectedDuration?: number
1516
+
1485
1517
  /**
1486
1518
  * Core search method (PULL mode).
1487
1519
  * The engine calls this method to get results from the provider.
@@ -1,6 +1,9 @@
1
1
  import path from 'node:path'
2
2
  import { net } from 'electron'
3
3
  import fse from 'fs-extra'
4
+ import { getLogger } from '../common/logger'
5
+
6
+ const downloadLog = getLogger('download-manager')
4
7
 
5
8
  interface DownloadItem {
6
9
  url: string
@@ -52,16 +55,16 @@ export class DownloadManager {
52
55
  const item = this.downloadQueue.shift()!
53
56
 
54
57
  try {
55
- console.log(`[DownloadManager] Starting to download ${item.filename} from ${item.url}`)
58
+ downloadLog.info(`Starting to download ${item.filename} from ${item.url}`)
56
59
  const filePath = await this.downloadFile(item.url, item.filename)
57
- console.log(`[DownloadManager] Download ${item.filename} completed`)
60
+ downloadLog.info(`Download ${item.filename} completed`)
58
61
 
59
62
  if (item.apply) {
60
63
  item.apply(filePath)
61
64
  }
62
65
  }
63
66
  catch (error) {
64
- console.error(`[DownloadManager] Download ${item.filename} failed:`, error)
67
+ downloadLog.error(`Download ${item.filename} failed`, { error })
65
68
  }
66
69
 
67
70
  this.processQueue()
@@ -78,10 +81,10 @@ export class DownloadManager {
78
81
  const request = net.request(url)
79
82
  const filePath = this.basePath ? path.join(this.basePath, filename) : filename
80
83
 
81
- console.log(`[DownloadManager] File download request sent for ${filename}.`)
84
+ downloadLog.info(`File download request sent for ${filename}.`)
82
85
 
83
86
  request.addListener('error', (error) => {
84
- console.error(`[DownloadManager] Download request error for ${filename}:`, error)
87
+ downloadLog.error(`Download request error for ${filename}`, { error })
85
88
  reject(error)
86
89
  })
87
90
 
@@ -89,12 +92,12 @@ export class DownloadManager {
89
92
  fse.createFileSync(filePath)
90
93
 
91
94
  response.addListener('data', (chunk: any) => {
92
- console.log(`[DownloadManager] Downloading ${filename}...`)
95
+ downloadLog.debug(`Downloading ${filename}...`)
93
96
  fse.appendFile(filePath, chunk, 'utf8')
94
97
  })
95
98
 
96
99
  response.addListener('end', () => {
97
- console.log(`[DownloadManager] Download ${filename} finished.`)
100
+ downloadLog.info(`Download ${filename} finished.`)
98
101
  resolve(filePath)
99
102
  })
100
103
  })