mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
fix(skill): clean separation of install / uninstall / hard-delete
This commit is contained in:
parent
7ca568c69b
commit
442ffa9c9e
@ -103,10 +103,19 @@ public class SkillController {
|
||||
return R.ok(skillService.updateSkill(skill));
|
||||
}
|
||||
|
||||
@Operation(summary = "删除技能")
|
||||
/**
|
||||
* RFC-090 §14.5 — admin-only hard delete: physical row removal +
|
||||
* workspace purge. UI surfaces this as "permanently delete" and
|
||||
* confirms with a destructive warning. The routine user-facing
|
||||
* "remove" button on the skill card calls
|
||||
* {@code DELETE /skills/install/{name}} instead, which goes through
|
||||
* {@link vip.mate.skill.installer.SkillInstaller#uninstall} for the
|
||||
* recoverable logical-delete + archive path.
|
||||
*/
|
||||
@Operation(summary = "硬删除技能 (admin only — 物理删除 + 工作区清空)")
|
||||
@DeleteMapping("/{id}")
|
||||
public R<Void> delete(@PathVariable Long id) {
|
||||
skillService.deleteSkill(id);
|
||||
skillService.hardDeleteSkill(id);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
|
||||
@ -74,10 +74,12 @@ public class SkillInstaller {
|
||||
}
|
||||
|
||||
/**
|
||||
* 卸载 skill(归档 workspace + 删除数据库记录)
|
||||
* RFC-090 §14.5 — user-facing uninstall: logical delete + workspace
|
||||
* archive. Re-installing the same name later is supported. For
|
||||
* admin-only physical removal, call
|
||||
* {@code SkillService.hardDeleteSkill} via {@code DELETE /skills/{id}}.
|
||||
*/
|
||||
public void uninstall(String skillName) {
|
||||
// 先在数据库中查找
|
||||
List<SkillEntity> skills = skillService.listSkills();
|
||||
SkillEntity target = skills.stream()
|
||||
.filter(s -> s.getName().equals(skillName))
|
||||
@ -85,10 +87,8 @@ public class SkillInstaller {
|
||||
.orElse(null);
|
||||
|
||||
if (target != null) {
|
||||
skillService.deleteSkill(target.getId());
|
||||
skillService.uninstallSkill(target.getId());
|
||||
}
|
||||
|
||||
// workspace 归档已在 SkillService.deleteSkill 中处理
|
||||
log.info("Uninstalled skill: {}", skillName);
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
package vip.mate.skill.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Delete;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import vip.mate.skill.model.SkillEntity;
|
||||
|
||||
/**
|
||||
@ -11,4 +13,14 @@ import vip.mate.skill.model.SkillEntity;
|
||||
*/
|
||||
@Mapper
|
||||
public interface SkillMapper extends BaseMapper<SkillEntity> {
|
||||
|
||||
/**
|
||||
* RFC-090 §14.5 — physical delete bypassing the {@code deleted}
|
||||
* logical-delete flag. Used by the admin "hard delete" path
|
||||
* ({@code DELETE /skills/{id}}); the user-facing "uninstall" path
|
||||
* still goes through {@link BaseMapper#deleteById} so the row can
|
||||
* be recovered by re-installing the same skill name.
|
||||
*/
|
||||
@Delete("DELETE FROM mate_skill WHERE id = #{id}")
|
||||
int hardDeleteById(@Param("id") Long id);
|
||||
}
|
||||
|
||||
@ -279,28 +279,75 @@ public class SkillService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除技能
|
||||
* 内置技能不可删除
|
||||
* RFC-090 §14.5 — uninstall path: logical delete (row stays in DB
|
||||
* with {@code deleted=1}) + archive workspace to
|
||||
* {@code .archived/}.
|
||||
*
|
||||
* <p>Recoverable: the user can re-install a skill of the same name
|
||||
* later, since the archived workspace and soft-deleted row stay
|
||||
* out of every standard query but on disk.
|
||||
*
|
||||
* <p>Builtin skills are protected: uninstalling a builtin would
|
||||
* leave the seed re-creating it on next boot, so we surface a
|
||||
* clear error instead of doing partial work.
|
||||
*/
|
||||
public void deleteSkill(Long id) {
|
||||
public void uninstallSkill(Long id) {
|
||||
SkillEntity skill = getSkill(id);
|
||||
if (Boolean.TRUE.equals(skill.getBuiltin())) {
|
||||
throw new MateClawException("err.skill.builtin_readonly", "内置技能不可删除: " + skill.getName());
|
||||
throw new MateClawException("err.skill.builtin_readonly",
|
||||
"内置技能不可卸载: " + skill.getName());
|
||||
}
|
||||
skillMapper.deleteById(id);
|
||||
log.info("Deleted skill: {}", skill.getName());
|
||||
skillMapper.deleteById(id); // logical delete (deleted=1)
|
||||
log.info("Uninstalled skill (logical delete + archive): {}", skill.getName());
|
||||
|
||||
// 归档工作区目录
|
||||
if ("archive".equals(workspaceProperties.getDeletePolicy())) {
|
||||
workspaceManager.archiveWorkspace(skill.getName());
|
||||
}
|
||||
|
||||
// 刷新 runtime cache
|
||||
if (runtimeService != null) {
|
||||
runtimeService.refreshActiveSkills();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* RFC-090 §14.5 — hard-delete path: physical SQL delete + workspace
|
||||
* purge. Admin only. Not recoverable.
|
||||
*
|
||||
* <p>Use this when you need to free the slug for an unrelated skill
|
||||
* or scrub a row that's been corrupted. UI buttons should call
|
||||
* {@link #uninstallSkill} instead unless the user explicitly chose
|
||||
* "permanently delete".
|
||||
*/
|
||||
public void hardDeleteSkill(Long id) {
|
||||
SkillEntity skill = getSkill(id);
|
||||
if (Boolean.TRUE.equals(skill.getBuiltin())) {
|
||||
throw new MateClawException("err.skill.builtin_readonly",
|
||||
"内置技能不可硬删除: " + skill.getName());
|
||||
}
|
||||
skillMapper.hardDeleteById(id); // bypass the logical-delete flag
|
||||
log.info("Hard-deleted skill (physical delete + purge): {}", skill.getName());
|
||||
|
||||
workspaceManager.purgeWorkspace(skill.getName());
|
||||
|
||||
if (runtimeService != null) {
|
||||
runtimeService.refreshActiveSkills();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy alias maintained for callers that still use {@code
|
||||
* deleteSkill}. Routes to the user-friendly uninstall semantics
|
||||
* (logical delete + archive). New code should pick {@link
|
||||
* #uninstallSkill} or {@link #hardDeleteSkill} explicitly.
|
||||
*
|
||||
* @deprecated Use {@link #uninstallSkill} for user-facing UI delete
|
||||
* (recoverable) or {@link #hardDeleteSkill} for admin
|
||||
* "permanent" delete.
|
||||
*/
|
||||
@Deprecated
|
||||
public void deleteSkill(Long id) {
|
||||
uninstallSkill(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用技能
|
||||
*/
|
||||
|
||||
@ -136,6 +136,33 @@ public class SkillWorkspaceManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* RFC-090 §14.5 — physically remove the workspace directory. Used
|
||||
* by hard-delete only; uninstall still calls
|
||||
* {@link #archiveWorkspace} so users can recover by re-installing.
|
||||
*/
|
||||
public void purgeWorkspace(String skillName) {
|
||||
Path workspaceDir = resolveConventionPath(skillName);
|
||||
if (!Files.exists(workspaceDir)) return;
|
||||
try {
|
||||
// Walk and delete bottom-up so non-empty dirs go away too.
|
||||
try (var stream = Files.walk(workspaceDir)) {
|
||||
stream.sorted(java.util.Comparator.reverseOrder()).forEach(p -> {
|
||||
try {
|
||||
Files.deleteIfExists(p);
|
||||
} catch (IOException ignored) {
|
||||
/* leave partial cleanup; best effort */
|
||||
}
|
||||
});
|
||||
}
|
||||
log.info("Purged skill workspace: {}", workspaceDir);
|
||||
eventPublisher.publishEvent(new SkillWorkspaceEvent(skillName,
|
||||
SkillWorkspaceEvent.Type.ARCHIVED, workspaceDir));
|
||||
} catch (IOException e) {
|
||||
log.warn("Failed to purge workspace for skill '{}': {}", skillName, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 归档 workspace 到 {root}/.archived/{name}-{timestamp}/
|
||||
*/
|
||||
|
||||
@ -309,9 +309,12 @@ public class SkillManageTool {
|
||||
}
|
||||
|
||||
try {
|
||||
skillService.deleteSkill(existing.getId());
|
||||
log.info("[SkillManage] Agent deleted skill: name={}", name);
|
||||
return "Skill '" + name + "' deleted.";
|
||||
// RFC-090 §14.5 — agent-triggered delete uses uninstall
|
||||
// (logical + archive) so a misbehaving agent can't
|
||||
// physically purge a row past recovery.
|
||||
skillService.uninstallSkill(existing.getId());
|
||||
log.info("[SkillManage] Agent uninstalled skill: name={}", name);
|
||||
return "Skill '" + name + "' uninstalled (workspace archived).";
|
||||
} catch (Exception e) {
|
||||
log.error("[SkillManage] Failed to delete skill '{}': {}", name, e.getMessage(), e);
|
||||
return "Error deleting skill: " + e.getMessage();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user