@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
|
@@ -1,4 +1,15 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ITuffTransport } from '@talex-touch/utils/transport'
|
|
2
|
+
import { defineRawEvent } from '@talex-touch/utils/transport/event/builder'
|
|
3
|
+
|
|
4
|
+
const terminalCreateEvent = defineRawEvent<{ command: string, args?: string[] }, { id: string }>(
|
|
5
|
+
'terminal:create',
|
|
6
|
+
)
|
|
7
|
+
const terminalWriteEvent = defineRawEvent<{ id: string, data: string }, void>('terminal:write')
|
|
8
|
+
const terminalKillEvent = defineRawEvent<{ id: string }, void>('terminal:kill')
|
|
9
|
+
const terminalDataEvent = defineRawEvent<{ id: string, data: string }, void>('terminal:data')
|
|
10
|
+
const terminalExitEvent = defineRawEvent<{ id: string, exitCode: number | null }, void>(
|
|
11
|
+
'terminal:exit',
|
|
12
|
+
)
|
|
2
13
|
|
|
3
14
|
type DataCallback = (data: string) => void
|
|
4
15
|
type ExitCallback = (exitCode: number | null) => void
|
|
@@ -7,10 +18,10 @@ export class Terminal {
|
|
|
7
18
|
private id: string | null = null
|
|
8
19
|
private onDataCallback: DataCallback | null = null
|
|
9
20
|
private onExitCallback: ExitCallback | null = null
|
|
10
|
-
private
|
|
21
|
+
private transport: ITuffTransport
|
|
11
22
|
|
|
12
|
-
constructor(
|
|
13
|
-
this.
|
|
23
|
+
constructor(transport: ITuffTransport) {
|
|
24
|
+
this.transport = transport
|
|
14
25
|
}
|
|
15
26
|
|
|
16
27
|
/**
|
|
@@ -26,19 +37,19 @@ export class Terminal {
|
|
|
26
37
|
// However, for simplicity in this refactor, we'll assume exec is called for a new, independent command.
|
|
27
38
|
// A more robust implementation might track multiple concurrent processes.
|
|
28
39
|
|
|
29
|
-
const { id } = await this.
|
|
40
|
+
const { id } = await this.transport.send(terminalCreateEvent, { command, args })
|
|
30
41
|
this.id = id
|
|
31
42
|
|
|
32
43
|
// Re-register listeners for the new process ID
|
|
33
|
-
this.
|
|
34
|
-
if (this.id ===
|
|
35
|
-
this.onDataCallback(
|
|
44
|
+
this.transport.on(terminalDataEvent, (payload) => {
|
|
45
|
+
if (this.id === payload.id && this.onDataCallback) {
|
|
46
|
+
this.onDataCallback(payload.data)
|
|
36
47
|
}
|
|
37
48
|
})
|
|
38
49
|
|
|
39
|
-
this.
|
|
40
|
-
if (this.id ===
|
|
41
|
-
this.onExitCallback(
|
|
50
|
+
this.transport.on(terminalExitEvent, (payload) => {
|
|
51
|
+
if (this.id === payload.id && this.onExitCallback) {
|
|
52
|
+
this.onExitCallback(payload.exitCode)
|
|
42
53
|
this.id = null
|
|
43
54
|
}
|
|
44
55
|
})
|
|
@@ -53,7 +64,7 @@ export class Terminal {
|
|
|
53
64
|
*/
|
|
54
65
|
public write(data: string): void {
|
|
55
66
|
if (this.id) {
|
|
56
|
-
this.
|
|
67
|
+
void this.transport.send(terminalWriteEvent, { id: this.id, data })
|
|
57
68
|
}
|
|
58
69
|
}
|
|
59
70
|
|
|
@@ -62,7 +73,7 @@ export class Terminal {
|
|
|
62
73
|
*/
|
|
63
74
|
public kill(): void {
|
|
64
75
|
if (this.id) {
|
|
65
|
-
this.
|
|
76
|
+
void this.transport.send(terminalKillEvent, { id: this.id })
|
|
66
77
|
this.id = null
|
|
67
78
|
}
|
|
68
79
|
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature Matching Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides enhanced search matching for plugin features with:
|
|
5
|
+
* - Pinyin/English token matching
|
|
6
|
+
* - Fuzzy match support
|
|
7
|
+
* - Match range generation for UI highlighting
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { fuzzyMatch, indicesToRanges } from './fuzzy-match'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Match range for highlighting
|
|
14
|
+
*/
|
|
15
|
+
export interface MatchRange {
|
|
16
|
+
start: number
|
|
17
|
+
end: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Feature match result with score and highlight ranges
|
|
22
|
+
*/
|
|
23
|
+
export interface FeatureMatchResult {
|
|
24
|
+
/** Whether the feature matches the query */
|
|
25
|
+
matched: boolean
|
|
26
|
+
/** Match score (0-1000, higher is better) */
|
|
27
|
+
score: number
|
|
28
|
+
/** Match type for debugging */
|
|
29
|
+
matchType: 'exact' | 'token' | 'prefix' | 'contains' | 'fuzzy' | 'none'
|
|
30
|
+
/** Match ranges for highlighting in title */
|
|
31
|
+
matchRanges: MatchRange[]
|
|
32
|
+
/** Which token matched (for debugging) */
|
|
33
|
+
matchedToken?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Options for feature matching
|
|
38
|
+
*/
|
|
39
|
+
export interface FeatureMatchOptions {
|
|
40
|
+
/** Feature title/name */
|
|
41
|
+
title: string
|
|
42
|
+
/** Feature description */
|
|
43
|
+
desc?: string
|
|
44
|
+
/** Pre-computed search tokens (pinyin, initials, etc.) */
|
|
45
|
+
searchTokens?: string[]
|
|
46
|
+
/** Search query */
|
|
47
|
+
query: string
|
|
48
|
+
/** Enable fuzzy matching (default: true) */
|
|
49
|
+
enableFuzzy?: boolean
|
|
50
|
+
/** Maximum fuzzy errors (default: 2) */
|
|
51
|
+
maxFuzzyErrors?: number
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Find substring match and return range
|
|
56
|
+
*/
|
|
57
|
+
function findSubstringMatch(text: string, query: string): MatchRange | null {
|
|
58
|
+
const lowerText = text.toLowerCase()
|
|
59
|
+
const lowerQuery = query.toLowerCase()
|
|
60
|
+
const index = lowerText.indexOf(lowerQuery)
|
|
61
|
+
|
|
62
|
+
if (index === -1)
|
|
63
|
+
return null
|
|
64
|
+
|
|
65
|
+
return { start: index, end: index + query.length }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Match query against a single token and return match info
|
|
70
|
+
*/
|
|
71
|
+
function matchToken(
|
|
72
|
+
token: string,
|
|
73
|
+
query: string,
|
|
74
|
+
): { matched: boolean, score: number, type: 'exact' | 'prefix' | 'contains' | 'none' } {
|
|
75
|
+
const lowerToken = token.toLowerCase()
|
|
76
|
+
const lowerQuery = query.toLowerCase()
|
|
77
|
+
|
|
78
|
+
// Exact match
|
|
79
|
+
if (lowerToken === lowerQuery) {
|
|
80
|
+
return { matched: true, score: 1000, type: 'exact' }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Prefix match (token starts with query)
|
|
84
|
+
if (lowerToken.startsWith(lowerQuery)) {
|
|
85
|
+
// Score based on how much of the token is matched
|
|
86
|
+
const coverage = lowerQuery.length / lowerToken.length
|
|
87
|
+
return { matched: true, score: 800 + Math.round(coverage * 100), type: 'prefix' }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Contains match
|
|
91
|
+
if (lowerToken.includes(lowerQuery)) {
|
|
92
|
+
const coverage = lowerQuery.length / lowerToken.length
|
|
93
|
+
return { matched: true, score: 600 + Math.round(coverage * 50), type: 'contains' }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { matched: false, score: 0, type: 'none' }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Match feature against search query
|
|
101
|
+
*
|
|
102
|
+
* This is the main matching function that:
|
|
103
|
+
* 1. Tries exact/prefix/contains match against title
|
|
104
|
+
* 2. Tries token matching (pinyin, initials, keywords)
|
|
105
|
+
* 3. Falls back to fuzzy matching
|
|
106
|
+
*
|
|
107
|
+
* @returns FeatureMatchResult with score and highlight ranges
|
|
108
|
+
*/
|
|
109
|
+
export function matchFeature(options: FeatureMatchOptions): FeatureMatchResult {
|
|
110
|
+
const {
|
|
111
|
+
title,
|
|
112
|
+
desc,
|
|
113
|
+
searchTokens = [],
|
|
114
|
+
query,
|
|
115
|
+
enableFuzzy = true,
|
|
116
|
+
maxFuzzyErrors = 2,
|
|
117
|
+
} = options
|
|
118
|
+
|
|
119
|
+
const trimmedQuery = query.trim()
|
|
120
|
+
|
|
121
|
+
// Empty query - no match
|
|
122
|
+
if (!trimmedQuery) {
|
|
123
|
+
return { matched: false, score: 0, matchType: 'none', matchRanges: [] }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const lowerQuery = trimmedQuery.toLowerCase()
|
|
127
|
+
const lowerTitle = title.toLowerCase()
|
|
128
|
+
|
|
129
|
+
// 1. Exact title match
|
|
130
|
+
if (lowerTitle === lowerQuery) {
|
|
131
|
+
return {
|
|
132
|
+
matched: true,
|
|
133
|
+
score: 1000,
|
|
134
|
+
matchType: 'exact',
|
|
135
|
+
matchRanges: [{ start: 0, end: title.length }],
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 2. Title prefix match
|
|
140
|
+
if (lowerTitle.startsWith(lowerQuery)) {
|
|
141
|
+
return {
|
|
142
|
+
matched: true,
|
|
143
|
+
score: 900,
|
|
144
|
+
matchType: 'prefix',
|
|
145
|
+
matchRanges: [{ start: 0, end: trimmedQuery.length }],
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 3. Title contains match
|
|
150
|
+
const titleMatch = findSubstringMatch(title, trimmedQuery)
|
|
151
|
+
if (titleMatch) {
|
|
152
|
+
return {
|
|
153
|
+
matched: true,
|
|
154
|
+
score: 700,
|
|
155
|
+
matchType: 'contains',
|
|
156
|
+
matchRanges: [titleMatch],
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 4. Search tokens matching (pinyin, initials, keywords)
|
|
161
|
+
// This enables searching "fanyi" to match "翻译"
|
|
162
|
+
if (searchTokens.length > 0) {
|
|
163
|
+
let bestTokenMatch: {
|
|
164
|
+
score: number
|
|
165
|
+
type: 'exact' | 'prefix' | 'contains'
|
|
166
|
+
token: string
|
|
167
|
+
} | null = null
|
|
168
|
+
|
|
169
|
+
for (const token of searchTokens) {
|
|
170
|
+
if (!token)
|
|
171
|
+
continue
|
|
172
|
+
|
|
173
|
+
const result = matchToken(token, trimmedQuery)
|
|
174
|
+
if (result.matched && (!bestTokenMatch || result.score > bestTokenMatch.score)) {
|
|
175
|
+
bestTokenMatch = {
|
|
176
|
+
score: result.score,
|
|
177
|
+
type: result.type as 'exact' | 'prefix' | 'contains',
|
|
178
|
+
token,
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (bestTokenMatch) {
|
|
184
|
+
// Token matched - highlight entire title since we can't map token back to characters
|
|
185
|
+
// For pinyin matches, the full Chinese title is relevant
|
|
186
|
+
return {
|
|
187
|
+
matched: true,
|
|
188
|
+
score: bestTokenMatch.score - 50, // Slightly lower than direct title match
|
|
189
|
+
matchType: 'token',
|
|
190
|
+
matchRanges: [{ start: 0, end: title.length }],
|
|
191
|
+
matchedToken: bestTokenMatch.token,
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 5. Description matching (lower priority)
|
|
197
|
+
if (desc) {
|
|
198
|
+
const descMatch = findSubstringMatch(desc, trimmedQuery)
|
|
199
|
+
if (descMatch) {
|
|
200
|
+
return {
|
|
201
|
+
matched: true,
|
|
202
|
+
score: 400,
|
|
203
|
+
matchType: 'contains',
|
|
204
|
+
matchRanges: [], // No title highlight for desc matches
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 6. Fuzzy matching on title
|
|
210
|
+
if (enableFuzzy) {
|
|
211
|
+
const fuzzyResult = fuzzyMatch(title, trimmedQuery, maxFuzzyErrors)
|
|
212
|
+
if (fuzzyResult.matched && fuzzyResult.score > 0.5) {
|
|
213
|
+
return {
|
|
214
|
+
matched: true,
|
|
215
|
+
score: Math.round(fuzzyResult.score * 500), // Scale to 0-500 range
|
|
216
|
+
matchType: 'fuzzy',
|
|
217
|
+
matchRanges: indicesToRanges(fuzzyResult.matchedIndices),
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Try fuzzy on tokens
|
|
222
|
+
for (const token of searchTokens) {
|
|
223
|
+
if (!token || token.length < 2)
|
|
224
|
+
continue
|
|
225
|
+
|
|
226
|
+
const tokenFuzzy = fuzzyMatch(token, trimmedQuery, maxFuzzyErrors)
|
|
227
|
+
if (tokenFuzzy.matched && tokenFuzzy.score > 0.6) {
|
|
228
|
+
return {
|
|
229
|
+
matched: true,
|
|
230
|
+
score: Math.round(tokenFuzzy.score * 400),
|
|
231
|
+
matchType: 'fuzzy',
|
|
232
|
+
matchRanges: [{ start: 0, end: title.length }],
|
|
233
|
+
matchedToken: token,
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return { matched: false, score: 0, matchType: 'none', matchRanges: [] }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Batch match multiple features and return sorted results
|
|
244
|
+
*
|
|
245
|
+
* @param features Array of features with their search metadata
|
|
246
|
+
* @param query Search query
|
|
247
|
+
* @returns Features sorted by match score (highest first), with match metadata
|
|
248
|
+
*/
|
|
249
|
+
export function matchFeatures<T extends { searchTokens?: string[] }>(
|
|
250
|
+
features: Array<{
|
|
251
|
+
feature: T
|
|
252
|
+
title: string
|
|
253
|
+
desc?: string
|
|
254
|
+
}>,
|
|
255
|
+
query: string,
|
|
256
|
+
): Array<{
|
|
257
|
+
feature: T
|
|
258
|
+
result: FeatureMatchResult
|
|
259
|
+
}> {
|
|
260
|
+
const results: Array<{ feature: T, result: FeatureMatchResult }> = []
|
|
261
|
+
|
|
262
|
+
for (const { feature, title, desc } of features) {
|
|
263
|
+
const result = matchFeature({
|
|
264
|
+
title,
|
|
265
|
+
desc,
|
|
266
|
+
searchTokens: feature.searchTokens,
|
|
267
|
+
query,
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
if (result.matched) {
|
|
271
|
+
results.push({ feature, result })
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Sort by score descending
|
|
276
|
+
results.sort((a, b) => b.result.score - a.result.score)
|
|
277
|
+
|
|
278
|
+
return results
|
|
279
|
+
}
|
package/search/fuzzy-match.ts
CHANGED
|
@@ -24,7 +24,7 @@ export interface FuzzyMatchResult {
|
|
|
24
24
|
export function fuzzyMatch(
|
|
25
25
|
target: string,
|
|
26
26
|
query: string,
|
|
27
|
-
maxErrors = 2
|
|
27
|
+
maxErrors = 2,
|
|
28
28
|
): FuzzyMatchResult {
|
|
29
29
|
if (!query || !target) {
|
|
30
30
|
return { matched: false, score: 0, matchedIndices: [] }
|
|
@@ -38,7 +38,7 @@ export function fuzzyMatch(
|
|
|
38
38
|
return {
|
|
39
39
|
matched: true,
|
|
40
40
|
score: 1,
|
|
41
|
-
matchedIndices: Array.from({ length: target.length }, (_, i) => i)
|
|
41
|
+
matchedIndices: Array.from({ length: target.length }, (_, i) => i),
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -48,7 +48,7 @@ export function fuzzyMatch(
|
|
|
48
48
|
return {
|
|
49
49
|
matched: true,
|
|
50
50
|
score: 0.95,
|
|
51
|
-
matchedIndices: Array.from({ length: query.length }, (_, i) => substringIndex + i)
|
|
51
|
+
matchedIndices: Array.from({ length: query.length }, (_, i) => substringIndex + i),
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -58,7 +58,7 @@ export function fuzzyMatch(
|
|
|
58
58
|
return {
|
|
59
59
|
matched: true,
|
|
60
60
|
score: 0.8 + (subsequenceResult.matchedIndices.length / target.length) * 0.1,
|
|
61
|
-
matchedIndices: subsequenceResult.matchedIndices
|
|
61
|
+
matchedIndices: subsequenceResult.matchedIndices,
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
|
|
@@ -77,8 +77,8 @@ export function fuzzyMatch(
|
|
|
77
77
|
*/
|
|
78
78
|
function subsequenceMatch(
|
|
79
79
|
target: string,
|
|
80
|
-
query: string
|
|
81
|
-
): { matched: boolean
|
|
80
|
+
query: string,
|
|
81
|
+
): { matched: boolean, matchedIndices: number[] } {
|
|
82
82
|
const matchedIndices: number[] = []
|
|
83
83
|
let queryIdx = 0
|
|
84
84
|
|
|
@@ -91,7 +91,7 @@ function subsequenceMatch(
|
|
|
91
91
|
|
|
92
92
|
return {
|
|
93
93
|
matched: queryIdx === query.length,
|
|
94
|
-
matchedIndices
|
|
94
|
+
matchedIndices,
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
|
|
@@ -102,13 +102,15 @@ function subsequenceMatch(
|
|
|
102
102
|
function fuzzyMatchWithErrors(
|
|
103
103
|
target: string,
|
|
104
104
|
query: string,
|
|
105
|
-
maxErrors: number
|
|
105
|
+
maxErrors: number,
|
|
106
106
|
): FuzzyMatchResult {
|
|
107
107
|
const m = query.length
|
|
108
108
|
const n = target.length
|
|
109
109
|
|
|
110
|
-
if (m === 0)
|
|
111
|
-
|
|
110
|
+
if (m === 0)
|
|
111
|
+
return { matched: true, score: 1, matchedIndices: [] }
|
|
112
|
+
if (n === 0)
|
|
113
|
+
return { matched: false, score: 0, matchedIndices: [] }
|
|
112
114
|
|
|
113
115
|
// Allow more errors for longer queries
|
|
114
116
|
const allowedErrors = Math.min(maxErrors, Math.floor(m / 3) + 1)
|
|
@@ -135,7 +137,7 @@ function fuzzyMatchWithErrors(
|
|
|
135
137
|
bestScore = score
|
|
136
138
|
bestStart = start
|
|
137
139
|
// Adjust indices to be relative to the full target string
|
|
138
|
-
bestMatchedIndices = matchedIndices.map(
|
|
140
|
+
bestMatchedIndices = matchedIndices.map(i => start + i)
|
|
139
141
|
}
|
|
140
142
|
}
|
|
141
143
|
}
|
|
@@ -145,7 +147,7 @@ function fuzzyMatchWithErrors(
|
|
|
145
147
|
return {
|
|
146
148
|
matched: true,
|
|
147
149
|
score: bestScore,
|
|
148
|
-
matchedIndices: bestMatchedIndices
|
|
150
|
+
matchedIndices: bestMatchedIndices,
|
|
149
151
|
}
|
|
150
152
|
}
|
|
151
153
|
|
|
@@ -157,25 +159,41 @@ function fuzzyMatchWithErrors(
|
|
|
157
159
|
*/
|
|
158
160
|
function editDistanceWithPath(
|
|
159
161
|
s1: string,
|
|
160
|
-
s2: string
|
|
161
|
-
): { distance: number
|
|
162
|
+
s2: string,
|
|
163
|
+
): { distance: number, matchedIndices: number[] } {
|
|
162
164
|
const m = s1.length
|
|
163
165
|
const n = s2.length
|
|
164
166
|
|
|
165
167
|
// DP table
|
|
166
|
-
const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1)
|
|
168
|
+
const dp: number[][] = Array.from({ length: m + 1 }, () => Array.from({ length: n + 1 }, () => 0))
|
|
167
169
|
|
|
168
170
|
// Initialize
|
|
169
|
-
for (let i = 0; i <= m; i++)
|
|
170
|
-
|
|
171
|
+
for (let i = 0; i <= m; i++) {
|
|
172
|
+
const row = dp[i]
|
|
173
|
+
if (row)
|
|
174
|
+
row[0] = i
|
|
175
|
+
}
|
|
176
|
+
const firstRow = dp[0]
|
|
177
|
+
if (firstRow) {
|
|
178
|
+
for (let j = 0; j <= n; j++)
|
|
179
|
+
firstRow[j] = j
|
|
180
|
+
}
|
|
171
181
|
|
|
172
182
|
// Fill DP table
|
|
173
183
|
for (let i = 1; i <= m; i++) {
|
|
174
184
|
for (let j = 1; j <= n; j++) {
|
|
185
|
+
const row = dp[i]
|
|
186
|
+
const prevRow = dp[i - 1]
|
|
187
|
+
if (!row || !prevRow)
|
|
188
|
+
continue
|
|
175
189
|
if (s1[i - 1] === s2[j - 1]) {
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
|
|
190
|
+
row[j] = prevRow[j - 1] ?? 0
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
const up = prevRow[j] ?? 0
|
|
194
|
+
const left = row[j - 1] ?? 0
|
|
195
|
+
const diag = prevRow[j - 1] ?? 0
|
|
196
|
+
row[j] = 1 + Math.min(up, left, diag)
|
|
179
197
|
}
|
|
180
198
|
}
|
|
181
199
|
}
|
|
@@ -190,20 +208,24 @@ function editDistanceWithPath(
|
|
|
190
208
|
matchedIndices.unshift(i - 1)
|
|
191
209
|
i--
|
|
192
210
|
j--
|
|
193
|
-
}
|
|
211
|
+
}
|
|
212
|
+
else if ((dp[i - 1]?.[j - 1] ?? 0) <= (dp[i - 1]?.[j] ?? 0)
|
|
213
|
+
&& (dp[i - 1]?.[j - 1] ?? 0) <= (dp[i]?.[j - 1] ?? 0)) {
|
|
194
214
|
// Substitution
|
|
195
215
|
i--
|
|
196
216
|
j--
|
|
197
|
-
}
|
|
217
|
+
}
|
|
218
|
+
else if ((dp[i - 1]?.[j] ?? 0) <= (dp[i]?.[j - 1] ?? 0)) {
|
|
198
219
|
// Deletion from s1
|
|
199
220
|
i--
|
|
200
|
-
}
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
201
223
|
// Insertion into s1
|
|
202
224
|
j--
|
|
203
225
|
}
|
|
204
226
|
}
|
|
205
227
|
|
|
206
|
-
return { distance: dp[m][n], matchedIndices }
|
|
228
|
+
return { distance: dp[m]?.[n] ?? 0, matchedIndices }
|
|
207
229
|
}
|
|
208
230
|
|
|
209
231
|
/**
|
|
@@ -213,7 +235,7 @@ function calculateFuzzyScore(
|
|
|
213
235
|
editDistance: number,
|
|
214
236
|
queryLength: number,
|
|
215
237
|
matchStart: number,
|
|
216
|
-
targetLength: number
|
|
238
|
+
targetLength: number,
|
|
217
239
|
): number {
|
|
218
240
|
// Base score from edit distance (0.5 - 0.7 range for fuzzy matches)
|
|
219
241
|
const distanceScore = Math.max(0, 1 - editDistance / queryLength) * 0.3 + 0.4
|
|
@@ -230,22 +252,30 @@ function calculateFuzzyScore(
|
|
|
230
252
|
/**
|
|
231
253
|
* Convert matched indices to Range array for highlighting
|
|
232
254
|
*/
|
|
233
|
-
export function indicesToRanges(indices: number[]): Array<{ start: number
|
|
234
|
-
if (!indices.length)
|
|
255
|
+
export function indicesToRanges(indices: number[]): Array<{ start: number, end: number }> {
|
|
256
|
+
if (!indices.length)
|
|
257
|
+
return []
|
|
235
258
|
|
|
236
259
|
const sorted = Array.from(new Set(indices)).sort((a, b) => a - b)
|
|
237
|
-
const ranges: Array<{ start: number
|
|
260
|
+
const ranges: Array<{ start: number, end: number }> = []
|
|
238
261
|
|
|
239
|
-
|
|
240
|
-
|
|
262
|
+
const first = sorted[0]
|
|
263
|
+
if (first === undefined)
|
|
264
|
+
return []
|
|
265
|
+
let start = first
|
|
266
|
+
let end = first + 1
|
|
241
267
|
|
|
242
268
|
for (let i = 1; i < sorted.length; i++) {
|
|
243
|
-
|
|
269
|
+
const current = sorted[i]
|
|
270
|
+
if (current === undefined)
|
|
271
|
+
continue
|
|
272
|
+
if (current === end) {
|
|
244
273
|
end++
|
|
245
|
-
}
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
246
276
|
ranges.push({ start, end })
|
|
247
|
-
start =
|
|
248
|
-
end =
|
|
277
|
+
start = current
|
|
278
|
+
end = current + 1
|
|
249
279
|
}
|
|
250
280
|
}
|
|
251
281
|
ranges.push({ start, end })
|
package/search/index.ts
ADDED
|
@@ -11,29 +11,35 @@ export function levenshteinDistance(s1: string, s2: string): number {
|
|
|
11
11
|
const n = s2.length
|
|
12
12
|
|
|
13
13
|
// Create a 2D array (m+1)x(n+1) to store distances
|
|
14
|
-
const dp: number[][] =
|
|
15
|
-
.fill(0)
|
|
16
|
-
.map(() => new Array(n + 1).fill(0))
|
|
14
|
+
const dp: number[][] = Array.from({ length: m + 1 }, () => Array.from({ length: n + 1 }, () => 0))
|
|
17
15
|
|
|
18
16
|
// Initialize the DP table
|
|
19
17
|
for (let i = 0; i <= m; i++) {
|
|
20
|
-
dp[i]
|
|
18
|
+
const row = dp[i]
|
|
19
|
+
if (row)
|
|
20
|
+
row[0] = i
|
|
21
21
|
}
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
const firstRow = dp[0]
|
|
23
|
+
if (firstRow) {
|
|
24
|
+
for (let j = 0; j <= n; j++)
|
|
25
|
+
firstRow[j] = j
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
// Fill the DP table
|
|
27
29
|
for (let i = 1; i <= m; i++) {
|
|
28
30
|
for (let j = 1; j <= n; j++) {
|
|
29
31
|
const cost = s1[i - 1] === s2[j - 1] ? 0 : 1
|
|
30
|
-
dp[i]
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
const row = dp[i]
|
|
33
|
+
const prevRow = dp[i - 1]
|
|
34
|
+
if (!row || !prevRow)
|
|
35
|
+
continue
|
|
36
|
+
row[j] = Math.min(
|
|
37
|
+
(prevRow[j] ?? 0) + 1, // Deletion
|
|
38
|
+
(row[j - 1] ?? 0) + 1, // Insertion
|
|
39
|
+
(prevRow[j - 1] ?? 0) + cost, // Substitution
|
|
34
40
|
)
|
|
35
41
|
}
|
|
36
42
|
}
|
|
37
43
|
|
|
38
|
-
return dp[m][n]
|
|
44
|
+
return dp[m]?.[n] ?? 0
|
|
39
45
|
}
|