feat(db): introduce Flyway migration framework and unify H2/MySQL schemas

- Add Flyway baseline migration (V1) for both H2 and MySQL
- Remove 5 legacy SchemaMigration ApplicationRunner classes
- Add WorkspacePathGuard for file tool sandbox enforcement
- Inject workspace basePath context into agent graph state and tool executor
- Add workspace basePath config UI in frontend
- Fix workspace slug preservation on update
This commit is contained in:
matevip 2026-04-11 16:34:09 +08:00
parent f98f4d68b9
commit cc28f665f1
45 changed files with 1618 additions and 374 deletions

View File

@ -1 +0,0 @@
Hello World

View File

@ -267,6 +267,16 @@
<version>2.2.0</version>
</dependency>
<!-- ===== Database Migration (Flyway) ===== -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
</dependency>
<!-- ===== Spring Boot Test ===== -->
<dependency>
<groupId>org.springframework.boot</groupId>

View File

@ -120,6 +120,7 @@ public class AgentGraphBuilder {
private final vip.mate.agent.context.ConversationWindowManager conversationWindowManager;
private final vip.mate.llm.chatgpt.ChatGPTResponsesClient chatGPTResponsesClient;
private final WikiContextService wikiContextService;
private final vip.mate.workspace.core.service.WorkspaceService workspaceService;
/**
* 根据 AgentEntity 构建完整的 Agent 实例
@ -202,6 +203,19 @@ public class AgentGraphBuilder {
agent.topP = runtimeModel.getTopP();
agent.toolCallingEnabled = toolCallingEnabled;
// 查找工作区活动目录
if (entity.getWorkspaceId() != null) {
try {
var workspace = workspaceService.getById(entity.getWorkspaceId());
if (workspace != null && workspace.getBasePath() != null && !workspace.getBasePath().isBlank()) {
agent.workspaceBasePath = workspace.getBasePath();
log.info("Agent {} bound to workspace basePath: {}", entity.getName(), agent.workspaceBasePath);
}
} catch (Exception e) {
log.warn("Failed to lookup workspace basePath for agent {}: {}", entity.getName(), e.getMessage());
}
}
log.info("Built agent instance: {} (type={}, protocol={}, tools={}, toolCallingEnabled={})",
entity.getName(), entity.getAgentType(), protocol.getId(),
toolSet.size(), agent.toolCallingEnabled);

View File

@ -46,6 +46,9 @@ public abstract class BaseAgent {
/** 最大工具调用迭代次数 */
protected int maxIterations = 25;
/** 工作区活动目录(限制文件工具访问范围,为空不限制) */
protected String workspaceBasePath;
/** 模型名称 */
protected String modelName;

View File

@ -27,4 +27,16 @@ public final class RuntimeContextInjector {
return "[system-context] 当前时间: " + now.format(DATE_FMT)
+ " " + now.format(TIME_FMT) + " (Asia/Shanghai)";
}
/**
* 构建运行时上下文消息包含当前日期时间和工作目录
*/
public static String buildContextMessage(String workspaceBasePath) {
StringBuilder sb = new StringBuilder(buildContextMessage());
if (workspaceBasePath != null && !workspaceBasePath.isBlank()) {
sb.append("\n[system-context] 工作目录: ").append(workspaceBasePath)
.append("\n你只能在此目录及其子目录内读写文件和执行命令。");
}
return sb.toString();
}
}

View File

@ -362,6 +362,7 @@ public class StateGraphReActAgent extends BaseAgent implements StructuredStreamC
inputs.put(USER_MESSAGE, userMessage);
inputs.put(CONVERSATION_ID, conversationId);
inputs.put(AGENT_ID, agentId != null ? agentId : "");
inputs.put(WORKSPACE_BASE_PATH, workspaceBasePath != null ? workspaceBasePath : "");
inputs.put(SYSTEM_PROMPT, systemPrompt != null ? systemPrompt : "你是一个有帮助的AI助手。");
inputs.put(MESSAGES, messages);
// 迭代控制

View File

