mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-16 04:18:17 +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)")
|
@Operation(summary = "重新扫描单个技能(RFC-042 §2.3.4)")
|
||||||
@PostMapping("/{id}/rescan")
|
@PostMapping("/{id}/rescan")
|
||||||
public R<SkillEntity> rescan(@PathVariable Long id) {
|
public R<SkillEntity> rescan(@PathVariable Long id) {
|
||||||
|
rejectVirtualSkillMutation(id);
|
||||||
return R.ok(skillService.rescanSecurity(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 = "获取已启用技能列表")
|
@Operation(summary = "获取已启用技能列表")
|
||||||
@GetMapping("/enabled")
|
@GetMapping("/enabled")
|
||||||
public R<List<SkillEntity>> listEnabled() {
|
public R<List<SkillEntity>> listEnabled() {
|
||||||
@ -321,6 +341,7 @@ public class SkillController {
|
|||||||
@Operation(summary = "更新技能")
|
@Operation(summary = "更新技能")
|
||||||
@PutMapping("/{id}")
|
@PutMapping("/{id}")
|
||||||
public R<SkillEntity> update(@PathVariable Long id, @RequestBody SkillEntity skill) {
|
public R<SkillEntity> update(@PathVariable Long id, @RequestBody SkillEntity skill) {
|
||||||
|
rejectVirtualSkillMutation(id);
|
||||||
skill.setId(id);
|
skill.setId(id);
|
||||||
return R.ok(skillService.updateSkill(skill));
|
return R.ok(skillService.updateSkill(skill));
|
||||||
}
|
}
|
||||||
@ -337,6 +358,7 @@ public class SkillController {
|
|||||||
@Operation(summary = "硬删除技能 (admin only — 物理删除 + 工作区清空)")
|
@Operation(summary = "硬删除技能 (admin only — 物理删除 + 工作区清空)")
|
||||||
@DeleteMapping("/{id}")
|
@DeleteMapping("/{id}")
|
||||||
public R<Void> delete(@PathVariable Long id) {
|
public R<Void> delete(@PathVariable Long id) {
|
||||||
|
rejectVirtualSkillMutation(id);
|
||||||
skillService.hardDeleteSkill(id);
|
skillService.hardDeleteSkill(id);
|
||||||
return R.ok();
|
return R.ok();
|
||||||
}
|
}
|
||||||
@ -344,6 +366,7 @@ public class SkillController {
|
|||||||
@Operation(summary = "启用/禁用技能")
|
@Operation(summary = "启用/禁用技能")
|
||||||
@PutMapping("/{id}/toggle")
|
@PutMapping("/{id}/toggle")
|
||||||
public R<SkillEntity> toggle(@PathVariable Long id, @RequestParam boolean enabled) {
|
public R<SkillEntity> toggle(@PathVariable Long id, @RequestParam boolean enabled) {
|
||||||
|
rejectVirtualSkillMutation(id);
|
||||||
return R.ok(skillService.toggleSkill(id, enabled));
|
return R.ok(skillService.toggleSkill(id, enabled));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -129,6 +129,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
v-if="!isSkillRowVirtual(skill)"
|
||||||
class="skill-btn"
|
class="skill-btn"
|
||||||
:title="t('skills.actions.configure')"
|
:title="t('skills.actions.configure')"
|
||||||
@click="openEditFromCard(skill)"
|
@click="openEditFromCard(skill)"
|
||||||
@ -139,7 +140,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="skill.skillType !== 'builtin'"
|
v-if="skill.skillType !== 'builtin' && !isSkillRowVirtual(skill)"
|
||||||
class="skill-btn danger"
|
class="skill-btn danger"
|
||||||
:title="t('skills.actions.delete')"
|
:title="t('skills.actions.delete')"
|
||||||
@click="deleteSkill(skill)"
|
@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
|
* {@link McpSkillBridge#VIRTUAL_ID_BASE} (= 9e18). The DB update path
|
||||||
* doesn't know about them, so the drawer hides the Edit affordance.
|
* 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. */
|
* Using string-length is robust against JS number precision loss past 2^53. */
|
||||||
const isVirtualSkill = computed(() => {
|
function isVirtualSkillId(id: unknown): boolean {
|
||||||
if (!detailSkill.value?.id) return false
|
if (id === null || id === undefined) return false
|
||||||
const idStr = String(detailSkill.value.id)
|
const idStr = String(id)
|
||||||
return idStr.length >= 19 && idStr.startsWith('9')
|
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)
|
const isBuiltinDetail = computed(() => detailSkill.value?.skillType === 'builtin' || !!detailSkill.value?.builtin)
|
||||||
|
|
||||||
/** RFC-090 §4.4 — pre-flight dialog state. */
|
/** 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
|
/** 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) {
|
function openEditFromCard(skill: Skill) {
|
||||||
openDetailDrawer(skill, 'overview', { editIdentity: true })
|
const opts = isSkillRowVirtual(skill) ? undefined : { editIdentity: true }
|
||||||
|
openDetailDrawer(skill, 'overview', opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Inline edit (Overview / Body) ====================
|
// ==================== Inline edit (Overview / Body) ====================
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user