mirror of
https://github.com/alibaba/higress.git
synced 2026-02-26 05:30:50 +08:00
Compare commits
38 Commits
v2.1.0-rc.
...
v2.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f47d3fc12 | ||
|
|
6773482300 | ||
|
|
b6d61f9568 | ||
|
|
1834d4acef | ||
|
|
7f9ae38e51 | ||
|
|
b13bce6a36 | ||
|
|
275cac9dbb | ||
|
|
8cce7f5d50 | ||
|
|
4f0834d817 | ||
|
|
7cf0dae824 | ||
|
|
707061fb68 | ||
|
|
3255925bf0 | ||
|
|
a44f7ef76e | ||
|
|
c7abfb8aff | ||
|
|
ed925ddf84 | ||
|
|
1301af4638 | ||
|
|
de6144439f | ||
|
|
e37c4dc286 | ||
|
|
b8e0baa5ab | ||
|
|
4a157e98e9 | ||
|
|
6af8b17216 | ||
|
|
4500b10a42 | ||
|
|
c5a86b5298 | ||
|
|
36806d9e5c | ||
|
|
d1700009e8 | ||
|
|
2c3188dad7 | ||
|
|
7d423cddbd | ||
|
|
0e94e1a58a | ||
|
|
b1307ba97e | ||
|
|
8ae810b01a | ||
|
|
83b38b896c | ||
|
|
1385028f01 | ||
|
|
af663b701a | ||
|
|
e5c24a10fb | ||
|
|
ea85ccb694 | ||
|
|
2467004dc9 | ||
|
|
5af818a94e | ||
|
|
728a9de165 |
69
.github/workflows/helm-docs.yaml
vendored
69
.github/workflows/helm-docs.yaml
vendored
@@ -6,7 +6,7 @@ on:
|
||||
- "*"
|
||||
paths:
|
||||
- 'helm/**'
|
||||
workflow_dispatch: ~
|
||||
workflow_dispatch: ~
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
@@ -39,7 +39,6 @@ jobs:
|
||||
rm -f ./helm-docs
|
||||
|
||||
translate-readme:
|
||||
if: ${{ ! always() }}
|
||||
needs: helm
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -52,7 +51,26 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y jq
|
||||
|
||||
- name: Compare README.md
|
||||
id: compare_readme
|
||||
run: |
|
||||
cd ./helm/higress
|
||||
BASE_BRANCH=main
|
||||
UPSTREAM_REPO=https://github.com/alibaba/higress.git
|
||||
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
git clone --depth 1 --branch $BASE_BRANCH $UPSTREAM_REPO $TEMP_DIR
|
||||
|
||||
if diff -q "$TEMP_DIR/README.md" README.md > /dev/null; then
|
||||
echo "README.md has no changes in comparison to base branch. Skipping translation."
|
||||
echo "skip_translation=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "README.md has changed in comparison to base branch. Proceeding with translation."
|
||||
echo "skip_translation=false" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Translate README.md to Chinese
|
||||
if: env.skip_translation == 'false'
|
||||
env:
|
||||
API_URL: ${{ secrets.HIGRESS_OPENAI_API_URL }}
|
||||
API_KEY: ${{ secrets.HIGRESS_OPENAI_API_KEY }}
|
||||
@@ -79,37 +97,30 @@ jobs:
|
||||
-H "Authorization: Bearer $API_KEY" \
|
||||
-d "$PAYLOAD")
|
||||
|
||||
echo "response: $RESPONSE"
|
||||
echo "Response: $RESPONSE"
|
||||
|
||||
TRANSLATED_CONTENT=$(echo "$RESPONSE" | jq -r '.choices[0].message.content')
|
||||
echo "$RESPONSE" | jq -c -r '.choices[] | .message.content' > README.zh.new.md
|
||||
|
||||
if [ -z "$TRANSLATED_CONTENT" ]; then
|
||||
echo "Translation failed! Response: $RESPONSE"
|
||||
if [ -f "README.zh.new.md" ]; then
|
||||
echo "Translation completed and saved to README.zh.new.md."
|
||||
else
|
||||
echo "Translation failed or no content returned!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "$TRANSLATED_CONTENT" > README.zh.new.md
|
||||
echo "Translation completed and saved to README.zh.new.md."
|
||||
mv README.zh.new.md README.zh.md
|
||||
|
||||
- name: Compare README.zh.md
|
||||
id: compare
|
||||
run: |
|
||||
cd ./helm/higress
|
||||
NEW_README_ZH="README.zh.new.md"
|
||||
EXISTING_README_ZH="README.zh.md"
|
||||
- name: Create Pull Request
|
||||
if: env.skip_translation == 'false'
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: "Update helm translated README.zh.md"
|
||||
branch: update-helm-readme-zh
|
||||
title: "Update helm translated README.zh.md"
|
||||
body: |
|
||||
This PR updates the translated README.zh.md file.
|
||||
|
||||
if [ ! -f "$EXISTING_README_ZH" ]; then
|
||||
echo "Add README.zh.md."
|
||||
mv "$NEW_README_ZH" "$EXISTING_README_ZH"
|
||||
echo "updated=true" >> $GITHUB_ENV
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! diff -q "$NEW_README_ZH" "$EXISTING_README_ZH"; then
|
||||
echo "Files are different. Updating README.zh.md."
|
||||
mv "$NEW_README_ZH" "$EXISTING_README_ZH"
|
||||
echo "updated=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "Files are identical. No update needed."
|
||||
echo "updated=false" >> $GITHUB_ENV
|
||||
fi
|
||||
- Automatically generated by GitHub Actions
|
||||
labels: translation, automated
|
||||
base: main
|
||||
@@ -144,7 +144,7 @@ docker-buildx-push: clean-env docker.higress-buildx
|
||||
export PARENT_GIT_TAG:=$(shell cat VERSION)
|
||||
export PARENT_GIT_REVISION:=$(TAG)
|
||||
|
||||
export ENVOY_PACKAGE_URL_PATTERN?=https://github.com/higress-group/proxy/releases/download/v2.1.3/envoy-symbol-ARCH.tar.gz
|
||||
export ENVOY_PACKAGE_URL_PATTERN?=https://github.com/higress-group/proxy/releases/download/v2.1.4/envoy-symbol-ARCH.tar.gz
|
||||
|
||||
build-envoy: prebuild
|
||||
./tools/hack/build-envoy.sh
|
||||
@@ -235,8 +235,7 @@ clean-gateway: clean-istio
|
||||
rm -rf external/proxy
|
||||
rm -rf external/go-control-plane
|
||||
rm -rf external/package/envoy.tar.gz
|
||||
rm -rf external/package/mcp-server_amd64.so
|
||||
rm -rf external/package/mcp-server_arm64.so
|
||||
rm -rf external/package/*.so
|
||||
|
||||
clean-env:
|
||||
rm -rf out/
|
||||
|
||||
18
README.md
18
README.md
@@ -14,12 +14,9 @@
|
||||
<a href="https://trendshift.io/repositories/10918" target="_blank"><img src="https://trendshift.io/api/badge/repositories/10918" alt="alibaba%2Fhigress | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
</div>
|
||||
|
||||
[**Official Site**](https://higress.io/en-us/) |
|
||||
[**Docs**](https://higress.io/en-us/docs/overview/what-is-higress) |
|
||||
[**Blog**](https://higress.io/en-us/blog) |
|
||||
[**Developer**](https://higress.io/en-us/docs/developers/developers_dev) |
|
||||
[**Higress in Cloud**](https://www.alibabacloud.com/product/microservices-engine?spm=higress-website.topbar.0.0.0)
|
||||
|
||||
[**Official Site**](https://higress.ai/en/) |
|
||||
[**MCP Server QuickStart**](https://higress.cn/en/ai/mcp-quick-start/) |
|
||||
[**Wasm Plugin Hub**](https://higress.cn/en/plugin/) |
|
||||
|
||||
<p>
|
||||
English | <a href="README_ZH.md">中文<a/> | <a href="README_JP.md">日本語<a/>
|
||||
@@ -60,7 +57,8 @@ Port descriptions:
|
||||
- Port 8080: Gateway HTTP protocol entry
|
||||
- Port 8443: Gateway HTTPS protocol entry
|
||||
|
||||
**All Higress Docker images use their own dedicated repository, unaffected by Docker Hub access restrictions in certain regions**
|
||||
> All Higress Docker images use Higress's own image repository and are not affected by Docker Hub rate limits.
|
||||
> In addition, the submission and updates of the images are protected by a security scanning mechanism (powered by Alibaba Cloud ACR), making them very secure for use in production environments.
|
||||
|
||||
For other installation methods such as Helm deployment under K8s, please refer to the official [Quick Start documentation](https://higress.io/en-us/docs/user/quickstart).
|
||||
|
||||
@@ -78,6 +76,10 @@ For other installation methods such as Helm deployment under K8s, please refer t
|
||||
|
||||

|
||||
|
||||
**🌟 Try it now!** Experience Higress-hosted Remote MCP Servers at [https://mcp.higress.ai/](https://mcp.higress.ai/)
|
||||
|
||||

|
||||
|
||||
By hosting MCP Servers with Higress, you can achieve:
|
||||
- Unified authentication and authorization mechanisms, ensuring the security of AI tool calls
|
||||
- Fine-grained rate limiting to prevent abuse and resource exhaustion
|
||||
@@ -86,6 +88,8 @@ For other installation methods such as Helm deployment under K8s, please refer t
|
||||
- Simplified deployment and management through Higress's plugin mechanism for quickly adding new MCP Servers
|
||||
- Dynamic updates without disruption: Thanks to Envoy's friendly handling of long connections and Wasm plugin's dynamic update mechanism, MCP Server logic can be updated on-the-fly without any traffic disruption or connection drops
|
||||
|
||||
[Learn more...](https://higress.cn/en/ai/mcp-quick-start/?spm=36971b57.7beea2de.0.0.d85f20a94jsWGm)
|
||||
|
||||
- **Kubernetes ingress controller**:
|
||||
|
||||
Higress can function as a feature-rich ingress controller, which is compatible with many annotations of K8s' nginx ingress controller.
|
||||
|
||||
20
README_JP.md
20
README_JP.md
@@ -73,6 +73,24 @@ K8sでのHelmデプロイなどの他のインストール方法については
|
||||
|
||||

|
||||
|
||||
- **MCP Server ホスティング**:
|
||||
|
||||
Higressは、EnvoyベースのAPIゲートウェイとして、プラグインメカニズムを通じてMCP Serverをホストすることができます。MCP(Model Context Protocol)は本質的にAIにより親和性の高いAPIであり、AI Agentが様々なツールやサービスを簡単に呼び出せるようにします。Higressはツール呼び出しの認証、認可、レート制限、可観測性などの統一機能を提供し、AIアプリケーションの開発とデプロイを簡素化します。
|
||||
|
||||

|
||||
|
||||
**🌟 今すぐ試してみよう!** [https://mcp.higress.ai/](https://mcp.higress.ai/) でHigressがホストするリモートMCPサーバーを体験できます。このプラットフォームでは、HigressがどのようにリモートMCPサーバーをホストおよび管理するかを直接体験できます。
|
||||
|
||||

|
||||
|
||||
Higressを使用してMCP Serverをホストすることで、以下のことが実現できます:
|
||||
- 統一された認証と認可メカニズム、AIツール呼び出しのセキュリティを確保
|
||||
- きめ細かいレート制限、乱用やリソース枯渇を防止
|
||||
- 包括的な監査ログ、すべてのツール呼び出し行動を記録
|
||||
- 豊富な可観測性、ツール呼び出しのパフォーマンスと健全性を監視
|
||||
- 簡素化されたデプロイと管理、Higressのプラグインメカニズムを通じて新しいMCP Serverを迅速に追加
|
||||
- 動的更新による無停止:Envoyの長時間接続に対する友好的なサポートとWasmプラグインの動的更新メカニズムにより、MCP Serverのロジックをリアルタイムで更新でき、トラフィックに完全に影響を与えず、接続が切断されることはありません
|
||||
|
||||
- **Kubernetes Ingressゲートウェイ**:
|
||||
|
||||
HigressはK8sクラスターのIngressエントリーポイントゲートウェイとして機能し、多くのK8s Nginx Ingressの注釈に対応しています。K8s Nginx IngressからHigressへのスムーズな移行が可能です。
|
||||
@@ -203,4 +221,4 @@ WeChat公式アカウント:
|
||||
<a href="#readme-top" style="text-decoration: none; color: #007bff; font-weight: bold;">
|
||||
↑ トップに戻る ↑
|
||||
</a>
|
||||
</p>
|
||||
</p>
|
||||
|
||||
@@ -89,6 +89,10 @@ K8s 下使用 Helm 部署等其他安装方式可以参考官网 [Quick Start
|
||||
|
||||

|
||||
|
||||
**🌟 立即体验!** 在 [https://mcp.higress.ai/](https://mcp.higress.ai/) 体验 Higress 托管的远程 MCP 服务器。这个平台让您可以体验基于 Higress 托管的远程 MCP 服务器的效果。
|
||||
|
||||

|
||||
|
||||
通过 Higress 托管 MCP Server,可以实现:
|
||||
- 统一的认证和鉴权机制,确保 AI 工具调用的安全性
|
||||
- 精细化的速率限制,防止滥用和资源耗尽
|
||||
|
||||
Submodule envoy/envoy updated: a2c5a07960...e114a74dd8
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
appVersion: 2.1.0-rc.2
|
||||
appVersion: 2.1.1
|
||||
description: Helm chart for deploying higress gateways
|
||||
icon: https://higress.io/img/higress_logo_small.png
|
||||
home: http://higress.io/
|
||||
@@ -15,4 +15,4 @@ dependencies:
|
||||
repository: "file://../redis"
|
||||
version: 0.0.1
|
||||
type: application
|
||||
version: 2.1.0-rc.2
|
||||
version: 2.1.1
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
dependencies:
|
||||
- name: higress-core
|
||||
repository: file://../core
|
||||
version: 2.1.0-rc.2
|
||||
version: 2.1.1
|
||||
- name: higress-console
|
||||
repository: https://higress.io/helm-charts/
|
||||
version: 2.1.0
|
||||
digest: sha256:2ad724f1db0e86c9237a05822e29c7356b3995d4248783b3b0b2723ac628f647
|
||||
generated: "2025-04-01T23:34:42.769494+08:00"
|
||||
version: 2.1.1
|
||||
digest: sha256:a40f56252d0aa995fbf62952a6d23304dd84845caf5766bc64f7270cb00f01e9
|
||||
generated: "2025-04-18T16:45:23.961507+08:00"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
appVersion: 2.1.0-rc.2
|
||||
appVersion: 2.1.1
|
||||
description: Helm chart for deploying Higress gateways
|
||||
icon: https://higress.io/img/higress_logo_small.png
|
||||
home: http://higress.io/
|
||||
@@ -12,9 +12,9 @@ sources:
|
||||
dependencies:
|
||||
- name: higress-core
|
||||
repository: "file://../core"
|
||||
version: 2.1.0-rc.2
|
||||
version: 2.1.1
|
||||
- name: higress-console
|
||||
repository: "https://higress.io/helm-charts/"
|
||||
version: 2.1.0
|
||||
version: 2.1.1
|
||||
type: application
|
||||
version: 2.1.0-rc.2
|
||||
version: 2.1.1
|
||||
|
||||
Submodule istio/istio updated: a698755c49...e6578f7dd0
@@ -41,6 +41,16 @@ type RedisConfig struct {
|
||||
DB int `json:"db,omitempty"`
|
||||
}
|
||||
|
||||
// MCPRatelimitConfig defines the configuration for rate limit
|
||||
type MCPRatelimitConfig struct {
|
||||
// The limit of the rate limit
|
||||
Limit int64 `json:"limit,omitempty"`
|
||||
// The window of the rate limit
|
||||
Window int64 `json:"window,omitempty"`
|
||||
// The white list of the rate limit
|
||||
WhiteList []string `json:"white_list,omitempty"`
|
||||
}
|
||||
|
||||
// SSEServer defines the configuration for Server-Sent Events (SSE) server
|
||||
type SSEServer struct {
|
||||
// The name of the SSE server
|
||||
@@ -75,13 +85,18 @@ type McpServer struct {
|
||||
Servers []*SSEServer `json:"servers,omitempty"`
|
||||
// List of match rules for filtering requests
|
||||
MatchList []*MatchRule `json:"match_list,omitempty"`
|
||||
// Flag to control whether user level server is enabled
|
||||
EnableUserLevelServer bool `json:"enable_user_level_server,omitempty"`
|
||||
// Rate limit config for MCP server
|
||||
Ratelimit *MCPRatelimitConfig `json:"rate_limit,omitempty"`
|
||||
}
|
||||
|
||||
func NewDefaultMcpServer() *McpServer {
|
||||
return &McpServer{
|
||||
Enable: false,
|
||||
Servers: make([]*SSEServer, 0),
|
||||
MatchList: make([]*MatchRule, 0),
|
||||
Enable: false,
|
||||
Servers: make([]*SSEServer, 0),
|
||||
MatchList: make([]*MatchRule, 0),
|
||||
EnableUserLevelServer: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,8 +109,8 @@ func validMcpServer(m *McpServer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if m.Enable && m.Redis == nil {
|
||||
return errors.New("redis config cannot be empty when mcp server is enabled")
|
||||
if m.EnableUserLevelServer && m.Redis == nil {
|
||||
return errors.New("redis config cannot be empty when user level server is enabled")
|
||||
}
|
||||
|
||||
// Validate match rule types
|
||||
@@ -149,9 +164,17 @@ func deepCopyMcpServer(mcp *McpServer) (*McpServer, error) {
|
||||
DB: mcp.Redis.DB,
|
||||
}
|
||||
}
|
||||
|
||||
if mcp.Ratelimit != nil {
|
||||
newMcp.Ratelimit = &MCPRatelimitConfig{
|
||||
Limit: mcp.Ratelimit.Limit,
|
||||
Window: mcp.Ratelimit.Window,
|
||||
WhiteList: mcp.Ratelimit.WhiteList,
|
||||
}
|
||||
}
|
||||
newMcp.SsePathSuffix = mcp.SsePathSuffix
|
||||
|
||||
newMcp.EnableUserLevelServer = mcp.EnableUserLevelServer
|
||||
|
||||
if len(mcp.Servers) > 0 {
|
||||
newMcp.Servers = make([]*SSEServer, len(mcp.Servers))
|
||||
for i, server := range mcp.Servers {
|
||||
@@ -352,40 +375,59 @@ func (m *McpServerController) constructMcpServerStruct(mcp *McpServer) string {
|
||||
matchList = fmt.Sprintf("[%s]", strings.Join(matchConfigs, ","))
|
||||
}
|
||||
|
||||
// 构建 Redis 配置
|
||||
redisConfig := "null"
|
||||
if mcp.Redis != nil {
|
||||
redisConfig = fmt.Sprintf(`{
|
||||
"address": "%s",
|
||||
"username": "%s",
|
||||
"password": "%s",
|
||||
"db": %d
|
||||
}`, mcp.Redis.Address, mcp.Redis.Username, mcp.Redis.Password, mcp.Redis.DB)
|
||||
}
|
||||
|
||||
// 构建限流配置
|
||||
rateLimitConfig := "null"
|
||||
if mcp.Ratelimit != nil {
|
||||
whiteList := "[]"
|
||||
if len(mcp.Ratelimit.WhiteList) > 0 {
|
||||
whiteList = fmt.Sprintf(`["%s"]`, strings.Join(mcp.Ratelimit.WhiteList, `","`))
|
||||
}
|
||||
rateLimitConfig = fmt.Sprintf(`{
|
||||
"limit": %d,
|
||||
"window": %d,
|
||||
"white_list": %s
|
||||
}`, mcp.Ratelimit.Limit, mcp.Ratelimit.Window, whiteList)
|
||||
}
|
||||
|
||||
// Build complete configuration structure
|
||||
structFmt := `{
|
||||
return fmt.Sprintf(`{
|
||||
"name": "envoy.filters.http.golang",
|
||||
"typed_config": {
|
||||
"@type": "type.googleapis.com/udpa.type.v1.TypedStruct",
|
||||
"type_url": "type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config",
|
||||
"value": {
|
||||
"library_id": "mcp-server",
|
||||
"library_path": "/var/lib/istio/envoy/mcp-server.so",
|
||||
"plugin_name": "mcp-server",
|
||||
"library_id": "mcp-session",
|
||||
"library_path": "/var/lib/istio/envoy/golang-filter.so",
|
||||
"plugin_name": "mcp-session",
|
||||
"plugin_config": {
|
||||
"@type": "type.googleapis.com/xds.type.v3.TypedStruct",
|
||||
"value": {
|
||||
"redis": {
|
||||
"address": "%s",
|
||||
"username": "%s",
|
||||
"password": "%s",
|
||||
"db": %d
|
||||
},
|
||||
"redis": %s,
|
||||
"rate_limit": %s,
|
||||
"sse_path_suffix": "%s",
|
||||
"match_list": %s,
|
||||
"servers": %s
|
||||
"servers": %s,
|
||||
"enable_user_level_server": %t
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
return fmt.Sprintf(structFmt,
|
||||
mcp.Redis.Address,
|
||||
mcp.Redis.Username,
|
||||
mcp.Redis.Password,
|
||||
mcp.Redis.DB,
|
||||
}`,
|
||||
redisConfig,
|
||||
rateLimitConfig,
|
||||
mcp.SsePathSuffix,
|
||||
matchList,
|
||||
servers)
|
||||
servers,
|
||||
mcp.EnableUserLevelServer)
|
||||
}
|
||||
|
||||
@@ -45,17 +45,30 @@ func Test_validMcpServer(t *testing.T) {
|
||||
{
|
||||
name: "enabled but no redis config",
|
||||
mcp: &McpServer{
|
||||
Enable: true,
|
||||
Redis: nil,
|
||||
MatchList: []*MatchRule{},
|
||||
Servers: []*SSEServer{},
|
||||
Enable: true,
|
||||
EnableUserLevelServer: false,
|
||||
Redis: nil,
|
||||
MatchList: []*MatchRule{},
|
||||
Servers: []*SSEServer{},
|
||||
},
|
||||
wantErr: errors.New("redis config cannot be empty when mcp server is enabled"),
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "enabled with user level server but no redis config",
|
||||
mcp: &McpServer{
|
||||
Enable: true,
|
||||
EnableUserLevelServer: true,
|
||||
Redis: nil,
|
||||
MatchList: []*MatchRule{},
|
||||
Servers: []*SSEServer{},
|
||||
},
|
||||
wantErr: errors.New("redis config cannot be empty when user level server is enabled"),
|
||||
},
|
||||
{
|
||||
name: "valid config with redis",
|
||||
mcp: &McpServer{
|
||||
Enable: true,
|
||||
Enable: true,
|
||||
EnableUserLevelServer: true,
|
||||
Redis: &RedisConfig{
|
||||
Address: "localhost:6379",
|
||||
Username: "default",
|
||||
|
||||
@@ -36,4 +36,4 @@ RUN if [ "$GOARCH" = "arm64" ]; then \
|
||||
FROM scratch AS output
|
||||
ARG GO_FILTER_NAME
|
||||
ARG GOARCH
|
||||
COPY --from=golang-base /${GO_FILTER_NAME}.so ${GO_FILTER_NAME}_${GOARCH}.so
|
||||
COPY --from=golang-base /${GO_FILTER_NAME}.so golang-filter_${GOARCH}.so
|
||||
@@ -3,9 +3,13 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
|
||||
xds "github.com/cncf/xds/go/xds/type/v3"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/handler"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
|
||||
_ "github.com/alibaba/higress/plugins/golang-filter/mcp-server/registry/nacos"
|
||||
_ "github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/gorm"
|
||||
@@ -13,20 +17,31 @@ import (
|
||||
envoyHttp "github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/http"
|
||||
)
|
||||
|
||||
const Name = "mcp-server"
|
||||
const Name = "mcp-session"
|
||||
const Version = "1.0.0"
|
||||
const DefaultServerName = "defaultServer"
|
||||
const ConfigPathSuffix = "/config"
|
||||
|
||||
func init() {
|
||||
envoyHttp.RegisterHttpFilterFactoryAndConfigParser(Name, filterFactory, &parser{})
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
api.LogErrorf("PProf server recovered from panic: %v", r)
|
||||
}
|
||||
}()
|
||||
api.LogError(http.ListenAndServe("localhost:6060", nil).Error())
|
||||
}()
|
||||
}
|
||||
|
||||
type config struct {
|
||||
ssePathSuffix string
|
||||
redisClient *internal.RedisClient
|
||||
servers []*internal.SSEServer
|
||||
defaultServer *internal.SSEServer
|
||||
matchList []internal.MatchRule
|
||||
ssePathSuffix string
|
||||
redisClient *internal.RedisClient
|
||||
servers []*internal.SSEServer
|
||||
defaultServer *internal.SSEServer
|
||||
matchList []internal.MatchRule
|
||||
enableUserLevelServer bool
|
||||
rateLimitConfig *handler.MCPRatelimitConfig
|
||||
}
|
||||
|
||||
func (c *config) Destroy() {
|
||||
@@ -34,6 +49,9 @@ func (c *config) Destroy() {
|
||||
api.LogDebug("Closing Redis client")
|
||||
c.redisClient.Close()
|
||||
}
|
||||
for _, server := range c.servers {
|
||||
server.Close()
|
||||
}
|
||||
}
|
||||
|
||||
type parser struct {
|
||||
@@ -71,22 +89,53 @@ func (p *parser) Parse(any *anypb.Any, callbacks api.ConfigCallbackHandler) (int
|
||||
}
|
||||
}
|
||||
|
||||
redisConfigMap, ok := v.AsMap()["redis"].(map[string]interface{})
|
||||
// Redis configuration is optional
|
||||
if redisConfigMap, ok := v.AsMap()["redis"].(map[string]interface{}); ok {
|
||||
redisConfig, err := internal.ParseRedisConfig(redisConfigMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse redis config: %w", err)
|
||||
}
|
||||
|
||||
redisClient, err := internal.NewRedisClient(redisConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize RedisClient: %w", err)
|
||||
}
|
||||
conf.redisClient = redisClient
|
||||
api.LogDebug("Redis client initialized")
|
||||
} else {
|
||||
api.LogDebug("Redis configuration not provided, running without Redis")
|
||||
}
|
||||
|
||||
enableUserLevelServer, ok := v.AsMap()["enable_user_level_server"].(bool)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("redis config is not set")
|
||||
enableUserLevelServer = false
|
||||
if conf.redisClient == nil {
|
||||
return nil, fmt.Errorf("redis configuration is not provided, enable_user_level_server is true")
|
||||
}
|
||||
}
|
||||
conf.enableUserLevelServer = enableUserLevelServer
|
||||
|
||||
redisConfig, err := internal.ParseRedisConfig(redisConfigMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse redis config: %w", err)
|
||||
if rateLimit, ok := v.AsMap()["rate_limit"].(map[string]interface{}); ok {
|
||||
rateLimitConfig := &handler.MCPRatelimitConfig{}
|
||||
if limit, ok := rateLimit["limit"].(float64); ok {
|
||||
rateLimitConfig.Limit = int(limit)
|
||||
}
|
||||
if window, ok := rateLimit["window"].(float64); ok {
|
||||
rateLimitConfig.Window = int(window)
|
||||
}
|
||||
if whiteList, ok := rateLimit["white_list"].([]interface{}); ok {
|
||||
for _, item := range whiteList {
|
||||
if uid, ok := item.(string); ok {
|
||||
rateLimitConfig.Whitelist = append(rateLimitConfig.Whitelist, uid)
|
||||
}
|
||||
}
|
||||
}
|
||||
if errorText, ok := rateLimit["error_text"].(string); ok {
|
||||
rateLimitConfig.ErrorText = errorText
|
||||
}
|
||||
conf.rateLimitConfig = rateLimitConfig
|
||||
}
|
||||
|
||||
redisClient, err := internal.NewRedisClient(redisConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize RedisClient: %w", err)
|
||||
}
|
||||
conf.redisClient = redisClient
|
||||
|
||||
ssePathSuffix, ok := v.AsMap()["sse_path_suffix"].(string)
|
||||
if !ok || ssePathSuffix == "" {
|
||||
return nil, fmt.Errorf("sse path suffix is not set or empty")
|
||||
@@ -127,7 +176,7 @@ func (p *parser) Parse(any *anypb.Any, callbacks api.ConfigCallbackHandler) (int
|
||||
}
|
||||
api.LogDebug(fmt.Sprintf("Server config: %+v", serverConfig))
|
||||
|
||||
err = server.ParseConfig(serverConfig)
|
||||
err := server.ParseConfig(serverConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse server config: %w", err)
|
||||
}
|
||||
@@ -138,7 +187,7 @@ func (p *parser) Parse(any *anypb.Any, callbacks api.ConfigCallbackHandler) (int
|
||||
}
|
||||
|
||||
conf.servers = append(conf.servers, internal.NewSSEServer(serverInstance,
|
||||
internal.WithRedisClient(redisClient),
|
||||
internal.WithRedisClient(conf.redisClient),
|
||||
internal.WithSSEEndpoint(fmt.Sprintf("%s%s", serverPath, ssePathSuffix)),
|
||||
internal.WithMessageEndpoint(serverPath)))
|
||||
api.LogDebug(fmt.Sprintf("Registered MCP Server: %s", serverType))
|
||||
@@ -158,11 +207,14 @@ func (p *parser) Merge(parent interface{}, child interface{}) interface{} {
|
||||
newConfig.ssePathSuffix = childConfig.ssePathSuffix
|
||||
}
|
||||
if childConfig.servers != nil {
|
||||
newConfig.servers = append(newConfig.servers, childConfig.servers...)
|
||||
newConfig.servers = childConfig.servers
|
||||
}
|
||||
if childConfig.defaultServer != nil {
|
||||
newConfig.defaultServer = childConfig.defaultServer
|
||||
}
|
||||
if childConfig.matchList != nil {
|
||||
newConfig.matchList = childConfig.matchList
|
||||
}
|
||||
return &newConfig
|
||||
}
|
||||
|
||||
@@ -172,9 +224,11 @@ func filterFactory(c interface{}, callbacks api.FilterCallbackHandler) api.Strea
|
||||
panic("unexpected config type")
|
||||
}
|
||||
return &filter{
|
||||
callbacks: callbacks,
|
||||
config: conf,
|
||||
stopChan: make(chan struct{}),
|
||||
callbacks: callbacks,
|
||||
config: conf,
|
||||
stopChan: make(chan struct{}),
|
||||
mcpConfigHandler: handler.NewMCPConfigHandler(conf.redisClient, callbacks),
|
||||
mcpRatelimitHandler: handler.NewMCPRatelimitHandler(conf.redisClient, callbacks, conf.rateLimitConfig),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/handler"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
const (
|
||||
RedisNotEnabledResponseBody = "Redis is not enabled, SSE connection is not supported"
|
||||
)
|
||||
|
||||
// The callbacks in the filter, like `DecodeHeaders`, can be implemented on demand.
|
||||
@@ -26,15 +34,21 @@ type filter struct {
|
||||
message bool
|
||||
proxyURL *url.URL
|
||||
skip bool
|
||||
|
||||
userLevelConfig bool
|
||||
mcpConfigHandler *handler.MCPConfigHandler
|
||||
ratelimit bool
|
||||
mcpRatelimitHandler *handler.MCPRatelimitHandler
|
||||
}
|
||||
|
||||
type RequestURL struct {
|
||||
method string
|
||||
scheme string
|
||||
host string
|
||||
path string
|
||||
baseURL string
|
||||
parsedURL *url.URL
|
||||
method string
|
||||
scheme string
|
||||
host string
|
||||
path string
|
||||
baseURL string
|
||||
parsedURL *url.URL
|
||||
internalIP bool
|
||||
}
|
||||
|
||||
func NewRequestURL(header api.RequestHeaderMap) *RequestURL {
|
||||
@@ -42,10 +56,11 @@ func NewRequestURL(header api.RequestHeaderMap) *RequestURL {
|
||||
scheme, _ := header.Get(":scheme")
|
||||
host, _ := header.Get(":authority")
|
||||
path, _ := header.Get(":path")
|
||||
internalIP, _ := header.Get("x-envoy-internal")
|
||||
baseURL := fmt.Sprintf("%s://%s", scheme, host)
|
||||
parsedURL, _ := url.Parse(path)
|
||||
api.LogDebugf("RequestURL: method=%s, scheme=%s, host=%s, path=%s", method, scheme, host, path)
|
||||
return &RequestURL{method: method, scheme: scheme, host: host, path: path, baseURL: baseURL, parsedURL: parsedURL}
|
||||
return &RequestURL{method: method, scheme: scheme, host: host, path: path, baseURL: baseURL, parsedURL: parsedURL, internalIP: internalIP == "true"}
|
||||
}
|
||||
|
||||
// Callbacks which are called in request path
|
||||
@@ -71,11 +86,11 @@ func (f *filter) DecodeHeaders(header api.RequestHeaderMap, endStream bool) api.
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusOK, body, nil, 0, "")
|
||||
}
|
||||
api.LogDebugf("%s SSE connection started", server.GetServerName())
|
||||
server.SetBaseURL(url.baseURL)
|
||||
return api.LocalReply
|
||||
} else if f.path == server.GetMessageEndpoint() {
|
||||
if url.method != http.MethodPost {
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusMethodNotAllowed, "Method not allowed", nil, 0, "")
|
||||
return api.LocalReply
|
||||
}
|
||||
// Create a new http.Request object
|
||||
f.req = &http.Request{
|
||||
@@ -97,9 +112,52 @@ func (f *filter) DecodeHeaders(header api.RequestHeaderMap, endStream bool) api.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
f.req = &http.Request{
|
||||
Method: url.method,
|
||||
URL: url.parsedURL,
|
||||
}
|
||||
|
||||
if strings.HasSuffix(f.path, ConfigPathSuffix) && f.config.enableUserLevelServer {
|
||||
if !url.internalIP {
|
||||
api.LogWarnf("Access denied: non-internal IP address %s", url.parsedURL.String())
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusForbidden, "", nil, 0, "")
|
||||
return api.LocalReply
|
||||
}
|
||||
if strings.HasSuffix(f.path, ConfigPathSuffix) && url.method == http.MethodGet {
|
||||
api.LogDebugf("Handling config request: %s", f.path)
|
||||
f.mcpConfigHandler.HandleConfigRequest(f.req, []byte{})
|
||||
return api.LocalReply
|
||||
}
|
||||
f.userLevelConfig = true
|
||||
if endStream {
|
||||
return api.Continue
|
||||
} else {
|
||||
return api.StopAndBuffer
|
||||
}
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(url.parsedURL.Path, f.config.ssePathSuffix) {
|
||||
f.proxyURL = url.parsedURL
|
||||
return api.Continue
|
||||
if f.config.enableUserLevelServer {
|
||||
parts := strings.Split(url.parsedURL.Path, "/")
|
||||
if len(parts) >= 3 {
|
||||
serverName := parts[1]
|
||||
uid := parts[2]
|
||||
// Get encoded config
|
||||
encodedConfig, _ := f.mcpConfigHandler.GetEncodedConfig(serverName, uid)
|
||||
if encodedConfig != "" {
|
||||
header.Set("x-higress-mcpserver-config", encodedConfig)
|
||||
api.LogDebugf("Set x-higress-mcpserver-config Header for %s:%s", serverName, uid)
|
||||
}
|
||||
}
|
||||
f.ratelimit = true
|
||||
}
|
||||
if endStream {
|
||||
return api.Continue
|
||||
} else {
|
||||
return api.StopAndBuffer
|
||||
}
|
||||
}
|
||||
|
||||
if url.method != http.MethodGet {
|
||||
@@ -112,7 +170,6 @@ func (f *filter) DecodeHeaders(header api.RequestHeaderMap, endStream bool) api.
|
||||
f.serverName = f.config.defaultServer.GetServerName()
|
||||
body := "SSE connection create"
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusOK, body, nil, 0, "")
|
||||
f.config.defaultServer.SetBaseURL(url.baseURL)
|
||||
}
|
||||
return api.LocalReply
|
||||
}
|
||||
@@ -123,21 +180,50 @@ func (f *filter) DecodeData(buffer api.BufferInstance, endStream bool) api.Statu
|
||||
if f.skip {
|
||||
return api.Continue
|
||||
}
|
||||
if !endStream {
|
||||
return api.StopAndBuffer
|
||||
}
|
||||
if f.message {
|
||||
if endStream {
|
||||
for _, server := range f.config.servers {
|
||||
if f.path == server.GetMessageEndpoint() {
|
||||
// Create a response recorder to capture the response
|
||||
recorder := httptest.NewRecorder()
|
||||
// Call the handleMessage method of SSEServer with complete body
|
||||
server.HandleMessage(recorder, f.req, buffer.Bytes())
|
||||
f.message = false
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(recorder.Code, recorder.Body.String(), recorder.Header(), 0, "")
|
||||
return api.LocalReply
|
||||
}
|
||||
for _, server := range f.config.servers {
|
||||
if f.path == server.GetMessageEndpoint() {
|
||||
// Create a response recorder to capture the response
|
||||
recorder := httptest.NewRecorder()
|
||||
// Call the handleMessage method of SSEServer with complete body
|
||||
httpStatus := server.HandleMessage(recorder, f.req, buffer.Bytes())
|
||||
f.message = false
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(httpStatus, recorder.Body.String(), recorder.Header(), 0, "")
|
||||
return api.LocalReply
|
||||
}
|
||||
}
|
||||
} else if f.userLevelConfig {
|
||||
// Handle config POST request
|
||||
api.LogDebugf("Handling config request: %s", f.path)
|
||||
f.mcpConfigHandler.HandleConfigRequest(f.req, buffer.Bytes())
|
||||
return api.LocalReply
|
||||
} else if f.ratelimit {
|
||||
if checkJSONRPCMethod(buffer.Bytes(), "tools/list") {
|
||||
api.LogDebugf("Not a tools call request, skipping ratelimit")
|
||||
return api.Continue
|
||||
}
|
||||
parts := strings.Split(f.req.URL.Path, "/")
|
||||
if len(parts) < 3 {
|
||||
api.LogWarnf("Access denied: no valid uid found")
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusForbidden, "", nil, 0, "")
|
||||
return api.LocalReply
|
||||
}
|
||||
serverName := parts[1]
|
||||
uid := parts[2]
|
||||
encodedConfig, err := f.mcpConfigHandler.GetEncodedConfig(serverName, uid)
|
||||
if err != nil {
|
||||
api.LogWarnf("Access denied: no valid config found for uid %s", uid)
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusForbidden, "", nil, 0, "")
|
||||
return api.LocalReply
|
||||
} else if encodedConfig == "" && checkJSONRPCMethod(buffer.Bytes(), "tools/call") {
|
||||
api.LogDebugf("Empty config found for %s:%s", serverName, uid)
|
||||
if !f.mcpRatelimitHandler.HandleRatelimit(f.req, buffer.Bytes()) {
|
||||
return api.LocalReply
|
||||
}
|
||||
}
|
||||
return api.StopAndBuffer
|
||||
}
|
||||
return api.Continue
|
||||
}
|
||||
@@ -149,11 +235,15 @@ func (f *filter) EncodeHeaders(header api.ResponseHeaderMap, endStream bool) api
|
||||
return api.Continue
|
||||
}
|
||||
if f.serverName != "" {
|
||||
header.Set("Content-Type", "text/event-stream")
|
||||
header.Set("Cache-Control", "no-cache")
|
||||
header.Set("Connection", "keep-alive")
|
||||
header.Set("Access-Control-Allow-Origin", "*")
|
||||
header.Del("Content-Length")
|
||||
if f.config.redisClient != nil {
|
||||
header.Set("Content-Type", "text/event-stream")
|
||||
header.Set("Cache-Control", "no-cache")
|
||||
header.Set("Connection", "keep-alive")
|
||||
header.Set("Access-Control-Allow-Origin", "*")
|
||||
header.Del("Content-Length")
|
||||
} else {
|
||||
header.Set("Content-Length", strconv.Itoa(len(RedisNotEnabledResponseBody)))
|
||||
}
|
||||
return api.Continue
|
||||
}
|
||||
return api.Continue
|
||||
@@ -168,7 +258,7 @@ func (f *filter) EncodeData(buffer api.BufferInstance, endStream bool) api.Statu
|
||||
if !endStream {
|
||||
return api.StopAndBuffer
|
||||
}
|
||||
if f.proxyURL != nil {
|
||||
if f.proxyURL != nil && f.config.redisClient != nil {
|
||||
sessionID := f.proxyURL.Query().Get("sessionId")
|
||||
if sessionID != "" {
|
||||
channel := internal.GetSSEChannelName(sessionID)
|
||||
@@ -181,21 +271,26 @@ func (f *filter) EncodeData(buffer api.BufferInstance, endStream bool) api.Statu
|
||||
}
|
||||
|
||||
if f.serverName != "" {
|
||||
// handle specific server
|
||||
for _, server := range f.config.servers {
|
||||
if f.serverName == server.GetServerName() {
|
||||
if f.config.redisClient != nil {
|
||||
// handle specific server
|
||||
for _, server := range f.config.servers {
|
||||
if f.serverName == server.GetServerName() {
|
||||
buffer.Reset()
|
||||
server.HandleSSE(f.callbacks, f.stopChan)
|
||||
return api.Running
|
||||
}
|
||||
}
|
||||
// handle default server
|
||||
if f.serverName == f.config.defaultServer.GetServerName() {
|
||||
buffer.Reset()
|
||||
server.HandleSSE(f.callbacks, f.stopChan)
|
||||
f.config.defaultServer.HandleSSE(f.callbacks, f.stopChan)
|
||||
return api.Running
|
||||
}
|
||||
return api.Continue
|
||||
} else {
|
||||
buffer.SetString(RedisNotEnabledResponseBody)
|
||||
return api.Continue
|
||||
}
|
||||
// handle default server
|
||||
if f.serverName == f.config.defaultServer.GetServerName() {
|
||||
buffer.Reset()
|
||||
f.config.defaultServer.HandleSSE(f.callbacks, f.stopChan)
|
||||
return api.Running
|
||||
}
|
||||
return api.Continue
|
||||
}
|
||||
return api.Continue
|
||||
}
|
||||
@@ -213,3 +308,14 @@ func (f *filter) OnDestroy(reason api.DestroyReason) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check if the request is a tools/call request
|
||||
func checkJSONRPCMethod(body []byte, method string) bool {
|
||||
var request mcp.CallToolRequest
|
||||
if err := json.Unmarshal(body, &request); err != nil {
|
||||
api.LogWarnf("Failed to unmarshal request body: %v, not a JSON RPC request", err)
|
||||
return true
|
||||
}
|
||||
|
||||
return request.Method == method
|
||||
}
|
||||
|
||||
@@ -101,4 +101,4 @@ require (
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
replace github.com/nacos-group/nacos-sdk-go/v2 v2.2.9 => github.com/luoxiner/nacos-sdk-go/v2 v2.2.9-30
|
||||
replace github.com/nacos-group/nacos-sdk-go/v2 v2.2.9 => github.com/luoxiner/nacos-sdk-go/v2 v2.2.9-40
|
||||
|
||||
@@ -136,12 +136,8 @@ github.com/deckarep/golang-set v1.7.1 h1:SCQV0S6gTtp6itiFrTqI+pfmJ4LN85S1YzhDf9r
|
||||
github.com/deckarep/golang-set v1.7.1/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/envoyproxy/envoy v1.32.3 h1:eftH199KwYfyBTtm4reeEzsWTqraACEaTQ6efl31v0I=
|
||||
github.com/envoyproxy/envoy v1.32.3/go.mod h1:KGS+IUehDX1mSIdqodPTWskKOo7bZMLLy3GHxvOKcJk=
|
||||
github.com/envoyproxy/envoy v1.33.1-0.20250325161043-11ab50a29d99 h1:jih/Ieb7BFgVCStgvY5fXQ3mI9ByOt4wfwUF0d7qmqI=
|
||||
github.com/envoyproxy/envoy v1.33.1-0.20250325161043-11ab50a29d99/go.mod h1:x7d0dNbE0xGuDBUkBg19VGCgnPQ+lJ2k8lDzDzKExow=
|
||||
github.com/envoyproxy/envoy v1.33.2 h1:k3ChySbVo4HejvbDRxkgRroUnj6TZZpXPJJ0UGaZkXs=
|
||||
github.com/envoyproxy/envoy v1.33.2/go.mod h1:faFqv1XeNGX/ph6Zto5Culdcpk4Klxp730Q6XhWarV4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
@@ -285,6 +281,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/luoxiner/nacos-sdk-go/v2 v2.2.9-40 h1:nzRTBplC0riQqQwEHZThw5H4/TH5LgWTQTm6A7t1lpY=
|
||||
github.com/luoxiner/nacos-sdk-go/v2 v2.2.9-40/go.mod h1:9FKXl6FqOiVmm72i8kADtbeK71egyG9y3uRDBg41tpQ=
|
||||
github.com/mark3labs/mcp-go v0.12.0 h1:Pue1Tdwqcz77GHq18uzgmLT3wmeDUxXUSAqSwhGLhVo=
|
||||
github.com/mark3labs/mcp-go v0.12.0/go.mod h1:cjMlBU0cv/cj9kjlgmRhoJ5JREdS7YX83xeIG9Ko/jE=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
@@ -302,8 +300,6 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY
|
||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nacos-group/nacos-sdk-go/v2 v2.2.9 h1:etzCMnB9EBeSKfaDIOe8zH4HO/8fycpc6s0AmXCrmAw=
|
||||
github.com/nacos-group/nacos-sdk-go/v2 v2.2.9/go.mod h1:9FKXl6FqOiVmm72i8kADtbeK71egyG9y3uRDBg41tpQ=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
|
||||
162
plugins/golang-filter/mcp-server/handler/config_handler.go
Normal file
162
plugins/golang-filter/mcp-server/handler/config_handler.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
)
|
||||
|
||||
// MCPConfigHandler handles configuration requests for MCP server
|
||||
type MCPConfigHandler struct {
|
||||
configStore ConfigStore
|
||||
callbacks api.FilterCallbackHandler
|
||||
}
|
||||
|
||||
// NewMCPConfigHandler creates a new instance of MCP configuration handler
|
||||
func NewMCPConfigHandler(redisClient *internal.RedisClient, callbacks api.FilterCallbackHandler) *MCPConfigHandler {
|
||||
return &MCPConfigHandler{
|
||||
configStore: NewRedisConfigStore(redisClient),
|
||||
callbacks: callbacks,
|
||||
}
|
||||
}
|
||||
|
||||
// HandleConfigRequest processes configuration requests
|
||||
func (h *MCPConfigHandler) HandleConfigRequest(req *http.Request, body []byte) bool {
|
||||
// Check if it's a configuration request
|
||||
if !strings.HasSuffix(req.URL.Path, "/config") {
|
||||
return false
|
||||
}
|
||||
|
||||
// Extract serverName and uid from path
|
||||
pathParts := strings.Split(strings.TrimSuffix(req.URL.Path, "/config"), "/")
|
||||
if len(pathParts) < 2 {
|
||||
h.sendErrorResponse(http.StatusBadRequest, "INVALID_PATH", "Invalid path format")
|
||||
return true
|
||||
}
|
||||
uid := pathParts[len(pathParts)-1]
|
||||
serverName := pathParts[len(pathParts)-2]
|
||||
|
||||
switch req.Method {
|
||||
case http.MethodGet:
|
||||
return h.handleGetConfig(serverName, uid)
|
||||
case http.MethodPost:
|
||||
return h.handleStoreConfig(serverName, uid, body)
|
||||
default:
|
||||
h.sendErrorResponse(http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "Method not allowed")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// handleGetConfig handles configuration retrieval requests
|
||||
func (h *MCPConfigHandler) handleGetConfig(serverName string, uid string) bool {
|
||||
config, err := h.configStore.GetConfig(serverName, uid)
|
||||
if err != nil {
|
||||
api.LogErrorf("Failed to get config for server %s, uid %s: %v", serverName, uid, err)
|
||||
h.sendErrorResponse(http.StatusInternalServerError, "CONFIG_ERROR", fmt.Sprintf("Failed to get configuration: %s", err.Error()))
|
||||
return true
|
||||
}
|
||||
|
||||
response := struct {
|
||||
Success bool `json:"success"`
|
||||
Config map[string]string `json:"config"`
|
||||
}{
|
||||
Success: true,
|
||||
Config: config,
|
||||
}
|
||||
|
||||
responseBytes, _ := json.Marshal(response)
|
||||
headers := map[string][]string{
|
||||
"Content-Type": {"application/json"},
|
||||
}
|
||||
h.callbacks.DecoderFilterCallbacks().SendLocalReply(
|
||||
http.StatusOK,
|
||||
string(responseBytes),
|
||||
headers, 0, "",
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
// handleStoreConfig handles configuration storage requests
|
||||
func (h *MCPConfigHandler) handleStoreConfig(serverName string, uid string, body []byte) bool {
|
||||
// Parse request body
|
||||
var requestBody struct {
|
||||
Config map[string]string `json:"config"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &requestBody); err != nil {
|
||||
api.LogErrorf("Invalid request format for server %s, uid %s: %v", serverName, uid, err)
|
||||
h.sendErrorResponse(http.StatusBadRequest, "INVALID_REQUEST", fmt.Sprintf("Invalid request format: %s", err.Error()))
|
||||
return true
|
||||
}
|
||||
|
||||
if requestBody.Config == nil {
|
||||
h.sendErrorResponse(http.StatusBadRequest, "INVALID_REQUEST", "Config cannot be null")
|
||||
return true
|
||||
}
|
||||
|
||||
response, err := h.configStore.StoreConfig(serverName, uid, requestBody.Config)
|
||||
if err != nil {
|
||||
api.LogErrorf("Failed to store config for server %s, uid %s: %v", serverName, uid, err)
|
||||
h.sendErrorResponse(http.StatusInternalServerError, "CONFIG_ERROR", fmt.Sprintf("Failed to store configuration: %s", err.Error()))
|
||||
return true
|
||||
}
|
||||
|
||||
responseBytes, _ := json.Marshal(response)
|
||||
headers := map[string][]string{
|
||||
"Content-Type": {"application/json"},
|
||||
}
|
||||
h.callbacks.DecoderFilterCallbacks().SendLocalReply(
|
||||
http.StatusOK,
|
||||
string(responseBytes),
|
||||
headers, 0, "",
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
// sendErrorResponse sends an error response with the specified status, code and message
|
||||
func (h *MCPConfigHandler) sendErrorResponse(status int, code string, message string) {
|
||||
response := &ConfigResponse{
|
||||
Success: false,
|
||||
Error: &struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}{
|
||||
Code: code,
|
||||
Message: message,
|
||||
},
|
||||
}
|
||||
responseBytes, _ := json.Marshal(response)
|
||||
headers := map[string][]string{
|
||||
"Content-Type": {"application/json"},
|
||||
}
|
||||
h.callbacks.DecoderFilterCallbacks().SendLocalReply(
|
||||
status,
|
||||
string(responseBytes),
|
||||
headers, 0, "",
|
||||
)
|
||||
}
|
||||
|
||||
// GetEncodedConfig retrieves and encodes the configuration for a given server and uid
|
||||
func (h *MCPConfigHandler) GetEncodedConfig(serverName string, uid string) (string, error) {
|
||||
conf, err := h.configStore.GetConfig(serverName, uid)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get config: %w", err)
|
||||
}
|
||||
|
||||
// Check if config exists and is not empty
|
||||
if len(conf) > 0 {
|
||||
// Convert config map to JSON string
|
||||
configBytes, err := json.Marshal(conf)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal config: %w", err)
|
||||
}
|
||||
// Encode JSON string to base64
|
||||
return base64.StdEncoding.EncodeToString(configBytes), nil
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
105
plugins/golang-filter/mcp-server/handler/config_store.go
Normal file
105
plugins/golang-filter/mcp-server/handler/config_store.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
|
||||
)
|
||||
|
||||
const (
|
||||
configExpiry = 7 * 24 * time.Hour
|
||||
)
|
||||
|
||||
// GetConfigStoreKey returns the Redis channel name for the given session ID
|
||||
func GetConfigStoreKey(serverName string, uid string) string {
|
||||
return fmt.Sprintf("mcp-server-config:%s:%s", serverName, uid)
|
||||
}
|
||||
|
||||
// ConfigResponse represents the response structure for configuration operations
|
||||
type ConfigResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Error *struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// ConfigStore defines the interface for configuration storage operations
|
||||
type ConfigStore interface {
|
||||
// StoreConfig stores user configuration
|
||||
StoreConfig(serverName string, uid string, config map[string]string) (*ConfigResponse, error)
|
||||
// GetConfig retrieves user configuration
|
||||
GetConfig(serverName string, uid string) (map[string]string, error)
|
||||
}
|
||||
|
||||
// RedisConfigStore implements configuration storage using Redis
|
||||
type RedisConfigStore struct {
|
||||
redisClient *internal.RedisClient
|
||||
}
|
||||
|
||||
// NewRedisConfigStore creates a new instance of Redis configuration storage
|
||||
func NewRedisConfigStore(redisClient *internal.RedisClient) ConfigStore {
|
||||
return &RedisConfigStore{
|
||||
redisClient: redisClient,
|
||||
}
|
||||
}
|
||||
|
||||
// StoreConfig stores configuration in Redis
|
||||
func (s *RedisConfigStore) StoreConfig(serverName string, uid string, config map[string]string) (*ConfigResponse, error) {
|
||||
key := GetConfigStoreKey(serverName, uid)
|
||||
|
||||
// Convert config to JSON
|
||||
configBytes, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return &ConfigResponse{
|
||||
Success: false,
|
||||
Error: &struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}{
|
||||
Code: "MARSHAL_ERROR",
|
||||
Message: "Failed to marshal configuration",
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
// Store in Redis with expiry
|
||||
err = s.redisClient.Set(key, string(configBytes), configExpiry)
|
||||
if err != nil {
|
||||
return &ConfigResponse{
|
||||
Success: false,
|
||||
Error: &struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}{
|
||||
Code: "REDIS_ERROR",
|
||||
Message: "Failed to store configuration in Redis",
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
return &ConfigResponse{
|
||||
Success: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetConfig retrieves configuration from Redis
|
||||
func (s *RedisConfigStore) GetConfig(serverName string, uid string) (map[string]string, error) {
|
||||
key := GetConfigStoreKey(serverName, uid)
|
||||
|
||||
// Get from Redis
|
||||
value, err := s.redisClient.Get(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Parse JSON
|
||||
var config map[string]string
|
||||
if err := json.Unmarshal([]byte(value), &config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
181
plugins/golang-filter/mcp-server/handler/rate_limit_handler.go
Normal file
181
plugins/golang-filter/mcp-server/handler/rate_limit_handler.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
type MCPRatelimitHandler struct {
|
||||
redisClient *internal.RedisClient
|
||||
callbacks api.FilterCallbackHandler
|
||||
limit int // Maximum requests allowed per window
|
||||
window int // Time window in seconds
|
||||
whitelist []string // Whitelist of UIDs that bypass rate limiting
|
||||
errorText string // Error text to be displayed
|
||||
}
|
||||
|
||||
// MCPRatelimitConfig is the configuration for the rate limit handler
|
||||
type MCPRatelimitConfig struct {
|
||||
Limit int `json:"limit"`
|
||||
Window int `json:"window"`
|
||||
Whitelist []string `json:"white_list"` // List of UIDs that bypass rate limiting
|
||||
ErrorText string `json:"error_text"` // Error text to be displayed
|
||||
}
|
||||
|
||||
// NewMCPRatelimitHandler creates a new rate limit handler
|
||||
func NewMCPRatelimitHandler(redisClient *internal.RedisClient, callbacks api.FilterCallbackHandler, conf *MCPRatelimitConfig) *MCPRatelimitHandler {
|
||||
if conf == nil {
|
||||
conf = &MCPRatelimitConfig{
|
||||
Limit: 100,
|
||||
Window: int(24 * time.Hour / time.Second), // 24 hours in seconds
|
||||
Whitelist: []string{},
|
||||
ErrorText: "API rate limit exceeded",
|
||||
}
|
||||
}
|
||||
return &MCPRatelimitHandler{
|
||||
redisClient: redisClient,
|
||||
callbacks: callbacks,
|
||||
limit: conf.Limit,
|
||||
window: conf.Window,
|
||||
whitelist: conf.Whitelist,
|
||||
errorText: conf.ErrorText,
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
// Lua script for rate limiting
|
||||
LimitScript = `
|
||||
local ttl = redis.call('ttl', KEYS[1])
|
||||
if ttl < 0 then
|
||||
redis.call('set', KEYS[1], ARGV[1] - 1, 'EX', ARGV[2])
|
||||
return {ARGV[1], ARGV[1] - 1, ARGV[2]}
|
||||
end
|
||||
return {ARGV[1], redis.call('incrby', KEYS[1], -1), ttl}
|
||||
`
|
||||
)
|
||||
|
||||
type LimitContext struct {
|
||||
Count int // Current request count
|
||||
Remaining int // Remaining requests allowed
|
||||
Reset int // Time until reset in seconds
|
||||
}
|
||||
|
||||
// TODO: needs to be refactored, rate limit should be registered as a request hook in MCP server
|
||||
func (h *MCPRatelimitHandler) HandleRatelimit(req *http.Request, body []byte) bool {
|
||||
parts := strings.Split(req.URL.Path, "/")
|
||||
if len(parts) < 3 {
|
||||
h.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusForbidden, "", nil, 0, "")
|
||||
return false
|
||||
}
|
||||
serverName := parts[1]
|
||||
uid := parts[2]
|
||||
|
||||
// Check if the UID is in whitelist
|
||||
for _, whitelistedUID := range h.whitelist {
|
||||
if whitelistedUID == uid {
|
||||
return true // Bypass rate limiting for whitelisted UIDs
|
||||
}
|
||||
}
|
||||
|
||||
// Build rate limit key using serverName, uid, window and limit
|
||||
limitKey := fmt.Sprintf("mcp-server-limit:%s:%s:%d:%d", serverName, uid, h.window, h.limit)
|
||||
keys := []string{limitKey}
|
||||
|
||||
args := []interface{}{h.limit, h.window}
|
||||
|
||||
result, err := h.redisClient.Eval(LimitScript, 1, keys, args)
|
||||
if err != nil {
|
||||
api.LogErrorf("Failed to check rate limit: %v", err)
|
||||
h.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusInternalServerError, "", nil, 0, "")
|
||||
return false
|
||||
}
|
||||
|
||||
// Process response
|
||||
resultArray, ok := result.([]interface{})
|
||||
if !ok || len(resultArray) != 3 {
|
||||
api.LogErrorf("Invalid response format: %v", result)
|
||||
h.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusInternalServerError, "", nil, 0, "")
|
||||
return false
|
||||
}
|
||||
|
||||
context := LimitContext{
|
||||
Count: parseRedisValue(resultArray[0]),
|
||||
Remaining: parseRedisValue(resultArray[1]),
|
||||
Reset: parseRedisValue(resultArray[2]),
|
||||
}
|
||||
|
||||
if context.Remaining < 0 {
|
||||
// Create error response content
|
||||
errorContent := []mcp.TextContent{
|
||||
{
|
||||
Type: "text",
|
||||
Text: h.errorText,
|
||||
},
|
||||
}
|
||||
// Create response result
|
||||
result := map[string]interface{}{
|
||||
"content": errorContent,
|
||||
"isError": true,
|
||||
}
|
||||
// Create JSON-RPC response
|
||||
id := getJSONPRCID(body)
|
||||
response := mcp.JSONRPCResponse{
|
||||
JSONRPC: mcp.JSONRPC_VERSION,
|
||||
ID: id,
|
||||
Result: result,
|
||||
}
|
||||
// Convert response to JSON
|
||||
jsonResponse, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
api.LogErrorf("Failed to marshal JSON response: %v", err)
|
||||
h.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusInternalServerError, "", nil, 0, "")
|
||||
return false
|
||||
}
|
||||
// Send JSON-RPC response
|
||||
sessionID := req.URL.Query().Get("sessionId")
|
||||
if sessionID != "" {
|
||||
h.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusAccepted, string(jsonResponse), nil, 0, "")
|
||||
} else {
|
||||
h.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusOK, string(jsonResponse), nil, 0, "")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func getJSONPRCID(body []byte) mcp.RequestId {
|
||||
baseMessage := struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
Method string `json:"method"`
|
||||
ID interface{} `json:"id,omitempty"`
|
||||
}{}
|
||||
if err := json.Unmarshal(body, &baseMessage); err != nil {
|
||||
api.LogWarnf("Failed to unmarshal request body: %v, not a JSON RPC request", err)
|
||||
return ""
|
||||
}
|
||||
return baseMessage.ID
|
||||
}
|
||||
|
||||
// parseRedisValue converts the value from Redis to an int
|
||||
func parseRedisValue(value interface{}) int {
|
||||
switch v := value.(type) {
|
||||
case int:
|
||||
return v
|
||||
case int64:
|
||||
return int(v)
|
||||
case string:
|
||||
if i, err := strconv.Atoi(v); err == nil {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
76
plugins/golang-filter/mcp-server/internal/crypto.go
Normal file
76
plugins/golang-filter/mcp-server/internal/crypto.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Crypto handles encryption and decryption operations using AES-GCM
|
||||
type Crypto struct {
|
||||
gcm cipher.AEAD
|
||||
}
|
||||
|
||||
func NewCrypto(secret string) (*Crypto, error) {
|
||||
if secret == "" {
|
||||
return nil, fmt.Errorf("secret cannot be empty")
|
||||
}
|
||||
|
||||
// Generate a 32-byte key using SHA-256
|
||||
hash := sha256.Sum256([]byte(secret))
|
||||
block, err := aes.NewCipher(hash[:])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create cipher: %v", err)
|
||||
}
|
||||
|
||||
// Create GCM mode
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create GCM: %v", err)
|
||||
}
|
||||
|
||||
return &Crypto{gcm: gcm}, nil
|
||||
}
|
||||
|
||||
// Encrypt encrypts the plaintext data using AES-GCM
|
||||
func (c *Crypto) Encrypt(plaintext []byte) (string, error) {
|
||||
// Generate random nonce
|
||||
nonce := make([]byte, c.gcm.NonceSize())
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return "", fmt.Errorf("failed to generate nonce: %v", err)
|
||||
}
|
||||
|
||||
// Encrypt and authenticate data
|
||||
ciphertext := c.gcm.Seal(nonce, nonce, plaintext, nil)
|
||||
return base64.StdEncoding.EncodeToString(ciphertext), nil
|
||||
}
|
||||
|
||||
// Decrypt decrypts the encrypted string using AES-GCM
|
||||
func (c *Crypto) Decrypt(encryptedStr string) ([]byte, error) {
|
||||
// Decode base64
|
||||
ciphertext, err := base64.StdEncoding.DecodeString(encryptedStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid encrypted data format")
|
||||
}
|
||||
|
||||
// Check if the ciphertext is too short
|
||||
if len(ciphertext) < c.gcm.NonceSize() {
|
||||
return nil, fmt.Errorf("invalid encrypted data length")
|
||||
}
|
||||
|
||||
// Extract nonce and ciphertext
|
||||
nonce := ciphertext[:c.gcm.NonceSize()]
|
||||
ciphertext = ciphertext[c.gcm.NonceSize():]
|
||||
|
||||
// Decrypt and verify data
|
||||
plaintext, err := c.gcm.Open(nil, nonce, ciphertext, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decryption failed")
|
||||
}
|
||||
|
||||
return plaintext, nil
|
||||
}
|
||||
@@ -10,35 +10,42 @@ import (
|
||||
)
|
||||
|
||||
type RedisConfig struct {
|
||||
Address string
|
||||
Username string
|
||||
Password string
|
||||
DB int
|
||||
address string
|
||||
username string
|
||||
password string
|
||||
db int
|
||||
secret string // Encryption key
|
||||
}
|
||||
|
||||
func ParseRedisConfig(config map[string]any) (*RedisConfig, error) {
|
||||
// ParseRedisConfig parses Redis configuration from a map
|
||||
func ParseRedisConfig(config map[string]interface{}) (*RedisConfig, error) {
|
||||
c := &RedisConfig{}
|
||||
|
||||
// address is required
|
||||
addr, ok := config["address"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("address is required and must be a string")
|
||||
if addr, ok := config["address"].(string); ok && addr != "" {
|
||||
c.address = addr
|
||||
} else {
|
||||
return nil, fmt.Errorf("address is required and must be a non-empty string")
|
||||
}
|
||||
c.Address = addr
|
||||
|
||||
// username is optional
|
||||
if username, ok := config["username"].(string); ok {
|
||||
c.Username = username
|
||||
c.username = username
|
||||
}
|
||||
|
||||
// password is optional
|
||||
if password, ok := config["password"].(string); ok {
|
||||
c.Password = password
|
||||
c.password = password
|
||||
}
|
||||
|
||||
// db is optional, default to 0
|
||||
if db, ok := config["db"].(int); ok {
|
||||
c.DB = db
|
||||
c.db = db
|
||||
}
|
||||
|
||||
// secret is optional
|
||||
if secret, ok := config["secret"].(string); ok {
|
||||
c.secret = secret
|
||||
}
|
||||
|
||||
return c, nil
|
||||
@@ -50,15 +57,16 @@ type RedisClient struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
config *RedisConfig
|
||||
crypto *Crypto
|
||||
}
|
||||
|
||||
// NewRedisClient creates a new RedisClient instance and establishes a connection to the Redis server
|
||||
func NewRedisClient(config *RedisConfig) (*RedisClient, error) {
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: config.Address,
|
||||
Username: config.Username,
|
||||
Password: config.Password,
|
||||
DB: config.DB,
|
||||
Addr: config.address,
|
||||
Username: config.username,
|
||||
Password: config.password,
|
||||
DB: config.db,
|
||||
})
|
||||
|
||||
// Ping the Redis server to check the connection
|
||||
@@ -69,11 +77,22 @@ func NewRedisClient(config *RedisConfig) (*RedisClient, error) {
|
||||
api.LogDebugf("Connected to Redis: %s", pong)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
var crypto *Crypto
|
||||
if config.secret != "" {
|
||||
crypto, err = NewCrypto(config.secret)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
redisClient := &RedisClient{
|
||||
client: client,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
config: config,
|
||||
crypto: crypto,
|
||||
}
|
||||
|
||||
// Start keep-alive check
|
||||
@@ -117,10 +136,10 @@ func (r *RedisClient) reconnect() error {
|
||||
|
||||
// Create new client
|
||||
r.client = redis.NewClient(&redis.Options{
|
||||
Addr: r.config.Address,
|
||||
Username: r.config.Username,
|
||||
Password: r.config.Password,
|
||||
DB: r.config.DB,
|
||||
Addr: r.config.address,
|
||||
Username: r.config.username,
|
||||
Password: r.config.password,
|
||||
DB: r.config.db,
|
||||
})
|
||||
|
||||
// Test the new connection
|
||||
@@ -150,6 +169,12 @@ func (r *RedisClient) Subscribe(channel string, stopChan chan struct{}, callback
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
api.LogErrorf("Redis Subscribe recovered from panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
defer func() {
|
||||
pubsub.Close()
|
||||
api.LogDebugf("Closed subscription to channel %s", channel)
|
||||
@@ -184,7 +209,19 @@ func (r *RedisClient) Subscribe(channel string, stopChan chan struct{}, callback
|
||||
|
||||
// Set sets the value of a key in Redis
|
||||
func (r *RedisClient) Set(key string, value string, expiration time.Duration) error {
|
||||
err := r.client.Set(r.ctx, key, value, expiration).Err()
|
||||
var finalValue string
|
||||
if r.crypto != nil {
|
||||
// Encrypt the data
|
||||
encryptedValue, err := r.crypto.Encrypt([]byte(value))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encrypt value: %w", err)
|
||||
}
|
||||
finalValue = encryptedValue
|
||||
} else {
|
||||
finalValue = value
|
||||
}
|
||||
|
||||
err := r.client.Set(r.ctx, key, finalValue, expiration).Err()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set key: %w", err)
|
||||
}
|
||||
@@ -193,13 +230,23 @@ func (r *RedisClient) Set(key string, value string, expiration time.Duration) er
|
||||
|
||||
// Get retrieves the value of a key from Redis
|
||||
func (r *RedisClient) Get(key string) (string, error) {
|
||||
val, err := r.client.Get(r.ctx, key).Result()
|
||||
value, err := r.client.Get(r.ctx, key).Result()
|
||||
if err == redis.Nil {
|
||||
return "", fmt.Errorf("key does not exist")
|
||||
} else if err != nil {
|
||||
return "", fmt.Errorf("failed to get key: %w", err)
|
||||
}
|
||||
return val, nil
|
||||
|
||||
if r.crypto != nil {
|
||||
// Decrypt the data
|
||||
decryptedValue, err := r.crypto.Decrypt(value)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to decrypt value: %w", err)
|
||||
}
|
||||
return string(decryptedValue), nil
|
||||
}
|
||||
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// Close closes the Redis client and stops the keepalive goroutine
|
||||
@@ -207,3 +254,13 @@ func (r *RedisClient) Close() error {
|
||||
r.cancel()
|
||||
return r.client.Close()
|
||||
}
|
||||
|
||||
// Eval executes a Lua script
|
||||
func (r *RedisClient) Eval(script string, numKeys int, keys []string, args []interface{}) (interface{}, error) {
|
||||
result, err := r.client.Eval(r.ctx, script, keys, args...).Result()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute Lua script: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@ type MCPServer struct {
|
||||
clientMu sync.Mutex // Separate mutex for client context
|
||||
currentClient NotificationContext
|
||||
initialized atomic.Bool // Use atomic for the initialized flag
|
||||
destory chan struct{}
|
||||
}
|
||||
|
||||
// serverKey is the context key for storing the server instance
|
||||
@@ -226,6 +227,7 @@ func NewMCPServer(
|
||||
prompts: nil,
|
||||
logging: false,
|
||||
},
|
||||
destory: make(chan struct{}),
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
@@ -419,6 +421,16 @@ func (s *MCPServer) HandleMessage(
|
||||
)
|
||||
}
|
||||
return s.handleToolCall(ctx, baseMessage.ID, request)
|
||||
case "":
|
||||
var response mcp.JSONRPCResponse
|
||||
if err := json.Unmarshal(message, &response); err != nil {
|
||||
return createErrorResponse(
|
||||
baseMessage.ID,
|
||||
mcp.INVALID_REQUEST,
|
||||
"Invalid message format",
|
||||
)
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return createErrorResponse(
|
||||
baseMessage.ID,
|
||||
@@ -816,6 +828,14 @@ func (s *MCPServer) handleNotification(
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MCPServer) Close() {
|
||||
close(s.destory)
|
||||
}
|
||||
|
||||
func (s *MCPServer) GetDestoryChannel() chan struct{} {
|
||||
return s.destory
|
||||
}
|
||||
|
||||
func createResponse(id interface{}, result interface{}) mcp.JSONRPCMessage {
|
||||
return mcp.JSONRPCResponse{
|
||||
JSONRPC: mcp.JSONRPC_VERSION,
|
||||
|
||||
@@ -28,10 +28,6 @@ type SSEServer struct {
|
||||
redisClient *RedisClient // Redis client for pub/sub
|
||||
}
|
||||
|
||||
func (s *SSEServer) SetBaseURL(baseURL string) {
|
||||
s.baseURL = baseURL
|
||||
}
|
||||
|
||||
func (s *SSEServer) GetMessageEndpoint() string {
|
||||
return s.messageEndpoint
|
||||
}
|
||||
@@ -148,6 +144,12 @@ func (s *SSEServer) HandleSSE(cb api.FilterCallbackHandler, stopChan chan struct
|
||||
|
||||
// Start health check handler
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
api.LogErrorf("Health check handler recovered from panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
@@ -158,7 +160,15 @@ func (s *SSEServer) HandleSSE(cb api.FilterCallbackHandler, stopChan chan struct
|
||||
case <-ticker.C:
|
||||
// Send health check message
|
||||
currentTime := time.Now().Format(time.RFC3339)
|
||||
healthCheckEvent := fmt.Sprintf(": ping - %s\n\n", currentTime)
|
||||
pingRequest := mcp.JSONRPCRequest{
|
||||
JSONRPC: mcp.JSONRPC_VERSION,
|
||||
ID: currentTime,
|
||||
Request: mcp.Request{
|
||||
Method: "ping",
|
||||
},
|
||||
}
|
||||
pingData, _ := json.Marshal(pingRequest)
|
||||
healthCheckEvent := fmt.Sprintf("event: message\ndata: %s\n\n", pingData)
|
||||
if err := s.redisClient.Publish(channel, healthCheckEvent); err != nil {
|
||||
api.LogErrorf("Failed to send health check: %v", err)
|
||||
}
|
||||
@@ -169,10 +179,10 @@ func (s *SSEServer) HandleSSE(cb api.FilterCallbackHandler, stopChan chan struct
|
||||
|
||||
// handleMessage processes incoming JSON-RPC messages from clients and sends responses
|
||||
// back through both the SSE connection and HTTP response.
|
||||
func (s *SSEServer) HandleMessage(w http.ResponseWriter, r *http.Request, body json.RawMessage) {
|
||||
func (s *SSEServer) HandleMessage(w http.ResponseWriter, r *http.Request, body json.RawMessage) int {
|
||||
if r.Method != http.MethodPost {
|
||||
s.writeJSONRPCError(w, nil, mcp.INVALID_REQUEST, fmt.Sprintf("Method %s not allowed", r.Method))
|
||||
return
|
||||
return http.StatusBadRequest
|
||||
}
|
||||
|
||||
sessionID := r.URL.Query().Get("sessionId")
|
||||
@@ -197,27 +207,34 @@ func (s *SSEServer) HandleMessage(w http.ResponseWriter, r *http.Request, body j
|
||||
|
||||
// Process message through MCPServer
|
||||
response := s.server.HandleMessage(ctx, body)
|
||||
|
||||
var status int
|
||||
// Only send response if there is one (not for notifications)
|
||||
if response != nil {
|
||||
eventData, _ := json.Marshal(response)
|
||||
|
||||
if sessionID != "" {
|
||||
if sessionID != "" && s.redisClient != nil {
|
||||
channel := GetSSEChannelName(sessionID)
|
||||
publishErr := s.redisClient.Publish(channel, fmt.Sprintf("event: message\ndata: %s\n\n", eventData))
|
||||
|
||||
if publishErr != nil {
|
||||
api.LogErrorf("Failed to publish message to Redis: %v", publishErr)
|
||||
}
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
status = http.StatusAccepted
|
||||
} else {
|
||||
// support streamable http
|
||||
w.WriteHeader(http.StatusOK)
|
||||
status = http.StatusOK
|
||||
}
|
||||
// Send HTTP response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
json.NewEncoder(w).Encode(response)
|
||||
} else {
|
||||
// For notifications, just send 202 Accepted with no body
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
status = http.StatusAccepted
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
// writeJSONRPCError writes a JSON-RPC error response with the given error details.
|
||||
@@ -232,3 +249,7 @@ func (s *SSEServer) writeJSONRPCError(
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
func (s *SSEServer) Close() {
|
||||
s.server.Close()
|
||||
}
|
||||
|
||||
@@ -40,6 +40,9 @@ func (n *NacosMcpRegsitry) ListToolsDesciption() []*registry.ToolDescription {
|
||||
}
|
||||
|
||||
func (n *NacosMcpRegsitry) GetToolRpcContext(toolName string) (*registry.RpcContext, bool) {
|
||||
if n.toolsRpcContext == nil {
|
||||
n.refreshToolsList()
|
||||
}
|
||||
tool, ok := n.toolsRpcContext[toolName]
|
||||
return tool, ok
|
||||
}
|
||||
@@ -87,9 +90,11 @@ func (n *NacosMcpRegsitry) refreshToolsListForGroup(group string, serviceMatcher
|
||||
|
||||
formatServiceName := getFormatServiceName(group, service)
|
||||
if _, ok := n.currentServiceSet[formatServiceName]; !ok {
|
||||
changed = true
|
||||
n.refreshToolsListForService(group, service)
|
||||
refreshed := n.refreshToolsListForService(group, service)
|
||||
n.listenToService(group, service)
|
||||
if refreshed {
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
|
||||
currentServiceList[formatServiceName] = true
|
||||
@@ -129,7 +134,23 @@ func getFormatServiceName(group string, service string) string {
|
||||
return fmt.Sprintf("%s_%s", group, service)
|
||||
}
|
||||
|
||||
func (n *NacosMcpRegsitry) refreshToolsListForServiceWithContent(group string, service string, newConfig *string, instances *[]model.Instance) {
|
||||
func (n *NacosMcpRegsitry) deleteToolForService(group string, service string) {
|
||||
toolsNeedReset := []string{}
|
||||
|
||||
formatServiceName := getFormatServiceName(group, service)
|
||||
for tool, _ := range n.toolsDescription {
|
||||
if strings.HasPrefix(tool, formatServiceName) {
|
||||
toolsNeedReset = append(toolsNeedReset, tool)
|
||||
}
|
||||
}
|
||||
|
||||
for _, tool := range toolsNeedReset {
|
||||
delete(n.toolsDescription, tool)
|
||||
delete(n.toolsRpcContext, tool)
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NacosMcpRegsitry) refreshToolsListForServiceWithContent(group string, service string, newConfig *string, instances *[]model.Instance) bool {
|
||||
|
||||
if newConfig == nil {
|
||||
dataId := makeToolsConfigId(service)
|
||||
@@ -140,7 +161,7 @@ func (n *NacosMcpRegsitry) refreshToolsListForServiceWithContent(group string, s
|
||||
|
||||
if err != nil {
|
||||
api.LogError(fmt.Sprintf("Get tools config for sercice %s:%s error %s", group, service, err))
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
newConfig = &content
|
||||
@@ -155,17 +176,27 @@ func (n *NacosMcpRegsitry) refreshToolsListForServiceWithContent(group string, s
|
||||
|
||||
if err != nil {
|
||||
api.LogError(fmt.Sprintf("List instance for sercice %s:%s error %s", group, service, err))
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
instances = &instancesFromNacos
|
||||
}
|
||||
|
||||
var applicationDescription registry.McpApplicationDescription
|
||||
if newConfig == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// config deleted, tools should be removed
|
||||
if len(*newConfig) == 0 {
|
||||
n.deleteToolForService(group, service)
|
||||
return true
|
||||
}
|
||||
|
||||
err := json.Unmarshal([]byte(*newConfig), &applicationDescription)
|
||||
if err != nil {
|
||||
api.LogError(fmt.Sprintf("Parse tools config for sercice %s:%s error, config is %s, error is %s", group, service, *newConfig, err))
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
wrappedInstances := []registry.Instance{}
|
||||
@@ -186,6 +217,8 @@ func (n *NacosMcpRegsitry) refreshToolsListForServiceWithContent(group string, s
|
||||
n.toolsRpcContext = map[string]*registry.RpcContext{}
|
||||
}
|
||||
|
||||
n.deleteToolForService(group, service)
|
||||
|
||||
for _, tool := range applicationDescription.ToolsDescription {
|
||||
meta := applicationDescription.ToolsMeta[tool.Name]
|
||||
|
||||
@@ -207,6 +240,7 @@ func (n *NacosMcpRegsitry) refreshToolsListForServiceWithContent(group string, s
|
||||
n.toolsRpcContext[tool.Name] = &context
|
||||
}
|
||||
n.currentServiceSet[getFormatServiceName(group, service)] = true
|
||||
return true
|
||||
}
|
||||
|
||||
func (n *NacosMcpRegsitry) GetCredential(name string, group string) *registry.CredentialInfo {
|
||||
@@ -231,8 +265,8 @@ func (n *NacosMcpRegsitry) GetCredential(name string, group string) *registry.Cr
|
||||
return &credential
|
||||
}
|
||||
|
||||
func (n *NacosMcpRegsitry) refreshToolsListForService(group string, service string) {
|
||||
n.refreshToolsListForServiceWithContent(group, service, nil, nil)
|
||||
func (n *NacosMcpRegsitry) refreshToolsListForService(group string, service string) bool {
|
||||
return n.refreshToolsListForServiceWithContent(group, service, nil, nil)
|
||||
}
|
||||
|
||||
func (n *NacosMcpRegsitry) listenToService(group string, service string) {
|
||||
|
||||
@@ -112,6 +112,10 @@ func (c *NacosConfig) ParseConfig(config map[string]any) error {
|
||||
return errors.New("missing serviceMatcher")
|
||||
}
|
||||
|
||||
if namespace, ok := config["namespace"].(string); ok {
|
||||
c.Namespace = &namespace
|
||||
}
|
||||
|
||||
matchers := map[string]string{}
|
||||
for key, value := range serviceMatcher {
|
||||
matchers[key] = value.(string)
|
||||
@@ -150,6 +154,12 @@ func (c *NacosConfig) NewServer(serverName string) (*internal.MCPServer, error)
|
||||
nacosRegistry.RegisterToolChangeEventListener(&listener)
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
api.LogErrorf("NacosToolsListRefresh recovered from panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
if nacosRegistry.refreshToolsList() {
|
||||
resetToolsToMcpServer(mcpServer, nacosRegistry)
|
||||
|
||||
@@ -50,8 +50,11 @@ func FixedQueryToken(cred *CredentialInfo, h *HttpRemoteCallHandle) {
|
||||
h.Query[key.(string)] = value.(string)
|
||||
}
|
||||
|
||||
func newHttpRemoteCallHandle(ctx *RpcContext) *HttpRemoteCallHandle {
|
||||
instance := selectOneInstance(ctx)
|
||||
func newHttpRemoteCallHandle(ctx *RpcContext) (*HttpRemoteCallHandle, error) {
|
||||
instance, err := selectOneInstance(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
method, ok := ctx.ToolMeta.InvokeContext["method"]
|
||||
if !ok {
|
||||
method = DEFAULT_HTTP_METHOD
|
||||
@@ -64,7 +67,7 @@ func newHttpRemoteCallHandle(ctx *RpcContext) *HttpRemoteCallHandle {
|
||||
|
||||
return &HttpRemoteCallHandle{
|
||||
CommonRemoteCallHandle: CommonRemoteCallHandle{
|
||||
Instance: &instance,
|
||||
Instance: instance,
|
||||
},
|
||||
Protocol: ctx.Protocol,
|
||||
Headers: http.Header{},
|
||||
@@ -72,7 +75,7 @@ func newHttpRemoteCallHandle(ctx *RpcContext) *HttpRemoteCallHandle {
|
||||
Query: map[string]string{},
|
||||
Path: path,
|
||||
Method: method,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
// http remote handle implementation
|
||||
@@ -119,14 +122,14 @@ func (h *HttpRemoteCallHandle) handleParamMapping(mapInfo *map[string]ParameterM
|
||||
for param, value := range params {
|
||||
if info, ok := paramMapInfo[param]; ok {
|
||||
if info.Position == "Query" {
|
||||
h.Query[info.BackendName] = fmt.Sprintf("%s", value)
|
||||
h.Query[info.BackendName] = fmt.Sprintf("%v", value)
|
||||
} else if info.Position == "Header" {
|
||||
h.Headers[info.BackendName] = []string{fmt.Sprintf("%s", value)}
|
||||
h.Headers[info.BackendName] = []string{fmt.Sprintf("%v", value)}
|
||||
} else {
|
||||
return fmt.Errorf("Unsupport position for args %s, pos is %s", param, info.Position)
|
||||
}
|
||||
} else {
|
||||
h.Query[param] = fmt.Sprintf("%s", value)
|
||||
h.Query[param] = fmt.Sprintf("%v", value)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -160,20 +163,25 @@ func (h *HttpRemoteCallHandle) doHttpCall() (*http.Response, error) {
|
||||
return http.DefaultClient.Do(&request)
|
||||
}
|
||||
|
||||
func selectOneInstance(ctx *RpcContext) Instance {
|
||||
func selectOneInstance(ctx *RpcContext) (*Instance, error) {
|
||||
instanceId := 0
|
||||
if ctx.Instances == nil || len(*ctx.Instances) == 0 {
|
||||
return nil, fmt.Errorf("No instance")
|
||||
}
|
||||
|
||||
instances := *ctx.Instances
|
||||
if len(instances) != 1 {
|
||||
if len(instances) > 1 {
|
||||
instanceId = rand.Intn(len(instances) - 1)
|
||||
}
|
||||
return instances[instanceId]
|
||||
select_instance := instances[instanceId]
|
||||
return &select_instance, nil
|
||||
}
|
||||
|
||||
func getRemoteCallhandle(ctx *RpcContext) RemoteCallHandle {
|
||||
func getRemoteCallhandle(ctx *RpcContext) (RemoteCallHandle, error) {
|
||||
if ctx.Protocol == PROTOCOL_HTTP || ctx.Protocol == PROTOCOL_HTTPS {
|
||||
return newHttpRemoteCallHandle(ctx)
|
||||
} else {
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,11 +192,15 @@ func CommonRemoteCall(reg McpServerRegistry, toolName string, parameters map[str
|
||||
return nil, fmt.Errorf("Unknown tool %s", toolName)
|
||||
}
|
||||
|
||||
remoteHandle := getRemoteCallhandle(ctx)
|
||||
remoteHandle, err := getRemoteCallhandle(ctx)
|
||||
if remoteHandle == nil {
|
||||
return nil, fmt.Errorf("Unknown backend protocol %s", ctx.Protocol)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Call backend server error: %w", err)
|
||||
}
|
||||
|
||||
return remoteHandle.HandleToolCall(ctx, parameters)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,47 +1,148 @@
|
||||
package gorm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
"gorm.io/driver/clickhouse"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
// DBClient is a struct to handle PostgreSQL connections and operations
|
||||
// DBClient is a struct to handle database connections and operations
|
||||
type DBClient struct {
|
||||
db *gorm.DB
|
||||
db *gorm.DB
|
||||
dsn string
|
||||
dbType string
|
||||
reconnect chan struct{}
|
||||
stop chan struct{}
|
||||
panicCount int32 // Add panic counter
|
||||
}
|
||||
|
||||
// NewDBClient creates a new DBClient instance and establishes a connection to the PostgreSQL database
|
||||
func NewDBClient(dsn string, dbType string) (*DBClient, error) {
|
||||
var db *gorm.DB
|
||||
var err error
|
||||
if dbType == "postgres" {
|
||||
db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
|
||||
} else if dbType == "clickhouse" {
|
||||
db, err = gorm.Open(clickhouse.Open(dsn), &gorm.Config{})
|
||||
} else if dbType == "mysql" {
|
||||
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
} else if dbType == "sqlite" {
|
||||
db, err = gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
} else {
|
||||
return nil, fmt.Errorf("unsupported database type %s", dbType)
|
||||
}
|
||||
// Connect to the database
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to database: %w", err)
|
||||
// NewDBClient creates a new DBClient instance and establishes a connection to the database
|
||||
func NewDBClient(dsn string, dbType string, stop chan struct{}) *DBClient {
|
||||
client := &DBClient{
|
||||
dsn: dsn,
|
||||
dbType: dbType,
|
||||
reconnect: make(chan struct{}, 1),
|
||||
stop: stop,
|
||||
}
|
||||
|
||||
return &DBClient{db: db}, nil
|
||||
// Start reconnection goroutine
|
||||
go client.reconnectLoop()
|
||||
|
||||
// Try initial connection
|
||||
if err := client.connect(); err != nil {
|
||||
api.LogErrorf("Initial database connection failed: %v", err)
|
||||
}
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
func (c *DBClient) connect() error {
|
||||
var db *gorm.DB
|
||||
var err error
|
||||
gormConfig := gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
}
|
||||
|
||||
switch c.dbType {
|
||||
case "postgres":
|
||||
db, err = gorm.Open(postgres.Open(c.dsn), &gormConfig)
|
||||
case "clickhouse":
|
||||
db, err = gorm.Open(clickhouse.Open(c.dsn), &gormConfig)
|
||||
case "mysql":
|
||||
db, err = gorm.Open(mysql.Open(c.dsn), &gormConfig)
|
||||
case "sqlite":
|
||||
db, err = gorm.Open(sqlite.Open(c.dsn), &gormConfig)
|
||||
default:
|
||||
return fmt.Errorf("unsupported database type %s", c.dbType)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to database: %w", err)
|
||||
}
|
||||
|
||||
c.db = db
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *DBClient) reconnectLoop() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
api.LogErrorf("Recovered from panic in reconnectLoop: %v", r)
|
||||
|
||||
// Increment panic counter
|
||||
atomic.AddInt32(&c.panicCount, 1)
|
||||
|
||||
// If panic count exceeds threshold, stop trying to reconnect
|
||||
if atomic.LoadInt32(&c.panicCount) > 3 {
|
||||
api.LogErrorf("Too many panics in reconnectLoop, stopping reconnection attempts")
|
||||
return
|
||||
}
|
||||
|
||||
// Wait for a while before restarting
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
// Restart the reconnect loop
|
||||
go c.reconnectLoop()
|
||||
}
|
||||
}()
|
||||
|
||||
ticker := time.NewTicker(30 * time.Second) // Try to reconnect every 30 seconds
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.stop:
|
||||
api.LogInfof("Database %s connection closed", c.dbType)
|
||||
return
|
||||
case <-ticker.C:
|
||||
if c.db == nil || c.Ping() != nil {
|
||||
if err := c.connect(); err != nil {
|
||||
api.LogErrorf("Database reconnection failed: %v", err)
|
||||
} else {
|
||||
api.LogInfof("Database reconnected successfully")
|
||||
// Reset panic count on successful connection
|
||||
atomic.StoreInt32(&c.panicCount, 0)
|
||||
}
|
||||
}
|
||||
case <-c.reconnect:
|
||||
if err := c.connect(); err != nil {
|
||||
api.LogErrorf("Database reconnection failed: %v", err)
|
||||
} else {
|
||||
api.LogInfof("Database reconnected successfully")
|
||||
// Reset panic count on successful connection
|
||||
atomic.StoreInt32(&c.panicCount, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ExecuteSQL executes a raw SQL query and returns the result as a slice of maps
|
||||
func (c *DBClient) ExecuteSQL(query string, args ...interface{}) ([]map[string]interface{}, error) {
|
||||
if c.db == nil {
|
||||
// Trigger reconnection
|
||||
select {
|
||||
case c.reconnect <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
return nil, fmt.Errorf("database is not connected, attempting to reconnect")
|
||||
}
|
||||
|
||||
rows, err := c.db.Raw(query, args...).Rows()
|
||||
if err != nil {
|
||||
// If execution fails, connection might be lost, trigger reconnection
|
||||
select {
|
||||
case c.reconnect <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
return nil, fmt.Errorf("failed to execute SQL query: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
@@ -88,3 +189,21 @@ func (c *DBClient) ExecuteSQL(query string, args ...interface{}) ([]map[string]i
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (c *DBClient) Ping() error {
|
||||
if c.db == nil {
|
||||
return fmt.Errorf("database connection is nil")
|
||||
}
|
||||
|
||||
// Use context to set timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Try to ping the database
|
||||
sqlDB, err := c.db.DB()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get underlying *sql.DB: %v", err)
|
||||
}
|
||||
|
||||
return sqlDB.PingContext(ctx)
|
||||
}
|
||||
|
||||
@@ -16,8 +16,9 @@ func init() {
|
||||
}
|
||||
|
||||
type DBConfig struct {
|
||||
dbType string
|
||||
dsn string
|
||||
dbType string
|
||||
dsn string
|
||||
description string
|
||||
}
|
||||
|
||||
func (c *DBConfig) ParseConfig(config map[string]any) error {
|
||||
@@ -33,6 +34,10 @@ func (c *DBConfig) ParseConfig(config map[string]any) error {
|
||||
}
|
||||
c.dbType = dbType
|
||||
api.LogDebugf("DBConfig ParseConfig: %+v", config)
|
||||
c.description, ok = config["description"].(string)
|
||||
if !ok {
|
||||
c.description = ""
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -43,14 +48,10 @@ func (c *DBConfig) NewServer(serverName string) (*internal.MCPServer, error) {
|
||||
internal.WithInstructions(fmt.Sprintf("This is a %s database server", c.dbType)),
|
||||
)
|
||||
|
||||
dbClient, err := NewDBClient(c.dsn, c.dbType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize DBClient: %w", err)
|
||||
}
|
||||
|
||||
dbClient := NewDBClient(c.dsn, c.dbType, mcpServer.GetDestoryChannel())
|
||||
// Add query tool
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("query", fmt.Sprintf("Run a read-only SQL query in database %s", c.dbType), GetQueryToolSchema()),
|
||||
mcp.NewToolWithRawSchema("query", fmt.Sprintf("Run a read-only SQL query in database %s. Database description: %s", c.dbType, c.description), GetQueryToolSchema()),
|
||||
HandleQueryTool(dbClient),
|
||||
)
|
||||
|
||||
|
||||
@@ -17,7 +17,14 @@ COPY . .
|
||||
WORKDIR /workspace/extensions/$PLUGIN_NAME
|
||||
|
||||
RUN go mod tidy
|
||||
RUN tinygo build -o /main.wasm -scheduler=none -gc=custom -tags="custommalloc nottinygc_finalizer $EXTRA_TAGS" -target=wasi ./
|
||||
RUN \
|
||||
if echo "$PLUGIN_NAME" | grep -Eq '^waf$'; then \
|
||||
# Please use higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/wasm-go-builder:go1.19-tinygo0.28.1-oras1.0.0 as BUILDER
|
||||
go run mage.go build && \
|
||||
mv ./local/main.wasm /main.wasm ; \
|
||||
else \
|
||||
tinygo build -o /main.wasm -scheduler=none -gc=custom -tags="custommalloc nottinygc_finalizer $EXTRA_TAGS" -target=wasi ./ ; \
|
||||
fi
|
||||
|
||||
FROM scratch as output
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ description: AI 代理插件配置参考
|
||||
| `type` | string | 必填 | - | AI 服务提供商名称 |
|
||||
| `apiTokens` | array of string | 非必填 | - | 用于在访问 AI 服务时进行认证的令牌。如果配置了多个 token,插件会在请求时随机进行选择。部分服务提供商只支持配置一个 token。 |
|
||||
| `timeout` | number | 非必填 | - | 访问 AI 服务的超时时间。单位为毫秒。默认值为 120000,即 2 分钟。此项配置目前仅用于获取上下文信息,并不影响实际转发大模型请求。 |
|
||||
| `modelMapping` | map of string | 非必填 | - | AI 模型映射表,用于将请求中的模型名称映射为服务提供商支持模型名称。<br/>1. 支持前缀匹配。例如用 "gpt-3-*" 匹配所有名称以“gpt-3-”开头的模型;<br/>2. 支持使用 "*" 为键来配置通用兜底映射关系;<br/>3. 如果映射的目标名称为空字符串 "",则表示保留原模型名称。 |
|
||||
| `modelMapping` | map of string | 非必填 | - | AI 模型映射表,用于将请求中的模型名称映射为服务提供商支持模型名称。<br/>1. 支持前缀匹配。例如用 "gpt-3-\*" 匹配所有名称以“gpt-3-”开头的模型;<br/>2. 支持使用 "\*" 为键来配置通用兜底映射关系;<br/>3. 如果映射的目标名称为空字符串 "",则表示保留原模型名称。 |
|
||||
| `protocol` | string | 非必填 | - | 插件对外提供的 API 接口契约。目前支持以下取值:openai(默认值,使用 OpenAI 的接口契约)、original(使用目标服务提供商的原始接口契约) |
|
||||
| `context` | object | 非必填 | - | 配置 AI 对话上下文信息 |
|
||||
| `customSettings` | array of customSetting | 非必填 | - | 为AI请求指定覆盖或者填充参数 |
|
||||
|
||||
@@ -34,7 +34,7 @@ Plugin execution priority: `100`
|
||||
| `type` | string | Required | - | Name of the AI service provider |
|
||||
| `apiTokens` | array of string | Optional | - | Tokens used for authentication when accessing AI services. If multiple tokens are configured, the plugin randomly selects one for each request. Some service providers only support configuring a single token. |
|
||||
| `timeout` | number | Optional | - | Timeout for accessing AI services, in milliseconds. The default value is 120000, which equals 2 minutes. Only used when retrieving context data. Won't affect the request forwarded to the LLM upstream. |
|
||||
| `modelMapping` | map of string | Optional | - | Mapping table for AI models, used to map model names in requests to names supported by the service provider.<br/>1. Supports prefix matching. For example, "gpt-3-*" matches all model names starting with “gpt-3-”;<br/>2. Supports using "*" as a key for a general fallback mapping;<br/>3. If the mapped target name is an empty string "", the original model name is preserved. |
|
||||
| `modelMapping` | map of string | Optional | - | Mapping table for AI models, used to map model names in requests to names supported by the service provider.<br/>1. Supports prefix matching. For example, "gpt-3-\*" matches all model names starting with “gpt-3-”;<br/>2. Supports using "\*" as a key for a general fallback mapping;<br/>3. If the mapped target name is an empty string "", the original model name is preserved. |
|
||||
| `protocol` | string | Optional | - | API contract provided by the plugin. Currently supports the following values: openai (default, uses OpenAI's interface contract), original (uses the raw interface contract of the target service provider) |
|
||||
| `context` | object | Optional | - | Configuration for AI conversation context information |
|
||||
| `customSettings` | array of customSetting | Optional | - | Specifies overrides or fills parameters for AI requests |
|
||||
|
||||
@@ -61,46 +61,6 @@ Attribute 配置说明:
|
||||
|
||||
### 空配置
|
||||
#### 监控
|
||||
```
|
||||
route_upstream_model_metric_input_token{ai_route="llm",ai_cluster="outbound|443||qwen.dns",ai_model="qwen-turbo"} 10
|
||||
route_upstream_model_metric_llm_duration_count{ai_route="llm",ai_cluster="outbound|443||qwen.dns",ai_model="qwen-turbo"} 1
|
||||
route_upstream_model_metric_llm_first_token_duration{ai_route="llm",ai_cluster="outbound|443||qwen.dns",ai_model="qwen-turbo"} 309
|
||||
route_upstream_model_metric_llm_service_duration{ai_route="llm",ai_cluster="outbound|443||qwen.dns",ai_model="qwen-turbo"} 1955
|
||||
route_upstream_model_metric_output_token{ai_route="llm",ai_cluster="outbound|443||qwen.dns",ai_model="qwen-turbo"} 69
|
||||
```
|
||||
|
||||
#### 日志
|
||||
```json
|
||||
{
|
||||
"ai_log":"{\"model\":\"qwen-turbo\",\"input_token\":\"10\",\"output_token\":\"69\",\"llm_first_token_duration\":\"309\",\"llm_service_duration\":\"1955\"}"
|
||||
}
|
||||
```
|
||||
|
||||
#### 链路追踪
|
||||
配置为空时,不会在span中添加额外的attribute
|
||||
|
||||
### 从非openai协议提取token使用信息
|
||||
在ai-proxy中设置协议为original时,以百炼为例,可作如下配置指定如何提取model, input_token, output_token
|
||||
|
||||
```yaml
|
||||
attributes:
|
||||
- key: model
|
||||
value_source: response_body
|
||||
value: usage.models.0.model_id
|
||||
apply_to_log: true
|
||||
apply_to_span: false
|
||||
- key: input_token
|
||||
value_source: response_body
|
||||
value: usage.models.0.input_tokens
|
||||
apply_to_log: true
|
||||
apply_to_span: false
|
||||
- key: output_token
|
||||
value_source: response_body
|
||||
value: usage.models.0.output_tokens
|
||||
apply_to_log: true
|
||||
apply_to_span: false
|
||||
```
|
||||
#### 监控
|
||||
|
||||
```
|
||||
# counter 类型,输入 token 数量的累加值
|
||||
@@ -140,11 +100,51 @@ irate(route_upstream_model_consumer_metric_llm_service_duration[2m])
|
||||
irate(route_upstream_model_consumer_metric_llm_duration_count[2m])
|
||||
```
|
||||
|
||||
#### 日志
|
||||
```json
|
||||
{
|
||||
"ai_log":"{\"model\":\"qwen-turbo\",\"input_token\":\"10\",\"output_token\":\"69\",\"llm_first_token_duration\":\"309\",\"llm_service_duration\":\"1955\"}"
|
||||
}
|
||||
```
|
||||
|
||||
#### 链路追踪
|
||||
配置为空时,不会在span中添加额外的attribute
|
||||
|
||||
### 从非openai协议提取token使用信息
|
||||
在ai-proxy中设置协议为original时,以百炼为例,可作如下配置指定如何提取model, input_token, output_token
|
||||
|
||||
```yaml
|
||||
attributes:
|
||||
- key: model
|
||||
value_source: response_body
|
||||
value: usage.models.0.model_id
|
||||
apply_to_log: true
|
||||
apply_to_span: false
|
||||
- key: input_token
|
||||
value_source: response_body
|
||||
value: usage.models.0.input_tokens
|
||||
apply_to_log: true
|
||||
apply_to_span: false
|
||||
- key: output_token
|
||||
value_source: response_body
|
||||
value: usage.models.0.output_tokens
|
||||
apply_to_log: true
|
||||
apply_to_span: false
|
||||
```
|
||||
#### 监控
|
||||
|
||||
```
|
||||
route_upstream_model_consumer_metric_input_token{ai_route="bailian",ai_cluster="qwen",ai_model="qwen-max"} 343
|
||||
route_upstream_model_consumer_metric_output_token{ai_route="bailian",ai_cluster="qwen",ai_model="qwen-max"} 153
|
||||
route_upstream_model_consumer_metric_llm_service_duration{ai_route="bailian",ai_cluster="qwen",ai_model="qwen-max"} 3725
|
||||
route_upstream_model_consumer_metric_llm_duration_count{ai_route="bailian",ai_cluster="qwen",ai_model="qwen-max"} 1
|
||||
```
|
||||
|
||||
#### 日志
|
||||
此配置下日志效果如下:
|
||||
```json
|
||||
{
|
||||
"ai_log": "{\"model\":\"qwen-max\",\"input_token\":\"343\",\"output_token\":\"153\",\"llm_service_duration\":\"19110\"}"
|
||||
"ai_log": "{\"model\":\"qwen-max\",\"input_token\":\"343\",\"output_token\":\"153\",\"llm_service_duration\":\"19110\"}"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -152,7 +152,7 @@ irate(route_upstream_model_consumer_metric_llm_duration_count[2m])
|
||||
链路追踪的 span 中可以看到 model, input_token, output_token 三个额外的 attribute
|
||||
|
||||
### 配合认证鉴权记录consumer
|
||||
举例如下:
|
||||
举例如下:
|
||||
```yaml
|
||||
attributes:
|
||||
- key: consumer # 配合认证鉴权记录consumer
|
||||
|
||||
@@ -48,12 +48,12 @@ The meanings of various values for `value_source` are as follows:
|
||||
|
||||
When `value_source` is `response_streaming_body`, `rule` should be configured to specify how to obtain the specified value from the streaming body. The meaning of the value is as follows:
|
||||
|
||||
- `first`: extract value from the first valid chunk
|
||||
- `replace`: extract value from the last valid chunk
|
||||
- `first`: extract value from the first valid chunk
|
||||
- `replace`: extract value from the last valid chunk
|
||||
- `append`: join value pieces from all valid chunks
|
||||
|
||||
## Configuration example
|
||||
If you want to record ai-statistic related statistical values in the gateway access log, you need to modify log_format and add a new field based on the original log_format. The example is as follows:
|
||||
If you want to record ai-statistic related statistical values in the gateway access log, you need to modify log_format and add a new field based on the original log_format. The example is as follows:
|
||||
|
||||
```yaml
|
||||
'{"ai_log":"%FILTER_STATE(wasm.ai_log:PLAIN)%"}'
|
||||
@@ -61,48 +61,6 @@ If you want to record ai-statistic related statistical values in the
|
||||
|
||||
### Empty
|
||||
#### Metric
|
||||
```
|
||||
route_upstream_model_metric_input_token{ai_route="llm",ai_cluster="outbound|443||qwen.dns",ai_model="qwen-turbo"} 10
|
||||
route_upstream_model_metric_llm_duration_count{ai_route="llm",ai_cluster="outbound|443||qwen.dns",ai_model="qwen-turbo"} 1
|
||||
route_upstream_model_metric_llm_first_token_duration{ai_route="llm",ai_cluster="outbound|443||qwen.dns",ai_model="qwen-turbo"} 309
|
||||
route_upstream_model_metric_llm_service_duration{ai_route="llm",ai_cluster="outbound|443||qwen.dns",ai_model="qwen-turbo"} 1955
|
||||
route_upstream_model_metric_output_token{ai_route="llm",ai_cluster="outbound|443||qwen.dns",ai_model="qwen-turbo"} 69
|
||||
```
|
||||
|
||||
#### Log
|
||||
```json
|
||||
{
|
||||
"ai_log":"{\"model\":\"qwen-turbo\",\"input_token\":\"10\",\"output_token\":\"69\",\"llm_first_token_duration\":\"309\",\"llm_service_duration\":\"1955\"}"
|
||||
}
|
||||
```
|
||||
|
||||
#### Trace
|
||||
When the configuration is empty, no additional attributes will be added to the span.
|
||||
|
||||
### Extract token usage information from non-openai protocols
|
||||
When setting the protocol to original in ai-proxy, taking Alibaba Cloud Bailian as an example, you can make the following configuration to specify how to extract `model`, `input_token`, `output_token`
|
||||
|
||||
```yaml
|
||||
attributes:
|
||||
- key: model
|
||||
value_source: response_body
|
||||
value: usage.models.0.model_id
|
||||
apply_to_log: true
|
||||
apply_to_span: false
|
||||
- key: input_token
|
||||
value_source: response_body
|
||||
value: usage.models.0.input_tokens
|
||||
apply_to_log: true
|
||||
apply_to_span: false
|
||||
- key: output_token
|
||||
value_source: response_body
|
||||
value: usage.models.0.output_tokens
|
||||
apply_to_log: true
|
||||
apply_to_span: false
|
||||
```
|
||||
#### Metric
|
||||
|
||||
Here is the English translation:
|
||||
|
||||
```
|
||||
# counter, cumulative count of input tokens
|
||||
@@ -145,7 +103,47 @@ irate(route_upstream_model_consumer_metric_llm_duration_count[2m])
|
||||
#### Log
|
||||
```json
|
||||
{
|
||||
"ai_log": "{\"model\":\"qwen-max\",\"input_token\":\"343\",\"output_token\":\"153\",\"llm_service_duration\":\"19110\"}"
|
||||
"ai_log":"{\"model\":\"qwen-turbo\",\"input_token\":\"10\",\"output_token\":\"69\",\"llm_first_token_duration\":\"309\",\"llm_service_duration\":\"1955\"}"
|
||||
}
|
||||
```
|
||||
|
||||
#### Trace
|
||||
When the configuration is empty, no additional attributes will be added to the span.
|
||||
|
||||
### Extract token usage information from non-openai protocols
|
||||
When setting the protocol to original in ai-proxy, taking Alibaba Cloud Bailian as an example, you can make the following configuration to specify how to extract `model`, `input_token`, `output_token`
|
||||
|
||||
```yaml
|
||||
attributes:
|
||||
- key: model
|
||||
value_source: response_body
|
||||
value: usage.models.0.model_id
|
||||
apply_to_log: true
|
||||
apply_to_span: false
|
||||
- key: input_token
|
||||
value_source: response_body
|
||||
value: usage.models.0.input_tokens
|
||||
apply_to_log: true
|
||||
apply_to_span: false
|
||||
- key: output_token
|
||||
value_source: response_body
|
||||
value: usage.models.0.output_tokens
|
||||
apply_to_log: true
|
||||
apply_to_span: false
|
||||
```
|
||||
#### Metric
|
||||
|
||||
```
|
||||
route_upstream_model_consumer_metric_input_token{ai_route="bailian",ai_cluster="qwen",ai_model="qwen-max"} 343
|
||||
route_upstream_model_consumer_metric_output_token{ai_route="bailian",ai_cluster="qwen",ai_model="qwen-max"} 153
|
||||
route_upstream_model_consumer_metric_llm_service_duration{ai_route="bailian",ai_cluster="qwen",ai_model="qwen-max"} 3725
|
||||
route_upstream_model_consumer_metric_llm_duration_count{ai_route="bailian",ai_cluster="qwen",ai_model="qwen-max"} 1
|
||||
```
|
||||
|
||||
#### Log
|
||||
```json
|
||||
{
|
||||
"ai_log": "{\"model\":\"qwen-max\",\"input_token\":\"343\",\"output_token\":\"153\",\"llm_service_duration\":\"19110\"}"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -164,7 +162,7 @@ attributes:
|
||||
### Record questions and answers
|
||||
```yaml
|
||||
attributes:
|
||||
- key: question
|
||||
- key: question
|
||||
value_source: request_body
|
||||
value: messages.@reverse.0.content
|
||||
apply_to_log: true
|
||||
|
||||
@@ -17,4 +17,5 @@ require (
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tidwall/resp v0.1.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
)
|
||||
|
||||
@@ -9,6 +9,7 @@ github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
|
||||
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94=
|
||||
github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
@@ -17,4 +18,6 @@ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
@@ -46,7 +46,7 @@ const (
|
||||
ResponseStreamingBody = "response_streaming_body"
|
||||
ResponseBody = "response_body"
|
||||
|
||||
// Inner metric & log attributes name
|
||||
// Inner metric & log attributes
|
||||
Model = "model"
|
||||
InputToken = "input_token"
|
||||
OutputToken = "output_token"
|
||||
@@ -55,6 +55,16 @@ const (
|
||||
LLMDurationCount = "llm_duration_count"
|
||||
LLMStreamDurationCount = "llm_stream_duration_count"
|
||||
ResponseType = "response_type"
|
||||
ChatID = "chat_id"
|
||||
ChatRound = "chat_round"
|
||||
|
||||
// Inner span attributes
|
||||
ArmsSpanKind = "gen_ai.span.kind"
|
||||
ArmsModelName = "gen_ai.model_name"
|
||||
ArmsRequestModel = "gen_ai.request.model"
|
||||
ArmsInputToken = "gen_ai.usage.input_tokens"
|
||||
ArmsOutputToken = "gen_ai.usage.output_tokens"
|
||||
ArmsTotalToken = "gen_ai.usage.total_tokens"
|
||||
|
||||
// Extract Rule
|
||||
RuleFirst = "first"
|
||||
@@ -171,7 +181,8 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, config AIStatisticsConfig, lo
|
||||
setAttributeBySource(ctx, config, FixedValue, nil, log)
|
||||
// Set user defined log & span attributes which type is request_header
|
||||
setAttributeBySource(ctx, config, RequestHeader, nil, log)
|
||||
// Set request start time.
|
||||
// Set span attributes for ARMS.
|
||||
setSpanAttribute(ArmsSpanKind, "LLM", log)
|
||||
|
||||
return types.ActionContinue
|
||||
}
|
||||
@@ -179,6 +190,22 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, config AIStatisticsConfig, lo
|
||||
func onHttpRequestBody(ctx wrapper.HttpContext, config AIStatisticsConfig, body []byte, log wrapper.Log) types.Action {
|
||||
// Set user defined log & span attributes.
|
||||
setAttributeBySource(ctx, config, RequestBody, body, log)
|
||||
// Set span attributes for ARMS.
|
||||
requestModel := gjson.GetBytes(body, "model").String()
|
||||
if requestModel == "" {
|
||||
requestModel = "UNKNOWN"
|
||||
}
|
||||
setSpanAttribute(ArmsRequestModel, requestModel, log)
|
||||
// Set the number of conversation rounds
|
||||
if gjson.GetBytes(body, "messages").Exists() {
|
||||
userPromptCount := 0
|
||||
for _, msg := range gjson.GetBytes(body, "messages").Array() {
|
||||
if msg.Get("role").String() == "user" {
|
||||
userPromptCount += 1
|
||||
}
|
||||
}
|
||||
ctx.SetUserAttribute(ChatRound, userPromptCount)
|
||||
}
|
||||
|
||||
// Write log
|
||||
ctx.WriteUserAttributeToLogWithKey(wrapper.AILogKey)
|
||||
@@ -211,6 +238,10 @@ func onHttpStreamingBody(ctx wrapper.HttpContext, config AIStatisticsConfig, dat
|
||||
}
|
||||
|
||||
ctx.SetUserAttribute(ResponseType, "stream")
|
||||
chatID := gjson.GetBytes(data, "id").String()
|
||||
if chatID != "" {
|
||||
ctx.SetUserAttribute(ChatID, chatID)
|
||||
}
|
||||
|
||||
// Get requestStartTime from http context
|
||||
requestStartTime, ok := ctx.GetContext(StatisticsRequestStartTime).(int64)
|
||||
@@ -231,6 +262,11 @@ func onHttpStreamingBody(ctx wrapper.HttpContext, config AIStatisticsConfig, dat
|
||||
ctx.SetUserAttribute(Model, model)
|
||||
ctx.SetUserAttribute(InputToken, inputToken)
|
||||
ctx.SetUserAttribute(OutputToken, outputToken)
|
||||
// Set span attributes for ARMS.
|
||||
setSpanAttribute(ArmsModelName, model, log)
|
||||
setSpanAttribute(ArmsInputToken, inputToken, log)
|
||||
setSpanAttribute(ArmsOutputToken, outputToken, log)
|
||||
setSpanAttribute(ArmsTotalToken, inputToken+outputToken, log)
|
||||
}
|
||||
// If the end of the stream is reached, record metrics/logs/spans.
|
||||
if endOfStream {
|
||||
@@ -263,12 +299,21 @@ func onHttpResponseBody(ctx wrapper.HttpContext, config AIStatisticsConfig, body
|
||||
ctx.SetUserAttribute(LLMServiceDuration, responseEndTime-requestStartTime)
|
||||
|
||||
ctx.SetUserAttribute(ResponseType, "normal")
|
||||
chatID := gjson.GetBytes(body, "id").String()
|
||||
if chatID != "" {
|
||||
ctx.SetUserAttribute(ChatID, chatID)
|
||||
}
|
||||
|
||||
// Set information about this request
|
||||
if model, inputToken, outputToken, ok := getUsage(body); ok {
|
||||
ctx.SetUserAttribute(Model, model)
|
||||
ctx.SetUserAttribute(InputToken, inputToken)
|
||||
ctx.SetUserAttribute(OutputToken, outputToken)
|
||||
// Set span attributes for ARMS.
|
||||
setSpanAttribute(ArmsModelName, model, log)
|
||||
setSpanAttribute(ArmsInputToken, inputToken, log)
|
||||
setSpanAttribute(ArmsOutputToken, outputToken, log)
|
||||
setSpanAttribute(ArmsTotalToken, inputToken+outputToken, log)
|
||||
}
|
||||
|
||||
// Set user defined log & span attributes.
|
||||
@@ -283,8 +328,14 @@ func onHttpResponseBody(ctx wrapper.HttpContext, config AIStatisticsConfig, body
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
func unifySSEChunk(data []byte) []byte {
|
||||
data = bytes.ReplaceAll(data, []byte("\r\n"), []byte("\n"))
|
||||
data = bytes.ReplaceAll(data, []byte("\r"), []byte("\n"))
|
||||
return data
|
||||
}
|
||||
|
||||
func getUsage(data []byte) (model string, inputTokenUsage int64, outputTokenUsage int64, ok bool) {
|
||||
chunks := bytes.Split(bytes.TrimSpace(data), []byte("\n\n"))
|
||||
chunks := bytes.Split(bytes.TrimSpace(unifySSEChunk(data)), []byte("\n\n"))
|
||||
for _, chunk := range chunks {
|
||||
// the feature strings are used to identify the usage data, like:
|
||||
// {"model":"gpt2","usage":{"prompt_tokens":1,"completion_tokens":1}}
|
||||
@@ -353,7 +404,7 @@ func setAttributeBySource(ctx wrapper.HttpContext, config AIStatisticsConfig, so
|
||||
}
|
||||
|
||||
func extractStreamingBodyByJsonPath(data []byte, jsonPath string, rule string, log wrapper.Log) interface{} {
|
||||
chunks := bytes.Split(bytes.TrimSpace(data), []byte("\n\n"))
|
||||
chunks := bytes.Split(bytes.TrimSpace(unifySSEChunk(data)), []byte("\n\n"))
|
||||
var value interface{}
|
||||
if rule == RuleFirst {
|
||||
for _, chunk := range chunks {
|
||||
|
||||
@@ -14,34 +14,152 @@ description: 自定义应答插件配置参考
|
||||
插件执行优先级:`910`
|
||||
|
||||
## 配置字段
|
||||
### 新版本-支持多种返回
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|-------|------------------|------|-----|-------------------------------------|
|
||||
| rules | array of object | 必填 | - | 规则组 |
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| -------- | -------- | -------- | -------- | -------- |
|
||||
| status_code | number | 选填 | 200 | 自定义 HTTP 应答状态码 |
|
||||
| headers | array of string | 选填 | - | 自定义 HTTP 应答头,key 和 value 用`=`分隔 |
|
||||
| body | string | 选填 | - | 自定义 HTTP 应答 Body |
|
||||
| enable_on_status | array of number | 选填 | - | 匹配原始状态码,生成自定义响应,不填写时,不判断原始状态码 |
|
||||
`rules`的配置字段说明如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|--------------------|---------------------------|------|-----|------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `status_code` | number | 选填 | 200 | 自定义 HTTP 应答状态码 |
|
||||
| `headers` | array of string | 选填 | - | 自定义 HTTP 应答头,key 和 value 用`=`分隔 |
|
||||
| `body` | string | 选填 | - | 自定义 HTTP 应答 Body |
|
||||
| `enable_on_status` | array of string or number | 选填 | - | 匹配原始状态码,生成自定义响应。可填写精确值如:`200`,`404`等,也可以模糊匹配例如:`2xx`来匹配200-299之间的状态码,`20x`来匹配200-209之间的状态码,x代表任意一位数字。不填写时,不判断原始状态码,取第一个`enable_on_status`为空的规则作为默认规则 |
|
||||
|
||||
#### 模糊匹配规则:
|
||||
* 长度为3
|
||||
* 至少一位数字
|
||||
* 至少一位x(不区分大小写)
|
||||
|
||||
| 规则 | 匹配内容 |
|
||||
|-----|------------------------------------------------------------------------------------------|
|
||||
| 40x | 400-409;前两位为40的情况 |
|
||||
| 1x4 | 104,114,124,134,144,154,164,174,184,194;第一位和第三位分别为1和4的情况 |
|
||||
| x23 | 023,123,223,323,423,523,623,723,823,923;第二位和第三位为23的情况 |
|
||||
| 4xx | 400-499;第一位为4的情况 |
|
||||
| x4x | 040-049,140-149,240-249,340-349,440-449,540-549,640-649,740-749,840-849,940-949;第二位为4的情况 |
|
||||
| xx4 | 尾数为4的情况 |
|
||||
|
||||
### 老版本-只支持一种返回
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| -------- | -------- |------| -------- |---------------------------------|
|
||||
| `status_code` | number | 选填 | 200 | 自定义 HTTP 应答状态码 |
|
||||
| `headers` | array of string | 选填 | - | 自定义 HTTP 应答头,key 和 value 用`=`分隔 |
|
||||
| `body` | string | 选填 | - | 自定义 HTTP 应答 Body |
|
||||
| `enable_on_status` | array of number | 选填 | - | 匹配原始状态码,生成自定义响应,不填写时,不判断原始状态码 |
|
||||
|
||||
匹配优先级:精确匹配 > 模糊匹配 > 默认配置(第一个enable_on_status为空的配置)
|
||||
|
||||
## 配置示例
|
||||
|
||||
### Mock 应答场景
|
||||
### 新版本-不同状态码不同应答场景
|
||||
|
||||
```yaml
|
||||
status_code: 200
|
||||
headers:
|
||||
- Content-Type=application/json
|
||||
- Hello=World
|
||||
body: "{\"hello\":\"world\"}"
|
||||
|
||||
rules:
|
||||
- body: '{"hello":"world 200"}'
|
||||
enable_on_status:
|
||||
- 200
|
||||
- 201
|
||||
headers:
|
||||
- key1=value1
|
||||
- key2=value2
|
||||
status_code: 200
|
||||
- body: '{"hello":"world 404"}'
|
||||
enable_on_status:
|
||||
- 404
|
||||
headers:
|
||||
- key1=value1
|
||||
- key2=value2
|
||||
status_code: 200
|
||||
```
|
||||
|
||||
根据该配置,请求将返回自定义应答如下:
|
||||
根据该配置,200、201请求将返回自定义应答如下:
|
||||
|
||||
```text
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
Hello: World
|
||||
Content-Length: 17
|
||||
key1: value1
|
||||
key2: value2
|
||||
Content-Length: 21
|
||||
|
||||
{"hello":"world 200"}
|
||||
```
|
||||
根据该配置,404请求将返回自定义应答如下:
|
||||
|
||||
```text
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
key1: value1
|
||||
key2: value2
|
||||
Content-Length: 21
|
||||
|
||||
{"hello":"world 400"}
|
||||
```
|
||||
|
||||
### 新版本-模糊匹配场景
|
||||
|
||||
```yaml
|
||||
rules:
|
||||
- body: '{"hello":"world 200"}'
|
||||
enable_on_status:
|
||||
- 200
|
||||
headers:
|
||||
- key1=value1
|
||||
- key2=value2
|
||||
status_code: 200
|
||||
- body: '{"hello":"world 40x"}'
|
||||
enable_on_status:
|
||||
- '40x'
|
||||
headers:
|
||||
- key1=value1
|
||||
- key2=value2
|
||||
status_code: 200
|
||||
```
|
||||
|
||||
根据该配置,200状态码将返回自定义应答如下:
|
||||
|
||||
```text
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
key1: value1
|
||||
key2: value2
|
||||
Content-Length: 21
|
||||
|
||||
{"hello":"world 200"}
|
||||
```
|
||||
根据该配置,401-409之间的状态码将返回自定义应答如下:
|
||||
|
||||
```text
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
key1: value1
|
||||
key2: value2
|
||||
Content-Length: 21
|
||||
|
||||
{"hello":"world 40x"}
|
||||
```
|
||||
|
||||
### 老版本-不同状态码相同应答场景
|
||||
|
||||
```yaml
|
||||
enable_on_status:
|
||||
- 200
|
||||
status_code: 200
|
||||
headers:
|
||||
- Content-Type=application/json
|
||||
- Hello=World
|
||||
body: "{\"hello\":\"world\"}"
|
||||
```
|
||||
根据该配置,200请求将返回自定义应答如下:
|
||||
|
||||
```text
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
key1: value1
|
||||
key2: value2
|
||||
Content-Length: 21
|
||||
|
||||
{"hello":"world"}
|
||||
```
|
||||
|
||||
@@ -12,30 +12,165 @@ Plugin Execution Phase: `Authentication Phase`
|
||||
Plugin Execution Priority: `910`
|
||||
|
||||
## Configuration Fields
|
||||
| Name | Data Type | Requirements | Default Value | Description |
|
||||
| -------- | -------- | -------- | -------- | -------- |
|
||||
| status_code | number | Optional | 200 | Custom HTTP response status code |
|
||||
| headers | array of string | Optional | - | Custom HTTP response headers, keys and values separated by `=` |
|
||||
| body | string | Optional | - | Custom HTTP response body |
|
||||
| enable_on_status | array of number | Optional | - | Match original status codes to generate custom responses; if not specified, the original status code is not checked |
|
||||
### New version - Supports multiple returns
|
||||
| Name | Data Type | Requirements | Default Value | Description |
|
||||
|---------------------|-----------------|----------|-----|------------|
|
||||
| rules | array of object | Required | - | rule array |
|
||||
|
||||
The configuration field description of `rules` is as follows:
|
||||
|
||||
| Name | Data Type | Requirements | Default Value | Description |
|
||||
|--------------------|---------------------------|--------------|-----|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `status_code` | number | Optional | 200 | Custom HTTP response status code |
|
||||
| `headers` | array of string | Optional | - | Custom HTTP response headers, keys and values separated by `=` |
|
||||
| `body` | string | Optional | - | Custom HTTP response body |
|
||||
| `enable_on_status` | array of string or number | Optional | - | Match the original status code to generate a custom response. You can fill in the exact value such as :`200`,`404`, etc., you can also fuzzy match such as: `2xx` to match the status code between 200-299, `20x` to match the status code between 200-209, x represents any digit. If enable_on_status is not specified, the original status code is not determined and the first rule with ENABLE_ON_status left blank is used as the default rule |
|
||||
|
||||
#### Fuzzy matching rule
|
||||
* Length is 3
|
||||
* At least one digit
|
||||
* At least one x(case insensitive)
|
||||
|
||||
| rule | Matching content |
|
||||
|------|------------------------------------------------------------------------------------------|
|
||||
| 40x | 400-409; If the first two digits are 40 |
|
||||
| 1x4 | 104,114,124,134,144,154,164,174,184,194;The first and third positions are 1 and 4 respectively |
|
||||
| x23 | 023,123,223,323,423,523,623,723,823,923;The second and third positions are 23 |
|
||||
| 4xx | 400-499;The first digit is 4 |
|
||||
| x4x | 040-049,140-149,240-249,340-349,440-449,540-549,640-649,740-749,840-849,940-949;The second digit is 4 |
|
||||
| xx4 | When the mantissa is 4 |
|
||||
|
||||
Matching priority: Exact Match > Fuzzy Match > Default configuration (the first enable_on_status parameter is null)
|
||||
|
||||
## Old version - Only one return is supported
|
||||
| Name | Data Type | Requirements | Default Value | Description |
|
||||
| -------- | -------- | -------- | -------- |----------------------------------------------------------------------------------------------------|
|
||||
| `status_code` | number | Optional | 200 | Custom HTTP response status code |
|
||||
| `headers` | array of string | Optional | - | Custom HTTP response headers, keys and values separated by `=` |
|
||||
| `body` | string | Optional | - | Custom HTTP response body |
|
||||
| `enable_on_status` | array of number | Optional | - | Match original status codes to generate custom responses; if not specified, the original status code is not checked |
|
||||
|
||||
|
||||
## Configuration Example
|
||||
### Mock Response Scenario
|
||||
|
||||
### Different status codes for different response scenarios
|
||||
|
||||
```yaml
|
||||
status_code: 200
|
||||
headers:
|
||||
- Content-Type=application/json
|
||||
- Hello=World
|
||||
body: "{\"hello\":\"world\"}"
|
||||
rules:
|
||||
- body: '{"hello":"world 200"}'
|
||||
enable_on_status:
|
||||
- '200'
|
||||
- '201'
|
||||
headers:
|
||||
- key1=value1
|
||||
- key2=value2
|
||||
status_code: 200
|
||||
- body: '{"hello":"world 404"}'
|
||||
enable_on_status:
|
||||
- '404'
|
||||
headers:
|
||||
- key1=value1
|
||||
- key2=value2
|
||||
status_code: 200
|
||||
```
|
||||
With this configuration, the request will return the following custom response:
|
||||
|
||||
According to this configuration 200 201 requests will return a custom response as follows:
|
||||
|
||||
```text
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
Hello: World
|
||||
Content-Length: 17
|
||||
key1: value1
|
||||
key2: value2
|
||||
Content-Length: 21
|
||||
|
||||
{"hello":"world 200"}
|
||||
```
|
||||
According to this configuration 404 requests will return a custom response as follows:
|
||||
|
||||
```text
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
key1: value1
|
||||
key2: value2
|
||||
Content-Length: 21
|
||||
|
||||
{"hello":"world 400"}
|
||||
```
|
||||
With this configuration, 404 response will return the following custom response:
|
||||
```text
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
key1: value1
|
||||
key2: value2
|
||||
Content-Length: 21
|
||||
|
||||
{"hello":"world 404"}
|
||||
```
|
||||
|
||||
### Fuzzy matching scene
|
||||
|
||||
```yaml
|
||||
rules:
|
||||
- body: '{"hello":"world 200"}'
|
||||
enable_on_status:
|
||||
- 200
|
||||
headers:
|
||||
- key1=value1
|
||||
- key2=value2
|
||||
status_code: 200
|
||||
- body: '{"hello":"world 40x"}'
|
||||
enable_on_status:
|
||||
- '40x'
|
||||
headers:
|
||||
- key1=value1
|
||||
- key2=value2
|
||||
status_code: 200
|
||||
```
|
||||
|
||||
According to this configuration, the status 200 will return a custom reply as follows:
|
||||
|
||||
```text
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
key1: value1
|
||||
key2: value2
|
||||
Content-Length: 21
|
||||
|
||||
{"hello":"world 200"}
|
||||
```
|
||||
According to this configuration, the status code between 401-409 will return a custom reply as follows:
|
||||
|
||||
```text
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
key1: value1
|
||||
key2: value2
|
||||
Content-Length: 21
|
||||
|
||||
{"hello":"world 40x"}
|
||||
```
|
||||
|
||||
### Mock Response Scenario
|
||||
```yaml
|
||||
enable_on_status:
|
||||
- 200
|
||||
status_code: 200
|
||||
headers:
|
||||
- Content-Type=application/json
|
||||
- Hello=World
|
||||
body: "{\"hello\":\"world\"}"
|
||||
```
|
||||
With this configuration, 200/201 response will return the following custom response:
|
||||
```text
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
key1: value1
|
||||
key2: value2
|
||||
Content-Length: 21
|
||||
|
||||
{"hello":"world"}
|
||||
```
|
||||
|
||||
### Custom Response on Rate Limiting
|
||||
```yaml
|
||||
enable_on_status:
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.0.0
|
||||
1.1.0
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
services:
|
||||
envoy:
|
||||
image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/gateway:v2.0.7
|
||||
entrypoint: /usr/local/bin/envoy
|
||||
# 注意这里对 Wasm 开启了 debug 级别日志,在生产环境部署时请使用默认的 info 级别
|
||||
# 如果需要将 Envoy 的日志级别调整为 debug,将 --log-level 参数设置为 debug
|
||||
command: -c /etc/envoy/envoy.yaml --log-level info --component-log-level wasm:debug
|
||||
depends_on:
|
||||
- echo-server
|
||||
networks:
|
||||
- wasmtest
|
||||
ports:
|
||||
- "10000:10000"
|
||||
- "9901:9901"
|
||||
volumes:
|
||||
- ./envoy.yaml:/etc/envoy/envoy.yaml
|
||||
- ./plugin.wasm:/etc/envoy/plugin.wasm
|
||||
echo-server:
|
||||
image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/echo-server:1.3.0
|
||||
networks:
|
||||
- wasmtest
|
||||
ports:
|
||||
- "3000:3000"
|
||||
networks:
|
||||
wasmtest: {}
|
||||
@@ -1,9 +1,3 @@
|
||||
admin:
|
||||
address:
|
||||
socket_address:
|
||||
protocol: TCP
|
||||
address: 0.0.0.0
|
||||
port_value: 9901
|
||||
static_resources:
|
||||
listeners:
|
||||
- name: listener_0
|
||||
@@ -27,9 +21,9 @@ static_resources:
|
||||
domains: ["*"]
|
||||
routes:
|
||||
- match:
|
||||
prefix: "/"
|
||||
prefix: "/echo"
|
||||
route:
|
||||
cluster: httpbin
|
||||
cluster: echo-server
|
||||
http_filters:
|
||||
- name: wasmdemo
|
||||
typed_config:
|
||||
@@ -42,30 +36,100 @@ static_resources:
|
||||
runtime: envoy.wasm.runtime.v8
|
||||
code:
|
||||
local:
|
||||
filename: /etc/envoy/main.wasm
|
||||
filename: /etc/envoy/plugin.wasm
|
||||
configuration:
|
||||
"@type": "type.googleapis.com/google.protobuf.StringValue"
|
||||
value: |
|
||||
# value: |-
|
||||
# {
|
||||
# "rules": [
|
||||
# {
|
||||
# "headers": [
|
||||
# "key1=value1",
|
||||
# "key2=value2"
|
||||
# ],
|
||||
# "status_code": 200,
|
||||
# "enable_on_status": [
|
||||
# 200,
|
||||
# 201
|
||||
# ],
|
||||
# "body": "{\"hello\":\"world 200\"}"
|
||||
# },
|
||||
# {
|
||||
# "headers": [
|
||||
# "key1=value1",
|
||||
# "key2=value2"
|
||||
# ],
|
||||
# "status_code": 200,
|
||||
# "enable_on_status": [
|
||||
# 404
|
||||
# ],
|
||||
# "body": "{\"hello\":\"world 404\"}"
|
||||
# }
|
||||
# ]
|
||||
# }
|
||||
value: |-
|
||||
{
|
||||
"headers": ["key1=value1", "key2=value2"],
|
||||
"status_code": 200,
|
||||
"enable_on_status": [200, 201],
|
||||
"body": "{\"hello\":\"world\"}"
|
||||
"rules": [
|
||||
{
|
||||
"headers": [
|
||||
"key1=value1",
|
||||
"key2=value2"
|
||||
],
|
||||
"status_code": 200,
|
||||
"enable_on_status": [
|
||||
200
|
||||
],
|
||||
"body": "{\"hello\":\"world 200\"}"
|
||||
},
|
||||
{
|
||||
"headers": [
|
||||
"key1=value1",
|
||||
"key2=value2"
|
||||
],
|
||||
"status_code": 200,
|
||||
"enable_on_status": [
|
||||
"40x"
|
||||
],
|
||||
"body": "{\"hello\":\"world 40x\"}"
|
||||
}
|
||||
]
|
||||
}
|
||||
# value: |-
|
||||
# {
|
||||
# "headers": [
|
||||
# "key1=value1",
|
||||
# "key2=value2"
|
||||
# ],
|
||||
# "status_code": 200,
|
||||
# "body": "{\"hello\":\"world 200\"}",
|
||||
# "enable_on_status": [
|
||||
# 200
|
||||
# ]
|
||||
# }
|
||||
# value: |-
|
||||
# {
|
||||
# "headers": [
|
||||
# "key1=value1",
|
||||
# "key2=value2"
|
||||
# ],
|
||||
# "status_code": 200,
|
||||
# "body": "{\"hello\":\"world 200\"}"
|
||||
# }
|
||||
- name: envoy.filters.http.router
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
|
||||
clusters:
|
||||
- name: httpbin
|
||||
- name: echo-server
|
||||
connect_timeout: 30s
|
||||
type: LOGICAL_DNS
|
||||
# Comment out the following line to test on v6 networks
|
||||
dns_lookup_family: V4_ONLY
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: httpbin
|
||||
cluster_name: echo-server
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: httpbin
|
||||
port_value: 80
|
||||
address: echo-server
|
||||
port_value: 3000
|
||||
@@ -16,10 +16,12 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/log"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
@@ -29,78 +31,160 @@ import (
|
||||
func main() {
|
||||
wrapper.SetCtx(
|
||||
"custom-response",
|
||||
wrapper.ParseConfigBy(parseConfig),
|
||||
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
|
||||
wrapper.ProcessResponseHeadersBy(onHttpResponseHeaders),
|
||||
wrapper.ParseConfig(parseConfig),
|
||||
wrapper.ProcessRequestHeaders(onHttpRequestHeaders),
|
||||
wrapper.ProcessResponseHeaders(onHttpResponseHeaders),
|
||||
)
|
||||
}
|
||||
|
||||
type CustomResponseConfig struct {
|
||||
rules []CustomResponseRule
|
||||
defaultRule *CustomResponseRule
|
||||
enableOnStatusRuleMap map[string]*CustomResponseRule
|
||||
}
|
||||
|
||||
type CustomResponseRule struct {
|
||||
statusCode uint32
|
||||
headers [][2]string
|
||||
body string
|
||||
enableOnStatus []uint32
|
||||
enableOnStatus []string
|
||||
contentType string
|
||||
}
|
||||
|
||||
func parseConfig(gjson gjson.Result, config *CustomResponseConfig, log wrapper.Log) error {
|
||||
func parseConfig(gjson gjson.Result, config *CustomResponseConfig) error {
|
||||
rules := gjson.Get("rules")
|
||||
rulesVersion := rules.Exists() && rules.IsArray()
|
||||
if rulesVersion {
|
||||
for _, cf := range gjson.Get("rules").Array() {
|
||||
item := new(CustomResponseRule)
|
||||
if err := parseRuleItem(cf, item); err != nil {
|
||||
return err
|
||||
}
|
||||
// the first rule item which enableOnStatus is empty to be set default
|
||||
if len(item.enableOnStatus) == 0 && config.defaultRule == nil {
|
||||
config.defaultRule = item
|
||||
}
|
||||
config.rules = append(config.rules, *item)
|
||||
}
|
||||
} else {
|
||||
rule := new(CustomResponseRule)
|
||||
if err := parseRuleItem(gjson, rule); err != nil {
|
||||
return err
|
||||
}
|
||||
config.rules = append(config.rules, *rule)
|
||||
config.defaultRule = rule
|
||||
}
|
||||
config.enableOnStatusRuleMap = make(map[string]*CustomResponseRule)
|
||||
for i, configItem := range config.rules {
|
||||
for _, statusCode := range configItem.enableOnStatus {
|
||||
if v, ok := config.enableOnStatusRuleMap[statusCode]; ok {
|
||||
log.Errorf("enable_on_status code used in %v, want to add %v", v, statusCode)
|
||||
return errors.New("enableOnStatus can only use once")
|
||||
}
|
||||
config.enableOnStatusRuleMap[statusCode] = &config.rules[i]
|
||||
}
|
||||
}
|
||||
if rulesVersion && config.defaultRule == nil && len(config.enableOnStatusRuleMap) == 0 {
|
||||
return errors.New("no valid config is found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseRuleItem(gjson gjson.Result, rule *CustomResponseRule) error {
|
||||
headersArray := gjson.Get("headers").Array()
|
||||
config.headers = make([][2]string, 0, len(headersArray))
|
||||
rule.headers = make([][2]string, 0, len(headersArray))
|
||||
for _, v := range headersArray {
|
||||
kv := strings.SplitN(v.String(), "=", 2)
|
||||
if len(kv) == 2 {
|
||||
key := strings.TrimSpace(kv[0])
|
||||
value := strings.TrimSpace(kv[1])
|
||||
if strings.EqualFold(key, "content-type") {
|
||||
config.contentType = value
|
||||
rule.contentType = value
|
||||
} else if strings.EqualFold(key, "content-length") {
|
||||
continue
|
||||
} else {
|
||||
config.headers = append(config.headers, [2]string{key, value})
|
||||
rule.headers = append(rule.headers, [2]string{key, value})
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("invalid header pair format: %s", v.String())
|
||||
}
|
||||
}
|
||||
|
||||
config.body = gjson.Get("body").String()
|
||||
if config.contentType == "" && config.body != "" {
|
||||
if json.Valid([]byte(config.body)) {
|
||||
config.contentType = "application/json; charset=utf-8"
|
||||
rule.body = gjson.Get("body").String()
|
||||
if rule.contentType == "" && rule.body != "" {
|
||||
if json.Valid([]byte(rule.body)) {
|
||||
rule.contentType = "application/json; charset=utf-8"
|
||||
} else {
|
||||
config.contentType = "text/plain; charset=utf-8"
|
||||
rule.contentType = "text/plain; charset=utf-8"
|
||||
}
|
||||
}
|
||||
config.headers = append(config.headers, [2]string{"content-type", config.contentType})
|
||||
rule.headers = append(rule.headers, [2]string{"content-type", rule.contentType})
|
||||
|
||||
config.statusCode = 200
|
||||
rule.statusCode = 200
|
||||
if gjson.Get("status_code").Exists() {
|
||||
statusCode := gjson.Get("status_code")
|
||||
parsedStatusCode, err := strconv.Atoi(statusCode.String())
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid status code value: %s", statusCode.String())
|
||||
}
|
||||
config.statusCode = uint32(parsedStatusCode)
|
||||
rule.statusCode = uint32(parsedStatusCode)
|
||||
}
|
||||
|
||||
enableOnStatusArray := gjson.Get("enable_on_status").Array()
|
||||
config.enableOnStatus = make([]uint32, 0, len(enableOnStatusArray))
|
||||
rule.enableOnStatus = make([]string, 0, len(enableOnStatusArray))
|
||||
for _, v := range enableOnStatusArray {
|
||||
parsedEnableOnStatus, err := strconv.Atoi(v.String())
|
||||
s := v.String()
|
||||
_, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid enable_on_status value: %s", v.String())
|
||||
matchString, err := isValidFuzzyMatchString(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rule.enableOnStatus = append(rule.enableOnStatus, matchString)
|
||||
continue
|
||||
}
|
||||
config.enableOnStatus = append(config.enableOnStatus, uint32(parsedEnableOnStatus))
|
||||
rule.enableOnStatus = append(rule.enableOnStatus, s)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config CustomResponseConfig, log wrapper.Log) types.Action {
|
||||
if len(config.enableOnStatus) != 0 {
|
||||
func isValidFuzzyMatchString(s string) (string, error) {
|
||||
const requiredLength = 3
|
||||
if len(s) != requiredLength {
|
||||
return "", fmt.Errorf("invalid enable_on_status %q: length must be %d", s, requiredLength)
|
||||
}
|
||||
|
||||
lower := strings.ToLower(s)
|
||||
hasX := false
|
||||
hasDigit := false
|
||||
|
||||
for _, c := range lower {
|
||||
switch {
|
||||
case c == 'x':
|
||||
hasX = true
|
||||
case c >= '0' && c <= '9':
|
||||
hasDigit = true
|
||||
default:
|
||||
return "", fmt.Errorf("invalid enable_on_status %q: must contain only digits and x/X", s)
|
||||
}
|
||||
}
|
||||
|
||||
if !hasX {
|
||||
return "", fmt.Errorf("invalid enable_on_status %q: fuzzy match must contain x/X (use enable_on_status for exact statusCode matching)", s)
|
||||
}
|
||||
if !hasDigit {
|
||||
return "", fmt.Errorf("invalid enable_on_status %q: must contain at least one digit", s)
|
||||
}
|
||||
|
||||
return lower, nil
|
||||
}
|
||||
|
||||
func onHttpRequestHeaders(_ wrapper.HttpContext, config CustomResponseConfig) types.Action {
|
||||
if len(config.enableOnStatusRuleMap) != 0 {
|
||||
return types.ActionContinue
|
||||
}
|
||||
err := proxywasm.SendHttpResponseWithDetail(config.statusCode, "custom-response", config.headers, []byte(config.body), -1)
|
||||
log.Infof("use default rule %+v", config.defaultRule)
|
||||
err := proxywasm.SendHttpResponseWithDetail(config.defaultRule.statusCode, "custom-response", config.defaultRule.headers, []byte(config.defaultRule.body), -1)
|
||||
if err != nil {
|
||||
log.Errorf("send http response failed: %v", err)
|
||||
}
|
||||
@@ -108,28 +192,62 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, config CustomResponseConfig,
|
||||
return types.ActionPause
|
||||
}
|
||||
|
||||
func onHttpResponseHeaders(ctx wrapper.HttpContext, config CustomResponseConfig, log wrapper.Log) types.Action {
|
||||
// enableOnStatus is not empty, compare the status code.
|
||||
func onHttpResponseHeaders(_ wrapper.HttpContext, config CustomResponseConfig) types.Action {
|
||||
// enableOnStatusRuleMap is not empty, compare the status code.
|
||||
// if match the status code, mock the response.
|
||||
statusCodeStr, err := proxywasm.GetHttpResponseHeader(":status")
|
||||
if err != nil {
|
||||
log.Errorf("get http response status code failed: %v", err)
|
||||
return types.ActionContinue
|
||||
}
|
||||
statusCode, err := strconv.ParseUint(statusCodeStr, 10, 32)
|
||||
if err != nil {
|
||||
log.Errorf("parse http response status code failed: %v", err)
|
||||
if rule, ok := config.enableOnStatusRuleMap[statusCodeStr]; ok {
|
||||
err = proxywasm.SendHttpResponseWithDetail(rule.statusCode, "custom-response", rule.headers, []byte(rule.body), -1)
|
||||
if err != nil {
|
||||
log.Errorf("send http response failed: %v", err)
|
||||
}
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
for _, v := range config.enableOnStatus {
|
||||
if uint32(statusCode) == v {
|
||||
err = proxywasm.SendHttpResponseWithDetail(config.statusCode, "custom-response", config.headers, []byte(config.body), -1)
|
||||
if err != nil {
|
||||
log.Errorf("send http response failed: %v", err)
|
||||
}
|
||||
if rule, match := fuzzyMatchCode(config.enableOnStatusRuleMap, statusCodeStr); match {
|
||||
err = proxywasm.SendHttpResponseWithDetail(rule.statusCode, "custom-response", rule.headers, []byte(rule.body), -1)
|
||||
if err != nil {
|
||||
log.Errorf("send http response failed: %v", err)
|
||||
}
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
func fuzzyMatchCode(statusRuleMap map[string]*CustomResponseRule, statusCode string) (*CustomResponseRule, bool) {
|
||||
if len(statusRuleMap) == 0 || statusCode == "" {
|
||||
return nil, false
|
||||
}
|
||||
codeLen := len(statusCode)
|
||||
for pattern, rule := range statusRuleMap {
|
||||
// 规则1:模式长度必须与状态码一致
|
||||
if len(pattern) != codeLen {
|
||||
continue
|
||||
}
|
||||
// 纯数字的enableOnStatus已经判断过,跳过
|
||||
if !strings.Contains(pattern, "x") {
|
||||
continue
|
||||
}
|
||||
// 规则2:所有数字位必须精确匹配
|
||||
match := true
|
||||
for i, c := range pattern {
|
||||
// 如果是数字位需要校验
|
||||
if c >= '0' && c <= '9' {
|
||||
// 边界检查防止panic
|
||||
if i >= codeLen || statusCode[i] != byte(c) {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
// 非数字位(如x)自动匹配
|
||||
}
|
||||
if match {
|
||||
return rule, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
80
plugins/wasm-go/extensions/custom-response/main_test.go
Normal file
80
plugins/wasm-go/extensions/custom-response/main_test.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_prefixMatchCode(t *testing.T) {
|
||||
rules := map[string]*CustomResponseRule{
|
||||
"x01": {},
|
||||
"2x3": {},
|
||||
"45x": {},
|
||||
"6xx": {},
|
||||
"x7x": {},
|
||||
"xx8": {},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
code string
|
||||
expectHit bool
|
||||
}{
|
||||
{"101", true}, // 匹配x01
|
||||
{"201", true}, // 匹配x01
|
||||
{"111", false}, // 不匹配
|
||||
{"203", true}, // 匹配2x3
|
||||
{"213", true}, // 匹配2x3
|
||||
{"450", true}, // 匹配45x
|
||||
{"451", true}, // 匹配45x
|
||||
{"600", true}, // 匹配6xx
|
||||
{"611", true}, // 匹配6xx
|
||||
{"612", true}, // 匹配6xx
|
||||
{"171", true}, // 匹配x7x
|
||||
{"161", false}, // 不匹配
|
||||
{"228", true}, // 匹配xx8
|
||||
{"229", false}, // 不匹配
|
||||
{"123", false}, // 不匹配
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
_, found := fuzzyMatchCode(rules, tt.code)
|
||||
if found != tt.expectHit {
|
||||
t.Errorf("code:%s expect:%v got:%v", tt.code, tt.expectHit, found)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsValidPrefixString(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
hasError bool
|
||||
}{
|
||||
{"x1x", "x1x", false},
|
||||
{"X2X", "x2x", false},
|
||||
{"xx1", "xx1", false},
|
||||
{"x12", "x12", false},
|
||||
{"1x2", "1x2", false},
|
||||
{"12x", "12x", false},
|
||||
{"123", "", true}, // 缺少x
|
||||
{"xxx", "", true}, // 缺少数字
|
||||
{"xYx", "", true}, // 非法字符
|
||||
{"x1", "", true}, // 长度不足
|
||||
{"x123", "", true}, // 长度超限
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result, err := isValidFuzzyMatchString(tt.input)
|
||||
if tt.hasError {
|
||||
if err == nil {
|
||||
t.Errorf("%q: expected error but got none", tt.input)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("%q: unexpected error: %v", tt.input, err)
|
||||
}
|
||||
if result != tt.expected {
|
||||
t.Errorf("%q: expected %q, got %q", tt.input, tt.expected, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
plugins/wasm-go/extensions/frontend-gray/Makefile
Normal file
11
plugins/wasm-go/extensions/frontend-gray/Makefile
Normal file
@@ -0,0 +1,11 @@
|
||||
.PHONY: reload
|
||||
|
||||
build:
|
||||
tinygo build -o main.wasm -scheduler=none -target=wasi -gc=custom -tags='custommalloc nottinygc_finalizer' ./main.go
|
||||
|
||||
reload:
|
||||
tinygo build -o main.wasm -scheduler=none -target=wasi -gc=custom -tags='custommalloc nottinygc_finalizer' ./main.go
|
||||
./envoy -c envoy.yaml --concurrency 0 --log-level info --component-log-level wasm:debug
|
||||
|
||||
start:
|
||||
./envoy -c envoy.yaml --concurrency 0 --log-level info --component-log-level wasm:debug
|
||||
@@ -9,8 +9,8 @@ description: 前端灰度插件配置参考
|
||||
|
||||
## 运行属性
|
||||
|
||||
插件执行阶段:`认证阶段`
|
||||
插件执行优先级:`450`
|
||||
插件执行阶段:`默认阶段`
|
||||
插件执行优先级:`1000`
|
||||
|
||||
|
||||
## 配置字段
|
||||
@@ -19,16 +19,17 @@ description: 前端灰度插件配置参考
|
||||
| `grayKey` | string | 非必填 | - | 用户ID的唯一标识,可以来自Cookie或者Header中,比如 userid,如果没有填写则使用`rules[].grayTagKey`和`rules[].grayTagValue`过滤灰度规则 |
|
||||
| `localStorageGrayKey` | string | 非必填 | - | 使用JWT鉴权方式,用户ID的唯一标识来自`localStorage`中,如果配置了当前参数,则`grayKey`失效 |
|
||||
| `graySubKey` | string | 非必填 | - | 用户身份信息可能以JSON形式透出,比如:`userInfo:{ userCode:"001" }`,当前例子`graySubKey`取值为`userCode` |
|
||||
| `userStickyMaxAge` | int | 非必填 | 172800 | 用户粘滞的时长:单位为秒,默认为`172800`,2天时间 |
|
||||
| `includePathPrefixes` | array of strings | 非必填 | - | 强制处理的路径。例如,在 微前端 场景下,XHR 接口如: `/resource/xxx`本质是一个资源请求,需要走插件转发逻辑。 |
|
||||
| `skippedPathPrefixes` | array of strings | 非必填 | - | 用于排除特定路径,避免当前插件处理这些请求。例如,在 rewrite 场景下,XHR 接口请求 `/api/xxx` 如果经过插件转发逻辑,可能会导致非预期的结果。 |
|
||||
| `storeMaxAge` | int | 非必填 | 60 * 60 * 24 * 365 | 网关设置Cookie最大存储时长:单位为秒,默认为1年 |
|
||||
| `indexPaths` | array of strings | 非必填 | - | 强制处理的路径,支持 `Glob` 模式匹配。例如:在 微前端场景下,XHR 接口如: `/resource/**/manifest-main.json`本质是一个资源请求,需要走插件转发逻辑。 |
|
||||
| `skippedPaths` | array of strings | 非必填 | - | 用于排除特定路径,避免当前插件处理这些请求,支持 `Glob` 模式匹配。例如,在 rewrite 场景下,XHR 接口请求 `/api/**` 如果经过插件转发逻辑,可能会导致非预期的结果。 |
|
||||
| `skippedByHeaders` | map of string to string | 非必填 | - | 用于通过请求头过滤,指定哪些请求不被当前插件
|
||||
处理。`skippedPathPrefixes` 的优先级高于当前配置,且页面HTML请求不受本配置的影响。若本配置为空,默认会判断`sec-fetch-mode=cors`以及`upgrade=websocket`两个header头,进行过滤 |
|
||||
处理。`skippedPaths` 的优先级高于当前配置,且页面HTML请求不受本配置的影响。 |
|
||||
| `rules` | array of object | 必填 | - | 用户定义不同的灰度规则,适配不同的灰度场景 |
|
||||
| `rewrite` | object | 必填 | - | 重写配置,一般用于OSS/CDN前端部署的重写配置 |
|
||||
| `baseDeployment` | object | 非必填 | - | 配置Base基线规则的配置 |
|
||||
| `grayDeployments` | array of object | 非必填 | - | 配置Gray灰度的生效规则,以及生效版本 |
|
||||
| `backendGrayTag` | string | 非必填 | `x-mse-tag` | 后端灰度版本Tag,如果配置了,cookie中将携带值为`${backendGrayTag}:${grayDeployments[].backendVersion}` |
|
||||
| `uniqueGrayTag` | string | 非必填 | `x-higress-uid` | 开启按照比例灰度时候,网关会生成一个唯一标识存在`cookie`中,一方面用于session黏贴,另一方面后端也可以使用这个值用于全链路的灰度串联 |
|
||||
| `injection` | object | 非必填 | - | 往首页HTML中注入全局信息,比如`<script>window.global = {...}</script>` |
|
||||
|
||||
|
||||
@@ -50,7 +51,6 @@ description: 前端灰度插件配置参考
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|------------|--------------|------|-----|------------------------------|
|
||||
| `host` | string | 非必填 | - | host地址,如果是OSS则设置为 VPC 内网访问地址 |
|
||||
| `notFoundUri` | string | 非必填 | - | 404 页面配置 |
|
||||
| `indexRouting` | map of string to string | 非必填 | - | 用于定义首页重写路由规则。每个键 (Key) 表示首页的路由路径,值 (Value) 则指向重定向的目标文件。例如,键为 `/app1` 对应的值为 `/mfe/app1/{version}/index.html`。生效version为`0.0.1`, 访问路径为 `/app1`,则重定向到 `/mfe/app1/0.0.1/index.html`。 |
|
||||
| `fileRouting` | map of string to string | 非必填 | - | 用于定义资源文件重写路由规则。每个键 (Key) 表示资源访问路径,值 (Value) 则指向重定向的目标文件。例如,键为 `/app1/` 对应的值为 `/mfe/app1/{version}`。生效version为`0.0.1`,访问路径为 `/app1/js/a.js`,则重定向到 `/mfe/app1/0.0.1/js/a.js`。 |
|
||||
|
||||
@@ -59,6 +59,7 @@ description: 前端灰度插件配置参考
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|----------------|--------------|------|-----|-----------------------------------------------------------------------------------|
|
||||
| `version` | string | 必填 | - | Base版本的版本号,作为兜底的版本 |
|
||||
| `backendVersion` | string | 必填 | - | 后端灰度版本,配合`key`为`${backendGrayTag}`,写入cookie中 |
|
||||
| `versionPredicates` | string | 必填 | - | 和`version`含义相同,但是满足多版本的需求:根据不同路由映射不同的`Version`版本。一般用于微前端的场景:一个主应用需要管理多个微应用 |
|
||||
|
||||
`grayDeployments`字段配置说明:
|
||||
@@ -70,17 +71,28 @@ description: 前端灰度插件配置参考
|
||||
| `backendVersion` | string | 必填 | - | 后端灰度版本,配合`key`为`${backendGrayTag}`,写入cookie中 |
|
||||
| `name` | string | 必填 | - | 规则名称和`rules[].name`关联 |
|
||||
| `enabled` | boolean | 必填 | - | 是否启动当前灰度规则 |
|
||||
| `weight` | int | 非必填 | - | 按照比例灰度,比如`50`。注意:灰度规则权重总和不能超过100,如果同时配置了`grayKey`以及`grayDeployments[0].weight`按照比例灰度优先生效 |
|
||||
> 为了实现按比例(weight) 进行灰度发布,并确保用户粘滞,我们需要确认客户端的唯一性。如果配置了 grayKey,则将其用作唯一标识;如果未配置 grayKey,则使用客户端的访问 IP 地址作为唯一标识。
|
||||
| `weight` | int | 非必填 | - | 按照比例灰度,比如50。 |
|
||||
>按照比例灰度注意下面几点:
|
||||
> 1. 如果同时配置了`按用户灰度`以及`按比例灰度`,按`比例灰度`优先生效
|
||||
> 2. 采用客户端设备标识符的哈希摘要机制实现流量比例控制,其唯一性判定逻辑遵循以下原则:自动生成全局唯一标识符(UUID)作为设备指纹,可以通过`uniqueGrayTag`配置`cookie`的key值,并通过SHA-256哈希算法生成对应灰度判定基准值。
|
||||
|
||||
|
||||
`injection`字段配置说明:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|--------|--------|------|-----|-------------------------------------------------|
|
||||
| `globalConfig` | object | 非必填 | - | 注入到HTML首页的全局变量 |
|
||||
| `head` | array of string | 非必填 | - | 注入head信息,比如`<link rel="stylesheet" href="https://cdn.example.com/styles.css">` |
|
||||
| `body` | object | 非必填 | - | 注入Body |
|
||||
|
||||
`injection.globalConfig`字段配置说明:
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|--------|--------|------|-----|-------------------------------------------------|
|
||||
| `key` | string | 非必填 | HIGRESS_CONSOLE_CONFIG | 注入到window全局变量的key值 |
|
||||
| `featureKey` | string | 非必填 | FEATURE_STATUS | 关于`rules`相关规则的命中情况,返回实例`{"beta-user":true,"inner-user":false}` |
|
||||
| `value` | string | 非必填 | - | 自定义的全局变量 |
|
||||
| `enabled` | boolean | 非必填 | false | 是否开启注入全局变量 |
|
||||
|
||||
`injection.body`字段配置说明:
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|--------|--------|------|-----|-------------------------------------------------|
|
||||
@@ -139,8 +151,7 @@ grayDeployments:
|
||||
enabled: true
|
||||
weight: 80
|
||||
```
|
||||
总的灰度规则为100%,其中灰度版本的权重为`80%`,基线版本为`20%`。一旦用户命中了灰度规则,会根据IP固定这个用户的灰度版本(否则会在下次请求时随机选择一个灰度版本)。
|
||||
|
||||
总的灰度规则为100%,其中灰度版本的权重为80%,基线版本为20%。
|
||||
### 用户信息存在JSON中
|
||||
|
||||
```yml
|
||||
@@ -218,7 +229,6 @@ rules:
|
||||
- level5
|
||||
rewrite:
|
||||
host: frontend-gray.oss-cn-shanghai-internal.aliyuncs.com
|
||||
notFoundUri: /mfe/app1/dev/404.html
|
||||
indexRouting:
|
||||
/app1: '/mfe/app1/{version}/index.html'
|
||||
/: '/mfe/app1/{version}/index.html',
|
||||
@@ -260,7 +270,6 @@ grayDeployments:
|
||||
- name: beta-user
|
||||
version: gray
|
||||
enabled: true
|
||||
weight: 80
|
||||
injection:
|
||||
head:
|
||||
- <script>console.log('Header')</script>
|
||||
|
||||
@@ -1,59 +1,95 @@
|
||||
---
|
||||
title: Frontend Gray
|
||||
keywords: [higress, frontend gray]
|
||||
description: Frontend gray plugin configuration reference
|
||||
---
|
||||
## Function Description
|
||||
The `frontend-gray` plugin implements the functionality of user gray release on the frontend. Through this plugin, it can be used for business `A/B testing`, while the `gradual release` combined with `monitorable` and `rollback` strategies ensures the stability of system release operations.
|
||||
description: Frontend Gray Plugin Configuration Reference
|
||||
|
||||
## Runtime Attributes
|
||||
Plugin execution phase: `Authentication Phase`
|
||||
Plugin execution priority: `450`
|
||||
## Feature Description
|
||||
The `frontend-gray` plugin implements frontend user grayscale capabilities. This plugin can be used for business `A/B testing` while ensuring system release stability through `grayscale`, `monitoring`, and `rollback` strategies.
|
||||
|
||||
## Runtime Properties
|
||||
|
||||
Execution Stage: `Default Stage`
|
||||
Execution Priority: `1000`
|
||||
|
||||
## Configuration Fields
|
||||
| Name | Data Type | Requirements | Default Value | Description |
|
||||
|-----------------|-------------------|---------------|---------------|-------------------------------------------------------------------------------------------------------------|
|
||||
| `grayKey` | string | Optional | - | The unique identifier of the user ID, which can be from Cookie or Header, such as userid. If not provided, uses `rules[].grayTagKey` and `rules[].grayTagValue` to filter gray release rules. |
|
||||
| `graySubKey` | string | Optional | - | User identity information may be output in JSON format, for example: `userInfo:{ userCode:"001" }`, in the current example, `graySubKey` is `userCode`. |
|
||||
| `rules` | array of object | Required | - | User-defined different gray release rules, adapted to different gray release scenarios. |
|
||||
| `rewrite` | object | Required | - | Rewrite configuration, generally used for OSS/CDN frontend deployment rewrite configurations. |
|
||||
| `baseDeployment`| object | Optional | - | Configuration of the Base baseline rules. |
|
||||
| `grayDeployments` | array of object | Optional | - | Configuration of the effective rules for gray release, as well as the effective versions. |
|
||||
| Name | Data Type | Required | Default | Description |
|
||||
|------|-----------|----------|---------|-------------|
|
||||
| `grayKey` | string | Optional | - | Unique user identifier from Cookie/Header (e.g., userid). If empty, uses `rules[].grayTagKey` and `rules[].grayTagValue` to filter rules. |
|
||||
| `localStorageGrayKey` | string | Optional | - | When using JWT authentication, user ID comes from `localStorage`. Overrides `grayKey` if configured. |
|
||||
| `graySubKey` | string | Optional | - | Used when user info is in JSON format (e.g., `userInfo:{ userCode:"001" }`). In this example, `graySubKey` would be `userCode`. |
|
||||
| `storeMaxAge` | int | Optional | 31536000 | Max cookie storage duration in seconds (default: 1 year). |
|
||||
| `indexPaths` | string[] | Optional | - | Paths requiring mandatory processing (supports Glob patterns). Example: `/resource/**/manifest-main.json` in micro-frontend scenarios. |
|
||||
| `skippedPaths` | string[] | Optional | - | Excluded paths (supports Glob patterns). Example: `/api/**` XHR requests in rewrite scenarios. |
|
||||
| `skippedByHeaders` | map<string, string> | Optional | - | Filter requests via headers. `skippedPaths` has higher priority. HTML page requests are unaffected. |
|
||||
| `rules` | object[] | Required | - | User-defined grayscale rules for different scenarios. |
|
||||
| `rewrite` | object | Required | - | Rewrite configuration for OSS/CDN deployments. |
|
||||
| `baseDeployment` | object | Optional | - | Baseline configuration. |
|
||||
| `grayDeployments` | object[] | Optional | - | Gray deployment rules and versions. |
|
||||
| `backendGrayTag` | string | Optional | `x-mse-tag` | Backend grayscale tag. Cookies will carry `${backendGrayTag}:${grayDeployments[].backendVersion}` if configured. |
|
||||
| `uniqueGrayTag` | string | Optional | `x-higress-uid` | UUID stored in cookies for percentage-based grayscale session stickiness and backend tracking. |
|
||||
| `injection` | object | Optional | - | Inject global info into HTML (e.g., `<script>window.global = {...}</script>`). |
|
||||
|
||||
`rules` field configuration description:
|
||||
| Name | Data Type | Requirements | Default Value | Description |
|
||||
|------------------|-------------------|---------------|---------------|--------------------------------------------------------------------------------------------|
|
||||
| `name` | string | Required | - | Unique identifier for the rule name, associated with `deploy.gray[].name` for effectiveness. |
|
||||
| `grayKeyValue` | array of string | Optional | - | Whitelist of user IDs. |
|
||||
| `grayTagKey` | string | Optional | - | Label key for user classification tagging, derived from Cookie. |
|
||||
| `grayTagValue` | array of string | Optional | - | Label value for user classification tagging, derived from Cookie. |
|
||||
### `rules` Field
|
||||
| Name | Data Type | Required | Default | Description |
|
||||
|------|-----------|----------|---------|-------------|
|
||||
| `name` | string | Required | - | Unique rule name linked to `grayDeployments[].name`. |
|
||||
| `grayKeyValue` | string[] | Optional | - | User ID whitelist. |
|
||||
| `grayTagKey` | string | Optional | - | User tag key from cookies. |
|
||||
| `grayTagValue` | string[] | Optional | - | User tag values from cookies. |
|
||||
|
||||
`rewrite` field configuration description:
|
||||
> `indexRouting` homepage rewrite and `fileRouting` file rewrite essentially use prefix matching, for example, `/app1`: `/mfe/app1/{version}/index.html` represents requests with the prefix /app1 routed to `/mfe/app1/{version}/index.html` page, where `{version}` represents the version number, which will be dynamically replaced by `baseDeployment.version` or `grayDeployments[].version` during execution.
|
||||
> `{version}` will be replaced dynamically during execution by the frontend version from `baseDeployment.version` or `grayDeployments[].version`.
|
||||
### `rewrite` Field
|
||||
> Both `indexRouting` and `fileRouting` use prefix matching. The `{version}` placeholder will be dynamically replaced by `baseDeployment.version` or `grayDeployments[].version`.
|
||||
|
||||
| Name | Data Type | Requirements | Default Value | Description |
|
||||
|------------------|-------------------|---------------|---------------|---------------------------------------|
|
||||
| `host` | string | Optional | - | Host address, if OSS set to the VPC internal access address. |
|
||||
| `notFoundUri` | string | Optional | - | 404 page configuration. |
|
||||
| `indexRouting` | map of string to string | Optional | - | Defines the homepage rewrite routing rules. Each key represents the homepage routing path, and the value points to the redirect target file. For example, the key `/app1` corresponds to the value `/mfe/app1/{version}/index.html`. If the effective version is `0.0.1`, the access path is `/app1`, it redirects to `/mfe/app1/0.0.1/index.html`. |
|
||||
| `fileRouting` | map of string to string | Optional | - | Defines resource file rewrite routing rules. Each key represents the resource access path, and the value points to the redirect target file. For example, the key `/app1/` corresponds to the value `/mfe/app1/{version}`. If the effective version is `0.0.1`, the access path is `/app1/js/a.js`, it redirects to `/mfe/app1/0.0.1/js/a.js`. |
|
||||
| Name | Data Type | Required | Default | Description |
|
||||
|------|-----------|----------|---------|-------------|
|
||||
| `host` | string | Optional | - | Host address (use VPC endpoint for OSS). |
|
||||
| `indexRouting` | map<string, string> | Optional | - | Homepage rewrite rules. Key: route path, Value: target file. Example: `/app1` → `/mfe/app1/{version}/index.html`. |
|
||||
| `fileRouting` | map<string, string> | Optional | - | Resource rewrite rules. Key: resource path, Value: target path. Example: `/app1/` → `/mfe/app1/{version}`. |
|
||||
|
||||
`baseDeployment` field configuration description:
|
||||
| Name | Data Type | Requirements | Default Value | Description |
|
||||
|------------------|-------------------|---------------|---------------|-------------------------------------------------------------------------------------------|
|
||||
| `version` | string | Required | - | The version number of the Base version, as a fallback version. |
|
||||
### `baseDeployment` Field
|
||||
| Name | Data Type | Required | Default | Description |
|
||||
|------|-----------|----------|---------|-------------|
|
||||
| `version` | string | Required | - | Baseline version as fallback. |
|
||||
| `backendVersion` | string | Required | - | Backend grayscale version written to cookies via `${backendGrayTag}`. |
|
||||
| `versionPredicates` | string | Required | - | Supports multi-version mapping for micro-frontend scenarios. |
|
||||
|
||||
`grayDeployments` field configuration description:
|
||||
| Name | Data Type | Requirements | Default Value | Description |
|
||||
|------------------|-------------------|---------------|---------------|----------------------------------------------------------------------------------------------|
|
||||
| `version` | string | Required | - | Version number of the Gray version, if the gray rules are hit, this version will be used. If it is a non-CDN deployment, add `x-higress-tag` to the header. |
|
||||
| `backendVersion` | string | Required | - | Gray version for the backend, which will add `x-mse-tag` to the header of `XHR/Fetch` requests. |
|
||||
| `name` | string | Required | - | Rule name associated with `rules[].name`. |
|
||||
| `enabled` | boolean | Required | - | Whether to activate the current gray release rule. |
|
||||
### `grayDeployments` Field
|
||||
| Name | Data Type | Required | Default | Description |
|
||||
|------|-----------|----------|---------|-------------|
|
||||
| `version` | string | Required | - | Gray version used when rules match. Adds `x-higress-tag` header for non-CDN deployments. |
|
||||
| `versionPredicates` | string | Required | - | Multi-version support for micro-frontends. |
|
||||
| `backendVersion` | string | Required | - | Backend grayscale version for cookies. |
|
||||
| `name` | string | Required | - | Linked to `rules[].name`. |
|
||||
| `enabled` | boolean | Required | - | Enable/disable rule. |
|
||||
| `weight` | int | Optional | - | Traffic percentage (e.g., 50). |
|
||||
|
||||
## Configuration Example
|
||||
### Basic Configuration
|
||||
> **Percentage-based Grayscale Notes**:
|
||||
> 1. Percentage rules override user-based rules when both exist.
|
||||
> 2. Uses UUID fingerprint hashed via SHA-256 for traffic distribution.
|
||||
|
||||
### `injection` Field
|
||||
| Name | Data Type | Required | Default | Description |
|
||||
|------|-----------|----------|---------|-------------|
|
||||
| `globalConfig` | object | Optional | - | Global variables injected into HTML. |
|
||||
| `head` | string[] | Optional | - | Inject elements into `<head>`. |
|
||||
| `body` | object | Optional | - | Inject elements into `<body>`. |
|
||||
|
||||
#### `globalConfig` Sub-field
|
||||
| Name | Data Type | Required | Default | Description |
|
||||
|------|-----------|----------|---------|-------------|
|
||||
| `key` | string | Optional | `HIGRESS_CONSOLE_CONFIG` | Window global variable key. |
|
||||
| `featureKey` | string | Optional | `FEATURE_STATUS` | Rule hit status (e.g., `{"beta-user":true}`). |
|
||||
| `value` | string | Optional | - | Custom global value. |
|
||||
| `enabled` | boolean | Optional | `false` | Enable global injection. |
|
||||
|
||||
#### `body` Sub-field
|
||||
| Name | Data Type | Required | Default | Description |
|
||||
|------|-----------|----------|---------|-------------|
|
||||
| `first` | string[] | Optional | - | Inject at body start. |
|
||||
| `after` | string[] | Optional | - | Inject at body end. |
|
||||
|
||||
## Configuration Examples
|
||||
### Basic Configuration (User-based)
|
||||
```yml
|
||||
grayKey: userid
|
||||
rules:
|
||||
@@ -75,88 +111,3 @@ grayDeployments:
|
||||
- name: beta-user
|
||||
version: gray
|
||||
enabled: true
|
||||
```
|
||||
|
||||
The unique identifier of the user in the cookie is `userid`, and the current gray release rule has configured the `beta-user` rule.
|
||||
When the following conditions are met, the version `version: gray` will be used:
|
||||
- `userid` in the cookie equals `00000002` or `00000003`
|
||||
- Users whose `level` in the cookie equals `level3` or `level5`
|
||||
Otherwise, use version `version: base`.
|
||||
|
||||
### User Information Exists in JSON
|
||||
```yml
|
||||
grayKey: appInfo
|
||||
graySubKey: userId
|
||||
rules:
|
||||
- name: inner-user
|
||||
grayKeyValue:
|
||||
- '00000001'
|
||||
- '00000005'
|
||||
- name: beta-user
|
||||
grayKeyValue:
|
||||
- '00000002'
|
||||
- '00000003'
|
||||
grayTagKey: level
|
||||
grayTagValue:
|
||||
- level3
|
||||
- level5
|
||||
baseDeployment:
|
||||
version: base
|
||||
grayDeployments:
|
||||
- name: beta-user
|
||||
version: gray
|
||||
enabled: true
|
||||
```
|
||||
|
||||
The cookie contains JSON data for `appInfo`, which includes the field `userId` as the current unique identifier.
|
||||
The current gray release rule has configured the `beta-user` rule.
|
||||
When the following conditions are met, the version `version: gray` will be used:
|
||||
- `userid` in the cookie equals `00000002` or `00000003`
|
||||
- Users whose `level` in the cookie equals `level3` or `level5`
|
||||
Otherwise, use version `version: base`.
|
||||
|
||||
### Rewrite Configuration
|
||||
> Generally used in CDN deployment scenarios.
|
||||
```yml
|
||||
grayKey: userid
|
||||
rules:
|
||||
- name: inner-user
|
||||
grayKeyValue:
|
||||
- '00000001'
|
||||
- '00000005'
|
||||
- name: beta-user
|
||||
grayKeyValue:
|
||||
- '00000002'
|
||||
- '00000003'
|
||||
grayTagKey: level
|
||||
grayTagValue:
|
||||
- level3
|
||||
- level5
|
||||
rewrite:
|
||||
host: frontend-gray.oss-cn-shanghai-internal.aliyuncs.com
|
||||
notFoundUri: /mfe/app1/dev/404.html
|
||||
indexRouting:
|
||||
/app1: '/mfe/app1/{version}/index.html'
|
||||
/: '/mfe/app1/{version}/index.html',
|
||||
fileRouting:
|
||||
/: '/mfe/app1/{version}'
|
||||
/app1/: '/mfe/app1/{version}'
|
||||
baseDeployment:
|
||||
version: base
|
||||
grayDeployments:
|
||||
- name: beta-user
|
||||
version: gray
|
||||
enabled: true
|
||||
```
|
||||
|
||||
`{version}` will be dynamically replaced with the actual version during execution.
|
||||
|
||||
#### indexRouting: Homepage Route Configuration
|
||||
Accessing `/app1`, `/app123`, `/app1/index.html`, `/app1/xxx`, `/xxxx` will route to '/mfe/app1/{version}/index.html'.
|
||||
|
||||
#### fileRouting: File Route Configuration
|
||||
The following file mappings are effective:
|
||||
- `/js/a.js` => `/mfe/app1/v1.0.0/js/a.js`
|
||||
- `/js/template/a.js` => `/mfe/app1/v1.0.0/js/template/a.js`
|
||||
- `/app1/js/a.js` => `/mfe/app1/v1.0.0/js/a.js`
|
||||
- `/app1/js/template/a.js` => `/mfe/app1/v1.0.0/js/template/a.js`
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
const (
|
||||
XHigressTag = "x-higress-tag"
|
||||
XUniqueClientId = "x-unique-client"
|
||||
XPreHigressTag = "x-pre-higress-tag"
|
||||
IsPageRequest = "is-page-request"
|
||||
IsNotFound = "is-not-found"
|
||||
EnabledGray = "enabled-gray"
|
||||
SecFetchMode = "sec-fetch-mode"
|
||||
XHigressTag = "x-higress-tag"
|
||||
PreHigressVersion = "pre-higress-version"
|
||||
IsHtmlRequest = "is-html-request"
|
||||
IsIndexRequest = "is-index-request"
|
||||
EnabledGray = "enabled-gray"
|
||||
)
|
||||
|
||||
type LogInfo func(format string, args ...interface{})
|
||||
|
||||
type GrayRule struct {
|
||||
Name string
|
||||
GrayKeyValue []string
|
||||
@@ -35,15 +35,22 @@ type Deployment struct {
|
||||
}
|
||||
|
||||
type Rewrite struct {
|
||||
Host string
|
||||
NotFound string
|
||||
Index map[string]string
|
||||
File map[string]string
|
||||
Host string
|
||||
Index map[string]string
|
||||
File map[string]string
|
||||
}
|
||||
|
||||
type Injection struct {
|
||||
Head []string
|
||||
Body *BodyInjection
|
||||
GlobalConfig *GlobalConfig
|
||||
Head []string
|
||||
Body *BodyInjection
|
||||
}
|
||||
|
||||
type GlobalConfig struct {
|
||||
Key string
|
||||
FeatureKey string
|
||||
Value string
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
type BodyInjection struct {
|
||||
@@ -52,8 +59,7 @@ type BodyInjection struct {
|
||||
}
|
||||
|
||||
type GrayConfig struct {
|
||||
UserStickyMaxAge string
|
||||
TotalGrayWeight int
|
||||
StoreMaxAge int
|
||||
GrayKey string
|
||||
LocalStorageGrayKey string
|
||||
GraySubKey string
|
||||
@@ -63,10 +69,26 @@ type GrayConfig struct {
|
||||
BaseDeployment *Deployment
|
||||
GrayDeployments []*Deployment
|
||||
BackendGrayTag string
|
||||
UniqueGrayTag string
|
||||
Injection *Injection
|
||||
SkippedPathPrefixes []string
|
||||
IncludePathPrefixes []string
|
||||
SkippedPaths []string
|
||||
SkippedByHeaders map[string]string
|
||||
IndexPaths []string
|
||||
GrayWeight int
|
||||
}
|
||||
|
||||
func isValidName(s string) bool {
|
||||
// 定义一个正则表达式,匹配字母、数字和下划线
|
||||
re := regexp.MustCompile(`^[a-zA-Z0-9_]+$`)
|
||||
return re.MatchString(s)
|
||||
}
|
||||
|
||||
func GetWithDefault(json gjson.Result, path, defaultValue string) string {
|
||||
res := json.Get(path)
|
||||
if res.Exists() {
|
||||
return res.String()
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func convertToStringList(results []gjson.Result) []string {
|
||||
@@ -77,6 +99,22 @@ func convertToStringList(results []gjson.Result) []string {
|
||||
return interfaces
|
||||
}
|
||||
|
||||
func compatibleConvertToStringList(results []gjson.Result, compatibleResults []gjson.Result) []string {
|
||||
// 优先使用兼容模式的数据
|
||||
if len(compatibleResults) == 0 {
|
||||
interfaces := make([]string, len(results)) // 预分配切片容量
|
||||
for i, result := range results {
|
||||
interfaces[i] = result.String() // 使用 String() 方法直接获取字符串
|
||||
}
|
||||
return interfaces
|
||||
}
|
||||
compatibleInterfaces := make([]string, len(compatibleResults)) // 预分配切片容量
|
||||
for i, result := range compatibleResults {
|
||||
compatibleInterfaces[i] = filepath.Join(result.String(), "**")
|
||||
}
|
||||
return compatibleInterfaces
|
||||
}
|
||||
|
||||
func convertToStringMap(result gjson.Result) map[string]string {
|
||||
m := make(map[string]string)
|
||||
result.ForEach(func(key, value gjson.Result) bool {
|
||||
@@ -86,7 +124,7 @@ func convertToStringMap(result gjson.Result) map[string]string {
|
||||
return m
|
||||
}
|
||||
|
||||
func JsonToGrayConfig(json gjson.Result, grayConfig *GrayConfig) {
|
||||
func JsonToGrayConfig(json gjson.Result, grayConfig *GrayConfig) error {
|
||||
// 解析 GrayKey
|
||||
grayConfig.LocalStorageGrayKey = json.Get("localStorageGrayKey").String()
|
||||
grayConfig.GrayKey = json.Get("grayKey").String()
|
||||
@@ -94,22 +132,18 @@ func JsonToGrayConfig(json gjson.Result, grayConfig *GrayConfig) {
|
||||
grayConfig.GrayKey = grayConfig.LocalStorageGrayKey
|
||||
}
|
||||
grayConfig.GraySubKey = json.Get("graySubKey").String()
|
||||
grayConfig.BackendGrayTag = json.Get("backendGrayTag").String()
|
||||
grayConfig.UserStickyMaxAge = json.Get("userStickyMaxAge").String()
|
||||
grayConfig.BackendGrayTag = GetWithDefault(json, "backendGrayTag", "x-mse-tag")
|
||||
grayConfig.UniqueGrayTag = GetWithDefault(json, "uniqueGrayTag", "x-higress-uid")
|
||||
grayConfig.StoreMaxAge = 60 * 60 * 24 * 365 // 默认一年
|
||||
storeMaxAge, err := strconv.Atoi(GetWithDefault(json, "StoreMaxAge", strconv.Itoa(grayConfig.StoreMaxAge)))
|
||||
if err != nil {
|
||||
grayConfig.StoreMaxAge = storeMaxAge
|
||||
}
|
||||
|
||||
grayConfig.Html = json.Get("html").String()
|
||||
grayConfig.SkippedPathPrefixes = convertToStringList(json.Get("skippedPathPrefixes").Array())
|
||||
grayConfig.SkippedPaths = compatibleConvertToStringList(json.Get("skippedPaths").Array(), json.Get("skippedPathPrefixes").Array())
|
||||
grayConfig.IndexPaths = compatibleConvertToStringList(json.Get("indexPaths").Array(), json.Get("includePathPrefixes").Array())
|
||||
grayConfig.SkippedByHeaders = convertToStringMap(json.Get("skippedByHeaders"))
|
||||
grayConfig.IncludePathPrefixes = convertToStringList(json.Get("includePathPrefixes").Array())
|
||||
|
||||
if grayConfig.UserStickyMaxAge == "" {
|
||||
// 默认值2天
|
||||
grayConfig.UserStickyMaxAge = "172800"
|
||||
}
|
||||
|
||||
if grayConfig.BackendGrayTag == "" {
|
||||
grayConfig.BackendGrayTag = "x-mse-tag"
|
||||
}
|
||||
|
||||
// 解析 Rules
|
||||
rules := json.Get("rules").Array()
|
||||
for _, rule := range rules {
|
||||
@@ -122,10 +156,9 @@ func JsonToGrayConfig(json gjson.Result, grayConfig *GrayConfig) {
|
||||
grayConfig.Rules = append(grayConfig.Rules, &grayRule)
|
||||
}
|
||||
grayConfig.Rewrite = &Rewrite{
|
||||
Host: json.Get("rewrite.host").String(),
|
||||
NotFound: json.Get("rewrite.notFoundUri").String(),
|
||||
Index: convertToStringMap(json.Get("rewrite.indexRouting")),
|
||||
File: convertToStringMap(json.Get("rewrite.fileRouting")),
|
||||
Host: json.Get("rewrite.host").String(),
|
||||
Index: convertToStringMap(json.Get("rewrite.indexRouting")),
|
||||
File: convertToStringMap(json.Get("rewrite.fileRouting")),
|
||||
}
|
||||
|
||||
// 解析 deployment
|
||||
@@ -134,6 +167,7 @@ func JsonToGrayConfig(json gjson.Result, grayConfig *GrayConfig) {
|
||||
|
||||
grayConfig.BaseDeployment = &Deployment{
|
||||
Name: baseDeployment.Get("name").String(),
|
||||
BackendVersion: baseDeployment.Get("backendVersion").String(),
|
||||
Version: strings.Trim(baseDeployment.Get("version").String(), " "),
|
||||
VersionPredicates: convertToStringMap(baseDeployment.Get("versionPredicates")),
|
||||
}
|
||||
@@ -141,16 +175,28 @@ func JsonToGrayConfig(json gjson.Result, grayConfig *GrayConfig) {
|
||||
if !item.Get("enabled").Bool() {
|
||||
continue
|
||||
}
|
||||
grayWeight := int(item.Get("weight").Int())
|
||||
weight := int(item.Get("weight").Int())
|
||||
grayConfig.GrayDeployments = append(grayConfig.GrayDeployments, &Deployment{
|
||||
Name: item.Get("name").String(),
|
||||
Enabled: item.Get("enabled").Bool(),
|
||||
Version: strings.Trim(item.Get("version").String(), " "),
|
||||
BackendVersion: item.Get("backendVersion").String(),
|
||||
Weight: grayWeight,
|
||||
Weight: weight,
|
||||
VersionPredicates: convertToStringMap(item.Get("versionPredicates")),
|
||||
})
|
||||
grayConfig.TotalGrayWeight += grayWeight
|
||||
if weight > 0 {
|
||||
grayConfig.GrayWeight = weight
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
injectGlobalFeatureKey := GetWithDefault(json, "injection.globalConfig.featureKey", "FEATURE_STATUS")
|
||||
injectGlobalKey := GetWithDefault(json, "injection.globalConfig.key", "HIGRESS_CONSOLE_CONFIG")
|
||||
if !isValidName(injectGlobalFeatureKey) {
|
||||
return errors.New("injection.globalConfig.featureKey is invalid")
|
||||
}
|
||||
if !isValidName(injectGlobalKey) {
|
||||
return errors.New("injection.globalConfig.featureKey is invalid")
|
||||
}
|
||||
|
||||
grayConfig.Injection = &Injection{
|
||||
@@ -159,5 +205,12 @@ func JsonToGrayConfig(json gjson.Result, grayConfig *GrayConfig) {
|
||||
First: convertToStringList(json.Get("injection.body.first").Array()),
|
||||
Last: convertToStringList(json.Get("injection.body.last").Array()),
|
||||
},
|
||||
GlobalConfig: &GlobalConfig{
|
||||
FeatureKey: injectGlobalFeatureKey,
|
||||
Key: injectGlobalKey,
|
||||
Value: json.Get("injection.globalConfig.value").String(),
|
||||
Enabled: json.Get("injection.globalConfig.enabled").Bool(),
|
||||
},
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -48,8 +48,8 @@ static_resources:
|
||||
value: |
|
||||
{
|
||||
"grayKey": "userId",
|
||||
"backendGrayTag": "x-mse-tag",
|
||||
"userStickyMaxAge": 172800,
|
||||
"backendGrayTag": "env",
|
||||
"uniqueGrayTag": "uuid",
|
||||
"rules": [
|
||||
{
|
||||
"name": "inner-user",
|
||||
@@ -72,30 +72,44 @@ static_resources:
|
||||
}
|
||||
],
|
||||
"rewrite": {
|
||||
"host": "frontend-gray-cn-shanghai.oss-cn-shanghai-internal.aliyuncs.com",
|
||||
"host": "apig-oss-integration.oss-cn-hangzhou.aliyuncs.com",
|
||||
"indexRouting": {
|
||||
"/app1": "/mfe/app1/{version}/index.html",
|
||||
"/": "/mfe/app1/{version}/index.html"
|
||||
"/": "/mfe/{version}/index.html"
|
||||
},
|
||||
"fileRouting": {
|
||||
"/": "/mfe/app1/{version}",
|
||||
"/app1": "/mfe/app1/{version}"
|
||||
"/": "/mfe/{version}",
|
||||
"/mfe": "/mfe/{version}"
|
||||
}
|
||||
},
|
||||
"skippedPathPrefixes": [
|
||||
"/api/"
|
||||
"skippedPaths": [
|
||||
"/api/**",
|
||||
"/v2/**"
|
||||
],
|
||||
"indexPaths": [
|
||||
"/mfe/**/mf-manifest-main.json"
|
||||
],
|
||||
"baseDeployment": {
|
||||
"version": "dev"
|
||||
"version": "v1"
|
||||
},
|
||||
"grayDeployments": [
|
||||
{
|
||||
"weight": 90,
|
||||
"name": "beta-user",
|
||||
"version": "0.0.1",
|
||||
"enabled": true
|
||||
"version": "v2",
|
||||
"enabled": true,
|
||||
"backendVersion":"gray",
|
||||
"versionPredicates": {
|
||||
"/mfe": "v1"
|
||||
}
|
||||
}
|
||||
],
|
||||
"injection": {
|
||||
"globalConfig": {
|
||||
"key": "HIGRESS_CONSOLE_CONFIG",
|
||||
"featureKey": "FEATURE_STATUS",
|
||||
"value": "{CONSOLE_GLOBAL: {'gray':'2.0.15','main':'2.0.15'}}",
|
||||
"enabled": true
|
||||
},
|
||||
"head": [
|
||||
"<script>console.log('Header')</script>"
|
||||
],
|
||||
@@ -127,5 +141,5 @@ static_resources:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: frontend-gray-cn-shanghai.oss-cn-shanghai.aliyuncs.com
|
||||
address: apig-oss-integration.oss-cn-hangzhou.aliyuncs.com
|
||||
port_value: 80
|
||||
|
||||
@@ -6,6 +6,7 @@ replace github.com/alibaba/higress/plugins/wasm-go => ../..
|
||||
|
||||
require (
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.3-0.20240727022514-bccfbde62188
|
||||
github.com/bmatcuk/doublestar/v4 v4.6.1
|
||||
github.com/higress-group/proxy-wasm-go-sdk v1.0.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tidwall/gjson v1.17.3
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
github.com/davecgh/go-spew v1.1.1+incompatible/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I=
|
||||
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1+incompatible/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA=
|
||||
@@ -11,9 +13,9 @@ github.com/higress-group/proxy-wasm-go-sdk v1.0.0 h1:BZRNf4R7jr9hwRivg/E29nkVaKE
|
||||
github.com/higress-group/proxy-wasm-go-sdk v1.0.0/go.mod h1:iiSyFbo+rAtbtGt/bsefv8GU57h9CCLYGJA74/tF5/0=
|
||||
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
|
||||
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/pmezard/go-difflib v1.0.0+incompatible/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.0+incompatible/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
|
||||
@@ -2,13 +2,11 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/frontend-gray/config"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/frontend-gray/util"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/log"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
@@ -19,44 +17,42 @@ import (
|
||||
func main() {
|
||||
wrapper.SetCtx(
|
||||
"frontend-gray",
|
||||
wrapper.ParseConfigBy(parseConfig),
|
||||
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
|
||||
wrapper.ProcessResponseHeadersBy(onHttpResponseHeader),
|
||||
wrapper.ProcessResponseBodyBy(onHttpResponseBody),
|
||||
wrapper.ParseConfig(parseConfig),
|
||||
wrapper.ProcessRequestHeaders(onHttpRequestHeaders),
|
||||
wrapper.ProcessResponseHeaders(onHttpResponseHeader),
|
||||
wrapper.ProcessResponseBody(onHttpResponseBody),
|
||||
)
|
||||
}
|
||||
|
||||
func parseConfig(json gjson.Result, grayConfig *config.GrayConfig, log wrapper.Log) error {
|
||||
func parseConfig(json gjson.Result, grayConfig *config.GrayConfig) error {
|
||||
// 解析json 为GrayConfig
|
||||
config.JsonToGrayConfig(json, grayConfig)
|
||||
log.Infof("Rewrite: %v, GrayDeployments: %v", json.Get("rewrite"), json.Get("grayDeployments"))
|
||||
if err := config.JsonToGrayConfig(json, grayConfig); err != nil {
|
||||
log.Errorf("failed to parse config: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig, log wrapper.Log) types.Action {
|
||||
requestPath, _ := proxywasm.GetHttpRequestHeader(":path")
|
||||
requestPath = path.Clean(requestPath)
|
||||
parsedURL, err := url.Parse(requestPath)
|
||||
if err == nil {
|
||||
requestPath = parsedURL.Path
|
||||
} else {
|
||||
log.Errorf("parse request path %s failed: %v", requestPath, err)
|
||||
}
|
||||
enabledGray := util.IsGrayEnabled(grayConfig, requestPath)
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig) types.Action {
|
||||
requestPath := util.GetRequestPath()
|
||||
enabledGray := util.IsGrayEnabled(requestPath, &grayConfig)
|
||||
ctx.SetContext(config.EnabledGray, enabledGray)
|
||||
secFetchMode, _ := proxywasm.GetHttpRequestHeader("sec-fetch-mode")
|
||||
ctx.SetContext(config.SecFetchMode, secFetchMode)
|
||||
route, _ := util.GetRouteName()
|
||||
|
||||
if !enabledGray {
|
||||
log.Infof("gray not enabled")
|
||||
log.Infof("route: %s, gray not enabled, requestPath: %v", route, requestPath)
|
||||
ctx.DontReadRequestBody()
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
cookies, _ := proxywasm.GetHttpRequestHeader("cookie")
|
||||
isPageRequest := util.IsPageRequest(requestPath)
|
||||
cookie, _ := proxywasm.GetHttpRequestHeader("cookie")
|
||||
isHtmlRequest := util.CheckIsHtmlRequest(requestPath)
|
||||
ctx.SetContext(config.IsHtmlRequest, isHtmlRequest)
|
||||
isIndexRequest := util.IsIndexRequest(requestPath, grayConfig.IndexPaths)
|
||||
ctx.SetContext(config.IsIndexRequest, isIndexRequest)
|
||||
hasRewrite := len(grayConfig.Rewrite.File) > 0 || len(grayConfig.Rewrite.Index) > 0
|
||||
grayKeyValueByCookie := util.ExtractCookieValueByKey(cookies, grayConfig.GrayKey)
|
||||
grayKeyValueByCookie := util.GetCookieValue(cookie, grayConfig.GrayKey)
|
||||
grayKeyValueByHeader, _ := proxywasm.GetHttpRequestHeader(grayConfig.GrayKey)
|
||||
// 优先从cookie中获取,否则从header中获取
|
||||
grayKeyValue := util.GetGrayKey(grayKeyValueByCookie, grayKeyValueByHeader, grayConfig.GraySubKey)
|
||||
@@ -65,93 +61,92 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig,
|
||||
// 禁止重新路由,要在更改Header之前操作,否则会失效
|
||||
ctx.DisableReroute()
|
||||
}
|
||||
frontendVersion := util.GetCookieValue(cookie, config.XHigressTag)
|
||||
|
||||
if grayConfig.GrayWeight > 0 {
|
||||
ctx.SetContext(grayConfig.UniqueGrayTag, util.GetGrayWeightUniqueId(cookie, grayConfig.UniqueGrayTag))
|
||||
}
|
||||
|
||||
// 删除Accept-Encoding,避免压缩, 如果是压缩的内容,后续插件就没法处理了
|
||||
_ = proxywasm.RemoveHttpRequestHeader("Accept-Encoding")
|
||||
_ = proxywasm.RemoveHttpRequestHeader("Content-Length")
|
||||
deployment := &config.Deployment{}
|
||||
|
||||
preVersion, preUniqueClientId := util.GetXPreHigressVersion(cookies)
|
||||
// 客户端唯一ID,用于在按照比率灰度时候 客户访问黏贴
|
||||
uniqueClientId := grayKeyValue
|
||||
if uniqueClientId == "" {
|
||||
xForwardedFor, _ := proxywasm.GetHttpRequestHeader("X-Forwarded-For")
|
||||
uniqueClientId = util.GetRealIpFromXff(xForwardedFor)
|
||||
globalConfig := grayConfig.Injection.GlobalConfig
|
||||
if globalConfig.Enabled {
|
||||
conditionRule := util.GetConditionRules(grayConfig.Rules, grayKeyValue, cookie)
|
||||
trimmedValue := strings.TrimSuffix(strings.TrimPrefix(strings.TrimSpace(globalConfig.Value), "{"), "}")
|
||||
ctx.SetContext(globalConfig.Key, fmt.Sprintf("<script>var %s = {\n%s:%s,\n %s \n}\n</script>", globalConfig.Key, globalConfig.FeatureKey, conditionRule, trimmedValue))
|
||||
}
|
||||
|
||||
// 如果没有配置比例,则进行灰度规则匹配
|
||||
if util.IsSupportMultiVersion(grayConfig) {
|
||||
deployment = util.FilterMultiVersionGrayRule(&grayConfig, grayKeyValue, requestPath)
|
||||
log.Infof("multi version %v", deployment)
|
||||
} else {
|
||||
if isPageRequest {
|
||||
if grayConfig.TotalGrayWeight > 0 {
|
||||
log.Infof("grayConfig.TotalGrayWeight: %v", grayConfig.TotalGrayWeight)
|
||||
deployment = util.FilterGrayWeight(&grayConfig, preVersion, preUniqueClientId, uniqueClientId)
|
||||
} else {
|
||||
deployment = util.FilterGrayRule(&grayConfig, grayKeyValue)
|
||||
}
|
||||
log.Infof("index deployment: %v, path: %v, backend: %v, xPreHigressVersion: %s,%s", deployment, requestPath, deployment.BackendVersion, preVersion, preUniqueClientId)
|
||||
} else {
|
||||
grayDeployment := util.FilterGrayRule(&grayConfig, grayKeyValue)
|
||||
deployment = util.GetVersion(grayConfig, grayDeployment, preVersion, isPageRequest)
|
||||
}
|
||||
ctx.SetContext(config.XPreHigressTag, deployment.Version)
|
||||
if isHtmlRequest {
|
||||
// index首页请求每次都会进度灰度规则判断
|
||||
deployment = util.FilterGrayRule(&grayConfig, grayKeyValue, cookie)
|
||||
log.Infof("route: %s, index html request: %v, backend: %v, xPreHigressVersion: %s", route, requestPath, deployment.BackendVersion, frontendVersion)
|
||||
ctx.SetContext(config.PreHigressVersion, deployment.Version)
|
||||
ctx.SetContext(grayConfig.BackendGrayTag, deployment.BackendVersion)
|
||||
} else {
|
||||
if util.IsSupportMultiVersion(grayConfig) {
|
||||
deployment = util.FilterMultiVersionGrayRule(&grayConfig, grayKeyValue, cookie, requestPath)
|
||||
log.Infof("route: %s, multi version %v", route, deployment)
|
||||
} else {
|
||||
grayDeployment := util.FilterGrayRule(&grayConfig, grayKeyValue, cookie)
|
||||
if isIndexRequest {
|
||||
deployment = grayDeployment
|
||||
} else {
|
||||
deployment = util.GetVersion(grayConfig, grayDeployment, frontendVersion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
proxywasm.AddHttpRequestHeader(config.XHigressTag, deployment.Version)
|
||||
|
||||
ctx.SetContext(config.IsPageRequest, isPageRequest)
|
||||
ctx.SetContext(config.XUniqueClientId, uniqueClientId)
|
||||
|
||||
rewrite := grayConfig.Rewrite
|
||||
if rewrite.Host != "" {
|
||||
err := proxywasm.ReplaceHttpRequestHeader(":authority", rewrite.Host)
|
||||
if err != nil {
|
||||
log.Errorf("host rewrite failed: %v", err)
|
||||
log.Errorf("route: %s, host rewrite failed: %v", route, err)
|
||||
}
|
||||
}
|
||||
|
||||
if hasRewrite {
|
||||
rewritePath := requestPath
|
||||
if isPageRequest {
|
||||
if isHtmlRequest {
|
||||
rewritePath = util.IndexRewrite(requestPath, deployment.Version, grayConfig.Rewrite.Index)
|
||||
} else {
|
||||
rewritePath = util.PrefixFileRewrite(requestPath, deployment.Version, grayConfig.Rewrite.File)
|
||||
}
|
||||
if requestPath != rewritePath {
|
||||
log.Infof("rewrite path:%s, rewritePath:%s, Version:%v", requestPath, rewritePath, deployment.Version)
|
||||
log.Infof("route: %s, rewrite path:%s, rewritePath:%s, Version:%v", route, requestPath, rewritePath, deployment.Version)
|
||||
proxywasm.ReplaceHttpRequestHeader(":path", rewritePath)
|
||||
}
|
||||
}
|
||||
log.Infof("request path:%s, has rewrited:%v, rewrite config:%+v", requestPath, hasRewrite, rewrite)
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig, log wrapper.Log) types.Action {
|
||||
func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig) types.Action {
|
||||
enabledGray, _ := ctx.GetContext(config.EnabledGray).(bool)
|
||||
if !enabledGray {
|
||||
ctx.DontReadResponseBody()
|
||||
return types.ActionContinue
|
||||
}
|
||||
secFetchMode, isSecFetchModeOk := ctx.GetContext(config.SecFetchMode).(string)
|
||||
if isSecFetchModeOk && secFetchMode == "cors" {
|
||||
isIndexRequest, indexOk := ctx.GetContext(config.IsIndexRequest).(bool)
|
||||
if indexOk && isIndexRequest {
|
||||
// 首页请求强制不缓存
|
||||
proxywasm.ReplaceHttpResponseHeader("cache-control", "no-cache, no-store, max-age=0, must-revalidate")
|
||||
ctx.DontReadResponseBody()
|
||||
return types.ActionContinue
|
||||
}
|
||||
isPageRequest, ok := ctx.GetContext(config.IsPageRequest).(bool)
|
||||
if !ok {
|
||||
isPageRequest = false // 默认值
|
||||
}
|
||||
|
||||
isHtmlRequest, htmlOk := ctx.GetContext(config.IsHtmlRequest).(bool)
|
||||
// response 不处理非首页的请求
|
||||
if !isPageRequest {
|
||||
if !htmlOk || !isHtmlRequest {
|
||||
ctx.DontReadResponseBody()
|
||||
return types.ActionContinue
|
||||
} else {
|
||||
// 不会进去Streaming 的Body处理
|
||||
ctx.BufferResponseBody()
|
||||
}
|
||||
|
||||
// 处理HTML的首页
|
||||
status, err := proxywasm.GetHttpResponseHeader(":status")
|
||||
if grayConfig.Rewrite != nil && grayConfig.Rewrite.Host != "" {
|
||||
// 删除Content-Disposition,避免自动下载文件
|
||||
@@ -163,117 +158,81 @@ func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig,
|
||||
|
||||
// 处理code为 200的情况
|
||||
if err != nil || status != "200" {
|
||||
if status == "404" {
|
||||
if grayConfig.Rewrite.NotFound != "" && isPageRequest {
|
||||
ctx.SetContext(config.IsNotFound, true)
|
||||
responseHeaders, _ := proxywasm.GetHttpResponseHeaders()
|
||||
headersMap := util.ConvertHeaders(responseHeaders)
|
||||
if _, ok := headersMap[":status"]; !ok {
|
||||
headersMap[":status"] = []string{"200"} // 如果没有初始化,设定默认值
|
||||
} else {
|
||||
headersMap[":status"][0] = "200" // 修改现有值
|
||||
}
|
||||
if _, ok := headersMap["content-type"]; !ok {
|
||||
headersMap["content-type"] = []string{"text/html"} // 如果没有初始化,设定默认值
|
||||
} else {
|
||||
headersMap["content-type"][0] = "text/html" // 修改现有值
|
||||
}
|
||||
// 删除 content-length 键
|
||||
delete(headersMap, "content-length")
|
||||
proxywasm.ReplaceHttpResponseHeaders(util.ReconvertHeaders(headersMap))
|
||||
ctx.BufferResponseBody()
|
||||
return types.ActionContinue
|
||||
} else {
|
||||
// 直接返回400
|
||||
ctx.DontReadResponseBody()
|
||||
}
|
||||
// 如果找不到HTML,但配置了HTML页面
|
||||
if status == "404" && grayConfig.Html != "" {
|
||||
responseHeaders, _ := proxywasm.GetHttpResponseHeaders()
|
||||
headersMap := util.ConvertHeaders(responseHeaders)
|
||||
delete(headersMap, "content-length")
|
||||
headersMap[":status"][0] = "200"
|
||||
headersMap["content-type"][0] = "text/html"
|
||||
ctx.BufferResponseBody()
|
||||
proxywasm.ReplaceHttpResponseHeaders(util.ReconvertHeaders(headersMap))
|
||||
} else {
|
||||
route, _ := util.GetRouteName()
|
||||
log.Errorf("route: %s, request error code: %s, message: %v", route, status, err)
|
||||
ctx.DontReadResponseBody()
|
||||
return types.ActionContinue
|
||||
}
|
||||
log.Errorf("error status: %s, error message: %v", status, err)
|
||||
return types.ActionContinue
|
||||
}
|
||||
cacheControl, _ := proxywasm.GetHttpResponseHeader("cache-control")
|
||||
if !strings.Contains(cacheControl, "no-cache") {
|
||||
proxywasm.ReplaceHttpResponseHeader("cache-control", "no-cache, no-store, max-age=0, must-revalidate")
|
||||
proxywasm.ReplaceHttpResponseHeader("cache-control", "no-cache, no-store, max-age=0, must-revalidate")
|
||||
|
||||
// 前端版本
|
||||
frontendVersion, isFrontendVersionOk := ctx.GetContext(config.PreHigressVersion).(string)
|
||||
if isFrontendVersionOk {
|
||||
proxywasm.AddHttpResponseHeader("Set-Cookie", fmt.Sprintf("%s=%s; Max-Age=%d; Path=/;", config.XHigressTag, frontendVersion, grayConfig.StoreMaxAge))
|
||||
}
|
||||
|
||||
frontendVersion, isFeVersionOk := ctx.GetContext(config.XPreHigressTag).(string)
|
||||
xUniqueClient, isUniqClientOk := ctx.GetContext(config.XUniqueClientId).(string)
|
||||
|
||||
// 设置前端的版本
|
||||
if isFeVersionOk && isUniqClientOk && frontendVersion != "" {
|
||||
proxywasm.AddHttpResponseHeader("Set-Cookie", fmt.Sprintf("%s=%s,%s; Max-Age=%s; Path=/;", config.XPreHigressTag, frontendVersion, xUniqueClient, grayConfig.UserStickyMaxAge))
|
||||
// 设置GrayWeight 唯一值
|
||||
if grayConfig.GrayWeight > 0 {
|
||||
uniqueId, isUniqueIdOk := ctx.GetContext(grayConfig.UniqueGrayTag).(string)
|
||||
if isUniqueIdOk {
|
||||
proxywasm.AddHttpResponseHeader("Set-Cookie", fmt.Sprintf("%s=%s; Max-Age=%d; Path=/;", grayConfig.UniqueGrayTag, uniqueId, grayConfig.StoreMaxAge))
|
||||
}
|
||||
}
|
||||
// 设置后端的版本
|
||||
if util.IsBackendGrayEnabled(grayConfig) {
|
||||
backendVersion, isBackVersionOk := ctx.GetContext(grayConfig.BackendGrayTag).(string)
|
||||
if isBackVersionOk && backendVersion != "" {
|
||||
proxywasm.AddHttpResponseHeader("Set-Cookie", fmt.Sprintf("%s=%s; Max-Age=%s; Path=/;", grayConfig.BackendGrayTag, backendVersion, grayConfig.UserStickyMaxAge))
|
||||
if isBackVersionOk {
|
||||
if backendVersion == "" {
|
||||
// 删除后端灰度版本
|
||||
proxywasm.AddHttpResponseHeader("Set-Cookie", fmt.Sprintf("%s=%s; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/;", grayConfig.BackendGrayTag, backendVersion))
|
||||
} else {
|
||||
proxywasm.AddHttpResponseHeader("Set-Cookie", fmt.Sprintf("%s=%s; Max-Age=%d; Path=/;", grayConfig.BackendGrayTag, backendVersion, grayConfig.StoreMaxAge))
|
||||
}
|
||||
}
|
||||
}
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
func onHttpResponseBody(ctx wrapper.HttpContext, grayConfig config.GrayConfig, body []byte, log wrapper.Log) types.Action {
|
||||
func onHttpResponseBody(ctx wrapper.HttpContext, grayConfig config.GrayConfig, body []byte) types.Action {
|
||||
enabledGray, _ := ctx.GetContext(config.EnabledGray).(bool)
|
||||
if !enabledGray {
|
||||
return types.ActionContinue
|
||||
}
|
||||
isPageRequest, isPageRequestOk := ctx.GetContext(config.IsPageRequest).(bool)
|
||||
frontendVersion, isFeVersionOk := ctx.GetContext(config.XPreHigressTag).(string)
|
||||
isHtmlRequest, isHtmlRequestOk := ctx.GetContext(config.IsHtmlRequest).(bool)
|
||||
frontendVersion, isFeVersionOk := ctx.GetContext(config.PreHigressVersion).(string)
|
||||
// 只处理首页相关请求
|
||||
if !isFeVersionOk || !isPageRequestOk || !isPageRequest {
|
||||
if !isFeVersionOk || !isHtmlRequestOk || !isHtmlRequest {
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
isNotFound, ok := ctx.GetContext(config.IsNotFound).(bool)
|
||||
if !ok {
|
||||
isNotFound = false // 默认值
|
||||
globalConfig := grayConfig.Injection.GlobalConfig
|
||||
globalConfigValue, isGobalConfigOk := ctx.GetContext(globalConfig.Key).(string)
|
||||
if !isGobalConfigOk {
|
||||
globalConfigValue = ""
|
||||
}
|
||||
|
||||
// 检查是否存在自定义 HTML, 如有则省略 rewrite.indexRouting 的内容
|
||||
newHtml := string(body)
|
||||
if grayConfig.Html != "" {
|
||||
log.Debugf("Returning custom HTML from config.")
|
||||
// 替换响应体为 config.Html 内容
|
||||
if err := proxywasm.ReplaceHttpResponseBody([]byte(grayConfig.Html)); err != nil {
|
||||
log.Errorf("Error replacing response body: %v", err)
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
newHtml := util.InjectContent(grayConfig.Html, grayConfig.Injection)
|
||||
// 替换当前html加载的动态文件版本
|
||||
newHtml = strings.ReplaceAll(newHtml, "{version}", frontendVersion)
|
||||
|
||||
// 最终替换响应体
|
||||
if err := proxywasm.ReplaceHttpResponseBody([]byte(newHtml)); err != nil {
|
||||
log.Errorf("Error replacing injected response body: %v", err)
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
return types.ActionContinue
|
||||
newHtml = grayConfig.Html
|
||||
}
|
||||
newHtml = util.InjectContent(newHtml, grayConfig.Injection, globalConfigValue)
|
||||
// 替换当前html加载的动态文件版本
|
||||
newHtml = strings.ReplaceAll(newHtml, "{version}", frontendVersion)
|
||||
newHtml = util.FixLocalStorageKey(newHtml, grayConfig.LocalStorageGrayKey)
|
||||
|
||||
// 针对404页面处理
|
||||
if isNotFound && grayConfig.Rewrite.Host != "" && grayConfig.Rewrite.NotFound != "" {
|
||||
client := wrapper.NewClusterClient(wrapper.RouteCluster{Host: grayConfig.Rewrite.Host})
|
||||
|
||||
client.Get(strings.Replace(grayConfig.Rewrite.NotFound, "{version}", frontendVersion, -1), nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
proxywasm.ReplaceHttpResponseBody(responseBody)
|
||||
proxywasm.ResumeHttpResponse()
|
||||
}, 1500)
|
||||
return types.ActionPause
|
||||
}
|
||||
|
||||
// 处理响应体HTML
|
||||
newBody := string(body)
|
||||
newBody = util.InjectContent(newBody, grayConfig.Injection)
|
||||
if grayConfig.LocalStorageGrayKey != "" {
|
||||
localStr := strings.ReplaceAll(`<script>
|
||||
!function(){var o,e,n="@@X_GRAY_KEY",t=document.cookie.split("; ").filter(function(o){return 0===o.indexOf(n+"=")});try{"undefined"!=typeof localStorage&&null!==localStorage&&(o=localStorage.getItem(n),e=0<t.length?decodeURIComponent(t[0].split("=")[1]):null,o)&&o.indexOf("=")<0&&e&&e!==o&&(document.cookie=n+"="+encodeURIComponent(o)+"; path=/;",window.location.reload())}catch(o){}}();
|
||||
</script>
|
||||
`, "@@X_GRAY_KEY", grayConfig.LocalStorageGrayKey)
|
||||
newBody = strings.ReplaceAll(newBody, "<body>", "<body>\n"+localStr)
|
||||
}
|
||||
if err := proxywasm.ReplaceHttpResponseBody([]byte(newBody)); err != nil {
|
||||
// 最终替换响应体
|
||||
if err := proxywasm.ReplaceHttpResponseBody([]byte(newHtml)); err != nil {
|
||||
route, _ := util.GetRouteName()
|
||||
log.Errorf("route: %s, Failed to replace response body: %v", route, err)
|
||||
return types.ActionContinue
|
||||
}
|
||||
return types.ActionContinue
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"encoding/json"
|
||||
"hash/crc32"
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bmatcuk/doublestar/v4"
|
||||
"github.com/google/uuid"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/frontend-gray/config"
|
||||
@@ -17,43 +18,26 @@ import (
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func LogInfof(format string, args ...interface{}) {
|
||||
format = fmt.Sprintf("[%s] %s", "frontend-gray", format)
|
||||
proxywasm.LogInfof(format, args...)
|
||||
func GetRequestPath() string {
|
||||
requestPath, _ := proxywasm.GetHttpRequestHeader(":path")
|
||||
requestPath = path.Clean(requestPath)
|
||||
parsedURL, err := url.Parse(requestPath)
|
||||
if err == nil {
|
||||
requestPath = parsedURL.Path
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
return requestPath
|
||||
}
|
||||
|
||||
func GetXPreHigressVersion(cookies string) (string, string) {
|
||||
xPreHigressVersion := ExtractCookieValueByKey(cookies, config.XPreHigressTag)
|
||||
preVersions := strings.Split(xPreHigressVersion, ",")
|
||||
if len(preVersions) == 0 {
|
||||
return "", ""
|
||||
func GetRouteName() (string, error) {
|
||||
if raw, err := proxywasm.GetProperty([]string{"route_name"}); err != nil {
|
||||
return "-", err
|
||||
} else {
|
||||
return string(raw), nil
|
||||
}
|
||||
if len(preVersions) == 1 {
|
||||
return preVersions[0], ""
|
||||
}
|
||||
|
||||
return strings.TrimSpace(preVersions[0]), strings.TrimSpace(preVersions[1])
|
||||
}
|
||||
|
||||
// 从xff中获取真实的IP
|
||||
func GetRealIpFromXff(xff string) string {
|
||||
if xff != "" {
|
||||
// 通常客户端的真实 IP 是 XFF 头中的第一个 IP
|
||||
ips := strings.Split(xff, ",")
|
||||
if len(ips) > 0 {
|
||||
return strings.TrimSpace(ips[0])
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func IsRequestSkippedByHeaders(grayConfig config.GrayConfig) bool {
|
||||
secFetchMode, _ := proxywasm.GetHttpRequestHeader("sec-fetch-mode")
|
||||
upgrade, _ := proxywasm.GetHttpRequestHeader("upgrade")
|
||||
if len(grayConfig.SkippedByHeaders) == 0 {
|
||||
// 默认不走插件逻辑的header
|
||||
return secFetchMode == "cors" || upgrade == "websocket"
|
||||
}
|
||||
func IsRequestSkippedByHeaders(grayConfig *config.GrayConfig) bool {
|
||||
for headerKey, headerValue := range grayConfig.SkippedByHeaders {
|
||||
requestHeader, _ := proxywasm.GetHttpRequestHeader(headerKey)
|
||||
if requestHeader == headerValue {
|
||||
@@ -63,34 +47,40 @@ func IsRequestSkippedByHeaders(grayConfig config.GrayConfig) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func IsGrayEnabled(grayConfig config.GrayConfig, requestPath string) bool {
|
||||
for _, prefix := range grayConfig.IncludePathPrefixes {
|
||||
if strings.HasPrefix(requestPath, prefix) {
|
||||
func IsIndexRequest(requestPath string, indexPaths []string) bool {
|
||||
for _, prefix := range indexPaths {
|
||||
matchResult, err := doublestar.Match(prefix, requestPath)
|
||||
if err == nil && matchResult {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 当前路径中前缀为 SkippedPathPrefixes,则不走插件逻辑
|
||||
for _, prefix := range grayConfig.SkippedPathPrefixes {
|
||||
if strings.HasPrefix(requestPath, prefix) {
|
||||
func IsGrayEnabled(requestPath string, grayConfig *config.GrayConfig) bool {
|
||||
if IsIndexRequest(requestPath, grayConfig.IndexPaths) {
|
||||
return true
|
||||
}
|
||||
// 当前路径中前缀为 SkippedPaths,则不走插件逻辑
|
||||
for _, prefix := range grayConfig.SkippedPaths {
|
||||
matchResult, err := doublestar.Match(prefix, requestPath)
|
||||
if err == nil && matchResult {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是首页,进入插件逻辑
|
||||
if IsPageRequest(requestPath) {
|
||||
if CheckIsHtmlRequest(requestPath) {
|
||||
return true
|
||||
}
|
||||
// 检查是否存在重写主机
|
||||
if grayConfig.Rewrite != nil && grayConfig.Rewrite.Host != "" {
|
||||
return true
|
||||
}
|
||||
// 检查header标识,判断是否需要跳过
|
||||
if IsRequestSkippedByHeaders(grayConfig) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查是否存在重写主机
|
||||
if grayConfig.Rewrite != nil && grayConfig.Rewrite.Host != "" {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查是否存在灰度版本配置
|
||||
return len(grayConfig.GrayDeployments) > 0
|
||||
}
|
||||
@@ -105,8 +95,8 @@ func IsBackendGrayEnabled(grayConfig config.GrayConfig) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// ExtractCookieValueByKey 根据 cookie 和 key 获取 cookie 值
|
||||
func ExtractCookieValueByKey(cookie string, key string) string {
|
||||
// GetCookieValue 根据 cookie 和 key 获取 cookie 值
|
||||
func GetCookieValue(cookie string, key string) string {
|
||||
if cookie == "" {
|
||||
return ""
|
||||
}
|
||||
@@ -170,7 +160,7 @@ var indexSuffixes = []string{
|
||||
".html", ".htm", ".jsp", ".php", ".asp", ".aspx", ".erb", ".ejs", ".twig",
|
||||
}
|
||||
|
||||
func IsPageRequest(requestPath string) bool {
|
||||
func CheckIsHtmlRequest(requestPath string) bool {
|
||||
if requestPath == "/" || requestPath == "" {
|
||||
return true
|
||||
}
|
||||
@@ -227,10 +217,7 @@ func PrefixFileRewrite(path, version string, matchRules map[string]string) strin
|
||||
return path
|
||||
}
|
||||
|
||||
func GetVersion(grayConfig config.GrayConfig, deployment *config.Deployment, xPreHigressVersion string, isPageRequest bool) *config.Deployment {
|
||||
if isPageRequest {
|
||||
return deployment
|
||||
}
|
||||
func GetVersion(grayConfig config.GrayConfig, deployment *config.Deployment, xPreHigressVersion string) *config.Deployment {
|
||||
// cookie 中为空,返回当前版本
|
||||
if xPreHigressVersion == "" {
|
||||
return deployment
|
||||
@@ -295,10 +282,75 @@ func IsSupportMultiVersion(grayConfig config.GrayConfig) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func GetConditionRules(rules []*config.GrayRule, grayKeyValue string, cookie string) string {
|
||||
ruleMaps := map[string]bool{}
|
||||
for _, grayRule := range rules {
|
||||
if grayRule.GrayKeyValue != nil && len(grayRule.GrayKeyValue) > 0 && grayKeyValue != "" {
|
||||
ruleMaps[grayRule.Name] = ContainsValue(grayRule.GrayKeyValue, grayKeyValue)
|
||||
continue
|
||||
} else if grayRule.GrayTagKey != "" && grayRule.GrayTagValue != nil && len(grayRule.GrayTagValue) > 0 {
|
||||
grayTagValue := GetCookieValue(cookie, grayRule.GrayTagKey)
|
||||
ruleMaps[grayRule.Name] = ContainsValue(grayRule.GrayTagValue, grayTagValue)
|
||||
continue
|
||||
} else {
|
||||
ruleMaps[grayRule.Name] = false
|
||||
}
|
||||
}
|
||||
jsonBytes, err := json.Marshal(ruleMaps)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(jsonBytes)
|
||||
}
|
||||
|
||||
func GetGrayWeightUniqueId(cookie string, uniqueGrayTag string) string {
|
||||
uniqueId := GetCookieValue(cookie, uniqueGrayTag)
|
||||
if uniqueId == "" {
|
||||
uniqueId = strings.ReplaceAll(uuid.NewString(), "-", "")
|
||||
}
|
||||
return uniqueId
|
||||
}
|
||||
|
||||
// FilterGrayRule 过滤灰度规则
|
||||
func FilterGrayRule(grayConfig *config.GrayConfig, grayKeyValue string, cookie string) *config.Deployment {
|
||||
if grayConfig.GrayWeight > 0 {
|
||||
uniqueId := GetGrayWeightUniqueId(cookie, grayConfig.UniqueGrayTag)
|
||||
// 计算哈希后取模
|
||||
mod := crc32.ChecksumIEEE([]byte(uniqueId)) % 100
|
||||
isGray := mod < uint32(grayConfig.GrayWeight)
|
||||
if isGray {
|
||||
for _, deployment := range grayConfig.GrayDeployments {
|
||||
if deployment.Enabled && deployment.Weight > 0 {
|
||||
return deployment
|
||||
}
|
||||
}
|
||||
}
|
||||
return grayConfig.BaseDeployment
|
||||
}
|
||||
|
||||
for _, deployment := range grayConfig.GrayDeployments {
|
||||
grayRule := GetRule(grayConfig.Rules, deployment.Name)
|
||||
// 首先:先校验用户名单ID
|
||||
if grayRule.GrayKeyValue != nil && len(grayRule.GrayKeyValue) > 0 && grayKeyValue != "" {
|
||||
if ContainsValue(grayRule.GrayKeyValue, grayKeyValue) {
|
||||
return deployment
|
||||
}
|
||||
}
|
||||
// 第二:校验Cookie中的 GrayTagKey
|
||||
if grayRule.GrayTagKey != "" && grayRule.GrayTagValue != nil && len(grayRule.GrayTagValue) > 0 {
|
||||
grayTagValue := GetCookieValue(cookie, grayRule.GrayTagKey)
|
||||
if ContainsValue(grayRule.GrayTagValue, grayTagValue) {
|
||||
return deployment
|
||||
}
|
||||
}
|
||||
}
|
||||
return grayConfig.BaseDeployment
|
||||
}
|
||||
|
||||
// FilterMultiVersionGrayRule 过滤多版本灰度规则
|
||||
func FilterMultiVersionGrayRule(grayConfig *config.GrayConfig, grayKeyValue string, requestPath string) *config.Deployment {
|
||||
func FilterMultiVersionGrayRule(grayConfig *config.GrayConfig, grayKeyValue string, cookie string, requestPath string) *config.Deployment {
|
||||
// 首先根据灰度键值获取当前部署
|
||||
currentDeployment := FilterGrayRule(grayConfig, grayKeyValue)
|
||||
currentDeployment := FilterGrayRule(grayConfig, grayKeyValue, cookie)
|
||||
|
||||
// 创建一个新的部署对象,初始化版本为当前部署的版本
|
||||
deployment := &config.Deployment{
|
||||
@@ -319,68 +371,13 @@ func FilterMultiVersionGrayRule(grayConfig *config.GrayConfig, grayKeyValue stri
|
||||
return deployment
|
||||
}
|
||||
|
||||
// FilterGrayRule 过滤灰度规则
|
||||
func FilterGrayRule(grayConfig *config.GrayConfig, grayKeyValue string) *config.Deployment {
|
||||
for _, deployment := range grayConfig.GrayDeployments {
|
||||
grayRule := GetRule(grayConfig.Rules, deployment.Name)
|
||||
// 首先:先校验用户名单ID
|
||||
if grayRule.GrayKeyValue != nil && len(grayRule.GrayKeyValue) > 0 && grayKeyValue != "" {
|
||||
if ContainsValue(grayRule.GrayKeyValue, grayKeyValue) {
|
||||
return deployment
|
||||
}
|
||||
}
|
||||
// 第二:校验Cookie中的 GrayTagKey
|
||||
if grayRule.GrayTagKey != "" && grayRule.GrayTagValue != nil && len(grayRule.GrayTagValue) > 0 {
|
||||
cookieStr, _ := proxywasm.GetHttpRequestHeader("cookie")
|
||||
grayTagValue := ExtractCookieValueByKey(cookieStr, grayRule.GrayTagKey)
|
||||
if ContainsValue(grayRule.GrayTagValue, grayTagValue) {
|
||||
return deployment
|
||||
}
|
||||
}
|
||||
}
|
||||
return grayConfig.BaseDeployment
|
||||
}
|
||||
|
||||
func FilterGrayWeight(grayConfig *config.GrayConfig, preVersion string, preUniqueClientId string, uniqueClientId string) *config.Deployment {
|
||||
// 如果没有灰度权重,直接返回基础版本
|
||||
if grayConfig.TotalGrayWeight == 0 {
|
||||
return grayConfig.BaseDeployment
|
||||
}
|
||||
|
||||
deployments := append(grayConfig.GrayDeployments, grayConfig.BaseDeployment)
|
||||
LogInfof("preVersion: %s, preUniqueClientId: %s, uniqueClientId: %s", preVersion, preUniqueClientId, uniqueClientId)
|
||||
// 用户粘滞,确保每个用户每次访问的都是走同一版本
|
||||
if preVersion != "" && uniqueClientId == preUniqueClientId {
|
||||
for _, deployment := range deployments {
|
||||
if deployment.Version == preVersion {
|
||||
return deployment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
totalWeight := 100
|
||||
// 如果总权重小于100,则将基础版本也加入到总版本列表中
|
||||
if grayConfig.TotalGrayWeight <= totalWeight {
|
||||
grayConfig.BaseDeployment.Weight = 100 - grayConfig.TotalGrayWeight
|
||||
} else {
|
||||
totalWeight = grayConfig.TotalGrayWeight
|
||||
}
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
randWeight := rand.Intn(totalWeight)
|
||||
sumWeight := 0
|
||||
for _, deployment := range deployments {
|
||||
sumWeight += deployment.Weight
|
||||
if randWeight < sumWeight {
|
||||
return deployment
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// InjectContent 用于将内容注入到 HTML 文档的指定位置
|
||||
func InjectContent(originalHtml string, injectionConfig *config.Injection) string {
|
||||
|
||||
headInjection := strings.Join(injectionConfig.Head, "\n")
|
||||
func InjectContent(originalHtml string, injectionConfig *config.Injection, globalConfigValue string) string {
|
||||
heads := injectionConfig.Head
|
||||
if globalConfigValue != "" {
|
||||
heads = append([]string{globalConfigValue}, injectionConfig.Head...)
|
||||
}
|
||||
headInjection := strings.Join(heads, "\n")
|
||||
bodyFirstInjection := strings.Join(injectionConfig.Body.First, "\n")
|
||||
bodyLastInjection := strings.Join(injectionConfig.Body.Last, "\n")
|
||||
|
||||
@@ -401,3 +398,14 @@ func InjectContent(originalHtml string, injectionConfig *config.Injection) strin
|
||||
|
||||
return modifiedHtml
|
||||
}
|
||||
|
||||
func FixLocalStorageKey(newHtml string, localStorageGrayKey string) string {
|
||||
if localStorageGrayKey != "" {
|
||||
localStr := strings.ReplaceAll(`<script>
|
||||
!function(){var o,e,n="@@X_GRAY_KEY",t=document.cookie.split("; ").filter(function(o){return 0===o.indexOf(n+"=")});try{"undefined"!=typeof localStorage&&null!==localStorage&&(o=localStorage.getItem(n),e=0<t.length?decodeURIComponent(t[0].split("=")[1]):null,o)&&o.indexOf("=")<0&&e&&e!==o&&(document.cookie=n+"="+encodeURIComponent(o)+"; path=/;",window.location.reload())}catch(o){}}();
|
||||
</script>
|
||||
`, "@@X_GRAY_KEY", localStorageGrayKey)
|
||||
newHtml = strings.ReplaceAll(newHtml, "<body>", "<body>\n"+localStr)
|
||||
}
|
||||
return newHtml
|
||||
}
|
||||
|
||||
@@ -3,12 +3,14 @@ package util
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/bmatcuk/doublestar/v4"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/frontend-gray/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestExtractCookieValueByKey(t *testing.T) {
|
||||
func TestGetCookieValue(t *testing.T) {
|
||||
var tests = []struct {
|
||||
cookie, cookieKey, output string
|
||||
}{
|
||||
@@ -21,7 +23,7 @@ func TestExtractCookieValueByKey(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
testName := test.cookie
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
output := ExtractCookieValueByKey(test.cookie, test.cookieKey)
|
||||
output := GetCookieValue(test.cookie, test.cookieKey)
|
||||
assert.Equal(t, test.output, output)
|
||||
})
|
||||
}
|
||||
@@ -106,7 +108,7 @@ func TestPrefixFileRewrite(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPageRequest(t *testing.T) {
|
||||
func TestCheckIsHtmlRequest(t *testing.T) {
|
||||
var tests = []struct {
|
||||
p string
|
||||
output bool
|
||||
@@ -121,30 +123,11 @@ func TestIsPageRequest(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
testPath := test.p
|
||||
t.Run(testPath, func(t *testing.T) {
|
||||
output := IsPageRequest(testPath)
|
||||
output := CheckIsHtmlRequest(testPath)
|
||||
assert.Equal(t, test.output, output)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterGrayWeight(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
input string
|
||||
}{
|
||||
{"demo", `{"grayKey":"userId","rules":[{"name":"inner-user","grayKeyValue":["00000001","00000005"]},{"name":"beta-user","grayKeyValue":["noah","00000003"],"grayTagKey":"level","grayTagValue":["level3","level5"]}],"rewrite":{"host":"frontend-gray-cn-shanghai.oss-cn-shanghai-internal.aliyuncs.com","notFoundUri":"/mfe/app1/dev/404.html","indexRouting":{"/app1":"/mfe/app1/{version}/index.html","/":"/mfe/app1/{version}/index.html"},"fileRouting":{"/":"/mfe/app1/{version}","/app1":"/mfe/app1/{version}"}},"baseDeployment":{"version":"dev"},"grayDeployments":[{"name":"beta-user","version":"0.0.1","backendVersion":"beta","enabled":true,"weight":50}]}`},
|
||||
}
|
||||
for _, test := range tests {
|
||||
testName := test.name
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
grayConfig := &config.GrayConfig{}
|
||||
config.JsonToGrayConfig(gjson.Parse(test.input), grayConfig)
|
||||
result := FilterGrayWeight(grayConfig, "base", "1.0.1", "192.168.1.1")
|
||||
t.Logf("result-----: %v", result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplaceHtml(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
@@ -158,8 +141,26 @@ func TestReplaceHtml(t *testing.T) {
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
grayConfig := &config.GrayConfig{}
|
||||
config.JsonToGrayConfig(gjson.Parse(test.input), grayConfig)
|
||||
result := InjectContent(grayConfig.Html, grayConfig.Injection)
|
||||
result := InjectContent(grayConfig.Html, grayConfig.Injection, "")
|
||||
t.Logf("result-----: %v", result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsIndexRequest(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
input string
|
||||
output bool
|
||||
}{
|
||||
{"/api/user.json", "/api/**", true},
|
||||
{"/api/blade-auth/oauth/captcha", "/api/**", true},
|
||||
}
|
||||
for _, test := range tests {
|
||||
testName := test.name
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
matchResult, _ := doublestar.Match(test.input, testName)
|
||||
assert.Equal(t, test.output, matchResult)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,9 @@ func claimsToHeader(claims map[string]any, cth []cfg.ClaimsToHeader) {
|
||||
if v, ok := claims[cth[i].Claim]; ok {
|
||||
if *cth[i].Override {
|
||||
proxywasm.ReplaceHttpRequestHeader(cth[i].Header, fmt.Sprint(v))
|
||||
} else {
|
||||
proxywasm.AddHttpRequestHeader(cth[i].Header, fmt.Sprint(v))
|
||||
}
|
||||
proxywasm.AddHttpRequestHeader(cth[i].Header, fmt.Sprint(v))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# MCP Server Implementation Guide
|
||||
|
||||
> **🌟 Try it now!** Experience Higress-hosted Remote MCP Servers at [https://mcp.higress.ai/](https://mcp.higress.ai/). This platform allows you to see firsthand how Higress can host and manage Remote MCP Servers.
|
||||
>
|
||||
> 
|
||||
|
||||
## Background
|
||||
|
||||
Higress, as an Envoy-based API gateway, supports hosting MCP Servers through its plugin mechanism. MCP (Model Context Protocol) is essentially an AI-friendly API that enables AI Agents to more easily call various tools and services. Higress provides unified capabilities for authentication, authorization, rate limiting, and observability for tool calls, simplifying the development and deployment of AI applications.
|
||||
@@ -15,6 +19,10 @@
|
||||
|
||||
This guide explains how to implement a Model Context Protocol (MCP) server using the Higress WASM Go SDK. MCP servers provide tools and resources that extend the capabilities of AI assistants.
|
||||
|
||||
[**MCP Server QuickStart**](https://higress.cn/en/ai/mcp-quick-start/)
|
||||
|
||||
[**Wasm Plugin Hub**](https://higress.cn/en/plugin/)
|
||||
|
||||
## Overview
|
||||
|
||||
An MCP server is a standalone application that communicates with AI assistants through the Model Context Protocol. It can provide:
|
||||
|
||||
@@ -15,6 +15,11 @@
|
||||
|
||||
下面介绍如何使用 Higress WASM Go SDK 实现 Model Context Protocol (MCP) 服务器。MCP 服务器提供工具和资源,扩展 AI 助手的能力。
|
||||
|
||||
[**MCP Server QuickStart**](https://higress.cn/en/ai/mcp-quick-start/)
|
||||
|
||||
[**Wasm Plugin Hub**](https://higress.cn/en/plugin/)
|
||||
|
||||
|
||||
## 概述
|
||||
|
||||
MCP 服务器是一个独立的应用程序,通过 Model Context Protocol 与 AI 助手通信。它可以提供:
|
||||
|
||||
@@ -6,9 +6,11 @@ replace quark-search => ../quark-search
|
||||
|
||||
replace amap-tools => ../amap-tools
|
||||
|
||||
replace github.com/alibaba/higress/plugins/wasm-go => /Users/zhangty/tmp/higress/plugins/wasm-go
|
||||
|
||||
require (
|
||||
amap-tools v0.0.0-00010101000000-000000000000
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250331125043-e9661a6dabda
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250417132640-fb2e8d5157ad
|
||||
quark-search v0.0.0-00010101000000-000000000000
|
||||
)
|
||||
|
||||
@@ -20,8 +22,8 @@ require (
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/higress-group/gjson_template v0.0.0-20250331062947-760bb2f96985 // indirect
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250323151219-d75620c61711 // indirect
|
||||
github.com/higress-group/gjson_template v0.0.0-20250413075336-4c4161ed428b // indirect
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250402062734-d50d98c305f0 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/invopop/jsonschema v0.13.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
|
||||
@@ -6,8 +6,6 @@ github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+
|
||||
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
|
||||
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250331125043-e9661a6dabda h1:kvubg2BmbBfw6CtqEvTMsiU/UdEII/DYyKFYwZ3+TP0=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250331125043-e9661a6dabda/go.mod h1:R/g1lYl9ToNltcs01QbOPhZG/h1klHcmjGaowyeWdEI=
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
@@ -20,10 +18,10 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/gjson_template v0.0.0-20250331062947-760bb2f96985 h1:rOxn1GyVZGphQ1GeE1bxSCtRNxtNLzE9KpA5Zyq5Ui0=
|
||||
github.com/higress-group/gjson_template v0.0.0-20250331062947-760bb2f96985/go.mod h1:rU3M+Tq5VrQOo0dxpKHGb03Ty0sdWIZfAH+YCOACx/Y=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250323151219-d75620c61711 h1:n5sZwSZWQ5uKS69hu50/0gliTFrIJ1w+g/FSdIIiZIs=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250323151219-d75620c61711/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/gjson_template v0.0.0-20250413075336-4c4161ed428b h1:rRI9+ThQbe+nw4jUiYEyOFaREkXCMMW9k1X2gy2d6pE=
|
||||
github.com/higress-group/gjson_template v0.0.0-20250413075336-4c4161ed428b/go.mod h1:rU3M+Tq5VrQOo0dxpKHGb03Ty0sdWIZfAH+YCOACx/Y=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250402062734-d50d98c305f0 h1:Ta+RBsZYML3hjoenbGJoS2L6aWJN+hqlxKoqzj/Y2SY=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250402062734-d50d98c305f0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
|
||||
|
||||
@@ -2,22 +2,31 @@ module amap-tools
|
||||
|
||||
go 1.24.1
|
||||
|
||||
require (
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250331125043-e9661a6dabda
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
)
|
||||
require github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250407124215-3431eeb8d374
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.1 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.3.0 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250323151219-d75620c61711 // indirect
|
||||
github.com/higress-group/gjson_template v0.0.0-20250331062947-760bb2f96985 // indirect
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250402062734-d50d98c305f0 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/invopop/jsonschema v0.13.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/spf13/cast v1.7.0 // indirect
|
||||
github.com/tidwall/gjson v1.18.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/resp v0.1.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
golang.org/x/crypto v0.26.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -1,37 +1,61 @@
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250329145934-61b36a20cd9c h1:giBV8e7p0qxRdBsCu4AjXbpUI8sNiGkEtPNWEXf6fA4=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250329145934-61b36a20cd9c/go.mod h1:csP9Mpkc+gVgbZsizCdcYSy0LJrQA+//RcnZBInyknc=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250330032733-a3f709c6bf3c h1:7NUKWzu/HH1OCHJEnsogHDFjf5GXcagv489qN6uudjw=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250330032733-a3f709c6bf3c/go.mod h1:csP9Mpkc+gVgbZsizCdcYSy0LJrQA+//RcnZBInyknc=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250331125043-e9661a6dabda h1:kvubg2BmbBfw6CtqEvTMsiU/UdEII/DYyKFYwZ3+TP0=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250331125043-e9661a6dabda/go.mod h1:R/g1lYl9ToNltcs01QbOPhZG/h1klHcmjGaowyeWdEI=
|
||||
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||
github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
|
||||
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
|
||||
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250407124215-3431eeb8d374 h1:Ht+XEuYcuytDa6YkgTXR/94h+/XAafX0GhGXcnr9siw=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250407124215-3431eeb8d374/go.mod h1:nAmuA22tHQhn8to3y980Ut7FFv/Ayjj/B7n/F8Wf5JY=
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250323151219-d75620c61711 h1:n5sZwSZWQ5uKS69hu50/0gliTFrIJ1w+g/FSdIIiZIs=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250323151219-d75620c61711/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/gjson_template v0.0.0-20250331062947-760bb2f96985 h1:rOxn1GyVZGphQ1GeE1bxSCtRNxtNLzE9KpA5Zyq5Ui0=
|
||||
github.com/higress-group/gjson_template v0.0.0-20250331062947-760bb2f96985/go.mod h1:rU3M+Tq5VrQOo0dxpKHGb03Ty0sdWIZfAH+YCOACx/Y=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250402062734-d50d98c305f0 h1:Ta+RBsZYML3hjoenbGJoS2L6aWJN+hqlxKoqzj/Y2SY=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250402062734-d50d98c305f0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
|
||||
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
|
||||
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94=
|
||||
github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
@@ -39,6 +63,8 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
@@ -37,7 +37,7 @@ type TransitIntegratedRequest struct {
|
||||
}
|
||||
|
||||
func (t TransitIntegratedRequest) Description() string {
|
||||
return "公交路径规划 API 可以根据用户起终点经纬度坐标规划综合各类公共(火车、公交、地铁)交通方式的通勤方案,并且返回通勤方案的数据,跨城场景下必须传起点城市与终点城市"
|
||||
return "公交路径规划 API 可以根据用户起终点经纬度坐标规划综合各类公共(火车、公交、地铁)交通方式的通勤方案,并且返回通勤方案的数据,跨城场景下必须传起点城市与终点城市, 起点城市名称可以通过基于ip定位位置的mcp工具获取"
|
||||
}
|
||||
|
||||
func (t TransitIntegratedRequest) InputSchema() map[string]any {
|
||||
|
||||
@@ -35,7 +35,7 @@ type GeoRequest struct {
|
||||
}
|
||||
|
||||
func (t GeoRequest) Description() string {
|
||||
return "将详细的结构化地址转换为经纬度坐标。支持对地标性名胜景区、建筑物名称解析为经纬度坐标"
|
||||
return "将详细的结构化地址转换为经纬度坐标。支持对地标性名胜景区、建筑物名称解析为经纬度坐标, 城市名称可以通过基于ip定位位置的mcp工具获取"
|
||||
}
|
||||
|
||||
func (t GeoRequest) InputSchema() map[string]any {
|
||||
|
||||
@@ -20,21 +20,23 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"amap-tools/config"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/mcp/server"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/mcp/utils"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
)
|
||||
|
||||
var _ server.Tool = IPLocationRequest{}
|
||||
|
||||
type IPLocationRequest struct {
|
||||
IP string `json:"ip" jsonschema_description:"IP地址"`
|
||||
IP string `json:"ip" jsonschema_description:"IP地址,获取不到则填写unknow,服务端将根据socket地址来获取IP"`
|
||||
}
|
||||
|
||||
func (t IPLocationRequest) Description() string {
|
||||
return "IP 定位根据用户输入的 IP 地址,定位 IP 的所在位置"
|
||||
return "通过IP定位所在的国家和城市等位置信息"
|
||||
}
|
||||
|
||||
func (t IPLocationRequest) InputSchema() map[string]any {
|
||||
@@ -53,7 +55,19 @@ func (t IPLocationRequest) Call(ctx server.HttpContext, s server.Server) error {
|
||||
if serverConfig.ApiKey == "" {
|
||||
return errors.New("amap API-KEY is not configured")
|
||||
}
|
||||
|
||||
if t.IP == "" || strings.Contains(t.IP, "unknow") {
|
||||
var bs []byte
|
||||
var ipStr string
|
||||
fromHeader := false
|
||||
bs, _ = proxywasm.GetProperty([]string{"source", "address"})
|
||||
if len(bs) > 0 {
|
||||
ipStr = string(bs)
|
||||
} else {
|
||||
ipStr, _ = proxywasm.GetHttpRequestHeader("x-forwarded-for")
|
||||
fromHeader = true
|
||||
}
|
||||
t.IP = parseIP(ipStr, fromHeader)
|
||||
}
|
||||
url := fmt.Sprintf("https://restapi.amap.com/v3/ip?ip=%s&key=%s&source=ts_mcp", url.QueryEscape(t.IP), serverConfig.ApiKey)
|
||||
return ctx.RouteCall(http.MethodGet, url,
|
||||
[][2]string{{"Accept", "application/json"}}, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
@@ -64,3 +78,21 @@ func (t IPLocationRequest) Call(ctx server.HttpContext, s server.Server) error {
|
||||
utils.SendMCPToolTextResult(ctx, string(responseBody))
|
||||
})
|
||||
}
|
||||
|
||||
// parseIP 解析IP
|
||||
func parseIP(source string, fromHeader bool) string {
|
||||
|
||||
if fromHeader {
|
||||
source = strings.Split(source, ",")[0]
|
||||
}
|
||||
source = strings.Trim(source, " ")
|
||||
if strings.Contains(source, ".") {
|
||||
// parse ipv4
|
||||
return strings.Split(source, ":")[0]
|
||||
}
|
||||
//parse ipv6
|
||||
if strings.Contains(source, "]") {
|
||||
return strings.Split(source, "]")[0][1:]
|
||||
}
|
||||
return source
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ type TextSearchRequest struct {
|
||||
}
|
||||
|
||||
func (t TextSearchRequest) Description() string {
|
||||
return "关键词搜,根据用户传入关键词,搜索出相关的POI"
|
||||
return "关键词搜,根据用户传入关键词,搜索出相关的POI,城市名称可以通过基于ip定位位置的mcp工具获取"
|
||||
}
|
||||
|
||||
func (t TextSearchRequest) InputSchema() map[string]any {
|
||||
|
||||
@@ -34,7 +34,7 @@ type WeatherRequest struct {
|
||||
}
|
||||
|
||||
func (t WeatherRequest) Description() string {
|
||||
return "根据城市名称或者标准adcode查询指定城市的天气"
|
||||
return "根据城市名称或者标准adcode查询指定城市的天气,城市名称可以通过基于ip定位位置的mcp工具获取"
|
||||
}
|
||||
|
||||
func (t WeatherRequest) InputSchema() map[string]any {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# Agricultural Product Price Inquiry
|
||||
|
||||
The APP Code required for API authentication can be applied for on the Alibaba Cloud API Marketplace: https://market.aliyun.com/apimarket/detail/cmapi00044839
|
||||
|
||||
# Agricultural Product Price Inquiry Server Configuration Document
|
||||
|
||||
This document aims to provide users with an overview of the basic functions of the Agricultural Product Price Inquiry MCP (Market Cloud Platform) server and a detailed introduction to the tools used. Through this document, users can learn how to use these tools to obtain agricultural product price information for specific regions and categories.
|
||||
|
||||
## Function Overview
|
||||
|
||||
The `agricultural-product-price-query` service is specifically designed to fetch the latest market price information for agricultural products from the Alibaba Cloud API Marketplace. It allows users to query relevant price statistical data based on specified geographic regions, product categories, and dates. This service is very useful for agricultural practitioners, research institutions, or any stakeholders who need to understand the latest market price trends.
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# 农产品价格查询
|
||||
|
||||
API认证需要的APP Code请在阿里云API市场申请: https://market.aliyun.com/apimarket/detail/cmapi00064763
|
||||
|
||||
## 什么是云市场API MCP服务
|
||||
|
||||
阿里云云市场是生态伙伴的交易服务平台,我们致力于为合作伙伴提供覆盖上云、商业化和售卖的全链路服务,帮助客户高效获取、部署和管理优质生态产品。云市场的API服务涵盖以下几个类目:应用开发、身份验证与金融、车辆交通与物流、企业服务、短信与运营商、AI应用与OCR、生活服务。
|
||||
云市场API依托Higress提供MCP服务,您只需在云市场完成订阅并获取AppCode,通过Higress MCP Server进行配置,即可无缝集成云市场API服务。
|
||||
|
||||
## 如何在使用云市场API MCP服务
|
||||
|
||||
1. 进入API详情页,订阅该API。您可以优先使用免费试用。
|
||||
2. 前往云市场用户控制台,使用阿里云账号登陆后查看已订阅API服务的AppCode,并配置到Higress MCP Server的配置中。注意:在阿里云市场订阅API服务后,您将获得AppCode。对于您订阅的所有API服务,此AppCode是相同的,您只需使用这一个AppCode即可访问所有已订阅的API服务。
|
||||
3. 云市场用户控制台会实时展示已订阅的预付费API服务的可用额度,如您免费试用额度已用完,您可以选择重新订阅。
|
||||
|
||||
# 农产品价格查询服务器配置文档
|
||||
|
||||
本文件旨在为用户提供关于农产品价格查询MCP(Market Cloud Platform)服务器的基本功能概述以及所使用工具的具体介绍。通过这份文档,用户可以了解到如何利用这些工具来获取特定地区及类目的农产品价格信息。
|
||||
|
||||
## 功能简介
|
||||
|
||||
`agricultural-product-price-query` 服务专门设计用于从阿里云API市场获取最新的农产品市场价格信息。它允许用户根据指定的地理区域、产品类别和日期查询相关的价格统计数据。此服务对于农业从业者、研究机构或任何需要了解最新市场价格趋势的利益相关者都非常有用。
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
{
|
||||
"info": {
|
||||
"description": "惠农行情数据接口可提供全国31个省级行政区、2818个县级行政区的蔬菜、水果、畜禽、水产等3000多个品类,2万多个常见农产品的价格数据服务、历史数据最早可追溯至2013年。惠农行情大数据是基于惠农网的线上电商交易平台,线下行情官系统和农业专家团队所发布的农产品信息,经标准化清洗、分类和数据库建设而形成的农业全产业链专业数据库;是农产品生产经营管理监测、分析、预警的有力工具。",
|
||||
"title": "农产品价格行情数据接口",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"openapi": "3.0.1",
|
||||
"paths": {
|
||||
"/aliyun/market/category/detail": {
|
||||
"get": {
|
||||
"operationId": "类目数据价格行情",
|
||||
"summary": "阿里云api市场-1日数据统计信息(类目)",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "中国地理行政区(县级市),支持地区下载路径https://files.cnhnb.com/area_list.xlsx",
|
||||
"example": "岳麓区",
|
||||
"in": "query",
|
||||
"name": "areaName",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "农产品三级类目名称,具体名单参照https://files.cnhnb.com/category_list.xlsx",
|
||||
"example": "大米",
|
||||
"in": "query",
|
||||
"name": "categoryName",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "要查询的日期。 例子: 2020-12-25",
|
||||
"example": "2020-12-22",
|
||||
"in": "query",
|
||||
"name": "date",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"description": "响应状态码",
|
||||
"example": 0
|
||||
},
|
||||
"msg": {
|
||||
"type": "string",
|
||||
"description": "响应消息",
|
||||
"example": "success"
|
||||
},
|
||||
"traceId": {
|
||||
"type": "string",
|
||||
"description": "跟踪ID",
|
||||
"example": "77e70b08b4ca064d"
|
||||
},
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cateName": {
|
||||
"type": "string",
|
||||
"description": "类别名称",
|
||||
"example": "大米"
|
||||
},
|
||||
"breedName": {
|
||||
"type": "string",
|
||||
"description": "品种名称",
|
||||
"example": "有机米"
|
||||
},
|
||||
"unit": {
|
||||
"type": "string",
|
||||
"description": "单位",
|
||||
"example": "斤"
|
||||
},
|
||||
"nowAvgPrice": {
|
||||
"type": "number",
|
||||
"description": "当前平均价格",
|
||||
"example": 2.8
|
||||
},
|
||||
"provinceName": {
|
||||
"type": "string",
|
||||
"description": "省份名称",
|
||||
"example": "湖南省"
|
||||
},
|
||||
"cityName": {
|
||||
"type": "string",
|
||||
"description": "城市名称",
|
||||
"example": "长沙市"
|
||||
},
|
||||
"areaName": {
|
||||
"type": "string",
|
||||
"description": "区域名称",
|
||||
"example": "岳麓区"
|
||||
},
|
||||
"marketDate": {
|
||||
"type": "string",
|
||||
"description": "市场日期",
|
||||
"example": "2020-12-24T00:00:00Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "成功响应"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "https://agroprice.market.alicloudapi.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
server:
|
||||
name: agricultural-product-price-query
|
||||
config:
|
||||
appCode: ""
|
||||
tools:
|
||||
- name: avg-price
|
||||
description: 地区均价
|
||||
args:
|
||||
- name: city
|
||||
description: 地级市名称
|
||||
type: string
|
||||
position: body
|
||||
- name: code
|
||||
description: 农产品代码,通过【支持产品查询】接口获取的code
|
||||
type: string
|
||||
required: true
|
||||
position: body
|
||||
- name: province
|
||||
description: 省份名称,暂不支持港澳台地区,省份名字不带“省”字,譬如:浙江省,输入浙江
|
||||
type: string
|
||||
required: true
|
||||
position: body
|
||||
requestTemplate:
|
||||
url: https://lhncpcx.market.alicloudapi.com/agricultural/products/region/average-price
|
||||
method: POST
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/x-www-form-urlencoded
|
||||
- key: Authorization
|
||||
value: APPCODE {{.config.appCode}}
|
||||
- key: X-Ca-Nonce
|
||||
value: '{{uuidv4}}'
|
||||
responseTemplate:
|
||||
prependBody: |+
|
||||
# API Response Information
|
||||
|
||||
Below is the response from an API call. To help you understand the data, I've provided:
|
||||
|
||||
1. A detailed description of all fields in the response structure
|
||||
2. The complete API response
|
||||
|
||||
## Response Structure
|
||||
|
||||
> Content-Type: application/json
|
||||
|
||||
- **code**: (Type: integer)
|
||||
- **data**: (Type: object)
|
||||
- **data.avg**: 参考均价 (Type: string)
|
||||
- **data.sample**: 样本数量 (Type: string)
|
||||
- **data.unit**: 计价单位 (Type: string)
|
||||
- **msg**: (Type: string)
|
||||
- **taskNo**: (Type: string)
|
||||
|
||||
## Original Response
|
||||
|
||||
- name: product-query
|
||||
description: 支持产品查询
|
||||
args:
|
||||
- name: name
|
||||
description: 农产品名称,支持模糊查询
|
||||
type: string
|
||||
position: body
|
||||
- name: type
|
||||
description: 农产品种类,1表示畜产,2表示水产,3代表粮油,4代表果品,5代表蔬菜
|
||||
type: string
|
||||
required: true
|
||||
position: body
|
||||
requestTemplate:
|
||||
url: https://lhncpcx.market.alicloudapi.com/agricultural/products/query
|
||||
method: POST
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/x-www-form-urlencoded
|
||||
- key: Authorization
|
||||
value: APPCODE {{.config.appCode}}
|
||||
- key: X-Ca-Nonce
|
||||
value: '{{uuidv4}}'
|
||||
responseTemplate:
|
||||
prependBody: |+
|
||||
# API Response Information
|
||||
|
||||
Below is the response from an API call. To help you understand the data, I've provided:
|
||||
|
||||
1. A detailed description of all fields in the response structure
|
||||
2. The complete API response
|
||||
|
||||
## Response Structure
|
||||
|
||||
> Content-Type: application/json
|
||||
|
||||
- **code**: 接口返回码【注意:不等于HTTP响应状态码】 (Type: integer)
|
||||
- **data**: (Type: array)
|
||||
- **data[].code**: 农产品代码 (Type: string)
|
||||
- **data[].genus**: 产品种类 (Type: string)
|
||||
- **data[].genusCode**: 产品种类代码 (Type: string)
|
||||
- **data[].name**: 名称 (Type: string)
|
||||
- **msg**: 接口返回码对应的描述信息 (Type: string)
|
||||
- **taskNo**: 任务订单号【可反馈服务商复核对应订单】 (Type: string)
|
||||
|
||||
## Original Response
|
||||
|
||||
- name: newest-price
|
||||
description: 最新参考价
|
||||
args:
|
||||
- name: city
|
||||
description: 地级市名称
|
||||
type: string
|
||||
position: body
|
||||
- name: code
|
||||
description: 农产品代码,通过【支持产品查询】接口获取的code
|
||||
type: string
|
||||
required: true
|
||||
position: body
|
||||
- name: province
|
||||
description: 省份名称,暂不支持港澳台地区,省份名字不带“省”字,譬如:浙江省,输入浙江
|
||||
type: string
|
||||
required: true
|
||||
position: body
|
||||
requestTemplate:
|
||||
url: https://lhncpcx.market.alicloudapi.com/agricultural/products/lastest/reference-price
|
||||
method: POST
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/x-www-form-urlencoded
|
||||
- key: Authorization
|
||||
value: APPCODE {{.config.appCode}}
|
||||
- key: X-Ca-Nonce
|
||||
value: '{{uuidv4}}'
|
||||
responseTemplate:
|
||||
prependBody: |+
|
||||
# API Response Information
|
||||
|
||||
Below is the response from an API call. To help you understand the data, I've provided:
|
||||
|
||||
1. A detailed description of all fields in the response structure
|
||||
2. The complete API response
|
||||
|
||||
## Response Structure
|
||||
|
||||
> Content-Type: application/json
|
||||
|
||||
- **code**: 接口返回码【注意:不等于HTTP响应状态码】 (Type: integer)
|
||||
- **data**: (Type: array)
|
||||
- **data[].address**: 价格获取地址(单位) (Type: string)
|
||||
- **data[].date**: 更新时间 (Type: string)
|
||||
- **data[].money**: 价格 (Type: string)
|
||||
- **data[].unit**: 单位 (Type: string)
|
||||
- **msg**: 接口返回码对应的描述信息 (Type: string)
|
||||
- **taskNo**: 任务订单号【可反馈服务商复核对应订单】 (Type: string)
|
||||
|
||||
## Original Response
|
||||
37
plugins/wasm-go/mcp-servers/mcp-book-query/README.md
Normal file
37
plugins/wasm-go/mcp-servers/mcp-book-query/README.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Book Query
|
||||
|
||||
The APP Code required for API authentication can be applied for at the Alibaba Cloud API Marketplace: https://market.aliyun.com/apimarket/detail/cmapi00066353
|
||||
|
||||
## Overview
|
||||
|
||||
The `book-query` service is primarily used to query detailed information about books using their ISBN numbers. This service accepts a request containing an ISBN number and sends a request to an external API to retrieve all available data related to that ISBN, including but not limited to the author, publication date, and publisher. This feature is very useful for library management systems, online bookstores, and other applications that need to quickly look up book details based on ISBN.
|
||||
|
||||
## Tool Introduction
|
||||
|
||||
### ISBN Number Query
|
||||
- **Purpose**: This tool allows users to input an ISBN number and obtain comprehensive information about the corresponding book.
|
||||
- **Use Cases**:
|
||||
- When developers or system integrators need to provide users with a book search function based on ISBN.
|
||||
- For those who want to quickly learn all relevant information about a book (such as author, edition, price, etc.) through its ISBN.
|
||||
- As a basic data query method when building platforms that involve managing or selling a large number of books.
|
||||
|
||||
#### Parameter Description
|
||||
- `isbn`: The ISBN number provided by the user, which is a string. This is the only required piece of information in the query process, used to locate a specific book record.
|
||||
|
||||
#### Request Example
|
||||
- **URL**: https://lhisbnshcx.market.alicloudapi.com/isbn/query
|
||||
- **Method**: POST
|
||||
- **Headers**:
|
||||
- Content-Type: application/x-www-form-urlencoded
|
||||
- Authorization: Use the APP CODE for authentication
|
||||
- X-Ca-Nonce: A generated random UUID value to ensure the uniqueness of each request
|
||||
|
||||
#### Response Structure
|
||||
The response will be returned in JSON format and will include the following main fields:
|
||||
- `code`: The status code returned by the interface, different from the HTTP status code.
|
||||
- `data`: An object containing specific book information.
|
||||
- `details[]`: An array of specific book details, where each element represents a record and includes various attributes such as author, title, and publisher.
|
||||
- `msg`: A description message corresponding to the returned status code.
|
||||
- `taskNo`: The task order number, which can be used for subsequent service provider verification.
|
||||
|
||||
This is a brief overview of the MCP server and its components mentioned in the YAML configuration file. Through these tools and services, effective querying and management of book information can be achieved.
|
||||
48
plugins/wasm-go/mcp-servers/mcp-book-query/README_ZH.md
Normal file
48
plugins/wasm-go/mcp-servers/mcp-book-query/README_ZH.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# 图书查询
|
||||
|
||||
API认证需要的APP Code请在阿里云API市场申请: https://market.aliyun.com/apimarket/detail/cmapi00066353
|
||||
|
||||
## 什么是云市场API MCP服务
|
||||
|
||||
阿里云云市场是生态伙伴的交易服务平台,我们致力于为合作伙伴提供覆盖上云、商业化和售卖的全链路服务,帮助客户高效获取、部署和管理优质生态产品。云市场的API服务涵盖以下几个类目:应用开发、身份验证与金融、车辆交通与物流、企业服务、短信与运营商、AI应用与OCR、生活服务。
|
||||
云市场API依托Higress提供MCP服务,您只需在云市场完成订阅并获取AppCode,通过Higress MCP Server进行配置,即可无缝集成云市场API服务。
|
||||
|
||||
## 如何在使用云市场API MCP服务
|
||||
|
||||
1. 进入API详情页,订阅该API。您可以优先使用免费试用。
|
||||
2. 前往云市场用户控制台,使用阿里云账号登陆后查看已订阅API服务的AppCode,并配置到Higress MCP Server的配置中。注意:在阿里云市场订阅API服务后,您将获得AppCode。对于您订阅的所有API服务,此AppCode是相同的,您只需使用这一个AppCode即可访问所有已订阅的API服务。
|
||||
3. 云市场用户控制台会实时展示已订阅的预付费API服务的可用额度,如您免费试用额度已用完,您可以选择重新订阅。
|
||||
|
||||
## 功能简介
|
||||
|
||||
主要用于通过ISBN书号来查询图书的详细信息。该服务接收一个包含ISBN书号的请求,并向外部API发送请求以获取与该ISBN相关的所有可用数据,包括但不限于作者、出版日期、出版社等信息。此功能对于图书馆管理系统、在线书店以及其他需要根据ISBN快速查找书籍详情的应用非常有用。
|
||||
|
||||
## 工具简介
|
||||
|
||||
### ISBN书号查询
|
||||
- **用途**:此工具允许用户输入ISBN书号并获取关于该书号所对应书籍的全面信息。
|
||||
- **使用场景**:
|
||||
- 当开发者或系统集成者需要为用户提供基于ISBN的书籍检索功能时。
|
||||
- 对于那些希望通过ISBN快速了解一本书籍的所有相关信息(如作者、版本、价格等)的情况。
|
||||
- 在构建涉及大量书籍管理或者销售的平台时作为基础的数据查询手段之一。
|
||||
|
||||
#### 参数说明
|
||||
- `isbn`: 用户必须提供的ISBN书号,类型为字符串。这是查询过程中唯一必需的信息点,用于定位特定的图书记录。
|
||||
|
||||
#### 请求示例
|
||||
- **URL**: https://lhisbnshcx.market.alicloudapi.com/isbn/query
|
||||
- **方法**: POST
|
||||
- **头部信息**:
|
||||
- Content-Type: application/x-www-form-urlencoded
|
||||
- Authorization: 使用APP CODE进行身份验证
|
||||
- X-Ca-Nonce: 生成的随机UUID值,确保每次请求的独特性
|
||||
|
||||
#### 响应结构
|
||||
响应将以JSON格式返回,并包含以下主要字段:
|
||||
- `code`: 接口返回的状态码,不同于HTTP状态码。
|
||||
- `data`: 包含具体图书信息的对象。
|
||||
- `details[]`: 图书的具体细节数组,每个元素代表一条记录,其中包括了诸如作者、标题、出版社等多种属性。
|
||||
- `msg`: 返回的状态码对应的描述信息。
|
||||
- `taskNo`: 任务订单号,可用于后续的服务商复核。
|
||||
|
||||
以上是针对YAML配置文件中提到的MCP服务器及其组件的一个简要概述。通过这些工具和服务,可以有效地实现对图书信息的查询和管理。
|
||||
191
plugins/wasm-go/mcp-servers/mcp-book-query/api.json
Normal file
191
plugins/wasm-go/mcp-servers/mcp-book-query/api.json
Normal file
@@ -0,0 +1,191 @@
|
||||
{
|
||||
"info": {
|
||||
"description": "【感受科技的温度】ISBN标准书号查询-ISBN书号查询-ISBN图书查询-ISBN图书详情信息查询-图书编号查询 —— 输入ISBN书号查询图书详情信息,返回包含书名、作者、出版社、价格、出版日期、印次、装帧方式、语种、摘要等详细图书信息。【怜花数科】",
|
||||
"title": "ISBN标准书号查询-ISBN书号查询-ISBN图书查询-ISBN图书详情信息查询-图书编号查询",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"openapi": "3.0.1",
|
||||
"paths": {
|
||||
"/isbn/query": {
|
||||
"post": {
|
||||
"operationId": "ISBN书号查询",
|
||||
"summary": "ISBN书号查询",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/x-www-form-urlencoded": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"isbn": {
|
||||
"description": "ISBN书号",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"isbn"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"description": "接口返回码【注意:不等于HTTP响应状态码】"
|
||||
},
|
||||
"msg": {
|
||||
"type": "string",
|
||||
"description": "接口返回码对应的描述信息"
|
||||
},
|
||||
"taskNo": {
|
||||
"type": "string",
|
||||
"description": "任务订单号【可反馈服务商复核对应订单】"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"details": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "书名"
|
||||
},
|
||||
"author": {
|
||||
"type": "string",
|
||||
"description": "作者、编者、译者信息"
|
||||
},
|
||||
"publisher": {
|
||||
"type": "string",
|
||||
"description": "出版社"
|
||||
},
|
||||
"pubDate": {
|
||||
"type": "string",
|
||||
"description": "出版日期"
|
||||
},
|
||||
"pubPlace": {
|
||||
"type": "string",
|
||||
"description": "出版地"
|
||||
},
|
||||
"isbn": {
|
||||
"type": "string",
|
||||
"description": "13位isbn号"
|
||||
},
|
||||
"isbn10": {
|
||||
"type": "string",
|
||||
"description": "10位isbn号"
|
||||
},
|
||||
"price": {
|
||||
"type": "string",
|
||||
"description": "定价"
|
||||
},
|
||||
"genus": {
|
||||
"type": "string",
|
||||
"description": "中图分类号"
|
||||
},
|
||||
"levelNum": {
|
||||
"type": "string",
|
||||
"description": "读者评分"
|
||||
},
|
||||
"heatNum": {
|
||||
"type": "string",
|
||||
"description": "图书热度"
|
||||
},
|
||||
"format": {
|
||||
"type": "string",
|
||||
"description": "纸张开数"
|
||||
},
|
||||
"binding": {
|
||||
"type": "string",
|
||||
"description": "装帧信息"
|
||||
},
|
||||
"page": {
|
||||
"type": "string",
|
||||
"description": "页数"
|
||||
},
|
||||
"wordNum": {
|
||||
"type": "string",
|
||||
"description": "字数"
|
||||
},
|
||||
"edition": {
|
||||
"type": "string",
|
||||
"description": "版次"
|
||||
},
|
||||
"yinci": {
|
||||
"type": "string",
|
||||
"description": "印次"
|
||||
},
|
||||
"paper": {
|
||||
"type": "string",
|
||||
"description": "书籍纸张类型"
|
||||
},
|
||||
"language": {
|
||||
"type": "string",
|
||||
"description": "语言"
|
||||
},
|
||||
"keyword": {
|
||||
"type": "string",
|
||||
"description": "图书关键词"
|
||||
},
|
||||
"img": {
|
||||
"type": "string",
|
||||
"description": "封面链接【提示:图片链接24小时有效,超过失效不可访问】"
|
||||
},
|
||||
"bookCatalog": {
|
||||
"type": "string",
|
||||
"description": "目录"
|
||||
},
|
||||
"gist": {
|
||||
"type": "string",
|
||||
"description": "图书内容简介"
|
||||
},
|
||||
"cipTxt": {
|
||||
"type": "string",
|
||||
"description": "cip信息"
|
||||
},
|
||||
"annotation": {
|
||||
"type": "string",
|
||||
"description": "附注"
|
||||
},
|
||||
"subject": {
|
||||
"type": "string",
|
||||
"description": "主题"
|
||||
},
|
||||
"series": {
|
||||
"type": "string",
|
||||
"description": "丛书信息,非丛书为空"
|
||||
},
|
||||
"batch": {
|
||||
"type": "string",
|
||||
"description": "丛编信息"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "成功"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "https://lhisbnshcx.market.alicloudapi.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
72
plugins/wasm-go/mcp-servers/mcp-book-query/mcp-server.yaml
Normal file
72
plugins/wasm-go/mcp-servers/mcp-book-query/mcp-server.yaml
Normal file
@@ -0,0 +1,72 @@
|
||||
server:
|
||||
name: book-query
|
||||
config:
|
||||
appCode: ""
|
||||
tools:
|
||||
- name: isbn-query
|
||||
description: ISBN书号查询
|
||||
args:
|
||||
- name: isbn
|
||||
description: ISBN书号
|
||||
type: string
|
||||
required: true
|
||||
position: body
|
||||
requestTemplate:
|
||||
url: https://lhisbnshcx.market.alicloudapi.com/isbn/query
|
||||
method: POST
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/x-www-form-urlencoded
|
||||
- key: Authorization
|
||||
value: APPCODE {{.config.appCode}}
|
||||
- key: X-Ca-Nonce
|
||||
value: '{{uuidv4}}'
|
||||
responseTemplate:
|
||||
prependBody: |+
|
||||
# API Response Information
|
||||
|
||||
Below is the response from an API call. To help you understand the data, I've provided:
|
||||
|
||||
1. A detailed description of all fields in the response structure
|
||||
2. The complete API response
|
||||
|
||||
## Response Structure
|
||||
|
||||
> Content-Type: application/json
|
||||
|
||||
- **code**: 接口返回码【注意:不等于HTTP响应状态码】 (Type: integer)
|
||||
- **data**: (Type: object)
|
||||
- **data.details**: (Type: array)
|
||||
- **data.details[].annotation**: 附注 (Type: string)
|
||||
- **data.details[].author**: 作者、编者、译者信息 (Type: string)
|
||||
- **data.details[].batch**: 丛编信息 (Type: string)
|
||||
- **data.details[].binding**: 装帧信息 (Type: string)
|
||||
- **data.details[].bookCatalog**: 目录 (Type: string)
|
||||
- **data.details[].cipTxt**: cip信息 (Type: string)
|
||||
- **data.details[].edition**: 版次 (Type: string)
|
||||
- **data.details[].format**: 纸张开数 (Type: string)
|
||||
- **data.details[].genus**: 中图分类号 (Type: string)
|
||||
- **data.details[].gist**: 图书内容简介 (Type: string)
|
||||
- **data.details[].heatNum**: 图书热度 (Type: string)
|
||||
- **data.details[].img**: 封面链接【提示:图片链接24小时有效,超过失效不可访问】 (Type: string)
|
||||
- **data.details[].isbn**: 13位isbn号 (Type: string)
|
||||
- **data.details[].isbn10**: 10位isbn号 (Type: string)
|
||||
- **data.details[].keyword**: 图书关键词 (Type: string)
|
||||
- **data.details[].language**: 语言 (Type: string)
|
||||
- **data.details[].levelNum**: 读者评分 (Type: string)
|
||||
- **data.details[].page**: 页数 (Type: string)
|
||||
- **data.details[].paper**: 书籍纸张类型 (Type: string)
|
||||
- **data.details[].price**: 定价 (Type: string)
|
||||
- **data.details[].pubDate**: 出版日期 (Type: string)
|
||||
- **data.details[].pubPlace**: 出版地 (Type: string)
|
||||
- **data.details[].publisher**: 出版社 (Type: string)
|
||||
- **data.details[].series**: 丛书信息,非丛书为空 (Type: string)
|
||||
- **data.details[].subject**: 主题 (Type: string)
|
||||
- **data.details[].title**: 书名 (Type: string)
|
||||
- **data.details[].wordNum**: 字数 (Type: string)
|
||||
- **data.details[].yinci**: 印次 (Type: string)
|
||||
- **msg**: 接口返回码对应的描述信息 (Type: string)
|
||||
- **taskNo**: 任务订单号【可反馈服务商复核对应订单】 (Type: string)
|
||||
|
||||
## Original Response
|
||||
|
||||
34
plugins/wasm-go/mcp-servers/mcp-bravesearch/README.md
Normal file
34
plugins/wasm-go/mcp-servers/mcp-bravesearch/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Brave Search MCP Server
|
||||
|
||||
An MCP server implementation that integrates the Brave Search API, providing web and local search capabilities.
|
||||
|
||||
## Features
|
||||
|
||||
- **Web Search**: Supports general queries, news, articles, with pagination and time control
|
||||
- **Local Search**: Find businesses, restaurants, and services with detailed information
|
||||
|
||||
Source code: [https://github.com/modelcontextprotocol/servers/tree/main/src/brave-search](https://github.com/modelcontextprotocol/servers/tree/main/src/brave-search)
|
||||
|
||||
# Usage Guide
|
||||
|
||||
## Get API-KEY
|
||||
|
||||
1. Register for a Brave Search API account [Visit official website](https://brave.com/search/api/)
|
||||
2. Choose a plan (free plan includes 2000 queries per month)
|
||||
3. Generate API key through developer console [Go to console](https://api.search.brave.com/app/keys)
|
||||
|
||||
## Generate SSE URL
|
||||
|
||||
On the MCP Server interface, log in and enter the API-KEY to generate the URL.
|
||||
|
||||
## Configure MCP Client
|
||||
|
||||
On the user's MCP Client interface, add the generated SSE URL to the MCP Server list.
|
||||
|
||||
```json
|
||||
"mcpServers": {
|
||||
"bravesearch": {
|
||||
"url": "http://mcp.higress.ai/mcp-brave-search/{generate_key}",
|
||||
}
|
||||
}
|
||||
```
|
||||
38
plugins/wasm-go/mcp-servers/mcp-bravesearch/README_ZH.md
Normal file
38
plugins/wasm-go/mcp-servers/mcp-bravesearch/README_ZH.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Brave Search MCP Server
|
||||
|
||||
一个集成Brave搜索API的MCP服务器实现,提供网页和本地搜索功能。
|
||||
|
||||
## 功能
|
||||
|
||||
- **网页搜索**:支持通用查询、新闻、文章,具备分页和时效性控制
|
||||
- **本地搜索**:查找带有详细信息的企业、餐厅和服务
|
||||
|
||||
源码地址:[https://github.com/modelcontextprotocol/servers/tree/main/src/brave-search](https://github.com/modelcontextprotocol/servers/tree/main/src/brave-search)
|
||||
|
||||
# 使用教程
|
||||
|
||||
## 获取 API-KEY
|
||||
|
||||
1. 注册Brave搜索API账号 [访问官网](https://brave.com/search/api/)
|
||||
2. 选择套餐(免费套餐每月包含2000次查询)
|
||||
3. 通过开发者控制台生成 API 密钥 [前往控制台](https://api.search.brave.com/app/keys)
|
||||
|
||||
## 生成 SSE URL
|
||||
|
||||
在 MCP Server 界面,登录后输入 API-KEY,生成URL。
|
||||
|
||||
|
||||
|
||||
## 配置 MCP Client
|
||||
|
||||
在用户的 MCP Client 界面,将生成的 SSE URL添加到MCP Server列表中。
|
||||
|
||||
```json
|
||||
"mcpServers": {
|
||||
"bravesearch": {
|
||||
"url": "http://mcp.higress.ai/mcp-brave-search/{generate_key}",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
76
plugins/wasm-go/mcp-servers/mcp-bravesearch/mcp-server.yaml
Normal file
76
plugins/wasm-go/mcp-servers/mcp-bravesearch/mcp-server.yaml
Normal file
@@ -0,0 +1,76 @@
|
||||
server:
|
||||
name: brave-search-server
|
||||
config:
|
||||
apiKey: ""
|
||||
tools:
|
||||
- name: brave_web_search
|
||||
description: "使用Brave Search API进行网页搜索,适用于一般查询、新闻、文章和在线内容。支持分页、内容过滤和新鲜度控制。每次请求最多返回20条结果。"
|
||||
args:
|
||||
- name: query
|
||||
description: "搜索查询(最多400字符,50个词)"
|
||||
type: string
|
||||
required: true
|
||||
- name: count
|
||||
description: "结果数量(1-20,默认10)"
|
||||
type: integer
|
||||
required: false
|
||||
default: 10
|
||||
- name: offset
|
||||
description: "分页偏移量(最大9,默认0)"
|
||||
type: integer
|
||||
required: false
|
||||
default: 0
|
||||
requestTemplate:
|
||||
url: "https://api.search.brave.com/res/v1/web/search"
|
||||
method: GET
|
||||
argsToUrlParam: true
|
||||
headers:
|
||||
- key: Accept
|
||||
value: "application/json"
|
||||
- key: Accept-Encoding
|
||||
value: "gzip"
|
||||
- key: X-Subscription-Token
|
||||
value: "{{.config.apiKey}}"
|
||||
responseTemplate:
|
||||
body: |
|
||||
{{- range $index, $item := .web.results }}
|
||||
## 结果 {{add $index 1}}
|
||||
- **标题**: {{ $item.title }}
|
||||
- **描述**: {{ $item.description }}
|
||||
- **URL**: {{ $item.url }}
|
||||
{{- end }}
|
||||
|
||||
- name: brave_local_search
|
||||
description: "使用Brave的Local Search API搜索本地商家和地点。适用于与物理位置、商家、餐厅、服务等相关的查询。返回详细信息包括商家名称、地址、评分、评论数、电话号码、营业时间等。如果没有本地结果,会自动回退到网页搜索。"
|
||||
args:
|
||||
- name: query
|
||||
description: "本地搜索查询(例如'Central Park附近的披萨')"
|
||||
type: string
|
||||
required: true
|
||||
- name: count
|
||||
description: "结果数量(1-20,默认5)"
|
||||
type: integer
|
||||
required: false
|
||||
default: 5
|
||||
requestTemplate:
|
||||
url: "https://api.search.brave.com/res/v1/web/search"
|
||||
method: GET
|
||||
argsToUrlParam: true
|
||||
headers:
|
||||
- key: Accept
|
||||
value: "application/json"
|
||||
- key: Accept-Encoding
|
||||
value: "gzip"
|
||||
- key: X-Subscription-Token
|
||||
value: "{{.config.apiKey}}"
|
||||
responseTemplate:
|
||||
body: |
|
||||
{{- range $index, $item := .results }}
|
||||
## 结果 {{add $index 1}}
|
||||
- **名称**: {{ $item.name }}
|
||||
- **地址**: {{ $item.address.streetAddress }}, {{ $item.address.addressLocality }}, {{ $item.address.addressRegion }} {{ $item.address.postalCode }}
|
||||
- **电话**: {{ $item.phone }}
|
||||
- **评分**: {{ $item.rating.ratingValue }} ({{ $item.rating.ratingCount }} 条评论)
|
||||
- **价格范围**: {{ $item.priceRange }}
|
||||
- **营业时间**: {{ join $item.openingHours ", " }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,45 @@
|
||||
# Enterprise Credit Rating
|
||||
|
||||
The APP Code required for API authentication can be applied for on the Alibaba Cloud API Marketplace: https://market.aliyun.com/apimarket/detail/cmapi00067564
|
||||
|
||||
# MCP Server Configuration Overview
|
||||
|
||||
## Function Overview
|
||||
This MCP server is primarily used to handle query requests related to enterprise credit ratings. By interacting with specific APIs available on the Alibaba Cloud Marketplace, this service can return detailed credit rating information of a company based on provided information such as the company name, registration number, or social credit code. This allows users to conveniently obtain the latest credit status of target companies, including but not limited to bond credit ratings, entity ratings, and rating outlooks.
|
||||
|
||||
## Tool Overview
|
||||
|
||||
### Enterprise Credit Rating
|
||||
- **Purpose**: Provides an interface for querying the credit rating information of specified enterprises.
|
||||
- **Use Cases**: Suitable for scenarios where a comprehensive understanding of an enterprise's credit status is needed, such as when financial institutions decide whether to provide loans to a company; or when suppliers investigate the creditworthiness of potential clients before choosing partners.
|
||||
|
||||
#### Parameter Description
|
||||
- **keyword** (Required): The search keyword, which can be the company name, registration number, or social credit code.
|
||||
- **pageNum**: The page number in the request pagination, defaulting to 1.
|
||||
- **pageSize**: The number of result items per page, with a default value of 10.
|
||||
|
||||
#### Request Template
|
||||
- **URL**: `https://slyhonour.market.alicloudapi.com/credit/rating`
|
||||
- **Method**: GET
|
||||
- **Headers**:
|
||||
- Authorization: Use the application code as the authentication method
|
||||
- X-Ca-Nonce: A unique identifier generated automatically
|
||||
|
||||
#### Response Structure
|
||||
- **code**: Status code
|
||||
- **data**:
|
||||
- **items[]**:
|
||||
- alias: Rating company alias
|
||||
- bondCreditLevel: Bond credit level
|
||||
- gid: Global ID
|
||||
- logo: Rating company logo
|
||||
- ratingCompanyName: Rating company name
|
||||
- ratingDate: Rating date
|
||||
- ratingOutlook: Rating outlook
|
||||
- subjectLevel: Subject level
|
||||
- orderNo: Order number
|
||||
- total: Total number of records
|
||||
- **msg**: Message content returned
|
||||
- **success**: Boolean flag indicating whether the operation was successful
|
||||
|
||||
This tool provides detailed enterprise credit assessment data, helping users to quickly and accurately evaluate a company's financial health and its ability to repay debts.
|
||||
@@ -0,0 +1,56 @@
|
||||
# 企业信用评级
|
||||
|
||||
API认证需要的APP Code请在阿里云API市场申请: https://market.aliyun.com/apimarket/detail/cmapi00067564
|
||||
|
||||
## 什么是云市场API MCP服务
|
||||
|
||||
阿里云云市场是生态伙伴的交易服务平台,我们致力于为合作伙伴提供覆盖上云、商业化和售卖的全链路服务,帮助客户高效获取、部署和管理优质生态产品。云市场的API服务涵盖以下几个类目:应用开发、身份验证与金融、车辆交通与物流、企业服务、短信与运营商、AI应用与OCR、生活服务。
|
||||
云市场API依托Higress提供MCP服务,您只需在云市场完成订阅并获取AppCode,通过Higress MCP Server进行配置,即可无缝集成云市场API服务。
|
||||
|
||||
## 如何在使用云市场API MCP服务
|
||||
|
||||
1. 进入API详情页,订阅该API。您可以优先使用免费试用。
|
||||
2. 前往云市场用户控制台,使用阿里云账号登陆后查看已订阅API服务的AppCode,并配置到Higress MCP Server的配置中。注意:在阿里云市场订阅API服务后,您将获得AppCode。对于您订阅的所有API服务,此AppCode是相同的,您只需使用这一个AppCode即可访问所有已订阅的API服务。
|
||||
3. 云市场用户控制台会实时展示已订阅的预付费API服务的可用额度,如您免费试用额度已用完,您可以选择重新订阅。
|
||||
|
||||
# MCP服务器配置功能简介
|
||||
|
||||
## 功能简介
|
||||
主要用于处理企业信用评级相关的查询请求。通过与阿里云市场上的特定API进行交互,该服务能够根据提供的公司名称、注册号或社会统一信用代码等信息返回对应企业的信用评级详情。这使得用户可以方便地获取到目标公司的最新信用状况,包括但不限于债券信用等级、主体等级以及评级展望等内容。
|
||||
|
||||
## 工具简介
|
||||
|
||||
### 企业信用评级
|
||||
- **用途**:提供一个接口用于查询指定企业的信用评级信息。
|
||||
- **使用场景**:适用于需要对企业信用状况进行全面了解的情况,比如金融机构在决定是否向某企业提供贷款时;供应商在选择合作伙伴前对潜在客户的资信进行调查等。
|
||||
|
||||
#### 参数说明
|
||||
- **keyword** (必填): 搜索关键字,可以是公司名称、注册号或者社会统一信用代码。
|
||||
- **pageNum**: 请求分页中的页码数,默认从1开始计数。
|
||||
- **pageSize**: 每页显示的结果条目数,默认值为10。
|
||||
|
||||
#### 请求模板
|
||||
- **URL**: `https://slyhonour.market.alicloudapi.com/credit/rating`
|
||||
- **方法**: GET
|
||||
- **头部信息**:
|
||||
- Authorization: 使用应用程序编码作为认证方式
|
||||
- X-Ca-Nonce: 自动生成的唯一标识符
|
||||
|
||||
#### 响应结构
|
||||
- **code**: 状态码
|
||||
- **data**:
|
||||
- **items[]**:
|
||||
- alias: 评级公司别名
|
||||
- bondCreditLevel: 债券信用等级
|
||||
- gid: 全球ID
|
||||
- logo: 评级公司Logo
|
||||
- ratingCompanyName: 评级公司名称
|
||||
- ratingDate: 评级日期
|
||||
- ratingOutlook: 评级展望
|
||||
- subjectLevel: 主体等级
|
||||
- orderNo: 订单号
|
||||
- total: 总记录数
|
||||
- **msg**: 返回的消息内容
|
||||
- **success**: 操作是否成功的布尔标志
|
||||
|
||||
此工具提供了详尽的企业信用评估数据,有助于用户快速准确地判断一家企业的财务健康状况及其偿还债务的能力。
|
||||
143
plugins/wasm-go/mcp-servers/mcp-business-credit-rating/api.json
Normal file
143
plugins/wasm-go/mcp-servers/mcp-business-credit-rating/api.json
Normal file
@@ -0,0 +1,143 @@
|
||||
{
|
||||
"info": {
|
||||
"description": "【企业信用评级-企业信用查询】★通过公司名称、注册号或社会统一信用代码任一项,查询企业信用评级信息。★毫秒级响应,支持高并发,24h不间断运维,专业技术支持在线服务。★新老客户享专属活动价,详情可咨询客服。——全品类接口专家",
|
||||
"title": "企业信用评级-企业信用查询【数链云】",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"openapi": "3.0.1",
|
||||
"paths": {
|
||||
"/credit/rating": {
|
||||
"get": {
|
||||
"operationId": "企业信用评级",
|
||||
"summary": "企业信用评级",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "搜索关键字(公司名称、注册号或社会统一信用代码)",
|
||||
"in": "query",
|
||||
"name": "keyword",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "分页数量 1开始",
|
||||
"in": "query",
|
||||
"name": "pageNum",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "每页数量 默认 10",
|
||||
"in": "query",
|
||||
"name": "pageSize",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"msg": {
|
||||
"description": "响应消息",
|
||||
"example": "成功",
|
||||
"type": "string"
|
||||
},
|
||||
"success": {
|
||||
"description": "是否成功",
|
||||
"example": "true",
|
||||
"type": "boolean"
|
||||
},
|
||||
"code": {
|
||||
"description": "状态码",
|
||||
"example": "200",
|
||||
"type": "integer"
|
||||
},
|
||||
"data": {
|
||||
"properties": {
|
||||
"orderNo": {
|
||||
"description": "订单号",
|
||||
"example": "276085547371344356",
|
||||
"type": "string"
|
||||
},
|
||||
"total": {
|
||||
"description": "总数",
|
||||
"example": "22",
|
||||
"type": "integer"
|
||||
},
|
||||
"items": {
|
||||
"items": {
|
||||
"properties": {
|
||||
"ratingOutlook": {
|
||||
"description": "评级展望",
|
||||
"example": "负面",
|
||||
"type": "string"
|
||||
},
|
||||
"ratingDate": {
|
||||
"description": "评级日期",
|
||||
"example": "2024-04-16",
|
||||
"format": "date",
|
||||
"type": "string"
|
||||
},
|
||||
"gid": {
|
||||
"description": "全球ID",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"ratingCompanyName": {
|
||||
"description": "评级公司名称",
|
||||
"example": "惠誉国际信用评级有限公司",
|
||||
"type": "string"
|
||||
},
|
||||
"logo": {
|
||||
"description": "评级公司Logo",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"alias": {
|
||||
"description": "评级公司别名",
|
||||
"example": "惠誉国际",
|
||||
"type": "string"
|
||||
},
|
||||
"bondCreditLevel": {
|
||||
"description": "债券信用等级",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"subjectLevel": {
|
||||
"description": "主体等级",
|
||||
"example": "A+",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "成功响应"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "https://slyhonour.market.alicloudapi.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
server:
|
||||
name: business-credit-rating
|
||||
config:
|
||||
appCode: ""
|
||||
tools:
|
||||
- name: bussiness-credit-rating
|
||||
description: 企业信用评级
|
||||
args:
|
||||
- name: keyword
|
||||
description: 搜索关键字(公司名称、注册号或社会统一信用代码)
|
||||
type: string
|
||||
required: true
|
||||
position: query
|
||||
- name: pageNum
|
||||
description: 分页数量 1开始
|
||||
type: string
|
||||
position: query
|
||||
- name: pageSize
|
||||
description: 每页数量 默认 10
|
||||
type: string
|
||||
position: query
|
||||
requestTemplate:
|
||||
url: https://slyhonour.market.alicloudapi.com/credit/rating
|
||||
method: GET
|
||||
headers:
|
||||
- key: Authorization
|
||||
value: APPCODE {{.config.appCode}}
|
||||
- key: X-Ca-Nonce
|
||||
value: '{{uuidv4}}'
|
||||
responseTemplate:
|
||||
prependBody: |+
|
||||
# API Response Information
|
||||
|
||||
Below is the response from an API call. To help you understand the data, I've provided:
|
||||
|
||||
1. A detailed description of all fields in the response structure
|
||||
2. The complete API response
|
||||
|
||||
## Response Structure
|
||||
|
||||
> Content-Type: application/json
|
||||
|
||||
- **code**: 状态码 (Type: integer)
|
||||
- **data**: (Type: object)
|
||||
- **data.items**: (Type: array)
|
||||
- **data.items[].alias**: 评级公司别名 (Type: string)
|
||||
- **data.items[].bondCreditLevel**: 债券信用等级 (Type: string)
|
||||
- **data.items[].gid**: 全球ID (Type: string)
|
||||
- **data.items[].logo**: 评级公司Logo (Type: string)
|
||||
- **data.items[].ratingCompanyName**: 评级公司名称 (Type: string)
|
||||
- **data.items[].ratingDate**: 评级日期 (Type: string)
|
||||
- **data.items[].ratingOutlook**: 评级展望 (Type: string)
|
||||
- **data.items[].subjectLevel**: 主体等级 (Type: string)
|
||||
- **data.orderNo**: 订单号 (Type: string)
|
||||
- **data.total**: 总数 (Type: integer)
|
||||
- **msg**: 响应消息 (Type: string)
|
||||
- **success**: 是否成功 (Type: boolean)
|
||||
|
||||
## Original Response
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
# Business Information Inquiry
|
||||
|
||||
The APP Code required for API authentication can be applied for on the Alibaba Cloud API Marketplace: https://market.aliyun.com/apimarket/detail/cmapi029030
|
||||
|
||||
# MCP Server Function Overview Document
|
||||
|
||||
## Function Overview
|
||||
The MCP server is primarily used to provide query services for enterprise-related information. Through a series of API interfaces, users can obtain detailed information including but not limited to patent information, copyright information, branch information, and business registration data of enterprises. These tools are designed to help businesses better understand their own or other companies' operational status, legal risks, and market performance.
|
||||
|
||||
## Tool Introduction
|
||||
|
||||
### 1. Enterprise Patent Information
|
||||
- **Purpose**: This tool is used to query all publicly available patent information of a specified company, covering various types such as inventions, utility models, and design patents.
|
||||
- **Use Case**: When evaluating a company's innovation capability or intellectual property layout, this tool can provide relevant data to support decision-making.
|
||||
|
||||
### 2. Other Copyright Information of Enterprises
|
||||
- **Purpose**: In addition to patents, it also provides a service for querying other types of copyright information of enterprises.
|
||||
- **Use Case**: Suitable for users interested in copyright protection in specific fields, such as copyright agencies or research institutions.
|
||||
|
||||
### 3. Enterprise Branch Information
|
||||
- **Purpose**: Lists all subsidiary or branch details under a given company name.
|
||||
- **Use Case**: Very useful for corporate analysts who want to fully understand the structure of a group.
|
||||
|
||||
### 4. Enterprise Name Search Suggestion Query
|
||||
- **Purpose**: Returns a list of related company names based on the input keywords, suitable for associative searches.
|
||||
- **Use Case**: Quickly find target companies during preliminary market research.
|
||||
|
||||
### 5. Enterprise Trademark Information
|
||||
- **Purpose**: Displays the trademarks held by an enterprise and their status.
|
||||
- **Use Case**: Brand management teams can use this service to monitor competitors' brand activities.
|
||||
|
||||
### 6. Enterprise External Investment Information
|
||||
- **Purpose**: Provides specific details about an enterprise's external investment projects.
|
||||
- **Use Case**: Investors and financial advisors can use this to understand the capital operations of a company.
|
||||
|
||||
### 7. Fuzzy Search for Enterprise Business Registration Data
|
||||
- **Purpose**: Finds basic information of enterprises based on incomplete matching conditions (e.g., abbreviations).
|
||||
- **Use Case**: Useful for quickly locating a target company when only partial information is known.
|
||||
|
||||
### 8. Precise Search for Enterprise Business Registration Data
|
||||
- **Purpose**: Conducts precise queries based on the full company name or social credit code.
|
||||
- **Use Case**: Suitable for situations where accurate and error-free business registration information is needed.
|
||||
|
||||
### 9. Enterprise Annual Report Information
|
||||
- **Purpose**: Views various financial indicators and other important information included in the annual report of an enterprise.
|
||||
- **Use Case**: Provides a basis for investors to analyze the financial health of a company.
|
||||
|
||||
### 10. Enterprise Recruitment Information
|
||||
- **Purpose**: Collects and displays job vacancies and related requirements posted by enterprises.
|
||||
- **Use Case**: Job seekers looking for opportunities; HR departments understanding industry talent demand trends.
|
||||
|
||||
### 11. Enterprise Legal Litigation Information
|
||||
- **Purpose**: Obtains details of legal disputes involving the enterprise.
|
||||
- **Use Case**: Legal advisors assessing the risk level of potential partners.
|
||||
|
||||
### 12. Enterprise Court Announcement Information
|
||||
- **Purpose**: Reviews the content of court announcements related to the enterprise.
|
||||
- **Use Case**: Tracking judicial dynamics of specific enterprises.
|
||||
|
||||
### 13. Enterprise Abnormal Operation Information
|
||||
- **Purpose**: Reveals records of issues that have occurred during the operation of the enterprise.
|
||||
- **Use Case**: Regulatory bodies overseeing corporate compliance; consumer protection organizations safeguarding public interests.
|
||||
|
||||
### 14. Enterprise Financing Information
|
||||
- **Purpose**: Tracks the specifics of each financing event of the enterprise.
|
||||
- **Use Case**: Startups monitoring the fundraising activities of competitors in the same industry.
|
||||
|
||||
### 15. Enterprise Executed Party Information
|
||||
- **Purpose**: Discloses the list of enterprises as executed parties and related case information.
|
||||
- **Use Case**: Financial institutions assessing the creditworthiness of loan applicants.
|
||||
|
||||
### 16. Enterprise Software Copyright Information
|
||||
- **Purpose**: Lists the software works owned by the enterprise and their copyright status.
|
||||
- **Use Case**: IT professionals understanding the technical strength of peers.
|
||||
|
||||
### 17. Big Data Enterprise Profile Tag Information
|
||||
- **Purpose**: A collection of enterprise characteristic tags generated through big data analysis.
|
||||
- **Use Case**: Marketing personnel customizing personalized promotion strategies; academic researchers conducting studies on enterprise behavior patterns.
|
||||
@@ -0,0 +1,89 @@
|
||||
# 工商信息查询
|
||||
|
||||
API认证需要的APP Code请在阿里云API市场申请: https://market.aliyun.com/apimarket/detail/cmapi029030
|
||||
|
||||
## 什么是云市场API MCP服务
|
||||
|
||||
阿里云云市场是生态伙伴的交易服务平台,我们致力于为合作伙伴提供覆盖上云、商业化和售卖的全链路服务,帮助客户高效获取、部署和管理优质生态产品。云市场的API服务涵盖以下几个类目:应用开发、身份验证与金融、车辆交通与物流、企业服务、短信与运营商、AI应用与OCR、生活服务。
|
||||
云市场API依托Higress提供MCP服务,您只需在云市场完成订阅并获取AppCode,通过Higress MCP Server进行配置,即可无缝集成云市场API服务。
|
||||
|
||||
## 如何在使用云市场API MCP服务
|
||||
|
||||
1. 进入API详情页,订阅该API。您可以优先使用免费试用。
|
||||
2. 前往云市场用户控制台,使用阿里云账号登陆后查看已订阅API服务的AppCode,并配置到Higress MCP Server的配置中。注意:在阿里云市场订阅API服务后,您将获得AppCode。对于您订阅的所有API服务,此AppCode是相同的,您只需使用这一个AppCode即可访问所有已订阅的API服务。
|
||||
3. 云市场用户控制台会实时展示已订阅的预付费API服务的可用额度,如您免费试用额度已用完,您可以选择重新订阅。
|
||||
|
||||
# MCP服务器功能简介文档
|
||||
|
||||
## 功能简介
|
||||
主要用于提供企业相关信息的查询服务。通过一系列API接口,用户可以获取到包括但不限于企业的专利信息、著作权信息、分支机构信息、工商数据等详细资料。这些工具旨在帮助企业更好地了解其自身或其他企业的经营状况、法律风险及市场表现等方面的信息。
|
||||
|
||||
## 工具简介
|
||||
|
||||
### 1. 企业专利信息
|
||||
- **用途**:此工具用于查询指定企业的所有公开专利信息,涵盖发明、实用新型、外观设计等多种类型。
|
||||
- **使用场景**:当需要评估一家企业的创新能力或知识产权布局时,可以通过该工具获取相关数据支持决策制定。
|
||||
|
||||
### 2. 企业其它著作权信息
|
||||
- **用途**:除了专利之外,还提供了关于企业其他类型的著作权信息查询服务。
|
||||
- **使用场景**:适用于对特定领域内版权保护情况感兴趣的用户,如版权代理机构或者研究机构。
|
||||
|
||||
### 3. 企业分支机构信息
|
||||
- **用途**:能够列出给定公司名下的所有子公司或分公司详情。
|
||||
- **使用场景**:对于希望全面了解某集团架构的企业分析师来说非常有用。
|
||||
|
||||
### 4. 企业名称搜索建议查询
|
||||
- **用途**:根据输入的关键字返回相关的公司名称列表,适合做联想式搜索。
|
||||
- **使用场景**:在进行初步市场调研时快速找到目标企业。
|
||||
|
||||
### 5. 企业商标信息
|
||||
- **用途**:显示企业所持有的商标及其状态等信息。
|
||||
- **使用场景**:品牌管理团队可利用这项服务来监控竞争对手的品牌活动。
|
||||
|
||||
### 6. 企业对外投资信息
|
||||
- **用途**:提供有关企业对外投资项目的具体细节。
|
||||
- **使用场景**:投资者和财务顾问可通过此途径了解企业的资本运作情况。
|
||||
|
||||
### 7. 企业工商数据模糊查询
|
||||
- **用途**:基于不完全匹配条件(如简称)查找符合条件的企业基本信息。
|
||||
- **使用场景**:当只知道部分企业信息时,可用于快速定位目标企业。
|
||||
|
||||
### 8. 企业工商数据精准查询
|
||||
- **用途**:针对已知完整企业名称或社会信用代码进行精确查询。
|
||||
- **使用场景**:适用于需要获取准确无误的企业注册信息的情况。
|
||||
|
||||
### 9. 企业年报信息
|
||||
- **用途**:查看企业年度报告中包含的各项财务指标及其他重要信息。
|
||||
- **使用场景**:为投资者分析企业财务健康状况提供依据。
|
||||
|
||||
### 10. 企业招聘信息
|
||||
- **用途**:收集并展示企业发布的职位空缺及相关要求。
|
||||
- **使用场景**:求职者寻找工作机会;人力资源部门了解行业人才需求趋势。
|
||||
|
||||
### 11. 企业法律诉讼信息
|
||||
- **用途**:获取涉及企业的法律纠纷案件详情。
|
||||
- **使用场景**:法律顾问评估潜在合作伙伴的风险等级。
|
||||
|
||||
### 12. 企业法院公告信息
|
||||
- **用途**:查阅与企业相关的法院公告内容。
|
||||
- **使用场景**:跟踪特定企业的司法动态。
|
||||
|
||||
### 13. 企业经营异常信息
|
||||
- **用途**:揭示企业在经营过程中出现的问题记录。
|
||||
- **使用场景**:监管机构监督企业合规性;消费者保护组织维护公众利益。
|
||||
|
||||
### 14. 企业融资信息
|
||||
- **用途**:追踪企业历次融资事件的具体情况。
|
||||
- **使用场景**:创业公司关注同行业内竞争者的资金募集情况。
|
||||
|
||||
### 15. 企业被执行人信息
|
||||
- **用途**:揭露作为被执行人的企业名单及相关案件信息。
|
||||
- **使用场景**:金融机构评估贷款申请者的信用水平。
|
||||
|
||||
### 16. 企业软件著作权信息
|
||||
- **用途**:列出企业拥有的软件作品及其版权状态。
|
||||
- **使用场景**:IT行业从业者了解同行技术实力。
|
||||
|
||||
### 17. 大数据企业画像标签信息
|
||||
- **用途**:基于大数据分析生成的企业特征标签集合。
|
||||
- **使用场景**:市场营销人员定制个性化推广策略;学术研究人员开展企业行为模式研究。
|
||||
1446
plugins/wasm-go/mcp-servers/mcp-business-info-query/api.json
Normal file
1446
plugins/wasm-go/mcp-servers/mcp-business-info-query/api.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,775 @@
|
||||
server:
|
||||
name: business-info-query
|
||||
config:
|
||||
appCode: ""
|
||||
tools:
|
||||
- name: business-patent-query
|
||||
description: 查询企业公布的专利信息,包括发明专利,实用新型,实用外观,发明授权等类型
|
||||
args:
|
||||
- name: CompanyName
|
||||
description: CompanyName
|
||||
type: string
|
||||
required: true
|
||||
position: path
|
||||
- name: isRaiseErrorCode
|
||||
description: "当请求传入不存在企业名称时是否抛出404错误。0为否,1为是,默认为否。可以避免传入不存在企业时扣减次数。\t"
|
||||
type: integer
|
||||
position: query
|
||||
requestTemplate:
|
||||
url: https://api.81api.com/getCompanyPatentsInfo/{CompanyName}/
|
||||
method: GET
|
||||
headers:
|
||||
- key: Authorization
|
||||
value: APPCODE {{.config.appCode}}
|
||||
- key: X-Ca-Nonce
|
||||
value: '{{uuidv4}}'
|
||||
responseTemplate:
|
||||
prependBody: |+
|
||||
# API Response Information
|
||||
|
||||
Below is the response from an API call. To help you understand the data, I've provided:
|
||||
|
||||
1. A detailed description of all fields in the response structure
|
||||
2. The complete API response
|
||||
|
||||
## Response Structure
|
||||
|
||||
> Content-Type: application/json
|
||||
|
||||
- **data**: (Type: object)
|
||||
- **data.list**: (Type: array)
|
||||
- **data.list[].createDate**: 创建日期 (Type: string)
|
||||
- **data.list[].createNum**: 专利编号 (Type: string)
|
||||
- **data.list[].patentName**: 专利名称 (Type: string)
|
||||
- **data.list[].type**: 专利类型 (Type: string)
|
||||
- **data.total**: 总数 (Type: integer)
|
||||
- **status**: 请求状态 (Type: boolean)
|
||||
|
||||
## Original Response
|
||||
|
||||
- name: business-other-copyright-query
|
||||
description: 企业其它著作权信息
|
||||
args:
|
||||
- name: CompanyName
|
||||
description: CompanyName
|
||||
type: string
|
||||
required: true
|
||||
position: path
|
||||
- name: isRaiseErrorCode
|
||||
description: "当请求传入不存在企业名称时是否抛出404错误。0为否,1为是,默认为否。可以避免传入不存在企业时扣减次数。\t"
|
||||
type: integer
|
||||
position: query
|
||||
requestTemplate:
|
||||
url: https://api.81api.com/getCompanyOtherCopyrightsInfo/{CompanyName}/
|
||||
method: GET
|
||||
headers:
|
||||
- key: Authorization
|
||||
value: APPCODE {{.config.appCode}}
|
||||
- key: X-Ca-Nonce
|
||||
value: '{{uuidv4}}'
|
||||
responseTemplate:
|
||||
prependBody: |+
|
||||
# API Response Information
|
||||
|
||||
Below is the response from an API call. To help you understand the data, I've provided:
|
||||
|
||||
1. A detailed description of all fields in the response structure
|
||||
2. The complete API response
|
||||
|
||||
## Response Structure
|
||||
|
||||
> Content-Type: application/json
|
||||
|
||||
- **data**: (Type: object)
|
||||
- **data.list**: (Type: array)
|
||||
- **data.list[].className**: 类别名称 (Type: string)
|
||||
- **data.list[].createDate**: 创建日期 (Type: string)
|
||||
- **data.list[].name**: 名称 (Type: string)
|
||||
- **data.list[].publishDate**: 发布日期 (Type: string)
|
||||
- **data.list[].regNo**: 注册号 (Type: string)
|
||||
- **data.total**: 总数量 (Type: integer)
|
||||
- **status**: 状态标志,表示请求是否成功 (Type: boolean)
|
||||
|
||||
## Original Response
|
||||
|
||||
- name: business-branch-query
|
||||
description: 企业所有分支机构信息
|
||||
args:
|
||||
- name: CompanyName
|
||||
description: CompanyName
|
||||
type: string
|
||||
required: true
|
||||
position: path
|
||||
- name: PageNum
|
||||
description: 查询页数,默认为第一页
|
||||
type: integer
|
||||
position: query
|
||||
- name: isRaiseErrorCode
|
||||
description: "当请求传入不存在企业名称时是否抛出404错误。0为否,1为是,默认为否。可以避免传入不存在企业时扣减次数。\t"
|
||||
type: integer
|
||||
position: query
|
||||
requestTemplate:
|
||||
url: https://api.81api.com/getCompanyBranchInfo/{CompanyName}/
|
||||
method: GET
|
||||
headers:
|
||||
- key: Authorization
|
||||
value: APPCODE {{.config.appCode}}
|
||||
- key: X-Ca-Nonce
|
||||
value: '{{uuidv4}}'
|
||||
responseTemplate:
|
||||
prependBody: |+
|
||||
# API Response Information
|
||||
|
||||
Below is the response from an API call. To help you understand the data, I've provided:
|
||||
|
||||
1. A detailed description of all fields in the response structure
|
||||
2. The complete API response
|
||||
|
||||
## Response Structure
|
||||
|
||||
> Content-Type: application/json
|
||||
|
||||
- **data**: (Type: object)
|
||||
- **data.list**: (Type: array)
|
||||
- **data.list[].name**: 分公司名称 (Type: string)
|
||||
- **data.total**: 总数 (Type: integer)
|
||||
- **status**: 状态标志,表示请求是否成功 (Type: boolean)
|
||||
|
||||
## Original Response
|
||||
|
||||
- name: business-name-query
|
||||
description: 企业工商名称搜索建议查询,只返回推荐匹配的企业名称,适合联想查询,输入框搜索建议
|
||||
args:
|
||||
- name: Keyword
|
||||
description: 查询关键字,至少3个字
|
||||
type: string
|
||||
required: true
|
||||
position: path
|
||||
- name: isRaiseErrorCode
|
||||
description: 当请求查询关键字无返回结果时是否抛出404错误。0为否,1为是,默认为否。可以避免传入无效关键字时扣减次数。
|
||||
type: integer
|
||||
position: query
|
||||
requestTemplate:
|
||||
url: https://api.81api.com/fuzzySuggestCompanyName/{Keyword}/
|
||||
method: GET
|
||||
headers:
|
||||
- key: Authorization
|
||||
value: APPCODE {{.config.appCode}}
|
||||
- key: X-Ca-Nonce
|
||||
value: '{{uuidv4}}'
|
||||
responseTemplate:
|
||||
prependBody: |+
|
||||
# API Response Information
|
||||
|
||||
Below is the response from an API call. To help you understand the data, I've provided:
|
||||
|
||||
1. A detailed description of all fields in the response structure
|
||||
2. The complete API response
|
||||
|
||||
## Response Structure
|
||||
|
||||
> Content-Type: application/json
|
||||
|
||||
- **data**: (Type: object)
|
||||
- **data.list**: (Type: array)
|
||||
- **data.list[].companyname**: 公司名称 (Type: string)
|
||||
- **status**: 响应状态,true表示成功 (Type: boolean)
|
||||
|
||||
## Original Response
|
||||
|
||||
- name: business-trademark-query
|
||||
description: 企业商标信息
|
||||
args:
|
||||
- name: CompanyName
|
||||
description: CompanyName
|
||||
type: string
|
||||
required: true
|
||||
position: path
|
||||
- name: isRaiseErrorCode
|
||||
description: "当请求传入不存在企业名称时是否抛出404错误。0为否,1为是,默认为否。可以避免传入不存在企业时扣减次数。\t"
|
||||
type: integer
|
||||
position: query
|
||||
requestTemplate:
|
||||
url: https://api.81api.com/getCompanyTrademarksInfo/{CompanyName}/
|
||||
method: GET
|
||||
headers:
|
||||
- key: Authorization
|
||||
value: APPCODE {{.config.appCode}}
|
||||
- key: X-Ca-Nonce
|
||||
value: '{{uuidv4}}'
|
||||
responseTemplate:
|
||||
prependBody: |+
|
||||
# API Response Information
|
||||
|
||||
Below is the response from an API call. To help you understand the data, I've provided:
|
||||
|
||||
1. A detailed description of all fields in the response structure
|
||||
2. The complete API response
|
||||
|
||||
## Response Structure
|
||||
|
||||
> Content-Type: application/json
|
||||
|
||||
- **data**: (Type: object)
|
||||
- **data.list**: (Type: array)
|
||||
- **data.list[].name**: 名称 (Type: string)
|
||||
- **data.list[].status**: 当前状态 (Type: string)
|
||||
- **data.list[].type**: 类型 (Type: string)
|
||||
- **data.total**: 总数 (Type: integer)
|
||||
- **status**: 状态标志 (Type: boolean)
|
||||
|
||||
## Original Response
|
||||
|
||||
- name: busiiness-invest-query
|
||||
description: 企业对外投资信息
|
||||
args:
|
||||
- name: CompanyName
|
||||
description: CompanyName
|
||||
type: string
|
||||
required: true
|
||||
position: path
|
||||
- name: isRaiseErrorCode
|
||||
description: "当请求传入不存在企业名称时是否抛出404错误。0为否,1为是,默认为否。可以避免传入不存在企业时扣减次数。\t"
|
||||
type: integer
|
||||
position: query
|
||||
requestTemplate:
|
||||
url: https://api.81api.com/getCompanyInvestEventsInfo/{CompanyName}/
|
||||
method: GET
|
||||
headers:
|
||||
- key: Authorization
|
||||
value: APPCODE {{.config.appCode}}
|
||||
- key: X-Ca-Nonce
|
||||
value: '{{uuidv4}}'
|
||||
responseTemplate:
|
||||
prependBody: |+
|
||||
# API Response Information
|
||||
|
||||
Below is the response from an API call. To help you understand the data, I've provided:
|
||||
|
||||
1. A detailed description of all fields in the response structure
|
||||
2. The complete API response
|
||||
|
||||
## Response Structure
|
||||
|
||||
> Content-Type: application/json
|
||||
|
||||
- **data**: (Type: object)
|
||||
- **data.list**: (Type: array)
|
||||
- **data.list[].investCapital**: 投资金额 (Type: string)
|
||||
- **data.list[].investCompanyName**: 投资公司名称 (Type: string)
|
||||
- **data.list[].investDate**: 投资日期 (Type: string)
|
||||
- **data.total**: 列表总数 (Type: integer)
|
||||
- **status**: 操作状态,true表示成功 (Type: boolean)
|
||||
|
||||
## Original Response
|
||||
|
||||
- name: business-basic-query
|
||||
description: 企业工商基本数据模糊查询
|
||||
args:
|
||||
- name: CompanyName
|
||||
description: 支持企业名称、简称、注册号、信任号等模糊匹配
|
||||
type: string
|
||||
required: true
|
||||
position: path
|
||||
- name: PageNum
|
||||
description: 查询页数,默认为第一页
|
||||
type: integer
|
||||
position: query
|
||||
requestTemplate:
|
||||
url: https://api.81api.com/fuzzyQueryCompanyInfo/{CompanyName}/
|
||||
method: GET
|
||||
headers:
|
||||
- key: Authorization
|
||||
value: APPCODE {{.config.appCode}}
|
||||
- key: X-Ca-Nonce
|
||||
value: '{{uuidv4}}'
|
||||
responseTemplate:
|
||||
prependBody: |+
|
||||
# API Response Information
|
||||
|
||||
Below is the response from an API call. To help you understand the data, I've provided:
|
||||
|
||||
1. A detailed description of all fields in the response structure
|
||||
2. The complete API response
|
||||
|
||||
## Response Structure
|
||||
|
||||
> Content-Type: application/json
|
||||
|
||||
- **data**: (Type: object)
|
||||
- **data.list**: (Type: array)
|
||||
- **data.list[].legal_person_name**: 法人代表姓名 (Type: string)
|
||||
- **data.list[].name**: 公司名称 (Type: string)
|
||||
- **data.list[].reg_capital**: 注册资本 (Type: string)
|
||||
- **data.list[].reg_date**: 注册日期 (Type: string)
|
||||
- **data.num**: 当前数量 (Type: integer)
|
||||
- **data.total**: 总数 (Type: integer)
|
||||
- **message**: 消息 (Type: string)
|
||||
- **status**: 状态码 (Type: string)
|
||||
|
||||
## Original Response
|
||||
|
||||
- name: exact-business-query
|
||||
description: 精准查询企业工商基本数据,包括工商注册信息,股东信息,变更记录,分支机构,董事会信息
|
||||
args:
|
||||
- name: CompanyNameOrCreditNo
|
||||
description: 支持企业全称和企业社会信任代码
|
||||
type: string
|
||||
required: true
|
||||
position: path
|
||||
- name: isRaiseErrorCode
|
||||
description: 当请求传入不存在企业名称时是否抛出404错误。0为否,1为是,默认为否。可以避免传入不存在企业时扣减次数。
|
||||
type: integer
|
||||
position: query
|
||||
requestTemplate:
|
||||
url: https://api.81api.com/getCompanyBaseInfo/{CompanyNameOrCreditNo}/
|
||||
method: GET
|
||||
headers:
|
||||
- key: Authorization
|
||||
value: APPCODE {{.config.appCode}}
|
||||
- key: X-Ca-Nonce
|
||||
value: '{{uuidv4}}'
|
||||
responseTemplate:
|
||||
prependBody: |+
|
||||
# API Response Information
|
||||
|
||||
Below is the response from an API call. To help you understand the data, I've provided:
|
||||
|
||||
1. A detailed description of all fields in the response structure
|
||||
2. The complete API response
|
||||
|
||||
## Response Structure
|
||||
|
||||
> Content-Type: application/json
|
||||
|
||||
- **data**: (Type: object)
|
||||
- **data.changeRecordData**: (Type: object)
|
||||
- **data.changeRecordData.hasMore**: 是否有更多变更记录 (Type: boolean)
|
||||
- **data.changeRecordData.list**: (Type: array)
|
||||
- **data.changeRecordData.list[].after**: 变更后内容 (Type: string)
|
||||
- **data.changeRecordData.list[].before**: 变更前内容 (Type: string)
|
||||
- **data.changeRecordData.list[].date**: 变更日期 (Type: string)
|
||||
- **data.changeRecordData.list[].item**: 变更项目 (Type: string)
|
||||
- **data.employeeData**: (Type: object)
|
||||
- **data.employeeData.list**: (Type: array)
|
||||
- **data.employeeData.list[].name**: 员工姓名 (Type: string)
|
||||
- **data.employeeData.list[].title**: 职位 (Type: string)
|
||||
- **data.employeeData.total**: 员工总数 (Type: integer)
|
||||
- **data.legalPersonName**: 法定代表人姓名 (Type: string)
|
||||
- **data.name**: 公司名称 (Type: string)
|
||||
- **data.partnerData**: (Type: object)
|
||||
- **data.partnerData.list**: (Type: array)
|
||||
- **data.partnerData.list[].partnerName**: 股东姓名 (Type: string)
|
||||
- **data.partnerData.list[].partnerType**: 股东类型 (Type: string)
|
||||
- **data.partnerData.list[].totalRealCapital**: 实缴资本 (Type: string)
|
||||
- **data.partnerData.list[].totalShouldCapital**: 认缴资本 (Type: string)
|
||||
- **data.partnerData.total**: 股东总数 (Type: integer)
|
||||
- **data.registerCapital**: 注册资本 (Type: string)
|
||||
- **data.registerData**: (Type: object)
|
||||
- **data.registerData.address**: 公司地址 (Type: string)
|
||||
- **data.registerData.belongOrg**: 登记机关 (Type: string)
|
||||
- **data.registerData.businessScope**: 经营范围 (Type: string)
|
||||
- **data.registerData.businessTerm**: 营业期限 (Type: string)
|
||||
- **data.registerData.creditNo**: 统一社会信用代码 (Type: string)
|
||||
- **data.registerData.orgNo**: 组织机构代码 (Type: string)
|
||||
- **data.registerData.regType**: 企业类型 (Type: string)
|
||||
- **data.registerData.registerNo**: 工商注册号 (Type: string)
|
||||
- **data.registerData.status**: 经营状态 (Type: string)
|
||||
- **data.startDate**: 公司成立日期 (Type: string)
|
||||
- **status**: 响应状态 (Type: boolean)
|
||||
|
||||
## Original Response
|
||||
|
||||
- name: business-year-report-query
|
||||
description: 企业年报信息
|
||||
args:
|
||||
- name: CompanyName
|
||||
description: CompanyName
|
||||
type: string
|
||||
required: true
|
||||
position: path
|
||||
- name: isRaiseErrorCode
|
||||
description: "当请求传入不存在企业名称时是否抛出404错误。0为否,1为是,默认为否。可以避免传入不存在企业时扣减次数。\t"
|
||||
type: integer
|
||||
position: query
|
||||
requestTemplate:
|
||||
url: https://api.81api.com/getCompanyYearReportInfo/{CompanyName}/
|
||||
method: GET
|
||||
headers:
|
||||
- key: Authorization
|
||||
value: APPCODE {{.config.appCode}}
|
||||
- key: X-Ca-Nonce
|
||||
value: '{{uuidv4}}'
|
||||
responseTemplate:
|
||||
prependBody: |+
|
||||
# API Response Information
|
||||
|
||||
Below is the response from an API call. To help you understand the data, I've provided:
|
||||
|
||||
1. A detailed description of all fields in the response structure
|
||||
2. The complete API response
|
||||
|
||||
## Response Structure
|
||||
|
||||
> Content-Type: application/json
|
||||
|
||||
- **data**: (Type: array)
|
||||
- **data[].rptDate**: 报告日期 (Type: string)
|
||||
- **data[].rptDetail**: (Type: object)
|
||||
- **data[].rptDetail.creditNo**: 统一社会信用代码 (Type: string)
|
||||
- **data[].rptDetail.isEquity**: 是否有股权 (Type: string)
|
||||
- **data[].rptDetail.isInvest**: 是否有投资 (Type: string)
|
||||
- **data[].rptDetail.name**: 公司名称 (Type: string)
|
||||
- **data[].rptDetail.staffNum**: 员工人数 (Type: string)
|
||||
- **data[].rptDetail.status**: 公司状态 (Type: string)
|
||||
- **data[].rptYear**: 报告年度 (Type: string)
|
||||
- **status**: 响应状态 (Type: boolean)
|
||||
|
||||
## Original Response
|
||||
|
||||
- name: business-jobs-query
|
||||
description: 企业招聘信息
|
||||
args:
|
||||
- name: CompanyName
|
||||
description: CompanyName
|
||||
type: string
|
||||
required: true
|
||||
position: path
|
||||
- name: isRaiseErrorCode
|
||||
description: "当请求传入不存在企业名称时是否抛出404错误。0为否,1为是,默认为否。可以避免传入不存在企业时扣减次数。\t"
|
||||
type: integer
|
||||
position: query
|
||||
requestTemplate:
|
||||
url: https://api.81api.com/getCompanyJobsInfo/{CompanyName}/
|
||||
method: GET
|
||||
headers:
|
||||
- key: Authorization
|
||||
value: APPCODE {{.config.appCode}}
|
||||
- key: X-Ca-Nonce
|
||||
value: '{{uuidv4}}'
|
||||
responseTemplate:
|
||||
prependBody: |+
|
||||
# API Response Information
|
||||
|
||||
Below is the response from an API call. To help you understand the data, I've provided:
|
||||
|
||||
1. A detailed description of all fields in the response structure
|
||||
2. The complete API response
|
||||
|
||||
## Response Structure
|
||||
|
||||
> Content-Type: application/json
|
||||
|
||||
- **data**: (Type: object)
|
||||
- **data.list**: (Type: array)
|
||||
- **data.list[].date**: 发布日期 (Type: string)
|
||||
- **data.list[].education**: 学历要求 (Type: string)
|
||||
- **data.list[].position**: 职位名称 (Type: string)
|
||||
- **data.list[].salary**: 薪资范围 (Type: string)
|
||||
- **data.list[].years**: 工作年限 (Type: string)
|
||||
- **data.total**: 总数 (Type: integer)
|
||||
- **status**: 响应状态 (Type: boolean)
|
||||
|
||||
## Original Response
|
||||
|
||||
- name: business-lawsuit-query
|
||||
description: 企业法律诉讼信息,主要是裁判文书
|
||||
args:
|
||||
- name: CompanyName
|
||||
description: 传入企业全称
|
||||
type: string
|
||||
required: true
|
||||
position: path
|
||||
- name: isRaiseErrorCode
|
||||
description: "当请求传入不存在企业名称时是否抛出404错误。0为否,1为是,默认为否。可以避免传入不存在企业时扣减次数。\t"
|
||||
type: integer
|
||||
position: query
|
||||
requestTemplate:
|
||||
url: https://api.81api.com/getCompanyLawsuitInfo/{CompanyName}/
|
||||
method: GET
|
||||
headers:
|
||||
- key: Authorization
|
||||
value: APPCODE {{.config.appCode}}
|
||||
- key: X-Ca-Nonce
|
||||
value: '{{uuidv4}}'
|
||||
responseTemplate:
|
||||
prependBody: |+
|
||||
# API Response Information
|
||||
|
||||
Below is the response from an API call. To help you understand the data, I've provided:
|
||||
|
||||
1. A detailed description of all fields in the response structure
|
||||
2. The complete API response
|
||||
|
||||
## Response Structure
|
||||
|
||||
> Content-Type: application/json
|
||||
|
||||
- **data**: (Type: array)
|
||||
- **data[].caseContent**: 案件内容 (Type: string)
|
||||
- **data[].caseName**: 案件名称 (Type: string)
|
||||
- **data[].caseNo**: 案号 (Type: string)
|
||||
- **data[].caseReason**: 案由 (Type: string)
|
||||
- **data[].pulishDate**: 发布日期 (Type: string)
|
||||
- **status**: 请求状态 (Type: boolean)
|
||||
|
||||
## Original Response
|
||||
|
||||
- name: business-court-query
|
||||
description: 企业法院公告信息
|
||||
args:
|
||||
- name: CompanyName
|
||||
description: CompanyName
|
||||
type: string
|
||||
required: true
|
||||
position: path
|
||||
- name: isRaiseErrorCode
|
||||
description: "当请求传入不存在企业名称时是否抛出404错误。0为否,1为是,默认为否。可以避免传入不存在企业时扣减次数。\t"
|
||||
type: integer
|
||||
position: query
|
||||
requestTemplate:
|
||||
url: https://api.81api.com/getCompanyCourtInfo/{CompanyName}/
|
||||
method: GET
|
||||
headers:
|
||||
- key: Authorization
|
||||
value: APPCODE {{.config.appCode}}
|
||||
- key: X-Ca-Nonce
|
||||
value: '{{uuidv4}}'
|
||||
responseTemplate:
|
||||
prependBody: |+
|
||||
# API Response Information
|
||||
|
||||
Below is the response from an API call. To help you understand the data, I've provided:
|
||||
|
||||
1. A detailed description of all fields in the response structure
|
||||
2. The complete API response
|
||||
|
||||
## Response Structure
|
||||
|
||||
> Content-Type: application/json
|
||||
|
||||
- **data**: (Type: array)
|
||||
- **data[].courtName**: 法庭名称 (Type: string)
|
||||
- **data[].courtNo**: 案件编号 (Type: string)
|
||||
- **data[].pulishDate**: 发布日期和时间 (Type: string)
|
||||
- **status**: 请求状态 (Type: boolean)
|
||||
|
||||
## Original Response
|
||||
|
||||
- name: business-abnormal-query
|
||||
description: 企业经营异常信息
|
||||
args:
|
||||
- name: CompanyName
|
||||
description: CompanyName
|
||||
type: string
|
||||
required: true
|
||||
position: path
|
||||
- name: isRaiseErrorCode
|
||||
description: "当请求传入不存在企业名称时是否抛出404错误。0为否,1为是,默认为否。可以避免传入不存在企业时扣减次数。\t"
|
||||
type: integer
|
||||
position: query
|
||||
requestTemplate:
|
||||
url: https://api.81api.com/getCompanyAbnormalInfo/{CompanyName}/
|
||||
method: GET
|
||||
headers:
|
||||
- key: Authorization
|
||||
value: APPCODE {{.config.appCode}}
|
||||
- key: X-Ca-Nonce
|
||||
value: '{{uuidv4}}'
|
||||
responseTemplate:
|
||||
prependBody: |+
|
||||
# API Response Information
|
||||
|
||||
Below is the response from an API call. To help you understand the data, I've provided:
|
||||
|
||||
1. A detailed description of all fields in the response structure
|
||||
2. The complete API response
|
||||
|
||||
## Response Structure
|
||||
|
||||
> Content-Type: application/json
|
||||
|
||||
- **data**: (Type: object)
|
||||
- **data.list**: (Type: array)
|
||||
- **data.list[].iDate**: (Type: string)
|
||||
- **data.list[].iReason**: (Type: string)
|
||||
- **data.list[].oDate**: (Type: string)
|
||||
- **data.list[].oReason**: (Type: string)
|
||||
- **data.list[].orgName**: (Type: string)
|
||||
- **data.total**: (Type: integer)
|
||||
- **status**: (Type: boolean)
|
||||
|
||||
## Original Response
|
||||
|
||||
- name: business-financing-query
|
||||
description: 企业融资信息
|
||||
args:
|
||||
- name: CompanyName
|
||||
description: CompanyName
|
||||
type: string
|
||||
required: true
|
||||
position: path
|
||||
- name: isRaiseErrorCode
|
||||
description: "当请求传入不存在企业名称时是否抛出404错误。0为否,1为是,默认为否。可以避免传入不存在企业时扣减次数。\t"
|
||||
type: integer
|
||||
position: query
|
||||
requestTemplate:
|
||||
url: https://api.81api.com/getCompanyFinancingInfo/{CompanyName}/
|
||||
method: GET
|
||||
headers:
|
||||
- key: Authorization
|
||||
value: APPCODE {{.config.appCode}}
|
||||
- key: X-Ca-Nonce
|
||||
value: '{{uuidv4}}'
|
||||
responseTemplate:
|
||||
prependBody: |+
|
||||
# API Response Information
|
||||
|
||||
Below is the response from an API call. To help you understand the data, I've provided:
|
||||
|
||||
1. A detailed description of all fields in the response structure
|
||||
2. The complete API response
|
||||
|
||||
## Response Structure
|
||||
|
||||
> Content-Type: application/json
|
||||
|
||||
- **data**: (Type: array)
|
||||
- **data[].amount**: 投资金额 (Type: string)
|
||||
- **data[].date**: 投资日期 (Type: string)
|
||||
- **data[].investors**: 投资者列表 (Type: string)
|
||||
- **data[].round**: 融资轮次 (Type: string)
|
||||
- **status**: 状态标识,true表示成功 (Type: boolean)
|
||||
|
||||
## Original Response
|
||||
|
||||
- name: business-debtor-query
|
||||
description: 企业被执行人信息
|
||||
args:
|
||||
- name: CompanyName
|
||||
description: 传入企业全称
|
||||
type: string
|
||||
required: true
|
||||
position: path
|
||||
- name: isRaiseErrorCode
|
||||
description: "当请求传入不存在企业名称时是否抛出404错误。0为否,1为是,默认为否。可以避免传入不存在企业时扣减次数。\t"
|
||||
type: integer
|
||||
position: query
|
||||
requestTemplate:
|
||||
url: https://api.81api.com/getCompanyJudgmentDebtorInfo/{CompanyName}/
|
||||
method: GET
|
||||
headers:
|
||||
- key: Authorization
|
||||
value: APPCODE {{.config.appCode}}
|
||||
- key: X-Ca-Nonce
|
||||
value: '{{uuidv4}}'
|
||||
responseTemplate:
|
||||
prependBody: |+
|
||||
# API Response Information
|
||||
|
||||
Below is the response from an API call. To help you understand the data, I've provided:
|
||||
|
||||
1. A detailed description of all fields in the response structure
|
||||
2. The complete API response
|
||||
|
||||
## Response Structure
|
||||
|
||||
> Content-Type: application/json
|
||||
|
||||
- **data**: (Type: object)
|
||||
- **data.list**: (Type: array)
|
||||
- **data.list[].caseMoney**: 案件金额 (Type: string)
|
||||
- **data.list[].caseNo**: 案件编号 (Type: string)
|
||||
- **data.list[].caseOrg**: 案件所属法院 (Type: string)
|
||||
- **data.list[].parties**: 当事人 (Type: string)
|
||||
- **data.list[].pulishDate**: 发布日期 (Type: string)
|
||||
- **data.total**: 数据总数 (Type: integer)
|
||||
- **status**: 状态标识,true表示成功 (Type: boolean)
|
||||
|
||||
## Original Response
|
||||
|
||||
- name: business-software-copyrights-query
|
||||
description: 企业软件著作权信息
|
||||
args:
|
||||
- name: CompanyName
|
||||
description: CompanyName
|
||||
type: string
|
||||
required: true
|
||||
position: path
|
||||
- name: isRaiseErrorCode
|
||||
description: "当请求传入不存在企业名称时是否抛出404错误。0为否,1为是,默认为否。可以避免传入不存在企业时扣减次数。\t"
|
||||
type: integer
|
||||
position: query
|
||||
requestTemplate:
|
||||
url: https://api.81api.com/getCompanySoftwareCopyrightsInfo/{CompanyName}/
|
||||
method: GET
|
||||
headers:
|
||||
- key: Authorization
|
||||
value: APPCODE {{.config.appCode}}
|
||||
- key: X-Ca-Nonce
|
||||
value: '{{uuidv4}}'
|
||||
responseTemplate:
|
||||
prependBody: |+
|
||||
# API Response Information
|
||||
|
||||
Below is the response from an API call. To help you understand the data, I've provided:
|
||||
|
||||
1. A detailed description of all fields in the response structure
|
||||
2. The complete API response
|
||||
|
||||
## Response Structure
|
||||
|
||||
> Content-Type: application/json
|
||||
|
||||
- **data**: (Type: object)
|
||||
- **data.list**: (Type: array)
|
||||
- **data.list[].name**: 软件名称 (Type: string)
|
||||
- **data.list[].publishDate**: 发布日期 (Type: string)
|
||||
- **data.list[].regNo**: 注册号 (Type: string)
|
||||
- **data.list[].shortName**: 简称 (Type: string)
|
||||
- **data.list[].typeNo**: 类型编号 (Type: string)
|
||||
- **data.list[].versionNo**: 版本号 (Type: string)
|
||||
- **data.total**: 总数量 (Type: integer)
|
||||
- **status**: 请求状态,true表示成功 (Type: boolean)
|
||||
|
||||
## Original Response
|
||||
|
||||
- name: business-profile-tags-query
|
||||
description: 基于大数据对企业的画像标签信息
|
||||
args:
|
||||
- name: CompanyName
|
||||
description: CompanyName
|
||||
type: string
|
||||
required: true
|
||||
position: path
|
||||
- name: isRaiseErrorCode
|
||||
description: 当请求传入不存在企业名称时是否抛出404错误。0为否,1为是,默认为否。可以避免传入不存在企业时扣减次数。
|
||||
type: integer
|
||||
position: query
|
||||
requestTemplate:
|
||||
url: https://api.81api.com/getCompanyProfileTags/{CompanyName}/
|
||||
method: GET
|
||||
headers:
|
||||
- key: Authorization
|
||||
value: APPCODE {{.config.appCode}}
|
||||
- key: X-Ca-Nonce
|
||||
value: '{{uuidv4}}'
|
||||
responseTemplate:
|
||||
prependBody: |+
|
||||
# API Response Information
|
||||
|
||||
Below is the response from an API call. To help you understand the data, I've provided:
|
||||
|
||||
1. A detailed description of all fields in the response structure
|
||||
2. The complete API response
|
||||
|
||||
## Response Structure
|
||||
|
||||
> Content-Type: application/json
|
||||
|
||||
- **data**: 数据列表 (Type: array)
|
||||
- **data[]**: Items of type string
|
||||
- **status**: 响应状态,true表示成功 (Type: boolean)
|
||||
|
||||
## Original Response
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# Enterprise Patent Query
|
||||
|
||||
The APP Code required for API authentication can be applied for at the Alibaba Cloud API Marketplace: https://market.aliyun.com/apimarket/detail/cmapi00049059
|
||||
|
||||
# MCP Server Configuration Document
|
||||
|
||||
This server is primarily used for querying enterprise patent information, supporting the retrieval of patent lists and detailed information.
|
||||
|
||||
## Function Overview
|
||||
The `business-patent-query` server focuses on providing services related to patent information for enterprises or individual users. Through this service, users can easily search for all relevant patents within a specific technical field, which helps in avoiding infringement of others' intellectual property rights and guiding their own R&D activities. It includes two core functions: patent information list retrieval and patent detail viewing.
|
||||
|
||||
## Tool Introduction
|
||||
|
||||
### 1. Patent Information List
|
||||
- **Purpose**: This tool allows users to find related patent lists based on keywords (such as company name, social credit code, etc.).
|
||||
- **Use Cases**: It is used when a comprehensive understanding of the patent layout of a particular industry or company is needed; it can also be used for market research, competitor analysis, and other areas.
|
||||
- **Parameter Description**:
|
||||
- `dtype`: The format of the returned data, default is JSON.
|
||||
- `keyword`: A required parameter, used to specify the search keyword.
|
||||
- `pageIndex`: Specifies the page number of the returned results, default is the first page.
|
||||
- `pageSize`: Sets the number of results displayed per page, default is 10 records, with a maximum of 10 records.
|
||||
|
||||
### 2. Patent Information Details
|
||||
- **Purpose**: Based on a known patent ID, this tool can obtain the specific details of a single patent.
|
||||
- **Use Cases**: It is suitable for in-depth study of a specific patent content or when detailed information about a certain technical solution is needed.
|
||||
- **Parameter Description**:
|
||||
- `dtype`: Defines the format of the response data, default is JSON.
|
||||
- `id`: A required field, representing the unique identifier of the patent to be queried, typically obtained from the "Patent Information List" interface.
|
||||
|
||||
Each tool provides detailed request and response templates to ensure that developers can correctly call the API and handle the returned data. Additionally, the response structure for each tool includes basic information about the requested patent as well as some additional metadata, such as order number, status code, etc., to facilitate tracking the request status and parsing the data.
|
||||
@@ -0,0 +1,41 @@
|
||||
# 企业专利查询
|
||||
|
||||
API认证需要的APP Code请在阿里云API市场申请: https://market.aliyun.com/apimarket/detail/cmapi00049059
|
||||
|
||||
## 什么是云市场API MCP服务
|
||||
|
||||
阿里云云市场是生态伙伴的交易服务平台,我们致力于为合作伙伴提供覆盖上云、商业化和售卖的全链路服务,帮助客户高效获取、部署和管理优质生态产品。云市场的API服务涵盖以下几个类目:应用开发、身份验证与金融、车辆交通与物流、企业服务、短信与运营商、AI应用与OCR、生活服务。
|
||||
云市场API依托Higress提供MCP服务,您只需在云市场完成订阅并获取AppCode,通过Higress MCP Server进行配置,即可无缝集成云市场API服务。
|
||||
|
||||
## 如何在使用云市场API MCP服务
|
||||
|
||||
1. 进入API详情页,订阅该API。您可以优先使用免费试用。
|
||||
2. 前往云市场用户控制台,使用阿里云账号登陆后查看已订阅API服务的AppCode,并配置到Higress MCP Server的配置中。注意:在阿里云市场订阅API服务后,您将获得AppCode。对于您订阅的所有API服务,此AppCode是相同的,您只需使用这一个AppCode即可访问所有已订阅的API服务。
|
||||
3. 云市场用户控制台会实时展示已订阅的预付费API服务的可用额度,如您免费试用额度已用完,您可以选择重新订阅。
|
||||
|
||||
# MCP服务器配置文档
|
||||
|
||||
该服务器主要用于查询企业的专利信息,支持获取专利列表及详细信息。
|
||||
|
||||
## 功能简介
|
||||
`business-patent-query`服务器专注于为企业或个人用户提供专利相关信息的服务。通过此服务,用户可以轻松地搜索到特定技术领域内的所有相关专利,这有助于避免侵犯他人的知识产权,并为自身的研发活动指明方向。它包括两大核心功能:专利信息列表检索与专利详情查看。
|
||||
|
||||
## 工具简介
|
||||
|
||||
### 1. 专利信息列表
|
||||
- **用途**:此工具允许用户根据关键字(如公司名称、社会统一信用代码等)来查找相关的专利列表。
|
||||
- **应用场景**:当需要对某一行业或公司的专利布局进行全面了解时使用;也可用于市场调研、竞争对手分析等领域。
|
||||
- **参数说明**:
|
||||
- `dtype`: 返回的数据格式,默认为JSON。
|
||||
- `keyword`: 必填项,用来指定搜索的关键字。
|
||||
- `pageIndex`: 指定返回结果的页码,默认第一页。
|
||||
- `pageSize`: 设置每页显示的结果数量,默认为10条记录,最大不超过10条。
|
||||
|
||||
### 2. 专利信息详情
|
||||
- **用途**:基于已知的专利ID,此工具能够获取单个专利的具体细节信息。
|
||||
- **应用场景**:适用于深入研究某一项具体的专利内容,或是需要详细了解某项技术解决方案的情况。
|
||||
- **参数说明**:
|
||||
- `dtype`: 同样定义了响应数据的格式,默认采用JSON形式。
|
||||
- `id`: 必需字段,代表想要查询的专利唯一标识符,通常是从“专利信息列表”接口获得的ID值。
|
||||
|
||||
每个工具都提供了详细的请求模板和响应模板说明,以确保开发者能够正确调用API并处理返回的数据。此外,对于每个工具而言,其响应结构均包含关于所请求专利的基本信息以及额外的一些元数据,比如订单号、状态码等,便于跟踪请求状态和解析数据。
|
||||
380
plugins/wasm-go/mcp-servers/mcp-business-patent-query/api.json
Normal file
380
plugins/wasm-go/mcp-servers/mcp-business-patent-query/api.json
Normal file
@@ -0,0 +1,380 @@
|
||||
{
|
||||
"info": {
|
||||
"description": "查询企业或某技术的专利,帮助用户掌握相同技术领域的发展状况,为规避他人知识产权和调整研发方向提供参考",
|
||||
"title": "企业专利信息-专利信息查询",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"openapi": "3.0.1",
|
||||
"paths": {
|
||||
"/utn/ip/PatentDetail": {
|
||||
"get": {
|
||||
"operationId": "专利信息详情",
|
||||
"summary": "查询企业或某技术的专利,帮助用户掌握相同技术领域的发展状况,为规避他人知识产权和调整研发方向提供参考",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "专利信息列表接口返回的Id",
|
||||
"in": "query",
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "返回数据格式:json或xml,默认json",
|
||||
"in": "query",
|
||||
"name": "dtype",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"orderNo": {
|
||||
"description": "订单编号",
|
||||
"example": "1359050786293813200",
|
||||
"type": "integer"
|
||||
},
|
||||
"data": {
|
||||
"properties": {
|
||||
"LegalStatusDate": {
|
||||
"description": "法律状态日期",
|
||||
"example": "2019-11-15 00:00:00",
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"Agent": {
|
||||
"description": "代理人",
|
||||
"example": "肖平安",
|
||||
"type": "string"
|
||||
},
|
||||
"PublicationDate": {
|
||||
"description": "公布日期",
|
||||
"example": "2018-11-13 00:00:00",
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"Agency": {
|
||||
"description": "代理机构",
|
||||
"example": "北京科亿知识产权代理事务所(普通合伙)",
|
||||
"type": "string"
|
||||
},
|
||||
"OtherReferences": {
|
||||
"description": "其他引用",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"IPCList": {
|
||||
"description": "国际专利分类号列表",
|
||||
"example": "H04L9/32",
|
||||
"type": "string"
|
||||
},
|
||||
"LegalStatusDesc": {
|
||||
"description": "法律状态描述",
|
||||
"example": "授权",
|
||||
"type": "string"
|
||||
},
|
||||
"Abstract": {
|
||||
"description": "摘要",
|
||||
"example": "本发明公开了一种基于非对称密码算法的保护隐私征信方法...",
|
||||
"type": "string"
|
||||
},
|
||||
"Title": {
|
||||
"description": "标题",
|
||||
"example": "一种基于非对称密码算法的保护隐私征信方法",
|
||||
"type": "string"
|
||||
},
|
||||
"KindCodeDesc": {
|
||||
"description": "类型代码描述",
|
||||
"example": "发明",
|
||||
"type": "string"
|
||||
},
|
||||
"PrimaryExaminer": {
|
||||
"description": "主审查员",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"AssiantExaminer": {
|
||||
"description": "辅助审查员",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"ApplicationDate": {
|
||||
"description": "申请日期",
|
||||
"example": "2015-05-13 00:00:00",
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"PatentImage": {
|
||||
"description": "专利图片链接",
|
||||
"example": "https://filecdn.shuidi.cn/img/upload/images_patent/cc/b4/eb/ccb4ebc86ad8b0093fcc0c30999be8fc.png/0x0.jpg",
|
||||
"type": "string"
|
||||
},
|
||||
"AssigneestringList": {
|
||||
"description": "专利权人列表",
|
||||
"example": "上海凭安企业信用征信有限公司,上海凭安征信服务有限公司",
|
||||
"type": "string"
|
||||
},
|
||||
"PatentLegalHistory": {
|
||||
"items": {
|
||||
"properties": {
|
||||
"LegalStatusDate": {
|
||||
"description": "法律状态日期",
|
||||
"example": "2019-11-15 00:00:00",
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"Desc": {
|
||||
"description": "描述",
|
||||
"example": "专利权人的姓名或者名称、地址的变更IPC(主分类):H04L 9/32变更前 专利权人:上海凭安企业信用征信有限公司 地址:201700 上海市长宁区广顺路33号8幢193室变更后 专利权人:上海凭安征信服务有限公司 地址:200335 上海市长宁区广顺路33号8幢193室",
|
||||
"type": "string"
|
||||
},
|
||||
"LegalStatus": {
|
||||
"description": "法律状态",
|
||||
"example": "专利权人的姓名或者名称、地址的变更",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"InventorStringList": {
|
||||
"description": "发明人列表",
|
||||
"example": "韩洪慧,杨茂江",
|
||||
"type": "string"
|
||||
},
|
||||
"IPCDesc": {
|
||||
"description": "国际专利分类号描述",
|
||||
"example": "包括用于检验系统用户的身份或凭据的装置〔5〕",
|
||||
"type": "string"
|
||||
},
|
||||
"DocumentTypes": {
|
||||
"description": "文档类型",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"ApplicationNumber": {
|
||||
"description": "申请号",
|
||||
"example": "CN201510241189.8",
|
||||
"type": "string"
|
||||
},
|
||||
"PublicationNumber": {
|
||||
"description": "公布号",
|
||||
"example": "CN104821883B",
|
||||
"type": "string"
|
||||
},
|
||||
"Cites": {
|
||||
"description": "引用",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"statusMessage": {
|
||||
"description": "状态消息",
|
||||
"example": "请求成功",
|
||||
"type": "string"
|
||||
},
|
||||
"statusCode": {
|
||||
"description": "状态码",
|
||||
"example": "1",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "请求成功"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/utn/ip/PatentPageByKey/V2": {
|
||||
"get": {
|
||||
"operationId": "专利信息列表",
|
||||
"summary": "查询企业或某技术的专利,帮助用户掌握相同技术领域的发展状况,为规避他人知识产权和调整研发方向提供参考",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "搜索关键字(公司名称、社会统一信用代码、注册号)",
|
||||
"in": "query",
|
||||
"name": "keyword",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "页码,默认第1页",
|
||||
"in": "query",
|
||||
"name": "pageIndex",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "每页条数,默认为10,最大不超过10条",
|
||||
"in": "query",
|
||||
"name": "pageSize",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "返回数据格式:json或xml,默认json",
|
||||
"in": "query",
|
||||
"name": "dtype",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"orderNo": {
|
||||
"description": "订单号",
|
||||
"example": "1359050786163789800",
|
||||
"type": "integer"
|
||||
},
|
||||
"data": {
|
||||
"properties": {
|
||||
"Paging": {
|
||||
"properties": {
|
||||
"PageSize": {
|
||||
"description": "每页显示条数",
|
||||
"example": "10",
|
||||
"type": "integer"
|
||||
},
|
||||
"TotalRecords": {
|
||||
"description": "总记录数",
|
||||
"example": "8842",
|
||||
"type": "integer"
|
||||
},
|
||||
"PageIndex": {
|
||||
"description": "当前页码",
|
||||
"example": "3",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"Items": {
|
||||
"items": {
|
||||
"properties": {
|
||||
"PublicationDate": {
|
||||
"description": "公开日期",
|
||||
"example": "2018-04-27 00:00:00",
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"Agency": {
|
||||
"description": "代理机构",
|
||||
"example": "北京三高永信知识产权代理有限责任公司",
|
||||
"type": "string"
|
||||
},
|
||||
"IPCList": {
|
||||
"description": "IPC分类号",
|
||||
"example": "H04M7/00",
|
||||
"type": "string"
|
||||
},
|
||||
"LegalStatusDesc": {
|
||||
"description": "法律状态描述",
|
||||
"example": "授权",
|
||||
"type": "string"
|
||||
},
|
||||
"Title": {
|
||||
"description": "标题",
|
||||
"example": "语音通道建立方法、装置及系统",
|
||||
"type": "string"
|
||||
},
|
||||
"KindCodeDesc": {
|
||||
"description": "类别代码描述",
|
||||
"example": "发明",
|
||||
"type": "string"
|
||||
},
|
||||
"ApplicationDate": {
|
||||
"description": "申请日期",
|
||||
"example": "2015-06-26 00:00:00",
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"AssigneeStringList": {
|
||||
"description": "申请人",
|
||||
"example": "小米科技有限责任公司",
|
||||
"type": "string"
|
||||
},
|
||||
"InventorStringList": {
|
||||
"description": "发明人",
|
||||
"example": "侯俊杰,辛显龙,金峰",
|
||||
"type": "string"
|
||||
},
|
||||
"IPCDesc": {
|
||||
"description": "IPC分类描述",
|
||||
"example": "交换中心之间的互连装置",
|
||||
"type": "string"
|
||||
},
|
||||
"ApplicationNumber": {
|
||||
"description": "申请号",
|
||||
"example": "CN201510363777.9",
|
||||
"type": "string"
|
||||
},
|
||||
"Id": {
|
||||
"description": "ID",
|
||||
"example": "45233394",
|
||||
"type": "integer"
|
||||
},
|
||||
"PublicationNumber": {
|
||||
"description": "公开号",
|
||||
"example": "CN105100523B",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"statusMessage": {
|
||||
"description": "状态消息",
|
||||
"example": "请求成功",
|
||||
"type": "string"
|
||||
},
|
||||
"statusCode": {
|
||||
"description": "状态码",
|
||||
"example": "1",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "请求成功"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "http://icpatent.market.alicloudapi.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
server:
|
||||
name: business-patent-query
|
||||
config:
|
||||
appCode: ""
|
||||
tools:
|
||||
- name: business-patent-query
|
||||
description: 查询企业或某技术的专利,帮助用户掌握相同技术领域的发展状况,为规避他人知识产权和调整研发方向提供参考
|
||||
args:
|
||||
- name: dtype
|
||||
description: 返回数据格式:json或xml,默认json
|
||||
type: string
|
||||
position: query
|
||||
- name: keyword
|
||||
description: 搜索关键字(公司名称、社会统一信用代码、注册号)
|
||||
type: string
|
||||
required: true
|
||||
position: query
|
||||
- name: pageIndex
|
||||
description: 页码,默认第1页
|
||||
type: integer
|
||||
position: query
|
||||
- name: pageSize
|
||||
description: 每页条数,默认为10,最大不超过10条
|
||||
type: integer
|
||||
position: query
|
||||
requestTemplate:
|
||||
url: http://icpatent.market.alicloudapi.com/utn/ip/PatentPageByKey/V2
|
||||
method: GET
|
||||
headers:
|
||||
- key: Authorization
|
||||
value: APPCODE {{.config.appCode}}
|
||||
- key: X-Ca-Nonce
|
||||
value: '{{uuidv4}}'
|
||||
responseTemplate:
|
||||
prependBody: |+
|
||||
# API Response Information
|
||||
|
||||
Below is the response from an API call. To help you understand the data, I've provided:
|
||||
|
||||
1. A detailed description of all fields in the response structure
|
||||
2. The complete API response
|
||||
|
||||
## Response Structure
|
||||
|
||||
> Content-Type: application/json
|
||||
|
||||
- **data**: (Type: object)
|
||||
- **data.Items**: (Type: array)
|
||||
- **data.Items[].Agency**: 代理机构 (Type: string)
|
||||
- **data.Items[].ApplicationDate**: 申请日期 (Type: string)
|
||||
- **data.Items[].ApplicationNumber**: 申请号 (Type: string)
|
||||
- **data.Items[].AssigneeStringList**: 申请人 (Type: string)
|
||||
- **data.Items[].IPCDesc**: IPC分类描述 (Type: string)
|
||||
- **data.Items[].IPCList**: IPC分类号 (Type: string)
|
||||
- **data.Items[].Id**: ID (Type: integer)
|
||||
- **data.Items[].InventorStringList**: 发明人 (Type: string)
|
||||
- **data.Items[].KindCodeDesc**: 类别代码描述 (Type: string)
|
||||
- **data.Items[].LegalStatusDesc**: 法律状态描述 (Type: string)
|
||||
- **data.Items[].PublicationDate**: 公开日期 (Type: string)
|
||||
- **data.Items[].PublicationNumber**: 公开号 (Type: string)
|
||||
- **data.Items[].Title**: 标题 (Type: string)
|
||||
- **data.Paging**: (Type: object)
|
||||
- **data.Paging.PageIndex**: 当前页码 (Type: integer)
|
||||
- **data.Paging.PageSize**: 每页显示条数 (Type: integer)
|
||||
- **data.Paging.TotalRecords**: 总记录数 (Type: integer)
|
||||
- **orderNo**: 订单号 (Type: integer)
|
||||
- **statusCode**: 状态码 (Type: integer)
|
||||
- **statusMessage**: 状态消息 (Type: string)
|
||||
|
||||
## Original Response
|
||||
|
||||
- name: patent-detail
|
||||
description: 查询企业或某技术的专利,帮助用户掌握相同技术领域的发展状况,为规避他人知识产权和调整研发方向提供参考
|
||||
args:
|
||||
- name: dtype
|
||||
description: 返回数据格式:json或xml,默认json
|
||||
type: string
|
||||
position: query
|
||||
- name: id
|
||||
description: 专利信息列表接口返回的Id
|
||||
type: integer
|
||||
required: true
|
||||
position: query
|
||||
requestTemplate:
|
||||
url: http://icpatent.market.alicloudapi.com/utn/ip/PatentDetail
|
||||
method: GET
|
||||
headers:
|
||||
- key: Authorization
|
||||
value: APPCODE {{.config.appCode}}
|
||||
- key: X-Ca-Nonce
|
||||
value: '{{uuidv4}}'
|
||||
responseTemplate:
|
||||
prependBody: |+
|
||||
# API Response Information
|
||||
|
||||
Below is the response from an API call. To help you understand the data, I've provided:
|
||||
|
||||
1. A detailed description of all fields in the response structure
|
||||
2. The complete API response
|
||||
|
||||
## Response Structure
|
||||
|
||||
> Content-Type: application/json
|
||||
|
||||
- **data**: (Type: object)
|
||||
- **data.Abstract**: 摘要 (Type: string)
|
||||
- **data.Agency**: 代理机构 (Type: string)
|
||||
- **data.Agent**: 代理人 (Type: string)
|
||||
- **data.ApplicationDate**: 申请日期 (Type: string)
|
||||
- **data.ApplicationNumber**: 申请号 (Type: string)
|
||||
- **data.AssiantExaminer**: 辅助审查员 (Type: string)
|
||||
- **data.AssigneestringList**: 专利权人列表 (Type: string)
|
||||
- **data.Cites**: 引用 (Type: string)
|
||||
- **data.DocumentTypes**: 文档类型 (Type: string)
|
||||
- **data.IPCDesc**: 国际专利分类号描述 (Type: string)
|
||||
- **data.IPCList**: 国际专利分类号列表 (Type: string)
|
||||
- **data.InventorStringList**: 发明人列表 (Type: string)
|
||||
- **data.KindCodeDesc**: 类型代码描述 (Type: string)
|
||||
- **data.LegalStatusDate**: 法律状态日期 (Type: string)
|
||||
- **data.LegalStatusDesc**: 法律状态描述 (Type: string)
|
||||
- **data.OtherReferences**: 其他引用 (Type: string)
|
||||
- **data.PatentImage**: 专利图片链接 (Type: string)
|
||||
- **data.PatentLegalHistory**: (Type: array)
|
||||
- **data.PatentLegalHistory[].Desc**: 描述 (Type: string)
|
||||
- **data.PatentLegalHistory[].LegalStatus**: 法律状态 (Type: string)
|
||||
- **data.PatentLegalHistory[].LegalStatusDate**: 法律状态日期 (Type: string)
|
||||
- **data.PrimaryExaminer**: 主审查员 (Type: string)
|
||||
- **data.PublicationDate**: 公布日期 (Type: string)
|
||||
- **data.PublicationNumber**: 公布号 (Type: string)
|
||||
- **data.Title**: 标题 (Type: string)
|
||||
- **orderNo**: 订单编号 (Type: integer)
|
||||
- **statusCode**: 状态码 (Type: integer)
|
||||
- **statusMessage**: 状态消息 (Type: string)
|
||||
|
||||
## Original Response
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# Chinese Almanac/Holiday Helper
|
||||
|
||||
The APP Code required for API authentication can be applied for at the Alibaba Cloud API Marketplace: https://market.aliyun.com/apimarket/detail/cmapi00066017
|
||||
|
||||
# MCP Server Configuration Document
|
||||
|
||||
## Function Overview
|
||||
The `calendar-holiday-helper` server is a service platform focused on providing holiday-related information and almanac fortune queries. It supports various API calls to obtain data including but not limited to holiday lists, detailed holiday information for specific dates, and almanac information based on traditional Chinese culture. These services are very useful for individuals and organizations that need to schedule activities based on specific dates or want to know the auspiciousness of a particular day.
|
||||
|
||||
## Tool Introduction
|
||||
|
||||
### 1. Holiday List
|
||||
- **Description**: This tool is used to list all holidays within a specified year.
|
||||
- **Use Case**: Suitable for businesses planning annual holidays, the travel industry formulating promotional plans, etc.
|
||||
- **Parameter Description**:
|
||||
- `year` (string): The year to query, defaulting to the current year. For non-current years, it also returns the current year's holiday data; next year's data can only be queried in December of the current year.
|
||||
|
||||
### 2. Holiday Details
|
||||
- **Description**: This tool provides detailed holiday information for a specific date (defaulting to the current day).
|
||||
- **Use Case**: Suitable for individuals or teams who want to know if a particular day is a holiday and its specific name.
|
||||
- **Parameter Description**:
|
||||
- `date` (string): The date to query, defaulting to the current day.
|
||||
- `needDesc` (string): Whether to return a brief description of public holidays, international days, and traditional Chinese festivals, with a value of 1 indicating to return, defaulting to not returning.
|
||||
|
||||
### 3. Almanac Fortune (New Version) - Auspicious Times
|
||||
- **Description**: Provides a daily auspicious time query service based on the traditional Chinese calendar.
|
||||
- **Use Case**: Particularly useful for those who believe in choosing auspicious times for important decisions.
|
||||
- **Parameter Description**:
|
||||
- `date` (string, required): The date to query, in the format yyyyMMdd.
|
||||
|
||||
### 4. Almanac Fortune (New Version) - Auspicious Deities and Inauspicious Spirits
|
||||
- **Description**: Displays information about auspicious deities and inauspicious spirits affecting fortune on a specific date.
|
||||
- **Use Case**: Helps users avoid unfavorable factors and seize favorable opportunities.
|
||||
- **Parameter Description**:
|
||||
- `date` (string, required): The date to query, in the format yyyyMMdd.
|
||||
|
||||
### 5. Almanac Fortune (New Version) - Almanac
|
||||
- **Description**: A comprehensive calendar service integrating the lunar calendar, Gregorian calendar, and other relevant astronomical information.
|
||||
- **Use Case**: Widely used for arranging various customary activities in daily life.
|
||||
- **Parameter Description**:
|
||||
- `date` (string, required): The date to query, in the format yyyyMMdd.
|
||||
|
||||
The above is an overview of the main tools and services provided by the `calendar-holiday-helper` server. By making reasonable use of these tools, users can more effectively manage their time and adjust their activity schedules as needed.
|
||||
@@ -0,0 +1,54 @@
|
||||
# 中国黄历/假期助手
|
||||
|
||||
API认证需要的APP Code请在阿里云API市场申请: https://market.aliyun.com/apimarket/detail/cmapi00066017
|
||||
|
||||
## 什么是云市场API MCP服务
|
||||
|
||||
阿里云云市场是生态伙伴的交易服务平台,我们致力于为合作伙伴提供覆盖上云、商业化和售卖的全链路服务,帮助客户高效获取、部署和管理优质生态产品。云市场的API服务涵盖以下几个类目:应用开发、身份验证与金融、车辆交通与物流、企业服务、短信与运营商、AI应用与OCR、生活服务。
|
||||
云市场API依托Higress提供MCP服务,您只需在云市场完成订阅并获取AppCode,通过Higress MCP Server进行配置,即可无缝集成云市场API服务。
|
||||
|
||||
## 如何在使用云市场API MCP服务
|
||||
|
||||
1. 进入API详情页,订阅该API。您可以优先使用免费试用。
|
||||
2. 前往云市场用户控制台,使用阿里云账号登陆后查看已订阅API服务的AppCode,并配置到Higress MCP Server的配置中。注意:在阿里云市场订阅API服务后,您将获得AppCode。对于您订阅的所有API服务,此AppCode是相同的,您只需使用这一个AppCode即可访问所有已订阅的API服务。
|
||||
3. 云市场用户控制台会实时展示已订阅的预付费API服务的可用额度,如您免费试用额度已用完,您可以选择重新订阅。
|
||||
|
||||
# MCP服务器配置文档
|
||||
|
||||
## 功能简介
|
||||
`calendar-holiday-helper`服务器是一个专注于提供节假日相关信息以及黄历运势查询的服务平台。它支持多种API调用来获取包括但不限于节假日列表、具体日期的节假日详情、以及基于中国传统文化的黄历信息等数据。这些服务对于需要根据特定日期安排活动或希望了解某日吉凶情况的个人和组织非常有用。
|
||||
|
||||
## 工具简介
|
||||
|
||||
### 1. 节假日列表
|
||||
- **描述**:此工具用于列出指定年份内的所有节假日。
|
||||
- **应用场景**:适用于企业规划年度假期、旅游行业制定促销计划等场合。
|
||||
- **参数说明**:
|
||||
- `year` (string):需要查询的年份,默认查当年。非当年日期也返回当年节假日数据;来年的数据需等到当年12月份才能查询。
|
||||
|
||||
### 2. 节假日详情
|
||||
- **描述**:该工具提供了一个具体的日期(默认为当天)下的节假日详细信息。
|
||||
- **应用场景**:适合于个人或团队想要了解某一天是否为节假日及其具体名称时使用。
|
||||
- **参数说明**:
|
||||
- `date` (string):查询的日期,默认为当天。
|
||||
- `needDesc` (string):是否需要返回当日公众日、国际日和我国传统节日的简介,值为1表示返回,默认不返回。
|
||||
|
||||
### 3. 黄历运势_新版_吉时
|
||||
- **描述**:提供了基于中国传统历法的每日吉时查询服务。
|
||||
- **应用场景**:对那些相信选择吉时进行重要决策的人群特别有用。
|
||||
- **参数说明**:
|
||||
- `date` (string, 必填):查询的日期,格式为yyyyMMdd。
|
||||
|
||||
### 4. 黄历运势_新版_吉神凶煞
|
||||
- **描述**:展示了特定日期内影响运势的吉神与凶煞信息。
|
||||
- **应用场景**:帮助用户避开不利因素并抓住有利时机。
|
||||
- **参数说明**:
|
||||
- `date` (string, 必填):查询的日期,格式为yyyyMMdd。
|
||||
|
||||
### 5. 黄历运势_新版_黄历
|
||||
- **描述**:综合了农历、公历以及其他相关天文学信息的日历服务。
|
||||
- **应用场景**:广泛应用于日常生活中的各种习俗活动安排。
|
||||
- **参数说明**:
|
||||
- `date` (string, 必填):查询的日期,格式为yyyyMMdd。
|
||||
|
||||
以上就是`calendar-holiday-helper`服务器提供的主要工具和服务概述。通过合理利用这些工具,用户能够更有效地管理时间,并根据需要调整自己的活动安排。
|
||||
909
plugins/wasm-go/mcp-servers/mcp-calendar-holiday-helper/api.json
Normal file
909
plugins/wasm-go/mcp-servers/mcp-calendar-holiday-helper/api.json
Normal file
@@ -0,0 +1,909 @@
|
||||
{
|
||||
"info": {
|
||||
"description": "【节假日查询 黄历查询 吉日查询 】接口可查询传统日历、节假日、运势、宜忌等信息,广泛用于日程安排,出行指南,风水评估等。 —— 我们只做精品!",
|
||||
"title": "【聚美智数】黄历查询-日历查询-节假日查询-运势查询-吉凶查询-万年历-阴阳历-国际法定节假日查询",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"openapi": "3.0.1",
|
||||
"paths": {
|
||||
"/holiday/list": {
|
||||
"post": {
|
||||
"operationId": "节假日列表",
|
||||
"summary": "节假日列表",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/x-www-form-urlencoded": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"year": {
|
||||
"description": "需要查询的年份【注意: 默认查当年,非当年日期也返回当年节假日数据,来年数据需等到当年12月份才能查】",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"description": "返回码"
|
||||
},
|
||||
"msg": {
|
||||
"type": "string",
|
||||
"description": "返回信息"
|
||||
},
|
||||
"taskNo": {
|
||||
"type": "string",
|
||||
"description": "请求号"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"count": {
|
||||
"type": "integer",
|
||||
"description": "一年的节假日数量"
|
||||
},
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"begin": {
|
||||
"type": "string"
|
||||
},
|
||||
"end": {
|
||||
"type": "string"
|
||||
},
|
||||
"holiday": {
|
||||
"type": "string"
|
||||
},
|
||||
"holiday_remark": {
|
||||
"type": "string"
|
||||
},
|
||||
"inverse_days": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "成功响应"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/luck-tendency/almanac": {
|
||||
"post": {
|
||||
"operationId": "黄历运势_新版_黄历",
|
||||
"summary": "黄历运势_新版_黄历",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/x-www-form-urlencoded": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"date": {
|
||||
"description": "查询的日期 格式为yyyyMMdd",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"date"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"msg": {
|
||||
"type": "string",
|
||||
"example": "成功"
|
||||
},
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"example": 200
|
||||
},
|
||||
"taskNo": {
|
||||
"type": "string",
|
||||
"example": 74848319667949360000
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"gongli": {
|
||||
"type": "string",
|
||||
"description": "公历"
|
||||
},
|
||||
"nongli": {
|
||||
"type": "string",
|
||||
"description": "农历"
|
||||
},
|
||||
"jieri": {
|
||||
"type": "string",
|
||||
"description": "节日"
|
||||
},
|
||||
"zhiri": {
|
||||
"type": "string",
|
||||
"description": "值日"
|
||||
},
|
||||
"zhishen": {
|
||||
"type": "string",
|
||||
"description": "值神"
|
||||
},
|
||||
"yi": {
|
||||
"type": "string",
|
||||
"description": "宜"
|
||||
},
|
||||
"ji": {
|
||||
"type": "string",
|
||||
"description": "忌"
|
||||
},
|
||||
"qixiang": {
|
||||
"type": "string",
|
||||
"description": "气象"
|
||||
},
|
||||
"jieqi24": {
|
||||
"type": "string",
|
||||
"description": "当前月包含的24节气"
|
||||
},
|
||||
"shengxiao": {
|
||||
"type": "string",
|
||||
"description": "生肖"
|
||||
},
|
||||
"xingzuo": {
|
||||
"type": "string",
|
||||
"description": "星座"
|
||||
},
|
||||
"rulueli": {
|
||||
"type": "string",
|
||||
"description": "儒略历"
|
||||
},
|
||||
"jsyq": {
|
||||
"type": "string",
|
||||
"description": "吉神宜趋"
|
||||
},
|
||||
"xsyj": {
|
||||
"type": "string",
|
||||
"description": "凶神宜忌"
|
||||
},
|
||||
"pzbj": {
|
||||
"type": "string",
|
||||
"description": "彭祖百忌"
|
||||
},
|
||||
"tszf": {
|
||||
"type": "string",
|
||||
"description": "胎神占方"
|
||||
},
|
||||
"chongsha": {
|
||||
"type": "string",
|
||||
"description": "冲煞"
|
||||
},
|
||||
"nayin": {
|
||||
"type": "string",
|
||||
"description": "纳音"
|
||||
},
|
||||
"dizhi": {
|
||||
"type": "string",
|
||||
"description": "地支"
|
||||
},
|
||||
"ganzhi": {
|
||||
"type": "string",
|
||||
"description": "干支"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "成功响应"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/luck-tendency/auspicious-demon": {
|
||||
"post": {
|
||||
"operationId": "黄历运势_新版_吉神凶煞",
|
||||
"summary": "黄历运势_新版_吉神凶煞",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/x-www-form-urlencoded": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"date": {
|
||||
"description": "查询的日期 格式为yyyyMMdd",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"date"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"msg": {
|
||||
"type": "string",
|
||||
"example": "成功"
|
||||
},
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"example": 200
|
||||
},
|
||||
"taskNo": {
|
||||
"type": "string",
|
||||
"example": "74848319667949359984"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"niansansha": {
|
||||
"type": "string"
|
||||
},
|
||||
"nianqisha": {
|
||||
"type": "string"
|
||||
},
|
||||
"niankongwang": {
|
||||
"type": "string"
|
||||
},
|
||||
"yuezhi": {
|
||||
"type": "string"
|
||||
},
|
||||
"yueling": {
|
||||
"type": "string"
|
||||
},
|
||||
"yuexiang": {
|
||||
"type": "string"
|
||||
},
|
||||
"yuesansha": {
|
||||
"type": "string"
|
||||
},
|
||||
"yueqisha": {
|
||||
"type": "string"
|
||||
},
|
||||
"yuekongwang": {
|
||||
"type": "string"
|
||||
},
|
||||
"risansha": {
|
||||
"type": "string"
|
||||
},
|
||||
"riqisha": {
|
||||
"type": "string"
|
||||
},
|
||||
"rikongwang": {
|
||||
"type": "string"
|
||||
},
|
||||
"tjjs": {
|
||||
"type": "string"
|
||||
},
|
||||
"taisuiwei": {
|
||||
"type": "string"
|
||||
},
|
||||
"fantaisui": {
|
||||
"type": "string"
|
||||
},
|
||||
"esbx": {
|
||||
"type": "string"
|
||||
},
|
||||
"jiuxing": {
|
||||
"type": "string"
|
||||
},
|
||||
"rilu": {
|
||||
"type": "string"
|
||||
},
|
||||
"zhongdong": {
|
||||
"type": "string"
|
||||
},
|
||||
"suipowei": {
|
||||
"type": "string"
|
||||
},
|
||||
"niantaisui": {
|
||||
"type": "string"
|
||||
},
|
||||
"caishen": {
|
||||
"type": "string"
|
||||
},
|
||||
"xishen": {
|
||||
"type": "string"
|
||||
},
|
||||
"yangguishen": {
|
||||
"type": "string"
|
||||
},
|
||||
"yinguishen": {
|
||||
"type": "string"
|
||||
},
|
||||
"fushen": {
|
||||
"type": "string"
|
||||
},
|
||||
"yjgx": {
|
||||
"type": "string"
|
||||
},
|
||||
"wuhou": {
|
||||
"type": "string"
|
||||
},
|
||||
"zhishn12": {
|
||||
"type": "string"
|
||||
},
|
||||
"zhiri12": {
|
||||
"type": "string"
|
||||
},
|
||||
"liuyao": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "成功响应"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/luck-tendency/auspicious-time": {
|
||||
"post": {
|
||||
"operationId": "黄历运势_新版_吉时",
|
||||
"summary": "黄历运势_新版_吉时",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/x-www-form-urlencoded": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"date": {
|
||||
"description": "查询的日期 格式为yyyyMMdd",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"date"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"msg": {
|
||||
"type": "string",
|
||||
"example": "成功"
|
||||
},
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"example": 200
|
||||
},
|
||||
"taskNo": {
|
||||
"type": "string",
|
||||
"example": 74848319667949360000
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"zi": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"shijian": {
|
||||
"type": "string",
|
||||
"example": "23:00:00-0:59:59"
|
||||
},
|
||||
"jixiong": {
|
||||
"type": "string",
|
||||
"example": "司命(吉)"
|
||||
},
|
||||
"jishen": {
|
||||
"type": "string",
|
||||
"example": "司命天乙贵人"
|
||||
},
|
||||
"xiongshen": {
|
||||
"type": "string",
|
||||
"example": "日刑"
|
||||
},
|
||||
"shichong": {
|
||||
"type": "string",
|
||||
"example": "冲马"
|
||||
},
|
||||
"shizhu": {
|
||||
"type": "string",
|
||||
"example": "甲子"
|
||||
}
|
||||
}
|
||||
},
|
||||
"hai": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"jixiong": {
|
||||
"type": "string",
|
||||
"example": "玄武(凶)"
|
||||
},
|
||||
"jishen": {
|
||||
"type": "string",
|
||||
"example": "无"
|
||||
},
|
||||
"shijian": {
|
||||
"type": "string",
|
||||
"example": "21:00:00-22:59:59"
|
||||
},
|
||||
"xiongshen": {
|
||||
"type": "string",
|
||||
"example": "玄武"
|
||||
},
|
||||
"shichong": {
|
||||
"type": "string",
|
||||
"example": "冲蛇"
|
||||
},
|
||||
"shizhu": {
|
||||
"type": "string",
|
||||
"example": "乙亥"
|
||||
}
|
||||
}
|
||||
},
|
||||
"wei": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"jixiong": {
|
||||
"type": "string",
|
||||
"example": "天德(吉)"
|
||||
},
|
||||
"jishen": {
|
||||
"type": "string",
|
||||
"example": "天德福星贵人"
|
||||
},
|
||||
"shijian": {
|
||||
"type": "string",
|
||||
"example": "13:00:00-14:59:59"
|
||||
},
|
||||
"xiongshen": {
|
||||
"type": "string",
|
||||
"example": "无"
|
||||
},
|
||||
"shichong": {
|
||||
"type": "string",
|
||||
"example": "冲牛"
|
||||
},
|
||||
"shizhu": {
|
||||
"type": "string",
|
||||
"example": "辛未"
|
||||
}
|
||||
}
|
||||
},
|
||||
"cheng": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"jixiong": {
|
||||
"type": "string",
|
||||
"example": "天刑(凶)"
|
||||
},
|
||||
"jishen": {
|
||||
"type": "string",
|
||||
"example": "无"
|
||||
},
|
||||
"shijian": {
|
||||
"type": "string",
|
||||
"example": "7:00:00-8:59:59"
|
||||
},
|
||||
"xiongshen": {
|
||||
"type": "string",
|
||||
"example": "天刑日害"
|
||||
},
|
||||
"shichong": {
|
||||
"type": "string",
|
||||
"example": "冲狗"
|
||||
},
|
||||
"shizhu": {
|
||||
"type": "string",
|
||||
"example": "戊辰"
|
||||
}
|
||||
}
|
||||
},
|
||||
"you": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"jixiong": {
|
||||
"type": "string",
|
||||
"example": "玉堂(吉)"
|
||||
},
|
||||
"jishen": {
|
||||
"type": "string",
|
||||
"example": "玉堂文昌贵人"
|
||||
},
|
||||
"shijian": {
|
||||
"type": "string",
|
||||
"example": "17:00:00-18:59:59"
|
||||
},
|
||||
"xiongshen": {
|
||||
"type": "string",
|
||||
"example": "日破"
|
||||
},
|
||||
"shichong": {
|
||||
"type": "string",
|
||||
"example": "冲兔"
|
||||
},
|
||||
"shizhu": {
|
||||
"type": "string",
|
||||
"example": "癸酉"
|
||||
}
|
||||
}
|
||||
},
|
||||
"wu": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"jixiong": {
|
||||
"type": "string",
|
||||
"example": "金匮(吉)"
|
||||
},
|
||||
"jishen": {
|
||||
"type": "string",
|
||||
"example": "金匮日禄"
|
||||
},
|
||||
"shijian": {
|
||||
"type": "string",
|
||||
"example": "11:00:00-12:59:59"
|
||||
},
|
||||
"xiongshen": {
|
||||
"type": "string",
|
||||
"example": "无"
|
||||
},
|
||||
"shichong": {
|
||||
"type": "string",
|
||||
"example": "冲鼠"
|
||||
},
|
||||
"shizhu": {
|
||||
"type": "string",
|
||||
"example": "庚午"
|
||||
}
|
||||
}
|
||||
},
|
||||
"si": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"jixiong": {
|
||||
"type": "string",
|
||||
"example": "朱雀(凶)"
|
||||
},
|
||||
"jishen": {
|
||||
"type": "string",
|
||||
"example": "日马"
|
||||
},
|
||||
"shijian": {
|
||||
"type": "string",
|
||||
"example": "9:00:00-10:59:59"
|
||||
},
|
||||
"xiongshen": {
|
||||
"type": "string",
|
||||
"example": "朱雀"
|
||||
},
|
||||
"shichong": {
|
||||
"type": "string",
|
||||
"example": "冲猪"
|
||||
},
|
||||
"shizhu": {
|
||||
"type": "string",
|
||||
"example": "己巳"
|
||||
}
|
||||
}
|
||||
},
|
||||
"xu": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"jixiong": {
|
||||
"type": "string",
|
||||
"example": "天牢(凶)"
|
||||
},
|
||||
"jishen": {
|
||||
"type": "string",
|
||||
"example": "日合"
|
||||
},
|
||||
"shijian": {
|
||||
"type": "string",
|
||||
"example": "19:00:00-20:59:59"
|
||||
},
|
||||
"xiongshen": {
|
||||
"type": "string",
|
||||
"example": "天牢"
|
||||
},
|
||||
"shichong": {
|
||||
"type": "string",
|
||||
"example": "冲龙"
|
||||
},
|
||||
"shizhu": {
|
||||
"type": "string",
|
||||
"example": "甲戌"
|
||||
}
|
||||
}
|
||||
},
|
||||
"chou": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"jixiong": {
|
||||
"type": "string",
|
||||
"example": "勾陈(凶)"
|
||||
},
|
||||
"jishen": {
|
||||
"type": "string",
|
||||
"example": "无"
|
||||
},
|
||||
"shijian": {
|
||||
"type": "string",
|
||||
"example": "1:00:00-2:59:59"
|
||||
},
|
||||
"xiongshen": {
|
||||
"type": "string",
|
||||
"example": "勾陈"
|
||||
},
|
||||
"shichong": {
|
||||
"type": "string",
|
||||
"example": "冲羊"
|
||||
},
|
||||
"shizhu": {
|
||||
"type": "string",
|
||||
"example": "乙丑"
|
||||
}
|
||||
}
|
||||
},
|
||||
"yin": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"jixiong": {
|
||||
"type": "string",
|
||||
"example": "青龙(吉)"
|
||||
},
|
||||
"jishen": {
|
||||
"type": "string",
|
||||
"example": "青龙喜神天官贵人"
|
||||
},
|
||||
"shijian": {
|
||||
"type": "string",
|
||||
"example": "3:00:00-4:59:59"
|
||||
},
|
||||
"xiongshen": {
|
||||
"type": "string",
|
||||
"example": "无"
|
||||
},
|
||||
"shichong": {
|
||||
"type": "string",
|
||||
"example": "冲猴"
|
||||
},
|
||||
"shizhu": {
|
||||
"type": "string",
|
||||
"example": "丙寅"
|
||||
}
|
||||
}
|
||||
},
|
||||
"shen": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"jixiong": {
|
||||
"type": "string",
|
||||
"example": "白虎(凶)"
|
||||
},
|
||||
"jishen": {
|
||||
"type": "string",
|
||||
"example": "天乙贵人"
|
||||
},
|
||||
"shijian": {
|
||||
"type": "string",
|
||||
"example": "15:00:00-16:59:59"
|
||||
},
|
||||
"xiongshen": {
|
||||
"type": "string",
|
||||
"example": "白虎"
|
||||
},
|
||||
"shichong": {
|
||||
"type": "string",
|
||||
"example": "冲虎"
|
||||
},
|
||||
"shizhu": {
|
||||
"type": "string",
|
||||
"example": "壬申"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mao": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"jixiong": {
|
||||
"type": "string",
|
||||
"example": "明堂(吉)"
|
||||
},
|
||||
"jishen": {
|
||||
"type": "string",
|
||||
"example": "明堂"
|
||||
},
|
||||
"shijian": {
|
||||
"type": "string",
|
||||
"example": "5:00:00-6:59:59"
|
||||
},
|
||||
"xiongshen": {
|
||||
"type": "string",
|
||||
"example": "无"
|
||||
},
|
||||
"shichong": {
|
||||
"type": "string",
|
||||
"example": "冲鸡"
|
||||
},
|
||||
"shizhu": {
|
||||
"type": "string",
|
||||
"example": "丁卯"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "成功响应"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/holiday/detail": {
|
||||
"post": {
|
||||
"operationId": "节假日详情",
|
||||
"summary": "节假日详情",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/x-www-form-urlencoded": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"date": {
|
||||
"description": "查询的日期,默认当天",
|
||||
"type": "string"
|
||||
},
|
||||
"needDesc": {
|
||||
"description": "是否需要返回当日公众日、国际日和我国传统节日的简介,1-返回,默认不返回",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"description": "返回码"
|
||||
},
|
||||
"msg": {
|
||||
"type": "string",
|
||||
"description": "返回消息"
|
||||
},
|
||||
"taskNo": {
|
||||
"type": "string",
|
||||
"description": "请求号"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"day": {
|
||||
"type": "string",
|
||||
"description": "查询的日期"
|
||||
},
|
||||
"holiday": {
|
||||
"type": "string",
|
||||
"description": "节日名称"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "日期类型"
|
||||
},
|
||||
"begin": {
|
||||
"type": "string",
|
||||
"description": "节日或周末开始时间"
|
||||
},
|
||||
"end": {
|
||||
"type": "string",
|
||||
"description": "节日或周末结束时间"
|
||||
},
|
||||
"holiday_remark": {
|
||||
"type": "string",
|
||||
"description": "节日备注"
|
||||
},
|
||||
"weekDay": {
|
||||
"type": "integer",
|
||||
"description": "星期几的数字"
|
||||
},
|
||||
"cn": {
|
||||
"type": "string",
|
||||
"description": "星期几的中文名"
|
||||
},
|
||||
"en": {
|
||||
"type": "string",
|
||||
"description": "星期几的英文名"
|
||||
},
|
||||
"h": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "节日名称"
|
||||
},
|
||||
"genus": {
|
||||
"type": "string",
|
||||
"description": "节日种类"
|
||||
},
|
||||
"day": {
|
||||
"type": "string",
|
||||
"description": "节日公历日期"
|
||||
},
|
||||
"lunaDay": {
|
||||
"type": "string"
|
||||
},
|
||||
"info": {
|
||||
"type": "string"
|
||||
},
|
||||
"origin": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "成功响应"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "https://jmhlysjjr.market.alicloudapi.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
server:
|
||||
name: calendar-holiday-helper
|
||||
config:
|
||||
appCode: ""
|
||||
tools:
|
||||
- name: chinese-holiday-list
|
||||
description: 查询指定年份的中国节假日信息,调休信息等
|
||||
args:
|
||||
- name: year
|
||||
description: 需要查询的年份,可以传空字符串表示今年,在不确定年份的情况下,请传空字符串
|
||||
type: string
|
||||
required: true
|
||||
requestTemplate:
|
||||
url: https://jmhlysjjr.market.alicloudapi.com/holiday/list
|
||||
method: POST
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/x-www-form-urlencoded
|
||||
- key: Authorization
|
||||
value: APPCODE {{.config.appCode}}
|
||||
- key: X-Ca-Nonce
|
||||
value: '{{uuidv4}}'
|
||||
body: |
|
||||
year={{ if empty .args.year }}{{ now | date "2006" }}{{ else }}{{ .args.year }}{{ end }}
|
||||
responseTemplate:
|
||||
prependBody: |+
|
||||
# API Response Information
|
||||
|
||||
Below is the response from an API call. To help you understand the data, I've provided:
|
||||
|
||||
1. A detailed description of all fields in the response structure
|
||||
2. The complete API response
|
||||
|
||||
## Response Structure
|
||||
|
||||
> Content-Type: application/json
|
||||
|
||||
- **code**: 返回码 (Type: integer)
|
||||
- **data**: (Type: object)
|
||||
- **data.count**: 一年的节假日数量 (Type: integer)
|
||||
- **data.items**: (Type: array)
|
||||
- **data.items[].begin**: 节假日的开始时间 (Type: string)
|
||||
- **data.items[].end**: 节假日的结束时间 (Type: string)
|
||||
- **data.items[].holiday**: 节假日名称 (Type: string)
|
||||
- **data.items[].holiday_remark**: 节假日描述 (Type: string)
|
||||
- **data.items[].inverse_days**: 具体调修日期列表 (Type: array)
|
||||
- **data.items[].inverse_days[]**: Items of type string
|
||||
- **msg**: 返回信息 (Type: string)
|
||||
- **taskNo**: 请求号 (Type: string)
|
||||
|
||||
## Original Response
|
||||
|
||||
- name: chinese-holiday-detail
|
||||
description: 中国节假日详情查询
|
||||
args:
|
||||
- name: date
|
||||
description: 查询的日期,格式为yyyyMMdd,可以传空字符串,则默认当天,在不确定日期的情况下,请传空字符串
|
||||
type: string
|
||||
required: true
|
||||
- name: needDesc
|
||||
description: 是否需要返回当日公众日、国际日和我国传统节日的简介,1-返回,默认不返回
|
||||
type: string
|
||||
position: body
|
||||
requestTemplate:
|
||||
url: https://jmhlysjjr.market.alicloudapi.com/holiday/detail
|
||||
method: POST
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/x-www-form-urlencoded
|
||||
- key: Authorization
|
||||
value: APPCODE {{.config.appCode}}
|
||||
- key: X-Ca-Nonce
|
||||
value: '{{uuidv4}}'
|
||||
body: |
|
||||
date={{ if empty .args.date }}{{ dateInZone "20060102" now "Asia/Shanghai" }}{{ else }}{{ .args.date }}{{ end }}
|
||||
responseTemplate:
|
||||
prependBody: |+
|
||||
# API Response Information
|
||||
|
||||
Below is the response from an API call. To help you understand the data, I've provided:
|
||||
|
||||
1. A detailed description of all fields in the response structure
|
||||
2. The complete API response
|
||||
|
||||
## Response Structure
|
||||
|
||||
> Content-Type: application/json
|
||||
|
||||
- **code**: 返回码 (Type: integer)
|
||||
- **data**: (Type: object)
|
||||
- **data.begin**: 节日或周末开始时间 (Type: string)
|
||||
- **data.cn**: 星期几的中文名 (Type: string)
|
||||
- **data.day**: 查询的日期 (Type: string)
|
||||
- **data.en**: 星期几的英文名 (Type: string)
|
||||
- **data.end**: 节日或周末结束时间 (Type: string)
|
||||
- **data.h**: (Type: array)
|
||||
- **data.h[].day**: 节日公历日期 (Type: string)
|
||||
- **data.h[].genus**: 节日种类 (Type: string)
|
||||
- **data.h[].info**: (Type: string)
|
||||
- **data.h[].lunaDay**: (Type: string)
|
||||
- **data.h[].name**: 节日名称 (Type: string)
|
||||
- **data.h[].origin**: (Type: string)
|
||||
- **data.holiday**: 节日名称 (Type: string)
|
||||
- **data.holiday_remark**: 节日备注 (Type: string)
|
||||
- **data.type**: 日期类型 (Type: string)
|
||||
- **data.weekDay**: 星期几的数字 (Type: integer)
|
||||
- **msg**: 返回消息 (Type: string)
|
||||
- **taskNo**: 请求号 (Type: string)
|
||||
|
||||
## Original Response
|
||||
|
||||
- name: chinese-almanac-auspicious-time
|
||||
description: 查询中国黄历指定日期的吉时
|
||||
args:
|
||||
- name: date
|
||||
description: 查询的日期,格式为yyyyMMdd,可以传空字符串,则默认当天,在不确定日期的情况下,请传空字符串
|
||||
type: string
|
||||
required: true
|
||||
- name: timeZone
|
||||
description: 基于IANA时区数据库规范的时区字符串(例如:Asia/Shanghai),仅当date传空字符串时有效,用于在服务器获取指定时区下的当天日期,可以尝试通过IP定位用户的位置,从而获取时区
|
||||
type: string
|
||||
default: Asia/Shanghai
|
||||
requestTemplate:
|
||||
url: https://jmhlysjjr.market.alicloudapi.com/luck-tendency/auspicious-time
|
||||
method: POST
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/x-www-form-urlencoded
|
||||
- key: Authorization
|
||||
value: APPCODE {{.config.appCode}}
|
||||
- key: X-Ca-Nonce
|
||||
value: '{{uuidv4}}'
|
||||
body: |
|
||||
date={{ if empty .args.date }}{{ dateInZone "20060102" now .args.timeZone }}{{ else }}{{ .args.date }}{{ end }}
|
||||
responseTemplate:
|
||||
prependBody: |+
|
||||
# API Response Information
|
||||
|
||||
Below is the response from an API call. To help you understand the data, I've provided:
|
||||
|
||||
1. A detailed description of all fields in the response structure
|
||||
2. The complete API response
|
||||
|
||||
## Response Structure
|
||||
|
||||
> Content-Type: application/json
|
||||
|
||||
- **code**: (Type: integer)
|
||||
- **data**: (Type: object)
|
||||
- **data.ut**: 来源数据更新时间 (Type: string)
|
||||
- **data.zi**: 子时 (11pm - 1am)
|
||||
- **data.zi.jishen**: 吉神(其他时辰含义相同,不赘述) (Type: string)
|
||||
- **data.zi.jixiong**: 吉凶 (其他时辰含义相同,不赘述)(Type: string)
|
||||
- **data.zi.shichong**: 相冲的生肖(其他时辰含义相同,不赘述) (Type: string)
|
||||
- **data.zi.shijian**: 时间(其他时辰含义相同,不赘述)(Type: string)
|
||||
- **data.zi.shizhu**: 时柱(其他时辰含义相同,不赘述) (Type: string)
|
||||
- **data.zi.xiongshen**: 凶神 (Type: string)
|
||||
- **data.chou**: 丑时 (1am - 3am)
|
||||
- **data.chou.jishen**: (Type: string)
|
||||
- **data.chou.jixiong**: (Type: string)
|
||||
- **data.chou.shichong**: (Type: string)
|
||||
- **data.chou.shijian**: (Type: string)
|
||||
- **data.chou.shizhu**: (Type: string)
|
||||
- **data.chou.xiongshen**: (Type: string)
|
||||
- **data.yin**: 寅时 (3am - 5am)
|
||||
- **data.yin.jishen**: (Type: string)
|
||||
- **data.yin.jixiong**: (Type: string)
|
||||
- **data.yin.shichong**: (Type: string)
|
||||
- **data.yin.shijian**: (Type: string)
|
||||
- **data.yin.shizhu**: (Type: string)
|
||||
- **data.yin.xiongshen**: (Type: string)
|
||||
- **data.mao**: 卯时 (5am - 7am)
|
||||
- **data.mao.jishen**: (Type: string)
|
||||
- **data.mao.jixiong**: (Type: string)
|
||||
- **data.mao.shichong**: (Type: string)
|
||||
- **data.mao.shijian**: (Type: string)
|
||||
- **data.mao.shizhu**: (Type: string)
|
||||
- **data.mao.xiongshen**: (Type: string)
|
||||
- **data.cheng**: 辰时 (7am - 9am)
|
||||
- **data.cheng.jishen**: 吉神 (Type: string)
|
||||
- **data.cheng.jixiong**: 吉凶 (Type: string)
|
||||
- **data.cheng.shichong**: 时冲 (Type: string)
|
||||
- **data.cheng.shijian**: 时间 (Type: string)
|
||||
- **data.cheng.shizhu**: 时柱 (Type: string)
|
||||
- **data.cheng.xiongshen**: 凶神 (Type: string)
|
||||
- **data.si**: 巳时 (9am - 11am)
|
||||
- **data.si.jishen**: (Type: string)
|
||||
- **data.si.jixiong**: (Type: string)
|
||||
- **data.si.shichong**: (Type: string)
|
||||
- **data.si.shijian**: (Type: string)
|
||||
- **data.si.shizhu**: (Type: string)
|
||||
- **data.si.xiongshen**: (Type: string)
|
||||
- **data.wu**: 午时 (11am - 1pm)
|
||||
- **data.wu.jishen**: (Type: string)
|
||||
- **data.wu.jixiong**: (Type: string)
|
||||
- **data.wu.shichong**: (Type: string)
|
||||
- **data.wu.shijian**: (Type: string)
|
||||
- **data.wu.shizhu**: (Type: string)
|
||||
- **data.wu.xiongshen**: (Type: string)
|
||||
- **data.wei**: 未时 (1pm - 3pm)
|
||||
- **data.wei.jishen**: (Type: string)
|
||||
- **data.wei.jixiong**: (Type: string)
|
||||
- **data.wei.shichong**: (Type: string)
|
||||
- **data.wei.shijian**: (Type: string)
|
||||
- **data.wei.shizhu**: (Type: string)
|
||||
- **data.wei.xiongshen**: (Type: string)
|
||||
- **data.shen**: 申时 (3pm - 5pm)
|
||||
- **data.shen.jishen**: (Type: string)
|
||||
- **data.shen.jixiong**: (Type: string)
|
||||
- **data.shen.shichong**: (Type: string)
|
||||
- **data.shen.shijian**: (Type: string)
|
||||
- **data.shen.shizhu**: (Type: string)
|
||||
- **data.shen.xiongshen**: (Type: string)
|
||||
- **data.you**: 酉时 (5pm - 7pm)
|
||||
- **data.you.jishen**: (Type: string)
|
||||
- **data.you.jixiong**: (Type: string)
|
||||
- **data.you.shichong**: (Type: string)
|
||||
- **data.you.shijian**: (Type: string)
|
||||
- **data.you.shizhu**: (Type: string)
|
||||
- **data.you.xiongshen**: (Type: string)
|
||||
- **data.xu**: 戌时 (7pm - 9pm)
|
||||
- **data.xu.jishen**: (Type: string)
|
||||
- **data.xu.jixiong**: (Type: string)
|
||||
- **data.xu.shichong**: (Type: string)
|
||||
- **data.xu.shijian**: (Type: string)
|
||||
- **data.xu.shizhu**: (Type: string)
|
||||
- **data.xu.xiongshen**: (Type: string)
|
||||
- **data.hai**: 亥时 (9pm - 11pm)
|
||||
- **data.hai.jishen**: (Type: string)
|
||||
- **data.hai.jixiong**: (Type: string)
|
||||
- **data.hai.shichong**: (Type: string)
|
||||
- **data.hai.shijian**: (Type: string)
|
||||
- **data.hai.shizhu**: (Type: string)
|
||||
- **data.hai.xiongshen**: (Type: string)
|
||||
|
||||
## Original Response
|
||||
|
||||
- name: chinese-almanac-time-deities-and-spirits
|
||||
description: 查询中国黄历指定日期的吉神凶煞
|
||||
args:
|
||||
- name: date
|
||||
description: 查询的日期,格式为yyyyMMdd,可以传空字符串,则默认当天,在不确定日期的情况下,请传空字符串
|
||||
type: string
|
||||
required: true
|
||||
- name: timeZone
|
||||
description: 基于IANA时区数据库规范的时区字符串(例如:Asia/Shanghai),仅当date传空字符串时有效,用于在服务器获取指定时区下的当天日期,可以尝试通过IP定位用户的位置,从而获取时区
|
||||
type: string
|
||||
default: Asia/Shanghai
|
||||
requestTemplate:
|
||||
url: https://jmhlysjjr.market.alicloudapi.com/luck-tendency/auspicious-demon
|
||||
method: POST
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/x-www-form-urlencoded
|
||||
- key: Authorization
|
||||
value: APPCODE {{.config.appCode}}
|
||||
- key: X-Ca-Nonce
|
||||
value: '{{uuidv4}}'
|
||||
body: |
|
||||
date={{ if empty .args.date }}{{ dateInZone "20060102" now .args.timeZone }}{{ else }}{{ .args.date }}{{ end }}
|
||||
responseTemplate:
|
||||
prependBody: |+
|
||||
# API Response Information
|
||||
|
||||
Below is the response from an API call. To help you understand the data, I've provided:
|
||||
|
||||
1. A detailed description of all fields in the response structure
|
||||
2. The complete API response
|
||||
|
||||
## Response Structure
|
||||
|
||||
> Content-Type: application/json
|
||||
|
||||
- **code**: (Type: integer)
|
||||
- **data**: (Type: object)
|
||||
- **data.caishen**: 财神 (Type: string)
|
||||
- **data.esbx**: 二十八宿 (Type: string)
|
||||
- **data.fantaisui**: 犯太岁 (Type: string)
|
||||
- **data.fushen**: 福神 (Type: string)
|
||||
- **data.jiuxing**: 九星 (Type: string)
|
||||
- **data.liuyao**: 六曜 (Type: string)
|
||||
- **data.niankongwang**: 年空亡 (Type: string)
|
||||
- **data.nianqisha**: 年空亡 (Type: string)
|
||||
- **data.niansansha**: 年三煞 (Type: string)
|
||||
- **data.niantaisui**: 年太岁 (Type: string)
|
||||
- **data.rikongwang**: 日空亡 (Type: string)
|
||||
- **data.rilu**: 日禄 (Type: string)
|
||||
- **data.riqisha**: 日七煞 (Type: string)
|
||||
- **data.risansha**: 日三煞 (Type: string)
|
||||
- **data.suipowei**: 岁破位 (Type: string)
|
||||
- **data.taisuiwei**: 太岁位 (Type: string)
|
||||
- **data.tjjs**: 推荐吉时 (Type: string)
|
||||
- **data.wuhou**: 物候 (Type: string)
|
||||
- **data.xishen**: 喜神 (Type: string)
|
||||
- **data.yangguishen**: 阳贵神 (Type: string)
|
||||
- **data.yinguishen**: 阴贵神 (Type: string)
|
||||
- **data.yjgx**: 易经卦象 (Type: string)
|
||||
- **data.yuekongwang**: 月空亡 (Type: string)
|
||||
- **data.yueling**: 月令 (Type: string)
|
||||
- **data.yueqisha**: 月七煞 (Type: string)
|
||||
- **data.yuesansha**: 月三煞 (Type: string)
|
||||
- **data.yuexiang**: 月相 (Type: string)
|
||||
- **data.yuezhi**: 月支 (Type: string)
|
||||
- **data.zhiri12**: 十二值日 (Type: string)
|
||||
- **data.zhishn12**: 十二值神 (Type: string)
|
||||
- **data.zhongdong**: 仲冬 (Type: string)
|
||||
- **msg**: (Type: string)
|
||||
- **taskNo**: (Type: string)
|
||||
|
||||
## Original Response
|
||||
|
||||
- name: chinese-almanac-time-detail
|
||||
description: 查询中国黄历指定日期的详情信息
|
||||
args:
|
||||
- name: date
|
||||
description: 查询的日期,格式为yyyyMMdd,可以传空字符串,则默认当天,在不确定日期的情况下,请传空字符串
|
||||
type: string
|
||||
required: true
|
||||
- name: timeZone
|
||||
description: 基于IANA时区数据库规范的时区字符串(例如:Asia/Shanghai),仅当date传空字符串时有效,用于在服务器获取指定时区下的当天日期,可以尝试通过IP定位用户的位置,从而获取时区
|
||||
type: string
|
||||
default: Asia/Shanghai
|
||||
requestTemplate:
|
||||
url: https://jmhlysjjr.market.alicloudapi.com/luck-tendency/almanac
|
||||
method: POST
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/x-www-form-urlencoded
|
||||
- key: Authorization
|
||||
value: APPCODE {{.config.appCode}}
|
||||
- key: X-Ca-Nonce
|
||||
value: '{{uuidv4}}'
|
||||
body: |
|
||||
date={{ if empty .args.date }}{{ dateInZone "20060102" now .args.timeZone }}{{ else }}{{ .args.date }}{{ end }}
|
||||
responseTemplate:
|
||||
prependBody: |+
|
||||
# API Response Information
|
||||
|
||||
Below is the response from an API call. To help you understand the data, I've provided:
|
||||
|
||||
1. A detailed description of all fields in the response structure
|
||||
2. The complete API response
|
||||
|
||||
## Response Structure
|
||||
|
||||
> Content-Type: application/json
|
||||
|
||||
- **code**: (Type: integer)
|
||||
- **data**: (Type: object)
|
||||
- **data.chongsha**: 冲煞 (Type: string)
|
||||
- **data.dizhi**: 地支 (Type: string)
|
||||
- **data.ganzhi**: 干支 (Type: string)
|
||||
- **data.gongli**: 公历 (Type: string)
|
||||
- **data.ji**: 忌 (Type: string)
|
||||
- **data.jieqi24**: 当前月包含的24节气 (Type: string)
|
||||
- **data.jieri**: 节日 (Type: string)
|
||||
- **data.jsyq**: 吉神宜趋 (Type: string)
|
||||
- **data.nayin**: 纳音 (Type: string)
|
||||
- **data.nongli**: 农历 (Type: string)
|
||||
- **data.pzbj**: 彭祖百忌 (Type: string)
|
||||
- **data.qixiang**: 气象 (Type: string)
|
||||
- **data.rulueli**: 儒略历 (Type: string)
|
||||
- **data.shengxiao**: 生肖 (Type: string)
|
||||
- **data.tszf**: 胎神占方 (Type: string)
|
||||
- **data.xingzuo**: 星座 (Type: string)
|
||||
- **data.xsyj**: 凶神宜忌 (Type: string)
|
||||
- **data.yi**: 宜 (Type: string)
|
||||
- **data.zhiri**: 值日 (Type: string)
|
||||
- **data.zhishen**: 值神 (Type: string)
|
||||
- **msg**: (Type: string)
|
||||
- **taskNo**: (Type: string)
|
||||
|
||||
## Original Response
|
||||
|
||||
27
plugins/wasm-go/mcp-servers/mcp-chatppt/README.md
Normal file
27
plugins/wasm-go/mcp-servers/mcp-chatppt/README.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# ChatPPT MCP Server
|
||||
|
||||
Biyou Technology's MCP Server currently covers 18 intelligent document processing interfaces, including but not limited to PPT creation, PPT beautification, PPT generation, resume creation, resume analysis, and person-job matching. Users can build their own document creation tools through the server, enabling more possibilities for intelligent document creation.
|
||||
|
||||
Source code: [https://github.com/YOOTeam/chatppt-mcp](https://github.com/YOOTeam/chatppt-mcp)
|
||||
|
||||
## Usage Guide
|
||||
|
||||
### Get API-KEY
|
||||
|
||||
Refer to the official documentation to get API-KEY [Create Application and Get Token](https://wiki.yoo-ai.com/mcp/McpServe/serve1.3.html)
|
||||
|
||||
### Generate SSE URL
|
||||
|
||||
On the MCP Server interface, log in and enter the API-KEY to generate the URL.
|
||||
|
||||
### Configure MCP Client
|
||||
|
||||
On the user's MCP Client interface, add the generated SSE URL to the MCP Server list.
|
||||
|
||||
```json
|
||||
"mcpServers": {
|
||||
"chatppt": {
|
||||
"url": "http://mcp.higress.ai/mcp-chatppt/{generate_key}",
|
||||
}
|
||||
}
|
||||
```
|
||||
28
plugins/wasm-go/mcp-servers/mcp-chatppt/README_ZH.md
Normal file
28
plugins/wasm-go/mcp-servers/mcp-chatppt/README_ZH.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# ChatPPT MCP Server
|
||||
|
||||
必优科技 MCP Server 目前已经覆盖了 18 个智能文档的接口能力,包括但不限于 PPT 创作,PPT 美化,PPT 生成,简历创作,简历分析,人岗匹配等场景下的文档处理能力,用户可通过 server 搭建自己的文档创作工具,让智能文档创作有更多可能。
|
||||
|
||||
源码地址: [https://github.com/YOOTeam/chatppt-mcp](https://github.com/YOOTeam/chatppt-mcp)
|
||||
|
||||
## 使用教程
|
||||
|
||||
### 获取 API-KEY
|
||||
|
||||
参考官方文档获取 API-KEY [创建应用获取 Token](https://wiki.yoo-ai.com/mcp/McpServe/serve1.3.html)
|
||||
|
||||
### 生成 SSE URL
|
||||
|
||||
在 MCP Server 界面,登录后输入 API-KEY,生成URL。
|
||||
|
||||
### 配置 MCP Client
|
||||
|
||||
在用户的 MCP Client 界面,将生成的 SSE URL添加到 MCP Server列表中。
|
||||
|
||||
```json
|
||||
"mcpServers": {
|
||||
"chatppt": {
|
||||
"url": "http://mcp.higress.ai/mcp-chatppt/{generate_key}",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
121
plugins/wasm-go/mcp-servers/mcp-chatppt/mcp-server.yaml
Normal file
121
plugins/wasm-go/mcp-servers/mcp-chatppt/mcp-server.yaml
Normal file
@@ -0,0 +1,121 @@
|
||||
server:
|
||||
name: chatppt-server
|
||||
config:
|
||||
apiKey: ""
|
||||
|
||||
tools:
|
||||
- name: check
|
||||
description: "查询用户当前配置token"
|
||||
args: []
|
||||
requestTemplate:
|
||||
url: "https://saas.api.yoo-ai.com"
|
||||
method: GET
|
||||
headers:
|
||||
- key: Authorization
|
||||
value: "Bearer {{.config.apiKey}}"
|
||||
responseTemplate:
|
||||
body: |
|
||||
{
|
||||
"apiKey": "{{.body}}"
|
||||
}
|
||||
|
||||
- name: query_ppt
|
||||
description: "根据PPT任务ID查询异步生成结果"
|
||||
args:
|
||||
- name: ppt_id
|
||||
description: "PPT-ID"
|
||||
type: string
|
||||
required: true
|
||||
requestTemplate:
|
||||
url: "https://saas.api.yoo-ai.com/apps/ppt-result"
|
||||
method: GET
|
||||
argsToUrlParam: true
|
||||
headers:
|
||||
- key: Authorization
|
||||
value: "Bearer {{.config.apiKey}}"
|
||||
responseTemplate:
|
||||
body: |
|
||||
{
|
||||
"status": "{{.body.status}}",
|
||||
"process_url": "{{.body.process_url}}"
|
||||
}
|
||||
|
||||
- name: build_ppt
|
||||
description: "根据描述的文本或markdown生成PPT"
|
||||
args:
|
||||
- name: text
|
||||
description: "输入描述的文本或markdown"
|
||||
type: string
|
||||
required: true
|
||||
requestTemplate:
|
||||
url: "https://saas.api.yoo-ai.com/apps/ppt-create"
|
||||
method: POST
|
||||
argsToFormBody: true
|
||||
headers:
|
||||
- key: Authorization
|
||||
value: "Bearer {{.config.apiKey}}"
|
||||
responseTemplate:
|
||||
body: |
|
||||
{
|
||||
"ppt_id": "{{.body}}"
|
||||
}
|
||||
|
||||
- name: replace_template_ppt
|
||||
description: "根据PPT-ID执行替换模板"
|
||||
args:
|
||||
- name: ppt_id
|
||||
description: "PPT-ID"
|
||||
type: string
|
||||
required: true
|
||||
requestTemplate:
|
||||
url: "https://saas.api.yoo-ai.com/apps/ppt-create-task"
|
||||
method: POST
|
||||
argsToFormBody: true
|
||||
headers:
|
||||
- key: Authorization
|
||||
value: "Bearer {{.config.apiKey}}"
|
||||
responseTemplate:
|
||||
body: |
|
||||
{
|
||||
"new_ppt_id": "{{.body}}"
|
||||
}
|
||||
|
||||
- name: download_ppt
|
||||
description: "生成PPT下载地址"
|
||||
args:
|
||||
- name: ppt_id
|
||||
description: "PPT-ID"
|
||||
type: string
|
||||
required: true
|
||||
requestTemplate:
|
||||
url: "https://saas.api.yoo-ai.com/apps/ppt-download"
|
||||
method: GET
|
||||
argsToUrlParam: true
|
||||
headers:
|
||||
- key: Authorization
|
||||
value: "Bearer {{.config.apiKey}}"
|
||||
responseTemplate:
|
||||
body: |
|
||||
{
|
||||
"download_url": "{{.body}}"
|
||||
}
|
||||
|
||||
- name: editor_ppt
|
||||
description: "生成PPT编辑器界面URL"
|
||||
args:
|
||||
- name: ppt_id
|
||||
description: "PPT-ID"
|
||||
type: string
|
||||
required: true
|
||||
requestTemplate:
|
||||
url: "https://saas.api.yoo-ai.com/apps/ppt-editor"
|
||||
method: POST
|
||||
argsToFormBody: true
|
||||
headers:
|
||||
- key: Authorization
|
||||
value: "Bearer {{.config.apiKey}}"
|
||||
responseTemplate:
|
||||
body: |
|
||||
{
|
||||
"editor_url": "{{.body}}"
|
||||
}
|
||||
38
plugins/wasm-go/mcp-servers/mcp-deadbeat-query/README.md
Normal file
38
plugins/wasm-go/mcp-servers/mcp-deadbeat-query/README.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Deadbeat Inquiry
|
||||
|
||||
The APP Code required for API authentication can be applied for on the Alibaba Cloud API Marketplace: https://market.aliyun.com/apimarket/detail/cmapi00047480
|
||||
|
||||
# MCP Server Configuration Document
|
||||
|
||||
## Overview
|
||||
This MCP server, named `deadbeat-query`, primarily serves as an external interface that allows users to query whether a person is a dishonest debtor by submitting basic personal information such as name, ID number, and mobile phone number. This service is particularly useful for financial institutions, credit departments, and other entities that need to perform risk control, helping these organizations quickly understand the credit status of potential customers and make more informed decisions.
|
||||
|
||||
## Tool Introduction
|
||||
### Dishonest Debtor Information Inquiry
|
||||
- **Purpose**: This tool is specifically designed to check if there are any records of a person being a dishonest debtor based on the provided personal information.
|
||||
- **Use Cases**: It is applicable in various fields such as bank loan approvals, lease business reviews, and employment background checks, where it can effectively assess the creditworthiness of relevant individuals.
|
||||
|
||||
#### Input Parameters
|
||||
- `idcard_number`: Required, represents the identification number of the individual being queried.
|
||||
- `mobile_number`: Required, indicates the mobile phone number used by the individual being queried.
|
||||
- `name`: Required, specifies the name of the person to be queried.
|
||||
|
||||
#### Request Example
|
||||
- **URL**: `https://jumjokk.market.alicloudapi.com/personal/disenforcement`
|
||||
- **Method**: POST
|
||||
- **Headers**:
|
||||
- `Content-Type: application/x-www-form-urlencoded`
|
||||
- `Authorization: APPCODE <appCode value>` (Replace `<appCode value>` with the actual application code)
|
||||
- `X-Ca-Nonce: <randomly generated unique identifier>`
|
||||
|
||||
#### Response Structure
|
||||
The response will include the following fields:
|
||||
- `code`: An integer indicating the status code of the API call result.
|
||||
- `data`: An object type, which contains specific details of the dishonesty records when data is returned.
|
||||
- `caseCount`: An integer showing the number of related cases found.
|
||||
- `caseList`: An array listing all the details of the related cases.
|
||||
- Each element includes information such as age (`age`), area name (`areaname`), business entity (`buesinessentity`), etc.
|
||||
- `msg`: A string providing a message or error prompt regarding this query operation.
|
||||
- `taskNo`: A string, a unique task number that can be used to track the processing of this request.
|
||||
|
||||
Please note that to ensure privacy, security, and legal compliance, please make sure you have obtained the appropriate authorization and comply with local laws and regulations before using this service.
|
||||
49
plugins/wasm-go/mcp-servers/mcp-deadbeat-query/README_ZH.md
Normal file
49
plugins/wasm-go/mcp-servers/mcp-deadbeat-query/README_ZH.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# 老赖查询
|
||||
|
||||
API认证需要的APP Code请在阿里云API市场申请: https://market.aliyun.com/apimarket/detail/cmapi00047480
|
||||
|
||||
## 什么是云市场API MCP服务
|
||||
|
||||
阿里云云市场是生态伙伴的交易服务平台,我们致力于为合作伙伴提供覆盖上云、商业化和售卖的全链路服务,帮助客户高效获取、部署和管理优质生态产品。云市场的API服务涵盖以下几个类目:应用开发、身份验证与金融、车辆交通与物流、企业服务、短信与运营商、AI应用与OCR、生活服务。
|
||||
云市场API依托Higress提供MCP服务,您只需在云市场完成订阅并获取AppCode,通过Higress MCP Server进行配置,即可无缝集成云市场API服务。
|
||||
|
||||
## 如何在使用云市场API MCP服务
|
||||
|
||||
1. 进入API详情页,订阅该API。您可以优先使用免费试用。
|
||||
2. 前往云市场用户控制台,使用阿里云账号登陆后查看已订阅API服务的AppCode,并配置到Higress MCP Server的配置中。注意:在阿里云市场订阅API服务后,您将获得AppCode。对于您订阅的所有API服务,此AppCode是相同的,您只需使用这一个AppCode即可访问所有已订阅的API服务。
|
||||
3. 云市场用户控制台会实时展示已订阅的预付费API服务的可用额度,如您免费试用额度已用完,您可以选择重新订阅。
|
||||
|
||||
# MCP服务器配置文档
|
||||
|
||||
## 功能简介
|
||||
本MCP服务器被命名为`deadbeat-query`,其主要功能是提供一个对外接口服务,允许用户通过提交个人基本信息如姓名、身份证号及手机号来查询该个人是否为失信被执行人。此服务对于金融机构、信贷部门等需要进行风险控制的场合非常有用,可以帮助这些机构快速了解潜在客户的信用状况,从而做出更加合理的决策。
|
||||
|
||||
## 工具简介
|
||||
### 失信被执行人信息查询
|
||||
- **用途**:此工具专用于根据提供的个人信息查询特定人员是否存在失信被执行记录。
|
||||
- **使用场景**:适用于银行贷款审批、租赁业务审核、就业背景调查等多个领域,在这些场景下能够有效评估相关人士的信用水平。
|
||||
|
||||
#### 输入参数说明
|
||||
- `idcard_number`: 必须提供,代表查询对象的身份证明号码。
|
||||
- `mobile_number`: 必须提供,表示查询对象所使用的移动电话号码。
|
||||
- `name`: 必须提供,指明要查询的人名。
|
||||
|
||||
#### 请求示例
|
||||
- **URL**: `https://jumjokk.market.alicloudapi.com/personal/disenforcement`
|
||||
- **方法**: POST
|
||||
- **请求头**:
|
||||
- `Content-Type: application/x-www-form-urlencoded`
|
||||
- `Authorization: APPCODE <appCode值>` (其中`<appCode值>`需替换为实际的应用程序代码)
|
||||
- `X-Ca-Nonce: <随机生成的唯一标识符>`
|
||||
|
||||
#### 响应结构
|
||||
响应将包含以下字段:
|
||||
- `code`: 整型数值,指示API调用结果的状态码。
|
||||
- `data`: 对象类型,当有数据返回时包含具体的失信记录详情。
|
||||
- `caseCount`: 整数,显示找到的相关案件数量。
|
||||
- `caseList`: 数组形式,列出所有相关的案件细节。
|
||||
- 每个元素中包含的信息包括但不限于年龄(`age`)、地区名称(`areaname`)、商业实体(`buesinessentity`)等。
|
||||
- `msg`: 字符串类型,给出关于此次查询操作的消息或错误提示。
|
||||
- `taskNo`: 字符串类型,唯一的任务编号,可用于跟踪本次请求处理过程。
|
||||
|
||||
请注意,为了保证隐私安全及合法合规性,在使用本服务前,请确保已获得适当授权并遵守当地法律法规要求。
|
||||
150
plugins/wasm-go/mcp-servers/mcp-deadbeat-query/api.json
Normal file
150
plugins/wasm-go/mcp-servers/mcp-deadbeat-query/api.json
Normal file
@@ -0,0 +1,150 @@
|
||||
{
|
||||
"info": {
|
||||
"description": "【法院失信被执行人个人失信被执行全国失信被执行信息查询】查询个人失信被执行详细信息,包括主体名称,法院名称、案件状态,执行标的、案号、法定代表人、执行文号、发布日期、执行情况等。直连官方,实时查询。—— 我们只做精品!",
|
||||
"title": "【聚美智数】法院失信被执行人查询-个人失信被执行--全国失信被执行人-老赖黑名单-失信被执行人-失信被执行查询",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"openapi": "3.0.1",
|
||||
"paths": {
|
||||
"/personal/disenforcement": {
|
||||
"post": {
|
||||
"operationId": "失信被执行人信息查询",
|
||||
"summary": "根据姓名、身份证号和手机号返回个人失信被执行情况",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/x-www-form-urlencoded": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"idcard_number": {
|
||||
"description": "身份证号",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "姓名",
|
||||
"type": "string"
|
||||
},
|
||||
"mobile_number": {
|
||||
"description": "手机号",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"idcard_number",
|
||||
"mobile_number"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"msg": {
|
||||
"type": "string",
|
||||
"example": "成功"
|
||||
},
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"example": 200
|
||||
},
|
||||
"taskNo": {
|
||||
"type": "string",
|
||||
"example": "074388502348792558"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"caseCount": {
|
||||
"type": "integer",
|
||||
"example": 2
|
||||
},
|
||||
"caseList": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"datatype": {
|
||||
"type": "string",
|
||||
"example": "失信被执行人"
|
||||
},
|
||||
"iname": {
|
||||
"type": "string",
|
||||
"example": "赵五"
|
||||
},
|
||||
"sexname": {
|
||||
"type": "string",
|
||||
"example": "女性"
|
||||
},
|
||||
"age": {
|
||||
"type": "string",
|
||||
"example": 35
|
||||
},
|
||||
"casecode": {
|
||||
"type": "string",
|
||||
"example": "(2018)粤0106执2984号"
|
||||
},
|
||||
"gistcid": {
|
||||
"type": "string",
|
||||
"example": "(2016)粤0106民初9317号"
|
||||
},
|
||||
"areaname": {
|
||||
"type": "string",
|
||||
"example": "广东省"
|
||||
},
|
||||
"courtname": {
|
||||
"type": "string",
|
||||
"example": "广东省广州市天河区人民法院"
|
||||
},
|
||||
"regdate": {
|
||||
"type": "string",
|
||||
"format": "date",
|
||||
"example": "2018-02-01T00:00:00Z"
|
||||
},
|
||||
"publishdate": {
|
||||
"type": "string",
|
||||
"format": "date",
|
||||
"example": "2018-05-22T00:00:00Z"
|
||||
},
|
||||
"buesinessentity": {
|
||||
"type": "string"
|
||||
},
|
||||
"partytypename": {
|
||||
"type": "string"
|
||||
},
|
||||
"sign": {
|
||||
"type": "string"
|
||||
},
|
||||
"signalDesc": {
|
||||
"type": "string"
|
||||
},
|
||||
"signalRating": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "成功响应"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "https://jumjokk.market.alicloudapi.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user