mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
docs(readme): lead with team / IT-deployable positioning
Add a callout above the existing intro to make the wedge explicit: multi-user workspaces, approval-gated sensitive actions, full audit trail, production-grade health monitoring, per-channel error isolation. One JAR on your own machine, zero data egress.
This commit is contained in:
parent
822a77eef5
commit
172784bc5f
@ -28,6 +28,10 @@
|
||||
|
||||
---
|
||||
|
||||
> **Other personal AI agents are built for one person. MateClaw is the one your IT department can actually sign off on.**
|
||||
>
|
||||
> Multi-user workspaces. Approval-gated sensitive actions. Full audit trail. Spring Boot Actuator health monitoring. Per-channel error isolation so one chat platform's outage doesn't take down the rest. One JAR on your own machine, zero data egress.
|
||||
|
||||
Most AI tools die when their vendor has a bad day. Most forget you the moment the tab closes. Most give you a chatbox and call it a product.
|
||||
|
||||
**MateClaw is the whole widget.** One deployment. Reasoning, knowledge, memory, tools, channels — built together, not bolted on. And when your primary model goes down, the next one picks up mid-sentence.
|
||||
|
||||
@ -28,6 +28,10 @@
|
||||
|
||||
---
|
||||
|
||||
> **别的 AI 助手是给一个人用的。MateClaw 是公司允许部署的那一个。**
|
||||
>
|
||||
> 多用户工作空间。敏感操作走审批。完整审计日志。Spring Boot Actuator 健康监控。单个渠道挂掉不影响其他渠道的错误隔离。一个 JAR 包跑在自己机器上,数据不出门。
|
||||
|
||||
大多数 AI 工具一到厂商抽风那天就两手一摊。关一次标签页就忘了你是谁。给你一个聊天框,就敢叫产品。
|
||||
|
||||
**MateClaw 是完整的一整套。** 一次部署——推理、知识、记忆、工具、多渠道入口,从第一天就一起设计,不是事后拼接。主模型挂了,下一家接着把这句话说完。
|
||||
|
||||
@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
import vip.mate.channel.web.Utf8SseEmitter;
|
||||
@ -11,9 +12,12 @@ import vip.mate.agent.AgentService;
|
||||
import vip.mate.agent.AgentState;
|
||||
import vip.mate.agent.model.AgentEntity;
|
||||
import vip.mate.audit.service.AuditEventService;
|
||||
import vip.mate.auth.model.UserEntity;
|
||||
import vip.mate.auth.service.AuthService;
|
||||
import vip.mate.common.result.R;
|
||||
import vip.mate.exception.MateClawException;
|
||||
import vip.mate.workspace.core.annotation.RequireWorkspaceRole;
|
||||
import vip.mate.workspace.core.service.WorkspaceService;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
@ -34,6 +38,8 @@ public class AgentController {
|
||||
|
||||
private final AgentService agentService;
|
||||
private final AuditEventService auditEventService;
|
||||
private final AuthService authService;
|
||||
private final WorkspaceService workspaceService;
|
||||
private final ExecutorService sseExecutor = Executors.newCachedThreadPool();
|
||||
|
||||
@Operation(summary = "获取Agent列表")
|
||||
@ -61,9 +67,12 @@ public class AgentController {
|
||||
@RequireWorkspaceRole("member")
|
||||
public R<AgentEntity> create(
|
||||
@RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId,
|
||||
@RequestBody AgentEntity agent) {
|
||||
@RequestBody AgentEntity agent,
|
||||
Authentication auth) {
|
||||
// 始终注入 workspace_id,无 header 时使用默认
|
||||
agent.setWorkspaceId(workspaceId != null ? workspaceId : 1L);
|
||||
// RFC-077 §4.4: 记录创建者,让 member 后续可删除自建 Agent
|
||||
agent.setCreatorUserId(resolveUserId(auth));
|
||||
AgentEntity created = agentService.createAgent(agent);
|
||||
auditEventService.record("CREATE", "AGENT", String.valueOf(created.getId()), created.getName(), null);
|
||||
return R.ok(created);
|
||||
@ -85,11 +94,24 @@ public class AgentController {
|
||||
|
||||
@Operation(summary = "删除Agent")
|
||||
@DeleteMapping("/{id}")
|
||||
@RequireWorkspaceRole("admin")
|
||||
@RequireWorkspaceRole("member")
|
||||
public R<Void> delete(@PathVariable Long id,
|
||||
@RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) {
|
||||
@RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId,
|
||||
Authentication auth) {
|
||||
AgentEntity agent = agentService.getAgent(id);
|
||||
verifyResourceWorkspace(agent.getWorkspaceId(), workspaceId);
|
||||
|
||||
// RFC-077 §4.4: 三选一鉴权 — 系统 admin / workspace admin+ / 创建者本人
|
||||
Long userId = resolveUserId(auth);
|
||||
boolean systemAdmin = isSystemAdmin(auth);
|
||||
boolean workspaceAdmin = !systemAdmin
|
||||
&& workspaceService.hasPermission(agent.getWorkspaceId(), userId, "admin");
|
||||
boolean isCreator = userId.equals(agent.getCreatorUserId());
|
||||
if (!systemAdmin && !workspaceAdmin && !isCreator) {
|
||||
throw new MateClawException("err.agent.delete_forbidden", 403,
|
||||
"Only the creator or a workspace admin can delete this Agent");
|
||||
}
|
||||
|
||||
agentService.deleteAgent(id);
|
||||
auditEventService.record("DELETE", "AGENT", String.valueOf(id), agent.getName(), null);
|
||||
return R.ok();
|
||||
@ -185,4 +207,23 @@ public class AgentController {
|
||||
throw new MateClawException("err.common.wrong_workspace", "资源不属于当前工作区");
|
||||
}
|
||||
}
|
||||
|
||||
private Long resolveUserId(Authentication auth) {
|
||||
if (auth == null) {
|
||||
throw new MateClawException("err.auth.unauthenticated", 401, "Not authenticated");
|
||||
}
|
||||
UserEntity user = authService.findByUsername(auth.getName());
|
||||
if (user == null) {
|
||||
throw new MateClawException("err.auth.user_not_found", 401, "User not found: " + auth.getName());
|
||||
}
|
||||
return user.getId();
|
||||
}
|
||||
|
||||
private boolean isSystemAdmin(Authentication auth) {
|
||||
if (auth == null) {
|
||||
return false;
|
||||
}
|
||||
UserEntity user = authService.findByUsername(auth.getName());
|
||||
return user != null && "admin".equalsIgnoreCase(user.getRole());
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,11 +3,16 @@ package vip.mate.agent.controller;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import vip.mate.agent.model.AgentEntity;
|
||||
import vip.mate.agent.model.TemplateDTO;
|
||||
import vip.mate.agent.service.TemplateService;
|
||||
import vip.mate.auth.model.UserEntity;
|
||||
import vip.mate.auth.service.AuthService;
|
||||
import vip.mate.common.result.R;
|
||||
import vip.mate.exception.MateClawException;
|
||||
import vip.mate.workspace.core.annotation.RequireWorkspaceRole;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ -23,6 +28,7 @@ import java.util.List;
|
||||
public class TemplateController {
|
||||
|
||||
private final TemplateService templateService;
|
||||
private final AuthService authService;
|
||||
|
||||
@Operation(summary = "获取模板列表")
|
||||
@GetMapping
|
||||
@ -32,7 +38,24 @@ public class TemplateController {
|
||||
|
||||
@Operation(summary = "应用模板创建Agent")
|
||||
@PostMapping("/{id}/apply")
|
||||
public R<AgentEntity> apply(@PathVariable String id) {
|
||||
return R.ok(templateService.applyTemplate(id));
|
||||
@RequireWorkspaceRole("member")
|
||||
public R<AgentEntity> apply(
|
||||
@PathVariable String id,
|
||||
@RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId,
|
||||
Authentication auth) {
|
||||
long wsId = workspaceId != null ? workspaceId : 1L;
|
||||
Long userId = resolveUserId(auth);
|
||||
return R.ok(templateService.applyTemplate(id, wsId, userId));
|
||||
}
|
||||
|
||||
private Long resolveUserId(Authentication auth) {
|
||||
if (auth == null) {
|
||||
throw new MateClawException("err.auth.unauthenticated", 401, "Not authenticated");
|
||||
}
|
||||
UserEntity user = authService.findByUsername(auth.getName());
|
||||
if (user == null) {
|
||||
throw new MateClawException("err.auth.user_not_found", 401, "User not found: " + auth.getName());
|
||||
}
|
||||
return user.getId();
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,6 +52,9 @@ public class AgentEntity {
|
||||
/** 所属工作区 ID(默认 1 = default) */
|
||||
private Long workspaceId;
|
||||
|
||||
/** Creator user ID — backfilled on create; lets members delete their own Agents without admin role */
|
||||
private Long creatorUserId;
|
||||
|
||||
/** 默认思考深度:off / low / medium / high / max,null 表示跟随模型默认 */
|
||||
private String defaultThinkingLevel;
|
||||
|
||||
|
||||
@ -65,17 +65,19 @@ public class TemplateService {
|
||||
/**
|
||||
* 应用模板创建 Agent 及其工作区文件
|
||||
*
|
||||
* @param templateId 模板 ID
|
||||
* @param templateId 模板 ID
|
||||
* @param workspaceId 目标工作区 ID(来自 X-Workspace-Id header)
|
||||
* @param creatorUserId 当前用户 ID(用于 RFC-077 创建者归属)
|
||||
* @return 创建的 AgentEntity
|
||||
*/
|
||||
@Transactional
|
||||
public AgentEntity applyTemplate(String templateId) {
|
||||
public AgentEntity applyTemplate(String templateId, Long workspaceId, Long creatorUserId) {
|
||||
TemplateDTO template = listTemplates().stream()
|
||||
.filter(t -> t.getId().equals(templateId))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new MateClawException("err.agent.template_not_found", "模板不存在: " + templateId));
|
||||
|
||||
// 1. 创建 Agent
|
||||
// 1. 创建 Agent — RFC-077: 显式注入 workspaceId/creatorUserId,避免 DB 默认值兜底成 1(issue #26 Bug A)
|
||||
AgentEntity agent = new AgentEntity();
|
||||
agent.setName(template.getName());
|
||||
agent.setDescription(template.getDescription());
|
||||
@ -83,6 +85,8 @@ public class TemplateService {
|
||||
agent.setIcon(template.getIcon());
|
||||
agent.setTags(template.getTags());
|
||||
agent.setMaxIterations(template.getMaxIterations());
|
||||
agent.setWorkspaceId(workspaceId);
|
||||
agent.setCreatorUserId(creatorUserId);
|
||||
AgentEntity created = agentService.createAgent(agent);
|
||||
|
||||
// 2. 创建工作区文件
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
-- RFC-077 §4.1: track which user created an Agent, so members can delete
|
||||
-- their own Agents without needing workspace admin role (issue #26 Bug B).
|
||||
|
||||
ALTER TABLE mate_agent ADD COLUMN IF NOT EXISTS creator_user_id BIGINT;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_agent_creator_user ON mate_agent(creator_user_id);
|
||||
@ -0,0 +1,10 @@
|
||||
-- RFC-077 §4.1: track which user created an Agent, so members can delete
|
||||
-- their own Agents without needing workspace admin role (issue #26 Bug B).
|
||||
|
||||
SET @c := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_agent' AND COLUMN_NAME = 'creator_user_id');
|
||||
SET @s := IF(@c = 0, 'ALTER TABLE mate_agent ADD COLUMN creator_user_id BIGINT', 'SELECT 1');
|
||||
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @c := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_agent' AND INDEX_NAME = 'idx_agent_creator_user');
|
||||
SET @s := IF(@c = 0, 'CREATE INDEX idx_agent_creator_user ON mate_agent(creator_user_id)', 'SELECT 1');
|
||||
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
@ -185,6 +185,7 @@ err.agent.dashscope_key_missing=DashScope API Key \u672a\u914d\u7f6e
|
||||
err.agent.anthropic_not_configured=Anthropic Provider \u672a\u5b8c\u6210\u914d\u7f6e
|
||||
err.agent.anthropic_key_invalid=Anthropic API Key \u672a\u914d\u7f6e\u6216\u65e0\u6548
|
||||
err.agent.template_not_found=\u6a21\u677f\u4e0d\u5b58\u5728
|
||||
err.agent.delete_forbidden=\u53ea\u6709\u521b\u5efa\u8005\u6216\u5de5\u4f5c\u533a\u7ba1\u7406\u5458\u53ef\u5220\u9664\u6b64 Agent
|
||||
err.common.wrong_workspace=\u8d44\u6e90\u4e0d\u5c5e\u4e8e\u5f53\u524d\u5de5\u4f5c\u533a
|
||||
# llm
|
||||
err.llm.model_config_not_found=\u6a21\u578b\u914d\u7f6e\u4e0d\u5b58\u5728
|
||||
|
||||
@ -197,6 +197,7 @@ err.agent.dashscope_key_missing=DashScope API Key not configured
|
||||
err.agent.anthropic_not_configured=Anthropic Provider not configured
|
||||
err.agent.anthropic_key_invalid=Anthropic API Key not configured or invalid
|
||||
err.agent.template_not_found=Template not found
|
||||
err.agent.delete_forbidden=Only the creator or a workspace admin can delete this Agent
|
||||
err.common.wrong_workspace=Resource does not belong to current workspace
|
||||
# llm
|
||||
err.llm.model_config_not_found=Model config not found
|
||||
|
||||
Loading…
Reference in New Issue
Block a user