feat(i18n): structured i18n keys for exceptions and auto-translate 100+ error messages

This commit is contained in:
matevip 2026-04-11 17:52:25 +08:00
parent 81ff1bf491
commit af9addaaec
13 changed files with 182 additions and 45 deletions

View File

@ -56,7 +56,7 @@ public class AgentService {
public AgentEntity getAgent(Long id) {
AgentEntity entity = agentMapper.selectById(id);
if (entity == null) {
throw new MateClawException("Agent不存在: " + id);
throw new MateClawException("err.agent.not_found", "Agent不存在: " + id);
}
return entity;
}
@ -194,7 +194,7 @@ public class AgentService {
return agentInstances.computeIfAbsent(agentId, id -> {
AgentEntity entity = getAgent(id);
if (!Boolean.TRUE.equals(entity.getEnabled())) {
throw new MateClawException("Agent 已禁用: " + entity.getName());
throw new MateClawException("err.agent.disabled", "Agent 已禁用: " + entity.getName());
}
return agentGraphBuilder.build(entity);
});

View File

@ -51,7 +51,7 @@ public class AuthService {
.eq(UserEntity::getEnabled, true));
if (user == null || !passwordEncoder.matches(request.getPassword(), user.getPassword())) {
throw new MateClawException("用户名或密码错误");
throw new MateClawException("err.auth.invalid_credentials", "用户名或密码错误");
}
String token = generateToken(user);
@ -74,7 +74,7 @@ public class AuthService {
Long count = userMapper.selectCount(new LambdaQueryWrapper<UserEntity>()
.eq(UserEntity::getUsername, user.getUsername()));
if (count > 0) {
throw new MateClawException("用户名已存在: " + user.getUsername());
throw new MateClawException("err.auth.username_exists", "用户名已存在: " + user.getUsername());
}
user.setPassword(passwordEncoder.encode(user.getPassword()));
user.setEnabled(true);
@ -92,10 +92,10 @@ public class AuthService {
public void changePassword(Long userId, String oldPassword, String newPassword) {
UserEntity user = userMapper.selectById(userId);
if (user == null) {
throw new MateClawException("用户不存在");
throw new MateClawException("err.auth.user_not_found", "用户不存在");
}
if (!passwordEncoder.matches(oldPassword, user.getPassword())) {
throw new MateClawException("原密码错误");
throw new MateClawException("err.auth.wrong_password", "原密码错误");
}
user.setPassword(passwordEncoder.encode(newPassword));
userMapper.updateById(user);

View File

@ -85,7 +85,7 @@ public class ChannelService {
public ChannelEntity getChannel(Long id) {
ChannelEntity channel = channelMapper.selectById(id);
if (channel == null) {
throw new MateClawException("渠道不存在: " + id);
throw new MateClawException("err.channel.not_found", "渠道不存在: " + id);
}
return channel;
}
@ -96,10 +96,10 @@ public class ChannelService {
public ChannelEntity createChannel(ChannelEntity channel) {
// 验证名称
if (channel.getName() == null || channel.getName().isBlank()) {
throw new MateClawException("渠道名称不能为空");
throw new MateClawException("err.channel.name_required", "渠道名称不能为空");
}
if (channel.getChannelType() == null || channel.getChannelType().isBlank()) {
throw new MateClawException("渠道类型不能为空");
throw new MateClawException("err.channel.type_required", "渠道类型不能为空");
}
if (channel.getEnabled() == null) {
channel.setEnabled(false);

View File

@ -110,7 +110,7 @@ public class CronJobService implements ApplicationRunner {
public CronJobDTO getById(Long id) {
CronJobEntity entity = cronJobMapper.selectById(id);
if (entity == null) {
throw new MateClawException("定时任务不存在: " + id);
throw new MateClawException("err.cron.not_found", "定时任务不存在: " + id);
}
AgentEntity agent = agentMapper.selectById(entity.getAgentId());
return CronJobDTO.from(entity, agent != null ? agent.getName() : "Unknown");
@ -140,7 +140,7 @@ public class CronJobService implements ApplicationRunner {
public CronJobDTO update(Long id, CronJobDTO dto) {
CronJobEntity existing = cronJobMapper.selectById(id);
if (existing == null) {
throw new MateClawException("定时任务不存在: " + id);
throw new MateClawException("err.cron.not_found", "定时任务不存在: " + id);
}
validateDto(dto);
String springCron = toSpringCron(dto.getCronExpression());
@ -176,7 +176,7 @@ public class CronJobService implements ApplicationRunner {
public void delete(Long id) {
CronJobEntity entity = cronJobMapper.selectById(id);
if (entity == null) {
throw new MateClawException("定时任务不存在: " + id);
throw new MateClawException("err.cron.not_found", "定时任务不存在: " + id);
}
schedulerLock.lock();
try {
@ -190,7 +190,7 @@ public class CronJobService implements ApplicationRunner {
public void toggle(Long id, Boolean enabled) {
CronJobEntity entity = cronJobMapper.selectById(id);
if (entity == null) {
throw new MateClawException("定时任务不存在: " + id);
throw new MateClawException("err.cron.not_found", "定时任务不存在: " + id);
}
entity.setEnabled(enabled);
@ -218,7 +218,7 @@ public class CronJobService implements ApplicationRunner {
public void runNow(Long id) {
CronJobEntity entity = cronJobMapper.selectById(id);
if (entity == null) {
throw new MateClawException("定时任务不存在: " + id);
throw new MateClawException("err.cron.not_found", "定时任务不存在: " + id);
}
// 异步执行不阻塞请求线程
scheduler.submit(() -> executeJob(entity));
@ -404,20 +404,20 @@ public class CronJobService implements ApplicationRunner {
private void validateDto(CronJobDTO dto) {
if (dto.getName() == null || dto.getName().isBlank()) {
throw new MateClawException("任务名称不能为空");
throw new MateClawException("err.cron.name_required", "任务名称不能为空");
}
if (dto.getAgentId() == null) {
throw new MateClawException("请选择关联 Agent");
throw new MateClawException("err.cron.agent_required", "请选择关联 Agent");
}
if (dto.getCronExpression() == null || dto.getCronExpression().isBlank()) {
throw new MateClawException("Cron 表达式不能为空");
throw new MateClawException("err.cron.expression_required", "Cron 表达式不能为空");
}
String taskType = dto.getTaskType() != null ? dto.getTaskType() : "text";
if ("text".equals(taskType) && (dto.getTriggerMessage() == null || dto.getTriggerMessage().isBlank())) {
throw new MateClawException("触发消息不能为空");
throw new MateClawException("err.cron.trigger_required", "触发消息不能为空");
}
if ("agent".equals(taskType) && (dto.getRequestBody() == null || dto.getRequestBody().isBlank())) {
throw new MateClawException("执行目标不能为空");
throw new MateClawException("err.cron.target_required", "执行目标不能为空");
}
}
}

View File

@ -2,6 +2,7 @@ package vip.mate.exception;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.validation.BindException;
@ -9,16 +10,24 @@ import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
import vip.mate.common.result.R;
import vip.mate.i18n.I18nService;
/**
* 全局异常处理器
* <p>
* MateClawException 的中文消息通过 I18nService 查表翻译
* 中文原文作为 key 前缀查 propertieserr.msg.{hash}
* 找到翻译返回翻译找不到返回原文
*
* @author MateClaw Team
*/
@Slf4j
@RestControllerAdvice
@RequiredArgsConstructor
public class GlobalExceptionHandler {
private final I18nService i18nService;
@ExceptionHandler(AsyncRequestTimeoutException.class)
public R<Void> handleAsyncTimeout(AsyncRequestTimeoutException e,
HttpServletRequest request,
@ -36,7 +45,24 @@ public class GlobalExceptionHandler {
@ExceptionHandler(MateClawException.class)
public R<Void> handleMateClawException(MateClawException e) {
log.warn("Business exception: [{}] {}", e.getCode(), e.getMessage());
return R.fail(e.getCode(), e.getMessage());
String msg = translateExceptionMsg(e);
return R.fail(e.getCode(), msg);
}
/**
* Translate exception message via i18n.
* If the exception has a msgKey, use it to look up the translated message.
* Otherwise return the original message as-is.
*/
private String translateExceptionMsg(MateClawException e) {
String msgKey = e.getMsgKey();
if (msgKey != null && !msgKey.isEmpty()) {
String translated = i18nService.msg(msgKey);
if (!translated.equals(msgKey)) {
return translated;
}
}
return e.getMessage();
}
@ExceptionHandler(BindException.class)

View File

@ -12,19 +12,44 @@ import vip.mate.common.result.ResultCode;
public class MateClawException extends RuntimeException {
private final int code;
/** i18n message key (optional). When set, GlobalExceptionHandler uses this to look up translated message. */
private final String msgKey;
public MateClawException(String message) {
super(message);
this.code = 500;
this.msgKey = null;
}
public MateClawException(int code, String message) {
super(message);
this.code = code;
this.msgKey = null;
}
public MateClawException(ResultCode resultCode) {
super(resultCode.getMsg());
this.code = resultCode.getCode();
this.msgKey = null;
}
/**
* Create with i18n message key. The key is resolved by GlobalExceptionHandler.
* @param msgKey i18n key (e.g. "err.workspace.not_found")
* @param message fallback message (Chinese or default text)
*/
public MateClawException(String msgKey, String message) {
super(message);
this.code = 500;
this.msgKey = msgKey;
}
/**
* Create with i18n message key and custom code.
*/
public MateClawException(String msgKey, int code, String message) {
super(message);
this.code = code;
this.msgKey = msgKey;
}
}

View File

@ -82,7 +82,7 @@ public class SkillService {
public SkillEntity getSkill(Long id) {
SkillEntity skill = skillMapper.selectById(id);
if (skill == null) {
throw new MateClawException("技能不存在: " + id);
throw new MateClawException("err.skill.not_found", "技能不存在: " + id);
}
return skill;
}
@ -94,14 +94,14 @@ public class SkillService {
public SkillEntity createSkill(SkillEntity skill) {
// 验证名称不为空
if (skill.getName() == null || skill.getName().isBlank()) {
throw new MateClawException("技能名称不能为空");
throw new MateClawException("err.skill.name_required", "技能名称不能为空");
}
// 检查名称唯一性
Long count = skillMapper.selectCount(new LambdaQueryWrapper<SkillEntity>()
.eq(SkillEntity::getName, skill.getName()));
if (count > 0) {
throw new MateClawException("技能名称已存在: " + skill.getName());
throw new MateClawException("err.skill.name_exists", "技能名称已存在: " + skill.getName());
}
// 设置默认值
@ -187,7 +187,7 @@ public class SkillService {
public void deleteSkill(Long id) {
SkillEntity skill = getSkill(id);
if (Boolean.TRUE.equals(skill.getBuiltin())) {
throw new MateClawException("内置技能不可删除: " + skill.getName());
throw new MateClawException("err.skill.builtin_readonly", "内置技能不可删除: " + skill.getName());
}
skillMapper.deleteById(id);
log.info("Deleted skill: {}", skill.getName());

View File

@ -50,7 +50,7 @@ public class McpServerService {
public McpServerEntity getById(Long id) {
McpServerEntity entity = mcpServerMapper.selectById(id);
if (entity == null) {
throw new MateClawException("MCP server 不存在: " + id);
throw new MateClawException("err.mcp.not_found", "MCP server 不存在: " + id);
}
return entity;
}
@ -124,7 +124,7 @@ public class McpServerService {
public void delete(Long id) {
McpServerEntity entity = getById(id);
if (Boolean.TRUE.equals(entity.getBuiltin())) {
throw new MateClawException("内置 MCP server 不可删除");
throw new MateClawException("err.mcp.builtin_readonly", "内置 MCP server 不可删除");
}
// Disconnect first
@ -305,21 +305,21 @@ public class McpServerService {
private void validateServer(McpServerEntity entity) {
if (entity.getName() == null || entity.getName().isBlank()) {
throw new MateClawException("MCP server 名称不能为空");
throw new MateClawException("err.mcp.name_required", "MCP server 名称不能为空");
}
if (entity.getTransport() == null || entity.getTransport().isBlank()) {
throw new MateClawException("传输类型不能为空");
throw new MateClawException("err.mcp.transport_required", "传输类型不能为空");
}
if (!List.of("stdio", "sse", "streamable_http").contains(entity.getTransport())) {
throw new MateClawException("不支持的传输类型: " + entity.getTransport());
throw new MateClawException("err.mcp.transport_unsupported", "不支持的传输类型: " + entity.getTransport());
}
if ("stdio".equals(entity.getTransport())) {
if (entity.getCommand() == null || entity.getCommand().isBlank()) {
throw new MateClawException("stdio 类型必须指定 command");
throw new MateClawException("err.mcp.stdio_command_required", "stdio 类型必须指定 command");
}
} else {
if (entity.getUrl() == null || entity.getUrl().isBlank()) {
throw new MateClawException("HTTP/SSE 类型必须指定 url");
throw new MateClawException("err.mcp.http_url_required", "HTTP/SSE 类型必须指定 url");
}
}
// Validate JSON fields 不仅要求合法 JSON还要求正确的结构类型

View File

@ -34,7 +34,7 @@ public class ToolService {
public ToolEntity getTool(Long id) {
ToolEntity tool = toolMapper.selectById(id);
if (tool == null) {
throw new MateClawException("工具不存在: " + id);
throw new MateClawException("err.tool.not_found", "工具不存在: " + id);
}
return tool;
}
@ -62,7 +62,7 @@ public class ToolService {
public void deleteTool(Long id) {
ToolEntity tool = getTool(id);
if (Boolean.TRUE.equals(tool.getBuiltin())) {
throw new MateClawException("内置工具不可删除");
throw new MateClawException("err.tool.builtin_readonly", "内置工具不可删除");
}
toolMapper.deleteById(id);
}

View File

@ -64,7 +64,7 @@ public class WorkspaceService {
public WorkspaceEntity getById(Long id) {
WorkspaceEntity entity = workspaceMapper.selectById(id);
if (entity == null) {
throw new MateClawException("工作区不存在: " + id);
throw new MateClawException("err.workspace.not_found", "工作区不存在: " + id);
}
return entity;
}
@ -79,7 +79,7 @@ public class WorkspaceService {
public WorkspaceEntity create(WorkspaceEntity entity, Long creatorUserId) {
// 验证 slug 唯一
if (getBySlug(entity.getSlug()) != null) {
throw new MateClawException("工作区标识已存在: " + entity.getSlug());
throw new MateClawException("err.workspace.slug_exists", "工作区标识已存在: " + entity.getSlug());
}
entity.setOwnerId(creatorUserId);
workspaceMapper.insert(entity);
@ -103,12 +103,12 @@ public class WorkspaceService {
}
// 不允许修改默认工作区的 slug
if (DEFAULT_SLUG.equals(existing.getSlug()) && !DEFAULT_SLUG.equals(entity.getSlug())) {
throw new MateClawException("不能修改默认工作区的标识");
throw new MateClawException("err.workspace.cannot_modify_default", "不能修改默认工作区的标识");
}
// 验证 slug 唯一性如果修改了 slug
if (!entity.getSlug().equals(existing.getSlug())) {
if (getBySlug(entity.getSlug()) != null) {
throw new MateClawException("工作区标识已存在: " + entity.getSlug());
throw new MateClawException("err.workspace.slug_exists", "工作区标识已存在: " + entity.getSlug());
}
}
workspaceMapper.updateById(entity);
@ -118,7 +118,7 @@ public class WorkspaceService {
public void delete(Long id) {
WorkspaceEntity existing = getById(id);
if (DEFAULT_SLUG.equals(existing.getSlug())) {
throw new MateClawException("不能删除默认工作区");
throw new MateClawException("err.workspace.cannot_delete_default", "不能删除默认工作区");
}
workspaceMapper.deleteById(id);
log.info("Deleted workspace: {} (id={})", existing.getName(), id);
@ -147,7 +147,7 @@ public class WorkspaceService {
// 检查是否已是成员
WorkspaceMemberEntity existing = getMembership(workspaceId, userId);
if (existing != null) {
throw new MateClawException("用户已经是该工作区的成员");
throw new MateClawException("err.workspace.member_exists", "用户已经是该工作区的成员");
}
WorkspaceMemberEntity member = new WorkspaceMemberEntity();
member.setWorkspaceId(workspaceId);
@ -162,10 +162,10 @@ public class WorkspaceService {
public WorkspaceMemberEntity updateMemberRole(Long workspaceId, Long userId, String role) {
WorkspaceMemberEntity member = getMembership(workspaceId, userId);
if (member == null) {
throw new MateClawException("用户不是该工作区的成员");
throw new MateClawException("err.workspace.not_member", "用户不是该工作区的成员");
}
if ("owner".equals(member.getRole())) {
throw new MateClawException("不能修改工作区拥有者的角色");
throw new MateClawException("err.workspace.cannot_modify_owner", "不能修改工作区拥有者的角色");
}
member.setRole(role);
memberMapper.updateById(member);
@ -176,10 +176,10 @@ public class WorkspaceService {
public void removeMember(Long workspaceId, Long userId) {
WorkspaceMemberEntity member = getMembership(workspaceId, userId);
if (member == null) {
throw new MateClawException("用户不是该工作区的成员");
throw new MateClawException("err.workspace.not_member", "用户不是该工作区的成员");
}
if ("owner".equals(member.getRole())) {
throw new MateClawException("不能移除工作区拥有者");
throw new MateClawException("err.workspace.cannot_remove_owner", "不能移除工作区拥有者");
}
memberMapper.deleteById(member.getId());
evictMembershipCache(workspaceId, userId);
@ -209,7 +209,7 @@ public class WorkspaceService {
*/
public void requirePermission(Long workspaceId, Long userId, String minRole) {
if (!hasPermission(workspaceId, userId, minRole)) {
throw new MateClawException("权限不足:需要 " + minRole + " 或更高角色");
throw new MateClawException("err.workspace.insufficient_permission", "权限不足:需要 " + minRole + " 或更高角色");
}
}

View File

@ -129,6 +129,45 @@ guard.CRED_AWS_KEY.fix=\u4f7f\u7528 IAM Role \u6216 AWS Secrets Manager
guard.CRED_PRIVATE_KEY.name=\u79c1\u94a5\u6cc4\u9732
guard.CRED_PRIVATE_KEY.fix=\u8bf7\u52ff\u5728\u53c2\u6570\u4e2d\u4f20\u9012\u79c1\u94a5
# --- Exception Messages (structured keys) ---
err.auth.invalid_credentials=\u7528\u6237\u540d\u6216\u5bc6\u7801\u9519\u8bef
err.auth.username_exists=\u7528\u6237\u540d\u5df2\u5b58\u5728
err.auth.user_not_found=\u7528\u6237\u4e0d\u5b58\u5728
err.auth.wrong_password=\u539f\u5bc6\u7801\u9519\u8bef
err.agent.not_found=Agent\u4e0d\u5b58\u5728
err.agent.disabled=Agent \u5df2\u7981\u7528
err.workspace.not_found=\u5de5\u4f5c\u533a\u4e0d\u5b58\u5728
err.workspace.slug_exists=\u5de5\u4f5c\u533a\u6807\u8bc6\u5df2\u5b58\u5728
err.workspace.cannot_modify_default=\u4e0d\u80fd\u4fee\u6539\u9ed8\u8ba4\u5de5\u4f5c\u533a\u7684\u6807\u8bc6
err.workspace.cannot_delete_default=\u4e0d\u80fd\u5220\u9664\u9ed8\u8ba4\u5de5\u4f5c\u533a
err.workspace.member_exists=\u7528\u6237\u5df2\u7ecf\u662f\u8be5\u5de5\u4f5c\u533a\u7684\u6210\u5458
err.workspace.not_member=\u7528\u6237\u4e0d\u662f\u8be5\u5de5\u4f5c\u533a\u7684\u6210\u5458
err.workspace.cannot_modify_owner=\u4e0d\u80fd\u4fee\u6539\u5de5\u4f5c\u533a\u62e5\u6709\u8005\u7684\u89d2\u8272
err.workspace.cannot_remove_owner=\u4e0d\u80fd\u79fb\u9664\u5de5\u4f5c\u533a\u62e5\u6709\u8005
err.workspace.insufficient_permission=\u6743\u9650\u4e0d\u8db3
err.channel.not_found=\u6e20\u9053\u4e0d\u5b58\u5728
err.channel.name_required=\u6e20\u9053\u540d\u79f0\u4e0d\u80fd\u4e3a\u7a7a
err.channel.type_required=\u6e20\u9053\u7c7b\u578b\u4e0d\u80fd\u4e3a\u7a7a
err.tool.not_found=\u5de5\u5177\u4e0d\u5b58\u5728
err.tool.builtin_readonly=\u5185\u7f6e\u5de5\u5177\u4e0d\u53ef\u5220\u9664
err.skill.not_found=\u6280\u80fd\u4e0d\u5b58\u5728
err.skill.name_required=\u6280\u80fd\u540d\u79f0\u4e0d\u80fd\u4e3a\u7a7a
err.skill.name_exists=\u6280\u80fd\u540d\u79f0\u5df2\u5b58\u5728
err.skill.builtin_readonly=\u5185\u7f6e\u6280\u80fd\u4e0d\u53ef\u5220\u9664
err.mcp.not_found=MCP server \u4e0d\u5b58\u5728
err.mcp.builtin_readonly=\u5185\u7f6e MCP server \u4e0d\u53ef\u5220\u9664
err.mcp.name_required=MCP server \u540d\u79f0\u4e0d\u80fd\u4e3a\u7a7a
err.mcp.transport_required=\u4f20\u8f93\u7c7b\u578b\u4e0d\u80fd\u4e3a\u7a7a
err.mcp.transport_unsupported=\u4e0d\u652f\u6301\u7684\u4f20\u8f93\u7c7b\u578b
err.mcp.stdio_command_required=stdio \u7c7b\u578b\u5fc5\u987b\u6307\u5b9a command
err.mcp.http_url_required=HTTP/SSE \u7c7b\u578b\u5fc5\u987b\u6307\u5b9a url
err.cron.not_found=\u5b9a\u65f6\u4efb\u52a1\u4e0d\u5b58\u5728
err.cron.name_required=\u4efb\u52a1\u540d\u79f0\u4e0d\u80fd\u4e3a\u7a7a
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
# --- 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.symlink_escape=\u8def\u5f84\u901a\u8fc7\u7b26\u53f7\u94fe\u63a5\u9003\u9038\u51fa\u5de5\u4f5c\u533a: {0}\uff0c\u5141\u8bb8\u7684\u6839\u76ee\u5f55: {1}

View File

@ -133,6 +133,53 @@ guard.CRED_PRIVATE_KEY.fix=Do not pass private keys in parameters
guard.path.not_allowed=Path is outside workspace boundary: {0}, allowed root: {1}
guard.path.symlink_escape=Path escapes workspace via symlink: {0}, allowed root: {1}
# --- Exception Messages (structured keys, resolved by GlobalExceptionHandler) ---
# auth
err.auth.invalid_credentials=Invalid username or password
err.auth.username_exists=Username already exists
err.auth.user_not_found=User not found
err.auth.wrong_password=Incorrect current password
# agent
err.agent.not_found=Agent not found
err.agent.disabled=Agent is disabled
# workspace
err.workspace.not_found=Workspace not found
err.workspace.slug_exists=Workspace slug already exists
err.workspace.cannot_modify_default=Cannot modify default workspace slug
err.workspace.cannot_delete_default=Cannot delete default workspace
err.workspace.member_exists=User is already a member of this workspace
err.workspace.not_member=User is not a member of this workspace
err.workspace.cannot_modify_owner=Cannot modify workspace owner role
err.workspace.cannot_remove_owner=Cannot remove workspace owner
err.workspace.insufficient_permission=Insufficient permission
# channel
err.channel.not_found=Channel not found
err.channel.name_required=Channel name cannot be empty
err.channel.type_required=Channel type cannot be empty
# tool
err.tool.not_found=Tool not found
err.tool.builtin_readonly=Built-in tool cannot be deleted
# skill
err.skill.not_found=Skill not found
err.skill.name_required=Skill name cannot be empty
err.skill.name_exists=Skill name already exists
err.skill.builtin_readonly=Built-in skill cannot be deleted
# mcp
err.mcp.not_found=MCP server not found
err.mcp.builtin_readonly=Built-in MCP server cannot be deleted
err.mcp.name_required=MCP server name cannot be empty
err.mcp.transport_required=Transport type cannot be empty
err.mcp.transport_unsupported=Unsupported transport type
err.mcp.stdio_command_required=stdio type requires command
err.mcp.http_url_required=HTTP/SSE type requires URL
# cron
err.cron.not_found=Scheduled task not found
err.cron.name_required=Task name cannot be empty
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
# --- RuntimeContext ---
context.current_time=[system-context] Current time: {0} {1} (Asia/Shanghai)
context.working_dir=[system-context] Working directory: {0}

View File

@ -140,7 +140,7 @@
</svg>
</button>
</div>
<span v-if="hasChanges" class="change-badge">{{ t('agentContext.modified') || '已修改' }}</span>
<span v-if="hasChanges" class="change-badge">{{ t('agentContext.modified') }}</span>
</div>
<div class="editor-content" :class="'mode-' + previewMode">