From ad1264ff5118e89962403abfc84f2504b4f99ad5 Mon Sep 17 00:00:00 2001 From: matevip Date: Thu, 7 May 2026 08:18:00 +0800 Subject: [PATCH] fix(skill): refuse mutating MCP/ACP virtual skills and hide their card-level edit affordance --- .../skill/controller/SkillController.java | 23 +++++++++++++++++++ mateclaw-ui/src/views/SkillMarket.vue | 23 +++++++++++++------ 2 files changed, 39 insertions(+), 7 deletions(-) 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 40b5da12..a24a81d7 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 @@ -249,9 +249,29 @@ public class SkillController { @Operation(summary = "重新扫描单个技能(RFC-042 §2.3.4)") @PostMapping("/{id}/rescan") public R rescan(@PathVariable Long id) { + rejectVirtualSkillMutation(id); return R.ok(skillService.rescanSecurity(id)); } + /** + * Mutation paths refuse virtual MCP/ACP skill ids upfront. The bridge + * synthesizes those rows on the fly from the upstream connection + * config; persisting an update against {@code mate_skill} would + * either silently no-op (no row to update) or — as users have hit — + * throw "技能不存在" because the lookup precedes the update. Sending + * a clear 4xx with a redirect hint is the right shape: the user + * wants the icon / display name / etc. to stick, and the only place + * those fields persist for an MCP entry is the MCP connection page. + */ + private void rejectVirtualSkillMutation(Long id) { + if (vip.mate.skill.mcp.McpSkillBridge.isVirtualMcpSkillId(id) + || vip.mate.skill.acp.AcpSkillBridge.isVirtualAcpSkillId(id)) { + throw new vip.mate.exception.MateClawException( + "err.skill.virtual_readonly", + "MCP/ACP 衍生技能不可在此编辑——请到 Settings ▸ MCP/ACP 连接页修改"); + } + } + @Operation(summary = "获取已启用技能列表") @GetMapping("/enabled") public R> listEnabled() { @@ -321,6 +341,7 @@ public class SkillController { @Operation(summary = "更新技能") @PutMapping("/{id}") public R update(@PathVariable Long id, @RequestBody SkillEntity skill) { + rejectVirtualSkillMutation(id); skill.setId(id); return R.ok(skillService.updateSkill(skill)); } @@ -337,6 +358,7 @@ public class SkillController { @Operation(summary = "硬删除技能 (admin only — 物理删除 + 工作区清空)") @DeleteMapping("/{id}") public R delete(@PathVariable Long id) { + rejectVirtualSkillMutation(id); skillService.hardDeleteSkill(id); return R.ok(); } @@ -344,6 +366,7 @@ public class SkillController { @Operation(summary = "启用/禁用技能") @PutMapping("/{id}/toggle") public R toggle(@PathVariable Long id, @RequestParam boolean enabled) { + rejectVirtualSkillMutation(id); return R.ok(skillService.toggleSkill(id, enabled)); } diff --git a/mateclaw-ui/src/views/SkillMarket.vue b/mateclaw-ui/src/views/SkillMarket.vue index cc906277..1eb1430f 100644 --- a/mateclaw-ui/src/views/SkillMarket.vue +++ b/mateclaw-ui/src/views/SkillMarket.vue @@ -129,6 +129,7 @@