@talex-touch/utils 1.0.40 → 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.
@@ -803,6 +803,65 @@ export interface TuffContext {
803
803
  tags?: string[]
804
804
  }
805
805
 
806
+ // ==================== Footer Hints 配置 ====================
807
+
808
+ /**
809
+ * Footer hint 单项配置
810
+ */
811
+ export interface TuffFooterHintItem {
812
+ /** 快捷键显示文本 */
813
+ key: string
814
+ /** 提示标签 */
815
+ label: string
816
+ /** 是否显示此提示 */
817
+ visible?: boolean
818
+ /** 触发的事件名称 */
819
+ event?: string
820
+ /** 事件携带的数据 */
821
+ eventData?: Record<string, unknown>
822
+ }
823
+
824
+ /**
825
+ * Footer hints 配置
826
+ * @description 控制 CoreBox 底部快捷键提示的显示和行为
827
+ */
828
+ export interface TuffFooterHints {
829
+ /**
830
+ * 主操作提示(回车键)
831
+ * @description 自定义回车键的文案,如 "发送"、"执行"、"打开" 等
832
+ */
833
+ primary?: {
834
+ /** 自定义标签文案 */
835
+ label?: string
836
+ /** 是否显示 */
837
+ visible?: boolean
838
+ }
839
+
840
+ /**
841
+ * 辅助操作提示(Meta+K)
842
+ * @description 控制 Meta+K 快捷键的显示和行为
843
+ */
844
+ secondary?: {
845
+ /** 自定义标签文案 */
846
+ label?: string
847
+ /** 是否显示,默认 false(隐藏) */
848
+ visible?: boolean
849
+ /** 触发的事件名称 */
850
+ event?: string
851
+ /** 事件携带的数据 */
852
+ eventData?: Record<string, unknown>
853
+ }
854
+
855
+ /**
856
+ * 快速选择提示(Meta+1-0)
857
+ * @description 控制快速选择快捷键的显示
858
+ */
859
+ quickSelect?: {
860
+ /** 是否显示,默认根据场景自动判断 */
861
+ visible?: boolean
862
+ }
863
+ }
864
+
806
865
  // ==================== 扩展元数据 ====================
807
866
 
808
867
  /**
@@ -1030,6 +1089,12 @@ export interface TuffMeta {
1030
1089
  * @warning 不建议添加太多关键词(建议 <= 10),过多会影响搜索性能
1031
1090
  */
1032
1091
  keywords?: string[]
1092
+
1093
+ /**
1094
+ * Footer hints 配置
1095
+ * @description 控制 CoreBox 底部快捷键提示的显示和行为
1096
+ */
1097
+ footerHints?: TuffFooterHints
1033
1098
  }
1034
1099
 
1035
1100
  // ==================== 前端展示结构 ====================
@@ -1,4 +1,4 @@
1
- import type { AiInvokeOptions, AiInvokeResult, AiProviderConfig } from '../types/intelligence'
1
+ import type { IntelligenceInvokeOptions, IntelligenceInvokeResult, IntelligenceProviderConfig } from '../types/intelligence'
2
2
 
3
3
  export interface IntelligenceClientChannel {
4
4
  send: (eventName: string, payload: unknown) => Promise<any>
@@ -50,10 +50,10 @@ async function assertResponse<T>(promise: Promise<ChannelResponse<T>>): Promise<
50
50
  }
51
51
 
52
52
  export interface IntelligenceClient {
53
- invoke: <T = any>(capabilityId: string, payload: any, options?: AiInvokeOptions) => Promise<AiInvokeResult<T>>
54
- testProvider: (config: AiProviderConfig) => Promise<unknown>
53
+ invoke: <T = any>(capabilityId: string, payload: any, options?: IntelligenceInvokeOptions) => Promise<IntelligenceInvokeResult<T>>
54
+ testProvider: (config: IntelligenceProviderConfig) => Promise<unknown>
55
55
  testCapability: (params: Record<string, any>) => Promise<unknown>
56
- fetchModels: (config: AiProviderConfig) => Promise<{ success: boolean, models?: string[], message?: string }>
56
+ fetchModels: (config: IntelligenceProviderConfig) => Promise<{ success: boolean, models?: string[], message?: string }>
57
57
  }
