* 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:
@@ -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),
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user