Compare commits

...

229 Commits

Author SHA1 Message Date
Dot.L 9ba746ba7f add azure openai
Feature/liujian 1.9
2025-08-26 17:28:36 +08:00
Liujian fdac169bda update azure open config 2025-08-26 17:26:45 +08:00
Liujian 3026e24bab add azure openai 2025-08-21 19:01:02 +08:00
Dot.L e010f00a19 Merge pull request #383 from APIParkLab/feature/liujian-1.9
fix bug: fail to get ai logs
2025-08-15 16:27:51 +08:00
Liujian 1a9b916bab fix bug: fail to get ai logs 2025-08-15 16:25:49 +08:00
JackLiu 3e4f4e1cff Update README.md 2025-08-15 09:33:53 +08:00
ningyv 3217ad2bba Merge pull request #380 from APIParkLab/feature/1.9-OAuth
fix: Fix department selection issue
2025-08-12 10:44:59 +08:00
lcx 1485b31226 fix: Fix department selection issue 2025-08-12 10:43:17 +08:00
ningyv 8968e1f961 Merge pull request #379 from APIParkLab/feature/1.9-OAuth
fix: Fix department selection issue.
2025-08-12 10:23:42 +08:00
lcx 4b8fa43c36 fix: Fix department selection issue. 2025-08-12 10:23:14 +08:00
ningyv c21d783c52 Merge pull request #378 from APIParkLab/feature/1.9-OAuth
fix: issue with adding user permissions
2025-08-11 19:00:31 +08:00
lcx e928cd84d7 fix: issue with adding user permissions 2025-08-11 18:59:39 +08:00
FreyLoong 412cb75bf0 Update readme-zh-cn.md 2025-08-08 18:42:46 +08:00
FreyLoong a13b2a8afe Update README.md 2025-08-08 18:40:14 +08:00
Dot.L c0472c8539 Merge pull request #375 from APIParkLab/feature/liujian-1.9
Feature/liujian 1.9
2025-08-08 12:06:44 +08:00
Liujian 5f40d5092c update qiniu rput param 2025-08-08 12:05:46 +08:00
Liujian b949939816 Fix: The issue where the upstream forwarding header is not effective 2025-08-08 12:04:40 +08:00
Dot.L 55f09f7542 Merge pull request #374 from APIParkLab/feature/liujian-1.9
fix: fail to delete model
2025-08-08 11:31:25 +08:00
Liujian ef1ccb81a9 fix: fail to delete model 2025-08-08 11:29:26 +08:00
Dot.L 7aad2174aa Merge pull request #368 from APIParkLab/feature/liujian-1.9
update service publish config
2025-07-23 20:06:32 +08:00
Liujian 8fba1911ad update service publish config 2025-07-23 20:05:41 +08:00
ningyv 2a951c2854 Merge pull request #366 from APIParkLab/feature/1.9-OAuth
feat: Add Feishu OAuth login & consumer-grade MCP
2025-07-23 15:40:50 +08:00
lcx e91a9e7726 feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-23 15:40:13 +08:00
ningyv e0d97186b1 Merge pull request #363 from APIParkLab/feature/1.9-OAuth
feat: Add Feishu OAuth login & consumer-grade MCP
2025-07-22 10:45:53 +08:00
lcx dff6e722c0 feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-22 10:45:16 +08:00
ningyv b8c92961c1 Merge pull request #362 from APIParkLab/feature/1.9-OAuth
feat: Add Feishu OAuth login & consumer-grade MCP
2025-07-22 10:30:18 +08:00
lcx 025bd4c6cc feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-22 10:28:59 +08:00
ningyv 3d2ec67fc1 Merge pull request #361 from APIParkLab/feature/1.9-OAuth
feat: Add Feishu OAuth login & consumer-grade MCP
2025-07-22 09:55:00 +08:00
lcx b2a8c8d901 feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-22 09:53:41 +08:00
ningyv 90226ac6af Merge pull request #360 from APIParkLab/feature/1.9-OAuth
feat: Add Feishu OAuth login & consumer-grade MCP
2025-07-22 09:41:09 +08:00
lcx 0e1efc9656 feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-22 09:40:30 +08:00
Dot.L 061027aa36 Merge pull request #359 from APIParkLab/feature/liujian-1.9
update ap-account version
2025-07-21 18:51:07 +08:00
Liujian 96e2e6ad51 update ap-account version 2025-07-21 18:49:36 +08:00
ningyv a083f994f4 Merge pull request #358 from APIParkLab/feature/1.9-OAuth
feat: Add Feishu OAuth login & consumer-grade MCP
2025-07-21 18:31:44 +08:00
lcx 5898337481 feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-21 18:30:50 +08:00
ningyv 0f1496137d Merge pull request #357 from APIParkLab/feature/1.9-OAuth
feat: Add Feishu OAuth login & consumer-grade MCP
2025-07-21 18:14:47 +08:00
lcx aff2d1ce01 feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-21 18:14:12 +08:00
Dot.L 78d10318ad Merge pull request #356 from APIParkLab/feature/liujian-1.9
Feature/liujian 1.9
2025-07-21 16:44:55 +08:00
Liujian 0525c51a8f Consumer MCP completed 2025-07-21 16:44:11 +08:00
Liujian 47950d9a2b Merge remote-tracking branch 'origin/main-github-pro' into feature/liujian-1.9 2025-07-21 15:55:52 +08:00
ningyv 4caa19586e Merge pull request #355 from APIParkLab/feature/1.9-OAuth
feat: Add Feishu OAuth login & consumer-grade MCP
2025-07-21 15:01:52 +08:00
lcx a574ed8dae feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-21 14:59:27 +08:00
Liujian 8eba1ea898 Merge remote-tracking branch 'origin/main-github-pro' into feature/liujian-1.9 2025-07-21 10:43:28 +08:00
ningyv 17bd63b27e Merge pull request #353 from APIParkLab/feature/1.9-OAuth
feat: Add Feishu OAuth login & consumer-grade MCP
2025-07-17 18:05:28 +08:00
Dot.L 88d890107e Merge pull request #352 from APIParkLab/feature/liujian-1.9
Feature/liujian 1.9
2025-07-17 18:05:05 +08:00
lcx 6b90770a92 feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-17 18:04:41 +08:00
Liujian c1c5eaa84e finish feishu login bug 2025-07-17 18:04:20 +08:00
Liujian 388cd7660d Merge remote-tracking branch 'github-pro/main' into feature/liujian-1.9 2025-07-17 17:33:30 +08:00
ningyv 3df303abc3 Merge pull request #351 from APIParkLab/feature/1.9-OAuth
feat: Add Feishu OAuth login & consumer-grade MCP
2025-07-17 17:30:17 +08:00
lcx 217e31787c feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-17 17:29:43 +08:00
ningyv 96d92e2323 Merge pull request #350 from APIParkLab/feature/1.9-OAuth
Feature/1.9 o auth
2025-07-17 16:35:23 +08:00
lcx 6ced12af75 feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-17 16:34:51 +08:00
lcx 9e7feff093 feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-17 16:32:29 +08:00
Liujian ba6342d207 Merge remote-tracking branch 'github-pro/main' into feature/liujian-1.9 2025-07-17 15:19:26 +08:00
ningyv 5b38b2f78a Merge pull request #349 from APIParkLab/feature/1.9-OAuth
feat: Add Feishu OAuth login & consumer-grade MCP
2025-07-17 15:18:30 +08:00
lcx 707d98ba34 feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-17 15:17:17 +08:00
Liujian d5f2578a3f Merge remote-tracking branch 'origin/main-github-pro' into feature/liujian-1.9 2025-07-17 14:46:09 +08:00
ningyv a8d39b021c Merge pull request #348 from APIParkLab/feature/1.9-OAuth
feat: Add Feishu OAuth login & consumer-grade MCP
2025-07-17 14:33:07 +08:00
lcx 78dc382e82 feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-17 14:32:06 +08:00
Liujian 4b58534cb6 Merge remote-tracking branch 'origin/main-github-pro' into feature/liujian-1.9 2025-07-17 14:30:15 +08:00
ningyv 63f5a552c8 Merge pull request #347 from APIParkLab/feature/1.9-OAuth
feat: Add Feishu OAuth login & consumer-grade MCP
2025-07-17 14:16:56 +08:00
lcx a03e310418 feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-17 14:16:08 +08:00
ningyv 28ad364352 Merge pull request #346 from APIParkLab/feature/1.9-OAuth
feat: Add Feishu OAuth login & consumer-grade MCP
2025-07-17 13:47:02 +08:00
lcx e485ae2511 feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-17 13:45:25 +08:00
ningyv 853c04337d Merge pull request #345 from APIParkLab/feature/1.9-OAuth
feat: Add Feishu OAuth login & consumer-grade MCP
2025-07-17 13:40:13 +08:00
lcx 526dddb579 feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-17 13:39:44 +08:00
Dot.L 902c64903c Merge pull request #344 from APIParkLab/feature/liujian-1.9
Feature/liujian 1.9
2025-07-17 12:14:04 +08:00
Liujian 2f314e0e64 support feishu login 2025-07-17 12:12:27 +08:00
Liujian 9879506c30 Merge remote-tracking branch 'github-pro/main' into feature/liujian-1.9 2025-07-17 11:52:47 +08:00
ningyv 6dabd5d31b Merge pull request #343 from APIParkLab/feature/1.9-OAuth
Feature/1.9 o auth
2025-07-17 11:52:14 +08:00
lcx 16e5a37087 feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-17 11:51:36 +08:00
lcx 12c5ac85d3 feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-17 11:50:13 +08:00
Liujian 997e9292a2 Merge branch 'feature/liujian-1.8' into feature/liujian-1.9 2025-07-17 11:42:03 +08:00
Liujian 76ae5287eb 新增飞书登录驱动 2025-07-17 11:40:08 +08:00
ningyv c9c88116a8 Merge pull request #342 from APIParkLab/feature/1.9-OAuth
feat: Add Feishu OAuth login & consumer-grade MCP
2025-07-17 11:30:03 +08:00
lcx 393271d72a feat: Add Feishu OAuth login & consumer-grade MCP 2025-07-17 11:29:20 +08:00
Dot.L 1bddef2bda Merge pull request #334 from APIParkLab/feature/liujian-1.8
Optimizing the parameter tiling of MCP Tool to facilitate AI understanding
2025-07-09 18:49:29 +08:00
Liujian 58d02bcf08 Optimizing the parameter tiling of MCP Tool to facilitate AI understanding 2025-07-09 18:48:35 +08:00
Dot.L b1b9f49b06 Merge pull request #329 from APIParkLab/feature/liujian-1.8
Fix the issue of failed compilation of ARM images
2025-06-26 20:05:03 +08:00
Liujian 8f2857cf55 Fix the issue of failed compilation of ARM images 2025-06-26 20:03:48 +08:00
Dot.L f149fede71 Merge pull request #328 from APIParkLab/feature/liujian-1.8
Fix the issue of failed compilation of ARM images
2025-06-26 18:27:45 +08:00
Liujian 972f072346 Fix the issue of failed compilation of ARM images 2025-06-26 18:24:06 +08:00
Dot.L 3cab3c1828 Merge pull request #326 from APIParkLab/feature/liujian-1.8
Fix a series of bug
2025-06-26 17:07:59 +08:00
Liujian 88bf7d0244 1. Fix the issue of ineffective interception of routing settings
2. The problem of long loading times for optimizing service lists and API portals
2025-06-26 17:06:03 +08:00
Dot.L a7523c7b54 Merge pull request #322 from APIParkLab/feature/liujian-1.8
fix: The problem of slow retrieval of service lists and API portal se…
2025-06-19 17:02:14 +08:00
Liujian 590f328e07 fix: The problem of slow retrieval of service lists and API portal service lists 2025-06-19 17:00:51 +08:00
Dot.L ca2682fb22 Merge pull request #311 from APIParkLab/feature/liujian-1.8
update docker run.sh
2025-05-26 19:33:51 +08:00
Liujian 2bd1d4a423 update docker run.sh 2025-05-26 19:32:10 +08:00
Dot.L b2baa711c2 Merge pull request #309 from APIParkLab/feature/1.8-cx
chore: Add annotations
2025-05-23 18:31:33 +08:00
Dot.L b55675e5a5 Merge pull request #308 from APIParkLab/feature/liujian-1.8
Feature/liujian 1.8
2025-05-23 18:24:32 +08:00
Liujian 9a33992a0b Merge remote-tracking branch 'github-pro/main' into feature/liujian-1.8
# Conflicts:
#	.gitlab-ci.yml
2025-05-23 18:23:52 +08:00
Liujian 07ae37eb5f update gitlab-ci.yml 2025-05-23 18:21:50 +08:00
Liujian c36726f25f update gitlab-ci.yml 2025-05-23 17:45:57 +08:00
Liujian e2e9abeb4c update gitlab-ci.yml 2025-05-23 16:19:28 +08:00
Liujian 0fcc2215f7 Merge remote-tracking branch 'origin/main' into main-github-pro 2025-05-23 15:42:51 +08:00
刘健 ce559c4643 Update .gitlab-ci.yml file 2025-05-23 15:30:54 +08:00
刘健 8d4b13f633 Update .gitlab-ci.yml file 2025-05-23 15:30:13 +08:00
Liujian 19a3378fa3 Merge branch 'main' into main-github-pro 2025-05-23 15:16:29 +08:00
Liujian cef1250199 Merge tag 'v1.8.0-beta' 2025-05-23 15:16:02 +08:00
刘健 3c85658931 Update .gitlab-ci.yml file 2025-05-23 15:05:39 +08:00
刘健 ba7022bc2d Update .gitlab-ci.yml file 2025-05-23 15:02:52 +08:00
刘健 fb24abc111 Update .gitlab-ci.yml file 2025-05-23 15:02:23 +08:00
刘健 0e3fb84e7c Update .gitlab-ci.yml file 2025-05-23 15:01:47 +08:00
刘健 5d6d949ca4 Update .gitlab-ci.yml file 2025-05-23 15:00:44 +08:00
刘健 3578182343 Update .gitlab-ci.yml file 2025-05-23 14:32:11 +08:00
刘健 28bad2d963 Update .gitlab-ci.yml file 2025-05-23 14:31:31 +08:00
刘健 384bd239fa Update .gitlab-ci.yml file 2025-05-23 14:21:58 +08:00
刘健 98710ad296 Update .gitlab-ci.yml file 2025-05-23 14:12:49 +08:00
刘健 4806e12907 Update .gitlab-ci.yml file 2025-05-23 14:12:09 +08:00
刘健 9097760a0f Update .gitlab-ci.yml file 2025-05-23 14:10:08 +08:00
刘健 a5639bff60 Update .gitlab-ci.yml file 2025-05-23 14:01:10 +08:00
刘健 1d66ed84f3 Update .gitlab-ci.yml file 2025-05-23 13:55:45 +08:00
刘健 249ac3ea1c Update .gitlab-ci.yml file 2025-05-23 13:55:22 +08:00
刘健 b02db8020d Update .gitlab-ci.yml file 2025-05-23 13:55:03 +08:00
刘健 4105540686 Update .gitlab-ci.yml file 2025-05-23 13:54:37 +08:00
刘健 36d10c5cfd Update .gitlab-ci.yml file 2025-05-23 13:54:17 +08:00
Dot.L f77bd76a14 Merge pull request #301 from APIParkLab/feature/liujian-1.8
Feature/liujian 1.8
2025-05-06 18:53:11 +08:00
ningyv cef548ce7d Merge pull request #299 from APIParkLab/feature/1.8-cx
Feature/1.8 cx
2025-05-06 18:52:12 +08:00
lichunxian 5efd19ef7c Merge branch 'feature/1.8-cx' into 'main'
feat: feature/1.8-Improve system observability

See merge request apipark/APIPark!377
2025-05-06 17:58:28 +08:00
lichunxian 28cd4fd91c Merge branch 'feature/1.8-cx' into 'main'
feature/1.8-Improve system observability

See merge request apipark/APIPark!376
2025-05-06 16:06:16 +08:00
刘健 82f4089f42 Merge branch 'feature/liujian-1.8' into 'main'
update build.sh

See merge request apipark/APIPark!375
2025-05-06 16:02:17 +08:00
Liujian 00ef4d2cfc update build.sh 2025-05-06 16:01:51 +08:00
刘健 cd33448446 Merge branch 'feature/liujian-1.8' into 'main'
fix bug

See merge request apipark/APIPark!373
2025-05-06 15:31:23 +08:00
Liujian ddd70b0ff5 fix: log detail bug 2025-05-06 15:31:07 +08:00
lichunxian e5b50a7073 Merge branch 'feature/1.8-cx' into 'main'
feature/1.8-Improve system observability

See merge request apipark/APIPark!374
2025-05-06 14:41:00 +08:00
Liujian 5a1baadf3b fix bug 2025-05-06 14:38:54 +08:00
刘健 10bd352bf4 Merge branch 'feature/liujian-1.8' into 'main'
update data

See merge request apipark/APIPark!372
2025-05-06 14:12:01 +08:00
Liujian cbea45e6e0 update data 2025-05-06 14:11:36 +08:00
刘健 e5c6e4fa82 Merge branch 'feature/liujian-1.8' into 'main'
Feature/liujian 1.8

See merge request apipark/APIPark!371
2025-05-06 12:04:47 +08:00
Liujian f05457fd2c update data 2025-05-06 12:04:11 +08:00
Liujian bc6875fe9f Merge remote-tracking branch 'origin/feature/1.8-cx' into feature/liujian-1.8 2025-05-06 11:18:15 +08:00
lichunxian 1572e03dd1 Merge branch 'feature/1.8-cx' into 'main'
feature/1.8-Improve system observability

See merge request apipark/APIPark!370
2025-05-06 11:09:42 +08:00
刘健 61025763ed Merge branch 'feature/liujian-1.8' into 'main'
Feature/liujian 1.8

See merge request apipark/APIPark!369
2025-05-06 10:49:33 +08:00
Liujian ef1c48e395 Modify the monitoring table to return field types 2025-05-06 10:43:09 +08:00
lichunxian 00905e4167 Merge branch 'feature/1.8-cx' into 'main'
feature/1.8-Improve system observability

See merge request apipark/APIPark!368
2025-04-30 19:09:44 +08:00
Liujian 9572c4157e Merge remote-tracking branch 'github-pro/feature/1.8-cx' into feature/liujian-1.8 2025-04-30 18:55:09 +08:00
Liujian fef49eb32c tmp commit 2025-04-30 18:55:01 +08:00
lichunxian a5a895e42d Merge branch 'feature/1.8-cx' into 'main'
feature/1.8-Improve system observability

See merge request apipark/APIPark!367
2025-04-30 17:18:00 +08:00
lichunxian ed1d19532b Merge branch 'feature/1.8-cx' into 'main'
feature/1.8-Improve system observability

See merge request apipark/APIPark!366
2025-04-30 16:33:49 +08:00
Liujian 9c1b19a1c7 Merge remote-tracking branch 'github-pro/feature/1.8-cx' into feature/liujian-1.8 2025-04-30 15:57:59 +08:00
刘健 bf990517dc Merge branch 'feature/liujian-1.8' into 'main'
update service logs

See merge request apipark/APIPark!365
2025-04-30 15:53:11 +08:00
Liujian 0cf7f952e2 update service logs 2025-04-30 15:52:48 +08:00
lichunxian 83873c8c92 Merge branch 'feature/1.8-cx' into 'main'
feature/1.8-Improve system observability

See merge request apipark/APIPark!364
2025-04-30 15:49:50 +08:00
刘健 a61e6ba67f Merge branch 'feature/liujian-1.8' into 'main'
Optimize chart data

See merge request apipark/APIPark!363
2025-04-30 15:06:24 +08:00
Liujian 94d881cc18 Optimize chart data 2025-04-30 15:05:47 +08:00
刘健 e081580786 Merge branch 'feature/liujian-1.8' into 'main'
fix:ai token monitor bug

See merge request apipark/APIPark!362
2025-04-30 14:00:22 +08:00
Liujian 8927211ea2 fix:ai token monitor bug 2025-04-30 13:59:50 +08:00
lichunxian 813905ca40 Merge branch 'feature/1.8-cx' into 'main'
feature/1.8-Improve system observability

See merge request apipark/APIPark!361
2025-04-30 10:25:41 +08:00
lichunxian 66be761d18 Merge branch 'feature/1.8-cx' into 'main'
feature/1.8-Improve system observability

See merge request apipark/APIPark!360
2025-04-30 09:46:26 +08:00
lichunxian 6b6fa5bd40 Merge branch 'feature/1.8-cx' into 'main'
feature/1.8-Improve system observability

See merge request apipark/APIPark!359
2025-04-30 09:30:12 +08:00
lichunxian 943ef4f9b0 Merge branch 'feature/1.8-cx' into 'main'
feature/1.8-Improve system observability

See merge request apipark/APIPark!358
2025-04-30 09:20:32 +08:00
lichunxian 78d9a1c23c Merge branch 'feature/1.8-cx' into 'main'
feature/1.8-Improve system observability

See merge request apipark/APIPark!357
2025-04-30 09:09:26 +08:00
刘健 342d022c43 Merge branch 'feature/liujian-1.8' into 'main'
Feature/liujian 1.8

See merge request apipark/APIPark!356
2025-04-30 00:24:33 +08:00
Liujian 1d36f4b821 fix monitor bug 2025-04-30 00:24:02 +08:00
Liujian 4e459168df Merge remote-tracking branch 'origin/feature/1.8-cx' into feature/liujian-1.8 2025-04-29 22:56:28 +08:00
lichunxian a8c14ee839 Merge branch 'feature/1.8-cx' into 'main'
feature/1.8-Improve system observability

See merge request apipark/APIPark!355
2025-04-29 19:40:30 +08:00
lichunxian 23c40efe0d Merge branch 'feature/1.8-cx' into 'main'
feature/1.8-Improve system observability

See merge request apipark/APIPark!354
2025-04-29 19:33:43 +08:00
lichunxian a76941ea17 Merge branch 'feature/1.8-cx' into 'main'
feature/1.8-Improve system observability

See merge request apipark/APIPark!353
2025-04-29 19:28:59 +08:00
刘健 0c392d2092 Merge branch 'feature/liujian-1.8' into 'main'
Log information returns the newly added Body

See merge request apipark/APIPark!352
2025-04-29 19:28:04 +08:00
Liujian e5f0423a90 Log information returns the newly added Body 2025-04-29 19:23:35 +08:00
刘健 46caf49f18 Merge branch 'feature/liujian-1.8' into 'main'
fix bug

See merge request apipark/APIPark!351
2025-04-29 19:16:54 +08:00
Liujian 7dc8d65235 fix bug 2025-04-29 19:16:21 +08:00
刘健 74c87ec308 Merge branch 'feature/liujian-1.8' into 'main'
finish service log module

See merge request apipark/APIPark!350
2025-04-29 19:08:43 +08:00
Liujian 604a8312ef finish service log module 2025-04-29 19:08:02 +08:00
lichunxian 8b318caa0b Merge branch 'feature/1.8-cx' into 'main'
feature/1.8-Improve system observability

See merge request apipark/APIPark!349
2025-04-29 19:06:52 +08:00
lichunxian 6bad1c3c7c Merge branch 'feature/1.8-cx' into 'main'
feature/1.8-Improve system observability

See merge request apipark/APIPark!348
2025-04-29 18:24:17 +08:00
刘健 1333d4ed02 Merge branch 'feature/liujian-1.8' into 'main'
finish log list

See merge request apipark/APIPark!347
2025-04-29 17:49:59 +08:00
Liujian a3bebde83c finish log list 2025-04-29 17:44:53 +08:00
lichunxian d3e91b04a2 Merge branch 'feature/1.8-cx' into 'main'
feature/1.8-Improve system observability

See merge request apipark/APIPark!346
2025-04-29 17:33:16 +08:00
lichunxian f4400c0130 Merge branch 'feature/1.8-cx' into 'main'
feature/1.8-Improve system observability

See merge request apipark/APIPark!345
2025-04-29 17:27:37 +08:00
lichunxian cc5d677d67 Merge branch 'feature/1.8-cx' into 'main'
feature/1.8-Improve system observability

See merge request apipark/APIPark!344
2025-04-29 11:52:46 +08:00
刘健 e4c3cbc99b Merge branch 'feature/liujian-1.8' into 'main'
update service overview

See merge request apipark/APIPark!343
2025-04-29 10:20:17 +08:00
Liujian 771c86229d update service overview 2025-04-29 10:18:02 +08:00
刘健 5c1db00d7e Merge branch 'feature/liujian-1.8' into 'main'
Feature/liujian 1.8

See merge request apipark/APIPark!342
2025-04-29 00:35:41 +08:00
Liujian cff536710e finish: monitor overview 2025-04-29 00:34:58 +08:00
lichunxian cd91f4bdb9 Merge branch 'feature/1.8-cx' into 'main'
feature/1.8-Improve system observability

See merge request apipark/APIPark!341
2025-04-27 17:44:31 +08:00
lichunxian 79860bc665 Merge branch 'feature/1.8-cx' into 'main'
feature/1.8-Improve system observability

See merge request apipark/APIPark!340
2025-04-27 14:55:12 +08:00
lichunxian 4623ba6fba Merge branch 'feature/1.8-cx' into 'main'
feature/1.8-Improve system observability

See merge request apipark/APIPark!339
2025-04-27 14:50:59 +08:00
lichunxian 1f4acdc99e Merge branch 'feature/1.8-cx' into 'main'
feature/1.8-Improve system observability

See merge request apipark/APIPark!338
2025-04-27 14:31:19 +08:00
lichunxian 0307282dbd Merge branch 'feature/1.8-cx' into 'main'
feature/1.8-Improve system observability

See merge request apipark/APIPark!337
2025-04-27 14:15:29 +08:00
lichunxian 7dbc2a1a78 Merge branch 'feature/1.8-cx' into 'main'
feature/1.8-Improve system observability

See merge request apipark/APIPark!336
2025-04-27 10:53:53 +08:00
lichunxian 12d42c4247 Merge branch 'feature/1.8-cx' into 'main'
Feature/1.8 cx

See merge request apipark/APIPark!335
2025-04-27 10:45:08 +08:00
Dot.L a22759136e Merge pull request #298 from APIParkLab/feature/1.7-liujian
update docker build script
2025-04-24 16:03:14 +08:00
Liujian b8ebbac2b8 update docker build script 2025-04-24 16:02:32 +08:00
Dot.L 9c4590db07 Merge pull request #297 from APIParkLab/feature/1.7-liujian
Fix: Apikey getting md5 when calling MCP Server at service level
2025-04-22 18:08:51 +08:00
Liujian 7ba8a57793 Fix: Apikey getting md5 when calling MCP Server at service level 2025-04-22 18:08:24 +08:00
lichunxian 4eb3368875 Merge branch 'feature/1.7-cxx' into 'main'
feature/1.7-MCP-analytics-table-optimize

See merge request apipark/APIPark!334
2025-04-16 15:47:13 +08:00
刘健 4478e6823a Merge branch 'feature/1.7-liujian' into 'main'
Fix: Dragging to modify the order of service categories will fail when there...

See merge request apipark/APIPark!333
2025-04-16 15:21:28 +08:00
lichunxian ab5bffea87 Merge branch 'feature/1.7-cxx' into 'main'
Feature/1.7 cxx

See merge request apipark/APIPark!332
2025-04-16 14:36:37 +08:00
刘健 8a48828a76 Merge branch 'feature/1.7-liujian' into 'main'
Fix: Issue of API duplicate publishing when publishing services

See merge request apipark/APIPark!331
2025-04-16 14:34:39 +08:00
刘健 c2b70e23e4 Merge branch 'feature/1.7-liujian' into 'main'
update

See merge request apipark/APIPark!330
2025-04-16 14:12:52 +08:00
lichunxian eb46a4365c Merge branch 'feature/1.7-cxx' into 'main'
feature/1.7-MCP

See merge request apipark/APIPark!329
2025-04-16 14:07:50 +08:00
lichunxian 058a8f7974 Merge branch 'feature/1.7-cxx' into 'main'
feature/1.7-MCP

See merge request apipark/APIPark!328
2025-04-16 13:48:41 +08:00
lichunxian b5585f548a Merge branch 'feature/1.7-cxx' into 'main'
feature/1.7-MCP

See merge request apipark/APIPark!327
2025-04-16 13:45:08 +08:00
lichunxian e4a3e1a1a2 Merge branch 'feature/1.7-cxx' into 'main'
feature/1.7-MCP

See merge request apipark/APIPark!326
2025-04-16 11:54:13 +08:00
刘健 674a15ef32 Merge branch 'feature/1.7-liujian' into 'main'
update api portal interface

See merge request apipark/APIPark!325
2025-04-16 11:11:14 +08:00
lichunxian d82d665280 Merge branch 'feature/1.7-cxx' into 'main'
feature/1.7-MCP

See merge request apipark/APIPark!324
2025-04-16 10:19:11 +08:00
lichunxian 752db42b3b Merge branch 'feature/1.7-cxx' into 'main'
feature/1.7-MCP

See merge request apipark/APIPark!323
2025-04-16 09:53:24 +08:00
lichunxian 10aaf85a26 Merge branch 'feature/1.7-cxx' into 'main'
feature/1.7-MCP

See merge request apipark/APIPark!322
2025-04-16 09:47:57 +08:00
lichunxian 5c97ef9416 Merge branch 'feature/1.7-cxx' into 'main'
feature/1.7-MCP

See merge request apipark/APIPark!321
2025-04-16 09:18:16 +08:00
刘健 5fc84299f1 Merge branch 'feature/1.7-liujian' into 'main'
update path

See merge request apipark/APIPark!320
2025-04-16 00:01:29 +08:00
刘健 2eeeebf7c2 Merge branch 'feature/1.7-liujian' into 'main'
MCP Server supports multiple languages.

See merge request apipark/APIPark!319
2025-04-15 22:42:51 +08:00
lichunxian 155ad537a9 Merge branch 'feature/1.7-cxx' into 'main'
feature/1.7-MCP

See merge request apipark/APIPark!318
2025-04-15 18:58:14 +08:00
lichunxian 4a1430c62a Merge branch 'feature/1.7-cxx' into 'main'
feature/1.7-MCP-Breadcrumb

See merge request apipark/APIPark!317
2025-04-14 17:45:58 +08:00
lichunxian 8cc0d038bd Merge branch 'feature/1.7-cxx' into 'main'
feature/1.7-MCP-Breadcrumb

See merge request apipark/APIPark!316
2025-04-14 17:33:06 +08:00
lichunxian 165759398e Merge branch 'feature/1.7-cxx' into 'main'
feature/1.7-MCP

See merge request apipark/APIPark!315
2025-04-14 14:09:47 +08:00
lichunxian 1091d4e086 Merge branch 'feature/1.7-cxx' into 'main'
Feature/1.7 cxx

See merge request apipark/APIPark!314
2025-04-14 13:37:01 +08:00
刘健 0523f13dfb Merge branch 'feature/1.7-liujian' into 'main'
Feature/1.7 liujian

See merge request apipark/APIPark!313
2025-04-14 11:57:40 +08:00
lichunxian 256c04f5bb Merge branch 'feature/1.7-cxx' into 'main'
feature/1.7-MCP

See merge request apipark/APIPark!312
2025-04-14 11:33:52 +08:00
lichunxian a9dcc78db6 Merge branch 'feature/1.7-cxx' into 'main'
feature/1.7-MCP

See merge request apipark/APIPark!311
2025-04-14 11:12:24 +08:00
刘健 1f6c173e18 Merge branch 'feature/1.7-liujian' into 'main'
Fix: API forwarding header setting failure issue

See merge request apipark/APIPark!310
2025-04-14 10:20:17 +08:00
lichunxian b593e8b57b Merge branch 'feature/1.7-cxx' into 'main'
feature/1.7-MCP

See merge request apipark/APIPark!309
2025-04-14 09:54:30 +08:00
lichunxian 1ec00de03c Merge branch 'feature/1.7-cxx' into 'main'
feature/1.7-MCP

See merge request apipark/APIPark!308
2025-04-11 18:41:00 +08:00
lichunxian bad7fbadda Merge branch 'feature/1.7-cxx' into 'main'
Feature/1.7 cxx

See merge request apipark/APIPark!307
2025-04-11 18:31:30 +08:00
lichunxian 7a506fc15e Merge branch 'feature/1.7-cxx' into 'main'
feature/1.7-MCP

See merge request apipark/APIPark!306
2025-04-11 17:28:46 +08:00
lichunxian dec2c3a23e Merge branch 'feature/1.7-cxx' into 'main'
feature/1.7-MCP

See merge request apipark/APIPark!305
2025-04-11 16:49:06 +08:00
刘健 2093541c37 Merge branch 'feature/1.7-liujian' into 'main'
Add the openapiaddress field to the API portal to obtain detailed service information

See merge request apipark/APIPark!304
2025-04-11 14:43:07 +08:00
lichunxian 40c7ba4305 Merge branch 'feature/1.7-cxx' into 'main'
feature/1.7-MCP

See merge request apipark/APIPark!303
2025-04-11 14:18:13 +08:00
刘健 a95bca31e2 Merge branch 'feature/1.7-liujian' into 'main'
service detail add invoke_count

See merge request apipark/APIPark!302
2025-04-11 11:58:33 +08:00
刘健 a541e45a53 Merge branch 'feature/1.7-liujian' into 'main'
Feature/1.7 liujian

See merge request apipark/APIPark!301
2025-04-11 11:04:56 +08:00
lichunxian b20c66b311 Merge branch 'feature/1.7-cxx' into 'main'
feature/1.7-MCP

See merge request apipark/APIPark!300
2025-04-11 10:06:06 +08:00
lichunxian 729e1f105c Merge branch 'feature/1.7-cxx' into 'main'
feature/1.7-MCP

See merge request apipark/APIPark!299
2025-04-10 17:48:12 +08:00
lichunxian 6aa96a2ae9 Merge branch 'feature/1.7-cxx' into 'main'
feature/1.7-MCP

See merge request apipark/APIPark!298
2025-04-10 16:29:48 +08:00
lichunxian 5093c98656 Merge branch 'feature/1.7-cxx' into 'main'
feature/1.7-MCP

See merge request apipark/APIPark!297
2025-04-10 16:22:52 +08:00
lichunxian f4b70d4e71 Merge branch 'feature/1.7-cxx' into 'main'
feature/1.7-MCP

See merge request apipark/APIPark!296
2025-04-10 16:11:01 +08:00
刘健 eeb36f43a4 Update .gitlab-ci.yml file 2025-04-10 15:52:44 +08:00
lichunxian 5a59a6d378 Merge branch 'feature/1.7-cxx' into 'main'
feature/1.7-MCP

See merge request apipark/APIPark!295
2025-04-10 15:27:13 +08:00
lichunxian c0045d17e2 Merge branch 'feature/1.7-cxx' into 'main'
Feature/1.7 cxx

See merge request apipark/APIPark!294
2025-04-10 10:47:54 +08:00
刘健 42963d3ee5 Merge branch 'feature/1.7-liujian' into 'main'
Revert "Auxiliary commit to revert individual files from da05525cbbf2510a2cbc37d7eed6bfb8248e448b"

See merge request apipark/APIPark!291
2025-04-10 10:00:30 +08:00
126 changed files with 69789 additions and 62459 deletions
+5
View File
@@ -75,6 +75,11 @@ jobs:
with:
name: frontend-package
path: frontend/dist
# 设置 QEMU 以支持多架构构建
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login Docker #登录docker
uses: docker/login-action@v1
with:
+2 -1
View File
@@ -7,4 +7,5 @@
/.vscode/
.air.toml
/tmp/
/work
/work
/cmd/
+88
View File
@@ -0,0 +1,88 @@
variables:
PATH: /opt/go-1.23/go/bin/:/opt/node-1.22/bin/:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
GOROOT: /opt/go-1.23/go
GOPROXY: https://goproxy.cn
VERSION: $CI_COMMIT_SHORT_SHA
APP: apipark
APP_PRE: ${APP}_${VERSION}
BUILD_DIR: ${APP}-build
DEPLOY_DESC: "DEV 环境"
VIEW_ADDR: http://172.18.166.219:8288
SAVE_DIR: /opt/${APP}
NODE_OPTIONS: --max_old_space_size=8192
stages:
# - notice
- build
- deploy
- webhook
#
#feishu-informer: # 飞书回调
# stage: notice
# variables:
# DIFF_URL: "$CI_MERGE_REQUEST_PROJECT_URL/-/merge_requests/$CI_MERGE_REQUEST_IID/diffs"
# rules:
# - if: $CI_PIPELINE_SOURCE=="merge_request_event" && $CI_COMMIT_BRANCH =~ "main-github-pro"
# script:
# - echo "merge request"
# - |
# curl -X POST -H "Content-Type: application/json" \
# -d "{\"msg_type\":\"text\",\"content\":{\"text\":\"项目:${CI_PROJECT_NAME}\\n提交人:${GITLAB_USER_NAME}\\n提交信息:${CI_MERGE_REQUEST_TITLE}\\n合并分支信息:${CI_MERGE_REQUEST_SOURCE_BRANCH_NAME} -> ${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}\\n差异性地址:${DIFF_URL}\\n请及时review代码\"}}" \
# ${FEISHU_WEBHOOK}
builder:
stage: build
rules:
- if: $CI_COMMIT_BRANCH == "main-github-pro" || $CI_COMMIT_BRANCH == "main"
script:
- set -e
- |
if [ ! -d "../artifacts" ]; then
mkdir -p ../artifacts
fi
if [ -d "../artifacts/dist" ]; then
cp -r ../artifacts/dist frontend/dist
fi
- |
if [ -n "$(git diff --name-status HEAD~1 HEAD -- frontend)" ]; then
./scripts/build.sh $BUILD_DIR ${VERSION} all ""
else
./scripts/build.sh $BUILD_DIR ${VERSION}
fi
if [ -d "frontend/dist" ]; then
echo "copy frontend/dist to artifacts/dist"
rm -fr ../artifacts/dist
cp -r frontend/dist ../artifacts/dist
fi
cp $BUILD_DIR/${APP_PRE}_linux_amd64.tar.gz ${SAVE_DIR}
deployer:
stage: deploy
rules:
- if: $CI_COMMIT_BRANCH == "main-github-pro" || $CI_COMMIT_BRANCH == "main"
variables:
APIPARK_GUEST_MODE: allow
APIPARK_GUEST_ID: dklejrfbhjqwdh
script:
- cd ${SAVE_DIR};mkdir -p ${APP_PRE};tar -zxvf ${APP_PRE}_linux_amd64.tar.gz -C ${APP_PRE};cd ${APP_PRE};./install.sh ${SAVE_DIR};./run.sh restart;cd ${SAVE_DIR} && ./clean.sh ${APP_PRE}
when: on_success
success:
stage: webhook
rules:
- if: $CI_COMMIT_BRANCH == "main-github-pro" || $CI_COMMIT_BRANCH == "main"
script:
- |
curl -X POST -H "Content-Type: application/json" \
-d "{\"msg_type\":\"text\",\"content\":{\"text\":\"最近一次提交:${CI_COMMIT_TITLE}\\n提交人:${GITLAB_USER_NAME}\\n项目:${CI_PROJECT_NAME}\\n环境:${DEPLOY_DESC}\\n更新部署完成.\\n访问地址:${VIEW_ADDR}\\n工作流地址:${CI_PIPELINE_URL}\"}}" \
${FEISHU_WEBHOOK}
when: on_success
failure:
stage: webhook
rules:
- if: $CI_COMMIT_BRANCH == "main-github-pro" || $CI_COMMIT_BRANCH == "main"
script:
- |
curl -X POST -H "Content-Type: application/json" \
-d "{\"msg_type\":\"text\",\"content\":{\"text\":\"最近一次提交:${CI_COMMIT_TITLE}\\n提交人:${GITLAB_USER_NAME}\\n项目:${CI_PROJECT_NAME}\\n环境:${DEPLOY_DESC}\\n更新部署失败,请及时到gitlab上查看\\n工作流地址:${CI_PIPELINE_URL}\"}}" \
${FEISHU_WEBHOOK}
when: on_failure
+54 -1
View File
@@ -156,6 +156,10 @@ curl -sSO https://download.apipark.com/install/quick-start.sh ; bash quick-sta
<br>
<br>
# 🚀 Use Cases
## Simplify AI Integration Costs
- Connect to 100+ major models from all mainstream AI vendors, with standardized API calls requiring no additional adaptation work.
@@ -199,11 +203,20 @@ To achieve this goal, we plan to add new features to APIPark, including:
# 📕 Documentation
Visit [APIPark Documentation](https://docs.apipark.com/docs/deploy) for detailed installation guides, API references, and usage instructions.
# 🧑‍🤝‍🧑Friendly Links
<a href="https://xroute.ai/">
<img width="1248" height="158" alt="新建 PPTX 演示文稿 (2)_03" src="https://github.com/user-attachments/assets/0ebd694c-410a-4e3f-a793-90f1140d15df" />
</a>
<br>
<br>
# 🧾 License
APIPark uses the Apache 2.0 License. For more details, please refer to the LICENSE file.
<br>
# 💌 Contact Us
@@ -212,6 +225,46 @@ For enterprise-level features and professional technical support, contact our pr
- Website: https://apipark.com
- Email: contact@apipark.com
🙏 A big thanks to everyone who helped shape APIPark. We are thrilled to hear the communitys thoughts! Lets make the world of APIs and AI stronger and more fun together. 🎉
<br>
🙏 A big thanks to everyone who helped shape APIPark. We are thrilled to hear the communitys thoughts! Lets make the world of APIs and AI stronger and more fun together. 🎉
# 🤝 Partner
- [Cursor](https://www.cursor.com/): Cursor is an AI-powered code editor that integrates artificial intelligence directly into the coding workflow, offering features like intelligent next edit suggestions, deep codebase understanding for relevant answers, and natural language editing to streamline development tasks and boost developer productivity.
- [Dify](https://dify.ai/): Dify is a leading Agentic AI Development Platform that provides a comprehensive suite of tools for building and extending AI applications, offering everything needed for agentic workflows, RAG pipelines, integrations, and observability, while allowing users to amplify their applications with various global Large Language Models (LLMs) and versatile plugins.
- [Trae](https://www.trae.ai/): Trae is an AI-native Integrated Development Environment (IDE) product that aims to embody the concept of “The Real AI Engineer” through intelligent productivity, seamlessly integrating into the development process to enhance quality and efficiency, featuring a chat-based interaction interface and supporting code generation and assistance.
- [Windsurf](https://windsurf.com/): Windsurf is an AI code editor designed to provide a seamless and limitless flow for developers, introducing a new purpose-built IDE that leverages AI to enhance coding with features like "Cascade" for deep codebase understanding, "Windsurf Tab" for intelligent autocompletion, and "Memories" for remembering important aspects of the codebase.
- [Coze](https://www.coze.com/): Coze is a next-generation AI application and chatbot development platform by ByteDance, empowering users to easily create and deploy powerful AI chatbots across various platforms with a no-code bot builder, integrated workflow logic, access to proprietary data, and simplified creation through pre-built plugins, knowledge bases, and workflows.
- [Claude Code](https://www.anthropic.com/claude-code): Claude Code is a command-line AI tool by Anthropic that embeds the Claude Opus 4 model directly into the users terminal, providing deep codebase awareness, the ability to edit files and execute commands, and making coordinated changes across multiple files, all while integrating seamlessly with popular IDEs and leveraging existing test suites.
- [Flowith](https://flowith.io/): Flowith is an AI creation workspace designed to revolutionize productivity and deep work by transforming knowledge and streamlining tasks through a multi-thread interface powered by advanced AI agents, offering an intuitive canvas-based user experience unlike traditional chat-based AI tools, and including a 24/7 operational version for complex tasks.
- [OpenManus](https://github.com/FoundationAgents/OpenManus): OpenManus is an open-source framework dedicated to building general AI agents, aiming to provide a platform where users can create and deploy their own agents without an invite code, supporting multi-agent capabilities, and requiring configuration for Large Language Model (LLM) APIs while integrating with browser automation tools.
- [Fellou](https://fellou.ai/): Fellou is an innovative Agentic Browser designed to transcend traditional web browsing by actively performing actions on behalf of the user, automating the entire process of information gathering and insight delivery, and excelling in in-depth research with seamless integrations with popular tools like Notion and LinkedIn.
- [Genspark](https://www.genspark.ai/): Genspark is an ultimate all-in-one AI companion offering a comprehensive suite of tools like AI Slides, AI Sheets, and AI Chat, designed to enhance various aspects of productivity and content creation, with personalized tools and AI Pods for generating content from diverse sources.
- [TEN](https://github.com/TEN-framework/ten-framework): TEN (The Embodied Narrator) is an open-source framework for building real-time, multimodal conversational voice AI agents, including components like TEN Framework, TEN Turn Detection, TEN Agent, TMAN Designer, and TEN Portal, offering features like Real-time Avatar, seamless MCP integration, real-time hardware communication, and vision/screenshare detection.
- [ChatGPT](https://chatgpt.com/): ChatGPT is an AI chatbot developed by OpenAI, built upon large language models like GPT-3.5 and GPT-4, designed to generate human-like conversational dialogue, understand context, answer follow-up questions, and integrate with various platforms for enhanced productivity through advanced language understanding, generation, and multilingual capabilities.
- [LangChain](https://www.langchain.com/): LangChain is a robust platform engineered for the development of reliable agents and Large Language Model (LLM) applications, offering a comprehensive product suite that seamlessly integrates various tools across the entire application development lifecycle, including LangGraph, LangSmith, and the LangGraph Platform, with functionalities for code generation, automation, and AI Search.
- [LEMON AI](https://lemonai.cc/): Lemon AI is the first Full-stack, Open-source, Agentic AI framework, offering a fully local alternative to platforms like Manus & Genspark AI. It features an integrated Code Interpreter VM sandbox for safe execution.
- [LobeChat](https://lobehub.com/): LobeHub offers LobeChat, a personal LLM productivity tool designed to elevate the user experience beyond traditional chatbots by empowering individuals to build personal AI agents and professional teams, supporting a wide array of LLMs, offering a simple chat interface, visual recognition, voice interaction, a rich plugin ecosystem, and knowledge base functionalities.
- [VS Code](https://code.visualstudio.com/): Visual Studio Code (VS Code) is a widely popular, free, and open-source code editor by Microsoft, renowned for its extensibility and customization, supporting vast programming languages, and integrating AI capabilities like intelligent next edit suggestions and an advanced “agent mode” for complex tasks, with broad compatibility with various AI models.
- [XRoute](https://xroute.ai): The Unified Interface For LLMs, provides better prices, better throughput, and no subscription.
- [XPack MCP Marketplace](https://github.com/xpack-ai/XPack-MCP-Marketplace): The world's first open-source MCP monetization platform, transform any OpenAPI into a monetizable MCP server and build your own API marketplace in just 10 minutes. Everything is open-source and ready for commercial use.
- [MemU](https://github.com/NevaMind-AI/memU): MemU is an open-source memory framework for AI companions
+62748 -60558
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="356px" height="48px" viewBox="0 0 356 48" enable-background="new 0 0 356 48" xml:space="preserve"> <image id="image0" width="356" height="48" x="0" y="0"
xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAWQAAAAwCAMAAAARrfBHAAAAIGNIUk0AAHomAACAhAAA+gAAAIDo
AAB1MAAA6mAAADqYAAAXcJy6UTwAAAEmUExURQAAACBAQCAwQCoqQEBAQICAgCQ3SSoqVTMzTBwo
OB0mOR0pORsoOBAgQBwoOB0qOhwpOR4oOxgoOBsqNR4pORwoOh0pOR0pOR4qOh0qOSAoOB0pOR0p
OR0xOx4qOh0pOR4pNxspOR0pOR0pOBwoOhslNR0qORwpOhwpOR0oOBwoOCAqNRslOh4qOCAwQB0p
OB4qOR0oOhwqOR4pOR0pOB0oOiYzQB0oOB0qOiAqQBwpOR4pOSoqOR0oOBgoOBwoOBwpOSAwQBwp
OiAgQCAoOB0pORwqOSMuOh0qOB0pOSArNSQuQB0pOCAlOiIqOx0oOBwqORslOhsoOB0pOREhQB0q
OR4nOR4nOSAwQBwoOCAlOiArNe9PIX65Ax0pOQOk7vu2BP///6G4x94AAABcdFJOUwAAAAAAAAAA
AEBQUGAQgJ+QXyAwb4De73/vIN+vAc/+cHC/oH8w7s/PYL8wMIAQv++fz3CwngGgTwGvjwFhIUGP
Ec4QIb4BAWDuMQG+MAHAzjFhURGvcXABvjEw0iVpWQAAAAFiS0dEYbKwTIYAAAAJcEhZcwAAFiUA
ABYlAUlSJPAAAAAHdElNRQfpCBUKLDZRNHdnAAAHQ0lEQVR42u2Z+X/TNhTABW1hSm3HMV7b2FlT
nIY27QhJuccOWLuDneweY9P2//8V03t6uhyXuUkIsI/fDxDL0tPTV++QXMYaaaSRRurKnxXC2F8V
8rotfYulgbwCaSCvQBrIK5AG8gqkgbwCaSCvQBrIK5AG8gqkgTyfXOKt+p0XgrwZBMEF5qopIY/a
7biTLK5Jmhdw83RNPoV+B57KmdI5Zrr8rhBbYd3ei0BOhJTt2lPVlJ2uUNJeFDNH+8xjJJ8y930r
p5l6F57pvV0h+rxu70Ug76GF15eKOCyEkXzBKBmgFoOiDHnfzrQ9vKDqy1LZjaxu70UgK0colgr5
AFR2e73DudZeZV98DuQRvDyKowF0yy8aj2uj6Lh25wUgv09usMx8Ad7V7cCvrJhn7a5sgi4nn5Ug
y4AXI/w1WHo8Lg8y2DZy7Eu4FUhyQ87JE92GMIgQIgtbadQqJUPI8l0NAijftMN5mrpJUOqxw3WP
qBX69u05+cKHPJZPt+jnRIgpNQ+DyEwTSrUhaE3AdN2KP9cT07ARyh5OfZUHjzTyTF0EsgyzEzax
+SKyOU5E6r1aRWYbpon8J4dGqm+FhznSXHHYRJUtGB4kGPt5R+9G4RVHp8dtl+Kdcck+Cxm0arIS
mIp9rtTmO+oJdghiK2A9Mxb84C67d18WPny+TAvZItOupP7zYpA5OnFsLa8FGROgdJKDyppTeBxg
ZUM1/LY+CCiGiX4UeUKQb+omXS0hyDqgUeeLUrqALSwFUsfY39YrnAqEzE3EytjoP7CQH5pC3UfT
4GxHzztLgDxAm7n1vVGhZEIhWgVZ7vJh8QFjj6DopMFg4p8BbQiDBLg+HJ5DiUKMCQWRmAbBQNdd
+f+BOJr2aAvNhoWo43olZKTTdsMak1UUpLu0mXgEFN2ieMBsxMphW8xA/hAW0o1TsASOdGsfgQVp
kObuEW9+yJgtcPbca9/X2KsgizzTy8E+kBJMfvBCmKnKGqlW1Smmw8JI16wR7afukeXK+WmKOyrf
FpWQs5wYmstIoc2L1dZzYT0opgqvsoWGvP4xJAY55tIn8sdjtvFEKv30VL44kz8+Y4vKJkajmt11
h2twuNe7MAs501GQm5XbPdId/cfMuOeYsnRu9qJQ1E2PPeX8TGcL7dAVkFk20JGtCkNCI9Q8AaUL
6qzzxUh5KEH+PNYO+0UPPBwcuY9zAPX+wueuAZnM3aOoypbKHSohn6heMiCLDOVL9wyY+bocT55a
f8qwpN1WwwdKpenBDWQN1+aLMmTZe6ATeYfcZqjU7qIdVhkz+QJZGsiQkZ86CgH6llLxFeZuJX9X
CGP/VIjP2J6AJu7V1fHWSsiUcYUnQ1epky64hUxpNcCw4d7ovBJyoptsvpiFDLaNeqiF+4UbB3Ph
3xdlChljVjCQv5ZL+sbR9rBwVfT1Ds0LeeRZZEx5ZIHUh2yzjZ/gIfYf0BHuopD3vD7ZuZAZpY3i
vyB/iw+blA4I8neTVwrZ02ZifN+N95dAlrZ9HxjxD23cmyPzMrW6W0Aaic3oViXk3LPv5ssgY/ba
Vu+tUbwEWaV/uR/PmO/Jz0qQ89mFzQk58X2RjmFQ9PJTB/KJWbkPeRcr/6xsmqpJwXKL9qiwmzDE
mPWvwWXIV3/w7TuZhXzfKQawmypMvE3wIUO+gCV0HMiYk1GLugJCTn7KZmROyLp2g5jzhVP0iKQ6
1Q5mIMdmX44D7txGxuCAdIjHC0eHIG+b+p8rzUR91OJhCTIE6b2BLTsYwnwG8p4wx7Nkgtswti0p
qvUhw+sdrYIgw8e4fkfP8oyt/ahPG+tJahc2J2T3asa1px045An+QcJCyNMlyFy9on0pOY+8IYRh
hrdVJIfnZOiN18Qp4drR3acOZDw4BSqMtUe98xPB8yEDM7EDNvCchsPtEO+L+2ohPmTKkGoFBHnj
idyfp0O66N1Vz1tS6cbPv+jD3NyQE+Fezeja9siJz1saGMjhDGTcgLwdtbv+vpSqj0o96sbXLXrQ
WUUKBEkeR4UuarhCCxmvBndJIy58ewYyzZTn+KkB1cLNSBRRXFDAlCCrWtpxIbMrv0Jb0Ybth+/L
eOMTvSgGrY8X8+Q9j02kcmRRgowkBXxWm4Ws3wn3wofym1VypNI7DiflXfUhJzN17Qbc/H4vQXaz
hQKij2huzrUmiFypPZ6YBn0FcCCP8W3oQcaEQZa0/Gfx3FSn+SDvevZSvpiBzPBrw1HGKiCz0aEC
OfM3nIxuCOYNQh7j9exIfygfqw8Z3edoRRmymy3oyhtXnC643rno1MztNpQg4wKnduPUV7g1Wshz
Un31D/qS55TmeY9wNeX4+PTcd5msx9Vvjzl3xlEgnPq9r77g/MX5ymsKfPvOyi21/6ykZD3zLdk4
Let4xZCXIaUvGm+hNJBXIA3kFcibD3n9rIH8ymX9rCiK0evmtJC8+ZD/B9JAXoE0kFcgDeQVSAO5
kUYaaeQNkX8BdcAtjNRFbWoAAAAldEVYdGRhdGU6Y3JlYXRlADIwMjUtMDgtMjFUMTA6NDQ6NTMr
MDA6MDBp1auWAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDI1LTA4LTIxVDEwOjQ0OjUzKzAwOjAwGIgT
KgAAACh0RVh0ZGF0ZTp0aW1lc3RhbXAAMjAyNS0wOC0yMVQxMDo0NDo1NCswMDowMIo6DHsAAAAA
SUVORK5CYII=" />
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

@@ -0,0 +1,8 @@
<svg width="21" height="22" viewBox="0 0 21 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Microsoft">
<rect id="Rectangle 1010" y="0.5" width="10" height="10" fill="#EF4F21"/>
<rect id="Rectangle 1012" y="11.5" width="10" height="10" fill="#03A4EE"/>
<rect id="Rectangle 1011" x="11" y="0.5" width="10" height="10" fill="#7EB903"/>
<rect id="Rectangle 1013" x="11" y="11.5" width="10" height="10" fill="#FBB604"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 439 B

@@ -0,0 +1,40 @@
provider: azure_openai
label:
en_US: Azure OpenAI
icon_small:
en_US: icon_s_en.svg
icon_large:
en_US: icon_l_en.svg
background: "#E3F0FF"
help:
title:
en_US: Get your API key from Azure
zh_Hans: 从 Azure 获取 API Key
url:
en_US: https://azure.microsoft.com/en-us/products/ai-services/openai-service
supported_model_types:
- llm
configurate_methods:
- customizable-model
provider_credential_schema:
credential_form_schemas:
- variable: api_key
label:
en_US: API Key
type: secret-input
required: true
placeholder:
zh_Hans: 在此输入您的 API Key
en_US: Enter your API Key
- variable: base_url
type: text-input
required: true
placeholder:
zh_Hans: 在此输入您的 Base URL
en_US: Enter your Base URL
- variable: api_version
label:
en_US: '2024-02-01'
type: text-input
required: true
address: https://docs-test-001.openai.azure.com/
+77
View File
@@ -0,0 +1,77 @@
package common
import (
"fmt"
"strconv"
)
func FormatCountInt64(count int64) string {
switch {
case count < 1000:
return strconv.FormatInt(count, 10)
case count < 1000000:
return fmt.Sprintf("%.1fK", float64(count)/1000)
case count < 1000000000:
return fmt.Sprintf("%.1fM", float64(count)/1000000)
case count < 1000000000000:
return fmt.Sprintf("%.1fB", float64(count)/1000000000)
default:
return fmt.Sprintf("%.1fT", float64(count)/1000000000000)
}
}
func FormatCountFloat64(count float64) string {
switch {
case count < 1000:
return fmt.Sprintf("%.1f", count)
case count < 1000000:
return fmt.Sprintf("%.1fK", count/1000)
case count < 1000000000:
return fmt.Sprintf("%.1fM", count/1000000)
case count < 1000000000000:
return fmt.Sprintf("%.1fB", count/1000000000)
default:
return fmt.Sprintf("%.1fT", count/1000000000000)
}
}
func FormatTime(t int64) string {
if t < 1000 {
return strconv.FormatInt(t, 10) + "ms"
}
if t < 1000000 {
return fmt.Sprintf("%.1fs", float64(t)/1000)
}
if t < 1000000000 {
return fmt.Sprintf("%.1fmin", float64(t)/1000000)
}
if t < 1000000000000 {
return fmt.Sprintf("%.1fhour", float64(t)/1000000000)
}
return fmt.Sprintf("%.1D", float64(t)/1000000000000)
}
func FormatByte(b int64) string {
const (
KB = 1000
MB = KB * 1000
GB = MB * 1000
TB = GB * 1000
PB = TB * 1000
)
switch {
case b < KB:
return fmt.Sprintf("%dB", b)
case b < MB:
return fmt.Sprintf("%.1fKB", float64(b)/KB)
case b < GB:
return fmt.Sprintf("%.1fMB", float64(b)/MB)
case b < TB:
return fmt.Sprintf("%.1fGB", float64(b)/GB)
case b < PB:
return fmt.Sprintf("%.1fTB", float64(b)/TB)
default:
return fmt.Sprintf("%.1fPB", float64(b)/PB)
}
}
+2
View File
@@ -29,6 +29,8 @@ func FmtIntFromInterface(val interface{}) int64 {
return int64(ret)
case int:
return int64(ret)
case float64:
return int64(ret)
default:
return 0
}
+124 -155
View File
@@ -6,6 +6,8 @@ import (
"strings"
"sync"
"github.com/APIParkLab/APIPark/module/service"
application_authorization "github.com/APIParkLab/APIPark/module/application-authorization"
mcp_server "github.com/APIParkLab/APIPark/mcp-server"
@@ -13,7 +15,6 @@ import (
"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"
)
@@ -22,12 +23,80 @@ var _ IMcpController = (*imlMcpController)(nil)
type imlMcpController struct {
settingModule system.ISettingModule `autowired:""`
authorizationModule application_authorization.IAuthorizationModule `autowired:""`
appModule service.IAppModule `autowired:""`
mcpModule mcp.IMcpModule `autowired:""`
sessionKeys sync.Map
server map[string]http.Handler
openServer http.Handler
}
func (i *imlMcpController) AppMCPHandle(ctx *gin.Context) {
appId := ctx.Param("app")
if appId == "" {
ctx.AbortWithStatusJSON(403, gin.H{"code": -1, "msg": "invalid app id", "success": "fail"})
return
}
cfg := i.settingModule.Get(ctx)
req := ctx.Request.WithContext(utils.SetGatewayInvoke(ctx.Request.Context(), cfg.InvokeAddress))
req = req.WithContext(utils.SetLabel(req.Context(), "app", appId))
paths := strings.Split(req.URL.Path, "/")
req.URL.Path = fmt.Sprintf("/api/v1/%s/%s", mcp_server.GlobalBasePath, paths[len(paths)-1])
locale := utils.I18n(ctx)
if v, ok := i.server[locale]; ok {
v.ServeHTTP(ctx.Writer, req)
return
}
i.server[languageEnUs].ServeHTTP(ctx.Writer, req)
}
func (i *imlMcpController) AppHandleSSE(ctx *gin.Context) {
apikey := ctx.Request.URL.Query().Get("apikey")
appId := ctx.Param("app")
if appId == "" {
ctx.AbortWithStatusJSON(403, gin.H{"code": -1, "msg": "invalid app id", "success": "fail"})
return
}
ok, err := i.authorizationModule.CheckAPIKeyAuthorizationByApp(ctx, appId, 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
}
ctx.Request.URL.Path = fmt.Sprintf("/openapi/v1/%s/sse", mcp_server.GlobalBasePath)
i.handleSSE(ctx, i.openServer, SessionInfo{
Apikey: apikey,
App: appId,
})
}
func (i *imlMcpController) AppHandleMessage(ctx *gin.Context) {
appId := ctx.Param("app")
if appId == "" {
ctx.AbortWithStatusJSON(403, gin.H{"code": -1, "msg": "invalid app id", "success": "fail"})
return
}
ctx.Request.URL.Path = fmt.Sprintf("/openapi/v1/%s/message", mcp_server.GlobalBasePath)
ctx.Request = ctx.Request.WithContext(utils.SetLabel(ctx.Request.Context(), "app", appId))
i.handleMessage(ctx, i.openServer)
}
func (i *imlMcpController) AppMCPConfig(ctx *gin.Context, appId string) (string, error) {
cfg := i.settingModule.Get(ctx)
if cfg.SitePrefix == "" {
return "", fmt.Errorf("site prefix is empty")
}
appInfo, err := i.appModule.GetApp(ctx, appId)
if err != nil {
return "", fmt.Errorf("get app info error: %v", err)
}
return fmt.Sprintf(mcpDefaultConfig, appInfo.Name, fmt.Sprintf("%s/openapi/v1/mcp/app/%s/sse?apikey={your_api_key}", strings.TrimSuffix(cfg.SitePrefix, "/"), appId)), nil
}
var mcpDefaultConfig = `{
"mcpServers": {
"%s": {
@@ -45,151 +114,18 @@ func (i *imlMcpController) GlobalMCPConfig(ctx *gin.Context) (string, error) {
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, "/"))))
for language, tools := range mcpToolsByLanguage {
s := server.NewMCPServer("APIPark MCP Server", "1.0.0", server.WithLogging())
s.AddTool(tools[ToolServiceList], i.mcpModule.Services)
s.AddTool(tools[ToolOpenAPIDocument], i.mcpModule.APIs)
s.AddTool(tools[ToolInvokeAPI], i.mcpModule.Invoke)
i.server[language] = server.NewSSEServer(s, server.WithStaticBasePath(fmt.Sprintf("/api/v1/%s", mcp_server.GlobalBasePath)))
if language == languageEnUs {
i.openServer = server.NewSSEServer(s, server.WithStaticBasePath(fmt.Sprintf("/openapi/v1/%s", strings.Trim(mcp_server.GlobalBasePath, "/"))))
}
}
}
func (i *imlMcpController) GlobalMCPHandle(ctx *gin.Context) {
@@ -200,15 +136,17 @@ func (i *imlMcpController) GlobalMCPHandle(ctx *gin.Context) {
v.ServeHTTP(ctx.Writer, req)
return
}
i.server["en-US"].ServeHTTP(ctx.Writer, req)
i.server[languageEnUs].ServeHTTP(ctx.Writer, req)
}
func (i *imlMcpController) GlobalHandleSSE(ctx *gin.Context) {
apikey := ctx.Request.URL.Query().Get("apikey")
i.handleSSE(ctx, i.openServer, apikey)
i.handleSSE(ctx, i.openServer, SessionInfo{
Apikey: apikey,
})
}
func (i *imlMcpController) handleSSE(ctx *gin.Context, server http.Handler, apikey string) {
func (i *imlMcpController) handleSSE(ctx *gin.Context, server http.Handler, sIn SessionInfo) {
writer := &ResponseWriter{
Writer: ctx.Writer,
@@ -222,7 +160,7 @@ func (i *imlMcpController) handleSSE(ctx *gin.Context, server http.Handler, apik
if !ok {
return
}
i.sessionKeys.Store(sessionId, apikey)
i.sessionKeys.Store(sessionId, sIn)
}()
server.ServeHTTP(writer, ctx.Request)
i.sessionKeys.Delete(sessionId)
@@ -246,7 +184,7 @@ func (i *imlMcpController) ServiceHandleSSE(ctx *gin.Context) {
ctx.AbortWithStatusJSON(403, gin.H{"code": -1, "msg": "invalid service id", "success": "fail"})
return
}
ok, err := i.authorizationModule.CheckAPIKeyAuthorization(ctx, serviceId, apikey)
ok, err := i.authorizationModule.CheckAPIKeyAuthorizationByService(ctx, serviceId, apikey)
if err != nil {
ctx.AbortWithStatusJSON(403, gin.H{"code": -1, "msg": err.Error(), "success": "fail"})
return
@@ -256,22 +194,53 @@ func (i *imlMcpController) ServiceHandleSSE(ctx *gin.Context) {
return
}
i.handleSSE(ctx, mcp_server.DefaultMCPServer(), apikey)
i.handleSSE(ctx, mcp_server.DefaultMCPServer(), SessionInfo{
Apikey: 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)
func (i *imlMcpController) ServiceHandleStreamHTTP(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.CheckAPIKeyAuthorizationByService(ctx, serviceId, apikey)
if err != nil {
ctx.AbortWithStatusJSON(403, gin.H{"code": -1, "msg": err.Error(), "success": "fail"})
return
}
if !ok {
ctx.String(403, "sessionId not found")
ctx.AbortWithStatusJSON(403, gin.H{"code": -1, "msg": "invalid apikey", "success": "fail"})
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)))
req = req.WithContext(utils.SetLabel(req.Context(), "apikey", apikey))
mcp_server.DefaultMCPServer().ServeHTTP(ctx.Writer, req)
}
func (i *imlMcpController) handleMessage(ctx *gin.Context, server http.Handler) {
sessionId := ctx.Request.URL.Query().Get("sessionId")
params, ok := i.sessionKeys.Load(sessionId)
if !ok {
ctx.String(403, "sessionId not found")
return
}
ps, ok := params.(SessionInfo)
cfg := i.settingModule.Get(ctx)
req := ctx.Request.WithContext(utils.SetGatewayInvoke(ctx.Request.Context(), cfg.InvokeAddress))
req = req.WithContext(utils.SetLabel(req.Context(), "apikey", ps.Apikey))
req = req.WithContext(utils.SetLabel(req.Context(), "app", ps.App))
server.ServeHTTP(ctx.Writer, req)
}
type SessionInfo struct {
Apikey string
App string
}
+10 -1
View File
@@ -9,12 +9,21 @@ import (
type IMcpController interface {
MCPHandle(ctx *gin.Context)
GlobalMCPHandle(ctx *gin.Context)
GlobalHandleSSE(ctx *gin.Context)
GlobalHandleMessage(ctx *gin.Context)
GlobalMCPConfig(ctx *gin.Context) (string, error)
AppMCPHandle(ctx *gin.Context)
AppHandleSSE(ctx *gin.Context)
AppHandleMessage(ctx *gin.Context)
AppMCPConfig(ctx *gin.Context, appId string) (string, error)
ServiceHandleSSE(ctx *gin.Context)
ServiceHandleMessage(ctx *gin.Context)
GlobalMCPConfig(ctx *gin.Context) (string, error)
ServiceHandleStreamHTTP(ctx *gin.Context)
}
func init() {
+104
View File
@@ -0,0 +1,104 @@
package mcp
import "github.com/mark3labs/mcp-go/mcp"
const (
ToolServiceList = "service_list"
ToolOpenAPIDocument = "openapi_document"
ToolInvokeAPI = "invoke_api"
languageZhCN = "zh-CN"
languageZhTW = "zh-TW"
languageEnUs = "en-US"
languageJaJp = "ja-JP"
)
var mcpToolsByLanguage = map[string]map[string]mcp.Tool{
languageZhCN: toolsZhCN,
languageZhTW: toolsZhTW,
languageEnUs: toolsEnUs,
languageJaJp: toolsJaJp,
}
var toolsZhTW = map[string]mcp.Tool{
ToolServiceList: mcp.NewTool(
ToolServiceList,
mcp.WithDescription("此工具用於獲取 APIPark 中已註冊服務的列表。每個服務包含其唯一標識(service ID)、名稱、描述及包含的 API 列表等關鍵信息。支持通過關鍵詞進行模糊搜索,以便快速縮小查找範圍。在獲得某個服務的 ID 後,可以調用 openapi_document 工具來獲取該服務的 OpenAPI 文檔,以便後續調用其提供的 API 接口。"),
mcp.WithString("keyword", mcp.Description("關鍵詞,用於模糊搜索服務"))),
ToolOpenAPIDocument: mcp.NewTool(
ToolOpenAPIDocument,
mcp.WithDescription("此工具用於獲取 APIPark 中服務的 OpenAPI 文檔。"),
mcp.WithString("service_id", mcp.Description("服務 ID"))),
ToolInvokeAPI: mcp.NewTool(
ToolInvokeAPI,
mcp.WithDescription("此工具可直接發送 API 請求。在調用此工具之前,需要根據該 API 的 OpenAPI 文檔構造所需的請求參數,如請求路徑、方法、查詢參數、頭部信息、主體內容等。使用此工具時不需要傳遞任何認證信息,例如 Authorization 頭部可以省略。"),
mcp.WithString("path", mcp.Description("API 的請求路徑"), mcp.Required()),
mcp.WithString("method", mcp.Description("API 的請求方法,例如 GET、POST、PUT"), mcp.Required()),
mcp.WithString("content-type", mcp.Description("請求的 Content-Type。若方法為 POST、PUT 或 PATCH,則必須指定")),
mcp.WithObject("query", mcp.Description("請求的查詢參數,類型為 map[string]string")),
mcp.WithObject("header", mcp.Description("請求的頭部,類型為 map[string]string")),
mcp.WithString("body", mcp.Description("請求主體內容,通常為 JSON 字符串")),
),
}
var toolsZhCN = map[string]mcp.Tool{
ToolServiceList: mcp.NewTool(
ToolServiceList,
mcp.WithDescription("此工具用于获取 APIPark 中已注册服务的列表。每个服务包含其唯一标识(service ID)、名称、描述及包含的 API 列表等关键信息。支持通过关键词进行模糊搜索,以便快速缩小查找范围。在获得某个服务的 ID 后,可以调用 openapi_document 工具来获取该服务的 OpenAPI 文档,以便后续调用其提供的 API 接口。"),
mcp.WithString("keyword", mcp.Description("关键词,用于模糊搜索服务"))),
ToolOpenAPIDocument: mcp.NewTool(
ToolOpenAPIDocument, mcp.WithDescription("此工具用于查询指定服务的 OpenAPI 文档。返回的格式支持 OpenAPI v3 和 v2 标准。通过输入服务 ID,可以查看该服务所有 API 的定义、参数结构、请求方式等详细信息,为后续构造 API 调用请求做准备。"),
mcp.WithString("service", mcp.Description("欲查询的服务唯一标识")),
),
ToolInvokeAPI: mcp.NewTool(ToolInvokeAPI,
mcp.WithDescription("此工具可直接发送 API 请求。在调用此工具之前,需要根据该 API 的 OpenAPI 文档构造所需的请求参数,如请求路径、方法、查询参数、头部信息、主体内容等。使用此工具时不需要传递任何认证信息,例如 Authorization 头部可以省略。"),
mcp.WithString("path", mcp.Description("API 的请求路径"), mcp.Required()),
mcp.WithString("method", mcp.Description("API 的请求方法,例如 GET、POST、PUT"), mcp.Required()),
mcp.WithString("content-type", mcp.Description("请求的 Content-Type。若方法为 POST、PUT 或 PATCH,则必须指定")),
mcp.WithObject("query", mcp.Description("请求的查询参数,类型为 map[string]string")),
mcp.WithObject("header", mcp.Description("请求的头部,类型为 map[string]string")),
mcp.WithString("body", mcp.Description("请求主体内容,通常为 JSON 字符串")),
),
}
var toolsEnUs = map[string]mcp.Tool{
ToolServiceList: mcp.NewTool(
ToolServiceList,
mcp.WithDescription("This tool is used to retrieve a list of registered services in APIPark. Each service includes its unique identifier (service ID), name, description, and a list of APIs it contains. It supports fuzzy searching by keyword for quick narrowing down of results. After obtaining a service ID, you can use the openapi_document tool to get the OpenAPI documentation for that service, which is necessary for invoking its APIs."),
mcp.WithString("keyword", mcp.Description("Keyword for fuzzy search of services"))),
ToolOpenAPIDocument: mcp.NewTool(
ToolOpenAPIDocument,
mcp.WithDescription("This tool is used to query the OpenAPI documentation of a specified service. The returned format supports both OpenAPI v3 and v2 standards. By entering the service ID, you can view detailed information about all APIs of that service, including definitions, parameter structures, request methods, etc., which prepares you for subsequent API calls."),
mcp.WithString("service", mcp.Description("Unique identifier of the service to query"))),
ToolInvokeAPI: mcp.NewTool(
ToolInvokeAPI,
mcp.WithDescription("This tool can directly send API requests. Before using this tool, you need to construct the required request parameters based on the OpenAPI documentation of the API, such as request path, method, query parameters, header information, body content, etc. No authentication information like Authorization header is required when using this tool."),
mcp.WithString("path", mcp.Description("API request path"), mcp.Required()),
mcp.WithString("method", mcp.Description("API request method, e.g., GET, POST, PUT"), mcp.Required()),
mcp.WithString("content-type", mcp.Description("Content-Type of the request. Must be specified if method is POST, PUT, or PATCH")),
mcp.WithObject("query", mcp.Description("Query parameters of the request, type map[string]string")),
mcp.WithObject("header", mcp.Description("Header information of the request, type map[string]string")),
mcp.WithString("body", mcp.Description("Body content of the request, usually in JSON string")),
),
}
var toolsJaJp = map[string]mcp.Tool{
ToolServiceList: mcp.NewTool(
ToolServiceList,
mcp.WithDescription("このツールは、APIParkに登録されているサービスのリストを取得するために使用されます。各サービスには、ユニークな識別子(サービスID)、名前、説明、および含まれるAPIのリストが含まれています。キーワードによるあいまい検索をサポートしており、結果を迅速に絞り込むことができます。サービスIDを取得した後は、openapi_documentツールを使用してそのサービスのOpenAPIドキュメントを取得し、そのAPIを呼び出す準備をします。"),
mcp.WithString("keyword", mcp.Description("サービスをあいまい検索するためのキーワード"))),
ToolOpenAPIDocument: mcp.NewTool(
ToolOpenAPIDocument,
mcp.WithDescription("このツールは、指定されたサービスのOpenAPIドキュメントを照会するために使用されます。返される形式は、OpenAPI v3およびv2標準の両方をサポートしています。サービスIDを入力することで、そのサービスのすべてのAPIに関する詳細情報(定義、パラメータ構造、リクエスト方法など)を表示し、後続のAPI呼び出しの準備をします。"),
mcp.WithString("service", mcp.Description("照会するサービスのユニークな識別子"))),
ToolInvokeAPI: mcp.NewTool(
ToolInvokeAPI,
mcp.WithDescription("このツールは、APIリクエストを直接送信できます。このツールを使用する前に、APIのOpenAPIドキュメントに基づいて、必要なリクエストパラメータ(リクエストパス、メソッド、クエリパラメータ、ヘッダー情報、ボディコンテンツなど)を構築する必要があります。このツールを使用する際には、Authorizationヘッダーなどの認証情報は必要ありません。"),
mcp.WithString("path", mcp.Description("APIのリクエストパス"), mcp.Required()),
mcp.WithString("method", mcp.Description("APIのリクエストメソッド(例:GET、POST、PUT"), mcp.Required()),
mcp.WithString("content-type", mcp.Description("リクエストのContent-Type。メソッドがPOST、PUT、またはPATCHの場合は必須です")),
mcp.WithObject("query", mcp.Description("リクエストのクエリパラメータ、タイプはmap[string]string")),
mcp.WithObject("header", mcp.Description("リクエストのヘッダー、タイプはmap[string]string")),
mcp.WithString("body", mcp.Description("リクエストのボディコンテンツ、通常はJSON文字列")),
),
}
+61
View File
@@ -2,6 +2,7 @@ package monitor
import (
"fmt"
"strconv"
"time"
"github.com/APIParkLab/APIPark/module/monitor"
@@ -17,6 +18,66 @@ type imlMonitorStatisticController struct {
module monitor.IMonitorStatisticModule `autowired:""`
}
func (i *imlMonitorStatisticController) ChartRestOverview(ctx *gin.Context, start string, end string) (*monitor_dto.ChartRestOverview, error) {
s, e, err := formatTime(start, end)
if err != nil {
return nil, err
}
return i.module.RestChartOverview(ctx, "", s, e)
}
func (i *imlMonitorStatisticController) ChartAIOverview(ctx *gin.Context, start string, end string) (*monitor_dto.ChartAIOverview, error) {
s, e, err := formatTime(start, end)
if err != nil {
return nil, err
}
return i.module.AIChartOverview(ctx, "", s, e)
}
func (i *imlMonitorStatisticController) AITopN(ctx *gin.Context, start string, end string, limit string) ([]*monitor_dto.TopN, []*monitor_dto.TopN, error) {
s, e, err := formatTime(start, end)
if err != nil {
return nil, nil, err
}
l, err := strconv.Atoi(limit)
if err != nil {
if limit == "" {
l = 10
} else {
return nil, nil, fmt.Errorf("parse limit %s error: %w", limit, err)
}
}
return i.module.Top(ctx, "", s, e, l, "ai")
}
func formatTime(start string, end string) (int64, int64, error) {
s, err := strconv.ParseInt(start, 10, 64)
if err != nil {
return 0, 0, fmt.Errorf("parse start time %s error: %w", start, err)
}
e, err := strconv.ParseInt(end, 10, 64)
if err != nil {
return 0, 0, fmt.Errorf("parse end time %s error: %w", end, err)
}
return s, e, nil
}
func (i *imlMonitorStatisticController) RestTopN(ctx *gin.Context, start string, end string, limit string) ([]*monitor_dto.TopN, []*monitor_dto.TopN, error) {
s, e, err := formatTime(start, end)
if err != nil {
return nil, nil, err
}
l, err := strconv.Atoi(limit)
if err != nil {
if limit == "" {
l = 10
} else {
return nil, nil, fmt.Errorf("parse limit %s error: %w", limit, err)
}
}
return i.module.Top(ctx, "", s, e, l, "rest")
}
func (i *imlMonitorStatisticController) Statistics(ctx *gin.Context, dataType string, input *monitor_dto.StatisticInput) (interface{}, error) {
switch dataType {
case monitor_dto.DataTypeApi:
+5
View File
@@ -22,6 +22,11 @@ type IMonitorStatisticController interface {
InvokeTrendInner(ctx *gin.Context, dataType string, typ string, api string, provider string, subscriber string, input *monitor_dto.CommonInput) (*monitor_dto.MonInvokeCountTrend, string, error)
StatisticsInner(ctx *gin.Context, dataType string, typ string, id string, input *monitor_dto.StatisticInput) (interface{}, error)
ChartRestOverview(ctx *gin.Context, start string, end string) (*monitor_dto.ChartRestOverview, error)
ChartAIOverview(ctx *gin.Context, start string, end string) (*monitor_dto.ChartAIOverview, error)
AITopN(ctx *gin.Context, start string, end string, limit string) ([]*monitor_dto.TopN, []*monitor_dto.TopN, error)
RestTopN(ctx *gin.Context, start string, end string, limit string) ([]*monitor_dto.TopN, []*monitor_dto.TopN, error)
}
type IMonitorConfigController interface {
+208 -18
View File
@@ -5,9 +5,13 @@ import (
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"
"github.com/APIParkLab/APIPark/module/monitor"
monitor_dto "github.com/APIParkLab/APIPark/module/monitor/dto"
ai_provider_local "github.com/APIParkLab/APIPark/ai-provider/local"
subscribe_dto "github.com/APIParkLab/APIPark/module/subscribe/dto"
@@ -55,10 +59,6 @@ import (
"github.com/google/uuid"
)
//var (
// ollamaConfig = "{\n \"mirostat\": 0,\n \"mirostat_eta\": 0.1,\n \"mirostat_tau\": 5.0,\n \"num_ctx\": 4096,\n \"repeat_last_n\":64,\n \"repeat_penalty\": 1.1,\n \"temperature\": 0.7,\n \"seed\": 42,\n \"num_predict\": 42,\n \"top_k\": 40,\n \"top_p\": 0.9,\n \"min_p\": 0.5\n}\n"
//)
var (
_ IServiceController = (*imlServiceController)(nil)
@@ -66,20 +66,210 @@ var (
)
type imlServiceController struct {
module service.IServiceModule `autowired:""`
docModule service.IServiceDocModule `autowired:""`
subscribeModule subscribe.ISubscribeModule `autowired:""`
aiAPIModule ai_api.IAPIModule `autowired:""`
routerModule router.IRouterModule `autowired:""`
apiDocModule api_doc.IAPIDocModule `autowired:""`
providerModule ai.IProviderModule `autowired:""`
aiLocalModel ai_local.ILocalModelModule `autowired:""`
appModule service.IAppModule `autowired:""`
upstreamModule upstream.IUpstreamModule `autowired:""`
settingModule system.ISettingModule `autowired:""`
teamModule team.ITeamModule `autowired:""`
catalogueModule catalogue.ICatalogueModule `autowired:""`
transaction store.ITransaction `autowired:""`
module service.IServiceModule `autowired:""`
docModule service.IServiceDocModule `autowired:""`
subscribeModule subscribe.ISubscribeModule `autowired:""`
aiAPIModule ai_api.IAPIModule `autowired:""`
routerModule router.IRouterModule `autowired:""`
apiDocModule api_doc.IAPIDocModule `autowired:""`
providerModule ai.IProviderModule `autowired:""`
aiLocalModel ai_local.ILocalModelModule `autowired:""`
appModule service.IAppModule `autowired:""`
upstreamModule upstream.IUpstreamModule `autowired:""`
settingModule system.ISettingModule `autowired:""`
teamModule team.ITeamModule `autowired:""`
catalogueModule catalogue.ICatalogueModule `autowired:""`
monitorModule monitor.IMonitorStatisticModule `autowired:""`
monitorConfigModule monitor.IMonitorConfigModule `autowired:""`
transaction store.ITransaction `autowired:""`
}
func (i *imlServiceController) RestLogInfo(ctx *gin.Context, serviceId string, logId string) (*service_dto.RestLogInfo, error) {
return i.module.RestLogInfo(ctx, serviceId, logId)
}
func (i *imlServiceController) AILogInfo(ctx *gin.Context, serviceId string, logId string) (*service_dto.AILogInfo, error) {
return i.module.AILogInfo(ctx, serviceId, logId)
}
func (i *imlServiceController) AILogs(ctx *gin.Context, serviceId string, start string, end string, page string, size string) ([]*service_dto.AILogItem, int64, error) {
s, e, err := formatTime(start, end)
if err != nil {
return nil, 0, err
}
if serviceId == "" {
return nil, 0, fmt.Errorf("service id is empty")
}
if page == "" {
page = "1"
}
if size == "" {
size = "20"
}
p, err := strconv.Atoi(page)
if err != nil {
return nil, 0, err
}
ps, err := strconv.Atoi(size)
if err != nil {
return nil, 0, err
}
return i.module.AILogs(ctx, serviceId, s, e, p, ps)
}
func (i *imlServiceController) RestLogs(ctx *gin.Context, serviceId string, start string, end string, page string, size string) ([]*service_dto.RestLogItem, int64, error) {
s, e, err := formatTime(start, end)
if err != nil {
return nil, 0, err
}
if serviceId == "" {
return nil, 0, fmt.Errorf("service id is empty")
}
if page == "" {
page = "1"
}
if size == "" {
size = "20"
}
p, err := strconv.Atoi(page)
if err != nil {
return nil, 0, err
}
ps, err := strconv.Atoi(size)
if err != nil {
return nil, 0, err
}
return i.module.RestLogs(ctx, serviceId, s, e, p, ps)
}
func (i *imlServiceController) ServiceOverview(ctx *gin.Context, serviceId string) (*service_dto.Overview, error) {
o, err := i.module.ServiceOverview(ctx, serviceId)
if err != nil {
return nil, err
}
cfg, err := i.monitorConfigModule.GetMonitorConfig(ctx)
if err != nil {
return nil, err
}
if len(cfg.Config) < 1 {
return o, nil
}
statistics, err := i.monitorModule.ProviderStatistics(ctx, &monitor_dto.StatisticInput{
Services: []string{serviceId},
CommonInput: &monitor_dto.CommonInput{
Start: time.Now().Add(-24 * 30 * time.Hour).Unix(),
End: time.Now().Unix(),
},
})
if err != nil {
return nil, err
}
if len(statistics) < 1 {
return o, nil
}
o.InvokeNum = statistics[0].RequestTotal
return o, nil
}
func (i *imlServiceController) AIChartOverview(ctx *gin.Context, serviceId string, start string, end string) (*monitor_dto.ServiceChartAIOverview, error) {
s, e, err := formatTime(start, end)
if err != nil {
return nil, err
}
if serviceId == "" {
return nil, fmt.Errorf("service is required")
}
so, err := i.module.ServiceOverview(ctx, serviceId)
if err != nil {
return nil, err
}
result := &monitor_dto.ServiceChartAIOverview{
EnableMCP: so.EnableMCP,
SubscriberNum: so.SubscriberNum,
APINum: so.APINum,
ServiceKind: so.ServiceKind,
}
cfg, err := i.monitorConfigModule.GetMonitorConfig(ctx)
if err != nil {
return nil, err
}
if len(cfg.Config) < 1 {
return result, nil
}
o, err := i.monitorModule.AIChartOverview(ctx, serviceId, s, e)
if err != nil {
return nil, err
}
result.AvailableMonitor = true
result.ChartAIOverview = o
return result, nil
}
func (i *imlServiceController) RestChartOverview(ctx *gin.Context, serviceId string, start string, end string) (*monitor_dto.ServiceChartRestOverview, error) {
s, e, err := formatTime(start, end)
if err != nil {
return nil, err
}
if serviceId == "" {
return nil, fmt.Errorf("service is required")
}
so, err := i.module.ServiceOverview(ctx, serviceId)
if err != nil {
return nil, err
}
result := &monitor_dto.ServiceChartRestOverview{
EnableMCP: so.EnableMCP,
SubscriberNum: so.SubscriberNum,
APINum: so.APINum,
ServiceKind: so.ServiceKind,
}
cfg, err := i.monitorConfigModule.GetMonitorConfig(ctx)
if err != nil {
return nil, err
}
if len(cfg.Config) < 1 {
return result, nil
}
o, err := i.monitorModule.RestChartOverview(ctx, serviceId, s, e)
if err != nil {
return nil, err
}
result.AvailableMonitor = true
result.ChartRestOverview = o
return result, nil
}
func formatTime(start string, end string) (int64, int64, error) {
s, err := strconv.ParseInt(start, 10, 64)
if err != nil {
return 0, 0, fmt.Errorf("parse start time %s error: %w", start, err)
}
e, err := strconv.ParseInt(end, 10, 64)
if err != nil {
return 0, 0, fmt.Errorf("parse end time %s error: %w", end, err)
}
return s, e, nil
}
func (i *imlServiceController) Top10(ctx *gin.Context, serviceId string, start string, end string) ([]*monitor_dto.TopN, []*monitor_dto.TopN, error) {
if serviceId == "" {
return nil, nil, fmt.Errorf("serviceId is required")
}
info, err := i.module.Get(ctx, serviceId)
if err != nil {
return nil, nil, err
}
s, e, err := formatTime(start, end)
if err != nil {
return nil, nil, err
}
return i.monitorModule.Top(ctx, serviceId, s, e, 10, info.ServiceKind)
}
func (i *imlServiceController) QuickCreateAIService(ctx *gin.Context, input *service_dto.QuickCreateAIService) error {
+15
View File
@@ -3,6 +3,8 @@ package service
import (
"reflect"
monitor_dto "github.com/APIParkLab/APIPark/module/monitor/dto"
service_dto "github.com/APIParkLab/APIPark/module/service/dto"
"github.com/gin-gonic/gin"
@@ -32,6 +34,19 @@ type IServiceController interface {
Swagger(ctx *gin.Context)
ExportSwagger(ctx *gin.Context)
Top10(ctx *gin.Context, serviceId string, start string, end string) ([]*monitor_dto.TopN, []*monitor_dto.TopN, error)
AIChartOverview(ctx *gin.Context, serviceId string, start string, end string) (*monitor_dto.ServiceChartAIOverview, error)
RestChartOverview(ctx *gin.Context, serviceId string, start string, end string) (*monitor_dto.ServiceChartRestOverview, error)
ServiceOverview(ctx *gin.Context, serviceId string) (*service_dto.Overview, error)
AILogs(ctx *gin.Context, serviceId string, start string, end string, page string, size string) ([]*service_dto.AILogItem, int64, error)
RestLogs(ctx *gin.Context, serviceId string, start string, end string, page string, size string) ([]*service_dto.RestLogItem, int64, error)
RestLogInfo(ctx *gin.Context, serviceId string, logId string) (*service_dto.RestLogInfo, error)
AILogInfo(ctx *gin.Context, serviceId string, logId string) (*service_dto.AILogInfo, error)
}
type IAppController interface {
+1 -1
View File
@@ -16,7 +16,7 @@ type IAPIKeyController interface {
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)
MyAPIKeysByService(ctx *gin.Context, serviceId string, appId string) ([]*system_apikey_dto.AuthorizationItem, error)
}
func init() {
+8 -2
View File
@@ -12,8 +12,14 @@ 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) MyAPIKeysByService(ctx *gin.Context, serviceId string, appId string) ([]*system_apikey_dto.AuthorizationItem, error) {
if serviceId != "" {
return i.apikeyModule.MyAPIKeysByService(ctx, serviceId)
}
if appId != "" {
return i.apikeyModule.MyAPIKeysByApp(ctx, appId)
}
return nil, nil
}
func (i *imlAPIKeyController) MyAPIKeys(ctx *gin.Context) ([]*system_apikey_dto.SimpleItem, error) {
+1 -1
View File
@@ -8,7 +8,7 @@
"description": "",
"scripts": {
"test": "jest",
"build": "set NODE_OPTIONS=--max-old-space-size=4096 && lerna run build --scope=core --stream --verbose ",
"build": "set NODE_OPTIONS=--max-old-space-size=8192 && lerna run build --scope=core --stream --verbose ",
"serve": "lerna run preview --parallel",
"serve:remotes": "lerna run serve --scope=remote --parallel",
"dev": "lerna run dev --scope=core --stream",
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

@@ -137,7 +137,7 @@ function BasicLayout({ project = 'core' }: { project: string }) {
const items: MenuProps['items'] = useMemo(
() =>
[
userInfo?.type !== 'guest' && {
!['guest', 'third-user'].includes(userInfo?.type as string) && {
key: '2',
label: (
<Button
@@ -119,6 +119,11 @@ export const PERMISSION_DEFINITION = [
anyOf: [{ backend: ['system.organization.role.manager_team_role'] }]
}
},
'system.organization.auth.view': {
granted: {
anyOf: [{ backend: ['system.settings.login.manager', 'system.settings.login.view'] }]
}
},
'system.api_market.service_classification.view': {
granted: {
anyOf: [{ backend: ['system.settings.general.view'] }]
@@ -634,6 +639,15 @@ export const PERMISSION_DEFINITION = [
]
}
},
'team.consumer.mcp.view': {
granted: {
anyOf: [
{
backend: ['team.consumer.mcp.manager', 'team.consumer.mcp.view']
}
]
}
},
'team.application.authorization.add': {
granted: {
anyOf: [{ backend: ['system.workspace.application.manager_all', 'team.consumer.authorization.manager'] }]
@@ -207,8 +207,15 @@ const mockData = [
name: '角色',
key: 'role',
path: '/role',
icon: 'ic:baseline-verified-user',
icon: 'ph:user-circle-gear-fill',
access: 'system.organization.role.view'
},
{
name: '鉴权',
key: 'auth',
path: '/auth',
icon: 'ic:baseline-verified-user',
access: 'system.organization.auth.view'
}
]
},
@@ -60,6 +60,16 @@ const mockData = {
}
]
},
{
driver: 'apipark.builtIn.component',
name: 'auth',
router: [
{
path: 'auth',
type: 'normal'
}
]
},
{
driver: 'apipark.builtIn.component',
name: 'cluster',
@@ -461,6 +461,17 @@
"待审核": "K35612f29",
"已审核": "K47eaafde",
"发布申请": "K56b4254f",
"鉴权": "Kb35e6a18",
"系统用户账号登录授权配置": "K679bd7e4",
"授权类型": "K9e7bb257",
"请选择授权类型": "Kc499fc1d",
"APP ID": "Kee9f8f26",
"请输入APP ID": "K9e4c19bb",
"APP ID 参数位于飞书开发人员控制台中的应用程序凭证和基础信息页面上": "K45c99f97",
"APP Secret": "K90f7c3b4",
"请输入APP Secret": "Kdc53d96f",
"APP Secret 参数位于飞书开发人员控制台中的应用程序凭证和基础信息页面上": "K56e77c4",
"启用授权": "K50693bd8",
"API 调用地址": "Kea2f9279",
"API base URL 一般设置为API 网关的外部网络访问地址,或者是API网关绑定的域名。": "K7fc496a1",
"OpenAPI & MCP 调用地址": "Ka7ca8fde",
@@ -553,6 +564,7 @@
"请输入密码": "K25c895d5",
"密码": "K551b0348",
"登录": "Kd2c1a316",
"飞书授权登录": "K682b11cb",
"访客模式": "K192b3e38",
"您可通过访客模式查看所有页面和功能,但是无法编辑数据。访客模式仅用于了解产品功能,您可以在正式产品中关闭该功能。": "K91aa4801",
"Version (0)-(1)": "K480045ce",
@@ -564,6 +576,8 @@
"AI 代理集成": "Ke6908f16",
"请先订阅该服务": "K71ed51fa",
"申请": "K4aa9ed2c",
"未配置 API Key": "Kf7b54a1",
"配置": "K2a1422d2",
"选择 API Key": "K1bec8cbe",
"新增 API Key": "Kb0e0aeda",
"API 密钥可用于调用系统级 Open API 和 MCP。": "K9d81999c",
@@ -972,5 +972,19 @@
"K6c267c7b": "Avg Requests per Subscriber",
"K133d4291": "Avg Traffic per Subscriber",
"K37c5f1d0": "Token",
"Kb98264d4": "Avg Token per Subscriber"
"Kb98264d4": "Avg Token per Subscriber",
"Kb35e6a18": "Auth",
"K679bd7e4": "System User Account Login Authorization Settings",
"K9e7bb257": "Authorization Type",
"Kc499fc1d": "Please select an authorization type",
"Kee9f8f26": "APP ID",
"K9e4c19bb": "Please enter APP ID",
"K90f7c3b4": "APP Secret",
"Kdc53d96f": "Please enter APP Secret",
"K50693bd8": "Enable Authorization",
"K682b11cb": "Feishu Authorization Login",
"K45c99f97": "The APP ID parameter can be found on the App Credentials and Basic Information page in the Feishu Developer Console",
"K56e77c4": "The APP Secret parameter can be found on the App Credentials and Basic Information page in the Feishu Developer Console",
"Kf7b54a1": "API Key not configured",
"K2a1422d2": "Configure"
}
@@ -994,5 +994,19 @@
"K6c267c7b": "消費者あたりの平均リクエスト数",
"K133d4291": "消費者あたりの平均ネットワークトラフィック",
"K37c5f1d0": "トークン消費量",
"Kb98264d4": "消費者あたりの平均トークン消費量"
"Kb98264d4": "消費者あたりの平均トークン消費量",
"Kb35e6a18": "認証",
"K679bd7e4": "システムユーザーアカウントログイン認可設定",
"K9e7bb257": "認可タイプ",
"Kc499fc1d": "認可タイプを選択してください",
"Kee9f8f26": "APP ID",
"K9e4c19bb": "APP ID を入力してください",
"K90f7c3b4": "APP Secret",
"Kdc53d96f": "APP Secret を入力してください",
"K50693bd8": "認可を有効化",
"K682b11cb": "Feishu 認証ログイン",
"K45c99f97": "APP ID パラメータは Feishu 開発者コンソールのアプリ認証情報と基本情報ページにあります",
"K56e77c4": "APP Secret パラメータは Feishu 開発者コンソールのアプリ認証情報と基本情報ページにあります",
"Kf7b54a1": "API Key が設定されていません",
"K2a1422d2": "設定"
}
@@ -923,5 +923,19 @@
"K6c267c7b": "平均每消费者的请求次数",
"K133d4291": "平均每消费者的网络流量",
"K37c5f1d0": "Token 消耗",
"Kb98264d4": "平均每消费者的 Token 消耗"
"Kb98264d4": "平均每消费者的 Token 消耗",
"Kb35e6a18": "鉴权",
"K679bd7e4": "系统用户账号登录授权配置",
"K9e7bb257": "授权类型",
"Kc499fc1d": "请选择授权类型",
"Kee9f8f26": "APP ID",
"K9e4c19bb": "请输入APP ID",
"K90f7c3b4": "APP Secret",
"Kdc53d96f": "请输入APP Secret",
"K50693bd8": "启用授权",
"K682b11cb": "飞书授权登录",
"K45c99f97": "APP ID 参数位于飞书开发人员控制台中的应用程序凭证和基础信息页面上",
"K56e77c4": "APP Secret 参数位于飞书开发人员控制台中的应用程序凭证和基础信息页面上",
"Kf7b54a1": "未配置 API Key",
"K2a1422d2": "配置"
}
@@ -994,5 +994,19 @@
"K6c267c7b": "平均每位使用者的請求次數",
"K133d4291": "平均每位使用者的網路流量",
"K37c5f1d0": "Token 消耗",
"Kb98264d4": "平均每位使用者的 Token 消耗"
"Kb98264d4": "平均每位使用者的 Token 消耗",
"Kb35e6a18": "鑑權",
"K679bd7e4": "系統用戶帳號登入授權配置",
"K9e7bb257": "授權類型",
"Kc499fc1d": "請選擇授權類型",
"Kee9f8f26": "APP ID",
"K9e4c19bb": "請輸入 APP ID",
"K90f7c3b4": "APP Secret",
"Kdc53d96f": "請輸入 APP Secret",
"K50693bd8": "啟用授權",
"K682b11cb": "飛書授權登入",
"K45c99f97": "APP ID 參數位於飛書開發人員控制台中的應用程式憑證與基礎資訊頁面",
"K56e77c4": "APP Secret 參數位於飛書開發人員控制台中的應用程式憑證與基礎資訊頁面",
"Kf7b54a1": "未配置 API Key",
"K2a1422d2": "配置"
}
@@ -618,6 +618,16 @@ export const routerMap: Map<string, RouterMapConfig> = new Map([
/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/management/ManagementAppSetting.tsx'
)
)
},
{
path: 'mcp',
key: 'consumerMcp',
lazy: lazy(
() =>
import(
/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/management/mcpContent.tsx'
)
)
}
]
},
@@ -693,6 +703,14 @@ export const routerMap: Map<string, RouterMapConfig> = new Map([
]
}
],
[
'auth',
{
type: 'module',
lazy: lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/auth/Auth.tsx')),
key: 'auth'
}
],
[
'analytics',
{
@@ -19,6 +19,7 @@ export type MemberTableListItem = {
enable:boolean
departmentId:string
roles:EntityItem[]
from: string
};
export type AddToDepartmentProps = {
@@ -40,7 +41,7 @@ export type MemberDropdownModalFieldType = {
export type MemberDropdownModalProps = {
type:'addDep'|'addChild'|'addMember'|'editMember'|'rename'
entity?:(MemberTableListItem & {departmentIds:string[]}) | ({id?:string, departmentIds?:string[],name?:string})
entity?:(MemberTableListItem & {departmentIds:string[]}) | ({id?:string, departmentIds?:string[],name?:string,from?:string})
selectedMemberGroupId?:string
}
+383 -199
View File
@@ -1,221 +1,405 @@
import {FC, useCallback, useEffect, useRef, useState} from "react";
import {App, Button, Divider, Form, FormInstance, Input, Spin, Tooltip} from "antd";
import {useGlobalContext} from "@common/contexts/GlobalStateContext.tsx";
import {useFetch} from "@common/hooks/http.ts";
import {BasicResponse, STATUS_CODE} from "@common/const/const.tsx";
import {useNavigate} from "react-router-dom";
import { FC, useCallback, useEffect, useRef, useState } from 'react'
import { App, Button, Divider, Form, FormInstance, Input, Spin, Tooltip } from 'antd'
import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx'
import { useFetch } from '@common/hooks/http.ts'
import { BasicResponse, STATUS_CODE } from '@common/const/const.tsx'
import { useLocation, useNavigate } from 'react-router-dom'
// import {useCrypto} from "../hooks/crypto.ts";
import Logo from '@common/assets/layout-logo.png'
import { $t } from "@common/locales";
import { Icon } from "@iconify/react/dist/iconify.js";
import LanguageSetting from "@common/components/aoplatform/LanguageSetting";
import { LoadingOutlined } from "@ant-design/icons";
import FeishuLogo from '@common/assets/feishu.png'
import { $t } from '@common/locales'
import { Icon } from '@iconify/react/dist/iconify.js'
import LanguageSetting from '@common/components/aoplatform/LanguageSetting'
import { LoadingOutlined } from '@ant-design/icons'
const Login:FC = ()=> {
const {state, dispatch} = useGlobalContext()
const {fetchData} = useFetch()
const { message } = App.useApp()
const navigate = useNavigate();
const formRef = useRef<FormInstance>(null);
const [loading,setLoading] = useState<boolean>()
const [allowGuest, setAllowGuest] = useState<boolean>(false)
const [spinning,setSpinning] = useState<boolean>(false)
const Login: FC = () => {
const { state, dispatch } = useGlobalContext()
const { fetchData } = useFetch()
const { message } = App.useApp()
const navigate = useNavigate()
const formRef = useRef<FormInstance>(null)
const [loading, setLoading] = useState<boolean>()
const [allowGuest, setAllowGuest] = useState<boolean>(false)
const [spinning, setSpinning] = useState<boolean>(false)
// 是否允许飞书登录
const [allowFeishuLogin, setAllowFeishuLogin] = useState<boolean>(false)
// 飞书登录app_id
const [feishuAppId, setFeishuAppId] = useState<string>()
// 获取 url 参数
const query = new URLSearchParams(useLocation().search)
// 是否是飞书登录
const [isFeishuLogin, setIsFeishuLogin] = useState<boolean>(false)
useEffect(() => {
if (isFeishuLogin) {
const callbackUrl = new URLSearchParams(window.location.search).get('callbackUrl')
if (callbackUrl && callbackUrl !== 'null') {
navigate(callbackUrl)
} else {
navigate(state.mainPage)
}
setIsFeishuLogin(false)
}
}, [isFeishuLogin])
/**
* 飞书登录
* @param feishuCode 飞书 code
*/
const feishuLogin = async (feishuCode: string) => {
try {
setLoading(true)
const feishuCallbackUrl = localStorage.getItem('feishuCallbackUrl')
const { code, msg } = await fetchData<BasicResponse<null>>('account/login/feishu', {
method: 'POST',
eoBody: {
code: feishuCode,
redirect_uri: feishuCallbackUrl
}
})
const check = useCallback(()=>{
state.isAuthenticated &&setSpinning(true)
fetchData<BasicResponse<{channel:Array<{name:string}>, status:string}>>('account/login',{method:'GET'}).then(response=>{
const {code,data} = response
if(code === STATUS_CODE.SUCCESS && data.status !== 'anonymous'){
dispatch({type:'LOGIN'})
navigate(state.mainPage,{replace:true})
}else{
dispatch({type:'LOGOUT'})
setAllowGuest(data.channel.filter(x=>x.name === 'guest_access').length > 0)
setSpinning(false)
}
})
},[])
if (code === STATUS_CODE.SUCCESS) {
dispatch({ type: 'LOGIN' })
setIsFeishuLogin(true)
} else {
dispatch({ type: 'LOGOUT' })
setIsFeishuLogin(false)
message.error(msg)
}
} catch (err) {
console.warn(err)
} finally {
setLoading(false)
}
}
const check = useCallback(() => {
state.isAuthenticated && setSpinning(true)
fetchData<BasicResponse<{ channel: Array<{ name: string; config: { [key: string]: any } }>; status: string }>>(
'account/login',
{ method: 'GET' }
).then((response) => {
const { code, data } = response
if (code === STATUS_CODE.SUCCESS && data.status !== 'anonymous') {
dispatch({ type: 'LOGIN' })
navigate(state.mainPage, { replace: true })
} else {
dispatch({ type: 'LOGOUT' })
setAllowGuest(data.channel.filter((x: any) => x.name === 'guest_access').length > 0)
const feishu = data.channel.find((x: any) => x.name === 'feishu')
if (feishu) {
setFeishuAppId(feishu.config.client_id)
setAllowFeishuLogin(true)
}
const code = query.get('code')
if (code) {
feishuLogin(code)
setSpinning(false)
return
}
if (isInFeishuClient() && feishu) {
openFeishuLogin(feishu.config.client_id)
}
setSpinning(false)
}
})
}, [])
const getSystemInfo = useCallback(() => {
fetchData<BasicResponse<{ version: string; buildTime: string }>>('common/version', {
method: 'GET',
eoTransformKeys: ['build_time']
}).then((response) => {
const { code, data } = response
if (code === STATUS_CODE.SUCCESS) {
dispatch({ type: 'UPDATE_VERSION', version: data.version })
dispatch({ type: 'UPDATE_DATE', updateDate: data.buildTime })
}
})
}, [])
const fetchLogin = async (values: any) => {
try {
setLoading(true)
const { username, password } = values
// const encryptedPassword = encryptByEnAES(username, password);
const body = {
name: username,
password: password
// client: 1,
// type: 1,
// app_type: 4,
}
const { code, msg } = await fetchData<BasicResponse<null>>('account/login/username', {
method: 'POST',
eoBody: body
})
if (code === STATUS_CODE.SUCCESS) {
dispatch({ type: 'LOGIN' })
// message.success($t(RESPONSE_TIPS.loginSuccess));
const callbackUrl = new URLSearchParams(window.location.search).get('callbackUrl')
if (callbackUrl && callbackUrl !== 'null') {
navigate(callbackUrl)
} else {
navigate(state.mainPage)
}
} else {
dispatch({ type: 'LOGOUT' })
message.error(msg)
}
} catch (err) {
console.warn(err)
} finally {
setLoading(false)
}
}
const login = async () => {
if (formRef.current) {
const values = await formRef.current.validateFields()
fetchLogin(values)
}
}
const loginAsGuest = () => {
fetchLogin({ username: 'guest', password: '12345678' })
}
const isInFeishuClient = () => {
// 方法1:检查User-Agent
const ua = navigator.userAgent.toLowerCase();
const isLark = ua.includes('lark') || ua.includes('feishu');
const getSystemInfo = useCallback(()=>{
fetchData<BasicResponse<{version:string, buildTime:string}>>('common/version',{method:'GET', eoTransformKeys:['build_time']}).then(response=>{
const {code,data} = response
if(code === STATUS_CODE.SUCCESS){
dispatch({type:'UPDATE_VERSION',version:data.version})
dispatch({type:'UPDATE_DATE',updateDate:data.buildTime})
}
})
},[])
// 方法2:检查全局对象
const hasSDK = typeof window.h5sdk !== 'undefined' || typeof window.tt !== 'undefined';
// 方法3:检查URL参数
const params = new URLSearchParams(window.location.search);
const hasFeishuParams = params.has('from') || params.has('required_launch_ability');
return isLark || hasSDK || hasFeishuParams;
}
// 打开飞书授权页面
const openFeishuLogin = (id?: string) => {
const href = window.location.origin + window.location.pathname
const authUrl = `https://accounts.feishu.cn/open-apis/authen/v1/authorize?client_id=${id || feishuAppId}&redirect_uri=${href}`
localStorage.setItem('feishuCallbackUrl', href)
window.location.href = authUrl
}
const fetchLogin = async (values:any)=>{
try {
setLoading(true);
const { username, password } = values;
// const encryptedPassword = encryptByEnAES(username, password);
useEffect(() => {
check()
getSystemInfo()
}, [])
const body = {
name:username,
password: password
// client: 1,
// type: 1,
// app_type: 4,
};
return spinning ? (
<Spin
indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />}
spinning={spinning}
className="w-full h-full flex items-center justify-center"
></Spin>
) : (
<div className="h-full w-full flex flex-col items-center overflow-auto min-h-[490px] bg-[#0d1117]">
<div id="glow-background" className="background-container">
<svg className="background-pattern" aria-hidden="true">
<defs>
<pattern id="pattern-bg" width="200" height="200" patternUnits="userSpaceOnUse">
<path d="M.5 200V.5H200" fill="none"></path>
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#pattern-bg)"></rect>
</svg>
const {code,msg } = await fetchData<BasicResponse<null>>('account/login/username',{method:'POST',eoBody:(body)})
<svg
xmlns="http://www.w3.org/2000/svg"
version="1.1"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:svgjs="http://svgjs.dev/svgjs"
viewBox="0 0 800 450"
opacity="1"
>
<defs>
<filter
id="bbblurry-filter"
x="-100%"
y="-100%"
width="400%"
height="400%"
filterUnits="objectBoundingBox"
primitiveUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
>
<feGaussianBlur
stdDeviation="99"
x="0%"
y="0%"
width="100%"
height="100%"
in="SourceGraphic"
edgeMode="none"
result="blur"
></feGaussianBlur>
</filter>
</defs>
<g filter="url(#bbblurry-filter)">
<ellipse
rx="80.5"
ry="66.5"
cx="623.0285107902043"
cy="25.708028895006635"
fill="hsla(187, 67%, 50%, 1.00)"
>
<animate
attributeName="fill"
values="hsla(187, 67%, 50%, 1.00); hsla(340, 85%, 60%, 1.00); hsla(60, 90%, 55%, 1.00); hsla(187, 67%, 50%, 1.00)"
dur="6s"
repeatCount="indefinite"
></animate>
</ellipse>
if (code === STATUS_CODE.SUCCESS) {
dispatch({type:'LOGIN'})
// message.success($t(RESPONSE_TIPS.loginSuccess));
const callbackUrl = new URLSearchParams(window.location.search).get('callbackUrl');
if (callbackUrl && callbackUrl !== 'null') {
navigate(callbackUrl);
} else {
navigate(state.mainPage);
}
}else{
dispatch({type:'LOGOUT'})
message.error(msg)
}
<ellipse
rx="80.5"
ry="66.5"
cx="446.471435546875"
cy="-11.694503784179688"
fill="hsla(234, 78%, 61%, 1.00)"
>
<animate
attributeName="fill"
values="hsla(234, 78%, 61%, 1.00); hsla(100, 75%, 60%, 1.00); hsla(290, 80%, 70%, 1.00); hsla(234, 78%, 61%, 1.00)"
dur="8s"
repeatCount="indefinite"
></animate>
</ellipse>
} catch (err) {
console.warn(err);
} finally {
setLoading(false)
}
}
const login = async () => {
if (formRef.current) {
const values = await formRef.current.validateFields();
fetchLogin(values);
}
};
<ellipse
rx="80.5"
ry="66.5"
cx="200.54574247724838"
cy="-19.02454901710908"
fill="hsla(167, 87%, 56%, 1.00)"
>
<animate
attributeName="fill"
values="hsla(167, 87%, 56%, 1.00); hsla(10, 90%, 65%, 1.00); hsla(300, 85%, 50%, 1.00); hsla(167, 87%, 56%, 1.00)"
dur="10s"
repeatCount="indefinite"
></animate>
</ellipse>
const loginAsGuest = ()=>{
fetchLogin({username:'guest',password:'12345678'})
}
<ellipse rx="80.5" ry="66.5" cx="340.05827594708103" cy="-9.424536458161867" fill="hsl(25, 100%, 64%)">
<animate
attributeName="fill"
values="hsl(25, 100%, 64%); hsl(200, 100%, 70%); hsl(50, 95%, 55%); hsl(25, 100%, 64%)"
dur="8s"
repeatCount="indefinite"
></animate>
</ellipse>
</g>
</svg>
</div>
{/* <div className="w-full border-box text-right pr-[40px]"></div> */}
<div className="mx-auto flex-1 flex flex-col items-center justify-center z-[3]">
<div className="mx-auto">
<span className="flex items-center justify-center">
<img className="h-[40px] mr-[8px]" src={Logo} />
</span>
</div>
useEffect(() => {
check()
getSystemInfo()
}, []);
<section className="block w-[410px] mx-auto mt-[46px] p-[30px] box-border rounded-[10px] shadow-[0_5px_20px_0_rgba(0,0,0,5%)] login-block">
<div className="h-full">
<div className="">
<Form onFinish={login} className="w-[350px]" ref={formRef}>
<Form.Item
className="p-0 bg-transparent rounded border-none"
name="username"
rules={[{ required: true, message: $t('请输入账号'), whitespace: true }]}
>
<Input
className="w-[350px] h-[40px] login-input"
placeholder={$t('账号')}
autoComplete="on"
autoFocus
/>
</Form.Item>
return (
spinning?
<Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} spinning={spinning} className='w-full h-full flex items-center justify-center'></Spin> :
<div className="h-full w-full flex flex-col items-center overflow-auto min-h-[490px] bg-[#0d1117]">
<div id="glow-background" className="background-container">
<svg className="background-pattern" aria-hidden="true">
<defs>
<pattern id="pattern-bg" width="200" height="200" patternUnits="userSpaceOnUse">
<path d="M.5 200V.5H200" fill="none"></path>
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#pattern-bg)"></rect>
</svg>
<Form.Item
className="p-0 bg-transparent rounded border-none "
name="password"
rules={[{ required: true, message: $t('请输入密码') }]}
>
<Input.Password
className="w-[350px] h-[40px] login-input"
placeholder={$t('密码')}
autoComplete="off"
/>
</Form.Item>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.dev/svgjs" viewBox="0 0 800 450" opacity="1">
<defs>
<filter id="bbblurry-filter" x="-100%" y="-100%" width="400%" height="400%" filterUnits="objectBoundingBox" primitiveUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feGaussianBlur stdDeviation="99" x="0%" y="0%" width="100%" height="100%" in="SourceGraphic" edgeMode="none" result="blur"></feGaussianBlur>
</filter>
</defs>
<g filter="url(#bbblurry-filter)">
<ellipse rx="80.5" ry="66.5" cx="623.0285107902043" cy="25.708028895006635" fill="hsla(187, 67%, 50%, 1.00)">
<animate attributeName="fill" values="hsla(187, 67%, 50%, 1.00); hsla(340, 85%, 60%, 1.00); hsla(60, 90%, 55%, 1.00); hsla(187, 67%, 50%, 1.00)" dur="6s" repeatCount="indefinite"></animate>
</ellipse>
<ellipse rx="80.5" ry="66.5" cx="446.471435546875" cy="-11.694503784179688" fill="hsla(234, 78%, 61%, 1.00)">
<animate attributeName="fill" values="hsla(234, 78%, 61%, 1.00); hsla(100, 75%, 60%, 1.00); hsla(290, 80%, 70%, 1.00); hsla(234, 78%, 61%, 1.00)" dur="8s" repeatCount="indefinite"></animate>
</ellipse>
<ellipse rx="80.5" ry="66.5" cx="200.54574247724838" cy="-19.02454901710908" fill="hsla(167, 87%, 56%, 1.00)">
<animate attributeName="fill" values="hsla(167, 87%, 56%, 1.00); hsla(10, 90%, 65%, 1.00); hsla(300, 85%, 50%, 1.00); hsla(167, 87%, 56%, 1.00)" dur="10s" repeatCount="indefinite"></animate>
</ellipse>
<ellipse rx="80.5" ry="66.5" cx="340.05827594708103" cy="-9.424536458161867" fill="hsl(25, 100%, 64%)">
<animate attributeName="fill" values="hsl(25, 100%, 64%); hsl(200, 100%, 70%); hsl(50, 95%, 55%); hsl(25, 100%, 64%)" dur="8s" repeatCount="indefinite"></animate>
</ellipse>
</g>
</svg>
<Form.Item className="p-0 bg-transparent rounded border-none ">
<Button
loading={loading}
className="h-[40px] mt-mbase w-full inline-flex justify-center items-center"
type="primary"
htmlType="submit"
>
{$t('登录')}
</Button>
</Form.Item>
{allowFeishuLogin && (
<>
<Divider />
<Form.Item className="p-0 bg-transparent rounded border-none mb-0">
<Button
loading={loading}
className="h-[40px] w-full inline-flex justify-center items-center"
type="default"
onClick={() => openFeishuLogin(feishuAppId)}
>
<img className="h-[30px]" src={FeishuLogo} />
{$t('飞书授权登录')}
</Button>
</Form.Item>
</>
)}
{allowGuest && (
<>
<Divider />
<Form.Item className="p-0 bg-transparent rounded border-none mb-0">
<Button
loading={loading}
className="h-[40px] w-full inline-flex justify-center items-center"
type="default"
onClick={loginAsGuest}
>
{$t('访客模式')}{' '}
<Tooltip
title={$t(
'您可通过访客模式查看所有页面和功能,但是无法编辑数据。访客模式仅用于了解产品功能,您可以在正式产品中关闭该功能。'
)}
>
<Icon icon="ic:baseline-help" height={18} width={18} />
</Tooltip>
</Button>
</Form.Item>
</>
)}
</Form>
</div>
{/* <div className="w-full border-box text-right pr-[40px]"></div> */}
<div className="mx-auto flex-1 flex flex-col items-center justify-center z-[3]" >
<div className="mx-auto">
<span className="flex items-center justify-center">
<img
className="h-[40px] mr-[8px]"
src={Logo}
/>
</span>
</div>
</div>
</section>
<section className="block w-[410px] mx-auto mt-[46px] p-[30px] box-border rounded-[10px] shadow-[0_5px_20px_0_rgba(0,0,0,5%)] login-block">
<div className="h-full">
<div className="">
<Form onFinish={login} className="w-[350px]"
ref={formRef}>
<Form.Item
className="p-0 bg-transparent rounded border-none"
name="username"
rules={[{ required: true, message: $t('请输入账号') ,whitespace:true }]}
>
<Input
className="w-[350px] h-[40px] login-input"
placeholder={$t("账号")}
autoComplete="on"
autoFocus
/>
</Form.Item>
<Form.Item
className="p-0 bg-transparent rounded border-none "
name="password"
rules={[{ required: true, message: $t('请输入密码') }]}
>
<Input.Password
className="w-[350px] h-[40px] login-input"
placeholder={$t("密码")}
autoComplete="off"
/>
</Form.Item>
<Form.Item
className="p-0 bg-transparent rounded border-none "
>
<Button loading={loading} className="h-[40px] mt-mbase w-full inline-flex justify-center items-center" type="primary" htmlType="submit">
{$t('登录')}
</Button>
</Form.Item>
{
allowGuest && <>
<Divider />
<Form.Item
className="p-0 bg-transparent rounded border-none mb-0"
>
<Button loading={loading} className="h-[40px] w-full inline-flex justify-center items-center" type="default" onClick={loginAsGuest}>
{$t('访客模式')} <Tooltip title={$t('您可通过访客模式查看所有页面和功能,但是无法编辑数据。访客模式仅用于了解产品功能,您可以在正式产品中关闭该功能。')}><Icon icon="ic:baseline-help" height={18} width={18} /></Tooltip>
</Button>
</Form.Item>
</>
}
</Form>
</div>
</div>
</section>
<section className="flex flex-col items-center mt-[46px] text-SECOND_TEXT">
<p className="leading-[28px]">
{$t('Version (0)-(1)',[state?.version,state?.updateDate])}, {$t(state?.powered || '-')}
</p>
<LanguageSetting mode="light"/>
</section>
</div>
</div>
);
<section className="flex flex-col items-center mt-[46px] text-SECOND_TEXT">
<p className="leading-[28px]">
{$t('Version (0)-(1)', [state?.version, state?.updateDate])}, {$t(state?.powered || '-')}
</p>
<LanguageSetting mode="light" />
</section>
</div>
</div>
)
}
export default Login;
export default Login
@@ -0,0 +1,171 @@
import InsidePage from '@common/components/aoplatform/InsidePage'
import WithPermission from '@common/components/aoplatform/WithPermission'
import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
import { useFetch } from '@common/hooks/http'
import { $t } from '@common/locales'
import { App, Button, Form, Input, Row, Select, Switch } from 'antd'
import { useEffect, useState } from 'react'
type AuthSetting = {
config: {
clientId: string
clientSecret: string
}
enabled: boolean
}
type AuthFieldType = {
authType: string
clientId: string
clientSecret: string
enabled: boolean
}
const Auth = () => {
const { message } = App.useApp()
const [form] = Form.useForm()
const { fetchData } = useFetch()
const [, forceUpdate] = useState<unknown>(null)
const { state } = useGlobalContext()
const [thirdPartyDrivers, setThirdPartyDrivers] = useState<{ label: string; value: string }[]>([])
useEffect(() => {
forceUpdate({})
}, [state.language])
const onFinish = () => {
form.validateFields().then((value) => {
return fetchData<BasicResponse<null>>(`account/third/${value.authType}`, {
method: 'POST',
eoBody: {
enable: value.enabled,
config: {
client_id: value.clientId,
client_secret: value.clientSecret
}
}
})
.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((errorInfo) => {
return Promise.reject(errorInfo)
})
})
}
/**
* 获取第三方授权列表
*/
const getThirdPartyAuthList = () => {
fetchData<
BasicResponse<{
drivers: {
name: string
value: string
}[]
}>
>('account/third', {
method: 'GET',
}).then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
setThirdPartyDrivers(data.drivers.map((item: any) => ({ label: item.name, value: item.value })))
if (data.drivers.length) {
form.setFieldValue('authType', data.drivers[0].value)
getThirdPartyAuthSetting()
}
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
}
/**
* 获取第三方授权配置
*/
const getThirdPartyAuthSetting = () => {
fetchData<BasicResponse<{ info: AuthSetting }>>(`account/third/${form.getFieldValue('authType')}`, {
method: 'GET',
eoTransformKeys: ['client_id', 'client_secret']
}).then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
form.setFieldsValue({
clientId: data.driver?.config?.clientId || '',
clientSecret: data.driver?.config?.clientSecret || '',
enabled: data.driver?.enable || false
})
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
}
useEffect(() => {
getThirdPartyAuthList()
}, [])
return (
<InsidePage pageTitle={$t('鉴权')} showBorder={false} contentClassName="pr-PAGE_INSIDE_X" scrollPage={false} description={$t("系统用户账号登录授权配置")}>
<WithPermission access="">
<Form
layout="vertical"
labelAlign="left"
scrollToFirstError
form={form}
className={`mx-auto`}
name="authConfig"
onFinish={onFinish}
autoComplete="off"
>
<Form.Item<AuthFieldType>
label={$t('授权类型')}
name="authType"
rules={[{ required: true, message: $t('请选择授权类型') }]}
>
<Select className="w-INPUT_NORMAL" placeholder={$t('请选择授权类型')} onChange={getThirdPartyAuthSetting} options={thirdPartyDrivers} />
</Form.Item>
<Form.Item<AuthFieldType>
label={$t('APP ID')}
name="clientId"
rules={[{ required: true, whitespace: true, message: $t('请输入APP ID') }]}
extra={$t('APP ID 参数位于飞书开发人员控制台中的应用程序凭证和基础信息页面上')}
>
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} autoComplete="off" />
</Form.Item>
<Form.Item<AuthFieldType>
label={$t('APP Secret')}
name="clientSecret"
rules={[{ required: true, whitespace: true, message: $t('请输入APP Secret') }]}
extra={$t('APP Secret 参数位于飞书开发人员控制台中的应用程序凭证和基础信息页面上')}
>
<Input.Password className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} autoComplete="new-password" />
</Form.Item>
<Form.Item<AuthFieldType> label={$t('启用授权')} name="enabled" valuePropName="checked">
<Switch checkedChildren={$t('启用')} unCheckedChildren={$t('停用')} />
</Form.Item>
<Row className="mb-[10px]">
<WithPermission access="system.devops.system_setting.edit">
<Button type="primary" htmlType="submit">
{$t('保存')}
</Button>
</WithPermission>
</Row>
</Form>
</WithPermission>
</InsidePage>
)
}
export default Auth
@@ -50,17 +50,23 @@ type ServiceApiKeyList = {
expired: number
}>
}
type ConsumerParamsType = {
consumerId: string
teamId: string
}
export interface IntegrationAIContainerRef {
getServiceKeysList: () => void;
}
export interface IntegrationAIContainerProps {
type: 'global' | 'service'
type: 'global' | 'service' | 'consumer'
handleToolsChange: (value: Tool[]) => void
customClassName?: string
service?: ServiceDetailType
serviceId?: string
currentTab?: string
openModal?: (type: 'apply') => void
consumerParams?: ConsumerParamsType
}
export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, IntegrationAIContainerProps>(
({
@@ -69,8 +75,9 @@ export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, Inte
customClassName,
service,
serviceId,
currentTab,
openModal
currentTab,
openModal,
consumerParams
}: IntegrationAIContainerProps, ref) => {
/** 当前激活的标签 */
const [activeTab, setActiveTab] = useState(type === 'service' ? 'openApi' : 'mcp')
@@ -180,7 +187,35 @@ export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, Inte
}
})
.catch((errorInfo) => {
message.error(errorInfo || $t(RESPONSE_TIPS.error))
message.error(errorInfo?.toString() || $t(RESPONSE_TIPS.error))
})
}
/**
* 获取消费者 MCP 配置
* @returns
*/
const getConsumerMcpConfig = () => {
fetchData<BasicResponse<null>>('app/mcp/config', {
method: 'GET',
eoParams: { app: consumerParams?.consumerId, team: consumerParams?.teamId }
})
.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?.toString() || $t(RESPONSE_TIPS.error))
})
}
@@ -191,6 +226,10 @@ export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, Inte
navigator('/mcpKey')
}
const dropAuthPage = () => {
navigator(`/consumer/${consumerParams?.teamId}/inside/${consumerParams?.consumerId}/authorization`)
}
/**
* 获取全局 API Key 列表
*/
@@ -217,7 +256,7 @@ export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, Inte
}
})
.catch((errorInfo) => {
message.error(errorInfo || $t(RESPONSE_TIPS.error))
message.error(errorInfo?.toString() || $t(RESPONSE_TIPS.error))
})
}
@@ -229,12 +268,12 @@ export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, Inte
}))
/**
* 获取服务 API Key 列表
* 获取 API Key 列表
*/
const getServiceKeysList = () => {
const getServiceKeysList = (consumerId?: string) => {
fetchData<BasicResponse<null>>(`my/app/apikeys`, {
method: 'GET',
eoParams: { service: serviceId }
eoParams: consumerId ? { app: consumerId } : { service: serviceId }
})
.then((response) => {
const { code, msg, data } = response
@@ -258,7 +297,7 @@ export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, Inte
}
})
.catch((errorInfo) => {
message.error(errorInfo || $t(RESPONSE_TIPS.error))
message.error(errorInfo?.toString() || $t(RESPONSE_TIPS.error))
})
}
@@ -345,6 +384,10 @@ export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, Inte
getGlobalMcpConfig()
setMcpServerUrl('mcp/global/sse')
getGlobalKeysList()
} else if (type === 'consumer'){
getConsumerMcpConfig()
setMcpServerUrl(`mcp/app/${consumerParams?.consumerId}/sse`)
getServiceKeysList(consumerParams?.consumerId)
} else {
service?.basic.enableMcp && setMcpServerUrl(`mcp/service/${serviceId}/sse`)
getServiceKeysList()
@@ -362,6 +405,7 @@ export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, Inte
useEffect(() => {
initTabsData()
type === 'global' && getGlobalMcpConfig()
type === 'consumer' && getConsumerMcpConfig()
}, [state.language])
/**
* 切换标签
@@ -408,7 +452,7 @@ export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, Inte
}
connectMcpServer()
}
}, [mcpServerUrl, ...(type === 'global' ? [state.language] : [])])
}, [mcpServerUrl, ...(type === 'global' || type === 'consumer' ? [state.language] : [])])
/**
* 获取 MCP tools
*/
@@ -452,7 +496,7 @@ export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, Inte
</div>
</div>
)}
{type === 'service' && !apiKeyList.length ? (
{(type === 'service' || type === 'consumer') && !apiKeyList.length ? (
<>
<Card
style={{ borderRadius: '10px' }}
@@ -461,12 +505,23 @@ export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, Inte
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>
{
type === 'service' ? (
<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>
) : (
<div className="flex flex-col items-center justify-center py-3">
<span className="text-[14px] mb-5">{$t('未配置 API Key')}</span>
<Button type="primary" onClick={() => dropAuthPage()}>
{$t('配置')}
</Button>
</div>
)
}
</Card>
</>
) : (
@@ -14,7 +14,7 @@ export const MemberDropdownModal = forwardRef<MemberDropdownModalHandle,MemberDr
const {fetchData} = useFetch()
const [departmentList, setDepartmentList] = useState<DepartmentListItem[]>([])
const { state } = useGlobalContext()
const [disableEditMemberData] = useState<boolean>(entity?.from === 'feishu')
const save:()=>Promise<boolean | string> = ()=>{
let url:string
let method:string
@@ -182,27 +182,28 @@ export const MemberDropdownModal = forwardRef<MemberDropdownModalHandle,MemberDr
name="name"
rules={[{required: true,whitespace:true }]}
>
<Input className="w-INPUT_NORMAL" disabled={type ==='editMember'} placeholder={$t(PLACEHOLDER.input)}/>
<Input className="w-INPUT_NORMAL" disabled={disableEditMemberData || type ==='editMember'} placeholder={$t(PLACEHOLDER.input)}/>
</Form.Item>
<Form.Item<MemberDropdownModalFieldType>
label={$t("邮箱")}
name="email"
rules={[{required: true,whitespace:true },{type:"email",message: $t(VALIDATE_MESSAGE.email)}]}
>
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)}/>
<Input className="w-INPUT_NORMAL" disabled={disableEditMemberData} placeholder={$t(PLACEHOLDER.input)}/>
</Form.Item>
<Form.Item<MemberDropdownModalFieldType>
label={$t("密码")}
name="password"
rules={[{required: type === 'addMember',whitespace:true }]}
>
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)}/>
<Input disabled={disableEditMemberData} className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)}/>
</Form.Item>
<Form.Item<MemberDropdownModalFieldType>
label={$t("部门")}
name="departmentIds"
>
<TreeSelect
disabled={disableEditMemberData}
className="w-INPUT_NORMAL"
fieldNames={{label:'name',value:'id',children:'children'}}
showSearch
@@ -95,14 +95,15 @@ const AddToDepartment = forwardRef<AddToDepartmentHandle, AddToDepartmentProps>(
treeData?.map((x: DataNode) => ({
...x,
name: $t((x as unknown as { name: string }).name),
checkable: false,
children: x.children?.map(y => ({ ...y, checkable: false }))
checkable: false, // 根节点不可选中
children: x.children?.map(y => ({ ...y, checkable: true })) // 子节点可以选中
})),
[state.language, treeData]
)
const onCheck: TreeProps['onCheck'] = (checkedKeys: string[]) => {
setSelectedKeys(checkedKeys.checked)
const onCheck: TreeProps['onCheck'] = (checkedKeys, info) => {
const selectedIds = Array.isArray(checkedKeys) ? checkedKeys : checkedKeys.checked || []
setSelectedKeys(selectedIds)
}
useEffect(() => {
@@ -153,11 +154,12 @@ const MemberList = () => {
const [tableHttpReload, setTableHttpReload] = useState(true)
const [tableListDataSource, setTableListDataSource] = useState<MemberTableListItem[]>([])
const pageListRef = useRef<ActionType>(null)
const { topGroupId, selectedDepartmentIds, refreshGroup } = useOutletContext<{
const { topGroupId, selectedDepartmentIds, refreshGroup, refreshTableCount } = useOutletContext<{
topGroupId: string
departmentList: DepartmentListItem[]
selectedDepartmentIds: string[]
refreshGroup: () => void
refreshTableCount: number
}>()
const AddMemberRef = useRef<MemberDropdownModalHandle>(null)
const EditMemberRef = useRef<MemberDropdownModalHandle>(null)
@@ -396,7 +398,7 @@ const MemberList = () => {
width: 600,
okText: $t('确认'),
okButtonProps: {
disabled: isActionAllowed(type)
disabled: isActionAllowed(type) || (type === 'editMember' && entity?.from === 'feishu')
},
cancelText: $t('取消'),
closable: true,
@@ -415,6 +417,13 @@ const MemberList = () => {
getDepartmentList()
}, [])
// 监听外部刷新触发器
useEffect(() => {
if (refreshTableCount > 0) {
manualReloadTable()
}
}, [refreshTableCount])
const getDepartmentList = async () => {
setDepartmentValueEnum([])
const { code, data, msg } = await fetchData<BasicResponse<{ department: DepartmentListItem }>>(
@@ -36,6 +36,11 @@ const MemberPage = ()=>{
const [selectedDepartmentId, setSelectedDepartmentId] = useState<string>('-1')
const {accessData,state} = useGlobalContext()
const [refreshMemberCount, setRefreshMemberCount] = useState<number>(0)
const [refreshTableCount, setRefreshTableCount] = useState<number>(0)
const refreshMemberTable = () => {
setRefreshTableCount(prev => prev + 1)
}
const onSearchWordChange = (e:string)=>{
setSearchWord(e || '')
}
@@ -90,7 +95,7 @@ const MemberPage = ()=>{
case 'addChild':
return AddChildRef.current?.save().then((res)=>{if(res === true)getDepartmentList()})
case 'addMember':
return AddMemberRef.current?.save().then((res)=>{if(res === true){getDepartmentList();setRefreshMemberCount(pre=>pre+1)}})
return AddMemberRef.current?.save().then((res)=>{if(res === true){getDepartmentList();setRefreshMemberCount(pre=>pre+1);refreshMemberTable()}})
case 'rename':
return RenameRef.current?.save().then((res)=>{if(res === true)getDepartmentList()})
case 'delete':
@@ -262,7 +267,7 @@ const MemberPage = ()=>{
</div>
</div>
<div className="flex-1 p-btnbase pr-PAGE_INSIDE_X overflow-x-hidden">
<Outlet context={{refreshMemberCount, selectedDepartmentIds,refreshGroup:()=>getDepartmentList()}}/>
<Outlet context={{refreshMemberCount, selectedDepartmentIds,refreshGroup:()=>getDepartmentList(), refreshTableCount}}/>
</div>
</div>
</InsidePage>);
@@ -20,7 +20,7 @@ export default function ManagementInsidePage() {
const { message } = App.useApp()
const { fetchData } = useFetch()
const { setBreadcrumb } = useBreadcrumb()
const [activeMenu, setActiveMenu] = useState<string>('service')
const [activeMenu, setActiveMenu] = useState<string>('authorization')
const { appId, teamId } = useParams<RouterParams>()
const navigateTo = useNavigate()
const currentUrl = useLocation().pathname
@@ -31,8 +31,9 @@ export default function ManagementInsidePage() {
const TENANT_MANAGEMENT_APP_MENU: MenuProps['items'] = useMemo(
() => [
getItem($t('订阅的服务'), 'service', undefined, undefined, undefined, 'team.application.subscription.view'),
getItem($t('访问授权'), 'authorization', undefined, undefined, undefined, 'team.consumer.authorization.view'),
getItem($t('MCP 服务'), 'mcp', undefined, undefined, undefined, 'team.consumer.mcp.view'),
getItem($t('订阅的服务'), 'service', undefined, undefined, undefined, 'team.application.subscription.view'),
getItem($t('消费者管理'), 'setting', undefined, undefined, undefined, 'team.application.application.view')
],
[state.language]
@@ -55,7 +56,7 @@ export default function ManagementInsidePage() {
}, [accessData, accessInit, TENANT_MANAGEMENT_APP_MENU])
useEffect(() => {
setActiveMenu(currentUrl.split('/').pop() || 'service')
setActiveMenu(currentUrl.split('/').pop() || 'authorization')
}, [currentUrl])
const onMenuClick: MenuProps['onClick'] = (node) => {
@@ -336,7 +336,7 @@ export default function ServiceHubManagement() {
setTableSearchWord={setTableSearchWord}
editApp={(row: ServiceHubAppListItem) => {
setAppName(row.name)
navigateTo(`/consumer/${row.team.id}/inside/${row.id}/service`)
navigateTo(`/consumer/${row.team.id}/inside/${row.id}/authorization`)
}}
/>
)}
@@ -0,0 +1,40 @@
import { $t } from '@common/locales'
import { IntegrationAIContainer } from '@core/pages/mcpService/IntegrationAIContainer'
import { Tool } from '@modelcontextprotocol/sdk/types.js'
import { useEffect, useState } from 'react'
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
import McpToolsContainer from '@core/pages/mcpService/McpToolsContainer'
import { useParams } from 'react-router-dom'
import { RouterParams } from '@common/const/type'
const mcpContent = () => {
const [tools, setTools] = useState<Tool[]>([])
const [, forceUpdate] = useState<unknown>(null)
const { teamId, appId } = useParams<RouterParams>()
const { state } = useGlobalContext()
const handleToolsChange = (value: Tool[]) => {
setTools(value)
}
useEffect(() => {
forceUpdate({})
}, [state.language])
return (
<div className=" h-full pt-[32px]">
<div className="flex items-center justify-between w-full ml-[10px] text-[18px] leading-[25px] pb-[16px]">
<span className="font-bold">{$t('MCP 服务')}</span>
</div>
<div className="h-[calc(100%-41px)] flex flex-col ">
<div className="flex mt-[10px] pr-[40px]">
<McpToolsContainer tools={tools} />
<IntegrationAIContainer
consumerParams={{ consumerId: appId!, teamId: teamId! }}
type={'consumer'}
handleToolsChange={handleToolsChange}
></IntegrationAIContainer>
</div>
</div>
</div>
)
}
export default mcpContent
+7 -5
View File
@@ -5,17 +5,17 @@ go 1.23.4
toolchain go1.23.6
require (
github.com/eolinker/ap-account v1.0.15
github.com/eolinker/ap-account v1.0.18
github.com/eolinker/eosc v0.18.3
github.com/eolinker/go-common v1.1.6
github.com/eolinker/go-common v1.1.7
github.com/gabriel-vasile/mimetype v1.4.4
github.com/getkin/kin-openapi v0.127.0
github.com/getkin/kin-openapi v0.132.0
github.com/gin-contrib/gzip v1.0.1
github.com/gin-gonic/gin v1.10.0
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/mark3labs/mcp-go v0.33.0
github.com/mitchellh/mapstructure v1.5.0
github.com/nsqio/go-nsq v1.1.0
github.com/ollama/ollama v0.5.8
@@ -46,7 +46,6 @@ require (
github.com/golang/snappy v0.0.4 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 // indirect
github.com/invopop/yaml v0.3.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
@@ -59,10 +58,13 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/oapi-codegen/runtime v1.0.0 // indirect
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/redis/go-redis/v9 v9.5.3 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/spf13/cast v1.7.1 // 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
+16 -10
View File
@@ -28,16 +28,18 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/eolinker/ap-account v1.0.15 h1:n6DJeL6RHZ8eLlZUcY2U3H4d/GPaA5oelAx3R0E6yL8=
github.com/eolinker/ap-account v1.0.15/go.mod h1:zm/Ivs6waJ/M/nEszhpPmM6g50y/MKO+5eABFAdeD0g=
github.com/eolinker/ap-account v1.0.18 h1:YgDHoUmAdofPmaGhOQx+vj3+uhsv486kD3KLU/JKmBs=
github.com/eolinker/ap-account v1.0.18/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.6 h1:s+NaQL0InjX/MwWY53+8y8qzAgsULIUc4U6nWXWQ2Nw=
github.com/eolinker/go-common v1.1.6/go.mod h1:Kb/jENMN1mApnodvRgV4YwO9FJby1Jkt2EUjrBjvSX4=
github.com/eolinker/go-common v1.1.7 h1:bi7wDmlCYQGjS3k8Bz/o+Mo9aMJAzmPsBLXWurxPfwk=
github.com/eolinker/go-common v1.1.7/go.mod h1:Kb/jENMN1mApnodvRgV4YwO9FJby1Jkt2EUjrBjvSX4=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
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=
github.com/getkin/kin-openapi v0.127.0/go.mod h1:OZrfXzUfGrNbsKj+xmFBx6E5c6yH3At/tAKSc2UszXM=
github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk=
github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/gzip v1.0.1 h1:HQ8ENHODeLY7a4g1Au/46Z92bdGFl74OhxcZble9WJE=
@@ -78,8 +80,6 @@ github.com/influxdata/influxdb-client-go/v2 v2.14.0 h1:AjbBfJuq+QoaXNcrova8smSjw
github.com/influxdata/influxdb-client-go/v2 v2.14.0/go.mod h1:Ahpm3QXKMJslpXl3IftVLVezreAUtBOTZssDrjZEFHI=
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU=
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo=
github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso=
github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
@@ -101,8 +101,8 @@ 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/mark3labs/mcp-go v0.33.0 h1:naxhjnTIs/tyPZmWUZFuG0lDmdA6sUyYGGf3gsHvTCc=
github.com/mark3labs/mcp-go v0.33.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
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=
@@ -118,6 +118,10 @@ github.com/nsqio/go-nsq v1.1.0 h1:PQg+xxiUjA7V+TLdXw7nVrJ5Jbl3sN86EhGCQj4+FYE=
github.com/nsqio/go-nsq v1.1.0/go.mod h1:vKq36oyeVXgsS5Q8YEO7WghqidAVXQlcFxzQbQTuDEY=
github.com/oapi-codegen/runtime v1.0.0 h1:P4rqFX5fMFWqRzY9M/3YF9+aPSPPB06IzP2P7oOxrWo=
github.com/oapi-codegen/runtime v1.0.0/go.mod h1:LmCUMQuPB4M/nLXilQXhHw+BLZdDb18B34OO356yJ/A=
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
github.com/ollama/ollama v0.5.8 h1:b2S6YdZ18/ntCsWzoy/HmB3BHGW4GX0Qp7RARrJtJXU=
github.com/ollama/ollama v0.5.8/go.mod h1:ibdmDvb/TjKY1OArBWIazL3pd1DHTk8eG2MMjEkWhiI=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
@@ -134,6 +138,8 @@ github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+1
View File
@@ -4,6 +4,7 @@ package main
import (
_ "github.com/APIParkLab/APIPark/frontend"
_ "github.com/APIParkLab/APIPark/gateway/apinto"
_ "github.com/APIParkLab/APIPark/login_driver/feishu"
_ "github.com/APIParkLab/APIPark/plugins/core"
_ "github.com/APIParkLab/APIPark/plugins/openapi"
_ "github.com/APIParkLab/APIPark/plugins/permit"
+2 -1
View File
@@ -9,7 +9,8 @@ import (
type ILogDriver interface {
LogInfo(clusterId string, id string) (*LogInfo, error)
LogCount(clusterId string, conditions map[string]string, spendHour int64, group string) (map[string]int64, error)
Logs(clusterId string, conditions map[string]string, start time.Time, end time.Time, limit int64, offset int64) ([]*Log, int64, error)
Logs(clusterId string, conditions map[string]string, start time.Time, end time.Time, limit int64, offset int64) ([]*LogItem, int64, error)
LogRecords(clusterId string, start time.Time, end time.Time) ([]*LogItem, error)
}
var (
+14 -2
View File
@@ -4,22 +4,34 @@ import (
"time"
)
type Log struct {
type LogItem struct {
ID string
Strategy string
Service string
API string
Method string
Url string
RemoteIP string
Consumer string
Authorization string
InputToken int64
OutputToken int64
TotalToken int64
AIProvider string
AIModel string
StatusCode int64
ResponseTime int64
Traffic int64
RecordTime time.Time
}
type LogInfo struct {
ID string
*LogItem
ContentType string
RequestBody string
ProxyBody string
ProxyResponseBody string
ResponseBody string
RequestHeader string
ResponseHeader string
}
+34 -19
View File
@@ -49,29 +49,44 @@ type LogCount struct {
}
type LogInfo struct {
Stream *LogDetail `json:"stream"`
Stream *LogDetail `json:"stream"`
Values []interface{} `json:"values"`
}
type LogDetail struct {
Api string `json:"api"`
Application string `json:"application"`
Strategy string `json:"strategy"`
ContentType string `json:"content_type"`
Cluster string `json:"cluster"`
Msec string `json:"msec"`
Node string `json:"node"`
RequestId string `json:"request_id"`
RequestMethod string `json:"request_method"`
RequestScheme string `json:"request_scheme"`
RequestTime string `json:"request_time"`
RequestUri string `json:"request_uri"`
type LogBodyDetail struct {
RequestBody string `json:"request_body"`
ProxyBody string `json:"proxy_body"`
ResponseBody string `json:"response_body"`
ProxyResponseBody string `json:"proxy_response_body"`
Service string `json:"service"`
Provider string `json:"provider"`
Authorization string `json:"authorization"`
SrcIp string `json:"src_ip"`
Status string `json:"status"`
}
type LogDetail struct {
Api string `json:"api"`
Application string `json:"application"`
Strategy string `json:"strategy"`
ContentType string `json:"content_type"`
Cluster string `json:"cluster"`
Msec string `json:"msec"`
Node string `json:"node"`
RequestId string `json:"request_id"`
RequestMethod string `json:"request_method"`
RequestScheme string `json:"request_scheme"`
RequestHeader string `json:"request_header"`
RequestTime string `json:"request_time"`
RequestUri string `json:"request_uri"`
RequestBody string `json:"request_body"`
ProxyBody string `json:"proxy_body"`
ResponseBody string `json:"response_body"`
ResponseHeader string `json:"response_header"`
ProxyResponseBody string `json:"proxy_response_body"`
Service string `json:"service"`
Provider string `json:"provider"`
Authorization string `json:"authorization"`
SrcIp string `json:"src_ip"`
Status string `json:"status"`
AIProvider string `json:"ai_provider"`
AIModel string `json:"ai_model"`
AIModelInputToken interface{} `json:"ai_model_input_token"`
AIModelOutputToken interface{} `json:"ai_model_output_token"`
AIModelTotalToken interface{} `json:"ai_model_total_token"`
}
+97 -21
View File
@@ -81,13 +81,39 @@ func (d *Driver) LogInfo(clusterId string, id string) (*log_driver.LogInfo, erro
return nil, fmt.Errorf("no log found")
}
stream := list[0].Stream
requestBody := stream.RequestBody
proxyRequestBody := stream.ProxyBody
proxyResponseBody := stream.ProxyResponseBody
responseBody := stream.ResponseBody
if len(list[0].Values) > 0 {
switch t := list[0].Values[0].(type) {
case []interface{}:
if len(t) > 1 {
v, ok := t[1].(string)
if !ok {
break
}
var tmp LogBodyDetail
err = json.Unmarshal([]byte(v), &tmp)
if err == nil {
requestBody = tmp.RequestBody
proxyRequestBody = tmp.ProxyBody
responseBody = tmp.ResponseBody
proxyResponseBody = tmp.ProxyBody
}
}
}
}
msec, _ := strconv.ParseInt(stream.Msec, 10, 64)
return &log_driver.LogInfo{
ID: stream.RequestId,
LogItem: ToLogItem(stream, msec),
ContentType: stream.ContentType,
RequestBody: stream.RequestBody,
ProxyBody: stream.ProxyBody,
ProxyResponseBody: stream.ProxyResponseBody,
ResponseBody: stream.ResponseBody,
RequestBody: requestBody,
ProxyBody: proxyRequestBody,
ProxyResponseBody: proxyResponseBody,
ResponseBody: responseBody,
RequestHeader: stream.RequestHeader,
ResponseHeader: stream.ResponseHeader,
}, nil
}
@@ -132,7 +158,25 @@ func (d *Driver) LogCount(clusterId string, conditions map[string]string, spendH
return result, nil
}
func (d *Driver) Logs(clusterId string, conditions map[string]string, start time.Time, end time.Time, limit int64, offset int64) ([]*log_driver.Log, int64, error) {
func (d *Driver) LogRecords(clusterId string, start time.Time, end time.Time) ([]*log_driver.LogItem, error) {
if start.After(end) {
return nil, fmt.Errorf("start time is greater than end time")
}
queries := url.Values{}
queries.Set("query", fmt.Sprintf("{cluster=\"%s\"} | json", clusterId))
queries.Set("direction", "backward")
queries.Set("start", strconv.FormatInt(start.UnixNano(), 10))
queries.Set("end", strconv.FormatInt(end.UnixNano(), 10))
log.Debug("query is ", queries.Get("query"))
logs, err := d.recuseLogs(queries, end, 1)
if err != nil {
return nil, err
}
return logs, nil
}
func (d *Driver) Logs(clusterId string, conditions map[string]string, start time.Time, end time.Time, limit int64, offset int64) ([]*log_driver.LogItem, int64, error) {
if start.After(end) {
return nil, 0, fmt.Errorf("start time is greater than end time")
}
@@ -177,7 +221,30 @@ func (d *Driver) Logs(clusterId string, conditions map[string]string, start time
return logs, count, nil
}
func (d *Driver) recuseLogs(queries url.Values, end time.Time, offset int64) ([]*log_driver.Log, error) {
func ToLogItem(detail *LogDetail, msec int64) *log_driver.LogItem {
return &log_driver.LogItem{
ID: detail.RequestId,
Strategy: detail.Strategy,
Service: detail.Provider,
API: detail.Api,
Method: detail.RequestMethod,
Url: detail.RequestUri,
RemoteIP: detail.SrcIp,
Consumer: detail.Application,
Authorization: detail.Authorization,
InputToken: parseToInt64(detail.AIModelInputToken),
OutputToken: parseToInt64(detail.AIModelOutputToken),
TotalToken: parseToInt64(detail.AIModelTotalToken),
AIProvider: detail.AIProvider,
AIModel: detail.AIModel,
StatusCode: parseToInt64(detail.Status),
ResponseTime: parseToInt64(detail.RequestTime),
Traffic: int64(len(detail.ResponseBody) + len(detail.RequestBody)),
RecordTime: time.UnixMilli(msec),
}
}
func (d *Driver) recuseLogs(queries url.Values, end time.Time, offset int64) ([]*log_driver.LogItem, error) {
queries.Set("end", strconv.FormatInt(end.UnixNano(), 10))
list, err := send[LogInfo](http.MethodGet, fmt.Sprintf("%s/loki/api/v1/query_range", d.url), d.headers, queries, "")
if err != nil {
@@ -198,24 +265,13 @@ func (d *Driver) recuseLogs(queries url.Values, end time.Time, offset int64) ([]
}
return d.recuseLogs(queries, time.UnixMilli(msec), offset-1)
}
logs := make([]*log_driver.Log, 0, len(list))
logs := make([]*log_driver.LogItem, 0, len(list))
for _, l := range list {
if l.Stream == nil {
continue
}
detail := l.Stream
msec, _ := strconv.ParseInt(detail.Msec, 10, 64)
logs = append(logs, &log_driver.Log{
ID: detail.RequestId,
Service: detail.Provider,
Method: detail.RequestMethod,
Url: detail.RequestUri,
RemoteIP: detail.SrcIp,
Consumer: detail.Application,
Authorization: detail.Authorization,
RecordTime: time.UnixMilli(msec),
})
msec, _ := strconv.ParseInt(l.Stream.Msec, 10, 64)
logs = append(logs, ToLogItem(l.Stream, msec))
}
sort.Slice(logs, func(i, j int) bool {
return logs[i].RecordTime.After(logs[j].RecordTime)
@@ -223,6 +279,26 @@ func (d *Driver) recuseLogs(queries url.Values, end time.Time, offset int64) ([]
return logs, nil
}
func parseToInt64(v interface{}) int64 {
switch t := v.(type) {
case int:
return int64(t)
case int64:
return t
case string:
if v == "" {
return 0
}
i, err := strconv.ParseInt(t, 10, 64)
if err != nil {
return 0
}
return i
default:
return 0
}
}
func (d *Driver) logCount(clusterId string, conditions map[string]string, start time.Time, end time.Time) (int64, error) {
// 先查在这段时间内符合条件的日志数量
queries := url.Values{}
+15 -15
View File
@@ -33,23 +33,23 @@ func TestLoki(t *testing.T) {
// headers["Content-Type"] = "application/json"
// headers["X-Scope-OrgID"] = "tenant1"
// queries := url.Values{}
// queries.Set("query", "{cluster=\"apinto\"} | json | request_id = `c9f6b19c-7dfe-496b-9b39-4d049232fe95`")
// queries.SetMCPServer("query", "{cluster=\"apinto\"} | json | request_id = `c9f6b19c-7dfe-496b-9b39-4d049232fe95`")
// now := time.Now()
// start := now.Add(-time.Hour * 24 * 30)
// queries.Set("start", strconv.FormatInt(start.UnixNano(), 10))
// queries.Set("end", strconv.FormatInt(now.UnixNano(), 10))
// queries.Set("limit", "100")
// queries.SetMCPServer("start", strconv.FormatInt(start.UnixNano(), 10))
// queries.SetMCPServer("end", strconv.FormatInt(now.UnixNano(), 10))
// queries.SetMCPServer("limit", "100")
// a := time.Now()
// result, err := send[LogInfo](http.MethodGet, "http://localhost:3100/loki/api/v1/query_range", headers, queries, "")
// if err != nil {
// t.Fatalf("failed to send request: %v", err)
// }
// t.Log(time.Now().Sub(a))
// t.LogItem(time.Now().Sub(a))
// data, err := json.Marshal(result)
// if err != nil {
// t.Fatalf("failed to marshal data: %v", err)
// }
// t.Log(string(data))
// t.LogItem(string(data))
//}
//
//func TestLokiLogCount(t *testing.T) {
@@ -57,8 +57,8 @@ func TestLoki(t *testing.T) {
// headers["Content-Type"] = "application/json"
// headers["X-Scope-OrgID"] = "tenant1"
// queries := url.Values{}
// //queries.Set("query", "sum(count_over_time({cluster=\"apinto\"}[24h])) by (strategy)")
// queries.Set("query", "sum(count_over_time({cluster=\"apinto\"}[24h]))")
// //queries.SetMCPServer("query", "sum(count_over_time({cluster=\"apinto\"}[24h])) by (strategy)")
// queries.SetMCPServer("query", "sum(count_over_time({cluster=\"apinto\"}[24h]))")
// result, err := send[LogCount](http.MethodGet, "http://localhost:3100/loki/api/v1/query", headers, queries, "")
// if err != nil {
// t.Fatalf("failed to send request: %v", err)
@@ -67,7 +67,7 @@ func TestLoki(t *testing.T) {
// if err != nil {
// t.Fatalf("failed to marshal data: %v", err)
// }
// t.Log(string(data))
// t.LogItem(string(data))
//}
//
//func TestLokiLogs(t *testing.T) {
@@ -75,15 +75,15 @@ func TestLoki(t *testing.T) {
// headers["Content-Type"] = "application/json"
// headers["X-Scope-OrgID"] = "tenant1"
// queries := url.Values{}
// queries.Set("query", "{cluster=\"apinto\"} | json | strategy=\"03899736-5d79-4f26-bd6a-c312a5880780\"")
// queries.SetMCPServer("query", "{cluster=\"apinto\"} | json | strategy=\"03899736-5d79-4f26-bd6a-c312a5880780\"")
// now := time.Now()
// start := now.Add(-time.Hour * 24 * 30)
// queries.Set("start", strconv.FormatInt(start.UnixNano(), 10))
// queries.Set("end", strconv.FormatInt(now.UnixNano(), 10))
// queries.Set("limit", "1")
// queries.SetMCPServer("start", strconv.FormatInt(start.UnixNano(), 10))
// queries.SetMCPServer("end", strconv.FormatInt(now.UnixNano(), 10))
// queries.SetMCPServer("limit", "1")
// now = time.Now()
// result, err := send[map[string]interface{}](http.MethodGet, "http://localhost:3100/loki/api/v1/query_range", headers, queries, "")
// t.Log(time.Now().Sub(now))
// t.LogItem(time.Now().Sub(now))
// if err != nil {
// t.Fatalf("failed to send request: %v", err)
// }
@@ -91,5 +91,5 @@ func TestLoki(t *testing.T) {
// if err != nil {
// t.Fatalf("failed to marshal data: %v", err)
// }
// t.Log(string(data))
// t.LogItem(string(data))
//}
+56
View File
@@ -0,0 +1,56 @@
package feishu
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
)
var (
client = http.Client{
Timeout: 10 * time.Second,
}
)
func SendRequest[T any](uri string, method string, header http.Header, query url.Values, body []byte) (*T, error) {
if uri == "" {
return nil, fmt.Errorf("invalid URL")
}
req, err := http.NewRequest(method, uri, bytes.NewReader(body))
if err != nil {
return nil, err
}
if query != nil {
req.URL.RawQuery = query.Encode()
}
if header != nil {
req.Header = header
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
result := new(T)
err = json.Unmarshal(respBody, result)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("status code error: %d, response: %s", resp.StatusCode, respBody)
}
return result, nil
}
+27
View File
@@ -0,0 +1,27 @@
package feishu
type UserTokenResponse struct {
Code int `json:"code"`
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
RefreshTokenExpiresIn int `json:"refresh_token_expires_in"`
TokenType string `json:"token_type"`
Scope string `json:"scope"`
Error string `json:"error"`
ErrorDescription string `json:"error_description"`
}
type UserInfoResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data UserInfo `json:"data"`
}
type UserInfo struct {
Name string `json:"name"`
OpenID string `json:"open_id"`
UnionId string `json:"union_id"`
Email string `json:"email"`
Mobile string `json:"mobile"`
}
+195
View File
@@ -0,0 +1,195 @@
package feishu
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"github.com/eolinker/go-common/autowire"
"github.com/eolinker/ap-account/service/role"
"github.com/eolinker/ap-account/service/user"
"github.com/eolinker/go-common/utils"
"github.com/google/uuid"
"gorm.io/gorm"
"github.com/eolinker/ap-account/service/account"
"github.com/eolinker/ap-account/auth_driver"
)
const (
name = "feishu"
title = "飞书"
getTokenUri = "https://open.feishu.cn/open-apis/authen/v2/oauth/token"
getUserInfoUri = "https://open.feishu.cn/open-apis/authen/v1/user_info"
)
var _ auth_driver.IDriver = (*Driver)(nil)
func init() {
d := &Driver{}
auth_driver.Register(name, d)
}
type Driver struct {
isInit bool
accountService account.IAccountService `autowired:""`
userService user.IUserService `autowired:""`
roleService role.IRoleService `autowired:""`
roleMemberService role.IRoleMemberService `autowired:""`
}
func (d *Driver) Init() {
if d.isInit {
return
}
autowire.Autowired(&d.accountService)
autowire.Autowired(&d.userService)
autowire.Autowired(&d.roleService)
autowire.Autowired(&d.roleMemberService)
d.isInit = true
}
func (d *Driver) FilterConfig(config map[string]string) {
delete(config, "client_secret")
}
func (d *Driver) Name() string {
return name
}
func (d *Driver) Title() string {
return title
}
func (d *Driver) ThirdLogin(ctx context.Context, args map[string]string) (string, error) {
code, ok := args["code"]
if !ok {
return "", fmt.Errorf("missing code parameter")
}
clientId, ok := args["client_id"]
if !ok {
return "", fmt.Errorf("missing client_id parameter")
}
clientSecret, ok := args["client_secret"]
if !ok {
return "", fmt.Errorf("missing client_secret parameter")
}
redirectUri, ok := args["redirect_uri"]
if !ok {
return "", fmt.Errorf("missing redirect_uri parameter")
}
u, err := url.Parse(redirectUri)
if err != nil {
return "", fmt.Errorf("invalid redirect_uri parameter")
}
query := u.Query()
query.Del("code")
redirectUri = fmt.Sprintf("%s://%s%s", u.Scheme, u.Host, u.Path)
if len(query) > 0 {
redirectUri = fmt.Sprintf("%s?%s", redirectUri, query.Encode())
}
tokenResp, err := getUserToken(code, redirectUri, clientId, clientSecret)
if err != nil {
return "", err
}
userInfoResp, err := getUserInfo(tokenResp.TokenType, tokenResp.AccessToken)
if err != nil {
return "", err
}
userId := userInfoResp.Data.UnionId
username := userInfoResp.Data.Name
email := userInfoResp.Data.Email
mobile := userInfoResp.Data.Mobile
info, err := d.accountService.GetIdentifier(ctx, name, userId)
if err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return "", err
}
uId := uuid.NewString()
err = d.accountService.Save(ctx, name, uId, userId, utils.Md5(fmt.Sprintf("%s%s", uId, userId)))
if err != nil {
return "", err
}
_, err = d.userService.Create(ctx, uId, username, email, mobile, name)
if err != nil {
return "", err
}
r, err := d.roleService.GetDefaultRole(ctx, role.SystemTarget())
if err != nil {
return "", err
}
err = d.roleMemberService.Add(ctx, &role.AddMember{
Role: r.Id,
User: uId,
Target: role.SystemTarget(),
})
if err != nil {
return "", err
}
return uId, nil
}
_, err = d.userService.Update(ctx, info.Uid, &username, &email, &mobile)
if err != nil {
return "", err
}
return info.Uid, nil
}
func getUserToken(code string, redirectUri, clientId string, clientSecret string) (*UserTokenResponse, error) {
headers := http.Header{}
headers.Set("Content-Type", "application/json")
//body := url.Values{}
//body.Set("grant_type", "authorization_code")
//body.Set("code", code)
//body.Set("client_id", clientId)
//body.Set("client_secret", clientSecret)
//body.Set("redirect_uri", redirectUri)
body := map[string]string{
"grant_type": "authorization_code",
"code": code,
"client_id": clientId,
"client_secret": clientSecret,
"redirect_uri": redirectUri,
}
bodyByte, _ := json.Marshal(body)
resp, err := SendRequest[UserTokenResponse](getTokenUri, http.MethodPost, headers, nil, bodyByte)
if err != nil {
return nil, fmt.Errorf("failed to get user token: %w", err)
}
if resp.Code != 0 {
return nil, fmt.Errorf("failed to get user token: %s", resp.ErrorDescription)
}
return resp, nil
}
func getUserInfo(tokenType string, token string) (*UserInfoResponse, error) {
headers := http.Header{}
headers.Set("Content-Type", "application/json")
switch tokenType {
case "Bearer":
headers.Set("Authorization", fmt.Sprintf("Bearer %s", token))
}
resp, err := SendRequest[UserInfoResponse](getUserInfoUri, http.MethodGet, headers, nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to get user info: %w", err)
}
if resp.Code != 0 {
return nil, fmt.Errorf("failed to get user info: %s", resp.Msg)
}
return resp, nil
}
func (d *Driver) Delete(ctx context.Context, ids ...string) error {
return d.accountService.OnRemoveUsers(ctx, ids...)
}
+87
View File
@@ -0,0 +1,87 @@
package mcp_server
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
)
var client = http.Client{}
type Position string
const (
PositionHeader Position = "header"
PositionBody Position = "body"
PositionQuery Position = "query"
PositionPath Position = "path"
)
type ContentType string
const (
ContentTypeJSON ContentType = "application/json"
ContentTypeXML ContentType = "application/xml"
ContentTypeHTML ContentType = "text/html"
ContentTypeText ContentType = "text/plain"
ContentTypeForm ContentType = "application/x-www-form-urlencoded"
ContentTypeFile ContentType = "multipart/form-data"
)
func NewParam(position Position, required bool, description string) *Param {
return &Param{position: position, required: required, description: description}
}
type Param struct {
position Position
required bool
description string
}
func (p *Param) Description() string {
return p.description
}
func (p *Param) Required() bool {
return p.required
}
type BodyParam struct {
contentType ContentType
params map[string]interface{}
}
func NewBodyParam(contentType string) *BodyParam {
t := ContentType(contentType)
if t == "" {
t = ContentTypeJSON
}
return &BodyParam{contentType: t}
}
func (p *BodyParam) Set(k string, v interface{}) {
if p.params == nil {
p.params = make(map[string]interface{})
}
p.params[k] = v
}
func (p *BodyParam) Encode() (string, error) {
switch p.contentType {
case ContentTypeJSON:
data, err := json.Marshal(p.params)
if err != nil {
return "", fmt.Errorf("body param encode error: %w", err)
}
return string(data), nil
case ContentTypeForm, ContentTypeFile:
data := url.Values{}
for k, v := range p.params {
data.Set(k, fmt.Sprintf("%v", v))
}
return data.Encode(), nil
default:
return "", fmt.Errorf("unsupported content type: %s", p.contentType)
}
}
+217 -34
View File
@@ -4,69 +4,137 @@ import (
"fmt"
"net/http"
"strings"
"sync"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mitchellh/mapstructure"
"github.com/mark3labs/mcp-go/server"
"github.com/eolinker/eosc"
)
var (
mcpServer = NewServer()
ServiceBasePath = "mcp/service"
GlobalBasePath = "mcp/global"
AppBasePath = "mcp/app"
)
func NewServer() *Server {
return &Server{
sseServers: eosc.BuildUntyped[string, *server.SSEServer](),
servers: make(map[string]*Handler),
}
}
type Server struct {
sseServers eosc.Untyped[string, *server.SSEServer]
servers map[string]*Handler
locker sync.RWMutex
}
func (s *Server) Set(path string, sseServer *server.SSEServer) {
s.sseServers.Set(path, sseServer)
type Handler struct {
*server.MCPServer
handlers map[string]http.Handler
}
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)
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
r.URL.Path = strings.TrimSuffix(r.URL.Path, "/")
if strings.HasSuffix(r.URL.Path, "/mcp") {
h.handlers["openapi-stream"].ServeHTTP(w, r)
return
}
if strings.HasPrefix(r.URL.Path, "/api") {
h.handlers["api-sse"].ServeHTTP(w, r)
return
} else if strings.HasPrefix(r.URL.Path, "/openapi") {
h.handlers["openapi-sse"].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)
func (s *Server) Set(id string, ser *server.MCPServer) {
s.locker.Lock()
defer s.locker.Unlock()
tmp := &Handler{
MCPServer: ser,
handlers: make(map[string]http.Handler),
}
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)))
tmp.handlers["api-sse"] = server.NewSSEServer(ser, server.WithStaticBasePath(fmt.Sprintf("/api/v1/%s/%s", ServiceBasePath, id)))
tmp.handlers["openapi-sse"] = server.NewSSEServer(ser, server.WithStaticBasePath(fmt.Sprintf("/openapi/v1/%s/%s", ServiceBasePath, id)))
tmp.handlers["openapi-stream"] = server.NewStreamableHTTPServer(ser, server.WithEndpointPath(fmt.Sprintf("/openapi/v1/%s/%s/mcp", ServiceBasePath, id)))
s.servers[id] = tmp
}
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 (s *Server) Del(id string) {
s.locker.Lock()
defer s.locker.Unlock()
delete(s.servers, id)
}
func (s *Server) Get(id string) (*Handler, bool) {
s.locker.RLock()
defer s.locker.RUnlock()
ser, has := s.servers[id]
if !has {
return nil, false
}
m := &Handler{
MCPServer: ser.MCPServer,
handlers: make(map[string]http.Handler),
}
for k, v := range ser.handlers {
m.handlers[k] = v
}
return m, true
}
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
sid, err := genPath(r.URL.Path)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
return
}
ser, has := s.Get(sid)
if has {
ser.ServeHTTP(w, r)
return
}
http.NotFound(w, r)
return
}
func genPath(path string) (sid string, err error) {
path = strings.TrimSuffix(path, "/")
ps := strings.Split(path, "/")
if len(ps) < 2 {
err = fmt.Errorf("invalid path: %s", path)
return
}
sid = ps[len(ps)-2]
return
}
func SetServer(sid string, name string, version string, tools ...ITool) {
ser, has := mcpServer.Get(sid)
if !has {
mcpServer.Set(sid, server.NewMCPServer(name, version, server.WithToolCapabilities(true)))
ser, has = mcpServer.Get(sid)
if !has {
return
}
}
ts := make([]server.ServerTool, 0, len(tools))
for _, tool := range tools {
ts = append(ts, tool.Tool())
}
ser.SetTools(ts...)
}
func DelServer(sid string) {
mcpServer.Del(sid)
}
func ServeHTTP(w http.ResponseWriter, r *http.Request) {
@@ -76,3 +144,118 @@ func ServeHTTP(w http.ResponseWriter, r *http.Request) {
func DefaultMCPServer() *Server {
return mcpServer
}
func SetServerByOpenapi(sid, name, version, content string) error {
mcpInfo, err := ConvertMCPFromOpenAPI3Data([]byte(content))
if err != nil {
return fmt.Errorf("convert mcp from openapi3 data error: %w", err)
}
tools := make([]ITool, 0, len(mcpInfo.Apis))
for _, a := range mcpInfo.Apis {
toolOptions := make([]mcp.ToolOption, 0, len(a.Params)+2)
toolOptions = append(toolOptions, mcp.WithDescription(a.Description))
params := make(map[string]*Param)
for _, v := range a.Params {
if v.In == "header" && v.Name == "Authorization" {
continue
}
params[v.Name] = NewParam(Position(v.In), v.Required, v.Description)
options := make([]mcp.PropertyOption, 0, 2)
if v.Required {
options = append(options, mcp.Required())
}
options = append(options, mcp.Description(v.Description))
toolOptions = append(toolOptions, mcp.WithString(v.Name, options...))
}
if a.Body != nil {
type Schema struct {
Type string `mapstructure:"type"`
Properties map[string]interface{} `mapstructure:"properties"`
Items interface{} `mapstructure:"items"`
Required interface{} `mapstructure:"required"`
}
var tmp Schema
err = mapstructure.Decode(a.Body, &tmp)
if err != nil {
return err
}
required := map[string]struct{}{}
switch t := tmp.Required.(type) {
case []interface{}:
for _, v := range t {
i, ok := v.(string)
if !ok {
continue
}
required[i] = struct{}{}
}
}
for k, v := range tmp.Properties {
description := ""
typ := "string"
isRequired := false
if _, ok := required[k]; ok {
isRequired = true
}
var props map[string]interface{}
var items interface{}
switch t := v.(type) {
case map[string]interface{}:
if m, ok := t["type"]; ok {
n, ok := m.(string)
if ok {
typ = n
}
}
if m, ok := t["description"]; ok {
n, ok := m.(string)
if ok {
description = n
}
}
switch typ {
case "array":
if m, ok := t["items"]; ok {
items = m
}
case "object":
if m, ok := t["properties"]; ok {
n, ok := m.(map[string]interface{})
if ok {
props = n
}
}
}
}
params[k] = NewParam(PositionBody, isRequired, description)
options := make([]mcp.PropertyOption, 0, 3)
options = append(options, mcp.Description(description))
if props != nil {
options = append(options, mcp.Properties(props))
}
if items != nil {
options = append(options, mcp.Items(items))
}
switch typ {
case "string":
toolOptions = append(toolOptions, mcp.WithString(k, options...))
case "integer", "number", "float":
toolOptions = append(toolOptions, mcp.WithNumber(k, options...))
case "boolean":
toolOptions = append(toolOptions, mcp.WithBoolean(k, options...))
case "array":
toolOptions = append(toolOptions, mcp.WithArray(k, options...))
case "object":
toolOptions = append(toolOptions, mcp.WithObject(k, options...))
default:
return fmt.Errorf("unsupported type: %s", typ)
}
}
}
tools = append(tools, NewTool(a.Summary, a.Path, a.Method, a.ContentType, params, toolOptions...))
}
SetServer(sid, name, version, tools...)
return nil
}
+45 -61
View File
@@ -2,7 +2,6 @@ package mcp_server
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
@@ -16,36 +15,38 @@ import (
)
type ITool interface {
RegisterMCP(s *server.MCPServer)
Tool() server.ServerTool
}
const (
MCPBody = "Body"
MCPHeader = "Header"
MCPQuery = "Query"
MCPPath = "Path"
)
type Tool struct {
name string
url string
method string
contentType string
params map[string]*Param
opts []mcp.ToolOption
}
func NewTool(name string, uri string, method string, contentType string, opts ...mcp.ToolOption) ITool {
func (t *Tool) Tool() server.ServerTool {
return server.ServerTool{
Tool: mcp.NewTool(t.name, t.opts...),
Handler: generateInvokeTool(t.url, t.method, t.contentType, t.params),
}
}
func NewTool(name string, uri string, method string, contentType string, params map[string]*Param, opts ...mcp.ToolOption) ITool {
return &Tool{
name: name,
url: uri,
method: method,
contentType: contentType,
params: params,
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) {
func generateInvokeTool(path string, method string, contentType string, params map[string]*Param) func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
invokeAddress := utils.GatewayInvoke(ctx)
if invokeAddress == "" {
return nil, fmt.Errorf("invoke address is empty")
@@ -58,73 +59,58 @@ func (t *Tool) RegisterMCP(s *server.MCPServer) {
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()
bodyParam := NewBodyParam(contentType)
for k, p := range params {
vv, ok := request.GetArguments()[k]
if !ok && p.required {
return nil, fmt.Errorf("param %s is required", k)
}
if p.position == PositionHeader || p.position == PositionQuery || p.position == PositionPath {
v, ok := vv.(string)
if !ok || v == "<nil>" {
if p.required {
return nil, fmt.Errorf("param %s is required", k)
}
default:
tmp, _ := json.Marshal(a)
body = string(tmp)
continue
}
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)
switch p.position {
case PositionPath:
path = strings.ReplaceAll(path, "{"+k+"}", fmt.Sprintf("%v", vv))
case PositionQuery:
queries.Set(k, fmt.Sprintf("%v", vv))
case PositionHeader:
headers[k] = fmt.Sprintf("%v", vv)
case PositionBody:
if vv == nil {
continue
}
bodyParam.Set(k, vv)
}
}
bodyData, err := bodyParam.Encode()
if err != nil {
return nil, err
}
u.Path = path
u.RawQuery = queries.Encode()
req, err := http.NewRequest(t.method, u.String(), strings.NewReader(body))
req, err := http.NewRequest(method, u.String(), strings.NewReader(bodyData))
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)
if contentType != "" {
req.Header.Set("Content-Type", contentType)
}
apikey := utils.Label(ctx, "apikey")
if apikey != "" {
req.Header.Set("Authorization", utils.Md5(apikey))
req.Header.Set("Authorization", apikey)
}
resp, err := client.Do(req)
@@ -141,7 +127,5 @@ func (t *Tool) RegisterMCP(s *server.MCPServer) {
}
return mcp.NewToolResultText(string(d)), nil
})
}
}
var client = http.Client{}
+32 -12
View File
@@ -8,6 +8,8 @@ import (
"net/http"
"strings"
service_overview "github.com/APIParkLab/APIPark/service/service-overview"
ai_provider_local "github.com/APIParkLab/APIPark/ai-provider/local"
model_runtime "github.com/APIParkLab/APIPark/ai-provider/model-runtime"
@@ -35,12 +37,13 @@ var (
)
type imlAPIModule struct {
serviceService service.IServiceService `autowired:""`
apiDocService api_doc.IAPIDocService `autowired:""`
aiAPIService ai_api.IAPIService `autowired:""`
aiModelService ai_model.IProviderModelService `autowired:""`
apiService api.IAPIService `autowired:""`
transaction store.ITransaction `autowired:""`
serviceService service.IServiceService `autowired:""`
serviceOverviewService service_overview.IOverviewService `autowired:""`
apiDocService api_doc.IAPIDocService `autowired:""`
aiAPIService ai_api.IAPIService `autowired:""`
aiModelService ai_model.IProviderModelService `autowired:""`
apiService api.IAPIService `autowired:""`
transaction store.ITransaction `autowired:""`
}
func (i *imlAPIModule) getAPIDoc(ctx context.Context, serviceId string) (*openapi3.T, error) {
@@ -77,9 +80,17 @@ func (i *imlAPIModule) updateAPIDoc(ctx context.Context, serviceId, serviceName,
if err != nil {
return err
}
return i.apiDocService.UpdateDoc(ctx, serviceId, &api_doc.UpdateDoc{
ID: uuid.New().String(),
Content: string(result),
return i.transaction.Transaction(ctx, func(ctx context.Context) error {
count, err := i.apiDocService.UpdateDoc(ctx, serviceId, &api_doc.UpdateDoc{
ID: uuid.New().String(),
Content: string(result),
})
if err != nil {
return fmt.Errorf("update api doc error:%v", err)
}
return i.serviceOverviewService.Update(ctx, serviceId, &service_overview.Update{
ApiCount: &count,
})
})
}
@@ -93,10 +104,19 @@ func (i *imlAPIModule) deleteAPIDoc(ctx context.Context, serviceId string, path
if err != nil {
return err
}
return i.apiDocService.UpdateDoc(ctx, serviceId, &api_doc.UpdateDoc{
ID: uuid.New().String(),
Content: string(result),
return i.transaction.Transaction(ctx, func(ctx context.Context) error {
count, err := i.apiDocService.UpdateDoc(ctx, serviceId, &api_doc.UpdateDoc{
ID: uuid.New().String(),
Content: string(result),
})
if err != nil {
return fmt.Errorf("update api doc error:%v", err)
}
return i.serviceOverviewService.Update(ctx, serviceId, &service_overview.Update{
ApiCount: &count,
})
})
}
func (i *imlAPIModule) Create(ctx context.Context, serviceId string, input *ai_api_dto.CreateAPI) error {
+24 -15
View File
@@ -7,7 +7,7 @@ import (
func genOpenAPI3Template(title string, description string) *openapi3.T {
result := new(openapi3.T)
result.OpenAPI = "3.1.0"
result.OpenAPI = "3.0.1"
result.Info = &openapi3.Info{
Title: title,
Description: description,
@@ -37,6 +37,8 @@ func genOperation(summary string, description string, variables []*ai_api_dto.Ai
func genRequestBody(variables []*ai_api_dto.AiPromptVariable) *openapi3.RequestBodyRef {
requestBody := openapi3.NewRequestBody()
requestBody.Description = "Request body"
requestBody.Required = true
requestBody.Content = openapi3.NewContentWithSchema(genRequestBodySchema(variables), []string{"application/json"})
return &openapi3.RequestBodyRef{
Value: requestBody,
@@ -55,10 +57,14 @@ func genResponse() *openapi3.ResponseRef {
func genRequestBodySchema(variables []*ai_api_dto.AiPromptVariable) *openapi3.Schema {
result := openapi3.NewObjectSchema()
required := make([]string, 0, 2)
required = append(required, "messages")
if len(variables) > 0 {
result.WithProperty("variables", genVariableSchema(variables))
result.WithRequired([]string{"variables", "messages"})
required = append(required, "variables")
}
result.WithRequired(required)
streamSchema := openapi3.NewBoolSchema()
streamSchema.Title = "stream"
streamSchema.Description = "Whether to stream the response"
@@ -129,6 +135,8 @@ func genMessageSchema() *openapi3.Schema {
"role": roleSchema,
"content": contentSchema,
})
result.WithRequired([]string{"role", "content"})
return result
}
@@ -137,20 +145,21 @@ func genMessagesSchema() *openapi3.Schema {
result.Title = "Messages"
result.Description = "Chat Messages"
result.Items = openapi3.NewSchemaRef("#/components/schemas/Message", messageSchema)
result.Required = []string{"content", "role"}
return result
}
func genResponseSchema() *openapi3.Schema {
result := openapi3.NewObjectSchema()
result.Description = "Response from the server"
// 创建 choices 数组
choicesSchema := openapi3.NewArraySchema()
choiceItemSchema := openapi3.NewObjectSchema()
// choice 中的 message 字段
choiceItemSchema.WithPropertyRef("message", messageSchemaRef)
// finish_reason 字段
finishReasonSchema := openapi3.NewStringSchema().WithEnum(
"stop",
@@ -160,41 +169,41 @@ func genResponseSchema() *openapi3.Schema {
"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
}
+15 -11
View File
@@ -128,17 +128,19 @@ func (i *imlProviderModelModule) DeleteProviderModel(ctx *gin.Context, provider
if err := i.providerModelService.Delete(ctx, id); err != nil {
return err
}
err = i.syncGateway(ctx, cluster.DefaultClusterID, []*gateway.DynamicRelease{
{
BasicItem: &gateway.BasicItem{
ID: fmt.Sprintf("%s#%s", provider, modelInfo.Name),
Resource: "ai-model",
if p.GetModelConfig().AccessConfigurationStatus {
err = i.syncGateway(ctx, cluster.DefaultClusterID, []*gateway.DynamicRelease{
{
BasicItem: &gateway.BasicItem{
ID: fmt.Sprintf("%s$%s", provider, modelInfo.Name),
Resource: "ai-model",
},
Attr: nil,
},
Attr: nil,
},
}, false)
if err != nil {
return err
}, false)
if err != nil {
return err
}
}
p.RemoveModel(id)
@@ -200,7 +202,9 @@ func (i *imlProviderModelModule) AddProviderModel(ctx *gin.Context, provider str
}
func newModel(provider string, model string, config string) *gateway.DynamicRelease {
if config == "" {
config = "{}"
}
return &gateway.DynamicRelease{
BasicItem: &gateway.BasicItem{
ID: fmt.Sprintf("%s$%s", provider, model),
+2 -1
View File
@@ -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
+21 -7
View File
@@ -3,6 +3,11 @@ package api_doc
import (
"context"
"errors"
"fmt"
"github.com/eolinker/go-common/store"
service_overview "github.com/APIParkLab/APIPark/service/service-overview"
api_doc_dto "github.com/APIParkLab/APIPark/module/api-doc/dto"
api_doc "github.com/APIParkLab/APIPark/service/api-doc"
@@ -16,8 +21,10 @@ import (
var _ IAPIDocModule = (*imlAPIDocModule)(nil)
type imlAPIDocModule struct {
apiDocService api_doc.IAPIDocService `autowired:""`
serviceService service.IServiceService `autowired:""`
apiDocService api_doc.IAPIDocService `autowired:""`
serviceService service.IServiceService `autowired:""`
serviceOverviewService service_overview.IOverviewService `autowired:""`
transaction store.ITransaction `autowired:""`
}
func (i *imlAPIDocModule) UpdateDoc(ctx context.Context, serviceId string, input *api_doc_dto.UpdateDoc) (*api_doc_dto.ApiDocDetail, error) {
@@ -29,11 +36,18 @@ func (i *imlAPIDocModule) UpdateDoc(ctx context.Context, serviceId string, input
input.Id = uuid.New().String()
}
// 每个API加上前缀
err = i.apiDocService.UpdateDoc(ctx, serviceId, &api_doc.UpdateDoc{
ID: input.Id,
Content: input.Content,
Prefix: info.Prefix,
err = i.transaction.Transaction(ctx, func(ctx context.Context) error {
count, err := i.apiDocService.UpdateDoc(ctx, serviceId, &api_doc.UpdateDoc{
ID: input.Id,
Content: input.Content,
Prefix: info.Prefix,
})
if err != nil {
return fmt.Errorf("update api doc error:%v", err)
}
return i.serviceOverviewService.Update(ctx, serviceId, &service_overview.Update{
ApiCount: &count,
})
})
if err != nil {
return nil, err
@@ -33,7 +33,8 @@ type IAuthorizationModule interface {
// Info 获取项目鉴权详情
Info(ctx context.Context, appId string, aid string) (*application_authorization_dto.Authorization, error)
CheckAPIKeyAuthorization(ctx context.Context, serviceId string, apikey string) (bool, error)
CheckAPIKeyAuthorizationByService(ctx context.Context, serviceId string, apikey string) (bool, error)
CheckAPIKeyAuthorizationByApp(ctx context.Context, appId string, apikey string) (bool, error)
//ExportAll(ctx context.Context) ([]*application_authorization_dto.ExportAuthorization, error)
}
+21 -1
View File
@@ -44,7 +44,27 @@ type imlAuthorizationModule struct {
transaction store.ITransaction `autowired:""`
}
func (i *imlAuthorizationModule) CheckAPIKeyAuthorization(ctx context.Context, serviceId string, apikey string) (bool, error) {
func (i *imlAuthorizationModule) CheckAPIKeyAuthorizationByApp(ctx context.Context, appId string, apikey string) (bool, error) {
authorizations, err := i.authorizationService.ListByApp(ctx, appId)
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) CheckAPIKeyAuthorizationByService(ctx context.Context, serviceId string, apikey string) (bool, error) {
list, err := i.subscribeService.ListBySubscribeStatus(ctx, serviceId, subscribe.ApplyStatusSubscribe)
if err != nil {
return false, err
+35 -32
View File
@@ -9,6 +9,8 @@ import (
"strings"
"time"
service_overview "github.com/APIParkLab/APIPark/service/service-overview"
mcp_server "github.com/APIParkLab/APIPark/mcp-server"
"github.com/APIParkLab/APIPark/module/monitor/driver"
@@ -58,21 +60,22 @@ var (
)
type imlCatalogueModule struct {
catalogueService catalogue.ICatalogueService `autowired:""`
apiService api.IAPIService `autowired:""`
apiDocService api_doc.IAPIDocService `autowired:""`
serviceService service.IServiceService `autowired:""`
serviceTagService service_tag.ITagService `autowired:""`
serviceDocService service_doc.IDocService `autowired:""`
tagService tag.ITagService `autowired:""`
releaseService release.IReleaseService `autowired:""`
subscribeService subscribe.ISubscribeService `autowired:""`
subscribeApplyService subscribe.ISubscribeApplyService `autowired:""`
transaction store.ITransaction `autowired:""`
clusterService cluster.IClusterService `autowired:""`
settingService setting.ISettingService `autowired:""`
monitorService monitor.IMonitorService `autowired:""`
root *Root
catalogueService catalogue.ICatalogueService `autowired:""`
apiService api.IAPIService `autowired:""`
apiDocService api_doc.IAPIDocService `autowired:""`
serviceService service.IServiceService `autowired:""`
serviceOverviewService service_overview.IOverviewService `autowired:""`
serviceTagService service_tag.ITagService `autowired:""`
serviceDocService service_doc.IDocService `autowired:""`
tagService tag.ITagService `autowired:""`
releaseService release.IReleaseService `autowired:""`
subscribeService subscribe.ISubscribeService `autowired:""`
subscribeApplyService subscribe.ISubscribeApplyService `autowired:""`
transaction store.ITransaction `autowired:""`
clusterService cluster.IClusterService `autowired:""`
settingService setting.ISettingService `autowired:""`
monitorService monitor.IMonitorService `autowired:""`
root *Root
}
func (i *imlCatalogueModule) DefaultCatalogue(ctx context.Context) (*catalogue_dto.Catalogue, error) {
@@ -447,27 +450,26 @@ func (i *imlCatalogueModule) Services(ctx context.Context, keyword string) ([]*c
if err != nil {
return nil, err
}
serviceIds := utils.SliceToSlice(items, func(i *service.Service) string {
return i.Id
}, func(s *service.Service) bool {
// 未发布的不给展示
_, err = i.releaseService.GetRunning(ctx, s.Id)
return err == nil
})
overviewMap, err := i.serviceOverviewService.Map(ctx, serviceIds...)
if err != nil {
return nil, err
}
serviceIds = utils.SliceToSlice(serviceIds, func(s string) string {
return s
}, func(s string) bool {
// 只展示已发布的服务
if info, ok := overviewMap[s]; ok && info.IsReleased {
return true
}
return false
})
if len(serviceIds) < 1 {
return nil, nil
}
commits, err := i.releaseService.GetRunningApiDocCommits(ctx, serviceIds...)
if err != nil {
return nil, err
}
apiCountMap, err := i.apiDocService.LatestAPICountByCommits(ctx, commits...)
if err != nil {
return nil, err
}
subscriberCountMap, err := i.subscribeService.CountMapByService(ctx, subscribe.ApplyStatusSubscribe, serviceIds...)
if err != nil {
return nil, err
@@ -479,8 +481,9 @@ func (i *imlCatalogueModule) Services(ctx context.Context, keyword string) ([]*c
result := make([]*catalogue_dto.ServiceItem, 0, len(items))
for _, v := range items {
apiNum, ok := apiCountMap[v.Id]
if !ok || apiNum < 1 {
ov, ok := overviewMap[v.Id]
if !ok || ov.ReleaseApiCount < 1 {
continue
}
@@ -489,8 +492,8 @@ func (i *imlCatalogueModule) Services(ctx context.Context, keyword string) ([]*c
Name: v.Name,
Tags: auto.List(serviceTagMap[v.Id]),
Catalogue: auto.UUID(v.Catalogue),
ApiNum: apiNum,
SubscriberNum: subscriberCountMap[v.Id],
ApiNum: ov.ReleaseApiCount,
Description: v.Description,
Logo: v.Logo,
EnableMCP: v.EnableMCP,
+179 -4
View File
@@ -4,9 +4,14 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/eolinker/go-common/server"
log_driver "github.com/APIParkLab/APIPark/log-driver"
"github.com/eolinker/go-common/register"
"github.com/eolinker/go-common/utils"
"github.com/APIParkLab/APIPark/gateway"
@@ -16,11 +21,11 @@ import (
"github.com/APIParkLab/APIPark/service/cluster"
"github.com/eolinker/go-common/auto"
log_dto "github.com/APIParkLab/APIPark/module/log/dto"
"github.com/APIParkLab/APIPark/service/log"
eosc_log "github.com/eolinker/eosc/log"
log_print "github.com/eolinker/eosc/log"
"github.com/eolinker/go-common/auto"
)
var _ ILogModule = (*imlLogModule)(nil)
@@ -28,7 +33,10 @@ var _ ILogModule = (*imlLogModule)(nil)
type imlLogModule struct {
service log.ILogService `autowired:""`
clusterService cluster.IClusterService `autowired:""`
transaction store.ITransaction `autowired:""`
transaction store.ITransaction `autowired:""`
//scheduleCtx context.Context
scheduleCancel context.CancelFunc
}
var labels = map[string]string{
@@ -54,6 +62,7 @@ var logFormatter = map[string]interface{}{
"$proxy_host",
"$proxy_header",
"$proxy_addr",
"$response_header",
"$response_headers",
"$status",
"$content_type",
@@ -70,6 +79,11 @@ var logFormatter = map[string]interface{}{
"$authorization",
"$response_body",
"$proxy_response_body",
"$ai_provider",
"$ai_model",
"$ai_model_input_token",
"$ai_model_output_token",
"$ai_model_total_token",
},
}
@@ -135,6 +149,11 @@ func (i *imlLogModule) Save(ctx context.Context, driver string, input *log_dto.S
return err
}
log_driver.SetDriver(driver, d)
newCtx, cancel := context.WithCancel(context.Background())
newCtx = utils.SetUserId(newCtx, "admin")
i.scheduleCancel()
i.scheduleCancel = cancel
i.scheduleUpdateLogRecord(newCtx)
return nil
})
}
@@ -164,8 +183,15 @@ func (i *imlLogModule) Get(ctx context.Context, driver string) (*log_dto.LogSour
}, nil
}
func (i *imlLogModule) OnComplete() {
func (i *imlLogModule) OnInit() {
register.Handle(func(v server.Server) {
ctx, cancel := context.WithCancel(context.Background())
ctx = utils.SetUserId(ctx, "admin")
//i.scheduleCtx = ctx
i.scheduleCancel = cancel
i.scheduleUpdateLogRecord(ctx)
})
}
func (i *imlLogModule) initGateway(ctx context.Context, clusterId string, clientDriver gateway.IClientDriver) error {
@@ -222,3 +248,152 @@ func (i *imlLogModule) initGateway(ctx context.Context, clusterId string, client
return nil
}
const (
oneSecond = 1
oneMinute = 60
oneHour = 60 * oneMinute
oneDay = 24 * oneHour
)
// 定时更新历史记录
func (i *imlLogModule) scheduleUpdateLogRecord(ctx context.Context) {
driver, has := log_driver.GetDriver("loki")
if !has {
eosc_log.Error("driver loki not found")
return
}
info, err := i.service.GetLogSource(ctx, "loki")
if err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
eosc_log.Errorf("get log source loki error: %s", err)
return
}
return
}
now := time.Now()
before90Days := now.Add(-7 * 24 * time.Hour)
beginTime := before90Days
if info.LastPullTime.After(before90Days) {
before90Days = info.LastPullTime
}
pauseTime := now
historyFinish := false
go func() {
eosc_log.Infof("start update history log record,start time: %s", beginTime.Format("2006-01-02 15:04:05"))
ticket := time.NewTicker(1 * time.Minute)
defer ticket.Stop()
for {
now = time.Now()
select {
case <-ctx.Done():
return
case <-ticket.C:
switch {
case now.Sub(beginTime) > oneDay:
endTime := beginTime.Add(oneDay)
err = i.updateLogRecord(ctx, driver, beginTime, endTime)
if err != nil {
eosc_log.Errorf("update log record error: %s", err)
continue
}
err = i.service.UpdateLogSource(ctx, "loki", &log.Save{
LastPullTime: &endTime,
})
if err != nil {
eosc_log.Errorf("update log source error: %s", err)
continue
}
beginTime = endTime
case now.Sub(pauseTime) <= oneDay:
endTime := pauseTime
err = i.updateLogRecord(ctx, driver, beginTime, endTime)
if err != nil {
eosc_log.Errorf("update log record error: %s", err)
historyFinish = true
return
}
historyFinish = true
err = i.service.UpdateLogSource(ctx, "loki", &log.Save{
LastPullTime: &endTime,
})
if err != nil {
eosc_log.Errorf("update log source error: %s", err)
return
}
eosc_log.Infof("update log record finish")
return
}
}
}
}()
go func() {
eosc_log.Infof("start update running log record,start time: %s", pauseTime.Format("2006-01-02 15:04:05"))
ticket := time.NewTicker(10 * time.Second)
defer ticket.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticket.C:
end := time.Now()
start := end.Add(-1 * time.Minute)
err = i.updateLogRecord(ctx, driver, start, end)
if err != nil {
eosc_log.Errorf("update log record error: %s", err)
continue
}
if historyFinish {
err = i.service.UpdateLogSource(ctx, "loki", &log.Save{
LastPullTime: &end,
})
if err != nil {
eosc_log.Errorf("update log source error: %s", err)
continue
}
}
}
}
}()
}
func (i *imlLogModule) updateLogRecord(ctx context.Context, driver log_driver.ILogDriver, start, end time.Time) error {
c, err := i.clusterService.Get(ctx, cluster.DefaultClusterID)
if err != nil {
return fmt.Errorf("cluster %s not found", cluster.DefaultClusterID)
}
logs, err := driver.LogRecords(c.Cluster, start, end)
if err != nil {
return fmt.Errorf("get log records error: %s", err)
}
for _, l := range logs {
err = i.service.InsertLog(ctx, "loki", &log.InsertLog{
ID: l.ID,
Driver: "loki",
Strategy: l.Strategy,
API: l.API,
Service: l.Service,
Method: l.Method,
Url: l.Url,
RemoteIP: l.RemoteIP,
Consumer: l.Consumer,
Authorization: l.Authorization,
InputToken: l.InputToken,
OutputToken: l.OutputToken,
TotalToken: l.TotalToken,
AIProvider: l.AIProvider,
AIModel: l.AIModel,
StatusCode: l.StatusCode,
ResponseTime: l.ResponseTime,
Traffic: l.Traffic,
RecordTime: l.RecordTime,
})
if err != nil {
eosc_log.Errorf("insert log record error: %s,log id: %s", err, l.ID)
continue
}
}
return nil
}
+86 -115
View File
@@ -10,7 +10,6 @@ import (
"net/url"
"strconv"
"strings"
"time"
"github.com/APIParkLab/APIPark/service/subscribe"
@@ -47,18 +46,43 @@ type imlMcpModule struct {
releaseService release.IReleaseService `autowired:""`
}
func (i *imlMcpModule) subscribeServiceIds(ctx context.Context, appId string) ([]string, error) {
subscribes, err := i.subscriberService.SubscriptionsByApplication(ctx, appId)
if err != nil {
return nil, fmt.Errorf("get subscriber error: %w,app id is %s", err, appId)
}
serviceIds := utils.SliceToSlice(subscribes, func(s *subscribe.Subscribe) string {
return s.Service
}, func(s *subscribe.Subscribe) bool {
return s.ApplyStatus == subscribe.ApplyStatusSubscribe
})
if len(serviceIds) == 0 {
return nil, fmt.Errorf("no subscriber found,app id is %s", appId)
}
return serviceIds, nil
}
func (i *imlMcpModule) Services(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
keyword, _ := req.Params.Arguments["keyword"].(string)
list, err := i.serviceService.Search(ctx, keyword, map[string]interface{}{
keyword, _ := req.GetArguments()["keyword"].(string)
appId := utils.Label(ctx, "app")
condition := map[string]interface{}{
"as_server": true,
}, "update_at desc")
}
if appId != "" {
serviceIds, err := i.subscribeServiceIds(ctx, appId)
if err != nil {
return nil, fmt.Errorf("get subscriber service ids error: %w,app id is %s", err, appId)
}
condition["uuid"] = serviceIds
}
list, err := i.serviceService.Search(ctx, keyword, condition, "update_at desc")
if err != nil {
return nil, fmt.Errorf("search service error: %w", err)
}
if len(list) == 0 {
list, err = i.serviceService.Search(ctx, "", map[string]interface{}{
"as_server": true,
}, "update_at desc")
list, err = i.serviceService.Search(ctx, "", condition, "update_at desc")
if err != nil {
return nil, fmt.Errorf("search service error: %w", err)
}
@@ -116,116 +140,57 @@ func (i *imlMcpModule) Services(ctx context.Context, req mcp.CallToolRequest) (*
}
func (i *imlMcpModule) Apps(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
keyword := req.Params.Arguments["keyword"].(string)
condition := make(map[string]interface{})
condition["as_app"] = true
list, err := i.serviceService.Search(ctx, keyword, condition, "update_at desc")
if err != nil {
return nil, fmt.Errorf("search service error: %w", err)
}
if len(list) == 0 {
list, err = i.serviceService.Search(ctx, "", condition, "update_at desc")
if err != nil {
return nil, fmt.Errorf("search service error: %w", err)
}
}
data, _ := json.Marshal(utils.SliceToSlice(list, func(s *service.Service) *mcp_dto.App {
return &mcp_dto.App{
Id: s.Id,
Name: s.Name,
Description: s.Name,
CreateTime: s.CreateTime,
UpdateTime: s.UpdateTime,
}
}))
return mcp.NewToolResultText(string(data)), nil
}
func (i *imlMcpModule) APIs(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
serviceId, _ := req.Params.Arguments["service"].(string)
serviceIds := make([]string, 0, 1)
serviceId, _ := req.GetArguments()["service"].(string)
if serviceId == "" {
serviceIds = append(serviceIds, serviceId)
return nil, fmt.Errorf("service id is empty")
}
serviceList, err := i.serviceService.ServiceList(ctx)
s, err := i.serviceService.Get(ctx, serviceId)
if err != nil {
return nil, fmt.Errorf("get service list error: %w", err)
return nil, fmt.Errorf("get service error: %w,service id is %s", err, serviceId)
}
appId := utils.Label(ctx, "app")
if appId != "" {
subscribers, err := i.subscriberService.ListByApplication(ctx, serviceId, appId)
if err != nil {
return nil, fmt.Errorf("get subscriber error: %w,app id is %s", err, appId)
}
if len(subscribers) < 1 || subscribers[0].ApplyStatus != subscribe.ApplyStatusSubscribe {
return nil, fmt.Errorf("no subscriber found,app id is %s", appId)
}
}
result := make([]*mcp_dto.ServiceAPI, 0, len(serviceList))
for _, s := range serviceList {
serviceRelease, err := i.releaseService.GetRunning(ctx, s.Id)
if err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("get service release error: %w,service id is %s", err, s.Id)
}
continue
serviceRelease, err := i.releaseService.GetRunning(ctx, serviceId)
if err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("get service release error: %w,service id is %s", err, s.Id)
}
_, _, apiDocRelease, _, _, err := i.releaseService.GetReleaseInfos(ctx, serviceRelease.UUID)
if err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("get service release info error: %w,service id is %s", err, s.Id)
}
continue
}
commit, err := i.apiDocService.GetDocCommit(ctx, apiDocRelease.Commit)
if err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("get api doc release error: %w,service id is %s", err, s.Id)
}
continue
}
T, err := openapi3Loader.LoadFromData([]byte(commit.Data.Content))
if err != nil {
return nil, fmt.Errorf("load openapi3 error: %w,service id is %s", err, s.Id)
}
result = append(result, &mcp_dto.ServiceAPI{
ServiceID: s.Id,
ServiceName: s.Name,
APIDoc: T,
})
return nil, fmt.Errorf("no service found,service id is %s", serviceId)
}
_, _, apiDocRelease, _, _, err := i.releaseService.GetReleaseInfos(ctx, serviceRelease.UUID)
if err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("get service release info error: %w,service id is %s", err, s.Id)
}
return nil, fmt.Errorf("no service found,service id is %s", serviceId)
}
commit, err := i.apiDocService.GetDocCommit(ctx, apiDocRelease.Commit)
if err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("get api doc release error: %w,service id is %s", err, s.Id)
}
return nil, fmt.Errorf("no service found,service id is %s", serviceId)
}
T, err := openapi3Loader.LoadFromData([]byte(commit.Data.Content))
if err != nil {
return nil, fmt.Errorf("load openapi3 error: %w,service id is %s", err, s.Id)
}
data, _ := json.Marshal(result)
return mcp.NewToolResultText(string(data)), nil
}
func (i *imlMcpModule) SubscriberAuthorizations(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
serviceId, ok := req.Params.Arguments["service"].(string)
if !ok {
return nil, fmt.Errorf("service id is required")
result := &mcp_dto.ServiceAPI{
ServiceID: serviceId,
ServiceName: s.Name,
APIDoc: T,
}
subscribes, err := i.subscriberService.Subscribers(ctx, serviceId, subscribe.ApplyStatusSubscribe)
if err != nil {
return nil, fmt.Errorf("get subscriber error: %w,service id is %s", err, serviceId)
}
appIds := utils.SliceToSlice(subscribes, func(s *subscribe.Subscribe) string {
return s.Application
})
if len(appIds) == 0 {
return nil, fmt.Errorf("no subscriber found,service id is %s", serviceId)
}
list, err := i.appAuthorizationService.ListByApp(ctx, appIds...)
if err != nil {
return nil, fmt.Errorf("get app authorization error: %w,app ids is %s", err, appIds)
}
result := utils.SliceToSlice(list, func(a *application_authorization.Authorization) *mcp_dto.AppAuthorization {
return &mcp_dto.AppAuthorization{
Id: a.UUID,
Name: a.Name,
Position: a.Position,
TokenName: a.TokenName,
Config: a.Config,
}
}, func(a *application_authorization.Authorization) bool {
if a.Type != "apikey" {
return false
}
if a.ExpireTime != 0 && a.ExpireTime < time.Now().Unix() {
return false
}
return true
})
data, _ := json.Marshal(result)
return mcp.NewToolResultText(string(data)), nil
}
@@ -248,18 +213,18 @@ func (i *imlMcpModule) Invoke(ctx context.Context, req mcp.CallToolRequest) (*mc
u.Scheme = "http"
}
path, ok := req.Params.Arguments["path"].(string)
path, ok := req.GetArguments()["path"].(string)
if !ok {
return nil, fmt.Errorf("invalid path")
}
u.Path = fmt.Sprintf("%s/%s", strings.TrimSuffix(u.Path, "/"), strings.TrimPrefix(path, "/"))
method, ok := req.Params.Arguments["method"].(string)
method, ok := req.GetArguments()["method"].(string)
if !ok {
method = "GET"
}
queryParam := url.Values{}
query, ok := req.Params.Arguments["query"].(map[string]interface{})
query, ok := req.GetArguments()["query"].(map[string]interface{})
if ok {
for k, v := range query {
switch v := v.(type) {
@@ -270,7 +235,7 @@ func (i *imlMcpModule) Invoke(ctx context.Context, req mcp.CallToolRequest) (*mc
queryParam.Add(k, value)
}
case float64:
queryParam.Add(k, strconv.FormatFloat(v, 'e', -1, 64))
queryParam.Add(k, strconv.FormatFloat(v, 'f', -1, 64))
default:
return nil, fmt.Errorf("invalid query param type: %T", v)
}
@@ -278,7 +243,7 @@ func (i *imlMcpModule) Invoke(ctx context.Context, req mcp.CallToolRequest) (*mc
}
u.RawQuery = queryParam.Encode()
headerParam := http.Header{}
header, ok := req.Params.Arguments["header"].(map[string]interface{})
header, ok := req.GetArguments()["header"].(map[string]interface{})
if ok {
for k, v := range header {
switch v := v.(type) {
@@ -294,12 +259,12 @@ func (i *imlMcpModule) Invoke(ctx context.Context, req mcp.CallToolRequest) (*mc
}
}
body, ok := req.Params.Arguments["body"].(string)
body, ok := req.GetArguments()["body"].(string)
if !ok {
body = ""
}
contentType, ok := req.Params.Arguments["content-type"].(string)
contentType, ok := req.GetArguments()["content-type"].(string)
if !ok {
contentType = "application/json"
}
@@ -310,8 +275,14 @@ func (i *imlMcpModule) Invoke(ctx context.Context, req mcp.CallToolRequest) (*mc
request.Header = headerParam
request.Header.Set("Content-Type", contentType)
apikey := utils.Label(ctx, "apikey")
if apikey != "" {
request.Header.Set("Authorization", utils.Md5(apikey))
appId := utils.Label(ctx, "app")
if appId == "" {
request.Header.Set("Authorization", utils.Md5(apikey))
} else {
request.Header.Set("Authorization", apikey)
}
}
resp, err := client.Do(request)
-6
View File
@@ -12,13 +12,7 @@ import (
type IMcpModule interface {
// Services 获取服务列表
Services(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error)
// Apps 获取应用列表
Apps(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error)
// APIs 获取API列表
APIs(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error)
// SubscriberAuthorizations 获取订阅者授权
SubscriberAuthorizations(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error)
Invoke(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error)
}
+22
View File
@@ -21,4 +21,26 @@ type IExecutor interface {
InvokeTrend(ctx context.Context, start time.Time, end time.Time, wheres []monitor.MonWhereItem) (*monitor.MonInvokeCountTrend, string, error)
ProxyTrend(ctx context.Context, start time.Time, end time.Time, wheres []monitor.MonWhereItem) (*monitor.MonInvokeCountTrend, string, error)
MessageTrend(ctx context.Context, start time.Time, end time.Time, wheres []monitor.MonWhereItem) (*monitor.MonMessageTrend, string, error)
IBasicOverview
IRestOverview
IAIOverview
}
type IBasicOverview interface {
RequestOverview(ctx context.Context, start time.Time, end time.Time, wheres []monitor.MonWhereItem) ([]time.Time, *monitor.StatusCodeOverview, []*monitor.StatusCodeOverview, error)
TopN(ctx context.Context, start time.Time, end time.Time, limit int, groupBy string, wheres []monitor.MonWhereItem) ([]*monitor.TopN, error)
ConsumerOverview(ctx context.Context, start time.Time, end time.Time, wheres []monitor.MonWhereItem) (int64, map[time.Time]int64, error)
}
type IRestOverview interface {
TrafficOverviewByStatusCode(ctx context.Context, start time.Time, end time.Time, wheres []monitor.MonWhereItem) ([]time.Time, *monitor.StatusCodeOverview, []*monitor.StatusCodeOverview, error)
AvgResponseTimeOverview(ctx context.Context, start time.Time, end time.Time, wheres []monitor.MonWhereItem) ([]time.Time, *monitor.Aggregate, []int64, error)
SumResponseTimeOverview(ctx context.Context, start time.Time, end time.Time, wheres []monitor.MonWhereItem) ([]time.Time, *monitor.Aggregate, []int64, error)
}
type IAIOverview interface {
TokenOverview(ctx context.Context, start time.Time, end time.Time, wheres []monitor.MonWhereItem) ([]time.Time, *monitor.TokenOverview, []*monitor.TokenOverview, error)
}
+331 -6
View File
@@ -3,6 +3,7 @@ package influxdb_v2
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
@@ -23,6 +24,9 @@ import (
"github.com/APIParkLab/APIPark/service/monitor"
)
var _ driver.IAIOverview = (*executor)(nil)
var _ driver.IRestOverview = (*executor)(nil)
func newExecutor(cfg string, fluxQuery flux.IFluxQuery) (driver.IExecutor, error) {
var data InfluxdbV2Config
err := json.Unmarshal([]byte(cfg), &data)
@@ -147,7 +151,7 @@ func (e *executor) MessageTrend(ctx context.Context, start time.Time, end time.T
fieldsConditions := []string{"request", "response"}
dates, groupValues, err := e.fluxQuery.CommonTendency(ctx, e.openApi, newStartTime, end, bucket, "request", filters, fieldsConditions, every, windowOffset)
dates, groupValues, err := e.fluxQuery.CommonTendency(ctx, e.openApi, newStartTime, end, bucket, "request", filters, fieldsConditions, every, windowOffset, flux.SumFn)
if err != nil {
return nil, "", err
}
@@ -166,9 +170,9 @@ func (e *executor) ProxyTrend(ctx context.Context, start time.Time, end time.Tim
filters := formatFilter(wheres)
proxyConditions := []string{"p_total", "p_success", "p_s4xx", "p_s5xx"}
proxyConditions := []string{"p_total", "p_success", "p_s2xx", "p_s4xx", "p_s5xx"}
dates, proxyValues, err := e.fluxQuery.CommonTendency(ctx, e.openApi, newStartTime, end, bucket, "proxy", filters, proxyConditions, every, windowOffset)
dates, proxyValues, err := e.fluxQuery.CommonTendency(ctx, e.openApi, newStartTime, end, bucket, "proxy", filters, proxyConditions, every, windowOffset, flux.SumFn)
if err != nil {
return nil, "", err
}
@@ -200,9 +204,9 @@ func (e *executor) InvokeTrend(ctx context.Context, start time.Time, end time.Ti
newStartTime, every, windowOffset, bucket := getTimeIntervalAndBucket(start, end)
filters := formatFilter(wheres)
requestConditions := []string{"total", "success", "s4xx", "s5xx"}
requestConditions := []string{"total", "success", "2xx", "s4xx", "s5xx"}
dates, requestValues, err := e.fluxQuery.CommonTendency(ctx, e.openApi, newStartTime, end, bucket, "request", filters, requestConditions, every, windowOffset)
dates, requestValues, err := e.fluxQuery.CommonTendency(ctx, e.openApi, newStartTime, end, bucket, "request", filters, requestConditions, every, windowOffset, flux.SumFn)
if err != nil {
return nil, "", err
}
@@ -221,7 +225,7 @@ func (e *executor) InvokeTrend(ctx context.Context, start time.Time, end time.Ti
proxyConditions := []string{"p_total", "p_success"}
_, proxyValues, err := e.fluxQuery.CommonTendency(ctx, e.openApi, newStartTime, end, bucket, "proxy", filters, proxyConditions, every, windowOffset)
_, proxyValues, err := e.fluxQuery.CommonTendency(ctx, e.openApi, newStartTime, end, bucket, "proxy", filters, proxyConditions, every, windowOffset, flux.SumFn)
if err != nil {
return nil, "", err
}
@@ -359,5 +363,326 @@ func (e *executor) CommonStatistics(ctx context.Context, start, end time.Time, g
}
return resultMap, nil
}
func (e *executor) overviewByStatusCode(ctx context.Context, start, end time.Time, table string, wheres []monitor.MonWhereItem, statusCode []string, dataFields []string, fn flux.AggregateFn) ([]time.Time, map[string][]int64, error) {
newStartTime, every, windowOffset, bucket := getTimeIntervalAndBucket(start, end)
var returnDates []time.Time
var returnResult = make(map[string][]int64)
for _, s := range statusCode {
newWheres := make([]monitor.MonWhereItem, 0, len(wheres)+1)
newWheres = append(newWheres, wheres...)
newWheres = append(newWheres, monitor.MonWhereItem{
Key: "status_code",
Operation: "=",
Values: []string{s},
})
dates, result, err := e.fluxQuery.CommonTendency(ctx, e.openApi, newStartTime, end, bucket, table, formatFilter(newWheres), dataFields, every, windowOffset, fn)
if err != nil {
return nil, nil, err
}
if len(dates) > 0 {
returnDates = dates
}
for _, v := range dataFields {
key := fmt.Sprintf("%s_%s", s, v)
if _, ok := returnResult[key]; !ok {
returnResult[key] = make([]int64, 0, len(returnDates))
}
returnResult[key] = append(returnResult[key], result[v]...)
}
}
return returnDates, returnResult, nil
}
func (e *executor) TrafficOverviewByStatusCode(ctx context.Context, start time.Time, end time.Time, wheres []monitor.MonWhereItem) ([]time.Time, *monitor.StatusCodeOverview, []*monitor.StatusCodeOverview, error) {
fieldsConditions := []string{"request", "response"}
statusFilters := []string{"2xx", "4xx", "5xx"}
dates, overview, err := e.overviewByStatusCode(ctx, start, end, "request", wheres, statusFilters, fieldsConditions, flux.SumFn)
if err != nil {
return nil, nil, nil, err
}
s2xxRequest := overview["2xx_request"]
s2xxRequestLen := len(s2xxRequest)
s4xxRequest := overview["4xx_request"]
s4xxRequestLen := len(s4xxRequest)
s5xxRequest := overview["5xx_request"]
s5xxRequestLen := len(s5xxRequest)
s2xxResponse := overview["2xx_response"]
s2xxResponseLen := len(s2xxResponse)
s4xxResponse := overview["4xx_response"]
s4xxResponseLen := len(s4xxResponse)
s5xxResponse := overview["5xx_response"]
s5xxResponseLen := len(s5xxResponse)
totalOverview := new(monitor.StatusCodeOverview)
result := make([]*monitor.StatusCodeOverview, 0, len(dates))
for i := range dates {
r := new(monitor.StatusCodeOverview)
if s2xxRequestLen > i {
r.Status2xx = s2xxRequest[i]
}
if s4xxRequestLen > i {
r.Status4xx = s4xxRequest[i]
}
if s5xxRequestLen > i {
r.Status5xx = s5xxRequest[i]
}
if s2xxResponseLen > i {
r.Status2xx += s2xxResponse[i]
}
if s4xxResponseLen > i {
r.Status4xx += s4xxResponse[i]
}
if s5xxResponseLen > i {
r.Status5xx += s5xxResponse[i]
}
r.StatusTotal += r.Status2xx + r.Status4xx + r.Status5xx
totalOverview.Status2xx += r.Status2xx
totalOverview.Status4xx += r.Status4xx
totalOverview.Status5xx += r.Status5xx
totalOverview.StatusTotal += r.StatusTotal
result = append(result, r)
}
return dates, totalOverview, result, nil
}
func (e *executor) aggregateSummary(ctx context.Context, start time.Time, end time.Time, measurement string, bucket string, filters string, fields []string) (map[string]*monitor.Aggregate, error) {
if len(fields) == 0 {
return nil, fmt.Errorf("fields is empty")
}
maxFields := make([]string, 0, len(fields))
minFields := make([]string, 0, len(fields))
avgFields := make([]string, 0, len(fields))
for _, field := range fields {
maxFields = append(maxFields, field+"_max")
minFields = append(minFields, field+"_min")
avgFields = append(avgFields, field+"_avg")
}
maxRes, err := e.fluxQuery.CommonQueryOnce(ctx, e.openApi, start, end, bucket, filters, &flux.StatisticsFilterConf{
Measurement: measurement,
AggregateFn: "max()",
Fields: maxFields,
})
if err != nil {
return nil, err
}
minRes, err := e.fluxQuery.CommonQueryOnce(ctx, e.openApi, start, end, bucket, filters, &flux.StatisticsFilterConf{
Measurement: measurement,
AggregateFn: "min()",
Fields: minFields,
})
if err != nil {
return nil, err
}
avgRes, err := e.fluxQuery.CommonQueryOnce(ctx, e.openApi, start, end, bucket, filters, &flux.StatisticsFilterConf{
Measurement: measurement,
AggregateFn: "mean()",
Fields: avgFields,
})
if err != nil {
return nil, err
}
result := make(map[string]*monitor.Aggregate)
for _, field := range fields {
a := new(monitor.Aggregate)
if v, ok := avgRes[field+"_avg"]; ok {
a.Avg = int64(v.(float64))
}
if v, ok := maxRes[field+"_max"]; ok {
a.Max = v.(int64)
}
if v, ok := minRes[field+"_min"]; ok {
a.Min = v.(int64)
}
result[field] = a
}
return result, nil
}
func (e *executor) SumResponseTimeOverview(ctx context.Context, start time.Time, end time.Time, wheres []monitor.MonWhereItem) ([]time.Time, *monitor.Aggregate, []int64, error) {
newStartTime, every, windowOffset, bucket := getTimeIntervalAndBucket(start, end)
filters := formatFilter(wheres)
fieldsConditions := []string{"timing"}
agg, err := e.aggregateSummary(ctx, newStartTime, end, "request", bucket, filters, []string{"timing"})
if err != nil {
return nil, nil, nil, err
}
dates, groupValues, err := e.fluxQuery.CommonTendency(ctx, e.openApi, newStartTime, end, bucket, "request", filters, fieldsConditions, every, windowOffset, flux.SumFn)
if err != nil {
return nil, nil, nil, err
}
timing := groupValues["timing"]
timingLen := len(timing)
result := make([]int64, 0, len(dates))
for i := range dates {
if timingLen > i {
result = append(result, timing[i])
}
}
return dates, agg["timing"], result, nil
}
func (e *executor) AvgResponseTimeOverview(ctx context.Context, start time.Time, end time.Time, wheres []monitor.MonWhereItem) ([]time.Time, *monitor.Aggregate, []int64, error) {
newStartTime, every, windowOffset, bucket := getTimeIntervalAndBucket(start, end)
filters := formatFilter(wheres)
fieldsConditions := []string{"timing_avg"}
agg, err := e.aggregateSummary(ctx, newStartTime, end, "request", bucket, filters, []string{"timing"})
if err != nil {
return nil, nil, nil, err
}
dates, groupValues, err := e.fluxQuery.CommonTendency(ctx, e.openApi, newStartTime, end, bucket, "request", filters, fieldsConditions, every, windowOffset, flux.AvgFn)
if err != nil {
return nil, nil, nil, err
}
timingAvg := groupValues["timing_avg"]
timingAvgLen := len(timingAvg)
result := make([]int64, 0, len(dates))
for i := range dates {
if timingAvgLen > i {
result = append(result, timingAvg[i])
}
}
return dates, agg["timing"], result, nil
}
func (e *executor) RequestOverview(ctx context.Context, start time.Time, end time.Time, wheres []monitor.MonWhereItem) ([]time.Time, *monitor.StatusCodeOverview, []*monitor.StatusCodeOverview, error) {
newStartTime, every, windowOffset, bucket := getTimeIntervalAndBucket(start, end)
filters := formatFilter(wheres)
requestConditions := []string{"total", "s2xx", "s4xx", "s5xx"}
dates, requestValues, err := e.fluxQuery.CommonTendency(ctx, e.openApi, newStartTime, end, bucket, "request", filters, requestConditions, every, windowOffset, flux.SumFn)
if err != nil {
return nil, nil, nil, err
}
total := requestValues["total"]
totalLen := len(total)
s2xx := requestValues["s2xx"]
s2xxLen := len(s2xx)
s4xx := requestValues["s4xx"]
s4xxLen := len(s4xx)
s5xx := requestValues["s5xx"]
s5xxLen := len(s5xx)
totalOverview := new(monitor.StatusCodeOverview)
result := make([]*monitor.StatusCodeOverview, 0, len(dates))
for i := range dates {
r := new(monitor.StatusCodeOverview)
if totalLen > i {
r.StatusTotal = total[i]
totalOverview.StatusTotal += r.StatusTotal
}
if s2xxLen > i {
r.Status2xx = s2xx[i]
totalOverview.Status2xx += r.Status2xx
}
if s4xxLen > i {
r.Status4xx = s4xx[i]
totalOverview.Status4xx += r.Status4xx
}
if s5xxLen > i {
r.Status5xx = s5xx[i]
totalOverview.Status5xx += r.Status5xx
}
result = append(result, r)
}
return dates, totalOverview, result, nil
}
func (e *executor) TopN(ctx context.Context, start time.Time, end time.Time, limit int, groupBy string, wheres []monitor.MonWhereItem) ([]*monitor.TopN, error) {
filters := formatFilter(wheres)
newStartTime, _, _, bucket := getTimeIntervalAndBucket(start, end)
statisticsConf := []*flux.StatisticsFilterConf{
{
Measurement: "request",
AggregateFn: "sum()",
Fields: []string{"total", "request", "response", "input_token", "output_token"},
},
{
Measurement: "proxy",
AggregateFn: "sum()",
Fields: []string{"p_total"},
},
}
results, err := e.fluxQuery.CommonStatistics(ctx, e.openApi, newStartTime, end, bucket, groupBy, filters, statisticsConf, limit)
if err != nil {
return nil, err
}
topN := make([]*monitor.TopN, 0, len(results))
for key, result := range results {
n := new(monitor.TopN)
n.Key = key
n.Request = result.Total
n.Token = result.TotalToken
n.Traffic = result.TotalRequest + result.TotalResponse
topN = append(topN, n)
}
return topN, nil
}
func (e *executor) TokenOverview(ctx context.Context, start time.Time, end time.Time, wheres []monitor.MonWhereItem) ([]time.Time, *monitor.TokenOverview, []*monitor.TokenOverview, error) {
newStartTime, every, windowOffset, bucket := getTimeIntervalAndBucket(start, end)
filters := formatFilter(wheres)
requestConditions := []string{"total_token", "input_token", "output_token"}
dates, requestValues, err := e.fluxQuery.CommonTendency(ctx, e.openApi, newStartTime, end, bucket, "request", filters, requestConditions, every, windowOffset, flux.SumFn)
if err != nil {
return nil, nil, nil, err
}
//total := requestValues["total_token"]
//totalLen := len(total)
input := requestValues["input_token"]
inputLen := len(input)
output := requestValues["output_token"]
outputLen := len(output)
totalOverview := new(monitor.TokenOverview)
result := make([]*monitor.TokenOverview, 0, len(dates))
for i := range dates {
r := new(monitor.TokenOverview)
if inputLen > i {
r.InputToken = input[i]
}
if outputLen > i {
r.OutputToken = output[i]
}
r.TotalToken = r.InputToken + r.OutputToken
totalOverview.InputToken += r.InputToken
totalOverview.OutputToken += r.OutputToken
totalOverview.TotalToken += r.TotalToken
result = append(result, r)
}
return dates, totalOverview, result, nil
}
func (e *executor) ConsumerOverview(ctx context.Context, start time.Time, end time.Time, wheres []monitor.MonWhereItem) (int64, map[time.Time]int64, error) {
newStartTime, every, offset, bucket := getTimeIntervalAndBucket(start, end)
filters := formatFilter(wheres)
return e.fluxQuery.CommonTendencyTag(ctx, e.openApi, newStartTime, end, bucket, "request", filters, every, offset, "app")
}
+97 -22
View File
@@ -14,7 +14,8 @@ import (
type IFluxQuery interface {
CommonStatistics(ctx context.Context, queryApi api.QueryAPI, start, end time.Time, bucket, groupBy, filters string, statisticsConf []*StatisticsFilterConf, limit int) (map[string]*FluxStatistics, error)
CommonProxyStatistics(ctx context.Context, queryApi api.QueryAPI, start, end time.Time, bucket, groupBy, filters string, statisticsConf []*StatisticsFilterConf, limit int) (map[string]*FluxStatistics, error)
CommonTendency(ctx context.Context, queryApi api.QueryAPI, start, end time.Time, bucket, table, filters string, dataFields []string, every, windowOffset string) ([]time.Time, map[string][]int64, error)
CommonTendency(ctx context.Context, queryApi api.QueryAPI, start, end time.Time, bucket, table, filters string, dataFields []string, every, windowOffset string, fn AggregateFn) ([]time.Time, map[string][]int64, error)
CommonTendencyTag(ctx context.Context, queryApi api.QueryAPI, start, end time.Time, bucket, table, filters, every, offset, tag string) (int64, map[time.Time]int64, error)
// CommonQueryOnce 查询只返回一条结果
CommonQueryOnce(ctx context.Context, queryApi api.QueryAPI, start, end time.Time, bucket, filters string, fieldsConf *StatisticsFilterConf) (map[string]interface{}, error)
CommonWarnStatistics(ctx context.Context, queryApi api.QueryAPI, start, end time.Time, bucket, groupBy, filters string, statisticsConf *StatisticsFilterConf) (map[string]*FluxWarnStatistics, error)
@@ -61,18 +62,30 @@ func (f *fluxQuery) CommonStatistics(ctx context.Context, queryApi api.QueryAPI,
totalRequest := common.FmtIntFromInterface(maps["request"])
maxRequest := common.FmtIntFromInterface(maps["request_max"])
minRequest := common.FmtIntFromInterface(maps["request_min"])
totalResponse := common.FmtIntFromInterface(maps["response"])
maxResponse := common.FmtIntFromInterface(maps["response_max"])
minResponse := common.FmtIntFromInterface(maps["response_min"])
inputToken := common.FmtIntFromInterface(maps["input_token"])
outputToken := common.FmtIntFromInterface(maps["output_token"])
//totalToken := common.FmtIntFromInterface(maps["total_token"])
//maxToken := common.FmtIntFromInterface(maps["total_token_max"])
//minToken := common.FmtIntFromInterface(maps["total_token_min"])
resultMap[key] = &FluxStatistics{
Total: total,
Success: success,
ProxyTotal: pTotal,
ProxySuccess: pSuccess,
TotalTiming: totalTiming,
MaxTiming: maxMinTiming,
MinTiming: minTiming,
TotalRequest: totalRequest,
RequestMax: maxRequest,
RequestMin: minRequest,
Total: total,
Success: success,
ProxyTotal: pTotal,
ProxySuccess: pSuccess,
TotalTiming: totalTiming,
MaxTiming: maxMinTiming,
MinTiming: minTiming,
TotalRequest: totalRequest,
RequestMax: maxRequest,
RequestMin: minRequest,
TotalResponse: totalResponse,
ResponseMax: maxResponse,
ResponseMin: minResponse,
TotalToken: inputToken + outputToken,
}
}
@@ -128,10 +141,10 @@ func (f *fluxQuery) CommonProxyStatistics(ctx context.Context, queryApi api.Quer
return resultMap, nil
}
func (f *fluxQuery) CommonTendency(ctx context.Context, queryApi api.QueryAPI, start, end time.Time, bucket, table, filters string, dataFields []string, every, windowOffset string) ([]time.Time, map[string][]int64, error) {
func (f *fluxQuery) CommonTendency(ctx context.Context, queryApi api.QueryAPI, start, end time.Time, bucket, table, filters string, dataFields []string, every, windowOffset string, fn AggregateFn) ([]time.Time, map[string][]int64, error) {
fieldConditions := f.assembleTendencyFieldCondition(dataFields)
//拼装请求
query := f.assembleTendencyFlux(start, end, bucket, table, filters, fieldConditions, every, windowOffset)
query := f.assembleTendencyFlux(start, end, bucket, table, filters, fieldConditions, every, windowOffset, fn)
log.Info("flux sql=", query)
result, err := queryApi.Query(ctx, query)
@@ -148,21 +161,46 @@ func (f *fluxQuery) CommonTendency(ctx context.Context, queryApi api.QueryAPI, s
//初始返回内容
dates := make([]time.Time, 0, len(resultList))
resultMap := make(map[string][]int64, len(dataFields))
for _, field := range dataFields {
resultMap[field] = make([]int64, 0, len(resultList))
}
for _, res := range resultList {
for _, field := range dataFields {
resultMap[field] = append(resultMap[field], common.FmtIntFromInterface(res[field]))
}
t, _ := res["_time"].(time.Time)
dates = append(dates, t)
dates = append(dates, t.In(time.Local))
}
return dates, resultMap, nil
}
func (f *fluxQuery) CommonTendencyTag(ctx context.Context, queryApi api.QueryAPI, start, end time.Time, bucket, table, filters, every, offset, tag string) (int64, map[time.Time]int64, error) {
query := f.assembleTendencyTagFlux(start, end, bucket, table, filters, every, offset, tag)
log.Info("flux sql=", query)
result, err := queryApi.Query(ctx, query)
if err != nil {
log.Error("flux err=", err)
return 0, nil, err
}
dateMap := map[time.Time]map[string]struct{}{}
tagMap := make(map[string]struct{})
defer result.Close()
for result.Next() {
date := result.Record().Values()["_start"].(time.Time).In(time.Local)
if _, ok := dateMap[date]; !ok {
dateMap[date] = map[string]struct{}{}
}
if vv, ok := result.Record().Values()[tag]; ok {
v := vv.(string)
tagMap[v] = struct{}{}
dateMap[date][v] = struct{}{}
}
}
returnMap := make(map[time.Time]int64)
for k, v := range dateMap {
returnMap[k] = int64(len(v))
}
return int64(len(tagMap)), returnMap, nil
}
func (f *fluxQuery) CommonQueryOnce(ctx context.Context, queryApi api.QueryAPI, start, end time.Time, bucket, filters string, fieldsConf *StatisticsFilterConf) (map[string]interface{}, error) {
query := f.getCircularMapFlux(start, end, bucket, filters, fieldsConf)
@@ -172,6 +210,7 @@ func (f *fluxQuery) CommonQueryOnce(ctx context.Context, queryApi api.QueryAPI,
log.Error("flux err=", err)
return nil, err
}
defer result.Close()
for result.Next() {
return result.Record().Values(), nil
@@ -270,7 +309,7 @@ from(bucket: "%s")
}
return fmt.Sprintf(`
union(tables: [
union(tables: [
%s
])
|> pivot(rowKey: ["%s"], columnKey: ["_field"], valueColumn: "_value")
@@ -278,23 +317,59 @@ union(tables: [
`, strings.Join(streams, ",\n"), groupBy, limitStr)
}
func (f *fluxQuery) assembleTendencyFlux(start, end time.Time, bucket, table, filters, fieldConditions, every string, windowOffset string) string {
type AggregateFn string
const (
SumFn AggregateFn = "sum"
MaxFn AggregateFn = "max"
MinFn AggregateFn = "min"
AvgFn AggregateFn = "mean"
)
var (
fns = map[AggregateFn]struct{}{
SumFn: {},
MaxFn: {},
MinFn: {},
}
)
func (f *fluxQuery) assembleTendencyFlux(start, end time.Time, bucket, table, filters, fieldConditions, every, windowOffset string, fn AggregateFn) string {
windowOffsetFlux := ""
if windowOffset != "" {
windowOffsetFlux = fmt.Sprintf(", offset: %s", windowOffset)
}
if _, ok := fns[fn]; !ok {
fn = SumFn
}
return fmt.Sprintf(`from(bucket: "%s")
|> range(start: %d, stop: %d)
|> filter(fn: (r) => r["_measurement"] == "%s")
%s
%s
|> group(columns: ["_field"])
|> aggregateWindow(every: %s, fn: sum, location: {offset: 0ns, zone: "Asia/Shanghai"}, timeSrc: "_start"%s)
|> aggregateWindow(every: %s, fn: %s, location: {offset: 0ns, zone: "Asia/Shanghai"}, timeSrc: "_start"%s)
|> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value")`, bucket, start.Unix(), end.Unix(), table,
filters, fieldConditions, every, windowOffsetFlux)
filters, fieldConditions, every, string(fn), windowOffsetFlux)
}
func (f *fluxQuery) assembleTendencyTagFlux(start, end time.Time, bucket, table, filters string, every, offset, tag string) string {
windowOffset := ""
if len(offset) > 0 {
windowOffset = fmt.Sprintf(", offset: %s", offset)
}
return fmt.Sprintf(`
from(bucket: "%s")
|> range(start: %d, stop: %d)
|> filter(fn: (r) => r["_measurement"] == "%s")
%s
|> keep(columns: ["_time", "%s"])
|> window(every: %s%s)
|> distinct(column: "%s")`, bucket, start.Unix(), end.Unix(), table, filters, tag, every, windowOffset, tag)
}
// assembleTendencyFieldCondition 封装趋势图需要的Field数据
func (f *fluxQuery) assembleTendencyFieldCondition(fieldConditions []string) string {
/*
@@ -2,22 +2,30 @@ package flux
// FluxStatistics flux统计通用字段
type FluxStatistics struct {
Total int64 `json:"total"` //总数
Success int64 `json:"success"` //成功数
ProxyTotal int64 `json:"p_total"` //转发总数
ProxySuccess int64 `json:"p_success"` //转发成功
TotalTiming int64 `json:"timing"` //平均响应时间
MaxTiming int64 `json:"timing_max"` //最大响应时间
MinTiming int64 `json:"timing_min"` //最响应时间
TotalRequest int64 `json:"request"` //总请求流量
RequestMax int64 `json:"request_max"` //最大流量
RequestMin int64 `json:"request_min"` //最流量
Total int64 `json:"total"` //总数
Success int64 `json:"success"` //成功数
S2xx int64 `json:"s2xx"` //2xx
ProxyTotal int64 `json:"p_total"` //转发
ProxySuccess int64 `json:"p_success"` //转发成功数
TotalTiming int64 `json:"timing"` //平均响应时间
MaxTiming int64 `json:"timing_max"` //最响应时间
MinTiming int64 `json:"timing_min"` //最小响应时间
TotalRequest int64 `json:"request"` //总请求流量
RequestMax int64 `json:"request_max"` //最流量
RequestMin int64 `json:"request_min"` //最小流量
TotalResponse int64 `json:"response"` //总请求流量
ResponseMax int64 `json:"response_max"` //最大流量
ResponseMin int64 `json:"response_min"` //最小流量
TotalToken int64 `json:"total_token"` //总token流量
TokenMax int64 `json:"total_token_max"` //最大token流量
TokenMin int64 `json:"total_token_min"` //最小token流量
}
// FluxWarnStatistics flux统计告警通用字段
type FluxWarnStatistics struct {
Total int64 `json:"total"` //总数
Success int64 `json:"success"` //成功数
S2xx int64 `json:"s2xx"`
S4xx int64 `json:"s4xx"`
S5xx int64 `json:"s5xx"`
ProxyTotal int64 `json:"p_total"` //转发总数
@@ -0,0 +1,309 @@
-
task_name: "apinto_day_request_v1"
cron: "0 0 * * *"
offset: "2m30s"
flux: |
from(bucket: "apinto/hour")
|> range(start: -1d)
|> filter(fn: (r) => r._measurement == "request")
|> filter(
fn: (r) =>
r._field == "total" or r._field == "success" or r._field == "s2xx" or r._field == "s4xx" or r._field == "s5xx"
or
r._field == "timing" or r._field == "request" or r._field == "response" or r._field
==
"retry"
or r._field == "total_token" or r._field == "input_token" or r._field == "output_token",
)
|> group(
columns: [
"api",
"app",
"upstream",
"method",
"node",
"cluster",
"provider",
"api_kind",
"status_code",
"_field",
],
)
|> sum()
|> set(key: "_measurement", value: "request")
|> to(
bucket: "apinto/day",
timeColumn: "_start",
tagColumns: [
"api",
"app",
"method",
"upstream",
"node",
"cluster",
"provider",
"api_kind",
"status_code",
],
)
from(bucket: "apinto/hour")
|> range(start: -1d)
|> filter(fn: (r) => r._measurement == "request")
|> filter(
fn: (r) =>
r._field == "timing_max" or r._field == "request_max" or r._field == "response_max"
or
r._field == "retry_max"
or
r._field == "input_token_max" or r._field == "output_token_max" or r._field == "total_token_max",
)
|> group(
columns: [
"api",
"app",
"upstream",
"method",
"node",
"cluster",
"provider",
"api_kind",
"status_code",
"_field",
],
)
|> max()
|> set(key: "_measurement", value: "request")
|> to(
bucket: "apinto/day",
timeColumn: "_start",
tagColumns: [
"api",
"app",
"method",
"upstream",
"node",
"cluster",
"provider",
"api_kind",
"status_code",
],
)
from(bucket: "apinto/hour")
|> range(start: -1d)
|> filter(fn: (r) => r._measurement == "request")
|> filter(
fn: (r) =>
r._field == "timing_min" or r._field == "request_min" or r._field == "response_min"
or
r._field == "retry_min"
or
r._field == "input_token_min" or r._field == "output_token_min" or r._field == "total_token_min",
)
|> group(
columns: [
"api",
"app",
"upstream",
"method",
"node",
"cluster",
"provider",
"api_kind",
"status_code",
"_field",
],
)
|> min()
|> set(key: "_measurement", value: "request")
|> to(
bucket: "apinto/day",
timeColumn: "_start",
tagColumns: [
"api",
"app",
"method",
"upstream",
"node",
"cluster",
"provider",
"api_kind",
"status_code",
],
)
from(bucket: "apinto/hour")
|> range(start: -1d)
|> filter(fn: (r) => r._measurement == "request")
|> filter(
fn: (r) =>
r._field == "timing_avg" or r._field == "request_avg" or r._field == "response_avg"
or
r._field == "input_token_avg" or r._field == "output_token_avg" or r._field == "total_token_avg",
)
|> group(
columns: [
"api",
"app",
"upstream",
"method",
"node",
"cluster",
"provider",
"api_kind",
"status_code",
"_field",
],
)
|> mean()
|> set(key: "_measurement", value: "request")
|> to(
bucket: "apinto/day",
timeColumn: "_start",
tagColumns: [
"api",
"app",
"method",
"upstream",
"node",
"cluster",
"provider",
"api_kind",
"status_code",
],
)
-
task_name: "apinto_day_proxy_v1"
cron: "0 0 * * *"
offset: "2m45s"
flux: |
from(bucket: "apinto/hour")
|> range(start: -1d)
|> filter(fn: (r) => r._measurement == "proxy")
|> filter(
fn: (r) =>
r._field == "p_total" or r._field == "p_success" or r._field == "p_s2xx" or r._field == "p_s4xx" or r._field
==
"p_s5xx" or r._field == "p_timing" or r._field == "p_request" or r._field
==
"p_response" or r._field == "p_retry",
)
|> group(
columns: [
"api",
"app",
"upstream",
"addr",
"method",
"node",
"cluster",
"provider",
"api_kind",
"status_code",
"_field",
],
)
|> sum()
|> set(key: "_measurement", value: "proxy")
|> to(
bucket: "apinto/day",
timeColumn: "_start",
tagColumns: [
"api",
"app",
"method",
"upstream",
"addr",
"node",
"cluster",
"provider",
"api_kind",
"status_code",
],
)
from(bucket: "apinto/hour")
|> range(start: -1d)
|> filter(fn: (r) => r._measurement == "proxy")
|> filter(
fn: (r) =>
r._field == "p_timing_max" or r._field == "p_request_max" or r._field
==
"p_response_max" or r._field == "p_retry_max",
)
|> group(
columns: [
"api",
"app",
"upstream",
"addr",
"method",
"node",
"cluster",
"provider",
"api_kind",
"status_code",
"_field",
],
)
|> max()
|> set(key: "_measurement", value: "proxy")
|> to(
bucket: "apinto/day",
timeColumn: "_start",
tagColumns: [
"api",
"app",
"method",
"upstream",
"addr",
"node",
"cluster",
"provider",
"api_kind",
"status_code",
],
)
from(bucket: "apinto/hour")
|> range(start: -1d)
|> filter(fn: (r) => r._measurement == "proxy")
|> filter(
fn: (r) =>
r._field == "p_timing_min" or r._field == "p_request_min" or r._field
==
"p_response_min" or r._field == "p_retry_min",
)
|> group(
columns: [
"api",
"app",
"upstream",
"addr",
"method",
"node",
"cluster",
"provider",
"api_kind",
"status_code",
"_field",
],
)
|> min()
|> set(key: "_measurement", value: "proxy")
|> to(
bucket: "apinto/day",
timeColumn: "_start",
tagColumns: [
"api",
"app",
"method",
"upstream",
"addr",
"node",
"cluster",
"provider",
"api_kind",
"status_code",
],
)
@@ -0,0 +1,309 @@
-
task_name: "apinto_hour_request_v1"
cron: "0 * * * *"
offset: "1m30s"
flux: |
from(bucket: "apinto/minute")
|> range(start: -1h)
|> filter(fn: (r) => r._measurement == "request")
|> filter(
fn: (r) =>
r._field == "total" or r._field == "success" or r._field == "s2xx" or r._field == "s4xx" or r._field == "s5xx"
or
r._field == "timing" or r._field == "request" or r._field == "response" or r._field
==
"retry"
or r._field == "total_token" or r._field == "input_token" or r._field == "output_token",
)
|> group(
columns: [
"api",
"app",
"upstream",
"method",
"node",
"cluster",
"provider",
"api_kind",
"status_code",
"_field",
"_measurement",
],
)
|> sum()
|> to(
bucket: "apinto/hour",
timeColumn: "_start",
tagColumns: [
"api",
"app",
"method",
"upstream",
"node",
"cluster",
"provider",
"api_kind",
"status_code",
],
)
from(bucket: "apinto/minute")
|> range(start: -1h)
|> filter(fn: (r) => r._measurement == "request")
|> filter(
fn: (r) =>
r._field == "timing_max" or r._field == "request_max" or r._field == "response_max"
or
r._field == "retry_max"
or
r._field == "input_token_max" or r._field == "output_token_max" or r._field == "total_token_max",
)
|> group(
columns: [
"api",
"app",
"upstream",
"method",
"node",
"cluster",
"provider",
"api_kind",
"status_code",
"_field",
"_measurement",
],
)
|> max()
|> to(
bucket: "apinto/hour",
timeColumn: "_start",
tagColumns: [
"api",
"app",
"method",
"upstream",
"node",
"cluster",
"provider",
"api_kind",
"status_code",
],
)
from(bucket: "apinto/minute")
|> range(start: -1h)
|> filter(fn: (r) => r._measurement == "request")
|> filter(
fn: (r) =>
r._field == "timing_min" or r._field == "request_min" or r._field == "response_min"
or
r._field == "retry_min"
or
r._field == "input_token_min" or r._field == "output_token_min" or r._field == "total_token_min",
)
|> group(
columns: [
"api",
"app",
"upstream",
"method",
"node",
"cluster",
"provider",
"api_kind",
"status_code",
"_field",
"_measurement",
],
)
|> min()
|> to(
bucket: "apinto/hour",
timeColumn: "_start",
tagColumns: [
"api",
"app",
"method",
"upstream",
"node",
"cluster",
"provider",
"api_kind",
"status_code",
],
)
from(bucket: "apinto/minute")
|> range(start: -1h)
|> filter(fn: (r) => r._measurement == "request")
|> filter(
fn: (r) =>
r._field == "timing_avg" or r._field == "request_avg" or r._field == "response_avg"
or
r._field == "input_token_avg" or r._field == "output_token_avg" or r._field == "total_token_avg",
)
|> group(
columns: [
"api",
"app",
"upstream",
"method",
"node",
"cluster",
"provider",
"api_kind",
"status_code",
"_field",
"_measurement",
],
)
|> mean()
|> to(
bucket: "apinto/hour",
timeColumn: "_start",
tagColumns: [
"api",
"app",
"method",
"upstream",
"node",
"cluster",
"provider",
"api_kind",
"status_code",
],
)
-
task_name: "apinto_hour_proxy_v1"
cron: "0 * * * *"
offset: "1m45s"
flux: |
from(bucket: "apinto/minute")
|> range(start: -1h)
|> filter(fn: (r) => r._measurement == "proxy")
|> filter(
fn: (r) =>
r._field == "p_total" or r._field == "p_success" or r._field == "p_s2xx" or r._field == "p_s4xx" or r._field
==
"p_s5xx" or r._field == "p_timing" or r._field == "p_request" or r._field
==
"p_response" or r._field == "p_retry",
)
|> group(
columns: [
"api",
"app",
"upstream",
"addr",
"method",
"node",
"cluster",
"provider",
"api_kind",
"status_code",
"_field",
"_measurement",
],
)
|> sum()
|> to(
bucket: "apinto/hour",
timeColumn: "_start",
tagColumns: [
"api",
"app",
"method",
"upstream",
"addr",
"node",
"cluster",
"provider",
"api_kind",
"status_code",
],
)
from(bucket: "apinto/minute")
|> range(start: -1h)
|> filter(fn: (r) => r._measurement == "proxy")
|> filter(
fn: (r) =>
r._field == "p_timing_max" or r._field == "p_request_max" or r._field
==
"p_response_max" or r._field == "p_retry_max",
)
|> group(
columns: [
"api",
"app",
"upstream",
"addr",
"method",
"node",
"cluster",
"provider",
"api_kind",
"status_code",
"_field",
"_measurement",
],
)
|> max()
|> to(
bucket: "apinto/hour",
timeColumn: "_start",
tagColumns: [
"api",
"app",
"method",
"upstream",
"addr",
"node",
"cluster",
"provider",
"api_kind",
"status_code",
],
)
from(bucket: "apinto/minute")
|> range(start: -1h)
|> filter(fn: (r) => r._measurement == "proxy")
|> filter(
fn: (r) =>
r._field == "p_timing_min" or r._field == "p_request_min" or r._field
==
"p_response_min" or r._field == "p_retry_min",
)
|> group(
columns: [
"api",
"app",
"upstream",
"addr",
"method",
"node",
"cluster",
"provider",
"api_kind",
"status_code",
"_field",
"_measurement",
],
)
|> min()
|> to(
bucket: "apinto/hour",
timeColumn: "_start",
tagColumns: [
"api",
"app",
"method",
"upstream",
"addr",
"node",
"cluster",
"provider",
"api_kind",
"status_code",
],
)
@@ -0,0 +1,307 @@
-
task_name: "apinto_week_request_v1"
cron: "0 0 * * 1"
offset: "3m30s"
flux: |
from(bucket: "apinto/day")
|> range(start: -1w)
|> filter(fn: (r) => r._measurement == "request")
|> filter(
fn: (r) =>
r._field == "total" or r._field == "success" or r._field == "s2xx" or r._field == "s4xx" or r._field == "s5xx"
or
r._field == "timing" or r._field == "request" or r._field == "response" or r._field
==
"retry"
or r._field == "total_token" or r._field == "input_token" or r._field == "output_token",
)
|> group(
columns: [
"api",
"app",
"upstream",
"method",
"node",
"cluster",
"provider",
"api_kind",
"status_code",
"_field",
],
)
|> sum()
|> set(key: "_measurement", value: "request")
|> to(
bucket: "apinto/week",
timeColumn: "_start",
tagColumns: [
"api",
"app",
"method",
"upstream",
"node",
"cluster",
"provider",
"api_kind",
"status_code",
],
)
from(bucket: "apinto/day")
|> range(start: -1w)
|> filter(fn: (r) => r._measurement == "request")
|> filter(
fn: (r) =>
r._field == "timing_max" or r._field == "request_max" or r._field == "response_max"
or
r._field == "retry_max"
or
r._field == "input_token_max" or r._field == "output_token_max" or r._field == "total_token_max",
)
|> group(
columns: [
"api",
"app",
"upstream",
"method",
"node",
"cluster",
"provider",
"api_kind",
"status_code",
"_field",
],
)
|> max()
|> set(key: "_measurement", value: "request")
|> to(
bucket: "apinto/week",
timeColumn: "_start",
tagColumns: [
"api",
"app",
"method",
"upstream",
"node",
"cluster",
"provider",
"api_kind",
"status_code",
],
)
from(bucket: "apinto/day")
|> range(start: -1w)
|> filter(fn: (r) => r._measurement == "request")
|> filter(
fn: (r) =>
r._field == "timing_min" or r._field == "request_min" or r._field == "response_min"
or
r._field == "retry_min"
or
r._field == "input_token_min" or r._field == "output_token_min" or r._field == "total_token_min",
)
|> group(
columns: [
"api",
"app",
"upstream",
"method",
"node",
"cluster",
"provider",
"api_kind",
"status_code",
"_field",
],
)
|> min()
|> set(key: "_measurement", value: "request")
|> to(
bucket: "apinto/week",
timeColumn: "_start",
tagColumns: [
"api",
"app",
"method",
"upstream",
"node",
"cluster",
"provider",
"api_kind",
"status_code",
],
)
from(bucket: "apinto/day")
|> range(start: -1w)
|> filter(fn: (r) => r._measurement == "request")
|> filter(
fn: (r) =>
r._field == "timing_avg" or r._field == "request_avg" or r._field == "response_avg"
or
r._field == "input_token_avg" or r._field == "output_token_avg" or r._field == "total_token_avg",
)
|> group(
columns: [
"api",
"app",
"upstream",
"method",
"node",
"cluster",
"provider",
"api_kind",
"status_code",
"_field",
],
)
|> mean()
|> set(key: "_measurement", value: "request")
|> to(
bucket: "apinto/week",
timeColumn: "_start",
tagColumns: [
"api",
"app",
"method",
"upstream",
"node",
"cluster",
"provider",
"api_kind",
"status_code",
],
)
-
task_name: "apinto_week_proxy_v1"
cron: "0 0 * * 1"
offset: "3m45s"
flux: |
from(bucket: "apinto/day")
|> range(start: -1w)
|> filter(fn: (r) => r._measurement == "proxy")
|> filter(
fn: (r) =>
r._field == "p_total" or r._field == "p_success" or r._field == "p_s2xx" or r._field == "p_s4xx" or r._field
==
"p_s5xx" or r._field == "p_timing" or r._field == "p_request" or r._field
==
"p_response" or r._field == "p_retry",
)
|> group(
columns: [
"api",
"app",
"upstream",
"addr",
"method",
"node",
"cluster",
"provider",
"api_kind",
"status_code",
"_field",
],
)
|> sum()
|> set(key: "_measurement", value: "proxy")
|> to(
bucket: "apinto/week",
timeColumn: "_start",
tagColumns: [
"api",
"app",
"method",
"upstream",
"addr",
"node",
"cluster",
"provider",
"api_kind",
"status_code",
],
)
from(bucket: "apinto/day")
|> range(start: -1w)
|> filter(fn: (r) => r._measurement == "proxy")
|> filter(
fn: (r) =>
r._field == "p_timing_max" or r._field == "p_request_max" or r._field
==
"p_response_max" or r._field == "p_retry_max",
)
|> group(
columns: [
"api",
"app",
"upstream",
"addr",
"method",
"node",
"cluster",
"provider",
"api_kind",
"status_code",
"_field",
],
)
|> max()
|> set(key: "_measurement", value: "proxy")
|> to(
bucket: "apinto/week",
timeColumn: "_start",
tagColumns: [
"api",
"app",
"method",
"upstream",
"addr",
"node",
"cluster",
"provider",
"api_kind",
"status_code",
],
)
from(bucket: "apinto/day")
|> range(start: -1w)
|> filter(fn: (r) => r._measurement == "proxy")
|> filter(
fn: (r) =>
r._field == "p_timing_min" or r._field == "p_request_min" or r._field
==
"p_response_min" or r._field == "p_retry_min",
)
|> group(
columns: [
"api",
"app",
"upstream",
"addr",
"method",
"node",
"cluster",
"provider",
"api_kind",
"status_code",
"_field",
],
)
|> min()
|> set(key: "_measurement", value: "proxy")
|> to(
bucket: "apinto/week",
timeColumn: "_start",
tagColumns: [
"api",
"app",
"method",
"upstream",
"addr",
"node",
"cluster",
"provider",
"api_kind",
"status_code",
],
)
@@ -1,13 +1,18 @@
package flux
import (
"embed"
_ "embed"
"fmt"
"strings"
"gopkg.in/yaml.v3"
"github.com/eolinker/eosc/log"
yaml "gopkg.in/yaml.v3"
)
//go:embed influxdb_config/tasks.yaml
var tasksData []byte
//go:embed tasks/*.yaml
var taskReader embed.FS
var (
taskList []*TaskConf
@@ -22,9 +27,28 @@ type TaskConf struct {
func initTasksConfig() {
conf := make([]*TaskConf, 0, 15)
err := yaml.Unmarshal(tasksData, &conf)
files, err := taskReader.ReadDir("tasks")
if err != nil {
panic(err)
panic(fmt.Sprintf("read tasks dir error: %v", err))
}
for _, file := range files {
if file.IsDir() || !strings.HasSuffix(file.Name(), ".yaml") {
continue
}
name := fmt.Sprintf("tasks/%s", file.Name())
data, err := taskReader.ReadFile(name)
if err != nil {
log.Errorf("read file(%s) error: %v", name, err)
continue
}
tmp := make([]*TaskConf, 0, 15)
err = yaml.Unmarshal(data, &tmp)
if err != nil {
log.Errorf("unmarshal file(%s) error: %v", name, err)
continue
}
conf = append(conf, tmp...)
}
taskList = conf
}
+14 -6
View File
@@ -15,9 +15,9 @@ const (
tenDay = 10 * oneDay
oneYear = 365 * oneDay
bucketMinuteRetention = (7 - 1) * oneDay
bucketHourRetention = (90 - 1) * oneDay
bucketDayRetention = (5*365 - 1) * oneDay
bucketMinuteRetention = (7) * oneDay
bucketHourRetention = (90) * oneDay
bucketDayRetention = (5 * 365) * oneDay
bucketMinute = "apinto/minute"
bucketHour = "apinto/hour"
@@ -127,11 +127,11 @@ func getTimeIntervalAndBucket(startTime, endTime time.Time) (time.Time, string,
switch minimumBucket {
case bucketMinute:
offset := ""
offsetTime := startTime.Minute() % 5
offsetTime := startTime.Minute() % 10
if offsetTime != 0 {
offset = fmt.Sprintf("%dm", offsetTime)
}
return startTime, "5m", offset, bucketMinute
return startTime, "10m", offset, bucketMinute
case bucketHour:
newStart := formatStartTimeHour(startTime, location)
@@ -148,7 +148,15 @@ func getTimeIntervalAndBucket(startTime, endTime time.Time) (time.Time, string,
} else if diff <= tenDay {
switch minimumBucket {
case bucketMinute, bucketHour:
case bucketMinute:
offset := ""
offsetTime := startTime.Minute()
if offsetTime != 0 {
offset = fmt.Sprintf("%dm", offsetTime)
}
return startTime, "1h", offset, bucketMinute
case bucketHour:
newStart := formatStartTimeHour(startTime, location)
return newStart, "1h", "", bucketHour
case bucketDay:
+98
View File
@@ -138,3 +138,101 @@ type MonitorCluster struct {
Name string `json:"name"`
Enable bool `json:"enable"`
}
type ChartOverview struct {
}
type StatusCodeOverview struct {
Status2xx int64 `json:"2xx"` //状态码2xx数
Status4xx int64 `json:"4xx"`
Status5xx int64 `json:"5xx"` //状态码5xx数
}
type TokenOverview struct {
TotalToken int64 `json:"total_token"` //总token流量
OutputToken int64 `json:"output_token"`
InputToken int64 `json:"input_token"` //最小token流量
}
type TokenFloatOverview struct {
TotalToken float64 `json:"total_token"` //总token流量
OutputToken float64 `json:"output_token"`
InputToken float64 `json:"input_token"` //最小token流量
}
type ChartAIOverview struct {
RequestOverview []*StatusCodeOverview `json:"request_overview"`
AvgRequestPerSubscriberOverview []float64 `json:"avg_request_per_subscriber_overview"` //平均响应时间概况
MaxRequestPerSubscriber float64 `json:"max_request_per_subscriber"`
MinRequestPerSubscriber float64 `json:"min_request_per_subscriber"`
RequestTotal int64 `json:"request_total"`
Request2xxTotal int64 `json:"request_2xx_total"`
Request4xxTotal int64 `json:"request_4xx_total"`
Request5xxTotal int64 `json:"request_5xx_total"`
TokenTotal int64 `json:"token_total"` //总token流量
InputTokenTotal int64 `json:"input_token_total"`
OutputTokenTotal int64 `json:"output_token_total"` //最大token流量
TokenOverview []*TokenOverview `json:"token_overview"` //token概况
AvgTokenOverview []float64 `json:"avg_token_overview"`
AvgTokenPerSubscriberOverview []*TokenFloatOverview `json:"avg_token_per_subscriber_overview"`
AvgToken float64 `json:"avg_token"`
MaxToken float64 `json:"max_token"`
MinToken float64 `json:"min_token"`
Date []string `json:"date"`
MaxTokenPerSubscriber float64 `json:"max_token_per_subscriber"`
MinTokenPerSubscriber float64 `json:"min_token_per_subscriber"`
}
type ChartRestOverview struct {
RequestOverview []*StatusCodeOverview `json:"request_overview"` //请求概况
AvgRequestPerSubscriberOverview []float64 `json:"avg_request_per_subscriber_overview"` //平均响应时间概况
MaxRequestPerSubscriber float64 `json:"max_request_per_subscriber"`
MinRequestPerSubscriber float64 `json:"min_request_per_subscriber"`
RequestTotal int64 `json:"request_total"`
Request2xxTotal int64 `json:"request_2xx_total"`
Request4xxTotal int64 `json:"request_4xx_total"`
Request5xxTotal int64 `json:"request_5xx_total"`
TrafficOverview []*StatusCodeOverview `json:"traffic_overview"` //流量概况
Traffic2xxTotal int64 `json:"traffic_2xx_total"`
Traffic4xxTotal int64 `json:"traffic_4xx_total"` //流量概况
Traffic5xxTotal int64 `json:"traffic_5xx_total"` //流量概况
AvgResponseTimeOverview []int64 `json:"avg_response_time_overview"` //平均响应时间概况
AvgTrafficPerSubscriberOverview []float64 `json:"avg_traffic_per_subscriber_overview"`
TrafficTotal int64 `json:"traffic_total"`
AvgResponseTime int64 `json:"avg_response_time"` //平均响应时间
MaxResponseTime int64 `json:"max_response_time"` //最大响应时间
MinResponseTime int64 `json:"min_response_time"` //最小响应时间
Date []string `json:"date"`
MaxTrafficPerSubscriber float64 `json:"max_traffic_per_subscriber"`
MinTrafficPerSubscriber float64 `json:"min_traffic_per_subscriber"`
}
type ServiceChartRestOverview struct {
EnableMCP bool `json:"enable_mcp"`
SubscriberNum int64 `json:"subscriber_num"`
APINum int64 `json:"api_num"`
ServiceKind string `json:"service_kind"`
AvailableMonitor bool `json:"available_monitor"`
*ChartRestOverview
}
type ServiceChartAIOverview struct {
EnableMCP bool `json:"enable_mcp"`
SubscriberNum int64 `json:"subscriber_num"`
APINum int64 `json:"api_num"`
ServiceKind string `json:"service_kind"`
AvailableMonitor bool `json:"available_monitor"`
*ChartAIOverview
}
type TopN struct {
Id string `json:"id"`
Name string `json:"name"`
Request string `json:"request"`
Traffic string `json:"traffic,omitempty"`
Token string `json:"token,omitempty"`
}
+471 -17
View File
@@ -5,9 +5,13 @@ import (
"encoding/json"
"errors"
"fmt"
"math"
"sort"
"sync"
"time"
"github.com/APIParkLab/APIPark/common"
"github.com/APIParkLab/APIPark/gateway"
"github.com/eolinker/eosc/log"
"github.com/eolinker/go-common/auto"
@@ -43,6 +47,439 @@ type imlMonitorStatisticModule struct {
apiService api.IAPIService `autowired:""`
}
func (i *imlMonitorStatisticModule) genOverviewWhere(ctx context.Context, serviceId string, apiKind []string) ([]monitor.MonWhereItem, error) {
clusterId := cluster.DefaultClusterID
_, err := i.clusterService.Get(ctx, clusterId)
if err != nil {
return nil, err
}
wheres, err := i.genCommonWheres(ctx, clusterId)
if err != nil {
return nil, err
}
if serviceId != "" {
wheres = append(wheres, monitor.MonWhereItem{
Key: "provider",
Operation: "=",
Values: []string{serviceId},
})
}
if len(apiKind) > 0 {
wheres = append(wheres, monitor.MonWhereItem{
Key: "api_kind",
Operation: "in",
Values: apiKind,
})
}
return wheres, nil
}
func (i *imlMonitorStatisticModule) AIChartOverview(ctx context.Context, serviceId string, start int64, end int64) (*monitor_dto.ChartAIOverview, error) {
wheres, err := i.genOverviewWhere(ctx, serviceId, []string{"ai"})
if err != nil {
return nil, err
}
executor, err := i.getExecutor(ctx, cluster.DefaultClusterID)
if err != nil {
return nil, err
}
_, consumerMap, err := executor.ConsumerOverview(ctx, formatTimeByMinute(start), formatTimeByMinute(end), wheres)
if err != nil {
return nil, err
}
var wg sync.WaitGroup
wg.Add(3)
errChan := make(chan error, 3)
result := new(monitor_dto.ChartAIOverview)
go func() {
defer wg.Done()
date, summary, items, err := executor.RequestOverview(ctx, formatTimeByMinute(start), formatTimeByMinute(end), wheres)
if err != nil {
errChan <- err
return
}
result.Date = utils.SliceToSlice(date, func(t time.Time) string {
return t.Format("2006/01/02 15:04")
})
result.AvgRequestPerSubscriberOverview = make([]float64, 0, len(items))
result.RequestOverview = make([]*monitor_dto.StatusCodeOverview, 0, len(items))
for index, item := range items {
consumerNum := consumerMap[date[index]]
avgRequestPerSubscriber := 0.0
if consumerNum != 0 {
avgRequestPerSubscriber = float64(item.StatusTotal) / float64(consumerNum)
if avgRequestPerSubscriber > result.MaxRequestPerSubscriber {
result.MaxRequestPerSubscriber = avgRequestPerSubscriber
}
if result.MinRequestPerSubscriber == 0 || result.MinRequestPerSubscriber > avgRequestPerSubscriber {
result.MinRequestPerSubscriber = avgRequestPerSubscriber
}
}
result.AvgRequestPerSubscriberOverview = append(result.AvgRequestPerSubscriberOverview, avgRequestPerSubscriber)
result.RequestOverview = append(result.RequestOverview, &monitor_dto.StatusCodeOverview{
Status2xx: item.Status2xx,
Status4xx: item.Status4xx,
Status5xx: item.Status5xx,
})
}
result.RequestTotal = summary.StatusTotal
result.Request2xxTotal = summary.Status2xx
result.Request4xxTotal = summary.Status4xx
result.Request5xxTotal = summary.Status5xx
}()
sumResponseTimes := make([]int64, 0)
go func() {
defer wg.Done()
_, _, items, err := executor.SumResponseTimeOverview(ctx, formatTimeByMinute(start), formatTimeByMinute(end), wheres)
if err != nil {
errChan <- err
return
}
for _, item := range items {
sumResponseTimes = append(sumResponseTimes, item)
}
}()
totalTokens := make([]int64, 0)
go func() {
defer wg.Done()
startTime := formatTimeByMinute(start)
endTime := formatTimeByMinute(end)
date, summary, items, err := executor.TokenOverview(ctx, startTime, endTime, wheres)
if err != nil {
errChan <- err
return
}
result.TokenOverview = make([]*monitor_dto.TokenOverview, 0, len(items))
result.AvgTokenOverview = make([]float64, 0, len(items))
result.AvgTokenPerSubscriberOverview = make([]*monitor_dto.TokenFloatOverview, 0, len(items))
var maxToken, minToken int64 = 0, 0
for index, item := range items {
if maxToken < item.TotalToken {
maxToken = item.TotalToken
}
if minToken == 0 || minToken > item.TotalToken {
minToken = item.TotalToken
}
result.TokenOverview = append(result.TokenOverview, &monitor_dto.TokenOverview{
TotalToken: item.TotalToken,
OutputToken: item.OutputToken,
InputToken: item.InputToken,
})
totalTokens = append(totalTokens, item.TotalToken)
consumerNum := consumerMap[date[index]]
avgTotalPerSubscriber := 0.0
avgOutputPerSubscriber := 0.0
avgInputPerSubscriber := 0.0
if consumerNum != 0 {
avgTotalPerSubscriber = float64(item.TotalToken) / float64(consumerNum)
avgOutputPerSubscriber = float64(item.OutputToken) / float64(consumerNum)
avgInputPerSubscriber = float64(item.InputToken) / float64(consumerNum)
if avgTotalPerSubscriber > result.MaxTokenPerSubscriber {
result.MaxTokenPerSubscriber = avgTotalPerSubscriber
}
if result.MinTokenPerSubscriber == 0 || result.MinTokenPerSubscriber > avgTotalPerSubscriber {
result.MinTokenPerSubscriber = avgTotalPerSubscriber
}
}
result.AvgTokenPerSubscriberOverview = append(result.AvgTokenPerSubscriberOverview, &monitor_dto.TokenFloatOverview{
TotalToken: avgTotalPerSubscriber,
OutputToken: avgOutputPerSubscriber,
InputToken: avgInputPerSubscriber,
})
}
result.TokenTotal = summary.TotalToken
result.InputTokenTotal = summary.InputToken
result.OutputTokenTotal = summary.OutputToken
}()
go func() {
wg.Wait()
close(errChan)
}()
errs := make([]error, 0, 3)
// 收集错误
for err := range errChan {
errs = append(errs, err)
}
if len(errs) > 0 {
return nil, fmt.Errorf("errors occurred: %v", errs)
}
sumResponseTime := 0.0
var maxTokenPerSecond, minTokenPerSecond float64 = 0, 0
for index, token := range totalTokens {
var p float64 = 0
if len(sumResponseTimes) > index && sumResponseTimes[index] > 0 {
p = math.Round(float64(token)*1000*100/float64(sumResponseTimes[index])) / 100
sumResponseTime += float64(sumResponseTimes[index])
}
result.AvgTokenOverview = append(result.AvgTokenOverview, p)
if maxTokenPerSecond < p {
maxTokenPerSecond = p
}
if p > 0 && (minTokenPerSecond == 0 || minTokenPerSecond > p) {
minTokenPerSecond = p
}
}
if sumResponseTime > 0 {
result.AvgToken = math.Round(float64(result.TokenTotal)*1000*100/sumResponseTime) / 100
}
result.MaxToken = maxTokenPerSecond
result.MinToken = minTokenPerSecond
return result, nil
}
func (i *imlMonitorStatisticModule) RestChartOverview(ctx context.Context, serviceId string, start int64, end int64) (*monitor_dto.ChartRestOverview, error) {
wheres, err := i.genOverviewWhere(ctx, serviceId, []string{"rest"})
if err != nil {
return nil, err
}
executor, err := i.getExecutor(ctx, cluster.DefaultClusterID)
if err != nil {
return nil, err
}
_, consumerMap, err := executor.ConsumerOverview(ctx, formatTimeByMinute(start), formatTimeByMinute(end), wheres)
if err != nil {
return nil, err
}
var wg sync.WaitGroup
wg.Add(3)
errChan := make(chan error, 2)
result := new(monitor_dto.ChartRestOverview)
go func() {
defer wg.Done()
date, summary, items, err := executor.RequestOverview(ctx, formatTimeByMinute(start), formatTimeByMinute(end), wheres)
if err != nil {
errChan <- err
return
}
result.Date = utils.SliceToSlice(date, func(t time.Time) string {
return t.Format("2006/01/02 15:04")
})
result.AvgRequestPerSubscriberOverview = make([]float64, 0, len(items))
result.RequestOverview = make([]*monitor_dto.StatusCodeOverview, 0, len(items))
for index, item := range items {
consumerNum := consumerMap[date[index]]
avgRequestPerSubscriber := 0.0
if consumerNum != 0 {
avgRequestPerSubscriber = float64(item.StatusTotal) / float64(consumerNum)
if avgRequestPerSubscriber > result.MaxRequestPerSubscriber {
result.MaxRequestPerSubscriber = avgRequestPerSubscriber
}
if result.MinRequestPerSubscriber == 0 || avgRequestPerSubscriber < result.MinRequestPerSubscriber {
result.MinRequestPerSubscriber = avgRequestPerSubscriber
}
}
result.AvgRequestPerSubscriberOverview = append(result.AvgRequestPerSubscriberOverview, avgRequestPerSubscriber)
result.RequestOverview = append(result.RequestOverview, &monitor_dto.StatusCodeOverview{
Status2xx: item.Status2xx,
Status4xx: item.Status4xx,
Status5xx: item.Status5xx,
})
}
result.RequestTotal = summary.StatusTotal
result.Request2xxTotal = summary.Status2xx
result.Request4xxTotal = summary.Status4xx
result.Request5xxTotal = summary.Status5xx
}()
go func() {
defer wg.Done()
startTime := formatTimeByMinute(start)
endTime := formatTimeByMinute(end)
_, summary, items, err := executor.AvgResponseTimeOverview(ctx, startTime, endTime, wheres)
if err != nil {
errChan <- err
return
}
for _, item := range items {
if item > result.MaxResponseTime {
result.MaxResponseTime = item
}
if item > 0 && (result.MinResponseTime == 0 || item < result.MinResponseTime) {
result.MinResponseTime = item
}
}
result.AvgResponseTimeOverview = items
result.AvgResponseTime = summary.Avg
}()
go func() {
defer wg.Done()
startTime := formatTimeByMinute(start)
endTime := formatTimeByMinute(end)
date, summary, items, err := executor.TrafficOverviewByStatusCode(ctx, startTime, endTime, wheres)
if err != nil {
errChan <- err
return
}
result.TrafficOverview = make([]*monitor_dto.StatusCodeOverview, 0, len(items))
result.AvgTrafficPerSubscriberOverview = make([]float64, 0, len(items))
for index, item := range items {
result.TrafficOverview = append(result.TrafficOverview, &monitor_dto.StatusCodeOverview{
Status2xx: item.Status2xx,
Status4xx: item.Status4xx,
Status5xx: item.Status5xx,
})
consumerNum := consumerMap[date[index]]
avgTrafficPerSubscriber := 0.0
if consumerNum != 0 {
avgTrafficPerSubscriber = float64(item.StatusTotal) / float64(consumerNum)
if avgTrafficPerSubscriber > result.MaxTrafficPerSubscriber {
result.MaxTrafficPerSubscriber = avgTrafficPerSubscriber
}
if result.MinTrafficPerSubscriber == 0 || result.MinTrafficPerSubscriber > avgTrafficPerSubscriber {
result.MinTrafficPerSubscriber = avgTrafficPerSubscriber
}
}
result.AvgTrafficPerSubscriberOverview = append(result.AvgTrafficPerSubscriberOverview, avgTrafficPerSubscriber)
}
result.TrafficTotal = summary.StatusTotal
result.Traffic2xxTotal = summary.Status2xx
result.Traffic4xxTotal = summary.Status4xx
result.Traffic5xxTotal = summary.Status5xx
}()
go func() {
wg.Wait()
close(errChan)
}()
errs := make([]error, 0, 3)
// 收集错误
for err := range errChan {
errs = append(errs, err)
}
if len(errs) > 0 {
return nil, fmt.Errorf("errors occurred: %v", errs)
}
return result, nil
}
func generateTopN(id string, name string, item *monitor.TopN, apiKind string) *monitor_dto.TopN {
n := &monitor_dto.TopN{
Id: id,
Name: name,
Request: common.FormatCountInt64(item.Request),
}
switch apiKind {
case "rest":
n.Traffic = common.FormatByte(item.Traffic)
case "ai":
n.Token = common.FormatCountInt64(item.Token)
}
return n
}
func (i *imlMonitorStatisticModule) Top(ctx context.Context, serviceId string, start int64, end int64, limit int, apiKind string) ([]*monitor_dto.TopN, []*monitor_dto.TopN, error) {
wheres, err := i.genOverviewWhere(ctx, serviceId, []string{apiKind})
if err != nil {
return nil, nil, err
}
executor, err := i.getExecutor(ctx, cluster.DefaultClusterID)
if err != nil {
return nil, nil, err
}
errChan := make(chan error, 2)
var wg sync.WaitGroup
apisResult, consumersResult := make([]*monitor_dto.TopN, 0), make([]*monitor_dto.TopN, 0)
var errs []error
wg.Add(2)
go func() {
defer wg.Done()
result, err := executor.TopN(ctx, formatTimeByMinute(start), formatTimeByMinute(end), limit, "api", wheres)
if err != nil {
errChan <- err
return
}
if len(result) < 1 {
return
}
apiIds := utils.SliceToSlice(result, func(t *monitor.TopN) string {
return t.Key
})
apis, err := i.apiService.ListInfo(ctx, apiIds...)
if err != nil {
errChan <- err
return
}
apiMap := utils.SliceToMap(apis, func(t *api.Info) string {
return t.UUID
})
for _, item := range result {
if v, ok := apiMap[item.Key]; ok {
apisResult = append(apisResult, generateTopN(v.UUID, v.Name, item, apiKind))
}
}
}()
go func() {
defer wg.Done()
result, err := executor.TopN(ctx, formatTimeByMinute(start), formatTimeByMinute(end), limit, "app", wheres)
if err != nil {
errChan <- err
return
}
if len(result) < 1 {
return
}
appIds := utils.SliceToSlice(result, func(t *monitor.TopN) string {
return t.Key
})
apps, err := i.serviceService.AppList(ctx, appIds...)
if err != nil {
errChan <- err
return
}
appMap := utils.SliceToMap(apps, func(t *service.Service) string {
return t.Id
})
appMap["apipark-global"] = &service.Service{
Id: "apipark-global",
Name: "System Consumer",
}
for _, item := range result {
if v, ok := appMap[item.Key]; ok {
consumersResult = append(consumersResult, generateTopN(v.Id, v.Name, item, apiKind))
}
}
}()
// 收集所有错误
go func() {
wg.Wait()
close(errChan)
}()
// 收集错误
for err := range errChan {
errs = append(errs, err)
}
if len(errs) > 0 {
return nil, nil, fmt.Errorf("errors occurred: %v", errs)
}
return apisResult, consumersResult, nil
}
func (i *imlMonitorStatisticModule) ApiStatistics(ctx context.Context, input *monitor_dto.StatisticInput) ([]*monitor_dto.ApiStatisticBasicItem, error) {
clusterId := cluster.DefaultClusterID
_, err := i.clusterService.Get(ctx, clusterId)
@@ -142,6 +579,10 @@ func (i *imlMonitorStatisticModule) SubscriberStatistics(ctx context.Context, in
if err != nil {
return nil, err
}
apps = append(apps, &service.Service{
Id: "apipark-global",
Name: "System Consumer",
})
appIds := utils.SliceToSlice(apps, func(p *service.Service) string {
return p.Id
})
@@ -350,18 +791,27 @@ func (i *imlMonitorStatisticModule) statisticOnApi(ctx context.Context, clusterI
if err != nil {
return nil, err
}
var service []*service.Service
var services []*service.Service
switch groupBy {
case "app":
service, err = i.serviceService.AppList(ctx)
services, err = i.serviceService.AppList(ctx)
if err != nil {
return nil, err
}
services = append(services, &service.Service{
Id: "apipark-global",
Name: "System Consumer",
})
case "provider":
service, err = i.serviceService.ServiceList(ctx)
services, err = i.serviceService.ServiceList(ctx)
if err != nil {
return nil, err
}
default:
return nil, errors.New("invalid group by")
}
if err != nil {
return nil, err
}
wheres, err := i.genCommonWheres(ctx, clusterId)
if err != nil {
@@ -379,7 +829,7 @@ func (i *imlMonitorStatisticModule) statisticOnApi(ctx context.Context, clusterI
}
result := make([]*monitor_dto.ServiceStatisticBasicItem, 0, len(statisticMap))
for _, item := range service {
for _, item := range services {
statisticItem := &monitor_dto.ServiceStatisticBasicItem{
Id: item.Id,
@@ -445,17 +895,21 @@ func (i *imlMonitorStatisticModule) ApiStatisticsOnSubscriber(ctx context.Contex
if err != nil {
return nil, err
}
// 根据订阅ID查询订阅的服务列表
subscriptions, err := i.subscribeService.MySubscribeServices(ctx, subscriberId, nil)
if err != nil {
return nil, err
}
serviceIds := utils.SliceToSlice(subscriptions, func(t *subscribe.Subscribe) string {
return t.Service
})
if len(serviceIds) < 1 {
return nil, nil
serviceIds := make([]string, 0)
if subscriberId != "apipark-global" {
// 根据订阅ID查询订阅的服务列表
subscriptions, err := i.subscribeService.MySubscribeServices(ctx, subscriberId, nil)
if err != nil {
return nil, err
}
serviceIds = utils.SliceToSlice(subscriptions, func(t *subscribe.Subscribe) string {
return t.Service
})
if len(serviceIds) < 1 {
return nil, nil
}
}
apiInfos, err := i.apiService.ListInfoForServices(ctx, serviceIds...)
if err != nil {
return nil, err
+4
View File
@@ -43,6 +43,10 @@ type IMonitorStatisticModule interface {
ApiStatisticsOnProvider(ctx context.Context, providerId string, input *monitor_dto.StatisticInput) ([]*monitor_dto.ApiStatisticBasicItem, error)
ApiStatisticsOnSubscriber(ctx context.Context, subscriberId string, input *monitor_dto.StatisticInput) ([]*monitor_dto.ApiStatisticBasicItem, error)
SubscriberStatisticsOnApi(ctx context.Context, apiId string, input *monitor_dto.StatisticInput) ([]*monitor_dto.ServiceStatisticBasicItem, error)
AIChartOverview(ctx context.Context, serviceId string, start int64, end int64) (*monitor_dto.ChartAIOverview, error)
RestChartOverview(ctx context.Context, serviceId string, start int64, end int64) (*monitor_dto.ChartRestOverview, error)
Top(ctx context.Context, serviceId string, start int64, end int64, limit int, apiKind string) ([]*monitor_dto.TopN, []*monitor_dto.TopN, error)
}
type IMonitorConfigModule interface {
+215 -196
View File
@@ -7,10 +7,14 @@ import (
"fmt"
"time"
"github.com/eolinker/go-common/server"
"github.com/eolinker/go-common/register"
service_overview "github.com/APIParkLab/APIPark/service/service-overview"
mcp_server "github.com/APIParkLab/APIPark/mcp-server"
api_doc "github.com/APIParkLab/APIPark/service/api-doc"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mitchellh/mapstructure"
strategy_driver "github.com/APIParkLab/APIPark/module/strategy/driver"
strategy_dto "github.com/APIParkLab/APIPark/module/strategy/dto"
@@ -51,47 +55,95 @@ var (
)
type imlPublishModule struct {
projectDiffModule serviceDiff.IServiceDiffModule `autowired:""`
releaseModule releaseModule.IReleaseModule `autowired:""`
publishService publish.IPublishService `autowired:""`
apiService api.IAPIService `autowired:""`
apiDocService api_doc.IAPIDocService `autowired:""`
upstreamService upstream.IUpstreamService `autowired:""`
strategyService strategy.IStrategyService `autowired:""`
releaseService release.IReleaseService `autowired:""`
clusterService cluster.IClusterService `autowired:""`
serviceService service.IServiceService `autowired:""`
transaction store.ITransaction `autowired:""`
projectDiffModule serviceDiff.IServiceDiffModule `autowired:""`
releaseModule releaseModule.IReleaseModule `autowired:""`
publishService publish.IPublishService `autowired:""`
apiService api.IAPIService `autowired:""`
apiDocService api_doc.IAPIDocService `autowired:""`
upstreamService upstream.IUpstreamService `autowired:""`
strategyService strategy.IStrategyService `autowired:""`
releaseService release.IReleaseService `autowired:""`
clusterService cluster.IClusterService `autowired:""`
serviceService service.IServiceService `autowired:""`
serviceOverviewService service_overview.IOverviewService `autowired:""`
transaction store.ITransaction `autowired:""`
}
func (m *imlPublishModule) initGateway(ctx context.Context, partitionId string, clientDriver gateway.IClientDriver) error {
return nil
projects, err := m.serviceService.List(ctx)
if err != nil {
return err
}
projectIds := utils.SliceToSlice(projects, func(p *service.Service) string {
return p.Id
func (i *imlPublishModule) OnInit() {
register.Handle(func(v server.Server) {
ctx := context.Background()
list, err := i.releaseService.GetRunningList(ctx)
if err != nil {
log.Errorf("onInit: get running list failed:%s", err.Error())
return
}
if len(list) < 1 {
return
}
serviceMap := make(map[string]*release.Release)
serviceIds := make([]string, 0, len(list))
for _, v := range list {
if _, ok := serviceMap[v.Service]; !ok {
serviceMap[v.Service] = v
serviceIds = append(serviceIds, v.Service)
}
}
overviewList, err := i.serviceOverviewService.List(ctx, serviceIds...)
if err != nil {
log.Errorf("onInit: get running list failed:%s", err.Error())
return
}
for _, v := range overviewList {
if v.IsReleased {
return
}
}
listCommits, err := i.apiDocService.ListLatestDocCommit(ctx, serviceIds...)
if err != nil {
log.Errorf("onInit: get running api doc commits failed:%s", err.Error())
return
}
isReleased := true
for _, v := range listCommits {
i.serviceOverviewService.Update(ctx, v.Target, &service_overview.Update{
ApiCount: nil,
ReleaseApiCount: &v.Data.APICount,
IsReleased: &isReleased,
})
}
})
for _, projectId := range projectIds {
releaseInfo, err := m.GetProjectRelease(ctx, projectId, partitionId)
if err != nil {
return err
}
if releaseInfo == nil {
continue
}
err = clientDriver.Project().Online(ctx, releaseInfo)
if err != nil {
return err
}
}
return nil
}
func (m *imlPublishModule) getProjectRelease(ctx context.Context, projectID string, commitId string) (*gateway.ProjectRelease, error) {
commits, err := m.releaseService.GetCommits(ctx, commitId)
func (i *imlPublishModule) initGateway(ctx context.Context, partitionId string, clientDriver gateway.IClientDriver) error {
return nil
//projects, err := i.serviceService.List(ctx)
//if err != nil {
// return err
//}
//projectIds := utils.SliceToSlice(projects, func(p *service.Service) string {
// return p.Id
//})
//for _, projectId := range projectIds {
// releaseInfo, err := i.GetProjectRelease(ctx, projectId, partitionId)
// if err != nil {
// return err
// }
// if releaseInfo == nil {
// continue
// }
//
// err = clientDriver.Project().Online(ctx, releaseInfo)
// if err != nil {
// return err
// }
//}
//return nil
}
func (i *imlPublishModule) getProjectRelease(ctx context.Context, projectID string, commitId string) (*gateway.ProjectRelease, error) {
commits, err := i.releaseService.GetCommits(ctx, commitId)
if err != nil {
return nil, err
}
@@ -110,13 +162,17 @@ func (m *imlPublishModule) getProjectRelease(ctx context.Context, projectID stri
strategyCommitIds = append(strategyCommitIds, c.Commit)
}
}
apiInfos, err := m.apiService.ListInfo(ctx, apiIds...)
serviceInfo, err := i.serviceService.Get(ctx, projectID)
if err != nil {
return nil, err
}
proxyCommits, err := m.apiService.ListProxyCommit(ctx, apiProxyCommitIds...)
apiInfos, err := i.apiService.ListInfo(ctx, apiIds...)
if err != nil {
return nil, err
}
proxyCommits, err := i.apiService.ListProxyCommit(ctx, apiProxyCommitIds...)
if err != nil {
return nil, err
}
@@ -129,6 +185,41 @@ func (m *imlPublishModule) getProjectRelease(ctx context.Context, projectID stri
Id: projectID,
Version: version,
}
upstreamProxyHeaders := make([]*gateway.ProxyHeader, 0)
var upstreamRelease *gateway.UpstreamRelease
if len(upstreamCommitIds) > 0 {
upstreamCommits, err := i.upstreamService.ListCommit(ctx, upstreamCommitIds...)
if err != nil {
return nil, err
}
for _, c := range upstreamCommits {
upstreamRelease = &gateway.UpstreamRelease{
BasicItem: &gateway.BasicItem{
ID: c.Target,
Version: version,
MatchLabels: map[string]string{
"serviceId": projectID,
},
},
PassHost: c.Data.PassHost,
Scheme: c.Data.Scheme,
Balance: c.Data.Balance,
Timeout: c.Data.Timeout,
Nodes: utils.SliceToSlice(c.Data.Nodes, func(n *upstream.NodeConfig) string {
return fmt.Sprintf("%s weight=%d", n.Address, n.Weight)
}),
}
upstreamProxyHeaders = utils.SliceToSlice(c.Data.ProxyHeaders, func(n *upstream.ProxyHeader) *gateway.ProxyHeader {
return &gateway.ProxyHeader{
Key: n.Key,
Value: n.Value,
Opt: n.OptType,
}
})
}
r.Upstream = upstreamRelease
}
apis := make([]*gateway.ApiRelease, 0, len(apiInfos))
hasUpstream := len(upstreamCommitIds) > 0
for _, a := range apiInfos {
@@ -140,6 +231,9 @@ func (m *imlPublishModule) getProjectRelease(ctx context.Context, projectID stri
},
Path: a.Path,
Methods: a.Methods,
Labels: map[string]string{
"api_kind": serviceInfo.Kind.String(),
},
//Service: a.Upstream,
}
if hasUpstream {
@@ -162,40 +256,17 @@ func (m *imlPublishModule) getProjectRelease(ctx context.Context, projectID stri
Opt: h.OptType,
}
})
apiInfo.ProxyHeaders = append(apiInfo.ProxyHeaders, upstreamProxyHeaders...)
apiInfo.Retry = proxy.Retry
apiInfo.Timeout = proxy.Timeout
}
apis = append(apis, apiInfo)
}
r.Apis = apis
var upstreamRelease *gateway.UpstreamRelease
if len(upstreamCommitIds) > 0 {
upstreamCommits, err := m.upstreamService.ListCommit(ctx, upstreamCommitIds...)
if err != nil {
return nil, err
}
for _, c := range upstreamCommits {
upstreamRelease = &gateway.UpstreamRelease{
BasicItem: &gateway.BasicItem{
ID: c.Target,
Version: version,
MatchLabels: map[string]string{
"serviceId": projectID,
},
},
PassHost: c.Data.PassHost,
Scheme: c.Data.Scheme,
Balance: c.Data.Balance,
Timeout: c.Data.Timeout,
Nodes: utils.SliceToSlice(c.Data.Nodes, func(n *upstream.NodeConfig) string {
return fmt.Sprintf("%s weight=%d", n.Address, n.Weight)
}),
}
}
r.Upstream = upstreamRelease
}
if len(strategyCommitIds) > 0 {
strategyCommits, err := m.strategyService.ListStrategyCommit(ctx, strategyCommitIds...)
strategyCommits, err := i.strategyService.ListStrategyCommit(ctx, strategyCommitIds...)
if err != nil {
return nil, err
}
@@ -227,9 +298,9 @@ func (m *imlPublishModule) getProjectRelease(ctx context.Context, projectID stri
return r, nil
}
func (m *imlPublishModule) GetProjectRelease(ctx context.Context, projectID string, partitionId string) (*gateway.ProjectRelease, error) {
func (i *imlPublishModule) GetProjectRelease(ctx context.Context, projectID string, partitionId string) (*gateway.ProjectRelease, error) {
releaseInfo, err := m.releaseService.GetRunning(ctx, projectID)
releaseInfo, err := i.releaseService.GetRunning(ctx, projectID)
if err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
@@ -237,11 +308,11 @@ func (m *imlPublishModule) GetProjectRelease(ctx context.Context, projectID stri
return nil, nil
}
return m.getProjectRelease(ctx, projectID, releaseInfo.UUID)
return i.getProjectRelease(ctx, projectID, releaseInfo.UUID)
}
func (m *imlPublishModule) getReleaseInfo(ctx context.Context, projectID, releaseId, version string, clusterIds []string) (map[string]*gateway.ProjectRelease, error) {
projectRelease, err := m.getProjectRelease(ctx, projectID, releaseId)
func (i *imlPublishModule) getReleaseInfo(ctx context.Context, projectID, releaseId, version string, clusterIds []string) (map[string]*gateway.ProjectRelease, error) {
projectRelease, err := i.getProjectRelease(ctx, projectID, releaseId)
if err != nil {
return nil, err
}
@@ -259,19 +330,19 @@ func (m *imlPublishModule) getReleaseInfo(ctx context.Context, projectID, releas
return projectReleaseMap, nil
}
func (m *imlPublishModule) PublishStatuses(ctx context.Context, serviceId string, id string) ([]*dto.PublishStatus, error) {
_, err := m.serviceService.Check(ctx, serviceId, asServer)
func (i *imlPublishModule) PublishStatuses(ctx context.Context, serviceId string, id string) ([]*dto.PublishStatus, error) {
_, err := i.serviceService.Check(ctx, serviceId, asServer)
if err != nil {
return nil, err
}
flow, err := m.publishService.Get(ctx, id)
flow, err := i.publishService.Get(ctx, id)
if err != nil {
return nil, err
}
if flow.Service != serviceId {
return nil, errors.New("服务不一致")
}
list, err := m.publishService.GetPublishStatus(ctx, id)
list, err := i.publishService.GetPublishStatus(ctx, id)
if err != nil {
return nil, err
}
@@ -295,18 +366,18 @@ func (m *imlPublishModule) PublishStatuses(ctx context.Context, serviceId string
//
// ctx context.Context, serviceId string, input *dto.ApplyInput
// *dto.Publish, error
func (m *imlPublishModule) Apply(ctx context.Context, serviceId string, input *dto.ApplyInput) (*dto.Publish, error) {
_, err := m.serviceService.Check(ctx, serviceId, asServer)
func (i *imlPublishModule) Apply(ctx context.Context, serviceId string, input *dto.ApplyInput) (*dto.Publish, error) {
_, err := i.serviceService.Check(ctx, serviceId, asServer)
if err != nil {
return nil, err
}
err = m.checkPublish(ctx, serviceId, input.Release)
err = i.checkPublish(ctx, serviceId, input.Release)
if err != nil {
return nil, err
}
previous := ""
running, err := m.releaseService.GetRunning(ctx, serviceId)
running, err := i.releaseService.GetRunning(ctx, serviceId)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
@@ -315,42 +386,42 @@ func (m *imlPublishModule) Apply(ctx context.Context, serviceId string, input *d
previous = running.UUID
}
releaseToPublish, err := m.releaseService.GetRelease(ctx, input.Release)
releaseToPublish, err := i.releaseService.GetRelease(ctx, input.Release)
if err != nil {
// 目标版本不存在
return nil, err
}
newPublishId := uuid.NewString()
diff, ok, err := m.projectDiffModule.DiffForLatest(ctx, serviceId, previous)
diff, ok, err := i.projectDiffModule.DiffForLatest(ctx, serviceId, previous)
if err != nil {
return nil, err
}
if !ok {
return nil, errors.New("latest completeness check failed")
}
err = m.publishService.Create(ctx, newPublishId, serviceId, releaseToPublish.UUID, previous, releaseToPublish.Version, input.Remark, diff)
err = i.publishService.Create(ctx, newPublishId, serviceId, releaseToPublish.UUID, previous, releaseToPublish.Version, input.Remark, diff)
if err != nil {
return nil, err
}
np, err := m.publishService.Get(ctx, newPublishId)
np, err := i.publishService.Get(ctx, newPublishId)
if err != nil {
return nil, err
}
return dto.FromModel(np, releaseToPublish.Remark), nil
}
func (m *imlPublishModule) CheckPublish(ctx context.Context, serviceId string, releaseId string) (*dto.DiffOut, error) {
_, err := m.serviceService.Check(ctx, serviceId, asServer)
func (i *imlPublishModule) CheckPublish(ctx context.Context, serviceId string, releaseId string) (*dto.DiffOut, error) {
_, err := i.serviceService.Check(ctx, serviceId, asServer)
if err != nil {
return nil, err
}
err = m.checkPublish(ctx, serviceId, releaseId)
err = i.checkPublish(ctx, serviceId, releaseId)
if err != nil {
return nil, err
}
running, err := m.releaseService.GetRunning(ctx, serviceId)
running, err := i.releaseService.GetRunning(ctx, serviceId)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
@@ -360,30 +431,30 @@ func (m *imlPublishModule) CheckPublish(ctx context.Context, serviceId string, r
}
if releaseId == "" {
// 发布latest 版本
diff, _, err := m.projectDiffModule.DiffForLatest(ctx, serviceId, runningReleaseId)
diff, _, err := i.projectDiffModule.DiffForLatest(ctx, serviceId, runningReleaseId)
if err != nil {
return nil, err
}
return m.projectDiffModule.Out(ctx, diff)
return i.projectDiffModule.Out(ctx, diff)
} else {
// 发布 releaseId 版本, 返回 与当前版本的差异
diff, err := m.projectDiffModule.Diff(ctx, serviceId, runningReleaseId, releaseId)
diff, err := i.projectDiffModule.Diff(ctx, serviceId, runningReleaseId, releaseId)
if err != nil {
return nil, err
}
return m.projectDiffModule.Out(ctx, diff)
return i.projectDiffModule.Out(ctx, diff)
}
}
func (m *imlPublishModule) checkPublish(ctx context.Context, serviceId string, releaseId string) error {
flows, err := m.publishService.ListForStatus(ctx, serviceId, publish.StatusApply, publish.StatusAccept)
func (i *imlPublishModule) checkPublish(ctx context.Context, serviceId string, releaseId string) error {
flows, err := i.publishService.ListForStatus(ctx, serviceId, publish.StatusApply, publish.StatusAccept)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
if len(flows) > 0 {
return errors.New("正在发布中")
}
running, err := m.releaseService.GetRunning(ctx, serviceId)
running, err := i.releaseService.GetRunning(ctx, serviceId)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
@@ -396,8 +467,8 @@ func (m *imlPublishModule) checkPublish(ctx context.Context, serviceId string, r
}
return nil
}
func (m *imlPublishModule) Close(ctx context.Context, serviceId, id string) error {
err := m.publishService.SetStatus(ctx, serviceId, id, publish.StatusClose)
func (i *imlPublishModule) Close(ctx context.Context, serviceId, id string) error {
err := i.publishService.SetStatus(ctx, serviceId, id, publish.StatusClose)
if err != nil {
return err
}
@@ -405,12 +476,12 @@ func (m *imlPublishModule) Close(ctx context.Context, serviceId, id string) erro
return nil
}
func (m *imlPublishModule) Stop(ctx context.Context, serviceId string, id string) error {
_, err := m.serviceService.Check(ctx, serviceId, asServer)
func (i *imlPublishModule) Stop(ctx context.Context, serviceId string, id string) error {
_, err := i.serviceService.Check(ctx, serviceId, asServer)
if err != nil {
return err
}
flow, err := m.publishService.Get(ctx, id)
flow, err := i.publishService.Get(ctx, id)
if err != nil {
return err
}
@@ -425,44 +496,44 @@ func (m *imlPublishModule) Stop(ctx context.Context, serviceId string, id string
if flow.Status == publish.StatusApply {
status = publish.StatusClose
}
return m.publishService.SetStatus(ctx, serviceId, id, status)
return i.publishService.SetStatus(ctx, serviceId, id, status)
}
func (m *imlPublishModule) Refuse(ctx context.Context, serviceId string, id string, commits string) error {
_, err := m.serviceService.Check(ctx, serviceId, asServer)
func (i *imlPublishModule) Refuse(ctx context.Context, serviceId string, id string, commits string) error {
_, err := i.serviceService.Check(ctx, serviceId, asServer)
if err != nil {
return err
}
return m.publishService.Refuse(ctx, serviceId, id, commits)
return i.publishService.Refuse(ctx, serviceId, id, commits)
}
func (m *imlPublishModule) Accept(ctx context.Context, serviceId string, id string, commits string) error {
_, err := m.serviceService.Check(ctx, serviceId, asServer)
func (i *imlPublishModule) Accept(ctx context.Context, serviceId string, id string, commits string) error {
_, err := i.serviceService.Check(ctx, serviceId, asServer)
if err != nil {
return err
}
return m.publishService.Accept(ctx, serviceId, id, commits)
return i.publishService.Accept(ctx, serviceId, id, commits)
}
func (m *imlPublishModule) publish(ctx context.Context, id string, clusterId string, projectRelease *gateway.ProjectRelease) error {
func (i *imlPublishModule) publish(ctx context.Context, id string, clusterId string, projectRelease *gateway.ProjectRelease) error {
publishStatus := &publish.Status{
Publish: id,
Status: publish.StatusPublishing,
UpdateAt: time.Now(),
}
err := m.publishService.SetPublishStatus(ctx, publishStatus)
err := i.publishService.SetPublishStatus(ctx, publishStatus)
if err != nil {
return fmt.Errorf("set publishing publishStatus error: %v", err)
}
defer func() {
err := m.publishService.SetPublishStatus(ctx, publishStatus)
err := i.publishService.SetPublishStatus(ctx, publishStatus)
if err != nil {
log.Errorf("set publishing publishStatus error: %v", err)
}
}()
client, err := m.clusterService.GatewayClient(ctx, clusterId)
client, err := i.clusterService.GatewayClient(ctx, clusterId)
if err != nil {
publishStatus.Status = publish.StatusPublishError
publishStatus.Error = err.Error()
@@ -494,12 +565,12 @@ func (m *imlPublishModule) publish(ctx context.Context, id string, clusterId str
return nil
}
func (m *imlPublishModule) Publish(ctx context.Context, serviceId string, id string) error {
_, err := m.serviceService.Check(ctx, serviceId, asServer)
func (i *imlPublishModule) Publish(ctx context.Context, serviceId string, id string) error {
_, err := i.serviceService.Check(ctx, serviceId, asServer)
if err != nil {
return err
}
flow, err := m.publishService.Get(ctx, id)
flow, err := i.publishService.Get(ctx, id)
if err != nil {
return err
}
@@ -509,7 +580,7 @@ func (m *imlPublishModule) Publish(ctx context.Context, serviceId string, id str
if flow.Status != publish.StatusAccept && flow.Status != publish.StatusDone {
return errors.New("只有通过状态才能发布")
}
clusters, err := m.clusterService.List(ctx)
clusters, err := i.clusterService.List(ctx)
if err != nil {
return err
}
@@ -517,21 +588,21 @@ func (m *imlPublishModule) Publish(ctx context.Context, serviceId string, id str
return i.Uuid
})
projectReleaseMap, err := m.getReleaseInfo(ctx, serviceId, flow.Release, flow.Release, clusterIds)
projectReleaseMap, err := i.getReleaseInfo(ctx, serviceId, flow.Release, flow.Release, clusterIds)
if err != nil {
return err
}
hasError := false
return m.transaction.Transaction(ctx, func(ctx context.Context) error {
return i.transaction.Transaction(ctx, func(ctx context.Context) error {
for _, c := range clusters {
err = m.publish(ctx, flow.Id, c.Uuid, projectReleaseMap[c.Uuid])
err = i.publish(ctx, flow.Id, c.Uuid, projectReleaseMap[c.Uuid])
if err != nil {
hasError = true
log.Error(err)
continue
}
}
err = m.releaseService.SetRunning(ctx, serviceId, flow.Release)
err = i.releaseService.SetRunning(ctx, serviceId, flow.Release)
if err != nil {
return err
}
@@ -540,29 +611,38 @@ func (m *imlPublishModule) Publish(ctx context.Context, serviceId string, id str
status = publish.StatusPublishError
}
if status == publish.StatusDone {
info, err := m.serviceService.Get(ctx, serviceId)
info, err := i.serviceService.Get(ctx, serviceId)
if err != nil {
return err
}
apiDocCommit, err := i.apiDocService.LatestDocCommit(ctx, serviceId)
if err != nil {
return err
}
isReleased := true
i.serviceOverviewService.Update(ctx, serviceId, &service_overview.Update{
ReleaseApiCount: &apiDocCommit.Data.APICount,
IsReleased: &isReleased,
})
if info.EnableMCP {
err = m.updateMCPServer(ctx, serviceId, info.Name, flow.Version)
err = mcp_server.SetServerByOpenapi(serviceId, info.Name, flow.Version, apiDocCommit.Data.Content)
if err != nil {
return err
}
}
}
return m.publishService.SetStatus(ctx, serviceId, id, status)
return i.publishService.SetStatus(ctx, serviceId, id, status)
})
}
func (m *imlPublishModule) List(ctx context.Context, serviceId string, page, pageSize int) ([]*dto.Publish, int64, error) {
_, err := m.serviceService.Check(ctx, serviceId, asServer)
func (i *imlPublishModule) List(ctx context.Context, serviceId string, page, pageSize int) ([]*dto.Publish, int64, error) {
_, err := i.serviceService.Check(ctx, serviceId, asServer)
if err != nil {
return nil, 0, err
}
list, total, err := m.publishService.ListProjectPage(ctx, serviceId, page, pageSize)
list, total, err := i.publishService.ListProjectPage(ctx, serviceId, page, pageSize)
if err != nil {
return nil, 0, err
}
@@ -588,95 +668,34 @@ func (i *imlPublishModule) updateMCPServer(ctx context.Context, sid string, name
if err != nil {
return fmt.Errorf("get api doc commit error: %w", err)
}
mcpInfo, err := mcp_server.ConvertMCPFromOpenAPI3Data([]byte(commitDoc.Data.Content))
if err != nil {
return fmt.Errorf("convert mcp from openapi3 data error: %w", err)
}
tools := make([]mcp_server.ITool, 0, len(mcpInfo.Apis))
for _, a := range mcpInfo.Apis {
toolOptions := make([]mcp.ToolOption, 0, len(a.Params)+2)
toolOptions = append(toolOptions, mcp.WithDescription(a.Description))
headers := make(map[string]interface{})
queries := make(map[string]interface{})
path := make(map[string]interface{})
for _, v := range a.Params {
p := map[string]interface{}{
"type": "string",
"required": v.Required,
"description": v.Description,
}
switch v.In {
case "header":
headers[v.Name] = p
case "query":
queries[v.Name] = p
case "path":
path[v.Name] = p
}
}
if len(headers) > 0 {
toolOptions = append(toolOptions, mcp.WithObject(mcp_server.MCPHeader, mcp.Properties(headers), mcp.Description("request headers.")))
}
if len(queries) > 0 {
toolOptions = append(toolOptions, mcp.WithObject(mcp_server.MCPQuery, mcp.Properties(queries), mcp.Description("request queries.")))
}
if len(path) > 0 {
toolOptions = append(toolOptions, mcp.WithObject(mcp_server.MCPPath, mcp.Properties(path), mcp.Description("request path params.")))
}
if a.Body != nil {
type Schema struct {
Type string `mapstructure:"type"`
Properties map[string]interface{} `mapstructure:"properties"`
Items interface{} `mapstructure:"items"`
}
var tmp Schema
err = mapstructure.Decode(a.Body, &tmp)
if err != nil {
return err
}
//switch a.ContentType {
//case "application/json":
switch tmp.Type {
case "object":
toolOptions = append(toolOptions, mcp.WithObject(mcp_server.MCPBody, mcp.Properties(tmp.Properties), mcp.Description("request body,it is avalible when method is POST、PUT、PATCH.")))
case "array":
toolOptions = append(toolOptions, mcp.WithArray(mcp_server.MCPBody, mcp.Items(tmp.Items), mcp.Description("request body,it is avalible when method is POST、PUT、PATCH.")))
}
//case "application/x-www-form-urlencoded":
// toolOptions = append(toolOptions, mcp.WithString(mcp_server.MCPBody, mcp.Items(tmp.Items), mcp.Description("request body,it is avalible when method is POST、PUT、PATCH.")))
}
tools = append(tools, mcp_server.NewTool(a.Summary, a.Path, a.Method, a.ContentType, toolOptions...))
}
mcp_server.SetSSEServer(sid, name, version, tools...)
return nil
return mcp_server.SetServerByOpenapi(sid, name, version, commitDoc.Data.Content)
}
func (m *imlPublishModule) Detail(ctx context.Context, serviceId string, id string) (*dto.PublishDetail, error) {
_, err := m.serviceService.Check(ctx, serviceId, asServer)
func (i *imlPublishModule) Detail(ctx context.Context, serviceId string, id string) (*dto.PublishDetail, error) {
_, err := i.serviceService.Check(ctx, serviceId, asServer)
if err != nil {
return nil, err
}
flow, err := m.publishService.Get(ctx, id)
flow, err := i.publishService.Get(ctx, id)
if err != nil {
return nil, err
}
if flow.Service != serviceId {
return nil, errors.New("项目不一致")
}
diff, err := m.publishService.GetDiff(ctx, id)
diff, err := i.publishService.GetDiff(ctx, id)
if err != nil {
return nil, err
}
out, err := m.projectDiffModule.Out(ctx, diff)
out, err := i.projectDiffModule.Out(ctx, diff)
if err != nil {
return nil, err
}
publishStatuses, err := m.PublishStatuses(ctx, serviceId, id)
publishStatuses, err := i.PublishStatuses(ctx, serviceId, id)
if err != nil {
return nil, err
}
releaseInfo, err := m.releaseService.GetRelease(ctx, flow.Release)
releaseInfo, err := i.releaseService.GetRelease(ctx, flow.Release)
if err != nil {
return nil, err
}
-1
View File
@@ -65,7 +65,6 @@ func (m *imlReleaseModule) latestStrategyCommits(ctx context.Context, serviceId
}
func (m *imlReleaseModule) Create(ctx context.Context, serviceId string, input *dto.CreateInput) (string, error) {
proInfo, err := m.projectService.Check(ctx, serviceId, projectRuleMustServer)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
+2 -2
View File
@@ -30,7 +30,7 @@ type Create struct {
MatchRules []Match `json:"match"`
Upstream string `json:"upstream"`
Proxy *InputProxy `json:"proxy"`
Disable bool `json:"disabled"`
Disable bool `json:"disable"`
}
type InputProxy struct {
@@ -70,7 +70,7 @@ type Edit struct {
Methods *[]string `json:"methods"`
Protocols *[]string `json:"protocols"`
MatchRules *[]Match `json:"match"`
Disable *bool `json:"disabled"`
Disable *bool `json:"disable"`
Upstream *string `json:"upstream"`
}
+2 -1
View File
@@ -11,6 +11,7 @@ import (
type Item struct {
Id string `json:"id"`
Name string `json:"name"`
Methods []string `json:"methods"`
Protocols []string `json:"protocols"`
Path string `json:"request_path"`
@@ -34,7 +35,7 @@ type Detail struct {
SimpleDetail
Proxy *Proxy `json:"proxy"`
Protocols []string `json:"protocols"`
Disable bool `json:"disabled"`
Disable bool `json:"disable"`
//Doc map[string]interface{} `json:"doc"`
}
+1
View File
@@ -205,6 +205,7 @@ func (i *imlRouterModule) Search(ctx context.Context, keyword string, serviceId
}
return &router_dto.Item{
Id: item.UUID,
Name: item.Name,
Methods: item.Methods,
Protocols: protocols,
Path: item.Path,
+1
View File
@@ -31,6 +31,7 @@ type EditService struct {
Name *string `json:"name"`
Description *string `json:"description"`
ServiceType *string `json:"service_type"`
Prefix *string `json:"prefix"`
Catalogue *string `json:"catalogue"`
Logo *string `json:"logo"`
Tags *[]string `json:"tags"`
+77
View File
@@ -221,3 +221,80 @@ type ExportApp struct {
Description string `json:"description"`
Team string `json:"team"`
}
type Overview struct {
Id string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
EnableMCP bool `json:"enable_mcp"`
ServiceKind string `json:"service_kind"`
SubscriberNum int64 `json:"subscriber_num"`
InvokeNum int64 `json:"invoke_num"`
Logo string `json:"logo"`
AvailableMonitor bool `json:"available_monitor"`
IsReleased bool `json:"is_released"`
Catalogue auto.Label `json:"catalogue" aolabel:"catalogue"`
APINum int64 `json:"api_num"`
}
type AILogItem struct {
Id string `json:"id"`
API auto.Label `json:"api" aolabel:"api"`
Status int64 `json:"status"`
LogTime auto.TimeLabel `json:"log_time"`
Ip string `json:"ip"`
Token int64 `json:"token"`
TokenPerSecond int64 `json:"token_per_second"`
Consumer auto.Label `json:"consumer" aolabel:"service"`
Provider auto.Label `json:"provider" aolabel:"ai_provider"`
Model string `json:"model"`
}
type RestLogItem struct {
Id string `json:"id"`
API auto.Label `json:"api" aolabel:"api"`
Status int64 `json:"status"`
LogTime auto.TimeLabel `json:"log_time"`
Ip string `json:"ip"`
Consumer auto.Label `json:"consumer" aolabel:"service"`
ResponseTime string `json:"response_time"`
Traffic string `json:"traffic"`
}
type RestLogInfo struct {
Id string `json:"id"`
API auto.Label `json:"api" aolabel:"api"`
Consumer auto.Label `json:"consumer" aolabel:"service"`
IsSystemConsumer bool `json:"is_system_consumer"`
Status int64 `json:"status"`
Ip string `json:"ip"`
ResponseTime string `json:"response_time"`
Traffic string `json:"traffic"`
LogTime auto.TimeLabel `json:"log_time"`
Request OriginRequest `json:"request"`
Response OriginRequest `json:"response"`
}
type AILogInfo struct {
Id string `json:"id"`
API auto.Label `json:"api" aolabel:"api"`
Consumer auto.Label `json:"consumer" aolabel:"service"`
IsSystemConsumer bool `json:"is_system_consumer"`
Status int64 `json:"status"`
Ip string `json:"ip"`
Provider auto.Label `json:"provider" aolabel:"ai_provider"`
Model string `json:"model"`
LogTime auto.TimeLabel `json:"log_time"`
Request OriginAIRequest `json:"request"`
Response OriginAIRequest `json:"response"`
}
type OriginRequest struct {
Header string `json:"header"`
Origin string `json:"origin"`
Body string `json:"body"`
}
type OriginAIRequest struct {
OriginRequest
Token int64 `json:"token"`
}
+267 -79
View File
@@ -5,16 +5,17 @@ import (
"encoding/json"
"errors"
"fmt"
"net/url"
"sort"
"strings"
"time"
"github.com/mitchellh/mapstructure"
service_overview "github.com/APIParkLab/APIPark/service/service-overview"
"github.com/APIParkLab/APIPark/common"
"github.com/eolinker/go-common/register"
"github.com/mark3labs/mcp-go/mcp"
mcp_server "github.com/APIParkLab/APIPark/mcp-server"
"github.com/APIParkLab/APIPark/service/release"
@@ -27,6 +28,7 @@ import (
model_runtime "github.com/APIParkLab/APIPark/ai-provider/model-runtime"
"github.com/APIParkLab/APIPark/resources/access"
log_service "github.com/APIParkLab/APIPark/service/log"
"github.com/eolinker/eosc/log"
"github.com/eolinker/go-common/server"
@@ -79,14 +81,222 @@ type imlServiceModule struct {
tagService tag.ITagService `autowired:""`
localModelService ai_local.ILocalModelService `autowired:""`
serviceTagService service_tag.ITagService `autowired:""`
apiService api.IAPIService `autowired:""`
apiDocService api_doc.IAPIDocService `autowired:""`
clusterService cluster.IClusterService `autowired:""`
transaction store.ITransaction `autowired:""`
serviceOverviewService service_overview.IOverviewService `autowired:""`
serviceTagService service_tag.ITagService `autowired:""`
apiService api.IAPIService `autowired:""`
apiDocService api_doc.IAPIDocService `autowired:""`
clusterService cluster.IClusterService `autowired:""`
subscribeServer subscribe.ISubscribeService `autowired:""`
releaseService release.IReleaseService `autowired:""`
serviceModelMappingService service_model_mapping.IServiceModelMappingService `autowired:""`
logService log_service.ILogService `autowired:""`
transaction store.ITransaction `autowired:""`
}
func formatHeader(header string) string {
result, err := url.QueryUnescape(header)
if err != nil {
return header
}
result = strings.ReplaceAll(result, "&", "\n")
result = strings.ReplaceAll(result, "=", ": ")
return result
}
func (i *imlServiceModule) RestLogInfo(ctx context.Context, serviceId string, logId string) (*service_dto.RestLogInfo, error) {
c, err := i.clusterService.Get(ctx, cluster.DefaultClusterID)
if err != nil {
return nil, fmt.Errorf("cluster %s not found", cluster.DefaultClusterID)
}
info, err := i.logService.LogInfo(ctx, "loki", c.Cluster, logId)
if err != nil {
return nil, err
}
if info.Service != serviceId {
return nil, errors.New("service not match")
}
logInfo := &service_dto.RestLogInfo{
Id: info.ID,
API: auto.UUID(info.API),
Consumer: auto.UUID(info.Consumer),
Status: info.StatusCode,
Ip: info.RemoteIP,
ResponseTime: common.FormatTime(info.ResponseTime),
Traffic: common.FormatByte(info.Traffic),
LogTime: auto.TimeLabel(info.RecordTime),
Request: service_dto.OriginRequest{
Header: formatHeader(info.RequestHeader),
Origin: info.RequestBody,
Body: info.RequestBody,
},
Response: service_dto.OriginRequest{
Header: formatHeader(info.ResponseHeader),
Origin: info.ResponseBody,
Body: info.ResponseBody,
},
}
if info.Consumer == "apipark-global" {
logInfo.IsSystemConsumer = true
logInfo.Consumer = auto.Label{
Id: info.Consumer,
Name: "System Consumer",
}
}
return logInfo, nil
}
func (i *imlServiceModule) AILogInfo(ctx context.Context, serviceId string, logId string) (*service_dto.AILogInfo, error) {
c, err := i.clusterService.Get(ctx, cluster.DefaultClusterID)
if err != nil {
return nil, fmt.Errorf("cluster %s not found", cluster.DefaultClusterID)
}
info, err := i.logService.LogInfo(ctx, "loki", c.Cluster, logId)
if err != nil {
return nil, err
}
if info.Service != serviceId {
return nil, errors.New("service not match")
}
response, err := parseAIResponse(info.ResponseBody)
if err != nil {
response = info.ResponseBody
}
logInfo := &service_dto.AILogInfo{
Id: info.ID,
API: auto.UUID(info.API),
Consumer: auto.UUID(info.Consumer),
Status: info.StatusCode,
Ip: info.RemoteIP,
Provider: auto.UUID(info.AIProvider),
Model: info.AIModel,
LogTime: auto.TimeLabel(info.RecordTime),
Request: service_dto.OriginAIRequest{
OriginRequest: service_dto.OriginRequest{
Header: formatHeader(info.RequestHeader),
Origin: info.RequestBody,
Body: parseAIRequest(info.RequestBody),
},
Token: info.InputToken,
},
Response: service_dto.OriginAIRequest{
OriginRequest: service_dto.OriginRequest{
Header: formatHeader(info.ResponseHeader),
Origin: info.ResponseBody,
Body: response,
},
Token: info.OutputToken,
},
}
if info.Consumer == "apipark-global" {
logInfo.IsSystemConsumer = true
logInfo.Consumer = auto.Label{
Id: info.Consumer,
Name: "System Consumer",
}
}
return logInfo, nil
}
func (i *imlServiceModule) RestLogs(ctx context.Context, serviceId string, start int64, end int64, page int, size int) ([]*service_dto.RestLogItem, int64, error) {
list, total, err := i.logService.LogRecordsByService(ctx, serviceId, time.Unix(start, 0), time.Unix(end, 0), page, size)
if err != nil {
return nil, 0, err
}
return utils.SliceToSlice(list, func(s *log_service.Item) *service_dto.RestLogItem {
item := &service_dto.RestLogItem{
Id: s.ID,
API: auto.UUID(s.API),
Status: s.StatusCode,
LogTime: auto.TimeLabel(s.RecordTime),
Ip: s.RemoteIP,
Consumer: auto.UUID(s.Consumer),
ResponseTime: common.FormatTime(s.ResponseTime),
Traffic: common.FormatByte(s.Traffic),
}
if s.Consumer == "apipark-global" {
item.Consumer = auto.Label{
Id: s.Consumer,
Name: "System Consumer",
}
}
return item
}), total, nil
}
func (i *imlServiceModule) AILogs(ctx context.Context, serviceId string, start int64, end int64, page int, size int) ([]*service_dto.AILogItem, int64, error) {
list, total, err := i.logService.LogRecordsByService(ctx, serviceId, time.Unix(start, 0), time.Unix(end, 0), page, size)
if err != nil {
return nil, 0, err
}
return utils.SliceToSlice(list, func(s *log_service.Item) *service_dto.AILogItem {
var tokenPerSecond int64 = 0
if s.ResponseTime > 0 {
tokenPerSecond = s.TotalToken * 1000 / s.ResponseTime
}
item := &service_dto.AILogItem{
Id: s.ID,
API: auto.UUID(s.API),
Status: s.StatusCode,
LogTime: auto.TimeLabel(s.RecordTime),
Ip: s.RemoteIP,
Token: s.TotalToken,
TokenPerSecond: tokenPerSecond,
Consumer: auto.UUID(s.Consumer),
Provider: auto.UUID(s.AIProvider),
Model: s.AIModel,
}
if s.Consumer == "apipark-global" {
item.Consumer = auto.Label{
Id: s.Consumer,
Name: "System Consumer",
}
}
return item
}), total, nil
}
func (i *imlServiceModule) ServiceOverview(ctx context.Context, id string) (*service_dto.Overview, error) {
info, err := i.serviceService.Get(ctx, id)
if err != nil {
return nil, err
}
apiCountMap, err := i.apiDocService.APICountByServices(ctx, id)
if err != nil {
return nil, err
}
subscribeMap, err := i.subscribeServer.CountMapByService(ctx, subscribe.ApplyStatusSubscribe, id)
if err != nil {
return nil, err
}
result := &service_dto.Overview{
Id: info.Id,
Name: info.Name,
Description: info.Description,
EnableMCP: info.EnableMCP,
ServiceKind: info.Kind.String(),
SubscriberNum: subscribeMap[id],
Logo: info.Logo,
Catalogue: auto.UUID(info.Catalogue),
APINum: apiCountMap[id],
}
_, err = i.releaseService.GetRunning(ctx, id)
if err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
} else {
result.IsReleased = true
}
return result, nil
}
func (i *imlServiceModule) OnInit() {
@@ -97,6 +307,7 @@ func (i *imlServiceModule) OnInit() {
log.Error(err)
return
}
for _, s := range services {
err = i.updateMCPServer(ctx, s.Id, s.Name, "1.0")
if err != nil {
@@ -104,6 +315,28 @@ func (i *imlServiceModule) OnInit() {
return
}
}
overviews, err := i.serviceOverviewService.List(ctx)
if err != nil {
log.Error(err)
return
}
if len(overviews) > 0 {
return
}
countMap, err := i.apiDocService.APICountByServices(ctx)
if err != nil {
log.Error(err)
return
}
for k, v := range countMap {
err = i.serviceOverviewService.Update(ctx, k, &service_overview.Update{
ApiCount: &v,
})
if err != nil {
log.Error(err)
return
}
}
})
}
@@ -162,67 +395,11 @@ func (i *imlServiceModule) updateMCPServer(ctx context.Context, sid string, name
if err != nil {
return fmt.Errorf("get api doc commit error: %w", err)
}
mcpInfo, err := mcp_server.ConvertMCPFromOpenAPI3Data([]byte(commitDoc.Data.Content))
if err != nil {
return fmt.Errorf("convert mcp from openapi3 data error: %w", err)
}
tools := make([]mcp_server.ITool, 0, len(mcpInfo.Apis))
for _, a := range mcpInfo.Apis {
toolOptions := make([]mcp.ToolOption, 0, len(a.Params)+2)
toolOptions = append(toolOptions, mcp.WithDescription(a.Description))
headers := make(map[string]interface{})
queries := make(map[string]interface{})
path := make(map[string]interface{})
for _, v := range a.Params {
p := map[string]interface{}{
"type": "string",
"required": v.Required,
"description": v.Description,
}
switch v.In {
case "header":
headers[v.Name] = p
case "query":
queries[v.Name] = p
case "path":
path[v.Name] = p
}
}
if len(headers) > 0 {
toolOptions = append(toolOptions, mcp.WithObject(mcp_server.MCPHeader, mcp.Properties(headers), mcp.Description("request headers.")))
}
if len(queries) > 0 {
toolOptions = append(toolOptions, mcp.WithObject(mcp_server.MCPQuery, mcp.Properties(queries), mcp.Description("request queries.")))
}
if len(path) > 0 {
toolOptions = append(toolOptions, mcp.WithObject(mcp_server.MCPPath, mcp.Properties(path), mcp.Description("request path params.")))
}
if a.Body != nil {
type Schema struct {
Type string `mapstructure:"type"`
Properties map[string]interface{} `mapstructure:"properties"`
Items interface{} `mapstructure:"items"`
}
var tmp Schema
err = mapstructure.Decode(a.Body, &tmp)
if err != nil {
return err
}
switch tmp.Type {
case "object":
toolOptions = append(toolOptions, mcp.WithObject(mcp_server.MCPBody, mcp.Properties(tmp.Properties), mcp.Description("request body,it is avalible when method is POST、PUT、PATCH.")))
case "array":
toolOptions = append(toolOptions, mcp.WithArray(mcp_server.MCPBody, mcp.Items(tmp.Items), mcp.Description("request body,it is avalible when method is POST、PUT、PATCH.")))
}
}
tools = append(tools, mcp_server.NewTool(a.Summary, a.Path, a.Method, a.ContentType, toolOptions...))
}
mcp_server.SetSSEServer(sid, name, version, tools...)
return nil
return mcp_server.SetServerByOpenapi(sid, name, version, commitDoc.Data.Content)
}
func (i *imlServiceModule) deleteMCPServer(ctx context.Context, sid string) {
mcp_server.DelSSEServer(sid)
mcp_server.DelServer(sid)
}
func (i *imlServiceModule) ExportAll(ctx context.Context) ([]*service_dto.ExportService, error) {
@@ -303,7 +480,14 @@ func (i *imlServiceModule) SearchMyServices(ctx context.Context, teamId string,
serviceIds := utils.SliceToSlice(services, func(p *service.Service) string {
return p.Id
})
apiCountMap, err := i.apiDocService.APICountByServices(ctx, serviceIds...)
//apiCountMap, err := i.apiDocService.APICountByServices(ctx, serviceIds...)
//if err != nil {
// return nil, err
//}
//serviceIds := utils.SliceToSlice(services, func(s *service.Service) string {
// return s.Id
//})
overviewMap, err := i.serviceOverviewService.Map(ctx, serviceIds...)
if err != nil {
return nil, err
}
@@ -313,10 +497,12 @@ func (i *imlServiceModule) SearchMyServices(ctx context.Context, teamId string,
if teamId != "" && model.Team != teamId {
continue
}
apiCount := apiCountMap[model.Id]
item := toServiceItem(model)
item.ApiNum = apiCount
item.CanDelete = apiCount == 0
if ov, ok := overviewMap[model.Id]; ok {
item.ApiNum = ov.ApiCount
item.CanDelete = ov.ApiCount == 0
}
items = append(items, item)
}
@@ -422,22 +608,21 @@ func (i *imlServiceModule) Search(ctx context.Context, teamID string, keyword st
if err != nil {
return nil, err
}
serviceIds := utils.SliceToSlice(list, func(s *service.Service) string {
return s.Id
})
apiCountMap, err := i.apiDocService.APICountByServices(ctx, serviceIds...)
overviewMap, err := i.serviceOverviewService.Map(ctx, serviceIds...)
if err != nil {
return nil, err
}
items := make([]*service_dto.ServiceItem, 0, len(list))
for _, model := range list {
apiCount := apiCountMap[model.Id]
item := toServiceItem(model)
item.ApiNum = apiCount
item.CanDelete = apiCount == 0
if v, ok := overviewMap[model.Id]; ok {
item.ApiNum = v.ApiCount
item.CanDelete = v.ApiCount == 0
}
items = append(items, item)
}
return items, nil
@@ -527,7 +712,7 @@ func (i *imlServiceModule) Create(ctx context.Context, teamID string, input *ser
mo.AsServer = *input.AsServer
}
input.Prefix = strings.Trim(strings.Trim(input.Prefix, " "), "/")
//input.Prefix = strings.Trim(strings.Trim(input.Prefix, " "), "/")
err := i.transaction.Transaction(ctx, func(ctx context.Context) error {
if input.Tags != nil {
tags, err := i.getTagUuids(ctx, input.Tags)
@@ -632,6 +817,7 @@ func (i *imlServiceModule) Edit(ctx context.Context, id string, input *service_d
ServiceType: serviceType,
Catalogue: input.Catalogue,
AdditionalConfig: &info.AdditionalConfig,
Prefix: input.Prefix,
ApprovalType: &approvalType,
EnableMCP: input.EnableMCP,
}
@@ -746,7 +932,9 @@ func (i *imlServiceModule) Delete(ctx context.Context, id string) error {
Id: id,
})
if err != nil {
return err
if err.Error() != "nil" {
return err
}
}
err = client.Subscribe().Offline(ctx, &gateway.SubscribeRelease{
Service: id,
+10
View File
@@ -34,6 +34,16 @@ type IServiceModule interface {
//MySimple 获取我的简易项目列表
MySimple(ctx context.Context) ([]*service_dto.SimpleServiceItem, error)
ServiceOverview(ctx context.Context, id string) (*service_dto.Overview, error)
ILogModule
}
type ILogModule interface {
AILogs(ctx context.Context, serviceId string, start int64, end int64, page int, size int) ([]*service_dto.AILogItem, int64, error)
RestLogs(ctx context.Context, serviceId string, start int64, end int64, page int, size int) ([]*service_dto.RestLogItem, int64, error)
RestLogInfo(ctx context.Context, serviceId string, logId string) (*service_dto.RestLogInfo, error)
AILogInfo(ctx context.Context, serviceId string, logId string) (*service_dto.AILogInfo, error)
}
type IServiceDocModule interface {
+115
View File
@@ -0,0 +1,115 @@
package service
import (
"bufio"
"encoding/json"
"strings"
)
// ChatCompletionChunk represents the structure of a single chunk in the streaming response
type ChatCompletionChunk struct {
Object string `json:"object"`
Choices []Choice `json:"choices"`
}
// ChatCompletion represents the structure of a non-streaming response
type ChatCompletion struct {
Object string `json:"object"`
Choices []FullChoice `json:"choices"`
}
// Choice represents a choice in the streaming chunk
type Choice struct {
Delta Delta `json:"delta"`
FinishReason *string `json:"finish_reason"`
}
// FullChoice represents a choice in the non-streaming response
type FullChoice struct {
Message Message `json:"message"`
}
// Delta represents the delta content in a streaming choice
type Delta struct {
Content string `json:"content"`
Role string `json:"role,omitempty"`
}
// Message represents the message content in a non-streaming choice
type Message struct {
Content string `json:"content"`
Role string `json:"role"`
}
// ParseAIResponse parses both streaming and non-streaming AI responses and returns the concatenated content
func parseAIResponse(input string) (string, error) {
// First, try to parse as a non-streaming response
var nonStreaming ChatCompletion
if err := json.Unmarshal([]byte(input), &nonStreaming); err == nil && nonStreaming.Object == "chat.completion" {
var result strings.Builder
for _, choice := range nonStreaming.Choices {
result.WriteString(choice.Message.Content)
}
return result.String(), nil
}
// If not non-streaming, parse as streaming response
var result strings.Builder
scanner := bufio.NewScanner(strings.NewReader(input))
for scanner.Scan() {
line := scanner.Text()
// Skip empty lines or [DONE]
if line == "" || line == "data: [DONE]" {
continue
}
// Check if line starts with "data: "
if !strings.HasPrefix(line, "data: ") {
continue
}
// Extract JSON data
jsonData := strings.TrimPrefix(line, "data: ")
var chunk ChatCompletionChunk
if err := json.Unmarshal([]byte(jsonData), &chunk); err != nil {
return "", err
}
// Process each choice
for _, choice := range chunk.Choices {
// Append content from delta
result.WriteString(choice.Delta.Content)
// Check if this is the final chunk
if choice.FinishReason != nil && *choice.FinishReason == "stop" {
return result.String(), nil
}
}
}
if err := scanner.Err(); err != nil {
return "", err
}
return result.String(), nil
}
func parseAIRequest(ori string) string {
type aiRequest struct {
Messages []struct {
Role string `json:"role"`
Content string `json:"content"`
} `json:"messages"`
}
var req aiRequest
err := json.Unmarshal([]byte(ori), &req)
if err != nil {
return ori
}
size := len(req.Messages)
if size == 0 {
return ""
}
return req.Messages[size-1].Content
}
+40
View File
@@ -41,6 +41,46 @@ type imlAPIKeyModule struct {
transaction store.ITransaction `autowired:""`
}
func (i *imlAPIKeyModule) MyAPIKeysByApp(ctx context.Context, appId string) ([]*system_apikey_dto.AuthorizationItem, error) {
appInfo, err := i.serviceService.Get(ctx, appId)
if err != nil {
return nil, err
}
auths, err := i.applicationAuthorizationService.ListByApp(ctx, appId)
if err != nil {
return nil, err
}
result := make(map[string]*system_apikey_dto.AuthorizationItem)
for _, a := range auths {
if a.Type != "apikey" {
continue
}
m := make(map[string]string)
json.Unmarshal([]byte(a.Config), &m)
if m["apikey"] == "" {
continue
}
if _, ok := result[appInfo.Id]; !ok {
result[appInfo.Id] = &system_apikey_dto.AuthorizationItem{
Id: appInfo.Id,
Name: appInfo.Name,
Apikeys: []system_apikey_dto.SimpleItem{},
}
}
result[appInfo.Id].Apikeys = append(result[appInfo.Id].Apikeys, system_apikey_dto.SimpleItem{
Id: a.UUID,
Name: a.Name,
Value: m["apikey"],
Expired: a.ExpireTime,
})
}
return utils.MapToSlice(result, func(k string, t *system_apikey_dto.AuthorizationItem) *system_apikey_dto.AuthorizationItem {
return t
}), nil
}
func (i *imlAPIKeyModule) MyAPIKeysByService(ctx context.Context, serviceId string) ([]*system_apikey_dto.AuthorizationItem, error) {
list, err := i.subscribeService.ListBySubscribeStatus(ctx, serviceId, subscribe.ApplyStatusSubscribe)
if err != nil {
+1
View File
@@ -19,6 +19,7 @@ type IAPIKeyModule interface {
SimpleList(ctx context.Context) ([]*system_apikey_dto.SimpleItem, error)
MyAPIKeys(ctx context.Context) ([]*system_apikey_dto.SimpleItem, error)
MyAPIKeysByService(ctx context.Context, serviceId string) ([]*system_apikey_dto.AuthorizationItem, error)
MyAPIKeysByApp(ctx context.Context, appId string) ([]*system_apikey_dto.AuthorizationItem, error)
}
func init() {
+7
View File
@@ -15,15 +15,22 @@ func (p *plugin) mcpAPIs() []pm3.Api {
serviceMessagePath := fmt.Sprintf("/api/v1/%s/:serviceId/message", mcp_server.ServiceBasePath)
globalSSEPath := fmt.Sprintf("/api/v1/%s/sse", mcp_server.GlobalBasePath)
globalMessagePath := fmt.Sprintf("/api/v1/%s/message", mcp_server.GlobalBasePath)
appSSEPath := fmt.Sprintf("/api/v1/%s/sse", mcp_server.AppBasePath)
appMessagePath := fmt.Sprintf("/api/v1/%s/message", mcp_server.AppBasePath)
ignore.IgnorePath("login", http.MethodGet, serviceSSEPath)
ignore.IgnorePath("login", http.MethodPost, serviceMessagePath)
ignore.IgnorePath("login", http.MethodGet, globalSSEPath)
ignore.IgnorePath("login", http.MethodPost, globalMessagePath)
ignore.IgnorePath("login", http.MethodGet, appSSEPath)
ignore.IgnorePath("login", http.MethodPost, appMessagePath)
return []pm3.Api{
pm3.CreateApiSimple(http.MethodGet, serviceSSEPath, p.mcpController.MCPHandle),
pm3.CreateApiSimple(http.MethodPost, serviceMessagePath, p.mcpController.MCPHandle),
pm3.CreateApiSimple(http.MethodGet, globalSSEPath, p.mcpController.GlobalMCPHandle),
pm3.CreateApiSimple(http.MethodPost, globalMessagePath, p.mcpController.GlobalMCPHandle),
pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/global/mcp/config", []string{"context"}, []string{"config"}, p.mcpController.GlobalMCPConfig),
pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/app/mcp/config", []string{"context", "query:app"}, []string{"config"}, p.mcpController.AppMCPConfig),
pm3.CreateApiSimple(http.MethodGet, fmt.Sprintf("/api/v1/%s/:app/sse", mcp_server.AppBasePath), p.mcpController.AppMCPHandle),
pm3.CreateApiSimple(http.MethodPost, fmt.Sprintf("/api/v1/%s/:app/message", mcp_server.AppBasePath), p.mcpController.AppMCPHandle),
}
}
+5
View File
@@ -22,5 +22,10 @@ func (p *plugin) monitorStatisticApis() []pm3.Api {
pm3.CreateApiWidthDoc(http.MethodPost, "/api/v1/monitor/:data_type/trend", []string{"context", "rest:data_type", "query:id", "body"}, []string{"tendency", "time_interval"}, p.monitorStatisticController.InvokeTrend),
pm3.CreateApiWidthDoc(http.MethodPost, "/api/v1/monitor/:data_type/trend/:typ", []string{"context", "rest:data_type", "rest:typ", "query:api", "query:provider", "query:subscriber", "body"}, []string{"tendency", "time_interval"}, p.monitorStatisticController.InvokeTrendInner),
pm3.CreateApiWidthDoc(http.MethodPost, "/api/v1/monitor/:data_type/statistics/:typ", []string{"context", "rest:data_type", "rest:typ", "query:id", "body"}, []string{"statistics"}, p.monitorStatisticController.StatisticsInner),
pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/monitor/overview/chart/rest", []string{"context", "query:start", "query:end"}, []string{"overview"}, p.monitorStatisticController.ChartRestOverview, access.SystemAnalysisRunViewView),
pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/monitor/overview/chart/ai", []string{"context", "query:start", "query:end"}, []string{"overview"}, p.monitorStatisticController.ChartAIOverview, access.SystemAnalysisRunViewView),
pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/monitor/overview/top10/rest", []string{"context", "query:start", "query:end", "query:limit"}, []string{"apis", "consumers"}, p.monitorStatisticController.RestTopN, access.SystemAnalysisRunViewView),
pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/monitor/overview/top10/ai", []string{"context", "query:start", "query:end", "query:limit"}, []string{"apis", "consumers"}, p.monitorStatisticController.AITopN, access.SystemAnalysisRunViewView),
}
}
+10
View File
@@ -39,5 +39,15 @@ func (p *plugin) ServiceApis() []pm3.Api {
pm3.CreateApiSimple(http.MethodGet, "/api/v1/service/swagger/:id", p.serviceController.Swagger),
pm3.CreateApiSimple(http.MethodGet, "/api/v1/service/apidoc/:id", p.serviceController.Swagger),
pm3.CreateApiSimple(http.MethodGet, "/api/v1/export/openapi/:id", p.serviceController.ExportSwagger),
pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/service/monitor/top10", []string{"context", "query:service", "query:start", "query:end"}, []string{"apis", "consumers"}, p.serviceController.Top10, access.SystemWorkspaceServiceViewAll, access.TeamTeamServiceView),
pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/service/overview/monitor/ai", []string{"context", "query:service", "query:start", "query:end"}, []string{"overview"}, p.serviceController.AIChartOverview, access.SystemWorkspaceServiceViewAll, access.TeamTeamServiceView),
pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/service/overview/monitor/rest", []string{"context", "query:service", "query:start", "query:end"}, []string{"overview"}, p.serviceController.RestChartOverview, access.SystemWorkspaceServiceViewAll, access.TeamTeamServiceView),
pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/service/overview/basic", []string{"context", "query:service"}, []string{"overview"}, p.serviceController.ServiceOverview, access.SystemWorkspaceServiceViewAll, access.TeamTeamServiceView),
pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/service/logs/ai", []string{"context", "query:service", "query:start", "query:end", "query:page", "query:page_size"}, []string{"logs", "total"}, p.serviceController.AILogs, access.SystemWorkspaceServiceViewAll, access.TeamTeamServiceView),
pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/service/logs/rest", []string{"context", "query:service", "query:start", "query:end", "query:page", "query:page_size"}, []string{"logs", "total"}, p.serviceController.RestLogs, access.SystemWorkspaceServiceViewAll, access.TeamTeamServiceView),
pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/service/log/rest", []string{"context", "query:service", "query:log"}, []string{"log"}, p.serviceController.RestLogInfo, access.SystemWorkspaceServiceViewAll, access.TeamTeamServiceView),
pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/service/log/ai", []string{"context", "query:service", "query:log"}, []string{"log"}, p.serviceController.AILogInfo, access.SystemWorkspaceServiceViewAll, access.TeamTeamServiceView),
}
}
+1 -1
View File
@@ -24,7 +24,7 @@ func (p *plugin) systemApikeyApis() []pm3.Api {
pm3.CreateApiWidthDoc(http.MethodDelete, "/api/v1/system/apikey", []string{"context", "query:apikey"}, nil, p.systemAPIKeyController.Delete),
pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/system/apikeys", []string{"context", "query:keyword"}, []string{"apikeys"}, p.systemAPIKeyController.Search),
pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/my/apikeys", []string{"context"}, []string{"apikeys"}, p.systemAPIKeyController.MyAPIKeys),
pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/my/app/apikeys", []string{"context", "query:service"}, []string{"apps"}, p.systemAPIKeyController.MyAPIKeysByService),
pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/my/app/apikeys", []string{"context", "query:service", "query:app"}, []string{"apps"}, p.systemAPIKeyController.MyAPIKeysByService),
pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/simple/system/apikeys", []string{"context"}, []string{"apikeys"}, p.systemAPIKeyController.SimpleList),
}
}
+26 -6
View File
@@ -5,25 +5,45 @@ import (
"net/http"
"strings"
"github.com/eolinker/go-common/ignore"
mcp_server "github.com/APIParkLab/APIPark/mcp-server"
"github.com/eolinker/go-common/ignore"
"github.com/eolinker/go-common/pm3"
)
func (p *plugin) mcpAPIs() []pm3.Api {
globalSSEPath := fmt.Sprintf("/openapi/v1/%s/sse", strings.Trim(mcp_server.GlobalBasePath, "/"))
globalMessagePath := fmt.Sprintf("/openapi/v1/%s/message", strings.Trim(mcp_server.GlobalBasePath, "/"))
serviceMessagePath := fmt.Sprintf("/openapi/v1/%s/:serviceId/message", strings.Trim(mcp_server.ServiceBasePath, "/"))
serviceSSEPath := fmt.Sprintf("/openapi/v1/%s/:serviceId/sse", strings.Trim(mcp_server.ServiceBasePath, "/"))
ignore.IgnorePath("openapi", http.MethodPost, globalMessagePath)
appSSEPath := fmt.Sprintf("/openapi/v1/%s/:app/sse", strings.Trim(mcp_server.AppBasePath, "/"))
appMessagePath := fmt.Sprintf("/openapi/v1/%s/:app/message", strings.Trim(mcp_server.AppBasePath, "/"))
serviceSSEPath := fmt.Sprintf("/openapi/v1/%s/:serviceId/sse", strings.Trim(mcp_server.ServiceBasePath, "/"))
serviceMessagePath := fmt.Sprintf("/openapi/v1/%s/:serviceId/message", strings.Trim(mcp_server.ServiceBasePath, "/"))
serviceStreamablePath := fmt.Sprintf("/openapi/v1/%s/:serviceId/mcp", strings.Trim(mcp_server.ServiceBasePath, "/"))
ignore.IgnorePath("openapi", http.MethodPost, globalMessagePath)
ignore.IgnorePath("openapi", http.MethodGet, globalSSEPath)
ignore.IgnorePath("openapi", http.MethodPost, appMessagePath)
ignore.IgnorePath("openapi", http.MethodGet, appSSEPath)
ignore.IgnorePath("openapi", http.MethodGet, serviceSSEPath)
ignore.IgnorePath("openapi", http.MethodPost, serviceMessagePath)
ignore.IgnorePath("openapi", http.MethodGet, serviceStreamablePath)
ignore.IgnorePath("openapi", http.MethodPost, serviceStreamablePath)
ignore.IgnorePath("openapi", http.MethodDelete, serviceStreamablePath)
return []pm3.Api{
pm3.CreateApiSimple(http.MethodGet, fmt.Sprintf("/openapi/v1/%s/sse", strings.Trim(mcp_server.GlobalBasePath, "/")), p.mcpController.GlobalHandleSSE),
pm3.CreateApiSimple(http.MethodGet, globalSSEPath, p.mcpController.GlobalHandleSSE),
pm3.CreateApiSimple(http.MethodPost, globalMessagePath, p.mcpController.GlobalHandleMessage),
pm3.CreateApiSimple(http.MethodGet, appSSEPath, p.mcpController.AppHandleSSE),
pm3.CreateApiSimple(http.MethodPost, appMessagePath, p.mcpController.AppHandleMessage),
pm3.CreateApiSimple(http.MethodGet, serviceSSEPath, p.mcpController.ServiceHandleSSE),
pm3.CreateApiSimple(http.MethodPost, serviceMessagePath, p.mcpController.ServiceHandleMessage),
pm3.CreateApiSimple(http.MethodPost, serviceStreamablePath, p.mcpController.ServiceHandleStreamHTTP),
pm3.CreateApiSimple(http.MethodDelete, serviceStreamablePath, p.mcpController.ServiceHandleStreamHTTP),
pm3.CreateApiSimple(http.MethodGet, serviceStreamablePath, p.mcpController.ServiceHandleStreamHTTP),
}
}
+10
View File
@@ -202,6 +202,16 @@ curl -sSO https://download.apipark.com/install/quick-start.sh; bash quick-start.
# 📕文档
访问 [APIPark文档](https://docs.apipark.com/docs/deploy) 获取详细的安装指南、API 参考和使用说明。
<br>
友情链接
<a href="https://xroute.ai/">
<img width="1248" height="158" alt="新建 PPTX 演示文稿 (2)_02(1)" src="https://github.com/user-attachments/assets/3e1cd0c1-c4c3-4f0c-8649-810d76f3166b" />
</a>
<br>
+21
View File
@@ -131,6 +131,16 @@ system:
value: 'manager'
dependents:
- system.settings.ai_api.view
- name: login
value: 'login'
children:
- name: view
value: 'view'
guest_allow: true
- name: manager
value: 'manager'
dependents:
- system.settings.login.view
- name: ai log
value: 'ai_log'
children:
@@ -294,6 +304,17 @@ team:
value: 'manager_subscribed_services'
dependents:
- team.consumer.subscription.manager_subscribed_services
- name: mcp
cname: MCP
value: 'mcp'
children:
- name: view
value: 'view'
guest_allow: true
- name: manager
value: 'manager'
dependents:
- team.consumer.mcp.view
- name: authorization
value: 'authorization'
children:
+10 -1
View File
@@ -26,6 +26,8 @@ system:
- system.settings.general.view
- system.settings.log_configuration.manager
- system.settings.log_configuration.view
- system.settings.login.manager
- system.settings.login.view
- system.settings.mcp.view
- system.settings.mcp.manager
- system.settings.role.view
@@ -69,6 +71,8 @@ system:
- system.settings.general.view
- system.settings.log_configuration.manager
- system.settings.log_configuration.view
- system.settings.login.manager
- system.settings.login.view
- system.settings.ssl_certificate.manager
- system.settings.ssl_certificate.view
- system.settings.strategy.view
@@ -88,6 +92,8 @@ team:
permits:
- team.consumer.authorization.manager
- team.consumer.authorization.view
- team.consumer.mcp.manager
- team.consumer.mcp.view
- team.consumer.subscription.manager_subscribed_services
- team.consumer.subscription.subscribe
- team.consumer.subscription.view_subscribed_service
@@ -161,6 +167,8 @@ team:
permits:
- team.consumer.authorization.manager
- team.consumer.authorization.view
- team.consumer.mcp.manager
- team.consumer.mcp.view
- team.consumer.subscription.manager_subscribed_services
- team.consumer.subscription.subscribe
- team.consumer.subscription.view_subscribed_service
@@ -172,9 +180,10 @@ team:
- name: consumer developer
value: consumer_developer
permits:
- team.consumer.application.manager
- team.consumer.authorization.manager
- team.consumer.authorization.view
- team.consumer.mcp.manager
- team.consumer.mcp.view
- team.consumer.subscription.subscribe
- team.consumer.subscription.view_subscribed_service
- team.team.consumer.view
+8 -4
View File
@@ -1,10 +1,14 @@
# 名称:apipark通用镜像
# 创建时间:2022-10-25
FROM centos:7.9.2009
MAINTAINER liujian
FROM alpine:latest
RUN sed -i 's|https://dl-cdn.alpinelinux.org/alpine|https://mirrors.aliyun.com/alpine|g' /etc/apk/repositories \
&& apk update \
&& apk add --no-cache curl tzdata bind-tools
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone
ARG APP

Some files were not shown because too many files have changed in this diff Show More