Skip to content

Commit 89c11fd

Browse files
authored
fix(extension): support searching installed plugins by display name (#5806) (#5811)
* fix(extension): support searching installed plugins by display name * fix: unify plugin search matching across installed and market tabs * refactor(extension): optimize plugin search matcher and remove redundant checks * refactor(extension-page): centralize search query normalization and text matching logic - Extract `buildSearchQuery` to create normalized query objects from raw input - Extract `matchesText` as a reusable text matching helper for normalized/loose/pinyin/initials matching - Remove unused `marketCustomFilter` to eliminate dead code - Simplify `matchesPluginSearch` to accept query object instead of pre-normalized string - Replace Set with Array for candidates to simplify control flow - Avoid redundant normalization by having callers pass raw strings to `buildSearchQuery` * refactor: remove unused marketCustomFilter from extension page components - Remove marketCustomFilter from destructuring in ExtensionPage.vue, InstalledPluginsTab.vue, and MarketPluginsTab.vue * refactor(extension): extract plugin search utilities into shared module - Create pluginSearch.js to centralize plugin search helpers - Move `normalizeStr`, `normalizeLoose`, `toPinyinText`, and `toInitials` into the shared module - Add `buildSearchQuery`, `matchesText`, and `matchesPluginSearch` for reusable search matching - Refactor useExtensionPage.js to consume the shared utilities - Simplify plugin search logic by consolidating normalization and matching in one place * refactor(extension): add caching to pinyin utilities and extract search fields helper - Add Map-based caching for `toPinyinText` and `toInitials` to avoid redundant pinyin computation - Extract `getPluginSearchFields` function to retrieve plugin fields for searching - Improve plugin search performance with caching and better code organization * perf(extension): add bounded caching for plugin search - cap normalization and pinyin caches with `MAX_SEARCH_CACHE_SIZE` - add `setCacheValue()` for oldest-entry eviction - cache normalized and loose text values to avoid repeated string processing - skip pinyin matching for non-CJK text using Unicode `\p{Unified_Ideograph}` property - improve search performance while keeping memory usage bounded * refactor(extension): extract memoizeLRU helper for cache management - Create `memoizeLRU` higher-order function to generate LRU-cached functions - Replace manual cache implementation with `memoizeLRU` for cleaner code - Optimize `matchesText` to lazily compute looseValue only when needed - Simplify caching logic while maintaining bounded cache size * refactor(extension): simplify memoization and remove LRU logic - Rename `memoizeLRU` to `memoizeStringFn` and remove bounded cache size - Simplify cache hit logic for cleaner code - Remove `MAX_SEARCH_CACHE_SIZE` constant as it's no longer needed
1 parent 7cfe2ac commit 89c11fd

5 files changed

Lines changed: 119 additions & 69 deletions

File tree

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { pinyin } from "pinyin-pro";
2+
3+
const HAN_IDEOGRAPH_RE = /\p{Unified_Ideograph}/u;
4+
5+
export const normalizeStr = (s) => (s ?? "").toString().toLowerCase().trim();
6+
7+
const normalizeLooseFromNormalized = (normalized) =>
8+
normalized.replace(/[\s_-]+/g, "").replace(/[()\[\]{}·]+/g, "");
9+
10+
export const normalizeLoose = (s) =>
11+
normalizeLooseFromNormalized(normalizeStr(s));
12+
13+
const memoizeStringFn = (fn) => {
14+
const cache = new Map();
15+
16+
return (raw) => {
17+
const key = (raw ?? "").toString();
18+
if (cache.has(key)) {
19+
return cache.get(key);
20+
}
21+
22+
const value = fn(key);
23+
cache.set(key, value);
24+
return value;
25+
};
26+
};
27+
28+
const getNormalizedText = memoizeStringFn(normalizeStr);
29+
30+
const getLooseText = memoizeStringFn((text) =>
31+
normalizeLooseFromNormalized(getNormalizedText(text)),
32+
);
33+
34+
export const toPinyinText = memoizeStringFn((text) =>
35+
pinyin(text, { toneType: "none" })
36+
.toLowerCase()
37+
.replace(/\s+/g, ""),
38+
);
39+
40+
export const toInitials = memoizeStringFn((text) =>
41+
pinyin(text, { pattern: "first", toneType: "none" })
42+
.toLowerCase()
43+
.replace(/\s+/g, ""),
44+
);
45+
46+
export const buildSearchQuery = (raw) => {
47+
const norm = getNormalizedText(raw);
48+
if (!norm) return null;
49+
return {
50+
norm,
51+
loose: getLooseText(raw),
52+
};
53+
};
54+
55+
export const matchesText = (value, query) => {
56+
if (value == null || !query?.norm) return false;
57+
const text = String(value);
58+
59+
const normalizedValue = getNormalizedText(text);
60+
const looseValue = query.loose ? getLooseText(text) : null;
61+
62+
if (normalizedValue.includes(query.norm)) return true;
63+
if (query.loose && looseValue?.includes(query.loose)) return true;
64+
65+
if (!HAN_IDEOGRAPH_RE.test(text)) return false;
66+
67+
const pinyinValue = toPinyinText(text);
68+
if (pinyinValue.includes(query.norm)) return true;
69+
70+
const initialsValue = toInitials(text);
71+
if (initialsValue.includes(query.norm)) return true;
72+
73+
return false;
74+
};
75+
76+
export const getPluginSearchFields = (plugin) => {
77+
const supportPlatforms = Array.isArray(plugin?.support_platforms)
78+
? plugin.support_platforms.join(" ")
79+
: "";
80+
const tags = Array.isArray(plugin?.tags) ? plugin.tags.join(" ") : "";
81+
82+
return [
83+
plugin?.name,
84+
plugin?.trimmedName,
85+
plugin?.display_name,
86+
plugin?.desc,
87+
plugin?.author,
88+
plugin?.repo,
89+
plugin?.version,
90+
plugin?.astrbot_version,
91+
supportPlatforms,
92+
tags,
93+
];
94+
};
95+
96+
export const matchesPluginSearch = (plugin, query) => {
97+
if (!query) return true;
98+
99+
return getPluginSearchFields(plugin).some((candidate) =>
100+
matchesText(candidate, query),
101+
);
102+
};

dashboard/src/views/ExtensionPage.vue

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@ const {
8484
normalizeStr,
8585
toPinyinText,
8686
toInitials,
87-
marketCustomFilter,
8887
plugin_handler_info_headers,
8988
pluginHeaders,
9089
filteredExtensions,

dashboard/src/views/extension/InstalledPluginsTab.vue

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,6 @@ const {
8181
normalizeStr,
8282
toPinyinText,
8383
toInitials,
84-
marketCustomFilter,
8584
plugin_handler_info_headers,
8685
pluginHeaders,
8786
filteredExtensions,

dashboard/src/views/extension/MarketPluginsTab.vue

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ const {
8282
normalizeStr,
8383
toPinyinText,
8484
toInitials,
85-
marketCustomFilter,
8685
plugin_handler_info_headers,
8786
pluginHeaders,
8887
filteredExtensions,

dashboard/src/views/extension/useExtensionPage.js

Lines changed: 17 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
import axios from "axios";
2-
import { pinyin } from "pinyin-pro";
32
import { useCommonStore } from "@/stores/common";
43
import { useI18n, useModuleI18n } from "@/i18n/composables";
54
import { getPlatformDisplayName } from "@/utils/platformUtils";
65
import { resolveErrorMessage } from "@/utils/errorUtils";
6+
import {
7+
buildSearchQuery,
8+
matchesPluginSearch,
9+
normalizeStr,
10+
toInitials,
11+
toPinyinText,
12+
} from "@/utils/pluginSearch";
713
import { ref, computed, onMounted, onUnmounted, reactive, watch } from "vue";
814
import { useRoute, useRouter } from "vue-router";
915
import { useDisplay } from "vuetify";
@@ -240,37 +246,6 @@ export const useExtensionPage = () => {
240246
});
241247

242248
// 插件市场拼音搜索
243-
const normalizeStr = (s) => (s ?? "").toString().toLowerCase().trim();
244-
const toPinyinText = (s) =>
245-
pinyin(s ?? "", { toneType: "none" })
246-
.toLowerCase()
247-
.replace(/\s+/g, "");
248-
const toInitials = (s) =>
249-
pinyin(s ?? "", { pattern: "first", toneType: "none" })
250-
.toLowerCase()
251-
.replace(/\s+/g, "");
252-
const marketCustomFilter = (value, query, item) => {
253-
const q = normalizeStr(query);
254-
if (!q) return true;
255-
256-
const candidates = new Set();
257-
if (value != null) candidates.add(String(value));
258-
if (item?.name) candidates.add(String(item.name));
259-
if (item?.trimmedName) candidates.add(String(item.trimmedName));
260-
if (item?.display_name) candidates.add(String(item.display_name));
261-
if (item?.desc) candidates.add(String(item.desc));
262-
if (item?.author) candidates.add(String(item.author));
263-
264-
for (const v of candidates) {
265-
const nv = normalizeStr(v);
266-
if (nv.includes(q)) return true;
267-
const pv = toPinyinText(v);
268-
if (pv.includes(q)) return true;
269-
const iv = toInitials(v);
270-
if (iv.includes(q)) return true;
271-
}
272-
return false;
273-
};
274249

275250
const plugin_handler_info_headers = computed(() => [
276251
{ title: tm("table.headers.eventType"), key: "event_type_h" },
@@ -347,47 +322,24 @@ export const useExtensionPage = () => {
347322
// 通过搜索过滤插件
348323
const filteredPlugins = computed(() => {
349324
const plugins = filteredExtensions.value;
350-
let filtered = plugins;
351-
352-
if (pluginSearch.value) {
353-
const search = pluginSearch.value.toLowerCase();
354-
filtered = plugins.filter((plugin) => {
355-
const pluginName = (plugin.name ?? "").toLowerCase();
356-
const pluginDesc = (plugin.desc ?? "").toLowerCase();
357-
const pluginAuthor = (plugin.author ?? "").toLowerCase();
358-
const supportPlatforms = Array.isArray(plugin.support_platforms)
359-
? plugin.support_platforms.join(" ").toLowerCase()
360-
: "";
361-
const astrbotVersion = (plugin.astrbot_version ?? "").toLowerCase();
362-
363-
return (
364-
pluginName.includes(search) ||
365-
pluginDesc.includes(search) ||
366-
pluginAuthor.includes(search) ||
367-
supportPlatforms.includes(search) ||
368-
astrbotVersion.includes(search)
369-
);
370-
});
371-
}
325+
const query = buildSearchQuery(pluginSearch.value);
326+
const filtered = query
327+
? plugins.filter((plugin) => matchesPluginSearch(plugin, query))
328+
: plugins;
372329

373330
return sortPluginsByName([...filtered]);
374331
});
375332

376333
// 过滤后的插件市场数据(带搜索)
377334
const filteredMarketPlugins = computed(() => {
378-
if (!debouncedMarketSearch.value) {
335+
const query = buildSearchQuery(debouncedMarketSearch.value);
336+
if (!query) {
379337
return pluginMarketData.value;
380338
}
381-
382-
const search = debouncedMarketSearch.value.toLowerCase();
383-
return pluginMarketData.value.filter((plugin) => {
384-
// 使用自定义过滤器
385-
return (
386-
marketCustomFilter(plugin.name, search, plugin) ||
387-
marketCustomFilter(plugin.desc, search, plugin) ||
388-
marketCustomFilter(plugin.author, search, plugin)
389-
);
390-
});
339+
340+
return pluginMarketData.value.filter((plugin) =>
341+
matchesPluginSearch(plugin, query),
342+
);
391343
});
392344

393345
// 所有插件列表,推荐插件排在前面
@@ -1563,7 +1515,6 @@ export const useExtensionPage = () => {
15631515
normalizeStr,
15641516
toPinyinText,
15651517
toInitials,
1566-
marketCustomFilter,
15671518
plugin_handler_info_headers,
15681519
pluginHeaders,
15691520
filteredExtensions,

0 commit comments

Comments
 (0)