mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(i18n): complete remaining i18n — DefaultToolGuard, LLM/Datasource services, Channels UI
This commit is contained in:
parent
af9addaaec
commit
408ad980dd
@ -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);
|
||||
|
||||
@ -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", "资源不属于当前工作区");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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", "资源不属于当前工作区");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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", "资源不属于当前工作区");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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", "资源不属于当前工作区");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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", "资源不属于当前工作区");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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: '技能管理',
|
||||
|
||||
@ -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 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') },
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user