mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
fix: make skill marketplace search functional
This commit is contained in:
parent
977cd6e87a
commit
790b0925ec
@ -117,7 +117,7 @@ export function SkillCenter() {
|
||||
|
||||
const handleClearSearch = () => {
|
||||
setQuery('')
|
||||
void fetchItems()
|
||||
void fetchItems({ query: '' })
|
||||
}
|
||||
|
||||
const handleOpenInstalled = (detail: SkillMarketDetail) => {
|
||||
|
||||
@ -89,6 +89,25 @@ describe('skillMarketStore', () => {
|
||||
expect(useSkillMarketStore.getState().error).toBeNull()
|
||||
})
|
||||
|
||||
it('allows callers to override the current query for an immediate refresh', async () => {
|
||||
mockedSkillMarketApi.list.mockResolvedValue({
|
||||
items: [],
|
||||
nextCursor: null,
|
||||
source: 'clawhub',
|
||||
sourceStatus: 'ok',
|
||||
})
|
||||
useSkillMarketStore.setState({ query: 'weather' })
|
||||
|
||||
await useSkillMarketStore.getState().fetchItems({ query: '' })
|
||||
|
||||
expect(mockedSkillMarketApi.list).toHaveBeenCalledWith({
|
||||
source: 'auto',
|
||||
sort: 'downloads',
|
||||
q: undefined,
|
||||
limit: 100,
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps the resolved fallback source status from the marketplace API', async () => {
|
||||
const item = makeItem({ source: 'skillhub', sourceMode: 'fallback' })
|
||||
mockedSkillMarketApi.list.mockResolvedValue({
|
||||
|
||||
@ -27,7 +27,7 @@ type SkillMarketStore = {
|
||||
setSource: (source: SkillMarketListSource) => void
|
||||
setSort: (sort: SkillMarketSort) => void
|
||||
setQuery: (query: string) => void
|
||||
fetchItems: () => Promise<void>
|
||||
fetchItems: (options?: { query?: string }) => Promise<void>
|
||||
fetchMore: () => Promise<void>
|
||||
fetchDetail: (source: SkillMarketSource, slug: string) => Promise<void>
|
||||
installSelected: () => Promise<void>
|
||||
@ -63,15 +63,16 @@ export const useSkillMarketStore = create<SkillMarketStore>((set, get) => ({
|
||||
},
|
||||
setQuery: (query) => set({ query }),
|
||||
|
||||
fetchItems: async () => {
|
||||
fetchItems: async (options) => {
|
||||
const { source, sort, query } = get()
|
||||
const requestedQuery = options?.query ?? query
|
||||
detailRequestSeq += 1
|
||||
set({ isLoading: true, isLoadingMore: false, isDetailLoading: false, selectedDetail: null, error: null })
|
||||
try {
|
||||
const result = await skillMarketApi.list({
|
||||
source,
|
||||
sort,
|
||||
q: query.trim() || undefined,
|
||||
q: requestedQuery.trim() || undefined,
|
||||
limit: MARKET_PAGE_LIMIT,
|
||||
})
|
||||
set({
|
||||
|
||||
@ -510,6 +510,42 @@ describe('skill market service source selection', () => {
|
||||
expect(fetchCalls[0]).toContain('limit=100')
|
||||
})
|
||||
|
||||
it('filters catalog results locally when the upstream source ignores query text', async () => {
|
||||
const service = createSkillMarketService({
|
||||
fetchImpl: async () => Response.json({
|
||||
items: [
|
||||
{
|
||||
slug: 'skill-vetter',
|
||||
displayName: 'Skill Vetter',
|
||||
summary: 'Security-first skill vetting for AI agents.',
|
||||
stats: { downloads: 260911, installs: 11988, stars: 1248 },
|
||||
tags: { latest: '1.0.0' },
|
||||
latestVersion: { version: '1.0.0', license: 'Apache-2.0' },
|
||||
},
|
||||
{
|
||||
slug: 'weather',
|
||||
displayName: 'Weather',
|
||||
summary: 'Get current weather and forecasts.',
|
||||
topics: ['forecast'],
|
||||
stats: { downloads: 162000, installs: 7000, stars: 420 },
|
||||
tags: { latest: '1.0.0' },
|
||||
latestVersion: { version: '1.0.0', license: 'MIT' },
|
||||
},
|
||||
],
|
||||
nextCursor: 'next-page',
|
||||
}),
|
||||
})
|
||||
|
||||
const result = await service.listSkills({ source: 'clawhub', query: 'weather forecast' })
|
||||
|
||||
expect(result.items).toHaveLength(1)
|
||||
expect(result.items[0]).toMatchObject({
|
||||
slug: 'weather',
|
||||
displayName: 'Weather',
|
||||
})
|
||||
expect(result.nextCursor).toBe('next-page')
|
||||
})
|
||||
|
||||
it('falls back to SkillHub in auto mode when ClawHub fails and marks installed skills', async () => {
|
||||
const fetchCalls: string[] = []
|
||||
const service = createSkillMarketService({
|
||||
|
||||
@ -150,7 +150,7 @@ export function createSkillMarketService(options: SkillMarketServiceOptions = {}
|
||||
try {
|
||||
const result = await cachedCatalog(cacheKey, async () => {
|
||||
const payload = await requestJson(fetchImpl, url, 'ClawHub')
|
||||
return normalizeClawHubList(payload)
|
||||
return filterCatalogByQuery(normalizeClawHubList(payload), params.query)
|
||||
})
|
||||
failureCache.delete(cacheKey)
|
||||
return result
|
||||
@ -170,7 +170,7 @@ export function createSkillMarketService(options: SkillMarketServiceOptions = {}
|
||||
return cachedCatalog(catalogCacheKey('skillhub', url), async () => {
|
||||
const payload = await requestJson(fetchImpl, url, 'SkillHub')
|
||||
return {
|
||||
...normalizeSkillHubList(payload),
|
||||
...filterCatalogByQuery(normalizeSkillHubList(payload), params.query),
|
||||
sourceStatus: 'ok',
|
||||
}
|
||||
})
|
||||
@ -414,6 +414,38 @@ function catalogCacheKey(source: 'clawhub' | 'skillhub', url: URL): string {
|
||||
return `${source}:${url.toString()}`
|
||||
}
|
||||
|
||||
function filterCatalogByQuery(result: SkillMarketListResult, query?: string): SkillMarketListResult {
|
||||
const terms = normalizeSearchTerms(query)
|
||||
if (terms.length === 0) {
|
||||
return result
|
||||
}
|
||||
|
||||
return {
|
||||
...result,
|
||||
items: result.items.filter((item) => {
|
||||
const haystack = [
|
||||
item.slug,
|
||||
item.displayName,
|
||||
item.summary,
|
||||
item.summaryZh,
|
||||
item.owner,
|
||||
item.category,
|
||||
item.license,
|
||||
...(item.tags ?? []),
|
||||
].filter(Boolean).join(' ').toLocaleLowerCase()
|
||||
return terms.every((term) => haystack.includes(term))
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeSearchTerms(query?: string): string[] {
|
||||
return (query ?? '')
|
||||
.trim()
|
||||
.toLocaleLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
async function requestJson(fetchImpl: FetchImpl, url: URL, sourceName: string): Promise<unknown> {
|
||||
let response: Response
|
||||
try {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user