feat(skill,tool,plugin): gate skill/tool/plugin endpoints by workspace role

This commit is contained in:
matevip 2026-05-15 10:17:56 +08:00
parent f4964bc6d8
commit 41f3408209
6 changed files with 55 additions and 0 deletions

View File

@ -7,6 +7,7 @@ import org.springframework.web.bind.annotation.*;
import vip.mate.common.result.R;
import vip.mate.plugin.PluginManager;
import vip.mate.plugin.model.PluginInfo;
import vip.mate.workspace.core.annotation.RequireWorkspaceRole;
import java.util.List;
import java.util.Map;
@ -26,18 +27,21 @@ public class PluginController {
@Operation(summary = "List all plugins")
@GetMapping
@RequireWorkspaceRole("admin")
public R<List<PluginInfo>> list() {
return R.ok(pluginManager.listPlugins());
}
@Operation(summary = "Get plugin detail")
@GetMapping("/{name}")
@RequireWorkspaceRole("admin")
public R<PluginInfo> get(@PathVariable String name) {
return R.ok(pluginManager.getPlugin(name));
}
@Operation(summary = "Disable a plugin")
@PostMapping("/{name}/disable")
@RequireWorkspaceRole("admin")
public R<Void> disable(@PathVariable String name) {
pluginManager.disablePlugin(name);
return R.ok();
@ -45,6 +49,7 @@ public class PluginController {
@Operation(summary = "Enable a plugin")
@PostMapping("/{name}/enable")
@RequireWorkspaceRole("admin")
public R<Void> enable(@PathVariable String name) {
pluginManager.enablePlugin(name);
return R.ok();
@ -52,6 +57,7 @@ public class PluginController {
@Operation(summary = "Update plugin configuration")
@PutMapping("/{name}/config")
@RequireWorkspaceRole("admin")
public R<Void> updateConfig(@PathVariable String name,
@RequestBody Map<String, Object> config) {
pluginManager.updateConfig(name, config);

View File

@ -8,6 +8,7 @@ import org.springframework.web.bind.annotation.*;
import vip.mate.common.result.R;
import vip.mate.agent.AgentService;
import vip.mate.agent.binding.model.AgentSkillBinding;
import vip.mate.workspace.core.annotation.RequireWorkspaceRole;
import vip.mate.agent.binding.repository.AgentSkillBindingMapper;
import vip.mate.agent.binding.service.AgentBindingService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@ -62,6 +63,7 @@ public class SkillController {
@Operation(summary = "获取技能分页列表RFC-042 §2.1")
@GetMapping
@RequireWorkspaceRole("member")
public R<IPage<SkillEntity>> list(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "20") int size,
@ -90,6 +92,7 @@ public class SkillController {
@Operation(summary = "获取各类型技能计数tab 徽章用)")
@GetMapping("/counts")
@RequireWorkspaceRole("member")
public R<Map<String, Long>> counts() {
Map<String, Long> result = skillService.countByType();
Set<String> realNames = realSkillNames();
@ -250,6 +253,7 @@ public class SkillController {
@Operation(summary = "重新扫描单个技能RFC-042 §2.3.4")
@PostMapping("/{id}/rescan")
@RequireWorkspaceRole("admin")
public R<SkillEntity> rescan(@PathVariable Long id) {
rejectVirtualSkillMutation(id);
return R.ok(skillService.rescanSecurity(id));
@ -260,6 +264,7 @@ public class SkillController {
"in a multi-instance deployment. Pulls every mate_skill_file row owned by the skill " +
"down to disk; if no rows exist yet but local files do, ingests them into the canonical store.")
@PostMapping("/{id}/sync-files")
@RequireWorkspaceRole("admin")
public R<Map<String, Object>> syncFiles(@PathVariable Long id) {
rejectVirtualSkillMutation(id);
SkillEntity skill = skillService.getSkill(id);
@ -278,6 +283,7 @@ public class SkillController {
description = "Bulk variant of /sync-files; primarily for ops debugging when you suspect " +
"the local workspace is out of sync with the canonical store.")
@PostMapping("/sync-files")
@RequireWorkspaceRole("admin")
public R<Map<String, Object>> syncAllFiles() {
var report = skillFileSyncer.syncAll();
Map<String, Object> body = new LinkedHashMap<>();
@ -310,6 +316,7 @@ public class SkillController {
@Operation(summary = "获取已启用技能列表")
@GetMapping("/enabled")
@RequireWorkspaceRole("member")
public R<List<SkillEntity>> listEnabled() {
// Mirror the merging the paginated /skills endpoint does so the agent
// edit picker (which calls this endpoint) sees MCP- and ACP-derived
@ -337,18 +344,21 @@ public class SkillController {
@Operation(summary = "按类型获取技能列表")
@GetMapping("/type/{skillType}")
@RequireWorkspaceRole("member")
public R<List<SkillEntity>> listByType(@PathVariable String skillType) {
return R.ok(skillService.listSkillsByType(skillType));
}
@Operation(summary = "获取已启用技能摘要(按类型分组)")
@GetMapping("/summary")
@RequireWorkspaceRole("member")
public R<Map<String, List<String>>> summary() {
return R.ok(skillService.getEnabledSkillSummary());
}
@Operation(summary = "获取技能详情")
@GetMapping("/{id}")
@RequireWorkspaceRole("member")
public R<SkillEntity> get(@PathVariable Long id) {
// RFC-090 §3.2 virtual MCP-derived skills synthesize a row
// on demand from the live MCP server entity.
@ -370,12 +380,14 @@ public class SkillController {
@Operation(summary = "创建技能")
@PostMapping
@RequireWorkspaceRole("admin")
public R<SkillEntity> create(@RequestBody SkillEntity skill) {
return R.ok(skillService.createSkill(skill));
}
@Operation(summary = "更新技能")
@PutMapping("/{id}")
@RequireWorkspaceRole("admin")
public R<SkillEntity> update(@PathVariable Long id, @RequestBody SkillEntity skill) {
rejectVirtualSkillMutation(id);
skill.setId(id);
@ -393,6 +405,7 @@ public class SkillController {
*/
@Operation(summary = "硬删除技能 (admin only — 物理删除 + 工作区清空)")
@DeleteMapping("/{id}")
@RequireWorkspaceRole("admin")
public R<Void> delete(@PathVariable Long id) {
rejectVirtualSkillMutation(id);
skillService.hardDeleteSkill(id);
@ -401,6 +414,7 @@ public class SkillController {
@Operation(summary = "启用/禁用技能")
@PutMapping("/{id}/toggle")
@RequireWorkspaceRole("admin")
public R<SkillEntity> toggle(@PathVariable Long id, @RequestParam boolean enabled) {
rejectVirtualSkillMutation(id);
return R.ok(skillService.toggleSkill(id, enabled));
@ -408,6 +422,7 @@ public class SkillController {
@Operation(summary = "预览技能 Prompt 增强效果(调试用,与 Agent 真实运行时一致)")
@GetMapping("/prompt-preview")
@RequireWorkspaceRole("admin")
public R<Map<String, Object>> promptPreview() {
String prompt = skillRuntimeService.buildSkillPromptEnhancement();
return R.ok(Map.of(
@ -421,6 +436,7 @@ public class SkillController {
@Operation(summary = "获取 active skills 运行时视图")
@GetMapping("/runtime/active")
@RequireWorkspaceRole("admin")
public R<Map<String, Object>> getActiveSkills() {
List<ResolvedSkill> skills = skillRuntimeService.getActiveSkills();
return R.ok(Map.of("count", skills.size(), "skills", skills));
@ -428,12 +444,14 @@ public class SkillController {
@Operation(summary = "获取所有技能的运行时解析状态(管理页面使用)")
@GetMapping("/runtime/status")
@RequireWorkspaceRole("admin")
public R<List<ResolvedSkill>> getRuntimeStatus() {
return R.ok(skillRuntimeService.resolveAllSkillsStatus());
}
@Operation(summary = "刷新 active skills 缓存resync=true 时同步内置技能到 workspace")
@PostMapping("/runtime/refresh")
@RequireWorkspaceRole("admin")
public R<Map<String, Object>> refreshRuntime(
@RequestParam(defaultValue = "false") boolean resync) {
List<String> resynced = List.of();
@ -459,6 +477,7 @@ public class SkillController {
*/
@Operation(summary = "Pre-flight requirement statuses for a skill (RFC-090)")
@GetMapping("/{id}/requirements")
@RequireWorkspaceRole("member")
public R<Map<String, Object>> requirements(@PathVariable Long id) {
ResolvedSkill resolved = skillRuntimeService.resolveAllSkillsStatus().stream()
.filter(r -> r != null && id.equals(r.getId()))
@ -524,6 +543,7 @@ public class SkillController {
*/
@Operation(summary = "List agents that can use this skill (RFC-090 §14.2)")
@GetMapping("/{id}/employees")
@RequireWorkspaceRole("member")
public R<List<Map<String, Object>>> employees(@PathVariable Long id) {
// Explicit bindings: agent_skill rows pointing to this skill.
List<AgentSkillBinding> explicitBindings = agentSkillBindingMapper.selectList(
@ -590,6 +610,7 @@ public class SkillController {
*/
@Operation(summary = "Read per-skill LESSONS.md (RFC-090 §11.4)")
@GetMapping("/{id}/lessons")
@RequireWorkspaceRole("member")
public R<Map<String, Object>> getLessons(@PathVariable Long id) {
ResolvedSkill resolved = skillRuntimeService.resolveAllSkillsStatus().stream()
.filter(r -> r != null && id.equals(r.getId()))
@ -620,6 +641,7 @@ public class SkillController {
@Operation(summary = "Clear all lessons for a skill (RFC-090 §11.4)")
@PostMapping("/{id}/lessons/clear")
@RequireWorkspaceRole("admin")
public R<Map<String, Object>> clearLessons(@PathVariable Long id) {
ResolvedSkill resolved = skillRuntimeService.resolveAllSkillsStatus().stream()
.filter(r -> r != null && id.equals(r.getId()))
@ -634,6 +656,7 @@ public class SkillController {
@Operation(summary = "从对话历史合成 SkillRFC-023")
@PostMapping("/synthesize-from-conversation")
@RequireWorkspaceRole("admin")
public R<Map<String, Object>> synthesizeFromConversation(@RequestBody Map<String, Object> body) {
String conversationId = (String) body.get("conversationId");
Long agentId = body.get("agentId") != null ? Long.valueOf(body.get("agentId").toString()) : null;
@ -666,6 +689,7 @@ public class SkillController {
@Operation(summary = "将 skill 导出到工作区目录")
@PostMapping("/{id}/export-workspace")
@RequireWorkspaceRole("admin")
public R<Map<String, Object>> exportToWorkspace(@PathVariable Long id) {
SkillEntity skill = skillService.getSkill(id);
var path = workspaceManager.exportToWorkspace(skill.getName(), skill.getSkillContent());
@ -677,6 +701,7 @@ public class SkillController {
@Operation(summary = "获取 skill 工作区信息")
@GetMapping("/{id}/workspace")
@RequireWorkspaceRole("admin")
public R<Map<String, Object>> getWorkspaceInfo(@PathVariable Long id) {
SkillEntity skill = skillService.getSkill(id);
return R.ok(workspaceManager.getWorkspaceInfo(skill.getName()));

View File

@ -8,6 +8,7 @@ import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import vip.mate.common.result.R;
import vip.mate.skill.installer.SkillInstaller;
import vip.mate.workspace.core.annotation.RequireWorkspaceRole;
import vip.mate.skill.installer.ZipSkillFetcher;
import vip.mate.skill.installer.model.*;
import vip.mate.skill.runtime.SkillFrontmatterParser;
@ -34,6 +35,7 @@ public class SkillInstallController {
@Operation(summary = "搜索 ClawHub 市场")
@GetMapping("/hub/search")
@RequireWorkspaceRole("admin")
public R<List<HubSkillInfo>> searchHub(
@RequestParam String q,
@RequestParam(defaultValue = "20") int limit) {
@ -42,6 +44,7 @@ public class SkillInstallController {
@Operation(summary = "开始异步安装 skill")
@PostMapping("/start")
@RequireWorkspaceRole("admin")
public R<InstallTask> startInstall(@RequestBody InstallRequest request) {
if (request.getBundleUrl() == null || request.getBundleUrl().isBlank()) {
return R.fail("bundleUrl is required");
@ -51,6 +54,7 @@ public class SkillInstallController {
@Operation(summary = "查询安装任务状态")
@GetMapping("/status/{taskId}")
@RequireWorkspaceRole("admin")
public R<InstallTask> getStatus(@PathVariable String taskId) {
InstallTask task = skillInstaller.getTaskStatus(taskId);
if (task == null) {
@ -61,6 +65,7 @@ public class SkillInstallController {
@Operation(summary = "取消安装任务")
@PostMapping("/cancel/{taskId}")
@RequireWorkspaceRole("admin")
public R<Void> cancel(@PathVariable String taskId) {
skillInstaller.cancelTask(taskId);
return R.ok();
@ -68,6 +73,7 @@ public class SkillInstallController {
@Operation(summary = "上传 ZIP 安装 skill")
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@RequireWorkspaceRole("admin")
public R<Map<String, Object>> uploadZip(
@RequestPart("file") MultipartFile zipFile,
@RequestParam(defaultValue = "true") Boolean enable,
@ -91,6 +97,7 @@ public class SkillInstallController {
@Operation(summary = "卸载 skill")
@DeleteMapping("/{skillName}")
@RequireWorkspaceRole("admin")
public R<Map<String, String>> uninstall(@PathVariable String skillName) {
skillInstaller.uninstall(skillName);
return R.ok(Map.of("message", "Skill '" + skillName + "' uninstalled"));

View File

@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import vip.mate.common.result.R;
import vip.mate.workspace.core.annotation.RequireWorkspaceRole;
import java.util.List;
import java.util.Map;
@ -36,12 +37,14 @@ public class SkillSecretController {
@Operation(summary = "List secret keys + masked previews for a skill")
@GetMapping
@RequireWorkspaceRole("admin")
public R<List<SkillSecretService.SecretSummary>> list(@PathVariable Long skillId) {
return R.ok(skillSecretService.listSummaries(skillId));
}
@Operation(summary = "Upsert a secret value (empty value deletes it)")
@PostMapping
@RequireWorkspaceRole("admin")
public R<Void> put(@PathVariable Long skillId, @RequestBody Map<String, String> body) {
skillSecretService.put(skillId, body.get("key"), body.get("value"));
return R.ok();
@ -49,6 +52,7 @@ public class SkillSecretController {
@Operation(summary = "Delete a single secret by key")
@DeleteMapping("/{key}")
@RequireWorkspaceRole("admin")
public R<Void> remove(@PathVariable Long skillId, @PathVariable String key) {
skillSecretService.remove(skillId, key);
return R.ok();

View File

@ -6,6 +6,7 @@ import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import vip.mate.common.result.R;
import vip.mate.skill.model.SkillEntity;
import vip.mate.workspace.core.annotation.RequireWorkspaceRole;
import java.util.List;
import java.util.Map;
@ -33,12 +34,14 @@ public class SkillTemplateController {
@Operation(summary = "List skill templates (RFC-091)")
@GetMapping
@RequireWorkspaceRole("member")
public R<List<SkillTemplate>> list() {
return R.ok(registry.all());
}
@Operation(summary = "Get a single skill template")
@GetMapping("/{id}")
@RequireWorkspaceRole("member")
public R<SkillTemplate> get(@PathVariable String id) {
SkillTemplate t = registry.find(id);
if (t == null) return R.fail("Template not found: " + id);
@ -47,6 +50,7 @@ public class SkillTemplateController {
@Operation(summary = "Instantiate a template into a skill")
@PostMapping("/{id}/instantiate")
@RequireWorkspaceRole("admin")
public R<SkillEntity> instantiate(
@PathVariable String id,
@RequestBody Map<String, Object> values) {

View File

@ -9,6 +9,7 @@ import vip.mate.tool.model.AvailableToolDTO;
import vip.mate.tool.model.ToolEntity;
import vip.mate.tool.service.AvailableToolService;
import vip.mate.tool.service.ToolService;
import vip.mate.workspace.core.annotation.RequireWorkspaceRole;
import java.util.List;
@ -28,36 +29,42 @@ public class ToolController {
@Operation(summary = "获取工具列表")
@GetMapping
@RequireWorkspaceRole("member")
public R<List<ToolEntity>> list() {
return R.ok(toolService.listTools());
}
@Operation(summary = "获取已启用工具列表")
@GetMapping("/enabled")
@RequireWorkspaceRole("member")
public R<List<ToolEntity>> listEnabled() {
return R.ok(toolService.listEnabledTools());
}
@Operation(summary = "获取员工可绑定的全部原子工具(含 MCP")
@GetMapping("/available")
@RequireWorkspaceRole("member")
public R<List<AvailableToolDTO>> listAvailable() {
return R.ok(availableToolService.listAvailable());
}
@Operation(summary = "获取工具详情")
@GetMapping("/{id}")
@RequireWorkspaceRole("admin")
public R<ToolEntity> get(@PathVariable Long id) {
return R.ok(toolService.getTool(id));
}
@Operation(summary = "创建工具MCP")
@PostMapping
@RequireWorkspaceRole("admin")
public R<ToolEntity> create(@RequestBody ToolEntity tool) {
return R.ok(toolService.createTool(tool));
}
@Operation(summary = "更新工具")
@PutMapping("/{id}")
@RequireWorkspaceRole("admin")
public R<ToolEntity> update(@PathVariable Long id, @RequestBody ToolEntity tool) {
tool.setId(id);
return R.ok(toolService.updateTool(tool));
@ -65,6 +72,7 @@ public class ToolController {
@Operation(summary = "删除工具")
@DeleteMapping("/{id}")
@RequireWorkspaceRole("admin")
public R<Void> delete(@PathVariable Long id) {
toolService.deleteTool(id);
return R.ok();
@ -72,6 +80,7 @@ public class ToolController {
@Operation(summary = "启用/禁用工具")
@PutMapping("/{id}/toggle")
@RequireWorkspaceRole("admin")
public R<ToolEntity> toggle(@PathVariable Long id, @RequestParam boolean enabled) {
return R.ok(toolService.toggleTool(id, enabled));
}