feat(i18n): complete backend i18n — guard rules, tool errors, response codes

This commit is contained in:
matevip 2026-04-11 17:26:58 +08:00
parent e662cce91a
commit 81ff1bf491
8 changed files with 37 additions and 23 deletions

View File

@ -431,7 +431,7 @@ public class NodeStreamingChatHelper {
// Client error (400): 不重试参数/格式错误重试也不会变
if (errorType == ErrorType.CLIENT_ERROR) {
log.error("[{}] Client error (400), not retrying: {}", phase, error.getMessage());
return buildErrorResultWithType("请求参数错误: " + extractUserFriendlyError(error),
return buildErrorResultWithType("Bad request: " + extractUserFriendlyError(error),
conversationId, phase, errorType);
}
@ -625,12 +625,12 @@ public class NodeStreamingChatHelper {
String msg = error.getMessage();
if (msg == null) return error.getClass().getSimpleName();
// Jackson 反序列化错误提取关键信息
if (msg.contains("engine_overloaded")) return "模型服务过载,请稍后重试";
if (msg.contains("unsupported image format") || msg.contains("unsupported")) return "不支持的文件格式(如 SVG请使用 PNG/JPG 等光栅图片";
if (msg.contains("invalid_request_error") || msg.contains("400 Bad Request")) return "请求参数错误,请检查输入";
if (msg.contains("rate_limit") || msg.contains("429")) return "请求频率过高,请稍后重试";
if (msg.contains("timeout") || msg.contains("Timeout")) return "请求超时,请重试";
if (msg.contains("502") || msg.contains("503") || msg.contains("504")) return "模型服务暂时不可用";
if (msg.contains("engine_overloaded")) return "Model service overloaded, please retry later";
if (msg.contains("unsupported image format") || msg.contains("unsupported")) return "Unsupported file format (e.g. SVG), use PNG/JPG instead";
if (msg.contains("invalid_request_error") || msg.contains("400 Bad Request")) return "Bad request, please check input";
if (msg.contains("rate_limit") || msg.contains("429")) return "Rate limit exceeded, please retry later";
if (msg.contains("timeout") || msg.contains("Timeout")) return "Request timeout, please retry";
if (msg.contains("502") || msg.contains("503") || msg.contains("504")) return "Model service temporarily unavailable";
// 截断过长的原始消息
return msg.length() > 100 ? msg.substring(0, 100) + "..." : msg;
}

View File

@ -230,9 +230,9 @@ public class ToolExecutionExecutor {
ToolCallback callback = toolCallbackMap.get(toolName);
if (callback == null) {
log.warn("[ToolExecutor] Tool not found: {}", toolName);
events.add(GraphEventPublisher.toolComplete(toolName, "工具不存在: " + toolName, false));
events.add(GraphEventPublisher.toolComplete(toolName, "Tool not found: " + toolName, false));
allResponses.add(new ToolResponseMessage.ToolResponse(
toolCall.id(), toolName, "工具不存在: " + toolName));
toolCall.id(), toolName, "Tool not found: " + toolName));
continue;
}
@ -270,8 +270,8 @@ public class ToolExecutionExecutor {
ToolCallback callback = toolCallbackMap.get(toolName);
if (callback == null) {
log.warn("[ToolExecutor] Pre-approved tool not found: {}", toolName);
events.add(GraphEventPublisher.toolComplete(toolName, "工具不存在: " + toolName, false));
return new ToolResponseMessage.ToolResponse(toolCall.id(), toolName, "工具不存在: " + toolName);
events.add(GraphEventPublisher.toolComplete(toolName, "Tool not found: " + toolName, false));
return new ToolResponseMessage.ToolResponse(toolCall.id(), toolName, "Tool not found: " + toolName);
}
try {
@ -288,7 +288,7 @@ public class ToolExecutionExecutor {
log.error("[ToolExecutor] Pre-approved tool {} failed: {}", toolName, e.getMessage());
events.add(GraphEventPublisher.toolComplete(toolName, e.getMessage(), false));
return new ToolResponseMessage.ToolResponse(
toolCall.id(), toolName, "工具执行失败: " + e.getMessage());
toolCall.id(), toolName, "Tool execution failed: " + e.getMessage());
}
}
@ -505,7 +505,7 @@ public class ToolExecutionExecutor {
}
private String normalizeToolExecutionError(Exception e) {
String message = e != null && e.getMessage() != null ? e.getMessage() : "未知错误";
String message = e != null && e.getMessage() != null ? e.getMessage() : "Unknown error";
String lower = message.toLowerCase(Locale.ROOT);
if (lower.contains("conversion from json")
@ -513,16 +513,16 @@ public class ToolExecutionExecutor {
|| lower.contains("unexpected character escape sequence")
|| lower.contains("json parse error")
|| lower.contains("malformed json")) {
return "工具执行失败:模型生成的工具参数不是合法 JSON通常表示单次 tool call 内容过长,"
return "Tool execution failed: model generated invalid JSON for tool arguments. "
+ "或在字符串转义位置被截断。请改为分步骤写入,拆成多个文件,或缩小单次 write_file/edit_file 的内容后重试。";
}
if (lower.contains("access denied") && lower.contains("path outside allowed directories")) {
// 提取目标路径和允许路径
return "工具执行失败:目标路径不在允许的工作目录范围内。请将文件操作改为用户主目录下的路径(如 ~/Documents/ 或 ~/Desktop/)。";
return "Tool execution failed: target path is outside the allowed workspace directory.";
}
return "工具执行失败: " + message;
return "Tool execution failed: " + message;
}
/**

View File

@ -95,7 +95,7 @@ public class FinalAnswerNode implements NodeAction {
finishReason = FinishReason.SUMMARIZED;
log.warn("[FinalAnswerNode] No finalAnswer or draft found, falling back to summarizedContext");
} else {
finalAnswer = "未能生成回答,请重试。";
finalAnswer = "Failed to generate a response, please retry.";
finalThinking = "";
finishReason = FinishReason.ERROR_FALLBACK;
log.error("[FinalAnswerNode] No answer source available, returning fallback");

View File

@ -85,13 +85,13 @@ public class DefaultToolGuard implements ToolGuard {
// Shell 工具即使未命中任何模式也需要审批任何本地命令执行都是敏感操作
if (isShellTool) {
log.info("[ToolGuard] NEEDS_APPROVAL (shell tool default): tool={}", toolName);
return ToolGuardResult.needsApproval("本地命令执行需要用户确认", "shell_tool_default");
return ToolGuardResult.needsApproval("Shell command execution requires user approval", "shell_tool_default");
}
// 文件写入/编辑工具需要审批
if (toolName != null && FILE_WRITE_TOOL_NAMES.contains(toolName)) {
log.info("[ToolGuard] NEEDS_APPROVAL (file write tool): tool={}", toolName);
return ToolGuardResult.needsApproval("文件写入/编辑操作需要用户确认", "file_write_tool_default");
return ToolGuardResult.needsApproval("File write/edit operation requires user approval", "file_write_tool_default");
}
return ToolGuardResult.allow();

View File

@ -1,8 +1,11 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { ArrowDown } from '@element-plus/icons-vue'
import type { Message } from '@/types'
const { t } = useI18n()
const props = defineProps<{
message: Message
}>()
@ -28,9 +31,9 @@ const compressedCount = computed(() => {
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 14 10 14 10 20"/><polyline points="20 10 14 10 14 4"/><line x1="14" y1="10" x2="21" y2="3"/><line x1="3" y1="21" x2="10" y2="14"/></svg>
</span>
<span v-if="compressedCount > 0" class="seg-compression__label">
{{ `之前的 ${compressedCount} 条对话已整理为摘要` }}
{{ t('chat.compressionWithCount', { count: compressedCount }) }}
</span>
<span v-else class="seg-compression__label">之前的对话已整理为摘要</span>
<span v-else class="seg-compression__label">{{ t('chat.compressionSummary') }}</span>
<el-icon class="seg-compression__arrow" :class="{ 'is-open': expanded }" :size="12"><ArrowDown /></el-icon>
</div>
<div v-if="expanded" class="seg-compression__body">

View File

@ -42,10 +42,10 @@
<div v-if="hasMore" ref="loadMoreRef" class="load-more-trigger text-center py-3">
<div v-if="loadingOlder" class="text-gray-400 dark:text-gray-500 text-sm flex items-center justify-center gap-2">
<span class="animate-spin inline-block w-4 h-4 border-2 border-gray-300 border-t-gray-500 rounded-full"></span>
加载更早的消息...
{{ t('chat.loadingOlder') }}
</div>
<button v-else class="text-gray-400 dark:text-gray-500 text-sm hover:text-gray-600 dark:hover:text-gray-300 transition-colors" @click="$emit('load-more')">
点击加载更早的消息
{{ t('chat.loadOlderMessages') }}
</button>
</div>
@ -85,7 +85,10 @@
<script setup lang="ts">
import { computed, watch, nextTick } from 'vue'
import { useI18n } from 'vue-i18n'
import { ChatDotRound, DataLine, EditPen, Monitor, Right } from '@element-plus/icons-vue'
const { t } = useI18n()
import MessageBubble from './MessageBubble.vue'
import CompressionSummary from './CompressionSummary.vue'
import { useStickToBottom } from '@/composables/chat/useStickToBottom'

View File

@ -44,6 +44,10 @@ export default {
stopped: 'Generation stopped',
interrupted: 'Interrupted',
failed: 'Generation failed',
compressionSummary: 'Previous conversations summarized',
compressionWithCount: '{count} previous messages summarized',
loadingOlder: 'Loading older messages...',
loadOlderMessages: 'Click to load older messages',
retry: 'Retry',
errorCode: 'Error code',
error: {

View File

@ -44,6 +44,10 @@ export default {
stopped: '已停止生成',
interrupted: '已中断',
failed: '生成失败',
compressionSummary: '之前的对话已整理为摘要',
compressionWithCount: '之前的 {count} 条对话已整理为摘要',
loadingOlder: '加载更早的消息...',
loadOlderMessages: '点击加载更早的消息',
retry: '重试',
errorCode: '错误码',
error: {