mirror of
https://github.com/alibaba/higress.git
synced 2026-02-25 21:21:01 +08:00
Compare commits
112 Commits
v2.1.9-rc.
...
v2.1.10
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce298054f1 | ||
|
|
24c69fb0b7 | ||
|
|
a38be77b9e | ||
|
|
27999dcc59 | ||
|
|
811179a6a0 | ||
|
|
5f43dd0224 | ||
|
|
e23ab3ca7c | ||
|
|
032a69556f | ||
|
|
ee6bb11730 | ||
|
|
fc600f204a | ||
|
|
357418853f | ||
|
|
e8586cccd7 | ||
|
|
d55b9a0837 | ||
|
|
4f04ac067b | ||
|
|
c7028bd7f2 | ||
|
|
95ff52cde9 | ||
|
|
7c7205b572 | ||
|
|
f342f50ca4 | ||
|
|
659d136bfe | ||
|
|
541e5e206f | ||
|
|
387c337654 | ||
|
|
8024a96881 | ||
|
|
f71c1900a8 | ||
|
|
1199946d36 | ||
|
|
b1571de6f0 | ||
|
|
20dae295a8 | ||
|
|
9a1f9e4606 | ||
|
|
6f4ef33590 | ||
|
|
fef8ecc822 | ||
|
|
0ade9504be | ||
|
|
6311fecfce | ||
|
|
5c225de080 | ||
|
|
bf9ef5eefd | ||
|
|
26f5737a80 | ||
|
|
50c1a5e78c | ||
|
|
647304eb45 | ||
|
|
0a7fc9f412 | ||
|
|
c9253264ef | ||
|
|
8c80084ada | ||
|
|
9f5ee99c2d | ||
|
|
3770bd2f55 | ||
|
|
698a395e89 | ||
|
|
2c72767203 | ||
|
|
bb3ac59834 | ||
|
|
6c1fe57034 | ||
|
|
5c5cc6ac90 | ||
|
|
265da8e4d6 | ||
|
|
119698eea4 | ||
|
|
18d20ca135 | ||
|
|
9978db2ac6 | ||
|
|
1582fa6ef9 | ||
|
|
2b49fd5b26 | ||
|
|
48433a6549 | ||
|
|
8ec48b3b85 | ||
|
|
32007d2ab8 | ||
|
|
27b088fc7e | ||
|
|
399dcb1ead | ||
|
|
810ef8f80b | ||
|
|
0dc69d5941 | ||
|
|
51e1804c5c | ||
|
|
ec5031c2f5 | ||
|
|
c3077d7981 | ||
|
|
0694616256 | ||
|
|
cdf0f16bf6 | ||
|
|
ca64c9a1c7 | ||
|
|
ec099e0a24 | ||
|
|
135a6b622f | ||
|
|
95077a1138 | ||
|
|
4a6d78380a | ||
|
|
8a3c0bb342 | ||
|
|
1300e09e28 | ||
|
|
d1998804c6 | ||
|
|
d4e6704f33 | ||
|
|
36df9ba5e8 | ||
|
|
826c4e8b4a | ||
|
|
1900609fd5 | ||
|
|
f79e3b9556 | ||
|
|
1602b6f94a | ||
|
|
d745bc0d0b | ||
|
|
ef6baf29e8 | ||
|
|
ccbb542fec | ||
|
|
af8748d754 | ||
|
|
b4c6903412 | ||
|
|
1e2975f669 | ||
|
|
ded2b80c83 | ||
|
|
5cc7454775 | ||
|
|
d386739e48 | ||
|
|
5e4c262814 | ||
|
|
268cf717fb | ||
|
|
2a320f87a6 | ||
|
|
2076ded06f | ||
|
|
1bcef0c00c | ||
|
|
7c4899ad38 | ||
|
|
7ea739292d | ||
|
|
17f899d860 | ||
|
|
7476fe7454 | ||
|
|
b1b39e285a | ||
|
|
5fc1d6b222 | ||
|
|
271e6036fa | ||
|
|
264a38c9ae | ||
|
|
94680379a3 | ||
|
|
0d7d4218d4 | ||
|
|
817cd322ff | ||
|
|
a7cd4c0ad6 | ||
|
|
a98971f8d5 | ||
|
|
67b92b76fe | ||
|
|
6b2d06a330 | ||
|
|
1f301be851 | ||
|
|
b026455701 | ||
|
|
15db773e24 | ||
|
|
fe69084c04 | ||
|
|
fcc7fc0139 |
135
.cursor/rules/plugin-development.mdc
Normal file
135
.cursor/rules/plugin-development.mdc
Normal file
@@ -0,0 +1,135 @@
|
||||
---
|
||||
description: Plugin Development Standards - Applies to all new wasm and golang-filter plugins
|
||||
globs:
|
||||
- "plugins/wasm-go/extensions/*/**"
|
||||
- "plugins/wasm-cpp/extensions/*/**"
|
||||
- "plugins/wasm-rust/extensions/*/**"
|
||||
- "plugins/wasm-assemblyscript/extensions/*/**"
|
||||
- "plugins/golang-filter/*/**"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Plugin Development Standards
|
||||
|
||||
## Strict Requirements for New Independent Plugins
|
||||
|
||||
When creating **new independent plugins** (e.g., newly implemented wasm plugins or golang-filter plugins), you **MUST** follow these standards:
|
||||
|
||||
### 1. Design Documentation Directory Requirements
|
||||
|
||||
- You **MUST** create a `design/` directory within the plugin directory
|
||||
- Directory structure example:
|
||||
```
|
||||
plugins/wasm-go/extensions/my-new-plugin/
|
||||
├── design/
|
||||
│ ├── design-doc.md # Design document
|
||||
│ ├── architecture.md # Architecture (optional)
|
||||
│ └── requirements.md # Requirements (optional)
|
||||
├── main.go
|
||||
├── go.mod
|
||||
└── README.md
|
||||
```
|
||||
|
||||
### 2. Design Documentation Content Requirements
|
||||
|
||||
The design documentation in the `design/` directory should include:
|
||||
|
||||
- **Plugin Purpose and Use Cases**: Clearly explain what problem the plugin solves
|
||||
- **Core Functionality Design**: Detailed description of main features and implementation approach
|
||||
- **Configuration Parameters**: List all configuration items and their meanings
|
||||
- **Technology Selection and Dependencies**: Explain the technology stack and third-party libraries used
|
||||
- **Boundary Conditions and Limitations**: Define the applicable scope and limitations of the plugin
|
||||
- **Testing Strategy**: How to verify plugin functionality
|
||||
|
||||
### 3. Documentation Provided to AI Coding Tools
|
||||
|
||||
If you are using AI Coding tools (such as Cursor, GitHub Copilot, etc.) to generate code:
|
||||
|
||||
- You **MUST** save the complete design documents, requirement descriptions, and prompts you provided to the AI in the `design/` directory
|
||||
- Recommended file naming:
|
||||
- `ai-prompts.md` - AI prompts record
|
||||
- `design-doc.md` - Complete design document
|
||||
- `requirements.md` - Feature requirements list
|
||||
|
||||
### 4. Files NOT to Commit to Git
|
||||
|
||||
Note: The following files should **NOT** be committed to Git:
|
||||
- AI Coding tool work summary documents (should be placed in PR description)
|
||||
- Temporary feature change summary documents
|
||||
|
||||
Design documents in the `design/` directory **SHOULD** be committed to Git, as they serve as the design basis and technical documentation for the plugin.
|
||||
|
||||
## Examples
|
||||
|
||||
### Good Plugin Directory Structure Example
|
||||
|
||||
```
|
||||
plugins/wasm-go/extensions/ai-security-guard/
|
||||
├── design/
|
||||
│ ├── design-doc.md # ✅ Detailed design document
|
||||
│ ├── ai-prompts.md # ✅ Prompts provided to AI
|
||||
│ └── architecture.png # ✅ Architecture diagram
|
||||
├── main.go
|
||||
├── config.go
|
||||
├── README.md
|
||||
└── go.mod
|
||||
```
|
||||
|
||||
### Design Document Template
|
||||
|
||||
When creating `design/design-doc.md`, you can refer to the following template:
|
||||
|
||||
```markdown
|
||||
# [Plugin Name] Design Document
|
||||
|
||||
## Overview
|
||||
- Plugin purpose
|
||||
- Problem it solves
|
||||
- Target users
|
||||
|
||||
## Functional Design
|
||||
### Core Feature 1
|
||||
- Feature description
|
||||
- Implementation approach
|
||||
- Key code logic
|
||||
|
||||
### Core Feature 2
|
||||
...
|
||||
|
||||
## Configuration Parameters
|
||||
| Parameter | Type | Required | Description | Default |
|
||||
|-----------|------|----------|-------------|---------|
|
||||
| ... | ... | ... | ... | ... |
|
||||
|
||||
## Technical Implementation
|
||||
- Technology selection
|
||||
- Dependencies
|
||||
- Performance considerations
|
||||
|
||||
## Test Plan
|
||||
- Unit tests
|
||||
- Integration tests
|
||||
- Boundary tests
|
||||
|
||||
## Limitations and Notes
|
||||
- Known limitations
|
||||
- Usage recommendations
|
||||
```
|
||||
|
||||
## Execution Checklist
|
||||
|
||||
When creating a new plugin, please confirm:
|
||||
|
||||
- [ ] Created `design/` directory within the plugin directory
|
||||
- [ ] Placed design documentation in the `design/` directory
|
||||
- [ ] If using AI Coding tools, saved prompts/requirement documents in the `design/` directory
|
||||
- [ ] Prepared AI Coding tool work summary (for PR description)
|
||||
- [ ] Design documentation is complete with necessary technical details
|
||||
|
||||
## Tips
|
||||
|
||||
- Design documentation is important technical documentation for the plugin, helpful for:
|
||||
- Understanding design intent during code review
|
||||
- Quickly understanding implementation approach during future maintenance
|
||||
- Learning and reference for other developers
|
||||
- Tracing the reasoning behind design decisions
|
||||
44
.github/PULL_REQUEST_TEMPLATE.md
vendored
44
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,17 +1,51 @@
|
||||
<!-- Please make sure you have read and understood the contributing guidelines -->
|
||||
|
||||
### Ⅰ. Describe what this PR did
|
||||
## Ⅰ. Describe what this PR did
|
||||
|
||||
|
||||
### Ⅱ. Does this pull request fix one issue?
|
||||
## Ⅱ. Does this pull request fix one issue?
|
||||
<!-- If that, add "fixes #xxx" below in the next line, for example, fixes #97. -->
|
||||
|
||||
|
||||
### Ⅲ. Why don't you add test cases (unit test/integration test)?
|
||||
## Ⅲ. Why don't you add test cases (unit test/integration test)?
|
||||
|
||||
|
||||
### Ⅳ. Describe how to verify it
|
||||
## Ⅳ. Describe how to verify it
|
||||
|
||||
|
||||
## Ⅴ. Special notes for reviews
|
||||
|
||||
|
||||
## Ⅵ. AI Coding Tool Usage Checklist (if applicable)
|
||||
<!--
|
||||
**IMPORTANT**: If you used AI Coding tools (e.g., Cursor, GitHub Copilot, etc.) to generate this PR, please check the following items.
|
||||
PRs that don't meet these requirements will have **LOWER REVIEW PRIORITY** and we **CANNOT GUARANTEE** timely reviews.
|
||||
|
||||
If you did NOT use AI Coding tools, you can skip this section entirely.
|
||||
-->
|
||||
|
||||
**Please check all applicable items:**
|
||||
|
||||
- [ ] **For new standalone features** (e.g., new wasm plugin or golang-filter plugin):
|
||||
- [ ] I have created a `design/` directory in the plugin folder
|
||||
- [ ] I have added the design document to the `design/` directory
|
||||
- [ ] I have included the AI Coding summary below
|
||||
|
||||
- [ ] **For regular updates/changes** (not new plugins):
|
||||
- [ ] I have provided the prompts/instructions I gave to the AI Coding tool below
|
||||
- [ ] I have included the AI Coding summary below
|
||||
|
||||
### AI Coding Prompts (for regular updates)
|
||||
<!-- Paste the prompts/instructions you provided to the AI Coding tool -->
|
||||
|
||||
|
||||
### AI Coding Summary
|
||||
<!--
|
||||
AI Coding tool should provide a summary after completing the work, including:
|
||||
- Key decisions made
|
||||
- Major changes implemented
|
||||
- Important considerations or limitations
|
||||
-->
|
||||
|
||||
|
||||
### Ⅴ. Special notes for reviews
|
||||
|
||||
|
||||
137
.github/workflows/build-and-test.yaml
vendored
137
.github/workflows/build-and-test.yaml
vendored
@@ -2,18 +2,20 @@ name: "Build and Test"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: ["*"]
|
||||
|
||||
env:
|
||||
GO_VERSION: 1.24
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.22
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
# There are too many lint errors in current code bases
|
||||
# uncomment when we decide what lint should be addressed or ignored.
|
||||
# - run: make lint
|
||||
@@ -21,40 +23,42 @@ jobs:
|
||||
coverage-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: "Setup Go"
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.22
|
||||
- name: "Setup Go"
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
- name: Setup Golang Caches
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |-
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-go
|
||||
- name: Setup Golang Caches
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |-
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-go
|
||||
|
||||
- run: git stash # restore patch
|
||||
- run: git stash # restore patch
|
||||
|
||||
# test
|
||||
- name: Run Coverage Tests
|
||||
run: |-
|
||||
go version
|
||||
GOPROXY="https://proxy.golang.org,direct" make go.test.coverage
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
fail_ci_if_error: false
|
||||
files: ./coverage.xml
|
||||
verbose: true
|
||||
# test
|
||||
- name: Run Coverage Tests
|
||||
run: |-
|
||||
go version
|
||||
GOPROXY="https://proxy.golang.org,direct" make go.test.coverage
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
with:
|
||||
fail_ci_if_error: false
|
||||
files: ./coverage.xml
|
||||
verbose: true
|
||||
|
||||
build:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint,coverage-test]
|
||||
needs: [lint, coverage-test]
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }}"
|
||||
uses: actions/checkout@v4
|
||||
@@ -64,7 +68,7 @@ jobs:
|
||||
- name: "Setup Go"
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.22
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
- name: Setup Golang Caches
|
||||
uses: actions/cache@v4
|
||||
@@ -90,45 +94,52 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
higress-conformance-test:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Free Up GitHub Actions Ubuntu Runner Disk Space 🔧
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
tool-cache: false
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
swap-storage: true
|
||||
|
||||
- name: "Setup Go"
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.22
|
||||
- name: Free Up GitHub Actions Ubuntu Runner Disk Space 🔧
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
tool-cache: false
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
swap-storage: true
|
||||
|
||||
- name: Setup Golang Caches
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |-
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-go
|
||||
|
||||
- run: git stash # restore patch
|
||||
- name: "Setup Go"
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
- name: Setup Golang Caches
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |-
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ github.run_id }}
|
||||
# key: ${{ runner.os }}-go-${{ env.GO_VERSION }}
|
||||
|
||||
restore-keys: ${{ runner.os }}-go
|
||||
|
||||
- run: git stash # restore patch
|
||||
|
||||
- name: update go mod
|
||||
run: |-
|
||||
make prebuild
|
||||
go mod tidy
|
||||
|
||||
- name: "Run Higress E2E Conformance Tests"
|
||||
run: GOPROXY="https://proxy.golang.org,direct" make higress-conformance-test
|
||||
|
||||
- name: "Run Higress E2E Conformance Tests"
|
||||
run: GOPROXY="https://proxy.golang.org,direct" make higress-conformance-test
|
||||
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [higress-conformance-test,gateway-conformance-test]
|
||||
needs: [higress-conformance-test, gateway-conformance-test]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
169
.github/workflows/wasm-plugin-unit-test.yml
vendored
169
.github/workflows/wasm-plugin-unit-test.yml
vendored
@@ -176,6 +176,18 @@ jobs:
|
||||
name: coverage-${{ matrix.plugin }}
|
||||
path: plugins/wasm-go/extensions/${{ matrix.plugin }}/coverage-${{ matrix.plugin }}.out
|
||||
retention-days: 30
|
||||
|
||||
- name: Upload coverage to Codecov for ${{ matrix.plugin }}
|
||||
uses: codecov/codecov-action@v4
|
||||
if: always()
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
with:
|
||||
file: plugins/wasm-go/extensions/${{ matrix.plugin }}/coverage-${{ matrix.plugin }}.out
|
||||
flags: wasm-go-plugin-${{ matrix.plugin }}
|
||||
name: codecov-${{ matrix.plugin }}
|
||||
fail_ci_if_error: false
|
||||
verbose: true
|
||||
|
||||
test-summary:
|
||||
name: Test Summary & Coverage
|
||||
@@ -196,6 +208,7 @@ jobs:
|
||||
- name: Install required tools
|
||||
run: |
|
||||
go install github.com/wadey/gocovmerge@latest
|
||||
sudo apt-get update && sudo apt-get install -y bc
|
||||
|
||||
- name: Download all test results
|
||||
uses: actions/download-artifact@v4
|
||||
@@ -266,11 +279,106 @@ jobs:
|
||||
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### 📈 Coverage Report" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# 覆盖率门禁检查
|
||||
coverage_failed=false
|
||||
|
||||
# 解析覆盖率文件 - 使用find命令查找覆盖率文件
|
||||
coverage_files=$(find ${{ github.workspace }} -name "coverage-*.out")
|
||||
|
||||
if [ -n "$coverage_files" ]; then
|
||||
echo "Found coverage files:"
|
||||
echo "$coverage_files"
|
||||
fi
|
||||
|
||||
for coverage_file in $coverage_files; do
|
||||
if [ -f "$coverage_file" ]; then
|
||||
plugin_name=$(basename "$coverage_file" | sed 's/coverage-\(.*\)\.out/\1/')
|
||||
|
||||
# 将覆盖率文件复制到对应插件目录,避免go tool cover的模块依赖问题
|
||||
echo "Processing coverage file: $coverage_file"
|
||||
|
||||
# 检查覆盖率文件是否存在且非空
|
||||
if [ -s "$coverage_file" ]; then
|
||||
# 将覆盖率文件复制到对应插件目录
|
||||
plugin_dir="plugins/wasm-go/extensions/$plugin_name"
|
||||
if [ -d "$plugin_dir" ]; then
|
||||
cp "$coverage_file" "$plugin_dir/"
|
||||
cd "$plugin_dir"
|
||||
|
||||
# 在插件目录中运行go tool cover,使用正确的模块环境
|
||||
coverage_stats=$(go tool cover -func="$(basename "$coverage_file")" 2>&1 | tail -1)
|
||||
cd - > /dev/null
|
||||
|
||||
# 清理复制的文件
|
||||
rm -f "$plugin_dir/$(basename "$coverage_file")"
|
||||
else
|
||||
echo "Plugin directory not found: $plugin_dir"
|
||||
coverage_stats=""
|
||||
fi
|
||||
|
||||
echo "Coverage stats result: $coverage_stats"
|
||||
|
||||
if [ -n "$coverage_stats" ] && echo "$coverage_stats" | grep -q "%"; then
|
||||
# 提取覆盖率百分比
|
||||
coverage_percent=$(echo "$coverage_stats" | grep -o '[0-9.]*%' | head -1 | sed 's/%//')
|
||||
|
||||
# 确保数值有效
|
||||
coverage_percent=${coverage_percent:-0}
|
||||
|
||||
if (( $(echo "$coverage_percent > 0" | bc -l) )); then
|
||||
# 根据覆盖率设置颜色和图标
|
||||
if (( $(echo "$coverage_percent >= 80" | bc -l) )); then
|
||||
coverage_icon="🟢"
|
||||
elif (( $(echo "$coverage_percent >= 30" | bc -l) )); then
|
||||
coverage_icon="🟡"
|
||||
else
|
||||
coverage_icon="🔴"
|
||||
coverage_failed=true
|
||||
fi
|
||||
|
||||
echo "$coverage_icon **$plugin_name**: $coverage_percent%" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# 检查覆盖率门禁
|
||||
if (( $(echo "$coverage_percent < 30" | bc -l) )); then
|
||||
echo "❌ **$plugin_name**: Coverage below 30% threshold!" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
else
|
||||
echo "⚪ **$plugin_name**: No statements to cover" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
else
|
||||
echo "⚪ **$plugin_name**: Coverage data unavailable" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
else
|
||||
echo "⚪ **$plugin_name**: Coverage file is empty or invalid" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "📊 **Coverage reports are now available on Codecov**" >> $GITHUB_STEP_SUMMARY
|
||||
echo "🔗 **This Commit Coverage**: https://codecov.io/gh/${{ github.repository }}/commit/${{ github.sha }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# 覆盖率门禁检查
|
||||
if [ "$coverage_failed" = true ]; then
|
||||
echo "### ❌ Coverage Gate Failed" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "🚫 **Coverage threshold not met**: Some plugins have coverage below 30%" >> $GITHUB_STEP_SUMMARY
|
||||
echo "📋 **Please improve test coverage before merging this PR**" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# 退出CI失败
|
||||
echo "Coverage gate failed - some plugins below 30% threshold"
|
||||
exit 1
|
||||
else
|
||||
echo "### ✅ Coverage Gate Passed" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "🎉 **All plugins meet the 30% coverage threshold**" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
echo "### 🎯 Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Total plugins**: $total_plugins" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Passed**: $passed_plugins ✅" >> $GITHUB_STEP_SUMMARY
|
||||
@@ -316,63 +424,4 @@ jobs:
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# - name: Merge coverage reports
|
||||
# run: |
|
||||
# echo "Merging coverage reports..."
|
||||
#
|
||||
# # 使用绝对路径查找,更可靠
|
||||
# coverage_files=$(find ${{ github.workspace }} -name "coverage-*")
|
||||
#
|
||||
# if [ -n "$coverage_files" ]; then
|
||||
# echo "Found coverage files:"
|
||||
# echo "$coverage_files"
|
||||
#
|
||||
# # 使用gocovmerge顺序合并
|
||||
# echo "Merging Go coverage files using gocovmerge sequential method..."
|
||||
#
|
||||
# # 将文件列表转换为数组
|
||||
# readarray -t coverage_array <<< "$coverage_files"
|
||||
# file_count=${#coverage_array[@]}
|
||||
#
|
||||
# echo "Total files to merge: $file_count"
|
||||
#
|
||||
# # 复制第一个文件作为基础
|
||||
# cp "${coverage_array[0]}" ${{ github.workspace }}/merged_coverage.out
|
||||
# echo "Starting with: ${coverage_array[0]}"
|
||||
#
|
||||
# # 如果有多个文件,逐个合并其他文件到最终目标
|
||||
# if [ $file_count -gt 1 ]; then
|
||||
# echo "Multiple files, merging sequentially with gocovmerge..."
|
||||
#
|
||||
# for ((i=1; i<file_count; i++)); do
|
||||
# current_file="${coverage_array[i]}"
|
||||
#
|
||||
# echo "Merging file $((i+1))/$file_count: $current_file"
|
||||
#
|
||||
# # 使用gocovmerge合并到最终目标文件
|
||||
# gocovmerge "${{ github.workspace }}/merged_coverage.out" "$current_file" > "${{ github.workspace }}/temp_merge.out"
|
||||
# mv "${{ github.workspace }}/temp_merge.out" "${{ github.workspace }}/merged_coverage.out"
|
||||
#
|
||||
# echo "Successfully merged with $current_file"
|
||||
# done
|
||||
# fi
|
||||
#
|
||||
# echo "Coverage reports merged successfully using gocovmerge sequential method"
|
||||
# echo "Merged file size: $(wc -c < ${{ github.workspace }}/merged_coverage.out) bytes"
|
||||
# else
|
||||
# echo "No coverage files found"
|
||||
# # 创建空的覆盖率文件
|
||||
# echo "mode: atomic" > ${{ github.workspace }}/merged_coverage.out
|
||||
# fi
|
||||
|
||||
# - name: Upload merged coverage to Codecov
|
||||
# uses: codecov/codecov-action@v4
|
||||
# if: always()
|
||||
# with:
|
||||
# file: ${{ github.workspace }}/merged_coverage.out
|
||||
# flags: wasm-go-plugins-tests
|
||||
# name: codecov-wasm-go-plugins
|
||||
# fail_ci_if_error: false
|
||||
# verbose: true
|
||||
fi
|
||||
@@ -35,6 +35,7 @@ header:
|
||||
- 'hgctl/pkg/manifests'
|
||||
- 'pkg/ingress/kube/gateway/istio/testdata'
|
||||
- 'release-notes/**'
|
||||
- '.cursor/**'
|
||||
|
||||
comment: on-failure
|
||||
dependency:
|
||||
|
||||
13
ADOPTERS.md
Normal file
13
ADOPTERS.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Adopters of Higress
|
||||
|
||||
Below are the adopters of the Higress project. If you are using Higress in your organization, please add your name to the list by submitting a pull request: this will help foster the Higress community. Kindly ensure the list remains in alphabetical order.
|
||||
|
||||
|
||||
| Organization | Contact (GitHub User Name) | Environment | Description of Use |
|
||||
|---------------------------------------|----------------------------------------|--------------------------------------------|-----------------------------------------------------------------------|
|
||||
| [antdigital](https://antdigital.com/) | [@Lovelcp](https://github.com/Lovelcp) | Production | Ingress Gateway, Microservice gateway, LLM Gateway, MCP Gateway |
|
||||
| [kuaishou](https://ir.kuaishou.com/) | [@maplecap](https://github.com/maplecap) | Production | LLM Gateway |
|
||||
| [Trip.com](https://www.trip.com/) | [@CH3CHO](https://github.com/CH3CHO) | Production | LLM Gateway, MCP Gateway |
|
||||
| [vipshop](https://github.com/vipshop/) | [@firebook](https://github.com/firebook) | Production | LLM Gateway, MCP Gateway, Inference Gateway |
|
||||
| [labring](https://github.com/labring/) | [@zzjin](https://github.com/zzjin) | Production | Ingress Gateway |
|
||||
| < company name here> | < your github handle here > | <Production/Testing/Experimenting/etc> | <Ingress Gateway/Microservice gateway/LLM Gateway/MCP Gateway/Inference Gateway> |
|
||||
@@ -169,6 +169,31 @@ git config --get user.email
|
||||
|
||||
PR 是更改 Higress 项目文件的唯一方法。为了帮助审查人更好地理解你的目的,PR 描述不能太详细。我们鼓励贡献者遵循 [PR 模板](./.github/PULL_REQUEST_TEMPLATE.md) 来完成拉取请求。
|
||||
|
||||
#### 使用 AI Coding 工具的特殊要求
|
||||
|
||||
如果你使用 AI Coding 工具(如 Cursor、GitHub Copilot 等)来生成 PR,我们有以下**严格要求**:
|
||||
|
||||
**针对新增独立插件的场景**(例如新实现的 wasm 插件或 golang-filter 插件):
|
||||
- 你**必须**在插件目录下创建 `design/` 目录
|
||||
- 将你提供给 AI Coding 工具的设计文档放在 `design/` 目录中
|
||||
- 在 PR 描述中提供 AI Coding 工具生成的工作总结
|
||||
|
||||
**针对日常更新/修改的场景**:
|
||||
- 在 PR 描述中提供你给 AI Coding 工具的提示词/指令
|
||||
- 在 PR 描述中提供 AI Coding 工具生成的工作总结
|
||||
|
||||
**AI Coding 工作总结应包括**:
|
||||
- 做出的关键决策
|
||||
- 实现的主要更改
|
||||
- 重要的注意事项或限制
|
||||
|
||||
**Review 优先级说明**:
|
||||
- 如果你使用了 AI Coding 工具但没有按照上述要求操作,你的 PR review 优先级将会**降低**
|
||||
- 我们**无法保证**对不符合要求的 AI Coding PR 进行及时 review
|
||||
- 如果不是使用 AI Coding 工具完成的 PR,则不需要遵循这些额外要求
|
||||
|
||||
这些要求的目的是确保使用 AI 生成的代码具有充分的文档记录和可追溯性,便于代码审查和后续维护。通过要求提供提示词/设计文档,我们可以更好地理解开发意图和上下文。
|
||||
|
||||
### 开发前准备
|
||||
|
||||
```shell
|
||||
|
||||
@@ -169,6 +169,31 @@ No matter commit message, or commit content, we do take more emphasis on code re
|
||||
|
||||
PR is the only way to make change to Higress project files. To help reviewers better get your purpose, PR description could not be too detailed. We encourage contributors to follow the [PR template](./.github/PULL_REQUEST_TEMPLATE.md) to finish the pull request.
|
||||
|
||||
#### Special Requirements for AI Coding Tool Usage
|
||||
|
||||
If you use AI Coding tools (such as Cursor, GitHub Copilot, etc.) to generate PRs, we have the following **strict requirements**:
|
||||
|
||||
**For new standalone plugin scenarios** (e.g., newly implemented wasm plugins or golang-filter plugins):
|
||||
- You **MUST** create a `design/` directory under the plugin directory
|
||||
- Place the design document you provided to the AI Coding tool in the `design/` directory
|
||||
- Provide an AI Coding summary in the PR description
|
||||
|
||||
**For regular updates/changes scenarios**:
|
||||
- Provide the prompts/instructions you gave to the AI Coding tool in the PR description
|
||||
- Provide an AI Coding summary in the PR description
|
||||
|
||||
**AI Coding Summary should include**:
|
||||
- Key decisions made
|
||||
- Major changes implemented
|
||||
- Important considerations or limitations
|
||||
|
||||
**Review Priority Notice**:
|
||||
- If you use AI Coding tools but do not follow the above requirements, your PR review priority will be **lowered**
|
||||
- We **cannot guarantee** timely reviews for AI Coding PRs that do not meet these requirements
|
||||
- If the PR is not completed using AI Coding tools, these additional requirements do not apply
|
||||
|
||||
The purpose of these requirements is to ensure that AI-generated code is adequately documented and traceable, facilitating code review and subsequent maintenance. By requiring prompts/design documents, we can better understand the development intent and context.
|
||||
|
||||
### Pre-development preparation
|
||||
|
||||
```shell
|
||||
|
||||
@@ -164,6 +164,31 @@ git config --get user.email
|
||||
|
||||
PR は Higress プロジェクトファイルを変更する唯一の方法です。レビュアーが目的をよりよく理解できるようにするために、PR 説明は詳細すぎることはありません。貢献者には、[PR テンプレート](./.github/PULL_REQUEST_TEMPLATE.md) に従ってプルリクエストを完了することを奨励します。
|
||||
|
||||
#### AI Coding ツール使用時の特別な要件
|
||||
|
||||
AI Coding ツール(Cursor、GitHub Copilot など)を使用して PR を生成する場合、以下の**厳格な要件**があります:
|
||||
|
||||
**新規独立プラグインのシナリオ**(新しく実装された wasm プラグインや golang-filter プラグインなど)の場合:
|
||||
- プラグインディレクトリの下に `design/` ディレクトリを作成する**必要があります**
|
||||
- AI Coding ツールに提供した設計ドキュメントを `design/` ディレクトリに配置してください
|
||||
- PR の説明に AI Coding サマリーを提供してください
|
||||
|
||||
**通常の更新/変更のシナリオ**の場合:
|
||||
- PR の説明に AI Coding ツールに与えたプロンプト/指示を提供してください
|
||||
- PR の説明に AI Coding サマリーを提供してください
|
||||
|
||||
**AI Coding サマリーには以下を含める必要があります**:
|
||||
- 行われた重要な決定
|
||||
- 実装された主要な変更
|
||||
- 重要な考慮事項または制限事項
|
||||
|
||||
**レビュー優先度に関する通知**:
|
||||
- AI Coding ツールを使用したが上記の要件に従わなかった場合、PR のレビュー優先度が**低下**します
|
||||
- 要件を満たしていない AI Coding PR に対して、タイムリーなレビューを**保証できません**
|
||||
- AI Coding ツールを使用せずに完了した PR の場合、これらの追加要件は適用されません
|
||||
|
||||
これらの要件の目的は、AI で生成されたコードが十分に文書化され、追跡可能であることを保証し、コードレビューと後続のメンテナンスを容易にすることです。プロンプト/設計ドキュメントを要求することで、開発意図とコンテキストをより良く理解できます。
|
||||
|
||||
### 開発前の準備
|
||||
|
||||
```shell
|
||||
|
||||
@@ -1 +1 @@
|
||||
higress-console: v2.1.8
|
||||
higress-console: v2.1.9
|
||||
@@ -146,7 +146,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.9/envoy-symbol-ARCH.tar.gz
|
||||
export ENVOY_PACKAGE_URL_PATTERN?=https://github.com/higress-group/proxy/releases/download/v2.1.10/envoy-symbol-ARCH.tar.gz
|
||||
|
||||
build-envoy: prebuild
|
||||
./tools/hack/build-envoy.sh
|
||||
@@ -166,9 +166,15 @@ build-gateway: prebuild buildx-prepare build-golang-filter
|
||||
USE_REAL_USER=1 TARGET_ARCH=arm64 DOCKER_TARGETS="docker.proxyv2" ./tools/hack/build-istio-image.sh init
|
||||
DOCKER_TARGETS="docker.proxyv2" IMG_URL="${IMG_URL}" ./tools/hack/build-istio-image.sh docker.buildx
|
||||
|
||||
build-gateway-local: prebuild build-golang-filter
|
||||
build-gateway-local: prebuild build-golang-filter-amd64
|
||||
TARGET_ARCH=${TARGET_ARCH} DOCKER_TARGETS="docker.proxyv2" ./tools/hack/build-istio-image.sh docker
|
||||
|
||||
build-golang-filter-amd64:
|
||||
TARGET_ARCH=amd64 ./tools/hack/build-golang-filters.sh
|
||||
|
||||
build-golang-filter-arm64:
|
||||
TARGET_ARCH=arm64 ./tools/hack/build-golang-filters.sh
|
||||
|
||||
build-golang-filter:
|
||||
TARGET_ARCH=amd64 ./tools/hack/build-golang-filters.sh
|
||||
TARGET_ARCH=arm64 ./tools/hack/build-golang-filters.sh
|
||||
@@ -194,8 +200,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 ?= 48da465cfd0dc5c9ac851bd2b9743780dc82dd8a
|
||||
ISTIO_LATEST_IMAGE_TAG ?= latest
|
||||
ENVOY_LATEST_IMAGE_TAG ?= cdf0f16bf622102f89a0d0257834f43f502e4b99
|
||||
ISTIO_LATEST_IMAGE_TAG ?= ec099e0a24d25aff9c6530cb45dc0ff86ebb78b9
|
||||
|
||||
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'
|
||||
|
||||
23
README.md
23
README.md
@@ -43,6 +43,13 @@ Higress's AI gateway capabilities support all [mainstream model providers](https
|
||||
|
||||
Higress was born within Alibaba to solve the issues of Tengine reload affecting long-connection services and insufficient load balancing capabilities for gRPC/Dubbo. Within Alibaba Cloud, Higress's AI gateway capabilities support core AI applications such as Tongyi Bailian model studio, machine learning PAI platform, and other critical AI services. Alibaba Cloud has built its cloud-native API gateway product based on Higress, providing 99.99% gateway high availability guarantee service capabilities for a large number of enterprise customers.
|
||||
|
||||
You can click the button below to install the enterprise version of Higress:
|
||||
|
||||
[](https://www.aliyun.com/product/api-gateway?spm=higress-github.topbar.0.0.0)
|
||||
|
||||
|
||||
If you use open-source Higress and wish to obtain enterprise-level support, you can contact the project maintainer johnlanni's email: **zty98751@alibaba-inc.com** or social media accounts (WeChat ID: **nomadao**, DingTalk ID: **chengtanzty**). Please note **Higress** when adding as a friend :)
|
||||
|
||||
## Summary
|
||||
|
||||
- [**Quick Start**](#quick-start)
|
||||
@@ -75,10 +82,15 @@ Port descriptions:
|
||||
>
|
||||
> If you experience a timeout when pulling image from `higress-registry.cn-hangzhou.cr.aliyuncs.com`, you can try replacing it with the following docker registry mirror source:
|
||||
>
|
||||
> **North America**: `higress-registry.us-west-1.cr.aliyuncs.com`
|
||||
>
|
||||
> **Southeast Asia**: `higress-registry.ap-southeast-7.cr.aliyuncs.com`
|
||||
|
||||
For other installation methods such as Helm deployment under K8s, please refer to the official [Quick Start documentation](https://higress.io/en-us/docs/user/quickstart).
|
||||
|
||||
If you are deploying on the cloud, it is recommended to use the [Enterprise Edition](https://www.aliyun.com/product/apigateway?spm=higress-github.topbar.0.0.0)
|
||||
|
||||
|
||||
## Use Cases
|
||||
|
||||
- **MCP Server Hosting**:
|
||||
@@ -107,7 +119,16 @@ For other installation methods such as Helm deployment under K8s, please refer t
|
||||
|
||||
Higress can function as a feature-rich ingress controller, which is compatible with many annotations of K8s' nginx ingress controller.
|
||||
|
||||
[Gateway API](https://gateway-api.sigs.k8s.io/) support is coming soon and will support smooth migration from Ingress API to Gateway API.
|
||||
[Gateway API](https://gateway-api.sigs.k8s.io/) is already supported, and it supports a smooth migration from Ingress API to Gateway API.
|
||||
|
||||
Compared to ingress-nginx, the resource overhead has significantly decreased, and the speed at which route changes take effect has improved by ten times.
|
||||
|
||||
> The following resource overhead comparison comes from [sealos](https://github.com/labring).
|
||||
>
|
||||
> For details, you can read this [article](https://sealos.io/blog/sealos-envoy-vs-nginx-2000-tenants) to understand how sealos migrates the monitoring of **tens of thousands of ingress** resources from nginx ingress to higress.
|
||||
|
||||

|
||||
|
||||
|
||||
- **Microservice gateway**:
|
||||
|
||||
|
||||
11
README_ZH.md
11
README_ZH.md
@@ -17,6 +17,7 @@
|
||||
[**官网**](https://higress.cn/) |
|
||||
[**文档**](https://higress.cn/docs/latest/overview/what-is-higress/) |
|
||||
[**博客**](https://higress.cn/blog/) |
|
||||
[**MCP Server 快速开始**](https://higress.cn/ai/mcp-quick-start/) |
|
||||
[**电子书**](https://higress.cn/docs/ebook/wasm14/) |
|
||||
[**开发指引**](https://higress.cn/docs/latest/dev/architecture/) |
|
||||
[**AI插件**](https://higress.cn/plugin/)
|
||||
@@ -44,6 +45,11 @@ Higress 的 AI 网关能力支持国内外所有[主流模型供应商](https://
|
||||
|
||||
Higress 在阿里内部为解决 Tengine reload 对长连接业务有损,以及 gRPC/Dubbo 负载均衡能力不足而诞生。在阿里云内部,Higress 的 AI 网关能力支撑了通义千问 APP、通义百炼模型工作室、机器学习 PAI 平台等核心 AI 应用。同时服务国内头部的 AIGC 企业(如零一万物),以及 AI 产品(如 FastGPT)。阿里云基于 Higress 构建了云原生 API 网关产品,为大量企业客户提供 99.99% 的网关高可用保障服务能力。
|
||||
|
||||
可以点下方按钮安装企业版 Higress:
|
||||
|
||||
[](https://www.aliyun.com/product/apigateway?spm=higress-github.topbar.0.0.0)
|
||||
|
||||
如果您使用开源的Higress并希望获得企业级支持,可以联系johnlanni的邮箱:zty98751@alibaba-inc.com或社交媒体账号(微信号:nomadao,钉钉号:chengtanzty)。添加好友时请备注Higress :)
|
||||
|
||||
## Summary
|
||||
|
||||
@@ -76,10 +82,7 @@ docker run -d --rm --name higress-ai -v ${PWD}:/data \
|
||||
|
||||
K8s 下使用 Helm 部署等其他安装方式可以参考官网 [Quick Start 文档](https://higress.cn/docs/latest/user/quickstart/)。
|
||||
|
||||
如果您是在云上部署,生产环境推荐使用[企业版](https://higress.io/cloud/),开发测试可以使用下面一键部署社区版:
|
||||
|
||||
[](https://computenest.console.aliyun.com/service/instance/create/default?type=user&ServiceName=Higress社区版)
|
||||
|
||||
如果您是在云上部署,推荐使用[企业版](https://www.aliyun.com/product/apigateway?spm=higress-github.topbar.0.0.0)
|
||||
|
||||
## 使用场景
|
||||
|
||||
|
||||
@@ -96,6 +96,17 @@ Ingress Config 包含 6 个控制器,各自负责不同的功能:
|
||||
- Http2Rpc Controller:监听 Http2Rpc 资源,实现 HTTP 协议到 RPC 协议的转换。用户可以通过配置协议转换,将 RPC 服务以 HTTP 接口的形式暴露,从而使用 HTTP 请求调用 RPC 接口。
|
||||
- WasmPlugin Controller:监听 WasmPlugin 资源,将 Higress WasmPlugin 转化为 Istio WasmPlugin。Higress WasmPlugin 在 Istio WasmPlugin 的基础上进行了扩展,支持全局、路由、域名、服务级别的配置。
|
||||
- ConfigmapMgr:监听 Higress 的全局配置 `higress-config` ConfigMap,可以根据 tracing、gzip 等配置构造 EnvoyFilter。
|
||||
`mcpServer.redis` 支持通过 Secret 引用保存敏感信息,密码字段可以使用 `passwordSecret` 指向 `higress-system` 命名空间下的 Kubernetes Secret,避免在 ConfigMap 中保存明文密码,例如:
|
||||
|
||||
```yaml
|
||||
higress: |-
|
||||
mcpServer:
|
||||
redis:
|
||||
address: "redis:6379"
|
||||
passwordSecret:
|
||||
name: redis-credentials
|
||||
key: password
|
||||
```
|
||||
|
||||
#### 2.2.2 Cert Server
|
||||
|
||||
@@ -140,4 +151,4 @@ Envoy 核心架构如下图:
|
||||
- [1] [Istio Pilot 组件介绍](https://www.zhaohuabing.com/post/2019-10-21-pilot-discovery-code-analysis/)
|
||||
- [2] [Istio 服务注册插件机制代码解析](https://www.zhaohuabing.com/post/2019-02-18-pilot-service-registry-code-analysis/)
|
||||
- [3] [Istio Pilot代码深度解析](https://www.zhaohuabing.com/post/2019-10-21-pilot-discovery-code-analysis/)
|
||||
- [4] [Envoy 官方文档](https://www.envoyproxy.io/docs/envoy/latest/intro/intro)
|
||||
- [4] [Envoy 官方文档](https://www.envoyproxy.io/docs/envoy/latest/intro/intro)
|
||||
|
||||
Submodule envoy/envoy updated: 7f18940fbc...0961b00718
14
go.mod
14
go.mod
@@ -1,6 +1,8 @@
|
||||
module github.com/alibaba/higress/v2
|
||||
|
||||
go 1.22.2
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.23.7
|
||||
|
||||
replace github.com/spf13/viper => github.com/istio/viper v1.3.3-0.20190515210538-2789fed3109c
|
||||
|
||||
@@ -35,7 +37,7 @@ require (
|
||||
github.com/onsi/gomega v1.27.10
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/tidwall/gjson v1.17.0
|
||||
go.uber.org/atomic v1.11.0
|
||||
go.uber.org/zap v1.27.0
|
||||
@@ -123,9 +125,9 @@ require (
|
||||
github.com/go-jose/go-jose/v3 v3.0.0 // indirect
|
||||
github.com/go-logr/logr v1.2.4 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.20.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/swag v0.22.4 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/mock v1.6.0 // indirect
|
||||
@@ -202,7 +204,7 @@ require (
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
github.com/spf13/cast v1.5.1 // indirect
|
||||
github.com/stoewer/go-strcase v1.3.0 // indirect
|
||||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.3 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
@@ -223,7 +225,7 @@ require (
|
||||
go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
|
||||
golang.org/x/exp v0.0.0-20250717185816-542afb5b7346 // indirect
|
||||
golang.org/x/mod v0.17.0 // indirect
|
||||
golang.org/x/oauth2 v0.13.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
|
||||
14
go.sum
14
go.sum
@@ -1010,8 +1010,8 @@ github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDB
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
|
||||
github.com/go-openapi/jsonpointer v0.20.0 h1:ESKJdU9ASRfaPNOPRx12IUyA1vn3R9GiE3KYD14BXdQ=
|
||||
github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwnU4G3YHOECG3CedA=
|
||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||
github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
|
||||
github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
|
||||
github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
|
||||
@@ -1045,8 +1045,8 @@ github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh
|
||||
github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=
|
||||
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4=
|
||||
github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA=
|
||||
github.com/go-openapi/validate v0.19.5/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4=
|
||||
@@ -1751,8 +1751,9 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
@@ -1764,8 +1765,9 @@ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1F
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tebeka/strftime v0.1.3 h1:5HQXOqWKYRFfNyBMNVc9z5+QzuBtIXy03psIhtdJYto=
|
||||
github.com/tebeka/strftime v0.1.3/go.mod h1:7wJm3dZlpr4l/oVK0t1HYIc4rMzQ2XJlOMIUJUJH6XQ=
|
||||
github.com/tetratelabs/wazero v1.7.3 h1:PBH5KVahrt3S2AHgEjKu4u+LlDbbk+nsGE3KLucy6Rw=
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
appVersion: 2.1.9-rc.1
|
||||
appVersion: 2.1.10
|
||||
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.9-rc.1
|
||||
version: 2.1.10
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
# Declare variables to be passed into your templates.
|
||||
global:
|
||||
# -- Specify the image registry and pull policy
|
||||
hub: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress
|
||||
# Will inherit from parent chart's global.hub if not set
|
||||
hub: ""
|
||||
# -- Specify image pull policy if default behavior isn't desired.
|
||||
# Default behavior: latest images will be Always else IfNotPresent.
|
||||
imagePullPolicy: ""
|
||||
|
||||
@@ -71,6 +71,11 @@ spec:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
routeType:
|
||||
enum:
|
||||
- HTTP
|
||||
- GRPC
|
||||
type: string
|
||||
service:
|
||||
items:
|
||||
type: string
|
||||
|
||||
@@ -203,7 +203,7 @@ template:
|
||||
{{- if $o11y.enabled }}
|
||||
{{- $config := $o11y.promtail }}
|
||||
- name: promtail
|
||||
image: {{ $config.image.repository }}:{{ $config.image.tag }}
|
||||
image: {{ $config.image.repository | default (printf "%s/promtail" .Values.global.hub) }}:{{ $config.image.tag }}
|
||||
imagePullPolicy: IfNotPresent
|
||||
args:
|
||||
- -config.file=/etc/promtail/promtail.yaml
|
||||
@@ -250,6 +250,10 @@ template:
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 6 }}
|
||||
{{- end }}
|
||||
{{- with .Values.gateway.topologySpreadConstraints }}
|
||||
topologySpreadConstraints:
|
||||
{{- toYaml . | nindent 6 }}
|
||||
{{- end }}
|
||||
volumes:
|
||||
- emptyDir: {}
|
||||
name: workload-socket
|
||||
|
||||
@@ -289,6 +289,10 @@ spec:
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.controller.topologySpreadConstraints }}
|
||||
topologySpreadConstraints:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
volumes:
|
||||
- name: log
|
||||
emptyDir: {}
|
||||
|
||||
@@ -5,6 +5,9 @@ metadata:
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "gateway.labels" . | nindent 4}}
|
||||
{{- with .Values.gateway.metrics.podMonitorSelector }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
annotations:
|
||||
{{- .Values.gateway.annotations | toYaml | nindent 4 }}
|
||||
spec:
|
||||
|
||||
@@ -24,9 +24,6 @@ spec:
|
||||
{{- end }}
|
||||
{{- with .Values.gateway.service.externalTrafficPolicy }}
|
||||
externalTrafficPolicy: "{{ . }}"
|
||||
{{- end }}
|
||||
{{- with .Values.gateway.service.loadBalancerClass}}
|
||||
loadBalancerClass: "{{ . }}"
|
||||
{{- end }}
|
||||
type: {{ .Values.gateway.service.type }}
|
||||
ports:
|
||||
|
||||
@@ -362,7 +362,7 @@ global:
|
||||
enabled: false
|
||||
promtail:
|
||||
image:
|
||||
repository: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/promtail
|
||||
repository: "" # Will use global.hub if not set
|
||||
tag: 2.9.4
|
||||
port: 3101
|
||||
resources:
|
||||
@@ -377,7 +377,7 @@ global:
|
||||
# The default value is "" and when caName="", the CA will be configured by other
|
||||
# mechanisms (e.g., environmental variable CA_PROVIDER).
|
||||
caName: ""
|
||||
hub: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress
|
||||
hub: "" # Will use global.hub if not set
|
||||
|
||||
clusterName: ""
|
||||
# -- meshConfig defines runtime configuration of components, including Istiod and istio-agent behavior
|
||||
@@ -433,7 +433,7 @@ gateway:
|
||||
# -- The readiness timeout seconds
|
||||
readinessTimeoutSeconds: 3
|
||||
|
||||
hub: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress
|
||||
hub: "" # Will use global.hub if not set
|
||||
tag: ""
|
||||
# -- revision declares which revision this gateway is a part of
|
||||
revision: ""
|
||||
@@ -522,12 +522,19 @@ gateway:
|
||||
|
||||
affinity: {}
|
||||
|
||||
topologySpreadConstraints: []
|
||||
|
||||
# -- If specified, the gateway will act as a network gateway for the given network.
|
||||
networkGateway: ""
|
||||
|
||||
metrics:
|
||||
# -- If true, create PodMonitor or VMPodScrape for gateway
|
||||
enabled: false
|
||||
# -- Selector for PodMonitor
|
||||
# When using monitoring.coreos.com/v1.PodMonitor, the selector must match
|
||||
# the label "release: kube-prome" is the default for kube-prometheus-stack
|
||||
podMonitorSelector:
|
||||
release: kube-prome
|
||||
# -- provider group name for CustomResourceDefinition, can be monitoring.coreos.com or operator.victoriametrics.com
|
||||
provider: monitoring.coreos.com
|
||||
interval: ""
|
||||
@@ -548,7 +555,7 @@ controller:
|
||||
replicas: 1
|
||||
image: higress
|
||||
|
||||
hub: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress
|
||||
hub: "" # Will use global.hub if not set
|
||||
tag: ""
|
||||
env: {}
|
||||
|
||||
@@ -624,6 +631,8 @@ controller:
|
||||
|
||||
affinity: {}
|
||||
|
||||
topologySpreadConstraints: []
|
||||
|
||||
autoscaling:
|
||||
enabled: false
|
||||
minReplicas: 1
|
||||
@@ -642,7 +651,7 @@ pilot:
|
||||
rollingMaxSurge: 100%
|
||||
rollingMaxUnavailable: 25%
|
||||
|
||||
hub: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress
|
||||
hub: "" # Will use global.hub if not set
|
||||
tag: ""
|
||||
|
||||
# -- Can be a full hub/image:tag
|
||||
@@ -795,7 +804,7 @@ pluginServer:
|
||||
replicas: 2
|
||||
image: plugin-server
|
||||
|
||||
hub: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress
|
||||
hub: "" # Will use global.hub if not set
|
||||
tag: ""
|
||||
|
||||
imagePullSecrets: []
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
dependencies:
|
||||
- name: higress-core
|
||||
repository: file://../core
|
||||
version: 2.1.9-rc.1
|
||||
version: 2.1.10
|
||||
- name: higress-console
|
||||
repository: https://higress.io/helm-charts/
|
||||
version: 2.1.8
|
||||
digest: sha256:0899e57f8744790bef3061413d6ce43ca4a54ac21fbe44fc0af7db973da28a79
|
||||
generated: "2025-10-09T17:25:21.377573+08:00"
|
||||
version: 2.1.9
|
||||
digest: sha256:fbb896461a8bdc1d5a4f8403253a59497b3b7a13909e9b92a4f3ce3f4f8d999d
|
||||
generated: "2026-02-03T16:05:30.300315+08:00"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
appVersion: 2.1.9-rc.1
|
||||
appVersion: 2.1.10
|
||||
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.9-rc.1
|
||||
version: 2.1.10
|
||||
- name: higress-console
|
||||
repository: "https://higress.io/helm-charts/"
|
||||
version: 2.1.8
|
||||
version: 2.1.9
|
||||
type: application
|
||||
version: 2.1.9-rc.1
|
||||
version: 2.1.10
|
||||
|
||||
@@ -44,7 +44,7 @@ The command removes all the Kubernetes components associated with the chart and
|
||||
| controller.autoscaling.minReplicas | int | `1` | |
|
||||
| controller.autoscaling.targetCPUUtilizationPercentage | int | `80` | |
|
||||
| controller.env | object | `{}` | |
|
||||
| controller.hub | string | `"higress-registry.cn-hangzhou.cr.aliyuncs.com/higress"` | |
|
||||
| controller.hub | string | `""` | |
|
||||
| controller.image | string | `"higress"` | |
|
||||
| controller.imagePullSecrets | list | `[]` | |
|
||||
| controller.labels | object | `{}` | |
|
||||
@@ -83,6 +83,7 @@ The command removes all the Kubernetes components associated with the chart and
|
||||
| controller.serviceAccount.name | string | `""` | If not set and create is true, a name is generated using the fullname template |
|
||||
| controller.tag | string | `""` | |
|
||||
| controller.tolerations | list | `[]` | |
|
||||
| controller.topologySpreadConstraints | list | `[]` | |
|
||||
| downstream | object | `{"connectionBufferLimits":32768,"http2":{"initialConnectionWindowSize":1048576,"initialStreamWindowSize":65535,"maxConcurrentStreams":100},"idleTimeout":180,"maxRequestHeadersKb":60,"routeTimeout":0}` | Downstream config settings |
|
||||
| gateway.affinity | object | `{}` | |
|
||||
| gateway.annotations | object | `{}` | Annotations to apply to all resources |
|
||||
@@ -95,7 +96,7 @@ The command removes all the Kubernetes components associated with the chart and
|
||||
| gateway.hostNetwork | bool | `false` | |
|
||||
| gateway.httpPort | int | `80` | |
|
||||
| gateway.httpsPort | int | `443` | |
|
||||
| gateway.hub | string | `"higress-registry.cn-hangzhou.cr.aliyuncs.com/higress"` | |
|
||||
| gateway.hub | string | `""` | |
|
||||
| gateway.image | string | `"gateway"` | |
|
||||
| gateway.kind | string | `"Deployment"` | Use a `DaemonSet` or `Deployment` |
|
||||
| gateway.labels | object | `{}` | Labels to apply to all resources |
|
||||
@@ -104,6 +105,7 @@ The command removes all the Kubernetes components associated with the chart and
|
||||
| gateway.metrics.interval | string | `""` | |
|
||||
| gateway.metrics.metricRelabelConfigs | list | `[]` | for operator.victoriametrics.com/v1beta1.VMPodScrape |
|
||||
| gateway.metrics.metricRelabelings | list | `[]` | for monitoring.coreos.com/v1.PodMonitor |
|
||||
| gateway.metrics.podMonitorSelector | object | `{"release":"kube-prome"}` | Selector for PodMonitor When using monitoring.coreos.com/v1.PodMonitor, the selector must match the label "release: kube-prome" is the default for kube-prometheus-stack |
|
||||
| gateway.metrics.provider | string | `"monitoring.coreos.com"` | provider group name for CustomResourceDefinition, can be monitoring.coreos.com or operator.victoriametrics.com |
|
||||
| gateway.metrics.rawSpec | object | `{}` | some more raw podMetricsEndpoints spec |
|
||||
| gateway.metrics.relabelConfigs | list | `[]` | |
|
||||
@@ -151,6 +153,7 @@ The command removes all the Kubernetes components associated with the chart and
|
||||
| gateway.serviceAccount.name | string | `""` | The name of the service account to use. If not set, the release name is used |
|
||||
| gateway.tag | string | `""` | |
|
||||
| gateway.tolerations | list | `[]` | |
|
||||
| gateway.topologySpreadConstraints | list | `[]` | |
|
||||
| gateway.unprivilegedPortSupported | string | `nil` | |
|
||||
| global.autoscalingv2API | bool | `true` | whether to use autoscaling/v2 template for HPA settings for internal usage only, not to be configured by users. |
|
||||
| global.caAddress | string | `""` | The customized CA address to retrieve certificates for the pods in the cluster. CSR clients such as the Istio Agent and ingress gateways can use this to specify the CA endpoint. If not set explicitly, default to the Istio discovery address. |
|
||||
@@ -191,7 +194,7 @@ The command removes all the Kubernetes components associated with the chart and
|
||||
| global.multiCluster.clusterName | string | `""` | Should be set to the name of the cluster this installation will run in. This is required for sidecar injection to properly label proxies |
|
||||
| global.multiCluster.enabled | bool | `true` | Set to true to connect two kubernetes clusters via their respective ingressgateway services when pods in each cluster cannot directly talk to one another. All clusters should be using Istio mTLS and must have a shared root CA for this model to work. |
|
||||
| global.network | string | `""` | Network defines the network this cluster belong to. This name corresponds to the networks in the map of mesh networks. |
|
||||
| global.o11y | object | `{"enabled":false,"promtail":{"image":{"repository":"higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/promtail","tag":"2.9.4"},"port":3101,"resources":{"limits":{"cpu":"500m","memory":"2Gi"}},"securityContext":{}}}` | Observability (o11y) configurations |
|
||||
| global.o11y | object | `{"enabled":false,"promtail":{"image":{"repository":"","tag":"2.9.4"},"port":3101,"resources":{"limits":{"cpu":"500m","memory":"2Gi"}},"securityContext":{}}}` | Observability (o11y) configurations |
|
||||
| global.omitSidecarInjectorConfigMap | bool | `false` | |
|
||||
| global.onDemandRDS | bool | `false` | |
|
||||
| global.oneNamespace | bool | `false` | Whether to restrict the applications namespace the controller manages; If not set, controller watches all namespaces |
|
||||
@@ -243,7 +246,7 @@ The command removes all the Kubernetes components associated with the chart and
|
||||
| global.watchNamespace | string | `""` | If not empty, Higress Controller will only watch resources in the specified namespace. When isolating different business systems using K8s namespace, if each namespace requires a standalone gateway instance, this parameter can be used to confine the Ingress watching of Higress within the given namespace. |
|
||||
| global.xdsMaxRecvMsgSize | string | `"104857600"` | |
|
||||
| gzip | object | `{"chunkSize":4096,"compressionLevel":"BEST_COMPRESSION","compressionStrategy":"DEFAULT_STRATEGY","contentType":["text/html","text/css","text/plain","text/xml","application/json","application/javascript","application/xhtml+xml","image/svg+xml"],"disableOnEtagHeader":true,"enable":true,"memoryLevel":5,"minContentLength":1024,"windowBits":12}` | Gzip compression settings |
|
||||
| hub | string | `"higress-registry.cn-hangzhou.cr.aliyuncs.com/higress"` | |
|
||||
| hub | string | `""` | |
|
||||
| meshConfig | object | `{"enablePrometheusMerge":true,"rootNamespace":null,"trustDomain":"cluster.local"}` | meshConfig defines runtime configuration of components, including Istiod and istio-agent behavior See https://istio.io/docs/reference/config/istio.mesh.v1alpha1/ for all available options |
|
||||
| meshConfig.rootNamespace | string | `nil` | The namespace to treat as the administrative root namespace for Istio configuration. When processing a leaf namespace Istio will search for declarations in that namespace first and if none are found it will search in the root namespace. Any matching declaration found in the root namespace is processed as if it were declared in the leaf namespace. |
|
||||
| meshConfig.trustDomain | string | `"cluster.local"` | The trust domain corresponds to the trust root of a system Refer to https://github.com/spiffe/spiffe/blob/master/standards/SPIFFE-ID.md#21-trust-domain |
|
||||
@@ -260,7 +263,7 @@ The command removes all the Kubernetes components associated with the chart and
|
||||
| pilot.env.PILOT_ENABLE_METADATA_EXCHANGE | string | `"false"` | |
|
||||
| pilot.env.PILOT_SCOPE_GATEWAY_TO_NAMESPACE | string | `"false"` | |
|
||||
| pilot.env.VALIDATION_ENABLED | string | `"false"` | |
|
||||
| pilot.hub | string | `"higress-registry.cn-hangzhou.cr.aliyuncs.com/higress"` | |
|
||||
| pilot.hub | string | `""` | |
|
||||
| pilot.image | string | `"pilot"` | Can be a full hub/image:tag |
|
||||
| pilot.jwksResolverExtraRootCA | string | `""` | You can use jwksResolverExtraRootCA to provide a root certificate in PEM format. This will then be trusted by pilot when resolving JWKS URIs. |
|
||||
| pilot.keepaliveMaxServerConnectionAge | string | `"30m"` | The following is used to limit how long a sidecar can be connected to a pilot. It balances out load across pilot instances at the cost of increasing system churn. |
|
||||
@@ -275,7 +278,7 @@ The command removes all the Kubernetes components associated with the chart and
|
||||
| pilot.serviceAnnotations | object | `{}` | |
|
||||
| pilot.tag | string | `""` | |
|
||||
| pilot.traceSampling | float | `1` | |
|
||||
| pluginServer.hub | string | `"higress-registry.cn-hangzhou.cr.aliyuncs.com/higress"` | |
|
||||
| pluginServer.hub | string | `""` | |
|
||||
| pluginServer.image | string | `"plugin-server"` | |
|
||||
| pluginServer.imagePullSecrets | list | `[]` | |
|
||||
| pluginServer.labels | object | `{}` | |
|
||||
|
||||
@@ -112,6 +112,7 @@ helm delete higress -n higress-system
|
||||
| gateway.metrics.rawSpec | object | `{}` | 额外的度量规范 |
|
||||
| gateway.metrics.relabelConfigs | list | `[]` | 重新标签配置 |
|
||||
| gateway.metrics.relabelings | list | `[]` | 重新标签项 |
|
||||
| gateway.metrics.podMonitorSelector | object | `{"release":"kube-prometheus-stack"}` | PodMonitor 选择器,当使用 prometheus stack 的podmonitor自动发现时,选择器必须匹配标签 "release: kube-prome",这是 kube-prometheus-stack 的默认设置 |
|
||||
| gateway.metrics.scrapeTimeout | string | `""` | 抓取的超时时间 |
|
||||
| gateway.name | string | `"higress-gateway"` | 网关名称 |
|
||||
| gateway.networkGateway | string | `""` | 网络网关指定 |
|
||||
|
||||
22
hgctl/go.mod
22
hgctl/go.mod
@@ -1,8 +1,8 @@
|
||||
module github.com/alibaba/higress/hgctl
|
||||
|
||||
go 1.22.2
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.23.7
|
||||
toolchain go1.24.1
|
||||
|
||||
replace github.com/spf13/viper => github.com/istio/viper v1.3.3-0.20190515210538-2789fed3109c
|
||||
|
||||
@@ -20,6 +20,7 @@ replace github.com/alibaba/higress/v2 => ../
|
||||
require (
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||
github.com/alibaba/higress/v2 v2.0.0-00010101000000-000000000000
|
||||
github.com/braydonk/yaml v0.7.0
|
||||
github.com/compose-spec/compose-go v1.17.0
|
||||
github.com/docker/cli v24.0.7+incompatible
|
||||
github.com/docker/compose/v2 v2.23.3
|
||||
@@ -28,6 +29,7 @@ require (
|
||||
github.com/fatih/color v1.15.0
|
||||
github.com/fatih/structtag v1.2.0
|
||||
github.com/google/yamlfmt v0.10.0
|
||||
github.com/higress-group/openapi-to-mcpserver v0.0.0-20250925065334-de60a170f950
|
||||
github.com/iancoleman/orderedmap v0.3.0
|
||||
github.com/kylelemons/godebug v1.1.0
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
@@ -37,7 +39,7 @@ require (
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/spf13/viper v1.16.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/stretchr/testify v1.10.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
helm.sh/helm/v3 v3.12.2
|
||||
@@ -51,6 +53,8 @@ require (
|
||||
sigs.k8s.io/yaml v1.4.0
|
||||
)
|
||||
|
||||
require github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
|
||||
|
||||
require (
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
@@ -79,7 +83,6 @@ require (
|
||||
github.com/aws/smithy-go v1.13.5 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bmatcuk/doublestar/v4 v4.6.0 // indirect
|
||||
github.com/braydonk/yaml v0.7.0 // indirect
|
||||
github.com/buger/goterm v1.0.4 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
|
||||
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
|
||||
@@ -111,13 +114,14 @@ require (
|
||||
github.com/fsnotify/fsevents v0.1.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/fvbommel/sortorder v1.1.0 // indirect
|
||||
github.com/getkin/kin-openapi v0.118.0 // indirect
|
||||
github.com/go-errors/errors v1.4.2 // indirect
|
||||
github.com/go-gorp/gorp/v3 v3.0.5 // indirect
|
||||
github.com/go-logr/logr v1.2.4 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.20.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/swag v0.22.4 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/gofrs/flock v0.8.1 // indirect
|
||||
@@ -147,6 +151,7 @@ require (
|
||||
github.com/imdario/mergo v1.0.0 // indirect
|
||||
github.com/in-toto/in-toto-golang v0.5.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/invopop/yaml v0.1.0 // indirect
|
||||
github.com/jmoiron/sqlx v1.3.5 // indirect
|
||||
github.com/jonboulle/clockwork v0.4.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
@@ -165,6 +170,7 @@ require (
|
||||
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/manifoldco/promptui v0.9.0
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||
@@ -187,6 +193,7 @@ require (
|
||||
github.com/moby/term v0.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
@@ -194,6 +201,7 @@ require (
|
||||
github.com/opencontainers/image-spec v1.1.0-rc5 // indirect
|
||||
github.com/opencontainers/runc v1.1.9 // indirect
|
||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.17.0 // indirect
|
||||
@@ -245,7 +253,7 @@ require (
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
|
||||
golang.org/x/exp v0.0.0-20250717185816-542afb5b7346 // indirect
|
||||
golang.org/x/mod v0.17.0 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/oauth2 v0.13.0 // indirect
|
||||
|
||||
37
hgctl/go.sum
37
hgctl/go.sum
@@ -752,6 +752,7 @@ github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
|
||||
github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk=
|
||||
github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA=
|
||||
github.com/chzyer/logex v1.1.11-0.20170329064859-445be9e134b2/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
@@ -910,6 +911,8 @@ github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyT
|
||||
github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw=
|
||||
github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
|
||||
github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
|
||||
github.com/getkin/kin-openapi v0.118.0 h1:z43njxPmJ7TaPpMSCQb7PN0dEYno4tyBPQcrFdHoLuM=
|
||||
github.com/getkin/kin-openapi v0.118.0/go.mod h1:l5e9PaFUo9fyLJCPGQeXI2ML8c3P8BHOEV2VaAVf/pc=
|
||||
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
||||
@@ -954,9 +957,10 @@ github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwds
|
||||
github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
|
||||
github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
|
||||
github.com/go-openapi/jsonpointer v0.20.0 h1:ESKJdU9ASRfaPNOPRx12IUyA1vn3R9GiE3KYD14BXdQ=
|
||||
github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwnU4G3YHOECG3CedA=
|
||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||
github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
|
||||
github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
|
||||
github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
|
||||
@@ -987,8 +991,8 @@ github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/
|
||||
github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=
|
||||
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4=
|
||||
github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA=
|
||||
github.com/go-openapi/validate v0.19.5/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4=
|
||||
@@ -1000,6 +1004,8 @@ github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
|
||||
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/gobuffalo/flect v0.2.0/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80=
|
||||
github.com/gobuffalo/logger v1.0.6 h1:nnZNpxYo0zx+Aj9RfMPBm+x9zAU2OayFh/xrAWi34HU=
|
||||
github.com/gobuffalo/logger v1.0.6/go.mod h1:J31TBEHR1QLV2683OXTAItYIg8pv2JMHnF/quuAbMjs=
|
||||
@@ -1211,6 +1217,8 @@ github.com/hashicorp/golang-lru/v2 v2.0.4 h1:7GHuZcgid37q8o5i3QI9KMT4nCWQQ3Kx3Ov
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.4/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/higress-group/openapi-to-mcpserver v0.0.0-20250925065334-de60a170f950 h1:a3/hCNZednJoFbp1DPx2O/LRUwvcsyeTpL0MP+qIApg=
|
||||
github.com/higress-group/openapi-to-mcpserver v0.0.0-20250925065334-de60a170f950/go.mod h1:jRTljni4fNs7aLiAbOhAAWIjctA4NSNtm5z7kGimG6U=
|
||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
|
||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
@@ -1231,6 +1239,8 @@ github.com/in-toto/in-toto-golang v0.5.0/go.mod h1:/Rq0IZHLV7Ku5gielPT4wPHJfH1Gd
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc=
|
||||
github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q=
|
||||
github.com/istio/viper v1.3.3-0.20190515210538-2789fed3109c h1:EFWADU43GY2T7NIYYbIHWdrG2hRiWyGSHeON57ZADBE=
|
||||
github.com/istio/viper v1.3.3-0.20190515210538-2789fed3109c/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
@@ -1337,6 +1347,8 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN
|
||||
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
|
||||
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
|
||||
github.com/markbates/errx v1.1.0 h1:QDFeR+UP95dO12JgW+tgi2UVfo0V8YBHiUIOaeBPiEI=
|
||||
github.com/markbates/errx v1.1.0/go.mod h1:PLa46Oex9KNbVDZhKel8v1OT7hD5JZ2eI7AHhA0wswc=
|
||||
github.com/markbates/oncer v1.0.0 h1:E83IaVAHygyndzPimgUYJjbshhDTALZyXxvk9FOlQRY=
|
||||
@@ -1425,6 +1437,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0=
|
||||
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
@@ -1481,6 +1495,9 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9
|
||||
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
|
||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/perimeterx/marshmallow v1.1.4/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
|
||||
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
|
||||
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
|
||||
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI=
|
||||
@@ -1623,8 +1640,9 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
@@ -1634,8 +1652,9 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/theupdateframework/notary v0.7.0 h1:QyagRZ7wlSpjT5N2qQAh/pN+DVqgekv4DzbAiAiEL3c=
|
||||
github.com/theupdateframework/notary v0.7.0/go.mod h1:c9DRxcmhHmVLDay4/2fUYdISnHqbFDGRSlXPO0AhYWw=
|
||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
@@ -1648,7 +1667,11 @@ github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea h1:SXhTLE6pb6eld/
|
||||
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea/go.mod h1:WPnis/6cRcDZSUvVmezrxJPkiO87ThFYsoUiMwWNDJk=
|
||||
github.com/tonistiigi/vt100 v0.0.0-20230623042737-f9a4f7ef6531 h1:Y/M5lygoNPKwVNLMPXgVfsRT40CSFKXCxuU8LoHySjs=
|
||||
github.com/tonistiigi/vt100 v0.0.0-20230623042737-f9a4f7ef6531/go.mod h1:ulncasL3N9uLrVann0m+CDlJKWsIAP34MPcOJF6VRvc=
|
||||
github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo=
|
||||
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
|
||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/vbatts/tar-split v0.11.3 h1:hLFqsOLQ1SsppQNTMpkpPXClLDfC2A3Zgy9OUU+RVck=
|
||||
github.com/vbatts/tar-split v0.11.3/go.mod h1:9QlHN18E+fEH7RdG+QAJJcuya3rqT7eXSTY7wGrAokY=
|
||||
@@ -1972,6 +1995,7 @@ golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -2534,6 +2558,7 @@ gopkg.in/yaml.v3 v3.0.0-20190905181640-827449938966/go.mod h1:K4uyk7z7BCEPqu6E+C
|
||||
gopkg.in/yaml.v3 v3.0.0-20200121175148-a6ecf24a6d71/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||
|
||||
46
hgctl/pkg/agent/agent.go
Normal file
46
hgctl/pkg/agent/agent.go
Normal file
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) 2025 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package agent
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
)
|
||||
|
||||
func NewAgentCmd() *cobra.Command {
|
||||
agentCmd := &cobra.Command{
|
||||
Use: "agent",
|
||||
Short: "start the interactive agent window",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(handleAgentInvoke(cmd.OutOrStdout()))
|
||||
},
|
||||
}
|
||||
|
||||
return agentCmd
|
||||
}
|
||||
|
||||
func handleAgentInvoke(w io.Writer) error {
|
||||
|
||||
return getAgent().Start()
|
||||
}
|
||||
|
||||
// Sub-Agent1:
|
||||
// 1. Parse the url provided by user to MCP server configuration.
|
||||
// 2. Publish the parsed MCP Server to Higress
|
||||
func addPrequisiteSubAgent() error {
|
||||
return nil
|
||||
}
|
||||
61
hgctl/pkg/agent/base.go
Normal file
61
hgctl/pkg/agent/base.go
Normal file
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) 2025 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package agent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
const (
|
||||
AgentBinaryName = "claude"
|
||||
BinaryVersion = "0.1.0"
|
||||
DevVersion = "dev"
|
||||
NodeLeastVersion = 18
|
||||
AgentInstallCmd = "npm install -g @anthropic-ai/claude-code"
|
||||
AgentReleasePage = "https://docs.claude.com/en/docs/claude-code/setup"
|
||||
)
|
||||
|
||||
// set up the core env
|
||||
// 1. check if npm is installed
|
||||
// 2. check the npm version
|
||||
// 3. install hgctl-agent
|
||||
func getAgent() *AgenticCore {
|
||||
if !checkAgentInstallStatus() {
|
||||
fmt.Println("⚠️ Prerequisites not satisfied. Exiting...")
|
||||
// exit directly
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return NewAgenticCore()
|
||||
}
|
||||
|
||||
func checkAgentInstallStatus() bool {
|
||||
// TODO: Support cross-platform:windows
|
||||
|
||||
if !checkNodeInstall() {
|
||||
if err := promptNodeInstall(); err != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if !checkAgentInstall() {
|
||||
if err := promptAgentInstall(); err != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
46
hgctl/pkg/agent/core.go
Normal file
46
hgctl/pkg/agent/core.go
Normal file
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) 2025 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package agent
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
type AgenticCore struct {
|
||||
}
|
||||
|
||||
func NewAgenticCore() *AgenticCore {
|
||||
return &AgenticCore{}
|
||||
}
|
||||
|
||||
func (c *AgenticCore) run(args ...string) error {
|
||||
cmd := exec.Command(AgentBinaryName, args...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
|
||||
}
|
||||
|
||||
// ------- Initialization -------
|
||||
func (c *AgenticCore) Start() error {
|
||||
return c.run(AgentBinaryName)
|
||||
}
|
||||
|
||||
// ------- MCP -------
|
||||
func (c *AgenticCore) AddMCPServer(name string, url string) error {
|
||||
return c.run("mcp", "add", "--transport", HTTP, name, url)
|
||||
}
|
||||
348
hgctl/pkg/agent/mcp.go
Normal file
348
hgctl/pkg/agent/mcp.go
Normal file
@@ -0,0 +1,348 @@
|
||||
// Copyright (c) 2025 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package agent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/hgctl/pkg/agent/services"
|
||||
"github.com/alibaba/higress/hgctl/pkg/helm"
|
||||
"github.com/fatih/color"
|
||||
"github.com/higress-group/openapi-to-mcpserver/pkg/models"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
)
|
||||
|
||||
type MCPType string
|
||||
|
||||
const (
|
||||
HTTP string = "http"
|
||||
SSE string = "sse"
|
||||
OPENAPI string = "openapi"
|
||||
DIRECT_ROUTE string = "DIRECT_ROUTE"
|
||||
OPEN_API string = "OPEN_API"
|
||||
|
||||
HIGRESS_CONSOLE_URL = "higress-console-url"
|
||||
HIGRESS_CONSOLE_USER = "higress-console-user"
|
||||
HIGRESS_CONSOLE_PASSWORD = "higress-console-password"
|
||||
)
|
||||
|
||||
type MCPAddArg struct {
|
||||
// higress console auth arg
|
||||
baseURL string
|
||||
hgUser string
|
||||
hgPassword string
|
||||
|
||||
name string
|
||||
url string
|
||||
transport string
|
||||
spec string
|
||||
scope string
|
||||
noPublish bool
|
||||
// TODO: support mcp env
|
||||
// env string
|
||||
|
||||
}
|
||||
|
||||
type MCPAddHandler struct {
|
||||
core *AgenticCore
|
||||
arg MCPAddArg
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
func NewMCPCmd() *cobra.Command {
|
||||
mcpCmd := &cobra.Command{
|
||||
Use: "mcp",
|
||||
Short: "for the mcp management",
|
||||
}
|
||||
|
||||
mcpCmd.AddCommand(newMCPAddCmd())
|
||||
|
||||
return mcpCmd
|
||||
}
|
||||
|
||||
func newMCPAddCmd() *cobra.Command {
|
||||
// parameter
|
||||
arg := &MCPAddArg{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "add [name]",
|
||||
Short: "add mcp server including http and openapi",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
arg.name = args[0]
|
||||
resolveHigressConsoleAuth(arg)
|
||||
cmdutil.CheckErr(handleAddMCP(cmd.OutOrStdout(), *arg))
|
||||
color.Cyan("Tip: Try doing 'kubectl port-forward' and add the server to the agent manually, if MCP Server connection failed")
|
||||
},
|
||||
Args: cobra.ExactArgs(1),
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().StringVarP(&arg.transport, "transport", "t", HTTP, "Determine the MCP Server's Type")
|
||||
cmd.PersistentFlags().StringVarP(&arg.url, "url", "u", "", "MCP server URL")
|
||||
cmd.PersistentFlags().StringVarP(&arg.scope, "scope", "s", "project", `Configuration scope (project or global)`)
|
||||
cmd.PersistentFlags().StringVar(&arg.spec, "spec", "", "Specification of the openapi api")
|
||||
cmd.PersistentFlags().BoolVar(&arg.noPublish, "no-publish", false, "If set then the mcp server will not be plubished to higress")
|
||||
|
||||
addHigressConsoleAuthFlag(cmd, arg)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newHanlder(c *AgenticCore, arg MCPAddArg, w io.Writer) *MCPAddHandler {
|
||||
return &MCPAddHandler{c, arg, w}
|
||||
}
|
||||
|
||||
func (h *MCPAddHandler) validateArg() error {
|
||||
if !h.arg.noPublish {
|
||||
if h.arg.baseURL == "" || h.arg.hgUser == "" || h.arg.hgPassword == "" {
|
||||
fmt.Println("--higress-console-user, --higress-console-url, --higress-console-password must be provided")
|
||||
return fmt.Errorf("invalid args")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (h *MCPAddHandler) addHTTPMCP() error {
|
||||
if err := h.core.AddMCPServer(h.arg.name, h.arg.url); err != nil {
|
||||
return fmt.Errorf("mcp add failed: %w", err)
|
||||
}
|
||||
|
||||
if !h.arg.noPublish {
|
||||
return publishToHigress(h.arg, nil)
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// hgctl mcp add -t openapi --name test-name --spec openapi.json
|
||||
func (h *MCPAddHandler) addOpenAPIMCP() error {
|
||||
// fmt.Printf("get mcp server: %s openapi-spec-file: %s\n", h.arg.name, h.arg.spec)
|
||||
config := h.parseOpenapiSpec()
|
||||
|
||||
// fmt.Printf("get config struct: %v", config)
|
||||
|
||||
// publish to higress
|
||||
if err := publishToHigress(h.arg, config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add mcp server to agent
|
||||
gatewayIP, err := GetHigressGatewayServiceIP()
|
||||
if err != nil {
|
||||
color.Red(
|
||||
"failed to add mcp server [%s] while getting higress-gateway ip due to: %v \n You may try to do port-forward and add it to agent manually", h.arg.name, err)
|
||||
return err
|
||||
}
|
||||
mcpURL := fmt.Sprintf("http://%s/mcp-servers/%s", gatewayIP, h.arg.name)
|
||||
return h.core.AddMCPServer(h.arg.name, mcpURL)
|
||||
}
|
||||
|
||||
func (h *MCPAddHandler) parseOpenapiSpec() *models.MCPConfig {
|
||||
return parseOpenapi2MCP(h.arg)
|
||||
}
|
||||
|
||||
func handleAddMCP(w io.Writer, arg MCPAddArg) error {
|
||||
client := getAgent()
|
||||
h := newHanlder(client, arg, w)
|
||||
if err := h.validateArg(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// spec -> OPENAPI
|
||||
// noPublish -> typ
|
||||
switch arg.transport {
|
||||
case HTTP:
|
||||
return h.addHTTPMCP()
|
||||
case OPENAPI:
|
||||
if arg.spec == "" {
|
||||
return fmt.Errorf("--spec is required for openapi type")
|
||||
}
|
||||
if arg.noPublish {
|
||||
return fmt.Errorf("--no-publish is not supported for openapi type")
|
||||
}
|
||||
if arg.url != "" {
|
||||
return fmt.Errorf("--url is not supported for openapi type")
|
||||
}
|
||||
return h.addOpenAPIMCP()
|
||||
default:
|
||||
return fmt.Errorf("unsupported mcp type")
|
||||
}
|
||||
}
|
||||
|
||||
func publishToHigress(arg MCPAddArg, config *models.MCPConfig) error {
|
||||
// 1. parse the raw http url
|
||||
// 2. add service source
|
||||
// 3. add MCP server request
|
||||
client := services.NewHigressClient(arg.baseURL, arg.hgUser, arg.hgPassword)
|
||||
|
||||
// mcp server's url
|
||||
rawURL := arg.url
|
||||
// DIRECT_ROUTE or OPEN_API
|
||||
mcpType := DIRECT_ROUTE
|
||||
|
||||
if config != nil {
|
||||
// TODO: here use tools's url directly, need to be considered
|
||||
rawURL = config.Tools[0].RequestTemplate.URL
|
||||
mcpType = OPEN_API
|
||||
}
|
||||
|
||||
res, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add service source
|
||||
srvType := ""
|
||||
srvPort := ""
|
||||
srvName := fmt.Sprintf("hgctl-%s", arg.name)
|
||||
srvPath := res.Path
|
||||
|
||||
if ip := net.ParseIP(res.Hostname()); ip == nil {
|
||||
srvType = "dns"
|
||||
} else {
|
||||
srvType = "static"
|
||||
}
|
||||
|
||||
if res.Port() == "" && res.Scheme == "http" {
|
||||
srvPort = "80"
|
||||
} else if res.Port() == "" && res.Scheme == "https" {
|
||||
srvPort = "443"
|
||||
} else {
|
||||
srvPort = res.Port()
|
||||
}
|
||||
|
||||
_, err = services.HandleAddServiceSource(client, map[string]interface{}{
|
||||
"domain": res.Host,
|
||||
"type": srvType,
|
||||
"port": srvPort,
|
||||
"name": srvName,
|
||||
"domainForEdit": res.Host,
|
||||
"protocol": res.Scheme,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
srvField := []map[string]interface{}{{
|
||||
"name": fmt.Sprintf("%s.%s", srvName, srvType),
|
||||
"port": srvPort,
|
||||
"version": "1.0",
|
||||
"weight": 100,
|
||||
}}
|
||||
|
||||
// generete mcp server add request body
|
||||
body := map[string]interface{}{
|
||||
"name": arg.name,
|
||||
// "description": "",
|
||||
"type": mcpType,
|
||||
"service": fmt.Sprintf("%s.%s:%s", srvName, srvType, srvPort),
|
||||
"upstreamPathPrefix": srvPath,
|
||||
"services": srvField,
|
||||
}
|
||||
|
||||
// fmt.Printf("request body: %v", body)
|
||||
|
||||
_, err = services.HandleAddMCPServer(client, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if mcpType == OPEN_API {
|
||||
addMCPToolConfig(client, config, srvField)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func addMCPToolConfig(client *services.HigressClient, config *models.MCPConfig, srvField []map[string]interface{}) {
|
||||
body := map[string]interface{}{
|
||||
"name": config.Server.Name,
|
||||
// "description": "",
|
||||
"services": srvField,
|
||||
"type": OPEN_API,
|
||||
"rawConfigurations": convertMCPConfigToStr(config),
|
||||
"mcpServerName": config.Server.Name,
|
||||
}
|
||||
|
||||
_, err := services.HandleAddOpenAPITool(client, body)
|
||||
if err != nil {
|
||||
fmt.Printf("add openapi tools failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
// fmt.Println("get openapi tools add response: ", string(resp))
|
||||
}
|
||||
|
||||
func addHigressConsoleAuthFlag(cmd *cobra.Command, arg *MCPAddArg) {
|
||||
cmd.PersistentFlags().StringVar(&arg.baseURL, HIGRESS_CONSOLE_URL, "", "The BaseURL of higress console")
|
||||
cmd.PersistentFlags().StringVar(&arg.hgUser, HIGRESS_CONSOLE_USER, "", "The username of higress console")
|
||||
cmd.PersistentFlags().StringVarP(&arg.hgPassword, HIGRESS_CONSOLE_PASSWORD, "p", "", "The password of higress console")
|
||||
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
|
||||
viper.AutomaticEnv()
|
||||
}
|
||||
|
||||
// resolve from viper
|
||||
func resolveHigressConsoleAuth(arg *MCPAddArg) {
|
||||
if arg.baseURL == "" {
|
||||
arg.baseURL = viper.GetString(HIGRESS_CONSOLE_URL)
|
||||
}
|
||||
if arg.hgUser == "" {
|
||||
arg.hgUser = viper.GetString(HIGRESS_CONSOLE_USER)
|
||||
}
|
||||
if arg.hgPassword == "" {
|
||||
arg.hgPassword = viper.GetString(HIGRESS_CONSOLE_PASSWORD)
|
||||
}
|
||||
|
||||
// fmt.Printf("arg: %v\n", arg)
|
||||
|
||||
if arg.hgUser == "" || arg.hgPassword == "" {
|
||||
// Here we do not return this error, cause it will failed when validate arg
|
||||
if err := tryToGetLocalCredential(arg); err != nil {
|
||||
fmt.Printf("failed to get local higress console credential: %s\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func tryToGetLocalCredential(arg *MCPAddArg) error {
|
||||
profileContexts, err := getAllProfiles()
|
||||
|
||||
// The higress is not installed by hgctl
|
||||
if err != nil || len(profileContexts) == 0 {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, ctx := range profileContexts {
|
||||
installTyp := ctx.Install
|
||||
if installTyp == helm.InstallK8s || installTyp == helm.InstallLocalK8s {
|
||||
user, pwd, err := getConsoleCredentials(ctx.Profile)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// TODO: always use the first one profile
|
||||
arg.hgUser = user
|
||||
arg.hgPassword = pwd
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
113
hgctl/pkg/agent/services/client.go
Normal file
113
hgctl/pkg/agent/services/client.go
Normal file
@@ -0,0 +1,113 @@
|
||||
// Copyright (c) 2025 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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 == 409 {
|
||||
return nil, fmt.Errorf("resource already exists")
|
||||
}
|
||||
|
||||
if resp.StatusCode == 400 {
|
||||
return nil, fmt.Errorf("invalid resource definition")
|
||||
}
|
||||
|
||||
if resp.StatusCode == 500 {
|
||||
return nil, fmt.Errorf("server internal error")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
129
hgctl/pkg/agent/services/service.go
Normal file
129
hgctl/pkg/agent/services/service.go
Normal file
@@ -0,0 +1,129 @@
|
||||
// Copyright (c) 2025 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func HandleAddServiceSource(client *HigressClient, body interface{}) ([]byte, error) {
|
||||
data, ok := body.(map[string]interface{})
|
||||
// fmt.Printf("request body: %v\n", data)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to parse request body")
|
||||
}
|
||||
// Validate
|
||||
if _, ok := data["name"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'name' in body")
|
||||
}
|
||||
if _, ok := data["type"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'type' in body")
|
||||
}
|
||||
if _, ok := data["domain"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'domain' in body")
|
||||
}
|
||||
if _, ok := data["port"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'port' in body")
|
||||
}
|
||||
|
||||
resp, err := client.Post("/v1/service-sources", data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to add service source: %w", err)
|
||||
}
|
||||
// res := make(map[string]interface{})
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// add MCP server to higress console, example request body as followed:
|
||||
//
|
||||
// {
|
||||
// "name": "mcp-deepwiki",
|
||||
// "description": "",
|
||||
// "type": "DIRECT_ROUTE", // or OPEN_API
|
||||
// "service": "hgctl-deepwiki.dns:443",
|
||||
// "upstreamPathPrefix": "/mcp",
|
||||
// "services": [
|
||||
// {
|
||||
// "name": "hgctl-deepwiki.dns",
|
||||
// "port": 443,
|
||||
// "version": "1.0",
|
||||
// "weight": 100
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
func HandleAddMCPServer(client *HigressClient, body interface{}) ([]byte, error) {
|
||||
data, ok := body.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to parse request body")
|
||||
}
|
||||
// Validate
|
||||
if _, ok := data["name"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'name' in body")
|
||||
}
|
||||
if _, ok := data["type"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'type' in body")
|
||||
}
|
||||
if _, ok := data["service"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'service' in body")
|
||||
}
|
||||
|
||||
// if _, ok := data["upstreamPathPrefix"]; !ok {
|
||||
// return nil, fmt.Errorf("missing required field 'upstreamPathPrefix' in body")
|
||||
// }
|
||||
|
||||
_, ok = data["services"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing required field 'port' in body")
|
||||
}
|
||||
|
||||
resp, err := client.Put("/v1/mcpServer", data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to add mcp server: %w", err)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// add OpenAPI MCP tools to higress console, example request body:
|
||||
//
|
||||
// {
|
||||
// "id": null,
|
||||
// "name": "openapi-name",
|
||||
// "description": "123",
|
||||
// "domains": [],
|
||||
// "services": [
|
||||
// {
|
||||
// "name": "kubernetes.default.svc.cluster.local",
|
||||
// "port": 443,
|
||||
// "version": null,
|
||||
// "weight": 100
|
||||
// }
|
||||
// ],
|
||||
// "type": "OPEN_API",
|
||||
// "consumerAuthInfo": {
|
||||
// "type": "key-auth",
|
||||
// "enable": false,
|
||||
// "allowedConsumers": []
|
||||
// },
|
||||
// "rawConfigurations": "", // MCP configuration str
|
||||
// "dsn": null,
|
||||
// "dbType": null,
|
||||
// "upstreamPathPrefix": null,
|
||||
// "mcpServerName": "openapi-name"
|
||||
// }
|
||||
func HandleAddOpenAPITool(client *HigressClient, body interface{}) ([]byte, error) {
|
||||
return client.Put("/v1/mcpServer", body)
|
||||
}
|
||||
134
hgctl/pkg/agent/types.go
Normal file
134
hgctl/pkg/agent/types.go
Normal file
@@ -0,0 +1,134 @@
|
||||
// Copyright (c) 2025 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package agent
|
||||
|
||||
type Message struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type Request struct {
|
||||
Model string `json:"model"`
|
||||
Messages []Message `json:"messages"`
|
||||
FrequencyPenalty float64 `json:"frequency_penalty"`
|
||||
PresencePenalty float64 `json:"presence_penalty"`
|
||||
Stream bool `json:"stream"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
Topp int32 `json:"top_p"`
|
||||
}
|
||||
|
||||
type Choice struct {
|
||||
Index int `json:"index"`
|
||||
Message Message `json:"message"`
|
||||
FinishReason string `json:"finish_reason"`
|
||||
}
|
||||
|
||||
type Usage struct {
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
CompletionTokens int `json:"completion_tokens"`
|
||||
TotalTokens int `json:"total_tokens"`
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
ID string `json:"id"`
|
||||
Choices []Choice `json:"choices"`
|
||||
Created int64 `json:"created"`
|
||||
Model string `json:"model"`
|
||||
Object string `json:"object"`
|
||||
Usage Usage `json:"usage"`
|
||||
}
|
||||
|
||||
type ToolsParam struct {
|
||||
ToolName string `yaml:"toolName"`
|
||||
Path string `yaml:"path"`
|
||||
Method string `yaml:"method"`
|
||||
ParamName []string `yaml:"paramName"`
|
||||
Parameter string `yaml:"parameter"`
|
||||
Description string `yaml:"description"`
|
||||
}
|
||||
|
||||
type Info struct {
|
||||
Title string `yaml:"title"`
|
||||
Description string `yaml:"description"`
|
||||
Version string `yaml:"version"`
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
URL string `yaml:"url"`
|
||||
}
|
||||
|
||||
type Parameter struct {
|
||||
Name string `yaml:"name"`
|
||||
In string `yaml:"in"`
|
||||
Description string `yaml:"description"`
|
||||
Required bool `yaml:"required"`
|
||||
Schema struct {
|
||||
Type string `yaml:"type"`
|
||||
Default string `yaml:"default"`
|
||||
Enum []string `yaml:"enum"`
|
||||
} `yaml:"schema"`
|
||||
}
|
||||
|
||||
type Items struct {
|
||||
Type string `yaml:"type"`
|
||||
Example string `yaml:"example"`
|
||||
}
|
||||
|
||||
type Property struct {
|
||||
Description string `yaml:"description"`
|
||||
Type string `yaml:"type"`
|
||||
Enum []string `yaml:"enum,omitempty"`
|
||||
Items *Items `yaml:"items,omitempty"`
|
||||
MaxItems int `yaml:"maxItems,omitempty"`
|
||||
Example string `yaml:"example,omitempty"`
|
||||
}
|
||||
|
||||
type Schema struct {
|
||||
Type string `yaml:"type"`
|
||||
Required []string `yaml:"required"`
|
||||
Properties map[string]Property `yaml:"properties"`
|
||||
}
|
||||
|
||||
type MediaType struct {
|
||||
Schema Schema `yaml:"schema"`
|
||||
}
|
||||
|
||||
type RequestBody struct {
|
||||
Required bool `yaml:"required"`
|
||||
Content map[string]MediaType `yaml:"content"`
|
||||
}
|
||||
|
||||
type PathItem struct {
|
||||
Description string `yaml:"description"`
|
||||
Summary string `yaml:"summary"`
|
||||
OperationID string `yaml:"operationId"`
|
||||
RequestBody RequestBody `yaml:"requestBody"`
|
||||
Parameters []Parameter `yaml:"parameters"`
|
||||
Deprecated bool `yaml:"deprecated"`
|
||||
}
|
||||
|
||||
type Paths map[string]map[string]PathItem
|
||||
|
||||
type Components struct {
|
||||
Schemas map[string]interface{} `yaml:"schemas"`
|
||||
}
|
||||
|
||||
type API struct {
|
||||
OpenAPI string `yaml:"openapi"`
|
||||
Info Info `yaml:"info"`
|
||||
Servers []Server `yaml:"servers"`
|
||||
Paths Paths `yaml:"paths"`
|
||||
Components Components `yaml:"components"`
|
||||
}
|
||||
513
hgctl/pkg/agent/utils.go
Normal file
513
hgctl/pkg/agent/utils.go
Normal file
@@ -0,0 +1,513 @@
|
||||
// Copyright (c) 2025 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package agent
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/alibaba/higress/hgctl/pkg/helm"
|
||||
"github.com/alibaba/higress/hgctl/pkg/installer"
|
||||
"github.com/alibaba/higress/hgctl/pkg/kubernetes"
|
||||
"github.com/alibaba/higress/v2/pkg/cmd/options"
|
||||
"github.com/braydonk/yaml"
|
||||
"github.com/fatih/color"
|
||||
"github.com/higress-group/openapi-to-mcpserver/pkg/converter"
|
||||
"github.com/higress-group/openapi-to-mcpserver/pkg/models"
|
||||
"github.com/higress-group/openapi-to-mcpserver/pkg/parser"
|
||||
"github.com/manifoldco/promptui"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
k8s "k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
|
||||
const (
|
||||
SecretConsoleUser = "adminUsername"
|
||||
SecretConsolePwd = "adminPassword"
|
||||
)
|
||||
|
||||
var binaryName = AgentBinaryName
|
||||
|
||||
// ------ cmd related ------
|
||||
func BindFlagToEnv(cmd *cobra.Command, flagName, envName string) {
|
||||
_ = viper.BindPFlag(flagName, cmd.PersistentFlags().Lookup(flagName))
|
||||
_ = viper.BindEnv(flagName, envName)
|
||||
}
|
||||
|
||||
// ------ Prompt to install prequisite environment ------
|
||||
func checkNodeInstall() bool {
|
||||
cmd := exec.Command("node", "-v")
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
versionStr := strings.TrimPrefix(strings.TrimSpace(string(out)), "v")
|
||||
parts := strings.Split(versionStr, ".")
|
||||
if len(parts) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
major, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return major >= NodeLeastVersion
|
||||
}
|
||||
|
||||
func promptNodeInstall() error {
|
||||
fmt.Println()
|
||||
color.Yellow("⚠️ Node.js is not installed or not found in PATH.")
|
||||
color.Cyan("🔧 Node.js is required to run the agent.")
|
||||
fmt.Println()
|
||||
|
||||
options := []string{
|
||||
"🚀 Install automatically (recommended)",
|
||||
"📖 Exit and show manual installation guide",
|
||||
}
|
||||
|
||||
var ans string
|
||||
prompt := &survey.Select{
|
||||
Message: "How would you like to install Node.js?",
|
||||
Options: options,
|
||||
}
|
||||
if err := survey.AskOne(prompt, &ans); err != nil {
|
||||
return fmt.Errorf("selection error: %w", err)
|
||||
}
|
||||
|
||||
switch ans {
|
||||
case "🚀 Install automatically (recommended)":
|
||||
fmt.Println()
|
||||
color.Green("🚀 Installing Node.js automatically...")
|
||||
|
||||
if err := installNodeAutomatically(); err != nil {
|
||||
color.Red("❌ Installation failed: %v", err)
|
||||
fmt.Println()
|
||||
showNodeManualInstallation()
|
||||
return errors.New("node.js installation failed")
|
||||
}
|
||||
|
||||
color.Green("✅ Node.js installation completed!")
|
||||
fmt.Println()
|
||||
color.Blue("🔍 Verifying installation...")
|
||||
|
||||
if checkNodeInstall() {
|
||||
color.Green("🎉 Node.js is now available!")
|
||||
return nil
|
||||
} else {
|
||||
color.Yellow("⚠️ Node.js installation completed but not found in PATH.")
|
||||
color.Cyan("💡 You may need to restart your terminal or source your shell profile.")
|
||||
return errors.New("node.js installed but not in PATH")
|
||||
}
|
||||
|
||||
case "📖 Exit and show manual installation guide":
|
||||
showNodeManualInstallation()
|
||||
return errors.New("node.js not installed")
|
||||
|
||||
default:
|
||||
return errors.New("invalid selection")
|
||||
}
|
||||
}
|
||||
|
||||
func installNodeAutomatically() error {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
color.Cyan("📦 Please download Node.js installer from https://nodejs.org and run it manually on Windows")
|
||||
return errors.New("automatic installation not supported on Windows yet")
|
||||
case "darwin":
|
||||
// macOS: use brew
|
||||
cmd := exec.Command("brew", "install", "node")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
case "linux":
|
||||
// Linux (Debian/Ubuntu example)
|
||||
cmd := exec.Command("sudo", "apt", "update")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
cmd = exec.Command("sudo", "apt", "install", "-y", "nodejs", "npm")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
default:
|
||||
return errors.New("unsupported OS for automatic installation")
|
||||
}
|
||||
}
|
||||
|
||||
func showNodeManualInstallation() {
|
||||
fmt.Println()
|
||||
|
||||
color.New(color.FgGreen, color.Bold).Println("📖 Manual Node.js Installation Guide")
|
||||
fmt.Println()
|
||||
|
||||
fmt.Println(color.MagentaString("Choose one of the following installation methods:"))
|
||||
fmt.Println()
|
||||
|
||||
color.Cyan("Method 1: Install via package manager")
|
||||
color.Cyan("macOS (brew): brew install node")
|
||||
color.Cyan("Ubuntu/Debian: sudo apt install -y nodejs npm")
|
||||
color.Cyan("Windows: download from https://nodejs.org and run installer")
|
||||
fmt.Println()
|
||||
|
||||
color.Yellow("Method 2: Download from official website")
|
||||
color.Yellow("1. Download Node.js from https://nodejs.org/en/download/")
|
||||
color.Yellow("2. Follow installer instructions and add to PATH if needed")
|
||||
fmt.Println()
|
||||
|
||||
color.Green("✅ Verify Installation")
|
||||
fmt.Println(color.WhiteString("node -v"))
|
||||
fmt.Println(color.WhiteString("npm -v"))
|
||||
fmt.Println()
|
||||
|
||||
color.Cyan("💡 After installation, restart your terminal or source your shell profile.")
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
func checkAgentInstall() bool {
|
||||
cmd := exec.Command(binaryName, "--version")
|
||||
if err := cmd.Run(); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func promptAgentInstall() error {
|
||||
fmt.Println()
|
||||
color.Yellow("⚠️ %s is not installed or not found in PATH.", binaryName)
|
||||
color.Cyan("🔧 %s is required to run the agent.", binaryName)
|
||||
fmt.Println()
|
||||
|
||||
options := []string{
|
||||
"🚀 Install automatically (recommended)",
|
||||
"📖 Exit and show manual installation guide",
|
||||
}
|
||||
|
||||
var ans string
|
||||
prompt := &survey.Select{
|
||||
Message: "How would you like to install " + binaryName + "?",
|
||||
Options: options,
|
||||
}
|
||||
if err := survey.AskOne(prompt, &ans); err != nil {
|
||||
return fmt.Errorf("selection error: %w", err)
|
||||
}
|
||||
|
||||
switch ans {
|
||||
case "🚀 Install automatically (recommended)":
|
||||
fmt.Println()
|
||||
color.Green("🚀 Installing %s automatically...", binaryName)
|
||||
|
||||
if err := installAgentAutomatically(); err != nil {
|
||||
color.Red("❌ Installation failed: %v", err)
|
||||
fmt.Println()
|
||||
showAgentManualInstallation()
|
||||
return errors.New(binaryName + " installation failed")
|
||||
}
|
||||
|
||||
color.Green("✅ %s installation completed!", binaryName)
|
||||
fmt.Println()
|
||||
color.Blue("🔍 Verifying installation...")
|
||||
|
||||
if checkAgentInstall() {
|
||||
color.Green("🎉 %s is now available!", binaryName)
|
||||
return nil
|
||||
} else {
|
||||
color.Yellow("⚠️ %s installed but not found in PATH.", binaryName)
|
||||
color.Cyan("💡 You may need to restart your terminal or source your shell profile.")
|
||||
return errors.New(binaryName + " installed but not in PATH")
|
||||
}
|
||||
|
||||
case "📖 Exit and show manual installation guide":
|
||||
showAgentManualInstallation()
|
||||
return errors.New(binaryName + " not installed")
|
||||
|
||||
default:
|
||||
return errors.New("invalid selection")
|
||||
}
|
||||
}
|
||||
|
||||
func installAgentAutomatically() error {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
cmd := exec.Command("cmd", "/C", AgentInstallCmd)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
case "darwin":
|
||||
cmd := exec.Command("bash", "-c", AgentInstallCmd)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
case "linux":
|
||||
cmd := exec.Command("bash", "-c", AgentInstallCmd)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
default:
|
||||
return errors.New("unsupported OS for automatic installation")
|
||||
}
|
||||
}
|
||||
|
||||
func showAgentManualInstallation() {
|
||||
fmt.Println()
|
||||
color.New(color.FgGreen, color.Bold).Printf("📖 Manual %s Installation Guide\n", binaryName)
|
||||
fmt.Println()
|
||||
|
||||
fmt.Println(color.MagentaString("Supported Operating Systems: macOS 10.15+, Ubuntu 20.04+/Debian 10+, or Windows 10+ (WSL/Git for Windows)"))
|
||||
fmt.Println(color.MagentaString("Hardware: 4GB+ RAM"))
|
||||
fmt.Println(color.MagentaString("Software: Node.js 18+"))
|
||||
fmt.Println(color.MagentaString("Network: Internet connection required for authentication and AI processing"))
|
||||
fmt.Println(color.MagentaString("Shell: Works best in Bash, Zsh, or Fish"))
|
||||
fmt.Println()
|
||||
|
||||
color.Cyan("Method 1: Download prebuilt binary")
|
||||
color.Cyan(fmt.Sprintf("1. Go to official release page: %s", AgentReleasePage))
|
||||
fmt.Printf(color.CyanString("2. Download %s for your OS\n"), binaryName)
|
||||
color.Cyan("3. Make it executable and place it in a directory in your PATH")
|
||||
fmt.Println()
|
||||
|
||||
fmt.Println()
|
||||
color.Green("✅ Verify Installation")
|
||||
fmt.Printf(color.WhiteString("%s --version\n"), binaryName)
|
||||
fmt.Println()
|
||||
color.Cyan("💡 After installation, restart your terminal or source your shell profile.")
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// ------ MCP convert utils function ------
|
||||
func parseOpenapi2MCP(arg MCPAddArg) *models.MCPConfig {
|
||||
path := arg.spec
|
||||
serverName := arg.name
|
||||
|
||||
// Create a new parser
|
||||
p := parser.NewParser()
|
||||
|
||||
p.SetValidation(true)
|
||||
|
||||
// Parse the OpenAPI specification
|
||||
err := p.ParseFile(path)
|
||||
if err != nil {
|
||||
fmt.Printf("Error parsing OpenAPI specification: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
c := converter.NewConverter(p, models.ConvertOptions{
|
||||
ServerName: serverName,
|
||||
ToolNamePrefix: "",
|
||||
TemplatePath: "",
|
||||
})
|
||||
|
||||
// Convert the OpenAPI specification to an MCP configuration
|
||||
config, err := c.Convert()
|
||||
if err != nil {
|
||||
fmt.Printf("Error converting OpenAPI specification: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
func convertMCPConfigToStr(cfg *models.MCPConfig) string {
|
||||
var data []byte
|
||||
var buffer bytes.Buffer
|
||||
encoder := yaml.NewEncoder(&buffer)
|
||||
encoder.SetIndent(2)
|
||||
|
||||
if err := encoder.Encode(cfg); err != nil {
|
||||
fmt.Printf("Error encoding YAML: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
data = buffer.Bytes()
|
||||
str := string(data)
|
||||
|
||||
// fmt.Println("Successfully converted OpenAPI specification to MCP Server")
|
||||
// fmt.Printf("Get MCP server config string: %v", str)
|
||||
return str
|
||||
|
||||
// if err != nil {
|
||||
// fmt.Printf("Error marshaling MCP configuration: %v\n", err)
|
||||
// os.Exit(1)
|
||||
// }
|
||||
|
||||
// err = os.WriteFile(*outputFile, data, 0644)
|
||||
// if err != nil {
|
||||
// fmt.Printf("Error writing MCP configuration: %v\n", err)
|
||||
// os.Exit(1)
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
func GetHigressGatewayServiceIP() (string, error) {
|
||||
color.Cyan("🚀 Adding openapi MCP Server to agent, checking Higress Gateway Pod status...")
|
||||
|
||||
defaultKubeconfig := filepath.Join(os.Getenv("HOME"), ".kube", "config")
|
||||
config, err := clientcmd.BuildConfigFromFlags("", defaultKubeconfig)
|
||||
if err != nil {
|
||||
color.Yellow("⚠️ Failed to load default kubeconfig: %v", err)
|
||||
return promptForServiceKubeSettingsAndRetry()
|
||||
}
|
||||
|
||||
clientset, err := k8s.NewForConfig(config)
|
||||
if err != nil {
|
||||
color.Yellow("⚠️ Failed to create Kubernetes client: %v", err)
|
||||
return promptForServiceKubeSettingsAndRetry()
|
||||
}
|
||||
|
||||
namespace := "higress-system"
|
||||
svc, err := clientset.CoreV1().Services(namespace).Get(context.Background(), "higress-gateway", metav1.GetOptions{})
|
||||
if err != nil || svc == nil {
|
||||
color.Yellow("⚠️ Could not find Higress Gateway Service in namespace '%s'.", namespace)
|
||||
return promptForServiceKubeSettingsAndRetry()
|
||||
}
|
||||
|
||||
ip, err := extractServiceIP(clientset, namespace, svc)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
color.Green("✅ Found Higress Gateway Service IP: %s (namespace: %s)", ip, namespace)
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
// higress-gateway should always be LoadBalancer
|
||||
func extractServiceIP(clientset *k8s.Clientset, namespace string, svc *v1.Service) (string, error) {
|
||||
return svc.Spec.ClusterIP, nil
|
||||
|
||||
// // fallback to Pod IP
|
||||
// if len(svc.Spec.Selector) > 0 {
|
||||
// selector := metav1.FormatLabelSelector(&metav1.LabelSelector{MatchLabels: svc.Spec.Selector})
|
||||
// pods, err := clientset.CoreV1().Pods(namespace).List(context.Background(), metav1.ListOptions{
|
||||
// LabelSelector: selector,
|
||||
// })
|
||||
// if err != nil {
|
||||
// return "", fmt.Errorf("failed to list pods for selector: %v", err)
|
||||
// }
|
||||
// if len(pods.Items) > 0 {
|
||||
// return pods.Items[0].Status.PodIP, nil
|
||||
// }
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
// prompt fallback for user input
|
||||
func promptForServiceKubeSettingsAndRetry() (string, error) {
|
||||
color.Cyan("Let's fix it manually 👇")
|
||||
|
||||
kubeconfigPrompt := promptui.Prompt{
|
||||
Label: "Enter kubeconfig path",
|
||||
Default: filepath.Join(os.Getenv("HOME"), ".kube", "config"),
|
||||
}
|
||||
kubeconfigPath, err := kubeconfigPrompt.Run()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("aborted: %v", err)
|
||||
}
|
||||
|
||||
nsPrompt := promptui.Prompt{
|
||||
Label: "Enter Higress namespace",
|
||||
Default: "higress-system",
|
||||
}
|
||||
namespace, err := nsPrompt.Run()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
config, err := clientcmd.BuildConfigFromFlags("", kubeconfigPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to load kubeconfig: %v", err)
|
||||
}
|
||||
|
||||
clientset, err := k8s.NewForConfig(config)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create kubernetes client: %v", err)
|
||||
}
|
||||
|
||||
svc, err := clientset.CoreV1().Services(namespace).Get(context.Background(), "higress-gateway", metav1.GetOptions{})
|
||||
if err != nil || svc == nil {
|
||||
color.Red("❌ Higress Gateway Service not found in namespace '%s'", namespace)
|
||||
return "", fmt.Errorf("service not found")
|
||||
}
|
||||
|
||||
ip, err := extractServiceIP(clientset, namespace, svc)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
color.Green("✅ Found Higress Gateway Service IP: %s (namespace: %s)", ip, namespace)
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
func getConsoleCredentials(profile *helm.Profile) (username, password string, err error) {
|
||||
cliClient, err := kubernetes.NewCLIClient(options.DefaultConfigFlags.ToRawKubeConfigLoader())
|
||||
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to build kubernetes client: %w", err)
|
||||
}
|
||||
|
||||
secret, err := cliClient.KubernetesInterface().CoreV1().Secrets(profile.Global.Namespace).Get(context.Background(), "higress-console", metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return string(secret.Data[SecretConsoleUser]), string(secret.Data[SecretConsolePwd]), nil
|
||||
}
|
||||
|
||||
// This function will do following things:
|
||||
// 1. read the profile from local-file
|
||||
// 2. read the profile from k8s' configMap
|
||||
// 3. combine the two type profiles together and return
|
||||
func getAllProfiles() ([]*installer.ProfileContext, error) {
|
||||
profileContexts := make([]*installer.ProfileContext, 0)
|
||||
profileInstalledPath, err := installer.GetProfileInstalledPath()
|
||||
if err != nil {
|
||||
return profileContexts, nil
|
||||
}
|
||||
fileProfileStore, err := installer.NewFileDirProfileStore(profileInstalledPath)
|
||||
if err != nil {
|
||||
return profileContexts, nil
|
||||
}
|
||||
fileProfileContexts, err := fileProfileStore.List()
|
||||
if err == nil {
|
||||
profileContexts = append(profileContexts, fileProfileContexts...)
|
||||
}
|
||||
|
||||
cliClient, err := kubernetes.NewCLIClient(options.DefaultConfigFlags.ToRawKubeConfigLoader())
|
||||
if err != nil {
|
||||
return profileContexts, nil
|
||||
}
|
||||
configmapProfileStore, err := installer.NewConfigmapProfileStore(cliClient)
|
||||
if err != nil {
|
||||
return profileContexts, nil
|
||||
}
|
||||
|
||||
configmapProfileContexts, err := configmapProfileStore.List()
|
||||
if err == nil {
|
||||
profileContexts = append(profileContexts, configmapProfileContexts...)
|
||||
}
|
||||
return profileContexts, nil
|
||||
}
|
||||
@@ -17,6 +17,7 @@ package hgctl
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/alibaba/higress/hgctl/pkg/agent"
|
||||
"github.com/alibaba/higress/hgctl/pkg/plugin"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -42,6 +43,8 @@ func GetRootCommand() *cobra.Command {
|
||||
rootCmd.AddCommand(plugin.NewCommand())
|
||||
rootCmd.AddCommand(newCompletionCmd(os.Stdout))
|
||||
rootCmd.AddCommand(newCodeDebugCmd())
|
||||
rootCmd.AddCommand(agent.NewMCPCmd())
|
||||
rootCmd.AddCommand(agent.NewAgentCmd())
|
||||
|
||||
return rootCmd
|
||||
}
|
||||
|
||||
Submodule istio/istio updated: fa8896cf33...017e7d8a4d
@@ -653,7 +653,7 @@ func (m *IngressConfig) convertEnvoyFilter(convertOptions *common.ConvertOptions
|
||||
loadBalance := route.WrapperConfig.AnnotationsConfig.LoadBalance
|
||||
if loadBalance != nil && loadBalance.McpSseStateful {
|
||||
IngressLog.Infof("Found MCP SSE stateful session for route %s", route.HTTPRoute.Name)
|
||||
envoyFilter, err := m.constructMcpSseStatefulSessionEnvoyFilter(route, m.namespace, initMcpSseGlobalFilter)
|
||||
envoyFilter, err := m.constructMcpSseStatefulSessionEnvoyFilter(route, m.namespace, initMcpSseGlobalFilter, loadBalance.McpSseStatefulKey)
|
||||
if err != nil {
|
||||
IngressLog.Errorf("Construct MCP SSE stateful session EnvoyFilter error %v", err)
|
||||
} else {
|
||||
@@ -857,8 +857,17 @@ func (m *IngressConfig) convertDestinationRule(configs []common.WrapperConfig) [
|
||||
portUpdated := false
|
||||
for _, policy := range dr.DestinationRule.TrafficPolicy.PortLevelSettings {
|
||||
if policy.Port.Number == portTrafficPolicy.Port.Number {
|
||||
policy.Tls = portTrafficPolicy.Tls
|
||||
policy.LoadBalancer = portTrafficPolicy.LoadBalancer
|
||||
// Only set Tls if not already configured
|
||||
if policy.Tls == nil && portTrafficPolicy.Tls != nil {
|
||||
policy.Tls = portTrafficPolicy.Tls
|
||||
}
|
||||
// Only set LoadBalancer if not already configured
|
||||
if policy.LoadBalancer == nil && portTrafficPolicy.LoadBalancer != nil {
|
||||
policy.LoadBalancer = portTrafficPolicy.LoadBalancer
|
||||
} else if policy.LoadBalancer != nil && policy.LoadBalancer.LbPolicy == nil &&
|
||||
portTrafficPolicy.LoadBalancer != nil && portTrafficPolicy.LoadBalancer.LbPolicy != nil {
|
||||
policy.LoadBalancer.LbPolicy = portTrafficPolicy.LoadBalancer.LbPolicy
|
||||
}
|
||||
portUpdated = true
|
||||
break
|
||||
}
|
||||
@@ -1956,7 +1965,7 @@ func (m *IngressConfig) Delete(config.GroupVersionKind, string, string, *string)
|
||||
return common.ErrUnsupportedOp
|
||||
}
|
||||
|
||||
func (m *IngressConfig) constructMcpSseStatefulSessionEnvoyFilter(route *common.WrapperHTTPRoute, namespace string, initGlobalFilter bool) (*config.Config, error) {
|
||||
func (m *IngressConfig) constructMcpSseStatefulSessionEnvoyFilter(route *common.WrapperHTTPRoute, namespace string, initGlobalFilter bool, mcpSseStatefulKey string) (*config.Config, error) {
|
||||
httpRoute := route.HTTPRoute
|
||||
|
||||
var configPatches []*networking.EnvoyFilter_EnvoyConfigObjectPatch
|
||||
@@ -2010,7 +2019,7 @@ func (m *IngressConfig) constructMcpSseStatefulSessionEnvoyFilter(route *common.
|
||||
},
|
||||
Patch: &networking.EnvoyFilter_Patch{
|
||||
Operation: networking.EnvoyFilter_Patch_MERGE,
|
||||
Value: buildPatchStruct(`{
|
||||
Value: buildPatchStruct(fmt.Sprintf(`{
|
||||
"typed_per_filter_config": {
|
||||
"envoy.filters.http.mcp_sse_stateful_session": {
|
||||
"@type": "type.googleapis.com/udpa.type.v1.TypedStruct",
|
||||
@@ -2023,7 +2032,7 @@ func (m *IngressConfig) constructMcpSseStatefulSessionEnvoyFilter(route *common.
|
||||
"@type": "type.googleapis.com/udpa.type.v1.TypedStruct",
|
||||
"type_url": "type.googleapis.com/envoy.extensions.http.mcp_sse_stateful_session.envelope.v3alpha.EnvelopeSessionState",
|
||||
"value": {
|
||||
"param_name": "sessionId",
|
||||
"param_name": "%s",
|
||||
"chunk_end_patterns": ["\r\n\r\n", "\n\n", "\r\r"]
|
||||
}
|
||||
}
|
||||
@@ -2033,7 +2042,7 @@ func (m *IngressConfig) constructMcpSseStatefulSessionEnvoyFilter(route *common.
|
||||
}
|
||||
}
|
||||
}
|
||||
}`),
|
||||
}`, mcpSseStatefulKey)),
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -41,6 +41,9 @@ const (
|
||||
|
||||
defaultAffinityCookieName = "INGRESSCOOKIE"
|
||||
defaultAffinityCookiePath = "/"
|
||||
|
||||
mcpSseStatefulKey = "mcp-sse-stateful-param-name"
|
||||
defaultMcpSseStatefulKey = "sessionId"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -66,10 +69,11 @@ type consistentHashByCookie struct {
|
||||
}
|
||||
|
||||
type LoadBalanceConfig struct {
|
||||
simple networking.LoadBalancerSettings_SimpleLB
|
||||
other *consistentHashByOther
|
||||
cookie *consistentHashByCookie
|
||||
McpSseStateful bool
|
||||
simple networking.LoadBalancerSettings_SimpleLB
|
||||
other *consistentHashByOther
|
||||
cookie *consistentHashByCookie
|
||||
McpSseStateful bool
|
||||
McpSseStatefulKey string
|
||||
}
|
||||
|
||||
type loadBalance struct{}
|
||||
@@ -139,6 +143,11 @@ func (l loadBalance) Parse(annotations Annotations, config *Ingress, _ *GlobalCo
|
||||
lb = strings.ToUpper(lb)
|
||||
if lb == "MCP-SSE" {
|
||||
loadBalanceConfig.McpSseStateful = true
|
||||
if key, err := annotations.ParseStringASAP(mcpSseStatefulKey); err == nil {
|
||||
loadBalanceConfig.McpSseStatefulKey = key
|
||||
} else {
|
||||
loadBalanceConfig.McpSseStatefulKey = defaultMcpSseStatefulKey
|
||||
}
|
||||
} else {
|
||||
loadBalanceConfig.simple = networking.LoadBalancerSettings_SimpleLB(networking.LoadBalancerSettings_SimpleLB_value[lb])
|
||||
}
|
||||
|
||||
@@ -39,10 +39,22 @@ type RedisConfig struct {
|
||||
Username string `json:"username,omitempty"`
|
||||
// The password for Redis authentication
|
||||
Password string `json:"password,omitempty"`
|
||||
// Reference to a secret containing the password
|
||||
PasswordSecret *SecretKeyReference `json:"passwordSecret,omitempty"`
|
||||
// The database index to use
|
||||
DB int `json:"db,omitempty"`
|
||||
}
|
||||
|
||||
// SecretKeyReference defines a reference to a key within a Kubernetes secret
|
||||
type SecretKeyReference struct {
|
||||
// The namespace of the secret. Defaults to the higress system namespace.
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
// The name of the secret
|
||||
Name string `json:"name,omitempty"`
|
||||
// The key within the secret data
|
||||
Key string `json:"key,omitempty"`
|
||||
}
|
||||
|
||||
// MCPRatelimitConfig defines the configuration for rate limit
|
||||
type MCPRatelimitConfig struct {
|
||||
// The limit of the rate limit
|
||||
@@ -119,6 +131,15 @@ func validMcpServer(m *McpServer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if m.Redis != nil && m.Redis.PasswordSecret != nil {
|
||||
if m.Redis.PasswordSecret.Name == "" {
|
||||
return errors.New("redis passwordSecret.name cannot be empty")
|
||||
}
|
||||
if m.Redis.PasswordSecret.Key == "" {
|
||||
return errors.New("redis passwordSecret.key cannot be empty")
|
||||
}
|
||||
}
|
||||
|
||||
if m.EnableUserLevelServer && m.Redis == nil {
|
||||
return errors.New("redis config cannot be empty when user level server is enabled")
|
||||
}
|
||||
@@ -184,6 +205,13 @@ func deepCopyMcpServer(mcp *McpServer) (*McpServer, error) {
|
||||
Password: mcp.Redis.Password,
|
||||
DB: mcp.Redis.DB,
|
||||
}
|
||||
if mcp.Redis.PasswordSecret != nil {
|
||||
newMcp.Redis.PasswordSecret = &SecretKeyReference{
|
||||
Namespace: mcp.Redis.PasswordSecret.Namespace,
|
||||
Name: mcp.Redis.PasswordSecret.Name,
|
||||
Key: mcp.Redis.PasswordSecret.Key,
|
||||
}
|
||||
}
|
||||
}
|
||||
if mcp.Ratelimit != nil {
|
||||
newMcp.Ratelimit = &MCPRatelimitConfig{
|
||||
@@ -504,12 +532,24 @@ func (m *McpServerController) constructMcpSessionStruct(mcp *McpServer) string {
|
||||
// Build redis configuration
|
||||
redisConfig := "null"
|
||||
if mcp.Redis != nil {
|
||||
passwordValue := mcp.Redis.Password
|
||||
if mcp.Redis.PasswordSecret != nil && mcp.Redis.PasswordSecret.Name != "" && mcp.Redis.PasswordSecret.Key != "" {
|
||||
ns := mcp.Redis.PasswordSecret.Namespace
|
||||
if ns == "" {
|
||||
ns = m.Namespace
|
||||
}
|
||||
if ns != "" {
|
||||
passwordValue = fmt.Sprintf("${secret.%s/%s.%s}", ns, mcp.Redis.PasswordSecret.Name, mcp.Redis.PasswordSecret.Key)
|
||||
} else {
|
||||
passwordValue = fmt.Sprintf("${secret.%s.%s}", mcp.Redis.PasswordSecret.Name, mcp.Redis.PasswordSecret.Key)
|
||||
}
|
||||
}
|
||||
redisConfig = fmt.Sprintf(`{
|
||||
"address": "%s",
|
||||
"username": "%s",
|
||||
"password": "%s",
|
||||
"db": %d
|
||||
}`, mcp.Redis.Address, mcp.Redis.Username, mcp.Redis.Password, mcp.Redis.DB)
|
||||
}`, mcp.Redis.Address, mcp.Redis.Username, passwordValue, mcp.Redis.DB)
|
||||
}
|
||||
|
||||
// Build rate limit configuration
|
||||
|
||||
@@ -120,6 +120,30 @@ func Test_validMcpServer(t *testing.T) {
|
||||
},
|
||||
wantErr: errors.New("redis config cannot be empty when user level server is enabled"),
|
||||
},
|
||||
{
|
||||
name: "redis config with password secret missing name",
|
||||
mcp: &McpServer{
|
||||
Enable: true,
|
||||
Redis: &RedisConfig{
|
||||
PasswordSecret: &SecretKeyReference{
|
||||
Key: "password",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: errors.New("redis passwordSecret.name cannot be empty"),
|
||||
},
|
||||
{
|
||||
name: "redis config with password secret missing key",
|
||||
mcp: &McpServer{
|
||||
Enable: true,
|
||||
Redis: &RedisConfig{
|
||||
PasswordSecret: &SecretKeyReference{
|
||||
Name: "redis-credentials",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: errors.New("redis passwordSecret.key cannot be empty"),
|
||||
},
|
||||
{
|
||||
name: "valid config with redis",
|
||||
mcp: &McpServer{
|
||||
@@ -152,6 +176,20 @@ func Test_validMcpServer(t *testing.T) {
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "valid config with redis password secret",
|
||||
mcp: &McpServer{
|
||||
Enable: true,
|
||||
Redis: &RedisConfig{
|
||||
Address: "localhost:6379",
|
||||
PasswordSecret: &SecretKeyReference{
|
||||
Name: "redis-credentials",
|
||||
Key: "password",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -265,7 +303,11 @@ func Test_deepCopyMcpServer(t *testing.T) {
|
||||
Address: "localhost:6379",
|
||||
Username: "default",
|
||||
Password: "password",
|
||||
DB: 0,
|
||||
PasswordSecret: &SecretKeyReference{
|
||||
Name: "redis-credentials",
|
||||
Key: "password",
|
||||
},
|
||||
DB: 0,
|
||||
},
|
||||
MatchList: []*MatchRule{},
|
||||
Servers: []*SSEServer{},
|
||||
@@ -276,7 +318,11 @@ func Test_deepCopyMcpServer(t *testing.T) {
|
||||
Address: "localhost:6379",
|
||||
Username: "default",
|
||||
Password: "password",
|
||||
DB: 0,
|
||||
PasswordSecret: &SecretKeyReference{
|
||||
Name: "redis-credentials",
|
||||
Key: "password",
|
||||
},
|
||||
DB: 0,
|
||||
},
|
||||
MatchList: []*MatchRule{},
|
||||
Servers: []*SSEServer{},
|
||||
@@ -291,7 +337,12 @@ func Test_deepCopyMcpServer(t *testing.T) {
|
||||
Address: "localhost:6379",
|
||||
Username: "default",
|
||||
Password: "password",
|
||||
DB: 0,
|
||||
PasswordSecret: &SecretKeyReference{
|
||||
Name: "redis-credentials",
|
||||
Namespace: "custom-ns",
|
||||
Key: "password",
|
||||
},
|
||||
DB: 0,
|
||||
},
|
||||
SSEPathSuffix: "/sse",
|
||||
MatchList: []*MatchRule{
|
||||
@@ -318,7 +369,12 @@ func Test_deepCopyMcpServer(t *testing.T) {
|
||||
Address: "localhost:6379",
|
||||
Username: "default",
|
||||
Password: "password",
|
||||
DB: 0,
|
||||
PasswordSecret: &SecretKeyReference{
|
||||
Name: "redis-credentials",
|
||||
Namespace: "custom-ns",
|
||||
Key: "password",
|
||||
},
|
||||
DB: 0,
|
||||
},
|
||||
SSEPathSuffix: "/sse",
|
||||
MatchList: []*MatchRule{
|
||||
@@ -706,6 +762,80 @@ func TestMcpServerController_constructMcpSessionStruct(t *testing.T) {
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "config with password secret",
|
||||
mcp: &McpServer{
|
||||
Enable: true,
|
||||
Redis: &RedisConfig{
|
||||
Address: "localhost:6379",
|
||||
Password: "ignored",
|
||||
PasswordSecret: &SecretKeyReference{
|
||||
Name: "redis-credentials",
|
||||
Key: "password",
|
||||
},
|
||||
},
|
||||
MatchList: []*MatchRule{},
|
||||
Servers: []*SSEServer{},
|
||||
},
|
||||
wantJSON: `{
|
||||
"@type": "type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config",
|
||||
"library_id": "mcp-session",
|
||||
"library_path": "/var/lib/istio/envoy/golang-filter.so",
|
||||
"plugin_name": "mcp-session",
|
||||
"plugin_config": {
|
||||
"@type": "type.googleapis.com/xds.type.v3.TypedStruct",
|
||||
"value": {
|
||||
"redis": {
|
||||
"address": "localhost:6379",
|
||||
"username": "",
|
||||
"password": "${secret.test-namespace/redis-credentials.password}",
|
||||
"db": 0
|
||||
},
|
||||
"rate_limit": null,
|
||||
"sse_path_suffix": "",
|
||||
"match_list": [],
|
||||
"enable_user_level_server": false
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "config with password secret and namespace",
|
||||
mcp: &McpServer{
|
||||
Enable: true,
|
||||
Redis: &RedisConfig{
|
||||
Address: "localhost:6379",
|
||||
PasswordSecret: &SecretKeyReference{
|
||||
Namespace: "other-ns",
|
||||
Name: "redis-credentials",
|
||||
Key: "password",
|
||||
},
|
||||
},
|
||||
MatchList: []*MatchRule{},
|
||||
Servers: []*SSEServer{},
|
||||
},
|
||||
wantJSON: `{
|
||||
"@type": "type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config",
|
||||
"library_id": "mcp-session",
|
||||
"library_path": "/var/lib/istio/envoy/golang-filter.so",
|
||||
"plugin_name": "mcp-session",
|
||||
"plugin_config": {
|
||||
"@type": "type.googleapis.com/xds.type.v3.TypedStruct",
|
||||
"value": {
|
||||
"redis": {
|
||||
"address": "localhost:6379",
|
||||
"username": "",
|
||||
"password": "${secret.other-ns/redis-credentials.password}",
|
||||
"db": 0
|
||||
},
|
||||
"rate_limit": null,
|
||||
"sse_path_suffix": "",
|
||||
"match_list": [],
|
||||
"enable_user_level_server": false
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -8,10 +8,15 @@ replace github.com/mark3labs/mcp-go => github.com/higress-group/mcp-go v0.0.0-20
|
||||
|
||||
require (
|
||||
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42
|
||||
github.com/distribution/distribution/v3 v3.0.0-20220526142353-ffbd94cbe269
|
||||
github.com/envoyproxy/envoy v1.33.1-0.20250325161043-11ab50a29d99
|
||||
github.com/go-redis/redis/v8 v8.11.5
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/mark3labs/mcp-go v0.12.0
|
||||
github.com/milvus-io/milvus-sdk-go/v2 v2.4.2
|
||||
github.com/openai/openai-go/v2 v2.7.0
|
||||
github.com/pkoukk/tiktoken-go v0.1.8
|
||||
github.com/stretchr/testify v1.9.0
|
||||
google.golang.org/protobuf v1.36.5
|
||||
gorm.io/driver/clickhouse v0.6.1
|
||||
gorm.io/driver/mysql v1.5.7
|
||||
@@ -45,23 +50,31 @@ require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.5.5 // indirect
|
||||
github.com/cockroachdb/errors v1.9.1 // indirect
|
||||
github.com/cockroachdb/logtags v0.0.0-20211118104740-dabe8e521a4f // indirect
|
||||
github.com/cockroachdb/redact v1.1.3 // indirect
|
||||
github.com/deckarep/golang-set v1.7.1 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/getsentry/sentry-go v0.12.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/mock v1.6.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/milvus-io/milvus-sdk-go/v2 v2.4.2 // indirect
|
||||
github.com/milvus-io/milvus-proto/go-api/v2 v2.4.10-0.20240819025435-512e3b98866a // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/openai/openai-go/v2 v2.7.0 // indirect
|
||||
github.com/orcaman/concurrent-map v0.0.0-20210501183033-44dafcb38ecc // indirect
|
||||
github.com/pkoukk/tiktoken-go v0.1.8 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_golang v1.14.0 // indirect
|
||||
github.com/prometheus/client_model v0.4.0 // indirect
|
||||
github.com/prometheus/common v0.37.0 // indirect
|
||||
github.com/prometheus/procfs v0.8.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.10.0 // indirect
|
||||
github.com/tidwall/gjson v1.18.0 // indirect
|
||||
github.com/tidwall/match v1.2.0 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
@@ -110,7 +123,7 @@ require (
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
replace github.com/nacos-group/nacos-sdk-go/v2 v2.2.9 => github.com/luoxiner/nacos-sdk-go/v2 v2.2.9-40
|
||||
|
||||
@@ -33,6 +33,7 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
|
||||
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
@@ -41,6 +42,11 @@ github.com/ClickHouse/ch-go v0.61.5 h1:zwR8QbYI0tsMiEcze/uIMK+Tz1D3XZXLdNrlaOpeE
|
||||
github.com/ClickHouse/ch-go v0.61.5/go.mod h1:s1LJW/F/LcFs5HJnuogFMta50kKDO0lf9zzfrbl0RQg=
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.23.2 h1:+DAKPMnxLS7pduQZsrJc8OhdLS2L9MfDEJ2TS+hpYDM=
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.23.2/go.mod h1:aNap51J1OM3yxQJRgM+AlP/MPkGBCL8A74uQThoQhR0=
|
||||
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno=
|
||||
github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo=
|
||||
github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY=
|
||||
github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0=
|
||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
@@ -109,6 +115,8 @@ github.com/aliyun/credentials-go v1.4.3 h1:N3iHyvHRMyOwY1+0qBLSf3hb5JFiOujVSVuEp
|
||||
github.com/aliyun/credentials-go v1.4.3/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
@@ -127,25 +135,57 @@ github.com/clbanning/mxj/v2 v2.5.5 h1:oT81vUeEiQQ/DcHbzSytRngP6Ky9O+L+0Bw0zSJag9
|
||||
github.com/clbanning/mxj/v2 v2.5.5/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 h1:Om6kYQYDUk5wWbT0t0q6pvyM49i9XZAv9dDrkDA7gjk=
|
||||
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
|
||||
github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU=
|
||||
github.com/cockroachdb/errors v1.9.1 h1:yFVvsI0VxmRShfawbt/laCIDy/mtTqqnvoNgiy5bEV8=
|
||||
github.com/cockroachdb/errors v1.9.1/go.mod h1:2sxOtL2WIc096WSZqZ5h8fa17rdDq9HZOZLBCor4mBk=
|
||||
github.com/cockroachdb/logtags v0.0.0-20211118104740-dabe8e521a4f h1:6jduT9Hfc0njg5jJ1DdKCFPdMBrp/mdZfCpa5h+WM74=
|
||||
github.com/cockroachdb/logtags v0.0.0-20211118104740-dabe8e521a4f/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs=
|
||||
github.com/cockroachdb/redact v1.1.3 h1:AKZds10rFSIj7qADf0g46UixK8NNLwWTNdCIGS5wfSQ=
|
||||
github.com/cockroachdb/redact v1.1.3/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
|
||||
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/deckarep/golang-set v1.7.1 h1:SCQV0S6gTtp6itiFrTqI+pfmJ4LN85S1YzhDf9rTHJQ=
|
||||
github.com/deckarep/golang-set v1.7.1/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ=
|
||||
github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4=
|
||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/distribution/distribution/v3 v3.0.0-20220526142353-ffbd94cbe269 h1:hbCT8ZPPMqefiAWD2ZKjn7ypokIGViTvBBg/ExLSdCk=
|
||||
github.com/distribution/distribution/v3 v3.0.0-20220526142353-ffbd94cbe269/go.mod h1:28YO/VJk9/64+sTGNuYaBjWxrXTPrj0C0XmgTIOjxX4=
|
||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew=
|
||||
github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw=
|
||||
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
|
||||
github.com/getsentry/sentry-go v0.12.0 h1:era7g0re5iY13bHSdN/xMkyV+5zZppjRVQhZrXCaEIk=
|
||||
github.com/getsentry/sentry-go v0.12.0/go.mod h1:NSap0JBYWzHND8oMbyi0+XZhUalc1TBdRL1M71JZW2c=
|
||||
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
|
||||
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
|
||||
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
|
||||
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
|
||||
github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
|
||||
@@ -161,14 +201,24 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
|
||||
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
||||
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
||||
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||
github.com/gogo/googleapis v0.0.0-20180223154316-0cd9801be74a/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
|
||||
github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY1WM=
|
||||
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
@@ -201,6 +251,7 @@ github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
@@ -215,6 +266,7 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
@@ -226,21 +278,36 @@ github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hf
|
||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
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/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y=
|
||||
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
|
||||
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/higress-group/envoy v0.0.0-20250430151331-2c556780b65c h1:chAOZk/qEXFhLILWoNucj3X6r9xYnRR+SWFvhsOa2oo=
|
||||
github.com/higress-group/envoy v0.0.0-20250430151331-2c556780b65c/go.mod h1:SU+IJUAfh1kkZtH+u0E1dnwho8AhbGeYMgp5vvjU+Gc=
|
||||
github.com/higress-group/mcp-go v0.0.0-20250428145706-792ce64b4b30 h1:N4NMq8M1nZyyChPyzn+EUUdHi5asig2uLR5hOyRmsXI=
|
||||
github.com/higress-group/mcp-go v0.0.0-20250428145706-792ce64b4b30/go.mod h1:O9gri9UOzthw728vusc2oNu99lVh8cKCajpxNfC90gE=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/hydrogen18/memlistener v0.0.0-20200120041712-dcc25e7acd91/go.mod h1:qEIFzExnS6016fRpRfxrExeVn2gbClQA99gQhnIcdhE=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI=
|
||||
github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0=
|
||||
github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk=
|
||||
github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g=
|
||||
github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||
@@ -261,6 +328,7 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfC
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
@@ -270,30 +338,58 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
|
||||
github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8=
|
||||
github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE=
|
||||
github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE=
|
||||
github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro=
|
||||
github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
|
||||
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/labstack/echo/v4 v4.5.0/go.mod h1:czIriw4a0C1dFun+ObrXp7ok03xON0N1awStJ6ArI7Y=
|
||||
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
|
||||
github.com/luoxiner/nacos-sdk-go/v2 v2.2.9-40 h1:nzRTBplC0riQqQwEHZThw5H4/TH5LgWTQTm6A7t1lpY=
|
||||
github.com/luoxiner/nacos-sdk-go/v2 v2.2.9-40/go.mod h1:9FKXl6FqOiVmm72i8kADtbeK71egyG9y3uRDBg41tpQ=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8=
|
||||
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
|
||||
github.com/milvus-io/milvus-proto/go-api/v2 v2.4.10-0.20240819025435-512e3b98866a h1:0B/8Fo66D8Aa23Il0yrQvg1KKz92tE/BJ5BvkUxxAAk=
|
||||
github.com/milvus-io/milvus-proto/go-api/v2 v2.4.10-0.20240819025435-512e3b98866a/go.mod h1:1OIl0v5PQeNxIJhCvY+K55CBUOYDZevw9g9380u1Wek=
|
||||
github.com/milvus-io/milvus-sdk-go/v2 v2.4.2 h1:Xqf+S7iicElwYoS2Zly8Nf/zKHuZsNy1xQajfdtygVY=
|
||||
github.com/milvus-io/milvus-sdk-go/v2 v2.4.2/go.mod h1:ulO1YUXKH0PGg50q27grw048GDY9ayB4FPmh7D+FFTA=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -302,24 +398,36 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
|
||||
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
|
||||
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.24.2 h1:J/tulyYK6JwBldPViHJReihxxZ+22FHs0piGjQAvoUE=
|
||||
github.com/onsi/gomega v1.24.2/go.mod h1:gs3J10IS7Z7r7eXRoNJIrNqU4ToQukCJhFtKrWgHWnk=
|
||||
github.com/openai/openai-go/v2 v2.7.0 h1:/8MSFCXcasin7AyuWQ2au6FraXL71gzAs+VfbMv+J3k=
|
||||
github.com/openai/openai-go/v2 v2.7.0/go.mod h1:jrJs23apqJKKbT+pqtFgNKpRju/KP9zpUTZhz3GElQE=
|
||||
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/orcaman/concurrent-map v0.0.0-20210501183033-44dafcb38ecc h1:Ak86L+yDSOzKFa7WM5bf5itSOo1e3Xh8bm5YCMUXIjQ=
|
||||
github.com/orcaman/concurrent-map v0.0.0-20210501183033-44dafcb38ecc/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI=
|
||||
github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU=
|
||||
github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
|
||||
github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
|
||||
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
@@ -355,21 +463,37 @@ github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1
|
||||
github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
|
||||
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=
|
||||
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
@@ -394,10 +518,28 @@ github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6
|
||||
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
|
||||
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
|
||||
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
|
||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w=
|
||||
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
|
||||
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
|
||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
|
||||
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
|
||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
|
||||
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
@@ -415,21 +557,28 @@ go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs=
|
||||
go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4=
|
||||
go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA=
|
||||
go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
||||
@@ -437,8 +586,6 @@ golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
@@ -463,6 +610,7 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
@@ -476,10 +624,13 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
@@ -488,6 +639,7 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
@@ -507,6 +659,7 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211008194852-3b03d305991f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
@@ -519,8 +672,6 @@ golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
@@ -547,8 +698,11 @@ golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -556,12 +710,15 @@ golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -581,12 +738,17 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -596,8 +758,6 @@ golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
@@ -614,6 +774,7 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
@@ -626,14 +787,17 @@ golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
@@ -673,6 +837,7 @@ golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -701,6 +866,7 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180518175338-11a468237815/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
@@ -722,6 +888,7 @@ google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfG
|
||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||
@@ -730,10 +897,12 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc
|
||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d h1:DoPTO70H+bcDXcd39vOqb2viZxgqeBeSGtZ55yZU4/Q=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
|
||||
google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
@@ -746,8 +915,11 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
|
||||
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
|
||||
google.golang.org/grpc/examples v0.0.0-20220617181431-3e7b97febc7f h1:rqzndB2lIQGivcXdTuY3Y9NBvr70X+y77woofSRluec=
|
||||
google.golang.org/grpc/examples v0.0.0-20220617181431-3e7b97febc7f/go.mod h1:gxndsbNG1n4TZcHGgsYEfVGnTxqfEdfiDv6/DADXX9o=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
@@ -772,10 +944,15 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
||||
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
|
||||
gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
@@ -788,6 +965,7 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
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.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
@@ -6,7 +6,9 @@ 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"
|
||||
_ "github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/higress/higress-ops"
|
||||
_ "github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/rag"
|
||||
_ "github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/tool-search"
|
||||
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"
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
)
|
||||
|
||||
@@ -20,11 +21,9 @@ type HigressClient struct {
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func NewHigressClient(baseURL, username, password string) *HigressClient {
|
||||
func NewHigressClient(baseURL string) *HigressClient {
|
||||
client := &HigressClient{
|
||||
baseURL: baseURL,
|
||||
username: username,
|
||||
password: password,
|
||||
baseURL: baseURL,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
@@ -35,22 +34,28 @@ func NewHigressClient(baseURL, username, password string) *HigressClient {
|
||||
return client
|
||||
}
|
||||
|
||||
func (c *HigressClient) Get(path string) ([]byte, error) {
|
||||
return c.request("GET", path, nil)
|
||||
func (c *HigressClient) Get(ctx context.Context, path string) ([]byte, error) {
|
||||
return c.request(ctx, "GET", path, nil)
|
||||
}
|
||||
|
||||
func (c *HigressClient) Post(path string, data interface{}) ([]byte, error) {
|
||||
return c.request("POST", path, data)
|
||||
func (c *HigressClient) Post(ctx context.Context, path string, data interface{}) ([]byte, error) {
|
||||
return c.request(ctx, "POST", path, data)
|
||||
}
|
||||
|
||||
func (c *HigressClient) Put(path string, data interface{}) ([]byte, error) {
|
||||
return c.request("PUT", path, data)
|
||||
func (c *HigressClient) Put(ctx context.Context, path string, data interface{}) ([]byte, error) {
|
||||
return c.request(ctx, "PUT", path, data)
|
||||
}
|
||||
|
||||
func (c *HigressClient) Delete(path string) ([]byte, error) {
|
||||
return c.request("DELETE", path, nil)
|
||||
func (c *HigressClient) Delete(ctx context.Context, path string) ([]byte, error) {
|
||||
return c.request(ctx, "DELETE", path, nil)
|
||||
}
|
||||
func (c *HigressClient) request(method, path string, data interface{}) ([]byte, error) {
|
||||
|
||||
// DeleteWithBody performs a DELETE request with a request body
|
||||
func (c *HigressClient) DeleteWithBody(ctx context.Context, path string, data interface{}) ([]byte, error) {
|
||||
return c.request(ctx, "DELETE", path, data)
|
||||
}
|
||||
|
||||
func (c *HigressClient) request(ctx context.Context, method, path string, data interface{}) ([]byte, error) {
|
||||
url := c.baseURL + path
|
||||
|
||||
var body io.Reader
|
||||
@@ -65,15 +70,27 @@ func (c *HigressClient) request(method, path string, data interface{}) ([]byte,
|
||||
api.LogDebugf("Higress API %s %s", method, url)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
// Create context with timeout if not already set
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
reqCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, url, body)
|
||||
req, err := http.NewRequestWithContext(reqCtx, method, url, body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.SetBasicAuth(c.username, c.password)
|
||||
// Try to get Authorization header from context first (passthrough from MCP client)
|
||||
if authHeader, ok := common.GetAuthHeader(ctx); ok && authHeader != "" {
|
||||
req.Header.Set("Authorization", authHeader)
|
||||
api.LogDebugf("Higress API request: Using Authorization header from context for %s %s", method, path)
|
||||
} else {
|
||||
api.LogWarnf("Higress API request: No authentication credentials available for %s %s", method, path)
|
||||
return nil, fmt.Errorf("no authentication credentials available for %s %s", method, path)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Higress API MCP Server
|
||||
|
||||
Higress API MCP Server 提供了 MCP 工具来管理 Higress 路由、服务来源和插件等资源。
|
||||
Higress API MCP Server 提供了 MCP 工具来管理 Higress 路由、服务来源、AI路由、AI提供商、MCP服务器和插件等资源。
|
||||
|
||||
## 功能特性
|
||||
|
||||
@@ -9,14 +9,41 @@ Higress API MCP Server 提供了 MCP 工具来管理 Higress 路由、服务来
|
||||
- `get-route`: 获取路由
|
||||
- `add-route`: 添加路由
|
||||
- `update-route`: 更新路由
|
||||
- `delete-route`: 删除路由
|
||||
|
||||
### AI路由管理
|
||||
- `list-ai-routes`: 列出AI路由
|
||||
- `get-ai-route`: 获取AI路由
|
||||
- `add-ai-route`: 添加AI路由
|
||||
- `update-ai-route`: 更新AI路由
|
||||
- `delete-ai-route`: 删除AI路由
|
||||
|
||||
### 服务来源管理
|
||||
- `list-service-sources`: 列出服务来源
|
||||
- `get-service-source`: 获取服务来源
|
||||
- `add-service-source`: 添加服务来源
|
||||
- `update-service-source`: 更新服务来源
|
||||
- `delete-service-source`: 删除服务来源
|
||||
|
||||
### AI提供商管理
|
||||
- `list-ai-providers`: 列出LLM提供商
|
||||
- `get-ai-provider`: 获取LLM提供商
|
||||
- `add-ai-provider`: 添加LLM提供商
|
||||
- `update-ai-provider`: 更新LLM提供商
|
||||
- `delete-ai-provider`: 删除LLM提供商
|
||||
|
||||
### MCP服务器管理
|
||||
- `list-mcp-servers`: 列出MCP服务器
|
||||
- `get-mcp-server`: 获取MCP服务器详情
|
||||
- `add-or-update-mcp-server`: 添加或更新MCP服务器
|
||||
- `delete-mcp-server`: 删除MCP服务器
|
||||
- `list-mcp-server-consumers`: 列出MCP服务器允许的消费者
|
||||
- `add-mcp-server-consumers`: 添加MCP服务器允许的消费者
|
||||
- `delete-mcp-server-consumers`: 删除MCP服务器允许的消费者
|
||||
- `swagger-to-mcp-config`: 将Swagger内容转换为MCP配置
|
||||
|
||||
### 插件管理
|
||||
- `list-plugin-instances`: 列出特定作用域下的所有插件实例(支持全局、域名、服务、路由级别)
|
||||
- `get-plugin`: 获取插件配置
|
||||
- `delete-plugin`: 删除插件
|
||||
- `update-request-block-plugin`: 更新 request-block 插件配置
|
||||
@@ -26,8 +53,6 @@ Higress API MCP Server 提供了 MCP 工具来管理 Higress 路由、服务来
|
||||
| 参数 | 类型 | 必需 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `higressURL` | string | 必填 | Higress Console 的 URL 地址 |
|
||||
| `username` | string | 必填 | Higress Console 登录用户名 |
|
||||
| `password` | string | 必填 | Higress Console 登录密码 |
|
||||
| `description` | string | 可选 | 服务器描述信息 |
|
||||
|
||||
配置示例:
|
||||
@@ -56,7 +81,11 @@ data:
|
||||
redis:
|
||||
address: redis-stack-server.higress-system.svc.cluster.local:6379 # Redis服务地址
|
||||
username: "" # Redis用户名(可选)
|
||||
password: "" # Redis密码(可选)
|
||||
password: "" # Redis密码(可选,明文方式)
|
||||
passwordSecret: # 从 Secret 引用密码(推荐,优先级高于 password)
|
||||
name: redis-credentials # Secret 名称
|
||||
key: password # Secret 中的 key
|
||||
namespace: higress-system # Secret 所在命名空间(可选,默认为 higress-system)
|
||||
db: 0 # Redis数据库(可选)
|
||||
match_list: # MCP Server 会话保持路由规则(当匹配下面路径时,将被识别为一个 MCP 会话,通过 SSE 等机制进行会话保持)
|
||||
- match_rule_domain: "*"
|
||||
@@ -68,6 +97,52 @@ data:
|
||||
type: higress-api # 类型和 RegisterServer 一致
|
||||
config:
|
||||
higressURL: http://higress-console.higress-system.svc.cluster.local:8080
|
||||
username: admin
|
||||
password: admin
|
||||
```
|
||||
|
||||
## 鉴权配置
|
||||
|
||||
Higress API MCP Server 使用 HTTP Basic Authentication 进行鉴权。客户端需要在请求头中携带 `Authorization` 头。
|
||||
|
||||
### 配置示例
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"higress_api_mcp": {
|
||||
"url": "http://127.0.0.1:80/higress-api/sse",
|
||||
"headers": {
|
||||
"Authorization": "Basic YWRtaW46YWRtaW4="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**说明:**
|
||||
- `Authorization` 头使用 Basic Authentication 格式:`Basic base64(username:password)`
|
||||
- 示例中的 `YWRtaW46YWRtaW4=` 是 `admin:admin` 的 Base64 编码
|
||||
- 您需要根据实际的 Higress Console 用户名和密码生成相应的 Base64 编码
|
||||
|
||||
### 生成 Authorization 头
|
||||
|
||||
使用以下命令生成 Basic Auth 的 Authorization 头:
|
||||
|
||||
```bash
|
||||
echo -n "username:password" | base64
|
||||
```
|
||||
|
||||
将 `username` 和 `password` 替换为您的 Higress Console 实际凭证。
|
||||
|
||||
## 演示
|
||||
|
||||
1. create openapi-mcp-server
|
||||
|
||||
https://private-user-images.githubusercontent.com/153273766/507768507-42077ff3-731e-42fe-8b10-ccae0d1b3378.mov?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NjE4Nzg4NjAsIm5iZiI6MTc2MTg3ODU2MCwicGF0aCI6Ii8xNTMyNzM3NjYvNTA3NzY4NTA3LTQyMDc3ZmYzLTczMWUtNDJmZS04YjEwLWNjYWUwZDFiMzM3OC5tb3Y_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUxMDMxJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MTAzMVQwMjQyNDBaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT0xODVlY2QzYTBmODY0YzRlMzFjNWI1NGE3MGIyZDAxMGRmZjczNTNhMDZmNjdhMGYxMjM2NzVjMjEyYzdlNWFkJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.qzpx2W52Zl9WuWidgEMTYP1sMfrqcgsXtNbNvYK39wE
|
||||
|
||||
2. create ai-route
|
||||
|
||||
https://private-user-images.githubusercontent.com/153273766/507769175-96b6002f-389d-46e8-b696-c5bcf518a1c6.mov?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NjE4Nzg4NjAsIm5iZiI6MTc2MTg3ODU2MCwicGF0aCI6Ii8xNTMyNzM3NjYvNTA3NzY5MTc1LTk2YjYwMDJmLTM4OWQtNDZlOC1iNjk2LWM1YmNmNTE4YTFjNi5tb3Y_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUxMDMxJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MTAzMVQwMjQyNDBaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1mYTFiZjY0Zjg0NWVhYzA3NzhiODc2NzUwMDg3MDZiYjI4ZTQ4YWRkNmIwMzEyMWI5ZjE0MTQ3NTZlZmU5NTEwJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.XW6eJxjCpcblQCCtidYoNCwn2yUkXt3d9zuDYxDIF8Q
|
||||
|
||||
3. create http-bin + custom response
|
||||
|
||||
https://private-user-images.githubusercontent.com/153273766/507769227-73b624d5-70b8-4c94-aa87-42b3ff8b094d.mov?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NjE4Nzg4NjAsIm5iZiI6MTc2MTg3ODU2MCwicGF0aCI6Ii8xNTMyNzM3NjYvNTA3NzY5MjI3LTczYjYyNGQ1LTcwYjgtNGM5NC1hYTg3LTQyYjNmZjhiMDk0ZC5tb3Y_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUxMDMxJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MTAzMVQwMjQyNDBaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1jMjc1N2MyZTE2N2RlYjJkZThhZWMwZTc5YWM1ODI3ODgyYjM1Yzk3Mzk1ZjVlMDljZGM4NGJhM2MwZTE5N2E5JlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.R4h7AmTKadKxd6qr7m-i8JPsxoJHcrN49eVbB0ixYyU
|
||||
@@ -1,6 +1,6 @@
|
||||
# Higress API MCP Server
|
||||
|
||||
Higress API MCP Server provides MCP tools to manage Higress routes, service sources, plugins and other resources.
|
||||
Higress API MCP Server provides MCP tools to manage Higress routes, service sources, AI routes, AI providers, MCP servers, plugins and other resources.
|
||||
|
||||
## Features
|
||||
|
||||
@@ -9,14 +9,41 @@ Higress API MCP Server provides MCP tools to manage Higress routes, service sour
|
||||
- `get-route`: Get route
|
||||
- `add-route`: Add route
|
||||
- `update-route`: Update route
|
||||
- `delete-route`: Delete route
|
||||
|
||||
### AI Route Management
|
||||
- `list-ai-routes`: List AI routes
|
||||
- `get-ai-route`: Get AI route
|
||||
- `add-ai-route`: Add AI route
|
||||
- `update-ai-route`: Update AI route
|
||||
- `delete-ai-route`: Delete AI 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
|
||||
- `delete-service-source`: Delete service source
|
||||
|
||||
### AI Provider Management
|
||||
- `list-ai-providers`: List LLM providers
|
||||
- `get-ai-provider`: Get LLM provider
|
||||
- `add-ai-provider`: Add LLM provider
|
||||
- `update-ai-provider`: Update LLM provider
|
||||
- `delete-ai-provider`: Delete LLM provider
|
||||
|
||||
### MCP Server Management
|
||||
- `list-mcp-servers`: List MCP servers
|
||||
- `get-mcp-server`: Get MCP server details
|
||||
- `add-or-update-mcp-server`: Add or update MCP server
|
||||
- `delete-mcp-server`: Delete MCP server
|
||||
- `list-mcp-server-consumers`: List MCP server allowed consumers
|
||||
- `add-mcp-server-consumers`: Add MCP server allowed consumers
|
||||
- `delete-mcp-server-consumers`: Delete MCP server allowed consumers
|
||||
- `swagger-to-mcp-config`: Convert Swagger content to MCP configuration
|
||||
|
||||
### Plugin Management
|
||||
- `list-plugin-instances`: List all plugin instances for a specific scope (supports global, domain, service, and route levels)
|
||||
- `get-plugin`: Get plugin configuration
|
||||
- `delete-plugin`: Delete plugin
|
||||
- `update-request-block-plugin`: Update request block configuration
|
||||
@@ -26,8 +53,6 @@ Higress API MCP Server provides MCP tools to manage Higress routes, service sour
|
||||
| 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:
|
||||
@@ -56,7 +81,11 @@ data:
|
||||
redis:
|
||||
address: redis-stack-server.higress-system.svc.cluster.local:6379 # Redis service address
|
||||
username: "" # Redis username (optional)
|
||||
password: "" # Redis password (optional)
|
||||
password: "" # Redis password (optional, plaintext)
|
||||
passwordSecret: # Reference password from Secret (recommended, higher priority than password)
|
||||
name: redis-credentials # Secret name
|
||||
key: password # Key in Secret
|
||||
namespace: higress-system # Secret namespace (optional, defaults to higress-system)
|
||||
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: "*"
|
||||
@@ -68,6 +97,52 @@ data:
|
||||
type: higress-api # Type defined in RegisterServer function
|
||||
config:
|
||||
higressURL: http://higress-console.higress-system.svc.cluster.local:8080
|
||||
username: admin
|
||||
password: admin
|
||||
```
|
||||
|
||||
## Authentication Configuration
|
||||
|
||||
Higress API MCP Server uses HTTP Basic Authentication for authorization. Clients need to include an `Authorization` header in their requests.
|
||||
|
||||
### Configuration Example
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"higress_api_mcp": {
|
||||
"url": "http://127.0.0.1:80/higress-api/sse",
|
||||
"headers": {
|
||||
"Authorization": "Basic YWRtaW46YWRtaW4="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- The `Authorization` header uses Basic Authentication format: `Basic base64(username:password)`
|
||||
- The example `YWRtaW46YWRtaW4=` is the Base64 encoding of `admin:admin`
|
||||
- You need to generate the appropriate Base64 encoding based on your actual Higress Console username and password
|
||||
|
||||
### Generating Authorization Header
|
||||
|
||||
Use the following command to generate the Basic Auth Authorization header:
|
||||
|
||||
```bash
|
||||
echo -n "username:password" | base64
|
||||
```
|
||||
|
||||
Replace `username` and `password` with your actual Higress Console credentials.
|
||||
|
||||
## Demo
|
||||
|
||||
1. create openapi-mcp-server
|
||||
|
||||
https://private-user-images.githubusercontent.com/153273766/507768507-42077ff3-731e-42fe-8b10-ccae0d1b3378.mov?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NjE4Nzg4NjAsIm5iZiI6MTc2MTg3ODU2MCwicGF0aCI6Ii8xNTMyNzM3NjYvNTA3NzY4NTA3LTQyMDc3ZmYzLTczMWUtNDJmZS04YjEwLWNjYWUwZDFiMzM3OC5tb3Y_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUxMDMxJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MTAzMVQwMjQyNDBaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT0xODVlY2QzYTBmODY0YzRlMzFjNWI1NGE3MGIyZDAxMGRmZjczNTNhMDZmNjdhMGYxMjM2NzVjMjEyYzdlNWFkJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.qzpx2W52Zl9WuWidgEMTYP1sMfrqcgsXtNbNvYK39wE
|
||||
|
||||
2. create ai-route
|
||||
|
||||
https://private-user-images.githubusercontent.com/153273766/507769175-96b6002f-389d-46e8-b696-c5bcf518a1c6.mov?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NjE4Nzg4NjAsIm5iZiI6MTc2MTg3ODU2MCwicGF0aCI6Ii8xNTMyNzM3NjYvNTA3NzY5MTc1LTk2YjYwMDJmLTM4OWQtNDZlOC1iNjk2LWM1YmNmNTE4YTFjNi5tb3Y_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUxMDMxJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MTAzMVQwMjQyNDBaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1mYTFiZjY0Zjg0NWVhYzA3NzhiODc2NzUwMDg3MDZiYjI4ZTQ4YWRkNmIwMzEyMWI5ZjE0MTQ3NTZlZmU5NTEwJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.XW6eJxjCpcblQCCtidYoNCwn2yUkXt3d9zuDYxDIF8Q
|
||||
|
||||
3. create http-bin + custom response
|
||||
|
||||
https://private-user-images.githubusercontent.com/153273766/507769227-73b624d5-70b8-4c94-aa87-42b3ff8b094d.mov?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NjE4Nzg4NjAsIm5iZiI6MTc2MTg3ODU2MCwicGF0aCI6Ii8xNTMyNzM3NjYvNTA3NzY5MjI3LTczYjYyNGQ1LTcwYjgtNGM5NC1hYTg3LTQyYjNmZjhiMDk0ZC5tb3Y_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUxMDMxJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MTAzMVQwMjQyNDBaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1jMjc1N2MyZTE2N2RlYjJkZThhZWMwZTc5YWM1ODI3ODgyYjM1Yzk3Mzk1ZjVlMDljZGM4NGJhM2MwZTE5N2E5JlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.R4h7AmTKadKxd6qr7m-i8JPsxoJHcrN49eVbB0ixYyU
|
||||
|
||||
@@ -18,8 +18,6 @@ func init() {
|
||||
|
||||
type HigressConfig struct {
|
||||
higressURL string
|
||||
username string
|
||||
password string
|
||||
description string
|
||||
}
|
||||
|
||||
@@ -30,26 +28,14 @@ func (c *HigressConfig) ParseConfig(config map[string]interface{}) error {
|
||||
}
|
||||
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)
|
||||
api.LogInfof("Higress MCP Server configuration parsed successfully. URL: %s",
|
||||
c.higressURL)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -62,13 +48,17 @@ func (c *HigressConfig) NewServer(serverName string) (*common.MCPServer, error)
|
||||
)
|
||||
|
||||
// Initialize Higress API client
|
||||
client := higress.NewHigressClient(c.higressURL, c.username, c.password)
|
||||
client := higress.NewHigressClient(c.higressURL)
|
||||
|
||||
// Register all tools
|
||||
tools.RegisterRouteTools(mcpServer, client)
|
||||
tools.RegisterServiceTools(mcpServer, client)
|
||||
tools.RegisterAiRouteTools(mcpServer, client)
|
||||
tools.RegisterAiProviderTools(mcpServer, client)
|
||||
tools.RegisterMcpServerTools(mcpServer, client)
|
||||
plugins.RegisterCommonPluginTools(mcpServer, client)
|
||||
plugins.RegisterRequestBlockPluginTools(mcpServer, client)
|
||||
plugins.RegisterCustomResponsePluginTools(mcpServer, client)
|
||||
|
||||
api.LogInfof("Higress MCP Server initialized: %s", serverName)
|
||||
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
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"
|
||||
)
|
||||
|
||||
// LlmProvider represents an LLM provider configuration
|
||||
type LlmProvider struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Protocol string `json:"protocol"`
|
||||
Tokens []string `json:"tokens,omitempty"`
|
||||
TokenFailoverConfig *TokenFailoverConfig `json:"tokenFailoverConfig,omitempty"`
|
||||
RawConfigs map[string]interface{} `json:"rawConfigs,omitempty"`
|
||||
}
|
||||
|
||||
// TokenFailoverConfig represents token failover configuration
|
||||
type TokenFailoverConfig struct {
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
FailureThreshold int `json:"failureThreshold,omitempty"`
|
||||
SuccessThreshold int `json:"successThreshold,omitempty"`
|
||||
HealthCheckInterval int `json:"healthCheckInterval,omitempty"`
|
||||
HealthCheckTimeout int `json:"healthCheckTimeout,omitempty"`
|
||||
HealthCheckModel string `json:"healthCheckModel,omitempty"`
|
||||
}
|
||||
|
||||
// LlmProviderResponse represents the API response for LLM provider operations
|
||||
type LlmProviderResponse = higress.APIResponse[LlmProvider]
|
||||
|
||||
// RegisterAiProviderTools registers all AI provider management tools
|
||||
func RegisterAiProviderTools(mcpServer *common.MCPServer, client *higress.HigressClient) {
|
||||
// List all LLM providers
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("list-ai-providers", "List all available LLM providers", listAiProvidersSchema()),
|
||||
handleListAiProviders(client),
|
||||
)
|
||||
|
||||
// Get specific LLM provider
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("get-ai-provider", "Get detailed information about a specific LLM provider", getAiProviderSchema()),
|
||||
handleGetAiProvider(client),
|
||||
)
|
||||
|
||||
// Add new LLM provider
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("add-ai-provider", "Add a new LLM provider", getAddAiProviderSchema()),
|
||||
handleAddAiProvider(client),
|
||||
)
|
||||
|
||||
// Update existing LLM provider
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("update-ai-provider", "Update an existing LLM provider", getUpdateAiProviderSchema()),
|
||||
handleUpdateAiProvider(client),
|
||||
)
|
||||
|
||||
// Delete existing LLM provider
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("delete-ai-provider", "Delete an existing LLM provider", getAiProviderSchema()),
|
||||
handleDeleteAiProvider(client),
|
||||
)
|
||||
}
|
||||
|
||||
func handleListAiProviders(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
respBody, err := client.Get(ctx, "/v1/ai/providers")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list LLM providers: %w", err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetAiProvider(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(ctx, fmt.Sprintf("/v1/ai/providers/%s", name))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get LLM provider '%s': %w", name, err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleAddAiProvider(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["protocol"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'protocol' in configurations")
|
||||
}
|
||||
|
||||
respBody, err := client.Post(ctx, "/v1/ai/providers", configurations)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to add LLM provider: %w", err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleUpdateAiProvider(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 LLM provider configuration to merge with updates
|
||||
currentBody, err := client.Get(ctx, fmt.Sprintf("/v1/ai/providers/%s", name))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get current LLM provider configuration: %w", err)
|
||||
}
|
||||
|
||||
var response LlmProviderResponse
|
||||
if err := json.Unmarshal(currentBody, &response); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse current LLM provider 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 LlmProvider
|
||||
if err := json.Unmarshal(configBytes, &newConfig); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse LLM provider configurations: %w", err)
|
||||
}
|
||||
|
||||
// Merge configurations (overwrite with new values where provided)
|
||||
if newConfig.Type != "" {
|
||||
currentConfig.Type = newConfig.Type
|
||||
}
|
||||
if newConfig.Protocol != "" {
|
||||
currentConfig.Protocol = newConfig.Protocol
|
||||
}
|
||||
if newConfig.Tokens != nil {
|
||||
currentConfig.Tokens = newConfig.Tokens
|
||||
}
|
||||
if newConfig.TokenFailoverConfig != nil {
|
||||
currentConfig.TokenFailoverConfig = newConfig.TokenFailoverConfig
|
||||
}
|
||||
if newConfig.RawConfigs != nil {
|
||||
currentConfig.RawConfigs = newConfig.RawConfigs
|
||||
}
|
||||
|
||||
respBody, err := client.Put(ctx, fmt.Sprintf("/v1/ai/providers/%s", name), currentConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update LLM provider '%s': %w", name, err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleDeleteAiProvider(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(ctx, fmt.Sprintf("/v1/ai/providers/%s", name))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to delete LLM provider '%s': %w", name, err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func listAiProvidersSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": [],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
|
||||
func getAiProviderSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the LLM provider"
|
||||
}
|
||||
},
|
||||
"required": ["name"],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
|
||||
func getAddAiProviderSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"configurations": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Provider name"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["qwen", "openai", "moonshot", "azure", "ai360", "github", "groq", "baichuan", "yi", "deepseek", "zhipuai", "ollama", "claude", "baidu", "hunyuan", "stepfun", "minimax", "cloudflare", "spark", "gemini", "deepl", "mistral", "cohere", "doubao", "coze", "together-ai"],
|
||||
"description": "LLM Service Provider Type"
|
||||
},
|
||||
"protocol": {
|
||||
"type": "string",
|
||||
"enum": ["openai/v1", "original"],
|
||||
"description": "LLM Service Provider Protocol"
|
||||
},
|
||||
"tokens": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Tokens used to request the provider"
|
||||
},
|
||||
"tokenFailoverConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean", "description": "Whether token failover is enabled"},
|
||||
"failureThreshold": {"type": "integer", "description": "Failure threshold"},
|
||||
"successThreshold": {"type": "integer", "description": "Success threshold"},
|
||||
"healthCheckInterval": {"type": "integer", "description": "Health check interval"},
|
||||
"healthCheckTimeout": {"type": "integer", "description": "Health check timeout"},
|
||||
"healthCheckModel": {"type": "string", "description": "Health check model"}
|
||||
},
|
||||
"description": "Token Failover Config"
|
||||
},
|
||||
"rawConfigs": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"description": "Raw configuration key-value pairs used by ai-proxy plugin"
|
||||
}
|
||||
},
|
||||
"required": ["name", "type", "protocol"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["configurations"],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
|
||||
func getUpdateAiProviderSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the LLM provider"
|
||||
},
|
||||
"configurations": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["qwen", "openai", "moonshot", "azure", "ai360", "github", "groq", "baichuan", "yi", "deepseek", "zhipuai", "ollama", "claude", "baidu", "hunyuan", "stepfun", "minimax", "cloudflare", "spark", "gemini", "deepl", "mistral", "cohere", "doubao", "coze", "together-ai"],
|
||||
"description": "LLM Service Provider Type"
|
||||
},
|
||||
"protocol": {
|
||||
"type": "string",
|
||||
"enum": ["openai/v1", "original"],
|
||||
"description": "LLM Service Provider Protocol"
|
||||
},
|
||||
"tokens": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Tokens used to request the provider"
|
||||
},
|
||||
"tokenFailoverConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean", "description": "Whether token failover is enabled"},
|
||||
"failureThreshold": {"type": "integer", "description": "Failure threshold"},
|
||||
"successThreshold": {"type": "integer", "description": "Success threshold"},
|
||||
"healthCheckInterval": {"type": "integer", "description": "Health check interval"},
|
||||
"healthCheckTimeout": {"type": "integer", "description": "Health check timeout"},
|
||||
"healthCheckModel": {"type": "string", "description": "Health check model"}
|
||||
},
|
||||
"description": "Token Failover Config"
|
||||
},
|
||||
"rawConfigs": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"description": "Raw configuration key-value pairs used by ai-proxy plugin"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["name", "configurations"],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
@@ -0,0 +1,601 @@
|
||||
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"
|
||||
)
|
||||
|
||||
// AiRoute represents an AI route configuration
|
||||
type AiRoute struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Domains []string `json:"domains,omitempty"`
|
||||
PathPredicate *AiRoutePredicate `json:"pathPredicate,omitempty"`
|
||||
HeaderPredicates []AiKeyedRoutePredicate `json:"headerPredicates,omitempty"`
|
||||
URLParamPredicates []AiKeyedRoutePredicate `json:"urlParamPredicates,omitempty"`
|
||||
Upstreams []AiUpstream `json:"upstreams,omitempty"`
|
||||
ModelPredicates []AiModelPredicate `json:"modelPredicates,omitempty"`
|
||||
AuthConfig *RouteAuthConfig `json:"authConfig,omitempty"`
|
||||
FallbackConfig *AiRouteFallbackConfig `json:"fallbackConfig,omitempty"`
|
||||
}
|
||||
|
||||
// AiRoutePredicate represents an AI route predicate
|
||||
type AiRoutePredicate struct {
|
||||
MatchType string `json:"matchType"`
|
||||
MatchValue string `json:"matchValue"`
|
||||
CaseSensitive bool `json:"caseSensitive,omitempty"`
|
||||
}
|
||||
|
||||
// AiKeyedRoutePredicate represents an AI route predicate with a key
|
||||
type AiKeyedRoutePredicate struct {
|
||||
Key string `json:"key"`
|
||||
MatchType string `json:"matchType"`
|
||||
MatchValue string `json:"matchValue"`
|
||||
CaseSensitive bool `json:"caseSensitive,omitempty"`
|
||||
}
|
||||
|
||||
// AiUpstream represents an AI upstream configuration
|
||||
type AiUpstream struct {
|
||||
Provider string `json:"provider"`
|
||||
Weight int `json:"weight"`
|
||||
ModelMapping map[string]string `json:"modelMapping,omitempty"`
|
||||
}
|
||||
|
||||
// AiModelPredicate represents an AI model predicate
|
||||
type AiModelPredicate struct {
|
||||
MatchType string `json:"matchType"`
|
||||
MatchValue string `json:"matchValue"`
|
||||
CaseSensitive bool `json:"caseSensitive,omitempty"`
|
||||
}
|
||||
|
||||
// AiRouteFallbackConfig represents AI route fallback configuration
|
||||
type AiRouteFallbackConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Upstreams []AiUpstream `json:"upstreams,omitempty"`
|
||||
FallbackStrategy string `json:"fallbackStrategy,omitempty"`
|
||||
ResponseCodes []string `json:"responseCodes,omitempty"`
|
||||
}
|
||||
|
||||
// AiRouteResponse represents the API response for AI route operations
|
||||
type AiRouteResponse = higress.APIResponse[AiRoute]
|
||||
|
||||
// RegisterAiRouteTools registers all AI route management tools
|
||||
func RegisterAiRouteTools(mcpServer *common.MCPServer, client *higress.HigressClient) {
|
||||
// List all AI routes
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("list-ai-routes", "List all available AI routes", listAiRoutesSchema()),
|
||||
handleListAiRoutes(client),
|
||||
)
|
||||
|
||||
// Get specific AI route
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("get-ai-route", "Get detailed information about a specific AI route", getAiRouteSchema()),
|
||||
handleGetAiRoute(client),
|
||||
)
|
||||
|
||||
// Add new AI route
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("add-ai-route", "Add a new AI route", getAddAiRouteSchema()),
|
||||
handleAddAiRoute(client),
|
||||
)
|
||||
|
||||
// Update existing AI route
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("update-ai-route", "Update an existing AI route", getUpdateAiRouteSchema()),
|
||||
handleUpdateAiRoute(client),
|
||||
)
|
||||
|
||||
// Delete existing AI route
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("delete-ai-route", "Delete an existing AI route", getAiRouteSchema()),
|
||||
handleDeleteAiRoute(client),
|
||||
)
|
||||
}
|
||||
|
||||
func handleListAiRoutes(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
respBody, err := client.Get(ctx, "/v1/ai/routes")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list AI routes: %w", err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetAiRoute(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(ctx, fmt.Sprintf("/v1/ai/routes/%s", name))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get AI route '%s': %w", name, err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleAddAiRoute(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["upstreams"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'upstreams' in configurations")
|
||||
}
|
||||
|
||||
// Validate AI providers exist in upstreams
|
||||
if upstreams, ok := configurations["upstreams"].([]interface{}); ok && len(upstreams) > 0 {
|
||||
for _, upstream := range upstreams {
|
||||
if upstreamMap, ok := upstream.(map[string]interface{}); ok {
|
||||
if providerName, ok := upstreamMap["provider"].(string); ok {
|
||||
// Check if AI provider exists
|
||||
_, err := client.Get(ctx, fmt.Sprintf("/v1/ai/providers/%s", providerName))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Please create the AI provider '%s' first and then create the AI route", providerName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate AI providers exist in fallback upstreams
|
||||
if fallbackConfig, ok := configurations["fallbackConfig"].(map[string]interface{}); ok {
|
||||
if fallbackUpstreams, ok := fallbackConfig["upstreams"].([]interface{}); ok && len(fallbackUpstreams) > 0 {
|
||||
for _, upstream := range fallbackUpstreams {
|
||||
if upstreamMap, ok := upstream.(map[string]interface{}); ok {
|
||||
if providerName, ok := upstreamMap["provider"].(string); ok {
|
||||
// Check if AI provider exists
|
||||
_, err := client.Get(ctx, fmt.Sprintf("/v1/ai/providers/%s", providerName))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Please create the AI provider '%s' first and then create the AI route", providerName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
respBody, err := client.Post(ctx, "/v1/ai/routes", configurations)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to add AI route: %w", err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleUpdateAiRoute(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 AI route configuration to merge with updates
|
||||
currentBody, err := client.Get(ctx, fmt.Sprintf("/v1/ai/routes/%s", name))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get current AI route configuration: %w", err)
|
||||
}
|
||||
|
||||
var response AiRouteResponse
|
||||
if err := json.Unmarshal(currentBody, &response); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse current AI 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 AiRoute
|
||||
if err := json.Unmarshal(configBytes, &newConfig); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse AI route configurations: %w", err)
|
||||
}
|
||||
|
||||
// Merge configurations (overwrite with new values where provided)
|
||||
if newConfig.Domains != nil {
|
||||
currentConfig.Domains = newConfig.Domains
|
||||
}
|
||||
if newConfig.PathPredicate != nil {
|
||||
currentConfig.PathPredicate = newConfig.PathPredicate
|
||||
}
|
||||
if newConfig.HeaderPredicates != nil {
|
||||
currentConfig.HeaderPredicates = newConfig.HeaderPredicates
|
||||
}
|
||||
if newConfig.URLParamPredicates != nil {
|
||||
currentConfig.URLParamPredicates = newConfig.URLParamPredicates
|
||||
}
|
||||
if newConfig.Upstreams != nil {
|
||||
currentConfig.Upstreams = newConfig.Upstreams
|
||||
}
|
||||
if newConfig.ModelPredicates != nil {
|
||||
currentConfig.ModelPredicates = newConfig.ModelPredicates
|
||||
}
|
||||
if newConfig.AuthConfig != nil {
|
||||
currentConfig.AuthConfig = newConfig.AuthConfig
|
||||
}
|
||||
if newConfig.FallbackConfig != nil {
|
||||
currentConfig.FallbackConfig = newConfig.FallbackConfig
|
||||
}
|
||||
|
||||
respBody, err := client.Put(ctx, fmt.Sprintf("/v1/ai/routes/%s", name), currentConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update AI route '%s': %w", name, err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleDeleteAiRoute(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(ctx, fmt.Sprintf("/v1/ai/routes/%s", name))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to delete AI route '%s': %w", name, err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func listAiRoutesSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": [],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
|
||||
func getAiRouteSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the AI route"
|
||||
}
|
||||
},
|
||||
"required": ["name"],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
|
||||
func getAddAiRouteSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"configurations": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "AI route name"
|
||||
},
|
||||
"domains": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Domains that the route applies to. If empty, the route applies to all domains."
|
||||
},
|
||||
"pathPredicate": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"matchType": {"type": "string", "enum": ["PRE"], "description": "Match type"},
|
||||
"matchValue": {"type": "string", "description": "The value to match against"},
|
||||
"caseSensitive": {"type": "boolean", "description": "Whether to match the value case-sensitively"}
|
||||
},
|
||||
"required": ["matchType", "matchValue"],
|
||||
"description": "Path predicate"
|
||||
},
|
||||
"headerPredicates": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {"type": "string", "description": "Header key"},
|
||||
"matchType": {"type": "string", "enum": ["EQUAL", "PRE", "REGULAR"], "description": "Match type"},
|
||||
"matchValue": {"type": "string", "description": "The value to match against"},
|
||||
"caseSensitive": {"type": "boolean", "description": "Whether to match the value case-sensitively"}
|
||||
},
|
||||
"required": ["key", "matchType", "matchValue"]
|
||||
},
|
||||
"description": "Header predicates"
|
||||
},
|
||||
"urlParamPredicates": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {"type": "string", "description": "URL parameter key"},
|
||||
"matchType": {"type": "string", "enum": ["EQUAL", "PRE", "REGULAR"], "description": "Match type"},
|
||||
"matchValue": {"type": "string", "description": "The value to match against"},
|
||||
"caseSensitive": {"type": "boolean", "description": "Whether to match the value case-sensitively"}
|
||||
},
|
||||
"required": ["key", "matchType", "matchValue"]
|
||||
},
|
||||
"description": "URL parameter predicates"
|
||||
},
|
||||
"upstreams": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {"type": "string", "description": "LLM provider name"},
|
||||
"weight": {"type": "integer", "description": "Weight of the upstream,The sum of upstream weights must be 100"},
|
||||
"modelMapping": {
|
||||
"type": "object",
|
||||
"additionalProperties": {"type": "string"},
|
||||
"description": "Model mapping"
|
||||
}
|
||||
},
|
||||
"required": ["provider", "weight"]
|
||||
},
|
||||
"description": "Route upstreams"
|
||||
},
|
||||
"modelPredicates": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"matchType": {"type": "string", "enum": ["EQUAL", "PRE"], "description": "Match type"},
|
||||
"matchValue": {"type": "string", "description": "The value to match against"},
|
||||
"caseSensitive": {"type": "boolean", "description": "Whether to match the value case-sensitively"}
|
||||
},
|
||||
"required": ["matchType", "matchValue"]
|
||||
},
|
||||
"description": "Model predicates"
|
||||
},
|
||||
"authConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean", "description": "Whether auth is enabled"},
|
||||
"allowedConsumers": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Allowed consumer names"
|
||||
}
|
||||
},
|
||||
"description": "Route auth configuration"
|
||||
},
|
||||
"fallbackConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean", "description": "Whether fallback is enabled"},
|
||||
"upstreams": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {"type": "string", "description": "LLM provider name"},
|
||||
"weight": {"type": "integer", "description": "Weight of the upstream"},
|
||||
"modelMapping": {
|
||||
"type": "object",
|
||||
"additionalProperties": {"type": "string"},
|
||||
"description": "Model mapping"
|
||||
}
|
||||
},
|
||||
"required": ["provider", "weight"]
|
||||
},
|
||||
"description": "Fallback upstreams. Only one upstream is allowed when fallbackStrategy is SEQ."
|
||||
},
|
||||
"fallbackStrategy": {"type": "string", "enum": ["RAND", "SEQ"], "description": "Fallback strategy"},
|
||||
"responseCodes": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Response codes that need fallback"
|
||||
}
|
||||
},
|
||||
"description": "AI Route fallback configuration"
|
||||
}
|
||||
},
|
||||
"required": ["name", "upstreams"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["configurations"],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
|
||||
func getUpdateAiRouteSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the AI route"
|
||||
},
|
||||
"configurations": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domains": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Domains that the route applies to. If empty, the route applies to all domains."
|
||||
},
|
||||
"pathPredicate": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"matchType": {"type": "string", "enum": ["EQUAL", "PRE", "REGULAR"], "description": "Match type"},
|
||||
"matchValue": {"type": "string", "description": "The value to match against"},
|
||||
"caseSensitive": {"type": "boolean", "description": "Whether to match the value case-sensitively"}
|
||||
},
|
||||
"required": ["matchType", "matchValue"],
|
||||
"description": "Path predicate"
|
||||
},
|
||||
"headerPredicates": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {"type": "string", "description": "Header key"},
|
||||
"matchType": {"type": "string", "enum": ["EQUAL", "PRE", "REGULAR"], "description": "Match type"},
|
||||
"matchValue": {"type": "string", "description": "The value to match against"},
|
||||
"caseSensitive": {"type": "boolean", "description": "Whether to match the value case-sensitively"}
|
||||
},
|
||||
"required": ["key", "matchType", "matchValue"]
|
||||
},
|
||||
"description": "Header predicates"
|
||||
},
|
||||
"urlParamPredicates": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {"type": "string", "description": "URL parameter key"},
|
||||
"matchType": {"type": "string", "enum": ["EQUAL", "PRE", "REGULAR"], "description": "Match type"},
|
||||
"matchValue": {"type": "string", "description": "The value to match against"},
|
||||
"caseSensitive": {"type": "boolean", "description": "Whether to match the value case-sensitively"}
|
||||
},
|
||||
"required": ["key", "matchType", "matchValue"]
|
||||
},
|
||||
"description": "URL parameter predicates"
|
||||
},
|
||||
"upstreams": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {"type": "string", "description": "LLM provider name"},
|
||||
"weight": {"type": "integer", "description": "Weight of the upstream"},
|
||||
"modelMapping": {
|
||||
"type": "object",
|
||||
"additionalProperties": {"type": "string"},
|
||||
"description": "Model mapping"
|
||||
}
|
||||
},
|
||||
"required": ["provider", "weight"]
|
||||
},
|
||||
"description": "Route upstreams"
|
||||
},
|
||||
"modelPredicates": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"matchType": {"type": "string", "enum": ["EQUAL", "PRE", "REGULAR"], "description": "Match type"},
|
||||
"matchValue": {"type": "string", "description": "The value to match against"},
|
||||
"caseSensitive": {"type": "boolean", "description": "Whether to match the value case-sensitively"}
|
||||
},
|
||||
"required": ["matchType", "matchValue"]
|
||||
},
|
||||
"description": "Model predicates"
|
||||
},
|
||||
"authConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean", "description": "Whether auth is enabled"},
|
||||
"allowedConsumers": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Allowed consumer names"
|
||||
}
|
||||
},
|
||||
"description": "Route auth configuration"
|
||||
},
|
||||
"fallbackConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean", "description": "Whether fallback is enabled"},
|
||||
"upstreams": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {"type": "string", "description": "LLM provider name"},
|
||||
"weight": {"type": "integer", "description": "Weight of the upstream"},
|
||||
"modelMapping": {
|
||||
"type": "object",
|
||||
"additionalProperties": {"type": "string"},
|
||||
"description": "Model mapping"
|
||||
}
|
||||
},
|
||||
"required": ["provider", "weight"]
|
||||
},
|
||||
"description": "Fallback upstreams. Only one upstream is allowed when fallbackStrategy is SEQ."
|
||||
},
|
||||
"fallbackStrategy": {"type": "string", "enum": ["RAND", "SEQ"], "description": "Fallback strategy"},
|
||||
"responseCodes": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Response codes that need fallback"
|
||||
}
|
||||
},
|
||||
"description": "AI Route fallback configuration"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["name", "configurations"],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
@@ -0,0 +1,610 @@
|
||||
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"
|
||||
)
|
||||
|
||||
// McpServer represents an MCP server configuration
|
||||
type McpServer struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Domains []string `json:"domains,omitempty"`
|
||||
Services []McpUpstreamService `json:"services,omitempty"`
|
||||
Type string `json:"type"`
|
||||
ConsumerAuthInfo *ConsumerAuthInfo `json:"consumerAuthInfo,omitempty"`
|
||||
RawConfigurations string `json:"rawConfigurations,omitempty"`
|
||||
DSN string `json:"dsn,omitempty"`
|
||||
DBType string `json:"dbType,omitempty"`
|
||||
UpstreamPathPrefix string `json:"upstreamPathPrefix,omitempty"`
|
||||
McpServerName string `json:"mcpServerName,omitempty"`
|
||||
}
|
||||
|
||||
// McpUpstreamService represents a service in MCP server
|
||||
type McpUpstreamService struct {
|
||||
Name string `json:"name"`
|
||||
Port int `json:"port"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Weight int `json:"weight"`
|
||||
}
|
||||
|
||||
// ConsumerAuthInfo represents consumer authentication information
|
||||
type ConsumerAuthInfo struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Enable bool `json:"enable,omitempty"`
|
||||
AllowedConsumers []string `json:"allowedConsumers,omitempty"`
|
||||
}
|
||||
|
||||
// McpServerConsumers represents MCP server consumers configuration
|
||||
type McpServerConsumers struct {
|
||||
McpServerName string `json:"mcpServerName"`
|
||||
Consumers []string `json:"consumers"`
|
||||
}
|
||||
|
||||
// McpServerConsumerDetail represents detailed consumer information
|
||||
type McpServerConsumerDetail struct {
|
||||
McpServerName string `json:"mcpServerName"`
|
||||
ConsumerName string `json:"consumerName"`
|
||||
Type string `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
// SwaggerContent represents swagger content for conversion
|
||||
type SwaggerContent struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// McpServerResponse represents the API response for MCP server operations
|
||||
type McpServerResponse = higress.APIResponse[McpServer]
|
||||
|
||||
// McpServerConsumerDetailResponse represents the API response for MCP server consumer operations
|
||||
type McpServerConsumerDetailResponse = higress.APIResponse[[]McpServerConsumerDetail]
|
||||
|
||||
// RegisterMcpServerTools registers all MCP server management tools
|
||||
func RegisterMcpServerTools(mcpServer *common.MCPServer, client *higress.HigressClient) {
|
||||
// List MCP servers
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("list-mcp-servers", "List all MCP servers", listMcpServersSchema()),
|
||||
handleListMcpServers(client),
|
||||
)
|
||||
|
||||
// Get specific MCP server
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("get-mcp-server", "Get detailed information about a specific MCP server", getMcpServerSchema()),
|
||||
handleGetMcpServer(client),
|
||||
)
|
||||
|
||||
// Add or update MCP server
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("add-or-update-mcp-server", "Add or update an MCP server instance", getAddOrUpdateMcpServerSchema()),
|
||||
handleAddOrUpdateMcpServer(client),
|
||||
)
|
||||
|
||||
// Delete MCP server
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("delete-mcp-server", "Delete an MCP server", getMcpServerSchema()),
|
||||
handleDeleteMcpServer(client),
|
||||
)
|
||||
|
||||
// List MCP server consumers
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("list-mcp-server-consumers", "List MCP server allowed consumers", listMcpServerConsumersSchema()),
|
||||
handleListMcpServerConsumers(client),
|
||||
)
|
||||
|
||||
// Add MCP server consumers
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("add-mcp-server-consumers", "Add MCP server allowed consumers", getMcpServerConsumersSchema()),
|
||||
handleAddMcpServerConsumers(client),
|
||||
)
|
||||
|
||||
// Delete MCP server consumers
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("delete-mcp-server-consumers", "Delete MCP server allowed consumers", getMcpServerConsumersSchema()),
|
||||
handleDeleteMcpServerConsumers(client),
|
||||
)
|
||||
|
||||
// Convert Swagger to MCP config
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("swagger-to-mcp-config", "Convert Swagger content to MCP configuration", getSwaggerToMcpConfigSchema()),
|
||||
handleSwaggerToMcpConfig(client),
|
||||
)
|
||||
}
|
||||
|
||||
func handleListMcpServers(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
|
||||
// Build query parameters
|
||||
queryParams := ""
|
||||
if mcpServerName, ok := arguments["mcpServerName"].(string); ok && mcpServerName != "" {
|
||||
queryParams += "?mcpServerName=" + mcpServerName
|
||||
}
|
||||
if mcpType, ok := arguments["type"].(string); ok && mcpType != "" {
|
||||
if queryParams == "" {
|
||||
queryParams += "?type=" + mcpType
|
||||
} else {
|
||||
queryParams += "&type=" + mcpType
|
||||
}
|
||||
}
|
||||
if pageNum, ok := arguments["pageNum"].(string); ok && pageNum != "" {
|
||||
if queryParams == "" {
|
||||
queryParams += "?pageNum=" + pageNum
|
||||
} else {
|
||||
queryParams += "&pageNum=" + pageNum
|
||||
}
|
||||
}
|
||||
if pageSize, ok := arguments["pageSize"].(string); ok && pageSize != "" {
|
||||
if queryParams == "" {
|
||||
queryParams += "?pageSize=" + pageSize
|
||||
} else {
|
||||
queryParams += "&pageSize=" + pageSize
|
||||
}
|
||||
}
|
||||
|
||||
respBody, err := client.Get(ctx, "/v1/mcpServer"+queryParams)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list MCP servers: %w", err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetMcpServer(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(ctx, fmt.Sprintf("/v1/mcpServer/%s", name))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get MCP server '%s': %w", name, err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleAddOrUpdateMcpServer(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")
|
||||
}
|
||||
|
||||
// Validate service sources exist
|
||||
if services, ok := configurations["services"].([]interface{}); ok && len(services) > 0 {
|
||||
for _, svc := range services {
|
||||
if serviceMap, ok := svc.(map[string]interface{}); ok {
|
||||
if serviceName, ok := serviceMap["name"].(string); ok {
|
||||
// Extract service source name from "serviceName.serviceType" format
|
||||
var serviceSourceName string
|
||||
for i := len(serviceName) - 1; i >= 0; i-- {
|
||||
if serviceName[i] == '.' {
|
||||
serviceSourceName = serviceName[:i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if serviceSourceName == "" {
|
||||
return nil, fmt.Errorf("invalid service name format '%s', expected 'serviceName.serviceType'", serviceName)
|
||||
}
|
||||
|
||||
// Check if service source exists
|
||||
_, err := client.Get(ctx, fmt.Sprintf("/v1/service-sources/%s", serviceSourceName))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Please create the service source '%s' first and then create the mcpserver", serviceSourceName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
respBody, err := client.Put(ctx, "/v1/mcpServer", configurations)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to add or update MCP server: %w", err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleDeleteMcpServer(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(ctx, fmt.Sprintf("/v1/mcpServer/%s", name))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to delete MCP server '%s': %w", name, err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleListMcpServerConsumers(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
|
||||
// Build query parameters
|
||||
queryParams := ""
|
||||
if mcpServerName, ok := arguments["mcpServerName"].(string); ok && mcpServerName != "" {
|
||||
queryParams += "?mcpServerName=" + mcpServerName
|
||||
}
|
||||
if consumerName, ok := arguments["consumerName"].(string); ok && consumerName != "" {
|
||||
if queryParams == "" {
|
||||
queryParams += "?consumerName=" + consumerName
|
||||
} else {
|
||||
queryParams += "&consumerName=" + consumerName
|
||||
}
|
||||
}
|
||||
if pageNum, ok := arguments["pageNum"].(string); ok && pageNum != "" {
|
||||
if queryParams == "" {
|
||||
queryParams += "?pageNum=" + pageNum
|
||||
} else {
|
||||
queryParams += "&pageNum=" + pageNum
|
||||
}
|
||||
}
|
||||
if pageSize, ok := arguments["pageSize"].(string); ok && pageSize != "" {
|
||||
if queryParams == "" {
|
||||
queryParams += "?pageSize=" + pageSize
|
||||
} else {
|
||||
queryParams += "&pageSize=" + pageSize
|
||||
}
|
||||
}
|
||||
|
||||
respBody, err := client.Get(ctx, "/v1/mcpServer/consumers"+queryParams)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list MCP server consumers: %w", err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleAddMcpServerConsumers(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["mcpServerName"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'mcpServerName' in configurations")
|
||||
}
|
||||
if _, ok := configurations["consumers"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'consumers' in configurations")
|
||||
}
|
||||
|
||||
respBody, err := client.Put(ctx, "/v1/mcpServer/consumers", configurations)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to add MCP server consumers: %w", err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleDeleteMcpServerConsumers(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["mcpServerName"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'mcpServerName' in configurations")
|
||||
}
|
||||
if _, ok := configurations["consumers"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'consumers' in configurations")
|
||||
}
|
||||
|
||||
respBody, err := client.DeleteWithBody(ctx, "/v1/mcpServer/consumers", configurations)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to delete MCP server consumers: %w", err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleSwaggerToMcpConfig(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["content"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'content' in configurations")
|
||||
}
|
||||
|
||||
respBody, err := client.Post(ctx, "/v1/mcpServer/swaggerToMcpConfig", configurations)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert swagger to MCP config: %w", err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Schema definitions
|
||||
|
||||
func listMcpServersSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mcpServerName": {
|
||||
"type": "string",
|
||||
"description": "McpServer name associated with route"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "Mcp server type"
|
||||
},
|
||||
"pageNum": {
|
||||
"type": "string",
|
||||
"description": "Page number, starting from 1. If omitted, all items will be returned"
|
||||
},
|
||||
"pageSize": {
|
||||
"type": "string",
|
||||
"description": "Number of items per page. If omitted, all items will be returned"
|
||||
}
|
||||
},
|
||||
"required": [],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
|
||||
func getMcpServerSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the MCP server"
|
||||
}
|
||||
},
|
||||
"required": ["name"],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
|
||||
func getAddOrUpdateMcpServerSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"configurations": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Mcp server name"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Mcp server description"
|
||||
},
|
||||
"domains": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Domains that the mcp server applies to"
|
||||
},
|
||||
"services": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string", "description": "must be service name + service type, such as:daxt-mcp.static .which must be real exist service"},
|
||||
"port": {"type": "integer", "description": "Service port"},
|
||||
"version": {"type": "string", "description": "Service version"},
|
||||
"weight": {"type": "integer", "description": "Service weight"}
|
||||
},
|
||||
"required": ["name", "port", "weight"]
|
||||
},
|
||||
"description": "Mcp server upstream services"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["OPEN_API", "DATABASE", "DIRECT_ROUTE"],
|
||||
"description": "Mcp Server Type"
|
||||
},
|
||||
"consumerAuthInfo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {"type": "string", "description": "Consumer auth type,if not enable, it value must be API_KEY "},
|
||||
"enable": {"type": "boolean", "description": "Whether consumer auth is enabled"},
|
||||
"allowedConsumers": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Allowed consumer names"
|
||||
}
|
||||
},
|
||||
"description": "Mcp server consumer auth info"
|
||||
},
|
||||
"rawConfigurations": {
|
||||
"type": "string",
|
||||
"description": "Raw configurations in YAML format"
|
||||
},
|
||||
"dsn": {
|
||||
"type": "string",
|
||||
"description": "Data Source Name. For DB type server, it is required such as username:passwd@tcp(ip:port)/Database?charset=utf8mb4&parseTime=True&loc=Local .For other, it can be empty."
|
||||
},
|
||||
"dbType": {
|
||||
"type": "string",
|
||||
"enum": ["MYSQL", "POSTGRESQL", "SQLITE", "CLICKHOUSE"],
|
||||
"description": "Mcp Server DB Type,only if type is DATABASE, it is required"
|
||||
},
|
||||
"upstreamPathPrefix": {
|
||||
"type": "string",
|
||||
"description": "The upstream MCP server will redirect requests based on the path prefix"
|
||||
},
|
||||
"mcpServerName": {
|
||||
"type": "string",
|
||||
"description": "Mcp server name (usually same as 'name' field)"
|
||||
}
|
||||
},
|
||||
"required": ["name", "type", "dsn", "services"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["configurations"],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
|
||||
func listMcpServerConsumersSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mcpServerName": {
|
||||
"type": "string",
|
||||
"description": "McpServer name associated with route"
|
||||
},
|
||||
"consumerName": {
|
||||
"type": "string",
|
||||
"description": "Consumer name for search"
|
||||
},
|
||||
"pageNum": {
|
||||
"type": "string",
|
||||
"description": "Page number, starting from 1. If omitted, all items will be returned"
|
||||
},
|
||||
"pageSize": {
|
||||
"type": "string",
|
||||
"description": "Number of items per page. If omitted, all items will be returned"
|
||||
}
|
||||
},
|
||||
"required": [],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
|
||||
func getMcpServerConsumersSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"configurations": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mcpServerName": {
|
||||
"type": "string",
|
||||
"description": "Mcp server route name"
|
||||
},
|
||||
"consumers": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Consumer names"
|
||||
}
|
||||
},
|
||||
"required": ["mcpServerName", "consumers"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["configurations"],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
|
||||
func getSwaggerToMcpConfigSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"configurations": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "Swagger content"
|
||||
}
|
||||
},
|
||||
"required": ["content"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["configurations"],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
@@ -12,6 +12,12 @@ import (
|
||||
|
||||
// RegisterCommonPluginTools registers all common plugin management tools
|
||||
func RegisterCommonPluginTools(mcpServer *common.MCPServer, client *higress.HigressClient) {
|
||||
// List plugin instances for a specific scope
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("list-plugin-instances", "List all plugin instances for a specific scope (e.g., a route, domain, or service)", getListPluginInstancesSchema()),
|
||||
handleListPluginInstances(client),
|
||||
)
|
||||
|
||||
// Get plugin configuration
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("get-plugin", "Get configuration for a specific plugin", getPluginConfigSchema()),
|
||||
@@ -25,6 +31,61 @@ func RegisterCommonPluginTools(mcpServer *common.MCPServer, client *higress.Higr
|
||||
)
|
||||
}
|
||||
|
||||
func handleListPluginInstances(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)
|
||||
}
|
||||
|
||||
// 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
|
||||
// The API endpoint for listing all plugin instances at a specific scope
|
||||
var path string
|
||||
switch scope {
|
||||
case ScopeGlobal:
|
||||
path = "/v1/global/plugin-instances"
|
||||
case ScopeDomain:
|
||||
path = fmt.Sprintf("/v1/domains/%s/plugin-instances", resourceName)
|
||||
case ScopeService:
|
||||
path = fmt.Sprintf("/v1/services/%s/plugin-instances", resourceName)
|
||||
case ScopeRoute:
|
||||
path = fmt.Sprintf("/v1/routes/%s/plugin-instances", resourceName)
|
||||
default:
|
||||
path = "/v1/global/plugin-instances"
|
||||
}
|
||||
|
||||
respBody, err := client.Get(ctx, path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list plugin instances at scope '%s': %w", scope, err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetPluginConfig(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
@@ -55,7 +116,7 @@ func handleGetPluginConfig(client *higress.HigressClient) common.ToolHandlerFunc
|
||||
|
||||
// Build API path and make request
|
||||
path := BuildPluginPath(pluginName, scope, resourceName)
|
||||
respBody, err := client.Get(path)
|
||||
respBody, err := client.Get(ctx, path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get plugin config for '%s' at scope '%s': %w", pluginName, scope, err)
|
||||
}
|
||||
@@ -101,7 +162,7 @@ func handleDeletePluginConfig(client *higress.HigressClient) common.ToolHandlerF
|
||||
|
||||
// Build API path and make request
|
||||
path := BuildPluginPath(pluginName, scope, resourceName)
|
||||
respBody, err := client.Delete(path)
|
||||
respBody, err := client.Delete(ctx, path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to delete plugin config for '%s' at scope '%s': %w", pluginName, scope, err)
|
||||
}
|
||||
@@ -117,6 +178,25 @@ func handleDeletePluginConfig(client *higress.HigressClient) common.ToolHandlerF
|
||||
}
|
||||
}
|
||||
|
||||
func getListPluginInstancesSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"scope": {
|
||||
"type": "string",
|
||||
"enum": ["GLOBAL", "DOMAIN", "SERVICE", "ROUTE"],
|
||||
"description": "The scope at which to list plugin instances"
|
||||
},
|
||||
"resource_name": {
|
||||
"type": "string",
|
||||
"description": "The name of the resource (required for DOMAIN, SERVICE, ROUTE scopes). For example, the route name, domain name, or service name"
|
||||
}
|
||||
},
|
||||
"required": ["scope"],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
|
||||
func getPluginConfigSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
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 CustomResponsePluginName = "custom-response"
|
||||
|
||||
// CustomResponseConfig represents the configuration for custom-response plugin
|
||||
type CustomResponseConfig struct {
|
||||
Body string `json:"body,omitempty"`
|
||||
Headers []string `json:"headers,omitempty"`
|
||||
StatusCode int `json:"status_code,omitempty"`
|
||||
EnableOnStatus []int `json:"enable_on_status,omitempty"`
|
||||
}
|
||||
|
||||
// CustomResponseInstance represents a custom-response plugin instance
|
||||
type CustomResponseInstance = PluginInstance[CustomResponseConfig]
|
||||
|
||||
// CustomResponseResponse represents the API response for custom-response plugin
|
||||
type CustomResponseResponse = higress.APIResponse[CustomResponseInstance]
|
||||
|
||||
// RegisterCustomResponsePluginTools registers all custom response plugin management tools
|
||||
func RegisterCustomResponsePluginTools(mcpServer *common.MCPServer, client *higress.HigressClient) {
|
||||
// Update custom response configuration
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema(fmt.Sprintf("update-%s-plugin", CustomResponsePluginName), "Update custom response plugin configuration", getAddOrUpdateCustomResponseConfigSchema()),
|
||||
handleAddOrUpdateCustomResponseConfig(client),
|
||||
)
|
||||
}
|
||||
|
||||
func handleAddOrUpdateCustomResponseConfig(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(CustomResponsePluginName, scope, resourceName)
|
||||
|
||||
// Get current custom response configuration to merge with updates
|
||||
currentBody, err := client.Get(ctx, path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get current custom response configuration: %w", err)
|
||||
}
|
||||
|
||||
var response CustomResponseResponse
|
||||
if err := json.Unmarshal(currentBody, &response); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse current custom response response: %w", err)
|
||||
}
|
||||
|
||||
currentConfig := response.Data
|
||||
currentConfig.Enabled = enabled
|
||||
currentConfig.Scope = scope
|
||||
|
||||
// Convert the input configurations to CustomResponseConfig and merge
|
||||
configBytes, err := json.Marshal(configurations)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal configurations: %w", err)
|
||||
}
|
||||
|
||||
var newConfig CustomResponseConfig
|
||||
if err := json.Unmarshal(configBytes, &newConfig); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse custom response configurations: %w", err)
|
||||
}
|
||||
|
||||
// Update configurations (overwrite with new values where provided)
|
||||
if newConfig.Body != "" {
|
||||
currentConfig.Configurations.Body = newConfig.Body
|
||||
}
|
||||
if newConfig.Headers != nil {
|
||||
currentConfig.Configurations.Headers = newConfig.Headers
|
||||
}
|
||||
if newConfig.StatusCode != 0 {
|
||||
currentConfig.Configurations.StatusCode = newConfig.StatusCode
|
||||
}
|
||||
if newConfig.EnableOnStatus != nil {
|
||||
currentConfig.Configurations.EnableOnStatus = newConfig.EnableOnStatus
|
||||
}
|
||||
|
||||
respBody, err := client.Put(ctx, path, currentConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update custom response config at scope '%s': %w", scope, err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func getAddOrUpdateCustomResponseConfigSchema() 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": {
|
||||
"body": {
|
||||
"type": "string",
|
||||
"description": "Custom response body content"
|
||||
},
|
||||
"headers": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "List of custom response headers in the format 'Header-Name=value'"
|
||||
},
|
||||
"status_code": {
|
||||
"type": "integer",
|
||||
"minimum": 100,
|
||||
"maximum": 599,
|
||||
"description": "HTTP status code to return in the custom response"
|
||||
},
|
||||
"enable_on_status": {
|
||||
"type": "array",
|
||||
"items": {"type": "integer"},
|
||||
"description": "List of upstream status codes that trigger this custom response"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["scope", "enabled", "configurations"],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
@@ -74,7 +74,7 @@ func handleAddOrUpdateRequestBlockConfig(client *higress.HigressClient) common.T
|
||||
path := BuildPluginPath(RequestBlockPluginName, scope, resourceName)
|
||||
|
||||
// Get current request block configuration to merge with updates
|
||||
currentBody, err := client.Get(path)
|
||||
currentBody, err := client.Get(ctx, path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get current request block configuration: %w", err)
|
||||
}
|
||||
@@ -114,7 +114,7 @@ func handleAddOrUpdateRequestBlockConfig(client *higress.HigressClient) common.T
|
||||
}
|
||||
currentConfig.Configurations.CaseSensitive = newConfig.CaseSensitive
|
||||
|
||||
respBody, err := client.Put(path, currentConfig)
|
||||
respBody, err := client.Put(ctx, path, currentConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update request block config at scope '%s': %w", scope, err)
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ type RouteResponse = higress.APIResponse[Route]
|
||||
func RegisterRouteTools(mcpServer *common.MCPServer, client *higress.HigressClient) {
|
||||
// List all routes
|
||||
mcpServer.AddTool(
|
||||
mcp.NewTool("list-routes", mcp.WithDescription("List all available routes")),
|
||||
mcp.NewToolWithRawSchema("list-routes", "List all available routes", listRouteSchema()),
|
||||
handleListRoutes(client),
|
||||
)
|
||||
|
||||
@@ -89,7 +89,7 @@ func RegisterRouteTools(mcpServer *common.MCPServer, client *higress.HigressClie
|
||||
|
||||
func handleListRoutes(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
respBody, err := client.Get("/v1/routes")
|
||||
respBody, err := client.Get(ctx, "/v1/routes")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list routes: %w", err)
|
||||
}
|
||||
@@ -113,7 +113,7 @@ func handleGetRoute(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return nil, fmt.Errorf("missing or invalid 'name' argument")
|
||||
}
|
||||
|
||||
respBody, err := client.Get(fmt.Sprintf("/v1/routes/%s", name))
|
||||
respBody, err := client.Get(ctx, fmt.Sprintf("/v1/routes/%s", name))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get route '%s': %w", name, err)
|
||||
}
|
||||
@@ -148,7 +148,35 @@ func handleAddRoute(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return nil, fmt.Errorf("missing required field 'services' in configurations")
|
||||
}
|
||||
|
||||
respBody, err := client.Post("/v1/routes", configurations)
|
||||
// Validate service sources exist
|
||||
if services, ok := configurations["services"].([]interface{}); ok && len(services) > 0 {
|
||||
for _, svc := range services {
|
||||
if serviceMap, ok := svc.(map[string]interface{}); ok {
|
||||
if serviceName, ok := serviceMap["name"].(string); ok {
|
||||
// Extract service source name from "serviceName.serviceType" format
|
||||
var serviceSourceName string
|
||||
for i := len(serviceName) - 1; i >= 0; i-- {
|
||||
if serviceName[i] == '.' {
|
||||
serviceSourceName = serviceName[:i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if serviceSourceName == "" {
|
||||
return nil, fmt.Errorf("invalid service name format '%s', expected 'serviceName.serviceType'", serviceName)
|
||||
}
|
||||
|
||||
// Check if service source exists
|
||||
_, err := client.Get(ctx, fmt.Sprintf("/v1/service-sources/%s", serviceSourceName))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Please create the service source '%s' first and then create the route", serviceSourceName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
respBody, err := client.Post(ctx, "/v1/routes", configurations)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to add route: %w", err)
|
||||
}
|
||||
@@ -178,7 +206,7 @@ func handleUpdateRoute(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
}
|
||||
|
||||
// Get current route configuration to merge with updates
|
||||
currentBody, err := client.Get(fmt.Sprintf("/v1/routes/%s", name))
|
||||
currentBody, err := client.Get(ctx, fmt.Sprintf("/v1/routes/%s", name))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get current route configuration: %w", err)
|
||||
}
|
||||
@@ -227,7 +255,7 @@ func handleUpdateRoute(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
currentConfig.CustomConfigs = newConfig.CustomConfigs
|
||||
}
|
||||
|
||||
respBody, err := client.Put(fmt.Sprintf("/v1/routes/%s", name), currentConfig)
|
||||
respBody, err := client.Put(ctx, fmt.Sprintf("/v1/routes/%s", name), currentConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update route '%s': %w", name, err)
|
||||
}
|
||||
@@ -251,7 +279,7 @@ func handleDeleteRoute(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return nil, fmt.Errorf("missing or invalid 'name' argument")
|
||||
}
|
||||
|
||||
respBody, err := client.Delete(fmt.Sprintf("/v1/routes/%s", name))
|
||||
respBody, err := client.Delete(ctx, fmt.Sprintf("/v1/routes/%s", name))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to delete route '%s': %w", name, err)
|
||||
}
|
||||
@@ -267,6 +295,15 @@ func handleDeleteRoute(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func listRouteSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": [],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
|
||||
func getRouteSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
@@ -295,7 +332,7 @@ func getAddRouteSchema() json.RawMessage {
|
||||
"domains": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "List of domain names, but only one domain is allowed"
|
||||
"description": "List of domain names, but only one domain is allowed,Do not fill in the code to match all"
|
||||
},
|
||||
"path": {
|
||||
"type": "object",
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/higress"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
@@ -37,7 +38,7 @@ type ServiceSourceResponse = higress.APIResponse[ServiceSource]
|
||||
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")),
|
||||
mcp.NewToolWithRawSchema("list-service-sources", "List all available service sources", listServiceSourcesSchema()),
|
||||
handleListServiceSources(client),
|
||||
)
|
||||
|
||||
@@ -68,7 +69,7 @@ func RegisterServiceTools(mcpServer *common.MCPServer, client *higress.HigressCl
|
||||
|
||||
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")
|
||||
respBody, err := client.Get(ctx, "/v1/service-sources")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list service sources: %w", err)
|
||||
}
|
||||
@@ -92,7 +93,7 @@ func handleGetServiceSource(client *higress.HigressClient) common.ToolHandlerFun
|
||||
return nil, fmt.Errorf("missing or invalid 'name' argument")
|
||||
}
|
||||
|
||||
respBody, err := client.Get(fmt.Sprintf("/v1/service-sources/%s", name))
|
||||
respBody, err := client.Get(ctx, fmt.Sprintf("/v1/service-sources/%s", name))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get service source '%s': %w", name, err)
|
||||
}
|
||||
@@ -129,8 +130,28 @@ func handleAddServiceSource(client *higress.HigressClient) common.ToolHandlerFun
|
||||
if _, ok := configurations["port"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'port' in configurations")
|
||||
}
|
||||
if t, ok := configurations["type"].(string); ok && t == "static" {
|
||||
if d, ok := configurations["domain"].(string); ok {
|
||||
host, port, err := net.SplitHostPort(d)
|
||||
if err != nil || host == "" || port == "" {
|
||||
return nil, fmt.Errorf("invalid 'domain' format for static type, expected ip:port, got '%s'", d)
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("invalid 'domain' field type, expected string")
|
||||
}
|
||||
}
|
||||
if t, ok := configurations["type"].(string); ok && t != "static" {
|
||||
if d, ok := configurations["domain"].(string); ok {
|
||||
host, _, err := net.SplitHostPort(d)
|
||||
if err == nil && host != "" {
|
||||
configurations["domain"] = host
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
respBody, err := client.Post("/v1/service-sources", configurations)
|
||||
// valid protocol,sni,properties,auth
|
||||
|
||||
respBody, err := client.Post(ctx, "/v1/service-sources", configurations)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to add service source: %w", err)
|
||||
}
|
||||
@@ -160,7 +181,7 @@ func handleUpdateServiceSource(client *higress.HigressClient) common.ToolHandler
|
||||
}
|
||||
|
||||
// Get current service source configuration to merge with updates
|
||||
currentBody, err := client.Get(fmt.Sprintf("/v1/service-sources/%s", name))
|
||||
currentBody, err := client.Get(ctx, fmt.Sprintf("/v1/service-sources/%s", name))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get current service source configuration: %w", err)
|
||||
}
|
||||
@@ -209,7 +230,7 @@ func handleUpdateServiceSource(client *higress.HigressClient) common.ToolHandler
|
||||
currentConfig.AuthN = newConfig.AuthN
|
||||
}
|
||||
|
||||
respBody, err := client.Put(fmt.Sprintf("/v1/service-sources/%s", name), currentConfig)
|
||||
respBody, err := client.Put(ctx, fmt.Sprintf("/v1/service-sources/%s", name), currentConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update service source '%s': %w", name, err)
|
||||
}
|
||||
@@ -233,7 +254,7 @@ func handleDeleteServiceSource(client *higress.HigressClient) common.ToolHandler
|
||||
return nil, fmt.Errorf("missing or invalid 'name' argument")
|
||||
}
|
||||
|
||||
respBody, err := client.Delete(fmt.Sprintf("/v1/service-sources/%s", name))
|
||||
respBody, err := client.Delete(ctx, fmt.Sprintf("/v1/service-sources/%s", name))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to delete service source '%s': %w", name, err)
|
||||
}
|
||||
@@ -249,6 +270,15 @@ func handleDeleteServiceSource(client *higress.HigressClient) common.ToolHandler
|
||||
}
|
||||
}
|
||||
|
||||
func listServiceSourcesSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": [],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
|
||||
func getServiceSourceSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
@@ -263,7 +293,6 @@ func getServiceSourceSchema() json.RawMessage {
|
||||
}`)
|
||||
}
|
||||
|
||||
// TODO: extend other types of service sources, e.g., nacos, zookeeper, euraka.
|
||||
func getAddServiceSourceSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
@@ -277,12 +306,12 @@ func getAddServiceSourceSchema() json.RawMessage {
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["static", "dns"],
|
||||
"description": "The type of service source: 'static' for static IPs, 'dns' for DNS resolution"
|
||||
"enum": ["static", "dns", "consul", "nacos3","nacos2","nacos1", "eureka", "zookeeper"],
|
||||
"description": "The type of service source. Supported types: 'static' (static IP), 'dns' (DNS resolution), 'consul' (Consul registry), 'nacos3' (Nacos 3.x), 'eureka' (Eureka registry), 'zookeeper' (ZooKeeper registry)"
|
||||
},
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "The domain name or IP address (required)"
|
||||
"description": "The domain name or IP address + port(such as: 127.0.0.1:8080) (required). For dns, use domain name (e.g., 'xxx.com')"
|
||||
},
|
||||
"port": {
|
||||
"type": "integer",
|
||||
@@ -292,12 +321,32 @@ func getAddServiceSourceSchema() json.RawMessage {
|
||||
},
|
||||
"protocol": {
|
||||
"type": "string",
|
||||
"enum": ["http", "https"],
|
||||
"description": "The protocol to use (optional, defaults to http)"
|
||||
"enum": ["http", "https", ""],
|
||||
"description": "The protocol to use (optional, defaults to http, can be empty string for null)"
|
||||
},
|
||||
"sni": {
|
||||
"type": "string",
|
||||
"description": "Server Name Indication for HTTPS connections (optional)"
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"description": "Type-specific configuration properties. Required fields by type: consul: 'consulDatacenter' (string), 'consulServiceTag' (string, format: 'key=value'); nacos3: 'nacosNamespaceId' (string, optional), 'nacosGroups' (array of strings), 'enableMCPServer' (boolean, optional), 'mcpServerBaseUrl' (string, required if enableMCPServer is true, e.g., '/mcp'), 'mcpServerExportDomains' (array of strings, required if enableMCPServer is true, e.g., ['xxx.com']); zookeeper: 'zkServicesPath' (array of strings); static/dns/eureka: no additional properties needed"
|
||||
},
|
||||
"authN": {
|
||||
"type": "object",
|
||||
"description": "Authentication configuration",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Whether authentication is enabled"
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"description": "Authentication properties by type. consul: 'consulToken' (string); nacos3: 'nacosUsername' (string), 'nacosPassword' (string)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name", "type", "domain", "port"],
|
||||
@@ -309,7 +358,6 @@ func getAddServiceSourceSchema() json.RawMessage {
|
||||
}`)
|
||||
}
|
||||
|
||||
// TODO: extend other types of service sources, e.g., nacos, zookeeper, euraka.
|
||||
func getUpdateServiceSourceSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
@@ -323,12 +371,12 @@ func getUpdateServiceSourceSchema() json.RawMessage {
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["static", "dns"],
|
||||
"description": "The type of service source: 'static' for static IPs, 'dns' for DNS resolution"
|
||||
"enum": ["static", "dns", "consul", "nacos3", "eureka", "zookeeper"],
|
||||
"description": "The type of service source. Supported types: 'static' (static IP), 'dns' (DNS resolution), 'consul' (Consul registry), 'nacos3' (Nacos 3.x), 'eureka' (Eureka registry), 'zookeeper' (ZooKeeper registry)"
|
||||
},
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "The domain name or IP address"
|
||||
"description": "The domain name or IP address + port(such as: 127.0.0.1:8080) (required). For dns, use domain name (e.g., 'xxx.com')"
|
||||
},
|
||||
"port": {
|
||||
"type": "integer",
|
||||
@@ -338,12 +386,32 @@ func getUpdateServiceSourceSchema() json.RawMessage {
|
||||
},
|
||||
"protocol": {
|
||||
"type": "string",
|
||||
"enum": ["http", "https"],
|
||||
"description": "The protocol to use (optional, defaults to http)"
|
||||
"enum": ["http", "https", ""],
|
||||
"description": "The protocol to use (optional, can be empty string for null)"
|
||||
},
|
||||
"sni": {
|
||||
"type": "string",
|
||||
"description": "Server Name Indication for HTTPS connections"
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"description": "Type-specific configuration properties. Required fields by type: consul: 'consulDatacenter' (string), 'consulServiceTag' (string, format: 'key=value'); nacos3: 'nacosNamespaceId' (string, optional), 'nacosGroups' (array of strings), 'enableMCPServer' (boolean, optional), 'mcpServerBaseUrl' (string, required if enableMCPServer is true, e.g., '/mcp'), 'mcpServerExportDomains' (array of strings, required if enableMCPServer is true, e.g., ['xxx.com']); zookeeper: 'zkServicesPath' (array of strings); static/dns/eureka: no additional properties needed"
|
||||
},
|
||||
"authN": {
|
||||
"type": "object",
|
||||
"description": "Authentication configuration",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Whether authentication is enabled"
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"description": "Authentication properties by type. consul: 'consulToken' (string); nacos: 'nacosUsername' (string), 'nacosPassword' (string)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
# Higress Ops MCP Server
|
||||
|
||||
Higress Ops MCP Server 提供了 MCP 工具来调试和监控 Istio 和 Envoy 组件,帮助运维人员进行故障诊断和性能分析。
|
||||
|
||||
## 功能特性
|
||||
|
||||
### Istiod 调试接口
|
||||
|
||||
#### 配置相关
|
||||
- `get-istiod-configz`: 获取 Istiod 的配置状态和错误信息
|
||||
|
||||
#### 服务发现相关
|
||||
- `get-istiod-endpointz`: 获取 Istiod 发现的所有服务端点信息
|
||||
- `get-istiod-clusters`: 获取 Istiod 发现的所有集群信息
|
||||
- `get-istiod-registryz`: 获取 Istiod 的服务注册表信息
|
||||
|
||||
#### 状态监控相关
|
||||
- `get-istiod-syncz`: 获取 Istiod 与 Envoy 代理的同步状态信息
|
||||
- `get-istiod-metrics`: 获取 Istiod 的 Prometheus 指标数据
|
||||
|
||||
#### 系统信息相关
|
||||
- `get-istiod-version`: 获取 Istiod 的版本信息
|
||||
- `get-istiod-debug-vars`: 获取 Istiod 的调试变量信息
|
||||
|
||||
### Envoy 调试接口
|
||||
|
||||
#### 配置相关
|
||||
- `get-envoy-config-dump`: 获取 Envoy 的完整配置快照,支持资源过滤和敏感信息掩码
|
||||
- `get-envoy-listeners`: 获取 Envoy 的所有监听器信息
|
||||
- `get-envoy-clusters`: 获取 Envoy 的所有集群信息和健康状态
|
||||
|
||||
#### 运行时相关
|
||||
- `get-envoy-stats`: 获取 Envoy 的统计信息,支持过滤器和多种输出格式
|
||||
- `get-envoy-runtime`: 获取 Envoy 的运行时配置信息
|
||||
- `get-envoy-memory`: 获取 Envoy 的内存使用情况
|
||||
|
||||
#### 状态检查相关
|
||||
- `get-envoy-server-info`: 获取 Envoy 服务器的基本信息
|
||||
- `get-envoy-ready`: 检查 Envoy 是否准备就绪
|
||||
- `get-envoy-hot-restart-version`: 获取 Envoy 热重启版本信息
|
||||
|
||||
#### 安全相关
|
||||
- `get-envoy-certs`: 获取 Envoy 的证书信息
|
||||
|
||||
## 配置参数
|
||||
|
||||
| 参数 | 类型 | 必需 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `istiodURL` | string | 必填 | Istiod 调试接口的 URL 地址 |
|
||||
| `envoyAdminURL` | string | 必填 | Envoy Admin 接口的 URL 地址 |
|
||||
| `namespace` | string | 可选 | Kubernetes 命名空间,默认为 `higress-system` |
|
||||
| `description` | string | 可选 | 服务器描述信息,默认为 "Higress Ops MCP Server, which provides debug interfaces for Istio and Envoy components." |
|
||||
|
||||
## 配置示例
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
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 会话保持路由规则
|
||||
- match_rule_domain: "*"
|
||||
match_rule_path: /higress-ops
|
||||
match_rule_type: "prefix"
|
||||
servers:
|
||||
- name: higress-ops-mcp-server
|
||||
path: /higress-ops
|
||||
type: higress-ops
|
||||
config:
|
||||
istiodURL: http://higress-controller.higress-system.svc.cluster.local:15014 # istiod url
|
||||
envoyAdminURL: http://127.0.0.1:15000 # envoy url 填127.0.0.1就行,和 gateway 于同一容器
|
||||
namespace: higress-system
|
||||
description: "Higress Ops MCP Server for Istio and Envoy debugging"
|
||||
```
|
||||
|
||||
## 鉴权配置
|
||||
|
||||
Higress Ops MCP Server 使用自定义 HTTP Header 进行鉴权。客户端需要在请求头中携带 Istiod 认证 Token。
|
||||
|
||||
### Token 生成方式
|
||||
|
||||
使用以下命令生成长期有效的 Istiod 认证 Token:
|
||||
|
||||
```bash
|
||||
kubectl create token higress-gateway -n higress-system --audience istio-ca --duration 87600h
|
||||
```
|
||||
|
||||
**参数说明:**
|
||||
- `higress-gateway`: ServiceAccount 名称(与 Higress Gateway Pod 使用的 ServiceAccount 一致)
|
||||
- `-n higress-system`: 命名空间(需要与配置参数 `namespace` 一致)
|
||||
- `--audience istio-ca`: Token 的受众,必须为 `istio-ca`
|
||||
- `--duration 87600h`: Token 有效期(87600小时 ≈ 10年)
|
||||
|
||||
### 配置示例
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"higress_ops_mcp": {
|
||||
"url": "http://127.0.0.1:80/higress-ops/sse",
|
||||
"headers": {
|
||||
"X-Istiod-Token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Im1IUlI0Z01ISUNBNVlZbDBHcVVBMjFhMklwQ3hFaHIxSlVlamtzTFRLOTQifQ..."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**说明:**
|
||||
- `X-Istiod-Token` 头用于携带 Istiod 认证 Token
|
||||
- Token 值由上述 `kubectl create token` 命令生成
|
||||
- 如果未配置 Token,跨 Pod 访问 Istiod 接口时会遇到 401 认证错误
|
||||
|
||||
## 演示
|
||||
|
||||
1. get envoy route information
|
||||
|
||||
https://private-user-images.githubusercontent.com/153273766/507769115-d8e20b70-db1a-4a82-b89a-9eefeb3c8982.mov?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NjE4Nzg4NjAsIm5iZiI6MTc2MTg3ODU2MCwicGF0aCI6Ii8xNTMyNzM3NjYvNTA3NzY5MTE1LWQ4ZTIwYjcwLWRiMWEtNGE4Mi1iODlhLTllZWZlYjNjODk4Mi5tb3Y_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUxMDMxJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MTAzMVQwMjQyNDBaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1kYzg1Y2FiOTdiN2FiOTNkMmQ0OTc1NzEyZGMyMTlkNDQ4YjQ0NGYyOGUwNTlhYzYyYzA1ODJhOWM0M2Y3ZTQyJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.Uz-HfM9tOzl7zrhGsPP1suunGg_K9ZbUN1BzAU5Oquo
|
||||
|
||||
2. get istiod cluster information
|
||||
|
||||
https://private-user-images.githubusercontent.com/153273766/507769013-9f598593-1251-4304-8e41-8bf4d1588897.mov?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NjE4Nzg4NjAsIm5iZiI6MTc2MTg3ODU2MCwicGF0aCI6Ii8xNTMyNzM3NjYvNTA3NzY5MDEzLTlmNTk4NTkzLTEyNTEtNDMwNC04ZTQxLThiZjRkMTU4ODg5Ny5tb3Y_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUxMDMxJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MTAzMVQwMjQyNDBaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1hZDQwYWE3MjM5OTU1NGNkMDcwNTgzNDMzZGI4NDRkYzdiNWRlNGJhODMwNjFlYjZiZjUzNzM3YWFhYzIyMjBjJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.g19-rxOHSLIIszdGYAI7CmRzLTlrbA1fJ0hB6duuDBI
|
||||
|
||||
|
||||
|
||||
## 使用场景
|
||||
|
||||
### 1. 故障诊断
|
||||
- 使用 `get-istiod-syncz` 检查配置同步状态
|
||||
- 使用 `get-envoy-clusters` 检查集群健康状态
|
||||
- 使用 `get-envoy-listeners` 检查监听器配置
|
||||
|
||||
### 2. 性能分析
|
||||
- 使用 `get-istiod-metrics` 获取 Istiod 性能指标
|
||||
- 使用 `get-envoy-stats` 获取 Envoy 统计信息
|
||||
- 使用 `get-envoy-memory` 监控内存使用
|
||||
|
||||
### 3. 配置验证
|
||||
- 使用 `get-istiod-configz` 验证 Istiod 配置状态
|
||||
- 使用 `get-envoy-config-dump` 验证 Envoy 配置
|
||||
|
||||
### 4. 安全审计
|
||||
- 使用 `get-envoy-certs` 检查证书状态
|
||||
- 使用 `get-istiod-debug-vars` 查看调试变量
|
||||
|
||||
## 工具参数示例
|
||||
|
||||
### Istiod 工具示例
|
||||
|
||||
```bash
|
||||
# 获取配置状态
|
||||
get-istiod-configz
|
||||
|
||||
# 获取同步状态
|
||||
get-istiod-syncz
|
||||
|
||||
# 获取端点信息
|
||||
get-istiod-endpointz
|
||||
```
|
||||
|
||||
### Envoy 工具示例
|
||||
|
||||
```bash
|
||||
# 获取配置快照,过滤监听器配置
|
||||
get-envoy-config-dump --resource="listeners"
|
||||
|
||||
# 获取集群信息,JSON 格式输出
|
||||
get-envoy-clusters --format="json"
|
||||
|
||||
# 获取统计信息,只显示包含 "cluster" 的统计项
|
||||
get-envoy-stats --filter="cluster.*" --format="json"
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 如何获取特定集群的详细信息?
|
||||
A: 使用 `get-envoy-clusters` 工具,然后使用 `get-envoy-config-dump --resource="clusters"` 获取详细配置。
|
||||
|
||||
### Q: 如何监控配置同步状态?
|
||||
A: 使用 `get-istiod-syncz` 查看整体同步状态,使用 `get-istiod-configz` 查看配置状态和错误信息。
|
||||
|
||||
### Q: 如何排查路由问题?
|
||||
A: 使用 `get-envoy-config-dump` 获取详细路由信息。
|
||||
|
||||
### Q: 支持哪些输出格式?
|
||||
A: 大部分工具支持 text 和 json 格式,统计信息还支持 prometheus 格式。
|
||||
@@ -0,0 +1,197 @@
|
||||
# Higress Ops MCP Server
|
||||
|
||||
Higress Ops MCP Server provides MCP tools for debugging and monitoring Istio and Envoy components, helping operations teams with troubleshooting and performance analysis.
|
||||
|
||||
## Features
|
||||
|
||||
### Istiod Debug Interfaces
|
||||
|
||||
#### Configuration
|
||||
- `get-istiod-configz`: Get Istiod configuration status and error information
|
||||
|
||||
#### Service Discovery
|
||||
- `get-istiod-endpointz`: Get all service endpoints discovered by Istiod
|
||||
- `get-istiod-clusters`: Get all clusters discovered by Istiod
|
||||
- `get-istiod-registryz`: Get Istiod service registry information
|
||||
|
||||
#### Status Monitoring
|
||||
- `get-istiod-syncz`: Get synchronization status between Istiod and Envoy proxies
|
||||
- `get-istiod-metrics`: Get Prometheus metrics from Istiod
|
||||
|
||||
#### System Information
|
||||
- `get-istiod-version`: Get Istiod version information
|
||||
- `get-istiod-debug-vars`: Get Istiod debug variables
|
||||
|
||||
### Envoy Debug Interfaces
|
||||
|
||||
#### Configuration
|
||||
- `get-envoy-config-dump`: Get complete Envoy configuration snapshot with resource filtering and sensitive data masking
|
||||
- `get-envoy-listeners`: Get all Envoy listener information
|
||||
- `get-envoy-clusters`: Get all Envoy cluster information and health status
|
||||
|
||||
#### Runtime
|
||||
- `get-envoy-stats`: Get Envoy statistics with filtering and multiple output formats
|
||||
- `get-envoy-runtime`: Get Envoy runtime configuration
|
||||
- `get-envoy-memory`: Get Envoy memory usage
|
||||
|
||||
#### Status Check
|
||||
- `get-envoy-server-info`: Get Envoy server basic information
|
||||
- `get-envoy-ready`: Check if Envoy is ready
|
||||
- `get-envoy-hot-restart-version`: Get Envoy hot restart version
|
||||
|
||||
#### Security
|
||||
- `get-envoy-certs`: Get Envoy certificate information
|
||||
|
||||
## Configuration Parameters
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `istiodURL` | string | Yes | URL address of Istiod debug interface |
|
||||
| `envoyAdminURL` | string | Yes | URL address of Envoy Admin interface |
|
||||
| `namespace` | string | Optional | Kubernetes namespace, defaults to `higress-system` |
|
||||
| `description` | string | Optional | Server description, defaults to "Higress Ops MCP Server, which provides debug interfaces for Istio and Envoy components." |
|
||||
|
||||
## Configuration Example
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
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
|
||||
- match_rule_domain: "*"
|
||||
match_rule_path: /higress-ops
|
||||
match_rule_type: "prefix"
|
||||
servers:
|
||||
- name: higress-ops-mcp-server
|
||||
path: /higress-ops
|
||||
type: higress-ops
|
||||
config:
|
||||
istiodURL: http://higress-controller.higress-system.svc.cluster.local:15014 # istiod url
|
||||
envoyAdminURL: http://127.0.0.1:15000 # envoy url, use 127.0.0.1 as it's in the same container as gateway
|
||||
namespace: higress-system
|
||||
description: "Higress Ops MCP Server for Istio and Envoy debugging"
|
||||
```
|
||||
|
||||
## Authentication Configuration
|
||||
|
||||
Higress Ops MCP Server uses custom HTTP headers for authentication. Clients need to include an Istiod authentication token in their request headers.
|
||||
|
||||
### Token Generation
|
||||
|
||||
Generate a long-lived Istiod authentication token with the following command:
|
||||
|
||||
```bash
|
||||
kubectl create token higress-gateway -n higress-system --audience istio-ca --duration 87600h
|
||||
```
|
||||
|
||||
**Parameter Description:**
|
||||
- `higress-gateway`: ServiceAccount name (must match the ServiceAccount used by Higress Gateway Pod)
|
||||
- `-n higress-system`: Namespace (must match the `namespace` configuration parameter)
|
||||
- `--audience istio-ca`: Token audience, must be `istio-ca`
|
||||
- `--duration 87600h`: Token validity period (87600 hours ≈ 10 years)
|
||||
|
||||
### Configuration Example
|
||||
|
||||
Add the following to your MCP client configuration file (e.g., `~/.cursor/mcp.json` or Claude Desktop config):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"higress_ops_mcp": {
|
||||
"url": "http://127.0.0.1:80/higress-ops/sse",
|
||||
"headers": {
|
||||
"X-Istiod-Token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Im1IUlI0Z01ISUNBNVlZbDBHcVVBMjFhMklwQ3hFaHIxSlVlamtzTFRLOTQifQ..."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- The `X-Istiod-Token` header is used to carry the Istiod authentication token
|
||||
- The token value is generated by the above `kubectl create token` command
|
||||
- If the token is not configured, accessing Istiod interfaces across pods will result in 401 authentication errors
|
||||
|
||||
## Demo
|
||||
|
||||
1. get envoy route information
|
||||
|
||||
https://private-user-images.githubusercontent.com/153273766/507769115-d8e20b70-db1a-4a82-b89a-9eefeb3c8982.mov?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NjE4Nzg4NjAsIm5iZiI6MTc2MTg3ODU2MCwicGF0aCI6Ii8xNTMyNzM3NjYvNTA3NzY5MTE1LWQ4ZTIwYjcwLWRiMWEtNGE4Mi1iODlhLTllZWZlYjNjODk4Mi5tb3Y_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUxMDMxJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MTAzMVQwMjQyNDBaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1kYzg1Y2FiOTdiN2FiOTNkMmQ0OTc1NzEyZGMyMTlkNDQ4YjQ0NGYyOGUwNTlhYzYyYzA1ODJhOWM0M2Y3ZTQyJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.Uz-HfM9tOzl7zrhGsPP1suunGg_K9ZbUN1BzAU5Oquo
|
||||
|
||||
2. get istiod cluster information
|
||||
|
||||
https://private-user-images.githubusercontent.com/153273766/507769013-9f598593-1251-4304-8e41-8bf4d1588897.mov?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NjE4Nzg4NjAsIm5iZiI6MTc2MTg3ODU2MCwicGF0aCI6Ii8xNTMyNzM3NjYvNTA3NzY5MDEzLTlmNTk4NTkzLTEyNTEtNDMwNC04ZTQxLThiZjRkMTU4ODg5Ny5tb3Y_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUxMDMxJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MTAzMVQwMjQyNDBaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1hZDQwYWE3MjM5OTU1NGNkMDcwNTgzNDMzZGI4NDRkYzdiNWRlNGJhODMwNjFlYjZiZjUzNzM3YWFhYzIyMjBjJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.g19-rxOHSLIIszdGYAI7CmRzLTlrbA1fJ0hB6duuDBI
|
||||
|
||||
## Use Cases
|
||||
|
||||
### 1. Troubleshooting
|
||||
- Use `get-istiod-syncz` to check configuration sync status
|
||||
- Use `get-envoy-clusters` to check cluster health status
|
||||
- Use `get-envoy-listeners` to check listener configuration
|
||||
|
||||
### 2. Performance Analysis
|
||||
- Use `get-istiod-metrics` to get Istiod performance metrics
|
||||
- Use `get-envoy-stats` to get Envoy statistics
|
||||
- Use `get-envoy-memory` to monitor memory usage
|
||||
|
||||
### 3. Configuration Validation
|
||||
- Use `get-istiod-config-dump` to validate Istiod configuration
|
||||
- Use `get-envoy-config-dump` to validate Envoy configuration
|
||||
|
||||
### 4. Security Audit
|
||||
- Use `get-envoy-certs` to check certificate status
|
||||
- Use `get-istiod-debug-vars` to view debug variables
|
||||
|
||||
## Tool Parameter Examples
|
||||
|
||||
### Istiod Tool Examples
|
||||
|
||||
```bash
|
||||
# Get specific proxy status
|
||||
get-istiod-proxy-status --proxy="gateway-proxy.istio-system"
|
||||
|
||||
# Get configuration dump
|
||||
get-istiod-config-dump
|
||||
|
||||
# Get sync status
|
||||
get-istiod-syncz
|
||||
```
|
||||
|
||||
### Envoy Tool Examples
|
||||
|
||||
```bash
|
||||
# Get config dump, filter listeners
|
||||
get-envoy-config-dump --resource="listeners"
|
||||
|
||||
# Get cluster info in JSON format
|
||||
get-envoy-clusters --format="json"
|
||||
|
||||
# Get stats containing "cluster", JSON format
|
||||
get-envoy-stats --filter="cluster.*" --format="json"
|
||||
```
|
||||
|
||||
## FAQ
|
||||
|
||||
### Q: How to get detailed information for a specific cluster?
|
||||
A: Use `get-envoy-clusters` tool, then use `get-envoy-config-dump --resource="clusters"` for detailed configuration.
|
||||
|
||||
### Q: How to monitor configuration sync status?
|
||||
A: Use `get-istiod-syncz` for overall sync status, use `get-istiod-proxy-status` for specific proxy status.
|
||||
|
||||
### Q: How to troubleshoot routing issues?
|
||||
A: Use `get-envoy-config-dump` for detailed route information.
|
||||
|
||||
### Q: What output formats are supported?
|
||||
A: Most tools support text and json formats, statistics also support prometheus format.
|
||||
@@ -0,0 +1,147 @@
|
||||
package higress_ops
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
)
|
||||
|
||||
// OpsClient handles Istio/Envoy debug API connections and operations
|
||||
type OpsClient struct {
|
||||
istiodURL string
|
||||
envoyAdminURL string
|
||||
namespace string
|
||||
istiodToken string // Istiod authentication token (audience: istio-ca)
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// NewOpsClient creates a new ops client for Istio/Envoy debug interfaces
|
||||
func NewOpsClient(istiodURL, envoyAdminURL, namespace string) *OpsClient {
|
||||
if namespace == "" {
|
||||
namespace = "higress-system"
|
||||
}
|
||||
|
||||
client := &OpsClient{
|
||||
istiodURL: istiodURL,
|
||||
envoyAdminURL: envoyAdminURL,
|
||||
namespace: namespace,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
// GetIstiodDebug calls Istiod debug endpoints
|
||||
func (c *OpsClient) GetIstiodDebug(ctx context.Context, path string) ([]byte, error) {
|
||||
return c.request(ctx, c.istiodURL, path)
|
||||
}
|
||||
|
||||
// GetEnvoyAdmin calls Envoy admin endpoints
|
||||
func (c *OpsClient) GetEnvoyAdmin(ctx context.Context, path string) ([]byte, error) {
|
||||
return c.request(ctx, c.envoyAdminURL, path)
|
||||
}
|
||||
|
||||
// GetIstiodDebugWithParams calls Istiod debug endpoints with query parameters
|
||||
func (c *OpsClient) GetIstiodDebugWithParams(ctx context.Context, path string, params map[string]string) ([]byte, error) {
|
||||
return c.requestWithParams(ctx, c.istiodURL, path, params)
|
||||
}
|
||||
|
||||
// GetEnvoyAdminWithParams calls Envoy admin endpoints with query parameters
|
||||
func (c *OpsClient) GetEnvoyAdminWithParams(ctx context.Context, path string, params map[string]string) ([]byte, error) {
|
||||
return c.requestWithParams(ctx, c.envoyAdminURL, path, params)
|
||||
}
|
||||
|
||||
func (c *OpsClient) request(ctx context.Context, baseURL, path string) ([]byte, error) {
|
||||
return c.requestWithParams(ctx, baseURL, path, nil)
|
||||
}
|
||||
|
||||
func (c *OpsClient) requestWithParams(ctx context.Context, baseURL, path string, params map[string]string) ([]byte, error) {
|
||||
fullURL := baseURL + path
|
||||
|
||||
// Add query parameters if provided
|
||||
if len(params) > 0 {
|
||||
u, err := url.Parse(fullURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse URL %s: %w", fullURL, err)
|
||||
}
|
||||
|
||||
q := u.Query()
|
||||
for key, value := range params {
|
||||
q.Set(key, value)
|
||||
}
|
||||
u.RawQuery = q.Encode()
|
||||
fullURL = u.String()
|
||||
}
|
||||
|
||||
api.LogDebugf("Ops API GET %s", fullURL)
|
||||
|
||||
// Use the provided context, or create a new one if nil
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
reqCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(reqCtx, "GET", fullURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
// Try to get Istiod token from context first (passthrough from MCP client)
|
||||
// This is only applied for Istiod requests, not Envoy admin
|
||||
if c.isBaseURL(baseURL, c.istiodURL) {
|
||||
if istiodToken, ok := common.GetIstiodToken(ctx); ok && istiodToken != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+istiodToken)
|
||||
api.LogInfof("Istiod API request: Using X-Istiod-Token from context for %s", path)
|
||||
} else {
|
||||
api.LogWarnf("Istiod API request: No authentication token available for %s. Request may fail with 401", path)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("HTTP error %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
|
||||
return respBody, nil
|
||||
}
|
||||
|
||||
// GetNamespace returns the configured namespace
|
||||
func (c *OpsClient) GetNamespace() string {
|
||||
return c.namespace
|
||||
}
|
||||
|
||||
// GetIstiodURL returns the Istiod URL
|
||||
func (c *OpsClient) GetIstiodURL() string {
|
||||
return c.istiodURL
|
||||
}
|
||||
|
||||
// GetEnvoyAdminURL returns the Envoy admin URL
|
||||
func (c *OpsClient) GetEnvoyAdminURL() string {
|
||||
return c.envoyAdminURL
|
||||
}
|
||||
|
||||
// isBaseURL checks if the baseURL matches the targetURL (for determining if token is needed)
|
||||
func (c *OpsClient) isBaseURL(baseURL, targetURL string) bool {
|
||||
return baseURL == targetURL
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package higress_ops
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/higress/higress-ops/tools"
|
||||
"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-ops", &HigressOpsConfig{})
|
||||
}
|
||||
|
||||
type HigressOpsConfig struct {
|
||||
istiodURL string
|
||||
envoyAdminURL string
|
||||
namespace string
|
||||
istiodToken string
|
||||
description string
|
||||
}
|
||||
|
||||
func (c *HigressOpsConfig) ParseConfig(config map[string]interface{}) error {
|
||||
istiodURL, ok := config["istiodURL"].(string)
|
||||
if !ok {
|
||||
return errors.New("missing istiodURL")
|
||||
}
|
||||
c.istiodURL = istiodURL
|
||||
|
||||
envoyAdminURL, ok := config["envoyAdminURL"].(string)
|
||||
if !ok {
|
||||
return errors.New("missing envoyAdminURL")
|
||||
}
|
||||
c.envoyAdminURL = envoyAdminURL
|
||||
|
||||
if namespace, ok := config["namespace"].(string); ok {
|
||||
c.namespace = namespace
|
||||
} else {
|
||||
c.namespace = "higress-system"
|
||||
}
|
||||
|
||||
// Optional: Istiod authentication token (required for cross-pod access)
|
||||
if istiodToken, ok := config["istiodToken"].(string); ok {
|
||||
c.istiodToken = istiodToken
|
||||
api.LogInfof("Istiod authentication token configured")
|
||||
} else {
|
||||
api.LogWarnf("No istiodToken configured. Cross-pod Istiod API requests may fail with 401 errors.")
|
||||
}
|
||||
|
||||
if desc, ok := config["description"].(string); ok {
|
||||
c.description = desc
|
||||
} else {
|
||||
c.description = "Higress Ops MCP Server, which provides debug interfaces for Istio and Envoy components."
|
||||
}
|
||||
|
||||
api.LogInfof("Higress Ops MCP Server configuration parsed successfully. IstiodURL: %s, EnvoyAdminURL: %s, Namespace: %s, Description: %s",
|
||||
c.istiodURL, c.envoyAdminURL, c.namespace, c.description)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *HigressOpsConfig) NewServer(serverName string) (*common.MCPServer, error) {
|
||||
mcpServer := common.NewMCPServer(
|
||||
serverName,
|
||||
Version,
|
||||
common.WithInstructions("This is a Higress Ops MCP Server that provides debug interfaces for Istio and Envoy components"),
|
||||
)
|
||||
|
||||
// Initialize Ops client with istiodToken
|
||||
client := NewOpsClient(c.istiodURL, c.envoyAdminURL, c.namespace)
|
||||
|
||||
// Register all tools with the client as an interface
|
||||
tools.RegisterIstiodTools(mcpServer, tools.OpsClient(client))
|
||||
tools.RegisterEnvoyTools(mcpServer, tools.OpsClient(client))
|
||||
|
||||
api.LogInfof("Higress Ops MCP Server initialized: %s", serverName)
|
||||
|
||||
return mcpServer, nil
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// OpsClient defines the interface for operations client
|
||||
type OpsClient interface {
|
||||
// GetIstiodDebug calls Istiod debug endpoints
|
||||
GetIstiodDebug(ctx context.Context, path string) ([]byte, error)
|
||||
|
||||
// GetEnvoyAdmin calls Envoy admin endpoints
|
||||
GetEnvoyAdmin(ctx context.Context, path string) ([]byte, error)
|
||||
|
||||
// GetIstiodDebugWithParams calls Istiod debug endpoints with query parameters
|
||||
GetIstiodDebugWithParams(ctx context.Context, path string, params map[string]string) ([]byte, error)
|
||||
|
||||
// GetEnvoyAdminWithParams calls Envoy admin endpoints with query parameters
|
||||
GetEnvoyAdminWithParams(ctx context.Context, path string, params map[string]string) ([]byte, error)
|
||||
|
||||
// GetNamespace returns the configured namespace
|
||||
GetNamespace() string
|
||||
|
||||
// GetIstiodURL returns the Istiod URL
|
||||
GetIstiodURL() string
|
||||
|
||||
// GetEnvoyAdminURL returns the Envoy admin URL
|
||||
GetEnvoyAdminURL() string
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
// RegisterEnvoyTools registers all Envoy admin tools
|
||||
func RegisterEnvoyTools(mcpServer *common.MCPServer, client OpsClient) {
|
||||
// Config dump tool
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema(
|
||||
"get-envoy-config-dump",
|
||||
"Get complete Envoy configuration snapshot, including all listeners, clusters, routes, etc.",
|
||||
CreateSimpleSchema(),
|
||||
),
|
||||
handleEnvoyConfigDump(client),
|
||||
)
|
||||
|
||||
// Clusters info tool
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema(
|
||||
"get-envoy-clusters",
|
||||
"Get all Envoy cluster information and health status",
|
||||
CreateParameterSchema(
|
||||
map[string]interface{}{
|
||||
"format": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "Output format: json or text (default text)",
|
||||
"enum": []string{"json", "text"},
|
||||
},
|
||||
},
|
||||
[]string{},
|
||||
),
|
||||
),
|
||||
handleEnvoyClusters(client),
|
||||
)
|
||||
|
||||
// Listeners info tool
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema(
|
||||
"get-envoy-listeners",
|
||||
"Get all Envoy listener information",
|
||||
CreateParameterSchema(
|
||||
map[string]interface{}{
|
||||
"format": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "Output format: json or text (default text)",
|
||||
"enum": []string{"json", "text"},
|
||||
},
|
||||
},
|
||||
[]string{},
|
||||
),
|
||||
),
|
||||
handleEnvoyListeners(client),
|
||||
)
|
||||
|
||||
// Stats tool
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema(
|
||||
"get-envoy-stats",
|
||||
"Get Envoy statistics information",
|
||||
CreateParameterSchema(
|
||||
map[string]interface{}{
|
||||
"filter": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "Statistics filter, supports regular expressions (optional)",
|
||||
},
|
||||
"format": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "Output format: json, prometheus or text (default text)",
|
||||
"enum": []string{"json", "prometheus", "text"},
|
||||
},
|
||||
},
|
||||
[]string{},
|
||||
),
|
||||
),
|
||||
handleEnvoyStats(client),
|
||||
)
|
||||
|
||||
// Server info tool
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema(
|
||||
"get-envoy-server-info",
|
||||
"Get Envoy server basic information",
|
||||
CreateSimpleSchema(),
|
||||
),
|
||||
handleEnvoyServerInfo(client),
|
||||
)
|
||||
|
||||
// Ready check tool
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema(
|
||||
"get-envoy-ready",
|
||||
"Check if Envoy is ready",
|
||||
CreateSimpleSchema(),
|
||||
),
|
||||
handleEnvoyReady(client),
|
||||
)
|
||||
|
||||
// Hot restart epoch tool
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema(
|
||||
"get-envoy-hot-restart-version",
|
||||
"Get Envoy hot restart version information",
|
||||
CreateSimpleSchema(),
|
||||
),
|
||||
handleEnvoyHotRestartVersion(client),
|
||||
)
|
||||
|
||||
// Certs info tool
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema(
|
||||
"get-envoy-certs",
|
||||
"Get Envoy certificate information",
|
||||
CreateSimpleSchema(),
|
||||
),
|
||||
handleEnvoyCerts(client),
|
||||
)
|
||||
}
|
||||
|
||||
func handleEnvoyConfigDump(client OpsClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
// Get complete config dump without any filters
|
||||
data, err := client.GetEnvoyAdmin(ctx, "/config_dump")
|
||||
if err != nil {
|
||||
return CreateErrorResult("failed to get Envoy config dump: " + err.Error())
|
||||
}
|
||||
return CreateToolResult(data, "json")
|
||||
}
|
||||
}
|
||||
|
||||
func handleEnvoyClusters(client OpsClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
format := GetStringParam(arguments, "format", "text")
|
||||
|
||||
path := "/clusters"
|
||||
params := make(map[string]string)
|
||||
|
||||
if format == "json" {
|
||||
params["format"] = "json"
|
||||
}
|
||||
|
||||
var data []byte
|
||||
var err error
|
||||
|
||||
if len(params) > 0 {
|
||||
data, err = client.GetEnvoyAdminWithParams(ctx, path, params)
|
||||
} else {
|
||||
data, err = client.GetEnvoyAdmin(ctx, path)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return CreateErrorResult("failed to get Envoy clusters: " + err.Error())
|
||||
}
|
||||
return CreateToolResult(data, format)
|
||||
}
|
||||
}
|
||||
|
||||
func handleEnvoyListeners(client OpsClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
format := GetStringParam(arguments, "format", "text")
|
||||
|
||||
path := "/listeners"
|
||||
params := make(map[string]string)
|
||||
|
||||
if format == "json" {
|
||||
params["format"] = "json"
|
||||
}
|
||||
|
||||
var data []byte
|
||||
var err error
|
||||
|
||||
if len(params) > 0 {
|
||||
data, err = client.GetEnvoyAdminWithParams(ctx, path, params)
|
||||
} else {
|
||||
data, err = client.GetEnvoyAdmin(ctx, path)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return CreateErrorResult("failed to get Envoy listeners: " + err.Error())
|
||||
}
|
||||
return CreateToolResult(data, format)
|
||||
}
|
||||
}
|
||||
|
||||
func handleEnvoyStats(client OpsClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
filter := GetStringParam(arguments, "filter", "")
|
||||
format := GetStringParam(arguments, "format", "text")
|
||||
|
||||
var path string
|
||||
switch format {
|
||||
case "json":
|
||||
path = "/stats?format=json"
|
||||
case "prometheus":
|
||||
path = "/stats/prometheus"
|
||||
default:
|
||||
path = "/stats"
|
||||
}
|
||||
|
||||
params := make(map[string]string)
|
||||
if filter != "" {
|
||||
params["filter"] = filter
|
||||
}
|
||||
|
||||
var data []byte
|
||||
var err error
|
||||
|
||||
if len(params) > 0 {
|
||||
data, err = client.GetEnvoyAdminWithParams(ctx, path, params)
|
||||
} else {
|
||||
data, err = client.GetEnvoyAdmin(ctx, path)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return CreateErrorResult("failed to get Envoy stats: " + err.Error())
|
||||
}
|
||||
return CreateToolResult(data, format)
|
||||
}
|
||||
}
|
||||
|
||||
func handleEnvoyServerInfo(client OpsClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
data, err := client.GetEnvoyAdmin(ctx, "/server_info")
|
||||
if err != nil {
|
||||
return CreateErrorResult("failed to get Envoy server info: " + err.Error())
|
||||
}
|
||||
return CreateToolResult(data, "json")
|
||||
}
|
||||
}
|
||||
|
||||
func handleEnvoyReady(client OpsClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
data, err := client.GetEnvoyAdmin(ctx, "/ready")
|
||||
if err != nil {
|
||||
return CreateErrorResult("failed to get Envoy ready status: " + err.Error())
|
||||
}
|
||||
return CreateToolResult(data, "text")
|
||||
}
|
||||
}
|
||||
|
||||
func handleEnvoyHotRestartVersion(client OpsClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
data, err := client.GetEnvoyAdmin(ctx, "/hot_restart_version")
|
||||
if err != nil {
|
||||
return CreateErrorResult("failed to get Envoy hot restart version: " + err.Error())
|
||||
}
|
||||
return CreateToolResult(data, "text")
|
||||
}
|
||||
}
|
||||
|
||||
func handleEnvoyCerts(client OpsClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
data, err := client.GetEnvoyAdmin(ctx, "/certs")
|
||||
if err != nil {
|
||||
return CreateErrorResult("failed to get Envoy certs: " + err.Error())
|
||||
}
|
||||
return CreateToolResult(data, "json")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
// RegisterIstiodTools registers all Istiod debug tools
|
||||
func RegisterIstiodTools(mcpServer *common.MCPServer, client OpsClient) {
|
||||
// Sync status tool
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema(
|
||||
"get-istiod-syncz",
|
||||
"Get synchronization status information between Istiod and Envoy proxies",
|
||||
CreateSimpleSchema(),
|
||||
),
|
||||
handleIstiodSyncz(client),
|
||||
)
|
||||
|
||||
// Endpoints debug tool
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema(
|
||||
"get-istiod-endpointz",
|
||||
"Get all service endpoint information discovered by Istiod",
|
||||
CreateSimpleSchema(),
|
||||
),
|
||||
handleIstiodEndpointz(client),
|
||||
)
|
||||
|
||||
// Config status tool
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema(
|
||||
"get-istiod-configz",
|
||||
"Get Istiod configuration status and error information",
|
||||
CreateSimpleSchema(),
|
||||
),
|
||||
handleIstiodConfigz(client),
|
||||
)
|
||||
|
||||
// Clusters debug tool
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema(
|
||||
"get-istiod-clusters",
|
||||
"Get all cluster information discovered by Istiod",
|
||||
CreateSimpleSchema(),
|
||||
),
|
||||
handleIstiodClusters(client),
|
||||
)
|
||||
|
||||
// Version info tool
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema(
|
||||
"get-istiod-version",
|
||||
"Get Istiod version information",
|
||||
CreateSimpleSchema(),
|
||||
),
|
||||
handleIstiodVersion(client),
|
||||
)
|
||||
|
||||
// Registry info tool
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema(
|
||||
"get-istiod-registryz",
|
||||
"Get Istiod service registry information",
|
||||
CreateSimpleSchema(),
|
||||
),
|
||||
handleIstiodRegistryz(client),
|
||||
)
|
||||
}
|
||||
|
||||
func handleIstiodSyncz(client OpsClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
data, err := client.GetIstiodDebug(ctx, "/debug/syncz")
|
||||
if err != nil {
|
||||
return CreateErrorResult("failed to get Istiod sync status: " + err.Error())
|
||||
}
|
||||
return CreateToolResult(data, "json")
|
||||
}
|
||||
}
|
||||
|
||||
func handleIstiodEndpointz(client OpsClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
data, err := client.GetIstiodDebug(ctx, "/debug/endpointz")
|
||||
if err != nil {
|
||||
return CreateErrorResult("failed to get Istiod endpoints: " + err.Error())
|
||||
}
|
||||
return CreateToolResult(data, "json")
|
||||
}
|
||||
}
|
||||
|
||||
func handleIstiodConfigz(client OpsClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
data, err := client.GetIstiodDebug(ctx, "/debug/configz")
|
||||
if err != nil {
|
||||
return CreateErrorResult("failed to get Istiod config status: " + err.Error())
|
||||
}
|
||||
return CreateToolResult(data, "json")
|
||||
}
|
||||
}
|
||||
|
||||
func handleIstiodClusters(client OpsClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
data, err := client.GetIstiodDebug(ctx, "/debug/clusterz")
|
||||
if err != nil {
|
||||
return CreateErrorResult("failed to get Istiod clusters: " + err.Error())
|
||||
}
|
||||
return CreateToolResult(data, "json")
|
||||
}
|
||||
}
|
||||
|
||||
func handleIstiodVersion(client OpsClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
data, err := client.GetIstiodDebug(ctx, "/version")
|
||||
if err != nil {
|
||||
return CreateErrorResult("failed to get Istiod version: " + err.Error())
|
||||
}
|
||||
return CreateToolResult(data, "json")
|
||||
}
|
||||
}
|
||||
|
||||
func handleIstiodRegistryz(client OpsClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
data, err := client.GetIstiodDebug(ctx, "/debug/registryz")
|
||||
if err != nil {
|
||||
return CreateErrorResult("failed to get Istiod registry: " + err.Error())
|
||||
}
|
||||
return CreateToolResult(data, "json")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
// FormatJSONResponse formats a JSON response for better readability
|
||||
func FormatJSONResponse(data []byte) (string, error) {
|
||||
var jsonData interface{}
|
||||
if err := json.Unmarshal(data, &jsonData); err != nil {
|
||||
// If not valid JSON, return as-is
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
formatted, err := json.MarshalIndent(jsonData, "", " ")
|
||||
if err != nil {
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
return string(formatted), nil
|
||||
}
|
||||
|
||||
// CreateToolResult creates a standardized tool result with formatted content
|
||||
func CreateToolResult(data []byte, contentType string) (*mcp.CallToolResult, error) {
|
||||
var content string
|
||||
var err error
|
||||
|
||||
if contentType == "json" || strings.Contains(string(data), "{") {
|
||||
content, err = FormatJSONResponse(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to format JSON response: %w", err)
|
||||
}
|
||||
} else {
|
||||
content = string(data)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: content,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateErrorResult creates an error result for tool calls
|
||||
func CreateErrorResult(message string) (*mcp.CallToolResult, error) {
|
||||
return nil, fmt.Errorf(message)
|
||||
}
|
||||
|
||||
// GetStringParam safely extracts a string parameter from arguments
|
||||
func GetStringParam(arguments map[string]interface{}, key string, defaultValue string) string {
|
||||
if value, ok := arguments[key].(string); ok {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// GetBoolParam safely extracts a boolean parameter from arguments
|
||||
func GetBoolParam(arguments map[string]interface{}, key string, defaultValue bool) bool {
|
||||
if value, ok := arguments[key].(bool); ok {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// ValidateRequiredParams validates that required parameters are present
|
||||
func ValidateRequiredParams(arguments map[string]interface{}, requiredParams []string) error {
|
||||
for _, param := range requiredParams {
|
||||
if _, ok := arguments[param]; !ok {
|
||||
return fmt.Errorf("missing required parameter: %s", param)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateSimpleSchema creates a simple JSON schema for tools with no parameters
|
||||
func CreateSimpleSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": [],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
|
||||
// CreateParameterSchema creates a JSON schema for tools with specific parameters
|
||||
func CreateParameterSchema(properties map[string]interface{}, required []string) json.RawMessage {
|
||||
schema := map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": properties,
|
||||
"required": required,
|
||||
"additionalProperties": false,
|
||||
}
|
||||
|
||||
schemaBytes, _ := json.Marshal(schema)
|
||||
return json.RawMessage(schemaBytes)
|
||||
}
|
||||
2
plugins/golang-filter/mcp-server/servers/higress/nginx-migration/.gitignore
vendored
Normal file
2
plugins/golang-filter/mcp-server/servers/higress/nginx-migration/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# 本地配置文件
|
||||
config/rag.json
|
||||
@@ -0,0 +1,59 @@
|
||||
.PHONY: all build standalone clean test help
|
||||
|
||||
# 默认目标
|
||||
all: standalone
|
||||
|
||||
# build 别名(指向 standalone)
|
||||
build: standalone
|
||||
|
||||
# 编译独立模式
|
||||
standalone:
|
||||
@echo "编译独立模式..."
|
||||
@cd standalone/cmd && go build -o ../../nginx-migration-mcp
|
||||
@echo "独立模式编译完成: ./nginx-migration-mcp"
|
||||
|
||||
# 清理编译产物
|
||||
clean:
|
||||
@echo "清理编译产物..."
|
||||
@rm -f nginx-migration-mcp
|
||||
@echo "清理完成"
|
||||
|
||||
# 运行测试
|
||||
test:
|
||||
@echo "运行测试..."
|
||||
@go test ./...
|
||||
@echo "测试完成"
|
||||
|
||||
|
||||
# 安装依赖
|
||||
deps:
|
||||
@echo "安装依赖..."
|
||||
@go mod tidy
|
||||
@echo "依赖安装完成"
|
||||
|
||||
# 格式化代码
|
||||
fmt:
|
||||
@echo "格式化代码..."
|
||||
@go fmt ./...
|
||||
@echo "代码格式化完成"
|
||||
|
||||
# 显示帮助
|
||||
help:
|
||||
@echo "Nginx Migration MCP Server - Makefile"
|
||||
@echo ""
|
||||
@echo "可用目标:"
|
||||
@echo " make - 编译独立模式(默认)"
|
||||
@echo " make build - 编译独立模式"
|
||||
@echo " make standalone - 编译独立模式"
|
||||
@echo " make test - 运行测试"
|
||||
@echo " make clean - 清理编译产物"
|
||||
@echo " make deps - 安装依赖"
|
||||
@echo " make fmt - 格式化代码"
|
||||
@echo " make help - 显示此帮助信息"
|
||||
@echo ""
|
||||
@echo "目录结构:"
|
||||
@echo " cmd/standalone/ - 独立模式入口"
|
||||
@echo " internal/standalone/ - 独立模式实现"
|
||||
@echo " integration/ - Higress MCP 框架集成"
|
||||
@echo " tools/ - 共享核心逻辑"
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
# Nginx Migration MCP 快速开始
|
||||
|
||||
|
||||
### 1. 构建服务器
|
||||
|
||||
```bash
|
||||
cd /path/to/higress/plugins/golang-filter/mcp-server/servers/higress/nginx-migration
|
||||
make build
|
||||
```
|
||||
|
||||
### 2. 配置 MCP 客户端
|
||||
|
||||
在 MCP 客户端配置文件中添加(以 Cursor 为例):
|
||||
|
||||
**位置**: `~/.cursor/config/mcp_settings.json` 或 Cursor 设置中的 MCP 配置
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"nginx-migration": {
|
||||
"command": "/path/to/nginx-migration/nginx-migration-mcp",
|
||||
"args": []
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 可用工具
|
||||
|
||||
### Nginx 配置转换
|
||||
|
||||
`parse_nginx_config` | 解析和分析 Nginx 配置文件 |
|
||||
`convert_to_higress` | 转换为 Higress HTTPRoute 和 Service |
|
||||
|
||||
### Lua 插件迁移
|
||||
|
||||
|
||||
`convert_lua_to_wasm` | 一键转换 Lua 脚本为 WASM 插件 |
|
||||
`analyze_lua_plugin` | 分析 Lua 插件兼容性 |
|
||||
`generate_conversion_hints` | 生成 API 映射和转换提示 |
|
||||
`validate_wasm_code` | 验证 Go WASM 代码 |
|
||||
`generate_deployment_config` | 生成部署配置包 |
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 示例 1:转换 Nginx 配置
|
||||
|
||||
```
|
||||
我有一个 Nginx 配置,帮我转换为 Higress HTTPRoute
|
||||
```
|
||||
|
||||
HOST LLM 会自动调用 `convert_to_higress` 工具完成转换。
|
||||
|
||||
### 示例 2:快速转换 Lua 插件
|
||||
|
||||
```
|
||||
将这个 Lua 限流插件转换为 Higress WASM 插件:
|
||||
[粘贴 Lua 代码]
|
||||
```
|
||||
|
||||
HOST LLM 会调用 `convert_lua_to_wasm` 工具自动转换。
|
||||
|
||||
### 示例 3:使用工具链精细转换
|
||||
|
||||
```
|
||||
分析这个 Lua 插件的兼容性:
|
||||
[粘贴 Lua 代码]
|
||||
```
|
||||
|
||||
然后按照工具链流程:
|
||||
1. LLM 调用 `analyze_lua_plugin` 分析
|
||||
2. LLM 调用 `generate_conversion_hints` 获取转换提示
|
||||
3. LLM 基于提示生成 Go WASM 代码
|
||||
4. LLM 调用 `validate_wasm_code` 验证代码
|
||||
5. LLM 调用 `generate_deployment_config` 生成部署配置
|
||||
|
||||
## 调试
|
||||
|
||||
启用调试日志:
|
||||
|
||||
```bash
|
||||
DEBUG=true ./nginx-migration-mcp
|
||||
```
|
||||
|
||||
查看工具列表:
|
||||
|
||||
```bash
|
||||
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | ./nginx-migration-mcp
|
||||
```
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
# Nginx Migration MCP Server
|
||||
|
||||
一个用于将 Nginx 配置和 Lua 插件迁移到 Higress 的 MCP 服务器。
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 配置转换工具
|
||||
- **parse_nginx_config** - 解析和分析 Nginx 配置文件
|
||||
- **convert_to_higress** - 将 Nginx 配置转换为 Higress Ingress(主要方式)或 HTTPRoute(可选)
|
||||
|
||||
### Lua 插件迁移工具链
|
||||
|
||||
#### 快速转换模式
|
||||
- **convert_lua_to_wasm** - 一键将 Lua 脚本转换为 WASM 插件
|
||||
|
||||
#### LLM 辅助工具链(精细化控制)
|
||||
1. **analyze_lua_plugin** - 分析 Lua 插件兼容性
|
||||
2. **generate_conversion_hints** - 生成详细的代码转换提示和 API 映射
|
||||
3. **validate_wasm_code** - 验证生成的 Go WASM 代码
|
||||
4. **generate_deployment_config** - 生成完整的部署配置包
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 构建
|
||||
|
||||
```bash
|
||||
make build
|
||||
```
|
||||
|
||||
构建后会生成 `nginx-migration-mcp` 可执行文件。
|
||||
|
||||
### 基础配置(无需知识库)
|
||||
|
||||
**默认模式**:工具可以直接使用,基于内置规则生成转换建议。
|
||||
|
||||
在 MCP 客户端(如 Cursor)的配置文件中添加:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"nginx-migration": {
|
||||
"command": "/path/to/nginx-migration-mcp",
|
||||
"args": []
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 进阶配置(启用 RAG 知识库)
|
||||
|
||||
RAG(检索增强生成)功能通过阿里云百炼集成 Higress 官方文档知识库,提供更准确的转换建议和 API 映射。
|
||||
|
||||
#### 适用场景
|
||||
|
||||
启用 RAG 后,以下工具将获得增强:
|
||||
- **generate_conversion_hints** - 提供基于官方文档的 API 映射和代码示例
|
||||
- **validate_wasm_code** - 基于最佳实践验证代码质量
|
||||
- **query_knowledge_base** - 直接查询 Higress 官方文档
|
||||
|
||||
#### 配置步骤
|
||||
|
||||
**步骤 1:获取阿里云百炼凭证**
|
||||
|
||||
1. 访问 [阿里云百炼控制台](https://bailian.console.aliyun.com/)
|
||||
2. 创建或选择一个应用空间,获取 **业务空间 ID** (`workspace_id`)
|
||||
3. 创建知识库并导入 Higress 文档,获取 **知识库 ID** (`knowledge_base_id`)
|
||||
4. 在 [阿里云 RAM 控制台](https://ram.console.aliyun.com/manage/ak) 创建 AccessKey
|
||||
- 获取 **AccessKey ID** (`access_key_id`)
|
||||
- 获取 **AccessKey Secret** (`access_key_secret`)
|
||||
|
||||
> **安全提示**:请妥善保管 AccessKey,避免泄露。建议使用 RAM 子账号并授予最小权限。
|
||||
|
||||
**步骤 2:复制配置文件**
|
||||
```bash
|
||||
cp config/rag.json.example config/rag.json
|
||||
```
|
||||
|
||||
**步骤 3:编辑配置文件**
|
||||
|
||||
有两种配置方式:
|
||||
|
||||
**方式 1:配置 rag.config**
|
||||
```json
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"rag": {
|
||||
"enabled": true,
|
||||
"provider": "bailian",
|
||||
"endpoint": "bailian.cn-beijing.aliyuncs.com",
|
||||
"workspace_id": "${WORKSPACE_ID}",
|
||||
"knowledge_base_id": "${INDEX_ID}",
|
||||
"access_key_id": "${ALIBABA_CLOUD_ACCESS_KEY_ID}",
|
||||
"access_key_secret": "${ALIBABA_CLOUD_ACCESS_KEY_SECRET}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 高级配置项
|
||||
|
||||
完整的配置选项(可选):
|
||||
|
||||
```json
|
||||
{
|
||||
"rag": {
|
||||
"enabled": true,
|
||||
|
||||
// === 必填:API 配置 ===
|
||||
"provider": "bailian",
|
||||
"endpoint": "bailian.cn-beijing.aliyuncs.com",
|
||||
"workspace_id": "llm-xxx",
|
||||
"knowledge_base_id": "idx-xxx",
|
||||
"access_key_id": "LTAI5t...",
|
||||
"access_key_secret": "your-secret",
|
||||
|
||||
// === 可选:检索配置 ===
|
||||
"context_mode": "full", // 上下文模式: full | summary | highlights
|
||||
"max_context_length": 4000, // 最大上下文长度(字符)
|
||||
"default_top_k": 3, // 默认返回文档数量
|
||||
"similarity_threshold": 0.7, // 相似度阈值(0-1)
|
||||
|
||||
// === 可选:性能配置 ===
|
||||
"enable_cache": true, // 启用查询缓存
|
||||
"cache_ttl": 3600, // 缓存过期时间(秒)
|
||||
"cache_max_size": 1000, // 最大缓存条目数
|
||||
"timeout": 10, // 请求超时(秒)
|
||||
"max_retries": 3, // 最大重试次数
|
||||
"retry_delay": 1, // 重试间隔(秒)
|
||||
|
||||
// === 可选:降级策略 ===
|
||||
"fallback_on_error": true, // RAG 失败时降级到基础模式
|
||||
|
||||
// === 可选:工具级配置 ===
|
||||
"tools": {
|
||||
"generate_conversion_hints": {
|
||||
"use_rag": true, // 为此工具启用 RAG
|
||||
"context_mode": "full",
|
||||
"top_k": 3
|
||||
},
|
||||
"validate_wasm_code": {
|
||||
"use_rag": true,
|
||||
"context_mode": "highlights",
|
||||
"top_k": 2
|
||||
}
|
||||
},
|
||||
|
||||
// === 可选:调试配置 ===
|
||||
"debug": true, // 启用调试日志
|
||||
"log_queries": true // 记录所有查询
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 验证配置
|
||||
|
||||
启动服务后,检查日志输出:
|
||||
|
||||
```bash
|
||||
# 正常启用 RAG
|
||||
✓ RAG enabled: bailian (endpoint: bailian.cn-beijing.aliyuncs.com)
|
||||
✓ Knowledge base: idx-xxx
|
||||
✓ Cache enabled (TTL: 3600s, Max: 1000)
|
||||
|
||||
# RAG 未启用
|
||||
⚠ RAG disabled, using rule-based conversion only
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 转换 Nginx 配置
|
||||
|
||||
使用 `convert_to_higress` 工具,传入 Nginx 配置内容:
|
||||
- **默认**:生成 Kubernetes Ingress 和 Service 资源
|
||||
- **可选**:设置 `use_gateway_api=true` 生成 Gateway API HTTPRoute(需确认已启用)
|
||||
|
||||
|
||||
### 迁移 Lua 插件
|
||||
|
||||
**方式一:快速转换**
|
||||
|
||||
使用 `convert_lua_to_wasm` 工具一键转换 Lua 脚本为 WASM 插件。
|
||||
|
||||
**方式二:AI 辅助工具链**
|
||||
|
||||
1. 使用 `analyze_lua_plugin` 分析 Lua 代码
|
||||
2. 使用 `generate_conversion_hints` 获取转换提示和 API 映射(可启用 RAG 增强)
|
||||
3. AI 根据提示生成 Go WASM 代码
|
||||
4. 使用 `validate_wasm_code` 验证生成的代码(可启用 RAG 增强)
|
||||
5. 使用 `generate_deployment_config` 生成部署配置
|
||||
|
||||
推荐使用工具链方式处理复杂插件,可获得更好的转换质量和 AI 辅助。
|
||||
|
||||
### 查询知识库(需启用 RAG)
|
||||
|
||||
使用 `query_knowledge_base` 工具直接查询 Higress 文档:
|
||||
|
||||
```javascript
|
||||
// 查询 Lua API 迁移方法
|
||||
query_knowledge_base({
|
||||
"query": "ngx.req.get_headers 在 Higress 中怎么实现?",
|
||||
"scenario": "lua_migration",
|
||||
"top_k": 5
|
||||
})
|
||||
|
||||
// 查询插件配置方法
|
||||
query_knowledge_base({
|
||||
"query": "Higress 限流插件配置",
|
||||
"scenario": "config_conversion",
|
||||
"top_k": 3
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
nginx-migration/
|
||||
├── config/ # 配置文件
|
||||
│ ├── rag.json.example # RAG 配置示例
|
||||
│ └── rag.json # RAG 配置(需自行创建)
|
||||
│
|
||||
├── integration/ # Higress 集成模式(MCP 集成)
|
||||
│ ├── server.go # MCP 服务器注册与初始化
|
||||
│ └── mcptools/ # MCP 工具实现
|
||||
│ ├── adapter.go # MCP 工具适配器
|
||||
│ ├── context.go # 迁移上下文管理
|
||||
│ ├── nginx_tools.go # Nginx 配置转换工具
|
||||
│ ├── lua_tools.go # Lua 插件迁移工具
|
||||
│ ├── tool_chain.go # 工具链实现(分析、验证、部署)
|
||||
│ └── rag_integration.go # RAG 知识库集成
|
||||
│
|
||||
├── standalone/ # 独立模式(可独立运行)
|
||||
│ ├── cmd/
|
||||
│ │ └── main.go # 独立模式入口
|
||||
│ ├── server.go # 独立模式 MCP 服务器
|
||||
│ ├── config.go # 配置加载
|
||||
│ └── types.go # 类型定义
|
||||
│
|
||||
├── internal/ # 内部实现包
|
||||
│ ├── rag/ # RAG 功能实现
|
||||
│ │ ├── config.go # RAG 配置结构
|
||||
│ │ ├── client.go # 百炼 API 客户端
|
||||
│ │ └── manager.go # RAG 管理器(查询、缓存)
|
||||
│ └── standalone/ # 独立模式内部实现
|
||||
│ └── server.go # 独立服务器逻辑
|
||||
│
|
||||
├── tools/ # 核心转换逻辑(共享库)
|
||||
│ ├── mcp_tools.go # MCP 工具定义和注册
|
||||
│ ├── nginx_parser.go # Nginx 配置解析器
|
||||
│ ├── lua_converter.go # Lua 到 WASM 转换器
|
||||
│ └── tool_chain.go # 工具链核心实现
|
||||
│
|
||||
├── docs/ # 文档目录
|
||||
│
|
||||
├── mcp-tools.json # MCP 工具元数据定义
|
||||
├── go.mod # Go 模块依赖
|
||||
├── go.sum # Go 模块校验和
|
||||
├── Makefile # 构建脚本
|
||||
│
|
||||
├── README.md # 项目说明文档
|
||||
├── QUICKSTART.md # 快速开始指南
|
||||
├── QUICK_TEST.md # 快速测试指南
|
||||
├── TEST_EXAMPLES.md # 测试示例
|
||||
└── CHANGELOG_INGRESS_PRIORITY.md # Ingress 优先级变更记录
|
||||
```
|
||||
|
||||
### 目录说明
|
||||
|
||||
#### 核心模块
|
||||
|
||||
- **`integration/`** - Higress 集成模式
|
||||
- 作为 Higress MCP 服务器的一部分运行
|
||||
- 提供完整的 MCP 工具集成
|
||||
- 支持 RAG 知识库增强
|
||||
|
||||
- **`standalone/`** - 独立模式
|
||||
- 可独立运行的 MCP 服务器
|
||||
- 适合本地开发和测试
|
||||
- 支持相同的工具功能
|
||||
|
||||
- **`tools/`** - 核心转换逻辑
|
||||
- 独立于运行模式的转换引擎
|
||||
- 包含 Nginx 解析、Lua 转换等核心功能
|
||||
- 可被集成模式和独立模式复用
|
||||
|
||||
- **`internal/rag/`** - RAG 功能实现
|
||||
- 阿里云百炼 API 客户端
|
||||
- 知识库查询和结果处理
|
||||
- 缓存管理和性能优化
|
||||
|
||||
|
||||
#### 配置文件
|
||||
|
||||
- **`config/rag.json`** - RAG 功能配置(需从 example 复制并填写凭证)
|
||||
- **`mcp-tools.json`** - MCP 工具元数据定义(工具描述、参数 schema)
|
||||
|
||||
## 开发
|
||||
|
||||
### 构建命令
|
||||
|
||||
```bash
|
||||
# 编译
|
||||
make build
|
||||
|
||||
# 清理
|
||||
make clean
|
||||
|
||||
# 格式化代码
|
||||
make fmt
|
||||
|
||||
# 运行测试
|
||||
make test
|
||||
|
||||
# 查看帮助
|
||||
make help
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"rag": {
|
||||
"enabled": true,
|
||||
"provider": "bailian",
|
||||
"endpoint": "bailian.cn-beijing.aliyuncs.com",
|
||||
"workspace_id": "${WORKSPACE_ID}",
|
||||
"knowledge_base_id": "${INDEX_ID}",
|
||||
"access_key_id": "${ALIBABA_CLOUD_ACCESS_KEY_ID}",
|
||||
"access_key_secret": "${ALIBABA_CLOUD_ACCESS_KEY_SECRET}",
|
||||
|
||||
"context_mode": "full",
|
||||
"max_context_length": 4000,
|
||||
"default_top_k": 3,
|
||||
"similarity_threshold": 0.7,
|
||||
|
||||
"enable_cache": true,
|
||||
"cache_ttl": 3600,
|
||||
"cache_max_size": 1000,
|
||||
|
||||
"timeout": 10,
|
||||
"max_retries": 3,
|
||||
"retry_delay": 1,
|
||||
|
||||
"fallback_on_error": true,
|
||||
|
||||
"tools": {
|
||||
"generate_conversion_hints": {
|
||||
"use_rag": true,
|
||||
"context_mode": "full",
|
||||
"top_k": 3
|
||||
},
|
||||
"validate_wasm_code": {
|
||||
"use_rag": true,
|
||||
"context_mode": "highlights",
|
||||
"top_k": 2
|
||||
},
|
||||
"convert_lua_to_wasm": {
|
||||
"use_rag": false
|
||||
}
|
||||
},
|
||||
|
||||
"debug": true,
|
||||
"log_queries": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
module nginx-migration-mcp
|
||||
|
||||
go 1.23
|
||||
|
||||
toolchain go1.24.9
|
||||
|
||||
require (
|
||||
github.com/alibaba/higress/plugins/golang-filter v0.0.0-20251023035326-7ea739292dea
|
||||
github.com/alibabacloud-go/bailian-20231229/v2 v2.5.0
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13
|
||||
github.com/alibabacloud-go/tea v1.3.13
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7
|
||||
github.com/envoyproxy/envoy v1.36.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
|
||||
github.com/alibabacloud-go/debug v1.0.1 // indirect
|
||||
github.com/aliyun/credentials-go v1.4.5 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/go-redis/redis/v8 v8.11.5 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/mark3labs/mcp-go v0.12.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
)
|
||||
@@ -0,0 +1,60 @@
|
||||
//go:build higress_integration
|
||||
// +build higress_integration
|
||||
|
||||
package mcptools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
// SimpleToolHandler is a simplified handler function that takes arguments and returns a string result
|
||||
type SimpleToolHandler func(args map[string]interface{}) (string, error)
|
||||
|
||||
// AdaptSimpleHandler converts a SimpleToolHandler to an MCP ToolHandlerFunc
|
||||
func AdaptSimpleHandler(handler SimpleToolHandler) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
// Extract arguments
|
||||
var args map[string]interface{}
|
||||
if request.Params.Arguments != nil {
|
||||
args = request.Params.Arguments
|
||||
} else {
|
||||
args = make(map[string]interface{})
|
||||
}
|
||||
|
||||
// Call the simple handler
|
||||
result, err := handler(args)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("tool execution failed: %w", err)
|
||||
}
|
||||
|
||||
// Return MCP result
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: result,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterSimpleTool registers a tool with a simplified handler
|
||||
func RegisterSimpleTool(
|
||||
server *common.MCPServer,
|
||||
name string,
|
||||
description string,
|
||||
inputSchema map[string]interface{},
|
||||
handler SimpleToolHandler,
|
||||
) {
|
||||
// Create tool with schema
|
||||
schemaBytes, _ := json.Marshal(inputSchema)
|
||||
|
||||
tool := mcp.NewToolWithRawSchema(name, description, schemaBytes)
|
||||
server.AddTool(tool, AdaptSimpleHandler(handler))
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
//go:build higress_integration
|
||||
// +build higress_integration
|
||||
|
||||
package mcptools
|
||||
|
||||
import (
|
||||
"log"
|
||||
"nginx-migration-mcp/internal/rag"
|
||||
)
|
||||
|
||||
// MigrationContext holds the configuration context for migration operations
|
||||
type MigrationContext struct {
|
||||
GatewayName string
|
||||
GatewayNamespace string
|
||||
DefaultNamespace string
|
||||
DefaultHostname string
|
||||
RoutePrefix string
|
||||
ServicePort int
|
||||
TargetPort int
|
||||
RAGManager *rag.RAGManager // RAG 管理器
|
||||
}
|
||||
|
||||
// NewDefaultMigrationContext creates a MigrationContext with default values
|
||||
func NewDefaultMigrationContext() *MigrationContext {
|
||||
return &MigrationContext{
|
||||
GatewayName: "higress-gateway",
|
||||
GatewayNamespace: "higress-system",
|
||||
DefaultNamespace: "default",
|
||||
DefaultHostname: "example.com",
|
||||
RoutePrefix: "nginx-migrated",
|
||||
ServicePort: 80,
|
||||
TargetPort: 8080,
|
||||
}
|
||||
}
|
||||
|
||||
// NewMigrationContextWithRAG creates a MigrationContext with RAG support
|
||||
func NewMigrationContextWithRAG(ragConfigPath string) *MigrationContext {
|
||||
ctx := NewDefaultMigrationContext()
|
||||
|
||||
// 加载 RAG 配置
|
||||
config, err := rag.LoadRAGConfig(ragConfigPath)
|
||||
if err != nil {
|
||||
log.Printf("⚠️ Failed to load RAG config: %v, RAG will be disabled", err)
|
||||
config = &rag.RAGConfig{Enabled: false}
|
||||
}
|
||||
|
||||
// 创建 RAG 管理器
|
||||
ctx.RAGManager = rag.NewRAGManager(config)
|
||||
|
||||
if ctx.RAGManager.IsEnabled() {
|
||||
log.Println("✅ MigrationContext: RAG enabled")
|
||||
} else {
|
||||
log.Println("📖 MigrationContext: RAG disabled, using rule-based approach")
|
||||
}
|
||||
|
||||
return ctx
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
//go:build higress_integration
|
||||
// +build higress_integration
|
||||
|
||||
package mcptools
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"nginx-migration-mcp/internal/rag"
|
||||
"nginx-migration-mcp/tools"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
)
|
||||
|
||||
// RegisterLuaPluginTools registers Lua plugin analysis and conversion tools
|
||||
func RegisterLuaPluginTools(server *common.MCPServer, ctx *MigrationContext) {
|
||||
RegisterSimpleTool(
|
||||
server,
|
||||
"analyze_lua_plugin",
|
||||
"分析 Nginx Lua 插件的兼容性,识别使用的 API 和潜在迁移问题",
|
||||
map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"lua_code": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "Nginx Lua 插件代码",
|
||||
},
|
||||
},
|
||||
"required": []string{"lua_code"},
|
||||
},
|
||||
func(args map[string]interface{}) (string, error) {
|
||||
return analyzeLuaPlugin(args, ctx)
|
||||
},
|
||||
)
|
||||
|
||||
RegisterSimpleTool(
|
||||
server,
|
||||
"convert_lua_to_wasm",
|
||||
"一键将 Nginx Lua 脚本转换为 Higress WASM 插件,自动生成 Go 代码和配置",
|
||||
map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"lua_code": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "要转换的 Nginx Lua 插件代码",
|
||||
},
|
||||
"plugin_name": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "生成的 WASM 插件名称 (小写字母和连字符)",
|
||||
},
|
||||
},
|
||||
"required": []string{"lua_code", "plugin_name"},
|
||||
},
|
||||
func(args map[string]interface{}) (string, error) {
|
||||
return convertLuaToWasm(args, ctx)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func analyzeLuaPlugin(args map[string]interface{}, ctx *MigrationContext) (string, error) {
|
||||
luaCode, ok := args["lua_code"].(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("missing or invalid lua_code parameter")
|
||||
}
|
||||
|
||||
// Analyze Lua features (基于规则)
|
||||
features := []string{}
|
||||
warnings := []string{}
|
||||
detectedAPIs := []string{}
|
||||
|
||||
if strings.Contains(luaCode, "ngx.var") {
|
||||
features = append(features, "- ngx.var - Nginx变量")
|
||||
detectedAPIs = append(detectedAPIs, "ngx.var")
|
||||
}
|
||||
if strings.Contains(luaCode, "ngx.req") {
|
||||
features = append(features, "- ngx.req - 请求API")
|
||||
detectedAPIs = append(detectedAPIs, "ngx.req")
|
||||
}
|
||||
if strings.Contains(luaCode, "ngx.exit") {
|
||||
features = append(features, "- ngx.exit - 请求终止")
|
||||
detectedAPIs = append(detectedAPIs, "ngx.exit")
|
||||
}
|
||||
if strings.Contains(luaCode, "ngx.shared") {
|
||||
features = append(features, "- ngx.shared - 共享字典 (警告)")
|
||||
warnings = append(warnings, "共享字典需要外部缓存替换")
|
||||
detectedAPIs = append(detectedAPIs, "ngx.shared")
|
||||
}
|
||||
if strings.Contains(luaCode, "ngx.location.capture") {
|
||||
features = append(features, "- ngx.location.capture - 内部请求 (警告)")
|
||||
warnings = append(warnings, "需要改为HTTP客户端调用")
|
||||
detectedAPIs = append(detectedAPIs, "ngx.location.capture")
|
||||
}
|
||||
|
||||
compatibility := "full"
|
||||
if len(warnings) > 0 {
|
||||
compatibility = "partial"
|
||||
}
|
||||
if len(warnings) > 2 {
|
||||
compatibility = "manual"
|
||||
}
|
||||
|
||||
// === RAG 增强:查询知识库获取转换建议 ===
|
||||
var ragContext *rag.RAGContext
|
||||
if ctx.RAGManager != nil && ctx.RAGManager.IsEnabled() && len(detectedAPIs) > 0 {
|
||||
query := fmt.Sprintf("Nginx Lua API %s 在 Higress WASM 中的转换方法和最佳实践", strings.Join(detectedAPIs, ", "))
|
||||
var err error
|
||||
ragContext, err = ctx.RAGManager.QueryForTool("analyze_lua_plugin", query, "lua_migration")
|
||||
if err != nil {
|
||||
log.Printf("⚠️ RAG query failed for analyze_lua_plugin: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 构建结果
|
||||
var result strings.Builder
|
||||
|
||||
// RAG 上下文(如果有)
|
||||
if ragContext != nil && ragContext.Enabled && len(ragContext.Documents) > 0 {
|
||||
result.WriteString("📚 知识库参考资料:\n\n")
|
||||
result.WriteString(ragContext.FormatContextForAI())
|
||||
result.WriteString("\n")
|
||||
}
|
||||
|
||||
// 基于规则的分析
|
||||
warningsText := "无"
|
||||
if len(warnings) > 0 {
|
||||
warningsText = strings.Join(warnings, "\n")
|
||||
}
|
||||
|
||||
result.WriteString(fmt.Sprintf(`Lua插件兼容性分析
|
||||
|
||||
检测特性:
|
||||
%s
|
||||
|
||||
兼容性警告:
|
||||
%s
|
||||
|
||||
兼容性级别: %s
|
||||
|
||||
迁移建议:`, strings.Join(features, "\n"), warningsText, compatibility))
|
||||
|
||||
switch compatibility {
|
||||
case "full":
|
||||
result.WriteString("\n- 可直接迁移到WASM插件")
|
||||
case "partial":
|
||||
result.WriteString("\n- 需要部分重构")
|
||||
case "manual":
|
||||
result.WriteString("\n- 需要手动重写")
|
||||
}
|
||||
|
||||
return result.String(), nil
|
||||
}
|
||||
|
||||
func convertLuaToWasm(args map[string]interface{}, ctx *MigrationContext) (string, error) {
|
||||
luaCode, ok := args["lua_code"].(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("missing or invalid lua_code parameter")
|
||||
}
|
||||
|
||||
pluginName, ok := args["plugin_name"].(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("missing or invalid plugin_name parameter")
|
||||
}
|
||||
|
||||
// 分析Lua脚本
|
||||
analyzer := tools.AnalyzeLuaScript(luaCode)
|
||||
|
||||
// === RAG 增强:查询转换模式和代码示例 ===
|
||||
var ragContext *rag.RAGContext
|
||||
if ctx.RAGManager != nil && ctx.RAGManager.IsEnabled() && len(analyzer.Features) > 0 {
|
||||
// 提取特性列表
|
||||
featureList := []string{}
|
||||
for feature := range analyzer.Features {
|
||||
featureList = append(featureList, feature)
|
||||
}
|
||||
|
||||
query := fmt.Sprintf("将使用了 %s 的 Nginx Lua 插件转换为 Higress WASM Go 插件的代码示例",
|
||||
strings.Join(featureList, ", "))
|
||||
var err error
|
||||
ragContext, err = ctx.RAGManager.QueryForTool("convert_lua_to_wasm", query, "lua_to_wasm")
|
||||
if err != nil {
|
||||
log.Printf("⚠️ RAG query failed for convert_lua_to_wasm: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 转换为WASM插件
|
||||
result, err := tools.ConvertLuaToWasm(analyzer, pluginName)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("conversion failed: %w", err)
|
||||
}
|
||||
|
||||
// 构建响应
|
||||
var response strings.Builder
|
||||
|
||||
// RAG 上下文(如果有)
|
||||
if ragContext != nil && ragContext.Enabled && len(ragContext.Documents) > 0 {
|
||||
response.WriteString("📚 知识库代码示例:\n\n")
|
||||
response.WriteString(ragContext.FormatContextForAI())
|
||||
response.WriteString("\n---\n\n")
|
||||
}
|
||||
|
||||
response.WriteString(fmt.Sprintf(`Go 代码:
|
||||
%s
|
||||
|
||||
WasmPlugin 配置:
|
||||
%s
|
||||
|
||||
复杂度: %s, 特性: %d, 警告: %d`,
|
||||
result.GoCode,
|
||||
result.WasmPluginYAML,
|
||||
analyzer.Complexity,
|
||||
len(analyzer.Features),
|
||||
len(analyzer.Warnings)))
|
||||
|
||||
return response.String(), nil
|
||||
}
|
||||
@@ -0,0 +1,491 @@
|
||||
//go:build higress_integration
|
||||
// +build higress_integration
|
||||
|
||||
package mcptools
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"nginx-migration-mcp/internal/rag"
|
||||
"nginx-migration-mcp/tools"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
)
|
||||
|
||||
// RegisterNginxConfigTools 注册 Nginx 配置分析和转换工具
|
||||
func RegisterNginxConfigTools(server *common.MCPServer, ctx *MigrationContext) {
|
||||
RegisterSimpleTool(
|
||||
server,
|
||||
"parse_nginx_config",
|
||||
"解析和分析 Nginx 配置文件,识别配置结构和复杂度",
|
||||
map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"config_content": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "Nginx 配置文件内容",
|
||||
},
|
||||
},
|
||||
"required": []string{"config_content"},
|
||||
},
|
||||
func(args map[string]interface{}) (string, error) {
|
||||
return parseNginxConfig(args, ctx)
|
||||
},
|
||||
)
|
||||
|
||||
RegisterSimpleTool(
|
||||
server,
|
||||
"convert_to_higress",
|
||||
"将 Nginx 配置转换为 Higress Ingress 和 Service 资源(主要方式)或 HTTPRoute(可选)",
|
||||
map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"config_content": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "Nginx 配置文件内容",
|
||||
},
|
||||
"namespace": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "目标 Kubernetes 命名空间",
|
||||
"default": "default",
|
||||
},
|
||||
"use_gateway_api": map[string]interface{}{
|
||||
"type": "boolean",
|
||||
"description": "是否使用 Gateway API (HTTPRoute)。默认 false,使用 Ingress",
|
||||
"default": false,
|
||||
},
|
||||
},
|
||||
"required": []string{"config_content"},
|
||||
},
|
||||
func(args map[string]interface{}) (string, error) {
|
||||
return convertToHigress(args, ctx)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func parseNginxConfig(args map[string]interface{}, ctx *MigrationContext) (string, error) {
|
||||
configContent, ok := args["config_content"].(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("missing or invalid config_content parameter")
|
||||
}
|
||||
|
||||
// Simple analysis
|
||||
serverCount := strings.Count(configContent, "server {")
|
||||
locationCount := strings.Count(configContent, "location")
|
||||
hasSSL := strings.Contains(configContent, "ssl")
|
||||
hasProxy := strings.Contains(configContent, "proxy_pass")
|
||||
hasRewrite := strings.Contains(configContent, "rewrite")
|
||||
|
||||
complexity := "Simple"
|
||||
if serverCount > 1 || (hasRewrite && hasSSL) {
|
||||
complexity = "Complex"
|
||||
} else if hasRewrite || hasSSL {
|
||||
complexity = "Medium"
|
||||
}
|
||||
|
||||
// 收集配置特性用于 RAG 查询
|
||||
features := []string{}
|
||||
if hasProxy {
|
||||
features = append(features, "反向代理")
|
||||
}
|
||||
if hasRewrite {
|
||||
features = append(features, "URL重写")
|
||||
}
|
||||
if hasSSL {
|
||||
features = append(features, "SSL配置")
|
||||
}
|
||||
|
||||
// === RAG 增强:查询 Nginx 配置迁移最佳实践 ===
|
||||
var ragContext *rag.RAGContext
|
||||
if ctx.RAGManager != nil && ctx.RAGManager.IsEnabled() && len(features) > 0 {
|
||||
query := fmt.Sprintf("Nginx %s 迁移到 Higress 的配置方法和最佳实践", strings.Join(features, "、"))
|
||||
var err error
|
||||
ragContext, err = ctx.RAGManager.QueryForTool("parse_nginx_config", query, "nginx_migration")
|
||||
if err != nil {
|
||||
log.Printf(" RAG query failed for parse_nginx_config: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 构建分析结果
|
||||
var result strings.Builder
|
||||
|
||||
// RAG 上下文(如果有)
|
||||
if ragContext != nil && ragContext.Enabled && len(ragContext.Documents) > 0 {
|
||||
result.WriteString("📚 知识库迁移指南:\n\n")
|
||||
result.WriteString(ragContext.FormatContextForAI())
|
||||
result.WriteString("\n---\n\n")
|
||||
}
|
||||
|
||||
result.WriteString(fmt.Sprintf(`Nginx配置分析结果
|
||||
|
||||
基础信息:
|
||||
- Server块: %d个
|
||||
- Location块: %d个
|
||||
- SSL配置: %t
|
||||
- 反向代理: %t
|
||||
- URL重写: %t
|
||||
|
||||
复杂度: %s
|
||||
|
||||
迁移建议:`, serverCount, locationCount, hasSSL, hasProxy, hasRewrite, complexity))
|
||||
|
||||
if hasProxy {
|
||||
result.WriteString("\n- 反向代理将转换为Ingress backend配置")
|
||||
}
|
||||
if hasRewrite {
|
||||
result.WriteString("\n- URL重写将使用Higress注解 (higress.io/rewrite-target)")
|
||||
}
|
||||
if hasSSL {
|
||||
result.WriteString("\n- SSL配置将转换为Ingress TLS配置")
|
||||
}
|
||||
|
||||
return result.String(), nil
|
||||
}
|
||||
|
||||
func convertToHigress(args map[string]interface{}, ctx *MigrationContext) (string, error) {
|
||||
configContent, ok := args["config_content"].(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("missing or invalid config_content parameter")
|
||||
}
|
||||
|
||||
namespace := ctx.DefaultNamespace
|
||||
if ns, ok := args["namespace"].(string); ok && ns != "" {
|
||||
namespace = ns
|
||||
}
|
||||
|
||||
// 检查是否使用 Gateway API
|
||||
useGatewayAPI := false
|
||||
if val, ok := args["use_gateway_api"].(bool); ok {
|
||||
useGatewayAPI = val
|
||||
}
|
||||
|
||||
// === 使用增强的解析器解析 Nginx 配置 ===
|
||||
nginxConfig, err := tools.ParseNginxConfig(configContent)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse Nginx config: %v", err)
|
||||
}
|
||||
|
||||
// 分析配置
|
||||
analysis := tools.AnalyzeNginxConfig(nginxConfig)
|
||||
|
||||
// === RAG 增强:查询转换示例和最佳实践 ===
|
||||
var ragContext string
|
||||
if ctx.RAGManager != nil && ctx.RAGManager.IsEnabled() {
|
||||
// 构建查询关键词
|
||||
queryBuilder := []string{"Nginx 配置转换到 Higress"}
|
||||
|
||||
if useGatewayAPI {
|
||||
queryBuilder = append(queryBuilder, "Gateway API HTTPRoute")
|
||||
} else {
|
||||
queryBuilder = append(queryBuilder, "Kubernetes Ingress")
|
||||
}
|
||||
|
||||
// 根据特性添加查询关键词
|
||||
if analysis.Features["ssl"] {
|
||||
queryBuilder = append(queryBuilder, "SSL TLS 证书配置")
|
||||
}
|
||||
if analysis.Features["rewrite"] {
|
||||
queryBuilder = append(queryBuilder, "URL 重写 rewrite 规则")
|
||||
}
|
||||
if analysis.Features["redirect"] {
|
||||
queryBuilder = append(queryBuilder, "重定向 redirect")
|
||||
}
|
||||
if analysis.Features["header_manipulation"] {
|
||||
queryBuilder = append(queryBuilder, "请求头 响应头处理")
|
||||
}
|
||||
if len(nginxConfig.Upstreams) > 0 {
|
||||
queryBuilder = append(queryBuilder, "负载均衡 upstream")
|
||||
}
|
||||
|
||||
queryString := strings.Join(queryBuilder, " ")
|
||||
log.Printf("🔍 RAG Query: %s", queryString)
|
||||
|
||||
ragResult, err := ctx.RAGManager.QueryForTool(
|
||||
"convert_to_higress",
|
||||
queryString,
|
||||
"nginx_to_higress",
|
||||
)
|
||||
|
||||
if err == nil && ragResult.Enabled && len(ragResult.Documents) > 0 {
|
||||
log.Printf("✅ RAG: Found %d documents for conversion", len(ragResult.Documents))
|
||||
ragContext = "\n\n## 📚 参考文档(来自知识库)\n\n" + ragResult.FormatContextForAI()
|
||||
} else {
|
||||
if err != nil {
|
||||
log.Printf("⚠️ RAG query failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === 将配置数据转换为 JSON 供 AI 使用 ===
|
||||
configJSON, _ := json.MarshalIndent(nginxConfig, "", " ")
|
||||
analysisJSON, _ := json.MarshalIndent(analysis, "", " ")
|
||||
|
||||
// === 构建返回消息 ===
|
||||
var result strings.Builder
|
||||
|
||||
result.WriteString(fmt.Sprintf(`📋 Nginx 配置解析完成
|
||||
|
||||
## 配置概览
|
||||
- Server 块: %d
|
||||
- Location 块: %d
|
||||
- 域名: %d 个
|
||||
- 复杂度: %s
|
||||
- 目标格式: %s
|
||||
- 命名空间: %s
|
||||
|
||||
## 检测到的特性
|
||||
%s
|
||||
|
||||
## 迁移建议
|
||||
%s
|
||||
%s
|
||||
|
||||
---
|
||||
|
||||
## Nginx 配置结构
|
||||
|
||||
`+"```json"+`
|
||||
%s
|
||||
`+"```"+`
|
||||
|
||||
## 分析结果
|
||||
|
||||
`+"```json"+`
|
||||
%s
|
||||
`+"```"+`
|
||||
%s
|
||||
`,
|
||||
analysis.ServerCount,
|
||||
analysis.LocationCount,
|
||||
analysis.DomainCount,
|
||||
analysis.Complexity,
|
||||
func() string {
|
||||
if useGatewayAPI {
|
||||
return "Gateway API (HTTPRoute)"
|
||||
}
|
||||
return "Kubernetes Ingress"
|
||||
}(),
|
||||
namespace,
|
||||
formatFeaturesForOutput(analysis.Features),
|
||||
formatSuggestionsForOutput(analysis.Suggestions),
|
||||
func() string {
|
||||
if ragContext != "" {
|
||||
return "\n\n✅ 已加载知识库参考文档"
|
||||
}
|
||||
return ""
|
||||
}(),
|
||||
string(configJSON),
|
||||
string(analysisJSON),
|
||||
ragContext,
|
||||
))
|
||||
|
||||
return result.String(), nil
|
||||
}
|
||||
|
||||
// generateIngressConfig 生成 Ingress 资源配置(主要方式)
|
||||
func generateIngressConfig(ingressName, namespace, hostname, serviceName string, ctx *MigrationContext) string {
|
||||
return fmt.Sprintf(`转换后的Higress配置(使用 Ingress - 推荐方式)
|
||||
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: %s
|
||||
namespace: %s
|
||||
annotations:
|
||||
higress.io/migrated-from: "nginx"
|
||||
higress.io/ingress.class: "higress"
|
||||
spec:
|
||||
ingressClassName: higress
|
||||
rules:
|
||||
- host: %s
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: %s
|
||||
port:
|
||||
number: %d
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: %s
|
||||
namespace: %s
|
||||
spec:
|
||||
selector:
|
||||
app: backend
|
||||
ports:
|
||||
- port: %d
|
||||
targetPort: %d
|
||||
protocol: TCP
|
||||
|
||||
转换完成
|
||||
|
||||
应用步骤:
|
||||
1. 保存为 higress-config.yaml
|
||||
2. 执行: kubectl apply -f higress-config.yaml
|
||||
3. 验证: kubectl get ingress -n %s
|
||||
|
||||
说明:
|
||||
- 使用 Ingress 是 Higress 的主要使用方式,兼容性最好
|
||||
- 如需使用 Gateway API (HTTPRoute),请设置参数 use_gateway_api=true`,
|
||||
ingressName, namespace,
|
||||
hostname,
|
||||
serviceName, ctx.ServicePort,
|
||||
serviceName, namespace,
|
||||
ctx.ServicePort, ctx.TargetPort,
|
||||
namespace)
|
||||
}
|
||||
|
||||
// generateHTTPRouteConfig 生成 HTTPRoute 资源配置(备用选项)
|
||||
func generateHTTPRouteConfig(routeName, namespace, hostname, serviceName string, ctx *MigrationContext) string {
|
||||
return fmt.Sprintf(`转换后的Higress配置(使用 Gateway API - 可选方式)
|
||||
|
||||
注意: Gateway API 在 Higress 中默认关闭,使用前需要确认已启用。
|
||||
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: %s
|
||||
namespace: %s
|
||||
annotations:
|
||||
higress.io/migrated-from: "nginx"
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: %s
|
||||
namespace: %s
|
||||
hostnames:
|
||||
- %s
|
||||
rules:
|
||||
- matches:
|
||||
- path:
|
||||
type: PathPrefix
|
||||
value: /
|
||||
backendRefs:
|
||||
- name: %s
|
||||
port: %d
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: %s
|
||||
namespace: %s
|
||||
spec:
|
||||
selector:
|
||||
app: backend
|
||||
ports:
|
||||
- port: %d
|
||||
targetPort: %d
|
||||
protocol: TCP
|
||||
|
||||
转换完成
|
||||
|
||||
应用步骤:
|
||||
1. 确认 Gateway API 已启用: PILOT_ENABLE_GATEWAY_API=true
|
||||
2. 保存为 higress-config.yaml
|
||||
3. 执行: kubectl apply -f higress-config.yaml
|
||||
4. 验证: kubectl get httproute -n %s
|
||||
|
||||
说明:
|
||||
- Gateway API 是可选功能,默认关闭
|
||||
- 推荐使用 Ingress (设置 use_gateway_api=false)`,
|
||||
routeName, namespace,
|
||||
ctx.GatewayName, ctx.GatewayNamespace, hostname,
|
||||
serviceName, ctx.ServicePort,
|
||||
serviceName, namespace,
|
||||
ctx.ServicePort, ctx.TargetPort,
|
||||
namespace)
|
||||
}
|
||||
|
||||
func generateIngressName(hostname string, ctx *MigrationContext) string {
|
||||
prefix := "nginx-migrated"
|
||||
if ctx.RoutePrefix != "" {
|
||||
prefix = ctx.RoutePrefix
|
||||
}
|
||||
|
||||
if hostname == "" || hostname == ctx.DefaultHostname {
|
||||
return fmt.Sprintf("%s-ingress", prefix)
|
||||
}
|
||||
// Replace dots and special characters for valid k8s name
|
||||
safeName := hostname
|
||||
for _, char := range []string{".", "_", ":"} {
|
||||
safeName = strings.ReplaceAll(safeName, char, "-")
|
||||
}
|
||||
return fmt.Sprintf("%s-%s", prefix, safeName)
|
||||
}
|
||||
|
||||
func generateRouteName(hostname string, ctx *MigrationContext) string {
|
||||
prefix := "nginx-migrated"
|
||||
if ctx.RoutePrefix != "" {
|
||||
prefix = ctx.RoutePrefix
|
||||
}
|
||||
|
||||
if hostname == "" || hostname == ctx.DefaultHostname {
|
||||
return fmt.Sprintf("%s-route", prefix)
|
||||
}
|
||||
// Replace dots and special characters for valid k8s name
|
||||
safeName := hostname
|
||||
for _, char := range []string{".", "_", ":"} {
|
||||
safeName = strings.ReplaceAll(safeName, char, "-")
|
||||
}
|
||||
return fmt.Sprintf("%s-%s", prefix, safeName)
|
||||
}
|
||||
|
||||
func generateServiceName(hostname string, ctx *MigrationContext) string {
|
||||
if hostname == "" || hostname == ctx.DefaultHostname {
|
||||
return "backend-service"
|
||||
}
|
||||
safeName := hostname
|
||||
for _, char := range []string{".", "_", ":"} {
|
||||
safeName = strings.ReplaceAll(safeName, char, "-")
|
||||
}
|
||||
return fmt.Sprintf("%s-service", safeName)
|
||||
}
|
||||
|
||||
// formatFeaturesForOutput 格式化特性列表用于输出
|
||||
func formatFeaturesForOutput(features map[string]bool) string {
|
||||
featureNames := map[string]string{
|
||||
"ssl": "SSL/TLS 加密",
|
||||
"proxy": "反向代理",
|
||||
"rewrite": "URL 重写",
|
||||
"redirect": "重定向",
|
||||
"return": "返回指令",
|
||||
"complex_routing": "复杂路由匹配",
|
||||
"header_manipulation": "请求头操作",
|
||||
"response_headers": "响应头操作",
|
||||
}
|
||||
|
||||
var result []string
|
||||
for key, enabled := range features {
|
||||
if enabled {
|
||||
if name, ok := featureNames[key]; ok {
|
||||
result = append(result, fmt.Sprintf("- ✅ %s", name))
|
||||
} else {
|
||||
result = append(result, fmt.Sprintf("- ✅ %s", key))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(result) == 0 {
|
||||
return "- 基础配置(无特殊特性)"
|
||||
}
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
|
||||
// formatSuggestionsForOutput 格式化建议列表用于输出
|
||||
func formatSuggestionsForOutput(suggestions []string) string {
|
||||
if len(suggestions) == 0 {
|
||||
return "- 无特殊建议"
|
||||
}
|
||||
var result []string
|
||||
for _, s := range suggestions {
|
||||
result = append(result, fmt.Sprintf("- 💡 %s", s))
|
||||
}
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
// Package mcptools 提供 RAG 集成到 MCP 工具的示例实现
|
||||
package mcptools
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"nginx-migration-mcp/internal/rag"
|
||||
)
|
||||
|
||||
// RAGToolContext MCP 工具的 RAG 上下文
|
||||
type RAGToolContext struct {
|
||||
Manager *rag.RAGManager
|
||||
}
|
||||
|
||||
// NewRAGToolContext 创建 RAG 工具上下文
|
||||
func NewRAGToolContext(configPath string) (*RAGToolContext, error) {
|
||||
// 加载配置
|
||||
config, err := rag.LoadRAGConfig(configPath)
|
||||
if err != nil {
|
||||
log.Printf("⚠️ Failed to load RAG config: %v, RAG will be disabled", err)
|
||||
// 创建禁用状态的配置
|
||||
config = &rag.RAGConfig{Enabled: false}
|
||||
}
|
||||
|
||||
// 创建 RAG 管理器
|
||||
manager := rag.NewRAGManager(config)
|
||||
|
||||
return &RAGToolContext{
|
||||
Manager: manager,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ==================== 工具示例:generate_conversion_hints ====================
|
||||
|
||||
// GenerateConversionHintsWithRAG 生成转换提示(带 RAG 增强)
|
||||
func (ctx *RAGToolContext) GenerateConversionHintsWithRAG(analysisResult string, pluginName string) (string, error) {
|
||||
var result strings.Builder
|
||||
result.WriteString(fmt.Sprintf("# %s 插件转换指南\n\n", pluginName))
|
||||
|
||||
// 提取 Nginx APIs(这里简化处理)
|
||||
nginxAPIs := extractNginxAPIs(analysisResult)
|
||||
|
||||
// === 核心:使用工具级别的 RAG 查询 ===
|
||||
toolName := "generate_conversion_hints"
|
||||
ragContext, err := ctx.Manager.QueryForTool(
|
||||
toolName,
|
||||
fmt.Sprintf("Nginx Lua API %s 在 Higress WASM 中的实现和转换方法", strings.Join(nginxAPIs, ", ")),
|
||||
"lua_migration",
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("❌ RAG query failed for %s: %v", toolName, err)
|
||||
// 降级到规则生成
|
||||
ragContext = &rag.RAGContext{
|
||||
Enabled: false,
|
||||
Message: fmt.Sprintf("RAG query failed: %v", err),
|
||||
}
|
||||
}
|
||||
|
||||
// 添加 RAG 上下文信息(如果有)
|
||||
if ragContext.Enabled && len(ragContext.Documents) > 0 {
|
||||
result.WriteString(ragContext.FormatContextForAI())
|
||||
} else {
|
||||
// RAG 未启用或查询失败
|
||||
result.WriteString(fmt.Sprintf("> ℹ️ %s\n\n", ragContext.Message))
|
||||
result.WriteString("> 使用基于规则的转换指南\n\n")
|
||||
}
|
||||
|
||||
// 为每个 API 生成转换提示(基于规则)
|
||||
result.WriteString("## 🔄 API 转换详情\n\n")
|
||||
for _, api := range nginxAPIs {
|
||||
result.WriteString(fmt.Sprintf("### %s\n\n", api))
|
||||
result.WriteString(generateBasicMapping(api))
|
||||
result.WriteString("\n")
|
||||
}
|
||||
|
||||
// 添加使用建议
|
||||
result.WriteString("\n---\n\n")
|
||||
result.WriteString("## 💡 使用建议\n\n")
|
||||
if ragContext.Enabled {
|
||||
result.WriteString("✅ 上述参考文档来自 Higress 官方知识库,请参考这些文档中的示例代码和最佳实践来生成 WASM 插件代码。\n\n")
|
||||
result.WriteString("建议按照知识库中的示例实现,确保代码符合 Higress 的最佳实践。\n")
|
||||
} else {
|
||||
result.WriteString("ℹ️ 当前未启用 RAG 知识库或查询失败,使用基于规则的映射。\n\n")
|
||||
result.WriteString("建议参考 Higress 官方文档:https://higress.cn/docs/plugins/wasm-go-sdk/\n")
|
||||
}
|
||||
|
||||
return result.String(), nil
|
||||
}
|
||||
|
||||
// ==================== 工具示例:validate_wasm_code ====================
|
||||
|
||||
// ValidateWasmCodeWithRAG 验证 WASM 代码(带 RAG 增强)
|
||||
func (ctx *RAGToolContext) ValidateWasmCodeWithRAG(goCode string, pluginName string) (string, error) {
|
||||
var result strings.Builder
|
||||
result.WriteString(fmt.Sprintf("## 🔍 %s 插件代码验证报告\n\n", pluginName))
|
||||
|
||||
// 基本验证(始终执行)
|
||||
basicIssues := validateBasicSyntax(goCode)
|
||||
apiIssues := validateAPIUsage(goCode)
|
||||
|
||||
if len(basicIssues) > 0 {
|
||||
result.WriteString("### ⚠️ 语法问题\n\n")
|
||||
for _, issue := range basicIssues {
|
||||
result.WriteString(fmt.Sprintf("- %s\n", issue))
|
||||
}
|
||||
result.WriteString("\n")
|
||||
}
|
||||
|
||||
if len(apiIssues) > 0 {
|
||||
result.WriteString("### ⚠️ API 使用问题\n\n")
|
||||
for _, issue := range apiIssues {
|
||||
result.WriteString(fmt.Sprintf("- %s\n", issue))
|
||||
}
|
||||
result.WriteString("\n")
|
||||
}
|
||||
|
||||
// === RAG 增强:查询最佳实践 ===
|
||||
toolName := "validate_wasm_code"
|
||||
ragContext, err := ctx.Manager.QueryForTool(
|
||||
toolName,
|
||||
"Higress WASM 插件开发最佳实践 错误处理 性能优化 代码规范",
|
||||
"best_practice",
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("❌ RAG query failed for %s: %v", toolName, err)
|
||||
ragContext = &rag.RAGContext{
|
||||
Enabled: false,
|
||||
Message: fmt.Sprintf("RAG query failed: %v", err),
|
||||
}
|
||||
}
|
||||
|
||||
// 添加最佳实践建议
|
||||
if ragContext.Enabled && len(ragContext.Documents) > 0 {
|
||||
result.WriteString("### 💡 最佳实践建议(基于知识库)\n\n")
|
||||
|
||||
for i, doc := range ragContext.Documents {
|
||||
result.WriteString(fmt.Sprintf("#### 建议 %d:%s\n\n", i+1, doc.Title))
|
||||
result.WriteString(fmt.Sprintf("**来源**: %s \n", doc.Source))
|
||||
if doc.URL != "" {
|
||||
result.WriteString(fmt.Sprintf("**链接**: %s \n", doc.URL))
|
||||
}
|
||||
result.WriteString("\n")
|
||||
|
||||
// 只展示关键片段(validate 工具通常配置为 highlights 模式)
|
||||
if len(doc.Highlights) > 0 {
|
||||
result.WriteString("**关键要点**:\n\n")
|
||||
for _, h := range doc.Highlights {
|
||||
result.WriteString(fmt.Sprintf("- %s\n", h))
|
||||
}
|
||||
} else {
|
||||
result.WriteString("**参考内容**:\n\n")
|
||||
result.WriteString("```\n")
|
||||
result.WriteString(doc.Content)
|
||||
result.WriteString("\n```\n")
|
||||
}
|
||||
result.WriteString("\n")
|
||||
}
|
||||
|
||||
// 基于知识库内容检查当前代码
|
||||
suggestions := checkCodeAgainstBestPractices(goCode, ragContext.Documents)
|
||||
if len(suggestions) > 0 {
|
||||
result.WriteString("### 📝 针对当前代码的改进建议\n\n")
|
||||
for _, s := range suggestions {
|
||||
result.WriteString(fmt.Sprintf("- %s\n", s))
|
||||
}
|
||||
result.WriteString("\n")
|
||||
}
|
||||
} else {
|
||||
result.WriteString("### 💡 基本建议\n\n")
|
||||
result.WriteString(fmt.Sprintf("> %s\n\n", ragContext.Message))
|
||||
result.WriteString(generateBasicValidationSuggestions(goCode))
|
||||
}
|
||||
|
||||
// 验证总结
|
||||
if len(basicIssues) == 0 && len(apiIssues) == 0 {
|
||||
result.WriteString("\n---\n\n")
|
||||
result.WriteString("### ✅ 验证通过\n\n")
|
||||
result.WriteString("代码基本验证通过,没有发现明显问题。\n")
|
||||
}
|
||||
|
||||
return result.String(), nil
|
||||
}
|
||||
|
||||
// ==================== 工具示例:convert_lua_to_wasm ====================
|
||||
|
||||
// ConvertLuaToWasmWithRAG 快速转换(通常不使用 RAG)
|
||||
func (ctx *RAGToolContext) ConvertLuaToWasmWithRAG(luaCode string, pluginName string) (string, error) {
|
||||
// 这个工具在配置中通常设置为 use_rag: false,保持快速响应
|
||||
|
||||
toolName := "convert_lua_to_wasm"
|
||||
|
||||
// 仍然可以查询,但如果配置禁用则会快速返回
|
||||
ragContext, _ := ctx.Manager.QueryForTool(
|
||||
toolName,
|
||||
"Lua to WASM conversion examples",
|
||||
"quick_convert",
|
||||
)
|
||||
|
||||
var result strings.Builder
|
||||
result.WriteString(fmt.Sprintf("# %s 插件转换结果\n\n", pluginName))
|
||||
|
||||
if ragContext.Enabled {
|
||||
result.WriteString("> 🚀 使用 RAG 增强转换\n\n")
|
||||
result.WriteString(ragContext.FormatContextForAI())
|
||||
} else {
|
||||
result.WriteString("> ⚡ 快速转换模式(未启用 RAG)\n\n")
|
||||
}
|
||||
|
||||
// 执行基于规则的转换
|
||||
wasmCode := performRuleBasedConversion(luaCode, pluginName)
|
||||
result.WriteString("## 生成的 Go WASM 代码\n\n")
|
||||
result.WriteString("```go\n")
|
||||
result.WriteString(wasmCode)
|
||||
result.WriteString("\n```\n")
|
||||
|
||||
return result.String(), nil
|
||||
}
|
||||
|
||||
// ==================== 辅助函数 ====================
|
||||
|
||||
func extractNginxAPIs(analysisResult string) []string {
|
||||
// 简化实现:从分析结果中提取 API
|
||||
apis := []string{"ngx.req.get_headers", "ngx.say", "ngx.var"}
|
||||
return apis
|
||||
}
|
||||
|
||||
func generateBasicMapping(api string) string {
|
||||
mappings := map[string]string{
|
||||
"ngx.req.get_headers": "**Higress WASM**: `proxywasm.GetHttpRequestHeaders()`\n\n示例:\n```go\nheaders, err := proxywasm.GetHttpRequestHeaders()\nif err != nil {\n proxywasm.LogError(\"failed to get headers\")\n return types.ActionContinue\n}\n```",
|
||||
"ngx.say": "**Higress WASM**: `proxywasm.SendHttpResponse()`",
|
||||
"ngx.var": "**Higress WASM**: `proxywasm.GetProperty()`",
|
||||
}
|
||||
|
||||
if mapping, ok := mappings[api]; ok {
|
||||
return mapping
|
||||
}
|
||||
return "映射信息暂未提供,请参考官方文档。"
|
||||
}
|
||||
|
||||
func validateBasicSyntax(goCode string) []string {
|
||||
// 简化实现
|
||||
issues := []string{}
|
||||
if !strings.Contains(goCode, "package main") {
|
||||
issues = append(issues, "缺少 package main 声明")
|
||||
}
|
||||
return issues
|
||||
}
|
||||
|
||||
func validateAPIUsage(goCode string) []string {
|
||||
// 简化实现
|
||||
issues := []string{}
|
||||
if strings.Contains(goCode, "proxywasm.") && !strings.Contains(goCode, "import") {
|
||||
issues = append(issues, "使用了 proxywasm API 但未导入相关包")
|
||||
}
|
||||
return issues
|
||||
}
|
||||
|
||||
func checkCodeAgainstBestPractices(goCode string, docs []rag.ContextDocument) []string {
|
||||
// 简化实现:基于文档内容检查代码
|
||||
suggestions := []string{}
|
||||
|
||||
// 检查错误处理
|
||||
if !strings.Contains(goCode, "if err != nil") {
|
||||
for _, doc := range docs {
|
||||
if strings.Contains(doc.Content, "错误处理") || strings.Contains(doc.Content, "error handling") {
|
||||
suggestions = append(suggestions, "建议添加完善的错误处理逻辑(参考知识库文档)")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查日志记录
|
||||
if !strings.Contains(goCode, "proxywasm.Log") {
|
||||
suggestions = append(suggestions, "建议添加适当的日志记录以便调试")
|
||||
}
|
||||
|
||||
return suggestions
|
||||
}
|
||||
|
||||
func generateBasicValidationSuggestions(goCode string) string {
|
||||
return "- 确保所有 API 调用都有错误处理\n" +
|
||||
"- 添加必要的日志记录\n" +
|
||||
"- 遵循 Higress WASM 插件开发规范\n"
|
||||
}
|
||||
|
||||
func performRuleBasedConversion(luaCode string, pluginName string) string {
|
||||
// 简化实现:基于规则的转换
|
||||
return fmt.Sprintf(`package main
|
||||
|
||||
import (
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||
)
|
||||
|
||||
func main() {
|
||||
wrapper.SetCtx(
|
||||
"%s",
|
||||
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
|
||||
)
|
||||
}
|
||||
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config PluginConfig, log wrapper.Log) types.Action {
|
||||
// TODO: 实现转换逻辑
|
||||
// 原始 Lua 代码:
|
||||
// %s
|
||||
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
type PluginConfig struct {
|
||||
// TODO: 添加配置字段
|
||||
}
|
||||
`, pluginName, luaCode)
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
//go:build higress_integration
|
||||
// +build higress_integration
|
||||
|
||||
package mcptools
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"nginx-migration-mcp/tools"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
)
|
||||
|
||||
// RegisterToolChainTools 注册工具链相关的工具
|
||||
func RegisterToolChainTools(server *common.MCPServer, ctx *MigrationContext) {
|
||||
RegisterSimpleTool(
|
||||
server,
|
||||
"generate_conversion_hints",
|
||||
"基于 Lua 分析结果生成代码转换模板",
|
||||
map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"analysis_result": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "analyze_lua_plugin 返回的 JSON 格式分析结果",
|
||||
},
|
||||
"plugin_name": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "目标插件名称(小写字母和连字符)",
|
||||
},
|
||||
},
|
||||
"required": []string{"analysis_result", "plugin_name"},
|
||||
},
|
||||
func(args map[string]interface{}) (string, error) {
|
||||
return generateConversionHints(args, ctx)
|
||||
},
|
||||
)
|
||||
|
||||
RegisterSimpleTool(
|
||||
server,
|
||||
"validate_wasm_code",
|
||||
"验证生成的 Go WASM 插件代码,检查语法、API 使用和配置结构",
|
||||
map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"go_code": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "生成的 Go WASM 插件代码",
|
||||
},
|
||||
"plugin_name": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "插件名称",
|
||||
},
|
||||
},
|
||||
"required": []string{"go_code", "plugin_name"},
|
||||
},
|
||||
func(args map[string]interface{}) (string, error) {
|
||||
return validateWasmCode(args, ctx)
|
||||
},
|
||||
)
|
||||
|
||||
RegisterSimpleTool(
|
||||
server,
|
||||
"generate_deployment_config",
|
||||
"为验证通过的 WASM 插件生成完整的部署配置包",
|
||||
map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"plugin_name": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "插件名称",
|
||||
},
|
||||
"go_code": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "验证通过的 Go 代码",
|
||||
},
|
||||
"config_schema": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "配置 JSON Schema(可选)",
|
||||
},
|
||||
"namespace": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "部署命名空间",
|
||||
"default": "higress-system",
|
||||
},
|
||||
},
|
||||
"required": []string{"plugin_name", "go_code"},
|
||||
},
|
||||
func(args map[string]interface{}) (string, error) {
|
||||
return generateDeploymentConfig(args, ctx)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func generateConversionHints(args map[string]interface{}, ctx *MigrationContext) (string, error) {
|
||||
analysisResultStr, ok := args["analysis_result"].(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("missing or invalid analysis_result parameter")
|
||||
}
|
||||
|
||||
pluginName, ok := args["plugin_name"].(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("missing or invalid plugin_name parameter")
|
||||
}
|
||||
|
||||
// 解析分析结果
|
||||
var analysis tools.AnalysisResultForAI
|
||||
if err := json.Unmarshal([]byte(analysisResultStr), &analysis); err != nil {
|
||||
return "", fmt.Errorf("failed to parse analysis_result: %w", err)
|
||||
}
|
||||
|
||||
// 生成转换提示
|
||||
hints := tools.GenerateConversionHints(analysis, pluginName)
|
||||
|
||||
// === RAG 增强(如果启用)===
|
||||
var ragInfo map[string]interface{}
|
||||
if ctx.RAGManager != nil && ctx.RAGManager.IsEnabled() {
|
||||
// 构建智能查询语句
|
||||
queryBuilder := []string{}
|
||||
if len(analysis.APICalls) > 0 {
|
||||
queryBuilder = append(queryBuilder, "Nginx Lua API 转换到 Higress WASM")
|
||||
|
||||
hasHeaderOps := analysis.Features["header_manipulation"] || analysis.Features["request_headers"] || analysis.Features["response_headers"]
|
||||
hasBodyOps := analysis.Features["request_body"] || analysis.Features["response_body"]
|
||||
hasResponseControl := analysis.Features["response_control"]
|
||||
|
||||
if hasHeaderOps {
|
||||
queryBuilder = append(queryBuilder, "请求头和响应头处理")
|
||||
}
|
||||
if hasBodyOps {
|
||||
queryBuilder = append(queryBuilder, "请求体和响应体处理")
|
||||
}
|
||||
if hasResponseControl {
|
||||
queryBuilder = append(queryBuilder, "响应控制和状态码设置")
|
||||
}
|
||||
|
||||
if len(analysis.APICalls) > 0 && len(analysis.APICalls) <= 5 {
|
||||
queryBuilder = append(queryBuilder, fmt.Sprintf("涉及 API: %s", strings.Join(analysis.APICalls, ", ")))
|
||||
}
|
||||
} else {
|
||||
queryBuilder = append(queryBuilder, "Higress WASM 插件开发 基础示例 Go SDK 使用")
|
||||
}
|
||||
|
||||
if analysis.Complexity == "high" {
|
||||
queryBuilder = append(queryBuilder, "复杂插件实现 高级功能")
|
||||
}
|
||||
|
||||
queryString := strings.Join(queryBuilder, " ")
|
||||
|
||||
ragContext, err := ctx.RAGManager.QueryForTool(
|
||||
"generate_conversion_hints",
|
||||
queryString,
|
||||
"lua_migration",
|
||||
)
|
||||
if err == nil && ragContext.Enabled && len(ragContext.Documents) > 0 {
|
||||
ragInfo = map[string]interface{}{
|
||||
"enabled": true,
|
||||
"documents": len(ragContext.Documents),
|
||||
"context": ragContext.FormatContextForAI(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 组合结果
|
||||
result := map[string]interface{}{
|
||||
"code_template": hints.CodeTemplate,
|
||||
"warnings": hints.Warnings,
|
||||
"rag": ragInfo,
|
||||
}
|
||||
|
||||
// 返回 JSON 结果,由 LLM 解释和使用
|
||||
resultJSON, _ := json.MarshalIndent(result, "", " ")
|
||||
return string(resultJSON), nil
|
||||
}
|
||||
|
||||
func validateWasmCode(args map[string]interface{}, ctx *MigrationContext) (string, error) {
|
||||
goCode, ok := args["go_code"].(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("missing or invalid go_code parameter")
|
||||
}
|
||||
|
||||
pluginName, ok := args["plugin_name"].(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("missing or invalid plugin_name parameter")
|
||||
}
|
||||
|
||||
// 执行验证(AI 驱动)
|
||||
report := tools.ValidateWasmCode(goCode, pluginName)
|
||||
|
||||
// 格式化输出,包含 AI 分析提示和基础信息
|
||||
var result strings.Builder
|
||||
|
||||
result.WriteString(fmt.Sprintf("## 代码验证报告\n\n"))
|
||||
result.WriteString(fmt.Sprintf("代码存在 %d 个必须修复的问题,%d 个建议修复的问题,%d 个可选优化项,%d 个最佳实践建议。请优先解决必须修复的问题。\n\n", 0, 0, 0, 0))
|
||||
|
||||
result.WriteString(fmt.Sprintf("### 发现的回调函数 (%d 个)\n", len(report.FoundCallbacks)))
|
||||
if len(report.FoundCallbacks) > 0 {
|
||||
for _, cb := range report.FoundCallbacks {
|
||||
result.WriteString(fmt.Sprintf("- %s\n", cb))
|
||||
}
|
||||
} else {
|
||||
result.WriteString("无\n")
|
||||
}
|
||||
result.WriteString("\n")
|
||||
|
||||
result.WriteString("### 配置结构\n")
|
||||
if report.HasConfig {
|
||||
result.WriteString(" 已定义配置结构体\n\n")
|
||||
} else {
|
||||
result.WriteString(" 未定义配置结构体\n\n")
|
||||
}
|
||||
|
||||
result.WriteString("### 问题分类\n\n")
|
||||
|
||||
result.WriteString("#### 必须修复 (0 个)\n")
|
||||
result.WriteString("无\n\n")
|
||||
|
||||
result.WriteString("#### 建议修复 (0 个)\n")
|
||||
result.WriteString("无\n\n")
|
||||
|
||||
result.WriteString("#### 可选优化 (0 个)\n")
|
||||
result.WriteString("无\n\n")
|
||||
|
||||
result.WriteString("#### 最佳实践 (0 个)\n")
|
||||
result.WriteString("无\n\n")
|
||||
|
||||
// 添加 AI 分析提示
|
||||
result.WriteString("---\n\n")
|
||||
result.WriteString(report.Summary)
|
||||
result.WriteString("\n\n")
|
||||
|
||||
// === RAG 增强:查询最佳实践 ===
|
||||
if ctx.RAGManager != nil && ctx.RAGManager.IsEnabled() {
|
||||
// 构建智能查询语句
|
||||
queryBuilder := []string{"Higress WASM 插件"}
|
||||
|
||||
// 根据回调函数类型添加特定查询
|
||||
for _, callback := range report.FoundCallbacks {
|
||||
if strings.Contains(callback, "RequestHeaders") {
|
||||
queryBuilder = append(queryBuilder, "请求头处理")
|
||||
}
|
||||
if strings.Contains(callback, "RequestBody") {
|
||||
queryBuilder = append(queryBuilder, "请求体处理")
|
||||
}
|
||||
if strings.Contains(callback, "ResponseHeaders") {
|
||||
queryBuilder = append(queryBuilder, "响应头处理")
|
||||
}
|
||||
}
|
||||
|
||||
queryBuilder = append(queryBuilder, "性能优化 最佳实践 错误处理")
|
||||
queryString := strings.Join(queryBuilder, " ")
|
||||
|
||||
ragContext, err := ctx.RAGManager.QueryForTool(
|
||||
"validate_wasm_code",
|
||||
queryString,
|
||||
"best_practice",
|
||||
)
|
||||
if err == nil && ragContext.Enabled && len(ragContext.Documents) > 0 {
|
||||
result.WriteString("\n\n### 📚 最佳实践建议(来自知识库)\n\n")
|
||||
result.WriteString(ragContext.FormatContextForAI())
|
||||
}
|
||||
}
|
||||
|
||||
// 添加 JSON 格式的结构化数据(供后续处理)
|
||||
reportJSON, _ := json.MarshalIndent(report, "", " ")
|
||||
result.WriteString("\n")
|
||||
result.WriteString(string(reportJSON))
|
||||
|
||||
return result.String(), nil
|
||||
}
|
||||
|
||||
func generateDeploymentConfig(args map[string]interface{}, ctx *MigrationContext) (string, error) {
|
||||
pluginName, ok := args["plugin_name"].(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("missing or invalid plugin_name parameter")
|
||||
}
|
||||
|
||||
_, ok = args["go_code"].(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("missing or invalid go_code parameter")
|
||||
}
|
||||
|
||||
namespace := "higress-system"
|
||||
if ns, ok := args["namespace"].(string); ok && ns != "" {
|
||||
namespace = ns
|
||||
}
|
||||
|
||||
// configSchema is optional, we don't use it for now but don't return error
|
||||
_ = args["config_schema"]
|
||||
|
||||
// 返回提示信息,由 LLM 生成具体配置文件
|
||||
result := fmt.Sprintf(`为插件 %s 生成以下部署配置:
|
||||
1. WasmPlugin YAML (namespace: %s)
|
||||
2. Makefile (TinyGo 构建)
|
||||
3. Dockerfile
|
||||
4. README.md
|
||||
5. 测试脚本
|
||||
|
||||
参考文档: https://higress.cn/docs/latest/user/wasm-go/`, pluginName, namespace)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
//go:build higress_integration
|
||||
// +build higress_integration
|
||||
|
||||
package nginx_migration
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"nginx-migration-mcp/integration/mcptools"
|
||||
"nginx-migration-mcp/internal/rag"
|
||||
|
||||
"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("nginx-migration", &NginxMigrationConfig{})
|
||||
}
|
||||
|
||||
// NginxMigrationConfig holds configuration for the Nginx Migration MCP Server
|
||||
type NginxMigrationConfig struct {
|
||||
gatewayName string
|
||||
gatewayNamespace string
|
||||
defaultNamespace string
|
||||
defaultHostname string
|
||||
description string
|
||||
ragConfigPath string // RAG 配置文件路径
|
||||
}
|
||||
|
||||
// ParseConfig parses the configuration map for the Nginx Migration server
|
||||
func (c *NginxMigrationConfig) ParseConfig(config map[string]interface{}) error {
|
||||
// Optional configurations with defaults
|
||||
if gatewayName, ok := config["gatewayName"].(string); ok {
|
||||
c.gatewayName = gatewayName
|
||||
} else {
|
||||
c.gatewayName = "higress-gateway"
|
||||
}
|
||||
|
||||
if gatewayNamespace, ok := config["gatewayNamespace"].(string); ok {
|
||||
c.gatewayNamespace = gatewayNamespace
|
||||
} else {
|
||||
c.gatewayNamespace = "higress-system"
|
||||
}
|
||||
|
||||
if defaultNamespace, ok := config["defaultNamespace"].(string); ok {
|
||||
c.defaultNamespace = defaultNamespace
|
||||
} else {
|
||||
c.defaultNamespace = "default"
|
||||
}
|
||||
|
||||
if defaultHostname, ok := config["defaultHostname"].(string); ok {
|
||||
c.defaultHostname = defaultHostname
|
||||
} else {
|
||||
c.defaultHostname = "example.com"
|
||||
}
|
||||
|
||||
if desc, ok := config["description"].(string); ok {
|
||||
c.description = desc
|
||||
} else {
|
||||
c.description = "Nginx Migration MCP Server - Convert Nginx configs and Lua plugins to Higress"
|
||||
}
|
||||
|
||||
// RAG 配置路径(可选)
|
||||
if ragPath, ok := config["ragConfigPath"].(string); ok {
|
||||
c.ragConfigPath = ragPath
|
||||
} else {
|
||||
c.ragConfigPath = "config/rag.json" // 默认路径
|
||||
}
|
||||
|
||||
api.LogDebugf("NginxMigrationConfig ParseConfig: gatewayName=%s, gatewayNamespace=%s, defaultNamespace=%s, ragConfig=%s",
|
||||
c.gatewayName, c.gatewayNamespace, c.defaultNamespace, c.ragConfigPath)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewServer creates a new MCP server instance for Nginx Migration
|
||||
func (c *NginxMigrationConfig) NewServer(serverName string) (*common.MCPServer, error) {
|
||||
if serverName == "" {
|
||||
return nil, errors.New("server name cannot be empty")
|
||||
}
|
||||
|
||||
mcpServer := common.NewMCPServer(
|
||||
serverName,
|
||||
Version,
|
||||
common.WithInstructions("Nginx Migration MCP Server: Analyze and convert Nginx configurations and Lua plugins to Higress"),
|
||||
)
|
||||
|
||||
// Create migration context with configuration
|
||||
migrationCtx := &mcptools.MigrationContext{
|
||||
GatewayName: c.gatewayName,
|
||||
GatewayNamespace: c.gatewayNamespace,
|
||||
DefaultNamespace: c.defaultNamespace,
|
||||
DefaultHostname: c.defaultHostname,
|
||||
}
|
||||
|
||||
// 初始化 RAG Manager(如果配置了)
|
||||
if c.ragConfigPath != "" {
|
||||
api.LogInfof("Loading RAG config from: %s", c.ragConfigPath)
|
||||
ragConfig, err := rag.LoadRAGConfig(c.ragConfigPath)
|
||||
if err != nil {
|
||||
api.LogWarnf("Failed to load RAG config: %v, RAG will be disabled", err)
|
||||
// 不返回错误,继续使用无 RAG 的模式
|
||||
ragConfig = &rag.RAGConfig{Enabled: false}
|
||||
}
|
||||
|
||||
// 创建 RAG Manager
|
||||
migrationCtx.RAGManager = rag.NewRAGManager(ragConfig)
|
||||
|
||||
if migrationCtx.RAGManager.IsEnabled() {
|
||||
api.LogInfof("✅ RAG enabled for Nginx Migration MCP Server")
|
||||
} else {
|
||||
api.LogInfof("📖 RAG disabled, using rule-based approach")
|
||||
}
|
||||
}
|
||||
|
||||
// Register all migration tools
|
||||
mcptools.RegisterNginxConfigTools(mcpServer, migrationCtx)
|
||||
mcptools.RegisterLuaPluginTools(mcpServer, migrationCtx)
|
||||
mcptools.RegisterToolChainTools(mcpServer, migrationCtx)
|
||||
|
||||
api.LogInfof("Nginx Migration MCP Server initialized: %s (tools registered)", serverName)
|
||||
|
||||
return mcpServer, nil
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
// Package rag 提供基于阿里云官方 SDK 的 RAG 客户端实现
|
||||
package rag
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
bailian "github.com/alibabacloud-go/bailian-20231229/v2/client"
|
||||
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
|
||||
util "github.com/alibabacloud-go/tea-utils/v2/service"
|
||||
"github.com/alibabacloud-go/tea/tea"
|
||||
)
|
||||
|
||||
// RAGQuery RAG 查询请求
|
||||
type RAGQuery struct {
|
||||
Query string `json:"query"` // 查询文本
|
||||
Scenario string `json:"scenario"` // 场景标识
|
||||
TopK int `json:"top_k"` // 返回文档数量
|
||||
ContextMode string `json:"context_mode"` // 上下文模式
|
||||
Filters map[string]interface{} `json:"filters"` // 过滤条件
|
||||
}
|
||||
|
||||
// RAGResponse RAG 查询响应
|
||||
type RAGResponse struct {
|
||||
Documents []RAGDocument `json:"documents"` // 检索到的文档
|
||||
Latency int64 `json:"latency"` // 查询延迟(毫秒)
|
||||
}
|
||||
|
||||
// RAGDocument 表示一个检索到的文档
|
||||
type RAGDocument struct {
|
||||
Title string `json:"title"` // 文档标题
|
||||
Content string `json:"content"` // 文档内容
|
||||
Source string `json:"source"` // 来源路径
|
||||
URL string `json:"url"` // 在线链接
|
||||
Score float64 `json:"score"` // 相关度分数
|
||||
Highlights []string `json:"highlights"` // 高亮片段
|
||||
}
|
||||
|
||||
// RAGClient 使用阿里云官方 SDK 的 RAG 客户端
|
||||
type RAGClient struct {
|
||||
config *RAGConfig
|
||||
client *bailian.Client
|
||||
cache *QueryCache
|
||||
}
|
||||
|
||||
// NewRAGClient 创建基于 SDK 的 RAG 客户端
|
||||
func NewRAGClient(config *RAGConfig) (*RAGClient, error) {
|
||||
// 创建 SDK 配置
|
||||
sdkConfig := &openapi.Config{
|
||||
AccessKeyId: tea.String(config.AccessKeyID),
|
||||
AccessKeySecret: tea.String(config.AccessKeySecret),
|
||||
}
|
||||
|
||||
// 设置端点(默认为北京区域)
|
||||
if config.Endpoint != "" {
|
||||
sdkConfig.Endpoint = tea.String(config.Endpoint)
|
||||
} else {
|
||||
sdkConfig.Endpoint = tea.String("bailian.cn-beijing.aliyuncs.com")
|
||||
}
|
||||
|
||||
// 创建客户端
|
||||
client, err := bailian.NewClient(sdkConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create Bailian SDK client: %w", err)
|
||||
}
|
||||
|
||||
c := &RAGClient{
|
||||
config: config,
|
||||
client: client,
|
||||
}
|
||||
|
||||
// 初始化缓存
|
||||
if config.EnableCache {
|
||||
c.cache = NewQueryCache(config.CacheMaxSize, time.Duration(config.CacheTTL)*time.Second)
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// SearchWithCache 查询知识库(带缓存)
|
||||
func (c *RAGClient) SearchWithCache(query *RAGQuery) (*RAGResponse, error) {
|
||||
// 检查缓存
|
||||
if c.cache != nil {
|
||||
cacheKey := c.buildCacheKey(query)
|
||||
if cached := c.cache.Get(cacheKey); cached != nil {
|
||||
if c.config.Debug {
|
||||
log.Printf("🎯 RAG cache hit: %s", query.Query)
|
||||
}
|
||||
return cached, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 执行查询
|
||||
startTime := time.Now()
|
||||
resp, err := c.search(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 记录延迟
|
||||
resp.Latency = time.Since(startTime).Milliseconds()
|
||||
|
||||
// 缓存结果
|
||||
if c.cache != nil {
|
||||
cacheKey := c.buildCacheKey(query)
|
||||
c.cache.Set(cacheKey, resp)
|
||||
}
|
||||
|
||||
if c.config.Debug {
|
||||
log.Printf("✅ RAG query completed: %s (latency: %dms, docs: %d)",
|
||||
query.Query, resp.Latency, len(resp.Documents))
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// search 执行实际的查询(带重试)
|
||||
func (c *RAGClient) search(query *RAGQuery) (*RAGResponse, error) {
|
||||
var lastErr error
|
||||
|
||||
for attempt := 0; attempt <= c.config.MaxRetries; attempt++ {
|
||||
if attempt > 0 {
|
||||
// 重试前等待
|
||||
time.Sleep(time.Duration(c.config.RetryDelay) * time.Second)
|
||||
log.Printf("🔄 Retrying RAG query (attempt %d/%d)", attempt, c.config.MaxRetries)
|
||||
}
|
||||
|
||||
resp, err := c.doSearchSDK(query)
|
||||
if err == nil {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("RAG query failed after %d retries: %w", c.config.MaxRetries, lastErr)
|
||||
}
|
||||
|
||||
// doSearchSDK 执行单次查询(使用 SDK)
|
||||
func (c *RAGClient) doSearchSDK(query *RAGQuery) (*RAGResponse, error) {
|
||||
// 构建检索请求
|
||||
request := &bailian.RetrieveRequest{
|
||||
IndexId: tea.String(c.config.KnowledgeBaseID),
|
||||
Query: tea.String(query.Query),
|
||||
}
|
||||
|
||||
// 设置可选参数
|
||||
if query.TopK > 0 {
|
||||
request.DenseSimilarityTopK = tea.Int32(int32(query.TopK))
|
||||
} else {
|
||||
request.DenseSimilarityTopK = tea.Int32(int32(c.config.DefaultTopK))
|
||||
}
|
||||
|
||||
// 启用重排序
|
||||
request.EnableReranking = tea.Bool(true)
|
||||
|
||||
// 准备请求头和运行时选项
|
||||
headers := make(map[string]*string)
|
||||
runtime := &util.RuntimeOptions{}
|
||||
|
||||
// 调用 SDK 检索接口
|
||||
response, err := c.client.RetrieveWithOptions(
|
||||
tea.String(c.config.WorkspaceID),
|
||||
request,
|
||||
headers,
|
||||
runtime,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("SDK retrieve failed: %w", err)
|
||||
}
|
||||
|
||||
// 检查响应
|
||||
if response == nil || response.Body == nil {
|
||||
return nil, fmt.Errorf("empty response from SDK")
|
||||
}
|
||||
|
||||
if !tea.BoolValue(response.Body.Success) {
|
||||
return nil, fmt.Errorf("SDK returned Success=false, Code=%s, Message=%s",
|
||||
tea.StringValue(response.Body.Code),
|
||||
tea.StringValue(response.Body.Message))
|
||||
}
|
||||
|
||||
// 转换为 RAGResponse
|
||||
ragResp := &RAGResponse{
|
||||
Documents: make([]RAGDocument, 0),
|
||||
}
|
||||
|
||||
if response.Body.Data != nil && response.Body.Data.Nodes != nil {
|
||||
for _, node := range response.Body.Data.Nodes {
|
||||
if node == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 过滤低相关度文档
|
||||
score := tea.Float64Value(node.Score)
|
||||
if score < c.config.SimilarityThreshold {
|
||||
continue
|
||||
}
|
||||
|
||||
// 从 Metadata 中提取信息
|
||||
title := ""
|
||||
source := ""
|
||||
url := ""
|
||||
|
||||
if node.Metadata != nil {
|
||||
// Metadata 是 interface{} 类型,需要先转换为 map
|
||||
if meta, ok := node.Metadata.(map[string]interface{}); ok {
|
||||
if t, ok := meta["title"].(string); ok {
|
||||
title = t
|
||||
}
|
||||
if s, ok := meta["doc_name"].(string); ok {
|
||||
source = s
|
||||
}
|
||||
if u, ok := meta["file_path"].(string); ok {
|
||||
url = u
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ragResp.Documents = append(ragResp.Documents, RAGDocument{
|
||||
Title: title,
|
||||
Content: tea.StringValue(node.Text),
|
||||
Source: source,
|
||||
URL: url,
|
||||
Score: score,
|
||||
Highlights: []string{}, // SDK 不返回 highlights
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return ragResp, nil
|
||||
}
|
||||
|
||||
// buildCacheKey 构建缓存键
|
||||
func (c *RAGClient) buildCacheKey(query *RAGQuery) string {
|
||||
return fmt.Sprintf("%s:%s:top%d:%s", query.Scenario, query.Query, query.TopK, query.ContextMode)
|
||||
}
|
||||
|
||||
// QueryCache 查询缓存
|
||||
type QueryCache struct {
|
||||
entries map[string]*CacheEntry
|
||||
mu sync.RWMutex
|
||||
maxSize int
|
||||
ttl time.Duration
|
||||
}
|
||||
|
||||
// CacheEntry 缓存条目
|
||||
type CacheEntry struct {
|
||||
Response *RAGResponse
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
// NewQueryCache 创建查询缓存
|
||||
func NewQueryCache(maxSize int, ttl time.Duration) *QueryCache {
|
||||
cache := &QueryCache{
|
||||
entries: make(map[string]*CacheEntry),
|
||||
maxSize: maxSize,
|
||||
ttl: ttl,
|
||||
}
|
||||
|
||||
// 启动清理协程
|
||||
go cache.cleanupLoop()
|
||||
|
||||
return cache
|
||||
}
|
||||
|
||||
// Get 获取缓存
|
||||
func (c *QueryCache) Get(key string) *RAGResponse {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
entry, ok := c.entries[key]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查是否过期
|
||||
if time.Now().After(entry.ExpiresAt) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return entry.Response
|
||||
}
|
||||
|
||||
// Set 设置缓存
|
||||
func (c *QueryCache) Set(key string, resp *RAGResponse) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
// 检查缓存大小
|
||||
if len(c.entries) >= c.maxSize {
|
||||
// 简单的 LRU:删除第一个条目
|
||||
for k := range c.entries {
|
||||
delete(c.entries, k)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
c.entries[key] = &CacheEntry{
|
||||
Response: resp,
|
||||
ExpiresAt: time.Now().Add(c.ttl),
|
||||
}
|
||||
}
|
||||
|
||||
// cleanupLoop 清理过期缓存
|
||||
func (c *QueryCache) cleanupLoop() {
|
||||
ticker := time.NewTicker(5 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
c.cleanup()
|
||||
}
|
||||
}
|
||||
|
||||
// cleanup 执行清理
|
||||
func (c *QueryCache) cleanup() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
for key, entry := range c.entries {
|
||||
if now.After(entry.ExpiresAt) {
|
||||
delete(c.entries, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
// Package rag 提供 RAG(检索增强生成)配置管理
|
||||
package rag
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RAGConfig RAG 完整配置
|
||||
type RAGConfig struct {
|
||||
Enabled bool `json:"enabled" yaml:"enabled"` // RAG 功能总开关
|
||||
|
||||
// API 配置
|
||||
Provider string `json:"provider" yaml:"provider"` // 服务提供商: "bailian"
|
||||
Endpoint string `json:"endpoint" yaml:"endpoint"` // API 端点(如 bailian.cn-beijing.aliyuncs.com)
|
||||
WorkspaceID string `json:"workspace_id" yaml:"workspace_id"` // 业务空间 ID(百炼必需)
|
||||
AccessKeyID string `json:"access_key_id" yaml:"access_key_id"` // 阿里云 AccessKey ID
|
||||
AccessKeySecret string `json:"access_key_secret" yaml:"access_key_secret"` // 阿里云 AccessKey Secret
|
||||
KnowledgeBaseID string `json:"knowledge_base_id" yaml:"knowledge_base_id"` // 知识库 ID(IndexId)
|
||||
|
||||
// 上下文配置
|
||||
ContextMode string `json:"context_mode" yaml:"context_mode"` // full | summary | highlights
|
||||
MaxContextLength int `json:"max_context_length" yaml:"max_context_length"` // 最大上下文长度(字符数)
|
||||
|
||||
// 检索配置
|
||||
DefaultTopK int `json:"default_top_k" yaml:"default_top_k"` // 默认返回文档数量
|
||||
SimilarityThreshold float64 `json:"similarity_threshold" yaml:"similarity_threshold"` // 相似度阈值
|
||||
|
||||
// 缓存配置
|
||||
EnableCache bool `json:"enable_cache" yaml:"enable_cache"` // 是否启用缓存
|
||||
CacheTTL int `json:"cache_ttl" yaml:"cache_ttl"` // 缓存过期时间(秒)
|
||||
CacheMaxSize int `json:"cache_max_size" yaml:"cache_max_size"` // 最大缓存条目数
|
||||
|
||||
// 性能配置
|
||||
Timeout int `json:"timeout" yaml:"timeout"` // 请求超时时间(秒)
|
||||
MaxRetries int `json:"max_retries" yaml:"max_retries"` // 最大重试次数
|
||||
RetryDelay int `json:"retry_delay" yaml:"retry_delay"` // 重试间隔(秒)
|
||||
|
||||
// 降级策略
|
||||
FallbackOnError bool `json:"fallback_on_error" yaml:"fallback_on_error"` // RAG 失败时是否降级
|
||||
|
||||
// 工具级别配置(核心功能)
|
||||
Tools map[string]*ToolConfig `json:"tools" yaml:"tools"`
|
||||
|
||||
// 调试模式
|
||||
Debug bool `json:"debug" yaml:"debug"` // 是否启用调试日志
|
||||
LogQueries bool `json:"log_queries" yaml:"log_queries"` // 是否记录所有查询
|
||||
}
|
||||
|
||||
// ToolConfig 工具级别的 RAG 配置
|
||||
type ToolConfig struct {
|
||||
UseRAG bool `json:"use_rag" yaml:"use_rag"` // 是否使用 RAG
|
||||
ContextMode string `json:"context_mode" yaml:"context_mode"` // 上下文模式(覆盖全局配置)
|
||||
TopK int `json:"top_k" yaml:"top_k"` // 返回文档数量(覆盖全局配置)
|
||||
}
|
||||
|
||||
// LoadRAGConfig 从配置文件加载 RAG 配置
|
||||
// 注意:需要安装 YAML 库支持
|
||||
// 运行:go get gopkg.in/yaml.v3
|
||||
//
|
||||
// 临时实现:使用 JSON 格式配置文件
|
||||
func LoadRAGConfig(configPath string) (*RAGConfig, error) {
|
||||
// 读取配置文件
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read config file: %w", err)
|
||||
}
|
||||
|
||||
// 简单的 YAML 到 JSON 转换(仅支持基本格式)
|
||||
// 在生产环境中应使用真正的 YAML 解析器
|
||||
jsonData := simpleYAMLToJSON(string(data))
|
||||
|
||||
// 解析 JSON
|
||||
var wrapper struct {
|
||||
RAG *RAGConfig `json:"rag"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(jsonData), &wrapper); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse config: %w", err)
|
||||
}
|
||||
|
||||
if wrapper.RAG == nil {
|
||||
return nil, fmt.Errorf("missing 'rag' section in config")
|
||||
}
|
||||
|
||||
config := wrapper.RAG
|
||||
|
||||
// 展开环境变量
|
||||
config.AccessKeyID = expandEnvVar(config.AccessKeyID)
|
||||
config.AccessKeySecret = expandEnvVar(config.AccessKeySecret)
|
||||
config.KnowledgeBaseID = expandEnvVar(config.KnowledgeBaseID)
|
||||
config.WorkspaceID = expandEnvVar(config.WorkspaceID)
|
||||
|
||||
// 设置默认值
|
||||
setDefaults(config)
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// simpleYAMLToJSON 简单的 YAML 到 JSON 转换
|
||||
// 注意:这是一个临时实现,仅支持基本的 YAML 格式
|
||||
// 生产环境请使用 gopkg.in/yaml.v3
|
||||
func simpleYAMLToJSON(yamlContent string) string {
|
||||
trimmed := strings.TrimSpace(yamlContent)
|
||||
|
||||
// 如果内容看起来像 JSON,直接返回
|
||||
if strings.HasPrefix(trimmed, "{") {
|
||||
return yamlContent
|
||||
}
|
||||
|
||||
// 否则返回默认禁用配置
|
||||
return `{"rag": {"enabled": false}}`
|
||||
}
|
||||
|
||||
// expandEnvVar 展开环境变量 ${VAR_NAME}
|
||||
func expandEnvVar(value string) string {
|
||||
if strings.HasPrefix(value, "${") && strings.HasSuffix(value, "}") {
|
||||
varName := value[2 : len(value)-1]
|
||||
return os.Getenv(varName)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// setDefaults 设置默认值
|
||||
func setDefaults(config *RAGConfig) {
|
||||
if config.ContextMode == "" {
|
||||
config.ContextMode = "full"
|
||||
}
|
||||
if config.MaxContextLength == 0 {
|
||||
config.MaxContextLength = 4000
|
||||
}
|
||||
if config.DefaultTopK == 0 {
|
||||
config.DefaultTopK = 3
|
||||
}
|
||||
if config.SimilarityThreshold == 0 {
|
||||
config.SimilarityThreshold = 0.7
|
||||
}
|
||||
if config.CacheTTL == 0 {
|
||||
config.CacheTTL = 3600
|
||||
}
|
||||
if config.CacheMaxSize == 0 {
|
||||
config.CacheMaxSize = 1000
|
||||
}
|
||||
if config.Timeout == 0 {
|
||||
config.Timeout = 10
|
||||
}
|
||||
if config.MaxRetries == 0 {
|
||||
config.MaxRetries = 3
|
||||
}
|
||||
if config.RetryDelay == 0 {
|
||||
config.RetryDelay = 1
|
||||
}
|
||||
|
||||
// 为每个工具配置设置默认值
|
||||
for _, toolConfig := range config.Tools {
|
||||
if toolConfig.ContextMode == "" {
|
||||
toolConfig.ContextMode = config.ContextMode
|
||||
}
|
||||
if toolConfig.TopK == 0 {
|
||||
toolConfig.TopK = config.DefaultTopK
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetToolConfig 获取指定工具的配置
|
||||
func (c *RAGConfig) GetToolConfig(toolName string) *ToolConfig {
|
||||
if toolConfig, ok := c.Tools[toolName]; ok {
|
||||
return toolConfig
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsToolRAGEnabled 检查指定工具是否启用 RAG
|
||||
func (c *RAGConfig) IsToolRAGEnabled(toolName string) bool {
|
||||
if !c.Enabled {
|
||||
return false
|
||||
}
|
||||
|
||||
toolConfig := c.GetToolConfig(toolName)
|
||||
if toolConfig == nil {
|
||||
// 没有工具级配置,使用全局配置
|
||||
return c.Enabled
|
||||
}
|
||||
|
||||
return toolConfig.UseRAG
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
// Package rag 提供 RAG(检索增强生成)功能
|
||||
// 支持可选的知识库集成,通过配置开关控制
|
||||
package rag
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RAGManager 管理 RAG 功能的开关和查询
|
||||
type RAGManager struct {
|
||||
enabled bool // RAG 功能是否启用
|
||||
client *RAGClient // RAG 客户端(仅在 enabled=true 时有效)
|
||||
config *RAGConfig // 配置
|
||||
}
|
||||
|
||||
// NewRAGManager 创建 RAG 管理器
|
||||
// 如果配置中 enabled=false,则返回禁用状态的管理器
|
||||
func NewRAGManager(config *RAGConfig) *RAGManager {
|
||||
if config == nil || !config.Enabled {
|
||||
log.Println("📖 RAG: Disabled (using rule-based generation)")
|
||||
return &RAGManager{
|
||||
enabled: false,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// 验证必要配置
|
||||
if config.KnowledgeBaseID == "" || config.WorkspaceID == "" {
|
||||
log.Println("⚠️ RAG: Missing workspace ID or knowledge base ID, disabling RAG")
|
||||
return &RAGManager{
|
||||
enabled: false,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// 检查 SDK 认证凭证
|
||||
if config.AccessKeyID == "" || config.AccessKeySecret == "" {
|
||||
log.Println("⚠️ RAG: Missing AccessKey credentials, disabling RAG")
|
||||
return &RAGManager{
|
||||
enabled: false,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化 RAG 客户端(使用 SDK)
|
||||
log.Println("🔧 RAG: Using Alibaba Cloud SDK authentication")
|
||||
client, err := NewRAGClient(config)
|
||||
if err != nil {
|
||||
log.Printf("❌ RAG: Failed to initialize SDK client: %v, disabling RAG\n", err)
|
||||
return &RAGManager{
|
||||
enabled: false,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("✅ RAG: Enabled (Provider: %s, KB: %s)\n", config.Provider, config.KnowledgeBaseID)
|
||||
|
||||
return &RAGManager{
|
||||
enabled: true,
|
||||
client: client,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// IsEnabled 返回 RAG 是否启用
|
||||
func (m *RAGManager) IsEnabled() bool {
|
||||
return m.enabled
|
||||
}
|
||||
|
||||
// QueryWithContext 查询知识库并返回上下文
|
||||
// 如果 RAG 未启用,返回空上下文(不报错)
|
||||
func (m *RAGManager) QueryWithContext(query string, scenario string, opts ...QueryOption) (*RAGContext, error) {
|
||||
// RAG 未启用,返回空上下文
|
||||
if !m.enabled {
|
||||
return &RAGContext{
|
||||
Enabled: false,
|
||||
Message: "RAG is disabled, using rule-based generation",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 构建查询
|
||||
ragQuery := &RAGQuery{
|
||||
Query: query,
|
||||
Scenario: scenario,
|
||||
TopK: m.config.DefaultTopK,
|
||||
}
|
||||
|
||||
// 应用可选参数
|
||||
for _, opt := range opts {
|
||||
opt(ragQuery)
|
||||
}
|
||||
|
||||
// 查询知识库
|
||||
resp, err := m.client.SearchWithCache(ragQuery)
|
||||
if err != nil {
|
||||
// 如果配置了降级策略,返回空上下文而不是报错
|
||||
if m.config.FallbackOnError {
|
||||
log.Printf("⚠️ RAG query failed, falling back to rules: %v\n", err)
|
||||
return &RAGContext{
|
||||
Enabled: false,
|
||||
Message: fmt.Sprintf("RAG query failed, using fallback: %v", err),
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("RAG query failed: %w", err)
|
||||
}
|
||||
|
||||
// 构建上下文
|
||||
return m.buildContext(resp), nil
|
||||
}
|
||||
|
||||
// QueryForTool 为特定工具查询(支持工具级配置覆盖)
|
||||
// 这是工具级别配置的核心实现
|
||||
func (m *RAGManager) QueryForTool(toolName string, query string, scenario string) (*RAGContext, error) {
|
||||
// 全局 RAG 未启用
|
||||
if !m.enabled {
|
||||
return &RAGContext{
|
||||
Enabled: false,
|
||||
Message: "RAG is disabled globally",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 检查工具级配置
|
||||
if toolConfig, ok := m.config.Tools[toolName]; ok {
|
||||
// 工具有专门的配置
|
||||
if !toolConfig.UseRAG {
|
||||
// 工具明确不使用 RAG
|
||||
return &RAGContext{
|
||||
Enabled: false,
|
||||
Message: fmt.Sprintf("RAG is disabled for tool: %s", toolName),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 使用工具级配置覆盖全局配置
|
||||
log.Printf("🔧 Using tool-specific RAG config for: %s (context_mode=%s, top_k=%d)",
|
||||
toolName, toolConfig.ContextMode, toolConfig.TopK)
|
||||
|
||||
return m.QueryWithContext(query, scenario,
|
||||
WithTopK(toolConfig.TopK),
|
||||
WithContextMode(toolConfig.ContextMode),
|
||||
)
|
||||
}
|
||||
|
||||
// 没有工具级配置,使用默认全局配置
|
||||
log.Printf("🔧 Using global RAG config for: %s", toolName)
|
||||
return m.QueryWithContext(query, scenario)
|
||||
}
|
||||
|
||||
// buildContext 构建上下文
|
||||
func (m *RAGManager) buildContext(resp *RAGResponse) *RAGContext {
|
||||
ctx := &RAGContext{
|
||||
Enabled: true,
|
||||
Documents: make([]ContextDocument, 0, len(resp.Documents)),
|
||||
}
|
||||
|
||||
for _, doc := range resp.Documents {
|
||||
ctxDoc := ContextDocument{
|
||||
Title: doc.Title,
|
||||
Source: doc.Source,
|
||||
URL: doc.URL,
|
||||
Score: doc.Score,
|
||||
Highlights: doc.Highlights,
|
||||
}
|
||||
|
||||
// 根据 context_mode 决定返回的内容
|
||||
switch m.config.ContextMode {
|
||||
case "full":
|
||||
ctxDoc.Content = doc.Content
|
||||
case "summary":
|
||||
ctxDoc.Content = m.summarize(doc.Content)
|
||||
case "highlights":
|
||||
if len(doc.Highlights) > 0 {
|
||||
ctxDoc.Content = strings.Join(doc.Highlights, "\n\n")
|
||||
} else {
|
||||
ctxDoc.Content = m.summarize(doc.Content)
|
||||
}
|
||||
default:
|
||||
ctxDoc.Content = doc.Content
|
||||
}
|
||||
|
||||
// 控制长度
|
||||
if len(ctxDoc.Content) > m.config.MaxContextLength {
|
||||
ctxDoc.Content = ctxDoc.Content[:m.config.MaxContextLength] + "\n\n[内容已截断...]"
|
||||
}
|
||||
|
||||
ctx.Documents = append(ctx.Documents, ctxDoc)
|
||||
}
|
||||
|
||||
ctx.Message = fmt.Sprintf("Retrieved %d relevant documents from knowledge base (latency: %dms)",
|
||||
len(ctx.Documents), resp.Latency)
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
// summarize 简单的内容摘要(截取前N个字符)
|
||||
func (m *RAGManager) summarize(content string, maxLen ...int) string {
|
||||
length := 500 // 默认500字符
|
||||
if len(maxLen) > 0 {
|
||||
length = maxLen[0]
|
||||
}
|
||||
|
||||
if len(content) <= length {
|
||||
return content
|
||||
}
|
||||
|
||||
// 尝试在句号或换行处截断
|
||||
truncated := content[:length]
|
||||
if idx := strings.LastIndexAny(truncated, "。\n."); idx > length/2 {
|
||||
return content[:idx+1]
|
||||
}
|
||||
|
||||
return truncated + "..."
|
||||
}
|
||||
|
||||
// FormatContextForAI 格式化上下文,供 AI 使用
|
||||
// 返回 Markdown 格式的文档上下文
|
||||
func (ctx *RAGContext) FormatContextForAI() string {
|
||||
if !ctx.Enabled || len(ctx.Documents) == 0 {
|
||||
return fmt.Sprintf("> ℹ️ %s\n", ctx.Message)
|
||||
}
|
||||
|
||||
var result strings.Builder
|
||||
|
||||
result.WriteString("## 📚 知识库参考文档\n\n")
|
||||
result.WriteString(fmt.Sprintf("> %s\n\n", ctx.Message))
|
||||
|
||||
for i, doc := range ctx.Documents {
|
||||
result.WriteString(fmt.Sprintf("### 参考文档 %d: %s\n\n", i+1, doc.Title))
|
||||
|
||||
// 元信息
|
||||
result.WriteString(fmt.Sprintf("**来源**: %s \n", doc.Source))
|
||||
if doc.URL != "" {
|
||||
result.WriteString(fmt.Sprintf("**链接**: %s \n", doc.URL))
|
||||
}
|
||||
result.WriteString(fmt.Sprintf("**相关度**: %.2f \n\n", doc.Score))
|
||||
|
||||
// 文档内容(重点)
|
||||
result.WriteString("**相关内容**:\n\n")
|
||||
result.WriteString("```\n")
|
||||
result.WriteString(doc.Content)
|
||||
result.WriteString("\n```\n\n")
|
||||
|
||||
// 高亮片段
|
||||
if len(doc.Highlights) > 0 {
|
||||
result.WriteString("**关键片段**:\n\n")
|
||||
for _, h := range doc.Highlights {
|
||||
result.WriteString(fmt.Sprintf("- %s\n", h))
|
||||
}
|
||||
result.WriteString("\n")
|
||||
}
|
||||
|
||||
result.WriteString("---\n\n")
|
||||
}
|
||||
|
||||
return result.String()
|
||||
}
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
// RAGContext 表示 RAG 查询返回的上下文
|
||||
type RAGContext struct {
|
||||
Enabled bool `json:"enabled"` // RAG 是否启用
|
||||
Documents []ContextDocument `json:"documents"` // 检索到的文档
|
||||
Message string `json:"message"` // 提示信息
|
||||
}
|
||||
|
||||
// ContextDocument 表示上下文中的一个文档
|
||||
type ContextDocument struct {
|
||||
Title string `json:"title"` // 文档标题
|
||||
Content string `json:"content"` // 文档内容(根据 context_mode 调整)
|
||||
Source string `json:"source"` // 来源路径
|
||||
URL string `json:"url"` // 在线链接
|
||||
Score float64 `json:"score"` // 相关度分数
|
||||
Highlights []string `json:"highlights"` // 高亮片段
|
||||
}
|
||||
|
||||
// ==================== 查询选项 ====================
|
||||
|
||||
// QueryOption 查询选项函数
|
||||
type QueryOption func(*RAGQuery)
|
||||
|
||||
// WithTopK 设置返回文档数量
|
||||
func WithTopK(k int) QueryOption {
|
||||
return func(q *RAGQuery) {
|
||||
q.TopK = k
|
||||
}
|
||||
}
|
||||
|
||||
// WithContextMode 设置上下文模式
|
||||
func WithContextMode(mode string) QueryOption {
|
||||
return func(q *RAGQuery) {
|
||||
q.ContextMode = mode
|
||||
}
|
||||
}
|
||||
|
||||
// WithFilters 设置过滤条件
|
||||
func WithFilters(filters map[string]interface{}) QueryOption {
|
||||
return func(q *RAGQuery) {
|
||||
q.Filters = filters
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,794 @@
|
||||
// MCP Server implementation for Nginx Migration Tools - Standalone Mode
|
||||
package standalone
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"nginx-migration-mcp/tools"
|
||||
)
|
||||
|
||||
// NewMCPServer creates a new MCP server instance
|
||||
func NewMCPServer(config *ServerConfig) *MCPServer {
|
||||
return &MCPServer{config: config}
|
||||
}
|
||||
|
||||
// HandleMessage processes an incoming MCP message
|
||||
func (s *MCPServer) HandleMessage(msg MCPMessage) MCPMessage {
|
||||
switch msg.Method {
|
||||
case "initialize":
|
||||
return s.handleInitialize(msg)
|
||||
case "tools/list":
|
||||
return s.handleToolsList(msg)
|
||||
case "tools/call":
|
||||
return s.handleToolsCall(msg)
|
||||
default:
|
||||
return s.errorResponse(msg.ID, -32601, "Method not found")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MCPServer) handleInitialize(msg MCPMessage) MCPMessage {
|
||||
return MCPMessage{
|
||||
JSONRPC: "2.0",
|
||||
ID: msg.ID,
|
||||
Result: map[string]interface{}{
|
||||
"protocolVersion": "2024-11-05",
|
||||
"capabilities": map[string]interface{}{
|
||||
"tools": map[string]interface{}{
|
||||
"listChanged": true,
|
||||
},
|
||||
},
|
||||
"serverInfo": map[string]interface{}{
|
||||
"name": s.config.Server.Name,
|
||||
"version": s.config.Server.Version,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MCPServer) handleToolsList(msg MCPMessage) MCPMessage {
|
||||
toolsList := tools.GetMCPTools()
|
||||
|
||||
return MCPMessage{
|
||||
JSONRPC: "2.0",
|
||||
ID: msg.ID,
|
||||
Result: map[string]interface{}{
|
||||
"tools": toolsList,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MCPServer) handleToolsCall(msg MCPMessage) MCPMessage {
|
||||
var params CallToolParams
|
||||
paramsBytes, _ := json.Marshal(msg.Params)
|
||||
json.Unmarshal(paramsBytes, ¶ms)
|
||||
|
||||
handlers := tools.GetToolHandlers(s)
|
||||
handler, exists := handlers[params.Name]
|
||||
|
||||
if !exists {
|
||||
return s.errorResponse(msg.ID, -32601, fmt.Sprintf("Unknown tool: %s", params.Name))
|
||||
}
|
||||
|
||||
result := handler(params.Arguments)
|
||||
|
||||
return MCPMessage{
|
||||
JSONRPC: "2.0",
|
||||
ID: msg.ID,
|
||||
Result: result,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MCPServer) errorResponse(id interface{}, code int, message string) MCPMessage {
|
||||
return MCPMessage{
|
||||
JSONRPC: "2.0",
|
||||
ID: id,
|
||||
Error: &MCPError{
|
||||
Code: code,
|
||||
Message: message,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Tool implementations
|
||||
|
||||
func (s *MCPServer) parseNginxConfig(args map[string]interface{}) tools.ToolResult {
|
||||
configContent, ok := args["config_content"].(string)
|
||||
if !ok {
|
||||
return tools.ToolResult{Content: []tools.Content{{Type: "text", Text: "Error: Missing config_content"}}}
|
||||
}
|
||||
|
||||
serverCount := strings.Count(configContent, "server {")
|
||||
locationCount := strings.Count(configContent, "location")
|
||||
hasSSL := strings.Contains(configContent, "ssl")
|
||||
hasProxy := strings.Contains(configContent, "proxy_pass")
|
||||
hasRewrite := strings.Contains(configContent, "rewrite")
|
||||
|
||||
complexity := "Simple"
|
||||
if serverCount > 1 || (hasRewrite && hasSSL) {
|
||||
complexity = "Complex"
|
||||
} else if hasRewrite || hasSSL {
|
||||
complexity = "Medium"
|
||||
}
|
||||
|
||||
analysis := fmt.Sprintf(`Nginx配置分析结果
|
||||
|
||||
基础信息:
|
||||
- Server块: %d个
|
||||
- Location块: %d个
|
||||
- SSL配置: %t
|
||||
- 反向代理: %t
|
||||
- URL重写: %t
|
||||
|
||||
复杂度: %s
|
||||
|
||||
迁移建议:`, serverCount, locationCount, hasSSL, hasProxy, hasRewrite, complexity)
|
||||
|
||||
if hasProxy {
|
||||
analysis += "\n- 反向代理将转换为HTTPRoute backendRefs"
|
||||
}
|
||||
if hasRewrite {
|
||||
analysis += "\n- URL重写将使用URLRewrite过滤器"
|
||||
}
|
||||
if hasSSL {
|
||||
analysis += "\n- SSL配置需要迁移到Gateway资源"
|
||||
}
|
||||
|
||||
return tools.ToolResult{Content: []tools.Content{{Type: "text", Text: analysis}}}
|
||||
}
|
||||
|
||||
func (s *MCPServer) convertToHigress(args map[string]interface{}) tools.ToolResult {
|
||||
configContent, ok := args["config_content"].(string)
|
||||
if !ok {
|
||||
return tools.ToolResult{Content: []tools.Content{{Type: "text", Text: "Error: Missing config_content"}}}
|
||||
}
|
||||
|
||||
namespace := s.config.Defaults.Namespace
|
||||
if ns, ok := args["namespace"].(string); ok {
|
||||
namespace = ns
|
||||
}
|
||||
|
||||
hostname := s.config.Defaults.Hostname
|
||||
lines := strings.Split(configContent, "\n")
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "server_name") && !strings.Contains(line, "#") {
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) >= 2 {
|
||||
hostname = strings.TrimSuffix(parts[1], ";")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
yamlConfig := fmt.Sprintf(`转换后的Higress配置
|
||||
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: %s
|
||||
namespace: %s
|
||||
annotations:
|
||||
higress.io/migrated-from: "nginx"
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: %s
|
||||
namespace: %s
|
||||
hostnames:
|
||||
- %s
|
||||
rules:
|
||||
- matches:
|
||||
- path:
|
||||
type: PathPrefix
|
||||
value: %s
|
||||
backendRefs:
|
||||
- name: %s
|
||||
port: %d
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: %s
|
||||
namespace: %s
|
||||
spec:
|
||||
selector:
|
||||
app: backend
|
||||
ports:
|
||||
- port: %d
|
||||
targetPort: %d
|
||||
|
||||
转换完成
|
||||
|
||||
应用步骤:
|
||||
1. 保存为 higress-config.yaml
|
||||
2. 执行: kubectl apply -f higress-config.yaml
|
||||
3. 验证: kubectl get httproute -n %s`,
|
||||
s.config.GenerateRouteName(hostname), namespace,
|
||||
s.config.Gateway.Name, s.config.Gateway.Namespace, hostname, s.config.Defaults.PathPrefix,
|
||||
s.config.GenerateServiceName(hostname), s.config.Service.DefaultPort,
|
||||
s.config.GenerateServiceName(hostname), namespace,
|
||||
s.config.Service.DefaultPort, s.config.Service.DefaultTarget, namespace)
|
||||
|
||||
return tools.ToolResult{Content: []tools.Content{{Type: "text", Text: yamlConfig}}}
|
||||
}
|
||||
|
||||
func (s *MCPServer) analyzeLuaPlugin(args map[string]interface{}) tools.ToolResult {
|
||||
luaCode, ok := args["lua_code"].(string)
|
||||
if !ok {
|
||||
return tools.ToolResult{Content: []tools.Content{{Type: "text", Text: "Error: Missing lua_code"}}}
|
||||
}
|
||||
|
||||
// 使用新的 AI 友好分析
|
||||
analysis := tools.AnalyzeLuaPluginForAI(luaCode)
|
||||
|
||||
// 生成用户友好的消息
|
||||
features := []string{}
|
||||
for feature := range analysis.Features {
|
||||
features = append(features, fmt.Sprintf("- %s", feature))
|
||||
}
|
||||
|
||||
userMessage := fmt.Sprintf(`✅ Lua 插件分析完成
|
||||
|
||||
📊 **检测到的特性**:
|
||||
%s
|
||||
|
||||
⚠️ **兼容性警告**:
|
||||
%s
|
||||
|
||||
📈 **复杂度**:%s
|
||||
🔄 **兼容性级别**:%s
|
||||
|
||||
💡 **迁移建议**:`,
|
||||
strings.Join(features, "\n"),
|
||||
strings.Join(analysis.Warnings, "\n- "),
|
||||
analysis.Complexity,
|
||||
analysis.Compatibility,
|
||||
)
|
||||
|
||||
switch analysis.Compatibility {
|
||||
case "full":
|
||||
userMessage += "\n- 可直接迁移到 WASM 插件\n- 建议使用工具链进行转换"
|
||||
case "partial":
|
||||
userMessage += "\n- 需要部分重构\n- 强烈建议使用工具链并让 AI 参与代码生成"
|
||||
case "manual":
|
||||
userMessage += "\n- 需要手动重写\n- 建议分步骤进行,使用工具链辅助"
|
||||
}
|
||||
|
||||
userMessage += "\n\n🔗 **后续操作**:\n"
|
||||
userMessage += "1. 调用 `generate_conversion_hints` 工具获取详细的转换提示\n"
|
||||
userMessage += "2. 基于提示生成 Go WASM 代码\n"
|
||||
userMessage += "3. 调用 `validate_wasm_code` 工具验证生成的代码\n"
|
||||
userMessage += "4. 调用 `generate_deployment_config` 工具生成部署配置\n"
|
||||
userMessage += "\n或者直接使用 `convert_lua_to_wasm` 进行一键转换。"
|
||||
|
||||
// 生成 AI 指令
|
||||
aiInstructions := fmt.Sprintf(`你现在已经获得了 Lua 插件的分析结果。基于这些信息,你可以:
|
||||
|
||||
### 选项 1:使用工具链进行精细控制
|
||||
|
||||
调用 generate_conversion_hints 工具,传入以下分析结果:
|
||||
`+"```json"+`
|
||||
{
|
||||
"analysis_result": %s,
|
||||
"plugin_name": "your-plugin-name"
|
||||
}
|
||||
`+"```"+`
|
||||
|
||||
这将为你提供代码生成模板,然后基于模板生成 Go WASM 代码。
|
||||
|
||||
### 选项 2:一键转换
|
||||
|
||||
如果用户希望快速转换,可以直接调用 convert_lua_to_wasm 工具。
|
||||
|
||||
### 建议的对话流程
|
||||
|
||||
1. **询问用户**:是否需要详细的转换提示,还是直接生成代码?
|
||||
2. **如果需要提示**:调用 generate_conversion_hints
|
||||
3. **生成代码后**:询问是否需要验证(调用 validate_wasm_code)
|
||||
4. **验证通过后**:询问是否需要生成部署配置(调用 generate_deployment_config)
|
||||
|
||||
### 关键注意事项
|
||||
|
||||
%s
|
||||
|
||||
### 代码生成要点
|
||||
|
||||
- 检测到的 Nginx 变量需要映射到 HTTP 头部
|
||||
- 复杂度为 %s,请相应调整代码结构
|
||||
- 兼容性级别为 %s,注意处理警告中的问题
|
||||
`,
|
||||
string(mustMarshalJSON(analysis)),
|
||||
formatWarningsForAI(analysis.Warnings),
|
||||
analysis.Complexity,
|
||||
analysis.Compatibility,
|
||||
)
|
||||
|
||||
return tools.FormatToolResultWithAIContext(userMessage, aiInstructions, analysis)
|
||||
}
|
||||
|
||||
func mustMarshalJSON(v interface{}) []byte {
|
||||
data, _ := json.Marshal(v)
|
||||
return data
|
||||
}
|
||||
|
||||
func formatWarningsForAI(warnings []string) string {
|
||||
if len(warnings) == 0 {
|
||||
return "- 无特殊警告,可以直接转换"
|
||||
}
|
||||
result := []string{}
|
||||
for _, w := range warnings {
|
||||
result = append(result, fmt.Sprintf("- ⚠️ %s", w))
|
||||
}
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
|
||||
func (s *MCPServer) convertLuaToWasm(args map[string]interface{}) tools.ToolResult {
|
||||
luaCode, ok := args["lua_code"].(string)
|
||||
if !ok {
|
||||
return tools.ToolResult{Content: []tools.Content{{Type: "text", Text: "Error: Missing lua_code"}}}
|
||||
}
|
||||
|
||||
pluginName, ok := args["plugin_name"].(string)
|
||||
if !ok {
|
||||
return tools.ToolResult{Content: []tools.Content{{Type: "text", Text: "Error: Missing plugin_name"}}}
|
||||
}
|
||||
|
||||
analyzer := tools.AnalyzeLuaScript(luaCode)
|
||||
result, err := tools.ConvertLuaToWasm(analyzer, pluginName)
|
||||
if err != nil {
|
||||
return tools.ToolResult{Content: []tools.Content{{Type: "text", Text: fmt.Sprintf("Error: %v", err)}}}
|
||||
}
|
||||
|
||||
response := fmt.Sprintf(`Lua脚本转换完成
|
||||
|
||||
转换分析:
|
||||
- 复杂度: %s
|
||||
- 检测特性: %d个
|
||||
- 兼容性警告: %d个
|
||||
|
||||
注意事项:
|
||||
%s
|
||||
|
||||
生成的文件:
|
||||
|
||||
==== main.go ====
|
||||
%s
|
||||
|
||||
==== WasmPlugin配置 ====
|
||||
%s
|
||||
|
||||
部署步骤:
|
||||
1. 创建插件目录: mkdir -p extensions/%s
|
||||
2. 保存Go代码到: extensions/%s/main.go
|
||||
3. 构建插件: PLUGIN_NAME=%s make build
|
||||
4. 应用配置: kubectl apply -f wasmplugin.yaml
|
||||
|
||||
提示:
|
||||
- 请根据实际需求调整配置
|
||||
- 测试插件功能后再部署到生产环境
|
||||
- 如有共享状态需求,请配置Redis等外部存储
|
||||
`,
|
||||
analyzer.Complexity,
|
||||
len(analyzer.Features),
|
||||
len(analyzer.Warnings),
|
||||
strings.Join(analyzer.Warnings, "\n- "),
|
||||
result.GoCode,
|
||||
result.WasmPluginYAML,
|
||||
pluginName, pluginName, pluginName)
|
||||
|
||||
return tools.ToolResult{Content: []tools.Content{{Type: "text", Text: response}}}
|
||||
}
|
||||
|
||||
// GenerateConversionHints 生成详细的代码转换提示
|
||||
func (s *MCPServer) GenerateConversionHints(args map[string]interface{}) tools.ToolResult {
|
||||
analysisResultStr, ok := args["analysis_result"].(string)
|
||||
if !ok {
|
||||
return tools.ToolResult{Content: []tools.Content{{Type: "text", Text: "Error: Missing analysis_result"}}}
|
||||
}
|
||||
|
||||
pluginName, ok := args["plugin_name"].(string)
|
||||
if !ok {
|
||||
return tools.ToolResult{Content: []tools.Content{{Type: "text", Text: "Error: Missing plugin_name"}}}
|
||||
}
|
||||
|
||||
// 解析分析结果
|
||||
var analysis tools.AnalysisResultForAI
|
||||
if err := json.Unmarshal([]byte(analysisResultStr), &analysis); err != nil {
|
||||
return tools.ToolResult{Content: []tools.Content{{Type: "text", Text: fmt.Sprintf("Error parsing analysis_result: %v", err)}}}
|
||||
}
|
||||
|
||||
// 生成转换提示
|
||||
hints := tools.GenerateConversionHints(analysis, pluginName)
|
||||
|
||||
// 格式化输出
|
||||
userMessage := fmt.Sprintf(`🎯 代码转换提示
|
||||
|
||||
**插件名称**: %s
|
||||
**代码模板**: %s
|
||||
|
||||
%s
|
||||
`,
|
||||
pluginName,
|
||||
hints.CodeTemplate,
|
||||
func() string {
|
||||
if len(hints.Warnings) > 0 {
|
||||
return "⚠️ **警告**: " + formatWarningsListForUser(hints.Warnings)
|
||||
}
|
||||
return ""
|
||||
}(),
|
||||
)
|
||||
|
||||
// 生成详细的 AI 指令
|
||||
aiInstructions := fmt.Sprintf(`现在你需要基于以下信息生成 Go WASM 插件代码。
|
||||
|
||||
## 代码模板
|
||||
|
||||
%s
|
||||
|
||||
## 生成代码的要求
|
||||
|
||||
1. **实现所需的回调函数**
|
||||
2. **保持 Lua 代码的业务逻辑等价**
|
||||
3. **添加适当的错误处理**
|
||||
4. **包含配置解析逻辑(如需要)**
|
||||
|
||||
## 输出格式
|
||||
|
||||
请按以下格式输出代码:
|
||||
|
||||
### main.go
|
||||
`+"```go"+`
|
||||
[完整的 Go 代码]
|
||||
`+"```"+`
|
||||
|
||||
生成代码后,建议调用 validate_wasm_code 工具进行验证。
|
||||
`,
|
||||
hints.CodeTemplate,
|
||||
)
|
||||
|
||||
return tools.FormatToolResultWithAIContext(userMessage, aiInstructions, hints)
|
||||
}
|
||||
|
||||
// ValidateWasmCode 验证生成的 Go WASM 代码
|
||||
func (s *MCPServer) ValidateWasmCode(args map[string]interface{}) tools.ToolResult {
|
||||
goCode, ok := args["go_code"].(string)
|
||||
if !ok {
|
||||
return tools.ToolResult{Content: []tools.Content{{Type: "text", Text: "Error: Missing go_code"}}}
|
||||
}
|
||||
|
||||
pluginName, ok := args["plugin_name"].(string)
|
||||
if !ok {
|
||||
return tools.ToolResult{Content: []tools.Content{{Type: "text", Text: "Error: Missing plugin_name"}}}
|
||||
}
|
||||
|
||||
// 执行验证
|
||||
report := tools.ValidateWasmCode(goCode, pluginName)
|
||||
|
||||
// 统计各类问题数量
|
||||
requiredCount := 0
|
||||
recommendedCount := 0
|
||||
optionalCount := 0
|
||||
bestPracticeCount := 0
|
||||
|
||||
for _, issue := range report.Issues {
|
||||
switch issue.Category {
|
||||
case "required":
|
||||
requiredCount++
|
||||
case "recommended":
|
||||
recommendedCount++
|
||||
case "optional":
|
||||
optionalCount++
|
||||
case "best_practice":
|
||||
bestPracticeCount++
|
||||
}
|
||||
}
|
||||
|
||||
// 构建用户消息
|
||||
userMessage := fmt.Sprintf(`## 代码验证报告
|
||||
|
||||
%s
|
||||
|
||||
### 发现的回调函数 (%d 个)
|
||||
%s
|
||||
|
||||
### 配置结构
|
||||
%s
|
||||
|
||||
### 问题分类
|
||||
|
||||
#### 必须修复 (%d 个)
|
||||
%s
|
||||
|
||||
#### 建议修复 (%d 个)
|
||||
%s
|
||||
|
||||
#### 可选优化 (%d 个)
|
||||
%s
|
||||
|
||||
#### 最佳实践 (%d 个)
|
||||
%s
|
||||
|
||||
### 缺失的导入包 (%d 个)
|
||||
%s
|
||||
|
||||
---
|
||||
|
||||
`,
|
||||
report.Summary,
|
||||
len(report.FoundCallbacks),
|
||||
formatCallbacksList(report.FoundCallbacks),
|
||||
formatConfigStatus(report.HasConfig),
|
||||
requiredCount,
|
||||
formatIssuesByCategory(report.Issues, "required"),
|
||||
recommendedCount,
|
||||
formatIssuesByCategory(report.Issues, "recommended"),
|
||||
optionalCount,
|
||||
formatIssuesByCategory(report.Issues, "optional"),
|
||||
bestPracticeCount,
|
||||
formatIssuesByCategory(report.Issues, "best_practice"),
|
||||
len(report.MissingImports),
|
||||
formatList(report.MissingImports),
|
||||
)
|
||||
|
||||
// 根据问题级别给出建议
|
||||
hasRequired := requiredCount > 0
|
||||
if hasRequired {
|
||||
userMessage += " **请优先修复 \"必须修复\" 的问题,否则代码可能无法编译或运行。**\n\n"
|
||||
} else if recommendedCount > 0 {
|
||||
userMessage += " **代码基本结构正确。** 建议修复 \"建议修复\" 的问题以提高代码质量。\n\n"
|
||||
} else {
|
||||
userMessage += " **代码验证通过!** 可以继续生成部署配置。\n\n"
|
||||
userMessage += "**下一步**:调用 `generate_deployment_config` 工具生成部署配置。\n"
|
||||
}
|
||||
|
||||
// AI 指令
|
||||
aiInstructions := ""
|
||||
if hasRequired {
|
||||
aiInstructions = `代码验证发现必须修复的问题。
|
||||
|
||||
## 修复指南
|
||||
|
||||
` + formatIssuesForAI(report.Issues, "required") + `
|
||||
|
||||
请修复上述问题后,再次调用 validate_wasm_code 工具进行验证。
|
||||
`
|
||||
} else if recommendedCount > 0 {
|
||||
aiInstructions = `代码基本结构正确,建议修复以下问题:
|
||||
|
||||
` + formatIssuesForAI(report.Issues, "recommended") + `
|
||||
|
||||
可以选择修复这些问题,或直接调用 generate_deployment_config 工具生成部署配置。
|
||||
`
|
||||
} else {
|
||||
aiInstructions = `代码验证通过!
|
||||
|
||||
## 下一步
|
||||
|
||||
调用 generate_deployment_config 工具,参数:
|
||||
` + "```json" + `
|
||||
{
|
||||
"plugin_name": "` + pluginName + `",
|
||||
"go_code": "[验证通过的代码]",
|
||||
"namespace": "higress-system"
|
||||
}
|
||||
` + "```" + `
|
||||
|
||||
这将生成完整的部署配置包。
|
||||
`
|
||||
}
|
||||
|
||||
return tools.FormatToolResultWithAIContext(userMessage, aiInstructions, report)
|
||||
}
|
||||
|
||||
// GenerateDeploymentConfig 生成部署配置
|
||||
func (s *MCPServer) GenerateDeploymentConfig(args map[string]interface{}) tools.ToolResult {
|
||||
pluginName, ok := args["plugin_name"].(string)
|
||||
if !ok {
|
||||
return tools.ToolResult{Content: []tools.Content{{Type: "text", Text: "Error: Missing plugin_name"}}}
|
||||
}
|
||||
|
||||
goCode, ok := args["go_code"].(string)
|
||||
if !ok {
|
||||
return tools.ToolResult{Content: []tools.Content{{Type: "text", Text: "Error: Missing go_code"}}}
|
||||
}
|
||||
|
||||
namespace := "higress-system"
|
||||
if ns, ok := args["namespace"].(string); ok && ns != "" {
|
||||
namespace = ns
|
||||
}
|
||||
|
||||
configSchema := ""
|
||||
if cs, ok := args["config_schema"].(string); ok {
|
||||
configSchema = cs
|
||||
}
|
||||
|
||||
// 生成部署包
|
||||
pkg := tools.GenerateDeploymentPackage(pluginName, goCode, configSchema, namespace)
|
||||
|
||||
// 格式化输出
|
||||
userMessage := fmt.Sprintf(`🎉 部署配置生成完成!
|
||||
|
||||
已为插件 **%s** 生成完整的部署配置包。
|
||||
|
||||
## 生成的文件
|
||||
|
||||
### 1. WasmPlugin 配置
|
||||
- 文件名:wasmplugin.yaml
|
||||
- 命名空间:%s
|
||||
- 包含默认配置和匹配规则
|
||||
|
||||
### 2. 构建脚本
|
||||
- Makefile:自动化构建和部署
|
||||
- Dockerfile:容器化打包
|
||||
|
||||
### 3. 文档
|
||||
- README.md:完整的使用说明
|
||||
- 包含快速开始、配置说明、问题排查
|
||||
|
||||
### 4. 测试脚本
|
||||
- test.sh:自动化测试脚本
|
||||
|
||||
### 5. 依赖清单
|
||||
- 列出了所有必需的 Go 模块
|
||||
|
||||
---
|
||||
|
||||
## 快速部署
|
||||
|
||||
`+"```bash"+`
|
||||
# 1. 保存文件
|
||||
# 保存 main.go
|
||||
# 保存 wasmplugin.yaml
|
||||
# 保存 Makefile
|
||||
# 保存 Dockerfile
|
||||
|
||||
# 2. 构建插件
|
||||
make build
|
||||
|
||||
# 3. 构建并推送镜像
|
||||
make docker-build docker-push
|
||||
|
||||
# 4. 部署到 Kubernetes
|
||||
make deploy
|
||||
|
||||
# 5. 验证部署
|
||||
kubectl get wasmplugin -n %s
|
||||
`+"```"+`
|
||||
|
||||
---
|
||||
|
||||
**文件内容请见下方结构化数据部分。**
|
||||
`,
|
||||
pluginName,
|
||||
namespace,
|
||||
namespace,
|
||||
)
|
||||
|
||||
aiInstructions := fmt.Sprintf(`部署配置已生成完毕。
|
||||
|
||||
## 向用户展示文件
|
||||
|
||||
请将以下文件内容清晰地展示给用户:
|
||||
|
||||
### 1. main.go
|
||||
用户已经有这个文件。
|
||||
|
||||
### 2. wasmplugin.yaml
|
||||
`+"```yaml"+`
|
||||
%s
|
||||
`+"```"+`
|
||||
|
||||
### 3. Makefile
|
||||
`+"```makefile"+`
|
||||
%s
|
||||
`+"```"+`
|
||||
|
||||
### 4. Dockerfile
|
||||
`+"```dockerfile"+`
|
||||
%s
|
||||
`+"```"+`
|
||||
|
||||
### 5. README.md
|
||||
`+"```markdown"+`
|
||||
%s
|
||||
`+"```"+`
|
||||
|
||||
### 6. test.sh
|
||||
`+"```bash"+`
|
||||
%s
|
||||
`+"```"+`
|
||||
|
||||
## 后续支持
|
||||
|
||||
询问用户是否需要:
|
||||
1. 解释任何配置项的含义
|
||||
2. 自定义某些配置
|
||||
3. 帮助解决部署问题
|
||||
`,
|
||||
pkg.WasmPluginYAML,
|
||||
pkg.Makefile,
|
||||
pkg.Dockerfile,
|
||||
pkg.README,
|
||||
pkg.TestScript,
|
||||
)
|
||||
|
||||
return tools.FormatToolResultWithAIContext(userMessage, aiInstructions, pkg)
|
||||
}
|
||||
|
||||
// 辅助格式化函数
|
||||
|
||||
func formatWarningsListForUser(warnings []string) string {
|
||||
if len(warnings) == 0 {
|
||||
return "无"
|
||||
}
|
||||
return strings.Join(warnings, "\n- ")
|
||||
}
|
||||
|
||||
func formatCallbacksList(callbacks []string) string {
|
||||
if len(callbacks) == 0 {
|
||||
return "无"
|
||||
}
|
||||
return "- " + strings.Join(callbacks, "\n- ")
|
||||
}
|
||||
|
||||
func formatConfigStatus(hasConfig bool) string {
|
||||
if hasConfig {
|
||||
return " 已定义配置结构体"
|
||||
}
|
||||
return "- 未定义配置结构体(如不需要配置可忽略)"
|
||||
}
|
||||
|
||||
func formatIssuesByCategory(issues []tools.ValidationIssue, category string) string {
|
||||
var filtered []string
|
||||
for _, issue := range issues {
|
||||
if issue.Category == category {
|
||||
filtered = append(filtered, fmt.Sprintf("- **[%s]** %s\n 💡 建议: %s\n 📌 影响: %s",
|
||||
issue.Type, issue.Message, issue.Suggestion, issue.Impact))
|
||||
}
|
||||
}
|
||||
if len(filtered) == 0 {
|
||||
return "无"
|
||||
}
|
||||
return strings.Join(filtered, "\n\n")
|
||||
}
|
||||
|
||||
func formatIssuesForAI(issues []tools.ValidationIssue, category string) string {
|
||||
var filtered []tools.ValidationIssue
|
||||
for _, issue := range issues {
|
||||
if issue.Category == category {
|
||||
filtered = append(filtered, issue)
|
||||
}
|
||||
}
|
||||
|
||||
if len(filtered) == 0 {
|
||||
return "无问题"
|
||||
}
|
||||
|
||||
result := []string{}
|
||||
for i, issue := range filtered {
|
||||
result = append(result, fmt.Sprintf(`
|
||||
### 问题 %d: %s
|
||||
|
||||
**类型**: %s
|
||||
**建议**: %s
|
||||
**影响**: %s
|
||||
|
||||
请根据建议修复此问题。
|
||||
`,
|
||||
i+1,
|
||||
issue.Message,
|
||||
issue.Type,
|
||||
issue.Suggestion,
|
||||
issue.Impact,
|
||||
))
|
||||
}
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
|
||||
func formatList(items []string) string {
|
||||
if len(items) == 0 {
|
||||
return "无"
|
||||
}
|
||||
return "- " + strings.Join(items, "\n- ")
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"name": "nginx-migration",
|
||||
"description": "Nginx 到 Higress 迁移工具集:支持配置转换和 Lua 插件迁移",
|
||||
"tools": [
|
||||
{
|
||||
"name": "parse_nginx_config",
|
||||
"description": "解析和分析 Nginx 配置文件,识别配置结构和复杂度",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"config_content": {
|
||||
"type": "string",
|
||||
"description": "Nginx 配置文件内容"
|
||||
}
|
||||
},
|
||||
"required": ["config_content"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "convert_to_higress",
|
||||
"description": "智能解析 Nginx 配置并通过 AI 推理生成 Higress Ingress/HTTPRoute 配置。支持复杂配置(SSL、重写、重定向、upstream 等)的智能转换,结合 RAG 知识库提供准确的转换方案",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"config_content": {
|
||||
"type": "string",
|
||||
"description": "Nginx 配置文件内容"
|
||||
},
|
||||
"namespace": {
|
||||
"type": "string",
|
||||
"description": "目标 Kubernetes 命名空间",
|
||||
"default": "default"
|
||||
},
|
||||
"use_gateway_api": {
|
||||
"type": "boolean",
|
||||
"description": "是否使用 Gateway API (HTTPRoute)。默认 false,使用 Ingress",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"required": ["config_content"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "analyze_lua_plugin",
|
||||
"description": "分析 Nginx Lua 插件的兼容性,识别使用的 API 和潜在迁移问题,返回结构化分析结果供后续工具使用",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"lua_code": {
|
||||
"type": "string",
|
||||
"description": "Nginx Lua 插件代码"
|
||||
}
|
||||
},
|
||||
"required": ["lua_code"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "generate_conversion_hints",
|
||||
"description": "基于 Lua 分析结果生成代码转换模板",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"analysis_result": {
|
||||
"type": "string",
|
||||
"description": "analyze_lua_plugin 返回的 JSON 格式分析结果"
|
||||
},
|
||||
"plugin_name": {
|
||||
"type": "string",
|
||||
"description": "目标插件名称(小写字母和连字符)"
|
||||
}
|
||||
},
|
||||
"required": ["analysis_result", "plugin_name"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "validate_wasm_code",
|
||||
"description": "验证生成的 Go WASM 插件代码,检查语法、API 使用、配置结构等,输出验证报告和改进建议",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"go_code": {
|
||||
"type": "string",
|
||||
"description": "生成的 Go WASM 插件代码"
|
||||
},
|
||||
"plugin_name": {
|
||||
"type": "string",
|
||||
"description": "插件名称"
|
||||
}
|
||||
},
|
||||
"required": ["go_code", "plugin_name"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "generate_deployment_config",
|
||||
"description": "为验证通过的 WASM 插件生成完整的部署配置包,包括 WasmPlugin YAML、Makefile、Dockerfile、README 和测试脚本",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"plugin_name": {
|
||||
"type": "string",
|
||||
"description": "插件名称"
|
||||
},
|
||||
"go_code": {
|
||||
"type": "string",
|
||||
"description": "验证通过的 Go 代码"
|
||||
},
|
||||
"config_schema": {
|
||||
"type": "string",
|
||||
"description": "配置 JSON Schema(可选)"
|
||||
},
|
||||
"namespace": {
|
||||
"type": "string",
|
||||
"description": "部署命名空间",
|
||||
"default": "higress-system"
|
||||
}
|
||||
},
|
||||
"required": ["plugin_name", "go_code"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "convert_lua_to_wasm",
|
||||
"description": "一键将 Nginx Lua 脚本转换为 Higress WASM 插件,自动生成 Go 代码和 WasmPlugin 配置。适合简单插件快速转换",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"lua_code": {
|
||||
"type": "string",
|
||||
"description": "要转换的 Nginx Lua 插件代码"
|
||||
},
|
||||
"plugin_name": {
|
||||
"type": "string",
|
||||
"description": "生成的 WASM 插件名称 (小写字母和连字符)"
|
||||
}
|
||||
},
|
||||
"required": ["lua_code", "plugin_name"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"nginx-migration-mcp/standalone"
|
||||
)
|
||||
|
||||
const Version = "1.0.0"
|
||||
|
||||
func main() {
|
||||
// Load config
|
||||
config := standalone.LoadConfig()
|
||||
|
||||
server := standalone.NewMCPServer(config)
|
||||
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
writer := bufio.NewWriter(os.Stdout)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Bytes()
|
||||
|
||||
var msg standalone.MCPMessage
|
||||
if err := json.Unmarshal(line, &msg); err != nil {
|
||||
log.Printf("Error parsing message: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
response := server.HandleMessage(msg)
|
||||
responseBytes, _ := json.Marshal(response)
|
||||
|
||||
writer.Write(responseBytes)
|
||||
writer.WriteByte('\n')
|
||||
writer.Flush()
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error reading from stdin: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
// Configuration management for nginx migration MCP server - Standalone Mode
|
||||
package standalone
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ServerConfig holds all configurable values
|
||||
type ServerConfig struct {
|
||||
Server ServerSettings `json:"server"`
|
||||
Gateway GatewaySettings `json:"gateway"`
|
||||
Service ServiceSettings `json:"service"`
|
||||
Defaults DefaultSettings `json:"defaults"`
|
||||
}
|
||||
|
||||
type ServerSettings struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Port string `json:"port"`
|
||||
APIBaseURL string `json:"api_base_url"`
|
||||
}
|
||||
|
||||
type GatewaySettings struct {
|
||||
Name string `json:"name"`
|
||||
Namespace string `json:"namespace"`
|
||||
}
|
||||
|
||||
type ServiceSettings struct {
|
||||
DefaultName string `json:"default_name"`
|
||||
DefaultPort int `json:"default_port"`
|
||||
DefaultTarget int `json:"default_target_port"`
|
||||
}
|
||||
|
||||
type DefaultSettings struct {
|
||||
Hostname string `json:"hostname"`
|
||||
Namespace string `json:"namespace"`
|
||||
PathPrefix string `json:"path_prefix"`
|
||||
RoutePrefix string `json:"route_prefix"`
|
||||
}
|
||||
|
||||
// LoadConfig loads configuration from environment variables and files
|
||||
func LoadConfig() *ServerConfig {
|
||||
config := &ServerConfig{
|
||||
Server: ServerSettings{
|
||||
Name: getEnvOrDefault("NGINX_MCP_SERVER_NAME", "nginx-migration-mcp"),
|
||||
Version: getEnvOrDefault("NGINX_MCP_VERSION", "1.0.0"),
|
||||
Port: getEnvOrDefault("NGINX_MCP_PORT", "8080"),
|
||||
APIBaseURL: getEnvOrDefault("NGINX_MIGRATION_API_URL", "http://localhost:8080"),
|
||||
},
|
||||
Gateway: GatewaySettings{
|
||||
Name: getEnvOrDefault("HIGRESS_GATEWAY_NAME", "higress-gateway"),
|
||||
Namespace: getEnvOrDefault("HIGRESS_GATEWAY_NAMESPACE", "higress-system"),
|
||||
},
|
||||
Service: ServiceSettings{
|
||||
DefaultName: getEnvOrDefault("DEFAULT_SERVICE_NAME", "backend-service"),
|
||||
DefaultPort: getIntEnvOrDefault("DEFAULT_SERVICE_PORT", 80),
|
||||
DefaultTarget: getIntEnvOrDefault("DEFAULT_TARGET_PORT", 8080),
|
||||
},
|
||||
Defaults: DefaultSettings{
|
||||
Hostname: getEnvOrDefault("DEFAULT_HOSTNAME", "example.com"),
|
||||
Namespace: getEnvOrDefault("DEFAULT_NAMESPACE", "default"),
|
||||
PathPrefix: getEnvOrDefault("DEFAULT_PATH_PREFIX", "/"),
|
||||
RoutePrefix: getEnvOrDefault("ROUTE_NAME_PREFIX", "nginx-migrated"),
|
||||
},
|
||||
}
|
||||
|
||||
// Try to load from config file if exists
|
||||
if configFile := os.Getenv("NGINX_MCP_CONFIG_FILE"); configFile != "" {
|
||||
if err := loadConfigFromFile(config, configFile); err != nil {
|
||||
fmt.Printf("Warning: Failed to load config from %s: %v\n", configFile, err)
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// loadConfigFromFile loads configuration from JSON file
|
||||
func loadConfigFromFile(config *ServerConfig, filename string) error {
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return json.Unmarshal(data, config)
|
||||
}
|
||||
|
||||
// getEnvOrDefault returns environment variable value or default
|
||||
func getEnvOrDefault(key, defaultValue string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// getIntEnvOrDefault returns environment variable as int or default
|
||||
func getIntEnvOrDefault(key string, defaultValue int) int {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
if intValue, err := strconv.Atoi(value); err == nil {
|
||||
return intValue
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// GenerateRouteName generates a unique route name
|
||||
func (c *ServerConfig) GenerateRouteName(hostname string) string {
|
||||
if hostname == "" || hostname == c.Defaults.Hostname {
|
||||
return fmt.Sprintf("%s-route", c.Defaults.RoutePrefix)
|
||||
}
|
||||
// Replace dots and special characters for valid k8s name
|
||||
safeName := hostname
|
||||
for _, char := range []string{".", "_", ":"} {
|
||||
safeName = strings.ReplaceAll(safeName, char, "-")
|
||||
}
|
||||
return fmt.Sprintf("%s-%s", c.Defaults.RoutePrefix, safeName)
|
||||
}
|
||||
|
||||
// GenerateIngressName generates a unique ingress name
|
||||
func (c *ServerConfig) GenerateIngressName(hostname string) string {
|
||||
if hostname == "" || hostname == c.Defaults.Hostname {
|
||||
return fmt.Sprintf("%s-ingress", c.Defaults.RoutePrefix)
|
||||
}
|
||||
// Replace dots and special characters for valid k8s name
|
||||
safeName := hostname
|
||||
for _, char := range []string{".", "_", ":"} {
|
||||
safeName = strings.ReplaceAll(safeName, char, "-")
|
||||
}
|
||||
return fmt.Sprintf("%s-%s", c.Defaults.RoutePrefix, safeName)
|
||||
}
|
||||
|
||||
// GenerateServiceName generates service name based on hostname
|
||||
func (c *ServerConfig) GenerateServiceName(hostname string) string {
|
||||
if hostname == "" || hostname == c.Defaults.Hostname {
|
||||
return c.Service.DefaultName
|
||||
}
|
||||
return fmt.Sprintf("%s-service", hostname)
|
||||
}
|
||||
@@ -0,0 +1,914 @@
|
||||
// Package standalone implements MCP Server for Nginx Migration Tools in standalone mode.
|
||||
package standalone
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"nginx-migration-mcp/internal/rag"
|
||||
"nginx-migration-mcp/tools"
|
||||
)
|
||||
|
||||
// NewMCPServer creates a new MCP server instance
|
||||
func NewMCPServer(config *ServerConfig) *MCPServer {
|
||||
// 初始化 RAG 管理器
|
||||
// 获取可执行文件所在目录
|
||||
execPath, err := os.Executable()
|
||||
if err != nil {
|
||||
log.Printf("WARNING: Failed to get executable path: %v", err)
|
||||
execPath = "."
|
||||
}
|
||||
execDir := filepath.Dir(execPath)
|
||||
|
||||
// 尝试多个可能的配置文件路径(相对于可执行文件)
|
||||
ragConfigPaths := []string{
|
||||
filepath.Join(execDir, "config", "rag.json"), // 同级 config 目录
|
||||
filepath.Join(execDir, "..", "config", "rag.json"), // 上级 config 目录
|
||||
"config/rag.json", // 当前工作目录
|
||||
}
|
||||
|
||||
var ragConfig *rag.RAGConfig
|
||||
var configErr error
|
||||
|
||||
for _, path := range ragConfigPaths {
|
||||
ragConfig, configErr = rag.LoadRAGConfig(path)
|
||||
if configErr == nil {
|
||||
log.Printf("Loaded RAG config from: %s", path)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if configErr != nil {
|
||||
log.Printf("WARNING: Failed to load RAG config: %v, RAG will be disabled", configErr)
|
||||
ragConfig = &rag.RAGConfig{Enabled: false}
|
||||
}
|
||||
|
||||
ragManager := rag.NewRAGManager(ragConfig)
|
||||
|
||||
if ragManager.IsEnabled() {
|
||||
log.Printf("RAG Manager initialized and enabled")
|
||||
} else {
|
||||
log.Printf("RAG Manager disabled, using rule-based approach")
|
||||
}
|
||||
|
||||
return &MCPServer{
|
||||
config: config,
|
||||
ragManager: ragManager,
|
||||
}
|
||||
}
|
||||
|
||||
// HandleMessage processes an incoming MCP message
|
||||
func (s *MCPServer) HandleMessage(msg MCPMessage) MCPMessage {
|
||||
switch msg.Method {
|
||||
case "initialize":
|
||||
return s.handleInitialize(msg)
|
||||
case "tools/list":
|
||||
return s.handleToolsList(msg)
|
||||
case "tools/call":
|
||||
return s.handleToolsCall(msg)
|
||||
default:
|
||||
return s.errorResponse(msg.ID, -32601, "Method not found")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MCPServer) handleInitialize(msg MCPMessage) MCPMessage {
|
||||
return MCPMessage{
|
||||
JSONRPC: "2.0",
|
||||
ID: msg.ID,
|
||||
Result: map[string]interface{}{
|
||||
"protocolVersion": "2024-11-05",
|
||||
"capabilities": map[string]interface{}{
|
||||
"tools": map[string]interface{}{
|
||||
"listChanged": true,
|
||||
},
|
||||
},
|
||||
"serverInfo": map[string]interface{}{
|
||||
"name": s.config.Server.Name,
|
||||
"version": s.config.Server.Version,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MCPServer) handleToolsList(msg MCPMessage) MCPMessage {
|
||||
toolsList := tools.GetMCPTools()
|
||||
|
||||
return MCPMessage{
|
||||
JSONRPC: "2.0",
|
||||
ID: msg.ID,
|
||||
Result: map[string]interface{}{
|
||||
"tools": toolsList,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MCPServer) handleToolsCall(msg MCPMessage) MCPMessage {
|
||||
var params CallToolParams
|
||||
paramsBytes, _ := json.Marshal(msg.Params)
|
||||
json.Unmarshal(paramsBytes, ¶ms)
|
||||
|
||||
handlers := tools.GetToolHandlers(s)
|
||||
handler, exists := handlers[params.Name]
|
||||
|
||||
if !exists {
|
||||
return s.errorResponse(msg.ID, -32601, fmt.Sprintf("Unknown tool: %s", params.Name))
|
||||
}
|
||||
|
||||
result := handler(params.Arguments)
|
||||
|
||||
return MCPMessage{
|
||||
JSONRPC: "2.0",
|
||||
ID: msg.ID,
|
||||
Result: result,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MCPServer) errorResponse(id interface{}, code int, message string) MCPMessage {
|
||||
return MCPMessage{
|
||||
JSONRPC: "2.0",
|
||||
ID: id,
|
||||
Error: &MCPError{
|
||||
Code: code,
|
||||
Message: message,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Tool implementations
|
||||
|
||||
func (s *MCPServer) ParseNginxConfig(args map[string]interface{}) tools.ToolResult {
|
||||
configContent, ok := args["config_content"].(string)
|
||||
if !ok {
|
||||
return tools.ToolResult{Content: []tools.Content{{Type: "text", Text: "Error: Missing config_content"}}}
|
||||
}
|
||||
|
||||
serverCount := strings.Count(configContent, "server {")
|
||||
locationCount := strings.Count(configContent, "location")
|
||||
hasSSL := strings.Contains(configContent, "ssl")
|
||||
hasProxy := strings.Contains(configContent, "proxy_pass")
|
||||
hasRewrite := strings.Contains(configContent, "rewrite")
|
||||
|
||||
complexity := "Simple"
|
||||
if serverCount > 1 || (hasRewrite && hasSSL) {
|
||||
complexity = "Complex"
|
||||
} else if hasRewrite || hasSSL {
|
||||
complexity = "Medium"
|
||||
}
|
||||
|
||||
analysis := fmt.Sprintf(`Nginx配置分析结果
|
||||
|
||||
基础信息:
|
||||
- Server块: %d个
|
||||
- Location块: %d个
|
||||
- SSL配置: %t
|
||||
- 反向代理: %t
|
||||
- URL重写: %t
|
||||
|
||||
复杂度: %s
|
||||
|
||||
迁移建议:`, serverCount, locationCount, hasSSL, hasProxy, hasRewrite, complexity)
|
||||
|
||||
if hasProxy {
|
||||
analysis += "\n- 反向代理将转换为Ingress backend配置"
|
||||
}
|
||||
if hasRewrite {
|
||||
analysis += "\n- URL重写将使用Higress注解 (higress.io/rewrite-target)"
|
||||
}
|
||||
if hasSSL {
|
||||
analysis += "\n- SSL配置将转换为Ingress TLS配置"
|
||||
}
|
||||
|
||||
return tools.ToolResult{Content: []tools.Content{{Type: "text", Text: analysis}}}
|
||||
}
|
||||
|
||||
func (s *MCPServer) ConvertToHigress(args map[string]interface{}) tools.ToolResult {
|
||||
configContent, ok := args["config_content"].(string)
|
||||
if !ok {
|
||||
return tools.ToolResult{Content: []tools.Content{{Type: "text", Text: "Error: Missing config_content"}}}
|
||||
}
|
||||
|
||||
namespace := s.config.Defaults.Namespace
|
||||
if ns, ok := args["namespace"].(string); ok {
|
||||
namespace = ns
|
||||
}
|
||||
|
||||
// 检查是否使用 Gateway API
|
||||
useGatewayAPI := false
|
||||
if val, ok := args["use_gateway_api"].(bool); ok {
|
||||
useGatewayAPI = val
|
||||
}
|
||||
|
||||
// === 使用增强的解析器解析 Nginx 配置 ===
|
||||
nginxConfig, err := tools.ParseNginxConfig(configContent)
|
||||
if err != nil {
|
||||
return tools.ToolResult{Content: []tools.Content{{Type: "text", Text: fmt.Sprintf("Error parsing Nginx config: %v", err)}}}
|
||||
}
|
||||
|
||||
// 分析配置
|
||||
analysis := tools.AnalyzeNginxConfig(nginxConfig)
|
||||
|
||||
// === RAG 增强:查询转换示例和最佳实践 ===
|
||||
var ragContext string
|
||||
if s.ragManager != nil && s.ragManager.IsEnabled() {
|
||||
// 构建查询关键词
|
||||
queryBuilder := []string{"Nginx 配置转换到 Higress"}
|
||||
|
||||
if useGatewayAPI {
|
||||
queryBuilder = append(queryBuilder, "Gateway API HTTPRoute")
|
||||
} else {
|
||||
queryBuilder = append(queryBuilder, "Kubernetes Ingress")
|
||||
}
|
||||
|
||||
// 根据特性添加查询关键词
|
||||
if analysis.Features["ssl"] {
|
||||
queryBuilder = append(queryBuilder, "SSL TLS 证书配置")
|
||||
}
|
||||
if analysis.Features["rewrite"] {
|
||||
queryBuilder = append(queryBuilder, "URL 重写 rewrite 规则")
|
||||
}
|
||||
if analysis.Features["redirect"] {
|
||||
queryBuilder = append(queryBuilder, "重定向 redirect")
|
||||
}
|
||||
if analysis.Features["header_manipulation"] {
|
||||
queryBuilder = append(queryBuilder, "请求头 响应头处理")
|
||||
}
|
||||
if len(nginxConfig.Upstreams) > 0 {
|
||||
queryBuilder = append(queryBuilder, "负载均衡 upstream")
|
||||
}
|
||||
|
||||
queryString := strings.Join(queryBuilder, " ")
|
||||
log.Printf("RAG Query: %s", queryString)
|
||||
|
||||
ragResult, err := s.ragManager.QueryForTool(
|
||||
"convert_to_higress",
|
||||
queryString,
|
||||
"nginx_to_higress",
|
||||
)
|
||||
|
||||
if err == nil && ragResult.Enabled && len(ragResult.Documents) > 0 {
|
||||
log.Printf("RAG: Found %d documents for conversion", len(ragResult.Documents))
|
||||
ragContext = "\n\n## 参考文档(来自知识库)\n\n" + ragResult.FormatContextForAI()
|
||||
} else {
|
||||
if err != nil {
|
||||
log.Printf("WARNING: RAG query failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === 将配置数据转换为 JSON 供 AI 使用 ===
|
||||
configJSON, _ := json.MarshalIndent(nginxConfig, "", " ")
|
||||
analysisJSON, _ := json.MarshalIndent(analysis, "", " ")
|
||||
|
||||
// === 构建返回消息 ===
|
||||
userMessage := fmt.Sprintf(`📋 Nginx 配置解析完成
|
||||
|
||||
## 配置概览
|
||||
- Server 块: %d
|
||||
- Location 块: %d
|
||||
- 域名: %d 个
|
||||
- 复杂度: %s
|
||||
- 目标格式: %s
|
||||
- 命名空间: %s
|
||||
|
||||
## 检测到的特性
|
||||
%s
|
||||
|
||||
## 迁移建议
|
||||
%s
|
||||
%s
|
||||
|
||||
---
|
||||
|
||||
## Nginx 配置结构
|
||||
|
||||
`+"```json"+`
|
||||
%s
|
||||
`+"```"+`
|
||||
|
||||
## 分析结果
|
||||
|
||||
`+"```json"+`
|
||||
%s
|
||||
`+"```"+`
|
||||
%s
|
||||
`,
|
||||
analysis.ServerCount,
|
||||
analysis.LocationCount,
|
||||
analysis.DomainCount,
|
||||
analysis.Complexity,
|
||||
func() string {
|
||||
if useGatewayAPI {
|
||||
return "Gateway API (HTTPRoute)"
|
||||
}
|
||||
return "Kubernetes Ingress"
|
||||
}(),
|
||||
namespace,
|
||||
formatFeatures(analysis.Features),
|
||||
formatSuggestions(analysis.Suggestions),
|
||||
func() string {
|
||||
if ragContext != "" {
|
||||
return "\n\n已加载知识库参考文档"
|
||||
}
|
||||
return ""
|
||||
}(),
|
||||
string(configJSON),
|
||||
string(analysisJSON),
|
||||
ragContext,
|
||||
)
|
||||
|
||||
return tools.FormatToolResultWithAIContext(userMessage, "", map[string]interface{}{
|
||||
"nginx_config": nginxConfig,
|
||||
"analysis": analysis,
|
||||
"namespace": namespace,
|
||||
"use_gateway_api": useGatewayAPI,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MCPServer) AnalyzeLuaPlugin(args map[string]interface{}) tools.ToolResult {
|
||||
luaCode, ok := args["lua_code"].(string)
|
||||
if !ok {
|
||||
return tools.ToolResult{Content: []tools.Content{{Type: "text", Text: "Error: Missing lua_code"}}}
|
||||
}
|
||||
|
||||
// 使用新的 AI 友好分析
|
||||
analysis := tools.AnalyzeLuaPluginForAI(luaCode)
|
||||
|
||||
// === RAG 增强:查询知识库获取转换建议 ===
|
||||
var ragContext string
|
||||
if s.ragManager != nil && s.ragManager.IsEnabled() && len(analysis.APICalls) > 0 {
|
||||
query := fmt.Sprintf("Nginx Lua API %s 在 Higress WASM 中的转换方法和最佳实践", strings.Join(analysis.APICalls, ", "))
|
||||
log.Printf("🔍 RAG Query: %s", query)
|
||||
|
||||
ragResult, err := s.ragManager.QueryForTool("analyze_lua_plugin", query, "lua_migration")
|
||||
if err == nil && ragResult.Enabled && len(ragResult.Documents) > 0 {
|
||||
log.Printf("RAG: Found %d documents for Lua analysis", len(ragResult.Documents))
|
||||
ragContext = "\n\n## 知识库参考资料\n\n" + ragResult.FormatContextForAI()
|
||||
} else if err != nil {
|
||||
log.Printf(" RAG query failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 生成用户友好的消息
|
||||
features := []string{}
|
||||
for feature := range analysis.Features {
|
||||
features = append(features, fmt.Sprintf("- %s", feature))
|
||||
}
|
||||
|
||||
userMessage := fmt.Sprintf(`Lua 插件分析完成
|
||||
|
||||
## 检测到的特性
|
||||
%s
|
||||
|
||||
## 基本信息
|
||||
- **复杂度**: %s
|
||||
- **兼容性**: %s
|
||||
|
||||
## 兼容性警告
|
||||
%s
|
||||
%s
|
||||
|
||||
## 后续操作
|
||||
- 调用 generate_conversion_hints 获取转换提示
|
||||
- 或直接使用 convert_lua_to_wasm 一键转换
|
||||
|
||||
## 分析结果
|
||||
|
||||
`+"```json"+`
|
||||
%s
|
||||
`+"```"+`
|
||||
`,
|
||||
strings.Join(features, "\n"),
|
||||
analysis.Complexity,
|
||||
analysis.Compatibility,
|
||||
func() string {
|
||||
if len(analysis.Warnings) > 0 {
|
||||
return "- " + strings.Join(analysis.Warnings, "\n- ")
|
||||
}
|
||||
return "无"
|
||||
}(),
|
||||
ragContext,
|
||||
string(mustMarshalJSON(analysis)),
|
||||
)
|
||||
|
||||
return tools.FormatToolResultWithAIContext(userMessage, "", analysis)
|
||||
}
|
||||
|
||||
func mustMarshalJSON(v interface{}) []byte {
|
||||
data, _ := json.Marshal(v)
|
||||
return data
|
||||
}
|
||||
|
||||
func (s *MCPServer) ConvertLuaToWasm(args map[string]interface{}) tools.ToolResult {
|
||||
luaCode, ok := args["lua_code"].(string)
|
||||
if !ok {
|
||||
return tools.ToolResult{Content: []tools.Content{{Type: "text", Text: "Error: Missing lua_code"}}}
|
||||
}
|
||||
|
||||
pluginName, ok := args["plugin_name"].(string)
|
||||
if !ok {
|
||||
return tools.ToolResult{Content: []tools.Content{{Type: "text", Text: "Error: Missing plugin_name"}}}
|
||||
}
|
||||
|
||||
analyzer := tools.AnalyzeLuaScript(luaCode)
|
||||
result, err := tools.ConvertLuaToWasm(analyzer, pluginName)
|
||||
if err != nil {
|
||||
return tools.ToolResult{Content: []tools.Content{{Type: "text", Text: fmt.Sprintf("Error: %v", err)}}}
|
||||
}
|
||||
|
||||
response := fmt.Sprintf(`Lua脚本转换完成
|
||||
|
||||
转换分析:
|
||||
- 复杂度: %s
|
||||
- 检测特性: %d个
|
||||
- 兼容性警告: %d个
|
||||
|
||||
注意事项:
|
||||
%s
|
||||
|
||||
生成的文件:
|
||||
|
||||
==== main.go ====
|
||||
%s
|
||||
|
||||
==== WasmPlugin配置 ====
|
||||
%s
|
||||
|
||||
部署步骤:
|
||||
1. 创建插件目录: mkdir -p extensions/%s
|
||||
2. 保存Go代码到: extensions/%s/main.go
|
||||
3. 构建插件: PLUGIN_NAME=%s make build
|
||||
4. 应用配置: kubectl apply -f wasmplugin.yaml
|
||||
|
||||
提示:
|
||||
- 请根据实际需求调整配置
|
||||
- 测试插件功能后再部署到生产环境
|
||||
- 如有共享状态需求,请配置Redis等外部存储
|
||||
`,
|
||||
analyzer.Complexity,
|
||||
len(analyzer.Features),
|
||||
len(analyzer.Warnings),
|
||||
strings.Join(analyzer.Warnings, "\n- "),
|
||||
result.GoCode,
|
||||
result.WasmPluginYAML,
|
||||
pluginName, pluginName, pluginName)
|
||||
|
||||
return tools.ToolResult{Content: []tools.Content{{Type: "text", Text: response}}}
|
||||
}
|
||||
|
||||
// GenerateConversionHints 生成详细的代码转换提示
|
||||
func (s *MCPServer) GenerateConversionHints(args map[string]interface{}) tools.ToolResult {
|
||||
analysisResultStr, ok := args["analysis_result"].(string)
|
||||
if !ok {
|
||||
return tools.ToolResult{Content: []tools.Content{{Type: "text", Text: "Error: Missing analysis_result"}}}
|
||||
}
|
||||
|
||||
pluginName, ok := args["plugin_name"].(string)
|
||||
if !ok {
|
||||
return tools.ToolResult{Content: []tools.Content{{Type: "text", Text: "Error: Missing plugin_name"}}}
|
||||
}
|
||||
|
||||
// 解析分析结果
|
||||
var analysis tools.AnalysisResultForAI
|
||||
if err := json.Unmarshal([]byte(analysisResultStr), &analysis); err != nil {
|
||||
return tools.ToolResult{Content: []tools.Content{{Type: "text", Text: fmt.Sprintf("Error parsing analysis_result: %v", err)}}}
|
||||
}
|
||||
|
||||
// 生成转换提示
|
||||
hints := tools.GenerateConversionHints(analysis, pluginName)
|
||||
|
||||
// === RAG 增强:查询 Nginx API 转换文档 ===
|
||||
var ragDocs string
|
||||
|
||||
// 构建更精确的查询语句
|
||||
queryBuilder := []string{}
|
||||
if len(analysis.APICalls) > 0 {
|
||||
queryBuilder = append(queryBuilder, "Nginx Lua API 转换到 Higress WASM")
|
||||
|
||||
// 针对不同的 API 类型使用不同的查询关键词
|
||||
hasHeaderOps := analysis.Features["header_manipulation"] || analysis.Features["request_headers"] || analysis.Features["response_headers"]
|
||||
hasBodyOps := analysis.Features["request_body"] || analysis.Features["response_body"]
|
||||
hasResponseControl := analysis.Features["response_control"]
|
||||
|
||||
if hasHeaderOps {
|
||||
queryBuilder = append(queryBuilder, "请求头和响应头处理")
|
||||
}
|
||||
if hasBodyOps {
|
||||
queryBuilder = append(queryBuilder, "请求体和响应体处理")
|
||||
}
|
||||
if hasResponseControl {
|
||||
queryBuilder = append(queryBuilder, "响应控制和状态码设置")
|
||||
}
|
||||
|
||||
// 添加具体的 API 调用
|
||||
if len(analysis.APICalls) > 0 && len(analysis.APICalls) <= 5 {
|
||||
queryBuilder = append(queryBuilder, fmt.Sprintf("涉及 API: %s", strings.Join(analysis.APICalls, ", ")))
|
||||
}
|
||||
} else {
|
||||
queryBuilder = append(queryBuilder, "Higress WASM 插件开发 基础示例 Go SDK 使用")
|
||||
}
|
||||
|
||||
// 添加复杂度相关的查询
|
||||
if analysis.Complexity == "high" {
|
||||
queryBuilder = append(queryBuilder, "复杂插件实现 高级功能")
|
||||
}
|
||||
|
||||
queryString := strings.Join(queryBuilder, " ")
|
||||
|
||||
// 只有当 RAG 启用时才查询
|
||||
if s.ragManager != nil && s.ragManager.IsEnabled() {
|
||||
log.Printf(" RAG Query: %s", queryString)
|
||||
|
||||
ragContext, err := s.ragManager.QueryForTool(
|
||||
"generate_conversion_hints",
|
||||
queryString,
|
||||
"lua_migration",
|
||||
)
|
||||
|
||||
if err == nil && ragContext.Enabled && len(ragContext.Documents) > 0 {
|
||||
log.Printf("RAG: Found %d documents for conversion hints", len(ragContext.Documents))
|
||||
ragDocs = "\n\n## 参考文档(来自知识库)\n\n" + ragContext.FormatContextForAI()
|
||||
} else {
|
||||
if err != nil {
|
||||
log.Printf(" RAG query failed: %v", err)
|
||||
}
|
||||
ragDocs = ""
|
||||
}
|
||||
} else {
|
||||
ragDocs = ""
|
||||
}
|
||||
|
||||
// 格式化输出
|
||||
userMessage := fmt.Sprintf(` 代码转换提示
|
||||
|
||||
**插件名称**: %s
|
||||
**复杂度**: %s
|
||||
**兼容性**: %s
|
||||
%s
|
||||
|
||||
## 代码模板
|
||||
|
||||
%s
|
||||
%s
|
||||
`,
|
||||
pluginName,
|
||||
analysis.Complexity,
|
||||
analysis.Compatibility,
|
||||
func() string {
|
||||
if len(hints.Warnings) > 0 {
|
||||
return "\n**警告**: " + formatWarningsListForUser(hints.Warnings)
|
||||
}
|
||||
return ""
|
||||
}(),
|
||||
hints.CodeTemplate,
|
||||
ragDocs,
|
||||
)
|
||||
|
||||
return tools.FormatToolResultWithAIContext(userMessage, "", hints)
|
||||
}
|
||||
|
||||
// ValidateWasmCode 验证生成的 Go WASM 代码
|
||||
func (s *MCPServer) ValidateWasmCode(args map[string]interface{}) tools.ToolResult {
|
||||
goCode, ok := args["go_code"].(string)
|
||||
if !ok {
|
||||
return tools.ToolResult{Content: []tools.Content{{Type: "text", Text: "Error: Missing go_code"}}}
|
||||
}
|
||||
|
||||
pluginName, ok := args["plugin_name"].(string)
|
||||
if !ok {
|
||||
return tools.ToolResult{Content: []tools.Content{{Type: "text", Text: "Error: Missing plugin_name"}}}
|
||||
}
|
||||
|
||||
// 执行验证
|
||||
report := tools.ValidateWasmCode(goCode, pluginName)
|
||||
|
||||
// 统计各类问题数量
|
||||
requiredCount := 0
|
||||
recommendedCount := 0
|
||||
optionalCount := 0
|
||||
bestPracticeCount := 0
|
||||
|
||||
for _, issue := range report.Issues {
|
||||
switch issue.Category {
|
||||
case "required":
|
||||
requiredCount++
|
||||
case "recommended":
|
||||
recommendedCount++
|
||||
case "optional":
|
||||
optionalCount++
|
||||
case "best_practice":
|
||||
bestPracticeCount++
|
||||
}
|
||||
}
|
||||
|
||||
// 构建用户消息
|
||||
userMessage := fmt.Sprintf(`## 代码验证报告
|
||||
|
||||
%s
|
||||
|
||||
### 发现的回调函数 (%d 个)
|
||||
%s
|
||||
|
||||
### 配置结构
|
||||
%s
|
||||
|
||||
### 问题分类
|
||||
|
||||
#### 必须修复 (%d 个)
|
||||
%s
|
||||
|
||||
#### 建议修复 (%d 个)
|
||||
%s
|
||||
|
||||
#### 可选优化 (%d 个)
|
||||
%s
|
||||
|
||||
#### 最佳实践 (%d 个)
|
||||
%s
|
||||
|
||||
### 缺失的导入包 (%d 个)
|
||||
%s
|
||||
|
||||
---
|
||||
|
||||
`,
|
||||
report.Summary,
|
||||
len(report.FoundCallbacks),
|
||||
formatCallbacksList(report.FoundCallbacks),
|
||||
formatConfigStatus(report.HasConfig),
|
||||
requiredCount,
|
||||
formatIssuesByCategory(report.Issues, "required"),
|
||||
recommendedCount,
|
||||
formatIssuesByCategory(report.Issues, "recommended"),
|
||||
optionalCount,
|
||||
formatIssuesByCategory(report.Issues, "optional"),
|
||||
bestPracticeCount,
|
||||
formatIssuesByCategory(report.Issues, "best_practice"),
|
||||
len(report.MissingImports),
|
||||
formatList(report.MissingImports),
|
||||
)
|
||||
|
||||
// === RAG 增强:查询最佳实践和代码规范 ===
|
||||
var ragBestPractices string
|
||||
|
||||
// 根据验证结果构建更针对性的查询
|
||||
queryBuilder := []string{"Higress WASM 插件"}
|
||||
|
||||
// 根据发现的问题类型添加关键词
|
||||
if requiredCount > 0 || recommendedCount > 0 {
|
||||
queryBuilder = append(queryBuilder, "常见错误")
|
||||
|
||||
// 检查具体问题类型
|
||||
for _, issue := range report.Issues {
|
||||
switch issue.Type {
|
||||
case "error_handling":
|
||||
queryBuilder = append(queryBuilder, "错误处理")
|
||||
case "api_usage":
|
||||
queryBuilder = append(queryBuilder, "API 使用规范")
|
||||
case "config":
|
||||
queryBuilder = append(queryBuilder, "配置解析")
|
||||
case "logging":
|
||||
queryBuilder = append(queryBuilder, "日志记录")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 代码已通过基础验证,查询优化建议
|
||||
queryBuilder = append(queryBuilder, "性能优化 最佳实践")
|
||||
}
|
||||
|
||||
// 根据回调函数类型添加特定查询
|
||||
for _, callback := range report.FoundCallbacks {
|
||||
if strings.Contains(callback, "RequestHeaders") {
|
||||
queryBuilder = append(queryBuilder, "请求头处理")
|
||||
}
|
||||
if strings.Contains(callback, "RequestBody") {
|
||||
queryBuilder = append(queryBuilder, "请求体处理")
|
||||
}
|
||||
if strings.Contains(callback, "ResponseHeaders") {
|
||||
queryBuilder = append(queryBuilder, "响应头处理")
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有缺失的导入,查询包管理相关信息
|
||||
if len(report.MissingImports) > 0 {
|
||||
queryBuilder = append(queryBuilder, "依赖包导入")
|
||||
}
|
||||
|
||||
queryString := strings.Join(queryBuilder, " ")
|
||||
|
||||
// 只有当 RAG 启用时才查询
|
||||
if s.ragManager != nil && s.ragManager.IsEnabled() {
|
||||
log.Printf("RAG Query: %s", queryString)
|
||||
|
||||
ragContext, err := s.ragManager.QueryForTool(
|
||||
"validate_wasm_code",
|
||||
queryString,
|
||||
"best_practice",
|
||||
)
|
||||
|
||||
if err == nil && ragContext.Enabled && len(ragContext.Documents) > 0 {
|
||||
log.Printf("RAG: Found %d best practice documents", len(ragContext.Documents))
|
||||
ragBestPractices = "\n\n### 最佳实践建议(来自知识库)\n\n" + ragContext.FormatContextForAI()
|
||||
userMessage += ragBestPractices
|
||||
} else {
|
||||
if err != nil {
|
||||
log.Printf(" RAG query failed for validation: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 根据问题级别给出建议
|
||||
hasRequired := requiredCount > 0
|
||||
if hasRequired {
|
||||
userMessage += "\n **请优先修复 \"必须修复\" 的问题**\n\n"
|
||||
} else if recommendedCount > 0 {
|
||||
userMessage += "\n **代码基本结构正确**,建议修复 \"建议修复\" 的问题\n\n"
|
||||
} else {
|
||||
userMessage += "\n **代码验证通过!** 可以调用 `generate_deployment_config` 生成部署配置\n\n"
|
||||
}
|
||||
|
||||
return tools.FormatToolResultWithAIContext(userMessage, "", report)
|
||||
}
|
||||
|
||||
// GenerateDeploymentConfig 生成部署配置
|
||||
func (s *MCPServer) GenerateDeploymentConfig(args map[string]interface{}) tools.ToolResult {
|
||||
pluginName, ok := args["plugin_name"].(string)
|
||||
if !ok {
|
||||
return tools.ToolResult{Content: []tools.Content{{Type: "text", Text: "Error: Missing plugin_name"}}}
|
||||
}
|
||||
|
||||
goCode, ok := args["go_code"].(string)
|
||||
if !ok {
|
||||
return tools.ToolResult{Content: []tools.Content{{Type: "text", Text: "Error: Missing go_code"}}}
|
||||
}
|
||||
|
||||
namespace := "higress-system"
|
||||
if ns, ok := args["namespace"].(string); ok && ns != "" {
|
||||
namespace = ns
|
||||
}
|
||||
|
||||
configSchema := ""
|
||||
if cs, ok := args["config_schema"].(string); ok {
|
||||
configSchema = cs
|
||||
}
|
||||
|
||||
// 生成部署包
|
||||
pkg := tools.GenerateDeploymentPackage(pluginName, goCode, configSchema, namespace)
|
||||
|
||||
// 格式化输出
|
||||
userMessage := fmt.Sprintf(`🎉 部署配置生成完成!
|
||||
|
||||
插件 **%s** 的部署配置已生成(命名空间: %s)
|
||||
|
||||
## 生成的文件
|
||||
|
||||
1. **wasmplugin.yaml** - WasmPlugin 配置
|
||||
2. **Makefile** - 构建和部署脚本
|
||||
3. **Dockerfile** - 容器化打包
|
||||
4. **README.md** - 使用文档
|
||||
5. **test.sh** - 测试脚本
|
||||
|
||||
## 快速部署
|
||||
|
||||
`+"```bash"+`
|
||||
# 构建插件
|
||||
make build
|
||||
|
||||
# 构建并推送镜像
|
||||
make docker-build docker-push
|
||||
|
||||
# 部署
|
||||
make deploy
|
||||
|
||||
# 验证
|
||||
kubectl get wasmplugin -n %s
|
||||
`+"```"+`
|
||||
|
||||
## 配置文件
|
||||
|
||||
### wasmplugin.yaml
|
||||
`+"```yaml"+`
|
||||
%s
|
||||
`+"```"+`
|
||||
|
||||
### Makefile
|
||||
`+"```makefile"+`
|
||||
%s
|
||||
`+"```"+`
|
||||
|
||||
### Dockerfile
|
||||
`+"```dockerfile"+`
|
||||
%s
|
||||
`+"```"+`
|
||||
|
||||
### README.md
|
||||
`+"```markdown"+`
|
||||
%s
|
||||
`+"```"+`
|
||||
|
||||
### test.sh
|
||||
`+"```bash"+`
|
||||
%s
|
||||
`+"```"+`
|
||||
`,
|
||||
pluginName,
|
||||
namespace,
|
||||
namespace,
|
||||
pkg.WasmPluginYAML,
|
||||
pkg.Makefile,
|
||||
pkg.Dockerfile,
|
||||
pkg.README,
|
||||
pkg.TestScript,
|
||||
)
|
||||
|
||||
return tools.FormatToolResultWithAIContext(userMessage, "", pkg)
|
||||
}
|
||||
|
||||
// 辅助格式化函数
|
||||
|
||||
func formatWarningsListForUser(warnings []string) string {
|
||||
if len(warnings) == 0 {
|
||||
return "无"
|
||||
}
|
||||
return strings.Join(warnings, "\n- ")
|
||||
}
|
||||
|
||||
func formatCallbacksList(callbacks []string) string {
|
||||
if len(callbacks) == 0 {
|
||||
return "无"
|
||||
}
|
||||
return "- " + strings.Join(callbacks, "\n- ")
|
||||
}
|
||||
|
||||
func formatConfigStatus(hasConfig bool) string {
|
||||
if hasConfig {
|
||||
return " 已定义配置结构体"
|
||||
}
|
||||
return "- 未定义配置结构体(如不需要配置可忽略)"
|
||||
}
|
||||
|
||||
func formatIssuesByCategory(issues []tools.ValidationIssue, category string) string {
|
||||
var filtered []string
|
||||
for _, issue := range issues {
|
||||
if issue.Category == category {
|
||||
filtered = append(filtered, fmt.Sprintf("- **[%s]** %s\n 💡 建议: %s\n 📌 影响: %s",
|
||||
issue.Type, issue.Message, issue.Suggestion, issue.Impact))
|
||||
}
|
||||
}
|
||||
if len(filtered) == 0 {
|
||||
return "无"
|
||||
}
|
||||
return strings.Join(filtered, "\n\n")
|
||||
}
|
||||
|
||||
func formatList(items []string) string {
|
||||
if len(items) == 0 {
|
||||
return "无"
|
||||
}
|
||||
return "- " + strings.Join(items, "\n- ")
|
||||
}
|
||||
|
||||
// formatFeatures 格式化特性列表
|
||||
func formatFeatures(features map[string]bool) string {
|
||||
featureNames := map[string]string{
|
||||
"ssl": "SSL/TLS 加密",
|
||||
"proxy": "反向代理",
|
||||
"rewrite": "URL 重写",
|
||||
"redirect": "重定向",
|
||||
"return": "返回指令",
|
||||
"complex_routing": "复杂路由匹配",
|
||||
"header_manipulation": "请求头操作",
|
||||
"response_headers": "响应头操作",
|
||||
}
|
||||
|
||||
var result []string
|
||||
for key, enabled := range features {
|
||||
if enabled {
|
||||
if name, ok := featureNames[key]; ok {
|
||||
result = append(result, fmt.Sprintf("- %s", name))
|
||||
} else {
|
||||
result = append(result, fmt.Sprintf("- %s", key))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(result) == 0 {
|
||||
return "- 基础配置(无特殊特性)"
|
||||
}
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
|
||||
// formatSuggestions 格式化建议列表
|
||||
func formatSuggestions(suggestions []string) string {
|
||||
if len(suggestions) == 0 {
|
||||
return "- 无特殊建议"
|
||||
}
|
||||
var result []string
|
||||
for _, s := range suggestions {
|
||||
result = append(result, fmt.Sprintf("- 💡 %s", s))
|
||||
}
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Common types for nginx migration MCP server - Standalone Mode
|
||||
package standalone
|
||||
|
||||
import (
|
||||
"nginx-migration-mcp/internal/rag"
|
||||
)
|
||||
|
||||
// MCPMessage represents a Model Context Protocol message structure
|
||||
type MCPMessage struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
Method string `json:"method,omitempty"`
|
||||
Params interface{} `json:"params,omitempty"`
|
||||
ID interface{} `json:"id,omitempty"`
|
||||
Result interface{} `json:"result,omitempty"`
|
||||
Error *MCPError `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type MCPError struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type CallToolParams struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]interface{} `json:"arguments,omitempty"`
|
||||
}
|
||||
|
||||
type MCPServer struct {
|
||||
config *ServerConfig
|
||||
ragManager *rag.RAGManager
|
||||
}
|
||||
|
||||
// MCPServer implements the tools.MCPServer interface
|
||||
// Method implementations are in server.go
|
||||
@@ -0,0 +1,405 @@
|
||||
// Lua to WASM conversion logic for Nginx migration
|
||||
package tools
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// LuaAnalyzer analyzes Lua script features and generates conversion mappings
|
||||
type LuaAnalyzer struct {
|
||||
Features map[string]bool
|
||||
Variables map[string]string
|
||||
Functions []LuaFunction
|
||||
Warnings []string
|
||||
Complexity string
|
||||
}
|
||||
|
||||
type LuaFunction struct {
|
||||
Name string
|
||||
Body string
|
||||
Phase string // request_headers, request_body, response_headers, etc.
|
||||
}
|
||||
|
||||
// ConversionResult holds the generated WASM plugin code
|
||||
type ConversionResult struct {
|
||||
PluginName string
|
||||
GoCode string
|
||||
ConfigSchema string
|
||||
Dependencies []string
|
||||
WasmPluginYAML string
|
||||
}
|
||||
|
||||
// AnalyzeLuaScript performs detailed analysis of Lua script
|
||||
func AnalyzeLuaScript(luaCode string) *LuaAnalyzer {
|
||||
analyzer := &LuaAnalyzer{
|
||||
Features: make(map[string]bool),
|
||||
Variables: make(map[string]string),
|
||||
Functions: []LuaFunction{},
|
||||
Warnings: []string{},
|
||||
Complexity: "simple",
|
||||
}
|
||||
|
||||
lines := strings.Split(luaCode, "\n")
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "--") {
|
||||
continue
|
||||
}
|
||||
|
||||
// 分析ngx变量使用
|
||||
analyzer.analyzeNginxVars(line)
|
||||
|
||||
// 分析API调用
|
||||
analyzer.analyzeAPICalls(line)
|
||||
|
||||
// 分析函数定义
|
||||
analyzer.analyzeFunctions(line, luaCode)
|
||||
}
|
||||
|
||||
// 根据特性确定复杂度
|
||||
analyzer.determineComplexity()
|
||||
|
||||
return analyzer
|
||||
}
|
||||
|
||||
func (la *LuaAnalyzer) analyzeNginxVars(line string) {
|
||||
// 匹配 ngx.var.xxx 模式
|
||||
varPattern := regexp.MustCompile(`ngx\.var\.(\w+)`)
|
||||
matches := varPattern.FindAllStringSubmatch(line, -1)
|
||||
|
||||
for _, match := range matches {
|
||||
if len(match) > 1 {
|
||||
varName := match[1]
|
||||
la.Features["ngx.var"] = true
|
||||
|
||||
// 映射常见变量到WASM等价物
|
||||
switch varName {
|
||||
case "uri":
|
||||
la.Variables[varName] = "proxywasm.GetHttpRequestHeader(\":path\")"
|
||||
case "request_method":
|
||||
la.Variables[varName] = "proxywasm.GetHttpRequestHeader(\":method\")"
|
||||
case "host":
|
||||
la.Variables[varName] = "proxywasm.GetHttpRequestHeader(\":authority\")"
|
||||
case "remote_addr":
|
||||
la.Variables[varName] = "proxywasm.GetHttpRequestHeader(\"x-forwarded-for\")"
|
||||
case "request_uri":
|
||||
la.Variables[varName] = "proxywasm.GetHttpRequestHeader(\":path\")"
|
||||
case "scheme":
|
||||
la.Variables[varName] = "proxywasm.GetHttpRequestHeader(\":scheme\")"
|
||||
default:
|
||||
la.Variables[varName] = fmt.Sprintf("proxywasm.GetHttpRequestHeader(\"%s\")", varName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (la *LuaAnalyzer) analyzeAPICalls(line string) {
|
||||
apiCalls := map[string]string{
|
||||
"ngx.req.get_headers": "request_headers",
|
||||
"ngx.req.get_body_data": "request_body",
|
||||
"ngx.req.read_body": "request_body",
|
||||
"ngx.exit": "response_control",
|
||||
"ngx.say": "response_control",
|
||||
"ngx.print": "response_control",
|
||||
"ngx.shared": "shared_dict",
|
||||
"ngx.location.capture": "internal_request",
|
||||
"ngx.req.set_header": "header_manipulation",
|
||||
"ngx.header": "response_headers",
|
||||
}
|
||||
|
||||
for apiCall, feature := range apiCalls {
|
||||
if strings.Contains(line, apiCall) {
|
||||
la.Features[feature] = true
|
||||
|
||||
// 添加特定警告
|
||||
switch feature {
|
||||
case "shared_dict":
|
||||
la.Warnings = append(la.Warnings, "共享字典需要使用Redis或其他外部缓存替代")
|
||||
case "internal_request":
|
||||
la.Warnings = append(la.Warnings, "内部请求需要改为HTTP客户端调用")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (la *LuaAnalyzer) analyzeFunctions(line string, fullCode string) {
|
||||
// 检测函数定义
|
||||
funcPattern := regexp.MustCompile(`function\s+(\w+)\s*\(`)
|
||||
matches := funcPattern.FindAllStringSubmatch(line, -1)
|
||||
|
||||
for _, match := range matches {
|
||||
if len(match) > 1 {
|
||||
funcName := match[1]
|
||||
|
||||
// 提取函数体 (简化实现)
|
||||
funcBody := la.extractFunctionBody(fullCode, funcName)
|
||||
|
||||
// 根据函数名推断执行阶段
|
||||
phase := "request_headers"
|
||||
if strings.Contains(funcName, "body") {
|
||||
phase = "request_body"
|
||||
} else if strings.Contains(funcName, "response") {
|
||||
phase = "response_headers"
|
||||
}
|
||||
|
||||
la.Functions = append(la.Functions, LuaFunction{
|
||||
Name: funcName,
|
||||
Body: funcBody,
|
||||
Phase: phase,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (la *LuaAnalyzer) extractFunctionBody(fullCode, funcName string) string {
|
||||
// 简化的函数体提取 - 实际实现应该更复杂
|
||||
pattern := fmt.Sprintf(`function\s+%s\s*\([^)]*\)(.*?)end`, funcName)
|
||||
re := regexp.MustCompile(pattern)
|
||||
match := re.FindStringSubmatch(fullCode)
|
||||
if len(match) > 1 {
|
||||
return strings.TrimSpace(match[1])
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (la *LuaAnalyzer) determineComplexity() {
|
||||
warningCount := len(la.Warnings)
|
||||
featureCount := len(la.Features)
|
||||
|
||||
if warningCount > 3 || featureCount > 6 {
|
||||
la.Complexity = "complex"
|
||||
} else if warningCount > 1 || featureCount > 3 {
|
||||
la.Complexity = "medium"
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertLuaToWasm converts analyzed Lua script to WASM plugin
|
||||
func ConvertLuaToWasm(analyzer *LuaAnalyzer, pluginName string) (*ConversionResult, error) {
|
||||
result := &ConversionResult{
|
||||
PluginName: pluginName,
|
||||
Dependencies: []string{
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm",
|
||||
"github.com/higress-group/wasm-go/pkg/wrapper",
|
||||
"github.com/higress-group/wasm-go/pkg/log",
|
||||
},
|
||||
}
|
||||
|
||||
// 生成Go代码
|
||||
goCode, err := generateGoCode(analyzer, pluginName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.GoCode = goCode
|
||||
|
||||
// 生成配置模式
|
||||
result.ConfigSchema = generateConfigSchema(analyzer)
|
||||
|
||||
// 生成WasmPlugin YAML
|
||||
result.WasmPluginYAML = generateWasmPluginYAML(pluginName)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func generateGoCode(analyzer *LuaAnalyzer, pluginName string) (string, error) {
|
||||
tmpl := `// Generated WASM plugin from Lua script
|
||||
// Plugin: {{.PluginName}}
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
func main() {}
|
||||
|
||||
func init() {
|
||||
wrapper.SetCtx(
|
||||
"{{.PluginName}}",
|
||||
wrapper.ParseConfigBy(parseConfig),
|
||||
{{- if .HasRequestHeaders}}
|
||||
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
|
||||
{{- end}}
|
||||
{{- if .HasRequestBody}}
|
||||
wrapper.ProcessRequestBodyBy(onHttpRequestBody),
|
||||
{{- end}}
|
||||
{{- if .HasResponseHeaders}}
|
||||
wrapper.ProcessResponseHeadersBy(onHttpResponseHeaders),
|
||||
{{- end}}
|
||||
)
|
||||
}
|
||||
|
||||
type {{.ConfigTypeName}} struct {
|
||||
// Generated from Lua analysis
|
||||
{{- range .ConfigFields}}
|
||||
{{.Name}} {{.Type}} ` + "`json:\"{{.JSONName}}\"`" + `
|
||||
{{- end}}
|
||||
}
|
||||
|
||||
func parseConfig(json gjson.Result, config *{{.ConfigTypeName}}, log log.Log) error {
|
||||
{{- range .ConfigFields}}
|
||||
config.{{.Name}} = json.Get("{{.JSONName}}").{{.ParseMethod}}()
|
||||
{{- end}}
|
||||
return nil
|
||||
}
|
||||
|
||||
{{- if .HasRequestHeaders}}
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config {{.ConfigTypeName}}, log log.Log) types.Action {
|
||||
{{.RequestHeadersLogic}}
|
||||
return types.ActionContinue
|
||||
}
|
||||
{{- end}}
|
||||
|
||||
{{- if .HasRequestBody}}
|
||||
func onHttpRequestBody(ctx wrapper.HttpContext, config {{.ConfigTypeName}}, body []byte, log log.Log) types.Action {
|
||||
{{.RequestBodyLogic}}
|
||||
return types.ActionContinue
|
||||
}
|
||||
{{- end}}
|
||||
|
||||
{{- if .HasResponseHeaders}}
|
||||
func onHttpResponseHeaders(ctx wrapper.HttpContext, config {{.ConfigTypeName}}, log log.Log) types.Action {
|
||||
{{.ResponseHeadersLogic}}
|
||||
return types.ActionContinue
|
||||
}
|
||||
{{- end}}
|
||||
`
|
||||
|
||||
// 准备模板数据
|
||||
data := map[string]interface{}{
|
||||
"PluginName": pluginName,
|
||||
"ConfigTypeName": strings.Title(pluginName) + "Config",
|
||||
"HasRequestHeaders": analyzer.Features["request_headers"] || analyzer.Features["ngx.var"],
|
||||
"HasRequestBody": analyzer.Features["request_body"],
|
||||
"HasResponseHeaders": analyzer.Features["response_headers"] || analyzer.Features["response_control"],
|
||||
"ConfigFields": generateConfigFields(analyzer),
|
||||
"RequestHeadersLogic": generateRequestHeadersLogic(analyzer),
|
||||
"RequestBodyLogic": generateRequestBodyLogic(analyzer),
|
||||
"ResponseHeadersLogic": generateResponseHeadersLogic(analyzer),
|
||||
}
|
||||
|
||||
t, err := template.New("wasm").Parse(tmpl)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var buf strings.Builder
|
||||
err = t.Execute(&buf, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func generateConfigFields(analyzer *LuaAnalyzer) []map[string]string {
|
||||
fields := []map[string]string{}
|
||||
|
||||
// 基于分析的特性生成配置字段
|
||||
if analyzer.Features["response_control"] {
|
||||
fields = append(fields, map[string]string{
|
||||
"Name": "EnableCustomResponse",
|
||||
"Type": "bool",
|
||||
"JSONName": "enable_custom_response",
|
||||
"ParseMethod": "Bool",
|
||||
})
|
||||
}
|
||||
|
||||
return fields
|
||||
}
|
||||
|
||||
func generateRequestHeadersLogic(analyzer *LuaAnalyzer) string {
|
||||
logic := []string{}
|
||||
|
||||
// 基于变量使用生成逻辑
|
||||
for varName, wasmCall := range analyzer.Variables {
|
||||
logic = append(logic, fmt.Sprintf(`
|
||||
// Access to ngx.var.%s
|
||||
%s, err := %s
|
||||
if err != nil {
|
||||
log.Warnf("Failed to get %s: %%v", err)
|
||||
}`, varName, varName, wasmCall, varName))
|
||||
}
|
||||
|
||||
if analyzer.Features["header_manipulation"] {
|
||||
logic = append(logic, `
|
||||
// Header manipulation logic
|
||||
err := proxywasm.AddHttpRequestHeader("x-converted-from", "nginx-lua")
|
||||
if err != nil {
|
||||
log.Warnf("Failed to add header: %v", err)
|
||||
}`)
|
||||
}
|
||||
|
||||
return strings.Join(logic, "\n")
|
||||
}
|
||||
|
||||
func generateRequestBodyLogic(analyzer *LuaAnalyzer) string {
|
||||
if analyzer.Features["request_body"] {
|
||||
return `
|
||||
// Process request body
|
||||
bodyStr := string(body)
|
||||
log.Infof("Processing request body: %s", bodyStr)
|
||||
|
||||
// Add your body processing logic here
|
||||
`
|
||||
}
|
||||
return "// No request body processing needed"
|
||||
}
|
||||
|
||||
func generateResponseHeadersLogic(analyzer *LuaAnalyzer) string {
|
||||
if analyzer.Features["response_control"] {
|
||||
return `
|
||||
// Response control logic
|
||||
if config.EnableCustomResponse {
|
||||
proxywasm.SendHttpResponseWithDetail(200, "lua-converted", nil, []byte("Response from converted Lua plugin"), -1)
|
||||
return types.ActionContinue
|
||||
}
|
||||
`
|
||||
}
|
||||
return "// No response processing needed"
|
||||
}
|
||||
|
||||
func generateConfigSchema(analyzer *LuaAnalyzer) string {
|
||||
schema := `{
|
||||
"type": "object",
|
||||
"properties": {`
|
||||
|
||||
properties := []string{}
|
||||
|
||||
if analyzer.Features["response_control"] {
|
||||
properties = append(properties, `
|
||||
"enable_custom_response": {
|
||||
"type": "boolean",
|
||||
"description": "Enable custom response handling"
|
||||
}`)
|
||||
}
|
||||
|
||||
schema += strings.Join(properties, ",")
|
||||
schema += `
|
||||
}
|
||||
}`
|
||||
|
||||
return schema
|
||||
}
|
||||
|
||||
func generateWasmPluginYAML(pluginName string) string {
|
||||
return fmt.Sprintf(`apiVersion: extensions.higress.io/v1alpha1
|
||||
kind: WasmPlugin
|
||||
metadata:
|
||||
name: %s
|
||||
namespace: higress-system
|
||||
spec:
|
||||
defaultConfig:
|
||||
enable_custom_response: true
|
||||
url: oci://your-registry/%s:latest
|
||||
`, pluginName, pluginName)
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
// MCP Tools Definitions
|
||||
// 定义所有可用的MCP工具及其描述信息
|
||||
package tools
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
)
|
||||
|
||||
// MCPTool represents a tool definition in MCP protocol
|
||||
type MCPTool struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
InputSchema json.RawMessage `json:"inputSchema"`
|
||||
}
|
||||
|
||||
// ToolResult represents the result of a tool call
|
||||
type ToolResult struct {
|
||||
Content []Content `json:"content"`
|
||||
}
|
||||
|
||||
// Content represents content within a tool result
|
||||
type Content struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
// MCPServer is an interface for server methods needed by tool handlers
|
||||
type MCPServer interface {
|
||||
ParseNginxConfig(args map[string]interface{}) ToolResult
|
||||
ConvertToHigress(args map[string]interface{}) ToolResult
|
||||
AnalyzeLuaPlugin(args map[string]interface{}) ToolResult
|
||||
ConvertLuaToWasm(args map[string]interface{}) ToolResult
|
||||
// 新增工具链方法
|
||||
GenerateConversionHints(args map[string]interface{}) ToolResult
|
||||
ValidateWasmCode(args map[string]interface{}) ToolResult
|
||||
GenerateDeploymentConfig(args map[string]interface{}) ToolResult
|
||||
}
|
||||
|
||||
// MCPToolsConfig 工具配置文件结构
|
||||
type MCPToolsConfig struct {
|
||||
Version string `json:"version"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Tools []MCPTool `json:"tools"`
|
||||
}
|
||||
|
||||
// LoadToolsFromFile 从 JSON 文件加载工具定义
|
||||
func LoadToolsFromFile(filename string) ([]MCPTool, error) {
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
// 文件不存在时使用默认配置
|
||||
return GetMCPToolsDefault(), nil
|
||||
}
|
||||
|
||||
var config MCPToolsConfig
|
||||
if err := json.Unmarshal(data, &config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return config.Tools, nil
|
||||
}
|
||||
|
||||
// GetMCPTools 返回所有可用的 MCP 工具定义
|
||||
// 优先从 mcp-tools.json 加载,失败时使用默认定义
|
||||
func GetMCPTools() []MCPTool {
|
||||
tools, err := LoadToolsFromFile("mcp-tools.json")
|
||||
if err != nil {
|
||||
return GetMCPToolsDefault()
|
||||
}
|
||||
return tools
|
||||
}
|
||||
|
||||
// GetMCPToolsDefault 返回默认的工具定义(包含完整工具链)
|
||||
func GetMCPToolsDefault() []MCPTool {
|
||||
return []MCPTool{
|
||||
{
|
||||
Name: "parse_nginx_config",
|
||||
Description: "解析和分析 Nginx 配置文件,识别配置结构和复杂度",
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"config_content": {
|
||||
"type": "string",
|
||||
"description": "Nginx 配置文件内容"
|
||||
}
|
||||
},
|
||||
"required": ["config_content"]
|
||||
}`),
|
||||
},
|
||||
{
|
||||
Name: "convert_to_higress",
|
||||
Description: "将 Nginx 配置转换为 Higress HTTPRoute 和 Service 资源",
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"config_content": {
|
||||
"type": "string",
|
||||
"description": "Nginx 配置文件内容"
|
||||
},
|
||||
"namespace": {
|
||||
"type": "string",
|
||||
"description": "目标 Kubernetes 命名空间",
|
||||
"default": "default"
|
||||
}
|
||||
},
|
||||
"required": ["config_content"]
|
||||
}`),
|
||||
},
|
||||
{
|
||||
Name: "analyze_lua_plugin",
|
||||
Description: "分析 Nginx Lua 插件的兼容性,识别使用的 API 和潜在迁移问题,返回结构化分析结果",
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"lua_code": {
|
||||
"type": "string",
|
||||
"description": "Nginx Lua 插件代码"
|
||||
}
|
||||
},
|
||||
"required": ["lua_code"]
|
||||
}`),
|
||||
},
|
||||
{
|
||||
Name: "generate_conversion_hints",
|
||||
Description: "基于 Lua 分析结果生成代码转换模板",
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"analysis_result": {
|
||||
"type": "string",
|
||||
"description": "analyze_lua_plugin 返回的 JSON 格式分析结果"
|
||||
},
|
||||
"plugin_name": {
|
||||
"type": "string",
|
||||
"description": "目标插件名称(小写字母和连字符)"
|
||||
}
|
||||
},
|
||||
"required": ["analysis_result", "plugin_name"]
|
||||
}`),
|
||||
},
|
||||
{
|
||||
Name: "validate_wasm_code",
|
||||
Description: "验证生成的 Go WASM 插件代码,检查语法、API 使用、配置结构等,输出验证报告和改进建议",
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"go_code": {
|
||||
"type": "string",
|
||||
"description": "生成的 Go WASM 插件代码"
|
||||
},
|
||||
"plugin_name": {
|
||||
"type": "string",
|
||||
"description": "插件名称"
|
||||
}
|
||||
},
|
||||
"required": ["go_code", "plugin_name"]
|
||||
}`),
|
||||
},
|
||||
{
|
||||
Name: "generate_deployment_config",
|
||||
Description: "为验证通过的 WASM 插件生成完整的部署配置包,包括 WasmPlugin YAML、Makefile、Dockerfile、README 和测试脚本",
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"plugin_name": {
|
||||
"type": "string",
|
||||
"description": "插件名称"
|
||||
},
|
||||
"go_code": {
|
||||
"type": "string",
|
||||
"description": "验证通过的 Go 代码"
|
||||
},
|
||||
"config_schema": {
|
||||
"type": "string",
|
||||
"description": "配置 JSON Schema(可选)"
|
||||
},
|
||||
"namespace": {
|
||||
"type": "string",
|
||||
"description": "部署命名空间",
|
||||
"default": "higress-system"
|
||||
}
|
||||
},
|
||||
"required": ["plugin_name", "go_code"]
|
||||
}`),
|
||||
},
|
||||
{
|
||||
Name: "convert_lua_to_wasm",
|
||||
Description: "一键将 Nginx Lua 脚本转换为 Higress WASM 插件,自动生成 Go 代码和 WasmPlugin 配置。适合简单插件快速转换",
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"lua_code": {
|
||||
"type": "string",
|
||||
"description": "要转换的 Nginx Lua 插件代码"
|
||||
},
|
||||
"plugin_name": {
|
||||
"type": "string",
|
||||
"description": "生成的 WASM 插件名称 (小写字母和连字符)"
|
||||
}
|
||||
},
|
||||
"required": ["lua_code", "plugin_name"]
|
||||
}`),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ToolHandler 定义工具处理函数的类型
|
||||
type ToolHandler func(args map[string]interface{}) ToolResult
|
||||
|
||||
// GetToolHandlers 返回工具名称到处理函数的映射
|
||||
func GetToolHandlers(s MCPServer) map[string]ToolHandler {
|
||||
return map[string]ToolHandler{
|
||||
"parse_nginx_config": s.ParseNginxConfig,
|
||||
"convert_to_higress": s.ConvertToHigress,
|
||||
"analyze_lua_plugin": s.AnalyzeLuaPlugin,
|
||||
"convert_lua_to_wasm": s.ConvertLuaToWasm,
|
||||
// 新增工具链处理器
|
||||
"generate_conversion_hints": s.GenerateConversionHints,
|
||||
"validate_wasm_code": s.ValidateWasmCode,
|
||||
"generate_deployment_config": s.GenerateDeploymentConfig,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,426 @@
|
||||
// Package tools provides Nginx configuration parsing and analysis capabilities.
|
||||
// This intelligent parser extracts semantic information from Nginx configs for AI reasoning.
|
||||
package tools
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// NginxConfig 表示解析后的 Nginx 配置结构
|
||||
type NginxConfig struct {
|
||||
Servers []NginxServer `json:"servers"`
|
||||
Upstreams []NginxUpstream `json:"upstreams"`
|
||||
Raw string `json:"raw"`
|
||||
}
|
||||
|
||||
// NginxServer 表示一个 server 块
|
||||
type NginxServer struct {
|
||||
Listen []string `json:"listen"` // 监听端口和地址
|
||||
ServerNames []string `json:"server_names"` // 域名列表
|
||||
Locations []NginxLocation `json:"locations"` // location 块列表
|
||||
SSL *NginxSSL `json:"ssl,omitempty"` // SSL 配置
|
||||
Directives map[string][]string `json:"directives"` // 其他指令
|
||||
}
|
||||
|
||||
// NginxLocation 表示一个 location 块
|
||||
type NginxLocation struct {
|
||||
Path string `json:"path"` // 路径
|
||||
Modifier string `json:"modifier"` // 修饰符(=, ~, ~*, ^~)
|
||||
ProxyPass string `json:"proxy_pass,omitempty"` // 代理目标
|
||||
Rewrite []string `json:"rewrite,omitempty"` // rewrite 规则
|
||||
Return *NginxReturn `json:"return,omitempty"` // return 指令
|
||||
Directives map[string][]string `json:"directives"` // 其他指令
|
||||
}
|
||||
|
||||
// NginxSSL 表示 SSL 配置
|
||||
type NginxSSL struct {
|
||||
Certificate string `json:"certificate,omitempty"`
|
||||
CertificateKey string `json:"certificate_key,omitempty"`
|
||||
Protocols []string `json:"protocols,omitempty"`
|
||||
Ciphers string `json:"ciphers,omitempty"`
|
||||
}
|
||||
|
||||
// NginxReturn 表示 return 指令
|
||||
type NginxReturn struct {
|
||||
Code int `json:"code"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
}
|
||||
|
||||
// NginxUpstream 表示 upstream 块
|
||||
type NginxUpstream struct {
|
||||
Name string `json:"name"`
|
||||
Servers []string `json:"servers"`
|
||||
Method string `json:"method,omitempty"` // 负载均衡方法
|
||||
}
|
||||
|
||||
// ParseNginxConfig 解析 Nginx 配置内容
|
||||
func ParseNginxConfig(content string) (*NginxConfig, error) {
|
||||
config := &NginxConfig{
|
||||
Raw: content,
|
||||
Servers: []NginxServer{},
|
||||
Upstreams: []NginxUpstream{},
|
||||
}
|
||||
|
||||
// 解析 upstream 块
|
||||
upstreams := extractUpstreams(content)
|
||||
config.Upstreams = upstreams
|
||||
|
||||
// 解析 server 块
|
||||
servers := extractServers(content)
|
||||
for _, serverContent := range servers {
|
||||
server := parseServer(serverContent)
|
||||
config.Servers = append(config.Servers, server)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// extractUpstreams 提取所有 upstream 块
|
||||
func extractUpstreams(content string) []NginxUpstream {
|
||||
upstreams := []NginxUpstream{}
|
||||
upstreamRegex := regexp.MustCompile(`upstream\s+(\S+)\s*\{([^}]*)\}`)
|
||||
matches := upstreamRegex.FindAllStringSubmatch(content, -1)
|
||||
|
||||
for _, match := range matches {
|
||||
if len(match) >= 3 {
|
||||
name := match[1]
|
||||
body := match[2]
|
||||
upstream := NginxUpstream{
|
||||
Name: name,
|
||||
Servers: []string{},
|
||||
}
|
||||
|
||||
// 提取 server 指令
|
||||
serverRegex := regexp.MustCompile(`server\s+([^;]+);`)
|
||||
serverMatches := serverRegex.FindAllStringSubmatch(body, -1)
|
||||
for _, sm := range serverMatches {
|
||||
if len(sm) >= 2 {
|
||||
upstream.Servers = append(upstream.Servers, strings.TrimSpace(sm[1]))
|
||||
}
|
||||
}
|
||||
|
||||
// 检测负载均衡方法
|
||||
if strings.Contains(body, "ip_hash") {
|
||||
upstream.Method = "ip_hash"
|
||||
} else if strings.Contains(body, "least_conn") {
|
||||
upstream.Method = "least_conn"
|
||||
}
|
||||
|
||||
upstreams = append(upstreams, upstream)
|
||||
}
|
||||
}
|
||||
|
||||
return upstreams
|
||||
}
|
||||
|
||||
// extractServers 提取所有 server 块的内容
|
||||
func extractServers(content string) []string {
|
||||
servers := []string{}
|
||||
|
||||
// 简单的大括号匹配提取
|
||||
lines := strings.Split(content, "\n")
|
||||
inServer := false
|
||||
braceCount := 0
|
||||
var currentServer strings.Builder
|
||||
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
|
||||
// 检测 server 块开始
|
||||
if strings.HasPrefix(trimmed, "server") && strings.Contains(trimmed, "{") {
|
||||
inServer = true
|
||||
currentServer.Reset()
|
||||
currentServer.WriteString(line + "\n")
|
||||
braceCount = strings.Count(line, "{") - strings.Count(line, "}")
|
||||
continue
|
||||
}
|
||||
|
||||
if inServer {
|
||||
currentServer.WriteString(line + "\n")
|
||||
braceCount += strings.Count(line, "{") - strings.Count(line, "}")
|
||||
|
||||
if braceCount == 0 {
|
||||
servers = append(servers, currentServer.String())
|
||||
inServer = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return servers
|
||||
}
|
||||
|
||||
// parseServer 解析单个 server 块
|
||||
func parseServer(content string) NginxServer {
|
||||
server := NginxServer{
|
||||
Listen: []string{},
|
||||
ServerNames: []string{},
|
||||
Locations: []NginxLocation{},
|
||||
Directives: make(map[string][]string),
|
||||
}
|
||||
|
||||
lines := strings.Split(content, "\n")
|
||||
|
||||
// 解析 listen 指令
|
||||
listenRegex := regexp.MustCompile(`^\s*listen\s+([^;]+);`)
|
||||
for _, line := range lines {
|
||||
if match := listenRegex.FindStringSubmatch(line); match != nil {
|
||||
server.Listen = append(server.Listen, strings.TrimSpace(match[1]))
|
||||
}
|
||||
}
|
||||
|
||||
// 解析 server_name 指令
|
||||
serverNameRegex := regexp.MustCompile(`^\s*server_name\s+([^;]+);`)
|
||||
for _, line := range lines {
|
||||
if match := serverNameRegex.FindStringSubmatch(line); match != nil {
|
||||
names := strings.Fields(match[1])
|
||||
server.ServerNames = append(server.ServerNames, names...)
|
||||
}
|
||||
}
|
||||
|
||||
// 解析 SSL 配置
|
||||
server.SSL = parseSSL(content)
|
||||
|
||||
// 解析 location 块
|
||||
server.Locations = extractLocations(content)
|
||||
|
||||
// 解析其他常见指令
|
||||
commonDirectives := []string{
|
||||
"root", "index", "access_log", "error_log",
|
||||
"client_max_body_size", "proxy_set_header",
|
||||
}
|
||||
|
||||
for _, directive := range commonDirectives {
|
||||
pattern := fmt.Sprintf(`(?m)^\s*%s\s+([^;]+);`, directive)
|
||||
regex := regexp.MustCompile(pattern)
|
||||
matches := regex.FindAllStringSubmatch(content, -1)
|
||||
for _, match := range matches {
|
||||
if len(match) >= 2 {
|
||||
server.Directives[directive] = append(server.Directives[directive], strings.TrimSpace(match[1]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
// parseSSL 解析 SSL 配置
|
||||
func parseSSL(content string) *NginxSSL {
|
||||
hasSSL := strings.Contains(content, "ssl") || strings.Contains(content, "443")
|
||||
if !hasSSL {
|
||||
return nil
|
||||
}
|
||||
|
||||
ssl := &NginxSSL{
|
||||
Protocols: []string{},
|
||||
}
|
||||
|
||||
// 提取证书路径
|
||||
certRegex := regexp.MustCompile(`ssl_certificate\s+([^;]+);`)
|
||||
if match := certRegex.FindStringSubmatch(content); match != nil {
|
||||
ssl.Certificate = strings.TrimSpace(match[1])
|
||||
}
|
||||
|
||||
// 提取私钥路径
|
||||
keyRegex := regexp.MustCompile(`ssl_certificate_key\s+([^;]+);`)
|
||||
if match := keyRegex.FindStringSubmatch(content); match != nil {
|
||||
ssl.CertificateKey = strings.TrimSpace(match[1])
|
||||
}
|
||||
|
||||
// 提取协议
|
||||
protocolRegex := regexp.MustCompile(`ssl_protocols\s+([^;]+);`)
|
||||
if match := protocolRegex.FindStringSubmatch(content); match != nil {
|
||||
ssl.Protocols = strings.Fields(match[1])
|
||||
}
|
||||
|
||||
// 提取加密套件
|
||||
cipherRegex := regexp.MustCompile(`ssl_ciphers\s+([^;]+);`)
|
||||
if match := cipherRegex.FindStringSubmatch(content); match != nil {
|
||||
ssl.Ciphers = strings.TrimSpace(match[1])
|
||||
}
|
||||
|
||||
return ssl
|
||||
}
|
||||
|
||||
// extractLocations 提取所有 location 块
|
||||
func extractLocations(content string) []NginxLocation {
|
||||
locations := []NginxLocation{}
|
||||
|
||||
// 匹配 location 块
|
||||
locationRegex := regexp.MustCompile(`location\s+(=|~|~\*|\^~)?\s*([^\s{]+)\s*\{([^}]*)\}`)
|
||||
matches := locationRegex.FindAllStringSubmatch(content, -1)
|
||||
|
||||
for _, match := range matches {
|
||||
if len(match) >= 4 {
|
||||
modifier := strings.TrimSpace(match[1])
|
||||
path := strings.TrimSpace(match[2])
|
||||
body := match[3]
|
||||
|
||||
location := NginxLocation{
|
||||
Path: path,
|
||||
Modifier: modifier,
|
||||
Rewrite: []string{},
|
||||
Directives: make(map[string][]string),
|
||||
}
|
||||
|
||||
// 提取 proxy_pass
|
||||
proxyPassRegex := regexp.MustCompile(`proxy_pass\s+([^;]+);`)
|
||||
if ppMatch := proxyPassRegex.FindStringSubmatch(body); ppMatch != nil {
|
||||
location.ProxyPass = strings.TrimSpace(ppMatch[1])
|
||||
}
|
||||
|
||||
// 提取 rewrite 规则
|
||||
rewriteRegex := regexp.MustCompile(`rewrite\s+([^;]+);`)
|
||||
rewriteMatches := rewriteRegex.FindAllStringSubmatch(body, -1)
|
||||
for _, rm := range rewriteMatches {
|
||||
if len(rm) >= 2 {
|
||||
location.Rewrite = append(location.Rewrite, strings.TrimSpace(rm[1]))
|
||||
}
|
||||
}
|
||||
|
||||
// 提取 return 指令
|
||||
returnRegex := regexp.MustCompile(`return\s+(\d+)(?:\s+([^;]+))?;`)
|
||||
if retMatch := returnRegex.FindStringSubmatch(body); retMatch != nil {
|
||||
code := 0
|
||||
fmt.Sscanf(retMatch[1], "%d", &code)
|
||||
location.Return = &NginxReturn{
|
||||
Code: code,
|
||||
}
|
||||
if len(retMatch) >= 3 {
|
||||
urlOrText := strings.TrimSpace(retMatch[2])
|
||||
if strings.HasPrefix(urlOrText, "http") {
|
||||
location.Return.URL = urlOrText
|
||||
} else {
|
||||
location.Return.Text = urlOrText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 提取其他指令
|
||||
commonDirectives := []string{
|
||||
"proxy_set_header", "proxy_redirect", "proxy_read_timeout",
|
||||
"add_header", "alias", "root", "try_files",
|
||||
}
|
||||
|
||||
for _, directive := range commonDirectives {
|
||||
pattern := fmt.Sprintf(`(?m)^\s*%s\s+([^;]+);`, directive)
|
||||
regex := regexp.MustCompile(pattern)
|
||||
matches := regex.FindAllStringSubmatch(body, -1)
|
||||
for _, m := range matches {
|
||||
if len(m) >= 2 {
|
||||
location.Directives[directive] = append(location.Directives[directive], strings.TrimSpace(m[1]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
locations = append(locations, location)
|
||||
}
|
||||
}
|
||||
|
||||
return locations
|
||||
}
|
||||
|
||||
// AnalyzeNginxConfig 分析 Nginx 配置,生成用于 AI 的分析报告
|
||||
func AnalyzeNginxConfig(config *NginxConfig) *NginxAnalysis {
|
||||
analysis := &NginxAnalysis{
|
||||
ServerCount: len(config.Servers),
|
||||
Features: make(map[string]bool),
|
||||
Complexity: "simple",
|
||||
Suggestions: []string{},
|
||||
}
|
||||
|
||||
totalLocations := 0
|
||||
hasSSL := false
|
||||
hasRewrite := false
|
||||
hasUpstream := len(config.Upstreams) > 0
|
||||
hasComplexRouting := false
|
||||
uniqueDomains := make(map[string]bool)
|
||||
|
||||
for _, server := range config.Servers {
|
||||
// 统计域名
|
||||
for _, name := range server.ServerNames {
|
||||
uniqueDomains[name] = true
|
||||
}
|
||||
|
||||
// 统计 location
|
||||
totalLocations += len(server.Locations)
|
||||
|
||||
// 检测 SSL
|
||||
if server.SSL != nil {
|
||||
hasSSL = true
|
||||
analysis.Features["ssl"] = true
|
||||
}
|
||||
|
||||
// 检测 location 特性
|
||||
for _, loc := range server.Locations {
|
||||
if loc.ProxyPass != "" {
|
||||
analysis.Features["proxy"] = true
|
||||
}
|
||||
if len(loc.Rewrite) > 0 {
|
||||
hasRewrite = true
|
||||
analysis.Features["rewrite"] = true
|
||||
}
|
||||
if loc.Return != nil {
|
||||
analysis.Features["return"] = true
|
||||
if loc.Return.Code >= 300 && loc.Return.Code < 400 {
|
||||
analysis.Features["redirect"] = true
|
||||
}
|
||||
}
|
||||
if loc.Modifier != "" {
|
||||
hasComplexRouting = true
|
||||
analysis.Features["complex_routing"] = true
|
||||
}
|
||||
// 检测其他指令
|
||||
if _, ok := loc.Directives["proxy_set_header"]; ok {
|
||||
analysis.Features["header_manipulation"] = true
|
||||
}
|
||||
if _, ok := loc.Directives["add_header"]; ok {
|
||||
analysis.Features["response_headers"] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
analysis.LocationCount = totalLocations
|
||||
analysis.DomainCount = len(uniqueDomains)
|
||||
|
||||
// 判断复杂度
|
||||
if analysis.ServerCount > 3 || totalLocations > 10 || (hasRewrite && hasSSL && hasComplexRouting) {
|
||||
analysis.Complexity = "high"
|
||||
} else if analysis.ServerCount > 1 || totalLocations > 5 || hasRewrite || hasSSL || hasUpstream {
|
||||
analysis.Complexity = "medium"
|
||||
}
|
||||
|
||||
// 生成建议
|
||||
if analysis.Features["proxy"] {
|
||||
analysis.Suggestions = append(analysis.Suggestions, "proxy_pass 将转换为 Ingress/HTTPRoute 的 backend 配置")
|
||||
}
|
||||
if analysis.Features["rewrite"] {
|
||||
analysis.Suggestions = append(analysis.Suggestions, "rewrite 规则需要使用 Higress 注解实现,如 higress.io/rewrite-target")
|
||||
}
|
||||
if analysis.Features["ssl"] {
|
||||
analysis.Suggestions = append(analysis.Suggestions, "SSL 证书需要创建 Kubernetes Secret,并在 Ingress 中引用")
|
||||
}
|
||||
if analysis.Features["redirect"] {
|
||||
analysis.Suggestions = append(analysis.Suggestions, "redirect 可以使用 Higress 的重定向注解或插件实现")
|
||||
}
|
||||
if hasUpstream {
|
||||
analysis.Suggestions = append(analysis.Suggestions, "upstream 负载均衡将由 Kubernetes Service 和 Endpoints 实现")
|
||||
}
|
||||
if analysis.Features["header_manipulation"] {
|
||||
analysis.Suggestions = append(analysis.Suggestions, "请求头操作可以使用 Higress 注解或 custom-response 插件实现")
|
||||
}
|
||||
|
||||
return analysis
|
||||
}
|
||||
|
||||
// NginxAnalysis 表示 Nginx 配置分析结果
|
||||
type NginxAnalysis struct {
|
||||
ServerCount int `json:"server_count"`
|
||||
LocationCount int `json:"location_count"`
|
||||
DomainCount int `json:"domain_count"`
|
||||
Features map[string]bool `json:"features"`
|
||||
Complexity string `json:"complexity"` // simple, medium, high
|
||||
Suggestions []string `json:"suggestions"`
|
||||
}
|
||||
@@ -0,0 +1,448 @@
|
||||
// Tool Chain implementations for LLM-guided Lua to WASM conversion
|
||||
package tools
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// AnalysisResultForAI 结构化的分析结果,用于 AI 协作
|
||||
type AnalysisResultForAI struct {
|
||||
Features map[string]bool `json:"features"`
|
||||
Variables map[string]string `json:"variables"`
|
||||
APICalls []string `json:"api_calls"`
|
||||
Warnings []string `json:"warnings"`
|
||||
Complexity string `json:"complexity"`
|
||||
Compatibility string `json:"compatibility"`
|
||||
// OriginalCode 字段已移除,避免返回大量数据
|
||||
}
|
||||
|
||||
// ConversionHints 代码转换提示(简化版)
|
||||
type ConversionHints struct {
|
||||
CodeTemplate string `json:"code_template"`
|
||||
Warnings []string `json:"warnings"`
|
||||
}
|
||||
|
||||
// ValidationReport 验证报告
|
||||
type ValidationReport struct {
|
||||
Issues []ValidationIssue `json:"issues"` // 所有发现的问题
|
||||
MissingImports []string `json:"missing_imports"` // 缺失的 import
|
||||
FoundCallbacks []string `json:"found_callbacks"` // 找到的回调函数
|
||||
HasConfig bool `json:"has_config"` // 是否有配置结构
|
||||
Summary string `json:"summary"` // 总体评估摘要
|
||||
}
|
||||
|
||||
// ValidationIssue 验证问题
|
||||
type ValidationIssue struct {
|
||||
Category string `json:"category"` // required, recommended, optional, best_practice
|
||||
Type string `json:"type"` // syntax, api_usage, config, error_handling, logging, etc.
|
||||
Message string `json:"message"` // 问题描述
|
||||
Suggestion string `json:"suggestion"` // 改进建议
|
||||
Impact string `json:"impact"` // 影响说明(为什么重要)
|
||||
}
|
||||
|
||||
// DeploymentPackage 部署配置包
|
||||
type DeploymentPackage struct {
|
||||
WasmPluginYAML string `json:"wasm_plugin_yaml"`
|
||||
Makefile string `json:"makefile"`
|
||||
Dockerfile string `json:"dockerfile"`
|
||||
ConfigMap string `json:"config_map"`
|
||||
README string `json:"readme"`
|
||||
TestScript string `json:"test_script"`
|
||||
Dependencies map[string]string `json:"dependencies"`
|
||||
}
|
||||
|
||||
// AnalyzeLuaPluginForAI 分析 Lua 插件并生成 AI 友好的输出
|
||||
func AnalyzeLuaPluginForAI(luaCode string) AnalysisResultForAI {
|
||||
analyzer := AnalyzeLuaScript(luaCode)
|
||||
|
||||
// 收集所有 API 调用
|
||||
apiCalls := []string{}
|
||||
for feature := range analyzer.Features {
|
||||
apiCalls = append(apiCalls, feature)
|
||||
}
|
||||
|
||||
// 确定兼容性级别
|
||||
compatibility := "full"
|
||||
if len(analyzer.Warnings) > 0 {
|
||||
compatibility = "partial"
|
||||
}
|
||||
if len(analyzer.Warnings) > 2 {
|
||||
compatibility = "manual"
|
||||
}
|
||||
|
||||
return AnalysisResultForAI{
|
||||
Features: analyzer.Features,
|
||||
Variables: analyzer.Variables,
|
||||
APICalls: apiCalls,
|
||||
Warnings: analyzer.Warnings,
|
||||
Complexity: analyzer.Complexity,
|
||||
Compatibility: compatibility,
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateConversionHints 生成代码转换提示(简化版)
|
||||
func GenerateConversionHints(analysis AnalysisResultForAI, pluginName string) ConversionHints {
|
||||
return ConversionHints{
|
||||
CodeTemplate: generateCodeTemplate(analysis, pluginName),
|
||||
Warnings: analysis.Warnings,
|
||||
}
|
||||
}
|
||||
|
||||
// generateCodeTemplate 生成代码模板提示
|
||||
func generateCodeTemplate(analysis AnalysisResultForAI, pluginName string) string {
|
||||
callbacks := generateCallbackSummary(analysis)
|
||||
return fmt.Sprintf(`生成 Go WASM 插件 %s,实现回调: %s
|
||||
参考文档: https://higress.cn/docs/latest/user/wasm-go/`,
|
||||
pluginName, callbacks)
|
||||
}
|
||||
|
||||
// generateCallbackRegistrations 生成回调注册代码
|
||||
func generateCallbackRegistrations(analysis AnalysisResultForAI) string {
|
||||
callbacks := []string{}
|
||||
|
||||
if analysis.Features["ngx.var"] || analysis.Features["request_headers"] || analysis.Features["header_manipulation"] {
|
||||
callbacks = append(callbacks, "wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders)")
|
||||
}
|
||||
|
||||
if analysis.Features["request_body"] {
|
||||
callbacks = append(callbacks, "wrapper.ProcessRequestBodyBy(onHttpRequestBody)")
|
||||
}
|
||||
|
||||
if analysis.Features["response_headers"] || analysis.Features["response_control"] {
|
||||
callbacks = append(callbacks, "wrapper.ProcessResponseHeadersBy(onHttpResponseHeaders)")
|
||||
}
|
||||
|
||||
if len(callbacks) == 0 {
|
||||
callbacks = append(callbacks, "wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders)")
|
||||
}
|
||||
|
||||
return "\n\t\t" + strings.Join(callbacks, ",\n\t\t")
|
||||
}
|
||||
|
||||
// generateCallbackSummary 生成回调函数摘要
|
||||
func generateCallbackSummary(analysis AnalysisResultForAI) string {
|
||||
callbacks := []string{}
|
||||
|
||||
if analysis.Features["ngx.var"] || analysis.Features["request_headers"] || analysis.Features["header_manipulation"] {
|
||||
callbacks = append(callbacks, "onHttpRequestHeaders")
|
||||
}
|
||||
if analysis.Features["request_body"] {
|
||||
callbacks = append(callbacks, "onHttpRequestBody")
|
||||
}
|
||||
if analysis.Features["response_headers"] || analysis.Features["response_control"] {
|
||||
callbacks = append(callbacks, "onHttpResponseHeaders")
|
||||
}
|
||||
|
||||
if len(callbacks) == 0 {
|
||||
return "onHttpRequestHeaders"
|
||||
}
|
||||
return strings.Join(callbacks, ", ")
|
||||
}
|
||||
|
||||
// ValidateWasmCode 验证生成的 Go WASM 代码
|
||||
func ValidateWasmCode(goCode, pluginName string) ValidationReport {
|
||||
report := ValidationReport{
|
||||
Issues: []ValidationIssue{},
|
||||
MissingImports: []string{},
|
||||
FoundCallbacks: []string{},
|
||||
HasConfig: false,
|
||||
}
|
||||
|
||||
// 移除注释以避免误判
|
||||
codeWithoutComments := removeComments(goCode)
|
||||
|
||||
// 检查必要的包声明
|
||||
packagePattern := regexp.MustCompile(`(?m)^package\s+main\s*$`)
|
||||
if !packagePattern.MatchString(goCode) {
|
||||
report.Issues = append(report.Issues, ValidationIssue{
|
||||
Category: "required",
|
||||
Type: "syntax",
|
||||
Message: "缺少 'package main' 声明",
|
||||
Suggestion: "在文件开头添加: package main",
|
||||
Impact: "WASM 插件必须使用 package main,否则无法编译",
|
||||
})
|
||||
}
|
||||
|
||||
// 检查 main 函数
|
||||
mainFuncPattern := regexp.MustCompile(`func\s+main\s*\(\s*\)`)
|
||||
if !mainFuncPattern.MatchString(codeWithoutComments) {
|
||||
report.Issues = append(report.Issues, ValidationIssue{
|
||||
Category: "required",
|
||||
Type: "syntax",
|
||||
Message: "缺少 main() 函数",
|
||||
Suggestion: "添加空的 main 函数: func main() {}",
|
||||
Impact: "WASM 插件必须有 main 函数,即使是空的",
|
||||
})
|
||||
}
|
||||
|
||||
// 检查 init 函数
|
||||
initFuncPattern := regexp.MustCompile(`func\s+init\s*\(\s*\)`)
|
||||
if !initFuncPattern.MatchString(codeWithoutComments) {
|
||||
report.Issues = append(report.Issues, ValidationIssue{
|
||||
Category: "required",
|
||||
Type: "api_usage",
|
||||
Message: "缺少 init() 函数",
|
||||
Suggestion: "添加 init() 函数用于注册插件",
|
||||
Impact: "插件需要在 init() 中调用 wrapper.SetCtx 进行注册",
|
||||
})
|
||||
}
|
||||
|
||||
// 检查 wrapper.SetCtx 调用
|
||||
setCtxPattern := regexp.MustCompile(`wrapper\.SetCtx\s*\(`)
|
||||
if !setCtxPattern.MatchString(codeWithoutComments) {
|
||||
report.Issues = append(report.Issues, ValidationIssue{
|
||||
Category: "required",
|
||||
Type: "api_usage",
|
||||
Message: "缺少 wrapper.SetCtx 调用",
|
||||
Suggestion: "在 init() 函数中调用 wrapper.SetCtx 注册插件上下文",
|
||||
Impact: "没有注册插件上下文将导致插件无法工作",
|
||||
})
|
||||
}
|
||||
|
||||
// 检查必要的 import
|
||||
requiredImports := map[string]string{
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types": "定义了 Action 等核心类型",
|
||||
"github.com/higress-group/wasm-go/pkg/wrapper": "提供了 Higress 插件开发的高级封装",
|
||||
}
|
||||
|
||||
for importPath, reason := range requiredImports {
|
||||
if !containsImport(goCode, importPath) {
|
||||
report.MissingImports = append(report.MissingImports, importPath)
|
||||
report.Issues = append(report.Issues, ValidationIssue{
|
||||
Category: "required",
|
||||
Type: "imports",
|
||||
Message: fmt.Sprintf("缺少必需的导入: %s", importPath),
|
||||
Suggestion: fmt.Sprintf(`添加导入: import "%s"`, importPath),
|
||||
Impact: reason,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 检查可选但推荐的 import
|
||||
if !containsImport(goCode, "github.com/higress-group/proxy-wasm-go-sdk/proxywasm") {
|
||||
report.Issues = append(report.Issues, ValidationIssue{
|
||||
Category: "optional",
|
||||
Type: "imports",
|
||||
Message: "未导入 proxywasm 包",
|
||||
Suggestion: "如需使用日志、HTTP 调用等底层 API,可导入 proxywasm 包",
|
||||
Impact: "proxywasm 提供了日志记录、外部 HTTP 调用等功能",
|
||||
})
|
||||
}
|
||||
|
||||
// 检查配置结构体
|
||||
configPattern := regexp.MustCompile(`type\s+\w+Config\s+struct\s*\{`)
|
||||
report.HasConfig = configPattern.MatchString(goCode)
|
||||
|
||||
if !report.HasConfig {
|
||||
report.Issues = append(report.Issues, ValidationIssue{
|
||||
Category: "optional",
|
||||
Type: "config",
|
||||
Message: "未定义配置结构体",
|
||||
Suggestion: "如果插件需要配置参数,建议定义配置结构体(如 type MyPluginConfig struct { ... })",
|
||||
Impact: "配置结构体用于接收和解析插件的配置参数,支持动态配置",
|
||||
})
|
||||
}
|
||||
|
||||
// 检查 parseConfig 函数
|
||||
parseConfigPattern := regexp.MustCompile(`func\s+parseConfig\s*\(`)
|
||||
hasParseConfig := parseConfigPattern.MatchString(codeWithoutComments)
|
||||
|
||||
if report.HasConfig && !hasParseConfig {
|
||||
report.Issues = append(report.Issues, ValidationIssue{
|
||||
Category: "recommended",
|
||||
Type: "config",
|
||||
Message: "定义了配置结构体但缺少 parseConfig 函数",
|
||||
Suggestion: "实现 parseConfig 函数来解析配置: func parseConfig(json gjson.Result, config *MyPluginConfig, log wrapper.Log) error",
|
||||
Impact: "parseConfig 函数负责将 JSON 配置解析到结构体,是配置系统的核心",
|
||||
})
|
||||
}
|
||||
|
||||
// 检查回调函数
|
||||
callbacks := map[string]*regexp.Regexp{
|
||||
"onHttpRequestHeaders": regexp.MustCompile(`func\s+onHttpRequestHeaders\s*\(`),
|
||||
"onHttpRequestBody": regexp.MustCompile(`func\s+onHttpRequestBody\s*\(`),
|
||||
"onHttpResponseHeaders": regexp.MustCompile(`func\s+onHttpResponseHeaders\s*\(`),
|
||||
"onHttpResponseBody": regexp.MustCompile(`func\s+onHttpResponseBody\s*\(`),
|
||||
}
|
||||
|
||||
for name, pattern := range callbacks {
|
||||
if pattern.MatchString(codeWithoutComments) {
|
||||
report.FoundCallbacks = append(report.FoundCallbacks, name)
|
||||
}
|
||||
}
|
||||
|
||||
if len(report.FoundCallbacks) == 0 {
|
||||
report.Issues = append(report.Issues, ValidationIssue{
|
||||
Category: "required",
|
||||
Type: "api_usage",
|
||||
Message: "未找到任何 HTTP 回调函数实现",
|
||||
Suggestion: "至少实现一个回调函数,如: func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyPluginConfig, log wrapper.Log) types.Action",
|
||||
Impact: "回调函数是插件逻辑的核心,没有回调函数插件将不会执行任何操作",
|
||||
})
|
||||
}
|
||||
|
||||
// 检查错误处理
|
||||
errHandlingCount := strings.Count(codeWithoutComments, "if err != nil")
|
||||
funcCount := strings.Count(codeWithoutComments, "func ")
|
||||
if funcCount > 3 && errHandlingCount == 0 {
|
||||
report.Issues = append(report.Issues, ValidationIssue{
|
||||
Category: "best_practice",
|
||||
Type: "error_handling",
|
||||
Message: "代码中缺少错误处理",
|
||||
Suggestion: "对可能返回错误的操作添加错误检查: if err != nil { ... }",
|
||||
Impact: "良好的错误处理可以提高插件的健壮性和可调试性",
|
||||
})
|
||||
}
|
||||
|
||||
// 检查日志记录
|
||||
hasLogging := strings.Contains(codeWithoutComments, "proxywasm.Log") ||
|
||||
strings.Contains(codeWithoutComments, "log.Error") ||
|
||||
strings.Contains(codeWithoutComments, "log.Warn") ||
|
||||
strings.Contains(codeWithoutComments, "log.Info") ||
|
||||
strings.Contains(codeWithoutComments, "log.Debug")
|
||||
|
||||
if !hasLogging {
|
||||
report.Issues = append(report.Issues, ValidationIssue{
|
||||
Category: "best_practice",
|
||||
Type: "logging",
|
||||
Message: "代码中没有日志记录",
|
||||
Suggestion: "添加适当的日志记录,如: proxywasm.LogInfo(), log.Errorf() 等",
|
||||
Impact: "日志记录有助于调试、监控和问题排查",
|
||||
})
|
||||
}
|
||||
|
||||
// 检查回调函数的返回值
|
||||
checkCallbackReturnErrors(&report, codeWithoutComments, report.FoundCallbacks)
|
||||
|
||||
// 生成总体评估摘要
|
||||
report.Summary = generateValidationSummary(report)
|
||||
|
||||
return report
|
||||
}
|
||||
|
||||
// removeComments 移除 Go 代码中的注释
|
||||
func removeComments(code string) string {
|
||||
// 移除单行注释
|
||||
singleLineComment := regexp.MustCompile(`//.*`)
|
||||
code = singleLineComment.ReplaceAllString(code, "")
|
||||
|
||||
// 移除多行注释
|
||||
multiLineComment := regexp.MustCompile(`(?s)/\*.*?\*/`)
|
||||
code = multiLineComment.ReplaceAllString(code, "")
|
||||
|
||||
return code
|
||||
}
|
||||
|
||||
// containsImport 检查是否包含特定的 import
|
||||
func containsImport(code, importPath string) bool {
|
||||
// 匹配 import "path" 或 import ("path")
|
||||
pattern := regexp.MustCompile(`import\s+(?:\([\s\S]*?)?["` + "`" + `]` +
|
||||
regexp.QuoteMeta(importPath) + `["` + "`" + `]`)
|
||||
return pattern.MatchString(code)
|
||||
}
|
||||
|
||||
// checkCallbackReturnErrors 检查回调函数的返回值错误
|
||||
func checkCallbackReturnErrors(report *ValidationReport, code string, foundCallbacks []string) {
|
||||
// 检查回调函数内是否有 return nil(应该返回 types.Action)
|
||||
for _, callback := range foundCallbacks {
|
||||
// 提取回调函数体(简化的检查)
|
||||
funcPattern := regexp.MustCompile(
|
||||
`func\s+` + callback + `\s*\([^)]*\)\s+types\.Action\s*\{[^}]*return\s+nil[^}]*\}`)
|
||||
if funcPattern.MatchString(code) {
|
||||
report.Issues = append(report.Issues, ValidationIssue{
|
||||
Category: "required",
|
||||
Type: "api_usage",
|
||||
Message: fmt.Sprintf("回调函数 %s 不应返回 nil", callback),
|
||||
Suggestion: "回调函数应返回 types.Action,如: return types.ActionContinue",
|
||||
Impact: "返回 nil 会导致编译错误或运行时异常",
|
||||
})
|
||||
break // 只报告一次
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否正确返回 types.Action
|
||||
if len(foundCallbacks) > 0 {
|
||||
hasActionReturn := strings.Contains(code, "types.ActionContinue") ||
|
||||
strings.Contains(code, "types.ActionPause") ||
|
||||
strings.Contains(code, "types.ActionSuspend")
|
||||
|
||||
if !hasActionReturn {
|
||||
report.Issues = append(report.Issues, ValidationIssue{
|
||||
Category: "recommended",
|
||||
Type: "api_usage",
|
||||
Message: "未找到明确的 Action 返回值",
|
||||
Suggestion: "回调函数应返回明确的 types.Action 值(ActionContinue、ActionPause 等)",
|
||||
Impact: "明确的返回值有助于代码可读性和正确性",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// generateValidationSummary 生成验证摘要
|
||||
func generateValidationSummary(report ValidationReport) string {
|
||||
requiredIssues := 0
|
||||
recommendedIssues := 0
|
||||
optionalIssues := 0
|
||||
bestPracticeIssues := 0
|
||||
|
||||
for _, issue := range report.Issues {
|
||||
switch issue.Category {
|
||||
case "required":
|
||||
requiredIssues++
|
||||
case "recommended":
|
||||
recommendedIssues++
|
||||
case "optional":
|
||||
optionalIssues++
|
||||
case "best_practice":
|
||||
bestPracticeIssues++
|
||||
}
|
||||
}
|
||||
|
||||
if requiredIssues > 0 {
|
||||
return fmt.Sprintf("代码存在 %d 个必须修复的问题,%d 个建议修复的问题,%d 个可选优化项,%d 个最佳实践建议。请优先解决必须修复的问题。",
|
||||
requiredIssues, recommendedIssues, optionalIssues, bestPracticeIssues)
|
||||
}
|
||||
|
||||
if recommendedIssues > 0 {
|
||||
return fmt.Sprintf("代码基本结构正确,但有 %d 个建议修复的问题,%d 个可选优化项,%d 个最佳实践建议。",
|
||||
recommendedIssues, optionalIssues, bestPracticeIssues)
|
||||
}
|
||||
|
||||
if optionalIssues > 0 || bestPracticeIssues > 0 {
|
||||
return fmt.Sprintf("代码结构良好,有 %d 个可选优化项和 %d 个最佳实践建议可以考虑。",
|
||||
optionalIssues, bestPracticeIssues)
|
||||
}
|
||||
|
||||
callbacksInfo := ""
|
||||
if len(report.FoundCallbacks) > 0 {
|
||||
callbacksInfo = fmt.Sprintf(",实现了 %d 个回调函数", len(report.FoundCallbacks))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("代码验证通过,未发现明显问题%s。", callbacksInfo)
|
||||
}
|
||||
|
||||
// GenerateDeploymentPackage 生成部署配置提示(由 LLM 根据代码生成具体内容)
|
||||
func GenerateDeploymentPackage(pluginName, goCode, configSchema, namespace string) DeploymentPackage {
|
||||
// 所有配置文件都由 LLM 根据实际代码生成,不使用固定模板
|
||||
return DeploymentPackage{
|
||||
WasmPluginYAML: "", // LLM 生成
|
||||
Makefile: "", // LLM 生成
|
||||
Dockerfile: "", // LLM 生成
|
||||
ConfigMap: "", // LLM 生成
|
||||
README: "", // LLM 生成
|
||||
TestScript: "", // LLM 生成
|
||||
Dependencies: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
// FormatToolResultWithAIContext 格式化工具结果
|
||||
func FormatToolResultWithAIContext(userMessage, aiInstructions string, structuredData interface{}) ToolResult {
|
||||
jsonData, _ := json.MarshalIndent(structuredData, "", " ")
|
||||
output := fmt.Sprintf("%s\n\n%s\n\n%s", userMessage, aiInstructions, string(jsonData))
|
||||
return ToolResult{
|
||||
Content: []Content{{Type: "text", Text: output}},
|
||||
}
|
||||
}
|
||||
@@ -132,7 +132,7 @@ data:
|
||||
- path_rewrite_prefix: ""
|
||||
upstream_type: ""
|
||||
enable_path_rewrite: false
|
||||
match_rule_domain: ""
|
||||
match_rule_domain: "*"
|
||||
match_rule_path: "/mcp-servers/rag"
|
||||
match_rule_type: "prefix"
|
||||
servers:
|
||||
@@ -323,5 +323,615 @@ Open your browser and navigate to http://localhost:8000
|
||||
```
|
||||
|
||||
|
||||
## 如何对接已有的向量数据库
|
||||
|
||||
### 1. 基于 langchain + langchain-milvus 代码样例,用于生成测试向量数据库。
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
基于 LangChain Milvus 的文档处理系统
|
||||
功能:
|
||||
1. 使用 langchain UnstructuredFileLoader 加载文本文件成 Document
|
||||
2. 使用 RecursiveTextSplitter 对 Document 进行 chunk 分割
|
||||
3. 使用 OpenAI 兼容的 embedding 模型生成向量
|
||||
4. 使用 langchain_milvus.Milvus 进行向量存储和检索, 参考文档 https://python.langchain.com/docs/integrations/vectorstores/milvus/
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
import uuid
|
||||
from typing import List, Dict, Any, Optional
|
||||
from pathlib import Path
|
||||
|
||||
# LangChain imports
|
||||
from langchain_community.document_loaders import UnstructuredFileLoader
|
||||
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
||||
from langchain_core.documents import Document
|
||||
from langchain_milvus import Milvus
|
||||
from langchain_core.embeddings import Embeddings
|
||||
|
||||
# OpenAI client import
|
||||
from openai import OpenAI
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DashScopeEmbeddings(Embeddings):
|
||||
def __init__(self, openai_api_key: Optional[str] = None, openai_api_base: Optional[str] = None, model: str = "text-embedding-v1", dim: int = 1536):
|
||||
self.client = OpenAI(
|
||||
api_key=openai_api_key or os.getenv("DASHSCOPE_API_KEY"),
|
||||
base_url=openai_api_base or "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||
)
|
||||
self.model = model
|
||||
self.dim = dim
|
||||
|
||||
def embed_documents(self, texts: List[str]) -> List[List[float]]:
|
||||
response = self.client.embeddings.create(
|
||||
model=self.model,
|
||||
input=texts,
|
||||
dimensions=self.dim,
|
||||
encoding_format="float"
|
||||
)
|
||||
return [data.embedding for data in response.data]
|
||||
|
||||
def embed_query(self, text: str) -> List[float]:
|
||||
response = self.client.embeddings.create(
|
||||
model=self.model,
|
||||
input=[text],
|
||||
dimensions=self.dim,
|
||||
encoding_format="float"
|
||||
)
|
||||
return response.data[0].embedding
|
||||
|
||||
|
||||
class LangChainMilvusProcessor:
|
||||
"""基于 LangChain Milvus 的文档处理器"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
milvus_uri: str = "http://localhost:19530",
|
||||
milvus_token: str = "",
|
||||
db_name: str = "default",
|
||||
collection_name: str = "langchain_rag",
|
||||
embedding_model: str = "text-embedding-v4",
|
||||
openai_api_key: Optional[str] = None,
|
||||
openai_api_base: Optional[str] = None,
|
||||
chunk_size: int = 500,
|
||||
chunk_overlap: int = 50,
|
||||
embedding_dim: int = 1024,
|
||||
drop_old: bool = False
|
||||
):
|
||||
"""
|
||||
初始化 LangChain Milvus 文档处理器
|
||||
|
||||
Args:
|
||||
milvus_uri: Milvus 服务器 URI
|
||||
milvus_token: Milvus 认证 token
|
||||
db_name: 数据库名称
|
||||
collection_name: 集合名称
|
||||
embedding_model: 嵌入模型名称
|
||||
openai_api_key: OpenAI API 密钥
|
||||
openai_api_base: OpenAI API 基础 URL
|
||||
chunk_size: 文本分割大小
|
||||
chunk_overlap: 文本分割重叠大小
|
||||
embedding_dim: 向量维度
|
||||
drop_old: 是否删除已存在的集合
|
||||
"""
|
||||
self.milvus_uri = milvus_uri
|
||||
self.milvus_token = milvus_token
|
||||
self.db_name = db_name
|
||||
self.collection_name = collection_name
|
||||
self.chunk_size = chunk_size
|
||||
self.chunk_overlap = chunk_overlap
|
||||
self.embedding_dim = embedding_dim
|
||||
self.drop_old = drop_old
|
||||
self.embedding_model = embedding_model
|
||||
self.embedding_dim = embedding_dim
|
||||
|
||||
# 初始化文本分割器
|
||||
self.text_splitter = RecursiveCharacterTextSplitter(
|
||||
chunk_size=chunk_size,
|
||||
chunk_overlap=chunk_overlap,
|
||||
length_function=len,
|
||||
separators=["\n\n", "\n", " ", ""]
|
||||
)
|
||||
|
||||
self.embeddings = DashScopeEmbeddings(
|
||||
openai_api_key=openai_api_key,
|
||||
openai_api_base=openai_api_base,
|
||||
model=embedding_model,
|
||||
dim=embedding_dim
|
||||
)
|
||||
|
||||
# 初始化 Milvus 向量存储
|
||||
self.vectorstore = None
|
||||
self._init_vectorstore()
|
||||
|
||||
def _init_vectorstore(self):
|
||||
"""初始化 Milvus 向量存储"""
|
||||
try:
|
||||
self.vectorstore = Milvus(
|
||||
embedding_function=self.embeddings,
|
||||
collection_name=self.collection_name,
|
||||
connection_args={
|
||||
"uri": self.milvus_uri,
|
||||
"token": self.milvus_token,
|
||||
"db_name": self.db_name
|
||||
},
|
||||
index_params={
|
||||
"index_type": "HNSW",
|
||||
"metric_type": "IP",
|
||||
"params": {"M": 8, "efConstruction": 64}
|
||||
},
|
||||
consistency_level="Strong",
|
||||
drop_old=self.drop_old,
|
||||
metadata_field="metadata" # 自定义元数据字段名
|
||||
)
|
||||
|
||||
logger.info(f"成功初始化 Milvus 向量存储: {self.collection_name}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"初始化 Milvus 向量存储失败: {e}")
|
||||
raise
|
||||
|
||||
def load_document(self, file_path: str) -> List[Document]:
|
||||
"""
|
||||
使用 UnstructuredFileLoader 加载文档
|
||||
|
||||
Args:
|
||||
file_path: 文件路径
|
||||
|
||||
Returns:
|
||||
Document 列表
|
||||
"""
|
||||
try:
|
||||
logger.info(f"加载文档: {file_path}")
|
||||
|
||||
# 检查文件是否存在
|
||||
if not os.path.exists(file_path):
|
||||
raise FileNotFoundError(f"文件不存在: {file_path}")
|
||||
|
||||
# 使用 UnstructuredFileLoader 加载文档
|
||||
loader = UnstructuredFileLoader(file_path)
|
||||
documents = loader.load()
|
||||
|
||||
# 添加文件路径到元数据
|
||||
for doc in documents:
|
||||
doc.metadata["source"] = os.path.basename(file_path)
|
||||
doc.metadata["filename"] = file_path
|
||||
|
||||
logger.info(f"成功加载 {len(documents)} 个文档")
|
||||
return documents
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"加载文档失败: {e}")
|
||||
return []
|
||||
|
||||
def split_documents(self, documents: List[Document]) -> List[Document]:
|
||||
"""
|
||||
使用 RecursiveTextSplitter 分割文档
|
||||
|
||||
Args:
|
||||
documents: 文档列表
|
||||
|
||||
Returns:
|
||||
分割后的文档 chunk 列表
|
||||
"""
|
||||
try:
|
||||
logger.info(f"开始分割 {len(documents)} 个文档")
|
||||
|
||||
chunks = self.text_splitter.split_documents(documents)
|
||||
# 为每个 chunk 添加唯一 ID
|
||||
for i, chunk in enumerate(chunks):
|
||||
chunk.metadata["chunk_id"] = str(uuid.uuid4())
|
||||
chunk.metadata["chunk_index"] = i
|
||||
|
||||
logger.info(f"文档分割完成,共生成 {len(chunks)} 个 chunk")
|
||||
return chunks
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"文档分割失败: {e}")
|
||||
return []
|
||||
|
||||
def add_documents(self, documents: List[Document], ids: Optional[List[str]] = None) -> List[str]:
|
||||
"""
|
||||
添加文档到向量存储
|
||||
|
||||
Args:
|
||||
documents: 文档列表
|
||||
ids: 文档 ID 列表(可选)
|
||||
|
||||
Returns:
|
||||
添加的文档 ID 列表
|
||||
"""
|
||||
try:
|
||||
if not documents:
|
||||
logger.warning("没有文档需要添加")
|
||||
return []
|
||||
|
||||
logger.info(f"开始添加 {len(documents)} 个文档到向量存储")
|
||||
|
||||
# 如果没有提供 ID,则生成 UUID
|
||||
if ids is None:
|
||||
ids = [str(uuid.uuid4()) for _ in range(len(documents))]
|
||||
|
||||
# 添加文档到向量存储
|
||||
added_ids = self.vectorstore.add_documents(documents=documents, ids=ids)
|
||||
|
||||
logger.info(f"成功添加 {len(added_ids)} 个文档到向量存储")
|
||||
return added_ids
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"添加文档到向量存储失败: {e}")
|
||||
return []
|
||||
|
||||
def process_file(self, file_path: str) -> bool:
|
||||
"""
|
||||
处理单个文件的完整流程
|
||||
|
||||
Args:
|
||||
file_path: 文件路径
|
||||
|
||||
Returns:
|
||||
是否成功
|
||||
"""
|
||||
try:
|
||||
logger.info(f"开始处理文件: {file_path}")
|
||||
|
||||
# 1. 加载文档
|
||||
documents = self.load_document(file_path)
|
||||
if not documents:
|
||||
return False
|
||||
|
||||
# 2. 分割文档
|
||||
chunks = self.split_documents(documents)
|
||||
if not chunks:
|
||||
return False
|
||||
|
||||
# 3. 添加到向量存储
|
||||
added_ids = self.add_documents(chunks)
|
||||
|
||||
if added_ids:
|
||||
logger.info(f"文件处理完成: {file_path},添加了 {len(added_ids)} 个 chunk")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"文件处理失败: {file_path}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"处理文件失败: {e}")
|
||||
return False
|
||||
|
||||
def process_directory(self, directory_path: str, file_extensions: List[str] = None) -> Dict[str, bool]:
|
||||
"""
|
||||
处理目录中的所有文件
|
||||
|
||||
Args:
|
||||
directory_path: 目录路径
|
||||
file_extensions: 支持的文件扩展名列表
|
||||
|
||||
Returns:
|
||||
文件处理结果字典
|
||||
"""
|
||||
if file_extensions is None:
|
||||
file_extensions = ['.txt', '.md']
|
||||
|
||||
results = {}
|
||||
|
||||
try:
|
||||
directory = Path(directory_path)
|
||||
if not directory.exists():
|
||||
logger.error(f"目录不存在: {directory_path}")
|
||||
return results
|
||||
|
||||
# 遍历目录中的文件
|
||||
for file_path in directory.rglob('*'):
|
||||
if file_path.is_file() and file_path.suffix.lower() in file_extensions:
|
||||
logger.info(f"处理文件: {file_path}")
|
||||
results[str(file_path)] = self.process_file(str(file_path))
|
||||
|
||||
# 统计结果
|
||||
success_count = sum(1 for success in results.values() if success)
|
||||
total_count = len(results)
|
||||
|
||||
logger.info(f"目录处理完成: {success_count}/{total_count} 个文件成功处理")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"处理目录失败: {e}")
|
||||
|
||||
return results
|
||||
|
||||
def similarity_search(self, query: str, k: int = 5) -> List[Document]:
|
||||
"""
|
||||
相似性搜索
|
||||
|
||||
注意:此方法仅用于原生 LangChain 检索示例,如果通过 Higress 网关进行检索,
|
||||
请使用网关提供的 MCP 工具(如 search 工具),无需直接调用此方法。
|
||||
|
||||
Args:
|
||||
query: 查询文本
|
||||
k: 返回结果数量
|
||||
|
||||
Returns:
|
||||
相似文档列表
|
||||
"""
|
||||
try:
|
||||
logger.info(f"执行相似性搜索: {query}")
|
||||
|
||||
results = self.vectorstore.similarity_search(query, k=k)
|
||||
|
||||
logger.info(f"搜索完成,返回 {len(results)} 个结果")
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"相似性搜索失败: {e}")
|
||||
return []
|
||||
|
||||
def similarity_search_with_score(self, query: str, k: int = 5) -> List[tuple]:
|
||||
"""
|
||||
带分数的相似性搜索
|
||||
|
||||
Args:
|
||||
query: 查询文本
|
||||
k: 返回结果数量
|
||||
|
||||
Returns:
|
||||
(文档, 分数) 元组列表
|
||||
"""
|
||||
try:
|
||||
logger.info(f"执行带分数的相似性搜索: {query}")
|
||||
|
||||
results = self.vectorstore.similarity_search_with_score(query, k=k)
|
||||
|
||||
logger.info(f"搜索完成,返回 {len(results)} 个结果")
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"带分数的相似性搜索失败: {e}")
|
||||
return []
|
||||
|
||||
def get_collection_stats(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取集合统计信息
|
||||
|
||||
Returns:
|
||||
集合统计信息字典
|
||||
"""
|
||||
try:
|
||||
# 通过 vectorstore 获取基本信息
|
||||
stats = {
|
||||
"collection_name": self.collection_name,
|
||||
"milvus_uri": self.milvus_uri,
|
||||
"db_name": self.db_name,
|
||||
"embedding_model": self.embedding_model,
|
||||
"embedding_dim": self.embedding_dim,
|
||||
"chunk_size": self.chunk_size,
|
||||
"chunk_overlap": self.chunk_overlap
|
||||
}
|
||||
|
||||
logger.info(f"成功获取集合 {self.collection_name} 的统计信息")
|
||||
return stats
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取集合统计信息失败: {e}")
|
||||
return {}
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数 - 示例用法"""
|
||||
# 配置参数
|
||||
config = {
|
||||
"milvus_uri": "http://localhost:19530",
|
||||
"milvus_token": "",
|
||||
"db_name": "default",
|
||||
"collection_name": "langchain_rag",
|
||||
"embedding_model": "text-embedding-v4",
|
||||
"openai_api_key": "sk-xxxx",
|
||||
"openai_api_base": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
"chunk_size": 500,
|
||||
"chunk_overlap": 50,
|
||||
"embedding_dim": 1024,
|
||||
"drop_old": False
|
||||
}
|
||||
|
||||
# 创建处理器
|
||||
processor = LangChainMilvusProcessor(**config)
|
||||
|
||||
# 示例:处理单个文件
|
||||
file_path = "/path/demo.txt"
|
||||
processor.process_file(file_path)
|
||||
|
||||
# 示例:添加一些测试文档
|
||||
test_documents = [
|
||||
Document(
|
||||
page_content="""Istio 介绍
|
||||
服务网格是一个基础设施层,它为应用程序提供零信任安全、可观察性和高级流量管理等功能, 而无需更改代码。Istio 是最受欢迎、最强大、最值得信赖的服务网格。 Istio 由 Google、IBM 和 Lyft 于 2016 年创立,是云原生计算基金会的一个毕业项目, 与 Kubernetes 和 Prometheus 等项目并列。
|
||||
Istio 可确保云原生和分布式系统具有弹性,帮助现代企业在保持连接和保护的同时跨不同平台维护其工作负载。 它启用安全和治理控制,包括 mTLS 加密、策略管理和访问控制、 支持网络功能,例如金丝雀部署、A/B 测试、负载平衡、故障恢复, 并增加对整个资产流量的可观察性。
|
||||
Istio 并不局限于单个集群、网络或运行时的边界——在 Kubernetes 或 VM、多云、混合或本地上运行的服务都可以包含在单个网格中。
|
||||
Istio 经过精心设计,具有可扩展性,并受到贡献者和合作伙伴的广泛生态系统的支持, 它为各种用例提供打包的集成和分发。您可以独立安装 Istio,也可以选择由提供基于 Istio 的解决方案的商业供应商提供的托管支持。""",
|
||||
metadata={"source": "istio introduction"}
|
||||
),
|
||||
Document(
|
||||
page_content="""Istio 安全概述
|
||||
Istio 安全功能提供了强大的身份、强大的策略、透明的 TLS 加密、 认证/授权/审计(AAA)工具来保护您的服务和数据。Istio 安全功能提供了强大的身份、强大的策略、透明的 TLS 加密、 认证/授权/审计(AAA)工具来保护您的服务和数据。
|
||||
Istio 中的安全性涉及多个组件:
|
||||
- 用于密钥和证书管理的证书颁发机构(CA)
|
||||
- 配置 API 服务器分发给代理:
|
||||
- 认证策略
|
||||
- 授权策略
|
||||
- 安全命名信息
|
||||
- Sidecar 和边缘代理作为策略执行点(PEP) 以保护客户端和服务器之间的通信安全。
|
||||
- 一组 Envoy 代理扩展,用于管理遥测和审计。""",
|
||||
metadata={"source": "istio security"}
|
||||
),
|
||||
Document(
|
||||
page_content="""Istio 流量管理介绍
|
||||
为了在网格中导流,Istio 需要知道所有的 endpoint 在哪以及它们属于哪些服务。 为了定位到 service registry(服务注册中心), Istio 会连接到一个服务发现系统。例如,如果您在 Kubernetes 集群上安装了 Istio, 那么它将自动检测该集群中的服务和 endpoint。
|
||||
使用此服务注册中心,Envoy 代理可以将流量定向到相关服务。大多数基于微服务的应用程序, 每个服务的工作负载都有多个实例来处理流量,称为负载均衡池。默认情况下, Envoy 代理基于轮询调度模型在服务的负载均衡池内分发流量,按顺序将请求发送给池中每个成员, 一旦所有服务实例均接收过一次请求后,就重新回到第一个池成员。
|
||||
Istio 基本的服务发现和负载均衡能力为您提供了一个可用的服务网格, 但它能做到的远比这多的多。在许多情况下,您可能希望对网格的流量情况进行更细粒度的控制。 作为 A/B 测试的一部分,您可能想将特定百分比的流量定向到新版本的服务, 或者为特定的服务实例子集应用不同的负载均衡策略。您可能还想对进出网格的流量应用特殊的规则, 或者将网格的外部依赖项添加到服务注册中心。通过使用 Istio 的流量管理 API 将流量配置添加到 Istio, 就可以完成所有这些甚至更多的工作。
|
||||
和其他 Istio 配置一样,这些 API 也使用 Kubernetes 的自定义资源定义 (CRD)来声明,您可以像示例中看到的那样使用 YAML 进行配置。""",
|
||||
metadata={"source": "istio traffic management"}
|
||||
)
|
||||
]
|
||||
|
||||
# 添加测试文档
|
||||
processor.add_documents(test_documents)
|
||||
|
||||
# 示例:搜索
|
||||
query = "Istio 安全功能"
|
||||
search_results = processor.similarity_search_with_score(query, k=3)
|
||||
|
||||
print(f"\n搜索查询: {query}")
|
||||
print("=" * 50)
|
||||
for doc, score in search_results:
|
||||
print(f"分数: {score:.4f}")
|
||||
print(f"内容: {doc.page_content[:100]}...")
|
||||
print(f"元数据: {doc.metadata}")
|
||||
print("-" * 30)
|
||||
|
||||
# 获取统计信息
|
||||
stats = processor.get_collection_stats()
|
||||
print("\n集合统计信息:")
|
||||
print("=" * 50)
|
||||
for key, value in stats.items():
|
||||
print(f"{key}: {value}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
```
|
||||
|
||||
### 2. python 参考 requirements.txt
|
||||
|
||||
```
|
||||
langchain>=1.0.2
|
||||
langchain-community>=0.4
|
||||
unstructured[all-docs]
|
||||
openai>=1.14.3
|
||||
|
||||
# Milvus向量数据库
|
||||
pymilvus>=2.6.2
|
||||
langchain-milvus>=0.2.2
|
||||
```
|
||||
|
||||
### 3. Higress RAG mcp server config 配置
|
||||
|
||||
```yaml
|
||||
rag:
|
||||
splitter:
|
||||
provider: "nosplitter"
|
||||
chunk_size: 500
|
||||
chunk_overlap: 50
|
||||
threshold: 0.5
|
||||
top_k: 10
|
||||
|
||||
embedding:
|
||||
provider: "openai"
|
||||
base_url: "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||
api_key: "sk-xxx"
|
||||
model: "text-embedding-v4"
|
||||
dimensions: 1024
|
||||
|
||||
vector_db:
|
||||
provider: "milvus"
|
||||
host: "localhost"
|
||||
port: 19530
|
||||
database: "default"
|
||||
collection: "langchain_rag"
|
||||
mapping:
|
||||
# 字段映射配置:当标准字段名与 Milvus Collection 中实际字段名不一致时,需要通过 mapping 进行映射
|
||||
# standard_name: 系统内部使用的标准字段名(如 id, content, vector, metadata, created_at)
|
||||
# raw_name: milvus collection 中的实际字段名
|
||||
fields:
|
||||
- standard_name: "id"
|
||||
raw_name: "pk"
|
||||
properties:
|
||||
max_length: 256
|
||||
auto_id: false
|
||||
- standard_name: "content"
|
||||
raw_name: "text"
|
||||
properties:
|
||||
max_length: 8192
|
||||
- standard_name: "vector"
|
||||
raw_name: "vector"
|
||||
properties: {}
|
||||
- standard_name: "metadata"
|
||||
raw_name: "metadata"
|
||||
properties: {}
|
||||
index:
|
||||
index_type: "HNSW"
|
||||
params:
|
||||
M: 8
|
||||
ef_construction: 64
|
||||
search:
|
||||
metric_type: "IP"
|
||||
params:
|
||||
ef: 32
|
||||
```
|
||||
|
||||
### 4. 关于 langchain-milvus 对 Document metadata 处理
|
||||
|
||||
在使用 langchain-milvus 进行文档处理时,有两种处理 metadata 的方法:
|
||||
|
||||
#### 方法一:JSON 字符串存储(推荐)
|
||||
- **特点**:metadata 会被转换为 JSON 字符串存储在 Milvus 中,查询时会将 JSON 字符串转换为 Python 字典
|
||||
- **优势**:可以动态添加字段
|
||||
- **支持**:Higress RAG 支持读写操作
|
||||
|
||||
**配置步骤**:
|
||||
1. 初始化 Milvus 时,需要指定 `metadata_field` 参数为实际的字段名称(这里为 "metadata")
|
||||
2. 在 mapping 配置中添加 metadata 字段
|
||||
|
||||
**Python 代码示例**:
|
||||
```python
|
||||
Milvus(
|
||||
...
|
||||
metadata_field="metadata" # 自定义元数据字段名
|
||||
)
|
||||
```
|
||||
|
||||
**YAML 配置示例**:
|
||||
```yaml
|
||||
mapping:
|
||||
fields:
|
||||
- standard_name: "metadata"
|
||||
raw_name: "metadata"
|
||||
properties: {}
|
||||
```
|
||||
|
||||
#### 方法二:字段展开存储
|
||||
- **特点**:metadata 中的字段会直接展开,metadata 里的 key 会作为字段名存储在 Milvus 中
|
||||
- **限制**:不可以动态添加字段
|
||||
- **支持**:Higress RAG 只支持读操作
|
||||
|
||||
**配置步骤**:
|
||||
1. 初始化 Milvus 时,不需要指定 `metadata_field` 参数
|
||||
2. 在 mapping 配置中移除 metadata 字段
|
||||
|
||||
**推荐使用方法一**,因为它提供了更好的灵活性和完整的读写支持。
|
||||
|
||||
|
||||
### 5. Higress RAG MCP 插件和 CherryStudio 集成
|
||||
|
||||
Higress RAG MCP 插件和 CherryStudio 集成,实现基于 RAG 的智能问答功能。
|
||||
|
||||
**配置步骤**:
|
||||
|
||||
1. 在 CherryStudio 中配置 Higress RAG MCP 插件的 endpoint: `http://<higress-gateway>:<port>/mcp-servers/rag/sse`, 如下图:
|
||||
|
||||

|
||||
|
||||
2. 查看 CherryStudio 中配置 Higress RAG MCP 插件的 Tools 列表, 如下图:
|
||||
|
||||

|
||||
|
||||
**对话**:
|
||||
|
||||
在 CherryStudio 对话中, 添加 Higress RAG MCP 插件。然后在对话中就可以调用 Higress RAG MCP 插件的提供工具方法。如下图:
|
||||
|
||||

|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 287 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 146 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user