@ -146,11 +146,21 @@ public class ToolExecutionExecutor {
/** 当前执行的 requesterId传递给 ToolExecutionContext */
private volatile String currentRequesterId;
/** 当前工作区活动目录(为空不限制),传递给 ToolExecutionContext */
private volatile String currentWorkspaceBasePath;
public ToolExecutionResult execute(List<AssistantMessage.ToolCall> toolCalls,
String conversationId, String agentId,
boolean isReplay, String requesterId) {
return execute(toolCalls, conversationId, agentId, isReplay, requesterId, null);
}
public ToolExecutionResult execute(List<AssistantMessage.ToolCall> toolCalls,
String conversationId, String agentId,
boolean isReplay, String requesterId,
String workspaceBasePath) {
this.currentRequesterId = requesterId;
this.currentWorkspaceBasePath = workspaceBasePath;
List<ToolResponseMessage.ToolResponse> allResponses = new ArrayList<>();
List<GraphEventPublisher.GraphEvent> events = Collections.synchronizedList(new ArrayList<>());
@ -228,7 +238,8 @@ public class ToolExecutionExecutor {
// 4. 分类: concurrencySafe
boolean safe = isConcurrencySafe(toolName);
preparedCalls.add(new PreparedToolCall(toolCall, callback, arguments, safe, allResponses.size(), conversationId));
preparedCalls.add(new PreparedToolCall(toolCall, callback, arguments, safe, allResponses.size(),
conversationId, currentRequesterId, currentWorkspaceBasePath));
// 占位Phase 2 填充
allResponses.add(null);
}
@ -392,8 +403,8 @@ public class ToolExecutionExecutor {
toolName, pc.arguments != null && pc.arguments.length() > 200
? pc.arguments.substring(0, 200) + "..." : pc.arguments);
// 注入工具执行上下文 VideoGenerateTool 等获取 conversationId / username
ToolExecutionContext.set(pc.conversationId, currentRequesterId);
// 注入工具执行上下文 VideoGenerateTool 等获取 conversationId / username / workspaceBasePath
ToolExecutionContext.set(pc.conversationId, pc.requesterId, pc.workspaceBasePath);
String result;
try {
result = pc.callback.call(pc.arguments);
@ -530,7 +541,9 @@ public class ToolExecutionExecutor {
String arguments,
boolean concurrencySafe,
int resultIndex,
String conversationId
String conversationId,
String requesterId,
String workspaceBasePath
) {}
private record ApprovalBarrier(String pendingId, String toolName) {}

View File

@ -62,9 +62,12 @@ public class ActionNode implements NodeAction {
// 请求者身份用于审批记录
String requesterId = accessor.requesterId();
// 获取工作区活动目录
String workspaceBasePath = state.value(MateClawStateKeys.WORKSPACE_BASE_PATH, "");
// 委托 ToolExecutionExecutor 执行两阶段顺序 Guard + 分段并发执行
ToolExecutionExecutor.ToolExecutionResult result = executor.execute(
toolCalls, conversationId, agentId, isReplay, requesterId);
toolCalls, conversationId, agentId, isReplay, requesterId, workspaceBasePath);
ToolResponseMessage toolResponseMessage = ToolResponseMessage.builder()
.responses(result.responses())

View File

@ -171,9 +171,10 @@ public class ReasoningNode implements NodeAction {
messages = trimmed;
}
String workspaceBasePath = state.value(vip.mate.agent.graph.state.MateClawStateKeys.WORKSPACE_BASE_PATH, "");
List<Message> promptMessages = new ArrayList<>();
promptMessages.add(new SystemMessage(systemPrompt));
promptMessages.add(new UserMessage(RuntimeContextInjector.buildContextMessage()));
promptMessages.add(new UserMessage(RuntimeContextInjector.buildContextMessage(workspaceBasePath)));
promptMessages.addAll(messages);
ChatOptions options;
@ -222,7 +223,7 @@ public class ReasoningNode implements NodeAction {
if (compactedMessages != null && compactedMessages.size() < messages.size()) {
List<Message> retryPromptMessages = new ArrayList<>();
retryPromptMessages.add(new SystemMessage(systemPrompt));
retryPromptMessages.add(new UserMessage(RuntimeContextInjector.buildContextMessage()));
retryPromptMessages.add(new UserMessage(RuntimeContextInjector.buildContextMessage(workspaceBasePath)));
retryPromptMessages.addAll(compactedMessages);
Prompt retryPrompt = new Prompt(retryPromptMessages, options);
log.info("[ReasoningNode] Retrying with compacted messages: {} -> {} messages",

View File

@ -269,6 +269,7 @@ public class StateGraphPlanExecuteAgent extends BaseAgent implements StructuredS
systemPrompt != null ? systemPrompt : "你是一个有帮助的AI助手。");
inputs.put(MateClawStateKeys.CONVERSATION_ID, conversationId);
inputs.put(MateClawStateKeys.AGENT_ID, agentId != null ? agentId : "");
inputs.put(MateClawStateKeys.WORKSPACE_BASE_PATH, workspaceBasePath != null ? workspaceBasePath : "");
// 注入会话消息复用 MateClawStateKeys.MESSAGES ReAct 一致
inputs.put(MateClawStateKeys.MESSAGES, messages);
// 注入 working context

View File

@ -133,8 +133,9 @@ public class PlanGenerationNode implements NodeAction {
// 拼接后会稀释 PLANNING_PROMPT 的指令优先级
List<Message> promptMessages = new ArrayList<>();
promptMessages.add(new SystemMessage(PLANNING_PROMPT));
// 注入运行时上下文当前时间
promptMessages.add(new UserMessage(RuntimeContextInjector.buildContextMessage()));
// 注入运行时上下文当前时间 + 工作目录
String workspaceBasePath = state.value(MateClawStateKeys.WORKSPACE_BASE_PATH, "");
promptMessages.add(new UserMessage(RuntimeContextInjector.buildContextMessage(workspaceBasePath)));
// 注入可用工具名称帮助 LLM 判断用户目标是否需要工具
if (toolSet != null && !toolSet.callbacks().isEmpty()) {

View File

@ -85,6 +85,7 @@ public class StepExecutionNode implements NodeAction {
String conversationId = state.value(MateClawStateKeys.CONVERSATION_ID, "");
String agentId = state.value(MateClawStateKeys.AGENT_ID, "");
String workspaceBasePath = state.value(MateClawStateKeys.WORKSPACE_BASE_PATH, "");
if (stepIndex >= steps.size()) {
log.warn("[StepExecution] stepIndex {} >= steps.size() {}, skipping", stepIndex, steps.size());
@ -105,7 +106,7 @@ public class StepExecutionNode implements NodeAction {
planningService.updateSubPlanStatus(planId, stepIndex, "running");
// 构建消息列表
List<Message> messages = buildStepMessages(accessor, step, systemPrompt);
List<Message> messages = buildStepMessages(accessor, step, systemPrompt, workspaceBasePath);
// 显式工具执行循环
String finalResult = null;
@ -186,7 +187,7 @@ public class StepExecutionNode implements NodeAction {
} else {
// 非预批准工具走正常执行器
ToolExecutionExecutor.ToolExecutionResult execResult = executor.execute(
List.of(toolCall), conversationId, agentId, false);
List.of(toolCall), conversationId, agentId, false, "", workspaceBasePath);
toolResponses.addAll(execResult.responses());
events.addAll(execResult.events());
if (execResult.awaitingApproval()) {
@ -199,7 +200,7 @@ public class StepExecutionNode implements NodeAction {
} else {
// 正常路径委托 ToolExecutionExecutor支持并发执行 + 审批 barrier
ToolExecutionExecutor.ToolExecutionResult execResult = executor.execute(
allToolCalls, conversationId, agentId, false);
allToolCalls, conversationId, agentId, false, "", workspaceBasePath);
toolResponses.addAll(execResult.responses());
events.addAll(execResult.events());
if (execResult.awaitingApproval()) {
@ -286,7 +287,7 @@ public class StepExecutionNode implements NodeAction {
.build();
}
private List<Message> buildStepMessages(PlanStateAccessor accessor, String step, String systemPrompt) {
private List<Message> buildStepMessages(PlanStateAccessor accessor, String step, String systemPrompt, String workspaceBasePath) {
List<Message> messages = new ArrayList<>();
// Layer 1: System prompt增强指令
@ -305,8 +306,8 @@ public class StepExecutionNode implements NodeAction {
8. 每一步最多做一个必要的检查和一个必要的执行不要无意义循环
""";
messages.add(new SystemMessage(enhancedSystemPrompt));
// 注入运行时上下文当前时间
messages.add(new UserMessage(RuntimeContextInjector.buildContextMessage()));
// 注入运行时上下文当前时间 + 工作目录
messages.add(new UserMessage(RuntimeContextInjector.buildContextMessage(workspaceBasePath)));
// Layer 2: Working context对话历史 + 步骤结果的受控长度摘要
String workingContext = accessor.workingContext();

View File

@ -20,6 +20,8 @@ public final class MateClawStateKeys {
public static final String CONVERSATION_ID = "conversation_id";
public static final String SYSTEM_PROMPT = "system_prompt";
public static final String AGENT_ID = "agent_id";
/** 工作区活动目录(为空不限制文件访问范围) */
public static final String WORKSPACE_BASE_PATH = "workspace_base_path";
// ===== 消息列表APPEND 策略=====
public static final String MESSAGES = "messages";

View File

@ -30,7 +30,7 @@ import java.util.Map;
*/
@Slf4j
@Service
@Order(55) // ApprovalSchemaMigration(50) 之后
@Order(55) // Schema Flyway 管理 Flyway 迁移完成后执行
@RequiredArgsConstructor
public class ApprovalWorkflowService implements ApplicationRunner {

View File

@ -1,34 +0,0 @@
package vip.mate.approval.config;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
/**
* 审批表增量 Schema 迁移
* <p>
* 基础表 mate_tool_approval 已在 schema.sql / schema-mysql.sql 中定义
* 本类仅负责增量字段/索引迁移确保从旧版本升级时自动补齐
* <p>
* 当前无需增量迁移保留框架以备未来扩展
*
* @author MateClaw Team
*/
@Slf4j
@Component
@Order(50)
@RequiredArgsConstructor
public class ApprovalSchemaMigration implements ApplicationRunner {
private final JdbcTemplate jdbcTemplate;
@Override
public void run(ApplicationArguments args) {
// 当前无增量迁移未来新增字段时在此添加 safeAddColumn 调用
log.debug("[ApprovalSchemaMigration] Incremental migration completed (no-op)");
}
}

View File

@ -68,7 +68,7 @@ public class DatabaseBootstrapRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
runSchemaScript();
// Schema creation is now handled by Flyway (db/migration/)
runToolSyncScript();
if (isDataAlreadySeeded()) {
@ -168,10 +168,6 @@ public class DatabaseBootstrapRunner implements ApplicationRunner {
return isMySQL;
}
private void runSchemaScript() {
runScript(isMySQL() ? "db/schema-mysql.sql" : "db/schema.sql");
}
private void runToolSyncScript() {
String script = isMySQL() ? "db/tools-sync-mysql.sql" : "db/tools-sync.sql";
runScript(script);

View File

@ -1,40 +0,0 @@
package vip.mate.cron.config;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
/**
* CronJob Schema 迁移
*
* @author MateClaw Team
*/
@Slf4j
@Component
@Order(200)
@RequiredArgsConstructor
public class CronSchemaMigration implements ApplicationRunner {
private final JdbcTemplate jdbcTemplate;
@Override
public void run(ApplicationArguments args) {
addColumnIfMissing("mate_cron_job", "timezone", "VARCHAR(64) NOT NULL DEFAULT 'Asia/Shanghai'");
addColumnIfMissing("mate_cron_job", "task_type", "VARCHAR(16) NOT NULL DEFAULT 'text'");
addColumnIfMissing("mate_cron_job", "request_body", "TEXT");
addColumnIfMissing("mate_cron_job", "next_run_time", "DATETIME");
log.info("[CronSchemaMigration] mate_cron_job schema migration completed");
}
private void addColumnIfMissing(String table, String column, String definition) {
try {
jdbcTemplate.execute("ALTER TABLE " + table + " ADD COLUMN IF NOT EXISTS " + column + " " + definition);
} catch (Exception e) {
log.debug("[CronSchemaMigration] Column {} may already exist: {}", column, e.getMessage());
}
}
}

View File

@ -60,7 +60,7 @@ public class CronJobService implements ApplicationRunner {
// ==================== 初始化与销毁 ====================
/**
* 实现 ApplicationRunner确保在 CronSchemaMigration(@Order=200) 执行完毕后再加载任务
* 实现 ApplicationRunner确保在 Flyway 迁移和 DatabaseBootstrapRunner 完成后再加载任务
*/
@Override
public void run(ApplicationArguments args) {

View File

@ -1,60 +0,0 @@
package vip.mate.memory.config;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
/**
* Memory 模块增量 Schema 迁移
* <p>
* 基础表 mate_memory_recall 已在 schema.sql / schema-mysql.sql 中定义
* 本类仅负责增量索引/字段迁移确保从旧版本升级时自动补齐
*
* @author MateClaw Team
*/
@Slf4j
@Component
@Order(201)
@RequiredArgsConstructor
public class MemorySchemaMigration implements ApplicationRunner {
private final JdbcTemplate jdbcTemplate;
@Override
public void run(ApplicationArguments args) {
// 增量索引补齐v1.1 新增的复合索引旧版本可能没有
safeExecute("CREATE INDEX IF NOT EXISTS idx_memory_recall_candidates ON mate_memory_recall(agent_id, promoted, deleted)");
// Session Search: MySQL FULLTEXT index on mate_message.content
if (isMySql()) {
safeExecute("ALTER TABLE mate_message ADD FULLTEXT INDEX ft_msg_content (content)");
log.info("[MemorySchemaMigration] MySQL FULLTEXT index on mate_message.content created (or already exists)");
}
// max_iterations 升级旧版默认 10新版 25确保已有 agent 也更新
safeExecute("UPDATE mate_agent SET max_iterations = 25 WHERE max_iterations = 10 AND deleted = 0");
log.debug("[MemorySchemaMigration] Incremental migration completed");
}
private boolean isMySql() {
try {
String url = jdbcTemplate.getDataSource().getConnection().getMetaData().getURL();
return url != null && url.contains("mysql");
} catch (Exception e) {
return false;
}
}
private void safeExecute(String sql) {
try {
jdbcTemplate.execute(sql);
} catch (Exception e) {
log.debug("[MemorySchemaMigration] Migration skipped (may already exist): {}", e.getMessage());
}
}
}

View File

@ -56,7 +56,12 @@ public class EditFileTool {
return errorResult(filePath, "oldText 和 newText 内容相同,无需替换");
}
Path path = Paths.get(filePath).toAbsolutePath().normalize();
Path path;
try {
path = vip.mate.tool.guard.WorkspacePathGuard.validatePath(filePath);
} catch (IllegalArgumentException e) {
return errorResult(filePath, e.getMessage());
}
if (!Files.exists(path)) {
return errorResult(filePath, "文件不存在: " + path);

View File

@ -62,7 +62,12 @@ public class ReadFileTool {
result.set("filePath", filePath);
try {
Path path = Paths.get(filePath).toAbsolutePath().normalize();
Path path;
try {
path = vip.mate.tool.guard.WorkspacePathGuard.validatePath(filePath);
} catch (IllegalArgumentException e) {
return errorResult(filePath, e.getMessage());
}
// 文件存在性和类型校验
if (!Files.exists(path)) {

View File

@ -129,12 +129,22 @@ public class ShellExecuteTool {
* Unix: /bin/sh -c command
*/
private static ProcessBuilder buildShellProcess(String command) {
ProcessBuilder pb;
if (IS_WINDOWS) {
String winCommand = sanitizeWindowsCommand(command);
return new ProcessBuilder("cmd.exe", "/D", "/S", "/C", winCommand);
pb = new ProcessBuilder("cmd.exe", "/D", "/S", "/C", winCommand);
} else {
return new ProcessBuilder("/bin/sh", "-c", command);
pb = new ProcessBuilder("/bin/sh", "-c", command);
}
// 设置工作区活动目录
java.nio.file.Path workingDir = vip.mate.tool.guard.WorkspacePathGuard.getWorkingDirectory();
if (workingDir != null && java.nio.file.Files.isDirectory(workingDir)) {
pb.directory(workingDir.toFile());
log.info("[ShellExecute] Working directory set to: {}", workingDir);
}
return pb;
}
/**

View File

@ -12,12 +12,21 @@ public final class ToolExecutionContext {
private static final ThreadLocal<String> CONVERSATION_ID = new ThreadLocal<>();
private static final ThreadLocal<String> USERNAME = new ThreadLocal<>();
/** 工作区活动目录(为空不限制) */
private static final ThreadLocal<String> WORKSPACE_BASE_PATH = new ThreadLocal<>();
private ToolExecutionContext() {}
public static void set(String conversationId, String username) {
CONVERSATION_ID.set(conversationId);
USERNAME.set(username);
WORKSPACE_BASE_PATH.remove();
}
public static void set(String conversationId, String username, String workspaceBasePath) {
CONVERSATION_ID.set(conversationId);
USERNAME.set(username);
WORKSPACE_BASE_PATH.set(workspaceBasePath);
}
public static String conversationId() {
@ -28,8 +37,14 @@ public final class ToolExecutionContext {
return USERNAME.get();
}
/** 获取当前工作区活动目录,为 null 表示不限制 */
public static String workspaceBasePath() {
return WORKSPACE_BASE_PATH.get();
}
public static void clear() {
CONVERSATION_ID.remove();
USERNAME.remove();
WORKSPACE_BASE_PATH.remove();
}
}

View File

@ -48,7 +48,12 @@ public class WriteFileTool {
content = "";
}
Path path = Paths.get(filePath).toAbsolutePath().normalize();
Path path;
try {
path = vip.mate.tool.guard.WorkspacePathGuard.validatePath(filePath);
} catch (IllegalArgumentException e) {
return errorResult(filePath, e.getMessage());
}
// 如果路径是已有目录拒绝
if (Files.isDirectory(path)) {

View File

@ -0,0 +1,81 @@
package vip.mate.tool.guard;
import lombok.extern.slf4j.Slf4j;
import vip.mate.tool.builtin.ToolExecutionContext;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* 工作区路径沙箱校验器
* <p>
* 限制文件工具只能在工作区活动目录basePath及其子目录内操作
* 跨平台支持Windows / macOS / Linux 路径统一处理
* <p>
* 使用方式在文件工具方法开头调用 {@link #validatePath(String)} 获取规范化路径
*
* @author MateClaw Team
*/
@Slf4j
public final class WorkspacePathGuard {
private WorkspacePathGuard() {}
/**
* 校验文件路径是否在当前工作区活动目录范围内
* <p>
* {@link ToolExecutionContext#workspaceBasePath()} 读取当前活动目录
* 为空时不限制向后兼容
*
* @param rawPath 用户传入的原始路径
* @return 规范化后的绝对路径
* @throws IllegalArgumentException 路径不在允许范围内
*/
public static Path validatePath(String rawPath) {
Path normalized = Paths.get(rawPath).toAbsolutePath().normalize();
String basePath = ToolExecutionContext.workspaceBasePath();
if (basePath == null || basePath.isBlank()) {
return normalized; // 未配置活动目录不限制
}
Path root = Paths.get(basePath).toAbsolutePath().normalize();
// 先用 normalize 检查再尝试 toRealPath 防符号链接逃逸
if (!normalized.startsWith(root)) {
throw new IllegalArgumentException(
"路径不在工作区允许范围内: " + normalized + ",允许的根目录: " + root);
}
// 对已存在的路径解析符号链接后再次校验
try {
if (normalized.toFile().exists()) {
Path realPath = normalized.toRealPath();
Path realRoot = root.toFile().exists() ? root.toRealPath() : root;
if (!realPath.startsWith(realRoot)) {
throw new IllegalArgumentException(
"路径通过符号链接逃逸出工作区: " + realPath + ",允许的根目录: " + realRoot);
}
return realPath;
}
} catch (IOException e) {
log.debug("[WorkspacePathGuard] 无法解析真实路径(文件可能不存在): {}", normalized);
}
return normalized;
}
/**
* 获取当前工作区活动目录的 Path用于设置 Shell 工作目录等
*
* @return 活动目录 Path未配置时返回 null
*/
public static Path getWorkingDirectory() {
String basePath = ToolExecutionContext.workspaceBasePath();
if (basePath == null || basePath.isBlank()) {
return null;
}
return Paths.get(basePath).toAbsolutePath().normalize();
}
}

View File

@ -1,55 +0,0 @@
package vip.mate.tool.guard.config;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
/**
* ToolGuard 增量 Schema 迁移
* <p>
* 基础表 mate_tool_guard_rule / config / audit_log 已在 schema.sql / schema-mysql.sql 中定义
* 本类仅负责增量字段迁移确保从旧版本升级时自动补齐新列
*
* @author MateClaw Team
*/
@Slf4j
@Component
@Order(100)
@RequiredArgsConstructor
public class ToolGuardSchemaMigration implements ApplicationRunner {
private final JdbcTemplate jdbcTemplate;
@Override
public void run(ApplicationArguments args) {
migrateGuardConfigAuditColumns();
}
/**
* mate_tool_guard_config 补充审计配置列向已有表兼容迁移
*/
private void migrateGuardConfigAuditColumns() {
safeAddColumn("mate_tool_guard_config", "audit_enabled", "BOOLEAN NOT NULL DEFAULT TRUE");
safeAddColumn("mate_tool_guard_config", "audit_min_severity", "VARCHAR(16) NOT NULL DEFAULT 'INFO'");
safeAddColumn("mate_tool_guard_config", "audit_retention_days", "INT NOT NULL DEFAULT 90");
log.debug("[ToolGuardSchemaMigration] Incremental migration completed");
}
private void safeAddColumn(String table, String column, String definition) {
try {
Integer count = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ? AND COLUMN_NAME = ?",
Integer.class, table.toUpperCase(), column.toUpperCase());
if (count != null && count > 0) {
return;
}
jdbcTemplate.execute("ALTER TABLE " + table + " ADD COLUMN " + column + " " + definition);
} catch (Exception e) {
log.debug("[ToolGuardSchemaMigration] Column migration skipped: {}", e.getMessage());
}
}
}

View File

@ -30,7 +30,7 @@ import java.util.stream.Collectors;
*/
@Slf4j
@Component
@Order(110) // ToolGuardSchemaMigration(100) 之后
@Order(110) // Schema Flyway 管理 Flyway 迁移完成后执行
@RequiredArgsConstructor
public class ToolGuardRuleSeedService implements ApplicationRunner {

View File

@ -1,52 +0,0 @@
package vip.mate.wiki.config;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
/**
* Wiki 模块增量 Schema 迁移
* <p>
* Wiki 基础表已在 schema.sql / schema-mysql.sql 中定义 DatabaseBootstrapRunner 初始化
* 本类仅负责增量字段迁移确保从旧版本升级时自动补齐新字段
* <p>
* 如果表已由 schema.sql 创建全新安装这里的迁移是无副作用的空操作
*
* @author MateClaw Team
*/
@Slf4j
@Component
@Order(202)
@RequiredArgsConstructor
public class WikiSchemaMigration implements ApplicationRunner {
private final JdbcTemplate jdbcTemplate;
@Override
public void run(ApplicationArguments args) {
migrateKnowledgeBaseColumns();
}
/**
* 增量字段迁移兼容从 wiki 功能上线前的旧版本升级
*/
private void migrateKnowledgeBaseColumns() {
try {
// v1.1: 添加 source_directory 本地目录扫描功能
jdbcTemplate.execute("ALTER TABLE mate_wiki_knowledge_base ADD COLUMN IF NOT EXISTS source_directory VARCHAR(512)");
log.debug("[WikiSchemaMigration] Incremental migration completed");
} catch (Exception e) {
// MySQL 不支持 ADD COLUMN IF NOT EXISTS忽略已存在的错误
String msg = e.getMessage();
if (msg != null && (msg.contains("Duplicate column") || msg.contains("already exists"))) {
log.debug("[WikiSchemaMigration] Column source_directory already exists, skipping");
} else {
log.warn("[WikiSchemaMigration] Migration warning: {}", msg);
}
}
}
}

View File

@ -7,6 +7,12 @@ import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
/**
* Conversation runtime data fixes (executed every startup).
* <p>
* Schema migrations have been moved to Flyway V4__conversation_extensions.sql.
* This runner only handles data normalization that must run on every boot.
*/
@Slf4j
@Component
@RequiredArgsConstructor
@ -16,13 +22,14 @@ public class ConversationSchemaMigration implements ApplicationRunner {
@Override
public void run(org.springframework.boot.ApplicationArguments args) throws Exception {
ensureColumn("mate_message", "content_parts", "TEXT");
ensureColumn("mate_message", "prompt_tokens", "INT DEFAULT 0");
ensureColumn("mate_message", "completion_tokens", "INT DEFAULT 0");
ensureColumn("mate_message", "runtime_model", "VARCHAR(128)");
ensureColumn("mate_message", "runtime_provider", "VARCHAR(64)");
ensureColumn("mate_conversation", "stream_status", "VARCHAR(16) NOT NULL DEFAULT 'idle'");
// 启动时重置孤儿状态上次意外关机可能残留 running 状态
resetOrphanedStreams();
normalizeSharedChannelConversationOwners();
}
/**
* Reset orphaned 'running' conversations to 'idle' (crash recovery).
*/
private void resetOrphanedStreams() {
try {
int updated = jdbcTemplate.update(
"UPDATE mate_conversation SET stream_status = 'idle' WHERE stream_status = 'running'");
@ -32,8 +39,6 @@ public class ConversationSchemaMigration implements ApplicationRunner {
} catch (DataAccessException e) {
log.debug("Skipping orphan reset (table may not exist yet): {}", e.getMessage());
}
normalizeSharedChannelConversationOwners();
}
private void normalizeSharedChannelConversationOwners() {
@ -61,21 +66,4 @@ public class ConversationSchemaMigration implements ApplicationRunner {
log.debug("Skipping shared channel owner normalization: {}", e.getMessage());
}
}
private void ensureColumn(String tableName, String columnName, String ddl) {
String sql = "ALTER TABLE " + tableName + " ADD COLUMN IF NOT EXISTS " + columnName + " " + ddl;
try {
jdbcTemplate.execute(sql);
log.info("Ensured schema column exists: {}.{}", tableName, columnName);
} catch (DataAccessException e) {
String message = e.getMessage() != null ? e.getMessage().toLowerCase() : "";
if (message.contains("duplicate column") || message.contains("already exists")
|| message.contains("not found")) {
log.info("Schema migration skipped for {}.{}: {}", tableName, columnName,
message.contains("not found") ? "table not yet created" : "column already exists");
return;
}
throw e;
}
}
}

View File

@ -67,7 +67,7 @@ public class WorkspaceSchemaMigration implements ApplicationRunner {
// 查找不在默认工作区中的用户
int inserted = jdbcTemplate.update("""
INSERT INTO mate_workspace_member (id, workspace_id, user_id, role, create_time, update_time, deleted)
SELECT u.id, 1, u.id, u.role, NOW(), NOW(), 0
SELECT u.id, 1, u.id, CASE WHEN u.role = 'admin' THEN 'owner' ELSE u.role END, NOW(), NOW(), 0
FROM mate_user u
WHERE u.deleted = 0
AND NOT EXISTS (

View File

@ -56,7 +56,7 @@ public class WorkspaceController {
@PutMapping("/{id}")
public R<WorkspaceEntity> update(@PathVariable Long id, @RequestBody WorkspaceEntity entity, Authentication auth) {
Long userId = resolveUserId(auth);
workspaceService.requirePermission(id, userId, "owner");
workspaceService.requirePermission(id, userId, "admin");
entity.setId(id);
return R.ok(workspaceService.update(entity));
}

View File

@ -32,6 +32,9 @@ public class WorkspaceEntity {
/** 拥有者用户 ID */
private Long ownerId;
/** 工作区活动目录(限制文件工具的访问范围,为空不限制) */
private String basePath;
/** 工作区级配置JSON */
@TableField(value = "settings_json", updateStrategy = FieldStrategy.ALWAYS)
private String settingsJson;

View File

@ -97,12 +97,16 @@ public class WorkspaceService {
public WorkspaceEntity update(WorkspaceEntity entity) {
WorkspaceEntity existing = getById(entity.getId());
// slug null 时保留原值不做修改
if (entity.getSlug() == null) {
entity.setSlug(existing.getSlug());
}
// 不允许修改默认工作区的 slug
if (DEFAULT_SLUG.equals(existing.getSlug()) && !DEFAULT_SLUG.equals(entity.getSlug())) {
throw new MateClawException("不能修改默认工作区的标识");
}
// 验证 slug 唯一性如果修改了 slug
if (entity.getSlug() != null && !entity.getSlug().equals(existing.getSlug())) {
if (!entity.getSlug().equals(existing.getSlug())) {
if (getBySlug(entity.getSlug()) != null) {
throw new MateClawException("工作区标识已存在: " + entity.getSlug());
}

View File

@ -5,6 +5,10 @@ spring:
username: ${DB_USERNAME:root}
password: ${DB_PASSWORD:mateclaw123}
flyway:
locations:
- classpath:db/migration/mysql
h2:
console:
enabled: false

View File

@ -25,6 +25,16 @@ spring:
init:
mode: never
# Flyway 数据库版本迁移
flyway:
enabled: true
baseline-on-migrate: true
baseline-version: "1"
locations:
- classpath:db/migration/h2
validate-on-migrate: true
clean-disabled: true
h2:
console:
enabled: true

View File

@ -1236,6 +1236,23 @@ VALUES (1000000008, 'WeChat', 'weixin', 1000000001, '', '{
'WeChat personal account channel (iLink Bot HTTP long polling). Get bot_token by scanning QR code to login, or enter existing token. Based on iLink Bot API, supports text, image, voice (ASR), file, and video messages', NOW(), NOW(), 0)
ON DUPLICATE KEY UPDATE name=VALUES(name), channel_type=VALUES(channel_type), agent_id=VALUES(agent_id), bot_prefix=VALUES(bot_prefix), config_json=VALUES(config_json), enabled=VALUES(enabled), description=VALUES(description), update_time=VALUES(update_time), deleted=VALUES(deleted);
-- 9. Slack (disabled by default, requires bot_token / app_token)
INSERT INTO mate_channel (id, name, channel_type, agent_id, bot_prefix, config_json, enabled, description, create_time, update_time, deleted)
VALUES (1000000009, 'Slack Bot', 'slack', 1000000001, '', '{
"bot_token": "",
"app_token": "",
"signing_secret": "",
"dm_policy": "open",
"group_policy": "mention",
"allow_from": [],
"deny_message": "Sorry, you do not have permission",
"filter_thinking": true,
"filter_tool_messages": true,
"message_format": "auto"
}', FALSE,
'Slack channel (Socket Mode). Get Bot Token (xoxb-) and App-Level Token (xapp-) from Slack App settings, enable Socket Mode to start using.', NOW(), NOW(), 0)
ON DUPLICATE KEY UPDATE name=VALUES(name), channel_type=VALUES(channel_type), agent_id=VALUES(agent_id), bot_prefix=VALUES(bot_prefix), config_json=VALUES(config_json), enabled=VALUES(enabled), description=VALUES(description), update_time=VALUES(update_time), deleted=VALUES(deleted);
-- ==================== Example Cron Jobs ====================
INSERT INTO mate_cron_job (id, name, cron_expression, timezone, agent_id, task_type, trigger_message, request_body, enabled, create_time, update_time, deleted)
VALUES (1000100001, 'Daily Greeting', '0 9 * * *', 'Asia/Shanghai', 1000000001, 'text', 'Good morning! Please give me today''s weather report and an inspirational quote.', NULL, TRUE, NOW(), NOW(), 0)

View File

@ -1238,6 +1238,23 @@ VALUES (1000000008, '微信', 'weixin', 1000000001, '', '{
'微信个人号渠道iLink Bot HTTP 长轮询)。通过扫描二维码登录获取 bot_token或直接填入已有 token。基于 iLink Bot API支持文本、图片、语音ASR、文件、视频消息', NOW(), NOW(), 0)
ON DUPLICATE KEY UPDATE name=VALUES(name), channel_type=VALUES(channel_type), agent_id=VALUES(agent_id), bot_prefix=VALUES(bot_prefix), config_json=VALUES(config_json), enabled=VALUES(enabled), description=VALUES(description), update_time=VALUES(update_time), deleted=VALUES(deleted);
-- 9. Slack默认禁用需配置 bot_token / app_token
INSERT INTO mate_channel (id, name, channel_type, agent_id, bot_prefix, config_json, enabled, description, create_time, update_time, deleted)
VALUES (1000000009, 'Slack Bot', 'slack', 1000000001, '', '{
"bot_token": "",
"app_token": "",
"signing_secret": "",
"dm_policy": "open",
"group_policy": "mention",
"allow_from": [],
"deny_message": "抱歉,您没有使用权限",
"filter_thinking": true,
"filter_tool_messages": true,
"message_format": "auto"
}', FALSE,
'Slack 渠道Socket Mode。在 Slack App 后台获取 Bot Tokenxoxb-)和 App-Level Tokenxapp-),启用 Socket Mode 后即可使用。', NOW(), NOW(), 0)
ON DUPLICATE KEY UPDATE name=VALUES(name), channel_type=VALUES(channel_type), agent_id=VALUES(agent_id), bot_prefix=VALUES(bot_prefix), config_json=VALUES(config_json), enabled=VALUES(enabled), description=VALUES(description), update_time=VALUES(update_time), deleted=VALUES(deleted);
-- ==================== 示例定时任务 ====================
INSERT INTO mate_cron_job (id, name, cron_expression, timezone, agent_id, task_type, trigger_message, request_body, enabled, create_time, update_time, deleted)
VALUES (1000100001, '每日问候', '0 9 * * *', 'Asia/Shanghai', 1000000001, 'text', '早上好!请给我今天的天气播报和一句励志名言。', NULL, TRUE, NOW(), NOW(), 0)

View File

@ -0,0 +1,642 @@
-- MateClaw 数据库初始化脚本
-- 兼容 H2开发模式和 MySQL 8.0+(生产模式)
-- 用户表
CREATE TABLE IF NOT EXISTS mate_user (
id BIGINT NOT NULL PRIMARY KEY,
username VARCHAR(64) NOT NULL UNIQUE,
password VARCHAR(200) NOT NULL,
nickname VARCHAR(64),
avatar VARCHAR(256),
email VARCHAR(128),
role VARCHAR(32) NOT NULL DEFAULT 'user',
enabled BOOLEAN NOT NULL DEFAULT TRUE,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
);
-- Agent 配置表
CREATE TABLE IF NOT EXISTS mate_agent (
id BIGINT NOT NULL PRIMARY KEY,
name VARCHAR(128) NOT NULL,
description TEXT,
agent_type VARCHAR(32) NOT NULL DEFAULT 'react',
system_prompt TEXT,
model_name VARCHAR(128),
max_iterations INT NOT NULL DEFAULT 10,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
icon VARCHAR(256),
tags VARCHAR(256),
workspace_id BIGINT NOT NULL DEFAULT 1,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
);
-- 模型配置表
CREATE TABLE IF NOT EXISTS mate_model_config (
id BIGINT NOT NULL PRIMARY KEY,
name VARCHAR(128) NOT NULL,
provider VARCHAR(64) NOT NULL DEFAULT 'dashscope',
model_name VARCHAR(128) NOT NULL,
description TEXT,
temperature DOUBLE,
max_tokens INT,
top_p DOUBLE,
builtin BOOLEAN NOT NULL DEFAULT TRUE,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
is_default BOOLEAN NOT NULL DEFAULT FALSE,
max_input_tokens INT DEFAULT 0,
enable_search BOOLEAN DEFAULT FALSE,
search_strategy VARCHAR(32) DEFAULT NULL,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
);
-- 模型 Provider 表
CREATE TABLE IF NOT EXISTS mate_model_provider (
provider_id VARCHAR(64) NOT NULL PRIMARY KEY,
name VARCHAR(128) NOT NULL,
api_key_prefix VARCHAR(32),
chat_model VARCHAR(64),
api_key VARCHAR(256),
base_url VARCHAR(512),
generate_kwargs TEXT,
is_custom BOOLEAN NOT NULL DEFAULT FALSE,
is_local BOOLEAN NOT NULL DEFAULT FALSE,
support_model_discovery BOOLEAN NOT NULL DEFAULT FALSE,
support_connection_check BOOLEAN NOT NULL DEFAULT FALSE,
freeze_url BOOLEAN NOT NULL DEFAULT FALSE,
require_api_key BOOLEAN NOT NULL DEFAULT TRUE,
auth_type VARCHAR(16) NOT NULL DEFAULT 'api_key',
oauth_access_token TEXT,
oauth_refresh_token TEXT,
oauth_expires_at BIGINT,
oauth_account_id VARCHAR(128),
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL
);
-- 系统设置表
CREATE TABLE IF NOT EXISTS mate_system_setting (
id BIGINT NOT NULL PRIMARY KEY,
setting_key VARCHAR(128) NOT NULL UNIQUE,
setting_value TEXT,
description VARCHAR(256),
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL
);
-- 技能表
CREATE TABLE IF NOT EXISTS mate_skill (
id BIGINT NOT NULL PRIMARY KEY,
name VARCHAR(128) NOT NULL,
description TEXT,
skill_type VARCHAR(32) NOT NULL DEFAULT 'dynamic',
icon VARCHAR(256),
version VARCHAR(32),
author VARCHAR(64),
config_json TEXT,
source_code TEXT,
skill_content TEXT,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
builtin BOOLEAN NOT NULL DEFAULT FALSE,
tags VARCHAR(256),
workspace_id BIGINT NOT NULL DEFAULT 1,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
);
-- 工具表
CREATE TABLE IF NOT EXISTS mate_tool (
id BIGINT NOT NULL PRIMARY KEY,
name VARCHAR(128) NOT NULL,
display_name VARCHAR(128),
description TEXT,
tool_type VARCHAR(32) NOT NULL DEFAULT 'builtin',
bean_name VARCHAR(128),
icon VARCHAR(256),
mcp_endpoint VARCHAR(256),
params_schema TEXT,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
builtin BOOLEAN NOT NULL DEFAULT FALSE,
workspace_id BIGINT NOT NULL DEFAULT 1,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
);
-- 渠道表
CREATE TABLE IF NOT EXISTS mate_channel (
id BIGINT NOT NULL PRIMARY KEY,
name VARCHAR(128) NOT NULL,
channel_type VARCHAR(32) NOT NULL,
agent_id BIGINT,
bot_prefix VARCHAR(64),
config_json TEXT,
enabled BOOLEAN NOT NULL DEFAULT FALSE,
description VARCHAR(256),
workspace_id BIGINT NOT NULL DEFAULT 1,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
);
-- 会话表
CREATE TABLE IF NOT EXISTS mate_conversation (
id BIGINT NOT NULL PRIMARY KEY,
conversation_id VARCHAR(64) NOT NULL UNIQUE,
title VARCHAR(256),
agent_id BIGINT,
username VARCHAR(64),
message_count INT NOT NULL DEFAULT 0,
last_message TEXT,
last_active_time DATETIME,
stream_status VARCHAR(16) NOT NULL DEFAULT 'idle',
workspace_id BIGINT NOT NULL DEFAULT 1,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
);
-- 消息表
CREATE TABLE IF NOT EXISTS mate_message (
id BIGINT NOT NULL PRIMARY KEY,
conversation_id VARCHAR(64) NOT NULL,
role VARCHAR(32) NOT NULL,
content TEXT,
content_parts TEXT,
tool_name VARCHAR(128),
token_usage INT,
prompt_tokens INT DEFAULT 0,
completion_tokens INT DEFAULT 0,
runtime_model VARCHAR(128),
runtime_provider VARCHAR(64),
status VARCHAR(32) NOT NULL DEFAULT 'completed',
metadata JSON, -- 存储 toolCalls, plan, currentPhase, pendingApproval 等元数据
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
);
-- 执行计划表
CREATE TABLE IF NOT EXISTS mate_plan (
id BIGINT NOT NULL PRIMARY KEY,
agent_id VARCHAR(64),
goal TEXT,
status VARCHAR(32) NOT NULL DEFAULT 'pending',
total_steps INT NOT NULL DEFAULT 0,
completed_steps INT NOT NULL DEFAULT 0,
summary TEXT,
start_time DATETIME,
end_time DATETIME,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
);
-- 子计划步骤表
CREATE TABLE IF NOT EXISTS mate_sub_plan (
id BIGINT NOT NULL PRIMARY KEY,
plan_id BIGINT NOT NULL,
step_index INT NOT NULL,
description TEXT,
status VARCHAR(32) NOT NULL DEFAULT 'pending',
result TEXT,
start_time DATETIME,
end_time DATETIME,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
);
-- 定时任务表
CREATE TABLE IF NOT EXISTS mate_cron_job (
id BIGINT NOT NULL PRIMARY KEY,
name VARCHAR(128) NOT NULL,
cron_expression VARCHAR(128) NOT NULL,
timezone VARCHAR(64) NOT NULL DEFAULT 'Asia/Shanghai',
agent_id BIGINT NOT NULL,
task_type VARCHAR(16) NOT NULL DEFAULT 'text',
trigger_message TEXT,
request_body TEXT,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
next_run_time DATETIME,
last_run_time DATETIME,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
);
-- 索引
CREATE INDEX IF NOT EXISTS idx_message_conversation ON mate_message(conversation_id);
CREATE INDEX IF NOT EXISTS idx_conversation_username ON mate_conversation(username);
CREATE INDEX IF NOT EXISTS idx_sub_plan_plan_id ON mate_sub_plan(plan_id);
CREATE INDEX IF NOT EXISTS idx_model_config_model_name ON mate_model_config(model_name);
-- 渠道会话存储表(主动推送标识缓存)
CREATE TABLE IF NOT EXISTS mate_channel_session (
id BIGINT NOT NULL PRIMARY KEY,
conversation_id VARCHAR(128) NOT NULL UNIQUE,
channel_type VARCHAR(32) NOT NULL,
target_id VARCHAR(512) NOT NULL,
sender_id VARCHAR(128),
sender_name VARCHAR(128),
channel_id BIGINT,
last_active_time DATETIME NOT NULL,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_channel_session_type ON mate_channel_session(channel_type);
CREATE INDEX IF NOT EXISTS idx_channel_session_channel_id ON mate_channel_session(channel_id);
-- 工作区文件表Agent 级 Markdown 文档管理)
CREATE TABLE IF NOT EXISTS mate_workspace_file (
id BIGINT NOT NULL PRIMARY KEY,
agent_id BIGINT NOT NULL,
filename VARCHAR(256) NOT NULL,
content CLOB,
file_size BIGINT NOT NULL DEFAULT 0,
enabled BOOLEAN NOT NULL DEFAULT FALSE,
sort_order INT NOT NULL DEFAULT 0,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_workspace_file_agent ON mate_workspace_file(agent_id);
-- ==================== MCP Server 管理 ====================
-- MCP Server 配置表(独立于 mate_tool一个 server 可暴露多个 tools
CREATE TABLE IF NOT EXISTS mate_mcp_server (
id BIGINT NOT NULL PRIMARY KEY,
name VARCHAR(128) NOT NULL,
description TEXT,
transport VARCHAR(32) NOT NULL DEFAULT 'stdio',
url VARCHAR(512),
headers_json TEXT,
command VARCHAR(512),
args_json TEXT,
env_json TEXT,
cwd VARCHAR(512),
enabled BOOLEAN NOT NULL DEFAULT TRUE,
connect_timeout_seconds INT NOT NULL DEFAULT 30,
read_timeout_seconds INT NOT NULL DEFAULT 30,
last_status VARCHAR(32) NOT NULL DEFAULT 'disconnected',
last_error TEXT,
last_connected_time DATETIME,
tool_count INT NOT NULL DEFAULT 0,
builtin BOOLEAN NOT NULL DEFAULT FALSE,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_mcp_server_enabled ON mate_mcp_server(enabled);
-- ==================== 工具安全治理ToolGuard ====================
-- 工具审批表
CREATE TABLE IF NOT EXISTS mate_tool_approval (
id BIGINT NOT NULL PRIMARY KEY,
pending_id VARCHAR(32) NOT NULL UNIQUE,
conversation_id VARCHAR(128) NOT NULL,
user_id VARCHAR(64),
agent_id VARCHAR(64),
channel_type VARCHAR(32),
requester_name VARCHAR(128),
reply_target VARCHAR(512),
tool_name VARCHAR(128) NOT NULL,
tool_arguments TEXT,
tool_call_payload TEXT,
tool_call_hash VARCHAR(64),
sibling_tool_calls TEXT,
summary TEXT,
findings_json TEXT,
max_severity VARCHAR(16),
status VARCHAR(32) NOT NULL DEFAULT 'PENDING',
resolved_by VARCHAR(64),
created_at DATETIME NOT NULL,
resolved_at DATETIME,
expire_at DATETIME,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_tool_approval_conv ON mate_tool_approval(conversation_id);
CREATE INDEX IF NOT EXISTS idx_tool_approval_status ON mate_tool_approval(status);
CREATE INDEX IF NOT EXISTS idx_tool_approval_pending_id ON mate_tool_approval(pending_id);
-- 安全规则表
CREATE TABLE IF NOT EXISTS mate_tool_guard_rule (
id BIGINT NOT NULL PRIMARY KEY,
rule_id VARCHAR(64) NOT NULL UNIQUE,
name VARCHAR(128) NOT NULL,
description TEXT,
tool_name VARCHAR(128),
param_name VARCHAR(128),
category VARCHAR(64) NOT NULL,
severity VARCHAR(16) NOT NULL,
decision VARCHAR(16) NOT NULL DEFAULT 'NEEDS_APPROVAL',
pattern VARCHAR(512) NOT NULL,
exclude_pattern VARCHAR(512),
remediation TEXT,
builtin BOOLEAN NOT NULL DEFAULT FALSE,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
priority INT NOT NULL DEFAULT 100,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
);
-- 安全全局配置表
CREATE TABLE IF NOT EXISTS mate_tool_guard_config (
id BIGINT NOT NULL PRIMARY KEY,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
guard_scope VARCHAR(32) NOT NULL DEFAULT 'all',
guarded_tools_json TEXT,
denied_tools_json TEXT,
file_guard_enabled BOOLEAN NOT NULL DEFAULT TRUE,
sensitive_paths_json TEXT,
audit_enabled BOOLEAN NOT NULL DEFAULT TRUE,
audit_min_severity VARCHAR(16) NOT NULL DEFAULT 'INFO',
audit_retention_days INT NOT NULL DEFAULT 90,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL
);
-- 安全审计日志表
CREATE TABLE IF NOT EXISTS mate_tool_guard_audit_log (
id BIGINT NOT NULL PRIMARY KEY,
conversation_id VARCHAR(128),
agent_id VARCHAR(64),
user_id VARCHAR(64),
channel_type VARCHAR(32),
tool_name VARCHAR(128) NOT NULL,
tool_params_json TEXT,
decision VARCHAR(16) NOT NULL,
max_severity VARCHAR(16),
findings_json TEXT,
pending_id VARCHAR(32),
replay_payload_hash VARCHAR(64),
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
);
-- 外部数据源表(查数功能)
CREATE TABLE IF NOT EXISTS mate_datasource (
id BIGINT NOT NULL PRIMARY KEY,
name VARCHAR(128) NOT NULL,
description VARCHAR(512),
db_type VARCHAR(32) NOT NULL,
host VARCHAR(256) NOT NULL,
port INT NOT NULL,
database_name VARCHAR(128) NOT NULL,
username VARCHAR(128),
password VARCHAR(512),
extra_params VARCHAR(512),
schema_name VARCHAR(128),
enabled BOOLEAN NOT NULL DEFAULT TRUE,
last_test_time DATETIME,
last_test_ok BOOLEAN,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_guard_audit_conv ON mate_tool_guard_audit_log(conversation_id);
CREATE INDEX IF NOT EXISTS idx_guard_audit_time ON mate_tool_guard_audit_log(create_time);
-- 记忆召回追踪表Dreaming 评分驱动记忆整合)
CREATE TABLE IF NOT EXISTS mate_memory_recall (
id BIGINT NOT NULL PRIMARY KEY,
agent_id BIGINT NOT NULL,
filename VARCHAR(256) NOT NULL,
snippet_hash VARCHAR(64),
snippet_preview VARCHAR(512),
recall_count INT NOT NULL DEFAULT 0,
daily_count INT NOT NULL DEFAULT 0,
query_hashes TEXT,
score DOUBLE NOT NULL DEFAULT 0.0,
last_recalled_at DATETIME,
promoted BOOLEAN NOT NULL DEFAULT FALSE,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_memory_recall_agent ON mate_memory_recall(agent_id);
CREATE INDEX IF NOT EXISTS idx_memory_recall_agent_file ON mate_memory_recall(agent_id, filename);
CREATE INDEX IF NOT EXISTS idx_memory_recall_score ON mate_memory_recall(agent_id, score);
CREATE INDEX IF NOT EXISTS idx_memory_recall_candidates ON mate_memory_recall(agent_id, promoted, deleted);
-- 补充复合索引(高频查询优化)
CREATE INDEX IF NOT EXISTS idx_message_conv_time ON mate_message(conversation_id, create_time);
CREATE INDEX IF NOT EXISTS idx_workspace_file_agent_enabled ON mate_workspace_file(agent_id, enabled);
-- 清理 Codex 不支持的 ChatGPT OAuth 模型gpt-4o, o3, o4-mini 在 Codex 模式下不可用)
DELETE FROM mate_model_config WHERE provider = 'openai-chatgpt' AND model_name IN ('gpt-4o', 'o3', 'o4-mini');
-- ==================== 异步任务(视频/图片生成等长耗时操作) ====================
CREATE TABLE IF NOT EXISTS mate_async_task (
id BIGINT NOT NULL PRIMARY KEY,
task_id VARCHAR(64) NOT NULL UNIQUE,
task_type VARCHAR(32) NOT NULL,
status VARCHAR(16) NOT NULL DEFAULT 'pending',
conversation_id VARCHAR(128),
message_id BIGINT,
provider_name VARCHAR(64),
provider_task_id VARCHAR(128),
request_json TEXT,
result_json TEXT,
error_message VARCHAR(512),
progress INT DEFAULT 0,
created_by VARCHAR(64),
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_async_task_conv ON mate_async_task(conversation_id);
CREATE INDEX IF NOT EXISTS idx_async_task_status ON mate_async_task(status);
-- ==================== Wiki 知识库 ====================
CREATE TABLE IF NOT EXISTS mate_wiki_knowledge_base (
id BIGINT NOT NULL PRIMARY KEY,
name VARCHAR(128) NOT NULL,
description TEXT,
agent_id BIGINT,
config_content CLOB,
source_directory VARCHAR(512),
status VARCHAR(32) NOT NULL DEFAULT 'active',
page_count INT NOT NULL DEFAULT 0,
raw_count INT NOT NULL DEFAULT 0,
workspace_id BIGINT NOT NULL DEFAULT 1,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_wiki_kb_agent ON mate_wiki_knowledge_base(agent_id);
CREATE TABLE IF NOT EXISTS mate_wiki_raw_material (
id BIGINT NOT NULL PRIMARY KEY,
kb_id BIGINT NOT NULL,
title VARCHAR(256) NOT NULL,
source_type VARCHAR(32) NOT NULL DEFAULT 'text',
source_path VARCHAR(512),
original_content CLOB,
extracted_text CLOB,
content_hash VARCHAR(64),
file_size BIGINT NOT NULL DEFAULT 0,
processing_status VARCHAR(32) NOT NULL DEFAULT 'pending',
last_processed_at DATETIME,
error_message VARCHAR(512),
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_wiki_raw_kb ON mate_wiki_raw_material(kb_id);
CREATE INDEX IF NOT EXISTS idx_wiki_raw_status ON mate_wiki_raw_material(kb_id, processing_status);
CREATE TABLE IF NOT EXISTS mate_wiki_page (
id BIGINT NOT NULL PRIMARY KEY,
kb_id BIGINT NOT NULL,
slug VARCHAR(256) NOT NULL,
title VARCHAR(256) NOT NULL,
content CLOB,
summary VARCHAR(1024),
outgoing_links CLOB,
source_raw_ids CLOB,
version INT NOT NULL DEFAULT 1,
last_updated_by VARCHAR(32) NOT NULL DEFAULT 'ai',
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0,
CONSTRAINT uk_wiki_page_kb_slug UNIQUE (kb_id, slug)
);
CREATE INDEX IF NOT EXISTS idx_wiki_page_kb ON mate_wiki_page(kb_id);
-- =============================================
-- 工作区表Phase 2
-- =============================================
-- 工作区
CREATE TABLE IF NOT EXISTS mate_workspace (
id BIGINT NOT NULL PRIMARY KEY,
name VARCHAR(128) NOT NULL,
slug VARCHAR(64) NOT NULL,
description VARCHAR(256),
owner_id BIGINT,
settings_json TEXT,
base_path VARCHAR(512),
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0,
CONSTRAINT uk_workspace_slug UNIQUE (slug)
);
-- 工作区成员
CREATE TABLE IF NOT EXISTS mate_workspace_member (
id BIGINT NOT NULL PRIMARY KEY,
workspace_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
role VARCHAR(32) NOT NULL DEFAULT 'member',
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_ws_member_workspace ON mate_workspace_member(workspace_id);
CREATE INDEX IF NOT EXISTS idx_ws_member_user ON mate_workspace_member(user_id);
-- =============================================
-- Agent-Skill / Agent-Tool 绑定表Phase 3 Sprint 2
-- =============================================
CREATE TABLE IF NOT EXISTS mate_agent_skill (
id BIGINT NOT NULL PRIMARY KEY,
agent_id BIGINT NOT NULL,
skill_id BIGINT NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
config_json TEXT,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
);
CREATE UNIQUE INDEX IF NOT EXISTS uk_agent_skill ON mate_agent_skill(agent_id, skill_id);
CREATE TABLE IF NOT EXISTS mate_agent_tool (
id BIGINT NOT NULL PRIMARY KEY,
agent_id BIGINT NOT NULL,
tool_name VARCHAR(128) NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
);
CREATE UNIQUE INDEX IF NOT EXISTS uk_agent_tool ON mate_agent_tool(agent_id, tool_name);
-- =============================================
-- CronJob 执行历史Phase 3 Sprint 3
-- =============================================
CREATE TABLE IF NOT EXISTS mate_cron_job_run (
id BIGINT NOT NULL PRIMARY KEY,
cron_job_id BIGINT NOT NULL,
conversation_id VARCHAR(64),
status VARCHAR(32) NOT NULL,
trigger_type VARCHAR(32) NOT NULL DEFAULT 'scheduled',
started_at DATETIME NOT NULL,
finished_at DATETIME,
error_message TEXT,
token_usage INT DEFAULT 0,
create_time DATETIME NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_cron_run_job ON mate_cron_job_run(cron_job_id, started_at);
-- =============================================
-- 用量日统计Phase 3 Sprint 3
-- =============================================
CREATE TABLE IF NOT EXISTS mate_usage_daily (
id BIGINT NOT NULL PRIMARY KEY,
workspace_id BIGINT NOT NULL,
agent_id BIGINT,
stat_date DATE NOT NULL,
conversation_count INT DEFAULT 0,
message_count INT DEFAULT 0,
total_tokens BIGINT DEFAULT 0,
prompt_tokens BIGINT DEFAULT 0,
completion_tokens BIGINT DEFAULT 0,
tool_call_count INT DEFAULT 0,
error_count INT DEFAULT 0,
create_time DATETIME NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS uk_usage_daily ON mate_usage_daily(workspace_id, agent_id, stat_date);
-- =============================================
-- 操作审计事件表Phase 3 Sprint 1
-- =============================================
CREATE TABLE IF NOT EXISTS mate_audit_event (
id BIGINT NOT NULL PRIMARY KEY,
workspace_id BIGINT,
user_id BIGINT NOT NULL,
username VARCHAR(64) NOT NULL,
action VARCHAR(64) NOT NULL,
resource_type VARCHAR(64) NOT NULL,
resource_id VARCHAR(128),
resource_name VARCHAR(256),
detail_json TEXT,
ip_address VARCHAR(64),
user_agent VARCHAR(256),
create_time DATETIME NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_audit_ws_time ON mate_audit_event(workspace_id, create_time);
CREATE INDEX IF NOT EXISTS idx_audit_user ON mate_audit_event(user_id);
CREATE INDEX IF NOT EXISTS idx_audit_resource ON mate_audit_event(resource_type, resource_id);

View File

@ -0,0 +1,629 @@
-- MateClaw 数据库初始化脚本MySQL / MariaDB 专用)
-- 用户表
CREATE TABLE IF NOT EXISTS mate_user (
id BIGINT NOT NULL PRIMARY KEY,
username VARCHAR(64) NOT NULL UNIQUE,
password VARCHAR(200) NOT NULL,
nickname VARCHAR(64),
avatar VARCHAR(256),
email VARCHAR(128),
role VARCHAR(32) NOT NULL DEFAULT 'user',
enabled TINYINT(1) NOT NULL DEFAULT 1,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Agent 配置表
CREATE TABLE IF NOT EXISTS mate_agent (
id BIGINT NOT NULL PRIMARY KEY,
name VARCHAR(128) NOT NULL,
description TEXT,
agent_type VARCHAR(32) NOT NULL DEFAULT 'react',
system_prompt TEXT,
model_name VARCHAR(128),
max_iterations INT NOT NULL DEFAULT 10,
enabled TINYINT(1) NOT NULL DEFAULT 1,
icon VARCHAR(256),
tags VARCHAR(256),
workspace_id BIGINT NOT NULL DEFAULT 1,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 模型配置表
CREATE TABLE IF NOT EXISTS mate_model_config (
id BIGINT NOT NULL PRIMARY KEY,
name VARCHAR(128) NOT NULL,
provider VARCHAR(64) NOT NULL DEFAULT 'dashscope',
model_name VARCHAR(128) NOT NULL,
description TEXT,
temperature DOUBLE,
max_tokens INT,
top_p DOUBLE,
builtin TINYINT(1) NOT NULL DEFAULT 1,
enabled TINYINT(1) NOT NULL DEFAULT 1,
is_default TINYINT(1) NOT NULL DEFAULT 0,
max_input_tokens INT DEFAULT 0,
enable_search TINYINT(1) DEFAULT 0,
search_strategy VARCHAR(32) DEFAULT NULL,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0,
INDEX idx_model_config_model_name (model_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 模型 Provider 表
CREATE TABLE IF NOT EXISTS mate_model_provider (
provider_id VARCHAR(64) NOT NULL PRIMARY KEY,
name VARCHAR(128) NOT NULL,
api_key_prefix VARCHAR(32),
chat_model VARCHAR(64),
api_key VARCHAR(256),
base_url VARCHAR(512),
generate_kwargs TEXT,
is_custom TINYINT(1) NOT NULL DEFAULT 0,
is_local TINYINT(1) NOT NULL DEFAULT 0,
support_model_discovery TINYINT(1) NOT NULL DEFAULT 0,
support_connection_check TINYINT(1) NOT NULL DEFAULT 0,
freeze_url TINYINT(1) NOT NULL DEFAULT 0,
require_api_key TINYINT(1) NOT NULL DEFAULT 1,
auth_type VARCHAR(16) NOT NULL DEFAULT 'api_key',
oauth_access_token TEXT,
oauth_refresh_token TEXT,
oauth_expires_at BIGINT,
oauth_account_id VARCHAR(128),
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 系统设置表
CREATE TABLE IF NOT EXISTS mate_system_setting (
id BIGINT NOT NULL PRIMARY KEY,
setting_key VARCHAR(128) NOT NULL UNIQUE,
setting_value TEXT,
description VARCHAR(256),
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 技能表
CREATE TABLE IF NOT EXISTS mate_skill (
id BIGINT NOT NULL PRIMARY KEY,
name VARCHAR(128) NOT NULL,
description TEXT,
skill_type VARCHAR(32) NOT NULL DEFAULT 'dynamic',
icon VARCHAR(256),
version VARCHAR(32),
author VARCHAR(64),
config_json TEXT,
source_code TEXT,
skill_content TEXT,
enabled TINYINT(1) NOT NULL DEFAULT 1,
builtin TINYINT(1) NOT NULL DEFAULT 0,
tags VARCHAR(256),
workspace_id BIGINT NOT NULL DEFAULT 1,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 工具表
CREATE TABLE IF NOT EXISTS mate_tool (
id BIGINT NOT NULL PRIMARY KEY,
name VARCHAR(128) NOT NULL,
display_name VARCHAR(128),
description TEXT,
tool_type VARCHAR(32) NOT NULL DEFAULT 'builtin',
bean_name VARCHAR(128),
icon VARCHAR(256),
mcp_endpoint VARCHAR(256),
params_schema TEXT,
enabled TINYINT(1) NOT NULL DEFAULT 1,
builtin TINYINT(1) NOT NULL DEFAULT 0,
workspace_id BIGINT NOT NULL DEFAULT 1,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 渠道表
CREATE TABLE IF NOT EXISTS mate_channel (
id BIGINT NOT NULL PRIMARY KEY,
name VARCHAR(128) NOT NULL,
channel_type VARCHAR(32) NOT NULL,
agent_id BIGINT,
bot_prefix VARCHAR(64),
config_json TEXT,
enabled TINYINT(1) NOT NULL DEFAULT 0,
description VARCHAR(256),
workspace_id BIGINT NOT NULL DEFAULT 1,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 会话表
CREATE TABLE IF NOT EXISTS mate_conversation (
id BIGINT NOT NULL PRIMARY KEY,
conversation_id VARCHAR(64) NOT NULL UNIQUE,
title VARCHAR(256),
agent_id BIGINT,
username VARCHAR(64),
message_count INT NOT NULL DEFAULT 0,
last_message TEXT,
last_active_time DATETIME,
stream_status VARCHAR(16) NOT NULL DEFAULT 'idle',
workspace_id BIGINT NOT NULL DEFAULT 1,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0,
INDEX idx_conversation_username (username)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 消息表
CREATE TABLE IF NOT EXISTS mate_message (
id BIGINT NOT NULL PRIMARY KEY,
conversation_id VARCHAR(64) NOT NULL,
role VARCHAR(32) NOT NULL,
content TEXT,
content_parts TEXT,
tool_name VARCHAR(128),
token_usage INT,
prompt_tokens INT DEFAULT 0,
completion_tokens INT DEFAULT 0,
runtime_model VARCHAR(128),
runtime_provider VARCHAR(64),
status VARCHAR(32) NOT NULL DEFAULT 'completed',
metadata JSON COMMENT '存储 toolCalls, plan, currentPhase, pendingApproval 等元数据',
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0,
INDEX idx_message_conversation (conversation_id),
INDEX idx_message_conv_time (conversation_id, create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 执行计划表
CREATE TABLE IF NOT EXISTS mate_plan (
id BIGINT NOT NULL PRIMARY KEY,
agent_id VARCHAR(64),
goal TEXT,
status VARCHAR(32) NOT NULL DEFAULT 'pending',
total_steps INT NOT NULL DEFAULT 0,
completed_steps INT NOT NULL DEFAULT 0,
summary TEXT,
start_time DATETIME,
end_time DATETIME,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 子计划步骤表
CREATE TABLE IF NOT EXISTS mate_sub_plan (
id BIGINT NOT NULL PRIMARY KEY,
plan_id BIGINT NOT NULL,
step_index INT NOT NULL,
description TEXT,
status VARCHAR(32) NOT NULL DEFAULT 'pending',
result TEXT,
start_time DATETIME,
end_time DATETIME,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0,
INDEX idx_sub_plan_plan_id (plan_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 定时任务表
CREATE TABLE IF NOT EXISTS mate_cron_job (
id BIGINT NOT NULL PRIMARY KEY,
name VARCHAR(128) NOT NULL,
cron_expression VARCHAR(128) NOT NULL,
timezone VARCHAR(64) NOT NULL DEFAULT 'Asia/Shanghai',
agent_id BIGINT NOT NULL,
task_type VARCHAR(16) NOT NULL DEFAULT 'text',
trigger_message TEXT,
request_body TEXT,
enabled TINYINT(1) NOT NULL DEFAULT 1,
next_run_time DATETIME,
last_run_time DATETIME,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 渠道会话存储表
CREATE TABLE IF NOT EXISTS mate_channel_session (
id BIGINT NOT NULL PRIMARY KEY,
conversation_id VARCHAR(128) NOT NULL UNIQUE,
channel_type VARCHAR(32) NOT NULL,
target_id VARCHAR(512) NOT NULL,
sender_id VARCHAR(128),
sender_name VARCHAR(128),
channel_id BIGINT,
last_active_time DATETIME NOT NULL,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0,
INDEX idx_channel_session_type (channel_type),
INDEX idx_channel_session_channel_id (channel_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 工作区文件表Agent 级 Markdown 文档管理)
CREATE TABLE IF NOT EXISTS mate_workspace_file (
id BIGINT NOT NULL PRIMARY KEY,
agent_id BIGINT NOT NULL,
filename VARCHAR(256) NOT NULL,
content LONGTEXT,
file_size BIGINT NOT NULL DEFAULT 0,
enabled TINYINT(1) NOT NULL DEFAULT 0,
sort_order INT NOT NULL DEFAULT 0,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0,
INDEX idx_workspace_file_agent (agent_id),
INDEX idx_workspace_file_agent_enabled (agent_id, enabled)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ==================== MCP Server 管理 ====================
CREATE TABLE IF NOT EXISTS mate_mcp_server (
id BIGINT NOT NULL PRIMARY KEY,
name VARCHAR(128) NOT NULL,
description TEXT,
transport VARCHAR(32) NOT NULL DEFAULT 'stdio',
url VARCHAR(512),
headers_json TEXT,
command VARCHAR(512),
args_json TEXT,
env_json TEXT,
cwd VARCHAR(512),
enabled TINYINT(1) NOT NULL DEFAULT 1,
connect_timeout_seconds INT NOT NULL DEFAULT 30,
read_timeout_seconds INT NOT NULL DEFAULT 30,
last_status VARCHAR(32) NOT NULL DEFAULT 'disconnected',
last_error TEXT,
last_connected_time DATETIME,
tool_count INT NOT NULL DEFAULT 0,
builtin TINYINT(1) NOT NULL DEFAULT 0,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0,
INDEX idx_mcp_server_enabled (enabled)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ==================== 工具安全治理ToolGuard ====================
-- 工具审批表
CREATE TABLE IF NOT EXISTS mate_tool_approval (
id BIGINT NOT NULL PRIMARY KEY,
pending_id VARCHAR(32) NOT NULL UNIQUE,
conversation_id VARCHAR(128) NOT NULL,
user_id VARCHAR(64),
agent_id VARCHAR(64),
channel_type VARCHAR(32),
requester_name VARCHAR(128),
reply_target VARCHAR(512),
tool_name VARCHAR(128) NOT NULL,
tool_arguments TEXT,
tool_call_payload TEXT,
tool_call_hash VARCHAR(64),
sibling_tool_calls TEXT,
summary TEXT,
findings_json TEXT,
max_severity VARCHAR(16),
status VARCHAR(32) NOT NULL DEFAULT 'PENDING',
resolved_by VARCHAR(64),
created_at DATETIME NOT NULL,
resolved_at DATETIME,
expire_at DATETIME,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0,
INDEX idx_tool_approval_conv (conversation_id),
INDEX idx_tool_approval_status (status),
INDEX idx_tool_approval_pending_id (pending_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 安全规则表
CREATE TABLE IF NOT EXISTS mate_tool_guard_rule (
id BIGINT NOT NULL PRIMARY KEY,
rule_id VARCHAR(64) NOT NULL UNIQUE,
name VARCHAR(128) NOT NULL,
description TEXT,
tool_name VARCHAR(128),
param_name VARCHAR(128),
category VARCHAR(64) NOT NULL,
severity VARCHAR(16) NOT NULL,
decision VARCHAR(16) NOT NULL DEFAULT 'NEEDS_APPROVAL',
pattern VARCHAR(512) NOT NULL,
exclude_pattern VARCHAR(512),
remediation TEXT,
builtin TINYINT(1) NOT NULL DEFAULT 0,
enabled TINYINT(1) NOT NULL DEFAULT 1,
priority INT NOT NULL DEFAULT 100,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 安全全局配置表
CREATE TABLE IF NOT EXISTS mate_tool_guard_config (
id BIGINT NOT NULL PRIMARY KEY,
enabled TINYINT(1) NOT NULL DEFAULT 1,
guard_scope VARCHAR(32) NOT NULL DEFAULT 'all',
guarded_tools_json TEXT,
denied_tools_json TEXT,
file_guard_enabled TINYINT(1) NOT NULL DEFAULT 1,
sensitive_paths_json TEXT,
audit_enabled TINYINT(1) NOT NULL DEFAULT 1,
audit_min_severity VARCHAR(16) NOT NULL DEFAULT 'INFO',
audit_retention_days INT NOT NULL DEFAULT 90,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 安全审计日志表
CREATE TABLE IF NOT EXISTS mate_tool_guard_audit_log (
id BIGINT NOT NULL PRIMARY KEY,
conversation_id VARCHAR(128),
agent_id VARCHAR(64),
user_id VARCHAR(64),
channel_type VARCHAR(32),
tool_name VARCHAR(128) NOT NULL,
tool_params_json TEXT,
decision VARCHAR(16) NOT NULL,
max_severity VARCHAR(16),
findings_json TEXT,
pending_id VARCHAR(32),
replay_payload_hash VARCHAR(64),
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0,
INDEX idx_guard_audit_conv (conversation_id),
INDEX idx_guard_audit_time (create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ==================== 外部数据源 ====================
CREATE TABLE IF NOT EXISTS mate_datasource (
id BIGINT NOT NULL PRIMARY KEY,
name VARCHAR(128) NOT NULL,
description VARCHAR(512),
db_type VARCHAR(32) NOT NULL,
host VARCHAR(256) NOT NULL,
port INT NOT NULL,
database_name VARCHAR(128) NOT NULL,
username VARCHAR(128),
password VARCHAR(512),
extra_params VARCHAR(512),
schema_name VARCHAR(128),
enabled TINYINT(1) NOT NULL DEFAULT 1,
last_test_time DATETIME,
last_test_ok TINYINT(1),
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ==================== 异步任务(视频/图片生成等长耗时操作) ====================
CREATE TABLE IF NOT EXISTS mate_async_task (
id BIGINT NOT NULL PRIMARY KEY,
task_id VARCHAR(64) NOT NULL,
task_type VARCHAR(32) NOT NULL,
status VARCHAR(16) NOT NULL DEFAULT 'pending',
conversation_id VARCHAR(128),
message_id BIGINT,
provider_name VARCHAR(64),
provider_task_id VARCHAR(128),
request_json TEXT,
result_json TEXT,
error_message VARCHAR(512),
progress INT DEFAULT 0,
created_by VARCHAR(64),
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
UNIQUE KEY uk_async_task_taskid (task_id),
INDEX idx_async_task_conv (conversation_id),
INDEX idx_async_task_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ==================== 记忆召回追踪 ====================
CREATE TABLE IF NOT EXISTS mate_memory_recall (
id BIGINT NOT NULL PRIMARY KEY,
agent_id BIGINT NOT NULL,
filename VARCHAR(256) NOT NULL,
snippet_hash VARCHAR(64),
snippet_preview VARCHAR(512),
recall_count INT NOT NULL DEFAULT 0,
daily_count INT NOT NULL DEFAULT 0,
query_hashes TEXT,
score DOUBLE NOT NULL DEFAULT 0.0,
last_recalled_at DATETIME,
promoted TINYINT(1) NOT NULL DEFAULT 0,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0,
INDEX idx_memory_recall_agent (agent_id),
INDEX idx_memory_recall_agent_file (agent_id, filename),
INDEX idx_memory_recall_score (agent_id, score),
INDEX idx_memory_recall_candidates (agent_id, promoted, deleted)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ==================== Wiki 知识库 ====================
CREATE TABLE IF NOT EXISTS mate_wiki_knowledge_base (
id BIGINT NOT NULL PRIMARY KEY,
name VARCHAR(128) NOT NULL,
description TEXT,
agent_id BIGINT,
config_content LONGTEXT,
source_directory VARCHAR(512),
status VARCHAR(32) NOT NULL DEFAULT 'active',
page_count INT NOT NULL DEFAULT 0,
raw_count INT NOT NULL DEFAULT 0,
workspace_id BIGINT NOT NULL DEFAULT 1,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0,
INDEX idx_wiki_kb_agent (agent_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS mate_wiki_raw_material (
id BIGINT NOT NULL PRIMARY KEY,
kb_id BIGINT NOT NULL,
title VARCHAR(256) NOT NULL,
source_type VARCHAR(32) NOT NULL DEFAULT 'text',
source_path VARCHAR(512),
original_content LONGTEXT,
extracted_text LONGTEXT,
content_hash VARCHAR(64),
file_size BIGINT NOT NULL DEFAULT 0,
processing_status VARCHAR(32) NOT NULL DEFAULT 'pending',
last_processed_at DATETIME,
error_message VARCHAR(512),
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0,
INDEX idx_wiki_raw_kb (kb_id),
INDEX idx_wiki_raw_status (kb_id, processing_status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS mate_wiki_page (
id BIGINT NOT NULL PRIMARY KEY,
kb_id BIGINT NOT NULL,
slug VARCHAR(256) NOT NULL,
title VARCHAR(256) NOT NULL,
content LONGTEXT,
summary VARCHAR(1024),
outgoing_links LONGTEXT,
source_raw_ids LONGTEXT,
version INT NOT NULL DEFAULT 1,
last_updated_by VARCHAR(32) NOT NULL DEFAULT 'ai',
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0,
UNIQUE KEY uk_wiki_page_kb_slug (kb_id, slug),
INDEX idx_wiki_page_kb (kb_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- =============================================
-- 工作区表Phase 2
-- =============================================
-- 工作区
CREATE TABLE IF NOT EXISTS mate_workspace (
id BIGINT NOT NULL PRIMARY KEY,
name VARCHAR(128) NOT NULL,
slug VARCHAR(64) NOT NULL,
description VARCHAR(256),
owner_id BIGINT,
settings_json TEXT,
base_path VARCHAR(512),
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0,
UNIQUE KEY uk_workspace_slug (slug)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 工作区成员
CREATE TABLE IF NOT EXISTS mate_workspace_member (
id BIGINT NOT NULL PRIMARY KEY,
workspace_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
role VARCHAR(32) NOT NULL DEFAULT 'member',
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0,
INDEX idx_ws_member_workspace (workspace_id),
INDEX idx_ws_member_user (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- =============================================
-- Agent-Skill / Agent-Tool 绑定表Phase 3 Sprint 2
-- =============================================
CREATE TABLE IF NOT EXISTS mate_agent_skill (
id BIGINT NOT NULL PRIMARY KEY,
agent_id BIGINT NOT NULL,
skill_id BIGINT NOT NULL,
enabled TINYINT(1) NOT NULL DEFAULT 1,
config_json TEXT,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0,
UNIQUE KEY uk_agent_skill (agent_id, skill_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS mate_agent_tool (
id BIGINT NOT NULL PRIMARY KEY,
agent_id BIGINT NOT NULL,
tool_name VARCHAR(128) NOT NULL,
enabled TINYINT(1) NOT NULL DEFAULT 1,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0,
UNIQUE KEY uk_agent_tool (agent_id, tool_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- =============================================
-- CronJob 执行历史Phase 3 Sprint 3
-- =============================================
CREATE TABLE IF NOT EXISTS mate_cron_job_run (
id BIGINT NOT NULL PRIMARY KEY,
cron_job_id BIGINT NOT NULL,
conversation_id VARCHAR(64),
status VARCHAR(32) NOT NULL,
trigger_type VARCHAR(32) NOT NULL DEFAULT 'scheduled',
started_at DATETIME NOT NULL,
finished_at DATETIME,
error_message TEXT,
token_usage INT DEFAULT 0,
create_time DATETIME NOT NULL,
INDEX idx_cron_run_job (cron_job_id, started_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS mate_usage_daily (
id BIGINT NOT NULL PRIMARY KEY,
workspace_id BIGINT NOT NULL,
agent_id BIGINT,
stat_date DATE NOT NULL,
conversation_count INT DEFAULT 0,
message_count INT DEFAULT 0,
total_tokens BIGINT DEFAULT 0,
prompt_tokens BIGINT DEFAULT 0,
completion_tokens BIGINT DEFAULT 0,
tool_call_count INT DEFAULT 0,
error_count INT DEFAULT 0,
create_time DATETIME NOT NULL,
UNIQUE KEY uk_usage_daily (workspace_id, agent_id, stat_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- =============================================
-- 操作审计事件表Phase 3 Sprint 1
-- =============================================
CREATE TABLE IF NOT EXISTS mate_audit_event (
id BIGINT NOT NULL PRIMARY KEY,
workspace_id BIGINT,
user_id BIGINT NOT NULL,
username VARCHAR(64) NOT NULL,
action VARCHAR(64) NOT NULL,
resource_type VARCHAR(64) NOT NULL,
resource_id VARCHAR(128),
resource_name VARCHAR(256),
detail_json TEXT,
ip_address VARCHAR(64),
user_agent VARCHAR(256),
create_time DATETIME NOT NULL,
INDEX idx_audit_ws_time (workspace_id, create_time),
INDEX idx_audit_user (user_id),
INDEX idx_audit_resource (resource_type, resource_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 清理 Codex 不支持的 ChatGPT OAuth 模型gpt-4o, o3, o4-mini 在 Codex 模式下不可用)
DELETE FROM mate_model_config WHERE provider = 'openai-chatgpt' AND model_name IN ('gpt-4o', 'o3', 'o4-mini');

View File

@ -27,6 +27,7 @@ CREATE TABLE IF NOT EXISTS mate_agent (
enabled TINYINT(1) NOT NULL DEFAULT 1,
icon VARCHAR(256),
tags VARCHAR(256),
workspace_id BIGINT NOT NULL DEFAULT 1,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
@ -103,6 +104,7 @@ CREATE TABLE IF NOT EXISTS mate_skill (
enabled TINYINT(1) NOT NULL DEFAULT 1,
builtin TINYINT(1) NOT NULL DEFAULT 0,
tags VARCHAR(256),
workspace_id BIGINT NOT NULL DEFAULT 1,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
@ -121,6 +123,7 @@ CREATE TABLE IF NOT EXISTS mate_tool (
params_schema TEXT,
enabled TINYINT(1) NOT NULL DEFAULT 1,
builtin TINYINT(1) NOT NULL DEFAULT 0,
workspace_id BIGINT NOT NULL DEFAULT 1,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
@ -136,6 +139,7 @@ CREATE TABLE IF NOT EXISTS mate_channel (
config_json TEXT,
enabled TINYINT(1) NOT NULL DEFAULT 0,
description VARCHAR(256),
workspace_id BIGINT NOT NULL DEFAULT 1,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
@ -152,6 +156,7 @@ CREATE TABLE IF NOT EXISTS mate_conversation (
last_message TEXT,
last_active_time DATETIME,
stream_status VARCHAR(16) NOT NULL DEFAULT 'idle',
workspace_id BIGINT NOT NULL DEFAULT 1,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0,
@ -176,7 +181,8 @@ CREATE TABLE IF NOT EXISTS mate_message (
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0,
INDEX idx_message_conversation (conversation_id)
INDEX idx_message_conversation (conversation_id),
INDEX idx_message_conv_time (conversation_id, create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 执行计划表
@ -258,7 +264,8 @@ CREATE TABLE IF NOT EXISTS mate_workspace_file (
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0,
INDEX idx_workspace_file_agent (agent_id)
INDEX idx_workspace_file_agent (agent_id),
INDEX idx_workspace_file_agent_enabled (agent_id, enabled)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ==================== MCP Server 管理 ====================
@ -460,6 +467,7 @@ CREATE TABLE IF NOT EXISTS mate_wiki_knowledge_base (
status VARCHAR(32) NOT NULL DEFAULT 'active',
page_count INT NOT NULL DEFAULT 0,
raw_count INT NOT NULL DEFAULT 0,
workspace_id BIGINT NOT NULL DEFAULT 1,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0,
@ -516,6 +524,7 @@ CREATE TABLE IF NOT EXISTS mate_workspace (
description VARCHAR(256),
owner_id BIGINT,
settings_json TEXT,
base_path VARCHAR(512),
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0,
@ -535,62 +544,32 @@ CREATE TABLE IF NOT EXISTS mate_workspace_member (
INDEX idx_ws_member_user (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 现有表增加 workspace_id 列(幂等)
-- MySQL 不支持 ADD COLUMN IF NOT EXISTS使用存储过程处理
DROP PROCEDURE IF EXISTS mate_add_workspace_id;
DELIMITER $$
CREATE PROCEDURE mate_add_workspace_id()
BEGIN
IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='mate_agent' AND COLUMN_NAME='workspace_id') THEN
ALTER TABLE mate_agent ADD COLUMN workspace_id BIGINT NOT NULL DEFAULT 1;
END IF;
IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='mate_channel' AND COLUMN_NAME='workspace_id') THEN
ALTER TABLE mate_channel ADD COLUMN workspace_id BIGINT NOT NULL DEFAULT 1;
END IF;
IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='mate_conversation' AND COLUMN_NAME='workspace_id') THEN
ALTER TABLE mate_conversation ADD COLUMN workspace_id BIGINT NOT NULL DEFAULT 1;
END IF;
IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='mate_wiki_knowledge_base' AND COLUMN_NAME='workspace_id') THEN
ALTER TABLE mate_wiki_knowledge_base ADD COLUMN workspace_id BIGINT NOT NULL DEFAULT 1;
END IF;
IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='mate_tool' AND COLUMN_NAME='workspace_id') THEN
ALTER TABLE mate_tool ADD COLUMN workspace_id BIGINT NOT NULL DEFAULT 1;
END IF;
IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='mate_skill' AND COLUMN_NAME='workspace_id') THEN
ALTER TABLE mate_skill ADD COLUMN workspace_id BIGINT NOT NULL DEFAULT 1;
END IF;
END$$
DELIMITER ;
CALL mate_add_workspace_id();
DROP PROCEDURE IF EXISTS mate_add_workspace_id;
-- =============================================
-- Agent-Skill / Agent-Tool 绑定表Phase 3 Sprint 2
-- =============================================
CREATE TABLE IF NOT EXISTS mate_agent_skill (
id BIGINT NOT NULL PRIMARY KEY,
agent_id BIGINT NOT NULL,
skill_id BIGINT NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
id BIGINT NOT NULL PRIMARY KEY,
agent_id BIGINT NOT NULL,
skill_id BIGINT NOT NULL,
enabled TINYINT(1) NOT NULL DEFAULT 1,
config_json TEXT,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0,
UNIQUE KEY uk_agent_skill (agent_id, skill_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS mate_agent_tool (
id BIGINT NOT NULL PRIMARY KEY,
agent_id BIGINT NOT NULL,
id BIGINT NOT NULL PRIMARY KEY,
agent_id BIGINT NOT NULL,
tool_name VARCHAR(128) NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0,
enabled TINYINT(1) NOT NULL DEFAULT 1,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0,
UNIQUE KEY uk_agent_tool (agent_id, tool_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- =============================================
-- CronJob 执行历史Phase 3 Sprint 3
@ -607,7 +586,7 @@ CREATE TABLE IF NOT EXISTS mate_cron_job_run (
token_usage INT DEFAULT 0,
create_time DATETIME NOT NULL,
INDEX idx_cron_run_job (cron_job_id, started_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS mate_usage_daily (
id BIGINT NOT NULL PRIMARY KEY,
@ -623,7 +602,7 @@ CREATE TABLE IF NOT EXISTS mate_usage_daily (
error_count INT DEFAULT 0,
create_time DATETIME NOT NULL,
UNIQUE KEY uk_usage_daily (workspace_id, agent_id, stat_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- =============================================
-- 操作审计事件表Phase 3 Sprint 1
@ -644,4 +623,7 @@ CREATE TABLE IF NOT EXISTS mate_audit_event (
INDEX idx_audit_ws_time (workspace_id, create_time),
INDEX idx_audit_user (user_id),
INDEX idx_audit_resource (resource_type, resource_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 清理 Codex 不支持的 ChatGPT OAuth 模型gpt-4o, o3, o4-mini 在 Codex 模式下不可用)
DELETE FROM mate_model_config WHERE provider = 'openai-chatgpt' AND model_name IN ('gpt-4o', 'o3', 'o4-mini');

View File

@ -28,6 +28,7 @@ CREATE TABLE IF NOT EXISTS mate_agent (
enabled BOOLEAN NOT NULL DEFAULT TRUE,
icon VARCHAR(256),
tags VARCHAR(256),
workspace_id BIGINT NOT NULL DEFAULT 1,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
@ -46,6 +47,9 @@ CREATE TABLE IF NOT EXISTS mate_model_config (
builtin BOOLEAN NOT NULL DEFAULT TRUE,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
is_default BOOLEAN NOT NULL DEFAULT FALSE,
max_input_tokens INT DEFAULT 0,
enable_search BOOLEAN DEFAULT FALSE,
search_strategy VARCHAR(32) DEFAULT NULL,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
@ -66,6 +70,11 @@ CREATE TABLE IF NOT EXISTS mate_model_provider (
support_connection_check BOOLEAN NOT NULL DEFAULT FALSE,
freeze_url BOOLEAN NOT NULL DEFAULT FALSE,
require_api_key BOOLEAN NOT NULL DEFAULT TRUE,
auth_type VARCHAR(16) NOT NULL DEFAULT 'api_key',
oauth_access_token TEXT,
oauth_refresh_token TEXT,
oauth_expires_at BIGINT,
oauth_account_id VARCHAR(128),
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL
);
@ -80,11 +89,6 @@ CREATE TABLE IF NOT EXISTS mate_system_setting (
update_time DATETIME NOT NULL
);
ALTER TABLE mate_model_config ADD COLUMN IF NOT EXISTS builtin BOOLEAN NOT NULL DEFAULT TRUE;
ALTER TABLE mate_model_config ADD COLUMN IF NOT EXISTS max_input_tokens INT DEFAULT 0;
ALTER TABLE mate_model_config ADD COLUMN IF NOT EXISTS enable_search BOOLEAN DEFAULT FALSE;
ALTER TABLE mate_model_config ADD COLUMN IF NOT EXISTS search_strategy VARCHAR(32) DEFAULT NULL;
-- 技能表
CREATE TABLE IF NOT EXISTS mate_skill (
id BIGINT NOT NULL PRIMARY KEY,
@ -100,6 +104,7 @@ CREATE TABLE IF NOT EXISTS mate_skill (
enabled BOOLEAN NOT NULL DEFAULT TRUE,
builtin BOOLEAN NOT NULL DEFAULT FALSE,
tags VARCHAR(256),
workspace_id BIGINT NOT NULL DEFAULT 1,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
@ -118,6 +123,7 @@ CREATE TABLE IF NOT EXISTS mate_tool (
params_schema TEXT,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
builtin BOOLEAN NOT NULL DEFAULT FALSE,
workspace_id BIGINT NOT NULL DEFAULT 1,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
@ -133,6 +139,7 @@ CREATE TABLE IF NOT EXISTS mate_channel (
config_json TEXT,
enabled BOOLEAN NOT NULL DEFAULT FALSE,
description VARCHAR(256),
workspace_id BIGINT NOT NULL DEFAULT 1,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
@ -149,6 +156,7 @@ CREATE TABLE IF NOT EXISTS mate_conversation (
last_message TEXT,
last_active_time DATETIME,
stream_status VARCHAR(16) NOT NULL DEFAULT 'idle',
workspace_id BIGINT NOT NULL DEFAULT 1,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
@ -404,9 +412,6 @@ CREATE TABLE IF NOT EXISTS mate_datasource (
deleted INT NOT NULL DEFAULT 0
);
-- 为现有表添加 metadata 列(向后兼容,防止迁移时数据丢失)
ALTER TABLE mate_message ADD COLUMN IF NOT EXISTS metadata JSON;
CREATE INDEX IF NOT EXISTS idx_guard_audit_conv ON mate_tool_guard_audit_log(conversation_id);
CREATE INDEX IF NOT EXISTS idx_guard_audit_time ON mate_tool_guard_audit_log(create_time);
@ -437,13 +442,6 @@ CREATE INDEX IF NOT EXISTS idx_memory_recall_candidates ON mate_memory_recall(ag
CREATE INDEX IF NOT EXISTS idx_message_conv_time ON mate_message(conversation_id, create_time);
CREATE INDEX IF NOT EXISTS idx_workspace_file_agent_enabled ON mate_workspace_file(agent_id, enabled);
-- ==================== OAuth 支持 ====================
ALTER TABLE mate_model_provider ADD COLUMN IF NOT EXISTS auth_type VARCHAR(16) NOT NULL DEFAULT 'api_key';
ALTER TABLE mate_model_provider ADD COLUMN IF NOT EXISTS oauth_access_token TEXT;
ALTER TABLE mate_model_provider ADD COLUMN IF NOT EXISTS oauth_refresh_token TEXT;
ALTER TABLE mate_model_provider ADD COLUMN IF NOT EXISTS oauth_expires_at BIGINT;
ALTER TABLE mate_model_provider ADD COLUMN IF NOT EXISTS oauth_account_id VARCHAR(128);
-- 清理 Codex 不支持的 ChatGPT OAuth 模型gpt-4o, o3, o4-mini 在 Codex 模式下不可用)
DELETE FROM mate_model_config WHERE provider = 'openai-chatgpt' AND model_name IN ('gpt-4o', 'o3', 'o4-mini');
@ -467,7 +465,6 @@ CREATE TABLE IF NOT EXISTS mate_async_task (
update_time DATETIME NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_async_task_taskid ON mate_async_task(task_id);
CREATE INDEX IF NOT EXISTS idx_async_task_conv ON mate_async_task(conversation_id);
CREATE INDEX IF NOT EXISTS idx_async_task_status ON mate_async_task(status);
@ -483,6 +480,7 @@ CREATE TABLE IF NOT EXISTS mate_wiki_knowledge_base (
status VARCHAR(32) NOT NULL DEFAULT 'active',
page_count INT NOT NULL DEFAULT 0,
raw_count INT NOT NULL DEFAULT 0,
workspace_id BIGINT NOT NULL DEFAULT 1,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0
@ -539,6 +537,7 @@ CREATE TABLE IF NOT EXISTS mate_workspace (
description VARCHAR(256),
owner_id BIGINT,
settings_json TEXT,
base_path VARCHAR(512),
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
deleted INT NOT NULL DEFAULT 0,
@ -558,14 +557,6 @@ CREATE TABLE IF NOT EXISTS mate_workspace_member (
CREATE INDEX IF NOT EXISTS idx_ws_member_workspace ON mate_workspace_member(workspace_id);
CREATE INDEX IF NOT EXISTS idx_ws_member_user ON mate_workspace_member(user_id);
-- 现有表增加 workspace_id 列
ALTER TABLE mate_agent ADD COLUMN IF NOT EXISTS workspace_id BIGINT NOT NULL DEFAULT 1;
ALTER TABLE mate_channel ADD COLUMN IF NOT EXISTS workspace_id BIGINT NOT NULL DEFAULT 1;
ALTER TABLE mate_conversation ADD COLUMN IF NOT EXISTS workspace_id BIGINT NOT NULL DEFAULT 1;
ALTER TABLE mate_wiki_knowledge_base ADD COLUMN IF NOT EXISTS workspace_id BIGINT NOT NULL DEFAULT 1;
ALTER TABLE mate_tool ADD COLUMN IF NOT EXISTS workspace_id BIGINT NOT NULL DEFAULT 1;
ALTER TABLE mate_skill ADD COLUMN IF NOT EXISTS workspace_id BIGINT NOT NULL DEFAULT 1;
-- =============================================
-- Agent-Skill / Agent-Tool 绑定表Phase 3 Sprint 2
-- =============================================

View File

@ -752,6 +752,7 @@ export default {
name: 'Name',
slug: 'Slug',
description: 'Description',
basePath: 'Working Directory',
created: 'Created',
actions: 'Actions',
},
@ -764,6 +765,9 @@ export default {
slugHint: 'Slug cannot be changed after creation.',
description: 'Description',
descriptionPlaceholder: 'Optional description',
basePath: 'Working Directory',
basePathPlaceholder: 'e.g. /home/user/project or D:\\codes\\MyProject',
basePathHint: 'Restrict agent file access to this directory. Leave empty for no restriction.',
},
editDialog: {
title: 'Edit Workspace',

View File

@ -752,6 +752,7 @@ export default {
name: '名称',
slug: '标识',
description: '描述',
basePath: '活动目录',
created: '创建时间',
actions: '操作',
},
@ -764,6 +765,9 @@ export default {
slugHint: '标识创建后不可修改。',
description: '描述',
descriptionPlaceholder: '可选描述',
basePath: '活动目录',
basePathPlaceholder: '例如:/home/user/project 或 D:\\codes\\MyProject',
basePathHint: '限制 Agent 文件操作的根目录,留空不限制。',
},
editDialog: {
title: '编辑工作区',

View File

@ -7,6 +7,7 @@ export interface Workspace {
name: string
slug: string
description?: string
basePath?: string
ownerId?: number
settingsJson?: string
createTime?: string

View File

@ -23,6 +23,7 @@
<th>{{ t('security.workspaces.columns.name') }}</th>
<th>{{ t('security.workspaces.columns.slug') }}</th>
<th>{{ t('security.workspaces.columns.description') }}</th>
<th>{{ t('security.workspaces.columns.basePath') }}</th>
<th>{{ t('security.workspaces.columns.created') }}</th>
<th style="width: 120px;">{{ t('security.workspaces.columns.actions') }}</th>
</tr>
@ -37,6 +38,7 @@
</td>
<td class="slug-cell">{{ ws.slug }}</td>
<td class="desc-cell">{{ ws.description || '-' }}</td>
<td class="slug-cell">{{ ws.basePath || '-' }}</td>
<td class="date-cell">{{ formatDate(ws.createTime) }}</td>
<td>
<div class="action-btns">
@ -93,6 +95,11 @@
<label>{{ t('security.workspaces.createDialog.description') }}</label>
<input v-model="form.description" class="form-input" :placeholder="t('security.workspaces.createDialog.descriptionPlaceholder')" />
</div>
<div class="form-group">
<label>{{ t('security.workspaces.createDialog.basePath') }}</label>
<input v-model="form.basePath" class="form-input mono" :placeholder="t('security.workspaces.createDialog.basePathPlaceholder')" />
<span class="form-hint">{{ t('security.workspaces.createDialog.basePathHint') }}</span>
</div>
</div>
</div>
<div class="modal-footer">
@ -150,6 +157,7 @@ const form = ref({
name: '',
slug: '',
description: '',
basePath: '',
})
onMounted(() => {
@ -170,7 +178,7 @@ async function fetchWorkspaces() {
function openCreateDialog() {
editingWs.value = null
form.value = { name: '', slug: '', description: '' }
form.value = { name: '', slug: '', description: '', basePath: '' }
showDialog.value = true
}
@ -180,6 +188,7 @@ function openEditDialog(ws: Workspace) {
name: ws.name,
slug: ws.slug,
description: ws.description || '',
basePath: ws.basePath || '',
}
showDialog.value = true
}
@ -199,12 +208,14 @@ async function saveWorkspace() {
await workspaceTeamApi.update(editingWs.value.id, {
name: form.value.name,
description: form.value.description,
basePath: form.value.basePath || null,
})
} else {
await workspaceTeamApi.create({
name: form.value.name,
slug: form.value.slug,
description: form.value.description,
basePath: form.value.basePath || null,
})
}
showDialog.value = false