diff --git a/mateclaw-server/src/main/java/vip/mate/skill/controller/SkillController.java b/mateclaw-server/src/main/java/vip/mate/skill/controller/SkillController.java index 9cb24b4f..7d0c41fd 100644 --- a/mateclaw-server/src/main/java/vip/mate/skill/controller/SkillController.java +++ b/mateclaw-server/src/main/java/vip/mate/skill/controller/SkillController.java @@ -1,5 +1,6 @@ package vip.mate.skill.controller; +import com.baomidou.mybatisplus.core.metadata.IPage; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; @@ -34,10 +35,21 @@ public class SkillController { private final SkillWorkspaceManager workspaceManager; private final SkillSynthesisService synthesisService; - @Operation(summary = "获取技能列表") + @Operation(summary = "获取技能分页列表(RFC-042 §2.1)") @GetMapping - public R> list() { - return R.ok(skillService.listSkills()); + public R> list( + @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "20") int size, + @RequestParam(required = false) String keyword, + @RequestParam(required = false) String skillType, + @RequestParam(required = false) Boolean enabled) { + return R.ok(skillService.pageSkills(page, size, keyword, skillType, enabled)); + } + + @Operation(summary = "获取各类型技能计数(tab 徽章用)") + @GetMapping("/counts") + public R> counts() { + return R.ok(skillService.countByType()); } @Operation(summary = "获取已启用技能列表") diff --git a/mateclaw-server/src/main/java/vip/mate/skill/service/SkillService.java b/mateclaw-server/src/main/java/vip/mate/skill/service/SkillService.java index a903155c..ad8f51e6 100644 --- a/mateclaw-server/src/main/java/vip/mate/skill/service/SkillService.java +++ b/mateclaw-server/src/main/java/vip/mate/skill/service/SkillService.java @@ -1,6 +1,8 @@ package vip.mate.skill.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -12,6 +14,7 @@ import vip.mate.skill.workspace.SkillWorkspaceProperties; import java.nio.file.Files; import java.nio.file.Path; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -58,6 +61,53 @@ public class SkillService { .orderByDesc(SkillEntity::getCreateTime)); } + /** + * Paginated skill listing for the SkillMarket admin UI. + * + *

RFC-042 §2.1 — replaces the unbounded {@code /skills} list. Filters + * are all optional; empty or {@code null} means "no filter". Keyword + * searches name / description / tags with LIKE. + */ + public IPage pageSkills(int page, int size, String keyword, + String skillType, Boolean enabled) { + Page pageParam = new Page<>(Math.max(page, 1), Math.max(size, 1)); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + if (keyword != null && !keyword.isBlank()) { + String kw = keyword.trim(); + wrapper.and(w -> w + .like(SkillEntity::getName, kw) + .or().like(SkillEntity::getDescription, kw) + .or().like(SkillEntity::getTags, kw)); + } + if (skillType != null && !skillType.isBlank()) { + wrapper.eq(SkillEntity::getSkillType, skillType); + } + if (enabled != null) { + wrapper.eq(SkillEntity::getEnabled, enabled); + } + + wrapper.orderByDesc(SkillEntity::getBuiltin) + .orderByDesc(SkillEntity::getCreateTime); + + return skillMapper.selectPage(pageParam, wrapper); + } + + /** + * Aggregate skill counts per {@code skill_type}, plus an {@code all} + * rollup. Feeds the SkillMarket tab badges without pulling every row. + */ + public Map countByType() { + Map result = new LinkedHashMap<>(); + result.put("all", skillMapper.selectCount(null)); + for (String type : List.of("builtin", "mcp", "dynamic")) { + result.put(type, skillMapper.selectCount( + new LambdaQueryWrapper() + .eq(SkillEntity::getSkillType, type))); + } + return result; + } + /** * 获取已启用的技能列表(Agent 运行时使用) *

diff --git a/mateclaw-ui/src/api/index.ts b/mateclaw-ui/src/api/index.ts index 56426cfb..e9b78996 100644 --- a/mateclaw-ui/src/api/index.ts +++ b/mateclaw-ui/src/api/index.ts @@ -144,7 +144,16 @@ export const conversationApi = { // ==================== Skill ==================== export const skillApi = { - list: () => http.get('/skills'), + /** RFC-042 §2.1 — paginated skill listing with search/type/enabled filters */ + page: (params: { + page?: number + size?: number + keyword?: string + skillType?: string + enabled?: boolean + } = {}) => http.get('/skills', { params }), + /** Tab count aggregate — returns { all, builtin, mcp, dynamic } */ + counts: () => http.get('/skills/counts'), listEnabled: () => http.get('/skills/enabled'), get: (id: string | number) => http.get(`/skills/${id}`), create: (data: any) => http.post('/skills', data), diff --git a/mateclaw-ui/src/i18n/locales/en-US.ts b/mateclaw-ui/src/i18n/locales/en-US.ts index 848256e5..be64eca1 100644 --- a/mateclaw-ui/src/i18n/locales/en-US.ts +++ b/mateclaw-ui/src/i18n/locales/en-US.ts @@ -1606,6 +1606,14 @@ export default { mcp: 'MCP', dynamic: 'Dynamic', }, + search: { + placeholder: 'Search by name, description, or tags...', + }, + filter: { + all: 'All statuses', + enabled: 'Enabled', + disabled: 'Disabled', + }, empty: 'No skills found', emptyDesc: 'Add skills to enhance your agents\' capabilities', noDescription: 'No description', diff --git a/mateclaw-ui/src/i18n/locales/zh-CN.ts b/mateclaw-ui/src/i18n/locales/zh-CN.ts index bf6eaa85..0bddbbb5 100644 --- a/mateclaw-ui/src/i18n/locales/zh-CN.ts +++ b/mateclaw-ui/src/i18n/locales/zh-CN.ts @@ -1616,6 +1616,14 @@ export default { mcp: 'MCP', dynamic: '动态', }, + search: { + placeholder: '搜索技能名称、描述或标签...', + }, + filter: { + all: '全部状态', + enabled: '已启用', + disabled: '已禁用', + }, empty: '暂无技能', emptyDesc: '添加技能以增强 Agent 的能力', noDescription: '暂无描述', diff --git a/mateclaw-ui/src/views/Agents.vue b/mateclaw-ui/src/views/Agents.vue index c048f18c..44989d5f 100644 --- a/mateclaw-ui/src/views/Agents.vue +++ b/mateclaw-ui/src/views/Agents.vue @@ -488,7 +488,9 @@ async function openEditModal(agent: Agent) { // Load available skills/tools/providers and current bindings in parallel try { const [skillsRes, toolsRes, providersRes, boundSkillsRes, boundToolsRes, providerPrefsRes] = await Promise.all([ - skillApi.list(), + // RFC-042: /skills is now paginated; binding dropdown only needs enabled skills, + // so listEnabled() is both semantically correct and shape-stable (returns array). + skillApi.listEnabled(), toolApi.list(), modelApi.listProviders(), agentBindingApi.listSkills(agent.id), diff --git a/mateclaw-ui/src/views/SkillMarket.vue b/mateclaw-ui/src/views/SkillMarket.vue index b6ce074d..b49890bd 100644 --- a/mateclaw-ui/src/views/SkillMarket.vue +++ b/mateclaw-ui/src/views/SkillMarket.vue @@ -34,16 +34,31 @@

+ +
+ + +
+ -
-
+
@@ -132,6 +147,20 @@

{{ t('skills.empty') }}

{{ t('skills.emptyDesc') }}

+ + +
@@ -222,7 +251,7 @@