feat(i18n): complete remaining i18n — DefaultToolGuard, LLM/Datasource services, Channels UI

This commit is contained in:
matevip 2026-04-11 18:02:03 +08:00
parent af9addaaec
commit 408ad980dd
21 changed files with 370 additions and 131 deletions

View File

@ -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<String, Object> kwargs = modelProviderService.readProviderGenerateKwargs(provider);
MultiValueMap<String, String> 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);

View File

@ -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", "资源不属于当前工作区");
}
}
}

View File

@ -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", "资源不属于当前工作区");
}
}
}

View File

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

View File

@ -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", "资源不属于当前工作区");
}
}
}

View File

@ -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", "资源不属于当前工作区");
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<ModelConfigEntity> 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<ModelInfoDTO> 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<ModelInfoDTO> 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<ModelInfoDTO> 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<ModelInfoDTO> 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<String, Object> 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<String, Object> 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<String, Object> 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())

View File

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

View File

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

View File

@ -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", "资源不属于当前工作区");
}
}
}

View File

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

View File

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

View File

@ -1395,6 +1395,67 @@ export default {
toggleFailed: 'Failed to toggle channel status',
invalidJson: 'Invalid config JSON format',
},
guide: {
dingtalk: {
step1: 'Create a bot on <a href="https://open-dev.dingtalk.com/" target="_blank" rel="noopener">DingTalk Open Platform</a>, fill in <b>AppKey</b> and <b>AppSecret</b>',
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 <a href="https://open.feishu.cn/" target="_blank" rel="noopener">Feishu Open Platform</a> and create an enterprise app',
step2: 'Fill in the app\'s <b>App ID</b> and <b>App Secret</b> below',
step3: '<b>Webhook mode</b>: Set the request URL to the Webhook URL above in "Event Subscriptions", and subscribe to <code>im.message.receive_v1</code> event',
step4: '<b>WebSocket mode</b>: 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 <code>contact:user.base:readonly</code> permission in "Permission Management"',
},
telegram: {
step1: 'Search for <a href="https://t.me/BotFather" target="_blank" rel="noopener">{at}BotFather</a> in Telegram, send <code>/newbot</code> to create a Bot',
step2: 'Fill in the <b>Bot Token</b> returned by BotFather below',
step3: '<b>Long-Polling mode</b> (recommended): Auto-receives messages via <code>getUpdates</code> polling, <b>no public IP required</b>',
step4: '<b>Webhook mode</b>: Requires a publicly accessible callback URL, switch connection mode and fill in Webhook URL',
},
discord: {
step1: 'Go to <a href="https://discord.com/developers/applications" target="_blank" rel="noopener">Discord Developer Portal</a> and create an Application',
step2: 'Create a Bot on the Bot page and copy the <b>Bot Token</b> below',
step3: 'Enable <b>MESSAGE CONTENT Intent</b> on the Bot page (Privileged Gateway Intents section)',
step4: 'In OAuth2 → URL Generator, check <b>bot</b> scope and <b>Send Messages / Read Message History</b> permissions, generate invite link to add Bot to target server',
step5: 'Auto-receives messages via <b>Gateway WebSocket</b> after startup, <b>no public IP or callback URL required</b>',
},
wecom: {
step1: 'Go to <a href="https://work.weixin.qq.com/wework_admin/frame" target="_blank" rel="noopener">WeCom Admin Console</a>, navigate to "App Management → Smart Bot" and create a new bot',
step2: 'In bot configuration, select "<b>API Mode → Configure Long Connection</b>"',
step3: 'Record the bot\'s <b>Bot ID</b> and <b>Secret</b>, fill in below',
step4: 'After starting the channel, scan QR code in WeCom to chat with the bot, <b>no public IP or callback URL required</b>',
},
weixin: {
step1: 'Ensure you have WeChat <b>iLink Bot</b> 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 <b>bot_token</b> into config',
step4: 'After starting the channel, auto-receives messages via <b>HTTP long polling</b>, <b>no public IP or callback URL required</b>',
},
qq: {
step1: 'Go to <a href="https://q.qq.com/" target="_blank" rel="noopener">QQ Open Platform</a> and create a bot application',
step2: 'Get <b>AppID</b> and <b>AppSecret</b> 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 <b>WebSocket long connection</b>, <b>no public IP or callback URL required</b>',
},
},
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',

View File

