mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
fix(skill): refuse mutating MCP/ACP virtual skills and hide their card-level edit affordance
This commit is contained in:
parent
5cb82ed8f8
commit
ad1264ff51
@ -249,9 +249,29 @@ public class SkillController {
|
||||
@Operation(summary = "重新扫描单个技能(RFC-042 §2.3.4)")
|
||||
@PostMapping("/{id}/rescan")
|
||||
public R<SkillEntity> 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<List<SkillEntity>> listEnabled() {
|
||||
@ -321,6 +341,7 @@ public class SkillController {
|
||||
@Operation(summary = "更新技能")
|
||||
@PutMapping("/{id}")
|
||||
public R<SkillEntity> 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<Void> 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<SkillEntity> toggle(@PathVariable Long id, @RequestParam boolean enabled) {
|
||||
rejectVirtualSkillMutation(id);
|
||||
return R.ok(skillService.toggleSkill(id, enabled));
|
||||
}
|
||||
|
||||
|
||||
@ -129,6 +129,7 @@
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
v-if="!isSkillRowVirtual(skill)"
|
||||
class="skill-btn"
|
||||
:title="t('skills.actions.configure')"
|
||||
@click="openEditFromCard(skill)"
|
||||
@ -139,7 +140,7 @@
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
v-if="skill.skillType !== 'builtin'"
|
||||
v-if="skill.skillType !== 'builtin' && !isSkillRowVirtual(skill)"
|
||||
class="skill-btn danger"
|
||||
:title="t('skills.actions.delete')"
|
||||
@click="deleteSkill(skill)"
|
||||
@ -740,11 +741,14 @@ const newForm = ref<{ name: string; description: string; icon: string }>({ name:
|
||||
* {@link McpSkillBridge#VIRTUAL_ID_BASE} (= 9e18). The DB update path
|
||||
* doesn't know about them, so the drawer hides the Edit affordance.
|
||||
* Using string-length is robust against JS number precision loss past 2^53. */
|
||||
const isVirtualSkill = computed(() => {
|
||||
if (!detailSkill.value?.id) return false
|
||||
const idStr = String(detailSkill.value.id)
|
||||
function isVirtualSkillId(id: unknown): boolean {
|
||||
if (id === null || id === undefined) return false
|
||||
const idStr = String(id)
|
||||
return idStr.length >= 19 && idStr.startsWith('9')
|
||||
})
|
||||
}
|
||||
/** Per-row check used by the card-level configure / delete buttons. */
|
||||
const isSkillRowVirtual = (skill: { id?: unknown } | null | undefined) => isVirtualSkillId(skill?.id)
|
||||
const isVirtualSkill = computed(() => isVirtualSkillId(detailSkill.value?.id))
|
||||
const isBuiltinDetail = computed(() => detailSkill.value?.skillType === 'builtin' || !!detailSkill.value?.builtin)
|
||||
|
||||
/** RFC-090 §4.4 — pre-flight dialog state. */
|
||||
@ -1034,9 +1038,14 @@ async function createSkillFromModal() {
|
||||
}
|
||||
|
||||
/** Old configure-on-card button now lands in the drawer Overview tab in
|
||||
* edit mode — keeps the surface contract but skips the modal trip. */
|
||||
* edit mode — keeps the surface contract but skips the modal trip.
|
||||
* Virtual MCP/ACP-derived skills aren't editable; the card-level button
|
||||
* is hidden for them, but if any other call path reaches here with a
|
||||
* virtual skill, fall through to view mode rather than opening an edit
|
||||
* surface that will 4xx on save. */
|
||||
function openEditFromCard(skill: Skill) {
|
||||
openDetailDrawer(skill, 'overview', { editIdentity: true })
|
||||
const opts = isSkillRowVirtual(skill) ? undefined : { editIdentity: true }
|
||||
openDetailDrawer(skill, 'overview', opts)
|
||||
}
|
||||
|
||||
// ==================== Inline edit (Overview / Body) ====================
|
||||
|
||||
Loading…
Reference in New Issue
Block a user