mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-26 13:19:34 +07:00
🐛 fix: fix skill search not found (#12432)
* fix skill issue * fix skills * fix skills search query
This commit is contained in:
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
159
packages/builtin-tool-skills/src/ExecutionRuntime/index.test.ts
Normal file
159
packages/builtin-tool-skills/src/ExecutionRuntime/index.test.ts
Normal 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.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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');
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
7
packages/builtin-tool-skills/vitest.config.mts
Normal file
7
packages/builtin-tool-skills/vitest.config.mts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
environment: 'node',
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user