mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
fix(skill): harden ckjia skill binding and virtual dedup
This commit is contained in:
parent
6fe9b6e5f7
commit
68e1c6f50d
@ -27,6 +27,7 @@ import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 技能管理接口
|
||||
@ -65,6 +66,11 @@ public class SkillController {
|
||||
@RequestParam(required = false) Boolean enabled,
|
||||
@RequestParam(required = false) String scanStatus) {
|
||||
IPage<SkillEntity> dbPage = skillService.pageSkills(page, size, keyword, skillType, enabled, scanStatus);
|
||||
boolean mergeMcpVirtuals = page == 1
|
||||
&& (skillType == null || skillType.isBlank() || "mcp".equalsIgnoreCase(skillType));
|
||||
boolean mergeAcpVirtuals = page == 1
|
||||
&& (skillType == null || skillType.isBlank() || "acp".equalsIgnoreCase(skillType));
|
||||
Set<String> realNames = (mergeMcpVirtuals || mergeAcpVirtuals) ? realSkillNames() : Set.of();
|
||||
|
||||
// RFC-090 §3.2 — surface MCP servers as virtual skills on the
|
||||
// first page so users see GitHub / Filesystem / etc. as cards
|
||||
@ -72,14 +78,14 @@ public class SkillController {
|
||||
// because virtual rows are unpaginated; a follow-up can push
|
||||
// them through proper SQL UNION ALL when MCP server count gets
|
||||
// into the dozens.
|
||||
if (page == 1 && (skillType == null || skillType.isBlank() || "mcp".equalsIgnoreCase(skillType))) {
|
||||
if (mergeMcpVirtuals) {
|
||||
try {
|
||||
List<SkillEntity> mcpSkills = mcpSkillBridge.listMcpDerivedSkillEntities();
|
||||
if (!mcpSkills.isEmpty()) {
|
||||
// In-memory filter mirrors the DB filters so the
|
||||
// user's filter chips still apply to virtual rows.
|
||||
String kw = keyword == null ? "" : keyword.trim().toLowerCase();
|
||||
List<SkillEntity> filtered = mcpSkills.stream()
|
||||
List<SkillEntity> filtered = filterShadowedVirtualSkills(mcpSkills, realNames).stream()
|
||||
.filter(s -> kw.isEmpty()
|
||||
|| (s.getName() != null && s.getName().toLowerCase().contains(kw))
|
||||
|| (s.getDescription() != null && s.getDescription().toLowerCase().contains(kw)))
|
||||
@ -97,12 +103,12 @@ public class SkillController {
|
||||
}
|
||||
}
|
||||
// RFC-090 §3.2 (parallel) — same auto-bridge for ACP endpoints.
|
||||
if (page == 1 && (skillType == null || skillType.isBlank() || "acp".equalsIgnoreCase(skillType))) {
|
||||
if (mergeAcpVirtuals) {
|
||||
try {
|
||||
List<SkillEntity> acpSkills = acpSkillBridge.listAcpDerivedSkillEntities();
|
||||
if (!acpSkills.isEmpty()) {
|
||||
String kw = keyword == null ? "" : keyword.trim().toLowerCase();
|
||||
List<SkillEntity> filtered = acpSkills.stream()
|
||||
List<SkillEntity> filtered = filterShadowedVirtualSkills(acpSkills, realNames).stream()
|
||||
.filter(s -> kw.isEmpty()
|
||||
|| (s.getName() != null && s.getName().toLowerCase().contains(kw))
|
||||
|| (s.getDescription() != null && s.getDescription().toLowerCase().contains(kw)))
|
||||
@ -126,11 +132,13 @@ public class SkillController {
|
||||
@GetMapping("/counts")
|
||||
public R<Map<String, Long>> counts() {
|
||||
Map<String, Long> result = skillService.countByType();
|
||||
Set<String> realNames = realSkillNames();
|
||||
// RFC-090 §3.2 — virtual MCP-derived skills aren't in mate_skill,
|
||||
// so countByType() misses them. Fold in the live count so the
|
||||
// "MCP" and "all" tab badges match what the list endpoint shows.
|
||||
try {
|
||||
long virtualMcp = mcpSkillBridge.listMcpDerivedSkillEntities().size();
|
||||
long virtualMcp = countUnshadowedVirtualSkills(
|
||||
mcpSkillBridge.listMcpDerivedSkillEntities(), realNames);
|
||||
if (virtualMcp > 0) {
|
||||
result.merge("mcp", virtualMcp, Long::sum);
|
||||
result.merge("all", virtualMcp, Long::sum);
|
||||
@ -139,7 +147,8 @@ public class SkillController {
|
||||
// Bridge failure must not break the badge fetch.
|
||||
}
|
||||
try {
|
||||
long virtualAcp = acpSkillBridge.listAcpDerivedSkillEntities().size();
|
||||
long virtualAcp = countUnshadowedVirtualSkills(
|
||||
acpSkillBridge.listAcpDerivedSkillEntities(), realNames);
|
||||
if (virtualAcp > 0) {
|
||||
result.merge("acp", virtualAcp, Long::sum);
|
||||
result.merge("all", virtualAcp, Long::sum);
|
||||
@ -150,6 +159,32 @@ public class SkillController {
|
||||
return R.ok(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Real skill rows own their slug. Same-name MCP/ACP virtual rows are
|
||||
* shadowed even when the real row is disabled, matching the runtime status
|
||||
* view and avoiding duplicate cards for the same capability. The comparison
|
||||
* is name-only because {@code mate_skill.name} is the unique skill slug.
|
||||
*/
|
||||
static List<SkillEntity> filterShadowedVirtualSkills(List<SkillEntity> virtualSkills,
|
||||
Set<String> realSkillNames) {
|
||||
if (virtualSkills == null || virtualSkills.isEmpty()) return List.of();
|
||||
Set<String> realNames = realSkillNames == null ? Set.of() : realSkillNames;
|
||||
return virtualSkills.stream()
|
||||
.filter(s -> s != null && !realNames.contains(s.getName()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
static long countUnshadowedVirtualSkills(List<SkillEntity> virtualSkills,
|
||||
Set<String> realSkillNames) {
|
||||
return filterShadowedVirtualSkills(virtualSkills, realSkillNames).size();
|
||||
}
|
||||
|
||||
private Set<String> realSkillNames() {
|
||||
return skillService.listSkills().stream()
|
||||
.map(SkillEntity::getName)
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
}
|
||||
|
||||
@Operation(summary = "重新扫描单个技能(RFC-042 §2.3.4)")
|
||||
@PostMapping("/{id}/rescan")
|
||||
public R<SkillEntity> rescan(@PathVariable Long id) {
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
-- Seed default ckjia-shopping MCP server config (disabled by default).
|
||||
-- Admin enables in Settings > MCP Connections after pointing url to their
|
||||
-- ckjia instance and configuring CKJIA_MCP_KEY env var for authorization.
|
||||
-- Seed ckjia-shopping MCP server config (disabled by default).
|
||||
-- The localhost URL below is a dev/test placeholder only. Production admins
|
||||
-- must replace it in Settings > MCP Connections with the official CKJIA SaaS
|
||||
-- domain or their private CKJIA deployment URL before enabling the server,
|
||||
-- then configure CKJIA_MCP_KEY for authorization.
|
||||
--
|
||||
-- Column name is `url` (not `endpoint`) per McpServerEntity.
|
||||
-- headers_json uses ${CKJIA_MCP_KEY} placeholder so the plaintext API key
|
||||
@ -22,7 +24,7 @@ VALUES (
|
||||
'http://localhost:8085/sse',
|
||||
'{"Authorization": "Bearer ${CKJIA_MCP_KEY}"}',
|
||||
FALSE,
|
||||
'CKJIA cross-platform price comparison MCP server (Taobao/JD/Tmall/Pinduoduo).',
|
||||
'CKJIA price comparison MCP server. Disabled by default; replace the dev/test localhost URL with the production CKJIA domain before enabling.',
|
||||
30, 30, TRUE,
|
||||
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 0
|
||||
);
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
-- Seed default ckjia-shopping MCP server config (disabled by default).
|
||||
-- Admin enables in Settings > MCP Connections after pointing url to their
|
||||
-- ckjia instance and configuring CKJIA_MCP_KEY env var for authorization.
|
||||
-- Seed ckjia-shopping MCP server config (disabled by default).
|
||||
-- The localhost URL below is a dev/test placeholder only. Production admins
|
||||
-- must replace it in Settings > MCP Connections with the official CKJIA SaaS
|
||||
-- domain or their private CKJIA deployment URL before enabling the server,
|
||||
-- then configure CKJIA_MCP_KEY for authorization.
|
||||
--
|
||||
-- Column name is `url` (not `endpoint`) per McpServerEntity.
|
||||
-- headers_json uses ${CKJIA_MCP_KEY} placeholder so the plaintext API key
|
||||
@ -20,7 +22,7 @@ SELECT 1000000903,
|
||||
'http://localhost:8085/sse',
|
||||
'{"Authorization": "Bearer ${CKJIA_MCP_KEY}"}',
|
||||
FALSE,
|
||||
'CKJIA cross-platform price comparison MCP server (Taobao/JD/Tmall/Pinduoduo).',
|
||||
'CKJIA price comparison MCP server. Disabled by default; replace the dev/test localhost URL with the production CKJIA domain before enabling.',
|
||||
30, 30, TRUE,
|
||||
NOW(), NOW(), 0
|
||||
FROM dual
|
||||
|
||||
@ -2,11 +2,15 @@
|
||||
name: ckjia-shopping
|
||||
nameZh: 参考价 - 比价购物
|
||||
nameEn: CKJIA Shopping
|
||||
version: "1.0.0"
|
||||
version: "1.0.1"
|
||||
icon: /skill-assets/ckjia-shopping/assets/ckjia_app_icon.png
|
||||
description: "跨平台比价与购物推荐 / Cross-platform price comparison. 淘宝 / 京东 / 天猫 / 拼多多商品聚合搜索 + 拍图识物。需要先启用 ckjia-shopping MCP server 并配置 CKJIA_MCP_KEY 才能用。"
|
||||
category: data
|
||||
type: mcp
|
||||
allowed-tools:
|
||||
- ckjia_shopping_recommend
|
||||
- ckjia_image_recognize
|
||||
- ckjia_ping
|
||||
tags:
|
||||
- shopping
|
||||
- price
|
||||
@ -93,6 +97,7 @@ tags:
|
||||
|
||||
- 同一个 query 不要在一次对话里反复调用 —— ckjia 侧已有缓存,重复调用浪费配额
|
||||
- 用户未登录时不必填 `user_id`;当前 Phase 1 所有调用以 API key owner 身份执行
|
||||
- `mate_mcp_server` 里预置的 localhost URL 只用于本地开发/测试;生产启用前必须在 `Settings ▸ MCP Connections` 改成 ckjia 官方 SaaS 域名或私有部署域名
|
||||
- API key 由管理员在 ckjia 控制台申请后填入 mateclaw `Settings ▸ MCP Connections` 的 `headers_json`,使用 `${CKJIA_MCP_KEY}` 环境变量占位符避免明文落库
|
||||
- 触发 429 `rate_limited` 时按 `Retry-After` 等待一次,再失败就汇总现有结果而不是无限重试
|
||||
|
||||
@ -102,4 +107,4 @@ tags:
|
||||
2. 选 `free` / `standard` tier 与勾选所需 scopes
|
||||
3. 一次性获得明文 key(形如 `ckjia_mcp_live_5fK8j2nQ…`)
|
||||
4. 在 mateclaw 部署环境配 `CKJIA_MCP_KEY=ckjia_mcp_live_xxx`
|
||||
5. Settings ▸ MCP Connections 启用 `ckjia-shopping` 即可
|
||||
5. Settings ▸ MCP Connections 将 `ckjia-shopping` 的 URL 改为生产域名后再启用
|
||||
|
||||
Loading…
Reference in New Issue
Block a user