mirror of
https://github.com/alibaba/higress.git
synced 2026-02-26 05:30:50 +08:00
Compare commits
59 Commits
v2.1.5-rc.
...
v2.1.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d98f8b8b21 | ||
|
|
bd19a5049b | ||
|
|
1070541f1d | ||
|
|
32b5c89c17 | ||
|
|
bd1101d711 | ||
|
|
27680223b9 | ||
|
|
93ea5e7355 | ||
|
|
ff9a29c5d9 | ||
|
|
6a1557f6ac | ||
|
|
e6e4193679 | ||
|
|
978d0afb63 | ||
|
|
39dd4538c9 | ||
|
|
f826d79109 | ||
|
|
7348c265b5 | ||
|
|
ea0bf7c1b7 | ||
|
|
ba1bf353b8 | ||
|
|
b56097e647 | ||
|
|
5b97b849b5 | ||
|
|
331fe57c70 | ||
|
|
4d32cc9468 | ||
|
|
34b5a6feea | ||
|
|
8736edaf61 | ||
|
|
30d5b4d32e | ||
|
|
c0133378a7 | ||
|
|
8346b4a4a2 | ||
|
|
ce271849de | ||
|
|
bdc3ecab71 | ||
|
|
9214dca078 | ||
|
|
c3eb8d0447 | ||
|
|
081ab6ee8d | ||
|
|
9a45f07972 | ||
|
|
da2ae4c7ee | ||
|
|
ff068258a1 | ||
|
|
0996ad21b1 | ||
|
|
45eb76d4cc | ||
|
|
36bcb595d6 | ||
|
|
783a8db512 | ||
|
|
44566f5259 | ||
|
|
73ba9238bd | ||
|
|
41a1455874 | ||
|
|
9d68ccbf35 | ||
|
|
db7dbb24a2 | ||
|
|
9a0cf9b762 | ||
|
|
bb786c9618 | ||
|
|
ef49d2f5f6 | ||
|
|
864bf5af39 | ||
|
|
527e922d50 | ||
|
|
1fe5eb6e13 | ||
|
|
87185baff2 | ||
|
|
76ada0b844 | ||
|
|
f4d3fec228 | ||
|
|
e94ac43dd1 | ||
|
|
dd29267fd7 | ||
|
|
01a9161153 | ||
|
|
ceb8b557dc | ||
|
|
753022e093 | ||
|
|
04cbbfc7e8 | ||
|
|
db66df39c4 | ||
|
|
dad6278a6d |
@@ -3,22 +3,22 @@ name: Build and Push Wasm Plugin Image
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "wasm-*-*-v*.*.*" # 匹配 wasm-{go|rust}-{pluginName}-vX.Y.Z 格式的标签
|
||||
- "wasm-*-*-v*.*.*" # 匹配 wasm-{go|rust}-{pluginName}-vX.Y.Z 格式的标签
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
plugin_type:
|
||||
description: 'Type of the plugin'
|
||||
description: "Type of the plugin"
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- go
|
||||
- rust
|
||||
plugin_name:
|
||||
description: 'Name of the plugin'
|
||||
description: "Name of the plugin"
|
||||
required: true
|
||||
type: string
|
||||
version:
|
||||
description: 'Version of the plugin (optional, without leading v)'
|
||||
description: "Version of the plugin (optional, without leading v)"
|
||||
required: false
|
||||
type: string
|
||||
|
||||
@@ -31,8 +31,7 @@ jobs:
|
||||
IMAGE_REGISTRY_SERVICE: ${{ vars.IMAGE_REGISTRY || 'higress-registry.cn-hangzhou.cr.aliyuncs.com' }}
|
||||
IMAGE_REPOSITORY: ${{ vars.PLUGIN_IMAGE_REPOSITORY || 'plugins' }}
|
||||
RUST_VERSION: 1.82
|
||||
GO_VERSION: 1.19
|
||||
TINYGO_VERSION: 0.28.1
|
||||
GO_VERSION: 1.24.0
|
||||
ORAS_VERSION: 1.0.0
|
||||
steps:
|
||||
- name: Set plugin_type, plugin_name and version from inputs or ref_name
|
||||
@@ -53,7 +52,7 @@ jobs:
|
||||
if [[ "$plugin_type" == "rust" ]]; then
|
||||
builder_image="higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/wasm-rust-builder:rust${{ env.RUST_VERSION }}-oras${{ env.ORAS_VERSION }}"
|
||||
else
|
||||
builder_image="higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/wasm-go-builder:go${{ env.GO_VERSION }}-tinygo${{ env.TINYGO_VERSION }}-oras${{ env.ORAS_VERSION }}"
|
||||
builder_image="higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/wasm-go-builder:go${{ env.GO_VERSION }}-oras${{ env.ORAS_VERSION }}"
|
||||
fi
|
||||
echo "PLUGIN_TYPE=$plugin_type" >> $GITHUB_ENV
|
||||
echo "PLUGIN_NAME=$plugin_name" >> $GITHUB_ENV
|
||||
@@ -62,9 +61,9 @@ jobs:
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
|
||||
- name: File Check
|
||||
run: |
|
||||
run: |
|
||||
workspace=${{ github.workspace }}/plugins/wasm-${PLUGIN_TYPE}/extensions/${PLUGIN_NAME}
|
||||
push_command="./plugin.tar.gz:application/vnd.oci.image.layer.v1.tar+gzip"
|
||||
|
||||
@@ -79,7 +78,7 @@ jobs:
|
||||
echo "README.md exists"
|
||||
push_command="./README.md:application/vnd.module.wasm.doc.v1+markdown $push_command "
|
||||
fi
|
||||
|
||||
|
||||
# 查找README_{lang}.md
|
||||
for file in ${workspace}/README_*.md; do
|
||||
if [ -f "$file" ]; then
|
||||
@@ -91,9 +90,9 @@ jobs:
|
||||
done
|
||||
|
||||
echo "PUSH_COMMAND=\"$push_command\"" >> $GITHUB_ENV
|
||||
|
||||
|
||||
- name: Run a wasm-builder
|
||||
env:
|
||||
env:
|
||||
PLUGIN_NAME: ${{ env.PLUGIN_NAME }}
|
||||
BUILDER_IMAGE: ${{ env.BUILDER_IMAGE }}
|
||||
run: |
|
||||
@@ -104,7 +103,7 @@ jobs:
|
||||
push_command=${{ env.PUSH_COMMAND }}
|
||||
push_command=${push_command#\"}
|
||||
push_command=${push_command%\"} # 删除PUSH_COMMAND中的双引号,确保oras push正常解析
|
||||
|
||||
|
||||
target_image="${{ env.IMAGE_REGISTRY_SERVICE }}/${{ env.IMAGE_REPOSITORY}}/${{ env.PLUGIN_NAME }}:${{ env.VERSION }}"
|
||||
target_image_latest="${{ env.IMAGE_REGISTRY_SERVICE }}/${{ env.IMAGE_REPOSITORY}}/${{ env.PLUGIN_NAME }}:latest"
|
||||
echo "TargetImage=${target_image}"
|
||||
@@ -123,7 +122,7 @@ jobs:
|
||||
set -e
|
||||
cd /workspace/plugins/wasm-go/extensions/${PLUGIN_NAME}
|
||||
go mod tidy
|
||||
tinygo build -o ./plugin.wasm -scheduler=none -target=wasi -gc=custom -tags=\"custommalloc nottinygc_finalizer ${EXTRA_TAGS}\" .
|
||||
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o plugin.wasm main.go
|
||||
tar czvf plugin.tar.gz plugin.wasm
|
||||
echo ${{ secrets.REGISTRY_PASSWORD }} | oras login -u ${{ secrets.REGISTRY_USERNAME }} --password-stdin ${{ env.IMAGE_REGISTRY_SERVICE }}
|
||||
oras push ${target_image} ${push_command}
|
||||
|
||||
32
.github/workflows/build-and-test-plugin.yaml
vendored
32
.github/workflows/build-and-test-plugin.yaml
vendored
@@ -2,20 +2,20 @@ name: "Build and Test Plugins"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'plugins/**'
|
||||
- 'test/**'
|
||||
- 'helm/**'
|
||||
- 'Makefile.core.mk'
|
||||
- "plugins/**"
|
||||
- "test/**"
|
||||
- "helm/**"
|
||||
- "Makefile.core.mk"
|
||||
pull_request:
|
||||
branches: [ "*" ]
|
||||
branches: ["*"]
|
||||
paths:
|
||||
- 'plugins/**'
|
||||
- 'test/**'
|
||||
- 'helm/**'
|
||||
- 'Makefile.core.mk'
|
||||
workflow_dispatch: ~
|
||||
- "plugins/**"
|
||||
- "test/**"
|
||||
- "helm/**"
|
||||
- "Makefile.core.mk"
|
||||
workflow_dispatch: ~
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.22
|
||||
go-version: 1.24
|
||||
# There are too many lint errors in current code bases
|
||||
# uncomment when we decide what lint should be addressed or ignored.
|
||||
# - run: make lint
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
# TODO(Xunzhuo): Enable C WASM Filters in CI
|
||||
wasmPluginType: [ GO, RUST ]
|
||||
wasmPluginType: [GO, RUST]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -46,12 +46,12 @@ jobs:
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
swap-storage: true
|
||||
swap-storage: true
|
||||
|
||||
- name: "Setup Go"
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.22
|
||||
go-version: 1.24
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
@@ -80,6 +80,6 @@ jobs:
|
||||
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ higress-wasmplugin-test ]
|
||||
needs: [higress-wasmplugin-test]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
echo "Version=$VERSION"
|
||||
# Step 3
|
||||
- name: Upload to OSS
|
||||
uses: doggycool/ossutil-github-action@master
|
||||
uses: go-choppy/ossutil-github-action@master
|
||||
with:
|
||||
ossArgs: 'cp -r -u ./artifact/ oss://higress-website-cn-hongkong/standalone/'
|
||||
accessKey: ${{ secrets.ACCESS_KEYID }}
|
||||
|
||||
4
.github/workflows/deploy-to-oss.yaml
vendored
4
.github/workflows/deploy-to-oss.yaml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
# Step 2
|
||||
- name: Download Helm Charts Index
|
||||
uses: doggycool/ossutil-github-action@master
|
||||
uses: go-choppy/ossutil-github-action@master
|
||||
with:
|
||||
ossArgs: 'cp oss://higress-website-cn-hongkong/helm-charts/index.yaml ./artifact/'
|
||||
accessKey: ${{ secrets.ACCESS_KEYID }}
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
sed -i 's/higress\.io/higress\.cn/g' ./artifact/cn-index.yaml
|
||||
# Step 5
|
||||
- name: Upload to OSS
|
||||
uses: doggycool/ossutil-github-action@master
|
||||
uses: go-choppy/ossutil-github-action@master
|
||||
with:
|
||||
ossArgs: 'cp -r -u ./artifact/ oss://higress-website-cn-hongkong/helm-charts/'
|
||||
accessKey: ${{ secrets.ACCESS_KEYID }}
|
||||
|
||||
217
.github/workflows/generate-release-notes.yaml
vendored
Normal file
217
.github/workflows/generate-release-notes.yaml
vendored
Normal file
@@ -0,0 +1,217 @@
|
||||
name: Generate Release Notes
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
workflow_dispatch: ~
|
||||
|
||||
jobs:
|
||||
generate-release-notes:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DASHSCOPE_API_KEY: ${{ secrets.HIGRESS_OPENAI_API_KEY }}
|
||||
MODEL_NAME: ${{ secrets.HIGRESS_OPENAI_API_MODEL }}
|
||||
MODEL_SERVER: ${{ secrets.MODEL_SERVER }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.24
|
||||
|
||||
- name: Clone GitHub MCP Server
|
||||
run: |
|
||||
git clone https://github.com/github/github-mcp-server.git
|
||||
cd github-mcp-server
|
||||
go build -o ../github-mcp-serve ./cmd/github-mcp-server
|
||||
cd ..
|
||||
chmod u+x github-mcp-serve
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Clone Higress Report Agent
|
||||
run: |
|
||||
git clone https://github.com/higress-group/higress-report-agent.git
|
||||
mv github-mcp-serve higress-report-agent/
|
||||
|
||||
- name: Clean up old release notes
|
||||
run: |
|
||||
RELEASE_VERSION=$(cat ${GITHUB_WORKSPACE}/VERSION)
|
||||
CLEAN_VERSION=${RELEASE_VERSION#v}
|
||||
if [ -d "release-notes/${CLEAN_VERSION}" ]; then
|
||||
echo "Removing old release notes directory: release-notes/${CLEAN_VERSION}"
|
||||
rm -rf release-notes/${CLEAN_VERSION}
|
||||
else
|
||||
echo "No old release notes directory found for version ${CLEAN_VERSION}."
|
||||
fi
|
||||
|
||||
- name: Create Release Report Script
|
||||
run: |
|
||||
cat > generate_release_report.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
# Script to generate release notes for Higress projects
|
||||
|
||||
echo "Fetching GitHub generated release notes for ${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}..."
|
||||
curl -L \
|
||||
"https://github.com/${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}/releases/tag/${RELEASE_VERSION}" \
|
||||
-o release_page.html
|
||||
|
||||
echo "Extracting PR numbers from ${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME} release notes..."
|
||||
PR_NUMS=$(cat release_page.html | grep -o "/${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}/pull/[0-9]*" | grep -o "[0-9]*$" | sort -n | uniq | tr '\n' ',')
|
||||
PR_NUMS=${PR_NUMS%,}
|
||||
if [ -z "${PR_NUMS}" ]; then
|
||||
echo "No PR numbers found in release notes for ${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME} tag=${RELEASE_VERSION}."
|
||||
rm release_page.html
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Identifying important PRs..."
|
||||
IMPORTANT_PR_NUMS=$(cat release_page.html | grep -o "<strong>.*/${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}/pull/[0-9]*.*</strong>" | grep -o "pull/[0-9]*" | grep -o "[0-9]*" | sort -n | uniq | tr '\n' ',')
|
||||
IMPORTANT_PR_NUMS=${IMPORTANT_PR_NUMS%,}
|
||||
|
||||
rm release_page.html
|
||||
|
||||
echo "Extracted PR numbers for ${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}: ${PR_NUMS}"
|
||||
echo "Important PR numbers: ${IMPORTANT_PR_NUMS}"
|
||||
|
||||
echo "Generating detailed release notes for ${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}..."
|
||||
cd higress-report-agent
|
||||
pip install uv
|
||||
uv sync
|
||||
if [ -n "${IMPORTANT_PR_NUMS}" ]; then
|
||||
uv run report_main.py --mode 2 --choice 2 --pr_nums ${PR_NUMS} --important_prs ${IMPORTANT_PR_NUMS}
|
||||
else
|
||||
uv run report_main.py --mode 2 --choice 2 --pr_nums ${PR_NUMS}
|
||||
fi
|
||||
cp report.md ../
|
||||
cp report.EN.md ../
|
||||
cd ..
|
||||
|
||||
# 去除主库版本号前缀v,以主库版本号为路径
|
||||
CLEAN_VERSION=${MAIN_RELEASE_VERSION#v}
|
||||
|
||||
echo "Creating release notes directory for main version ${MAIN_RELEASE_VERSION}..."
|
||||
mkdir -p release-notes/${CLEAN_VERSION}
|
||||
|
||||
echo "# ${REPORT_TITLE}" >>release-notes/${CLEAN_VERSION}/README_ZH.md
|
||||
sed 's/# Release Notes//' report.md >>release-notes/${CLEAN_VERSION}/README_ZH.md
|
||||
echo -e "\n" >>release-notes/${CLEAN_VERSION}/README_ZH.md
|
||||
|
||||
echo "# ${REPORT_TITLE}" >>release-notes/${CLEAN_VERSION}/README.md
|
||||
sed 's/# Release Notes//' report.EN.md >>release-notes/${CLEAN_VERSION}/README.md
|
||||
echo -e "\n" >>release-notes/${CLEAN_VERSION}/README.md
|
||||
|
||||
rm report.md
|
||||
rm report.EN.md
|
||||
echo "${REPORT_TITLE} release notes saved to release-notes/${CLEAN_VERSION}/"
|
||||
|
||||
EOF
|
||||
chmod +x generate_release_report.sh
|
||||
|
||||
- name: Generate Release Notes for Higress
|
||||
env:
|
||||
GITHUB_REPO_OWNER: alibaba
|
||||
GITHUB_REPO_NAME: higress
|
||||
GITHUB_PERSONAL_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
REPORT_TITLE: Higress
|
||||
run: |
|
||||
export MAIN_RELEASE_VERSION=$(cat ${GITHUB_WORKSPACE}/VERSION)
|
||||
export RELEASE_VERSION=$(cat ${GITHUB_WORKSPACE}/VERSION)
|
||||
bash generate_release_report.sh
|
||||
|
||||
- name: Generate Release Notes for Higress Console
|
||||
env:
|
||||
GITHUB_REPO_OWNER: higress-group
|
||||
GITHUB_REPO_NAME: higress-console
|
||||
GITHUB_PERSONAL_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
REPORT_TITLE: Higress Console
|
||||
run: |
|
||||
export MAIN_RELEASE_VERSION=$(cat ${GITHUB_WORKSPACE}/VERSION)
|
||||
export RELEASE_VERSION=$(grep "^higress-console:" ${GITHUB_WORKSPACE}/DEP_VERSION | head -n1 | sed 's/higress-console: //')
|
||||
bash generate_release_report.sh
|
||||
|
||||
- name: Create Update Release Notes Script
|
||||
run: |
|
||||
cat > update_release_note.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
CLEAN_VERSION=${RELEASE_VERSION#v}
|
||||
|
||||
RELEASE_INFO=$(curl -s -L \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
https://api.github.com/repos/${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}/releases/tags/${RELEASE_VERSION})
|
||||
RELEASE_ID=$(echo $RELEASE_INFO | jq -r .id)
|
||||
|
||||
RELEASE_BODY=$(echo $RELEASE_INFO | jq -r .body)
|
||||
NEW_CONTRIBUTORS=$(echo "$RELEASE_BODY" | awk '/## New Contributors/{flag=1; next} /\*\*Full Changelog\*\*/{flag=0} flag' | sed 's/\\n/\n/g')
|
||||
FULL_CHANGELOG=$(echo "$RELEASE_BODY" | awk '/\*\*Full Changelog\*\*:/{print $0}' | sed 's/\*\*Full Changelog\*\*: //g' | sed 's/\\n/\n/g')
|
||||
|
||||
RELEASE_NOTES=$(cat release-notes/${CLEAN_VERSION}/README.md | sed 's/# /## /g')
|
||||
|
||||
if [[ -n "$NEW_CONTRIBUTORS" ]]; then
|
||||
RELEASE_NOTES="${RELEASE_NOTES}
|
||||
|
||||
## New Contributors
|
||||
|
||||
${NEW_CONTRIBUTORS}"
|
||||
fi
|
||||
if [[ -n "$FULL_CHANGELOG" ]]; then
|
||||
RELEASE_NOTES="${RELEASE_NOTES}
|
||||
|
||||
**Full Changelog**: ${FULL_CHANGELOG}"
|
||||
fi
|
||||
|
||||
JSON_DATA=$(jq -n \
|
||||
--arg body "$RELEASE_NOTES" \
|
||||
'{body: $body}')
|
||||
|
||||
curl -L \
|
||||
-X PATCH \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
https://api.github.com/repos/${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}/releases/${RELEASE_ID} \
|
||||
-d "$JSON_DATA"
|
||||
|
||||
EOF
|
||||
chmod +x update_release_note.sh
|
||||
|
||||
- name: Update Release Notes
|
||||
env:
|
||||
GITHUB_REPO_OWNER: alibaba
|
||||
GITHUB_REPO_NAME: higress
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
export RELEASE_VERSION=$(cat ${GITHUB_WORKSPACE}/VERSION)
|
||||
bash update_release_note.sh
|
||||
|
||||
- name: Clean
|
||||
run: |
|
||||
rm generate_release_report.sh
|
||||
rm update_release_note.sh
|
||||
rm -rf higress-report-agent
|
||||
rm -rf github-mcp-server
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: "Add release notes"
|
||||
branch: add-release-notes
|
||||
title: "Add release notes"
|
||||
body: |
|
||||
This PR adds release notes.
|
||||
|
||||
- Automatically generated by GitHub Actions
|
||||
labels: release notes, automated
|
||||
base: main
|
||||
@@ -27,6 +27,7 @@ header:
|
||||
- 'plugins/**'
|
||||
- 'CODEOWNERS'
|
||||
- 'VERSION'
|
||||
- 'DEP_VERSION'
|
||||
- 'tools/'
|
||||
- 'test/README.md'
|
||||
- 'test/README_CN.md'
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
/envoy @gengleilei @johnlanni
|
||||
/istio @SpecialYang @johnlanni
|
||||
/pkg @SpecialYang @johnlanni @CH3CHO
|
||||
/plugins @johnlanni @CH3CHO @rinfx
|
||||
/plugins/wasm-go/extensions/ai-proxy @cr7258 @CH3CHO @rinfx
|
||||
/plugins @johnlanni @CH3CHO @rinfx @erasernoob
|
||||
/plugins/wasm-go/extensions/ai-proxy @cr7258 @CH3CHO @rinfx @wydream
|
||||
/plugins/wasm-rust @007gzs @jizhuozhi
|
||||
/registry @NameHaibinZhang @2456868764 @johnlanni
|
||||
/registry @Erica177 @2456868764 @johnlanni
|
||||
/test @Xunzhuo @2456868764 @CH3CHO
|
||||
/tools @johnlanni @Xunzhuo @2456868764
|
||||
|
||||
|
||||
1
DEP_VERSION
Normal file
1
DEP_VERSION
Normal file
@@ -0,0 +1 @@
|
||||
higress-console: v2.1.6
|
||||
@@ -144,7 +144,7 @@ docker-buildx-push: clean-env docker.higress-buildx
|
||||
export PARENT_GIT_TAG:=$(shell cat VERSION)
|
||||
export PARENT_GIT_REVISION:=$(TAG)
|
||||
|
||||
export ENVOY_PACKAGE_URL_PATTERN?=https://github.com/higress-group/proxy/releases/download/v2.1.7/envoy-symbol-ARCH.tar.gz
|
||||
export ENVOY_PACKAGE_URL_PATTERN?=https://github.com/higress-group/proxy/releases/download/v2.1.8/envoy-symbol-ARCH.tar.gz
|
||||
|
||||
build-envoy: prebuild
|
||||
./tools/hack/build-envoy.sh
|
||||
@@ -192,8 +192,8 @@ install: pre-install
|
||||
helm install higress helm/higress -n higress-system --create-namespace --set 'global.local=true'
|
||||
|
||||
HIGRESS_LATEST_IMAGE_TAG ?= latest
|
||||
ENVOY_LATEST_IMAGE_TAG ?= 958467a353d411ae3f06e03b096bfd342cddb2c6
|
||||
ISTIO_LATEST_IMAGE_TAG ?= d9c728d3b01f64855e012b08d136e306f1160397
|
||||
ENVOY_LATEST_IMAGE_TAG ?= latest
|
||||
ISTIO_LATEST_IMAGE_TAG ?= latest
|
||||
|
||||
install-dev: pre-install
|
||||
helm install higress helm/core -n higress-system --create-namespace --set 'controller.tag=$(TAG)' --set 'gateway.replicas=1' --set 'pilot.tag=$(ISTIO_LATEST_IMAGE_TAG)' --set 'gateway.tag=$(ENVOY_LATEST_IMAGE_TAG)' --set 'global.local=true'
|
||||
|
||||
Submodule envoy/envoy updated: 583feb54ce...e2707255f1
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
appVersion: 2.1.5-rc.1
|
||||
appVersion: 2.1.6
|
||||
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.5-rc.1
|
||||
version: 2.1.6
|
||||
|
||||
@@ -9,9 +9,7 @@
|
||||
accessLogFile: "/dev/stdout"
|
||||
{{- end }}
|
||||
ingressControllerMode: "OFF"
|
||||
accessLogFormat: '{"ai_log":"%FILTER_STATE(wasm.ai_log:PLAIN)%","authority":"%REQ(X-ENVOY-ORIGINAL-HOST?:AUTHORITY)%","bytes_received":"%BYTES_RECEIVED%","bytes_sent":"%BYTES_SENT%","downstream_local_address":"%DOWNSTREAM_LOCAL_ADDRESS%","downstream_remote_address":"%DOWNSTREAM_REMOTE_ADDRESS%","duration":"%DURATION%","istio_policy_status":"%DYNAMIC_METADATA(istio.mixer:status)%","method":"%REQ(:METHOD)%","path":"%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%","protocol":"%PROTOCOL%","request_id":"%REQ(X-REQUEST-ID)%","requested_server_name":"%REQUESTED_SERVER_NAME%","response_code":"%RESPONSE_CODE%","response_flags":"%RESPONSE_FLAGS%","route_name":"%ROUTE_NAME%","start_time":"%START_TIME%","trace_id":"%REQ(X-B3-TRACEID)%","upstream_cluster":"%UPSTREAM_CLUSTER%","upstream_host":"%UPSTREAM_HOST%","upstream_local_address":"%UPSTREAM_LOCAL_ADDRESS%","upstream_service_time":"%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%","upstream_transport_failure_reason":"%UPSTREAM_TRANSPORT_FAILURE_REASON%","user_agent":"%REQ(USER-AGENT)%","x_forwarded_for":"%REQ(X-FORWARDED-FOR)%","response_code_details":"%RESPONSE_CODE_DETAILS%"}
|
||||
|
||||
'
|
||||
accessLogFormat: '{"ai_log":"%FILTER_STATE(wasm.ai_log:PLAIN)%","authority":"%REQ(X-ENVOY-ORIGINAL-HOST?:AUTHORITY)%","bytes_received":"%BYTES_RECEIVED%","bytes_sent":"%BYTES_SENT%","downstream_local_address":"%DOWNSTREAM_LOCAL_ADDRESS%","downstream_remote_address":"%DOWNSTREAM_REMOTE_ADDRESS%","duration":"%DURATION%","istio_policy_status":"%DYNAMIC_METADATA(istio.mixer:status)%","method":"%REQ(:METHOD)%","path":"%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%","protocol":"%PROTOCOL%","request_id":"%REQ(X-REQUEST-ID)%","requested_server_name":"%REQUESTED_SERVER_NAME%","response_code":"%RESPONSE_CODE%","response_flags":"%RESPONSE_FLAGS%","route_name":"%ROUTE_NAME%","start_time":"%START_TIME%","trace_id":"%REQ(X-B3-TRACEID)%","upstream_cluster":"%UPSTREAM_CLUSTER%","upstream_host":"%UPSTREAM_HOST%","upstream_local_address":"%UPSTREAM_LOCAL_ADDRESS%","upstream_service_time":"%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%","upstream_transport_failure_reason":"%UPSTREAM_TRANSPORT_FAILURE_REASON%","user_agent":"%REQ(USER-AGENT)%","x_forwarded_for":"%REQ(X-FORWARDED-FOR)%","response_code_details":"%RESPONSE_CODE_DETAILS%"}'
|
||||
dnsRefreshRate: 200s
|
||||
enableAutoMtls: false
|
||||
enablePrometheusMerge: false
|
||||
@@ -99,7 +97,7 @@ metadata:
|
||||
name: higress-config
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "gateway.labels" . | nindent 4 }}
|
||||
{{- include "gateway.labels" . | nindent 4 }}
|
||||
data:
|
||||
higress: |-
|
||||
{{- $existingConfig := lookup "v1" "ConfigMap" .Release.Namespace "higress-config" }}
|
||||
@@ -126,7 +124,7 @@ data:
|
||||
{{- else }}
|
||||
networks: {}
|
||||
{{- end }}
|
||||
|
||||
|
||||
mesh: |-
|
||||
{{- if .Values.meshConfig }}
|
||||
{{ $mesh | toYaml | indent 4 }}
|
||||
|
||||
@@ -6,4 +6,8 @@ metadata:
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "controller.labels" . | nindent 4 }}
|
||||
{{- with .Values.controller.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
@@ -581,8 +581,7 @@ controller:
|
||||
# -- Labels to apply to the pod
|
||||
podLabels: {}
|
||||
|
||||
podSecurityContext:
|
||||
{}
|
||||
podSecurityContext: {}
|
||||
# fsGroup: 2000
|
||||
|
||||
ports:
|
||||
@@ -709,13 +708,13 @@ tracing:
|
||||
enable: false
|
||||
sampling: 100
|
||||
timeout: 500
|
||||
skywalking:
|
||||
# access_token: ""
|
||||
service: ""
|
||||
port: 11800
|
||||
# skywalking:
|
||||
# access_token: ""
|
||||
# service: ""
|
||||
# port: 11800
|
||||
# zipkin:
|
||||
# service: ""
|
||||
# port: 9411
|
||||
# service: ""
|
||||
# port: 9411
|
||||
|
||||
# -- Downstream config settings
|
||||
downstream:
|
||||
@@ -787,7 +786,7 @@ pluginServer:
|
||||
|
||||
# Plugin-server Service configuration
|
||||
service:
|
||||
port: 80 # Container target port (usually fixed)
|
||||
port: 80 # Container target port (usually fixed)
|
||||
|
||||
resources:
|
||||
requests:
|
||||
@@ -795,4 +794,4 @@ pluginServer:
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 256Mi
|
||||
memory: 256Mi
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
dependencies:
|
||||
- name: higress-core
|
||||
repository: file://../core
|
||||
version: 2.1.5-rc.1
|
||||
version: 2.1.6
|
||||
- name: higress-console
|
||||
repository: https://higress.io/helm-charts/
|
||||
version: 2.1.4
|
||||
digest: sha256:6dbbfb24eabe0927a167c11896799ea20c7f8590aa2889b853dc9a210d075d3a
|
||||
generated: "2025-06-18T09:15:09.621898+08:00"
|
||||
version: 2.1.6
|
||||
digest: sha256:c5bebb3bd92bf799804443faf9ab69e88ed26815a709e58911859b504b3d04db
|
||||
generated: "2025-07-30T21:13:57.834398+08:00"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
appVersion: 2.1.5-rc.1
|
||||
appVersion: 2.1.6
|
||||
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.5-rc.1
|
||||
version: 2.1.6
|
||||
- name: higress-console
|
||||
repository: "https://higress.io/helm-charts/"
|
||||
version: 2.1.4
|
||||
version: 2.1.6
|
||||
type: application
|
||||
version: 2.1.5-rc.1
|
||||
version: 2.1.6
|
||||
|
||||
@@ -306,7 +306,5 @@ The command removes all the Kubernetes components associated with the chart and
|
||||
| revision | string | `""` | |
|
||||
| tracing.enable | bool | `false` | |
|
||||
| tracing.sampling | int | `100` | |
|
||||
| tracing.skywalking.port | int | `11800` | |
|
||||
| tracing.skywalking.service | string | `""` | |
|
||||
| tracing.timeout | int | `500` | |
|
||||
| upstream | object | `{"connectionBufferLimits":10485760,"idleTimeout":10}` | Upstream config settings |
|
||||
@@ -33,7 +33,8 @@ import (
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||
logs "github.com/higress-group/wasm-go/pkg/log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -72,13 +73,13 @@ type PluginConfig struct {
|
||||
secondField string ` + "`required:\"true\"`" + `
|
||||
}
|
||||
|
||||
func parseConfig(json gjson.Result, config *PluginConfig, log wrapper.Log) error {
|
||||
func parseConfig(json gjson.Result, config *PluginConfig, log logs.Log) error {
|
||||
config.firstField = json.Get("firstField").String()
|
||||
config.secondField = json.Get("secondField").String()
|
||||
return nil
|
||||
}
|
||||
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config PluginConfig, log wrapper.Log) types.Action {
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config PluginConfig, log logs.Log) types.Action {
|
||||
err := proxywasm.AddHttpRequestHeader(config.firstField, config.secondField)
|
||||
if err != nil {
|
||||
log.Critical("failed to set request header")
|
||||
@@ -90,10 +91,10 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, config PluginConfig, log wrap
|
||||
|
||||
module {{ .Name }}
|
||||
|
||||
go 1.19
|
||||
go 1.24
|
||||
|
||||
require (
|
||||
github.com/alibaba/higress/plugins/wasm-go main
|
||||
github.com/higress-group/wasm-go main
|
||||
github.com/higress-group/proxy-wasm-go-sdk main
|
||||
github.com/tidwall/gjson v1.14.3
|
||||
)
|
||||
|
||||
Submodule istio/istio updated: a92e5a15cd...fa8896cf33
@@ -22,8 +22,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
mirrorTargetService = "mirror-target-service"
|
||||
mirrorPercentage = "mirror-percentage"
|
||||
mirrorTargetService = "mirror-target-service"
|
||||
mirrorPercentage = "mirror-percentage"
|
||||
mirrorTargetFQDN = "mirror-target-fqdn"
|
||||
mirrorTargetFQDNPort = "mirror-target-fqdn-port"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -34,6 +36,8 @@ var (
|
||||
type MirrorConfig struct {
|
||||
util.ServiceInfo
|
||||
Percentage *wrappers.DoubleValue
|
||||
FQDN string
|
||||
FPort uint32 // Port for FQDN
|
||||
}
|
||||
|
||||
type mirror struct{}
|
||||
@@ -43,6 +47,24 @@ func (m mirror) Parse(annotations Annotations, config *Ingress, globalContext *G
|
||||
return nil
|
||||
}
|
||||
|
||||
// if FQDN is set, then parse FQDN
|
||||
if fqdn, err := annotations.ParseStringASAP(mirrorTargetFQDN); err == nil {
|
||||
// default is 80
|
||||
var port uint32
|
||||
port = 80
|
||||
|
||||
if p, err := annotations.ParseInt32ASAP(mirrorTargetFQDNPort); err == nil {
|
||||
port = uint32(p)
|
||||
}
|
||||
|
||||
config.Mirror = &MirrorConfig{
|
||||
Percentage: parsePercentage(annotations),
|
||||
FQDN: fqdn,
|
||||
FPort: port,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
target, err := annotations.ParseStringASAP(mirrorTargetService)
|
||||
if err != nil {
|
||||
IngressLog.Errorf("Get mirror target service fail, err: %v", err)
|
||||
@@ -78,7 +100,16 @@ func (m mirror) Parse(annotations Annotations, config *Ingress, globalContext *G
|
||||
serviceInfo.Port = uint32(service.Spec.Ports[0].Port)
|
||||
}
|
||||
|
||||
config.Mirror = &MirrorConfig{
|
||||
ServiceInfo: serviceInfo,
|
||||
Percentage: parsePercentage(annotations),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parsePercentage(annotations Annotations) *wrappers.DoubleValue {
|
||||
var percentage *wrappers.DoubleValue
|
||||
|
||||
if value, err := annotations.ParseIntASAP(mirrorPercentage); err == nil {
|
||||
if value < 100 {
|
||||
percentage = &wrappers.DoubleValue{
|
||||
@@ -86,12 +117,7 @@ func (m mirror) Parse(annotations Annotations, config *Ingress, globalContext *G
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
config.Mirror = &MirrorConfig{
|
||||
ServiceInfo: serviceInfo,
|
||||
Percentage: percentage,
|
||||
}
|
||||
return nil
|
||||
return percentage
|
||||
}
|
||||
|
||||
func (m mirror) ApplyRoute(route *networking.HTTPRoute, config *Ingress) {
|
||||
@@ -99,10 +125,21 @@ func (m mirror) ApplyRoute(route *networking.HTTPRoute, config *Ingress) {
|
||||
return
|
||||
}
|
||||
|
||||
var mirrorHost string
|
||||
var mirrorPort uint32
|
||||
|
||||
if config.Mirror.FQDN != "" {
|
||||
mirrorHost = config.Mirror.FQDN
|
||||
mirrorPort = config.Mirror.FPort
|
||||
} else {
|
||||
mirrorHost = util.CreateServiceFQDN(config.Mirror.Namespace, config.Mirror.Name)
|
||||
mirrorPort = config.Mirror.Port
|
||||
}
|
||||
|
||||
route.Mirror = &networking.Destination{
|
||||
Host: util.CreateServiceFQDN(config.Mirror.Namespace, config.Mirror.Name),
|
||||
Host: mirrorHost,
|
||||
Port: &networking.PortSelector{
|
||||
Number: config.Mirror.Port,
|
||||
Number: mirrorPort,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -114,5 +151,5 @@ func (m mirror) ApplyRoute(route *networking.HTTPRoute, config *Ingress) {
|
||||
}
|
||||
|
||||
func needMirror(annotations Annotations) bool {
|
||||
return annotations.HasASAP(mirrorTargetService)
|
||||
return annotations.HasASAP(mirrorTargetService) || annotations.HasASAP(mirrorTargetFQDN)
|
||||
}
|
||||
|
||||
@@ -15,12 +15,13 @@
|
||||
package annotations
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/util"
|
||||
"github.com/golang/protobuf/proto"
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseMirror(t *testing.T) {
|
||||
@@ -29,6 +30,28 @@ func TestParseMirror(t *testing.T) {
|
||||
expect *MirrorConfig
|
||||
}{
|
||||
{},
|
||||
{
|
||||
input: []map[string]string{
|
||||
{buildHigressAnnotationKey(mirrorTargetFQDN): "www.example.com"},
|
||||
{buildNginxAnnotationKey(mirrorTargetFQDN): "www.example.com"},
|
||||
},
|
||||
expect: &MirrorConfig{
|
||||
ServiceInfo: util.ServiceInfo{},
|
||||
FQDN: "www.example.com",
|
||||
FPort: 80,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: []map[string]string{
|
||||
{buildHigressAnnotationKey(mirrorTargetFQDN): "192.168.252.112", buildHigressAnnotationKey(mirrorTargetFQDNPort): "8080"},
|
||||
{buildNginxAnnotationKey(mirrorTargetFQDN): "192.168.252.112", buildNginxAnnotationKey(mirrorTargetFQDNPort): "8080"},
|
||||
},
|
||||
expect: &MirrorConfig{
|
||||
ServiceInfo: util.ServiceInfo{},
|
||||
FQDN: "192.168.252.112",
|
||||
FPort: 8080,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: []map[string]string{
|
||||
{buildHigressAnnotationKey(mirrorTargetService): "test/app"},
|
||||
@@ -149,6 +172,42 @@ func TestMirror_ApplyRoute(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
Mirror: &MirrorConfig{
|
||||
ServiceInfo: util.ServiceInfo{},
|
||||
FQDN: "www.example.com",
|
||||
FPort: 80,
|
||||
},
|
||||
},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPRoute{
|
||||
Mirror: &networking.Destination{
|
||||
Host: "www.example.com",
|
||||
Port: &networking.PortSelector{
|
||||
Number: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
Mirror: &MirrorConfig{
|
||||
ServiceInfo: util.ServiceInfo{},
|
||||
FQDN: "192.168.252.112",
|
||||
FPort: 8080,
|
||||
},
|
||||
},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPRoute{
|
||||
Mirror: &networking.Destination{
|
||||
Host: "192.168.252.112",
|
||||
Port: &networking.PortSelector{
|
||||
Number: 8080,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mirror := mirror{}
|
||||
|
||||
@@ -17,7 +17,7 @@ RUN if [ "$GOARCH" = "arm64" ]; then \
|
||||
else \
|
||||
echo "Installing AMD64 toolchain" && \
|
||||
apt-get update && \
|
||||
apt-get install -y gcc binutils; \
|
||||
apt-get install -y gcc-x86-64-linux-gnu binutils-x86-64-linux-gnu; \
|
||||
fi
|
||||
|
||||
WORKDIR /workspace
|
||||
@@ -30,7 +30,7 @@ RUN go mod tidy
|
||||
RUN if [ "$GOARCH" = "arm64" ]; then \
|
||||
CC=aarch64-linux-gnu-gcc AS=aarch64-linux-gnu-as go build -o /$GO_FILTER_NAME.so -buildmode=c-shared .; \
|
||||
else \
|
||||
go build -o /$GO_FILTER_NAME.so -buildmode=c-shared .; \
|
||||
CC=x86_64-linux-gnu-gcc AS=x86_64-linux-gnu-as go build -o /$GO_FILTER_NAME.so -buildmode=c-shared .; \
|
||||
fi
|
||||
|
||||
FROM scratch AS output
|
||||
|
||||
@@ -54,8 +54,15 @@ http_filters:
|
||||
|
||||
## 快速构建
|
||||
|
||||
使用以下命令可以快速构建 golang filter 插件:
|
||||
使用以下命令可以快速构建 golang filter 插件:
|
||||
|
||||
```bash
|
||||
make build
|
||||
```
|
||||
|
||||
如果是 arm64 架构,请设置 `GOARCH=arm64`:
|
||||
|
||||
```bash
|
||||
make build GOARCH=arm64
|
||||
```
|
||||
你也可以直接在 Higress 项目的根目录下执行 `make build-gateway-local` 来构建 Higress Gateway 镜像,`golang-filter.so` 将会自动构建并复制到镜像中。
|
||||
|
||||
@@ -58,4 +58,12 @@ Use the following command to quickly build the golang filter plugin:
|
||||
|
||||
```bash
|
||||
make build
|
||||
```
|
||||
```
|
||||
|
||||
If you are on an arm64 architecture, please set `GOARCH=arm64`:
|
||||
|
||||
```bash
|
||||
make build GOARCH=arm64
|
||||
```
|
||||
|
||||
Alternatively, you can build the Higress Gateway image directly by running `make build-gateway-local` in the root directory of the Higress project. The `golang-filter.so` file will be automatically built and included in the image.
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
_ "github.com/alibaba/higress/plugins/golang-filter/mcp-server/registry/nacos"
|
||||
_ "github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/gorm"
|
||||
_ "github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/higress/higress-api"
|
||||
mcp_session "github.com/alibaba/higress/plugins/golang-filter/mcp-session"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
xds "github.com/cncf/xds/go/xds/type/v3"
|
||||
@@ -99,7 +100,7 @@ func (p *Parser) Parse(any *anypb.Any, callbacks api.ConfigCallbackHandler) (int
|
||||
|
||||
serverInstance, err := server.NewServer(serverName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize DBServer: %w", err)
|
||||
return nil, fmt.Errorf("failed to initialize MCP Server: %w", err)
|
||||
}
|
||||
|
||||
conf.servers = append(conf.servers, &SSEServerWrapper{
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"github.com/nacos-group/nacos-sdk-go/v2/vo"
|
||||
)
|
||||
|
||||
type NacosMcpRegsitry struct {
|
||||
type NacosMcpRegistry struct {
|
||||
serviceMatcher map[string]string
|
||||
configClient config_client.IConfigClient
|
||||
namingClient naming_client.INamingClient
|
||||
@@ -27,7 +27,7 @@ type NacosMcpRegsitry struct {
|
||||
const DEFAULT_SERVICE_LIST_MAX_PGSIZXE = 10000
|
||||
const MCP_TOOL_SUBFIX = "-mcp-tools.json"
|
||||
|
||||
func (n *NacosMcpRegsitry) ListToolsDesciption() []*registry.ToolDescription {
|
||||
func (n *NacosMcpRegistry) ListToolsDescription() []*registry.ToolDescription {
|
||||
if n.toolsDescription == nil {
|
||||
n.refreshToolsList()
|
||||
}
|
||||
@@ -39,7 +39,7 @@ func (n *NacosMcpRegsitry) ListToolsDesciption() []*registry.ToolDescription {
|
||||
return result
|
||||
}
|
||||
|
||||
func (n *NacosMcpRegsitry) GetToolRpcContext(toolName string) (*registry.RpcContext, bool) {
|
||||
func (n *NacosMcpRegistry) GetToolRpcContext(toolName string) (*registry.RpcContext, bool) {
|
||||
if n.toolsRpcContext == nil {
|
||||
n.refreshToolsList()
|
||||
}
|
||||
@@ -47,11 +47,11 @@ func (n *NacosMcpRegsitry) GetToolRpcContext(toolName string) (*registry.RpcCont
|
||||
return tool, ok
|
||||
}
|
||||
|
||||
func (n *NacosMcpRegsitry) RegisterToolChangeEventListener(listener registry.ToolChangeEventListener) {
|
||||
func (n *NacosMcpRegistry) RegisterToolChangeEventListener(listener registry.ToolChangeEventListener) {
|
||||
n.toolChangeEventListeners = append(n.toolChangeEventListeners, listener)
|
||||
}
|
||||
|
||||
func (n *NacosMcpRegsitry) refreshToolsList() bool {
|
||||
func (n *NacosMcpRegistry) refreshToolsList() bool {
|
||||
changed := false
|
||||
for group, serviceMatcher := range n.serviceMatcher {
|
||||
if n.refreshToolsListForGroup(group, serviceMatcher) {
|
||||
@@ -61,7 +61,7 @@ func (n *NacosMcpRegsitry) refreshToolsList() bool {
|
||||
return changed
|
||||
}
|
||||
|
||||
func (n *NacosMcpRegsitry) refreshToolsListForGroup(group string, serviceMatcher string) bool {
|
||||
func (n *NacosMcpRegistry) refreshToolsListForGroup(group string, serviceMatcher string) bool {
|
||||
services, err := n.namingClient.GetAllServicesInfo(vo.GetAllServiceInfoParam{
|
||||
GroupName: group,
|
||||
PageNo: 1,
|
||||
@@ -134,7 +134,7 @@ func getFormatServiceName(group string, service string) string {
|
||||
return fmt.Sprintf("%s_%s", group, service)
|
||||
}
|
||||
|
||||
func (n *NacosMcpRegsitry) deleteToolForService(group string, service string) {
|
||||
func (n *NacosMcpRegistry) deleteToolForService(group string, service string) {
|
||||
toolsNeedReset := []string{}
|
||||
|
||||
formatServiceName := getFormatServiceName(group, service)
|
||||
@@ -150,7 +150,7 @@ func (n *NacosMcpRegsitry) deleteToolForService(group string, service string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NacosMcpRegsitry) refreshToolsListForServiceWithContent(group string, service string, newConfig *string, instances *[]model.Instance) bool {
|
||||
func (n *NacosMcpRegistry) refreshToolsListForServiceWithContent(group string, service string, newConfig *string, instances *[]model.Instance) bool {
|
||||
|
||||
if newConfig == nil {
|
||||
dataId := makeToolsConfigId(service)
|
||||
@@ -243,7 +243,7 @@ func (n *NacosMcpRegsitry) refreshToolsListForServiceWithContent(group string, s
|
||||
return true
|
||||
}
|
||||
|
||||
func (n *NacosMcpRegsitry) GetCredential(name string, group string) *registry.CredentialInfo {
|
||||
func (n *NacosMcpRegistry) GetCredential(name string, group string) *registry.CredentialInfo {
|
||||
dataId := makeCredentialDataId(name)
|
||||
content, err := n.configClient.GetConfig(vo.ConfigParam{
|
||||
DataId: dataId,
|
||||
@@ -265,11 +265,11 @@ func (n *NacosMcpRegsitry) GetCredential(name string, group string) *registry.Cr
|
||||
return &credential
|
||||
}
|
||||
|
||||
func (n *NacosMcpRegsitry) refreshToolsListForService(group string, service string) bool {
|
||||
func (n *NacosMcpRegistry) refreshToolsListForService(group string, service string) bool {
|
||||
return n.refreshToolsListForServiceWithContent(group, service, nil, nil)
|
||||
}
|
||||
|
||||
func (n *NacosMcpRegsitry) listenToService(group string, service string) {
|
||||
func (n *NacosMcpRegistry) listenToService(group string, service string) {
|
||||
|
||||
// config changed, tools description may be changed
|
||||
err := n.configClient.ListenConfig(vo.ConfigParam{
|
||||
|
||||
@@ -35,7 +35,7 @@ func (l *McpServerToolsChangeListener) OnToolChanged(reg registry.McpServerRegis
|
||||
resetToolsToMcpServer(l.mcpServer, reg)
|
||||
}
|
||||
|
||||
func CreateNacosMcpRegsitry(config *NacosConfig) (*NacosMcpRegsitry, error) {
|
||||
func CreateNacosMcpRegistry(config *NacosConfig) (*NacosMcpRegistry, error) {
|
||||
sc := []constant.ServerConfig{
|
||||
*constant.NewServerConfig(*config.ServerAddr, 8848, constant.WithContextPath("/nacos")),
|
||||
}
|
||||
@@ -90,7 +90,7 @@ func CreateNacosMcpRegsitry(config *NacosConfig) (*NacosMcpRegsitry, error) {
|
||||
return nil, fmt.Errorf("failed to initial naming config client: %w", err)
|
||||
}
|
||||
|
||||
return &NacosMcpRegsitry{
|
||||
return &NacosMcpRegistry{
|
||||
configClient: configClient,
|
||||
namingClient: namingClient,
|
||||
serviceMatcher: *config.ServiceMatcher,
|
||||
@@ -143,7 +143,7 @@ func (c *NacosConfig) NewServer(serverName string) (*common.MCPServer, error) {
|
||||
"1.0.0",
|
||||
)
|
||||
|
||||
nacosRegistry, err := CreateNacosMcpRegsitry(c)
|
||||
nacosRegistry, err := CreateNacosMcpRegistry(c)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize NacosMcpRegistry: %w", err)
|
||||
}
|
||||
@@ -172,7 +172,7 @@ func (c *NacosConfig) NewServer(serverName string) (*common.MCPServer, error) {
|
||||
|
||||
func resetToolsToMcpServer(mcpServer *common.MCPServer, reg registry.McpServerRegistry) {
|
||||
wrappedTools := []common.ServerTool{}
|
||||
tools := reg.ListToolsDesciption()
|
||||
tools := reg.ListToolsDescription()
|
||||
for _, tool := range tools {
|
||||
wrappedTools = append(wrappedTools, common.ServerTool{
|
||||
Tool: mcp.NewToolWithRawSchema(tool.Name, tool.Description, tool.InputSchema),
|
||||
|
||||
@@ -36,7 +36,7 @@ type ToolChangeEventListener interface {
|
||||
}
|
||||
|
||||
type McpServerRegistry interface {
|
||||
ListToolsDesciption() []*ToolDescription
|
||||
ListToolsDescription() []*ToolDescription
|
||||
GetToolRpcContext(toolname string) (*RpcContext, bool)
|
||||
RegisterToolChangeEventListener(listener ToolChangeEventListener)
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@ func selectOneInstance(ctx *RpcContext) (*Instance, error) {
|
||||
return &select_instance, nil
|
||||
}
|
||||
|
||||
func getRemoteCallhandle(ctx *RpcContext) (RemoteCallHandle, error) {
|
||||
func getRemoteCallHandle(ctx *RpcContext) (RemoteCallHandle, error) {
|
||||
if ctx.Protocol == PROTOCOL_HTTP || ctx.Protocol == PROTOCOL_HTTPS {
|
||||
return newHttpRemoteCallHandle(ctx)
|
||||
} else {
|
||||
@@ -192,7 +192,7 @@ func CommonRemoteCall(reg McpServerRegistry, toolName string, parameters map[str
|
||||
return nil, fmt.Errorf("Unknown tool %s", toolName)
|
||||
}
|
||||
|
||||
remoteHandle, err := getRemoteCallhandle(ctx)
|
||||
remoteHandle, err := getRemoteCallHandle(ctx)
|
||||
if remoteHandle == nil {
|
||||
return nil, fmt.Errorf("Unknown backend protocol %s", ctx.Protocol)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package gorm
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
@@ -25,6 +26,14 @@ type DBClient struct {
|
||||
panicCount int32 // Add panic counter
|
||||
}
|
||||
|
||||
// supports database types
|
||||
const (
|
||||
MYSQL = "mysql"
|
||||
POSTGRES = "postgres"
|
||||
CLICKHOUSE = "clickhouse"
|
||||
SQLITE = "sqlite"
|
||||
)
|
||||
|
||||
// NewDBClient creates a new DBClient instance and establishes a connection to the database
|
||||
func NewDBClient(dsn string, dbType string, stop chan struct{}) *DBClient {
|
||||
client := &DBClient{
|
||||
@@ -53,13 +62,13 @@ func (c *DBClient) connect() error {
|
||||
}
|
||||
|
||||
switch c.dbType {
|
||||
case "postgres":
|
||||
case POSTGRES:
|
||||
db, err = gorm.Open(postgres.Open(c.dsn), &gormConfig)
|
||||
case "clickhouse":
|
||||
case CLICKHOUSE:
|
||||
db, err = gorm.Open(clickhouse.Open(c.dsn), &gormConfig)
|
||||
case "mysql":
|
||||
case MYSQL:
|
||||
db, err = gorm.Open(mysql.Open(c.dsn), &gormConfig)
|
||||
case "sqlite":
|
||||
case SQLITE:
|
||||
db, err = gorm.Open(sqlite.Open(c.dsn), &gormConfig)
|
||||
default:
|
||||
return fmt.Errorf("unsupported database type %s", c.dbType)
|
||||
@@ -125,25 +134,164 @@ func (c *DBClient) reconnectLoop() {
|
||||
}
|
||||
}
|
||||
|
||||
// ExecuteSQL executes a raw SQL query and returns the result as a slice of maps
|
||||
func (c *DBClient) ExecuteSQL(query string, args ...interface{}) ([]map[string]interface{}, error) {
|
||||
func (c *DBClient) reconnectIfDbEmpty() error {
|
||||
if c.db == nil {
|
||||
// Trigger reconnection
|
||||
select {
|
||||
case c.reconnect <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
return nil, fmt.Errorf("database is not connected, attempting to reconnect")
|
||||
return fmt.Errorf("database is not connected, attempting to reconnect")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
rows, err := c.db.Raw(query, args...).Rows()
|
||||
func (c *DBClient) handleSQLError(err error) error {
|
||||
if err != nil {
|
||||
// If execution fails, connection might be lost, trigger reconnection
|
||||
select {
|
||||
case c.reconnect <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
return nil, fmt.Errorf("failed to execute SQL query: %w", err)
|
||||
return fmt.Errorf("failed to execute SQL: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DescribeTable Get the structure of a specific table.
|
||||
func (c *DBClient) DescribeTable(table string) ([]map[string]interface{}, error) {
|
||||
var sql string
|
||||
switch c.dbType {
|
||||
case MYSQL:
|
||||
sql = `
|
||||
select
|
||||
column_name,
|
||||
column_type,
|
||||
is_nullable,
|
||||
column_key,
|
||||
column_default,
|
||||
extra,
|
||||
column_comment
|
||||
from information_schema.columns
|
||||
where table_schema = database() and table_name = ?
|
||||
`
|
||||
return c.Query(sql, table)
|
||||
|
||||
case POSTGRES:
|
||||
sql = `
|
||||
select
|
||||
column_name,
|
||||
data_type as column_type,
|
||||
is_nullable,
|
||||
case
|
||||
when column_default like 'nextval%' then 'auto_increment'
|
||||
when column_default is not null then 'default'
|
||||
else ''
|
||||
end as column_key,
|
||||
column_default,
|
||||
case
|
||||
when column_default like 'nextval%' then 'auto_increment'
|
||||
else ''
|
||||
end as extra,
|
||||
col_description((select oid from pg_class where relname = ?), ordinal_position) as column_comment
|
||||
from information_schema.columns
|
||||
where table_name = ?
|
||||
`
|
||||
lowerTable := strings.ToLower(table)
|
||||
return c.Query(sql, lowerTable, lowerTable)
|
||||
|
||||
case CLICKHOUSE:
|
||||
sql = `
|
||||
select
|
||||
name as column_name,
|
||||
type as column_type,
|
||||
if(is_nullable, 'YES', 'NO') as is_nullable,
|
||||
default_kind as column_key,
|
||||
default_expression as column_default,
|
||||
default_kind as extra,
|
||||
comment as column_comment
|
||||
from system.columns
|
||||
where database = currentDatabase() and table = ?
|
||||
`
|
||||
return c.Query(sql, table)
|
||||
|
||||
case SQLITE:
|
||||
sql = `
|
||||
select
|
||||
name as column_name,
|
||||
type as column_type,
|
||||
not (notnull = 1) as is_nullable,
|
||||
pk as column_key,
|
||||
dflt_value as column_default,
|
||||
'' as extra,
|
||||
'' as column_comment
|
||||
from pragma_table_info(?)
|
||||
`
|
||||
return c.Query(sql, table)
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported database type: %s", c.dbType)
|
||||
}
|
||||
}
|
||||
|
||||
// ListTables List all tables in the connected database.
|
||||
func (c *DBClient) ListTables() ([]string, error) {
|
||||
var sql string
|
||||
switch c.dbType {
|
||||
case MYSQL:
|
||||
sql = "show tables"
|
||||
case POSTGRES:
|
||||
sql = "select tablename from pg_tables where schemaname = 'public'"
|
||||
case CLICKHOUSE:
|
||||
sql = "select name from system.tables where database = currentDatabase()"
|
||||
case SQLITE:
|
||||
sql = "select name from sqlite_master where type='table'"
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported database type: %s", c.dbType)
|
||||
}
|
||||
|
||||
rows, err := c.db.Raw(sql).Rows()
|
||||
if err := c.handleSQLError(err); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var tables []string
|
||||
for rows.Next() {
|
||||
var table string
|
||||
if err := rows.Scan(&table); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan table name: %w", err)
|
||||
}
|
||||
tables = append(tables, table)
|
||||
}
|
||||
|
||||
return tables, nil
|
||||
}
|
||||
|
||||
// Execute executes an INSERT, UPDATE, or DELETE raw SQL and returns the rows affected
|
||||
func (c *DBClient) Execute(sql string, args ...interface{}) (int64, error) {
|
||||
if err := c.reconnectIfDbEmpty(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
tx := c.db.Exec(sql, args...)
|
||||
if err := c.handleSQLError(tx.Error); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer tx.Commit()
|
||||
|
||||
return tx.RowsAffected, nil
|
||||
}
|
||||
|
||||
// Query executes a raw SQL query and returns the result as a slice of maps
|
||||
func (c *DBClient) Query(sql string, args ...interface{}) ([]map[string]interface{}, error) {
|
||||
if err := c.reconnectIfDbEmpty(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rows, err := c.db.Raw(sql, args...).Rows()
|
||||
if err := c.handleSQLError(err); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
|
||||
@@ -49,11 +49,24 @@ func (c *DBConfig) NewServer(serverName string) (*common.MCPServer, error) {
|
||||
)
|
||||
|
||||
dbClient := NewDBClient(c.dsn, c.dbType, mcpServer.GetDestoryChannel())
|
||||
descriptionSuffix := fmt.Sprintf("in database %s. Database description: %s", c.dbType, c.description)
|
||||
// Add query tool
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("query", fmt.Sprintf("Run a read-only SQL query in database %s. Database description: %s", c.dbType, c.description), GetQueryToolSchema()),
|
||||
mcp.NewToolWithRawSchema("query", fmt.Sprintf("Run a read-only SQL query %s", descriptionSuffix), GetQueryToolSchema()),
|
||||
HandleQueryTool(dbClient),
|
||||
)
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("execute", fmt.Sprintf("Execute an insert, update, or delete SQL %s", descriptionSuffix), GetExecuteToolSchema()),
|
||||
HandleExecuteTool(dbClient),
|
||||
)
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("list tables", fmt.Sprintf("List all tables %s", descriptionSuffix), GetListTablesToolSchema()),
|
||||
HandleListTablesTool(dbClient),
|
||||
)
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("describe table", fmt.Sprintf("Get the structure of a specific table %s", descriptionSuffix), GetDescribeTableToolSchema()),
|
||||
HandleDescribeTableTool(dbClient),
|
||||
)
|
||||
|
||||
return mcpServer, nil
|
||||
}
|
||||
|
||||
@@ -18,27 +18,80 @@ func HandleQueryTool(dbClient *DBClient) common.ToolHandlerFunc {
|
||||
return nil, fmt.Errorf("invalid message argument")
|
||||
}
|
||||
|
||||
results, err := dbClient.ExecuteSQL(message)
|
||||
results, err := dbClient.Query(message)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute SQL query: %w", err)
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(results)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal SQL results: %w", err)
|
||||
return buildCallToolResult(results)
|
||||
}
|
||||
}
|
||||
|
||||
// HandleExecuteTool handles SQL INSERT, UPDATE, or DELETE execution
|
||||
func HandleExecuteTool(dbClient *DBClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
message, ok := arguments["sql"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid message argument")
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(jsonData),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
results, err := dbClient.Execute(message)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute SQL query: %w", err)
|
||||
}
|
||||
|
||||
return buildCallToolResult(results)
|
||||
}
|
||||
}
|
||||
|
||||
// HandleListTablesTool handles list all tables
|
||||
func HandleListTablesTool(dbClient *DBClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
results, err := dbClient.ListTables()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute SQL query: %w", err)
|
||||
}
|
||||
|
||||
return buildCallToolResult(results)
|
||||
}
|
||||
}
|
||||
|
||||
// HandleDescribeTableTool handles describe table
|
||||
func HandleDescribeTableTool(dbClient *DBClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
message, ok := arguments["table"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid message argument")
|
||||
}
|
||||
|
||||
results, err := dbClient.DescribeTable(message)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute SQL query: %w", err)
|
||||
}
|
||||
|
||||
return buildCallToolResult(results)
|
||||
}
|
||||
}
|
||||
|
||||
// buildCallToolResult builds the call tool result
|
||||
func buildCallToolResult(results any) (*mcp.CallToolResult, error) {
|
||||
jsonData, err := json.Marshal(results)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal SQL results: %w", err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(jsonData),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetQueryToolSchema returns the schema for query tool
|
||||
func GetQueryToolSchema() json.RawMessage {
|
||||
return json.RawMessage(`
|
||||
@@ -53,3 +106,44 @@ func GetQueryToolSchema() json.RawMessage {
|
||||
}
|
||||
`)
|
||||
}
|
||||
|
||||
// GetExecuteToolSchema returns the schema for execute tool
|
||||
func GetExecuteToolSchema() json.RawMessage {
|
||||
return json.RawMessage(`
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sql": {
|
||||
"type": "string",
|
||||
"description": "The sql to execute"
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
}
|
||||
|
||||
// GetDescribeTableToolSchema returns the schema for DescribeTable tool
|
||||
func GetDescribeTableToolSchema() json.RawMessage {
|
||||
return json.RawMessage(`
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"table": {
|
||||
"type": "string",
|
||||
"description": "table name"
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
}
|
||||
|
||||
// GetListTablesToolSchema returns the schema for ListTables tool
|
||||
func GetListTablesToolSchema() json.RawMessage {
|
||||
return json.RawMessage(`
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
}
|
||||
}
|
||||
`)
|
||||
}
|
||||
|
||||
95
plugins/golang-filter/mcp-server/servers/higress/client.go
Normal file
95
plugins/golang-filter/mcp-server/servers/higress/client.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package higress
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
)
|
||||
|
||||
// HigressClient handles Higress Console API connections and operations
|
||||
type HigressClient struct {
|
||||
baseURL string
|
||||
username string
|
||||
password string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func NewHigressClient(baseURL, username, password string) *HigressClient {
|
||||
client := &HigressClient{
|
||||
baseURL: baseURL,
|
||||
username: username,
|
||||
password: password,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
}
|
||||
|
||||
api.LogInfof("Higress Console client initialized: %s", baseURL)
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
func (c *HigressClient) Get(path string) ([]byte, error) {
|
||||
return c.request("GET", path, nil)
|
||||
}
|
||||
|
||||
func (c *HigressClient) Post(path string, data interface{}) ([]byte, error) {
|
||||
return c.request("POST", path, data)
|
||||
}
|
||||
|
||||
func (c *HigressClient) Put(path string, data interface{}) ([]byte, error) {
|
||||
return c.request("PUT", path, data)
|
||||
}
|
||||
|
||||
func (c *HigressClient) Delete(path string) ([]byte, error) {
|
||||
return c.request("DELETE", path, nil)
|
||||
}
|
||||
func (c *HigressClient) request(method, path string, data interface{}) ([]byte, error) {
|
||||
url := c.baseURL + path
|
||||
|
||||
var body io.Reader
|
||||
if data != nil {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal request data: %w", err)
|
||||
}
|
||||
body = bytes.NewBuffer(jsonData)
|
||||
api.LogDebugf("Higress API %s %s: %s", method, url, string(jsonData))
|
||||
} else {
|
||||
api.LogDebugf("Higress API %s %s", method, url)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, url, body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.SetBasicAuth(c.username, c.password)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("HTTP error %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
|
||||
return respBody, nil
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
# Higress API MCP Server
|
||||
|
||||
Higress API MCP Server 提供了 MCP 工具来管理 Higress 路由、服务来源和插件等资源。
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 路由管理
|
||||
- `list-routes`: 列出路由
|
||||
- `get-route`: 获取路由
|
||||
- `add-route`: 添加路由
|
||||
- `update-route`: 更新路由
|
||||
|
||||
### 服务来源管理
|
||||
- `list-service-sources`: 列出服务来源
|
||||
- `get-service-source`: 获取服务来源
|
||||
- `add-service-source`: 添加服务来源
|
||||
- `update-service-source`: 更新服务来源
|
||||
|
||||
### 插件管理
|
||||
- `get-plugin`: 获取插件配置
|
||||
- `delete-plugin`: 删除插件
|
||||
- `update-request-block-plugin`: 更新 request-block 插件配置
|
||||
|
||||
## 配置参数
|
||||
|
||||
| 参数 | 类型 | 必需 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `higressURL` | string | 必填 | Higress Console 的 URL 地址 |
|
||||
| `username` | string | 必填 | Higress Console 登录用户名 |
|
||||
| `password` | string | 必填 | Higress Console 登录密码 |
|
||||
| `description` | string | 可选 | 服务器描述信息 |
|
||||
|
||||
配置示例:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
annotations:
|
||||
meta.helm.sh/release-name: higress
|
||||
meta.helm.sh/release-namespace: higress-system
|
||||
labels:
|
||||
app: higress-gateway
|
||||
app.kubernetes.io/managed-by: Helm
|
||||
app.kubernetes.io/name: higress-gateway
|
||||
app.kubernetes.io/version: 2.1.4
|
||||
helm.sh/chart: higress-core-2.1.4
|
||||
higress: higress-system-higress-gateway
|
||||
name: higress-config
|
||||
namespace: higress-system
|
||||
data:
|
||||
higress: |-
|
||||
mcpServer:
|
||||
sse_path_suffix: /sse # SSE 连接的路径后缀
|
||||
enable: true # 启用 MCP Server
|
||||
redis:
|
||||
address: redis-stack-server.higress-system.svc.cluster.local:6379 # Redis服务地址
|
||||
username: "" # Redis用户名(可选)
|
||||
password: "" # Redis密码(可选)
|
||||
db: 0 # Redis数据库(可选)
|
||||
match_list: # MCP Server 会话保持路由规则(当匹配下面路径时,将被识别为一个 MCP 会话,通过 SSE 等机制进行会话保持)
|
||||
- match_rule_domain: "*"
|
||||
match_rule_path: /higress-api
|
||||
match_rule_type: "prefix"
|
||||
servers:
|
||||
- name: higress-api-mcp-server # MCP Server 名称
|
||||
path: /higress-api # 访问路径,需要与 match_list 中的配置匹配
|
||||
type: higress-api # 类型和 RegisterServer 一致
|
||||
config:
|
||||
higressURL: http://higress-console.higress-system.svc.cluster.local:8080
|
||||
username: admin
|
||||
password: admin
|
||||
```
|
||||
@@ -0,0 +1,73 @@
|
||||
# Higress API MCP Server
|
||||
|
||||
Higress API MCP Server provides MCP tools to manage Higress routes, service sources, plugins and other resources.
|
||||
|
||||
## Features
|
||||
|
||||
### Route Management
|
||||
- `list-routes`: List routes
|
||||
- `get-route`: Get route
|
||||
- `add-route`: Add route
|
||||
- `update-route`: Update route
|
||||
|
||||
### Service Source Management
|
||||
- `list-service-sources`: List service sources
|
||||
- `get-service-source`: Get service source
|
||||
- `add-service-source`: Add service source
|
||||
- `update-service-source`: Update service source
|
||||
|
||||
### Plugin Management
|
||||
- `get-plugin`: Get plugin configuration
|
||||
- `delete-plugin`: Delete plugin
|
||||
- `update-request-block-plugin`: Update request block configuration
|
||||
|
||||
## Configuration Parameters
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `higressURL` | string | Required | Higress Console URL address |
|
||||
| `username` | string | Required | Higress Console login username |
|
||||
| `password` | string | Required | Higress Console login password |
|
||||
| `description` | string | Optional | MCP Server description |
|
||||
|
||||
Configuration Example:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
annotations:
|
||||
meta.helm.sh/release-name: higress
|
||||
meta.helm.sh/release-namespace: higress-system
|
||||
labels:
|
||||
app: higress-gateway
|
||||
app.kubernetes.io/managed-by: Helm
|
||||
app.kubernetes.io/name: higress-gateway
|
||||
app.kubernetes.io/version: 2.1.4
|
||||
helm.sh/chart: higress-core-2.1.4
|
||||
higress: higress-system-higress-gateway
|
||||
name: higress-config
|
||||
namespace: higress-system
|
||||
data:
|
||||
higress: |-
|
||||
mcpServer:
|
||||
sse_path_suffix: /sse # SSE connection path suffix
|
||||
enable: true # Enable MCP Server
|
||||
redis:
|
||||
address: redis-stack-server.higress-system.svc.cluster.local:6379 # Redis service address
|
||||
username: "" # Redis username (optional)
|
||||
password: "" # Redis password (optional)
|
||||
db: 0 # Redis database (optional)
|
||||
match_list: # MCP Server session persistence routing rules (when matching the following paths, it will be recognized as an MCP session and maintained through SSE)
|
||||
- match_rule_domain: "*"
|
||||
match_rule_path: /higress-api
|
||||
match_rule_type: "prefix"
|
||||
servers:
|
||||
- name: higress-api-mcp-server # MCP Server name
|
||||
path: /higress-api # Access path, needs to match the configuration in match_list
|
||||
type: higress-api # Type defined in RegisterServer function
|
||||
config:
|
||||
higressURL: http://higress-console.higress-system.svc.cluster.local:8080
|
||||
username: admin
|
||||
password: admin
|
||||
```
|
||||
@@ -0,0 +1,76 @@
|
||||
package higress_ops
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/higress"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/higress/higress-api/tools"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/higress/higress-api/tools/plugins"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
)
|
||||
|
||||
const Version = "1.0.0"
|
||||
|
||||
func init() {
|
||||
common.GlobalRegistry.RegisterServer("higress-api", &HigressConfig{})
|
||||
}
|
||||
|
||||
type HigressConfig struct {
|
||||
higressURL string
|
||||
username string
|
||||
password string
|
||||
description string
|
||||
}
|
||||
|
||||
func (c *HigressConfig) ParseConfig(config map[string]interface{}) error {
|
||||
higressURL, ok := config["higressURL"].(string)
|
||||
if !ok {
|
||||
return errors.New("missing higressURL")
|
||||
}
|
||||
c.higressURL = higressURL
|
||||
|
||||
username, ok := config["username"].(string)
|
||||
if !ok {
|
||||
return errors.New("missing username")
|
||||
}
|
||||
c.username = username
|
||||
|
||||
password, ok := config["password"].(string)
|
||||
if !ok {
|
||||
return errors.New("missing password")
|
||||
}
|
||||
c.password = password
|
||||
|
||||
if desc, ok := config["description"].(string); ok {
|
||||
c.description = desc
|
||||
} else {
|
||||
c.description = "Higress API MCP Server, which invokes Higress Console APIs to manage resources such as routes, services, and plugins."
|
||||
}
|
||||
|
||||
api.LogDebugf("HigressConfig ParseConfig: higressURL=%s, username=%s, description=%s",
|
||||
c.higressURL, c.username, c.description)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *HigressConfig) NewServer(serverName string) (*common.MCPServer, error) {
|
||||
mcpServer := common.NewMCPServer(
|
||||
serverName,
|
||||
Version,
|
||||
common.WithInstructions("This is a Higress API MCP Server"),
|
||||
)
|
||||
|
||||
// Initialize Higress API client
|
||||
client := higress.NewHigressClient(c.higressURL, c.username, c.password)
|
||||
|
||||
// Register all tools
|
||||
tools.RegisterRouteTools(mcpServer, client)
|
||||
tools.RegisterServiceTools(mcpServer, client)
|
||||
plugins.RegisterCommonPluginTools(mcpServer, client)
|
||||
plugins.RegisterRequestBlockPluginTools(mcpServer, client)
|
||||
|
||||
api.LogInfof("Higress MCP Server initialized: %s", serverName)
|
||||
|
||||
return mcpServer, nil
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/higress"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
// RegisterCommonPluginTools registers all common plugin management tools
|
||||
func RegisterCommonPluginTools(mcpServer *common.MCPServer, client *higress.HigressClient) {
|
||||
// Get plugin configuration
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("get-plugin", "Get configuration for a specific plugin", getPluginConfigSchema()),
|
||||
handleGetPluginConfig(client),
|
||||
)
|
||||
|
||||
// Delete plugin configuration
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("delete-plugin", "Delete configuration for a specific plugin", getPluginConfigSchema()),
|
||||
handleDeletePluginConfig(client),
|
||||
)
|
||||
}
|
||||
|
||||
func handleGetPluginConfig(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
|
||||
// Parse required parameters
|
||||
pluginName, ok := arguments["name"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing or invalid 'name' argument")
|
||||
}
|
||||
|
||||
scope, ok := arguments["scope"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing or invalid 'scope' argument")
|
||||
}
|
||||
|
||||
if !IsValidScope(scope) {
|
||||
return nil, fmt.Errorf("invalid scope '%s', must be one of: %v", scope, ValidScopes)
|
||||
}
|
||||
|
||||
// Parse resource_name (required for non-global scopes)
|
||||
var resourceName string
|
||||
if scope != ScopeGlobal {
|
||||
resourceName, ok = arguments["resource_name"].(string)
|
||||
if !ok || resourceName == "" {
|
||||
return nil, fmt.Errorf("'resource_name' is required for scope '%s'", scope)
|
||||
}
|
||||
}
|
||||
|
||||
// Build API path and make request
|
||||
path := BuildPluginPath(pluginName, scope, resourceName)
|
||||
respBody, err := client.Get(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get plugin config for '%s' at scope '%s': %w", pluginName, scope, err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleDeletePluginConfig(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
|
||||
// Parse required parameters
|
||||
pluginName, ok := arguments["name"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing or invalid 'name' argument")
|
||||
}
|
||||
|
||||
scope, ok := arguments["scope"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing or invalid 'scope' argument")
|
||||
}
|
||||
|
||||
if !IsValidScope(scope) {
|
||||
return nil, fmt.Errorf("invalid scope '%s', must be one of: %v", scope, ValidScopes)
|
||||
}
|
||||
|
||||
// Parse resource_name (required for non-global scopes)
|
||||
var resourceName string
|
||||
if scope != ScopeGlobal {
|
||||
resourceName, ok = arguments["resource_name"].(string)
|
||||
if !ok || resourceName == "" {
|
||||
return nil, fmt.Errorf("'resource_name' is required for scope '%s'", scope)
|
||||
}
|
||||
}
|
||||
|
||||
// Build API path and make request
|
||||
path := BuildPluginPath(pluginName, scope, resourceName)
|
||||
respBody, err := client.Delete(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to delete plugin config for '%s' at scope '%s': %w", pluginName, scope, err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func getPluginConfigSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the plugin"
|
||||
},
|
||||
"scope": {
|
||||
"type": "string",
|
||||
"enum": ["GLOBAL", "DOMAIN", "SERVICE", "ROUTE"],
|
||||
"description": "The scope at which the plugin is applied"
|
||||
},
|
||||
"resource_name": {
|
||||
"type": "string",
|
||||
"description": "The name of the resource (required for DOMAIN, SERVICE, ROUTE scopes)"
|
||||
}
|
||||
},
|
||||
"required": ["name", "scope"],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/higress"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
const RequestBlockPluginName = "request-block"
|
||||
|
||||
// RequestBlockConfig represents the configuration for request-block plugin
|
||||
type RequestBlockConfig struct {
|
||||
BlockBodies []string `json:"block_bodies,omitempty"`
|
||||
BlockHeaders []string `json:"block_headers,omitempty"`
|
||||
BlockUrls []string `json:"block_urls,omitempty"`
|
||||
BlockedCode int `json:"blocked_code,omitempty"`
|
||||
CaseSensitive bool `json:"case_sensitive,omitempty"`
|
||||
}
|
||||
|
||||
// RequestBlockInstance represents a request-block plugin instance
|
||||
type RequestBlockInstance = PluginInstance[RequestBlockConfig]
|
||||
|
||||
// RequestBlockResponse represents the API response for request-block plugin
|
||||
type RequestBlockResponse = higress.APIResponse[RequestBlockInstance]
|
||||
|
||||
// RegisterRequestBlockPluginTools registers all request block plugin management tools
|
||||
func RegisterRequestBlockPluginTools(mcpServer *common.MCPServer, client *higress.HigressClient) {
|
||||
// Update request block configuration
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema(fmt.Sprintf("update-%s-plugin", RequestBlockPluginName), "Update request block plugin configuration", getAddOrUpdateRequestBlockConfigSchema()),
|
||||
handleAddOrUpdateRequestBlockConfig(client),
|
||||
)
|
||||
}
|
||||
|
||||
func handleAddOrUpdateRequestBlockConfig(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
|
||||
// Parse required parameters
|
||||
scope, ok := arguments["scope"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing or invalid 'scope' argument")
|
||||
}
|
||||
|
||||
if !IsValidScope(scope) {
|
||||
return nil, fmt.Errorf("invalid scope '%s', must be one of: %v", scope, ValidScopes)
|
||||
}
|
||||
|
||||
enabled, ok := arguments["enabled"].(bool)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing or invalid 'enabled' argument")
|
||||
}
|
||||
|
||||
configurations, ok := arguments["configurations"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing 'configurations' argument")
|
||||
}
|
||||
|
||||
// Parse resource_name for non-global scopes
|
||||
var resourceName string
|
||||
if scope != ScopeGlobal {
|
||||
// Validate and get resource_name
|
||||
resourceName, ok = arguments["resource_name"].(string)
|
||||
if !ok || resourceName == "" {
|
||||
return nil, fmt.Errorf("'resource_name' is required for scope '%s'", scope)
|
||||
}
|
||||
}
|
||||
|
||||
// Build API path
|
||||
path := BuildPluginPath(RequestBlockPluginName, scope, resourceName)
|
||||
|
||||
// Get current request block configuration to merge with updates
|
||||
currentBody, err := client.Get(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get current request block configuration: %w", err)
|
||||
}
|
||||
|
||||
var response RequestBlockResponse
|
||||
if err := json.Unmarshal(currentBody, &response); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse current request block response: %w", err)
|
||||
}
|
||||
|
||||
currentConfig := response.Data
|
||||
currentConfig.Enabled = enabled
|
||||
currentConfig.Scope = scope
|
||||
|
||||
// Convert the input configurations to RequestBlockConfig and merge
|
||||
configBytes, err := json.Marshal(configurations)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal configurations: %w", err)
|
||||
}
|
||||
|
||||
var newConfig RequestBlockConfig
|
||||
if err := json.Unmarshal(configBytes, &newConfig); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse request block configurations: %w", err)
|
||||
}
|
||||
|
||||
// Update configurations (overwrite with new values where provided)
|
||||
if newConfig.BlockBodies != nil {
|
||||
currentConfig.Configurations.BlockBodies = newConfig.BlockBodies
|
||||
}
|
||||
if newConfig.BlockHeaders != nil {
|
||||
currentConfig.Configurations.BlockHeaders = newConfig.BlockHeaders
|
||||
}
|
||||
if newConfig.BlockUrls != nil {
|
||||
currentConfig.Configurations.BlockUrls = newConfig.BlockUrls
|
||||
}
|
||||
if newConfig.BlockedCode != 0 {
|
||||
currentConfig.Configurations.BlockedCode = newConfig.BlockedCode
|
||||
}
|
||||
currentConfig.Configurations.CaseSensitive = newConfig.CaseSensitive
|
||||
|
||||
respBody, err := client.Put(path, currentConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update request block config at scope '%s': %w", scope, err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func getAddOrUpdateRequestBlockConfigSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"scope": {
|
||||
"type": "string",
|
||||
"enum": ["GLOBAL", "DOMAIN", "SERVICE", "ROUTE"],
|
||||
"description": "The scope at which the plugin is applied"
|
||||
},
|
||||
|
||||
"resource_name": {
|
||||
"type": "string",
|
||||
"description": "The name of the resource (required for DOMAIN, SERVICE, ROUTE scopes)"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the plugin is enabled"
|
||||
},
|
||||
"configurations": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"block_bodies": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "List of patterns to match against request body content"
|
||||
},
|
||||
"block_headers": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "List of patterns to match against request headers"
|
||||
},
|
||||
"block_urls": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "List of patterns to match against request URLs"
|
||||
},
|
||||
"blocked_code": {
|
||||
"type": "integer",
|
||||
"minimum": 100,
|
||||
"maximum": 599,
|
||||
"description": "HTTP status code to return when a block is matched"
|
||||
},
|
||||
"case_sensitive": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the block matching is case sensitive"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["scope", "enabled", "configurations"],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package plugins
|
||||
|
||||
// PluginTargets represents the targets for different scopes
|
||||
type PluginTargets struct {
|
||||
Domain string `json:"DOMAIN,omitempty"`
|
||||
Service string `json:"SERVICE,omitempty"`
|
||||
Route string `json:"ROUTE,omitempty"`
|
||||
}
|
||||
|
||||
// PluginInstance represents a plugin instance configuration
|
||||
type PluginInstance[T any] struct {
|
||||
Version string `json:"version,omitempty"`
|
||||
Scope string `json:"scope"`
|
||||
Target string `json:"target,omitempty"`
|
||||
Targets PluginTargets `json:"targets,omitempty"`
|
||||
PluginName string `json:"pluginName,omitempty"`
|
||||
PluginVersion string `json:"pluginVersion,omitempty"`
|
||||
Internal bool `json:"internal,omitempty"`
|
||||
Enabled bool `json:"enabled"`
|
||||
RawConfigurations string `json:"rawConfigurations,omitempty"`
|
||||
Configurations T `json:"configurations,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package plugins
|
||||
|
||||
import "fmt"
|
||||
|
||||
const (
|
||||
ScopeGlobal = "GLOBAL"
|
||||
ScopeDomain = "DOMAIN"
|
||||
ScopeService = "SERVICE"
|
||||
ScopeRoute = "ROUTE"
|
||||
)
|
||||
|
||||
// ValidScopes contains all valid plugin scopes
|
||||
var ValidScopes = []string{ScopeGlobal, ScopeDomain, ScopeService, ScopeRoute}
|
||||
|
||||
// IsValidScope checks if the given scope is valid
|
||||
func IsValidScope(scope string) bool {
|
||||
for _, validScope := range ValidScopes {
|
||||
if scope == validScope {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// BuildPluginPath builds the API path for plugin operations based on scope and resource
|
||||
func BuildPluginPath(pluginName, scope, resourceName string) string {
|
||||
switch scope {
|
||||
case ScopeGlobal:
|
||||
return fmt.Sprintf("/v1/global/plugin-instances/%s", pluginName)
|
||||
case ScopeDomain:
|
||||
return fmt.Sprintf("/v1/domains/%s/plugin-instances/%s", resourceName, pluginName)
|
||||
case ScopeService:
|
||||
return fmt.Sprintf("/v1/services/%s/plugin-instances/%s", resourceName, pluginName)
|
||||
case ScopeRoute:
|
||||
return fmt.Sprintf("/v1/routes/%s/plugin-instances/%s", resourceName, pluginName)
|
||||
default:
|
||||
return fmt.Sprintf("/v1/global/plugin-instances/%s", pluginName)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,456 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/higress"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
// Route represents a route configuration
|
||||
type Route struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Domains []string `json:"domains,omitempty"`
|
||||
Path *RoutePath `json:"path,omitempty"`
|
||||
Methods []string `json:"methods,omitempty"`
|
||||
Headers []RouteMatch `json:"headers,omitempty"`
|
||||
URLParams []RouteMatch `json:"urlParams,omitempty"`
|
||||
Services []RouteService `json:"services,omitempty"`
|
||||
AuthConfig *RouteAuthConfig `json:"authConfig,omitempty"`
|
||||
CustomConfigs map[string]interface{} `json:"customConfigs,omitempty"`
|
||||
}
|
||||
|
||||
// RoutePath represents path matching configuration
|
||||
type RoutePath struct {
|
||||
MatchType string `json:"matchType"`
|
||||
MatchValue string `json:"matchValue"`
|
||||
CaseSensitive bool `json:"caseSensitive,omitempty"`
|
||||
}
|
||||
|
||||
// RouteMatch represents header or URL parameter matching configuration
|
||||
type RouteMatch struct {
|
||||
Key string `json:"key"`
|
||||
MatchType string `json:"matchType"`
|
||||
MatchValue string `json:"matchValue"`
|
||||
}
|
||||
|
||||
// RouteService represents a service in the route
|
||||
type RouteService struct {
|
||||
Name string `json:"name"`
|
||||
Port int `json:"port"`
|
||||
Weight int `json:"weight"`
|
||||
}
|
||||
|
||||
// RouteAuthConfig represents authentication configuration for a route
|
||||
type RouteAuthConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
AllowedConsumers []string `json:"allowedConsumers,omitempty"`
|
||||
}
|
||||
|
||||
// RouteResponse represents the API response for route operations
|
||||
type RouteResponse = higress.APIResponse[Route]
|
||||
|
||||
// RegisterRouteTools registers all route management tools
|
||||
func RegisterRouteTools(mcpServer *common.MCPServer, client *higress.HigressClient) {
|
||||
// List all routes
|
||||
mcpServer.AddTool(
|
||||
mcp.NewTool("list-routes", mcp.WithDescription("List all available routes")),
|
||||
handleListRoutes(client),
|
||||
)
|
||||
|
||||
// Get specific route
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("get-route", "Get detailed information about a specific route", getRouteSchema()),
|
||||
handleGetRoute(client),
|
||||
)
|
||||
|
||||
// Add new route
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("add-route", "Add a new route", getAddRouteSchema()),
|
||||
handleAddRoute(client),
|
||||
)
|
||||
|
||||
// Update existing route
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("update-route", "Update an existing route", getUpdateRouteSchema()),
|
||||
handleUpdateRoute(client),
|
||||
)
|
||||
|
||||
// Delete existing route
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("delete-route", "Delete an existing route", getRouteSchema()),
|
||||
handleDeleteRoute(client),
|
||||
)
|
||||
}
|
||||
|
||||
func handleListRoutes(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
respBody, err := client.Get("/v1/routes")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list routes: %w", err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetRoute(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
name, ok := arguments["name"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing or invalid 'name' argument")
|
||||
}
|
||||
|
||||
respBody, err := client.Get(fmt.Sprintf("/v1/routes/%s", name))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get route '%s': %w", name, err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleAddRoute(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
configurations, ok := arguments["configurations"].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing or invalid 'configurations' argument")
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if _, ok := configurations["name"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'name' in configurations")
|
||||
}
|
||||
if _, ok := configurations["path"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'path' in configurations")
|
||||
}
|
||||
if _, ok := configurations["services"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'services' in configurations")
|
||||
}
|
||||
|
||||
respBody, err := client.Post("/v1/routes", configurations)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to add route: %w", err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleUpdateRoute(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
name, ok := arguments["name"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing or invalid 'name' argument")
|
||||
}
|
||||
|
||||
configurations, ok := arguments["configurations"].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing or invalid 'configurations' argument")
|
||||
}
|
||||
|
||||
// Get current route configuration to merge with updates
|
||||
currentBody, err := client.Get(fmt.Sprintf("/v1/routes/%s", name))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get current route configuration: %w", err)
|
||||
}
|
||||
|
||||
var response RouteResponse
|
||||
if err := json.Unmarshal(currentBody, &response); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse current route response: %w", err)
|
||||
}
|
||||
|
||||
currentConfig := response.Data
|
||||
|
||||
// Update configurations using JSON marshal/unmarshal for type conversion
|
||||
configBytes, err := json.Marshal(configurations)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal configurations: %w", err)
|
||||
}
|
||||
|
||||
var newConfig Route
|
||||
if err := json.Unmarshal(configBytes, &newConfig); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse route configurations: %w", err)
|
||||
}
|
||||
|
||||
// Merge configurations (overwrite with new values where provided)
|
||||
if newConfig.Domains != nil {
|
||||
currentConfig.Domains = newConfig.Domains
|
||||
}
|
||||
if newConfig.Path != nil {
|
||||
currentConfig.Path = newConfig.Path
|
||||
}
|
||||
if newConfig.Methods != nil {
|
||||
currentConfig.Methods = newConfig.Methods
|
||||
}
|
||||
if newConfig.Headers != nil {
|
||||
currentConfig.Headers = newConfig.Headers
|
||||
}
|
||||
if newConfig.URLParams != nil {
|
||||
currentConfig.URLParams = newConfig.URLParams
|
||||
}
|
||||
if newConfig.Services != nil {
|
||||
currentConfig.Services = newConfig.Services
|
||||
}
|
||||
if newConfig.AuthConfig != nil {
|
||||
currentConfig.AuthConfig = newConfig.AuthConfig
|
||||
}
|
||||
if newConfig.CustomConfigs != nil {
|
||||
currentConfig.CustomConfigs = newConfig.CustomConfigs
|
||||
}
|
||||
|
||||
respBody, err := client.Put(fmt.Sprintf("/v1/routes/%s", name), currentConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update route '%s': %w", name, err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleDeleteRoute(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
name, ok := arguments["name"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing or invalid 'name' argument")
|
||||
}
|
||||
|
||||
respBody, err := client.Delete(fmt.Sprintf("/v1/routes/%s", name))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to delete route '%s': %w", name, err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func getRouteSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the route"
|
||||
}
|
||||
},
|
||||
"required": ["name"],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
|
||||
func getAddRouteSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"configurations": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the route"
|
||||
},
|
||||
"domains": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "List of domain names, but only one domain is allowed"
|
||||
},
|
||||
"path": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"matchType": {"type": "string", "enum": ["PRE", "EQUAL", "REGULAR"], "description": "Match type of path"},
|
||||
"matchValue": {"type": "string", "description": "Value to match"},
|
||||
"caseSensitive": {"type": "boolean", "description": "Whether matching is case sensitive"}
|
||||
},
|
||||
"required": ["matchType", "matchValue"],
|
||||
"description": "List of path match conditions"
|
||||
},
|
||||
"methods": {
|
||||
"type": "array",
|
||||
"items": {"type": "string", "enum": ["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE", "CONNECT"]},
|
||||
"description": "List of HTTP methods"
|
||||
},
|
||||
"headers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"matchType": {"type": "string", "enum": ["PRE", "EQUAL", "REGULAR"], "description": "Match type of header"},
|
||||
"matchValue": {"type": "string", "description": "Value to match"},
|
||||
"caseSensitive": {"type": "boolean", "description": "Whether matching is case sensitive"},
|
||||
"key": {"type": "string", "description": "Header key name"}
|
||||
},
|
||||
"required": ["matchType", "matchValue", "key"]
|
||||
},
|
||||
"description": "List of header match conditions"
|
||||
},
|
||||
"urlParams": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"matchType": {"type": "string", "enum": ["PRE", "EQUAL", "REGULAR"], "description": "Match type of URL parameter"},
|
||||
"matchValue": {"type": "string", "description": "Value to match"},
|
||||
"caseSensitive": {"type": "boolean", "description": "Whether matching is case sensitive"},
|
||||
"key": {"type": "string", "description": "Parameter key name"}
|
||||
},
|
||||
"required": ["matchType", "matchValue", "key"]
|
||||
},
|
||||
"description": "List of URL parameter match conditions"
|
||||
},
|
||||
"services": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string", "description": "Service name"},
|
||||
"port": {"type": "integer", "description": "Service port"},
|
||||
"weight": {"type": "integer", "description": "Service weight"}
|
||||
},
|
||||
"required": ["name", "port", "weight"]
|
||||
},
|
||||
"description": "List of services for this route"
|
||||
},
|
||||
"customConfigs": {
|
||||
"type": "object",
|
||||
"additionalProperties": {"type": "string"},
|
||||
"description": "Dictionary of custom configurations"
|
||||
}
|
||||
},
|
||||
"required": ["name", "path", "services"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["configurations"],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
|
||||
func getUpdateRouteSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the route"
|
||||
},
|
||||
"configurations": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domains": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "List of domain names, but only one domain is allowed",
|
||||
"maxItems": 1
|
||||
},
|
||||
"path": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"matchType": {"type": "string", "enum": ["PRE", "EQUAL", "REGULAR"], "description": "Match type of path"},
|
||||
"matchValue": {"type": "string", "description": "Value to match"},
|
||||
"caseSensitive": {"type": "boolean", "description": "Whether matching is case sensitive"}
|
||||
},
|
||||
"required": ["matchType", "matchValue"],
|
||||
"description": "The path configuration"
|
||||
},
|
||||
"methods": {
|
||||
"type": "array",
|
||||
"items": {"type": "string", "enum": ["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE", "CONNECT"]},
|
||||
"description": "List of HTTP methods"
|
||||
},
|
||||
"headers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"matchType": {"type": "string", "enum": ["PRE", "EQUAL", "REGULAR"], "description": "Match type of header"},
|
||||
"matchValue": {"type": "string", "description": "Value to match"},
|
||||
"caseSensitive": {"type": "boolean", "description": "Whether matching is case sensitive"},
|
||||
"key": {"type": "string", "description": "Header key name"}
|
||||
},
|
||||
"required": ["matchType", "matchValue", "key"]
|
||||
},
|
||||
"description": "List of header match conditions"
|
||||
},
|
||||
"urlParams": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"matchType": {"type": "string", "enum": ["PRE", "EQUAL", "REGULAR"], "description": "Match type of URL parameter"},
|
||||
"matchValue": {"type": "string", "description": "Value to match"},
|
||||
"caseSensitive": {"type": "boolean", "description": "Whether matching is case sensitive"},
|
||||
"key": {"type": "string", "description": "Parameter key name"}
|
||||
},
|
||||
"required": ["matchType", "matchValue", "key"]
|
||||
},
|
||||
"description": "List of URL parameter match conditions"
|
||||
},
|
||||
"services": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string", "description": "Service name"},
|
||||
"port": {"type": "integer", "description": "Service port"},
|
||||
"weight": {"type": "integer", "description": "Service weight"}
|
||||
},
|
||||
"required": ["name", "port", "weight"]
|
||||
},
|
||||
"description": "List of services for this route"
|
||||
},
|
||||
"customConfigs": {
|
||||
"type": "object",
|
||||
"additionalProperties": {"type": "string"},
|
||||
"description": "Dictionary of custom configurations"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["name", "configurations"],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/higress"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
// ServiceSource represents a service source configuration
|
||||
type ServiceSource struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Domain string `json:"domain"`
|
||||
Port int `json:"port"`
|
||||
Protocol string `json:"protocol,omitempty"`
|
||||
SNI *string `json:"sni,omitempty"`
|
||||
Properties map[string]interface{} `json:"properties,omitempty"`
|
||||
AuthN *ServiceSourceAuthN `json:"authN,omitempty"`
|
||||
Valid bool `json:"valid,omitempty"`
|
||||
}
|
||||
|
||||
// ServiceSourceAuthN represents authentication configuration for service source
|
||||
type ServiceSourceAuthN struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Properties map[string]interface{} `json:"properties,omitempty"`
|
||||
}
|
||||
|
||||
// ServiceSourceResponse represents the API response for service source operations
|
||||
type ServiceSourceResponse = higress.APIResponse[ServiceSource]
|
||||
|
||||
// RegisterServiceTools registers all service source management tools
|
||||
func RegisterServiceTools(mcpServer *common.MCPServer, client *higress.HigressClient) {
|
||||
// List all service sources
|
||||
mcpServer.AddTool(
|
||||
mcp.NewTool("list-service-sources", mcp.WithDescription("List all available service sources")),
|
||||
handleListServiceSources(client),
|
||||
)
|
||||
|
||||
// Get specific service source
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("get-service-source", "Get detailed information about a specific service source", getServiceSourceSchema()),
|
||||
handleGetServiceSource(client),
|
||||
)
|
||||
|
||||
// Add new service source
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("add-service-source", "Add a new service source", getAddServiceSourceSchema()),
|
||||
handleAddServiceSource(client),
|
||||
)
|
||||
|
||||
// Update existing service source
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("update-service-source", "Update an existing service source", getUpdateServiceSourceSchema()),
|
||||
handleUpdateServiceSource(client),
|
||||
)
|
||||
|
||||
// Delete existing service source
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("delete-service-source", "Delete an existing service source", getServiceSourceSchema()),
|
||||
handleDeleteServiceSource(client),
|
||||
)
|
||||
}
|
||||
|
||||
func handleListServiceSources(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
respBody, err := client.Get("/v1/service-sources")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list service sources: %w", err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetServiceSource(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
name, ok := arguments["name"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing or invalid 'name' argument")
|
||||
}
|
||||
|
||||
respBody, err := client.Get(fmt.Sprintf("/v1/service-sources/%s", name))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get service source '%s': %w", name, err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleAddServiceSource(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
configurations, ok := arguments["configurations"].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing or invalid 'configurations' argument")
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if _, ok := configurations["name"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'name' in configurations")
|
||||
}
|
||||
if _, ok := configurations["type"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'type' in configurations")
|
||||
}
|
||||
if _, ok := configurations["domain"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'domain' in configurations")
|
||||
}
|
||||
if _, ok := configurations["port"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'port' in configurations")
|
||||
}
|
||||
|
||||
respBody, err := client.Post("/v1/service-sources", configurations)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to add service source: %w", err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleUpdateServiceSource(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
name, ok := arguments["name"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing or invalid 'name' argument")
|
||||
}
|
||||
|
||||
configurations, ok := arguments["configurations"].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing or invalid 'configurations' argument")
|
||||
}
|
||||
|
||||
// Get current service source configuration to merge with updates
|
||||
currentBody, err := client.Get(fmt.Sprintf("/v1/service-sources/%s", name))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get current service source configuration: %w", err)
|
||||
}
|
||||
|
||||
var response ServiceSourceResponse
|
||||
if err := json.Unmarshal(currentBody, &response); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse current service source response: %w", err)
|
||||
}
|
||||
|
||||
currentConfig := response.Data
|
||||
|
||||
// Update configurations using JSON marshal/unmarshal for type conversion
|
||||
configBytes, err := json.Marshal(configurations)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal configurations: %w", err)
|
||||
}
|
||||
|
||||
var newConfig ServiceSource
|
||||
if err := json.Unmarshal(configBytes, &newConfig); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse service source configurations: %w", err)
|
||||
}
|
||||
|
||||
// Merge configurations (overwrite with new values where provided)
|
||||
if newConfig.Name != "" {
|
||||
currentConfig.Name = newConfig.Name
|
||||
}
|
||||
if newConfig.Type != "" {
|
||||
currentConfig.Type = newConfig.Type
|
||||
}
|
||||
if newConfig.Domain != "" {
|
||||
currentConfig.Domain = newConfig.Domain
|
||||
}
|
||||
if newConfig.Port != 0 {
|
||||
currentConfig.Port = newConfig.Port
|
||||
}
|
||||
if newConfig.Protocol != "" {
|
||||
currentConfig.Protocol = newConfig.Protocol
|
||||
}
|
||||
if newConfig.SNI != nil {
|
||||
currentConfig.SNI = newConfig.SNI
|
||||
}
|
||||
if newConfig.Properties != nil {
|
||||
currentConfig.Properties = newConfig.Properties
|
||||
}
|
||||
if newConfig.AuthN != nil {
|
||||
currentConfig.AuthN = newConfig.AuthN
|
||||
}
|
||||
|
||||
respBody, err := client.Put(fmt.Sprintf("/v1/service-sources/%s", name), currentConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update service source '%s': %w", name, err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleDeleteServiceSource(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
name, ok := arguments["name"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing or invalid 'name' argument")
|
||||
}
|
||||
|
||||
respBody, err := client.Delete(fmt.Sprintf("/v1/service-sources/%s", name))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to delete service source '%s': %w", name, err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func getServiceSourceSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the service source to retrieve"
|
||||
}
|
||||
},
|
||||
"required": ["name"],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
|
||||
// TODO: extend other types of service sources, e.g., nacos, zookeeper, euraka.
|
||||
func getAddServiceSourceSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"configurations": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the service source"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["static", "dns"],
|
||||
"description": "The type of service source: 'static' for static IPs, 'dns' for DNS resolution"
|
||||
},
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "The domain name or IP address (required)"
|
||||
},
|
||||
"port": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 65535,
|
||||
"description": "The port number (required)"
|
||||
},
|
||||
"protocol": {
|
||||
"type": "string",
|
||||
"enum": ["http", "https"],
|
||||
"description": "The protocol to use (optional, defaults to http)"
|
||||
},
|
||||
"sni": {
|
||||
"type": "string",
|
||||
"description": "Server Name Indication for HTTPS connections (optional)"
|
||||
}
|
||||
},
|
||||
"required": ["name", "type", "domain", "port"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["configurations"],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
|
||||
// TODO: extend other types of service sources, e.g., nacos, zookeeper, euraka.
|
||||
func getUpdateServiceSourceSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the service source to update"
|
||||
},
|
||||
"configurations": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["static", "dns"],
|
||||
"description": "The type of service source: 'static' for static IPs, 'dns' for DNS resolution"
|
||||
},
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "The domain name or IP address"
|
||||
},
|
||||
"port": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 65535,
|
||||
"description": "The port number"
|
||||
},
|
||||
"protocol": {
|
||||
"type": "string",
|
||||
"enum": ["http", "https"],
|
||||
"description": "The protocol to use (optional, defaults to http)"
|
||||
},
|
||||
"sni": {
|
||||
"type": "string",
|
||||
"description": "Server Name Indication for HTTPS connections"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["name", "configurations"],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package higress
|
||||
|
||||
// APIResponse represents the standard Higress API response format
|
||||
type APIResponse[T any] struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Data T `json:"data,omitempty"`
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -94,13 +95,15 @@ func (s *SSEServer) HandleSSE(cb api.FilterCallbackHandler, stopChan chan struct
|
||||
defer s.sessions.Delete(sessionID)
|
||||
|
||||
channel := GetSSEChannelName(sessionID)
|
||||
u, err := url.Parse(s.baseURL + s.messageEndpoint)
|
||||
if err != nil {
|
||||
api.LogErrorf("Failed to parse base URL: %v", err)
|
||||
}
|
||||
|
||||
messageEndpoint := fmt.Sprintf(
|
||||
"%s%s?sessionId=%s",
|
||||
s.baseURL,
|
||||
s.messageEndpoint,
|
||||
sessionID,
|
||||
)
|
||||
q := u.Query()
|
||||
q.Set("sessionId", sessionID)
|
||||
u.RawQuery = q.Encode()
|
||||
messageEndpoint := u.String()
|
||||
|
||||
// go func() {
|
||||
// for {
|
||||
@@ -126,7 +129,7 @@ func (s *SSEServer) HandleSSE(cb api.FilterCallbackHandler, stopChan chan struct
|
||||
// }
|
||||
// }()
|
||||
|
||||
err := s.redisClient.Subscribe(channel, stopChan, func(message string) {
|
||||
err = s.redisClient.Subscribe(channel, stopChan, func(message string) {
|
||||
defer cb.EncoderFilterCallbacks().RecoverPanic()
|
||||
api.LogDebugf("SSE Send message: %s", message)
|
||||
cb.EncoderFilterCallbacks().InjectData([]byte(message))
|
||||
@@ -210,7 +213,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 != "" {
|
||||
if sessionID != "" {
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
status = http.StatusAccepted
|
||||
} else {
|
||||
|
||||
@@ -64,7 +64,7 @@ func (p *Parser) Parse(any *anypb.Any, callbacks api.ConfigCallbackHandler) (int
|
||||
|
||||
redisClient, err := common.NewRedisClient(redisConfig)
|
||||
if err != nil {
|
||||
api.LogErrorf("Failed to initialize Redis client: %w", err)
|
||||
api.LogErrorf("Failed to initialize Redis client: %v", err)
|
||||
} else {
|
||||
api.LogDebug("Redis client initialized")
|
||||
}
|
||||
|
||||
@@ -129,9 +129,15 @@ func (f *filter) processMcpRequestHeadersForRestUpstream(header api.RequestHeade
|
||||
if method != http.MethodGet {
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusMethodNotAllowed, "Method not allowed", nil, 0, "")
|
||||
} else {
|
||||
// to support the query param in Message Endpoint
|
||||
trimmed := strings.TrimSuffix(requestUrl.Path, GlobalSSEPathSuffix)
|
||||
if rq := requestUrl.RawQuery; rq != "" {
|
||||
trimmed += "?" + rq
|
||||
}
|
||||
|
||||
f.config.defaultServer = common.NewSSEServer(common.NewMCPServer(DefaultServerName, Version),
|
||||
common.WithSSEEndpoint(GlobalSSEPathSuffix),
|
||||
common.WithMessageEndpoint(strings.TrimSuffix(requestUrl.Path, GlobalSSEPathSuffix)),
|
||||
common.WithMessageEndpoint(trimmed),
|
||||
common.WithRedisClient(f.config.redisClient))
|
||||
f.serverName = f.config.defaultServer.GetServerName()
|
||||
body := "SSE connection create"
|
||||
@@ -391,48 +397,93 @@ func (f *filter) findNextLineBreak(bufferData string) (error, string) {
|
||||
}
|
||||
|
||||
func (f *filter) findEndpointUrl(bufferData string) (error, string) {
|
||||
eventIndex := strings.Index(bufferData, "event:")
|
||||
if eventIndex == -1 {
|
||||
return nil, ""
|
||||
// Keep searching for events until we find an endpoint event or run out of data
|
||||
for {
|
||||
eventIndex := strings.Index(bufferData, "event:")
|
||||
if eventIndex == -1 {
|
||||
// No more events found
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
// Move to the start of the event
|
||||
bufferData = bufferData[eventIndex:]
|
||||
|
||||
// Find the end of the event line
|
||||
err, lineBreak := f.findNextLineBreak(bufferData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find endpoint URL in SSE data: %v", err), ""
|
||||
}
|
||||
if lineBreak == "" {
|
||||
// No line break found, which means the data is not enough.
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
api.LogDebugf("event line break sequence: %v", []byte(lineBreak))
|
||||
eventEndIndex := strings.Index(bufferData, lineBreak)
|
||||
if eventEndIndex == -1 {
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
eventName := strings.TrimSpace(bufferData[len("event:"):eventEndIndex])
|
||||
|
||||
// Move past the event line
|
||||
bufferData = bufferData[eventEndIndex+len(lineBreak):]
|
||||
|
||||
if eventName == "endpoint" {
|
||||
// Found endpoint event, now look for the data field
|
||||
err, lineBreak = f.findNextLineBreak(bufferData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find endpoint URL in SSE data: %v", err), ""
|
||||
}
|
||||
if lineBreak == "" {
|
||||
// No line break found, which means the data is not enough.
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
api.LogDebugf("data line break sequence: %v", []byte(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:"):])
|
||||
} else {
|
||||
// Not an endpoint event, skip to the next event
|
||||
api.LogDebugf("Skipping non-endpoint event: %s", eventName)
|
||||
|
||||
// First, we need to skip the data field of this event
|
||||
err, lineBreak = f.findNextLineBreak(bufferData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find endpoint URL in SSE data: %v", err), ""
|
||||
}
|
||||
if lineBreak == "" {
|
||||
// No line break found, which means the data is not enough.
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
dataEndIndex := strings.Index(bufferData, lineBreak)
|
||||
if dataEndIndex == -1 {
|
||||
// Data received not enough.
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
// Move past the data line
|
||||
bufferData = bufferData[dataEndIndex+len(lineBreak):]
|
||||
|
||||
// Skip any additional empty lines that separate events
|
||||
for strings.HasPrefix(bufferData, lineBreak) {
|
||||
bufferData = bufferData[len(lineBreak):]
|
||||
}
|
||||
|
||||
// Continue to look for the next event
|
||||
}
|
||||
}
|
||||
bufferData = bufferData[eventIndex:]
|
||||
err, lineBreak := f.findNextLineBreak(bufferData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find endpoint URL in SSE data: %v", err), ""
|
||||
}
|
||||
if lineBreak == "" {
|
||||
// No line break found, which means the data is not enough.
|
||||
return nil, ""
|
||||
}
|
||||
api.LogDebugf("event line break sequence: %v", []byte(lineBreak))
|
||||
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):]
|
||||
err, lineBreak = f.findNextLineBreak(bufferData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find endpoint URL in SSE data: %v", err), ""
|
||||
}
|
||||
if lineBreak == "" {
|
||||
// No line break found, which means the data is not enough.
|
||||
return nil, ""
|
||||
}
|
||||
api.LogDebugf("data line break sequence: %v", []byte(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
|
||||
|
||||
464
plugins/golang-filter/mcp-session/filter_test.go
Normal file
464
plugins/golang-filter/mcp-session/filter_test.go
Normal file
@@ -0,0 +1,464 @@
|
||||
package mcp_session
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
)
|
||||
|
||||
// Mock implementation of CommonCAPI for testing
|
||||
type mockCommonCAPI struct {
|
||||
logs []string
|
||||
}
|
||||
|
||||
func (m *mockCommonCAPI) Log(level api.LogType, message string) {
|
||||
fmt.Printf("[%s] %s", level, message)
|
||||
m.logs = append(m.logs, message)
|
||||
}
|
||||
|
||||
func (m *mockCommonCAPI) LogLevel() api.LogType {
|
||||
return api.Debug
|
||||
}
|
||||
|
||||
// Test helper to create a filter instance for testing
|
||||
func createTestFilter() *filter {
|
||||
return &filter{}
|
||||
}
|
||||
|
||||
// Test helper to create a match rule for testing
|
||||
func createTestMatchRule() common.MatchRule {
|
||||
return common.MatchRule{
|
||||
UpstreamType: common.SSEUpstream,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/api/v1",
|
||||
MatchRulePath: "/mcp",
|
||||
MatchRuleType: common.PrefixMatch,
|
||||
MatchRuleDomain: "example.com",
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindEndpointUrl_ValidEndpointMessage tests the current behavior with a valid endpoint message
|
||||
func TestFindEndpointUrl_ValidEndpointMessage(t *testing.T) {
|
||||
// Setup mock API
|
||||
mockAPI := &mockCommonCAPI{}
|
||||
api.SetCommonCAPI(mockAPI)
|
||||
|
||||
f := createTestFilter()
|
||||
|
||||
// Test with valid endpoint message
|
||||
sseData := "event: endpoint\ndata: https://api.example.com/chat\n\n"
|
||||
|
||||
err, endpointUrl := f.findEndpointUrl(sseData)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got: %v", err)
|
||||
}
|
||||
|
||||
expectedUrl := "https://api.example.com/chat"
|
||||
if endpointUrl != expectedUrl {
|
||||
t.Errorf("Expected endpoint URL '%s', got '%s'", expectedUrl, endpointUrl)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindEndpointUrl_NonEndpointFirstMessage tests improved behavior with non-endpoint first message
|
||||
func TestFindEndpointUrl_NonEndpointFirstMessage(t *testing.T) {
|
||||
// Setup mock API
|
||||
mockAPI := &mockCommonCAPI{}
|
||||
api.SetCommonCAPI(mockAPI)
|
||||
|
||||
f := createTestFilter()
|
||||
|
||||
// Test with ping message first (this should now succeed with improved implementation)
|
||||
sseData := "event: ping\ndata: alive\n\nevent: endpoint\ndata: https://api.example.com/chat\n\n"
|
||||
|
||||
err, endpointUrl := f.findEndpointUrl(sseData)
|
||||
|
||||
// Improved implementation should handle non-endpoint first message
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error for non-endpoint first message, got: %v", err)
|
||||
}
|
||||
|
||||
expectedUrl := "https://api.example.com/chat"
|
||||
if endpointUrl != expectedUrl {
|
||||
t.Errorf("Expected endpoint URL '%s', got '%s'", expectedUrl, endpointUrl)
|
||||
}
|
||||
|
||||
// Check that the non-endpoint event was logged
|
||||
found := false
|
||||
for _, log := range mockAPI.logs {
|
||||
if log == "Skipping non-endpoint event: ping" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Expected log message about skipping ping event not found")
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindEndpointUrl_MultipleNonEndpointMessages tests multiple non-endpoint messages before endpoint
|
||||
func TestFindEndpointUrl_MultipleNonEndpointMessages(t *testing.T) {
|
||||
// Setup mock API
|
||||
mockAPI := &mockCommonCAPI{}
|
||||
api.SetCommonCAPI(mockAPI)
|
||||
|
||||
f := createTestFilter()
|
||||
|
||||
// Test with multiple non-endpoint messages before endpoint
|
||||
sseData := "event: ping\ndata: alive\n\nevent: status\ndata: connecting\n\nevent: info\ndata: ready\n\nevent: endpoint\ndata: https://api.example.com/chat\n\n"
|
||||
|
||||
err, endpointUrl := f.findEndpointUrl(sseData)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got: %v", err)
|
||||
}
|
||||
|
||||
expectedUrl := "https://api.example.com/chat"
|
||||
if endpointUrl != expectedUrl {
|
||||
t.Errorf("Expected endpoint URL '%s', got '%s'", expectedUrl, endpointUrl)
|
||||
}
|
||||
|
||||
// Check that all non-endpoint events were logged
|
||||
expectedLogs := []string{
|
||||
"Skipping non-endpoint event: ping",
|
||||
"Skipping non-endpoint event: status",
|
||||
"Skipping non-endpoint event: info",
|
||||
}
|
||||
|
||||
for _, expectedLog := range expectedLogs {
|
||||
found := false
|
||||
for _, log := range mockAPI.logs {
|
||||
if log == expectedLog {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Expected log message '%s' not found", expectedLog)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindEndpointUrl_EndpointInMiddle tests endpoint message in the middle of other messages
|
||||
func TestFindEndpointUrl_EndpointInMiddle(t *testing.T) {
|
||||
// Setup mock API
|
||||
mockAPI := &mockCommonCAPI{}
|
||||
api.SetCommonCAPI(mockAPI)
|
||||
|
||||
f := createTestFilter()
|
||||
|
||||
// Test with endpoint message in the middle
|
||||
sseData := "event: ping\ndata: alive\n\nevent: endpoint\ndata: https://api.example.com/chat\n\nevent: status\ndata: ready\n\n"
|
||||
|
||||
err, endpointUrl := f.findEndpointUrl(sseData)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got: %v", err)
|
||||
}
|
||||
|
||||
expectedUrl := "https://api.example.com/chat"
|
||||
if endpointUrl != expectedUrl {
|
||||
t.Errorf("Expected endpoint URL '%s', got '%s'", expectedUrl, endpointUrl)
|
||||
}
|
||||
|
||||
// Check that the ping event was logged as skipped
|
||||
found := false
|
||||
for _, log := range mockAPI.logs {
|
||||
if log == "Skipping non-endpoint event: ping" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Expected log message about skipping ping event not found")
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindEndpointUrl_NoEndpointMessage tests when no endpoint message is present
|
||||
func TestFindEndpointUrl_NoEndpointMessage(t *testing.T) {
|
||||
// Setup mock API
|
||||
mockAPI := &mockCommonCAPI{}
|
||||
api.SetCommonCAPI(mockAPI)
|
||||
|
||||
f := createTestFilter()
|
||||
|
||||
// Test with no endpoint message
|
||||
sseData := "event: ping\ndata: alive\n\nevent: status\ndata: connecting\n\nevent: info\ndata: ready\n\n"
|
||||
|
||||
err, endpointUrl := f.findEndpointUrl(sseData)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error when no endpoint found, got: %v", err)
|
||||
}
|
||||
|
||||
if endpointUrl != "" {
|
||||
t.Errorf("Expected empty endpoint URL when no endpoint found, got '%s'", endpointUrl)
|
||||
}
|
||||
|
||||
// Check that all non-endpoint events were logged
|
||||
expectedLogs := []string{
|
||||
"Skipping non-endpoint event: ping",
|
||||
"Skipping non-endpoint event: status",
|
||||
"Skipping non-endpoint event: info",
|
||||
}
|
||||
|
||||
for _, expectedLog := range expectedLogs {
|
||||
found := false
|
||||
for _, log := range mockAPI.logs {
|
||||
if log == expectedLog {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Expected log message '%s' not found", expectedLog)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindEndpointUrl_IncompleteEndpointMessage tests incomplete endpoint message
|
||||
func TestFindEndpointUrl_IncompleteEndpointMessage(t *testing.T) {
|
||||
// Setup mock API
|
||||
mockAPI := &mockCommonCAPI{}
|
||||
api.SetCommonCAPI(mockAPI)
|
||||
|
||||
f := createTestFilter()
|
||||
|
||||
// Test with incomplete endpoint message (missing final line break)
|
||||
sseData := "event: ping\ndata: alive\n\nevent: endpoint\ndata: https://api.example.com/chat"
|
||||
|
||||
err, endpointUrl := f.findEndpointUrl(sseData)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error for incomplete endpoint message, got: %v", err)
|
||||
}
|
||||
|
||||
if endpointUrl != "" {
|
||||
t.Errorf("Expected empty endpoint URL for incomplete message, got '%s'", endpointUrl)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindEndpointUrl_IncompleteNonEndpointMessage tests incomplete non-endpoint message
|
||||
func TestFindEndpointUrl_IncompleteNonEndpointMessage(t *testing.T) {
|
||||
// Setup mock API
|
||||
mockAPI := &mockCommonCAPI{}
|
||||
api.SetCommonCAPI(mockAPI)
|
||||
|
||||
f := createTestFilter()
|
||||
|
||||
// Test with incomplete non-endpoint message
|
||||
sseData := "event: ping\ndata: alive"
|
||||
|
||||
err, endpointUrl := f.findEndpointUrl(sseData)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error for incomplete non-endpoint message, got: %v", err)
|
||||
}
|
||||
|
||||
if endpointUrl != "" {
|
||||
t.Errorf("Expected empty endpoint URL for incomplete message, got '%s'", endpointUrl)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindEndpointUrl_MalformedEndpointData tests malformed endpoint data
|
||||
func TestFindEndpointUrl_MalformedEndpointData(t *testing.T) {
|
||||
// Setup mock API
|
||||
mockAPI := &mockCommonCAPI{}
|
||||
api.SetCommonCAPI(mockAPI)
|
||||
|
||||
f := createTestFilter()
|
||||
|
||||
// Test with malformed endpoint data (missing data field)
|
||||
sseData := "event: ping\ndata: alive\n\nevent: endpoint\nnotdata: https://api.example.com/chat\n\n"
|
||||
|
||||
err, endpointUrl := f.findEndpointUrl(sseData)
|
||||
|
||||
// Should return error for malformed endpoint data
|
||||
if err == nil {
|
||||
t.Errorf("Expected error for malformed endpoint data, but got none")
|
||||
}
|
||||
|
||||
if endpointUrl != "" {
|
||||
t.Errorf("Expected empty endpoint URL when error occurs, got '%s'", endpointUrl)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindEndpointUrl_DifferentLineBreaks tests different line break formats with improved version
|
||||
func TestFindEndpointUrl_DifferentLineBreaks(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
sseData string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "CRLF line breaks with ping first",
|
||||
sseData: "event: ping\r\ndata: alive\r\n\r\nevent: endpoint\r\ndata: https://api.example.com/chat\r\n\r\n",
|
||||
expected: "https://api.example.com/chat",
|
||||
},
|
||||
{
|
||||
name: "CR line breaks with status first",
|
||||
sseData: "event: status\rdata: ready\r\revent: endpoint\rdata: https://api.example.com/chat\r\r",
|
||||
expected: "https://api.example.com/chat",
|
||||
},
|
||||
{
|
||||
name: "LF line breaks with info first",
|
||||
sseData: "event: info\ndata: starting\n\nevent: endpoint\ndata: https://api.example.com/chat\n\n",
|
||||
expected: "https://api.example.com/chat",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Setup mock API
|
||||
mockAPI := &mockCommonCAPI{}
|
||||
api.SetCommonCAPI(mockAPI)
|
||||
|
||||
f := createTestFilter()
|
||||
|
||||
err, endpointUrl := f.findEndpointUrl(tc.sseData)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got: %v", err)
|
||||
}
|
||||
|
||||
if endpointUrl != tc.expected {
|
||||
t.Errorf("Expected endpoint URL '%s', got '%s'", tc.expected, endpointUrl)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindEndpointUrl_WithWhitespace tests improved version with whitespace
|
||||
func TestFindEndpointUrl_WithWhitespace(t *testing.T) {
|
||||
// Setup mock API
|
||||
mockAPI := &mockCommonCAPI{}
|
||||
api.SetCommonCAPI(mockAPI)
|
||||
|
||||
f := createTestFilter()
|
||||
|
||||
// Test with whitespace around event names and data
|
||||
sseData := "event: ping \ndata: alive \n\nevent: endpoint \ndata: https://api.example.com/chat \n\n"
|
||||
|
||||
err, endpointUrl := f.findEndpointUrl(sseData)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got: %v", err)
|
||||
}
|
||||
|
||||
expectedUrl := "https://api.example.com/chat"
|
||||
if endpointUrl != expectedUrl {
|
||||
t.Errorf("Expected endpoint URL '%s', got '%s'", expectedUrl, endpointUrl)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindEndpointUrl_NoEventFound tests behavior when no event is found
|
||||
func TestFindEndpointUrl_NoEventFound(t *testing.T) {
|
||||
// Setup mock API
|
||||
mockAPI := &mockCommonCAPI{}
|
||||
api.SetCommonCAPI(mockAPI)
|
||||
|
||||
f := createTestFilter()
|
||||
|
||||
// Test with data that doesn't contain event
|
||||
sseData := "some random data without event"
|
||||
|
||||
err, endpointUrl := f.findEndpointUrl(sseData)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error when no event found, got: %v", err)
|
||||
}
|
||||
|
||||
if endpointUrl != "" {
|
||||
t.Errorf("Expected empty endpoint URL when no event found, got '%s'", endpointUrl)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindEndpointUrl_MalformedData tests behavior with malformed SSE data
|
||||
func TestFindEndpointUrl_MalformedData(t *testing.T) {
|
||||
// Setup mock API
|
||||
mockAPI := &mockCommonCAPI{}
|
||||
api.SetCommonCAPI(mockAPI)
|
||||
|
||||
f := createTestFilter()
|
||||
|
||||
// Test with malformed data (missing data field)
|
||||
sseData := "event: endpoint\nnotdata: https://api.example.com/chat\n\n"
|
||||
|
||||
err, endpointUrl := f.findEndpointUrl(sseData)
|
||||
|
||||
// Should return error for malformed data
|
||||
if err == nil {
|
||||
t.Errorf("Expected error for malformed data, but got none")
|
||||
}
|
||||
|
||||
if endpointUrl != "" {
|
||||
t.Errorf("Expected empty endpoint URL when error occurs, got '%s'", endpointUrl)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindNextLineBreak tests the line break detection functionality
|
||||
func TestFindNextLineBreak(t *testing.T) {
|
||||
// Setup mock API
|
||||
mockAPI := &mockCommonCAPI{}
|
||||
api.SetCommonCAPI(mockAPI)
|
||||
|
||||
f := createTestFilter()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
input string
|
||||
expectedBreak string
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
name: "LF only",
|
||||
input: "some text\nmore text",
|
||||
expectedBreak: "\n",
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "CR only",
|
||||
input: "some text\rmore text",
|
||||
expectedBreak: "\r",
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "CRLF",
|
||||
input: "some text\r\nmore text",
|
||||
expectedBreak: "\r\n",
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "No line break",
|
||||
input: "some text without break",
|
||||
expectedBreak: "",
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "LF before CR (separate)",
|
||||
input: "some text\n\rmore text",
|
||||
expectedBreak: "",
|
||||
expectedError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err, lineBreak := f.findNextLineBreak(tc.input)
|
||||
|
||||
if tc.expectedError && err == nil {
|
||||
t.Errorf("Expected error, but got none")
|
||||
}
|
||||
|
||||
if !tc.expectedError && err != nil {
|
||||
t.Errorf("Expected no error, got: %v", err)
|
||||
}
|
||||
|
||||
if lineBreak != tc.expectedBreak {
|
||||
t.Errorf("Expected line break '%v', got '%v'", []byte(tc.expectedBreak), []byte(lineBreak))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,10 @@
|
||||
ARG BUILDER=higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/wasm-go-builder:go1.20.14-tinygo0.29.0-oras1.0.0
|
||||
ARG BUILDER=higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/wasm-go-builder:go1.24.0-oras1.0.0
|
||||
FROM $BUILDER AS builder
|
||||
|
||||
|
||||
ARG GOPROXY
|
||||
ENV GOPROXY=${GOPROXY}
|
||||
|
||||
ARG EXTRA_TAGS=""
|
||||
ENV EXTRA_TAGS=${EXTRA_TAGS}
|
||||
|
||||
ARG PLUGIN_NAME=hello-world
|
||||
|
||||
WORKDIR /workspace
|
||||
@@ -18,13 +15,7 @@ WORKDIR /workspace/extensions/$PLUGIN_NAME
|
||||
|
||||
RUN go mod tidy
|
||||
RUN \
|
||||
if echo "$PLUGIN_NAME" | grep -Eq '^waf$'; then \
|
||||
# Please use higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/wasm-go-builder:go1.19-tinygo0.28.1-oras1.0.0 as BUILDER
|
||||
go run mage.go build && \
|
||||
mv ./local/main.wasm /main.wasm ; \
|
||||
else \
|
||||
tinygo build -o /main.wasm -scheduler=none -gc=custom -tags="custommalloc nottinygc_finalizer $EXTRA_TAGS" -target=wasi ./ ; \
|
||||
fi
|
||||
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o /main.wasm ./
|
||||
|
||||
FROM scratch AS output
|
||||
|
||||
|
||||
@@ -1,115 +1,35 @@
|
||||
# The Dockerfile for wasm-go builder only support amd64 and arm64 yet.
|
||||
# If you want to build on another architecture, the following information may be helpful.
|
||||
#
|
||||
# - arch: amd64
|
||||
# base image: docker.io/ubuntu
|
||||
# go_url: https://golang.google.cn/dl/go1.20.1.linux-amd64.tar.gz"
|
||||
# tinygo_url: https://github.com/alibaba/higress/releases/download/v1.0.0-rc/higress-tinygo0.25.0.linux-amd64.tar.gz
|
||||
# oras_url: https://github.com/oras-project/oras/releases/download/v1.0.0/oras_1.0.0_linux_amd64.tar.gz
|
||||
#
|
||||
# - arch: arm64
|
||||
# base image: docker.io/ubuntu
|
||||
# go_url: https://golang.google.cn/dl/go1.20.1.linux-arm64.tar.gz
|
||||
# tinygo_url: https://github.com/alibaba/higress/releases/download/v1.0.0-rc/higress-tinygo0.25.0.linux-arm64.tar.gz
|
||||
# oras_url: https://github.com/oras-project/oras/releases/download/v1.0.0/oras_1.0.0_linux_arm64.tar.gz
|
||||
#
|
||||
# - arch: armel
|
||||
# base image: build yourself
|
||||
# go_url: install from source code
|
||||
# tinygo_url: build yourself
|
||||
# oras_url: build your self
|
||||
#
|
||||
# - arch: i386
|
||||
# base image: build yourself
|
||||
# go_url: https://dl.google.com/go/go1.20.1.linux-386.tar.gz
|
||||
# tinygo_url: build yourself
|
||||
# oras_url: build your self
|
||||
#
|
||||
# - arch: mips64el
|
||||
# base image: build your self
|
||||
# go_url: https://dl.google.com/go/go1.20.1.linux-386.tar.gz
|
||||
# tinygo_url: build your self
|
||||
# oras_url: build your self
|
||||
#
|
||||
# - arch: ppc64el
|
||||
# base image: build your self
|
||||
# go_url: https://dl.google.com/go/go1.20.1.linux-ppc64le.tar.gz
|
||||
# tinygo_url: build your self
|
||||
# oras_url: build your self
|
||||
#
|
||||
# - arch: s390x
|
||||
# base image: docker.io/ubuntu
|
||||
# go_url: https://dl.google.com/go/go1.20.1.linux-s390x.tar.gz
|
||||
# tinygo_url: build your self
|
||||
# oras_url: build your self
|
||||
#
|
||||
# - arch: armhf
|
||||
# base image: build your self
|
||||
# go_url: https://golang.google.cn/dl/go1.20.1.linux-armv6l.tar.gz
|
||||
# tinygo_url: https://github.com/tinygo-org/tinygo/releases/download/v0.25.0/tinygo_0.25.0_armhf.deb
|
||||
# oras_url: build your self
|
||||
|
||||
ARG BASE_IMAGE=docker.io/ubuntu
|
||||
FROM $BASE_IMAGE
|
||||
|
||||
ARG GO_VERSION
|
||||
ARG TINYGO_VERSION
|
||||
ARG ORAS_VERSION
|
||||
ARG HIGRESS_VERSION
|
||||
ARG USE_HIGRESS_TINYGO
|
||||
ARG GO_VERSION=1.24.4
|
||||
ARG ORAS_VERSION=1.0.0
|
||||
ARG HIGRESS_VERSION=1.0.0-rc
|
||||
|
||||
LABEL go_version=$GO_VERSION tinygo_version=$TINYGO_VERSION oras_version=$ORAS_VERSION
|
||||
LABEL go_version=$GO_VERSION oras_version=$ORAS_VERSION
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y wget \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
RUN apt-get update && apt-get install -y wget tar && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN arch="$(dpkg --print-architecture)"; arch="${arch##*-}"; \
|
||||
go_url=; \
|
||||
tinygo_url=; \
|
||||
go_version=${GO_VERSION:-1.19}; \
|
||||
tinygo_version=${TINYGO_VERSION:-0.25.0}; \
|
||||
oras_version=${ORAS_VERSION:-1.0.0}; \
|
||||
higress_version=${HIGRESS_VERSION:-1.0.0-rc}; \
|
||||
use_higress_tinygo=${USE_HIGRESS_TINYGO:-false}; \
|
||||
echo "arch: '$arch'"; \
|
||||
echo "go go_version: '$go_version'"; \
|
||||
echo "tinygo_version: '$tinygo_version'"; \
|
||||
echo "oras_version: '$oras_version'"; \
|
||||
echo "higress_version: '$higress_version'"; \
|
||||
echo "use_higress_tinygo: '$use_higress_tinygo'"; \
|
||||
RUN set -e; \
|
||||
arch="$(dpkg --print-architecture)"; arch="${arch##*-}"; \
|
||||
echo "arch: $arch"; \
|
||||
go_url=""; tinygo_url=""; \
|
||||
case "$arch" in \
|
||||
'amd64') \
|
||||
go_url="https://golang.google.cn/dl/go$go_version.linux-amd64.tar.gz"; \
|
||||
if [ "$use_higress_tinygo" = "true" ]; \
|
||||
then \
|
||||
tinygo_url="https://github.com/alibaba/higress/releases/download/v$higress_version/higress-tinygo${tinygo_version}.linux-amd64.tar.gz"; \
|
||||
else \
|
||||
tinygo_url="https://github.com/tinygo-org/tinygo/releases/download/v$tinygo_version/tinygo${tinygo_version}.linux-amd64.tar.gz"; \
|
||||
fi; \
|
||||
oras_url="https://github.com/oras-project/oras/releases/download/v$oras_version/oras_${oras_version}_linux_amd64.tar.gz"; \
|
||||
;; \
|
||||
'arm64') \
|
||||
go_url="https://golang.google.cn/dl/go$go_version.linux-arm64.tar.gz"; \
|
||||
if [ "$use_higress_tinygo" = "true" ]; \
|
||||
then \
|
||||
tinygo_url="https://github.com/alibaba/higress/releases/download/v$higress_version/higress-tinygo${tinygo_version}.linux-arm64.tar.gz"; \
|
||||
else \
|
||||
tinygo_url="https://github.com/tinygo-org/tinygo/releases/download/v$tinygo_version/tinygo${tinygo_version}.linux-arm64.tar.gz"; \
|
||||
fi; \
|
||||
oras_url="https://github.com/oras-project/oras/releases/download/v$oras_version/oras_${oras_version}_linux_arm64.tar.gz"; \
|
||||
;; \
|
||||
*) echo >&2 "error: unsupported architecture '$arch' "; exit 1 ;; \
|
||||
'amd64') \
|
||||
go_url="https://golang.google.cn/dl/go${GO_VERSION}.linux-amd64.tar.gz"; \
|
||||
oras_url="https://github.com/oras-project/oras/releases/download/v${ORAS_VERSION}/oras_${ORAS_VERSION}_linux_amd64.tar.gz"; \
|
||||
;; \
|
||||
'arm64') \
|
||||
go_url="https://golang.google.cn/dl/go${GO_VERSION}.linux-arm64.tar.gz"; \
|
||||
oras_url="https://github.com/oras-project/oras/releases/download/v${ORAS_VERSION}/oras_${ORAS_VERSION}_linux_arm64.tar.gz"; \
|
||||
;; \
|
||||
*) echo >&2 "unsupported architecture: $arch"; exit 1 ;; \
|
||||
esac; \
|
||||
echo "go_url: '$go_url'"; \
|
||||
wget -O go.tgz "$go_url" --progress=dot:giga; \
|
||||
rm -rf /usr/local/go && tar -C /usr/local -xzf go.tgz && rm -rf go.tgz; \
|
||||
echo "tinygo_url: '$tinygo_url'"; \
|
||||
wget -O tinygo.tgz "$tinygo_url" --progress=dot:giga; \
|
||||
rm -rf /usr/local/tinygo && tar -C /usr/local -xzf tinygo.tgz && rm -rf tinygo.tgz; \
|
||||
echo "oras_url: '$oras_url'"; \
|
||||
wget -O oras.tgz "$oras_url" --progress=dot:giga; \
|
||||
tar -C /usr/local/bin -xzf oras.tgz && rm -rf oras.tgz; \
|
||||
echo "go_url: $go_url"; \
|
||||
wget -O go.tgz "$go_url" --progress=dot:giga || (echo "Failed to download Go" && exit 1); \
|
||||
tar -C /usr/local -xzf go.tgz && rm go.tgz; \
|
||||
echo "oras_url: $oras_url"; \
|
||||
wget -O oras.tgz "$oras_url" --progress=dot:giga || (echo "Failed to download ORAS" && exit 1); \
|
||||
tar -C /usr/local/bin -xzf oras.tgz && rm oras.tgz; \
|
||||
echo "done";
|
||||
|
||||
ENV PATH=$PATH:/usr/local/go/bin:/usr/local/tinygo/bin:/usr/local/bin
|
||||
ENV PATH=$PATH:/usr/local/go/bin:/usr/local/bin
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
PLUGIN_NAME ?= hello-world
|
||||
BUILDER_REGISTRY ?= higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/
|
||||
REGISTRY ?= higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/
|
||||
GO_VERSION ?= 1.20.14
|
||||
GO_VERSION ?= 1.24.4
|
||||
TINYGO_VERSION ?= 0.29.0
|
||||
ORAS_VERSION ?= 1.0.0
|
||||
HIGRESS_VERSION ?= 1.0.0-rc
|
||||
USE_HIGRESS_TINYGO ?= false
|
||||
BUILDER ?= ${BUILDER_REGISTRY}wasm-go-builder:go${GO_VERSION}-tinygo${TINYGO_VERSION}-oras${ORAS_VERSION}
|
||||
BUILDER ?= ${BUILDER_REGISTRY}wasm-go-builder:go${GO_VERSION}-oras${ORAS_VERSION}
|
||||
BUILD_TIME := $(shell date "+%Y%m%d-%H%M%S")
|
||||
COMMIT_ID := $(shell git rev-parse --short HEAD 2>/dev/null)
|
||||
IMAGE_TAG = $(if $(strip $(PLUGIN_VERSION)),${PLUGIN_VERSION},${BUILD_TIME}-${COMMIT_ID})
|
||||
IMG ?= ${REGISTRY}${PLUGIN_NAME}:${IMAGE_TAG}
|
||||
GOPROXY := $(shell go env GOPROXY)
|
||||
EXTRA_TAGS := $(shell [ -f extensions/${PLUGIN_NAME}/.buildrc ] && . extensions/${PLUGIN_NAME}/.buildrc && echo $$EXTRA_TAGS || echo "")
|
||||
|
||||
.DEFAULT:
|
||||
build:
|
||||
DOCKER_BUILDKIT=1 docker build --build-arg PLUGIN_NAME=${PLUGIN_NAME} \
|
||||
--build-arg BUILDER=${BUILDER} \
|
||||
--build-arg GOPROXY=$(GOPROXY) \
|
||||
--build-arg EXTRA_TAGS=${EXTRA_TAGS} \
|
||||
-t ${IMG} \
|
||||
--output extensions/${PLUGIN_NAME} \
|
||||
.
|
||||
@@ -30,7 +28,6 @@ build-image:
|
||||
DOCKER_BUILDKIT=1 docker build --build-arg PLUGIN_NAME=${PLUGIN_NAME} \
|
||||
--build-arg BUILDER=${BUILDER} \
|
||||
--build-arg GOPROXY=$(GOPROXY) \
|
||||
--build-arg EXTRA_TAGS=${EXTRA_TAGS} \
|
||||
-t ${IMG} \
|
||||
.
|
||||
@echo ""
|
||||
@@ -52,7 +49,6 @@ builder:
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--build-arg BASE_IMAGE=docker.io/ubuntu \
|
||||
--build-arg GO_VERSION=$(GO_VERSION) \
|
||||
--build-arg TINYGO_VERSION=$(TINYGO_VERSION) \
|
||||
--build-arg ORAS_VERSION=$(ORAS_VERSION) \
|
||||
--build-arg HIGRESS_VERSION=$(HIGRESS_VERSION) \
|
||||
--build-arg USE_HIGRESS_TINYGO=$(USE_HIGRESS_TINYGO) \
|
||||
@@ -64,9 +60,8 @@ builder:
|
||||
@echo "image: ${BUILDER}"
|
||||
|
||||
local-build:
|
||||
tinygo build -scheduler=none -target=wasi -gc=custom -tags='custommalloc nottinygc_finalizer' \
|
||||
-o extensions/${PLUGIN_NAME}/main.wasm \
|
||||
extensions/${PLUGIN_NAME}/main.go
|
||||
cd extensions/${PLUGIN_NAME};GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o ./main.wasm ./
|
||||
|
||||
@echo ""
|
||||
@echo "wasm: extensions/${PLUGIN_NAME}/main.wasm"
|
||||
|
||||
|
||||
@@ -45,16 +45,15 @@ output wasm file: extensions/request-block/plugin.wasm
|
||||
|
||||
编译环境要求如下:
|
||||
|
||||
- Go 版本: >= 1.18 (需要支持范型特性)
|
||||
|
||||
- TinyGo 版本: >= 0.28.1
|
||||
- Go 版本: >= 1.24 (需要支持 wasm 构建特性)
|
||||
|
||||
下面是本地多步骤构建 [request-block](extensions/request-block) 的例子。
|
||||
|
||||
### step1. 编译 wasm
|
||||
|
||||
```bash
|
||||
tinygo build -o main.wasm -scheduler=none -target=wasi -gc=custom -tags='custommalloc nottinygc_finalizer' ./extensions/request-block/main.go
|
||||
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o ./extensions/request-block/main.wasm ./extensions/request-block
|
||||
|
||||
```
|
||||
|
||||
详细的编译说明,包括要使用更复杂的 Header 状态管理机制,请参考[ Go 开发插件的最佳实践](https://higress.io/docs/latest/user/wasm-go/#3-%E7%BC%96%E8%AF%91%E7%94%9F%E6%88%90-wasm-%E6%96%87%E4%BB%B6)。
|
||||
@@ -70,8 +69,8 @@ COPY main.wasm plugin.wasm
|
||||
```
|
||||
|
||||
```bash
|
||||
docker build -t <your_registry_hub>/request-block:1.0.0 -f <your_dockerfile> .
|
||||
docker push <your_registry_hub>/request-block:1.0.0
|
||||
docker build -t <your_registry_hub>/request-block:2.0.0 -f <your_dockerfile> .
|
||||
docker push <your_registry_hub>/request-block:2.0.0
|
||||
```
|
||||
|
||||
## 创建 WasmPlugin 资源使插件生效
|
||||
@@ -144,7 +143,7 @@ spec:
|
||||
block_bodies:
|
||||
- "foo"
|
||||
- "bar"
|
||||
url: oci://<your_registry_hub>/request-block:1.0.0
|
||||
url: oci://<your_registry_hub>/request-block:2.0.0
|
||||
```
|
||||
|
||||
所有规则会按上面配置的顺序一次执行匹配,当有一个规则匹配时,就停止匹配,并选择匹配的配置执行插件逻辑。
|
||||
|
||||
@@ -41,16 +41,14 @@ You can also use `make build-push` to build and push the image at the same time.
|
||||
|
||||
You can also build wasm locally and copy it to a Docker image. This requires a local build environment:
|
||||
|
||||
Go version: >= 1.18
|
||||
|
||||
TinyGo version: >= 0.25.0
|
||||
Go version: >= 1.24
|
||||
|
||||
The following is an example of building the plugin [request-block](extensions/request-block).
|
||||
|
||||
### step1. build wasm
|
||||
|
||||
```bash
|
||||
tinygo build -o main.wasm -scheduler=none -target=wasi ./extensions/request-block/main.go
|
||||
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o ./extensions/request-block/main.wasm ./extensions/request-block
|
||||
```
|
||||
|
||||
### step2. build and push docker image
|
||||
@@ -63,8 +61,8 @@ COPY main.wasm plugin.wasm
|
||||
```
|
||||
|
||||
```bash
|
||||
docker build -t <your_registry_hub>/request-block:1.0.0 -f <your_dockerfile> .
|
||||
docker push <your_registry_hub>/request-block:1.0.0
|
||||
docker build -t <your_registry_hub>/request-block:2.0.0 -f <your_dockerfile> .
|
||||
docker push <your_registry_hub>/request-block:2.0.0
|
||||
```
|
||||
|
||||
## Apply WasmPlugin API
|
||||
@@ -83,7 +81,7 @@ spec:
|
||||
defaultConfig:
|
||||
block_urls:
|
||||
- "swagger.html"
|
||||
url: oci://<your_registry_hub>/request-block:1.0.0
|
||||
url: oci://<your_registry_hub>/request-block:2.0.0
|
||||
```
|
||||
|
||||
When the resource is applied on the Kubernetes cluster with `kubectl apply -f <your-wasm-plugin-yaml>`,
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
module github.com/alibaba/higress/plugins/wasm-go/extensions/custom-logs
|
||||
|
||||
go 1.18
|
||||
go 1.24.1
|
||||
|
||||
replace github.com/alibaba/higress/plugins/wasm-go => ../..
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/alibaba/higress/plugins/wasm-go v0.0.0
|
||||
github.com/higress-group/proxy-wasm-go-sdk v1.0.0
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80
|
||||
github.com/higress-group/wasm-go v1.0.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 // indirect
|
||||
github.com/magefile/mage v1.14.0 // indirect
|
||||
github.com/tidwall/gjson v1.17.3 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/tidwall/gjson v1.18.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/resp v0.1.1 // indirect
|
||||
)
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA=
|
||||
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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80 h1:xqmtTZI0JQ2O+Lg9/CE6c+Tw9KD6FnvWw8EpLVuuvfg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.0 h1:4Ik5n3FsJ5+r13KLQl2ky+8NuAE8dfWQwoKxXYD2KAw=
|
||||
github.com/higress-group/wasm-go v1.0.0/go.mod h1:ODBV27sjmhIW8Cqv3R74EUcTnbdkE69bmXBQFuRkY1M=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94=
|
||||
github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -19,10 +19,12 @@ import (
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||
)
|
||||
|
||||
func main() {
|
||||
func main() {}
|
||||
|
||||
func init() {
|
||||
wrapper.SetCtx(
|
||||
"custom-log",
|
||||
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
module github.com/alibaba/higress/plugins/wasm-go/extensions/custom-logs
|
||||
|
||||
go 1.18
|
||||
go 1.24.1
|
||||
|
||||
replace github.com/alibaba/higress/plugins/wasm-go => ../..
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/alibaba/higress/plugins/wasm-go v0.0.0
|
||||
github.com/higress-group/proxy-wasm-go-sdk v1.0.0
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80
|
||||
github.com/higress-group/wasm-go v1.0.0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 // indirect
|
||||
github.com/magefile/mage v1.14.0 // indirect
|
||||
github.com/tidwall/gjson v1.17.3 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/resp v0.1.1 // indirect
|
||||
)
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA=
|
||||
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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80 h1:xqmtTZI0JQ2O+Lg9/CE6c+Tw9KD6FnvWw8EpLVuuvfg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.0 h1:4Ik5n3FsJ5+r13KLQl2ky+8NuAE8dfWQwoKxXYD2KAw=
|
||||
github.com/higress-group/wasm-go v1.0.0/go.mod h1:ODBV27sjmhIW8Cqv3R74EUcTnbdkE69bmXBQFuRkY1M=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94=
|
||||
github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -22,10 +22,12 @@ import (
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||
)
|
||||
|
||||
func main() {
|
||||
func main() {}
|
||||
|
||||
func init() {
|
||||
wrapper.SetCtx(
|
||||
"custom-span-attribute",
|
||||
wrapper.ParseConfigBy(parseConfig),
|
||||
|
||||
@@ -293,7 +293,7 @@ apis:
|
||||
本示例配置了三个服务,演示了get与post两种类型的工具。其中get类型的工具包括高德地图与心知天气,post类型的工具是deepl翻译。三个服务都需要现在Higress的服务中以DNS域名的方式配置好,并确保健康。
|
||||
高德地图提供了两个工具,分别是获取指定地点的坐标,以及搜索坐标附近的感兴趣的地点。文档:https://lbs.amap.com/api/webservice/guide/api-advanced/newpoisearch
|
||||
心知天气提供了一个工具,用于获取指定城市的实时天气情况,支持中文,英文,日语返回,以及摄氏度和华氏度的表示。文档:https://seniverse.yuque.com/hyper_data/api_v3/nyiu3t
|
||||
deepl提供了一个工具,用于翻译给定的句子,支持多语言。。文档:https://developers.deepl.com/docs/v/zh/api-reference/translate?fallback=true
|
||||
deepl提供了一个工具,用于翻译给定的句子,支持多语言。文档:https://developers.deepl.com/api-reference/translate/request-translation
|
||||
|
||||
|
||||
以下为测试用例,为了效果的稳定性,建议保持大模型版本的稳定,本例子中使用的qwen-max-0403:
|
||||
|
||||
@@ -283,7 +283,7 @@ apis:
|
||||
This example configures three services demonstrating both GET and POST types of tools. The GET type tools include Amap and XZWeather, while the POST type tool is the DeepL translation. All three services need to be properly configured in the Higress service with DNS domain names and should be healthy.
|
||||
Amap provides two tools, one for obtaining the coordinates of a specified location and the other for searching for points of interest near the coordinates. Document: https://lbs.amap.com/api/webservice/guide/api-advanced/newpoisearch
|
||||
XZWeather provides one tool to get real-time weather conditions for a specified city, supporting results in Chinese, English, and Japanese, as well as representations in Celsius and Fahrenheit. Document: https://seniverse.yuque.com/hyper_data/api_v3/nyiu3t
|
||||
DeepL provides one tool for translating given sentences, supporting multiple languages. Document: https://developers.deepl.com/docs/v/zh/api-reference/translate?fallback=true
|
||||
DeepL provides one tool for translating given sentences, supporting multiple languages. Document: https://developers.deepl.com/api-reference/translate/request-translation
|
||||
|
||||
Below are test cases. For stability, it is recommended to maintain a stable version of the large model. The example used here is qwen-max-0403:
|
||||
**Request Example**
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
module github.com/alibaba/higress/plugins/wasm-go/extensions/ai-agent
|
||||
|
||||
go 1.19
|
||||
go 1.24.1
|
||||
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.2
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f
|
||||
github.com/tidwall/gjson v1.17.3
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80
|
||||
github.com/higress-group/wasm-go v0.0.0-20250628101008-bea7da01a545
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 // indirect
|
||||
github.com/magefile/mage v1.14.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/resp v0.1.1 // indirect
|
||||
)
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.2 h1:gH7OIGXm4wtW5Vo7L2deMPqF7OVWNESDHv1CaaTGu6s=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.2/go.mod h1:359don/ahMxpfeLMzr29Cjwcu8IywTTDUzWlBPRNLHw=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520/go.mod h1:Nz8ORLaFiLWotg6GeKlJMhv8cci8mM43uEnLA5t8iew=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f h1:ZIiIBRvIw62gA5MJhuwp1+2wWbqL9IGElQ499rUsYYg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80 h1:xqmtTZI0JQ2O+Lg9/CE6c+Tw9KD6FnvWw8EpLVuuvfg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v0.0.0-20250628101008-bea7da01a545 h1:qb/Rhhfm1gzr/stim/L0cKNo0MPatdo0Rd8iYOAPWE0=
|
||||
github.com/higress-group/wasm-go v0.0.0-20250628101008-bea7da01a545/go.mod h1:ODBV27sjmhIW8Cqv3R74EUcTnbdkE69bmXBQFuRkY1M=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94=
|
||||
github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
@@ -24,3 +24,4 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -11,9 +11,10 @@ import (
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-agent/dashscope"
|
||||
prompttpl "github.com/alibaba/higress/plugins/wasm-go/extensions/ai-agent/promptTpl"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/log"
|
||||
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
@@ -26,7 +27,9 @@ const ActionPattern = `Action:\s*(.*?)[.\n]`
|
||||
const ActionInputPattern = `Action Input:\s*(.*)`
|
||||
const FinalAnswerPattern = `Final Answer:(.*)`
|
||||
|
||||
func main() {
|
||||
func main() {}
|
||||
|
||||
func init() {
|
||||
wrapper.SetCtx(
|
||||
"ai-agent",
|
||||
wrapper.ParseConfigBy(parseConfig),
|
||||
@@ -37,7 +40,7 @@ func main() {
|
||||
)
|
||||
}
|
||||
|
||||
func parseConfig(gjson gjson.Result, c *PluginConfig, log wrapper.Log) error {
|
||||
func parseConfig(gjson gjson.Result, c *PluginConfig, log log.Log) error {
|
||||
initResponsePromptTpl(gjson, c)
|
||||
|
||||
err := initAPIs(gjson, c)
|
||||
@@ -54,11 +57,12 @@ func parseConfig(gjson gjson.Result, c *PluginConfig, log wrapper.Log) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config PluginConfig, log wrapper.Log) types.Action {
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config PluginConfig, log log.Log) types.Action {
|
||||
ctx.DisableReroute()
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
func firstReq(ctx wrapper.HttpContext, config PluginConfig, prompt string, rawRequest Request, log wrapper.Log) types.Action {
|
||||
func firstReq(ctx wrapper.HttpContext, config PluginConfig, prompt string, rawRequest Request, log log.Log) types.Action {
|
||||
log.Debugf("[onHttpRequestBody] firstreq:%s", prompt)
|
||||
|
||||
var userMessage Message
|
||||
@@ -88,7 +92,7 @@ func firstReq(ctx wrapper.HttpContext, config PluginConfig, prompt string, rawRe
|
||||
}
|
||||
}
|
||||
|
||||
func onHttpRequestBody(ctx wrapper.HttpContext, config PluginConfig, body []byte, log wrapper.Log) types.Action {
|
||||
func onHttpRequestBody(ctx wrapper.HttpContext, config PluginConfig, body []byte, log log.Log) types.Action {
|
||||
log.Debug("onHttpRequestBody start")
|
||||
defer log.Debug("onHttpRequestBody end")
|
||||
|
||||
@@ -172,7 +176,7 @@ func onHttpRequestBody(ctx wrapper.HttpContext, config PluginConfig, body []byte
|
||||
return ret
|
||||
}
|
||||
|
||||
func onHttpResponseHeaders(ctx wrapper.HttpContext, config PluginConfig, log wrapper.Log) types.Action {
|
||||
func onHttpResponseHeaders(ctx wrapper.HttpContext, config PluginConfig, log log.Log) types.Action {
|
||||
log.Debug("onHttpResponseHeaders start")
|
||||
defer log.Debug("onHttpResponseHeaders end")
|
||||
|
||||
@@ -200,7 +204,7 @@ func extractJson(bodyStr string) (string, error) {
|
||||
return jsonStr, nil
|
||||
}
|
||||
|
||||
func jsonFormat(llmClient wrapper.HttpClient, llmInfo LLMInfo, jsonSchema map[string]interface{}, assistantMessage Message, actionInput string, headers [][2]string, streamMode bool, rawResponse Response, log wrapper.Log) string {
|
||||
func jsonFormat(llmClient wrapper.HttpClient, llmInfo LLMInfo, jsonSchema map[string]interface{}, assistantMessage Message, actionInput string, headers [][2]string, streamMode bool, rawResponse Response, log log.Log) string {
|
||||
prompt := fmt.Sprintf(prompttpl.Json_Resp_Template, jsonSchema, actionInput)
|
||||
|
||||
messages := []dashscope.Message{{Role: "user", Content: prompt}}
|
||||
@@ -241,7 +245,7 @@ func jsonFormat(llmClient wrapper.HttpClient, llmInfo LLMInfo, jsonSchema map[st
|
||||
return content
|
||||
}
|
||||
|
||||
func noneStream(assistantMessage Message, actionInput string, rawResponse Response, log wrapper.Log) {
|
||||
func noneStream(assistantMessage Message, actionInput string, rawResponse Response, log log.Log) {
|
||||
assistantMessage.Role = "assistant"
|
||||
assistantMessage.Content = actionInput
|
||||
rawResponse.Choices[0].Message = assistantMessage
|
||||
@@ -257,7 +261,7 @@ func noneStream(assistantMessage Message, actionInput string, rawResponse Respon
|
||||
}
|
||||
}
|
||||
|
||||
func stream(actionInput string, rawResponse Response, log wrapper.Log) {
|
||||
func stream(actionInput string, rawResponse Response, log log.Log) {
|
||||
headers := [][2]string{{"content-type", "text/event-stream; charset=utf-8"}}
|
||||
proxywasm.ReplaceHttpResponseHeaders(headers)
|
||||
// Remove quotes from actionInput
|
||||
@@ -271,7 +275,7 @@ func stream(actionInput string, rawResponse Response, log wrapper.Log) {
|
||||
proxywasm.ResumeHttpResponse()
|
||||
}
|
||||
|
||||
func toolsCallResult(ctx wrapper.HttpContext, llmClient wrapper.HttpClient, llmInfo LLMInfo, jsonResp JsonResp, aPIsParam []APIsParam, aPIClient []wrapper.HttpClient, content string, rawResponse Response, log wrapper.Log, statusCode int, responseBody []byte) {
|
||||
func toolsCallResult(ctx wrapper.HttpContext, llmClient wrapper.HttpClient, llmInfo LLMInfo, jsonResp JsonResp, aPIsParam []APIsParam, aPIClient []wrapper.HttpClient, content string, rawResponse Response, log log.Log, statusCode int, responseBody []byte) {
|
||||
if statusCode != http.StatusOK {
|
||||
log.Debugf("statusCode: %d", statusCode)
|
||||
}
|
||||
@@ -332,7 +336,7 @@ func toolsCallResult(ctx wrapper.HttpContext, llmClient wrapper.HttpClient, llmI
|
||||
}
|
||||
}
|
||||
|
||||
func outputParser(response string, log wrapper.Log) (string, string) {
|
||||
func outputParser(response string, log log.Log) (string, string) {
|
||||
log.Debugf("Raw response:%s", response)
|
||||
|
||||
start := strings.Index(response, "```")
|
||||
@@ -379,7 +383,7 @@ func outputParser(response string, log wrapper.Log) (string, string) {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
func toolsCall(ctx wrapper.HttpContext, llmClient wrapper.HttpClient, llmInfo LLMInfo, jsonResp JsonResp, aPIsParam []APIsParam, aPIClient []wrapper.HttpClient, content string, rawResponse Response, log wrapper.Log) (types.Action, string) {
|
||||
func toolsCall(ctx wrapper.HttpContext, llmClient wrapper.HttpClient, llmInfo LLMInfo, jsonResp JsonResp, aPIsParam []APIsParam, aPIClient []wrapper.HttpClient, content string, rawResponse Response, log log.Log) (types.Action, string) {
|
||||
dashscope.MessageStore.AddForAssistant(content)
|
||||
|
||||
action, actionInput := outputParser(content, log)
|
||||
@@ -514,7 +518,7 @@ func toolsCall(ctx wrapper.HttpContext, llmClient wrapper.HttpClient, llmInfo LL
|
||||
}
|
||||
|
||||
// 从response接收到firstreq的大模型返回
|
||||
func onHttpResponseBody(ctx wrapper.HttpContext, config PluginConfig, body []byte, log wrapper.Log) types.Action {
|
||||
func onHttpResponseBody(ctx wrapper.HttpContext, config PluginConfig, body []byte, log log.Log) types.Action {
|
||||
log.Debugf("onHttpResponseBody start")
|
||||
defer log.Debugf("onHttpResponseBody end")
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
EXTRA_TAGS=proxy_wasm_version_0_2_100
|
||||
@@ -4,7 +4,8 @@ import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/wasm-go/pkg/log"
|
||||
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
@@ -15,7 +16,7 @@ const (
|
||||
|
||||
type providerInitializer interface {
|
||||
ValidateConfig(ProviderConfig) error
|
||||
CreateProvider(ProviderConfig, wrapper.Log) (Provider, error)
|
||||
CreateProvider(ProviderConfig, log.Log) (Provider, error)
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -128,7 +129,7 @@ func (c *ProviderConfig) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateProvider(pc ProviderConfig, log wrapper.Log) (Provider, error) {
|
||||
func CreateProvider(pc ProviderConfig, log log.Log) (Provider, error) {
|
||||
initializer, has := providerInitializers[pc.typ]
|
||||
if !has {
|
||||
return nil, errors.New("unknown provider type: " + pc.typ)
|
||||
|
||||
@@ -3,7 +3,8 @@ package cache
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/wasm-go/pkg/log"
|
||||
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||
)
|
||||
|
||||
type redisProviderInitializer struct {
|
||||
@@ -16,7 +17,7 @@ func (r *redisProviderInitializer) ValidateConfig(cf ProviderConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *redisProviderInitializer) CreateProvider(cf ProviderConfig, log wrapper.Log) (Provider, error) {
|
||||
func (r *redisProviderInitializer) CreateProvider(cf ProviderConfig, log log.Log) (Provider, error) {
|
||||
rp := redisProvider{
|
||||
config: cf,
|
||||
client: wrapper.NewRedisClusterClient(wrapper.FQDNCluster{
|
||||
@@ -32,7 +33,7 @@ func (r *redisProviderInitializer) CreateProvider(cf ProviderConfig, log wrapper
|
||||
type redisProvider struct {
|
||||
config ProviderConfig
|
||||
client wrapper.RedisClient
|
||||
log wrapper.Log
|
||||
log log.Log
|
||||
}
|
||||
|
||||
func (rp *redisProvider) GetProviderType() string {
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-cache/cache"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-cache/embedding"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-cache/vector"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/wasm-go/pkg/log"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
@@ -46,7 +46,7 @@ type PluginConfig struct {
|
||||
CacheKeyStrategy string
|
||||
}
|
||||
|
||||
func (c *PluginConfig) FromJson(json gjson.Result, log wrapper.Log) {
|
||||
func (c *PluginConfig) FromJson(json gjson.Result, log log.Log) {
|
||||
c.embeddingProviderConfig = &embedding.ProviderConfig{}
|
||||
c.vectorProviderConfig = &vector.ProviderConfig{}
|
||||
c.cacheProviderConfig = &cache.ProviderConfig{}
|
||||
@@ -91,7 +91,7 @@ func (c *PluginConfig) FromJson(json gjson.Result, log wrapper.Log) {
|
||||
if json.Get("enableSemanticCache").Exists() {
|
||||
c.EnableSemanticCache = json.Get("enableSemanticCache").Bool()
|
||||
} else if c.GetVectorProvider() == nil {
|
||||
c.EnableSemanticCache = false // set value to false when no vector provider
|
||||
c.EnableSemanticCache = false // set value to false when no vector provider
|
||||
} else {
|
||||
c.EnableSemanticCache = true // set default value to true
|
||||
}
|
||||
@@ -142,7 +142,7 @@ func (c *PluginConfig) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *PluginConfig) Complete(log wrapper.Log) error {
|
||||
func (c *PluginConfig) Complete(log log.Log) error {
|
||||
var err error
|
||||
if c.embeddingProviderConfig.GetProviderType() != "" {
|
||||
log.Debugf("embedding provider is set to %s", c.embeddingProviderConfig.GetProviderType())
|
||||
@@ -193,7 +193,7 @@ func (c *PluginConfig) GetCacheProvider() cache.Provider {
|
||||
return c.cacheProvider
|
||||
}
|
||||
|
||||
func convertLegacyMapFields(c *PluginConfig, json gjson.Result, log wrapper.Log) {
|
||||
func convertLegacyMapFields(c *PluginConfig, json gjson.Result, log log.Log) {
|
||||
keyMap := map[string]string{
|
||||
"cacheKeyFrom.requestBody": "cacheKeyFrom",
|
||||
"cacheValueFrom.requestBody": "cacheValueFrom",
|
||||
@@ -212,7 +212,7 @@ func convertLegacyMapFields(c *PluginConfig, json gjson.Result, log wrapper.Log)
|
||||
}
|
||||
}
|
||||
|
||||
func setField(c *PluginConfig, fieldName string, value string, log wrapper.Log) {
|
||||
func setField(c *PluginConfig, fieldName string, value string, log log.Log) {
|
||||
switch fieldName {
|
||||
case "cacheKeyFrom":
|
||||
c.CacheKeyFrom = value
|
||||
|
||||
@@ -8,13 +8,14 @@ import (
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-cache/config"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-cache/vector"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
logs "github.com/higress-group/wasm-go/pkg/log"
|
||||
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/resp"
|
||||
)
|
||||
|
||||
// CheckCacheForKey checks if the key is in the cache, or triggers similarity search if not found.
|
||||
func CheckCacheForKey(key string, ctx wrapper.HttpContext, c config.PluginConfig, log wrapper.Log, stream bool, useSimilaritySearch bool) error {
|
||||
func CheckCacheForKey(key string, ctx wrapper.HttpContext, c config.PluginConfig, log logs.Log, stream bool, useSimilaritySearch bool) error {
|
||||
activeCacheProvider := c.GetCacheProvider()
|
||||
if activeCacheProvider == nil {
|
||||
log.Debugf("[%s] [CheckCacheForKey] no cache provider configured, performing similarity search", PLUGIN_NAME)
|
||||
@@ -37,7 +38,7 @@ func CheckCacheForKey(key string, ctx wrapper.HttpContext, c config.PluginConfig
|
||||
}
|
||||
|
||||
// handleCacheResponse processes cache response and handles cache hits and misses.
|
||||
func handleCacheResponse(key string, response resp.Value, ctx wrapper.HttpContext, log wrapper.Log, stream bool, c config.PluginConfig, useSimilaritySearch bool) {
|
||||
func handleCacheResponse(key string, response resp.Value, ctx wrapper.HttpContext, log logs.Log, stream bool, c config.PluginConfig, useSimilaritySearch bool) {
|
||||
if err := response.Error(); err == nil && !response.IsNull() {
|
||||
log.Infof("[%s] cache hit for key: %s", PLUGIN_NAME, key)
|
||||
processCacheHit(key, response.String(), stream, ctx, c, log)
|
||||
@@ -60,7 +61,7 @@ func handleCacheResponse(key string, response resp.Value, ctx wrapper.HttpContex
|
||||
}
|
||||
|
||||
// processCacheHit handles a successful cache hit.
|
||||
func processCacheHit(key string, response string, stream bool, ctx wrapper.HttpContext, c config.PluginConfig, log wrapper.Log) {
|
||||
func processCacheHit(key string, response string, stream bool, ctx wrapper.HttpContext, c config.PluginConfig, log logs.Log) {
|
||||
if strings.TrimSpace(response) == "" {
|
||||
log.Warnf("[%s] [processCacheHit] cached response for key %s is empty", PLUGIN_NAME, key)
|
||||
proxywasm.ResumeHttpRequest()
|
||||
@@ -85,7 +86,7 @@ func processCacheHit(key string, response string, stream bool, ctx wrapper.HttpC
|
||||
}
|
||||
|
||||
// performSimilaritySearch determines the appropriate similarity search method to use.
|
||||
func performSimilaritySearch(key string, ctx wrapper.HttpContext, c config.PluginConfig, log wrapper.Log, queryString string, stream bool) error {
|
||||
func performSimilaritySearch(key string, ctx wrapper.HttpContext, c config.PluginConfig, log logs.Log, queryString string, stream bool) error {
|
||||
activeVectorProvider := c.GetVectorProvider()
|
||||
if activeVectorProvider == nil {
|
||||
return logAndReturnError(log, "[performSimilaritySearch] no vector provider configured for similarity search")
|
||||
@@ -107,19 +108,19 @@ func performSimilaritySearch(key string, ctx wrapper.HttpContext, c config.Plugi
|
||||
}
|
||||
|
||||
// performStringQuery executes the string-based similarity search.
|
||||
func performStringQuery(key string, queryString string, ctx wrapper.HttpContext, c config.PluginConfig, log wrapper.Log, stream bool) error {
|
||||
func performStringQuery(key string, queryString string, ctx wrapper.HttpContext, c config.PluginConfig, log logs.Log, stream bool) error {
|
||||
stringQuerier, ok := c.GetVectorProvider().(vector.StringQuerier)
|
||||
if !ok {
|
||||
return logAndReturnError(log, "[performStringQuery] active vector provider does not implement StringQuerier interface")
|
||||
}
|
||||
|
||||
return stringQuerier.QueryString(queryString, ctx, log, func(results []vector.QueryResult, ctx wrapper.HttpContext, log wrapper.Log, err error) {
|
||||
return stringQuerier.QueryString(queryString, ctx, log, func(results []vector.QueryResult, ctx wrapper.HttpContext, log logs.Log, err error) {
|
||||
handleQueryResults(key, results, ctx, log, stream, c, err)
|
||||
})
|
||||
}
|
||||
|
||||
// performEmbeddingQuery executes the embedding-based similarity search.
|
||||
func performEmbeddingQuery(key string, ctx wrapper.HttpContext, c config.PluginConfig, log wrapper.Log, stream bool) error {
|
||||
func performEmbeddingQuery(key string, ctx wrapper.HttpContext, c config.PluginConfig, log logs.Log, stream bool) error {
|
||||
embeddingQuerier, ok := c.GetVectorProvider().(vector.EmbeddingQuerier)
|
||||
if !ok {
|
||||
return logAndReturnError(log, fmt.Sprintf("[performEmbeddingQuery] active vector provider does not implement EmbeddingQuerier interface"))
|
||||
@@ -138,7 +139,7 @@ func performEmbeddingQuery(key string, ctx wrapper.HttpContext, c config.PluginC
|
||||
}
|
||||
ctx.SetContext(CACHE_KEY_EMBEDDING_KEY, textEmbedding)
|
||||
|
||||
err = embeddingQuerier.QueryEmbedding(textEmbedding, ctx, log, func(results []vector.QueryResult, ctx wrapper.HttpContext, log wrapper.Log, err error) {
|
||||
err = embeddingQuerier.QueryEmbedding(textEmbedding, ctx, log, func(results []vector.QueryResult, ctx wrapper.HttpContext, log logs.Log, err error) {
|
||||
handleQueryResults(key, results, ctx, log, stream, c, err)
|
||||
})
|
||||
if err != nil {
|
||||
@@ -148,7 +149,7 @@ func performEmbeddingQuery(key string, ctx wrapper.HttpContext, c config.PluginC
|
||||
}
|
||||
|
||||
// handleQueryResults processes the results of similarity search and determines next actions.
|
||||
func handleQueryResults(key string, results []vector.QueryResult, ctx wrapper.HttpContext, log wrapper.Log, stream bool, c config.PluginConfig, err error) {
|
||||
func handleQueryResults(key string, results []vector.QueryResult, ctx wrapper.HttpContext, log logs.Log, stream bool, c config.PluginConfig, err error) {
|
||||
if err != nil {
|
||||
handleInternalError(err, fmt.Sprintf("[%s] [handleQueryResults] error querying vector database for key: %s", PLUGIN_NAME, key), log)
|
||||
return
|
||||
@@ -186,14 +187,14 @@ func handleQueryResults(key string, results []vector.QueryResult, ctx wrapper.Ht
|
||||
}
|
||||
|
||||
// logAndReturnError logs an error and returns it.
|
||||
func logAndReturnError(log wrapper.Log, message string) error {
|
||||
func logAndReturnError(log logs.Log, message string) error {
|
||||
message = fmt.Sprintf("[%s] %s", PLUGIN_NAME, message)
|
||||
log.Errorf(message)
|
||||
return errors.New(message)
|
||||
}
|
||||
|
||||
// handleInternalError logs an error and resumes the HTTP request.
|
||||
func handleInternalError(err error, message string, log wrapper.Log) {
|
||||
func handleInternalError(err error, message string, log logs.Log) {
|
||||
if err != nil {
|
||||
log.Errorf("[%s] [handleInternalError] %s: %v", PLUGIN_NAME, message, err)
|
||||
} else {
|
||||
@@ -204,7 +205,7 @@ func handleInternalError(err error, message string, log wrapper.Log) {
|
||||
}
|
||||
|
||||
// Caches the response value
|
||||
func cacheResponse(ctx wrapper.HttpContext, c config.PluginConfig, key string, value string, log wrapper.Log) {
|
||||
func cacheResponse(ctx wrapper.HttpContext, c config.PluginConfig, key string, value string, log logs.Log) {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
log.Warnf("[%s] [cacheResponse] cached value for key %s is empty", PLUGIN_NAME, key)
|
||||
return
|
||||
@@ -219,7 +220,7 @@ func cacheResponse(ctx wrapper.HttpContext, c config.PluginConfig, key string, v
|
||||
}
|
||||
|
||||
// Handles embedding upload if available
|
||||
func uploadEmbeddingAndAnswer(ctx wrapper.HttpContext, c config.PluginConfig, key string, value string, log wrapper.Log) {
|
||||
func uploadEmbeddingAndAnswer(ctx wrapper.HttpContext, c config.PluginConfig, key string, value string, log logs.Log) {
|
||||
embedding := ctx.GetContext(CACHE_KEY_EMBEDDING_KEY)
|
||||
if embedding == nil {
|
||||
return
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/log"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/wasm-go/pkg/log"
|
||||
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/log"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/wasm-go/pkg/log"
|
||||
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/log"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/wasm-go/pkg/log"
|
||||
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/log"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/wasm-go/pkg/log"
|
||||
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/log"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/wasm-go/pkg/log"
|
||||
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/log"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/wasm-go/pkg/log"
|
||||
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ package embedding
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/log"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/wasm-go/pkg/log"
|
||||
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/log"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/wasm-go/pkg/log"
|
||||
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
|
||||
@@ -2,23 +2,20 @@
|
||||
|
||||
module github.com/alibaba/higress/plugins/wasm-go/extensions/ai-cache
|
||||
|
||||
go 1.19
|
||||
go 1.24.1
|
||||
|
||||
replace github.com/alibaba/higress/plugins/wasm-go => ../..
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.2
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/higress-group/proxy-wasm-go-sdk v1.0.0
|
||||
github.com/tidwall/gjson v1.17.3
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80
|
||||
github.com/higress-group/wasm-go v1.0.0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
github.com/tidwall/resp v0.1.1
|
||||
// github.com/weaviate/weaviate-go-client/v4 v4.15.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 // indirect
|
||||
github.com/magefile/mage v1.14.0 // indirect
|
||||
github.com/stretchr/testify v1.9.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
)
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA=
|
||||
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/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80 h1:xqmtTZI0JQ2O+Lg9/CE6c+Tw9KD6FnvWw8EpLVuuvfg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.0 h1:4Ik5n3FsJ5+r13KLQl2ky+8NuAE8dfWQwoKxXYD2KAw=
|
||||
github.com/higress-group/wasm-go v1.0.0/go.mod h1:ODBV27sjmhIW8Cqv3R74EUcTnbdkE69bmXBQFuRkY1M=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94=
|
||||
github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -6,9 +6,10 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-cache/config"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/log"
|
||||
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
@@ -26,7 +27,9 @@ const (
|
||||
DEFAULT_MAX_BODY_BYTES uint32 = 100 * 1024 * 1024
|
||||
)
|
||||
|
||||
func main() {
|
||||
func main() {}
|
||||
|
||||
func init() {
|
||||
// CreateClient()
|
||||
wrapper.SetCtx(
|
||||
PLUGIN_NAME,
|
||||
@@ -38,7 +41,7 @@ func main() {
|
||||
)
|
||||
}
|
||||
|
||||
func parseConfig(json gjson.Result, c *config.PluginConfig, log wrapper.Log) error {
|
||||
func parseConfig(json gjson.Result, c *config.PluginConfig, log log.Log) error {
|
||||
// config.EmbeddingProviderConfig.FromJson(json.Get("embeddingProvider"))
|
||||
// config.VectorDatabaseProviderConfig.FromJson(json.Get("vectorBaseProvider"))
|
||||
// config.RedisConfig.FromJson(json.Get("redis"))
|
||||
@@ -54,7 +57,8 @@ func parseConfig(json gjson.Result, c *config.PluginConfig, log wrapper.Log) err
|
||||
return nil
|
||||
}
|
||||
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, c config.PluginConfig, log wrapper.Log) types.Action {
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, c config.PluginConfig, log log.Log) types.Action {
|
||||
ctx.DisableReroute()
|
||||
skipCache, _ := proxywasm.GetHttpRequestHeader(SKIP_CACHE_HEADER)
|
||||
if skipCache == "on" {
|
||||
ctx.SetContext(SKIP_CACHE_HEADER, struct{}{})
|
||||
@@ -78,7 +82,7 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, c config.PluginConfig, log wr
|
||||
return types.HeaderStopIteration
|
||||
}
|
||||
|
||||
func onHttpRequestBody(ctx wrapper.HttpContext, c config.PluginConfig, body []byte, log wrapper.Log) types.Action {
|
||||
func onHttpRequestBody(ctx wrapper.HttpContext, c config.PluginConfig, body []byte, log log.Log) types.Action {
|
||||
|
||||
bodyJson := gjson.ParseBytes(body)
|
||||
// TODO: It may be necessary to support stream mode determination for different LLM providers.
|
||||
@@ -128,7 +132,7 @@ func onHttpRequestBody(ctx wrapper.HttpContext, c config.PluginConfig, body []by
|
||||
return types.ActionPause
|
||||
}
|
||||
|
||||
func onHttpResponseHeaders(ctx wrapper.HttpContext, c config.PluginConfig, log wrapper.Log) types.Action {
|
||||
func onHttpResponseHeaders(ctx wrapper.HttpContext, c config.PluginConfig, log log.Log) types.Action {
|
||||
skipCache := ctx.GetContext(SKIP_CACHE_HEADER)
|
||||
if skipCache != nil {
|
||||
ctx.SetUserAttribute("cache_status", "skip")
|
||||
@@ -150,7 +154,7 @@ func onHttpResponseHeaders(ctx wrapper.HttpContext, c config.PluginConfig, log w
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
func onHttpResponseBody(ctx wrapper.HttpContext, c config.PluginConfig, chunk []byte, isLastChunk bool, log wrapper.Log) []byte {
|
||||
func onHttpResponseBody(ctx wrapper.HttpContext, c config.PluginConfig, chunk []byte, isLastChunk bool, log log.Log) []byte {
|
||||
log.Debugf("[onHttpResponseBody] is last chunk: %v", isLastChunk)
|
||||
log.Debugf("[onHttpResponseBody] chunk: %s", string(chunk))
|
||||
|
||||
|
||||
@@ -5,14 +5,13 @@ version: 1.0.0
|
||||
build:
|
||||
# The official builder image version
|
||||
builder:
|
||||
go: 1.19
|
||||
tinygo: 0.28.1
|
||||
go: 1.24.4
|
||||
oras: 1.0.0
|
||||
# The WASM plugin project directory
|
||||
input: ./
|
||||
# The output of the build products
|
||||
output:
|
||||
# Choose between 'files' and 'image'
|
||||
# Choose between 'files' and 'image'
|
||||
type: files
|
||||
# Destination address: when type=files, specify the local directory path, e.g., './out' or
|
||||
# type=image, specify the remote docker repository, e.g., 'docker.io/<your_username>/<your_image>'
|
||||
|
||||
@@ -6,11 +6,12 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-cache/config"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/wasm-go/pkg/log"
|
||||
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func handleNonStreamChunk(ctx wrapper.HttpContext, c config.PluginConfig, chunk []byte, log wrapper.Log) error {
|
||||
func handleNonStreamChunk(ctx wrapper.HttpContext, c config.PluginConfig, chunk []byte, log log.Log) error {
|
||||
tempContentI := ctx.GetContext(CACHE_CONTENT_CONTEXT_KEY)
|
||||
if tempContentI == nil {
|
||||
ctx.SetContext(CACHE_CONTENT_CONTEXT_KEY, chunk)
|
||||
@@ -28,7 +29,7 @@ func unifySSEChunk(data []byte) []byte {
|
||||
return data
|
||||
}
|
||||
|
||||
func handleStreamChunk(ctx wrapper.HttpContext, c config.PluginConfig, chunk []byte, log wrapper.Log) error {
|
||||
func handleStreamChunk(ctx wrapper.HttpContext, c config.PluginConfig, chunk []byte, log log.Log) error {
|
||||
var partialMessage []byte
|
||||
partialMessageI := ctx.GetContext(PARTIAL_MESSAGE_CONTEXT_KEY)
|
||||
log.Debugf("[handleStreamChunk] cache content: %v", ctx.GetContext(CACHE_CONTENT_CONTEXT_KEY))
|
||||
@@ -54,7 +55,7 @@ func handleStreamChunk(ctx wrapper.HttpContext, c config.PluginConfig, chunk []b
|
||||
return nil
|
||||
}
|
||||
|
||||
func processNonStreamLastChunk(ctx wrapper.HttpContext, c config.PluginConfig, chunk []byte, log wrapper.Log) (string, error) {
|
||||
func processNonStreamLastChunk(ctx wrapper.HttpContext, c config.PluginConfig, chunk []byte, log log.Log) (string, error) {
|
||||
var body []byte
|
||||
tempContentI := ctx.GetContext(CACHE_CONTENT_CONTEXT_KEY)
|
||||
if tempContentI != nil {
|
||||
@@ -70,7 +71,7 @@ func processNonStreamLastChunk(ctx wrapper.HttpContext, c config.PluginConfig, c
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func processStreamLastChunk(ctx wrapper.HttpContext, c config.PluginConfig, chunk []byte, log wrapper.Log) (string, error) {
|
||||
func processStreamLastChunk(ctx wrapper.HttpContext, c config.PluginConfig, chunk []byte, log log.Log) (string, error) {
|
||||
if len(chunk) > 0 {
|
||||
var lastMessage []byte
|
||||
partialMessageI := ctx.GetContext(PARTIAL_MESSAGE_CONTEXT_KEY)
|
||||
@@ -96,7 +97,7 @@ func processStreamLastChunk(ctx wrapper.HttpContext, c config.PluginConfig, chun
|
||||
return tempContentI.(string), nil
|
||||
}
|
||||
|
||||
func processSSEMessage(ctx wrapper.HttpContext, c config.PluginConfig, sseMessage string, log wrapper.Log) (string, error) {
|
||||
func processSSEMessage(ctx wrapper.HttpContext, c config.PluginConfig, sseMessage string, log log.Log) (string, error) {
|
||||
content := ""
|
||||
for _, chunk := range strings.Split(sseMessage, "\n\n") {
|
||||
log.Debugf("single sse message: %s", chunk)
|
||||
|
||||
@@ -6,7 +6,8 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/wasm-go/pkg/log"
|
||||
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||
)
|
||||
|
||||
type chromaProviderInitializer struct{}
|
||||
@@ -44,8 +45,8 @@ func (c *ChromaProvider) GetProviderType() string {
|
||||
func (d *ChromaProvider) QueryEmbedding(
|
||||
emb []float64,
|
||||
ctx wrapper.HttpContext,
|
||||
log wrapper.Log,
|
||||
callback func(results []QueryResult, ctx wrapper.HttpContext, log wrapper.Log, err error)) error {
|
||||
log log.Log,
|
||||
callback func(results []QueryResult, ctx wrapper.HttpContext, log log.Log, err error)) error {
|
||||
// 最少需要填写的参数为 collection_id, embeddings 和 ids
|
||||
// 下面是一个例子
|
||||
// {
|
||||
@@ -96,8 +97,8 @@ func (d *ChromaProvider) UploadAnswerAndEmbedding(
|
||||
queryEmb []float64,
|
||||
queryAnswer string,
|
||||
ctx wrapper.HttpContext,
|
||||
log wrapper.Log,
|
||||
callback func(ctx wrapper.HttpContext, log wrapper.Log, err error)) error {
|
||||
log log.Log,
|
||||
callback func(ctx wrapper.HttpContext, log log.Log, err error)) error {
|
||||
// 最少需要填写的参数为 collection_id, embeddings 和 ids
|
||||
// 下面是一个例子
|
||||
// {
|
||||
@@ -177,7 +178,7 @@ type chromaQueryResponse struct {
|
||||
Included []string `json:"included"`
|
||||
}
|
||||
|
||||
func (d *ChromaProvider) parseQueryResponse(responseBody []byte, log wrapper.Log) ([]QueryResult, error) {
|
||||
func (d *ChromaProvider) parseQueryResponse(responseBody []byte, log log.Log) ([]QueryResult, error) {
|
||||
var queryResp chromaQueryResponse
|
||||
err := json.Unmarshal(responseBody, &queryResp)
|
||||
if err != nil {
|
||||
|
||||
@@ -6,7 +6,8 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/wasm-go/pkg/log"
|
||||
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||
)
|
||||
|
||||
type dashVectorProviderInitializer struct {
|
||||
@@ -119,8 +120,8 @@ func (d *DvProvider) parseQueryResponse(responseBody []byte) (queryResponse, err
|
||||
func (d *DvProvider) QueryEmbedding(
|
||||
emb []float64,
|
||||
ctx wrapper.HttpContext,
|
||||
log wrapper.Log,
|
||||
callback func(results []QueryResult, ctx wrapper.HttpContext, log wrapper.Log, err error)) error {
|
||||
log log.Log,
|
||||
callback func(results []QueryResult, ctx wrapper.HttpContext, log log.Log, err error)) error {
|
||||
url, body, headers, err := d.constructEmbeddingQueryParameters(emb)
|
||||
log.Debugf("url:%s, body:%s, headers:%v", url, string(body), headers)
|
||||
if err != nil {
|
||||
@@ -157,7 +158,7 @@ func getStringValue(fields map[string]interface{}, key string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (d *DvProvider) ParseQueryResponse(responseBody []byte, ctx wrapper.HttpContext, log wrapper.Log) ([]QueryResult, error) {
|
||||
func (d *DvProvider) ParseQueryResponse(responseBody []byte, ctx wrapper.HttpContext, log log.Log) ([]QueryResult, error) {
|
||||
resp, err := d.parseQueryResponse(responseBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -215,7 +216,7 @@ func (d *DvProvider) constructUploadParameters(emb []float64, queryString string
|
||||
return url, requestBody, header, err
|
||||
}
|
||||
|
||||
func (d *DvProvider) UploadEmbedding(queryString string, queryEmb []float64, ctx wrapper.HttpContext, log wrapper.Log, callback func(ctx wrapper.HttpContext, log wrapper.Log, err error)) error {
|
||||
func (d *DvProvider) UploadEmbedding(queryString string, queryEmb []float64, ctx wrapper.HttpContext, log log.Log, callback func(ctx wrapper.HttpContext, log log.Log, err error)) error {
|
||||
url, body, headers, err := d.constructUploadParameters(queryEmb, queryString, "")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -235,7 +236,7 @@ func (d *DvProvider) UploadEmbedding(queryString string, queryEmb []float64, ctx
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *DvProvider) UploadAnswerAndEmbedding(queryString string, queryEmb []float64, queryAnswer string, ctx wrapper.HttpContext, log wrapper.Log, callback func(ctx wrapper.HttpContext, log wrapper.Log, err error)) error {
|
||||
func (d *DvProvider) UploadAnswerAndEmbedding(queryString string, queryEmb []float64, queryAnswer string, ctx wrapper.HttpContext, log log.Log, callback func(ctx wrapper.HttpContext, log log.Log, err error)) error {
|
||||
url, body, headers, err := d.constructUploadParameters(queryEmb, queryString, queryAnswer)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/wasm-go/pkg/log"
|
||||
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||
)
|
||||
|
||||
type esProviderInitializer struct{}
|
||||
@@ -45,8 +46,8 @@ func (c *ESProvider) GetProviderType() string {
|
||||
func (d *ESProvider) QueryEmbedding(
|
||||
emb []float64,
|
||||
ctx wrapper.HttpContext,
|
||||
log wrapper.Log,
|
||||
callback func(results []QueryResult, ctx wrapper.HttpContext, log wrapper.Log, err error)) error {
|
||||
log log.Log,
|
||||
callback func(results []QueryResult, ctx wrapper.HttpContext, log log.Log, err error)) error {
|
||||
|
||||
requestBody, err := json.Marshal(esQueryRequest{
|
||||
Source: Source{Excludes: []string{"embedding"}},
|
||||
@@ -99,8 +100,8 @@ func (d *ESProvider) UploadAnswerAndEmbedding(
|
||||
queryEmb []float64,
|
||||
queryAnswer string,
|
||||
ctx wrapper.HttpContext,
|
||||
log wrapper.Log,
|
||||
callback func(ctx wrapper.HttpContext, log wrapper.Log, err error)) error {
|
||||
log log.Log,
|
||||
callback func(ctx wrapper.HttpContext, log log.Log, err error)) error {
|
||||
// 最少需要填写的参数为 index, embeddings 和 question
|
||||
// 下面是一个例子
|
||||
// POST /<index>/_doc
|
||||
@@ -176,7 +177,7 @@ type esQueryResponse struct {
|
||||
} `json:"hits"`
|
||||
}
|
||||
|
||||
func (d *ESProvider) parseQueryResponse(responseBody []byte, log wrapper.Log) ([]QueryResult, error) {
|
||||
func (d *ESProvider) parseQueryResponse(responseBody []byte, log log.Log) ([]QueryResult, error) {
|
||||
log.Infof("[ES] responseBody: %s", string(responseBody))
|
||||
var queryResp esQueryResponse
|
||||
err := json.Unmarshal(responseBody, &queryResp)
|
||||
|
||||
@@ -6,7 +6,8 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/wasm-go/pkg/log"
|
||||
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
@@ -58,8 +59,8 @@ func (d *milvusProvider) UploadAnswerAndEmbedding(
|
||||
queryEmb []float64,
|
||||
queryAnswer string,
|
||||
ctx wrapper.HttpContext,
|
||||
log wrapper.Log,
|
||||
callback func(ctx wrapper.HttpContext, log wrapper.Log, err error)) error {
|
||||
log log.Log,
|
||||
callback func(ctx wrapper.HttpContext, log log.Log, err error)) error {
|
||||
// 最少需要填写的参数为 collectionName, data 和 Authorization. question, answer 可选
|
||||
// 需要填写 id,否则 v2.4.13-hotfix 提示 invalid syntax: invalid parameter[expected=Int64][actual=]
|
||||
// 如果不填写 id,要在创建 collection 的时候设置 autoId 为 true
|
||||
@@ -120,8 +121,8 @@ type milvusQueryRequest struct {
|
||||
func (d *milvusProvider) QueryEmbedding(
|
||||
emb []float64,
|
||||
ctx wrapper.HttpContext,
|
||||
log wrapper.Log,
|
||||
callback func(results []QueryResult, ctx wrapper.HttpContext, log wrapper.Log, err error)) error {
|
||||
log log.Log,
|
||||
callback func(results []QueryResult, ctx wrapper.HttpContext, log log.Log, err error)) error {
|
||||
// 最少需要填写的参数为 collectionName, data, annsField. outputFields 为可选参数
|
||||
// 下面是一个例子
|
||||
// {
|
||||
@@ -175,7 +176,7 @@ func (d *milvusProvider) QueryEmbedding(
|
||||
)
|
||||
}
|
||||
|
||||
func (d *milvusProvider) parseQueryResponse(responseBody []byte, log wrapper.Log) ([]QueryResult, error) {
|
||||
func (d *milvusProvider) parseQueryResponse(responseBody []byte, log log.Log) ([]QueryResult, error) {
|
||||
if !gjson.GetBytes(responseBody, "data.0.distance").Exists() {
|
||||
log.Errorf("[Milvus] No distance found in response body: %s", responseBody)
|
||||
return nil, errors.New("[Milvus] No distance found in response body")
|
||||
|
||||
@@ -6,8 +6,9 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/google/uuid"
|
||||
"github.com/higress-group/wasm-go/pkg/log"
|
||||
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
@@ -67,8 +68,8 @@ func (d *pineconeProvider) UploadAnswerAndEmbedding(
|
||||
queryEmb []float64,
|
||||
queryAnswer string,
|
||||
ctx wrapper.HttpContext,
|
||||
log wrapper.Log,
|
||||
callback func(ctx wrapper.HttpContext, log wrapper.Log, err error)) error {
|
||||
log log.Log,
|
||||
callback func(ctx wrapper.HttpContext, log log.Log, err error)) error {
|
||||
// 最少需要填写的参数为 vector 和 question
|
||||
// 下面是一个例子
|
||||
// {
|
||||
@@ -122,8 +123,8 @@ type pineconeQueryRequest struct {
|
||||
func (d *pineconeProvider) QueryEmbedding(
|
||||
emb []float64,
|
||||
ctx wrapper.HttpContext,
|
||||
log wrapper.Log,
|
||||
callback func(results []QueryResult, ctx wrapper.HttpContext, log wrapper.Log, err error)) error {
|
||||
log log.Log,
|
||||
callback func(results []QueryResult, ctx wrapper.HttpContext, log log.Log, err error)) error {
|
||||
// 最少需要填写的参数为 vector
|
||||
// 下面是一个例子
|
||||
// {
|
||||
@@ -163,7 +164,7 @@ func (d *pineconeProvider) QueryEmbedding(
|
||||
)
|
||||
}
|
||||
|
||||
func (d *pineconeProvider) parseQueryResponse(responseBody []byte, log wrapper.Log) ([]QueryResult, error) {
|
||||
func (d *pineconeProvider) parseQueryResponse(responseBody []byte, log log.Log) ([]QueryResult, error) {
|
||||
if !gjson.GetBytes(responseBody, "matches.0.score").Exists() {
|
||||
log.Errorf("[Pinecone] No distance found in response body: %s", responseBody)
|
||||
return nil, errors.New("[Pinecone] No distance found in response body")
|
||||
|
||||
@@ -3,7 +3,8 @@ package vector
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/wasm-go/pkg/log"
|
||||
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
@@ -50,8 +51,8 @@ type EmbeddingQuerier interface {
|
||||
QueryEmbedding(
|
||||
emb []float64,
|
||||
ctx wrapper.HttpContext,
|
||||
log wrapper.Log,
|
||||
callback func(results []QueryResult, ctx wrapper.HttpContext, log wrapper.Log, err error)) error
|
||||
log log.Log,
|
||||
callback func(results []QueryResult, ctx wrapper.HttpContext, log log.Log, err error)) error
|
||||
}
|
||||
|
||||
type EmbeddingUploader interface {
|
||||
@@ -59,8 +60,8 @@ type EmbeddingUploader interface {
|
||||
queryString string,
|
||||
queryEmb []float64,
|
||||
ctx wrapper.HttpContext,
|
||||
log wrapper.Log,
|
||||
callback func(ctx wrapper.HttpContext, log wrapper.Log, err error)) error
|
||||
log log.Log,
|
||||
callback func(ctx wrapper.HttpContext, log log.Log, err error)) error
|
||||
}
|
||||
|
||||
type AnswerAndEmbeddingUploader interface {
|
||||
@@ -69,16 +70,16 @@ type AnswerAndEmbeddingUploader interface {
|
||||
queryEmb []float64,
|
||||
answer string,
|
||||
ctx wrapper.HttpContext,
|
||||
log wrapper.Log,
|
||||
callback func(ctx wrapper.HttpContext, log wrapper.Log, err error)) error
|
||||
log log.Log,
|
||||
callback func(ctx wrapper.HttpContext, log log.Log, err error)) error
|
||||
}
|
||||
|
||||
type StringQuerier interface {
|
||||
QueryString(
|
||||
queryString string,
|
||||
ctx wrapper.HttpContext,
|
||||
log wrapper.Log,
|
||||
callback func(results []QueryResult, ctx wrapper.HttpContext, log wrapper.Log, err error)) error
|
||||
log log.Log,
|
||||
callback func(results []QueryResult, ctx wrapper.HttpContext, log log.Log, err error)) error
|
||||
}
|
||||
|
||||
type ProviderConfig struct {
|
||||
|
||||
@@ -6,8 +6,9 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/google/uuid"
|
||||
"github.com/higress-group/wasm-go/pkg/log"
|
||||
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
@@ -63,8 +64,8 @@ func (d *qdrantProvider) UploadAnswerAndEmbedding(
|
||||
queryEmb []float64,
|
||||
queryAnswer string,
|
||||
ctx wrapper.HttpContext,
|
||||
log wrapper.Log,
|
||||
callback func(ctx wrapper.HttpContext, log wrapper.Log, err error)) error {
|
||||
log log.Log,
|
||||
callback func(ctx wrapper.HttpContext, log log.Log, err error)) error {
|
||||
// 最少需要填写的参数为 id 和 vector. payload 可选
|
||||
// 下面是一个例子
|
||||
// {
|
||||
@@ -122,8 +123,8 @@ type qdrantQueryRequest struct {
|
||||
func (d *qdrantProvider) QueryEmbedding(
|
||||
emb []float64,
|
||||
ctx wrapper.HttpContext,
|
||||
log wrapper.Log,
|
||||
callback func(results []QueryResult, ctx wrapper.HttpContext, log wrapper.Log, err error)) error {
|
||||
log log.Log,
|
||||
callback func(results []QueryResult, ctx wrapper.HttpContext, log log.Log, err error)) error {
|
||||
// 最少需要填写的参数为 vector 和 limit. with_payload 可选,为了直接得到问题答案,所以这里需要
|
||||
// 下面是一个例子
|
||||
// {
|
||||
@@ -164,7 +165,7 @@ func (d *qdrantProvider) QueryEmbedding(
|
||||
)
|
||||
}
|
||||
|
||||
func (d *qdrantProvider) parseQueryResponse(responseBody []byte, log wrapper.Log) ([]QueryResult, error) {
|
||||
func (d *qdrantProvider) parseQueryResponse(responseBody []byte, log log.Log) ([]QueryResult, error) {
|
||||
// 返回的内容例子如下
|
||||
// {
|
||||
// "time": 0.002,
|
||||
|
||||
@@ -6,7 +6,8 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/wasm-go/pkg/log"
|
||||
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
@@ -45,8 +46,8 @@ func (c *WeaviateProvider) GetProviderType() string {
|
||||
func (d *WeaviateProvider) QueryEmbedding(
|
||||
emb []float64,
|
||||
ctx wrapper.HttpContext,
|
||||
log wrapper.Log,
|
||||
callback func(results []QueryResult, ctx wrapper.HttpContext, log wrapper.Log, err error)) error {
|
||||
log log.Log,
|
||||
callback func(results []QueryResult, ctx wrapper.HttpContext, log log.Log, err error)) error {
|
||||
// 最少需要填写的参数为 class, vector
|
||||
// 下面是一个例子
|
||||
// {"query": "{ Get { Higress ( limit: 2 nearVector: { vector: [0.1, 0.2, 0.3] } ) { question _additional { distance } } } }"}
|
||||
@@ -109,8 +110,8 @@ func (d *WeaviateProvider) UploadAnswerAndEmbedding(
|
||||
queryEmb []float64,
|
||||
queryAnswer string,
|
||||
ctx wrapper.HttpContext,
|
||||
log wrapper.Log,
|
||||
callback func(ctx wrapper.HttpContext, log wrapper.Log, err error)) error {
|
||||
log log.Log,
|
||||
callback func(ctx wrapper.HttpContext, log log.Log, err error)) error {
|
||||
// 最少需要填写的参数为 class, vector 和 question 和 answer
|
||||
// 下面是一个例子
|
||||
// {"class": "Higress", "vector": [0.1, 0.2, 0.3], "properties": {"question": "这里是问题", "answer": "这里是答案"}}
|
||||
@@ -155,7 +156,7 @@ type weaviateQueryRequest struct {
|
||||
Query string `json:"query"`
|
||||
}
|
||||
|
||||
func (d *WeaviateProvider) parseQueryResponse(responseBody []byte, log wrapper.Log) ([]QueryResult, error) {
|
||||
func (d *WeaviateProvider) parseQueryResponse(responseBody []byte, log log.Log) ([]QueryResult, error) {
|
||||
log.Infof("[Weaviate] queryResp: %s", string(responseBody))
|
||||
|
||||
if !gjson.GetBytes(responseBody, fmt.Sprintf("data.Get.%s.0._additional.distance", d.config.collectionID)).Exists() {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
EXTRA_TAGS=proxy_wasm_version_0_2_100
|
||||
@@ -2,21 +2,19 @@
|
||||
|
||||
module github.com/alibaba/higress/plugins/wasm-go/extensions/ai-history
|
||||
|
||||
go 1.19
|
||||
go 1.24.1
|
||||
|
||||
replace github.com/alibaba/higress/plugins/wasm-go => ../..
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.3.6-0.20240528060522-53bccf89f441
|
||||
github.com/higress-group/proxy-wasm-go-sdk v1.0.0
|
||||
github.com/tidwall/gjson v1.17.3
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80
|
||||
github.com/higress-group/wasm-go v1.0.0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
github.com/tidwall/resp v0.1.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 // indirect
|
||||
github.com/magefile/mage v1.14.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
)
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA=
|
||||
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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80 h1:xqmtTZI0JQ2O+Lg9/CE6c+Tw9KD6FnvWw8EpLVuuvfg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.0 h1:4Ik5n3FsJ5+r13KLQl2ky+8NuAE8dfWQwoKxXYD2KAw=
|
||||
github.com/higress-group/wasm-go v1.0.0/go.mod h1:ODBV27sjmhIW8Cqv3R74EUcTnbdkE69bmXBQFuRkY1M=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94=
|
||||
github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -11,9 +11,10 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/log"
|
||||
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/resp"
|
||||
)
|
||||
@@ -29,7 +30,9 @@ const (
|
||||
ChatHistories = "chatHistories"
|
||||
)
|
||||
|
||||
func main() {
|
||||
func main() {}
|
||||
|
||||
func init() {
|
||||
wrapper.SetCtx(
|
||||
"ai-history",
|
||||
wrapper.ParseConfigBy(parseConfig),
|
||||
@@ -121,7 +124,7 @@ type ChatHistory struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
func parseConfig(json gjson.Result, c *PluginConfig, log wrapper.Log) error {
|
||||
func parseConfig(json gjson.Result, c *PluginConfig, log log.Log) error {
|
||||
c.RedisInfo.ServiceName = json.Get("redis.serviceName").String()
|
||||
if c.RedisInfo.ServiceName == "" {
|
||||
return errors.New("redis service name must not be empty")
|
||||
@@ -166,7 +169,8 @@ func parseConfig(json gjson.Result, c *PluginConfig, log wrapper.Log) error {
|
||||
return c.redisClient.Init(c.RedisInfo.Username, c.RedisInfo.Password, int64(c.RedisInfo.Timeout), wrapper.WithDataBase(c.RedisInfo.Database))
|
||||
}
|
||||
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config PluginConfig, log wrapper.Log) types.Action {
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config PluginConfig, log log.Log) types.Action {
|
||||
ctx.DisableReroute()
|
||||
contentType, _ := proxywasm.GetHttpRequestHeader("content-type")
|
||||
if !strings.Contains(contentType, "application/json") {
|
||||
log.Warnf("content is not json, can't process:%s", contentType)
|
||||
@@ -192,7 +196,7 @@ func TrimQuote(source string) string {
|
||||
return strings.Trim(source, `"`)
|
||||
}
|
||||
|
||||
func onHttpRequestBody(ctx wrapper.HttpContext, config PluginConfig, body []byte, log wrapper.Log) types.Action {
|
||||
func onHttpRequestBody(ctx wrapper.HttpContext, config PluginConfig, body []byte, log log.Log) types.Action {
|
||||
bodyJson := gjson.ParseBytes(body)
|
||||
if bodyJson.Get("stream").Bool() {
|
||||
ctx.SetContext(StreamContextKey, struct{}{})
|
||||
@@ -319,7 +323,7 @@ func getIntQueryParameter(name string, path string, defaultValue int) int {
|
||||
return num
|
||||
}
|
||||
|
||||
func processSSEMessage(ctx wrapper.HttpContext, config PluginConfig, sseMessage string, log wrapper.Log) string {
|
||||
func processSSEMessage(ctx wrapper.HttpContext, config PluginConfig, sseMessage string, log log.Log) string {
|
||||
content := ""
|
||||
for _, chunk := range strings.Split(sseMessage, "\n\n") {
|
||||
subMessages := strings.Split(chunk, "\n")
|
||||
@@ -355,14 +359,14 @@ func processSSEMessage(ctx wrapper.HttpContext, config PluginConfig, sseMessage
|
||||
return content
|
||||
}
|
||||
|
||||
func onHttpResponseHeaders(ctx wrapper.HttpContext, config PluginConfig, log wrapper.Log) types.Action {
|
||||
func onHttpResponseHeaders(ctx wrapper.HttpContext, config PluginConfig, log log.Log) types.Action {
|
||||
contentType, _ := proxywasm.GetHttpResponseHeader("content-type")
|
||||
if strings.Contains(contentType, "text/event-stream") {
|
||||
ctx.SetContext(StreamContextKey, struct{}{})
|
||||
}
|
||||
return types.ActionContinue
|
||||
}
|
||||
func onHttpStreamResponseBody(ctx wrapper.HttpContext, config PluginConfig, chunk []byte, isLastChunk bool, log wrapper.Log) []byte {
|
||||
func onHttpStreamResponseBody(ctx wrapper.HttpContext, config PluginConfig, chunk []byte, isLastChunk bool, log log.Log) []byte {
|
||||
if ctx.GetContext(ToolCallsContextKey) != nil {
|
||||
// we should not cache tool call result
|
||||
return chunk
|
||||
@@ -454,7 +458,7 @@ func onHttpStreamResponseBody(ctx wrapper.HttpContext, config PluginConfig, chun
|
||||
return chunk
|
||||
}
|
||||
|
||||
func saveChatHistory(ctx wrapper.HttpContext, config PluginConfig, questionI any, value string, log wrapper.Log) {
|
||||
func saveChatHistory(ctx wrapper.HttpContext, config PluginConfig, questionI any, value string, log log.Log) {
|
||||
question := questionI.(string)
|
||||
identityKey := ctx.GetStringContext(IdentityKey, "")
|
||||
var chat []ChatHistory
|
||||
|
||||
@@ -5,14 +5,13 @@ version: 1.0.0
|
||||
build:
|
||||
# The official builder image version
|
||||
builder:
|
||||
go: 1.19
|
||||
tinygo: 0.28.1
|
||||
go: 1.24.4
|
||||
oras: 1.0.0
|
||||
# The WASM plugin project directory
|
||||
input: ./
|
||||
# The output of the build products
|
||||
output:
|
||||
# Choose between 'files' and 'image'
|
||||
# Choose between 'files' and 'image'
|
||||
type: files
|
||||
# Destination address: when type=files, specify the local directory path, e.g., './out' or
|
||||
# type=image, specify the remote docker repository, e.g., 'docker.io/<your_username>/<your_image>'
|
||||
|
||||
98
plugins/wasm-go/extensions/ai-image-reader/README.md
Normal file
98
plugins/wasm-go/extensions/ai-image-reader/README.md
Normal file
@@ -0,0 +1,98 @@
|
||||
---
|
||||
title: AI IMAGE READER
|
||||
keywords: [ AI网关, AI IMAGE READER ]
|
||||
description: AI IMAGE READER 插件配置参考
|
||||
|
||||
---
|
||||
|
||||
## 功能说明
|
||||
|
||||
通过对接OCR服务实现AI-IMAGE-READER,目前支持阿里云模型服务灵积(dashscope)的qwen-vl-ocr模型提供OCR服务,流程如图所示:
|
||||
|
||||
<img src=".\ai-image-reader.png">
|
||||
|
||||
## 运行属性
|
||||
|
||||
插件执行阶段:`默认阶段`
|
||||
插件执行优先级:`400`
|
||||
|
||||
|
||||
## 配置说明
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| ------------- | -------- | -------- | ------ | -------------------------------------- |
|
||||
| `apiKey` | string | 必填 | - | 用于在访问OCR服务时进行认证的令牌。 |
|
||||
| `type` | string | 必填 | - | 后端OCR服务提供商类型(例如dashscope) |
|
||||
| `serviceHost` | string | 必填 | - | 后端OCR服务域名 |
|
||||
| `serviceName` | string | 必填 | - | 后端OCR服务名 |
|
||||
| `servicePort` | int | 必填 | - | 后端OCR服务端口 |
|
||||
| `model` | string | 必填 | - | 后端OCR服务模型名称(例如qwen-vl-ocr) |
|
||||
| `timeout` | int | 选填 | 10000 | API调用超时时间(毫秒) |
|
||||
|
||||
## 示例
|
||||
|
||||
```yaml
|
||||
"apiKey": "YOUR_API_KEY",
|
||||
"type": "dashscope",
|
||||
"model": "qwen-vl-ocr",
|
||||
"timeout": 10000,
|
||||
"serviceHost": "dashscope.aliyuncs.com",
|
||||
"serviceName": "dashscope",
|
||||
"servicePort": "443"
|
||||
```
|
||||
|
||||
请求遵循openai api协议规范:
|
||||
|
||||
URL传递图片:
|
||||
|
||||
```
|
||||
messages=[{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "What's in this image?"},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20241108/ctdzex/biaozhun.jpg",
|
||||
},
|
||||
},
|
||||
],
|
||||
}],
|
||||
```
|
||||
|
||||
Base64编码传递图片:
|
||||
|
||||
```
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{ "type": "text", "text": "what's in this image?" },
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": f"data:image/jpeg;base64,{base64_image}",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
```
|
||||
|
||||
以下为使用ai-image-reader进行增强的例子,原始请求为:
|
||||
|
||||
```
|
||||
图片内容是什么?
|
||||
```
|
||||
|
||||
未经过ai-image-reader插件处理LLM返回的结果为:
|
||||
|
||||
```
|
||||
对不起,作为一个文本AI助手,我无法查看图片内容。您可以描述一下图片的内容,我可以尽力帮助您识别。
|
||||
```
|
||||
|
||||
经过ai-image-reader插件处理后LLM返回的结果为:
|
||||
|
||||
```
|
||||
非常感谢您分享的图片内容!根据您提供的文字信息,学习编写shell脚本对Linux系统管理员来说是非常有益的。通过自动化系统管理任务,可以提高效率并减少手动操作的时间。对于家用Linux爱好者来说,了解如何在命令行下操作也是很重要的,因为在某些情况下,命令行操作可能更为便捷和高效。在本书中,您将学习如何运用shell脚本处理系统管理任务,以及如何在Linux命令行下进行操作。希望这本书能够帮助您更好地理解和应用Linux系统管理和操作的知识!如果您有任何其他问题或需要进一步帮助,请随时告诉我。
|
||||
```
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user