improve translation

This commit is contained in:
HaoZhen Liu
2024-10-18 23:15:29 +08:00
37 changed files with 155 additions and 1576 deletions
+31
View File
@@ -10,6 +10,7 @@ const systemLanguage = {
};
const localesDir = 'packages/common/src/locales/scan';
const newJsonDir = 'packages/common/src/locales/scan/newJson';
const oldJsonDir = 'packages/common/src/locales/scan/oldJson';
const keyHashFile = 'packages/common/src/locales/keyHashMap.json';
let existData = {};
let keyHashMap = {};
@@ -35,6 +36,14 @@ fs.readdirSync(localesDir).forEach(file => {
const keyList = Object.keys(existData);
// 清空 newJson 目录下的所有语言文件
Object.values(systemLanguage).forEach(lng => {
const newJsonPath = path.join(newJsonDir, `${lng}.json`);
fs.writeFileSync(newJsonPath, JSON.stringify({})); // 清空文件
});
module.exports = {
input: [
'packages/*/src/**/*.{js,jsx,tsx,ts}',
@@ -110,6 +119,28 @@ module.exports = {
flush: function(done) {
// 将 keyHashMap 写入文件
fs.writeFileSync(keyHashFile, JSON.stringify(keyHashMap, null, 2));
// 遍历每种语言,处理旧字段
keyList.forEach((lng) => {
const localeFilePath = path.join(localesDir, `${lng}.json`);
const oldJsonPath = path.join(oldJsonDir, `${lng}.json`);
const langData = existData[lng] || {};
let oldJsonData = {};
// 将不存在于 keyHashMap 中的键移动到 oldJson 文件中
Object.keys(langData).forEach(hashKey => {
if (!Object.values(keyHashMap).includes(hashKey)) {
oldJsonData[hashKey] = langData[hashKey]; // 将旧的 key 移到 oldJson 中
}
});
// 写入 oldJson 文件
if (Object.keys(oldJsonData).length > 0) {
fs.writeFileSync(oldJsonPath, JSON.stringify(oldJsonData, null, 2));
}
});
done();
}
};
@@ -110,7 +110,7 @@ export const GlobalProvider: FC<{children:ReactNode}> = ({ children }) => {
updateDate: '2024-07-01',
powered:'Powered by https://apipark.com',
mainPage:'/guide/page',
language:'en'
language:'en-US'
});
const [accessData,setAccessData] = useState<Map<string,string[]>>(new Map())
const [pluginAccessDictionary, setPluginAccessDictionary] = useState<{[k:string]:string}>({})
@@ -6,8 +6,12 @@ import crc32 from 'crc/crc32';
// 引入需要实现国际化的简体、繁体、英文三种数据的json文件
import zhCN from 'antd/locale/zh_CN';
import enUS from 'antd/locale/en_US';
import jaJP from 'antd/locale/ja_JP';
import zhTW from 'antd/locale/zh_TW';
import localZh_CN from './scan/zh-CN.json'; // 本地翻译中文文件
import localEn_US from './scan/en-US.json'; // 本地翻译英文文件
import localZh_TW from './scan/zh-TW.json'; // 本地翻译英文文件
import localJa_JP from './scan/ja-JP.json'; // 本地翻译英文文件
// import config from '../../../../i18next-scanner.config.js';
const resources = {
@@ -18,6 +22,14 @@ const resources = {
'en-US': {
translation: localEn_US,
...enUS
},
'zh-TW': {
translation: localZh_TW,
...zhTW
},
'ja-JP': {
translation: localJa_JP,
...jaJP
}
};
@@ -131,13 +131,13 @@
"上传 OpenAPI 文档 (.json/.yaml)": "K6206e4ad",
"替换 OpenAPI 文档 (.json/.yaml)": "Kfba46e6d",
"打开 OpenAPI YAML 编辑器": "Kdac8ce7e",
"无需审核:允许任何应用调用该服务": "Kf5da1284",
"人工审核:仅允许通过人工审核的应用调用该服务": "Kc59ff06d",
"无需审核:允许任何应用调用该服务": "Kffd7e274",
"人工审核:仅允许通过人工审核的应用调用该服务": "K8a8b13e4",
"永久": "Kbfe02d7f",
"否": "K1e9c479e",
"是": "Kaddfcb6b",
"无需审核": "K6a7fa303",
"需要审核": "Kd196e8a4",
"无需审核": "K593e0c7e",
"需要审核": "Ke2d747d9",
"暂无操作权限,请联系管理员分配。": "K23fda291",
"微信小程序": "K4618cb0a",
"获取文件,需填路径": "Ka854f511",
@@ -236,7 +236,7 @@
"使用说明": "Kdefa9caa",
"发布": "K36856e71",
"订阅管理": "K6382bbfd",
"订阅审核": "K2eef4e4",
"订阅审核": "K31af5b99",
"订阅方管理": "Ka97bd9e5",
"管理": "K5974bf24",
"调用拓扑图": "K3fa5c4c3",
@@ -260,7 +260,7 @@
"模型供应商": "Kcf9f90b8",
"模型": "Kfede1c7c",
"参数": "Ke99513a0",
"审核": "K3818f03d",
"审核": "Kb595f40",
"通过": "K54e27f57",
"拒绝": "K8582af3f",
"发布结果": "Kd568e15c",
@@ -280,8 +280,8 @@
"AI 模型管理": "K7ac2be34",
"配置好 AI 模型后,你可以使用对应的大模型来创建 AI 服务": "K2260837a",
"同步最新模型": "K18dccc1a",
"待审核": "K6208054",
"已审核": "K74ab00a3",
"待审核": "K35612f29",
"已审核": "K47eaafde",
"发布申请": "K56b4254f",
"API 调用地址": "Kea2f9279",
"API base URL 一般设置为API 网关的外部网络访问地址,或者是API网关绑定的域名。": "K7fc496a1",
@@ -315,12 +315,12 @@
"服务内包含一组 API,并且可以发布到 API 市场被其他团队使用。": "Kd5be0cd7",
"权限管理": "K62e89ee7",
"订阅服务": "K8f7808e6",
"如果需要调用某个服务的 API,需要先订阅该服务,并且等待提供服务的团队审核后才可发起 API 请求。": "Kb0755523",
"审核订阅申请": "Kd28a1aa5",
"提供服务的团队可以审核来自其他团队的订阅申请,审核通过后的应用才可发起 API 请求。": "K1c15bb2e",
"如果需要调用某个服务的 API,需要先订阅该服务,并且等待提供服务的团队审核后才可发起 API 请求。": "Kf2410413",
"审核订阅申请": "K6c2e44b8",
"提供服务的团队可以审核来自其他团队的订阅申请,审核通过后的应用才可发起 API 请求。": "Ka0a8840a",
"APIPark 提供详尽的 API 调用日志,帮助企业监控、分析和审计 API 的运行状况。": "K3453272",
"Hello!欢迎使用 APIPark": "Kd518ba3e",
"你能通过 APIPark 快速在企业内部构建 API 开放门户/市场,享受极致的转发性能、API 可观测、服务治理、多租户管理、订阅审核流程等诸多好处。": "Ke66e4182",
"你能通过 APIPark 快速在企业内部构建 API 开放门户/市场,享受极致的转发性能、API 可观测、服务治理、多租户管理、订阅审核流程等诸多好处。": "K7e04ea16",
"如果你喜欢我们的产品,欢迎给我们 Star 或提供产品反馈意见。": "Kedd41c18",
"快速入门": "Kef02fd87",
"我们提供了一些任务来帮你快速了解 APIPark": "K43a3b38d",
@@ -542,13 +542,13 @@
"永不过期": "K9dfa2c97",
"到期时间": "Kfa920c0",
"订阅的服务": "Kcce1af60",
"审核详情": "Kbeb4e991",
"审核详情": "Kfefa9b58",
"取消订阅": "K3118fdb0",
"请确认是否取消订阅?": "Ked811bb1",
"取消订阅申请": "K50c39a62",
"请确认是否取消订阅申请?": "K1856c229",
"搜索服务": "K66ea2f0",
"审核中": "Kfeb2559b",
"审核中": "K8adf7f8b",
"添加应用": "K667bbbe7",
"暂无服务描述": "Ka4b45550",
"订阅的服务数量:已通过 (0) 个,申请中 (1) 个": "K3c7b175f",
@@ -557,12 +557,11 @@
"服务详情": "Kf7ec36d",
"申请服务": "K58ca9485",
"介绍": "K59cdbec3",
"Base URL": "K1b6777bb",
"Base URL": "Kc29dabf2",
"申请": "K4aa9ed2c",
"服务信息": "K6c060779",
"接入应用": "K8723422e",
"供应方": "Kb97544cb",
"申请审核": "Kd55c6887",
"分类": "Kb32f0afe",
"版本": "K81634069",
"更新时间": "Keefda53d",
@@ -91,8 +91,8 @@
"K6206e4ad": "Upload OpenAPI Document (.json/.yaml)",
"Kfba46e6d": "Replace OpenAPI Document (.json/.yaml)",
"Kdac8ce7e": "Open OpenAPI YAML Editor",
"Kf5da1284": "No Review: All applications are allowed to subscribe to this service",
"Kc59ff06d": "Manual Review: Only reviewed and approved applications can subscribe to this service",
"Kffd7e274": "No Review: All applications are allowed to subscribe to this service",
"K8a8b13e4": "Manual Review: Only reviewed and approved applications can subscribe to this service",
"Kbfe02d7f": "Permanent",
"K1e9c479e": "No",
"Kaddfcb6b": "Yes",
@@ -194,7 +194,7 @@
"Kdefa9caa": "Usage Instructions",
"K36856e71": "Publish",
"K6382bbfd": "Subscription Management",
"K2eef4e4": "Subscription Review",
"K31af5b99": "Subscription Review",
"Ka97bd9e5": "Subscriber Management",
"K5974bf24": "Management",
"K3fa5c4c3": "Call Topology",
@@ -218,7 +218,7 @@
"Kcf9f90b8": "Model Provider",
"Kfede1c7c": "Model",
"Ke99513a0": "Parameter",
"K3818f03d": "Review",
"Kb595f40": "Review",
"K54e27f57": "Approve",
"K8582af3f": "Reject",
"Kd568e15c": "Publish Result",
@@ -238,8 +238,8 @@
"K7ac2be34": "AI Model Management",
"K2260837a": "After setting up the AI model, you can use the model to create AI services",
"K18dccc1a": "Sync Latest Model",
"K6208054": "Pending Review",
"K74ab00a3": "Reviewed",
"K35612f29": "Pending Review",
"K47eaafde": "Reviewed",
"K56b4254f": "Publish Request",
"Kea2f9279": "API Call Address",
"K7fc496a1": "The API Base URL is generally set as the external network access address of the API Gateway, or the domain bound to the API Gateway.",
@@ -273,12 +273,12 @@
"Kd5be0cd7": "Services include a set of APIs and can be published to the API Marketplace for use by other teams.",
"K62e89ee7": "Permission Management",
"K8f7808e6": "Subscribe to Services",
"Kb0755523": "To call an API from a service, you need to subscribe to that service and wait for approval from the providing team before initiating the API request.",
"Kd28a1aa5": "Review Subscription",
"K1c15bb2e": "Review subscription requests from other applications. Only approved requests can initiate API calls.",
"Kf2410413": "To call an API from a service, you need to subscribe to that service and wait for approval from the providing team before initiating the API request.",
"K6c2e44b8": "Review Subscription",
"Ka0a8840a": "Review subscription requests from other applications. Only approved requests can initiate API calls.",
"K3453272": "APIPark provides detailed API call logs, helping enterprises monitor, analyze, and audit API operations.",
"Kd518ba3e": "Hello! Welcome to APIPark",
"Ke66e4182": "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.",
"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 our product, please give us a Star or provide feedback.",
"Kef02fd87": "Quick Start",
"K43a3b38d": "We've provided tasks to help you quickly understand APIPark",
@@ -500,13 +500,13 @@
"K9dfa2c97": "Never Expires",
"Kfa920c0": "Expiration Time",
"Kcce1af60": "Subscribed Services",
"Kbeb4e991": "Review Details",
"Kfefa9b58": "Review Details",
"K3118fdb0": "Unsubscribe",
"Ked811bb1": "Are you sure you want to unsubscribe?",
"K50c39a62": "Cancel Subscription Request",
"K1856c229": "Are you sure you want to cancel the subscription request?",
"K66ea2f0": "Search Services",
"Kfeb2559b": "Under Review",
"K8adf7f8b": "Under Review",
"K667bbbe7": "Add Application",
"Ka4b45550": "No Service Description",
"K3c7b175f": "Number of Subscribed Services: (0) Approved, (1) Pending",
@@ -521,6 +521,7 @@
"Kb97544cb": "Provider",
"Kb32f0afe": "Category",
"K81634069": "Version",
"K8b7c2592": "Updated By",
"Keefda53d": "Last Update Time",
"K96a2f1c8": "No Tags",
"K72b0c0b3": "Number of APIs",
@@ -571,9 +572,9 @@
"Kb3960e83": "Unpublished",
"K8bd1e18": "Pending Publish",
"K225a6c43": "Unit: s, Minimum: 1",
"K6a7fa303": "No Review Required",
"Kd196e8a4": "Review Required",
"K1b6777bb": "Base URL",
"K593e0c7e": "No Review Required",
"Ke2d747d9": "Review Required",
"Kc29dabf2": "Base URL",
"Kd55c6887": "Review",
"K300c89d4": "When creating an API, this provider is selected by default. Changing the default provider will not affect existing APIs."
}
@@ -91,8 +91,8 @@
"K6206e4ad": "OpenAPI ドキュメント (.json/.yaml) をアップロード",
"Kfba46e6d": "OpenAPI ドキュメント (.json/.yaml) を置き換え",
"Kdac8ce7e": "OpenAPI YAML エディターを開く",
"Kf5da1284": "無審査:すべてのアプリケーションがこのサービスにサブスクライブできます",
"Kc59ff06d": "手動審査:承認されたアプリケーションのみがこのサービスにサブスクライブできます",
"Kffd7e274": "無審査:すべてのアプリケーションがこのサービスにサブスクライブできます",
"K8a8b13e4": "手動審査:承認されたアプリケーションのみがこのサービスにサブスクライブできます",
"Kbfe02d7f": "永久",
"K1e9c479e": "いいえ",
"Kaddfcb6b": "はい",
@@ -194,7 +194,7 @@
"Kdefa9caa": "説明ドキュメント",
"K36856e71": "公開",
"K6382bbfd": "サブスクリプション管理",
"K2eef4e4": "サブスクリプションレビュー",
"K31af5b99": "サブスクリプションレビュー",
"Ka97bd9e5": "サブスクライバー管理",
"K5974bf24": "管理",
"K3fa5c4c3": "トポロジー図を呼び出す",
@@ -218,7 +218,7 @@
"Kcf9f90b8": "モデルプロバイダー",
"Kfede1c7c": "モデル",
"Ke99513a0": "パラメーター",
"K3818f03d": "レビュー",
"Kb595f40": "レビュー",
"K54e27f57": "承認",
"K8582af3f": "拒否",
"Kd568e15c": "公開結果",
@@ -238,8 +238,8 @@
"K7ac2be34": "AI モデル管理",
"K2260837a": "AI モデルを設定したら、そのモデルを使って AI サービスを作成できます",
"K18dccc1a": "最新モデルを同期",
"K6208054": "レビュー中",
"K74ab00a3": "承認済み",
"K35612f29": "レビュー中",
"K47eaafde": "承認済み",
"K56b4254f": "公開申請",
"Kea2f9279": "API 呼び出し先",
"K7fc496a1": "API Base URL は、通常 API ゲートウェイの外部ネットワークアクセスアドレス、または API ゲートウェイにバインドされたドメインを設定します。",
@@ -273,12 +273,12 @@
"Kd5be0cd7": "サービスには一連の API が含まれており、他のチームが使用できるように API マーケットに公開できます。",
"K62e89ee7": "権限管理",
"K8f7808e6": "サービスのサブスクリプション",
"Kb0755523": "あるサービスの API を呼び出すには、まずそのサービスにサブスクライブし、提供チームの承認を待ってから API リクエストを発行できます。",
"Kd28a1aa5": "サブスクリプションをレビュー",
"K1c15bb2e": "他のアプリケーションのサブスクリプション申請をレビューし、承認後に API リクエストが発行できます。",
"Kf2410413": "あるサービスの API を呼び出すには、まずそのサービスにサブスクライブし、提供チームの承認を待ってから API リクエストを発行できます。",
"K6c2e44b8": "サブスクリプションをレビュー",
"Ka0a8840a": "他のアプリケーションのサブスクリプション申請をレビューし、承認後に API リクエストが発行できます。",
"K3453272": "APIPark は詳細な API 呼び出しログを提供し、企業が API の運用状況を監視、分析、監査するのに役立ちます。",
"Kd518ba3e": "こんにちは!APIPark へようこそ",
"Ke66e4182": "APIPark は、100 以上の AI モデルに簡単に接続できるオープンソースの AI ゲートウェイと API 開発者ポータルです。AI モデルとプロンプトを新しい API に組み合わせ、すべての AI リクエストのデータ形式を統一し、AI モデルを切り替えたりプロンプトを調整したりしても、アプリケーションやマイクロサービスに影響を与えません。また、APIPark の開発者ポータルを使用してチーム内で API を共有し、アプリケーションを管理し、API のセキュリティを確保し、AI API の使用状況を監視するための明確なグラフを提供します。",
"K7e04ea16": "APIPark は、100 以上の AI モデルに簡単に接続できるオープンソースの AI ゲートウェイと API 開発者ポータルです。AI モデルとプロンプトを新しい API に組み合わせ、すべての AI リクエストのデータ形式を統一し、AI モデルを切り替えたりプロンプトを調整したりしても、アプリケーションやマイクロサービスに影響を与えません。また、APIPark の開発者ポータルを使用してチーム内で API を共有し、アプリケーションを管理し、API のセキュリティを確保し、AI API の使用状況を監視するための明確なグラフを提供します。",
"Kedd41c18": "もし私たちの製品を気に入ったら、Star を付けるか、フィードバックをお寄せください。",
"Kef02fd87": "クイックスタート",
"K43a3b38d": "APIPark をすばやく理解するためのいくつかのタスクを提供しています",
@@ -500,13 +500,13 @@
"K9dfa2c97": "期限なし",
"Kfa920c0": "有効期限",
"Kcce1af60": "サブスクライブされたサービス",
"Kbeb4e991": "レビューの詳細",
"Kfefa9b58": "レビューの詳細",
"K3118fdb0": "サブスクリプションをキャンセル",
"Ked811bb1": "サブスクリプションをキャンセルしますか?",
"K50c39a62": "サブスクリプション申請をキャンセル",
"K1856c229": "サブスクリプション申請をキャンセルしますか?",
"K66ea2f0": "Service を検索",
"Kfeb2559b": "レビュー中",
"K8adf7f8b": "レビュー中",
"K667bbbe7": "Application を追加",
"Ka4b45550": "サービス説明がありません",
"K3c7b175f": "サブスクライブサービス:承認済み (0)、レビュー中 (1)",
@@ -572,9 +572,9 @@
"Kb3960e83": "未公開",
"K8bd1e18": "公開待ち",
"K225a6c43": "単位: s、最小値: 1",
"K6a7fa303": "レビュー不要",
"Kd196e8a4": "レビュー必要",
"K1b6777bb": "Base URL",
"K593e0c7e": "レビュー不要",
"Ke2d747d9": "レビュー必要",
"Kc29dabf2": "Base URL",
"Kd55c6887": "レビュー",
"K300c89d4": "API 作成時にこのプロバイダーがデフォルトで選択されます。デフォルトプロバイダーを変更しても既存の API には影響しません。"
}
@@ -0,0 +1 @@
{}
@@ -0,0 +1 @@
{}
@@ -0,0 +1 @@
{}
@@ -0,0 +1 @@
{}
@@ -0,0 +1,3 @@
{
"Kd55c6887": "Review"
}
@@ -0,0 +1,3 @@
{
"Kd55c6887": "レビュー"
}
@@ -0,0 +1,3 @@
{
"Kd55c6887": "审核"
}
@@ -0,0 +1,3 @@
{
"Kd55c6887": "審核"
}
@@ -91,8 +91,8 @@
"K6206e4ad": "上传 OpenAPI 文档 (.json/.yaml)",
"Kfba46e6d": "替换 OpenAPI 文档 (.json/.yaml)",
"Kdac8ce7e": "打开 OpenAPI YAML 编辑器",
"Kf5da1284": "无审核:允许所有应用订阅该服务",
"Kc59ff06d": "人工审核:仅允许审核通过的应用订阅该服务",
"Kffd7e274": "无审核:允许所有应用订阅该服务",
"K8a8b13e4": "人工审核:仅允许审核通过的应用订阅该服务",
"Kbfe02d7f": "永久",
"K1e9c479e": "否",
"Kaddfcb6b": "是",
@@ -194,7 +194,7 @@
"Kdefa9caa": "说明文档",
"K36856e71": "发布",
"K6382bbfd": "订阅管理",
"K2eef4e4": "订阅审核",
"K31af5b99": "订阅审核",
"Ka97bd9e5": "订阅方管理",
"K5974bf24": "管理",
"K3fa5c4c3": "调用拓扑图",
@@ -218,7 +218,7 @@
"Kcf9f90b8": "模型供应商",
"Kfede1c7c": "模型",
"Ke99513a0": "参数",
"K3818f03d": "审核",
"Kb595f40": "审核",
"K54e27f57": "通过",
"K8582af3f": "拒绝",
"Kd568e15c": "发布结果",
@@ -238,8 +238,8 @@
"K7ac2be34": "AI 模型管理",
"K2260837a": "设置好 AI 模型后,你可以使用对应的大模型来创建 AI 服务",
"K18dccc1a": "同步最新模型",
"K6208054": "待审核",
"K74ab00a3": "已审核",
"K35612f29": "待审核",
"K47eaafde": "已审核",
"K56b4254f": "发布申请",
"Kea2f9279": "API 调用地址",
"K7fc496a1": "API Base URL 一般设置为 API 网关的外部网络访问地址,或者是API网关绑定的域名。",
@@ -273,12 +273,12 @@
"Kd5be0cd7": "服务内包含一组 API,并且可以发布到 API 市场被其他团队使用。",
"K62e89ee7": "权限管理",
"K8f7808e6": "订阅服务",
"Kb0755523": "如果需要调用某个服务的 API,需要先订阅该服务,并且等待提供服务的团队审核后才可发起 API 请求。",
"Kd28a1aa5": "审核订阅",
"K1c15bb2e": "审核其他应用的订阅申请,审核通过后的才可发起 API 请求。",
"Kf2410413": "如果需要调用某个服务的 API,需要先订阅该服务,并且等待提供服务的团队审核后才可发起 API 请求。",
"K6c2e44b8": "审核订阅",
"Ka0a8840a": "审核其他应用的订阅申请,审核通过后的才可发起 API 请求。",
"K3453272": "APIPark 提供详尽的 API 调用日志,帮助企业监控、分析和审计 API 的运行状况。",
"Kd518ba3e": "Hello!欢迎使用 APIPark",
"Ke66e4182": "APIPark 是开源的一站式 AI 网关和 API 开发者门户,帮助企业和开发者快速接入 100+ AI 模型,将 AI 模型和 Prompt 提示词组合成新的 API,并且统一所有 AI 的请求数据格式,避免切换 AI 模型或调整提示词时影响你的 APP 应用或者微服务。你还可以通过 APIPark 的开发者门户在团队内共享 API,管理调用的应用并保障你的 API 安全,通过清晰的图表来监控你的 AI API 使用情况。",
"K7e04ea16": "APIPark 是开源的一站式 AI 网关和 API 开发者门户,帮助企业和开发者快速接入 100+ AI 模型,将 AI 模型和 Prompt 提示词组合成新的 API,并且统一所有 AI 的请求数据格式,避免切换 AI 模型或调整提示词时影响你的 APP 应用或者微服务。你还可以通过 APIPark 的开发者门户在团队内共享 API,管理调用的应用并保障你的 API 安全,通过清晰的图表来监控你的 AI API 使用情况。",
"Kedd41c18": "如果你喜欢我们的产品,欢迎给我们 Star 或提供产品反馈意见。",
"Kef02fd87": "快速入门",
"K43a3b38d": "我们提供了一些任务来帮你快速了解 APIPark",
@@ -500,13 +500,13 @@
"K9dfa2c97": "永不过期",
"Kfa920c0": "过期时间",
"Kcce1af60": "订阅的服务",
"Kbeb4e991": "审核详情",
"Kfefa9b58": "审核详情",
"K3118fdb0": "取消订阅",
"Ked811bb1": "是否取消订阅?",
"K50c39a62": "取消订阅申请",
"K1856c229": "是否取消订阅申请?",
"K66ea2f0": "搜索服务",
"Kfeb2559b": "审核中",
"K8adf7f8b": "审核中",
"K667bbbe7": "添加应用",
"Ka4b45550": "无服务描述",
"K3c7b175f": "订阅服务:已通过 (0) ,审核中 (1)",
@@ -572,9 +572,9 @@
"Kb3960e83": "未发布",
"K8bd1e18": "待发布",
"K225a6c43": "单位:s,最小值:1",
"K6a7fa303": "无需审核",
"Kd196e8a4": "需要审核",
"K1b6777bb": "Base URL",
"K593e0c7e": "无需审核",
"Ke2d747d9": "需要审核",
"Kc29dabf2": "Base URL",
"Kd55c6887": "审核",
"K300c89d4": "创建 API 时会默认选择该供应商,修改默认供应商不会影响现有 API"
}
@@ -91,8 +91,8 @@
"K6206e4ad": "上傳 OpenAPI 文檔 (.json/.yaml)",
"Kfba46e6d": "替換 OpenAPI 文檔 (.json/.yaml)",
"Kdac8ce7e": "打開 OpenAPI YAML 編輯器",
"Kf5da1284": "無審核:允許所有應用程式訂閱該服務",
"Kc59ff06d": "人工審核:僅允許審核通過的應用程式訂閱該服務",
"Kffd7e274": "無審核:允許所有應用程式訂閱該服務",
"K8a8b13e4": "人工審核:僅允許審核通過的應用程式訂閱該服務",
"Kbfe02d7f": "永久",
"K1e9c479e": "否",
"Kaddfcb6b": "是",
@@ -194,7 +194,7 @@
"Kdefa9caa": "說明文檔",
"K36856e71": "發布",
"K6382bbfd": "訂閱管理",
"K2eef4e4": "訂閱審核",
"K31af5b99": "訂閱審核",
"Ka97bd9e5": "訂閱方管理",
"K5974bf24": "管理",
"K3fa5c4c3": "調用拓撲圖",
@@ -218,7 +218,7 @@
"Kcf9f90b8": "模型供應商",
"Kfede1c7c": "模型",
"Ke99513a0": "參數",
"K3818f03d": "審核",
"Kb595f40": "審核",
"K54e27f57": "通過",
"K8582af3f": "拒絕",
"Kd568e15c": "發布結果",
@@ -238,8 +238,8 @@
"K7ac2be34": "AI 模型管理",
"K2260837a": "設置好 AI 模型後,你可以使用對應的大模型來創建 AI 服務",
"K18dccc1a": "同步最新模型",
"K6208054": "待審核",
"K74ab00a3": "已審核",
"K35612f29": "待審核",
"K47eaafde": "已審核",
"K56b4254f": "發布申請",
"Kea2f9279": "API 調用地址",
"K7fc496a1": "API Base URL 一般設置為 API 網關的外部網絡訪問地址,或者是API網關綁定的域名。",
@@ -273,12 +273,12 @@
"Kd5be0cd7": "服務內包含一組 API,並且可以發布到 API 市場被其他團隊使用。",
"K62e89ee7": "權限管理",
"K8f7808e6": "訂閱服務",
"Kb0755523": "如果需要調用某個服務的 API,需要先訂閱該服務,並且等待提供服務的團隊審核後才可發起 API 請求。",
"Kd28a1aa5": "審核訂閱",
"K1c15bb2e": "審核其他應用程式的訂閱申請,審核通過後的才可發起 API 請求。",
"Kf2410413": "如果需要調用某個服務的 API,需要先訂閱該服務,並且等待提供服務的團隊審核後才可發起 API 請求。",
"K6c2e44b8": "審核訂閱",
"Ka0a8840a": "審核其他應用程式的訂閱申請,審核通過後的才可發起 API 請求。",
"K3453272": "APIPark 提供詳盡的 API 調用日誌,幫助企業監控、分析和審計 API 的運行狀況。",
"Kd518ba3e": "Hello!歡迎使用 APIPark",
"Ke66e4182": "APIPark 是開源的一站式 AI 網關和 API 開發者門戶,幫助企業和開發者快速接入 100+ AI 模型,將 AI 模型和 Prompt 提示詞組合成新的 API,並且統一所有 AI 的請求數據格式,避免切換 AI 模型或調整提示詞時影響你的 APP 應用程式或者微服務。你還可以通過 APIPark 的開發者門戶在團隊內共享 API,管理調用的應用程式並保障你的 API 安全,通過清晰的圖表來監控你的 AI API 使用情況。",
"K7e04ea16": "APIPark 是開源的一站式 AI 網關和 API 開發者門戶,幫助企業和開發者快速接入 100+ AI 模型,將 AI 模型和 Prompt 提示詞組合成新的 API,並且統一所有 AI 的請求數據格式,避免切換 AI 模型或調整提示詞時影響你的 APP 應用程式或者微服務。你還可以通過 APIPark 的開發者門戶在團隊內共享 API,管理調用的應用程式並保障你的 API 安全,通過清晰的圖表來監控你的 AI API 使用情況。",
"Kedd41c18": "如果你喜歡我們的產品,歡迎給我們 Star 或提供產品反饋意見。",
"Kef02fd87": "快速入門",
"K43a3b38d": "我們提供了一些任務來幫你快速了解 APIPark",
@@ -500,13 +500,13 @@
"K9dfa2c97": "永不過期",
"Kfa920c0": "過期時間",
"Kcce1af60": "訂閱的服務",
"Kbeb4e991": "審核詳情",
"Kfefa9b58": "審核詳情",
"K3118fdb0": "取消訂閱",
"Ked811bb1": "是否取消訂閱?",
"K50c39a62": "取消訂閱申請",
"K1856c229": "是否取消訂閱申請?",
"K66ea2f0": "搜索服務",
"Kfeb2559b": "審核中",
"K8adf7f8b": "審核中",
"K667bbbe7": "添加應用程式",
"Ka4b45550": "無服務描述",
"K3c7b175f": "訂閱服務:已通過 (0) ,審核中 (1)",
@@ -572,9 +572,9 @@
"Kb3960e83": "未發布",
"K8bd1e18": "待發布",
"K225a6c43": "單位:s,最小值:1",
"K6a7fa303": "無需審核",
"Kd196e8a4": "需要審核",
"K1b6777bb": "Base URL",
"K593e0c7e": "無需審核",
"Ke2d747d9": "需要審核",
"Kc29dabf2": "Base URL",
"Kd55c6887": "審核",
"K300c89d4": "創建 API 時會默認選擇該供應商,修改默認供應商不會影響現有 API"
}
@@ -1,416 +0,0 @@
import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from "react";
import { App, Button, Form, Input, Radio, Row, Select, TreeSelect, Upload } from "antd";
import { Link, useNavigate, useParams } from "react-router-dom";
import { RouterParams } from "@core/components/aoplatform/RenderRoutes.tsx";
import { BasicResponse, DELETE_TIPS, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from "@common/const/const.tsx";
import { useFetch} from "@common/hooks/http.ts";
import { DefaultOptionType } from "antd/es/cascader";
import { EntityItem, MemberItem, SimpleTeamItem } from "@common/const/type.ts";
import { v4 as uuidv4 } from 'uuid'
import { validateUrlSlash } from "@common/utils/validate.ts";
import { normFile } from "@common/utils/uploadPic.ts";
import { useBreadcrumb } from "@common/contexts/BreadcrumbContext.tsx";
import { SERVICE_VISUALIZATION_OPTIONS } from "@core/const/system/const.tsx";
import { RcFile, UploadChangeParam, UploadFile, UploadProps } from "antd/es/upload/interface";
import { LoadingOutlined } from "@ant-design/icons";
import { getImgBase64 } from "@common/utils/dataTransfer.ts";
import { CategorizesType } from "@market/const/serviceHub/type.ts";
import WithPermission from "@common/components/aoplatform/WithPermission.tsx";
import { Icon } from "@iconify/react/dist/iconify.js";
import { useGlobalContext } from "@common/contexts/GlobalStateContext.tsx";
import { $t } from "@common/locales/index.ts";
import { AiServiceConfigHandle, AiServiceConfigFieldType } from "@core/const/ai-service/type";
import { useAiServiceContext } from "@core/contexts/AiServiceContext";
type SimpleAiProviderItem = EntityItem & {
configured:boolean
logo:string
}
const AiServiceConfig = forwardRef<AiServiceConfigHandle>((_,ref) => {
const { message,modal } = App.useApp()
const { teamId, serviceId } = useParams<RouterParams>();
const [onEdit, setOnEdit] = useState<boolean>(!!teamId)
const [form] = Form.useForm();
const {fetchData} = useFetch()
const [teamOptionList, setTeamOptionList] = useState<DefaultOptionType[]>()
const [providerOptionList, setProviderOptionList] = useState<DefaultOptionType[]>()
const navigate = useNavigate();
const {setBreadcrumb} = useBreadcrumb()
const { setAiServiceInfo} = useAiServiceContext()
const [showClassify, setShowClassify] = useState<boolean>()
const [imageBase64, setImageBase64] = useState<string | null>(null);
const [tagOptionList, setTagOptionList] = useState<DefaultOptionType[]>([])
const [serviceClassifyOptionList, setServiceClassifyOptionList] = useState<DefaultOptionType[]>()
const [uploadLoading, setUploadLoading] = useState<boolean>(false)
const {checkPermission,accessInit, getGlobalAccessData,state, aiConfigFlushed, setAiConfigFlushed} = useGlobalContext()
useImperativeHandle(ref, () => ({
save:onFinish
}));
const beforeUpload = async (file: RcFile) => {
if (!['image/png', 'image/jpeg', 'image/svg+xml'].includes(file.type)) {
alert($t('只允许上传PNG、JPG或SVG格式的图片'));
return false;
}
const reader = new FileReader();
reader.onload = (e: ProgressEvent<FileReader>) => {
setImageBase64(e.target?.result as string);
form.setFieldValue('logo', e.target?.result);
};
reader.readAsDataURL(file);
// }
return false;
};
const handleChange: UploadProps['onChange'] = (info: UploadChangeParam<UploadFile>) => {
if (info.file.status === 'uploading') {
setUploadLoading(true);
return;
}
if (info.file.status === 'done') {
getImgBase64(info.file.originFileObj as RcFile, () => {
setUploadLoading(false);
});
}
if (info.fileList.length === 0) {
form.setFieldValue( "logo", null );
}
};
const uploadButton = (
<div>
{uploadLoading ? <LoadingOutlined /> : <Icon icon="ic:baseline-add" width="24" height="24"/>}
</div>
);
const getTagAndServiceClassifyList = ()=>{
setTagOptionList([])
setServiceClassifyOptionList([])
fetchData<BasicResponse<{ catalogues:CategorizesType[],tags:EntityItem[]}>>('catalogues',{method:'GET'}).then(response=>{
const {code,data,msg} = response
if(code === STATUS_CODE.SUCCESS){
setTagOptionList(data.tags?.map((x:EntityItem)=>{return {
label:x.name, value:x.name
}})||[])
setServiceClassifyOptionList(data.catalogues)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
}
// 获取表单默认值
const getAiServiceInfo = () => {
fetchData<BasicResponse<{ service: AiServiceConfigFieldType }>>('ai-service/info',{method:'GET',eoParams:{team:teamId, service:serviceId},eoTransformKeys:['team_id','service_type']}).then(response=>{
const {code,data,msg} = response
if(code === STATUS_CODE.SUCCESS){
setTimeout(()=>{
form.setFieldsValue({
...data.service,
team:data.service.team.id,
catalogue:data.service.catalogue?.id,
tags:data.service.tags?.map((x:EntityItem)=>x.name),
provider:data.service.provider.id,
logoFile:[
{
uid: '-1', // 文件唯一标识
name: 'image.png', // 文件名
status: 'done', // 状态有:uploading, done, error, removed
url: data.service?.logo || '', // 图片 Base64 数据
}
]
})
setImageBase64(data.service.logo)
setShowClassify(data.service.serviceType === 'public')
},0)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
};
const onFinish:()=>Promise<boolean|string> = () => {
return form.validateFields().then((value)=>{
return fetchData<BasicResponse<{service:{id:string}}>>(serviceId === undefined? 'team/ai-service':'ai-service/info',{method:serviceId === undefined? 'POST' : 'PUT',eoParams: {...(serviceId === undefined ? {team:value.team} :{service:serviceId,team:teamId})},eoBody:({...value,prefix:value.prefix?.trim()}), eoTransformKeys:['serviceType']},).then(response=>{
const {code,data,msg} = response
if(code === STATUS_CODE.SUCCESS){
message.success(msg || $t(RESPONSE_TIPS.success))
setAiServiceInfo(data.service)
return Promise.resolve(true)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
return Promise.reject(msg || $t(RESPONSE_TIPS.error))
}
}).catch((errorInfo)=>{
return Promise.reject(errorInfo)
})
})
};
const getProviderOptionList = ()=>{
setProviderOptionList([])
fetchData<BasicResponse<{ providers: SimpleAiProviderItem[] }>>('simple/ai/providers',{method:'GET',eoTransformKeys:[]}).then(response=>{
const {code,data,msg} = response
if(code === STATUS_CODE.SUCCESS){
setProviderOptionList(data.providers?.filter(x=>x.configured)?.map((x:SimpleAiProviderItem)=>{return {...x,
label: <div className="flex items-center" dangerouslySetInnerHTML={{ __html: x.logo }} />, value:x.id
}}))
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
}
const getTeamOptionList = ()=>{
setTeamOptionList([])
fetchData<BasicResponse<{ teams: SimpleTeamItem[] }>>(!checkPermission('system.workspace.team.view_all') ?'simple/teams/mine' :'simple/teams',{method:'GET',eoTransformKeys:[]}).then(response=>{
const {code,data,msg} = response
if(code === STATUS_CODE.SUCCESS){
setTeamOptionList(data.teams?.map((x:MemberItem)=>{return {...x,
label:x.name, value:x.id
}}))
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
}
const deleteAiService = ()=>{
fetchData<BasicResponse<null>>('team/ai-service',{method:'DELETE',eoParams:{team:teamId,service:serviceId}}).then(response=>{
const {code,msg} = response
if(code === STATUS_CODE.SUCCESS){
message.success(msg || $t(RESPONSE_TIPS.success))
navigate(`/aiservice/list`)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
}
useEffect(()=>{
aiConfigFlushed && getProviderOptionList()
},[aiConfigFlushed])
useEffect(() => {
getProviderOptionList()
getTagAndServiceClassifyList()
if(accessInit){
getTeamOptionList()
}else{
getGlobalAccessData()?.then(()=>{
getTeamOptionList()
})
}
if (serviceId !== undefined) {
setOnEdit(true);
getAiServiceInfo();
setBreadcrumb([
{
title: <Link to={`/aiservice/list`}>{$t('服务')}</Link>
},
{
title: $t('设置')
}])
} else {
setOnEdit(false);
form.setFieldValue('id',uuidv4());
form.setFieldValue('team',teamId);
form.setFieldValue('serviceType','inner');
}
return (form.setFieldsValue({}))
}, [serviceId]);
const deleteAiServiceModal = async ()=>{
modal.confirm({
title:$t('删除'),
content:$t(DELETE_TIPS.default),
onOk:()=> {
return deleteAiService()
},
width:600,
okText:$t('确认'),
okButtonProps:{
danger:true
},
cancelText:$t('取消'),
closable:true,
icon:<></>
})
}
const visualizationOptions = useMemo(()=>SERVICE_VISUALIZATION_OPTIONS.map((x)=>({...x, label:$t(x.label)})),[state.language])
return (
<>
<WithPermission access={onEdit ? 'team.service.service.edit' :''}>
<Form
layout='vertical'
labelAlign='left'
scrollToFirstError
form={form}
className="w-full pr-PAGE_INSIDE_X "
name="systemConfig"
onFinish={onFinish}
autoComplete="off"
>
<div>
<Form.Item<AiServiceConfigFieldType>
label={$t("服务名称")}
name="name"
rules={[{ required: true ,whitespace:true }]}
>
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)}/>
</Form.Item>
<Form.Item<AiServiceConfigFieldType>
label={$t("服务ID")}
name="id"
rules={[{ required: true ,whitespace:true }]}
>
<Input className="w-INPUT_NORMAL" disabled={onEdit} placeholder={$t(PLACEHOLDER.input)}/>
</Form.Item>
<Form.Item<AiServiceConfigFieldType>
label={$t("AI 模型供应商")}
name="provider"
rules={[{ required: true }]}
>{
(providerOptionList && providerOptionList.length >0 ) ? <Select className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} options={providerOptionList} >
</Select> : <p> AI <a href="/aisetting" target="_blank" onClick={()=>setAiConfigFlushed(false)}></a></p>
}
</Form.Item>
<Form.Item<AiServiceConfigFieldType>
label={$t("API 调用前缀")}
name="prefix"
extra={$t("选填,作为服务内所有API的前缀,比如host/{service_name}/{api_path},一旦保存无法修改")}
rules={[
{
validator: validateUrlSlash,
}]}
>
<Input prefix={onEdit ? '' : '/'} className="w-INPUT_NORMAL" disabled={onEdit} placeholder={$t(PLACEHOLDER.input)}/>
</Form.Item>
<Form.Item<AiServiceConfigFieldType>
label={$t("图标")}
name="logoFile"
extra={$t("仅支持 .png .jpg .jpeg .svg 格式的图片文件, 大于 1KB 的文件将被压缩")}
valuePropName="fileList" getValueFromEvent={normFile}
>
<Upload
listType="picture"
beforeUpload={beforeUpload}
onChange={handleChange}
showUploadList={false}
maxCount={1}
accept=".png, .jpg, .jpeg, .svg"
>
<div className="h-[68px] w-[68px] border-[1px] border-dashed border-BORDER flex items-center justify-center rounded bg-bar-theme cursor-pointer" style={{ marginTop: 8 }}>
{imageBase64 ? <img src={imageBase64} alt="Logo" style={{ maxWidth: '200px', width:'68px',height:'68px'}} /> : uploadButton}
</div>
</Upload>
</Form.Item>
<Form.Item<AiServiceConfigFieldType>
label={$t("描述")}
name="description"
>
<Input.TextArea className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)}/>
</Form.Item>
<Form.Item<AiServiceConfigFieldType>
label={$t("Logo")}
name="logo"
hidden
>
</Form.Item>
{!onEdit && <Form.Item<AiServiceConfigFieldType>
label={$t("所属团队")}
name="team"
rules={[{ required: true }]}
>
<Select className="w-INPUT_NORMAL" disabled={onEdit} placeholder={$t(PLACEHOLDER.input)} options={teamOptionList} >
</Select>
</Form.Item>}
<Form.Item<AiServiceConfigFieldType>
label={$t("标签")}
name="tags"
>
<Select
className="w-INPUT_NORMAL"
mode="tags"
placeholder={$t(PLACEHOLDER.select)}
options={tagOptionList}>
</Select>
</Form.Item>
<Form.Item<AiServiceConfigFieldType>
label={$t("服务类型")}
name="serviceType"
rules={[{required: true}]}
>
<Radio.Group className="flex flex-col" options={visualizationOptions} onChange={(e)=>{setShowClassify(e.target.value === 'public')}} />
</Form.Item>
{showClassify &&
<Form.Item<AiServiceConfigFieldType>
label={$t("所属服务分类")}
name="catalogue"
extra={$t("设置服务展示在服务市场中的哪个分类下")}
rules={[{required: true}]}
>
<TreeSelect
className="w-INPUT_NORMAL"
fieldNames={{label:'name',value:'id',children:'children'}}
showSearch
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
placeholder={$t(PLACEHOLDER.select)}
allowClear
treeDefaultExpandAll
treeData={serviceClassifyOptionList}
/>
</Form.Item>
}
{onEdit && <>
<Row className="mb-[10px]"
// wrapperCol={{ offset: 5, span: 19 }}
>
<WithPermission access={onEdit ? 'team.service.service.edit' :''}>
<Button type="primary" htmlType="submit">
{$t('保存')}
</Button>
</WithPermission>
</Row></>}
</div>
{onEdit && <>
<WithPermission access="team.service.service.delete" showDisabled={false}>
<div className="bg-[rgb(255_120_117_/_5%)] rounded-[10px] mt-[50px] p-btnrbase pb-0">
<p className="text-left"><span className="font-bold">{$t('删除服务')}</span>{$t('删除操作不可恢复,请谨慎操作!')}</p>
<div className="text-left">
<WithPermission access="team.service.service.delete">
<Button className="m-auto mt-[16px] mb-[20px]" type="default" danger={true} onClick={deleteAiServiceModal}>{$t('删除服务')}</Button>
</WithPermission>
</div>
</div>
</WithPermission>
</>}
</Form>
</WithPermission>
</>
)
})
export default AiServiceConfig
@@ -80,7 +80,7 @@ const ServiceInsideDocument = ()=>{
], toolbar: 'undo redo | styles | bold italic | alignleft aligncenter alignright alignjustify | codesample |table|' +
'bullist numlist outdent indent | link image | print preview media fullscreen | ' +
'forecolor backcolor emoticons | help',
content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:14px }',
content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:14px; } img { max-width: 100%; }',
setup: setupEditor,
codesample_languages:[
{
@@ -1,159 +0,0 @@
import PageList from "@common/components/aoplatform/PageList.tsx"
import {ActionType} from "@ant-design/pro-components";
import {FC, useEffect, useMemo, useRef, useState} from "react";
import {useNavigate} from "react-router-dom";
import { App} from "antd";
import {useBreadcrumb} from "@common/contexts/BreadcrumbContext.tsx";
import {BasicResponse, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx";
import {useFetch} from "@common/hooks/http.ts";
import { SimpleTeamItem ,SimpleMemberItem} from "@common/const/type.ts";
import { DrawerWithFooter } from "@common/components/aoplatform/DrawerWithFooter.tsx";
import AiServiceConfig from "./AiServiceConfig.tsx";
import { useGlobalContext } from "@common/contexts/GlobalStateContext.tsx";
import { $t } from "@common/locales/index.ts";
import { AiServiceTableListItem, AiServiceConfigHandle } from "@core/const/ai-service/type.ts";
import { AI_SERVICE_TABLE_COLUMNS } from "@core/const/ai-service/const.tsx";
const AiServiceList:FC = ()=>{
const navigate = useNavigate();
const [tableSearchWord, setTableSearchWord] = useState<string>('')
const { setBreadcrumb } = useBreadcrumb()
const [teamList, setTeamList] = useState<{ [k: string]: { text: string; }; }>()
const {fetchData} = useFetch()
const [tableListDataSource, setTableListDataSource] = useState<AiServiceTableListItem[]>([]);
const [tableHttpReload, setTableHttpReload] = useState(true);
const { message } = App.useApp()
const pageListRef = useRef<ActionType>(null);
const [memberValueEnum, setMemberValueEnum] = useState<{[k:string]:{text:string}}>({})
const [open, setOpen] = useState(false);
const drawerFormRef = useRef<AiServiceConfigHandle>(null)
const {checkPermission,accessInit, getGlobalAccessData,state} = useGlobalContext()
const getAiServiceList = ()=>{
if(!accessInit){
getGlobalAccessData()?.then(()=>{
getAiServiceList()
})
return
}
if(!tableHttpReload){
setTableHttpReload(true)
return Promise.resolve({
data: tableListDataSource,
success: true,
});
}
return fetchData<BasicResponse<{services:AiServiceTableListItem[]}>>(!checkPermission('system.workspace.service.view_all') ? 'my_ai_services':'ai-services',{method:'GET',eoParams:{keyword:tableSearchWord},eoTransformKeys:['api_num','can_delete','create_time']}).then(response=>{
const {code,data,msg} = response
if(code === STATUS_CODE.SUCCESS){
setTableListDataSource(data.services)
setTableHttpReload(false)
return {data:data.services, success: true}
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
return {data:[], success:false}
}
}).catch(() => {
return {data:[], success:false}
})
}
const getTeamsList = ()=>{
if(!accessInit){
getGlobalAccessData()?.then(()=>{
getTeamsList()
})
return
}
fetchData<BasicResponse<{ teams: SimpleTeamItem[] }>>(!checkPermission('system.workspace.team.view_all') ?'simple/teams/mine' :'simple/teams',{method:'GET',eoTransformKeys:[]}).then(response=>{
const {code,data,msg} = response
setTeamList(data.teams)
if(code === STATUS_CODE.SUCCESS){
const tmpValueEnum:{[k:string]:{text:string}} = {}
data.teams?.forEach((x:SimpleMemberItem)=>{
tmpValueEnum[x.name] = {text:x.name}
})
setTeamList(tmpValueEnum)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
return {data:[], success:false}
}
})
}
const manualReloadTable = () => {
setTableHttpReload(true); // 表格数据需要从后端接口获取
pageListRef.current?.reload()
};
const getMemberList = async ()=>{
setMemberValueEnum({})
const {code,data,msg} = await fetchData<BasicResponse<{ members: SimpleMemberItem[] }>>('simple/member',{method:'GET'})
if(code === STATUS_CODE.SUCCESS){
const tmpValueEnum:{[k:string]:{text:string}} = {}
data.members?.forEach((x:SimpleMemberItem)=>{
tmpValueEnum[x.name] = {text:x.name}
})
setMemberValueEnum(tmpValueEnum)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
}
}
useEffect(() => {
getTeamsList();
getMemberList()
setBreadcrumb([
{
title: $t('服务')
}])
}, []);
const onClose = () => {
setOpen(false);
};
const columns = useMemo(()=>{
const res = AI_SERVICE_TABLE_COLUMNS.map(x=>{
if(x.filters &&((x.dataIndex as string[])?.indexOf('master') !== -1 ) ){
x.valueEnum = memberValueEnum
}
if(x.filters &&((x.dataIndex as string[])?.indexOf('team') !== -1 ) ){
x.valueEnum = teamList
}
return {...x,title:typeof x.title === 'string' ? $t(x.title as string) : x.title}})
return res
},[memberValueEnum,teamList,state.language]);
return (
<div className="h-full w-full pr-PAGE_INSIDE_X pb-PAGE_INSIDE_B">
{/* <Joyride steps={steps} run={true} /> */}
<PageList
id="global_ai_system"
ref={pageListRef}
columns={[...columns]}
request={()=>getAiServiceList()}
addNewBtnTitle={$t("添加服务")}
addNewBtnWrapperClass={'my-first-step'}
searchPlaceholder={$t("输入名称、ID、所属团队、负责人查找服务")}
onAddNewBtnClick={() => {
setOpen(true)
}}
manualReloadTable={manualReloadTable}
onChange={() => {
setTableHttpReload(false)
}}
onSearchWordChange={(e) => {
setTableSearchWord(e.target.value)
}}
onRowClick={(row:AiServiceTableListItem)=>navigate(`/aiservice/${row.team.id}/inside/${row.id}`)}
/>
<DrawerWithFooter title={$t("添加 AI 服务")} open={open} onClose={onClose} onSubmit={()=>drawerFormRef.current?.save()?.then((res)=>{res && manualReloadTable();return res})} >
<AiServiceConfig ref={drawerFormRef} />
</DrawerWithFooter>
</div>
)
}
export default AiServiceList
@@ -67,7 +67,7 @@ export default function ApiRequestSetting(){
>
<Form.Item<ApiRequestSettingFieldType>
label={$t("API 调用地址")}
name="name"
name="invokeAddress"
rules={[{ required: true,whitespace:true }]}
extra={$t("API base URL 一般设置为API 网关的外部网络访问地址,或者是API网关绑定的域名。")}
>
@@ -348,7 +348,7 @@ const MemberList = ()=>{
className="w-full"
mode="multiple"
value={entity.roles?.map((x:EntityItem)=>x.id)}
options={roleSelectableList?.map((x:{id:string,name:string})=>({label:$t(x.name), value:x.id}))}
options={roleSelectableList?.map((x:{id:string,name:string})=>({label:(x.name), value:x.id}))}
onChange={(value)=>{
changeMemberInfo(value,entity ).then((res)=>{
if(res) manualReloadTable()
@@ -11,7 +11,6 @@ import { useGlobalContext } from "@common/contexts/GlobalStateContext";
type PermissionItem = {
name:string
cname:string
value:string
}
@@ -57,9 +56,9 @@ const PermissionContent = ({permits,onChange,value=[],id,dependenciesMap}:{permi
permits.map((item:PermissionClassify)=>(
<>
<div className="flex flex-col gap-btnbase" key={`group-${item.name}`}>
{item.cname !== '' && <p className="">{$t(item.cname)}</p>}
{item.name !== '' && <p className="">{(item.name)}</p>}
<div className=" pl-[20px]">
{item.children.map(x=><Checkbox id={x.value} key={x.value} checked={value && value.length > 0 && value.indexOf(x.value)>-1} onChange={onSingleCheckboxChange}>{$t(x.cname)}</Checkbox>)}
{item.children.map(x=><Checkbox id={x.value} key={x.value} checked={value && value.length > 0 && value.indexOf(x.value)>-1} onChange={onSingleCheckboxChange}>{(x.name)}</Checkbox>)}
</div>
</div>
</>
@@ -76,7 +75,7 @@ const PermissionContent = ({permits,onChange,value=[],id,dependenciesMap}:{permi
const items = useMemo(()=>{
const generatePermissionItem = (permissionItem:RolePermissionItem[])=> permissionItem.map((item:RolePermissionItem)=>({
key:item.name,
label:$t(item.cname),
label:(item.name),
children:<PermissionContent value={value} permits={item.children} onChange={(e)=>onChange?.(e)} id={id!} dependenciesMap={dependenciesMap!}/>
}))
return permissionTemplate && permissionTemplate.length > 0 ? generatePermissionItem(permissionTemplate) : []
@@ -45,7 +45,7 @@ const RoleList = ()=>{
return fetchData<BasicResponse<{roles:RoleTableListItem[]}>>(`${group}/roles`,{method:'GET'}).then(response=>{
const {code,data,msg} = response
if(code === STATUS_CODE.SUCCESS){
return {data:data.roles?.map((x:RoleTableListItem)=>({...x,name:$t(x.name)})), success: true}
return {data:data.roles?.map((x:RoleTableListItem)=>({...x,name:(x.name)})), success: true}
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
return {data:[], success:false}
@@ -1,277 +0,0 @@
import TreeWithMore from "@common/components/aoplatform/TreeWithMore";
import WithPermission from "@common/components/aoplatform/WithPermission";
import { BasicResponse, DELETE_TIPS, RESPONSE_TIPS, STATUS_CODE } from "@common/const/const";
import { PERMISSION_DEFINITION } from "@common/const/permissions";
import { useFetch } from "@common/hooks/http";
import { checkAccess } from "@common/utils/permission";
import { CategorizesType, ServiceHubCategoryConfigHandle } from "@market/const/serviceHub/type";
import { App, Button, Spin, Tree, TreeDataNode, TreeProps } from "antd";
import { DataNode } from "antd/es/tree";
import { Key, useEffect, useMemo, useRef, useState } from "react";
import { ServiceHubCategoryConfig } from "./ServiceHubCategoryConfig";
import { useGlobalContext } from "@common/contexts/GlobalStateContext";
import { useBreadcrumb } from "@common/contexts/BreadcrumbContext";
import { LoadingOutlined } from "@ant-design/icons";
import { cloneDeep } from "lodash-es";
import { Icon } from "@iconify/react/dist/iconify.js";
import InsidePage from "@common/components/aoplatform/InsidePage";
import { EntityItem } from "@common/const/type";
import { $t } from "@common/locales";
export default function ServiceCategory(){
const [gData, setGData] = useState<CategorizesType[]>([]);
const [cateData, setCateData] = useState<CategorizesType[]>([]);
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
const {message,modal} = App.useApp()
const {fetchData} = useFetch()
const addRef = useRef<ServiceHubCategoryConfigHandle>(null)
const addChildRef = useRef<ServiceHubCategoryConfigHandle>(null)
const renameRef = useRef<ServiceHubCategoryConfigHandle>(null)
const {accessData} = useGlobalContext()
const { setBreadcrumb } = useBreadcrumb()
const [loading, setLoading] = useState<boolean>(false)
const onDrop: TreeProps['onDrop'] = (info) => {
const dropKey = info.node.key;
const dragKey = info.dragNode.key;
const dropPos = info.node.pos.split('-');
const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]); // the drop position relative to the drop node, inside 0, top -1, bottom 1
const loop = (
data: TreeDataNode[],
key: React.Key,
callback: (node: TreeDataNode, i: number, data: TreeDataNode[]) => void,
) => {
for (let i = 0; i < data.length; i++) {
if (data[i].id === key) {
return callback(data[i], i, data);
}
if (data[i].children) {
loop(data[i].children!, key, callback);
}
}
};
const data = cloneDeep(gData);
// Find dragObject
let dragObj: TreeDataNode;
loop(data, dragKey, (item, index, arr) => {
arr.splice(index, 1);
dragObj = item;
});
if (!info.dropToGap) {
// Drop on the content
loop(data, dropKey, (item) => {
item.children = item.children || [];
// where to insert. New item was inserted to the start of the array in this example, but can be anywhere
item.children.unshift(dragObj);
});
} else {
let ar: TreeDataNode[] = [];
let i: number;
loop(data, dropKey, (_item, index, arr) => {
ar = arr;
i = index;
});
if (dropPosition === -1) {
// Drop on the top of the drop node
ar.splice(i!, 0, dragObj!);
} else {
// Drop on the bottom of the drop node
ar.splice(i! + 1, 0, dragObj!);
}
}
setGData(data);
sortCategories(data)
};
const dropdownMenu = (entity:CategorizesType) => [
{
key: 'addChildCate',
label: (
<WithPermission access="system.api_market.service_classification.add"><Button className="border-none p-0 flex items-center bg-transparent " onClick={()=>openModal('addChildCate',entity)}>
{$t('添加子分类')}
</Button></WithPermission>
),
},
{
key: 'renameCate',
label: (
<WithPermission access="system.api_market.service_classification.edit"><Button className=" border-none p-0 flex items-center bg-transparent " onClick={()=>openModal('renameCate',entity)}>
{$t('修改分类名称')}
</Button></WithPermission>
),
},
{
key: 'delete',
label: (
<WithPermission access="system.api_market.service_classification.delete"><Button className=" border-none p-0 flex items-center bg-transparent " onClick={()=>openModal('delete',entity)}>
{$t('删除')}
</Button></WithPermission>
),
},
];
const treeData = useMemo(() => {
setExpandedKeys([])
const loop = (data: CategorizesType[]): DataNode[] =>
data?.map((item) => {
if (item.children) {
setExpandedKeys(prev=>[...prev,item.id])
return {
title: <TreeWithMore
stopClick={false}
dropdownMenu={dropdownMenu(item as CategorizesType)}>{item.name}</TreeWithMore> ,
key: item.id, children: loop(item.children)
};
}
return {
title: <TreeWithMore
stopClick={false}
dropdownMenu={dropdownMenu(item as CategorizesType)}>{item.name}</TreeWithMore>,
key: item.id,
};
});
return loop(gData ?? [])
}, [gData]);
const isActionAllowed = (type:'addCate'|'addChildCate'|'renameCate'|'delete') => {
const actionToPermissionMap = {
'addCate': 'add',
'addChildCate': 'add',
'renameCate': 'edit',
'delete': 'delete'
};
const action = actionToPermissionMap[type];
const permission :keyof typeof PERMISSION_DEFINITION[0]= `system.api_market.service_classification.${action}`;
return !checkAccess(permission, accessData);
};
const openModal = (type:'addCate'|'addChildCate'|'renameCate'|'delete',entity?:CategorizesType)=>{
let title:string = ''
let content:string|React.ReactNode = ''
switch (type){
case 'addCate':
title=$t('添加分类')
content=<ServiceHubCategoryConfig WithPermission={WithPermission} ref={addRef} type={type} />
break;
case 'addChildCate':
title=$t('添加子分类')
content=<ServiceHubCategoryConfig WithPermission={WithPermission} ref={addChildRef} type={type} entity={entity} />
break;
case 'renameCate':
title=$t('重命名分类')
content=<ServiceHubCategoryConfig WithPermission={WithPermission} ref={renameRef} type={type} entity={entity} />
break;
case 'delete':
title=$t('删除')
content=$t(DELETE_TIPS.default)
break;
}
modal.confirm({
title,
content,
onOk:()=>{
switch (type){
case 'addCate':
return addRef.current?.save().then((res)=>{if(res === true) getCategoryList()})
case 'addChildCate':
return addChildRef.current?.save().then((res)=>{if(res === true) getCategoryList()})
case 'renameCate':
return renameRef.current?.save().then((res)=>{if(res === true) getCategoryList()})
case 'delete':
return deleteCate(entity!).then((res)=>{if(res === true) getCategoryList()})
}
},
width:600,
okText:$t('确认'),
okButtonProps:{
disabled : isActionAllowed(type)
},
cancelText:$t('取消'),
closable:true,
icon:<></>,
})
}
const deleteCate = (entity:CategorizesType)=>{
return new Promise((resolve, reject)=>{
fetchData<BasicResponse<null>>('catalogue',{method:'DELETE',eoParams:{catalogue:entity.id},}).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))
})
}
const sortCategories = (newData:CategorizesType[])=>{
setLoading(true)
fetchData<BasicResponse<null>>('catalogue/sort',{method:'PUT',eoBody:newData}).then(response=>{
const {code,msg} = response
if(code === STATUS_CODE.SUCCESS){
getCategoryList()
}else{
setGData(cateData)
message.error(msg || $t(RESPONSE_TIPS.error))
}
}).catch(()=>{setGData(cateData)}).finally(()=>{setLoading(false)})
}
const getCategoryList = ()=>{
setLoading(true)
fetchData<BasicResponse<{ catalogues:CategorizesType[],tags:EntityItem[]}>>('catalogues',{method:'GET'}).then(response=>{
const {code,data,msg} = response
if(code === STATUS_CODE.SUCCESS){
setGData(data.catalogues)
setCateData(data.catalogues)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
}
}).finally(()=>{setLoading(false)})
}
useEffect(()=>{
setBreadcrumb([
{
title: $t('服务分类管理')}])
getCategoryList()
},[])
return (
<InsidePage
pageTitle={$t('服务分类管理')}
description={$t("设置服务可选择的分类,方便团队成员快速找到API。")}
showBorder={false}
contentClassName="pr-PAGE_INSIDE_X"
scrollPage={false}
>
<div className="border border-solid border-BORDER p-[20px] rounded-[10px] ">
<Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} spinning={loading} className=''>
<Tree
showIcon
draggable
blockNode
expandedKeys={expandedKeys}
onExpand={(expandedKeys:Key[])=>{setExpandedKeys(expandedKeys as string[])}}
onDrop={onDrop}
treeData={treeData}
/>
<WithPermission access="system.api_market.service_classification.add">
<Button type="link" className="mt-[12px] pl-[0px]" onClick={()=>openModal('addCate')}><Icon icon="ic:baseline-add" width="18" height="18" className='mr-[2px]'/>{$t('添加分类')}</Button>
</WithPermission>
</Spin>
</div>
</InsidePage>
)
}
@@ -1,122 +0,0 @@
import {App, Form, Input} from "antd";
import {forwardRef, useEffect, useImperativeHandle} from "react";
import {BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE, VALIDATE_MESSAGE} from "@common/const/const.tsx";
import {useFetch} from "@common/hooks/http.ts";
import { ServiceHubCategoryConfigHandle, ServiceHubCategoryConfigFieldType, ServiceHubCategoryConfigProps } from "@market/const/serviceHub/type.ts"
import WithPermission from "@common/components/aoplatform/WithPermission";
import { $t } from "@common/locales";
export const ServiceHubCategoryConfig = forwardRef<ServiceHubCategoryConfigHandle,ServiceHubCategoryConfigProps>((props,ref)=>{
const { message } = App.useApp()
const [form] = Form.useForm();
const {type,entity} = props
const {fetchData} = useFetch()
const save:()=>Promise<boolean | string> = ()=>{
const url:string = 'catalogue'
let method:string
switch (type){
case 'addCate':
case 'addChildCate':
method = 'POST'
break;
case 'renameCate':
method = 'PUT'
break
}
return new Promise((resolve, reject)=>{
if(!url || !method){
reject($t(RESPONSE_TIPS.error))
return
}
form.validateFields().then((value)=>{
fetchData<BasicResponse<null>>(url,{method,eoBody:(value), eoParams:{ ...(type === 'renameCate' ? {catalogue:value.id} :undefined)}}).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))
})
}
useImperativeHandle(ref, ()=>({
save
})
)
useEffect(() => {
switch(type){
case 'addCate':
form.setFieldsValue({})
break
case 'addChildCate':
form.setFieldsValue({parent:entity!.id})
break
case 'renameCate':
form.setFieldsValue(entity)
break
}
}, []);
return (
<WithPermission access={type === 'addCate'? 'system.api_market.service_classification.add': 'system.api_market.service_classification.edit'}>
<Form
layout='vertical'
scrollToFirstError
labelAlign='left'
form={form}
className="mx-auto "
name="serviceHubCategoryConfig"
autoComplete="off"
>
{type === 'renameCate' &&
<Form.Item<ServiceHubCategoryConfigFieldType>
label={$t("ID")}
name="id"
hidden
rules={[{ required: true,whitespace:true }]}
>
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)}/>
</Form.Item>
}
{(type === 'addCate' || type === 'renameCate') &&
<Form.Item<ServiceHubCategoryConfigFieldType>
label={$t("分类名称")}
name="name"
rules={[{ required: true ,whitespace:true }]}
>
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)}/>
</Form.Item>}
{type === 'addChildCate' &&<>
<Form.Item<ServiceHubCategoryConfigFieldType>
label={$t("父分类 ID")}
name="parent"
hidden
rules={[{ required: true,whitespace:true }]}
>
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)}/>
</Form.Item>
<Form.Item<ServiceHubCategoryConfigFieldType>
label={$t("子分类名称")}
name="name"
rules={[{ required: true ,whitespace:true }]}
>
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)}/>
</Form.Item>
</>
}
</Form>
</WithPermission>
)
})
@@ -237,7 +237,7 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_,ref) => {
if(accessInit){
getTeamOptionList()
}else{
getGlobalAccessData()?.then(()=>{
getGlobalAccessData()?.then?.(()=>{
getTeamOptionList()
})
}
@@ -80,7 +80,7 @@ const ServiceInsideDocument = ()=>{
], toolbar: 'undo redo | styles | bold italic | alignleft aligncenter alignright alignjustify | codesample |table|' +
'bullist numlist outdent indent | link image | print preview media fullscreen | ' +
'forecolor backcolor emoticons | help',
content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:14px }',
content_style: 'body{ font-family:Helvetica,Arial,sans-serif; font-size:14px } img{ max-width: 100%; }',
setup: setupEditor,
codesample_languages:[
{
@@ -34,7 +34,7 @@ const SystemList:FC = ()=>{
const getSystemList = ()=>{
if(!accessInit){
getGlobalAccessData()?.then(()=>{
getGlobalAccessData()?.then?.(()=>{
getSystemList()
})
return
@@ -63,7 +63,7 @@ const SystemList:FC = ()=>{
const getTeamsList = ()=>{
if(!accessInit){
getGlobalAccessData()?.then(()=>{
getGlobalAccessData()?.then?.(()=>{
getTeamsList()
})
return
@@ -1,156 +0,0 @@
import {App, Col, Form, Input, Row, Select} from "antd";
import {forwardRef, useEffect, useImperativeHandle, useRef} from "react";
import EditableTableWithModal from "@common/components/aoplatform/EditableTableWithModal.tsx";
import styles from "./SystemInsideApi.module.css"
import {BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE, VALIDATE_MESSAGE} from "@common/const/const.tsx";
import {useFetch} from "@common/hooks/http.ts";
import { HTTP_METHOD, MATCH_CONFIG } from "../../../const/system/const.tsx";
import { SystemInsideApiCreateHandle, SystemInsideApiCreateProps, SystemApiProxyFieldType, SystemInsideApiProxyHandle } from "../../../const/system/type.ts";
import { MatchItem } from "@common/const/type.ts";
import { validateUrlSlash } from "@common/utils/validate.ts";
import { $t } from "@common/locales/index.ts";
import SystemInsideApiProxy from "@core/pages/system/api/SystemInsideApiProxy";
const SystemInsideApiCreate = forwardRef<SystemInsideApiCreateHandle,SystemInsideApiCreateProps>((props, ref) => {
const { message } = App.useApp()
const {type, entity, serviceId,teamId, modalApiPrefix:apiPrefix, modalPrefixForce:prefixForce} = props
const [form] = Form.useForm();
const {fetchData} = useFetch()
const proxyRef = useRef<SystemInsideApiProxyHandle>(null)
const onFinish = ()=>{
return Promise.all([proxyRef.current?.validate?.(), form.validateFields()]).then(([,formValue])=>{
const body = {...formValue,path:formValue.path.trim(),proxy:{...formValue.proxy,path:formValue.proxy.path ? (formValue.proxy.path.startsWith('/')? formValue.proxy.path: '/'+ formValue.proxy.path) : undefined}}
return fetchData<BasicResponse<{api:SystemApiProxyFieldType}>>('service/api',{method:'POST',eoBody:(body), eoParams: {service:serviceId,team:teamId},eoTransformKeys:['matchType']}).then(response=>{
const {code,msg} = response
if(code === STATUS_CODE.SUCCESS){
message.success(msg || $t(RESPONSE_TIPS.success))
return Promise.resolve(true)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
return Promise.reject(msg || $t(RESPONSE_TIPS.error))
}
}).catch(errInfo=>Promise.reject(errInfo))
})
}
const copy: ()=>Promise<boolean | string> = ()=>{
return new Promise((resolve, reject)=>{
return form.validateFields().then((value)=>{
fetchData<BasicResponse<{api:SystemApiProxyFieldType}>>('service/api/copy',{method:'POST',eoParams:{service:serviceId,team:teamId, api:entity!.id},eoBody:({...value,path:value.path.trim()})}).then(response=>{
const {code,data,msg} = response
if(code === STATUS_CODE.SUCCESS){
message.success(msg || $t(RESPONSE_TIPS.success))
return resolve(data.api.id)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
return reject(msg || $t(RESPONSE_TIPS.error))
}
}).catch((errorInfo)=> reject(errorInfo))
}).catch((errorInfo)=> reject(errorInfo))
})
}
useImperativeHandle(ref, ()=>({
copy,
save:onFinish
})
)
useEffect(() => {
if(type === 'copy'){
form.setFieldsValue({
...entity,
name:`${$t('副本')}-${entity!.name}`,
...(prefixForce?
{prefix:apiPrefix,path: entity!.path.substring(apiPrefix?.length|| 0)}:
{}),
proxy:{timeout:10000, retry:0, ...entity?.proxy}
});
}
else{
form.setFieldValue('prefix',apiPrefix)
form.setFieldValue(['proxy','timeout'],10000)
form.setFieldValue(['proxy','retry'],0)
}
return (form.setFieldsValue({}))
}, []);
return (<div className="h-full w-full">
<Form
layout='vertical'
labelAlign='left'
scrollToFirstError
form={form}
className="mx-auto flex flex-col h-full"
name="systemInsideApiCreate"
onFinish={onFinish}
autoComplete="off"
>
<div className="">
<Row className="mb-btnybase" > <Col ><span className="font-bold mr-[13px]">{$t('API 基础信息')}</span></Col></Row>
<Form.Item<SystemApiProxyFieldType>
label={$t("API 名称")}
name="name"
rules={[{ required: true ,whitespace:true }]}
>
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)}/>
</Form.Item>
<Form.Item<SystemApiProxyFieldType>
label={$t("描述")}
name="description"
>
<Input.TextArea className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)}/>
</Form.Item>
<Form.Item<SystemApiProxyFieldType>
label={$t("请求方式")}
name="method"
rules={[{ required: true }]}
>
<Select className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.select)} options={HTTP_METHOD.map((method:string)=>{
return { label:method, value:method}
})}>
</Select>
</Form.Item>
<Form.Item<SystemApiProxyFieldType>
label={$t("请求路径")}
name="path"
rules={[{ required: true,whitespace:true },
{
validator: validateUrlSlash,
}]}
className={styles['form-input-group']}
>
<Input prefix={(prefixForce ? `${apiPrefix}/` :"/")} className="w-INPUT_NORMAL"
placeholder={$t(PLACEHOLDER.input)}/>
</Form.Item>
<Form.Item<SystemApiProxyFieldType>
label={$t("高级匹配")}
name="match"
>
<EditableTableWithModal<MatchItem & {_id:string}>
configFields={MATCH_CONFIG}
/>
</Form.Item>
{/* } */}
{ type !== 'copy' &&<>
<Row className="mb-btnybase mt-[40px]"><Col ><span className="font-bold mr-[13px]">{$t('转发规则设置')} </span></Col></Row>
<Form.Item<SystemApiProxyFieldType>
className="mb-0 bg-transparent border-none p-0"
name="proxy"
>
<SystemInsideApiProxy serviceId={serviceId!} teamId={teamId!} ref={proxyRef} />
</Form.Item>
</>}
</div>
</Form>
</div>
)
})
export default SystemInsideApiCreate
@@ -1,99 +0,0 @@
import {useEffect, useRef, useState} from "react";
import {BasicResponse, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx";
import {useFetch} from "@common/hooks/http.ts";
import {App, Button, Spin} from "antd";
import ApiBasicInfoDisplay from "@common/components/postcat/api/ApiPreview/components/ApiBasicInfoDisplay";
import ApiPreview from "@common/components/postcat/ApiPreview.tsx";
import ApiMatch from "@common/components/postcat/api/ApiPreview/components/ApiMatch";
import {v4 as uuidv4} from 'uuid'
import ApiProxy from "@common/components/postcat/api/ApiPreview/components/ApiProxy";
import { ProxyHeaderItem, SystemApiDetail, SystemInsideApiDetailProps, SystemInsideApiDocumentHandle } from "../../../const/system/type.ts";
import { MatchItem } from "@common/const/type.ts";
import { DrawerWithFooter } from "@common/components/aoplatform/DrawerWithFooter.tsx";
import SystemInsideApiDocument from "./SystemInsideApiDocument.tsx";
import ScrollableSection from "@common/components/aoplatform/ScrollableSection.tsx";
import WithPermission from "@common/components/aoplatform/WithPermission.tsx";
import { LoadingOutlined } from "@ant-design/icons";
import { $t } from "@common/locales/index.ts";
const SystemInsideApiDetail = (props:SystemInsideApiDetailProps)=>{
const { message } = App.useApp()
const {serviceId, teamId, apiId} = props
const {fetchData} = useFetch()
const [apiDetail, setApiDetail] = useState<SystemApiDetail>()
const [open, setOpen] = useState(false);
const drawerFormRef = useRef<SystemInsideApiDocumentHandle>(null)
const [loading, setLoading] = useState<boolean>(false)
const getApiDetail = ()=>{
setLoading(true)
fetchData<BasicResponse<{api:SystemApiDetail}>>('service/api/detail',{method:'GET',eoParams:{service:serviceId,team:teamId, api:apiId},eoTransformKeys:['create_time','update_time','match_type','upstream_id','opt_type']}).then(response=>{
const {code,data,msg} = response
if(code === STATUS_CODE.SUCCESS){
const newApiDetail = {
...data.api,
match:data.api.match?.map((x:MatchItem)=>{x.id = x.id ?? uuidv4();return x}) || [],
...data.api.proxy && {proxy:{...data.api.proxy,
headers:data.api.proxy?.headers?.map((x:ProxyHeaderItem)=>{x.id = x.id?? uuidv4();return x || []
})}
}
}
setApiDetail(newApiDetail)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
}
}).finally(()=>{setLoading(false)})
}
const onClose = ()=>{
setOpen(false)
}
useEffect(() => {
getApiDetail()
}, []);
return (
<Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} spinning={loading} className="h-full 1" rootClassName="h-full 2" wrapperClassName="h-full 3" >
<div className="pb-btnbase h-full overflow-hidden box-border">
<ScrollableSection>
<div className="content-before pb-[8px] mb-[4px]">
{
apiDetail !== undefined && <>
<div className="flex justify-between">
<ApiBasicInfoDisplay apiName={apiDetail?.name} protocol={apiDetail?.protocol || 'HTTP'} method={apiDetail?.method} uri={apiDetail?.path} />
<WithPermission access="team.service.api.edit"><Button type="primary" onClick={()=>setOpen(true)}>{$t('编辑文档')}</Button></WithPermission>
</div>
<p className="text-[14px] leading-[22px] text-[#999999]">
<span className="mr-[20px]">{$t('创建者')}:{apiDetail?.creator.name || '-'}</span>
<span className="mr-[20px]">{$t('最后编辑人')}:{apiDetail?.updater.name || '-'}</span><span>{$t('更新时间')}:{apiDetail?.updateTime || '-'}</span></p></>
}
</div>
<div className="scroll-area h-[calc(100%-84px)] overflow-auto">
{
apiDetail?.match && apiDetail.match?.length > 0 &&
<ApiMatch title={$t('高级匹配')} rows={apiDetail?.match} />
}
{
apiDetail?.proxy && Object.keys(apiDetail?.proxy).length > 0 &&
<ApiProxy title={$t('转发规则')} proxyInfo={apiDetail?.proxy} />
}
{apiDetail && <ApiPreview entity={{...apiDetail.doc,name:apiDetail.name, method:apiDetail.method,uri:apiDetail.path, protocol:apiDetail.protocol||'HTTP'}} />}
</div>
</ScrollableSection>
<DrawerWithFooter
title={$t("编辑 API")}
open={open}
onClose={onClose}
onSubmit={()=>drawerFormRef.current?.save()?.then((res)=>{res&& getApiDetail();return res})}
showLastStep={true}
>
<SystemInsideApiDocument ref={drawerFormRef} serviceId={serviceId} teamId={teamId} apiId={apiId}/>
</DrawerWithFooter>
</div>
</Spin>)
}
export default SystemInsideApiDetail
@@ -1,251 +0,0 @@
import PageList, { PageProColumns } from "@common/components/aoplatform/PageList.tsx"
import {ActionType} from "@ant-design/pro-components";
import {FC, useEffect, useMemo, useRef, useState} from "react";
import {Link, useParams} from "react-router-dom";
import {useBreadcrumb} from "@common/contexts/BreadcrumbContext.tsx";
import {App, Divider} from "antd";
import {BasicResponse, COLUMNS_TITLE, DELETE_TIPS, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx";
import { SimpleMemberItem} from '@common/const/type.ts'
import {useFetch} from "@common/hooks/http.ts";
import {RouterParams} from "@core/components/aoplatform/RenderRoutes.tsx";
import SystemInsideApiCreate from "./SystemInsideApiCreate.tsx";
import {useSystemContext} from "../../../contexts/SystemContext.tsx";
import { SYSTEM_API_TABLE_COLUMNS } from "../../../const/system/const.tsx";
import { SystemApiSimpleFieldType, SystemApiTableListItem, SystemInsideApiCreateHandle, SystemInsideApiDocumentHandle } from "../../../const/system/type.ts";
import TableBtnWithPermission from "@common/components/aoplatform/TableBtnWithPermission.tsx";
import { useGlobalContext } from "@common/contexts/GlobalStateContext.tsx";
import { checkAccess } from "@common/utils/permission.ts";
import { DrawerWithFooter } from "@common/components/aoplatform/DrawerWithFooter.tsx";
import SystemInsideApiDetail from "./SystemInsideApiDetail.tsx";
import SystemInsideApiDocument from "./SystemInsideApiDocument.tsx";
import { $t } from "@common/locales/index.ts";
const SystemInsideApiList:FC = ()=>{
const [searchWord, setSearchWord] = useState<string>('')
const { setBreadcrumb } = useBreadcrumb()
const { modal,message } = App.useApp()
// const [confirmLoading, setConfirmLoading] = useState(false);
const [init, setInit] = useState<boolean>(true)
const [tableListDataSource, setTableListDataSource] = useState<SystemApiTableListItem[]>([]);
const [tableHttpReload, setTableHttpReload] = useState(true);
const {fetchData} = useFetch()
const pageListRef = useRef<ActionType>(null);
const copyRef = useRef<SystemInsideApiCreateHandle>(null)
const {apiPrefix, prefixForce} = useSystemContext()
const [memberValueEnum, setMemberValueEnum] = useState<SimpleMemberItem[]>([])
const {accessData,state} = useGlobalContext()
const [drawerType,setDrawerType]= useState<'add'|'edit'|'view'|'upstream'|undefined>()
const [open, setOpen] = useState(false);
const drawerEditFormRef = useRef<SystemInsideApiDocumentHandle>(null)
const drawerAddFormRef = useRef<SystemInsideApiCreateHandle>(null)
const {serviceId, teamId} = useParams<RouterParams>()
const [curApi, setCurApi] = useState<SystemApiTableListItem>()
const getApiList = (): Promise<{ data: SystemApiTableListItem[], success: boolean }>=> {
//console.log(sorter, filter)
if(!tableHttpReload){
setTableHttpReload(true)
return Promise.resolve({
data: tableListDataSource,
success: true,
});
}
return fetchData<BasicResponse<{apis:SystemApiTableListItem}>>('service/apis',{method:'GET',eoParams:{service:serviceId,team:teamId, keyword:searchWord},eoTransformKeys:['request_path','create_time','update_time','can_delete']}).then(response=>{
const {code,data,msg} = response
if(code === STATUS_CODE.SUCCESS){
setTableListDataSource(data.apis)
setInit((prev)=>prev ? false : prev)
setTableHttpReload(false)
return {data:data.apis, success: true}
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
return {data:[], success:false}
}
}).catch(() => {
return {data:[], success:false}
})
}
const deleteApi = (entity:SystemApiTableListItem)=>{
return new Promise((resolve, reject)=>{
fetchData<BasicResponse<null>>('service/api',{method:'DELETE',eoParams:{service:serviceId,team:teamId, api:entity!.id}}).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))
})
}
const openModal = async (type:'copy' | 'delete',entity:SystemApiTableListItem) =>{
let title:string = ''
let content:string|React.ReactNode = ''
switch (type){
case 'copy':{
title=$t('复制 API')
message.loading($t(RESPONSE_TIPS.loading))
const {code,data,msg} = await fetchData<BasicResponse<{api:SystemApiSimpleFieldType}>>('service/api/detail/simple',{method:'GET',eoParams:{service:serviceId,team:teamId, api:entity!.id}})
message.destroy()
if(code === STATUS_CODE.SUCCESS){
content=<SystemInsideApiCreate ref={copyRef} type={type} entity={{...data.api, path:(data.api.path?.startsWith('/')? data.api.path.substring(1): data.api.path),serviceId:serviceId}} serviceId={serviceId!} teamId={teamId!} modalApiPrefix={apiPrefix} modalPrefixForce={prefixForce}/>
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
return
}
break;}
case 'delete':
title=$t('删除')
content=$t(DELETE_TIPS.default)
break;
}
modal.confirm({
title,
content,
onOk:()=> {
switch (type){
case 'copy':
return copyRef.current?.copy().then(()=> {
manualReloadTable()
})
case 'delete':
return deleteApi(entity).then((res)=>{if(res === true) manualReloadTable()})
}
},
width:type==='copy'? 900: 600,
okText:$t('确认'),
okButtonProps:{
disabled : !checkAccess( `team.service.api.${type}`, accessData )
},
cancelText:$t('取消'),
closable:true,
icon:<></>,
})
}
const operation:PageProColumns<SystemApiTableListItem>[] =[
{
title: COLUMNS_TITLE.operate,
key: 'option',
btnNums:4,
fixed:'right',
valueType: 'option',
render: (_: React.ReactNode, entity: SystemApiTableListItem) => [
<TableBtnWithPermission access="team.service.api.view" key="view" btnType="view" onClick={()=>{openDrawer('view',entity)}} btnTitle="详情"/>,
<Divider type="vertical" className="mx-0" key="div1" />,
<TableBtnWithPermission access="team.service.api.copy" key="copy" btnType="copy" onClick={()=>{openModal('copy',entity)}} btnTitle="复制"/>,
<Divider type="vertical" className="mx-0" key="div2"/>,
<TableBtnWithPermission access="team.service.api.edit" key="edit" btnType="edit" onClick={()=>{openDrawer('edit',entity)}} btnTitle="编辑"/>,
entity.canDelete && <Divider type="vertical" className="mx-0" key="div3"/>,
entity.canDelete && <TableBtnWithPermission access="team.service.api.delete" key="delete" btnType="delete" onClick={()=>{openModal('delete',entity)}} btnTitle="删除"/>,
],
}
]
const manualReloadTable = () => {
setTableHttpReload(true); // 表格数据需要从后端接口获取
pageListRef.current?.reload()
};
const getMemberList = async ()=>{
setMemberValueEnum([])
const {code,data,msg} = await fetchData<BasicResponse<{ members: SimpleMemberItem[] }>>('simple/member',{method:'GET'})
if(code === STATUS_CODE.SUCCESS){
setMemberValueEnum(data.members)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
}
}
const openDrawer = (type:'add'|'edit'|'view',entity?:SystemApiTableListItem)=>{
setCurApi(entity)
setDrawerType(type)
}
useEffect(()=>{drawerType !== undefined ? setOpen(true):setOpen(false)},[drawerType])
useEffect(() => {
setBreadcrumb([
{
title:<Link to={`/service/list`}>{$t('服务')}</Link>
},
{
title:$t('API')
}
])
getMemberList()
manualReloadTable()
}, [serviceId]);
const onClose = () => {
setDrawerType(undefined);
setCurApi(undefined)
};
const columns = useMemo(()=>{
return [...SYSTEM_API_TABLE_COLUMNS].map(x=>{
if(x.filters &&((x.dataIndex as string[])?.indexOf('creator') !== -1) ){
const tmpValueEnum:{[k:string]:{text:string}} = {}
memberValueEnum?.forEach((x:SimpleMemberItem)=>{
tmpValueEnum[x.name] = {text:x.name}
})
x.valueEnum = tmpValueEnum
}
return {...x,title:typeof x.title === 'string' ? $t(x.title as string) : x.title}})
},[memberValueEnum,state.language])
const handlerSubmit:() => Promise<string | boolean>|undefined= ()=>{
switch(drawerType){
case 'add':{
return drawerAddFormRef.current?.save()?.then((res)=>{res && manualReloadTable();return res})
}
case 'edit':{
return drawerEditFormRef.current?.save()?.then((res)=>{res && manualReloadTable();return res})
}
default:return undefined
}
}
return (
<>
<PageList
id="global_system_api"
ref={pageListRef}
columns = {[...columns,...operation]}
request={()=>getApiList()}
dataSource={tableListDataSource}
addNewBtnTitle={$t('添加 API')}
searchPlaceholder={$t('输入名称、URL 查找 API')}
onAddNewBtnClick={()=>{openDrawer('add')}}
addNewBtnAccess="team.service.api.add"
tableClickAccess="team.service.api.view"
manualReloadTable={manualReloadTable}
onSearchWordChange={(e)=>{setSearchWord(e.target.value)}}
onChange={() => {
setTableHttpReload(false)
}}
onRowClick={(row:SystemApiTableListItem)=>openDrawer('view',row)}
tableClass="mr-PAGE_INSIDE_X "
/>
<DrawerWithFooter
title={drawerType === 'add' ? $t("添加 API"):$t("API 详情")}
open={open}
onClose={onClose}
onSubmit={()=>handlerSubmit()}
showOkBtn={drawerType !== 'view'}
>
{drawerType === 'add' && <SystemInsideApiCreate ref={drawerAddFormRef} modalApiPrefix={apiPrefix} serviceId={serviceId!} teamId={teamId!} modalPrefixForce={prefixForce}/>}
{drawerType === 'edit' && <SystemInsideApiDocument ref={drawerEditFormRef} serviceId={serviceId!} teamId={teamId!} apiId={curApi!.id!}/>}
{drawerType === 'view' && <SystemInsideApiDetail serviceId={serviceId!} teamId={teamId!} apiId={curApi!.id!}/>}
</DrawerWithFooter>
</>
)
}
export default SystemInsideApiList
@@ -253,7 +253,7 @@ const TeamInsideMember:FC = ()=>{
className="w-full"
mode="multiple"
value={entity.roles?.map((x:EntityItem)=>x.id)}
options={roleList?.map((x:{id:string,name:string})=>({label:$t(x.name), value:x.id}))}
options={roleList?.map((x:{id:string,name:string})=>({label:(x.name), value:x.id}))}
onChange={(value)=>{
changeMemberInfo(value,entity ).then((res)=>{
if(res) manualReloadTable()
@@ -35,7 +35,7 @@ const TeamList:FC = ()=>{
const getTeamList = ()=>{
if(!accessInit){
getGlobalAccessData()?.then(()=>{getTeamList()})
getGlobalAccessData()?.then?.(()=>{getTeamList()})
return
}
return fetchData<BasicResponse<{teams:TeamTableListItem}>>(!checkPermission('system.workspace.team.view_all') ? 'teams':'manager/teams',{method:'GET',eoParams:{keyword:searchWord},eoTransformKeys:['create_time','service_num','can_delete']}).then(response=>{
@@ -17,6 +17,7 @@ export type ServiceBasicInfoType = {
logo?:string
invokeAddress:string
approvalType:'auto'|'manual'
serviceType:'ai'|'rest'
}
export type ServiceDetailType = {
@@ -35,7 +35,7 @@ const ServiceHubDetail = ()=>{
const navigate = useNavigate();
const getServiceBasicInfo = ()=>{
fetchData<BasicResponse<{service:ServiceDetailType}>>('catalogue/service',{method:'GET',eoParams:{service:serviceId}, eoTransformKeys:['app_num','api_num','update_time','api_doc','invoke_address','approval_type']}).then(response=>{
fetchData<BasicResponse<{service:ServiceDetailType}>>('catalogue/service',{method:'GET',eoParams:{service:serviceId}, eoTransformKeys:['app_num','api_num','update_time','api_doc','invoke_address','approval_type','service_type']}).then(response=>{
const {code,data,msg} = response
if(code === STATUS_CODE.SUCCESS){
setService(data.service)
@@ -113,7 +113,7 @@ const ServiceHubDetail = ()=>{
{
key: 'api-document',
label: $t('API 文档'),
children: <div className="p-btnbase"><ServiceHubApiDocument service={service!} /></div>,
children: <div className={`p-btnbase ${serviceBasicInfo?.serviceType?.toLocaleLowerCase() === 'ai' ? 'ai-service-api-preview' : ''}`}><ServiceHubApiDocument service={service!} /></div>,
icon: <ApiFilled />
}
]
@@ -78,7 +78,7 @@ export const initialServiceHubListState = {
if(!dataSet.selectedTag || dataSet.selectedTag.length === 0) return false
if((!x.tags || !x.tags.length )&& dataSet.selectedTag.indexOf('empty') === -1) return false
if(x.tags && x.tags.length && !x.tags.some(tag => dataSet.selectedTag.includes(tag.id))) return false;
if( dataSet.keyword && !x.name.includes(dataSet.keyword)) return false
if( dataSet.keyword && !x.name.toLocaleLowerCase().includes(dataSet.keyword.toLocaleLowerCase())) return false
return true
})
}
@@ -33,7 +33,7 @@ export default function ServiceHubManagement() {
const getServiceList = ()=>{
if(!accessInit){
getGlobalAccessData()?.then(()=>{getServiceList()})
getGlobalAccessData()?.then?.(()=>{getServiceList()})
return
}
setServiceLoading(true)
@@ -56,7 +56,7 @@ const getServiceList = ()=>{
const getTeamsList = ()=>{
if(!accessInit){
getGlobalAccessData()?.then(()=>{getTeamsList()})
getGlobalAccessData()?.then?.(()=>{getTeamsList()})
return
}
setPageLoading(true)