mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-16 20:34:39 +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 {
|
try {
|
||||||
runtimeModel = modelConfigService.getDefaultModel();
|
runtimeModel = modelConfigService.getDefaultModel();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new MateClawException("无法构建 Agent:请先在「设置 → 模型」中配置并启用默认模型");
|
throw new MateClawException("err.agent.no_default_model", "无法构建 Agent:请先在「设置 → 模型」中配置并启用默认模型");
|
||||||
}
|
}
|
||||||
|
|
||||||
ModelProviderEntity provider;
|
ModelProviderEntity provider;
|
||||||
try {
|
try {
|
||||||
provider = modelProviderService.getProviderConfig(runtimeModel.getProvider());
|
provider = modelProviderService.getProviderConfig(runtimeModel.getProvider());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new MateClawException("模型 " + runtimeModel.getModelName()
|
throw new MateClawException("err.agent.model_not_configured", "模型 " + runtimeModel.getModelName()
|
||||||
+ " 的 Provider(" + runtimeModel.getProvider() + ")未配置,请检查模型设置");
|
+ " 的 Provider(" + runtimeModel.getProvider() + ")未配置,请检查模型设置");
|
||||||
}
|
}
|
||||||
ModelProtocol protocol = ModelProtocol.fromChatModel(provider.getChatModel());
|
ModelProtocol protocol = ModelProtocol.fromChatModel(provider.getChatModel());
|
||||||
@ -171,7 +171,7 @@ public class AgentGraphBuilder {
|
|||||||
|
|
||||||
// 当前仅支持 DashScope 和 OpenAI-compatible,其他协议直接拒绝
|
// 当前仅支持 DashScope 和 OpenAI-compatible,其他协议直接拒绝
|
||||||
if (!supportsStateGraph(protocol)) {
|
if (!supportsStateGraph(protocol)) {
|
||||||
throw new MateClawException("当前不支持协议 " + protocol.getId()
|
throw new MateClawException("err.agent.protocol_not_supported", "当前不支持协议 " + protocol.getId()
|
||||||
+ ",请切换到 DashScope 或 OpenAI-compatible 模型");
|
+ ",请切换到 DashScope 或 OpenAI-compatible 模型");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -332,7 +332,7 @@ public class AgentGraphBuilder {
|
|||||||
.recursionLimit(maxIterations > 0 ? maxIterations * 3 + 10 : 300)
|
.recursionLimit(maxIterations > 0 ? maxIterations * 3 + 10 : 300)
|
||||||
.build());
|
.build());
|
||||||
} catch (Exception e) {
|
} 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())
|
.withLifecycleListener(new ReActLifecycleListener())
|
||||||
.build());
|
.build());
|
||||||
} catch (Exception e) {
|
} 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();
|
.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) {
|
OpenAiApi buildOpenAiApi(ModelProviderEntity provider) {
|
||||||
if (provider == null || !modelProviderService.isProviderConfigured(provider.getProviderId())) {
|
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();
|
String apiKey = provider.getApiKey();
|
||||||
if (!modelProviderService.hasUsableApiKey(apiKey)) {
|
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());
|
String baseUrl = normalizeOpenAiBaseUrl(provider.getBaseUrl());
|
||||||
if (!StringUtils.hasText(baseUrl)) {
|
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);
|
Map<String, Object> kwargs = modelProviderService.readProviderGenerateKwargs(provider);
|
||||||
MultiValueMap<String, String> headers = buildOpenAiHeaders(kwargs);
|
MultiValueMap<String, String> headers = buildOpenAiHeaders(kwargs);
|
||||||
@ -892,7 +892,7 @@ public class AgentGraphBuilder {
|
|||||||
apiKey = readApiKeyFromDefaultChatModel();
|
apiKey = readApiKeyFromDefaultChatModel();
|
||||||
}
|
}
|
||||||
if (!modelProviderService.hasUsableApiKey(apiKey)) {
|
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());
|
builder.apiKey(apiKey.trim());
|
||||||
|
|
||||||
@ -915,11 +915,11 @@ public class AgentGraphBuilder {
|
|||||||
|
|
||||||
private AnthropicApi buildAnthropicApi(ModelProviderEntity provider) {
|
private AnthropicApi buildAnthropicApi(ModelProviderEntity provider) {
|
||||||
if (provider == null || !modelProviderService.isProviderConfigured(provider.getProviderId())) {
|
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();
|
String apiKey = provider.getApiKey();
|
||||||
if (!modelProviderService.hasUsableApiKey(apiKey)) {
|
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();
|
String baseUrl = provider.getBaseUrl();
|
||||||
RestClient.Builder restClientBuilder = restClientBuilderProvider.getIfAvailable(RestClient::builder);
|
RestClient.Builder restClientBuilder = restClientBuilderProvider.getIfAvailable(RestClient::builder);
|
||||||
|
|||||||
@ -111,7 +111,7 @@ public class AgentBindingController {
|
|||||||
}
|
}
|
||||||
long requestedWs = headerWorkspaceId != null ? headerWorkspaceId : 1L;
|
long requestedWs = headerWorkspaceId != null ? headerWorkspaceId : 1L;
|
||||||
if (agent.getWorkspaceId() != null && !agent.getWorkspaceId().equals(requestedWs)) {
|
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) {
|
private void verifyResourceWorkspace(Long resourceWorkspaceId, Long headerWorkspaceId) {
|
||||||
long requestedWs = headerWorkspaceId != null ? headerWorkspaceId : 1L;
|
long requestedWs = headerWorkspaceId != null ? headerWorkspaceId : 1L;
|
||||||
if (resourceWorkspaceId != null && !resourceWorkspaceId.equals(requestedWs)) {
|
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()
|
TemplateDTO template = listTemplates().stream()
|
||||||
.filter(t -> t.getId().equals(templateId))
|
.filter(t -> t.getId().equals(templateId))
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.orElseThrow(() -> new MateClawException("模板不存在: " + templateId));
|
.orElseThrow(() -> new MateClawException("err.agent.template_not_found", "模板不存在: " + templateId));
|
||||||
|
|
||||||
// 1. 创建 Agent
|
// 1. 创建 Agent
|
||||||
AgentEntity agent = new AgentEntity();
|
AgentEntity agent = new AgentEntity();
|
||||||
|
|||||||
@ -129,7 +129,7 @@ public class ChannelController {
|
|||||||
private void verifyResourceWorkspace(Long resourceWorkspaceId, Long headerWorkspaceId) {
|
private void verifyResourceWorkspace(Long resourceWorkspaceId, Long headerWorkspaceId) {
|
||||||
long requestedWs = headerWorkspaceId != null ? headerWorkspaceId : 1L;
|
long requestedWs = headerWorkspaceId != null ? headerWorkspaceId : 1L;
|
||||||
if (resourceWorkspaceId != null && !resourceWorkspaceId.equals(requestedWs)) {
|
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) {
|
if (agent != null && agent.getWorkspaceId() != null) {
|
||||||
long wsId = workspaceId != null ? workspaceId : 1L;
|
long wsId = workspaceId != null ? workspaceId : 1L;
|
||||||
if (!agent.getWorkspaceId().equals(wsId)) {
|
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 {
|
try {
|
||||||
return ds.getConnection();
|
return ds.getConnection();
|
||||||
} catch (SQLException e) {
|
} 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);
|
baseUrl = String.format("jdbc:clickhouse://%s:%d/%s", host, port, dbName);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new MateClawException("不支持的数据库类型: " + dbType);
|
throw new MateClawException("err.datasource.unsupported_db", "不支持的数据库类型: " + dbType);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (extra != null && !extra.isBlank()) {
|
if (extra != null && !extra.isBlank()) {
|
||||||
// 安全检查:拒绝危险参数
|
// 安全检查:拒绝危险参数
|
||||||
String lowerExtra = extra.toLowerCase();
|
String lowerExtra = extra.toLowerCase();
|
||||||
if (lowerExtra.contains("allowloadlocalinfile") || lowerExtra.contains("autodeserialize")) {
|
if (lowerExtra.contains("allowloadlocalinfile") || lowerExtra.contains("autodeserialize")) {
|
||||||
throw new MateClawException("JDBC 参数包含不安全选项");
|
throw new MateClawException("err.datasource.unsafe_jdbc", "JDBC 参数包含不安全选项");
|
||||||
}
|
}
|
||||||
baseUrl += (baseUrl.contains("?") ? "&" : "?") + extra;
|
baseUrl += (baseUrl.contains("?") ? "&" : "?") + extra;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,7 +51,7 @@ public class DatasourceService {
|
|||||||
public DatasourceEntity getById(Long id) {
|
public DatasourceEntity getById(Long id) {
|
||||||
DatasourceEntity entity = datasourceMapper.selectById(id);
|
DatasourceEntity entity = datasourceMapper.selectById(id);
|
||||||
if (entity == null) {
|
if (entity == null) {
|
||||||
throw new MateClawException("数据源不存在: " + id);
|
throw new MateClawException("err.datasource.not_found", "数据源不存在: " + id);
|
||||||
}
|
}
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -37,7 +37,7 @@ public class SqlValidationService {
|
|||||||
*/
|
*/
|
||||||
public String validateAndNormalize(String sql) {
|
public String validateAndNormalize(String sql) {
|
||||||
if (sql == null || sql.isBlank()) {
|
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);
|
Statements stmts = CCJSqlParserUtil.parseStatements(sql);
|
||||||
if (stmts.getStatements().size() != 1) {
|
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);
|
statement = stmts.getStatements().get(0);
|
||||||
} catch (JSQLParserException e) {
|
} catch (JSQLParserException e) {
|
||||||
throw new MateClawException("SQL 解析失败: " + e.getMessage());
|
throw new MateClawException("err.datasource.sql_parse_failed", "SQL 解析失败: " + e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 仅允许 SELECT
|
// 仅允许 SELECT
|
||||||
if (!(statement instanceof 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;
|
Select select = (Select) statement;
|
||||||
|
|||||||
@ -95,7 +95,7 @@ public class ChatGPTResponsesClient {
|
|||||||
.filter(line -> !line.isBlank() && !line.equals("[DONE]"))
|
.filter(line -> !line.isBlank() && !line.equals("[DONE]"))
|
||||||
.mapNotNull(this::parseSSEEvent)
|
.mapNotNull(this::parseSSEEvent)
|
||||||
.onErrorMap(e -> e instanceof MateClawException ? e
|
.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)) {
|
if ("response.failed".equals(type)) {
|
||||||
String error = node.path("response").path("error").path("message").asText("Unknown error");
|
String error = node.path("response").path("error").path("message").asText("Unknown error");
|
||||||
log.error("[ChatGPT] Responses API error: {}", error);
|
log.error("[ChatGPT] Responses API error: {}", error);
|
||||||
throw new MateClawException("ChatGPT 返回错误: " + error);
|
throw new MateClawException("err.llm.chatgpt_error", "ChatGPT 返回错误: " + error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@ -305,7 +305,7 @@ public class ChatGPTResponsesClient {
|
|||||||
|
|
||||||
private void setHeaders(HttpHeaders headers, String accessToken, String accountId) {
|
private void setHeaders(HttpHeaders headers, String accessToken, String accountId) {
|
||||||
if (accountId == null || accountId.isBlank()) {
|
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.setBearerAuth(accessToken);
|
||||||
headers.set("chatgpt-account-id", accountId);
|
headers.set("chatgpt-account-id", accountId);
|
||||||
|
|||||||
@ -191,7 +191,7 @@ public class OpenAIOAuthService {
|
|||||||
private void exchangeToken(String code, String state) {
|
private void exchangeToken(String code, String state) {
|
||||||
String codeVerifier = pendingStates.remove(state);
|
String codeVerifier = pendingStates.remove(state);
|
||||||
if (codeVerifier == null) {
|
if (codeVerifier == null) {
|
||||||
throw new MateClawException("无效的 OAuth state,可能已过期或重复使用");
|
throw new MateClawException("err.llm.oauth_state_invalid", "无效的 OAuth state,可能已过期或重复使用");
|
||||||
}
|
}
|
||||||
|
|
||||||
String body = "grant_type=authorization_code"
|
String body = "grant_type=authorization_code"
|
||||||
@ -210,7 +210,7 @@ public class OpenAIOAuthService {
|
|||||||
public void refreshToken() {
|
public void refreshToken() {
|
||||||
ModelProviderEntity provider = getProvider();
|
ModelProviderEntity provider = getProvider();
|
||||||
if (!StringUtils.hasText(provider.getOauthRefreshToken())) {
|
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"
|
String body = "grant_type=refresh_token"
|
||||||
@ -227,7 +227,7 @@ public class OpenAIOAuthService {
|
|||||||
public String ensureValidAccessToken() {
|
public String ensureValidAccessToken() {
|
||||||
ModelProviderEntity provider = getProvider();
|
ModelProviderEntity provider = getProvider();
|
||||||
if (!StringUtils.hasText(provider.getOauthAccessToken())) {
|
if (!StringUtils.hasText(provider.getOauthAccessToken())) {
|
||||||
throw new MateClawException("未连接 OpenAI OAuth,请先登录");
|
throw new MateClawException("err.llm.oauth_not_connected", "未连接 OpenAI OAuth,请先登录");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提前 5 分钟刷新
|
// 提前 5 分钟刷新
|
||||||
@ -299,7 +299,7 @@ public class OpenAIOAuthService {
|
|||||||
return objectMapper.readTree(response);
|
return objectMapper.readTree(response);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("OpenAI OAuth token 请求失败", 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);
|
int expiresIn = tokenResponse.path("expires_in").asInt(3600);
|
||||||
|
|
||||||
if (!StringUtils.hasText(accessToken)) {
|
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);
|
String accountId = extractAccountIdFromJwt(accessToken);
|
||||||
@ -354,7 +354,7 @@ public class OpenAIOAuthService {
|
|||||||
private ModelProviderEntity getProvider() {
|
private ModelProviderEntity getProvider() {
|
||||||
ModelProviderEntity provider = modelProviderMapper.selectById(PROVIDER_ID);
|
ModelProviderEntity provider = modelProviderMapper.selectById(PROVIDER_ID);
|
||||||
if (provider == null) {
|
if (provider == null) {
|
||||||
throw new MateClawException("OpenAI ChatGPT provider 未配置,请检查数据库初始化");
|
throw new MateClawException("err.llm.chatgpt_not_configured", "OpenAI ChatGPT provider 未配置,请检查数据库初始化");
|
||||||
}
|
}
|
||||||
return provider;
|
return provider;
|
||||||
}
|
}
|
||||||
@ -384,7 +384,7 @@ public class OpenAIOAuthService {
|
|||||||
byte[] digest = md.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII));
|
byte[] digest = md.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII));
|
||||||
return base64UrlEncode(digest);
|
return base64UrlEncode(digest);
|
||||||
} catch (Exception e) {
|
} 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) {
|
public ModelConfigEntity getModel(Long id) {
|
||||||
ModelConfigEntity entity = modelConfigMapper.selectById(id);
|
ModelConfigEntity entity = modelConfigMapper.selectById(id);
|
||||||
if (entity == null) {
|
if (entity == null) {
|
||||||
throw new MateClawException("模型配置不存在: " + id);
|
throw new MateClawException("err.llm.model_config_not_found", "模型配置不存在: " + id);
|
||||||
}
|
}
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
@ -64,7 +64,7 @@ public class ModelConfigService {
|
|||||||
.orderByAsc(ModelConfigEntity::getName)
|
.orderByAsc(ModelConfigEntity::getName)
|
||||||
.last("LIMIT 1"));
|
.last("LIMIT 1"));
|
||||||
if (entity == null) {
|
if (entity == null) {
|
||||||
throw new MateClawException("没有可用的模型配置");
|
throw new MateClawException("err.llm.no_available_model", "没有可用的模型配置");
|
||||||
}
|
}
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
@ -103,7 +103,7 @@ public class ModelConfigService {
|
|||||||
clearDefaultFlag();
|
clearDefaultFlag();
|
||||||
}
|
}
|
||||||
if (existing.getIsDefault() && Boolean.FALSE.equals(entity.getEnabled())) {
|
if (existing.getIsDefault() && Boolean.FALSE.equals(entity.getEnabled())) {
|
||||||
throw new MateClawException("默认模型不能被禁用,请先切换默认模型");
|
throw new MateClawException("err.llm.cannot_disable_default", "默认模型不能被禁用,请先切换默认模型");
|
||||||
}
|
}
|
||||||
modelConfigMapper.updateById(entity);
|
modelConfigMapper.updateById(entity);
|
||||||
ensureDefaultExists();
|
ensureDefaultExists();
|
||||||
@ -114,7 +114,7 @@ public class ModelConfigService {
|
|||||||
public void deleteModel(Long id) {
|
public void deleteModel(Long id) {
|
||||||
ModelConfigEntity entity = getModel(id);
|
ModelConfigEntity entity = getModel(id);
|
||||||
if (Boolean.TRUE.equals(entity.getIsDefault())) {
|
if (Boolean.TRUE.equals(entity.getIsDefault())) {
|
||||||
throw new MateClawException("默认模型不能删除,请先切换默认模型");
|
throw new MateClawException("err.llm.cannot_delete_default", "默认模型不能删除,请先切换默认模型");
|
||||||
}
|
}
|
||||||
modelConfigMapper.deleteById(id);
|
modelConfigMapper.deleteById(id);
|
||||||
ensureDefaultExists();
|
ensureDefaultExists();
|
||||||
@ -123,14 +123,14 @@ public class ModelConfigService {
|
|||||||
|
|
||||||
public ModelConfigEntity addModelToProvider(String providerId, String modelId, String displayName, boolean builtin) {
|
public ModelConfigEntity addModelToProvider(String providerId, String modelId, String displayName, boolean builtin) {
|
||||||
if (!StringUtils.hasText(providerId) || !StringUtils.hasText(modelId)) {
|
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>()
|
ModelConfigEntity existing = modelConfigMapper.selectOne(new LambdaQueryWrapper<ModelConfigEntity>()
|
||||||
.eq(ModelConfigEntity::getProvider, providerId)
|
.eq(ModelConfigEntity::getProvider, providerId)
|
||||||
.eq(ModelConfigEntity::getModelName, modelId)
|
.eq(ModelConfigEntity::getModelName, modelId)
|
||||||
.last("LIMIT 1"));
|
.last("LIMIT 1"));
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
throw new MateClawException("模型已存在: " + modelId);
|
throw new MateClawException("err.llm.model_exists", "模型已存在: " + modelId);
|
||||||
}
|
}
|
||||||
ModelConfigEntity entity = new ModelConfigEntity();
|
ModelConfigEntity entity = new ModelConfigEntity();
|
||||||
entity.setName(StringUtils.hasText(displayName) ? displayName : modelId);
|
entity.setName(StringUtils.hasText(displayName) ? displayName : modelId);
|
||||||
@ -155,10 +155,10 @@ public class ModelConfigService {
|
|||||||
.eq(ModelConfigEntity::getModelName, modelId)
|
.eq(ModelConfigEntity::getModelName, modelId)
|
||||||
.last("LIMIT 1"));
|
.last("LIMIT 1"));
|
||||||
if (entity == null) {
|
if (entity == null) {
|
||||||
throw new MateClawException("模型不存在: " + modelId);
|
throw new MateClawException("err.llm.model_not_found", "模型不存在: " + modelId);
|
||||||
}
|
}
|
||||||
if (Boolean.TRUE.equals(entity.getBuiltin())) {
|
if (Boolean.TRUE.equals(entity.getBuiltin())) {
|
||||||
throw new MateClawException("内置模型不支持删除");
|
throw new MateClawException("err.llm.builtin_readonly", "内置模型不支持删除");
|
||||||
}
|
}
|
||||||
deleteModel(entity.getId());
|
deleteModel(entity.getId());
|
||||||
}
|
}
|
||||||
@ -175,7 +175,7 @@ public class ModelConfigService {
|
|||||||
public ModelConfigEntity setDefaultModel(Long id) {
|
public ModelConfigEntity setDefaultModel(Long id) {
|
||||||
ModelConfigEntity entity = getModel(id);
|
ModelConfigEntity entity = getModel(id);
|
||||||
if (!Boolean.TRUE.equals(entity.getEnabled())) {
|
if (!Boolean.TRUE.equals(entity.getEnabled())) {
|
||||||
throw new MateClawException("只有启用状态的模型才能设为默认");
|
throw new MateClawException("err.llm.only_enabled_default", "只有启用状态的模型才能设为默认");
|
||||||
}
|
}
|
||||||
clearDefaultFlag();
|
clearDefaultFlag();
|
||||||
entity.setIsDefault(true);
|
entity.setIsDefault(true);
|
||||||
@ -190,7 +190,7 @@ public class ModelConfigService {
|
|||||||
.eq(ModelConfigEntity::getModelName, modelName)
|
.eq(ModelConfigEntity::getModelName, modelName)
|
||||||
.last("LIMIT 1"));
|
.last("LIMIT 1"));
|
||||||
if (entity == null) {
|
if (entity == null) {
|
||||||
throw new MateClawException("模型不存在: " + providerId + "/" + modelName);
|
throw new MateClawException("err.llm.model_not_found", "模型不存在: " + providerId + "/" + modelName);
|
||||||
}
|
}
|
||||||
if (!Boolean.TRUE.equals(entity.getEnabled())) {
|
if (!Boolean.TRUE.equals(entity.getEnabled())) {
|
||||||
// Auto-enable when setting as default (e.g. local Ollama models)
|
// 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) {
|
private void validateModel(ModelConfigEntity entity, Long currentId) {
|
||||||
if (!StringUtils.hasText(entity.getName())) {
|
if (!StringUtils.hasText(entity.getName())) {
|
||||||
throw new MateClawException("模型名称不能为空");
|
throw new MateClawException("err.llm.name_required", "模型名称不能为空");
|
||||||
}
|
}
|
||||||
if (!StringUtils.hasText(entity.getProvider())) {
|
if (!StringUtils.hasText(entity.getProvider())) {
|
||||||
entity.setProvider("dashscope");
|
entity.setProvider("dashscope");
|
||||||
}
|
}
|
||||||
if (!StringUtils.hasText(entity.getModelName())) {
|
if (!StringUtils.hasText(entity.getModelName())) {
|
||||||
throw new MateClawException("模型标识不能为空");
|
throw new MateClawException("err.llm.id_required", "模型标识不能为空");
|
||||||
}
|
}
|
||||||
ModelConfigEntity duplicate = modelConfigMapper.selectOne(new LambdaQueryWrapper<ModelConfigEntity>()
|
ModelConfigEntity duplicate = modelConfigMapper.selectOne(new LambdaQueryWrapper<ModelConfigEntity>()
|
||||||
.eq(ModelConfigEntity::getProvider, entity.getProvider())
|
.eq(ModelConfigEntity::getProvider, entity.getProvider())
|
||||||
@ -232,7 +232,7 @@ public class ModelConfigService {
|
|||||||
.ne(currentId != null, ModelConfigEntity::getId, currentId)
|
.ne(currentId != null, ModelConfigEntity::getId, currentId)
|
||||||
.last("LIMIT 1"));
|
.last("LIMIT 1"));
|
||||||
if (duplicate != null) {
|
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) {
|
public DiscoverResult discoverModels(String providerId) {
|
||||||
ModelProviderEntity provider = modelProviderService.getProviderConfig(providerId);
|
ModelProviderEntity provider = modelProviderService.getProviderConfig(providerId);
|
||||||
if (!Boolean.TRUE.equals(provider.getSupportModelDiscovery())) {
|
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());
|
ModelProtocol protocol = ModelProtocol.fromChatModel(provider.getChatModel());
|
||||||
@ -66,7 +66,7 @@ public class ModelDiscoveryService {
|
|||||||
// 不支持模型发现(如智谱):用第一个已配置模型发送测试请求
|
// 不支持模型发现(如智谱):用第一个已配置模型发送测试请求
|
||||||
List<ModelConfigEntity> models = modelConfigService.listModelsByProvider(providerId);
|
List<ModelConfigEntity> models = modelConfigService.listModelsByProvider(providerId);
|
||||||
if (models.isEmpty()) {
|
if (models.isEmpty()) {
|
||||||
throw new MateClawException("该供应商没有已配置的模型,无法测试连接");
|
throw new MateClawException("err.llm.no_model_for_test", "该供应商没有已配置的模型,无法测试连接");
|
||||||
}
|
}
|
||||||
String testModelId = models.get(0).getModelName();
|
String testModelId = models.get(0).getModelName();
|
||||||
String response = sendTestPrompt(provider, protocol, testModelId);
|
String response = sendTestPrompt(provider, protocol, testModelId);
|
||||||
@ -122,14 +122,14 @@ public class ModelDiscoveryService {
|
|||||||
case DASHSCOPE_NATIVE -> fetchDashScopeModels(provider);
|
case DASHSCOPE_NATIVE -> fetchDashScopeModels(provider);
|
||||||
case GEMINI_NATIVE -> fetchGeminiModels(provider);
|
case GEMINI_NATIVE -> fetchGeminiModels(provider);
|
||||||
case ANTHROPIC_MESSAGES -> fetchAnthropicModels(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) {
|
private List<ModelInfoDTO> fetchOpenAiCompatibleModels(ModelProviderEntity provider) {
|
||||||
String baseUrl = normalizeBaseUrl(provider.getBaseUrl());
|
String baseUrl = normalizeBaseUrl(provider.getBaseUrl());
|
||||||
if (!StringUtils.hasText(baseUrl)) {
|
if (!StringUtils.hasText(baseUrl)) {
|
||||||
throw new MateClawException("Base URL 未配置");
|
throw new MateClawException("err.llm.base_url_missing", "Base URL 未配置");
|
||||||
}
|
}
|
||||||
String apiKey = provider.getApiKey();
|
String apiKey = provider.getApiKey();
|
||||||
|
|
||||||
@ -153,7 +153,7 @@ public class ModelDiscoveryService {
|
|||||||
private List<ModelInfoDTO> fetchDashScopeModels(ModelProviderEntity provider) {
|
private List<ModelInfoDTO> fetchDashScopeModels(ModelProviderEntity provider) {
|
||||||
String apiKey = provider.getApiKey();
|
String apiKey = provider.getApiKey();
|
||||||
if (!modelProviderService.hasUsableApiKey(apiKey)) {
|
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 端点
|
// DashScope 兼容模式暴露了 OpenAI 兼容的 /v1/models 端点
|
||||||
@ -170,7 +170,7 @@ public class ModelDiscoveryService {
|
|||||||
private List<ModelInfoDTO> fetchGeminiModels(ModelProviderEntity provider) {
|
private List<ModelInfoDTO> fetchGeminiModels(ModelProviderEntity provider) {
|
||||||
String apiKey = provider.getApiKey();
|
String apiKey = provider.getApiKey();
|
||||||
if (!modelProviderService.hasUsableApiKey(apiKey)) {
|
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()
|
RestClient client = RestClient.builder()
|
||||||
@ -188,7 +188,7 @@ public class ModelDiscoveryService {
|
|||||||
private List<ModelInfoDTO> fetchAnthropicModels(ModelProviderEntity provider) {
|
private List<ModelInfoDTO> fetchAnthropicModels(ModelProviderEntity provider) {
|
||||||
String apiKey = provider.getApiKey();
|
String apiKey = provider.getApiKey();
|
||||||
if (!modelProviderService.hasUsableApiKey(apiKey)) {
|
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())
|
String baseUrl = StringUtils.hasText(provider.getBaseUrl())
|
||||||
@ -214,14 +214,14 @@ public class ModelDiscoveryService {
|
|||||||
case DASHSCOPE_NATIVE -> sendDashScopeTestPrompt(provider, modelId);
|
case DASHSCOPE_NATIVE -> sendDashScopeTestPrompt(provider, modelId);
|
||||||
case GEMINI_NATIVE -> sendGeminiTestPrompt(provider, modelId);
|
case GEMINI_NATIVE -> sendGeminiTestPrompt(provider, modelId);
|
||||||
case ANTHROPIC_MESSAGES -> sendAnthropicTestPrompt(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) {
|
private String sendOpenAiTestPrompt(ModelProviderEntity provider, String modelId) {
|
||||||
String baseUrl = normalizeBaseUrl(provider.getBaseUrl());
|
String baseUrl = normalizeBaseUrl(provider.getBaseUrl());
|
||||||
if (!StringUtils.hasText(baseUrl)) {
|
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(
|
Map<String, Object> requestBody = Map.of(
|
||||||
@ -255,7 +255,7 @@ public class ModelDiscoveryService {
|
|||||||
private String sendDashScopeTestPrompt(ModelProviderEntity provider, String modelId) {
|
private String sendDashScopeTestPrompt(ModelProviderEntity provider, String modelId) {
|
||||||
String apiKey = provider.getApiKey();
|
String apiKey = provider.getApiKey();
|
||||||
if (!modelProviderService.hasUsableApiKey(apiKey)) {
|
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(
|
Map<String, Object> requestBody = Map.of(
|
||||||
@ -281,7 +281,7 @@ public class ModelDiscoveryService {
|
|||||||
private String sendGeminiTestPrompt(ModelProviderEntity provider, String modelId) {
|
private String sendGeminiTestPrompt(ModelProviderEntity provider, String modelId) {
|
||||||
String apiKey = provider.getApiKey();
|
String apiKey = provider.getApiKey();
|
||||||
if (!modelProviderService.hasUsableApiKey(apiKey)) {
|
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(
|
Map<String, Object> requestBody = Map.of(
|
||||||
@ -306,7 +306,7 @@ public class ModelDiscoveryService {
|
|||||||
private String sendAnthropicTestPrompt(ModelProviderEntity provider, String modelId) {
|
private String sendAnthropicTestPrompt(ModelProviderEntity provider, String modelId) {
|
||||||
String apiKey = provider.getApiKey();
|
String apiKey = provider.getApiKey();
|
||||||
if (!modelProviderService.hasUsableApiKey(apiKey)) {
|
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())
|
String baseUrl = StringUtils.hasText(provider.getBaseUrl())
|
||||||
|
|||||||
@ -51,10 +51,10 @@ public class ModelProviderService {
|
|||||||
|
|
||||||
public ProviderInfoDTO createCustomProvider(CreateCustomProviderRequest request) {
|
public ProviderInfoDTO createCustomProvider(CreateCustomProviderRequest request) {
|
||||||
if (!StringUtils.hasText(request.getId()) || !StringUtils.hasText(request.getName())) {
|
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) {
|
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();
|
ModelProviderEntity provider = new ModelProviderEntity();
|
||||||
provider.setProviderId(request.getId());
|
provider.setProviderId(request.getId());
|
||||||
@ -83,7 +83,7 @@ public class ModelProviderService {
|
|||||||
public void deleteCustomProvider(String providerId) {
|
public void deleteCustomProvider(String providerId) {
|
||||||
ModelProviderEntity provider = getProvider(providerId);
|
ModelProviderEntity provider = getProvider(providerId);
|
||||||
if (!Boolean.TRUE.equals(provider.getIsCustom())) {
|
if (!Boolean.TRUE.equals(provider.getIsCustom())) {
|
||||||
throw new MateClawException("内置 Provider 不支持删除");
|
throw new MateClawException("err.llm.provider_builtin_readonly", "内置 Provider 不支持删除");
|
||||||
}
|
}
|
||||||
modelConfigService.deleteModelsByProvider(providerId);
|
modelConfigService.deleteModelsByProvider(providerId);
|
||||||
modelProviderMapper.deleteById(providerId);
|
modelProviderMapper.deleteById(providerId);
|
||||||
@ -159,7 +159,7 @@ public class ModelProviderService {
|
|||||||
private ModelProviderEntity getProvider(String providerId) {
|
private ModelProviderEntity getProvider(String providerId) {
|
||||||
ModelProviderEntity provider = modelProviderMapper.selectById(providerId);
|
ModelProviderEntity provider = modelProviderMapper.selectById(providerId);
|
||||||
if (provider == null) {
|
if (provider == null) {
|
||||||
throw new MateClawException("Provider 不存在: " + providerId);
|
throw new MateClawException("err.llm.provider_not_found", "Provider 不存在: " + providerId);
|
||||||
}
|
}
|
||||||
return provider;
|
return provider;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -106,27 +106,27 @@ public class DefaultToolGuard implements ToolGuard {
|
|||||||
new DangerousPattern(
|
new DangerousPattern(
|
||||||
"rm\\s+-(rf|fr)\\s+/\\s*$",
|
"rm\\s+-(rf|fr)\\s+/\\s*$",
|
||||||
"filesystem_destroy",
|
"filesystem_destroy",
|
||||||
"递归强制删除根目录"),
|
"Recursive force delete root directory"),
|
||||||
new DangerousPattern(
|
new DangerousPattern(
|
||||||
"mkfs\\b",
|
"mkfs\\b",
|
||||||
"filesystem_destroy",
|
"filesystem_destroy",
|
||||||
"文件系统格式化命令"),
|
"Filesystem formatting command"),
|
||||||
new DangerousPattern(
|
new DangerousPattern(
|
||||||
"dd\\s+if=.+of=/dev/",
|
"dd\\s+if=.+of=/dev/",
|
||||||
"filesystem_destroy",
|
"filesystem_destroy",
|
||||||
"直接磁盘写入操作"),
|
"Direct disk write operation"),
|
||||||
new DangerousPattern(
|
new DangerousPattern(
|
||||||
"\\bkill\\s+-9\\s+1\\b",
|
"\\bkill\\s+-9\\s+1\\b",
|
||||||
"system_danger",
|
"system_danger",
|
||||||
"杀死 init/systemd 进程"),
|
"Kill init/systemd process"),
|
||||||
new DangerousPattern(
|
new DangerousPattern(
|
||||||
"curl.*\\|\\s*(sh|bash|zsh)",
|
"curl.*\\|\\s*(sh|bash|zsh)",
|
||||||
"code_injection",
|
"code_injection",
|
||||||
"管道下载内容到 Shell 执行"),
|
"Pipe download to shell execution"),
|
||||||
new DangerousPattern(
|
new DangerousPattern(
|
||||||
"wget.*\\|\\s*(sh|bash|zsh)",
|
"wget.*\\|\\s*(sh|bash|zsh)",
|
||||||
"code_injection",
|
"code_injection",
|
||||||
"管道下载内容到 Shell 执行")
|
"Pipe download to shell execution")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,69 +139,69 @@ public class DefaultToolGuard implements ToolGuard {
|
|||||||
new DangerousPattern(
|
new DangerousPattern(
|
||||||
"rm\\s+-(rf|fr)",
|
"rm\\s+-(rf|fr)",
|
||||||
"filesystem_destroy",
|
"filesystem_destroy",
|
||||||
"递归强制删除操作"),
|
"Recursive force delete"),
|
||||||
new DangerousPattern(
|
new DangerousPattern(
|
||||||
"rm\\s+/",
|
"rm\\s+/",
|
||||||
"filesystem_destroy",
|
"filesystem_destroy",
|
||||||
"从根路径删除文件"),
|
"Delete from root path"),
|
||||||
new DangerousPattern(
|
new DangerousPattern(
|
||||||
"rmdir\\s+/",
|
"rmdir\\s+/",
|
||||||
"filesystem_destroy",
|
"filesystem_destroy",
|
||||||
"从根路径删除目录"),
|
"Delete directory from root path"),
|
||||||
|
|
||||||
// SQL
|
// SQL
|
||||||
new DangerousPattern(
|
new DangerousPattern(
|
||||||
"DROP\\s+(TABLE|DATABASE|INDEX|VIEW|SCHEMA)",
|
"DROP\\s+(TABLE|DATABASE|INDEX|VIEW|SCHEMA)",
|
||||||
"sql_destroy",
|
"sql_destroy",
|
||||||
"SQL DROP 语句"),
|
"SQL DROP statement"),
|
||||||
new DangerousPattern(
|
new DangerousPattern(
|
||||||
"TRUNCATE\\s+TABLE",
|
"TRUNCATE\\s+TABLE",
|
||||||
"sql_destroy",
|
"sql_destroy",
|
||||||
"SQL TRUNCATE TABLE 语句"),
|
"SQL TRUNCATE TABLE statement"),
|
||||||
new DangerousPattern(
|
new DangerousPattern(
|
||||||
"DELETE\\s+FROM\\s+\\w+\\s*;",
|
"DELETE\\s+FROM\\s+\\w+\\s*;",
|
||||||
"sql_destroy",
|
"sql_destroy",
|
||||||
"无条件 DELETE(缺少 WHERE 子句)"),
|
"Unconditional DELETE (missing WHERE clause)"),
|
||||||
new DangerousPattern(
|
new DangerousPattern(
|
||||||
"ALTER\\s+TABLE\\s+\\w+\\s+DROP",
|
"ALTER\\s+TABLE\\s+\\w+\\s+DROP",
|
||||||
"sql_destroy",
|
"sql_destroy",
|
||||||
"ALTER TABLE DROP 操作"),
|
"ALTER TABLE DROP operation"),
|
||||||
|
|
||||||
// 系统
|
// System
|
||||||
new DangerousPattern(
|
new DangerousPattern(
|
||||||
"\\bshutdown\\b",
|
"\\bshutdown\\b",
|
||||||
"system_danger",
|
"system_danger",
|
||||||
"系统关机命令"),
|
"System shutdown command"),
|
||||||
new DangerousPattern(
|
new DangerousPattern(
|
||||||
"\\breboot\\b",
|
"\\breboot\\b",
|
||||||
"system_danger",
|
"system_danger",
|
||||||
"系统重启命令"),
|
"System reboot command"),
|
||||||
new DangerousPattern(
|
new DangerousPattern(
|
||||||
"chmod\\s+777",
|
"chmod\\s+777",
|
||||||
"system_danger",
|
"system_danger",
|
||||||
"过度宽松的权限设置"),
|
"Overly permissive file permissions"),
|
||||||
|
|
||||||
// 代码注入
|
// Code injection
|
||||||
new DangerousPattern(
|
new DangerousPattern(
|
||||||
"eval\\s*\\(",
|
"eval\\s*\\(",
|
||||||
"code_injection",
|
"code_injection",
|
||||||
"动态代码执行(eval)"),
|
"Dynamic code execution (eval)"),
|
||||||
|
|
||||||
// 凭据
|
// Credentials
|
||||||
new DangerousPattern(
|
new DangerousPattern(
|
||||||
"(password|secret|api[_-]?key|token)\\s*=\\s*['\"]?\\S{8,}",
|
"(password|secret|api[_-]?key|token)\\s*=\\s*['\"]?\\S{8,}",
|
||||||
"credential_exposure",
|
"credential_exposure",
|
||||||
"可能的凭据信息暴露"),
|
"Potential credential exposure"),
|
||||||
|
|
||||||
// Git
|
// Git
|
||||||
new DangerousPattern(
|
new DangerousPattern(
|
||||||
"git\\s+push\\s+.*--force",
|
"git\\s+push\\s+.*--force",
|
||||||
"git_danger",
|
"git_danger",
|
||||||
"Git 强制推送"),
|
"Git force push"),
|
||||||
new DangerousPattern(
|
new DangerousPattern(
|
||||||
"git\\s+reset\\s+--hard",
|
"git\\s+reset\\s+--hard",
|
||||||
"git_danger",
|
"git_danger",
|
||||||
"Git 硬重置")
|
"Git hard reset")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -355,7 +355,7 @@ public class WikiController {
|
|||||||
}
|
}
|
||||||
long wsId = headerWorkspaceId != null ? headerWorkspaceId : 1L;
|
long wsId = headerWorkspaceId != null ? headerWorkspaceId : 1L;
|
||||||
if (kb.getWorkspaceId() != null && !kb.getWorkspaceId().equals(wsId)) {
|
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.expression_required=Cron \u8868\u8fbe\u5f0f\u4e0d\u80fd\u4e3a\u7a7a
|
||||||
err.cron.trigger_required=\u89e6\u53d1\u6d88\u606f\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
|
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 ---
|
# --- 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}
|
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.expression_required=Cron expression cannot be empty
|
||||||
err.cron.trigger_required=Trigger message cannot be empty
|
err.cron.trigger_required=Trigger message cannot be empty
|
||||||
err.cron.target_required=Execution target 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 ---
|
# --- RuntimeContext ---
|
||||||
context.current_time=[system-context] Current time: {0} {1} (Asia/Shanghai)
|
context.current_time=[system-context] Current time: {0} {1} (Asia/Shanghai)
|
||||||
|
|||||||
@ -1395,6 +1395,67 @@ export default {
|
|||||||
toggleFailed: 'Failed to toggle channel status',
|
toggleFailed: 'Failed to toggle channel status',
|
||||||
invalidJson: 'Invalid config JSON format',
|
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: {
|
skills: {
|
||||||
title: 'Skills',
|
title: 'Skills',
|
||||||
|
|||||||
@ -1405,6 +1405,67 @@ export default {
|
|||||||
toggleFailed: '切换渠道状态失败',
|
toggleFailed: '切换渠道状态失败',
|
||||||
invalidJson: '配置 JSON 格式不正确',
|
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: {
|
skills: {
|
||||||
title: '技能管理',
|
title: '技能管理',
|
||||||
|
|||||||
@ -543,69 +543,69 @@ interface WebhookGuideInfo {
|
|||||||
steps: string[]
|
steps: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const WEBHOOK_GUIDES: Record<string, WebhookGuideInfo> = {
|
const WEBHOOK_GUIDES = computed<Record<string, WebhookGuideInfo>>(() => ({
|
||||||
dingtalk: {
|
dingtalk: {
|
||||||
steps: [
|
steps: [
|
||||||
'在 <a href="https://open-dev.dingtalk.com/" target="_blank" rel="noopener">钉钉开放平台</a> 创建机器人,填入 <b>AppKey</b> 和 <b>AppSecret</b>',
|
t('channels.guide.dingtalk.step1'),
|
||||||
'Stream 模式(推荐):消息接收模式选「Stream 模式」,无需公网 IP 即可使用',
|
t('channels.guide.dingtalk.step2'),
|
||||||
'Webhook 模式:消息接收模式选「HTTP 模式」,将 Webhook URL 填入回调地址',
|
t('channels.guide.dingtalk.step3'),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
feishu: {
|
feishu: {
|
||||||
steps: [
|
steps: [
|
||||||
'前往 <a href="https://open.feishu.cn/" target="_blank" rel="noopener">飞书开放平台</a> 创建企业自建应用',
|
t('channels.guide.feishu.step1'),
|
||||||
'将应用的 <b>App ID</b> 和 <b>App Secret</b> 填入下方配置',
|
t('channels.guide.feishu.step2'),
|
||||||
'<b>Webhook 模式</b>:在「事件订阅」配置中将请求地址设置为上方 Webhook URL,并订阅 <code>im.message.receive_v1</code> 事件',
|
t('channels.guide.feishu.step3'),
|
||||||
'<b>WebSocket 模式</b>:无需公网地址,在下方接入模式中选择「WebSocket(长连接)」即可。需在飞书后台「事件订阅」中选择长连接方式',
|
t('channels.guide.feishu.step4'),
|
||||||
'如需昵称显示,请在「权限管理」中申请 <code>contact:user.base:readonly</code> 权限',
|
t('channels.guide.feishu.step5'),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
telegram: {
|
telegram: {
|
||||||
steps: [
|
steps: [
|
||||||
'在 Telegram 中搜索 <a href="https://t.me/BotFather" target="_blank" rel="noopener">@BotFather</a>,发送 <code>/newbot</code> 创建 Bot',
|
t('channels.guide.telegram.step1'),
|
||||||
'将 BotFather 返回的 <b>Bot Token</b> 填入下方配置',
|
t('channels.guide.telegram.step2'),
|
||||||
'<b>Long-Polling 模式</b>(推荐):启动后自动通过 <code>getUpdates</code> 轮询接收消息,<b>无需公网 IP</b>',
|
t('channels.guide.telegram.step3'),
|
||||||
'<b>Webhook 模式</b>:需要公网可访问的回调地址,切换接入模式后填入 Webhook URL',
|
t('channels.guide.telegram.step4'),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
discord: {
|
discord: {
|
||||||
steps: [
|
steps: [
|
||||||
'前往 <a href="https://discord.com/developers/applications" target="_blank" rel="noopener">Discord Developer Portal</a> 创建 Application',
|
t('channels.guide.discord.step1'),
|
||||||
'在 Bot 页面创建 Bot 并复制 <b>Bot Token</b> 填入下方配置',
|
t('channels.guide.discord.step2'),
|
||||||
'在 Bot 页面开启 <b>MESSAGE CONTENT Intent</b>(Privileged Gateway Intents 区域)',
|
t('channels.guide.discord.step3'),
|
||||||
'在 OAuth2 → URL Generator 中勾选 <b>bot</b> scope 和 <b>Send Messages / Read Message History</b> 权限,生成邀请链接将 Bot 添加到目标服务器',
|
t('channels.guide.discord.step4'),
|
||||||
'启动后通过 <b>Gateway WebSocket</b> 自动接收消息,<b>无需公网 IP 和回调 URL</b>',
|
t('channels.guide.discord.step5'),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
wecom: {
|
wecom: {
|
||||||
steps: [
|
steps: [
|
||||||
'前往 <a href="https://work.weixin.qq.com/wework_admin/frame" target="_blank" rel="noopener">企业微信管理后台</a>,进入「应用管理 → 智能机器人」创建一个新的智能机器人',
|
t('channels.guide.wecom.step1'),
|
||||||
'在机器人配置中选择「<b>API 模式 → 配置长连接</b>」',
|
t('channels.guide.wecom.step2'),
|
||||||
'记录机器人的 <b>Bot ID</b> 和 <b>Secret</b>,填入下方配置',
|
t('channels.guide.wecom.step3'),
|
||||||
'启动渠道后即可在企业微信中扫码添加机器人对话,<b>无需公网 IP 和回调 URL</b>',
|
t('channels.guide.wecom.step4'),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
weixin: {
|
weixin: {
|
||||||
steps: [
|
steps: [
|
||||||
'确保已获得微信 <b>iLink Bot</b> 内测资格(目前为受邀内测阶段)',
|
t('channels.guide.weixin.step1'),
|
||||||
'点击下方「获取登录二维码」按钮,用微信扫码登录',
|
t('channels.guide.weixin.step2'),
|
||||||
'扫码成功后系统自动获取 <b>bot_token</b> 并填入配置',
|
t('channels.guide.weixin.step3'),
|
||||||
'启动渠道后通过 <b>HTTP 长轮询</b>自动接收消息,<b>无需公网 IP 和回调 URL</b>',
|
t('channels.guide.weixin.step4'),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
qq: {
|
qq: {
|
||||||
steps: [
|
steps: [
|
||||||
'前往 <a href="https://q.qq.com/" target="_blank" rel="noopener">QQ 开放平台</a> 创建机器人应用',
|
t('channels.guide.qq.step1'),
|
||||||
'在应用管理页面获取 <b>AppID</b> 和 <b>AppSecret</b>,填入下方配置',
|
t('channels.guide.qq.step2'),
|
||||||
'在「功能配置 → 消息订阅」中开启需要的消息类型(C2C 消息、群聊 @消息、频道消息等)',
|
t('channels.guide.qq.step3'),
|
||||||
'启动渠道后通过 <b>WebSocket 长连接</b>自动接收消息,<b>无需公网 IP 和回调 URL</b>',
|
t('channels.guide.qq.step4'),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}
|
}))
|
||||||
|
|
||||||
/** 当前渠道是否有接入引导 */
|
/** 当前渠道是否有接入引导 */
|
||||||
const webhookGuide = computed<WebhookGuideInfo | null>(() => {
|
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 模式不需要) */
|
/** 当前渠道是否需要 Webhook URL(WebSocket / Stream / Long-Polling 模式不需要) */
|
||||||
@ -823,37 +823,32 @@ const feishuPermissionUrl = computed(() => {
|
|||||||
const feishuRequiredPermissions = computed(() => {
|
const feishuRequiredPermissions = computed(() => {
|
||||||
const perms: { scope: string; desc: string; reason: string }[] = []
|
const perms: { scope: string; desc: string; reason: string }[] = []
|
||||||
|
|
||||||
// 基础权限(始终需要)
|
|
||||||
perms.push(
|
perms.push(
|
||||||
{ scope: 'im:message', desc: '获取与发送单聊、群组消息', reason: '基础:收发消息' },
|
{ scope: 'im:message', desc: t('channels.feishu.perm.message'), reason: t('channels.feishu.perm.messageReason') },
|
||||||
{ scope: 'im:message.receive_v1', desc: '接收消息事件', reason: '基础:接收用户消息' },
|
{ scope: 'im:message.receive_v1', desc: t('channels.feishu.perm.receive'), reason: t('channels.feishu.perm.receiveReason') },
|
||||||
)
|
)
|
||||||
|
|
||||||
// WebSocket 模式需要的权限
|
|
||||||
if (channelConfig.value?.connection_mode === 'websocket') {
|
if (channelConfig.value?.connection_mode === 'websocket') {
|
||||||
perms.push(
|
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) {
|
if (channelConfig.value?.enable_reaction !== false) {
|
||||||
perms.push(
|
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) {
|
if (channelConfig.value?.enable_nickname_cache !== false) {
|
||||||
perms.push(
|
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) {
|
if (channelConfig.value?.media_download_enabled) {
|
||||||
perms.push(
|
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