Merge remote-tracking branch 'origin/feature/1.6-cx' into feature/1.6-liujian

This commit is contained in:
Liujian
2025-03-10 19:47:56 +08:00
25 changed files with 1125 additions and 175 deletions
@@ -15,6 +15,8 @@ export type DrawerWithFooterProps = DrawerProps & {
extraBtn?: React.ReactNode
okBtnTitle?: string
cancelBtnTitle?: string
width?: string
footerLeft?: React.ReactNode
}
export function DrawerWithFooter(props: DrawerWithFooterProps) {
const {
@@ -32,7 +34,9 @@ export function DrawerWithFooter(props: DrawerWithFooterProps) {
onLastStep,
notAutoClose,
showOkBtn = true,
extraBtn
extraBtn,
width = '60%',
footerLeft
} = props
const [submitLoading, setSubmitLoading] = useState<boolean>(false)
const handlerSubmit = () => {
@@ -56,23 +60,26 @@ export function DrawerWithFooter(props: DrawerWithFooterProps) {
push={false}
title={title}
placement={placement}
width="60%"
width={width}
destroyOnClose={true}
maskClosable={false}
classNames={{ footer: 'text-right' }}
footer={
<Space className="flex flex-row-reverse" style={{}}>
{showOkBtn && (
<WithPermission access={submitAccess}>
<Button onClick={handlerSubmit} type="primary" loading={submitLoading} disabled={submitDisabled}>
{okBtnTitle}
</Button>
</WithPermission>
)}
{showLastStep && <Button onClick={onLastStep ?? onClose}> {$t('上一步')}</Button>}
{extraBtn}
<Button onClick={onClose}>{cancelBtnTitle ?? (showOkBtn ? $t('取消') : $t('关闭'))}</Button>
</Space>
<div className={footerLeft ? 'flex justify-between items-center' : ''}>
{footerLeft}
<Space className="flex flex-row-reverse" style={{}}>
{showOkBtn && (
<WithPermission access={submitAccess}>
<Button onClick={handlerSubmit} type="primary" loading={submitLoading} disabled={submitDisabled}>
{okBtnTitle}
</Button>
</WithPermission>
)}
{showLastStep && <Button onClick={onLastStep ?? onClose}> {$t('上一步')}</Button>}
{extraBtn}
<Button onClick={onClose}>{cancelBtnTitle ?? (showOkBtn ? $t('取消') : $t('关闭'))}</Button>
</Space>
</div>
}
onClose={onClose}
open={open}
@@ -25,7 +25,7 @@
"已发布": "K7a369eef",
"下线": "Kcfa1a4d2",
"上线": "K771dc3b7",
"查看": "K530f5951",
"查看 ": "Kf553a17e",
"删除": "Kecbd7449",
"确认": "K1cbe2507",
"搜索(0)名称": "K48325b6",
@@ -378,9 +378,13 @@
"路由": "Kf9dcef3a",
"添加路由": "K6134bbe8",
"输入 URL 查找路由": "Kf85b83a0",
"线上模型": "K84b2cf2d",
"本地模型": "Kdbf37ece",
"模型类型": "Kc7f7aa98",
"模型供应商": "Kcf9f90b8",
"参数": "Ke99513a0",
"审核": "Kb595f40",
"查看": "K530f5951",
"通过": "K54e27f57",
"拒绝": "K8582af3f",
"发布结果": "Kd568e15c",
@@ -392,19 +396,15 @@
"终止发布": "Ke1b79b93",
"请确认是否终止发布?": "Ka2449180",
"新建版本": "K2cb02f38",
"未配置 AI 模型": "Kd752a3a8",
"前往设置": "K8b7ac871",
"AI 模型": "K99935e6f",
"配置好 AI 模型后,你可以使用对应的大模型来创建 AI 服务": "K2260837a",
"已设置": "Kf97448b3",
"未设置": "K30d4d8df",
"在线模型": "K42213ffa",
"保存后供应商状态变为【停用】,使用本供应商的 API 将临时使用负载优先级最高的正常供应商。": "Ke32702ac",
"保存后供应商状态变为【正常】,恢复调用本供应商的 AI 能力。": "Ka08c28d4",
"添加 (0) 模型": "K500b7535",
"添加自定义供应商": "K10a99afa",
"默认模型": "Kc2ee5223",
"负载优先级": "K608af899",
"负载优先级决定在原供应商异常或停用后,优先使用哪一个供应商。优先级数字越小,优先级越高。": "K65b7a96",
"优先级必须大于 0": "K9eccff16",
"请输入优先级": "Kfcf02780",
"添加模型": "K663648ae",
"API Key(默认 Key": "K5c6dcf58",
"LLM 状态管理": "K59bf8ed9",
"当前调用状态:": "Kab8fe398",
@@ -412,11 +412,46 @@
"停用": "Kedd64e4d",
"异常": "K23a3bd72",
"启用": "K52c8a730",
"已配置": "K66a7d24c",
"未配置": "Kaf074220",
"默认": "Kd9a46c29",
"默认:": "K2a3aeb8d",
"Ollama 地址": "K6b99dce8",
"输入例如:https://www.apipark.com": "K8d4f5b44",
"自定义": "K8929cbb1",
"模型名称": "K1fd51aaa",
"访问配置": "Kd6285399",
"模型参数模板": "K7eb03edd",
"模型参数": "K49b434e9",
"供应商名称": "K16ef56b1",
"注意:": "K484de451",
"仅支持使用 OpenAI 输入输出格式和认证方法(APIKey)的供应商。如果不满足此条件,创建后自定义供应商将不可用。": "Kbd80dde0",
"从 (0) 获取 API KEY": "Kb3e34847",
"该模型为官方模型,不可编辑": "Kcb6a1c57",
"存在使用当前模型的接口,需要先解绑后才能编辑": "Kf9300eb4",
"该模型为官方模型,不可删除": "K8af71816",
"存在使用当前模型的接口,需要先解绑后才能删除": "Kb8ad0af5",
"模型值": "K73cb9ff1",
"接口请求时若指定使用当前模型,则需要在 Model 参数中填入以下值。例如若想使用火山引擎的 Deepseek-R1 模型,那么就需要填入 Volcengine/Deepseek-R1 , 值的大小写不敏感。": "Kd9643194",
"编辑 (0) 模型": "K60b54d0",
"确定删除吗?": "Kbf48dc2d",
"删除成功": "K28190dbc",
"删除失败": "K3d2324d0",
"配置 Ollama 服务": "K481442d3",
"如何部署 Ollama": "Kf9b341e3",
"模型部署服务未配置": "K8632bef2",
"配置服务": "Kbbd8ce81",
"模型设置": "K15e69f64",
"部署本地模型": "K68f1c446",
"删除模型": "K953bbe54",
"有": "K1bbe8b92",
"个API使用当前模型,删除当前的模型配置后,该模型相关的API将会切换为使用负载均衡中优先级最高的可用模型。并且当前模型下的所有API KEY和相关数据将会被清空,是否确认删除当前模型?": "Kca29bf8b",
"当前模型为最后一个模型,不支持删除": "Kf02ec68c",
"部署过程": "Kf63cb5b4",
"Apis": "K2b2e787c",
"请输入名称搜索": "Kd25acba1",
"部署模型": "K11372aaf",
"Models": "Ke37a353f",
"Keys": "K14bcebd2",
"添加供应商": "Kd87397b0",
"编辑供应商": "K5bcf8c48",
"(0) 模型": "Kf7a916be",
"待审核": "K35612f29",
"已审核": "K47eaafde",
"发布申请": "K56b4254f",
@@ -434,6 +469,14 @@
"分类名称": "Ke595a20a",
"父分类 ID": "K9679728f",
"子分类名称": "K9b2d08fd",
"添加 Rest 服务": "K2c93168c",
"导入OpenAPI文档,将现有系统的API发布到APIPark。": "K39a8d392",
"添加在线 AI API": "K68932d54",
"添加公有云AI模型的 API Key,通过APIPark 统一调用公有云的AI模型。": "Ka742e079",
"本地部署 AI 并生成 API": "K8341389c",
"快速在本地部署开源模型并自动生成 API。": "Kf4e629f9",
"部署": "K26b9d431",
"⚡您可快速通过以下方式开放API供大家使用:": "K3af90490",
"快速接入 AI": "K71671763",
"配置你的 AI 模型": "Ka8a5ec5",
"通过 APIPark 快速接入各种 AI 模型,使用统一的格式来调用API,并且可以随意切换模型。": "K10d7e99f",
@@ -462,14 +505,19 @@
"集成": "K4057391a",
"APIPark 提供详尽的 API 调用日志,帮助企业监控、分析和审计 API 的运行状况。": "K3453272",
"Hello!欢迎使用 APIPark": "Kd518ba3e",
"你能通过 APIPark 快速在企业内部构建 API 开放门户/市场,享受极致的转发性能、API 可观测、服务治理、多租户管理、订阅审核流程等诸多好处。": "K7e04ea16",
"如果你喜欢我们的产品,欢迎给我们 Star 或提供产品反馈意见。": "Kedd41c18",
"是开源的一站式 AI 网关与 API 门户,可快速接入 OpenAI/DeepSeek 等各类 AI 模型,通过统一请求格式避免模型切换对业务造成影响,提供企业级 API 安全防护(鉴权/限流/敏感词过滤)与实时用量监控,支持团队内 API 共享协作,管理接口订阅授权并保证您的API安全。": "K8097d6be",
"✨ 欢迎在 Github 为我们 Star 或提供产品反馈意见。": "Kf1ce5b3",
"点击这里": "K8facd134",
"点击": "K96871eb8",
"快速入门": "Kef02fd87",
"我们提供了一些任务来帮你快速了解 APIPark": "K43a3b38d",
"进阶教程": "K408bfcf1",
"了解 APIPark 如何更好地管理 API 和 AI": "K1afaf20e",
"了解更多功能": "K48f7e21f",
"隐藏该教程": "K698296e2",
"热点模型": "K40c527de",
"Tags": "Kcf8e3b18",
"选择 OpenAPI 文件 (.json / .yaml)": "Kcdb675ed",
"请输入 APIKey": "K8b88ef63",
"API Key": "Kcbd30819",
"请填写 APIKey": "Kcb6e2d3e",
@@ -482,16 +530,17 @@
"请选择状态": "K3fde5b49",
"添加 (0) APIKey": "K4880fd04",
"编辑 APIKey": "K434b7e76",
"删除成功": "K28190dbc",
"停用成功": "Kb5fcf5b8",
"启用成功": "K5940d788",
"排序成功": "K8743bccd",
"编辑": "Kad207008",
"调用优先级": "K19590c2c",
"APIKey 资源池": "Kefb03657",
"支持单个 API 模型供应商下创建多个 APIKey APIKey 进行智能负载均衡": "Kc0352e64",
"请输入名称搜索": "Kd25acba1",
"API Key 负载": "K9ae48909",
"支持单个 API 模型供应商下创建多个 API Key 进行智能负载均衡": "K437724fc",
"添加 APIKey": "K6d0388a0",
"模型灾备": "K7d17707e",
"系统自动识别异常AI模型后,自动替换成以下优先级最高的可用模型。这将确保您的AI应用保持高可用性和最佳性能,从而防止任何单个LLM异常成为您的性能瓶颈。": "Kfac16394",
"请输入...": "K769d59d",
"请输入账号": "Kf076f63c",
"账号": "K80a560a1",
"请输入密码": "K25c895d5",
@@ -547,6 +596,7 @@
"集群": "Ke93d36ed",
"修改配置": "K877985b7",
"设置访问 API 的集群,让 API 在分布式环境中稳定运行,并且能够根据业务需求进行灵活扩展和优化。": "Kdf66a675",
"未配置": "Kaf074220",
"私有网络": "Ke1b1865",
"公共网络": "K4786c57c",
"管理地址": "Kf12b3034",
@@ -603,6 +653,14 @@
"请求协议": "K6bc47edb",
"请求方式": "K1365fe45",
"转发规则设置": "K90f3c02f",
"下载": "K65b21404",
"初始化": "K7cc5269",
"停止部署": "Kf9308d46",
"确定停止部署吗?": "K3de04ec6",
"删除服务": "Kde6bae17",
"确定删除服务吗?": "K881fef4c",
"停止": "K24540de",
"继续等待": "Kd85b3f64",
"只允许上传PNG、JPG或SVG格式的图片": "Ka9c08390",
"服务名称": "K413b9869",
"服务类型": "K9919285b",
@@ -612,13 +670,14 @@
"未配置任何 AI 模型供应商,": "Kcab588a9",
"立即配置": "Kb9b56111",
"API 调用前缀": "Kcf756b7a",
"作为服务内所有API的前缀,比如host/{service_name}/{api_path}一旦保存无法修改": "K13edc043",
"作为服务内所有API的前缀,比如host/{service_name}/{api_path}影响较大,谨慎修改": "K1400a1fc",
"所属服务分类": "Kf52a584d",
"设置服务展示在服务市场中的哪个分类下": "K72b21be5",
"图标": "Kdc840242",
"仅支持 .png .jpg .jpeg .svg 格式的图片文件, 大于 1KB 的文件将被压缩": "K427a5bd5",
"仅支持 .png .jpg .jpeg .svg 格式的图片文件": "Kc007db4a",
"Logo": "K44bc352d",
"删除服务": "Kde6bae17",
"模型值重定向": "Kf62170f0",
"本项非必填,用于简化请求中的 Model 参数值,key 为希望使用的简化值,value 为原来的对应值。例如: {\"deepseek-r1\":\"ollama/deepseek-r1-1.5b-qwen-distill-fp16\"}": "K5e28ee82",
"删除操作不可恢复,请谨慎操作!": "K885ea699",
"上游": "Kda8d5ea1",
"服务提供了高性能 API 网关,并且可以无缝接入多种大型 AI 模型,并将这些 AI 能力打包成 API 进行调用,从而大幅简化了 AI 模型的使用门槛。同时,我们的平台提供了完善的 API 管理功能,支持 API 的创建、监控、访问控制等,保障开发者可以高效、安全地开发和管理 API 服务。": "K12f58863",
@@ -722,6 +781,7 @@
"步骤二:导入 API 文档数据": "K49b81d06",
"可通过以下 URL 或 下载 Json 文件,导入 API 文档数据到 Agent 平台中。": "K4a3b62be",
"复制 URL": "K42697e11",
"或": "Kb8185132",
"下载 Json 文件": "K27a809c5",
"步骤三:配置 API 密钥": "K1e61fdee",
"在": "K55912595",
@@ -871,5 +871,32 @@
"Kf1ce5b3": "✨ We'd love your support on Github! Leave us a star or share your feedback. ",
"K3af90490": "⚡ You can quickly open the API for everyone to use via the following methods:",
"K6b99dce8": "address",
"Kce2fcdbf": "No Permission"
"K500b7535": "Add (0) Models",
"Kd87397b0": "Add Provider",
"K5bcf8c48": "Edit Provider",
"K10a99afa": "Add Custom Provider",
"Kd6285399": "Access Configuration",
"K7eb03edd": "Model Parameters Template",
"K49b434e9": "Model Parameters",
"K16ef56b1": "Provider Name",
"K484de451": "Notice: ",
"Kbd80dde0": "Only providers that use the OpenAI input and output format and the authentication method (APIKey) are supported. If this condition is not met, the custom provider will not be available after it is created.",
"K73cb9ff1": "Model Value",
"Kd9643194": "If the current model is specified in an API request, the following value must be entered in the Model parameter. For example, to use VolcEngine's Deepseek-R1 model, you need to enter 'Volcengine/Deepseek-R1'. The value is case-insensitive.",
"Kf7a916be": "(0) Model",
"Ke37a353f": "Models",
"Kf62170f0": "Model Redirection",
"K5e28ee82": "This field is optional and is used to simplify the Model parameter value in requests. The key represents the desired simplified value, and the value corresponds to the original value. For example: {\"deepseek-r1\":\"ollama/deepseek-r1-1.5b-qwen-distill-fp16\"}",
"K8929cbb1": "Customize",
"Kcb6a1c57": "This model is an official model and cannot be edited.",
"Kf9300eb4": "There are interfaces using the current model. Please unbind them before editing.",
"K8af71816": "This model is an official model and cannot be deleted.",
"Kb8ad0af5": "There are interfaces using the current model. Please unbind them before deleting.",
"K60b54d0": "Edit (0) Model",
"Kbf48dc2d": "Are you sure you want to delete it?",
"K3d2324d0": "Deletion failed.",
"Kce2fcdbf": "No Permission",
"K24f6a5b4": "Custom (Empty Template)",
"Kea608112": "Load Preset Template",
"Kee7de862": "Edit Provider( (0) )"
}
@@ -893,5 +893,32 @@
"Kf1ce5b3": "✨ Githubでスターを付けていただくか、製品フィードバックをお寄せください。",
"K3af90490": "⚡ 以下の方法で、APIをすぐに公開して皆さんに利用してもらえます:",
"K6b99dce8": "Ollama アドレス",
"Kce2fcdbf": "権限がありません"
"K500b7535": "(0) モデルを追加",
"Kd87397b0": "サプライヤーを追加",
"K5bcf8c48": "サプライヤーを編集",
"K10a99afa": "カスタムサプライヤーを追加",
"Kd6285399": "アクセス設定",
"K7eb03edd": "モデルパラメータテンプレート",
"K49b434e9": "モデルパラメータ",
"K16ef56b1": "サプライヤー名",
"K484de451": "注意:",
"Kbd80dde0": "OpenAI の入出力フォーマットと認証方法(APIKey)を使用するサプライヤーのみサポートされています。この条件を満たさない場合、作成後にカスタムサプライヤーは利用できません。",
"K73cb9ff1": "モデル値",
"Kd9643194": "APIリクエスト時に現在のモデルを指定する場合、Model パラメータに以下の値を入力する必要があります。例えば、VolcEngine の Deepseek-R1 モデルを使用する場合、「Volcengine/Deepseek-R1」と入力してください。この値は大文字小文字を区別しません。",
"Kf7a916be": "(0) モデル",
"Ke37a353f": "Models",
"Kf62170f0": "モデル値のリダイレクト",
"K5e28ee82": "この項目は必須ではなく、リクエスト内の Model パラメータ値を簡略化するために使用されます。キーは希望する簡略値を表し、値は元の対応する値を示します。例えば: {\"deepseek-r1\":\"ollama/deepseek-r1-1.5b-qwen-distill-fp16\"}",
"K8929cbb1": "カスタマイズ",
"Kcb6a1c57": "このモデルは公式モデルのため、編集できません。",
"Kf9300eb4": "現在のモデルを使用しているインターフェースがあります。編集する前に、まず解除してください。",
"K8af71816": "このモデルは公式モデルのため、削除できません。",
"Kb8ad0af5": "現在のモデルを使用しているインターフェースがあります。削除する前に、まず解除してください。",
"K60b54d0": "編集 (0) モデル",
"Kbf48dc2d": "本当に削除しますか?",
"K3d2324d0": "削除に失敗しました。",
"Kce2fcdbf": "権限がありません",
"K24f6a5b4": "カスタム(空のテンプレート)",
"Kea608112": "プリセットテンプレートを読み込む",
"Kee7de862": "サプライヤーを編集( (0) )"
}
@@ -1,6 +1 @@
{
"Ke32702ac": "保存后供应商状态变为【停用】,使用本供应商的 API 将临时使用负载优先级最高的正常供应商。",
"Ka08c28d4": "保存后供应商状态变为【正常】,恢复调用本供应商的 AI 能力。",
"Kab8fe398": "当前调用状态:",
"K4880fd04": "添加 (0) APIKey"
}
{}
@@ -1,6 +1 @@
{
"Ke32702ac": "保存后供应商状态变为【停用】,使用本供应商的 API 将临时使用负载优先级最高的正常供应商。",
"Ka08c28d4": "保存后供应商状态变为【正常】,恢复调用本供应商的 AI 能力。",
"Kab8fe398": "当前调用状态:",
"K4880fd04": "添加 (0) APIKey"
}
{}
@@ -44,12 +44,8 @@
"Kd2850420": "待删除",
"K83237c89": "输入的IP或CIDR不符合格式",
"K5ae2c87a": "请正确输入路径,如/usr/*或*/usr/*",
"Ke32702ac": "保存后供应商状态变为【停用】,使用本供应商的 API 将临时使用负载优先级最高的正常供应商。",
"Ka08c28d4": "保存后供应商状态变为【正常】,恢复调用本供应商的 AI 能力。",
"Kab8fe398": "当前调用状态:",
"K508d8bf4": "集成地址",
"K67f4e9bb": "与外部平台集成时,获取 API 市场中文档信息的域名",
"K4880fd04": "添加 (0) APIKey",
"Kc82b8374": "编辑策略",
"K4b34a5e5": "策略类型",
"K57f0fee8": "匹配条件",
@@ -1,6 +1 @@
{
"Ke32702ac": "保存后供应商状态变为【停用】,使用本供应商的 API 将临时使用负载优先级最高的正常供应商。",
"Ka08c28d4": "保存后供应商状态变为【正常】,恢复调用本供应商的 AI 能力。",
"Kab8fe398": "当前调用状态:",
"K4880fd04": "添加 (0) APIKey"
}
{}
@@ -10,14 +10,40 @@
"K631d646f": "Open API",
"K1196b104": "APIPark",
"Kf1b166e7": "Details",
"K66a7d24c": "Set",
"Kd9a46c29": "Default",
"K7ac2be34": "AI Model Management",
"K18dccc1a": "Sync Latest Model",
"K7e04ea16": "🦄 APIPark is an open-source, all-in-one AI gateway and API developer portal, enabling enterprises and developers to quickly integrate over 100 AI models, combine AI models and prompts into new APIs, and standardize all AI request data formats, ensuring that switching AI models or adjusting prompts does not affect your APP or microservice. Additionally, APIParks developer portal allows you to share APIs within your team, manage applications that call your APIs, and ensure API security, while monitoring your AI API usage with clear charts.",
"Kedd41c18": "✨ If you like APIPark, please support us with a Github Star.",
"K5cfdd950": "This data cannot be recovered after deletion. Are you sure you want to delete?",
"K28435c5c": "API Details",
"K13edc043": "As a prefix for all APIs in the service, e.g., host/{service_name}/{api_path}, cannot be modified once saved.",
"K427a5bd5": "Only .png, .jpg, .jpeg, .svg format image files are supported. Files larger than 1KB will be compressed.",
"Kb9052305": "Search Username, Email",
"Kbe3e9335": "Exit Test",
"K40a89bd8": "Enter Name, ID to Search Member",
"K1da86266": "Invalid",
"Kb147fabc": "Create",
"K40ca4f2": "Update"
"K40ca4f2": "Update",
"Kd752a3a8": "AI Model Not Configured",
"K8b7ac871": "Go to Settings",
"Kf97448b3": "Configured",
"K30d4d8df": "Not Configured",
"K608af899": "Load Priority",
"K65b7a96": "Load priority determines which provider to use first when the original provider is abnormal or disabled. The smaller the priority number, the higher the priority.",
"K9eccff16": "Priority must be greater than 0",
"Kfcf02780": "Please enter priority",
"K2a3aeb8d": "Default:",
"Kefb03657": "APIKey Resource Pool",
"Kc0352e64": "Supports creating multiple APIKeys under a single API model provider for intelligent load balancing",
"K31086771": "Supports batch addition of existing API documents for unified external access",
"K659140c3": "Quickly call cloud service API of AI model, conveniently manage prompt and unified billing",
"Kbb028f95": "Add Load Balancing",
"Ka791de39": "Deploying",
"Kf7056787": "Public Service",
"Kbe98ba9e": "Private Service",
"K83829a3b": "Model Exception",
"Kb92fb02b": "Deployment Failed",
"Kacf10c44": "Ollama Endpoint"
}
@@ -13,13 +13,19 @@
"Kffd7e274": "無審査:すべてのアプリケーションがこのサービスにサブスクライブできます",
"K8a8b13e4": "手動審査:承認されたアプリケーションのみがこのサービスにサブスクライブできます",
"Kf1b166e7": "詳細",
"K66a7d24c": "設定済み",
"Kd9a46c29": "デフォルト",
"K7ac2be34": "AI モデル管理",
"K18dccc1a": "最新モデルを同期",
"K9bdd8403": "API を安全に呼び出すためには、アプリケーションとトークンを作成する必要があります。",
"Kc8239422": "チームにはユーザー、アプリケーション、サービスが含まれ、異なるチームのアプリケーションとサービスのデータは分離されています。企業内の部門/プロジェクトグループ/チームの管理に使用できます。",
"Ka0a8840a": "他のアプリケーションのサブスクリプション申請をレビューし、承認後に API リクエストが発行できます。",
"K7e04ea16": "🦄 APIPark は、100 以上の AI モデルに簡単に接続できるオープンソースの AI ゲートウェイと API 開発者ポータルです。AI モデルとプロンプトを新しい API に組み合わせ、すべての AI リクエストのデータ形式を統一し、AI モデルを切り替えたりプロンプトを調整したりしても、アプリケーションやマイクロサービスに影響を与えません。また、APIPark の開発者ポータルを使用してチーム内で API を共有し、アプリケーションを管理し、API のセキュリティを確保し、AI API の使用状況を監視するための明確なグラフを提供します。",
"Kedd41c18": "✨ APIParkが好きなら、Githubでスターや製品フィードバックの意見を提供してください。",
"K5cfdd950": "このデータを削除すると、復元できません。削除しますか?",
"K28435c5c": "API 詳細",
"K13edc043": "すべての API に適用されるプレフィックス(例: host/{service_name}/{api_path})、保存後は変更できません",
"K427a5bd5": "PNG、JPG、JPEG、SVG 形式の画像ファイルのみをサポートし、1KB を超えるファイルは圧縮されます",
"Kb9052305": "ユーザー名またはメールを検索",
"K5ece3bac": "チームとメンバーを設定してから、チーム内でサービスとアプリケーションを作成し、API をサブスクライブできます。メンバーは所属チーム内のサービスとアプリケーションのみを表示できます。",
"K1512e983": "アプリケーション呼び出し統計",
@@ -41,5 +47,25 @@
"K40a89bd8": "名前または ID を入力してサービスを検索",
"K1da86266": "無効",
"Kb147fabc": "新規作成",
"K40ca4f2": "更新"
"K40ca4f2": "更新",
"Kd752a3a8": "AI モデルが設定されていません",
"K8b7ac871": "設定へ移動",
"Kf97448b3": "設定済み",
"K30d4d8df": "未設定",
"K608af899": "負荷優先度",
"K65b7a96": "負荷優先度は、元のプロバイダーが異常または停止した場合に、どのプロバイダーを優先的に使用するかを決定します。優先度の数値が小さいほど、優先度が高くなります。",
"K9eccff16": "優先度は0より大きい必要があります",
"Kfcf02780": "優先度を入力してください",
"K2a3aeb8d": "デフォルト:",
"Kefb03657": "API キーリソースプール",
"Kc0352e64": "単一の API モデルプロバイダーで複数の API キーを作成し、インテリジェントな負荷分散を実現できます",
"K31086771": "既存の API ドキュメントを一括追加し、統一された外部アクセスを実現できます。",
"K659140c3": "AI モデルのクラウド API を素早く呼び出し、プロンプト管理や一元的な課金管理を簡単にします。",
"Kbb028f95": "ロードバランシングを追加",
"Ka791de39": "デプロイ中",
"Kf7056787": "パブリックサービス",
"Kbe98ba9e": "プライベートサービス",
"K83829a3b": "モデル異常",
"Kb92fb02b": "デプロイ失敗",
"Kacf10c44": "Ollama エンドポイント"
}
@@ -13,13 +13,19 @@
"Kffd7e274": "无审核:允许所有消费者订阅该服务",
"K8a8b13e4": "人工审核:仅允许审核通过的消费者订阅该服务",
"Kf1b166e7": "详情",
"K66a7d24c": "已设置",
"Kd9a46c29": "默认",
"K7ac2be34": "AI 模型管理",
"K18dccc1a": "同步最新模型",
"K9bdd8403": "为了安全地调用 API,你需要创建一个消费者以及Token。",
"Kc8239422": "团队中包含了人员、消费者和服务,不同团队之间的消费者和服务数据是隔离的,可用于管理企业内部不同的部门/项目组/团队。",
"Ka0a8840a": "审核其他消费者的订阅申请,审核通过后的才可发起 API 请求。",
"K7e04ea16": "🦄 APIPark 是开源的一站式 AI 网关和 API 开发者门户,帮助企业和开发者快速接入 100+ AI 模型,将 AI 模型和 Prompt 提示词组合成新的 API,并且统一所有 AI 的请求数据格式,避免切换 AI 模型或调整提示词时影响你的 APP 消费者或者微服务。你还可以通过 APIPark 的开发者门户在团队内共享 API,管理调用的消费者并保障你的 API 安全,通过清晰的图表来监控你的 AI API 使用情况。",
"Kedd41c18": "✨ 如果你喜欢 APIPark,欢迎在 Github 为我们 Star 或提供产品反馈意见。",
"K5cfdd950": "该数据删除后将无法找回,是否删除?",
"K28435c5c": "API 详情",
"K13edc043": "作为服务内所有API的前缀,比如host/{service_name}/{api_path},一旦保存无法修改",
"K427a5bd5": "仅支持 .png .jpg .jpeg .svg 格式的图片文件, 大于 1KB 的文件将被压缩",
"Kb9052305": "搜索用户名、邮箱",
"K5ece3bac": "设置团队和成员,然后你可以在团队内创建服务和消费者、订阅API,成员只能看到所属团队内的服务和消费者。",
"K1512e983": "消费者调用统计",
@@ -40,5 +46,25 @@
"K831aa6c0": "申请方-消费者",
"K40a89bd8": "输入名称、ID 查找服务",
"Kb147fabc": "新建",
"K40ca4f2": "更新"
"K40ca4f2": "更新",
"Kd752a3a8": "未配置 AI 模型",
"K8b7ac871": "前往设置",
"Kf97448b3": "已设置",
"K30d4d8df": "未设置",
"K608af899": "负载优先级",
"K65b7a96": "负载优先级决定在原供应商异常或停用后,优先使用哪一个供应商。优先级数字越小,优先级越高。",
"K9eccff16": "优先级必须大于 0",
"Kfcf02780": "请输入优先级",
"K2a3aeb8d": "默认:",
"Kefb03657": "APIKey 资源池",
"Kc0352e64": "支持单个 API 模型供应商下创建多个 APIKey APIKey 进行智能负载均衡",
"K31086771": "支持批量添加现有 API 文档以实现统一的外部访问。",
"K659140c3": "快速调用 AI 模型的云服务 API,方便管理提示词和统一计费。",
"Kbb028f95": "添加负载均衡",
"Ka791de39": "部署中",
"Kf7056787": "公共服务",
"Kbe98ba9e": "私有服务",
"K83829a3b": "模型异常",
"Kb92fb02b": "部署失败",
"Kacf10c44": "Ollama 端点"
}
@@ -13,13 +13,19 @@
"Kffd7e274": "無審核:允許所有應用程式訂閱該服務",
"K8a8b13e4": "人工審核:僅允許審核通過的應用程式訂閱該服務",
"Kf1b166e7": "詳情",
"K66a7d24c": "已設置",
"Kd9a46c29": "默認",
"K7ac2be34": "AI 模型管理",
"K18dccc1a": "同步最新模型",
"K9bdd8403": "為了安全地調用 API,你需要創建一個應用以及Token。",
"Kc8239422": "團隊中包含了人員、應用程式和服務,不同團隊之間的應用程式和服務數據是隔離的,可用於管理企業內部不同的部門/項目組/團隊。",
"Ka0a8840a": "審核其他應用程式的訂閱申請,審核通過後的才可發起 API 請求。",
"K7e04ea16": "🦄 APIPark 是開源的一站式 AI 網關和 API 開發者門戶,幫助企業和開發者快速接入 100+ AI 模型,將 AI 模型和 Prompt 提示詞組合成新的 API,並且統一所有 AI 的請求數據格式,避免切換 AI 模型或調整提示詞時影響你的 APP 應用程式或者微服務。你還可以通過 APIPark 的開發者門戶在團隊內共享 API,管理調用的應用程式並保障你的 API 安全,通過清晰的圖表來監控你的 AI API 使用情況。",
"Kedd41c18": "✨ 如果你喜歡APIPark,歡迎在Github為我們Star或提供產品回饋意見。",
"K5cfdd950": "該數據刪除後將無法找回,是否刪除?",
"K28435c5c": "API 詳情",
"K13edc043": "作為服務內所有API的前綴,比如host/{service_name}/{api_path},一旦保存無法修改",
"K427a5bd5": "僅支持 .png .jpg .jpeg .svg 格式的圖片文件, 大於 1KB 的文件將被壓縮",
"Kb9052305": "搜索用戶名、電郵",
"K5ece3bac": "設置團隊和成員,然後你可以在團隊內創建服務和應用程式、訂閱API,成員只能看到所屬團隊內的服務和應用程式。",
"K1512e983": "應用程式調用統計",
@@ -41,5 +47,25 @@
"K40a89bd8": "輸入名稱、ID 查找服務",
"K1da86266": "無效",
"Kb147fabc": "新建",
"K40ca4f2": "更新"
"K40ca4f2": "更新",
"Kd752a3a8": "未配置 AI 模型",
"K8b7ac871": "前往设置",
"Kf97448b3": "已设置",
"K30d4d8df": "未设置",
"K608af899": "负载优先级",
"K65b7a96": "负载优先级决定在原供应商异常或停用后,优先使用哪一个供应商。优先级数字越小,优先级越高。",
"K9eccff16": "优先级必须大于 0",
"Kfcf02780": "请输入优先级",
"K2a3aeb8d": "默认:",
"Kefb03657": "APIKey 资源池",
"Kc0352e64": "支持单个 API 模型供应商下创建多个 APIKey APIKey 进行智能负载均衡",
"K31086771": "支持批量添加現有 API 文檔,以實現統一的外部訪問。",
"K659140c3": "快速調用 AI 模型的雲端 API,方便管理提示詞和統一計費。",
"Kbb028f95": "添加負載均衡",
"Ka791de39": "部署中",
"Kf7056787": "公共服務",
"Kbe98ba9e": "私有服務",
"K83829a3b": "模型異常",
"Kb92fb02b": "部署失敗",
"Kacf10c44": "Ollama 端點"
}
@@ -824,5 +824,32 @@
"Kf1ce5b3": "✨ 欢迎在 Github 为我们 Star 或提供产品反馈意见。",
"K3af90490": "⚡您可快速通过以下方式开放API供大家使用:",
"K6b99dce8": "Ollama 地址",
"Kce2fcdbf": "暂无权限"
"K500b7535": "添加 (0) 模型",
"Kd87397b0": "添加供应商",
"K5bcf8c48": "编辑供应商",
"K10a99afa": "添加自定义供应商",
"Kd6285399": "访问配置",
"K7eb03edd": "模型参数模板",
"K49b434e9": "模型参数",
"K16ef56b1": "供应商名称",
"K484de451": "注意:",
"Kbd80dde0": "仅支持使用 OpenAI 输入输出格式和认证方法(APIKey)的供应商。如果不满足此条件,创建后自定义供应商将不可用。",
"K73cb9ff1": "模型值",
"Kd9643194": "接口请求时若指定使用当前模型,则需要在 Model 参数中填入以下值。例如若想使用火山引擎的 Deepseek-R1 模型,那么就需要填入 Volcengine/Deepseek-R1 , 值的大小写不敏感。",
"Kf7a916be": "(0) 模型",
"Ke37a353f": "Models",
"Kf62170f0": "模型值重定向",
"K5e28ee82": "本项非必填,用于简化请求中的 Model 参数值,key 为希望使用的简化值,value 为原来的对应值。例如: {\"deepseek-r1\":\"ollama/deepseek-r1-1.5b-qwen-distill-fp16\"}",
"K8929cbb1": "自定义",
"Kcb6a1c57": "该模型为官方模型,不可编辑",
"Kf9300eb4": "存在使用当前模型的接口,需要先解绑后才能编辑",
"K8af71816": "该模型为官方模型,不可删除",
"Kb8ad0af5": "存在使用当前模型的接口,需要先解绑后才能删除",
"K60b54d0": "编辑 (0) 模型",
"Kbf48dc2d": "确定删除吗?",
"K3d2324d0": "删除失败",
"Kce2fcdbf": "暂无权限",
"K24f6a5b4": "自定义(空模板)",
"Kea608112": "载入预置模板",
"Kee7de862": "编辑供应商( (0) )"
}
@@ -893,5 +893,32 @@
"Kf1ce5b3": "✨ 歡迎在 Github 為我們 Star 或提供產品反饋意見。",
"K3af90490": "⚡ 您可以快速通過以下方式開放 API 供大家使用:",
"K6b99dce8": "Ollama 地址",
"Kce2fcdbf": "暫無權限"
"K500b7535": "新增 (0) 模型",
"Kd87397b0": "新增供應商",
"K5bcf8c48": "編輯供應商",
"K10a99afa": "添加自定義供應商",
"Kd6285399": "訪問配置",
"K7eb03edd": "模型參數模板",
"K49b434e9": "模型參數",
"K16ef56b1": "供應商名稱",
"K484de451": "注意:",
"Kbd80dde0": "僅支持使用 OpenAI 輸入輸出格式和認證方法(APIKey)的供應商。如果不滿足此條件,創建後自定義供應商將不可用。",
"K73cb9ff1": "模型值",
"Kd9643194": "接口請求時若指定使用當前模型,則需要在 Model 參數中填入以下值。例如若想使用火山引擎的 Deepseek-R1 模型,那麼就需要填入 Volcengine/Deepseek-R1,值的大小寫不敏感。",
"Kf7a916be": "(0) 模型",
"Ke37a353f": "Models",
"Kf62170f0": "模型值重定向",
"K5e28ee82": "本項非必填,用於簡化請求中的 Model 參數值,key 為希望使用的簡化值,value 為原來的對應值。例如: {\"deepseek-r1\":\"ollama/deepseek-r1-1.5b-qwen-distill-fp16\"}",
"K8929cbb1": "自訂",
"Kcb6a1c57": "該模型為官方模型,不可編輯。",
"Kf9300eb4": "存在使用當前模型的介面,需先解除綁定後才能編輯。",
"K8af71816": "該模型為官方模型,不可刪除。",
"Kb8ad0af5": "存在使用當前模型的介面,需先解除綁定後才能刪除。",
"K60b54d0": "編輯 (0) 模型",
"Kbf48dc2d": "確定要刪除嗎?",
"K3d2324d0": "刪除失敗。",
"Kce2fcdbf": "暫無權限",
"K24f6a5b4": "自訂(空模板)",
"Kea608112": "載入預設模板",
"Kee7de862": "編輯供應商( (0) )"
}
@@ -30,6 +30,7 @@ export type SystemConfigFieldType = {
serviceKind:'ai'|'rest';
catalogue?:string | string[];
approvalType?:string;
modelMapping?: string;
};
export type SystemSubServiceTableListItem = {
@@ -65,6 +65,7 @@ const AiServiceInsideRouterCreate = () => {
const [llmList, setLlmList] = useState<AiProviderLlmsItems[]>([])
const [variablesTableRef, setVariablesTableRef] = useState<MutableRefObject<EditableFormInstance<T> | undefined>>()
const { state } = useGlobalContext()
const [resultPath, setResultPath] = useState<string>('')
const onFinish = () => {
return variablesTableRef?.current
@@ -114,6 +115,26 @@ const AiServiceInsideRouterCreate = () => {
drawerType !== undefined ? setOpen(true) : setOpen(false)
}, [drawerType])
const getPath = (path: string) => {
let newPath = path
let pathMatch = 'full'
if (prefixForce && path?.startsWith(apiPrefix + '/')) {
newPath = path.slice((apiPrefix?.length || 0) + 1)
}
if (newPath.endsWith('/*')) {
newPath = newPath.slice(0, -2)
pathMatch = 'prefix'
}
return { newPath, pathMatch }
}
useEffect(() => {
if (resultPath) {
const { newPath, pathMatch } = getPath(resultPath)
form.setFieldsValue({ path: newPath, pathMatch })
}
}, [apiPrefix, resultPath])
const getRouterConfig = () => {
setLoading(true)
fetchData<BasicResponse<{ api: AiServiceRouterConfig }>>('service/ai-router', {
@@ -125,21 +146,14 @@ const AiServiceInsideRouterCreate = () => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
const { path, aiPrompt, aiModel } = data.api
let newPath = path
let pathMatch = 'full'
if (prefixForce && path?.startsWith(apiPrefix + '/')) {
newPath = path.slice((apiPrefix?.length || 0) + 1)
}
if (newPath.endsWith('/*')) {
newPath = newPath.slice(0, -2)
pathMatch = 'prefix'
}
const { newPath, pathMatch } = getPath(path)
form.setFieldsValue({
...data.api,
...aiPrompt,
path: newPath,
pathMatch
})
setResultPath(path)
setVariablesTable(aiPrompt.variables as VariableItems[])
setDefaultLlm(
(prev) =>
@@ -198,7 +198,7 @@ const AiServiceRouterModelConfig = forwardRef<AiServiceRouterModelConfigHandle,
value: x.id,
label: (
<div className="flex items-center gap-[10px]" key={x.id}>
<span>{x.id}</span>
<span>{x.name || x.id}</span>
{modelType === 'online' && x?.scopes?.map((s: any) => <Tag>{s?.toLocaleUpperCase()}</Tag>)}
</div>
)
@@ -7,6 +7,8 @@ import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 're
import { AiProviderLlmsItems, ModelDetailData, AiSettingListItem, AISettingEntityItem } from './types'
import { MemberItem, SimpleTeamItem } from '@common/const/type'
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
import AddModels, { addModelsContentHandle } from './contexts/AddModels'
import AddProvider, { addProviderContentHandle } from './contexts/AddProvider'
export type AiSettingModalContentProps = {
entity?: AISettingEntityItem
@@ -24,7 +26,7 @@ export type AiSettingModalContentHandle = {
const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingModalContentProps>((props, ref) => {
const [form] = Form.useForm()
const { message } = App.useApp()
const { message, modal } = App.useApp()
const { entity, readOnly, modelMode = 'auto', updateEntityData, source } = props
const { fetchData } = useFetch()
const [llmList, setLlmList] = useState<AiProviderLlmsItems[]>()
@@ -38,6 +40,12 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
const [modelModeLoading, setModelModeLoading] = useState<boolean>(false)
const [enableState, setEnableState] = useState<boolean>(localEntity?.status === 'enabled')
const { checkPermission } = useGlobalContext()
// 添加模型弹窗
const addModelModalRef = useRef<addModelsContentHandle>()
// 添加供应商弹窗
const addProviderModalRef = useRef<addProviderContentHandle>()
// 记录最后的 llm id,因为如果手动加了 model,那么需要刷新
const [lastLlmID, setLastLlmID] = useState<string | undefined>('')
/**
* llm
@@ -45,9 +53,11 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
*/
const getLlmList = (id?: string) => {
setLoading(true)
const providerID = id || localEntity?.id
setLastLlmID(providerID)
fetchData<BasicResponse<{ llms: AiProviderLlmsItems[] }>>(`ai/provider/llms`, {
method: 'GET',
eoParams: { provider: id || localEntity?.id }
eoParams: { provider: providerID }
})
.then((response) => {
const { code, data, msg } = response
@@ -89,7 +99,7 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
/**
*
*/
const getModelProviderList = () => {
const getModelProviderList = (setModelValue = true, defaultId?: string | number) => {
setModelModeLoading(true)
fetchData<BasicResponse<{ providers: AiSettingListItem[] }>>(`ai/providers/unconfigured`, {
method: 'GET',
@@ -100,10 +110,10 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
if (code === STATUS_CODE.SUCCESS) {
const providers = data.providers || []
modelProviderListRef.current = providers
if (providers.length) {
const id = providers[0].id
if ((setModelValue && providers.length) || defaultId) {
const id = defaultId || providers[0].id
form.setFieldValue('modelMode', id)
getModelConfig(id)
getModelConfig(id, defaultId)
}
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
@@ -118,7 +128,7 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
*
* @param id
*/
const getModelConfig = (id: string) => {
const getModelConfig = (id: string, defaultId?: string | number) => {
getLlmList(id)
fetchData<BasicResponse<{ providers: ModelDetailData[] }>>(`ai/provider/config`, {
method: 'GET',
@@ -129,7 +139,8 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
const modelEntity = {
...data.provider
...data.provider,
isNewProvider: !!defaultId
}
setLocalEntity(modelEntity)
setFormFieldsValue(modelEntity)
@@ -208,7 +219,7 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
/**
*
* @returns
* @returns
*/
const save: () => Promise<boolean | string> = () => {
return new Promise((resolve, reject) => {
@@ -220,23 +231,24 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
...value
}
fetchData<BasicResponse<null>>('ai/provider/config', {
method: 'PUT',
eoParams: { provider: localEntity?.id },
eoBody: finalValue,
eoTransformKeys: ['defaultLlm']
// eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/'
})
.then((response) => {
const { code, msg } = response
if (code === STATUS_CODE.SUCCESS) {
message.success(msg || $t(RESPONSE_TIPS.success))
resolve(true)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
reject(msg || $t(RESPONSE_TIPS.error))
}
}).catch((errorInfo) => reject(errorInfo))
fetchData<BasicResponse<null>>('ai/provider/config', {
method: 'PUT',
eoParams: { provider: localEntity?.id },
eoBody: finalValue,
eoTransformKeys: ['defaultLlm']
// eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/'
})
.then((response) => {
const { code, msg } = response
if (code === STATUS_CODE.SUCCESS) {
message.success(msg || $t(RESPONSE_TIPS.success))
resolve(true)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
reject(msg || $t(RESPONSE_TIPS.error))
}
})
.catch((errorInfo) => reject(errorInfo))
})
.catch((errorInfo) => reject(errorInfo))
} catch (error) {
@@ -252,6 +264,60 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
return $t('保存后供应商状态变为【正常】,恢复调用本供应商的 AI 能力。')
}
/**
*
*/
const addModel = () => {
const providerName = localEntity?.name || form.getFieldValue('modelMode')
const showAccessConfig = localEntity?.model_config?.access_configuration_status || false
const accessConfig = localEntity?.model_config?.access_configuration_demo || ''
modal.confirm({
title: $t('添加 (0) 模型', [providerName]),
content: (
<AddModels
ref={addModelModalRef}
showAccessConfig={showAccessConfig}
accessConfig={accessConfig}
providerID={localEntity?.id}
></AddModels>
),
onOk: () => {
return addModelModalRef.current?.save().then((res) => {
if (res === true) {
getLlmList(lastLlmID)
}
})
},
width: 600,
okText: $t('确认'),
cancelText: $t('取消'),
closable: true,
icon: <></>
})
}
/**
*
*/
const addProvider = () => {
modal.confirm({
title: $t('添加自定义供应商'),
content: <AddProvider ref={addProviderModalRef}></AddProvider>,
onOk: () => {
return addProviderModalRef.current?.save().then((res) => {
if (res) {
getModelProviderList(false, res.id)
}
})
},
width: 600,
okText: $t('确认'),
cancelText: $t('取消'),
closable: true,
icon: <></>
})
}
useImperativeHandle(ref, () => ({
save,
deployAIServer
@@ -268,47 +334,68 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
autoComplete="off"
disabled={readOnly}
>
{modelMode === 'manual' && (
<Form.Item<ModelDetailData> label={$t('模型供应商')} name="modelMode" rules={[{ required: true }]}>
{modelMode === 'manual' && !localEntity?.isNewProvider && (
<Form.Item<ModelDetailData> label={$t('模型供应商')}>
<span className="absolute top-[-28px] right-0 text-theme cursor-pointer" onClick={addProvider}>
+ {$t('添加自定义供应商')}
</span>
<Form.Item<ModelDetailData> name="modelMode" rules={[{ required: true }]}>
<Select
showSearch
className="w-INPUT_NORMAL"
filterOption={(input, option) => (option?.searchText ?? '').includes(input.toLowerCase())}
placeholder={$t(PLACEHOLDER.select)}
loading={modelModeLoading}
options={modelProviderListRef.current?.map((x) => ({
value: x.id,
label: (
<div className="flex items-center gap-[10px]">
<span>{x.name}</span>
</div>
),
searchText: x.name.toLowerCase()
}))}
onChange={(e) => {
getModelConfig(e)
}}
></Select>
</Form.Item>
</Form.Item>
)}
<Form.Item<ModelDetailData> label={$t('默认模型')}>
<span className="absolute top-[-28px] right-0 text-theme cursor-pointer" onClick={addModel}>
+ {$t('添加模型')}
</span>
<Form.Item<ModelDetailData> name="defaultLlm" rules={[{ required: true }]}>
<Select
showSearch
className="w-INPUT_NORMAL"
filterOption={(input, option) => (option?.searchText ?? '').includes(input.toLowerCase())}
placeholder={$t(PLACEHOLDER.select)}
loading={modelModeLoading}
options={modelProviderListRef.current?.map((x) => ({
loading={loading}
options={llmList?.map((x) => ({
value: x.id,
label: (
<div className="flex items-center gap-[10px]">
<span>{x.name}</span>
<span>{x.name || x.id}</span>
{x?.scopes?.map((s) => <Tag key={s}>{s?.toLocaleUpperCase()}</Tag>)}
</div>
)
),
searchText: x.name.toLowerCase()
}))}
onChange={(e) => {
getModelConfig(e)
}}
></Select>
</Form.Item>
)}
<Form.Item<ModelDetailData> label={$t('默认模型')} name="defaultLlm" rules={[{ required: true }]}>
<Select
className="w-INPUT_NORMAL"
placeholder={$t(PLACEHOLDER.select)}
loading={loading}
options={llmList?.map((x) => ({
value: x.id,
label: (
<div className="flex items-center gap-[10px]">
<span>{x.id}</span>
{x?.scopes?.map((s) => <Tag key={s}>{s?.toLocaleUpperCase()}</Tag>)}
</div>
)
}))}
></Select>
</Form.Item>
{source === 'guide' && (
<Form.Item label={$t('所属团队')} name="team" className="mt-[16px]" rules={[{ required: true }]}>
<Select className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} options={teamList} onChange={(value) => {
form.setFieldValue('team', value)
}}></Select>
<Select
className="w-INPUT_NORMAL"
placeholder={$t(PLACEHOLDER.input)}
options={teamList}
onChange={(value) => {
form.setFieldValue('team', value)
}}
></Select>
</Form.Item>
)}
<Form.Item<ModelDetailData> label={$t('API Key(默认 Key')} name="config">
@@ -4,10 +4,15 @@ import TableBtnWithPermission from '@common/components/aoplatform/TableBtnWithPe
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
import { useFetch } from '@common/hooks/http'
import { $t } from '@common/locales'
import { App, Divider, Space, Typography } from 'antd'
import { App, Divider, Modal, Space, Typography } from 'antd'
import React, { useRef, useState } from 'react'
import { useAiSetting } from './contexts/AiSettingContext'
import { AiSettingListItem, ModelListData } from './types'
import { AISettingEntityItem, ModelListData } from './types'
import { DrawerWithFooter } from '@common/components/aoplatform/DrawerWithFooter'
import AiSettingModalContent, { AiSettingModalContentHandle } from './AiSettingModal'
import { checkAccess } from '@common/utils/permission'
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
import ModelsDetailTable from './contexts/ModelsDetailTable'
import { Icon } from '@iconify/react/dist/iconify.js'
const OnlineModelList: React.FC = () => {
const pageListRef = useRef<ActionType>(null)
@@ -15,18 +20,22 @@ const OnlineModelList: React.FC = () => {
const { fetchData } = useFetch()
const [searchWord, setSearchWord] = useState<string>('')
const [total, setTotal] = useState<number>(0)
const { openConfigModal } = useAiSetting()
const [drawerOpen, setDrawerOpen] = useState<boolean>(false)
const drawerFormRef = useRef<AiSettingModalContentHandle>(null)
const [entity, setEntity] = useState<AISettingEntityItem>()
const [footerLeft, setFooterLeft] = useState<React.ReactNode>()
const { aiConfigFlushed, setAiConfigFlushed, accessData } = useGlobalContext()
const [modalVisible, setModalVisible] = useState(false)
const [selectedProviderID, setSelectedProviderID] = useState('')
const [selectedProviderName, setSelectedProviderName] = useState('')
const [currentEditDrawerData, setCurrentEditDrawerData] = useState<any>()
const handleEdit = (record: ModelListData) => {
openConfigModal({ id: record.id, defaultLlm: record.defaultLlm } as AiSettingListItem, () => {
pageListRef.current?.reload()
})
}
const handleAdd = () => {
openConfigModal(undefined, () => {
pageListRef.current?.reload()
setEntity({
id: record.id,
defaultLlm: record.defaultLlm
})
setDrawerOpen(true)
}
const handleDelete = async (id: string, apiCount: number) => {
@@ -41,18 +50,20 @@ const OnlineModelList: React.FC = () => {
eoParams: {
provider: id
}
}).then((response) => {
if (response.code === STATUS_CODE.SUCCESS) {
message.success($t('删除成功'))
pageListRef.current?.reload()
} else {
message.error(response.msg || RESPONSE_TIPS.error)
}
resolve(true)
}).catch((error) => {
message.error(RESPONSE_TIPS.error)
resolve(true)
})
.then((response) => {
if (response.code === STATUS_CODE.SUCCESS) {
message.success($t('删除成功'))
pageListRef.current?.reload()
} else {
message.error(response.msg || RESPONSE_TIPS.error)
}
resolve(true)
})
.catch((error) => {
message.error(RESPONSE_TIPS.error)
resolve(true)
})
} catch (error) {
message.error(RESPONSE_TIPS.error)
resolve(true)
@@ -65,7 +76,6 @@ const OnlineModelList: React.FC = () => {
closable: true,
icon: <></>
})
}
const requestList = async (params: any) => {
@@ -77,7 +87,7 @@ const OnlineModelList: React.FC = () => {
keyword: searchWord,
page: params.current
},
eoTransformKeys: ['default_llm', 'api_count', 'key_count', 'can_delete']
eoTransformKeys: ['default_llm', 'api_count', 'key_count', 'model_count', 'can_delete']
})
if (response.code === STATUS_CODE.SUCCESS) {
@@ -103,6 +113,34 @@ const OnlineModelList: React.FC = () => {
}
}
}
// 更新抽屉底部
const updateEntityData = (data: any) => {
// 0 常规,1 自定义
setCurrentEditDrawerData(data)
setFooterLeft(
<a target="_blank" rel="noopener noreferrer" href={data?.getApikeyUrl} className="flex items-center gap-[8px]">
<span>{$t('从 (0) 获取 API KEY', [data?.name])}</span>
<Icon icon="ic:baseline-open-in-new" width={16} height={16} />
</a>
)
}
/**
*
* @param e
* @param record
*/
const openModelsModal = (e: any, record: ModelListData) => {
e.stopPropagation()
setSelectedProviderID(record.id as string)
setSelectedProviderName(record.name)
setModalVisible(true)
}
const handleCloseModal = () => {
setModalVisible(false);
};
const statusEnum = {
enabled: { text: <Typography.Text type="success">{$t('正常')}</Typography.Text> },
disabled: { text: <Typography.Text type="warning">{$t('停用')}</Typography.Text> },
@@ -140,7 +178,7 @@ const OnlineModelList: React.FC = () => {
const columns: PageProColumns<ModelListData>[] = [
{
title: $t('名称'),
title: $t('供应商名称'),
dataIndex: 'name',
render: (dom: React.ReactNode, entity: ModelListData) => <Space>{entity.name}</Space>
},
@@ -159,6 +197,15 @@ const OnlineModelList: React.FC = () => {
ellipsis: true,
dataIndex: 'defaultLlm'
},
{
title: $t('Models'),
dataIndex: 'modelCount',
render: (dom: React.ReactNode, record: ModelListData) => (
<span className="text-[#2196f3] cursor-pointer" onClick={(e) => openModelsModal(e, record)}>
{record.modelCount || '0'}
</span>
)
},
{
title: $t('Apis'),
dataIndex: 'apiCount',
@@ -205,21 +252,64 @@ const OnlineModelList: React.FC = () => {
]
return (
<PageList
ref={pageListRef}
rowKey="id"
request={requestList}
onSearchWordChange={(e) => {
setSearchWord(e.target.value)
pageListRef.current?.reload()
}}
showPagination={true}
searchPlaceholder={$t('请输入名称搜索')}
columns={columns}
addNewBtnAccess="system.devops.ai_provider.edit"
addNewBtnTitle={$t('添加模型')}
onAddNewBtnClick={handleAdd}
/>
<>
<PageList
ref={pageListRef}
rowKey="id"
request={requestList}
onSearchWordChange={(e) => {
setSearchWord(e.target.value)
pageListRef.current?.reload()
}}
showPagination={true}
searchPlaceholder={$t('请输入名称搜索')}
columns={columns}
addNewBtnAccess="system.devops.ai_provider.edit"
addNewBtnTitle={$t('添加供应商')}
onAddNewBtnClick={() => setDrawerOpen(true)}
/>
<DrawerWithFooter
title={currentEditDrawerData?.isNewProvider ? $t('编辑供应商( (0) )', [currentEditDrawerData.name]) : entity ? $t('编辑供应商') : $t('添加供应商')}
open={drawerOpen}
width="30%"
onClose={() => {
setEntity(undefined)
setDrawerOpen(false)
}}
onSubmit={() =>
drawerFormRef.current?.save()?.then((res) => {
res && pageListRef.current?.reload()
setAiConfigFlushed(!aiConfigFlushed)
setEntity(undefined)
return res
})
}
footerLeft={!currentEditDrawerData?.type ? footerLeft : undefined}
submitAccess=""
>
<AiSettingModalContent
ref={drawerFormRef}
entity={{ id: entity?.id, defaultLlm: entity?.defaultLlm }}
modelMode={entity ? 'auto' : 'manual'}
updateEntityData={updateEntityData}
readOnly={!checkAccess('system.devops.ai_provider.edit', accessData)}
/>
</DrawerWithFooter>
<Modal
title={$t('(0) 模型', [selectedProviderName])}
visible={modalVisible}
destroyOnClose={true}
onCancel={handleCloseModal}
footer={null}
wrapClassName="modal-without-footer"
width={600}
maskClosable={true}
>
<div className="pb-btnybase flex flex-nowrap flex-col h-full w-full items-center justify-between">
<ModelsDetailTable providerID={selectedProviderID}></ModelsDetailTable>
</div>
</Modal>
</>
)
}
@@ -0,0 +1,182 @@
import { App, Dropdown, Form, Input } from 'antd'
import { $t } from '@common/locales'
import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
import { Codebox } from '@common/components/postcat/api/Codebox'
import { useFetch } from '@common/hooks/http'
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'
import { Icon } from '@iconify/react/dist/iconify.js'
type modelFieldType = {
name: string
type: string
model_parameters: string
access_configuration: string
}
export type addModelsContentHandle = {
save: () => Promise<boolean | string>
}
type addModelContentProps = {
showAccessConfig: boolean
accessConfig: string
modelParameters?: string
modelName?: string
type?: 'add' | 'edit'
providerID?: string
modelID?: string
}
type TemplatesItems = {
providerName: string
modelName: string
modelParameters: string
}
const AddModels = forwardRef<addModelsContentHandle, addModelContentProps>((props, ref) => {
const { showAccessConfig, accessConfig, modelParameters, modelName, providerID, type, modelID } = props
const [form] = Form.useForm()
const { message } = App.useApp()
const { fetchData } = useFetch()
const [templateList, setTemplateList] = useState<
{
key: string
label: string
config: string
}[]
>([])
/**
* modelTemplateList
* @param id
*/
const getModelTemplateList = () => {
fetchData<BasicResponse<{ templates: TemplatesItems[] }>>(`ai/provider/model/templates`, {
method: 'GET',
eoTransformKeys: ['provider_name', 'model_name', 'model_parameters']
})
.then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
const templates = data.templates || []
const emptyTemplate = {
key: '自定义(空模板)',
label: $t('自定义(空模板)'),
config: '{\n \n}'
}
setTemplateList([emptyTemplate, ...templates.map((template: any) => ({
key: template.id,
label: `${template.providerName} ${template.modelName}`,
config: template.modelParameters
}))])
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
}
useEffect(() => {
getModelTemplateList()
form.setFieldsValue({
access_configuration: accessConfig || '{\n \n}',
model_parameters: modelParameters || '{\n \n}',
name: modelName || ''
})
}, [])
const modelParameterClick = ({ key }: { key: string }) => {
const config = templateList.find((item) => item.key === key)?.config
form.setFieldValue('model_parameters', config || '{\n \n}')
}
/**
*
* @returns
*/
const save: () => Promise<boolean | string> = () => {
return new Promise((resolve, reject) => {
try {
form
.validateFields()
.then((value) => {
const finalValue = {
...value,
id: modelID
}
fetchData<BasicResponse<null>>('ai/provider/model', {
method: type === 'edit' ? 'PUT' : 'POST',
eoParams: { provider: providerID },
eoBody: finalValue,
eoTransformKeys: ['defaultLlm']
})
.then((response) => {
const { code, msg } = response
if (code === STATUS_CODE.SUCCESS) {
message.success($t(RESPONSE_TIPS.success) || msg)
resolve(true)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
reject(msg || $t(RESPONSE_TIPS.error))
}
})
.catch((errorInfo) => reject(errorInfo))
})
.catch((errorInfo) => reject(errorInfo))
} catch (error) {
reject(error)
}
})
}
useImperativeHandle(ref, () => ({
save
}))
return (
<Form
form={form}
layout="vertical"
labelAlign="left"
scrollToFirstError
className="flex flex-col mx-auto h-full"
name="aiServiceInsideRouterModalConfig"
autoComplete="off"
>
<Form.Item<modelFieldType> label={$t('模型名称')} name="name" rules={[{ required: true }]}>
<Input disabled={type === 'edit'} className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} />
</Form.Item>
{showAccessConfig && (
<Form.Item<modelFieldType> label={$t('访问配置')} name="access_configuration" rules={[{ required: true }]}>
<Codebox
editorTheme="vs-dark"
readOnly={false}
width="100%"
height="70px"
language="json"
enableToolbar={false}
/>
</Form.Item>
)}
<Form.Item<modelFieldType> label={$t('模型参数')}>
<span className="absolute top-[-28px] right-0 text-theme cursor-pointer">
<Icon icon="ph:download-simple-light" className="align-sub mr-[5px]" width={20} height={20} />
<Dropdown overlayClassName="w-[200px] [&>.ant-dropdown-menu>.ant-dropdown-menu-item>.ant-dropdown-menu-title-content]:truncate" placement="bottom" trigger={['click']} key="menu" menu={{ items: templateList, onClick: modelParameterClick }}>
<span>{$t('载入预置模板')}</span>
</Dropdown>
</span>
<Form.Item<modelFieldType> name="model_parameters">
<Codebox
editorTheme="vs-dark"
readOnly={false}
width="100%"
height="200px"
language="json"
enableToolbar={false}
/>
</Form.Item>
</Form.Item>
</Form>
)
})
export default AddModels
@@ -0,0 +1,87 @@
import { App, Form, Input, Select, Tag } from 'antd'
import { $t } from '@common/locales'
import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
import { useFetch } from '@common/hooks/http'
import { forwardRef, useImperativeHandle } from 'react'
import { AISettingEntityItem } from '../types'
type modelFieldType = {
name: string
}
export type addProviderContentHandle = {
save: () => Promise<AISettingEntityItem>
}
type addProviderContentProps = {
provider: string
}
const AddProvider = forwardRef<addProviderContentHandle, addProviderContentProps>((props, ref) => {
const [form] = Form.useForm()
const { message } = App.useApp()
const { fetchData } = useFetch()
/**
*
* @returns
*/
const save: () => Promise<AISettingEntityItem> = () => {
return new Promise((resolve, reject) => {
try {
form
.validateFields()
.then((value) => {
const finalValue = {
...value
}
fetchData<BasicResponse<null>>('ai/provider', {
method: 'POST',
eoBody: finalValue
})
.then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
message.success($t(RESPONSE_TIPS.success) || msg)
resolve(data.provider)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
reject(msg || $t(RESPONSE_TIPS.error))
}
})
.catch((errorInfo) => reject(errorInfo))
})
.catch((errorInfo) => reject(errorInfo))
} catch (error) {
reject(error)
}
})
}
useImperativeHandle(ref, () => ({
save
}))
return (
<Form
form={form}
layout="vertical"
labelAlign="left"
scrollToFirstError
className="flex flex-col mx-auto h-full"
name="aiServiceInsideRouterModalConfig"
autoComplete="off"
>
<Form.Item<modelFieldType> label={$t('供应商名称')} name="name" rules={[{ required: true }]}>
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} />
</Form.Item>
<p className="mt-[20px]">
<span className="font-bold">{$t('注意:')}</span>
<span>
{$t('仅支持使用 OpenAI 输入输出格式和认证方法(APIKey)的供应商。如果不满足此条件,创建后自定义供应商将不可用。')}
</span>
</p>
</Form>
)
})
export default AddProvider
@@ -0,0 +1,197 @@
import { ActionType } from '@ant-design/pro-components'
import PageList, { PageProColumns } from '@common/components/aoplatform/PageList'
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
import { useFetch } from '@common/hooks/http'
import { useRef, useState } from 'react'
import { $t } from '@common/locales'
import { AiProviderLlmsItems, ModelListData } from '../types'
import { App, Divider, Tooltip } from 'antd'
import { Icon } from '@iconify/react/dist/iconify.js'
import TableBtnWithPermission from '@common/components/aoplatform/TableBtnWithPermission'
import AddModels, { addModelsContentHandle } from './AddModels'
const ModelsDetailTable = (props: { providerID?: string }) => {
const { providerID } = props
const pageListRef = useRef<ActionType>(null)
const { fetchData } = useFetch()
const { message, modal } = App.useApp()
const [providerData, setProviderData] = useState<any>()
const addModelModalRef = useRef<addModelsContentHandle>()
const operation: PageProColumns<any>[] = [
{
title: '',
key: 'option',
btnNums: 2,
fixed: 'right',
valueType: 'option',
render: (_: React.ReactNode, entity: any) => [
<TableBtnWithPermission
access="system.devops.ai_provider.edit"
key="edit"
btnType="edit"
disabled={entity?.is_system}
tooltip={
entity?.is_system ? $t('该模型为官方模型,不可编辑') : ''
}
onClick={() => handleEdit(entity)}
btnTitle={$t('设置')}
/>,
<Divider type="vertical" className="mx-0" />,
<TableBtnWithPermission
access="system.devops.ai_provider.edit"
key="delete"
disabled={entity?.is_system || entity?.api_count}
tooltip={
entity?.is_system ? $t('该模型为官方模型,不可删除') : $t('存在使用当前模型的接口,需要先解绑后才能删除')
}
btnType="delete"
onClick={() => handleDelete(entity)}
btnTitle={$t('删除')}
/>
]
}
]
const columns: PageProColumns<any>[] = [
{
title: $t('模型名称'),
ellipsis: true,
copyable: true,
dataIndex: 'name'
},
{
title: $t('模型类型'),
ellipsis: true,
width: 100,
dataIndex: 'type'
},
{
title: (
<span>
{$t('模型值')}
<Tooltip
title={$t(
'接口请求时若指定使用当前模型,则需要在 Model 参数中填入以下值。例如若想使用火山引擎的 Deepseek-R1 模型,那么就需要填入 Volcengine/Deepseek-R1 , 值的大小写不敏感。'
)}
>
<Icon className="align-sub ml-[3px]" icon="fe:question" width="18" height="18" />
</Tooltip>
</span>
),
ellipsis: true,
copyable: true,
dataIndex: 'modelValue'
},
...operation
]
const handleEdit = (entity: any) => {
const accessConfig = entity.access_configuration || ''
const modelConfig = entity.model_parameters || ''
modal.confirm({
title: $t('编辑 (0) 模型', [providerData.name]),
content: (
<AddModels
ref={addModelModalRef}
showAccessConfig={!!accessConfig}
accessConfig={accessConfig}
modelParameters={modelConfig}
modelID={entity.id}
modelName={entity.name}
type={'edit'}
providerID={providerData.id}
></AddModels>
),
onOk: () => {
return addModelModalRef.current?.save().then((res) => {
if (res === true) {
pageListRef.current?.reload()
}
})
},
width: 600,
okText: $t('确认'),
cancelText: $t('取消'),
closable: true,
icon: <></>
})
}
const handleDelete = (entity: any) => {
modal.confirm({
title: $t('删除'),
content: $t('确定删除吗?'),
onOk: async () => {
try {
const response = await fetchData<BasicResponse<'success'>>('ai/provider/model', {
method: 'DELETE',
eoParams: { provider: providerID, id: entity.id }
})
if (response.code === STATUS_CODE.SUCCESS) {
message.success($t('删除成功'))
pageListRef.current?.reload()
}
} catch (error) {
message.error($t('删除失败'))
}
},
width: 600,
okText: $t('确认'),
cancelText: $t('取消'),
closable: true,
icon: <></>
})
}
const requestList = async (params: any) => {
try {
const response = await fetchData<BasicResponse<{ llms: AiProviderLlmsItems[] }>>('ai/provider/llms', {
method: 'GET',
eoParams: { provider: providerID }
})
if (response.code === STATUS_CODE.SUCCESS) {
setProviderData(response.data.provider)
const tableData = response.data.llms.map((item) => {
return {
...item,
modelValue: `${response.data.provider.id}/${item.id}`
}
})
return {
data: tableData,
success: true,
total: response.data.total
}
} else {
message.error(response.msg || $t(RESPONSE_TIPS.error))
return {
data: [],
success: false,
total: response.data.total
}
}
} catch (error) {
return {
data: [],
success: false,
total: 0
}
}
}
return (
<div className="w-full h-full">
<PageList
ref={pageListRef}
rowKey="id"
minVirtualHeight={400}
request={requestList}
showPagination={false}
columns={columns}
/>
</div>
)
}
export default ModelsDetailTable
@@ -18,16 +18,26 @@ export interface ModelListData {
status: ModelStatus
state?: ModelDeployStatus
apiCount: number
modelCount: number
keyCount: number
isDisabled?: boolean
keys: KeyData[]
canDelete: boolean
}
export type ModelConfigItem = {
access_configuration_status: boolean
access_configuration_demo: string
}
export interface AISettingEntityItem {
id: string | undefined
status?: ModelStatus | undefined
defaultLlm: string | undefined
name?: string
type?: number
model_config?: ModelConfigItem
isNewProvider?: boolean
}
export interface ModelDetailData extends ModelListData {
enable: boolean
@@ -46,6 +56,7 @@ export type AiSettingListItem = {
enable: boolean
configured: boolean
priority?: number
type?: string | number
}
export type AiProviderLlmsItems = {
@@ -53,6 +64,7 @@ export type AiProviderLlmsItems = {
logo: string
scopes: ('chat' | 'completions')[]
config: string
name: string
}
export type AiProviderDefaultConfig = {
@@ -209,7 +209,7 @@ const AddLoadBalancingModel = forwardRef<LoadBalancingHandle>((props, ref: any)
value: x.id,
label: (
<div className="flex items-center gap-[10px]">
<span>{x.id}</span>
<span>{x.name || x.id}</span>
{ modelType === 'online' &&x?.scopes?.map((s: any) => <Tag key={s}>{s?.toLocaleUpperCase()}</Tag>)}
</div>
)
@@ -14,7 +14,7 @@ import { AiServiceConfigFieldType } from '@core/const/ai-service/type.ts'
import { SERVICE_APPROVAL_OPTIONS } from '@core/const/system/const.tsx'
import { Icon } from '@iconify/react/dist/iconify.js'
import { CategorizesType } from '@market/const/serviceHub/type.ts'
import { App, Button, Form, Input, Radio, Row, Select, TreeSelect, Upload } from 'antd'
import { App, Button, Form, Input, Radio, Row, Select, Tooltip, TreeSelect, Upload } from 'antd'
import { DefaultOptionType } from 'antd/es/cascader'
import { RcFile, UploadChangeParam, UploadFile, UploadProps } from 'antd/es/upload/interface'
import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from 'react'
@@ -22,6 +22,7 @@ import { Link, useLocation, useNavigate, useParams } from 'react-router-dom'
import { v4 as uuidv4 } from 'uuid'
import { SystemConfigFieldType, SystemConfigHandle } from '../../const/system/type.ts'
import { useSystemContext } from '../../contexts/SystemContext.tsx'
import { Codebox } from '@common/components/postcat/api/Codebox/index.tsx'
export type SimpleAiProviderItem = EntityItem & {
configured: boolean
@@ -225,7 +226,7 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
fetchData<BasicResponse<{ service: SystemConfigFieldType }>>('service/info', {
method: 'GET',
eoParams: { team: teamId, service: serviceId },
eoTransformKeys: ['team_id', 'service_type', 'approval_type', 'service_kind']
eoTransformKeys: ['team_id', 'service_type', 'approval_type', 'service_kind', 'model_mapping']
}).then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
@@ -265,7 +266,7 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
...(serviceId === undefined ? { team: value.team } : { service: serviceId, team: teamId })
},
eoBody: { ...value, prefix: value.prefix?.trim() },
eoTransformKeys: ['serviceType', 'approvalType', 'serviceKind']
eoTransformKeys: ['serviceType', 'approvalType', 'serviceKind', 'modelMapping']
}
)
.then((response) => {
@@ -568,6 +569,25 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
options={tagOptionList}
></Select>
</Form.Item>
{showAI && (
<Form.Item<SystemConfigFieldType>
label={
<span>
{$t('模型值重定向')}
<Tooltip
title={$t(
'本项非必填,用于简化请求中的 Model 参数值,key 为希望使用的简化值,value 为原来的对应值。例如: {"deepseek-r1":"ollama/deepseek-r1-1.5b-qwen-distill-fp16"}'
)}
>
<Icon className="align-sub ml-[3px]" icon="fe:question" width="18" height="18" />
</Tooltip>
</span>
}
name="modelMapping"
>
<Codebox editorTheme="vs-dark" width="100%" height="150px" language="json" enableToolbar={false} />
</Form.Item>
)}
{onEdit && (
<>