@ -1405,6 +1405,67 @@ export default {
toggleFailed: '切换渠道状态失败',
invalidJson: '配置 JSON 格式不正确',
},
guide: {
dingtalk: {
step1: '在 <a href="https://open-dev.dingtalk.com/" target="_blank" rel="noopener">钉钉开放平台</a> 创建机器人,填入 <b>AppKey</b> 和 <b>AppSecret</b>',
step2: 'Stream 模式推荐消息接收模式选「Stream 模式」,无需公网 IP 即可使用',
step3: 'Webhook 模式消息接收模式选「HTTP 模式」,将 Webhook URL 填入回调地址',
},
feishu: {
step1: '前往 <a href="https://open.feishu.cn/" target="_blank" rel="noopener">飞书开放平台</a> 创建企业自建应用',
step2: '将应用的 <b>App ID</b> 和 <b>App Secret</b> 填入下方配置',
step3: '<b>Webhook 模式</b>:在「事件订阅」配置中将请求地址设置为上方 Webhook URL并订阅 <code>im.message.receive_v1</code> 事件',
step4: '<b>WebSocket 模式</b>无需公网地址在下方接入模式中选择「WebSocket长连接」即可。需在飞书后台「事件订阅」中选择长连接方式',
step5: '如需昵称显示,请在「权限管理」中申请 <code>contact:user.base:readonly</code> 权限',
},
telegram: {
step1: '在 Telegram 中搜索 <a href="https://t.me/BotFather" target="_blank" rel="noopener">{at}BotFather</a>,发送 <code>/newbot</code> 创建 Bot',
step2: '将 BotFather 返回的 <b>Bot Token</b> 填入下方配置',
step3: '<b>Long-Polling 模式</b>(推荐):启动后自动通过 <code>getUpdates</code> 轮询接收消息,<b>无需公网 IP</b>',
step4: '<b>Webhook 模式</b>:需要公网可访问的回调地址,切换接入模式后填入 Webhook URL',
},
discord: {
step1: '前往 <a href="https://discord.com/developers/applications" target="_blank" rel="noopener">Discord Developer Portal</a> 创建 Application',
step2: '在 Bot 页面创建 Bot 并复制 <b>Bot Token</b> 填入下方配置',
step3: '在 Bot 页面开启 <b>MESSAGE CONTENT Intent</b>Privileged Gateway Intents 区域)',
step4: '在 OAuth2 → URL Generator 中勾选 <b>bot</b> scope 和 <b>Send Messages / Read Message History</b> 权限,生成邀请链接将 Bot 添加到目标服务器',
step5: '启动后通过 <b>Gateway WebSocket</b> 自动接收消息,<b>无需公网 IP 和回调 URL</b>',
},
wecom: {
step1: '前往 <a href="https://work.weixin.qq.com/wework_admin/frame" target="_blank" rel="noopener">企业微信管理后台</a>,进入「应用管理 → 智能机器人」创建一个新的智能机器人',
step2: '在机器人配置中选择「<b>API 模式 → 配置长连接</b>」',
step3: '记录机器人的 <b>Bot ID</b> 和 <b>Secret</b>,填入下方配置',
step4: '启动渠道后即可在企业微信中扫码添加机器人对话,<b>无需公网 IP 和回调 URL</b>',
},
weixin: {
step1: '确保已获得微信 <b>iLink Bot</b> 内测资格(目前为受邀内测阶段)',
step2: '点击下方「获取登录二维码」按钮,用微信扫码登录',
step3: '扫码成功后系统自动获取 <b>bot_token</b> 并填入配置',
step4: '启动渠道后通过 <b>HTTP 长轮询</b>自动接收消息,<b>无需公网 IP 和回调 URL</b>',
},
qq: {
step1: '前往 <a href="https://q.qq.com/" target="_blank" rel="noopener">QQ 开放平台</a> 创建机器人应用',
step2: '在应用管理页面获取 <b>AppID</b> 和 <b>AppSecret</b>,填入下方配置',
step3: '在「功能配置 → 消息订阅」中开启需要的消息类型C2C 消息、群聊 @消息、频道消息等)',
step4: '启动渠道后通过 <b>WebSocket 长连接</b>自动接收消息,<b>无需公网 IP 和回调 URL</b>',
},
},
feishu: {
perm: {
message: '获取与发送单聊、群组消息',
messageReason: '基础:收发消息',
receive: '接收消息事件',
receiveReason: '基础:接收用户消息',
resource: '获取消息中的资源文件',
resourceReason: 'WebSocket 模式下获取消息内容',
reactions: '管理消息表情回复',
reactionsReason: '消息反应:添加 👍 表情',
contact: '获取用户基本信息',
contactReason: '昵称获取:显示用户真实姓名',
media: '获取消息中的资源文件',
mediaReason: '媒体下载:下载图片和文件',
},
},
},
skills: {
title: '技能管理',

View File

@ -543,69 +543,69 @@ interface WebhookGuideInfo {
steps: string[]
}
const WEBHOOK_GUIDES: Record<string, WebhookGuideInfo> = {
const WEBHOOK_GUIDES = computed<Record<string, WebhookGuideInfo>>(() => ({
dingtalk: {
steps: [
'在 <a href="https://open-dev.dingtalk.com/" target="_blank" rel="noopener">钉钉开放平台</a> 创建机器人,填入 <b>AppKey</b> 和 <b>AppSecret</b>',
'Stream 模式推荐消息接收模式选「Stream 模式」,无需公网 IP 即可使用',
'Webhook 模式消息接收模式选「HTTP 模式」,将 Webhook URL 填入回调地址',
t('channels.guide.dingtalk.step1'),
t('channels.guide.dingtalk.step2'),
t('channels.guide.dingtalk.step3'),
],
},
feishu: {
steps: [
'前往 <a href="https://open.feishu.cn/" target="_blank" rel="noopener">飞书开放平台</a> 创建企业自建应用',
'将应用的 <b>App ID</b> 和 <b>App Secret</b> 填入下方配置',
'<b>Webhook 模式</b>:在「事件订阅」配置中将请求地址设置为上方 Webhook URL并订阅 <code>im.message.receive_v1</code> 事件',
'<b>WebSocket 模式</b>无需公网地址在下方接入模式中选择「WebSocket长连接」即可。需在飞书后台「事件订阅」中选择长连接方式',
'如需昵称显示,请在「权限管理」中申请 <code>contact:user.base:readonly</code> 权限',
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 中搜索 <a href="https://t.me/BotFather" target="_blank" rel="noopener">@BotFather</a>,发送 <code>/newbot</code> 创建 Bot',
'将 BotFather 返回的 <b>Bot Token</b> 填入下方配置',
'<b>Long-Polling 模式</b>(推荐):启动后自动通过 <code>getUpdates</code> 轮询接收消息,<b>无需公网 IP</b>',
'<b>Webhook 模式</b>:需要公网可访问的回调地址,切换接入模式后填入 Webhook URL',
t('channels.guide.telegram.step1'),
t('channels.guide.telegram.step2'),
t('channels.guide.telegram.step3'),
t('channels.guide.telegram.step4'),
],
},
discord: {
steps: [
'前往 <a href="https://discord.com/developers/applications" target="_blank" rel="noopener">Discord Developer Portal</a> 创建 Application',
'在 Bot 页面创建 Bot 并复制 <b>Bot Token</b> 填入下方配置',
'在 Bot 页面开启 <b>MESSAGE CONTENT Intent</b>Privileged Gateway Intents 区域)',
'在 OAuth2 → URL Generator 中勾选 <b>bot</b> scope 和 <b>Send Messages / Read Message History</b> 权限,生成邀请链接将 Bot 添加到目标服务器',
'启动后通过 <b>Gateway WebSocket</b> 自动接收消息,<b>无需公网 IP 和回调 URL</b>',
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: [
'前往 <a href="https://work.weixin.qq.com/wework_admin/frame" target="_blank" rel="noopener">企业微信管理后台</a>,进入「应用管理 → 智能机器人」创建一个新的智能机器人',
'在机器人配置中选择「<b>API 模式 → 配置长连接</b>」',
'记录机器人的 <b>Bot ID</b> 和 <b>Secret</b>,填入下方配置',
'启动渠道后即可在企业微信中扫码添加机器人对话,<b>无需公网 IP 和回调 URL</b>',
t('channels.guide.wecom.step1'),
t('channels.guide.wecom.step2'),
t('channels.guide.wecom.step3'),
t('channels.guide.wecom.step4'),
],
},
weixin: {
steps: [
'确保已获得微信 <b>iLink Bot</b> 内测资格(目前为受邀内测阶段)',
'点击下方「获取登录二维码」按钮,用微信扫码登录',
'扫码成功后系统自动获取 <b>bot_token</b> 并填入配置',
'启动渠道后通过 <b>HTTP 长轮询</b>自动接收消息,<b>无需公网 IP 和回调 URL</b>',
t('channels.guide.weixin.step1'),
t('channels.guide.weixin.step2'),
t('channels.guide.weixin.step3'),
t('channels.guide.weixin.step4'),
],
},
qq: {
steps: [
'前往 <a href="https://q.qq.com/" target="_blank" rel="noopener">QQ 开放平台</a> 创建机器人应用',
'在应用管理页面获取 <b>AppID</b> 和 <b>AppSecret</b>,填入下方配置',
'在「功能配置 → 消息订阅」中开启需要的消息类型C2C 消息、群聊 @消息、频道消息等)',
'启动渠道后通过 <b>WebSocket 长连接</b>自动接收消息,<b>无需公网 IP 和回调 URL</b>',
t('channels.guide.qq.step1'),
t('channels.guide.qq.step2'),
t('channels.guide.qq.step3'),
t('channels.guide.qq.step4'),
],
},
}
}))
/** 当前渠道是否有接入引导 */
const webhookGuide = computed<WebhookGuideInfo | null>(() => {
return WEBHOOK_GUIDES[form.value.channelType] || null
return WEBHOOK_GUIDES.value[form.value.channelType] || null
})
/** 当前渠道是否需要 Webhook URLWebSocket / 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') },
)
}