@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.
- package/.eslintcache +1 -0
- package/__tests__/cloud-sync-sdk.test.ts +442 -0
- package/__tests__/icons/icons.test.ts +84 -0
- package/__tests__/plugin-sdk-lifecycle.test.ts +130 -0
- package/__tests__/power-sdk.test.ts +143 -0
- package/__tests__/preset-export-types.test.ts +108 -0
- package/__tests__/search/fuzzy-match.test.ts +137 -0
- package/__tests__/transport/port-policy.test.ts +44 -0
- package/__tests__/transport-domain-sdks.test.ts +152 -0
- package/__tests__/types/update.test.ts +67 -0
- package/account/account-sdk.ts +915 -0
- package/account/index.ts +2 -0
- package/account/types.ts +321 -0
- package/analytics/client.ts +136 -0
- package/analytics/index.ts +2 -0
- package/analytics/types.ts +156 -0
- package/animation/auto-resize.ts +322 -0
- package/animation/window-node.ts +26 -19
- package/auth/clerk-types.ts +12 -30
- package/auth/index.ts +0 -2
- package/auth/useAuthState.ts +6 -14
- package/base/index.ts +2 -0
- package/base/log-level.ts +105 -0
- package/channel/index.ts +170 -69
- package/cloud-sync/cloud-sync-sdk.ts +450 -0
- package/cloud-sync/index.ts +1 -0
- package/common/file-scan-utils.ts +17 -9
- package/common/index.ts +4 -0
- package/common/logger/index.ts +46 -0
- package/common/logger/logger-manager.ts +303 -0
- package/common/logger/module-logger.ts +270 -0
- package/common/logger/transport-logger.ts +234 -0
- package/common/logger/types.ts +93 -0
- package/common/search/gather.ts +48 -6
- package/common/search/index.ts +8 -0
- package/common/storage/constants.ts +13 -0
- package/common/storage/entity/app-settings.ts +245 -0
- package/common/storage/entity/index.ts +3 -0
- package/common/storage/entity/layout-atom-types.ts +147 -0
- package/common/storage/entity/openers.ts +1 -0
- package/common/storage/entity/preset-cloud-api.ts +132 -0
- package/common/storage/entity/preset-export-types.ts +256 -0
- package/common/storage/entity/shortcut-settings.ts +1 -0
- package/common/storage/shortcut-storage.ts +11 -0
- package/common/utils/clone-diagnostics.ts +105 -0
- package/common/utils/file.ts +16 -8
- package/common/utils/index.ts +6 -2
- package/common/utils/payload-preview.ts +173 -0
- package/common/utils/polling.ts +167 -13
- package/common/utils/safe-path.ts +103 -0
- package/common/utils/safe-shell.ts +115 -0
- package/common/utils/task-queue.ts +4 -1
- package/core-box/builder/tuff-builder.ts +0 -1
- package/core-box/index.ts +1 -1
- package/core-box/recommendation.ts +38 -1
- package/core-box/tuff/tuff-dsl.ts +32 -0
- package/electron/download-manager.ts +10 -7
- package/electron/env-tool.ts +42 -40
- package/electron/index.ts +0 -1
- package/env/index.ts +156 -0
- package/eslint.config.js +55 -0
- package/i18n/index.ts +62 -0
- package/i18n/locales/en.json +226 -0
- package/i18n/locales/zh.json +226 -0
- package/i18n/message-keys.ts +236 -0
- package/i18n/resolver.ts +181 -0
- package/icons/index.ts +257 -0
- package/icons/svg.ts +69 -0
- package/index.ts +9 -1
- package/intelligence/client.ts +72 -42
- package/market/constants.ts +9 -5
- package/market/index.ts +1 -1
- package/market/types.ts +19 -4
- package/package.json +15 -5
- package/permission/index.ts +143 -46
- package/permission/legacy.ts +26 -0
- package/permission/registry.ts +304 -0
- package/permission/types.ts +164 -0
- package/plugin/channel.ts +68 -39
- package/plugin/index.ts +80 -7
- package/plugin/install.ts +3 -0
- package/plugin/log/types.ts +22 -5
- package/plugin/node/logger-manager.ts +11 -3
- package/plugin/node/logger.ts +24 -17
- package/plugin/preload.ts +25 -2
- package/plugin/providers/index.ts +4 -4
- package/plugin/providers/market-client.ts +6 -3
- package/plugin/providers/npm-provider.ts +22 -7
- package/plugin/providers/tpex-provider.ts +22 -8
- package/plugin/sdk/box-items.ts +14 -0
- package/plugin/sdk/box-sdk.ts +64 -0
- package/plugin/sdk/channel.ts +119 -4
- package/plugin/sdk/clipboard.ts +26 -12
- package/plugin/sdk/cloud-sync.ts +113 -0
- package/plugin/sdk/common.ts +19 -11
- package/plugin/sdk/core-box.ts +6 -15
- package/plugin/sdk/division-box.ts +160 -65
- package/plugin/sdk/examples/storage-onDidChange-example.js +5 -2
- package/plugin/sdk/feature-sdk.ts +111 -76
- package/plugin/sdk/flow.ts +146 -45
- package/plugin/sdk/hooks/bridge.ts +13 -6
- package/plugin/sdk/hooks/life-cycle.ts +35 -16
- package/plugin/sdk/index.ts +14 -3
- package/plugin/sdk/intelligence.ts +87 -0
- package/plugin/sdk/meta/README.md +179 -0
- package/plugin/sdk/meta-sdk.ts +244 -0
- package/plugin/sdk/notification.ts +9 -0
- package/plugin/sdk/plugin-info.ts +64 -0
- package/plugin/sdk/power.ts +155 -0
- package/plugin/sdk/recommend.ts +21 -0
- package/plugin/sdk/service/index.ts +12 -8
- package/plugin/sdk/sqlite.ts +141 -0
- package/plugin/sdk/storage.ts +2 -6
- package/plugin/sdk/system.ts +2 -9
- package/plugin/sdk/temp-files.ts +41 -0
- package/plugin/sdk/touch-sdk.ts +18 -0
- package/plugin/sdk/types.ts +44 -4
- package/plugin/sdk/window/index.ts +12 -9
- package/plugin/sdk-version.ts +231 -0
- package/preload/renderer.ts +3 -2
- package/renderer/hooks/arg-mapper.ts +16 -2
- package/renderer/hooks/index.ts +13 -0
- package/renderer/hooks/initialize.ts +2 -1
- package/renderer/hooks/use-agent-market-sdk.ts +7 -0
- package/renderer/hooks/use-agent-market.ts +106 -0
- package/renderer/hooks/use-agents-sdk.ts +7 -0
- package/renderer/hooks/use-app-sdk.ts +7 -0
- package/renderer/hooks/use-channel.ts +33 -4
- package/renderer/hooks/use-download-sdk.ts +21 -0
- package/renderer/hooks/use-intelligence-sdk.ts +7 -0
- package/renderer/hooks/use-intelligence-stats.ts +290 -0
- package/renderer/hooks/use-intelligence.ts +55 -214
- package/renderer/hooks/use-market-sdk.ts +16 -0
- package/renderer/hooks/use-notification-sdk.ts +7 -0
- package/renderer/hooks/use-permission-sdk.ts +7 -0
- package/renderer/hooks/use-permission.ts +325 -0
- package/renderer/hooks/use-platform-sdk.ts +7 -0
- package/renderer/hooks/use-plugin-sdk.ts +16 -0
- package/renderer/hooks/use-settings-sdk.ts +7 -0
- package/renderer/hooks/use-update-sdk.ts +21 -0
- package/renderer/index.ts +1 -0
- package/renderer/ref.ts +19 -10
- package/renderer/shared/components/SharedPluginDetailContent.vue +84 -0
- package/renderer/shared/components/SharedPluginDetailHeader.vue +116 -0
- package/renderer/shared/components/SharedPluginDetailMetaList.vue +39 -0
- package/renderer/shared/components/SharedPluginDetailReadme.vue +45 -0
- package/renderer/shared/components/SharedPluginDetailVersions.vue +98 -0
- package/renderer/shared/components/index.ts +5 -0
- package/renderer/shared/components/shims-vue.d.ts +5 -0
- package/renderer/shared/index.ts +2 -0
- package/renderer/shared/plugin-detail.ts +62 -0
- package/renderer/storage/app-settings.ts +3 -1
- package/renderer/storage/base-storage.ts +508 -82
- package/renderer/storage/intelligence-storage.ts +31 -40
- package/renderer/storage/openers.ts +3 -1
- package/renderer/storage/storage-subscription.ts +126 -42
- package/renderer/touch-sdk/env.ts +10 -10
- package/renderer/touch-sdk/index.ts +114 -18
- package/renderer/touch-sdk/terminal.ts +24 -13
- package/search/feature-matcher.ts +279 -0
- package/search/fuzzy-match.ts +64 -34
- package/search/index.ts +10 -0
- package/search/levenshtein-utils.ts +17 -11
- package/transport/errors.ts +310 -0
- package/transport/event/builder.ts +378 -0
- package/transport/event/index.ts +7 -0
- package/transport/event/types.ts +292 -0
- package/transport/events/index.ts +2690 -0
- package/transport/events/meta-overlay.ts +79 -0
- package/transport/events/types/agents.ts +177 -0
- package/transport/events/types/app-index.ts +20 -0
- package/transport/events/types/app.ts +475 -0
- package/transport/events/types/box-item.ts +222 -0
- package/transport/events/types/clipboard.ts +80 -0
- package/transport/events/types/core-box.ts +534 -0
- package/transport/events/types/device-idle.ts +7 -0
- package/transport/events/types/division-box.ts +99 -0
- package/transport/events/types/download.ts +115 -0
- package/transport/events/types/file-index.ts +84 -0
- package/transport/events/types/flow.ts +149 -0
- package/transport/events/types/index.ts +70 -0
- package/transport/events/types/market.ts +39 -0
- package/transport/events/types/meta-overlay.ts +184 -0
- package/transport/events/types/notification.ts +140 -0
- package/transport/events/types/permission.ts +90 -0
- package/transport/events/types/platform.ts +8 -0
- package/transport/events/types/plugin.ts +631 -0
- package/transport/events/types/sentry.ts +20 -0
- package/transport/events/types/storage.ts +208 -0
- package/transport/events/types/transport.ts +60 -0
- package/transport/events/types/tray.ts +16 -0
- package/transport/events/types/update.ts +78 -0
- package/transport/index.ts +141 -0
- package/transport/main.ts +2 -0
- package/transport/prelude.ts +208 -0
- package/transport/sdk/constants.ts +29 -0
- package/transport/sdk/domains/agents-market.ts +47 -0
- package/transport/sdk/domains/agents.ts +62 -0
- package/transport/sdk/domains/app.ts +48 -0
- package/transport/sdk/domains/disposable.ts +35 -0
- package/transport/sdk/domains/download.ts +139 -0
- package/transport/sdk/domains/index.ts +13 -0
- package/transport/sdk/domains/intelligence.ts +616 -0
- package/transport/sdk/domains/market.ts +35 -0
- package/transport/sdk/domains/notification.ts +62 -0
- package/transport/sdk/domains/permission.ts +85 -0
- package/transport/sdk/domains/platform.ts +19 -0
- package/transport/sdk/domains/plugin.ts +144 -0
- package/transport/sdk/domains/settings.ts +102 -0
- package/transport/sdk/domains/update.ts +64 -0
- package/transport/sdk/index.ts +60 -0
- package/transport/sdk/main-transport.ts +710 -0
- package/transport/sdk/main.ts +9 -0
- package/transport/sdk/plugin-transport.ts +654 -0
- package/transport/sdk/port-policy.ts +38 -0
- package/transport/sdk/renderer-transport.ts +1165 -0
- package/transport/types.ts +605 -0
- package/types/agent.ts +399 -0
- package/types/cloud-sync.ts +157 -0
- package/types/division-box.ts +31 -31
- package/types/download.ts +1 -0
- package/types/flow.ts +63 -12
- package/types/icon.ts +2 -1
- package/types/index.ts +5 -0
- package/types/intelligence.ts +166 -173
- package/types/modules/base.ts +2 -0
- package/types/path-browserify.d.ts +5 -0
- package/types/platform.ts +12 -0
- package/types/startup-info.ts +32 -0
- package/types/touch-app-core.ts +8 -8
- package/types/update.ts +94 -1
- package/vitest.config.ts +25 -0
- package/auth/useClerkConfig.ts +0 -40
- package/auth/useClerkProvider.ts +0 -52
package/common/utils/polling.ts
CHANGED
|
@@ -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
|
|
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: {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
161
|
+
this.recordStartAttempt()
|
|
118
162
|
return
|
|
119
163
|
}
|
|
120
164
|
this.isRunning = true
|
|
121
|
-
|
|
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
|
|
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
|
-
|
|
227
|
+
if (this.shouldVerboseLog()) {
|
|
228
|
+
console.log(`[PollingService] Stopping polling service: ${reason}`)
|
|
229
|
+
}
|
|
182
230
|
}
|
|
183
231
|
else {
|
|
184
|
-
|
|
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
|
-
|
|
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) {
|
package/core-box/index.ts
CHANGED
|
@@ -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
|
-
|
|
58
|
+
downloadLog.info(`Starting to download ${item.filename} from ${item.url}`)
|
|
56
59
|
const filePath = await this.downloadFile(item.url, item.filename)
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
84
|
+
downloadLog.info(`File download request sent for ${filename}.`)
|
|
82
85
|
|
|
83
86
|
request.addListener('error', (error) => {
|
|
84
|
-
|
|
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
|
-
|
|
95
|
+
downloadLog.debug(`Downloading ${filename}...`)
|
|
93
96
|
fse.appendFile(filePath, chunk, 'utf8')
|
|
94
97
|
})
|
|
95
98
|
|
|
96
99
|
response.addListener('end', () => {
|
|
97
|
-
|
|
100
|
+
downloadLog.info(`Download ${filename} finished.`)
|
|
98
101
|
resolve(filePath)
|
|
99
102
|
})
|
|
100
103
|
})
|