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
This commit is contained in:
ChuwuYo
2026-03-08 16:41:45 +08:00
committed by GitHub
parent 7cfe2aca99
commit 89c11fd683
5 changed files with 119 additions and 69 deletions
+102
View File
@@ -0,0 +1,102 @@
import { pinyin } from "pinyin-pro";
const HAN_IDEOGRAPH_RE = /\p{Unified_Ideograph}/u;
export const normalizeStr = (s) => (s ?? "").toString().toLowerCase().trim();
const normalizeLooseFromNormalized = (normalized) =>
normalized.replace(/[\s_-]+/g, "").replace(/[()()【】\[\]{}·•]+/g, "");
export const normalizeLoose = (s) =>
normalizeLooseFromNormalized(normalizeStr(s));
const memoizeStringFn = (fn) => {
const cache = new Map();
return (raw) => {
const key = (raw ?? "").toString();
if (cache.has(key)) {
return cache.get(key);
}
const value = fn(key);
cache.set(key, value);
return value;
};
};
const getNormalizedText = memoizeStringFn(normalizeStr);
const getLooseText = memoizeStringFn((text) =>
normalizeLooseFromNormalized(getNormalizedText(text)),
);
export const toPinyinText = memoizeStringFn((text) =>
pinyin(text, { toneType: "none" })
.toLowerCase()
.replace(/\s+/g, ""),
);
export const toInitials = memoizeStringFn((text) =>
pinyin(text, { pattern: "first", toneType: "none" })
.toLowerCase()
.replace(/\s+/g, ""),
);
export const buildSearchQuery = (raw) => {
const norm = getNormalizedText(raw);
if (!norm) return null;
return {
norm,
loose: getLooseText(raw),
};
};
export const matchesText = (value, query) => {
if (value == null || !query?.norm) return false;
const text = String(value);
const normalizedValue = getNormalizedText(text);
const looseValue = query.loose ? getLooseText(text) : null;
if (normalizedValue.includes(query.norm)) return true;
if (query.loose && looseValue?.includes(query.loose)) return true;
if (!HAN_IDEOGRAPH_RE.test(text)) return false;
const pinyinValue = toPinyinText(text);
if (pinyinValue.includes(query.norm)) return true;
const initialsValue = toInitials(text);
if (initialsValue.includes(query.norm)) return true;
return false;
};
export const getPluginSearchFields = (plugin) => {
const supportPlatforms = Array.isArray(plugin?.support_platforms)
? plugin.support_platforms.join(" ")
: "";
const tags = Array.isArray(plugin?.tags) ? plugin.tags.join(" ") : "";
return [
plugin?.name,
plugin?.trimmedName,
plugin?.display_name,
plugin?.desc,
plugin?.author,
plugin?.repo,
plugin?.version,
plugin?.astrbot_version,
supportPlatforms,
tags,
];
};
export const matchesPluginSearch = (plugin, query) => {
if (!query) return true;
return getPluginSearchFields(plugin).some((candidate) =>
matchesText(candidate, query),
);
};
-1
View File
@@ -84,7 +84,6 @@ const {
normalizeStr,
toPinyinText,
toInitials,
marketCustomFilter,
plugin_handler_info_headers,
pluginHeaders,
filteredExtensions,
@@ -81,7 +81,6 @@ const {
normalizeStr,
toPinyinText,
toInitials,
marketCustomFilter,
plugin_handler_info_headers,
pluginHeaders,
filteredExtensions,
@@ -82,7 +82,6 @@ const {
normalizeStr,
toPinyinText,
toInitials,
marketCustomFilter,
plugin_handler_info_headers,
pluginHeaders,
filteredExtensions,
@@ -1,9 +1,15 @@
import axios from "axios";
import { pinyin } from "pinyin-pro";
import { useCommonStore } from "@/stores/common";
import { useI18n, useModuleI18n } from "@/i18n/composables";
import { getPlatformDisplayName } from "@/utils/platformUtils";
import { resolveErrorMessage } from "@/utils/errorUtils";
import {
buildSearchQuery,
matchesPluginSearch,
normalizeStr,
toInitials,
toPinyinText,
} from "@/utils/pluginSearch";
import { ref, computed, onMounted, onUnmounted, reactive, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { useDisplay } from "vuetify";
@@ -240,37 +246,6 @@ export const useExtensionPage = () => {
});
// 插件市场拼音搜索
const normalizeStr = (s) => (s ?? "").toString().toLowerCase().trim();
const toPinyinText = (s) =>
pinyin(s ?? "", { toneType: "none" })
.toLowerCase()
.replace(/\s+/g, "");
const toInitials = (s) =>
pinyin(s ?? "", { pattern: "first", toneType: "none" })
.toLowerCase()
.replace(/\s+/g, "");
const marketCustomFilter = (value, query, item) => {
const q = normalizeStr(query);
if (!q) return true;
const candidates = new Set();
if (value != null) candidates.add(String(value));
if (item?.name) candidates.add(String(item.name));
if (item?.trimmedName) candidates.add(String(item.trimmedName));
if (item?.display_name) candidates.add(String(item.display_name));
if (item?.desc) candidates.add(String(item.desc));
if (item?.author) candidates.add(String(item.author));
for (const v of candidates) {
const nv = normalizeStr(v);
if (nv.includes(q)) return true;
const pv = toPinyinText(v);
if (pv.includes(q)) return true;
const iv = toInitials(v);
if (iv.includes(q)) return true;
}
return false;
};
const plugin_handler_info_headers = computed(() => [
{ title: tm("table.headers.eventType"), key: "event_type_h" },
@@ -347,47 +322,24 @@ export const useExtensionPage = () => {
// 通过搜索过滤插件
const filteredPlugins = computed(() => {
const plugins = filteredExtensions.value;
let filtered = plugins;
if (pluginSearch.value) {
const search = pluginSearch.value.toLowerCase();
filtered = plugins.filter((plugin) => {
const pluginName = (plugin.name ?? "").toLowerCase();
const pluginDesc = (plugin.desc ?? "").toLowerCase();
const pluginAuthor = (plugin.author ?? "").toLowerCase();
const supportPlatforms = Array.isArray(plugin.support_platforms)
? plugin.support_platforms.join(" ").toLowerCase()
: "";
const astrbotVersion = (plugin.astrbot_version ?? "").toLowerCase();
return (
pluginName.includes(search) ||
pluginDesc.includes(search) ||
pluginAuthor.includes(search) ||
supportPlatforms.includes(search) ||
astrbotVersion.includes(search)
);
});
}
const query = buildSearchQuery(pluginSearch.value);
const filtered = query
? plugins.filter((plugin) => matchesPluginSearch(plugin, query))
: plugins;
return sortPluginsByName([...filtered]);
});
// 过滤后的插件市场数据(带搜索)
const filteredMarketPlugins = computed(() => {
if (!debouncedMarketSearch.value) {
const query = buildSearchQuery(debouncedMarketSearch.value);
if (!query) {
return pluginMarketData.value;
}
const search = debouncedMarketSearch.value.toLowerCase();
return pluginMarketData.value.filter((plugin) => {
// 使用自定义过滤器
return (
marketCustomFilter(plugin.name, search, plugin) ||
marketCustomFilter(plugin.desc, search, plugin) ||
marketCustomFilter(plugin.author, search, plugin)
);
});
return pluginMarketData.value.filter((plugin) =>
matchesPluginSearch(plugin, query),
);
});
// 所有插件列表,推荐插件排在前面
@@ -1563,7 +1515,6 @@ export const useExtensionPage = () => {
normalizeStr,
toPinyinText,
toInitials,
marketCustomFilter,
plugin_handler_info_headers,
pluginHeaders,
filteredExtensions,