mirror of
https://github.com/APIParkLab/APIPark.git
synced 2026-06-14 20:41:15 +08:00
Compare commits
1273 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a22759136e | |||
| b8ebbac2b8 | |||
| 9c4590db07 | |||
| 7ba8a57793 | |||
| 2dc16f4bb8 | |||
| b68496a82a | |||
| 6892fa34d8 | |||
| cdbfd3400c | |||
| c7c3e8033b | |||
| cc524810e8 | |||
| b6f593c8d0 | |||
| ca54fc581c | |||
| 596f6f5668 | |||
| e7372cb7f2 | |||
| feac0428ef | |||
| 7f83f9e37f | |||
| c8e5b7541d | |||
| 638b87950d | |||
| 62b6476420 | |||
| 16b16bae32 | |||
| b872e695b0 | |||
| 8365f77c67 | |||
| 54c88e189e | |||
| 41dae0daf4 | |||
| 596be2cf7f | |||
| ddba01e3bd | |||
| 96f675ae9c | |||
| ae3c189089 | |||
| 0ff5bcd8fa | |||
| 37d421ad8a | |||
| 52a4ed8193 | |||
| 211f11b363 | |||
| 6c637bf78c | |||
| f846836d5d | |||
| 43690156e3 | |||
| 71cc0b8916 | |||
| b703ddaae8 | |||
| 5ae9d5d3f1 | |||
| 061ea05935 | |||
| ca6fdccdb2 | |||
| fb31ecc012 | |||
| c2d7d96f9f | |||
| 741bdd682c | |||
| 70a8da7682 | |||
| 7ccfb69e8d | |||
| 18615bad33 | |||
| d9af85ce86 | |||
| 8f82436421 | |||
| f42a80e56c | |||
| c2651d2e5f | |||
| 44bb977d1a | |||
| 71bbd9ac8f | |||
| 4beb497032 | |||
| 56d4a9b69d | |||
| b6115faf7c | |||
| 4fdb677103 | |||
| 34cf8e2b26 | |||
| 93f686af14 | |||
| 1332770df4 | |||
| 48ab6d7c7e | |||
| 08cecd8a0e | |||
| 42678fce89 | |||
| 79f29254ca | |||
| 30f9d310e5 | |||
| cc34ca510c | |||
| a285c54be2 | |||
| b7119fe248 | |||
| 7b9f7d5acd | |||
| b0d642f5e3 | |||
| 4397784749 | |||
| 1eeba2d648 | |||
| 8cff5b3d40 | |||
| 2302f39d73 | |||
| 114af0c6d2 | |||
| 9069287cd8 | |||
| ca9b127b8b | |||
| 2106836b2c | |||
| cf5cf5b574 | |||
| 34240a2609 | |||
| eed1f41b29 | |||
| 99c6aa19b3 | |||
| 9598254d18 | |||
| da05525cbb | |||
| 0bc89cda8c | |||
| 11a9b0200e | |||
| a38b244a68 | |||
| 636fe2b27c | |||
| 29603388a2 | |||
| b9d3486f5b | |||
| 09cc099dcd | |||
| 4f2c02704f | |||
| 93e0d51872 | |||
| 776a410312 | |||
| 2028f8167f | |||
| 03269e703a | |||
| 9772b8123c | |||
| 31dfe7e295 | |||
| 7c3985fea4 | |||
| 9c18aa7f3b | |||
| bc85e59352 | |||
| 68a294eb71 | |||
| 33c61f6131 | |||
| 5dd055a43f | |||
| 937877fa7a | |||
| 8f994bbea7 | |||
| 6aca6600d7 | |||
| e7aba90808 | |||
| b2fa67c189 | |||
| 271bb32115 | |||
| 7bb0771768 | |||
| 76309c28d2 | |||
| 1b55c5534f | |||
| 98de71c086 | |||
| 9556fcdf5a | |||
| 68764a4403 | |||
| 0084ff260f | |||
| 0c729944b0 | |||
| 22ccd3d59e | |||
| 4c87b5a032 | |||
| 7a5d4c6e9f | |||
| 9c530ec470 | |||
| 358459f37a | |||
| 563b029801 | |||
| 7005cb1e76 | |||
| c79216fd14 | |||
| 2fda023607 | |||
| a52d8f1c1f | |||
| a7fee633b0 | |||
| 4d64be29ac | |||
| 7dedf04e26 | |||
| 9a9b4fc521 | |||
| debf9c820c | |||
| 51ea7e52b8 | |||
| aa1f34f157 | |||
| 670b2d845f | |||
| d8da36d05d | |||
| 0c2d47333f | |||
| 210cd2f93f | |||
| 2c997398b9 | |||
| 57a3936a6d | |||
| fa09e991f4 | |||
| 680f814e6b | |||
| afec1e681c | |||
| cf07a106b5 | |||
| a79b7ec812 | |||
| d8d1cb5665 | |||
| ac4b29d13c | |||
| 5ae7749554 | |||
| a9d5585ef9 | |||
| 224b28bc2c | |||
| 9fc48341f7 | |||
| 8b83b2f7e0 | |||
| 6bb818b9a1 | |||
| 1e8a75e8f7 | |||
| 6392272a2e | |||
| 51191c3eb6 | |||
| 2aa021f74b | |||
| 4ed8f01981 | |||
| 9f2eadbfad | |||
| 084d2f6c09 | |||
| 737bf822d1 | |||
| 00c7c4e205 | |||
| c4670e1cb1 | |||
| 4cfaf64de3 | |||
| 2eb5d30e91 | |||
| 0c6238e380 | |||
| 299ee0fd25 | |||
| 4b610eceba | |||
| 84cf398cc5 | |||
| 470a945bad | |||
| ea28f6024f | |||
| c1ddfbf21c | |||
| 490f9191ff | |||
| dbdd0ac4bf | |||
| a3b740a390 | |||
| cbcd693086 | |||
| 428cfb144e | |||
| e352ddeb99 | |||
| 1e4ae7b4c7 | |||
| cf48d2f3f1 | |||
| d462fda694 | |||
| 5089901491 | |||
| 71d9dfc724 | |||
| 92523eaf30 | |||
| 2be3aecf39 | |||
| d206eafd77 | |||
| e9f259bf6f | |||
| 897f2b5bd2 | |||
| 7b414ce5b5 | |||
| 35714779f9 | |||
| 1d926c2411 | |||
| 753b81d3dd | |||
| 6078322db5 | |||
| 4577aca730 | |||
| a53a527d80 | |||
| 67a6501ee0 | |||
| fce631cc87 | |||
| 2e4287d585 | |||
| 8624576bf8 | |||
| 4d72446c64 | |||
| faf621b646 | |||
| f2acea30e2 | |||
| c6dbe334e9 | |||
| a8c842b8d0 | |||
| 95e34f0b73 | |||
| 69fd1b915b | |||
| 7483455bb8 | |||
| 77e6f100b7 | |||
| 69a3a7d5ba | |||
| 6dae2e7068 | |||
| d3bd782165 | |||
| b49782c7b4 | |||
| 8ca8d51025 | |||
| f8affd2f29 | |||
| 3d4e2c3165 | |||
| 7cca06377c | |||
| 22f15f5cbf | |||
| b8c73b7730 | |||
| fb71498e6a | |||
| bc16d7f5ce | |||
| 62bc87e251 | |||
| b1f010ad60 | |||
| 8e4a3fc42b | |||
| bf08952fe0 | |||
| 51792a8eda | |||
| dd4fdd8de3 | |||
| b9f6abc9b3 | |||
| 4ca4d9fd82 | |||
| 1ed05b96b4 | |||
| e9b7cb505d | |||
| a7f1da85b1 | |||
| 59c0bfe3f5 | |||
| f893819f32 | |||
| 8c40e1cf05 | |||
| afb9cc2e44 | |||
| 8e854adaa3 | |||
| 860874ef5c | |||
| 4b263c1e7a | |||
| eaeae88f04 | |||
| 7a84c5aec3 | |||
| 1d91f9e78c | |||
| 1a8538b617 | |||
| ddd1d662b1 | |||
| c56319d1c2 | |||
| d489912705 | |||
| f95437ad1a | |||
| 04bbac1252 | |||
| f058f2bc71 | |||
| aa873f75f2 | |||
| 70de481b52 | |||
| b41e8bbb8c | |||
| 17f700c77d | |||
| 89b4a51774 | |||
| 92627b0a69 | |||
| 8e974a4fe3 | |||
| 60a151f798 | |||
| 6839539321 | |||
| 0dc5685aa9 | |||
| 35f8a1e539 | |||
| 3adbe4b74f | |||
| 179b9e16d6 | |||
| b573b380ab | |||
| fb9c92e941 | |||
| 2ae6af46ef | |||
| 224411b593 | |||
| e366f3545f | |||
| 3721eea772 | |||
| e16e06d8b6 | |||
| a60bd5eba7 | |||
| 8dca13f4e5 | |||
| d0464b988a | |||
| 60662c443d | |||
| 2420cfdd9a | |||
| 2aad0959c4 | |||
| 9a145cb0b0 | |||
| 690c2fe2f7 | |||
| 9a833023ce | |||
| 62456f632e | |||
| 4128de90fd | |||
| 0eb24ef7b1 | |||
| 1c96e82629 | |||
| 7af43de521 | |||
| 502af9c782 | |||
| 1897fe9467 | |||
| 1da4c35cdc | |||
| 9de48bf400 | |||
| ffb3f51fb6 | |||
| 7e2852b7e6 | |||
| 184db4c933 | |||
| 6beb2576ce | |||
| 7f3e43d929 | |||
| 4dcb71042c | |||
| 30c09006b0 | |||
| fbd9e1d979 | |||
| d143aee8c3 | |||
| aaadc5e450 | |||
| 861c389277 | |||
| c3b41b047d | |||
| 1499036792 | |||
| 903f9a08a2 | |||
| 1228bdd756 | |||
| 9e35325600 | |||
| 0ec9492838 | |||
| fc7fe48920 | |||
| e3d272404d | |||
| 9e4efae3a5 | |||
| df16946d37 | |||
| 61f4742d19 | |||
| cad081f1bc | |||
| bc8870a735 | |||
| 713227f0fe | |||
| 661aa92732 | |||
| f8f15c51b1 | |||
| 32805d9bf8 | |||
| 7767284c98 | |||
| 9efb9600c9 | |||
| dcb3f2879a | |||
| 6d12f251a1 | |||
| 8ae66e9a22 | |||
| db74c102f9 | |||
| d1eea10fe6 | |||
| 1b4009e4a1 | |||
| 983a5423c9 | |||
| 99934ec16c | |||
| 7d59a6074f | |||
| 70972cc92e | |||
| c81f6d659d | |||
| 6097b0e946 | |||
| 7c1865f36f | |||
| 3a0c165cec | |||
| e8a8d4ec19 | |||
| cd0982ce20 | |||
| b4aeb435c6 | |||
| c51f9f1dcb | |||
| 94d5ed0773 | |||
| baf1369f58 | |||
| 35b25289bd | |||
| e336b8324a | |||
| d0e6162386 | |||
| c5e4316e37 | |||
| b52c12a922 | |||
| 1127df66f7 | |||
| 4b98ceb2d4 | |||
| 5286b90b27 | |||
| 9ffa194d6d | |||
| b9c6bac15b | |||
| 1aa18177c9 | |||
| 068d852c20 | |||
| 5caf9560cb | |||
| e1194ff391 | |||
| 6fb3bece43 | |||
| 90ae7b0741 | |||
| 8a13c7b312 | |||
| 519673ec01 | |||
| b4a22b29f3 | |||
| 2bbddc81b1 | |||
| e0171c45e1 | |||
| 3427d8fd07 | |||
| 2b3bfdec99 | |||
| 2cca6d4bba | |||
| c776dc2206 | |||
| 34c971ad77 | |||
| 91b2932d62 | |||
| 3127cc6780 | |||
| 0dc5439726 | |||
| f7e3db9043 | |||
| 7a635430f1 | |||
| 0bb11bcca5 | |||
| 412d15a9b4 | |||
| 10eda384e6 | |||
| 7866572191 | |||
| 4b5cbb3fcc | |||
| 614b46e6fc | |||
| f7ad32c1cc | |||
| 2afb106ba9 | |||
| fce2cc8636 | |||
| ba4362ae64 | |||
| 8545938654 | |||
| 4d87b3aafb | |||
| 5784d3f5d1 | |||
| fb1b5280fe | |||
| 253e655b31 | |||
| 13eb5bb1e8 | |||
| 72dcd0a073 | |||
| 1971db2405 | |||
| 3093471063 | |||
| 14874d7869 | |||
| bfdec9e08b | |||
| cbec924300 | |||
| a03f87b907 | |||
| e100f475ae | |||
| 76e66bcf6f | |||
| 26a50b9a79 | |||
| 4e38760b44 | |||
| aef9805bdf | |||
| bb7ccde958 | |||
| 55aef95304 | |||
| aa338f7aa7 | |||
| ee06368a4e | |||
| edfb2006b2 | |||
| 015ecfd182 | |||
| 99b65bead6 | |||
| 98efa0f804 | |||
| 2ff7458c4e | |||
| 60fa35376b | |||
| 2fbaba710d | |||
| 222eafe86d | |||
| 86bb513db6 | |||
| 88d4c5101e | |||
| df4126514f | |||
| 471643d4ea | |||
| 444a48c8f2 | |||
| ec951dd87f | |||
| 7ac385b317 | |||
| 2f435d561e | |||
| 3a718d2cd8 | |||
| d87a420752 | |||
| 6491de3064 | |||
| 5cc01d7aab | |||
| 628cd98fd0 | |||
| 8ce65cbe3d | |||
| 0b2928eb3c | |||
| 45ce43ab15 | |||
| 637044e99d | |||
| 3a96e02fc7 | |||
| 18a3283cb4 | |||
| c3b8ba4d04 | |||
| a51ccf6d67 | |||
| 6b1224f9f8 | |||
| 907f9c00fb | |||
| ed72062d33 | |||
| af9f5cef2f | |||
| 2c205921d6 | |||
| 4e2b424eeb | |||
| 22e738a164 | |||
| 6824cdd4a1 | |||
| d44dd05462 | |||
| 7d6666c0b6 | |||
| 070882655d | |||
| cdb0831174 | |||
| 0a864aea41 | |||
| a4464610eb | |||
| 18a03d57b4 | |||
| 49bbc0754a | |||
| f21b061443 | |||
| 34446fae37 | |||
| d1aeb621d4 | |||
| 2f81e087e7 | |||
| fe96afc8c3 | |||
| 9236993c22 | |||
| ad2d62d13b | |||
| 8f5833ca15 | |||
| 1adf8c890f | |||
| 416c2a56fb | |||
| 1140e43d67 | |||
| ab4faa36da | |||
| 38ca9815ba | |||
| 125965dabd | |||
| ce4369976c | |||
| dc340fc766 | |||
| c28e3ec5c2 | |||
| 6fe7cf18bd | |||
| 5af9577d31 | |||
| c03c37420d | |||
| b818c0feac | |||
| bcb3003784 | |||
| ecd033ffd9 | |||
| abadeef7d2 | |||
| 427212ed13 | |||
| 73435497a2 | |||
| 067cb0f21d | |||
| a087a535d2 | |||
| 28745e81f8 | |||
| 29c74bbbe1 | |||
| 35a7b13cde | |||
| 62b77cc9fa | |||
| 782314d5bd | |||
| 33b5e02f23 | |||
| ec518c2ece | |||
| b7b2dff26b | |||
| 456ffb8586 | |||
| 1d09965d9a | |||
| 82cff8f66e | |||
| b97acc7862 | |||
| 0adc963129 | |||
| 4be1387c62 | |||
| f4e17f0de7 | |||
| a03e1aa1f8 | |||
| 7fcfbb2a54 | |||
| 59a4761424 | |||
| 6ceae331ce | |||
| 2a1f0175a9 | |||
| e5fca5f545 | |||
| 3f8ebc4a19 | |||
| ff99f55b0a | |||
| d049658cdc | |||
| e8103b0dbe | |||
| 537ea1fd83 | |||
| fd66b2b351 | |||
| ab6ffb74b7 | |||
| 20fb2b3083 | |||
| e9877bb52b | |||
| 7debf34fb0 | |||
| 41afaf4f66 | |||
| ee19c214e7 | |||
| 534ca2b042 | |||
| ac5cbd2d22 | |||
| 17372cd0a4 | |||
| 0bbeb481f4 | |||
| 61c9709974 | |||
| d0364e3734 | |||
| 5b5852e336 | |||
| 883aeebc8f | |||
| c16e0edaf0 | |||
| 98bb6eaf2a | |||
| 36d8b96b61 | |||
| 3bb3d6a6cd | |||
| 9971a86d5c | |||
| 71ab2c8dea | |||
| 80561c8784 | |||
| 4be0b1b3b6 | |||
| 48a32e8f6d | |||
| 9e6fe50aed | |||
| fa4edcba93 | |||
| b863e3892e | |||
| 0a57a7b7ee | |||
| e249b62d13 | |||
| 5c015f275b | |||
| 6faddc15e9 | |||
| 894cd21032 | |||
| 7a1b56e202 | |||
| 19911eb8c8 | |||
| a307917b66 | |||
| cb5f45acc7 | |||
| 0dbf9c907d | |||
| 9fc8234eaf | |||
| 61840de99f | |||
| 30da6e10a1 | |||
| 4650edbd17 | |||
| 7646e3d9a1 | |||
| c5198b198a | |||
| 87b70a3faf | |||
| 1ef0c7bd14 | |||
| 02ee5b8c7e | |||
| 5a2b509d68 | |||
| 11cbcd45bb | |||
| e82d5be71d | |||
| 065d5ade3f | |||
| 464015bed6 | |||
| 190323cf05 | |||
| 94654c753b | |||
| 8a589982d1 | |||
| bd9325c66d | |||
| 1bdd4720bb | |||
| 55aec4cc8b | |||
| 5ec294207d | |||
| 3b45a339d3 | |||
| 0742c2be33 | |||
| e8730d6d18 | |||
| 2bc3b72e15 | |||
| 56124fd90d | |||
| 81ccba83c3 | |||
| 5f64a3ae33 | |||
| a80b1aed08 | |||
| 3724045e37 | |||
| a368af0598 | |||
| 8128a7acac | |||
| 51910e6740 | |||
| 8a2be8195a | |||
| ec2c418c4d | |||
| b89b9e4ebd | |||
| de008edc68 | |||
| 129c7a15c7 | |||
| 29325aa341 | |||
| a864e67a6c | |||
| 190aff51b6 | |||
| ab398032df | |||
| 8f1d127e96 | |||
| 01ea108ab9 | |||
| 2b10162d5e | |||
| 4b1e1a22db | |||
| f500adb158 | |||
| 5ec7e680e6 | |||
| 7277389c9a | |||
| 4c7a2737d0 | |||
| cd4594f3ae | |||
| 4733b3c919 | |||
| aa78945ed8 | |||
| 7ac8beb161 | |||
| 0f30d85ae8 | |||
| f8a976290c | |||
| cd73e76712 | |||
| 4616470493 | |||
| ac6afad280 | |||
| dde76789a1 | |||
| b8c4482dad | |||
| fb90189f3c | |||
| ac56f9f7a6 | |||
| 9fd9979ce3 | |||
| a1ae7ca119 | |||
| 08f29b7067 | |||
| 503367b8e2 | |||
| 36aac1d8c8 | |||
| 42229fe6bc | |||
| 0ae824ec35 | |||
| 0a1d8245ee | |||
| 95c2288890 | |||
| 7ac97ca739 | |||
| 14548e4bc5 | |||
| 72c5e7c35a | |||
| 4fbc4fccdc | |||
| a4de291155 | |||
| 67ea0a5c24 | |||
| d5eeab16b6 | |||
| 0be2248f41 | |||
| 7b7f968ae2 | |||
| ca1e1abeb8 | |||
| 00451ae78f | |||
| 04e9fc7315 | |||
| 6dbef3f743 | |||
| 42cc086bd0 | |||
| 4c40b04c8e | |||
| 6126ba8678 | |||
| 51cc7692cb | |||
| 414f8c64c0 | |||
| 60a735b726 | |||
| d6f6642271 | |||
| be3880a05d | |||
| f40f1a4304 | |||
| d8bb1d649c | |||
| 1bf43cf137 | |||
| 86fa87bb7a | |||
| 5c89b5a7a2 | |||
| 9a8b4cacff | |||
| da6a54aa74 | |||
| 478d65a649 | |||
| abd391266a | |||
| f282e0e69b | |||
| f283cbbcca | |||
| 89a392b749 | |||
| 2a3e016dc2 | |||
| 5d56177bc7 | |||
| c043dbf8e5 | |||
| af0ec4e3da | |||
| 618bc05a8b | |||
| b86f0a874f | |||
| c473c72b84 | |||
| 87405dc9ea | |||
| b01993fe28 | |||
| 24c7cd0c8d | |||
| 2739f84fea | |||
| 98304b8816 | |||
| 9a29cef69a | |||
| ff9738f5cc | |||
| a657908c20 | |||
| f59128f25d | |||
| c08a617c65 | |||
| 5017fce70c | |||
| 304cd9ddfc | |||
| e7eec6e160 | |||
| 3dd44c97e9 | |||
| bc00f2d577 | |||
| d80bfe61df | |||
| de3ac01b98 | |||
| 5c488fe4ea | |||
| 1d7874fad6 | |||
| c4f337f2ae | |||
| 09d70866e3 | |||
| 5052720efb | |||
| 73f53a913c | |||
| 13ce234ac8 | |||
| a38ee5e16c | |||
| 0a30ea157b | |||
| 658f0cbea5 | |||
| 51378e922f | |||
| 295aee494e | |||
| 72350f2fae | |||
| 49bbadd721 | |||
| 220e062d1e | |||
| b0ac98a5a9 | |||
| 84decf1310 | |||
| 7d66f2628a | |||
| 285e0c7a81 | |||
| e0153d6c11 | |||
| c241a0ba8f | |||
| ca7c7efc9c | |||
| 6a29b9492c | |||
| 599040d64a | |||
| ec6c2303be | |||
| f4419d2357 | |||
| c75df95cce | |||
| b2e8c75572 | |||
| 6bf3737c24 | |||
| 075c1f234a | |||
| fd21273251 | |||
| bb822801fb | |||
| 8d1fc7faa5 | |||
| 94728e7b6a | |||
| e031f80b2e | |||
| 6111dc69ad | |||
| 49188b291b | |||
| ba8632aa22 | |||
| 726bbe4f6f | |||
| f1e1d94380 | |||
| 607970a1ed | |||
| aef65d9d05 | |||
| 4d26fe1edd | |||
| e0bc987c25 | |||
| a07c5c8d9d | |||
| 777256bf30 | |||
| 60de3b3250 | |||
| 074919b70c | |||
| f90e9b4eba | |||
| 6789b1c327 | |||
| f46fe0ec4c | |||
| f88aea2d32 | |||
| 29f534501c | |||
| 7b18e06006 | |||
| 50567665ab | |||
| 2135205f40 | |||
| 51241108fa | |||
| 05c5b2968d | |||
| 00e6d1aa7a | |||
| eb4fb872a3 | |||
| 9e7825aee8 | |||
| f8b75c7237 | |||
| 843b2f32e0 | |||
| 4db7154130 | |||
| c7190a0d07 | |||
| 8bd273c48a | |||
| 9f0e9a5525 | |||
| 1486c56249 | |||
| 53d895f046 | |||
| d6e0ea049a | |||
| 58f192b6e9 | |||
| dba4bae830 | |||
| 8df6362ac0 | |||
| 2a84bdcf9f | |||
| 158f94f069 | |||
| 3fde0fa7a3 | |||
| 1ac9a0e154 | |||
| 8c816edab4 | |||
| 858b8a25bb | |||
| 9dfac9c5f5 | |||
| 0a828e843d | |||
| 16414f6a4a | |||
| 4e902e891e | |||
| 5cff8b3bed | |||
| b5671d14a4 | |||
| be202fd53b | |||
| 601663b658 | |||
| cb63931f30 | |||
| 651a1de601 | |||
| 841adc05cd | |||
| e78f1b804f | |||
| 8d94a7e872 | |||
| ce53bb2d47 | |||
| 09a74768d5 | |||
| e4f89f7084 | |||
| a6df9e1f04 | |||
| 0e885d21f9 | |||
| 387eccb44a | |||
| 7086073227 | |||
| 9006a9719e | |||
| 9e554ca0af | |||
| 6899084f06 | |||
| 4396902982 | |||
| 73dc9c3439 | |||
| 3c59985734 | |||
| 13a23a9753 | |||
| 3c543d686a | |||
| 2bd0bb68a1 | |||
| 9d9dbd0048 | |||
| a1dcdd49fb | |||
| 880e97a922 | |||
| 9ba1537f18 | |||
| c0d167a2b0 | |||
| 3931f9c679 | |||
| 3e4d2b069a | |||
| d679f69386 | |||
| 193276a0b2 | |||
| 1e308eea98 | |||
| cc9ffdd182 | |||
| 1e4a8afc11 | |||
| b6dee4671a | |||
| 98756d4c7f | |||
| 16c6535baf | |||
| fe2c6dafcd | |||
| 1c531fb659 | |||
| 2ece064216 | |||
| fe74e18ea3 | |||
| fa714f7176 | |||
| 7a057bda13 | |||
| 7348828cc3 | |||
| 74ef7b58fb | |||
| 3fc784ec9d | |||
| 52292aefed | |||
| ba0088ed73 | |||
| 452e64c919 | |||
| ad7df9a6ef | |||
| abb55346f9 | |||
| b16ed52378 | |||
| be8d347b0c | |||
| 0146794b74 | |||
| 996319a9cb | |||
| a68b978044 | |||
| 556f8f1db0 | |||
| bbee4b42ae | |||
| 935227f104 | |||
| 9e540a2128 | |||
| 3c7dbd6b0b | |||
| 4809299f86 | |||
| 41ad5f5109 | |||
| 104bd61576 | |||
| e833233982 | |||
| 8f83dc5fd4 | |||
| 9f9d45e59c | |||
| bb27293aba | |||
| 445b1277e9 | |||
| 3016f563bf | |||
| d1b9c29f0f | |||
| 255c03a4a3 | |||
| b49c377b5f | |||
| 45fcda443c | |||
| 73daecf9b7 | |||
| e6f5e06ca5 | |||
| 9677d1dba9 | |||
| 134004e787 | |||
| 6ea6d5e220 | |||
| 003758e8cc | |||
| 3054a4ee49 | |||
| 21aeadcc56 | |||
| d5b4374c0d | |||
| 39f6e449ed | |||
| a87cbedd52 | |||
| 90649ce00b | |||
| de3eb7f259 | |||
| 5298475de2 | |||
| 5f4c573282 | |||
| 16084dabfe | |||
| 7c46243641 | |||
| 96f1913592 | |||
| b92cad8d91 | |||
| f968dbe926 | |||
| c693a78cc1 | |||
| 4aa1b6070b | |||
| 08724b1d53 | |||
| 6c39c8c73b | |||
| c3d35ac282 | |||
| 37c6a562cd | |||
| 98ab569665 | |||
| 318d4194c3 | |||
| 4b50b27e14 | |||
| af02d1727c | |||
| d2a39999be | |||
| 3207a06e3f | |||
| 98457883cc | |||
| 8e4ce11053 | |||
| 6ae5b4992f | |||
| 3c624d5039 | |||
| ddd2efe441 | |||
| 7e419cb2de | |||
| 0093c7cfb7 | |||
| 65c45d4c28 | |||
| 98fc9ea703 | |||
| dbeb08e5e1 | |||
| a48523c94b | |||
| 1335fffdaa | |||
| 78f96dbcf9 | |||
| 86afeea532 | |||
| 66559785ea | |||
| f016fc4d06 | |||
| e0bddbc84a | |||
| 3639dc93ef | |||
| e1da4bd1f0 | |||
| 99ef9118ee | |||
| 68b1c0eabb | |||
| d73ca273e0 | |||
| 9badf0e3e3 | |||
| c720edcf41 | |||
| 483c6a2758 | |||
| 6168f4d758 | |||
| 687730be7b | |||
| cddb2bc2cf | |||
| fa7c9b4c47 | |||
| 1fd429f582 | |||
| 6d61c036f9 | |||
| f82f4b0d7b | |||
| fff39caa71 | |||
| 5968d64e3d | |||
| 202c53375b | |||
| 9eea853724 | |||
| 9526fa294a | |||
| e025fa2995 | |||
| 5c15af2b9c | |||
| 3e094c7bf4 | |||
| 45a4b2b97d | |||
| 1d62a6613d | |||
| 28f182a291 | |||
| f78ba3ad1e | |||
| e2ff036965 | |||
| 7c7071a3a9 | |||
| e90809c010 | |||
| 2426fcd7d6 | |||
| f433c28e75 | |||
| cac8755305 | |||
| a283dc0c62 | |||
| 99002fa1cf | |||
| 9060c87d60 | |||
| 1d275e3313 | |||
| 77da2a7fd2 | |||
| b9cc53b694 | |||
| 62d47077b1 | |||
| 3269e79ff3 | |||
| 307faed57b | |||
| dc6a9a53f0 | |||
| 285119852b | |||
| 65fa6517d3 | |||
| 2c27074b04 | |||
| 66e4be0b57 | |||
| 14d94e33bb | |||
| 541f50aecc | |||
| 95d87f7183 | |||
| 6738960e46 | |||
| ab60eaac95 | |||
| 76f009c546 | |||
| b9c61f8b94 | |||
| 125be3f2bf | |||
| 9df44eeb87 | |||
| 97aa6ee0d6 | |||
| 80c9bca75b | |||
| 9e253086ef | |||
| 6aeb5299dc | |||
| 888eabe154 | |||
| 29d5c1bdd7 | |||
| eee33cda04 | |||
| df4f65d36d | |||
| 8e9335454e | |||
| 4e4b0f2f51 | |||
| c28d6c85af | |||
| 3cdbac76a5 | |||
| 20bcc662c8 | |||
| 908accd290 | |||
| a621c5fc72 | |||
| 92f0ac1578 | |||
| 8b3667ec9b | |||
| 875fa0936b | |||
| 669844f055 | |||
| 9785b930da | |||
| f3195d257e | |||
| 349162852d | |||
| 69f330737f | |||
| 7d10fbaa25 | |||
| 9d964b572b | |||
| a0978269f8 | |||
| 9a6196bbd7 | |||
| 812f589762 | |||
| b9e676d926 | |||
| 49e6e54c31 | |||
| 4d398fd4d5 | |||
| b74f4adfd8 | |||
| be5abccd81 | |||
| 1792489cd0 | |||
| 4284fd8fc6 | |||
| 036106db82 | |||
| c6ed032659 | |||
| b521204c99 | |||
| b38502e0bd | |||
| 2fe03bd4b3 | |||
| 68a0f616dc | |||
| 1edce4295e | |||
| ca386c343e | |||
| efb0b6526a | |||
| 4a406ad261 | |||
| fb701cc20d | |||
| 0b0478c9ba | |||
| 79ff630c41 | |||
| 4679440520 | |||
| b38f77acea | |||
| 896330d0ec | |||
| 205803a800 | |||
| e6187f099a | |||
| c148a5c797 | |||
| 3d730547af | |||
| c3f1b12b7e | |||
| 9f9a347272 | |||
| d63869ca15 | |||
| d33a243020 | |||
| 558e8fad61 | |||
| b0f094a9d9 | |||
| 84e1528a1d | |||
| 2a1372926f | |||
| 73b2fea954 | |||
| 8263f1ec85 | |||
| 436ab32937 | |||
| cb904e5386 | |||
| 486bf0edcd | |||
| 06e5f2878f | |||
| 5d1a7a7ca4 | |||
| 197fe3c15b | |||
| 09ac4cf970 | |||
| f64a20fbac | |||
| 86b566a8cd | |||
| 2e7ecc93c8 | |||
| c7f79d4b7b | |||
| c452f092cc | |||
| 0764b4bfd4 | |||
| cd9552c9e0 | |||
| 508bfb7891 | |||
| 998b5af4ae | |||
| 5b1eac58e4 | |||
| 9e0d25b77e | |||
| 62488211f7 | |||
| 0b4b183ba7 | |||
| f3532f6128 | |||
| a5e792d3ba | |||
| 19fc1b6958 | |||
| 2702b71cc6 | |||
| 97b3ef0f03 | |||
| 3431805088 | |||
| 306da2fcc5 | |||
| a7f2a91e6a | |||
| dcd85d0e9a | |||
| 82a3476f62 | |||
| cf32ae9a00 | |||
| 561c001b08 | |||
| 26ff021887 | |||
| eb461b5bfa | |||
| 42114a7f0d | |||
| 10b08ce42c | |||
| c0934136d0 | |||
| fb77d33cb7 | |||
| 7ebbd8c0bf | |||
| 6c16251876 | |||
| dc18ce41f5 | |||
| 2669e381b1 | |||
| 3deb9cbb35 | |||
| eeaef841c8 | |||
| 7f52cc95ec | |||
| 71e9c464b9 | |||
| 84fd74de4a | |||
| f0661e6d19 | |||
| 6f03c8f969 | |||
| d38cff4def | |||
| e45468cf3c | |||
| ed843b7285 | |||
| 117dd82617 | |||
| b043db8237 | |||
| ee703df2f3 | |||
| 8b7cf85f23 | |||
| c44bd22108 | |||
| 5c245c70f7 | |||
| debdd18a58 | |||
| 29504e8d62 | |||
| a0edee3c45 | |||
| 7f457d9ae8 | |||
| dd5ac2d87b | |||
| 76172d49c3 | |||
| 7b6fc8e03e | |||
| 0be7620209 | |||
| 3b6a38dd90 | |||
| 0b001913de | |||
| b293b5b30e | |||
| 9f7f6ab157 | |||
| b5c88a59bf | |||
| 040c10596e | |||
| 1f1c7c0668 | |||
| ad1561f090 | |||
| 83306364ce | |||
| 8ba0d49fb3 | |||
| e1c7c16d91 | |||
| 01cca463bb | |||
| 44aa842f20 | |||
| 2d94bbaef7 | |||
| b2bc656e15 | |||
| 21b2f554df | |||
| cd4795d50a | |||
| 583141a374 | |||
| 816cc11a19 | |||
| d27729d110 | |||
| f4a37539a0 | |||
| cec2100915 | |||
| cab4629c4b | |||
| 6f3c389e22 | |||
| 729e2882b5 | |||
| b786ee4a36 | |||
| 7569ca1fd4 | |||
| 93925c1bfb | |||
| 658179d9c3 | |||
| bc74168e36 | |||
| 22afea72f5 | |||
| a2d42af747 | |||
| b04d06da4c | |||
| f1052ae2f3 | |||
| c660aadf90 | |||
| 0fd8abe0ff | |||
| fcb3183e71 | |||
| 3f2ed774ad | |||
| 6c90612fd2 | |||
| ffb10a588e | |||
| 43069c396d | |||
| 974c5cf8a2 | |||
| c434d7c954 | |||
| 518846a281 | |||
| ec316f589b | |||
| 92ea8e4308 | |||
| 0a6fc1e020 | |||
| d06cf41581 | |||
| c94c746120 | |||
| 324c8a2b98 | |||
| 0a2b191711 | |||
| 334db692b2 | |||
| 458176a0af | |||
| 954cf5af1f | |||
| d46158b381 | |||
| f883525c27 | |||
| 453412ad4d | |||
| 5f80f8e3af | |||
| 61c308a0e3 | |||
| 96a7394aaf | |||
| 429e153cd4 | |||
| d856554aa4 | |||
| 227552f54b | |||
| 32ed7faff2 | |||
| 35481c1f59 | |||
| f6510bd667 | |||
| b51eb92256 | |||
| 2b48c779ef | |||
| 9783f77110 | |||
| e764fd07b9 | |||
| 5aa8e88381 | |||
| 2775aa023b | |||
| 096adebb48 | |||
| 1ba08ec630 | |||
| d72337b770 | |||
| c053aa6299 | |||
| 5d70b98cd4 | |||
| ea0812da97 | |||
| a771eda369 | |||
| be40015526 | |||
| c56f6ff1fb | |||
| 6dab6c327c | |||
| 4f4e4e7b1d | |||
| 259a1b9566 | |||
| 43533b5f3a | |||
| 51f7628564 | |||
| 2aa2591f0f | |||
| 6301b8f2fe | |||
| dbc29b86d8 | |||
| 5e1e088edd | |||
| 1c74f3d2ce | |||
| 583918d617 | |||
| a15e1fbca0 | |||
| 8699a7e97c | |||
| a3869c3be0 | |||
| f777103030 | |||
| 09eabbf773 | |||
| 64932c97a4 | |||
| 00e3710d66 | |||
| bd99164416 | |||
| e37cee3be7 | |||
| 7c33abdbe6 | |||
| e6bb9a972b | |||
| e40773e436 | |||
| 3c4c8b8fe6 | |||
| 5aaa32b788 | |||
| e6f53ce2e7 | |||
| f18d7c484b | |||
| fefc0fec76 | |||
| 2949c65c79 | |||
| 19dcc1a914 | |||
| 64cdbb37f5 | |||
| c31f8865c5 | |||
| 9698ba0f5d | |||
| 836f15f5d7 | |||
| 0d863b781f | |||
| c016524079 | |||
| d85773c2da | |||
| 00c123fd1e | |||
| 48a8811026 | |||
| ab01f59eae | |||
| 2bdd24180c | |||
| 126d1e4c58 | |||
| 582072e174 | |||
| fbbea88502 | |||
| 100841d6f7 | |||
| cc096a548f | |||
| f6c9680551 | |||
| 1f7951bf9f | |||
| a96e465b13 | |||
| bf6c955cd0 | |||
| 854578d300 | |||
| beb0a2007e | |||
| e0ab35920e | |||
| 75cff407bf | |||
| 297b20857c | |||
| 04ccc272e1 | |||
| 22f6267a50 | |||
| 8fed0c2b1f | |||
| 154527947d | |||
| a8b29b388b | |||
| df845a0db4 | |||
| 43b769e3e8 | |||
| ea58b708f8 | |||
| 917786f8b8 | |||
| 8402209afd | |||
| 5c0b661189 | |||
| 47f7d78258 | |||
| c50becc085 | |||
| 0771b831e7 | |||
| b47e1c22c5 | |||
| 166d223577 | |||
| 310f983075 | |||
| ac0f472ac6 | |||
| 977163588f | |||
| da0b6e557e | |||
| 411a74b2aa | |||
| 0380353ff4 | |||
| c0590ddc0f | |||
| 020aba1c90 | |||
| d46c5a10fc | |||
| 1e24758b21 | |||
| 308b41a1ec | |||
| e07e6b0f0f | |||
| 35ec6677ba | |||
| 2110788f43 | |||
| d61e250e0b | |||
| 3d3c0377ff | |||
| 1c8135388f | |||
| e47ccfc58d | |||
| 768cc5e10b | |||
| 2be5cf5ee8 | |||
| 0ac1e42658 | |||
| 131851e650 | |||
| 56bb0b26ca | |||
| 3d8a1eb8ae | |||
| a930dda037 | |||
| 97154f97f1 | |||
| 626077d625 | |||
| f45b83e947 | |||
| a2ea577fd8 | |||
| 3a69a179bc | |||
| 703d33b69c | |||
| ef5b68cb9b | |||
| bf6017c60c | |||
| 097a588fc1 | |||
| cac2cb2bac | |||
| ef571ba096 | |||
| 419702a1ee | |||
| 2c249f5167 | |||
| b90660f46c | |||
| 578999e705 | |||
| e2d6e9d812 | |||
| 250937bd51 | |||
| d85532ca85 | |||
| 862c762eaa | |||
| 8e6c4ec683 | |||
| d82fe39a17 | |||
| ca43a5ff43 | |||
| 449254581f | |||
| 7a8557590c | |||
| be6ecaa380 | |||
| 54a29b39bf | |||
| a000ce2ec5 | |||
| 44436ab29d | |||
| 4863b8c9a3 | |||
| 1662faafd2 | |||
| 693b76ee18 | |||
| 7da4235fc8 | |||
| 66b5315a13 | |||
| 5dd04479c2 | |||
| 469a612e41 | |||
| 7c8d175c9c |
+2
-1
@@ -7,4 +7,5 @@
|
||||
/.vscode/
|
||||
.air.toml
|
||||
/tmp/
|
||||
/work
|
||||
/work
|
||||
/cmd/
|
||||
|
||||
@@ -3,6 +3,7 @@ package catalogue
|
||||
import (
|
||||
"github.com/APIParkLab/APIPark/module/catalogue"
|
||||
catalogue_dto "github.com/APIParkLab/APIPark/module/catalogue/dto"
|
||||
"github.com/APIParkLab/APIPark/module/service"
|
||||
"github.com/APIParkLab/APIPark/module/tag"
|
||||
tag_dto "github.com/APIParkLab/APIPark/module/tag/dto"
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -14,6 +15,7 @@ var (
|
||||
|
||||
type imlCatalogueController struct {
|
||||
catalogueModule catalogue.ICatalogueModule `autowired:""`
|
||||
appModule service.IAppModule `autowired:""`
|
||||
tagModule tag.ITagModule `autowired:""`
|
||||
}
|
||||
|
||||
@@ -26,7 +28,17 @@ func (i *imlCatalogueController) Subscribe(ctx *gin.Context, subscribeInfo *cata
|
||||
}
|
||||
|
||||
func (i *imlCatalogueController) ServiceDetail(ctx *gin.Context, sid string) (*catalogue_dto.ServiceDetail, error) {
|
||||
return i.catalogueModule.ServiceDetail(ctx, sid)
|
||||
detail, err := i.catalogueModule.ServiceDetail(ctx, sid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, canSubscribe, err := i.appModule.SearchCanSubscribe(ctx, sid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
detail.CanSubscribe = canSubscribe
|
||||
return detail, nil
|
||||
|
||||
}
|
||||
|
||||
func (i *imlCatalogueController) Search(ctx *gin.Context, keyword string) ([]*catalogue_dto.Item, []*tag_dto.Item, error) {
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
application_authorization "github.com/APIParkLab/APIPark/module/application-authorization"
|
||||
|
||||
mcp_server "github.com/APIParkLab/APIPark/mcp-server"
|
||||
"github.com/APIParkLab/APIPark/module/mcp"
|
||||
"github.com/APIParkLab/APIPark/module/system"
|
||||
"github.com/eolinker/go-common/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
mcp2 "github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
|
||||
var _ IMcpController = (*imlMcpController)(nil)
|
||||
|
||||
type imlMcpController struct {
|
||||
settingModule system.ISettingModule `autowired:""`
|
||||
authorizationModule application_authorization.IAuthorizationModule `autowired:""`
|
||||
mcpModule mcp.IMcpModule `autowired:""`
|
||||
sessionKeys sync.Map
|
||||
server map[string]http.Handler
|
||||
openServer http.Handler
|
||||
}
|
||||
|
||||
var mcpDefaultConfig = `{
|
||||
"mcpServers": {
|
||||
"%s": {
|
||||
"url": "%s"
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
func (i *imlMcpController) GlobalMCPConfig(ctx *gin.Context) (string, error) {
|
||||
cfg := i.settingModule.Get(ctx)
|
||||
if cfg.SitePrefix == "" {
|
||||
return "", fmt.Errorf("site prefix is empty")
|
||||
}
|
||||
return fmt.Sprintf(mcpDefaultConfig, "APIPark-MCP-Server", fmt.Sprintf("%s/openapi/v1/%s/sse?apikey={your_api_key}", strings.TrimSuffix(cfg.SitePrefix, "/"), mcp_server.GlobalBasePath)), nil
|
||||
}
|
||||
|
||||
func (i *imlMcpController) generateZhCNMCPServer() *server.MCPServer {
|
||||
s := server.NewMCPServer("APIPark MCP Server", "1.0.0", server.WithLogging())
|
||||
s.AddTool(
|
||||
mcp2.NewTool(
|
||||
"service_list",
|
||||
mcp2.WithDescription("此工具用于获取 APIPark 中已注册服务的列表。每个服务包含其唯一标识(service ID)、名称、描述及包含的 API 列表等关键信息。支持通过关键词进行模糊搜索,以便快速缩小查找范围。在获得某个服务的 ID 后,可以调用 openapi_document 工具来获取该服务的 OpenAPI 文档,以便后续调用其提供的 API 接口。"),
|
||||
mcp2.WithString("keyword", mcp2.Description("关键词,用于模糊搜索服务")),
|
||||
),
|
||||
i.mcpModule.Services,
|
||||
)
|
||||
s.AddTool(
|
||||
mcp2.NewTool(
|
||||
"openapi_document",
|
||||
mcp2.WithDescription("此工具用于获取指定服务的 OpenAPI 接口文档。返回内容支持 OpenAPI v3 与 v2 两种规范格式。通过传入服务 ID,可以查看该服务的所有 API 定义、参数结构、请求方式等详细信息,为后续构造请求做准备。"),
|
||||
mcp2.WithString("service", mcp2.Description("服务的唯一标识 ID")),
|
||||
),
|
||||
i.mcpModule.APIs,
|
||||
)
|
||||
s.AddTool(
|
||||
mcp2.NewTool(
|
||||
"invoke_api",
|
||||
mcp2.WithDescription("此工具用于直接调用指定的 API 接口。调用前需根据该接口的 OpenAPI 文档构造必要的请求参数,如请求路径、方法、查询参数、请求头、请求体等。调用过程中无需传递认证信息,例如请求头中的 Authorization 字段不需要提供。"),
|
||||
mcp2.WithString("path", mcp2.Description("API 请求路径"), mcp2.Required()),
|
||||
mcp2.WithString("method", mcp2.Description("API 请求方法,例如 GET、POST、PUT"), mcp2.Required()),
|
||||
mcp2.WithString("content-type", mcp2.Description("请求的 Content-Type 类型。如果方法为 POST、PUT 或 PATCH,则必须指定该字段。")),
|
||||
mcp2.WithObject("query", mcp2.Description("请求的查询参数,类型为 map[string]string")),
|
||||
mcp2.WithObject("header", mcp2.Description("请求的头部参数,类型为 map[string]string")),
|
||||
mcp2.WithString("body", mcp2.Description("请求体内容,通常为 JSON 字符串")),
|
||||
),
|
||||
i.mcpModule.Invoke,
|
||||
)
|
||||
return s
|
||||
}
|
||||
|
||||
func (i *imlMcpController) generateZhTWMCPServer() *server.MCPServer {
|
||||
s := server.NewMCPServer("APIPark MCP Server", "1.0.0", server.WithLogging())
|
||||
s.AddTool(
|
||||
mcp2.NewTool(
|
||||
"service_list",
|
||||
mcp2.WithDescription("此工具用於獲取 APIPark 中已註冊服務的清單。每個服務包含其唯一識別碼(service ID)、名稱、描述以及該服務所包含的 API 列表。支援關鍵字模糊搜尋,可快速縮小查詢範圍。獲取到服務 ID 後,可使用 openapi_document 工具來查詢該服務對應的 OpenAPI 文件,為後續 API 呼叫做準備。"),
|
||||
mcp2.WithString("keyword", mcp2.Description("關鍵字,用於模糊搜尋服務")),
|
||||
),
|
||||
i.mcpModule.Services,
|
||||
)
|
||||
s.AddTool(
|
||||
mcp2.NewTool(
|
||||
"openapi_document",
|
||||
mcp2.WithDescription("此工具用於查詢指定服務的 OpenAPI 文件。返回的格式支援 OpenAPI v3 與 v2 標準。透過輸入服務 ID,可查閱該服務所有 API 的定義、參數結構、請求方式等細節,有助於後續構造 API 呼叫請求。"),
|
||||
mcp2.WithString("service", mcp2.Description("欲查詢的服務唯一識別碼")),
|
||||
),
|
||||
i.mcpModule.APIs,
|
||||
)
|
||||
s.AddTool(
|
||||
mcp2.NewTool(
|
||||
"invoke_api",
|
||||
mcp2.WithDescription("此工具可直接發送 API 請求。在呼叫此工具之前,需根據該 API 的 OpenAPI 文件構造所需的請求參數,如請求路徑、方法、查詢參數、標頭、主體等。使用此工具時不需傳送任何認證資訊,例如 Authorization 標頭可省略。"),
|
||||
mcp2.WithString("path", mcp2.Description("API 的請求路徑"), mcp2.Required()),
|
||||
mcp2.WithString("method", mcp2.Description("API 的請求方法,例如 GET、POST、PUT"), mcp2.Required()),
|
||||
mcp2.WithString("content-type", mcp2.Description("請求的 Content-Type。若方法為 POST、PUT 或 PATCH,則必須指定")),
|
||||
mcp2.WithObject("query", mcp2.Description("請求的查詢參數,類型為 map[string]string")),
|
||||
mcp2.WithObject("header", mcp2.Description("請求的標頭,類型為 map[string]string")),
|
||||
mcp2.WithString("body", mcp2.Description("請求主體內容,通常為 JSON 字串")),
|
||||
),
|
||||
i.mcpModule.Invoke,
|
||||
)
|
||||
return s
|
||||
}
|
||||
|
||||
func (i *imlMcpController) generateEnMCPServer() *server.MCPServer {
|
||||
s := server.NewMCPServer("APIPark MCP Server", "1.0.0", server.WithLogging())
|
||||
s.AddTool(
|
||||
mcp2.NewTool(
|
||||
"service_list",
|
||||
mcp2.WithDescription("This tool can retrieve a list of registered services on APIPark, including key information such as service ID, name, description, and API list within the service. Support keyword search to quickly narrow down the search scope. After obtaining the service ID, you can use this ID to call the tool openapi_document to obtain the openapi document of the service for the corresponding service, preparing for subsequent API calls."),
|
||||
mcp2.WithString("keyword", mcp2.Description("Keyword for fuzzy search")),
|
||||
),
|
||||
i.mcpModule.Services,
|
||||
)
|
||||
s.AddTool(
|
||||
mcp2.NewTool(
|
||||
"openapi_document",
|
||||
mcp2.WithDescription("This tool returns the openAPI documentation for the corresponding service. The format supports the specifications of OpenAPI v3 and OpenAPI v2."),
|
||||
mcp2.WithString("service", mcp2.Description("Service ID")),
|
||||
),
|
||||
i.mcpModule.APIs,
|
||||
)
|
||||
s.AddTool(
|
||||
mcp2.NewTool(
|
||||
"invoke_api",
|
||||
mcp2.WithDescription("This tool can directly make API calls. Before calling this tool, it is necessary to construct relevant parameters based on the corresponding API's openAPI documentation, including query, header, body, method, path, and other parameters. By using this tool, no authentication related information needs to be transmitted, that is, no request header Authorization needs to be transmitted."),
|
||||
mcp2.WithString("path", mcp2.Description("API path"), mcp2.Required()),
|
||||
mcp2.WithString("method", mcp2.Description("API method"), mcp2.Required()),
|
||||
mcp2.WithString("content-type", mcp2.Description("API Request Content-Type. If method is POST,PUT,PATCH, it must be set. If not set, it will be ignored.")),
|
||||
mcp2.WithObject("query", mcp2.Description("API Request query,param type is map[string]string")),
|
||||
mcp2.WithObject("header", mcp2.Description("API Request header,param type is map[string]string")),
|
||||
mcp2.WithString("body", mcp2.Description("API Request body")),
|
||||
),
|
||||
i.mcpModule.Invoke,
|
||||
)
|
||||
return s
|
||||
}
|
||||
|
||||
func (i *imlMcpController) generateJPMCPServer() *server.MCPServer {
|
||||
s := server.NewMCPServer("APIPark MCP Server", "1.0.0", server.WithLogging())
|
||||
s.AddTool(
|
||||
mcp2.NewTool(
|
||||
"service_list",
|
||||
mcp2.WithDescription("このツールは、APIPark に登録されているサービスの一覧を取得するためのものです。各サービスには、サービスID、名称、説明、およびそのサービスに含まれるAPI一覧といった重要な情報が含まれます。キーワードによるあいまい検索が可能で、目的のサービスを素早く絞り込むことができます。取得したサービスIDを使用して openapi_document ツールを呼び出すことで、そのサービスの OpenAPI ドキュメントを取得でき、APIの利用準備が整います。"),
|
||||
mcp2.WithString("keyword", mcp2.Description("キーワード。サービスをあいまい検索するための文字列")),
|
||||
),
|
||||
i.mcpModule.Services,
|
||||
)
|
||||
s.AddTool(
|
||||
mcp2.NewTool(
|
||||
"openapi_document",
|
||||
mcp2.WithDescription("指定されたサービスの OpenAPI ドキュメントを取得するためのツールです。OpenAPI v3 および v2 のフォーマットに対応しています。このドキュメントを使用することで、APIのエンドポイント、リクエスト方法、パラメータなどの詳細を確認でき、API呼び出しの準備に役立ちます。"),
|
||||
mcp2.WithString("service", mcp2.Description("対象のサービスID")),
|
||||
),
|
||||
i.mcpModule.APIs,
|
||||
)
|
||||
s.AddTool(
|
||||
mcp2.NewTool(
|
||||
"invoke_api",
|
||||
mcp2.WithDescription("このツールは、指定された API を直接呼び出すためのものです。呼び出し前に、OpenAPI ドキュメントに基づいて必要なパラメータ(パス、メソッド、クエリ、ヘッダー、ボディなど)を構築する必要があります。呼び出し時に認証情報(例:Authorization ヘッダー)を送信する必要はありません。"),
|
||||
mcp2.WithString("path", mcp2.Description("API のリクエストパス"), mcp2.Required()),
|
||||
mcp2.WithString("method", mcp2.Description("HTTPメソッド(GET、POST、PUTなど)。"), mcp2.Required()),
|
||||
mcp2.WithString("content-type", mcp2.Description("リクエストの Content-Type。メソッドが POST、PUT、PATCH の場合に必須。")),
|
||||
mcp2.WithObject("query", mcp2.Description("リクエストのクエリパラメータ。型は map[string]string")),
|
||||
mcp2.WithObject("header", mcp2.Description("リクエストヘッダー。型は map[string]string")),
|
||||
mcp2.WithString("body", mcp2.Description("リクエストボディ。通常はJSON文字列")),
|
||||
),
|
||||
i.mcpModule.Invoke,
|
||||
)
|
||||
return s
|
||||
}
|
||||
|
||||
func (i *imlMcpController) OnComplete() {
|
||||
i.server = make(map[string]http.Handler)
|
||||
enSer := i.generateEnMCPServer()
|
||||
i.server["en-US"] = server.NewSSEServer(enSer, server.WithBasePath(fmt.Sprintf("/api/v1/%s", mcp_server.GlobalBasePath)))
|
||||
i.server["zh-CN"] = server.NewSSEServer(i.generateZhCNMCPServer(), server.WithBasePath(fmt.Sprintf("/api/v1/%s", mcp_server.GlobalBasePath)))
|
||||
i.server["zh-TW"] = server.NewSSEServer(i.generateZhTWMCPServer(), server.WithBasePath(fmt.Sprintf("/api/v1/%s", mcp_server.GlobalBasePath)))
|
||||
i.server["ja-JP"] = server.NewSSEServer(i.generateJPMCPServer(), server.WithBasePath(fmt.Sprintf("/api/v1/%s", mcp_server.GlobalBasePath)))
|
||||
|
||||
i.openServer = server.NewSSEServer(enSer, server.WithBasePath(fmt.Sprintf("/openapi/v1/%s", strings.Trim(mcp_server.GlobalBasePath, "/"))))
|
||||
}
|
||||
|
||||
func (i *imlMcpController) GlobalMCPHandle(ctx *gin.Context) {
|
||||
cfg := i.settingModule.Get(ctx)
|
||||
req := ctx.Request.WithContext(utils.SetGatewayInvoke(ctx.Request.Context(), cfg.InvokeAddress))
|
||||
locale := utils.I18n(ctx)
|
||||
if v, ok := i.server[locale]; ok {
|
||||
v.ServeHTTP(ctx.Writer, req)
|
||||
return
|
||||
}
|
||||
i.server["en-US"].ServeHTTP(ctx.Writer, req)
|
||||
}
|
||||
|
||||
func (i *imlMcpController) GlobalHandleSSE(ctx *gin.Context) {
|
||||
apikey := ctx.Request.URL.Query().Get("apikey")
|
||||
i.handleSSE(ctx, i.openServer, apikey)
|
||||
}
|
||||
|
||||
func (i *imlMcpController) handleSSE(ctx *gin.Context, server http.Handler, apikey string) {
|
||||
|
||||
writer := &ResponseWriter{
|
||||
Writer: ctx.Writer,
|
||||
sessionId: make(chan string),
|
||||
}
|
||||
defer close(writer.sessionId)
|
||||
sessionId := ""
|
||||
go func() {
|
||||
var ok bool
|
||||
sessionId, ok = <-writer.sessionId
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
i.sessionKeys.Store(sessionId, apikey)
|
||||
}()
|
||||
server.ServeHTTP(writer, ctx.Request)
|
||||
i.sessionKeys.Delete(sessionId)
|
||||
}
|
||||
|
||||
func (i *imlMcpController) GlobalHandleMessage(ctx *gin.Context) {
|
||||
i.handleMessage(ctx, i.openServer)
|
||||
}
|
||||
|
||||
func (i *imlMcpController) MCPHandle(ctx *gin.Context) {
|
||||
cfg := i.settingModule.Get(ctx)
|
||||
|
||||
req := ctx.Request.WithContext(utils.SetGatewayInvoke(ctx.Request.Context(), cfg.InvokeAddress))
|
||||
mcp_server.ServeHTTP(ctx.Writer, req)
|
||||
}
|
||||
|
||||
func (i *imlMcpController) ServiceHandleSSE(ctx *gin.Context) {
|
||||
apikey := ctx.Request.URL.Query().Get("apikey")
|
||||
serviceId := ctx.Param("serviceId")
|
||||
if serviceId == "" {
|
||||
ctx.AbortWithStatusJSON(403, gin.H{"code": -1, "msg": "invalid service id", "success": "fail"})
|
||||
return
|
||||
}
|
||||
ok, err := i.authorizationModule.CheckAPIKeyAuthorization(ctx, serviceId, apikey)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(403, gin.H{"code": -1, "msg": err.Error(), "success": "fail"})
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
ctx.AbortWithStatusJSON(403, gin.H{"code": -1, "msg": "invalid apikey", "success": "fail"})
|
||||
return
|
||||
}
|
||||
|
||||
i.handleSSE(ctx, mcp_server.DefaultMCPServer(), apikey)
|
||||
}
|
||||
|
||||
func (i *imlMcpController) ServiceHandleMessage(ctx *gin.Context) {
|
||||
i.handleMessage(ctx, mcp_server.DefaultMCPServer())
|
||||
}
|
||||
|
||||
func (i *imlMcpController) handleMessage(ctx *gin.Context, server http.Handler) {
|
||||
sessionId := ctx.Request.URL.Query().Get("sessionId")
|
||||
apikey, ok := i.sessionKeys.Load(sessionId)
|
||||
if !ok {
|
||||
ctx.String(403, "sessionId not found")
|
||||
return
|
||||
}
|
||||
cfg := i.settingModule.Get(ctx)
|
||||
req := ctx.Request.WithContext(utils.SetGatewayInvoke(ctx.Request.Context(), cfg.InvokeAddress))
|
||||
req = req.WithContext(utils.SetLabel(req.Context(), "apikey", apikey.(string)))
|
||||
server.ServeHTTP(ctx.Writer, req)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/eolinker/go-common/autowire"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type IMcpController interface {
|
||||
MCPHandle(ctx *gin.Context)
|
||||
GlobalMCPHandle(ctx *gin.Context)
|
||||
GlobalHandleSSE(ctx *gin.Context)
|
||||
GlobalHandleMessage(ctx *gin.Context)
|
||||
ServiceHandleSSE(ctx *gin.Context)
|
||||
ServiceHandleMessage(ctx *gin.Context)
|
||||
GlobalMCPConfig(ctx *gin.Context) (string, error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
autowire.Auto[IMcpController](func() reflect.Value {
|
||||
return reflect.ValueOf(new(imlMcpController))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
type ResponseWriter struct {
|
||||
Writer http.ResponseWriter
|
||||
sessionId chan string
|
||||
}
|
||||
|
||||
func (r *ResponseWriter) Flush() {
|
||||
fluster, ok := r.Writer.(http.Flusher)
|
||||
if ok {
|
||||
fluster.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ResponseWriter) Header() http.Header {
|
||||
return r.Writer.Header()
|
||||
}
|
||||
|
||||
func (r *ResponseWriter) Write(bytes []byte) (int, error) {
|
||||
re := regexp.MustCompile(`sessionId=([^&?\s]+)`)
|
||||
match := re.FindStringSubmatch(string(bytes))
|
||||
if len(match) > 1 {
|
||||
r.sessionId <- match[1]
|
||||
}
|
||||
return r.Writer.Write(bytes)
|
||||
}
|
||||
|
||||
func (r *ResponseWriter) WriteHeader(statusCode int) {
|
||||
r.Writer.WriteHeader(statusCode)
|
||||
}
|
||||
@@ -542,8 +542,9 @@ type imlAppController struct {
|
||||
authModule application_authorization.IAuthorizationModule `autowired:""`
|
||||
}
|
||||
|
||||
func (i *imlAppController) SearchCanSubscribe(ctx *gin.Context, serviceId string) ([]*service_dto.SimpleAppItem, error) {
|
||||
return i.module.SearchCanSubscribe(ctx, serviceId)
|
||||
func (i *imlAppController) SearchCanSubscribe(ctx *gin.Context, serviceId string) ([]*service_dto.SubscribeAppItem, error) {
|
||||
items, _, err := i.module.SearchCanSubscribe(ctx, serviceId)
|
||||
return items, err
|
||||
}
|
||||
|
||||
func (i *imlAppController) Search(ctx *gin.Context, teamId string, keyword string) ([]*service_dto.AppItem, error) {
|
||||
|
||||
@@ -44,7 +44,7 @@ type IAppController interface {
|
||||
// SimpleApps 获取简易项目列表
|
||||
SimpleApps(ctx *gin.Context, keyword string) ([]*service_dto.SimpleAppItem, error)
|
||||
MySimpleApps(ctx *gin.Context, keyword string) ([]*service_dto.SimpleAppItem, error)
|
||||
SearchCanSubscribe(ctx *gin.Context, keyword string) ([]*service_dto.SimpleAppItem, error)
|
||||
SearchCanSubscribe(ctx *gin.Context, serviceId string) ([]*service_dto.SubscribeAppItem, error)
|
||||
GetApp(ctx *gin.Context, appId string) (*service_dto.App, error)
|
||||
DeleteApp(ctx *gin.Context, appId string) error
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package system_apikey
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
system_apikey_dto "github.com/APIParkLab/APIPark/module/system-apikey/dto"
|
||||
"github.com/eolinker/go-common/autowire"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type IAPIKeyController interface {
|
||||
Create(ctx *gin.Context, input *system_apikey_dto.Create) error
|
||||
Update(ctx *gin.Context, id string, input *system_apikey_dto.Update) error
|
||||
Delete(ctx *gin.Context, id string) error
|
||||
Get(ctx *gin.Context, id string) (*system_apikey_dto.APIKey, error)
|
||||
Search(ctx *gin.Context, keyword string) ([]*system_apikey_dto.Item, error)
|
||||
SimpleList(ctx *gin.Context) ([]*system_apikey_dto.SimpleItem, error)
|
||||
MyAPIKeys(ctx *gin.Context) ([]*system_apikey_dto.SimpleItem, error)
|
||||
MyAPIKeysByService(ctx *gin.Context, serviceId string) ([]*system_apikey_dto.AuthorizationItem, error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
autowire.Auto[IAPIKeyController](func() reflect.Value {
|
||||
return reflect.ValueOf(new(imlAPIKeyController))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package system_apikey
|
||||
|
||||
import (
|
||||
system_apikey "github.com/APIParkLab/APIPark/module/system-apikey"
|
||||
system_apikey_dto "github.com/APIParkLab/APIPark/module/system-apikey/dto"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var _ IAPIKeyController = new(imlAPIKeyController)
|
||||
|
||||
type imlAPIKeyController struct {
|
||||
apikeyModule system_apikey.IAPIKeyModule `autowired:""`
|
||||
}
|
||||
|
||||
func (i *imlAPIKeyController) MyAPIKeysByService(ctx *gin.Context, serviceId string) ([]*system_apikey_dto.AuthorizationItem, error) {
|
||||
return i.apikeyModule.MyAPIKeysByService(ctx, serviceId)
|
||||
}
|
||||
|
||||
func (i *imlAPIKeyController) MyAPIKeys(ctx *gin.Context) ([]*system_apikey_dto.SimpleItem, error) {
|
||||
return i.apikeyModule.MyAPIKeys(ctx)
|
||||
}
|
||||
|
||||
func (i *imlAPIKeyController) Create(ctx *gin.Context, input *system_apikey_dto.Create) error {
|
||||
return i.apikeyModule.Create(ctx, input)
|
||||
}
|
||||
|
||||
func (i *imlAPIKeyController) Update(ctx *gin.Context, id string, input *system_apikey_dto.Update) error {
|
||||
return i.apikeyModule.Update(ctx, id, input)
|
||||
}
|
||||
|
||||
func (i *imlAPIKeyController) Delete(ctx *gin.Context, id string) error {
|
||||
return i.apikeyModule.Delete(ctx, id)
|
||||
}
|
||||
|
||||
func (i *imlAPIKeyController) Get(ctx *gin.Context, id string) (*system_apikey_dto.APIKey, error) {
|
||||
return i.apikeyModule.Get(ctx, id)
|
||||
}
|
||||
|
||||
func (i *imlAPIKeyController) Search(ctx *gin.Context, keyword string) ([]*system_apikey_dto.Item, error) {
|
||||
return i.apikeyModule.Search(ctx, keyword)
|
||||
}
|
||||
|
||||
func (i *imlAPIKeyController) SimpleList(ctx *gin.Context) ([]*system_apikey_dto.SimpleItem, error) {
|
||||
return i.apikeyModule.SimpleList(ctx)
|
||||
}
|
||||
@@ -47,7 +47,10 @@
|
||||
"swagger-ui-react": "^5.17.14",
|
||||
"tailwindcss": "^3.3.5",
|
||||
"uuid": "^9.0.1",
|
||||
"vite-tsconfig-paths": "^4.3.2"
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
"react-json-view": "^1.21.3",
|
||||
"zod": "^3.23.8",
|
||||
"@modelcontextprotocol/sdk": "^1.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ant-design/cssinjs": "^1.18.2",
|
||||
|
||||
@@ -1,11 +1,25 @@
|
||||
import { Breadcrumb } from 'antd'
|
||||
import { useBreadcrumb } from '@common/contexts/BreadcrumbContext.tsx'
|
||||
import { FC, useEffect } from 'react'
|
||||
import { LeftOutlined } from '@ant-design/icons'
|
||||
|
||||
const TopBreadcrumb: FC = () => {
|
||||
const TopBreadcrumb: FC<{ handleBackCallback?: () => void }> = ({ handleBackCallback }) => {
|
||||
const { breadcrumb } = useBreadcrumb()
|
||||
useEffect(() => {}, [breadcrumb])
|
||||
return <Breadcrumb items={breadcrumb} />
|
||||
const handleBack = () => {
|
||||
handleBackCallback?.()
|
||||
}
|
||||
return (
|
||||
<div className="flex text-[18px] leading-[25px] pb-[12px]">
|
||||
<div
|
||||
onClick={handleBack}
|
||||
className="hover:bg-gray-100 items-center mt-[1px] mr-[12px] flex justify-center rounded-lg border cursor-pointer border-gray-300 w-[30px] h-[30px] border border-solid "
|
||||
>
|
||||
<LeftOutlined className="text-xs" />
|
||||
</div>
|
||||
<Breadcrumb items={breadcrumb} className="flex-1" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TopBreadcrumb
|
||||
|
||||
@@ -4,11 +4,12 @@ import { $t } from '@common/locales'
|
||||
import { Button, Tag } from 'antd'
|
||||
import { FC, ReactNode } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import TopBreadcrumb from './Breadcrumb'
|
||||
|
||||
class InsidePageProps {
|
||||
showBanner?: boolean = true
|
||||
pageTitle: string | React.ReactNode = ''
|
||||
tagList?: Array<{ label: string | ReactNode }> = []
|
||||
tagList?: Array<{ label: string | ReactNode; className?: string; color?: string }> = []
|
||||
children: React.ReactNode
|
||||
showBtn?: boolean = false
|
||||
btnTitle?: string = ''
|
||||
@@ -64,14 +65,7 @@ const InsidePage: FC<InsidePageProps> = ({
|
||||
<></>
|
||||
) : (
|
||||
<div className={customPadding ? '' : 'mb-[30px]'}>
|
||||
{backUrl && (
|
||||
<div className="text-[18px] leading-[25px] mb-[12px]">
|
||||
<Button type="text" onClick={goBack}>
|
||||
<ArrowLeftOutlined className="max-h-[14px]" />
|
||||
{$t('返回')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{backUrl && <TopBreadcrumb handleBackCallback={() => goBack()} />}
|
||||
<div className="flex justify-between mb-[20px] items-center ">
|
||||
<div className="flex items-center gap-TAG_LEFT">
|
||||
<div className="text-theme text-[26px] ">{pageTitle}</div>
|
||||
@@ -79,7 +73,7 @@ const InsidePage: FC<InsidePageProps> = ({
|
||||
tagList?.length > 0 &&
|
||||
tagList?.map((tag) => {
|
||||
return (
|
||||
<Tag key={tag.label as string} bordered={false}>
|
||||
<Tag key={tag.label as string} bordered={false} color={tag.color} className={tag.className}>
|
||||
{tag.label}
|
||||
</Tag>
|
||||
)
|
||||
|
||||
@@ -55,6 +55,8 @@ export const TranslateWord = () => {
|
||||
{$t('打开 OpenAPI YAML 编辑器')}
|
||||
{$t('无需审核:允许任何消费者调用该服务')}
|
||||
{$t('人工审核:仅允许通过人工审核的消费者调用该服务')}
|
||||
{$t('开启:AI Agent 等产品能够通过 MCP 方式调用服务')}
|
||||
{$t('总览')}
|
||||
{$t('永久')}
|
||||
{$t('否')}
|
||||
{$t('是')}
|
||||
|
||||
@@ -234,12 +234,16 @@ export default function ApiEdit({
|
||||
<>
|
||||
<Space.Compact className="w-full mb-btnybase">
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
className="w-[15%] min-w-[100px]"
|
||||
value={apiInfo?.protocol || 'HTTP'}
|
||||
disabled={true}
|
||||
options={protocolOptionList}
|
||||
/>
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
className="w-[15%] min-w-[100px]"
|
||||
value={apiInfo?.method}
|
||||
disabled={true}
|
||||
|
||||
@@ -254,6 +254,21 @@ export const PERMISSION_DEFINITION = [
|
||||
anyOf: [{ backend: ['system.settings.ai_balance.manager'] }]
|
||||
}
|
||||
},
|
||||
'system.settings.mcp.manager': {
|
||||
granted: {
|
||||
anyOf: [{ backend: ['system.settings.mcp.manager'] }]
|
||||
}
|
||||
},
|
||||
'system.settings.mcp.view': {
|
||||
granted: {
|
||||
anyOf: [{ backend: ['system.settings.mcp.view'] }]
|
||||
}
|
||||
},
|
||||
'system.settings.apikey.view': {
|
||||
granted: {
|
||||
anyOf: [{ backend: ['system.settings.apikey.view'] }]
|
||||
}
|
||||
},
|
||||
'system.devops.policy.view': {
|
||||
granted: {
|
||||
anyOf: [{ backend: ['system.settings.strategy.view'] }]
|
||||
|
||||
@@ -81,6 +81,7 @@ export type MatchItem = {
|
||||
export type EntityItem = {
|
||||
id: string
|
||||
name: string
|
||||
isSubscribed: boolean
|
||||
}
|
||||
|
||||
export type DynamicMenuItem = {
|
||||
|
||||
@@ -18,5 +18,19 @@ export const useBreadcrumb = () => {
|
||||
export const BreadcrumbProvider = ({ children }: unknown) => {
|
||||
const [breadcrumb, setBreadcrumb] = useState<BreadcrumbItemType[]>([])
|
||||
|
||||
return <BreadcrumbContext.Provider value={{ setBreadcrumb, breadcrumb }}>{children}</BreadcrumbContext.Provider>
|
||||
return (
|
||||
<BreadcrumbContext.Provider
|
||||
value={{
|
||||
setBreadcrumb: (newItems) => {
|
||||
newItems.slice(0, newItems.length - 1).forEach((item) => {
|
||||
item.title = <span className="cursor-pointer hover:text-theme">{item.title}</span>
|
||||
})
|
||||
setBreadcrumb(newItems)
|
||||
},
|
||||
breadcrumb
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</BreadcrumbContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -99,10 +99,31 @@ const mockData = [
|
||||
icon: 'ic:baseline-bar-chart',
|
||||
children: [
|
||||
{
|
||||
name: '运行视图',
|
||||
key: 'analytics',
|
||||
path: '/analytics',
|
||||
icon: 'ic:baseline-bar-chart',
|
||||
name: '总览',
|
||||
key: 'analyticsTotal',
|
||||
path: '/analytics/total',
|
||||
icon: 'material-symbols:bar-chart',
|
||||
access: 'system.analysis.run_view.view'
|
||||
},
|
||||
{
|
||||
name: '服务',
|
||||
key: 'analyticsSubscriber',
|
||||
path: '/analytics/subscriber/list',
|
||||
icon: 'ic:baseline-blinds-closed',
|
||||
access: 'system.analysis.run_view.view'
|
||||
},
|
||||
{
|
||||
name: '消费者',
|
||||
key: 'analyticsProvider',
|
||||
path: '/analytics/provider/list',
|
||||
icon: 'ic:baseline-apps',
|
||||
access: 'system.analysis.run_view.view'
|
||||
},
|
||||
{
|
||||
name: 'API',
|
||||
key: 'analyticsApi',
|
||||
path: '/analytics/api/list',
|
||||
icon: 'gravity-ui:plug-connection',
|
||||
access: 'system.analysis.run_view.view'
|
||||
}
|
||||
],
|
||||
@@ -196,6 +217,20 @@ const mockData = [
|
||||
key: 'maintenanceCenter',
|
||||
path: '/datasourcing',
|
||||
children: [
|
||||
{
|
||||
name: 'MCP 服务',
|
||||
key: 'mcpService',
|
||||
path: '/mcpService',
|
||||
icon: 'ph:network-x',
|
||||
access: 'system.settings.mcp.view'
|
||||
},
|
||||
{
|
||||
name: 'API Key',
|
||||
key: 'mcpKey',
|
||||
path: '/mcpKey',
|
||||
icon: 'material-symbols:key',
|
||||
access: 'system.settings.apikey.view'
|
||||
},
|
||||
{
|
||||
name: '数据源',
|
||||
key: 'datasourcing',
|
||||
|
||||
@@ -220,6 +220,26 @@ const mockData = {
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
driver: 'apipark.builtIn.component',
|
||||
name: 'mcpService',
|
||||
router: [
|
||||
{
|
||||
path: 'mcpService',
|
||||
type: 'normal'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
driver: 'apipark.builtIn.component',
|
||||
name: 'mcpKey',
|
||||
router: [
|
||||
{
|
||||
path: 'mcpKey',
|
||||
type: 'normal'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
driver: 'apipark.builtIn.component',
|
||||
name: 'loadBalancing',
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"添加(0)": "Ka7aaaeb",
|
||||
"请输入Key": "Kaff78ecf",
|
||||
"请输入Value": "K65d46535",
|
||||
"返回": "Kc14b2ea3",
|
||||
"ID": "K11d3633a",
|
||||
"名称": "Kbff43de3",
|
||||
"Driver": "K16ca79ef",
|
||||
@@ -114,6 +113,7 @@
|
||||
"打开 OpenAPI YAML 编辑器": "Kdac8ce7e",
|
||||
"无需审核:允许任何消费者调用该服务": "K1fc2cc28",
|
||||
"人工审核:仅允许通过人工审核的消费者调用该服务": "K8dabb98e",
|
||||
"开启:AI Agent 等产品能够通过 MCP 方式调用服务": "Ke959f135",
|
||||
"永久": "Kbfe02d7f",
|
||||
"否": "K1e9c479e",
|
||||
"是": "Kaddfcb6b",
|
||||
@@ -345,10 +345,11 @@
|
||||
"重置": "K50d471b2",
|
||||
"查询": "Kee8ae330",
|
||||
"请输入 APIURL 搜索": "Kf8187c33",
|
||||
"服务": "Kb58e0c3f",
|
||||
"说明文档": "K6cd677b",
|
||||
"最近一次更新者": "K617f34f1",
|
||||
"最近一次更新时间": "K6ebca204",
|
||||
"保存": "Kabfe9512",
|
||||
"服务": "Kb58e0c3f",
|
||||
"API 路由": "K51d1eb5d",
|
||||
"API 文档": "Ka2b6d281",
|
||||
"使用说明": "Kdefa9caa",
|
||||
@@ -365,6 +366,9 @@
|
||||
"手动添加": "K18307d56",
|
||||
"订阅申请": "K705fe9f5",
|
||||
"订阅方": "K3a67ea90",
|
||||
"API": "K3ba29a85",
|
||||
"编辑 API": "Ke93388fd",
|
||||
"添加 API": "K84aabfd4",
|
||||
"AI 路由设置": "Kefa2a4cf",
|
||||
"路由名称": "K66060758",
|
||||
"请求路径": "K5582ac8",
|
||||
@@ -414,17 +418,16 @@
|
||||
"启用": "K52c8a730",
|
||||
"Ollama 地址": "K6b99dce8",
|
||||
"输入例如:https://www.apipark.com": "K8d4f5b44",
|
||||
"自定义": "K8929cbb1",
|
||||
"自定义(空模板)": "K24f6a5b4",
|
||||
"模型名称": "K1fd51aaa",
|
||||
"访问配置": "Kd6285399",
|
||||
"模型参数模板": "K7eb03edd",
|
||||
"模型参数": "K49b434e9",
|
||||
"载入预置模板": "Kea608112",
|
||||
"供应商名称": "K16ef56b1",
|
||||
"注意:": "K484de451",
|
||||
"仅支持使用 OpenAI 输入输出格式和认证方法(APIKey)的供应商。如果不满足此条件,创建后自定义供应商将不可用。": "Kbd80dde0",
|
||||
"从 (0) 获取 API KEY": "Kb3e34847",
|
||||
"该模型为官方模型,不可编辑": "Kcb6a1c57",
|
||||
"存在使用当前模型的接口,需要先解绑后才能编辑": "Kf9300eb4",
|
||||
"该模型为官方模型,不可删除": "K8af71816",
|
||||
"存在使用当前模型的接口,需要先解绑后才能删除": "Kb8ad0af5",
|
||||
"模型值": "K73cb9ff1",
|
||||
@@ -450,6 +453,7 @@
|
||||
"Models": "Ke37a353f",
|
||||
"Keys": "K14bcebd2",
|
||||
"添加供应商": "Kd87397b0",
|
||||
"编辑供应商( (0) )": "Kee7de862",
|
||||
"编辑供应商": "K5bcf8c48",
|
||||
"(0) 模型": "Kf7a916be",
|
||||
"待审核": "K35612f29",
|
||||
@@ -457,7 +461,7 @@
|
||||
"发布申请": "K56b4254f",
|
||||
"API 调用地址": "Kea2f9279",
|
||||
"API base URL 一般设置为API 网关的外部网络访问地址,或者是API网关绑定的域名。": "K7fc496a1",
|
||||
"集成地址": "K508d8bf4",
|
||||
"OpenAPI & MCP 调用地址": "Ka7ca8fde",
|
||||
"与外部平台集成时,获取 API 市场中文档信息的域名": "K67f4e9bb",
|
||||
"常规设置": "K8ab0fc95",
|
||||
"API 请求设置": "Kb66fec9d",
|
||||
@@ -469,6 +473,7 @@
|
||||
"分类名称": "Ke595a20a",
|
||||
"父分类 ID": "K9679728f",
|
||||
"子分类名称": "K9b2d08fd",
|
||||
"暂无权限": "Kce2fcdbf",
|
||||
"添加 Rest 服务": "K2c93168c",
|
||||
"导入OpenAPI文档,将现有系统的API发布到APIPark。": "K39a8d392",
|
||||
"添加在线 AI API": "K68932d54",
|
||||
@@ -551,6 +556,13 @@
|
||||
"Version (0)-(1)": "K480045ce",
|
||||
"日志配置": "Kadee8e49",
|
||||
"提供详尽的 API 调用日志,帮助企业监控、分析和审计 API 的运行状况。": "K2724314b",
|
||||
"MCP 配置": "K6e9c928f",
|
||||
"Open API 文档": "Kb6d0eb39",
|
||||
"AI 代理集成": "Ke6908f16",
|
||||
"新增 API Key": "Kb0e0aeda",
|
||||
"API 密钥可用于调用系统级 Open API 和 MCP。": "K9d81999c",
|
||||
"MCP 服务": "Kf106bc62",
|
||||
"MCP Service 充当 AI 模型与 API 之间的桥梁,允许智能助手(如 Claude)动态发现和调用 Gateway 上的 API,无需繁琐的手动配置或自定义集成。": "K7c2bfeff",
|
||||
"部门名称": "K33c76dbc",
|
||||
"父部门 ID": "K84829ca9",
|
||||
"子部门名称": "K4d7fc74b",
|
||||
@@ -615,6 +627,8 @@
|
||||
"处理日志": "Ke429194e",
|
||||
"脱敏前": "K8c34c02f",
|
||||
"脱敏后": "K8e3d388d",
|
||||
"编辑服务策略": "Kf06f6737",
|
||||
"添加服务策略": "K205971e1",
|
||||
"编辑策略": "Kc82b8374",
|
||||
"策略类型": "K4b34a5e5",
|
||||
"匹配条件": "K57f0fee8",
|
||||
@@ -641,6 +655,7 @@
|
||||
"支持对系统全局进行统一的策略配置,从而简化管理并确保一致性。全局策略的优先级比服务策略略低。": "Kc975cd5a",
|
||||
"资源配置": "K8e7a0f80",
|
||||
"角色": "Kf644225f",
|
||||
"角色配置": "Kc9f2249c",
|
||||
"设置角色的权限范围。": "K95c3fd8b",
|
||||
"系统级别角色": "K138facd3",
|
||||
"添加角色": "K6eac768d",
|
||||
@@ -662,9 +677,13 @@
|
||||
"停止": "K24540de",
|
||||
"继续等待": "Kd85b3f64",
|
||||
"只允许上传PNG、JPG或SVG格式的图片": "Ka9c08390",
|
||||
"关闭 MCP": "K30595880",
|
||||
"关闭后将无法通过MCP方式调用服务": "Kc081047c",
|
||||
"了解": "Ka73a5801",
|
||||
"服务名称": "K413b9869",
|
||||
"服务类型": "K9919285b",
|
||||
"REST 服务": "K62840d62",
|
||||
"MCP": "K373c8ab3",
|
||||
"默认 AI 供应商": "Kcef64f4d",
|
||||
"创建 API 时会默认选择该供应商,修改默认供应商不会影响现有 API": "K300c89d4",
|
||||
"未配置任何 AI 模型供应商,": "Kcab588a9",
|
||||
@@ -811,6 +830,7 @@
|
||||
"添加授权": "Kd23d1716",
|
||||
"到期时间": "Kfa920c0",
|
||||
"订阅的服务": "Kcce1af60",
|
||||
"返回": "Kc14b2ea3",
|
||||
"审核详情": "Kfefa9b58",
|
||||
"取消订阅": "K3118fdb0",
|
||||
"请确认是否取消订阅?": "Ked811bb1",
|
||||
@@ -823,22 +843,21 @@
|
||||
"创建并管理自己的消费者实体,每个消费者可以订阅多个API服务,确保在调用之前已获得相应权限。你可以为消费者生成 API 密钥等鉴权方式,用于安全地调用 API 服务": "K5c4e2865",
|
||||
"订阅的服务数量:已通过 (0) 个,申请中 (1) 个": "K3c7b175f",
|
||||
"输入名称、ID 查找消费者": "K3a6f905d",
|
||||
"服务市场": "K370a3eb2",
|
||||
"API 门户": "Kc84dbd1a",
|
||||
"服务详情": "Kf7ec36d",
|
||||
"申请服务": "K58ca9485",
|
||||
"介绍": "K59cdbec3",
|
||||
"Base URL": "Kc29dabf2",
|
||||
"申请": "K4aa9ed2c",
|
||||
"服务信息": "K6c060779",
|
||||
"接入消费者": "Kba74f26d",
|
||||
"供应方": "Kb97544cb",
|
||||
"分类": "Kb32f0afe",
|
||||
"版本": "K81634069",
|
||||
"更新时间": "Keefda53d",
|
||||
"无标签": "K96a2f1c8",
|
||||
"介绍": "K59cdbec3",
|
||||
"暂无服务描述": "Ka4b45550",
|
||||
"申请": "K4aa9ed2c",
|
||||
"无标签": "K96a2f1c8",
|
||||
"分类": "Kb32f0afe",
|
||||
"服务市场": "K370a3eb2",
|
||||
"API 数量": "K72b0c0b3",
|
||||
"接入消费者数量": "K70b79760",
|
||||
"30天内调用次数": "K3d52b756",
|
||||
"关联标签": "K96059c69",
|
||||
"更新者": "K8b7c2592",
|
||||
"添加 Open Api": "K32263abd",
|
||||
|
||||
@@ -498,7 +498,7 @@
|
||||
"K370a3eb2": "Service Marketplace",
|
||||
"Kf7ec36d": "Service Details",
|
||||
"K58ca9485": "Subscribe",
|
||||
"K59cdbec3": "Intro",
|
||||
"K59cdbec3": "Readme",
|
||||
"K4aa9ed2c": "Subscribe",
|
||||
"K6c060779": "Service Information",
|
||||
"Kb97544cb": "Provider",
|
||||
@@ -898,5 +898,34 @@
|
||||
"Kce2fcdbf": "No Permission",
|
||||
"K24f6a5b4": "Custom (Empty Template)",
|
||||
"Kea608112": "Load Preset Template",
|
||||
"Kee7de862": "Edit Provider( (0) )"
|
||||
"Kee7de862": "Edit Provider( (0) )",
|
||||
"Kb0e0aeda": "New API Key",
|
||||
"K9d81999c": "The API Key can be used to call system-level Open API and MCP.",
|
||||
"Ka7ca8fde": "OpenAPI & MCP Invocation Address",
|
||||
"K6e9c928f": "MCP Config",
|
||||
"Kb6d0eb39": "Open API Docs",
|
||||
"Ke6908f16": "Integration to AI Agent",
|
||||
"Kf106bc62": "MCP Service",
|
||||
"K7c2bfeff": "MCP Service acts as a bridge between AI models and APIs, enabling intelligent assistants (such as Claude) to dynamically discover and invoke APIs on the Gateway without the need for tedious manual configuration or custom integration.",
|
||||
"K30595880": "Disable MCP",
|
||||
"Kc081047c": "After turning it off, you won’t be able to use the service through the MCP.",
|
||||
"K373c8ab3": "MCP",
|
||||
"K3ba29a85": "API",
|
||||
"K3d52b756": "Number of invocations within 30 days",
|
||||
"Ke959f135": "Enable: AI Agent and other products can invoke services through MCP.",
|
||||
"Ka73a5801": "I Understand",
|
||||
"K6cd677b": "Documentation",
|
||||
"Kec68fe24": "Edit API Settings",
|
||||
"Kc9aeee51": "Add API Settings",
|
||||
"Kf06f6737": "Edit Service Policy",
|
||||
"K205971e1": "Add Service Policy",
|
||||
"Kc9f2249c": "Role Configuration",
|
||||
"Kba68dfc1": "Add API Settings",
|
||||
"Kc84dbd1a": "API Portal",
|
||||
"Ke93388fd": "Edit API",
|
||||
"K84aabfd4": "Add API",
|
||||
"K71ed51fa": "Please subscribe to the service first",
|
||||
"K1bec8cbe": "Select API Key",
|
||||
"K5611e01e": "This consumer is already subscribed",
|
||||
"Kaf9e8011": "Overview"
|
||||
}
|
||||
|
||||
@@ -920,5 +920,34 @@
|
||||
"Kce2fcdbf": "権限がありません",
|
||||
"K24f6a5b4": "カスタム(空のテンプレート)",
|
||||
"Kea608112": "プリセットテンプレートを読み込む",
|
||||
"Kee7de862": "サプライヤーを編集( (0) )"
|
||||
"Kee7de862": "サプライヤーを編集( (0) )",
|
||||
"Kb0e0aeda": "APIキーを新規作成",
|
||||
"K9d81999c": "APIキーは、システムレベルのOpen APIおよびMCPの呼び出しに使用できます。",
|
||||
"Ka7ca8fde": "OpenAPI & MCP 呼び出しアドレス",
|
||||
"K6e9c928f": "MCP 設定",
|
||||
"Kb6d0eb39": "Open API ドキュメント",
|
||||
"Ke6908f16": "AI プロキシ統合",
|
||||
"Kf106bc62": "MCP サービス",
|
||||
"K7c2bfeff": "MCP サービスは、AI モデルと API の間の橋渡し役を果たし、スマートアシスタント(Claude など)が、面倒な手動設定やカスタム統合なしで、ゲートウェイ上の API を動的に発見し、呼び出すことを可能にします。",
|
||||
"K30595880": "MCP 無効化",
|
||||
"Kc081047c": "無効化すると、MCP を通じてサービスを呼び出すことができなくなります。",
|
||||
"K373c8ab3": "MCP",
|
||||
"K3ba29a85": "API",
|
||||
"K3d52b756": "30日以内の呼び出し回数",
|
||||
"Ke959f135": "有効化:AI Agent などの製品が MCP 経由でサービスを呼び出すことができる",
|
||||
"Ka73a5801": "理解する",
|
||||
"K6cd677b": "説明書",
|
||||
"Kec68fe24": "API設定の編集",
|
||||
"Kc9aeee51": "API設定の追加",
|
||||
"Kf06f6737": "サービスポリシーの編集",
|
||||
"K205971e1": "サービスポリシーの追加",
|
||||
"Kc9f2249c": "役割の設定",
|
||||
"Kba68dfc1": "API設定の追加",
|
||||
"Kc84dbd1a": "APIポータル",
|
||||
"Ke93388fd": "APIの編集",
|
||||
"K84aabfd4": "APIの追加",
|
||||
"K71ed51fa": "このサービスに先にサブスクリプションしてください",
|
||||
"K1bec8cbe": "APIキーを選択してください",
|
||||
"K5611e01e": "この消費者はすでに購読しています",
|
||||
"Kaf9e8011": "概要"
|
||||
}
|
||||
|
||||
@@ -44,7 +44,6 @@
|
||||
"Kd2850420": "待删除",
|
||||
"K83237c89": "输入的IP或CIDR不符合格式",
|
||||
"K5ae2c87a": "请正确输入路径,如/usr/*或*/usr/*",
|
||||
"K508d8bf4": "集成地址",
|
||||
"K67f4e9bb": "与外部平台集成时,获取 API 市场中文档信息的域名",
|
||||
"Kc82b8374": "编辑策略",
|
||||
"K4b34a5e5": "策略类型",
|
||||
|
||||
@@ -851,5 +851,34 @@
|
||||
"Kce2fcdbf": "暂无权限",
|
||||
"K24f6a5b4": "自定义(空模板)",
|
||||
"Kea608112": "载入预置模板",
|
||||
"Kee7de862": "编辑供应商( (0) )"
|
||||
"Kee7de862": "编辑供应商( (0) )",
|
||||
"Kb0e0aeda": "新增 API Key",
|
||||
"K9d81999c": "API 密钥可用于调用系统级 Open API 和 MCP。",
|
||||
"Ka7ca8fde": "OpenAPI & MCP 调用地址",
|
||||
"K6e9c928f": "MCP 配置",
|
||||
"Kb6d0eb39": "Open API 文档",
|
||||
"Ke6908f16": "AI 代理集成",
|
||||
"Kf106bc62": "MCP 服务",
|
||||
"K7c2bfeff": "MCP Service 充当 AI 模型与 API 之间的桥梁,允许智能助手(如 Claude)动态发现和调用 Gateway 上的 API,无需繁琐的手动配置或自定义集成。",
|
||||
"K30595880": "关闭 MCP",
|
||||
"Kc081047c": "关闭后将无法通过MCP方式调用服务",
|
||||
"K373c8ab3": "MCP",
|
||||
"K3ba29a85": "API",
|
||||
"K3d52b756": "30天内调用次数",
|
||||
"Ke959f135": "开启:AI Agent 等产品能够通过 MCP 方式调用服务",
|
||||
"Ka73a5801": "了解",
|
||||
"K6cd677b": "说明文档",
|
||||
"Kec68fe24": "编辑 API 设置",
|
||||
"Kc9aeee51": "新增 API 设置",
|
||||
"Kf06f6737": "编辑服务策略",
|
||||
"K205971e1": "添加服务策略",
|
||||
"Kc9f2249c": "角色配置",
|
||||
"Kba68dfc1": "添加 API 设置",
|
||||
"Kc84dbd1a": "API 门户",
|
||||
"Ke93388fd": "编辑 API",
|
||||
"K84aabfd4": "添加 API",
|
||||
"K71ed51fa": "请先订阅该服务",
|
||||
"K1bec8cbe": "选择 API Key",
|
||||
"K5611e01e": "该消费者已订阅",
|
||||
"Kaf9e8011": "总览"
|
||||
}
|
||||
|
||||
@@ -920,5 +920,34 @@
|
||||
"Kce2fcdbf": "暫無權限",
|
||||
"K24f6a5b4": "自訂(空模板)",
|
||||
"Kea608112": "載入預設模板",
|
||||
"Kee7de862": "編輯供應商( (0) )"
|
||||
"Kee7de862": "編輯供應商( (0) )",
|
||||
"Kb0e0aeda": "新增 API 金鑰",
|
||||
"K9d81999c": "API 金鑰可用於調用系統級 Open API 和 MCP。",
|
||||
"Ka7ca8fde": "OpenAPI & MCP 呼叫地址",
|
||||
"K6e9c928f": "MCP 設定",
|
||||
"Kb6d0eb39": "Open API 文件",
|
||||
"Ke6908f16": "AI 代理整合",
|
||||
"Kf106bc62": "MCP 服務",
|
||||
"K7c2bfeff": "MCP 服務作為 AI 模型與 API 之間的橋樑,允許智能助手(如 Claude)動態發現並呼叫 Gateway 上的 API,而無需繁瑣的手動設定或自訂整合。",
|
||||
"K30595880": "關閉 MCP",
|
||||
"Kc081047c": "關閉後將無法透過 MCP 方式呼叫服務",
|
||||
"K373c8ab3": "MCP",
|
||||
"K3ba29a85": "API",
|
||||
"K3d52b756": "30 天內呼叫次數",
|
||||
"Ke959f135": "開啟:AI Agent 等產品能夠透過 MCP 方式呼叫服務",
|
||||
"Ka73a5801": "了解",
|
||||
"K6cd677b": "說明文檔",
|
||||
"Kec68fe24": "編輯 API 設定",
|
||||
"Kc9aeee51": "新增 API 設定",
|
||||
"Kf06f6737": "編輯服務策略",
|
||||
"K205971e1": "新增服務策略",
|
||||
"Kc9f2249c": "角色配置",
|
||||
"Kba68dfc1": "新增 API 設定",
|
||||
"Kc84dbd1a": "API 門戶",
|
||||
"Ke93388fd": "編輯 API",
|
||||
"K84aabfd4": "新增 API",
|
||||
"K71ed51fa": "請先訂閱該服務",
|
||||
"K1bec8cbe": "選擇 API Key",
|
||||
"K5611e01e": "該消費者已訂閱",
|
||||
"Kaf9e8011": "總覽"
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ export type AiServiceConfigFieldType = {
|
||||
catalogue?:string | string[];
|
||||
approvalType?:string;
|
||||
providerType?:string
|
||||
enable_mcp?: boolean
|
||||
};
|
||||
|
||||
export type AiServiceSubServiceTableListItem = {
|
||||
|
||||
@@ -800,6 +800,22 @@ export const routerMap: Map<string, RouterMapConfig> = new Map([
|
||||
]
|
||||
}
|
||||
],
|
||||
[
|
||||
'mcpService',
|
||||
{
|
||||
type: 'module',
|
||||
lazy: lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/mcpService/McpServiceContainer')),
|
||||
key: 'mcpService'
|
||||
}
|
||||
],
|
||||
[
|
||||
'mcpKey',
|
||||
{
|
||||
type: 'module',
|
||||
lazy: lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/mcpService/McpKeyContainer')),
|
||||
key: 'mcpKey'
|
||||
}
|
||||
],
|
||||
[
|
||||
'loadBalancing',
|
||||
{
|
||||
|
||||
@@ -85,7 +85,7 @@ export const SYSTEM_TABLE_COLUMNS: PageProColumns<SystemTableListItem>[] = [
|
||||
title: '服务名称',
|
||||
dataIndex: 'name',
|
||||
ellipsis: true,
|
||||
width: 160,
|
||||
width: 220,
|
||||
fixed: 'left',
|
||||
sorter: (a, b) => {
|
||||
return a.name.localeCompare(b.name)
|
||||
@@ -100,24 +100,20 @@ export const SYSTEM_TABLE_COLUMNS: PageProColumns<SystemTableListItem>[] = [
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'service_kind',
|
||||
width: 140,
|
||||
width: 120,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '所属团队',
|
||||
dataIndex: ['team', 'name'],
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
width: 140,
|
||||
dataIndex: 'state',
|
||||
ellipsis: true
|
||||
ellipsis: true,
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
title: 'API 数量',
|
||||
dataIndex: 'apiNum',
|
||||
ellipsis: true,
|
||||
width: 140,
|
||||
sorter: (a, b) => {
|
||||
return a.apiNum - b.apiNum
|
||||
}
|
||||
@@ -362,6 +358,10 @@ export const SERVICE_APPROVAL_OPTIONS = [
|
||||
{ label: '无需审核:允许任何消费者调用该服务', value: 'auto' },
|
||||
{ label: '人工审核:仅允许通过人工审核的消费者调用该服务', value: 'manual' }
|
||||
]
|
||||
export const MCP_OPTIONS = [
|
||||
{ label: '禁用', value: false },
|
||||
{ label: '开启:AI Agent 等产品能够通过 MCP 方式调用服务', value: true }
|
||||
]
|
||||
export const SERVICE_KIND_OPTIONS = [
|
||||
{ label: 'REST', value: 'rest' },
|
||||
{ label: 'AI', value: 'ai' }
|
||||
|
||||
@@ -31,6 +31,7 @@ export type SystemConfigFieldType = {
|
||||
catalogue?:string | string[];
|
||||
approvalType?:string;
|
||||
modelMapping?: string;
|
||||
enable_mcp?: boolean;
|
||||
};
|
||||
|
||||
export type SystemSubServiceTableListItem = {
|
||||
|
||||
@@ -1109,6 +1109,11 @@ p{
|
||||
width: 16px !important;
|
||||
justify-content:center;
|
||||
}
|
||||
.ant-select .ant-select-clear {
|
||||
height:16px !important;
|
||||
width: 16px !important;
|
||||
top: 45%;
|
||||
}
|
||||
|
||||
.ant-table-wrapper .ant-table{
|
||||
scrollbar-color: none !important;
|
||||
@@ -1213,6 +1218,38 @@ p{
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 将折叠图标移到右侧 */
|
||||
.service-hub-custom-switcher .ant-tree-switcher {
|
||||
position: absolute !important;
|
||||
right: 0 !important;
|
||||
left: auto !important;
|
||||
z-index: 10 !important;
|
||||
}
|
||||
|
||||
/* 设置节点内容宽度 */
|
||||
.service-hub-custom-switcher .ant-tree-node-content-wrapper {
|
||||
width: calc(100% - 24px) !important;
|
||||
padding-left: 8px !important;
|
||||
padding-right: 24px !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
/* 强制控制标题元素的样式 */
|
||||
.service-hub-custom-switcher .ant-tree-title {
|
||||
display: inline-block !important;
|
||||
width: calc(100% - 10px) !important;
|
||||
overflow: hidden !important;
|
||||
white-space: nowrap !important;
|
||||
text-overflow: ellipsis !important;
|
||||
vertical-align: middle !important;
|
||||
}
|
||||
|
||||
/* 确保选中框的间距 */
|
||||
.service-hub-custom-switcher .ant-tree-checkbox {
|
||||
margin-right: 8px !important;
|
||||
}
|
||||
|
||||
|
||||
.no-selected-tree .ant-tree-node-selected{
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
@@ -1,145 +1,203 @@
|
||||
import { Editor } from '@tinymce/tinymce-react';
|
||||
import hljs from 'highlight.js';
|
||||
import 'highlight.js/styles/default.css';
|
||||
import {useEffect, useState} from "react";
|
||||
import {BasicResponse, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx";
|
||||
import {useFetch} from "@common/hooks/http.ts";
|
||||
import {App, Button} from "antd";
|
||||
import { EntityItem } from '@common/const/type.ts';
|
||||
import WithPermission from '@common/components/aoplatform/WithPermission.tsx';
|
||||
import { RouterParams } from '@core/components/aoplatform/RenderRoutes';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { $t } from '@common/locales';
|
||||
const ServiceInsideDocument = ()=>{
|
||||
const { message } = App.useApp()
|
||||
const [updater,setUpdater] = useState<string>()
|
||||
const [updateTime,setUpdateTime]=useState<string>()
|
||||
const [initDoc, setInitDoc] = useState<string>()
|
||||
const [doc, setDoc] = useState<string>()
|
||||
const {fetchData} = useFetch()
|
||||
const { serviceId, teamId} = useParams<RouterParams>();
|
||||
import { Editor } from '@tinymce/tinymce-react'
|
||||
import hljs from 'highlight.js'
|
||||
import 'highlight.js/styles/default.css'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const.tsx'
|
||||
import { useFetch } from '@common/hooks/http.ts'
|
||||
import { App, Button } from 'antd'
|
||||
import { EntityItem } from '@common/const/type.ts'
|
||||
import WithPermission from '@common/components/aoplatform/WithPermission.tsx'
|
||||
import { RouterParams } from '@core/components/aoplatform/RenderRoutes'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { $t } from '@common/locales'
|
||||
import { useBreadcrumb } from '@common/contexts/BreadcrumbContext'
|
||||
const ServiceInsideDocument = () => {
|
||||
const { message } = App.useApp()
|
||||
const [updater, setUpdater] = useState<string>()
|
||||
const [updateTime, setUpdateTime] = useState<string>()
|
||||
const [initDoc, setInitDoc] = useState<string>()
|
||||
const [doc, setDoc] = useState<string>()
|
||||
const { fetchData } = useFetch()
|
||||
const { serviceId, teamId } = useParams<RouterParams>()
|
||||
const { setBreadcrumb } = useBreadcrumb()
|
||||
const navigator = useNavigate()
|
||||
|
||||
const save = ()=>{
|
||||
fetchData<BasicResponse<{service:{ id:string,name:string,updater:string,updateTime:string, doc:string} }>>('service/doc',{method:'PUT',eoBody:({doc:doc}) ,eoParams:{service:serviceId,team:teamId},eoTransformKeys:['update_time']}).then(response=>{
|
||||
const {code,msg} = response
|
||||
if(code === STATUS_CODE.SUCCESS){
|
||||
message.success(msg || $t(RESPONSE_TIPS.success))
|
||||
getServiceDoc()
|
||||
}else{
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleEditorChange = (content:string, editor:unknown) => {
|
||||
setDoc(content)
|
||||
};
|
||||
const setupEditor = (editor:unknown) => {
|
||||
editor.on('init', () => {
|
||||
editor.contentDocument.querySelectorAll('pre code').forEach((block:HTMLElement) => {
|
||||
hljs.highlightBlock(block);
|
||||
});
|
||||
});
|
||||
|
||||
editor.on('SetContent', () => {
|
||||
editor.contentDocument.querySelectorAll('pre code').forEach((block:HTMLElement) => {
|
||||
hljs.highlightBlock(block);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const getServiceDoc = ()=>{
|
||||
fetchData<BasicResponse<{doc:{ id:string,name:string,updater:EntityItem,updateTime:string,creater:EntityItem, doc:string} }>>('service/doc',{method:'GET',eoParams:{service:serviceId,team:teamId},eoTransformKeys:['update_time']}).then(response=>{
|
||||
const {code,data,msg} = response
|
||||
if(code === STATUS_CODE.SUCCESS){
|
||||
setUpdater(data.doc.updater.id === '' ? '-' : data.doc.updater.name)
|
||||
setUpdateTime(data.doc.updater.id === '' ? '-' : data.doc.updateTime)
|
||||
setInitDoc(data.doc.doc)
|
||||
}else{
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const save = () => {
|
||||
fetchData<
|
||||
BasicResponse<{ service: { id: string; name: string; updater: string; updateTime: string; doc: string } }>
|
||||
>('service/doc', {
|
||||
method: 'PUT',
|
||||
eoBody: { doc: doc },
|
||||
eoParams: { service: serviceId, team: teamId },
|
||||
eoTransformKeys: ['update_time']
|
||||
}).then((response) => {
|
||||
const { code, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
message.success(msg || $t(RESPONSE_TIPS.success))
|
||||
getServiceDoc()
|
||||
}, []);
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full border-[1px] rounded-[10px] border-BORDER border-solid mr-PAGE_INSIDE_X">
|
||||
<Editor
|
||||
tinymceScriptSrc={'/tinymce/tinymce.min.js'}
|
||||
initialValue={initDoc}
|
||||
init={{
|
||||
height: '100%',
|
||||
menubar: false,
|
||||
plugins: [
|
||||
'advlist', 'autolink', 'link', 'image', 'lists', 'charmap', 'preview', 'anchor', 'pagebreak',
|
||||
'searchreplace', 'wordcount', 'visualblocks', 'visualchars', 'codesample', 'fullscreen', 'insertdatetime',
|
||||
'media', 'table', 'emoticons', 'help'
|
||||
], 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; } img { max-width: 100%; }',
|
||||
setup: setupEditor,
|
||||
codesample_languages:[
|
||||
{
|
||||
text: 'HTML/XML',
|
||||
value: 'markup'
|
||||
},
|
||||
{
|
||||
text: 'JavaScript',
|
||||
value: 'javascript'
|
||||
},
|
||||
{
|
||||
text: 'CSS',
|
||||
value: 'css'
|
||||
},
|
||||
{
|
||||
text: 'PHP',
|
||||
value: 'php'
|
||||
},
|
||||
{
|
||||
text: 'Ruby',
|
||||
value: 'ruby'
|
||||
},
|
||||
{
|
||||
text:'GO',
|
||||
value:'go'
|
||||
},
|
||||
{
|
||||
text: 'Python',
|
||||
value: 'python'
|
||||
},
|
||||
{
|
||||
text: 'Java',
|
||||
value: 'java'
|
||||
},
|
||||
{
|
||||
text: 'C',
|
||||
value: 'c'
|
||||
},
|
||||
{
|
||||
text: 'C#',
|
||||
value: 'csharp'
|
||||
},
|
||||
{
|
||||
text: 'C++',
|
||||
value: 'cpp'
|
||||
},
|
||||
{ text: 'Bash/Shell', value: 'bash' },
|
||||
{ text: 'SQL', value: 'sql' }
|
||||
]
|
||||
}}
|
||||
onEditorChange={handleEditorChange}
|
||||
/>
|
||||
|
||||
<div className=" pl-[8px] py-btnbase ">
|
||||
<div className="flex justify-between items-center">
|
||||
<p className="text-[14px] leading-[20px] text-[#999999]"><span className="mr-[20px]">{$t('最近一次更新者')}:{updater || '-'}</span><span>{$t('最近一次更新时间')}:{updateTime || '-'}</span></p>
|
||||
<WithPermission access="team.service.service_intro.edit"><Button type="primary" className="mr-btnbase" onClick={save}>{$t('保存')}</Button></WithPermission>
|
||||
</div>
|
||||
</div>
|
||||
</div>)
|
||||
const handleEditorChange = (content: string, editor: unknown) => {
|
||||
setDoc(content)
|
||||
}
|
||||
const setupEditor = (editor: unknown) => {
|
||||
editor.on('init', () => {
|
||||
editor.contentDocument.querySelectorAll('pre code').forEach((block: HTMLElement) => {
|
||||
hljs.highlightBlock(block)
|
||||
})
|
||||
})
|
||||
|
||||
editor.on('SetContent', () => {
|
||||
editor.contentDocument.querySelectorAll('pre code').forEach((block: HTMLElement) => {
|
||||
hljs.highlightBlock(block)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const getServiceDoc = () => {
|
||||
fetchData<
|
||||
BasicResponse<{
|
||||
doc: { id: string; name: string; updater: EntityItem; updateTime: string; creater: EntityItem; doc: string }
|
||||
}>
|
||||
>('service/doc', {
|
||||
method: 'GET',
|
||||
eoParams: { service: serviceId, team: teamId },
|
||||
eoTransformKeys: ['update_time']
|
||||
}).then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setUpdater(data.doc.updater.id === '' ? '-' : data.doc.updater.name)
|
||||
setUpdateTime(data.doc.updater.id === '' ? '-' : data.doc.updateTime)
|
||||
setInitDoc(data.doc.doc)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setBreadcrumb([
|
||||
{
|
||||
title: $t('服务'),
|
||||
onClick: () => navigator('/service/list')
|
||||
},
|
||||
{
|
||||
title: $t('使用说明')
|
||||
}
|
||||
])
|
||||
getServiceDoc()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full border-[1px] rounded-[10px] border-BORDER border-solid mr-PAGE_INSIDE_X">
|
||||
<Editor
|
||||
tinymceScriptSrc={'/tinymce/tinymce.min.js'}
|
||||
initialValue={initDoc}
|
||||
init={{
|
||||
height: '100%',
|
||||
menubar: false,
|
||||
plugins: [
|
||||
'advlist',
|
||||
'autolink',
|
||||
'link',
|
||||
'image',
|
||||
'lists',
|
||||
'charmap',
|
||||
'preview',
|
||||
'anchor',
|
||||
'pagebreak',
|
||||
'searchreplace',
|
||||
'wordcount',
|
||||
'visualblocks',
|
||||
'visualchars',
|
||||
'codesample',
|
||||
'fullscreen',
|
||||
'insertdatetime',
|
||||
'media',
|
||||
'table',
|
||||
'emoticons',
|
||||
'help'
|
||||
],
|
||||
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; } img { max-width: 100%; }',
|
||||
setup: setupEditor,
|
||||
codesample_languages: [
|
||||
{
|
||||
text: 'HTML/XML',
|
||||
value: 'markup'
|
||||
},
|
||||
{
|
||||
text: 'JavaScript',
|
||||
value: 'javascript'
|
||||
},
|
||||
{
|
||||
text: 'CSS',
|
||||
value: 'css'
|
||||
},
|
||||
{
|
||||
text: 'PHP',
|
||||
value: 'php'
|
||||
},
|
||||
{
|
||||
text: 'Ruby',
|
||||
value: 'ruby'
|
||||
},
|
||||
{
|
||||
text: 'GO',
|
||||
value: 'go'
|
||||
},
|
||||
{
|
||||
text: 'Python',
|
||||
value: 'python'
|
||||
},
|
||||
{
|
||||
text: 'Java',
|
||||
value: 'java'
|
||||
},
|
||||
{
|
||||
text: 'C',
|
||||
value: 'c'
|
||||
},
|
||||
{
|
||||
text: 'C#',
|
||||
value: 'csharp'
|
||||
},
|
||||
{
|
||||
text: 'C++',
|
||||
value: 'cpp'
|
||||
},
|
||||
{ text: 'Bash/Shell', value: 'bash' },
|
||||
{ text: 'SQL', value: 'sql' }
|
||||
]
|
||||
}}
|
||||
onEditorChange={handleEditorChange}
|
||||
/>
|
||||
|
||||
<div className=" pl-[8px] py-btnbase ">
|
||||
<div className="flex justify-between items-center">
|
||||
<p className="text-[14px] leading-[20px] text-[#999999]">
|
||||
<span className="mr-[20px]">
|
||||
{$t('最近一次更新者')}:{updater || '-'}
|
||||
</span>
|
||||
<span>
|
||||
{$t('最近一次更新时间')}:{updateTime || '-'}
|
||||
</span>
|
||||
</p>
|
||||
<WithPermission access="team.service.service_intro.edit">
|
||||
<Button type="primary" className="mr-btnbase" onClick={save}>
|
||||
{$t('保存')}
|
||||
</Button>
|
||||
</WithPermission>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default ServiceInsideDocument
|
||||
export default ServiceInsideDocument
|
||||
|
||||
@@ -221,6 +221,10 @@ const AiServiceInsidePage: FC = () => {
|
||||
useEffect(() => {
|
||||
serviceId && getAiServiceInfo()
|
||||
}, [serviceId])
|
||||
// 创建一个回调函数
|
||||
const onSaveCallback = () => {
|
||||
getAiServiceInfo()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -228,6 +232,7 @@ const AiServiceInsidePage: FC = () => {
|
||||
<InsidePage
|
||||
pageTitle={aiServiceInfo?.name || '-'}
|
||||
tagList={[
|
||||
...(aiServiceInfo?.enable_mcp ? [{ label: 'MCP', color: '#FFF0C1', className: 'text-[#000]' }] : []),
|
||||
{
|
||||
label: (
|
||||
<Paragraph className="mb-0" copyable={serviceId ? { text: serviceId } : false}>
|
||||
@@ -250,7 +255,7 @@ const AiServiceInsidePage: FC = () => {
|
||||
<div
|
||||
className={` ${['setting', 'upstream'].indexOf(activeMenu!) !== -1 ? '' : ''} w-full h-full flex flex-1 flex-col overflow-auto bg-MAIN_BG pt-[20px] pl-[20px] pb-PAGE_INSIDE_B `}
|
||||
>
|
||||
<Outlet />
|
||||
<Outlet context={{ onSaveComplete: onSaveCallback }} />
|
||||
</div>
|
||||
</div>
|
||||
</InsidePage>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {ActionType} from "@ant-design/pro-components";
|
||||
import {FC, forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState} from "react";
|
||||
import {Link, useParams} from "react-router-dom";
|
||||
import {Link, useNavigate, useParams} from "react-router-dom";
|
||||
import {App, Form,TreeSelect} from "antd";
|
||||
import {useBreadcrumb} from "@common/contexts/BreadcrumbContext.tsx";
|
||||
import {useFetch} from "@common/hooks/http.ts";
|
||||
@@ -26,6 +26,7 @@ const AiServiceInsideSubscriber:FC = ()=>{
|
||||
const pageListRef = useRef<ActionType>(null);
|
||||
const [memberValueEnum, setMemberValueEnum] = useState<SimpleMemberItem[]>([])
|
||||
const {accessData,state} = useGlobalContext()
|
||||
const navigator = useNavigate()
|
||||
const getAiServiceSubscriber = ()=>{
|
||||
return fetchData<BasicResponse<{subscribers:AiServiceSubscriberTableListItem[]}>>('service/subscribers',{method:'GET',eoParams:{service:serviceId,team:teamId},eoTransformKeys:['apply_time']}).then(response=>{
|
||||
const {code,data,msg} = response
|
||||
@@ -121,7 +122,8 @@ const AiServiceInsideSubscriber:FC = ()=>{
|
||||
useEffect(() => {
|
||||
setBreadcrumb([
|
||||
{
|
||||
title:<Link to={`/service/list`}>{$t('服务')}</Link>
|
||||
title: $t('服务'),
|
||||
onClick: () => navigator('/service/list')
|
||||
},
|
||||
{
|
||||
title:$t('订阅方管理')
|
||||
|
||||
@@ -1,58 +1,84 @@
|
||||
|
||||
import {forwardRef, useEffect, useState} from "react";
|
||||
import { Empty, Spin, message} from "antd";
|
||||
import {BasicResponse, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx";
|
||||
import {useFetch} from "@common/hooks/http.ts";
|
||||
import { LoadingOutlined } from "@ant-design/icons";
|
||||
import { forwardRef, useEffect, useState } from 'react'
|
||||
import { Empty, Spin, message } from 'antd'
|
||||
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const.tsx'
|
||||
import { useFetch } from '@common/hooks/http.ts'
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import EmptySVG from '@common/assets/empty.svg'
|
||||
import { $t } from "@common/locales/index.ts";
|
||||
import { $t } from '@common/locales/index.ts'
|
||||
import ApiDocument from '@common/components/aoplatform/ApiDocument.tsx'
|
||||
import { useParams } from "react-router-dom";
|
||||
import { RouterParams } from "@core/components/aoplatform/RenderRoutes.tsx";
|
||||
import { AiServiceInsideApiDocumentHandle, AiServiceInsideApiDocumentProps, AiServiceApiDetail } from "@core/const/ai-service/type.ts";
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { RouterParams } from '@core/components/aoplatform/RenderRoutes.tsx'
|
||||
import {
|
||||
AiServiceInsideApiDocumentHandle,
|
||||
AiServiceInsideApiDocumentProps,
|
||||
AiServiceApiDetail
|
||||
} from '@core/const/ai-service/type.ts'
|
||||
import { useBreadcrumb } from '@common/contexts/BreadcrumbContext'
|
||||
|
||||
const AiServiceInsideApiDocument = forwardRef<AiServiceInsideApiDocumentHandle,AiServiceInsideApiDocumentProps>(() => {
|
||||
const {serviceId, teamId} = useParams<RouterParams>()
|
||||
const {fetchData} = useFetch()
|
||||
const [apiDetail, setApiDetail] = useState<AiServiceApiDetail>()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
useEffect(() => {
|
||||
getApiDetail()
|
||||
}, []);
|
||||
const AiServiceInsideApiDocument = forwardRef<AiServiceInsideApiDocumentHandle, AiServiceInsideApiDocumentProps>(() => {
|
||||
const { serviceId, teamId } = useParams<RouterParams>()
|
||||
const { fetchData } = useFetch()
|
||||
const [apiDetail, setApiDetail] = useState<AiServiceApiDetail>()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const { setBreadcrumb } = useBreadcrumb()
|
||||
const navigator = useNavigate()
|
||||
|
||||
const getApiDetail = ()=>{
|
||||
setLoading(true)
|
||||
fetchData<BasicResponse<{doc:AiServiceApiDetail}>>('service/api_doc',{method:'GET',eoParams:{service:serviceId,team:teamId },eoTransformKeys:['update_time']}).then(response=>{
|
||||
const {code,data,msg} = response
|
||||
if(code === STATUS_CODE.SUCCESS){
|
||||
setApiDetail(data.doc?.content)
|
||||
}else{
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
}).finally(()=>{setLoading(false)})
|
||||
}
|
||||
useEffect(() => {
|
||||
setBreadcrumb([
|
||||
{
|
||||
title: $t('服务'),
|
||||
onClick: () => navigator('/service/list')
|
||||
},
|
||||
{
|
||||
title: $t('API 文档')
|
||||
}
|
||||
])
|
||||
getApiDetail()
|
||||
}, [])
|
||||
|
||||
const getApiDetail = () => {
|
||||
setLoading(true)
|
||||
fetchData<BasicResponse<{ doc: AiServiceApiDetail }>>('service/api_doc', {
|
||||
method: 'GET',
|
||||
eoParams: { service: serviceId, team: teamId },
|
||||
eoTransformKeys: ['update_time']
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setApiDetail(data.doc?.content)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
const ApiPreview = ({spec}:{spec?:string | object})=>{
|
||||
return (
|
||||
<div className="h-full overflow-hidden">
|
||||
<div className="flex-1 h-full overflow-auto pr-PAGE_INSIDE_X">
|
||||
<ApiDocument spec={spec}/>
|
||||
</div>
|
||||
const ApiPreview = ({ spec }: { spec?: string | object }) => {
|
||||
return (
|
||||
<div className="h-full overflow-hidden">
|
||||
<div className="flex-1 h-full overflow-auto pr-PAGE_INSIDE_X">
|
||||
<ApiDocument spec={spec} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
return (<>
|
||||
<Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} spinning={loading} wrapperClassName=' h-full overflow-hidden '>
|
||||
<div className=" h-full ai-service-api-preview">
|
||||
{ apiDetail ? <ApiPreview spec={apiDetail} />
|
||||
: <Empty image={EmptySVG} >
|
||||
</Empty>}
|
||||
</div>
|
||||
</Spin>
|
||||
</>)
|
||||
return (
|
||||
<>
|
||||
<Spin
|
||||
indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />}
|
||||
spinning={loading}
|
||||
wrapperClassName=" h-full overflow-hidden "
|
||||
>
|
||||
<div className=" h-full ai-service-api-preview">
|
||||
{apiDetail ? <ApiPreview spec={apiDetail} /> : <Empty image={EmptySVG}></Empty>}
|
||||
</div>
|
||||
</Spin>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
export default AiServiceInsideApiDocument
|
||||
export default AiServiceInsideApiDocument
|
||||
|
||||
@@ -21,6 +21,7 @@ import { MutableRefObject, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import AiServiceRouterModelConfig, { AiServiceRouterModelConfigHandle } from './AiServiceInsideRouterModelConfig'
|
||||
import { AiProviderDefaultConfig, AiProviderLlmsItems } from '@core/pages/aiSetting/types'
|
||||
import { useBreadcrumb } from '@common/contexts/BreadcrumbContext'
|
||||
|
||||
type AiServiceRouterField = {
|
||||
name: string
|
||||
@@ -66,6 +67,7 @@ const AiServiceInsideRouterCreate = () => {
|
||||
const [variablesTableRef, setVariablesTableRef] = useState<MutableRefObject<EditableFormInstance<T> | undefined>>()
|
||||
const { state } = useGlobalContext()
|
||||
const [resultPath, setResultPath] = useState<string>('')
|
||||
const { setBreadcrumb } = useBreadcrumb()
|
||||
|
||||
const onFinish = () => {
|
||||
return variablesTableRef?.current
|
||||
@@ -289,6 +291,19 @@ const AiServiceInsideRouterCreate = () => {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setBreadcrumb([
|
||||
{
|
||||
title: $t('服务'),
|
||||
onClick: () => navigator('/service/list')
|
||||
},
|
||||
{
|
||||
title:$t('API'),
|
||||
onClick: () => navigator(backUrl)
|
||||
},
|
||||
{
|
||||
title: routeId ? $t('编辑 API') : $t('添加 API')
|
||||
}
|
||||
])
|
||||
!routeId && aiServiceInfo?.provider && getDefaultModelConfig()
|
||||
}, [aiServiceInfo])
|
||||
|
||||
@@ -363,11 +378,6 @@ const AiServiceInsideRouterCreate = () => {
|
||||
setDrawerType(undefined)
|
||||
}
|
||||
|
||||
const apiPathMatchRulesOptions = useMemo(
|
||||
() => API_PATH_MATCH_RULES.map((x) => ({ label: $t(x.label), value: x.value })),
|
||||
[state.language]
|
||||
)
|
||||
|
||||
return (
|
||||
<InsidePage
|
||||
pageTitle={$t('AI 路由设置') || '-'}
|
||||
|
||||
@@ -158,18 +158,21 @@ const AiServiceInsideRouterList: FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getMemberList()
|
||||
manualReloadTable()
|
||||
}, [serviceId])
|
||||
useEffect(() => {
|
||||
setBreadcrumb([
|
||||
{
|
||||
title: <Link to={`/service/list`}>{$t('服务')}</Link>
|
||||
title: $t('服务'),
|
||||
onClick: () => navigator('/service/list')
|
||||
},
|
||||
{
|
||||
title: $t('路由')
|
||||
}
|
||||
])
|
||||
getMemberList()
|
||||
manualReloadTable()
|
||||
}, [serviceId])
|
||||
}, [state.language])
|
||||
|
||||
const columns = useMemo(() => {
|
||||
return [...AI_SERVICE_ROUTER_TABLE_COLUMNS].map((x) => {
|
||||
|
||||
@@ -163,6 +163,8 @@ const AiServiceRouterModelConfig = forwardRef<AiServiceRouterModelConfigHandle,
|
||||
>
|
||||
<Form.Item<AiServiceRouterModelConfigField> label={$t('模型类型')} name="type" rules={[{ required: true }]}>
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.select)}
|
||||
options={modelTypeList}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
import {ActionType} from "@ant-design/pro-components";
|
||||
import {FC, useEffect, useMemo, useRef, useState} from "react";
|
||||
import {Link, useLocation, useParams} from "react-router-dom";
|
||||
import {Link, useLocation, useNavigate, useParams} from "react-router-dom";
|
||||
import PageList, { PageProColumns } from "@common/components/aoplatform/PageList.tsx";
|
||||
import {useBreadcrumb} from "@common/contexts/BreadcrumbContext.tsx";
|
||||
import {App, Button} from "antd";
|
||||
@@ -40,6 +40,7 @@ const AiServiceInsideApprovalList:FC = ()=>{
|
||||
const [approvalBtnLoading,setApprovalBtnLoading] = useState<boolean>(false)
|
||||
const [memberValueEnum, setMemberValueEnum] = useState<SimpleMemberItem[]>([])
|
||||
const {accessData,state} = useGlobalContext()
|
||||
const navigator = useNavigate()
|
||||
|
||||
const openModal = async (type:'approval'|'view',entity:SubscribeApprovalTableListItem)=>{
|
||||
message.loading($t(RESPONSE_TIPS.loading))
|
||||
@@ -143,7 +144,8 @@ const AiServiceInsideApprovalList:FC = ()=>{
|
||||
useEffect(() => {
|
||||
setBreadcrumb([
|
||||
{
|
||||
title:<Link to={`/service/list`}>{$t('服务')}</Link>
|
||||
title: $t('服务'),
|
||||
onClick: () => navigator('/service/list')
|
||||
},
|
||||
{
|
||||
title:$t('订阅审核')
|
||||
|
||||
@@ -28,7 +28,8 @@ const AiServiceInsidePublic:FC = ()=>{
|
||||
useEffect(() => {
|
||||
setBreadcrumb([
|
||||
{
|
||||
title:<Link to={`/service/list`}>{$t('服务')}</Link>
|
||||
title: $t('服务'),
|
||||
onClick: () => navigateTo('/service/list')
|
||||
},
|
||||
{
|
||||
title:$t('发布')
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ActionType, ParamsType } from "@ant-design/pro-components";
|
||||
import { App, Button, Divider } from "antd";
|
||||
import { useState, useRef, useEffect, useMemo, FC } from "react";
|
||||
import { useParams, Link, useLocation } from "react-router-dom";
|
||||
import { useParams, Link, useLocation, useNavigate } from "react-router-dom";
|
||||
import PageList, { PageProColumns } from "@common/components/aoplatform/PageList";
|
||||
import { PublishApprovalModalContent } from "@common/components/aoplatform/PublishApprovalModalContent";
|
||||
import { RouterParams } from "@core/components/aoplatform/RenderRoutes";
|
||||
@@ -45,6 +45,7 @@ const AiServiceInsidePublicList:FC = ()=>{
|
||||
const [drawerData, setDrawerData] = useState<PublishTableListItem|PublishVersionTableListItem >({} as PublishTableListItem)
|
||||
const [drawerOkTitle, setDrawerOkTitle] = useState<string>('确认')
|
||||
const [isOkToPublish, setIsOkToPublish] = useState<boolean>(false)
|
||||
const navigator = useNavigate()
|
||||
const getAiServicePublishList = (params?: ParamsType & {
|
||||
pageSize?: number | undefined;
|
||||
current?: number | undefined;
|
||||
@@ -352,10 +353,11 @@ const AiServiceInsidePublicList:FC = ()=>{
|
||||
useEffect(() => {
|
||||
setBreadcrumb([
|
||||
{
|
||||
title:<Link to={`/service/list`}>{$t('服务')}</Link>
|
||||
title: $t('服务'),
|
||||
onClick: () => navigator('/service/list')
|
||||
},
|
||||
{
|
||||
title:$t('发布')
|
||||
title: $t('发布')
|
||||
}
|
||||
])
|
||||
getMemberList()
|
||||
|
||||
@@ -390,6 +390,8 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
|
||||
{source === 'guide' && (
|
||||
<Form.Item label={$t('所属团队')} name="team" className="mt-[16px]" rules={[{ required: true }]}>
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.input)}
|
||||
options={teamList}
|
||||
|
||||
@@ -86,9 +86,9 @@ export default function ApiRequestSetting() {
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<ApiRequestSettingFieldType>
|
||||
label={$t('集成地址')}
|
||||
label={$t('OpenAPI & MCP 调用地址')}
|
||||
name="sitePrefix"
|
||||
rules={[{ whitespace: true }]}
|
||||
rules={[{ required: true, whitespace: true }]}
|
||||
extra={$t('与外部平台集成时,获取 API 市场中文档信息的域名')}
|
||||
>
|
||||
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} />
|
||||
|
||||
@@ -181,6 +181,8 @@ const LocalAiDeploy = forwardRef<LocalAiDeployHandle, any>((props: any, ref: any
|
||||
</Form.Item>
|
||||
<Form.Item label={$t('所属团队')} name="team" className="mt-[16px]" rules={[{ required: true }]}>
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.input)}
|
||||
options={teamList}
|
||||
|
||||
@@ -106,6 +106,8 @@ const RestAIDeploy = forwardRef<RestAIDeployHandle, any>((props: any, ref: any)
|
||||
</Form.Item>
|
||||
<Form.Item label={$t('所属团队')} name="team" className="mt-[16px]" rules={[{ required: true }]}>
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.input)}
|
||||
options={teamList}
|
||||
|
||||
@@ -171,6 +171,8 @@ const AddLoadBalancingModel = forwardRef<LoadBalancingHandle>((props, ref: any)
|
||||
>
|
||||
<Form.Item<LoadModelDetailData> label={$t('模型类型')} name="type" rules={[{ required: true }]}>
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.select)}
|
||||
options={modelTypeList}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { App, Form, Input } 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 { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import { forwardRef, useEffect, useImperativeHandle } from 'react'
|
||||
type modelFieldType = {
|
||||
name: string
|
||||
type: string
|
||||
model_parameters: string
|
||||
access_configuration: string
|
||||
}
|
||||
|
||||
export type addMcpKeysHandle = {
|
||||
save: () => Promise<boolean | string>
|
||||
}
|
||||
|
||||
type addMcpKeysProps = {
|
||||
name?: string
|
||||
value?: string
|
||||
type?: string
|
||||
apikey?: string
|
||||
}
|
||||
|
||||
const AddMcpKey = forwardRef<addMcpKeysHandle, addMcpKeysProps>((props, ref) => {
|
||||
const { name = '', value: editValue = '', type = 'new', apikey = '' } = props
|
||||
const [form] = Form.useForm()
|
||||
const { message } = App.useApp()
|
||||
const { fetchData } = useFetch()
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue({
|
||||
name,
|
||||
value: editValue
|
||||
})
|
||||
}, [])
|
||||
/**
|
||||
* 保存
|
||||
* @returns
|
||||
*/
|
||||
const save: () => Promise<boolean | string> = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
form
|
||||
.validateFields()
|
||||
.then((value) => {
|
||||
console.log('value', value)
|
||||
const finalValue = {
|
||||
...value,
|
||||
value: editValue ? editValue : uuidv4(),
|
||||
expired: 0
|
||||
}
|
||||
fetchData<BasicResponse<any>>('system/apikey', {
|
||||
method: type === 'new' ? 'POST' : 'PUT',
|
||||
eoBody: finalValue,
|
||||
...(type === 'edit' ? {
|
||||
eoParams: { apikey }
|
||||
} : {})
|
||||
})
|
||||
.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="mcpKeyModalConfig"
|
||||
autoComplete="off"
|
||||
>
|
||||
<Form.Item<modelFieldType> label={$t('名称')} name="name" rules={[{ required: true }]}>
|
||||
<Input autoFocus className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)
|
||||
})
|
||||
|
||||
export default AddMcpKey
|
||||
@@ -0,0 +1,601 @@
|
||||
import { App, Button, Card, CascaderProps, Empty, Select } from 'antd'
|
||||
import { $t } from '@common/locales/index.ts'
|
||||
import { Icon } from '@iconify/react/dist/iconify.js'
|
||||
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'
|
||||
import ReactJson from 'react-json-view'
|
||||
import { IconButton } from '@common/components/postcat/api/IconButton'
|
||||
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
|
||||
import { useFetch } from '@common/hooks/http'
|
||||
import { useConnection } from './hook/useConnection'
|
||||
import { ClientRequest, Tool, ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js'
|
||||
import { z } from 'zod'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { ServiceDetailType } from '@market/const/serviceHub/type'
|
||||
import useCopyToClipboard from '@common/hooks/copy'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
|
||||
import { Cascader } from 'antd/lib'
|
||||
|
||||
type ConfigList = {
|
||||
openApi?: {
|
||||
title: string
|
||||
configContent: string
|
||||
apiKeys: string[]
|
||||
}
|
||||
mcp: {
|
||||
title: string
|
||||
configContent: string
|
||||
apiKeys: string[]
|
||||
}
|
||||
}
|
||||
|
||||
type ApiKeyItem = {
|
||||
expired: number
|
||||
id: string
|
||||
name: string
|
||||
value: string
|
||||
}
|
||||
interface Option {
|
||||
value: string
|
||||
label: string
|
||||
children?: Option[]
|
||||
}
|
||||
|
||||
type ServiceApiKeyList = {
|
||||
id: string
|
||||
name: string
|
||||
apikeys: Array<{
|
||||
id: string
|
||||
name: string
|
||||
value: string
|
||||
expired: number
|
||||
}>
|
||||
}
|
||||
export interface IntegrationAIContainerRef {
|
||||
getServiceKeysList: () => void;
|
||||
}
|
||||
export interface IntegrationAIContainerProps {
|
||||
type: 'global' | 'service'
|
||||
handleToolsChange: (value: Tool[]) => void
|
||||
customClassName?: string
|
||||
service?: ServiceDetailType
|
||||
serviceId?: string
|
||||
currentTab?: string
|
||||
openModal?: (type: 'apply') => void
|
||||
}
|
||||
export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, IntegrationAIContainerProps>(
|
||||
({
|
||||
type,
|
||||
handleToolsChange,
|
||||
customClassName,
|
||||
service,
|
||||
serviceId,
|
||||
currentTab,
|
||||
openModal
|
||||
}: IntegrationAIContainerProps, ref) => {
|
||||
/** 当前激活的标签 */
|
||||
const [activeTab, setActiveTab] = useState(type === 'service' ? 'openApi' : 'mcp')
|
||||
/** 弹窗组件 */
|
||||
const { message } = App.useApp()
|
||||
/** 配置内容 */
|
||||
const [configContent, setConfigContent] = useState<string>('')
|
||||
/** 当前选中 API Key */
|
||||
const [apiKey, setApiKey] = useState<string>('')
|
||||
/** API Key 列表 */
|
||||
const [apiKeyList, setApiKeyList] = useState<any[]>([])
|
||||
/** Cascader Key 列表 */
|
||||
const [cascaderKeyList, setCascaderKeyList] = useState<string[]>([])
|
||||
/** MCP 服务器地址 */
|
||||
const [mcpServerUrl, setMcpServerUrl] = useState<string>('')
|
||||
/** 全局状态 */
|
||||
const { state } = useGlobalContext()
|
||||
const navigator = useNavigate()
|
||||
/** 复制组件 */
|
||||
const { copyToClipboard } = useCopyToClipboard()
|
||||
/** 错误提示 */
|
||||
const [errors, setErrors] = useState<Record<string, string | null>>({
|
||||
resources: null,
|
||||
prompts: null,
|
||||
tools: null
|
||||
})
|
||||
/** 标签内容 */
|
||||
const [tabContent, setTabContent] = useState<ConfigList>({
|
||||
mcp: {
|
||||
title: $t('MCP 配置'),
|
||||
configContent: '',
|
||||
apiKeys: []
|
||||
}
|
||||
})
|
||||
/** HTTP 请求 */
|
||||
const { fetchData } = useFetch()
|
||||
|
||||
/**
|
||||
* 初始化标签数据
|
||||
*/
|
||||
const initTabsData = () => {
|
||||
const params: ConfigList = {
|
||||
mcp: {
|
||||
title: $t('MCP 配置'),
|
||||
configContent: service?.mcpAccessConfig || '',
|
||||
apiKeys: []
|
||||
}
|
||||
}
|
||||
if (type === 'service') {
|
||||
params.openApi = {
|
||||
title: $t('Open API 文档'),
|
||||
configContent: service?.openapiAddress || '',
|
||||
apiKeys: []
|
||||
}
|
||||
}
|
||||
setTabContent(params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制
|
||||
* @param value
|
||||
* @returns
|
||||
*/
|
||||
const handleCopy = async (value: string): Promise<void> => {
|
||||
if (value) {
|
||||
copyToClipboard(value)
|
||||
message.success($t(RESPONSE_TIPS.copySuccess))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择 API Key
|
||||
* @param value
|
||||
*/
|
||||
const handleSelectChange = (value: string) => {
|
||||
setApiKey(value)
|
||||
}
|
||||
/**
|
||||
* Cascader 选择
|
||||
* @param value
|
||||
*/
|
||||
const handleCascaderChange: CascaderProps<Option>['onChange'] = (value) => {
|
||||
setApiKey(value.at(-1) || '')
|
||||
setCascaderKeyList(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取全局 MCP 配置
|
||||
* @returns
|
||||
*/
|
||||
const getGlobalMcpConfig = () => {
|
||||
fetchData<BasicResponse<null>>('global/mcp/config', {
|
||||
method: 'GET'
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, msg, data } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setTabContent((prevTabContent) => ({
|
||||
...prevTabContent,
|
||||
mcp: {
|
||||
...prevTabContent.mcp,
|
||||
configContent: data.config || ''
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
message.error(errorInfo || $t(RESPONSE_TIPS.error))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局 MCP 跳转
|
||||
*/
|
||||
const addKey = () => {
|
||||
navigator('/mcpKey')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取全局 API Key 列表
|
||||
*/
|
||||
const getGlobalKeysList = () => {
|
||||
fetchData<BasicResponse<null>>('simple/system/apikeys', {
|
||||
method: 'GET'
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, msg, data } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
if (data.apikeys && data.apikeys.length > 0) {
|
||||
setApiKeyList(
|
||||
data.apikeys.map((item: ApiKeyItem) => {
|
||||
return {
|
||||
label: item.name,
|
||||
value: item.value
|
||||
}
|
||||
})
|
||||
)
|
||||
setApiKey(data.apikeys[0].value)
|
||||
}
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
message.error(errorInfo || $t(RESPONSE_TIPS.error))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 抛出获取服务 API Key 列表
|
||||
*/
|
||||
useImperativeHandle(ref, () => ({
|
||||
getServiceKeysList
|
||||
}))
|
||||
|
||||
/**
|
||||
* 获取服务 API Key 列表
|
||||
*/
|
||||
const getServiceKeysList = () => {
|
||||
fetchData<BasicResponse<null>>(`my/app/apikeys`, {
|
||||
method: 'GET',
|
||||
eoParams: { service: serviceId }
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, msg, data } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
if (data.apps && data.apps.length > 0) {
|
||||
// 转换数据结构为 Cascader 所需格式
|
||||
const transformedData = data.apps.map((app: ServiceApiKeyList) => ({
|
||||
value: app.id,
|
||||
label: app.name,
|
||||
children: app.apikeys.map((key) => ({
|
||||
...key,
|
||||
label: key.name
|
||||
}))
|
||||
}))
|
||||
setApiKeyList(transformedData)
|
||||
if (data.apps[0].apikeys?.length) {
|
||||
setApiKey(data.apps[0].apikeys[0].value)
|
||||
setCascaderKeyList([data.apps[0].id, data.apps[0].apikeys[0].value])
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
message.error(errorInfo || $t(RESPONSE_TIPS.error))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除错误提示
|
||||
*/
|
||||
const clearError = (tabKey: keyof typeof errors) => {
|
||||
setErrors((prev) => ({ ...prev, [tabKey]: null }))
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送请求
|
||||
*/
|
||||
const makeRequest = async <T extends z.ZodType>(request: ClientRequest, schema: T, tabKey?: keyof typeof errors) => {
|
||||
try {
|
||||
const response = await makeConnectionRequest(request, schema)
|
||||
if (tabKey !== undefined) {
|
||||
clearError(tabKey)
|
||||
}
|
||||
return response
|
||||
} catch (e) {
|
||||
const errorString = (e as Error).message ?? String(e)
|
||||
if (tabKey !== undefined) {
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
[tabKey]: errorString
|
||||
}))
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 MCP 的 tools
|
||||
*/
|
||||
const listTools = async () => {
|
||||
const response = await makeRequest(
|
||||
{
|
||||
method: 'tools/list' as const,
|
||||
params: {}
|
||||
},
|
||||
ListToolsResultSchema,
|
||||
'tools'
|
||||
)
|
||||
handleToolsChange(response.tools)
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化连接 mcp
|
||||
*/
|
||||
const {
|
||||
connectionStatus,
|
||||
serverCapabilities,
|
||||
mcpClient,
|
||||
requestHistory,
|
||||
makeRequest: makeConnectionRequest,
|
||||
sendNotification,
|
||||
handleCompletion,
|
||||
completionsSupported,
|
||||
connect: connectMcpServer,
|
||||
disconnect: disconnectMcpServer
|
||||
} = useConnection({
|
||||
transportType: 'sse',
|
||||
sseUrl: '',
|
||||
proxyServerUrl: mcpServerUrl,
|
||||
requestTimeout: 1000
|
||||
})
|
||||
// 使用 useRef 保存最新的连接状态和断开函数
|
||||
const connectionStatusRef = useRef(connectionStatus)
|
||||
const disconnectFnRef = useRef(disconnectMcpServer)
|
||||
|
||||
// 当连接状态或断开函数变化时更新 ref
|
||||
useEffect(() => {
|
||||
connectionStatusRef.current = connectionStatus
|
||||
disconnectFnRef.current = disconnectMcpServer
|
||||
}, [connectionStatus, disconnectMcpServer])
|
||||
|
||||
/**
|
||||
* 初始化数据
|
||||
*/
|
||||
const setupComponent = () => {
|
||||
initTabsData()
|
||||
if (type === 'global') {
|
||||
getGlobalMcpConfig()
|
||||
setMcpServerUrl('mcp/global/sse')
|
||||
getGlobalKeysList()
|
||||
} else {
|
||||
service?.basic.enableMcp && setMcpServerUrl(`mcp/service/${serviceId}/sse`)
|
||||
getServiceKeysList()
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 初始化数据
|
||||
*/
|
||||
useEffect(() => {
|
||||
setupComponent()
|
||||
}, [service])
|
||||
/**
|
||||
* 初始化标签数据
|
||||
*/
|
||||
useEffect(() => {
|
||||
initTabsData()
|
||||
type === 'global' && getGlobalMcpConfig()
|
||||
}, [state.language])
|
||||
/**
|
||||
* 切换标签
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (type === 'service') {
|
||||
currentTab === 'MCP' ? setActiveTab('mcp') : setActiveTab('openApi')
|
||||
}
|
||||
}, [currentTab])
|
||||
/**
|
||||
* 仅在组件加载时执行初始化逻辑
|
||||
*/
|
||||
useEffect(() => {
|
||||
// 返回清理函数,只会在组件卸载时执行
|
||||
return () => {
|
||||
try {
|
||||
// 使用 ref 中保存的最新函数强制断开连接
|
||||
const disconnectFn = disconnectFnRef.current
|
||||
if (disconnectFn) {
|
||||
disconnectFn()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('断开连接时出错:', err)
|
||||
}
|
||||
}
|
||||
}, [type])
|
||||
/**
|
||||
* 切换标签时更新配置内容
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (activeTab === 'openApi' && tabContent?.openApi?.configContent) {
|
||||
setConfigContent(tabContent?.openApi?.configContent)
|
||||
} else if (activeTab === 'mcp' && tabContent?.mcp?.configContent) {
|
||||
setConfigContent(tabContent.mcp.configContent?.replace('{your_api_key}', apiKey || '{your_api_key}'))
|
||||
}
|
||||
}, [service, apiKey, activeTab, tabContent])
|
||||
/**
|
||||
* 连接 MCP 服务器
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (mcpServerUrl) {
|
||||
if (connectionStatus === 'connected') {
|
||||
disconnectMcpServer()
|
||||
}
|
||||
connectMcpServer()
|
||||
}
|
||||
}, [mcpServerUrl, ...(type === 'global' ? [state.language] : [])])
|
||||
/**
|
||||
* 获取 MCP tools
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (connectionStatus === 'connected') {
|
||||
listTools()
|
||||
}
|
||||
}, [connectionStatus])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card
|
||||
style={{ borderRadius: '10px' }}
|
||||
className={`w-[400px] h-fit ${customClassName}`}
|
||||
classNames={{
|
||||
body: 'p-[10px]'
|
||||
}}
|
||||
>
|
||||
<p>
|
||||
<Icon
|
||||
icon="icon-park-solid:connection-point-two"
|
||||
className="align-text-bottom mr-[5px]"
|
||||
width="16"
|
||||
height="16"
|
||||
/>
|
||||
{$t('AI 代理集成')}
|
||||
</p>
|
||||
{type === 'service' && service?.basic.enableMcp && (
|
||||
<div className="mt-3 tab-nav flex rounded-md overflow-hidden border border-solid border-[#3D46F2] w-fit">
|
||||
<div
|
||||
className={`tab-item px-5 py-1.5 cursor-pointer text-sm transition-colors ${activeTab === 'openApi' ? 'bg-[#3D46F2] text-white' : 'bg-white text-[#3D46F2]'}`}
|
||||
onClick={() => setActiveTab('openApi')}
|
||||
>
|
||||
Open API
|
||||
</div>
|
||||
<div
|
||||
className={`tab-item px-5 py-1.5 cursor-pointer text-sm transition-colors ${activeTab === 'mcp' ? 'bg-[#3D46F2] text-white' : 'bg-white text-[#3D46F2]'}`}
|
||||
onClick={() => setActiveTab('mcp')}
|
||||
>
|
||||
MCP
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{type === 'service' && !apiKeyList.length ? (
|
||||
<>
|
||||
<Card
|
||||
style={{ borderRadius: '10px' }}
|
||||
className={`w-full mt-3`}
|
||||
classNames={{
|
||||
body: 'p-[10px]'
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center py-3">
|
||||
<span className="text-[14px] mb-5">{$t('请先订阅该服务')}</span>
|
||||
<Button type="primary" onClick={() => openModal?.('apply')}>
|
||||
{$t('申请')}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="tab-container mt-3">
|
||||
<div className="tab-content font-semibold mt-[10px]">
|
||||
{activeTab === 'openApi' ? tabContent.openApi?.title : tabContent.mcp.title}
|
||||
</div>
|
||||
{/* 标签页内容区域 */}
|
||||
<div className="bg-[#0a0b21] text-white p-4 rounded-md my-2 font-mono text-sm overflow-auto relative">
|
||||
{activeTab === 'mcp' ? (
|
||||
<ReactJson
|
||||
src={
|
||||
configContent
|
||||
? typeof configContent === 'string'
|
||||
? (() => {
|
||||
try {
|
||||
return JSON.parse(configContent)
|
||||
} catch (e) {
|
||||
return {}
|
||||
}
|
||||
})()
|
||||
: configContent
|
||||
: {}
|
||||
}
|
||||
theme="monokai"
|
||||
indentWidth={2}
|
||||
displayDataTypes={false}
|
||||
displayObjectSize={false}
|
||||
name={false}
|
||||
collapsed={false}
|
||||
enableClipboard={false}
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'normal'
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<pre className="whitespace-pre-wrap break-words">{configContent || ''}</pre>
|
||||
</>
|
||||
)}
|
||||
<IconButton
|
||||
name="copy"
|
||||
onClick={() => handleCopy(configContent)}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '5px',
|
||||
right: '5px',
|
||||
color: '#999',
|
||||
transition: 'none',
|
||||
'&.MuiButtonBase-root:hover': {
|
||||
background: 'transparent',
|
||||
color: '#3D46F2',
|
||||
transition: 'none'
|
||||
}
|
||||
}}
|
||||
></IconButton>
|
||||
</div>
|
||||
</div>
|
||||
{activeTab === 'mcp' && (
|
||||
<>
|
||||
<div className="tab-content font-semibold my-[10px]">API Key</div>
|
||||
{apiKeyList.length ? (
|
||||
<>
|
||||
{type === 'global' ? (
|
||||
<>
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
value={apiKey}
|
||||
className="w-full"
|
||||
onChange={handleSelectChange}
|
||||
options={apiKeyList}
|
||||
/>
|
||||
<Card
|
||||
style={{ borderRadius: '5px' }}
|
||||
className="w-full mt-[5px] "
|
||||
classNames={{
|
||||
body: 'p-[5px]'
|
||||
}}
|
||||
>
|
||||
<div className="relative h-[25px]">
|
||||
{apiKey}
|
||||
<IconButton
|
||||
name="copy"
|
||||
onClick={() => handleCopy(apiKey)}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '0px',
|
||||
right: '5px',
|
||||
color: '#999',
|
||||
transition: 'none',
|
||||
'&.MuiButtonBase-root:hover': {
|
||||
background: 'transparent',
|
||||
color: '#3D46F2',
|
||||
transition: 'none'
|
||||
}
|
||||
}}
|
||||
></IconButton>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Cascader
|
||||
className='w-full'
|
||||
allowClear={false}
|
||||
options={apiKeyList}
|
||||
value={cascaderKeyList}
|
||||
onChange={handleCascaderChange}
|
||||
placeholder={$t('选择 API Key')}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={''}>
|
||||
<Button onClick={addKey} type="primary">
|
||||
{$t('新增 API Key')}
|
||||
</Button>
|
||||
</Empty>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
</>
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,209 @@
|
||||
import InsidePage from '@common/components/aoplatform/InsidePage'
|
||||
import { IconButton } from '@common/components/postcat/api/IconButton'
|
||||
import { $t } from '@common/locales/index.ts'
|
||||
import { Button, Card, App, Empty } from 'antd'
|
||||
import { useFetch } from '@common/hooks/http'
|
||||
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import AddMcpKey, { addMcpKeysHandle } from './AddMcpKey'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
|
||||
|
||||
const McpKeyContainer = () => {
|
||||
const { fetchData } = useFetch()
|
||||
const { message, modal } = App.useApp()
|
||||
const [keys, setKeys] = useState<any[]>([])
|
||||
const [, forceUpdate] = useState<unknown>(null)
|
||||
const { state } = useGlobalContext()
|
||||
const addMcpKeyModalRef = useRef<addMcpKeysHandle>(null)
|
||||
|
||||
/**
|
||||
* 新增 API Key
|
||||
*/
|
||||
const addKey = () => {
|
||||
modal.confirm({
|
||||
title: $t('新增 API Key'),
|
||||
content: <AddMcpKey ref={addMcpKeyModalRef}></AddMcpKey>,
|
||||
onOk: () => {
|
||||
return addMcpKeyModalRef.current?.save().then((res) => {
|
||||
if (res) {
|
||||
getKeysList()
|
||||
}
|
||||
})
|
||||
},
|
||||
width: 600,
|
||||
okText: $t('确认'),
|
||||
cancelText: $t('取消'),
|
||||
closable: true,
|
||||
icon: <></>
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 API Key 列表
|
||||
*/
|
||||
const getKeysList = () => {
|
||||
fetchData<BasicResponse<null>>('system/apikeys', {
|
||||
method: 'GET'
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, msg, data } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setKeys(data.apikeys || [])
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
message.error(errorInfo || $t(RESPONSE_TIPS.error))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制 API Key
|
||||
*/
|
||||
const copyCode = async (value: string): Promise<void> => {
|
||||
if (value) {
|
||||
await navigator.clipboard.writeText(value)
|
||||
message.success($t(RESPONSE_TIPS.copySuccess))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 API Key
|
||||
*/
|
||||
const deleteKey = (id: string) => {
|
||||
modal.confirm({
|
||||
title: $t('删除'),
|
||||
content: $t('确定删除吗?'),
|
||||
onOk: async () => {
|
||||
try {
|
||||
const response = await fetchData<BasicResponse<'success'>>('system/apikey', {
|
||||
method: 'DELETE',
|
||||
eoParams: { apikey: id }
|
||||
})
|
||||
if (response.code === STATUS_CODE.SUCCESS) {
|
||||
message.success($t('删除成功'))
|
||||
getKeysList()
|
||||
}
|
||||
} catch (error) {
|
||||
message.error($t('删除失败'))
|
||||
}
|
||||
},
|
||||
width: 600,
|
||||
okText: $t('确认'),
|
||||
cancelText: $t('取消'),
|
||||
closable: true,
|
||||
icon: <></>
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑 API Key
|
||||
*/
|
||||
const editKey = (key: any) => {
|
||||
modal.confirm({
|
||||
title: $t('编辑'),
|
||||
content: (
|
||||
<AddMcpKey ref={addMcpKeyModalRef} name={key.name} value={key.value} apikey={key.id} type={'edit'}></AddMcpKey>
|
||||
),
|
||||
onOk: () => {
|
||||
return addMcpKeyModalRef.current?.save().then((res) => {
|
||||
if (res) {
|
||||
getKeysList()
|
||||
}
|
||||
})
|
||||
},
|
||||
width: 600,
|
||||
okText: $t('确认'),
|
||||
cancelText: $t('取消'),
|
||||
closable: true,
|
||||
icon: <></>
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getKeysList()
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
forceUpdate({})
|
||||
}, [state.language])
|
||||
return (
|
||||
<>
|
||||
<InsidePage
|
||||
pageTitle={$t('API Key')}
|
||||
description={$t('API 密钥可用于调用系统级 Open API 和 MCP。')}
|
||||
showBorder={false}
|
||||
scrollPage={false}
|
||||
>
|
||||
<Button type="primary" onClick={addKey}>
|
||||
{$t('新增 API Key')}
|
||||
</Button>
|
||||
<div className="api-key-container mt-[20px]">
|
||||
{keys.length ? (
|
||||
keys.map((key, index) => (
|
||||
<Card style={{ width: 600, borderRadius: '10px' }} key={index} className="mt-[10px]">
|
||||
<div className="flex">
|
||||
<div className="flex-1">
|
||||
<p className="text-[14px] font-bold">{key.name}</p>
|
||||
<div className="flex">
|
||||
<span className="h-[26px] leading-[28px]">{key.value}</span>
|
||||
<IconButton
|
||||
name="copy"
|
||||
onClick={() => {
|
||||
copyCode(key?.value)
|
||||
}}
|
||||
sx={{
|
||||
color: '#333',
|
||||
transition: 'none',
|
||||
'&.MuiButtonBase-root:hover': {
|
||||
background: 'transparent',
|
||||
color: '#3D46F2',
|
||||
transition: 'none'
|
||||
}
|
||||
}}
|
||||
></IconButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-[30px] flex justify-center items-center">
|
||||
<IconButton
|
||||
name="edit"
|
||||
onClick={() => {
|
||||
editKey(key)
|
||||
}}
|
||||
sx={{
|
||||
color: '#333',
|
||||
transition: 'none',
|
||||
'&.MuiButtonBase-root:hover': {
|
||||
background: 'transparent',
|
||||
color: '#3D46F2',
|
||||
transition: 'none'
|
||||
}
|
||||
}}
|
||||
></IconButton>
|
||||
<IconButton
|
||||
name="delete"
|
||||
onClick={() => {
|
||||
deleteKey(key.id)
|
||||
}}
|
||||
sx={{
|
||||
color: '#333',
|
||||
transition: 'none',
|
||||
'&.MuiButtonBase-root:hover': { background: 'transparent', color: 'red', transition: 'none' }
|
||||
}}
|
||||
></IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<>
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE}/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</InsidePage>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default McpKeyContainer
|
||||
@@ -0,0 +1,37 @@
|
||||
import InsidePage from "@common/components/aoplatform/InsidePage"
|
||||
import { $t } from '@common/locales/index.ts'
|
||||
import { IntegrationAIContainer } from "./IntegrationAIContainer"
|
||||
import { Tool } from "@modelcontextprotocol/sdk/types.js"
|
||||
import { useEffect, useState } from "react"
|
||||
import McpToolsContainer from "./McpToolsContainer"
|
||||
import { useGlobalContext } from "@common/contexts/GlobalStateContext"
|
||||
|
||||
const McpServiceContainer = () => {
|
||||
const [tools, setTools] = useState<Tool[]>([]);
|
||||
const [, forceUpdate] = useState<unknown>(null)
|
||||
const { state } = useGlobalContext()
|
||||
const handleToolsChange = (value: Tool[]) => {
|
||||
setTools(value)
|
||||
}
|
||||
useEffect(() => {
|
||||
forceUpdate({})
|
||||
}, [state.language])
|
||||
return (
|
||||
<>
|
||||
<InsidePage
|
||||
pageTitle={$t('MCP 服务')}
|
||||
description={$t('MCP Service 充当 AI 模型与 API 之间的桥梁,允许智能助手(如 Claude)动态发现和调用 Gateway 上的 API,无需繁琐的手动配置或自定义集成。')}
|
||||
showBorder={false}
|
||||
scrollPage={false}
|
||||
>
|
||||
|
||||
<div className="flex mt-[10px] pr-[40px]">
|
||||
<McpToolsContainer tools={tools} />
|
||||
<IntegrationAIContainer type={'global'} handleToolsChange={handleToolsChange}></IntegrationAIContainer>
|
||||
</div>
|
||||
</InsidePage>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default McpServiceContainer
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Icon } from '@iconify/react/dist/iconify.js'
|
||||
import { Tool } from '@modelcontextprotocol/sdk/types.js'
|
||||
import { Card } from 'antd'
|
||||
|
||||
const McpToolsContainer = ({ tools = [], customClassName }: { tools: Tool[]; customClassName?: string }) => {
|
||||
return (
|
||||
<>
|
||||
<Card
|
||||
style={{ borderRadius: '10px' }}
|
||||
className={`w-full flex-1 mr-[10px] ${customClassName}`}
|
||||
classNames={{
|
||||
body: 'p-[10px]'
|
||||
}}
|
||||
>
|
||||
<div className="mb-[10px]">
|
||||
<Icon icon="gravity-ui:plug-connection" className="align-text-bottom mr-[5px]" width="16" height="16" />
|
||||
<span className="text-[14px] font-bold align-middle">Tools</span>
|
||||
</div>
|
||||
{tools.map((tool, index) => (
|
||||
<Card style={{ borderRadius: '10px' }} key={index} className={`w-full ${index > 0 ? 'mt-[10px]' : ''}`}>
|
||||
<p className="text-[14px] font-bold">{tool.name}</p>
|
||||
<div className="leading-[28px]">{tool.description}</div>
|
||||
</Card>
|
||||
))}
|
||||
</Card>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default McpToolsContainer
|
||||
@@ -0,0 +1,33 @@
|
||||
// import { InspectorConfig } from "./configurationTypes";
|
||||
|
||||
// OAuth-related session storage keys
|
||||
export const SESSION_KEYS = {
|
||||
CODE_VERIFIER: "mcp_code_verifier",
|
||||
SERVER_URL: "mcp_server_url",
|
||||
TOKENS: "mcp_tokens",
|
||||
CLIENT_INFORMATION: "mcp_client_information",
|
||||
} as const;
|
||||
|
||||
export type ConnectionStatus =
|
||||
| "disconnected"
|
||||
| "connected"
|
||||
| "error"
|
||||
| "error-connecting-to-proxy";
|
||||
|
||||
export const DEFAULT_MCP_PROXY_LISTEN_PORT = "6277";
|
||||
|
||||
/**
|
||||
* Default configuration for the MCP Inspector, Currently persisted in local_storage in the Browser.
|
||||
* Future plans: Provide json config file + Browser local_storage to override default values
|
||||
**/
|
||||
export const DEFAULT_INSPECTOR_CONFIG: any = {
|
||||
MCP_SERVER_REQUEST_TIMEOUT: {
|
||||
description: "Timeout for requests to the MCP server (ms)",
|
||||
value: 10000,
|
||||
},
|
||||
MCP_PROXY_FULL_ADDRESS: {
|
||||
description:
|
||||
"Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577",
|
||||
value: "",
|
||||
},
|
||||
} as const;
|
||||
@@ -0,0 +1,22 @@
|
||||
import {
|
||||
NotificationSchema as BaseNotificationSchema,
|
||||
ClientNotificationSchema,
|
||||
ServerNotificationSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import { z } from "zod";
|
||||
|
||||
export const StdErrNotificationSchema = BaseNotificationSchema.extend({
|
||||
method: z.literal("notifications/stderr"),
|
||||
params: z.object({
|
||||
content: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const NotificationSchema = ClientNotificationSchema.or(
|
||||
StdErrNotificationSchema,
|
||||
)
|
||||
.or(ServerNotificationSchema)
|
||||
.or(BaseNotificationSchema);
|
||||
|
||||
export type StdErrNotification = z.infer<typeof StdErrNotificationSchema>;
|
||||
export type Notification = z.infer<typeof NotificationSchema>;
|
||||
@@ -0,0 +1,391 @@
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
import {
|
||||
SSEClientTransport,
|
||||
SseError,
|
||||
} from "@modelcontextprotocol/sdk/client/sse.js";
|
||||
import { App } from 'antd'
|
||||
import {
|
||||
ClientNotification,
|
||||
ClientRequest,
|
||||
CreateMessageRequestSchema,
|
||||
ListRootsRequestSchema,
|
||||
ProgressNotificationSchema,
|
||||
ResourceUpdatedNotificationSchema,
|
||||
LoggingMessageNotificationSchema,
|
||||
Request,
|
||||
Result,
|
||||
ServerCapabilities,
|
||||
PromptReference,
|
||||
ResourceReference,
|
||||
McpError,
|
||||
CompleteResultSchema,
|
||||
ErrorCode,
|
||||
CancelledNotificationSchema,
|
||||
ResourceListChangedNotificationSchema,
|
||||
ToolListChangedNotificationSchema,
|
||||
PromptListChangedNotificationSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import { useState } from "react";
|
||||
import { z } from "zod";
|
||||
import { ConnectionStatus, SESSION_KEYS } from "./constants";
|
||||
import { Notification, StdErrNotificationSchema } from "./notificationTypes";
|
||||
// import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
|
||||
// import { authProvider } from "../auth";
|
||||
// import packageJson from "../../../package.json";
|
||||
|
||||
|
||||
interface UseConnectionOptions {
|
||||
transportType: "stdio" | "sse";
|
||||
command?: string;
|
||||
args?: string;
|
||||
sseUrl: string;
|
||||
env?: Record<string, string>;
|
||||
proxyServerUrl: string;
|
||||
bearerToken?: string;
|
||||
requestTimeout?: number;
|
||||
onNotification?: (notification: Notification) => void;
|
||||
onStdErrNotification?: (notification: Notification) => void;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
onPendingRequest?: (request: any, resolve: any, reject: any) => void;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
getRoots?: () => any[];
|
||||
}
|
||||
|
||||
interface RequestOptions {
|
||||
signal?: AbortSignal;
|
||||
timeout?: number;
|
||||
suppressToast?: boolean;
|
||||
}
|
||||
|
||||
export function useConnection({
|
||||
transportType,
|
||||
command,
|
||||
args,
|
||||
sseUrl,
|
||||
env,
|
||||
proxyServerUrl,
|
||||
bearerToken,
|
||||
requestTimeout,
|
||||
onNotification,
|
||||
onStdErrNotification,
|
||||
onPendingRequest,
|
||||
getRoots,
|
||||
}: UseConnectionOptions) {
|
||||
const [connectionStatus, setConnectionStatus] =
|
||||
useState<ConnectionStatus>("disconnected");
|
||||
const { message } = App.useApp()
|
||||
const [serverCapabilities, setServerCapabilities] =
|
||||
useState<ServerCapabilities | null>(null);
|
||||
const [mcpClient, setMcpClient] = useState<Client | null>(null);
|
||||
const [requestHistory, setRequestHistory] = useState<
|
||||
{ request: string; response?: string }[]
|
||||
>([]);
|
||||
const [completionsSupported, setCompletionsSupported] = useState(true);
|
||||
|
||||
const pushHistory = (request: object, response?: object) => {
|
||||
setRequestHistory((prev) => [
|
||||
...prev,
|
||||
{
|
||||
request: JSON.stringify(request),
|
||||
response: response !== undefined ? JSON.stringify(response) : undefined,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const makeRequest = async <T extends z.ZodType>(
|
||||
request: ClientRequest,
|
||||
schema: T,
|
||||
options?: RequestOptions,
|
||||
): Promise<z.output<T>> => {
|
||||
if (!mcpClient) {
|
||||
throw new Error("MCP client not connected");
|
||||
}
|
||||
|
||||
try {
|
||||
const abortController = new AbortController();
|
||||
const timeoutId = setTimeout(() => {
|
||||
abortController.abort("Request timed out");
|
||||
}, options?.timeout ?? requestTimeout);
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await mcpClient.request(request, schema, {
|
||||
signal: options?.signal ?? abortController.signal,
|
||||
});
|
||||
pushHistory(request, response);
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
pushHistory(request, { error: errorMessage });
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (e: unknown) {
|
||||
if (!options?.suppressToast) {
|
||||
const errorString = (e as Error).message ?? String(e);
|
||||
message.error(errorString)
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCompletion = async (
|
||||
ref: ResourceReference | PromptReference,
|
||||
argName: string,
|
||||
value: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<string[]> => {
|
||||
if (!mcpClient || !completionsSupported) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const request: ClientRequest = {
|
||||
method: "completion/complete",
|
||||
params: {
|
||||
argument: {
|
||||
name: argName,
|
||||
value,
|
||||
},
|
||||
ref,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await makeRequest(request, CompleteResultSchema, {
|
||||
signal,
|
||||
suppressToast: true,
|
||||
});
|
||||
return response?.completion.values || [];
|
||||
} catch (e: unknown) {
|
||||
// Disable completions silently if the server doesn't support them.
|
||||
// See https://github.com/modelcontextprotocol/specification/discussions/122
|
||||
if (e instanceof McpError && e.code === ErrorCode.MethodNotFound) {
|
||||
setCompletionsSupported(false);
|
||||
return [];
|
||||
}
|
||||
|
||||
// Unexpected errors - show toast and rethrow
|
||||
message.error(e instanceof Error ? e.message : String(e))
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const sendNotification = async (notification: ClientNotification) => {
|
||||
if (!mcpClient) {
|
||||
const error = new Error("MCP client not connected");
|
||||
message.error(error.message)
|
||||
throw error;
|
||||
}
|
||||
|
||||
try {
|
||||
await mcpClient.notification(notification);
|
||||
// Log successful notifications
|
||||
pushHistory(notification);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof McpError) {
|
||||
// Log MCP protocol errors
|
||||
pushHistory(notification, { error: e.message });
|
||||
}
|
||||
message.error(e instanceof Error ? e.message : String(e))
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
// TODO_先屏蔽,暂时不需要
|
||||
// const checkProxyHealth = async () => {
|
||||
// try {
|
||||
// const proxyHealthUrl = new URL(`${proxyServerUrl}/health`);
|
||||
// const proxyHealthResponse = await fetch(proxyHealthUrl);
|
||||
// const proxyHealth = await proxyHealthResponse.json();
|
||||
// if (proxyHealth?.status !== "ok") {
|
||||
// throw new Error("MCP Proxy Server is not healthy");
|
||||
// }
|
||||
// } catch (e) {
|
||||
// console.error("Couldn't connect to MCP Proxy Server", e);
|
||||
// throw e;
|
||||
// }
|
||||
// };
|
||||
// TODO_先屏蔽,暂时不需要
|
||||
// const handleAuthError = async (error: unknown) => {
|
||||
// if (error instanceof SseError && error.code === 401) {
|
||||
// sessionStorage.setItem(SESSION_KEYS.SERVER_URL, sseUrl);
|
||||
|
||||
// const result = await auth(authProvider, { serverUrl: sseUrl });
|
||||
// return result === "AUTHORIZED";
|
||||
// }
|
||||
|
||||
// return false;
|
||||
// };
|
||||
|
||||
const connect = async (_e?: unknown, retryCount: number = 0) => {
|
||||
const client = new Client<Request, Notification, Result>(
|
||||
{
|
||||
name: "mcp-inspector",
|
||||
version: '0.0.1',
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
sampling: {},
|
||||
roots: {
|
||||
listChanged: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
// TODO_暂时不需要
|
||||
// try {
|
||||
// await checkProxyHealth();
|
||||
// } catch {
|
||||
// setConnectionStatus("error-connecting-to-proxy");
|
||||
// return;
|
||||
// }
|
||||
// 使用与http.ts一致的方式处理URL
|
||||
// 注意:proxyServerUrl应该是完整URL,或者我们需要为其添加基础URL
|
||||
// 处理两种情况:完整URL或相对路径
|
||||
let fullUrl;
|
||||
if (proxyServerUrl.startsWith('http://') || proxyServerUrl.startsWith('https://')) {
|
||||
// 如果是完整URL,直接使用
|
||||
fullUrl = `${proxyServerUrl}/sse`;
|
||||
} else {
|
||||
// 如果是相对路径,添加基础URL和API前缀
|
||||
const baseUrl = window.location.origin;
|
||||
const apiPrefix = '/api/v1/';
|
||||
fullUrl = `${baseUrl}${apiPrefix}${proxyServerUrl}`;
|
||||
}
|
||||
// let newSseUrl = ''
|
||||
// if (sseUrl.startsWith('http://') || sseUrl.startsWith('https://')) {
|
||||
// // 如果是完整URL,直接使用
|
||||
// newSseUrl = sseUrl
|
||||
// } else {
|
||||
// // 如果是相对路径,添加基础URL和API前缀
|
||||
// const baseUrl = window.location.origin;
|
||||
// const apiPrefix = '/api/v1/';
|
||||
// newSseUrl = `${baseUrl}${apiPrefix}${sseUrl}`;
|
||||
// }
|
||||
const mcpProxyServerUrl = new URL(fullUrl);
|
||||
// mcpProxyServerUrl.searchParams.append("transportType", transportType);
|
||||
// if (transportType === "stdio") {
|
||||
// mcpProxyServerUrl.searchParams.append("command", command || '');
|
||||
// mcpProxyServerUrl.searchParams.append("args", args || '');
|
||||
// mcpProxyServerUrl.searchParams.append("env", JSON.stringify(env || {}));
|
||||
// } else {
|
||||
// mcpProxyServerUrl.searchParams.append("url", newSseUrl);
|
||||
// }
|
||||
// console.log('sseUrl===', newSseUrl)
|
||||
try {
|
||||
// Inject auth manually instead of using SSEClientTransport, because we're
|
||||
// proxying through the inspector server first.
|
||||
const headers: HeadersInit = {};
|
||||
|
||||
// TODO_暂时不需要。Use manually provided bearer token if available, otherwise use OAuth tokens
|
||||
// const token = bearerToken || (await authProvider.tokens())?.access_token;
|
||||
// if (token) {
|
||||
// headers["Authorization"] = `Bearer ${token}`;
|
||||
// }
|
||||
// 创建SSE客户端传输层
|
||||
const clientTransport = new SSEClientTransport(mcpProxyServerUrl, {
|
||||
eventSourceInit: {
|
||||
fetch: (url, init) => fetch(url, { ...init, headers }),
|
||||
},
|
||||
requestInit: {
|
||||
headers,
|
||||
},
|
||||
});
|
||||
// TODO_暂时不需要
|
||||
// if (onNotification) {
|
||||
// [
|
||||
// CancelledNotificationSchema,
|
||||
// ProgressNotificationSchema,
|
||||
// LoggingMessageNotificationSchema,
|
||||
// ResourceUpdatedNotificationSchema,
|
||||
// ResourceListChangedNotificationSchema,
|
||||
// ToolListChangedNotificationSchema,
|
||||
// PromptListChangedNotificationSchema,
|
||||
// ].forEach((notificationSchema) => {
|
||||
// client.setNotificationHandler(notificationSchema, onNotification);
|
||||
// });
|
||||
|
||||
// client.fallbackNotificationHandler = (
|
||||
// notification: Notification,
|
||||
// ): Promise<void> => {
|
||||
// onNotification(notification);
|
||||
// return Promise.resolve();
|
||||
// };
|
||||
// }
|
||||
|
||||
// if (onStdErrNotification) {
|
||||
// client.setNotificationHandler(
|
||||
// StdErrNotificationSchema,
|
||||
// onStdErrNotification,
|
||||
// );
|
||||
// }
|
||||
|
||||
try {
|
||||
await client.connect(clientTransport);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to connect to MCP Server via the MCP Inspector Proxy: ${mcpProxyServerUrl}:`,
|
||||
error,
|
||||
);
|
||||
// TODO_先屏蔽,后续如果需要再处理
|
||||
// const shouldRetry = await handleAuthError(error);
|
||||
// if (shouldRetry) {
|
||||
// return connect(undefined, retryCount + 1);
|
||||
// }
|
||||
|
||||
if (error instanceof SseError && error.code === 401) {
|
||||
// Don't set error state if we're about to redirect for auth
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const capabilities = client.getServerCapabilities();
|
||||
setServerCapabilities(capabilities ?? null);
|
||||
setCompletionsSupported(true); // Reset completions support on new connection
|
||||
// TODO_暂时不需要
|
||||
// if (onPendingRequest) {
|
||||
// client.setRequestHandler(CreateMessageRequestSchema, (request) => {
|
||||
// return new Promise((resolve, reject) => {
|
||||
// onPendingRequest(request, resolve, reject);
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
// if (getRoots) {
|
||||
// client.setRequestHandler(ListRootsRequestSchema, async () => {
|
||||
// return { roots: getRoots() };
|
||||
// });
|
||||
// }
|
||||
|
||||
setMcpClient(client);
|
||||
setConnectionStatus("connected");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setConnectionStatus("error");
|
||||
}
|
||||
};
|
||||
|
||||
const disconnect = async () => {
|
||||
await mcpClient?.close();
|
||||
setMcpClient(null);
|
||||
setConnectionStatus("disconnected");
|
||||
setCompletionsSupported(false);
|
||||
setServerCapabilities(null);
|
||||
};
|
||||
|
||||
return {
|
||||
connectionStatus,
|
||||
serverCapabilities,
|
||||
mcpClient,
|
||||
requestHistory,
|
||||
makeRequest,
|
||||
sendNotification,
|
||||
handleCompletion,
|
||||
completionsSupported,
|
||||
connect,
|
||||
disconnect,
|
||||
};
|
||||
}
|
||||
@@ -480,6 +480,8 @@ const MemberList = () => {
|
||||
render: (_, entity) => (
|
||||
<WithPermission access="system.organization.member.edit">
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
className="w-full"
|
||||
mode="multiple"
|
||||
value={entity.roles?.map((x: EntityItem) => x.id)}
|
||||
|
||||
@@ -60,7 +60,13 @@ export type DashboardSettingEditProps = {
|
||||
name="driver"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.select)} options={[...DASHBOARD_SETTING_DRIVER_OPTION_LIST]}/>
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.select)}
|
||||
options={[...DASHBOARD_SETTING_DRIVER_OPTION_LIST]}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<PartitionDashboardConfigFieldType>
|
||||
|
||||
@@ -301,7 +301,13 @@ const FilterForm = forwardRef<FilterFormHandle, FilterFormProps>(
|
||||
return (
|
||||
<Form form={form} layout="vertical" onValuesChange={handleValuesChange}>
|
||||
<Form.Item name="name" label={$t('属性名称')} rules={[{ required: true }]}>
|
||||
<Select disabled={disabled} onChange={handleTypeChange} options={filterOptionsList} />
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
disabled={disabled}
|
||||
onChange={handleTypeChange}
|
||||
options={filterOptionsList}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="values"
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
import { useEffect } from "react";
|
||||
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||
import { useBreadcrumb } from '@common/contexts/BreadcrumbContext'
|
||||
import { useEffect } from 'react'
|
||||
import { Outlet, useLocation, useNavigate } from 'react-router-dom'
|
||||
import { $t } from '@common/locales'
|
||||
|
||||
export default function ServicePolicyLayout(){
|
||||
const location = useLocation()
|
||||
const pathName = location.pathname
|
||||
const navigator = useNavigate()
|
||||
useEffect(()=>{
|
||||
const tmpPath = pathName.split('/')
|
||||
if(tmpPath[tmpPath.length -1 ] === 'servicepolicy'){
|
||||
navigator('datamasking/list')
|
||||
export default function ServicePolicyLayout() {
|
||||
const location = useLocation()
|
||||
const pathName = location.pathname
|
||||
const navigator = useNavigate()
|
||||
const { setBreadcrumb } = useBreadcrumb()
|
||||
useEffect(() => {
|
||||
const tmpPath = pathName.split('/')
|
||||
if (tmpPath[tmpPath.length - 1] === 'servicepolicy') {
|
||||
setBreadcrumb([
|
||||
{
|
||||
title: $t('服务'),
|
||||
onClick: () => navigator('/service/list')
|
||||
},
|
||||
{
|
||||
title: $t('服务策略')
|
||||
}
|
||||
},[pathName])
|
||||
return (<Outlet></Outlet>)
|
||||
}
|
||||
])
|
||||
navigator('datamasking/list')
|
||||
}
|
||||
}, [pathName])
|
||||
return <Outlet></Outlet>
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import FilterTable from "../FilterTable"
|
||||
import { DataMaskingConfigHandle ,DataMaskingConfigFieldType, PolicyMatchType} from "@common/const/policy/type"
|
||||
import {PolicyOptions} from '@common/const/policy/consts'
|
||||
import {v4 as uuidv4} from 'uuid'
|
||||
import { useBreadcrumb } from "@common/contexts/BreadcrumbContext"
|
||||
|
||||
const DataMaskingConfig = forwardRef<DataMaskingConfigHandle>((_,ref) => {
|
||||
const { message,modal } = App.useApp()
|
||||
@@ -24,6 +25,7 @@ const DataMaskingConfig = forwardRef<DataMaskingConfigHandle>((_,ref) => {
|
||||
const { state } = useGlobalContext()
|
||||
const [ loading, setLoading ] = useState<boolean>(false)
|
||||
const navigator = useNavigate()
|
||||
const { setBreadcrumb } = useBreadcrumb()
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
save:onFinish
|
||||
@@ -78,6 +80,19 @@ const DataMaskingConfig = forwardRef<DataMaskingConfigHandle>((_,ref) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setBreadcrumb([
|
||||
{
|
||||
title: $t('服务'),
|
||||
onClick: () => navigator('/service/list')
|
||||
},
|
||||
{
|
||||
title:$t('服务策略'),
|
||||
onClick: () => navigator(serviceId ? `/service/${teamId}/aiInside/${serviceId}/servicepolicy` : '')
|
||||
},
|
||||
{
|
||||
title: policyId !== undefined ? $t('编辑服务策略') : $t('添加服务策略')
|
||||
}
|
||||
])
|
||||
if (policyId !== undefined) {
|
||||
setOnEdit(true);
|
||||
getPolicyInfo();
|
||||
@@ -96,7 +111,7 @@ const DataMaskingConfig = forwardRef<DataMaskingConfigHandle>((_,ref) => {
|
||||
showBorder={false}
|
||||
scrollPage={false}
|
||||
className="overflow-y-auto"
|
||||
backUrl={serviceId ? '../list' : undefined}
|
||||
backUrl={serviceId ? `/service/${teamId}/aiInside/${serviceId}/servicepolicy` : undefined}
|
||||
>
|
||||
<Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} spinning={loading} wrapperClassName=' pb-PAGE_INSIDE_B pr-PAGE_INSIDE_X'>
|
||||
<WithPermission access={onEdit ? [`${ serviceId === undefined ? 'system.devops':'team.service'}.policy.edit`] :''}>
|
||||
@@ -124,8 +139,13 @@ const DataMaskingConfig = forwardRef<DataMaskingConfigHandle>((_,ref) => {
|
||||
name="type"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} options={policyOptions} >
|
||||
</Select>
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.input)}
|
||||
options={policyOptions}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<DataMaskingConfigFieldType>
|
||||
|
||||
@@ -146,6 +146,8 @@ const DataMaskRuleForm: React.FC<DataMaskRuleFormProps> = ({
|
||||
<Form form={form} layout="vertical" className="p-4">
|
||||
<Form.Item name={['match', 'type']} label={$t('匹配类型')} rules={[{ required: true }]}>
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
placeholder={$t(PLACEHOLDER.select)}
|
||||
onChange={handleMatchTypeChange}
|
||||
options={matchRuleOptions}
|
||||
@@ -156,6 +158,8 @@ const DataMaskRuleForm: React.FC<DataMaskRuleFormProps> = ({
|
||||
<Form.Item name={['match', 'value']} label={$t('匹配值')} rules={[{ required: true }]}>
|
||||
{matchType === 'inner' ? (
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
placeholder={$t(PLACEHOLDER.select)}
|
||||
onChange={handleMatchValueChange}
|
||||
options={dataFormatOptions}
|
||||
@@ -168,6 +172,8 @@ const DataMaskRuleForm: React.FC<DataMaskRuleFormProps> = ({
|
||||
|
||||
<Form.Item name={['mask', 'type']} label={$t('脱敏类型')} rules={[{ required: true }]}>
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
placeholder={$t(PLACEHOLDER.select)}
|
||||
onChange={handleMaskTypeChange}
|
||||
options={
|
||||
@@ -197,6 +203,8 @@ const DataMaskRuleForm: React.FC<DataMaskRuleFormProps> = ({
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
placeholder={$t(PLACEHOLDER.select)}
|
||||
onChange={handleReplaceTypeChange}
|
||||
options={dataMaskReplaceStrOptions}
|
||||
|
||||
@@ -1,251 +1,313 @@
|
||||
import { useEffect, useMemo, useState} from "react";
|
||||
import {App, Button, Checkbox, Collapse, Form, GetProp, Input} from "antd";
|
||||
import {useFetch} from "@common/hooks/http.ts";
|
||||
import {BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE, VALIDATE_MESSAGE} from "@common/const/const.tsx";
|
||||
import WithPermission from "@common/components/aoplatform/WithPermission.tsx";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { RouterParams } from "@core/components/aoplatform/RenderRoutes.tsx";
|
||||
import { ArrowLeftOutlined } from "@ant-design/icons";
|
||||
import { $t } from "@common/locales";
|
||||
import { useGlobalContext } from "@common/contexts/GlobalStateContext";
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { App, Button, Checkbox, Collapse, Form, GetProp, Input } from 'antd'
|
||||
import { useFetch } from '@common/hooks/http.ts'
|
||||
import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const.tsx'
|
||||
import WithPermission from '@common/components/aoplatform/WithPermission.tsx'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { $t } from '@common/locales'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
|
||||
import { useBreadcrumb } from '@common/contexts/BreadcrumbContext'
|
||||
import TopBreadcrumb from '@common/components/aoplatform/Breadcrumb.tsx'
|
||||
|
||||
type PermissionItem = {
|
||||
name:string
|
||||
value:string
|
||||
name: string
|
||||
value: string
|
||||
}
|
||||
|
||||
type PermissionClassify = PermissionItem & {children : ( PermissionItem & {dependents:string[]})[]}
|
||||
type PermissionClassify = PermissionItem & { children: (PermissionItem & { dependents: string[] })[] }
|
||||
|
||||
type RolePermissionItem = PermissionItem & {
|
||||
children:PermissionClassify[]}
|
||||
children: PermissionClassify[]
|
||||
}
|
||||
|
||||
|
||||
type DependenciesMapType = Map<string, {dependents:string[], control:string[]}>
|
||||
type DependenciesMapType = Map<string, { dependents: string[]; control: string[] }>
|
||||
|
||||
type PermissionCollapseProps = {
|
||||
id?: string;
|
||||
value?: string[];
|
||||
onChange?: (value:string[]) => void;
|
||||
permissionTemplate:RolePermissionItem[]
|
||||
dependenciesMap?: DependenciesMapType
|
||||
id?: string
|
||||
value?: string[]
|
||||
onChange?: (value: string[]) => void
|
||||
permissionTemplate: RolePermissionItem[]
|
||||
dependenciesMap?: DependenciesMapType
|
||||
}
|
||||
|
||||
type PermissionInfo = {
|
||||
permit: string[]
|
||||
description: string
|
||||
update_time: string
|
||||
create_time: string
|
||||
name: string
|
||||
permit: string[]
|
||||
description: string
|
||||
update_time: string
|
||||
create_time: string
|
||||
name: string
|
||||
}
|
||||
|
||||
const PermissionContent = ({permits,onChange,value=[],id,dependenciesMap}:{permits:PermissionClassify[],dependenciesMap:DependenciesMapType,value:string[],id:string, onChange?: (value:string[]) => void;})=>{
|
||||
|
||||
|
||||
const onSingleCheckboxChange: GetProp<typeof Checkbox, 'onChange'> = (e) => {
|
||||
if(e.target.checked){
|
||||
onChange?.(Array.from(new Set([...value, e.target.id, ...(dependenciesMap?.get(e.target.id!)?.dependents || [])] as string[])))
|
||||
}else{
|
||||
const cancelValue = [...dependenciesMap?.get(e.target.id!)?.control || [], e.target.id]
|
||||
onChange?.(value.filter(x=>!cancelValue.includes(x)))
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div id={id} className="flex flex-col gap-btnbase p-btnbase">
|
||||
{
|
||||
permits.map((item:PermissionClassify)=>(
|
||||
<>
|
||||
<div className="flex flex-col gap-btnbase" key={`group-${item.name}`}>
|
||||
{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}>{(x.name)}</Checkbox>)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
))
|
||||
}</div>
|
||||
)
|
||||
}
|
||||
// 自定义表单控件
|
||||
const PermissionCollapse:React.FC<PermissionCollapseProps> = (props)=>{
|
||||
const { id, value = [], onChange,permissionTemplate ,dependenciesMap} = props;
|
||||
const [openCollapses, setOpenCollapses] = useState<string[]>([])
|
||||
const {state} = useGlobalContext()
|
||||
|
||||
const items = useMemo(()=>{
|
||||
const generatePermissionItem = (permissionItem:RolePermissionItem[])=> permissionItem.map((item:RolePermissionItem)=>({
|
||||
key:item.name,
|
||||
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) : []
|
||||
},[permissionTemplate,value,state.language])
|
||||
|
||||
useEffect(()=>{
|
||||
permissionTemplate && setOpenCollapses(permissionTemplate?.map(x=>x.name))
|
||||
},[permissionTemplate])
|
||||
|
||||
const onCollapseChange = (keys: string | string[]) => {
|
||||
setOpenCollapses(keys as string[])
|
||||
};
|
||||
|
||||
return <Collapse items={items} activeKey={openCollapses} onChange={onCollapseChange} />
|
||||
const PermissionContent = ({
|
||||
permits,
|
||||
onChange,
|
||||
value = [],
|
||||
id,
|
||||
dependenciesMap
|
||||
}: {
|
||||
permits: PermissionClassify[]
|
||||
dependenciesMap: DependenciesMapType
|
||||
value: string[]
|
||||
id: string
|
||||
onChange?: (value: string[]) => void
|
||||
}) => {
|
||||
const onSingleCheckboxChange: GetProp<typeof Checkbox, 'onChange'> = (e) => {
|
||||
if (e.target.checked) {
|
||||
onChange?.(
|
||||
Array.from(
|
||||
new Set([...value, e.target.id, ...(dependenciesMap?.get(e.target.id!)?.dependents || [])] as string[])
|
||||
)
|
||||
)
|
||||
} else {
|
||||
const cancelValue = [...(dependenciesMap?.get(e.target.id!)?.control || []), e.target.id]
|
||||
onChange?.(value.filter((x) => !cancelValue.includes(x)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const RoleConfig = ()=>{
|
||||
const { message } = App.useApp()
|
||||
const [form] = Form.useForm();
|
||||
const {fetchData} = useFetch()
|
||||
const navigateTo = useNavigate()
|
||||
const { roleType, roleId} = useParams<RouterParams>()
|
||||
const [permissionTemplate, setPermissionTemplate] = useState<RolePermissionItem[]>()
|
||||
const [dependenciesMap, setDependenciesMap] = useState<DependenciesMapType>()
|
||||
const APP_MODE = import.meta.env.VITE_APP_MODE;
|
||||
const [permissionInfo, setPermissionInfo] = useState<PermissionInfo>()
|
||||
const { state } = useGlobalContext()
|
||||
|
||||
const generateDependenciesMap = (data:RolePermissionItem[])=>{
|
||||
const map = new Map<string, {dependents:string[], control:string[]}>()
|
||||
data.forEach((item:RolePermissionItem)=>{
|
||||
item.children.forEach((child:PermissionClassify)=>{
|
||||
child.children.forEach((permission:PermissionItem & {dependents:string[]})=>{
|
||||
|
||||
if (permission.dependents && permission.dependents.length > 0) {
|
||||
// 获取当前权限的依赖
|
||||
const currentDependents = map.get(permission.value);
|
||||
if (currentDependents) {
|
||||
currentDependents.dependents.push(...permission.dependents);
|
||||
} else {
|
||||
map.set(permission.value, { dependents: [...permission.dependents], control: [] });
|
||||
}
|
||||
|
||||
// 更新依赖项的控制项
|
||||
permission.dependents.forEach((dependent: string) => {
|
||||
const dependentEntry = map.get(dependent);
|
||||
if (dependentEntry) {
|
||||
dependentEntry.control.push(permission.value);
|
||||
} else {
|
||||
map.set(dependent, { dependents: [], control: [permission.value] });
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
setDependenciesMap(map)
|
||||
}
|
||||
|
||||
const generateNewPermit:(data:RolePermissionItem[])=>RolePermissionItem[] = (data:RolePermissionItem[]) =>{
|
||||
return data.map((item:RolePermissionItem)=>({
|
||||
...item,children:item.children.map((child:PermissionClassify)=>({
|
||||
...child,
|
||||
children:child.children.map((permission:PermissionItem & {dependents:string[]})=>({
|
||||
...permission, value:`${roleType}.${item.value}.${child.value}.${permission.value}`
|
||||
}))
|
||||
}))
|
||||
}))
|
||||
}
|
||||
|
||||
const getPermissionTemplate = ()=>{
|
||||
return fetchData<BasicResponse<{permits:RolePermissionItem[]}>>(`${roleType}/role/template`,{method:'GET'}).then(response=>{
|
||||
const {code,data,msg} = response
|
||||
if(code === STATUS_CODE.SUCCESS){
|
||||
const newPermits = generateNewPermit(data.permits)
|
||||
generateDependenciesMap(newPermits)
|
||||
setPermissionTemplate(newPermits)
|
||||
}else{
|
||||
message.error(msg || $t(RESPONSE_TIPS.dataError))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getPermissionInfo = ()=>{
|
||||
fetchData<BasicResponse<{role:PermissionInfo}>>(`${roleType}/role`,{method:'GET',eoParams:{role:roleId}}).then(response=>{
|
||||
const {code,data,msg} = response
|
||||
if(code === STATUS_CODE.SUCCESS){
|
||||
setPermissionInfo(data.role)
|
||||
}else{
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
}).catch((errInfo)=>console.error(errInfo))
|
||||
}
|
||||
|
||||
useEffect(()=>{
|
||||
form.setFieldsValue({name:$t(permissionInfo?.name || ''),permits:permissionInfo?.permit})
|
||||
},[permissionInfo, state.language])
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue({name:'',permits:[]})
|
||||
if(roleId){
|
||||
getPermissionInfo()
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(()=>{
|
||||
getPermissionTemplate()
|
||||
|
||||
},[state.language])
|
||||
|
||||
const onFinish =async() => {
|
||||
const body = await form.validateFields()
|
||||
|
||||
return fetchData<BasicResponse<null>>(`${roleType}/role`,{method:roleId === undefined? 'POST' : 'PUT',eoBody:({...body}),...(roleId !== undefined?{eoParams:{role:roleId}}:{})}).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))
|
||||
};
|
||||
|
||||
return (<div className="h-full flex flex-col overflow-hidden ">
|
||||
<div className="text-[18px] leading-[25px] pb-[12px]">
|
||||
<Button className="flex items-center" type="text" onClick={()=>navigateTo(-1)}><ArrowLeftOutlined className="max-h-[14px]" /><span>{$t('返回')}</span></Button>
|
||||
</div>
|
||||
<WithPermission access={roleId !== undefined ? `system.organization.role.${roleType}.edit`: `system.organization.role.${roleType}.add`}>
|
||||
<Form
|
||||
id="permission"
|
||||
layout='vertical'
|
||||
labelAlign='left'
|
||||
scrollToFirstError
|
||||
form={form}
|
||||
className="mx-auto w-full flex-1 no-bg-form overflow-hidden "
|
||||
name="rolePermissionConfig"
|
||||
onFinish={onFinish}
|
||||
autoComplete="off"
|
||||
return (
|
||||
<div id={id} className="flex flex-col gap-btnbase p-btnbase">
|
||||
{permits.map((item: PermissionClassify) => (
|
||||
<>
|
||||
<div className="flex flex-col gap-btnbase" key={`group-${item.name}`}>
|
||||
{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}
|
||||
>
|
||||
<div className="flex flex-col h-full">
|
||||
<Form.Item
|
||||
className=" m-btnbase mr-PAGE_INSIDE_X"
|
||||
name="name"
|
||||
rules={[{ required: true,whitespace:true }]}
|
||||
>
|
||||
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)}/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="permits"
|
||||
className="m-btnbase mr-0 flex-1 overflow-auto pr-PAGE_INSIDE_X"
|
||||
>
|
||||
<PermissionCollapse permissionTemplate={permissionTemplate!} dependenciesMap={dependenciesMap} />
|
||||
</Form.Item>
|
||||
|
||||
{APP_MODE === 'pro' && <div className="p-btnbase">
|
||||
<WithPermission access={roleId === undefined ?`system.organization.role.${roleType}.edit`:`system.organization.role.${roleType}.add`}>
|
||||
<Button type="primary" htmlType="submit">
|
||||
{$t('保存')}
|
||||
</Button>
|
||||
</WithPermission>
|
||||
<Button className="ml-btnrbase" type="default" onClick={() => navigateTo(-1)}>
|
||||
{$t('取消')}
|
||||
</Button>
|
||||
</div>}
|
||||
</div>
|
||||
</Form>
|
||||
</WithPermission>
|
||||
</div>)
|
||||
{x.name}
|
||||
</Checkbox>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default RoleConfig
|
||||
// 自定义表单控件
|
||||
const PermissionCollapse: React.FC<PermissionCollapseProps> = (props) => {
|
||||
const { id, value = [], onChange, permissionTemplate, dependenciesMap } = props
|
||||
const [openCollapses, setOpenCollapses] = useState<string[]>([])
|
||||
const { state } = useGlobalContext()
|
||||
|
||||
const items = useMemo(() => {
|
||||
const generatePermissionItem = (permissionItem: RolePermissionItem[]) =>
|
||||
permissionItem.map((item: RolePermissionItem) => ({
|
||||
key: item.name,
|
||||
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) : []
|
||||
}, [permissionTemplate, value, state.language])
|
||||
|
||||
useEffect(() => {
|
||||
permissionTemplate && setOpenCollapses(permissionTemplate?.map((x) => x.name))
|
||||
}, [permissionTemplate])
|
||||
|
||||
const onCollapseChange = (keys: string | string[]) => {
|
||||
setOpenCollapses(keys as string[])
|
||||
}
|
||||
|
||||
return <Collapse items={items} activeKey={openCollapses} onChange={onCollapseChange} />
|
||||
}
|
||||
|
||||
const RoleConfig = () => {
|
||||
const { message } = App.useApp()
|
||||
const [form] = Form.useForm()
|
||||
const { fetchData } = useFetch()
|
||||
const navigateTo = useNavigate()
|
||||
const { roleType, roleId } = useParams<RouterParams>()
|
||||
const [permissionTemplate, setPermissionTemplate] = useState<RolePermissionItem[]>()
|
||||
const [dependenciesMap, setDependenciesMap] = useState<DependenciesMapType>()
|
||||
const APP_MODE = import.meta.env.VITE_APP_MODE
|
||||
const [permissionInfo, setPermissionInfo] = useState<PermissionInfo>()
|
||||
const { state } = useGlobalContext()
|
||||
const { setBreadcrumb } = useBreadcrumb()
|
||||
|
||||
const generateDependenciesMap = (data: RolePermissionItem[]) => {
|
||||
const map = new Map<string, { dependents: string[]; control: string[] }>()
|
||||
data.forEach((item: RolePermissionItem) => {
|
||||
item.children.forEach((child: PermissionClassify) => {
|
||||
child.children.forEach((permission: PermissionItem & { dependents: string[] }) => {
|
||||
if (permission.dependents && permission.dependents.length > 0) {
|
||||
// 获取当前权限的依赖
|
||||
const currentDependents = map.get(permission.value)
|
||||
if (currentDependents) {
|
||||
currentDependents.dependents.push(...permission.dependents)
|
||||
} else {
|
||||
map.set(permission.value, { dependents: [...permission.dependents], control: [] })
|
||||
}
|
||||
|
||||
// 更新依赖项的控制项
|
||||
permission.dependents.forEach((dependent: string) => {
|
||||
const dependentEntry = map.get(dependent)
|
||||
if (dependentEntry) {
|
||||
dependentEntry.control.push(permission.value)
|
||||
} else {
|
||||
map.set(dependent, { dependents: [], control: [permission.value] })
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
setDependenciesMap(map)
|
||||
}
|
||||
|
||||
const generateNewPermit: (data: RolePermissionItem[]) => RolePermissionItem[] = (data: RolePermissionItem[]) => {
|
||||
return data.map((item: RolePermissionItem) => ({
|
||||
...item,
|
||||
children: item.children.map((child: PermissionClassify) => ({
|
||||
...child,
|
||||
children: child.children.map((permission: PermissionItem & { dependents: string[] }) => ({
|
||||
...permission,
|
||||
value: `${roleType}.${item.value}.${child.value}.${permission.value}`
|
||||
}))
|
||||
}))
|
||||
}))
|
||||
}
|
||||
|
||||
const getPermissionTemplate = () => {
|
||||
return fetchData<BasicResponse<{ permits: RolePermissionItem[] }>>(`${roleType}/role/template`, {
|
||||
method: 'GET'
|
||||
}).then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
const newPermits = generateNewPermit(data.permits)
|
||||
generateDependenciesMap(newPermits)
|
||||
setPermissionTemplate(newPermits)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.dataError))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getPermissionInfo = () => {
|
||||
fetchData<BasicResponse<{ role: PermissionInfo }>>(`${roleType}/role`, {
|
||||
method: 'GET',
|
||||
eoParams: { role: roleId }
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setPermissionInfo(data.role)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
.catch((errInfo) => console.error(errInfo))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue({ name: $t(permissionInfo?.name || ''), permits: permissionInfo?.permit })
|
||||
}, [permissionInfo, state.language])
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue({ name: '', permits: [] })
|
||||
if (roleId) {
|
||||
getPermissionInfo()
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setBreadcrumb([
|
||||
{
|
||||
title: $t('角色'),
|
||||
onClick: () => navigateTo(-1)
|
||||
},
|
||||
{ title: $t('角色配置') }
|
||||
])
|
||||
getPermissionTemplate()
|
||||
}, [state.language])
|
||||
|
||||
const onFinish = async () => {
|
||||
const body = await form.validateFields()
|
||||
|
||||
return fetchData<BasicResponse<null>>(`${roleType}/role`, {
|
||||
method: roleId === undefined ? 'POST' : 'PUT',
|
||||
eoBody: { ...body },
|
||||
...(roleId !== undefined ? { eoParams: { role: roleId } } : {})
|
||||
})
|
||||
.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))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col overflow-hidden ">
|
||||
<TopBreadcrumb handleBackCallback={() => navigateTo(-1)} />
|
||||
<WithPermission
|
||||
access={
|
||||
roleId !== undefined
|
||||
? `system.organization.role.${roleType}.edit`
|
||||
: `system.organization.role.${roleType}.add`
|
||||
}
|
||||
>
|
||||
<Form
|
||||
id="permission"
|
||||
layout="vertical"
|
||||
labelAlign="left"
|
||||
scrollToFirstError
|
||||
form={form}
|
||||
className="mx-auto w-full flex-1 no-bg-form overflow-hidden "
|
||||
name="rolePermissionConfig"
|
||||
onFinish={onFinish}
|
||||
autoComplete="off"
|
||||
>
|
||||
<div className="flex flex-col h-full">
|
||||
<Form.Item
|
||||
className=" m-btnbase mr-PAGE_INSIDE_X"
|
||||
name="name"
|
||||
rules={[{ required: true, whitespace: true }]}
|
||||
>
|
||||
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} />
|
||||
</Form.Item>
|
||||
<Form.Item name="permits" className="m-btnbase mr-0 flex-1 overflow-auto pr-PAGE_INSIDE_X">
|
||||
<PermissionCollapse permissionTemplate={permissionTemplate!} dependenciesMap={dependenciesMap} />
|
||||
</Form.Item>
|
||||
|
||||
{APP_MODE === 'pro' && (
|
||||
<div className="p-btnbase">
|
||||
<WithPermission
|
||||
access={
|
||||
roleId === undefined
|
||||
? `system.organization.role.${roleType}.edit`
|
||||
: `system.organization.role.${roleType}.add`
|
||||
}
|
||||
>
|
||||
<Button type="primary" htmlType="submit">
|
||||
{$t('保存')}
|
||||
</Button>
|
||||
</WithPermission>
|
||||
<Button className="ml-btnrbase" type="default" onClick={() => navigateTo(-1)}>
|
||||
{$t('取消')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Form>
|
||||
</WithPermission>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default RoleConfig
|
||||
|
||||
@@ -11,14 +11,26 @@ import { normFile } from '@common/utils/uploadPic.ts'
|
||||
import { validateUrlSlash } from '@common/utils/validate.ts'
|
||||
import { RouterParams } from '@core/components/aoplatform/RenderRoutes.tsx'
|
||||
import { AiServiceConfigFieldType } from '@core/const/ai-service/type.ts'
|
||||
import { SERVICE_APPROVAL_OPTIONS } from '@core/const/system/const.tsx'
|
||||
import { MCP_OPTIONS, 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, Tooltip, TreeSelect, Upload } from 'antd'
|
||||
import {
|
||||
App,
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
Radio,
|
||||
RadioChangeEvent,
|
||||
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'
|
||||
import { Link, useLocation, useNavigate, useParams } from 'react-router-dom'
|
||||
import { Link, useLocation, useNavigate, useOutletContext, 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'
|
||||
@@ -43,6 +55,8 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
|
||||
const [showAI, setShowAI] = useState<boolean>(false)
|
||||
const [imageBase64, setImageBase64] = useState<string | null>(null)
|
||||
const [tagOptionList, setTagOptionList] = useState<DefaultOptionType[]>([])
|
||||
const context = useOutletContext<{ onSaveComplete?: () => void }>()
|
||||
const onSaveComplete = context?.onSaveComplete
|
||||
const [serviceClassifyOptionList, setServiceClassifyOptionList] = useState<DefaultOptionType[]>()
|
||||
const [uploadLoading, setUploadLoading] = useState<boolean>(false)
|
||||
const { checkPermission, accessInit, getGlobalAccessData, state, aiConfigFlushed, setAiConfigFlushed } =
|
||||
@@ -274,6 +288,7 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
message.success(msg || $t(RESPONSE_TIPS.success))
|
||||
setSystemInfo(data.service)
|
||||
onSaveComplete?.()
|
||||
return Promise.resolve(true)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
@@ -342,7 +357,8 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
|
||||
getSystemInfo()
|
||||
setBreadcrumb([
|
||||
{
|
||||
title: <Link to={`/service/list`}>{$t('服务')}</Link>
|
||||
title: $t('服务'),
|
||||
onClick: () => navigate('/service/list')
|
||||
},
|
||||
{
|
||||
title: $t('设置')
|
||||
@@ -359,10 +375,32 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
|
||||
form.setFieldValue('team', teamId)
|
||||
form.setFieldValue('serviceType', 'public')
|
||||
form.setFieldValue('approvalType', 'auto')
|
||||
form.setFieldValue('enable_mcp', false)
|
||||
}
|
||||
return form.setFieldsValue({})
|
||||
}, [serviceId])
|
||||
|
||||
const handleMcpChange = (e: RadioChangeEvent) => {
|
||||
if (e.target.value) {
|
||||
return
|
||||
}
|
||||
modal.confirm({
|
||||
title: $t('关闭 MCP'),
|
||||
content: $t('关闭后将无法通过MCP方式调用服务'),
|
||||
onOk: () => {
|
||||
form.setFieldValue('enable_mcp', false)
|
||||
},
|
||||
onCancel: () => {
|
||||
form.setFieldValue('enable_mcp', true)
|
||||
},
|
||||
width: 600,
|
||||
okText: $t('了解'),
|
||||
cancelText: $t('取消'),
|
||||
closable: true,
|
||||
icon: <></>
|
||||
})
|
||||
}
|
||||
|
||||
const deleteSystemModal = async () => {
|
||||
modal.confirm({
|
||||
title: $t('删除'),
|
||||
@@ -387,6 +425,7 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
|
||||
() => SERVICE_APPROVAL_OPTIONS.map((x) => ({ ...x, label: $t(x.label) })),
|
||||
[state.language]
|
||||
)
|
||||
const mcpOptions = useMemo(() => MCP_OPTIONS.map((x) => ({ ...x, label: $t(x.label) })), [state.language])
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -440,6 +479,13 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item<AiServiceConfigFieldType> label={$t('MCP')} name="enable_mcp" rules={[{ required: true }]}>
|
||||
<Radio.Group
|
||||
className="flex flex-col"
|
||||
options={mcpOptions}
|
||||
onChange={serviceId ? handleMcpChange : undefined}
|
||||
/>
|
||||
</Form.Item>
|
||||
{showAI && (
|
||||
<>
|
||||
<Form.Item<AiServiceConfigFieldType>
|
||||
@@ -477,10 +523,14 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
|
||||
filterOption={(input, option) => (option?.searchText ?? '').includes(input.toLowerCase())}
|
||||
className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.input)}
|
||||
options={modelList ? modelList.map((x) => ({
|
||||
...x,
|
||||
searchText: x.name.toLowerCase()
|
||||
})) : []}
|
||||
options={
|
||||
modelList
|
||||
? modelList.map((x) => ({
|
||||
...x,
|
||||
searchText: x.name.toLowerCase()
|
||||
}))
|
||||
: []
|
||||
}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
</>
|
||||
@@ -501,6 +551,8 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
|
||||
{!onEdit && (
|
||||
<Form.Item<SystemConfigFieldType> label={$t('所属团队')} name="team" rules={[{ required: true }]}>
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
className="w-INPUT_NORMAL"
|
||||
disabled={onEdit}
|
||||
placeholder={$t(PLACEHOLDER.input)}
|
||||
@@ -577,6 +629,8 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
|
||||
|
||||
<Form.Item<SystemConfigFieldType> label={$t('标签')} name="tags">
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
className="w-INPUT_NORMAL"
|
||||
mode="tags"
|
||||
placeholder={$t(PLACEHOLDER.select)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import WithPermission from '@common/components/aoplatform/WithPermission.tsx'
|
||||
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const.tsx'
|
||||
import { EntityItem } from '@common/const/type.ts'
|
||||
import { useBreadcrumb } from '@common/contexts/BreadcrumbContext'
|
||||
import { useFetch } from '@common/hooks/http.ts'
|
||||
import { $t } from '@common/locales'
|
||||
import { RouterParams } from '@core/components/aoplatform/RenderRoutes'
|
||||
@@ -9,7 +10,7 @@ import { App, Button } from 'antd'
|
||||
import hljs from 'highlight.js'
|
||||
import 'highlight.js/styles/default.css'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
const ServiceInsideDocument = () => {
|
||||
const { message } = App.useApp()
|
||||
const [updater, setUpdater] = useState<string>()
|
||||
@@ -18,7 +19,8 @@ const ServiceInsideDocument = () => {
|
||||
const [doc, setDoc] = useState<string>()
|
||||
const { fetchData } = useFetch()
|
||||
const { serviceId, teamId } = useParams<RouterParams>()
|
||||
|
||||
const { setBreadcrumb } = useBreadcrumb()
|
||||
const navigator = useNavigate()
|
||||
const save = () => {
|
||||
fetchData<
|
||||
BasicResponse<{
|
||||
@@ -86,6 +88,15 @@ const ServiceInsideDocument = () => {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setBreadcrumb([
|
||||
{
|
||||
title: $t('服务'),
|
||||
onClick: () => navigator('/service/list')
|
||||
},
|
||||
{
|
||||
title: $t('使用说明')
|
||||
}
|
||||
])
|
||||
getServiceDoc()
|
||||
}, [])
|
||||
|
||||
|
||||
@@ -234,6 +234,7 @@ const SystemInsidePage: FC = () => {
|
||||
<InsidePage
|
||||
pageTitle={systemInfo?.name || '-'}
|
||||
tagList={[
|
||||
...(systemInfo?.enable_mcp ? [{ label: 'MCP', color: '#FFF0C1', className: 'text-[#000]' }] : []),
|
||||
{
|
||||
label: (
|
||||
<Paragraph className="mb-0" copyable={serviceId ? { text: serviceId } : false}>
|
||||
|
||||
@@ -20,7 +20,7 @@ import { RouterParams } from '@core/components/aoplatform/RenderRoutes.tsx'
|
||||
import { App, Form, TreeSelect } from 'antd'
|
||||
import { DefaultOptionType } from 'antd/es/cascader'
|
||||
import { FC, forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
|
||||
import { Link, useParams } from 'react-router-dom'
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom'
|
||||
import { SYSTEM_SUBSCRIBER_TABLE_COLUMNS } from '../../const/system/const.tsx'
|
||||
import {
|
||||
SimpleSystemItem,
|
||||
@@ -39,6 +39,7 @@ const SystemInsideSubscriber: FC = () => {
|
||||
const pageListRef = useRef<ActionType>(null)
|
||||
const [memberValueEnum, setMemberValueEnum] = useState<SimpleMemberItem[]>([])
|
||||
const { accessData, state } = useGlobalContext()
|
||||
const navigator = useNavigate()
|
||||
const getSystemSubscriber = () => {
|
||||
return fetchData<BasicResponse<{ subscribers: SystemSubscriberTableListItem[] }>>(
|
||||
'service/subscribers',
|
||||
@@ -163,7 +164,8 @@ const SystemInsideSubscriber: FC = () => {
|
||||
useEffect(() => {
|
||||
setBreadcrumb([
|
||||
{
|
||||
title: <Link to={`/service/list`}>{$t('服务')}</Link>
|
||||
title: $t('服务'),
|
||||
onClick: () => navigator('/service/list')
|
||||
},
|
||||
{
|
||||
title: $t('订阅方管理')
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useBreadcrumb } from '@common/contexts/BreadcrumbContext.tsx'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx'
|
||||
import { useFetch } from '@common/hooks/http.ts'
|
||||
import { $t } from '@common/locales/index.ts'
|
||||
import { App } from 'antd'
|
||||
import { App, Tag } from 'antd'
|
||||
import { FC, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { SERVICE_KIND_OPTIONS, SYSTEM_TABLE_COLUMNS } from '../../const/system/const.tsx'
|
||||
@@ -176,10 +176,30 @@ const SystemList: FC = () => {
|
||||
x.valueEnum = teamList
|
||||
}
|
||||
if ((x.dataIndex as string) === 'service_kind') {
|
||||
x.valueEnum = {}
|
||||
SERVICE_KIND_OPTIONS.forEach((option) => {
|
||||
;(x.valueEnum as any)[option.value] = { text: $t(option.label) }
|
||||
})
|
||||
x.render = (dom: React.ReactNode, record: any) => (
|
||||
<span
|
||||
className={`text-[13px] `}
|
||||
>
|
||||
<Tag
|
||||
color={`#${record.service_kind === 'ai' ? 'EADEFF' : 'DEFFE7'}`}
|
||||
className={`text-[#000] font-normal border-0 mr-[10px] max-w-[150px] truncate`}
|
||||
bordered={false}
|
||||
title={record.service_kind || '-'}
|
||||
>
|
||||
{SERVICE_KIND_OPTIONS.find((x) => x.value === record.service_kind)?.label || '-'}
|
||||
</Tag>
|
||||
{record?.enable_mcp && (
|
||||
<Tag
|
||||
color="#FFF0C1"
|
||||
className="text-[#000] font-normal border-0 mr-[12px] max-w-[150px] truncate"
|
||||
bordered={false}
|
||||
title={'MCP'}
|
||||
>
|
||||
MCP
|
||||
</Tag>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
if ((x.dataIndex as string) === 'state') {
|
||||
x.render = (dom: React.ReactNode, record: any) => (
|
||||
|
||||
@@ -28,6 +28,8 @@ export default function SystemTopology() {
|
||||
const { systemInfo } = useSystemContext()
|
||||
const { setBreadcrumb } = useBreadcrumb()
|
||||
const [zoomNum, setZoomNum] = useState<number>(1)
|
||||
const navigate = useNavigate()
|
||||
|
||||
|
||||
const getNodeData = () => {
|
||||
fetchData<BasicResponse<SystemTopologyResponse>>('service/topology', {
|
||||
@@ -105,7 +107,8 @@ export default function SystemTopology() {
|
||||
getNodeData()
|
||||
setBreadcrumb([
|
||||
{
|
||||
title: <Link to={`/service/list`}>{$t('服务')}</Link>
|
||||
title: $t('服务'),
|
||||
onClick: () => navigate('/service/list')
|
||||
},
|
||||
{
|
||||
title: $t('调用拓扑图')
|
||||
|
||||
@@ -9,12 +9,13 @@ import { $t } from '@common/locales/index.ts'
|
||||
import { RouterParams } from '@core/components/aoplatform/RenderRoutes.tsx'
|
||||
import { Button, Empty, Spin, Upload, message } from 'antd'
|
||||
import { forwardRef, useEffect, useState } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import {
|
||||
SystemApiDetail,
|
||||
SystemInsideApiDocumentHandle,
|
||||
SystemInsideApiDocumentProps
|
||||
} from '../../../const/system/type.ts'
|
||||
import { useBreadcrumb } from '@common/contexts/BreadcrumbContext.tsx'
|
||||
|
||||
const SystemInsideApiDocument = forwardRef<
|
||||
SystemInsideApiDocumentHandle,
|
||||
@@ -25,7 +26,18 @@ const SystemInsideApiDocument = forwardRef<
|
||||
const [apiDetail, setApiDetail] = useState<SystemApiDetail>()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [showEditor, setShowEditor] = useState<boolean>(false)
|
||||
const { setBreadcrumb } = useBreadcrumb()
|
||||
const navigator = useNavigate()
|
||||
useEffect(() => {
|
||||
setBreadcrumb([
|
||||
{
|
||||
title: $t('服务'),
|
||||
onClick: () => navigator('/service/list')
|
||||
},
|
||||
{
|
||||
title: $t('API 文档')
|
||||
}
|
||||
])
|
||||
getApiDetail()
|
||||
}, [])
|
||||
|
||||
|
||||
@@ -30,7 +30,10 @@ const SystemInsideApiProxy = forwardRef<SystemInsideApiProxyHandle,SystemInsideA
|
||||
const ProxyHeadeerConfig = useMemo(()=>PROXY_HEADER_CONFIG.map((x)=>({
|
||||
...x,
|
||||
...(x.key === 'optType' ? {
|
||||
component: <Select className="w-INPUT_NORMAL" options={UPSTREAM_PROXY_HEADER_TYPE_OPTIONS.map((x)=>({...x, label:$t(x.label)}))}/>
|
||||
component: <Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
className="w-INPUT_NORMAL" options={UPSTREAM_PROXY_HEADER_TYPE_OPTIONS.map((x)=>({...x, label:$t(x.label)}))}/>
|
||||
} : {})
|
||||
}))
|
||||
,[state.language])
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
SystemInsideRouterCreateHandle,
|
||||
SystemInsideRouterCreateProps
|
||||
} from '../../../const/system/type.ts'
|
||||
import { useBreadcrumb } from '@common/contexts/BreadcrumbContext.tsx'
|
||||
|
||||
const SystemInsideRouterCreate = forwardRef<SystemInsideRouterCreateHandle, SystemInsideRouterCreateProps>(
|
||||
(props, ref) => {
|
||||
@@ -38,6 +39,8 @@ const SystemInsideRouterCreate = forwardRef<SystemInsideRouterCreateHandle, Syst
|
||||
const { state } = useGlobalContext()
|
||||
const { apiPrefix, prefixForce } = useSystemContext()
|
||||
const navigator = useNavigate()
|
||||
const { setBreadcrumb } = useBreadcrumb()
|
||||
|
||||
|
||||
const onFinish = () => {
|
||||
return Promise.all([proxyRef.current?.validate?.(), form.validateFields()]).then(([, formValue]) => {
|
||||
@@ -144,6 +147,19 @@ const SystemInsideRouterCreate = forwardRef<SystemInsideRouterCreateHandle, Syst
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setBreadcrumb([
|
||||
{
|
||||
title: $t('服务'),
|
||||
onClick: () => navigator('/service/list')
|
||||
},
|
||||
{
|
||||
title:$t('API'),
|
||||
onClick: () => navigator(`/service/${teamId}/inside/${serviceId}/route`)
|
||||
},
|
||||
{
|
||||
title: routeId ? $t('编辑 API') : $t('添加 API')
|
||||
}
|
||||
])
|
||||
if (routeId) {
|
||||
getRouterConfig()
|
||||
} else {
|
||||
@@ -163,6 +179,8 @@ const SystemInsideRouterCreate = forwardRef<SystemInsideRouterCreateHandle, Syst
|
||||
...item,
|
||||
component: (
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
className="w-INPUT_NORMAL"
|
||||
options={Object.entries(MatchPositionEnum)?.map(([key, value]) => {
|
||||
return { label: $t(value), value: key }
|
||||
@@ -176,6 +194,8 @@ const SystemInsideRouterCreate = forwardRef<SystemInsideRouterCreateHandle, Syst
|
||||
...item,
|
||||
component: (
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
className="w-INPUT_NORMAL"
|
||||
options={Object.entries(MatchTypeEnum)?.map(([key, value]) => {
|
||||
return { label: $t(value), value: key }
|
||||
@@ -236,6 +256,8 @@ const SystemInsideRouterCreate = forwardRef<SystemInsideRouterCreateHandle, Syst
|
||||
|
||||
<Form.Item<SystemApiProxyFieldType> label={$t('请求协议')} name="protocols" rules={[{ required: true }]}>
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.select)}
|
||||
mode="multiple"
|
||||
@@ -255,6 +277,8 @@ const SystemInsideRouterCreate = forwardRef<SystemInsideRouterCreateHandle, Syst
|
||||
noStyle
|
||||
>
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
placeholder={$t(PLACEHOLDER.select)}
|
||||
options={apiPathMatchRulesOptions}
|
||||
className="w-[30%] min-w-[100px]"
|
||||
@@ -262,8 +286,12 @@ const SystemInsideRouterCreate = forwardRef<SystemInsideRouterCreateHandle, Syst
|
||||
</Form.Item>
|
||||
<Form.Item<SystemApiProxyFieldType>
|
||||
name="path"
|
||||
dependencies={['pathMatch']}
|
||||
rules={[
|
||||
{ required: true, whitespace: true },
|
||||
({ getFieldValue }) => ({
|
||||
required: getFieldValue('pathMatch') !== 'prefix',
|
||||
whitespace: true
|
||||
}),
|
||||
{
|
||||
validator: validateUrlSlash
|
||||
}
|
||||
@@ -287,6 +315,8 @@ const SystemInsideRouterCreate = forwardRef<SystemInsideRouterCreateHandle, Syst
|
||||
|
||||
<Form.Item<SystemApiProxyFieldType> label={$t('请求方式')} name="methods" rules={[{ required: true }]}>
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.select)}
|
||||
mode="multiple"
|
||||
|
||||
@@ -158,18 +158,21 @@ const SystemInsideRouterList: FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getMemberList()
|
||||
manualReloadTable()
|
||||
}, [serviceId])
|
||||
useEffect(() => {
|
||||
setBreadcrumb([
|
||||
{
|
||||
title: <Link to={`/service/list`}>{$t('服务')}</Link>
|
||||
title: $t('服务'),
|
||||
onClick: () => navigator('/service/list')
|
||||
},
|
||||
{
|
||||
title: $t('路由')
|
||||
}
|
||||
])
|
||||
getMemberList()
|
||||
manualReloadTable()
|
||||
}, [serviceId])
|
||||
}, [state.language])
|
||||
|
||||
const columns = useMemo(() => {
|
||||
return [...SYSTEM_API_TABLE_COLUMNS].map((x) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
import {ActionType} from "@ant-design/pro-components";
|
||||
import {FC, useEffect, useMemo, useRef, useState} from "react";
|
||||
import {Link, useLocation, useParams} from "react-router-dom";
|
||||
import {Link, useLocation, useNavigate, useParams} from "react-router-dom";
|
||||
import PageList, { PageProColumns } from "@common/components/aoplatform/PageList.tsx";
|
||||
import {useBreadcrumb} from "@common/contexts/BreadcrumbContext.tsx";
|
||||
import {App, Button} from "antd";
|
||||
@@ -40,6 +40,7 @@ const SystemInsideApprovalList:FC = ()=>{
|
||||
const [approvalBtnLoading,setApprovalBtnLoading] = useState<boolean>(false)
|
||||
const [memberValueEnum, setMemberValueEnum] = useState<SimpleMemberItem[]>([])
|
||||
const {accessData,state} = useGlobalContext()
|
||||
const navigator = useNavigate()
|
||||
|
||||
const openModal = async (type:'approval'|'view',entity:SubscribeApprovalTableListItem)=>{
|
||||
message.loading($t(RESPONSE_TIPS.loading))
|
||||
@@ -143,7 +144,8 @@ const SystemInsideApprovalList:FC = ()=>{
|
||||
useEffect(() => {
|
||||
setBreadcrumb([
|
||||
{
|
||||
title:<Link to={`/service/list`}>{$t('服务')}</Link>
|
||||
title: $t('服务'),
|
||||
onClick: () => navigator('/service/list')
|
||||
},
|
||||
{
|
||||
title:$t('订阅审核')
|
||||
|
||||
@@ -28,7 +28,8 @@ const SystemInsidePublic:FC = ()=>{
|
||||
useEffect(() => {
|
||||
setBreadcrumb([
|
||||
{
|
||||
title:<Link to={`/service/list`}>{$t('服务')}</Link>
|
||||
title: $t('服务'),
|
||||
onClick: () => navigateTo('/service/list')
|
||||
},
|
||||
{
|
||||
title:$t('发布')
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ActionType, ParamsType } from "@ant-design/pro-components";
|
||||
import { App, Button, Divider } from "antd";
|
||||
import { useState, useRef, useEffect, useMemo, FC } from "react";
|
||||
import { useParams, Link, useLocation } from "react-router-dom";
|
||||
import { useParams, Link, useLocation, useNavigate } from "react-router-dom";
|
||||
import PageList, { PageProColumns } from "@common/components/aoplatform/PageList";
|
||||
import { PublishApprovalModalContent } from "@common/components/aoplatform/PublishApprovalModalContent";
|
||||
import { PUBLISH_APPROVAL_RECORD_INNER_TABLE_COLUMN, PUBLISH_APPROVAL_VERSION_INNER_TABLE_COLUMN, PublishApplyStatusEnum, PublishStatusEnum, PublishTableStatusColorClass } from "@common/const/approval/const";
|
||||
@@ -44,6 +44,7 @@ const SystemInsidePublicList:FC = ()=>{
|
||||
const [drawerData, setDrawerData] = useState<PublishTableListItem|PublishVersionTableListItem >({} as PublishTableListItem)
|
||||
const [drawerOkTitle, setDrawerOkTitle] = useState<string>('确认')
|
||||
const [isOkToPublish, setIsOkToPublish] = useState<boolean>(false)
|
||||
const navigateTo = useNavigate()
|
||||
const getSystemPublishList = (params?: ParamsType & {
|
||||
pageSize?: number | undefined;
|
||||
current?: number | undefined;
|
||||
@@ -351,7 +352,8 @@ const SystemInsidePublicList:FC = ()=>{
|
||||
useEffect(() => {
|
||||
setBreadcrumb([
|
||||
{
|
||||
title:<Link to={`/service/list`}>{$t('服务')}</Link>
|
||||
title: $t('服务'),
|
||||
onClick: () => navigateTo('/service/list')
|
||||
},
|
||||
{
|
||||
title:$t('发布')
|
||||
|
||||
@@ -8,7 +8,7 @@ import EditableTable from "@common/components/aoplatform/EditableTable.tsx";
|
||||
import EditableTableWithModal from "@common/components/aoplatform/EditableTableWithModal.tsx";
|
||||
import WithPermission from "@common/components/aoplatform/WithPermission.tsx";
|
||||
import { UPSTREAM_TYPE_OPTIONS, SYSTEM_UPSTREAM_GLOBAL_CONFIG_TABLE_COLUMNS, schemeOptions, UPSTREAM_BALANCE_OPTIONS, UPSTREAM_PASS_HOST_OPTIONS, PROXY_HEADER_CONFIG, UPSTREAM_PROXY_HEADER_TYPE_OPTIONS } from "../../../const/system/const.tsx";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||
import { RouterParams } from "@core/components/aoplatform/RenderRoutes.tsx";
|
||||
import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE, VALIDATE_MESSAGE } from "@common/const/const.tsx";
|
||||
import { useFetch } from "@common/hooks/http.ts";
|
||||
@@ -36,6 +36,7 @@ const SystemInsideUpstreamContent= forwardRef<SystemInsideUpstreamContentHandle>
|
||||
const { setBreadcrumb } = useBreadcrumb()
|
||||
const [form] = Form.useForm();
|
||||
const {state} = useGlobalContext()
|
||||
const navigate = useNavigate()
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
save:()=>formRef.current?.save()
|
||||
@@ -110,7 +111,8 @@ const globalConfigNodesRule: FormItemProps['rules'] = [
|
||||
useEffect(() => {
|
||||
setBreadcrumb([
|
||||
{
|
||||
title: <Link to={`/service/list`}>{$t('服务')}</Link>
|
||||
title: $t('服务'),
|
||||
onClick: () => navigate('/service/list')
|
||||
},
|
||||
{
|
||||
title: $t('上游')
|
||||
@@ -127,7 +129,7 @@ const globalConfigNodesRule: FormItemProps['rules'] = [
|
||||
const ProxyHeadeerConfig = useMemo(()=>PROXY_HEADER_CONFIG.map((x)=>({
|
||||
...x,
|
||||
...(x.key === 'optType' ? {
|
||||
component: <Select className="w-INPUT_NORMAL" options={UPSTREAM_PROXY_HEADER_TYPE_OPTIONS.map((x)=>({...x, label:$t(x.label)}))}/>
|
||||
component: <Select showSearch optionFilterProp="label" className="w-INPUT_NORMAL" options={UPSTREAM_PROXY_HEADER_TYPE_OPTIONS.map((x)=>({...x, label:$t(x.label)}))}/>
|
||||
} : {})
|
||||
}))
|
||||
,[state.language])
|
||||
@@ -173,7 +175,7 @@ const globalConfigNodesRule: FormItemProps['rules'] = [
|
||||
name="scheme"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.select)} options={schemeOptions}>
|
||||
<Select showSearch optionFilterProp="label" className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.select)} options={schemeOptions}>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
@@ -190,7 +192,7 @@ const globalConfigNodesRule: FormItemProps['rules'] = [
|
||||
name="passHost"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.select)} options={passHostOptions} onChange={(val)=>setFormShowHost(val === 'rewrite')}>
|
||||
<Select showSearch optionFilterProp="label" className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.select)} options={passHostOptions} onChange={(val)=>setFormShowHost(val === 'rewrite')}>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
|
||||
@@ -1,215 +1,257 @@
|
||||
import { forwardRef, useEffect, useImperativeHandle, useMemo, useState} from "react";
|
||||
import {App, Button, Form, Input, Row, Select} from "antd";
|
||||
import {Link, useLocation, useNavigate, useParams} from "react-router-dom";
|
||||
import {RouterParams} from "@core/components/aoplatform/RenderRoutes.tsx";
|
||||
import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from 'react'
|
||||
import { App, Button, Form, Input, Row, Select } from 'antd'
|
||||
import { Link, useLocation, useNavigate, useParams } from 'react-router-dom'
|
||||
import { RouterParams } from '@core/components/aoplatform/RenderRoutes.tsx'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import {BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx";
|
||||
import {MemberItem} from "@common/const/type.ts";
|
||||
import {useFetch} from "@common/hooks/http.ts";
|
||||
import {DefaultOptionType} from "antd/es/cascader";
|
||||
import { TeamConfigFieldType } from "../../const/team/type.ts";
|
||||
import WithPermission from "@common/components/aoplatform/WithPermission.tsx";
|
||||
import { useBreadcrumb } from "@common/contexts/BreadcrumbContext.tsx";
|
||||
import { useTeamContext } from "../../contexts/TeamContext.tsx";
|
||||
import { useGlobalContext } from "@common/contexts/GlobalStateContext.tsx";
|
||||
import { $t } from "@common/locales/index.ts";
|
||||
import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const.tsx'
|
||||
import { MemberItem } from '@common/const/type.ts'
|
||||
import { useFetch } from '@common/hooks/http.ts'
|
||||
import { DefaultOptionType } from 'antd/es/cascader'
|
||||
import { TeamConfigFieldType } from '../../const/team/type.ts'
|
||||
import WithPermission from '@common/components/aoplatform/WithPermission.tsx'
|
||||
import { useBreadcrumb } from '@common/contexts/BreadcrumbContext.tsx'
|
||||
import { useTeamContext } from '../../contexts/TeamContext.tsx'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx'
|
||||
import { $t } from '@common/locales/index.ts'
|
||||
|
||||
export type TeamConfigHandle = {
|
||||
save:()=>Promise<string|boolean>|undefined
|
||||
save: () => Promise<string | boolean> | undefined
|
||||
}
|
||||
|
||||
type TeamConfigProps = {
|
||||
entity?:TeamConfigFieldType
|
||||
entity?: TeamConfigFieldType
|
||||
}
|
||||
|
||||
const TeamConfig= forwardRef<TeamConfigHandle,TeamConfigProps>((props,ref) => {
|
||||
const {entity} = props
|
||||
const { message } = App.useApp()
|
||||
const { teamId } = useParams<RouterParams>();
|
||||
const [onEdit, setOnEdit] = useState<boolean>(!!teamId)
|
||||
const [form] = Form.useForm();
|
||||
const location = useLocation()
|
||||
const currentUrl = location.pathname
|
||||
const {fetchData} = useFetch()
|
||||
const [managerOption, setManagerOption] = useState<DefaultOptionType[]>([])
|
||||
const { setBreadcrumb} = useBreadcrumb()
|
||||
const { setTeamInfo } =useTeamContext()
|
||||
const {checkPermission,accessInit,state} = useGlobalContext()
|
||||
const pageType= useMemo(()=>{
|
||||
if(!accessInit) return 'myteam'
|
||||
return checkPermission('system.workspace.team.view_all') ? 'manage' : 'myteam'
|
||||
},[checkPermission,accessInit])
|
||||
|
||||
const [canDelete, setCanDelete] = useState<boolean>(false)
|
||||
const navigateTo = useNavigate()
|
||||
useImperativeHandle(ref, () => ({
|
||||
save:onFinish
|
||||
}));
|
||||
|
||||
const onFinish = () => {
|
||||
return form.validateFields().then((value)=>{
|
||||
let params:{[k:string]:string} = {}
|
||||
if(pageType === 'manage'){
|
||||
params = {id:teamId!}
|
||||
}else{
|
||||
params = {team:teamId!}
|
||||
}
|
||||
return fetchData<BasicResponse<{team:TeamConfigFieldType}>>(pageType === 'manage'?'manager/team' : 'team',{method:onEdit ? 'PUT' : 'POST', eoParams:params,eoBody:(value),eoTransformKeys:['teamId']}).then(response=>{
|
||||
const {code,data,msg} = response
|
||||
if(code === STATUS_CODE.SUCCESS){
|
||||
message.success(msg || $t(RESPONSE_TIPS.success))
|
||||
setTeamInfo?.(data.team)
|
||||
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 TeamConfig = forwardRef<TeamConfigHandle, TeamConfigProps>((props, ref) => {
|
||||
const { entity } = props
|
||||
const { message } = App.useApp()
|
||||
const { teamId } = useParams<RouterParams>()
|
||||
const [onEdit, setOnEdit] = useState<boolean>(!!teamId)
|
||||
const [form] = Form.useForm()
|
||||
const location = useLocation()
|
||||
const currentUrl = location.pathname
|
||||
const { fetchData } = useFetch()
|
||||
const [managerOption, setManagerOption] = useState<DefaultOptionType[]>([])
|
||||
const { setBreadcrumb } = useBreadcrumb()
|
||||
const { setTeamInfo } = useTeamContext()
|
||||
const { checkPermission, accessInit, state } = useGlobalContext()
|
||||
const pageType = useMemo(() => {
|
||||
if (!accessInit) return 'myteam'
|
||||
return checkPermission('system.workspace.team.view_all') ? 'manage' : 'myteam'
|
||||
}, [checkPermission, accessInit])
|
||||
|
||||
// 获取表单默认值
|
||||
const getTeamInfo = () => {
|
||||
fetchData<BasicResponse<{ team: TeamConfigFieldType }>>(pageType === 'manage'?'manager/team' : 'team',{method:'GET',eoParams:(pageType === 'manage'? {id:teamId}:{team:teamId}),eoTransformKeys:['can_delete']}).then(response=>{
|
||||
const {code,data,msg} = response
|
||||
if(code === STATUS_CODE.SUCCESS){
|
||||
setCanDelete(data.team.canDelete)
|
||||
setTimeout(()=>{form.setFieldsValue({...data.team})},0)
|
||||
}else{
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
};
|
||||
const [canDelete, setCanDelete] = useState<boolean>(false)
|
||||
const navigateTo = useNavigate()
|
||||
useImperativeHandle(ref, () => ({
|
||||
save: onFinish
|
||||
}))
|
||||
|
||||
const getManagerList = ()=>{
|
||||
setManagerOption([])
|
||||
fetchData<BasicResponse<{ members: MemberItem[] }>>('simple/member',{method:'GET'}).then(response=>{
|
||||
const {code,data,msg} = response
|
||||
if(code === STATUS_CODE.SUCCESS){
|
||||
setManagerOption(data.members?.map((x:MemberItem)=>{return {
|
||||
label:x.name, value:x.id
|
||||
}}) || [])
|
||||
}else{
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
const onFinish = () => {
|
||||
return form.validateFields().then((value) => {
|
||||
let params: { [k: string]: string } = {}
|
||||
if (pageType === 'manage') {
|
||||
params = { id: teamId! }
|
||||
} else {
|
||||
params = { team: teamId! }
|
||||
}
|
||||
return fetchData<BasicResponse<{ team: TeamConfigFieldType }>>(pageType === 'manage' ? 'manager/team' : 'team', {
|
||||
method: onEdit ? 'PUT' : 'POST',
|
||||
eoParams: params,
|
||||
eoBody: value,
|
||||
eoTransformKeys: ['teamId']
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
message.success(msg || $t(RESPONSE_TIPS.success))
|
||||
setTeamInfo?.(data.team)
|
||||
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 getTeamInfo = () => {
|
||||
fetchData<BasicResponse<{ team: TeamConfigFieldType }>>(pageType === 'manage' ? 'manager/team' : 'team', {
|
||||
method: 'GET',
|
||||
eoParams: pageType === 'manage' ? { id: teamId } : { team: teamId },
|
||||
eoTransformKeys: ['can_delete']
|
||||
}).then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setCanDelete(data.team.canDelete)
|
||||
setTimeout(() => {
|
||||
form.setFieldsValue({ ...data.team })
|
||||
}, 0)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getManagerList = () => {
|
||||
setManagerOption([])
|
||||
fetchData<BasicResponse<{ members: MemberItem[] }>>('simple/member', { method: 'GET' }).then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setManagerOption(
|
||||
data.members?.map((x: MemberItem) => {
|
||||
return {
|
||||
label: x.name,
|
||||
value: x.id
|
||||
}
|
||||
}) || []
|
||||
)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const deleteTeam = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fetchData<BasicResponse<null>>(`manager/team`, { method: 'DELETE', eoParams: { id: form.getFieldValue('id') } })
|
||||
.then((response) => {
|
||||
const { code, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
message.success(msg || $t(RESPONSE_TIPS.success))
|
||||
navigateTo('/team/list')
|
||||
|
||||
resolve(true)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
reject(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => reject(errorInfo))
|
||||
})
|
||||
}
|
||||
useEffect(() => {
|
||||
setBreadcrumb([
|
||||
{
|
||||
title: $t('团队'),
|
||||
onClick: () => navigateTo('/team/list')
|
||||
},
|
||||
{ title: $t('设置') }
|
||||
])
|
||||
}, [state.language])
|
||||
useEffect(() => {
|
||||
|
||||
getManagerList()
|
||||
if (entity) {
|
||||
setOnEdit(true)
|
||||
form.setFieldsValue(entity)
|
||||
} else if (teamId !== undefined) {
|
||||
setOnEdit(true)
|
||||
getTeamInfo()
|
||||
} else {
|
||||
setOnEdit(false)
|
||||
form.setFieldsValue({ id: uuidv4(), master: state?.userData?.uid }) // 清空 initialValues
|
||||
}
|
||||
|
||||
const deleteTeam = ()=>{
|
||||
return new Promise((resolve, reject)=>{
|
||||
fetchData<BasicResponse<null>>(`manager/team`,{method:'DELETE',eoParams:{id:form.getFieldValue('id')}}).then(response=>{
|
||||
const {code,msg} = response
|
||||
if(code === STATUS_CODE.SUCCESS){
|
||||
message.success(msg || $t(RESPONSE_TIPS.success))
|
||||
navigateTo('/team/list')
|
||||
return form.setFieldsValue({})
|
||||
}, [teamId])
|
||||
|
||||
resolve(true)
|
||||
}else{
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
reject(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
}).catch((errorInfo)=> reject(errorInfo))
|
||||
})
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className="overflow-auto h-full w-full pr-PAGE_INSIDE_X">
|
||||
<WithPermission
|
||||
access={
|
||||
onEdit
|
||||
? currentUrl.split('/')[1] === 'myteam'
|
||||
? 'team.team.team.edit'
|
||||
: 'system.organization.team.edit'
|
||||
: 'system.organization.team.add'
|
||||
}
|
||||
>
|
||||
<Form
|
||||
layout="vertical"
|
||||
labelAlign="left"
|
||||
scrollToFirstError
|
||||
form={form}
|
||||
className={`mx-auto`}
|
||||
name="teamConfig"
|
||||
onFinish={onFinish}
|
||||
autoComplete="off"
|
||||
>
|
||||
<Form.Item<TeamConfigFieldType>
|
||||
label={$t('团队名称')}
|
||||
name="name"
|
||||
rules={[{ required: true, whitespace: true }]}
|
||||
>
|
||||
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} />
|
||||
</Form.Item>
|
||||
|
||||
useEffect(() => {
|
||||
getManagerList()
|
||||
if(entity){
|
||||
setOnEdit(true);
|
||||
form.setFieldsValue(entity)
|
||||
}else if (teamId !== undefined) {
|
||||
setBreadcrumb([
|
||||
{title:<Link to="/team/list">{$t('团队')}</Link>},
|
||||
{title:$t('设置')}
|
||||
])
|
||||
setOnEdit(true);
|
||||
getTeamInfo();
|
||||
} else {
|
||||
setOnEdit(false);
|
||||
form.setFieldsValue(
|
||||
{id:uuidv4(),
|
||||
master:state?.userData?.uid
|
||||
}); // 清空 initialValues
|
||||
}
|
||||
return (form.setFieldsValue({}))
|
||||
}, [teamId]);
|
||||
<Form.Item<TeamConfigFieldType>
|
||||
label={$t('团队 ID')}
|
||||
name="id"
|
||||
extra={$t('团队 ID(team_id)可用于检索团队,一旦保存无法修改。')}
|
||||
rules={[{ required: true, whitespace: true }]}
|
||||
>
|
||||
<Input className="w-INPUT_NORMAL" disabled={onEdit} placeholder={$t(PLACEHOLDER.input)} />
|
||||
</Form.Item>
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='overflow-auto h-full w-full pr-PAGE_INSIDE_X'>
|
||||
<WithPermission access={onEdit ?(currentUrl.split('/')[1] === 'myteam'? 'team.team.team.edit':'system.organization.team.edit') : 'system.organization.team.add'}>
|
||||
<Form
|
||||
layout='vertical'
|
||||
labelAlign='left'
|
||||
scrollToFirstError
|
||||
form={form}
|
||||
className={`mx-auto`}
|
||||
name="teamConfig"
|
||||
onFinish={onFinish}
|
||||
autoComplete="off"
|
||||
>
|
||||
<Form.Item<TeamConfigFieldType>
|
||||
label={$t("团队名称")}
|
||||
name="name"
|
||||
rules={[{ required: true,whitespace:true }]}
|
||||
>
|
||||
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)}/>
|
||||
</Form.Item>
|
||||
{!onEdit && (
|
||||
<Form.Item<TeamConfigFieldType>
|
||||
label={$t('团队负责人')}
|
||||
name="master"
|
||||
extra={$t('负责人对团队内的团队、服务、成员有管理权限')}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.select)}
|
||||
options={managerOption}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item<TeamConfigFieldType>
|
||||
label={$t("团队 ID")}
|
||||
name="id"
|
||||
extra={$t("团队 ID(team_id)可用于检索团队,一旦保存无法修改。")}
|
||||
rules={[{ required: true,whitespace:true }]}
|
||||
>
|
||||
<Input className="w-INPUT_NORMAL" disabled={onEdit} placeholder={$t(PLACEHOLDER.input)}/>
|
||||
</Form.Item>
|
||||
<Form.Item<TeamConfigFieldType> label={$t('描述')} name="description">
|
||||
<Input.TextArea className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} />
|
||||
</Form.Item>
|
||||
|
||||
{!onEdit &&
|
||||
<Form.Item<TeamConfigFieldType>
|
||||
label={$t("团队负责人")}
|
||||
name="master"
|
||||
extra={$t("负责人对团队内的团队、服务、成员有管理权限")}
|
||||
rules={[{required: true}]}
|
||||
>
|
||||
<Select className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.select)} options={managerOption}>
|
||||
</Select>
|
||||
</Form.Item>}
|
||||
|
||||
|
||||
<Form.Item<TeamConfigFieldType>
|
||||
label={$t("描述")}
|
||||
name="description"
|
||||
>
|
||||
<Input.TextArea className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)}/>
|
||||
</Form.Item>
|
||||
|
||||
{ onEdit &&
|
||||
<Row className="mb-[10px]"
|
||||
>
|
||||
<WithPermission access={['system.organization.team.edit','team.team.team.edit']}><Button type="primary" htmlType="submit">
|
||||
{$t('保存')}
|
||||
</Button></WithPermission>
|
||||
</Row>
|
||||
}
|
||||
{onEdit &&
|
||||
<WithPermission access="system.organization.team.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="system.organization.team.delete" disabled={!canDelete} tooltip={canDelete ? '':$t('服务数据清除后,方可删除')}>
|
||||
<Button className="m-auto mt-[16px] mb-[20px]" type="default" danger onClick={()=>deleteTeam()}>{$t('删除')}</Button>
|
||||
</WithPermission>
|
||||
</div>
|
||||
</div>
|
||||
</WithPermission>
|
||||
}
|
||||
</Form>
|
||||
{onEdit && (
|
||||
<Row className="mb-[10px]">
|
||||
<WithPermission access={['system.organization.team.edit', 'team.team.team.edit']}>
|
||||
<Button type="primary" htmlType="submit">
|
||||
{$t('保存')}
|
||||
</Button>
|
||||
</WithPermission>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
</Row>
|
||||
)}
|
||||
{onEdit && (
|
||||
<WithPermission access="system.organization.team.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="system.organization.team.delete"
|
||||
disabled={!canDelete}
|
||||
tooltip={canDelete ? '' : $t('服务数据清除后,方可删除')}
|
||||
>
|
||||
<Button className="m-auto mt-[16px] mb-[20px]" type="default" danger onClick={() => deleteTeam()}>
|
||||
{$t('删除')}
|
||||
</Button>
|
||||
</WithPermission>
|
||||
</div>
|
||||
</div>
|
||||
</WithPermission>
|
||||
)}
|
||||
</Form>
|
||||
</WithPermission>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
})
|
||||
export default TeamConfig
|
||||
export default TeamConfig
|
||||
|
||||
@@ -1,347 +1,433 @@
|
||||
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, Button, Modal, Select} from "antd";
|
||||
import {BasicResponse, COLUMNS_TITLE, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx";
|
||||
import {useFetch} from "@common/hooks/http.ts";
|
||||
import {RouterParams} from "@core/components/aoplatform/RenderRoutes.tsx";
|
||||
import {EntityItem, MemberItem} from "@common/const/type.ts";
|
||||
import { TeamMemberTableListItem } from "../../const/team/type.ts";
|
||||
import { TEAM_MEMBER_TABLE_COLUMNS } from "../../const/team/const.tsx";
|
||||
import TableBtnWithPermission from "@common/components/aoplatform/TableBtnWithPermission.tsx";
|
||||
import { checkAccess } from "@common/utils/permission.ts";
|
||||
import { useGlobalContext } from "@common/contexts/GlobalStateContext.tsx";
|
||||
import MemberTransfer, { TransferTableHandle } from "@common/components/aoplatform/MemberTransfer.tsx";
|
||||
import { DepartmentListItem } from "../../const/member/type.ts";
|
||||
import {v4 as uuidv4} from 'uuid'
|
||||
import WithPermission from "@common/components/aoplatform/WithPermission.tsx";
|
||||
import { $t } from "@common/locales/index.ts";
|
||||
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, useNavigate, useParams } from 'react-router-dom'
|
||||
import { useBreadcrumb } from '@common/contexts/BreadcrumbContext.tsx'
|
||||
import { App, Button, Modal, Select } from 'antd'
|
||||
import { BasicResponse, COLUMNS_TITLE, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const.tsx'
|
||||
import { useFetch } from '@common/hooks/http.ts'
|
||||
import { RouterParams } from '@core/components/aoplatform/RenderRoutes.tsx'
|
||||
import { EntityItem, MemberItem } from '@common/const/type.ts'
|
||||
import { TeamMemberTableListItem } from '../../const/team/type.ts'
|
||||
import { TEAM_MEMBER_TABLE_COLUMNS } from '../../const/team/const.tsx'
|
||||
import TableBtnWithPermission from '@common/components/aoplatform/TableBtnWithPermission.tsx'
|
||||
import { checkAccess } from '@common/utils/permission.ts'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx'
|
||||
import MemberTransfer, { TransferTableHandle } from '@common/components/aoplatform/MemberTransfer.tsx'
|
||||
import { DepartmentListItem } from '../../const/member/type.ts'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import WithPermission from '@common/components/aoplatform/WithPermission.tsx'
|
||||
import { $t } from '@common/locales/index.ts'
|
||||
|
||||
export const getDepartmentWithMember = (department:(DepartmentListItem & {type?:'department'|'member'})[],departmentMap:Map<string, (MemberItem & {type:'department'|'member'})[]>) : (DepartmentWithMemberItem | undefined)[] =>{
|
||||
return department?.map((x:DepartmentListItem & {type?:'department'|'member'})=>{
|
||||
const res = ({
|
||||
...x,
|
||||
key:x.id,
|
||||
title:x.name,
|
||||
type: x.type || 'department',
|
||||
children:((x.type === 'member' || (!x.children||x.children.length === 0 )&& (!departmentMap.get(x.id) || departmentMap.get(x.id)!.length === 0))? undefined : [...(x.children && x.children.length > 0 ? getDepartmentWithMember(x.children,departmentMap) : []),...departmentMap.get(x.id) || []])
|
||||
});
|
||||
return res})?.filter(node=>node.type === 'member' ||( node.children && node.children.length > 0)) || []
|
||||
export const getDepartmentWithMember = (
|
||||
department: (DepartmentListItem & { type?: 'department' | 'member' })[],
|
||||
departmentMap: Map<string, (MemberItem & { type: 'department' | 'member' })[]>
|
||||
): (any)[] => {
|
||||
return (
|
||||
department
|
||||
?.map((x: DepartmentListItem & { type?: 'department' | 'member' }) => {
|
||||
const res = {
|
||||
...x,
|
||||
key: x.id,
|
||||
title: x.name,
|
||||
type: x.type || 'department',
|
||||
children:
|
||||
x.type === 'member' ||
|
||||
((!x.children || x.children.length === 0) &&
|
||||
(!departmentMap.get(x.id) || departmentMap.get(x.id)!.length === 0))
|
||||
? undefined
|
||||
: [
|
||||
...(x.children && x.children.length > 0 ? getDepartmentWithMember(x.children, departmentMap) : []),
|
||||
...(departmentMap.get(x.id) || [])
|
||||
]
|
||||
}
|
||||
return res
|
||||
})
|
||||
?.filter((node) => node.type === 'member' || (node.children && node.children.length > 0)) || []
|
||||
)
|
||||
}
|
||||
|
||||
export const addMemberToDepartment = (departmentMap: Map<string, (MemberItem & {type:'department'|'member'})[]>, departmentId: string, member: MemberItem) => {
|
||||
const members = departmentMap.get(departmentId) || [];
|
||||
members.push({...member, type: 'member'});
|
||||
departmentMap.set(departmentId, members);
|
||||
export const addMemberToDepartment = (
|
||||
departmentMap: Map<string, (MemberItem & { type: 'department' | 'member' })[]>,
|
||||
departmentId: string,
|
||||
member: MemberItem
|
||||
) => {
|
||||
const members = departmentMap.get(departmentId) || []
|
||||
members.push({ ...member, type: 'member' })
|
||||
departmentMap.set(departmentId, members)
|
||||
}
|
||||
|
||||
const TeamInsideMember: FC = () => {
|
||||
const [searchWord, setSearchWord] = useState<string>('')
|
||||
const { setBreadcrumb } = useBreadcrumb()
|
||||
const { modal, message } = App.useApp()
|
||||
const { fetchData } = useFetch()
|
||||
const { teamId } = useParams<RouterParams>()
|
||||
const addRef = useRef<TransferTableHandle<TeamMemberTableListItem>>(null)
|
||||
const pageListRef = useRef<ActionType>(null)
|
||||
const [allMemberIds, setAllMemberIds] = useState<string[]>([])
|
||||
const { accessData, state } = useGlobalContext()
|
||||
const [selectableMemberIds, setSelectableMemberIds] = useState<Set<string>>(new Set())
|
||||
const [addMemberBtnLoading, setAddMemberBtnLoading] = useState<boolean>(false)
|
||||
const [modalVisible, setModalVisible] = useState<boolean>(false)
|
||||
const [addMemberBtnDisabled, setAddMemberBtnDisabled] = useState<boolean>(true)
|
||||
const [allMemberSelectedDepartIds, setAllMemberSelectedDepartIds] = useState<string[]>([])
|
||||
const [roleList, setRoleList] = useState<EntityItem[]>([])
|
||||
const navigator = useNavigate()
|
||||
|
||||
const operation: PageProColumns<TeamMemberTableListItem>[] = [
|
||||
{
|
||||
title: COLUMNS_TITLE.operate,
|
||||
key: 'option',
|
||||
btnNums: 1,
|
||||
fixed: 'right',
|
||||
valueType: 'option',
|
||||
render: (_: React.ReactNode, entity: TeamMemberTableListItem) => [
|
||||
<TableBtnWithPermission
|
||||
disabled={!entity.isDelete}
|
||||
tooltip="暂无权限"
|
||||
access="team.team.member.edit"
|
||||
key="delete"
|
||||
btnType="delete"
|
||||
onClick={() => {
|
||||
openModal('remove', entity)
|
||||
}}
|
||||
btnTitle="移出团队"
|
||||
/>
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const getDepartmentMemberList = () => {
|
||||
const topDepartmentId: string = uuidv4()
|
||||
return Promise.all([
|
||||
fetchData<BasicResponse<{ department: DepartmentListItem }>>('simple/departments', { method: 'GET' }),
|
||||
fetchData<BasicResponse<{ members: MemberItem }>>('simple/member', {
|
||||
method: 'GET',
|
||||
eoParams: {},
|
||||
eoTransformKeys: []
|
||||
})
|
||||
])
|
||||
.then(([departmentResponse, memberResponse]) => {
|
||||
const departmentMap = new Map<string, (MemberItem & { type: 'department' | 'member' })[]>()
|
||||
memberResponse.data.members.forEach((member: MemberItem) => {
|
||||
setSelectableMemberIds((pre) => {
|
||||
pre.add(member.id)
|
||||
return pre
|
||||
})
|
||||
member = { ...member, title: member.name, key: member.id }
|
||||
if (member.department) {
|
||||
member.department.forEach((department: EntityItem) => {
|
||||
addMemberToDepartment(departmentMap, department.id, member)
|
||||
})
|
||||
} else {
|
||||
addMemberToDepartment(departmentMap, '_withoutDepartment', member)
|
||||
}
|
||||
})
|
||||
|
||||
const finalData = departmentResponse.data.department
|
||||
? [
|
||||
{
|
||||
id: topDepartmentId,
|
||||
key: topDepartmentId,
|
||||
name: departmentResponse.data.department.name,
|
||||
title: departmentResponse.data.department.name,
|
||||
children: [
|
||||
...getDepartmentWithMember(departmentResponse.data.department?.children || [], departmentMap),
|
||||
...(departmentMap.get('_withoutDepartment') || [])
|
||||
]
|
||||
}
|
||||
]
|
||||
: [...(departmentMap.get('_withoutDepartment') || [])]
|
||||
|
||||
let allMemberSelectedFlag: boolean = true
|
||||
for (const [k, v] of departmentMap) {
|
||||
if (k !== '_withoutDepartment' && allMemberIds.length > 0) {
|
||||
// 筛选出部门内没被勾选的用户,如果不存在没勾选用户,需要将部门id放入ids中
|
||||
if (v.filter((m) => allMemberIds.indexOf(m.id) === -1).length === 0) {
|
||||
setAllMemberSelectedDepartIds((pre) => [...pre, k])
|
||||
} else if (['unknown', 'disable'].indexOf(k) === -1) {
|
||||
allMemberSelectedFlag = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
departmentMap.get('_withoutDepartment')?.filter((x) => allMemberIds.indexOf(x) !== -1).length === 0 &&
|
||||
allMemberSelectedFlag
|
||||
) {
|
||||
setAllMemberSelectedDepartIds((pre) => [...pre, topDepartmentId])
|
||||
}
|
||||
|
||||
return { data: finalData, success: true }
|
||||
})
|
||||
.catch(() => ({ data: [], success: false }))
|
||||
}
|
||||
|
||||
const TeamInsideMember:FC = ()=>{
|
||||
const [searchWord, setSearchWord] = useState<string>('')
|
||||
const { setBreadcrumb} = useBreadcrumb()
|
||||
const { modal,message } = App.useApp()
|
||||
const {fetchData} = useFetch()
|
||||
const {teamId} = useParams<RouterParams>();
|
||||
const addRef = useRef<TransferTableHandle<TeamMemberTableListItem>>(null)
|
||||
const pageListRef = useRef<ActionType>(null);
|
||||
const [allMemberIds, setAllMemberIds] = useState<string[]>([])
|
||||
const {accessData,state} = useGlobalContext()
|
||||
const [selectableMemberIds,setSelectableMemberIds] = useState<Set<string>>(new Set())
|
||||
const [addMemberBtnLoading, setAddMemberBtnLoading] = useState<boolean>(false)
|
||||
const [modalVisible, setModalVisible] = useState<boolean>(false)
|
||||
const [addMemberBtnDisabled, setAddMemberBtnDisabled] = useState<boolean>(true)
|
||||
const [allMemberSelectedDepartIds, setAllMemberSelectedDepartIds] = useState<string[]>([])
|
||||
const [roleList, setRoleList] = useState<EntityItem[]>([])
|
||||
|
||||
const operation:PageProColumns<TeamMemberTableListItem>[] =[
|
||||
{
|
||||
title: COLUMNS_TITLE.operate,
|
||||
key: 'option',
|
||||
btnNums:1,
|
||||
fixed:'right',
|
||||
valueType: 'option',
|
||||
render: (_: React.ReactNode, entity: TeamMemberTableListItem) => [
|
||||
<TableBtnWithPermission disabled={!entity.isDelete} tooltip="暂无权限" access="team.team.member.edit" key="delete" btnType="delete" onClick={()=>{openModal('remove',entity)}} btnTitle="移出团队"/>]
|
||||
}
|
||||
]
|
||||
|
||||
const getDepartmentMemberList = () => {
|
||||
const topDepartmentId:string = uuidv4()
|
||||
return Promise.all([
|
||||
fetchData<BasicResponse<{department:DepartmentListItem}>>('simple/departments', {method:'GET'}),
|
||||
fetchData<BasicResponse<{members:MemberItem}>>('simple/member', {method:'GET', eoParams:{}, eoTransformKeys:[]})
|
||||
]).then(([departmentResponse, memberResponse])=>{
|
||||
const departmentMap = new Map<string, (MemberItem & {type:'department'|'member'})[]>();
|
||||
memberResponse.data.members.forEach((member: MemberItem) => {
|
||||
setSelectableMemberIds((pre)=>{pre.add(member.id);return pre})
|
||||
member = {...member, title:member.name, key:member.id}
|
||||
if (member.department) {
|
||||
member.department.forEach((department: EntityItem) => {
|
||||
addMemberToDepartment(departmentMap, department.id, member);
|
||||
});
|
||||
} else {
|
||||
addMemberToDepartment(departmentMap, '_withoutDepartment', member);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const finalData = departmentResponse.data.department
|
||||
? [
|
||||
{
|
||||
id: topDepartmentId,
|
||||
key:topDepartmentId,
|
||||
name: departmentResponse.data.department.name,
|
||||
title:departmentResponse.data.department.name,
|
||||
children: [
|
||||
...getDepartmentWithMember(departmentResponse.data.department?.children || [], departmentMap),
|
||||
...departmentMap.get('_withoutDepartment') || []
|
||||
]
|
||||
}
|
||||
]
|
||||
: [...departmentMap.get('_withoutDepartment') || []];
|
||||
|
||||
let allMemberSelectedFlag:boolean = true
|
||||
for(const [k,v] of departmentMap){
|
||||
if(k !== '_withoutDepartment' && allMemberIds.length > 0 ){
|
||||
// 筛选出部门内没被勾选的用户,如果不存在没勾选用户,需要将部门id放入ids中
|
||||
if(v.filter(m => allMemberIds.indexOf(m.id) === -1).length === 0){
|
||||
setAllMemberSelectedDepartIds((pre)=>[...pre, k])
|
||||
}else if(['unknown','disable'].indexOf(k) === -1){
|
||||
allMemberSelectedFlag = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(departmentMap.get('_withoutDepartment')?.filter(x=>allMemberIds.indexOf(x)!==-1).length === 0 && allMemberSelectedFlag){
|
||||
setAllMemberSelectedDepartIds((pre)=>[...pre, topDepartmentId])
|
||||
}
|
||||
|
||||
return {data:finalData, success: true}
|
||||
}).catch(()=>({data:[], success:false}))
|
||||
}
|
||||
|
||||
|
||||
const getMemberList = ()=>{
|
||||
return fetchData<BasicResponse<{members:TeamMemberTableListItem}>>('team/members',{method:'GET',eoParams:{keyword:searchWord, team:teamId},eoTransformKeys:['attach_time','is_delete']}).then(response=>{
|
||||
const {code,data,msg} = response
|
||||
if(code === STATUS_CODE.SUCCESS){
|
||||
if(!searchWord){
|
||||
setAllMemberIds(data.members?.map((x:TeamMemberTableListItem)=>x.user.id) || [])
|
||||
}
|
||||
return {data:data.members, success: true}
|
||||
}else{
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
return {data:[], success:false}
|
||||
}
|
||||
}).catch(() => {
|
||||
return {data:[], success:false}
|
||||
})
|
||||
}
|
||||
|
||||
const addMember = (selectableMemberIds:Set<string>)=>{
|
||||
setAddMemberBtnLoading(true)
|
||||
const keyFromModal = addRef.current?.selectedRowKeys()
|
||||
const memberKeyFromModal = keyFromModal?.filter(x => allMemberIds.indexOf(x as string) === -1 && selectableMemberIds.has(x)) || [];
|
||||
return new Promise((resolve, reject)=>{
|
||||
fetchData<BasicResponse<null>>('team/member',{method:'POST' ,eoBody:({users:memberKeyFromModal}),eoParams:{team:teamId}}).then(response=>{
|
||||
const {code,msg} = response
|
||||
if(code === STATUS_CODE.SUCCESS){
|
||||
message.success(msg || $t(RESPONSE_TIPS.success))
|
||||
manualReloadTable()
|
||||
cleanModalData()
|
||||
resolve(true)
|
||||
}else{
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
reject(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
}).catch((errorInfo)=> reject(errorInfo)).finally(()=>setAddMemberBtnLoading(false))
|
||||
const getMemberList = () => {
|
||||
return fetchData<BasicResponse<{ members: TeamMemberTableListItem }>>('team/members', {
|
||||
method: 'GET',
|
||||
eoParams: { keyword: searchWord, team: teamId },
|
||||
eoTransformKeys: ['attach_time', 'is_delete']
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const removeMember = (entity:TeamMemberTableListItem) =>{
|
||||
return new Promise((resolve, reject)=>{
|
||||
fetchData<BasicResponse<null>>(`team/member`,{method:'DELETE',eoParams:{team:teamId,user:entity.user.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 cleanModalData = ()=>{
|
||||
setModalVisible(false);setAddMemberBtnDisabled(true);setAddMemberBtnLoading(false)
|
||||
}
|
||||
|
||||
const openModal = async (type:'add'|'remove',entity?:TeamMemberTableListItem)=>{
|
||||
let title:string = ''
|
||||
let content:string|React.ReactNode = ''
|
||||
switch(type){
|
||||
case 'add':
|
||||
setModalVisible(true)
|
||||
setAddMemberBtnDisabled(true)
|
||||
setAddMemberBtnLoading(false)
|
||||
return
|
||||
case 'remove':
|
||||
title=$t('移除成员')
|
||||
content=<span>{$t('确定删除成员?此操作无法恢复,确认操作?')}</span>
|
||||
break
|
||||
.then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
if (!searchWord) {
|
||||
setAllMemberIds(data.members?.map((x: TeamMemberTableListItem) => x.user.id) || [])
|
||||
}
|
||||
return { data: data.members, success: true }
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
return { data: [], success: false }
|
||||
}
|
||||
|
||||
modal.confirm({
|
||||
title,
|
||||
content,
|
||||
onOk:()=>{
|
||||
return removeMember(entity!).then((res)=>{if(res === true) manualReloadTable()})
|
||||
},
|
||||
width:600,
|
||||
okText:$t('确认'),
|
||||
okButtonProps:{
|
||||
disabled: !checkAccess(`team.team.member.edit`,accessData)
|
||||
},
|
||||
cancelText:$t('取消'),
|
||||
closable:true,
|
||||
icon:<></>
|
||||
})
|
||||
.catch(() => {
|
||||
return { data: [], success: false }
|
||||
})
|
||||
}
|
||||
|
||||
const addMember = (selectableMemberIds: Set<string>) => {
|
||||
setAddMemberBtnLoading(true)
|
||||
const keyFromModal = addRef.current?.selectedRowKeys()
|
||||
const memberKeyFromModal =
|
||||
keyFromModal?.filter((x) => allMemberIds.indexOf(x as string) === -1 && selectableMemberIds.has(x)) || []
|
||||
return new Promise((resolve, reject) => {
|
||||
fetchData<BasicResponse<null>>('team/member', {
|
||||
method: 'POST',
|
||||
eoBody: { users: memberKeyFromModal },
|
||||
eoParams: { team: teamId }
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
message.success(msg || $t(RESPONSE_TIPS.success))
|
||||
manualReloadTable()
|
||||
cleanModalData()
|
||||
resolve(true)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
reject(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => reject(errorInfo))
|
||||
.finally(() => setAddMemberBtnLoading(false))
|
||||
})
|
||||
}
|
||||
|
||||
const removeMember = (entity: TeamMemberTableListItem) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fetchData<BasicResponse<null>>(`team/member`, {
|
||||
method: 'DELETE',
|
||||
eoParams: { team: teamId, user: entity.user.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 cleanModalData = () => {
|
||||
setModalVisible(false)
|
||||
setAddMemberBtnDisabled(true)
|
||||
setAddMemberBtnLoading(false)
|
||||
}
|
||||
|
||||
const openModal = async (type: 'add' | 'remove', entity?: TeamMemberTableListItem) => {
|
||||
let title: string = ''
|
||||
let content: string | React.ReactNode = ''
|
||||
switch (type) {
|
||||
case 'add':
|
||||
setModalVisible(true)
|
||||
setAddMemberBtnDisabled(true)
|
||||
setAddMemberBtnLoading(false)
|
||||
return
|
||||
case 'remove':
|
||||
title = $t('移除成员')
|
||||
content = <span>{$t('确定删除成员?此操作无法恢复,确认操作?')}</span>
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
const manualReloadTable = () => {
|
||||
pageListRef.current?.reload()
|
||||
};
|
||||
|
||||
|
||||
const changeMemberInfo = (value:string[],entity:TeamMemberTableListItem )=>{
|
||||
return new Promise((resolve, reject) => {
|
||||
fetchData<BasicResponse<null>>(`team/member/role`, {method: 'PUT',eoBody:({roles:value, users:[entity.user.id]}), eoParams: {team:teamId}}).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))
|
||||
modal.confirm({
|
||||
title,
|
||||
content,
|
||||
onOk: () => {
|
||||
return removeMember(entity!).then((res) => {
|
||||
if (res === true) manualReloadTable()
|
||||
})
|
||||
}
|
||||
|
||||
const getRoleList = ()=>{
|
||||
fetchData<BasicResponse<{roles:EntityItem[]}>>('simple/roles', {method: 'GET', eoParams: {group:'team'}}).then(response => {
|
||||
const {code, data,msg} = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setRoleList(data.roles)
|
||||
return
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
},
|
||||
width: 600,
|
||||
okText: $t('确认'),
|
||||
okButtonProps: {
|
||||
disabled: !checkAccess(`team.team.member.edit`, accessData)
|
||||
},
|
||||
cancelText: $t('取消'),
|
||||
closable: true,
|
||||
icon: <></>
|
||||
})
|
||||
}
|
||||
|
||||
const manualReloadTable = () => {
|
||||
pageListRef.current?.reload()
|
||||
}
|
||||
|
||||
const changeMemberInfo = (value: string[], entity: TeamMemberTableListItem) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fetchData<BasicResponse<null>>(`team/member/role`, {
|
||||
method: 'PUT',
|
||||
eoBody: { roles: value, users: [entity.user.id] },
|
||||
eoParams: { team: teamId }
|
||||
})
|
||||
.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 translatedCol = useMemo(()=>{
|
||||
const res = TEAM_MEMBER_TABLE_COLUMNS?.map(x=>{
|
||||
if(x.dataIndex === 'roles'){
|
||||
return {
|
||||
...x,
|
||||
title: typeof x.title === 'string' ? $t(x.title as string) : x.title,
|
||||
render: (_,entity)=>(
|
||||
<WithPermission access="team.team.member.edit">
|
||||
<Select
|
||||
className="w-full"
|
||||
mode="multiple"
|
||||
maxTagCount="responsive"
|
||||
value={entity.roles?.map((x:EntityItem)=>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()
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</WithPermission>
|
||||
),
|
||||
filters:roleList?.map((x:{id:string,name:string})=>({text:x.name, value:x.id})),
|
||||
onFilter:(value: unknown, record:TeamMemberTableListItem) =>{
|
||||
return record.roles ? record.roles?.map((x)=>x.id).indexOf(value as string) !== -1 : false;}
|
||||
}
|
||||
}
|
||||
return({...x, title: typeof x.title === 'string' ? $t(x.title as string) : x.title}) })
|
||||
return res
|
||||
},[ state.language,roleList])
|
||||
const getRoleList = () => {
|
||||
fetchData<BasicResponse<{ roles: EntityItem[] }>>('simple/roles', {
|
||||
method: 'GET',
|
||||
eoParams: { group: 'team' }
|
||||
}).then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setRoleList(data.roles)
|
||||
return
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setBreadcrumb([
|
||||
{title:<Link to="/team/list">{$t('团队')}</Link>},
|
||||
{title:$t('成员')}
|
||||
])
|
||||
manualReloadTable()
|
||||
}, [teamId]);
|
||||
const translatedCol = useMemo(() => {
|
||||
const res = TEAM_MEMBER_TABLE_COLUMNS?.map((x) => {
|
||||
if (x.dataIndex === 'roles') {
|
||||
return {
|
||||
...x,
|
||||
title: typeof x.title === 'string' ? $t(x.title as string) : x.title,
|
||||
render: (_, entity) => (
|
||||
<WithPermission access="team.team.member.edit">
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
className="w-full"
|
||||
mode="multiple"
|
||||
maxTagCount="responsive"
|
||||
value={entity.roles?.map((x: EntityItem) => 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()
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</WithPermission>
|
||||
),
|
||||
filters: roleList?.map((x: { id: string; name: string }) => ({ text: x.name, value: x.id })),
|
||||
onFilter: (value: unknown, record: TeamMemberTableListItem) => {
|
||||
return record.roles ? record.roles?.map((x) => x.id).indexOf(value as string) !== -1 : false
|
||||
}
|
||||
}
|
||||
}
|
||||
return { ...x, title: typeof x.title === 'string' ? $t(x.title as string) : x.title }
|
||||
})
|
||||
return res
|
||||
}, [state.language, roleList])
|
||||
|
||||
useEffect(() => {
|
||||
manualReloadTable()
|
||||
}, [teamId])
|
||||
|
||||
useEffect(()=>{
|
||||
getRoleList()
|
||||
},[state.language])
|
||||
useEffect(() => {
|
||||
setBreadcrumb([
|
||||
{
|
||||
title: $t('团队'),
|
||||
onClick: () => navigator('/team/list')
|
||||
},
|
||||
{ title: $t('成员') }
|
||||
])
|
||||
getRoleList()
|
||||
}, [state.language])
|
||||
|
||||
const treeDisabledData = useMemo(()=>{ return [...allMemberIds,...allMemberSelectedDepartIds]},[allMemberIds,allMemberSelectedDepartIds])
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageList
|
||||
id="global_team_member"
|
||||
ref={pageListRef}
|
||||
columns = {[...translatedCol,...operation]}
|
||||
request={()=>getMemberList()}
|
||||
primaryKey="user.id"
|
||||
addNewBtnTitle={$t('添加成员')}
|
||||
className="ml-[20px] mt-[20px] "
|
||||
searchPlaceholder={$t("输入姓名查找")}
|
||||
onAddNewBtnClick={()=>{openModal('add')}}
|
||||
addNewBtnAccess="team.team.member.add"
|
||||
tableClickAccess="team.team.member.edit"
|
||||
onSearchWordChange={(e)=>{setSearchWord(e.target.value)}}
|
||||
const treeDisabledData = useMemo(() => {
|
||||
return [...allMemberIds, ...allMemberSelectedDepartIds]
|
||||
}, [allMemberIds, allMemberSelectedDepartIds])
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageList
|
||||
id="global_team_member"
|
||||
ref={pageListRef}
|
||||
columns={[...translatedCol, ...operation]}
|
||||
request={() => getMemberList()}
|
||||
primaryKey="user.id"
|
||||
addNewBtnTitle={$t('添加成员')}
|
||||
className="ml-[20px] mt-[20px] "
|
||||
searchPlaceholder={$t('输入姓名查找')}
|
||||
onAddNewBtnClick={() => {
|
||||
openModal('add')
|
||||
}}
|
||||
addNewBtnAccess="team.team.member.add"
|
||||
tableClickAccess="team.team.member.edit"
|
||||
onSearchWordChange={(e) => {
|
||||
setSearchWord(e.target.value)
|
||||
}}
|
||||
/>
|
||||
<Modal
|
||||
title={$t('添加成员')}
|
||||
open={modalVisible}
|
||||
destroyOnClose={true}
|
||||
width={600}
|
||||
onCancel={() => cleanModalData()}
|
||||
maskClosable={false}
|
||||
footer={[
|
||||
<Button key="back" onClick={() => cleanModalData()}>
|
||||
{$t('取消')}
|
||||
</Button>,
|
||||
<WithPermission access="team.team.member.add">
|
||||
<Button
|
||||
key="submit"
|
||||
type="primary"
|
||||
disabled={addMemberBtnDisabled}
|
||||
loading={addMemberBtnLoading}
|
||||
onClick={() => addMember(selectableMemberIds as Set<string>)}
|
||||
>
|
||||
{$t('确认')}
|
||||
</Button>
|
||||
</WithPermission>
|
||||
]}
|
||||
>
|
||||
<MemberTransfer
|
||||
ref={addRef}
|
||||
primaryKey="id"
|
||||
disabledData={treeDisabledData}
|
||||
request={() => getDepartmentMemberList()}
|
||||
onSelect={(selectedData: Set<string>) => {
|
||||
const memberKeyFromModal =
|
||||
Array.from(selectedData)?.filter((x) => allMemberIds.indexOf(x) === -1 && selectableMemberIds.has(x)) ||
|
||||
[]
|
||||
setAddMemberBtnDisabled(memberKeyFromModal.length === 0)
|
||||
}}
|
||||
searchPlaceholder={$t('输入名称查找用户')}
|
||||
/>
|
||||
<Modal
|
||||
title={$t("添加成员")}
|
||||
open={modalVisible}
|
||||
destroyOnClose={true}
|
||||
width={600}
|
||||
onCancel={() => cleanModalData()}
|
||||
maskClosable={false}
|
||||
footer={[
|
||||
<Button key="back" onClick={() => cleanModalData()}>
|
||||
{$t('取消')}
|
||||
</Button>,
|
||||
<WithPermission access="team.team.member.add"><Button
|
||||
key="submit"
|
||||
type="primary"
|
||||
disabled={addMemberBtnDisabled}
|
||||
loading={addMemberBtnLoading}
|
||||
onClick={()=>addMember(selectableMemberIds as Set<string>)}
|
||||
>
|
||||
{$t('确认')}
|
||||
</Button></WithPermission>,
|
||||
]}
|
||||
>
|
||||
<MemberTransfer
|
||||
ref={addRef}
|
||||
primaryKey="id"
|
||||
disabledData={treeDisabledData}
|
||||
request={()=>getDepartmentMemberList()}
|
||||
onSelect={(selectedData: Set<string>) => {
|
||||
const memberKeyFromModal = Array.from(selectedData)?.filter(x => allMemberIds.indexOf(x) === -1 &&selectableMemberIds.has(x)) || [];
|
||||
setAddMemberBtnDisabled((memberKeyFromModal.length === 0));
|
||||
}}
|
||||
searchPlaceholder={$t("输入名称查找用户")}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default TeamInsideMember
|
||||
export default TeamInsideMember
|
||||
|
||||
@@ -187,7 +187,7 @@ export default function MonitorApiPage(props: MonitorApiPageProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden h-full">
|
||||
<div className="overflow-hidden h-full pr-PAGE_INSIDE_X">
|
||||
<ScrollableSection>
|
||||
<div className="pl-btnbase pr-btnrbase pb-btnbase content-before">
|
||||
<TimeRangeSelector
|
||||
@@ -200,6 +200,8 @@ export default function MonitorApiPage(props: MonitorApiPageProps) {
|
||||
<div className="flex flex-nowrap items-center pt-btnybase mr-btnybase">
|
||||
<label className="inline-block whitespace-nowrap">{$t('服务')}:</label>
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
className="w-[346px]"
|
||||
value={queryData?.services}
|
||||
options={projectOptionList}
|
||||
@@ -216,6 +218,8 @@ export default function MonitorApiPage(props: MonitorApiPageProps) {
|
||||
<div className="flex flex-nowrap items-center pt-btnybase mr-btnybase">
|
||||
<label className=" whitespace-nowrap inline-block w-[42px] text-right">API :</label>
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
className="w-[346px]"
|
||||
value={queryData?.apis}
|
||||
options={apiOptionList}
|
||||
|
||||
@@ -164,7 +164,7 @@ export default function MonitorAppPage(props: MonitorAppPageProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-hidden">
|
||||
<div className="h-full overflow-hidden pr-PAGE_INSIDE_X">
|
||||
<div className="pl-btnbase pr-btnrbase pb-btnybase">
|
||||
<TimeRangeSelector
|
||||
initialTimeButton={timeButton}
|
||||
@@ -176,6 +176,8 @@ export default function MonitorAppPage(props: MonitorAppPageProps) {
|
||||
<div>
|
||||
<label className="inline-block whitespace-nowrap">{$t('消费者')}:</label>
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
className="w-[346px]"
|
||||
mode="multiple"
|
||||
maxTagCount={1}
|
||||
|
||||
@@ -169,7 +169,7 @@ export default function MonitorSubPage(props: MonitorSubPageProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden h-full">
|
||||
<div className="overflow-hidden h-full pr-PAGE_INSIDE_X">
|
||||
<div className="pl-btnbase pr-btnrbase pb-btnybase">
|
||||
<TimeRangeSelector
|
||||
initialTimeButton={timeButton}
|
||||
@@ -181,6 +181,8 @@ export default function MonitorSubPage(props: MonitorSubPageProps) {
|
||||
<div>
|
||||
<label className="inline-block whitespace-nowrap">服务:</label>
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
className="w-[346px]"
|
||||
mode="multiple"
|
||||
maxTagCount={1}
|
||||
|
||||
@@ -7,49 +7,16 @@ import { RouterParams } from '@common/const/type'
|
||||
|
||||
export default function DashboardTabPage() {
|
||||
const { dashboardType } = useParams<RouterParams>()
|
||||
const [activeKey, setActiveKey] = useState<string>('total')
|
||||
const navigateTo = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
setActiveKey(dashboardType || 'total')
|
||||
const activeKey = dashboardType || 'total'
|
||||
navigateTo(`/analytics/${activeKey === 'total' ? activeKey : `${activeKey}/list`}`)
|
||||
}, [dashboardType])
|
||||
|
||||
const monitorTabItems: TabsProps['items'] = [
|
||||
{
|
||||
label: $t('监控总览'),
|
||||
key: 'total',
|
||||
children: <DashboardTotal />
|
||||
},
|
||||
{
|
||||
label: $t('服务被调用统计'),
|
||||
key: 'subscriber',
|
||||
children: <Outlet />
|
||||
},
|
||||
{
|
||||
label: $t('消费者调用统计'),
|
||||
key: 'provider',
|
||||
children: <Outlet />
|
||||
},
|
||||
{
|
||||
label: $t('API 调用统计'),
|
||||
key: 'api',
|
||||
children: <Outlet />
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tabs
|
||||
activeKey={activeKey}
|
||||
onChange={(val) => {
|
||||
setActiveKey(val)
|
||||
navigateTo(`/analytics/${val === 'total' ? val : `${val}/list`}`)
|
||||
}}
|
||||
items={monitorTabItems}
|
||||
className="h-full overflow-hidden mt-[6px] [&>.ant-tabs-content-holder]:overflow-auto [&>.ant-tabs-content-holder]:pr-PAGE_INSIDE_X [&>.ant-tabs-content-holder>.ant-tabs-content]:h-full [&>.ant-tabs-content-holder>.ant-tabs-content>.ant-tabs-tabpane]:h-full"
|
||||
size="small"
|
||||
tabBarStyle={{ paddingLeft: '10px', marginTop: '0px', marginBottom: '0px' }}
|
||||
/>
|
||||
<Outlet />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ export type ServiceBasicInfoType = {
|
||||
approvalType: 'auto' | 'manual'
|
||||
serviceKind: 'ai' | 'rest'
|
||||
sitePrefix?: string
|
||||
enableMcp: boolean
|
||||
invokeCount: number
|
||||
}
|
||||
|
||||
export type ServiceDetailType = {
|
||||
@@ -25,6 +27,9 @@ export type ServiceDetailType = {
|
||||
basic: ServiceBasicInfoType
|
||||
apiDoc: string
|
||||
applied: boolean
|
||||
mcpServerAddress?: string
|
||||
mcpAccessConfig?: string
|
||||
openapiAddress?: string
|
||||
}
|
||||
|
||||
export type ServiceHubCategoryConfigFieldType = {
|
||||
@@ -54,9 +59,12 @@ export type ServiceHubTableListItem = {
|
||||
tags?: EntityItem[]
|
||||
catalogue: EntityItem
|
||||
apiNum: number
|
||||
subscribeNum: number
|
||||
subscriberNum: number
|
||||
description: string
|
||||
logo: string
|
||||
enableMcp: boolean
|
||||
serviceKind: 'ai' | 'rest'
|
||||
invokeCount: number
|
||||
}
|
||||
|
||||
export type ApplyServiceProps = {
|
||||
|
||||
@@ -1053,6 +1053,11 @@ p{
|
||||
.ant-table-wrapper .ant-table{
|
||||
scrollbar-color: none !important;
|
||||
}
|
||||
.ant-select .ant-select-clear {
|
||||
height:16px !important;
|
||||
width: 16px !important;
|
||||
top: 45%;
|
||||
}
|
||||
|
||||
.eo_page_drag .ant-table-body{
|
||||
overflow-y: auto !important;
|
||||
|
||||
@@ -2,7 +2,7 @@ import WithPermission from '@common/components/aoplatform/WithPermission'
|
||||
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
|
||||
import { useFetch } from '@common/hooks/http'
|
||||
import { $t } from '@common/locales'
|
||||
import { App, Col, Form, Input, Row, Select } from 'antd'
|
||||
import { App, Col, Form, Input, Row, Select, Tooltip } from 'antd'
|
||||
import { forwardRef, useEffect, useImperativeHandle } from 'react'
|
||||
import { ApplyServiceHandle, ApplyServiceProps } from '../../const/serviceHub/type'
|
||||
|
||||
@@ -70,11 +70,23 @@ export const ApplyServiceModal = forwardRef<ApplyServiceHandle, ApplyServiceProp
|
||||
</Row>
|
||||
<Form.Item label={$t('消费者')} name="applications" rules={[{ required: true }]}>
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
className="w-INPUT_NORMAL"
|
||||
disabled={reApply}
|
||||
placeholder={$t('搜索或选择消费者')}
|
||||
mode="multiple"
|
||||
options={mySystemOptionList?.filter((x) => x.value !== entity.id)}
|
||||
optionRender={(option) => {
|
||||
if (option.data.disabled) {
|
||||
return (
|
||||
<Tooltip title={$t('该消费者已订阅')}>
|
||||
<div>{option.data.label}</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
return <div>{option.data.label}</div>
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
{entity.approvalType === 'manual' && (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiFilled, ArrowLeftOutlined } from '@ant-design/icons'
|
||||
import { ApiFilled, ApiOutlined, ArrowLeftOutlined } from '@ant-design/icons'
|
||||
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const.tsx'
|
||||
import { EntityItem, RouterParams } from '@common/const/type.ts'
|
||||
import { useBreadcrumb } from '@common/contexts/BreadcrumbContext.tsx'
|
||||
@@ -6,15 +6,27 @@ import { useFetch } from '@common/hooks/http.ts'
|
||||
import { $t } from '@common/locales/index.ts'
|
||||
import { Icon } from '@iconify/react/dist/iconify.js'
|
||||
import { approvalTypeTranslate } from '@market/const/serviceHub/const.tsx'
|
||||
import { App, Avatar, Button, Descriptions, Divider, Tabs } from 'antd'
|
||||
import { App, Avatar, Button, Card, Descriptions, Divider, Tabs, Tag, Tooltip } from 'antd'
|
||||
import { DefaultOptionType } from 'antd/es/cascader'
|
||||
import DOMPurify from 'dompurify'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom'
|
||||
import { ApplyServiceHandle, ServiceBasicInfoType, ServiceDetailType } from '../../const/serviceHub/type.ts'
|
||||
import { ApplyServiceModal } from './ApplyServiceModal.tsx'
|
||||
import ServiceHubApiDocument from './ServiceHubApiDocument.tsx'
|
||||
import Integrate from './integrate.tsx'
|
||||
import { SERVICE_KIND_OPTIONS } from '@core/const/system/const.tsx'
|
||||
import { IntegrationAIContainer, IntegrationAIContainerRef } from '@core/pages/mcpService/IntegrationAIContainer.tsx'
|
||||
import { Tool } from '@modelcontextprotocol/sdk/types.js'
|
||||
import McpToolsContainer from '@core/pages/mcpService/McpToolsContainer.tsx'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx'
|
||||
import TopBreadcrumb from '@common/components/aoplatform/Breadcrumb.tsx'
|
||||
|
||||
type TabItemType = {
|
||||
key: string
|
||||
label: string
|
||||
children: React.ReactNode
|
||||
icon?: React.ReactNode
|
||||
}
|
||||
|
||||
const ServiceHubDetail = () => {
|
||||
const { serviceId } = useParams<RouterParams>()
|
||||
@@ -28,14 +40,24 @@ const ServiceHubDetail = () => {
|
||||
const { modal, message } = App.useApp()
|
||||
const [mySystemOptionList, setMySystemOptionList] = useState<DefaultOptionType[]>()
|
||||
const [service, setService] = useState<ServiceDetailType>()
|
||||
const [serviceMetrics, setServiceMetrics] = useState<{ title: string; icon: React.ReactNode; value: string }[]>([])
|
||||
const [serviceTags, setServiceTags] = useState<
|
||||
{ color: string; textColor: string; title: string; content: React.ReactNode }[]
|
||||
>([])
|
||||
const [tools, setTools] = useState<Tool[]>([])
|
||||
const [tabItem, setTabItem] = useState<TabItemType[]>([])
|
||||
const [currentTab, setCurrentTab] = useState('')
|
||||
const { state } = useGlobalContext()
|
||||
const integrationAIContainerRef = useRef<IntegrationAIContainerRef>(null)
|
||||
const navigate = useNavigate()
|
||||
|
||||
const modifyApiDoc = (apiDoc: string, apiPrefix: string) => {
|
||||
if (!apiDoc) return ''
|
||||
if (!apiPrefix) return apiDoc
|
||||
|
||||
try {
|
||||
const openApiSpec = JSON.parse(apiDoc)
|
||||
// 遍历并修改 paths,给每个路径添加前缀
|
||||
// 遍历并修改paths,给每个路径添加前缀
|
||||
const modifiedPaths: Record<string, unknown> = {}
|
||||
for (const [path, pathItem] of Object.entries(openApiSpec.paths)) {
|
||||
modifiedPaths[apiPrefix + path] = pathItem
|
||||
@@ -43,7 +65,59 @@ const ServiceHubDetail = () => {
|
||||
openApiSpec.paths = modifiedPaths
|
||||
return JSON.stringify(openApiSpec)
|
||||
} catch (err) {
|
||||
console.warn('拼接api前缀失败', err)
|
||||
// 针对YAML格式或特殊格式的文本,直接进行字符串处理
|
||||
try {
|
||||
if (apiDoc.includes('paths:') && apiDoc.includes('openapi:')) {
|
||||
// 在paths:后面的路径前添加前缀
|
||||
// 找到paths:行的位置
|
||||
const pathsIndex = apiDoc.indexOf('paths:')
|
||||
if (pathsIndex !== -1) {
|
||||
try {
|
||||
// 在paths:之后的每个路径(以/开头的行)添加前缀
|
||||
let result = apiDoc.substring(0, pathsIndex + 6) // 包含'paths:'
|
||||
const rest = apiDoc.substring(pathsIndex + 6)
|
||||
|
||||
// 添加servers部分
|
||||
if (!apiDoc.includes('servers:')) {
|
||||
const serverConfig = `info:
|
||||
title: API Space API
|
||||
version: 1.0.0
|
||||
openapi: 3.0.1
|
||||
servers:
|
||||
- url: ${apiPrefix}
|
||||
description: 默认服务器
|
||||
`
|
||||
result = serverConfig + result.substring(result.indexOf('paths:'))
|
||||
}
|
||||
|
||||
// 处理路径
|
||||
const lines = rest.split('\n')
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i]
|
||||
const trimmedLine = line.trim()
|
||||
|
||||
// 检测是否是路径行
|
||||
if (trimmedLine.match(/^\//)) {
|
||||
// 这是一个路径行
|
||||
const indentation = line.substring(0, line.indexOf('/'))
|
||||
const pathWithoutIndent = line.substring(line.indexOf('/'))
|
||||
lines[i] = indentation + apiPrefix + pathWithoutIndent
|
||||
}
|
||||
}
|
||||
|
||||
return result + lines.join('\n')
|
||||
} catch (yamlProcessingError) {
|
||||
console.warn('处理YAML格式的API文档时出错', yamlProcessingError)
|
||||
// 处理失败时返回原始文档
|
||||
return apiDoc
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (outerError) {
|
||||
console.warn('拼接api前缀失败', outerError)
|
||||
return apiDoc
|
||||
}
|
||||
}
|
||||
return apiDoc
|
||||
}
|
||||
@@ -60,7 +134,12 @@ const ServiceHubDetail = () => {
|
||||
'invoke_address',
|
||||
'approval_type',
|
||||
'service_kind',
|
||||
'site_prefix'
|
||||
'site_prefix',
|
||||
'enable_mcp',
|
||||
'mcp_server_address',
|
||||
'mcp_access_config',
|
||||
'openapi_address',
|
||||
'invoke_count'
|
||||
]
|
||||
}).then((response) => {
|
||||
const { code, data, msg } = response
|
||||
@@ -73,12 +152,66 @@ const ServiceHubDetail = () => {
|
||||
setServiceName(data.service.name)
|
||||
setServiceDesc(data.service.description)
|
||||
setServiceDoc(DOMPurify.sanitize(data.service.document))
|
||||
setServiceMetricsList(data.service.basic)
|
||||
setTabItemList(data.service.basic)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleTabChange = (value: any) => {
|
||||
setCurrentTab(value)
|
||||
}
|
||||
|
||||
const setServiceMetricsList = (serviceBasicInfo: ServiceBasicInfoType) => {
|
||||
// 设置服务指标数据
|
||||
setServiceMetrics([
|
||||
{
|
||||
title: 'API 数量',
|
||||
icon: <ApiOutlined className="mr-[1px] text-[14px] h-[14px] w-[14px]" />,
|
||||
value: serviceBasicInfo.apiNum.toString()
|
||||
},
|
||||
{
|
||||
title: '接入消费者数量',
|
||||
icon: <Icon icon="tabler:api-app" width="14" height="14" />,
|
||||
value: serviceBasicInfo.appNum.toString()
|
||||
},
|
||||
{
|
||||
title: '30天内调用次数',
|
||||
icon: <Icon icon="iconoir:graph-up" width="14" height="14" />,
|
||||
value: formatInvokeCount(serviceBasicInfo.invokeCount ?? 0)
|
||||
}
|
||||
])
|
||||
// 设置服务标签数据
|
||||
const tags = [
|
||||
{
|
||||
color: '#7371fc1b',
|
||||
textColor: 'text-theme',
|
||||
title: serviceBasicInfo?.catalogue?.name || '-',
|
||||
content: serviceBasicInfo?.catalogue?.name || '-'
|
||||
},
|
||||
{
|
||||
color: `#${serviceBasicInfo?.serviceKind === 'ai' ? 'EADEFF' : 'DEFFE7'}`,
|
||||
textColor: 'text-[#000]',
|
||||
title: serviceBasicInfo?.serviceKind || '-',
|
||||
content: SERVICE_KIND_OPTIONS.find((x) => x.value === serviceBasicInfo?.serviceKind)?.label || '-'
|
||||
}
|
||||
]
|
||||
|
||||
// 如果启用了MCP,添加MCP标签
|
||||
if (serviceBasicInfo?.enableMcp) {
|
||||
tags.push({
|
||||
color: '#FFF0C1',
|
||||
textColor: 'text-[#000]',
|
||||
title: 'MCP',
|
||||
content: 'MCP'
|
||||
})
|
||||
}
|
||||
|
||||
setServiceTags(tags)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!serviceId) {
|
||||
console.warn('缺少serviceId')
|
||||
@@ -89,21 +222,41 @@ const ServiceHubDetail = () => {
|
||||
|
||||
useEffect(() => {
|
||||
getMySelectList()
|
||||
setBreadcrumb([{ title: <Link to={`/serviceHub/list`}>{$t('服务市场')}</Link> }, { title: $t('服务详情') }])
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
setBreadcrumb([
|
||||
{
|
||||
title: $t('API 门户'),
|
||||
onClick: () => navigate(`/serviceHub/list`)
|
||||
},
|
||||
{ title: $t('服务详情') }
|
||||
])
|
||||
}, [state.language])
|
||||
|
||||
const getMySelectList = () => {
|
||||
setMySystemOptionList([])
|
||||
fetchData<BasicResponse<{ app: EntityItem[] }>>('apps/can_subscribe', { method: 'GET' }).then((response) => {
|
||||
fetchData<BasicResponse<{ app: EntityItem[] }>>('apps/can_subscribe', {
|
||||
method: 'GET',
|
||||
eoParams: { service: serviceId },
|
||||
eoTransformKeys: ['is_subscribed']
|
||||
}).then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setMySystemOptionList(
|
||||
data.app?.map((x: EntityItem) => {
|
||||
return {
|
||||
label: x.name,
|
||||
value: x.id
|
||||
}
|
||||
})
|
||||
data.app
|
||||
?.sort((a: EntityItem, b: EntityItem) => {
|
||||
// 已订阅的排在后面
|
||||
if (a.isSubscribed && !b.isSubscribed) return 1
|
||||
if (!a.isSubscribed && b.isSubscribed) return -1
|
||||
return 0
|
||||
})
|
||||
.map((x: EntityItem) => {
|
||||
return {
|
||||
label: x.name,
|
||||
value: x.id,
|
||||
disabled: x.isSubscribed // 已订阅的设为禁用
|
||||
}
|
||||
})
|
||||
)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
@@ -123,7 +276,10 @@ const ServiceHubDetail = () => {
|
||||
),
|
||||
onOk: () => {
|
||||
return applyRef.current?.apply().then((res) => {
|
||||
// if(res === true) setApplied(true)
|
||||
if (res === true) {
|
||||
integrationAIContainerRef.current?.getServiceKeysList()
|
||||
getMySelectList()
|
||||
}
|
||||
})
|
||||
},
|
||||
okText: $t('确认'),
|
||||
@@ -134,120 +290,233 @@ const ServiceHubDetail = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const items = [
|
||||
{
|
||||
key: 'introduction',
|
||||
label: $t('介绍'),
|
||||
children: (
|
||||
<>
|
||||
<div
|
||||
className="p-btnbase preview-document mb-PAGE_INSIDE_B"
|
||||
dangerouslySetInnerHTML={{ __html: serviceDoc || '' }}
|
||||
></div>
|
||||
</>
|
||||
),
|
||||
icon: <Icon icon="ic:baseline-space-dashboard" width="14" height="14" />
|
||||
},
|
||||
{
|
||||
key: 'api-document',
|
||||
label: $t('API 文档'),
|
||||
children: (
|
||||
<div
|
||||
className={`p-btnbase ${serviceBasicInfo?.serviceKind?.toLocaleLowerCase() === 'ai' ? 'ai-service-api-preview' : ''}`}
|
||||
>
|
||||
<ServiceHubApiDocument service={service!} />
|
||||
</div>
|
||||
),
|
||||
icon: <ApiFilled />
|
||||
},
|
||||
{
|
||||
key: 'api-integrate',
|
||||
label: $t('集成'),
|
||||
children: (
|
||||
<div
|
||||
className={`p-btnbase ${serviceBasicInfo?.serviceKind?.toLocaleLowerCase() === 'ai' ? 'ai-service-api-preview' : ''}`}
|
||||
>
|
||||
<Integrate service={service!} />
|
||||
</div>
|
||||
),
|
||||
icon: <Icon icon="icon-park-solid:whole-site-accelerator" width="15" height="15" />
|
||||
const handleToolsChange = (value: Tool[]) => {
|
||||
setTools(value)
|
||||
}
|
||||
// 格式化调用次数,添加K和M单位
|
||||
const formatInvokeCount = (count: number | null | undefined): string => {
|
||||
if (count === null || count === undefined) return '-'
|
||||
if (count >= 1000000) {
|
||||
const value = Math.floor(count / 100000) / 10
|
||||
return `${value}M`
|
||||
}
|
||||
]
|
||||
if (count >= 1000) {
|
||||
const value = Math.floor(count / 100) / 10
|
||||
return `${value}K`
|
||||
}
|
||||
return count.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义一个更新标签项的函数,在serviceBasicInfo或tools变化时调用
|
||||
*/
|
||||
const updateTabItems = useCallback(() => {
|
||||
if (!serviceBasicInfo) return
|
||||
const descriptionItem = [
|
||||
{
|
||||
label: $t('供应方'),
|
||||
value: serviceBasicInfo?.team?.name || '-',
|
||||
className: 'pb-[10px]'
|
||||
},
|
||||
{
|
||||
label: $t('版本'),
|
||||
value: serviceBasicInfo?.version || '-',
|
||||
className: 'pb-[10px]'
|
||||
},
|
||||
{
|
||||
label: $t('更新时间'),
|
||||
value: serviceBasicInfo?.updateTime || '-',
|
||||
className: 'pb-[10px]',
|
||||
isTimeString: true
|
||||
},
|
||||
{
|
||||
label: $t('审核'),
|
||||
value: serviceBasicInfo?.approvalType ? $t(approvalTypeTranslate[serviceBasicInfo?.approvalType] || '-') : '-',
|
||||
className: 'pb-[0px]'
|
||||
}
|
||||
]
|
||||
const items: TabItemType[] = [
|
||||
{
|
||||
key: 'introduction',
|
||||
label: $t('介绍'),
|
||||
children: (
|
||||
<>
|
||||
<Card
|
||||
style={{
|
||||
borderRadius: '10px'
|
||||
}}
|
||||
className="w-full h-[calc(100vh-420px)] overflow-auto"
|
||||
classNames={{
|
||||
body: 'p-[10px]'
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
style={{
|
||||
borderRadius: '10px'
|
||||
}}
|
||||
className={`w-full`}
|
||||
classNames={{
|
||||
body: 'p-[15px] h-auto bg-[#f8f8f8]'
|
||||
}}
|
||||
>
|
||||
<Descriptions column={1}>
|
||||
{descriptionItem.map((item, index) => (
|
||||
<Descriptions.Item key={index} label={item.label} className={item.className}>
|
||||
{item.isTimeString ? (
|
||||
<span className="truncate" title={item.value}>
|
||||
{item.value}
|
||||
</span>
|
||||
) : (
|
||||
item.value
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
))}
|
||||
</Descriptions>
|
||||
</Card>
|
||||
<div
|
||||
className="p-btnbase preview-document mb-PAGE_INSIDE_B"
|
||||
dangerouslySetInnerHTML={{ __html: serviceDoc || '' }}
|
||||
></div>
|
||||
</Card>
|
||||
</>
|
||||
),
|
||||
icon: <Icon icon="ic:baseline-space-dashboard" width="14" height="14" />
|
||||
},
|
||||
{
|
||||
key: 'api-document',
|
||||
label: $t('API'),
|
||||
children: (
|
||||
<Card
|
||||
style={{
|
||||
borderRadius: '10px'
|
||||
}}
|
||||
className="w-full h-[calc(100vh-420px)] overflow-auto"
|
||||
classNames={{
|
||||
body: 'p-[10px] pt-[0px]'
|
||||
}}
|
||||
>
|
||||
<ServiceHubApiDocument service={service!} />
|
||||
</Card>
|
||||
),
|
||||
icon: <ApiFilled />
|
||||
}
|
||||
]
|
||||
if (serviceBasicInfo.enableMcp) {
|
||||
items.push({
|
||||
key: 'MCP',
|
||||
label: 'MCP',
|
||||
children: <McpToolsContainer tools={tools} customClassName="h-[calc(100vh-420px)] overflow-auto" />,
|
||||
icon: <Icon icon="ph:network-x-fill" width="15" height="15" />
|
||||
})
|
||||
}
|
||||
setTabItem(items)
|
||||
}, [serviceBasicInfo, serviceDoc, service, tools, state.language])
|
||||
|
||||
/**
|
||||
* 当初始化serviceBasicInfo时调用的函数
|
||||
* @param _serviceBasicInfo
|
||||
*/
|
||||
const setTabItemList = (_serviceBasicInfo: ServiceBasicInfoType) => {
|
||||
// 只调用更新函数,更新将由useEffect处理
|
||||
updateTabItems()
|
||||
}
|
||||
useEffect(() => {
|
||||
if (serviceBasicInfo) {
|
||||
updateTabItems()
|
||||
}
|
||||
}, [tools, updateTabItems, serviceBasicInfo])
|
||||
|
||||
return (
|
||||
<section className="grid grid-cols-5 h-full mr-PAGE_INSIDE_X">
|
||||
<section className="col-span-4 border-0 border-r-[1px] border-solid border-BORDER flex flex-col overflow-hidden">
|
||||
<section className="flex flex-col gap-btnbase p-btnbase">
|
||||
<div className="text-[18px] leading-[25px] pb-[12px]">
|
||||
<Button type="text" onClick={() => navigate(`/serviceHub/list`)}>
|
||||
<ArrowLeftOutlined className="max-h-[14px]" />
|
||||
{$t('返回')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex">
|
||||
{/* <Avatar shape="square" size={50} className=" bg-[linear-gradient(135deg,white,#f0f0f0)] text-[#333] rounded-[12px]" > {service?.name?.substring(0,1)}</Avatar> */}
|
||||
<Avatar
|
||||
shape="square"
|
||||
size={50}
|
||||
className={`rounded-[12px] border-none rounded-[12px] ${serviceBasicInfo?.logo ? 'bg-[linear-gradient(135deg,white,#f0f0f0)]' : 'bg-theme'}`}
|
||||
src={
|
||||
serviceBasicInfo?.logo ? (
|
||||
<img
|
||||
src={serviceBasicInfo?.logo}
|
||||
alt="Logo"
|
||||
style={{ maxWidth: '200px', width: '45px', height: '45px', objectFit: 'unset' }}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
icon={serviceBasicInfo?.logo ? '' : <iconpark-icon name="auto-generate-api"></iconpark-icon>}
|
||||
>
|
||||
{' '}
|
||||
</Avatar>
|
||||
|
||||
<div className="pl-[20px] w-[calc(100%-50px)]">
|
||||
<p className="text-[14px] h-[20px] leading-[20px] truncate font-bold flex items-center gap-[4px]">
|
||||
<div className="pr-[40px]">
|
||||
<header>
|
||||
<TopBreadcrumb handleBackCallback={() => navigate(`/serviceHub/list`)} />
|
||||
</header>
|
||||
<Card
|
||||
style={{
|
||||
borderRadius: '10px',
|
||||
background: 'linear-gradient(35deg, rgb(246, 246, 260) 0%, rgb(255, 255, 255) 40%)'
|
||||
}}
|
||||
className={`w-full mt-[20px]`}
|
||||
classNames={{
|
||||
body: 'p-[15px] h-[180px]'
|
||||
}}
|
||||
>
|
||||
<div className="service-info">
|
||||
<div className="flex items-center">
|
||||
<div>
|
||||
<Avatar
|
||||
shape="square"
|
||||
size={50}
|
||||
className={`rounded-[12px] border-none rounded-[12px] ${serviceBasicInfo?.logo ? 'bg-[linear-gradient(135deg,white,#f0f0f0)]' : 'bg-theme'}`}
|
||||
src={
|
||||
serviceBasicInfo?.logo ? (
|
||||
<img
|
||||
src={serviceBasicInfo?.logo}
|
||||
alt="Logo"
|
||||
style={{ maxWidth: '200px', width: '45px', height: '45px', objectFit: 'unset' }}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
icon={serviceBasicInfo?.logo ? '' : <Icon icon="tabler:api-app" />}
|
||||
>
|
||||
{' '}
|
||||
</Avatar>
|
||||
</div>
|
||||
<div className="pl-[20px] w-[calc(100%-50px)] overflow-hidden">
|
||||
<p className="text-[14px] h-[20px] leading-[20px] truncate font-bold w-full flex items-center gap-[4px]">
|
||||
{serviceName}
|
||||
</p>
|
||||
<div className="mt-[10px] flex flex-col gap-btnrbase font-normal">
|
||||
<p>{serviceDesc || '-'}</p>
|
||||
<p className="flex items-center gap-[4px]">
|
||||
<Icon icon="ic:baseline-link" width="18" height="18" />
|
||||
<span className="font-bold">{$t('Base URL')}</span>: {serviceBasicInfo?.invokeAddress || '-'}
|
||||
</p>
|
||||
<div>
|
||||
<Button type="primary" onClick={() => openModal('apply')}>
|
||||
{$t('申请')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-[5px] h-[20px] flex items-center font-normal">
|
||||
{serviceTags.map((tag, index) => (
|
||||
<Tag
|
||||
key={index}
|
||||
color={tag.color}
|
||||
className={`${tag.textColor} font-normal border-0 mr-[12px] max-w-[150px] truncate`}
|
||||
bordered={false}
|
||||
title={tag.title}
|
||||
>
|
||||
{tag.content}
|
||||
</Tag>
|
||||
))}
|
||||
{serviceMetrics.map((item, index) => (
|
||||
<Tooltip key={index} title={$t(item.title)}>
|
||||
<span className="mr-[12px] flex items-center">
|
||||
<span className="h-[14px] mr-[4px] flex items-center">{item.icon}</span>
|
||||
<span className="font-normal text-[14px]">{item.value}</span>
|
||||
</span>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<Tabs className="p-btnbase pr-0 overflow-hidden [&>.ant-tabs-content-holder]:overflow-auto" items={items} />
|
||||
</section>
|
||||
<section className="col-span-1 p-btnbase px-btnrbase">
|
||||
<Descriptions title={$t('服务信息')} column={1} size={'small'}>
|
||||
<Descriptions.Item label={$t('接入消费者')}>{serviceBasicInfo?.appNum ?? '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label={$t('供应方')}>{serviceBasicInfo?.team?.name || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label={$t('审核')}>
|
||||
{serviceBasicInfo?.approvalType ? $t(approvalTypeTranslate[serviceBasicInfo?.approvalType] || '-') : '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={$t('分类')}>{serviceBasicInfo?.catalogue?.name || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label={$t('标签')}>
|
||||
{serviceBasicInfo?.tags?.map((x) => x.name)?.join(',') || '-'}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<Divider />
|
||||
<Descriptions column={1}>
|
||||
<Descriptions.Item label={$t('版本')}>{serviceBasicInfo?.version || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label={$t('更新时间')}>
|
||||
<span className="truncate" title={serviceBasicInfo?.updateTime}>
|
||||
{serviceBasicInfo?.updateTime || '-'}
|
||||
</span>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</section>
|
||||
</section>
|
||||
<span className="line-clamp-2 mt-[15px] text-[12px] text-[#666]" title={serviceDesc}>
|
||||
{serviceDesc || $t('暂无服务描述')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="absolute bottom-[15px]">
|
||||
<Button type="primary" onClick={() => openModal('apply')}>
|
||||
{$t('申请')}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
<div className="flex">
|
||||
<Tabs
|
||||
className="p-btnbase pr-0 overflow-hidden [&>.ant-tabs-content-holder]:overflow-auto w-full flex-1 mr-[10px]"
|
||||
onChange={handleTabChange}
|
||||
items={tabItem}
|
||||
/>
|
||||
<IntegrationAIContainer
|
||||
ref={integrationAIContainerRef}
|
||||
service={service}
|
||||
currentTab={currentTab}
|
||||
serviceId={serviceId}
|
||||
customClassName="mt-[70px] max-h-[calc(100vh-420px)] overflow-auto"
|
||||
type={'service'}
|
||||
openModal={openModal}
|
||||
handleToolsChange={handleToolsChange}
|
||||
></IntegrationAIContainer>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,20 @@ export const ServiceHubGroup = ({ children, filterOption, dispatch }: ServiceHub
|
||||
dispatch({ type: SERVICE_HUB_LIST_ACTIONS.LIST_LOADING, payload: false })
|
||||
}
|
||||
|
||||
// 递归获取所有分类ID(包括子分类)
|
||||
const getAllCategoryIds = (categories: CategorizesType[]): string[] => {
|
||||
let ids: string[] = []
|
||||
categories.forEach((category) => {
|
||||
// 添加当前分类ID
|
||||
ids.push(category.id)
|
||||
// 如果有子分类,递归获取子分类ID
|
||||
if (category.children && category.children.length > 0) {
|
||||
ids = [...ids, ...getAllCategoryIds(category.children)]
|
||||
}
|
||||
})
|
||||
return ids
|
||||
}
|
||||
|
||||
const getTagAndServiceClassifyList = () => {
|
||||
fetchData<BasicResponse<{ catalogues: CategorizesType[]; tags: EntityItem[] }>>('catalogues', {
|
||||
method: 'GET'
|
||||
@@ -50,9 +64,11 @@ export const ServiceHubGroup = ({ children, filterOption, dispatch }: ServiceHub
|
||||
type: SERVICE_HUB_LIST_ACTIONS.GET_TAGS,
|
||||
payload: [...data.tags, { id: 'empty', name: $t('无标签') }]
|
||||
})
|
||||
// 使用递归函数获取所有分类ID
|
||||
const allCategoryIds = getAllCategoryIds(data.catalogues)
|
||||
dispatch({
|
||||
type: SERVICE_HUB_LIST_ACTIONS.SET_SELECTED_CATE,
|
||||
payload: [...data.catalogues.map((x: CategorizesType) => x.id)]
|
||||
payload: allCategoryIds
|
||||
})
|
||||
dispatch({
|
||||
type: SERVICE_HUB_LIST_ACTIONS.SET_SELECTED_TAG,
|
||||
@@ -103,7 +119,7 @@ export const ServiceHubGroup = ({ children, filterOption, dispatch }: ServiceHub
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 h-full">
|
||||
<div className="w-[220px] border-0 border-solid border-r-[1px] border-r-BORDER">
|
||||
<div className="w-[220px] border-0">
|
||||
<div className=" h-full">
|
||||
<Input
|
||||
className="rounded-SEARCH_RADIUS m-[10px] h-[40px] bg-[#f8f8f8] w-[200px]"
|
||||
@@ -116,7 +132,7 @@ export const ServiceHubGroup = ({ children, filterOption, dispatch }: ServiceHub
|
||||
<div className="mt-[20px] ml-[20px] pr-[10px] ">
|
||||
<p className="text-[18px] h-[25px] leading-[25px] font-bold mb-[15px]">{$t('分类')}</p>
|
||||
<Tree
|
||||
className={`no-selected-tree ${transferToTreeData(filterOption.categoriesList).filter((x) => x.children && x.children.length > 0).length > 0 ? '' : 'no-first-switch-tree'}`}
|
||||
className={`no-selected-tree service-hub-custom-switcher ${transferToTreeData(filterOption.categoriesList).filter((x) => x.children && x.children.length > 0).length > 0 ? '' : 'no-first-switch-tree'}`}
|
||||
checkable
|
||||
blockNode={true}
|
||||
checkedKeys={filterOption.selectedCate}
|
||||
@@ -130,7 +146,7 @@ export const ServiceHubGroup = ({ children, filterOption, dispatch }: ServiceHub
|
||||
<div className="ml-[20px] pr-[10px]">
|
||||
<p className="text-[18px] h-[25px] leading-[25px] font-bold mb-[15px]">{$t('标签')}</p>
|
||||
<Tree
|
||||
className="no-first-switch-tree no-selected-tree"
|
||||
className="no-first-switch-tree no-selected-tree service-hub-custom-switcher"
|
||||
checkable
|
||||
blockNode={true}
|
||||
checkedKeys={filterOption.selectedTag}
|
||||
|
||||
@@ -12,6 +12,8 @@ import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { VirtuosoGrid } from 'react-virtuoso'
|
||||
import { CategorizesType, ServiceHubTableListItem } from '../../const/serviceHub/type.ts'
|
||||
import ServiceHubGroup from './ServiceHubGroup.tsx'
|
||||
import { SERVICE_KIND_OPTIONS } from '@core/const/system/const.tsx'
|
||||
import { Icon } from '@iconify/react/dist/iconify.js'
|
||||
|
||||
export enum SERVICE_HUB_LIST_ACTIONS {
|
||||
GET_CATEGORIES = 'GET_CATEGORIES',
|
||||
@@ -43,7 +45,7 @@ export const initialServiceHubListState = {
|
||||
selectedTag: [] as string[],
|
||||
keyword: '',
|
||||
getCateAndTagData: false,
|
||||
listLoading: false
|
||||
listLoading: true
|
||||
}
|
||||
|
||||
function reducer(state: typeof initialServiceHubListState, action: ServiceHubListActionType) {
|
||||
@@ -103,7 +105,7 @@ const ServiceHubList: FC = () => {
|
||||
dispatch({ type: SERVICE_HUB_LIST_ACTIONS.LIST_LOADING, payload: true })
|
||||
fetchData<BasicResponse<{ services: ServiceHubTableListItem }>>('catalogue/services', {
|
||||
method: 'GET',
|
||||
eoTransformKeys: ['api_num', 'subscriber_num']
|
||||
eoTransformKeys: ['api_num', 'subscriber_num', 'enable_mcp', 'service_kind', 'invoke_count']
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, data, msg } = response
|
||||
@@ -140,7 +142,13 @@ const ServiceHubList: FC = () => {
|
||||
<Spin
|
||||
className="h-full"
|
||||
wrapperClassName="h-full"
|
||||
indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />}
|
||||
indicator={
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div style={{ transform: 'scale(1.5)' }}>
|
||||
<LoadingOutlined style={{ fontSize: 30 }} spin />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
spinning={filterOption.listLoading}
|
||||
>
|
||||
{filterOption.showServicesList && filterOption.showServicesList.length > 0 ? (
|
||||
@@ -154,13 +162,14 @@ const ServiceHubList: FC = () => {
|
||||
<div className="pt-[20px]">
|
||||
<Card
|
||||
title={CardTitle(item)}
|
||||
className="shadow-[0_5px_10px_0_rgba(0,0,0,0.05)] rounded-[10px] overflow-visible cursor-pointer h-[180px] m-0 transition duration-500 hover:shadow-[0_5px_20px_0_rgba(0,0,0,0.15)] hover:scale-[1.05]"
|
||||
classNames={{ header: 'border-b-[0px] p-[20px] ', body: 'pt-0' }}
|
||||
className="shadow-[0_5px_10px_0_rgba(0,0,0,0.05)] rounded-[10px] overflow-visible cursor-pointer h-[200px] m-0 transition duration-500 hover:shadow-[0_5px_20px_0_rgba(0,0,0,0.15)] hover:scale-[1.05]"
|
||||
classNames={{ header: 'border-b-[0px] p-[20px] ', body: 'pt-0 h-[110px]' }}
|
||||
onClick={() => showDocumentDetail(item)}
|
||||
>
|
||||
<span className="line-clamp-3 text-[12px] text-[#666] " style={{ 'word-break': 'auto-phrase' }}>
|
||||
<span className="line-clamp-3 text-[12px] text-[#666]" title={item.description}>
|
||||
{item.description || $t('暂无服务描述')}
|
||||
</span>
|
||||
<CardAction service={item} />
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
@@ -186,7 +195,12 @@ const ServiceHubList: FC = () => {
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
<>
|
||||
{!filterOption.listLoading &&
|
||||
(!filterOption.showServicesList || filterOption.showServicesList.length === 0) && (
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Spin>
|
||||
</div>
|
||||
@@ -222,30 +236,77 @@ const CardTitle = (service: ServiceHubTableListItem) => {
|
||||
<div className="mt-[10px] h-[20px] flex items-center font-normal">
|
||||
<Tag
|
||||
color="#7371fc1b"
|
||||
className="text-theme font-normal border-0 mr-[12px] max-w-[150px] truncate"
|
||||
className="text-theme font-normal border-0 mr-[12px] max-w-[100px] truncate"
|
||||
key={service.id}
|
||||
bordered={false}
|
||||
title={service.catalogue?.name || '-'}
|
||||
>
|
||||
{service.catalogue?.name || '-'}
|
||||
</Tag>
|
||||
|
||||
<Tooltip title={$t('API 数量')}>
|
||||
<span className="mr-[12px] flex items-center">
|
||||
<ApiOutlined className="mr-[1px] text-[14px] h-[14px] w-[14px]" />
|
||||
<span className="font-normal text-[14px]">{service.apiNum ?? '-'}</span>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip title={$t('接入消费者数量')}>
|
||||
<span className="mr-[12px] flex items-center">
|
||||
<span className="h-[14px] mr-[4px] flex items-center ">
|
||||
<iconpark-icon size="14px" name="auto-generate-api"></iconpark-icon>
|
||||
</span>
|
||||
<span className="font-normal text-[14px]">{service.subscriberNum ?? '-'}</span>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tag
|
||||
color={`#${service.serviceKind === 'ai' ? 'EADEFF' : 'DEFFE7'}`}
|
||||
className={`text-[#000] font-normal border-0 mr-[12px] max-w-[150px] truncate`}
|
||||
bordered={false}
|
||||
title={service.serviceKind || '-'}
|
||||
>
|
||||
{SERVICE_KIND_OPTIONS.find((x) => x.value === service.serviceKind)?.label || '-'}
|
||||
</Tag>
|
||||
{service?.enableMcp && (
|
||||
<Tag
|
||||
color="#FFF0C1"
|
||||
className="text-[#000] font-normal border-0 mr-[12px] max-w-[150px] truncate"
|
||||
bordered={false}
|
||||
title={'MCP'}
|
||||
>
|
||||
MCP
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 格式化调用次数,添加K和M单位
|
||||
const formatInvokeCount = (count: number | null | undefined): string => {
|
||||
if (count === null || count === undefined) return '-'
|
||||
if (count >= 1000000) {
|
||||
const value = Math.floor(count / 100000) / 10
|
||||
return `${value}M`
|
||||
}
|
||||
if (count >= 1000) {
|
||||
const value = Math.floor(count / 100) / 10
|
||||
return `${value}K`
|
||||
}
|
||||
return count.toString()
|
||||
}
|
||||
|
||||
const CardAction = (props: { service: ServiceHubTableListItem }) => {
|
||||
const { service } = props
|
||||
return (
|
||||
<div className="absolute bottom-[20px] h-[20px] flex items-center font-normal">
|
||||
<Tooltip title={$t('API 数量')}>
|
||||
<span className="mr-[12px] flex items-center">
|
||||
<ApiOutlined className="mr-[1px] text-[14px] h-[14px] w-[14px]" />
|
||||
<span className="font-normal text-[14px]">{service.apiNum ?? '-'}</span>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip title={$t('接入消费者数量')}>
|
||||
<span className="mr-[12px] flex items-center">
|
||||
<span className="h-[14px] mr-[4px] flex items-center ">
|
||||
<Icon icon="tabler:api-app" width="14" height="14" />
|
||||
</span>
|
||||
<span className="font-normal text-[14px]">{service.subscriberNum ?? '-'}</span>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip title={$t('30天内调用次数')}>
|
||||
<span className="mr-[12px] flex items-center">
|
||||
<span className="h-[14px] mr-[4px] flex items-center ">
|
||||
<Icon icon="iconoir:graph-up" width="14" height="14" />
|
||||
</span>
|
||||
<span className="font-normal text-[14px]">{formatInvokeCount(service.invokeCount ?? 0)}</span>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -65,6 +65,8 @@ export const ManagementAuthorityConfig = forwardRef<ManagementAuthorityConfigHan
|
||||
const prefixSelector = (
|
||||
<Form.Item name="position" noStyle>
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
style={{ width: 90 }}
|
||||
options={[
|
||||
{ label: 'Header', value: 'Header' },
|
||||
@@ -127,6 +129,8 @@ export const ManagementAuthorityConfig = forwardRef<ManagementAuthorityConfigHan
|
||||
|
||||
<Form.Item<EditAuthFieldType> label={$t('鉴权类型')} name="driver" rules={[{ required: true }]}>
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
disabled={type === 'edit'}
|
||||
className="w-INPUT_NORMAL"
|
||||
options={[
|
||||
@@ -179,6 +183,8 @@ export const ManagementAuthorityConfig = forwardRef<ManagementAuthorityConfigHan
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
className="w-INPUT_NORMAL"
|
||||
options={ALGORITHM_ITEM}
|
||||
onChange={(value) => {
|
||||
@@ -205,6 +211,8 @@ export const ManagementAuthorityConfig = forwardRef<ManagementAuthorityConfigHan
|
||||
|
||||
<Form.Item<EditAuthFieldType> label={$t('校验字段')} name={['config', 'claimsToVerify']}>
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
className="w-INPUT_NORMAL"
|
||||
mode="multiple"
|
||||
options={[
|
||||
|
||||
@@ -204,6 +204,8 @@ const ManagementConfig = forwardRef<ManagementConfigHandle, ManagementConfigProp
|
||||
{dataShowType === 'list' && (
|
||||
<Form.Item<ManagementConfigFieldType> label={$t('所属团队')} name="team" rules={[{ required: true }]}>
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
className="w-INPUT_NORMAL"
|
||||
disabled={type === 'edit'}
|
||||
placeholder={$t(PLACEHOLDER.input)}
|
||||
|
||||
@@ -7,7 +7,7 @@ toolchain go1.23.6
|
||||
require (
|
||||
github.com/eolinker/ap-account v1.0.15
|
||||
github.com/eolinker/eosc v0.18.3
|
||||
github.com/eolinker/go-common v1.1.5
|
||||
github.com/eolinker/go-common v1.1.6
|
||||
github.com/gabriel-vasile/mimetype v1.4.4
|
||||
github.com/getkin/kin-openapi v0.127.0
|
||||
github.com/gin-contrib/gzip v1.0.1
|
||||
@@ -15,6 +15,8 @@ require (
|
||||
github.com/go-sql-driver/mysql v1.7.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/influxdata/influxdb-client-go/v2 v2.14.0
|
||||
github.com/mark3labs/mcp-go v0.17.0
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/nsqio/go-nsq v1.1.0
|
||||
github.com/ollama/ollama v0.5.8
|
||||
github.com/urfave/cli v1.22.16
|
||||
@@ -63,6 +65,7 @@ require (
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.13 // indirect
|
||||
go.uber.org/atomic v1.7.0 // indirect
|
||||
go.uber.org/multierr v1.6.0 // indirect
|
||||
|
||||
@@ -32,8 +32,8 @@ github.com/eolinker/ap-account v1.0.15 h1:n6DJeL6RHZ8eLlZUcY2U3H4d/GPaA5oelAx3R0
|
||||
github.com/eolinker/ap-account v1.0.15/go.mod h1:zm/Ivs6waJ/M/nEszhpPmM6g50y/MKO+5eABFAdeD0g=
|
||||
github.com/eolinker/eosc v0.18.3 h1:3IK5HkAPnJRfLbQ0FR7kWsZr6Y/OiqqGazvN1q2BL5A=
|
||||
github.com/eolinker/eosc v0.18.3/go.mod h1:O9PQQXFCpB6fjHf+oFt/LN6EOAv779ItbMixMKCfTfk=
|
||||
github.com/eolinker/go-common v1.1.5 h1:unYPcpptqL2zk+BNuzc4cJVJBDEpjs918nTkGUw+q7U=
|
||||
github.com/eolinker/go-common v1.1.5/go.mod h1:Kb/jENMN1mApnodvRgV4YwO9FJby1Jkt2EUjrBjvSX4=
|
||||
github.com/eolinker/go-common v1.1.6 h1:s+NaQL0InjX/MwWY53+8y8qzAgsULIUc4U6nWXWQ2Nw=
|
||||
github.com/eolinker/go-common v1.1.6/go.mod h1:Kb/jENMN1mApnodvRgV4YwO9FJby1Jkt2EUjrBjvSX4=
|
||||
github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I=
|
||||
github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s=
|
||||
github.com/getkin/kin-openapi v0.127.0 h1:Mghqi3Dhryf3F8vR370nN67pAERW+3a95vomb3MAREY=
|
||||
@@ -101,8 +101,12 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mark3labs/mcp-go v0.17.0 h1:5Ps6T7qXr7De/2QTqs9h6BKeZ/qdeUeGrgM5lPzi930=
|
||||
github.com/mark3labs/mcp-go v0.17.0/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -149,6 +153,8 @@ github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65E
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ=
|
||||
github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.13 h1:RVZSAnWWWiI5IrYAXjQorajncORbS0zI48LQlE2kQWg=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.13/go.mod h1:XxHT4u1qU12E2+po+UVPrEeL94Um6zL58ppuJWXSAB8=
|
||||
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
package mcp_server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
)
|
||||
|
||||
var (
|
||||
openapi3Loader = openapi3.NewLoader()
|
||||
)
|
||||
|
||||
type MCPInfo struct {
|
||||
Name string
|
||||
Description string
|
||||
Apis []*API
|
||||
}
|
||||
|
||||
type API struct {
|
||||
Path string `json:"path"`
|
||||
Method string `json:"method"`
|
||||
ContentType string `json:"content_type"`
|
||||
Summary string `json:"summary"`
|
||||
Description string `json:"description"`
|
||||
Params []*openapi3.Parameter `json:"params"`
|
||||
Body map[string]interface{} `json:"body"`
|
||||
}
|
||||
|
||||
func ConvertMCPFromOpenAPI3Data(data []byte) (*MCPInfo, error) {
|
||||
spec, err := openapi3Loader.LoadFromData(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parseOpenAPI3(spec)
|
||||
}
|
||||
|
||||
func parseOpenAPI3(spec *openapi3.T) (*MCPInfo, error) {
|
||||
items := spec.Paths.Map()
|
||||
apis := make([]*API, 0, len(items)*4)
|
||||
for path, item := range items {
|
||||
pathApis, err := genAPIs(path, item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
apis = append(apis, pathApis...)
|
||||
}
|
||||
|
||||
return &MCPInfo{
|
||||
Name: spec.Info.Title,
|
||||
Description: spec.Info.Description,
|
||||
Apis: apis,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var (
|
||||
validMethods = []string{
|
||||
http.MethodGet,
|
||||
http.MethodPost,
|
||||
http.MethodPut,
|
||||
http.MethodPatch,
|
||||
http.MethodDelete,
|
||||
http.MethodHead,
|
||||
http.MethodOptions,
|
||||
}
|
||||
)
|
||||
|
||||
func genAPIs(path string, item *openapi3.PathItem) ([]*API, error) {
|
||||
apis := make([]*API, 0, 8)
|
||||
for _, method := range validMethods {
|
||||
opt := item.GetOperation(method)
|
||||
if opt == nil {
|
||||
continue
|
||||
}
|
||||
api, err := genAPI(method, path, opt, item.Parameters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
apis = append(apis, api)
|
||||
}
|
||||
return apis, nil
|
||||
}
|
||||
|
||||
func genAPI(method string, path string, opt *openapi3.Operation, params openapi3.Parameters) (*API, error) {
|
||||
api := &API{
|
||||
Method: method,
|
||||
Path: path,
|
||||
Summary: opt.Summary,
|
||||
Description: opt.Description,
|
||||
Params: make([]*openapi3.Parameter, 0, len(params)+len(opt.Parameters)),
|
||||
}
|
||||
if api.Summary == "" {
|
||||
api.Summary = opt.Description
|
||||
}
|
||||
parameters := make([]*openapi3.ParameterRef, 0, len(params)+len(opt.Parameters))
|
||||
parameters = append(parameters, opt.Parameters...)
|
||||
parameters = append(parameters, params...)
|
||||
for _, param := range parameters {
|
||||
if param.Value != nil {
|
||||
api.Params = append(api.Params, param.Value)
|
||||
}
|
||||
}
|
||||
if opt.RequestBody != nil && opt.RequestBody.Value != nil && opt.RequestBody.Value.Content != nil {
|
||||
for mediaType, media := range opt.RequestBody.Value.Content {
|
||||
if media != nil && media.Schema != nil {
|
||||
api.ContentType = mediaType
|
||||
body, err := recurseSchemaRef(media.Schema)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
api.Body = body
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return api, nil
|
||||
}
|
||||
|
||||
func recurseSchemaRef(ref *openapi3.SchemaRef) (map[string]interface{}, error) {
|
||||
if ref == nil || ref.Value == nil {
|
||||
return nil, nil
|
||||
}
|
||||
data, err := json.Marshal(ref.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := make(map[string]interface{})
|
||||
err = json.Unmarshal(data, &m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ref.Value.Properties != nil {
|
||||
m["properties"] = make(map[string]interface{})
|
||||
for k, v := range ref.Value.Properties {
|
||||
v, err := recurseSchemaRef(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m["properties"].(map[string]interface{})[k] = v
|
||||
}
|
||||
}
|
||||
if ref.Value.Items != nil {
|
||||
v, err := recurseSchemaRef(ref.Value.Items)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m["items"] = v
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package mcp_server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
|
||||
"github.com/eolinker/eosc"
|
||||
)
|
||||
|
||||
var (
|
||||
mcpServer = NewServer()
|
||||
ServiceBasePath = "mcp/service"
|
||||
GlobalBasePath = "mcp/global"
|
||||
)
|
||||
|
||||
func NewServer() *Server {
|
||||
return &Server{
|
||||
sseServers: eosc.BuildUntyped[string, *server.SSEServer](),
|
||||
}
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
sseServers eosc.Untyped[string, *server.SSEServer]
|
||||
}
|
||||
|
||||
func (s *Server) Set(path string, sseServer *server.SSEServer) {
|
||||
s.sseServers.Set(path, sseServer)
|
||||
}
|
||||
|
||||
func (s *Server) Del(path string) {
|
||||
s.sseServers.Del(path)
|
||||
}
|
||||
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
sseServer, has := s.sseServers.Get(trimPath(r.URL.Path))
|
||||
if has {
|
||||
sseServer.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
func trimPath(path string) string {
|
||||
path = strings.TrimSuffix(path, "/")
|
||||
path = strings.TrimSuffix(path, "/message")
|
||||
path = strings.TrimSuffix(path, "/sse")
|
||||
return path
|
||||
}
|
||||
|
||||
func SetSSEServer(sid string, name string, version string, tools ...ITool) {
|
||||
s := server.NewMCPServer(name, version)
|
||||
for _, tool := range tools {
|
||||
tool.RegisterMCP(s)
|
||||
}
|
||||
apiPath := fmt.Sprintf("/api/v1/%s/%s", ServiceBasePath, sid)
|
||||
openAPIPath := fmt.Sprintf("/openapi/v1/%s/%s", ServiceBasePath, sid)
|
||||
mcpServer.Set(apiPath, server.NewSSEServer(s, server.WithBasePath(apiPath)))
|
||||
mcpServer.Set(openAPIPath, server.NewSSEServer(s, server.WithBasePath(openAPIPath)))
|
||||
}
|
||||
|
||||
func DelSSEServer(sid string) {
|
||||
apiPath := fmt.Sprintf("/api/v1/%s/%s", ServiceBasePath, sid)
|
||||
openAPIPath := fmt.Sprintf("/openapi/v1/%s/%s", ServiceBasePath, sid)
|
||||
mcpServer.Del(apiPath)
|
||||
mcpServer.Del(openAPIPath)
|
||||
}
|
||||
|
||||
func ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
mcpServer.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func DefaultMCPServer() *Server {
|
||||
return mcpServer
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package mcp_server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/eolinker/go-common/utils"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
|
||||
type ITool interface {
|
||||
RegisterMCP(s *server.MCPServer)
|
||||
}
|
||||
|
||||
const (
|
||||
MCPBody = "Body"
|
||||
MCPHeader = "Header"
|
||||
MCPQuery = "Query"
|
||||
MCPPath = "Path"
|
||||
)
|
||||
|
||||
type Tool struct {
|
||||
name string
|
||||
url string
|
||||
method string
|
||||
contentType string
|
||||
opts []mcp.ToolOption
|
||||
}
|
||||
|
||||
func NewTool(name string, uri string, method string, contentType string, opts ...mcp.ToolOption) ITool {
|
||||
return &Tool{
|
||||
name: name,
|
||||
url: uri,
|
||||
method: method,
|
||||
contentType: contentType,
|
||||
opts: opts,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tool) RegisterMCP(s *server.MCPServer) {
|
||||
s.AddTool(mcp.NewTool(t.name, t.opts...), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
invokeAddress := utils.GatewayInvoke(ctx)
|
||||
if invokeAddress == "" {
|
||||
return nil, fmt.Errorf("invoke address is empty")
|
||||
}
|
||||
u, err := url.Parse(invokeAddress)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid invoke address %s", invokeAddress)
|
||||
}
|
||||
if u.Scheme == "" {
|
||||
u.Scheme = "http"
|
||||
}
|
||||
|
||||
path := t.url
|
||||
queries := url.Values{}
|
||||
headers := make(map[string]string)
|
||||
body := ""
|
||||
for k, v := range request.Params.Arguments {
|
||||
if k == "Body" {
|
||||
switch a := v.(type) {
|
||||
case string:
|
||||
body = a
|
||||
case map[string]interface{}:
|
||||
switch t.contentType {
|
||||
case "application/json":
|
||||
tmp, _ := json.Marshal(a)
|
||||
body = string(tmp)
|
||||
case "application/x-www-form-urlencoded":
|
||||
bodyValue := url.Values{}
|
||||
for kk, vv := range a {
|
||||
bodyValue.Set(kk, fmt.Sprintf("%v", vv))
|
||||
}
|
||||
body = bodyValue.Encode()
|
||||
}
|
||||
default:
|
||||
tmp, _ := json.Marshal(a)
|
||||
body = string(tmp)
|
||||
}
|
||||
continue
|
||||
}
|
||||
tmp, ok := v.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
switch k {
|
||||
case MCPHeader:
|
||||
for kk, vv := range tmp {
|
||||
headers[kk] = fmt.Sprintf("%v", vv)
|
||||
}
|
||||
|
||||
case MCPQuery:
|
||||
for kk, vv := range tmp {
|
||||
queries.Set(kk, fmt.Sprintf("%v", vv))
|
||||
}
|
||||
case MCPPath:
|
||||
for kk, vv := range tmp {
|
||||
p, ok := vv.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid path %s", v)
|
||||
}
|
||||
path = strings.Replace(path, fmt.Sprintf("{%s}", kk), p, -1)
|
||||
}
|
||||
}
|
||||
}
|
||||
u.Path = path
|
||||
u.RawQuery = queries.Encode()
|
||||
|
||||
req, err := http.NewRequest(t.method, u.String(), strings.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
if t.contentType != "" {
|
||||
req.Header.Set("Content-Type", t.contentType)
|
||||
}
|
||||
apikey := utils.Label(ctx, "apikey")
|
||||
if apikey != "" {
|
||||
req.Header.Set("Authorization", apikey)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
d, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("status code %d, %s", resp.StatusCode, string(d))
|
||||
}
|
||||
|
||||
return mcp.NewToolResultText(string(d)), nil
|
||||
})
|
||||
}
|
||||
|
||||
var client = http.Client{}
|
||||
+52
-44
@@ -35,43 +35,6 @@ func genOperation(summary string, description string, variables []*ai_api_dto.Ai
|
||||
return operation
|
||||
}
|
||||
|
||||
func genRequestHeaders() openapi3.Parameters {
|
||||
return openapi3.Parameters{
|
||||
{
|
||||
Value: &openapi3.Parameter{
|
||||
Name: "Authorization",
|
||||
In: "header",
|
||||
Description: "your_apipark_apikey", // 替换Prompt的变量列表
|
||||
Required: true,
|
||||
Example: "your_apipark_apikey",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func genRequestParameters(variables []*ai_api_dto.AiPromptVariable) openapi3.Parameters {
|
||||
return openapi3.Parameters{
|
||||
{
|
||||
Value: &openapi3.Parameter{
|
||||
Name: "variables",
|
||||
In: "body",
|
||||
Description: "Replace the variable list of Prompt", // 替换Prompt的变量列表
|
||||
Schema: genVariableSchema(variables).NewRef(),
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
Value: &openapi3.Parameter{
|
||||
Name: "messages",
|
||||
In: "body",
|
||||
Description: "Chat Message",
|
||||
Schema: messagesSchemaRef,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func genRequestBody(variables []*ai_api_dto.AiPromptVariable) *openapi3.RequestBodyRef {
|
||||
requestBody := openapi3.NewRequestBody()
|
||||
requestBody.Content = openapi3.NewContentWithSchema(genRequestBodySchema(variables), []string{"application/json"})
|
||||
@@ -96,6 +59,10 @@ func genRequestBodySchema(variables []*ai_api_dto.AiPromptVariable) *openapi3.Sc
|
||||
result.WithProperty("variables", genVariableSchema(variables))
|
||||
result.WithRequired([]string{"variables", "messages"})
|
||||
}
|
||||
streamSchema := openapi3.NewBoolSchema()
|
||||
streamSchema.Title = "stream"
|
||||
streamSchema.Description = "Whether to stream the response"
|
||||
result.WithProperty("stream", streamSchema)
|
||||
|
||||
result.WithPropertyRef("messages", messagesSchemaRef)
|
||||
|
||||
@@ -176,17 +143,58 @@ func genMessagesSchema() *openapi3.Schema {
|
||||
func genResponseSchema() *openapi3.Schema {
|
||||
result := openapi3.NewObjectSchema()
|
||||
result.Description = "Response from the server"
|
||||
result.WithPropertyRef("message", messageSchemaRef)
|
||||
openapi3.NewIntegerSchema()
|
||||
result.WithProperty("code", openapi3.NewIntegerSchema())
|
||||
result.WithProperty("error", openapi3.NewStringSchema())
|
||||
result.WithProperty("finish_reason", openapi3.NewStringSchema().WithEnum(
|
||||
|
||||
// 创建 choices 数组
|
||||
choicesSchema := openapi3.NewArraySchema()
|
||||
choiceItemSchema := openapi3.NewObjectSchema()
|
||||
|
||||
// choice 中的 message 字段
|
||||
choiceItemSchema.WithPropertyRef("message", messageSchemaRef)
|
||||
|
||||
// finish_reason 字段
|
||||
finishReasonSchema := openapi3.NewStringSchema().WithEnum(
|
||||
"stop",
|
||||
"length",
|
||||
"function_call",
|
||||
"content_filter",
|
||||
"null",
|
||||
))
|
||||
|
||||
)
|
||||
choiceItemSchema.WithProperty("finish_reason", finishReasonSchema)
|
||||
|
||||
// index 字段
|
||||
choiceItemSchema.WithProperty("index", openapi3.NewIntegerSchema())
|
||||
|
||||
// logprobs 字段,可以为 null
|
||||
choiceItemSchema.WithProperty("logprobs", openapi3.NewSchema().WithNullable())
|
||||
|
||||
choicesSchema.Items = &openapi3.SchemaRef{Value: choiceItemSchema}
|
||||
result.WithProperty("choices", choicesSchema)
|
||||
|
||||
// object 字段
|
||||
result.WithProperty("object", openapi3.NewStringSchema().WithEnum("chat.completion"))
|
||||
|
||||
// usage 字段
|
||||
usageSchema := openapi3.NewObjectSchema()
|
||||
usageSchema.WithProperty("prompt_tokens", openapi3.NewIntegerSchema())
|
||||
usageSchema.WithProperty("completion_tokens", openapi3.NewIntegerSchema())
|
||||
usageSchema.WithProperty("total_tokens", openapi3.NewIntegerSchema())
|
||||
|
||||
// prompt_tokens_details 字段
|
||||
promptTokensDetailsSchema := openapi3.NewObjectSchema()
|
||||
promptTokensDetailsSchema.WithProperty("cached_tokens", openapi3.NewIntegerSchema())
|
||||
usageSchema.WithProperty("prompt_tokens_details", promptTokensDetailsSchema)
|
||||
|
||||
result.WithProperty("usage", usageSchema)
|
||||
|
||||
// 其他字段
|
||||
result.WithProperty("created", openapi3.NewIntegerSchema())
|
||||
result.WithProperty("system_fingerprint", openapi3.NewStringSchema().WithNullable())
|
||||
result.WithProperty("model", openapi3.NewStringSchema())
|
||||
result.WithProperty("id", openapi3.NewStringSchema())
|
||||
|
||||
// 保留原有的错误字段
|
||||
result.WithProperty("code", openapi3.NewIntegerSchema())
|
||||
result.WithProperty("error", openapi3.NewStringSchema())
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
+2
-1
@@ -718,7 +718,8 @@ func (i *imlProviderModule) getAiProviders(ctx context.Context) ([]*gateway.Dyna
|
||||
}
|
||||
model, has := driver.GetModel(l.DefaultLLM)
|
||||
if !has {
|
||||
return nil, fmt.Errorf("model not found: %s", l.DefaultLLM)
|
||||
continue
|
||||
//return nil, fmt.Errorf("model not found: %s", l.DefaultLLM)
|
||||
}
|
||||
cfg := make(map[string]interface{})
|
||||
cfg["provider"] = l.Id
|
||||
|
||||
@@ -3,7 +3,7 @@ package auth_driver
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
|
||||
application_authorization_dto "github.com/APIParkLab/APIPark/module/application-authorization/dto"
|
||||
)
|
||||
|
||||
@@ -83,6 +83,6 @@ func generateStruct[T any](cfg interface{}) (*T, error) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ package oauth2
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
|
||||
auth_driver "github.com/APIParkLab/APIPark/module/application-authorization/auth-driver"
|
||||
|
||||
|
||||
application_authorization_dto "github.com/APIParkLab/APIPark/module/application-authorization/dto"
|
||||
)
|
||||
|
||||
@@ -33,7 +33,7 @@ func (cfg *Config) ID() string {
|
||||
}
|
||||
|
||||
func (cfg *Config) Valid() ([]byte, error) {
|
||||
|
||||
|
||||
if cfg.HashSecret && !cfg.Hashed {
|
||||
// 未加密
|
||||
secret, err := hashSecret([]byte(cfg.ClientSecret), 0, 0, 0)
|
||||
@@ -48,9 +48,9 @@ func (cfg *Config) Valid() ([]byte, error) {
|
||||
}
|
||||
|
||||
func (cfg *Config) Detail() []application_authorization_dto.DetailItem {
|
||||
|
||||
|
||||
redirectURLs, _ := json.Marshal(cfg.RedirectUrls)
|
||||
|
||||
|
||||
return []application_authorization_dto.DetailItem{
|
||||
{Key: "客户端ID", Value: cfg.ClientId},
|
||||
{Key: "客户端密钥", Value: cfg.ClientSecret},
|
||||
|
||||
@@ -2,9 +2,10 @@ package application_authorization
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/APIParkLab/APIPark/module/system"
|
||||
"reflect"
|
||||
|
||||
"github.com/APIParkLab/APIPark/module/system"
|
||||
|
||||
application_authorization_dto "github.com/APIParkLab/APIPark/module/application-authorization/dto"
|
||||
|
||||
"github.com/APIParkLab/APIPark/gateway"
|
||||
@@ -31,6 +32,9 @@ type IAuthorizationModule interface {
|
||||
Detail(ctx context.Context, appId string, aid string) ([]application_authorization_dto.DetailItem, error)
|
||||
// Info 获取项目鉴权详情
|
||||
Info(ctx context.Context, appId string, aid string) (*application_authorization_dto.Authorization, error)
|
||||
|
||||
CheckAPIKeyAuthorization(ctx context.Context, serviceId string, apikey string) (bool, error)
|
||||
|
||||
//ExportAll(ctx context.Context) ([]*application_authorization_dto.ExportAuthorization, error)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/APIParkLab/APIPark/service/subscribe"
|
||||
|
||||
application_authorization "github.com/APIParkLab/APIPark/service/application-authorization"
|
||||
|
||||
"github.com/eolinker/eosc/log"
|
||||
@@ -36,11 +38,80 @@ var (
|
||||
|
||||
type imlAuthorizationModule struct {
|
||||
serviceService service.IServiceService `autowired:""`
|
||||
subscribeService subscribe.ISubscribeService `autowired:""`
|
||||
authorizationService application_authorization.IAuthorizationService `autowired:""`
|
||||
clusterService cluster.IClusterService `autowired:""`
|
||||
transaction store.ITransaction `autowired:""`
|
||||
}
|
||||
|
||||
func (i *imlAuthorizationModule) CheckAPIKeyAuthorization(ctx context.Context, serviceId string, apikey string) (bool, error) {
|
||||
list, err := i.subscribeService.ListBySubscribeStatus(ctx, serviceId, subscribe.ApplyStatusSubscribe)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(list) < 1 {
|
||||
return false, fmt.Errorf("no application found")
|
||||
}
|
||||
appIds := utils.SliceToSlice(list, func(s *subscribe.Subscribe) string {
|
||||
return s.Application
|
||||
})
|
||||
authorizations, err := i.authorizationService.ListByApp(ctx, appIds...)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, a := range authorizations {
|
||||
if a.Type != "apikey" {
|
||||
continue
|
||||
}
|
||||
cfg := make(map[string]interface{})
|
||||
if a.Config != "" {
|
||||
json.Unmarshal([]byte(a.Config), &cfg)
|
||||
}
|
||||
if cfg["apikey"] == apikey {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (i *imlAuthorizationModule) AuthorizationsByService(ctx context.Context, serviceId string, authorizationType string) ([]*application_authorization_dto.Authorization, error) {
|
||||
list, err := i.subscribeService.ListBySubscribeStatus(ctx, serviceId, subscribe.ApplyStatusSubscribe)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(list) < 1 {
|
||||
return nil, fmt.Errorf("no application found")
|
||||
}
|
||||
appIds := utils.SliceToSlice(list, func(s *subscribe.Subscribe) string {
|
||||
return s.Application
|
||||
})
|
||||
authorizations, err := i.authorizationService.ListByApp(ctx, appIds...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]*application_authorization_dto.Authorization, 0, len(authorizations))
|
||||
for _, a := range authorizations {
|
||||
if authorizationType != "" && a.Type != authorizationType {
|
||||
continue
|
||||
}
|
||||
cfg := make(map[string]interface{})
|
||||
if a.Config != "" {
|
||||
json.Unmarshal([]byte(a.Config), &cfg)
|
||||
}
|
||||
result = append(result, &application_authorization_dto.Authorization{
|
||||
UUID: a.UUID,
|
||||
Name: a.Name,
|
||||
Driver: a.Type,
|
||||
Position: a.Position,
|
||||
TokenName: a.TokenName,
|
||||
Config: cfg,
|
||||
ExpireTime: a.ExpireTime,
|
||||
HideCredential: a.HideCredential,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (i *imlAuthorizationModule) ExportAll(ctx context.Context) ([]*application_authorization_dto.ExportAuthorization, error) {
|
||||
list, err := i.authorizationService.List(ctx)
|
||||
if err != nil {
|
||||
@@ -110,7 +181,7 @@ func (i *imlAuthorizationModule) getApplications(ctx context.Context, appIds []s
|
||||
}
|
||||
|
||||
func (i *imlAuthorizationModule) initGateway(ctx context.Context, partitionId string, clientDriver gateway.IClientDriver) error {
|
||||
services, err := i.serviceService.List(ctx)
|
||||
services, err := i.serviceService.AppList(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -129,11 +200,13 @@ func (i *imlAuthorizationModule) initGateway(ctx context.Context, partitionId st
|
||||
}
|
||||
|
||||
func (i *imlAuthorizationModule) online(ctx context.Context, s *service.Service) error {
|
||||
|
||||
clusters, err := i.clusterService.List(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(clusters) < 1 {
|
||||
return nil
|
||||
}
|
||||
authorizations, err := i.authorizationService.ListByApp(ctx, s.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -3,6 +3,7 @@ package catalogue
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/APIParkLab/APIPark/module/system"
|
||||
|
||||
@@ -47,3 +48,9 @@ func init() {
|
||||
return reflect.ValueOf(catalogueModule)
|
||||
})
|
||||
}
|
||||
|
||||
func formatTimeByMinute(org int64) time.Time {
|
||||
t := time.Unix(org, 0)
|
||||
location, _ := time.LoadLocation("Asia/Shanghai")
|
||||
return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), 0, 0, location)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user