feat(favorite): optimize list caching and expand MCP type support

Add 30s in-memory cache for favorite item list to reduce API calls

Expand supported MCP server types beyond local/remote to stdio/sse/ws/sdk and more

Change single MCP server conversion to use stdio type with separate command/args/env fields

Reduce page size from 100 to 20 for faster initial load

Switch parallel remote fetching to sequential to avoid rate limiting

Signed-off-by: 林凯90331 <90331@sangfor.com>

Co-authored-by: CoStrict <zgsm@sangfor.com.cn>
This commit is contained in:
林凯90331 2026-05-14 18:56:44 +08:00
parent fd509086f3
commit fd36738e9e

View File

@ -8,8 +8,14 @@ import { clearSkillCaches } from '../../skills/loadSkillsDir.js'
import { parseFrontmatter } from '../../utils/frontmatterParser.js'
import type { McpServerConfig } from '../../services/mcp/types.js'
const FAVORITE_PAGE_SIZE = 100
const FAVORITE_PAGE_SIZE = 20
const FAVORITE_MAX_PAGES = 20
const FAVORITE_LIST_CACHE_TTL_MS = 30000
let _listFavoriteItemsCache: {
promise: Promise<FavoriteItemWithStatus[]>
expiry: number
} | null = null
export type FavoriteItemType = 'skill' | 'agent' | 'command' | 'mcp'
@ -395,8 +401,19 @@ async function ensureInstalled(slugOrId: string): Promise<FavoriteStateRecord> {
}
}
const SUPPORTED_MCP_TYPES = [
'stdio',
'sse',
'http',
'ws',
'sdk',
'claudeai-proxy',
'sse-ide',
'ws-ide',
]
function convertMcpConfig(config: Record<string, unknown>): Record<string, unknown> | undefined {
if (typeof config.type === 'string' && (config.type === 'local' || config.type === 'remote')) {
if (typeof config.type === 'string' && SUPPORTED_MCP_TYPES.includes(config.type)) {
return config
}
if (config.mcpServers && typeof config.mcpServers === 'object' && !Array.isArray(config.mcpServers)) {
@ -430,11 +447,14 @@ function convertSingleMcpServer(server: Record<string, unknown>): Record<string,
}
if (cmdArray.length === 0) return undefined
const result: Record<string, unknown> = {
type: 'local',
command: cmdArray,
type: 'stdio',
command: cmdArray[0],
}
if (cmdArray.length > 1) {
result.args = cmdArray.slice(1)
}
if (typeof server.environment === 'object' && server.environment !== null) {
result.environment = server.environment
result.env = server.environment
}
return result
}
@ -572,18 +592,29 @@ async function readItemForConfig(installed: FavoriteStateRecord): Promise<Favori
}
export async function listFavoriteItems(type?: FavoriteItemType): Promise<FavoriteItemWithStatus[]> {
const cacheKey = type ?? 'all'
const now = Date.now()
if (
_listFavoriteItemsCache &&
_listFavoriteItemsCache.expiry > now &&
cacheKey === 'all'
) {
return _listFavoriteItemsCache.promise
}
const storeTypes = type ? [LOCAL_TO_STORE_TYPE[type]] : Object.values(LOCAL_TO_STORE_TYPE)
const errors: Error[] = []
const candidatePages = await Promise.all(
[...new Set(storeTypes)].map(async (st) =>
listRemoteCandidates(st, { favorited: 'true' }).catch((error) => {
console.warn('failed to fetch remote favorite candidates', { type: st, error })
if (error instanceof Error) errors.push(error)
return []
}),
),
)
const candidates = candidatePages.flat()
const candidates: Array<{ id: string } & Record<string, unknown>> = []
for (const st of [...new Set(storeTypes)]) {
try {
const page = await listRemoteCandidates(st, { favorited: 'true' })
candidates.push(...page)
} catch (error) {
console.warn('failed to fetch remote favorite candidates', { type: st, error })
if (error instanceof Error) errors.push(error)
}
}
// If all remote fetches failed, propagate the first error so the UI can show it
if (candidates.length === 0 && errors.length > 0) {
@ -604,6 +635,7 @@ export async function listFavoriteItems(type?: FavoriteItemType): Promise<Favori
for (const candidate of candidates) {
const item = parseFavoriteListItem(candidate)
if (!item) continue
if (!item.favorited) continue
if (seen.has(item.slug)) continue
seen.add(item.slug)
const local = state.items[item.slug]
@ -632,7 +664,16 @@ export async function listFavoriteItems(type?: FavoriteItemType): Promise<Favori
}
}
return result.sort((a, b) => a.name.localeCompare(b.name))
const sorted = result.sort((a, b) => a.name.localeCompare(b.name))
if (!type) {
_listFavoriteItemsCache = {
promise: Promise.resolve(sorted),
expiry: Date.now() + FAVORITE_LIST_CACHE_TTL_MS,
}
}
return sorted
}
export async function viewFavoriteItem(slugOrId: string): Promise<FavoriteItemWithStatus> {