mirror of
https://github.com/alibaba/higress.git
synced 2026-02-26 05:30:50 +08:00
Compare commits
28 Commits
v2.1.2-rc.
...
cr7258-pat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a289d914f | ||
|
|
675a8ce4a9 | ||
|
|
06c5ddd80b | ||
|
|
8ccc170500 | ||
|
|
ff308d5292 | ||
|
|
af8502b0b0 | ||
|
|
c683936b1c | ||
|
|
8b3f1aab1a | ||
|
|
b5eadcdbee | ||
|
|
8ca8fd27ab | ||
|
|
ab014cf912 | ||
|
|
3f67b05fab | ||
|
|
cd271c1f87 | ||
|
|
755de5ae67 | ||
|
|
40402e7dbd | ||
|
|
0a2fb35ae2 | ||
|
|
b16954d8c1 | ||
|
|
29370b18d7 | ||
|
|
c9733d405c | ||
|
|
ec6004dd27 | ||
|
|
ea9a6de8c3 | ||
|
|
5e40a700ae | ||
|
|
48b220453b | ||
|
|
489a800868 | ||
|
|
60c9f21e1c | ||
|
|
ab73f21017 | ||
|
|
806563298b | ||
|
|
02fabbb35f |
112
.github/workflows/helm-docs.yaml
vendored
112
.github/workflows/helm-docs.yaml
vendored
@@ -6,11 +6,13 @@ on:
|
||||
- "*"
|
||||
paths:
|
||||
- 'helm/**'
|
||||
- '!helm/higress/README.zh.md'
|
||||
workflow_dispatch: ~
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- 'helm/**'
|
||||
- '!helm/higress/README.zh.md'
|
||||
|
||||
jobs:
|
||||
helm:
|
||||
@@ -31,7 +33,7 @@ jobs:
|
||||
run: |
|
||||
GOBIN=$PWD GO111MODULE=on go install github.com/norwoodj/helm-docs/cmd/helm-docs@v1.14.2
|
||||
./helm-docs -c ${GITHUB_WORKSPACE}/helm/higress -f ../core/values.yaml
|
||||
DIFF=$(git diff ${GITHUB_WORKSPACE}/helm/higress/*md)
|
||||
DIFF=$(git diff ${GITHUB_WORKSPACE}/helm/higress/README.md)
|
||||
if [ ! -z "$DIFF" ]; then
|
||||
echo "Please use helm-docs in your clone, of your fork, of the project, and commit a updated README.md for the chart."
|
||||
fi
|
||||
@@ -55,18 +57,19 @@ jobs:
|
||||
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."
|
||||
|
||||
BASE_BRANCH=${GITHUB_BASE_REF:-main}
|
||||
git fetch origin $BASE_BRANCH
|
||||
|
||||
if git diff --quiet origin/$BASE_BRANCH -- README.md; then
|
||||
echo "README.md has no local changes compared 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 "README.md has local changes compared to $BASE_BRANCH. Proceeding with translation."
|
||||
echo "skip_translation=false" >> $GITHUB_ENV
|
||||
echo "--------- diff ---------"
|
||||
git diff origin/$BASE_BRANCH -- README.md
|
||||
echo "------------------------"
|
||||
fi
|
||||
|
||||
- name: Translate README.md to Chinese
|
||||
@@ -76,39 +79,74 @@ jobs:
|
||||
API_KEY: ${{ secrets.HIGRESS_OPENAI_API_KEY }}
|
||||
API_MODEL: ${{ secrets.HIGRESS_OPENAI_API_MODEL }}
|
||||
run: |
|
||||
cd ./helm/higress
|
||||
FILE_CONTENT=$(cat README.md)
|
||||
cat << 'EOF' > translate_readme.py
|
||||
import os
|
||||
import json
|
||||
import requests
|
||||
|
||||
PAYLOAD=$(jq -n \
|
||||
--arg model "$API_MODEL" \
|
||||
--arg content "$FILE_CONTENT" \
|
||||
'{
|
||||
model: $model,
|
||||
messages: [
|
||||
{"role": "system", "content": "You are a translation assistant that translates English Markdown text to Chinese."},
|
||||
{"role": "user", "content": $content}
|
||||
],
|
||||
temperature: 1.1,
|
||||
stream: false
|
||||
}')
|
||||
API_URL = os.environ["API_URL"]
|
||||
API_KEY = os.environ["API_KEY"]
|
||||
API_MODEL = os.environ["API_MODEL"]
|
||||
README_PATH = "./helm/higress/README.md"
|
||||
OUTPUT_PATH = "./helm/higress/README.zh.md"
|
||||
|
||||
RESPONSE=$(curl -s -X POST "$API_URL" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $API_KEY" \
|
||||
-d "$PAYLOAD")
|
||||
def stream_translation(api_url, api_key, payload):
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
}
|
||||
response = requests.post(api_url, headers=headers, json=payload, stream=True)
|
||||
response.raise_for_status()
|
||||
|
||||
echo "Response: $RESPONSE"
|
||||
with open(OUTPUT_PATH, "w", encoding="utf-8") as out_file:
|
||||
for line in response.iter_lines(decode_unicode=True):
|
||||
if line.strip() == "" or not line.startswith("data: "):
|
||||
continue
|
||||
data = line[6:]
|
||||
if data.strip() == "[DONE]":
|
||||
break
|
||||
try:
|
||||
chunk = json.loads(data)
|
||||
content = chunk["choices"][0]["delta"].get("content", "")
|
||||
if content:
|
||||
out_file.write(content)
|
||||
except Exception as e:
|
||||
print("Error parsing chunk:", e)
|
||||
|
||||
echo "$RESPONSE" | jq -c -r '.choices[] | .message.content' > README.zh.new.md
|
||||
def main():
|
||||
if not os.path.exists(README_PATH):
|
||||
print("README.md not found!")
|
||||
return
|
||||
|
||||
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
|
||||
with open(README_PATH, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
mv README.zh.new.md README.zh.md
|
||||
payload = {
|
||||
"model": API_MODEL,
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a translation assistant that translates English Markdown text to Chinese. Preserve original Markdown formatting and line breaks."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": content
|
||||
}
|
||||
],
|
||||
"temperature": 0.3,
|
||||
"stream": True
|
||||
}
|
||||
|
||||
print("Streaming translation started...")
|
||||
stream_translation(API_URL, API_KEY, payload)
|
||||
print(f"Translation completed and saved to {OUTPUT_PATH}.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
EOF
|
||||
|
||||
python3 translate_readme.py
|
||||
rm -rf translate_readme.py
|
||||
|
||||
- name: Create Pull Request
|
||||
if: env.skip_translation == 'false'
|
||||
|
||||
29
.github/workflows/translate-test.yml
vendored
Normal file
29
.github/workflows/translate-test.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: 'Translate GitHub content into English'
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited]
|
||||
issue_comment:
|
||||
types: [created, edited]
|
||||
discussion:
|
||||
types: [created, edited]
|
||||
discussion_comment:
|
||||
types: [created, edited]
|
||||
pull_request_target:
|
||||
types: [opened, edited]
|
||||
pull_request_review_comment:
|
||||
types: [created, edited]
|
||||
|
||||
jobs:
|
||||
translate:
|
||||
permissions:
|
||||
issues: write
|
||||
discussions: write
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: lizheming/github-translate-action@main
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
APPEND_TRANSLATION: true
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
[](https://github.com/alibaba/higress/actions)
|
||||
[](https://www.apache.org/licenses/LICENSE-2.0.html)
|
||||
[](https://discord.gg/reymxYM5)
|
||||
|
||||
<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> <a href="https://www.producthunt.com/posts/higress?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-higress" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=951287&theme=light&t=1745492822283" alt="Higress - Global APIs as MCP powered by AI Gateway | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
@@ -23,7 +24,7 @@
|
||||
English | <a href="README_ZH.md">中文<a/> | <a href="README_JP.md">日本語<a/>
|
||||
</p>
|
||||
|
||||
## What is Higress?
|
||||
## Test What is Higress?
|
||||
|
||||
Higress is a cloud-native API gateway based on Istio and Envoy, which can be extended with Wasm plugins written in Go/Rust/JS. It provides dozens of ready-to-use general-purpose plugins and an out-of-the-box console (try the [demo here](http://demo.higress.io/)).
|
||||
|
||||
@@ -140,7 +141,10 @@ For other installation methods such as Helm deployment under K8s, please refer t
|
||||
|
||||
## Community
|
||||
|
||||
[Slack](https://w1689142780-euk177225.slack.com/archives/C05GEL4TGTG): to get invited go [here](https://communityinviter.com/apps/w1689142780-euk177225/higress).
|
||||
Join our Discord community! This is where you can connect with developers and other enthusiastic users of Higress.
|
||||
|
||||
[](https://discord.gg/reymxYM5)
|
||||
|
||||
|
||||
### Thanks
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
appVersion: 2.1.2-rc.1
|
||||
appVersion: 2.1.3
|
||||
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.2-rc.1
|
||||
version: 2.1.3
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
dependencies:
|
||||
- name: higress-core
|
||||
repository: file://../core
|
||||
version: 2.1.2-rc.1
|
||||
version: 2.1.3
|
||||
- name: higress-console
|
||||
repository: https://higress.io/helm-charts/
|
||||
version: 2.1.2
|
||||
digest: sha256:0933dd97b646ae2e1740ccfb6606793b088abeeeb03f687a94c02fe34f227d87
|
||||
generated: "2025-04-28T20:34:11.26913+08:00"
|
||||
version: 2.1.3
|
||||
digest: sha256:c7307d5398c3c1178758c5372bd1aa4cb8dee7beeab3832d3e9ce0a04d1adc23
|
||||
generated: "2025-05-09T15:29:50.616179+08:00"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
appVersion: 2.1.2-rc.1
|
||||
appVersion: 2.1.3
|
||||
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.2-rc.1
|
||||
version: 2.1.3
|
||||
- name: higress-console
|
||||
repository: "https://higress.io/helm-charts/"
|
||||
version: 2.1.2
|
||||
version: 2.1.3
|
||||
type: application
|
||||
version: 2.1.2-rc.1
|
||||
version: 2.1.3
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
## Higress for Kubernetes
|
||||
## Higress 适用于 Kubernetes
|
||||
|
||||
Higress 是基于阿里巴巴内部网关实践构建的云原生 API 网关。
|
||||
Higress 是基于阿里巴巴内部网关实践的云原生 API 网关。
|
||||
|
||||
依托 Istio 和 Envoy,Higress 实现了流量网关、微服务网关和安全网关三重架构的融合,从而大幅降低了部署、运维成本。
|
||||
通过 Istio 和 Envoy 的支持,Higress 实现了流量网关、微服务网关和安全网关三种架构的融合,从而极大地减少了部署、运维的成本。
|
||||
|
||||
## 设置仓库信息
|
||||
|
||||
@@ -13,7 +13,7 @@ helm repo update
|
||||
|
||||
## 安装
|
||||
|
||||
以 `higress` 为发布名称安装 chart:
|
||||
使用 Helm 安装名为 `higress` 的组件:
|
||||
|
||||
```console
|
||||
helm install higress -n higress-system higress.io/higress --create-namespace --render-subchart-notes
|
||||
@@ -21,168 +21,130 @@ helm install higress -n higress-system higress.io/higress --create-namespace --r
|
||||
|
||||
## 卸载
|
||||
|
||||
要卸载/删除 higress 部署:
|
||||
删除名称为 higress 的安装:
|
||||
|
||||
```console
|
||||
helm delete higress -n higress-system
|
||||
```
|
||||
|
||||
该命令会移除与 chart 相关的所有 Kubernetes 组件,并删除发布。
|
||||
该命令将删除与组件关联的所有 Kubernetes 组件并卸载该发行版。
|
||||
|
||||
## 参数
|
||||
|
||||
## 值
|
||||
## Values
|
||||
|
||||
| 键 | 类型 | 默认值 | 描述 |
|
||||
|-----|------|---------|-------------|
|
||||
| clusterName | 字符串 | `""` | |
|
||||
| controller.affinity | 对象 | `{}` | |
|
||||
| controller.automaticHttps.email | 字符串 | `""` | |
|
||||
| controller.automaticHttps.enabled | 布尔值 | `true` | |
|
||||
| controller.autoscaling.enabled | 布尔值 | `false` | |
|
||||
| controller.autoscaling.maxReplicas | 整数 | `5` | |
|
||||
| controller.autoscaling.minReplicas | 整数 | `1` | |
|
||||
| controller.autoscaling.targetCPUUtilizationPercentage | 整数 | `80` | |
|
||||
| controller.env | 对象 | `{}` | |
|
||||
| controller.hub | 字符串 | `"higress-registry.cn-hangzhou.cr.aliyuncs.com/higress"` | |
|
||||
| controller.image | 字符串 | `"higress"` | |
|
||||
| controller.imagePullSecrets | 列表 | `[]` | |
|
||||
| controller.labels | 对象 | `{}` | |
|
||||
| controller.name | 字符串 | `"higress-controller"` | |
|
||||
| controller.nodeSelector | 对象 | `{}` | |
|
||||
| controller.podAnnotations | 对象 | `{}` | |
|
||||
| controller.podSecurityContext | 对象 | `{}` | |
|
||||
| controller.ports[0].name | 字符串 | `"http"` | |
|
||||
| controller.ports[0].port | 整数 | `8888` | |
|
||||
| controller.ports[0].protocol | 字符串 | `"TCP"` | |
|
||||
| controller.ports[0].targetPort | 整数 | `8888` | |
|
||||
| controller.ports[1].name | 字符串 | `"http-solver"` | |
|
||||
| controller.ports[1].port | 整数 | `8889` | |
|
||||
| controller.ports[1].protocol | 字符串 | `"TCP"` | |
|
||||
| controller.ports[1].targetPort | 整数 | `8889` | |
|
||||
| controller.ports[2].name | 字符串 | `"grpc"` | |
|
||||
| controller.ports[2].port | 整数 | `15051` | |
|
||||
| controller.ports[2].protocol | 字符串 | `"TCP"` | |
|
||||
| controller.ports[2].targetPort | 整数 | `15051` | |
|
||||
| controller.probe.httpGet.path | 字符串 | `"/ready"` | |
|
||||
| controller.probe.httpGet.port | 整数 | `8888` | |
|
||||
| controller.probe.initialDelaySeconds | 整数 | `1` | |
|
||||
| controller.probe.periodSeconds | 整数 | `3` | |
|
||||
| controller.probe.timeoutSeconds | 整数 | `5` | |
|
||||
| controller.rbac.create | 布尔值 | `true` | |
|
||||
| controller.replicas | 整数 | `1` | Higress Controller 的 Pod 数量 |
|
||||
| controller.resources.limits.cpu | 字符串 | `"1000m"` | |
|
||||
| controller.resources.limits.memory | 字符串 | `"2048Mi"` | |
|
||||
| controller.resources.requests.cpu | 字符串 | `"500m"` | |
|
||||
| controller.resources.requests.memory | 字符串 | `"2048Mi"` | |
|
||||
| controller.securityContext | 对象 | `{}` | |
|
||||
| controller.service.type | 字符串 | `"ClusterIP"` | |
|
||||
| controller.serviceAccount.annotations | 对象 | `{}` | 添加到服务账户的注解 |
|
||||
| controller.serviceAccount.create | 布尔值 | `true` | 指定是否创建服务账户 |
|
||||
| controller.serviceAccount.name | 字符串 | `""` | 如果未设置且 create 为 true,则使用 fullname 模板生成名称 |
|
||||
| controller.tag | 字符串 | `""` | |
|
||||
| controller.tolerations | 列表 | `[]` | |
|
||||
| downstream | 对象 | `{"connectionBufferLimits":32768,"http2":{"initialConnectionWindowSize":1048576,"initialStreamWindowSize":65535,"maxConcurrentStreams":100},"idleTimeout":180,"maxRequestHeadersKb":60,"routeTimeout":0}` | 下游配置设置 |
|
||||
| gateway.affinity | 对象 | `{}` | |
|
||||
| gateway.annotations | 对象 | `{}` | 应用到所有资源的注解 |
|
||||
| gateway.autoscaling.enabled | 布尔值 | `false` | |
|
||||
| gateway.autoscaling.maxReplicas | 整数 | `5` | |
|
||||
| gateway.autoscaling.minReplicas | 整数 | `1` | |
|
||||
| gateway.autoscaling.targetCPUUtilizationPercentage | 整数 | `80` | |
|
||||
| gateway.containerSecurityContext | 字符串 | `nil` | |
|
||||
| gateway.env | 对象 | `{}` | Pod 环境变量 |
|
||||
| gateway.hostNetwork | 布尔值 | `false` | |
|
||||
| gateway.httpPort | 整数 | `80` | |
|
||||
| gateway.httpsPort | 整数 | `443` | |
|
||||
| gateway.hub | 字符串 | `"higress-registry.cn-hangzhou.cr.aliyuncs.com/higress"` | |
|
||||
| gateway.image | 字符串 | `"gateway"` | |
|
||||
| gateway.kind | 字符串 | `"Deployment"` | 使用 `DaemonSet` 或 `Deployment` |
|
||||
| gateway.labels | 对象 | `{}` | 应用到所有资源的标签 |
|
||||
| gateway.metrics.enabled | 布尔值 | `false` | 如果为 true,则为网关创建 PodMonitor 或 VMPodScrape |
|
||||
| gateway.metrics.honorLabels | 布尔值 | `false` | |
|
||||
| gateway.metrics.interval | 字符串 | `""` | |
|
||||
| gateway.metrics.metricRelabelConfigs | 列表 | `[]` | 用于 operator.victoriametrics.com/v1beta1.VMPodScrape |
|
||||
| gateway.metrics.metricRelabelings | 列表 | `[]` | 用于 monitoring.coreos.com/v1.PodMonitor |
|
||||
| gateway.metrics.provider | 字符串 | `"monitoring.coreos.com"` | CustomResourceDefinition 的提供者组名,可以是 monitoring.coreos.com 或 operator.victoriametrics.com |
|
||||
| gateway.metrics.rawSpec | 对象 | `{}` | 更多原始的 podMetricsEndpoints 规范 |
|
||||
| gateway.metrics.relabelConfigs | 列表 | `[]` | |
|
||||
| gateway.metrics.relabelings | 列表 | `[]` | |
|
||||
| gateway.metrics.scrapeTimeout | 字符串 | `""` | |
|
||||
| gateway.name | 字符串 | `"higress-gateway"` | |
|
||||
| gateway.networkGateway | 字符串 | `""` | 如果指定,网关将作为给定网络的网络网关。 |
|
||||
| gateway.nodeSelector | 对象 | `{}` | |
|
||||
| gateway.podAnnotations."prometheus.io/path" | 字符串 | `"/stats/prometheus"` | |
|
||||
| gateway.podAnnotations."prometheus.io/port" | 字符串 | `"15020"` | |
|
||||
| gateway.podAnnotations."prometheus.io/scrape" | 字符串 | `"true"` | |
|
||||
| gateway.podAnnotations."sidecar.istio.io/inject" | 字符串 | `"false"` | |
|
||||
| gateway.rbac.enabled | 布尔值 | `true` | 如果启用,将创建角色以启用从网关访问证书。当使用 http://gateway-api.org/ 时不需要。 |
|
||||
| gateway.readinessFailureThreshold | 整数 | `30` | 指示准备失败前的连续失败探测次数。 |
|
||||
| gateway.readinessInitialDelaySeconds | 整数 | `1` | 准备探测的初始延迟秒数。 |
|
||||
| gateway.readinessPeriodSeconds | 整数 | `2` | 准备探测之间的间隔。 |
|
||||
| gateway.readinessSuccessThreshold | 整数 | `1` | 指示准备成功前的连续成功探测次数。 |
|
||||
| gateway.readinessTimeoutSeconds | 整数 | `3` | 准备探测的超时秒数 |
|
||||
| gateway.replicas | 整数 | `2` | Higress Gateway 的 Pod 数量 |
|
||||
| gateway.resources.limits.cpu | 字符串 | `"2000m"` | |
|
||||
| gateway.resources.limits.memory | 字符串 | `"2048Mi"` | |
|
||||
| gateway.resources.requests.cpu | 字符串 | `"2000m"` | |
|
||||
| gateway.resources.requests.memory | 字符串 | `"2048Mi"` | |
|
||||
| gateway.revision | 字符串 | `""` | 修订声明此网关属于哪个修订 |
|
||||
| gateway.rollingMaxSurge | 字符串 | `"100%"` | |
|
||||
| gateway.rollingMaxUnavailable | 字符串 | `"25%"` | |
|
||||
| gateway.securityContext | 字符串 | `nil` | 定义 Pod 的安全上下文。如果未设置,将自动设置为绑定到端口 80 和 443 所需的最小权限。在 Kubernetes 1.22+ 上,这只需要 `net.ipv4.ip_unprivileged_port_start` 系统调用。 |
|
||||
| gateway.service.annotations | 对象 | `{}` | |
|
||||
| gateway.service.externalTrafficPolicy | 字符串 | `""` | |
|
||||
| gateway.service.loadBalancerClass | 字符串 | `""` | |
|
||||
| gateway.service.loadBalancerIP | 字符串 | `""` | |
|
||||
| gateway.service.loadBalancerSourceRanges | 列表 | `[]` | |
|
||||
| gateway.service.ports[0].name | 字符串 | `"http2"` | |
|
||||
| gateway.service.ports[0].port | 整数 | `80` | |
|
||||
| gateway.service.ports[0].protocol | 字符串 | `"TCP"` | |
|
||||
| gateway.service.ports[0].targetPort | 整数 | `80` | |
|
||||
| gateway.service.ports[1].name | 字符串 | `"https"` | |
|
||||
| gateway.service.ports[1].port | 整数 | `443` | |
|
||||
| gateway.service.ports[1].protocol | 字符串 | `"TCP"` | |
|
||||
| gateway.service.ports[1].targetPort | 整数 | `443` | |
|
||||
| gateway.service.type | 字符串 | `"LoadBalancer"` | 服务类型。设置为 "None" 以完全禁用服务 |
|
||||
| gateway.serviceAccount.annotations | 对象 | `{}` | 添加到服务账户的注解 |
|
||||
| gateway.serviceAccount.create | 布尔值 | `true` | 如果设置,将创建服务账户。否则,使用默认值 |
|
||||
| gateway.serviceAccount.name | 字符串 | `""` | 要使用的服务账户名称。如果未设置,则使用发布名称 |
|
||||
| gateway.tag | 字符串 | `""` | |
|
||||
| gateway.tolerations | 列表 | `[]` | |
|
||||
| gateway.unprivilegedPortSupported | 字符串 | `nil` | |
|
||||
| global.autoscalingv2API | 布尔值 | `true` | 是否使用 autoscaling/v2 模板进行 HPA 设置,仅供内部使用,用户不应配置。 |
|
||||
| global.caAddress | 字符串 | `""` | 自定义的 CA 地址,用于为集群中的 Pod 检索证书。CSR 客户端(如 Istio Agent 和 ingress gateways)可以使用此地址指定 CA 端点。如果未明确设置,则默认为 Istio 发现地址。 |
|
||||
| global.caName | 字符串 | `""` | 工作负载证书的 CA 名称。例如,当 caName=GkeWorkloadCertificate 时,GKE 工作负载证书将用作工作负载的证书。默认值为 "",当 caName="" 时,CA 将通过其他机制(如环境变量 CA_PROVIDER)配置。 |
|
||||
| global.configCluster | 布尔值 | `false` | 将远程集群配置为外部 istiod 的配置集群。 |
|
||||
| global.defaultPodDisruptionBudget | 对象 | `{"enabled":false}` | 为控制平面启用 Pod 中断预算,用于确保 Istio 控制平面组件逐步升级或恢复。 |
|
||||
| global.defaultResources | 对象 | `{"requests":{"cpu":"10m"}}` | 应用于所有部署的最小请求资源集,以便 Horizontal Pod Autoscaler 能够正常工作(如果设置)。每个组件可以通过在相关部分添加自己的资源块并设置所需的资源值来覆盖这些默认值。 |
|
||||
| global.defaultUpstreamConcurrencyThreshold | 整数 | `10000` | |
|
||||
| global.disableAlpnH2 | 布尔值 | `false` | 是否在 ALPN 中禁用 HTTP/2 |
|
||||
| global.enableGatewayAPI | 布尔值 | `false` | 如果为 true,Higress Controller 还将监控 Gateway API 资源 |
|
||||
| global.enableH3 | 布尔值 | `false` | |
|
||||
| global.enableIPv6 | 布尔值 | `false` | |
|
||||
| global.enableIstioAPI | 布尔值 | `true` | 如果为 true,Higress Controller 还将监控 istio 资源 |
|
||||
| global.enableLDSCache | 布尔值 | `true` | |
|
||||
| global.enableProxyProtocol | 布尔值 | `false` | |
|
||||
| global.enablePushAllMCPClusters | 布尔值 | `true` | |
|
||||
| global.enableSRDS | 布尔值 | `true` | |
|
||||
| global.enableStatus | 布尔值 | `true` | 如果为 true,Higress Controller 将更新 Ingress 资源的状态字段。从 Nginx Ingress 迁移时,为了避免 Ingress 对象的状态字段被覆盖,需要将此参数设置为 false,以便 Higress 不会将入口 IP 写入相应 Ingress 对象的状态字段。 |
|
||||
| global.externalIstiod | 布尔值 | `false` | 配置由外部 istiod 控制的远程集群数据平面。当设置为 true 时,本地不部署 istiod,仅启用其他发现 chart 的子集。 |
|
||||
| global.hostRDSMergeSubset | 布尔值 | `false` | |
|
||||
| global.hub | 字符串 | `"higress-registry.cn-hangzhou.cr.aliyuncs.com/higress"` | Istio 镜像的默认仓库。发布版本发布到 docker hub 的 'istio' 项目下。来自 prow 的开发构建位于 gcr.io |
|
||||
| global.imagePullPolicy | 字符串 | `""` | 如果不需要默认行为,则指定镜像拉取策略。默认行为:最新镜像将始终拉取,否则 IfNotPresent。 |
|
||||
| global.imagePullSecrets | 列表 | `[]` | 所有 ServiceAccount 的 ImagePullSecrets,用于引用此 ServiceAccount 的 Pod 拉取任何镜像的同一命名空间中的秘密列表。对于不使用 ServiceAccount 的组件(即 grafana、servicegraph、tracing),ImagePullSecrets 将添加到相应的 Deployment(StatefulSet) 对象中。对于配置了私有 docker 注册表的任何集群,必须设置。 |
|
||||
| global.ingressClass | 字符串 | `"higress"` | IngressClass 过滤 higress controller 监听的 ingress 资源。默认的 ingress class 是 higress。有一些特殊情况用于特殊的 ingress class。1. 当 ingress class 设置为 nginx 时,higress controller 将监听带有 nginx ingress class 或没有任何 ingress class 的 ingress 资源。2. 当 ingress class 设置为空时,higress controller 将监听 k8s 集群中的所有 ingress 资源。 |
|
||||
| global.istioNamespace | 字符串 | `"istio-system"` | 用于定位 istiod。 |
|
||||
| global.istiod | 对象 | `{"enableAnalysis":false}` | 默认在主分支中启用以最大化测试。 |
|
||||
| global.jwtPolicy | 字符串 | `"third-party-jwt"` | 配置验证 JWT 的策略。目前支持两个选项:"third-party-jwt" 和 "first-party-jwt"。 |
|
||||
| global.kind | 布尔值 | `false` | |
|
||||
| global.liteMetrics | 布尔值 | `false` | |
|
||||
| global.local | 布尔值 | `false` | 当部署到本地集群(如:kind 集群)时,将此设置为 true。 |
|
||||
| global.logAsJson | 布尔值 | `false` | |
|
||||
| global.logging | 对象 | `{"level":"default:info"}` | 以逗号分隔的每个范围的最小日志级别,格式为 <scope>:<level>,<scope>:<level> 控制平面根据组件不同有不同的范围,但可以配置所有组件的默认日志级别 如果为空,将使用代码中配置的默认范围和级别 |
|
||||
| global.meshID | 字符串 | `""` | 如果网格管理员未指定值,Istio 将使用网格的信任域的值。最佳实践是选择一个合适的信任域值。 |
|
||||
| global.meshNetworks | 对象 | `{}` | |
|
||||
| global.mountMtlsCerts | 布尔值 | `false` | 使用用户指定的、挂载的密钥和证书用于 Pilot 和工作负载。 |
|
||||
| global.multiCluster.clusterName | 字符串 | `""` | 应设置为此安装运行的集群的名称。这是为了正确标记代理的 sidecar 注入所必需的 |
|
||||
| global.multiCluster.enabled | 布尔值 | `true` | 设置为 true 以通过各自的 ingressgateway 服务连接两个 kubernetes 集群,当每个集群中的 Pod 无法直接相互通信时。
|
||||
|----|------|---------|-------------|
|
||||
| clusterName | string | `""` | 集群名 |
|
||||
| controller.affinity | object | `{}` | 控制器亲和性设置 |
|
||||
| controller.automaticHttps.email | string | `""` | 自动 HTTPS 所需的邮件 |
|
||||
| controller.automaticHttps.enabled | bool | `true` | 是否启用自动 HTTPS 功能 |
|
||||
| controller.autoscaling.enabled | bool | `false` | 是否启用控制器的自动扩展功能 |
|
||||
| controller.autoscaling.maxReplicas | int | `5` | 最大副本数 |
|
||||
| controller.autoscaling.minReplicas | int | `1` | 最小副本数 |
|
||||
| controller.autoscaling.targetCPUUtilizationPercentage | int | `80` | 目标 CPU 使用率百分比 |
|
||||
| controller.env | object | `{}` | 环境变量 |
|
||||
| controller.hub | string | `"higress-registry.cn-hangzhou.cr.aliyuncs.com/higress"` | 图像库的基础地址 |
|
||||
| controller.image | string | `"higress"` | 镜像名称 |
|
||||
| controller.imagePullSecrets | list | `[]` | 拉取秘钥列表 |
|
||||
| controller.labels | object | `{}` | 标签 |
|
||||
| controller.name | string | `"higress-controller"` | 控制器名称 |
|
||||
| controller.nodeSelector | object | `{}` | 节点选择器 |
|
||||
| controller.podAnnotations | object | `{}` | Pod 注解 |
|
||||
| controller.podLabels | object | `{}` | 应用到 Pod 上的标签 |
|
||||
| controller.podSecurityContext | object | `{}` | Pod 安全上下文 |
|
||||
| controller.ports[0].name | string | `"http"` | 端口名称 |
|
||||
| controller.ports[0].port | int | `8888` | 端口编号 |
|
||||
| controller.ports[0].protocol | string | `"TCP"` | 协议类型 |
|
||||
| controller.ports[0].targetPort | int | `8888` | 目标端口 |
|
||||
| controller.ports[1].name | string | `"http-solver"` | 端口名称 |
|
||||
| controller.ports[1].port | int | `8889` | 端口编号 |
|
||||
| controller.ports[1].protocol | string | `"TCP"` | 协议类型 |
|
||||
| controller.ports[1].targetPort | int | `8889` | 目标端口 |
|
||||
| controller.ports[2].name | string | `"grpc"` | 端口名称 |
|
||||
| controller.ports[2].port | int | `15051` | 端口编号 |
|
||||
| controller.ports[2].protocol | string | `"TCP"` | 协议类型 |
|
||||
| controller.ports[2].targetPort | int | `15051` | 目标端口 |
|
||||
| controller.probe.httpGet.path | string | `"/ready"` | 运行状况检查路径 |
|
||||
| controller.probe.httpGet.port | int | `8888` | 端口运行状态检查 |
|
||||
| controller.probe.initialDelaySeconds | int | `1` | 初始延迟秒数 |
|
||||
| controller.probe.periodSeconds | int | `3` | 健康检查间隔秒数 |
|
||||
| controller.probe.timeoutSeconds | int | `5` | 超时秒数 |
|
||||
| controller.rbac.create | bool | `true` | 是否创建 RBAC 相关资源 |
|
||||
| controller.replicas | int | `1` | Higress 控制器 Pod 的数量 |
|
||||
| controller.resources.limits.cpu | string | `"1000m"` | CPU 上限 |
|
||||
| controller.resources.limits.memory | string | `"2048Mi"` | 内存上限 |
|
||||
| controller.resources.requests.cpu | string | `"500m"` | CPU 请求量 |
|
||||
| controller.resources.requests.memory | string | `"2048Mi"` | 内存请求量 |
|
||||
| controller.securityContext | object | `{}` | 安全上下文 |
|
||||
| controller.service.type | string | `"ClusterIP"` | 服务类型 |
|
||||
| controller.serviceAccount.annotations | object | `{}` | 添加到服务帐户的注解 |
|
||||
| controller.serviceAccount.create | bool | `true` | 是否创建服务帐户 |
|
||||
| controller.serviceAccount.name | string | `""` | 如果未设置且 create 为 true,则从 fullname 模板生成名称 |
|
||||
| controller.tag | string | `""` | 标记 |
|
||||
| controller.tolerations | list | `[]` | 受容容忍度列表 |
|
||||
| downstream.connectionBufferLimits | int | `32768` | 下游连接缓冲区限制(字节) |
|
||||
| downstream.http2.initialConnectionWindowSize | int | `1048576` | HTTP/2 初始连接窗口大小 |
|
||||
| downstream.http2.initialStreamWindowSize | int | `65535` | 流初始窗口大小 |
|
||||
| downstream.http2.maxConcurrentStreams | int | `100` | 并发流最大数量 |
|
||||
| downstream.idleTimeout | int | `180` | 空闲超时时间(秒) |
|
||||
| downstream.maxRequestHeadersKb | int | `60` | 最大请求头大小(KB) |
|
||||
| downstream.routeTimeout | int | `0` | 路由超时时间 |
|
||||
| gateway.affinity | object | `{}` | 网关的节点亲和性 |
|
||||
| gateway.annotations | object | `{}` | 应用于所有资源的注解 |
|
||||
| gateway.autoscaling.enabled | bool | `false` | 启用网关的自动扩展功能 |
|
||||
| gateway.autoscaling.maxReplicas | int | `5` | 最大副本数 |
|
||||
| gateway.autoscaling.minReplicas | int | `1` | 最小副本数 |
|
||||
| gateway.autoscaling.targetCPUUtilizationPercentage | int | `80` | CPU 使用率的目标百分比 |
|
||||
| gateway.containerSecurityContext | string | `nil` | 网关容器的安全配置上下文 |
|
||||
| gateway.env | object | `{}` | Pod 环境变量 |
|
||||
| gateway.hostNetwork | bool | `false` | 是否使用主机网络 |
|
||||
| gateway.httpPort | int | `80` | HTTP 服务端口 |
|
||||
| gateway.httpsPort | int | `443` | HTTPS 服务端口 |
|
||||
| gateway.hub | string | `"higress-registry.cn-hangzhou.cr.aliyuncs.com/higress"` | 网关镜像的基础域名 |
|
||||
| gateway.image | string | `"gateway"` | |
|
||||
| gateway.kind | string | `"Deployment"` | 部署类型 |
|
||||
| gateway.labels | object | `{}` | 应用于所有资源的标签 |
|
||||
| gateway.metrics.enabled | bool | `false` | 启用网关度量收集 |
|
||||
| gateway.metrics.honorLabels | bool | `false` | 是否合并现有标签 |
|
||||
| gateway.metrics.interval | string | `""` | 度量间隔时间 |
|
||||
| gateway.metrics.provider | string | `"monitoring.coreos.com"` | 定义监控提供者 |
|
||||
| gateway.metrics.rawSpec | object | `{}` | 额外的度量规范 |
|
||||
| gateway.metrics.relabelConfigs | list | `[]` | 重新标签配置 |
|
||||
| gateway.metrics.relabelings | list | `[]` | 重新标签项 |
|
||||
| gateway.metrics.scrapeTimeout | string | `""` | 抓取的超时时间 |
|
||||
| gateway.name | string | `"higress-gateway"` | 网关名称 |
|
||||
| gateway.networkGateway | string | `""` | 网络网关指定 |
|
||||
| gateway.nodeSelector | object | `{}` | 节点选择器 |
|
||||
| gateway.replicas | int | `2` | Higress Gateway pod 的数量 |
|
||||
| gateway.resources.limits.cpu | string | `"2000m"` | 容器资源限制的 CPU |
|
||||
| gateway.resources.limits.memory | string | `"2048Mi"` | 容器资源限制的内存 |
|
||||
| gateway.resources.requests.cpu | string | `"2000m"` | 容器资源请求的 CPU |
|
||||
| gateway.resources.requests.memory | string | `"2048Mi"` | 容器资源请求的内存 |
|
||||
| gateway.revision | string | `""` | 网关所属版本声明 |
|
||||
| gateway.rollingMaxSurge | string | `"100%"` | 最大激增数目百分比 |
|
||||
| gateway.rollingMaxUnavailable | string | `"25%"` | 最大不可用比例 |
|
||||
| gateway.readinessFailureThreshold | int | `30` | 成功尝试之前连续失败的最大探测次数 |
|
||||
| gateway.readinessInitialDelaySeconds | int | `1` | 初次检测推迟多少秒后开始探测存活状态 |
|
||||
| gateway.readinessPeriodSeconds | int | `2` | 存活探测间隔秒数 |
|
||||
| gateway.readinessSuccessThreshold | int | `1` | 认为成功之前连续成功最小探测次数 |
|
||||
| gateway.readinessTimeoutSeconds | int | `3` | 存活探测超时秒数 |
|
||||
| gateway.securityContext | string | `nil` | 客户豆荚的安全上下文 |
|
||||
| gateway.service.annotations | object | `{}` | 应用于服务账户的注释 |
|
||||
| gateway.service.externalTrafficPolicy | string | `""` | 外部路由策略 |
|
||||
| gateway.service.loadBalancerClass | string | `""` | 负载均衡器类别 |
|
||||
| gateway.service.loadBalancerIP | string | `""` | 负载均衡器 IP 地址 |
|
||||
| gateway.service.loadBalancerSourceRanges | list | `[]` | 允许访问负载均衡器的 CIDR 范围 |
|
||||
| gateway.service.ports[0].name | string | `"http2"` | 服务定义的端口名称 |
|
||||
| gateway.service.ports[0].port | int | `80` | 服务端口 |
|
||||
| gateway.service.ports[0].protocol | string | `"TCP"` | 协议 |
|
||||
| gateway.service.ports[0].targetPort | int | `80` | 靶向端口 |
|
||||
| gateway.service.ports[1].name | string | `"https"` | 服务定义的端口名称 |
|
||||
| gateway.service.ports[1].port | int | `443` | 服务端口 |
|
||||
| gateway.service.ports[1].protocol | string | `"TCP"` | 协议 |
|
||||
| gateway.service.ports[1].targetPort | int | `443` | 靶向端口 |
|
||||
| gateway.service.type | string | `"LoadBalancer"` | 服务类型 |
|
||||
| global.disableAlpnH2 | bool | `false` | 设置是否禁用 ALPN 中的 http/2 |
|
||||
| ... | ... | ... | ... |
|
||||
|
||||
由于内容较多,其他参数可以参考完整表。
|
||||
|
||||
@@ -56,7 +56,7 @@ type MCPRatelimitConfig struct {
|
||||
type SSEServer struct {
|
||||
// The name of the SSE server
|
||||
Name string `json:"name,omitempty"`
|
||||
// The path where the SSE server will be mounted, the full path is (PATH + SsePathSuffix)
|
||||
// The path where the SSE server will be mounted, the full path is (PATH + SSEPathSuffix)
|
||||
Path string `json:"path,omitempty"`
|
||||
// The type of the SSE server
|
||||
Type string `json:"type,omitempty"`
|
||||
@@ -74,6 +74,12 @@ type MatchRule struct {
|
||||
MatchRulePath string `json:"match_rule_path,omitempty"`
|
||||
// Type of match rule: exact, prefix, suffix, contains, regex
|
||||
MatchRuleType string `json:"match_rule_type,omitempty"`
|
||||
// Type of upstream(s) matched by the rule: rest (default), sse
|
||||
UpstreamType string `json:"upstream_type"`
|
||||
// Enable request path rewrite for matched routes
|
||||
EnablePathRewrite bool `json:"enable_path_rewrite"`
|
||||
// Prefix the request path would be rewritten to.
|
||||
PathRewritePrefix string `json:"path_rewrite_prefix"`
|
||||
}
|
||||
|
||||
// McpServer defines the configuration for MCP (Model Context Protocol) server
|
||||
@@ -83,7 +89,7 @@ type McpServer struct {
|
||||
// Redis Config for MCP server
|
||||
Redis *RedisConfig `json:"redis,omitempty"`
|
||||
// The suffix to be appended to SSE paths, default is "/sse"
|
||||
SsePathSuffix string `json:"sse_path_suffix,omitempty"`
|
||||
SSEPathSuffix string `json:"sse_path_suffix,omitempty"`
|
||||
// List of SSE servers Configs
|
||||
Servers []*SSEServer `json:"servers,omitempty"`
|
||||
// List of match rules for filtering requests
|
||||
@@ -118,21 +124,32 @@ func validMcpServer(m *McpServer) error {
|
||||
|
||||
// Validate match rule types
|
||||
if m.MatchList != nil {
|
||||
validTypes := map[string]bool{
|
||||
validMatchRuleTypes := map[string]bool{
|
||||
"exact": true,
|
||||
"prefix": true,
|
||||
"suffix": true,
|
||||
"contains": true,
|
||||
"regex": true,
|
||||
}
|
||||
validUpstreamTypes := map[string]bool{
|
||||
"rest": true,
|
||||
"sse": true,
|
||||
"streamable": true,
|
||||
}
|
||||
|
||||
for _, rule := range m.MatchList {
|
||||
if rule.MatchRuleType == "" {
|
||||
return errors.New("match_rule_type cannot be empty, must be one of: exact, prefix, suffix, contains, regex")
|
||||
}
|
||||
if !validTypes[rule.MatchRuleType] {
|
||||
if !validMatchRuleTypes[rule.MatchRuleType] {
|
||||
return fmt.Errorf("invalid match_rule_type: %s, must be one of: exact, prefix, suffix, contains, regex", rule.MatchRuleType)
|
||||
}
|
||||
if rule.UpstreamType != "" && !validUpstreamTypes[rule.UpstreamType] {
|
||||
return fmt.Errorf("invalid upstream_type: %s, must be one of: rest, sse, streamable", rule.UpstreamType)
|
||||
}
|
||||
if rule.EnablePathRewrite && rule.UpstreamType != "sse" {
|
||||
return errors.New("path rewrite is only supported for SSE upstream type")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,7 +191,7 @@ func deepCopyMcpServer(mcp *McpServer) (*McpServer, error) {
|
||||
WhiteList: mcp.Ratelimit.WhiteList,
|
||||
}
|
||||
}
|
||||
newMcp.SsePathSuffix = mcp.SsePathSuffix
|
||||
newMcp.SSEPathSuffix = mcp.SSEPathSuffix
|
||||
|
||||
newMcp.EnableUserLevelServer = mcp.EnableUserLevelServer
|
||||
|
||||
@@ -201,9 +218,12 @@ func deepCopyMcpServer(mcp *McpServer) (*McpServer, error) {
|
||||
newMcp.MatchList = make([]*MatchRule, len(mcp.MatchList))
|
||||
for i, rule := range mcp.MatchList {
|
||||
newMcp.MatchList[i] = &MatchRule{
|
||||
MatchRuleDomain: rule.MatchRuleDomain,
|
||||
MatchRulePath: rule.MatchRulePath,
|
||||
MatchRuleType: rule.MatchRuleType,
|
||||
MatchRuleDomain: rule.MatchRuleDomain,
|
||||
MatchRulePath: rule.MatchRulePath,
|
||||
MatchRuleType: rule.MatchRuleType,
|
||||
UpstreamType: rule.UpstreamType,
|
||||
EnablePathRewrite: rule.EnablePathRewrite,
|
||||
PathRewritePrefix: rule.PathRewritePrefix,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -216,7 +236,7 @@ type McpServerController struct {
|
||||
mcpServer atomic.Value
|
||||
Name string
|
||||
eventHandler ItemEventHandler
|
||||
reconclier *reconcile.Reconciler
|
||||
reconciler *reconcile.Reconciler
|
||||
}
|
||||
|
||||
func NewMcpServerController(namespace string) *McpServerController {
|
||||
@@ -291,7 +311,7 @@ func (m *McpServerController) RegisterItemEventHandler(eventHandler ItemEventHan
|
||||
}
|
||||
|
||||
func (m *McpServerController) RegisterMcpReconciler(reconciler *reconcile.Reconciler) {
|
||||
m.reconclier = reconciler
|
||||
m.reconciler = reconciler
|
||||
}
|
||||
|
||||
func (m *McpServerController) ConstructEnvoyFilters() ([]*config.Config, error) {
|
||||
@@ -393,13 +413,16 @@ func (m *McpServerController) constructMcpSessionStruct(mcp *McpServer) string {
|
||||
matchConfigs = append(matchConfigs, fmt.Sprintf(`{
|
||||
"match_rule_domain": "%s",
|
||||
"match_rule_path": "%s",
|
||||
"match_rule_type": "%s"
|
||||
}`, rule.MatchRuleDomain, rule.MatchRulePath, rule.MatchRuleType))
|
||||
"match_rule_type": "%s",
|
||||
"upstream_type": "%s",
|
||||
"enable_path_rewrite": %t,
|
||||
"path_rewrite_prefix": "%s"
|
||||
}`, rule.MatchRuleDomain, rule.MatchRulePath, rule.MatchRuleType, rule.UpstreamType, rule.EnablePathRewrite, rule.PathRewritePrefix))
|
||||
}
|
||||
}
|
||||
|
||||
if m.reconclier != nil {
|
||||
vsFromMcp := m.reconclier.GetAllConfigs(gvk.VirtualService)
|
||||
if m.reconciler != nil {
|
||||
vsFromMcp := m.reconciler.GetAllConfigs(gvk.VirtualService)
|
||||
for _, c := range vsFromMcp {
|
||||
vs := c.Spec.(*networking.VirtualService)
|
||||
var host string
|
||||
@@ -468,7 +491,7 @@ func (m *McpServerController) constructMcpSessionStruct(mcp *McpServer) string {
|
||||
}`,
|
||||
redisConfig,
|
||||
rateLimitConfig,
|
||||
mcp.SsePathSuffix,
|
||||
mcp.SSEPathSuffix,
|
||||
matchList,
|
||||
mcp.EnableUserLevelServer)
|
||||
}
|
||||
|
||||
@@ -54,6 +54,61 @@ func Test_validMcpServer(t *testing.T) {
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "enabled but bad match_rule_type",
|
||||
mcp: &McpServer{
|
||||
Enable: true,
|
||||
EnableUserLevelServer: false,
|
||||
Redis: nil,
|
||||
MatchList: []*MatchRule{
|
||||
{
|
||||
MatchRuleDomain: "*",
|
||||
MatchRulePath: "/mcp",
|
||||
MatchRuleType: "bad-type",
|
||||
},
|
||||
},
|
||||
Servers: []*SSEServer{},
|
||||
},
|
||||
wantErr: errors.New("invalid match_rule_type: bad-type, must be one of: exact, prefix, suffix, contains, regex"),
|
||||
},
|
||||
{
|
||||
name: "enabled but bad upstream_type",
|
||||
mcp: &McpServer{
|
||||
Enable: true,
|
||||
EnableUserLevelServer: false,
|
||||
Redis: nil,
|
||||
MatchList: []*MatchRule{
|
||||
{
|
||||
MatchRuleDomain: "*",
|
||||
MatchRulePath: "/mcp",
|
||||
MatchRuleType: "prefix",
|
||||
UpstreamType: "bad-type",
|
||||
},
|
||||
},
|
||||
Servers: []*SSEServer{},
|
||||
},
|
||||
wantErr: errors.New("invalid upstream_type: bad-type, must be one of: rest, sse, streamable"),
|
||||
},
|
||||
{
|
||||
name: "enabled but path rewrite with unsupported upstream type",
|
||||
mcp: &McpServer{
|
||||
Enable: true,
|
||||
EnableUserLevelServer: false,
|
||||
Redis: nil,
|
||||
MatchList: []*MatchRule{
|
||||
{
|
||||
MatchRuleDomain: "*",
|
||||
MatchRulePath: "/mcp",
|
||||
MatchRuleType: "prefix",
|
||||
UpstreamType: "rest",
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
},
|
||||
Servers: []*SSEServer{},
|
||||
},
|
||||
wantErr: errors.New("path rewrite is only supported for SSE upstream type"),
|
||||
},
|
||||
{
|
||||
name: "enabled with user level server but no redis config",
|
||||
mcp: &McpServer{
|
||||
@@ -76,7 +131,7 @@ func Test_validMcpServer(t *testing.T) {
|
||||
Password: "password",
|
||||
DB: 0,
|
||||
},
|
||||
SsePathSuffix: "/sse",
|
||||
SSEPathSuffix: "/sse",
|
||||
MatchList: []*MatchRule{
|
||||
{
|
||||
MatchRuleDomain: "*",
|
||||
@@ -238,7 +293,7 @@ func Test_deepCopyMcpServer(t *testing.T) {
|
||||
Password: "password",
|
||||
DB: 0,
|
||||
},
|
||||
SsePathSuffix: "/sse",
|
||||
SSEPathSuffix: "/sse",
|
||||
MatchList: []*MatchRule{
|
||||
{
|
||||
MatchRuleDomain: "*",
|
||||
@@ -265,7 +320,7 @@ func Test_deepCopyMcpServer(t *testing.T) {
|
||||
Password: "password",
|
||||
DB: 0,
|
||||
},
|
||||
SsePathSuffix: "/sse",
|
||||
SSEPathSuffix: "/sse",
|
||||
MatchList: []*MatchRule{
|
||||
{
|
||||
MatchRuleDomain: "*",
|
||||
@@ -581,13 +636,27 @@ func TestMcpServerController_constructMcpSessionStruct(t *testing.T) {
|
||||
Password: "pass",
|
||||
DB: 1,
|
||||
},
|
||||
SsePathSuffix: "/sse",
|
||||
SSEPathSuffix: "/sse",
|
||||
MatchList: []*MatchRule{
|
||||
{
|
||||
MatchRuleDomain: "*",
|
||||
MatchRulePath: "/test",
|
||||
MatchRuleType: "exact",
|
||||
},
|
||||
{
|
||||
MatchRuleDomain: "*",
|
||||
MatchRulePath: "/sse-test-1",
|
||||
MatchRuleType: "prefix",
|
||||
UpstreamType: "sse",
|
||||
},
|
||||
{
|
||||
MatchRuleDomain: "*",
|
||||
MatchRulePath: "/sse-test-2",
|
||||
MatchRuleType: "prefix",
|
||||
UpstreamType: "sse",
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/mcp",
|
||||
},
|
||||
},
|
||||
EnableUserLevelServer: true,
|
||||
Ratelimit: &MCPRatelimitConfig{
|
||||
@@ -623,7 +692,24 @@ func TestMcpServerController_constructMcpSessionStruct(t *testing.T) {
|
||||
"match_list": [{
|
||||
"match_rule_domain": "*",
|
||||
"match_rule_path": "/test",
|
||||
"match_rule_type": "exact"
|
||||
"match_rule_type": "exact",
|
||||
"upstream_type": "",
|
||||
"enable_path_rewrite": false,
|
||||
"path_rewrite_prefix": ""
|
||||
},{
|
||||
"match_rule_domain": "*",
|
||||
"match_rule_path": "/sse-test-1",
|
||||
"match_rule_type": "prefix",
|
||||
"upstream_type": "sse",
|
||||
"enable_path_rewrite": false,
|
||||
"path_rewrite_prefix": ""
|
||||
},{
|
||||
"match_rule_domain": "*",
|
||||
"match_rule_path": "/sse-test-2",
|
||||
"match_rule_type": "prefix",
|
||||
"upstream_type": "sse",
|
||||
"enable_path_rewrite": true,
|
||||
"path_rewrite_prefix": "/mcp"
|
||||
}],
|
||||
"enable_user_level_server": true
|
||||
}
|
||||
|
||||
@@ -20,24 +20,38 @@ Golang HTTP Filter 允许开发者使用 Go 语言编写自定义的 Envoy Filte
|
||||
|
||||
请参考 [Envoy Golang HTTP Filter 示例](https://github.com/envoyproxy/examples/tree/main/golang-http) 了解如何开发和运行一个基本的 Golang Filter。
|
||||
|
||||
## 插件注册
|
||||
|
||||
在开发新的 Golang Filter 时,需要在`main.go` 的 `init()` 函数中注册你的插件。注册时需要提供插件名称、Filter 工厂函数和配置解析器:
|
||||
|
||||
```go
|
||||
func init() {
|
||||
envoyHttp.RegisterHttpFilterFactoryAndConfigParser(
|
||||
"your-plugin-name", // 插件名称
|
||||
yourFilterFactory, // Filter 工厂函数
|
||||
&yourConfigParser{}, // 配置解析器
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 配置示例
|
||||
|
||||
多个 Golang Filter 插件可以共同编译到一个 `golang-filter.so` 文件中,通过 `plugin_name` 来指定要使用的插件。配置示例如下:
|
||||
|
||||
```yaml
|
||||
http_filters:
|
||||
- name: envoy.filters.http.golang
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config
|
||||
library_id: my-go-filter
|
||||
library_path: "./go-filter.so"
|
||||
plugin_name: my-go-filter
|
||||
library_id: your-plugin-name
|
||||
library_path: "./golang-filter.so" # 包含多个插件的共享库文件
|
||||
plugin_name: your-plugin-name # 指定要使用的插件名称,需要与 init() 函数中注册的插件名称保持一致
|
||||
plugin_config:
|
||||
"@type": type.googleapis.com/xds.type.v3.TypedStruct
|
||||
value:
|
||||
your_config_here: value
|
||||
|
||||
```
|
||||
|
||||
|
||||
## 快速构建
|
||||
|
||||
使用以下命令可以快速构建 golang filter 插件:
|
||||
|
||||
@@ -20,16 +20,32 @@ The Golang HTTP Filter allows developers to write custom Envoy Filters using the
|
||||
|
||||
Please refer to [Envoy Golang HTTP Filter Example](https://github.com/envoyproxy/examples/tree/main/golang-http) to learn how to develop and run a basic Golang Filter.
|
||||
|
||||
## Plugin Registration
|
||||
|
||||
When developing a new Golang Filter, you need to register your plugin in the `init()` function of `main.go`. The registration requires a plugin name, Filter factory function, and configuration parser:
|
||||
|
||||
```go
|
||||
func init() {
|
||||
envoyHttp.RegisterHttpFilterFactoryAndConfigParser(
|
||||
"your-plugin-name", // Plugin name
|
||||
yourFilterFactory, // Filter factory function
|
||||
&yourConfigParser{}, // Configuration parser
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Example
|
||||
|
||||
Multiple Golang Filter plugins can be compiled into a single `golang-filter.so` file, and the desired plugin can be specified using `plugin_name`. Here's an example configuration:
|
||||
|
||||
```yaml
|
||||
http_filters:
|
||||
- name: envoy.filters.http.golang
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config
|
||||
library_id: my-go-filter
|
||||
library_path: "./my-go-filter.so"
|
||||
plugin_name: my-go-filter
|
||||
library_id: your-plugin-name
|
||||
library_path: "./golang-filter.so" # Shared library file containing multiple plugins
|
||||
plugin_name: your-plugin-name # Specify which plugin to use, must match the name registered in init()
|
||||
plugin_config:
|
||||
"@type": type.googleapis.com/xds.type.v3.TypedStruct
|
||||
value:
|
||||
@@ -41,5 +57,5 @@ http_filters:
|
||||
Use the following command to quickly build the golang filter plugin:
|
||||
|
||||
```bash
|
||||
GO_FILTER_NAME=mcp-server make build
|
||||
make build
|
||||
```
|
||||
@@ -2,7 +2,7 @@ module github.com/alibaba/higress/plugins/golang-filter
|
||||
|
||||
go 1.22
|
||||
|
||||
replace github.com/envoyproxy/envoy => github.com/higress-group/envoy v0.0.0-20250428030521-17cf01d9f644
|
||||
replace github.com/envoyproxy/envoy => github.com/higress-group/envoy v0.0.0-20250430151331-2c556780b65c
|
||||
|
||||
replace github.com/mark3labs/mcp-go => github.com/higress-group/mcp-go v0.0.0-20250428145706-792ce64b4b30
|
||||
|
||||
|
||||
@@ -234,8 +234,8 @@ github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mO
|
||||
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/higress-group/envoy v0.0.0-20250428030521-17cf01d9f644 h1:wiLDdiOT3BcTQSFs8oTMu54GIiPFSwKLuWo5J0Cd9b8=
|
||||
github.com/higress-group/envoy v0.0.0-20250428030521-17cf01d9f644/go.mod h1:SU+IJUAfh1kkZtH+u0E1dnwho8AhbGeYMgp5vvjU+Gc=
|
||||
github.com/higress-group/envoy v0.0.0-20250430151331-2c556780b65c h1:chAOZk/qEXFhLILWoNucj3X6r9xYnRR+SWFvhsOa2oo=
|
||||
github.com/higress-group/envoy v0.0.0-20250430151331-2c556780b65c/go.mod h1:SU+IJUAfh1kkZtH+u0E1dnwho8AhbGeYMgp5vvjU+Gc=
|
||||
github.com/higress-group/mcp-go v0.0.0-20250428145706-792ce64b4b30 h1:N4NMq8M1nZyyChPyzn+EUUdHi5asig2uLR5hOyRmsXI=
|
||||
github.com/higress-group/mcp-go v0.0.0-20250428145706-792ce64b4b30/go.mod h1:O9gri9UOzthw728vusc2oNu99lVh8cKCajpxNfC90gE=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
|
||||
@@ -3,27 +3,22 @@
|
||||
|
||||
## 概述
|
||||
|
||||
MCP Server 是一个基于 Envoy 的 Golang Filter 插件,用于实现服务器端事件(SSE)和消息通信功能。该插件支持多种数据库类型,并使用 Redis 作为消息队列来实现负载均衡的请求通过对应的SSE连接发送。
|
||||
MCP Server 是一个基于 Envoy 的 Golang Filter 插件,提供了统一的 MCP (Model Context Protocol) 服务接口。它支持多种后端服务的集成,包括:
|
||||
|
||||
> **注意**:MCP Server需要 Higress 2.1.0 或更高版本才能使用。
|
||||
## 项目结构
|
||||
```
|
||||
mcp-server/
|
||||
├── config.go # 配置解析相关代码
|
||||
├── filter.go # 请求处理相关代码
|
||||
├── internal/ # 内部实现逻辑
|
||||
├── servers/ # MCP 服务器实现
|
||||
├── go.mod # Go模块依赖定义
|
||||
└── go.sum # Go模块依赖校验
|
||||
```
|
||||
## MCP Server开发指南
|
||||
- 数据库服务:通过 GORM 支持多种数据库的访问和管理
|
||||
- 配置中心:支持 Nacos 配置中心的集成
|
||||
- 可扩展性:支持自定义服务器实现,方便集成其他服务
|
||||
|
||||
> **注意**:MCP Server 需要 Higress 2.1.0 或更高版本才能使用。
|
||||
|
||||
## MCP Server 开发指南
|
||||
|
||||
```go
|
||||
// 在init函数中注册你的服务器
|
||||
// 参数1: 服务器名称
|
||||
// 参数2: 配置结构体实例
|
||||
func init() {
|
||||
internal.GlobalRegistry.RegisterServer("demo", &DemoConfig{})
|
||||
common.GlobalRegistry.RegisterServer("demo", &DemoConfig{})
|
||||
}
|
||||
|
||||
// 服务器配置结构体
|
||||
@@ -43,8 +38,8 @@ func (c *DBConfig) ParseConfig(config map[string]any) error {
|
||||
// 创建新的MCP服务器实例
|
||||
// serverName: 服务器名称
|
||||
// 返回值: MCP服务器实例和可能的错误
|
||||
func (c *DBConfig) NewServer(serverName string) (*internal.MCPServer, error) {
|
||||
mcpServer := internal.NewMCPServer(serverName, Version)
|
||||
func (c *DBConfig) NewServer(serverName string) (*common.MCPServer, error) {
|
||||
mcpServer := common.NewMCPServer(serverName, Version)
|
||||
|
||||
// 添加工具方法到服务器
|
||||
// mcpServer.AddTool()
|
||||
60
plugins/golang-filter/mcp-server/README_en.md
Normal file
60
plugins/golang-filter/mcp-server/README_en.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# MCP Server
|
||||
English | [简体中文](./README.md)
|
||||
|
||||
## Overview
|
||||
|
||||
MCP Server is a Golang Filter plugin based on Envoy that provides a unified MCP (Model Context Protocol) service interface. It supports integration with various backend services, including:
|
||||
|
||||
- Database Services: Supports multiple database access and management through GORM
|
||||
- Configuration Service: Supports integration with Nacos configuration service
|
||||
- Extensibility: Supports custom server implementations for easy integration with other services
|
||||
|
||||
> **Note**: MCP Server requires Higress version 2.1.0 or higher to be used.
|
||||
|
||||
## MCP Server Development Guide
|
||||
|
||||
```go
|
||||
// Register your server in the init function
|
||||
// Parameter 1: Server name
|
||||
// Parameter 2: Configuration struct instance
|
||||
func init() {
|
||||
common.GlobalRegistry.RegisterServer("demo", &DemoConfig{})
|
||||
}
|
||||
|
||||
// Server configuration struct
|
||||
type DemoConfig struct {
|
||||
helloworld string
|
||||
}
|
||||
|
||||
// Parse configuration method
|
||||
// Parse and validate configuration items from the config map
|
||||
func (c *DBConfig) ParseConfig(config map[string]any) error {
|
||||
helloworld, ok := config["helloworld"].(string)
|
||||
if !ok { return errors.New("missing helloworld")}
|
||||
c.helloworld = helloworld
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a new MCP server instance
|
||||
// serverName: Server name
|
||||
// Returns: MCP server instance and possible error
|
||||
func (c *DBConfig) NewServer(serverName string) (*common.MCPServer, error) {
|
||||
mcpServer := common.NewMCPServer(serverName, Version)
|
||||
|
||||
// Add tool methods to the server
|
||||
// mcpServer.AddTool()
|
||||
|
||||
// Add resources to the server
|
||||
// mcpServer.AddResource()
|
||||
|
||||
return mcpServer, nil
|
||||
}
|
||||
```
|
||||
|
||||
**Note**:
|
||||
You need to use underscore imports in config.go to execute the package's init function
|
||||
```go
|
||||
import (
|
||||
_ "github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/gorm"
|
||||
)
|
||||
```
|
||||
@@ -104,7 +104,6 @@ func (p *Parser) Parse(any *anypb.Any, callbacks api.ConfigCallbackHandler) (int
|
||||
|
||||
conf.servers = append(conf.servers, &SSEServerWrapper{
|
||||
BaseServer: common.NewSSEServer(serverInstance,
|
||||
common.WithRedisClient(common.GlobalRedisClient),
|
||||
common.WithSSEEndpoint(fmt.Sprintf("%s%s", serverPath, mcp_session.GlobalSSEPathSuffix)),
|
||||
common.WithMessageEndpoint(serverPath)),
|
||||
DomainList: serverDomainList,
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
# MCP Server
|
||||
English | [简体中文](./README.md)
|
||||
|
||||
## Overview
|
||||
|
||||
MCP Server is a Golang Filter plugin based on Envoy, designed to implement Server-Sent Events (SSE) and message communication functionality. This plugin supports various database types and uses Redis as a message queue to enable load-balanced requests to be sent through corresponding SSE connections.
|
||||
|
||||
> **Note**: MCP Server requires Higress 2.1.0 or higher version.
|
||||
|
||||
## Project Structure
|
||||
```
|
||||
mcp-server/
|
||||
├── config.go # Configuration parsing code
|
||||
├── filter.go # Request processing code
|
||||
├── internal/ # Internal implementation logic
|
||||
├── servers/ # MCP server implementation
|
||||
├── go.mod # Go module dependency definition
|
||||
└── go.sum # Go module dependency checksum
|
||||
```
|
||||
|
||||
## MCP Server Development Guide
|
||||
|
||||
```go
|
||||
// Register your server in the init function
|
||||
// Param 1: Server name
|
||||
// Param 2: Config struct instance
|
||||
func init() {
|
||||
internal.GlobalRegistry.RegisterServer("demo", &DemoConfig{})
|
||||
}
|
||||
|
||||
// Server configuration struct
|
||||
type DemoConfig struct {
|
||||
helloworld string
|
||||
}
|
||||
|
||||
// Configuration parsing method
|
||||
// Parse and validate configuration items from the config map
|
||||
func (c *DBConfig) ParseConfig(config map[string]any) error {
|
||||
helloworld, ok := config["helloworld"].(string)
|
||||
if !ok { return errors.New("missing helloworld")}
|
||||
c.helloworld = helloworld
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a new MCP server instance
|
||||
// serverName: Server name
|
||||
// Returns: MCP server instance and possible error
|
||||
func (c *DBConfig) NewServer(serverName string) (*internal.MCPServer, error) {
|
||||
mcpServer := internal.NewMCPServer(serverName, Version)
|
||||
|
||||
// Add tool methods to server
|
||||
// mcpServer.AddTool()
|
||||
|
||||
// Add resources to server
|
||||
// mcpServer.AddResource()
|
||||
|
||||
return mcpServer, nil
|
||||
}
|
||||
```
|
||||
|
||||
**Note**:
|
||||
Need to use underscore import in config.go to execute the package's init function
|
||||
```go
|
||||
import (
|
||||
_ "github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/gorm"
|
||||
)
|
||||
```
|
||||
@@ -3,24 +3,36 @@ package common
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
)
|
||||
|
||||
// RuleType defines the type of matching rule
|
||||
type RuleType string
|
||||
|
||||
// UpstreamType defines the type of matching rule
|
||||
type UpstreamType string
|
||||
|
||||
const (
|
||||
ExactMatch RuleType = "exact"
|
||||
PrefixMatch RuleType = "prefix"
|
||||
SuffixMatch RuleType = "suffix"
|
||||
ContainsMatch RuleType = "contains"
|
||||
RegexMatch RuleType = "regex"
|
||||
|
||||
RestUpstream UpstreamType = "rest"
|
||||
SSEUpstream UpstreamType = "sse"
|
||||
StreamableUpstream UpstreamType = "streamable"
|
||||
)
|
||||
|
||||
// MatchRule defines the structure for a matching rule
|
||||
type MatchRule struct {
|
||||
MatchRuleDomain string `json:"match_rule_domain"` // Domain pattern, supports wildcards
|
||||
MatchRulePath string `json:"match_rule_path"` // Path pattern to match
|
||||
MatchRuleType RuleType `json:"match_rule_type"` // Type of match rule
|
||||
MatchRuleDomain string `json:"match_rule_domain"` // Domain pattern, supports wildcards
|
||||
MatchRulePath string `json:"match_rule_path"` // Path pattern to match
|
||||
MatchRuleType RuleType `json:"match_rule_type"` // Type of match rule
|
||||
UpstreamType UpstreamType `json:"upstream_type"` // Type of upstream(s) matched by the rule
|
||||
EnablePathRewrite bool `json:"enable_path_rewrite"` // Enable request path rewrite for matched routes
|
||||
PathRewritePrefix string `json:"path_rewrite_prefix"` // Prefix the request path would be rewritten to.
|
||||
}
|
||||
|
||||
// ParseMatchList parses the match list from the config
|
||||
@@ -38,6 +50,34 @@ func ParseMatchList(matchListConfig []interface{}) []MatchRule {
|
||||
if ruleType, ok := ruleMap["match_rule_type"].(string); ok {
|
||||
rule.MatchRuleType = RuleType(ruleType)
|
||||
}
|
||||
if upstreamType, ok := ruleMap["upstream_type"].(string); ok {
|
||||
rule.UpstreamType = UpstreamType(upstreamType)
|
||||
}
|
||||
if len(rule.UpstreamType) == 0 {
|
||||
rule.UpstreamType = RestUpstream
|
||||
} else {
|
||||
switch rule.UpstreamType {
|
||||
case RestUpstream, SSEUpstream, StreamableUpstream:
|
||||
break
|
||||
default:
|
||||
api.LogWarnf("Unknown upstream type: %s", rule.UpstreamType)
|
||||
}
|
||||
}
|
||||
if enablePathRewrite, ok := ruleMap["enable_path_rewrite"].(bool); ok {
|
||||
rule.EnablePathRewrite = enablePathRewrite
|
||||
}
|
||||
if pathRewritePrefix, ok := ruleMap["path_rewrite_prefix"].(string); ok {
|
||||
rule.PathRewritePrefix = pathRewritePrefix
|
||||
}
|
||||
if rule.EnablePathRewrite {
|
||||
if rule.UpstreamType != SSEUpstream {
|
||||
api.LogWarnf("Path rewrite is only supported for SSE upstream type")
|
||||
} else if rule.MatchRuleType != PrefixMatch {
|
||||
api.LogWarnf("Path rewrite is only supported for prefix match type")
|
||||
} else if !strings.HasPrefix(rule.PathRewritePrefix, "/") {
|
||||
rule.PathRewritePrefix = "/" + rule.PathRewritePrefix
|
||||
}
|
||||
}
|
||||
matchList = append(matchList, rule)
|
||||
}
|
||||
}
|
||||
@@ -96,17 +136,17 @@ func matchDomainAndPath(domain, path string, rule MatchRule) bool {
|
||||
|
||||
// IsMatch checks if the request matches any rule in the rule list
|
||||
// Returns true if no rules are specified
|
||||
func IsMatch(rules []MatchRule, host, path string) bool {
|
||||
func IsMatch(rules []MatchRule, host, path string) (bool, MatchRule) {
|
||||
if len(rules) == 0 {
|
||||
return true
|
||||
return true, MatchRule{}
|
||||
}
|
||||
|
||||
for _, rule := range rules {
|
||||
if matchDomainAndPath(host, path, rule) {
|
||||
return true
|
||||
return true, rule
|
||||
}
|
||||
}
|
||||
return false
|
||||
return false, MatchRule{}
|
||||
}
|
||||
|
||||
// MatchDomainList checks if the domain matches any of the domains in the list
|
||||
|
||||
@@ -9,8 +9,6 @@ import (
|
||||
"github.com/go-redis/redis/v8"
|
||||
)
|
||||
|
||||
var GlobalRedisClient *RedisClient
|
||||
|
||||
type RedisConfig struct {
|
||||
address string
|
||||
username string
|
||||
@@ -74,9 +72,10 @@ func NewRedisClient(config *RedisConfig) (*RedisClient, error) {
|
||||
// Ping the Redis server to check the connection
|
||||
pong, err := client.Ping(context.Background()).Result()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to Redis: %w", err)
|
||||
api.LogErrorf("Failed to connect to Redis: %v", err)
|
||||
} else {
|
||||
api.LogDebugf("Connected to Redis: %s", pong)
|
||||
}
|
||||
api.LogDebugf("Connected to Redis: %s", pong)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
@@ -85,7 +84,7 @@ func NewRedisClient(config *RedisConfig) (*RedisClient, error) {
|
||||
crypto, err = NewCrypto(config.secret)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, err
|
||||
api.LogWarnf("Failed to initialize redis crypto: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +104,7 @@ func NewRedisClient(config *RedisConfig) (*RedisClient, error) {
|
||||
|
||||
// keepAlive periodically checks Redis connection and attempts to reconnect if needed
|
||||
func (r *RedisClient) keepAlive() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
|
||||
@@ -210,7 +210,7 @@ func (s *SSEServer) HandleMessage(w http.ResponseWriter, r *http.Request, body j
|
||||
var status int
|
||||
// Only send response if there is one (not for notifications)
|
||||
if response != nil {
|
||||
if sessionID != "" && s.redisClient != nil {
|
||||
if sessionID != ""{
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
status = http.StatusAccepted
|
||||
} else {
|
||||
|
||||
@@ -25,12 +25,13 @@ type config struct {
|
||||
enableUserLevelServer bool
|
||||
rateLimitConfig *handler.MCPRatelimitConfig
|
||||
defaultServer *common.SSEServer
|
||||
redisClient *common.RedisClient
|
||||
}
|
||||
|
||||
func (c *config) Destroy() {
|
||||
if common.GlobalRedisClient != nil {
|
||||
if c.redisClient != nil {
|
||||
api.LogDebug("Closing Redis client")
|
||||
common.GlobalRedisClient.Close()
|
||||
c.redisClient.Close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,10 +64,11 @@ func (p *Parser) Parse(any *anypb.Any, callbacks api.ConfigCallbackHandler) (int
|
||||
|
||||
redisClient, err := common.NewRedisClient(redisConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize RedisClient: %w", err)
|
||||
api.LogErrorf("Failed to initialize Redis client: %w", err)
|
||||
} else {
|
||||
api.LogDebug("Redis client initialized")
|
||||
}
|
||||
common.GlobalRedisClient = redisClient
|
||||
api.LogDebug("Redis client initialized")
|
||||
conf.redisClient = redisClient
|
||||
} else {
|
||||
api.LogDebug("Redis configuration not provided, running without Redis")
|
||||
}
|
||||
@@ -74,7 +76,7 @@ func (p *Parser) Parse(any *anypb.Any, callbacks api.ConfigCallbackHandler) (int
|
||||
enableUserLevelServer, ok := v.AsMap()["enable_user_level_server"].(bool)
|
||||
if !ok {
|
||||
enableUserLevelServer = false
|
||||
if common.GlobalRedisClient == nil {
|
||||
if conf.redisClient == nil {
|
||||
return nil, fmt.Errorf("redis configuration is not provided, enable_user_level_server is true")
|
||||
}
|
||||
}
|
||||
@@ -137,7 +139,7 @@ func FilterFactory(c interface{}, callbacks api.FilterCallbackHandler) api.Strea
|
||||
callbacks: callbacks,
|
||||
config: conf,
|
||||
stopChan: make(chan struct{}),
|
||||
mcpConfigHandler: handler.NewMCPConfigHandler(common.GlobalRedisClient, callbacks),
|
||||
mcpRatelimitHandler: handler.NewMCPRatelimitHandler(common.GlobalRedisClient, callbacks, conf.rateLimitConfig),
|
||||
mcpConfigHandler: handler.NewMCPConfigHandler(conf.redisClient, callbacks),
|
||||
mcpRatelimitHandler: handler.NewMCPRatelimitHandler(conf.redisClient, callbacks, conf.rateLimitConfig),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package mcp_session
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -28,10 +29,14 @@ type filter struct {
|
||||
config *config
|
||||
stopChan chan struct{}
|
||||
|
||||
req *http.Request
|
||||
serverName string
|
||||
proxyURL *url.URL
|
||||
neepProcess bool
|
||||
req *http.Request
|
||||
serverName string
|
||||
proxyURL *url.URL
|
||||
matchedRule common.MatchRule
|
||||
needProcess bool
|
||||
skipRequestBody bool
|
||||
skipResponseBody bool
|
||||
cachedResponseBody []byte
|
||||
|
||||
userLevelConfig bool
|
||||
mcpConfigHandler *handler.MCPConfigHandler
|
||||
@@ -42,31 +47,33 @@ type filter struct {
|
||||
// Callbacks which are called in request path
|
||||
// The endStream is true if the request doesn't have body
|
||||
func (f *filter) DecodeHeaders(header api.RequestHeaderMap, endStream bool) api.StatusType {
|
||||
url := common.NewRequestURL(header)
|
||||
if url == nil {
|
||||
requestUrl := common.NewRequestURL(header)
|
||||
if requestUrl == nil {
|
||||
return api.Continue
|
||||
}
|
||||
f.path = url.ParsedURL.Path
|
||||
f.path = requestUrl.ParsedURL.Path
|
||||
|
||||
// Check if request matches any rule in match_list
|
||||
if !common.IsMatch(f.config.matchList, url.Host, f.path) {
|
||||
api.LogDebugf("Request does not match any rule in match_list: %s", url.ParsedURL.String())
|
||||
matched, matchedRule := common.IsMatch(f.config.matchList, requestUrl.Host, f.path)
|
||||
if !matched {
|
||||
api.LogDebugf("Request does not match any rule in match_list: %s", requestUrl.ParsedURL.String())
|
||||
return api.Continue
|
||||
}
|
||||
f.neepProcess = true
|
||||
f.needProcess = true
|
||||
f.matchedRule = matchedRule
|
||||
|
||||
f.req = &http.Request{
|
||||
Method: url.Method,
|
||||
URL: url.ParsedURL,
|
||||
Method: requestUrl.Method,
|
||||
URL: requestUrl.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())
|
||||
if !requestUrl.InternalIP {
|
||||
api.LogWarnf("Access denied: non-Internal IP address %s", requestUrl.ParsedURL.String())
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusForbidden, "", nil, 0, "")
|
||||
return api.LocalReply
|
||||
}
|
||||
if strings.HasSuffix(f.path, ConfigPathSuffix) && url.Method == http.MethodGet {
|
||||
if strings.HasSuffix(f.path, ConfigPathSuffix) && requestUrl.Method == http.MethodGet {
|
||||
api.LogDebugf("Handling config request: %s", f.path)
|
||||
f.mcpConfigHandler.HandleConfigRequest(f.req, []byte{})
|
||||
return api.LocalReply
|
||||
@@ -79,10 +86,27 @@ func (f *filter) DecodeHeaders(header api.RequestHeaderMap, endStream bool) api.
|
||||
}
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(url.ParsedURL.Path, GlobalSSEPathSuffix) {
|
||||
f.proxyURL = url.ParsedURL
|
||||
return f.processMcpRequestHeaders(header, endStream)
|
||||
}
|
||||
|
||||
func (f *filter) processMcpRequestHeaders(header api.RequestHeaderMap, endStream bool) api.StatusType {
|
||||
switch f.matchedRule.UpstreamType {
|
||||
case common.RestUpstream, common.StreamableUpstream:
|
||||
return f.processMcpRequestHeadersForRestUpstream(header, endStream)
|
||||
case common.SSEUpstream:
|
||||
return f.processMcpRequestHeadersForSSEUpstream(header, endStream)
|
||||
}
|
||||
f.needProcess = false
|
||||
return api.Continue
|
||||
}
|
||||
|
||||
func (f *filter) processMcpRequestHeadersForRestUpstream(header api.RequestHeaderMap, endStream bool) api.StatusType {
|
||||
method := f.req.Method
|
||||
requestUrl := f.req.URL
|
||||
if !strings.HasSuffix(requestUrl.Path, GlobalSSEPathSuffix) {
|
||||
f.proxyURL = requestUrl
|
||||
if f.config.enableUserLevelServer {
|
||||
parts := strings.Split(url.ParsedURL.Path, "/")
|
||||
parts := strings.Split(requestUrl.Path, "/")
|
||||
if len(parts) >= 3 {
|
||||
serverName := parts[1]
|
||||
uid := parts[2]
|
||||
@@ -102,13 +126,13 @@ func (f *filter) DecodeHeaders(header api.RequestHeaderMap, endStream bool) api.
|
||||
}
|
||||
}
|
||||
|
||||
if url.Method != http.MethodGet {
|
||||
if method != http.MethodGet {
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusMethodNotAllowed, "Method not allowed", nil, 0, "")
|
||||
} else {
|
||||
f.config.defaultServer = common.NewSSEServer(common.NewMCPServer(DefaultServerName, Version),
|
||||
common.WithSSEEndpoint(GlobalSSEPathSuffix),
|
||||
common.WithMessageEndpoint(strings.TrimSuffix(url.ParsedURL.Path, GlobalSSEPathSuffix)),
|
||||
common.WithRedisClient(common.GlobalRedisClient))
|
||||
common.WithMessageEndpoint(strings.TrimSuffix(requestUrl.Path, GlobalSSEPathSuffix)),
|
||||
common.WithRedisClient(f.config.redisClient))
|
||||
f.serverName = f.config.defaultServer.GetServerName()
|
||||
body := "SSE connection create"
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusOK, body, nil, 0, "")
|
||||
@@ -116,10 +140,60 @@ func (f *filter) DecodeHeaders(header api.RequestHeaderMap, endStream bool) api.
|
||||
return api.LocalReply
|
||||
}
|
||||
|
||||
func (f *filter) processMcpRequestHeadersForSSEUpstream(header api.RequestHeaderMap, endStream bool) api.StatusType {
|
||||
// We don't need to process the request body for SSE upstream.
|
||||
f.skipRequestBody = true
|
||||
f.rewritePathForSSEUpstream(header)
|
||||
return api.Continue
|
||||
}
|
||||
|
||||
func (f *filter) rewritePathForSSEUpstream(header api.RequestHeaderMap) {
|
||||
matchedRule := f.matchedRule
|
||||
if !matchedRule.EnablePathRewrite || matchedRule.MatchRuleType != common.PrefixMatch {
|
||||
// No rewrite required, so we don't need to process the response body, either.
|
||||
f.skipResponseBody = true
|
||||
return
|
||||
}
|
||||
|
||||
path := f.req.URL.Path
|
||||
if !strings.HasPrefix(path, matchedRule.MatchRulePath) {
|
||||
api.LogWarnf("Unexpected: Path %s does not match the configured prefix %s", path, matchedRule.MatchRulePath)
|
||||
return
|
||||
}
|
||||
|
||||
rewrittenPath := path[len(matchedRule.MatchRulePath):]
|
||||
|
||||
if rewrittenPath == "" {
|
||||
rewrittenPath = matchedRule.PathRewritePrefix
|
||||
} else {
|
||||
rewritePrefixHasTrailingSlash := strings.HasSuffix(matchedRule.PathRewritePrefix, "/")
|
||||
pathSuffixHasLeadingSlash := strings.HasPrefix(rewrittenPath, "/")
|
||||
if rewritePrefixHasTrailingSlash != pathSuffixHasLeadingSlash {
|
||||
// One has, the other doesn't have.
|
||||
rewrittenPath = matchedRule.PathRewritePrefix + rewrittenPath
|
||||
} else if pathSuffixHasLeadingSlash {
|
||||
// Both have.
|
||||
rewrittenPath = matchedRule.PathRewritePrefix + rewrittenPath[1:]
|
||||
} else {
|
||||
// Neither have.
|
||||
rewrittenPath = matchedRule.PathRewritePrefix + "/" + rewrittenPath
|
||||
}
|
||||
}
|
||||
|
||||
if f.req.URL.RawQuery != "" {
|
||||
rewrittenPath = rewrittenPath + "?" + f.req.URL.RawQuery
|
||||
}
|
||||
|
||||
header.SetPath(rewrittenPath)
|
||||
}
|
||||
|
||||
// DecodeData might be called multiple times during handling the request body.
|
||||
// The endStream is true when handling the last piece of the body.
|
||||
func (f *filter) DecodeData(buffer api.BufferInstance, endStream bool) api.StatusType {
|
||||
if !f.neepProcess {
|
||||
if !f.needProcess || f.skipRequestBody {
|
||||
return api.Continue
|
||||
}
|
||||
if f.matchedRule.UpstreamType != common.RestUpstream && f.matchedRule.UpstreamType != common.StreamableUpstream {
|
||||
return api.Continue
|
||||
}
|
||||
if !endStream {
|
||||
@@ -158,14 +232,21 @@ func (f *filter) DecodeData(buffer api.BufferInstance, endStream bool) api.Statu
|
||||
return api.Continue
|
||||
}
|
||||
|
||||
// Callbacks which are called in response path
|
||||
// The endStream is true if the response doesn't have body
|
||||
// EncodeHeaders Callbacks which are called in response path.
|
||||
// The endStream is true if the response doesn't have body.
|
||||
func (f *filter) EncodeHeaders(header api.ResponseHeaderMap, endStream bool) api.StatusType {
|
||||
if !f.neepProcess {
|
||||
if !f.needProcess {
|
||||
return api.Continue
|
||||
}
|
||||
if f.matchedRule.UpstreamType != common.RestUpstream && f.matchedRule.UpstreamType != common.StreamableUpstream {
|
||||
if contentType, ok := header.Get("content-type"); !ok || !strings.HasPrefix(contentType, "text/event-stream") {
|
||||
api.LogDebugf("Skip response body for non-SSE upstream. Content-Type: %s", contentType)
|
||||
f.skipResponseBody = true
|
||||
}
|
||||
return api.Continue
|
||||
}
|
||||
if f.serverName != "" {
|
||||
if common.GlobalRedisClient != nil {
|
||||
if f.config.redisClient != nil {
|
||||
header.Set("Content-Type", "text/event-stream")
|
||||
header.Set("Cache-Control", "no-cache")
|
||||
header.Set("Connection", "keep-alive")
|
||||
@@ -182,18 +263,41 @@ func (f *filter) EncodeHeaders(header api.ResponseHeaderMap, endStream bool) api
|
||||
// EncodeData might be called multiple times during handling the response body.
|
||||
// The endStream is true when handling the last piece of the body.
|
||||
func (f *filter) EncodeData(buffer api.BufferInstance, endStream bool) api.StatusType {
|
||||
if !f.neepProcess {
|
||||
if !f.needProcess || f.skipResponseBody {
|
||||
return api.Continue
|
||||
}
|
||||
|
||||
ret := api.Continue
|
||||
api.LogDebugf("Upstream Type: %s", f.matchedRule.UpstreamType)
|
||||
switch f.matchedRule.UpstreamType {
|
||||
case common.RestUpstream, common.StreamableUpstream:
|
||||
api.LogDebugf("Encoding data from Rest upstream")
|
||||
ret = f.encodeDataFromRestUpstream(buffer, endStream)
|
||||
break
|
||||
case common.SSEUpstream:
|
||||
api.LogDebugf("Encoding data from SSE upstream")
|
||||
ret = f.encodeDataFromSSEUpstream(buffer, endStream)
|
||||
if endStream {
|
||||
// Always continue as long as the stream has ended.
|
||||
ret = api.Continue
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (f *filter) encodeDataFromRestUpstream(buffer api.BufferInstance, endStream bool) api.StatusType {
|
||||
if !f.needProcess {
|
||||
return api.Continue
|
||||
}
|
||||
if !endStream {
|
||||
return api.StopAndBuffer
|
||||
}
|
||||
if f.proxyURL != nil && common.GlobalRedisClient != nil {
|
||||
if f.proxyURL != nil && f.config.redisClient != nil {
|
||||
sessionID := f.proxyURL.Query().Get("sessionId")
|
||||
if sessionID != "" {
|
||||
channel := common.GetSSEChannelName(sessionID)
|
||||
eventData := fmt.Sprintf("event: message\ndata: %s\n\n", buffer.String())
|
||||
publishErr := common.GlobalRedisClient.Publish(channel, eventData)
|
||||
publishErr := f.config.redisClient.Publish(channel, eventData)
|
||||
if publishErr != nil {
|
||||
api.LogErrorf("Failed to publish wasm mcp server message to Redis: %v", publishErr)
|
||||
}
|
||||
@@ -201,19 +305,163 @@ func (f *filter) EncodeData(buffer api.BufferInstance, endStream bool) api.Statu
|
||||
}
|
||||
|
||||
if f.serverName != "" {
|
||||
if common.GlobalRedisClient != nil {
|
||||
if f.config.redisClient != nil {
|
||||
// handle default server
|
||||
buffer.Reset()
|
||||
f.config.defaultServer.HandleSSE(f.callbacks, f.stopChan)
|
||||
return api.Running
|
||||
} else {
|
||||
buffer.SetString(RedisNotEnabledResponseBody)
|
||||
_ = buffer.SetString(RedisNotEnabledResponseBody)
|
||||
return api.Continue
|
||||
}
|
||||
}
|
||||
return api.Continue
|
||||
}
|
||||
|
||||
func (f *filter) encodeDataFromSSEUpstream(buffer api.BufferInstance, endStream bool) api.StatusType {
|
||||
bufferBytes := buffer.Bytes()
|
||||
bufferData := string(bufferBytes)
|
||||
|
||||
err, lineBreak := f.findSSELineBreak(bufferData)
|
||||
if err != nil {
|
||||
api.LogWarnf("Failed to find line break in SSE data: %v", err)
|
||||
f.needProcess = false
|
||||
return api.Continue
|
||||
}
|
||||
if lineBreak == "" {
|
||||
// Have not found any line break. Need to buffer and check again.
|
||||
return api.StopAndBuffer
|
||||
}
|
||||
|
||||
api.LogDebugf("Line break sequence: %v", []byte(lineBreak))
|
||||
|
||||
err, endpointUrl := f.findEndpointUrl(bufferData, lineBreak)
|
||||
if err != nil {
|
||||
api.LogWarnf("Failed to find endpoint URL in SSE data: %v", err)
|
||||
f.needProcess = false
|
||||
return api.Continue
|
||||
}
|
||||
if endpointUrl == "" {
|
||||
// No endpoint URL found. Need to buffer and check again.
|
||||
return api.StopAndBuffer
|
||||
}
|
||||
|
||||
// Remove query string since we don't need to change it.
|
||||
queryStringIndex := strings.IndexAny(endpointUrl, "?")
|
||||
if queryStringIndex != -1 {
|
||||
endpointUrl = endpointUrl[:queryStringIndex]
|
||||
}
|
||||
|
||||
if changed, newEndpointUrl := f.rewriteEndpointUrl(endpointUrl); changed {
|
||||
api.LogDebugf("The endpoint URL is changed.\n Old: %s\n New: %s", endpointUrl, newEndpointUrl)
|
||||
|
||||
endpointUrlIndex := strings.Index(bufferData, endpointUrl)
|
||||
if endpointUrlIndex == -1 {
|
||||
api.LogWarnf("Something wrong, the previously found endpoint URL %s not found in the SSE data now", endpointUrl)
|
||||
} else {
|
||||
bufferData = bufferData[:endpointUrlIndex] + newEndpointUrl + bufferData[endpointUrlIndex+len(endpointUrl):]
|
||||
_ = buffer.SetString(bufferData)
|
||||
}
|
||||
} else {
|
||||
api.LogDebugf("The endpoint URL %s is not changed", endpointUrl)
|
||||
}
|
||||
|
||||
f.needProcess = false
|
||||
return api.Continue
|
||||
}
|
||||
|
||||
func (f *filter) rewriteEndpointUrl(endpointUrl string) (bool, string) {
|
||||
if !f.matchedRule.EnablePathRewrite {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
if schemeIndex := strings.Index(endpointUrl, "://"); schemeIndex != -1 {
|
||||
endpointUrl = endpointUrl[schemeIndex+3:]
|
||||
if slashIndex := strings.Index(endpointUrl, "/"); slashIndex != -1 {
|
||||
endpointUrl = endpointUrl[slashIndex:]
|
||||
} else {
|
||||
endpointUrl = "/"
|
||||
}
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(endpointUrl, f.matchedRule.PathRewritePrefix) {
|
||||
// The endpoint URL does not match the path rewrite prefix. We are unable to rewrite it back.
|
||||
api.LogWarnf("The endpoint URL %s does not match the path rewrite prefix %s", endpointUrl, f.matchedRule.PathRewritePrefix)
|
||||
return false, ""
|
||||
}
|
||||
|
||||
suffix := endpointUrl[len(f.matchedRule.PathRewritePrefix):]
|
||||
|
||||
if len(suffix) == 0 {
|
||||
endpointUrl = f.matchedRule.MatchRulePath
|
||||
} else {
|
||||
matchPathHasTrailingSlash := strings.HasSuffix(f.matchedRule.MatchRulePath, "/")
|
||||
suffixHasLeadingSlash := strings.HasPrefix(suffix, "/")
|
||||
if matchPathHasTrailingSlash != suffixHasLeadingSlash {
|
||||
// One has, the other doesn't have.
|
||||
endpointUrl = f.matchedRule.MatchRulePath + suffix
|
||||
} else if matchPathHasTrailingSlash {
|
||||
// Both have.
|
||||
endpointUrl = f.matchedRule.MatchRulePath + suffix[1:]
|
||||
} else {
|
||||
// Neither have.
|
||||
endpointUrl = f.matchedRule.MatchRulePath + "/" + suffix
|
||||
}
|
||||
}
|
||||
|
||||
return true, endpointUrl
|
||||
}
|
||||
|
||||
func (f *filter) findSSELineBreak(bufferData string) (error, string) {
|
||||
// See https://html.spec.whatwg.org/multipage/server-sent-events.html
|
||||
crIndex := strings.IndexAny(bufferData, "\r")
|
||||
lfIndex := strings.IndexAny(bufferData, "\n")
|
||||
if crIndex == -1 && lfIndex == -1 {
|
||||
// No line break found.
|
||||
return nil, ""
|
||||
}
|
||||
lineBreak := ""
|
||||
if crIndex != -1 && lfIndex != -1 {
|
||||
if crIndex+1 != lfIndex {
|
||||
// Found both line breaks, but they are not adjacent. Skip body processing.
|
||||
return errors.New("found non-adjacent CR and LF"), ""
|
||||
}
|
||||
lineBreak = "\r\n"
|
||||
} else if crIndex != -1 {
|
||||
lineBreak = "\r"
|
||||
} else {
|
||||
lineBreak = "\n"
|
||||
}
|
||||
return nil, lineBreak
|
||||
}
|
||||
|
||||
func (f *filter) findEndpointUrl(bufferData, lineBreak string) (error, string) {
|
||||
eventIndex := strings.Index(bufferData, "event:")
|
||||
if eventIndex == -1 {
|
||||
return nil, ""
|
||||
}
|
||||
bufferData = bufferData[eventIndex:]
|
||||
eventEndIndex := strings.Index(bufferData, lineBreak)
|
||||
if eventEndIndex == -1 {
|
||||
return nil, ""
|
||||
}
|
||||
eventName := strings.TrimSpace(bufferData[len("event:"):eventEndIndex])
|
||||
if eventName != "endpoint" {
|
||||
return fmt.Errorf("the initial event [%s] is not an endpoint event. Skip processing", eventName), ""
|
||||
}
|
||||
bufferData = bufferData[eventEndIndex+len(lineBreak):]
|
||||
dataEndIndex := strings.Index(bufferData, lineBreak)
|
||||
if dataEndIndex == -1 {
|
||||
// Data received not enough.
|
||||
return nil, ""
|
||||
}
|
||||
eventData := bufferData[:dataEndIndex]
|
||||
if !strings.HasPrefix(eventData, "data:") {
|
||||
return fmt.Errorf("an unexpected non-data field found in the event. Skip processing. Field: %s", eventData), ""
|
||||
}
|
||||
return nil, strings.TrimSpace(eventData[len("data:"):])
|
||||
}
|
||||
|
||||
// OnDestroy stops the goroutine
|
||||
func (f *filter) OnDestroy(reason api.DestroyReason) {
|
||||
api.LogDebugf("OnDestroy: reason=%v", reason)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
| `modelKey` | string | 选填 | model | 请求body中model参数的位置 |
|
||||
| `addProviderHeader` | string | 选填 | - | 从model参数中解析出的provider名字放到哪个请求header中 |
|
||||
| `modelToHeader` | string | 选填 | - | 直接将model参数放到哪个请求header中 |
|
||||
| `enableOnPathSuffix` | array of string | 选填 | ["/v1/chat/completions"] | 只对这些特定路径后缀的请求生效,可以配置为 "*" 以匹配所有路径 |
|
||||
| `enableOnPathSuffix` | array of string | 选填 | ["/completions","/embeddings","/images/generations","/audio/speech","/fine_tuning/jobs","/moderations"] | 只对这些特定路径后缀的请求生效,可以配置为 "*" 以匹配所有路径 |
|
||||
|
||||
## 运行属性
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
ARG BUILDER=higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/wasm-go-builder:go1.20.14-tinygo0.29.0-oras1.0.0
|
||||
FROM $BUILDER as builder
|
||||
FROM $BUILDER AS builder
|
||||
|
||||
|
||||
ARG GOPROXY
|
||||
@@ -26,6 +26,6 @@ RUN \
|
||||
tinygo build -o /main.wasm -scheduler=none -gc=custom -tags="custommalloc nottinygc_finalizer $EXTRA_TAGS" -target=wasi ./ ; \
|
||||
fi
|
||||
|
||||
FROM scratch as output
|
||||
FROM scratch AS output
|
||||
|
||||
COPY --from=builder /main.wasm plugin.wasm
|
||||
|
||||
@@ -11,11 +11,11 @@ require (
|
||||
github.com/higress-group/proxy-wasm-go-sdk v1.0.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/tidwall/gjson v1.17.3
|
||||
github.com/wasilibs/go-re2 v1.6.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/tetratelabs/wazero v1.7.2 // indirect
|
||||
github.com/wasilibs/go-re2 v1.6.0 // indirect
|
||||
golang.org/x/sys v0.21.0 // indirect
|
||||
)
|
||||
|
||||
|
||||
@@ -6,8 +6,6 @@ github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbG
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520/go.mod h1:Nz8ORLaFiLWotg6GeKlJMhv8cci8mM43uEnLA5t8iew=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v1.0.0 h1:BZRNf4R7jr9hwRivg/E29nkVaKEak5MWjBDhWjuHijU=
|
||||
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/magefile/mage v1.15.1-0.20230912152418-9f54e0f83e2a h1:tdPcGgyiH0K+SbsJBBm2oPyEIOTAvLBwD9TuUwVtZho=
|
||||
github.com/magefile/mage v1.15.1-0.20230912152418-9f54e0f83e2a/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
@@ -29,6 +27,7 @@ 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/wasilibs/go-re2 v1.6.0 h1:CLlhDebt38wtl/zz4ww+hkXBMcxjrKFvTDXzFW2VOz8=
|
||||
github.com/wasilibs/go-re2 v1.6.0/go.mod h1:prArCyErsypRBI/jFAFJEbzyHzjABKqkzlidF0SNA04=
|
||||
github.com/wasilibs/nottinygc v0.4.0 h1:h1TJMihMC4neN6Zq+WKpLxgd9xCFMw7O9ETLwY2exJQ=
|
||||
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
|
||||
@@ -358,6 +358,9 @@ func getApiName(path string) provider.ApiName {
|
||||
if strings.HasSuffix(path, "/v1/files") {
|
||||
return provider.ApiNameFiles
|
||||
}
|
||||
if strings.HasSuffix(path, "/v1/models") {
|
||||
return provider.ApiNameModels
|
||||
}
|
||||
// cohere style
|
||||
if strings.HasSuffix(path, "/v1/rerank") {
|
||||
return provider.ApiNameCohereV1Rerank
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"hash/crc32"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -32,7 +33,10 @@ const (
|
||||
bedrockChatCompletionPath = "/model/%s/converse"
|
||||
// converseStream路径 /model/{modelId}/converse-stream
|
||||
bedrockStreamChatCompletionPath = "/model/%s/converse-stream"
|
||||
bedrockSignedHeaders = "host;x-amz-date"
|
||||
// invoke_model 路径 /model/{modelId}/invoke
|
||||
bedrockInvokeModelPath = "/model/%s/invoke"
|
||||
bedrockSignedHeaders = "host;x-amz-date"
|
||||
requestIdHeader = "X-Amzn-Requestid"
|
||||
)
|
||||
|
||||
type bedrockProviderInitializer struct {
|
||||
@@ -50,7 +54,8 @@ func (b *bedrockProviderInitializer) ValidateConfig(config *ProviderConfig) erro
|
||||
|
||||
func (b *bedrockProviderInitializer) DefaultCapabilities() map[string]string {
|
||||
return map[string]string{
|
||||
string(ApiNameChatCompletion): bedrockChatCompletionPath,
|
||||
string(ApiNameChatCompletion): bedrockChatCompletionPath,
|
||||
string(ApiNameImageGeneration): bedrockInvokeModelPath,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +104,7 @@ func (b *bedrockProvider) convertEventFromBedrockToOpenAI(ctx wrapper.HttpContex
|
||||
chatChoice.FinishReason = stopReasonBedrock2OpenAI(*bedrockEvent.StopReason)
|
||||
}
|
||||
choices = append(choices, chatChoice)
|
||||
requestId := ctx.GetStringContext("X-Amzn-Requestid", "")
|
||||
requestId := ctx.GetStringContext(requestIdHeader, "")
|
||||
openAIFormattedChunk := &chatCompletionResponse{
|
||||
Id: requestId,
|
||||
Created: time.Now().UnixMilli() / 1000,
|
||||
@@ -152,6 +157,74 @@ type toolUseBlockDelta struct {
|
||||
Input string `json:"input"`
|
||||
}
|
||||
|
||||
type bedrockImageGenerationResponse struct {
|
||||
Images []string `json:"images"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type bedrockImageGenerationTextToImageParams struct {
|
||||
Text string `json:"text"`
|
||||
NegativeText string `json:"negativeText,omitempty"`
|
||||
ConditionImage string `json:"conditionImage,omitempty"`
|
||||
ControlMode string `json:"controlMode,omitempty"`
|
||||
ControlStrength float32 `json:"controlLength,omitempty"`
|
||||
}
|
||||
|
||||
type bedrockImageGenerationConfig struct {
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Quality string `json:"quality,omitempty"`
|
||||
CfgScale float32 `json:"cfgScale,omitempty"`
|
||||
Seed int `json:"seed,omitempty"`
|
||||
NumberOfImages int `json:"numberOfImages"`
|
||||
}
|
||||
|
||||
type bedrockImageGenerationColorGuidedGenerationParams struct {
|
||||
Colors []string `json:"colors"`
|
||||
ReferenceImage string `json:"referenceImage"`
|
||||
Text string `json:"text"`
|
||||
NegativeText string `json:"negativeText,omitempty"`
|
||||
}
|
||||
|
||||
type bedrockImageGenerationImageVariationParams struct {
|
||||
Images []string `json:"images"`
|
||||
SimilarityStrength float32 `json:"similarityStrength"`
|
||||
Text string `json:"text"`
|
||||
NegativeText string `json:"negativeText,omitempty"`
|
||||
}
|
||||
|
||||
type bedrockImageGenerationInPaintingParams struct {
|
||||
Image string `json:"image"`
|
||||
MaskPrompt string `json:"maskPrompt"`
|
||||
MaskImage string `json:"maskImage"`
|
||||
Text string `json:"text"`
|
||||
NegativeText string `json:"negativeText,omitempty"`
|
||||
}
|
||||
|
||||
type bedrockImageGenerationOutPaintingParams struct {
|
||||
Image string `json:"image"`
|
||||
MaskPrompt string `json:"maskPrompt"`
|
||||
MaskImage string `json:"maskImage"`
|
||||
OutPaintingMode string `json:"outPaintingMode"`
|
||||
Text string `json:"text"`
|
||||
NegativeText string `json:"negativeText,omitempty"`
|
||||
}
|
||||
|
||||
type bedrockImageGenerationBackgroundRemovalParams struct {
|
||||
Image string `json:"image"`
|
||||
}
|
||||
|
||||
type bedrockImageGenerationRequest struct {
|
||||
TaskType string `json:"taskType"`
|
||||
ImageGenerationConfig *bedrockImageGenerationConfig `json:"imageGenerationConfig"`
|
||||
TextToImageParams *bedrockImageGenerationTextToImageParams `json:"textToImageParams,omitempty"`
|
||||
ColorGuidedGenerationParams *bedrockImageGenerationColorGuidedGenerationParams `json:"colorGuidedGenerationParams,omitempty"`
|
||||
ImageVariationParams *bedrockImageGenerationImageVariationParams `json:"imageVariationParams,omitempty"`
|
||||
InPaintingParams *bedrockImageGenerationInPaintingParams `json:"inPaintingParams,omitempty"`
|
||||
OutPaintingParams *bedrockImageGenerationOutPaintingParams `json:"outPaintingParams,omitempty"`
|
||||
BackgroundRemovalParams *bedrockImageGenerationBackgroundRemovalParams `json:"backgroundRemovalParams,omitempty"`
|
||||
}
|
||||
|
||||
func extractAmazonEventStreamEvents(ctx wrapper.HttpContext, chunk []byte) []ConverseStreamEvent {
|
||||
body := chunk
|
||||
if bufferedStreamingBody, has := ctx.GetContext(ctxKeyStreamingBody).([]byte); has {
|
||||
@@ -489,7 +562,7 @@ func validateCRC(r io.Reader, expect uint32) error {
|
||||
}
|
||||
|
||||
func (b *bedrockProvider) TransformResponseHeaders(ctx wrapper.HttpContext, apiName ApiName, headers http.Header) {
|
||||
ctx.SetContext("X-Amzn-Requestid", headers.Get("X-Amzn-Requestid"))
|
||||
ctx.SetContext(requestIdHeader, headers.Get(requestIdHeader))
|
||||
if headers.Get("Content-Type") == "application/vnd.amazon.eventstream" {
|
||||
headers.Set("Content-Type", "text/event-stream; charset=utf-8")
|
||||
}
|
||||
@@ -537,18 +610,83 @@ func (b *bedrockProvider) TransformRequestBodyHeaders(ctx wrapper.HttpContext, a
|
||||
switch apiName {
|
||||
case ApiNameChatCompletion:
|
||||
return b.onChatCompletionRequestBody(ctx, body, headers)
|
||||
case ApiNameImageGeneration:
|
||||
return b.onImageGenerationRequestBody(ctx, body, headers)
|
||||
default:
|
||||
return b.config.defaultTransformRequestBody(ctx, apiName, body)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *bedrockProvider) TransformResponseBody(ctx wrapper.HttpContext, apiName ApiName, body []byte) ([]byte, error) {
|
||||
if apiName == ApiNameChatCompletion {
|
||||
switch apiName {
|
||||
case ApiNameChatCompletion:
|
||||
return b.onChatCompletionResponseBody(ctx, body)
|
||||
case ApiNameImageGeneration:
|
||||
return b.onImageGenerationResponseBody(ctx, body)
|
||||
}
|
||||
return nil, errUnsupportedApiName
|
||||
}
|
||||
|
||||
func (b *bedrockProvider) onImageGenerationResponseBody(ctx wrapper.HttpContext, body []byte) ([]byte, error) {
|
||||
bedrockResponse := &bedrockImageGenerationResponse{}
|
||||
if err := json.Unmarshal(body, bedrockResponse); err != nil {
|
||||
log.Errorf("unable to unmarshal bedrock image gerneration response: %v", err)
|
||||
return nil, fmt.Errorf("unable to unmarshal bedrock image generation response: %v", err)
|
||||
}
|
||||
response := b.buildBedrockImageGenerationResponse(ctx, bedrockResponse)
|
||||
return json.Marshal(response)
|
||||
}
|
||||
|
||||
func (b *bedrockProvider) onImageGenerationRequestBody(ctx wrapper.HttpContext, body []byte, headers http.Header) ([]byte, error) {
|
||||
request := &imageGenerationRequest{}
|
||||
err := b.config.parseRequestAndMapModel(ctx, request, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
headers.Set("Accept", "*/*")
|
||||
util.OverwriteRequestPathHeader(headers, fmt.Sprintf(bedrockInvokeModelPath, request.Model))
|
||||
return b.buildBedrockImageGenerationRequest(request, headers)
|
||||
}
|
||||
|
||||
func (b *bedrockProvider) buildBedrockImageGenerationRequest(origRequest *imageGenerationRequest, headers http.Header) ([]byte, error) {
|
||||
width, height := 1024, 1024
|
||||
pairs := strings.Split(origRequest.Size, "x")
|
||||
if len(pairs) == 2 {
|
||||
width, _ = strconv.Atoi(pairs[0])
|
||||
height, _ = strconv.Atoi(pairs[1])
|
||||
}
|
||||
|
||||
request := &bedrockImageGenerationRequest{
|
||||
TaskType: "TEXT_IMAGE",
|
||||
TextToImageParams: &bedrockImageGenerationTextToImageParams{
|
||||
Text: origRequest.Prompt,
|
||||
},
|
||||
ImageGenerationConfig: &bedrockImageGenerationConfig{
|
||||
NumberOfImages: origRequest.N,
|
||||
Width: width,
|
||||
Height: height,
|
||||
Quality: origRequest.Quality,
|
||||
},
|
||||
}
|
||||
util.OverwriteRequestPathHeader(headers, fmt.Sprintf(bedrockInvokeModelPath, origRequest.Model))
|
||||
requestBytes, err := json.Marshal(request)
|
||||
b.setAuthHeaders(requestBytes, headers)
|
||||
return requestBytes, err
|
||||
}
|
||||
|
||||
func (b *bedrockProvider) buildBedrockImageGenerationResponse(ctx wrapper.HttpContext, bedrockResponse *bedrockImageGenerationResponse) *imageGenerationResponse {
|
||||
data := make([]imageGenerationData, len(bedrockResponse.Images))
|
||||
for i, image := range bedrockResponse.Images {
|
||||
data[i] = imageGenerationData{
|
||||
B64: image,
|
||||
}
|
||||
}
|
||||
return &imageGenerationResponse{
|
||||
Created: time.Now().UnixMilli() / 1000,
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *bedrockProvider) onChatCompletionResponseBody(ctx wrapper.HttpContext, body []byte) ([]byte, error) {
|
||||
bedrockResponse := &bedrockConverseResponse{}
|
||||
if err := json.Unmarshal(body, bedrockResponse); err != nil {
|
||||
@@ -613,7 +751,7 @@ func (b *bedrockProvider) buildChatCompletionResponse(ctx wrapper.HttpContext, b
|
||||
FinishReason: stopReasonBedrock2OpenAI(bedrockResponse.StopReason),
|
||||
},
|
||||
}
|
||||
requestId := ctx.GetStringContext("X-Amzn-Requestid", "")
|
||||
requestId := ctx.GetStringContext(requestIdHeader, "")
|
||||
return &chatCompletionResponse{
|
||||
Id: requestId,
|
||||
Created: time.Now().UnixMilli() / 1000,
|
||||
|
||||
@@ -36,7 +36,10 @@ func (g *geminiProviderInitializer) ValidateConfig(config *ProviderConfig) error
|
||||
}
|
||||
|
||||
func (g *geminiProviderInitializer) DefaultCapabilities() map[string]string {
|
||||
return map[string]string{}
|
||||
return map[string]string{
|
||||
string(ApiNameChatCompletion): "",
|
||||
string(ApiNameEmbeddings): "",
|
||||
}
|
||||
}
|
||||
|
||||
func (g *geminiProviderInitializer) CreateProvider(config ProviderConfig) (Provider, error) {
|
||||
@@ -65,6 +68,7 @@ func (g *geminiProvider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiNa
|
||||
func (g *geminiProvider) TransformRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, headers http.Header) {
|
||||
util.OverwriteRequestHostHeader(headers, geminiDomain)
|
||||
headers.Set(geminiApiKeyHeader, g.config.GetApiTokenInUse(ctx))
|
||||
util.OverwriteRequestAuthorizationHeader(headers, "")
|
||||
}
|
||||
|
||||
func (g *geminiProvider) OnRequestBody(ctx wrapper.HttpContext, apiName ApiName, body []byte) (types.Action, error) {
|
||||
|
||||
@@ -356,10 +356,39 @@ func (e *StreamEvent) ToHttpString() string {
|
||||
|
||||
// https://platform.openai.com/docs/guides/images
|
||||
type imageGenerationRequest struct {
|
||||
Model string `json:"model"`
|
||||
Prompt string `json:"prompt"`
|
||||
N int `json:"n,omitempty"`
|
||||
Size string `json:"size,omitempty"`
|
||||
Model string `json:"model"`
|
||||
Prompt string `json:"prompt"`
|
||||
Background string `json:"background,omitempty"`
|
||||
Moderation string `json:"moderation,omitempty"`
|
||||
OutputCompression int `json:"output_compression,omitempty"`
|
||||
OutputFormat string `json:"output_format,omitempty"`
|
||||
Quality string `json:"quality,omitempty"`
|
||||
ResponseFormat string `json:"response_format,omitempty"`
|
||||
Style string `json:"style,omitempty"`
|
||||
N int `json:"n,omitempty"`
|
||||
Size string `json:"size,omitempty"`
|
||||
}
|
||||
|
||||
type imageGenerationData struct {
|
||||
URL string `json:"url,omitempty"`
|
||||
B64 string `json:"b64_json,omitempty"`
|
||||
RevisedPrompt string `json:"revised_prompt,omitempty"`
|
||||
}
|
||||
|
||||
type imageGenerationUsage struct {
|
||||
TotalTokens int `json:"total_tokens"`
|
||||
InputTokens int `json:"input_tokens"`
|
||||
OutputTokens int `json:"output_tokens"`
|
||||
InputTokensDetails struct {
|
||||
TextTokens int `json:"text_tokens"`
|
||||
ImageTokens int `json:"image_tokens"`
|
||||
} `json:"input_tokens_details"`
|
||||
}
|
||||
|
||||
type imageGenerationResponse struct {
|
||||
Created int64 `json:"created"`
|
||||
Data []imageGenerationData `json:"data"`
|
||||
Usage *imageGenerationUsage `json:"usage,omitempty"`
|
||||
}
|
||||
|
||||
// https://platform.openai.com/docs/guides/speech-to-text
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
const (
|
||||
moonshotDomain = "api.moonshot.cn"
|
||||
moonshotChatCompletionPath = "/v1/chat/completions"
|
||||
moonshotModelsPath = "/v1/models"
|
||||
)
|
||||
|
||||
type moonshotProviderInitializer struct {
|
||||
@@ -38,6 +39,7 @@ func (m *moonshotProviderInitializer) ValidateConfig(config *ProviderConfig) err
|
||||
func (m *moonshotProviderInitializer) DefaultCapabilities() map[string]string {
|
||||
return map[string]string{
|
||||
string(ApiNameChatCompletion): moonshotChatCompletionPath,
|
||||
string(ApiNameModels): moonshotModelsPath,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ func (m *ollamaProviderInitializer) DefaultCapabilities() map[string]string {
|
||||
// ollama的chat接口path和OpenAI的chat接口一样
|
||||
string(ApiNameChatCompletion): PathOpenAIChatCompletions,
|
||||
string(ApiNameEmbeddings): PathOpenAIEmbeddings,
|
||||
string(ApiNameModels): PathOpenAIModels,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ const (
|
||||
defaultOpenaiEmbeddingsPath = "/v1/embeddings"
|
||||
defaultOpenaiAudioSpeech = "/v1/audio/speech"
|
||||
defaultOpenaiImageGeneration = "/v1/images/generations"
|
||||
defaultOpenaiModels = "/v1/models"
|
||||
)
|
||||
|
||||
type openaiProviderInitializer struct {
|
||||
@@ -37,6 +38,7 @@ func (m *openaiProviderInitializer) DefaultCapabilities() map[string]string {
|
||||
string(ApiNameEmbeddings): defaultOpenaiEmbeddingsPath,
|
||||
string(ApiNameImageGeneration): defaultOpenaiImageGeneration,
|
||||
string(ApiNameAudioSpeech): defaultOpenaiAudioSpeech,
|
||||
string(ApiNameModels): defaultOpenaiModels,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,12 +31,14 @@ const (
|
||||
ApiNameAudioSpeech ApiName = "openai/v1/audiospeech"
|
||||
ApiNameFiles ApiName = "openai/v1/files"
|
||||
ApiNameBatches ApiName = "openai/v1/batches"
|
||||
ApiNameModels ApiName = "openai/v1/models"
|
||||
|
||||
PathOpenAICompletions = "/v1/completions"
|
||||
PathOpenAIChatCompletions = "/v1/chat/completions"
|
||||
PathOpenAIEmbeddings = "/v1/embeddings"
|
||||
PathOpenAIFiles = "/v1/files"
|
||||
PathOpenAIBatches = "/v1/batches"
|
||||
PathOpenAIModels = "/v1/models"
|
||||
|
||||
// TODO: 以下是一些非标准的API名称,需要进一步确认是否支持
|
||||
ApiNameCohereV1Rerank ApiName = "cohere/v1/rerank"
|
||||
@@ -532,6 +534,11 @@ func (c *ProviderConfig) parseRequestAndMapModel(ctx wrapper.HttpContext, reques
|
||||
return err
|
||||
}
|
||||
return c.setRequestModel(ctx, req)
|
||||
case *imageGenerationRequest:
|
||||
if err := decodeImageGenerationRequest(body, req); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.setRequestModel(ctx, req)
|
||||
default:
|
||||
return errors.New("unsupported request type")
|
||||
}
|
||||
@@ -545,6 +552,8 @@ func (c *ProviderConfig) setRequestModel(ctx wrapper.HttpContext, request interf
|
||||
model = &req.Model
|
||||
case *embeddingsRequest:
|
||||
model = &req.Model
|
||||
case *imageGenerationRequest:
|
||||
model = &req.Model
|
||||
default:
|
||||
return errors.New("unsupported request type")
|
||||
}
|
||||
|
||||
@@ -25,6 +25,13 @@ func decodeEmbeddingsRequest(body []byte, request *embeddingsRequest) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeImageGenerationRequest(body []byte, request *imageGenerationRequest) error {
|
||||
if err := json.Unmarshal(body, request); err != nil {
|
||||
return fmt.Errorf("unable to unmarshal request: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func replaceJsonRequestBody(request interface{}) error {
|
||||
body, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
|
||||
@@ -27,6 +27,7 @@ func main() {
|
||||
}
|
||||
|
||||
const (
|
||||
defaultMaxBodyBytes uint32 = 100 * 1024 * 1024
|
||||
// Context consts
|
||||
StatisticsRequestStartTime = "ai-statistics-request-start-time"
|
||||
StatisticsFirstTokenTime = "ai-statistics-first-token-time"
|
||||
@@ -176,6 +177,11 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, config AIStatisticsConfig, lo
|
||||
if consumer, _ := proxywasm.GetHttpRequestHeader(ConsumerKey); consumer != "" {
|
||||
ctx.SetContext(ConsumerKey, consumer)
|
||||
}
|
||||
hasRequestBody := wrapper.HasRequestBody()
|
||||
if hasRequestBody {
|
||||
_ = proxywasm.RemoveHttpRequestHeader("Content-Length")
|
||||
ctx.SetRequestBodyBufferLimit(defaultMaxBodyBytes)
|
||||
}
|
||||
|
||||
// Set user defined log & span attributes which type is fixed_value
|
||||
setAttributeBySource(ctx, config, FixedValue, nil, log)
|
||||
|
||||
@@ -8,7 +8,7 @@ replace amap-tools => ../amap-tools
|
||||
|
||||
require (
|
||||
amap-tools v0.0.0-00010101000000-000000000000
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250423015849-23258157a406
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250507130917-ed12a186173a
|
||||
quark-search v0.0.0-00010101000000-000000000000
|
||||
)
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ 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.20250423015849-23258157a406 h1:pWZsjfarQyUPlzJ9CMy4C5iHl0jb2jntscd1wCGwGB0=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250423015849-23258157a406/go.mod h1:yObZXF1xTx/8peEsSbtHIzz7KlTr/tZCrokIHtwF0Jk=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250507130917-ed12a186173a h1:CvTkMBU9+SGIyJEJYFEvg/esoVbLzQP9WVeoZzMHM9E=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250507130917-ed12a186173a/go.mod h1:yObZXF1xTx/8peEsSbtHIzz7KlTr/tZCrokIHtwF0Jk=
|
||||
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=
|
||||
|
||||
@@ -2,7 +2,10 @@ module amap-tools
|
||||
|
||||
go 1.24.1
|
||||
|
||||
require github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250407124215-3431eeb8d374
|
||||
require (
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250507122328-b62384cff88a
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250402062734-d50d98c305f0
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.1 // indirect
|
||||
@@ -12,8 +15,7 @@ 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-20250402062734-d50d98c305f0 // indirect
|
||||
github.com/higress-group/gjson_template v0.0.0-20250413075336-4c4161ed428b // 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,8 @@ 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.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/alibaba/higress/plugins/wasm-go v1.4.4-0.20250507122328-b62384cff88a h1:VQrtP0CR4pgIL3FGnIAb+uY3yRwaMQk2c3AT3p+LVwk=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250507122328-b62384cff88a/go.mod h1:yObZXF1xTx/8peEsSbtHIzz7KlTr/tZCrokIHtwF0Jk=
|
||||
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,8 +20,8 @@ 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/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=
|
||||
|
||||
@@ -58,11 +58,11 @@ func (t AroundSearchRequest) Call(ctx server.HttpContext, s server.Server) error
|
||||
|
||||
url := fmt.Sprintf("http://restapi.amap.com/v3/place/around?key=%s&location=%s&radius=%s&keywords=%s&source=ts_mcp", serverConfig.ApiKey, url.QueryEscape(t.Location), url.QueryEscape(t.Radius), url.QueryEscape(t.Keywords))
|
||||
return ctx.RouteCall(http.MethodGet, url,
|
||||
[][2]string{{"Accept", "application/json"}}, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
[][2]string{{"Accept", "application/json"}}, nil, func(sendDirectly bool, statusCode int, responseHeaders [][2]string, responseBody []byte) {
|
||||
if statusCode != http.StatusOK {
|
||||
utils.OnMCPToolCallError(ctx, fmt.Errorf("around search call failed, status: %d", statusCode))
|
||||
utils.OnMCPToolCallError(sendDirectly, ctx, fmt.Errorf("around search call failed, status: %d", statusCode))
|
||||
return
|
||||
}
|
||||
utils.SendMCPToolTextResult(ctx, string(responseBody))
|
||||
utils.SendMCPToolTextResult(sendDirectly, ctx, string(responseBody))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -57,11 +57,11 @@ func (t BicyclingRequest) Call(ctx server.HttpContext, s server.Server) error {
|
||||
|
||||
url := fmt.Sprintf("http://restapi.amap.com/v4/direction/bicycling?key=%s&origin=%s&destination=%s&source=ts_mcp", serverConfig.ApiKey, url.QueryEscape(t.Origin), url.QueryEscape(t.Destination))
|
||||
return ctx.RouteCall(http.MethodGet, url,
|
||||
[][2]string{{"Accept", "application/json"}}, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
[][2]string{{"Accept", "application/json"}}, nil, func(sendDirectly bool, statusCode int, responseHeaders [][2]string, responseBody []byte) {
|
||||
if statusCode != http.StatusOK {
|
||||
utils.OnMCPToolCallError(ctx, fmt.Errorf("bicycling call failed, status: %d", statusCode))
|
||||
utils.OnMCPToolCallError(sendDirectly, ctx, fmt.Errorf("bicycling call failed, status: %d", statusCode))
|
||||
return
|
||||
}
|
||||
utils.SendMCPToolTextResult(ctx, string(responseBody))
|
||||
utils.SendMCPToolTextResult(sendDirectly, ctx, string(responseBody))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -57,11 +57,11 @@ func (t DrivingRequest) Call(ctx server.HttpContext, s server.Server) error {
|
||||
|
||||
url := fmt.Sprintf("http://restapi.amap.com/v3/direction/driving?key=%s&origin=%s&destination=%s&source=ts_mcp", serverConfig.ApiKey, url.QueryEscape(t.Origin), url.QueryEscape(t.Destination))
|
||||
return ctx.RouteCall(http.MethodGet, url,
|
||||
[][2]string{{"Accept", "application/json"}}, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
[][2]string{{"Accept", "application/json"}}, nil, func(sendDirectly bool, statusCode int, responseHeaders [][2]string, responseBody []byte) {
|
||||
if statusCode != http.StatusOK {
|
||||
utils.OnMCPToolCallError(ctx, fmt.Errorf("driving call failed, status: %d", statusCode))
|
||||
utils.OnMCPToolCallError(sendDirectly, ctx, fmt.Errorf("driving call failed, status: %d", statusCode))
|
||||
return
|
||||
}
|
||||
utils.SendMCPToolTextResult(ctx, string(responseBody))
|
||||
utils.SendMCPToolTextResult(sendDirectly, ctx, string(responseBody))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -59,11 +59,11 @@ func (t TransitIntegratedRequest) Call(ctx server.HttpContext, s server.Server)
|
||||
|
||||
url := fmt.Sprintf("http://restapi.amap.com/v3/direction/transit/integrated?key=%s&origin=%s&destination=%s&city=%s&cityd=%s&source=ts_mcp", serverConfig.ApiKey, url.QueryEscape(t.Origin), url.QueryEscape(t.Destination), url.QueryEscape(t.City), url.QueryEscape(t.Cityd))
|
||||
return ctx.RouteCall(http.MethodGet, url,
|
||||
[][2]string{{"Accept", "application/json"}}, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
[][2]string{{"Accept", "application/json"}}, nil, func(sendDirectly bool, statusCode int, responseHeaders [][2]string, responseBody []byte) {
|
||||
if statusCode != http.StatusOK {
|
||||
utils.OnMCPToolCallError(ctx, fmt.Errorf("transit integrated call failed, status: %d", statusCode))
|
||||
utils.OnMCPToolCallError(sendDirectly, ctx, fmt.Errorf("transit integrated call failed, status: %d", statusCode))
|
||||
return
|
||||
}
|
||||
utils.SendMCPToolTextResult(ctx, string(responseBody))
|
||||
utils.SendMCPToolTextResult(sendDirectly, ctx, string(responseBody))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -57,11 +57,11 @@ func (t WalkingRequest) Call(ctx server.HttpContext, s server.Server) error {
|
||||
|
||||
url := fmt.Sprintf("http://restapi.amap.com/v3/direction/walking?key=%s&origin=%s&destination=%s&source=ts_mcp", serverConfig.ApiKey, url.QueryEscape(t.Origin), url.QueryEscape(t.Destination))
|
||||
return ctx.RouteCall(http.MethodGet, url,
|
||||
[][2]string{{"Accept", "application/json"}}, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
[][2]string{{"Accept", "application/json"}}, nil, func(sendDirectly bool, statusCode int, responseHeaders [][2]string, responseBody []byte) {
|
||||
if statusCode != http.StatusOK {
|
||||
utils.OnMCPToolCallError(ctx, fmt.Errorf("walking call failed, status: %d", statusCode))
|
||||
utils.OnMCPToolCallError(sendDirectly, ctx, fmt.Errorf("walking call failed, status: %d", statusCode))
|
||||
return
|
||||
}
|
||||
utils.SendMCPToolTextResult(ctx, string(responseBody))
|
||||
utils.SendMCPToolTextResult(sendDirectly, ctx, string(responseBody))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -57,11 +57,11 @@ func (t DistanceRequest) Call(ctx server.HttpContext, s server.Server) error {
|
||||
|
||||
url := fmt.Sprintf("http://restapi.amap.com/v3/distance?key=%s&origins=%s&destination=%s&type=%s&source=ts_mcp", serverConfig.ApiKey, url.QueryEscape(t.Origins), url.QueryEscape(t.Destination), url.QueryEscape(t.Type))
|
||||
return ctx.RouteCall(http.MethodGet, url,
|
||||
[][2]string{{"Accept", "application/json"}}, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
[][2]string{{"Accept", "application/json"}}, nil, func(sendDirectly bool, statusCode int, responseHeaders [][2]string, responseBody []byte) {
|
||||
if statusCode != http.StatusOK {
|
||||
utils.OnMCPToolCallError(ctx, fmt.Errorf("distance call failed, status: %d", statusCode))
|
||||
utils.OnMCPToolCallError(sendDirectly, ctx, fmt.Errorf("distance call failed, status: %d", statusCode))
|
||||
return
|
||||
}
|
||||
utils.SendMCPToolTextResult(ctx, string(responseBody))
|
||||
utils.SendMCPToolTextResult(sendDirectly, ctx, string(responseBody))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -58,11 +58,11 @@ func (t GeoRequest) Call(ctx server.HttpContext, s server.Server) error {
|
||||
apiKey := serverConfig.ApiKey
|
||||
url := fmt.Sprintf("https://restapi.amap.com/v3/geocode/geo?key=%s&address=%s&city=%s&source=ts_mcp", apiKey, url.QueryEscape(t.Address), url.QueryEscape(t.City))
|
||||
return ctx.RouteCall(http.MethodGet, url,
|
||||
[][2]string{{"Accept", "application/json"}}, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
[][2]string{{"Accept", "application/json"}}, nil, func(sendDirectly bool, statusCode int, responseHeaders [][2]string, responseBody []byte) {
|
||||
if statusCode != http.StatusOK {
|
||||
utils.OnMCPToolCallError(ctx, fmt.Errorf("geo call failed, status: %d", statusCode))
|
||||
utils.OnMCPToolCallError(sendDirectly, ctx, fmt.Errorf("geo call failed, status: %d", statusCode))
|
||||
return
|
||||
}
|
||||
utils.SendMCPToolTextResult(ctx, string(responseBody))
|
||||
utils.SendMCPToolTextResult(sendDirectly, ctx, string(responseBody))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -70,12 +70,12 @@ func (t IPLocationRequest) Call(ctx server.HttpContext, s server.Server) error {
|
||||
}
|
||||
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) {
|
||||
[][2]string{{"Accept", "application/json"}}, nil, func(sendDirectly bool, statusCode int, responseHeaders [][2]string, responseBody []byte) {
|
||||
if statusCode != http.StatusOK {
|
||||
utils.OnMCPToolCallError(ctx, fmt.Errorf("ip location call failed, status: %d", statusCode))
|
||||
utils.OnMCPToolCallError(sendDirectly, ctx, fmt.Errorf("ip location call failed, status: %d", statusCode))
|
||||
return
|
||||
}
|
||||
utils.SendMCPToolTextResult(ctx, string(responseBody))
|
||||
utils.SendMCPToolTextResult(sendDirectly, ctx, string(responseBody))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -56,11 +56,11 @@ func (t ReGeocodeRequest) Call(ctx server.HttpContext, s server.Server) error {
|
||||
|
||||
url := fmt.Sprintf("http://restapi.amap.com/v3/geocode/regeo?location=%s&key=%s&source=ts_mcp", url.QueryEscape(t.Location), serverConfig.ApiKey)
|
||||
return ctx.RouteCall(http.MethodGet, url,
|
||||
[][2]string{{"Accept", "application/json"}}, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
[][2]string{{"Accept", "application/json"}}, nil, func(sendDirectly bool, statusCode int, responseHeaders [][2]string, responseBody []byte) {
|
||||
if statusCode != http.StatusOK {
|
||||
utils.OnMCPToolCallError(ctx, fmt.Errorf("regeocode call failed, status: %d", statusCode))
|
||||
utils.OnMCPToolCallError(sendDirectly, ctx, fmt.Errorf("regeocode call failed, status: %d", statusCode))
|
||||
return
|
||||
}
|
||||
utils.SendMCPToolTextResult(ctx, string(responseBody))
|
||||
utils.SendMCPToolTextResult(sendDirectly, ctx, string(responseBody))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -56,11 +56,11 @@ func (t SearchDetailRequest) Call(ctx server.HttpContext, s server.Server) error
|
||||
|
||||
url := fmt.Sprintf("http://restapi.amap.com/v3/place/detail?id=%s&key=%s&source=ts_mcp", url.QueryEscape(t.ID), serverConfig.ApiKey)
|
||||
return ctx.RouteCall(http.MethodGet, url,
|
||||
[][2]string{{"Accept", "application/json"}}, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
[][2]string{{"Accept", "application/json"}}, nil, func(sendDirectly bool, statusCode int, responseHeaders [][2]string, responseBody []byte) {
|
||||
if statusCode != http.StatusOK {
|
||||
utils.OnMCPToolCallError(ctx, fmt.Errorf("search detail call failed, status: %d", statusCode))
|
||||
utils.OnMCPToolCallError(sendDirectly, ctx, fmt.Errorf("search detail call failed, status: %d", statusCode))
|
||||
return
|
||||
}
|
||||
utils.SendMCPToolTextResult(ctx, string(responseBody))
|
||||
utils.SendMCPToolTextResult(sendDirectly, ctx, string(responseBody))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -58,11 +58,11 @@ func (t TextSearchRequest) Call(ctx server.HttpContext, s server.Server) error {
|
||||
|
||||
url := fmt.Sprintf("http://restapi.amap.com/v3/place/text?key=%s&keywords=%s&city=%s&citylimit=%s&source=ts_mcp", serverConfig.ApiKey, url.QueryEscape(t.Keywords), url.QueryEscape(t.City), url.QueryEscape(t.Citylimit))
|
||||
return ctx.RouteCall(http.MethodGet, url,
|
||||
[][2]string{{"Accept", "application/json"}}, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
[][2]string{{"Accept", "application/json"}}, nil, func(sendDirectly bool, statusCode int, responseHeaders [][2]string, responseBody []byte) {
|
||||
if statusCode != http.StatusOK {
|
||||
utils.OnMCPToolCallError(ctx, fmt.Errorf("text search call failed, status: %d", statusCode))
|
||||
utils.OnMCPToolCallError(sendDirectly, ctx, fmt.Errorf("text search call failed, status: %d", statusCode))
|
||||
return
|
||||
}
|
||||
utils.SendMCPToolTextResult(ctx, string(responseBody))
|
||||
utils.SendMCPToolTextResult(sendDirectly, ctx, string(responseBody))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -56,11 +56,11 @@ func (t WeatherRequest) Call(ctx server.HttpContext, s server.Server) error {
|
||||
|
||||
url := fmt.Sprintf("http://restapi.amap.com/v3/weather/weatherInfo?city=%s&key=%s&source=ts_mcp&extensions=all", url.QueryEscape(t.City), serverConfig.ApiKey)
|
||||
return ctx.RouteCall(http.MethodGet, url,
|
||||
[][2]string{{"Accept", "application/json"}}, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
[][2]string{{"Accept", "application/json"}}, nil, func(sendDirectly bool, statusCode int, responseHeaders [][2]string, responseBody []byte) {
|
||||
if statusCode != http.StatusOK {
|
||||
utils.OnMCPToolCallError(ctx, fmt.Errorf("weather call failed, status: %d", statusCode))
|
||||
utils.OnMCPToolCallError(sendDirectly, ctx, fmt.Errorf("weather call failed, status: %d", statusCode))
|
||||
return
|
||||
}
|
||||
utils.SendMCPToolTextResult(ctx, string(responseBody))
|
||||
utils.SendMCPToolTextResult(sendDirectly, ctx, string(responseBody))
|
||||
})
|
||||
}
|
||||
|
||||
49
plugins/wasm-go/mcp-servers/mcp-context7/README.md
Normal file
49
plugins/wasm-go/mcp-servers/mcp-context7/README.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Context7 MCP Server
|
||||
|
||||
An implementation of the Model Context Protocol (MCP) server that integrates [Context7](https://context7.com), providing up-to-date, version-specific documentation and code examples.
|
||||
|
||||
Source Code: [https://github.com/upstash/context7](https://github.com/upstash/context7)
|
||||
|
||||
## Features
|
||||
|
||||
- Get up-to-date, version-specific documentation
|
||||
- Extract real, working code examples from source
|
||||
- Provide concise, relevant information without filler
|
||||
- Free for personal use
|
||||
- Integration with your MCP server and tools
|
||||
|
||||
## Usage Guide
|
||||
|
||||
### 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": {
|
||||
"context7": {
|
||||
"url": "https://mcp.higress.ai/mcp-context7/{generate_key}",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Available Tools
|
||||
|
||||
#### resolve-library-id
|
||||
Resolves a general package name into a Context7-compatible library ID. This is a required first step before using the get-library-docs tool.
|
||||
|
||||
Parameters:
|
||||
- query: Library name to search for and retrieve a Context7-compatible library ID (required)
|
||||
|
||||
#### get-library-docs
|
||||
Fetches up-to-date documentation for a library. You must call resolve-library-id first to obtain the exact Context7-compatible library ID.
|
||||
|
||||
Parameters:
|
||||
- folders: Folders filter for organizing documentation
|
||||
- libraryId: Unique identifier of the library (required)
|
||||
- tokens: Maximum number of tokens to return (default: 5000)
|
||||
- topic: Specific topic within the documentation
|
||||
- type: Type of documentation to retrieve (currently only "txt" supported)
|
||||
49
plugins/wasm-go/mcp-servers/mcp-context7/README_ZH.md
Normal file
49
plugins/wasm-go/mcp-servers/mcp-context7/README_ZH.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Context7 MCP Server
|
||||
|
||||
一个集成了[Context7](https://context7.com)的模型上下文协议(MCP)服务器实现,提供最新、版本特定的文档和代码示例。
|
||||
|
||||
源码地址:[https://github.com/upstash/context7](https://github.com/upstash/context7)
|
||||
|
||||
## 功能
|
||||
|
||||
- 获取最新、版本特定的文档
|
||||
- 从源码中提取真实可用的代码示例
|
||||
- 提供简洁、相关的信息,无冗余内容
|
||||
- 支持个人免费使用
|
||||
- 与MCP服务器和工具集成
|
||||
|
||||
## 使用教程
|
||||
|
||||
### 生成 SSE URL
|
||||
|
||||
在 MCP Server 界面,登录后输入 API-KEY,生成URL。
|
||||
|
||||
### 配置 MCP Client
|
||||
|
||||
在用户的 MCP Client 界面,将生成的 SSE URL添加到 MCP Server列表中。
|
||||
|
||||
```json
|
||||
"mcpServers": {
|
||||
"context7": {
|
||||
"url": "https://mcp.higress.ai/mcp-context7/{generate_key}",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 可用工具
|
||||
|
||||
#### resolve-library-id
|
||||
用于将通用包名解析为Context7兼容的库ID,是使用get-library-docs工具获取文档的必要前置步骤。
|
||||
|
||||
参数说明:
|
||||
- query: 要搜索的库名称,用于获取Context7兼容的库ID (必填)
|
||||
|
||||
#### get-library-docs
|
||||
获取库的最新文档。使用前必须先调用resolve-library-id工具获取Context7兼容的库ID。
|
||||
|
||||
参数说明:
|
||||
- folders: 用于组织文档的文件夹过滤器
|
||||
- libraryId: 库的唯一标识符 (必填)
|
||||
- tokens: 返回的最大token数,默认5000
|
||||
- topic: 文档中的特定主题
|
||||
- type: 要检索的文档类型,目前仅支持"txt"
|
||||
55
plugins/wasm-go/mcp-servers/mcp-context7/mcp-server.yaml
Normal file
55
plugins/wasm-go/mcp-servers/mcp-context7/mcp-server.yaml
Normal file
@@ -0,0 +1,55 @@
|
||||
server:
|
||||
name: context7-mcp-server
|
||||
tools:
|
||||
- name: resolve-library-id
|
||||
description: Required first step - Resolves a general package name into a Context7-compatible library ID. Must be called before using 'get-library-docs' to retrieve a valid Context7-compatible library ID.
|
||||
args:
|
||||
- name: query
|
||||
description: Library name to search for and retrieve a Context7-compatible library ID.
|
||||
type: string
|
||||
required: true
|
||||
position: query
|
||||
requestTemplate:
|
||||
url: https://context7.com/api/v1/search
|
||||
method: GET
|
||||
responseTemplate:
|
||||
body: |
|
||||
{{- range $index, $item := .results }}
|
||||
## 结果 {{add $index 1}}
|
||||
- **id**: {{ $item.id }}
|
||||
- **title**: {{ $item.title }}
|
||||
- **description**: {{ $item.description }}
|
||||
{{- end }}
|
||||
- name: get-library-docs
|
||||
description: Fetches up-to-date documentation for a library. You must call 'resolve-library-id' first to obtain the exact Context7-compatible library ID required to use this tool.
|
||||
args:
|
||||
- name: folders
|
||||
description: Folders filter for organizing documentation
|
||||
type: string
|
||||
position: query
|
||||
- name: libraryId
|
||||
description: Unique identifier of the library
|
||||
type: string
|
||||
required: true
|
||||
position: path
|
||||
- name: tokens
|
||||
description: Maximum number of tokens to return
|
||||
type: integer
|
||||
position: query
|
||||
default: 5000
|
||||
- name: topic
|
||||
description: Specific topic within the documentation
|
||||
type: string
|
||||
position: query
|
||||
- name: type
|
||||
description: Type of documentation to retrieve
|
||||
type: string
|
||||
position: query
|
||||
enum: ["txt"]
|
||||
requestTemplate:
|
||||
url: https://context7.com/api/v1{libraryId}
|
||||
method: GET
|
||||
headers:
|
||||
- key: X-Context7-Source
|
||||
value: server
|
||||
|
||||
@@ -3,7 +3,7 @@ module quark-search
|
||||
go 1.24.1
|
||||
|
||||
require (
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250407124215-3431eeb8d374
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250507122328-b62384cff88a
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ 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/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
|
||||
|
||||
@@ -6,8 +6,8 @@ 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.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/alibaba/higress/plugins/wasm-go v1.4.4-0.20250507122328-b62384cff88a h1:VQrtP0CR4pgIL3FGnIAb+uY3yRwaMQk2c3AT3p+LVwk=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250507122328-b62384cff88a/go.mod h1:yObZXF1xTx/8peEsSbtHIzz7KlTr/tZCrokIHtwF0Jk=
|
||||
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,8 +20,8 @@ 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/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=
|
||||
|
||||
@@ -99,9 +99,9 @@ func (t WebSearch) Call(ctx server.HttpContext, s server.Server) error {
|
||||
}
|
||||
return ctx.RouteCall(http.MethodGet, fmt.Sprintf("https://cloud-iqs.aliyuncs.com/search/genericSearch?query=%s", url.QueryEscape(t.Query)),
|
||||
[][2]string{{"Accept", "application/json"},
|
||||
{"X-API-Key", serverConfig.ApiKey}}, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
{"X-API-Key", serverConfig.ApiKey}}, nil, func(sendDirectly bool, statusCode int, responseHeaders [][2]string, responseBody []byte) {
|
||||
if statusCode != http.StatusOK {
|
||||
utils.OnMCPToolCallError(ctx, fmt.Errorf("quark search call failed, status: %d", statusCode))
|
||||
utils.OnMCPToolCallError(sendDirectly, ctx, fmt.Errorf("quark search call failed, status: %d", statusCode))
|
||||
return
|
||||
}
|
||||
jsonObj := gjson.ParseBytes(responseBody)
|
||||
@@ -125,6 +125,6 @@ func (t WebSearch) Call(ctx server.HttpContext, s server.Server) error {
|
||||
results = append(results, result.Format())
|
||||
}
|
||||
}
|
||||
utils.SendMCPToolTextResult(ctx, fmt.Sprintf("# Search Results\n\n%s", strings.Join(results, "\n\n")))
|
||||
utils.SendMCPToolTextResult(sendDirectly, ctx, fmt.Sprintf("# Search Results\n\n%s", strings.Join(results, "\n\n")))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ type ToolArgs struct {
|
||||
Required bool `json:"required,omitempty"`
|
||||
Default interface{} `json:"default,omitempty"`
|
||||
Enum []interface{} `json:"enum,omitempty"`
|
||||
Items []interface{} `json:"items,omitempty"`
|
||||
Items interface{} `json:"items,omitempty"`
|
||||
Properties interface{} `json:"properties,omitempty"`
|
||||
Position string `json:"position,omitempty"`
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ type watcher struct {
|
||||
nacosClientConfig *constant.ClientConfig
|
||||
namespace string
|
||||
clusterId string
|
||||
authOption provider.AuthOption
|
||||
}
|
||||
|
||||
type WatcherOption func(w *watcher)
|
||||
@@ -111,9 +112,10 @@ func NewWatcher(cache memory.Cache, opts ...WatcherOption) (provider.Watcher, er
|
||||
opt(w)
|
||||
}
|
||||
|
||||
if w.NacosNamespace == "" {
|
||||
w.NacosNamespace = w.NacosNamespaceId
|
||||
}
|
||||
// The nacos mcp server uses these restricted namespaces and groups, and may be adjusted in the future.
|
||||
w.NacosNamespace = "nacos-default-mcp"
|
||||
w.NacosNamespaceId = w.NacosNamespace
|
||||
w.NacosGroups = []string{"mcp-server"}
|
||||
|
||||
mcpServerLog.Infof("new nacos mcp server watcher with config Name:%s", w.Name)
|
||||
|
||||
@@ -130,6 +132,8 @@ func NewWatcher(cache memory.Cache, opts ...WatcherOption) (provider.Watcher, er
|
||||
constant.WithNamespaceId(w.NacosNamespaceId),
|
||||
constant.WithAccessKey(w.NacosAccessKey),
|
||||
constant.WithSecretKey(w.NacosSecretKey),
|
||||
constant.WithUsername(w.authOption.NacosUsername),
|
||||
constant.WithPassword(w.authOption.NacosPassword),
|
||||
)
|
||||
|
||||
initTimer := time.NewTimer(DefaultInitTimeout)
|
||||
@@ -177,32 +181,6 @@ func WithNacosSecretKey(nacosSecretKey string) WatcherOption {
|
||||
}
|
||||
}
|
||||
|
||||
func WithNacosNamespaceId(nacosNamespaceId string) WatcherOption {
|
||||
return func(w *watcher) {
|
||||
if nacosNamespaceId == "" {
|
||||
w.NacosNamespaceId = "nacos-default-mcp"
|
||||
} else {
|
||||
w.NacosNamespaceId = nacosNamespaceId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func WithNacosNamespace(nacosNamespace string) WatcherOption {
|
||||
return func(w *watcher) {
|
||||
w.NacosNamespace = nacosNamespace
|
||||
}
|
||||
}
|
||||
|
||||
func WithNacosGroups(nacosGroups []string) WatcherOption {
|
||||
return func(w *watcher) {
|
||||
if len(nacosGroups) == 0 {
|
||||
w.NacosGroups = []string{"mcp-server"}
|
||||
} else {
|
||||
w.NacosGroups = nacosGroups
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func WithNacosRefreshInterval(refreshInterval int64) WatcherOption {
|
||||
return func(w *watcher) {
|
||||
if refreshInterval < int64(DefaultRefreshIntervalLimit) {
|
||||
@@ -266,6 +244,12 @@ func WithClusterId(id string) WatcherOption {
|
||||
}
|
||||
}
|
||||
|
||||
func WithAuthOption(authOption provider.AuthOption) WatcherOption {
|
||||
return func(w *watcher) {
|
||||
w.authOption = authOption
|
||||
}
|
||||
}
|
||||
|
||||
func (w *watcher) Run() {
|
||||
ticker := time.NewTicker(time.Duration(w.NacosRefreshInterval))
|
||||
defer ticker.Stop()
|
||||
@@ -658,6 +642,8 @@ func (w *watcher) buildServiceEntryForMcpServer(mcpServer *provider.McpServer, c
|
||||
constant.WithNamespaceId(serviceNamespace),
|
||||
constant.WithAccessKey(w.NacosAccessKey),
|
||||
constant.WithSecretKey(w.NacosSecretKey),
|
||||
constant.WithUsername(w.authOption.NacosUsername),
|
||||
constant.WithPassword(w.authOption.NacosPassword),
|
||||
)
|
||||
client, err := clients.NewNamingClient(vo.NacosClientParam{
|
||||
ClientConfig: namingConfig,
|
||||
@@ -686,21 +672,24 @@ func (w *watcher) getServiceCallback(server *provider.McpServer, configGroup, da
|
||||
serviceName := server.RemoteServerConfig.ServiceRef.ServiceName
|
||||
path := server.RemoteServerConfig.ExportPath
|
||||
protocol := server.Protocol
|
||||
host := getNacosServiceFullHost(groupName, namespace, serviceName)
|
||||
|
||||
return func(services []model.Instance) {
|
||||
defer w.UpdateService()
|
||||
|
||||
configKey := strings.Join([]string{w.Name, w.NacosNamespace, configGroup, dataId}, DefaultJoiner)
|
||||
|
||||
host := getNacosServiceFullHost(groupName, namespace, serviceName)
|
||||
mcpServerLog.Infof("callback for %s/%s, serviceName : %s", configGroup, dataId, host)
|
||||
configKey := strings.Join([]string{w.Name, w.NacosNamespace, configGroup, dataId}, DefaultJoiner)
|
||||
if len(services) == 0 {
|
||||
mcpServerLog.Errorf("callback for %s return empty service instance list, skip generate config", host)
|
||||
return
|
||||
}
|
||||
|
||||
serviceEntry := w.generateServiceEntry(host, services)
|
||||
se := &config.Config{
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.ServiceEntry,
|
||||
Name: fmt.Sprintf("%s-%s-%s", provider.IstioMcpAutoGeneratedSeName, configGroup, strings.TrimSuffix(dataId, ".json")),
|
||||
Namespace: w.namespace,
|
||||
Namespace: "mcp",
|
||||
},
|
||||
Spec: serviceEntry,
|
||||
}
|
||||
@@ -717,12 +706,12 @@ func (w *watcher) getServiceCallback(server *provider.McpServer, configGroup, da
|
||||
w.cache.UpdateConfigCache(gvk.DestinationRule, configKey, dr, false)
|
||||
}
|
||||
w.cache.UpdateConfigCache(gvk.ServiceEntry, configKey, se, false)
|
||||
vs := w.buildVirtualServiceForMcpServer(serviceEntry, configGroup, dataId, path, server.Name)
|
||||
vs := w.buildVirtualServiceForMcpServer(serviceEntry, configGroup, dataId, path, server)
|
||||
w.cache.UpdateConfigCache(gvk.VirtualService, configKey, vs, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *watcher) buildVirtualServiceForMcpServer(serviceentry *v1alpha3.ServiceEntry, group, dataId, path, serverName string) *config.Config {
|
||||
func (w *watcher) buildVirtualServiceForMcpServer(serviceentry *v1alpha3.ServiceEntry, group, dataId, path string, server *provider.McpServer) *config.Config {
|
||||
if serviceentry == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -739,7 +728,7 @@ func (w *watcher) buildVirtualServiceForMcpServer(serviceentry *v1alpha3.Service
|
||||
common2.CreateConvertedName(constants.IstioIngressGatewayName, cleanHost))
|
||||
}
|
||||
routeName := fmt.Sprintf("%s-%s-%s", provider.IstioMcpAutoGeneratedHttpRouteName, group, strings.TrimSuffix(dataId, ".json"))
|
||||
mergePath := "/" + serverName
|
||||
mergePath := "/" + server.Name
|
||||
if w.McpServerBaseUrl != "/" {
|
||||
mergePath = strings.TrimSuffix(w.McpServerBaseUrl, "/") + mergePath
|
||||
}
|
||||
@@ -759,9 +748,6 @@ func (w *watcher) buildVirtualServiceForMcpServer(serviceentry *v1alpha3.Service
|
||||
},
|
||||
},
|
||||
}},
|
||||
Rewrite: &v1alpha3.HTTPRewrite{
|
||||
Uri: path,
|
||||
},
|
||||
Route: []*v1alpha3.HTTPRouteDestination{{
|
||||
Destination: &v1alpha3.Destination{
|
||||
Host: serviceentry.Hosts[0],
|
||||
@@ -773,6 +759,12 @@ func (w *watcher) buildVirtualServiceForMcpServer(serviceentry *v1alpha3.Service
|
||||
}},
|
||||
}
|
||||
|
||||
if server.Protocol == provider.McpStreambleProtocol {
|
||||
vs.Http[0].Rewrite = &v1alpha3.HTTPRewrite{
|
||||
Uri: path,
|
||||
}
|
||||
}
|
||||
|
||||
mcpServerLog.Debugf("construct virtualservice %v", vs)
|
||||
|
||||
return &config.Config{
|
||||
@@ -964,7 +956,8 @@ func (w *watcher) Stop() {
|
||||
}
|
||||
mcpServerLog.Infof("stop all service nameing client")
|
||||
for _, client := range w.serviceCache {
|
||||
client.Stop()
|
||||
// TODO: This is a temporary implementation because of a bug in the nacos-go-sdk, which causes a block when stoping.
|
||||
go client.Stop()
|
||||
}
|
||||
|
||||
w.isStop = true
|
||||
|
||||
@@ -187,25 +187,41 @@ func (r *Reconciler) generateWatcherFromRegistryConfig(registry *apiv1.RegistryC
|
||||
nacosv2.WithAuthOption(authOption),
|
||||
)
|
||||
case string(Nacos3):
|
||||
watcher, err = mcpserver.NewWatcher(
|
||||
r.Cache,
|
||||
mcpserver.WithType(registry.Type),
|
||||
mcpserver.WithName(registry.Name),
|
||||
mcpserver.WithNacosAddressServer(registry.NacosAddressServer),
|
||||
mcpserver.WithDomain(registry.Domain),
|
||||
mcpserver.WithPort(registry.Port),
|
||||
mcpserver.WithNacosAccessKey(registry.NacosAccessKey),
|
||||
mcpserver.WithNacosSecretKey(registry.NacosSecretKey),
|
||||
mcpserver.WithNacosNamespaceId(registry.NacosNamespaceId),
|
||||
mcpserver.WithNacosNamespace(registry.NacosNamespace),
|
||||
mcpserver.WithNacosGroups(registry.NacosGroups),
|
||||
mcpserver.WithNacosRefreshInterval(registry.NacosRefreshInterval),
|
||||
mcpserver.WithMcpExportDomains(registry.McpServerExportDomains),
|
||||
mcpserver.WithMcpBaseUrl(registry.McpServerBaseUrl),
|
||||
mcpserver.WithEnableMcpServer(registry.EnableMCPServer),
|
||||
mcpserver.WithClusterId(r.clusterId),
|
||||
mcpserver.WithNamespace(r.namespace),
|
||||
)
|
||||
if registry.EnableMCPServer.GetValue() {
|
||||
watcher, err = mcpserver.NewWatcher(
|
||||
r.Cache,
|
||||
mcpserver.WithType(registry.Type),
|
||||
mcpserver.WithName(registry.Name),
|
||||
mcpserver.WithNacosAddressServer(registry.NacosAddressServer),
|
||||
mcpserver.WithDomain(registry.Domain),
|
||||
mcpserver.WithPort(registry.Port),
|
||||
mcpserver.WithNacosAccessKey(registry.NacosAccessKey),
|
||||
mcpserver.WithNacosSecretKey(registry.NacosSecretKey),
|
||||
mcpserver.WithNacosRefreshInterval(registry.NacosRefreshInterval),
|
||||
mcpserver.WithMcpExportDomains(registry.McpServerExportDomains),
|
||||
mcpserver.WithMcpBaseUrl(registry.McpServerBaseUrl),
|
||||
mcpserver.WithEnableMcpServer(registry.EnableMCPServer),
|
||||
mcpserver.WithClusterId(r.clusterId),
|
||||
mcpserver.WithNamespace(r.namespace),
|
||||
mcpserver.WithAuthOption(authOption),
|
||||
)
|
||||
} else {
|
||||
watcher, err = nacosv2.NewWatcher(
|
||||
r.Cache,
|
||||
nacosv2.WithType(registry.Type),
|
||||
nacosv2.WithName(registry.Name),
|
||||
nacosv2.WithNacosAddressServer(registry.NacosAddressServer),
|
||||
nacosv2.WithDomain(registry.Domain),
|
||||
nacosv2.WithPort(registry.Port),
|
||||
nacosv2.WithNacosAccessKey(registry.NacosAccessKey),
|
||||
nacosv2.WithNacosSecretKey(registry.NacosSecretKey),
|
||||
nacosv2.WithNacosNamespaceId(registry.NacosNamespaceId),
|
||||
nacosv2.WithNacosNamespace(registry.NacosNamespace),
|
||||
nacosv2.WithNacosGroups(registry.NacosGroups),
|
||||
nacosv2.WithNacosRefreshInterval(registry.NacosRefreshInterval),
|
||||
nacosv2.WithAuthOption(authOption),
|
||||
)
|
||||
}
|
||||
case string(Zookeeper):
|
||||
watcher, err = zookeeper.NewWatcher(
|
||||
r.Cache,
|
||||
|
||||
Reference in New Issue
Block a user