diff --git a/mateclaw-server/src/main/java/vip/mate/agent/binding/controller/AgentBindingController.java b/mateclaw-server/src/main/java/vip/mate/agent/binding/controller/AgentBindingController.java index b562ce33..98ed79ee 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/binding/controller/AgentBindingController.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/binding/controller/AgentBindingController.java @@ -8,8 +8,10 @@ import vip.mate.agent.AgentService; import vip.mate.agent.binding.model.AgentSkillBinding; import vip.mate.agent.binding.model.AgentToolBinding; import vip.mate.agent.binding.service.AgentBindingService; +import vip.mate.agent.model.AgentEntity; import vip.mate.audit.service.AuditEventService; import vip.mate.common.result.R; +import vip.mate.exception.MateClawException; import vip.mate.workspace.core.annotation.RequireWorkspaceRole; import java.util.List; @@ -35,14 +37,18 @@ public class AgentBindingController { @Operation(summary = "获取 Agent 已绑定的 Skills") @GetMapping("/skills") @RequireWorkspaceRole("viewer") - public R> listSkills(@PathVariable Long agentId) { + public R> listSkills(@PathVariable Long agentId, + @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) { + verifyAgentWorkspace(agentId, workspaceId); return R.ok(bindingService.listSkillBindings(agentId)); } @Operation(summary = "批量设置 Agent 的 Skill 绑定") @PutMapping("/skills") @RequireWorkspaceRole("member") - public R setSkills(@PathVariable Long agentId, @RequestBody List skillIds) { + public R setSkills(@PathVariable Long agentId, @RequestBody List skillIds, + @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) { + verifyAgentWorkspace(agentId, workspaceId); bindingService.setSkillBindings(agentId, skillIds); agentService.invalidateAgentCache(agentId); auditEventService.record("UPDATE", "AGENT_SKILL", String.valueOf(agentId), @@ -53,7 +59,9 @@ public class AgentBindingController { @Operation(summary = "绑定单个 Skill") @PostMapping("/skills/{skillId}") @RequireWorkspaceRole("member") - public R bindSkill(@PathVariable Long agentId, @PathVariable Long skillId) { + public R bindSkill(@PathVariable Long agentId, @PathVariable Long skillId, + @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) { + verifyAgentWorkspace(agentId, workspaceId); AgentSkillBinding binding = bindingService.bindSkill(agentId, skillId); agentService.invalidateAgentCache(agentId); return R.ok(binding); @@ -62,7 +70,9 @@ public class AgentBindingController { @Operation(summary = "解绑单个 Skill") @DeleteMapping("/skills/{skillId}") @RequireWorkspaceRole("member") - public R unbindSkill(@PathVariable Long agentId, @PathVariable Long skillId) { + public R unbindSkill(@PathVariable Long agentId, @PathVariable Long skillId, + @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) { + verifyAgentWorkspace(agentId, workspaceId); bindingService.unbindSkill(agentId, skillId); agentService.invalidateAgentCache(agentId); return R.ok(); @@ -73,18 +83,35 @@ public class AgentBindingController { @Operation(summary = "获取 Agent 已绑定的 Tools") @GetMapping("/tools") @RequireWorkspaceRole("viewer") - public R> listTools(@PathVariable Long agentId) { + public R> listTools(@PathVariable Long agentId, + @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) { + verifyAgentWorkspace(agentId, workspaceId); return R.ok(bindingService.listToolBindings(agentId)); } @Operation(summary = "批量设置 Agent 的 Tool 绑定") @PutMapping("/tools") @RequireWorkspaceRole("member") - public R setTools(@PathVariable Long agentId, @RequestBody List toolNames) { + public R setTools(@PathVariable Long agentId, @RequestBody List toolNames, + @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) { + verifyAgentWorkspace(agentId, workspaceId); bindingService.setToolBindings(agentId, toolNames); agentService.invalidateAgentCache(agentId); auditEventService.record("UPDATE", "AGENT_TOOL", String.valueOf(agentId), "tools=" + toolNames.size(), null); return R.ok(); } + + // ==================== Workspace Verification ==================== + + private void verifyAgentWorkspace(Long agentId, Long headerWorkspaceId) { + AgentEntity agent = agentService.getAgent(agentId); + if (agent == null) { + throw new MateClawException("Agent not found"); + } + long requestedWs = headerWorkspaceId != null ? headerWorkspaceId : 1L; + if (agent.getWorkspaceId() != null && !agent.getWorkspaceId().equals(requestedWs)) { + throw new MateClawException("资源不属于当前工作区"); + } + } } diff --git a/mateclaw-server/src/main/java/vip/mate/agent/controller/AgentController.java b/mateclaw-server/src/main/java/vip/mate/agent/controller/AgentController.java index 4934d37d..a895bbff 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/controller/AgentController.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/controller/AgentController.java @@ -96,10 +96,14 @@ public class AgentController { @Operation(summary = "流式对话(SSE)") @GetMapping("/{id}/chat/stream") + @RequireWorkspaceRole("viewer") public SseEmitter chatStream( @PathVariable Long id, @RequestParam String message, - @RequestParam(defaultValue = "default") String conversationId) { + @RequestParam(defaultValue = "default") String conversationId, + @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) { + AgentEntity agent = agentService.getAgent(id); + verifyResourceWorkspace(agent != null ? agent.getWorkspaceId() : null, workspaceId); SseEmitter emitter = new SseEmitter(5 * 60 * 1000L); sseExecutor.execute(() -> { @@ -131,23 +135,35 @@ public class AgentController { @Operation(summary = "同步对话") @PostMapping("/{id}/chat") + @RequireWorkspaceRole("viewer") public R chat( @PathVariable Long id, - @RequestBody ChatRequest request) { + @RequestBody ChatRequest request, + @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) { + AgentEntity agent = agentService.getAgent(id); + verifyResourceWorkspace(agent != null ? agent.getWorkspaceId() : null, workspaceId); return R.ok(agentService.chat(id, request.getMessage(), request.getConversationId())); } @Operation(summary = "执行复杂任务(Plan-Execute)") @PostMapping("/{id}/execute") + @RequireWorkspaceRole("viewer") public R execute( @PathVariable Long id, - @RequestBody ChatRequest request) { + @RequestBody ChatRequest request, + @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) { + AgentEntity agent = agentService.getAgent(id); + verifyResourceWorkspace(agent != null ? agent.getWorkspaceId() : null, workspaceId); return R.ok(agentService.execute(id, request.getMessage(), request.getConversationId())); } @Operation(summary = "获取Agent运行状态") @GetMapping("/{id}/state") - public R getState(@PathVariable Long id) { + @RequireWorkspaceRole("viewer") + public R getState(@PathVariable Long id, + @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) { + AgentEntity agent = agentService.getAgent(id); + verifyResourceWorkspace(agent != null ? agent.getWorkspaceId() : null, workspaceId); return R.ok(agentService.getAgentState(id)); } diff --git a/mateclaw-server/src/main/java/vip/mate/audit/service/AuditEventService.java b/mateclaw-server/src/main/java/vip/mate/audit/service/AuditEventService.java index 933dd804..669c0e5b 100644 --- a/mateclaw-server/src/main/java/vip/mate/audit/service/AuditEventService.java +++ b/mateclaw-server/src/main/java/vip/mate/audit/service/AuditEventService.java @@ -42,9 +42,23 @@ public class AuditEventService { */ public void record(String action, String resourceType, String resourceId, String resourceName, String detailJson) { + record(action, resourceType, resourceId, resourceName, detailJson, null); + } + + /** + * 异步记录审计事件(显式指定 workspace ID)。 + *

+ * 当调用方已知 workspace ID 时,优先使用此方法以避免依赖 request header 解析。 + */ + public void record(String action, String resourceType, String resourceId, + String resourceName, String detailJson, Long workspaceId) { // 在请求线程中构建事件(可以访问 SecurityContext 和 RequestContext) AuditEventEntity event = buildEvent(action, resourceType, resourceId, resourceName, detailJson); if (event != null) { + // 显式传入的 workspaceId 优先于 header 解析结果 + if (workspaceId != null) { + event.setWorkspaceId(workspaceId); + } insertAsync(event); } } diff --git a/mateclaw-server/src/main/java/vip/mate/channel/ChannelMessageRouter.java b/mateclaw-server/src/main/java/vip/mate/channel/ChannelMessageRouter.java index bfeb5769..9941c956 100644 --- a/mateclaw-server/src/main/java/vip/mate/channel/ChannelMessageRouter.java +++ b/mateclaw-server/src/main/java/vip/mate/channel/ChannelMessageRouter.java @@ -378,8 +378,8 @@ public class ChannelMessageRouter { } // ======= 审批拦截层结束 ======= - // 确保会话存在 - conversationService.getOrCreateSharedConversation(conversationId, agentId); + // 确保会话存在(workspace 感知) + conversationService.getOrCreateSharedConversation(conversationId, agentId, channelEntity.getWorkspaceId()); // 更新渠道会话存储(用于主动推送) String replyTarget = resolveReplyTarget(message); @@ -566,7 +566,7 @@ public class ChannelMessageRouter { String conversationId = buildConversationId(message); String username = message.getSenderName() != null ? message.getSenderName() : message.getSenderId(); - conversationService.getOrCreateConversation(conversationId, agentId, username); + conversationService.getOrCreateConversation(conversationId, agentId, username, channelEntity.getWorkspaceId()); List parts = message.getContentParts(); conversationService.saveMessage(conversationId, "user", message.getContent(), parts); diff --git a/mateclaw-server/src/main/java/vip/mate/channel/controller/ChannelController.java b/mateclaw-server/src/main/java/vip/mate/channel/controller/ChannelController.java index 0c3f3620..15b51866 100644 --- a/mateclaw-server/src/main/java/vip/mate/channel/controller/ChannelController.java +++ b/mateclaw-server/src/main/java/vip/mate/channel/controller/ChannelController.java @@ -45,8 +45,10 @@ public class ChannelController { @RequireWorkspaceRole("viewer") @Operation(summary = "按类型获取渠道列表") @GetMapping("/type/{channelType}") - public R> listByType(@PathVariable String channelType) { - return R.ok(channelService.listChannelsByType(channelType)); + public R> listByType(@PathVariable String channelType, + @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) { + long wsId = workspaceId != null ? workspaceId : 1L; + return R.ok(channelService.listChannelsByTypeAndWorkspace(channelType, wsId)); } @RequireWorkspaceRole("viewer") @@ -117,8 +119,8 @@ public class ChannelController { return R.ok(channel); } - @RequireWorkspaceRole("viewer") - @Operation(summary = "获取渠道运行状态") + @RequireWorkspaceRole("admin") + @Operation(summary = "获取渠道运行状态(全局系统视图,仅管理员可见)") @GetMapping("/status") public R> status() { return R.ok(channelManager.getStatus()); diff --git a/mateclaw-server/src/main/java/vip/mate/channel/service/ChannelService.java b/mateclaw-server/src/main/java/vip/mate/channel/service/ChannelService.java index 78d2379b..17aa46f2 100644 --- a/mateclaw-server/src/main/java/vip/mate/channel/service/ChannelService.java +++ b/mateclaw-server/src/main/java/vip/mate/channel/service/ChannelService.java @@ -54,7 +54,7 @@ public class ChannelService { } /** - * 按类型获取渠道列表 + * 按类型获取渠道列表(全局,向后兼容) */ public List listChannelsByType(String channelType) { return channelMapper.selectList(new LambdaQueryWrapper() @@ -62,6 +62,16 @@ public class ChannelService { .orderByDesc(ChannelEntity::getCreateTime)); } + /** + * 按类型和 workspace 获取渠道列表 + */ + public List listChannelsByTypeAndWorkspace(String channelType, Long workspaceId) { + return channelMapper.selectList(new LambdaQueryWrapper() + .eq(ChannelEntity::getChannelType, channelType) + .eq(ChannelEntity::getWorkspaceId, workspaceId) + .orderByDesc(ChannelEntity::getCreateTime)); + } + /** * 获取渠道详情 */ diff --git a/mateclaw-server/src/main/java/vip/mate/channel/web/ChatController.java b/mateclaw-server/src/main/java/vip/mate/channel/web/ChatController.java index 53afa1f1..10b330ce 100644 --- a/mateclaw-server/src/main/java/vip/mate/channel/web/ChatController.java +++ b/mateclaw-server/src/main/java/vip/mate/channel/web/ChatController.java @@ -17,6 +17,7 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import org.springframework.context.ApplicationEventPublisher; import vip.mate.common.result.R; import vip.mate.agent.AgentService; +import vip.mate.agent.model.AgentEntity; import vip.mate.approval.ApprovalService; import vip.mate.approval.PendingApproval; import vip.mate.memory.event.ConversationCompletedEvent; @@ -73,6 +74,7 @@ public class ChatController { @PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter chatStream( @RequestBody ChatStreamRequest request, + @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId, Authentication auth) { String conversationId = request.getConversationId() != null ? request.getConversationId() : "default"; @@ -125,6 +127,26 @@ public class ChatController { String username = auth.getName(); log.info("SSE chat: agentId={}, conversationId={}, user={}", agentId, conversationId, username); + // ---- Workspace 边界校验:确保 agent 属于当前 workspace ---- + if (agentId != null) { + AgentEntity agent = agentService.getAgent(agentId); + if (agent != null && agent.getWorkspaceId() != null) { + long wsId = workspaceId != null ? workspaceId : 1L; + if (!agent.getWorkspaceId().equals(wsId)) { + log.warn("Chat workspace mismatch: agent {} belongs to workspace {}, request workspace {}", + agentId, agent.getWorkspaceId(), wsId); + try { + sendEvent(emitter, "error", Map.of("message", "Agent 不属于当前工作区")); + sendEvent(emitter, "done", Map.of("status", "completed")); + } catch (IOException e) { + log.warn("SSE workspace error send failed: {}", e.getMessage()); + } + emitter.complete(); + return emitter; + } + } + } + // ---- 审批命令拦截:/approve、/deny 走 SSE 流式 replay ---- String normalizedMsg = message.trim().toLowerCase(); boolean isApprovalCommand = "/approve".equals(normalizedMsg) || "approve".equals(normalizedMsg); @@ -382,7 +404,7 @@ public class ChatController { StreamAccumulator accumulator = new StreamAccumulator(); AtomicBoolean finalized = new AtomicBoolean(false); try { - conversationService.getOrCreateConversation(conversationId, agentId, username); + conversationService.getOrCreateConversation(conversationId, agentId, username, workspaceId); List requestParts = normalizeRequestParts(request); String promptText = buildPromptText(message, requestParts); conversationService.saveMessage(conversationId, "user", message, requestParts); @@ -785,13 +807,14 @@ public class ChatController { public R chat( @RequestParam Long agentId, @RequestBody ChatRequest request, + @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId, Authentication auth) { String username = auth != null ? auth.getName() : null; if (username == null) { return R.fail("未登录,请先登录"); } - conversationService.getOrCreateConversation(request.getConversationId(), agentId, username); + conversationService.getOrCreateConversation(request.getConversationId(), agentId, username, workspaceId); conversationService.saveMessage(request.getConversationId(), "user", request.getMessage(), request.getContentParts()); String promptText = buildPromptText(request.getMessage(), request.getContentParts()); diff --git a/mateclaw-server/src/main/java/vip/mate/channel/web/TalkModeWebSocketHandler.java b/mateclaw-server/src/main/java/vip/mate/channel/web/TalkModeWebSocketHandler.java index df06e207..81eda93f 100644 --- a/mateclaw-server/src/main/java/vip/mate/channel/web/TalkModeWebSocketHandler.java +++ b/mateclaw-server/src/main/java/vip/mate/channel/web/TalkModeWebSocketHandler.java @@ -130,9 +130,11 @@ public class TalkModeWebSocketHandler extends AbstractWebSocketHandler { // 3. 推送转写结果 sendJson(session, Map.of("type", "transcript", "text", transcript)); - // 4. 保存用户消息 + // 4. 保存用户消息(workspace 从 agent 获取) + var talkAgent = agentService.getAgent(talkSession.agentId); + Long talkWsId = talkAgent != null ? talkAgent.getWorkspaceId() : 1L; conversationService.getOrCreateConversation( - talkSession.conversationId, talkSession.agentId, talkSession.username); + talkSession.conversationId, talkSession.agentId, talkSession.username, talkWsId); conversationService.saveMessage(talkSession.conversationId, "user", transcript, List.of()); // 5. Agent 对话(同步) diff --git a/mateclaw-server/src/main/java/vip/mate/channel/webchat/WebChatController.java b/mateclaw-server/src/main/java/vip/mate/channel/webchat/WebChatController.java index ffc4b73d..1c80c161 100644 --- a/mateclaw-server/src/main/java/vip/mate/channel/webchat/WebChatController.java +++ b/mateclaw-server/src/main/java/vip/mate/channel/webchat/WebChatController.java @@ -95,8 +95,10 @@ public class WebChatController { sseExecutor.execute(() -> { try { - // 创建或获取会话 - var conv = conversationService.getOrCreateConversation(conversationId, agentId, "webchat:" + visitorId); + // 创建或获取会话(workspace 从 agent 获取) + var webAgent = agentService.getAgent(agentId); + Long webWsId = webAgent != null ? webAgent.getWorkspaceId() : 1L; + var conv = conversationService.getOrCreateConversation(conversationId, agentId, "webchat:" + visitorId, webWsId); // 保存用户消息 conversationService.saveMessage(conversationId, "user", message, List.of()); diff --git a/mateclaw-server/src/main/java/vip/mate/config/AsyncSecurityConfig.java b/mateclaw-server/src/main/java/vip/mate/config/AsyncSecurityConfig.java new file mode 100644 index 00000000..7829fe1f --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/config/AsyncSecurityConfig.java @@ -0,0 +1,33 @@ +package vip.mate.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.AsyncConfigurer; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.security.task.DelegatingSecurityContextAsyncTaskExecutor; + +import java.util.concurrent.Executor; + +/** + * 全局异步线程池配置,确保 SecurityContext 传播到 @Async 线程。 + *

+ * 使用 {@link DelegatingSecurityContextAsyncTaskExecutor} 包装线程池, + * 使得审计、记忆摘要等异步任务能正确获取调用线程的用户身份和权限上下文。 + * + * @author MateClaw Team + */ +@Configuration +@EnableAsync +public class AsyncSecurityConfig implements AsyncConfigurer { + + @Override + public Executor getAsyncExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(4); + executor.setMaxPoolSize(16); + executor.setQueueCapacity(100); + executor.setThreadNamePrefix("async-sec-"); + executor.initialize(); + return new DelegatingSecurityContextAsyncTaskExecutor(executor); + } +} diff --git a/mateclaw-server/src/main/java/vip/mate/cron/controller/CronJobController.java b/mateclaw-server/src/main/java/vip/mate/cron/controller/CronJobController.java index bee74e95..52b3a350 100644 --- a/mateclaw-server/src/main/java/vip/mate/cron/controller/CronJobController.java +++ b/mateclaw-server/src/main/java/vip/mate/cron/controller/CronJobController.java @@ -7,6 +7,7 @@ import org.springframework.web.bind.annotation.*; import vip.mate.common.result.R; import vip.mate.cron.model.CronJobDTO; import vip.mate.cron.service.CronJobService; +import vip.mate.workspace.core.annotation.RequireWorkspaceRole; import java.util.List; @@ -25,30 +26,35 @@ public class CronJobController { @Operation(summary = "获取定时任务列表") @GetMapping + @RequireWorkspaceRole("viewer") public R> list() { return R.ok(cronJobService.list()); } @Operation(summary = "获取定时任务详情") @GetMapping("/{id}") + @RequireWorkspaceRole("viewer") public R get(@PathVariable Long id) { return R.ok(cronJobService.getById(id)); } @Operation(summary = "创建定时任务") @PostMapping + @RequireWorkspaceRole("member") public R create(@RequestBody CronJobDTO dto) { return R.ok(cronJobService.create(dto)); } @Operation(summary = "更新定时任务") @PutMapping("/{id}") + @RequireWorkspaceRole("member") public R update(@PathVariable Long id, @RequestBody CronJobDTO dto) { return R.ok(cronJobService.update(id, dto)); } @Operation(summary = "删除定时任务") @DeleteMapping("/{id}") + @RequireWorkspaceRole("admin") public R delete(@PathVariable Long id) { cronJobService.delete(id); return R.ok(); @@ -56,6 +62,7 @@ public class CronJobController { @Operation(summary = "启用/禁用定时任务") @PutMapping("/{id}/toggle") + @RequireWorkspaceRole("member") public R toggle(@PathVariable Long id, @RequestParam boolean enabled) { cronJobService.toggle(id, enabled); return R.ok(); @@ -63,6 +70,7 @@ public class CronJobController { @Operation(summary = "立即执行定时任务") @PostMapping("/{id}/run") + @RequireWorkspaceRole("member") public R runNow(@PathVariable Long id) { cronJobService.runNow(id); return R.ok(); diff --git a/mateclaw-server/src/main/java/vip/mate/cron/service/CronJobService.java b/mateclaw-server/src/main/java/vip/mate/cron/service/CronJobService.java index 41588276..cf19aad2 100644 --- a/mateclaw-server/src/main/java/vip/mate/cron/service/CronJobService.java +++ b/mateclaw-server/src/main/java/vip/mate/cron/service/CronJobService.java @@ -256,8 +256,10 @@ public class CronJobService implements ApplicationRunner { try { log.info("[CronJob] Executing job {} ({}), type={}", job.getId(), job.getName(), job.getTaskType()); - // 确保会话存在(使用 SYSTEM_USER 作为定时触发的所有者标识) - conversationService.getOrCreateConversation(conversationId, job.getAgentId(), SYSTEM_USER); + // 确保会话存在(使用 SYSTEM_USER 作为定时触发的所有者标识,workspace 从 agent 获取) + AgentEntity cronAgent = agentMapper.selectById(job.getAgentId()); + Long cronWorkspaceId = cronAgent != null ? cronAgent.getWorkspaceId() : 1L; + conversationService.getOrCreateConversation(conversationId, job.getAgentId(), SYSTEM_USER, cronWorkspaceId); String userMessage; String result; diff --git a/mateclaw-server/src/main/java/vip/mate/dashboard/controller/DashboardController.java b/mateclaw-server/src/main/java/vip/mate/dashboard/controller/DashboardController.java index 64f03f26..cd00fa80 100644 --- a/mateclaw-server/src/main/java/vip/mate/dashboard/controller/DashboardController.java +++ b/mateclaw-server/src/main/java/vip/mate/dashboard/controller/DashboardController.java @@ -4,10 +4,15 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; +import vip.mate.agent.AgentService; +import vip.mate.agent.model.AgentEntity; import vip.mate.common.result.R; +import vip.mate.cron.model.CronJobEntity; +import vip.mate.cron.repository.CronJobMapper; import vip.mate.dashboard.model.CronJobRunEntity; import vip.mate.dashboard.service.CronJobRunService; import vip.mate.dashboard.service.DashboardService; +import vip.mate.exception.MateClawException; import vip.mate.workspace.core.annotation.RequireWorkspaceRole; import java.util.List; @@ -26,6 +31,8 @@ public class DashboardController { private final DashboardService dashboardService; private final CronJobRunService cronJobRunService; + private final CronJobMapper cronJobMapper; + private final AgentService agentService; @Operation(summary = "获取概览统计") @GetMapping("/overview") @@ -49,8 +56,19 @@ public class DashboardController { @RequireWorkspaceRole("viewer") public R> cronJobRuns( @PathVariable Long cronJobId, + @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId, @RequestParam(defaultValue = "20") int limit) { - // TODO: 校验 cronJobId 对应的 agent 属于当前 workspace + // 校验 cronJobId 对应的 agent 属于当前 workspace + CronJobEntity job = cronJobMapper.selectById(cronJobId); + if (job != null && job.getAgentId() != null) { + AgentEntity agent = agentService.getAgent(job.getAgentId()); + if (agent != null && agent.getWorkspaceId() != null) { + long wsId = workspaceId != null ? workspaceId : 1L; + if (!agent.getWorkspaceId().equals(wsId)) { + throw new MateClawException("资源不属于当前工作区"); + } + } + } return R.ok(cronJobRunService.listByJobId(cronJobId, Math.min(limit, 100))); } diff --git a/mateclaw-server/src/main/java/vip/mate/dashboard/service/DashboardService.java b/mateclaw-server/src/main/java/vip/mate/dashboard/service/DashboardService.java index 0ada1512..68e054c1 100644 --- a/mateclaw-server/src/main/java/vip/mate/dashboard/service/DashboardService.java +++ b/mateclaw-server/src/main/java/vip/mate/dashboard/service/DashboardService.java @@ -72,13 +72,37 @@ public class DashboardService { } long conversations = conversationMapper.selectCount(convWrapper); - // 消息统计(只统计 assistant 消息的 token) + // Workspace 级消息过滤:通过 conversation 关联 workspace + // MessageEntity 没有 workspaceId 字段,需通过所属 conversation 间接过滤 + List wsConversationIds = null; + if (workspaceId != null) { + List wsConvs = conversationMapper.selectList( + new LambdaQueryWrapper() + .eq(ConversationEntity::getWorkspaceId, workspaceId) + .select(ConversationEntity::getConversationId)); + wsConversationIds = wsConvs.stream() + .map(ConversationEntity::getConversationId).toList(); + if (wsConversationIds.isEmpty()) { + // 该 workspace 无任何对话,直接返回零值 + Map empty = new LinkedHashMap<>(); + empty.put("conversations", conversations); + empty.put("messages", 0L); + empty.put("totalTokens", 0L); + empty.put("promptTokens", 0L); + empty.put("completionTokens", 0L); + empty.put("toolCalls", 0L); + return empty; + } + } + + // 总消息数 LambdaQueryWrapper msgWrapper = new LambdaQueryWrapper() .ge(MessageEntity::getCreateTime, startTime) .le(MessageEntity::getCreateTime, endTime) .eq(MessageEntity::getDeleted, 0); - - // 总消息数 + if (wsConversationIds != null) { + msgWrapper.in(MessageEntity::getConversationId, wsConversationIds); + } long messages = messageMapper.selectCount(msgWrapper); // Token 统计(assistant 消息) @@ -88,6 +112,9 @@ public class DashboardService { .le(MessageEntity::getCreateTime, endTime) .eq(MessageEntity::getDeleted, 0) .select(MessageEntity::getPromptTokens, MessageEntity::getCompletionTokens); + if (wsConversationIds != null) { + tokenWrapper.in(MessageEntity::getConversationId, wsConversationIds); + } List assistantMessages = messageMapper.selectList(tokenWrapper); @@ -106,6 +133,9 @@ public class DashboardService { .ge(MessageEntity::getCreateTime, startTime) .le(MessageEntity::getCreateTime, endTime) .eq(MessageEntity::getDeleted, 0); + if (wsConversationIds != null) { + toolWrapper.in(MessageEntity::getConversationId, wsConversationIds); + } long toolCalls = messageMapper.selectCount(toolWrapper); Map stats = new LinkedHashMap<>(); diff --git a/mateclaw-server/src/main/java/vip/mate/memory/MemoryAutoConfiguration.java b/mateclaw-server/src/main/java/vip/mate/memory/MemoryAutoConfiguration.java index f9354971..096b4743 100644 --- a/mateclaw-server/src/main/java/vip/mate/memory/MemoryAutoConfiguration.java +++ b/mateclaw-server/src/main/java/vip/mate/memory/MemoryAutoConfiguration.java @@ -2,7 +2,6 @@ package vip.mate.memory; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Configuration; -import org.springframework.scheduling.annotation.EnableAsync; /** * 记忆模块自动配置 @@ -10,7 +9,6 @@ import org.springframework.scheduling.annotation.EnableAsync; * @author MateClaw Team */ @Configuration -@EnableAsync @EnableConfigurationProperties(MemoryProperties.class) public class MemoryAutoConfiguration { } diff --git a/mateclaw-server/src/main/java/vip/mate/memory/controller/MemoryController.java b/mateclaw-server/src/main/java/vip/mate/memory/controller/MemoryController.java index b95ebd03..f8491d36 100644 --- a/mateclaw-server/src/main/java/vip/mate/memory/controller/MemoryController.java +++ b/mateclaw-server/src/main/java/vip/mate/memory/controller/MemoryController.java @@ -6,6 +6,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; import vip.mate.common.result.R; +import vip.mate.workspace.core.annotation.RequireWorkspaceRole; import vip.mate.memory.service.MemoryEmergenceService; import vip.mate.memory.service.MemoryRecallService; import vip.mate.memory.service.MemorySummarizationService; @@ -39,6 +40,7 @@ public class MemoryController { @Operation(summary = "手动触发记忆整合(daily notes → MEMORY.md)") @PostMapping("/{agentId}/emergence") + @RequireWorkspaceRole("member") public R> triggerEmergence(@PathVariable Long agentId) { try { emergenceService.consolidate(agentId); @@ -51,6 +53,7 @@ public class MemoryController { @Operation(summary = "手动触发对话记忆提取") @PostMapping("/{agentId}/summarize/{conversationId}") + @RequireWorkspaceRole("member") public R> triggerSummarize( @PathVariable Long agentId, @PathVariable String conversationId) { @@ -68,6 +71,7 @@ public class MemoryController { @Operation(summary = "查询 Dreaming 状态(配置、统计、上次运行时间)") @GetMapping("/{agentId}/dreaming/status") + @RequireWorkspaceRole("viewer") public R> getDreamingStatus(@PathVariable Long agentId) { Map status = recallService.getDreamingStatus(agentId); status.put("lastRunTime", dreamingScheduler.getLastRunTime()); @@ -76,12 +80,14 @@ public class MemoryController { @Operation(summary = "查询召回候选列表(含评分详情)") @GetMapping("/{agentId}/dreaming/candidates") + @RequireWorkspaceRole("viewer") public R>> getDreamingCandidates(@PathVariable Long agentId) { return R.ok(recallService.listCandidatesWithDetails(agentId)); } @Operation(summary = "查询 DREAMS.md 整合日记") @GetMapping("/{agentId}/dreaming/dreams") + @RequireWorkspaceRole("viewer") public R> getDreams(@PathVariable Long agentId) { WorkspaceFileEntity file = workspaceFileService.getFile(agentId, "DREAMS.md"); Map result = new LinkedHashMap<>(); diff --git a/mateclaw-server/src/main/java/vip/mate/skill/workspace/SkillWorkspaceAutoConfiguration.java b/mateclaw-server/src/main/java/vip/mate/skill/workspace/SkillWorkspaceAutoConfiguration.java index 48effab1..796293fc 100644 --- a/mateclaw-server/src/main/java/vip/mate/skill/workspace/SkillWorkspaceAutoConfiguration.java +++ b/mateclaw-server/src/main/java/vip/mate/skill/workspace/SkillWorkspaceAutoConfiguration.java @@ -2,7 +2,6 @@ package vip.mate.skill.workspace; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Configuration; -import org.springframework.scheduling.annotation.EnableAsync; import vip.mate.skill.installer.SkillHubProperties; /** @@ -11,7 +10,6 @@ import vip.mate.skill.installer.SkillHubProperties; * @author MateClaw Team */ @Configuration -@EnableAsync @EnableConfigurationProperties({SkillWorkspaceProperties.class, SkillHubProperties.class}) public class SkillWorkspaceAutoConfiguration { } diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/WikiAutoConfiguration.java b/mateclaw-server/src/main/java/vip/mate/wiki/WikiAutoConfiguration.java index 8757fb61..1954237e 100644 --- a/mateclaw-server/src/main/java/vip/mate/wiki/WikiAutoConfiguration.java +++ b/mateclaw-server/src/main/java/vip/mate/wiki/WikiAutoConfiguration.java @@ -2,7 +2,6 @@ package vip.mate.wiki; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Configuration; -import org.springframework.scheduling.annotation.EnableAsync; /** * Wiki 知识库模块自动配置 @@ -10,7 +9,6 @@ import org.springframework.scheduling.annotation.EnableAsync; * @author MateClaw Team */ @Configuration -@EnableAsync @EnableConfigurationProperties(WikiProperties.class) public class WikiAutoConfiguration { } diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/controller/WikiController.java b/mateclaw-server/src/main/java/vip/mate/wiki/controller/WikiController.java index adda9dc7..a65bb4a8 100644 --- a/mateclaw-server/src/main/java/vip/mate/wiki/controller/WikiController.java +++ b/mateclaw-server/src/main/java/vip/mate/wiki/controller/WikiController.java @@ -7,6 +7,7 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import vip.mate.common.result.R; +import vip.mate.exception.MateClawException; import vip.mate.workspace.core.annotation.RequireWorkspaceRole; import vip.mate.wiki.WikiProperties; import vip.mate.wiki.event.WikiProcessingEvent; @@ -54,16 +55,16 @@ public class WikiController { @GetMapping("/knowledge-bases") public R> listKBs( @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) { - if (workspaceId != null) { - return R.ok(kbService.listByWorkspace(workspaceId)); - } - return R.ok(kbService.listAll()); + long wsId = workspaceId != null ? workspaceId : 1L; + return R.ok(kbService.listByWorkspace(wsId)); } @RequireWorkspaceRole("viewer") @Operation(summary = "获取知识库详情") @GetMapping("/knowledge-bases/{id}") - public R getKB(@PathVariable Long id) { + public R getKB(@PathVariable Long id, + @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) { + verifyKBWorkspace(id, workspaceId); WikiKnowledgeBaseEntity kb = kbService.getById(id); if (kb == null) return R.fail("Knowledge base not found"); return R.ok(kb); @@ -72,8 +73,14 @@ public class WikiController { @RequireWorkspaceRole("viewer") @Operation(summary = "按 Agent 获取知识库") @GetMapping("/knowledge-bases/agent/{agentId}") - public R> listKBsByAgent(@PathVariable Long agentId) { - return R.ok(kbService.listByAgentId(agentId)); + public R> listKBsByAgent(@PathVariable Long agentId, + @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) { + long wsId = workspaceId != null ? workspaceId : 1L; + // 按 agent 查询后,过滤出属于当前 workspace 的知识库 + List kbs = kbService.listByAgentId(agentId); + return R.ok(kbs.stream() + .filter(kb -> kb.getWorkspaceId() == null || kb.getWorkspaceId().equals(wsId)) + .toList()); } @RequireWorkspaceRole("member") @@ -84,19 +91,17 @@ public class WikiController { String name = (String) body.get("name"); String description = (String) body.get("description"); Long agentId = body.get("agentId") != null ? Long.valueOf(body.get("agentId").toString()) : null; - WikiKnowledgeBaseEntity kb = kbService.create(name, description, agentId); - // 注入 workspace_id(create 方法内部不感知 workspace,需要在 controller 层补充) - if (kb.getWorkspaceId() == null || kb.getWorkspaceId() == 0) { - kb.setWorkspaceId(workspaceId != null ? workspaceId : 1L); - kbService.updateWorkspaceId(kb.getId(), kb.getWorkspaceId()); - } + long wsId = workspaceId != null ? workspaceId : 1L; + WikiKnowledgeBaseEntity kb = kbService.create(name, description, agentId, wsId); return R.ok(kb); } @RequireWorkspaceRole("member") @Operation(summary = "更新知识库") @PutMapping("/knowledge-bases/{id}") - public R updateKB(@PathVariable Long id, @RequestBody Map body) { + public R updateKB(@PathVariable Long id, @RequestBody Map body, + @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) { + verifyKBWorkspace(id, workspaceId); String name = (String) body.get("name"); String description = (String) body.get("description"); Long agentId = body.get("agentId") != null ? Long.valueOf(body.get("agentId").toString()) : null; @@ -106,7 +111,9 @@ public class WikiController { @RequireWorkspaceRole("admin") @Operation(summary = "删除知识库") @DeleteMapping("/knowledge-bases/{id}") - public R deleteKB(@PathVariable Long id) { + public R deleteKB(@PathVariable Long id, + @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) { + verifyKBWorkspace(id, workspaceId); kbService.delete(id); return R.ok(); } @@ -114,7 +121,9 @@ public class WikiController { @RequireWorkspaceRole("viewer") @Operation(summary = "获取知识库配置") @GetMapping("/knowledge-bases/{id}/config") - public R> getConfig(@PathVariable Long id) { + public R> getConfig(@PathVariable Long id, + @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) { + verifyKBWorkspace(id, workspaceId); WikiKnowledgeBaseEntity kb = kbService.getById(id); if (kb == null) return R.fail("Knowledge base not found"); return R.ok(Map.of("content", kb.getConfigContent() != null ? kb.getConfigContent() : "")); @@ -123,7 +132,9 @@ public class WikiController { @RequireWorkspaceRole("member") @Operation(summary = "更新知识库配置") @PutMapping("/knowledge-bases/{id}/config") - public R updateConfig(@PathVariable Long id, @RequestBody Map body) { + public R updateConfig(@PathVariable Long id, @RequestBody Map body, + @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) { + verifyKBWorkspace(id, workspaceId); kbService.updateConfig(id, body.get("content")); return R.ok(); } @@ -133,7 +144,9 @@ public class WikiController { @RequireWorkspaceRole("member") @Operation(summary = "设置知识库关联目录") @PutMapping("/knowledge-bases/{id}/source-directory") - public R setSourceDirectory(@PathVariable Long id, @RequestBody Map body) { + public R setSourceDirectory(@PathVariable Long id, @RequestBody Map body, + @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) { + verifyKBWorkspace(id, workspaceId); String path = body.get("path"); kbService.updateSourceDirectory(id, path); return R.ok(); @@ -142,7 +155,9 @@ public class WikiController { @RequireWorkspaceRole("member") @Operation(summary = "扫描关联目录导入文件") @PostMapping("/knowledge-bases/{id}/scan") - public R> scanDirectory(@PathVariable Long id) { + public R> scanDirectory(@PathVariable Long id, + @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) { + verifyKBWorkspace(id, workspaceId); WikiDirectoryScanService.ScanResult result = scanService.scan(id); Map response = new LinkedHashMap<>(); response.put("scanned", result.scanned()); @@ -157,14 +172,18 @@ public class WikiController { @RequireWorkspaceRole("viewer") @Operation(summary = "获取原始材料列表") @GetMapping("/knowledge-bases/{kbId}/raw") - public R> listRaw(@PathVariable Long kbId) { + public R> listRaw(@PathVariable Long kbId, + @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) { + verifyKBWorkspace(kbId, workspaceId); return R.ok(rawService.listByKbId(kbId)); } @RequireWorkspaceRole("member") @Operation(summary = "添加文本材料") @PostMapping("/knowledge-bases/{kbId}/raw/text") - public R addRawText(@PathVariable Long kbId, @RequestBody Map body) { + public R addRawText(@PathVariable Long kbId, @RequestBody Map body, + @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) { + verifyKBWorkspace(kbId, workspaceId); String title = body.get("title"); String content = body.get("content"); return R.ok(rawService.addText(kbId, title, content)); @@ -174,7 +193,9 @@ public class WikiController { @Operation(summary = "上传文件材料") @PostMapping("/knowledge-bases/{kbId}/raw/upload") public R uploadRaw(@PathVariable Long kbId, - @RequestParam("file") MultipartFile file) throws IOException { + @RequestParam("file") MultipartFile file, + @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) throws IOException { + verifyKBWorkspace(kbId, workspaceId); String originalName = file.getOriginalFilename(); String extension = originalName != null && originalName.contains(".") ? originalName.substring(originalName.lastIndexOf(".") + 1).toLowerCase() @@ -206,7 +227,9 @@ public class WikiController { @RequireWorkspaceRole("admin") @Operation(summary = "删除原始材料") @DeleteMapping("/knowledge-bases/{kbId}/raw/{rawId}") - public R deleteRaw(@PathVariable Long kbId, @PathVariable Long rawId) { + public R deleteRaw(@PathVariable Long kbId, @PathVariable Long rawId, + @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) { + verifyKBWorkspace(kbId, workspaceId); WikiRawMaterialEntity raw = rawService.getById(rawId); if (raw == null || !kbId.equals(raw.getKbId())) { return R.fail("Raw material not found in this knowledge base"); @@ -219,7 +242,9 @@ public class WikiController { @RequireWorkspaceRole("member") @Operation(summary = "重新处理原始材料") @PostMapping("/knowledge-bases/{kbId}/raw/{rawId}/reprocess") - public R reprocessRaw(@PathVariable Long kbId, @PathVariable Long rawId) { + public R reprocessRaw(@PathVariable Long kbId, @PathVariable Long rawId, + @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) { + verifyKBWorkspace(kbId, workspaceId); WikiRawMaterialEntity raw = rawService.getById(rawId); if (raw == null || !kbId.equals(raw.getKbId())) { return R.fail("Raw material not found in this knowledge base"); @@ -233,14 +258,18 @@ public class WikiController { @RequireWorkspaceRole("viewer") @Operation(summary = "获取 Wiki 页面列表") @GetMapping("/knowledge-bases/{kbId}/pages") - public R> listPages(@PathVariable Long kbId) { + public R> listPages(@PathVariable Long kbId, + @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) { + verifyKBWorkspace(kbId, workspaceId); return R.ok(pageService.listByKbId(kbId)); } @RequireWorkspaceRole("viewer") @Operation(summary = "获取 Wiki 页面内容") @GetMapping("/knowledge-bases/{kbId}/pages/{slug}") - public R getPage(@PathVariable Long kbId, @PathVariable String slug) { + public R getPage(@PathVariable Long kbId, @PathVariable String slug, + @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) { + verifyKBWorkspace(kbId, workspaceId); WikiPageEntity page = pageService.getBySlug(kbId, slug); if (page == null) return R.fail("Page not found"); return R.ok(page); @@ -250,14 +279,18 @@ public class WikiController { @Operation(summary = "手动编辑 Wiki 页面") @PutMapping("/knowledge-bases/{kbId}/pages/{slug}") public R updatePage(@PathVariable Long kbId, @PathVariable String slug, - @RequestBody Map body) { + @RequestBody Map body, + @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) { + verifyKBWorkspace(kbId, workspaceId); return R.ok(pageService.updatePageManually(kbId, slug, body.get("content"), body.get("summary"))); } @RequireWorkspaceRole("admin") @Operation(summary = "删除 Wiki 页面") @DeleteMapping("/knowledge-bases/{kbId}/pages/{slug}") - public R deletePage(@PathVariable Long kbId, @PathVariable String slug) { + public R deletePage(@PathVariable Long kbId, @PathVariable String slug, + @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) { + verifyKBWorkspace(kbId, workspaceId); pageService.delete(kbId, slug); kbService.setPageCount(kbId, pageService.countByKbId(kbId)); return R.ok(); @@ -266,7 +299,9 @@ public class WikiController { @RequireWorkspaceRole("viewer") @Operation(summary = "获取反向链接") @GetMapping("/knowledge-bases/{kbId}/pages/{slug}/backlinks") - public R> getBacklinks(@PathVariable Long kbId, @PathVariable String slug) { + public R> getBacklinks(@PathVariable Long kbId, @PathVariable String slug, + @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) { + verifyKBWorkspace(kbId, workspaceId); return R.ok(pageService.getBacklinks(kbId, slug)); } @@ -275,7 +310,9 @@ public class WikiController { @RequireWorkspaceRole("member") @Operation(summary = "触发知识库处理(异步)") @PostMapping("/knowledge-bases/{kbId}/process") - public R> processKB(@PathVariable Long kbId) { + public R> processKB(@PathVariable Long kbId, + @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) { + verifyKBWorkspace(kbId, workspaceId); List pending = rawService.listPending(kbId); for (WikiRawMaterialEntity raw : pending) { eventPublisher.publishEvent(new WikiProcessingEvent(this, raw.getId(), kbId)); @@ -286,7 +323,9 @@ public class WikiController { @RequireWorkspaceRole("viewer") @Operation(summary = "获取处理状态") @GetMapping("/knowledge-bases/{kbId}/processing-status") - public R> getProcessingStatus(@PathVariable Long kbId) { + public R> getProcessingStatus(@PathVariable Long kbId, + @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) { + verifyKBWorkspace(kbId, workspaceId); WikiKnowledgeBaseEntity kb = kbService.getById(kbId); if (kb == null) return R.fail("Knowledge base not found"); @@ -306,4 +345,17 @@ public class WikiController { "totalPages", kb.getPageCount() )); } + + // ==================== Workspace Verification ==================== + + private void verifyKBWorkspace(Long kbId, Long headerWorkspaceId) { + WikiKnowledgeBaseEntity kb = kbService.getById(kbId); + if (kb == null) { + throw new MateClawException("Knowledge base not found"); + } + long wsId = headerWorkspaceId != null ? headerWorkspaceId : 1L; + if (kb.getWorkspaceId() != null && !kb.getWorkspaceId().equals(wsId)) { + throw new MateClawException("资源不属于当前工作区"); + } + } } diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiKnowledgeBaseService.java b/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiKnowledgeBaseService.java index 54c120fa..8e34e3bb 100644 --- a/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiKnowledgeBaseService.java +++ b/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiKnowledgeBaseService.java @@ -79,16 +79,22 @@ public class WikiKnowledgeBaseService { @Transactional public WikiKnowledgeBaseEntity create(String name, String description, Long agentId) { + return create(name, description, agentId, 1L); + } + + @Transactional + public WikiKnowledgeBaseEntity create(String name, String description, Long agentId, Long workspaceId) { WikiKnowledgeBaseEntity entity = new WikiKnowledgeBaseEntity(); entity.setName(name); entity.setDescription(description); entity.setAgentId(agentId); + entity.setWorkspaceId(workspaceId); entity.setConfigContent(DEFAULT_CONFIG); entity.setStatus("active"); entity.setPageCount(0); entity.setRawCount(0); kbMapper.insert(entity); - log.info("[Wiki] Knowledge base created: id={}, name={}", entity.getId(), name); + log.info("[Wiki] Knowledge base created: id={}, name={}, workspaceId={}", entity.getId(), name, workspaceId); return entity; } diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/tool/WikiTool.java b/mateclaw-server/src/main/java/vip/mate/wiki/tool/WikiTool.java index 7b74cbb6..c0c37bf6 100644 --- a/mateclaw-server/src/main/java/vip/mate/wiki/tool/WikiTool.java +++ b/mateclaw-server/src/main/java/vip/mate/wiki/tool/WikiTool.java @@ -233,10 +233,6 @@ public class WikiTool { */ private Long resolveKbId(Long agentId) { List kbs = kbService.listByAgentId(agentId); - if (kbs.isEmpty()) { - // agentId 为 null 时也尝试查公共 KB - kbs = kbService.listAll(); - } return kbs.isEmpty() ? null : kbs.get(0).getId(); } diff --git a/mateclaw-server/src/main/java/vip/mate/workspace/conversation/ConversationService.java b/mateclaw-server/src/main/java/vip/mate/workspace/conversation/ConversationService.java index 07f9d2b1..92d45826 100644 --- a/mateclaw-server/src/main/java/vip/mate/workspace/conversation/ConversationService.java +++ b/mateclaw-server/src/main/java/vip/mate/workspace/conversation/ConversationService.java @@ -96,10 +96,19 @@ public class ConversationService { } /** - * 获取或创建会话 + * 获取或创建会话(向后兼容,默认 workspace 1) */ @Transactional public ConversationEntity getOrCreateConversation(String conversationId, Long agentId, String username) { + return getOrCreateConversation(conversationId, agentId, username, 1L); + } + + /** + * 获取或创建会话(workspace 感知) + */ + @Transactional + public ConversationEntity getOrCreateConversation(String conversationId, Long agentId, + String username, Long workspaceId) { ConversationEntity conv = conversationMapper.selectOne(new LambdaQueryWrapper() .eq(ConversationEntity::getConversationId, conversationId)); if (conv == null) { @@ -107,6 +116,7 @@ public class ConversationService { conv.setConversationId(conversationId); conv.setAgentId(agentId); conv.setUsername(username != null ? username : "anonymous"); + conv.setWorkspaceId(workspaceId != null ? workspaceId : 1L); conv.setTitle("新对话"); conv.setMessageCount(0); conv.setLastActiveTime(LocalDateTime.now()); @@ -126,6 +136,14 @@ public class ConversationService { */ @Transactional public ConversationEntity getOrCreateSharedConversation(String conversationId, Long agentId) { + return getOrCreateSharedConversation(conversationId, agentId, null); + } + + /** + * 获取或创建共享渠道会话(workspace 感知) + */ + @Transactional + public ConversationEntity getOrCreateSharedConversation(String conversationId, Long agentId, Long workspaceId) { ConversationEntity conv = conversationMapper.selectOne(new LambdaQueryWrapper() .eq(ConversationEntity::getConversationId, conversationId)); if (conv == null) { @@ -133,6 +151,7 @@ public class ConversationService { conv.setConversationId(conversationId); conv.setAgentId(agentId); conv.setUsername(SYSTEM_USER); + conv.setWorkspaceId(workspaceId != null ? workspaceId : 1L); conv.setTitle("新对话"); conv.setMessageCount(0); conv.setLastActiveTime(LocalDateTime.now()); diff --git a/mateclaw-ui/src/api/index.ts b/mateclaw-ui/src/api/index.ts index 7cc154cc..54a174d2 100644 --- a/mateclaw-ui/src/api/index.ts +++ b/mateclaw-ui/src/api/index.ts @@ -280,7 +280,7 @@ export const settingsApi = { const encodeFilePath = (filename: string) => filename.split('/').map(encodeURIComponent).join('/') -export const workspaceApi = { +export const agentContextApi = { listFiles: (agentId: string | number) => http.get(`/agents/${agentId}/workspace/files`), getFile: (agentId: string | number, filename: string) => diff --git a/mateclaw-ui/src/assets/main.css b/mateclaw-ui/src/assets/main.css index d5951511..87b2ff23 100644 --- a/mateclaw-ui/src/assets/main.css +++ b/mateclaw-ui/src/assets/main.css @@ -1,30 +1,45 @@ @import "tailwindcss"; /* ================================================================ - CSS Variables — Light / Dark theme (Terracotta Earth-Tone) + CSS Variables — Light / Dark theme ================================================================ */ :root { /* brand */ - --mc-primary: #D97757; - --mc-primary-light: #E08860; - --mc-primary-hover: #C1572B; - --mc-primary-bg: #F5E4D8; + --mc-primary: #d96d46; + --mc-primary-light: #ebb08f; + --mc-primary-hover: #bb4f27; + --mc-primary-bg: #f6e2d7; + --mc-accent: #184a45; + --mc-accent-soft: #dce8e4; /* surfaces */ - --mc-bg: #FAFAF8; + --mc-bg: #f6f1ea; --mc-bg-elevated: #ffffff; - --mc-bg-sunken: #EDE8E3; + --mc-bg-sunken: #ebe3db; + --mc-bg-muted: #f1e8df; + --mc-surface-strong: #fdfaf6; + --mc-surface-overlay: rgba(255, 255, 255, 0.72); + --mc-panel-top: rgba(255, 255, 255, 0.94); + --mc-panel-bottom: rgba(255, 252, 248, 0.9); + --mc-panel-raised: rgba(255, 255, 255, 0.86); /* borders */ - --mc-border: #DDD5CC; - --mc-border-light: #EDE8E3; + --mc-border: #d9cec2; + --mc-border-light: #ebe3db; + --mc-border-strong: #cdbdad; /* text */ - --mc-text-primary: #1C1410; - --mc-text-secondary: #6B5344; - --mc-text-tertiary: #A08070; + --mc-text-primary: #1d1612; + --mc-text-secondary: #665245; + --mc-text-tertiary: #9b7d6c; --mc-text-inverse: #ffffff; + /* shadows */ + --mc-shadow-soft: 0 10px 30px rgba(58, 32, 19, 0.08); + --mc-shadow-medium: 0 18px 48px rgba(58, 32, 19, 0.12); + --mc-shadow-strong: 0 24px 70px rgba(58, 32, 19, 0.16); + --mc-glow: radial-gradient(circle at top, rgba(217, 109, 70, 0.18), transparent 55%); + /* sidebar */ --mc-sidebar-bg: #ffffff; --mc-sidebar-border: #DDD5CC; @@ -34,6 +49,8 @@ --mc-sidebar-text-active: #D97757; --mc-sidebar-group-title: #A08070; --mc-sidebar-logo-name: #1C1410; + --mc-sidebar-footer-bg: rgba(245, 238, 230, 0.82); + --mc-sidebar-floating-bg: rgba(255, 252, 248, 0.95); /* chat */ --mc-chat-bg: #FAFAF8; @@ -97,19 +114,28 @@ html.dark { /* brand (lighter in dark bg context) */ - --mc-primary: #E08860; - --mc-primary-light: #F0C4A0; - --mc-primary-hover: #D97757; + --mc-primary: #eb8f65; + --mc-primary-light: #f1c3ab; + --mc-primary-hover: #d96d46; --mc-primary-bg: rgba(224, 136, 96, 0.15); + --mc-accent: #5ca69d; + --mc-accent-soft: rgba(92, 166, 157, 0.12); /* surfaces */ - --mc-bg: #1A1410; - --mc-bg-elevated: #231C17; - --mc-bg-sunken: #2A211C; + --mc-bg: #17110e; + --mc-bg-elevated: #221a16; + --mc-bg-sunken: #2a211c; + --mc-bg-muted: #201813; + --mc-surface-strong: #2a201a; + --mc-surface-overlay: rgba(34, 26, 22, 0.78); + --mc-panel-top: rgba(36, 28, 24, 0.96); + --mc-panel-bottom: rgba(27, 21, 18, 0.94); + --mc-panel-raised: rgba(42, 32, 26, 0.9); /* borders */ --mc-border: #3D3028; --mc-border-light: #2E2420; + --mc-border-strong: #4a392f; /* text */ --mc-text-primary: #F0EAE4; @@ -117,6 +143,12 @@ html.dark { --mc-text-tertiary: #8A7060; --mc-text-inverse: #1C1410; + /* shadows */ + --mc-shadow-soft: 0 14px 32px rgba(0, 0, 0, 0.22); + --mc-shadow-medium: 0 20px 60px rgba(0, 0, 0, 0.28); + --mc-shadow-strong: 0 28px 90px rgba(0, 0, 0, 0.35); + --mc-glow: radial-gradient(circle at top, rgba(235, 143, 101, 0.12), transparent 55%); + /* sidebar */ --mc-sidebar-bg: #1A1410; --mc-sidebar-border: #3D3028; @@ -126,6 +158,8 @@ html.dark { --mc-sidebar-text-active: #E08860; --mc-sidebar-group-title: #8A7060; --mc-sidebar-logo-name: #F0EAE4; + --mc-sidebar-footer-bg: rgba(34, 26, 22, 0.9); + --mc-sidebar-floating-bg: rgba(36, 28, 24, 0.96); /* chat */ --mc-chat-bg: #1A1410; @@ -189,12 +223,48 @@ html.dark { ================================================================ */ * { box-sizing: border-box; margin: 0; padding: 0; } +html, body, #app { + min-height: 100%; +} + body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', 'Microsoft YaHei', sans-serif; + font-family: 'Avenir Next', 'SF Pro Display', 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif; background-color: var(--mc-bg); color: var(--mc-text-primary); -webkit-font-smoothing: antialiased; transition: background-color 0.2s ease, color 0.2s ease; + background-image: + radial-gradient(circle at top left, rgba(217, 109, 70, 0.1), transparent 26%), + radial-gradient(circle at bottom right, rgba(24, 74, 69, 0.08), transparent 24%); +} + +html.dark body { + background-image: + radial-gradient(circle at top left, rgba(235, 143, 101, 0.14), transparent 28%), + radial-gradient(circle at bottom right, rgba(92, 166, 157, 0.08), transparent 26%); +} + +body::before { + content: ''; + position: fixed; + inset: 0; + pointer-events: none; + background-image: + linear-gradient(rgba(255, 255, 255, 0.015) 1px, transparent 1px), + linear-gradient(90deg, rgba(255, 255, 255, 0.015) 1px, transparent 1px); + background-size: 24px 24px; + opacity: 0.35; + mix-blend-mode: soft-light; +} + +html.dark body::before { + opacity: 0.18; + mix-blend-mode: normal; +} + +::selection { + background: var(--mc-primary-bg); + color: var(--mc-text-primary); } /* ================================================================ @@ -320,6 +390,106 @@ body { font-size: 13px; } +/* ================================================================ + Shared page shell + ================================================================ */ +.mc-page-shell { + height: 100%; + overflow-y: auto; + padding: 28px; +} + +.mc-page-frame { + position: relative; + border: 1px solid var(--mc-border); + background: var(--mc-surface-overlay); + backdrop-filter: blur(18px); + border-radius: 28px; + box-shadow: var(--mc-shadow-soft); +} + +.mc-page-frame::before { + content: ''; + position: absolute; + inset: 0; + border-radius: inherit; + background: var(--mc-glow); + opacity: 0.8; + pointer-events: none; +} + +.mc-page-inner { + position: relative; + z-index: 1; + padding: 28px; +} + +.mc-page-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 18px; + margin-bottom: 28px; +} + +.mc-page-kicker { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 6px 10px; + border-radius: 999px; + background: var(--mc-bg-muted); + border: 1px solid var(--mc-border-light); + color: var(--mc-accent); + font-size: 11px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + margin-bottom: 12px; +} + +.mc-page-title { + font-size: clamp(26px, 3vw, 40px); + line-height: 1; + font-weight: 800; + letter-spacing: -0.04em; + color: var(--mc-text-primary); + margin: 0 0 10px; +} + +.mc-page-desc { + max-width: 760px; + color: var(--mc-text-secondary); + font-size: 15px; + line-height: 1.7; +} + +.mc-surface-card { + border: 1px solid var(--mc-border-light); + background: linear-gradient(180deg, rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0.92)); + border-radius: 22px; + box-shadow: var(--mc-shadow-soft); +} + +html.dark .mc-surface-card { + background: linear-gradient(180deg, rgba(42, 32, 26, 0.86), rgba(34, 26, 22, 0.96)); +} + +@media (max-width: 900px) { + .mc-page-shell { + padding: 16px; + } + + .mc-page-inner { + padding: 18px; + } + + .mc-page-header { + flex-direction: column; + align-items: stretch; + } +} + /* highlight.js token colors (One Dark inspired — works on dark bg) */ .hljs-keyword, .hljs-selector-tag, .hljs-built_in, .hljs-literal { color: #c678dd; } .hljs-string, .hljs-attr { color: #98c379; } diff --git a/mateclaw-ui/src/components/workspace/WorkspaceSwitcher.vue b/mateclaw-ui/src/components/workspace/WorkspaceSwitcher.vue index 65c96880..7896cb24 100644 --- a/mateclaw-ui/src/components/workspace/WorkspaceSwitcher.vue +++ b/mateclaw-ui/src/components/workspace/WorkspaceSwitcher.vue @@ -35,6 +35,14 @@ {{ ws.name }} + + + + + + + Manage Workspaces + @@ -43,6 +51,7 @@ diff --git a/mateclaw-ui/src/composables/chat/useChat.ts b/mateclaw-ui/src/composables/chat/useChat.ts index a40dee63..61fbd58a 100644 --- a/mateclaw-ui/src/composables/chat/useChat.ts +++ b/mateclaw-ui/src/composables/chat/useChat.ts @@ -152,10 +152,18 @@ export function useChat(options: UseChatOptions): UseChatReturn { // 消息队列 const messageQueue = useMessageQueue() - // 流连接 + // 流连接(注入 auth + workspace header,与 axios interceptor 保持一致) + const streamHeaders: Record = {} + if (token) { + streamHeaders['Authorization'] = `Bearer ${token}` + } + const wsId = localStorage.getItem('mc-workspace-id') + if (wsId) { + streamHeaders['X-Workspace-Id'] = wsId + } const stream = useStream({ url: `${baseUrl}/api/v1/chat/stream`, - headers: token ? { Authorization: `Bearer ${token}` } : {}, + headers: streamHeaders, }) // ===== SSE 事件处理器 ===== diff --git a/mateclaw-ui/src/i18n/locales/en-US.ts b/mateclaw-ui/src/i18n/locales/en-US.ts index d5989814..efa0d303 100644 --- a/mateclaw-ui/src/i18n/locales/en-US.ts +++ b/mateclaw-ui/src/i18n/locales/en-US.ts @@ -186,6 +186,7 @@ export default { sessions: 'Sessions', agent: 'Agent', workspace: 'Workspace', + agentContext: 'Agent Context', skills: 'Skills', wiki: 'Wiki KB', tools: 'Tools', @@ -204,6 +205,9 @@ export default { themeLight: 'Light', themeDark: 'Dark', themeSystem: 'System', + themeLabel: 'Appearance', + languageLabel: 'Language', + appearance: 'Appearance & Language', roleUser: 'User', roleAdmin: 'Admin', }, @@ -236,6 +240,44 @@ export default { systemDesc: 'Language and runtime behavior settings', aboutTitle: 'About MateClaw', aboutDesc: 'Version and system information', + about: { + heroKicker: 'Product', + heroTitle: 'MateClaw is not a chat window. It is an AI operating system for ongoing work.', + heroDesc: 'The point is not to generate a few more answers. The point is to place models inside a continuous loop of context, memory, execution, knowledge, and delivery so the product behaves like a real system.', + manifestoKicker: 'Why It Exists', + manifestoTitle: 'This page should not stop at a version number. It should explain what this product is trying to become.', + manifestoDesc: 'MateClaw is not defined by how many features it exposes. It is defined by how tightly it pulls context, tools, memory, and output into one operating surface that can keep pace with real work.', + systemKicker: 'System Shape', + systemTitle: 'A valuable product is not a pile of capabilities. It is a set of capabilities that lock together.', + systemDesc: 'Agents, Wiki, Memory, Channels, Workspace, and governance are not parallel modules. They are supposed to form one system that users can understand, trust, and extend.', + foundationKicker: 'Foundation', + foundationTitle: 'The experience needs a backbone, and the backbone has to be strong.', + foundationDesc: 'The stack matters only if it can support a stable runtime, extensible agentic patterns, and boundaries that hold up under real work.', + pillars: { + contextTitle: 'Keep context continuous', + contextDesc: 'Models, knowledge, workspace, and the active task should live inside the same frame instead of starting over every turn.', + executionTitle: 'Make execution part of the product', + executionDesc: 'Browser actions, tools, channels, and async tasks should not feel bolted on. They need to be the real execution surface.', + memoryTitle: 'Make memory compound', + memoryDesc: 'Memory should not just be stored. It should be shaped, recalled, and turned into leverage for the next task.', + }, + manifestoItems: { + runtimeTitle: 'It is a runtime before it is a feature list', + runtimeDesc: 'Strong products solve runtime coherence first: context, permissions, state, and outcomes need to speak the same language.', + knowledgeTitle: 'It must know how to organize knowledge', + knowledgeDesc: 'Wiki and memory matter because they let the system learn what is worth preserving and what should be available again later.', + multimodalTitle: 'It should handle multimodal work naturally', + multimodalDesc: 'Voice, image, video, and text should feel like one expression and execution system, not separate tricks.', + }, + systemItems: { + workspaceTitle: 'Workspace is a boundary, not a decoration', + workspaceDesc: 'Every resource, state transition, and execution path needs to know which workspace it belongs to. Platform boundaries must be real.', + governanceTitle: 'Governance belongs in the core', + governanceDesc: 'Audit, approvals, guards, activity, and observability are not back-office extras. They define whether users trust the system.', + deliveryTitle: 'Delivery matters more than answers', + deliveryDesc: 'A mature AI product should not stop at generating a response. It should keep moving the task, expose state, and leave useful results behind.', + }, + }, model: { title: 'Model Management', desc: 'Configure model providers, credentials, and model lists', @@ -499,9 +541,9 @@ export default { enUS: 'English', }, }, - workspace: { - title: 'Workspace', - desc: 'Manage Agent Markdown system prompt files', + agentContext: { + title: 'Agent Context', + desc: 'Manage prompt files and memory for your agents', selectAgent: 'Select Agent', noAgent: 'Please select an agent first', files: 'Files', @@ -608,6 +650,7 @@ export default { fileGuard: 'File Guard', auditLogs: 'Audit Logs', members: 'Members', + workspaces: 'Workspaces', activity: 'Activity', }, activity: { @@ -665,6 +708,51 @@ export default { removeFailed: 'Failed to remove member', }, }, + workspaces: { + title: 'Workspaces', + desc: 'Manage workspaces for your organization.', + newWorkspace: 'New Workspace', + loading: 'Loading workspaces...', + noWorkspaces: 'No workspaces found.', + current: 'Current', + columns: { + name: 'Name', + slug: 'Slug', + description: 'Description', + created: 'Created', + actions: 'Actions', + }, + createDialog: { + title: 'New Workspace', + name: 'Name', + namePlaceholder: 'e.g. Engineering', + slug: 'Slug', + slugPlaceholder: 'e.g. engineering', + slugHint: 'Slug cannot be changed after creation.', + description: 'Description', + descriptionPlaceholder: 'Optional description', + }, + editDialog: { + title: 'Edit Workspace', + }, + deleteDialog: { + title: 'Delete Workspace', + confirm: 'Are you sure you want to delete {name}? This will remove all resources associated with this workspace.', + }, + actions: { + edit: 'Edit', + delete: 'Delete', + cancel: 'Cancel', + create: 'Create', + save: 'Save', + }, + messages: { + saveSuccess: 'Workspace saved successfully', + saveFailed: 'Failed to save workspace', + deleteSuccess: 'Workspace deleted', + deleteFailed: 'Failed to delete workspace', + }, + }, toolGuard: { title: 'Tool Guard', desc: 'Manage tool invocation security rules and global guard settings', diff --git a/mateclaw-ui/src/i18n/locales/zh-CN.ts b/mateclaw-ui/src/i18n/locales/zh-CN.ts index f4a765ec..875434fd 100644 --- a/mateclaw-ui/src/i18n/locales/zh-CN.ts +++ b/mateclaw-ui/src/i18n/locales/zh-CN.ts @@ -189,6 +189,7 @@ export default { system: '系统', agent: '智能体', workspace: '工作区', + agentContext: 'Agent 上下文', skills: '技能', wiki: 'Wiki 知识库', tools: '工具', @@ -204,6 +205,9 @@ export default { themeLight: '浅色', themeDark: '深色', themeSystem: '跟随系统', + themeLabel: '外观', + languageLabel: '语言', + appearance: '外观与语言', roleUser: '用户', roleAdmin: '管理员', }, @@ -226,6 +230,44 @@ export default { systemDesc: '语言与运行行为配置', aboutTitle: '关于 MateClaw', aboutDesc: '版本与系统信息', + about: { + heroKicker: 'Product', + heroTitle: 'MateClaw 不是一个聊天窗口,而是一套持续运转的 AI 工作系统。', + heroDesc: '它的目标不是帮你多生成几段文本,而是让模型真正进入任务、知识、记忆、执行和协作的连续流程里,像一个系统一样稳定工作。', + manifestoKicker: 'Why It Exists', + manifestoTitle: '这页不该只是版本号,它应该回答这个产品到底想成为什么。', + manifestoDesc: 'MateClaw 的核心不是“功能很多”,而是把上下文、工具、记忆和交付收拢成一个统一运行面,让 AI 能持续理解你正在做什么,并把结果带回来。', + systemKicker: 'System Shape', + systemTitle: '真正有价值的产品,不是把能力摆满,而是让能力彼此咬合。', + systemDesc: 'Agent、Wiki、Memory、Channels、Workspace 和治理能力不是平行堆叠的模块,它们应该共同构成一个可被理解、可被信任、可被扩展的系统。', + foundationKicker: 'Foundation', + foundationTitle: '体验必须有骨架,骨架必须足够硬。', + foundationDesc: '底层技术栈的价值不在于名字响亮,而在于它们能否支撑稳定的 runtime、可扩展的 agentic patterns,以及面向真实工作的产品边界。', + pillars: { + contextTitle: '让上下文保持连续', + contextDesc: '把模型、知识、工作区和当前任务放进同一个语境里,而不是每次重新开始。', + executionTitle: '让执行成为产品的一部分', + executionDesc: 'Browser、tools、channels 与异步任务不应是外挂,它们必须成为系统的真实执行面。', + memoryTitle: '让记忆真正产生复利', + memoryDesc: '记忆不只是被保存,而是被整理、被召回,并在下一次任务里变得更有价值。', + }, + manifestoItems: { + runtimeTitle: '它首先是一套 runtime', + runtimeDesc: '任何强产品都必须先解决运行时的一致性:上下文、权限、状态和结果必须说同一种语言。', + knowledgeTitle: '它必须会组织知识', + knowledgeDesc: 'Wiki 与记忆系统的意义,不是多一个存储区,而是让系统逐渐知道什么值得保留、什么值得再次使用。', + multimodalTitle: '它应该自然处理多模态', + multimodalDesc: '语音、图片、视频与文本不能彼此割裂,用户看到的应该是一套统一的表达与执行能力。', + }, + systemItems: { + workspaceTitle: 'Workspace 是边界,不是装饰', + workspaceDesc: '所有资源、状态与执行路径都必须知道自己属于哪个工作区,平台边界要真实,而不是靠页面暗示。', + governanceTitle: '治理能力必须进入核心', + governanceDesc: 'Audit、审批、guard、activity 和 observability 不是后台功能,它们决定用户是否真正信任这个系统。', + deliveryTitle: '交付比回答更重要', + deliveryDesc: '一个成熟的 AI 产品不该停在回答层,它应该能持续推进任务、暴露状态、沉淀结果。', + }, + }, model: { title: '模型管理', desc: '配置模型 Provider、凭证和模型列表', @@ -499,9 +541,9 @@ export default { enUS: 'English', }, }, - workspace: { - title: '工作区', - desc: '管理 Agent 的 Markdown 系统提示文件', + agentContext: { + title: 'Agent 上下文', + desc: '管理 Agent 的提示文件和记忆', selectAgent: '选择 Agent', noAgent: '请先选择一个 Agent', files: '文件列表', @@ -608,6 +650,7 @@ export default { fileGuard: '文件防护', auditLogs: '审计日志', members: '成员管理', + workspaces: '工作区', activity: '操作日志', }, activity: { @@ -665,6 +708,51 @@ export default { removeFailed: '移除成员失败', }, }, + workspaces: { + title: '工作区管理', + desc: '管理组织的工作区。', + newWorkspace: '新建工作区', + loading: '加载中...', + noWorkspaces: '暂无工作区。', + current: '当前', + columns: { + name: '名称', + slug: '标识', + description: '描述', + created: '创建时间', + actions: '操作', + }, + createDialog: { + title: '新建工作区', + name: '名称', + namePlaceholder: '例如:工程团队', + slug: '标识', + slugPlaceholder: '例如:engineering', + slugHint: '标识创建后不可修改。', + description: '描述', + descriptionPlaceholder: '可选描述', + }, + editDialog: { + title: '编辑工作区', + }, + deleteDialog: { + title: '删除工作区', + confirm: '确定要删除 {name} 吗?这将移除该工作区下的所有资源。', + }, + actions: { + edit: '编辑', + delete: '删除', + cancel: '取消', + create: '创建', + save: '保存', + }, + messages: { + saveSuccess: '工作区保存成功', + saveFailed: '保存工作区失败', + deleteSuccess: '工作区已删除', + deleteFailed: '删除工作区失败', + }, + }, toolGuard: { title: '工具防护', desc: '管理工具调用安全规则和全局防护配置', diff --git a/mateclaw-ui/src/router/index.ts b/mateclaw-ui/src/router/index.ts index 7fcb5d34..ca6ba6be 100644 --- a/mateclaw-ui/src/router/index.ts +++ b/mateclaw-ui/src/router/index.ts @@ -102,10 +102,10 @@ const router = createRouter({ }, // Advanced (absorbed from top-level nav) { - path: 'workspace', - name: 'SettingsWorkspace', - component: () => import('@/views/AgentWorkspace.vue'), - meta: { title: 'Settings - Workspace' }, + path: 'agent-context', + name: 'SettingsAgentContext', + component: () => import('@/views/AgentContext.vue'), + meta: { title: 'Settings - Agent Context' }, }, { path: 'cron-jobs', @@ -163,23 +163,29 @@ const router = createRouter({ component: () => import('@/views/Security/AuditLogs/index.vue'), meta: { title: 'Security - Audit Logs' }, }, - { - path: 'activity', - name: 'SecurityActivity', - component: () => import('@/views/Security/Activity/index.vue'), - meta: { title: 'Security - Activity' }, - }, { path: 'members', name: 'SecurityMembers', component: () => import('@/views/Security/Members/index.vue'), meta: { title: 'Security - Members' }, }, + { + path: 'workspaces', + name: 'SecurityWorkspaces', + component: () => import('@/views/Security/Workspaces/index.vue'), + meta: { title: 'Security - Workspaces' }, + }, + { + path: 'activity', + name: 'SecurityActivity', + component: () => import('@/views/Security/Activity/index.vue'), + meta: { title: 'Security - Activity' }, + }, ], }, // ==================== Redirects (backward compatibility) ==================== { path: 'sessions', redirect: '/chat' }, - { path: 'workspace', redirect: '/settings/workspace' }, + { path: 'workspace', redirect: '/settings/agent-context' }, { path: 'cron-jobs', redirect: '/settings/cron-jobs' }, { path: 'datasources', redirect: '/settings/datasources' }, { path: 'mcp-servers', redirect: '/settings/mcp-servers' }, diff --git a/mateclaw-ui/src/views/AgentWorkspace.vue b/mateclaw-ui/src/views/AgentContext.vue similarity index 90% rename from mateclaw-ui/src/views/AgentWorkspace.vue rename to mateclaw-ui/src/views/AgentContext.vue index e91c5881..7869b6a8 100644 --- a/mateclaw-ui/src/views/AgentWorkspace.vue +++ b/mateclaw-ui/src/views/AgentContext.vue @@ -3,12 +3,12 @@

-

{{ t('workspace.title') }}

-

{{ t('workspace.desc') }}

+

{{ t('agentContext.title') }}

+

{{ t('agentContext.desc') }}

-

{{ t('workspace.newFileHint') }}

+

{{ t('agentContext.newFileHint') }}