diff --git a/mateclaw-server/src/main/java/vip/mate/agent/AgentGraphBuilder.java b/mateclaw-server/src/main/java/vip/mate/agent/AgentGraphBuilder.java index 29104db5..eb251f16 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/AgentGraphBuilder.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/AgentGraphBuilder.java @@ -140,14 +140,14 @@ public class AgentGraphBuilder { try { runtimeModel = modelConfigService.getDefaultModel(); } catch (Exception e) { - throw new MateClawException("无法构建 Agent:请先在「设置 → 模型」中配置并启用默认模型"); + throw new MateClawException("err.agent.no_default_model", "无法构建 Agent:请先在「设置 → 模型」中配置并启用默认模型"); } ModelProviderEntity provider; try { provider = modelProviderService.getProviderConfig(runtimeModel.getProvider()); } catch (Exception e) { - throw new MateClawException("模型 " + runtimeModel.getModelName() + throw new MateClawException("err.agent.model_not_configured", "模型 " + runtimeModel.getModelName() + " 的 Provider(" + runtimeModel.getProvider() + ")未配置,请检查模型设置"); } ModelProtocol protocol = ModelProtocol.fromChatModel(provider.getChatModel()); @@ -171,7 +171,7 @@ public class AgentGraphBuilder { // 当前仅支持 DashScope 和 OpenAI-compatible,其他协议直接拒绝 if (!supportsStateGraph(protocol)) { - throw new MateClawException("当前不支持协议 " + protocol.getId() + throw new MateClawException("err.agent.protocol_not_supported", "当前不支持协议 " + protocol.getId() + ",请切换到 DashScope 或 OpenAI-compatible 模型"); } @@ -332,7 +332,7 @@ public class AgentGraphBuilder { .recursionLimit(maxIterations > 0 ? maxIterations * 3 + 10 : 300) .build()); } catch (Exception e) { - throw new MateClawException("Plan-Execute StateGraph 编译失败: " + e.getMessage()); + throw new MateClawException("err.agent.plan_compile_failed", "Plan-Execute StateGraph 编译失败: " + e.getMessage()); } } @@ -442,7 +442,7 @@ public class AgentGraphBuilder { .withLifecycleListener(new ReActLifecycleListener()) .build()); } catch (Exception e) { - throw new MateClawException("StateGraph v2 编译失败: " + e.getMessage()); + throw new MateClawException("err.agent.graph_compile_failed", "StateGraph v2 编译失败: " + e.getMessage()); } } @@ -502,7 +502,7 @@ public class AgentGraphBuilder { .build(); } - throw new MateClawException("StateGraph 当前仅支持 DashScope 原生协议、OpenAI-compatible 协议和 Anthropic Messages 协议: " + protocol.getId()); + throw new MateClawException("err.agent.protocol_limited", "StateGraph 当前仅支持 DashScope 原生协议、OpenAI-compatible 协议和 Anthropic Messages 协议: " + protocol.getId()); } /** @@ -791,15 +791,15 @@ public class AgentGraphBuilder { OpenAiApi buildOpenAiApi(ModelProviderEntity provider) { if (provider == null || !modelProviderService.isProviderConfigured(provider.getProviderId())) { - throw new MateClawException("Provider 未完成配置,请在模型设置中填写有效的 API Key 和 Base URL"); + throw new MateClawException("err.agent.provider_not_configured", "Provider 未完成配置,请在模型设置中填写有效的 API Key 和 Base URL"); } String apiKey = provider.getApiKey(); if (!modelProviderService.hasUsableApiKey(apiKey)) { - throw new MateClawException("Provider API Key 未配置或无效: " + provider.getProviderId()); + throw new MateClawException("err.agent.provider_apikey_invalid", "Provider API Key 未配置或无效: " + provider.getProviderId()); } String baseUrl = normalizeOpenAiBaseUrl(provider.getBaseUrl()); if (!StringUtils.hasText(baseUrl)) { - throw new MateClawException("Provider Base URL 未配置: " + provider.getProviderId()); + throw new MateClawException("err.agent.provider_baseurl_missing", "Provider Base URL 未配置: " + provider.getProviderId()); } Map kwargs = modelProviderService.readProviderGenerateKwargs(provider); MultiValueMap headers = buildOpenAiHeaders(kwargs); @@ -892,7 +892,7 @@ public class AgentGraphBuilder { apiKey = readApiKeyFromDefaultChatModel(); } if (!modelProviderService.hasUsableApiKey(apiKey)) { - throw new MateClawException("DashScope API Key 未配置,请在模型设置中填写 dashscope 的 API Key,或设置 DASHSCOPE_API_KEY 环境变量"); + throw new MateClawException("err.agent.dashscope_key_missing", "DashScope API Key 未配置,请在模型设置中填写 dashscope 的 API Key,或设置 DASHSCOPE_API_KEY 环境变量"); } builder.apiKey(apiKey.trim()); @@ -915,11 +915,11 @@ public class AgentGraphBuilder { private AnthropicApi buildAnthropicApi(ModelProviderEntity provider) { if (provider == null || !modelProviderService.isProviderConfigured(provider.getProviderId())) { - throw new MateClawException("Anthropic Provider 未完成配置,请在模型设置中填写有效的 API Key 和 Base URL"); + throw new MateClawException("err.agent.anthropic_not_configured", "Anthropic Provider 未完成配置,请在模型设置中填写有效的 API Key 和 Base URL"); } String apiKey = provider.getApiKey(); if (!modelProviderService.hasUsableApiKey(apiKey)) { - throw new MateClawException("Anthropic API Key 未配置或无效: " + provider.getProviderId()); + throw new MateClawException("err.agent.anthropic_key_invalid", "Anthropic API Key 未配置或无效: " + provider.getProviderId()); } String baseUrl = provider.getBaseUrl(); RestClient.Builder restClientBuilder = restClientBuilderProvider.getIfAvailable(RestClient::builder); diff --git a/mateclaw-server/src/main/java/vip/mate/agent/binding/controller/AgentBindingController.java b/mateclaw-server/src/main/java/vip/mate/agent/binding/controller/AgentBindingController.java index 98ed79ee..39b28594 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/binding/controller/AgentBindingController.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/binding/controller/AgentBindingController.java @@ -111,7 +111,7 @@ public class AgentBindingController { } long requestedWs = headerWorkspaceId != null ? headerWorkspaceId : 1L; if (agent.getWorkspaceId() != null && !agent.getWorkspaceId().equals(requestedWs)) { - throw new MateClawException("资源不属于当前工作区"); + throw new MateClawException("err.common.wrong_workspace", "资源不属于当前工作区"); } } } diff --git a/mateclaw-server/src/main/java/vip/mate/agent/controller/AgentController.java b/mateclaw-server/src/main/java/vip/mate/agent/controller/AgentController.java index a895bbff..ee2ffa19 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/controller/AgentController.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/controller/AgentController.java @@ -180,7 +180,7 @@ public class AgentController { private void verifyResourceWorkspace(Long resourceWorkspaceId, Long headerWorkspaceId) { long requestedWs = headerWorkspaceId != null ? headerWorkspaceId : 1L; if (resourceWorkspaceId != null && !resourceWorkspaceId.equals(requestedWs)) { - throw new MateClawException("资源不属于当前工作区"); + throw new MateClawException("err.common.wrong_workspace", "资源不属于当前工作区"); } } } diff --git a/mateclaw-server/src/main/java/vip/mate/agent/service/TemplateService.java b/mateclaw-server/src/main/java/vip/mate/agent/service/TemplateService.java index cc5621cc..14359e03 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/service/TemplateService.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/service/TemplateService.java @@ -73,7 +73,7 @@ public class TemplateService { TemplateDTO template = listTemplates().stream() .filter(t -> t.getId().equals(templateId)) .findFirst() - .orElseThrow(() -> new MateClawException("模板不存在: " + templateId)); + .orElseThrow(() -> new MateClawException("err.agent.template_not_found", "模板不存在: " + templateId)); // 1. 创建 Agent AgentEntity agent = new AgentEntity(); diff --git a/mateclaw-server/src/main/java/vip/mate/channel/controller/ChannelController.java b/mateclaw-server/src/main/java/vip/mate/channel/controller/ChannelController.java index 15b51866..6792b8c0 100644 --- a/mateclaw-server/src/main/java/vip/mate/channel/controller/ChannelController.java +++ b/mateclaw-server/src/main/java/vip/mate/channel/controller/ChannelController.java @@ -129,7 +129,7 @@ public class ChannelController { private void verifyResourceWorkspace(Long resourceWorkspaceId, Long headerWorkspaceId) { long requestedWs = headerWorkspaceId != null ? headerWorkspaceId : 1L; if (resourceWorkspaceId != null && !resourceWorkspaceId.equals(requestedWs)) { - throw new MateClawException("资源不属于当前工作区"); + throw new MateClawException("err.common.wrong_workspace", "资源不属于当前工作区"); } } } diff --git a/mateclaw-server/src/main/java/vip/mate/dashboard/controller/DashboardController.java b/mateclaw-server/src/main/java/vip/mate/dashboard/controller/DashboardController.java index cd00fa80..23e49e5c 100644 --- a/mateclaw-server/src/main/java/vip/mate/dashboard/controller/DashboardController.java +++ b/mateclaw-server/src/main/java/vip/mate/dashboard/controller/DashboardController.java @@ -65,7 +65,7 @@ public class DashboardController { if (agent != null && agent.getWorkspaceId() != null) { long wsId = workspaceId != null ? workspaceId : 1L; if (!agent.getWorkspaceId().equals(wsId)) { - throw new MateClawException("资源不属于当前工作区"); + throw new MateClawException("err.common.wrong_workspace", "资源不属于当前工作区"); } } } diff --git a/mateclaw-server/src/main/java/vip/mate/datasource/service/DatasourceConnectionManager.java b/mateclaw-server/src/main/java/vip/mate/datasource/service/DatasourceConnectionManager.java index e26d6640..19b5fc70 100644 --- a/mateclaw-server/src/main/java/vip/mate/datasource/service/DatasourceConnectionManager.java +++ b/mateclaw-server/src/main/java/vip/mate/datasource/service/DatasourceConnectionManager.java @@ -34,7 +34,7 @@ public class DatasourceConnectionManager implements DisposableBean { try { return ds.getConnection(); } catch (SQLException e) { - throw new MateClawException("获取数据库连接失败: " + e.getMessage()); + throw new MateClawException("err.datasource.connection_failed", "获取数据库连接失败: " + e.getMessage()); } } @@ -122,14 +122,14 @@ public class DatasourceConnectionManager implements DisposableBean { baseUrl = String.format("jdbc:clickhouse://%s:%d/%s", host, port, dbName); break; default: - throw new MateClawException("不支持的数据库类型: " + dbType); + throw new MateClawException("err.datasource.unsupported_db", "不支持的数据库类型: " + dbType); } if (extra != null && !extra.isBlank()) { // 安全检查:拒绝危险参数 String lowerExtra = extra.toLowerCase(); if (lowerExtra.contains("allowloadlocalinfile") || lowerExtra.contains("autodeserialize")) { - throw new MateClawException("JDBC 参数包含不安全选项"); + throw new MateClawException("err.datasource.unsafe_jdbc", "JDBC 参数包含不安全选项"); } baseUrl += (baseUrl.contains("?") ? "&" : "?") + extra; } diff --git a/mateclaw-server/src/main/java/vip/mate/datasource/service/DatasourceService.java b/mateclaw-server/src/main/java/vip/mate/datasource/service/DatasourceService.java index 3618115c..1c72747d 100644 --- a/mateclaw-server/src/main/java/vip/mate/datasource/service/DatasourceService.java +++ b/mateclaw-server/src/main/java/vip/mate/datasource/service/DatasourceService.java @@ -51,7 +51,7 @@ public class DatasourceService { public DatasourceEntity getById(Long id) { DatasourceEntity entity = datasourceMapper.selectById(id); if (entity == null) { - throw new MateClawException("数据源不存在: " + id); + throw new MateClawException("err.datasource.not_found", "数据源不存在: " + id); } return entity; } diff --git a/mateclaw-server/src/main/java/vip/mate/datasource/service/SqlValidationService.java b/mateclaw-server/src/main/java/vip/mate/datasource/service/SqlValidationService.java index 0e9b3e0e..12456da7 100644 --- a/mateclaw-server/src/main/java/vip/mate/datasource/service/SqlValidationService.java +++ b/mateclaw-server/src/main/java/vip/mate/datasource/service/SqlValidationService.java @@ -37,7 +37,7 @@ public class SqlValidationService { */ public String validateAndNormalize(String sql) { if (sql == null || sql.isBlank()) { - throw new MateClawException("SQL 不能为空"); + throw new MateClawException("err.datasource.sql_empty", "SQL 不能为空"); } // 去除末尾分号 @@ -51,16 +51,16 @@ public class SqlValidationService { // 尝试解析为多条语句,检查是否有多语句注入 Statements stmts = CCJSqlParserUtil.parseStatements(sql); if (stmts.getStatements().size() != 1) { - throw new MateClawException("仅允许执行单条 SQL 语句,检测到 " + stmts.getStatements().size() + " 条"); + throw new MateClawException("err.datasource.only_single_sql", "仅允许执行单条 SQL 语句,检测到 " + stmts.getStatements().size() + " 条"); } statement = stmts.getStatements().get(0); } catch (JSQLParserException e) { - throw new MateClawException("SQL 解析失败: " + e.getMessage()); + throw new MateClawException("err.datasource.sql_parse_failed", "SQL 解析失败: " + e.getMessage()); } // 仅允许 SELECT if (!(statement instanceof Select)) { - throw new MateClawException("仅允许 SELECT 查询,检测到: " + statement.getClass().getSimpleName()); + throw new MateClawException("err.datasource.only_select", "仅允许 SELECT 查询,检测到: " + statement.getClass().getSimpleName()); } Select select = (Select) statement; diff --git a/mateclaw-server/src/main/java/vip/mate/llm/chatgpt/ChatGPTResponsesClient.java b/mateclaw-server/src/main/java/vip/mate/llm/chatgpt/ChatGPTResponsesClient.java index 007e1a89..7749cc7d 100644 --- a/mateclaw-server/src/main/java/vip/mate/llm/chatgpt/ChatGPTResponsesClient.java +++ b/mateclaw-server/src/main/java/vip/mate/llm/chatgpt/ChatGPTResponsesClient.java @@ -95,7 +95,7 @@ public class ChatGPTResponsesClient { .filter(line -> !line.isBlank() && !line.equals("[DONE]")) .mapNotNull(this::parseSSEEvent) .onErrorMap(e -> e instanceof MateClawException ? e - : new MateClawException("ChatGPT 流式调用失败: " + e.getMessage())); + : new MateClawException("err.llm.chatgpt_stream_failed", "ChatGPT 流式调用失败: " + e.getMessage())); } /** @@ -290,7 +290,7 @@ public class ChatGPTResponsesClient { if ("response.failed".equals(type)) { String error = node.path("response").path("error").path("message").asText("Unknown error"); log.error("[ChatGPT] Responses API error: {}", error); - throw new MateClawException("ChatGPT 返回错误: " + error); + throw new MateClawException("err.llm.chatgpt_error", "ChatGPT 返回错误: " + error); } return null; @@ -305,7 +305,7 @@ public class ChatGPTResponsesClient { private void setHeaders(HttpHeaders headers, String accessToken, String accountId) { if (accountId == null || accountId.isBlank()) { - throw new MateClawException("chatgpt-account-id 缺失,请断开后重新 OAuth 登录"); + throw new MateClawException("err.llm.chatgpt_account_missing", "chatgpt-account-id 缺失,请断开后重新 OAuth 登录"); } headers.setBearerAuth(accessToken); headers.set("chatgpt-account-id", accountId); diff --git a/mateclaw-server/src/main/java/vip/mate/llm/oauth/OpenAIOAuthService.java b/mateclaw-server/src/main/java/vip/mate/llm/oauth/OpenAIOAuthService.java index e0900d31..cac2b096 100644 --- a/mateclaw-server/src/main/java/vip/mate/llm/oauth/OpenAIOAuthService.java +++ b/mateclaw-server/src/main/java/vip/mate/llm/oauth/OpenAIOAuthService.java @@ -191,7 +191,7 @@ public class OpenAIOAuthService { private void exchangeToken(String code, String state) { String codeVerifier = pendingStates.remove(state); if (codeVerifier == null) { - throw new MateClawException("无效的 OAuth state,可能已过期或重复使用"); + throw new MateClawException("err.llm.oauth_state_invalid", "无效的 OAuth state,可能已过期或重复使用"); } String body = "grant_type=authorization_code" @@ -210,7 +210,7 @@ public class OpenAIOAuthService { public void refreshToken() { ModelProviderEntity provider = getProvider(); if (!StringUtils.hasText(provider.getOauthRefreshToken())) { - throw new MateClawException("无 refresh_token,请重新登录"); + throw new MateClawException("err.llm.oauth_no_refresh", "无 refresh_token,请重新登录"); } String body = "grant_type=refresh_token" @@ -227,7 +227,7 @@ public class OpenAIOAuthService { public String ensureValidAccessToken() { ModelProviderEntity provider = getProvider(); if (!StringUtils.hasText(provider.getOauthAccessToken())) { - throw new MateClawException("未连接 OpenAI OAuth,请先登录"); + throw new MateClawException("err.llm.oauth_not_connected", "未连接 OpenAI OAuth,请先登录"); } // 提前 5 分钟刷新 @@ -299,7 +299,7 @@ public class OpenAIOAuthService { return objectMapper.readTree(response); } catch (Exception e) { log.error("OpenAI OAuth token 请求失败", e); - throw new MateClawException("OAuth token 交换失败: " + e.getMessage()); + throw new MateClawException("err.llm.oauth_exchange_failed", "OAuth token 交换失败: " + e.getMessage()); } } @@ -309,7 +309,7 @@ public class OpenAIOAuthService { int expiresIn = tokenResponse.path("expires_in").asInt(3600); if (!StringUtils.hasText(accessToken)) { - throw new MateClawException("OAuth 响应中缺少 access_token"); + throw new MateClawException("err.llm.oauth_no_token", "OAuth 响应中缺少 access_token"); } String accountId = extractAccountIdFromJwt(accessToken); @@ -354,7 +354,7 @@ public class OpenAIOAuthService { private ModelProviderEntity getProvider() { ModelProviderEntity provider = modelProviderMapper.selectById(PROVIDER_ID); if (provider == null) { - throw new MateClawException("OpenAI ChatGPT provider 未配置,请检查数据库初始化"); + throw new MateClawException("err.llm.chatgpt_not_configured", "OpenAI ChatGPT provider 未配置,请检查数据库初始化"); } return provider; } @@ -384,7 +384,7 @@ public class OpenAIOAuthService { byte[] digest = md.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII)); return base64UrlEncode(digest); } catch (Exception e) { - throw new MateClawException("PKCE code_challenge 生成失败: " + e.getMessage()); + throw new MateClawException("err.llm.pkce_failed", "PKCE code_challenge 生成失败: " + e.getMessage()); } } diff --git a/mateclaw-server/src/main/java/vip/mate/llm/service/ModelConfigService.java b/mateclaw-server/src/main/java/vip/mate/llm/service/ModelConfigService.java index 2906520f..c00af994 100644 --- a/mateclaw-server/src/main/java/vip/mate/llm/service/ModelConfigService.java +++ b/mateclaw-server/src/main/java/vip/mate/llm/service/ModelConfigService.java @@ -47,7 +47,7 @@ public class ModelConfigService { public ModelConfigEntity getModel(Long id) { ModelConfigEntity entity = modelConfigMapper.selectById(id); if (entity == null) { - throw new MateClawException("模型配置不存在: " + id); + throw new MateClawException("err.llm.model_config_not_found", "模型配置不存在: " + id); } return entity; } @@ -64,7 +64,7 @@ public class ModelConfigService { .orderByAsc(ModelConfigEntity::getName) .last("LIMIT 1")); if (entity == null) { - throw new MateClawException("没有可用的模型配置"); + throw new MateClawException("err.llm.no_available_model", "没有可用的模型配置"); } return entity; } @@ -103,7 +103,7 @@ public class ModelConfigService { clearDefaultFlag(); } if (existing.getIsDefault() && Boolean.FALSE.equals(entity.getEnabled())) { - throw new MateClawException("默认模型不能被禁用,请先切换默认模型"); + throw new MateClawException("err.llm.cannot_disable_default", "默认模型不能被禁用,请先切换默认模型"); } modelConfigMapper.updateById(entity); ensureDefaultExists(); @@ -114,7 +114,7 @@ public class ModelConfigService { public void deleteModel(Long id) { ModelConfigEntity entity = getModel(id); if (Boolean.TRUE.equals(entity.getIsDefault())) { - throw new MateClawException("默认模型不能删除,请先切换默认模型"); + throw new MateClawException("err.llm.cannot_delete_default", "默认模型不能删除,请先切换默认模型"); } modelConfigMapper.deleteById(id); ensureDefaultExists(); @@ -123,14 +123,14 @@ public class ModelConfigService { public ModelConfigEntity addModelToProvider(String providerId, String modelId, String displayName, boolean builtin) { if (!StringUtils.hasText(providerId) || !StringUtils.hasText(modelId)) { - throw new MateClawException("Provider 和模型标识不能为空"); + throw new MateClawException("err.llm.provider_model_required", "Provider 和模型标识不能为空"); } ModelConfigEntity existing = modelConfigMapper.selectOne(new LambdaQueryWrapper() .eq(ModelConfigEntity::getProvider, providerId) .eq(ModelConfigEntity::getModelName, modelId) .last("LIMIT 1")); if (existing != null) { - throw new MateClawException("模型已存在: " + modelId); + throw new MateClawException("err.llm.model_exists", "模型已存在: " + modelId); } ModelConfigEntity entity = new ModelConfigEntity(); entity.setName(StringUtils.hasText(displayName) ? displayName : modelId); @@ -155,10 +155,10 @@ public class ModelConfigService { .eq(ModelConfigEntity::getModelName, modelId) .last("LIMIT 1")); if (entity == null) { - throw new MateClawException("模型不存在: " + modelId); + throw new MateClawException("err.llm.model_not_found", "模型不存在: " + modelId); } if (Boolean.TRUE.equals(entity.getBuiltin())) { - throw new MateClawException("内置模型不支持删除"); + throw new MateClawException("err.llm.builtin_readonly", "内置模型不支持删除"); } deleteModel(entity.getId()); } @@ -175,7 +175,7 @@ public class ModelConfigService { public ModelConfigEntity setDefaultModel(Long id) { ModelConfigEntity entity = getModel(id); if (!Boolean.TRUE.equals(entity.getEnabled())) { - throw new MateClawException("只有启用状态的模型才能设为默认"); + throw new MateClawException("err.llm.only_enabled_default", "只有启用状态的模型才能设为默认"); } clearDefaultFlag(); entity.setIsDefault(true); @@ -190,7 +190,7 @@ public class ModelConfigService { .eq(ModelConfigEntity::getModelName, modelName) .last("LIMIT 1")); if (entity == null) { - throw new MateClawException("模型不存在: " + providerId + "/" + modelName); + throw new MateClawException("err.llm.model_not_found", "模型不存在: " + providerId + "/" + modelName); } if (!Boolean.TRUE.equals(entity.getEnabled())) { // Auto-enable when setting as default (e.g. local Ollama models) @@ -218,13 +218,13 @@ public class ModelConfigService { private void validateModel(ModelConfigEntity entity, Long currentId) { if (!StringUtils.hasText(entity.getName())) { - throw new MateClawException("模型名称不能为空"); + throw new MateClawException("err.llm.name_required", "模型名称不能为空"); } if (!StringUtils.hasText(entity.getProvider())) { entity.setProvider("dashscope"); } if (!StringUtils.hasText(entity.getModelName())) { - throw new MateClawException("模型标识不能为空"); + throw new MateClawException("err.llm.id_required", "模型标识不能为空"); } ModelConfigEntity duplicate = modelConfigMapper.selectOne(new LambdaQueryWrapper() .eq(ModelConfigEntity::getProvider, entity.getProvider()) @@ -232,7 +232,7 @@ public class ModelConfigService { .ne(currentId != null, ModelConfigEntity::getId, currentId) .last("LIMIT 1")); if (duplicate != null) { - throw new MateClawException("模型标识已存在: " + entity.getProvider() + "/" + entity.getModelName()); + throw new MateClawException("err.llm.id_exists", "模型标识已存在: " + entity.getProvider() + "/" + entity.getModelName()); } } diff --git a/mateclaw-server/src/main/java/vip/mate/llm/service/ModelDiscoveryService.java b/mateclaw-server/src/main/java/vip/mate/llm/service/ModelDiscoveryService.java index 54475b31..1f728e03 100644 --- a/mateclaw-server/src/main/java/vip/mate/llm/service/ModelDiscoveryService.java +++ b/mateclaw-server/src/main/java/vip/mate/llm/service/ModelDiscoveryService.java @@ -32,7 +32,7 @@ public class ModelDiscoveryService { public DiscoverResult discoverModels(String providerId) { ModelProviderEntity provider = modelProviderService.getProviderConfig(providerId); if (!Boolean.TRUE.equals(provider.getSupportModelDiscovery())) { - throw new MateClawException("该供应商不支持模型发现: " + providerId); + throw new MateClawException("err.llm.discovery_not_supported", "该供应商不支持模型发现: " + providerId); } ModelProtocol protocol = ModelProtocol.fromChatModel(provider.getChatModel()); @@ -66,7 +66,7 @@ public class ModelDiscoveryService { // 不支持模型发现(如智谱):用第一个已配置模型发送测试请求 List models = modelConfigService.listModelsByProvider(providerId); if (models.isEmpty()) { - throw new MateClawException("该供应商没有已配置的模型,无法测试连接"); + throw new MateClawException("err.llm.no_model_for_test", "该供应商没有已配置的模型,无法测试连接"); } String testModelId = models.get(0).getModelName(); String response = sendTestPrompt(provider, protocol, testModelId); @@ -122,14 +122,14 @@ public class ModelDiscoveryService { case DASHSCOPE_NATIVE -> fetchDashScopeModels(provider); case GEMINI_NATIVE -> fetchGeminiModels(provider); case ANTHROPIC_MESSAGES -> fetchAnthropicModels(provider); - case OPENAI_CHATGPT -> throw new MateClawException("ChatGPT OAuth provider 不支持模型发现"); + case OPENAI_CHATGPT -> throw new MateClawException("err.llm.chatgpt_no_discovery", "ChatGPT OAuth provider 不支持模型发现"); }; } private List fetchOpenAiCompatibleModels(ModelProviderEntity provider) { String baseUrl = normalizeBaseUrl(provider.getBaseUrl()); if (!StringUtils.hasText(baseUrl)) { - throw new MateClawException("Base URL 未配置"); + throw new MateClawException("err.llm.base_url_missing", "Base URL 未配置"); } String apiKey = provider.getApiKey(); @@ -153,7 +153,7 @@ public class ModelDiscoveryService { private List fetchDashScopeModels(ModelProviderEntity provider) { String apiKey = provider.getApiKey(); if (!modelProviderService.hasUsableApiKey(apiKey)) { - throw new MateClawException("DashScope API Key 未配置"); + throw new MateClawException("err.llm.dashscope_key_missing", "DashScope API Key 未配置"); } // DashScope 兼容模式暴露了 OpenAI 兼容的 /v1/models 端点 @@ -170,7 +170,7 @@ public class ModelDiscoveryService { private List fetchGeminiModels(ModelProviderEntity provider) { String apiKey = provider.getApiKey(); if (!modelProviderService.hasUsableApiKey(apiKey)) { - throw new MateClawException("Gemini API Key 未配置"); + throw new MateClawException("err.llm.gemini_key_missing", "Gemini API Key 未配置"); } RestClient client = RestClient.builder() @@ -188,7 +188,7 @@ public class ModelDiscoveryService { private List fetchAnthropicModels(ModelProviderEntity provider) { String apiKey = provider.getApiKey(); if (!modelProviderService.hasUsableApiKey(apiKey)) { - throw new MateClawException("Anthropic API Key 未配置"); + throw new MateClawException("err.llm.anthropic_key_missing", "Anthropic API Key 未配置"); } String baseUrl = StringUtils.hasText(provider.getBaseUrl()) @@ -214,14 +214,14 @@ public class ModelDiscoveryService { case DASHSCOPE_NATIVE -> sendDashScopeTestPrompt(provider, modelId); case GEMINI_NATIVE -> sendGeminiTestPrompt(provider, modelId); case ANTHROPIC_MESSAGES -> sendAnthropicTestPrompt(provider, modelId); - case OPENAI_CHATGPT -> throw new MateClawException("ChatGPT OAuth provider 不支持模型测试"); + case OPENAI_CHATGPT -> throw new MateClawException("err.llm.chatgpt_no_test", "ChatGPT OAuth provider 不支持模型测试"); }; } private String sendOpenAiTestPrompt(ModelProviderEntity provider, String modelId) { String baseUrl = normalizeBaseUrl(provider.getBaseUrl()); if (!StringUtils.hasText(baseUrl)) { - throw new MateClawException("Base URL 未配置"); + throw new MateClawException("err.llm.base_url_missing", "Base URL 未配置"); } Map requestBody = Map.of( @@ -255,7 +255,7 @@ public class ModelDiscoveryService { private String sendDashScopeTestPrompt(ModelProviderEntity provider, String modelId) { String apiKey = provider.getApiKey(); if (!modelProviderService.hasUsableApiKey(apiKey)) { - throw new MateClawException("DashScope API Key 未配置"); + throw new MateClawException("err.llm.dashscope_key_missing", "DashScope API Key 未配置"); } Map requestBody = Map.of( @@ -281,7 +281,7 @@ public class ModelDiscoveryService { private String sendGeminiTestPrompt(ModelProviderEntity provider, String modelId) { String apiKey = provider.getApiKey(); if (!modelProviderService.hasUsableApiKey(apiKey)) { - throw new MateClawException("Gemini API Key 未配置"); + throw new MateClawException("err.llm.gemini_key_missing", "Gemini API Key 未配置"); } Map requestBody = Map.of( @@ -306,7 +306,7 @@ public class ModelDiscoveryService { private String sendAnthropicTestPrompt(ModelProviderEntity provider, String modelId) { String apiKey = provider.getApiKey(); if (!modelProviderService.hasUsableApiKey(apiKey)) { - throw new MateClawException("Anthropic API Key 未配置"); + throw new MateClawException("err.llm.anthropic_key_missing", "Anthropic API Key 未配置"); } String baseUrl = StringUtils.hasText(provider.getBaseUrl()) diff --git a/mateclaw-server/src/main/java/vip/mate/llm/service/ModelProviderService.java b/mateclaw-server/src/main/java/vip/mate/llm/service/ModelProviderService.java index 0078c590..7519ae47 100644 --- a/mateclaw-server/src/main/java/vip/mate/llm/service/ModelProviderService.java +++ b/mateclaw-server/src/main/java/vip/mate/llm/service/ModelProviderService.java @@ -51,10 +51,10 @@ public class ModelProviderService { public ProviderInfoDTO createCustomProvider(CreateCustomProviderRequest request) { if (!StringUtils.hasText(request.getId()) || !StringUtils.hasText(request.getName())) { - throw new MateClawException("Provider id 和名称不能为空"); + throw new MateClawException("err.llm.provider_fields_required", "Provider id 和名称不能为空"); } if (modelProviderMapper.selectById(request.getId()) != null) { - throw new MateClawException("Provider 已存在: " + request.getId()); + throw new MateClawException("err.llm.provider_exists", "Provider 已存在: " + request.getId()); } ModelProviderEntity provider = new ModelProviderEntity(); provider.setProviderId(request.getId()); @@ -83,7 +83,7 @@ public class ModelProviderService { public void deleteCustomProvider(String providerId) { ModelProviderEntity provider = getProvider(providerId); if (!Boolean.TRUE.equals(provider.getIsCustom())) { - throw new MateClawException("内置 Provider 不支持删除"); + throw new MateClawException("err.llm.provider_builtin_readonly", "内置 Provider 不支持删除"); } modelConfigService.deleteModelsByProvider(providerId); modelProviderMapper.deleteById(providerId); @@ -159,7 +159,7 @@ public class ModelProviderService { private ModelProviderEntity getProvider(String providerId) { ModelProviderEntity provider = modelProviderMapper.selectById(providerId); if (provider == null) { - throw new MateClawException("Provider 不存在: " + providerId); + throw new MateClawException("err.llm.provider_not_found", "Provider 不存在: " + providerId); } return provider; } diff --git a/mateclaw-server/src/main/java/vip/mate/tool/guard/DefaultToolGuard.java b/mateclaw-server/src/main/java/vip/mate/tool/guard/DefaultToolGuard.java index b34e8d9c..cfc462d1 100644 --- a/mateclaw-server/src/main/java/vip/mate/tool/guard/DefaultToolGuard.java +++ b/mateclaw-server/src/main/java/vip/mate/tool/guard/DefaultToolGuard.java @@ -106,27 +106,27 @@ public class DefaultToolGuard implements ToolGuard { new DangerousPattern( "rm\\s+-(rf|fr)\\s+/\\s*$", "filesystem_destroy", - "递归强制删除根目录"), + "Recursive force delete root directory"), new DangerousPattern( "mkfs\\b", "filesystem_destroy", - "文件系统格式化命令"), + "Filesystem formatting command"), new DangerousPattern( "dd\\s+if=.+of=/dev/", "filesystem_destroy", - "直接磁盘写入操作"), + "Direct disk write operation"), new DangerousPattern( "\\bkill\\s+-9\\s+1\\b", "system_danger", - "杀死 init/systemd 进程"), + "Kill init/systemd process"), new DangerousPattern( "curl.*\\|\\s*(sh|bash|zsh)", "code_injection", - "管道下载内容到 Shell 执行"), + "Pipe download to shell execution"), new DangerousPattern( "wget.*\\|\\s*(sh|bash|zsh)", "code_injection", - "管道下载内容到 Shell 执行") + "Pipe download to shell execution") ); } @@ -139,69 +139,69 @@ public class DefaultToolGuard implements ToolGuard { new DangerousPattern( "rm\\s+-(rf|fr)", "filesystem_destroy", - "递归强制删除操作"), + "Recursive force delete"), new DangerousPattern( "rm\\s+/", "filesystem_destroy", - "从根路径删除文件"), + "Delete from root path"), new DangerousPattern( "rmdir\\s+/", "filesystem_destroy", - "从根路径删除目录"), + "Delete directory from root path"), // SQL new DangerousPattern( "DROP\\s+(TABLE|DATABASE|INDEX|VIEW|SCHEMA)", "sql_destroy", - "SQL DROP 语句"), + "SQL DROP statement"), new DangerousPattern( "TRUNCATE\\s+TABLE", "sql_destroy", - "SQL TRUNCATE TABLE 语句"), + "SQL TRUNCATE TABLE statement"), new DangerousPattern( "DELETE\\s+FROM\\s+\\w+\\s*;", "sql_destroy", - "无条件 DELETE(缺少 WHERE 子句)"), + "Unconditional DELETE (missing WHERE clause)"), new DangerousPattern( "ALTER\\s+TABLE\\s+\\w+\\s+DROP", "sql_destroy", - "ALTER TABLE DROP 操作"), + "ALTER TABLE DROP operation"), - // 系统 + // System new DangerousPattern( "\\bshutdown\\b", "system_danger", - "系统关机命令"), + "System shutdown command"), new DangerousPattern( "\\breboot\\b", "system_danger", - "系统重启命令"), + "System reboot command"), new DangerousPattern( "chmod\\s+777", "system_danger", - "过度宽松的权限设置"), + "Overly permissive file permissions"), - // 代码注入 + // Code injection new DangerousPattern( "eval\\s*\\(", "code_injection", - "动态代码执行(eval)"), + "Dynamic code execution (eval)"), - // 凭据 + // Credentials new DangerousPattern( "(password|secret|api[_-]?key|token)\\s*=\\s*['\"]?\\S{8,}", "credential_exposure", - "可能的凭据信息暴露"), + "Potential credential exposure"), // Git new DangerousPattern( "git\\s+push\\s+.*--force", "git_danger", - "Git 强制推送"), + "Git force push"), new DangerousPattern( "git\\s+reset\\s+--hard", "git_danger", - "Git 硬重置") + "Git hard reset") ); } } diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/controller/WikiController.java b/mateclaw-server/src/main/java/vip/mate/wiki/controller/WikiController.java index a65bb4a8..a8f1d1a0 100644 --- a/mateclaw-server/src/main/java/vip/mate/wiki/controller/WikiController.java +++ b/mateclaw-server/src/main/java/vip/mate/wiki/controller/WikiController.java @@ -355,7 +355,7 @@ public class WikiController { } long wsId = headerWorkspaceId != null ? headerWorkspaceId : 1L; if (kb.getWorkspaceId() != null && !kb.getWorkspaceId().equals(wsId)) { - throw new MateClawException("资源不属于当前工作区"); + throw new MateClawException("err.common.wrong_workspace", "资源不属于当前工作区"); } } } diff --git a/mateclaw-server/src/main/resources/messages.properties b/mateclaw-server/src/main/resources/messages.properties index 08001d19..2ce9a50b 100644 --- a/mateclaw-server/src/main/resources/messages.properties +++ b/mateclaw-server/src/main/resources/messages.properties @@ -167,6 +167,67 @@ err.cron.agent_required=\u8bf7\u9009\u62e9\u5173\u8054 Agent err.cron.expression_required=Cron \u8868\u8fbe\u5f0f\u4e0d\u80fd\u4e3a\u7a7a err.cron.trigger_required=\u89e6\u53d1\u6d88\u606f\u4e0d\u80fd\u4e3a\u7a7a err.cron.target_required=\u6267\u884c\u76ee\u6807\u4e0d\u80fd\u4e3a\u7a7a +# agent (extended) +err.agent.no_default_model=\u65e0\u6cd5\u6784\u5efa Agent\uff1a\u8bf7\u5148\u914d\u7f6e\u5e76\u542f\u7528\u9ed8\u8ba4\u6a21\u578b +err.agent.model_not_configured=\u6a21\u578b Provider \u672a\u5b8c\u6210\u914d\u7f6e +err.agent.protocol_not_supported=\u5f53\u524d\u4e0d\u652f\u6301\u8be5\u534f\u8bae +err.agent.plan_compile_failed=Plan-Execute StateGraph \u7f16\u8bd1\u5931\u8d25 +err.agent.graph_compile_failed=StateGraph v2 \u7f16\u8bd1\u5931\u8d25 +err.agent.protocol_limited=StateGraph \u5f53\u524d\u4ec5\u652f\u6301 DashScope/OpenAI/Anthropic \u534f\u8bae +err.agent.provider_not_configured=Provider \u672a\u5b8c\u6210\u914d\u7f6e +err.agent.provider_apikey_invalid=Provider API Key \u672a\u914d\u7f6e\u6216\u65e0\u6548 +err.agent.provider_baseurl_missing=Provider Base URL \u672a\u914d\u7f6e +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.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 +err.llm.no_available_model=\u6ca1\u6709\u53ef\u7528\u7684\u6a21\u578b\u914d\u7f6e +err.llm.cannot_disable_default=\u9ed8\u8ba4\u6a21\u578b\u4e0d\u80fd\u88ab\u7981\u7528 +err.llm.cannot_delete_default=\u9ed8\u8ba4\u6a21\u578b\u4e0d\u80fd\u5220\u9664 +err.llm.provider_model_required=Provider \u548c\u6a21\u578b\u6807\u8bc6\u4e0d\u80fd\u4e3a\u7a7a +err.llm.model_exists=\u6a21\u578b\u5df2\u5b58\u5728 +err.llm.model_not_found=\u6a21\u578b\u4e0d\u5b58\u5728 +err.llm.builtin_readonly=\u5185\u7f6e\u6a21\u578b\u4e0d\u652f\u6301\u5220\u9664 +err.llm.only_enabled_default=\u53ea\u6709\u542f\u7528\u72b6\u6001\u7684\u6a21\u578b\u624d\u80fd\u8bbe\u4e3a\u9ed8\u8ba4 +err.llm.name_required=\u6a21\u578b\u540d\u79f0\u4e0d\u80fd\u4e3a\u7a7a +err.llm.id_required=\u6a21\u578b\u6807\u8bc6\u4e0d\u80fd\u4e3a\u7a7a +err.llm.id_exists=\u6a21\u578b\u6807\u8bc6\u5df2\u5b58\u5728 +err.llm.provider_fields_required=Provider ID \u548c\u540d\u79f0\u4e0d\u80fd\u4e3a\u7a7a +err.llm.provider_exists=Provider \u5df2\u5b58\u5728 +err.llm.provider_builtin_readonly=\u5185\u7f6e Provider \u4e0d\u652f\u6301\u5220\u9664 +err.llm.provider_not_found=Provider \u4e0d\u5b58\u5728 +err.llm.discovery_not_supported=\u8be5\u4f9b\u5e94\u5546\u4e0d\u652f\u6301\u6a21\u578b\u53d1\u73b0 +err.llm.no_model_for_test=\u6ca1\u6709\u5df2\u914d\u7f6e\u7684\u6a21\u578b\uff0c\u65e0\u6cd5\u6d4b\u8bd5 +err.llm.chatgpt_no_discovery=ChatGPT OAuth \u4e0d\u652f\u6301\u6a21\u578b\u53d1\u73b0 +err.llm.chatgpt_no_test=ChatGPT OAuth \u4e0d\u652f\u6301\u6a21\u578b\u6d4b\u8bd5 +err.llm.base_url_missing=Base URL \u672a\u914d\u7f6e +err.llm.dashscope_key_missing=DashScope API Key \u672a\u914d\u7f6e +err.llm.gemini_key_missing=Gemini API Key \u672a\u914d\u7f6e +err.llm.anthropic_key_missing=Anthropic API Key \u672a\u914d\u7f6e +err.llm.oauth_state_invalid=\u65e0\u6548\u7684 OAuth state +err.llm.oauth_no_refresh=\u65e0 refresh_token\uff0c\u8bf7\u91cd\u65b0\u767b\u5f55 +err.llm.oauth_not_connected=\u672a\u8fde\u63a5 OpenAI OAuth +err.llm.oauth_exchange_failed=OAuth token \u4ea4\u6362\u5931\u8d25 +err.llm.oauth_no_token=OAuth \u54cd\u5e94\u4e2d\u7f3a\u5c11 access_token +err.llm.chatgpt_not_configured=ChatGPT provider \u672a\u914d\u7f6e +err.llm.pkce_failed=PKCE \u751f\u6210\u5931\u8d25 +err.llm.chatgpt_stream_failed=ChatGPT \u6d41\u5f0f\u8c03\u7528\u5931\u8d25 +err.llm.chatgpt_error=ChatGPT \u8fd4\u56de\u9519\u8bef +err.llm.chatgpt_account_missing=chatgpt-account-id \u7f3a\u5931 +# datasource +err.datasource.not_found=\u6570\u636e\u6e90\u4e0d\u5b58\u5728 +err.datasource.sql_empty=SQL \u4e0d\u80fd\u4e3a\u7a7a +err.datasource.only_single_sql=\u4ec5\u5141\u8bb8\u6267\u884c\u5355\u6761 SQL +err.datasource.sql_parse_failed=SQL \u89e3\u6790\u5931\u8d25 +err.datasource.only_select=\u4ec5\u5141\u8bb8 SELECT \u67e5\u8be2 +err.datasource.connection_failed=\u83b7\u53d6\u6570\u636e\u5e93\u8fde\u63a5\u5931\u8d25 +err.datasource.unsupported_db=\u4e0d\u652f\u6301\u7684\u6570\u636e\u5e93\u7c7b\u578b +err.datasource.unsafe_jdbc=JDBC \u53c2\u6570\u5305\u542b\u4e0d\u5b89\u5168\u9009\u9879 +# approval +err.approval.not_found=\u5ba1\u6279\u8bb0\u5f55\u4e0d\u5b58\u5728\u6216\u5df2\u8fc7\u671f # --- WorkspacePathGuard --- guard.path.not_allowed=\u8def\u5f84\u4e0d\u5728\u5de5\u4f5c\u533a\u5141\u8bb8\u8303\u56f4\u5185: {0}\uff0c\u5141\u8bb8\u7684\u6839\u76ee\u5f55: {1} diff --git a/mateclaw-server/src/main/resources/messages_en.properties b/mateclaw-server/src/main/resources/messages_en.properties index 0d9c6bd6..07e2710a 100644 --- a/mateclaw-server/src/main/resources/messages_en.properties +++ b/mateclaw-server/src/main/resources/messages_en.properties @@ -179,6 +179,67 @@ err.cron.agent_required=Please select an Agent err.cron.expression_required=Cron expression cannot be empty err.cron.trigger_required=Trigger message cannot be empty err.cron.target_required=Execution target cannot be empty +# agent (extended) +err.agent.no_default_model=Cannot build Agent: please configure and enable a default model in Settings > Models +err.agent.model_not_configured=Model provider not configured, please fill in API Key in Settings > Models +err.agent.protocol_not_supported=Protocol not currently supported +err.agent.plan_compile_failed=Plan-Execute StateGraph compilation failed +err.agent.graph_compile_failed=StateGraph v2 compilation failed +err.agent.protocol_limited=StateGraph currently only supports DashScope, OpenAI-compatible, and Anthropic protocols +err.agent.provider_not_configured=Provider not configured, please fill in valid API Key and Base URL +err.agent.provider_apikey_invalid=Provider API Key not configured or invalid +err.agent.provider_baseurl_missing=Provider Base URL not configured +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.common.wrong_workspace=Resource does not belong to current workspace +# llm +err.llm.model_config_not_found=Model config not found +err.llm.no_available_model=No available model config +err.llm.cannot_disable_default=Cannot disable default model, switch default first +err.llm.cannot_delete_default=Cannot delete default model, switch default first +err.llm.provider_model_required=Provider and model ID cannot be empty +err.llm.model_exists=Model already exists +err.llm.model_not_found=Model not found +err.llm.builtin_readonly=Built-in model cannot be deleted +err.llm.only_enabled_default=Only enabled models can be set as default +err.llm.name_required=Model name cannot be empty +err.llm.id_required=Model identifier cannot be empty +err.llm.id_exists=Model identifier already exists +err.llm.provider_fields_required=Provider ID and name cannot be empty +err.llm.provider_exists=Provider already exists +err.llm.provider_builtin_readonly=Built-in provider cannot be deleted +err.llm.provider_not_found=Provider not found +err.llm.discovery_not_supported=This provider does not support model discovery +err.llm.no_model_for_test=No configured models, cannot test connection +err.llm.chatgpt_no_discovery=ChatGPT OAuth does not support model discovery +err.llm.chatgpt_no_test=ChatGPT OAuth does not support model testing +err.llm.base_url_missing=Base URL not configured +err.llm.dashscope_key_missing=DashScope API Key not configured +err.llm.gemini_key_missing=Gemini API Key not configured +err.llm.anthropic_key_missing=Anthropic API Key not configured +err.llm.oauth_state_invalid=Invalid OAuth state, may be expired or reused +err.llm.oauth_no_refresh=No refresh_token, please log in again +err.llm.oauth_not_connected=Not connected to OpenAI OAuth, please log in first +err.llm.oauth_exchange_failed=OAuth token exchange failed +err.llm.oauth_no_token=access_token missing in OAuth response +err.llm.chatgpt_not_configured=ChatGPT provider not configured, check database initialization +err.llm.pkce_failed=PKCE code_challenge generation failed +err.llm.chatgpt_stream_failed=ChatGPT streaming call failed +err.llm.chatgpt_error=ChatGPT returned an error +err.llm.chatgpt_account_missing=chatgpt-account-id missing, disconnect and re-login via OAuth +# datasource +err.datasource.not_found=Datasource not found +err.datasource.sql_empty=SQL cannot be empty +err.datasource.only_single_sql=Only single SQL statement allowed +err.datasource.sql_parse_failed=SQL parse failed +err.datasource.only_select=Only SELECT queries allowed +err.datasource.connection_failed=Failed to get database connection +err.datasource.unsupported_db=Unsupported database type +err.datasource.unsafe_jdbc=JDBC parameters contain unsafe options +# approval +err.approval.not_found=Approval record not found or expired # --- RuntimeContext --- context.current_time=[system-context] Current time: {0} {1} (Asia/Shanghai) diff --git a/mateclaw-ui/src/i18n/locales/en-US.ts b/mateclaw-ui/src/i18n/locales/en-US.ts index 23893334..666e9526 100644 --- a/mateclaw-ui/src/i18n/locales/en-US.ts +++ b/mateclaw-ui/src/i18n/locales/en-US.ts @@ -1395,6 +1395,67 @@ export default { toggleFailed: 'Failed to toggle channel status', invalidJson: 'Invalid config JSON format', }, + guide: { + dingtalk: { + step1: 'Create a bot on DingTalk Open Platform, fill in AppKey and AppSecret', + step2: 'Stream mode (recommended): Select "Stream Mode" for message receiving, no public IP required', + step3: 'Webhook mode: Select "HTTP Mode", paste the Webhook URL as the callback address', + }, + feishu: { + step1: 'Go to Feishu Open Platform and create an enterprise app', + step2: 'Fill in the app\'s App ID and App Secret below', + step3: 'Webhook mode: Set the request URL to the Webhook URL above in "Event Subscriptions", and subscribe to im.message.receive_v1 event', + step4: 'WebSocket mode: No public address needed, select "WebSocket (Long Connection)" in the connection mode below. Enable long connection in Feishu "Event Subscriptions"', + step5: 'For nickname display, request contact:user.base:readonly permission in "Permission Management"', + }, + telegram: { + step1: 'Search for {at}BotFather in Telegram, send /newbot to create a Bot', + step2: 'Fill in the Bot Token returned by BotFather below', + step3: 'Long-Polling mode (recommended): Auto-receives messages via getUpdates polling, no public IP required', + step4: 'Webhook mode: Requires a publicly accessible callback URL, switch connection mode and fill in Webhook URL', + }, + discord: { + step1: 'Go to Discord Developer Portal and create an Application', + step2: 'Create a Bot on the Bot page and copy the Bot Token below', + step3: 'Enable MESSAGE CONTENT Intent on the Bot page (Privileged Gateway Intents section)', + step4: 'In OAuth2 → URL Generator, check bot scope and Send Messages / Read Message History permissions, generate invite link to add Bot to target server', + step5: 'Auto-receives messages via Gateway WebSocket after startup, no public IP or callback URL required', + }, + wecom: { + step1: 'Go to WeCom Admin Console, navigate to "App Management → Smart Bot" and create a new bot', + step2: 'In bot configuration, select "API Mode → Configure Long Connection"', + step3: 'Record the bot\'s Bot ID and Secret, fill in below', + step4: 'After starting the channel, scan QR code in WeCom to chat with the bot, no public IP or callback URL required', + }, + weixin: { + step1: 'Ensure you have WeChat iLink Bot beta access (currently invite-only)', + step2: 'Click the "Get Login QR Code" button below, scan with WeChat to log in', + step3: 'After successful scan, the system auto-fills bot_token into config', + step4: 'After starting the channel, auto-receives messages via HTTP long polling, no public IP or callback URL required', + }, + qq: { + step1: 'Go to QQ Open Platform and create a bot application', + step2: 'Get AppID and AppSecret from the app management page, fill in below', + step3: 'Enable required message types in "Feature Config → Message Subscription" (C2C, Group @, Channel messages, etc.)', + step4: 'After starting the channel, auto-receives messages via WebSocket long connection, no public IP or callback URL required', + }, + }, + feishu: { + perm: { + message: 'Send and receive messages', + messageReason: 'Core: send and receive messages', + receive: 'Receive message events', + receiveReason: 'Core: receive user messages', + resource: 'Access message resources', + resourceReason: 'Get message content in WebSocket mode', + reactions: 'Manage message reactions', + reactionsReason: 'Message reactions: add 👍 emoji', + contact: 'Get basic user info', + contactReason: 'Nickname: display real user name', + media: 'Access message resources', + mediaReason: 'Media download: download images and files', + }, + }, }, skills: { title: 'Skills', diff --git a/mateclaw-ui/src/i18n/locales/zh-CN.ts b/mateclaw-ui/src/i18n/locales/zh-CN.ts index f8499565..405acccf 100644 --- a/mateclaw-ui/src/i18n/locales/zh-CN.ts +++ b/mateclaw-ui/src/i18n/locales/zh-CN.ts @@ -1405,6 +1405,67 @@ export default { toggleFailed: '切换渠道状态失败', invalidJson: '配置 JSON 格式不正确', }, + guide: { + dingtalk: { + step1: '在 钉钉开放平台 创建机器人,填入 AppKeyAppSecret', + step2: 'Stream 模式(推荐):消息接收模式选「Stream 模式」,无需公网 IP 即可使用', + step3: 'Webhook 模式:消息接收模式选「HTTP 模式」,将 Webhook URL 填入回调地址', + }, + feishu: { + step1: '前往 飞书开放平台 创建企业自建应用', + step2: '将应用的 App IDApp Secret 填入下方配置', + step3: 'Webhook 模式:在「事件订阅」配置中将请求地址设置为上方 Webhook URL,并订阅 im.message.receive_v1 事件', + step4: 'WebSocket 模式:无需公网地址,在下方接入模式中选择「WebSocket(长连接)」即可。需在飞书后台「事件订阅」中选择长连接方式', + step5: '如需昵称显示,请在「权限管理」中申请 contact:user.base:readonly 权限', + }, + telegram: { + step1: '在 Telegram 中搜索 {at}BotFather,发送 /newbot 创建 Bot', + step2: '将 BotFather 返回的 Bot Token 填入下方配置', + step3: 'Long-Polling 模式(推荐):启动后自动通过 getUpdates 轮询接收消息,无需公网 IP', + step4: 'Webhook 模式:需要公网可访问的回调地址,切换接入模式后填入 Webhook URL', + }, + discord: { + step1: '前往 Discord Developer Portal 创建 Application', + step2: '在 Bot 页面创建 Bot 并复制 Bot Token 填入下方配置', + step3: '在 Bot 页面开启 MESSAGE CONTENT Intent(Privileged Gateway Intents 区域)', + step4: '在 OAuth2 → URL Generator 中勾选 bot scope 和 Send Messages / Read Message History 权限,生成邀请链接将 Bot 添加到目标服务器', + step5: '启动后通过 Gateway WebSocket 自动接收消息,无需公网 IP 和回调 URL', + }, + wecom: { + step1: '前往 企业微信管理后台,进入「应用管理 → 智能机器人」创建一个新的智能机器人', + step2: '在机器人配置中选择「API 模式 → 配置长连接」', + step3: '记录机器人的 Bot IDSecret,填入下方配置', + step4: '启动渠道后即可在企业微信中扫码添加机器人对话,无需公网 IP 和回调 URL', + }, + weixin: { + step1: '确保已获得微信 iLink Bot 内测资格(目前为受邀内测阶段)', + step2: '点击下方「获取登录二维码」按钮,用微信扫码登录', + step3: '扫码成功后系统自动获取 bot_token 并填入配置', + step4: '启动渠道后通过 HTTP 长轮询自动接收消息,无需公网 IP 和回调 URL', + }, + qq: { + step1: '前往 QQ 开放平台 创建机器人应用', + step2: '在应用管理页面获取 AppIDAppSecret,填入下方配置', + step3: '在「功能配置 → 消息订阅」中开启需要的消息类型(C2C 消息、群聊 @消息、频道消息等)', + step4: '启动渠道后通过 WebSocket 长连接自动接收消息,无需公网 IP 和回调 URL', + }, + }, + feishu: { + perm: { + message: '获取与发送单聊、群组消息', + messageReason: '基础:收发消息', + receive: '接收消息事件', + receiveReason: '基础:接收用户消息', + resource: '获取消息中的资源文件', + resourceReason: 'WebSocket 模式下获取消息内容', + reactions: '管理消息表情回复', + reactionsReason: '消息反应:添加 👍 表情', + contact: '获取用户基本信息', + contactReason: '昵称获取:显示用户真实姓名', + media: '获取消息中的资源文件', + mediaReason: '媒体下载:下载图片和文件', + }, + }, }, skills: { title: '技能管理', diff --git a/mateclaw-ui/src/views/Channels.vue b/mateclaw-ui/src/views/Channels.vue index 10bbbe4a..ce4dccd9 100644 --- a/mateclaw-ui/src/views/Channels.vue +++ b/mateclaw-ui/src/views/Channels.vue @@ -543,69 +543,69 @@ interface WebhookGuideInfo { steps: string[] } -const WEBHOOK_GUIDES: Record = { +const WEBHOOK_GUIDES = computed>(() => ({ dingtalk: { steps: [ - '在 钉钉开放平台 创建机器人,填入 AppKeyAppSecret', - 'Stream 模式(推荐):消息接收模式选「Stream 模式」,无需公网 IP 即可使用', - 'Webhook 模式:消息接收模式选「HTTP 模式」,将 Webhook URL 填入回调地址', + t('channels.guide.dingtalk.step1'), + t('channels.guide.dingtalk.step2'), + t('channels.guide.dingtalk.step3'), ], }, feishu: { steps: [ - '前往 飞书开放平台 创建企业自建应用', - '将应用的 App IDApp Secret 填入下方配置', - 'Webhook 模式:在「事件订阅」配置中将请求地址设置为上方 Webhook URL,并订阅 im.message.receive_v1 事件', - 'WebSocket 模式:无需公网地址,在下方接入模式中选择「WebSocket(长连接)」即可。需在飞书后台「事件订阅」中选择长连接方式', - '如需昵称显示,请在「权限管理」中申请 contact:user.base:readonly 权限', + t('channels.guide.feishu.step1'), + t('channels.guide.feishu.step2'), + t('channels.guide.feishu.step3'), + t('channels.guide.feishu.step4'), + t('channels.guide.feishu.step5'), ], }, telegram: { steps: [ - '在 Telegram 中搜索 @BotFather,发送 /newbot 创建 Bot', - '将 BotFather 返回的 Bot Token 填入下方配置', - 'Long-Polling 模式(推荐):启动后自动通过 getUpdates 轮询接收消息,无需公网 IP', - 'Webhook 模式:需要公网可访问的回调地址,切换接入模式后填入 Webhook URL', + t('channels.guide.telegram.step1'), + t('channels.guide.telegram.step2'), + t('channels.guide.telegram.step3'), + t('channels.guide.telegram.step4'), ], }, discord: { steps: [ - '前往 Discord Developer Portal 创建 Application', - '在 Bot 页面创建 Bot 并复制 Bot Token 填入下方配置', - '在 Bot 页面开启 MESSAGE CONTENT Intent(Privileged Gateway Intents 区域)', - '在 OAuth2 → URL Generator 中勾选 bot scope 和 Send Messages / Read Message History 权限,生成邀请链接将 Bot 添加到目标服务器', - '启动后通过 Gateway WebSocket 自动接收消息,无需公网 IP 和回调 URL', + t('channels.guide.discord.step1'), + t('channels.guide.discord.step2'), + t('channels.guide.discord.step3'), + t('channels.guide.discord.step4'), + t('channels.guide.discord.step5'), ], }, wecom: { steps: [ - '前往 企业微信管理后台,进入「应用管理 → 智能机器人」创建一个新的智能机器人', - '在机器人配置中选择「API 模式 → 配置长连接」', - '记录机器人的 Bot IDSecret,填入下方配置', - '启动渠道后即可在企业微信中扫码添加机器人对话,无需公网 IP 和回调 URL', + t('channels.guide.wecom.step1'), + t('channels.guide.wecom.step2'), + t('channels.guide.wecom.step3'), + t('channels.guide.wecom.step4'), ], }, weixin: { steps: [ - '确保已获得微信 iLink Bot 内测资格(目前为受邀内测阶段)', - '点击下方「获取登录二维码」按钮,用微信扫码登录', - '扫码成功后系统自动获取 bot_token 并填入配置', - '启动渠道后通过 HTTP 长轮询自动接收消息,无需公网 IP 和回调 URL', + t('channels.guide.weixin.step1'), + t('channels.guide.weixin.step2'), + t('channels.guide.weixin.step3'), + t('channels.guide.weixin.step4'), ], }, qq: { steps: [ - '前往 QQ 开放平台 创建机器人应用', - '在应用管理页面获取 AppIDAppSecret,填入下方配置', - '在「功能配置 → 消息订阅」中开启需要的消息类型(C2C 消息、群聊 @消息、频道消息等)', - '启动渠道后通过 WebSocket 长连接自动接收消息,无需公网 IP 和回调 URL', + t('channels.guide.qq.step1'), + t('channels.guide.qq.step2'), + t('channels.guide.qq.step3'), + t('channels.guide.qq.step4'), ], }, -} +})) /** 当前渠道是否有接入引导 */ const webhookGuide = computed(() => { - return WEBHOOK_GUIDES[form.value.channelType] || null + return WEBHOOK_GUIDES.value[form.value.channelType] || null }) /** 当前渠道是否需要 Webhook URL(WebSocket / Stream / Long-Polling 模式不需要) */ @@ -823,37 +823,32 @@ const feishuPermissionUrl = computed(() => { const feishuRequiredPermissions = computed(() => { const perms: { scope: string; desc: string; reason: string }[] = [] - // 基础权限(始终需要) perms.push( - { scope: 'im:message', desc: '获取与发送单聊、群组消息', reason: '基础:收发消息' }, - { scope: 'im:message.receive_v1', desc: '接收消息事件', reason: '基础:接收用户消息' }, + { scope: 'im:message', desc: t('channels.feishu.perm.message'), reason: t('channels.feishu.perm.messageReason') }, + { scope: 'im:message.receive_v1', desc: t('channels.feishu.perm.receive'), reason: t('channels.feishu.perm.receiveReason') }, ) - // WebSocket 模式需要的权限 if (channelConfig.value?.connection_mode === 'websocket') { perms.push( - { scope: 'im:resource', desc: '获取消息中的资源文件', reason: 'WebSocket 模式下获取消息内容' }, + { scope: 'im:resource', desc: t('channels.feishu.perm.resource'), reason: t('channels.feishu.perm.resourceReason') }, ) } - // 消息反应 if (channelConfig.value?.enable_reaction !== false) { perms.push( - { scope: 'im:message.reactions', desc: '管理消息表情回复', reason: '消息反应:添加 👍 表情' }, + { scope: 'im:message.reactions', desc: t('channels.feishu.perm.reactions'), reason: t('channels.feishu.perm.reactionsReason') }, ) } - // 昵称获取 if (channelConfig.value?.enable_nickname_cache !== false) { perms.push( - { scope: 'contact:user.base:readonly', desc: '获取用户基本信息', reason: '昵称获取:显示用户真实姓名' }, + { scope: 'contact:user.base:readonly', desc: t('channels.feishu.perm.contact'), reason: t('channels.feishu.perm.contactReason') }, ) } - // 媒体下载 if (channelConfig.value?.media_download_enabled) { perms.push( - { scope: 'im:message.resource', desc: '获取消息中的资源文件', reason: '媒体下载:下载图片和文件' }, + { scope: 'im:message.resource', desc: t('channels.feishu.perm.media'), reason: t('channels.feishu.perm.mediaReason') }, ) }