diff --git a/frontend/src/i18n/locales/en/common.json b/frontend/src/i18n/locales/en/common.json index 0450a3379..ebc380e87 100644 --- a/frontend/src/i18n/locales/en/common.json +++ b/frontend/src/i18n/locales/en/common.json @@ -599,6 +599,10 @@ } }, "common": { + "yes": "Yes", + "no": "No", + "cancel": "Cancel", + "refresh": "Refresh", "settings": { "title": "Settings" }, @@ -1373,6 +1377,8 @@ "descriptionPlaceholder": "Describe the purpose", "srcDataset": "Source Dataset", "srcDatasetPlaceholder": "Select source dataset", + "destDatasetName": "Target Dataset Name", + "destDatasetNamePlaceholder": "Enter or select target dataset name", "destDatasetType": "Target Dataset Type", "useSourceDataset": "Use Source Dataset", "useSourceDatasetHint": "Output will be replaced in source dataset", @@ -1871,6 +1877,52 @@ "latestVersion": "Latest Version", "noReleases": "No version release information", "initialize": "Click to initialize" + }, + "examples": { + "copyCode": "Copy Code" + }, + "service": { + "notSupported": "Deployment not supported", + "confirmUninstall": "Confirm uninstall service?", + "uninstallConfirm": "The service will stop running after uninstallation. Continue?", + "uninstall": "Uninstall", + "status": { + "running": "Running", + "failed": "Failed", + "standby": "Standby", + "stopped": "Stopped" + }, + "notDeployed": "Not deployed", + "clickToDeploy": "Click to deploy", + "deploy": "Deploy", + "notSupportedHint": "This operator does not support deployment", + "deploymentInfo": "Deployment Info", + "deployedAt": "Deployed At", + "version": "Version", + "replicas": "Replicas", + "uninstallService": "Uninstall Service", + "podMonitor": "Pod Monitor", + "noPods": "No Pods", + "createdAt": "Created At", + "cpuUsage": "CPU Usage", + "memoryUsage": "Memory Usage", + "restartCount": "Restart Count", + "viewLogs": "View Logs", + "deploying": "Deploying", + "logs": "Logs", + "copyLogs": "Copy Logs", + "close": "Close" + }, + "requirement": { + "systemRequirements": "System Requirements", + "cpuSpec": "CPU Spec", + "noLimit": "No Limit", + "memorySpec": "Memory Spec", + "storage": "Storage", + "gpuSupport": "GPU Support", + "npuSupport": "NPU Support", + "dependencies": "Dependencies", + "parseError": "Failed to parse data" } }, "create": { diff --git a/frontend/src/i18n/locales/zh/common.json b/frontend/src/i18n/locales/zh/common.json index 575abac32..4f595ba58 100644 --- a/frontend/src/i18n/locales/zh/common.json +++ b/frontend/src/i18n/locales/zh/common.json @@ -599,6 +599,10 @@ } }, "common": { + "yes": "是", + "no": "否", + "cancel": "取消", + "refresh": "刷新", "settings": { "title": "设置" }, @@ -1872,6 +1876,52 @@ "latestVersion": "最新版本", "noReleases": "暂无版本发布信息", "initialize": "点击初始化" + }, + "examples": { + "copyCode": "复制代码" + }, + "service": { + "notSupported": "暂不支持部署", + "confirmUninstall": "确认卸载服务?", + "uninstallConfirm": "卸载后服务将停止运行,是否继续?", + "uninstall": "卸载", + "status": { + "running": "运行中", + "failed": "失败", + "standby": "待机", + "stopped": "已停止" + }, + "notDeployed": "未部署", + "clickToDeploy": "点击部署", + "deploy": "部署", + "notSupportedHint": "当前算子不支持部署功能", + "deploymentInfo": "部署信息", + "deployedAt": "部署时间", + "version": "版本", + "replicas": "副本数", + "uninstallService": "卸载服务", + "podMonitor": "Pod 监控", + "noPods": "暂无 Pod", + "createdAt": "创建时间", + "cpuUsage": "CPU 使用率", + "memoryUsage": "内存使用率", + "restartCount": "重启次数", + "viewLogs": "查看日志", + "deploying": "部署中", + "logs": "日志", + "copyLogs": "复制日志", + "close": "关闭" + }, + "requirement": { + "systemRequirements": "系统规格", + "cpuSpec": "CPU 规格", + "noLimit": "无限制", + "memorySpec": "内存规格", + "storage": "存储", + "gpuSupport": "GPU 支持", + "npuSupport": "NPU 支持", + "dependencies": "依赖项", + "parseError": "数据解析失败" } }, "create": { diff --git a/frontend/src/pages/DataCleansing/Create/components/ParamConfig.tsx b/frontend/src/pages/DataCleansing/Create/components/ParamConfig.tsx index 8be0d8ea1..1b41eff85 100644 --- a/frontend/src/pages/DataCleansing/Create/components/ParamConfig.tsx +++ b/frontend/src/pages/DataCleansing/Create/components/ParamConfig.tsx @@ -11,6 +11,7 @@ import { Switch, } from "antd"; import { ConfigI, OperatorI } from "@/pages/OperatorMarket/operator.model"; +import { useTranslation } from "react-i18next"; interface ParamConfigProps { operator: OperatorI; @@ -25,6 +26,7 @@ const ParamConfig: React.FC = ({ param, onParamChange, }) => { + const { t } = useTranslation(); if (!param) return null; let defaultVal: any = operator.overrides?.[paramKey] ?? param.defaultVal; if (param.type === "range") { @@ -53,7 +55,7 @@ const ParamConfig: React.FC = ({ updateValue(e.target.value)} - placeholder={`请输入${param.name}`} + placeholder={t("dataCleansing.paramConfig.enterValue", { name: param.name })} className="w-full" /> @@ -202,7 +204,7 @@ const ParamConfig: React.FC = ({ updateValue(val)} - placeholder={`请输入${param.name}`} + placeholder={t("dataCleansing.paramConfig.enterValue", { name: param.name })} className="w-full" min={param.min} max={param.max} diff --git a/frontend/src/pages/DataCleansing/Create/hooks/useOperatorOperations.ts b/frontend/src/pages/DataCleansing/Create/hooks/useOperatorOperations.ts index a5e9a01be..25e67db22 100644 --- a/frontend/src/pages/DataCleansing/Create/hooks/useOperatorOperations.ts +++ b/frontend/src/pages/DataCleansing/Create/hooks/useOperatorOperations.ts @@ -7,6 +7,7 @@ import { queryOperatorsUsingPost, } from "@/pages/OperatorMarket/operator.api"; import {useParams} from "react-router"; +import i18n from "@/i18n"; export function useOperatorOperations() { const { id = "" } = useParams(); @@ -97,6 +98,16 @@ export function useOperatorOperations() { initOperators(); }, []); + useEffect(() => { + const handleLanguageChange = () => { + initOperators(); + }; + i18n.on('languageChanged', handleLanguageChange); + return () => { + i18n.off('languageChanged', handleLanguageChange); + }; + }, [i18n]); + const toggleOperator = (operator: OperatorI) => { const exist = selectedOperators.find((op) => op.id === operator.id); if (exist) { diff --git a/frontend/src/pages/DataCleansing/Detail/components/LogsTable.tsx b/frontend/src/pages/DataCleansing/Detail/components/LogsTable.tsx index 7a7bb3cc2..b474f412d 100644 --- a/frontend/src/pages/DataCleansing/Detail/components/LogsTable.tsx +++ b/frontend/src/pages/DataCleansing/Detail/components/LogsTable.tsx @@ -86,7 +86,7 @@ export default function LogsTable({ taskLog: initialLogs, fetchTaskLog, retryCou const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; - a.download = `${taskName}_第${selectedLog}次运行.log`; + a.download = `${taskName}_${t("dataCleansing.logTable.nthRun", { num: selectedLog })}.log`; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); diff --git a/frontend/src/pages/OperatorMarket/Create/OperatorPluginCreate.tsx b/frontend/src/pages/OperatorMarket/Create/OperatorPluginCreate.tsx index 8e1075b85..22ffa7ec0 100644 --- a/frontend/src/pages/OperatorMarket/Create/OperatorPluginCreate.tsx +++ b/frontend/src/pages/OperatorMarket/Create/OperatorPluginCreate.tsx @@ -192,9 +192,9 @@ export default function OperatorPluginCreate() { {uploadStep === "configure" && (
- +
)} diff --git a/frontend/src/pages/OperatorMarket/Detail/OperatorPluginDetail.tsx b/frontend/src/pages/OperatorMarket/Detail/OperatorPluginDetail.tsx index d6b7ea3ee..cf0eb9e1b 100644 --- a/frontend/src/pages/OperatorMarket/Detail/OperatorPluginDetail.tsx +++ b/frontend/src/pages/OperatorMarket/Detail/OperatorPluginDetail.tsx @@ -10,6 +10,7 @@ import {Clock, GitBranch} from "lucide-react"; import DetailHeader from "@/components/DetailHeader"; import {Link, useNavigate, useParams} from "react-router"; import { useTranslation } from "react-i18next"; +import i18n from "@/i18n"; import Overview from "./components/Overview"; import Requirement from "./components/Requirement"; import Documentation from "./components/Documentation"; @@ -42,6 +43,16 @@ export default function OperatorPluginDetail() { fetchOperator(); }, [id]); + useEffect(() => { + const handleLanguageChange = () => { + fetchOperator(); + }; + i18n.on('languageChanged', handleLanguageChange); + return () => { + i18n.off('languageChanged', handleLanguageChange); + }; + }, [i18n, id]); + if (!operator) { return
Loading...
; } diff --git a/frontend/src/pages/OperatorMarket/Detail/components/Examples.tsx b/frontend/src/pages/OperatorMarket/Detail/components/Examples.tsx index a0252f181..4725b6431 100644 --- a/frontend/src/pages/OperatorMarket/Detail/components/Examples.tsx +++ b/frontend/src/pages/OperatorMarket/Detail/components/Examples.tsx @@ -1,8 +1,11 @@ import { copyToClipboard } from "@/utils/unit"; import { Card, Button } from "antd"; import { Copy } from "lucide-react"; +import { useTranslation } from "react-i18next"; export default function Examples({ operator }) { + const { t } = useTranslation(); + return (
{operator.examples.map((example, index) => ( @@ -13,7 +16,7 @@ export default function Examples({ operator }) {
diff --git a/frontend/src/pages/OperatorMarket/Detail/components/OperatorServiceMonitor.tsx b/frontend/src/pages/OperatorMarket/Detail/components/OperatorServiceMonitor.tsx index 5bea509cb..fa6326b59 100644 --- a/frontend/src/pages/OperatorMarket/Detail/components/OperatorServiceMonitor.tsx +++ b/frontend/src/pages/OperatorMarket/Detail/components/OperatorServiceMonitor.tsx @@ -2,6 +2,7 @@ import type React from "react" import { useState } from "react" import { Card, Button, Modal, Empty, Spin, Progress } from "antd" import { Cloud, Power, Trash2, AlertCircle, CheckCircle, Clock, Terminal, Copy, RefreshCw } from "lucide-react" +import { useTranslation } from "react-i18next" interface Pod { id: string @@ -30,6 +31,7 @@ export default function OperatorServiceMonitor({ operatorName, supportsService = true, }: OperatorServiceMonitorProps) { + const { t } = useTranslation(); const [serviceDeployment, setServiceDeployment] = useState({ status: "not_deployed", replicas: 0, @@ -44,7 +46,7 @@ export default function OperatorServiceMonitor({ // 模拟部署 const handleDeploy = async () => { if (!supportsService) { - setDeploymentError(`${operatorName} 算子不支持服务部署`) + setDeploymentError(t("operatorMarket.detail.service.notSupported", { name: operatorName })) return } @@ -95,11 +97,11 @@ export default function OperatorServiceMonitor({ // 模拟卸载 const handleUndeploy = () => { Modal.confirm({ - title: "确认卸载服务", - content: `确定要卸载 ${operatorName} 的服务吗?这将停止所有运行中的Pod实例。`, - okText: "卸载", + title: t("operatorMarket.detail.service.confirmUninstall"), + content: t("operatorMarket.detail.service.uninstallConfirm", { name: operatorName }), + okText: t("operatorMarket.detail.service.uninstall"), okType: "danger", - cancelText: "取消", + cancelText: t("common.cancel"), onOk() { setServiceDeployment({ status: "not_deployed", @@ -116,22 +118,22 @@ export default function OperatorServiceMonitor({ running: { color: "bg-green-100 text-green-800 border-green-200", icon: , - label: "运行中", + label: t("operatorMarket.detail.service.status.running"), }, - pending: { + pending:{ color: "bg-yellow-100 text-yellow-800 border-yellow-200", icon: , - label: "待机中", + label: t("operatorMarket.detail.service.status.standby"), }, failed: { color: "bg-red-100 text-red-800 border-red-200", icon: , - label: "失败", + label: t("operatorMarket.detail.service.status.failed"), }, terminated: { color: "bg-gray-100 text-gray-800 border-gray-200", icon: , - label: "已停止", + label: t("operatorMarket.detail.service.status.stopped"), }, } return config[status] || config.pending @@ -140,16 +142,16 @@ export default function OperatorServiceMonitor({ const renderNotDeployed = () => (
-

服务未部署

-

点击下方按钮部署 {operatorName} 服务

+

{t("operatorMarket.detail.service.notDeployed")}

+

{t("operatorMarket.detail.service.clickToDeploy", { name: operatorName })}

{!supportsService && (
- 该算子不支持服务部署 + {t("operatorMarket.detail.service.notSupportedHint")}
)}
@@ -161,16 +163,16 @@ export default function OperatorServiceMonitor({
-

部署信息

+

{t("operatorMarket.detail.service.deploymentInfo")}

-
部署时间: {new Date(serviceDeployment.deployedAt!).toLocaleString("zh-CN")}
-
版本: {serviceDeployment.version}
-
副本数: {serviceDeployment.replicas}
+
{t("operatorMarket.detail.service.deployedAt")}: {new Date(serviceDeployment.deployedAt!).toLocaleString("zh-CN")}
+
{t("operatorMarket.detail.service.version")}: {serviceDeployment.version}
+
{t("operatorMarket.detail.service.replicas")}: {serviceDeployment.replicas}
@@ -178,15 +180,15 @@ export default function OperatorServiceMonitor({ {/* Pod 监控 */}
-

Pod 实例监控

+

{t("operatorMarket.detail.service.podMonitor")}

{serviceDeployment.pods.length === 0 ? ( - + ) : (
{serviceDeployment.pods.map((pod) => { @@ -200,7 +202,7 @@ export default function OperatorServiceMonitor({
{pod.name}
- 创建时间: {new Date(pod.createdAt).toLocaleString("zh-CN")} + {t("operatorMarket.detail.service.createdAt")}: {new Date(pod.createdAt).toLocaleString("zh-CN")}
@@ -215,7 +217,7 @@ export default function OperatorServiceMonitor({ {/* 资源使用情况 */}
-
CPU 使用率
+
{t("operatorMarket.detail.service.cpuUsage")}
-
内存使用率
+
{t("operatorMarket.detail.service.memoryUsage")}
-
重启次数
+
{t("operatorMarket.detail.service.restartCount")}
{pod.restarts}
@@ -254,7 +256,7 @@ export default function OperatorServiceMonitor({ }} > - 查看日志 + {t("operatorMarket.detail.service.viewLogs")}
@@ -282,7 +284,7 @@ export default function OperatorServiceMonitor({ {isDeploying ? ( -

正在部署服务...

+

{t("operatorMarket.detail.service.deploying")}

) : serviceDeployment.status === "not_deployed" ? ( {renderNotDeployed()} @@ -292,13 +294,13 @@ export default function OperatorServiceMonitor({ {/* Pod 日志模态框 */} setShowLogModal(false)} width={800} footer={[ , , ]} > diff --git a/frontend/src/pages/OperatorMarket/Detail/components/Overview.tsx b/frontend/src/pages/OperatorMarket/Detail/components/Overview.tsx index 5ee4b8b6c..54e742857 100644 --- a/frontend/src/pages/OperatorMarket/Detail/components/Overview.tsx +++ b/frontend/src/pages/OperatorMarket/Detail/components/Overview.tsx @@ -103,7 +103,8 @@ export default function Overview({ operator }) { )} {/* 输入输出格式 */} - {operator.categories?.includes('系统预置') && ( + + {['系统预置', 'System Preset'].some(cat => operator.categories?.includes(cat)) && (

{t("operatorMarket.detail.overview.supportedFormats")}

diff --git a/frontend/src/pages/OperatorMarket/Detail/components/Requirement.tsx b/frontend/src/pages/OperatorMarket/Detail/components/Requirement.tsx index 03d416218..056746c16 100644 --- a/frontend/src/pages/OperatorMarket/Detail/components/Requirement.tsx +++ b/frontend/src/pages/OperatorMarket/Detail/components/Requirement.tsx @@ -1,7 +1,10 @@ import { Card, Button } from "antd"; import { Copy } from "lucide-react"; +import { useTranslation } from "react-i18next"; export default function Requirement({ operator }) { + const { t } = useTranslation(); + const copyToClipboard = (text: string) => { navigator.clipboard.writeText(text); // 这里可以添加提示消息 @@ -11,43 +14,43 @@ export default function Requirement({ operator }) { try { requirement = JSON.parse(operator.runtime || "{}"); } catch (e) { - console.error("数据解析失败", e); + console.error(t("operatorMarket.detail.requirement.parseError"), e); } return (
{/* 系统要求 */} -

系统要求

+

{t("operatorMarket.detail.requirement.systemRequirements")}

- CPU规格 + {t("operatorMarket.detail.requirement.cpuSpec")} - {requirement?.cpu || '无限制'} + {requirement?.cpu || t("operatorMarket.detail.requirement.noLimit")}
- 内存规格 + {t("operatorMarket.detail.requirement.memorySpec")} - {requirement?.memory || "无限制"} + {requirement?.memory || t("operatorMarket.detail.requirement.noLimit")}
- 存储空间 + {t("operatorMarket.detail.requirement.storage")} - {requirement?.storage || "无限制"} + {requirement?.storage || t("operatorMarket.detail.requirement.noLimit")}
- GPU 支持 + {t("operatorMarket.detail.requirement.gpuSupport")} - {requirement?.gpu > 0 ? "是" : "否"} + {requirement?.gpu > 0 ? t("common.yes") : t("common.no")}
- NPU 支持 + {t("operatorMarket.detail.requirement.npuSupport")} - {requirement?.npu > 0 ? "是" : "否" } + {requirement?.npu > 0 ? t("common.yes") : t("common.no")}
@@ -55,7 +58,7 @@ export default function Requirement({ operator }) { {/* 依赖项 */} -

依赖项

+

{t("operatorMarket.detail.requirement.dependencies")}

{operator.requirements?.map((dep, index) => (
{/* 评分统计 */} @@ -18,7 +21,7 @@ export default function Reviews({ operator }) { ))}
- 基于 {operator.reviews.length} 个评价 + {t("operatorMarket.detail.reviews.basedOn", { count: operator.reviews.length })}
@@ -31,7 +34,7 @@ export default function Reviews({ operator }) { return (
- {rating}星 + {t("operatorMarket.detail.reviews.stars", { rating })}
{ + const handleLanguageChange = () => { + initCategoriesTree(); + }; + i18n.on('languageChanged', handleLanguageChange); + return () => { + i18n.off('languageChanged', handleLanguageChange); + }; + }, [i18n]); + const { tableData, pagination, diff --git a/frontend/src/utils/request.ts b/frontend/src/utils/request.ts index 74371466c..36f4edefa 100644 --- a/frontend/src/utils/request.ts +++ b/frontend/src/utils/request.ts @@ -1,6 +1,10 @@ import {message} from "antd"; import Loading from "./loading"; import {errorConfigStore} from "@/utils/errorConfigStore.ts"; +import i18n from "@/i18n"; +import i18n from "@/i18n"; +import i18n from "@/i18n"; +import i18n from "@/i18n"; /** * 通用请求工具类 @@ -531,6 +535,13 @@ request.addRequestInterceptor((config) => { console.error('Failed to parse session data', e); } } + + const language = i18n.language || localStorage.getItem('language') || 'zh'; + config.headers = { + ...config.headers, + 'Accept-Language': language, + }; + return config; }); diff --git a/runtime/datamate-python/app/db/models/operator.py b/runtime/datamate-python/app/db/models/operator.py index 573624614..83270efb9 100644 --- a/runtime/datamate-python/app/db/models/operator.py +++ b/runtime/datamate-python/app/db/models/operator.py @@ -2,7 +2,18 @@ Operator Market Data Models 算子市场数据模型 """ -from sqlalchemy import Column, String, Integer, Boolean, BigInteger, Text, JSON, TIMESTAMP, Index + +from sqlalchemy import ( + Column, + String, + Integer, + Boolean, + BigInteger, + Text, + JSON, + TIMESTAMP, + Index, +) from sqlalchemy.sql import func from app.db.models.base_entity import Base, BaseEntity @@ -10,6 +21,7 @@ class Operator(BaseEntity): """算子实体""" + __tablename__ = "t_operator" id = Column(String(36), primary_key=True, index=True, comment="算子ID") @@ -26,17 +38,17 @@ class Operator(BaseEntity): usage_count = Column(Integer, default=0, nullable=False, comment="使用次数") is_star = Column(Boolean, default=False, nullable=False, comment="是否收藏") - __table_args__ = ( - Index("idx_is_star", "is_star"), - ) + __table_args__ = (Index("idx_is_star", "is_star"),) class Category(BaseEntity): """算子分类实体""" + __tablename__ = "t_operator_category" id = Column(String(36), primary_key=True, index=True, comment="分类ID") - name = Column(String(255), nullable=False, comment="分类名称") + name = Column(String(255), nullable=False, comment="分类名称(中文)") + name_en = Column(String(255), nullable=True, comment="分类名称(英文)") value = Column(String(255), nullable=True, comment="分类值") type = Column(String(50), nullable=True, comment="分类类型") parent_id = Column(String(36), nullable=False, default="0", comment="父分类ID") @@ -44,6 +56,7 @@ class Category(BaseEntity): class CategoryRelation(BaseEntity): """算子分类关系实体""" + __tablename__ = "t_operator_category_relation" category_id = Column(String(36), primary_key=True, comment="分类ID") @@ -57,11 +70,14 @@ class CategoryRelation(BaseEntity): class OperatorRelease(BaseEntity): """算子发布版本实体""" + __tablename__ = "t_operator_release" id = Column(String(36), primary_key=True, comment="算子ID") version = Column(String(50), primary_key=True, comment="版本号") - release_date = Column(TIMESTAMP, nullable=False, default=func.now(), comment="发布时间") + release_date = Column( + TIMESTAMP, nullable=False, default=func.now(), comment="发布时间" + ) changelog = Column(JSON, nullable=True, comment="更新日志列表") diff --git a/runtime/datamate-python/app/module/operator/interface/category_routes.py b/runtime/datamate-python/app/module/operator/interface/category_routes.py index 7483a5f0d..5692e6f6b 100644 --- a/runtime/datamate-python/app/module/operator/interface/category_routes.py +++ b/runtime/datamate-python/app/module/operator/interface/category_routes.py @@ -2,7 +2,9 @@ Category API Routes 分类 API 路由 """ -from fastapi import APIRouter, Depends + +from typing import Optional +from fastapi import APIRouter, Depends, Header from app.db.models.operator import Category, CategoryRelation, Operator from app.db.session import get_db @@ -32,15 +34,22 @@ def get_category_service() -> CategoryService: "/tree", response_model=StandardResponse[PaginatedCategoryTree], summary="获取分类树", - description="获取算子树状分类结构,包含分组维度(如语言、模态)及资源统计数量", - tags=['mcp'] + description="获取算子树状分类结构,包含分组维度(如语言、模态)及资源统计数量。支持locale参数返回对应语言的分类名称.", + tags=["mcp"], ) async def get_category_tree( service: CategoryService = Depends(get_category_service), - db=Depends(get_db) + db=Depends(get_db), + accept_language: Optional[str] = Header(None, alias="Accept-Language"), ): """获取分类树""" - result = await service.get_all_categories(db) + # 解析语言参数 + lang = None + if accept_language: + # Accept-Language: zh-CN,zh;q=0.0 -> 取 zh + lang = accept_language.split(",")[0].split("-")[0].strip() + + result = await service.get_all_categories(db, locale=lang) return StandardResponse( code="0", @@ -52,4 +61,5 @@ async def get_category_tree( total_pages=1, star_count=result.star_count, content=result.categories, - )) + ), + ) diff --git a/runtime/datamate-python/app/module/operator/interface/operator_routes.py b/runtime/datamate-python/app/module/operator/interface/operator_routes.py index 4ae78f3a7..e5fa3817a 100644 --- a/runtime/datamate-python/app/module/operator/interface/operator_routes.py +++ b/runtime/datamate-python/app/module/operator/interface/operator_routes.py @@ -4,7 +4,7 @@ """ from typing import Optional -from fastapi import APIRouter, Depends, UploadFile, Form, File, Body +from fastapi import APIRouter, Depends, UploadFile, Form, File, Body, Header from fastapi.responses import FileResponse from app.core.logging import get_logger @@ -95,10 +95,17 @@ async def list_operators( async def get_operator( operator_id: str, service: OperatorService = Depends(get_operator_service), - db = Depends(get_db) + db = Depends(get_db), + accept_language: Optional[str] = Header(None, alias="Accept-Language"), ): """获取算子详情""" - operator = await service.get_operator_by_id(operator_id, db) + # 解析语言参数 + lang = None + if accept_language: + # Accept-Language: zh-CN,zh;q=0.0 -> 取 zh + lang = accept_language.split(",")[0].split("-")[0].strip() + + operator = await service.get_operator_by_id(operator_id, db, locale=lang) operator.file_name = None return StandardResponse(code="0", message="success", data=operator) diff --git a/runtime/datamate-python/app/module/operator/service/category_service.py b/runtime/datamate-python/app/module/operator/service/category_service.py index c84a4906a..0f7e06c1a 100644 --- a/runtime/datamate-python/app/module/operator/service/category_service.py +++ b/runtime/datamate-python/app/module/operator/service/category_service.py @@ -2,7 +2,8 @@ Category Service 分类服务层 """ -from typing import List + +from typing import List, Optional from sqlalchemy.ext.asyncio import AsyncSession @@ -33,10 +34,21 @@ def __init__( self.operator_repo = operator_repo async def get_all_categories( - self, - db: AsyncSession + self, db: AsyncSession, locale: Optional[str] = None ) -> CategoryTreePagedResponse: - """获取所有分类(树状结构)""" + """获取所有分类(树状结构) + + Args: + db: 数据库会话 + locale: 语言代码 (zh/en),None 时返回中文 + """ + + # Helper function to get localized name + def get_localized_name(cat) -> str: + if locale == "en" and getattr(cat, "name_en", None): + return cat.name_en + return cat.name + # Get all categories all_categories = await self.category_repo.find_all(db) category_map = {c.id: c for c in all_categories} @@ -58,10 +70,7 @@ async def get_all_categories( grouped_by_parent[cat.parent_id].append(cat) # Build category trees - parent_ids = sorted( - grouped_by_parent.keys(), - key=lambda pid: pid - ) + parent_ids = sorted(grouped_by_parent.keys(), key=lambda pid: pid) category_trees = [] for parent_id in parent_ids: @@ -74,7 +83,7 @@ async def get_all_categories( for cat in sorted(group, key=lambda c: c.created_at or 0): cat_dto = CategoryDto( id=cat.id, - name=cat.name, + name=get_localized_name(cat), value=cat.value, type=cat.type, parent_id=cat.parent_id, @@ -86,7 +95,7 @@ async def get_all_categories( tree = CategoryTreeResponse( id=parent_id, - name=parent_category.name, + name=get_localized_name(parent_category), count=total_count, categories=child_dtos, ) diff --git a/runtime/datamate-python/app/module/operator/service/operator_service.py b/runtime/datamate-python/app/module/operator/service/operator_service.py index aced839d9..19fb4e061 100644 --- a/runtime/datamate-python/app/module/operator/service/operator_service.py +++ b/runtime/datamate-python/app/module/operator/service/operator_service.py @@ -230,16 +230,21 @@ async def count_operators( async def get_operator_by_id( self, operator_id: str, - db: AsyncSession + db: AsyncSession, + locale: Optional[str] = None ) -> OperatorDto: """根据 ID 获取算子详情""" + attr = "category_name" + if locale == "en": + attr = "category_name_en" + result = await db.execute( - text(""" + text(f""" SELECT operator_id, operator_name, description, version, inputs, outputs, runtime, settings, is_star, file_name, file_size, usage_count, metrics, created_at, updated_at, created_by, updated_by, - string_agg(category_name, ',' ORDER BY created_at DESC) AS categories + string_agg({attr}, ',' ORDER BY created_at DESC) AS categories FROM v_operator WHERE operator_id = :operator_id GROUP BY operator_id, operator_name, description, version, inputs, outputs, runtime, diff --git a/scripts/db/data-operator-init.sql b/scripts/db/data-operator-init.sql index 286ec17b8..8dff750c1 100644 --- a/scripts/db/data-operator-init.sql +++ b/scripts/db/data-operator-init.sql @@ -61,6 +61,7 @@ CREATE TABLE IF NOT EXISTS t_operator_category ( id VARCHAR(64) PRIMARY KEY, name VARCHAR(64) UNIQUE, + name_en VARCHAR(64), value VARCHAR(64) UNIQUE, type VARCHAR(64), parent_id VARCHAR(64), @@ -143,32 +144,33 @@ SELECT o.created_by, o.updated_by, toc.id AS category_id, - toc.name AS category_name + toc.name AS category_name, + toc.name_en AS category_name_en FROM t_operator_category_relation tocr LEFT JOIN t_operator o ON tocr.operator_id = o.id LEFT JOIN t_operator_category toc ON tocr.category_id = toc.id; COMMENT ON VIEW v_operator IS '算子视图'; -INSERT INTO t_operator_category(id, name, value, type, parent_id) -VALUES ('64465bec-b46b-11f0-8291-00155d0e4808', '模态', 'modal', 'predefined', '0'), - ('873000a2-65b3-474b-8ccc-4813c08c76fb', '语言', 'language', 'predefined', '0'), - ('4857cc9e-7b72-429e-b2a8-ddd1c48c4483', '功能', 'function', 'predefined', '0'), - ('d8a5df7a-52a9-42c2-83c4-01062e60f597', '文本', 'text', 'predefined', '64465bec-b46b-11f0-8291-00155d0e4808'), - ('de36b61c-9e8a-4422-8c31-d30585c7100f', '图片', 'image', 'predefined', '64465bec-b46b-11f0-8291-00155d0e4808'), - ('42dd9392-73e4-458c-81ff-41751ada47b5', '音频', 'audio', 'predefined', '64465bec-b46b-11f0-8291-00155d0e4808'), - ('a233d584-73c8-4188-ad5d-8f7c8dda9c27', '视频', 'video', 'predefined', '64465bec-b46b-11f0-8291-00155d0e4808'), - ('4d7dbd77-0a92-44f3-9056-2cd62d4a71e4', '多模态', 'multimodal', 'predefined', '64465bec-b46b-11f0-8291-00155d0e4808'), - ('9eda9d5d-072b-499b-916c-797a0a8750e1', 'Python', 'python', 'predefined', '873000a2-65b3-474b-8ccc-4813c08c76fb'), - ('8c09476a-a922-418f-a908-733f8a0de521', '清洗', 'cleaning', 'predefined', '4857cc9e-7b72-429e-b2a8-ddd1c48c4483'), - ('cfa9d8e2-5b5f-4f1e-9f12-1234567890ab', '标注', 'annotation', 'predefined', '4857cc9e-7b72-429e-b2a8-ddd1c48c4483'), - ('16e2d99e-eafb-44fc-acd0-f35a2bad28f8', '来源', 'origin', 'predefined', '0'), - ('96a3b07a-3439-4557-a835-525faad60ca3', '系统预置', 'predefined', 'predefined', '16e2d99e-eafb-44fc-acd0-f35a2bad28f8'), - ('ec2cdd17-8b93-4a81-88c4-ac9e98d10757', '用户上传', 'customized', 'predefined', '16e2d99e-eafb-44fc-acd0-f35a2bad28f8'), - ('0ed75eea-e20b-11f0-88e6-00155d5c9528', '归属', 'vendor', 'predefined', '0'), - ('431e7798-5426-4e1a-aae6-b9905a836b34', 'DataMate', 'datamate', 'predefined', '0ed75eea-e20b-11f0-88e6-00155d5c9528'), - ('79b385b4-fde8-4617-bcba-02a176938996', 'DataJuicer', 'data-juicer', 'predefined', '0ed75eea-e20b-11f0-88e6-00155d5c9528'), - ('f00eaa3e-96c1-4de4-96cd-9848ef5429ec', '其他', 'others', 'predefined', '0ed75eea-e20b-11f0-88e6-00155d5c9528') +INSERT INTO t_operator_category(id, name, name_en, value, type, parent_id) +VALUES ('64465bec-b46b-11f0-8291-00155d0e4808', '模态', 'Modal', 'modal', 'predefined', '0'), + ('873000a2-65b3-474b-8ccc-4813c08c76fb', '语言', 'Language', 'language', 'predefined', '0'), + ('4857cc9e-7b72-429e-b2a8-ddd1c48c4483', '功能', 'Function', 'function', 'predefined', '0'), + ('d8a5df7a-52a9-42c2-83c4-01062e60f597', '文本', 'Text', 'text', 'predefined', '64465bec-b46b-11f0-8291-00155d0e4808'), + ('de36b61c-9e8a-4422-8c31-d30585c7100f', '图片', 'Image', 'image', 'predefined', '64465bec-b46b-11f0-8291-00155d0e4808'), + ('42dd9392-73e4-458c-81ff-41751ada47b5', '音频', 'Audio', 'audio', 'predefined', '64465bec-b46b-11f0-8291-00155d0e4808'), + ('a233d584-73c8-4188-ad5d-8f7c8dda9c27', '视频', 'Video', 'video', 'predefined', '64465bec-b46b-11f0-8291-00155d0e4808'), + ('4d7dbd77-0a92-44f3-9056-2cd62d4a71e4', '多模态', 'Multimodal', 'multimodal', 'predefined', '64465bec-b46b-11f0-8291-00155d0e4808'), + ('9eda9d5d-072b-499b-916c-797a0a8750e1', 'Python', 'Python', 'python', 'predefined', '873000a2-65b3-474b-8ccc-4813c08c76fb'), + ('8c09476a-a922-418f-a908-733f8a0de521', '清洗', 'Cleaning', 'cleaning', 'predefined', '4857cc9e-7b72-429e-b2a8-ddd1c48c4483'), + ('cfa9d8e2-5b5f-4f1e-9f12-1234567890ab', '标注', 'Annotation', 'annotation', 'predefined', '4857cc9e-7b72-429e-b2a8-ddd1c48c4483'), + ('16e2d99e-eafb-44fc-acd0-f35a2bad28f8', '来源', 'Origin', 'origin', 'predefined', '0'), + ('96a3b07a-3439-4557-a835-525faad60ca3', '系统预置', 'System Preset', 'predefined', 'predefined', '16e2d99e-eafb-44fc-acd0-f35a2bad28f8'), + ('ec2cdd17-8b93-4a81-88c4-ac9e98d10757', '用户上传', 'User Upload', 'customized', 'predefined', '16e2d99e-eafb-44fc-acd0-f35a2bad28f8'), + ('0ed75eea-e20b-11f0-88e6-00155d5c9528', '归属', 'Vendor', 'vendor', 'predefined', '0'), + ('431e7798-5426-4e1a-aae6-b9905a836b34', 'DataMate', 'DataMate', 'datamate', 'predefined', '0ed75eea-e20b-11f0-88e6-00155d5c9528'), + ('79b385b4-fde8-4617-bcba-02a176938996', 'DataJuicer', 'DataJuicer', 'data-juicer', 'predefined', '0ed75eea-e20b-11f0-88e6-00155d5c9528'), + ('f00eaa3e-96c1-4de4-96cd-9848ef5429ec', '其他', 'Others', 'others', 'predefined', '0ed75eea-e20b-11f0-88e6-00155d5c9528') ON CONFLICT DO NOTHING; INSERT INTO t_operator