🐛 fix: fix skill search not found (#12432)

* fix skill issue

* fix skills

* fix skills search query
This commit is contained in:
Arvin Xu
2026-02-23 00:28:10 +08:00
committed by GitHub
parent bdc901d1dc
commit ddce51eaba
10 changed files with 239 additions and 26 deletions

View File

@@ -201,8 +201,6 @@
"@lobechat/builtin-tool-web-browsing": "workspace:*", "@lobechat/builtin-tool-web-browsing": "workspace:*",
"@lobechat/business-config": "workspace:*", "@lobechat/business-config": "workspace:*",
"@lobechat/business-const": "workspace:*", "@lobechat/business-const": "workspace:*",
"@lobechat/eval-dataset-parser": "workspace:*",
"@lobechat/eval-rubric": "workspace:*",
"@lobechat/config": "workspace:*", "@lobechat/config": "workspace:*",
"@lobechat/const": "workspace:*", "@lobechat/const": "workspace:*",
"@lobechat/context-engine": "workspace:*", "@lobechat/context-engine": "workspace:*",
@@ -213,6 +211,8 @@
"@lobechat/editor-runtime": "workspace:*", "@lobechat/editor-runtime": "workspace:*",
"@lobechat/electron-client-ipc": "workspace:*", "@lobechat/electron-client-ipc": "workspace:*",
"@lobechat/electron-server-ipc": "workspace:*", "@lobechat/electron-server-ipc": "workspace:*",
"@lobechat/eval-dataset-parser": "workspace:*",
"@lobechat/eval-rubric": "workspace:*",
"@lobechat/fetch-sse": "workspace:*", "@lobechat/fetch-sse": "workspace:*",
"@lobechat/file-loaders": "workspace:*", "@lobechat/file-loaders": "workspace:*",
"@lobechat/memory-user-memory": "workspace:*", "@lobechat/memory-user-memory": "workspace:*",
@@ -230,7 +230,7 @@
"@lobehub/desktop-ipc-typings": "workspace:*", "@lobehub/desktop-ipc-typings": "workspace:*",
"@lobehub/editor": "^3.16.1", "@lobehub/editor": "^3.16.1",
"@lobehub/icons": "^4.1.0", "@lobehub/icons": "^4.1.0",
"@lobehub/market-sdk": "0.29.3", "@lobehub/market-sdk": "^0.30.3",
"@lobehub/tts": "^4.0.2", "@lobehub/tts": "^4.0.2",
"@lobehub/ui": "^4.38.1", "@lobehub/ui": "^4.38.1",
"@modelcontextprotocol/sdk": "^1.25.3", "@modelcontextprotocol/sdk": "^1.25.3",
@@ -491,4 +491,4 @@
"drizzle-orm": "0.44.7" "drizzle-orm": "0.44.7"
} }
} }
} }

View File

@@ -0,0 +1,159 @@
import { describe, expect, it, vi } from 'vitest';
import type { CommandResult } from '../types';
import { SkillsExecutionRuntime, type SkillRuntimeService } from './index';
const createMockService = (overrides?: Partial<SkillRuntimeService>): SkillRuntimeService => ({
findAll: vi.fn().mockResolvedValue({ data: [], total: 0 }),
findById: vi.fn().mockResolvedValue(undefined),
findByName: vi.fn().mockResolvedValue(undefined),
importFromGitHub: vi.fn(),
importFromUrl: vi.fn(),
importFromZipUrl: vi.fn(),
readResource: vi.fn(),
...overrides,
});
describe('SkillsExecutionRuntime', () => {
describe('execScript', () => {
const args = { command: 'echo hello', description: 'test command' };
describe('via execScript service method', () => {
it('should return success: true when script succeeds', async () => {
const service = createMockService({
execScript: vi.fn().mockResolvedValue({
exitCode: 0,
output: 'hello',
success: true,
} satisfies CommandResult),
});
const runtime = new SkillsExecutionRuntime({ service });
const result = await runtime.execScript(args);
expect(result.success).toBe(true);
expect(result.content).toBe('hello');
expect(result.state).toEqual({ command: 'echo hello', exitCode: 0, success: true });
});
it('should return success: false when script fails with non-zero exit code', async () => {
const service = createMockService({
execScript: vi.fn().mockResolvedValue({
exitCode: 1,
output: '',
stderr: 'command not found',
success: false,
} satisfies CommandResult),
});
const runtime = new SkillsExecutionRuntime({ service });
const result = await runtime.execScript(args);
expect(result.success).toBe(false);
expect(result.content).toBe('command not found');
expect(result.state).toEqual({ command: 'echo hello', exitCode: 1, success: false });
});
it('should combine output and stderr', async () => {
const service = createMockService({
execScript: vi.fn().mockResolvedValue({
exitCode: 0,
output: 'stdout line',
stderr: 'stderr line',
success: true,
} satisfies CommandResult),
});
const runtime = new SkillsExecutionRuntime({ service });
const result = await runtime.execScript(args);
expect(result.content).toBe('stdout line\nstderr line');
});
it('should return "(no output)" when output is empty', async () => {
const service = createMockService({
execScript: vi.fn().mockResolvedValue({
exitCode: 0,
output: '',
success: true,
} satisfies CommandResult),
});
const runtime = new SkillsExecutionRuntime({ service });
const result = await runtime.execScript(args);
expect(result.content).toBe('(no output)');
});
it('should return success: false when execScript throws', async () => {
const service = createMockService({
execScript: vi.fn().mockRejectedValue(new Error('sandbox timeout')),
});
const runtime = new SkillsExecutionRuntime({ service });
const result = await runtime.execScript(args);
expect(result.success).toBe(false);
expect(result.content).toBe('Failed to execute command: sandbox timeout');
});
});
describe('via runCommand fallback', () => {
it('should return success: true when command succeeds', async () => {
const service = createMockService({
runCommand: vi.fn().mockResolvedValue({
exitCode: 0,
output: 'ok',
success: true,
} satisfies CommandResult),
});
const runtime = new SkillsExecutionRuntime({ service });
const result = await runtime.execScript(args);
expect(result.success).toBe(true);
expect(result.content).toBe('ok');
});
it('should return success: false when command fails with non-zero exit code', async () => {
const service = createMockService({
runCommand: vi.fn().mockResolvedValue({
exitCode: 127,
output: '',
stderr: 'not found',
success: false,
} satisfies CommandResult),
});
const runtime = new SkillsExecutionRuntime({ service });
const result = await runtime.execScript(args);
expect(result.success).toBe(false);
expect(result.content).toBe('not found');
expect(result.state).toEqual({ command: 'echo hello', exitCode: 127, success: false });
});
it('should return success: false when runCommand throws', async () => {
const service = createMockService({
runCommand: vi.fn().mockRejectedValue(new Error('connection lost')),
});
const runtime = new SkillsExecutionRuntime({ service });
const result = await runtime.execScript(args);
expect(result.success).toBe(false);
expect(result.content).toBe('Failed to execute command: connection lost');
});
it('should return success: false when neither execScript nor runCommand is available', async () => {
const service = createMockService();
const runtime = new SkillsExecutionRuntime({ service });
const result = await runtime.execScript(args);
expect(result.success).toBe(false);
expect(result.content).toBe('Command execution is not available in this environment.');
});
});
});
});

