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:
matevip 2026-04-29 14:10:12 +08:00
parent 822a77eef5
commit 172784bc5f
10 changed files with 105 additions and 8 deletions

View File

@ -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.

View File

@ -28,6 +28,10 @@
---
> **别的 AI 助手是给一个人用的。MateClaw 是公司允许部署的那一个。**
>
> 多用户工作空间。敏感操作走审批。完整审计日志。Spring Boot Actuator 健康监控。单个渠道挂掉不影响其他渠道的错误隔离。一个 JAR 包跑在自己机器上,数据不出门。
大多数 AI 工具一到厂商抽风那天就两手一摊。关一次标签页就忘了你是谁。给你一个聊天框,就敢叫产品。
**MateClaw 是完整的一整套。** 一次部署——推理、知识、记忆、工具、多渠道入口,从第一天就一起设计,不是事后拼接。主模型挂了,下一家接着把这句话说完。

View File

@ -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());
}
}

View File

@ -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();
}
}

View File

@ -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 / maxnull 表示跟随模型默认 */
private String defaultThinkingLevel;

View File

@ -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 默认值兜底成 1issue #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. 创建工作区文件

View File

@ -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);

View File

@ -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;

View File

@ -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

View File

@ -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