@talex-touch/utils 1.0.39 → 1.0.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core-box/tuff/tuff-dsl.ts +133 -54
- package/intelligence/client.ts +8 -8
- package/market/constants.ts +14 -0
- package/market/types.ts +1 -1
- package/package.json +1 -1
- package/plugin/index.ts +7 -1
- package/plugin/providers/index.ts +4 -0
- package/plugin/providers/market-client.ts +215 -0
- package/plugin/providers/npm-provider.ts +213 -0
- package/plugin/providers/tpex-provider.ts +283 -0
- package/plugin/providers/tpex-types.ts +34 -0
- package/plugin/sdk/README.md +54 -6
- package/plugin/sdk/clipboard.ts +196 -23
- package/plugin/sdk/enum/bridge-event.ts +1 -0
- package/plugin/sdk/feature-sdk.ts +85 -6
- package/plugin/sdk/flow.ts +246 -0
- package/plugin/sdk/hooks/bridge.ts +113 -39
- package/plugin/sdk/index.ts +2 -0
- package/plugin/sdk/performance.ts +186 -0
- package/plugin/widget.ts +5 -0
- package/renderer/hooks/arg-mapper.ts +20 -6
- package/renderer/hooks/use-intelligence.ts +291 -34
- package/renderer/storage/base-storage.ts +98 -15
- package/renderer/storage/intelligence-storage.ts +9 -9
- package/renderer/storage/storage-subscription.ts +17 -9
- package/search/fuzzy-match.ts +254 -0
- package/types/division-box.ts +20 -0
- package/types/flow.ts +283 -0
- package/types/index.ts +1 -0
- package/types/intelligence.ts +1496 -78
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import type { IManifest } from '..'
|
|
2
|
+
import type {
|
|
3
|
+
PluginInstallRequest,
|
|
4
|
+
PluginInstallResult,
|
|
5
|
+
PluginProvider,
|
|
6
|
+
PluginProviderContext,
|
|
7
|
+
} from './types'
|
|
8
|
+
import { PluginProviderType } from './types'
|
|
9
|
+
|
|
10
|
+
const NPM_REGISTRY = 'https://registry.npmjs.org'
|
|
11
|
+
const TUFF_PLUGIN_PREFIX = 'tuff-plugin-'
|
|
12
|
+
const TUFF_PLUGIN_SCOPE = '@tuff/'
|
|
13
|
+
|
|
14
|
+
export interface NpmPackageInfo {
|
|
15
|
+
name: string
|
|
16
|
+
version: string
|
|
17
|
+
description?: string
|
|
18
|
+
author?: string | { name: string, email?: string }
|
|
19
|
+
main?: string
|
|
20
|
+
keywords?: string[]
|
|
21
|
+
dist: {
|
|
22
|
+
tarball: string
|
|
23
|
+
shasum: string
|
|
24
|
+
integrity?: string
|
|
25
|
+
}
|
|
26
|
+
tuff?: {
|
|
27
|
+
icon?: string
|
|
28
|
+
activationKeywords?: string[]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface NpmPackageVersions {
|
|
33
|
+
name: string
|
|
34
|
+
'dist-tags': {
|
|
35
|
+
latest: string
|
|
36
|
+
[tag: string]: string
|
|
37
|
+
}
|
|
38
|
+
versions: Record<string, NpmPackageInfo>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parse NPM source string to extract package name and optional version
|
|
43
|
+
* Formats:
|
|
44
|
+
* - "npm:package-name"
|
|
45
|
+
* - "npm:package-name@version"
|
|
46
|
+
* - "npm:@scope/package-name"
|
|
47
|
+
* - "npm:@scope/package-name@version"
|
|
48
|
+
* - "tuff-plugin-xxx" (when hintType is NPM)
|
|
49
|
+
* - "@tuff/xxx" (when hintType is NPM)
|
|
50
|
+
*/
|
|
51
|
+
function parseNpmSource(source: string): { packageName: string, version?: string } | null {
|
|
52
|
+
const npmMatch = source.match(/^npm:(@?[a-z0-9][\w\-.]*(?:\/[a-z0-9][\w\-.]*)?)(?:@(.+))?$/i)
|
|
53
|
+
if (npmMatch) {
|
|
54
|
+
return { packageName: npmMatch[1], version: npmMatch[2] }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const scopedMatch = source.match(/^(@tuff\/[a-z0-9][\w\-.]*)(?:@(.+))?$/i)
|
|
58
|
+
if (scopedMatch) {
|
|
59
|
+
return { packageName: scopedMatch[1], version: scopedMatch[2] }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const prefixMatch = source.match(/^(tuff-plugin-[a-z0-9][\w\-.]*)(?:@(.+))?$/i)
|
|
63
|
+
if (prefixMatch) {
|
|
64
|
+
return { packageName: prefixMatch[1], version: prefixMatch[2] }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check if a package name looks like a Tuff plugin
|
|
72
|
+
*/
|
|
73
|
+
function isTuffPluginPackage(name: string): boolean {
|
|
74
|
+
return name.startsWith(TUFF_PLUGIN_PREFIX) || name.startsWith(TUFF_PLUGIN_SCOPE)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export class NpmProvider implements PluginProvider {
|
|
78
|
+
readonly type = PluginProviderType.NPM
|
|
79
|
+
private registry: string
|
|
80
|
+
|
|
81
|
+
constructor(registry: string = NPM_REGISTRY) {
|
|
82
|
+
this.registry = registry.replace(/\/$/, '')
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
canHandle(request: PluginInstallRequest): boolean {
|
|
86
|
+
if (request.hintType === PluginProviderType.NPM) {
|
|
87
|
+
return parseNpmSource(request.source) !== null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (request.source.startsWith('npm:')) {
|
|
91
|
+
return true
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const parsed = parseNpmSource(request.source)
|
|
95
|
+
return parsed !== null && isTuffPluginPackage(parsed.packageName)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async install(
|
|
99
|
+
request: PluginInstallRequest,
|
|
100
|
+
context?: PluginProviderContext,
|
|
101
|
+
): Promise<PluginInstallResult> {
|
|
102
|
+
const parsed = parseNpmSource(request.source)
|
|
103
|
+
if (!parsed) {
|
|
104
|
+
throw new Error(`Invalid NPM source format: ${request.source}`)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const { packageName, version } = parsed
|
|
108
|
+
const packageInfo = await this.getPackageInfo(packageName, version)
|
|
109
|
+
|
|
110
|
+
if (!packageInfo) {
|
|
111
|
+
throw new Error(`Package not found: ${packageName}`)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const tarballUrl = packageInfo.dist.tarball
|
|
115
|
+
const downloadRes = await fetch(tarballUrl)
|
|
116
|
+
if (!downloadRes.ok) {
|
|
117
|
+
throw new Error(`Failed to download package: ${downloadRes.statusText}`)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const arrayBuffer = await downloadRes.arrayBuffer()
|
|
121
|
+
const tempDir = context?.tempDir ?? '/tmp'
|
|
122
|
+
const safePackageName = packageName.replace(/[@/]/g, '-')
|
|
123
|
+
const fileName = `${safePackageName}-${packageInfo.version}.tgz`
|
|
124
|
+
const filePath = `${tempDir}/${fileName}`
|
|
125
|
+
|
|
126
|
+
if (typeof globalThis.process !== 'undefined') {
|
|
127
|
+
const fs = await import('node:fs/promises')
|
|
128
|
+
await fs.writeFile(filePath, Buffer.from(arrayBuffer))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const authorName = typeof packageInfo.author === 'string'
|
|
132
|
+
? packageInfo.author
|
|
133
|
+
: packageInfo.author?.name ?? 'Unknown'
|
|
134
|
+
|
|
135
|
+
const manifest: IManifest = {
|
|
136
|
+
id: packageInfo.name,
|
|
137
|
+
name: packageInfo.name.replace(TUFF_PLUGIN_PREFIX, '').replace(TUFF_PLUGIN_SCOPE, ''),
|
|
138
|
+
version: packageInfo.version,
|
|
139
|
+
description: packageInfo.description ?? '',
|
|
140
|
+
author: authorName,
|
|
141
|
+
main: packageInfo.main ?? 'index.js',
|
|
142
|
+
icon: packageInfo.tuff?.icon,
|
|
143
|
+
activationKeywords: packageInfo.tuff?.activationKeywords ?? packageInfo.keywords,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
provider: PluginProviderType.NPM,
|
|
148
|
+
filePath,
|
|
149
|
+
official: packageName.startsWith(TUFF_PLUGIN_SCOPE),
|
|
150
|
+
manifest,
|
|
151
|
+
metadata: {
|
|
152
|
+
packageName: packageInfo.name,
|
|
153
|
+
version: packageInfo.version,
|
|
154
|
+
tarball: tarballUrl,
|
|
155
|
+
shasum: packageInfo.dist.shasum,
|
|
156
|
+
integrity: packageInfo.dist.integrity,
|
|
157
|
+
},
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Get package info from npm registry
|
|
163
|
+
*/
|
|
164
|
+
async getPackageInfo(packageName: string, version?: string): Promise<NpmPackageInfo | null> {
|
|
165
|
+
const encodedName = encodeURIComponent(packageName).replace('%40', '@')
|
|
166
|
+
const res = await fetch(`${this.registry}/${encodedName}`)
|
|
167
|
+
|
|
168
|
+
if (!res.ok) {
|
|
169
|
+
if (res.status === 404) return null
|
|
170
|
+
throw new Error(`Failed to fetch package info: ${res.statusText}`)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const data: NpmPackageVersions = await res.json()
|
|
174
|
+
const targetVersion = version ?? data['dist-tags'].latest
|
|
175
|
+
|
|
176
|
+
return data.versions[targetVersion] ?? null
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Search for Tuff plugins on npm
|
|
181
|
+
*/
|
|
182
|
+
async searchPlugins(keyword?: string): Promise<NpmPackageInfo[]> {
|
|
183
|
+
const searchTerms = [
|
|
184
|
+
`keywords:tuff-plugin`,
|
|
185
|
+
keyword ? `${keyword}` : '',
|
|
186
|
+
].filter(Boolean).join('+')
|
|
187
|
+
|
|
188
|
+
const res = await fetch(
|
|
189
|
+
`${this.registry}/-/v1/search?text=${encodeURIComponent(searchTerms)}&size=100`,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
if (!res.ok) {
|
|
193
|
+
throw new Error(`Failed to search packages: ${res.statusText}`)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const data = await res.json() as {
|
|
197
|
+
objects: Array<{ package: NpmPackageInfo }>
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return data.objects
|
|
201
|
+
.map(obj => obj.package)
|
|
202
|
+
.filter(pkg => isTuffPluginPackage(pkg.name))
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* List all available Tuff plugins from npm
|
|
207
|
+
*/
|
|
208
|
+
async listPlugins(): Promise<NpmPackageInfo[]> {
|
|
209
|
+
return this.searchPlugins()
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export const npmProvider = new NpmProvider()
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import type { IManifest } from '..'
|
|
2
|
+
import type {
|
|
3
|
+
PluginInstallRequest,
|
|
4
|
+
PluginInstallResult,
|
|
5
|
+
PluginProvider,
|
|
6
|
+
PluginProviderContext,
|
|
7
|
+
} from './types'
|
|
8
|
+
import { PluginProviderType } from './types'
|
|
9
|
+
|
|
10
|
+
const DEFAULT_TPEX_API = 'https://tuff.tagzxia.com'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Check if source is a .tpex file path or URL
|
|
14
|
+
*/
|
|
15
|
+
function isTpexFile(source: string): boolean {
|
|
16
|
+
return source.trim().toLowerCase().endsWith('.tpex')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check if source is a remote URL
|
|
21
|
+
*/
|
|
22
|
+
function isRemoteUrl(source: string): boolean {
|
|
23
|
+
return /^https?:\/\//i.test(source)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface TpexPluginInfo {
|
|
27
|
+
id: string
|
|
28
|
+
slug: string
|
|
29
|
+
name: string
|
|
30
|
+
summary: string
|
|
31
|
+
category: string
|
|
32
|
+
installs: number
|
|
33
|
+
homepage?: string | null
|
|
34
|
+
isOfficial: boolean
|
|
35
|
+
badges: string[]
|
|
36
|
+
author?: { name: string, avatarColor?: string } | null
|
|
37
|
+
iconUrl?: string | null
|
|
38
|
+
latestVersion?: {
|
|
39
|
+
id: string
|
|
40
|
+
version: string
|
|
41
|
+
channel: string
|
|
42
|
+
packageUrl: string
|
|
43
|
+
packageSize: number
|
|
44
|
+
manifest?: Record<string, unknown> | null
|
|
45
|
+
changelog?: string | null
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface TpexListResponse {
|
|
50
|
+
plugins: TpexPluginInfo[]
|
|
51
|
+
total: number
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface TpexDetailResponse {
|
|
55
|
+
plugin: TpexPluginInfo & {
|
|
56
|
+
versions?: Array<{
|
|
57
|
+
id: string
|
|
58
|
+
version: string
|
|
59
|
+
channel: string
|
|
60
|
+
packageUrl: string
|
|
61
|
+
packageSize: number
|
|
62
|
+
manifest?: Record<string, unknown> | null
|
|
63
|
+
changelog?: string | null
|
|
64
|
+
}>
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Parse TPEX source string to extract slug and optional version
|
|
70
|
+
* Formats: "tpex:slug", "tpex:slug@version", "slug" (when hintType is TPEX)
|
|
71
|
+
*/
|
|
72
|
+
function parseTpexSource(source: string): { slug: string, version?: string } | null {
|
|
73
|
+
const tpexMatch = source.match(/^tpex:([a-z0-9][a-z0-9\-_.]{1,62}[a-z0-9])(?:@(.+))?$/i)
|
|
74
|
+
if (tpexMatch) {
|
|
75
|
+
return { slug: tpexMatch[1], version: tpexMatch[2] }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const slugMatch = source.match(/^([a-z0-9][a-z0-9\-_.]{1,62}[a-z0-9])(?:@(.+))?$/i)
|
|
79
|
+
if (slugMatch) {
|
|
80
|
+
return { slug: slugMatch[1], version: slugMatch[2] }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export class TpexProvider implements PluginProvider {
|
|
87
|
+
readonly type = PluginProviderType.TPEX
|
|
88
|
+
private apiBase: string
|
|
89
|
+
|
|
90
|
+
constructor(apiBase: string = DEFAULT_TPEX_API) {
|
|
91
|
+
this.apiBase = apiBase.replace(/\/$/, '')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
canHandle(request: PluginInstallRequest): boolean {
|
|
95
|
+
// Handle .tpex file paths (local or remote URL)
|
|
96
|
+
if (isTpexFile(request.source)) {
|
|
97
|
+
return true
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (request.hintType === PluginProviderType.TPEX) {
|
|
101
|
+
return parseTpexSource(request.source) !== null
|
|
102
|
+
}
|
|
103
|
+
return request.source.startsWith('tpex:')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async install(
|
|
107
|
+
request: PluginInstallRequest,
|
|
108
|
+
context?: PluginProviderContext,
|
|
109
|
+
): Promise<PluginInstallResult> {
|
|
110
|
+
// Handle .tpex file directly (local path or remote URL)
|
|
111
|
+
if (isTpexFile(request.source)) {
|
|
112
|
+
return this.installFromFile(request, context)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Handle tpex:slug format - fetch from API
|
|
116
|
+
return this.installFromRegistry(request, context)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Install from a .tpex file (local path or remote URL)
|
|
121
|
+
*/
|
|
122
|
+
private async installFromFile(
|
|
123
|
+
request: PluginInstallRequest,
|
|
124
|
+
context?: PluginProviderContext,
|
|
125
|
+
): Promise<PluginInstallResult> {
|
|
126
|
+
let filePath = request.source
|
|
127
|
+
let arrayBuffer: ArrayBuffer | undefined
|
|
128
|
+
|
|
129
|
+
if (isRemoteUrl(request.source)) {
|
|
130
|
+
// Download remote .tpex file
|
|
131
|
+
const downloadRes = await fetch(request.source)
|
|
132
|
+
if (!downloadRes.ok) {
|
|
133
|
+
throw new Error(`Failed to download TPEX file: ${downloadRes.statusText}`)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
arrayBuffer = await downloadRes.arrayBuffer()
|
|
137
|
+
const tempDir = context?.tempDir ?? '/tmp'
|
|
138
|
+
const fileName = `tpex-${Date.now()}.tpex`
|
|
139
|
+
filePath = `${tempDir}/${fileName}`
|
|
140
|
+
|
|
141
|
+
if (typeof globalThis.process !== 'undefined') {
|
|
142
|
+
const fs = await import('node:fs/promises')
|
|
143
|
+
await fs.writeFile(filePath, Buffer.from(arrayBuffer))
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// For local files, just return the path - manifest extraction happens in core-app
|
|
148
|
+
return {
|
|
149
|
+
provider: PluginProviderType.TPEX,
|
|
150
|
+
filePath,
|
|
151
|
+
official: false,
|
|
152
|
+
metadata: {
|
|
153
|
+
sourceType: 'file',
|
|
154
|
+
originalSource: request.source,
|
|
155
|
+
},
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Install from TPEX registry (tpex:slug format)
|
|
161
|
+
*/
|
|
162
|
+
private async installFromRegistry(
|
|
163
|
+
request: PluginInstallRequest,
|
|
164
|
+
context?: PluginProviderContext,
|
|
165
|
+
): Promise<PluginInstallResult> {
|
|
166
|
+
const parsed = parseTpexSource(request.source)
|
|
167
|
+
if (!parsed) {
|
|
168
|
+
throw new Error(`Invalid TPEX source format: ${request.source}`)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const { slug, version } = parsed
|
|
172
|
+
|
|
173
|
+
const detailRes = await fetch(`${this.apiBase}/api/market/plugins/${slug}`)
|
|
174
|
+
if (!detailRes.ok) {
|
|
175
|
+
if (detailRes.status === 404) {
|
|
176
|
+
throw new Error(`Plugin not found: ${slug}`)
|
|
177
|
+
}
|
|
178
|
+
throw new Error(`Failed to fetch plugin details: ${detailRes.statusText}`)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const detail: TpexDetailResponse = await detailRes.json()
|
|
182
|
+
const plugin = detail.plugin
|
|
183
|
+
|
|
184
|
+
let targetVersion = plugin.latestVersion
|
|
185
|
+
if (version && plugin.versions) {
|
|
186
|
+
targetVersion = plugin.versions.find(v => v.version === version) ?? targetVersion
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!targetVersion?.packageUrl) {
|
|
190
|
+
throw new Error(`No downloadable version found for plugin: ${slug}`)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const downloadUrl = targetVersion.packageUrl.startsWith('http')
|
|
194
|
+
? targetVersion.packageUrl
|
|
195
|
+
: `${this.apiBase}${targetVersion.packageUrl}`
|
|
196
|
+
|
|
197
|
+
const downloadRes = await fetch(downloadUrl)
|
|
198
|
+
if (!downloadRes.ok) {
|
|
199
|
+
throw new Error(`Failed to download plugin package: ${downloadRes.statusText}`)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const arrayBuffer = await downloadRes.arrayBuffer()
|
|
203
|
+
const tempDir = context?.tempDir ?? '/tmp'
|
|
204
|
+
const fileName = `${slug}-${targetVersion.version}.tpex`
|
|
205
|
+
const filePath = `${tempDir}/${fileName}`
|
|
206
|
+
|
|
207
|
+
if (typeof globalThis.process !== 'undefined') {
|
|
208
|
+
const fs = await import('node:fs/promises')
|
|
209
|
+
await fs.writeFile(filePath, Buffer.from(arrayBuffer))
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const manifest: IManifest | undefined = targetVersion.manifest
|
|
213
|
+
? {
|
|
214
|
+
id: plugin.slug,
|
|
215
|
+
name: plugin.name,
|
|
216
|
+
version: targetVersion.version,
|
|
217
|
+
description: plugin.summary,
|
|
218
|
+
author: plugin.author?.name ?? 'Unknown',
|
|
219
|
+
main: (targetVersion.manifest as Record<string, unknown>).main as string ?? 'index.js',
|
|
220
|
+
icon: plugin.iconUrl ?? undefined,
|
|
221
|
+
...targetVersion.manifest,
|
|
222
|
+
}
|
|
223
|
+
: undefined
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
provider: PluginProviderType.TPEX,
|
|
227
|
+
filePath,
|
|
228
|
+
official: plugin.isOfficial,
|
|
229
|
+
manifest,
|
|
230
|
+
metadata: {
|
|
231
|
+
sourceType: 'registry',
|
|
232
|
+
slug: plugin.slug,
|
|
233
|
+
version: targetVersion.version,
|
|
234
|
+
channel: targetVersion.channel,
|
|
235
|
+
packageSize: targetVersion.packageSize,
|
|
236
|
+
installs: plugin.installs,
|
|
237
|
+
},
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* List all available plugins from TPEX registry
|
|
243
|
+
*/
|
|
244
|
+
async listPlugins(): Promise<TpexPluginInfo[]> {
|
|
245
|
+
const res = await fetch(`${this.apiBase}/api/market/plugins`)
|
|
246
|
+
if (!res.ok) {
|
|
247
|
+
throw new Error(`Failed to fetch plugin list: ${res.statusText}`)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const data: TpexListResponse = await res.json()
|
|
251
|
+
return data.plugins
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Get plugin details by slug
|
|
256
|
+
*/
|
|
257
|
+
async getPlugin(slug: string): Promise<TpexDetailResponse['plugin'] | null> {
|
|
258
|
+
const res = await fetch(`${this.apiBase}/api/market/plugins/${slug}`)
|
|
259
|
+
if (!res.ok) {
|
|
260
|
+
if (res.status === 404) return null
|
|
261
|
+
throw new Error(`Failed to fetch plugin: ${res.statusText}`)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const data: TpexDetailResponse = await res.json()
|
|
265
|
+
return data.plugin
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Search plugins by keyword
|
|
270
|
+
*/
|
|
271
|
+
async searchPlugins(keyword: string): Promise<TpexPluginInfo[]> {
|
|
272
|
+
const plugins = await this.listPlugins()
|
|
273
|
+
const lowerKeyword = keyword.toLowerCase()
|
|
274
|
+
|
|
275
|
+
return plugins.filter(plugin =>
|
|
276
|
+
plugin.name.toLowerCase().includes(lowerKeyword)
|
|
277
|
+
|| plugin.slug.toLowerCase().includes(lowerKeyword)
|
|
278
|
+
|| plugin.summary.toLowerCase().includes(lowerKeyword),
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export const tpexProvider = new TpexProvider()
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metadata extracted from a .tpex package file
|
|
3
|
+
*/
|
|
4
|
+
export interface TpexMetadata {
|
|
5
|
+
readmeMarkdown?: string | null
|
|
6
|
+
manifest?: Record<string, unknown> | null
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Result of package preview operation
|
|
11
|
+
*/
|
|
12
|
+
export interface TpexPackagePreviewResult {
|
|
13
|
+
manifest: Record<string, unknown> | null
|
|
14
|
+
readmeMarkdown: string | null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Extracted manifest fields from tpex package
|
|
19
|
+
*/
|
|
20
|
+
export interface TpexExtractedManifest {
|
|
21
|
+
id?: string
|
|
22
|
+
name?: string
|
|
23
|
+
description?: string
|
|
24
|
+
version?: string
|
|
25
|
+
homepage?: string
|
|
26
|
+
changelog?: string
|
|
27
|
+
channel?: string
|
|
28
|
+
category?: string
|
|
29
|
+
icon?: {
|
|
30
|
+
type?: string
|
|
31
|
+
value?: string
|
|
32
|
+
}
|
|
33
|
+
[key: string]: unknown
|
|
34
|
+
}
|
package/plugin/sdk/README.md
CHANGED
|
@@ -66,6 +66,53 @@ const unsubscribe = plugin.feature.onInputChange((input) => {
|
|
|
66
66
|
|
|
67
67
|
// 取消监听
|
|
68
68
|
unsubscribe()
|
|
69
|
+
|
|
70
|
+
// 监听键盘事件(UI View 模式下)
|
|
71
|
+
const unsubscribeKey = plugin.feature.onKeyEvent((event) => {
|
|
72
|
+
if (event.key === 'Enter') {
|
|
73
|
+
// 处理回车键
|
|
74
|
+
submitSelection()
|
|
75
|
+
} else if (event.key === 'ArrowDown') {
|
|
76
|
+
// 向下导航
|
|
77
|
+
selectNext()
|
|
78
|
+
} else if (event.key === 'ArrowUp') {
|
|
79
|
+
// 向上导航
|
|
80
|
+
selectPrev()
|
|
81
|
+
} else if (event.metaKey && event.key === 'k') {
|
|
82
|
+
// 处理 Cmd+K
|
|
83
|
+
openSearch()
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 3. 键盘事件自动处理
|
|
89
|
+
|
|
90
|
+
当插件的 UI View 附加到 CoreBox 时,系统会自动处理以下行为:
|
|
91
|
+
|
|
92
|
+
#### ESC 键自动退出
|
|
93
|
+
- 在 UI View 中按下 ESC 键会**自动退出 UI 模式**(deactivate providers)
|
|
94
|
+
- 插件无需手动处理 ESC 键的退出逻辑
|
|
95
|
+
- 这与 CoreBox 主界面的 ESC 行为保持一致
|
|
96
|
+
|
|
97
|
+
#### 键盘事件转发
|
|
98
|
+
以下按键会从 CoreBox 主输入框转发到插件 UI View:
|
|
99
|
+
- **Enter** - 确认/提交
|
|
100
|
+
- **ArrowUp / ArrowDown** - 上下导航
|
|
101
|
+
- **Meta/Ctrl + 任意键** - 快捷键组合(Cmd+V 除外,用于粘贴)
|
|
102
|
+
|
|
103
|
+
> **注意**:`ArrowLeft` 和 `ArrowRight` 不会被转发,因为它们用于输入框中的文本编辑。如果需要左右导航,请使用 `Meta/Ctrl + ArrowLeft/ArrowRight`。
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
// 键盘事件数据结构
|
|
107
|
+
interface ForwardedKeyEvent {
|
|
108
|
+
key: string // 按键名称,如 'Enter', 'ArrowDown'
|
|
109
|
+
code: string // 按键代码,如 'Enter', 'ArrowDown'
|
|
110
|
+
metaKey: boolean // Cmd/Win 键是否按下
|
|
111
|
+
ctrlKey: boolean // Ctrl 键是否按下
|
|
112
|
+
altKey: boolean // Alt 键是否按下
|
|
113
|
+
shiftKey: boolean // Shift 键是否按下
|
|
114
|
+
repeat: boolean // 是否为重复按键
|
|
115
|
+
}
|
|
69
116
|
```
|
|
70
117
|
|
|
71
118
|
## 废弃的 API
|
|
@@ -151,7 +198,7 @@ onInit(context) {
|
|
|
151
198
|
export default {
|
|
152
199
|
onInit(context) {
|
|
153
200
|
const { feature, box } = context.utils
|
|
154
|
-
|
|
201
|
+
|
|
155
202
|
// 监听输入变化
|
|
156
203
|
feature.onInputChange((input) => {
|
|
157
204
|
if (input.length > 2) {
|
|
@@ -163,10 +210,10 @@ export default {
|
|
|
163
210
|
}
|
|
164
211
|
})
|
|
165
212
|
},
|
|
166
|
-
|
|
213
|
+
|
|
167
214
|
onFeatureTriggered(featureId, query, feature) {
|
|
168
215
|
const { feature: featureSDK, box } = this.context.utils
|
|
169
|
-
|
|
216
|
+
|
|
170
217
|
// 推送结果
|
|
171
218
|
featureSDK.pushItems([
|
|
172
219
|
{
|
|
@@ -176,10 +223,10 @@ export default {
|
|
|
176
223
|
source: { id: this.pluginName, name: this.pluginName }
|
|
177
224
|
}
|
|
178
225
|
])
|
|
179
|
-
|
|
226
|
+
|
|
180
227
|
// 扩展窗口显示结果
|
|
181
228
|
box.expand({ length: 5 })
|
|
182
|
-
|
|
229
|
+
|
|
183
230
|
// 3秒后隐藏
|
|
184
231
|
setTimeout(() => {
|
|
185
232
|
box.hide()
|
|
@@ -207,7 +254,8 @@ export default {
|
|
|
207
254
|
- `core-box:get-input` - 获取当前输入值
|
|
208
255
|
- `core-box:set-input` - 设置输入框内容
|
|
209
256
|
- `core-box:clear-input` - 清空输入框
|
|
210
|
-
- `core-box:input-
|
|
257
|
+
- `core-box:input-change` - 输入变化广播(主进程 → 插件)
|
|
258
|
+
- `core-box:key-event` - 键盘事件转发(主进程 → 插件 UI View)
|
|
211
259
|
- `core-box:set-input-visibility` - 设置输入框可见性(主进程 → 渲染进程)
|
|
212
260
|
- `core-box:request-input-value` - 请求输入值(主进程 → 渲染进程)
|
|
213
261
|
|