mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(skill,mcp): ckjia-shopping skill bundle + same-name dedup against bridged MCP/ACP virtual skills
This commit is contained in:
parent
03a82ece93
commit
c07048f258
@ -6,6 +6,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
@ -39,4 +40,20 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
||||
.allowCredentials(true)
|
||||
.maxAge(3600);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expose per-skill bundled assets (logos, screenshots, ...) at
|
||||
* {@code /skill-assets/<skillName>/...}.
|
||||
*
|
||||
* <p>Source layout: {@code src/main/resources/skills/<name>/assets/<file>}.
|
||||
* Built-in skills can ship icons / hero images alongside SKILL.md without
|
||||
* polluting {@code src/main/resources/static/} (gitignored — that path is
|
||||
* the mateclaw-ui Vite build output).
|
||||
*/
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
registry.addResourceHandler("/skill-assets/**")
|
||||
.addResourceLocations("classpath:/skills/")
|
||||
.setCachePeriod(86400);
|
||||
}
|
||||
}
|
||||
|
||||
@ -202,19 +202,29 @@ public class SkillRuntimeService {
|
||||
.map(packageResolver::resolve)
|
||||
.filter(SkillRuntimeService::passesActiveGate)
|
||||
.collect(Collectors.toList());
|
||||
// Track real skill names so a same-named bridged virtual skill is
|
||||
// suppressed (real wins). Without this, a real SKILL.md packaged
|
||||
// alongside a same-name MCP/ACP server produces two cards on the
|
||||
// Skills page.
|
||||
Set<String> realNames = resolved.stream()
|
||||
.map(ResolvedSkill::getName)
|
||||
.collect(Collectors.toSet());
|
||||
try {
|
||||
// RFC-090 §3.2 — MCP-derived virtual skills go through the
|
||||
// same active gate so a disconnected MCP server doesn't
|
||||
// pollute the prompt enhancement.
|
||||
// MCP-derived virtual skills go through the same active gate
|
||||
// so a disconnected MCP server doesn't pollute the prompt
|
||||
// enhancement. Same-name virtuals are suppressed by the real
|
||||
// skill above.
|
||||
for (ResolvedSkill virt : mcpSkillBridge.listMcpDerivedResolvedSkills()) {
|
||||
if (realNames.contains(virt.getName())) continue;
|
||||
if (passesActiveGate(virt)) resolved.add(virt);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("MCP skill bridge active merge failed: {}", e.getMessage());
|
||||
}
|
||||
try {
|
||||
// RFC-090 §3.2 (parallel) — ACP-derived virtual skills.
|
||||
// ACP-derived virtual skills. Same dedup as MCP.
|
||||
for (ResolvedSkill virt : acpSkillBridge.listAcpDerivedResolvedSkills()) {
|
||||
if (realNames.contains(virt.getName())) continue;
|
||||
if (passesActiveGate(virt)) resolved.add(virt);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@ -240,13 +250,25 @@ public class SkillRuntimeService {
|
||||
List<ResolvedSkill> resolved = allSkills.stream()
|
||||
.map(packageResolver::resolve)
|
||||
.collect(Collectors.toList());
|
||||
// Same dedup-by-name as refreshActiveSkills(): a real skill with
|
||||
// the same name as a bridged virtual one suppresses the virtual,
|
||||
// so the Skills admin page never shows two cards for the same name.
|
||||
Set<String> realNames = resolved.stream()
|
||||
.map(ResolvedSkill::getName)
|
||||
.collect(Collectors.toSet());
|
||||
try {
|
||||
resolved.addAll(mcpSkillBridge.listMcpDerivedResolvedSkills());
|
||||
for (ResolvedSkill virt : mcpSkillBridge.listMcpDerivedResolvedSkills()) {
|
||||
if (realNames.contains(virt.getName())) continue;
|
||||
resolved.add(virt);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("MCP skill bridge merge failed: {}", e.getMessage());
|
||||
}
|
||||
try {
|
||||
resolved.addAll(acpSkillBridge.listAcpDerivedResolvedSkills());
|
||||
for (ResolvedSkill virt : acpSkillBridge.listAcpDerivedResolvedSkills()) {
|
||||
if (realNames.contains(virt.getName())) continue;
|
||||
resolved.add(virt);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("ACP skill bridge merge failed: {}", e.getMessage());
|
||||
}
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
-- 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.
|
||||
--
|
||||
-- Column name is `url` (not `endpoint`) per McpServerEntity.
|
||||
-- headers_json uses ${CKJIA_MCP_KEY} placeholder so the plaintext API key
|
||||
-- never lands in the database (parseHeaders expands env vars at request time).
|
||||
|
||||
MERGE INTO mate_mcp_server (
|
||||
name, transport, url, headers_json, enabled, description,
|
||||
connect_timeout_seconds, read_timeout_seconds, builtin, create_time, update_time, deleted
|
||||
) KEY(name)
|
||||
VALUES (
|
||||
'ckjia-shopping',
|
||||
'sse',
|
||||
'http://localhost:8088/mcp/sse',
|
||||
'{"Authorization": "Bearer ${CKJIA_MCP_KEY}"}',
|
||||
FALSE,
|
||||
'CKJIA cross-platform price comparison MCP server (Taobao/JD/Tmall/Pinduoduo).',
|
||||
30, 30, TRUE,
|
||||
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 0
|
||||
);
|
||||
@ -0,0 +1,22 @@
|
||||
-- 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.
|
||||
--
|
||||
-- Column name is `url` (not `endpoint`) per McpServerEntity.
|
||||
-- headers_json uses ${CKJIA_MCP_KEY} placeholder so the plaintext API key
|
||||
-- never lands in the database (parseHeaders expands env vars at request time).
|
||||
|
||||
INSERT INTO mate_mcp_server (
|
||||
name, transport, url, headers_json, enabled, description,
|
||||
connect_timeout_seconds, read_timeout_seconds, builtin, create_time, update_time, deleted
|
||||
)
|
||||
SELECT 'ckjia-shopping',
|
||||
'sse',
|
||||
'http://localhost:8088/mcp/sse',
|
||||
'{"Authorization": "Bearer ${CKJIA_MCP_KEY}"}',
|
||||
FALSE,
|
||||
'CKJIA cross-platform price comparison MCP server (Taobao/JD/Tmall/Pinduoduo).',
|
||||
30, 30, TRUE,
|
||||
NOW(), NOW(), 0
|
||||
FROM dual
|
||||
WHERE NOT EXISTS (SELECT 1 FROM mate_mcp_server WHERE name = 'ckjia-shopping');
|
||||
@ -0,0 +1,45 @@
|
||||
---
|
||||
name: ckjia-shopping
|
||||
version: "1.0.0"
|
||||
icon: /skill-assets/ckjia-shopping/assets/ckjia_logo_new.png
|
||||
description: "跨平台比价与购物推荐 / Cross-platform price comparison and shopping recommendation. 淘宝 / 京东 / 天猫 / 拼多多商品聚合搜索,拍图识物,AI 购物意图识别。Phase 1 ships shopping_recommend + image_recognize; unified_search / price_history / compare_skus 在后续迭代追加。"
|
||||
category: data
|
||||
type: mcp
|
||||
dependencies:
|
||||
tools:
|
||||
- ckjia_shopping_recommend
|
||||
- ckjia_image_recognize
|
||||
- ckjia_ping
|
||||
---
|
||||
|
||||
# 查价 · 比价购物
|
||||
|
||||
当用户询问"X 多少钱 / 哪里便宜 / 帮我推荐 X / 这个值不值买 / 拍照认一下这是什么"时使用本技能。
|
||||
|
||||
## 决策树
|
||||
|
||||
1. **"推荐 / 帮我挑 / 性价比 / 想买 X"** → `ckjia_shopping_recommend(query, top_n=5)`
|
||||
- 想要 ckjia 顺便给出意图理解(用于澄清后续问句)→ `include_intent=true`
|
||||
2. **附带图片 / 拍照识物** → `ckjia_image_recognize(image_url)` → 拿到 `suggested_query` 后再 `ckjia_shopping_recommend(suggested_query)`
|
||||
3. **transport 健康自检** → `ckjia_ping("hello")`,验证 MCP 链路通
|
||||
|
||||
## 输出渲染建议
|
||||
|
||||
- `recommendations: ProductCard[]` 在 chat UI 自动渲染成卡片网格(移动端单列、桌面端多列)
|
||||
- `intent` 字段是给 agent 自己看的"我理解对了吗"——拿到后若 `budgetRange` 与用户原话不符,应主动澄清
|
||||
- `needsClarification=true` 时,把 `clarificationQuestion` + `clarificationOptions` 直接抛给用户选
|
||||
|
||||
## 注意
|
||||
|
||||
- 同一个 query 不要在一次对话里反复调用 —— ckjia 侧已有缓存,重复调用浪费配额
|
||||
- 用户未登录时不必填 `user_id`;当前 Phase 1 所有调用以 API key owner 身份执行
|
||||
- API key 由管理员在 ckjia 控制台申请后填入 mateclaw `Settings ▸ MCP Connections` 的 `headers_json`,使用 `${CKJIA_MCP_KEY}` 环境变量占位符避免明文落库
|
||||
- 触发 429 `rate_limited` 时按 `Retry-After` 等待一次,再失败就汇总现有结果而不是无限重试
|
||||
|
||||
## 如何申请 API Key
|
||||
|
||||
1. 访问 ckjia 控制台 `https://ckjia.com/console/mcp-keys`(自助申请页面 P2 落地后开放;Phase 1 需联系运维手工签发)
|
||||
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` 即可
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Loading…
Reference in New Issue
Block a user