From 0f6809251a0bf23a55614b86487cea3e6928c079 Mon Sep 17 00:00:00 2001 From: HouYunFei <1844025705@qq.com> Date: Wed, 5 Aug 2026 14:08:03 +0800 Subject: [PATCH] feat(i18n): implement internationalization framework and update UI components for language support --- CHANGELOG.md | 1 + docs/content/docs/progress/pending-test.mdx | 1 + web/bun.lock | 8 + web/package.json | 2 + .../components/agent/agent-chat-composer.tsx | 66 +- .../components/agent/agent-chat-message.tsx | 113 ++-- web/src/components/agent/agent-chat.tsx | 16 +- .../components/agent/agent-connect-view.tsx | 44 +- .../agent/agent-event-formatters.ts | 226 ++++--- .../components/agent/agent-history-view.tsx | 29 +- web/src/components/agent/agent-log-view.tsx | 65 +- web/src/components/agent/agent-panel-tabs.tsx | 4 +- web/src/components/agent/agent-panel.tsx | 4 +- .../components/agent/agent-skill-picker.tsx | 12 +- .../components/agent/agent-skills-view.tsx | 124 ++-- .../components/agent/local-agent-panel.tsx | 237 +++---- web/src/components/audio-settings-panel.tsx | 14 +- .../components/canvas/asset-picker-modal.tsx | 33 +- .../canvas/canvas-config-composer.tsx | 16 +- .../canvas/canvas-config-node-panel.tsx | 26 +- .../components/canvas/canvas-context-menu.tsx | 6 +- .../components/canvas/canvas-create-menus.tsx | 21 +- .../canvas/canvas-delete-projects-dialog.tsx | 10 +- .../canvas/canvas-image-settings-popover.tsx | 4 +- .../canvas-image-toolbar-settings-modal.tsx | 20 +- .../canvas/canvas-image-toolbar-tools.tsx | 57 +- .../canvas/canvas-node-angle-dialog.tsx | 22 +- .../canvas/canvas-node-crop-dialog.tsx | 47 +- .../canvas/canvas-node-generation.ts | 3 +- .../canvas/canvas-node-hover-toolbar.tsx | 58 +- .../canvas/canvas-node-mask-edit-dialog.tsx | 44 +- .../canvas/canvas-node-prompt-panel.tsx | 15 +- .../canvas/canvas-node-split-dialog.tsx | 48 +- .../canvas/canvas-node-upscale-dialog.tsx | 34 +- web/src/components/canvas/canvas-node.tsx | 53 +- .../canvas/canvas-plugin-manager-modal.tsx | 50 +- .../components/canvas/canvas-project-card.tsx | 18 +- .../canvas/canvas-prompt-chip-input.tsx | 3 +- .../canvas/canvas-prompt-library.tsx | 6 +- .../components/canvas/canvas-side-panel.tsx | 112 ++-- .../components/canvas/canvas-size-picker.tsx | 4 +- .../canvas/canvas-text-settings-popover.tsx | 4 +- web/src/components/canvas/canvas-toolbar.tsx | 88 +-- web/src/components/canvas/canvas-top-bar.tsx | 61 +- .../canvas/canvas-zoom-controls.tsx | 32 +- .../components/canvas/nodes/builtin-nodes.tsx | 14 +- .../components/image-generation-pending.tsx | 5 +- web/src/components/image-settings-panel.tsx | 37 +- .../components/layout/app-config-modal.tsx | 176 +++--- web/src/components/layout/app-providers.tsx | 18 +- web/src/components/layout/app-top-nav.tsx | 14 +- .../layout/channel-editor-drawer.tsx | 45 +- .../components/layout/client-root-init.tsx | 8 +- .../layout/config-prompt-sources.tsx | 65 +- .../components/layout/mobile-nav-drawer.tsx | 7 +- .../components/layout/model-script-editor.tsx | 33 +- .../components/layout/model-select-modal.tsx | 36 +- .../layout/prompt-source-content-modal.tsx | 34 +- .../layout/prompt-source-editor-drawer.tsx | 30 +- .../components/layout/user-status-actions.tsx | 12 +- .../layout/version-release-modal.tsx | 34 +- web/src/components/model-picker.tsx | 16 +- web/src/components/prompts/prompt-card.tsx | 8 +- .../prompts/prompt-select-dialog.tsx | 22 +- web/src/components/text-settings-panel.tsx | 23 +- .../components/ui/animated-theme-toggler.tsx | 4 +- web/src/components/video-settings-panel.tsx | 56 +- web/src/constant/canvas.ts | 25 +- web/src/constant/navigation-tools.ts | 6 - web/src/hooks/use-copy-text.ts | 4 +- web/src/hooks/use-version-check.ts | 12 +- web/src/i18n/index.ts | 29 + web/src/i18n/locales/en-US.ts | 589 ++++++++++++++++++ web/src/i18n/locales/zh-CN.ts | 589 ++++++++++++++++++ web/src/lib/agent/agent-site-tools.ts | 47 +- web/src/lib/canvas/canvas-agent-ops.ts | 11 +- web/src/lib/canvas/canvas-export.ts | 7 +- .../lib/canvas/canvas-generation-helpers.ts | 13 +- .../lib/canvas/canvas-resource-references.ts | 3 +- web/src/lib/canvas/node-registry.ts | 4 +- web/src/lib/canvas/plugin-loader.ts | 7 +- web/src/lib/canvas/plugin-registry.ts | 3 +- web/src/lib/image-reference-prompt.ts | 6 +- web/src/lib/image-utils.ts | 5 +- web/src/lib/seedance-video.ts | 41 +- web/src/main.tsx | 1 + web/src/pages/assets/asset-transfer.ts | 4 +- web/src/pages/assets/index.tsx | 139 ++--- .../pages/canvas/hooks/use-agent-bridge.ts | 3 +- .../pages/canvas/hooks/use-plugin-host.tsx | 12 +- web/src/pages/canvas/index.tsx | 38 +- web/src/pages/canvas/project.tsx | 157 +++-- web/src/pages/config/index.tsx | 8 +- web/src/pages/home/index.tsx | 29 +- web/src/pages/image/index.tsx | 149 ++--- web/src/pages/not-found/index.tsx | 8 +- .../components/prompt-detail-dialog.tsx | 9 +- web/src/pages/prompts/index.tsx | 30 +- web/src/pages/video/index.tsx | 159 ++--- web/src/services/api/audio.ts | 42 +- web/src/services/api/canvas-agent.ts | 3 +- web/src/services/api/image.ts | 91 +-- web/src/services/api/model-plugin.ts | 110 ++-- web/src/services/api/prompt-source-presets.ts | 2 +- web/src/services/api/prompt-source-runtime.ts | 11 +- web/src/services/api/prompts.ts | 11 +- web/src/services/api/video.ts | 82 +-- web/src/services/app-sync.ts | 5 +- web/src/services/config-file.ts | 5 +- web/src/services/image-storage.ts | 3 +- web/src/services/webdav-sync.ts | 26 +- web/src/stores/canvas/use-canvas-store.ts | 5 +- web/src/stores/use-agent-skill-store.ts | 7 +- web/src/stores/use-agent-store.ts | 11 +- web/src/stores/use-config-store.ts | 12 +- web/src/stores/use-prompt-source-store.ts | 8 +- 116 files changed, 3269 insertions(+), 1870 deletions(-) create mode 100644 web/src/i18n/index.ts create mode 100644 web/src/i18n/locales/en-US.ts create mode 100644 web/src/i18n/locales/zh-CN.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index ad5364a..b8e71f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased ++ [新增] 前端搭建中英文国际化框架,支持持久化切换全局导航、首页、配置中心、Ant Design 和日期组件语言。 + [新增] Agent 支持查看、创建、编辑、删除、启停和显式调用本地 Codex Skill,并同步多标签页变更。 + [新增] Agent 支持从当前对话或当前画布生成可编辑的 Skill 草稿,并在用户确认后保存。 + [调整] Agent 面板连接状态、内容标签和新对话统一排版,窄面板仅隐藏文字并保留图标与数量。 diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx index 11f44e8..ff78a3b 100644 --- a/docs/content/docs/progress/pending-test.mdx +++ b/docs/content/docs/progress/pending-test.mdx @@ -5,6 +5,7 @@ description: 当前版本已实现但仍需人工验证的变更项 # 待测试 +- 国际化基础框架:进入「配置与用户偏好 → 偏好设置」切换简体中文或 English 后,首页、顶部及移动端导航、版本弹窗、配置主面板、渠道编辑、模型选择、页面标题、页面描述和 Ant Design 内置文案应立即切换,刷新页面后保持所选语言;WebDAV 同步错误、提示词来源及其他尚未接入翻译的业务模块暂时仍可能显示中文。 - 画布节点缩放稳定性:选中任意节点并反复拖动四角缩放时,节点工具条应在缩放期间隐藏、松开后恢复,页面不应再出现 `Maximum update depth exceeded`。 - Agent MCP 初始化状态:首次进入空白对话、点击「新对话」或恢复历史对话后,无需先发送消息,对话区应立即显示 Codex 会话准备,并列出 `codex_apps`、`infinite-canvas`、`node_repl` 等完整 MCP 服务状态;全部服务进入终态前可输入并保留草稿,但发送按钮、上传和回车提交应禁用;可选 MCP 失败时应提示警告但允许发送,`infinite-canvas` 缺失或失败时应阻止发送并显示明确原因;首轮及后续任务运行期间再次收到的 MCP 状态只进入诊断日志,模型开始处理及回复完成后对话区不应残留初始化卡片。 - Agent 模型设置:连接 Canvas Agent 后,输入框左下方应显示当前 Codex 模型与推理强度;模型列表应来自当前账号实际可用模型且不显示内部审查模型或重复项,切换模型后强度选项随模型能力更新且无空白选项,刷新页面后保留选择,发送任务时实际使用所选模型和强度;本地控制台与右侧「日志」应记录本轮使用的模型和推理强度。 diff --git a/web/bun.lock b/web/bun.lock index ea09631..2f59aa1 100644 --- a/web/bun.lock +++ b/web/bun.lock @@ -19,6 +19,7 @@ "dayjs": "^1.11.20", "fflate": "^0.8.3", "file-saver": "^2.0.5", + "i18next": "^26.3.6", "localforage": "^1.10.0", "lucide-react": "^1.16.0", "motion": "^12.38.0", @@ -26,6 +27,7 @@ "radix-ui": "^1.4.3", "react": "19.2.5", "react-dom": "19.2.5", + "react-i18next": "^17.0.11", "react-router": "^7.12.0", "react-router-dom": "^7.12.0", "shadcn": "^4.7.0", @@ -1125,6 +1127,8 @@ "hono": ["hono@4.12.18", "https://registry.npmmirror.com/hono/-/hono-4.12.18.tgz", {}, "sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ=="], + "html-parse-stringify": ["html-parse-stringify@4.0.1", "", {}, "sha512-0zHsZJrK7S3K2aucXWL6ycoYJ/iNtIcFHC/nYQgFklPtrv5LpJctIiSCroWZWeuoXvuyFdzp6KzjJQ+OT5MfFw=="], + "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], @@ -1135,6 +1139,8 @@ "human-signals": ["human-signals@8.0.1", "https://registry.npmmirror.com/human-signals/-/human-signals-8.0.1.tgz", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="], + "i18next": ["i18next@26.3.6", "", { "peerDependencies": { "typescript": "^5 || ^6 || ^7" }, "optionalPeers": ["typescript"] }, "sha512-Bu5Z2nAXgfVyM8xvW3jk9EKRIuX37PudsrBViThNFx7CR7aaYTpP01cxNB/E4c4UUzTDiAZRstEhsRfPOL/8xA=="], + "iconv-lite": ["iconv-lite@0.7.2", "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.2.tgz", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], "ignore": ["ignore@5.3.2", "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], @@ -1531,6 +1537,8 @@ "react-draggable": ["react-draggable@4.5.0", "https://registry.npmmirror.com/react-draggable/-/react-draggable-4.5.0.tgz", { "dependencies": { "clsx": "^2.1.1", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">= 16.3.0", "react-dom": ">= 16.3.0" } }, "sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw=="], + "react-i18next": ["react-i18next@17.0.11", "", { "dependencies": { "@babel/runtime": "^7.29.2", "html-parse-stringify": "^4.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 26.2.0", "react": ">= 16.8.0", "typescript": "^5 || ^6 || ^7" }, "optionalPeers": ["typescript"] }, "sha512-cDtkXgxjuFTWUH6V+aQn1Ve5vDiUztCNPWW5GtSHDccsgRXO1nE6QFWCEmc1KAutrb3OUv87wFShJL5RhUwPXg=="], + "react-is": ["react-is@18.3.1", "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], "react-layout-kit": ["react-layout-kit@1.9.2", "https://registry.npmmirror.com/react-layout-kit/-/react-layout-kit-1.9.2.tgz", { "dependencies": { "@babel/runtime": "^7", "@emotion/css": "^11" }, "peerDependencies": { "react": ">=18" } }, "sha512-fzmrwMBNGIAiDIrdFMV3NvJhUNl01QC9EMcI8SP7osg51N4j+z6w4tx9i2yWxEEXZ2armLV6EtkFd3KST8PYiA=="], diff --git a/web/package.json b/web/package.json index f0fec03..41d50e4 100644 --- a/web/package.json +++ b/web/package.json @@ -26,6 +26,7 @@ "dayjs": "^1.11.20", "fflate": "^0.8.3", "file-saver": "^2.0.5", + "i18next": "^26.3.6", "localforage": "^1.10.0", "lucide-react": "^1.16.0", "motion": "^12.38.0", @@ -33,6 +34,7 @@ "radix-ui": "^1.4.3", "react": "19.2.5", "react-dom": "19.2.5", + "react-i18next": "^17.0.11", "react-router": "^7.12.0", "react-router-dom": "^7.12.0", "shadcn": "^4.7.0", diff --git a/web/src/components/agent/agent-chat-composer.tsx b/web/src/components/agent/agent-chat-composer.tsx index 51d8682..73bd554 100644 --- a/web/src/components/agent/agent-chat-composer.tsx +++ b/web/src/components/agent/agent-chat-composer.tsx @@ -1,6 +1,7 @@ import { useRef, useState, type ReactNode } from "react"; import { Button, Dropdown, Tooltip } from "antd"; import { ArrowUp, Check, ChevronUp, Cpu, Gauge, Hand, ImagePlus, LoaderCircle, RefreshCw, ShieldAlert, ShieldCheck, ShieldOff, Square, X } from "lucide-react"; +import { useTranslation } from "react-i18next"; import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select"; import { canvasThemes } from "@/lib/canvas-theme"; @@ -55,6 +56,7 @@ export function AgentChatComposer({ onReasoningEffortChange?: (effort: AgentReasoningEffort) => void; left?: ReactNode; }) { + const { t } = useTranslation(); const fileInputRef = useRef(null); const selectedSkill = useAgentSkillStore((state) => state.selectedSkill); const clearSkillSelection = useAgentSkillStore((state) => state.clearSelection); @@ -68,7 +70,7 @@ export function AgentChatComposer({
{item.name} {onRemoveAttachment ? ( - ) : null} @@ -80,7 +82,7 @@ export function AgentChatComposer({
{selectedSkill.interface?.displayName || selectedSkill.name} - @@ -114,8 +116,8 @@ export function AgentChatComposer({ void onAddFiles(event.target.files); event.target.value = ""; }} /> - -
{sending && onStop ? ( -
@@ -139,15 +141,17 @@ export function AgentChatComposer({ } function AgentModelControls({ models, model, reasoningEffort, onModelChange, onReasoningEffortChange }: { models: AgentModel[]; model: string; reasoningEffort: AgentReasoningEffort; onModelChange: (model: string) => void; onReasoningEffortChange: (effort: AgentReasoningEffort) => void }) { + const { t } = useTranslation(); const current = models.find((item) => item.model === model) || models[0]; + const effortLabel = (effort: AgentReasoningEffort) => t(`agent.composer.effort.${effort}`); const [modelOpen, setModelOpen] = useState(false); const [reasoningOpen, setReasoningOpen] = useState(false); return (
- + - + @@ -176,21 +180,17 @@ function AgentModelControls({ models, model, reasoningEffort, onModelChange, onR ); } -const effortLabels: Record = { - minimal: "最低", - low: "轻度", - medium: "中", - high: "高", - xhigh: "极高", - max: "最高", - ultra: "Ultra", -}; - function PermissionModeMenu({ permissionMode, theme, onChange }: { permissionMode: AgentPermissionMode; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; onChange: (permissionMode: AgentPermissionMode) => void }) { + const { t } = useTranslation(); + const permissionOptions: Array<{ key: AgentPermissionMode; title: string; shortTitle: string; description: string; icon: ReactNode }> = [ + { key: "request", title: t("agent.composer.permission.request"), shortTitle: t("agent.composer.permission.request"), description: t("agent.composer.permission.requestDescription"), icon: }, + { key: "automatic", title: t("agent.composer.permission.automatic"), shortTitle: t("agent.composer.permission.automatic"), description: t("agent.composer.permission.automaticDescription"), icon: }, + { key: "full", title: t("agent.composer.permission.full"), shortTitle: t("agent.composer.permission.fullShort"), description: t("agent.composer.permission.fullDescription"), icon: }, + ]; const current = permissionOptions.find((item) => item.key === permissionMode) || permissionOptions[0]; const [open, setOpen] = useState(false); return ( - + - diff --git a/web/src/components/agent/agent-chat-message.tsx b/web/src/components/agent/agent-chat-message.tsx index bf3dc0c..a3e9160 100644 --- a/web/src/components/agent/agent-chat-message.tsx +++ b/web/src/components/agent/agent-chat-message.tsx @@ -2,30 +2,27 @@ import { useEffect, useId, useState, type ReactNode } from "react"; import { App, Button, Image, Modal } from "antd"; import { Brain, CheckCircle2, ChevronDown, ChevronRight, Circle, CircleAlert, Copy, ExternalLink, FilePenLine, FileText, FolderOpen, ListChecks, LoaderCircle, Search, ShieldAlert, TerminalSquare, Wrench, XCircle } from "lucide-react"; import { Streamdown, type LinkSafetyModalProps } from "streamdown"; +import { useTranslation } from "react-i18next"; +import i18n from "@/i18n"; import { useCopyText } from "@/hooks/use-copy-text"; import { canvasThemes } from "@/lib/canvas-theme"; import { useAgentStore, type AgentPendingApproval } from "@/stores/use-agent-store"; import { revealAgentLocalFile } from "@/services/api/canvas-agent"; -const streamdownProps = { +const streamdownProps = () => ({ className: "agent-streamdown", controls: { code: { copy: true, download: false }, table: { copy: true, download: false, fullscreen: false } }, linkSafety: { enabled: true, renderModal: (props: LinkSafetyModalProps) => }, lineNumbers: false, translations: { - close: "关闭", - copied: "已复制", - copyCode: "复制代码", - copyLink: "复制链接", - externalLinkWarning: "即将打开以下外部链接,请确认链接可信。", - openExternalLink: "打开外部链接?", - openLink: "继续打开", + close: tr("close"), copied: tr("copied"), copyCode: tr("copyCode"), copyLink: tr("copyLink"), externalLinkWarning: tr("externalWarning"), openExternalLink: tr("openExternal"), openLink: tr("continueOpen"), }, -} as const; +} as const); const streamdownAnimation = { duration: 20, stagger: 0, sep: "word" } as const; function AgentLinkModal({ isOpen, onClose, onConfirm, url }: LinkSafetyModalProps) { + const { t } = useTranslation(); const { message } = App.useApp(); const copyText = useCopyText(); const localPath = localFilePath(url); @@ -36,26 +33,26 @@ function AgentLinkModal({ isOpen, onClose, onConfirm, url }: LinkSafetyModalProp setOpening(true); try { await revealAgentLocalFile(endpoint, token, localPath); - message.success("已在文件管理器中定位"); + message.success(t("agent.message.revealed")); onClose(); } catch (error) { - message.error(error instanceof Error ? error.message : "无法打开本地文件"); + message.error(error instanceof Error ? error.message : t("agent.message.openLocalFailed")); } finally { setOpening(false); } }; return ( - +
- {localPath ? "将在本机文件管理器中定位该路径,不会通过浏览器打开。" : "即将打开以下外部链接,请确认链接可信。"} + {t(localPath ? "agent.message.localDescription" : "agent.message.externalDescription")}
{localPath || url}
-
@@ -117,7 +114,7 @@ export function AgentChatMessage({ item, theme, onRejectTool, onApproveTool }: { } if (item.role === "tool") { if (objectField(item.detail, "status") === "pending") return onRejectTool?.(item.id)} onApprove={() => onApproveTool?.(item.id)} />; - return ; + return ; } return (
@@ -128,7 +125,7 @@ export function AgentChatMessage({ item, theme, onRejectTool, onApproveTool }: { {isUser ? (
{item.text}
) : ( - {item.text} + {item.text} )} {item.attachments?.length ? : null} {item.meta ?
{item.meta}
: null} @@ -138,6 +135,7 @@ export function AgentChatMessage({ item, theme, onRejectTool, onApproveTool }: { } export function AgentPendingToolCard({ summary, detail, theme, onReject, onApprove }: { summary: string; detail?: unknown; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; onReject?: () => void; onApprove?: () => void }) { + const { t } = useTranslation(); const view = userDetail(detail); return (
@@ -145,7 +143,7 @@ export function AgentPendingToolCard({ summary, detail, theme, onReject, onAppro { if (!view) event.preventDefault(); }}>
- 等待确认 + {t("agent.message.awaitingConfirmation")} {view ? : null}
{summary}
@@ -155,10 +153,10 @@ export function AgentPendingToolCard({ summary, detail, theme, onReject, onAppro {onReject || onApprove ? (
) : null} @@ -167,9 +165,10 @@ export function AgentPendingToolCard({ summary, detail, theme, onReject, onAppro } export function AgentApprovalCard({ approval, theme, onDecision }: { approval: AgentPendingApproval; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; onDecision: (decision: "accept" | "acceptForSession" | "decline") => void }) { + const { t } = useTranslation(); const isFile = approval.method === "item/fileChange/requestApproval"; const isNetwork = Boolean(approval.networkApprovalContext); - const title = isNetwork ? "请求网络访问" : isFile ? "请求编辑文件" : approval.method === "item/permissions/requestApproval" ? "请求扩展权限" : "请求执行命令"; + const title = t(isNetwork ? "agent.message.networkApproval" : isFile ? "agent.message.fileApproval" : approval.method === "item/permissions/requestApproval" ? "agent.message.permissionApproval" : "agent.message.commandApproval"); const target = isNetwork ? approvalTarget(approval.networkApprovalContext) : isFile ? approval.grantRoot || approval.cwd : commandText(approval.command) || approval.cwd; return (
@@ -182,9 +181,9 @@ export function AgentApprovalCard({ approval, theme, onDecision }: { approval: A
- - - + + +
); @@ -226,6 +225,7 @@ export function AgentToolCard({ title, text, detail, theme }: { title: string; t } function AgentReasoningSummary({ text, detail, theme }: { text: string; detail?: unknown; theme: (typeof canvasThemes)[keyof typeof canvasThemes] }) { + const { t } = useTranslation(); const status = String(objectField(detail, "status") || ""); const running = ["inProgress", "in_progress", "running", "started", "pending"].includes(status); return ( @@ -233,12 +233,12 @@ function AgentReasoningSummary({ text, detail, theme }: { text: string; detail?:
{running ? : } - {running ? "正在思考" : "思考摘要"} + {t(running ? "agent.message.thinking" : "agent.events.reasoning")}
- {text} + {text}
); @@ -247,14 +247,13 @@ function AgentReasoningSummary({ text, detail, theme }: { text: string; detail?: type AgentCommandItem = Pick; export function AgentCommandGroup({ items, theme }: { items: AgentCommandItem[]; theme: (typeof canvasThemes)[keyof typeof canvasThemes] }) { + const { t } = useTranslation(); const states = items.map((item) => commandViewState(item.detail)); const running = states.some((state) => state.running); const failed = states.filter((state) => state.failed).length; const expandable = items.some((item) => Boolean(item.text.trim() || userDetail(item.detail))); const color = running ? "#d97706" : failed ? "#dc2626" : theme.node.muted; - const label = running - ? items.length > 1 ? `正在执行 ${items.length} 条命令` : "正在执行命令" - : `已执行 ${items.length} 条命令${failed ? ` · ${failed} 条失败` : ""}`; + const label = running ? t(items.length > 1 ? "agent.message.commandsRunning" : "agent.message.commandRunning", { count: items.length }) : t("agent.message.commandsCompleted", { count: items.length, failed: failed ? t("agent.message.commandsFailed", { count: failed }) : "" }); const header = (
{running ? : } @@ -285,16 +284,17 @@ function AgentSingleCommand({ item, theme }: { item: AgentCommandItem; theme: (t } function AgentCommandEntry({ item, index, theme }: { item: AgentCommandItem; index: number; theme: (typeof canvasThemes)[keyof typeof canvasThemes] }) { + const { t } = useTranslation(); const [open, setOpen] = useState(false); const detailId = useId(); const view = userDetail(item.detail); const state = commandViewState(item.detail); - const status = state.failed ? "执行失败" : state.running ? "执行中" : "已完成"; + const status = t(state.failed ? "agent.message.failed" : state.running ? "agent.message.running" : "agent.message.completed"); const color = state.failed ? "#dc2626" : state.running ? "#d97706" : "#16a34a"; const content = ( <> {index + 1} - {item.text || "命令"} + {item.text || t("agent.message.command")} {state.running ? : state.failed ? : } @@ -350,6 +350,7 @@ function AgentPlanCard({ title, plan, theme }: { title: string; plan: PlanDetail } export function AgentWorkingMessage({ text, detail, status = "running", mcpStatuses = [], activityKey, theme }: { text: string; detail?: string; status?: "running" | "ready" | "error"; mcpStatuses?: Array<{ name: string; status: "running" | "ready" | "error"; detail: string }>; activityKey: string; theme: (typeof canvasThemes)[keyof typeof canvasThemes] }) { + const { t } = useTranslation(); const [elapsed, setElapsed] = useState(0); useEffect(() => { const startedAt = Date.now(); @@ -378,15 +379,15 @@ export function AgentWorkingMessage({ text, detail, status = "running", mcpStatu ))}
) : null} - {status === "running" && elapsed >= 30 ?
响应时间较长,但任务仍在运行。可以继续等待,或点击输入框右侧的停止按钮结束本轮。
: null} + {status === "running" && elapsed >= 30 ?
{t("agent.message.slowResponse")}
: null}
); } function waitingTime(seconds: number) { - if (seconds < 60) return `已等待 ${seconds} 秒`; + if (seconds < 60) return tr("waitingSeconds", { seconds }); const minutes = Math.floor(seconds / 60); - return `已等待 ${minutes} 分 ${seconds % 60} 秒`; + return tr("waitingMinutes", { minutes, seconds: seconds % 60 }); } function commandText(value: unknown) { @@ -406,6 +407,7 @@ type PlanDetail = { status: string; tasks: PlanTask[]; explanation?: string }; type UserDetail = { kind?: string; status?: string; rows?: Array<{ label: string; value: string }>; output?: string; files?: Array<{ path: string; action?: string }> }; function AgentDetailBlock({ detail, theme }: { detail: UserDetail; theme: (typeof canvasThemes)[keyof typeof canvasThemes] }) { + const { t } = useTranslation(); return (
{detail.rows?.length ? ( @@ -420,7 +422,7 @@ function AgentDetailBlock({ detail, theme }: { detail: UserDetail; theme: (typeo ) : null} {detail.files?.length ? (
-
涉及文件
+
{t("agent.message.files")}
{detail.files.map((file) => (
@@ -432,7 +434,7 @@ function AgentDetailBlock({ detail, theme }: { detail: UserDetail; theme: (typeo ) : null} {detail.output ? (
-
{detail.status === "failed" || detail.status === "error" ? "错误信息" : "运行输出"}
+
{t(detail.status === "failed" || detail.status === "error" ? "agent.message.errorInfo" : "agent.message.output")}
{detail.output}
) : null} @@ -441,6 +443,7 @@ function AgentDetailBlock({ detail, theme }: { detail: UserDetail; theme: (typeo } function AgentMessageAttachments({ attachments, alignRight }: { attachments: AgentChatAttachment[]; alignRight?: boolean }) { + const { t } = useTranslation(); const [previewUrl, setPreviewUrl] = useState(null); return ( <> @@ -450,7 +453,7 @@ function AgentMessageAttachments({ attachments, alignRight }: { attachments: Age key={item.id} src={item.url} alt={item.name} - title="点击查看大图" + title={t("agent.message.viewLarge")} className="size-10 cursor-zoom-in rounded-lg object-cover" draggable={false} onClick={() => setPreviewUrl(item.url)} @@ -459,7 +462,7 @@ function AgentMessageAttachments({ attachments, alignRight }: { attachments: Age
{previewUrl ? (
- 图片附件预览 !visible && setPreviewUrl(null) }} /> + {t("agent.message.attachmentPreview")} !visible && setPreviewUrl(null) }} />
) : null} @@ -470,12 +473,12 @@ function toolCardState(title: string, text: string, detail?: unknown) { const raw = `${title} ${text} ${normalizeText(objectField(detail, "error"))}`; const lower = raw.toLowerCase(); const status = String(objectField(detail, "status") || "").toLowerCase(); - if (status === "noop" || /未生效|无需|没有找到|没有.*可|已存在/.test(raw)) return { label: "未生效", color: "#d97706", icon: , isError: false }; - if (["declined", "rejected", "cancelled", "canceled"].includes(status) || /拒绝|取消/.test(raw)) return { label: "已取消", color: "#dc2626", icon: , isError: true }; - if (["failed", "error"].includes(status) || /失败|错误/.test(raw) || lower.includes("failed") || lower.includes("error")) return { label: "执行失败", color: "#dc2626", icon: , isError: true }; - if (["inprogress", "in_progress", "running", "started", "pending"].includes(status)) return { label: "进行中", color: "#d97706", icon: , isError: false }; - if (["completed", "succeeded", "success"].includes(status) || /完成|成功/.test(raw)) return { label: "已完成", color: "#16a34a", icon: , isError: false }; - return { label: "已记录", color: "#2563eb", icon: , isError: false }; + if (status === "noop" || /未生效|无需|没有找到|没有.*可|已存在/.test(raw)) return { label: tr("noEffect"), color: "#d97706", icon: , isError: false }; + if (["declined", "rejected", "cancelled", "canceled"].includes(status) || /拒绝|取消/.test(raw)) return { label: tr("canceled"), color: "#dc2626", icon: , isError: true }; + if (["failed", "error"].includes(status) || /失败|错误/.test(raw) || lower.includes("failed") || lower.includes("error")) return { label: tr("failed"), color: "#dc2626", icon: , isError: true }; + if (["inprogress", "in_progress", "running", "started", "pending"].includes(status)) return { label: tr("running"), color: "#d97706", icon: , isError: false }; + if (["completed", "succeeded", "success"].includes(status) || /完成|成功/.test(raw)) return { label: tr("completed"), color: "#16a34a", icon: , isError: false }; + return { label: tr("recorded"), color: "#2563eb", icon: , isError: false }; } function toolIcon(kind: string | undefined, fallback: ReactNode) { @@ -486,17 +489,17 @@ function toolIcon(kind: string | undefined, fallback: ReactNode) { } function planCardState(plan: PlanDetail, completed: number) { - if (plan.status === "failed") return { label: "执行失败", color: "#dc2626" }; - if (["interrupted", "cancelled", "canceled"].includes(plan.status)) return { label: "已停止", color: "#d97706" }; - if (completed === plan.tasks.length) return { label: "已完成", color: "#16a34a" }; - if (plan.status === "finished") return { label: "已结束", color: "#2563eb" }; - return { label: "进行中", color: "#d97706" }; + if (plan.status === "failed") return { label: tr("failed"), color: "#dc2626" }; + if (["interrupted", "cancelled", "canceled"].includes(plan.status)) return { label: tr("stopped"), color: "#d97706" }; + if (completed === plan.tasks.length) return { label: tr("completed"), color: "#16a34a" }; + if (plan.status === "finished") return { label: tr("finished"), color: "#2563eb" }; + return { label: tr("running"), color: "#d97706" }; } function planTaskState(status: string, muted: string) { - if (status === "completed") return { label: "已完成", color: "#16a34a", icon: }; - if (status === "inProgress") return { label: "进行中", color: "#d97706", icon: }; - return { label: "待处理", color: muted, icon: }; + if (status === "completed") return { label: tr("completed"), color: "#16a34a", icon: }; + if (status === "inProgress") return { label: tr("running"), color: "#d97706", icon: }; + return { label: tr("pending"), color: muted, icon: }; } function planDetail(value: unknown): PlanDetail | null { @@ -546,3 +549,7 @@ function normalizeText(value: unknown) { function objectField(value: unknown, key: string) { return value && typeof value === "object" ? (value as Record)[key] : undefined; } + +function tr(key: string, options?: Record) { + return i18n.t(`agent.message.${key}`, options); +} diff --git a/web/src/components/agent/agent-chat.tsx b/web/src/components/agent/agent-chat.tsx index 8f20599..1ad8129 100644 --- a/web/src/components/agent/agent-chat.tsx +++ b/web/src/components/agent/agent-chat.tsx @@ -1,5 +1,6 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { motion, useSpring, useTransform } from "motion/react"; +import { useTranslation } from "react-i18next"; import { canvasThemes } from "@/lib/canvas-theme"; import { summarizeCanvasAgentOps } from "@/lib/canvas/canvas-agent-ops"; @@ -30,6 +31,7 @@ export function AgentChatTimeline({ onApproveTool: () => void; onApprovalDecision: (approval: AgentPendingApproval, decision: "accept" | "acceptForSession" | "decline") => void; }) { + const { t } = useTranslation(); const messages = useAgentStore((state) => state.messages); const bootstrapStatus = useAgentStore((state) => state.bootstrapStatus); const mcpStartupStatuses = useAgentStore((state) => state.mcpStartupStatuses); @@ -94,18 +96,19 @@ export function AgentChatTimeline({
{showScrollToBottom ? ( - scrollToBottom()} /> + scrollToBottom()} /> ) : null} ); } export function AgentTaskProgress({ theme, busy }: { theme: (typeof canvasThemes)[keyof typeof canvasThemes]; busy: boolean }) { + const { t } = useTranslation(); const plan = useAgentStore((state) => busy ? currentPlanMessage(state.messages) : latestPlanMessage(state.messages)); if (!plan) return null; return (
- +
); } @@ -159,12 +162,13 @@ function isCommandMessage(item: AgentChatItem) { } export function AgentUsageBar({ usage, theme }: { usage: AgentTokenUsage; theme: (typeof canvasThemes)[keyof typeof canvasThemes] }) { + const { t } = useTranslation(); return (
- 最新调用 - - - + {t("agent.chat.latestCall")} + + +
); } diff --git a/web/src/components/agent/agent-connect-view.tsx b/web/src/components/agent/agent-connect-view.tsx index b1cf4bd..e0d5055 100644 --- a/web/src/components/agent/agent-connect-view.tsx +++ b/web/src/components/agent/agent-connect-view.tsx @@ -2,13 +2,10 @@ import { Fragment } from "react"; import { App, Button, Input, Tooltip } from "antd"; import copyToClipboard from "copy-to-clipboard"; import { Copy, KeyRound, Link2, PlugZap } from "lucide-react"; +import { useTranslation } from "react-i18next"; import { canvasThemes } from "@/lib/canvas-theme"; -const AGENT_CONNECT_STEPS = [ - { title: "方式一:在 Codex 中使用插件", text: "在 Codex app 安装 Infinite Canvas 插件后,通过插件启动画布,插件会自动启动本地 Agent 并带上连接信息。" }, - { title: "方式二:直接运行 Agent", text: "不使用 Codex 插件时,在终端运行下面命令,再回到网页里连接或手动填入 Local URL 和 Connect token。", command: "npx -y @basketikun/canvas-agent" }, -]; const AGENT_PLUGIN_REMOVE_COMMAND = "codex plugin remove infinite-canvas"; const AGENT_MCP_REMOVE_COMMAND = "codex mcp remove infinite-canvas"; @@ -35,30 +32,32 @@ export function AgentConnectView({ onTokenChange: (value: string) => void; onToggleEnabled: () => void; }) { + const { t } = useTranslation(); const { message } = App.useApp(); - const statusText = connectError ? "连接失败" : connected ? activity : enabled ? "连接中" : "未连接"; + const steps = [{ title: t("agent.connect.pluginTitle"), text: t("agent.connect.pluginText") }, { title: t("agent.connect.directTitle"), text: t("agent.connect.directText"), command: "npx -y @basketikun/canvas-agent" }]; + const statusText = connectError ? t("agent.status.failed") : connected ? activity : enabled ? t("agent.status.connecting") : t("agent.status.disconnected"); const statusColor = connectError ? "#dc2626" : connected ? "#16a34a" : enabled ? "#d97706" : theme.node.muted; const copyCommand = (command: string) => { copyToClipboard(command); - message.success("命令已复制"); + message.success(t("agent.connect.commandCopied")); }; const codexPluginReminder = (
- Codex 插件提醒 + {t("agent.connect.pluginReminder")}
-
只有安装 Codex 插件或手动添加 MCP 后,工具列表才会进入 Codex 上下文并增加 token 消耗;仅运行 `npx -y @basketikun/canvas-agent` 启动本地 Agent 不会安装 MCP。
+
{t("agent.connect.pluginReminderText")}
{[ - ["移除插件", AGENT_PLUGIN_REMOVE_COMMAND], - ["移除手动 MCP", AGENT_MCP_REMOVE_COMMAND], + [t("agent.connect.removePlugin"), AGENT_PLUGIN_REMOVE_COMMAND], + [t("agent.connect.removeMcp"), AGENT_MCP_REMOVE_COMMAND], ].map(([label, command]) => (
{label} {command} - +
@@ -70,13 +69,13 @@ export function AgentConnectView({
-
连接本地 Agent
+
{t("agent.connect.title")}
- 按使用场景选择一种连接方式。 + {t("agent.connect.description")}
- {AGENT_CONNECT_STEPS.map((step, index) => { + {steps.map((step, index) => { const command = "command" in step ? step.command : ""; return ( @@ -88,7 +87,7 @@ export function AgentConnectView({ {command ? (
{command} - +
@@ -103,7 +102,7 @@ export function AgentConnectView({
- 网页连接 + {t("agent.connect.webConnection")}
- 默认自动读取 Local URL 和 Connect token,失败时再手动填写。 + {t("agent.connect.autoDiscover")}
{connectError ? ( @@ -154,4 +153,3 @@ export function AgentConnectView({
); } - diff --git a/web/src/components/agent/agent-event-formatters.ts b/web/src/components/agent/agent-event-formatters.ts index ee4223d..b012116 100644 --- a/web/src/components/agent/agent-event-formatters.ts +++ b/web/src/components/agent/agent-event-formatters.ts @@ -1,9 +1,10 @@ import { isSiteTool, SITE_TOOL_LABELS } from "@/lib/agent/agent-site-tools"; +import i18n from "@/i18n"; import { summarizeCanvasAgentOps, type CanvasAgentOp } from "@/lib/canvas/canvas-agent-ops"; import { randomId } from "@/lib/utils"; import { useAgentStore, type AgentAttachment, type AgentChatItem, type AgentEventLog, type AgentTokenUsage } from "@/stores/use-agent-store"; import type { AgentChatAttachment } from "./agent-chat-message"; -export const REASONING_PLACEHOLDER = "正在分析任务…"; +export const REASONING_PLACEHOLDER = i18n.t("agent.events.analyzing"); export type AgentEventPayload = { agent?: string; @@ -78,33 +79,33 @@ export function formatAgentActivity(event: AgentEventPayload): Omit item.status === "completed").length; return { role: "tool", - title: "任务进度", - text: `已完成 ${completed}/${tasks.length} 项`, + title: tr("progress"), + text: tr("progressCount", { completed, total: tasks.length }), detail: { kind: "todo", status: completed === tasks.length ? "completed" : "inProgress", tasks, explanation: stringText(event.explanation) }, }; } @@ -148,14 +149,14 @@ export function turnPlanStatus(detail: unknown, turnStatus?: string) { } export function activityDeltaFallback(item: AgentEventItem, delta: string): AgentChatItem { - if (item.type === "command_execution") return { id: item.id || randomId(), role: "tool", title: "执行命令", text: activityPlaceholder(item.type), detail: { kind: "command", status: "inProgress", output: delta } }; - return { id: item.id || randomId(), role: "tool", title: item.type === "plan" ? "执行计划" : "思考摘要", text: delta, detail: { kind: activityKind(item.type), status: "inProgress" } }; + if (item.type === "command_execution") return { id: item.id || randomId(), role: "tool", title: tr("executeCommand"), text: activityPlaceholder(item.type), detail: { kind: "command", status: "inProgress", output: delta } }; + return { id: item.id || randomId(), role: "tool", title: item.type === "plan" ? tr("plan") : tr("reasoning"), text: delta, detail: { kind: activityKind(item.type), status: "inProgress" } }; } export function activityPlaceholder(type?: string) { - if (type === "plan") return "正在整理执行步骤…"; - if (type === "command_execution") return "正在执行命令…"; - return REASONING_PLACEHOLDER; + if (type === "plan") return tr("planning"); + if (type === "command_execution") return tr("executingCommand"); + return tr("analyzing"); } export function activityKind(type?: string) { @@ -170,7 +171,7 @@ export function activityDetail(value: unknown, kind: string, status: string): Ag } function commandActivityDetail(item: AgentEventItem, status: string): AgentUserDetail { - const rows = [detailRow("工作目录", item.cwd), detailRow("退出状态", item.exitCode), durationDetailRow(item.durationMs)].flatMap((row) => (row ? [row] : [])); + const rows = [detailRow(tr("workingDirectory"), item.cwd), detailRow(tr("exitStatus"), item.exitCode), durationDetailRow(item.durationMs)].flatMap((row) => (row ? [row] : [])); const commandStatus = typeof item.exitCode === "number" && item.exitCode !== 0 ? "failed" : status; return { kind: "command", status: commandStatus, rows, output: item.error?.message || stringText(item.aggregatedOutput) }; } @@ -183,23 +184,23 @@ function activityFiles(value: unknown) { } function fileActivitySummary(files: Array<{ path: string; action?: string }>, completed: boolean) { - if (!files.length) return completed ? "已完成文件修改" : "正在准备文件修改…"; - if (files.length === 1) return `${completed ? "已" : "正在"}${files[0].action || "修改"} ${files[0].path}`; - const names = files.slice(0, 3).map((file) => file.path).join("、"); - return `${completed ? "已修改" : "正在修改"} ${files.length} 个文件:${names}${files.length > 3 ? " 等" : ""}`; + if (!files.length) return tr(completed ? "filesCompleted" : "preparingFiles"); + if (files.length === 1) return tr(completed ? "fileCompleted" : "editingFile", { action: files[0].action || tr("edit"), path: files[0].path }); + const names = files.slice(0, 3).map((file) => file.path).join(", "); + return tr(completed ? "filesEdited" : "editingFiles", { count: files.length, names, more: files.length > 3 ? tr("andMore") : "" }); } function webSearchSummary(item: AgentEventItem) { const action = item.action; const type = stringText(objectField(action, "type")); - if (type === "openPage") return `打开网页:${stringText(objectField(action, "url"))}`; - if (type === "findInPage") return `在网页中查找“${stringText(objectField(action, "pattern")) || "内容"}”`; - return `搜索:${stringText(item.query) || stringText(objectField(action, "query")) || "相关资料"}`; + if (type === "openPage") return tr("openPage", { url: stringText(objectField(action, "url")) }); + if (type === "findInPage") return tr("findInPage", { pattern: stringText(objectField(action, "pattern")) || tr("content") }); + return tr("search", { query: stringText(item.query) || stringText(objectField(action, "query")) || tr("relatedInfo") }); } function webSearchDetailRows(item: AgentEventItem) { const action = item.action; - return [detailRow("关键词", item.query || objectField(action, "query")), detailRow("网页", objectField(action, "url"))].flatMap((row) => (row ? [row] : [])); + return [detailRow(tr("keyword"), item.query || objectField(action, "query")), detailRow(tr("webpage"), objectField(action, "url"))].flatMap((row) => (row ? [row] : [])); } function readableText(value: unknown): string { @@ -215,13 +216,13 @@ function detailRow(label: string, value: unknown) { function durationDetailRow(value: unknown) { const duration = Number(value || 0); - return duration > 0 ? { label: "耗时", value: `${(duration / 1000).toFixed(1)} 秒` } : null; + return duration > 0 ? { label: tr("duration"), value: tr("seconds", { value: (duration / 1000).toFixed(1) }) } : null; } function changeAction(value: unknown) { - if (value === "add") return "新增"; - if (value === "delete") return "删除"; - return "修改"; + if (value === "add") return tr("add"); + if (value === "delete") return tr("delete"); + return tr("edit"); } export function parseEventData(event: Event) { @@ -252,13 +253,13 @@ export function registerLiveAgentTurn( export function formatLogText(logs: AgentEventLog[], context: AgentLogContext) { const head = [ - "Infinite Canvas Agent 诊断", - `地址:${context.endpoint}`, - `连接:${context.connected ? "在线" : context.enabled ? "连接中" : "未启用"} · 状态:${context.activity}`, - `消息:${context.messages} · 工具:${context.pendingTool ? toolName(context.pendingTool) : "无"}`, + tr("diagnostics"), + tr("address", { endpoint: context.endpoint }), + tr("connection", { connection: tr(context.connected ? "online" : context.enabled ? "connecting" : "disabled"), status: context.activity }), + tr("messageTool", { messages: context.messages, tool: context.pendingTool ? toolName(context.pendingTool) : tr("none") }), ].join("\n"); const body = logs.map((item) => `${item.time} ${item.title}${item.text && item.text !== item.title ? ` · ${item.text}` : ""}`).join("\n"); - return [head, body || "暂无事件日志"].join("\n\n"); + return [head, body || tr("noLogs")].join("\n\n"); } export function formatLogJson(logs: AgentEventLog[], context: AgentLogContext) { @@ -267,29 +268,29 @@ export function formatLogJson(logs: AgentEventLog[], context: AgentLogContext) { export function formatAgentEventLog(event: AgentEventPayload) { const item = event.item; - if (event.type === "thread.started") return { title: "创建会话", text: shortId(event.thread_id) }; - if (event.type === "turn.started") return { title: "开始处理", text: shortId(event.turn_id) }; + if (event.type === "thread.started") return { title: tr("threadCreated"), text: shortId(event.thread_id) }; + if (event.type === "turn.started") return { title: tr("turnStarted"), text: shortId(event.turn_id) }; if (event.type === "plan.updated") { const tasks = planTasks(event.plan); - return { title: "更新任务进度", text: `已完成 ${tasks.filter((item) => item.status === "completed").length}/${tasks.length} 项` }; + return { title: tr("progressUpdated"), text: tr("progressCount", { completed: tasks.filter((item) => item.status === "completed").length, total: tasks.length }) }; } - if (event.type === "turn.completed" && event.status === "failed") return { title: "处理失败", text: agentErrorView(event.error?.message).text }; - if (event.type === "turn.completed") return { title: event.status === "interrupted" ? "处理已停止" : "处理完成", text: turnSummary(event) }; - if (event.type === "turn.failed" || event.type === "error") return { title: "处理失败", text: agentErrorView(event.message || event.error?.message).text }; - if (event.type === "item.started" && isMcpToolItem(item)) return { title: "调用工具", text: toolName(String(item?.tool || "")) }; - if (event.type === "item.completed" && isMcpToolItem(item)) return { title: item.error ? "工具失败" : "工具完成", text: `${toolName(String(item?.tool || ""))}${item.error?.message ? ` · ${item.error.message}` : ""}` }; - if (event.type === "item.completed" && item?.type === "agent_message") return { title: "收到回复", text: compactText(stringText(item.text)) }; + if (event.type === "turn.completed" && event.status === "failed") return { title: tr("turnFailed"), text: agentErrorView(event.error?.message).text }; + if (event.type === "turn.completed") return { title: tr(event.status === "interrupted" ? "turnStopped" : "turnCompleted"), text: turnSummary(event) }; + if (event.type === "turn.failed" || event.type === "error") return { title: tr("turnFailed"), text: agentErrorView(event.message || event.error?.message).text }; + if (event.type === "item.started" && isMcpToolItem(item)) return { title: tr("toolCalled"), text: toolName(String(item?.tool || "")) }; + if (event.type === "item.completed" && isMcpToolItem(item)) return { title: tr(item.error ? "toolFailed" : "toolCompleted"), text: `${toolName(String(item?.tool || ""))}${item.error?.message ? ` · ${item.error.message}` : ""}` }; + if (event.type === "item.completed" && item?.type === "agent_message") return { title: tr("replyReceived"), text: compactText(stringText(item.text)) }; return null; } function turnSummary(event: AgentEventPayload) { - return event.duration_ms ? `${(event.duration_ms / 1000).toFixed(1)} 秒` : "完成"; + return event.duration_ms ? tr("seconds", { value: (event.duration_ms / 1000).toFixed(1) }) : tr("completed"); } export function agentErrorView(value: unknown) { const text = normalizeText(value); - if (/selected model is at capacity/i.test(text)) return { title: "模型暂时繁忙", text: "当前选择的模型请求量过大,暂时无法处理。请稍后重试,或切换其他模型后再试。" }; - return { title: "任务失败", text: text || "Codex 未能完成本次任务,请稍后重试。" }; + if (/selected model is at capacity/i.test(text)) return { title: tr("modelBusy"), text: tr("modelBusyDescription") }; + return { title: tr("taskFailed"), text: text || tr("taskFailedDescription") }; } export function eventUsage(event: AgentEventPayload): AgentTokenUsage { @@ -314,53 +315,33 @@ export function isConnectionErrorMessage(item: AgentChatItem) { } export function toolName(name: string) { - if (name === "imagegen" || name.endsWith("__imagegen")) return "生成图片"; - if (name === "view_image" || name.endsWith("__view_image")) return "查看图片"; - if (name === "exec" || name === "exec_command" || name.endsWith("__exec_command")) return "执行命令"; - if (name === "apply_patch" || name.endsWith("__apply_patch")) return "修改文件"; - if (name === "web__run" || name.endsWith("__web__run")) return "搜索资料"; - if (name === "canvas_apply_ops") return "画布操作"; - if (name === "canvas_get_state") return "读取画布"; - if (name === "canvas_get_selection") return "读取选区"; - if (name === "canvas_export_snapshot") return "导出快照"; - if (name === "canvas_create_node") return "创建节点"; - if (name === "canvas_create_attachment_nodes") return "添加附件图片"; - if (name === "canvas_create_text_node") return "创建文本"; - if (name === "canvas_create_text_nodes") return "批量创建文本"; - if (name === "canvas_create_config_node") return "创建生成配置"; - if (name === "canvas_create_image_prompt_flow") return "创建生图流程"; - if (name === "canvas_create_generation_flow") return "创建生成流程"; - if (name === "canvas_generate_text") return "生成文本"; - if (name === "canvas_generate_image") return "生成图片"; - if (name === "canvas_generate_video") return "生成视频"; - if (name === "canvas_generate_audio") return "生成音频"; - if (name === "canvas_update_node") return "更新节点"; - if (name === "canvas_update_node_text") return "更新文本"; - if (name === "canvas_move_nodes") return "移动节点"; - if (name === "canvas_resize_node") return "调整节点尺寸"; - if (name === "canvas_delete_nodes") return "删除节点"; - if (name === "canvas_connect_nodes") return "连接节点"; - if (name === "canvas_select_nodes") return "选择节点"; - if (name === "canvas_set_viewport") return "调整视口"; - if (name === "canvas_run_generation") return "触发生成"; - if (name === "site_navigate") return "打开页面"; + if (name === "imagegen" || name.endsWith("__imagegen")) return tr("tools.generateImage"); + if (name === "view_image" || name.endsWith("__view_image")) return tr("tools.viewImage"); + if (name === "exec" || name === "exec_command" || name.endsWith("__exec_command")) return tr("tools.executeCommand"); + if (name === "apply_patch" || name.endsWith("__apply_patch")) return tr("tools.editFiles"); + if (name === "web__run" || name.endsWith("__web__run")) return tr("tools.searchWeb"); + if (toolTranslationKeys[name]) return tr(`tools.${toolTranslationKeys[name]}`); if (isSiteTool(name)) return SITE_TOOL_LABELS[name]; - return name ? `调用工具:${name}` : "工具操作"; + return name ? tr("toolNamed", { name }) : tr("toolOperation"); } +const toolTranslationKeys: Record = { + canvas_apply_ops: "canvasOps", canvas_get_state: "readCanvas", canvas_get_selection: "readSelection", canvas_export_snapshot: "exportSnapshot", canvas_create_node: "createNode", canvas_create_attachment_nodes: "addAttachments", canvas_create_text_node: "createText", canvas_create_text_nodes: "createTexts", canvas_create_config_node: "createConfig", canvas_create_image_prompt_flow: "createImageFlow", canvas_create_generation_flow: "createGenerationFlow", canvas_generate_text: "generateText", canvas_generate_image: "generateImage", canvas_generate_video: "generateVideo", canvas_generate_audio: "generateAudio", canvas_update_node: "updateNode", canvas_update_node_text: "updateText", canvas_move_nodes: "moveNodes", canvas_resize_node: "resizeNode", canvas_delete_nodes: "deleteNodes", canvas_connect_nodes: "connectNodes", canvas_select_nodes: "selectNodes", canvas_set_viewport: "setViewport", canvas_run_generation: "runGeneration", site_navigate: "openPage", +}; + function siteToolSummary(name: string, result: unknown, input: unknown) { const data = result && typeof result === "object" ? (result as Record) : {}; - if (name === "site_navigate") return `已打开${routeName(stringText(objectField(input, "path")) || "/")}`; - if (name === "canvas_list_projects") return `共 ${numberField(data, "total")} 个画布`; - if (name === "prompts_search") return `找到 ${numberField(data, "total")} 条提示词`; - if (name === "assets_list") return `共 ${numberField(data, "total")} 个资产`; - if (name === "assets_add") return "已加入我的素材"; + if (name === "site_navigate") return tr("openedRoute", { route: routeName(stringText(objectField(input, "path")) || "/") }); + if (name === "canvas_list_projects") return tr("canvasCount", { count: numberField(data, "total") }); + if (name === "prompts_search") return tr("promptCount", { count: numberField(data, "total") }); + if (name === "assets_list") return tr("assetCount", { count: numberField(data, "total") }); + if (name === "assets_add") return tr("assetAdded"); if (name === "generation_get_status") { const summary = data.summary && typeof data.summary === "object" ? (data.summary as Record) : {}; - return `共 ${numberField(data, "total")} 个任务,排队 ${numberField(summary, "queued")},运行中 ${numberField(summary, "running")},成功 ${numberField(summary, "succeeded")},失败 ${numberField(summary, "failed")}`; + return tr("generationStatus", { total: numberField(data, "total"), queued: numberField(summary, "queued"), running: numberField(summary, "running"), succeeded: numberField(summary, "succeeded"), failed: numberField(summary, "failed") }); } - if (name === "workbench_image_generate" || name === "workbench_video_generate") return typeof data.note === "string" ? data.note : "已在工作台执行"; - if (name === "workbench_image_get_config" || name === "workbench_video_get_config") return "已读取工作台配置"; + if (name === "workbench_image_generate" || name === "workbench_video_generate") return typeof data.note === "string" ? data.note : tr("workbenchExecuted"); + if (name === "workbench_image_get_config" || name === "workbench_video_get_config") return tr("workbenchConfigRead"); return ""; } @@ -379,11 +360,11 @@ export function toolCallDetail(name: string, input: unknown, status: string, err function toolInputRows(name: string, input: unknown) { input = parseToolArguments(input); - if (name === "site_navigate") return [detailRow("目标页面", routeName(stringText(objectField(input, "path")) || "/"))].flatMap((row) => (row ? [row] : [])); - if (name === "prompts_search") return [detailRow("搜索内容", objectField(input, "query"))].flatMap((row) => (row ? [row] : [])); - if (name === "canvas_create_text_node") return [detailRow("文本内容", objectField(input, "text"))].flatMap((row) => (row ? [row] : [])); - if (name === "canvas_apply_ops") return [detailRow("操作内容", summarizeCanvasAgentOps((objectField(input, "ops") as CanvasAgentOp[] | undefined) || []))].flatMap((row) => (row ? [row] : [])); - if (name === "canvas_create_attachment_nodes") return [detailRow("图片数量", Array.isArray(objectField(input, "attachmentIds")) ? (objectField(input, "attachmentIds") as unknown[]).length : 0)].flatMap((row) => (row ? [row] : [])); + if (name === "site_navigate") return [detailRow(tr("targetPage"), routeName(stringText(objectField(input, "path")) || "/"))].flatMap((row) => (row ? [row] : [])); + if (name === "prompts_search") return [detailRow(tr("searchContent"), objectField(input, "query"))].flatMap((row) => (row ? [row] : [])); + if (name === "canvas_create_text_node") return [detailRow(tr("textContent"), objectField(input, "text"))].flatMap((row) => (row ? [row] : [])); + if (name === "canvas_apply_ops") return [detailRow(tr("operationContent"), summarizeCanvasAgentOps((objectField(input, "ops") as CanvasAgentOp[] | undefined) || []))].flatMap((row) => (row ? [row] : [])); + if (name === "canvas_create_attachment_nodes") return [detailRow(tr("imageCount"), Array.isArray(objectField(input, "attachmentIds")) ? (objectField(input, "attachmentIds") as unknown[]).length : 0)].flatMap((row) => (row ? [row] : [])); return []; } @@ -395,8 +376,8 @@ export function toolSummary(item?: AgentEventItem) { const connectionField = objectField(result, "connections"); const nodes = Array.isArray(nodeField) ? nodeField : []; const connections = Array.isArray(connectionField) ? connectionField : []; - if (name === "canvas_get_state") return Array.isArray(nodeField) || Array.isArray(connectionField) ? canvasContentSummary(nodes, connections.length) : "已读取当前画布内容"; - if (name === "canvas_get_selection") return "已读取当前选中内容"; + if (name === "canvas_get_state") return Array.isArray(nodeField) || Array.isArray(connectionField) ? canvasContentSummary(nodes, connections.length) : tr("canvasRead"); + if (name === "canvas_get_selection") return tr("selectionRead"); return ""; } @@ -409,33 +390,32 @@ function canvasContentSummary(nodes: unknown[], connections: number) { const known = new Set(["text", "image", "config", "video", "audio", "group"]); const other = Object.entries(counts).reduce((total, [type, count]) => total + (known.has(type) ? 0 : count), 0); const parts = [ - counts.text ? `${counts.text} 个文本` : "", - counts.image ? `${counts.image} 张图片` : "", - counts.config ? `${counts.config} 个配置` : "", - counts.video ? `${counts.video} 个视频` : "", - counts.audio ? `${counts.audio} 个音频` : "", - counts.group ? `${counts.group} 个分组` : "", - other ? `${other} 个其他节点` : "", - connections ? `${connections} 条连线` : "", + counts.text ? tr("textNodes", { count: counts.text }) : "", + counts.image ? tr("imageNodes", { count: counts.image }) : "", + counts.config ? tr("configNodes", { count: counts.config }) : "", + counts.video ? tr("videoNodes", { count: counts.video }) : "", + counts.audio ? tr("audioNodes", { count: counts.audio }) : "", + counts.group ? tr("groupNodes", { count: counts.group }) : "", + other ? tr("otherNodes", { count: other }) : "", + connections ? tr("connections", { count: connections }) : "", ].filter(Boolean); - return parts.length ? parts.join("、") : "当前画布为空"; + return parts.length ? parts.join(i18n.language === "zh-CN" ? "、" : ", ") : tr("emptyCanvas"); } export function toolAction(name: string) { const label = toolName(name); - if (label.startsWith("读取") || label.startsWith("查看") || label.startsWith("搜索") || label.startsWith("打开")) return label; - return `执行${label}`; + return tr("executeTool", { tool: label }); } export function routeName(path: string) { - if (path === "/") return "首页"; - if (path === "/canvas") return "画布页面"; - if (path.startsWith("/canvas/")) return "指定画布"; - if (path.startsWith("/image")) return "生图工作台"; - if (path.startsWith("/video")) return "视频工作台"; - if (path.startsWith("/prompts")) return "提示词中心"; - if (path.startsWith("/assets")) return "我的素材"; - if (path.startsWith("/config")) return "配置页面"; + if (path === "/") return tr("routes.home"); + if (path === "/canvas") return tr("routes.canvas"); + if (path.startsWith("/canvas/")) return tr("routes.canvasProject"); + if (path.startsWith("/image")) return tr("routes.image"); + if (path.startsWith("/video")) return tr("routes.video"); + if (path.startsWith("/prompts")) return tr("routes.prompts"); + if (path.startsWith("/assets")) return tr("routes.assets"); + if (path.startsWith("/config")) return tr("routes.config"); return path; } @@ -443,10 +423,10 @@ export function workingActivity(item?: AgentChatItem) { const status = String(objectField(item?.detail, "status") || ""); const output = stringText(objectField(item?.detail, "output")); const key = `${item?.id || "waiting"}-${status}-${item?.text || ""}-${output.length}`; - if (item?.role !== "tool") return { key, text: "正在思考..." }; - if (["inProgress", "in_progress", "running", "pending"].includes(status)) return { key, text: `${item.title || "工具操作"}正在进行...` }; - if (item.title === "读取画布") return { key, text: "画布已读取,Codex 正在整理结果..." }; - return { key, text: `${item.title || "工具操作"}已完成,Codex 正在继续处理...` }; + if (item?.role !== "tool") return { key, text: tr("thinking") }; + if (["inProgress", "in_progress", "running", "pending"].includes(status)) return { key, text: tr("operationRunning", { operation: item.title || tr("toolOperation") }) }; + if (item.title === toolName("canvas_get_state")) return { key, text: tr("organizingCanvas") }; + return { key, text: tr("operationCompleted", { operation: item.title || tr("toolOperation") }) }; } export function currentPlanMessage(messages: AgentChatItem[]) { @@ -503,7 +483,7 @@ function numberField(value: unknown, key: string) { } export function promptWithAttachments(text: string, attachments: AgentAttachment[]) { - return text || (attachments.length ? "请处理上传的图片附件。" : ""); + return text || (attachments.length ? tr("attachmentPrompt") : ""); } export function attachmentPayloadBytes(attachments: AgentAttachment[]) { @@ -615,3 +595,9 @@ export function mergeStreamText(prefix: string, incoming: string) { if (!incoming || prefix.startsWith(incoming)) return prefix; return incoming.length >= prefix.length ? incoming : prefix; } + +function tr(key: string, options?: Record) { + const path = `agent.events.${key}`; + const extraPath = `agent.eventExtra.${key}`; + return i18n.t(i18n.exists(path) ? path : i18n.exists(extraPath) ? extraPath : `agent.eventMore.${key}`, options); +} diff --git a/web/src/components/agent/agent-history-view.tsx b/web/src/components/agent/agent-history-view.tsx index 0918a2a..eb5ffe7 100644 --- a/web/src/components/agent/agent-history-view.tsx +++ b/web/src/components/agent/agent-history-view.tsx @@ -1,6 +1,7 @@ import { useState } from "react"; import { Button, Checkbox } from "antd"; import { FolderOpen, Plus, RefreshCw, Trash2 } from "lucide-react"; +import { useTranslation } from "react-i18next"; import { canvasThemes } from "@/lib/canvas-theme"; import type { AgentThreadSummary } from "@/stores/use-agent-store"; @@ -30,6 +31,7 @@ export function AgentHistoryView({ onResumeThread: (threadId: string) => void; onDeleteThreads: (threadIds: string[]) => void; }) { + const { i18n, t } = useTranslation(); const [selectedIds, setSelectedIds] = useState(() => new Set()); const selectedThreads = threads.filter((thread) => selectedIds.has(thread.id)); const allSelected = Boolean(threads.length) && selectedThreads.length === threads.length; @@ -46,27 +48,27 @@ export function AgentHistoryView({
- 工作空间 + {t("agent.history.workspace")} - {workspacePath || "默认画布目录"} + {workspacePath || t("agent.history.defaultWorkspace")}
{threads.length ? setSelectedIds(allSelected ? new Set() : new Set(threads.map((thread) => thread.id)))} /> : null} - {selectedThreads.length ? `已选 ${selectedThreads.length} 条` : threads.length ? `${threads.length} 条历史` : connected ? "暂无历史" : "未连接"} + {selectedThreads.length ? t("agent.history.selected", { count: selectedThreads.length }) : threads.length ? t("agent.history.count", { count: threads.length }) : connected ? t("agent.history.empty") : t("agent.status.disconnected")}
{selectedThreads.length ? ( ) : null}
@@ -94,7 +96,7 @@ export function AgentHistoryView({ event.stopPropagation()} onKeyDown={(event) => event.stopPropagation()} onChange={() => toggleThread(thread.id)} @@ -103,21 +105,21 @@ export function AgentHistoryView({
{active ? ( - 当前 + {t("agent.history.current")} ) : null} -
{thread.name || thread.preview || "未命名对话"}
+
{thread.name || thread.preview || t("agent.history.untitled")}
{thread.preview || thread.id}
- {formatThreadTime(thread.updatedAt || thread.createdAt)} + {formatThreadTime(thread.updatedAt || thread.createdAt, i18n.language)}
); })} {!threads.length ? (
- {connected ? "当前工作空间还没有对话记录" : "连接本地 Agent 后显示历史记录"} + {t(connected ? "agent.history.noWorkspaceThreads" : "agent.history.connectHint")}
) : null}
@@ -126,8 +128,7 @@ export function AgentHistoryView({ ); } -function formatThreadTime(value?: number) { +function formatThreadTime(value: number | undefined, locale: string) { if (!value) return ""; - return new Date(value * 1000).toLocaleString(); + return new Date(value * 1000).toLocaleString(locale); } - diff --git a/web/src/components/agent/agent-log-view.tsx b/web/src/components/agent/agent-log-view.tsx index e5188cb..a9ab314 100644 --- a/web/src/components/agent/agent-log-view.tsx +++ b/web/src/components/agent/agent-log-view.tsx @@ -2,7 +2,9 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Button, Segmented, Tooltip } from "antd"; import copyToClipboard from "copy-to-clipboard"; import { CheckCircle2, ChevronDown, CircleAlert, CircleDot, Copy, Trash2, TriangleAlert } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import i18n from "@/i18n"; import { canvasThemes } from "@/lib/canvas-theme"; import type { AgentEventLog } from "@/stores/use-agent-store"; import { formatLogJson, formatLogText, type AgentLogContext } from "./agent-event-formatters"; @@ -27,6 +29,7 @@ export function AgentLogView({ onCopied: (text: string) => void; onCopyBlocked: (text: string) => void; }) { + const { t } = useTranslation(); const [mode, setMode] = useState<"text" | "json">("text"); const [filter, setFilter] = useState("all"); const textareaRef = useRef(null); @@ -93,28 +96,28 @@ export function AgentLogView({ }); return () => cancelAnimationFrame(frame); }, [mode, scrollToBottom, updateScrollState, visibleLogCount]); - const copy = async (value = content, tip = "日志已复制") => { + const copy = async (value = content, tip = t("agent.logs.copied")) => { if (await copyToClipboard(value)) { onCopied(tip); return; } textareaRef.current?.focus(); textareaRef.current?.select(); - onCopyBlocked(mode === "json" ? "已选中日志,请手动复制" : "复制失败,请切换到原始 JSON 后手动复制"); + onCopyBlocked(t(mode === "json" ? "agent.logs.selectedManual" : "agent.logs.copyFailed")); }; - const connectionLabel = context.connected ? "在线" : context.enabled ? "连接中" : "未启用"; + const connectionLabel = t(context.connected ? "agent.events.online" : context.enabled ? "agent.events.connecting" : "agent.events.disabled"); return (
-
运行日志
+
{t("agent.logs.title")}
setMode(value as "text" | "json")} options={[ - { label: "排查日志", value: "text" }, - { label: "原始 JSON", value: "json" }, + { label: t("agent.logs.diagnostics"), value: "text" }, + { label: t("agent.logs.rawJson"), value: "json" }, ]} />
@@ -134,8 +137,8 @@ export function AgentLogView({
-
{context.messages} 条消息
-
{context.pendingTool ? `工具:${context.pendingTool}` : "无待处理工具"}
+
{t("agent.logs.messages", { count: context.messages })}
+
{context.pendingTool ? t("agent.logs.tool", { tool: context.pendingTool }) : t("agent.logs.noPendingTool")}
@@ -148,30 +151,30 @@ export function AgentLogView({ value={filter} onChange={(value) => setFilter(value as LogFilter)} options={[ - { label: `全部 ${counts.all}`, value: "all" }, - { label: `错误 ${counts.error}`, value: "error" }, - { label: `警告 ${counts.warning}`, value: "warning" }, - { label: `信息 ${counts.info}`, value: "info" }, + { label: t("agent.logs.all", { count: counts.all }), value: "all" }, + { label: t("agent.logs.errors", { count: counts.error }), value: "error" }, + { label: t("agent.logs.warnings", { count: counts.warning }), value: "warning" }, + { label: t("agent.logs.info", { count: counts.info }), value: "info" }, ]} /> void copy(value, tip)} context={context} />
-
+
{visibleLogs.map((item, index) => ( ))} {!visibleLogs.length ? (
- {logs.length ? "当前筛选下没有日志" : "暂无事件日志"} + {t(logs.length ? "agent.logs.noFiltered" : "agent.logs.empty")}
) : null}
{showScrollToBottom ? ( scrollToBottom()} /> @@ -182,7 +185,7 @@ export function AgentLogView({ <>
- 完整诊断数据 · {logs.length} 条 + {t("agent.logs.fullData", { count: logs.length })} void copy(value, tip)} context={context} />
@@ -202,22 +205,24 @@ export function AgentLogView({ } function LogActions({ logs, lastError, context, onClear, onCopy }: { logs: AgentEventLog[]; lastError?: AgentEventLog; context: AgentLogContext; onClear: () => void; onCopy: (value?: string, tip?: string) => void }) { + const { t } = useTranslation(); return (
- -
); } function LogRow({ item, theme, onToggle }: { item: DisplayLog; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; onToggle?: (open: boolean) => void }) { + const { t } = useTranslation(); const tone = item.level === "error" ? "text-red-600 dark:text-red-400" : item.level === "warning" ? "text-amber-600 dark:text-amber-400" : item.success ? "text-emerald-600 dark:text-emerald-400" : ""; const Icon = item.level === "error" ? CircleAlert : item.level === "warning" ? TriangleAlert : item.success ? CheckCircle2 : CircleDot; return ( @@ -233,7 +238,7 @@ function LogRow({ item, theme, onToggle }: { item: DisplayLog; theme: (typeof ca {item.title} {item.count > 1 ? ( - 重复 {item.count} 次 + {t("agent.logs.repeated", { count: item.count })} ) : null}
@@ -248,7 +253,7 @@ function LogRow({ item, theme, onToggle }: { item: DisplayLog; theme: (typeof ca
- 详细信息 + {t("agent.logs.details")}
                     {item.detail}
@@ -323,13 +328,13 @@ function declaredLogLevel(value: unknown): DisplayLog["level"] | "" {
 }
 
 function logTitle(fallback: string, value: unknown) {
-    if (fallback !== "日志" || !value || typeof value !== "object" || Array.isArray(value)) return fallback;
+    if (fallback !== "日志" && fallback !== "Log" || !value || typeof value !== "object" || Array.isArray(value)) return fallback;
     const target = String((value as Record).target || "").toLowerCase();
-    if (target.includes("skill")) return "技能加载";
-    if (target.includes("plugin")) return "插件";
+    if (target.includes("skill")) return i18n.t("agent.logs.skillLoading");
+    if (target.includes("plugin")) return i18n.t("agent.logs.plugin");
     if (target.includes("mcp") || target.includes("rmcp")) return "MCP";
-    if (target.includes("shell")) return "终端";
-    if (target.includes("state_db")) return "会话存储";
+    if (target.includes("shell")) return i18n.t("agent.logs.terminal");
+    if (target.includes("state_db")) return i18n.t("agent.logs.conversationStorage");
     return "Codex";
 }
 
diff --git a/web/src/components/agent/agent-panel-tabs.tsx b/web/src/components/agent/agent-panel-tabs.tsx
index 39d2b60..6e865f7 100644
--- a/web/src/components/agent/agent-panel-tabs.tsx
+++ b/web/src/components/agent/agent-panel-tabs.tsx
@@ -1,14 +1,16 @@
 import type { ReactNode } from "react";
 import { Tooltip } from "antd";
+import { useTranslation } from "react-i18next";
 
 import { canvasThemes } from "@/lib/canvas-theme";
 
 export function AgentPanelTabs({ value, items, theme, leading, right, onChange }: { value: T; items: { value: T; label: string; icon?: ReactNode; count?: number }[]; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; leading?: ReactNode; right?: ReactNode; onChange: (value: T) => void }) {
+    const { t } = useTranslation();
     return (
         
{leading ?
{leading}
: null} -
- setQuery(event.target.value)} prefix={} placeholder="搜索 Skill" /> + setQuery(event.target.value)} prefix={} placeholder={t("agent.skills.search")} /> size="small" variant="borderless" - aria-label="按来源筛选 Skill" + aria-label={t("agent.skillManager.filterBySource")} className="w-28 shrink-0" disabled={!connected} value={scope} onChange={setScope} - options={[{ value: "all", label: "全部来源" }, ...Object.entries(scopeLabels).map(([value, label]) => ({ value: value as AgentSkillScope, label }))]} + options={[{ value: "all", label: t("agent.skillManager.scopes.all") }, ...(["repo", "user", "system", "admin"] as AgentSkillScope[]).map((value) => ({ value, label: t(`agent.skillManager.scopes.${value}`) }))]} />
{errors.length ? ( - + ) : null}
{loading && !loaded ? ( -
正在读取 Skill
+
{t("agent.skills.loading")}
) : filteredSkills.length ? (
{filteredSkills.map((skill) => { @@ -318,25 +318,25 @@ export function AgentSkillsView({ clientId }: { clientId: string }) {
{skill.interface?.displayName || skill.name} - {!skill.managed ? : null} + {!skill.managed ? : null}
-
{skill.interface?.shortDescription || skill.shortDescription || skill.description || "暂无说明"}
+
{skill.interface?.shortDescription || skill.shortDescription || skill.description || t("agent.skillManager.noDescription")}
-
{scopeLabels[skill.scope] || skill.scope} · {skill.name}
+
{t(`agent.skillManager.scopes.${skill.scope}`)} · {skill.name}
- + {skill.managed ? ( <> -
@@ -348,23 +348,23 @@ export function AgentSkillsView({ clientId }: { clientId: string }) { ) : (
-
{!connected ? "连接 Agent 后查看 Skill" : skills.length ? "没有匹配的 Skill" : "还没有本地 Skill"}
-
{!connected ? "连接成功后会读取本机已安装的 Skill" : skills.length ? "换个关键词或来源试试" : "创建一个,或在本机安装后刷新"}
+
{t(!connected ? "agent.skillManager.connectToView" : skills.length ? "agent.skillManager.noMatch" : "agent.skillManager.none")}
+
{t(!connected ? "agent.skillManager.connectDescription" : skills.length ? "agent.skillManager.tryAnotherFilter" : "agent.skillManager.createOrInstall")}
)}
- setErrorsOpen(false)}> + setErrorsOpen(false)}>
{errors.map((error, index) =>
{error}
)}
void saveSkill()} > -
保存到 本地 Agent 工作区 · .agents/skills
+
{t("agent.skillManager.saveLocation")} · .agents/skills
-
基本信息
+
{t("agent.skillManager.basicInfo")}
- - + + - +
- typeof value === "string" && /[<>]/.test(value) ? Promise.reject(new Error("使用场景不能包含尖括号")) : Promise.resolve() }]}> - + typeof value === "string" && /[<>]/.test(value) ? Promise.reject(new Error(t("agent.skillManager.noAngleBrackets"))) : Promise.resolve() }]}> + 高级设置, + label: {t("agent.skillManager.advanced")}, children: ( <> - - + + ), }]} diff --git a/web/src/components/agent/local-agent-panel.tsx b/web/src/components/agent/local-agent-panel.tsx index a364711..627eb64 100644 --- a/web/src/components/agent/local-agent-panel.tsx +++ b/web/src/components/agent/local-agent-panel.tsx @@ -3,7 +3,9 @@ import { useNavigate, useSearchParams } from "react-router-dom"; import { App, Button, Tooltip } from "antd"; import dayjs from "dayjs"; import { Bot, History, MessageSquare, PanelRightClose, PlugZap, Plus, Sparkles, Terminal } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import i18n from "@/i18n"; import { canvasThemes } from "@/lib/canvas-theme"; import { imageMetadata } from "@/lib/canvas/canvas-node-factory"; import { fitNodeSize } from "@/lib/canvas/canvas-node-size"; @@ -69,7 +71,7 @@ const DEFAULT_AGENT_URL = "http://127.0.0.1:17371"; const AGENT_PROTOCOL_VERSION = 5; const HISTORY_RETRY_DELAYS_MS = [0, 150, 350, 700, 1200]; const AGENT_REASONING_EFFORTS = new Set(["minimal", "low", "medium", "high", "xhigh", "max", "ultra"]); -const AGENT_REASONING_LABELS: Record = { minimal: "最低", low: "轻度", medium: "中", high: "高", xhigh: "极高", max: "最高", ultra: "Ultra" }; +const rt = (key: string, options?: Record) => i18n.t(`agent.runtime.${key}`, options); type AgentWorkspace = { workspacePath: string; activeThreadId?: string }; type AgentThreadsResponse = { ok?: boolean; workspace?: AgentWorkspace; conversation?: AgentConversationState; data?: AgentThreadSummary[] }; @@ -95,29 +97,30 @@ function agentErrorState(error: unknown) { function conversationBootstrapView(conversation: AgentConversationState) { const mcpStartupStatuses: Record = Object.fromEntries(Object.entries(conversation.mcpStatuses).map(([name, item]) => { const view: AgentBootstrapStatus = item.status === "starting" - ? { key: `mcp:${name}:starting`, text: `正在启动 MCP:${name}`, detail: "正在建立工具连接并读取可用工具列表", status: "running" } + ? { key: `mcp:${name}:starting`, text: rt("mcpStarting", { name }), detail: rt("mcpConnecting"), status: "running" } : item.status === "ready" - ? { key: `mcp:${name}:ready`, text: `MCP 已就绪:${name}`, detail: "工具列表加载完成,可以开始对话", status: "ready" } - : { key: `mcp:${name}:${item.status}`, text: item.status === "failed" ? `MCP 启动失败:${name}` : `MCP 启动已取消:${name}`, detail: item.error || "工具服务未能完成初始化", status: "error" }; + ? { key: `mcp:${name}:ready`, text: rt("mcpReadyNamed", { name }), detail: rt("toolsReady"), status: "ready" } + : { key: `mcp:${name}:${item.status}`, text: rt(item.status === "failed" ? "mcpFailedNamed" : "mcpCanceledNamed", { name }), detail: item.error || rt("toolInitFailed"), status: "error" }; return [name, view]; })); const services = Object.values(mcpStartupStatuses); const pending = services.filter((item) => item.status === "running").length; const bootstrapStatus: AgentBootstrapStatus | null = conversation.status === "idle" || conversation.status === "preparing" ? services.length - ? { key: "mcp:starting", text: "正在启动 MCP 服务", detail: pending ? `还有 ${pending} 个工具服务正在初始化` : "正在确认工具服务状态", status: "running" } - : { key: "codex:preparing", text: "正在初始化 Codex 对话", detail: "正在创建会话并启动画布工具服务", status: "running" } + ? { key: "mcp:starting", text: rt("mcpServicesStarting"), detail: pending ? rt("toolServicesPending", { count: pending }) : rt("checkingToolServices"), status: "running" } + : { key: "codex:preparing", text: rt("conversationInitializing"), detail: rt("conversationCreating"), status: "running" } : conversation.status === "warning" - ? { key: "mcp:warning", text: "部分 MCP 服务初始化失败", detail: "其余工具已就绪,可以开始对话", status: "error" } + ? { key: "mcp:warning", text: rt("someMcpFailed"), detail: rt("remainingToolsReady"), status: "error" } : conversation.status === "failed" - ? { key: "codex:prepare_failed", text: "Codex 对话初始化失败", detail: conversation.error || "无法创建 Codex 会话", status: "error" } + ? { key: "codex:prepare_failed", text: rt("conversationInitFailed"), detail: conversation.error || rt("conversationCreateFailed"), status: "error" } : conversation.status === "ready" - ? { key: "mcp:ready", text: `${services.length} 个 MCP 服务已完成初始化`, detail: "工具列表加载完成,可以开始对话", status: "ready" } + ? { key: "mcp:ready", text: rt("mcpServicesReady", { count: services.length }), detail: rt("toolsReady"), status: "ready" } : null; return { bootstrapStatus, mcpStartupStatuses }; } export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded?: boolean; headless?: boolean; autoConnect?: boolean }) { + const { t } = useTranslation(); const theme = canvasThemes[useThemeStore((state) => state.theme)]; const { message, modal } = App.useApp(); const [searchParams] = useSearchParams(); @@ -307,7 +310,7 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? liveTurnKeysRef.current.clear(); } } catch (error) { - addEventLog("读取历史失败", error); + addEventLog(rt("historyReadFailed"), error); } finally { if (sequence === loadThreadsSequenceRef.current && !threadOperationRef.current) setAgentState({ loadingThreads: false }); } @@ -348,7 +351,7 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? eventQueue = eventQueue.then(async () => { if (isCurrentConnection()) await task(); }).catch((error) => { - if (isCurrentConnection()) addEventLog("同步会话失败", error); + if (isCurrentConnection()) addEventLog(rt("conversationSyncFailed"), error); }); }; const source = new EventSource(`${endpoint}/events?token=${encodeURIComponent(token)}&clientId=${encodeURIComponent(clientId)}`); @@ -356,13 +359,13 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? if (!isCurrentConnection()) return; const hello = parseEventData(event); if (hello?.protocolVersion !== AGENT_PROTOCOL_VERSION) { - const text = "本地 Agent 版本过旧,请重启 Canvas Agent 后重新连接"; + const text = rt("agentOutdated"); protocolRejected = true; source.close(); connectedRef.current = false; - setAgentState({ enabled: false, connected: false, waiting: false, sending: false, activity: "需要重启 Agent", connectError: text, silentConnect: false, pendingTool: null, pendingApprovals: [] }); + setAgentState({ enabled: false, connected: false, waiting: false, sending: false, activity: rt("restartRequired"), connectError: text, silentConnect: false, pendingTool: null, pendingApprovals: [] }); useAgentSkillStore.getState().reset(); - addEventLog("Agent 版本不匹配", text, hello); + addEventLog(rt("versionMismatch"), text, hello); if (!headless) message.error(text); return; } @@ -383,7 +386,7 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? connectedRef.current = true; setAgentState({ connected: true, - activity: pendingApprovals.length ? "等待权限确认" : busy ? "Codex 正在运行" : "已连接", + activity: pendingApprovals.length ? rt("awaitingApproval") : busy ? rt("codexRunning") : rt("connected"), waiting: busy, sending: false, connectError: "", @@ -393,7 +396,7 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? messages, pendingApprovals, }); - if (!headless) message.success("本地 Agent 已连接"); + if (!headless) message.success(rt("localAgentConnected")); void postState(endpoint, token, clientId, canvasContextRef.current?.snapshot || null); if (document.visibilityState === "visible" && document.hasFocus()) void activateAgentClient(endpoint, token, clientId); if (!busy && !nextThreadId && (!hello?.conversation || hello.conversation.status === "idle")) { @@ -402,7 +405,7 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? .catch((error) => { const state = agentErrorState(error); if (state) applyConversationState(state); - addEventLog("Codex 对话初始化失败", error); + addEventLog(rt("conversationInitFailed"), error); }); } }); @@ -419,7 +422,7 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? const activeTurnId = busy ? turnId : ""; const messages = activeTurnId ? bindPendingTurnMessages(current.messages, current.activeThreadId, activeTurnId) : current.messages; setAgentState({ - activity: busy ? "Codex 正在运行" : current.activity === "处理失败" ? "处理失败" : "完成", + activity: busy ? rt("codexRunning") : current.activity === rt("processingFailed") ? rt("processingFailed") : rt("completed"), waiting: busy, sending: false, activeTurnId, @@ -437,8 +440,8 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? if (!isCurrentConnection()) return; const data = parseEventData(event); if (!data || !isCurrentThreadEvent(data)) return; - setAgentState({ pendingApprovals: [...useAgentStore.getState().pendingApprovals.filter((item) => item.requestId !== data.requestId), data], activity: "等待权限确认" }); - addEventLog("等待权限确认", data.reason || data.method, data); + setAgentState({ pendingApprovals: [...useAgentStore.getState().pendingApprovals.filter((item) => item.requestId !== data.requestId), data], activity: rt("awaitingApproval") }); + addEventLog(rt("awaitingApproval"), data.reason || data.method, data); }); source.addEventListener("codex_approval_resolved", (event) => { if (!isCurrentConnection()) return; @@ -449,7 +452,7 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? const pendingApprovals = current.pendingApprovals.filter((item) => item.requestId !== data.requestId); setAgentState({ pendingApprovals, activity: approvalActivity(pendingApprovals, current.waiting, current.activity) }); const decision = data.decision || approval?.deciding; - if (approval && decision) addEventLog(decision === "accept" || decision === "acceptForSession" ? "已批准权限" : "已取消权限", approval.reason || approval.method, approval); + if (approval && decision) addEventLog(rt(decision === "accept" || decision === "acceptForSession" ? "approvalGranted" : "approvalCanceled"), approval.reason || approval.method, approval); }); source.addEventListener("agent_event", (event) => { const data = parseEventData(event); @@ -464,26 +467,26 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? const data = parseEventData(event); if (!data?.type) return; if (data.type === "codex.preparing") { - addEventLog("正在初始化 Codex 对话", "正在创建会话并启动画布工具服务", data); + addEventLog(rt("conversationInitializing"), rt("conversationCreating"), data); return; } if (data.type === "codex.prepare_failed") { - addEventLog("Codex 对话初始化失败", data.error, data); + addEventLog(rt("conversationInitFailed"), data.error, data); return; } if (data.type === "mcp.complete") { - addEventLog("MCP 状态确认完成", "已读取 Codex 返回的完整工具服务清单", data); + addEventLog(rt("mcpStatusComplete"), rt("mcpListRead"), data); return; } if (!data.name || !data.status) return; const label = data.name; const status = data.status === "starting" - ? { text: `正在启动 MCP:${label}`, detail: "正在建立工具连接并读取可用工具列表", status: "running" as const } + ? { text: rt("mcpStarting", { name: label }), detail: rt("mcpConnecting"), status: "running" as const } : data.status === "ready" - ? { text: `MCP 已就绪:${label}`, detail: "工具列表加载完成,可以开始对话", status: "ready" as const } + ? { text: rt("mcpReadyNamed", { name: label }), detail: rt("toolsReady"), status: "ready" as const } : data.status === "failed" - ? { text: `MCP 启动失败:${label}`, detail: data.error || "工具服务未能完成初始化", status: "error" as const } - : { text: `MCP 启动已取消:${label}`, detail: "工具服务初始化已取消", status: "error" as const }; + ? { text: rt("mcpFailedNamed", { name: label }), detail: data.error || rt("toolInitFailed"), status: "error" as const } + : { text: rt("mcpCanceledNamed", { name: label }), detail: rt("toolInitCanceled"), status: "error" as const }; addEventLog(status.text, status.detail, data); }); source.addEventListener("conversation_changed", (event) => { @@ -542,7 +545,7 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? source.addEventListener("agent_log", (event) => { if (!isCurrentConnection()) return; const text = parseEventData<{ text?: unknown }>(event)?.text; - addEventLog("日志", text, text); + addEventLog(rt("log"), text, text); }); source.addEventListener("skills_changed", (event) => { if (!isCurrentConnection()) return; @@ -562,16 +565,16 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? if (disposed || protocolRejected) return; const wasConnected = connectedRef.current; const silent = useAgentStore.getState().silentConnect && !wasConnected; - const text = wasConnected ? "本地 Agent 连接失败或已断开" : "连接失败,请检查地址和 token"; + const text = rt(wasConnected ? "connectionLostDescription" : "connectionFailedDescription"); if (!errorLoggedRef.current || wasConnected) { - addEventLog(wasConnected ? "连接断开" : "连接失败", text); + addEventLog(rt(wasConnected ? "connectionLost" : "connectionFailed"), text); if (!headless && !silent) message.error(text); } errorLoggedRef.current = true; connectedRef.current = false; pendingToolRef.current = null; setAgentState({ - activity: wasConnected ? "连接断开" : "连接失败", + activity: rt(wasConnected ? "connectionLost" : "connectionFailed"), connected: false, waiting: false, sending: false, @@ -624,7 +627,7 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? localStorage.setItem("canvas-agent-model", current.model); localStorage.setItem("canvas-agent-reasoning-effort", nextEffort); setAgentState({ models, model: current.model, reasoningEffort: nextEffort }); - }).catch((error) => addEventLog("读取模型列表失败", error)); + }).catch((error) => addEventLog(rt("modelListFailed"), error)); }, [connected, endpoint, setAgentState, token]); useEffect(() => { @@ -650,22 +653,22 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? const currentState = useAgentStore.getState(); if (!currentState.connected || !requestPrompt || currentState.sending || currentState.waiting || currentState.loadingThreads || !["ready", "warning"].includes(currentState.conversation.status)) return; if (attachmentPayloadBytes(files) > MAX_ATTACHMENT_PAYLOAD_BYTES) { - addMessage({ role: "error", title: "图片过大", text: "图片附件超过 30MB,请删减后再发送。" }); + addMessage({ role: "error", title: rt("imageTooLarge"), text: rt("imagePayloadTooLarge") }); return; } const messageId = createId(); - const userText = text || `发送了 ${files.length} 张图片`; + const userText = text || rt("imagesSent", { count: files.length }); loadThreadsSequenceRef.current += 1; const currentBeforeSend = useAgentStore.getState(); const requestThreadId = currentBeforeSend.activeThreadId; - setAgentState({ prompt: "", attachments: [], activity: "发送中", sending: true, loadingThreads: false, activeTurnId: "", messages: currentBeforeSend.messages }); + setAgentState({ prompt: "", attachments: [], activity: rt("sending"), sending: true, loadingThreads: false, activeTurnId: "", messages: currentBeforeSend.messages }); addMessage({ id: messageId, itemId: "synthetic:user", clientMessageId: messageId, threadId: requestThreadId, turnId: "", role: "user", text: userText, historyText: requestPrompt, attachments: files }); let threadId = requestThreadId; try { if (files.length) await savePendingAgentUserMessage({ id: messageId, role: "user", text: userText, historyText: requestPrompt, attachments: files }); - const modelName = models.find((item) => item.model === model)?.displayName || model || "默认模型"; - const effortName = reasoningEffort ? AGENT_REASONING_LABELS[reasoningEffort] : "默认强度"; - addEventLog("发送任务", `${modelName} · ${effortName}${selectedSkill ? ` · Skill ${selectedSkill.name}` : ""}${files.length ? ` · 附件 ${files.length}` : ""} · ${compactText(text) || "仅附件"}`); + const modelName = models.find((item) => item.model === model)?.displayName || model || rt("defaultModel"); + const effortName = reasoningEffort ? i18n.t(`agent.composer.effort.${reasoningEffort}`) : rt("defaultEffort"); + addEventLog(rt("sendTask"), `${modelName} · ${effortName}${selectedSkill ? ` · Skill ${selectedSkill.name}` : ""}${files.length ? ` · ${rt("attachmentCount", { count: files.length })}` : ""} · ${compactText(text) || rt("attachmentsOnly")}`); const accepted = await fetchAgentJson(endpoint, token, "/agent/codex/turn", { method: "POST", headers: { "content-type": "application/json" }, @@ -685,12 +688,12 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? }), }); threadId = accepted.threadId || threadId; - if (!threadId) throw new Error("启动对话失败"); + if (!threadId) throw new Error(rt("startConversationFailed")); if (selectedSkill) clearSkillSelection(selectedSkillRevision); if (files.length) { const latestMessage = useAgentStore.getState().messages.find((item) => item.clientMessageId === messageId); const acceptedThreadId = latestMessage?.threadId || threadId; - await bindPendingAgentUserMessage(acceptedThreadId, messageId, latestMessage?.turnId || "").catch((error) => addEventLog("保存附件历史失败", error)); + await bindPendingAgentUserMessage(acceptedThreadId, messageId, latestMessage?.turnId || "").catch((error) => addEventLog(rt("attachmentHistoryFailed"), error)); } files.forEach((item) => { URL.revokeObjectURL(item.url); @@ -698,7 +701,7 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? }); } catch (error) { if (files.length) await deletePendingAgentUserMessage(messageId).catch(() => undefined); - const text = error instanceof Error ? error.message : "发送失败"; + const text = error instanceof Error ? error.message : rt("sendFailed"); const response = error instanceof AgentApiError ? error.response as { code?: string; state?: AgentConversationState } : undefined; if (response?.state) applyConversationState(response.state); const stale = response?.code === "CONVERSATION_STALE"; @@ -713,28 +716,28 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? const restoreDraft = state.prompt || state.attachments.length ? {} : { prompt, attachments: files }; if (ownsCurrentThread) { setAgentState({ - activity: stale ? "会话已同步" : busy ? "Codex 正在运行" : "发送失败", + activity: rt(stale ? "conversationSynced" : busy ? "codexRunning" : "sendFailed"), sending: false, messages: removeFailedPending(state.messages), ...restoreDraft, }); - addMessage({ threadId: state.activeThreadId, turnId: "", role: "error", title: stale ? "会话已同步" : busy ? "任务仍在运行" : "发送失败", text }); + addMessage({ threadId: state.activeThreadId, turnId: "", role: "error", title: rt(stale ? "conversationSynced" : busy ? "taskStillRunning" : "sendFailed"), text }); } else { setAgentState({ sending: false, messages: removeFailedPending(state.messages), ...restoreDraft }); } - addEventLog("发送失败", error); + addEventLog(rt("sendFailed"), error); } }; const stopTurn = async () => { if (!connected || (!sending && !waiting)) return; - setAgentState({ activity: "停止中" }); + setAgentState({ activity: rt("stopping") }); try { await interruptCodexTurn(endpoint, token, useAgentStore.getState().activeThreadId || undefined); - addEventLog("停止任务", "任务已停止"); + addEventLog(rt("stopTask"), rt("taskStopped")); } catch (error) { - setAgentState({ activity: "停止失败" }); - addEventLog("停止失败", error); + setAgentState({ activity: rt("stopFailed") }); + addEventLog(rt("stopFailed"), error); } }; @@ -758,12 +761,12 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? URL.revokeObjectURL(item.url); attachmentUrlsRef.current.delete(item.url); }); - addMessage({ role: "error", title: "图片过大", text: "图片附件最多约 30MB。" }); + addMessage({ role: "error", title: rt("imageTooLarge"), text: rt("imageLimit") }); return; } if (next.length) setAgentState({ attachments: merged }); } catch (error) { - addMessage({ role: "error", title: "图片读取失败", text: error instanceof Error ? error.message : "图片读取失败" }); + addMessage({ role: "error", title: rt("imageReadFailed"), text: error instanceof Error ? error.message : rt("imageReadFailed") }); } }; @@ -779,12 +782,12 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? const handleToolCall = async (endpoint: string, token: string, payload: AgentPendingToolCall) => { if (confirmToolsRef.current && isCanvasWriteTool(payload.name)) { if (pendingToolRef.current) { - await postToolResult(endpoint, token, clientIdRef.current, { requestId: payload.requestId, error: "仍有待确认的画布工具调用" }); + await postToolResult(endpoint, token, clientIdRef.current, { requestId: payload.requestId, error: rt("pendingCanvasTool") }); return; } pendingToolRef.current = payload; setAgentState({ pendingTool: payload }); - addEventLog("等待确认", payload, payload); + addEventLog(rt("awaitingConfirmation"), payload, payload); return; } await runToolCall(endpoint, token, payload); @@ -796,9 +799,9 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? addEventLog(toolName(payload.name), payload, payload); const result = await runSiteTool(payload.name, payload.input || {}, navigate, { canvasSnapshot: canvasContextRef.current?.snapshot || null }); await postToolResult(endpoint, token, clientIdRef.current, { requestId: payload.requestId, result }); - addEventLog(`${toolName(payload.name)}完成`, result, result); + addEventLog(rt("toolCompleted", { tool: toolName(payload.name) }), result, result); } catch (error) { - const message = error instanceof Error ? error.message : "工具执行失败"; + const message = error instanceof Error ? error.message : rt("toolExecutionFailed"); await postToolResult(endpoint, token, clientIdRef.current, { requestId: payload.requestId, error: message }); } return; @@ -814,31 +817,31 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? result = { ok: true, path }; } else if (payload.name === "canvas_apply_ops") { const context = canvasContextRef.current; - if (!context) throw new Error("当前不在画布页,请先用 site_navigate 打开画布"); + if (!context) throw new Error(rt("openCanvasFirst")); result = context.applyOps(appliedOps); void postState(endpoint, token, clientIdRef.current, result as CanvasAgentSnapshot); } else if (payload.name === "canvas_create_attachment_nodes") { const context = canvasContextRef.current; - if (!context) throw new Error("当前不在画布页,请先用 site_navigate 打开画布"); + if (!context) throw new Error(rt("openCanvasFirst")); appliedOps = await attachmentNodeOps(endpoint, token, clientIdRef.current, payload.input?.nodes); result = context.applyOps(appliedOps); await postState(endpoint, token, clientIdRef.current, result as CanvasAgentSnapshot); } else { const snapshot = canvasContextRef.current?.snapshot; - if (!snapshot) throw new Error("当前不在画布页,请先用 site_navigate 打开画布"); + if (!snapshot) throw new Error(rt("openCanvasFirst")); result = snapshot; } await postToolResult(endpoint, token, clientIdRef.current, { requestId: payload.requestId, result }); - addEventLog(`${toolName(payload.name)}完成`, result, result); + addEventLog(rt("toolCompleted", { tool: toolName(payload.name) }), result, result); } catch (error) { - const message = error instanceof Error ? error.message : "画布操作失败"; + const message = error instanceof Error ? error.message : rt("canvasOperationFailed"); await postToolResult(endpoint, token, clientIdRef.current, { requestId: payload.requestId, error: message }); } }; const rejectPendingTool = async () => { if (!pendingTool) return; - await postToolResult(endpoint, token, clientIdRef.current, { requestId: pendingTool.requestId, error: "用户取消了画布工具调用" }); + await postToolResult(endpoint, token, clientIdRef.current, { requestId: pendingTool.requestId, error: rt("canvasToolCanceled") }); pendingToolRef.current = null; setAgentState({ pendingTool: null }); }; @@ -855,11 +858,11 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? const current = useAgentStore.getState(); const pending = current.pendingApprovals.find((item) => item.requestId === approval.requestId); if (!pending || pending.deciding) return; - setAgentState({ pendingApprovals: current.pendingApprovals.map((item) => item.requestId === approval.requestId ? { ...item, deciding: decision } : item), activity: "正在提交权限决定" }); + setAgentState({ pendingApprovals: current.pendingApprovals.map((item) => item.requestId === approval.requestId ? { ...item, deciding: decision } : item), activity: rt("submittingApproval") }); try { await postCodexApproval(endpoint, token, approval.requestId, decision); const latest = useAgentStore.getState(); - if (latest.pendingApprovals.some((item) => item.requestId === approval.requestId)) setAgentState({ activity: "等待 Codex 确认权限" }); + if (latest.pendingApprovals.some((item) => item.requestId === approval.requestId)) setAgentState({ activity: rt("waitingCodexApproval") }); } catch (error) { const latest = useAgentStore.getState(); const expired = error instanceof Error && error.message.includes("审批请求已失效"); @@ -869,8 +872,8 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? : latest.pendingApprovals.map((item) => item.requestId === approval.requestId ? { ...item, deciding: undefined } : item); setAgentState({ pendingApprovals, activity: approvalActivity(pendingApprovals, latest.waiting, latest.activity) }); if (resolved) return; - addEventLog("权限审批失败", error); - message.error(error instanceof Error ? error.message : "权限审批失败"); + addEventLog(rt("approvalFailed"), error); + message.error(error instanceof Error ? error.message : rt("approvalFailed")); } }; @@ -881,18 +884,18 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? }; if (nextMode !== "full") return apply(); modal.confirm({ - title: "启用完全访问权限", - content: "Codex 将不受沙箱限制,可访问互联网及本机任意文件。请仅在信任当前任务时使用。", - okText: "启用完全访问", + title: rt("enableFullAccess"), + content: rt("fullAccessDescription"), + okText: rt("enableFullAccessAction"), okType: "danger", - cancelText: "取消", + cancelText: t("common.cancel"), onOk: apply, }); }; const toggleAgentConnection = async ({ silent = false }: { silent?: boolean } = {}) => { if (enabled) { - clearAgentSession({ enabled: false, connected: false, activity: "离线", connectError: "" }); + clearAgentSession({ enabled: false, connected: false, activity: rt("offline"), connectError: "" }); return; } const urlToken = searchParams.get("agentToken") || ""; @@ -901,7 +904,7 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? const nextEndpoint = (urlEndpoint || discovered?.url || endpoint || DEFAULT_AGENT_URL).trim().replace(/\/$/, ""); const nextToken = (urlToken || token.trim() || discovered?.token || "").trim(); if (!nextEndpoint) { - const text = "请填写本地 Agent 地址"; + const text = rt("addressRequired"); if (!silent) { setAgentState({ connectError: text }); if (!headless) message.warning(text); @@ -909,7 +912,7 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? return; } if (!nextToken) { - const text = "没有发现本地 Agent,请先在 Codex 使用插件或手动启动 Canvas Agent"; + const text = rt("agentNotFound"); if (!silent) { setAgentState({ connectError: text }); if (!headless) message.warning(text); @@ -920,7 +923,7 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? const parsed = new URL(nextEndpoint); if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error("invalid protocol"); } catch { - const text = "本地 Agent 地址格式不正确"; + const text = rt("invalidAddress"); if (!silent) { setAgentState({ connectError: text }); if (!headless) message.warning(text); @@ -928,7 +931,7 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? return; } errorLoggedRef.current = false; - setAgentState({ url: nextEndpoint, token: nextToken, enabled: true, connected: false, silentConnect: silent, activity: "连接中", connectError: "", activeTab: "setup" }); + setAgentState({ url: nextEndpoint, token: nextToken, enabled: true, connected: false, silentConnect: silent, activity: rt("connecting"), connectError: "", activeTab: "setup" }); }; useEffect(() => { @@ -986,17 +989,17 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? if (!current.connected || current.sending || current.waiting || current.loadingThreads || ["preparing", "running"].includes(current.conversation.status)) return; const operation = beginThreadOperation(); clearSkillSelection(); - setAgentState({ activeTab: "chat", activity: "正在新建对话" }); + setAgentState({ activeTab: "chat", activity: rt("creatingConversation") }); try { const result = await fetchAgentJson(endpoint, token, "/agent/codex/threads/reset", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ clientId: clientIdRef.current, permissionMode }) }); if (threadOperationRef.current !== operation) return; if (result.conversation) applyConversationState(result.conversation); - setAgentState({ activeTab: "chat", activity: "新对话" }); + setAgentState({ activeTab: "chat", activity: rt("newConversation") }); } catch (error) { const state = agentErrorState(error); if (state) applyConversationState(state); - addEventLog("新建对话失败", error); - message.error(error instanceof Error ? error.message : "新建对话失败"); + addEventLog(rt("newConversationFailed"), error); + message.error(error instanceof Error ? error.message : rt("newConversationFailed")); await loadThreads(); } finally { finishThreadOperation(operation); @@ -1011,12 +1014,12 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? const result = await fetchAgentJson(endpoint, token, `/agent/codex/threads/${encodeURIComponent(threadId)}/resume`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ permissionMode, clientId: clientIdRef.current }) }); if (result.conversation) applyConversationState(result.conversation); await loadThreads(); - if (useAgentStore.getState().activeThreadId === threadId) setAgentState({ activeTab: "chat", activity: "已恢复会话" }); + if (useAgentStore.getState().activeThreadId === threadId) setAgentState({ activeTab: "chat", activity: rt("conversationResumed") }); } catch (error) { const state = agentErrorState(error); if (state) applyConversationState(state); - addEventLog("恢复对话失败", error); - message.error(error instanceof Error ? error.message : "恢复对话失败"); + addEventLog(rt("resumeConversationFailed"), error); + message.error(error instanceof Error ? error.message : rt("resumeConversationFailed")); await loadThreads(); } finally { finishThreadOperation(operation); @@ -1035,14 +1038,14 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? } void deleteAgentThreadMessages(deletedThreadIds).catch(() => undefined); await loadThreads(); - message.success(`已删除 ${deletedThreadIds.length} 条记录`); + message.success(rt("recordsDeleted", { count: deletedThreadIds.length })); } catch (error) { if (deletedThreadIds.length) { void deleteAgentThreadMessages(deletedThreadIds).catch(() => undefined); } await loadThreads(); - addEventLog("删除对话失败", error); - message.error(error instanceof Error ? error.message : "删除对话失败"); + addEventLog(rt("deleteConversationFailed"), error); + message.error(error instanceof Error ? error.message : rt("deleteConversationFailed")); } finally { finishThreadOperation(operation); } @@ -1050,11 +1053,11 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? const confirmDeleteThreads = (threadIds: string[]) => { modal.confirm({ - title: `删除 ${threadIds.length} 条对话记录`, - content: "删除后无法恢复,确定继续吗?", - okText: "删除", + title: rt("deleteConversations", { count: threadIds.length }), + content: rt("deleteConversationsDescription"), + okText: t("common.delete"), okType: "danger", - cancelText: "取消", + cancelText: t("common.cancel"), onOk: () => deleteThreads(threadIds), }); }; @@ -1091,7 +1094,7 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? const activityItems = { ...(current?.activityItems || {}) }; const previous = activityItems[item.id] || ""; activityItems[item.id] = isDelta ? `${previous === activityPlaceholder("reasoning") ? "" : previous}${text}` : text; - upsertActivityMessage({ ...scoped, title: "思考摘要", text: reasoningActivityText(activityItems), activityItems, detail: activityDetail(current?.detail || scoped.detail, "reasoning", "inProgress") }); + upsertActivityMessage({ ...scoped, title: i18n.t("agent.events.reasoning"), text: reasoningActivityText(activityItems), activityItems, detail: activityDetail(current?.detail || scoped.detail, "reasoning", "inProgress") }); return; } const scoped = scopeEventChatItem(event, activityDeltaFallback(item, text), item.id); @@ -1125,7 +1128,7 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? const previous = activityItems[itemId] || ""; const incoming = normalizeText(item.text); activityItems[itemId] = incoming === "已完成分析" && previous && previous !== activityPlaceholder("reasoning") ? previous : incoming; - upsertActivityMessage({ ...scoped, title: "思考摘要", text: reasoningActivityText(activityItems, incoming), activityItems }); + upsertActivityMessage({ ...scoped, title: i18n.t("agent.events.reasoning"), text: reasoningActivityText(activityItems, incoming), activityItems }); return; } upsertActivityMessage(scopeEventChatItem(event, { ...item, id: itemId }, itemId)); @@ -1167,8 +1170,8 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? const current = state.messages.find((message) => message.id === item.id); if (current && !normalizeText(value)) return; upsertActivityMessage(item); - setAgentState({ activity: "处理失败", pendingApprovals: [] }); - if (log) addEventLog("处理失败", error.text, value); + setAgentState({ activity: rt("processingFailed"), pendingApprovals: [] }); + if (log) addEventLog(rt("processingFailed"), error.text, value); }; const handleAgentEvent = async (event: AgentEventPayload) => { @@ -1243,7 +1246,7 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? const result = context.applyOps(ops); void postState(endpoint, token, clientIdRef.current, result); } - addEventLog("导入生成图片", context ? "已添加到发起任务的画布" : "图片已生成"); + addEventLog(rt("importGeneratedImages"), rt(context ? "addedToSourceCanvas" : "imageGenerated")); } } if (activity && event.item?.id) { @@ -1281,7 +1284,7 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? setAgentState({ messages: currentMessages.map((message, itemIndex) => itemIndex === index ? { ...message, text: isDelta ? `${message.text}${text}` : mergeStreamText(message.text, text) } : message) }); }; - const connectionStatus = connectError ? "连接失败" : connected ? "已连接" : enabled ? "连接中" : "未连接"; + const connectionStatus = t(connectError ? "agent.status.failed" : connected ? "agent.status.connected" : enabled ? "agent.status.connecting" : "agent.status.disconnected"); const connectionStatusColor = connectError ? "#dc2626" : connected ? "#16a34a" : enabled ? "#d97706" : theme.node.muted; const content = ( <> @@ -1294,18 +1297,18 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded?
Agent
- -
} items={[ - { value: "chat", label: "对话", icon: }, - { value: "history", label: "历史", icon: , count: threads.length }, - { value: "skills", label: "技能", icon: , count: skillCount }, - { value: "log", label: "日志", icon: , count: eventLogs.length }, + { value: "chat", label: t("agent.panel.chat"), icon: }, + { value: "history", label: t("agent.panel.history"), icon: , count: threads.length }, + { value: "skills", label: t("agent.panel.skills"), icon: , count: skillCount }, + { value: "log", label: t("agent.panel.logs"), icon: , count: eventLogs.length }, ]} onChange={(activeTab) => { setAgentState({ activeTab }); @@ -1313,13 +1316,13 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? }} right={ <> - - - -