58
58
 
59
59
  export function createIntelligenceClient(channel?: IntelligenceClientChannel, resolvers?: IntelligenceChannelResolver[]): IntelligenceClient {
@@ -63,12 +63,12 @@ export function createIntelligenceClient(channel?: IntelligenceClientChannel, re
63
63
  }
64
64
 
65
65
  return {
66
- invoke<T = any>(capabilityId: string, payload: any, options?: AiInvokeOptions) {
67
- return assertResponse<AiInvokeResult<T>>(
66
+ invoke<T = any>(capabilityId: string, payload: any, options?: IntelligenceInvokeOptions) {
67
+ return assertResponse<IntelligenceInvokeResult<T>>(
68
68
  resolvedChannel.send('intelligence:invoke', { capabilityId, payload, options }),
69
69
  )
70
70
  },
71
- testProvider(config: AiProviderConfig) {
71
+ testProvider(config: IntelligenceProviderConfig) {
72
72
  return assertResponse(
73
73
  resolvedChannel.send('intelligence:test-provider', { provider: config }),
74
74
  )
@@ -78,7 +78,7 @@ export function createIntelligenceClient(channel?: IntelligenceClientChannel, re
78
78
  resolvedChannel.send('intelligence:test-capability', params),
79
79
  )
80
80
  },
81
- fetchModels(config: AiProviderConfig) {
81
+ fetchModels(config: IntelligenceProviderConfig) {
82
82
  return assertResponse<{ success: boolean, models?: string[], message?: string }>(
83
83
  resolvedChannel.send('intelligence:fetch-models', { provider: config }),
84
84
  )
@@ -21,6 +21,20 @@ function defineProvider(
21
21
  }
22
22
 
23
23
  export const DEFAULT_MARKET_PROVIDERS: MarketProviderDefinition[] = [
24
+ defineProvider({
25
+ id: 'tuff-nexus',
26
+ name: 'Tuff Nexus',
27
+ type: 'tpexApi',
28
+ url: 'https://tuff.tagzxia.com',
29
+ description: 'Tuff 官方插件市场,提供经过审核的插件。',
30
+ enabled: true,
31
+ priority: 110,
32
+ trustLevel: 'official',
33
+ readOnly: true,
34
+ config: {
35
+ apiUrl: 'https://tuff.tagzxia.com/api/market/plugins',
36
+ },
37
+ }),
24
38
  defineProvider({
25
39
  id: 'talex-official',
26
40
  name: 'Talex Official',
package/market/types.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { StorageList } from '../common/storage/constants'
2
2
 
3
- export type MarketProviderType = 'repository' | 'nexusStore' | 'npmPackage'
3
+ export type MarketProviderType = 'repository' | 'nexusStore' | 'npmPackage' | 'tpexApi'
4
4
 
5
5
  export type MarketProviderTrustLevel = 'official' | 'verified' | 'unverified'
6
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@talex-touch/utils",
3
- "version": "1.0.40",
3
+ "version": "1.0.42",
4
4
  "private": false,
5
5
  "description": "Tuff series utils",
6
6
  "author": "TalexDreamSoul",
package/plugin/index.ts CHANGED
@@ -134,7 +134,8 @@ export interface ITouchPlugin extends IPluginBaseInfo {
134
134
  export interface IFeatureCommand {
135
135
  type: 'match' | 'contain' | 'regex' | 'function' | 'over' | 'image' | 'files' | 'directory' | 'window'
136
136
  value: string | string[] | RegExp | Function
137
- onTrigger: () => void
137
+ /** Optional trigger callback - not serialized over IPC */
138
+ onTrigger?: () => void
138
139
  }
139
140
 
140
141
  export interface IPluginFeature {
@@ -1,2 +1,6 @@
1
1
  export * from './registry'
2
2
  export * from './types'
3
+ export * from './tpex-types'
4
+ export * from './tpex-provider'
5
+ export * from './npm-provider'
6
+ export * from './market-client'
@@ -0,0 +1,215 @@
1
+ import type { NpmPackageInfo } from './npm-provider'
2
+ import type { TpexPluginInfo } from './tpex-provider'
3
+ import { NpmProvider } from './npm-provider'
4
+ import { TpexProvider } from './tpex-provider'
5
+ import { PluginProviderType } from './types'
6
+
7
+ export type PluginSourceType = 'tpex' | 'npm' | 'all'
8
+
9
+ export interface MarketPluginInfo {
10
+ id: string
11
+ name: string
12
+ slug: string
13
+ version: string
14
+ description: string
15
+ author: string
16
+ icon?: string
17
+ source: PluginSourceType
18
+ isOfficial: boolean
19
+ downloads?: number
20
+ category?: string
21
+ keywords?: string[]
22
+ homepage?: string
23
+ packageUrl?: string
24
+ raw: TpexPluginInfo | NpmPackageInfo
25
+ }
26
+
27
+ export interface MarketSearchOptions {
28
+ keyword?: string
29
+ source?: PluginSourceType
30
+ category?: string
31
+ limit?: number
32
+ offset?: number
33
+ }
34
+
35
+ export interface MarketSearchResult {
36
+ plugins: MarketPluginInfo[]
37
+ total: number
38
+ sources: {
39
+ tpex: number
40
+ npm: number
41
+ }
42
+ }
43
+
44
+ function normalizeTpexPlugin(plugin: TpexPluginInfo): MarketPluginInfo {
45
+ return {
46
+ id: plugin.id,
47
+ name: plugin.name,
48
+ slug: plugin.slug,
49
+ version: plugin.latestVersion?.version ?? '0.0.0',
50
+ description: plugin.summary,
51
+ author: plugin.author?.name ?? 'Unknown',
52
+ icon: plugin.iconUrl ?? undefined,
53
+ source: 'tpex',
54
+ isOfficial: plugin.isOfficial,
55
+ downloads: plugin.installs,
56
+ category: plugin.category,
57
+ homepage: plugin.homepage ?? undefined,
58
+ packageUrl: plugin.latestVersion?.packageUrl,
59
+ raw: plugin,
60
+ }
61
+ }
62
+
63
+ function normalizeNpmPlugin(pkg: NpmPackageInfo): MarketPluginInfo {
64
+ const authorName = typeof pkg.author === 'string'
65
+ ? pkg.author
66
+ : pkg.author?.name ?? 'Unknown'
67
+
68
+ return {
69
+ id: pkg.name,
70
+ name: pkg.name.replace(/^tuff-plugin-/, '').replace(/^@tuff\//, ''),
71
+ slug: pkg.name,
72
+ version: pkg.version,
73
+ description: pkg.description ?? '',
74
+ author: authorName,
75
+ icon: pkg.tuff?.icon,
76
+ source: 'npm',
77
+ isOfficial: pkg.name.startsWith('@tuff/'),
78
+ keywords: pkg.keywords,
79
+ packageUrl: pkg.dist.tarball,
80
+ raw: pkg,
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Unified plugin market client supporting multiple sources
86
+ */
87
+ export class PluginMarketClient {
88
+ private tpexProvider: TpexProvider
89
+ private npmProvider: NpmProvider
90
+
91
+ constructor(options?: {
92
+ tpexApiBase?: string
93
+ npmRegistry?: string
94
+ }) {
95
+ this.tpexProvider = new TpexProvider(options?.tpexApiBase)
96
+ this.npmProvider = new NpmProvider(options?.npmRegistry)
97
+ }
98
+
99
+ /**
100
+ * Search plugins from all sources
101
+ */
102
+ async search(options: MarketSearchOptions = {}): Promise<MarketSearchResult> {
103
+ const { keyword, source = 'all', limit = 50, offset = 0 } = options
104
+ const results: MarketPluginInfo[] = []
105
+ let tpexCount = 0
106
+ let npmCount = 0
107
+
108
+ if (source === 'all' || source === 'tpex') {
109
+ try {
110
+ const tpexPlugins = keyword
111
+ ? await this.tpexProvider.searchPlugins(keyword)
112
+ : await this.tpexProvider.listPlugins()
113
+
114
+ const normalized = tpexPlugins.map(normalizeTpexPlugin)
115
+ results.push(...normalized)
116
+ tpexCount = normalized.length
117
+ }
118
+ catch (error) {
119
+ console.warn('[MarketClient] TPEX search failed:', error)
120
+ }
121
+ }
122
+
123
+ if (source === 'all' || source === 'npm') {
124
+ try {
125
+ const npmPlugins = await this.npmProvider.searchPlugins(keyword)
126
+ const normalized = npmPlugins.map(normalizeNpmPlugin)
127
+ results.push(...normalized)
128
+ npmCount = normalized.length
129
+ }
130
+ catch (error) {
131
+ console.warn('[MarketClient] NPM search failed:', error)
132
+ }
133
+ }
134
+
135
+ const sorted = results.sort((a, b) => {
136
+ if (a.isOfficial !== b.isOfficial) return a.isOfficial ? -1 : 1
137
+ return (b.downloads ?? 0) - (a.downloads ?? 0)
138
+ })
139
+
140
+ const paginated = sorted.slice(offset, offset + limit)
141
+
142
+ return {
143
+ plugins: paginated,
144
+ total: results.length,
145
+ sources: {
146
+ tpex: tpexCount,
147
+ npm: npmCount,
148
+ },
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Get plugin details by identifier
154
+ */
155
+ async getPlugin(identifier: string, source?: PluginSourceType): Promise<MarketPluginInfo | null> {
156
+ if (source === 'tpex' || (!source && !identifier.includes('/'))) {
157
+ try {
158
+ const plugin = await this.tpexProvider.getPlugin(identifier)
159
+ if (plugin) return normalizeTpexPlugin(plugin)
160
+ }
161
+ catch {
162
+ // Fall through to npm
163
+ }
164
+ }
165
+
166
+ if (source === 'npm' || !source) {
167
+ try {
168
+ const pkg = await this.npmProvider.getPackageInfo(identifier)
169
+ if (pkg) return normalizeNpmPlugin(pkg)
170
+ }
171
+ catch {
172
+ // Not found
173
+ }
174
+ }
175
+
176
+ return null
177
+ }
178
+
179
+ /**
180
+ * Get install source string for a plugin
181
+ */
182
+ getInstallSource(plugin: MarketPluginInfo): string {
183
+ if (plugin.source === 'tpex') {
184
+ return `tpex:${plugin.slug}`
185
+ }
186
+ return `npm:${plugin.id}`
187
+ }
188
+
189
+ /**
190
+ * Get provider type for a plugin
191
+ */
192
+ getProviderType(plugin: MarketPluginInfo): PluginProviderType {
193
+ return plugin.source === 'tpex'
194
+ ? PluginProviderType.TPEX
195
+ : PluginProviderType.NPM
196
+ }
197
+
198
+ /**
199
+ * List all plugins from official source (TPEX)
200
+ */
201
+ async listOfficialPlugins(): Promise<MarketPluginInfo[]> {
202
+ const plugins = await this.tpexProvider.listPlugins()
203
+ return plugins.map(normalizeTpexPlugin)
204
+ }
205
+
206
+ /**
207
+ * List all plugins from npm
208
+ */
209
+ async listNpmPlugins(): Promise<MarketPluginInfo[]> {
210
+ const plugins = await this.npmProvider.listPlugins()
211
+ return plugins.map(normalizeNpmPlugin)
212
+ }
213
+ }
214
+
215
+ export const defaultMarketClient = new PluginMarketClient()
@@ -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()