View File

@@ -137,7 +137,7 @@ export class SkillsExecutionRuntime {
exitCode: result.exitCode, exitCode: result.exitCode,
success: result.success, success: result.success,
}, },
success: true, success: result.success,
}; };
} catch (e) { } catch (e) {
return { return {
@@ -167,7 +167,7 @@ export class SkillsExecutionRuntime {
exitCode: result.exitCode, exitCode: result.exitCode,
success: result.success, success: result.success,
}, },
success: true, success: result.success,
}; };
} catch (e) { } catch (e) {
return { return {
@@ -322,8 +322,8 @@ export class SkillsExecutionRuntime {
if (result.items.length === 0) { if (result.items.length === 0) {
return { return {
content: args.search content: args.q
? `No skills found matching "${args.search}"` ? `No skills found matching "${args.q}"`
: 'No skills found in the market', : 'No skills found in the market',
state: result, state: result,
success: true, success: true,
@@ -334,7 +334,7 @@ export class SkillsExecutionRuntime {
const skillsList = result.items const skillsList = result.items
.map( .map(
(skill, index) => (skill, index) =>
`${index + 1}. **${skill.name}** (${skill.identifier})\n ${skill.description}${skill.summary ? `\n Summary: ${skill.summary}` : ''}${skill.repository ? `\n Repository: ${skill.repository}` : ''}${skill.downloadCount ? `\n Downloads: ${skill.downloadCount}` : ''}`, `${index + 1}. **${skill.name}** (${skill.identifier})\n ${skill.description}${skill.summary ? `\n Summary: ${skill.summary}` : ''}${skill.repository ? `\n Repository: ${skill.repository}` : ''}${skill.installCount ? `\n Installs: ${skill.installCount}` : ''}`,
) )
.join('\n\n'); .join('\n\n');

View File

@@ -1,4 +1,4 @@
import { BuiltinToolManifest } from '@lobechat/types'; import type { BuiltinToolManifest } from '@lobechat/types';
import { isDesktop } from './const'; import { isDesktop } from './const';
import { systemPrompt } from './systemRole'; import { systemPrompt } from './systemRole';
@@ -134,7 +134,7 @@ export const SkillsManifest: BuiltinToolManifest = {
}, },
{ {
description: description:
'Search for skills in the LobeHub Market. Use this to discover available skills that can extend Claude\'s capabilities. Search across skill names, descriptions, and summaries. Results can be filtered and sorted by various criteria (stars, downloads, etc).', "Search for skills in the LobeHub Market. Use this to discover available skills that can extend Claude's capabilities. Search across skill names, descriptions, and summaries. Results can be filtered and sorted by various criteria (stars, downloads, etc).",
name: SkillsApiName.searchSkill, name: SkillsApiName.searchSkill,
parameters: { parameters: {
properties: { properties: {
@@ -155,15 +155,24 @@ export const SkillsManifest: BuiltinToolManifest = {
description: 'Number of results per page. Default: 20.', description: 'Number of results per page. Default: 20.',
type: 'number', type: 'number',
}, },
search: { q: {
description: description:
'Search query to filter skills. Searches across skill name, description, and summary. Optional.', 'Search query to filter skills. Searches across skill name, description, and summary. Optional.',
type: 'string', type: 'string',
}, },
sort: { sort: {
description: description:
'Field to sort by. Options: createdAt (creation date), downloadCount (downloads), forks (GitHub forks), name (alphabetical), stars (GitHub stars), updatedAt (last update), watchers (GitHub watchers). Default: "updatedAt".', 'Field to sort by. Options: createdAt (creation date), installCount (installs), forks (GitHub forks), name (alphabetical), relevance (search relevance), stars (GitHub stars), updatedAt (last update), watchers (GitHub watchers). Default: "updatedAt".',
enum: ['createdAt', 'downloadCount', 'forks', 'name', 'stars', 'updatedAt', 'watchers'], enum: [
'createdAt',
'forks',
'installCount',
'name',
'relevance',
'stars',
'updatedAt',
'watchers',
],
type: 'string', type: 'string',
}, },
}, },

View File

@@ -131,19 +131,27 @@ export interface SearchSkillParams {
/** /**
* Search query (searches name, description, summary) * Search query (searches name, description, summary)
*/ */
search?: string; q?: string;
/** /**
* Sort field: createdAt | downloadCount | forks | name | stars | updatedAt | watchers * Sort field: createdAt | installCount | forks | name | relevance | stars | updatedAt | watchers
*/ */
sort?: 'createdAt' | 'downloadCount' | 'forks' | 'name' | 'stars' | 'updatedAt' | 'watchers'; sort?:
| 'createdAt'
| 'forks'
| 'installCount'
| 'name'
| 'relevance'
| 'stars'
| 'updatedAt'
| 'watchers';
} }
export interface MarketSkillItem { export interface MarketSkillItem {
category?: string; category?: string;
createdAt: string; createdAt: string;
description: string; description: string;
downloadCount: number;
identifier: string; identifier: string;
installCount: number;
name: string; name: string;
repository?: string; repository?: string;
sourceUrl?: string; sourceUrl?: string;

View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'node',
},
});

View File

@@ -5,7 +5,12 @@ const branchName = process.env.VERCEL_GIT_COMMIT_REF || '';
function shouldProceedBuild() { function shouldProceedBuild() {
// 如果是 lighthouse 分支或以 testgru 开头的分支,取消构建 // 如果是 lighthouse 分支或以 testgru 开头的分支,取消构建
if (branchName === 'lighthouse' || branchName.startsWith('gru/')) { if (
branchName === 'lighthouse' ||
['gru', 'automatic', 'reproduction'].some((item) =>
branchName.startsWith(`${item.toLowerCase()}/`),
)
) {
return false; return false;
} }

View File

@@ -31,9 +31,18 @@ export const skillRouter = router({
order: z.enum(['asc', 'desc']).optional(), order: z.enum(['asc', 'desc']).optional(),
page: z.number().optional(), page: z.number().optional(),
pageSize: z.number().optional(), pageSize: z.number().optional(),
search: z.string().optional(), q: z.string().optional(),
sort: z sort: z
.enum(['createdAt', 'downloadCount', 'forks', 'name', 'stars', 'updatedAt', 'watchers']) .enum([
'createdAt',
'forks',
'installCount',
'name',
'relevance',
'stars',
'updatedAt',
'watchers',
])
.optional(), .optional(),
}), }),
) )

View File

@@ -396,8 +396,16 @@ export class MarketService {
order?: 'asc' | 'desc'; order?: 'asc' | 'desc';
page?: number; page?: number;
pageSize?: number; pageSize?: number;
search?: string; q?: string;
sort?: 'createdAt' | 'downloadCount' | 'forks' | 'name' | 'stars' | 'updatedAt' | 'watchers'; sort?:
| 'createdAt'
| 'forks'
| 'installCount'
| 'name'
| 'relevance'
| 'stars'
| 'updatedAt'
| 'watchers';
}) { }) {
log('searchSkill: %O', params); log('searchSkill: %O', params);

View File

@@ -213,14 +213,22 @@ export class MarketApiService {
order?: 'asc' | 'desc'; order?: 'asc' | 'desc';
page?: number; page?: number;
pageSize?: number; pageSize?: number;
search?: string; q?: string;
sort?: 'createdAt' | 'downloadCount' | 'forks' | 'name' | 'stars' | 'updatedAt' | 'watchers'; sort?:
| 'createdAt'
| 'forks'
| 'installCount'
| 'name'
| 'relevance'
| 'stars'
| 'updatedAt'
| 'watchers';
}): Promise<{ }): Promise<{
items: Array<{ items: Array<{
category?: string; category?: string;
createdAt: string; createdAt: string;
description: string; description: string;
downloadCount: number; installCount: number;
identifier: string; identifier: string;
name: string; name: string;
repository?: string; repository?: string;