mirror of
https://github.com/alibaba/higress.git
synced 2026-02-25 21:21:01 +08:00
Compare commits
63 Commits
feat/claud
...
v2.1.11
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
516b016584 | ||
|
|
69f481d25e | ||
|
|
87357ae9ac | ||
|
|
82b6830af3 | ||
|
|
3b0f0bb31f | ||
|
|
bae535c980 | ||
|
|
caf00bfeac | ||
|
|
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 |
@@ -1,138 +0,0 @@
|
||||
# Agent Session Monitor - Quick Start
|
||||
|
||||
实时Agent对话观测程序,用于监控Higress访问日志,追踪多轮对话的token开销和模型使用情况。
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 运行Demo
|
||||
|
||||
```bash
|
||||
cd example
|
||||
bash demo.sh
|
||||
```
|
||||
|
||||
这将:
|
||||
- 解析示例日志文件
|
||||
- 列出所有session
|
||||
- 显示session详细信息(包括完整的messages、question、answer、reasoning、tool_calls)
|
||||
- 按模型和日期统计token开销
|
||||
- 导出FinOps报表
|
||||
|
||||
### 2. 启动Web界面(推荐)
|
||||
|
||||
```bash
|
||||
# 先解析日志生成session数据
|
||||
python3 main.py --log-path /var/log/higress/access.log --output-dir ./sessions
|
||||
|
||||
# 启动Web服务器
|
||||
python3 scripts/webserver.py --data-dir ./sessions --port 8888
|
||||
|
||||
# 浏览器访问
|
||||
open http://localhost:8888
|
||||
```
|
||||
|
||||
Web界面功能:
|
||||
- 📊 总览所有session,按模型分组统计
|
||||
- 🔍 点击session ID下钻查看完整对话
|
||||
- 💬 查看每轮的messages、question、answer、reasoning、tool_calls
|
||||
- 💰 实时计算token开销和成本
|
||||
- 🔄 每30秒自动刷新
|
||||
|
||||
### 3. 在Clawdbot对话中使用
|
||||
|
||||
当用户询问当前会话token消耗时,生成观测链接:
|
||||
|
||||
```
|
||||
你的当前会话ID: agent:main:discord:channel:1465367993012981988
|
||||
|
||||
查看详情:http://localhost:8888/session?id=agent:main:discord:channel:1465367993012981988
|
||||
|
||||
点击可以看到:
|
||||
✅ 完整对话历史(每轮messages)
|
||||
✅ Token消耗明细
|
||||
✅ 工具调用记录
|
||||
✅ 成本统计
|
||||
```
|
||||
|
||||
### 4. 使用CLI查询(可选)
|
||||
|
||||
```bash
|
||||
# 查看session详细信息
|
||||
python3 scripts/cli.py show <session-id>
|
||||
|
||||
# 列出所有session
|
||||
python3 scripts/cli.py list
|
||||
|
||||
# 按模型统计
|
||||
python3 scripts/cli.py stats-model
|
||||
|
||||
# 导出报表
|
||||
python3 scripts/cli.py export finops-report.json
|
||||
```
|
||||
|
||||
## 核心功能
|
||||
|
||||
✅ **完整对话追踪**:记录每轮对话的完整messages、question、answer、reasoning、tool_calls
|
||||
✅ **Token开销统计**:区分input/output/reasoning/cached token,实时计算成本
|
||||
✅ **Session聚合**:按session_id关联多轮对话
|
||||
✅ **Web可视化界面**:浏览器访问,总览+下钻查看session详情
|
||||
✅ **实时URL生成**:Clawdbot可根据当前会话ID生成观测链接
|
||||
✅ **FinOps报表**:导出JSON/CSV格式的成本分析报告
|
||||
|
||||
## 日志格式要求
|
||||
|
||||
Higress访问日志需要包含ai_log字段(JSON格式),示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"__file_offset__": "1000",
|
||||
"timestamp": "2026-02-01T09:30:15Z",
|
||||
"ai_log": "{\"session_id\":\"sess_abc\",\"messages\":[...],\"question\":\"...\",\"answer\":\"...\",\"input_token\":250,\"output_token\":160,\"model\":\"Qwen3-rerank\"}"
|
||||
}
|
||||
```
|
||||
|
||||
ai_log字段支持的属性:
|
||||
- `session_id`: 会话标识(必需)
|
||||
- `messages`: 完整对话历史
|
||||
- `question`: 当前轮次问题
|
||||
- `answer`: AI回答
|
||||
- `reasoning`: 思考过程(DeepSeek等模型)
|
||||
- `tool_calls`: 工具调用列表
|
||||
- `input_token`: 输入token数
|
||||
- `output_token`: 输出token数
|
||||
- `model`: 模型名称
|
||||
- `response_type`: 响应类型
|
||||
|
||||
## 输出目录结构
|
||||
|
||||
```
|
||||
sessions/
|
||||
├── agent:main:discord:1465367993012981988.json
|
||||
└── agent:test:discord:9999999999999999999.json
|
||||
```
|
||||
|
||||
每个session文件包含:
|
||||
- 基本信息(创建时间、更新时间、模型)
|
||||
- Token统计(总输入、总输出、总reasoning、总cached)
|
||||
- 对话轮次列表(每轮的完整messages、question、answer、reasoning、tool_calls)
|
||||
|
||||
## 常见问题
|
||||
|
||||
**Q: 如何在Higress中配置session_id header?**
|
||||
A: 在ai-statistics插件中配置`session_id_header`,或使用默认header(x-openclaw-session-key、x-clawdbot-session-key等)。详见PR #3420。
|
||||
|
||||
**Q: 支持哪些模型的pricing?**
|
||||
A: 目前支持Qwen、DeepSeek、GPT-4、Claude等主流模型。可以在main.py的TOKEN_PRICING字典中添加新模型。
|
||||
|
||||
**Q: 如何实时监控日志文件变化?**
|
||||
A: 直接运行main.py即可,程序使用定时轮询机制(每秒自动检查一次),无需安装额外依赖。
|
||||
|
||||
**Q: CLI查询速度慢?**
|
||||
A: 大量session时,可以使用`--limit`限制结果数量,或按条件过滤(如`--sort-by cost`只查看成本最高的session)。
|
||||
|
||||
## 下一步
|
||||
|
||||
- 集成到Higress FinOps Dashboard
|
||||
- 支持更多模型的pricing
|
||||
- 添加趋势预测和异常检测
|
||||
- 支持多数据源聚合分析
|
||||
@@ -1,71 +0,0 @@
|
||||
# Agent Session Monitor
|
||||
|
||||
Real-time agent conversation monitoring for Clawdbot, designed to monitor Higress access logs and track token usage across multi-turn conversations.
|
||||
|
||||
## Features
|
||||
|
||||
- 🔍 **Complete Conversation Tracking**: Records messages, question, answer, reasoning, tool_calls for each turn
|
||||
- 💰 **Token Usage Statistics**: Distinguishes input/output/reasoning/cached tokens, calculates costs in real-time
|
||||
- 🌐 **Web Visualization**: Browser-based UI with overview and drill-down into session details
|
||||
- 🔗 **Real-time URL Generation**: Clawdbot can generate observation links based on current session ID
|
||||
- 🔄 **Log Rotation Support**: Automatically handles rotated log files (access.log, access.log.1, etc.)
|
||||
- 📊 **FinOps Reporting**: Export usage data in JSON/CSV formats
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Run Demo
|
||||
|
||||
```bash
|
||||
cd example
|
||||
bash demo.sh
|
||||
```
|
||||
|
||||
### 2. Start Web UI
|
||||
|
||||
```bash
|
||||
# Parse logs
|
||||
python3 main.py --log-path /var/log/higress/access.log --output-dir ./sessions
|
||||
|
||||
# Start web server
|
||||
python3 scripts/webserver.py --data-dir ./sessions --port 8888
|
||||
|
||||
# Access in browser
|
||||
open http://localhost:8888
|
||||
```
|
||||
|
||||
### 3. Use in Clawdbot
|
||||
|
||||
When users ask "How many tokens did this conversation use?", you can respond with:
|
||||
|
||||
```
|
||||
Your current session statistics:
|
||||
- Session ID: agent:main:discord:channel:1465367993012981988
|
||||
- View details: http://localhost:8888/session?id=agent:main:discord:channel:1465367993012981988
|
||||
|
||||
Click to see:
|
||||
✅ Complete conversation history
|
||||
✅ Token usage breakdown per turn
|
||||
✅ Tool call records
|
||||
✅ Cost statistics
|
||||
```
|
||||
|
||||
## Files
|
||||
|
||||
- `main.py`: Background monitor, parses Higress access logs
|
||||
- `scripts/webserver.py`: Web server, provides browser-based UI
|
||||
- `scripts/cli.py`: Command-line tools for queries and exports
|
||||
- `example/`: Demo examples and test data
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Python 3.8+
|
||||
- No external dependencies (uses only standard library)
|
||||
|
||||
## Documentation
|
||||
|
||||
- `SKILL.md`: Main skill documentation
|
||||
- `QUICKSTART.md`: Quick start guide
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -1,376 +0,0 @@
|
||||
---
|
||||
name: agent-session-monitor
|
||||
description: Real-time agent conversation monitoring - monitors Higress access logs, aggregates conversations by session, tracks token usage. Supports web interface for viewing complete conversation history and costs. Use when users ask about current session token consumption, conversation history, or cost statistics.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Real-time monitoring of Higress access logs, extracting ai_log JSON, grouping multi-turn conversations by session_id, and calculating token costs with visualization.
|
||||
|
||||
### Core Features
|
||||
|
||||
- **Real-time Log Monitoring**: Monitors Higress access log files, parses new ai_log entries in real-time
|
||||
- **Log Rotation Support**: Full logrotate support, automatically tracks access.log.1~5 etc.
|
||||
- **Incremental Parsing**: Inode-based tracking, processes only new content, no duplicates
|
||||
- **Session Grouping**: Associates multi-turn conversations by session_id (each turn is a separate request)
|
||||
- **Complete Conversation Tracking**: Records messages, question, answer, reasoning, tool_calls for each turn
|
||||
- **Token Usage Tracking**: Distinguishes input/output/reasoning/cached tokens
|
||||
- **Web Visualization**: Browser-based UI with overview and session drill-down
|
||||
- **Real-time URL Generation**: Clawdbot can generate observation links based on current session ID
|
||||
- **Background Processing**: Independent process, continuously parses access logs
|
||||
- **State Persistence**: Maintains parsing progress and session data across runs
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. Background Monitoring (Continuous)
|
||||
|
||||
```bash
|
||||
# Parse Higress access logs (with log rotation support)
|
||||
python3 main.py --log-path /var/log/proxy/access.log --output-dir ./sessions
|
||||
|
||||
# Filter by session key
|
||||
python3 main.py --log-path /var/log/proxy/access.log --session-key <session-id>
|
||||
|
||||
# Scheduled task (incremental parsing every minute)
|
||||
* * * * * python3 /path/to/main.py --log-path /var/log/proxy/access.log --output-dir /var/lib/sessions
|
||||
```
|
||||
|
||||
### 2. Start Web UI (Recommended)
|
||||
|
||||
```bash
|
||||
# Start web server
|
||||
python3 scripts/webserver.py --data-dir ./sessions --port 8888
|
||||
|
||||
# Access in browser
|
||||
open http://localhost:8888
|
||||
```
|
||||
|
||||
Web UI features:
|
||||
- 📊 Overview: View all session statistics and group by model
|
||||
- 🔍 Session Details: Click session ID to drill down into complete conversation history
|
||||
- 💬 Conversation Log: Display messages, question, answer, reasoning, tool_calls for each turn
|
||||
- 💰 Cost Statistics: Real-time token usage and cost calculation
|
||||
- 🔄 Auto Refresh: Updates every 30 seconds
|
||||
|
||||
### 3. Use in Clawdbot Conversations
|
||||
|
||||
When users ask about current session token consumption or conversation history:
|
||||
|
||||
1. Get current session_id (from runtime or context)
|
||||
2. Generate web UI URL and return to user
|
||||
|
||||
Example response:
|
||||
|
||||
```
|
||||
Your current session statistics:
|
||||
- Session ID: agent:main:discord:channel:1465367993012981988
|
||||
- View details: http://localhost:8888/session?id=agent:main:discord:channel:1465367993012981988
|
||||
|
||||
Click the link to see:
|
||||
✅ Complete conversation history
|
||||
✅ Token usage breakdown per turn
|
||||
✅ Tool call records
|
||||
✅ Cost statistics
|
||||
```
|
||||
|
||||
### 4. CLI Queries (Optional)
|
||||
|
||||
```bash
|
||||
# View specific session details
|
||||
python3 scripts/cli.py show <session-id>
|
||||
|
||||
# List all sessions
|
||||
python3 scripts/cli.py list --sort-by cost --limit 10
|
||||
|
||||
# Statistics by model
|
||||
python3 scripts/cli.py stats-model
|
||||
|
||||
# Statistics by date (last 7 days)
|
||||
python3 scripts/cli.py stats-date --days 7
|
||||
|
||||
# Export reports
|
||||
python3 scripts/cli.py export finops-report.json
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### main.py (Background Monitor)
|
||||
|
||||
| Parameter | Description | Required | Default |
|
||||
|-----------|-------------|----------|---------|
|
||||
| `--log-path` | Higress access log file path | Yes | /var/log/higress/access.log |
|
||||
| `--output-dir` | Session data storage directory | No | ./sessions |
|
||||
| `--session-key` | Monitor only specified session key | No | Monitor all sessions |
|
||||
| `--state-file` | State file path (records read offsets) | No | <output-dir>/.state.json |
|
||||
| `--refresh-interval` | Log refresh interval (seconds) | No | 1 |
|
||||
|
||||
### webserver.py (Web UI)
|
||||
|
||||
| Parameter | Description | Required | Default |
|
||||
|-----------|-------------|----------|---------|
|
||||
| `--data-dir` | Session data directory | No | ./sessions |
|
||||
| `--port` | HTTP server port | No | 8888 |
|
||||
| `--host` | HTTP server address | No | 0.0.0.0 |
|
||||
|
||||
## Output Examples
|
||||
|
||||
### 1. Real-time Monitor
|
||||
|
||||
```
|
||||
🔍 Session Monitor - Active
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
📊 Active Sessions: 3
|
||||
|
||||
┌──────────────────────────┬─────────┬──────────┬───────────┐
|
||||
│ Session ID │ Msgs │ Input │ Output │
|
||||
├──────────────────────────┼─────────┼──────────┼───────────┤
|
||||
│ sess_abc123 │ 5 │ 1,250 │ 800 │
|
||||
│ sess_xyz789 │ 3 │ 890 │ 650 │
|
||||
│ sess_def456 │ 8 │ 2,100 │ 1,200 │
|
||||
└──────────────────────────┴─────────┴──────────┴───────────┘
|
||||
|
||||
📈 Token Statistics
|
||||
Total Input: 4240 tokens
|
||||
Total Output: 2650 tokens
|
||||
Total Cached: 0 tokens
|
||||
Total Cost: $0.00127
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
```
|
||||
|
||||
### 2. CLI Session Details
|
||||
|
||||
```bash
|
||||
$ python3 scripts/cli.py show agent:main:discord:channel:1465367993012981988
|
||||
|
||||
======================================================================
|
||||
📊 Session Detail: agent:main:discord:channel:1465367993012981988
|
||||
======================================================================
|
||||
|
||||
🕐 Created: 2026-02-01T09:30:00+08:00
|
||||
🕑 Updated: 2026-02-01T10:35:12+08:00
|
||||
🤖 Model: Qwen3-rerank
|
||||
💬 Messages: 5
|
||||
|
||||
📈 Token Statistics:
|
||||
Input: 1,250 tokens
|
||||
Output: 800 tokens
|
||||
Reasoning: 150 tokens
|
||||
Total: 2,200 tokens
|
||||
|
||||
💰 Estimated Cost: $0.00126000 USD
|
||||
|
||||
📝 Conversation Rounds (5):
|
||||
──────────────────────────────────────────────────────────────────────
|
||||
|
||||
Round 1 @ 2026-02-01T09:30:15+08:00
|
||||
Tokens: 250 in → 160 out
|
||||
🔧 Tool calls: Yes
|
||||
Messages (2):
|
||||
[user] Check Beijing weather
|
||||
❓ Question: Check Beijing weather
|
||||
✅ Answer: Checking Beijing weather for you...
|
||||
🧠 Reasoning: User wants to know Beijing weather, I need to call weather API.
|
||||
🛠️ Tool Calls:
|
||||
- get_weather({"location":"Beijing"})
|
||||
```
|
||||
|
||||
### 3. Statistics by Model
|
||||
|
||||
```bash
|
||||
$ python3 scripts/cli.py stats-model
|
||||
|
||||
================================================================================
|
||||
📊 Statistics by Model
|
||||
================================================================================
|
||||
|
||||
Model Sessions Input Output Cost (USD)
|
||||
────────────────────────────────────────────────────────────────────────────
|
||||
Qwen3-rerank 12 15,230 9,840 $ 0.016800
|
||||
DeepSeek-R1 5 8,450 6,200 $ 0.010600
|
||||
Qwen-Max 3 4,200 3,100 $ 0.008300
|
||||
GPT-4 2 2,100 1,800 $ 0.017100
|
||||
────────────────────────────────────────────────────────────────────────────
|
||||
TOTAL 22 29,980 20,940 $ 0.052800
|
||||
|
||||
================================================================================
|
||||
```
|
||||
|
||||
### 4. Statistics by Date
|
||||
|
||||
```bash
|
||||
$ python3 scripts/cli.py stats-date --days 7
|
||||
|
||||
================================================================================
|
||||
📊 Statistics by Date (Last 7 days)
|
||||
================================================================================
|
||||
|
||||
Date Sessions Input Output Cost (USD) Models
|
||||
────────────────────────────────────────────────────────────────────────────
|
||||
2026-01-26 3 2,100 1,450 $ 0.0042 Qwen3-rerank
|
||||
2026-01-27 5 4,850 3,200 $ 0.0096 Qwen3-rerank, GPT-4
|
||||
2026-01-28 4 3,600 2,800 $ 0.0078 DeepSeek-R1, Qwen
|
||||
────────────────────────────────────────────────────────────────────────────
|
||||
TOTAL 22 29,980 20,940 $ 0.0528
|
||||
|
||||
================================================================================
|
||||
```
|
||||
|
||||
### 5. Web UI (Recommended)
|
||||
|
||||
Access `http://localhost:8888` to see:
|
||||
|
||||
**Home Page:**
|
||||
- 📊 Total sessions, token consumption, cost cards
|
||||
- 📋 Recent sessions list (clickable for details)
|
||||
- 📈 Statistics by model table
|
||||
|
||||
**Session Detail Page:**
|
||||
- 💬 Complete conversation log (messages, question, answer, reasoning, tool_calls per turn)
|
||||
- 🔧 Tool call history
|
||||
- 💰 Token usage breakdown and costs
|
||||
|
||||
**Features:**
|
||||
- 🔄 Auto-refresh every 30 seconds
|
||||
- 📱 Responsive design, mobile-friendly
|
||||
- 🎨 Clean UI, easy to read
|
||||
|
||||
## Session Data Structure
|
||||
|
||||
Each session is stored as an independent JSON file with complete conversation history and token statistics:
|
||||
|
||||
```json
|
||||
{
|
||||
"session_id": "agent:main:discord:channel:1465367993012981988",
|
||||
"created_at": "2026-02-01T10:30:00Z",
|
||||
"updated_at": "2026-02-01T10:35:12Z",
|
||||
"messages_count": 5,
|
||||
"total_input_tokens": 1250,
|
||||
"total_output_tokens": 800,
|
||||
"total_reasoning_tokens": 150,
|
||||
"total_cached_tokens": 0,
|
||||
"model": "Qwen3-rerank",
|
||||
"rounds": [
|
||||
{
|
||||
"round": 1,
|
||||
"timestamp": "2026-02-01T10:30:15Z",
|
||||
"input_tokens": 250,
|
||||
"output_tokens": 160,
|
||||
"reasoning_tokens": 0,
|
||||
"cached_tokens": 0,
|
||||
"model": "Qwen3-rerank",
|
||||
"has_tool_calls": true,
|
||||
"response_type": "normal",
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a helpful assistant..."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Check Beijing weather"
|
||||
}
|
||||
],
|
||||
"question": "Check Beijing weather",
|
||||
"answer": "Checking Beijing weather for you...",
|
||||
"reasoning": "User wants to know Beijing weather, need to call weather API.",
|
||||
"tool_calls": [
|
||||
{
|
||||
"index": 0,
|
||||
"id": "call_abc123",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"arguments": "{\"location\":\"Beijing\"}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"input_token_details": {"cached_tokens": 0},
|
||||
"output_token_details": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Field Descriptions
|
||||
|
||||
**Session Level:**
|
||||
- `session_id`: Unique session identifier (from ai_log's session_id field)
|
||||
- `created_at`: Session creation time
|
||||
- `updated_at`: Last update time
|
||||
- `messages_count`: Number of conversation turns
|
||||
- `total_input_tokens`: Cumulative input tokens
|
||||
- `total_output_tokens`: Cumulative output tokens
|
||||
- `total_reasoning_tokens`: Cumulative reasoning tokens (DeepSeek, o1, etc.)
|
||||
- `total_cached_tokens`: Cumulative cached tokens (prompt caching)
|
||||
- `model`: Current model in use
|
||||
|
||||
**Round Level (rounds):**
|
||||
- `round`: Turn number
|
||||
- `timestamp`: Current turn timestamp
|
||||
- `input_tokens`: Input tokens for this turn
|
||||
- `output_tokens`: Output tokens for this turn
|
||||
- `reasoning_tokens`: Reasoning tokens (o1, etc.)
|
||||
- `cached_tokens`: Cached tokens (prompt caching)
|
||||
- `model`: Model used for this turn
|
||||
- `has_tool_calls`: Whether includes tool calls
|
||||
- `response_type`: Response type (normal/error, etc.)
|
||||
- `messages`: Complete conversation history (OpenAI messages format)
|
||||
- `question`: User's question for this turn (last user message)
|
||||
- `answer`: AI's answer for this turn
|
||||
- `reasoning`: AI's thinking process (if model supports)
|
||||
- `tool_calls`: Tool call list (if any)
|
||||
- `input_token_details`: Complete input token details (JSON)
|
||||
- `output_token_details`: Complete output token details (JSON)
|
||||
|
||||
## Log Format Requirements
|
||||
|
||||
Higress access logs must include ai_log field (JSON format). Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"__file_offset__": "1000",
|
||||
"timestamp": "2026-02-01T09:30:15Z",
|
||||
"ai_log": "{\"session_id\":\"sess_abc\",\"messages\":[...],\"question\":\"...\",\"answer\":\"...\",\"input_token\":250,\"output_token\":160,\"model\":\"Qwen3-rerank\"}"
|
||||
}
|
||||
```
|
||||
|
||||
Supported ai_log attributes:
|
||||
- `session_id`: Session identifier (required)
|
||||
- `messages`: Complete conversation history
|
||||
- `question`: Question for current turn
|
||||
- `answer`: AI answer
|
||||
- `reasoning`: Thinking process (DeepSeek, o1, etc.)
|
||||
- `reasoning_tokens`: Reasoning token count (from PR #3424)
|
||||
- `cached_tokens`: Cached token count (from PR #3424)
|
||||
- `tool_calls`: Tool call list
|
||||
- `input_token`: Input token count
|
||||
- `output_token`: Output token count
|
||||
- `input_token_details`: Complete input token details (JSON)
|
||||
- `output_token_details`: Complete output token details (JSON)
|
||||
- `model`: Model name
|
||||
- `response_type`: Response type
|
||||
|
||||
## Implementation
|
||||
|
||||
### Technology Stack
|
||||
|
||||
- **Log Parsing**: Direct JSON parsing, no regex needed
|
||||
- **File Monitoring**: Polling-based (no watchdog dependency)
|
||||
- **Session Management**: In-memory + disk hybrid storage
|
||||
- **Token Calculation**: Model-specific pricing for GPT-4, Qwen, Claude, o1, etc.
|
||||
|
||||
### Privacy and Security
|
||||
|
||||
- ✅ Does not record conversation content in logs, only token statistics
|
||||
- ✅ Session data stored locally, not uploaded to external services
|
||||
- ✅ Supports log file path allowlist
|
||||
- ✅ Session key access control
|
||||
|
||||
### Performance Optimization
|
||||
|
||||
- Incremental log parsing, avoids full scans
|
||||
- In-memory session data with periodic persistence
|
||||
- Optimized log file reading (offset tracking)
|
||||
- Inode-based file identification (handles rotation efficiently)
|
||||
@@ -1,101 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
演示如何在Clawdbot中生成Session观测URL
|
||||
"""
|
||||
|
||||
from urllib.parse import quote
|
||||
|
||||
def generate_session_url(session_id: str, base_url: str = "http://localhost:8888") -> dict:
|
||||
"""
|
||||
生成session观测URL
|
||||
|
||||
Args:
|
||||
session_id: 当前会话的session ID
|
||||
base_url: Web服务器基础URL
|
||||
|
||||
Returns:
|
||||
包含各种URL的字典
|
||||
"""
|
||||
# URL编码session_id(处理特殊字符)
|
||||
encoded_id = quote(session_id, safe='')
|
||||
|
||||
return {
|
||||
"session_detail": f"{base_url}/session?id={encoded_id}",
|
||||
"api_session": f"{base_url}/api/session?id={encoded_id}",
|
||||
"index": f"{base_url}/",
|
||||
"api_sessions": f"{base_url}/api/sessions",
|
||||
"api_stats": f"{base_url}/api/stats",
|
||||
}
|
||||
|
||||
|
||||
def format_response_message(session_id: str, base_url: str = "http://localhost:8888") -> str:
|
||||
"""
|
||||
生成给用户的回复消息
|
||||
|
||||
Args:
|
||||
session_id: 当前会话的session ID
|
||||
base_url: Web服务器基础URL
|
||||
|
||||
Returns:
|
||||
格式化的回复消息
|
||||
"""
|
||||
urls = generate_session_url(session_id, base_url)
|
||||
|
||||
return f"""你的当前会话信息:
|
||||
|
||||
📊 **Session ID**: `{session_id}`
|
||||
|
||||
🔗 **查看详情**: {urls['session_detail']}
|
||||
|
||||
点击链接可以看到:
|
||||
✅ 完整对话历史(每轮messages)
|
||||
✅ Token消耗明细(input/output/reasoning)
|
||||
✅ 工具调用记录
|
||||
✅ 实时成本统计
|
||||
|
||||
**更多链接:**
|
||||
- 📋 所有会话: {urls['index']}
|
||||
- 📥 API数据: {urls['api_session']}
|
||||
- 📊 总体统计: {urls['api_stats']}
|
||||
"""
|
||||
|
||||
|
||||
# 示例使用
|
||||
if __name__ == '__main__':
|
||||
# 模拟clawdbot的session ID
|
||||
demo_session_id = "agent:main:discord:channel:1465367993012981988"
|
||||
|
||||
print("=" * 70)
|
||||
print("🤖 Clawdbot Session Monitor Demo")
|
||||
print("=" * 70)
|
||||
print()
|
||||
|
||||
# 生成URL
|
||||
urls = generate_session_url(demo_session_id)
|
||||
|
||||
print("生成的URL:")
|
||||
print(f" Session详情: {urls['session_detail']}")
|
||||
print(f" API数据: {urls['api_session']}")
|
||||
print(f" 总览页面: {urls['index']}")
|
||||
print()
|
||||
|
||||
# 生成回复消息
|
||||
message = format_response_message(demo_session_id)
|
||||
|
||||
print("回复消息模板:")
|
||||
print("-" * 70)
|
||||
print(message)
|
||||
print("-" * 70)
|
||||
print()
|
||||
|
||||
print("✅ 在Clawdbot中,你可以直接返回上面的消息给用户")
|
||||
print()
|
||||
|
||||
# 测试特殊字符的session ID
|
||||
special_session_id = "agent:test:session/with?special&chars"
|
||||
special_urls = generate_session_url(special_session_id)
|
||||
|
||||
print("特殊字符处理示例:")
|
||||
print(f" 原始ID: {special_session_id}")
|
||||
print(f" URL: {special_urls['session_detail']}")
|
||||
print()
|
||||
@@ -1,101 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Agent Session Monitor - 演示脚本
|
||||
|
||||
set -e
|
||||
|
||||
SKILL_DIR="$(dirname "$(dirname "$(realpath "$0")")")"
|
||||
EXAMPLE_DIR="$SKILL_DIR/example"
|
||||
LOG_FILE="$EXAMPLE_DIR/test_access.log"
|
||||
OUTPUT_DIR="$EXAMPLE_DIR/sessions"
|
||||
|
||||
echo "========================================"
|
||||
echo "Agent Session Monitor - Demo"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
# 清理旧数据
|
||||
if [ -d "$OUTPUT_DIR" ]; then
|
||||
echo "🧹 Cleaning up old session data..."
|
||||
rm -rf "$OUTPUT_DIR"
|
||||
fi
|
||||
|
||||
echo "📂 Log file: $LOG_FILE"
|
||||
echo "📁 Output dir: $OUTPUT_DIR"
|
||||
echo ""
|
||||
|
||||
# 步骤1:解析日志文件(单次模式)
|
||||
echo "========================================"
|
||||
echo "步骤1:解析日志文件"
|
||||
echo "========================================"
|
||||
python3 "$SKILL_DIR/main.py" \
|
||||
--log-path "$LOG_FILE" \
|
||||
--output-dir "$OUTPUT_DIR"
|
||||
|
||||
echo ""
|
||||
echo "✅ 日志解析完成!Session数据已保存到: $OUTPUT_DIR"
|
||||
echo ""
|
||||
|
||||
# 步骤2:列出所有session
|
||||
echo "========================================"
|
||||
echo "步骤2:列出所有session"
|
||||
echo "========================================"
|
||||
python3 "$SKILL_DIR/scripts/cli.py" list \
|
||||
--data-dir "$OUTPUT_DIR" \
|
||||
--limit 10
|
||||
|
||||
# 步骤3:查看第一个session的详细信息
|
||||
echo "========================================"
|
||||
echo "步骤3:查看session详细信息"
|
||||
echo "========================================"
|
||||
FIRST_SESSION=$(ls -1 "$OUTPUT_DIR"/*.json | head -1 | xargs -I {} basename {} .json)
|
||||
python3 "$SKILL_DIR/scripts/cli.py" show "$FIRST_SESSION" \
|
||||
--data-dir "$OUTPUT_DIR"
|
||||
|
||||
# 步骤4:按模型统计
|
||||
echo "========================================"
|
||||
echo "步骤4:按模型统计token开销"
|
||||
echo "========================================"
|
||||
python3 "$SKILL_DIR/scripts/cli.py" stats-model \
|
||||
--data-dir "$OUTPUT_DIR"
|
||||
|
||||
# 步骤5:按日期统计
|
||||
echo "========================================"
|
||||
echo "步骤5:按日期统计token开销"
|
||||
echo "========================================"
|
||||
python3 "$SKILL_DIR/scripts/cli.py" stats-date \
|
||||
--data-dir "$OUTPUT_DIR" \
|
||||
--days 7
|
||||
|
||||
# 步骤6:导出FinOps报表
|
||||
echo "========================================"
|
||||
echo "步骤6:导出FinOps报表"
|
||||
echo "========================================"
|
||||
python3 "$SKILL_DIR/scripts/cli.py" export "$EXAMPLE_DIR/finops-report.json" \
|
||||
--data-dir "$OUTPUT_DIR" \
|
||||
--format json
|
||||
|
||||
echo ""
|
||||
echo "✅ 报表已导出到: $EXAMPLE_DIR/finops-report.json"
|
||||
echo ""
|
||||
|
||||
# 显示报表内容
|
||||
if [ -f "$EXAMPLE_DIR/finops-report.json" ]; then
|
||||
echo "📊 FinOps报表内容:"
|
||||
echo "========================================"
|
||||
cat "$EXAMPLE_DIR/finops-report.json" | python3 -m json.tool | head -50
|
||||
echo "..."
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo "✅ Demo完成!"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
echo "💡 提示:"
|
||||
echo " - Session数据保存在: $OUTPUT_DIR/"
|
||||
echo " - FinOps报表: $EXAMPLE_DIR/finops-report.json"
|
||||
echo " - 使用 'python3 scripts/cli.py --help' 查看更多命令"
|
||||
echo ""
|
||||
echo "🌐 启动Web界面查看:"
|
||||
echo " python3 $SKILL_DIR/scripts/webserver.py --data-dir $OUTPUT_DIR --port 8888"
|
||||
echo " 然后访问: http://localhost:8888"
|
||||
@@ -1,76 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Agent Session Monitor - Demo for PR #3424 token details
|
||||
|
||||
set -e
|
||||
|
||||
SKILL_DIR="$(dirname "$(dirname "$(realpath "$0")")")"
|
||||
EXAMPLE_DIR="$SKILL_DIR/example"
|
||||
LOG_FILE="$EXAMPLE_DIR/test_access_v2.log"
|
||||
OUTPUT_DIR="$EXAMPLE_DIR/sessions_v2"
|
||||
|
||||
echo "========================================"
|
||||
echo "Agent Session Monitor - Token Details Demo"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
# 清理旧数据
|
||||
if [ -d "$OUTPUT_DIR" ]; then
|
||||
echo "🧹 Cleaning up old session data..."
|
||||
rm -rf "$OUTPUT_DIR"
|
||||
fi
|
||||
|
||||
echo "📂 Log file: $LOG_FILE"
|
||||
echo "📁 Output dir: $OUTPUT_DIR"
|
||||
echo ""
|
||||
|
||||
# 步骤1:解析日志文件
|
||||
echo "========================================"
|
||||
echo "步骤1:解析日志文件(包含token details)"
|
||||
echo "========================================"
|
||||
python3 "$SKILL_DIR/main.py" \
|
||||
--log-path "$LOG_FILE" \
|
||||
--output-dir "$OUTPUT_DIR"
|
||||
|
||||
echo ""
|
||||
echo "✅ 日志解析完成!Session数据已保存到: $OUTPUT_DIR"
|
||||
echo ""
|
||||
|
||||
# 步骤2:查看使用prompt caching的session(gpt-4o)
|
||||
echo "========================================"
|
||||
echo "步骤2:查看GPT-4o session(包含cached tokens)"
|
||||
echo "========================================"
|
||||
python3 "$SKILL_DIR/scripts/cli.py" show "agent:main:discord:1465367993012981988" \
|
||||
--data-dir "$OUTPUT_DIR"
|
||||
|
||||
# 步骤3:查看使用reasoning的session(o1)
|
||||
echo "========================================"
|
||||
echo "步骤3:查看o1 session(包含reasoning tokens)"
|
||||
echo "========================================"
|
||||
python3 "$SKILL_DIR/scripts/cli.py" show "agent:main:discord:9999999999999999999" \
|
||||
--data-dir "$OUTPUT_DIR"
|
||||
|
||||
# 步骤4:按模型统计
|
||||
echo "========================================"
|
||||
echo "步骤4:按模型统计(包含新token类型)"
|
||||
echo "========================================"
|
||||
python3 "$SKILL_DIR/scripts/cli.py" stats-model \
|
||||
--data-dir "$OUTPUT_DIR"
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo "✅ Demo完成!"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
echo "💡 新功能说明:"
|
||||
echo " ✅ cached_tokens - 缓存命中的token数(prompt caching)"
|
||||
echo " ✅ reasoning_tokens - 推理token数(o1等模型)"
|
||||
echo " ✅ input_token_details - 完整输入token详情(JSON)"
|
||||
echo " ✅ output_token_details - 完整输出token详情(JSON)"
|
||||
echo ""
|
||||
echo "💰 成本计算已优化:"
|
||||
echo " - cached tokens通常比regular input便宜(50-90%折扣)"
|
||||
echo " - reasoning tokens单独计费(o1系列)"
|
||||
echo ""
|
||||
echo "🌐 启动Web界面查看:"
|
||||
echo " python3 $SKILL_DIR/scripts/webserver.py --data-dir $OUTPUT_DIR --port 8889"
|
||||
echo " 然后访问: http://localhost:8889"
|
||||
@@ -1,4 +0,0 @@
|
||||
{"__file_offset__":"1000","timestamp":"2026-02-01T09:30:15Z","ai_log":"{\"session_id\":\"agent:main:discord:1465367993012981988\",\"api\":\"Qwen3-rerank@higress\",\"api_type\":\"LLM\",\"chat_round\":1,\"consumer\":\"clawdbot\",\"input_token\":250,\"output_token\":160,\"model\":\"Qwen3-rerank\",\"response_type\":\"normal\",\"total_token\":410,\"messages\":[{\"role\":\"system\",\"content\":\"You are a helpful assistant.\"},{\"role\":\"user\",\"content\":\"查询北京天气\"}],\"question\":\"查询北京天气\",\"answer\":\"正在为您查询北京天气...\",\"reasoning\":\"用户想知道北京的天气,我需要调用天气查询工具。\",\"tool_calls\":[{\"index\":0,\"id\":\"call_abc123\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"{\\\"location\\\":\\\"Beijing\\\"}\"}}]}"}
|
||||
{"__file_offset__":"2000","timestamp":"2026-02-01T09:32:00Z","ai_log":"{\"session_id\":\"agent:main:discord:1465367993012981988\",\"api\":\"Qwen3-rerank@higress\",\"api_type\":\"LLM\",\"chat_round\":2,\"consumer\":\"clawdbot\",\"input_token\":320,\"output_token\":180,\"model\":\"Qwen3-rerank\",\"response_type\":\"normal\",\"total_token\":500,\"messages\":[{\"role\":\"tool\",\"content\":\"{\\\"temperature\\\": 15, \\\"weather\\\": \\\"晴\\\"}\"}],\"question\":\"\",\"answer\":\"北京今天天气晴朗,温度15°C。\",\"reasoning\":\"\",\"tool_calls\":[]}"}
|
||||
{"__file_offset__":"3000","timestamp":"2026-02-01T09:35:12Z","ai_log":"{\"session_id\":\"agent:main:discord:1465367993012981988\",\"api\":\"Qwen3-rerank@higress\",\"api_type\":\"LLM\",\"chat_round\":3,\"consumer\":\"clawdbot\",\"input_token\":380,\"output_token\":220,\"model\":\"Qwen3-rerank\",\"response_type\":\"normal\",\"total_token\":600,\"messages\":[{\"role\":\"user\",\"content\":\"谢谢!\"},{\"role\":\"assistant\",\"content\":\"不客气!如果还有其他问题,随时问我。\"}],\"question\":\"谢谢!\",\"answer\":\"不客气!如果还有其他问题,随时问我。\",\"reasoning\":\"\",\"tool_calls\":[]}"}
|
||||
{"__file_offset__":"4000","timestamp":"2026-02-01T10:00:00Z","ai_log":"{\"session_id\":\"agent:test:discord:9999999999999999999\",\"api\":\"DeepSeek-R1@higress\",\"api_type\":\"LLM\",\"chat_round\":1,\"consumer\":\"clawdbot\",\"input_token\":50,\"output_token\":30,\"model\":\"DeepSeek-R1\",\"response_type\":\"normal\",\"total_token\":80,\"messages\":[{\"role\":\"user\",\"content\":\"计算2+2\"}],\"question\":\"计算2+2\",\"answer\":\"4\",\"reasoning\":\"这是一个简单的加法运算,2加2等于4。\",\"tool_calls\":[]}"}
|
||||
@@ -1,4 +0,0 @@
|
||||
{"__file_offset__":"1000","timestamp":"2026-02-01T10:00:00Z","ai_log":"{\"session_id\":\"agent:main:discord:1465367993012981988\",\"api\":\"gpt-4o\",\"api_type\":\"LLM\",\"chat_round\":1,\"consumer\":\"clawdbot\",\"input_token\":150,\"output_token\":100,\"reasoning_tokens\":0,\"cached_tokens\":120,\"input_token_details\":\"{\\\"cached_tokens\\\":120}\",\"output_token_details\":\"{}\",\"model\":\"gpt-4o\",\"response_type\":\"normal\",\"total_token\":250,\"messages\":[{\"role\":\"system\",\"content\":\"You are a helpful assistant.\"},{\"role\":\"user\",\"content\":\"你好\"}],\"question\":\"你好\",\"answer\":\"你好!有什么我可以帮助你的吗?\",\"reasoning\":\"\",\"tool_calls\":[]}"}
|
||||
{"__file_offset__":"2000","timestamp":"2026-02-01T10:01:00Z","ai_log":"{\"session_id\":\"agent:main:discord:1465367993012981988\",\"api\":\"gpt-4o\",\"api_type\":\"LLM\",\"chat_round\":2,\"consumer\":\"clawdbot\",\"input_token\":200,\"output_token\":150,\"reasoning_tokens\":0,\"cached_tokens\":80,\"input_token_details\":\"{\\\"cached_tokens\\\":80}\",\"output_token_details\":\"{}\",\"model\":\"gpt-4o\",\"response_type\":\"normal\",\"total_token\":350,\"messages\":[{\"role\":\"user\",\"content\":\"介绍一下你的能力\"}],\"question\":\"介绍一下你的能力\",\"answer\":\"我可以帮助你回答问题、写作、编程等...\",\"reasoning\":\"\",\"tool_calls\":[]}"}
|
||||
{"__file_offset__":"3000","timestamp":"2026-02-01T10:02:00Z","ai_log":"{\"session_id\":\"agent:main:discord:9999999999999999999\",\"api\":\"o1\",\"api_type\":\"LLM\",\"chat_round\":1,\"consumer\":\"clawdbot\",\"input_token\":100,\"output_token\":80,\"reasoning_tokens\":500,\"cached_tokens\":0,\"input_token_details\":\"{}\",\"output_token_details\":\"{\\\"reasoning_tokens\\\":500}\",\"model\":\"o1\",\"response_type\":\"normal\",\"total_token\":580,\"messages\":[{\"role\":\"user\",\"content\":\"解释量子纠缠\"}],\"question\":\"解释量子纠缠\",\"answer\":\"量子纠缠是量子力学中的一种现象...\",\"reasoning\":\"这是一个复杂的物理概念,我需要仔细思考如何用简单的方式解释...\",\"tool_calls\":[]}"}
|
||||
{"__file_offset__":"4000","timestamp":"2026-02-01T10:03:00Z","ai_log":"{\"session_id\":\"agent:main:discord:1465367993012981988\",\"api\":\"gpt-4o\",\"api_type\":\"LLM\",\"chat_round\":3,\"consumer\":\"clawdbot\",\"input_token\":300,\"output_token\":200,\"reasoning_tokens\":0,\"cached_tokens\":200,\"input_token_details\":\"{\\\"cached_tokens\\\":200}\",\"output_token_details\":\"{}\",\"model\":\"gpt-4o\",\"response_type\":\"normal\",\"total_token\":500,\"messages\":[{\"role\":\"user\",\"content\":\"写一个Python函数计算斐波那契数列\"}],\"question\":\"写一个Python函数计算斐波那契数列\",\"answer\":\"```python\\ndef fibonacci(n):\\n if n <= 1:\\n return n\\n return fibonacci(n-1) + fibonacci(n-2)\\n```\",\"reasoning\":\"\",\"tool_calls\":[]}"}
|
||||
@@ -1,137 +0,0 @@
|
||||
#!/bin/bash
|
||||
# 测试日志轮转功能
|
||||
|
||||
set -e
|
||||
|
||||
SKILL_DIR="$(dirname "$(dirname "$(realpath "$0")")")"
|
||||
EXAMPLE_DIR="$SKILL_DIR/example"
|
||||
TEST_DIR="$EXAMPLE_DIR/rotation_test"
|
||||
LOG_FILE="$TEST_DIR/access.log"
|
||||
OUTPUT_DIR="$TEST_DIR/sessions"
|
||||
|
||||
echo "========================================"
|
||||
echo "Log Rotation Test"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
# 清理旧测试数据
|
||||
rm -rf "$TEST_DIR"
|
||||
mkdir -p "$TEST_DIR"
|
||||
|
||||
echo "📁 Test directory: $TEST_DIR"
|
||||
echo ""
|
||||
|
||||
# 模拟日志轮转场景
|
||||
echo "========================================"
|
||||
echo "步骤1:创建初始日志文件"
|
||||
echo "========================================"
|
||||
|
||||
# 创建第一批日志(10条)
|
||||
for i in {1..10}; do
|
||||
echo "{\"timestamp\":\"2026-02-01T10:0${i}:00Z\",\"ai_log\":\"{\\\"session_id\\\":\\\"session_001\\\",\\\"model\\\":\\\"gpt-4o\\\",\\\"input_token\\\":$((100+i)),\\\"output_token\\\":$((50+i)),\\\"cached_tokens\\\":$((30+i))}\"}" >> "$LOG_FILE"
|
||||
done
|
||||
|
||||
echo "✅ Created $LOG_FILE with 10 lines"
|
||||
echo ""
|
||||
|
||||
# 首次解析
|
||||
echo "========================================"
|
||||
echo "步骤2:首次解析(应该处理10条记录)"
|
||||
echo "========================================"
|
||||
python3 "$SKILL_DIR/main.py" \
|
||||
--log-path "$LOG_FILE" \
|
||||
--output-dir "$OUTPUT_DIR" \
|
||||
|
||||
|
||||
echo ""
|
||||
|
||||
# 检查session数据
|
||||
echo "Session数据:"
|
||||
cat "$OUTPUT_DIR/session_001.json" | python3 -c "import sys, json; d=json.load(sys.stdin); print(f\" Messages: {d['messages_count']}, Total Input: {d['total_input_tokens']}\")"
|
||||
echo ""
|
||||
|
||||
# 模拟日志轮转
|
||||
echo "========================================"
|
||||
echo "步骤3:模拟日志轮转"
|
||||
echo "========================================"
|
||||
mv "$LOG_FILE" "$LOG_FILE.1"
|
||||
echo "✅ Rotated: access.log -> access.log.1"
|
||||
echo ""
|
||||
|
||||
# 创建新的日志文件(5条新记录)
|
||||
for i in {11..15}; do
|
||||
echo "{\"timestamp\":\"2026-02-01T10:${i}:00Z\",\"ai_log\":\"{\\\"session_id\\\":\\\"session_001\\\",\\\"model\\\":\\\"gpt-4o\\\",\\\"input_token\\\":$((100+i)),\\\"output_token\\\":$((50+i)),\\\"cached_tokens\\\":$((30+i))}\"}" >> "$LOG_FILE"
|
||||
done
|
||||
|
||||
echo "✅ Created new $LOG_FILE with 5 lines"
|
||||
echo ""
|
||||
|
||||
# 再次解析(应该只处理新的5条)
|
||||
echo "========================================"
|
||||
echo "步骤4:再次解析(应该只处理新的5条)"
|
||||
echo "========================================"
|
||||
python3 "$SKILL_DIR/main.py" \
|
||||
--log-path "$LOG_FILE" \
|
||||
--output-dir "$OUTPUT_DIR" \
|
||||
|
||||
|
||||
echo ""
|
||||
|
||||
# 检查session数据
|
||||
echo "Session数据:"
|
||||
cat "$OUTPUT_DIR/session_001.json" | python3 -c "import sys, json; d=json.load(sys.stdin); print(f\" Messages: {d['messages_count']}, Total Input: {d['total_input_tokens']} (应该是15条记录)\")"
|
||||
echo ""
|
||||
|
||||
# 再次轮转
|
||||
echo "========================================"
|
||||
echo "步骤5:再次轮转"
|
||||
echo "========================================"
|
||||
mv "$LOG_FILE.1" "$LOG_FILE.2"
|
||||
mv "$LOG_FILE" "$LOG_FILE.1"
|
||||
echo "✅ Rotated: access.log -> access.log.1"
|
||||
echo "✅ Rotated: access.log.1 -> access.log.2"
|
||||
echo ""
|
||||
|
||||
# 创建新的日志文件(3条新记录)
|
||||
for i in {16..18}; do
|
||||
echo "{\"timestamp\":\"2026-02-01T10:${i}:00Z\",\"ai_log\":\"{\\\"session_id\\\":\\\"session_001\\\",\\\"model\\\":\\\"gpt-4o\\\",\\\"input_token\\\":$((100+i)),\\\"output_token\\\":$((50+i)),\\\"cached_tokens\\\":$((30+i))}\"}" >> "$LOG_FILE"
|
||||
done
|
||||
|
||||
echo "✅ Created new $LOG_FILE with 3 lines"
|
||||
echo ""
|
||||
|
||||
# 再次解析(应该只处理新的3条)
|
||||
echo "========================================"
|
||||
echo "步骤6:再次解析(应该只处理新的3条)"
|
||||
echo "========================================"
|
||||
python3 "$SKILL_DIR/main.py" \
|
||||
--log-path "$LOG_FILE" \
|
||||
--output-dir "$OUTPUT_DIR" \
|
||||
|
||||
|
||||
echo ""
|
||||
|
||||
# 检查session数据
|
||||
echo "Session数据:"
|
||||
cat "$OUTPUT_DIR/session_001.json" | python3 -c "import sys, json; d=json.load(sys.stdin); print(f\" Messages: {d['messages_count']}, Total Input: {d['total_input_tokens']} (应该是18条记录)\")"
|
||||
echo ""
|
||||
|
||||
# 检查状态文件
|
||||
echo "========================================"
|
||||
echo "步骤7:查看状态文件"
|
||||
echo "========================================"
|
||||
echo "状态文件内容:"
|
||||
cat "$OUTPUT_DIR/.state.json" | python3 -m json.tool | head -20
|
||||
echo ""
|
||||
|
||||
echo "========================================"
|
||||
echo "✅ 测试完成!"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
echo "💡 验证要点:"
|
||||
echo " 1. 首次解析处理了10条记录"
|
||||
echo " 2. 轮转后只处理新增的5条记录(总计15条)"
|
||||
echo " 3. 再次轮转后只处理新增的3条记录(总计18条)"
|
||||
echo " 4. 状态文件记录了每个文件的inode和offset"
|
||||
echo ""
|
||||
echo "📂 测试数据保存在: $TEST_DIR/"
|
||||
@@ -1,639 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Agent Session Monitor - 实时Agent对话观测程序
|
||||
监控Higress访问日志,按session聚合对话,追踪token开销
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
# 使用定时轮询机制,不依赖watchdog
|
||||
|
||||
# ============================================================================
|
||||
# 配置
|
||||
# ============================================================================
|
||||
|
||||
# Token定价(单位:美元/1M tokens)
|
||||
TOKEN_PRICING = {
|
||||
"Qwen": {
|
||||
"input": 0.0002, # $0.2/1M
|
||||
"output": 0.0006,
|
||||
"cached": 0.0001, # cached tokens通常是input的50%
|
||||
},
|
||||
"Qwen3-rerank": {
|
||||
"input": 0.0003,
|
||||
"output": 0.0012,
|
||||
"cached": 0.00015,
|
||||
},
|
||||
"Qwen-Max": {
|
||||
"input": 0.0005,
|
||||
"output": 0.002,
|
||||
"cached": 0.00025,
|
||||
},
|
||||
"GPT-4": {
|
||||
"input": 0.003,
|
||||
"output": 0.006,
|
||||
"cached": 0.0015,
|
||||
},
|
||||
"GPT-4o": {
|
||||
"input": 0.0025,
|
||||
"output": 0.01,
|
||||
"cached": 0.00125, # GPT-4o prompt caching: 50% discount
|
||||
},
|
||||
"GPT-4-32k": {
|
||||
"input": 0.01,
|
||||
"output": 0.03,
|
||||
"cached": 0.005,
|
||||
},
|
||||
"o1": {
|
||||
"input": 0.015,
|
||||
"output": 0.06,
|
||||
"cached": 0.0075,
|
||||
"reasoning": 0.06, # o1 reasoning tokens same as output
|
||||
},
|
||||
"o1-mini": {
|
||||
"input": 0.003,
|
||||
"output": 0.012,
|
||||
"cached": 0.0015,
|
||||
"reasoning": 0.012,
|
||||
},
|
||||
"Claude": {
|
||||
"input": 0.015,
|
||||
"output": 0.075,
|
||||
"cached": 0.0015, # Claude prompt caching: 90% discount
|
||||
},
|
||||
"DeepSeek-R1": {
|
||||
"input": 0.004,
|
||||
"output": 0.012,
|
||||
"reasoning": 0.002,
|
||||
"cached": 0.002,
|
||||
}
|
||||
}
|
||||
|
||||
DEFAULT_LOG_PATH = "/var/log/higress/access.log"
|
||||
DEFAULT_OUTPUT_DIR = "./sessions"
|
||||
|
||||
# ============================================================================
|
||||
# Session管理器
|
||||
# ============================================================================
|
||||
|
||||
class SessionManager:
|
||||
"""管理多个会话的token统计"""
|
||||
|
||||
def __init__(self, output_dir: str, load_existing: bool = True):
|
||||
self.output_dir = Path(output_dir)
|
||||
self.output_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.sessions: Dict[str, dict] = {}
|
||||
|
||||
# 加载已有的session数据
|
||||
if load_existing:
|
||||
self._load_existing_sessions()
|
||||
|
||||
def _load_existing_sessions(self):
|
||||
"""加载已有的session数据"""
|
||||
loaded_count = 0
|
||||
for session_file in self.output_dir.glob("*.json"):
|
||||
try:
|
||||
with open(session_file, 'r', encoding='utf-8') as f:
|
||||
session = json.load(f)
|
||||
session_id = session.get('session_id')
|
||||
if session_id:
|
||||
self.sessions[session_id] = session
|
||||
loaded_count += 1
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to load session {session_file}: {e}", file=sys.stderr)
|
||||
|
||||
if loaded_count > 0:
|
||||
print(f"📦 Loaded {loaded_count} existing session(s)")
|
||||
|
||||
def update_session(self, session_id: str, ai_log: dict) -> dict:
|
||||
"""更新或创建session"""
|
||||
if session_id not in self.sessions:
|
||||
self.sessions[session_id] = {
|
||||
"session_id": session_id,
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"updated_at": datetime.now().isoformat(),
|
||||
"messages_count": 0,
|
||||
"total_input_tokens": 0,
|
||||
"total_output_tokens": 0,
|
||||
"total_reasoning_tokens": 0,
|
||||
"total_cached_tokens": 0,
|
||||
"rounds": [],
|
||||
"model": ai_log.get("model", "unknown")
|
||||
}
|
||||
|
||||
session = self.sessions[session_id]
|
||||
|
||||
# 更新统计
|
||||
model = ai_log.get("model", "unknown")
|
||||
session["model"] = model
|
||||
session["updated_at"] = datetime.now().isoformat()
|
||||
|
||||
# Token统计
|
||||
session["total_input_tokens"] += ai_log.get("input_token", 0)
|
||||
session["total_output_tokens"] += ai_log.get("output_token", 0)
|
||||
|
||||
# 检查reasoning tokens(优先使用ai_log中的reasoning_tokens字段)
|
||||
reasoning_tokens = ai_log.get("reasoning_tokens", 0)
|
||||
if reasoning_tokens == 0 and "reasoning" in ai_log and ai_log["reasoning"]:
|
||||
# 如果没有reasoning_tokens字段,估算reasoning的token数(大致按字符数/4)
|
||||
reasoning_text = ai_log["reasoning"]
|
||||
reasoning_tokens = len(reasoning_text) // 4
|
||||
session["total_reasoning_tokens"] += reasoning_tokens
|
||||
|
||||
# 检查cached tokens(prompt caching)
|
||||
cached_tokens = ai_log.get("cached_tokens", 0)
|
||||
session["total_cached_tokens"] += cached_tokens
|
||||
|
||||
# 检查是否有tool_calls(工具调用)
|
||||
has_tool_calls = "tool_calls" in ai_log and ai_log["tool_calls"]
|
||||
|
||||
# 更新消息数
|
||||
session["messages_count"] += 1
|
||||
|
||||
# 解析token details(如果有)
|
||||
input_token_details = {}
|
||||
output_token_details = {}
|
||||
|
||||
if "input_token_details" in ai_log:
|
||||
try:
|
||||
# input_token_details可能是字符串或字典
|
||||
details = ai_log["input_token_details"]
|
||||
if isinstance(details, str):
|
||||
import json
|
||||
input_token_details = json.loads(details)
|
||||
else:
|
||||
input_token_details = details
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
pass
|
||||
|
||||
if "output_token_details" in ai_log:
|
||||
try:
|
||||
# output_token_details可能是字符串或字典
|
||||
details = ai_log["output_token_details"]
|
||||
if isinstance(details, str):
|
||||
import json
|
||||
output_token_details = json.loads(details)
|
||||
else:
|
||||
output_token_details = details
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
pass
|
||||
|
||||
# 添加轮次记录(包含完整的llm请求和响应信息)
|
||||
round_data = {
|
||||
"round": session["messages_count"],
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"input_tokens": ai_log.get("input_token", 0),
|
||||
"output_tokens": ai_log.get("output_token", 0),
|
||||
"reasoning_tokens": reasoning_tokens,
|
||||
"cached_tokens": cached_tokens,
|
||||
"model": model,
|
||||
"has_tool_calls": has_tool_calls,
|
||||
"response_type": ai_log.get("response_type", "normal"),
|
||||
# 完整的对话信息
|
||||
"messages": ai_log.get("messages", []),
|
||||
"question": ai_log.get("question", ""),
|
||||
"answer": ai_log.get("answer", ""),
|
||||
"reasoning": ai_log.get("reasoning", ""),
|
||||
"tool_calls": ai_log.get("tool_calls", []),
|
||||
# Token详情
|
||||
"input_token_details": input_token_details,
|
||||
"output_token_details": output_token_details,
|
||||
}
|
||||
session["rounds"].append(round_data)
|
||||
|
||||
# 保存到文件
|
||||
self._save_session(session)
|
||||
|
||||
return session
|
||||
|
||||
def _save_session(self, session: dict):
|
||||
"""保存session数据到文件"""
|
||||
session_file = self.output_dir / f"{session['session_id']}.json"
|
||||
with open(session_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(session, f, ensure_ascii=False, indent=2)
|
||||
|
||||
def get_all_sessions(self) -> List[dict]:
|
||||
"""获取所有session"""
|
||||
return list(self.sessions.values())
|
||||
|
||||
def get_session(self, session_id: str) -> Optional[dict]:
|
||||
"""获取指定session"""
|
||||
return self.sessions.get(session_id)
|
||||
|
||||
def get_summary(self) -> dict:
|
||||
"""获取总体统计"""
|
||||
total_input = sum(s["total_input_tokens"] for s in self.sessions.values())
|
||||
total_output = sum(s["total_output_tokens"] for s in self.sessions.values())
|
||||
total_reasoning = sum(s.get("total_reasoning_tokens", 0) for s in self.sessions.values())
|
||||
total_cached = sum(s.get("total_cached_tokens", 0) for s in self.sessions.values())
|
||||
|
||||
# 计算成本
|
||||
total_cost = 0
|
||||
for session in self.sessions.values():
|
||||
model = session.get("model", "unknown")
|
||||
input_tokens = session["total_input_tokens"]
|
||||
output_tokens = session["total_output_tokens"]
|
||||
reasoning_tokens = session.get("total_reasoning_tokens", 0)
|
||||
cached_tokens = session.get("total_cached_tokens", 0)
|
||||
|
||||
pricing = TOKEN_PRICING.get(model, TOKEN_PRICING.get("GPT-4", {}))
|
||||
|
||||
# 基础成本计算
|
||||
# 注意:cached_tokens已经包含在input_tokens中,需要分开计算
|
||||
regular_input_tokens = input_tokens - cached_tokens
|
||||
input_cost = regular_input_tokens * pricing.get("input", 0) / 1000000
|
||||
output_cost = output_tokens * pricing.get("output", 0) / 1000000
|
||||
|
||||
# reasoning成本
|
||||
reasoning_cost = 0
|
||||
if "reasoning" in pricing and reasoning_tokens > 0:
|
||||
reasoning_cost = reasoning_tokens * pricing["reasoning"] / 1000000
|
||||
|
||||
# cached成本(通常比input便宜)
|
||||
cached_cost = 0
|
||||
if "cached" in pricing and cached_tokens > 0:
|
||||
cached_cost = cached_tokens * pricing["cached"] / 1000000
|
||||
|
||||
total_cost += input_cost + output_cost + reasoning_cost + cached_cost
|
||||
|
||||
return {
|
||||
"total_sessions": len(self.sessions),
|
||||
"total_input_tokens": total_input,
|
||||
"total_output_tokens": total_output,
|
||||
"total_reasoning_tokens": total_reasoning,
|
||||
"total_cached_tokens": total_cached,
|
||||
"total_tokens": total_input + total_output + total_reasoning + total_cached,
|
||||
"total_cost_usd": round(total_cost, 4),
|
||||
"active_session_ids": list(self.sessions.keys())
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 日志解析器
|
||||
# ============================================================================
|
||||
|
||||
class LogParser:
|
||||
"""解析Higress访问日志,提取ai_log,支持日志轮转"""
|
||||
|
||||
def __init__(self, state_file: str = None):
|
||||
self.state_file = Path(state_file) if state_file else None
|
||||
self.file_offsets = {} # {文件路径: 已读取的字节偏移}
|
||||
self._load_state()
|
||||
|
||||
def _load_state(self):
|
||||
"""加载上次的读取状态"""
|
||||
if self.state_file and self.state_file.exists():
|
||||
try:
|
||||
with open(self.state_file, 'r') as f:
|
||||
self.file_offsets = json.load(f)
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to load state file: {e}", file=sys.stderr)
|
||||
|
||||
def _save_state(self):
|
||||
"""保存当前的读取状态"""
|
||||
if self.state_file:
|
||||
try:
|
||||
self.state_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(self.state_file, 'w') as f:
|
||||
json.dump(self.file_offsets, f, indent=2)
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to save state file: {e}", file=sys.stderr)
|
||||
|
||||
def parse_log_line(self, line: str) -> Optional[dict]:
|
||||
"""解析单行日志,提取ai_log JSON"""
|
||||
try:
|
||||
# 直接解析整个日志行为JSON
|
||||
log_obj = json.loads(line.strip())
|
||||
|
||||
# 获取ai_log字段(这是一个JSON字符串)
|
||||
if 'ai_log' in log_obj:
|
||||
ai_log_str = log_obj['ai_log']
|
||||
|
||||
# 解析内层JSON
|
||||
ai_log = json.loads(ai_log_str)
|
||||
return ai_log
|
||||
except (json.JSONDecodeError, ValueError, KeyError):
|
||||
# 静默忽略非JSON行或缺少ai_log字段的行
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def parse_rotated_logs(self, log_pattern: str, session_manager) -> None:
|
||||
"""解析日志文件及其轮转文件
|
||||
|
||||
Args:
|
||||
log_pattern: 日志文件路径,如 /var/log/proxy/access.log
|
||||
session_manager: Session管理器
|
||||
"""
|
||||
base_path = Path(log_pattern)
|
||||
|
||||
# 自动扫描所有轮转的日志文件(从旧到新)
|
||||
log_files = []
|
||||
|
||||
# 自动扫描轮转文件(最多扫描到 .100,超过这个数量的日志应该很少见)
|
||||
for i in range(100, 0, -1):
|
||||
rotated_path = Path(f"{log_pattern}.{i}")
|
||||
if rotated_path.exists():
|
||||
log_files.append(str(rotated_path))
|
||||
|
||||
# 添加当前日志文件
|
||||
if base_path.exists():
|
||||
log_files.append(str(base_path))
|
||||
|
||||
if not log_files:
|
||||
print(f"❌ No log files found for pattern: {log_pattern}")
|
||||
return
|
||||
|
||||
print(f"📂 Found {len(log_files)} log file(s):")
|
||||
for f in log_files:
|
||||
print(f" - {f}")
|
||||
print()
|
||||
|
||||
# 按顺序解析每个文件(从旧到新)
|
||||
for log_file in log_files:
|
||||
self._parse_file_incremental(log_file, session_manager)
|
||||
|
||||
# 保存状态
|
||||
self._save_state()
|
||||
|
||||
def _parse_file_incremental(self, file_path: str, session_manager) -> None:
|
||||
"""增量解析单个日志文件"""
|
||||
try:
|
||||
file_stat = os.stat(file_path)
|
||||
file_size = file_stat.st_size
|
||||
file_inode = file_stat.st_ino
|
||||
|
||||
# 使用inode作为主键
|
||||
inode_key = str(file_inode)
|
||||
last_offset = self.file_offsets.get(inode_key, 0)
|
||||
|
||||
# 如果文件变小了,说明是新文件(被truncate或新创建),从头开始读
|
||||
if file_size < last_offset:
|
||||
print(f" 📝 File truncated or recreated, reading from start: {file_path}")
|
||||
last_offset = 0
|
||||
|
||||
# 如果offset相同,说明没有新内容
|
||||
if file_size == last_offset:
|
||||
print(f" ⏭️ No new content in: {file_path} (inode:{inode_key})")
|
||||
return
|
||||
|
||||
print(f" 📖 Reading {file_path} from offset {last_offset} to {file_size} (inode:{inode_key})")
|
||||
|
||||
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
f.seek(last_offset)
|
||||
lines_processed = 0
|
||||
|
||||
for line in f:
|
||||
ai_log = self.parse_log_line(line)
|
||||
if ai_log:
|
||||
session_id = ai_log.get("session_id", "default")
|
||||
session_manager.update_session(session_id, ai_log)
|
||||
lines_processed += 1
|
||||
|
||||
# 每处理1000行打印一次进度
|
||||
if lines_processed % 1000 == 0:
|
||||
print(f" Processed {lines_processed} lines, {len(session_manager.sessions)} sessions")
|
||||
|
||||
# 更新offset(使用inode作为key)
|
||||
current_offset = f.tell()
|
||||
self.file_offsets[inode_key] = current_offset
|
||||
|
||||
print(f" ✅ Processed {lines_processed} new lines from {file_path}")
|
||||
|
||||
except FileNotFoundError:
|
||||
print(f" ❌ File not found: {file_path}")
|
||||
except Exception as e:
|
||||
print(f" ❌ Error parsing {file_path}: {e}")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 实时显示器
|
||||
# ============================================================================
|
||||
|
||||
class RealtimeMonitor:
|
||||
"""实时监控显示和交互(定时轮询模式)"""
|
||||
|
||||
def __init__(self, session_manager: SessionManager, log_parser=None, log_path: str = None, refresh_interval: int = 1):
|
||||
self.session_manager = session_manager
|
||||
self.log_parser = log_parser
|
||||
self.log_path = log_path
|
||||
self.refresh_interval = refresh_interval
|
||||
self.running = True
|
||||
self.last_poll_time = 0
|
||||
|
||||
def start(self):
|
||||
"""启动实时监控(定时轮询日志文件)"""
|
||||
print(f"\n{'=' * 50}")
|
||||
print(f"🔍 Agent Session Monitor - Real-time View")
|
||||
print(f"{'=' * 50}")
|
||||
print()
|
||||
print("Press Ctrl+C to stop...")
|
||||
print()
|
||||
|
||||
try:
|
||||
while self.running:
|
||||
# 定时轮询日志文件(检查新增内容和轮转)
|
||||
current_time = time.time()
|
||||
if self.log_parser and self.log_path and (current_time - self.last_poll_time >= self.refresh_interval):
|
||||
self.log_parser.parse_rotated_logs(self.log_path, self.session_manager)
|
||||
self.last_poll_time = current_time
|
||||
|
||||
# 显示状态
|
||||
self._display_status()
|
||||
time.sleep(self.refresh_interval)
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n👋 Stopping monitor...")
|
||||
self.running = False
|
||||
self._display_summary()
|
||||
|
||||
def _display_status(self):
|
||||
"""显示当前状态"""
|
||||
summary = self.session_manager.get_summary()
|
||||
|
||||
# 清屏
|
||||
os.system('clear' if os.name == 'posix' else 'cls')
|
||||
|
||||
print(f"{'=' * 50}")
|
||||
print(f"🔍 Session Monitor - Active")
|
||||
print(f"{'=' * 50}")
|
||||
print()
|
||||
print(f"📊 Active Sessions: {summary['total_sessions']}")
|
||||
print()
|
||||
|
||||
# 显示活跃session的token统计
|
||||
if summary['active_session_ids']:
|
||||
print("┌──────────────────────────┬─────────┬──────────┬───────────┐")
|
||||
print("│ Session ID │ Msgs │ Input │ Output │")
|
||||
print("├──────────────────────────┼─────────┼──────────┼───────────┤")
|
||||
|
||||
for session_id in summary['active_session_ids'][:10]: # 最多显示10个
|
||||
session = self.session_manager.get_session(session_id)
|
||||
if session:
|
||||
sid = session_id[:24] if len(session_id) > 24 else session_id
|
||||
print(f"│ {sid:<24} │ {session['messages_count']:>7} │ {session['total_input_tokens']:>8,} │ {session['total_output_tokens']:>9,} │")
|
||||
|
||||
print("└──────────────────────────┴─────────┴──────────┴───────────┘")
|
||||
|
||||
print()
|
||||
print(f"📈 Token Statistics")
|
||||
print(f" Total Input: {summary['total_input_tokens']:,} tokens")
|
||||
print(f" Total Output: {summary['total_output_tokens']:,} tokens")
|
||||
if summary['total_reasoning_tokens'] > 0:
|
||||
print(f" Total Reasoning: {summary['total_reasoning_tokens']:,} tokens")
|
||||
print(f" Total Cached: {summary['total_cached_tokens']:,} tokens")
|
||||
print(f" Total Cost: ${summary['total_cost_usd']:.4f}")
|
||||
|
||||
def _display_summary(self):
|
||||
"""显示最终汇总"""
|
||||
summary = self.session_manager.get_summary()
|
||||
|
||||
print()
|
||||
print(f"{'=' * 50}")
|
||||
print(f"📊 Session Monitor - Summary")
|
||||
print(f"{'=' * 50}")
|
||||
print()
|
||||
print(f"📈 Final Statistics")
|
||||
print(f" Total Sessions: {summary['total_sessions']}")
|
||||
print(f" Total Input: {summary['total_input_tokens']:,} tokens")
|
||||
print(f" Total Output: {summary['total_output_tokens']:,} tokens")
|
||||
if summary['total_reasoning_tokens'] > 0:
|
||||
print(f" Total Reasoning: {summary['total_reasoning_tokens']:,} tokens")
|
||||
print(f" Total Cached: {summary['total_cached_tokens']:,} tokens")
|
||||
print(f" Total Tokens: {summary['total_tokens']:,} tokens")
|
||||
print(f" Total Cost: ${summary['total_cost_usd']:.4f}")
|
||||
print(f"{'=' * 50}")
|
||||
print()
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 主程序
|
||||
# ============================================================================
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Agent Session Monitor - 实时监控多轮Agent对话的token开销",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
示例:
|
||||
# 监控默认日志
|
||||
%(prog)s
|
||||
|
||||
# 监控指定日志文件
|
||||
%(prog)s --log-path /var/log/higress/access.log
|
||||
|
||||
# 设置预算为500K tokens
|
||||
%(prog)s --budget 500000
|
||||
|
||||
# 监控特定session
|
||||
%(prog)s --session-key agent:main:discord:channel:1465367993012981988
|
||||
""",
|
||||
allow_abbrev=False
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--log-path',
|
||||
default=DEFAULT_LOG_PATH,
|
||||
help=f'Higress访问日志文件路径(默认: {DEFAULT_LOG_PATH})'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--output-dir',
|
||||
default=DEFAULT_OUTPUT_DIR,
|
||||
help=f'Session数据存储目录(默认: {DEFAULT_OUTPUT_DIR})'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--session-key',
|
||||
help='只监控包含指定session key的日志'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--refresh-interval',
|
||||
type=int,
|
||||
default=1,
|
||||
help=f'实时监控刷新间隔(秒,默认: 1)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--state-file',
|
||||
help='状态文件路径,用于记录已读取的offset(默认: <output-dir>/.state.json)'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 初始化组件
|
||||
session_manager = SessionManager(output_dir=args.output_dir)
|
||||
|
||||
# 状态文件路径
|
||||
state_file = args.state_file or str(Path(args.output_dir) / '.state.json')
|
||||
|
||||
log_parser = LogParser(state_file=state_file)
|
||||
|
||||
print(f"{'=' * 60}")
|
||||
print(f"🔍 Agent Session Monitor")
|
||||
print(f"{'=' * 60}")
|
||||
print()
|
||||
print(f"📂 Log path: {args.log_path}")
|
||||
print(f"📁 Output dir: {args.output_dir}")
|
||||
if args.session_key:
|
||||
print(f"🔑 Session key filter: {args.session_key}")
|
||||
print(f"{'=' * 60}")
|
||||
print()
|
||||
|
||||
# 模式选择:实时监控或单次解析
|
||||
if len(sys.argv) == 1:
|
||||
# 默认模式:实时监控(定时轮询)
|
||||
print("📺 Mode: Real-time monitoring (polling mode with log rotation support)")
|
||||
print(f" Refresh interval: {args.refresh_interval} second(s)")
|
||||
print()
|
||||
|
||||
# 首次解析现有日志文件(包括轮转的文件)
|
||||
log_parser.parse_rotated_logs(args.log_path, session_manager)
|
||||
|
||||
# 启动实时监控(定时轮询模式)
|
||||
monitor = RealtimeMonitor(
|
||||
session_manager,
|
||||
log_parser=log_parser,
|
||||
log_path=args.log_path,
|
||||
refresh_interval=args.refresh_interval
|
||||
)
|
||||
monitor.start()
|
||||
|
||||
else:
|
||||
# 单次解析模式
|
||||
print("📊 Mode: One-time log parsing (with log rotation support)")
|
||||
print()
|
||||
log_parser.parse_rotated_logs(args.log_path, session_manager)
|
||||
|
||||
# 显示汇总
|
||||
summary = session_manager.get_summary()
|
||||
print(f"\n{'=' * 50}")
|
||||
print(f"📊 Session Summary")
|
||||
print(f"{'=' * 50}")
|
||||
print()
|
||||
print(f"📈 Final Statistics")
|
||||
print(f" Total Sessions: {summary['total_sessions']}")
|
||||
print(f" Total Input: {summary['total_input_tokens']:,} tokens")
|
||||
print(f" Total Output: {summary['total_output_tokens']:,} tokens")
|
||||
if summary['total_reasoning_tokens'] > 0:
|
||||
print(f" Total Reasoning: {summary['total_reasoning_tokens']:,} tokens")
|
||||
print(f" Total Cached: {summary['total_cached_tokens']:,} tokens")
|
||||
print(f" Total Tokens: {summary['total_tokens']:,} tokens")
|
||||
print(f" Total Cost: ${summary['total_cost_usd']:.4f}")
|
||||
print(f"{'=' * 50}")
|
||||
print()
|
||||
print(f"💾 Session data saved to: {args.output_dir}/")
|
||||
print(f" Run with --output-dir to specify custom directory")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,600 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Agent Session Monitor CLI - 查询和分析agent对话数据
|
||||
支持:
|
||||
1. 实时查询指定session的完整llm请求和响应
|
||||
2. 按模型统计token开销
|
||||
3. 按日期统计token开销
|
||||
4. 生成FinOps报表
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
import re
|
||||
|
||||
# Token定价(单位:美元/1M tokens)
|
||||
TOKEN_PRICING = {
|
||||
"Qwen": {
|
||||
"input": 0.0002, # $0.2/1M
|
||||
"output": 0.0006,
|
||||
"cached": 0.0001, # cached tokens通常是input的50%
|
||||
},
|
||||
"Qwen3-rerank": {
|
||||
"input": 0.0003,
|
||||
"output": 0.0012,
|
||||
"cached": 0.00015,
|
||||
},
|
||||
"Qwen-Max": {
|
||||
"input": 0.0005,
|
||||
"output": 0.002,
|
||||
"cached": 0.00025,
|
||||
},
|
||||
"GPT-4": {
|
||||
"input": 0.003,
|
||||
"output": 0.006,
|
||||
"cached": 0.0015,
|
||||
},
|
||||
"GPT-4o": {
|
||||
"input": 0.0025,
|
||||
"output": 0.01,
|
||||
"cached": 0.00125, # GPT-4o prompt caching: 50% discount
|
||||
},
|
||||
"GPT-4-32k": {
|
||||
"input": 0.01,
|
||||
"output": 0.03,
|
||||
"cached": 0.005,
|
||||
},
|
||||
"o1": {
|
||||
"input": 0.015,
|
||||
"output": 0.06,
|
||||
"cached": 0.0075,
|
||||
"reasoning": 0.06, # o1 reasoning tokens same as output
|
||||
},
|
||||
"o1-mini": {
|
||||
"input": 0.003,
|
||||
"output": 0.012,
|
||||
"cached": 0.0015,
|
||||
"reasoning": 0.012,
|
||||
},
|
||||
"Claude": {
|
||||
"input": 0.015,
|
||||
"output": 0.075,
|
||||
"cached": 0.0015, # Claude prompt caching: 90% discount
|
||||
},
|
||||
"DeepSeek-R1": {
|
||||
"input": 0.004,
|
||||
"output": 0.012,
|
||||
"reasoning": 0.002,
|
||||
"cached": 0.002,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class SessionAnalyzer:
|
||||
"""Session数据分析器"""
|
||||
|
||||
def __init__(self, data_dir: str):
|
||||
self.data_dir = Path(data_dir)
|
||||
if not self.data_dir.exists():
|
||||
raise FileNotFoundError(f"Session data directory not found: {data_dir}")
|
||||
|
||||
def load_session(self, session_id: str) -> Optional[dict]:
|
||||
"""加载指定session的完整数据"""
|
||||
session_file = self.data_dir / f"{session_id}.json"
|
||||
if not session_file.exists():
|
||||
return None
|
||||
|
||||
with open(session_file, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
def load_all_sessions(self) -> List[dict]:
|
||||
"""加载所有session数据"""
|
||||
sessions = []
|
||||
for session_file in self.data_dir.glob("*.json"):
|
||||
try:
|
||||
with open(session_file, 'r', encoding='utf-8') as f:
|
||||
session = json.load(f)
|
||||
sessions.append(session)
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to load {session_file}: {e}", file=sys.stderr)
|
||||
return sessions
|
||||
|
||||
def display_session_detail(self, session_id: str, show_messages: bool = True):
|
||||
"""显示session的详细信息"""
|
||||
session = self.load_session(session_id)
|
||||
if not session:
|
||||
print(f"❌ Session not found: {session_id}")
|
||||
return
|
||||
|
||||
print(f"\n{'='*70}")
|
||||
print(f"📊 Session Detail: {session_id}")
|
||||
print(f"{'='*70}\n")
|
||||
|
||||
# 基本信息
|
||||
print(f"🕐 Created: {session['created_at']}")
|
||||
print(f"🕑 Updated: {session['updated_at']}")
|
||||
print(f"🤖 Model: {session['model']}")
|
||||
print(f"💬 Messages: {session['messages_count']}")
|
||||
print()
|
||||
|
||||
# Token统计
|
||||
print(f"📈 Token Statistics:")
|
||||
|
||||
total_input = session['total_input_tokens']
|
||||
total_output = session['total_output_tokens']
|
||||
total_reasoning = session.get('total_reasoning_tokens', 0)
|
||||
total_cached = session.get('total_cached_tokens', 0)
|
||||
|
||||
# 区分regular input和cached input
|
||||
regular_input = total_input - total_cached
|
||||
|
||||
if total_cached > 0:
|
||||
print(f" Input: {regular_input:>10,} tokens (regular)")
|
||||
print(f" Cached: {total_cached:>10,} tokens (from cache)")
|
||||
print(f" Total Input:{total_input:>10,} tokens")
|
||||
else:
|
||||
print(f" Input: {total_input:>10,} tokens")
|
||||
|
||||
print(f" Output: {total_output:>10,} tokens")
|
||||
|
||||
if total_reasoning > 0:
|
||||
print(f" Reasoning: {total_reasoning:>10,} tokens")
|
||||
|
||||
# 总计(不重复计算cached)
|
||||
total_tokens = total_input + total_output + total_reasoning
|
||||
print(f" ────────────────────────")
|
||||
print(f" Total: {total_tokens:>10,} tokens")
|
||||
print()
|
||||
|
||||
# 成本计算
|
||||
cost = self._calculate_cost(session)
|
||||
print(f"💰 Estimated Cost: ${cost:.8f} USD")
|
||||
print()
|
||||
|
||||
# 对话轮次
|
||||
if show_messages and 'rounds' in session:
|
||||
print(f"📝 Conversation Rounds ({len(session['rounds'])}):")
|
||||
print(f"{'─'*70}")
|
||||
|
||||
for i, round_data in enumerate(session['rounds'], 1):
|
||||
timestamp = round_data.get('timestamp', 'N/A')
|
||||
input_tokens = round_data.get('input_tokens', 0)
|
||||
output_tokens = round_data.get('output_tokens', 0)
|
||||
has_tool_calls = round_data.get('has_tool_calls', False)
|
||||
response_type = round_data.get('response_type', 'normal')
|
||||
|
||||
print(f"\n Round {i} @ {timestamp}")
|
||||
print(f" Tokens: {input_tokens:,} in → {output_tokens:,} out")
|
||||
|
||||
if has_tool_calls:
|
||||
print(f" 🔧 Tool calls: Yes")
|
||||
|
||||
if response_type != 'normal':
|
||||
print(f" Type: {response_type}")
|
||||
|
||||
# 显示完整的messages(如果有)
|
||||
if 'messages' in round_data:
|
||||
messages = round_data['messages']
|
||||
print(f" Messages ({len(messages)}):")
|
||||
for msg in messages[-3:]: # 只显示最后3条
|
||||
role = msg.get('role', 'unknown')
|
||||
content = msg.get('content', '')
|
||||
content_preview = content[:100] + '...' if len(content) > 100 else content
|
||||
print(f" [{role}] {content_preview}")
|
||||
|
||||
# 显示question/answer/reasoning(如果有)
|
||||
if 'question' in round_data:
|
||||
q = round_data['question']
|
||||
q_preview = q[:150] + '...' if len(q) > 150 else q
|
||||
print(f" ❓ Question: {q_preview}")
|
||||
|
||||
if 'answer' in round_data:
|
||||
a = round_data['answer']
|
||||
a_preview = a[:150] + '...' if len(a) > 150 else a
|
||||
print(f" ✅ Answer: {a_preview}")
|
||||
|
||||
if 'reasoning' in round_data and round_data['reasoning']:
|
||||
r = round_data['reasoning']
|
||||
r_preview = r[:150] + '...' if len(r) > 150 else r
|
||||
print(f" 🧠 Reasoning: {r_preview}")
|
||||
|
||||
if 'tool_calls' in round_data and round_data['tool_calls']:
|
||||
print(f" 🛠️ Tool Calls:")
|
||||
for tool_call in round_data['tool_calls']:
|
||||
func_name = tool_call.get('function', {}).get('name', 'unknown')
|
||||
args = tool_call.get('function', {}).get('arguments', '')
|
||||
print(f" - {func_name}({args[:80]}...)")
|
||||
|
||||
# 显示token details(如果有)
|
||||
if round_data.get('input_token_details'):
|
||||
print(f" 📊 Input Token Details: {round_data['input_token_details']}")
|
||||
|
||||
if round_data.get('output_token_details'):
|
||||
print(f" 📊 Output Token Details: {round_data['output_token_details']}")
|
||||
|
||||
print(f"\n{'─'*70}")
|
||||
|
||||
print(f"\n{'='*70}\n")
|
||||
|
||||
def _calculate_cost(self, session: dict) -> float:
|
||||
"""计算session的成本"""
|
||||
model = session.get('model', 'unknown')
|
||||
pricing = TOKEN_PRICING.get(model, TOKEN_PRICING.get("GPT-4", {}))
|
||||
|
||||
input_tokens = session['total_input_tokens']
|
||||
output_tokens = session['total_output_tokens']
|
||||
reasoning_tokens = session.get('total_reasoning_tokens', 0)
|
||||
cached_tokens = session.get('total_cached_tokens', 0)
|
||||
|
||||
# 区分regular input和cached input
|
||||
regular_input_tokens = input_tokens - cached_tokens
|
||||
|
||||
input_cost = regular_input_tokens * pricing.get('input', 0) / 1000000
|
||||
output_cost = output_tokens * pricing.get('output', 0) / 1000000
|
||||
|
||||
reasoning_cost = 0
|
||||
if 'reasoning' in pricing and reasoning_tokens > 0:
|
||||
reasoning_cost = reasoning_tokens * pricing['reasoning'] / 1000000
|
||||
|
||||
cached_cost = 0
|
||||
if 'cached' in pricing and cached_tokens > 0:
|
||||
cached_cost = cached_tokens * pricing['cached'] / 1000000
|
||||
|
||||
return input_cost + output_cost + reasoning_cost + cached_cost
|
||||
|
||||
def stats_by_model(self) -> Dict[str, dict]:
|
||||
"""按模型统计token开销"""
|
||||
sessions = self.load_all_sessions()
|
||||
|
||||
stats = defaultdict(lambda: {
|
||||
'session_count': 0,
|
||||
'total_input': 0,
|
||||
'total_output': 0,
|
||||
'total_reasoning': 0,
|
||||
'total_cost': 0.0
|
||||
})
|
||||
|
||||
for session in sessions:
|
||||
model = session.get('model', 'unknown')
|
||||
stats[model]['session_count'] += 1
|
||||
stats[model]['total_input'] += session['total_input_tokens']
|
||||
stats[model]['total_output'] += session['total_output_tokens']
|
||||
stats[model]['total_reasoning'] += session.get('total_reasoning_tokens', 0)
|
||||
stats[model]['total_cost'] += self._calculate_cost(session)
|
||||
|
||||
return dict(stats)
|
||||
|
||||
def stats_by_date(self, days: int = 30) -> Dict[str, dict]:
|
||||
"""按日期统计token开销(最近N天)"""
|
||||
sessions = self.load_all_sessions()
|
||||
|
||||
stats = defaultdict(lambda: {
|
||||
'session_count': 0,
|
||||
'total_input': 0,
|
||||
'total_output': 0,
|
||||
'total_reasoning': 0,
|
||||
'total_cost': 0.0,
|
||||
'models': set()
|
||||
})
|
||||
|
||||
cutoff_date = datetime.now() - timedelta(days=days)
|
||||
|
||||
for session in sessions:
|
||||
created_at = datetime.fromisoformat(session['created_at'])
|
||||
if created_at < cutoff_date:
|
||||
continue
|
||||
|
||||
date_key = created_at.strftime('%Y-%m-%d')
|
||||
stats[date_key]['session_count'] += 1
|
||||
stats[date_key]['total_input'] += session['total_input_tokens']
|
||||
stats[date_key]['total_output'] += session['total_output_tokens']
|
||||
stats[date_key]['total_reasoning'] += session.get('total_reasoning_tokens', 0)
|
||||
stats[date_key]['total_cost'] += self._calculate_cost(session)
|
||||
stats[date_key]['models'].add(session.get('model', 'unknown'))
|
||||
|
||||
# 转换sets为lists以便JSON序列化
|
||||
for date_key in stats:
|
||||
stats[date_key]['models'] = list(stats[date_key]['models'])
|
||||
|
||||
return dict(stats)
|
||||
|
||||
def display_model_stats(self):
|
||||
"""显示按模型的统计"""
|
||||
stats = self.stats_by_model()
|
||||
|
||||
print(f"\n{'='*80}")
|
||||
print(f"📊 Statistics by Model")
|
||||
print(f"{'='*80}\n")
|
||||
|
||||
print(f"{'Model':<20} {'Sessions':<10} {'Input':<15} {'Output':<15} {'Cost (USD)':<12}")
|
||||
print(f"{'─'*80}")
|
||||
|
||||
# 按成本降序排列
|
||||
sorted_models = sorted(stats.items(), key=lambda x: x[1]['total_cost'], reverse=True)
|
||||
|
||||
for model, data in sorted_models:
|
||||
print(f"{model:<20} "
|
||||
f"{data['session_count']:<10} "
|
||||
f"{data['total_input']:>12,} "
|
||||
f"{data['total_output']:>12,} "
|
||||
f"${data['total_cost']:>10.6f}")
|
||||
|
||||
# 总计
|
||||
total_sessions = sum(d['session_count'] for d in stats.values())
|
||||
total_input = sum(d['total_input'] for d in stats.values())
|
||||
total_output = sum(d['total_output'] for d in stats.values())
|
||||
total_cost = sum(d['total_cost'] for d in stats.values())
|
||||
|
||||
print(f"{'─'*80}")
|
||||
print(f"{'TOTAL':<20} "
|
||||
f"{total_sessions:<10} "
|
||||
f"{total_input:>12,} "
|
||||
f"{total_output:>12,} "
|
||||
f"${total_cost:>10.6f}")
|
||||
|
||||
print(f"\n{'='*80}\n")
|
||||
|
||||
def display_date_stats(self, days: int = 30):
|
||||
"""显示按日期的统计"""
|
||||
stats = self.stats_by_date(days)
|
||||
|
||||
print(f"\n{'='*80}")
|
||||
print(f"📊 Statistics by Date (Last {days} days)")
|
||||
print(f"{'='*80}\n")
|
||||
|
||||
print(f"{'Date':<12} {'Sessions':<10} {'Input':<15} {'Output':<15} {'Cost (USD)':<12} {'Models':<20}")
|
||||
print(f"{'─'*80}")
|
||||
|
||||
# 按日期升序排列
|
||||
sorted_dates = sorted(stats.items())
|
||||
|
||||
for date, data in sorted_dates:
|
||||
models_str = ', '.join(data['models'][:3]) # 最多显示3个模型
|
||||
if len(data['models']) > 3:
|
||||
models_str += f" +{len(data['models'])-3}"
|
||||
|
||||
print(f"{date:<12} "
|
||||
f"{data['session_count']:<10} "
|
||||
f"{data['total_input']:>12,} "
|
||||
f"{data['total_output']:>12,} "
|
||||
f"${data['total_cost']:>10.4f} "
|
||||
f"{models_str}")
|
||||
|
||||
# 总计
|
||||
total_sessions = sum(d['session_count'] for d in stats.values())
|
||||
total_input = sum(d['total_input'] for d in stats.values())
|
||||
total_output = sum(d['total_output'] for d in stats.values())
|
||||
total_cost = sum(d['total_cost'] for d in stats.values())
|
||||
|
||||
print(f"{'─'*80}")
|
||||
print(f"{'TOTAL':<12} "
|
||||
f"{total_sessions:<10} "
|
||||
f"{total_input:>12,} "
|
||||
f"{total_output:>12,} "
|
||||
f"${total_cost:>10.4f}")
|
||||
|
||||
print(f"\n{'='*80}\n")
|
||||
|
||||
def list_sessions(self, limit: int = 20, sort_by: str = 'updated'):
|
||||
"""列出所有session"""
|
||||
sessions = self.load_all_sessions()
|
||||
|
||||
# 排序
|
||||
if sort_by == 'updated':
|
||||
sessions.sort(key=lambda s: s.get('updated_at', ''), reverse=True)
|
||||
elif sort_by == 'cost':
|
||||
sessions.sort(key=lambda s: self._calculate_cost(s), reverse=True)
|
||||
elif sort_by == 'tokens':
|
||||
sessions.sort(key=lambda s: s['total_input_tokens'] + s['total_output_tokens'], reverse=True)
|
||||
|
||||
print(f"\n{'='*100}")
|
||||
print(f"📋 Sessions (sorted by {sort_by}, showing {min(limit, len(sessions))} of {len(sessions)})")
|
||||
print(f"{'='*100}\n")
|
||||
|
||||
print(f"{'Session ID':<30} {'Updated':<20} {'Model':<15} {'Msgs':<6} {'Tokens':<12} {'Cost':<10}")
|
||||
print(f"{'─'*100}")
|
||||
|
||||
for session in sessions[:limit]:
|
||||
session_id = session['session_id'][:28] + '..' if len(session['session_id']) > 30 else session['session_id']
|
||||
updated = session.get('updated_at', 'N/A')[:19]
|
||||
model = session.get('model', 'unknown')[:13]
|
||||
msg_count = session.get('messages_count', 0)
|
||||
total_tokens = session['total_input_tokens'] + session['total_output_tokens']
|
||||
cost = self._calculate_cost(session)
|
||||
|
||||
print(f"{session_id:<30} {updated:<20} {model:<15} {msg_count:<6} {total_tokens:>10,} ${cost:>8.4f}")
|
||||
|
||||
print(f"\n{'='*100}\n")
|
||||
|
||||
def export_finops_report(self, output_file: str, format: str = 'json'):
|
||||
"""导出FinOps报表"""
|
||||
model_stats = self.stats_by_model()
|
||||
date_stats = self.stats_by_date(30)
|
||||
|
||||
report = {
|
||||
'generated_at': datetime.now().isoformat(),
|
||||
'summary': {
|
||||
'total_sessions': sum(d['session_count'] for d in model_stats.values()),
|
||||
'total_input_tokens': sum(d['total_input'] for d in model_stats.values()),
|
||||
'total_output_tokens': sum(d['total_output'] for d in model_stats.values()),
|
||||
'total_cost_usd': sum(d['total_cost'] for d in model_stats.values()),
|
||||
},
|
||||
'by_model': model_stats,
|
||||
'by_date': date_stats,
|
||||
}
|
||||
|
||||
output_path = Path(output_file)
|
||||
|
||||
if format == 'json':
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(report, f, ensure_ascii=False, indent=2)
|
||||
print(f"✅ FinOps report exported to: {output_path}")
|
||||
|
||||
elif format == 'csv':
|
||||
import csv
|
||||
|
||||
# 按模型导出CSV
|
||||
model_csv = output_path.with_suffix('.model.csv')
|
||||
with open(model_csv, 'w', newline='', encoding='utf-8') as f:
|
||||
writer = csv.writer(f)
|
||||
writer.writerow(['Model', 'Sessions', 'Input Tokens', 'Output Tokens', 'Cost (USD)'])
|
||||
for model, data in model_stats.items():
|
||||
writer.writerow([
|
||||
model,
|
||||
data['session_count'],
|
||||
data['total_input'],
|
||||
data['total_output'],
|
||||
f"{data['total_cost']:.6f}"
|
||||
])
|
||||
|
||||
# 按日期导出CSV
|
||||
date_csv = output_path.with_suffix('.date.csv')
|
||||
with open(date_csv, 'w', newline='', encoding='utf-8') as f:
|
||||
writer = csv.writer(f)
|
||||
writer.writerow(['Date', 'Sessions', 'Input Tokens', 'Output Tokens', 'Cost (USD)', 'Models'])
|
||||
for date, data in sorted(date_stats.items()):
|
||||
writer.writerow([
|
||||
date,
|
||||
data['session_count'],
|
||||
data['total_input'],
|
||||
data['total_output'],
|
||||
f"{data['total_cost']:.6f}",
|
||||
', '.join(data['models'])
|
||||
])
|
||||
|
||||
print(f"✅ FinOps report exported to:")
|
||||
print(f" Model stats: {model_csv}")
|
||||
print(f" Date stats: {date_csv}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Agent Session Monitor CLI - 查询和分析agent对话数据",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Commands:
|
||||
show <session-id> 显示session的详细信息
|
||||
list 列出所有session
|
||||
stats-model 按模型统计token开销
|
||||
stats-date 按日期统计token开销(默认30天)
|
||||
export 导出FinOps报表
|
||||
|
||||
Examples:
|
||||
# 查看特定session的详细对话
|
||||
%(prog)s show agent:main:discord:channel:1465367993012981988
|
||||
|
||||
# 列出最近20个session(按更新时间)
|
||||
%(prog)s list
|
||||
|
||||
# 列出token开销最高的10个session
|
||||
%(prog)s list --sort-by cost --limit 10
|
||||
|
||||
# 按模型统计token开销
|
||||
%(prog)s stats-model
|
||||
|
||||
# 按日期统计token开销(最近7天)
|
||||
%(prog)s stats-date --days 7
|
||||
|
||||
# 导出FinOps报表(JSON格式)
|
||||
%(prog)s export finops-report.json
|
||||
|
||||
# 导出FinOps报表(CSV格式)
|
||||
%(prog)s export finops-report --format csv
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'command',
|
||||
choices=['show', 'list', 'stats-model', 'stats-date', 'export'],
|
||||
help='命令'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'args',
|
||||
nargs='*',
|
||||
help='命令参数(例如:session-id或输出文件名)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--data-dir',
|
||||
default='./sessions',
|
||||
help='Session数据目录(默认: ./sessions)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--limit',
|
||||
type=int,
|
||||
default=20,
|
||||
help='list命令的结果限制(默认: 20)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--sort-by',
|
||||
choices=['updated', 'cost', 'tokens'],
|
||||
default='updated',
|
||||
help='list命令的排序方式(默认: updated)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--days',
|
||||
type=int,
|
||||
default=30,
|
||||
help='stats-date命令的天数(默认: 30)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--format',
|
||||
choices=['json', 'csv'],
|
||||
default='json',
|
||||
help='export命令的输出格式(默认: json)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--no-messages',
|
||||
action='store_true',
|
||||
help='show命令:不显示对话内容'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
analyzer = SessionAnalyzer(args.data_dir)
|
||||
|
||||
if args.command == 'show':
|
||||
if not args.args:
|
||||
parser.error("show命令需要session-id参数")
|
||||
session_id = args.args[0]
|
||||
analyzer.display_session_detail(session_id, show_messages=not args.no_messages)
|
||||
|
||||
elif args.command == 'list':
|
||||
analyzer.list_sessions(limit=args.limit, sort_by=args.sort_by)
|
||||
|
||||
elif args.command == 'stats-model':
|
||||
analyzer.display_model_stats()
|
||||
|
||||
elif args.command == 'stats-date':
|
||||
analyzer.display_date_stats(days=args.days)
|
||||
|
||||
elif args.command == 'export':
|
||||
if not args.args:
|
||||
parser.error("export命令需要输出文件名参数")
|
||||
output_file = args.args[0]
|
||||
analyzer.export_finops_report(output_file, format=args.format)
|
||||
|
||||
except FileNotFoundError as e:
|
||||
print(f"❌ Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"❌ Unexpected error: {e}", file=sys.stderr)
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,755 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Agent Session Monitor - Web Server
|
||||
提供浏览器访问的观测界面
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta
|
||||
import re
|
||||
|
||||
# 添加父目录到path以导入cli模块
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
try:
|
||||
from scripts.cli import SessionAnalyzer, TOKEN_PRICING
|
||||
except ImportError:
|
||||
# 如果导入失败,定义简单版本
|
||||
TOKEN_PRICING = {
|
||||
"Qwen3-rerank": {"input": 0.0003, "output": 0.0012},
|
||||
"DeepSeek-R1": {"input": 0.004, "output": 0.012, "reasoning": 0.002},
|
||||
}
|
||||
|
||||
|
||||
class SessionMonitorHandler(BaseHTTPRequestHandler):
|
||||
"""HTTP请求处理器"""
|
||||
|
||||
def __init__(self, *args, data_dir=None, **kwargs):
|
||||
self.data_dir = Path(data_dir) if data_dir else Path("./sessions")
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def do_GET(self):
|
||||
"""处理GET请求"""
|
||||
parsed_path = urlparse(self.path)
|
||||
path = parsed_path.path
|
||||
query = parse_qs(parsed_path.query)
|
||||
|
||||
if path == '/' or path == '/index.html':
|
||||
self.serve_index()
|
||||
elif path == '/session':
|
||||
session_id = query.get('id', [None])[0]
|
||||
if session_id:
|
||||
self.serve_session_detail(session_id)
|
||||
else:
|
||||
self.send_error(400, "Missing session id")
|
||||
elif path == '/api/sessions':
|
||||
self.serve_api_sessions()
|
||||
elif path == '/api/session':
|
||||
session_id = query.get('id', [None])[0]
|
||||
if session_id:
|
||||
self.serve_api_session(session_id)
|
||||
else:
|
||||
self.send_error(400, "Missing session id")
|
||||
elif path == '/api/stats':
|
||||
self.serve_api_stats()
|
||||
else:
|
||||
self.send_error(404, "Not Found")
|
||||
|
||||
def serve_index(self):
|
||||
"""首页 - 总览"""
|
||||
html = self.generate_index_html()
|
||||
self.send_html(html)
|
||||
|
||||
def serve_session_detail(self, session_id: str):
|
||||
"""Session详情页"""
|
||||
html = self.generate_session_html(session_id)
|
||||
self.send_html(html)
|
||||
|
||||
def serve_api_sessions(self):
|
||||
"""API: 获取所有session列表"""
|
||||
sessions = self.load_all_sessions()
|
||||
|
||||
# 简化数据
|
||||
data = []
|
||||
for session in sessions:
|
||||
data.append({
|
||||
'session_id': session['session_id'],
|
||||
'model': session.get('model', 'unknown'),
|
||||
'messages_count': session.get('messages_count', 0),
|
||||
'total_tokens': session['total_input_tokens'] + session['total_output_tokens'],
|
||||
'updated_at': session.get('updated_at', ''),
|
||||
'cost': self.calculate_cost(session)
|
||||
})
|
||||
|
||||
# 按更新时间降序排序
|
||||
data.sort(key=lambda x: x['updated_at'], reverse=True)
|
||||
|
||||
self.send_json(data)
|
||||
|
||||
def serve_api_session(self, session_id: str):
|
||||
"""API: 获取指定session的详细数据"""
|
||||
session = self.load_session(session_id)
|
||||
if session:
|
||||
session['cost'] = self.calculate_cost(session)
|
||||
self.send_json(session)
|
||||
else:
|
||||
self.send_error(404, "Session not found")
|
||||
|
||||
def serve_api_stats(self):
|
||||
"""API: 获取统计数据"""
|
||||
sessions = self.load_all_sessions()
|
||||
|
||||
# 按模型统计
|
||||
by_model = defaultdict(lambda: {
|
||||
'count': 0,
|
||||
'input_tokens': 0,
|
||||
'output_tokens': 0,
|
||||
'cost': 0.0
|
||||
})
|
||||
|
||||
# 按日期统计
|
||||
by_date = defaultdict(lambda: {
|
||||
'count': 0,
|
||||
'input_tokens': 0,
|
||||
'output_tokens': 0,
|
||||
'cost': 0.0,
|
||||
'models': set()
|
||||
})
|
||||
|
||||
total_cost = 0.0
|
||||
|
||||
for session in sessions:
|
||||
model = session.get('model', 'unknown')
|
||||
cost = self.calculate_cost(session)
|
||||
total_cost += cost
|
||||
|
||||
# 按模型
|
||||
by_model[model]['count'] += 1
|
||||
by_model[model]['input_tokens'] += session['total_input_tokens']
|
||||
by_model[model]['output_tokens'] += session['total_output_tokens']
|
||||
by_model[model]['cost'] += cost
|
||||
|
||||
# 按日期
|
||||
created_at = session.get('created_at', '')
|
||||
date_key = created_at[:10] if len(created_at) >= 10 else 'unknown'
|
||||
by_date[date_key]['count'] += 1
|
||||
by_date[date_key]['input_tokens'] += session['total_input_tokens']
|
||||
by_date[date_key]['output_tokens'] += session['total_output_tokens']
|
||||
by_date[date_key]['cost'] += cost
|
||||
by_date[date_key]['models'].add(model)
|
||||
|
||||
# 转换sets为lists
|
||||
for date in by_date:
|
||||
by_date[date]['models'] = list(by_date[date]['models'])
|
||||
|
||||
stats = {
|
||||
'total_sessions': len(sessions),
|
||||
'total_cost': total_cost,
|
||||
'by_model': dict(by_model),
|
||||
'by_date': dict(sorted(by_date.items(), reverse=True))
|
||||
}
|
||||
|
||||
self.send_json(stats)
|
||||
|
||||
def load_session(self, session_id: str):
|
||||
"""加载指定session"""
|
||||
session_file = self.data_dir / f"{session_id}.json"
|
||||
if session_file.exists():
|
||||
with open(session_file, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
return None
|
||||
|
||||
def load_all_sessions(self):
|
||||
"""加载所有session"""
|
||||
sessions = []
|
||||
for session_file in self.data_dir.glob("*.json"):
|
||||
try:
|
||||
with open(session_file, 'r', encoding='utf-8') as f:
|
||||
sessions.append(json.load(f))
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to load {session_file}: {e}", file=sys.stderr)
|
||||
return sessions
|
||||
|
||||
def calculate_cost(self, session: dict) -> float:
|
||||
"""计算session成本"""
|
||||
model = session.get('model', 'unknown')
|
||||
pricing = TOKEN_PRICING.get(model, TOKEN_PRICING.get("GPT-4", {"input": 0.003, "output": 0.006}))
|
||||
|
||||
input_tokens = session['total_input_tokens']
|
||||
output_tokens = session['total_output_tokens']
|
||||
reasoning_tokens = session.get('total_reasoning_tokens', 0)
|
||||
cached_tokens = session.get('total_cached_tokens', 0)
|
||||
|
||||
# 区分regular input和cached input
|
||||
regular_input_tokens = input_tokens - cached_tokens
|
||||
|
||||
input_cost = regular_input_tokens * pricing.get('input', 0) / 1000000
|
||||
output_cost = output_tokens * pricing.get('output', 0) / 1000000
|
||||
|
||||
reasoning_cost = 0
|
||||
if 'reasoning' in pricing and reasoning_tokens > 0:
|
||||
reasoning_cost = reasoning_tokens * pricing['reasoning'] / 1000000
|
||||
|
||||
cached_cost = 0
|
||||
if 'cached' in pricing and cached_tokens > 0:
|
||||
cached_cost = cached_tokens * pricing['cached'] / 1000000
|
||||
|
||||
return input_cost + output_cost + reasoning_cost + cached_cost
|
||||
|
||||
def send_html(self, html: str):
|
||||
"""发送HTML响应"""
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'text/html; charset=utf-8')
|
||||
self.end_headers()
|
||||
self.wfile.write(html.encode('utf-8'))
|
||||
|
||||
def send_json(self, data):
|
||||
"""发送JSON响应"""
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'application/json; charset=utf-8')
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps(data, ensure_ascii=False, indent=2).encode('utf-8'))
|
||||
|
||||
def generate_index_html(self) -> str:
|
||||
"""生成首页HTML"""
|
||||
return '''<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Agent Session Monitor</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
.container { max-width: 1400px; margin: 0 auto; }
|
||||
header {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
h1 { color: #333; margin-bottom: 10px; }
|
||||
.subtitle { color: #666; font-size: 14px; }
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.stat-card {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
.stat-label { color: #666; font-size: 14px; margin-bottom: 8px; }
|
||||
.stat-value { color: #333; font-size: 32px; font-weight: bold; }
|
||||
.stat-unit { color: #999; font-size: 16px; margin-left: 4px; }
|
||||
|
||||
.section {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
h2 { color: #333; margin-bottom: 20px; font-size: 20px; }
|
||||
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
thead { background: #f8f9fa; }
|
||||
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #e9ecef; }
|
||||
th { font-weight: 600; color: #666; font-size: 14px; }
|
||||
td { color: #333; }
|
||||
tbody tr:hover { background: #f8f9fa; }
|
||||
|
||||
.session-link {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
.session-link:hover { text-decoration: underline; }
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.badge-qwen { background: #e3f2fd; color: #1976d2; }
|
||||
.badge-deepseek { background: #f3e5f5; color: #7b1fa2; }
|
||||
.badge-gpt { background: #e8f5e9; color: #388e3c; }
|
||||
.badge-claude { background: #fff3e0; color: #f57c00; }
|
||||
|
||||
.loading { text-align: center; padding: 40px; color: #666; }
|
||||
.error { color: #d32f2f; padding: 20px; }
|
||||
|
||||
.refresh-btn {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
.refresh-btn:hover { background: #0056b3; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>🔍 Agent Session Monitor</h1>
|
||||
<p class="subtitle">实时观测Clawdbot对话过程和Token开销</p>
|
||||
</header>
|
||||
|
||||
<div class="stats-grid" id="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">总会话数</div>
|
||||
<div class="stat-value">-</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">总Token消耗</div>
|
||||
<div class="stat-value">-</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">总成本</div>
|
||||
<div class="stat-value">-</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>📊 最近会话</h2>
|
||||
<button class="refresh-btn" onclick="loadSessions()">🔄 刷新</button>
|
||||
<div id="sessions-table">
|
||||
<div class="loading">加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>📈 按模型统计</h2>
|
||||
<div id="model-stats">
|
||||
<div class="loading">加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function loadSessions() {
|
||||
fetch('/api/sessions')
|
||||
.then(r => r.json())
|
||||
.then(sessions => {
|
||||
const html = `
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Session ID</th>
|
||||
<th>模型</th>
|
||||
<th>消息数</th>
|
||||
<th>总Token</th>
|
||||
<th>成本</th>
|
||||
<th>更新时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${sessions.slice(0, 50).map(s => `
|
||||
<tr>
|
||||
<td><a href="/session?id=${encodeURIComponent(s.session_id)}" class="session-link">${s.session_id}</a></td>
|
||||
<td>${getModelBadge(s.model)}</td>
|
||||
<td>${s.messages_count}</td>
|
||||
<td>${s.total_tokens.toLocaleString()}</td>
|
||||
<td>$${s.cost.toFixed(6)}</td>
|
||||
<td>${new Date(s.updated_at).toLocaleString()}</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
document.getElementById('sessions-table').innerHTML = html;
|
||||
})
|
||||
.catch(err => {
|
||||
document.getElementById('sessions-table').innerHTML = `<div class="error">加载失败: ${err.message}</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
function loadStats() {
|
||||
fetch('/api/stats')
|
||||
.then(r => r.json())
|
||||
.then(stats => {
|
||||
// 更新顶部统计卡片
|
||||
const cards = document.querySelectorAll('.stat-card');
|
||||
cards[0].querySelector('.stat-value').textContent = stats.total_sessions;
|
||||
|
||||
const totalTokens = Object.values(stats.by_model).reduce((sum, m) => sum + m.input_tokens + m.output_tokens, 0);
|
||||
cards[1].querySelector('.stat-value').innerHTML = totalTokens.toLocaleString() + '<span class="stat-unit">tokens</span>';
|
||||
|
||||
cards[2].querySelector('.stat-value').innerHTML = '$' + stats.total_cost.toFixed(4);
|
||||
|
||||
// 模型统计表格
|
||||
const modelHtml = `
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>模型</th>
|
||||
<th>会话数</th>
|
||||
<th>输入Token</th>
|
||||
<th>输出Token</th>
|
||||
<th>成本</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${Object.entries(stats.by_model).map(([model, data]) => `
|
||||
<tr>
|
||||
<td>${getModelBadge(model)}</td>
|
||||
<td>${data.count}</td>
|
||||
<td>${data.input_tokens.toLocaleString()}</td>
|
||||
<td>${data.output_tokens.toLocaleString()}</td>
|
||||
<td>$${data.cost.toFixed(6)}</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
document.getElementById('model-stats').innerHTML = modelHtml;
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Failed to load stats:', err);
|
||||
});
|
||||
}
|
||||
|
||||
function getModelBadge(model) {
|
||||
let cls = 'badge';
|
||||
if (model.includes('Qwen')) cls += ' badge-qwen';
|
||||
else if (model.includes('DeepSeek')) cls += ' badge-deepseek';
|
||||
else if (model.includes('GPT')) cls += ' badge-gpt';
|
||||
else if (model.includes('Claude')) cls += ' badge-claude';
|
||||
return `<span class="${cls}">${model}</span>`;
|
||||
}
|
||||
|
||||
// 初始加载
|
||||
loadSessions();
|
||||
loadStats();
|
||||
|
||||
// 每30秒自动刷新
|
||||
setInterval(() => {
|
||||
loadSessions();
|
||||
loadStats();
|
||||
}, 30000);
|
||||
</script>
|
||||
</body>
|
||||
</html>'''
|
||||
|
||||
def generate_session_html(self, session_id: str) -> str:
|
||||
"""生成Session详情页HTML"""
|
||||
session = self.load_session(session_id)
|
||||
if not session:
|
||||
return f'<html><body><h1>Session not found: {session_id}</h1></body></html>'
|
||||
|
||||
cost = self.calculate_cost(session)
|
||||
|
||||
# 生成对话轮次HTML
|
||||
rounds_html = []
|
||||
for r in session.get('rounds', []):
|
||||
messages_html = ''
|
||||
if r.get('messages'):
|
||||
messages_html = '<div class="messages">'
|
||||
for msg in r['messages'][-5:]: # 最多显示5条
|
||||
role = msg.get('role', 'unknown')
|
||||
content = msg.get('content', '')
|
||||
messages_html += f'<div class="message message-{role}"><strong>[{role}]</strong> {self.escape_html(content)}</div>'
|
||||
messages_html += '</div>'
|
||||
|
||||
tool_calls_html = ''
|
||||
if r.get('tool_calls'):
|
||||
tool_calls_html = '<div class="tool-calls"><strong>🛠️ Tool Calls:</strong><ul>'
|
||||
for tc in r['tool_calls']:
|
||||
func_name = tc.get('function', {}).get('name', 'unknown')
|
||||
tool_calls_html += f'<li>{func_name}()</li>'
|
||||
tool_calls_html += '</ul></div>'
|
||||
|
||||
# Token详情显示
|
||||
token_details_html = ''
|
||||
if r.get('input_token_details') or r.get('output_token_details'):
|
||||
token_details_html = '<div class="token-details"><strong>📊 Token Details:</strong><ul>'
|
||||
if r.get('input_token_details'):
|
||||
token_details_html += f'<li>Input: {r["input_token_details"]}</li>'
|
||||
if r.get('output_token_details'):
|
||||
token_details_html += f'<li>Output: {r["output_token_details"]}</li>'
|
||||
token_details_html += '</ul></div>'
|
||||
|
||||
# Token类型标签
|
||||
token_badges = ''
|
||||
if r.get('cached_tokens', 0) > 0:
|
||||
token_badges += f' <span class="token-badge token-badge-cached">📦 {r["cached_tokens"]:,} cached</span>'
|
||||
if r.get('reasoning_tokens', 0) > 0:
|
||||
token_badges += f' <span class="token-badge token-badge-reasoning">🧠 {r["reasoning_tokens"]:,} reasoning</span>'
|
||||
|
||||
rounds_html.append(f'''
|
||||
<div class="round">
|
||||
<div class="round-header">
|
||||
<span class="round-number">Round {r['round']}</span>
|
||||
<span class="round-time">{r['timestamp']}</span>
|
||||
<span class="round-tokens">{r['input_tokens']:,} in → {r['output_tokens']:,} out{token_badges}</span>
|
||||
</div>
|
||||
{messages_html}
|
||||
{f'<div class="question"><strong>❓ Question:</strong> {self.escape_html(r.get("question", ""))}</div>' if r.get('question') else ''}
|
||||
{f'<div class="answer"><strong>✅ Answer:</strong> {self.escape_html(r.get("answer", ""))}</div>' if r.get('answer') else ''}
|
||||
{f'<div class="reasoning"><strong>🧠 Reasoning:</strong> {self.escape_html(r.get("reasoning", ""))}</div>' if r.get('reasoning') else ''}
|
||||
{tool_calls_html}
|
||||
{token_details_html}
|
||||
</div>
|
||||
''')
|
||||
|
||||
return f'''<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{session_id} - Session Monitor</title>
|
||||
<style>
|
||||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||||
body {{
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
}}
|
||||
.container {{ max-width: 1200px; margin: 0 auto; }}
|
||||
|
||||
header {{
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
margin-bottom: 20px;
|
||||
}}
|
||||
h1 {{ color: #333; margin-bottom: 10px; font-size: 24px; }}
|
||||
.back-link {{ color: #007bff; text-decoration: none; margin-bottom: 10px; display: inline-block; }}
|
||||
.back-link:hover {{ text-decoration: underline; }}
|
||||
|
||||
.info-grid {{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
margin-top: 20px;
|
||||
}}
|
||||
.info-item {{ padding: 10px 0; }}
|
||||
.info-label {{ color: #666; font-size: 14px; }}
|
||||
.info-value {{ color: #333; font-size: 18px; font-weight: 600; margin-top: 4px; }}
|
||||
|
||||
.section {{
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
margin-bottom: 20px;
|
||||
}}
|
||||
h2 {{ color: #333; margin-bottom: 20px; font-size: 20px; }}
|
||||
|
||||
.round {{
|
||||
border-left: 3px solid #007bff;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
}}
|
||||
.round-header {{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 15px;
|
||||
font-size: 14px;
|
||||
}}
|
||||
.round-number {{ font-weight: 600; color: #007bff; }}
|
||||
.round-time {{ color: #666; }}
|
||||
.round-tokens {{ color: #333; }}
|
||||
|
||||
.messages {{ margin: 15px 0; }}
|
||||
.message {{
|
||||
padding: 10px;
|
||||
margin: 5px 0;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}}
|
||||
.message-system {{ background: #fff3cd; }}
|
||||
.message-user {{ background: #d1ecf1; }}
|
||||
.message-assistant {{ background: #d4edda; }}
|
||||
.message-tool {{ background: #e2e3e5; }}
|
||||
|
||||
.question, .answer, .reasoning, .tool-calls {{
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}}
|
||||
.question {{ border-left: 3px solid #ffc107; }}
|
||||
.answer {{ border-left: 3px solid #28a745; }}
|
||||
.reasoning {{ border-left: 3px solid #17a2b8; }}
|
||||
.tool-calls {{ border-left: 3px solid #6c757d; }}
|
||||
.tool-calls ul {{ margin-left: 20px; margin-top: 5px; }}
|
||||
|
||||
.token-details {{
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
border-left: 3px solid #17a2b8;
|
||||
}}
|
||||
.token-details ul {{ margin-left: 20px; margin-top: 5px; color: #666; }}
|
||||
|
||||
.token-badge {{
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
margin-left: 5px;
|
||||
}}
|
||||
.token-badge-cached {{
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}}
|
||||
.token-badge-reasoning {{
|
||||
background: #cce5ff;
|
||||
color: #004085;
|
||||
}}
|
||||
|
||||
.badge {{
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<a href="/" class="back-link">← 返回列表</a>
|
||||
<h1>📊 Session Detail</h1>
|
||||
<p style="color: #666; font-family: monospace; font-size: 14px; margin-top: 10px;">{session_id}</p>
|
||||
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<div class="info-label">模型</div>
|
||||
<div class="info-value"><span class="badge">{session.get('model', 'unknown')}</span></div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">消息数</div>
|
||||
<div class="info-value">{session.get('messages_count', 0)}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">总Token</div>
|
||||
<div class="info-value">{session['total_input_tokens'] + session['total_output_tokens']:,}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">成本</div>
|
||||
<div class="info-value">${cost:.6f}</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="section">
|
||||
<h2>💬 对话记录 ({len(session.get('rounds', []))} 轮)</h2>
|
||||
{"".join(rounds_html) if rounds_html else '<p style="color: #666;">暂无对话记录</p>'}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>'''
|
||||
|
||||
def escape_html(self, text: str) -> str:
|
||||
"""转义HTML特殊字符"""
|
||||
return (text.replace('&', '&')
|
||||
.replace('<', '<')
|
||||
.replace('>', '>')
|
||||
.replace('"', '"')
|
||||
.replace("'", '''))
|
||||
|
||||
def log_message(self, format, *args):
|
||||
"""重写日志方法,简化输出"""
|
||||
pass # 不打印每个请求
|
||||
|
||||
|
||||
def create_handler(data_dir):
|
||||
"""创建带数据目录的处理器"""
|
||||
def handler(*args, **kwargs):
|
||||
return SessionMonitorHandler(*args, data_dir=data_dir, **kwargs)
|
||||
return handler
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Agent Session Monitor - Web Server",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--data-dir',
|
||||
default='./sessions',
|
||||
help='Session数据目录(默认: ./sessions)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--port',
|
||||
type=int,
|
||||
default=8888,
|
||||
help='HTTP服务器端口(默认: 8888)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--host',
|
||||
default='0.0.0.0',
|
||||
help='HTTP服务器地址(默认: 0.0.0.0)'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 检查数据目录是否存在
|
||||
data_dir = Path(args.data_dir)
|
||||
if not data_dir.exists():
|
||||
print(f"❌ Error: Data directory not found: {data_dir}")
|
||||
print(f" Please run main.py first to generate session data.")
|
||||
sys.exit(1)
|
||||
|
||||
# 创建HTTP服务器
|
||||
handler_class = create_handler(args.data_dir)
|
||||
server = HTTPServer((args.host, args.port), handler_class)
|
||||
|
||||
print(f"{'=' * 60}")
|
||||
print(f"🌐 Agent Session Monitor - Web Server")
|
||||
print(f"{'=' * 60}")
|
||||
print()
|
||||
print(f"📂 Data directory: {args.data_dir}")
|
||||
print(f"🌍 Server address: http://{args.host}:{args.port}")
|
||||
print()
|
||||
print(f"✅ Server started. Press Ctrl+C to stop.")
|
||||
print(f"{'=' * 60}")
|
||||
print()
|
||||
|
||||
try:
|
||||
server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n👋 Shutting down server...")
|
||||
server.shutdown()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,139 +0,0 @@
|
||||
---
|
||||
name: higress-auto-router
|
||||
description: "Configure automatic model routing using the get-ai-gateway.sh CLI tool for Higress AI Gateway. Use when: (1) User wants to configure automatic model routing, (2) User mentions 'route to', 'switch model', 'use model when', 'auto routing', (3) User describes scenarios that should trigger specific models, (4) User wants to add, list, or remove routing rules."
|
||||
---
|
||||
|
||||
# Higress Auto Router
|
||||
|
||||
Configure automatic model routing using the get-ai-gateway.sh CLI tool for intelligent model selection based on message content triggers.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Higress AI Gateway running (container name: `higress-ai-gateway`)
|
||||
- get-ai-gateway.sh script downloaded
|
||||
|
||||
## CLI Commands
|
||||
|
||||
### Add a Routing Rule
|
||||
|
||||
```bash
|
||||
./get-ai-gateway.sh route add --model <model-name> --trigger "<trigger-phrases>"
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `--model MODEL` (required): Target model to route to
|
||||
- `--trigger PHRASE`: Trigger phrase(s), separated by `|` (e.g., `"深入思考|deep thinking"`)
|
||||
- `--pattern REGEX`: Custom regex pattern (alternative to `--trigger`)
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
# Route complex reasoning to Claude
|
||||
./get-ai-gateway.sh route add \
|
||||
--model claude-opus-4.5 \
|
||||
--trigger "深入思考|deep thinking"
|
||||
|
||||
# Route coding tasks to Qwen Coder
|
||||
./get-ai-gateway.sh route add \
|
||||
--model qwen-coder \
|
||||
--trigger "写代码|code:|coding:"
|
||||
|
||||
# Route creative writing
|
||||
./get-ai-gateway.sh route add \
|
||||
--model gpt-4o \
|
||||
--trigger "创意写作|creative:"
|
||||
|
||||
# Use custom regex pattern
|
||||
./get-ai-gateway.sh route add \
|
||||
--model deepseek-chat \
|
||||
--pattern "(?i)^(数学题|math:)"
|
||||
```
|
||||
|
||||
### List Routing Rules
|
||||
|
||||
```bash
|
||||
./get-ai-gateway.sh route list
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
Default model: qwen-turbo
|
||||
|
||||
ID Pattern Model
|
||||
----------------------------------------------------------------------
|
||||
0 (?i)^(深入思考|deep thinking) claude-opus-4.5
|
||||
1 (?i)^(写代码|code:|coding:) qwen-coder
|
||||
```
|
||||
|
||||
### Remove a Routing Rule
|
||||
|
||||
```bash
|
||||
./get-ai-gateway.sh route remove --rule-id <id>
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
# Remove rule with ID 0
|
||||
./get-ai-gateway.sh route remove --rule-id 0
|
||||
```
|
||||
|
||||
## Common Trigger Mappings
|
||||
|
||||
| Scenario | Suggested Triggers | Recommended Model |
|
||||
|----------|-------------------|-------------------|
|
||||
| Complex reasoning | `深入思考\|deep thinking` | claude-opus-4.5, o1 |
|
||||
| Coding tasks | `写代码\|code:\|coding:` | qwen-coder, deepseek-coder |
|
||||
| Creative writing | `创意写作\|creative:` | gpt-4o, claude-sonnet |
|
||||
| Translation | `翻译:\|translate:` | gpt-4o, qwen-max |
|
||||
| Math problems | `数学题\|math:` | deepseek-r1, o1-mini |
|
||||
| Quick answers | `快速回答\|quick:` | qwen-turbo, gpt-4o-mini |
|
||||
|
||||
## Usage Flow
|
||||
|
||||
1. **User Request:** "我希望在解决困难问题时路由到claude-opus-4.5"
|
||||
|
||||
2. **Execute CLI:**
|
||||
```bash
|
||||
./get-ai-gateway.sh route add \
|
||||
--model claude-opus-4.5 \
|
||||
--trigger "深入思考|deep thinking"
|
||||
```
|
||||
|
||||
3. **Response to User:**
|
||||
```
|
||||
✅ 自动路由配置完成!
|
||||
|
||||
触发方式:以 "深入思考" 或 "deep thinking" 开头
|
||||
目标模型:claude-opus-4.5
|
||||
|
||||
使用示例:
|
||||
- 深入思考 这道算法题应该怎么解?
|
||||
- deep thinking What's the best architecture?
|
||||
|
||||
提示:确保请求中 model 参数为 'higress/auto'
|
||||
```
|
||||
|
||||
## How Auto-Routing Works
|
||||
|
||||
1. User sends request with `model: "higress/auto"`
|
||||
2. Higress checks message content against routing rules
|
||||
3. If a trigger pattern matches, routes to the specified model
|
||||
4. If no match, uses the default model (e.g., `qwen-turbo`)
|
||||
|
||||
## Configuration File
|
||||
|
||||
Rules are stored in the container at:
|
||||
```
|
||||
/data/wasmplugins/model-router.internal.yaml
|
||||
```
|
||||
|
||||
The CLI tool automatically:
|
||||
- Edits the configuration file
|
||||
- Triggers hot-reload (no container restart needed)
|
||||
- Validates YAML syntax
|
||||
|
||||
## Error Handling
|
||||
|
||||
- **Container not running:** Start with `./get-ai-gateway.sh start`
|
||||
- **Rule ID not found:** Use `route list` to see valid IDs
|
||||
- **Invalid model:** Check configured providers in Higress Console
|
||||
@@ -1,431 +0,0 @@
|
||||
---
|
||||
name: higress-clawdbot-integration
|
||||
description: "Deploy and configure Higress AI Gateway for Clawdbot/OpenClaw integration. Use when: (1) User wants to deploy Higress AI Gateway, (2) User wants to configure Clawdbot/OpenClaw to use Higress as a model provider, (3) User mentions 'higress', 'ai gateway', 'model gateway', 'AI网关', (4) User wants to set up model routing or auto-routing, (5) User needs to manage LLM provider API keys, (6) User wants to track token usage and conversation history."
|
||||
---
|
||||
|
||||
# Higress AI Gateway Integration
|
||||
|
||||
Deploy and configure Higress AI Gateway for Clawdbot/OpenClaw integration with one-click deployment, model provider configuration, auto-routing, and session monitoring.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker installed and running
|
||||
- Internet access to download the setup script
|
||||
- LLM provider API keys (at least one)
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Download Setup Script
|
||||
|
||||
Download the official get-ai-gateway.sh script:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/higress-group/higress-standalone/main/all-in-one/get-ai-gateway.sh -o get-ai-gateway.sh
|
||||
chmod +x get-ai-gateway.sh
|
||||
```
|
||||
|
||||
### Step 2: Gather Configuration
|
||||
|
||||
Ask the user for:
|
||||
|
||||
1. **LLM Provider API Keys** (at least one required):
|
||||
|
||||
**Top Commonly Used Providers:**
|
||||
- Aliyun Dashscope (Qwen): `--dashscope-key`
|
||||
- DeepSeek: `--deepseek-key`
|
||||
- Moonshot (Kimi): `--moonshot-key`
|
||||
- Zhipu AI: `--zhipuai-key`
|
||||
- Minimax: `--minimax-key`
|
||||
- Azure OpenAI: `--azure-key`
|
||||
- AWS Bedrock: `--bedrock-key`
|
||||
- Google Vertex AI: `--vertex-key`
|
||||
- OpenAI: `--openai-key`
|
||||
- OpenRouter: `--openrouter-key`
|
||||
- Grok: `--grok-key`
|
||||
|
||||
See CLI Parameters Reference for complete list with model pattern options.
|
||||
|
||||
2. **Port Configuration** (optional):
|
||||
- HTTP port: `--http-port` (default: 8080)
|
||||
- HTTPS port: `--https-port` (default: 8443)
|
||||
- Console port: `--console-port` (default: 8001)
|
||||
|
||||
3. **Auto-routing** (optional):
|
||||
- Enable: `--auto-routing`
|
||||
- Default model: `--auto-routing-default-model`
|
||||
|
||||
### Step 3: Run Setup Script
|
||||
|
||||
Run the script in non-interactive mode with gathered parameters:
|
||||
|
||||
```bash
|
||||
./get-ai-gateway.sh start --non-interactive \
|
||||
--dashscope-key sk-xxx \
|
||||
--openai-key sk-xxx \
|
||||
--auto-routing \
|
||||
--auto-routing-default-model qwen-turbo
|
||||
```
|
||||
|
||||
**Automatic Repository Selection:**
|
||||
|
||||
The script automatically detects your timezone and selects the geographically closest registry for both:
|
||||
- **Container image** (`IMAGE_REPO`)
|
||||
- **WASM plugins** (`PLUGIN_REGISTRY`)
|
||||
|
||||
| Region | Timezone Examples | Selected Registry |
|
||||
|--------|------------------|-------------------|
|
||||
| China & nearby | Asia/Shanghai, Asia/Hong_Kong, etc. | `higress-registry.cn-hangzhou.cr.aliyuncs.com` |
|
||||
| Southeast Asia | Asia/Singapore, Asia/Jakarta, etc. | `higress-registry.ap-southeast-7.cr.aliyuncs.com` |
|
||||
| North America | America/*, US/*, Canada/* | `higress-registry.us-west-1.cr.aliyuncs.com` |
|
||||
| Others | Default fallback | `higress-registry.cn-hangzhou.cr.aliyuncs.com` |
|
||||
|
||||
**Manual Override (optional):**
|
||||
|
||||
If you want to use a specific registry:
|
||||
|
||||
```bash
|
||||
IMAGE_REPO="higress-registry.ap-southeast-7.cr.aliyuncs.com/higress/all-in-one" \
|
||||
PLUGIN_REGISTRY="higress-registry.ap-southeast-7.cr.aliyuncs.com" \
|
||||
./get-ai-gateway.sh start --non-interactive \
|
||||
--dashscope-key sk-xxx \
|
||||
--openai-key sk-xxx
|
||||
```
|
||||
|
||||
### Step 4: Verify Deployment
|
||||
|
||||
After script completion:
|
||||
|
||||
1. Check container is running:
|
||||
```bash
|
||||
docker ps --filter "name=higress-ai-gateway"
|
||||
```
|
||||
|
||||
2. Test the gateway endpoint:
|
||||
```bash
|
||||
curl http://localhost:8080/v1/models
|
||||
```
|
||||
|
||||
3. Access the console (optional):
|
||||
```
|
||||
http://localhost:8001
|
||||
```
|
||||
|
||||
### Step 5: Configure Clawdbot/OpenClaw Plugin
|
||||
|
||||
If the user wants to use Higress with Clawdbot/OpenClaw, install the appropriate plugin:
|
||||
|
||||
#### Automatic Installation
|
||||
|
||||
Detect runtime and install the correct plugin version:
|
||||
|
||||
```bash
|
||||
# Detect which runtime is installed
|
||||
if command -v clawdbot &> /dev/null; then
|
||||
RUNTIME="clawdbot"
|
||||
RUNTIME_DIR="$HOME/.clawdbot"
|
||||
PLUGIN_SRC="scripts/plugin-clawdbot"
|
||||
elif command -v openclaw &> /dev/null; then
|
||||
RUNTIME="openclaw"
|
||||
RUNTIME_DIR="$HOME/.openclaw"
|
||||
PLUGIN_SRC="scripts/plugin"
|
||||
else
|
||||
echo "Error: Neither clawdbot nor openclaw is installed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Install the plugin
|
||||
PLUGIN_DEST="$RUNTIME_DIR/extensions/higress-ai-gateway"
|
||||
echo "Installing Higress AI Gateway plugin for $RUNTIME..."
|
||||
mkdir -p "$(dirname "$PLUGIN_DEST")"
|
||||
[ -d "$PLUGIN_DEST" ] && rm -rf "$PLUGIN_DEST"
|
||||
cp -r "$PLUGIN_SRC" "$PLUGIN_DEST"
|
||||
echo "✓ Plugin installed at: $PLUGIN_DEST"
|
||||
|
||||
# Configure provider
|
||||
echo
|
||||
echo "Configuring provider..."
|
||||
$RUNTIME models auth login --provider higress
|
||||
```
|
||||
|
||||
The plugin will guide you through an interactive setup for:
|
||||
1. Gateway URL (default: `http://localhost:8080`)
|
||||
2. Console URL (default: `http://localhost:8001`)
|
||||
3. API Key (optional for local deployments)
|
||||
4. Model list (auto-detected or manually specified)
|
||||
5. Auto-routing default model (if using `higress/auto`)
|
||||
|
||||
### Step 6: Manage API Keys (optional)
|
||||
|
||||
After deployment, manage API keys without redeploying:
|
||||
|
||||
```bash
|
||||
# View configured API keys
|
||||
./get-ai-gateway.sh config list
|
||||
|
||||
# Add or update an API key (hot-reload, no restart needed)
|
||||
./get-ai-gateway.sh config add --provider <provider> --key <api-key>
|
||||
|
||||
# Remove an API key (hot-reload, no restart needed)
|
||||
./get-ai-gateway.sh config remove --provider <provider>
|
||||
```
|
||||
|
||||
**Note:** Changes take effect immediately via hot-reload. No container restart required.
|
||||
|
||||
## CLI Parameters Reference
|
||||
|
||||
### Basic Options
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|-----------|-------------|---------|
|
||||
| `--non-interactive` | Run without prompts | - |
|
||||
| `--http-port` | Gateway HTTP port | 8080 |
|
||||
| `--https-port` | Gateway HTTPS port | 8443 |
|
||||
| `--console-port` | Console port | 8001 |
|
||||
| `--container-name` | Container name | higress-ai-gateway |
|
||||
| `--data-folder` | Data folder path | ./higress |
|
||||
| `--auto-routing` | Enable auto-routing feature | - |
|
||||
| `--auto-routing-default-model` | Default model when no rule matches | - |
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `PLUGIN_REGISTRY` | Registry URL for container images and WASM plugins (auto-selected based on timezone) | `higress-registry.cn-hangzhou.cr.aliyuncs.com` |
|
||||
|
||||
**Auto-Selection Logic:**
|
||||
|
||||
The registry is automatically selected based on your timezone:
|
||||
|
||||
- **China & nearby** (Asia/Shanghai, etc.) → `higress-registry.cn-hangzhou.cr.aliyuncs.com`
|
||||
- **Southeast Asia** (Asia/Singapore, etc.) → `higress-registry.ap-southeast-7.cr.aliyuncs.com`
|
||||
- **North America** (America/*, etc.) → `higress-registry.us-west-1.cr.aliyuncs.com`
|
||||
- **Others** → `higress-registry.cn-hangzhou.cr.aliyuncs.com` (default)
|
||||
|
||||
Both container images and WASM plugins use the same registry for consistency.
|
||||
|
||||
**Manual Override:**
|
||||
|
||||
```bash
|
||||
PLUGIN_REGISTRY="higress-registry.ap-southeast-7.cr.aliyuncs.com" \
|
||||
./get-ai-gateway.sh start --non-interactive ...
|
||||
```
|
||||
|
||||
### LLM Provider API Keys
|
||||
|
||||
**Top Providers:**
|
||||
|
||||
| Parameter | Provider |
|
||||
|-----------|----------|
|
||||
| `--dashscope-key` | Aliyun Dashscope (Qwen) |
|
||||
| `--deepseek-key` | DeepSeek |
|
||||
| `--moonshot-key` | Moonshot (Kimi) |
|
||||
| `--zhipuai-key` | Zhipu AI |
|
||||
| `--openai-key` | OpenAI |
|
||||
| `--openrouter-key` | OpenRouter |
|
||||
| `--claude-key` | Claude |
|
||||
| `--gemini-key` | Google Gemini |
|
||||
| `--groq-key` | Groq |
|
||||
|
||||
**Additional Providers:**
|
||||
`--doubao-key`, `--baichuan-key`, `--yi-key`, `--stepfun-key`, `--minimax-key`, `--cohere-key`, `--mistral-key`, `--github-key`, `--fireworks-key`, `--togetherai-key`, `--grok-key`, `--azure-key`, `--bedrock-key`, `--vertex-key`
|
||||
|
||||
## Managing Configuration
|
||||
|
||||
### API Keys
|
||||
|
||||
```bash
|
||||
# List all configured API keys
|
||||
./get-ai-gateway.sh config list
|
||||
|
||||
# Add or update an API key (hot-reload)
|
||||
./get-ai-gateway.sh config add --provider deepseek --key sk-xxx
|
||||
|
||||
# Remove an API key (hot-reload)
|
||||
./get-ai-gateway.sh config remove --provider deepseek
|
||||
```
|
||||
|
||||
**Supported provider aliases:**
|
||||
`dashscope`/`qwen`, `moonshot`/`kimi`, `zhipuai`/`zhipu`, `togetherai`/`together`
|
||||
|
||||
### Routing Rules
|
||||
|
||||
```bash
|
||||
# Add a routing rule
|
||||
./get-ai-gateway.sh route add --model claude-opus-4.5 --trigger "深入思考|deep thinking"
|
||||
|
||||
# List all rules
|
||||
./get-ai-gateway.sh route list
|
||||
|
||||
# Remove a rule
|
||||
./get-ai-gateway.sh route remove --rule-id 0
|
||||
```
|
||||
|
||||
See [higress-auto-router](../higress-auto-router/SKILL.md) for detailed documentation.
|
||||
|
||||
## Access Logs
|
||||
|
||||
Gateway access logs are available at:
|
||||
```
|
||||
$DATA_FOLDER/logs/access.log
|
||||
```
|
||||
|
||||
These logs can be used with the **agent-session-monitor** skill for token tracking and conversation analysis.
|
||||
|
||||
## Related Skills
|
||||
|
||||
- **higress-auto-router**: Configure automatic model routing using CLI commands
|
||||
See: [higress-auto-router](../higress-auto-router/SKILL.md)
|
||||
|
||||
- **agent-session-monitor**: Monitor and track token usage across sessions
|
||||
See: [agent-session-monitor](../agent-session-monitor/SKILL.md)
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Basic Deployment with Dashscope
|
||||
|
||||
**User:** 帮我部署一个Higress AI网关,使用阿里云的通义千问
|
||||
|
||||
**Steps:**
|
||||
1. Download script
|
||||
2. Get Dashscope API key from user
|
||||
3. Run (script auto-detects timezone and selects optimal registry):
|
||||
```bash
|
||||
./get-ai-gateway.sh start --non-interactive \
|
||||
--dashscope-key sk-xxx
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```
|
||||
Auto-detected timezone: Asia/Shanghai
|
||||
Selected plugin registry: higress-registry.cn-hangzhou.cr.aliyuncs.com
|
||||
|
||||
✅ Higress AI Gateway 部署完成!
|
||||
|
||||
网关地址: http://localhost:8080/v1/chat/completions
|
||||
控制台: http://localhost:8001
|
||||
日志目录: ./higress/logs
|
||||
|
||||
已配置的模型提供商:
|
||||
- Aliyun Dashscope (Qwen)
|
||||
|
||||
测试命令:
|
||||
curl 'http://localhost:8080/v1/chat/completions' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"model": "qwen-turbo", "messages": [{"role": "user", "content": "Hello!"}]}'
|
||||
```
|
||||
|
||||
### Example 2: Full Integration with Clawdbot
|
||||
|
||||
**User:** 完整配置Higress和Clawdbot的集成
|
||||
|
||||
**Steps:**
|
||||
1. Deploy Higress AI Gateway (auto-detects timezone)
|
||||
2. Install and configure Clawdbot plugin
|
||||
3. Enable auto-routing
|
||||
4. Set up session monitoring
|
||||
|
||||
**Response:**
|
||||
```
|
||||
Auto-detected timezone: Asia/Shanghai
|
||||
Selected plugin registry: higress-registry.cn-hangzhou.cr.aliyuncs.com
|
||||
|
||||
✅ Higress AI Gateway 集成完成!
|
||||
|
||||
1. 网关已部署:
|
||||
- HTTP: http://localhost:8080
|
||||
- Console: http://localhost:8001
|
||||
- 容器镜像: Hangzhou (自动选择)
|
||||
- 插件镜像: Hangzhou (自动选择)
|
||||
|
||||
2. Clawdbot 插件配置:
|
||||
Plugin installed at: /root/.clawdbot/extensions/higress-ai-gateway
|
||||
Run: clawdbot models auth login --provider higress
|
||||
|
||||
3. 自动路由:
|
||||
已启用,使用 model="higress/auto"
|
||||
|
||||
4. 会话监控:
|
||||
日志路径: ./higress/logs/access.log
|
||||
|
||||
需要我帮你配置自动路由规则吗?
|
||||
```
|
||||
|
||||
### Example 3: Manage API Keys
|
||||
|
||||
**User:** 帮我查看当前配置的API keys,并添加一个DeepSeek的key
|
||||
|
||||
**Steps:**
|
||||
1. List current API keys:
|
||||
```bash
|
||||
./get-ai-gateway.sh config list
|
||||
```
|
||||
|
||||
2. Add DeepSeek API key:
|
||||
```bash
|
||||
./get-ai-gateway.sh config add --provider deepseek --key sk-xxx
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```
|
||||
当前配置的API keys:
|
||||
|
||||
Aliyun Dashscope (Qwen): sk-ab***ef12
|
||||
OpenAI: sk-cd***gh34
|
||||
|
||||
Adding API key for DeepSeek...
|
||||
|
||||
✅ API key updated successfully!
|
||||
|
||||
Provider: DeepSeek
|
||||
Key: sk-xx***yy56
|
||||
|
||||
Configuration has been hot-reloaded (no restart needed).
|
||||
```
|
||||
|
||||
### Example 4: North America Deployment
|
||||
|
||||
**User:** 帮我部署Higress AI网关
|
||||
|
||||
**Context:** User's timezone is America/Los_Angeles
|
||||
|
||||
**Steps:**
|
||||
1. Download script
|
||||
2. Get API keys from user
|
||||
3. Run (script auto-detects timezone and selects North America mirror):
|
||||
```bash
|
||||
./get-ai-gateway.sh start --non-interactive \
|
||||
--openai-key sk-xxx \
|
||||
--openrouter-key sk-xxx
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```
|
||||
Auto-detected timezone: America/Los_Angeles
|
||||
Selected plugin registry: higress-registry.us-west-1.cr.aliyuncs.com
|
||||
|
||||
✅ Higress AI Gateway 部署完成!
|
||||
|
||||
网关地址: http://localhost:8080/v1/chat/completions
|
||||
控制台: http://localhost:8001
|
||||
日志目录: ./higress/logs
|
||||
|
||||
镜像优化:
|
||||
- 容器镜像: North America (基于时区自动选择)
|
||||
- 插件镜像: North America (基于时区自动选择)
|
||||
|
||||
已配置的模型提供商:
|
||||
- OpenAI
|
||||
- OpenRouter
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
For detailed troubleshooting guides, see [TROUBLESHOOTING.md](references/TROUBLESHOOTING.md).
|
||||
|
||||
Common issues:
|
||||
- **Container fails to start**: Check Docker status, port availability, and container logs
|
||||
- **"too many open files" error**: Increase `fs.inotify.max_user_instances` to 8192
|
||||
- **Gateway not responding**: Verify container status and port mapping
|
||||
- **Plugin not recognized**: Check installation path and restart runtime
|
||||
- **Auto-routing not working**: Verify model list and routing rules
|
||||
- **Timezone detection fails**: Manually set `IMAGE_REPO` environment variable
|
||||
@@ -1,325 +0,0 @@
|
||||
# Higress AI Gateway - Troubleshooting
|
||||
|
||||
Common issues and solutions for Higress AI Gateway deployment and operation.
|
||||
|
||||
## Container Issues
|
||||
|
||||
### Container fails to start
|
||||
|
||||
**Check Docker is running:**
|
||||
```bash
|
||||
docker info
|
||||
```
|
||||
|
||||
**Check port availability:**
|
||||
```bash
|
||||
netstat -tlnp | grep 8080
|
||||
```
|
||||
|
||||
**View container logs:**
|
||||
```bash
|
||||
docker logs higress-ai-gateway
|
||||
```
|
||||
|
||||
### Gateway not responding
|
||||
|
||||
**Check container status:**
|
||||
```bash
|
||||
docker ps -a
|
||||
```
|
||||
|
||||
**Verify port mapping:**
|
||||
```bash
|
||||
docker port higress-ai-gateway
|
||||
```
|
||||
|
||||
**Test locally:**
|
||||
```bash
|
||||
curl http://localhost:8080/v1/models
|
||||
```
|
||||
|
||||
## File System Issues
|
||||
|
||||
### "too many open files" error from API server
|
||||
|
||||
**Symptom:**
|
||||
```
|
||||
panic: unable to create REST storage for a resource due to too many open files, will die
|
||||
```
|
||||
or
|
||||
```
|
||||
command failed err="failed to create shared file watcher: too many open files"
|
||||
```
|
||||
|
||||
**Root Cause:**
|
||||
|
||||
The system's `fs.inotify.max_user_instances` limit is too low. This commonly occurs on systems with many Docker containers, as each container can consume inotify instances.
|
||||
|
||||
**Check current limit:**
|
||||
```bash
|
||||
cat /proc/sys/fs/inotify/max_user_instances
|
||||
```
|
||||
|
||||
Default is often 128, which is insufficient when running multiple containers.
|
||||
|
||||
**Solution:**
|
||||
|
||||
Increase the inotify instance limit to 8192:
|
||||
|
||||
```bash
|
||||
# Temporarily (until next reboot)
|
||||
sudo sysctl -w fs.inotify.max_user_instances=8192
|
||||
|
||||
# Permanently (survives reboots)
|
||||
echo "fs.inotify.max_user_instances = 8192" | sudo tee -a /etc/sysctl.conf
|
||||
sudo sysctl -p
|
||||
```
|
||||
|
||||
**Verify:**
|
||||
```bash
|
||||
cat /proc/sys/fs/inotify/max_user_instances
|
||||
# Should output: 8192
|
||||
```
|
||||
|
||||
**Restart the container:**
|
||||
```bash
|
||||
docker restart higress-ai-gateway
|
||||
```
|
||||
|
||||
**Additional inotify tunables** (if still experiencing issues):
|
||||
```bash
|
||||
# Increase max watches per user
|
||||
sudo sysctl -w fs.inotify.max_user_watches=524288
|
||||
|
||||
# Increase max queued events
|
||||
sudo sysctl -w fs.inotify.max_queued_events=32768
|
||||
```
|
||||
|
||||
To make these permanent as well:
|
||||
```bash
|
||||
echo "fs.inotify.max_user_watches = 524288" | sudo tee -a /etc/sysctl.conf
|
||||
echo "fs.inotify.max_queued_events = 32768" | sudo tee -a /etc/sysctl.conf
|
||||
sudo sysctl -p
|
||||
```
|
||||
|
||||
## Plugin Issues
|
||||
|
||||
### Plugin not recognized
|
||||
|
||||
**Verify plugin installation:**
|
||||
|
||||
For Clawdbot:
|
||||
```bash
|
||||
ls -la ~/.clawdbot/extensions/higress-ai-gateway
|
||||
```
|
||||
|
||||
For OpenClaw:
|
||||
```bash
|
||||
ls -la ~/.openclaw/extensions/higress-ai-gateway
|
||||
```
|
||||
|
||||
**Check package.json:**
|
||||
|
||||
Ensure `package.json` contains the correct extension field:
|
||||
- Clawdbot: `"clawdbot.extensions"`
|
||||
- OpenClaw: `"openclaw.extensions"`
|
||||
|
||||
**Restart the runtime:**
|
||||
```bash
|
||||
# Restart Clawdbot gateway
|
||||
clawdbot gateway restart
|
||||
|
||||
# Or OpenClaw gateway
|
||||
openclaw gateway restart
|
||||
```
|
||||
|
||||
## Routing Issues
|
||||
|
||||
### Auto-routing not working
|
||||
|
||||
**Confirm model is in list:**
|
||||
```bash
|
||||
# Check if higress/auto is available
|
||||
clawdbot models list | grep "higress/auto"
|
||||
```
|
||||
|
||||
**Check routing rules exist:**
|
||||
```bash
|
||||
./get-ai-gateway.sh route list
|
||||
```
|
||||
|
||||
**Verify default model is configured:**
|
||||
```bash
|
||||
./get-ai-gateway.sh config list
|
||||
```
|
||||
|
||||
**Check gateway logs:**
|
||||
```bash
|
||||
docker logs higress-ai-gateway | grep -i routing
|
||||
```
|
||||
|
||||
**View access logs:**
|
||||
```bash
|
||||
tail -f ./higress/logs/access.log
|
||||
```
|
||||
|
||||
## Configuration Issues
|
||||
|
||||
### Timezone detection fails
|
||||
|
||||
**Manually check timezone:**
|
||||
```bash
|
||||
timedatectl show --property=Timezone --value
|
||||
```
|
||||
|
||||
**Or check timezone file:**
|
||||
```bash
|
||||
cat /etc/timezone
|
||||
```
|
||||
|
||||
**Fallback behavior:**
|
||||
- If detection fails, defaults to Hangzhou mirror
|
||||
- Manual override: Set `IMAGE_REPO` environment variable
|
||||
|
||||
**Manual repository selection:**
|
||||
```bash
|
||||
# For China/Asia
|
||||
IMAGE_REPO="higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/all-in-one"
|
||||
|
||||
# For Southeast Asia
|
||||
IMAGE_REPO="higress-registry.ap-southeast-7.cr.aliyuncs.com/higress/all-in-one"
|
||||
|
||||
# For North America
|
||||
IMAGE_REPO="higress-registry.us-west-1.cr.aliyuncs.com/higress/all-in-one"
|
||||
|
||||
# Use in deployment
|
||||
IMAGE_REPO="$IMAGE_REPO" ./get-ai-gateway.sh start --non-interactive ...
|
||||
```
|
||||
|
||||
## Performance Issues
|
||||
|
||||
### Slow image downloads
|
||||
|
||||
**Check selected repository:**
|
||||
```bash
|
||||
echo $IMAGE_REPO
|
||||
```
|
||||
|
||||
**Manually select closest mirror:**
|
||||
|
||||
See [Configuration Issues → Timezone detection fails](#timezone-detection-fails) for manual repository selection.
|
||||
|
||||
### High memory usage
|
||||
|
||||
**Check container stats:**
|
||||
```bash
|
||||
docker stats higress-ai-gateway
|
||||
```
|
||||
|
||||
**View resource limits:**
|
||||
```bash
|
||||
docker inspect higress-ai-gateway | grep -A 10 "HostConfig"
|
||||
```
|
||||
|
||||
**Set memory limits:**
|
||||
```bash
|
||||
# Stop container
|
||||
./get-ai-gateway.sh stop
|
||||
|
||||
# Manually restart with limits
|
||||
docker run -d \
|
||||
--name higress-ai-gateway \
|
||||
--memory="4g" \
|
||||
--memory-swap="4g" \
|
||||
...
|
||||
```
|
||||
|
||||
## Log Analysis
|
||||
|
||||
### Access logs location
|
||||
|
||||
```bash
|
||||
# Default location
|
||||
./higress/logs/access.log
|
||||
|
||||
# View real-time logs
|
||||
tail -f ./higress/logs/access.log
|
||||
```
|
||||
|
||||
### Container logs
|
||||
|
||||
```bash
|
||||
# View all logs
|
||||
docker logs higress-ai-gateway
|
||||
|
||||
# Follow logs
|
||||
docker logs -f higress-ai-gateway
|
||||
|
||||
# Last 100 lines
|
||||
docker logs --tail 100 higress-ai-gateway
|
||||
|
||||
# With timestamps
|
||||
docker logs -t higress-ai-gateway
|
||||
```
|
||||
|
||||
## Network Issues
|
||||
|
||||
### Cannot connect to gateway
|
||||
|
||||
**Verify container is running:**
|
||||
```bash
|
||||
docker ps | grep higress-ai-gateway
|
||||
```
|
||||
|
||||
**Check port bindings:**
|
||||
```bash
|
||||
docker port higress-ai-gateway
|
||||
```
|
||||
|
||||
**Test from inside container:**
|
||||
```bash
|
||||
docker exec higress-ai-gateway curl localhost:8080/v1/models
|
||||
```
|
||||
|
||||
**Check firewall rules:**
|
||||
```bash
|
||||
# Check if port is accessible
|
||||
sudo ufw status | grep 8080
|
||||
|
||||
# Allow port (if needed)
|
||||
sudo ufw allow 8080/tcp
|
||||
```
|
||||
|
||||
### DNS resolution issues
|
||||
|
||||
**Test from container:**
|
||||
```bash
|
||||
docker exec higress-ai-gateway ping -c 3 api.openai.com
|
||||
```
|
||||
|
||||
**Check DNS settings:**
|
||||
```bash
|
||||
docker exec higress-ai-gateway cat /etc/resolv.conf
|
||||
```
|
||||
|
||||
## Getting Help
|
||||
|
||||
If you're still experiencing issues:
|
||||
|
||||
1. **Collect logs:**
|
||||
```bash
|
||||
docker logs higress-ai-gateway > gateway.log 2>&1
|
||||
cat ./higress/logs/access.log > access.log
|
||||
```
|
||||
|
||||
2. **Check system info:**
|
||||
```bash
|
||||
docker version
|
||||
docker info
|
||||
uname -a
|
||||
cat /proc/sys/fs/inotify/max_user_instances
|
||||
```
|
||||
|
||||
3. **Report issue:**
|
||||
- Repository: https://github.com/higress-group/higress-standalone
|
||||
- Include: logs, system info, deployment command used
|
||||
@@ -1,79 +0,0 @@
|
||||
# Higress AI Gateway Plugin (Clawdbot)
|
||||
|
||||
Clawdbot model provider plugin for Higress AI Gateway with auto-routing support.
|
||||
|
||||
## What is this?
|
||||
|
||||
This is a TypeScript-based provider plugin that enables Clawdbot to use Higress AI Gateway as a model provider. It provides:
|
||||
|
||||
- **Auto-routing support**: Use `higress/auto` to intelligently route requests based on message content
|
||||
- **Dynamic model discovery**: Auto-detect available models from Higress Console
|
||||
- **Smart URL handling**: Automatic URL normalization and validation
|
||||
- **Flexible authentication**: Support for both local and remote gateway deployments
|
||||
|
||||
## Files
|
||||
|
||||
- **index.ts**: Main plugin implementation
|
||||
- **package.json**: NPM package metadata and Clawdbot extension declaration
|
||||
- **clawdbot.plugin.json**: Plugin manifest for Clawdbot
|
||||
|
||||
## Installation
|
||||
|
||||
This plugin is automatically installed when you use the `higress-clawdbot-integration` skill. See the parent SKILL.md for complete installation instructions.
|
||||
|
||||
### Manual Installation
|
||||
|
||||
If you need to install manually:
|
||||
|
||||
```bash
|
||||
# Copy plugin files
|
||||
mkdir -p "$HOME/.clawdbot/extensions/higress-ai-gateway"
|
||||
cp -r ./* "$HOME/.clawdbot/extensions/higress-ai-gateway/"
|
||||
|
||||
# Configure provider
|
||||
clawdbot models auth login --provider higress
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
After installation, configure Higress as a model provider:
|
||||
|
||||
```bash
|
||||
clawdbot models auth login --provider higress
|
||||
```
|
||||
|
||||
The plugin will prompt for:
|
||||
1. Gateway URL (default: http://localhost:8080)
|
||||
2. Console URL (default: http://localhost:8001)
|
||||
3. API Key (optional for local deployments)
|
||||
4. Model list (auto-detected or manually specified)
|
||||
5. Auto-routing default model (if using higress/auto)
|
||||
|
||||
## Auto-routing
|
||||
|
||||
To use auto-routing, include `higress/auto` in your model list during configuration. Then use it in your conversations:
|
||||
|
||||
```bash
|
||||
# Use auto-routing
|
||||
clawdbot chat --model higress/auto "深入思考 这个问题应该怎么解决?"
|
||||
|
||||
# The gateway will automatically route to the appropriate model based on:
|
||||
# - Message content triggers (configured via higress-auto-router skill)
|
||||
# - Fallback to default model if no rule matches
|
||||
```
|
||||
|
||||
## Related Resources
|
||||
|
||||
- **Parent Skill**: [higress-clawdbot-integration](../SKILL.md)
|
||||
- **Auto-routing Configuration**: [higress-auto-router](../../higress-auto-router/SKILL.md)
|
||||
- **Session Monitoring**: [agent-session-monitor](../../agent-session-monitor/SKILL.md)
|
||||
- **Higress AI Gateway**: https://github.com/higress-group/higress-standalone
|
||||
|
||||
## Compatibility
|
||||
|
||||
- **Clawdbot**: v2.0.0+
|
||||
- **Higress AI Gateway**: All versions
|
||||
|
||||
## License
|
||||
|
||||
Apache-2.0
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"id": "higress-ai-gateway",
|
||||
"name": "Higress AI Gateway",
|
||||
"description": "Model provider plugin for Higress AI Gateway with auto-routing support",
|
||||
"providers": ["higress"],
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
@@ -1,284 +0,0 @@
|
||||
import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk";
|
||||
|
||||
const DEFAULT_GATEWAY_URL = "http://localhost:8080";
|
||||
const DEFAULT_CONSOLE_URL = "http://localhost:8001";
|
||||
const DEFAULT_CONTEXT_WINDOW = 128_000;
|
||||
const DEFAULT_MAX_TOKENS = 8192;
|
||||
|
||||
// Common models that Higress AI Gateway typically supports
|
||||
const DEFAULT_MODEL_IDS = [
|
||||
// Auto-routing special model
|
||||
"higress/auto",
|
||||
// OpenAI models
|
||||
"gpt-5.2",
|
||||
"gpt-5-mini",
|
||||
"gpt-5-nano",
|
||||
// Anthropic models
|
||||
"claude-opus-4.5",
|
||||
"claude-sonnet-4.5",
|
||||
"claude-haiku-4.5",
|
||||
// Qwen models
|
||||
"qwen3-turbo",
|
||||
"qwen3-plus",
|
||||
"qwen3-max",
|
||||
"qwen3-coder-480b-a35b-instruct",
|
||||
// DeepSeek models
|
||||
"deepseek-chat",
|
||||
"deepseek-reasoner",
|
||||
// Other common models
|
||||
"kimi-k2.5",
|
||||
"glm-4.7",
|
||||
"MiniMax-M2.1",
|
||||
] as const;
|
||||
|
||||
function normalizeBaseUrl(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return DEFAULT_GATEWAY_URL;
|
||||
let normalized = trimmed;
|
||||
while (normalized.endsWith("/")) normalized = normalized.slice(0, -1);
|
||||
if (!normalized.endsWith("/v1")) normalized = `${normalized}/v1`;
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function validateUrl(value: string): string | undefined {
|
||||
const normalized = normalizeBaseUrl(value);
|
||||
try {
|
||||
new URL(normalized);
|
||||
} catch {
|
||||
return "Enter a valid URL";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function parseModelIds(input: string): string[] {
|
||||
const parsed = input
|
||||
.split(/[\n,]/)
|
||||
.map((model) => model.trim())
|
||||
.filter(Boolean);
|
||||
return Array.from(new Set(parsed));
|
||||
}
|
||||
|
||||
function buildModelDefinition(modelId: string) {
|
||||
const isAutoModel = modelId === "higress/auto";
|
||||
return {
|
||||
id: modelId,
|
||||
name: isAutoModel ? "Higress Auto Router" : modelId,
|
||||
api: "openai-completions",
|
||||
reasoning: false,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: DEFAULT_CONTEXT_WINDOW,
|
||||
maxTokens: DEFAULT_MAX_TOKENS,
|
||||
};
|
||||
}
|
||||
|
||||
async function testGatewayConnection(gatewayUrl: string): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${gatewayUrl}/v1/models`, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
return response.ok || response.status === 401; // 401 means gateway is up but needs auth
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAvailableModels(consoleUrl: string): Promise<string[]> {
|
||||
try {
|
||||
// Try to get models from Higress Console API
|
||||
const response = await fetch(`${consoleUrl}/v1/ai/routes`, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = (await response.json()) as { data?: { model?: string }[] };
|
||||
if (data.data && Array.isArray(data.data)) {
|
||||
return data.data
|
||||
.map((route: { model?: string }) => route.model)
|
||||
.filter((m): m is string => typeof m === "string");
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors, use defaults
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
const higressPlugin = {
|
||||
id: "higress-ai-gateway",
|
||||
name: "Higress AI Gateway",
|
||||
description: "Model provider plugin for Higress AI Gateway with auto-routing support",
|
||||
configSchema: emptyPluginConfigSchema(),
|
||||
register(api) {
|
||||
api.registerProvider({
|
||||
id: "higress",
|
||||
label: "Higress AI Gateway",
|
||||
docsPath: "/providers/models",
|
||||
aliases: ["higress-gateway", "higress-ai"],
|
||||
auth: [
|
||||
{
|
||||
id: "api-key",
|
||||
label: "API Key",
|
||||
hint: "Configure Higress AI Gateway endpoint with optional API key",
|
||||
kind: "custom",
|
||||
run: async (ctx) => {
|
||||
// Step 1: Get Gateway URL
|
||||
const gatewayUrlInput = await ctx.prompter.text({
|
||||
message: "Higress AI Gateway URL",
|
||||
initialValue: DEFAULT_GATEWAY_URL,
|
||||
validate: validateUrl,
|
||||
});
|
||||
const gatewayUrl = normalizeBaseUrl(gatewayUrlInput);
|
||||
|
||||
// Step 2: Get Console URL (for auto-router configuration)
|
||||
const consoleUrlInput = await ctx.prompter.text({
|
||||
message: "Higress Console URL (for auto-router config)",
|
||||
initialValue: DEFAULT_CONSOLE_URL,
|
||||
validate: validateUrl,
|
||||
});
|
||||
const consoleUrl = normalizeBaseUrl(consoleUrlInput);
|
||||
|
||||
// Step 3: Test connection (create a new spinner)
|
||||
const spin = ctx.prompter.progress("Testing gateway connection…");
|
||||
const isConnected = await testGatewayConnection(gatewayUrl);
|
||||
if (!isConnected) {
|
||||
spin.stop("Gateway connection failed");
|
||||
await ctx.prompter.note(
|
||||
[
|
||||
"Could not connect to Higress AI Gateway.",
|
||||
"Make sure the gateway is running and the URL is correct.",
|
||||
"",
|
||||
`Tried: ${gatewayUrl}/v1/models`,
|
||||
].join("\n"),
|
||||
"Connection Warning",
|
||||
);
|
||||
} else {
|
||||
spin.stop("Gateway connected");
|
||||
}
|
||||
|
||||
// Step 4: Get API Key (optional for local gateway)
|
||||
const apiKeyInput = await ctx.prompter.text({
|
||||
message: "API Key (leave empty if not required)",
|
||||
initialValue: "",
|
||||
}) || '';
|
||||
const apiKey = apiKeyInput.trim() || "higress-local";
|
||||
|
||||
// Step 5: Fetch available models (create a new spinner)
|
||||
const spin2 = ctx.prompter.progress("Fetching available models…");
|
||||
const fetchedModels = await fetchAvailableModels(consoleUrl);
|
||||
const defaultModels = fetchedModels.length > 0
|
||||
? ["higress/auto", ...fetchedModels]
|
||||
: DEFAULT_MODEL_IDS;
|
||||
spin2.stop();
|
||||
|
||||
// Step 6: Let user customize model list
|
||||
const modelInput = await ctx.prompter.text({
|
||||
message: "Model IDs (comma-separated, higress/auto enables auto-routing)",
|
||||
initialValue: defaultModels.slice(0, 10).join(", "),
|
||||
validate: (value) =>
|
||||
parseModelIds(value).length > 0 ? undefined : "Enter at least one model id",
|
||||
});
|
||||
|
||||
const modelIds = parseModelIds(modelInput);
|
||||
const hasAutoModel = modelIds.includes("higress/auto");
|
||||
|
||||
// FIX: Avoid double prefix - if modelId already starts with provider, don't add prefix again
|
||||
const defaultModelId = hasAutoModel
|
||||
? "higress/auto"
|
||||
: (modelIds[0] ?? "qwen-turbo");
|
||||
const defaultModelRef = defaultModelId.startsWith("higress/")
|
||||
? defaultModelId
|
||||
: `higress/${defaultModelId}`;
|
||||
|
||||
// Step 7: Configure default model for auto-routing
|
||||
let autoRoutingDefaultModel = "qwen-turbo";
|
||||
if (hasAutoModel) {
|
||||
const autoRoutingModelInput = await ctx.prompter.text({
|
||||
message: "Default model for auto-routing (when no rule matches)",
|
||||
initialValue: "qwen-turbo",
|
||||
});
|
||||
autoRoutingDefaultModel = autoRoutingModelInput.trim(); // FIX: Add trim() here
|
||||
}
|
||||
|
||||
return {
|
||||
profiles: [
|
||||
{
|
||||
profileId: `higress:${apiKey === "higress-local" ? "local" : "default"}`,
|
||||
credential: {
|
||||
type: "token",
|
||||
provider: "higress",
|
||||
token: apiKey,
|
||||
},
|
||||
},
|
||||
],
|
||||
configPatch: {
|
||||
models: {
|
||||
providers: {
|
||||
higress: {
|
||||
baseUrl: `${gatewayUrl}/v1`,
|
||||
apiKey: apiKey,
|
||||
api: "openai-completions",
|
||||
authHeader: apiKey !== "higress-local",
|
||||
models: modelIds.map((modelId) => buildModelDefinition(modelId)),
|
||||
},
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
defaults: {
|
||||
models: Object.fromEntries(
|
||||
modelIds.map((modelId) => {
|
||||
// FIX: Avoid double prefix - only add provider prefix if not already present
|
||||
const modelRef = modelId.startsWith("higress/")
|
||||
? modelId
|
||||
: `higress/${modelId}`;
|
||||
return [modelRef, {}];
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
"higress-ai-gateway": {
|
||||
enabled: true,
|
||||
config: {
|
||||
gatewayUrl,
|
||||
consoleUrl,
|
||||
autoRoutingDefaultModel,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultModel: defaultModelRef,
|
||||
notes: [
|
||||
"Higress AI Gateway is now configured as a model provider.",
|
||||
hasAutoModel
|
||||
? `Auto-routing enabled: use model "higress/auto" to route based on message content.`
|
||||
: "Add 'higress/auto' to models to enable auto-routing.",
|
||||
`Gateway endpoint: ${gatewayUrl}/v1/chat/completions`,
|
||||
`Console: ${consoleUrl}`,
|
||||
"",
|
||||
"🎯 Recommended Skills (install via Clawdbot conversation):",
|
||||
"",
|
||||
"1. Auto-Routing Skill:",
|
||||
" Configure automatic model routing based on message content",
|
||||
" https://github.com/alibaba/higress/tree/main/.claude/skills/higress-auto-router",
|
||||
' Say: "Install higress-auto-router skill"',
|
||||
"",
|
||||
"2. Agent Session Monitor Skill:",
|
||||
" Track token usage and monitor conversation history",
|
||||
" https://github.com/alibaba/higress/tree/main/.claude/skills/agent-session-monitor",
|
||||
' Say: "Install agent-session-monitor skill"',
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default higressPlugin;
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"name": "@higress/higress-ai-gateway",
|
||||
"version": "1.0.0",
|
||||
"description": "Higress AI Gateway model provider plugin for Clawdbot with auto-routing support",
|
||||
"main": "index.ts",
|
||||
"clawdbot": {
|
||||
"extensions": ["./index.ts"]
|
||||
},
|
||||
"keywords": [
|
||||
"clawdbot",
|
||||
"higress",
|
||||
"ai-gateway",
|
||||
"model-router",
|
||||
"auto-routing"
|
||||
],
|
||||
"author": "Higress Team",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/alibaba/higress"
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
# Higress AI Gateway Plugin
|
||||
|
||||
OpenClaw/Clawdbot model provider plugin for Higress AI Gateway with auto-routing support.
|
||||
|
||||
## What is this?
|
||||
|
||||
This is a TypeScript-based provider plugin that enables Clawdbot and OpenClaw to use Higress AI Gateway as a model provider. It provides:
|
||||
|
||||
- **Auto-routing support**: Use `higress/auto` to intelligently route requests based on message content
|
||||
- **Dynamic model discovery**: Auto-detect available models from Higress Console
|
||||
- **Smart URL handling**: Automatic URL normalization and validation
|
||||
- **Flexible authentication**: Support for both local and remote gateway deployments
|
||||
|
||||
## Files
|
||||
|
||||
- **index.ts**: Main plugin implementation
|
||||
- **package.json**: NPM package metadata and OpenClaw extension declaration
|
||||
- **openclaw.plugin.json**: Plugin manifest for OpenClaw
|
||||
|
||||
## Installation
|
||||
|
||||
This plugin is automatically installed when you use the `higress-clawdbot-integration` skill. See the parent SKILL.md for complete installation instructions.
|
||||
|
||||
### Manual Installation
|
||||
|
||||
If you need to install manually:
|
||||
|
||||
```bash
|
||||
# Detect runtime
|
||||
if command -v clawdbot &> /dev/null; then
|
||||
RUNTIME_DIR="$HOME/.clawdbot"
|
||||
elif command -v openclaw &> /dev/null; then
|
||||
RUNTIME_DIR="$HOME/.openclaw"
|
||||
else
|
||||
echo "Error: Neither clawdbot nor openclaw is installed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Copy plugin files
|
||||
mkdir -p "$RUNTIME_DIR/extensions/higress-ai-gateway"
|
||||
cp -r ./* "$RUNTIME_DIR/extensions/higress-ai-gateway/"
|
||||
|
||||
# Configure provider
|
||||
clawdbot models auth login --provider higress
|
||||
# or
|
||||
openclaw models auth login --provider higress
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
After installation, configure Higress as a model provider:
|
||||
|
||||
```bash
|
||||
clawdbot models auth login --provider higress
|
||||
```
|
||||
|
||||
The plugin will prompt for:
|
||||
1. Gateway URL (default: http://localhost:8080)
|
||||
2. Console URL (default: http://localhost:8001)
|
||||
3. API Key (optional for local deployments)
|
||||
4. Model list (auto-detected or manually specified)
|
||||
5. Auto-routing default model (if using higress/auto)
|
||||
|
||||
## Auto-routing
|
||||
|
||||
To use auto-routing, include `higress/auto` in your model list during configuration. Then use it in your conversations:
|
||||
|
||||
```bash
|
||||
# Use auto-routing
|
||||
clawdbot chat --model higress/auto "深入思考 这个问题应该怎么解决?"
|
||||
|
||||
# The gateway will automatically route to the appropriate model based on:
|
||||
# - Message content triggers (configured via higress-auto-router skill)
|
||||
# - Fallback to default model if no rule matches
|
||||
```
|
||||
|
||||
## Related Resources
|
||||
|
||||
- **Parent Skill**: [higress-clawdbot-integration](../SKILL.md)
|
||||
- **Auto-routing Configuration**: [higress-auto-router](../../higress-auto-router/SKILL.md)
|
||||
- **Session Monitoring**: [agent-session-monitor](../../agent-session-monitor/SKILL.md)
|
||||
- **Higress AI Gateway**: https://github.com/higress-group/higress-standalone
|
||||
|
||||
## Compatibility
|
||||
|
||||
- **OpenClaw**: v2.0.0+
|
||||
- **Clawdbot**: v2.0.0+
|
||||
- **Higress AI Gateway**: All versions
|
||||
|
||||
## License
|
||||
|
||||
Apache-2.0
|
||||
@@ -1,284 +0,0 @@
|
||||
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
||||
|
||||
const DEFAULT_GATEWAY_URL = "http://localhost:8080";
|
||||
const DEFAULT_CONSOLE_URL = "http://localhost:8001";
|
||||
const DEFAULT_CONTEXT_WINDOW = 128_000;
|
||||
const DEFAULT_MAX_TOKENS = 8192;
|
||||
|
||||
// Common models that Higress AI Gateway typically supports
|
||||
const DEFAULT_MODEL_IDS = [
|
||||
// Auto-routing special model
|
||||
"higress/auto",
|
||||
// OpenAI models
|
||||
"gpt-5.2",
|
||||
"gpt-5-mini",
|
||||
"gpt-5-nano",
|
||||
// Anthropic models
|
||||
"claude-opus-4.5",
|
||||
"claude-sonnet-4.5",
|
||||
"claude-haiku-4.5",
|
||||
// Qwen models
|
||||
"qwen3-turbo",
|
||||
"qwen3-plus",
|
||||
"qwen3-max",
|
||||
"qwen3-coder-480b-a35b-instruct",
|
||||
// DeepSeek models
|
||||
"deepseek-chat",
|
||||
"deepseek-reasoner",
|
||||
// Other common models
|
||||
"kimi-k2.5",
|
||||
"glm-4.7",
|
||||
"MiniMax-M2.1",
|
||||
] as const;
|
||||
|
||||
function normalizeBaseUrl(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return DEFAULT_GATEWAY_URL;
|
||||
let normalized = trimmed;
|
||||
while (normalized.endsWith("/")) normalized = normalized.slice(0, -1);
|
||||
if (!normalized.endsWith("/v1")) normalized = `${normalized}/v1`;
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function validateUrl(value: string): string | undefined {
|
||||
const normalized = normalizeBaseUrl(value);
|
||||
try {
|
||||
new URL(normalized);
|
||||
} catch {
|
||||
return "Enter a valid URL";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function parseModelIds(input: string): string[] {
|
||||
const parsed = input
|
||||
.split(/[\n,]/)
|
||||
.map((model) => model.trim())
|
||||
.filter(Boolean);
|
||||
return Array.from(new Set(parsed));
|
||||
}
|
||||
|
||||
function buildModelDefinition(modelId: string) {
|
||||
const isAutoModel = modelId === "higress/auto";
|
||||
return {
|
||||
id: modelId,
|
||||
name: isAutoModel ? "Higress Auto Router" : modelId,
|
||||
api: "openai-completions",
|
||||
reasoning: false,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: DEFAULT_CONTEXT_WINDOW,
|
||||
maxTokens: DEFAULT_MAX_TOKENS,
|
||||
};
|
||||
}
|
||||
|
||||
async function testGatewayConnection(gatewayUrl: string): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${gatewayUrl}/v1/models`, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
return response.ok || response.status === 401; // 401 means gateway is up but needs auth
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAvailableModels(consoleUrl: string): Promise<string[]> {
|
||||
try {
|
||||
// Try to get models from Higress Console API
|
||||
const response = await fetch(`${consoleUrl}/v1/ai/routes`, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = (await response.json()) as { data?: { model?: string }[] };
|
||||
if (data.data && Array.isArray(data.data)) {
|
||||
return data.data
|
||||
.map((route: { model?: string }) => route.model)
|
||||
.filter((m): m is string => typeof m === "string");
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors, use defaults
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
const higressPlugin = {
|
||||
id: "higress-ai-gateway",
|
||||
name: "Higress AI Gateway",
|
||||
description: "Model provider plugin for Higress AI Gateway with auto-routing support",
|
||||
configSchema: emptyPluginConfigSchema(),
|
||||
register(api) {
|
||||
api.registerProvider({
|
||||
id: "higress",
|
||||
label: "Higress AI Gateway",
|
||||
docsPath: "/providers/models",
|
||||
aliases: ["higress-gateway", "higress-ai"],
|
||||
auth: [
|
||||
{
|
||||
id: "api-key",
|
||||
label: "API Key",
|
||||
hint: "Configure Higress AI Gateway endpoint with optional API key",
|
||||
kind: "custom",
|
||||
run: async (ctx) => {
|
||||
// Step 1: Get Gateway URL
|
||||
const gatewayUrlInput = await ctx.prompter.text({
|
||||
message: "Higress AI Gateway URL",
|
||||
initialValue: DEFAULT_GATEWAY_URL,
|
||||
validate: validateUrl,
|
||||
});
|
||||
const gatewayUrl = normalizeBaseUrl(gatewayUrlInput);
|
||||
|
||||
// Step 2: Get Console URL (for auto-router configuration)
|
||||
const consoleUrlInput = await ctx.prompter.text({
|
||||
message: "Higress Console URL (for auto-router config)",
|
||||
initialValue: DEFAULT_CONSOLE_URL,
|
||||
validate: validateUrl,
|
||||
});
|
||||
const consoleUrl = normalizeBaseUrl(consoleUrlInput);
|
||||
|
||||
// Step 3: Test connection (create a new spinner)
|
||||
const spin = ctx.prompter.progress("Testing gateway connection…");
|
||||
const isConnected = await testGatewayConnection(gatewayUrl);
|
||||
if (!isConnected) {
|
||||
spin.stop("Gateway connection failed");
|
||||
await ctx.prompter.note(
|
||||
[
|
||||
"Could not connect to Higress AI Gateway.",
|
||||
"Make sure the gateway is running and the URL is correct.",
|
||||
"",
|
||||
`Tried: ${gatewayUrl}/v1/models`,
|
||||
].join("\n"),
|
||||
"Connection Warning",
|
||||
);
|
||||
} else {
|
||||
spin.stop("Gateway connected");
|
||||
}
|
||||
|
||||
// Step 4: Get API Key (optional for local gateway)
|
||||
const apiKeyInput = await ctx.prompter.text({
|
||||
message: "API Key (leave empty if not required)",
|
||||
initialValue: "",
|
||||
}) || '';
|
||||
const apiKey = apiKeyInput.trim() || "higress-local";
|
||||
|
||||
// Step 5: Fetch available models (create a new spinner)
|
||||
const spin2 = ctx.prompter.progress("Fetching available models…");
|
||||
const fetchedModels = await fetchAvailableModels(consoleUrl);
|
||||
const defaultModels = fetchedModels.length > 0
|
||||
? ["higress/auto", ...fetchedModels]
|
||||
: DEFAULT_MODEL_IDS;
|
||||
spin2.stop();
|
||||
|
||||
// Step 6: Let user customize model list
|
||||
const modelInput = await ctx.prompter.text({
|
||||
message: "Model IDs (comma-separated, higress/auto enables auto-routing)",
|
||||
initialValue: defaultModels.slice(0, 10).join(", "),
|
||||
validate: (value) =>
|
||||
parseModelIds(value).length > 0 ? undefined : "Enter at least one model id",
|
||||
});
|
||||
|
||||
const modelIds = parseModelIds(modelInput);
|
||||
const hasAutoModel = modelIds.includes("higress/auto");
|
||||
|
||||
// FIX: Avoid double prefix - if modelId already starts with provider, don't add prefix again
|
||||
const defaultModelId = hasAutoModel
|
||||
? "higress/auto"
|
||||
: (modelIds[0] ?? "qwen-turbo");
|
||||
const defaultModelRef = defaultModelId.startsWith("higress/")
|
||||
? defaultModelId
|
||||
: `higress/${defaultModelId}`;
|
||||
|
||||
// Step 7: Configure default model for auto-routing
|
||||
let autoRoutingDefaultModel = "qwen-turbo";
|
||||
if (hasAutoModel) {
|
||||
const autoRoutingModelInput = await ctx.prompter.text({
|
||||
message: "Default model for auto-routing (when no rule matches)",
|
||||
initialValue: "qwen-turbo",
|
||||
});
|
||||
autoRoutingDefaultModel = autoRoutingModelInput.trim(); // FIX: Add trim() here
|
||||
}
|
||||
|
||||
return {
|
||||
profiles: [
|
||||
{
|
||||
profileId: `higress:${apiKey === "higress-local" ? "local" : "default"}`,
|
||||
credential: {
|
||||
type: "token",
|
||||
provider: "higress",
|
||||
token: apiKey,
|
||||
},
|
||||
},
|
||||
],
|
||||
configPatch: {
|
||||
models: {
|
||||
providers: {
|
||||
higress: {
|
||||
baseUrl: `${gatewayUrl}/v1`,
|
||||
apiKey: apiKey,
|
||||
api: "openai-completions",
|
||||
authHeader: apiKey !== "higress-local",
|
||||
models: modelIds.map((modelId) => buildModelDefinition(modelId)),
|
||||
},
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
defaults: {
|
||||
models: Object.fromEntries(
|
||||
modelIds.map((modelId) => {
|
||||
// FIX: Avoid double prefix - only add provider prefix if not already present
|
||||
const modelRef = modelId.startsWith("higress/")
|
||||
? modelId
|
||||
: `higress/${modelId}`;
|
||||
return [modelRef, {}];
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
"higress-ai-gateway": {
|
||||
enabled: true,
|
||||
config: {
|
||||
gatewayUrl,
|
||||
consoleUrl,
|
||||
autoRoutingDefaultModel,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultModel: defaultModelRef,
|
||||
notes: [
|
||||
"Higress AI Gateway is now configured as a model provider.",
|
||||
hasAutoModel
|
||||
? `Auto-routing enabled: use model "higress/auto" to route based on message content.`
|
||||
: "Add 'higress/auto' to models to enable auto-routing.",
|
||||
`Gateway endpoint: ${gatewayUrl}/v1/chat/completions`,
|
||||
`Console: ${consoleUrl}`,
|
||||
"",
|
||||
"🎯 Recommended Skills (install via Clawdbot conversation):",
|
||||
"",
|
||||
"1. Auto-Routing Skill:",
|
||||
" Configure automatic model routing based on message content",
|
||||
" https://github.com/alibaba/higress/tree/main/.claude/skills/higress-auto-router",
|
||||
' Say: "Install higress-auto-router skill"',
|
||||
"",
|
||||
"2. Agent Session Monitor Skill:",
|
||||
" Track token usage and monitor conversation history",
|
||||
" https://github.com/alibaba/higress/tree/main/.claude/skills/agent-session-monitor",
|
||||
' Say: "Install agent-session-monitor skill"',
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default higressPlugin;
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"id": "higress-ai-gateway",
|
||||
"name": "Higress AI Gateway",
|
||||
"description": "Model provider plugin for Higress AI Gateway with auto-routing support",
|
||||
"providers": ["higress"],
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"name": "@higress/higress-ai-gateway",
|
||||
"version": "1.0.0",
|
||||
"description": "Higress AI Gateway model provider plugin for OpenClaw with auto-routing support",
|
||||
"main": "index.ts",
|
||||
"openclaw": {
|
||||
"extensions": ["./index.ts"]
|
||||
},
|
||||
"keywords": [
|
||||
"openclaw",
|
||||
"higress",
|
||||
"ai-gateway",
|
||||
"model-router",
|
||||
"auto-routing"
|
||||
],
|
||||
"author": "Higress Team",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/alibaba/higress"
|
||||
}
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
# Higress 社区治理日报 - Clawdbot Skill
|
||||
|
||||
这个 skill 让 AI 助手通过 Clawdbot 自动追踪 Higress 项目的 GitHub 活动,并生成结构化的每日社区治理报告。
|
||||
|
||||
## 架构概览
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ Clawdbot │────▶│ AI + Skill │────▶│ GitHub API │
|
||||
│ (Gateway) │ │ │ │ (gh CLI) │
|
||||
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||
│ │
|
||||
│ ▼
|
||||
│ ┌─────────────────┐
|
||||
│ │ 数据文件 │
|
||||
│ │ - tracking.json│
|
||||
│ │ - knowledge.md │
|
||||
│ └─────────────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────────────┐ ┌─────────────────┐
|
||||
│ Discord/Slack │◀────│ 日报输出 │
|
||||
│ Channel │ │ │
|
||||
└─────────────────┘ └─────────────────┘
|
||||
```
|
||||
|
||||
## 什么是 Clawdbot?
|
||||
|
||||
[Clawdbot](https://github.com/clawdbot/clawdbot) 是一个 AI Agent 网关,可以将 Claude、GPT、GLM 等 AI 模型连接到各种消息平台(Discord、Slack、Telegram 等)和工具(GitHub CLI、浏览器、文件系统等)。
|
||||
|
||||
通过 Clawdbot,AI 助手可以:
|
||||
- 接收来自 Discord 等平台的消息
|
||||
- 执行 shell 命令(如 `gh` CLI)
|
||||
- 读写文件
|
||||
- 定时执行任务(cron)
|
||||
- 将生成的内容发送回消息平台
|
||||
|
||||
## 工作流程
|
||||
|
||||
### 1. 定时触发
|
||||
|
||||
通过 Clawdbot 的 cron 功能,每天定时触发日报生成:
|
||||
|
||||
```
|
||||
# Clawdbot 配置示例
|
||||
cron:
|
||||
- schedule: "0 9 * * *" # 每天早上 9 点
|
||||
task: "生成 Higress 昨日日报并发送到 #issue-pr-notify 频道"
|
||||
```
|
||||
|
||||
### 2. Skill 加载
|
||||
|
||||
当 AI 助手收到生成日报的指令时,会自动加载此 skill(SKILL.md),获取:
|
||||
- 数据获取方法(gh CLI 命令)
|
||||
- 数据结构定义
|
||||
- 日报格式模板
|
||||
- 知识库维护规则
|
||||
|
||||
### 3. 数据获取
|
||||
|
||||
AI 助手使用 GitHub CLI 获取数据:
|
||||
|
||||
```bash
|
||||
# 获取昨日新建的 issues
|
||||
gh search issues --repo alibaba/higress --created yesterday --json number,title,author,url,body,state,labels
|
||||
|
||||
# 获取昨日新建的 PRs
|
||||
gh search prs --repo alibaba/higress --created yesterday --json number,title,author,url,body,state
|
||||
|
||||
# 获取特定 issue 的评论
|
||||
gh api repos/alibaba/higress/issues/{number}/comments
|
||||
```
|
||||
|
||||
### 4. 状态追踪
|
||||
|
||||
AI 助手维护一个 JSON 文件追踪每个 issue 的状态:
|
||||
|
||||
```json
|
||||
{
|
||||
"issues": [
|
||||
{
|
||||
"number": 3398,
|
||||
"title": "浏览器发起的options请求报401",
|
||||
"lastCommentCount": 13,
|
||||
"status": "waiting_for_user",
|
||||
"waitingFor": "用户验证解决方案"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 知识沉淀
|
||||
|
||||
当 issue 被解决时,AI 助手会将问题模式和解决方案记录到知识库:
|
||||
|
||||
```markdown
|
||||
## KB-001: OPTIONS 预检请求被认证拦截
|
||||
|
||||
**问题**: 浏览器 OPTIONS 请求返回 401
|
||||
**根因**: key-auth 在 AUTHN 阶段执行,先于 CORS
|
||||
**解决方案**: 为 OPTIONS 请求创建单独路由,不启用认证插件
|
||||
**关联 Issue**: #3398
|
||||
```
|
||||
|
||||
### 6. 日报生成
|
||||
|
||||
最终生成结构化日报,包含:
|
||||
- 📋 概览统计
|
||||
- 📌 新增 Issues
|
||||
- 🔀 新增 PRs
|
||||
- 🔔 Issue 动态(新评论、已解决)
|
||||
- ⏰ 跟进提醒
|
||||
- 📚 知识沉淀
|
||||
|
||||
### 7. 消息推送
|
||||
|
||||
AI 助手通过 Clawdbot 将日报发送到指定的 Discord 频道。
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 前置要求
|
||||
|
||||
1. 安装并配置 [Clawdbot](https://github.com/clawdbot/clawdbot)
|
||||
2. 配置 GitHub CLI (`gh`) 并登录
|
||||
3. 配置消息平台(如 Discord)
|
||||
|
||||
### 配置 Skill
|
||||
|
||||
将此 skill 目录复制到 Clawdbot 的 skills 目录:
|
||||
|
||||
```bash
|
||||
cp -r .claude/skills/higress-daily-report ~/.clawdbot/skills/
|
||||
```
|
||||
|
||||
### 使用方式
|
||||
|
||||
**手动触发:**
|
||||
```
|
||||
生成 Higress 昨日日报
|
||||
```
|
||||
|
||||
**定时触发(推荐):**
|
||||
在 Clawdbot 配置中添加 cron 任务,每天自动生成并推送日报。
|
||||
|
||||
## 文件说明
|
||||
|
||||
```
|
||||
higress-daily-report/
|
||||
├── README.md # 本文件
|
||||
├── SKILL.md # Skill 定义(AI 助手读取)
|
||||
└── scripts/
|
||||
└── generate-report.sh # 辅助脚本(可选)
|
||||
```
|
||||
|
||||
## 自定义
|
||||
|
||||
### 修改日报格式
|
||||
|
||||
编辑 `SKILL.md` 中的「日报格式」章节。
|
||||
|
||||
### 添加新的追踪维度
|
||||
|
||||
在 `SKILL.md` 的数据结构中添加新字段。
|
||||
|
||||
### 调整知识库规则
|
||||
|
||||
修改 `SKILL.md` 中的「知识沉淀」章节。
|
||||
|
||||
## 示例日报
|
||||
|
||||
```markdown
|
||||
📊 Higress 项目每日报告 - 2026-01-29
|
||||
|
||||
📋 概览
|
||||
• 新增 Issues: 2 个
|
||||
• 新增 PRs: 3 个
|
||||
• 待跟进: 1 个
|
||||
|
||||
📌 新增 Issues
|
||||
• #3399: 网关启动失败问题
|
||||
- 作者: user123
|
||||
- 标签: bug
|
||||
|
||||
🔔 Issue 动态
|
||||
✅ 已解决
|
||||
• #3398: OPTIONS 请求 401 问题
|
||||
- 知识库: KB-001
|
||||
|
||||
⏰ 跟进提醒
|
||||
🟡 等待反馈
|
||||
• #3396: 等待用户提供配置信息(2天)
|
||||
```
|
||||
|
||||
## 相关链接
|
||||
|
||||
- [Clawdbot 文档](https://docs.clawd.bot)
|
||||
- [Higress 项目](https://github.com/alibaba/higress)
|
||||
- [GitHub CLI 文档](https://cli.github.com/manual/)
|
||||
@@ -1,257 +0,0 @@
|
||||
---
|
||||
name: higress-daily-report
|
||||
description: 生成 Higress 项目每日报告,追踪 issue/PR 动态,沉淀问题处理经验,驱动社区问题闭环。用于生成日报、跟进 issue、记录解决方案。
|
||||
---
|
||||
|
||||
# Higress Daily Report
|
||||
|
||||
驱动 Higress 社区问题处理的智能工作流。
|
||||
|
||||
## 核心目标
|
||||
|
||||
1. **每日感知** - 追踪新 issues/PRs 和评论动态
|
||||
2. **进度跟踪** - 确保每个 issue 被持续跟进直到关闭
|
||||
3. **知识沉淀** - 积累问题分析和解决方案,提升处理能力
|
||||
4. **闭环驱动** - 通过日报推动问题解决,避免遗忘
|
||||
|
||||
## 数据文件
|
||||
|
||||
| 文件 | 用途 |
|
||||
|------|------|
|
||||
| `/root/clawd/memory/higress-issue-tracking.json` | Issue 追踪状态(评论数、跟进状态) |
|
||||
| `/root/clawd/memory/higress-knowledge-base.md` | 知识库:问题模式、解决方案、经验教训 |
|
||||
| `/root/clawd/reports/report_YYYY-MM-DD.md` | 每日报告存档 |
|
||||
|
||||
## 工作流程
|
||||
|
||||
### 1. 获取每日数据
|
||||
|
||||
```bash
|
||||
# 获取昨日 issues
|
||||
gh search issues --repo alibaba/higress --created yesterday --json number,title,author,url,body,state,labels --limit 50
|
||||
|
||||
# 获取昨日 PRs
|
||||
gh search prs --repo alibaba/higress --created yesterday --json number,title,author,url,body,state,additions,deletions,reviewDecision --limit 50
|
||||
```
|
||||
|
||||
### 2. Issue 追踪状态管理
|
||||
|
||||
**追踪数据结构** (`higress-issue-tracking.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"date": "2026-01-28",
|
||||
"issues": [
|
||||
{
|
||||
"number": 3398,
|
||||
"title": "Issue 标题",
|
||||
"state": "open",
|
||||
"author": "username",
|
||||
"url": "https://github.com/...",
|
||||
"created_at": "2026-01-27",
|
||||
"comment_count": 11,
|
||||
"last_comment_by": "johnlanni",
|
||||
"last_comment_at": "2026-01-28",
|
||||
"follow_up_status": "waiting_user",
|
||||
"follow_up_note": "等待用户提供请求日志",
|
||||
"priority": "high",
|
||||
"category": "cors",
|
||||
"solution_ref": "KB-001"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**跟进状态枚举**:
|
||||
- `new` - 新 issue,待分析
|
||||
- `analyzing` - 正在分析中
|
||||
- `waiting_user` - 等待用户反馈
|
||||
- `waiting_review` - 等待 PR review
|
||||
- `in_progress` - 修复进行中
|
||||
- `resolved` - 已解决(待关闭)
|
||||
- `closed` - 已关闭
|
||||
- `wontfix` - 不予修复
|
||||
- `stale` - 超过 7 天无活动
|
||||
|
||||
### 3. 知识库结构
|
||||
|
||||
**知识库** (`higress-knowledge-base.md`) 用于沉淀经验:
|
||||
|
||||
```markdown
|
||||
# Higress 问题知识库
|
||||
|
||||
## 问题模式索引
|
||||
|
||||
### 认证与跨域类
|
||||
- KB-001: OPTIONS 预检请求被认证拦截
|
||||
- KB-002: CORS 配置不生效
|
||||
|
||||
### 路由配置类
|
||||
- KB-010: 路由状态 address 为空
|
||||
- KB-011: 服务发现失败
|
||||
|
||||
### 部署运维类
|
||||
- KB-020: Helm 安装问题
|
||||
- KB-021: 升级兼容性问题
|
||||
|
||||
---
|
||||
|
||||
## KB-001: OPTIONS 预检请求被认证拦截
|
||||
|
||||
**问题特征**:
|
||||
- 浏览器 OPTIONS 请求返回 401
|
||||
- 已配置 CORS 和认证插件
|
||||
|
||||
**根因分析**:
|
||||
Higress 插件执行阶段优先级:AUTHN (310) > AUTHZ (340) > STATS
|
||||
- key-auth 在 AUTHN 阶段执行
|
||||
- CORS 在 AUTHZ 阶段执行
|
||||
- OPTIONS 请求先被 key-auth 拦截,CORS 无机会处理
|
||||
|
||||
**解决方案**:
|
||||
1. **推荐**:修改 CORS 插件 stage 从 AUTHZ 改为 AUTHN
|
||||
2. **Workaround**:创建 OPTIONS 专用路由,不启用认证
|
||||
3. **Workaround**:使用实例级 CORS 配置
|
||||
|
||||
**关联 Issue**:#3398
|
||||
|
||||
**学到的经验**:
|
||||
- 排查跨域问题时,首先确认插件执行顺序
|
||||
- Higress 阶段优先级由 phase 决定,不是 priority 数值
|
||||
```
|
||||
|
||||
### 4. 日报生成规则
|
||||
|
||||
**报告结构**:
|
||||
|
||||
```markdown
|
||||
# 📊 Higress 项目每日报告 - YYYY-MM-DD
|
||||
|
||||
## 📋 概览
|
||||
- 统计时间: YYYY-MM-DD
|
||||
- 新增 Issues: X 个
|
||||
- 新增 PRs: X 个
|
||||
- 待跟进 Issues: X 个
|
||||
- 本周关闭: X 个
|
||||
|
||||
## 📌 新增 Issues
|
||||
(按优先级排序,包含分类标签)
|
||||
|
||||
## 🔀 新增 PRs
|
||||
(包含代码变更量和 review 状态)
|
||||
|
||||
## 🔔 Issue 动态
|
||||
(有新评论的 issues,标注最新进展)
|
||||
|
||||
## ⏰ 跟进提醒
|
||||
|
||||
### 🔴 需要立即处理
|
||||
(等待我方回复超过 24h 的 issues)
|
||||
|
||||
### 🟡 等待用户反馈
|
||||
(等待用户回复的 issues,标注等待天数)
|
||||
|
||||
### 🟢 进行中
|
||||
(正在处理的 issues)
|
||||
|
||||
### ⚪ 已过期
|
||||
(超过 7 天无活动的 issues,需决定是否关闭)
|
||||
|
||||
## 📚 本周知识沉淀
|
||||
(新增的知识库条目摘要)
|
||||
```
|
||||
|
||||
### 5. 智能分析能力
|
||||
|
||||
生成日报时,对每个新 issue 进行初步分析:
|
||||
|
||||
1. **问题分类** - 根据标题和内容判断类别
|
||||
2. **知识库匹配** - 检索相似问题的解决方案
|
||||
3. **优先级评估** - 根据影响范围和紧急程度
|
||||
4. **建议回复** - 基于知识库生成初步回复建议
|
||||
|
||||
### 6. Issue 跟进触发
|
||||
|
||||
当用户在 Discord 中提到以下关键词时触发跟进记录:
|
||||
|
||||
**完成跟进**:
|
||||
- "已跟进 #xxx"
|
||||
- "已回复 #xxx"
|
||||
- "issue #xxx 已处理"
|
||||
|
||||
**记录解决方案**:
|
||||
- "issue #xxx 的问题是..."
|
||||
- "#xxx 根因是..."
|
||||
- "#xxx 解决方案..."
|
||||
|
||||
触发后更新追踪状态和知识库。
|
||||
|
||||
## 执行检查清单
|
||||
|
||||
每次生成日报时:
|
||||
|
||||
- [ ] 获取昨日新 issues 和 PRs
|
||||
- [ ] 加载追踪数据,检查评论变化
|
||||
- [ ] 对比 `last_comment_by` 判断是等待用户还是等待我方
|
||||
- [ ] 超过 7 天无活动的 issue 标记为 stale
|
||||
- [ ] 检索知识库,为新 issue 匹配相似问题
|
||||
- [ ] 生成报告并保存到 `/root/clawd/reports/`
|
||||
- [ ] 更新追踪数据
|
||||
- [ ] 发送到 Discord channel:1465549185632702591
|
||||
- [ ] 格式:使用列表而非表格(Discord 不支持 Markdown 表格)
|
||||
|
||||
## 知识库维护
|
||||
|
||||
### 新增条目时机
|
||||
|
||||
1. Issue 被成功解决后
|
||||
2. 发现新的问题模式
|
||||
3. 踩坑后的经验总结
|
||||
|
||||
### 条目模板
|
||||
|
||||
```markdown
|
||||
## KB-XXX: 问题简述
|
||||
|
||||
**问题特征**:
|
||||
- 症状1
|
||||
- 症状2
|
||||
|
||||
**根因分析**:
|
||||
(技术原因说明)
|
||||
|
||||
**解决方案**:
|
||||
1. 推荐方案
|
||||
2. 备选方案
|
||||
|
||||
**关联 Issue**:#xxx
|
||||
|
||||
**学到的经验**:
|
||||
- 经验1
|
||||
- 经验2
|
||||
```
|
||||
|
||||
## 命令参考
|
||||
|
||||
```bash
|
||||
# 查看 issue 详情和评论
|
||||
gh issue view <number> --repo alibaba/higress --json number,title,state,comments,author,createdAt,labels,url
|
||||
|
||||
# 查看 issue 评论
|
||||
gh issue view <number> --repo alibaba/higress --comments
|
||||
|
||||
# 发送 issue 评论
|
||||
gh issue comment <number> --repo alibaba/higress --body "评论内容"
|
||||
|
||||
# 关闭 issue
|
||||
gh issue close <number> --repo alibaba/higress --reason completed
|
||||
|
||||
# 添加标签
|
||||
gh issue edit <number> --repo alibaba/higress --add-label "bug"
|
||||
```
|
||||
|
||||
## Discord 输出
|
||||
|
||||
- 频道: `channel:1465549185632702591`
|
||||
- 格式: 纯文本 + emoji + 链接(用 `<url>` 抑制预览)
|
||||
- 长度: 单条消息不超过 2000 字符,超过则分多条发送
|
||||
@@ -1,273 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Higress Daily Report Generator
|
||||
# Generates daily report for alibaba/higress repository
|
||||
|
||||
# set -e # 临时禁用以调试
|
||||
|
||||
REPO="alibaba/higress"
|
||||
CHANNEL="1465549185632702591"
|
||||
DATE=$(date +"%Y-%m-%d")
|
||||
REPORT_DIR="/root/clawd/reports"
|
||||
TRACKING_DIR="/root/clawd/memory"
|
||||
RECORD_FILE="${TRACKING_DIR}/higress-issue-process-record.md"
|
||||
|
||||
mkdir -p "$REPORT_DIR" "$TRACKING_DIR"
|
||||
|
||||
echo "=== Higress Daily Report - $DATE ==="
|
||||
|
||||
# Get yesterday's date
|
||||
YESTERDAY=$(date -d "yesterday" +"%Y-%m-%d" 2>/dev/null || date -v-1d +"%Y-%m-%d")
|
||||
|
||||
echo "Fetching issues created on $YESTERDAY..."
|
||||
|
||||
# Fetch issues created yesterday
|
||||
ISSUES=$(gh search issues --repo "${REPO}" --state open --created "${YESTERDAY}..${YESTERDAY}" --json number,title,labels,author,url,body,state --limit 50 2>/dev/null)
|
||||
|
||||
if [ -z "$ISSUES" ]; then
|
||||
ISSUES_COUNT=0
|
||||
else
|
||||
ISSUES_COUNT=$(echo "$ISSUES" | jq 'length' 2>/dev/null || echo "0")
|
||||
fi
|
||||
|
||||
# Fetch PRs created yesterday
|
||||
PRS=$(gh search prs --repo "${REPO}" --state open --created "${YESTERDAY}..${YESTERDAY}" --json number,title,labels,author,url,reviewDecision,additions,deletions,body,state --limit 50 2>/dev/null)
|
||||
|
||||
if [ -z "$PRS" ]; then
|
||||
PRS_COUNT=0
|
||||
else
|
||||
PRS_COUNT=$(echo "$PRS" | jq 'length' 2>/dev/null || echo "0")
|
||||
fi
|
||||
|
||||
echo "Found: $ISSUES_COUNT issues, $PRS_COUNT PRs"
|
||||
|
||||
# Build report
|
||||
REPORT="📊 **Higress 项目每日报告 - ${DATE}**
|
||||
|
||||
**📋 概览**
|
||||
- 统计时间: ${YESTERDAY} 全天
|
||||
- 新增 Issues: **${ISSUES_COUNT}** 个
|
||||
- 新增 PRs: **${PRS_COUNT}** 个
|
||||
|
||||
---
|
||||
|
||||
"
|
||||
|
||||
# Process issues
|
||||
if [ "$ISSUES_COUNT" -gt 0 ]; then
|
||||
REPORT="${REPORT}**📌 Issues 详情**
|
||||
|
||||
"
|
||||
|
||||
# Use a temporary file to avoid subshell variable scoping issues
|
||||
ISSUE_DETAILS=$(mktemp)
|
||||
|
||||
echo "$ISSUES" | jq -r '.[] | @json' | while IFS= read -r ISSUE; do
|
||||
NUM=$(echo "$ISSUE" | jq -r '.number')
|
||||
TITLE=$(echo "$ISSUE" | jq -r '.title')
|
||||
URL=$(echo "$ISSUE" | jq -r '.url')
|
||||
AUTHOR=$(echo "$ISSUE" | jq -r '.author.login')
|
||||
BODY=$(echo "$ISSUE" | jq -r '.body // ""')
|
||||
LABELS=$(echo "$ISSUE" | jq -r '.labels[]?.name // ""' | head -1)
|
||||
|
||||
# Determine emoji
|
||||
EMOJI="📝"
|
||||
echo "$LABELS" | grep -q "priority/high" && EMOJI="🔴"
|
||||
echo "$LABELS" | grep -q "type/bug" && EMOJI="🐛"
|
||||
echo "$LABELS" | grep -q "type/enhancement" && EMOJI="✨"
|
||||
|
||||
# Extract content
|
||||
CONTENT=$(echo "$BODY" | head -n 8 | sed 's/```.*```//g' | sed 's/`//g' | tr '\n' ' ' | head -c 300)
|
||||
|
||||
if [ -z "$CONTENT" ]; then
|
||||
CONTENT="无详细描述"
|
||||
fi
|
||||
|
||||
if [ ${#CONTENT} -eq 300 ]; then
|
||||
CONTENT="${CONTENT}..."
|
||||
fi
|
||||
|
||||
# Append to temporary file
|
||||
echo "${EMOJI} **[#${NUM}](${URL})**: ${TITLE}
|
||||
👤 @${AUTHOR}
|
||||
📝 ${CONTENT}
|
||||
" >> "$ISSUE_DETAILS"
|
||||
done
|
||||
|
||||
# Read from temp file and append to REPORT
|
||||
REPORT="${REPORT}$(cat $ISSUE_DETAILS)"
|
||||
|
||||
rm -f "$ISSUE_DETAILS"
|
||||
fi
|
||||
|
||||
REPORT="${REPORT}
|
||||
---
|
||||
|
||||
"
|
||||
|
||||
# Process PRs
|
||||
if [ "$PRS_COUNT" -gt 0 ]; then
|
||||
REPORT="${REPORT}**🔀 PRs 详情**
|
||||
|
||||
"
|
||||
|
||||
# Use a temporary file to avoid subshell variable scoping issues
|
||||
PR_DETAILS=$(mktemp)
|
||||
|
||||
echo "$PRS" | jq -r '.[] | @json' | while IFS= read -r PR; do
|
||||
NUM=$(echo "$PR" | jq -r '.number')
|
||||
TITLE=$(echo "$PR" | jq -r '.title')
|
||||
URL=$(echo "$PR" | jq -r '.url')
|
||||
AUTHOR=$(echo "$PR" | jq -r '.author.login')
|
||||
ADDITIONS=$(echo "$PR" | jq -r '.additions')
|
||||
DELETIONS=$(echo "$PR" | jq -r '.deletions')
|
||||
REVIEW=$(echo "$PR" | jq -r '.reviewDecision // "pending"')
|
||||
BODY=$(echo "$PR" | jq -r '.body // ""')
|
||||
|
||||
# Determine status
|
||||
STATUS="👀"
|
||||
[ "$REVIEW" = "APPROVED" ] && STATUS="✅"
|
||||
[ "$REVIEW" = "CHANGES_REQUESTED" ] && STATUS="🔄"
|
||||
|
||||
# Calculate size
|
||||
TOTAL=$((ADDITIONS + DELETIONS))
|
||||
SIZE="M"
|
||||
[ $TOTAL -lt 100 ] && SIZE="XS"
|
||||
[ $TOTAL -lt 500 ] && SIZE="S"
|
||||
[ $TOTAL -lt 1000 ] && SIZE="M"
|
||||
[ $TOTAL -lt 5000 ] && SIZE="L"
|
||||
[ $TOTAL -ge 5000 ] && SIZE="XL"
|
||||
|
||||
# Extract content
|
||||
CONTENT=$(echo "$BODY" | head -n 8 | sed 's/```.*```//g' | sed 's/`//g' | tr '\n' ' ' | head -c 300)
|
||||
|
||||
if [ -z "$CONTENT" ]; then
|
||||
CONTENT="无详细描述"
|
||||
fi
|
||||
|
||||
if [ ${#CONTENT} -eq 300 ]; then
|
||||
CONTENT="${CONTENT}..."
|
||||
fi
|
||||
|
||||
# Append to temporary file
|
||||
echo "${STATUS} **[#${NUM}](${URL})**: ${TITLE} ${SIZE}
|
||||
👤 @${AUTHOR} | ${STATUS} | 变更: +${ADDITIONS}/-${DELETIONS}
|
||||
📝 ${CONTENT}
|
||||
" >> "$PR_DETAILS"
|
||||
done
|
||||
|
||||
# Read from temp file and append to REPORT
|
||||
REPORT="${REPORT}$(cat $PR_DETAILS)"
|
||||
|
||||
rm -f "$PR_DETAILS"
|
||||
fi
|
||||
|
||||
# Check for new comments on tracked issues
|
||||
TRACKING_FILE="${TRACKING_DIR}/higress-issue-tracking.json"
|
||||
|
||||
echo ""
|
||||
echo "Checking for new comments on tracked issues..."
|
||||
|
||||
# Load previous tracking data
|
||||
if [ -f "$TRACKING_FILE" ]; then
|
||||
PREV_TRACKING=$(cat "$TRACKING_FILE")
|
||||
PREV_ISSUES=$(echo "$PREV_TRACKING" | jq -r '.issues[]?.number // empty' 2>/dev/null)
|
||||
|
||||
if [ -n "$PREV_ISSUES" ]; then
|
||||
REPORT="${REPORT}**🔔 Issue跟进(新评论)**"
|
||||
|
||||
HAS_NEW_COMMENTS=false
|
||||
|
||||
for issue_num in $PREV_ISSUES; do
|
||||
# Get current comment count
|
||||
CURRENT_INFO=$(gh issue view "$issue_num" --repo "$REPO" --json number,title,state,comments,url 2>/dev/null)
|
||||
if [ -n "$CURRENT_INFO" ]; then
|
||||
CURRENT_COUNT=$(echo "$CURRENT_INFO" | jq '.comments | length')
|
||||
CURRENT_TITLE=$(echo "$CURRENT_INFO" | jq -r '.title')
|
||||
CURRENT_STATE=$(echo "$CURRENT_INFO" | jq -r '.state')
|
||||
ISSUE_URL=$(echo "$CURRENT_INFO" | jq -r '.url')
|
||||
PREV_COUNT=$(echo "$PREV_TRACKING" | jq -r ".issues[] | select(.number == $issue_num) | .comment_count // 0")
|
||||
|
||||
if [ -z "$PREV_COUNT" ]; then
|
||||
PREV_COUNT=0
|
||||
fi
|
||||
|
||||
NEW_COMMENTS=$((CURRENT_COUNT - PREV_COUNT))
|
||||
|
||||
if [ "$NEW_COMMENTS" -gt 0 ]; then
|
||||
HAS_NEW_COMMENTS=true
|
||||
REPORT="${REPORT}
|
||||
|
||||
• [#${issue_num}](${ISSUE_URL}) ${CURRENT_TITLE}
|
||||
📬 +${NEW_COMMENTS}条新评论(总计: ${CURRENT_COUNT}) | 状态: ${CURRENT_STATE}"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$HAS_NEW_COMMENTS" = false ]; then
|
||||
REPORT="${REPORT}
|
||||
|
||||
• 暂无新评论"
|
||||
fi
|
||||
|
||||
REPORT="${REPORT}
|
||||
|
||||
---
|
||||
"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Save current tracking data for tomorrow
|
||||
echo "Saving issue tracking data for follow-up..."
|
||||
|
||||
if [ -z "$ISSUES" ]; then
|
||||
TRACKING_DATA='{"date":"'"$DATE"'","issues":[]}'
|
||||
else
|
||||
TRACKING_DATA=$(echo "$ISSUES" | jq '{
|
||||
date: "'"$DATE"'",
|
||||
issues: [.[] | {
|
||||
number: .number,
|
||||
title: .title,
|
||||
state: .state,
|
||||
comment_count: 0,
|
||||
url: .url
|
||||
}]
|
||||
}')
|
||||
fi
|
||||
|
||||
echo "$TRACKING_DATA" > "$TRACKING_FILE"
|
||||
echo "Tracking data saved to $TRACKING_FILE"
|
||||
|
||||
# Save report to file
|
||||
REPORT_FILE="${REPORT_DIR}/report_${DATE}.md"
|
||||
echo "$REPORT" > "$REPORT_FILE"
|
||||
echo "Report saved to $REPORT_FILE"
|
||||
|
||||
# Follow-up reminder
|
||||
FOLLOWUP_ISSUES=$(echo "$PREV_TRACKING" | jq -r '[.issues[] | select(.comment_count > 0 or .state == "open")] | "#\(.number) [\(.title)]"' 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$FOLLOWUP_ISSUES" ]; then
|
||||
REPORT="${REPORT}
|
||||
|
||||
**📌 需要跟进的Issues**
|
||||
|
||||
以下Issues需要跟进处理:
|
||||
${FOLLOWUP_ISSUES}
|
||||
|
||||
---
|
||||
|
||||
"
|
||||
fi
|
||||
|
||||
# Footer
|
||||
REPORT="${REPORT}
|
||||
---
|
||||
📅 生成时间: $(date +"%Y-%m-%d %H:%M:%S %Z")
|
||||
🔗 项目: https://github.com/${REPO}
|
||||
🤖 本报告由 AI 辅助生成,所有链接均可点击跳转
|
||||
"
|
||||
|
||||
# Send report
|
||||
echo "Sending report to Discord..."
|
||||
echo "$REPORT" | /root/.nvm/versions/node/v24.13.0/bin/clawdbot message send --channel discord -t "$CHANNEL" -m "$(cat -)"
|
||||
|
||||
echo "Done!"
|
||||
@@ -1,251 +0,0 @@
|
||||
---
|
||||
name: higress-wasm-go-plugin
|
||||
description: Develop Higress WASM plugins using Go 1.24+. Use when creating, modifying, or debugging Higress gateway plugins for HTTP request/response processing, external service calls, Redis integration, or custom gateway logic.
|
||||
---
|
||||
|
||||
# Higress WASM Go Plugin Development
|
||||
|
||||
Develop Higress gateway WASM plugins using Go language with the `wasm-go` SDK.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Project Setup
|
||||
|
||||
```bash
|
||||
# Create project directory
|
||||
mkdir my-plugin && cd my-plugin
|
||||
|
||||
# Initialize Go module
|
||||
go mod init my-plugin
|
||||
|
||||
# Set proxy (China)
|
||||
go env -w GOPROXY=https://proxy.golang.com.cn,direct
|
||||
|
||||
# Download dependencies
|
||||
go get github.com/higress-group/proxy-wasm-go-sdk@go-1.24
|
||||
go get github.com/higress-group/wasm-go@main
|
||||
go get github.com/tidwall/gjson
|
||||
```
|
||||
|
||||
### Minimal Plugin Template
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func main() {}
|
||||
|
||||
func init() {
|
||||
wrapper.SetCtx(
|
||||
"my-plugin",
|
||||
wrapper.ParseConfig(parseConfig),
|
||||
wrapper.ProcessRequestHeaders(onHttpRequestHeaders),
|
||||
)
|
||||
}
|
||||
|
||||
type MyConfig struct {
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
func parseConfig(json gjson.Result, config *MyConfig) error {
|
||||
config.Enabled = json.Get("enabled").Bool()
|
||||
return nil
|
||||
}
|
||||
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
|
||||
if config.Enabled {
|
||||
proxywasm.AddHttpRequestHeader("x-my-header", "hello")
|
||||
}
|
||||
return types.HeaderContinue
|
||||
}
|
||||
```
|
||||
|
||||
### Compile
|
||||
|
||||
```bash
|
||||
go mod tidy
|
||||
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o main.wasm ./
|
||||
```
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Plugin Lifecycle
|
||||
|
||||
1. **init()** - Register plugin with `wrapper.SetCtx()`
|
||||
2. **parseConfig** - Parse YAML config (auto-converted to JSON)
|
||||
3. **HTTP processing phases** - Handle requests/responses
|
||||
|
||||
### HTTP Processing Phases
|
||||
|
||||
| Phase | Trigger | Handler |
|
||||
|-------|---------|---------|
|
||||
| Request Headers | Gateway receives client request headers | `ProcessRequestHeaders` |
|
||||
| Request Body | Gateway receives client request body | `ProcessRequestBody` |
|
||||
| Response Headers | Gateway receives backend response headers | `ProcessResponseHeaders` |
|
||||
| Response Body | Gateway receives backend response body | `ProcessResponseBody` |
|
||||
| Stream Done | HTTP stream completes | `ProcessStreamDone` |
|
||||
|
||||
### Action Return Values
|
||||
|
||||
| Action | Behavior |
|
||||
|--------|----------|
|
||||
| `types.HeaderContinue` | Continue to next filter |
|
||||
| `types.HeaderStopIteration` | Stop header processing, wait for body |
|
||||
| `types.HeaderStopAllIterationAndWatermark` | Stop all processing, buffer data, call `proxywasm.ResumeHttpRequest/Response()` to resume |
|
||||
|
||||
## API Reference
|
||||
|
||||
### HttpContext Methods
|
||||
|
||||
```go
|
||||
// Request info (cached, safe to call in any phase)
|
||||
ctx.Scheme() // :scheme
|
||||
ctx.Host() // :authority
|
||||
ctx.Path() // :path
|
||||
ctx.Method() // :method
|
||||
|
||||
// Body handling
|
||||
ctx.HasRequestBody() // Check if request has body
|
||||
ctx.HasResponseBody() // Check if response has body
|
||||
ctx.DontReadRequestBody() // Skip reading request body
|
||||
ctx.DontReadResponseBody() // Skip reading response body
|
||||
ctx.BufferRequestBody() // Buffer instead of stream
|
||||
ctx.BufferResponseBody() // Buffer instead of stream
|
||||
|
||||
// Content detection
|
||||
ctx.IsWebsocket() // Check WebSocket upgrade
|
||||
ctx.IsBinaryRequestBody() // Check binary content
|
||||
ctx.IsBinaryResponseBody() // Check binary content
|
||||
|
||||
// Context storage
|
||||
ctx.SetContext(key, value)
|
||||
ctx.GetContext(key)
|
||||
ctx.GetStringContext(key, defaultValue)
|
||||
ctx.GetBoolContext(key, defaultValue)
|
||||
|
||||
// Custom logging
|
||||
ctx.SetUserAttribute(key, value)
|
||||
ctx.WriteUserAttributeToLog()
|
||||
```
|
||||
|
||||
### Header/Body Operations (proxywasm)
|
||||
|
||||
```go
|
||||
// Request headers
|
||||
proxywasm.GetHttpRequestHeader(name)
|
||||
proxywasm.AddHttpRequestHeader(name, value)
|
||||
proxywasm.ReplaceHttpRequestHeader(name, value)
|
||||
proxywasm.RemoveHttpRequestHeader(name)
|
||||
proxywasm.GetHttpRequestHeaders()
|
||||
proxywasm.ReplaceHttpRequestHeaders(headers)
|
||||
|
||||
// Response headers
|
||||
proxywasm.GetHttpResponseHeader(name)
|
||||
proxywasm.AddHttpResponseHeader(name, value)
|
||||
proxywasm.ReplaceHttpResponseHeader(name, value)
|
||||
proxywasm.RemoveHttpResponseHeader(name)
|
||||
proxywasm.GetHttpResponseHeaders()
|
||||
proxywasm.ReplaceHttpResponseHeaders(headers)
|
||||
|
||||
// Request body (only in body phase)
|
||||
proxywasm.GetHttpRequestBody(start, size)
|
||||
proxywasm.ReplaceHttpRequestBody(body)
|
||||
proxywasm.AppendHttpRequestBody(data)
|
||||
proxywasm.PrependHttpRequestBody(data)
|
||||
|
||||
// Response body (only in body phase)
|
||||
proxywasm.GetHttpResponseBody(start, size)
|
||||
proxywasm.ReplaceHttpResponseBody(body)
|
||||
proxywasm.AppendHttpResponseBody(data)
|
||||
proxywasm.PrependHttpResponseBody(data)
|
||||
|
||||
// Direct response
|
||||
proxywasm.SendHttpResponse(statusCode, headers, body, grpcStatus)
|
||||
|
||||
// Flow control
|
||||
proxywasm.ResumeHttpRequest() // Resume paused request
|
||||
proxywasm.ResumeHttpResponse() // Resume paused response
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### External HTTP Call
|
||||
|
||||
See [references/http-client.md](references/http-client.md) for complete HTTP client patterns.
|
||||
|
||||
```go
|
||||
func parseConfig(json gjson.Result, config *MyConfig) error {
|
||||
serviceName := json.Get("serviceName").String()
|
||||
servicePort := json.Get("servicePort").Int()
|
||||
config.client = wrapper.NewClusterClient(wrapper.FQDNCluster{
|
||||
FQDN: serviceName,
|
||||
Port: servicePort,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
|
||||
err := config.client.Get("/api/check", nil, func(statusCode int, headers http.Header, body []byte) {
|
||||
if statusCode != 200 {
|
||||
proxywasm.SendHttpResponse(403, nil, []byte("Forbidden"), -1)
|
||||
return
|
||||
}
|
||||
proxywasm.ResumeHttpRequest()
|
||||
}, 3000) // timeout ms
|
||||
|
||||
if err != nil {
|
||||
return types.HeaderContinue // fallback on error
|
||||
}
|
||||
return types.HeaderStopAllIterationAndWatermark
|
||||
}
|
||||
```
|
||||
|
||||
### Redis Integration
|
||||
|
||||
See [references/redis-client.md](references/redis-client.md) for complete Redis patterns.
|
||||
|
||||
```go
|
||||
func parseConfig(json gjson.Result, config *MyConfig) error {
|
||||
config.redis = wrapper.NewRedisClusterClient(wrapper.FQDNCluster{
|
||||
FQDN: json.Get("redisService").String(),
|
||||
Port: json.Get("redisPort").Int(),
|
||||
})
|
||||
return config.redis.Init(
|
||||
json.Get("username").String(),
|
||||
json.Get("password").String(),
|
||||
json.Get("timeout").Int(),
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Multi-level Config
|
||||
|
||||
插件配置支持在控制台不同级别设置:全局、域名级、路由级。控制面会自动处理配置的优先级和匹配逻辑,插件代码中通过 `parseConfig` 解析到的就是当前请求匹配到的配置。
|
||||
|
||||
## Local Testing
|
||||
|
||||
See [references/local-testing.md](references/local-testing.md) for Docker Compose setup.
|
||||
|
||||
## Advanced Topics
|
||||
|
||||
See [references/advanced-patterns.md](references/advanced-patterns.md) for:
|
||||
- Streaming body processing
|
||||
- Route call pattern
|
||||
- Tick functions (periodic tasks)
|
||||
- Leader election
|
||||
- Memory management
|
||||
- Custom logging
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Never call Resume after SendHttpResponse** - Response auto-resumes
|
||||
2. **Check HasRequestBody() before returning HeaderStopIteration** - Avoids blocking
|
||||
3. **Use cached ctx methods** - `ctx.Path()` works in any phase, `GetHttpRequestHeader(":path")` only in header phase
|
||||
4. **Handle external call failures gracefully** - Return `HeaderContinue` on error to avoid blocking
|
||||
5. **Set appropriate timeouts** - Default HTTP call timeout is 500ms
|
||||
@@ -1,253 +0,0 @@
|
||||
# Advanced Patterns
|
||||
|
||||
## Streaming Body Processing
|
||||
|
||||
Process body chunks as they arrive without buffering:
|
||||
|
||||
```go
|
||||
func init() {
|
||||
wrapper.SetCtx(
|
||||
"streaming-plugin",
|
||||
wrapper.ParseConfig(parseConfig),
|
||||
wrapper.ProcessStreamingRequestBody(onStreamingRequestBody),
|
||||
wrapper.ProcessStreamingResponseBody(onStreamingResponseBody),
|
||||
)
|
||||
}
|
||||
|
||||
func onStreamingRequestBody(ctx wrapper.HttpContext, config MyConfig, chunk []byte, isLastChunk bool) []byte {
|
||||
// Modify chunk and return
|
||||
modified := bytes.ReplaceAll(chunk, []byte("old"), []byte("new"))
|
||||
return modified
|
||||
}
|
||||
|
||||
func onStreamingResponseBody(ctx wrapper.HttpContext, config MyConfig, chunk []byte, isLastChunk bool) []byte {
|
||||
// Can call external services with NeedPauseStreamingResponse()
|
||||
return chunk
|
||||
}
|
||||
```
|
||||
|
||||
## Buffered Body Processing
|
||||
|
||||
Buffer entire body before processing:
|
||||
|
||||
```go
|
||||
func init() {
|
||||
wrapper.SetCtx(
|
||||
"buffered-plugin",
|
||||
wrapper.ParseConfig(parseConfig),
|
||||
wrapper.ProcessRequestBody(onRequestBody),
|
||||
wrapper.ProcessResponseBody(onResponseBody),
|
||||
)
|
||||
}
|
||||
|
||||
func onRequestBody(ctx wrapper.HttpContext, config MyConfig, body []byte) types.Action {
|
||||
// Full request body available
|
||||
var data map[string]interface{}
|
||||
json.Unmarshal(body, &data)
|
||||
|
||||
// Modify and replace
|
||||
data["injected"] = "value"
|
||||
newBody, _ := json.Marshal(data)
|
||||
proxywasm.ReplaceHttpRequestBody(newBody)
|
||||
|
||||
return types.ActionContinue
|
||||
}
|
||||
```
|
||||
|
||||
## Route Call Pattern
|
||||
|
||||
Call the current route's upstream with modified request:
|
||||
|
||||
```go
|
||||
func onRequestBody(ctx wrapper.HttpContext, config MyConfig, body []byte) types.Action {
|
||||
err := ctx.RouteCall("POST", "/modified-path", [][2]string{
|
||||
{"Content-Type", "application/json"},
|
||||
{"X-Custom", "header"},
|
||||
}, body, func(statusCode int, headers [][2]string, body []byte) {
|
||||
// Handle response from upstream
|
||||
proxywasm.SendHttpResponse(statusCode, headers, body, -1)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
proxywasm.SendHttpResponse(500, nil, []byte("Route call failed"), -1)
|
||||
}
|
||||
return types.ActionContinue
|
||||
}
|
||||
```
|
||||
|
||||
## Tick Functions (Periodic Tasks)
|
||||
|
||||
Register periodic background tasks:
|
||||
|
||||
```go
|
||||
func parseConfig(json gjson.Result, config *MyConfig) error {
|
||||
// Register tick functions during config parsing
|
||||
wrapper.RegisterTickFunc(1000, func() {
|
||||
// Executes every 1 second
|
||||
log.Info("1s tick")
|
||||
})
|
||||
|
||||
wrapper.RegisterTickFunc(5000, func() {
|
||||
// Executes every 5 seconds
|
||||
log.Info("5s tick")
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
## Leader Election
|
||||
|
||||
For tasks that should run on only one VM instance:
|
||||
|
||||
```go
|
||||
func init() {
|
||||
wrapper.SetCtx(
|
||||
"leader-plugin",
|
||||
wrapper.PrePluginStartOrReload(onPluginStart),
|
||||
wrapper.ParseConfig(parseConfig),
|
||||
)
|
||||
}
|
||||
|
||||
func onPluginStart(ctx wrapper.PluginContext) error {
|
||||
ctx.DoLeaderElection()
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseConfig(json gjson.Result, config *MyConfig) error {
|
||||
wrapper.RegisterTickFunc(10000, func() {
|
||||
if ctx.IsLeader() {
|
||||
// Only leader executes this
|
||||
log.Info("Leader task")
|
||||
}
|
||||
})
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
## Plugin Context Storage
|
||||
|
||||
Store data across requests at plugin level:
|
||||
|
||||
```go
|
||||
type MyConfig struct {
|
||||
// Config fields
|
||||
}
|
||||
|
||||
func init() {
|
||||
wrapper.SetCtx(
|
||||
"context-plugin",
|
||||
wrapper.ParseConfigWithContext(parseConfigWithContext),
|
||||
wrapper.ProcessRequestHeaders(onHttpRequestHeaders),
|
||||
)
|
||||
}
|
||||
|
||||
func parseConfigWithContext(ctx wrapper.PluginContext, json gjson.Result, config *MyConfig) error {
|
||||
// Store in plugin context (survives across requests)
|
||||
ctx.SetContext("initTime", time.Now().Unix())
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
## Rule-Level Config Isolation
|
||||
|
||||
Enable graceful degradation when rule config parsing fails:
|
||||
|
||||
```go
|
||||
func init() {
|
||||
wrapper.SetCtx(
|
||||
"isolated-plugin",
|
||||
wrapper.PrePluginStartOrReload(func(ctx wrapper.PluginContext) error {
|
||||
ctx.EnableRuleLevelConfigIsolation()
|
||||
return nil
|
||||
}),
|
||||
wrapper.ParseOverrideConfig(parseGlobal, parseRule),
|
||||
)
|
||||
}
|
||||
|
||||
func parseGlobal(json gjson.Result, config *MyConfig) error {
|
||||
// Parse global config
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseRule(json gjson.Result, global MyConfig, config *MyConfig) error {
|
||||
// Parse per-rule config, inheriting from global
|
||||
*config = global // Copy global defaults
|
||||
// Override with rule-specific values
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
## Memory Management
|
||||
|
||||
Configure automatic VM rebuild to prevent memory leaks:
|
||||
|
||||
```go
|
||||
func init() {
|
||||
wrapper.SetCtxWithOptions(
|
||||
"memory-managed-plugin",
|
||||
wrapper.ParseConfig(parseConfig),
|
||||
wrapper.WithRebuildAfterRequests(10000), // Rebuild after 10k requests
|
||||
wrapper.WithRebuildMaxMemBytes(100*1024*1024), // Rebuild at 100MB
|
||||
wrapper.WithMaxRequestsPerIoCycle(20), // Limit concurrent requests
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Logging
|
||||
|
||||
Add structured fields to access logs:
|
||||
|
||||
```go
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
|
||||
// Set custom attributes
|
||||
ctx.SetUserAttribute("user_id", "12345")
|
||||
ctx.SetUserAttribute("request_type", "api")
|
||||
|
||||
return types.HeaderContinue
|
||||
}
|
||||
|
||||
func onHttpResponseHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
|
||||
// Write to access log
|
||||
ctx.WriteUserAttributeToLog()
|
||||
|
||||
// Or write to trace spans
|
||||
ctx.WriteUserAttributeToTrace()
|
||||
|
||||
return types.HeaderContinue
|
||||
}
|
||||
```
|
||||
|
||||
## Disable Re-routing
|
||||
|
||||
Prevent Envoy from recalculating routes after header modification:
|
||||
|
||||
```go
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
|
||||
// Call BEFORE modifying headers
|
||||
ctx.DisableReroute()
|
||||
|
||||
// Now safe to modify headers without triggering re-route
|
||||
proxywasm.ReplaceHttpRequestHeader(":path", "/new-path")
|
||||
|
||||
return types.HeaderContinue
|
||||
}
|
||||
```
|
||||
|
||||
## Buffer Limits
|
||||
|
||||
Set per-request buffer limits to control memory usage:
|
||||
|
||||
```go
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
|
||||
// Allow larger request bodies for this request
|
||||
ctx.SetRequestBodyBufferLimit(10 * 1024 * 1024) // 10MB
|
||||
return types.HeaderContinue
|
||||
}
|
||||
|
||||
func onHttpResponseHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
|
||||
// Allow larger response bodies
|
||||
ctx.SetResponseBodyBufferLimit(50 * 1024 * 1024) // 50MB
|
||||
return types.HeaderContinue
|
||||
}
|
||||
```
|
||||
@@ -1,179 +0,0 @@
|
||||
# HTTP Client Reference
|
||||
|
||||
## Cluster Types
|
||||
|
||||
### FQDNCluster (Most Common)
|
||||
|
||||
For services registered in Higress with FQDN:
|
||||
|
||||
```go
|
||||
wrapper.NewClusterClient(wrapper.FQDNCluster{
|
||||
FQDN: "my-service.dns", // Service FQDN with suffix
|
||||
Port: 8080,
|
||||
Host: "optional-host-header", // Optional
|
||||
})
|
||||
```
|
||||
|
||||
Common FQDN suffixes:
|
||||
- `.dns` - DNS service
|
||||
- `.static` - Static IP service (port defaults to 80)
|
||||
- `.nacos` - Nacos service
|
||||
|
||||
### K8sCluster
|
||||
|
||||
For Kubernetes services:
|
||||
|
||||
```go
|
||||
wrapper.NewClusterClient(wrapper.K8sCluster{
|
||||
ServiceName: "my-service",
|
||||
Namespace: "default",
|
||||
Port: 8080,
|
||||
Version: "", // Optional subset version
|
||||
})
|
||||
// Generates: outbound|8080||my-service.default.svc.cluster.local
|
||||
```
|
||||
|
||||
### NacosCluster
|
||||
|
||||
For Nacos registry services:
|
||||
|
||||
```go
|
||||
wrapper.NewClusterClient(wrapper.NacosCluster{
|
||||
ServiceName: "my-service",
|
||||
Group: "DEFAULT-GROUP",
|
||||
NamespaceID: "public",
|
||||
Port: 8080,
|
||||
IsExtRegistry: false, // true for EDAS/SAE
|
||||
})
|
||||
```
|
||||
|
||||
### StaticIpCluster
|
||||
|
||||
For static IP services:
|
||||
|
||||
```go
|
||||
wrapper.NewClusterClient(wrapper.StaticIpCluster{
|
||||
ServiceName: "my-service",
|
||||
Port: 8080,
|
||||
})
|
||||
// Generates: outbound|8080||my-service.static
|
||||
```
|
||||
|
||||
### DnsCluster
|
||||
|
||||
For DNS-resolved services:
|
||||
|
||||
```go
|
||||
wrapper.NewClusterClient(wrapper.DnsCluster{
|
||||
ServiceName: "my-service",
|
||||
Domain: "api.example.com",
|
||||
Port: 443,
|
||||
})
|
||||
```
|
||||
|
||||
### RouteCluster
|
||||
|
||||
Use current route's upstream:
|
||||
|
||||
```go
|
||||
wrapper.NewClusterClient(wrapper.RouteCluster{
|
||||
Host: "optional-host-override",
|
||||
})
|
||||
```
|
||||
|
||||
### TargetCluster
|
||||
|
||||
Direct cluster name specification:
|
||||
|
||||
```go
|
||||
wrapper.NewClusterClient(wrapper.TargetCluster{
|
||||
Cluster: "outbound|8080||my-service.dns",
|
||||
Host: "api.example.com",
|
||||
})
|
||||
```
|
||||
|
||||
## HTTP Methods
|
||||
|
||||
```go
|
||||
client.Get(path, headers, callback, timeout...)
|
||||
client.Post(path, headers, body, callback, timeout...)
|
||||
client.Put(path, headers, body, callback, timeout...)
|
||||
client.Patch(path, headers, body, callback, timeout...)
|
||||
client.Delete(path, headers, body, callback, timeout...)
|
||||
client.Head(path, headers, callback, timeout...)
|
||||
client.Options(path, headers, callback, timeout...)
|
||||
client.Call(method, path, headers, body, callback, timeout...)
|
||||
```
|
||||
|
||||
## Callback Signature
|
||||
|
||||
```go
|
||||
func(statusCode int, responseHeaders http.Header, responseBody []byte)
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
```go
|
||||
type MyConfig struct {
|
||||
client wrapper.HttpClient
|
||||
requestPath string
|
||||
tokenHeader string
|
||||
}
|
||||
|
||||
func parseConfig(json gjson.Result, config *MyConfig) error {
|
||||
config.tokenHeader = json.Get("tokenHeader").String()
|
||||
if config.tokenHeader == "" {
|
||||
return errors.New("missing tokenHeader")
|
||||
}
|
||||
|
||||
config.requestPath = json.Get("requestPath").String()
|
||||
if config.requestPath == "" {
|
||||
return errors.New("missing requestPath")
|
||||
}
|
||||
|
||||
serviceName := json.Get("serviceName").String()
|
||||
servicePort := json.Get("servicePort").Int()
|
||||
if servicePort == 0 {
|
||||
if strings.HasSuffix(serviceName, ".static") {
|
||||
servicePort = 80
|
||||
}
|
||||
}
|
||||
|
||||
config.client = wrapper.NewClusterClient(wrapper.FQDNCluster{
|
||||
FQDN: serviceName,
|
||||
Port: servicePort,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
|
||||
err := config.client.Get(config.requestPath, nil,
|
||||
func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
if statusCode != http.StatusOK {
|
||||
log.Errorf("http call failed, status: %d", statusCode)
|
||||
proxywasm.SendHttpResponse(http.StatusInternalServerError, nil,
|
||||
[]byte("http call failed"), -1)
|
||||
return
|
||||
}
|
||||
|
||||
token := responseHeaders.Get(config.tokenHeader)
|
||||
if token != "" {
|
||||
proxywasm.AddHttpRequestHeader(config.tokenHeader, token)
|
||||
}
|
||||
proxywasm.ResumeHttpRequest()
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("http call dispatch failed: %v", err)
|
||||
return types.HeaderContinue
|
||||
}
|
||||
return types.HeaderStopAllIterationAndWatermark
|
||||
}
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
1. **Cannot use net/http** - Must use wrapper's HTTP client
|
||||
2. **Default timeout is 500ms** - Pass explicit timeout for longer calls
|
||||
3. **Callback is async** - Must return `HeaderStopAllIterationAndWatermark` and call `ResumeHttpRequest()` in callback
|
||||
4. **Error handling** - If dispatch fails, return `HeaderContinue` to avoid blocking
|
||||
@@ -1,189 +0,0 @@
|
||||
# Local Testing with Docker Compose
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker installed
|
||||
- Compiled `main.wasm` file
|
||||
|
||||
## Setup
|
||||
|
||||
Create these files in your plugin directory:
|
||||
|
||||
### docker-compose.yaml
|
||||
|
||||
```yaml
|
||||
version: '3.7'
|
||||
services:
|
||||
envoy:
|
||||
image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/gateway:v2.1.5
|
||||
entrypoint: /usr/local/bin/envoy
|
||||
command: -c /etc/envoy/envoy.yaml --component-log-level wasm:debug
|
||||
depends_on:
|
||||
- httpbin
|
||||
networks:
|
||||
- wasmtest
|
||||
ports:
|
||||
- "10000:10000"
|
||||
volumes:
|
||||
- ./envoy.yaml:/etc/envoy/envoy.yaml
|
||||
- ./main.wasm:/etc/envoy/main.wasm
|
||||
|
||||
httpbin:
|
||||
image: kennethreitz/httpbin:latest
|
||||
networks:
|
||||
- wasmtest
|
||||
ports:
|
||||
- "12345:80"
|
||||
|
||||
networks:
|
||||
wasmtest: {}
|
||||
```
|
||||
|
||||
### envoy.yaml
|
||||
|
||||
```yaml
|
||||
admin:
|
||||
address:
|
||||
socket_address:
|
||||
protocol: TCP
|
||||
address: 0.0.0.0
|
||||
port_value: 9901
|
||||
|
||||
static_resources:
|
||||
listeners:
|
||||
- name: listener_0
|
||||
address:
|
||||
socket_address:
|
||||
protocol: TCP
|
||||
address: 0.0.0.0
|
||||
port_value: 10000
|
||||
filter_chains:
|
||||
- filters:
|
||||
- name: envoy.filters.network.http_connection_manager
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
|
||||
scheme_header_transformation:
|
||||
scheme_to_overwrite: https
|
||||
stat_prefix: ingress_http
|
||||
route_config:
|
||||
name: local_route
|
||||
virtual_hosts:
|
||||
- name: local_service
|
||||
domains: ["*"]
|
||||
routes:
|
||||
- match:
|
||||
prefix: "/"
|
||||
route:
|
||||
cluster: httpbin
|
||||
http_filters:
|
||||
- name: wasmdemo
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/udpa.type.v1.TypedStruct
|
||||
type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
|
||||
value:
|
||||
config:
|
||||
name: wasmdemo
|
||||
vm_config:
|
||||
runtime: envoy.wasm.runtime.v8
|
||||
code:
|
||||
local:
|
||||
filename: /etc/envoy/main.wasm
|
||||
configuration:
|
||||
"@type": "type.googleapis.com/google.protobuf.StringValue"
|
||||
value: |
|
||||
{
|
||||
"mockEnable": false
|
||||
}
|
||||
- name: envoy.filters.http.router
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
|
||||
|
||||
clusters:
|
||||
- name: httpbin
|
||||
connect_timeout: 30s
|
||||
type: LOGICAL_DNS
|
||||
dns_lookup_family: V4_ONLY
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: httpbin
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: httpbin
|
||||
port_value: 80
|
||||
```
|
||||
|
||||
## Running
|
||||
|
||||
```bash
|
||||
# Start
|
||||
docker compose up
|
||||
|
||||
# Test without gateway (baseline)
|
||||
curl http://127.0.0.1:12345/get
|
||||
|
||||
# Test with gateway (plugin applied)
|
||||
curl http://127.0.0.1:10000/get
|
||||
|
||||
# Stop
|
||||
docker compose down
|
||||
```
|
||||
|
||||
## Modifying Plugin Config
|
||||
|
||||
1. Edit the `configuration.value` section in `envoy.yaml`
|
||||
2. Restart: `docker compose restart envoy`
|
||||
|
||||
## Viewing Logs
|
||||
|
||||
```bash
|
||||
# Follow Envoy logs
|
||||
docker compose logs -f envoy
|
||||
|
||||
# WASM debug logs (enabled by --component-log-level wasm:debug)
|
||||
```
|
||||
|
||||
## Adding External Services
|
||||
|
||||
To test external HTTP/Redis calls, add services to docker-compose.yaml:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
# ... existing services ...
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
networks:
|
||||
- wasmtest
|
||||
ports:
|
||||
- "6379:6379"
|
||||
|
||||
auth-service:
|
||||
image: your-auth-service:latest
|
||||
networks:
|
||||
- wasmtest
|
||||
```
|
||||
|
||||
Then add clusters to envoy.yaml:
|
||||
|
||||
```yaml
|
||||
clusters:
|
||||
# ... existing clusters ...
|
||||
|
||||
- name: outbound|6379||redis.static
|
||||
connect_timeout: 5s
|
||||
type: LOGICAL_DNS
|
||||
dns_lookup_family: V4_ONLY
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: redis
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: redis
|
||||
port_value: 6379
|
||||
```
|
||||
@@ -1,215 +0,0 @@
|
||||
# Redis Client Reference
|
||||
|
||||
## Initialization
|
||||
|
||||
```go
|
||||
type MyConfig struct {
|
||||
redis wrapper.RedisClient
|
||||
qpm int
|
||||
}
|
||||
|
||||
func parseConfig(json gjson.Result, config *MyConfig) error {
|
||||
serviceName := json.Get("serviceName").String()
|
||||
servicePort := json.Get("servicePort").Int()
|
||||
if servicePort == 0 {
|
||||
servicePort = 6379
|
||||
}
|
||||
|
||||
config.redis = wrapper.NewRedisClusterClient(wrapper.FQDNCluster{
|
||||
FQDN: serviceName,
|
||||
Port: servicePort,
|
||||
})
|
||||
|
||||
return config.redis.Init(
|
||||
json.Get("username").String(),
|
||||
json.Get("password").String(),
|
||||
json.Get("timeout").Int(), // milliseconds
|
||||
// Optional settings:
|
||||
// wrapper.WithDataBase(1),
|
||||
// wrapper.WithBufferFlushTimeout(3*time.Millisecond),
|
||||
// wrapper.WithMaxBufferSizeBeforeFlush(1024),
|
||||
// wrapper.WithDisableBuffer(), // For latency-sensitive scenarios
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Callback Signature
|
||||
|
||||
```go
|
||||
func(response resp.Value)
|
||||
|
||||
// Check for errors
|
||||
if response.Error() != nil {
|
||||
// Handle error
|
||||
}
|
||||
|
||||
// Get values
|
||||
response.Integer() // int
|
||||
response.String() // string
|
||||
response.Bool() // bool
|
||||
response.Array() // []resp.Value
|
||||
response.Bytes() // []byte
|
||||
```
|
||||
|
||||
## Available Commands
|
||||
|
||||
### Key Operations
|
||||
|
||||
```go
|
||||
redis.Del(key, callback)
|
||||
redis.Exists(key, callback)
|
||||
redis.Expire(key, ttlSeconds, callback)
|
||||
redis.Persist(key, callback)
|
||||
```
|
||||
|
||||
### String Operations
|
||||
|
||||
```go
|
||||
redis.Get(key, callback)
|
||||
redis.Set(key, value, callback)
|
||||
redis.SetEx(key, value, ttlSeconds, callback)
|
||||
redis.SetNX(key, value, ttlSeconds, callback) // ttl=0 means no expiry
|
||||
redis.MGet(keys, callback)
|
||||
redis.MSet(kvMap, callback)
|
||||
redis.Incr(key, callback)
|
||||
redis.Decr(key, callback)
|
||||
redis.IncrBy(key, delta, callback)
|
||||
redis.DecrBy(key, delta, callback)
|
||||
```
|
||||
|
||||
### List Operations
|
||||
|
||||
```go
|
||||
redis.LLen(key, callback)
|
||||
redis.RPush(key, values, callback)
|
||||
redis.RPop(key, callback)
|
||||
redis.LPush(key, values, callback)
|
||||
redis.LPop(key, callback)
|
||||
redis.LIndex(key, index, callback)
|
||||
redis.LRange(key, start, stop, callback)
|
||||
redis.LRem(key, count, value, callback)
|
||||
redis.LInsertBefore(key, pivot, value, callback)
|
||||
redis.LInsertAfter(key, pivot, value, callback)
|
||||
```
|
||||
|
||||
### Hash Operations
|
||||
|
||||
```go
|
||||
redis.HExists(key, field, callback)
|
||||
redis.HDel(key, fields, callback)
|
||||
redis.HLen(key, callback)
|
||||
redis.HGet(key, field, callback)
|
||||
redis.HSet(key, field, value, callback)
|
||||
redis.HMGet(key, fields, callback)
|
||||
redis.HMSet(key, kvMap, callback)
|
||||
redis.HKeys(key, callback)
|
||||
redis.HVals(key, callback)
|
||||
redis.HGetAll(key, callback)
|
||||
redis.HIncrBy(key, field, delta, callback)
|
||||
redis.HIncrByFloat(key, field, delta, callback)
|
||||
```
|
||||
|
||||
### Set Operations
|
||||
|
||||
```go
|
||||
redis.SCard(key, callback)
|
||||
redis.SAdd(key, values, callback)
|
||||
redis.SRem(key, values, callback)
|
||||
redis.SIsMember(key, value, callback)
|
||||
redis.SMembers(key, callback)
|
||||
redis.SDiff(key1, key2, callback)
|
||||
redis.SDiffStore(dest, key1, key2, callback)
|
||||
redis.SInter(key1, key2, callback)
|
||||
redis.SInterStore(dest, key1, key2, callback)
|
||||
redis.SUnion(key1, key2, callback)
|
||||
redis.SUnionStore(dest, key1, key2, callback)
|
||||
```
|
||||
|
||||
### Sorted Set Operations
|
||||
|
||||
```go
|
||||
redis.ZCard(key, callback)
|
||||
redis.ZAdd(key, memberScoreMap, callback)
|
||||
redis.ZCount(key, min, max, callback)
|
||||
redis.ZIncrBy(key, member, delta, callback)
|
||||
redis.ZScore(key, member, callback)
|
||||
redis.ZRank(key, member, callback)
|
||||
redis.ZRevRank(key, member, callback)
|
||||
redis.ZRem(key, members, callback)
|
||||
redis.ZRange(key, start, stop, callback)
|
||||
redis.ZRevRange(key, start, stop, callback)
|
||||
```
|
||||
|
||||
### Lua Script
|
||||
|
||||
```go
|
||||
redis.Eval(script, numkeys, keys, args, callback)
|
||||
```
|
||||
|
||||
### Raw Command
|
||||
|
||||
```go
|
||||
redis.Command([]interface{}{"SET", "key", "value"}, callback)
|
||||
```
|
||||
|
||||
## Rate Limiting Example
|
||||
|
||||
```go
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
|
||||
now := time.Now()
|
||||
minuteAligned := now.Truncate(time.Minute)
|
||||
timeStamp := strconv.FormatInt(minuteAligned.Unix(), 10)
|
||||
|
||||
err := config.redis.Incr(timeStamp, func(response resp.Value) {
|
||||
if response.Error() != nil {
|
||||
log.Errorf("redis error: %v", response.Error())
|
||||
proxywasm.ResumeHttpRequest()
|
||||
return
|
||||
}
|
||||
|
||||
count := response.Integer()
|
||||
ctx.SetContext("timeStamp", timeStamp)
|
||||
ctx.SetContext("callTimeLeft", strconv.Itoa(config.qpm - count))
|
||||
|
||||
if count == 1 {
|
||||
// First request in this minute, set expiry
|
||||
config.redis.Expire(timeStamp, 60, func(response resp.Value) {
|
||||
if response.Error() != nil {
|
||||
log.Errorf("expire error: %v", response.Error())
|
||||
}
|
||||
proxywasm.ResumeHttpRequest()
|
||||
})
|
||||
} else if count > config.qpm {
|
||||
proxywasm.SendHttpResponse(429, [][2]string{
|
||||
{"timeStamp", timeStamp},
|
||||
{"callTimeLeft", "0"},
|
||||
}, []byte("Too many requests\n"), -1)
|
||||
} else {
|
||||
proxywasm.ResumeHttpRequest()
|
||||
}
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("redis call failed: %v", err)
|
||||
return types.HeaderContinue
|
||||
}
|
||||
return types.HeaderStopAllIterationAndWatermark
|
||||
}
|
||||
|
||||
func onHttpResponseHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
|
||||
if ts := ctx.GetContext("timeStamp"); ts != nil {
|
||||
proxywasm.AddHttpResponseHeader("timeStamp", ts.(string))
|
||||
}
|
||||
if left := ctx.GetContext("callTimeLeft"); left != nil {
|
||||
proxywasm.AddHttpResponseHeader("callTimeLeft", left.(string))
|
||||
}
|
||||
return types.HeaderContinue
|
||||
}
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
1. **Check Ready()** - `redis.Ready()` returns false if init failed
|
||||
2. **Auto-reconnect** - Client handles NOAUTH errors and re-authenticates automatically
|
||||
3. **Buffering** - Default 3ms flush timeout and 1024 byte buffer; use `WithDisableBuffer()` for latency-sensitive scenarios
|
||||
4. **Error handling** - Always check `response.Error()` in callbacks
|
||||
@@ -1,495 +0,0 @@
|
||||
# Nginx to Higress Migration Skill
|
||||
|
||||
Complete end-to-end solution for migrating from ingress-nginx to Higress gateway, featuring intelligent compatibility validation, automated migration toolchain, and AI-driven capability enhancement.
|
||||
|
||||
## Overview
|
||||
|
||||
This skill is built on real-world production migration experience, providing:
|
||||
- 🔍 **Configuration Analysis & Compatibility Assessment**: Automated scanning of nginx Ingress configurations to identify migration risks
|
||||
- 🧪 **Kind Cluster Simulation**: Local fast verification of configuration compatibility to ensure safe migration
|
||||
- 🚀 **Gradual Migration Strategy**: Phased migration approach to minimize business risk
|
||||
- 🤖 **AI-Driven Capability Enhancement**: Automated WASM plugin development to fill gaps in Higress functionality
|
||||
|
||||
## Core Advantages
|
||||
|
||||
### 🎯 Simple Mode: Zero-Configuration Migration
|
||||
|
||||
**For standard Ingress resources with common nginx annotations:**
|
||||
|
||||
✅ **100% Annotation Compatibility** - All standard `nginx.ingress.kubernetes.io/*` annotations work out-of-the-box
|
||||
✅ **Zero Configuration Changes** - Apply your existing Ingress YAML directly to Higress
|
||||
✅ **Instant Migration** - No learning curve, no manual conversion, no risk
|
||||
✅ **Parallel Deployment** - Install Higress alongside nginx for safe testing
|
||||
|
||||
**Example:**
|
||||
```yaml
|
||||
# Your existing nginx Ingress - works immediately on Higress
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/rewrite-target: /api/$2
|
||||
nginx.ingress.kubernetes.io/rate-limit: "100"
|
||||
nginx.ingress.kubernetes.io/cors-allow-origin: "*"
|
||||
spec:
|
||||
ingressClassName: nginx # Same class name, both controllers watch it
|
||||
rules:
|
||||
- host: api.example.com
|
||||
http:
|
||||
paths:
|
||||
- path: /v1(/|$)(.*)
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: backend
|
||||
port:
|
||||
number: 8080
|
||||
```
|
||||
|
||||
**No conversion needed. No manual rewrite. Just deploy and validate.**
|
||||
|
||||
### ⚙️ Complex Mode: Full DevOps Automation for Custom Plugins
|
||||
|
||||
**When nginx snippets or custom Lua logic require WASM plugins:**
|
||||
|
||||
✅ **Automated Requirement Analysis** - AI extracts functionality from nginx snippets
|
||||
✅ **Code Generation** - Type-safe Go code with proxy-wasm SDK automatically generated
|
||||
✅ **Build & Validation** - Compile, test, and package as OCI images
|
||||
✅ **Production Deployment** - Push to registry and deploy WasmPlugin CRD
|
||||
|
||||
**Complete workflow automation:**
|
||||
```
|
||||
nginx snippet → AI analysis → Go WASM code → Build → Test → Deploy → Validate
|
||||
↓ ↓ ↓ ↓ ↓ ↓ ↓
|
||||
minutes seconds seconds seconds 1min instant instant
|
||||
```
|
||||
|
||||
**Example: Custom IP-based routing + HMAC signature validation**
|
||||
|
||||
**Original nginx snippet:**
|
||||
```nginx
|
||||
location /payment {
|
||||
access_by_lua_block {
|
||||
local client_ip = ngx.var.remote_addr
|
||||
local signature = ngx.req.get_headers()["X-Signature"]
|
||||
-- Complex IP routing and HMAC validation logic
|
||||
if not validate_signature(signature) then
|
||||
ngx.exit(403)
|
||||
end
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**AI-generated WASM plugin** (automatic):
|
||||
1. Analyze requirement: IP routing + HMAC-SHA256 validation
|
||||
2. Generate Go code with proper error handling
|
||||
3. Build, test, deploy - **fully automated**
|
||||
|
||||
**Result**: Original functionality preserved, business logic unchanged, zero manual coding required.
|
||||
|
||||
## Migration Workflow
|
||||
|
||||
### Mode 1: Simple Migration (Standard Ingress)
|
||||
|
||||
**Prerequisites**: Your Ingress uses standard annotations (check with `kubectl get ingress -A -o yaml`)
|
||||
|
||||
**Steps:**
|
||||
```bash
|
||||
# 1. Install Higress alongside nginx (same ingressClass)
|
||||
helm install higress higress/higress \
|
||||
-n higress-system --create-namespace \
|
||||
--set global.ingressClass=nginx \
|
||||
--set global.enableStatus=false
|
||||
|
||||
# 2. Generate validation tests
|
||||
./scripts/generate-migration-test.sh > test.sh
|
||||
|
||||
# 3. Run tests against Higress gateway
|
||||
./test.sh ${HIGRESS_IP}
|
||||
|
||||
# 4. If all tests pass → switch traffic (DNS/LB)
|
||||
# nginx continues running as fallback
|
||||
```
|
||||
|
||||
**Timeline**: 30 minutes for 50+ Ingress resources (including validation)
|
||||
|
||||
### Mode 2: Complex Migration (Custom Snippets/Lua)
|
||||
|
||||
**Prerequisites**: Your Ingress uses `server-snippet`, `configuration-snippet`, or Lua logic
|
||||
|
||||
**Steps:**
|
||||
```bash
|
||||
# 1. Analyze incompatible features
|
||||
./scripts/analyze-ingress.sh
|
||||
|
||||
# 2. For each snippet:
|
||||
# - AI reads the snippet
|
||||
# - Designs WASM plugin architecture
|
||||
# - Generates type-safe Go code
|
||||
# - Builds and validates
|
||||
|
||||
# 3. Deploy plugins
|
||||
kubectl apply -f generated-wasm-plugins/
|
||||
|
||||
# 4. Validate + switch traffic
|
||||
```
|
||||
|
||||
**Timeline**: 1-2 hours including AI-driven plugin development
|
||||
|
||||
## AI Execution Example
|
||||
|
||||
**User**: "Migrate my nginx Ingress to Higress"
|
||||
|
||||
**AI Agent Workflow**:
|
||||
|
||||
1. **Discovery**
|
||||
```bash
|
||||
kubectl get ingress -A -o yaml > backup.yaml
|
||||
kubectl get configmap -n ingress-nginx ingress-nginx-controller -o yaml
|
||||
```
|
||||
|
||||
2. **Compatibility Analysis**
|
||||
- ✅ Standard annotations: direct migration
|
||||
- ⚠️ Snippet annotations: require WASM plugins
|
||||
- Identify patterns: rate limiting, auth, routing logic
|
||||
|
||||
3. **Parallel Deployment**
|
||||
```bash
|
||||
helm install higress higress/higress -n higress-system \
|
||||
--set global.ingressClass=nginx \
|
||||
--set global.enableStatus=false
|
||||
```
|
||||
|
||||
4. **Automated Testing**
|
||||
```bash
|
||||
./scripts/generate-migration-test.sh > test.sh
|
||||
./test.sh ${HIGRESS_IP}
|
||||
# ✅ 60/60 routes passed
|
||||
```
|
||||
|
||||
5. **Plugin Development** (if needed)
|
||||
- Read `higress-wasm-go-plugin` skill
|
||||
- Generate Go code for custom logic
|
||||
- Build, validate, deploy
|
||||
- Re-test affected routes
|
||||
|
||||
6. **Gradual Cutover**
|
||||
- Phase 1: 10% traffic → validate
|
||||
- Phase 2: 50% traffic → monitor
|
||||
- Phase 3: 100% traffic → decommission nginx
|
||||
|
||||
## Production Case Studies
|
||||
|
||||
### Case 1: E-Commerce API Gateway (60+ Ingress Resources)
|
||||
|
||||
**Environment**:
|
||||
- 60+ Ingress resources
|
||||
- 3-node HA cluster
|
||||
- TLS termination for 15+ domains
|
||||
- Rate limiting, CORS, JWT auth
|
||||
|
||||
**Migration**:
|
||||
```yaml
|
||||
# Example Ingress (one of 60+)
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: product-api
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/rewrite-target: /$2
|
||||
nginx.ingress.kubernetes.io/rate-limit: "1000"
|
||||
nginx.ingress.kubernetes.io/cors-allow-origin: "https://shop.example.com"
|
||||
nginx.ingress.kubernetes.io/auth-url: "http://auth-service/validate"
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
tls:
|
||||
- hosts:
|
||||
- api.example.com
|
||||
secretName: api-tls
|
||||
rules:
|
||||
- host: api.example.com
|
||||
http:
|
||||
paths:
|
||||
- path: /api(/|$)(.*)
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: product-service
|
||||
port:
|
||||
number: 8080
|
||||
```
|
||||
|
||||
**Validation in Kind cluster**:
|
||||
```bash
|
||||
# Apply directly without modification
|
||||
kubectl apply -f product-api-ingress.yaml
|
||||
|
||||
# Test all functionality
|
||||
curl https://api.example.com/api/products/123
|
||||
# ✅ URL rewrite: /products/123 (correct)
|
||||
# ✅ Rate limiting: active
|
||||
# ✅ CORS headers: injected
|
||||
# ✅ Auth validation: working
|
||||
# ✅ TLS certificate: valid
|
||||
```
|
||||
|
||||
**Results**:
|
||||
| Metric | Value | Notes |
|
||||
|--------|-------|-------|
|
||||
| Ingress resources migrated | 60+ | Zero modification |
|
||||
| Annotation types supported | 20+ | 100% compatibility |
|
||||
| TLS certificates | 15+ | Direct secret reuse |
|
||||
| Configuration changes | **0** | No YAML edits needed |
|
||||
| Migration time | **30 min** | Including validation |
|
||||
| Downtime | **0 sec** | Zero-downtime cutover |
|
||||
| Rollback needed | **0** | All tests passed |
|
||||
|
||||
### Case 2: Financial Services with Custom Auth Logic
|
||||
|
||||
**Challenge**: Payment service required custom IP-based routing + HMAC-SHA256 request signing validation (implemented as nginx Lua snippet)
|
||||
|
||||
**Original nginx configuration**:
|
||||
```nginx
|
||||
location /payment/process {
|
||||
access_by_lua_block {
|
||||
local client_ip = ngx.var.remote_addr
|
||||
local signature = ngx.req.get_headers()["X-Payment-Signature"]
|
||||
local timestamp = ngx.req.get_headers()["X-Timestamp"]
|
||||
|
||||
-- IP allowlist check
|
||||
if not is_allowed_ip(client_ip) then
|
||||
ngx.log(ngx.ERR, "Blocked IP: " .. client_ip)
|
||||
ngx.exit(403)
|
||||
end
|
||||
|
||||
-- HMAC-SHA256 signature validation
|
||||
local payload = ngx.var.request_uri .. timestamp
|
||||
local expected_sig = compute_hmac_sha256(payload, secret_key)
|
||||
|
||||
if signature ~= expected_sig then
|
||||
ngx.log(ngx.ERR, "Invalid signature from: " .. client_ip)
|
||||
ngx.exit(403)
|
||||
end
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**AI-Driven Plugin Development**:
|
||||
|
||||
1. **Requirement Analysis** (AI reads snippet)
|
||||
- IP allowlist validation
|
||||
- HMAC-SHA256 signature verification
|
||||
- Request timestamp validation
|
||||
- Error logging requirements
|
||||
|
||||
2. **Auto-Generated WASM Plugin** (Go)
|
||||
```go
|
||||
// Auto-generated by AI agent
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
|
||||
)
|
||||
|
||||
type PaymentAuthPlugin struct {
|
||||
proxywasm.DefaultPluginContext
|
||||
}
|
||||
|
||||
func (ctx *PaymentAuthPlugin) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action {
|
||||
// IP allowlist check
|
||||
clientIP, _ := proxywasm.GetProperty([]string{"source", "address"})
|
||||
if !isAllowedIP(string(clientIP)) {
|
||||
proxywasm.LogError("Blocked IP: " + string(clientIP))
|
||||
proxywasm.SendHttpResponse(403, nil, []byte("Forbidden"), -1)
|
||||
return types.ActionPause
|
||||
}
|
||||
|
||||
// HMAC signature validation
|
||||
signature, _ := proxywasm.GetHttpRequestHeader("X-Payment-Signature")
|
||||
timestamp, _ := proxywasm.GetHttpRequestHeader("X-Timestamp")
|
||||
uri, _ := proxywasm.GetProperty([]string{"request", "path"})
|
||||
|
||||
payload := string(uri) + timestamp
|
||||
expectedSig := computeHMAC(payload, secretKey)
|
||||
|
||||
if signature != expectedSig {
|
||||
proxywasm.LogError("Invalid signature from: " + string(clientIP))
|
||||
proxywasm.SendHttpResponse(403, nil, []byte("Invalid signature"), -1)
|
||||
return types.ActionPause
|
||||
}
|
||||
|
||||
return types.ActionContinue
|
||||
}
|
||||
```
|
||||
|
||||
3. **Automated Build & Deployment**
|
||||
```bash
|
||||
# AI agent executes automatically:
|
||||
go mod tidy
|
||||
GOOS=wasip1 GOARCH=wasm go build -o payment-auth.wasm
|
||||
docker build -t registry.example.com/payment-auth:v1 .
|
||||
docker push registry.example.com/payment-auth:v1
|
||||
|
||||
kubectl apply -f - <<EOF
|
||||
apiVersion: extensions.higress.io/v1alpha1
|
||||
kind: WasmPlugin
|
||||
metadata:
|
||||
name: payment-auth
|
||||
namespace: higress-system
|
||||
spec:
|
||||
url: oci://registry.example.com/payment-auth:v1
|
||||
phase: AUTHN
|
||||
priority: 100
|
||||
EOF
|
||||
```
|
||||
|
||||
**Results**:
|
||||
- ✅ Original functionality preserved (IP check + HMAC validation)
|
||||
- ✅ Improved security (type-safe code, compiled WASM)
|
||||
- ✅ Better performance (native WASM vs interpreted Lua)
|
||||
- ✅ Full automation (requirement → deployment in <10 minutes)
|
||||
- ✅ Zero business logic changes required
|
||||
|
||||
### Case 3: Multi-Tenant SaaS Platform (Custom Routing)
|
||||
|
||||
**Challenge**: Route requests to different backend clusters based on tenant ID in JWT token
|
||||
|
||||
**AI Solution**:
|
||||
- Extract tenant ID from JWT claims
|
||||
- Generate WASM plugin for dynamic upstream selection
|
||||
- Deploy with zero manual coding
|
||||
|
||||
**Timeline**: 15 minutes (analysis → code → deploy → validate)
|
||||
|
||||
## Key Statistics
|
||||
|
||||
### Migration Efficiency
|
||||
|
||||
| Metric | Simple Mode | Complex Mode |
|
||||
|--------|-------------|--------------|
|
||||
| Configuration compatibility | 100% | 95%+ |
|
||||
| Manual code changes required | 0 | 0 (AI-generated) |
|
||||
| Average migration time | 30 min | 1-2 hours |
|
||||
| Downtime required | 0 | 0 |
|
||||
| Rollback complexity | Trivial | Simple |
|
||||
|
||||
### Production Validation
|
||||
|
||||
- **Total Ingress resources migrated**: 200+
|
||||
- **Environments**: Financial services, e-commerce, SaaS platforms
|
||||
- **Success rate**: 100% (all production deployments successful)
|
||||
- **Average configuration compatibility**: 98%
|
||||
- **Plugin development time saved**: 80% (AI-driven automation)
|
||||
|
||||
## When to Use Each Mode
|
||||
|
||||
### Use Simple Mode When:
|
||||
- ✅ Using standard Ingress annotations
|
||||
- ✅ No custom Lua scripts or snippets
|
||||
- ✅ Standard features: TLS, routing, rate limiting, CORS, auth
|
||||
- ✅ Need fastest migration path
|
||||
|
||||
### Use Complex Mode When:
|
||||
- ⚠️ Using `server-snippet`, `configuration-snippet`, `http-snippet`
|
||||
- ⚠️ Custom Lua logic in annotations
|
||||
- ⚠️ Advanced nginx features (variables, complex rewrites)
|
||||
- ⚠️ Need to preserve custom business logic
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### For Simple Mode:
|
||||
- kubectl with cluster access
|
||||
- helm 3.x
|
||||
|
||||
### For Complex Mode (additional):
|
||||
- Go 1.24+ (for WASM plugin development)
|
||||
- Docker (for plugin image builds)
|
||||
- Image registry access (Harbor, DockerHub, ACR, etc.)
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Analyze Your Current Setup
|
||||
```bash
|
||||
# Clone this skill
|
||||
git clone https://github.com/alibaba/higress.git
|
||||
cd higress/.claude/skills/nginx-to-higress-migration
|
||||
|
||||
# Check for snippet usage (complex mode indicator)
|
||||
kubectl get ingress -A -o yaml | grep -E "snippet" | wc -l
|
||||
|
||||
# If output is 0 → Simple mode
|
||||
# If output > 0 → Complex mode (AI will handle plugin generation)
|
||||
```
|
||||
|
||||
### 2. Local Validation (Kind)
|
||||
```bash
|
||||
# Create Kind cluster
|
||||
kind create cluster --name higress-test
|
||||
|
||||
# Install Higress
|
||||
helm install higress higress/higress \
|
||||
-n higress-system --create-namespace \
|
||||
--set global.ingressClass=nginx
|
||||
|
||||
# Apply your Ingress resources
|
||||
kubectl apply -f your-ingress.yaml
|
||||
|
||||
# Validate
|
||||
kubectl port-forward -n higress-system svc/higress-gateway 8080:80 &
|
||||
curl -H "Host: your-domain.com" http://localhost:8080/
|
||||
```
|
||||
|
||||
### 3. Production Migration
|
||||
```bash
|
||||
# Generate test script
|
||||
./scripts/generate-migration-test.sh > test.sh
|
||||
|
||||
# Get Higress IP
|
||||
HIGRESS_IP=$(kubectl get svc -n higress-system higress-gateway \
|
||||
-o jsonpath='{.status.loadBalancer.ingress[0].ip}')
|
||||
|
||||
# Run validation
|
||||
./test.sh ${HIGRESS_IP}
|
||||
|
||||
# If all tests pass → switch traffic (DNS/LB)
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always validate locally first** - Kind cluster testing catches 95%+ of issues
|
||||
2. **Keep nginx running during migration** - Enables instant rollback if needed
|
||||
3. **Use gradual traffic cutover** - 10% → 50% → 100% with monitoring
|
||||
4. **Leverage AI for plugin development** - 80% time savings vs manual coding
|
||||
5. **Document custom plugins** - AI-generated code includes inline documentation
|
||||
|
||||
## Common Questions
|
||||
|
||||
### Q: Do I need to modify my Ingress YAML?
|
||||
**A**: No. Standard Ingress resources with common annotations work directly on Higress.
|
||||
|
||||
### Q: What about nginx ConfigMap settings?
|
||||
**A**: AI agent analyzes ConfigMap and generates WASM plugins if needed to preserve functionality.
|
||||
|
||||
### Q: How do I rollback if something goes wrong?
|
||||
**A**: Since nginx continues running during migration, just switch traffic back (DNS/LB). Recommended: keep nginx for 1 week post-migration.
|
||||
|
||||
### Q: How does WASM plugin performance compare to Lua?
|
||||
**A**: WASM plugins are compiled (vs interpreted Lua), typically faster and more secure.
|
||||
|
||||
### Q: Can I customize the AI-generated plugin code?
|
||||
**A**: Yes. All generated code is standard Go with clear structure, easy to modify if needed.
|
||||
|
||||
## Related Resources
|
||||
|
||||
- [Higress Official Documentation](https://higress.io/)
|
||||
- [Nginx Ingress Controller](https://kubernetes.github.io/ingress-nginx/)
|
||||
- [WASM Plugin Development Guide](./SKILL.md)
|
||||
- [Annotation Compatibility Matrix](./references/annotation-mapping.md)
|
||||
- [Built-in Plugin Catalog](./references/builtin-plugins.md)
|
||||
|
||||
---
|
||||
|
||||
**Language**: [English](./README.md) | [中文](./README_CN.md)
|
||||
@@ -1,495 +0,0 @@
|
||||
# Nginx 到 Higress 迁移技能
|
||||
|
||||
一站式 ingress-nginx 到 Higress 网关迁移解决方案,提供智能兼容性验证、自动化迁移工具链和 AI 驱动的能力增强。
|
||||
|
||||
## 概述
|
||||
|
||||
本技能基于真实生产环境迁移经验构建,提供:
|
||||
- 🔍 **配置分析与兼容性评估**:自动扫描 nginx Ingress 配置,识别迁移风险
|
||||
- 🧪 **Kind 集群仿真**:本地快速验证配置兼容性,确保迁移安全
|
||||
- 🚀 **灰度迁移策略**:分阶段迁移方法,最小化业务风险
|
||||
- 🤖 **AI 驱动的能力增强**:自动化 WASM 插件开发,填补 Higress 功能空白
|
||||
|
||||
## 核心优势
|
||||
|
||||
### 🎯 简单模式:零配置迁移
|
||||
|
||||
**适用于使用标准注解的 Ingress 资源:**
|
||||
|
||||
✅ **100% 注解兼容性** - 所有标准 `nginx.ingress.kubernetes.io/*` 注解开箱即用
|
||||
✅ **零配置变更** - 现有 Ingress YAML 直接应用到 Higress
|
||||
✅ **即时迁移** - 无学习曲线,无手动转换,无风险
|
||||
✅ **并行部署** - Higress 与 nginx 并存,安全测试
|
||||
|
||||
**示例:**
|
||||
```yaml
|
||||
# 现有的 nginx Ingress - 在 Higress 上立即可用
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/rewrite-target: /api/$2
|
||||
nginx.ingress.kubernetes.io/rate-limit: "100"
|
||||
nginx.ingress.kubernetes.io/cors-allow-origin: "*"
|
||||
spec:
|
||||
ingressClassName: nginx # 相同的类名,两个控制器同时监听
|
||||
rules:
|
||||
- host: api.example.com
|
||||
http:
|
||||
paths:
|
||||
- path: /v1(/|$)(.*)
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: backend
|
||||
port:
|
||||
number: 8080
|
||||
```
|
||||
|
||||
**无需转换。无需手动重写。直接部署并验证。**
|
||||
|
||||
### ⚙️ 复杂模式:自定义插件的全流程 DevOps 自动化
|
||||
|
||||
**当 nginx snippet 或自定义 Lua 逻辑需要 WASM 插件时:**
|
||||
|
||||
✅ **自动化需求分析** - AI 从 nginx snippet 提取功能需求
|
||||
✅ **代码生成** - 使用 proxy-wasm SDK 自动生成类型安全的 Go 代码
|
||||
✅ **构建与验证** - 编译、测试、打包为 OCI 镜像
|
||||
✅ **生产部署** - 推送到镜像仓库并部署 WasmPlugin CRD
|
||||
|
||||
**完整工作流自动化:**
|
||||
```
|
||||
nginx snippet → AI 分析 → Go WASM 代码 → 构建 → 测试 → 部署 → 验证
|
||||
↓ ↓ ↓ ↓ ↓ ↓ ↓
|
||||
分钟级 秒级 秒级 1分钟 1分钟 即时 即时
|
||||
```
|
||||
|
||||
**示例:基于 IP 的自定义路由 + HMAC 签名验证**
|
||||
|
||||
**原始 nginx snippet:**
|
||||
```nginx
|
||||
location /payment {
|
||||
access_by_lua_block {
|
||||
local client_ip = ngx.var.remote_addr
|
||||
local signature = ngx.req.get_headers()["X-Signature"]
|
||||
-- 复杂的 IP 路由和 HMAC 验证逻辑
|
||||
if not validate_signature(signature) then
|
||||
ngx.exit(403)
|
||||
end
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**AI 生成的 WASM 插件**(自动完成):
|
||||
1. 分析需求:IP 路由 + HMAC-SHA256 验证
|
||||
2. 生成带有适当错误处理的 Go 代码
|
||||
3. 构建、测试、部署 - **完全自动化**
|
||||
|
||||
**结果**:保留原始功能,业务逻辑不变,无需手动编码。
|
||||
|
||||
## 迁移工作流
|
||||
|
||||
### 模式 1:简单迁移(标准 Ingress)
|
||||
|
||||
**前提条件**:Ingress 使用标准注解(使用 `kubectl get ingress -A -o yaml` 检查)
|
||||
|
||||
**步骤:**
|
||||
```bash
|
||||
# 1. 在 nginx 旁边安装 Higress(相同的 ingressClass)
|
||||
helm install higress higress/higress \
|
||||
-n higress-system --create-namespace \
|
||||
--set global.ingressClass=nginx \
|
||||
--set global.enableStatus=false
|
||||
|
||||
# 2. 生成验证测试
|
||||
./scripts/generate-migration-test.sh > test.sh
|
||||
|
||||
# 3. 对 Higress 网关运行测试
|
||||
./test.sh ${HIGRESS_IP}
|
||||
|
||||
# 4. 如果所有测试通过 → 切换流量(DNS/LB)
|
||||
# nginx 继续运行作为备份
|
||||
```
|
||||
|
||||
**时间线**:50+ 个 Ingress 资源 30 分钟(包括验证)
|
||||
|
||||
### 模式 2:复杂迁移(自定义 Snippet/Lua)
|
||||
|
||||
**前提条件**:Ingress 使用 `server-snippet`、`configuration-snippet` 或 Lua 逻辑
|
||||
|
||||
**步骤:**
|
||||
```bash
|
||||
# 1. 分析不兼容的特性
|
||||
./scripts/analyze-ingress.sh
|
||||
|
||||
# 2. 对于每个 snippet:
|
||||
# - AI 读取 snippet
|
||||
# - 设计 WASM 插件架构
|
||||
# - 生成类型安全的 Go 代码
|
||||
# - 构建和验证
|
||||
|
||||
# 3. 部署插件
|
||||
kubectl apply -f generated-wasm-plugins/
|
||||
|
||||
# 4. 验证 + 切换流量
|
||||
```
|
||||
|
||||
**时间线**:1-2 小时,包括 AI 驱动的插件开发
|
||||
|
||||
## AI 执行示例
|
||||
|
||||
**用户**:"帮我将 nginx Ingress 迁移到 Higress"
|
||||
|
||||
**AI Agent 工作流**:
|
||||
|
||||
1. **发现**
|
||||
```bash
|
||||
kubectl get ingress -A -o yaml > backup.yaml
|
||||
kubectl get configmap -n ingress-nginx ingress-nginx-controller -o yaml
|
||||
```
|
||||
|
||||
2. **兼容性分析**
|
||||
- ✅ 标准注解:直接迁移
|
||||
- ⚠️ Snippet 注解:需要 WASM 插件
|
||||
- 识别模式:限流、认证、路由逻辑
|
||||
|
||||
3. **并行部署**
|
||||
```bash
|
||||
helm install higress higress/higress -n higress-system \
|
||||
--set global.ingressClass=nginx \
|
||||
--set global.enableStatus=false
|
||||
```
|
||||
|
||||
4. **自动化测试**
|
||||
```bash
|
||||
./scripts/generate-migration-test.sh > test.sh
|
||||
./test.sh ${HIGRESS_IP}
|
||||
# ✅ 60/60 路由通过
|
||||
```
|
||||
|
||||
5. **插件开发**(如需要)
|
||||
- 读取 `higress-wasm-go-plugin` 技能
|
||||
- 为自定义逻辑生成 Go 代码
|
||||
- 构建、验证、部署
|
||||
- 重新测试受影响的路由
|
||||
|
||||
6. **逐步切换**
|
||||
- 阶段 1:10% 流量 → 验证
|
||||
- 阶段 2:50% 流量 → 监控
|
||||
- 阶段 3:100% 流量 → 下线 nginx
|
||||
|
||||
## 生产案例研究
|
||||
|
||||
### 案例 1:电商 API 网关(60+ Ingress 资源)
|
||||
|
||||
**环境**:
|
||||
- 60+ Ingress 资源
|
||||
- 3 节点高可用集群
|
||||
- 15+ 域名的 TLS 终止
|
||||
- 限流、CORS、JWT 认证
|
||||
|
||||
**迁移:**
|
||||
```yaml
|
||||
# Ingress 示例(60+ 个中的一个)
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: product-api
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/rewrite-target: /$2
|
||||
nginx.ingress.kubernetes.io/rate-limit: "1000"
|
||||
nginx.ingress.kubernetes.io/cors-allow-origin: "https://shop.example.com"
|
||||
nginx.ingress.kubernetes.io/auth-url: "http://auth-service/validate"
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
tls:
|
||||
- hosts:
|
||||
- api.example.com
|
||||
secretName: api-tls
|
||||
rules:
|
||||
- host: api.example.com
|
||||
http:
|
||||
paths:
|
||||
- path: /api(/|$)(.*)
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: product-service
|
||||
port:
|
||||
number: 8080
|
||||
```
|
||||
|
||||
**在 Kind 集群中验证**:
|
||||
```bash
|
||||
# 直接应用,无需修改
|
||||
kubectl apply -f product-api-ingress.yaml
|
||||
|
||||
# 测试所有功能
|
||||
curl https://api.example.com/api/products/123
|
||||
# ✅ URL 重写:/products/123(正确)
|
||||
# ✅ 限流:激活
|
||||
# ✅ CORS 头部:已注入
|
||||
# ✅ 认证验证:工作中
|
||||
# ✅ TLS 证书:有效
|
||||
```
|
||||
|
||||
**结果**:
|
||||
| 指标 | 值 | 备注 |
|
||||
|------|-----|------|
|
||||
| 迁移的 Ingress 资源 | 60+ | 零修改 |
|
||||
| 支持的注解类型 | 20+ | 100% 兼容性 |
|
||||
| TLS 证书 | 15+ | 直接复用 Secret |
|
||||
| 配置变更 | **0** | 无需编辑 YAML |
|
||||
| 迁移时间 | **30 分钟** | 包括验证 |
|
||||
| 停机时间 | **0 秒** | 零停机切换 |
|
||||
| 需要回滚 | **0** | 所有测试通过 |
|
||||
|
||||
### 案例 2:金融服务自定义认证逻辑
|
||||
|
||||
**挑战**:支付服务需要自定义的基于 IP 的路由 + HMAC-SHA256 请求签名验证(实现为 nginx Lua snippet)
|
||||
|
||||
**原始 nginx 配置**:
|
||||
```nginx
|
||||
location /payment/process {
|
||||
access_by_lua_block {
|
||||
local client_ip = ngx.var.remote_addr
|
||||
local signature = ngx.req.get_headers()["X-Payment-Signature"]
|
||||
local timestamp = ngx.req.get_headers()["X-Timestamp"]
|
||||
|
||||
-- IP 白名单检查
|
||||
if not is_allowed_ip(client_ip) then
|
||||
ngx.log(ngx.ERR, "Blocked IP: " .. client_ip)
|
||||
ngx.exit(403)
|
||||
end
|
||||
|
||||
-- HMAC-SHA256 签名验证
|
||||
local payload = ngx.var.request_uri .. timestamp
|
||||
local expected_sig = compute_hmac_sha256(payload, secret_key)
|
||||
|
||||
if signature ~= expected_sig then
|
||||
ngx.log(ngx.ERR, "Invalid signature from: " .. client_ip)
|
||||
ngx.exit(403)
|
||||
end
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**AI 驱动的插件开发**:
|
||||
|
||||
1. **需求分析**(AI 读取 snippet)
|
||||
- IP 白名单验证
|
||||
- HMAC-SHA256 签名验证
|
||||
- 请求时间戳验证
|
||||
- 错误日志需求
|
||||
|
||||
2. **自动生成的 WASM 插件**(Go)
|
||||
```go
|
||||
// 由 AI agent 自动生成
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
|
||||
)
|
||||
|
||||
type PaymentAuthPlugin struct {
|
||||
proxywasm.DefaultPluginContext
|
||||
}
|
||||
|
||||
func (ctx *PaymentAuthPlugin) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action {
|
||||
// IP 白名单检查
|
||||
clientIP, _ := proxywasm.GetProperty([]string{"source", "address"})
|
||||
if !isAllowedIP(string(clientIP)) {
|
||||
proxywasm.LogError("Blocked IP: " + string(clientIP))
|
||||
proxywasm.SendHttpResponse(403, nil, []byte("Forbidden"), -1)
|
||||
return types.ActionPause
|
||||
}
|
||||
|
||||
// HMAC 签名验证
|
||||
signature, _ := proxywasm.GetHttpRequestHeader("X-Payment-Signature")
|
||||
timestamp, _ := proxywasm.GetHttpRequestHeader("X-Timestamp")
|
||||
uri, _ := proxywasm.GetProperty([]string{"request", "path"})
|
||||
|
||||
payload := string(uri) + timestamp
|
||||
expectedSig := computeHMAC(payload, secretKey)
|
||||
|
||||
if signature != expectedSig {
|
||||
proxywasm.LogError("Invalid signature from: " + string(clientIP))
|
||||
proxywasm.SendHttpResponse(403, nil, []byte("Invalid signature"), -1)
|
||||
return types.ActionPause
|
||||
}
|
||||
|
||||
return types.ActionContinue
|
||||
}
|
||||
```
|
||||
|
||||
3. **自动化构建与部署**
|
||||
```bash
|
||||
# AI agent 自动执行:
|
||||
go mod tidy
|
||||
GOOS=wasip1 GOARCH=wasm go build -o payment-auth.wasm
|
||||
docker build -t registry.example.com/payment-auth:v1 .
|
||||
docker push registry.example.com/payment-auth:v1
|
||||
|
||||
kubectl apply -f - <<EOF
|
||||
apiVersion: extensions.higress.io/v1alpha1
|
||||
kind: WasmPlugin
|
||||
metadata:
|
||||
name: payment-auth
|
||||
namespace: higress-system
|
||||
spec:
|
||||
url: oci://registry.example.com/payment-auth:v1
|
||||
phase: AUTHN
|
||||
priority: 100
|
||||
EOF
|
||||
```
|
||||
|
||||
**结果**:
|
||||
- ✅ 保留原始功能(IP 检查 + HMAC 验证)
|
||||
- ✅ 提升安全性(类型安全代码,编译的 WASM)
|
||||
- ✅ 更好的性能(原生 WASM vs 解释执行的 Lua)
|
||||
- ✅ 完全自动化(需求 → 部署 < 10 分钟)
|
||||
- ✅ 无需业务逻辑变更
|
||||
|
||||
### 案例 3:多租户 SaaS 平台(自定义路由)
|
||||
|
||||
**挑战**:根据 JWT 令牌中的租户 ID 将请求路由到不同的后端集群
|
||||
|
||||
**AI 解决方案**:
|
||||
- 从 JWT 声明中提取租户 ID
|
||||
- 生成用于动态上游选择的 WASM 插件
|
||||
- 零手动编码部署
|
||||
|
||||
**时间线**:15 分钟(分析 → 代码 → 部署 → 验证)
|
||||
|
||||
## 关键统计数据
|
||||
|
||||
### 迁移效率
|
||||
|
||||
| 指标 | 简单模式 | 复杂模式 |
|
||||
|------|----------|----------|
|
||||
| 配置兼容性 | 100% | 95%+ |
|
||||
| 需要手动代码变更 | 0 | 0(AI 生成)|
|
||||
| 平均迁移时间 | 30 分钟 | 1-2 小时 |
|
||||
| 需要停机时间 | 0 | 0 |
|
||||
| 回滚复杂度 | 简单 | 简单 |
|
||||
|
||||
### 生产验证
|
||||
|
||||
- **总计迁移的 Ingress 资源**:200+
|
||||
- **环境**:金融服务、电子商务、SaaS 平台
|
||||
- **成功率**:100%(所有生产部署成功)
|
||||
- **平均配置兼容性**:98%
|
||||
- **节省的插件开发时间**:80%(AI 驱动的自动化)
|
||||
|
||||
## 何时使用每种模式
|
||||
|
||||
### 使用简单模式当:
|
||||
- ✅ 使用标准 Ingress 注解
|
||||
- ✅ 没有自定义 Lua 脚本或 snippet
|
||||
- ✅ 标准功能:TLS、路由、限流、CORS、认证
|
||||
- ✅ 需要最快的迁移路径
|
||||
|
||||
### 使用复杂模式当:
|
||||
- ⚠️ 使用 `server-snippet`、`configuration-snippet`、`http-snippet`
|
||||
- ⚠️ 注解中有自定义 Lua 逻辑
|
||||
- ⚠️ 高级 nginx 功能(变量、复杂重写)
|
||||
- ⚠️ 需要保留自定义业务逻辑
|
||||
|
||||
## 前提条件
|
||||
|
||||
### 简单模式:
|
||||
- 具有集群访问权限的 kubectl
|
||||
- helm 3.x
|
||||
|
||||
### 复杂模式(额外需要):
|
||||
- Go 1.24+(用于 WASM 插件开发)
|
||||
- Docker(用于插件镜像构建)
|
||||
- 镜像仓库访问权限(Harbor、DockerHub、ACR 等)
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 分析当前设置
|
||||
```bash
|
||||
# 克隆此技能
|
||||
git clone https://github.com/alibaba/higress.git
|
||||
cd higress/.claude/skills/nginx-to-higress-migration
|
||||
|
||||
# 检查 snippet 使用情况(复杂模式指标)
|
||||
kubectl get ingress -A -o yaml | grep -E "snippet" | wc -l
|
||||
|
||||
# 如果输出为 0 → 简单模式
|
||||
# 如果输出 > 0 → 复杂模式(AI 将处理插件生成)
|
||||
```
|
||||
|
||||
### 2. 本地验证(Kind)
|
||||
```bash
|
||||
# 创建 Kind 集群
|
||||
kind create cluster --name higress-test
|
||||
|
||||
# 安装 Higress
|
||||
helm install higress higress/higress \
|
||||
-n higress-system --create-namespace \
|
||||
--set global.ingressClass=nginx
|
||||
|
||||
# 应用 Ingress 资源
|
||||
kubectl apply -f your-ingress.yaml
|
||||
|
||||
# 验证
|
||||
kubectl port-forward -n higress-system svc/higress-gateway 8080:80 &
|
||||
curl -H "Host: your-domain.com" http://localhost:8080/
|
||||
```
|
||||
|
||||
### 3. 生产迁移
|
||||
```bash
|
||||
# 生成测试脚本
|
||||
./scripts/generate-migration-test.sh > test.sh
|
||||
|
||||
# 获取 Higress IP
|
||||
HIGRESS_IP=$(kubectl get svc -n higress-system higress-gateway \
|
||||
-o jsonpath='{.status.loadBalancer.ingress[0].ip}')
|
||||
|
||||
# 运行验证
|
||||
./test.sh ${HIGRESS_IP}
|
||||
|
||||
# 如果所有测试通过 → 切换流量(DNS/LB)
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **始终先在本地验证** - Kind 集群测试可发现 95%+ 的问题
|
||||
2. **迁移期间保持 nginx 运行** - 如需要可即时回滚
|
||||
3. **使用逐步流量切换** - 10% → 50% → 100% 并监控
|
||||
4. **利用 AI 进行插件开发** - 比手动编码节省 80% 时间
|
||||
5. **记录自定义插件** - AI 生成的代码包含内联文档
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q:我需要修改 Ingress YAML 吗?
|
||||
**A**:不需要。使用常见注解的标准 Ingress 资源可直接在 Higress 上运行。
|
||||
|
||||
### Q:nginx ConfigMap 设置怎么办?
|
||||
**A**:AI agent 会分析 ConfigMap,如需保留功能会生成 WASM 插件。
|
||||
|
||||
### Q:如果出现问题如何回滚?
|
||||
**A**:由于 nginx 在迁移期间继续运行,只需切换回流量(DNS/LB)。建议:迁移后保留 nginx 1 周。
|
||||
|
||||
### Q:WASM 插件性能与 Lua 相比如何?
|
||||
**A**:WASM 插件是编译的(vs 解释执行的 Lua),通常更快且更安全。
|
||||
|
||||
### Q:我可以自定义 AI 生成的插件代码吗?
|
||||
**A**:可以。所有生成的代码都是结构清晰的标准 Go 代码,如需要易于修改。
|
||||
|
||||
## 相关资源
|
||||
|
||||
- [Higress 官方文档](https://higress.io/)
|
||||
- [Nginx Ingress Controller](https://kubernetes.github.io/ingress-nginx/)
|
||||
- [WASM 插件开发指南](./SKILL.md)
|
||||
- [注解兼容性矩阵](./references/annotation-mapping.md)
|
||||
- [内置插件目录](./references/builtin-plugins.md)
|
||||
|
||||
---
|
||||
|
||||
**语言**:[English](./README.md) | [中文](./README_CN.md)
|
||||
@@ -1,477 +0,0 @@
|
||||
---
|
||||
name: nginx-to-higress-migration
|
||||
description: "Migrate from ingress-nginx to Higress in Kubernetes environments. Use when (1) analyzing existing ingress-nginx setup (2) reading nginx Ingress resources and ConfigMaps (3) installing Higress via helm with proper ingressClass (4) identifying unsupported nginx annotations (5) generating WASM plugins for nginx snippets/advanced features (6) building and deploying custom plugins to image registry. Supports full migration workflow with compatibility analysis and plugin generation."
|
||||
---
|
||||
|
||||
# Nginx to Higress Migration
|
||||
|
||||
Automate migration from ingress-nginx to Higress in Kubernetes environments.
|
||||
|
||||
## ⚠️ Critical Limitation: Snippet Annotations NOT Supported
|
||||
|
||||
> **Before you begin:** Higress does **NOT** support the following nginx annotations:
|
||||
> - `nginx.ingress.kubernetes.io/server-snippet`
|
||||
> - `nginx.ingress.kubernetes.io/configuration-snippet`
|
||||
> - `nginx.ingress.kubernetes.io/http-snippet`
|
||||
>
|
||||
> These annotations will be **silently ignored**, causing functionality loss!
|
||||
>
|
||||
> **Pre-migration check (REQUIRED):**
|
||||
> ```bash
|
||||
> kubectl get ingress -A -o yaml | grep -E "snippet" | wc -l
|
||||
> ```
|
||||
> If count > 0, you MUST plan WASM plugin replacements before migration.
|
||||
> See [Phase 6](#phase-6-use-built-in-plugins-or-create-custom-wasm-plugin-if-needed) for alternatives.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- kubectl configured with cluster access
|
||||
- helm 3.x installed
|
||||
- Go 1.24+ (for WASM plugin compilation)
|
||||
- Docker (for plugin image push)
|
||||
|
||||
## Pre-Migration Checklist
|
||||
|
||||
### Before Starting
|
||||
|
||||
- [ ] Backup all Ingress resources
|
||||
```bash
|
||||
kubectl get ingress -A -o yaml > ingress-backup.yaml
|
||||
```
|
||||
- [ ] Identify snippet usage (see warning above)
|
||||
- [ ] List all nginx annotations in use
|
||||
```bash
|
||||
kubectl get ingress -A -o yaml | grep "nginx.ingress.kubernetes.io" | sort | uniq -c
|
||||
```
|
||||
- [ ] Verify Higress compatibility for each annotation (see [annotation-mapping.md](references/annotation-mapping.md))
|
||||
- [ ] Plan WASM plugins for unsupported features
|
||||
- [ ] Prepare test environment (Kind/Minikube for testing recommended)
|
||||
|
||||
### During Migration
|
||||
|
||||
- [ ] Install Higress in parallel with nginx
|
||||
- [ ] Verify all pods running in higress-system namespace
|
||||
- [ ] Run test script against Higress gateway
|
||||
- [ ] Compare responses between nginx and Higress
|
||||
- [ ] Deploy any required WASM plugins
|
||||
- [ ] Configure monitoring/alerting
|
||||
|
||||
### After Migration
|
||||
|
||||
- [ ] All routes verified working
|
||||
- [ ] Custom functionality (snippet replacements) tested
|
||||
- [ ] Monitoring dashboards configured
|
||||
- [ ] Team trained on Higress operations
|
||||
- [ ] Documentation updated
|
||||
- [ ] Rollback procedure tested
|
||||
|
||||
## Migration Workflow
|
||||
|
||||
### Phase 1: Discovery
|
||||
|
||||
```bash
|
||||
# Check for ingress-nginx installation
|
||||
kubectl get pods -A | grep ingress-nginx
|
||||
kubectl get ingressclass
|
||||
|
||||
# List all Ingress resources using nginx class
|
||||
kubectl get ingress -A -o json | jq '.items[] | select(.spec.ingressClassName=="nginx" or .metadata.annotations["kubernetes.io/ingress.class"]=="nginx")'
|
||||
|
||||
# Get nginx ConfigMap
|
||||
kubectl get configmap -n ingress-nginx ingress-nginx-controller -o yaml
|
||||
```
|
||||
|
||||
### Phase 2: Compatibility Analysis
|
||||
|
||||
Run the analysis script to identify unsupported features:
|
||||
|
||||
```bash
|
||||
./scripts/analyze-ingress.sh [namespace]
|
||||
```
|
||||
|
||||
**Key point: No Ingress modification needed!**
|
||||
|
||||
Higress natively supports `nginx.ingress.kubernetes.io/*` annotations - your existing Ingress resources work as-is.
|
||||
|
||||
See [references/annotation-mapping.md](references/annotation-mapping.md) for the complete list of supported annotations.
|
||||
|
||||
**Unsupported annotations** (require built-in plugin or custom WASM plugin):
|
||||
- `nginx.ingress.kubernetes.io/server-snippet`
|
||||
- `nginx.ingress.kubernetes.io/configuration-snippet`
|
||||
- `nginx.ingress.kubernetes.io/lua-resty-waf*`
|
||||
- Complex Lua logic in snippets
|
||||
|
||||
For these, check [references/builtin-plugins.md](references/builtin-plugins.md) first - Higress may already have a plugin!
|
||||
|
||||
### Phase 3: Higress Installation (Parallel with nginx)
|
||||
|
||||
Higress natively supports `nginx.ingress.kubernetes.io/*` annotations. Install Higress **alongside** nginx for safe parallel testing.
|
||||
|
||||
```bash
|
||||
# 1. Get current nginx ingressClass name
|
||||
INGRESS_CLASS=$(kubectl get ingressclass -o jsonpath='{.items[?(@.spec.controller=="k8s.io/ingress-nginx")].metadata.name}')
|
||||
echo "Current nginx ingressClass: $INGRESS_CLASS"
|
||||
|
||||
# 2. Detect timezone and select nearest registry
|
||||
# China/Asia: higress-registry.cn-hangzhou.cr.aliyuncs.com (default)
|
||||
# North America: higress-registry.us-west-1.cr.aliyuncs.com
|
||||
# Southeast Asia: higress-registry.ap-southeast-7.cr.aliyuncs.com
|
||||
TZ_OFFSET=$(date +%z)
|
||||
case "$TZ_OFFSET" in
|
||||
-1*|-0*) REGISTRY="higress-registry.us-west-1.cr.aliyuncs.com" ;; # Americas
|
||||
+07*|+08*|+09*) REGISTRY="higress-registry.cn-hangzhou.cr.aliyuncs.com" ;; # Asia
|
||||
+05*|+06*) REGISTRY="higress-registry.ap-southeast-7.cr.aliyuncs.com" ;; # Southeast Asia
|
||||
*) REGISTRY="higress-registry.cn-hangzhou.cr.aliyuncs.com" ;; # Default
|
||||
esac
|
||||
echo "Using registry: $REGISTRY"
|
||||
|
||||
# 3. Add Higress repo
|
||||
helm repo add higress https://higress.io/helm-charts
|
||||
helm repo update
|
||||
|
||||
# 4. Install Higress with parallel-safe settings
|
||||
# Note: Override ALL component hubs to use the selected registry
|
||||
helm install higress higress/higress \
|
||||
-n higress-system --create-namespace \
|
||||
--set global.ingressClass=${INGRESS_CLASS:-nginx} \
|
||||
--set global.hub=${REGISTRY}/higress \
|
||||
--set global.enableStatus=false \
|
||||
--set higress-core.controller.hub=${REGISTRY}/higress \
|
||||
--set higress-core.gateway.hub=${REGISTRY}/higress \
|
||||
--set higress-core.pilot.hub=${REGISTRY}/higress \
|
||||
--set higress-core.pluginServer.hub=${REGISTRY}/higress \
|
||||
--set higress-core.gateway.replicas=2
|
||||
```
|
||||
|
||||
Key helm values:
|
||||
- `global.ingressClass`: Use the **same** class as ingress-nginx
|
||||
- `global.hub`: Image registry (auto-selected by timezone)
|
||||
- `global.enableStatus=false`: **Disable Ingress status updates** to avoid conflicts with nginx (reduces API server pressure)
|
||||
- Override all component hubs to ensure consistent registry usage
|
||||
- Both nginx and Higress will watch the same Ingress resources
|
||||
- Higress automatically recognizes `nginx.ingress.kubernetes.io/*` annotations
|
||||
- Traffic still flows through nginx until you switch the entry point
|
||||
|
||||
⚠️ **Note**: After nginx is uninstalled, you can enable status updates:
|
||||
```bash
|
||||
helm upgrade higress higress/higress -n higress-system \
|
||||
--reuse-values \
|
||||
--set global.enableStatus=true
|
||||
```
|
||||
|
||||
#### Kind/Local Environment Setup
|
||||
|
||||
In Kind or local Kubernetes clusters, the LoadBalancer service will stay in `PENDING` state. Use one of these methods:
|
||||
|
||||
**Option 1: Port Forward (Recommended for testing)**
|
||||
```bash
|
||||
# Forward Higress gateway to local port
|
||||
kubectl port-forward -n higress-system svc/higress-gateway 8080:80 8443:443 &
|
||||
|
||||
# Test with Host header
|
||||
curl -H "Host: example.com" http://localhost:8080/
|
||||
```
|
||||
|
||||
**Option 2: NodePort**
|
||||
```bash
|
||||
# Patch service to NodePort
|
||||
kubectl patch svc -n higress-system higress-gateway \
|
||||
-p '{"spec":{"type":"NodePort"}}'
|
||||
|
||||
# Get assigned port
|
||||
NODE_PORT=$(kubectl get svc -n higress-system higress-gateway \
|
||||
-o jsonpath='{.spec.ports[?(@.port==80)].nodePort}')
|
||||
|
||||
# Test (use docker container IP for Kind)
|
||||
curl -H "Host: example.com" http://localhost:${NODE_PORT}/
|
||||
```
|
||||
|
||||
**Option 3: Kind with Port Mapping (Requires cluster recreation)**
|
||||
```yaml
|
||||
# kind-config.yaml
|
||||
kind: Cluster
|
||||
apiVersion: kind.x-k8s.io/v1alpha4
|
||||
nodes:
|
||||
- role: control-plane
|
||||
extraPortMappings:
|
||||
- containerPort: 30080
|
||||
hostPort: 80
|
||||
- containerPort: 30443
|
||||
hostPort: 443
|
||||
```
|
||||
|
||||
### Phase 4: Generate and Run Test Script
|
||||
|
||||
After Higress is running, generate a test script covering all Ingress routes:
|
||||
|
||||
```bash
|
||||
# Generate test script
|
||||
./scripts/generate-migration-test.sh > migration-test.sh
|
||||
chmod +x migration-test.sh
|
||||
|
||||
# Get Higress gateway address
|
||||
# Option A: If LoadBalancer is supported
|
||||
HIGRESS_IP=$(kubectl get svc -n higress-system higress-gateway -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
|
||||
|
||||
# Option B: If LoadBalancer is NOT supported, use port-forward
|
||||
kubectl port-forward -n higress-system svc/higress-gateway 8080:80 &
|
||||
HIGRESS_IP="127.0.0.1:8080"
|
||||
|
||||
# Run tests
|
||||
./migration-test.sh ${HIGRESS_IP}
|
||||
```
|
||||
|
||||
The test script will:
|
||||
- Extract all hosts and paths from Ingress resources
|
||||
- Test each route against Higress gateway
|
||||
- Verify response codes and basic functionality
|
||||
- Report any failures for investigation
|
||||
|
||||
### Phase 5: Traffic Cutover (User Action Required)
|
||||
|
||||
⚠️ **Only proceed after all tests pass!**
|
||||
|
||||
Choose your cutover method based on infrastructure:
|
||||
|
||||
**Option A: DNS Switch**
|
||||
```bash
|
||||
# Update DNS records to point to Higress gateway IP
|
||||
# Example: example.com A record -> ${HIGRESS_IP}
|
||||
```
|
||||
|
||||
**Option B: Layer 4 Proxy/Load Balancer Switch**
|
||||
```bash
|
||||
# Update upstream in your L4 proxy (e.g., F5, HAProxy, cloud LB)
|
||||
# From: nginx-ingress-controller service IP
|
||||
# To: higress-gateway service IP
|
||||
```
|
||||
|
||||
**Option C: Kubernetes Service Switch** (if using external traffic via Service)
|
||||
```bash
|
||||
# Update your external-facing Service selector or endpoints
|
||||
```
|
||||
|
||||
### Phase 6: Use Built-in Plugins or Create Custom WASM Plugin (If Needed)
|
||||
|
||||
Before writing custom plugins, check if Higress has a built-in plugin that meets your needs!
|
||||
|
||||
#### Built-in Plugins (Recommended First)
|
||||
|
||||
Higress provides many built-in plugins. Check [references/builtin-plugins.md](references/builtin-plugins.md) for the full list.
|
||||
|
||||
Common replacements for nginx features:
|
||||
| nginx feature | Higress built-in plugin |
|
||||
|---------------|------------------------|
|
||||
| Basic Auth snippet | `basic-auth` |
|
||||
| IP restriction | `ip-restriction` |
|
||||
| Rate limiting | `key-rate-limit`, `cluster-key-rate-limit` |
|
||||
| WAF/ModSecurity | `waf` |
|
||||
| Request validation | `request-validation` |
|
||||
| Bot detection | `bot-detect` |
|
||||
| JWT auth | `jwt-auth` |
|
||||
| CORS headers | `cors` |
|
||||
| Custom response | `custom-response` |
|
||||
| Request/Response transform | `transformer` |
|
||||
|
||||
#### Common Snippet Replacements
|
||||
|
||||
| nginx snippet pattern | Higress solution |
|
||||
|----------------------|------------------|
|
||||
| Custom health endpoint (`location /health`) | WASM plugin: custom-location |
|
||||
| Add response headers | WASM plugin: custom-response-headers |
|
||||
| Request validation/blocking | WASM plugin with `OnHttpRequestHeaders` |
|
||||
| Lua rate limiting | `key-rate-limit` plugin |
|
||||
|
||||
#### Custom WASM Plugin (If No Built-in Matches)
|
||||
|
||||
When nginx snippets or Lua logic has no built-in equivalent:
|
||||
|
||||
1. **Analyze snippet** - Extract nginx directives/Lua code
|
||||
2. **Generate Go WASM code** - Use higress-wasm-go-plugin skill
|
||||
3. **Build plugin**:
|
||||
```bash
|
||||
cd plugin-dir
|
||||
go mod tidy
|
||||
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o main.wasm ./
|
||||
```
|
||||
|
||||
4. **Push to registry**:
|
||||
|
||||
If you don't have an image registry, install Harbor:
|
||||
```bash
|
||||
./scripts/install-harbor.sh
|
||||
# Follow the prompts to install Harbor in your cluster
|
||||
```
|
||||
|
||||
If you have your own registry:
|
||||
```bash
|
||||
# Build OCI image
|
||||
docker build -t <registry>/higress-plugin-<name>:v1 .
|
||||
docker push <registry>/higress-plugin-<name>:v1
|
||||
```
|
||||
|
||||
5. **Deploy plugin**:
|
||||
```yaml
|
||||
apiVersion: extensions.higress.io/v1alpha1
|
||||
kind: WasmPlugin
|
||||
metadata:
|
||||
name: custom-plugin
|
||||
namespace: higress-system
|
||||
spec:
|
||||
url: oci://<registry>/higress-plugin-<name>:v1
|
||||
phase: UNSPECIFIED_PHASE
|
||||
priority: 100
|
||||
```
|
||||
|
||||
See [references/plugin-deployment.md](references/plugin-deployment.md) for detailed plugin deployment.
|
||||
|
||||
## Common Snippet Conversions
|
||||
|
||||
### Header Manipulation
|
||||
```nginx
|
||||
# nginx snippet
|
||||
more_set_headers "X-Custom: value";
|
||||
```
|
||||
→ Use `headerControl` annotation or generate plugin with `proxywasm.AddHttpResponseHeader()`.
|
||||
|
||||
### Request Validation
|
||||
```nginx
|
||||
# nginx snippet
|
||||
if ($request_uri ~* "pattern") { return 403; }
|
||||
```
|
||||
→ Generate WASM plugin with request header/path check.
|
||||
|
||||
### Rate Limiting with Custom Logic
|
||||
```nginx
|
||||
# nginx snippet with Lua
|
||||
access_by_lua_block { ... }
|
||||
```
|
||||
→ Generate WASM plugin implementing the logic.
|
||||
|
||||
See [references/snippet-patterns.md](references/snippet-patterns.md) for common patterns.
|
||||
|
||||
## Validation
|
||||
|
||||
Before traffic switch, use the generated test script:
|
||||
|
||||
```bash
|
||||
# Generate test script
|
||||
./scripts/generate-migration-test.sh > migration-test.sh
|
||||
chmod +x migration-test.sh
|
||||
|
||||
# Get Higress gateway IP
|
||||
HIGRESS_IP=$(kubectl get svc -n higress-system higress-gateway -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
|
||||
|
||||
# Run all tests
|
||||
./migration-test.sh ${HIGRESS_IP}
|
||||
```
|
||||
|
||||
The test script will:
|
||||
- Test every host/path combination from all Ingress resources
|
||||
- Report pass/fail for each route
|
||||
- Provide a summary and next steps
|
||||
|
||||
**Only proceed with traffic cutover after all tests pass!**
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### Q1: Ingress created but routes return 404
|
||||
**Symptoms:** Ingress shows Ready, but curl returns 404
|
||||
|
||||
**Check:**
|
||||
1. Verify IngressClass matches Higress config
|
||||
```bash
|
||||
kubectl get ingress <name> -o yaml | grep ingressClassName
|
||||
```
|
||||
2. Check controller logs
|
||||
```bash
|
||||
kubectl logs -n higress-system -l app=higress-controller --tail=100
|
||||
```
|
||||
3. Verify backend service is reachable
|
||||
```bash
|
||||
kubectl run test --rm -it --image=curlimages/curl -- \
|
||||
curl http://<service>.<namespace>.svc
|
||||
```
|
||||
|
||||
#### Q2: rewrite-target not working
|
||||
**Symptoms:** Path not being rewritten, backend receives original path
|
||||
|
||||
**Solution:** Ensure `use-regex: "true"` is also set:
|
||||
```yaml
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/rewrite-target: /$2
|
||||
nginx.ingress.kubernetes.io/use-regex: "true"
|
||||
```
|
||||
|
||||
#### Q3: Snippet annotations silently ignored
|
||||
**Symptoms:** nginx snippet features not working after migration
|
||||
|
||||
**Cause:** Higress does not support snippet annotations (by design, for security)
|
||||
|
||||
**Solution:**
|
||||
- Check [references/builtin-plugins.md](references/builtin-plugins.md) for built-in alternatives
|
||||
- Create custom WASM plugin (see Phase 6)
|
||||
|
||||
#### Q4: TLS certificate issues
|
||||
**Symptoms:** HTTPS not working or certificate errors
|
||||
|
||||
**Check:**
|
||||
1. Verify Secret exists and is type `kubernetes.io/tls`
|
||||
```bash
|
||||
kubectl get secret <secret-name> -o yaml
|
||||
```
|
||||
2. Check TLS configuration in Ingress
|
||||
```bash
|
||||
kubectl get ingress <name> -o jsonpath='{.spec.tls}'
|
||||
```
|
||||
|
||||
### Useful Debug Commands
|
||||
|
||||
```bash
|
||||
# View Higress controller logs
|
||||
kubectl logs -n higress-system -l app=higress-controller -c higress-core
|
||||
|
||||
# View gateway access logs
|
||||
kubectl logs -n higress-system -l app=higress-gateway | grep "GET\|POST"
|
||||
|
||||
# Check Envoy config dump
|
||||
kubectl exec -n higress-system deploy/higress-gateway -c istio-proxy -- \
|
||||
curl -s localhost:15000/config_dump | jq '.configs[2].dynamic_listeners'
|
||||
|
||||
# View gateway stats
|
||||
kubectl exec -n higress-system deploy/higress-gateway -c istio-proxy -- \
|
||||
curl -s localhost:15000/stats | grep http
|
||||
```
|
||||
|
||||
## Rollback
|
||||
|
||||
Since nginx keeps running during migration, rollback is simply switching traffic back:
|
||||
|
||||
```bash
|
||||
# If traffic was switched via DNS:
|
||||
# - Revert DNS records to nginx gateway IP
|
||||
|
||||
# If traffic was switched via L4 proxy:
|
||||
# - Revert upstream to nginx service IP
|
||||
|
||||
# Nginx is still running, no action needed on k8s side
|
||||
```
|
||||
|
||||
## Post-Migration Cleanup
|
||||
|
||||
**Only after traffic has been fully migrated and stable:**
|
||||
|
||||
```bash
|
||||
# 1. Monitor Higress for a period (recommended: 24-48h)
|
||||
|
||||
# 2. Backup nginx resources
|
||||
kubectl get all -n ingress-nginx -o yaml > ingress-nginx-backup.yaml
|
||||
|
||||
# 3. Scale down nginx (keep for emergency rollback)
|
||||
kubectl scale deployment -n ingress-nginx ingress-nginx-controller --replicas=0
|
||||
|
||||
# 4. (Optional) After extended stable period, remove nginx
|
||||
kubectl delete namespace ingress-nginx
|
||||
```
|
||||
@@ -1,192 +0,0 @@
|
||||
# Nginx to Higress Annotation Compatibility
|
||||
|
||||
## ⚠️ Important: Do NOT Modify Your Ingress Resources!
|
||||
|
||||
**Higress natively supports `nginx.ingress.kubernetes.io/*` annotations** - no conversion or modification needed!
|
||||
|
||||
The Higress controller uses `ParseStringASAP()` which first tries `nginx.ingress.kubernetes.io/*` prefix, then falls back to `higress.io/*`. Your existing Ingress resources work as-is with Higress.
|
||||
|
||||
## Fully Compatible Annotations (Work As-Is)
|
||||
|
||||
These nginx annotations work directly with Higress without any changes:
|
||||
|
||||
| nginx annotation (keep as-is) | Higress also accepts | Notes |
|
||||
|-------------------------------|---------------------|-------|
|
||||
| `nginx.ingress.kubernetes.io/rewrite-target` | `higress.io/rewrite-target` | Supports capture groups |
|
||||
| `nginx.ingress.kubernetes.io/use-regex` | `higress.io/use-regex` | Enable regex path matching |
|
||||
| `nginx.ingress.kubernetes.io/ssl-redirect` | `higress.io/ssl-redirect` | Force HTTPS |
|
||||
| `nginx.ingress.kubernetes.io/force-ssl-redirect` | `higress.io/force-ssl-redirect` | Same behavior |
|
||||
| `nginx.ingress.kubernetes.io/backend-protocol` | `higress.io/backend-protocol` | HTTP/HTTPS/GRPC |
|
||||
| `nginx.ingress.kubernetes.io/proxy-body-size` | `higress.io/proxy-body-size` | Max body size |
|
||||
|
||||
### CORS
|
||||
|
||||
| nginx annotation | Higress annotation |
|
||||
|------------------|-------------------|
|
||||
| `nginx.ingress.kubernetes.io/enable-cors` | `higress.io/enable-cors` |
|
||||
| `nginx.ingress.kubernetes.io/cors-allow-origin` | `higress.io/cors-allow-origin` |
|
||||
| `nginx.ingress.kubernetes.io/cors-allow-methods` | `higress.io/cors-allow-methods` |
|
||||
| `nginx.ingress.kubernetes.io/cors-allow-headers` | `higress.io/cors-allow-headers` |
|
||||
| `nginx.ingress.kubernetes.io/cors-expose-headers` | `higress.io/cors-expose-headers` |
|
||||
| `nginx.ingress.kubernetes.io/cors-allow-credentials` | `higress.io/cors-allow-credentials` |
|
||||
| `nginx.ingress.kubernetes.io/cors-max-age` | `higress.io/cors-max-age` |
|
||||
|
||||
### Timeout & Retry
|
||||
|
||||
| nginx annotation | Higress annotation |
|
||||
|------------------|-------------------|
|
||||
| `nginx.ingress.kubernetes.io/proxy-connect-timeout` | `higress.io/proxy-connect-timeout` |
|
||||
| `nginx.ingress.kubernetes.io/proxy-send-timeout` | `higress.io/proxy-send-timeout` |
|
||||
| `nginx.ingress.kubernetes.io/proxy-read-timeout` | `higress.io/proxy-read-timeout` |
|
||||
| `nginx.ingress.kubernetes.io/proxy-next-upstream-tries` | `higress.io/proxy-next-upstream-tries` |
|
||||
|
||||
### Canary (Grayscale)
|
||||
|
||||
| nginx annotation | Higress annotation |
|
||||
|------------------|-------------------|
|
||||
| `nginx.ingress.kubernetes.io/canary` | `higress.io/canary` |
|
||||
| `nginx.ingress.kubernetes.io/canary-weight` | `higress.io/canary-weight` |
|
||||
| `nginx.ingress.kubernetes.io/canary-header` | `higress.io/canary-header` |
|
||||
| `nginx.ingress.kubernetes.io/canary-header-value` | `higress.io/canary-header-value` |
|
||||
| `nginx.ingress.kubernetes.io/canary-header-pattern` | `higress.io/canary-header-pattern` |
|
||||
| `nginx.ingress.kubernetes.io/canary-by-cookie` | `higress.io/canary-by-cookie` |
|
||||
|
||||
### Authentication
|
||||
|
||||
| nginx annotation | Higress annotation |
|
||||
|------------------|-------------------|
|
||||
| `nginx.ingress.kubernetes.io/auth-type` | `higress.io/auth-type` |
|
||||
| `nginx.ingress.kubernetes.io/auth-secret` | `higress.io/auth-secret` |
|
||||
| `nginx.ingress.kubernetes.io/auth-realm` | `higress.io/auth-realm` |
|
||||
|
||||
### Load Balancing
|
||||
|
||||
| nginx annotation | Higress annotation |
|
||||
|------------------|-------------------|
|
||||
| `nginx.ingress.kubernetes.io/load-balance` | `higress.io/load-balance` |
|
||||
| `nginx.ingress.kubernetes.io/upstream-hash-by` | `higress.io/upstream-hash-by` |
|
||||
|
||||
### IP Access Control
|
||||
|
||||
| nginx annotation | Higress annotation |
|
||||
|------------------|-------------------|
|
||||
| `nginx.ingress.kubernetes.io/whitelist-source-range` | `higress.io/whitelist-source-range` |
|
||||
| `nginx.ingress.kubernetes.io/denylist-source-range` | `higress.io/denylist-source-range` |
|
||||
|
||||
### Redirect
|
||||
|
||||
| nginx annotation | Higress annotation |
|
||||
|------------------|-------------------|
|
||||
| `nginx.ingress.kubernetes.io/permanent-redirect` | `higress.io/permanent-redirect` |
|
||||
| `nginx.ingress.kubernetes.io/temporal-redirect` | `higress.io/temporal-redirect` |
|
||||
| `nginx.ingress.kubernetes.io/permanent-redirect-code` | `higress.io/permanent-redirect-code` |
|
||||
|
||||
### Header Control
|
||||
|
||||
| nginx annotation | Higress annotation |
|
||||
|------------------|-------------------|
|
||||
| `nginx.ingress.kubernetes.io/proxy-set-headers` | `higress.io/proxy-set-headers` |
|
||||
| `nginx.ingress.kubernetes.io/proxy-hide-headers` | `higress.io/proxy-hide-headers` |
|
||||
| `nginx.ingress.kubernetes.io/proxy-pass-headers` | `higress.io/proxy-pass-headers` |
|
||||
|
||||
### Upstream TLS
|
||||
|
||||
| nginx annotation | Higress annotation |
|
||||
|------------------|-------------------|
|
||||
| `nginx.ingress.kubernetes.io/proxy-ssl-secret` | `higress.io/proxy-ssl-secret` |
|
||||
| `nginx.ingress.kubernetes.io/proxy-ssl-verify` | `higress.io/proxy-ssl-verify` |
|
||||
|
||||
### TLS Protocol & Cipher Control
|
||||
|
||||
Higress provides fine-grained TLS control via dedicated annotations:
|
||||
|
||||
| nginx annotation | Higress annotation | Notes |
|
||||
|------------------|-------------------|-------|
|
||||
| `nginx.ingress.kubernetes.io/ssl-protocols` | (see below) | Use Higress-specific annotations |
|
||||
|
||||
**Higress TLS annotations (no nginx equivalent - use these directly):**
|
||||
|
||||
| Higress annotation | Description | Example value |
|
||||
|-------------------|-------------|---------------|
|
||||
| `higress.io/tls-min-protocol-version` | Minimum TLS version | `TLSv1.2` |
|
||||
| `higress.io/tls-max-protocol-version` | Maximum TLS version | `TLSv1.3` |
|
||||
| `higress.io/ssl-cipher` | Allowed cipher suites | `ECDHE-RSA-AES128-GCM-SHA256` |
|
||||
|
||||
**Example: Restrict to TLS 1.2+**
|
||||
```yaml
|
||||
# nginx (using ssl-protocols)
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/ssl-protocols: "TLSv1.2 TLSv1.3"
|
||||
|
||||
# Higress (use dedicated annotations)
|
||||
annotations:
|
||||
higress.io/tls-min-protocol-version: "TLSv1.2"
|
||||
higress.io/tls-max-protocol-version: "TLSv1.3"
|
||||
```
|
||||
|
||||
**Example: Custom cipher suites**
|
||||
```yaml
|
||||
annotations:
|
||||
higress.io/ssl-cipher: "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384"
|
||||
```
|
||||
|
||||
## Unsupported Annotations (Require WASM Plugin)
|
||||
|
||||
These annotations have no direct Higress equivalent and require custom WASM plugins:
|
||||
|
||||
### Configuration Snippets
|
||||
```yaml
|
||||
# NOT supported - requires WASM plugin
|
||||
nginx.ingress.kubernetes.io/server-snippet: |
|
||||
location /custom { ... }
|
||||
nginx.ingress.kubernetes.io/configuration-snippet: |
|
||||
more_set_headers "X-Custom: value";
|
||||
nginx.ingress.kubernetes.io/stream-snippet: |
|
||||
# TCP/UDP snippets
|
||||
```
|
||||
|
||||
### Lua Scripting
|
||||
```yaml
|
||||
# NOT supported - convert to WASM plugin
|
||||
nginx.ingress.kubernetes.io/lua-resty-waf: "active"
|
||||
nginx.ingress.kubernetes.io/lua-resty-waf-score-threshold: "10"
|
||||
```
|
||||
|
||||
### ModSecurity
|
||||
```yaml
|
||||
# NOT supported - use Higress WAF plugin or custom WASM
|
||||
nginx.ingress.kubernetes.io/enable-modsecurity: "true"
|
||||
nginx.ingress.kubernetes.io/modsecurity-snippet: |
|
||||
SecRule ...
|
||||
```
|
||||
|
||||
### Rate Limiting (Complex)
|
||||
```yaml
|
||||
# Basic rate limiting supported via plugin
|
||||
# Complex Lua-based rate limiting requires WASM
|
||||
nginx.ingress.kubernetes.io/limit-rps: "10"
|
||||
nginx.ingress.kubernetes.io/limit-connections: "5"
|
||||
```
|
||||
|
||||
### Other Unsupported
|
||||
```yaml
|
||||
# NOT directly supported
|
||||
nginx.ingress.kubernetes.io/client-body-buffer-size
|
||||
nginx.ingress.kubernetes.io/proxy-buffering
|
||||
nginx.ingress.kubernetes.io/proxy-buffers-number
|
||||
nginx.ingress.kubernetes.io/proxy-buffer-size
|
||||
nginx.ingress.kubernetes.io/mirror-uri
|
||||
nginx.ingress.kubernetes.io/mirror-request-body
|
||||
nginx.ingress.kubernetes.io/grpc-backend
|
||||
nginx.ingress.kubernetes.io/custom-http-errors
|
||||
nginx.ingress.kubernetes.io/default-backend
|
||||
```
|
||||
|
||||
## Migration Script
|
||||
|
||||
Use this script to analyze Ingress annotations:
|
||||
|
||||
```bash
|
||||
# scripts/analyze-ingress.sh in this skill
|
||||
./scripts/analyze-ingress.sh <namespace>
|
||||
```
|
||||
@@ -1,115 +0,0 @@
|
||||
# Higress Built-in Plugins
|
||||
|
||||
Before writing custom WASM plugins, check if Higress has a built-in plugin that meets your needs.
|
||||
|
||||
**Plugin docs and images**: https://github.com/higress-group/higress-console/tree/main/backend/sdk/src/main/resources/plugins
|
||||
|
||||
## Authentication & Authorization
|
||||
|
||||
| Plugin | Description | Replaces nginx feature |
|
||||
|--------|-------------|----------------------|
|
||||
| `basic-auth` | HTTP Basic Authentication | `auth_basic` directive |
|
||||
| `jwt-auth` | JWT token validation | JWT Lua scripts |
|
||||
| `key-auth` | API Key authentication | Custom auth headers |
|
||||
| `hmac-auth` | HMAC signature authentication | Signature validation |
|
||||
| `oauth` | OAuth 2.0 authentication | OAuth Lua scripts |
|
||||
| `oidc` | OpenID Connect | OIDC integration |
|
||||
| `ext-auth` | External authorization service | `auth_request` directive |
|
||||
| `opa` | Open Policy Agent integration | Complex auth logic |
|
||||
|
||||
## Traffic Control
|
||||
|
||||
| Plugin | Description | Replaces nginx feature |
|
||||
|--------|-------------|----------------------|
|
||||
| `key-rate-limit` | Rate limiting by key | `limit_req` directive |
|
||||
| `cluster-key-rate-limit` | Distributed rate limiting | `limit_req` with shared state |
|
||||
| `ip-restriction` | IP whitelist/blacklist | `allow`/`deny` directives |
|
||||
| `request-block` | Block requests by pattern | `if` + `return 403` |
|
||||
| `traffic-tag` | Traffic tagging | Custom headers for routing |
|
||||
| `bot-detect` | Bot detection & blocking | Bot detection Lua scripts |
|
||||
|
||||
## Request/Response Modification
|
||||
|
||||
| Plugin | Description | Replaces nginx feature |
|
||||
|--------|-------------|----------------------|
|
||||
| `transformer` | Transform request/response | `proxy_set_header`, `more_set_headers` |
|
||||
| `cors` | CORS headers | `add_header` CORS headers |
|
||||
| `custom-response` | Custom static response | `return` directive |
|
||||
| `request-validation` | Request parameter validation | Validation Lua scripts |
|
||||
| `de-graphql` | GraphQL to REST conversion | GraphQL handling |
|
||||
|
||||
## Security
|
||||
|
||||
| Plugin | Description | Replaces nginx feature |
|
||||
|--------|-------------|----------------------|
|
||||
| `waf` | Web Application Firewall | ModSecurity module |
|
||||
| `geo-ip` | GeoIP-based access control | `geoip` module |
|
||||
|
||||
## Caching & Performance
|
||||
|
||||
| Plugin | Description | Replaces nginx feature |
|
||||
|--------|-------------|----------------------|
|
||||
| `cache-control` | Cache control headers | `expires`, `add_header Cache-Control` |
|
||||
|
||||
## AI Features (Higress-specific)
|
||||
|
||||
| Plugin | Description |
|
||||
|--------|-------------|
|
||||
| `ai-proxy` | AI model proxy |
|
||||
| `ai-cache` | AI response caching |
|
||||
| `ai-quota` | AI token quota |
|
||||
| `ai-token-ratelimit` | AI token rate limiting |
|
||||
| `ai-transformer` | AI request/response transform |
|
||||
| `ai-security-guard` | AI content security |
|
||||
| `ai-statistics` | AI usage statistics |
|
||||
| `mcp-server` | Model Context Protocol server |
|
||||
|
||||
## Using Built-in Plugins
|
||||
|
||||
### Via WasmPlugin CRD
|
||||
|
||||
```yaml
|
||||
apiVersion: extensions.higress.io/v1alpha1
|
||||
kind: WasmPlugin
|
||||
metadata:
|
||||
name: basic-auth-plugin
|
||||
namespace: higress-system
|
||||
spec:
|
||||
# Use built-in plugin image
|
||||
url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/basic-auth:1.0.0
|
||||
phase: AUTHN
|
||||
priority: 320
|
||||
defaultConfig:
|
||||
consumers:
|
||||
- name: user1
|
||||
credential: "admin:123456"
|
||||
```
|
||||
|
||||
### Via Higress Console
|
||||
|
||||
1. Navigate to **Plugins** → **Plugin Market**
|
||||
2. Find the desired plugin
|
||||
3. Click **Enable** and configure
|
||||
|
||||
## Image Registry Locations
|
||||
|
||||
Select the nearest registry based on your location:
|
||||
|
||||
| Region | Registry |
|
||||
|--------|----------|
|
||||
| China/Default | `higress-registry.cn-hangzhou.cr.aliyuncs.com` |
|
||||
| North America | `higress-registry.us-west-1.cr.aliyuncs.com` |
|
||||
| Southeast Asia | `higress-registry.ap-southeast-7.cr.aliyuncs.com` |
|
||||
|
||||
Example with regional registry:
|
||||
```yaml
|
||||
spec:
|
||||
url: oci://higress-registry.us-west-1.cr.aliyuncs.com/plugins/basic-auth:1.0.0
|
||||
```
|
||||
|
||||
## Plugin Configuration Reference
|
||||
|
||||
Each plugin has its own configuration schema. View the spec.yaml in the plugin directory:
|
||||
https://github.com/higress-group/higress-console/tree/main/backend/sdk/src/main/resources/plugins/<plugin-name>/spec.yaml
|
||||
|
||||
Or check the README files for detailed documentation.
|
||||
@@ -1,245 +0,0 @@
|
||||
# WASM Plugin Build and Deployment
|
||||
|
||||
## Plugin Project Structure
|
||||
|
||||
```
|
||||
my-plugin/
|
||||
├── main.go # Plugin entry point
|
||||
├── go.mod # Go module
|
||||
├── go.sum # Dependencies
|
||||
├── Dockerfile # OCI image build
|
||||
└── wasmplugin.yaml # K8s deployment manifest
|
||||
```
|
||||
|
||||
## Build Process
|
||||
|
||||
### 1. Initialize Project
|
||||
|
||||
```bash
|
||||
mkdir my-plugin && cd my-plugin
|
||||
go mod init my-plugin
|
||||
|
||||
# Set proxy (only needed in China due to network restrictions)
|
||||
# Skip this step if you're outside China or have direct access to GitHub
|
||||
go env -w GOPROXY=https://proxy.golang.com.cn,direct
|
||||
|
||||
# Get dependencies
|
||||
go get github.com/higress-group/proxy-wasm-go-sdk@go-1.24
|
||||
go get github.com/higress-group/wasm-go@main
|
||||
go get github.com/tidwall/gjson
|
||||
```
|
||||
|
||||
### 2. Write Plugin Code
|
||||
|
||||
See the higress-wasm-go-plugin skill for detailed API reference. Basic template:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func main() {}
|
||||
|
||||
func init() {
|
||||
wrapper.SetCtx(
|
||||
"my-plugin",
|
||||
wrapper.ParseConfig(parseConfig),
|
||||
wrapper.ProcessRequestHeaders(onHttpRequestHeaders),
|
||||
)
|
||||
}
|
||||
|
||||
type MyConfig struct {
|
||||
// Config fields
|
||||
}
|
||||
|
||||
func parseConfig(json gjson.Result, config *MyConfig) error {
|
||||
// Parse YAML config (converted to JSON)
|
||||
return nil
|
||||
}
|
||||
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
|
||||
// Process request
|
||||
return types.HeaderContinue
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Compile to WASM
|
||||
|
||||
```bash
|
||||
go mod tidy
|
||||
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o main.wasm ./
|
||||
```
|
||||
|
||||
### 4. Create Dockerfile
|
||||
|
||||
```dockerfile
|
||||
FROM scratch
|
||||
COPY main.wasm /plugin.wasm
|
||||
```
|
||||
|
||||
### 5. Build and Push Image
|
||||
|
||||
#### Option A: Use Your Own Registry
|
||||
|
||||
```bash
|
||||
# User provides registry
|
||||
REGISTRY=your-registry.com/higress-plugins
|
||||
|
||||
# Build
|
||||
docker build -t ${REGISTRY}/my-plugin:v1 .
|
||||
|
||||
# Push
|
||||
docker push ${REGISTRY}/my-plugin:v1
|
||||
```
|
||||
|
||||
#### Option B: Install Harbor (If No Registry Available)
|
||||
|
||||
If you don't have an image registry, we can install Harbor for you:
|
||||
|
||||
```bash
|
||||
# Prerequisites
|
||||
# - Kubernetes cluster with LoadBalancer or Ingress support
|
||||
# - Persistent storage (PVC)
|
||||
# - At least 4GB RAM and 2 CPU cores available
|
||||
|
||||
# Install Harbor via Helm
|
||||
helm repo add harbor https://helm.goharbor.io
|
||||
helm repo update
|
||||
|
||||
# Install with minimal configuration
|
||||
helm install harbor harbor/harbor \
|
||||
--namespace harbor-system --create-namespace \
|
||||
--set expose.type=nodePort \
|
||||
--set expose.tls.enabled=false \
|
||||
--set persistence.enabled=true \
|
||||
--set harborAdminPassword=Harbor12345
|
||||
|
||||
# Get Harbor access info
|
||||
export NODE_PORT=$(kubectl get svc -n harbor-system harbor-core -o jsonpath='{.spec.ports[0].nodePort}')
|
||||
export NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[0].address}')
|
||||
echo "Harbor URL: http://${NODE_IP}:${NODE_PORT}"
|
||||
echo "Username: admin"
|
||||
echo "Password: Harbor12345"
|
||||
|
||||
# Login to Harbor
|
||||
docker login ${NODE_IP}:${NODE_PORT} -u admin -p Harbor12345
|
||||
|
||||
# Create project in Harbor UI (http://${NODE_IP}:${NODE_PORT})
|
||||
# - Project Name: higress-plugins
|
||||
# - Access Level: Public
|
||||
|
||||
# Build and push plugin
|
||||
docker build -t ${NODE_IP}:${NODE_PORT}/higress-plugins/my-plugin:v1 .
|
||||
docker push ${NODE_IP}:${NODE_PORT}/higress-plugins/my-plugin:v1
|
||||
```
|
||||
|
||||
**Note**: For production use, enable TLS and use proper persistent storage.
|
||||
|
||||
## Deployment
|
||||
|
||||
### WasmPlugin CRD
|
||||
|
||||
```yaml
|
||||
apiVersion: extensions.higress.io/v1alpha1
|
||||
kind: WasmPlugin
|
||||
metadata:
|
||||
name: my-plugin
|
||||
namespace: higress-system
|
||||
spec:
|
||||
# OCI image URL
|
||||
url: oci://your-registry.com/higress-plugins/my-plugin:v1
|
||||
|
||||
# Plugin phase (when to execute)
|
||||
# UNSPECIFIED_PHASE | AUTHN | AUTHZ | STATS
|
||||
phase: UNSPECIFIED_PHASE
|
||||
|
||||
# Priority (higher = earlier execution)
|
||||
priority: 100
|
||||
|
||||
# Plugin configuration
|
||||
defaultConfig:
|
||||
key: value
|
||||
|
||||
# Optional: specific routes/domains
|
||||
matchRules:
|
||||
- domain:
|
||||
- "*.example.com"
|
||||
config:
|
||||
key: domain-specific-value
|
||||
- ingress:
|
||||
- default/my-ingress
|
||||
config:
|
||||
key: ingress-specific-value
|
||||
```
|
||||
|
||||
### Apply to Cluster
|
||||
|
||||
```bash
|
||||
kubectl apply -f wasmplugin.yaml
|
||||
```
|
||||
|
||||
### Verify Deployment
|
||||
|
||||
```bash
|
||||
# Check plugin status
|
||||
kubectl get wasmplugin -n higress-system
|
||||
|
||||
# Check gateway logs
|
||||
kubectl logs -n higress-system -l app=higress-gateway | grep -i plugin
|
||||
|
||||
# Test endpoint
|
||||
curl -v http://<gateway-ip>/test-path
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Plugin Not Loading
|
||||
|
||||
```bash
|
||||
# Check image accessibility
|
||||
kubectl run test --rm -it --image=your-registry.com/higress-plugins/my-plugin:v1 -- ls
|
||||
|
||||
# Check gateway events
|
||||
kubectl describe pod -n higress-system -l app=higress-gateway
|
||||
```
|
||||
|
||||
### Plugin Errors
|
||||
|
||||
```bash
|
||||
# Enable debug logging
|
||||
kubectl set env deployment/higress-gateway -n higress-system LOG_LEVEL=debug
|
||||
|
||||
# View plugin logs
|
||||
kubectl logs -n higress-system -l app=higress-gateway -f
|
||||
```
|
||||
|
||||
### Image Pull Issues
|
||||
|
||||
```bash
|
||||
# Create image pull secret if needed
|
||||
kubectl create secret docker-registry regcred \
|
||||
--docker-server=your-registry.com \
|
||||
--docker-username=user \
|
||||
--docker-password=pass \
|
||||
-n higress-system
|
||||
|
||||
# Reference in WasmPlugin
|
||||
spec:
|
||||
imagePullSecrets:
|
||||
- name: regcred
|
||||
```
|
||||
|
||||
## Plugin Configuration via Console
|
||||
|
||||
If using Higress Console:
|
||||
|
||||
1. Navigate to **Plugins** → **Custom Plugins**
|
||||
2. Click **Add Plugin**
|
||||
3. Enter OCI URL: `oci://your-registry.com/higress-plugins/my-plugin:v1`
|
||||
4. Configure plugin settings
|
||||
5. Apply to routes/domains as needed
|
||||
@@ -1,331 +0,0 @@
|
||||
# Common Nginx Snippet to WASM Plugin Patterns
|
||||
|
||||
## Header Manipulation
|
||||
|
||||
### Add Response Header
|
||||
|
||||
**Nginx snippet:**
|
||||
```nginx
|
||||
more_set_headers "X-Custom-Header: custom-value";
|
||||
more_set_headers "X-Request-ID: $request_id";
|
||||
```
|
||||
|
||||
**WASM plugin:**
|
||||
```go
|
||||
func onHttpResponseHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
|
||||
proxywasm.AddHttpResponseHeader("X-Custom-Header", "custom-value")
|
||||
|
||||
// For request ID, get from request context
|
||||
if reqId, err := proxywasm.GetHttpRequestHeader("x-request-id"); err == nil {
|
||||
proxywasm.AddHttpResponseHeader("X-Request-ID", reqId)
|
||||
}
|
||||
return types.HeaderContinue
|
||||
}
|
||||
```
|
||||
|
||||
### Remove Headers
|
||||
|
||||
**Nginx snippet:**
|
||||
```nginx
|
||||
more_clear_headers "Server";
|
||||
more_clear_headers "X-Powered-By";
|
||||
```
|
||||
|
||||
**WASM plugin:**
|
||||
```go
|
||||
func onHttpResponseHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
|
||||
proxywasm.RemoveHttpResponseHeader("Server")
|
||||
proxywasm.RemoveHttpResponseHeader("X-Powered-By")
|
||||
return types.HeaderContinue
|
||||
}
|
||||
```
|
||||
|
||||
### Conditional Header
|
||||
|
||||
**Nginx snippet:**
|
||||
```nginx
|
||||
if ($http_x_custom_flag = "enabled") {
|
||||
more_set_headers "X-Feature: active";
|
||||
}
|
||||
```
|
||||
|
||||
**WASM plugin:**
|
||||
```go
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
|
||||
flag, _ := proxywasm.GetHttpRequestHeader("x-custom-flag")
|
||||
if flag == "enabled" {
|
||||
proxywasm.AddHttpRequestHeader("X-Feature", "active")
|
||||
}
|
||||
return types.HeaderContinue
|
||||
}
|
||||
```
|
||||
|
||||
## Request Validation
|
||||
|
||||
### Block by Path Pattern
|
||||
|
||||
**Nginx snippet:**
|
||||
```nginx
|
||||
if ($request_uri ~* "(\.php|\.asp|\.aspx)$") {
|
||||
return 403;
|
||||
}
|
||||
```
|
||||
|
||||
**WASM plugin:**
|
||||
```go
|
||||
import "regexp"
|
||||
|
||||
type MyConfig struct {
|
||||
BlockPattern *regexp.Regexp
|
||||
}
|
||||
|
||||
func parseConfig(json gjson.Result, config *MyConfig) error {
|
||||
pattern := json.Get("blockPattern").String()
|
||||
if pattern == "" {
|
||||
pattern = `\.(php|asp|aspx)$`
|
||||
}
|
||||
config.BlockPattern = regexp.MustCompile(pattern)
|
||||
return nil
|
||||
}
|
||||
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
|
||||
path := ctx.Path()
|
||||
if config.BlockPattern.MatchString(path) {
|
||||
proxywasm.SendHttpResponse(403, nil, []byte("Forbidden"), -1)
|
||||
return types.HeaderStopAllIterationAndWatermark
|
||||
}
|
||||
return types.HeaderContinue
|
||||
}
|
||||
```
|
||||
|
||||
### Block by User Agent
|
||||
|
||||
**Nginx snippet:**
|
||||
```nginx
|
||||
if ($http_user_agent ~* "(bot|crawler|spider)") {
|
||||
return 403;
|
||||
}
|
||||
```
|
||||
|
||||
**WASM plugin:**
|
||||
```go
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
|
||||
ua, _ := proxywasm.GetHttpRequestHeader("user-agent")
|
||||
ua = strings.ToLower(ua)
|
||||
|
||||
blockedPatterns := []string{"bot", "crawler", "spider"}
|
||||
for _, pattern := range blockedPatterns {
|
||||
if strings.Contains(ua, pattern) {
|
||||
proxywasm.SendHttpResponse(403, nil, []byte("Blocked"), -1)
|
||||
return types.HeaderStopAllIterationAndWatermark
|
||||
}
|
||||
}
|
||||
return types.HeaderContinue
|
||||
}
|
||||
```
|
||||
|
||||
### Request Size Validation
|
||||
|
||||
**Nginx snippet:**
|
||||
```nginx
|
||||
if ($content_length > 10485760) {
|
||||
return 413;
|
||||
}
|
||||
```
|
||||
|
||||
**WASM plugin:**
|
||||
```go
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
|
||||
clStr, _ := proxywasm.GetHttpRequestHeader("content-length")
|
||||
if cl, err := strconv.ParseInt(clStr, 10, 64); err == nil {
|
||||
if cl > 10*1024*1024 { // 10MB
|
||||
proxywasm.SendHttpResponse(413, nil, []byte("Request too large"), -1)
|
||||
return types.HeaderStopAllIterationAndWatermark
|
||||
}
|
||||
}
|
||||
return types.HeaderContinue
|
||||
}
|
||||
```
|
||||
|
||||
## Request Modification
|
||||
|
||||
### URL Rewrite with Logic
|
||||
|
||||
**Nginx snippet:**
|
||||
```nginx
|
||||
set $backend "default";
|
||||
if ($http_x_version = "v2") {
|
||||
set $backend "v2";
|
||||
}
|
||||
rewrite ^/api/(.*)$ /api/$backend/$1 break;
|
||||
```
|
||||
|
||||
**WASM plugin:**
|
||||
```go
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
|
||||
version, _ := proxywasm.GetHttpRequestHeader("x-version")
|
||||
backend := "default"
|
||||
if version == "v2" {
|
||||
backend = "v2"
|
||||
}
|
||||
|
||||
path := ctx.Path()
|
||||
if strings.HasPrefix(path, "/api/") {
|
||||
newPath := "/api/" + backend + path[4:]
|
||||
proxywasm.ReplaceHttpRequestHeader(":path", newPath)
|
||||
}
|
||||
return types.HeaderContinue
|
||||
}
|
||||
```
|
||||
|
||||
### Add Query Parameter
|
||||
|
||||
**Nginx snippet:**
|
||||
```nginx
|
||||
if ($args !~ "source=") {
|
||||
set $args "${args}&source=gateway";
|
||||
}
|
||||
```
|
||||
|
||||
**WASM plugin:**
|
||||
```go
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
|
||||
path := ctx.Path()
|
||||
if !strings.Contains(path, "source=") {
|
||||
separator := "?"
|
||||
if strings.Contains(path, "?") {
|
||||
separator = "&"
|
||||
}
|
||||
newPath := path + separator + "source=gateway"
|
||||
proxywasm.ReplaceHttpRequestHeader(":path", newPath)
|
||||
}
|
||||
return types.HeaderContinue
|
||||
}
|
||||
```
|
||||
|
||||
## Lua Script Conversion
|
||||
|
||||
### Simple Lua Access Check
|
||||
|
||||
**Nginx Lua:**
|
||||
```lua
|
||||
access_by_lua_block {
|
||||
local token = ngx.var.http_authorization
|
||||
if not token or token == "" then
|
||||
ngx.exit(401)
|
||||
end
|
||||
}
|
||||
```
|
||||
|
||||
**WASM plugin:**
|
||||
```go
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
|
||||
token, _ := proxywasm.GetHttpRequestHeader("authorization")
|
||||
if token == "" {
|
||||
proxywasm.SendHttpResponse(401, [][2]string{
|
||||
{"WWW-Authenticate", "Bearer"},
|
||||
}, []byte("Unauthorized"), -1)
|
||||
return types.HeaderStopAllIterationAndWatermark
|
||||
}
|
||||
return types.HeaderContinue
|
||||
}
|
||||
```
|
||||
|
||||
### Lua with Redis
|
||||
|
||||
**Nginx Lua:**
|
||||
```lua
|
||||
access_by_lua_block {
|
||||
local redis = require "resty.redis"
|
||||
local red = redis:new()
|
||||
red:connect("127.0.0.1", 6379)
|
||||
|
||||
local ip = ngx.var.remote_addr
|
||||
local count = red:incr("rate:" .. ip)
|
||||
if count > 100 then
|
||||
ngx.exit(429)
|
||||
end
|
||||
red:expire("rate:" .. ip, 60)
|
||||
}
|
||||
```
|
||||
|
||||
**WASM plugin:**
|
||||
```go
|
||||
// See references/redis-client.md in higress-wasm-go-plugin skill
|
||||
func parseConfig(json gjson.Result, config *MyConfig) error {
|
||||
config.redis = wrapper.NewRedisClusterClient(wrapper.FQDNCluster{
|
||||
FQDN: json.Get("redisService").String(),
|
||||
Port: json.Get("redisPort").Int(),
|
||||
})
|
||||
return config.redis.Init("", json.Get("redisPassword").String(), 1000)
|
||||
}
|
||||
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
|
||||
ip, _ := proxywasm.GetHttpRequestHeader("x-real-ip")
|
||||
if ip == "" {
|
||||
ip, _ = proxywasm.GetHttpRequestHeader("x-forwarded-for")
|
||||
}
|
||||
|
||||
key := "rate:" + ip
|
||||
err := config.redis.Incr(key, func(val int) {
|
||||
if val > 100 {
|
||||
proxywasm.SendHttpResponse(429, nil, []byte("Rate limited"), -1)
|
||||
return
|
||||
}
|
||||
config.redis.Expire(key, 60, nil)
|
||||
proxywasm.ResumeHttpRequest()
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return types.HeaderContinue // Fallback on Redis error
|
||||
}
|
||||
return types.HeaderStopAllIterationAndWatermark
|
||||
}
|
||||
```
|
||||
|
||||
## Response Modification
|
||||
|
||||
### Inject Script/Content
|
||||
|
||||
**Nginx snippet:**
|
||||
```nginx
|
||||
sub_filter '</head>' '<script src="/tracking.js"></script></head>';
|
||||
sub_filter_once on;
|
||||
```
|
||||
|
||||
**WASM plugin:**
|
||||
```go
|
||||
func init() {
|
||||
wrapper.SetCtx(
|
||||
"inject-script",
|
||||
wrapper.ParseConfig(parseConfig),
|
||||
wrapper.ProcessResponseHeaders(onHttpResponseHeaders),
|
||||
wrapper.ProcessResponseBody(onHttpResponseBody),
|
||||
)
|
||||
}
|
||||
|
||||
func onHttpResponseHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
|
||||
contentType, _ := proxywasm.GetHttpResponseHeader("content-type")
|
||||
if strings.Contains(contentType, "text/html") {
|
||||
ctx.BufferResponseBody()
|
||||
proxywasm.RemoveHttpResponseHeader("content-length")
|
||||
}
|
||||
return types.HeaderContinue
|
||||
}
|
||||
|
||||
func onHttpResponseBody(ctx wrapper.HttpContext, config MyConfig, body []byte) types.Action {
|
||||
bodyStr := string(body)
|
||||
injection := `<script src="/tracking.js"></script></head>`
|
||||
newBody := strings.Replace(bodyStr, "</head>", injection, 1)
|
||||
proxywasm.ReplaceHttpResponseBody([]byte(newBody))
|
||||
return types.BodyContinue
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Error Handling**: Always handle external call failures gracefully
|
||||
2. **Performance**: Cache regex patterns in config, avoid recompiling
|
||||
3. **Timeout**: Set appropriate timeouts for external calls (default 500ms)
|
||||
4. **Logging**: Use `proxywasm.LogInfo/Warn/Error` for debugging
|
||||
5. **Testing**: Test locally with Docker Compose before deploying
|
||||
@@ -1,198 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Analyze nginx Ingress resources and identify migration requirements
|
||||
|
||||
set -e
|
||||
|
||||
NAMESPACE="${1:-}"
|
||||
OUTPUT_FORMAT="${2:-text}"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Supported nginx annotations that map to Higress
|
||||
SUPPORTED_ANNOTATIONS=(
|
||||
"rewrite-target"
|
||||
"use-regex"
|
||||
"ssl-redirect"
|
||||
"force-ssl-redirect"
|
||||
"backend-protocol"
|
||||
"proxy-body-size"
|
||||
"enable-cors"
|
||||
"cors-allow-origin"
|
||||
"cors-allow-methods"
|
||||
"cors-allow-headers"
|
||||
"cors-expose-headers"
|
||||
"cors-allow-credentials"
|
||||
"cors-max-age"
|
||||
"proxy-connect-timeout"
|
||||
"proxy-send-timeout"
|
||||
"proxy-read-timeout"
|
||||
"proxy-next-upstream-tries"
|
||||
"canary"
|
||||
"canary-weight"
|
||||
"canary-header"
|
||||
"canary-header-value"
|
||||
"canary-header-pattern"
|
||||
"canary-by-cookie"
|
||||
"auth-type"
|
||||
"auth-secret"
|
||||
"auth-realm"
|
||||
"load-balance"
|
||||
"upstream-hash-by"
|
||||
"whitelist-source-range"
|
||||
"denylist-source-range"
|
||||
"permanent-redirect"
|
||||
"temporal-redirect"
|
||||
"permanent-redirect-code"
|
||||
"proxy-set-headers"
|
||||
"proxy-hide-headers"
|
||||
"proxy-pass-headers"
|
||||
"proxy-ssl-secret"
|
||||
"proxy-ssl-verify"
|
||||
)
|
||||
|
||||
# Unsupported annotations requiring WASM plugins
|
||||
UNSUPPORTED_ANNOTATIONS=(
|
||||
"server-snippet"
|
||||
"configuration-snippet"
|
||||
"stream-snippet"
|
||||
"lua-resty-waf"
|
||||
"lua-resty-waf-score-threshold"
|
||||
"enable-modsecurity"
|
||||
"modsecurity-snippet"
|
||||
"limit-rps"
|
||||
"limit-connections"
|
||||
"limit-rate"
|
||||
"limit-rate-after"
|
||||
"client-body-buffer-size"
|
||||
"proxy-buffering"
|
||||
"proxy-buffers-number"
|
||||
"proxy-buffer-size"
|
||||
"custom-http-errors"
|
||||
"default-backend"
|
||||
)
|
||||
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo -e "${BLUE}Nginx to Higress Migration Analysis${NC}"
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo ""
|
||||
|
||||
# Check for ingress-nginx
|
||||
echo -e "${YELLOW}Checking for ingress-nginx...${NC}"
|
||||
if kubectl get pods -A 2>/dev/null | grep -q ingress-nginx; then
|
||||
echo -e "${GREEN}✓ ingress-nginx found${NC}"
|
||||
kubectl get pods -A | grep ingress-nginx | head -5
|
||||
else
|
||||
echo -e "${RED}✗ ingress-nginx not found${NC}"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Check IngressClass
|
||||
echo -e "${YELLOW}IngressClass resources:${NC}"
|
||||
kubectl get ingressclass 2>/dev/null || echo "No IngressClass resources found"
|
||||
echo ""
|
||||
|
||||
# Get Ingress resources
|
||||
if [ -n "$NAMESPACE" ]; then
|
||||
INGRESS_LIST=$(kubectl get ingress -n "$NAMESPACE" -o json 2>/dev/null)
|
||||
else
|
||||
INGRESS_LIST=$(kubectl get ingress -A -o json 2>/dev/null)
|
||||
fi
|
||||
|
||||
if [ -z "$INGRESS_LIST" ] || [ "$(echo "$INGRESS_LIST" | jq '.items | length')" -eq 0 ]; then
|
||||
echo -e "${RED}No Ingress resources found${NC}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
TOTAL_INGRESS=$(echo "$INGRESS_LIST" | jq '.items | length')
|
||||
echo -e "${YELLOW}Found ${TOTAL_INGRESS} Ingress resources${NC}"
|
||||
echo ""
|
||||
|
||||
# Analyze each Ingress
|
||||
COMPATIBLE_COUNT=0
|
||||
NEEDS_PLUGIN_COUNT=0
|
||||
UNSUPPORTED_FOUND=()
|
||||
|
||||
echo "$INGRESS_LIST" | jq -c '.items[]' | while read -r ingress; do
|
||||
NAME=$(echo "$ingress" | jq -r '.metadata.name')
|
||||
NS=$(echo "$ingress" | jq -r '.metadata.namespace')
|
||||
INGRESS_CLASS=$(echo "$ingress" | jq -r '.spec.ingressClassName // .metadata.annotations["kubernetes.io/ingress.class"] // "unknown"')
|
||||
|
||||
# Skip non-nginx ingresses
|
||||
if [[ "$INGRESS_CLASS" != "nginx" && "$INGRESS_CLASS" != "unknown" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
echo -e "${BLUE}-------------------------------------------${NC}"
|
||||
echo -e "${BLUE}Ingress: ${NS}/${NAME}${NC}"
|
||||
echo -e "IngressClass: ${INGRESS_CLASS}"
|
||||
|
||||
# Get annotations
|
||||
ANNOTATIONS=$(echo "$ingress" | jq -r '.metadata.annotations // {}')
|
||||
|
||||
HAS_UNSUPPORTED=false
|
||||
SUPPORTED_LIST=()
|
||||
UNSUPPORTED_LIST=()
|
||||
|
||||
# Check each annotation
|
||||
echo "$ANNOTATIONS" | jq -r 'keys[]' | while read -r key; do
|
||||
# Extract annotation name (remove prefix)
|
||||
ANNO_NAME=$(echo "$key" | sed 's/nginx.ingress.kubernetes.io\///' | sed 's/higress.io\///')
|
||||
|
||||
if [[ "$key" == nginx.ingress.kubernetes.io/* ]]; then
|
||||
# Check if supported
|
||||
IS_SUPPORTED=false
|
||||
for supported in "${SUPPORTED_ANNOTATIONS[@]}"; do
|
||||
if [[ "$ANNO_NAME" == "$supported" ]]; then
|
||||
IS_SUPPORTED=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Check if explicitly unsupported
|
||||
for unsupported in "${UNSUPPORTED_ANNOTATIONS[@]}"; do
|
||||
if [[ "$ANNO_NAME" == "$unsupported" ]]; then
|
||||
IS_SUPPORTED=false
|
||||
HAS_UNSUPPORTED=true
|
||||
VALUE=$(echo "$ANNOTATIONS" | jq -r --arg k "$key" '.[$k]')
|
||||
echo -e " ${RED}✗ $ANNO_NAME${NC} (requires WASM plugin)"
|
||||
if [[ "$ANNO_NAME" == *"snippet"* ]]; then
|
||||
echo -e " Value preview: $(echo "$VALUE" | head -1)"
|
||||
fi
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$IS_SUPPORTED" = true ]; then
|
||||
echo -e " ${GREEN}✓ $ANNO_NAME${NC}"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$HAS_UNSUPPORTED" = true ]; then
|
||||
echo -e "\n ${YELLOW}Status: Requires WASM plugin for full compatibility${NC}"
|
||||
else
|
||||
echo -e "\n ${GREEN}Status: Fully compatible${NC}"
|
||||
fi
|
||||
echo ""
|
||||
done
|
||||
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo -e "${BLUE}Summary${NC}"
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo -e "Total Ingress resources: ${TOTAL_INGRESS}"
|
||||
echo ""
|
||||
echo -e "${GREEN}✓ No Ingress modification needed!${NC}"
|
||||
echo " Higress natively supports nginx.ingress.kubernetes.io/* annotations."
|
||||
echo ""
|
||||
echo -e "${YELLOW}Next Steps:${NC}"
|
||||
echo "1. Install Higress with the SAME ingressClass as nginx"
|
||||
echo " (set global.enableStatus=false to disable Ingress status updates)"
|
||||
echo "2. For snippets/Lua: check Higress built-in plugins first, then generate custom WASM if needed"
|
||||
echo "3. Generate and run migration test script"
|
||||
echo "4. Switch traffic via DNS or L4 proxy after tests pass"
|
||||
echo "5. After stable period, uninstall nginx and enable status updates (global.enableStatus=true)"
|
||||
@@ -1,210 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Generate test script for all Ingress routes
|
||||
# Tests each route against Higress gateway to validate migration
|
||||
|
||||
set -e
|
||||
|
||||
NAMESPACE="${1:-}"
|
||||
|
||||
# Colors for output script
|
||||
cat << 'HEADER'
|
||||
#!/bin/bash
|
||||
# Higress Migration Test Script
|
||||
# Auto-generated - tests all Ingress routes against Higress gateway
|
||||
|
||||
set -e
|
||||
|
||||
GATEWAY_IP="${1:-}"
|
||||
TIMEOUT="${2:-5}"
|
||||
VERBOSE="${3:-false}"
|
||||
|
||||
if [ -z "$GATEWAY_IP" ]; then
|
||||
echo "Usage: $0 <higress-gateway-ip[:port]> [timeout] [verbose]"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " # With LoadBalancer IP"
|
||||
echo " $0 10.0.0.100 5 true"
|
||||
echo ""
|
||||
echo " # With port-forward (run this first: kubectl port-forward -n higress-system svc/higress-gateway 8080:80 &)"
|
||||
echo " $0 127.0.0.1:8080 5 true"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
TOTAL=0
|
||||
PASSED=0
|
||||
FAILED=0
|
||||
FAILED_TESTS=()
|
||||
|
||||
test_route() {
|
||||
local host="$1"
|
||||
local path="$2"
|
||||
local expected_code="${3:-200}"
|
||||
local description="$4"
|
||||
|
||||
TOTAL=$((TOTAL + 1))
|
||||
|
||||
# Build URL
|
||||
local url="http://${GATEWAY_IP}${path}"
|
||||
|
||||
# Make request
|
||||
local response
|
||||
response=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||
-H "Host: ${host}" \
|
||||
--connect-timeout "${TIMEOUT}" \
|
||||
--max-time $((TIMEOUT * 2)) \
|
||||
"${url}" 2>/dev/null) || response="000"
|
||||
|
||||
# Check result
|
||||
if [ "$response" = "$expected_code" ] || [ "$expected_code" = "*" ]; then
|
||||
PASSED=$((PASSED + 1))
|
||||
echo -e "${GREEN}✓${NC} [${response}] ${host}${path}"
|
||||
if [ "$VERBOSE" = "true" ]; then
|
||||
echo " Expected: ${expected_code}, Got: ${response}"
|
||||
fi
|
||||
else
|
||||
FAILED=$((FAILED + 1))
|
||||
FAILED_TESTS+=("${host}${path} (expected ${expected_code}, got ${response})")
|
||||
echo -e "${RED}✗${NC} [${response}] ${host}${path}"
|
||||
echo " Expected: ${expected_code}, Got: ${response}"
|
||||
fi
|
||||
}
|
||||
|
||||
echo "========================================"
|
||||
echo "Higress Migration Test"
|
||||
echo "========================================"
|
||||
echo "Gateway IP: ${GATEWAY_IP}"
|
||||
echo "Timeout: ${TIMEOUT}s"
|
||||
echo ""
|
||||
echo "Testing routes..."
|
||||
echo ""
|
||||
|
||||
HEADER
|
||||
|
||||
# Get Ingress resources
|
||||
if [ -n "$NAMESPACE" ]; then
|
||||
INGRESS_JSON=$(kubectl get ingress -n "$NAMESPACE" -o json 2>/dev/null)
|
||||
else
|
||||
INGRESS_JSON=$(kubectl get ingress -A -o json 2>/dev/null)
|
||||
fi
|
||||
|
||||
if [ -z "$INGRESS_JSON" ] || [ "$(echo "$INGRESS_JSON" | jq '.items | length')" -eq 0 ]; then
|
||||
echo "# No Ingress resources found"
|
||||
echo "echo 'No Ingress resources found to test'"
|
||||
echo "exit 0"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Generate test cases for each Ingress
|
||||
echo "$INGRESS_JSON" | jq -c '.items[]' | while read -r ingress; do
|
||||
NAME=$(echo "$ingress" | jq -r '.metadata.name')
|
||||
NS=$(echo "$ingress" | jq -r '.metadata.namespace')
|
||||
|
||||
echo ""
|
||||
echo "# ================================================"
|
||||
echo "# Ingress: ${NS}/${NAME}"
|
||||
echo "# ================================================"
|
||||
|
||||
# Check for TLS hosts
|
||||
TLS_HOSTS=$(echo "$ingress" | jq -r '.spec.tls[]?.hosts[]?' 2>/dev/null | sort -u)
|
||||
|
||||
# Process each rule
|
||||
echo "$ingress" | jq -c '.spec.rules[]?' | while read -r rule; do
|
||||
HOST=$(echo "$rule" | jq -r '.host // "*"')
|
||||
|
||||
# Process each path
|
||||
echo "$rule" | jq -c '.http.paths[]?' | while read -r path_item; do
|
||||
PATH=$(echo "$path_item" | jq -r '.path // "/"')
|
||||
PATH_TYPE=$(echo "$path_item" | jq -r '.pathType // "Prefix"')
|
||||
SERVICE=$(echo "$path_item" | jq -r '.backend.service.name // .backend.serviceName // "unknown"')
|
||||
PORT=$(echo "$path_item" | jq -r '.backend.service.port.number // .backend.service.port.name // .backend.servicePort // "80"')
|
||||
|
||||
# Generate test
|
||||
# For Prefix paths, test the exact path
|
||||
# For Exact paths, test exactly
|
||||
# Add a simple 200 or * expectation (can be customized)
|
||||
|
||||
echo ""
|
||||
echo "# Path: ${PATH} (${PATH_TYPE}) -> ${SERVICE}:${PORT}"
|
||||
|
||||
# Test the path
|
||||
if [ "$PATH_TYPE" = "Exact" ]; then
|
||||
echo "test_route \"${HOST}\" \"${PATH}\" \"*\" \"Exact path\""
|
||||
else
|
||||
# For Prefix, test base path and a subpath
|
||||
echo "test_route \"${HOST}\" \"${PATH}\" \"*\" \"Prefix path\""
|
||||
|
||||
# If path doesn't end with /, add a subpath test
|
||||
if [[ ! "$PATH" =~ /$ ]] && [ "$PATH" != "/" ]; then
|
||||
echo "test_route \"${HOST}\" \"${PATH}/\" \"*\" \"Prefix path with trailing slash\""
|
||||
fi
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
# Check for specific annotations that might need special testing
|
||||
REWRITE=$(echo "$ingress" | jq -r '.metadata.annotations["nginx.ingress.kubernetes.io/rewrite-target"] // .metadata.annotations["higress.io/rewrite-target"] // ""')
|
||||
if [ -n "$REWRITE" ] && [ "$REWRITE" != "null" ]; then
|
||||
echo ""
|
||||
echo "# Note: This Ingress has rewrite-target: ${REWRITE}"
|
||||
echo "# Verify the rewritten path manually if needed"
|
||||
fi
|
||||
|
||||
CANARY=$(echo "$ingress" | jq -r '.metadata.annotations["nginx.ingress.kubernetes.io/canary"] // .metadata.annotations["higress.io/canary"] // ""')
|
||||
if [ "$CANARY" = "true" ]; then
|
||||
echo ""
|
||||
echo "# Note: This is a canary Ingress - test with appropriate headers/cookies"
|
||||
CANARY_HEADER=$(echo "$ingress" | jq -r '.metadata.annotations["nginx.ingress.kubernetes.io/canary-header"] // .metadata.annotations["higress.io/canary-header"] // ""')
|
||||
CANARY_VALUE=$(echo "$ingress" | jq -r '.metadata.annotations["nginx.ingress.kubernetes.io/canary-header-value"] // .metadata.annotations["higress.io/canary-header-value"] // ""')
|
||||
if [ -n "$CANARY_HEADER" ] && [ "$CANARY_HEADER" != "null" ]; then
|
||||
echo "# Canary header: ${CANARY_HEADER}=${CANARY_VALUE}"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Generate summary section
|
||||
cat << 'FOOTER'
|
||||
|
||||
# ================================================
|
||||
# Summary
|
||||
# ================================================
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo "Test Summary"
|
||||
echo "========================================"
|
||||
echo -e "Total: ${TOTAL}"
|
||||
echo -e "Passed: ${GREEN}${PASSED}${NC}"
|
||||
echo -e "Failed: ${RED}${FAILED}${NC}"
|
||||
echo ""
|
||||
|
||||
if [ ${FAILED} -gt 0 ]; then
|
||||
echo -e "${YELLOW}Failed tests:${NC}"
|
||||
for test in "${FAILED_TESTS[@]}"; do
|
||||
echo -e " ${RED}•${NC} $test"
|
||||
done
|
||||
echo ""
|
||||
echo -e "${YELLOW}⚠ Some tests failed. Please investigate before switching traffic.${NC}"
|
||||
exit 1
|
||||
else
|
||||
echo -e "${GREEN}✓ All tests passed!${NC}"
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo -e "${GREEN}Ready for Traffic Cutover${NC}"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo "1. Switch traffic to Higress gateway:"
|
||||
echo " - DNS: Update A/CNAME records to ${GATEWAY_IP}"
|
||||
echo " - L4 Proxy: Update upstream to ${GATEWAY_IP}"
|
||||
echo ""
|
||||
echo "2. Monitor for errors after switch"
|
||||
echo ""
|
||||
echo "3. Once stable, scale down nginx:"
|
||||
echo " kubectl scale deployment -n ingress-nginx ingress-nginx-controller --replicas=0"
|
||||
echo ""
|
||||
fi
|
||||
FOOTER
|
||||
@@ -1,261 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Generate WASM plugin scaffold for nginx snippet migration
|
||||
|
||||
set -e
|
||||
|
||||
if [ "$#" -lt 1 ]; then
|
||||
echo "Usage: $0 <plugin-name> [output-dir]"
|
||||
echo ""
|
||||
echo "Example: $0 custom-headers ./plugins"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PLUGIN_NAME="$1"
|
||||
OUTPUT_DIR="${2:-.}"
|
||||
PLUGIN_DIR="${OUTPUT_DIR}/${PLUGIN_NAME}"
|
||||
|
||||
# Colors
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
echo -e "${YELLOW}Generating WASM plugin scaffold: ${PLUGIN_NAME}${NC}"
|
||||
|
||||
# Create directory
|
||||
mkdir -p "$PLUGIN_DIR"
|
||||
|
||||
# Generate go.mod
|
||||
cat > "${PLUGIN_DIR}/go.mod" << EOF
|
||||
module ${PLUGIN_NAME}
|
||||
|
||||
go 1.24
|
||||
|
||||
require (
|
||||
github.com/higress-group/proxy-wasm-go-sdk v1.0.1-0.20241230091623-edc7227eb588
|
||||
github.com/higress-group/wasm-go v1.0.1-0.20250107151137-19a0ab53cfec
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
)
|
||||
EOF
|
||||
|
||||
# Generate main.go
|
||||
cat > "${PLUGIN_DIR}/main.go" << 'EOF'
|
||||
package main
|
||||
|
||||
import (
|
||||
"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/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func main() {}
|
||||
|
||||
func init() {
|
||||
wrapper.SetCtx(
|
||||
"PLUGIN_NAME_PLACEHOLDER",
|
||||
wrapper.ParseConfig(parseConfig),
|
||||
wrapper.ProcessRequestHeaders(onHttpRequestHeaders),
|
||||
wrapper.ProcessRequestBody(onHttpRequestBody),
|
||||
wrapper.ProcessResponseHeaders(onHttpResponseHeaders),
|
||||
wrapper.ProcessResponseBody(onHttpResponseBody),
|
||||
)
|
||||
}
|
||||
|
||||
// PluginConfig holds the plugin configuration
|
||||
type PluginConfig struct {
|
||||
// TODO: Add configuration fields
|
||||
// Example:
|
||||
// HeaderName string
|
||||
// HeaderValue string
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
// parseConfig parses the plugin configuration from YAML (converted to JSON)
|
||||
func parseConfig(json gjson.Result, config *PluginConfig) error {
|
||||
// TODO: Parse configuration
|
||||
// Example:
|
||||
// config.HeaderName = json.Get("headerName").String()
|
||||
// config.HeaderValue = json.Get("headerValue").String()
|
||||
config.Enabled = json.Get("enabled").Bool()
|
||||
|
||||
proxywasm.LogInfof("Plugin config loaded: enabled=%v", config.Enabled)
|
||||
return nil
|
||||
}
|
||||
|
||||
// onHttpRequestHeaders is called when request headers are received
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config PluginConfig) types.Action {
|
||||
if !config.Enabled {
|
||||
return types.HeaderContinue
|
||||
}
|
||||
|
||||
// TODO: Implement request header processing
|
||||
// Example: Add custom header
|
||||
// proxywasm.AddHttpRequestHeader(config.HeaderName, config.HeaderValue)
|
||||
|
||||
// Example: Check path and block
|
||||
// path := ctx.Path()
|
||||
// if strings.Contains(path, "/blocked") {
|
||||
// proxywasm.SendHttpResponse(403, nil, []byte("Forbidden"), -1)
|
||||
// return types.HeaderStopAllIterationAndWatermark
|
||||
// }
|
||||
|
||||
return types.HeaderContinue
|
||||
}
|
||||
|
||||
// onHttpRequestBody is called when request body is received
|
||||
// Remove this function from init() if not needed
|
||||
func onHttpRequestBody(ctx wrapper.HttpContext, config PluginConfig, body []byte) types.Action {
|
||||
if !config.Enabled {
|
||||
return types.BodyContinue
|
||||
}
|
||||
|
||||
// TODO: Implement request body processing
|
||||
// Example: Log body size
|
||||
// proxywasm.LogInfof("Request body size: %d", len(body))
|
||||
|
||||
return types.BodyContinue
|
||||
}
|
||||
|
||||
// onHttpResponseHeaders is called when response headers are received
|
||||
func onHttpResponseHeaders(ctx wrapper.HttpContext, config PluginConfig) types.Action {
|
||||
if !config.Enabled {
|
||||
return types.HeaderContinue
|
||||
}
|
||||
|
||||
// TODO: Implement response header processing
|
||||
// Example: Add security headers
|
||||
// proxywasm.AddHttpResponseHeader("X-Content-Type-Options", "nosniff")
|
||||
// proxywasm.AddHttpResponseHeader("X-Frame-Options", "DENY")
|
||||
|
||||
return types.HeaderContinue
|
||||
}
|
||||
|
||||
// onHttpResponseBody is called when response body is received
|
||||
// Remove this function from init() if not needed
|
||||
func onHttpResponseBody(ctx wrapper.HttpContext, config PluginConfig, body []byte) types.Action {
|
||||
if !config.Enabled {
|
||||
return types.BodyContinue
|
||||
}
|
||||
|
||||
// TODO: Implement response body processing
|
||||
// Example: Modify response body
|
||||
// newBody := strings.Replace(string(body), "old", "new", -1)
|
||||
// proxywasm.ReplaceHttpResponseBody([]byte(newBody))
|
||||
|
||||
return types.BodyContinue
|
||||
}
|
||||
EOF
|
||||
|
||||
# Replace plugin name placeholder
|
||||
sed -i "s/PLUGIN_NAME_PLACEHOLDER/${PLUGIN_NAME}/g" "${PLUGIN_DIR}/main.go"
|
||||
|
||||
# Generate Dockerfile
|
||||
cat > "${PLUGIN_DIR}/Dockerfile" << 'EOF'
|
||||
FROM scratch
|
||||
COPY main.wasm /plugin.wasm
|
||||
EOF
|
||||
|
||||
# Generate build script
|
||||
cat > "${PLUGIN_DIR}/build.sh" << 'EOF'
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "Downloading dependencies..."
|
||||
go mod tidy
|
||||
|
||||
echo "Building WASM plugin..."
|
||||
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o main.wasm ./
|
||||
|
||||
echo "Build complete: main.wasm"
|
||||
ls -lh main.wasm
|
||||
EOF
|
||||
chmod +x "${PLUGIN_DIR}/build.sh"
|
||||
|
||||
# Generate WasmPlugin manifest
|
||||
cat > "${PLUGIN_DIR}/wasmplugin.yaml" << EOF
|
||||
apiVersion: extensions.higress.io/v1alpha1
|
||||
kind: WasmPlugin
|
||||
metadata:
|
||||
name: ${PLUGIN_NAME}
|
||||
namespace: higress-system
|
||||
spec:
|
||||
# TODO: Replace with your registry
|
||||
url: oci://YOUR_REGISTRY/${PLUGIN_NAME}:v1
|
||||
phase: UNSPECIFIED_PHASE
|
||||
priority: 100
|
||||
defaultConfig:
|
||||
enabled: true
|
||||
# TODO: Add your configuration
|
||||
# Optional: Apply to specific routes/domains
|
||||
# matchRules:
|
||||
# - domain:
|
||||
# - "*.example.com"
|
||||
# config:
|
||||
# enabled: true
|
||||
EOF
|
||||
|
||||
# Generate README
|
||||
cat > "${PLUGIN_DIR}/README.md" << EOF
|
||||
# ${PLUGIN_NAME}
|
||||
|
||||
A Higress WASM plugin migrated from nginx configuration.
|
||||
|
||||
## Build
|
||||
|
||||
\`\`\`bash
|
||||
./build.sh
|
||||
\`\`\`
|
||||
|
||||
## Push to Registry
|
||||
|
||||
\`\`\`bash
|
||||
# Set your registry
|
||||
REGISTRY=your-registry.com/higress-plugins
|
||||
|
||||
# Build Docker image
|
||||
docker build -t \${REGISTRY}/${PLUGIN_NAME}:v1 .
|
||||
|
||||
# Push
|
||||
docker push \${REGISTRY}/${PLUGIN_NAME}:v1
|
||||
\`\`\`
|
||||
|
||||
## Deploy
|
||||
|
||||
1. Update \`wasmplugin.yaml\` with your registry URL
|
||||
2. Apply to cluster:
|
||||
\`\`\`bash
|
||||
kubectl apply -f wasmplugin.yaml
|
||||
\`\`\`
|
||||
|
||||
## Configuration
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| enabled | bool | true | Enable/disable plugin |
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] Implement plugin logic in main.go
|
||||
- [ ] Add configuration fields
|
||||
- [ ] Test locally
|
||||
- [ ] Push to registry
|
||||
- [ ] Deploy to cluster
|
||||
EOF
|
||||
|
||||
echo -e "\n${GREEN}✓ Plugin scaffold generated at: ${PLUGIN_DIR}${NC}"
|
||||
echo ""
|
||||
echo "Files created:"
|
||||
echo " - ${PLUGIN_DIR}/main.go (plugin source)"
|
||||
echo " - ${PLUGIN_DIR}/go.mod (Go module)"
|
||||
echo " - ${PLUGIN_DIR}/Dockerfile (OCI image)"
|
||||
echo " - ${PLUGIN_DIR}/build.sh (build script)"
|
||||
echo " - ${PLUGIN_DIR}/wasmplugin.yaml (K8s manifest)"
|
||||
echo " - ${PLUGIN_DIR}/README.md (documentation)"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Next steps:${NC}"
|
||||
echo "1. cd ${PLUGIN_DIR}"
|
||||
echo "2. Edit main.go to implement your logic"
|
||||
echo "3. Run: ./build.sh"
|
||||
echo "4. Push image to your registry"
|
||||
echo "5. Update wasmplugin.yaml with registry URL"
|
||||
echo "6. Deploy: kubectl apply -f wasmplugin.yaml"
|
||||
@@ -1,157 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Install Harbor registry for WASM plugin images
|
||||
# Only use this if you don't have an existing image registry
|
||||
|
||||
set -e
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
HARBOR_NAMESPACE="${1:-harbor-system}"
|
||||
HARBOR_PASSWORD="${2:-Harbor12345}"
|
||||
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo -e "${BLUE}Harbor Registry Installation${NC}"
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}This will install Harbor in your cluster.${NC}"
|
||||
echo ""
|
||||
echo "Configuration:"
|
||||
echo " Namespace: ${HARBOR_NAMESPACE}"
|
||||
echo " Admin Password: ${HARBOR_PASSWORD}"
|
||||
echo " Exposure: NodePort (no TLS)"
|
||||
echo " Persistence: Enabled (default StorageClass)"
|
||||
echo ""
|
||||
read -p "Continue? (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Aborted."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check prerequisites
|
||||
echo -e "\n${YELLOW}Checking prerequisites...${NC}"
|
||||
|
||||
# Check for helm
|
||||
if ! command -v helm &> /dev/null; then
|
||||
echo -e "${RED}✗ helm not found. Please install helm 3.x${NC}"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${GREEN}✓ helm found${NC}"
|
||||
|
||||
# Check for kubectl
|
||||
if ! command -v kubectl &> /dev/null; then
|
||||
echo -e "${RED}✗ kubectl not found${NC}"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${GREEN}✓ kubectl found${NC}"
|
||||
|
||||
# Check cluster access
|
||||
if ! kubectl get nodes &> /dev/null; then
|
||||
echo -e "${RED}✗ Cannot access cluster${NC}"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${GREEN}✓ Cluster access OK${NC}"
|
||||
|
||||
# Check for default StorageClass
|
||||
if ! kubectl get storageclass -o name | grep -q .; then
|
||||
echo -e "${YELLOW}⚠ No StorageClass found. Harbor needs persistent storage.${NC}"
|
||||
echo " You may need to install a storage provisioner first."
|
||||
read -p "Continue anyway? (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Add Harbor helm repo
|
||||
echo -e "\n${YELLOW}Adding Harbor helm repository...${NC}"
|
||||
helm repo add harbor https://helm.goharbor.io
|
||||
helm repo update
|
||||
echo -e "${GREEN}✓ Repository added${NC}"
|
||||
|
||||
# Install Harbor
|
||||
echo -e "\n${YELLOW}Installing Harbor...${NC}"
|
||||
helm install harbor harbor/harbor \
|
||||
--namespace "${HARBOR_NAMESPACE}" --create-namespace \
|
||||
--set expose.type=nodePort \
|
||||
--set expose.tls.enabled=false \
|
||||
--set persistence.enabled=true \
|
||||
--set harborAdminPassword="${HARBOR_PASSWORD}" \
|
||||
--wait --timeout 10m
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${RED}✗ Harbor installation failed${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✓ Harbor installed successfully${NC}"
|
||||
|
||||
# Wait for Harbor to be ready
|
||||
echo -e "\n${YELLOW}Waiting for Harbor to be ready...${NC}"
|
||||
kubectl wait --for=condition=ready pod -l app=harbor -n "${HARBOR_NAMESPACE}" --timeout=300s
|
||||
|
||||
# Get access information
|
||||
echo -e "\n${BLUE}========================================${NC}"
|
||||
echo -e "${BLUE}Harbor Access Information${NC}"
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
|
||||
NODE_PORT=$(kubectl get svc -n "${HARBOR_NAMESPACE}" harbor-core -o jsonpath='{.spec.ports[0].nodePort}')
|
||||
NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="ExternalIP")].address}')
|
||||
if [ -z "$NODE_IP" ]; then
|
||||
NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}')
|
||||
fi
|
||||
|
||||
HARBOR_URL="${NODE_IP}:${NODE_PORT}"
|
||||
|
||||
echo ""
|
||||
echo -e "Harbor URL: ${GREEN}http://${HARBOR_URL}${NC}"
|
||||
echo -e "Username: ${GREEN}admin${NC}"
|
||||
echo -e "Password: ${GREEN}${HARBOR_PASSWORD}${NC}"
|
||||
echo ""
|
||||
|
||||
# Test Docker login
|
||||
echo -e "${YELLOW}Testing Docker login...${NC}"
|
||||
if docker login "${HARBOR_URL}" -u admin -p "${HARBOR_PASSWORD}" &> /dev/null; then
|
||||
echo -e "${GREEN}✓ Docker login successful${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠ Docker login failed. You may need to:${NC}"
|
||||
echo " 1. Add '${HARBOR_URL}' to Docker's insecure registries"
|
||||
echo " 2. Restart Docker daemon"
|
||||
echo ""
|
||||
echo " Edit /etc/docker/daemon.json (Linux) or Docker Desktop settings (Mac/Windows):"
|
||||
echo " {"
|
||||
echo " \"insecure-registries\": [\"${HARBOR_URL}\"]"
|
||||
echo " }"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo -e "${BLUE}Next Steps${NC}"
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo ""
|
||||
echo "1. Open Harbor UI: http://${HARBOR_URL}"
|
||||
echo "2. Login with admin/${HARBOR_PASSWORD}"
|
||||
echo "3. Create a new project:"
|
||||
echo " - Click 'Projects' → 'New Project'"
|
||||
echo " - Name: higress-plugins"
|
||||
echo " - Access Level: Public"
|
||||
echo ""
|
||||
echo "4. Build and push your plugin:"
|
||||
echo " docker build -t ${HARBOR_URL}/higress-plugins/my-plugin:v1 ."
|
||||
echo " docker push ${HARBOR_URL}/higress-plugins/my-plugin:v1"
|
||||
echo ""
|
||||
echo "5. Use in WasmPlugin:"
|
||||
echo " url: oci://${HARBOR_URL}/higress-plugins/my-plugin:v1"
|
||||
echo ""
|
||||
echo -e "${YELLOW}⚠ Note: This is a basic installation for testing.${NC}"
|
||||
echo " For production use:"
|
||||
echo " - Enable TLS (set expose.tls.enabled=true)"
|
||||
echo " - Use LoadBalancer or Ingress instead of NodePort"
|
||||
echo " - Configure proper persistent storage"
|
||||
echo " - Set strong admin password"
|
||||
echo ""
|
||||
8
.gitmodules
vendored
8
.gitmodules
vendored
@@ -1,17 +1,17 @@
|
||||
[submodule "istio/api"]
|
||||
path = istio/api
|
||||
url = https://github.com/higress-group/api
|
||||
branch = istio-1.27
|
||||
branch = istio-1.19
|
||||
shallow = true
|
||||
[submodule "istio/istio"]
|
||||
path = istio/istio
|
||||
url = https://github.com/higress-group/istio
|
||||
branch = istio-1.27
|
||||
branch = istio-1.19
|
||||
shallow = true
|
||||
[submodule "istio/client-go"]
|
||||
path = istio/client-go
|
||||
url = https://github.com/higress-group/client-go
|
||||
branch = istio-1.27
|
||||
branch = istio-1.19
|
||||
shallow = true
|
||||
[submodule "istio/pkg"]
|
||||
path = istio/pkg
|
||||
@@ -26,7 +26,7 @@
|
||||
[submodule "envoy/go-control-plane"]
|
||||
path = envoy/go-control-plane
|
||||
url = https://github.com/higress-group/go-control-plane
|
||||
branch = istio-1.27
|
||||
branch = istio-1.19
|
||||
shallow = true
|
||||
[submodule "envoy/envoy"]
|
||||
path = envoy/envoy
|
||||
|
||||
@@ -35,8 +35,7 @@ header:
|
||||
- 'hgctl/pkg/manifests'
|
||||
- 'pkg/ingress/kube/gateway/istio/testdata'
|
||||
- 'release-notes/**'
|
||||
- '.cursor/**'
|
||||
- '.claude/**'
|
||||
- '.cursor/**'
|
||||
|
||||
comment: on-failure
|
||||
dependency:
|
||||
|
||||
@@ -1 +1 @@
|
||||
higress-console: v2.1.9
|
||||
higress-console: v2.1.11
|
||||
@@ -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.2.0/envoy-symbol-ARCH.tar.gz
|
||||
export ENVOY_PACKAGE_URL_PATTERN?=https://github.com/higress-group/proxy/releases/download/v2.1.11/envoy-symbol-ARCH.tar.gz
|
||||
|
||||
build-envoy: prebuild
|
||||
./tools/hack/build-envoy.sh
|
||||
@@ -201,7 +201,7 @@ install: pre-install
|
||||
|
||||
HIGRESS_LATEST_IMAGE_TAG ?= latest
|
||||
ENVOY_LATEST_IMAGE_TAG ?= cdf0f16bf622102f89a0d0257834f43f502e4b99
|
||||
ISTIO_LATEST_IMAGE_TAG ?= a7525f292c38d7d3380f3ce7ee971ad6e3c46adf
|
||||
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'
|
||||
|
||||
@@ -41,56 +41,6 @@ const (
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
// Route type for matching rules.
|
||||
// Extended by Higress
|
||||
type RouteType int32
|
||||
|
||||
const (
|
||||
// HTTP route (default)
|
||||
RouteType_HTTP RouteType = 0
|
||||
// GRPC route
|
||||
RouteType_GRPC RouteType = 1
|
||||
)
|
||||
|
||||
// Enum value maps for RouteType.
|
||||
var (
|
||||
RouteType_name = map[int32]string{
|
||||
0: "HTTP",
|
||||
1: "GRPC",
|
||||
}
|
||||
RouteType_value = map[string]int32{
|
||||
"HTTP": 0,
|
||||
"GRPC": 1,
|
||||
}
|
||||
)
|
||||
|
||||
func (x RouteType) Enum() *RouteType {
|
||||
p := new(RouteType)
|
||||
*p = x
|
||||
return p
|
||||
}
|
||||
|
||||
func (x RouteType) String() string {
|
||||
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
|
||||
}
|
||||
|
||||
func (RouteType) Descriptor() protoreflect.EnumDescriptor {
|
||||
return file_extensions_v1alpha1_wasmplugin_proto_enumTypes[0].Descriptor()
|
||||
}
|
||||
|
||||
func (RouteType) Type() protoreflect.EnumType {
|
||||
return &file_extensions_v1alpha1_wasmplugin_proto_enumTypes[0]
|
||||
}
|
||||
|
||||
func (x RouteType) Number() protoreflect.EnumNumber {
|
||||
return protoreflect.EnumNumber(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use RouteType.Descriptor instead.
|
||||
func (RouteType) EnumDescriptor() ([]byte, []int) {
|
||||
return file_extensions_v1alpha1_wasmplugin_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
// The phase in the filter chain where the plugin will be injected.
|
||||
type PluginPhase int32
|
||||
|
||||
@@ -134,11 +84,11 @@ func (x PluginPhase) String() string {
|
||||
}
|
||||
|
||||
func (PluginPhase) Descriptor() protoreflect.EnumDescriptor {
|
||||
return file_extensions_v1alpha1_wasmplugin_proto_enumTypes[1].Descriptor()
|
||||
return file_extensions_v1alpha1_wasmplugin_proto_enumTypes[0].Descriptor()
|
||||
}
|
||||
|
||||
func (PluginPhase) Type() protoreflect.EnumType {
|
||||
return &file_extensions_v1alpha1_wasmplugin_proto_enumTypes[1]
|
||||
return &file_extensions_v1alpha1_wasmplugin_proto_enumTypes[0]
|
||||
}
|
||||
|
||||
func (x PluginPhase) Number() protoreflect.EnumNumber {
|
||||
@@ -147,7 +97,7 @@ func (x PluginPhase) Number() protoreflect.EnumNumber {
|
||||
|
||||
// Deprecated: Use PluginPhase.Descriptor instead.
|
||||
func (PluginPhase) EnumDescriptor() ([]byte, []int) {
|
||||
return file_extensions_v1alpha1_wasmplugin_proto_rawDescGZIP(), []int{1}
|
||||
return file_extensions_v1alpha1_wasmplugin_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
// The pull behaviour to be applied when fetching an OCI image,
|
||||
@@ -196,11 +146,11 @@ func (x PullPolicy) String() string {
|
||||
}
|
||||
|
||||
func (PullPolicy) Descriptor() protoreflect.EnumDescriptor {
|
||||
return file_extensions_v1alpha1_wasmplugin_proto_enumTypes[2].Descriptor()
|
||||
return file_extensions_v1alpha1_wasmplugin_proto_enumTypes[1].Descriptor()
|
||||
}
|
||||
|
||||
func (PullPolicy) Type() protoreflect.EnumType {
|
||||
return &file_extensions_v1alpha1_wasmplugin_proto_enumTypes[2]
|
||||
return &file_extensions_v1alpha1_wasmplugin_proto_enumTypes[1]
|
||||
}
|
||||
|
||||
func (x PullPolicy) Number() protoreflect.EnumNumber {
|
||||
@@ -209,7 +159,7 @@ func (x PullPolicy) Number() protoreflect.EnumNumber {
|
||||
|
||||
// Deprecated: Use PullPolicy.Descriptor instead.
|
||||
func (PullPolicy) EnumDescriptor() ([]byte, []int) {
|
||||
return file_extensions_v1alpha1_wasmplugin_proto_rawDescGZIP(), []int{2}
|
||||
return file_extensions_v1alpha1_wasmplugin_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
type EnvValueSource int32
|
||||
@@ -244,11 +194,11 @@ func (x EnvValueSource) String() string {
|
||||
}
|
||||
|
||||
func (EnvValueSource) Descriptor() protoreflect.EnumDescriptor {
|
||||
return file_extensions_v1alpha1_wasmplugin_proto_enumTypes[3].Descriptor()
|
||||
return file_extensions_v1alpha1_wasmplugin_proto_enumTypes[2].Descriptor()
|
||||
}
|
||||
|
||||
func (EnvValueSource) Type() protoreflect.EnumType {
|
||||
return &file_extensions_v1alpha1_wasmplugin_proto_enumTypes[3]
|
||||
return &file_extensions_v1alpha1_wasmplugin_proto_enumTypes[2]
|
||||
}
|
||||
|
||||
func (x EnvValueSource) Number() protoreflect.EnumNumber {
|
||||
@@ -257,7 +207,7 @@ func (x EnvValueSource) Number() protoreflect.EnumNumber {
|
||||
|
||||
// Deprecated: Use EnvValueSource.Descriptor instead.
|
||||
func (EnvValueSource) EnumDescriptor() ([]byte, []int) {
|
||||
return file_extensions_v1alpha1_wasmplugin_proto_rawDescGZIP(), []int{3}
|
||||
return file_extensions_v1alpha1_wasmplugin_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
type FailStrategy int32
|
||||
@@ -296,11 +246,11 @@ func (x FailStrategy) String() string {
|
||||
}
|
||||
|
||||
func (FailStrategy) Descriptor() protoreflect.EnumDescriptor {
|
||||
return file_extensions_v1alpha1_wasmplugin_proto_enumTypes[4].Descriptor()
|
||||
return file_extensions_v1alpha1_wasmplugin_proto_enumTypes[3].Descriptor()
|
||||
}
|
||||
|
||||
func (FailStrategy) Type() protoreflect.EnumType {
|
||||
return &file_extensions_v1alpha1_wasmplugin_proto_enumTypes[4]
|
||||
return &file_extensions_v1alpha1_wasmplugin_proto_enumTypes[3]
|
||||
}
|
||||
|
||||
func (x FailStrategy) Number() protoreflect.EnumNumber {
|
||||
@@ -309,7 +259,7 @@ func (x FailStrategy) Number() protoreflect.EnumNumber {
|
||||
|
||||
// Deprecated: Use FailStrategy.Descriptor instead.
|
||||
func (FailStrategy) EnumDescriptor() ([]byte, []int) {
|
||||
return file_extensions_v1alpha1_wasmplugin_proto_rawDescGZIP(), []int{4}
|
||||
return file_extensions_v1alpha1_wasmplugin_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
// <!-- crd generation tags
|
||||
@@ -535,8 +485,6 @@ type MatchRule struct {
|
||||
Config *_struct.Struct `protobuf:"bytes,3,opt,name=config,proto3" json:"config,omitempty"`
|
||||
ConfigDisable *wrappers.BoolValue `protobuf:"bytes,4,opt,name=config_disable,json=configDisable,proto3" json:"config_disable,omitempty"`
|
||||
Service []string `protobuf:"bytes,5,rep,name=service,proto3" json:"service,omitempty"`
|
||||
// Route type for this match rule, defaults to HTTP
|
||||
RouteType RouteType `protobuf:"varint,6,opt,name=route_type,json=routeType,proto3,enum=higress.extensions.v1alpha1.RouteType" json:"route_type,omitempty"`
|
||||
}
|
||||
|
||||
func (x *MatchRule) Reset() {
|
||||
@@ -606,13 +554,6 @@ func (x *MatchRule) GetService() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *MatchRule) GetRouteType() RouteType {
|
||||
if x != nil {
|
||||
return x.RouteType
|
||||
}
|
||||
return RouteType_HTTP
|
||||
}
|
||||
|
||||
// Configuration for a Wasm VM.
|
||||
// more details can be found [here](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/wasm/v3/wasm.proto#extensions-wasm-v3-vmconfig).
|
||||
type VmConfig struct {
|
||||
@@ -795,7 +736,7 @@ var file_extensions_v1alpha1_wasmplugin_proto_rawDesc = []byte{
|
||||
0x73, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x67, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f,
|
||||
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f,
|
||||
0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x14, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74,
|
||||
0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x22, 0x92, 0x02,
|
||||
0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x22, 0xcb, 0x01,
|
||||
0x0a, 0x09, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x69,
|
||||
0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x69, 0x6e,
|
||||
0x67, 0x72, 0x65, 0x73, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18,
|
||||
@@ -808,44 +749,37 @@ var file_extensions_v1alpha1_wasmplugin_proto_rawDesc = []byte{
|
||||
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c,
|
||||
0x75, 0x65, 0x52, 0x0d, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c,
|
||||
0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x05, 0x20, 0x03,
|
||||
0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x45, 0x0a, 0x0a, 0x72,
|
||||
0x6f, 0x75, 0x74, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32,
|
||||
0x26, 0x2e, 0x68, 0x69, 0x67, 0x72, 0x65, 0x73, 0x73, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73,
|
||||
0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x52, 0x6f,
|
||||
0x75, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x52, 0x09, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x54, 0x79,
|
||||
0x70, 0x65, 0x22, 0x41, 0x0a, 0x08, 0x56, 0x6d, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x35,
|
||||
0x0a, 0x03, 0x65, 0x6e, 0x76, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x68, 0x69,
|
||||
0x67, 0x72, 0x65, 0x73, 0x73, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73,
|
||||
0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x45, 0x6e, 0x76, 0x56, 0x61, 0x72,
|
||||
0x52, 0x03, 0x65, 0x6e, 0x76, 0x22, 0x7e, 0x0a, 0x06, 0x45, 0x6e, 0x76, 0x56, 0x61, 0x72, 0x12,
|
||||
0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e,
|
||||
0x61, 0x6d, 0x65, 0x12, 0x4a, 0x0a, 0x0a, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x66, 0x72, 0x6f,
|
||||
0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2b, 0x2e, 0x68, 0x69, 0x67, 0x72, 0x65, 0x73,
|
||||
0x73, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x61,
|
||||
0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x45, 0x6e, 0x76, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x53, 0x6f,
|
||||
0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x46, 0x72, 0x6f, 0x6d, 0x12,
|
||||
0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05,
|
||||
0x76, 0x61, 0x6c, 0x75, 0x65, 0x2a, 0x1f, 0x0a, 0x09, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x54, 0x79,
|
||||
0x70, 0x65, 0x12, 0x08, 0x0a, 0x04, 0x48, 0x54, 0x54, 0x50, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04,
|
||||
0x47, 0x52, 0x50, 0x43, 0x10, 0x01, 0x2a, 0x45, 0x0a, 0x0b, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e,
|
||||
0x50, 0x68, 0x61, 0x73, 0x65, 0x12, 0x15, 0x0a, 0x11, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49,
|
||||
0x46, 0x49, 0x45, 0x44, 0x5f, 0x50, 0x48, 0x41, 0x53, 0x45, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05,
|
||||
0x41, 0x55, 0x54, 0x48, 0x4e, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x41, 0x55, 0x54, 0x48, 0x5a,
|
||||
0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x53, 0x54, 0x41, 0x54, 0x53, 0x10, 0x03, 0x2a, 0x42, 0x0a,
|
||||
0x0a, 0x50, 0x75, 0x6c, 0x6c, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x16, 0x0a, 0x12, 0x55,
|
||||
0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x5f, 0x50, 0x4f, 0x4c, 0x49, 0x43,
|
||||
0x59, 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x49, 0x66, 0x4e, 0x6f, 0x74, 0x50, 0x72, 0x65, 0x73,
|
||||
0x65, 0x6e, 0x74, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x6c, 0x77, 0x61, 0x79, 0x73, 0x10,
|
||||
0x02, 0x2a, 0x26, 0x0a, 0x0e, 0x45, 0x6e, 0x76, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x53, 0x6f, 0x75,
|
||||
0x72, 0x63, 0x65, 0x12, 0x0a, 0x0a, 0x06, 0x49, 0x4e, 0x4c, 0x49, 0x4e, 0x45, 0x10, 0x00, 0x12,
|
||||
0x08, 0x0a, 0x04, 0x48, 0x4f, 0x53, 0x54, 0x10, 0x01, 0x2a, 0x2d, 0x0a, 0x0c, 0x46, 0x61, 0x69,
|
||||
0x6c, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x0e, 0x0a, 0x0a, 0x46, 0x41, 0x49,
|
||||
0x4c, 0x5f, 0x43, 0x4c, 0x4f, 0x53, 0x45, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x46, 0x41, 0x49,
|
||||
0x4c, 0x5f, 0x4f, 0x50, 0x45, 0x4e, 0x10, 0x01, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68,
|
||||
0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x61, 0x6c, 0x69, 0x62, 0x61, 0x62, 0x61, 0x2f, 0x68,
|
||||
0x69, 0x67, 0x72, 0x65, 0x73, 0x73, 0x2f, 0x76, 0x32, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x65, 0x78,
|
||||
0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61,
|
||||
0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x22, 0x41, 0x0a, 0x08, 0x56,
|
||||
0x6d, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x35, 0x0a, 0x03, 0x65, 0x6e, 0x76, 0x18, 0x01,
|
||||
0x20, 0x03, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x68, 0x69, 0x67, 0x72, 0x65, 0x73, 0x73, 0x2e, 0x65,
|
||||
0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68,
|
||||
0x61, 0x31, 0x2e, 0x45, 0x6e, 0x76, 0x56, 0x61, 0x72, 0x52, 0x03, 0x65, 0x6e, 0x76, 0x22, 0x7e,
|
||||
0x0a, 0x06, 0x45, 0x6e, 0x76, 0x56, 0x61, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65,
|
||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x4a, 0x0a, 0x0a,
|
||||
0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x66, 0x72, 0x6f, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e,
|
||||
0x32, 0x2b, 0x2e, 0x68, 0x69, 0x67, 0x72, 0x65, 0x73, 0x73, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e,
|
||||
0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x45,
|
||||
0x6e, 0x76, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x76,
|
||||
0x61, 0x6c, 0x75, 0x65, 0x46, 0x72, 0x6f, 0x6d, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75,
|
||||
0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x2a, 0x45,
|
||||
0x0a, 0x0b, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x50, 0x68, 0x61, 0x73, 0x65, 0x12, 0x15, 0x0a,
|
||||
0x11, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x5f, 0x50, 0x48, 0x41,
|
||||
0x53, 0x45, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x41, 0x55, 0x54, 0x48, 0x4e, 0x10, 0x01, 0x12,
|
||||
0x09, 0x0a, 0x05, 0x41, 0x55, 0x54, 0x48, 0x5a, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x53, 0x54,
|
||||
0x41, 0x54, 0x53, 0x10, 0x03, 0x2a, 0x42, 0x0a, 0x0a, 0x50, 0x75, 0x6c, 0x6c, 0x50, 0x6f, 0x6c,
|
||||
0x69, 0x63, 0x79, 0x12, 0x16, 0x0a, 0x12, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49,
|
||||
0x45, 0x44, 0x5f, 0x50, 0x4f, 0x4c, 0x49, 0x43, 0x59, 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x49,
|
||||
0x66, 0x4e, 0x6f, 0x74, 0x50, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x10, 0x01, 0x12, 0x0a, 0x0a,
|
||||
0x06, 0x41, 0x6c, 0x77, 0x61, 0x79, 0x73, 0x10, 0x02, 0x2a, 0x26, 0x0a, 0x0e, 0x45, 0x6e, 0x76,
|
||||
0x56, 0x61, 0x6c, 0x75, 0x65, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x0a, 0x0a, 0x06, 0x49,
|
||||
0x4e, 0x4c, 0x49, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x48, 0x4f, 0x53, 0x54, 0x10,
|
||||
0x01, 0x2a, 0x2d, 0x0a, 0x0c, 0x46, 0x61, 0x69, 0x6c, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67,
|
||||
0x79, 0x12, 0x0e, 0x0a, 0x0a, 0x46, 0x41, 0x49, 0x4c, 0x5f, 0x43, 0x4c, 0x4f, 0x53, 0x45, 0x10,
|
||||
0x00, 0x12, 0x0d, 0x0a, 0x09, 0x46, 0x41, 0x49, 0x4c, 0x5f, 0x4f, 0x50, 0x45, 0x4e, 0x10, 0x01,
|
||||
0x42, 0x34, 0x5a, 0x32, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x61,
|
||||
0x6c, 0x69, 0x62, 0x61, 0x62, 0x61, 0x2f, 0x68, 0x69, 0x67, 0x72, 0x65, 0x73, 0x73, 0x2f, 0x61,
|
||||
0x70, 0x69, 0x2f, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x76, 0x31,
|
||||
0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -860,42 +794,40 @@ func file_extensions_v1alpha1_wasmplugin_proto_rawDescGZIP() []byte {
|
||||
return file_extensions_v1alpha1_wasmplugin_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_extensions_v1alpha1_wasmplugin_proto_enumTypes = make([]protoimpl.EnumInfo, 5)
|
||||
var file_extensions_v1alpha1_wasmplugin_proto_enumTypes = make([]protoimpl.EnumInfo, 4)
|
||||
var file_extensions_v1alpha1_wasmplugin_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
|
||||
var file_extensions_v1alpha1_wasmplugin_proto_goTypes = []interface{}{
|
||||
(RouteType)(0), // 0: higress.extensions.v1alpha1.RouteType
|
||||
(PluginPhase)(0), // 1: higress.extensions.v1alpha1.PluginPhase
|
||||
(PullPolicy)(0), // 2: higress.extensions.v1alpha1.PullPolicy
|
||||
(EnvValueSource)(0), // 3: higress.extensions.v1alpha1.EnvValueSource
|
||||
(FailStrategy)(0), // 4: higress.extensions.v1alpha1.FailStrategy
|
||||
(*WasmPlugin)(nil), // 5: higress.extensions.v1alpha1.WasmPlugin
|
||||
(*MatchRule)(nil), // 6: higress.extensions.v1alpha1.MatchRule
|
||||
(*VmConfig)(nil), // 7: higress.extensions.v1alpha1.VmConfig
|
||||
(*EnvVar)(nil), // 8: higress.extensions.v1alpha1.EnvVar
|
||||
(*_struct.Struct)(nil), // 9: google.protobuf.Struct
|
||||
(*wrappers.Int32Value)(nil), // 10: google.protobuf.Int32Value
|
||||
(*wrappers.BoolValue)(nil), // 11: google.protobuf.BoolValue
|
||||
(PluginPhase)(0), // 0: higress.extensions.v1alpha1.PluginPhase
|
||||
(PullPolicy)(0), // 1: higress.extensions.v1alpha1.PullPolicy
|
||||
(EnvValueSource)(0), // 2: higress.extensions.v1alpha1.EnvValueSource
|
||||
(FailStrategy)(0), // 3: higress.extensions.v1alpha1.FailStrategy
|
||||
(*WasmPlugin)(nil), // 4: higress.extensions.v1alpha1.WasmPlugin
|
||||
(*MatchRule)(nil), // 5: higress.extensions.v1alpha1.MatchRule
|
||||
(*VmConfig)(nil), // 6: higress.extensions.v1alpha1.VmConfig
|
||||
(*EnvVar)(nil), // 7: higress.extensions.v1alpha1.EnvVar
|
||||
(*_struct.Struct)(nil), // 8: google.protobuf.Struct
|
||||
(*wrappers.Int32Value)(nil), // 9: google.protobuf.Int32Value
|
||||
(*wrappers.BoolValue)(nil), // 10: google.protobuf.BoolValue
|
||||
}
|
||||
var file_extensions_v1alpha1_wasmplugin_proto_depIdxs = []int32{
|
||||
2, // 0: higress.extensions.v1alpha1.WasmPlugin.image_pull_policy:type_name -> higress.extensions.v1alpha1.PullPolicy
|
||||
9, // 1: higress.extensions.v1alpha1.WasmPlugin.plugin_config:type_name -> google.protobuf.Struct
|
||||
1, // 2: higress.extensions.v1alpha1.WasmPlugin.phase:type_name -> higress.extensions.v1alpha1.PluginPhase
|
||||
10, // 3: higress.extensions.v1alpha1.WasmPlugin.priority:type_name -> google.protobuf.Int32Value
|
||||
4, // 4: higress.extensions.v1alpha1.WasmPlugin.fail_strategy:type_name -> higress.extensions.v1alpha1.FailStrategy
|
||||
7, // 5: higress.extensions.v1alpha1.WasmPlugin.vm_config:type_name -> higress.extensions.v1alpha1.VmConfig
|
||||
9, // 6: higress.extensions.v1alpha1.WasmPlugin.default_config:type_name -> google.protobuf.Struct
|
||||
6, // 7: higress.extensions.v1alpha1.WasmPlugin.match_rules:type_name -> higress.extensions.v1alpha1.MatchRule
|
||||
11, // 8: higress.extensions.v1alpha1.WasmPlugin.default_config_disable:type_name -> google.protobuf.BoolValue
|
||||
9, // 9: higress.extensions.v1alpha1.MatchRule.config:type_name -> google.protobuf.Struct
|
||||
11, // 10: higress.extensions.v1alpha1.MatchRule.config_disable:type_name -> google.protobuf.BoolValue
|
||||
0, // 11: higress.extensions.v1alpha1.MatchRule.route_type:type_name -> higress.extensions.v1alpha1.RouteType
|
||||
8, // 12: higress.extensions.v1alpha1.VmConfig.env:type_name -> higress.extensions.v1alpha1.EnvVar
|
||||
3, // 13: higress.extensions.v1alpha1.EnvVar.value_from:type_name -> higress.extensions.v1alpha1.EnvValueSource
|
||||
14, // [14:14] is the sub-list for method output_type
|
||||
14, // [14:14] is the sub-list for method input_type
|
||||
14, // [14:14] is the sub-list for extension type_name
|
||||
14, // [14:14] is the sub-list for extension extendee
|
||||
0, // [0:14] is the sub-list for field type_name
|
||||
1, // 0: higress.extensions.v1alpha1.WasmPlugin.image_pull_policy:type_name -> higress.extensions.v1alpha1.PullPolicy
|
||||
8, // 1: higress.extensions.v1alpha1.WasmPlugin.plugin_config:type_name -> google.protobuf.Struct
|
||||
0, // 2: higress.extensions.v1alpha1.WasmPlugin.phase:type_name -> higress.extensions.v1alpha1.PluginPhase
|
||||
9, // 3: higress.extensions.v1alpha1.WasmPlugin.priority:type_name -> google.protobuf.Int32Value
|
||||
3, // 4: higress.extensions.v1alpha1.WasmPlugin.fail_strategy:type_name -> higress.extensions.v1alpha1.FailStrategy
|
||||
6, // 5: higress.extensions.v1alpha1.WasmPlugin.vm_config:type_name -> higress.extensions.v1alpha1.VmConfig
|
||||
8, // 6: higress.extensions.v1alpha1.WasmPlugin.default_config:type_name -> google.protobuf.Struct
|
||||
5, // 7: higress.extensions.v1alpha1.WasmPlugin.match_rules:type_name -> higress.extensions.v1alpha1.MatchRule
|
||||
10, // 8: higress.extensions.v1alpha1.WasmPlugin.default_config_disable:type_name -> google.protobuf.BoolValue
|
||||
8, // 9: higress.extensions.v1alpha1.MatchRule.config:type_name -> google.protobuf.Struct
|
||||
10, // 10: higress.extensions.v1alpha1.MatchRule.config_disable:type_name -> google.protobuf.BoolValue
|
||||
7, // 11: higress.extensions.v1alpha1.VmConfig.env:type_name -> higress.extensions.v1alpha1.EnvVar
|
||||
2, // 12: higress.extensions.v1alpha1.EnvVar.value_from:type_name -> higress.extensions.v1alpha1.EnvValueSource
|
||||
13, // [13:13] is the sub-list for method output_type
|
||||
13, // [13:13] is the sub-list for method input_type
|
||||
13, // [13:13] is the sub-list for extension type_name
|
||||
13, // [13:13] is the sub-list for extension extendee
|
||||
0, // [0:13] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_extensions_v1alpha1_wasmplugin_proto_init() }
|
||||
@@ -958,7 +890,7 @@ func file_extensions_v1alpha1_wasmplugin_proto_init() {
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_extensions_v1alpha1_wasmplugin_proto_rawDesc,
|
||||
NumEnums: 5,
|
||||
NumEnums: 4,
|
||||
NumMessages: 4,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
|
||||
@@ -122,18 +122,6 @@ message MatchRule {
|
||||
google.protobuf.Struct config = 3;
|
||||
google.protobuf.BoolValue config_disable = 4;
|
||||
repeated string service = 5;
|
||||
// Route type for this match rule, defaults to HTTP
|
||||
RouteType route_type = 6;
|
||||
}
|
||||
|
||||
// Route type for matching rules.
|
||||
// Extended by Higress
|
||||
enum RouteType {
|
||||
// HTTP route (default)
|
||||
HTTP = 0;
|
||||
|
||||
// GRPC route
|
||||
GRPC = 1;
|
||||
}
|
||||
|
||||
// The phase in the filter chain where the plugin will be injected.
|
||||
|
||||
@@ -71,11 +71,6 @@ spec:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
routeType:
|
||||
enum:
|
||||
- HTTP
|
||||
- GRPC
|
||||
type: string
|
||||
service:
|
||||
items:
|
||||
type: string
|
||||
|
||||
@@ -504,11 +504,11 @@ var file_networking_v1_http_2_rpc_proto_rawDesc = []byte{
|
||||
0x69, 0x72, 0x65, 0x42, 0x6f, 0x64, 0x79, 0x12, 0x22, 0x0a, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d,
|
||||
0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x03, 0xe0, 0x41, 0x02,
|
||||
0x52, 0x09, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x54, 0x79, 0x70, 0x65, 0x22, 0x0d, 0x0a, 0x0b, 0x47,
|
||||
0x72, 0x70, 0x63, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x69,
|
||||
0x72, 0x70, 0x63, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x69,
|
||||
0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x61, 0x6c, 0x69, 0x62, 0x61, 0x62, 0x61,
|
||||
0x2f, 0x68, 0x69, 0x67, 0x72, 0x65, 0x73, 0x73, 0x2f, 0x76, 0x32, 0x2f, 0x61, 0x70, 0x69, 0x2f,
|
||||
0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70,
|
||||
0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
0x2f, 0x68, 0x69, 0x67, 0x72, 0x65, 0x73, 0x73, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x6e, 0x65, 0x74,
|
||||
0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
|
||||
0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
@@ -749,10 +749,10 @@ var file_networking_v1_mcp_bridge_proto_rawDesc = []byte{
|
||||
0x72, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79,
|
||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76,
|
||||
0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75,
|
||||
0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e,
|
||||
0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e,
|
||||
0x63, 0x6f, 0x6d, 0x2f, 0x61, 0x6c, 0x69, 0x62, 0x61, 0x62, 0x61, 0x2f, 0x68, 0x69, 0x67, 0x72,
|
||||
0x65, 0x73, 0x73, 0x2f, 0x76, 0x32, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x6e, 0x65, 0x74, 0x77, 0x6f,
|
||||
0x72, 0x6b, 0x69, 0x6e, 0x67, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
0x65, 0x73, 0x73, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x69,
|
||||
0x6e, 0x67, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
Submodule envoy/envoy updated: 3fe314c698...384e5aab43
Submodule envoy/go-control-plane updated: 90eca02281...0ab56e249a
288
go.mod
288
go.mod
@@ -1,6 +1,8 @@
|
||||
module github.com/alibaba/higress/v2
|
||||
|
||||
go 1.24.4
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.23.7
|
||||
|
||||
replace github.com/spf13/viper => github.com/istio/viper v1.3.3-0.20190515210538-2789fed3109c
|
||||
|
||||
@@ -20,61 +22,57 @@ require (
|
||||
github.com/caddyserver/certmagic v0.21.3
|
||||
github.com/dubbogo/go-zookeeper v1.0.4-0.20211212162352-f9d2183d89d5
|
||||
github.com/dubbogo/gost v1.13.1
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.35.0
|
||||
github.com/go-errors/errors v1.5.1
|
||||
github.com/envoyproxy/go-control-plane v0.11.2-0.20230725211550-11bfe846bcd4
|
||||
github.com/go-errors/errors v1.4.2
|
||||
github.com/gogo/protobuf v1.3.2
|
||||
github.com/golang/protobuf v1.5.4
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/golang/protobuf v1.5.3
|
||||
github.com/google/go-cmp v0.6.0
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
|
||||
github.com/hashicorp/consul/api v1.32.0
|
||||
github.com/hashicorp/consul/api v1.21.0
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/hudl/fargo v1.4.0
|
||||
github.com/mholt/acmez v1.2.0
|
||||
github.com/nacos-group/nacos-sdk-go v1.0.8
|
||||
github.com/nacos-group/nacos-sdk-go/v2 v2.3.2
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/spf13/pflag v1.0.7
|
||||
github.com/stretchr/testify v1.11.1
|
||||
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.10.0
|
||||
github.com/tidwall/gjson v1.17.0
|
||||
go.uber.org/atomic v1.11.0
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/net v0.44.0
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4
|
||||
google.golang.org/grpc v1.76.0
|
||||
google.golang.org/protobuf v1.36.10
|
||||
istio.io/api v1.27.1-0.20250820125923-f5a5d3a605a9
|
||||
istio.io/client-go v1.27.1-0.20250820130622-12f6d11feb40
|
||||
golang.org/x/net v0.33.0
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230920204549-e6e6cdab5c13
|
||||
google.golang.org/grpc v1.59.0
|
||||
google.golang.org/protobuf v1.33.0
|
||||
istio.io/api v1.19.5-0.20231206014255-f55a2b1e931e
|
||||
istio.io/istio v0.0.0
|
||||
istio.io/pkg v0.0.0-20250718200944-0aab346caa39
|
||||
k8s.io/api v0.34.1
|
||||
k8s.io/apiextensions-apiserver v0.34.1
|
||||
k8s.io/apimachinery v0.34.1
|
||||
k8s.io/cli-runtime v0.33.3
|
||||
k8s.io/client-go v0.34.1
|
||||
istio.io/pkg v0.0.0-20231221211216-7635388a563e
|
||||
k8s.io/api v0.28.3
|
||||
k8s.io/apiextensions-apiserver v0.28.3
|
||||
k8s.io/apimachinery v0.28.3
|
||||
k8s.io/cli-runtime v0.28.0
|
||||
k8s.io/client-go v0.28.3
|
||||
knative.dev/networking v0.0.0-20220302134042-e8b2eb995165
|
||||
knative.dev/pkg v0.0.0-20220301181942-2fdd5f232e77
|
||||
sigs.k8s.io/controller-runtime v0.22.3
|
||||
sigs.k8s.io/gateway-api v1.4.0
|
||||
sigs.k8s.io/gateway-api-inference-extension v1.1.0
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.7.0
|
||||
sigs.k8s.io/yaml v1.6.0
|
||||
sigs.k8s.io/controller-runtime v0.16.3
|
||||
sigs.k8s.io/gateway-api v0.8.0
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.3.0
|
||||
sigs.k8s.io/yaml v1.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.24.0 // indirect
|
||||
cloud.google.com/go v0.120.0 // indirect
|
||||
cloud.google.com/go/auth v0.16.5 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.8.4 // indirect
|
||||
cloud.google.com/go/logging v1.13.0 // indirect
|
||||
cloud.google.com/go/longrunning v0.6.7 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||
cloud.google.com/go v0.110.8 // indirect
|
||||
cloud.google.com/go/compute v1.23.0 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
cloud.google.com/go/logging v1.8.1 // indirect
|
||||
cloud.google.com/go/longrunning v0.5.1 // indirect
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
||||
github.com/alecholmes/xfccparser v0.4.0 // indirect
|
||||
github.com/alecthomas/participle/v2 v2.1.4 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.2.1 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
|
||||
github.com/alecholmes/xfccparser v0.1.0 // indirect
|
||||
github.com/alecthomas/participle v0.4.1 // indirect
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 // indirect
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
|
||||
github.com/alibabacloud-go/darabonba-array v0.1.0 // indirect
|
||||
@@ -96,189 +94,177 @@ require (
|
||||
github.com/aliyun/alibabacloud-dkms-transfer-go-sdk v0.1.8 // indirect
|
||||
github.com/aliyun/aliyun-secretsmanager-client-go v1.1.5 // indirect
|
||||
github.com/aliyun/credentials-go v1.4.3 // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
||||
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect
|
||||
github.com/armon/go-metrics v0.4.1 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
|
||||
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/clbanning/mxj v1.8.4 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.5.5 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
|
||||
github.com/coreos/go-oidc/v3 v3.14.1 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect
|
||||
github.com/coreos/go-oidc/v3 v3.6.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/deckarep/golang-set v1.7.1 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
github.com/docker/cli v28.1.1+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.9.3 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
|
||||
github.com/envoyproxy/go-control-plane v0.13.4 // indirect
|
||||
github.com/envoyproxy/go-control-plane/contrib v0.0.0-20251016030003-90eca0228178 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
||||
github.com/docker/cli v24.0.7+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.2+incompatible // indirect
|
||||
github.com/docker/docker v24.0.7+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.7.0 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.0.2 // indirect
|
||||
github.com/evanphx/json-patch v5.7.0+incompatible // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.7.0 // indirect
|
||||
github.com/fatih/color v1.15.0 // indirect
|
||||
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.2 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
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.21.2 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.1 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // 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.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
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/cel-go v0.26.0 // indirect
|
||||
github.com/google/gnostic-models v0.7.0 // indirect
|
||||
github.com/google/go-containerregistry v0.20.3 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/google/btree v1.1.2 // indirect
|
||||
github.com/google/cel-go v0.16.1 // indirect
|
||||
github.com/google/gnostic-models v0.6.8 // indirect
|
||||
github.com/google/go-containerregistry v0.15.2 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/s2a-go v0.1.4 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
|
||||
github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 // indirect
|
||||
github.com/google/uuid v1.3.1 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.5 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
|
||||
github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd // indirect
|
||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-hclog v1.6.3 // indirect
|
||||
github.com/hashicorp/go-hclog v1.5.0 // indirect
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
|
||||
github.com/hashicorp/go-version v1.7.0 // indirect
|
||||
github.com/hashicorp/go-version v1.6.0 // indirect
|
||||
github.com/hashicorp/golang-lru v0.6.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.4 // indirect
|
||||
github.com/hashicorp/serf v0.10.1 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/huandu/xstrings v1.4.0 // indirect
|
||||
github.com/imdario/mergo v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/compress v1.16.5 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.3 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
|
||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||
github.com/lestrrat-go/iter v1.0.2 // indirect
|
||||
github.com/lestrrat-go/jwx v1.2.31 // indirect
|
||||
github.com/lestrrat-go/jwx v1.2.26 // indirect
|
||||
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||
github.com/lestrrat/go-file-rotatelogs v0.0.0-20180223000712-d3151e2a480f // indirect
|
||||
github.com/lestrrat/go-strftime v0.0.0-20180220042222-ba3bf9c1d042 // indirect
|
||||
github.com/libdns/libdns v0.2.2 // indirect
|
||||
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/miekg/dns v1.1.68 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
|
||||
github.com/miekg/dns v1.1.59 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/moby/spdystream v0.5.0 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/moby/spdystream v0.2.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/openshift/api v0.0.0-20250507150912-7318813e48da // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0-rc5 // indirect
|
||||
github.com/openshift/api v0.0.0-20230720094506-afcbe27aec7c // indirect
|
||||
github.com/orcaman/concurrent-map v0.0.0-20210501183033-44dafcb38ecc // indirect
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240409071808-615f978279ca // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.1 // indirect
|
||||
github.com/prometheus/procfs v0.17.0 // indirect
|
||||
github.com/prometheus/prometheus v0.307.1 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/prometheus/client_golang v1.17.0 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.45.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/prometheus/prometheus v0.45.0 // indirect
|
||||
github.com/sergi/go-diff v1.2.0 // indirect
|
||||
github.com/shopspring/decimal v1.3.1 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/spf13/cast v1.8.0 // indirect
|
||||
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.2 // indirect
|
||||
github.com/tetratelabs/wazero v1.9.0 // 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
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/toolkits/concurrent v0.0.0-20150624120057-a4371d70e3e3 // indirect
|
||||
github.com/vbatts/tar-split v0.12.1 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/vbatts/tar-split v0.11.3 // indirect
|
||||
github.com/xlab/treeprint v1.2.0 // indirect
|
||||
github.com/yl2chen/cidranger v1.0.2 // indirect
|
||||
github.com/zeebo/blake3 v0.2.3 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.57.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
|
||||
go.opentelemetry.io/otel v1.17.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.39.1-0.20230714155235-03b8c47770f2 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.17.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.16.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v0.39.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.17.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
|
||||
go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.42.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250808145144-a408d31f581a // indirect
|
||||
golang.org/x/mod v0.28.0 // indirect
|
||||
golang.org/x/oauth2 v0.31.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/term v0.35.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
golang.org/x/time v0.13.0 // indirect
|
||||
golang.org/x/tools v0.37.0 // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
|
||||
google.golang.org/api v0.250.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
||||
golang.org/x/crypto v0.31.0 // 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
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/term v0.27.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
|
||||
google.golang.org/api v0.132.0 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/genproto v0.0.0-20231002182017-d307bd883b97 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231009173412-8bfb1ae86b6c // indirect
|
||||
gopkg.in/gcfg.v1 v1.2.3 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/apiserver v0.34.1 // indirect
|
||||
k8s.io/component-base v0.34.1 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250814151709-d7b6acb124c3 // indirect
|
||||
k8s.io/kubectl v0.33.3 // indirect
|
||||
k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d // indirect
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.32.1 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||
sigs.k8s.io/kustomize/api v0.19.0 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.19.0 // indirect
|
||||
sigs.k8s.io/mcs-api v0.1.1-0.20240624222831-d7001fe1d21c // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
|
||||
gotest.tools/v3 v3.5.0 // indirect
|
||||
istio.io/client-go v1.19.5-0.20231206015206-8cdf6a3b3cfd // indirect
|
||||
k8s.io/apiserver v0.28.3 // indirect
|
||||
k8s.io/component-base v0.28.3 // indirect
|
||||
k8s.io/klog/v2 v2.100.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect
|
||||
k8s.io/kubectl v0.28.0 // indirect
|
||||
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
||||
sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.14.3 // indirect
|
||||
sigs.k8s.io/mcs-api v0.1.0 // indirect
|
||||
)
|
||||
|
||||
replace istio.io/api => ./external/api
|
||||
|
||||
replace github.com/envoyproxy/go-control-plane => ./external/go-control-plane
|
||||
|
||||
replace github.com/envoyproxy/go-control-plane/contrib => ./external/go-control-plane/contrib
|
||||
|
||||
replace github.com/envoyproxy/go-control-plane/envoy => ./external/go-control-plane/envoy
|
||||
|
||||
replace istio.io/pkg => ./external/pkg
|
||||
|
||||
replace istio.io/client-go => ./external/client-go
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
appVersion: 2.1.9
|
||||
appVersion: 2.1.11
|
||||
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
|
||||
version: 2.1.11
|
||||
|
||||
@@ -38,14 +38,6 @@ rules:
|
||||
resources: ["*"]
|
||||
verbs: ["get", "watch", "list", "create", "update", "delete", "patch"]
|
||||
|
||||
# Gateway api inference extension
|
||||
- apiGroups: ["inference.networking.k8s.io"]
|
||||
resources: ["*"]
|
||||
verbs: ["get", "watch", "list", "create", "update", "delete", "patch"]
|
||||
- apiGroups: ["inference.networking.x-k8s.io"]
|
||||
resources: ["*"]
|
||||
verbs: ["get", "watch", "list", "create", "update", "delete", "patch"]
|
||||
|
||||
# Needed for multicluster secret reading, possibly ingress certs in the future
|
||||
- apiGroups: [""]
|
||||
resources: ["secrets"]
|
||||
|
||||
@@ -78,10 +78,6 @@ spec:
|
||||
value: "{{ .Values.global.enableGatewayAPI }}"
|
||||
- name: PILOT_ENABLE_ALPHA_GATEWAY_API
|
||||
value: "{{ .Values.global.enableGatewayAPI }}"
|
||||
{{- if .Values.global.enableInferenceExtension }}
|
||||
- name: ENABLE_GATEWAY_API_INFERENCE_EXTENSION
|
||||
value: "true"
|
||||
{{- end }}
|
||||
{{- if .Values.controller.env }}
|
||||
{{- range $key, $val := .Values.controller.env }}
|
||||
- name: {{ $key }}
|
||||
@@ -178,15 +174,7 @@ spec:
|
||||
- name: HOST_RDS_MERGE_SUBSET
|
||||
value: "{{ .Values.global.hostRDSMergeSubset }}"
|
||||
- name: PILOT_FILTER_GATEWAY_CLUSTER_CONFIG
|
||||
{{- if .Values.global.enableInferenceExtension }}
|
||||
value: "false"
|
||||
{{- else }}
|
||||
value: "{{ .Values.global.onlyPushRouteCluster }}"
|
||||
{{- end }}
|
||||
{{- if .Values.global.enableInferenceExtension }}
|
||||
- name: ENABLE_GATEWAY_API_INFERENCE_EXTENSION
|
||||
value: "true"
|
||||
{{- end }}
|
||||
- name: HIGRESS_CONTROLLER_SVC
|
||||
value: "127.0.0.1"
|
||||
- name: HIGRESS_CONTROLLER_PORT
|
||||
|
||||
@@ -44,9 +44,7 @@ global:
|
||||
# -- If true, Higress Controller will monitor istio resources as well
|
||||
enableIstioAPI: true
|
||||
# -- If true, Higress Controller will monitor Gateway API resources as well
|
||||
enableGatewayAPI: true
|
||||
# -- If true, enable Gateway API Inference Extension support
|
||||
enableInferenceExtension: false
|
||||
enableGatewayAPI: false
|
||||
# -- Used to locate istiod.
|
||||
istioNamespace: istio-system
|
||||
# -- enable pod disruption budget for the control plane, which is used to
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
dependencies:
|
||||
- name: higress-core
|
||||
repository: file://../core
|
||||
version: 2.1.9
|
||||
version: 2.1.11
|
||||
- name: higress-console
|
||||
repository: https://higress.io/helm-charts/
|
||||
version: 2.1.9
|
||||
digest: sha256:d696af6726b40219cc16e7cf8de7400101479dfbd8deb3101d7ee736415b9875
|
||||
generated: "2025-11-13T16:33:49.721553+08:00"
|
||||
version: 2.1.11
|
||||
digest: sha256:09058429db5bef8f2d3e5820f3f84457b3dad34f4638878018cd22623fa38f92
|
||||
generated: "2026-02-20T23:47:51.258092+08:00"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
appVersion: 2.1.9
|
||||
appVersion: 2.1.11
|
||||
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
|
||||
version: 2.1.11
|
||||
- name: higress-console
|
||||
repository: "https://higress.io/helm-charts/"
|
||||
version: 2.1.9
|
||||
version: 2.1.11
|
||||
type: application
|
||||
version: 2.1.9
|
||||
version: 2.1.11
|
||||
|
||||
@@ -163,10 +163,9 @@ The command removes all the Kubernetes components associated with the chart and
|
||||
| global.defaultResources | object | `{"requests":{"cpu":"10m"}}` | A minimal set of requested resources to applied to all deployments so that Horizontal Pod Autoscaler will be able to function (if set). Each component can overwrite these default values by adding its own resources block in the relevant section below and setting the desired resources values. |
|
||||
| global.defaultUpstreamConcurrencyThreshold | int | `10000` | |
|
||||
| global.disableAlpnH2 | bool | `false` | Whether to disable HTTP/2 in ALPN |
|
||||
| global.enableGatewayAPI | bool | `true` | If true, Higress Controller will monitor Gateway API resources as well |
|
||||
| global.enableGatewayAPI | bool | `false` | If true, Higress Controller will monitor Gateway API resources as well |
|
||||
| global.enableH3 | bool | `false` | |
|
||||
| global.enableIPv6 | bool | `false` | |
|
||||
| global.enableInferenceExtension | bool | `false` | If true, enable Gateway API Inference Extension support |
|
||||
| global.enableIstioAPI | bool | `true` | If true, Higress Controller will monitor istio resources as well |
|
||||
| global.enableLDSCache | bool | `false` | |
|
||||
| global.enablePluginServer | bool | `false` | |
|
||||
|
||||
@@ -146,7 +146,6 @@ helm delete higress -n higress-system
|
||||
| gateway.service.ports[1].targetPort | int | `443` | 靶向端口 |
|
||||
| gateway.service.type | string | `"LoadBalancer"` | 服务类型 |
|
||||
| global.disableAlpnH2 | bool | `false` | 设置是否禁用 ALPN 中的 http/2 |
|
||||
| global.enableInferenceExtension | bool | `false` | 是否启用 Gateway API Inference Extension 支持 |
|
||||
| ... | ... | ... | ... |
|
||||
|
||||
由于内容较多,其他参数可以参考完整表。
|
||||
|
||||
331
hgctl/go.mod
331
hgctl/go.mod
@@ -1,6 +1,8 @@
|
||||
module github.com/alibaba/higress/hgctl
|
||||
|
||||
go 1.24.4
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.24.1
|
||||
|
||||
replace github.com/spf13/viper => github.com/istio/viper v1.3.3-0.20190515210538-2789fed3109c
|
||||
|
||||
@@ -20,11 +22,11 @@ require (
|
||||
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 v28.1.1+incompatible
|
||||
github.com/docker/cli v24.0.7+incompatible
|
||||
github.com/docker/compose/v2 v2.23.3
|
||||
github.com/docker/docker v28.4.0+incompatible
|
||||
github.com/evanphx/json-patch/v5 v5.9.11
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/docker/docker v24.0.7+incompatible
|
||||
github.com/evanphx/json-patch/v5 v5.7.0
|
||||
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
|
||||
@@ -34,214 +36,190 @@ require (
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/spf13/pflag v1.0.7
|
||||
github.com/spf13/viper v1.20.1
|
||||
github.com/stretchr/testify v1.11.1
|
||||
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.10.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
helm.sh/helm/v3 v3.18.5
|
||||
helm.sh/helm/v3 v3.12.2
|
||||
istio.io/istio v0.0.0
|
||||
k8s.io/api v0.34.1
|
||||
k8s.io/apimachinery v0.34.1
|
||||
k8s.io/cli-runtime v0.33.3
|
||||
k8s.io/client-go v0.34.1
|
||||
k8s.io/kubectl v0.33.3
|
||||
sigs.k8s.io/controller-runtime v0.22.3
|
||||
sigs.k8s.io/yaml v1.6.0
|
||||
k8s.io/api v0.28.3
|
||||
k8s.io/apimachinery v0.28.3
|
||||
k8s.io/cli-runtime v0.28.0
|
||||
k8s.io/client-go v0.28.3
|
||||
k8s.io/kubectl v0.28.0
|
||||
sigs.k8s.io/controller-runtime v0.16.3
|
||||
sigs.k8s.io/yaml v1.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.24.0 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/chzyer/readline v1.5.0 // indirect
|
||||
github.com/containerd/containerd/api v1.8.0 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/platforms v0.2.1 // indirect
|
||||
github.com/containerd/ttrpc v1.2.7 // indirect
|
||||
github.com/envoyproxy/go-control-plane/contrib v0.0.0-20251016030003-90eca0228178 // indirect
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.35.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.2 // indirect
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
|
||||
github.com/moby/sys/userns v0.1.0 // indirect
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240409071808-615f978279ca // indirect
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
||||
oras.land/oras-go/v2 v2.6.0 // indirect
|
||||
sigs.k8s.io/gateway-api-inference-extension v1.1.0 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
|
||||
)
|
||||
require github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
|
||||
|
||||
require (
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/BurntSushi/toml v1.2.1 // indirect
|
||||
github.com/MakeNowJust/heredoc v1.0.0 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.2.1 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
|
||||
github.com/Masterminds/squirrel v1.5.4 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/Microsoft/hcsshim v0.11.7 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||
github.com/Microsoft/hcsshim v0.11.1 // indirect
|
||||
github.com/RageCage64/multilinediff v0.2.0 // indirect
|
||||
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.6 // indirect
|
||||
github.com/aws/smithy-go v1.23.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.17.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.24 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.30 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.24 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.31 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.24 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.12.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.18.6 // indirect
|
||||
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/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
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/chai2010/gettext-go v1.0.3 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/chai2010/gettext-go v1.0.2 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 // indirect
|
||||
github.com/containerd/console v1.0.3 // indirect
|
||||
github.com/containerd/containerd v1.7.27 // indirect
|
||||
github.com/containerd/continuity v0.4.4 // indirect
|
||||
github.com/containerd/containerd v1.7.7 // indirect
|
||||
github.com/containerd/continuity v0.4.2 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/containerd/typeurl/v2 v2.2.3 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
||||
github.com/containerd/typeurl/v2 v2.1.1 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
github.com/distribution/distribution/v3 v3.0.0 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
||||
github.com/distribution/distribution/v3 v3.0.0-20230601133803-97b1d649c493 // indirect
|
||||
github.com/docker/buildx v0.12.0 // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.9.3 // indirect
|
||||
github.com/docker/distribution v2.8.2+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.7.0 // indirect
|
||||
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/go-metrics v0.0.1 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
|
||||
github.com/evanphx/json-patch v5.9.11+incompatible // indirect
|
||||
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
|
||||
github.com/envoyproxy/go-control-plane v0.11.2-0.20230725211550-11bfe846bcd4 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.0.2 // indirect
|
||||
github.com/evanphx/json-patch v5.7.0+incompatible // indirect
|
||||
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
|
||||
github.com/felixge/httpsnoop v1.0.3 // indirect
|
||||
github.com/fsnotify/fsevents v0.1.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // 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.5.1 // indirect
|
||||
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // 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.21.2 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.1 // 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.23.0 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/gofrs/flock v0.12.1 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/gofrs/flock v0.8.1 // indirect
|
||||
github.com/gogo/googleapis v1.4.1 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/mock v1.6.0 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/cel-go v0.26.0 // indirect
|
||||
github.com/google/gnostic-models v0.7.0 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/btree v1.1.2 // indirect
|
||||
github.com/google/cel-go v0.16.1 // indirect
|
||||
github.com/google/gnostic-models v0.6.8 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/google/uuid v1.3.1 // indirect
|
||||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
github.com/gosuri/uitable v0.0.4 // indirect
|
||||
github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 // indirect
|
||||
github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd // indirect
|
||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.2 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-version v1.7.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/hashicorp/go-version v1.6.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.4 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/huandu/xstrings v1.4.0 // indirect
|
||||
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.4.0 // indirect
|
||||
github.com/jonboulle/clockwork v0.5.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
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/compress v1.16.5 // indirect
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.3 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
|
||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||
github.com/lestrrat-go/iter v1.0.2 // indirect
|
||||
github.com/lestrrat-go/jwx v1.2.31 // indirect
|
||||
github.com/lestrrat-go/jwx v1.2.26 // indirect
|
||||
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/manifoldco/promptui v0.9.0
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
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
|
||||
github.com/mattn/go-shellwords v1.0.12 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
|
||||
github.com/miekg/dns v1.1.68 // indirect
|
||||
github.com/miekg/dns v1.1.59 // indirect
|
||||
github.com/miekg/pkcs11 v1.1.1 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/moby/buildkit v0.21.1 // indirect
|
||||
github.com/moby/buildkit v0.13.0-beta1.0.20231023114302-d5c1d785b042 // indirect
|
||||
github.com/moby/locker v1.0.1 // indirect
|
||||
github.com/moby/patternmatcher v0.6.0 // indirect
|
||||
github.com/moby/spdystream v0.5.0 // indirect
|
||||
github.com/moby/spdystream v0.2.0 // indirect
|
||||
github.com/moby/sys/mountinfo v0.6.2 // indirect
|
||||
github.com/moby/sys/sequential v0.5.0 // indirect
|
||||
github.com/moby/sys/signal v0.7.0 // indirect
|
||||
github.com/moby/sys/symlink v0.2.0 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
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.3-0.20250322232337-35a7c28c31ee // 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
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
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.23.2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.1 // indirect
|
||||
github.com/prometheus/procfs v0.17.0 // indirect
|
||||
github.com/prometheus/prometheus v0.307.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rubenv/sql-migrate v1.8.0 // indirect
|
||||
github.com/prometheus/client_golang v1.17.0 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.45.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/prometheus/prometheus v0.45.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.3 // indirect
|
||||
github.com/rubenv/sql-migrate v1.3.1 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.4.0 // indirect
|
||||
github.com/serialx/hashring v0.0.0-20190422032157-8b2912629002 // indirect
|
||||
github.com/shibumi/go-pathspec v1.3.0 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/shopspring/decimal v1.3.1 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/spf13/afero v1.14.0 // indirect
|
||||
github.com/spf13/cast v1.8.0 // indirect
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
github.com/spf13/afero v1.9.5 // indirect
|
||||
github.com/spf13/cast v1.5.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/stoewer/go-strcase v1.3.0 // indirect
|
||||
github.com/theupdateframework/notary v0.7.0 // indirect
|
||||
@@ -256,63 +234,65 @@ require (
|
||||
github.com/xlab/treeprint v1.2.0 // indirect
|
||||
github.com/zmap/zcrypto v0.0.0-20230310154051-c8b263fd8300 // indirect
|
||||
github.com/zmap/zlint/v3 v3.6.3 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.57.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.40.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.40.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0 // indirect
|
||||
go.opentelemetry.io/otel v1.17.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.16.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.16.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.39.1-0.20230714155235-03b8c47770f2 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.17.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.16.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v0.39.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.17.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
|
||||
go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/crypto v0.42.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250808145144-a408d31f581a // indirect
|
||||
golang.org/x/mod v0.28.0 // indirect
|
||||
golang.org/x/net v0.44.0 // indirect
|
||||
golang.org/x/oauth2 v0.31.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/term v0.35.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
golang.org/x/time v0.13.0 // indirect
|
||||
golang.org/x/tools v0.37.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 // indirect
|
||||
google.golang.org/grpc v1.76.0 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
golang.org/x/crypto v0.31.0 // 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
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/term v0.27.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/genproto v0.0.0-20231002182017-d307bd883b97 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230920204549-e6e6cdab5c13 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231009173412-8bfb1ae86b6c // indirect
|
||||
google.golang.org/grpc v1.59.0 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
istio.io/api v1.27.1-0.20250820125923-f5a5d3a605a9 // indirect
|
||||
istio.io/client-go v1.27.1-0.20250820130622-12f6d11feb40 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.34.1 // indirect
|
||||
k8s.io/apiserver v0.34.1 // indirect
|
||||
k8s.io/component-base v0.34.1 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250814151709-d7b6acb124c3 // indirect
|
||||
k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d // indirect
|
||||
sigs.k8s.io/gateway-api v1.4.0 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||
sigs.k8s.io/kustomize/api v0.19.0 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.19.0 // indirect
|
||||
sigs.k8s.io/mcs-api v0.1.1-0.20240624222831-d7001fe1d21c // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||
istio.io/api v1.19.5-0.20231206014255-f55a2b1e931e // indirect
|
||||
istio.io/client-go v1.19.5-0.20231206015206-8cdf6a3b3cfd // indirect
|
||||
k8s.io/apiextensions-apiserver v0.28.3 // indirect
|
||||
k8s.io/apiserver v0.28.3 // indirect
|
||||
k8s.io/component-base v0.28.3 // indirect
|
||||
k8s.io/klog/v2 v2.100.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect
|
||||
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
|
||||
oras.land/oras-go v1.2.3 // indirect
|
||||
sigs.k8s.io/gateway-api v0.8.0 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
||||
sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.14.3 // indirect
|
||||
sigs.k8s.io/mcs-api v0.1.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.3.0 // indirect
|
||||
)
|
||||
|
||||
replace istio.io/api => ../external/api
|
||||
|
||||
replace github.com/envoyproxy/go-control-plane => ../external/go-control-plane
|
||||
|
||||
replace github.com/envoyproxy/go-control-plane/contrib => ../external/go-control-plane/contrib
|
||||
|
||||
replace github.com/envoyproxy/go-control-plane/envoy => ../external/go-control-plane/envoy
|
||||
|
||||
replace istio.io/pkg => ../external/pkg
|
||||
|
||||
replace istio.io/client-go => ../external/client-go
|
||||
@@ -323,7 +303,6 @@ replace github.com/alibaba/higress => ../
|
||||
|
||||
replace (
|
||||
github.com/cucumber/godog => github.com/laurazard/godog v0.0.0-20220922095256-4c4b17abdae7
|
||||
github.com/distribution/distribution/v3 => github.com/distribution/distribution/v3 v3.0.0-20230601133803-97b1d649c493
|
||||
github.com/docker/buildx => github.com/docker/buildx v0.11.2
|
||||
github.com/docker/cli => github.com/docker/cli v24.0.6+incompatible
|
||||
github.com/docker/compose/v2 => github.com/docker/compose/v2 v2.20.2
|
||||
|
||||
2117
hgctl/go.sum
2117
hgctl/go.sum
File diff suppressed because it is too large
Load Diff
@@ -1,770 +0,0 @@
|
||||
// Copyright (c) 2022 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.
|
||||
|
||||
# Agent Module
|
||||
|
||||
`pkg/agent` 是 hgctl 中用于 Agent 生命周期管理的核心模块,提供了从创建、配置、部署到发布的完整工作流。
|
||||
|
||||
## 目录
|
||||
|
||||
- [概述](#概述)
|
||||
- [架构设计](#架构设计)
|
||||
- [核心功能](#核心功能)
|
||||
- [主要组件](#主要组件)
|
||||
- [使用方式](#使用方式)
|
||||
- [配置管理](#配置管理)
|
||||
- [部署方式](#部署方式)
|
||||
- [集成说明](#集成说明)
|
||||
|
||||
## 概述
|
||||
|
||||
Agent 模块提供了一套完整的 AI Agent 开发和部署解决方案,支持:
|
||||
|
||||
- **多种 Agentic Core**:集成 Claude Code 和 Qodercli
|
||||
- **本地和云端部署**:支持本地运行和 AgentRun (阿里云函数计算)
|
||||
- **MCP Server 管理**:支持 HTTP 和 OpenAPI 类型的 MCP Server
|
||||
- **Higress 集成**:自动发布 Agent API 到 Higress 网关
|
||||
- **Himarket 发布**:支持将 Agent 发布到 Himarket 市场
|
||||
|
||||
## 架构设计
|
||||
|
||||
```
|
||||
pkg/agent/
|
||||
├── agent.go # CLI 命令入口和主要业务逻辑
|
||||
├── core.go # Agentic Core (Claude/Qodercli) 封装
|
||||
├── new.go # Agent 创建流程
|
||||
├── deploy.go # Agent 部署处理(本地/云端)
|
||||
├── mcp.go # MCP Server 管理
|
||||
├── config.go # 配置管理和初始化
|
||||
├── base.go # 基础函数和环境检查
|
||||
├── types.go # 类型定义
|
||||
├── utils.go # 工具函数
|
||||
├── common/ # 通用类型定义
|
||||
│ └── base.go # ProductType 等常量
|
||||
├── services/ # 外部服务客户端
|
||||
│ ├── client.go # HTTP 客户端封装
|
||||
│ ├── service.go # Higress/Himarket API 封装
|
||||
│ └── utils.go # 服务工具函数
|
||||
└── prompt/ # Prompt 模板和指导
|
||||
├── base.go # Agent 开发指南
|
||||
└── agent_guide.md
|
||||
```
|
||||
|
||||
## 核心功能
|
||||
|
||||
### 1. Agent 创建 (new.go)
|
||||
|
||||
提供两种 Agent 创建方式:
|
||||
|
||||
#### 1.1 交互式创建
|
||||
通过命令行交互式问答,逐步配置:
|
||||
- Agent 名称和描述
|
||||
- 系统 Prompt(支持直接输入、从文件导入、LLM 生成)
|
||||
- AI 模型配置(DashScope、OpenAI、Anthropic 等)
|
||||
- 工具集选择(AgentScope 内置工具)
|
||||
- MCP Server 配置
|
||||
- 部署设置
|
||||
|
||||
#### 1.2 从 Core 导入
|
||||
从 Agentic Core 的 subagent 目录导入已有的 Agent 配置。
|
||||
|
||||
**关键代码位置**:
|
||||
- `createAgentCmd()` (new.go:99): 创建命令定义
|
||||
- `getAgentConfig()` (utils.go:289): 获取 Agent 配置
|
||||
- `createAgentTemplate()` (new.go:205): 生成 Agent 模板文件
|
||||
|
||||
### 2. Agent 部署 (deploy.go)
|
||||
|
||||
支持两种部署模式:
|
||||
|
||||
#### 2.1 本地部署 (Local)
|
||||
- 基于 AgentScope Runtime
|
||||
- 自动管理 Python 虚拟环境
|
||||
- 依赖管理:`agentscope`, `agentscope-runtime==1.0.0`
|
||||
- 默认端口:8090
|
||||
|
||||
#### 2.2 云端部署 (AgentRun)
|
||||
- 部署到阿里云函数计算
|
||||
- 使用 Serverless Devs (s工具)
|
||||
- 自动构建和部署
|
||||
- 需要配置阿里云 Access Key
|
||||
|
||||
**关键代码位置**:
|
||||
- `DeployHandler` (deploy.go:35): 部署处理器
|
||||
- `HandleLocal()` (deploy.go:350): 本地部署逻辑
|
||||
- `HandleAgentRun()` (deploy.go:305): AgentRun 部署逻辑
|
||||
|
||||
### 3. MCP Server 管理 (mcp.go)
|
||||
|
||||
支持两种类型的 MCP Server:
|
||||
|
||||
#### 3.1 HTTP MCP Server
|
||||
直接通过 HTTP URL 添加:
|
||||
```bash
|
||||
hgctl mcp add [name] [url] --type http
|
||||
```
|
||||
|
||||
#### 3.2 OpenAPI MCP Server
|
||||
从 OpenAPI 规范文件创建:
|
||||
```bash
|
||||
hgctl mcp add [name] [spec-file] --type openapi
|
||||
```
|
||||
|
||||
功能特性:
|
||||
- 自动解析 OpenAPI 规范
|
||||
- 转换为 MCP Server 配置
|
||||
- 自动添加到 Agentic Core
|
||||
- 可选发布到 Higress
|
||||
- 支持发布到 Himarket 市场
|
||||
|
||||
**关键代码位置**:
|
||||
- `handleAddMCP()` (mcp.go:183): MCP 添加主逻辑
|
||||
- `publishMCPToHigress()` (mcp.go:228): 发布到 Higress
|
||||
- `parseOpenapi2MCP()` (utils.go:79): OpenAPI 解析
|
||||
|
||||
### 4. Agentic Core 集成 (core.go)
|
||||
|
||||
封装了 Agentic Core(Claude Code/Qodercli)的交互:
|
||||
|
||||
#### 支持的 Core 类型
|
||||
```go
|
||||
const (
|
||||
CORE_CLAUDE CoreType = "claude"
|
||||
CORE_QODERCLI CoreType = "qodercli"
|
||||
)
|
||||
```
|
||||
|
||||
#### 核心功能
|
||||
- **Setup()**: 初始化环境和插件
|
||||
- **Start()**: 启动交互式窗口
|
||||
- **AddMCPServer()**: 添加 MCP Server 到 Core
|
||||
- **ImproveNewAgent()**: 在特定 Agent 目录运行 Core 进行改进
|
||||
|
||||
**关键代码位置**:
|
||||
- `AgenticCore` (core.go:32): Core 封装结构
|
||||
- `Setup()` (core.go:108): 环境初始化
|
||||
- `addHigressAPIMCP()` (core.go:161): 自动添加 Higress API MCP
|
||||
|
||||
### 5. Higress 集成
|
||||
|
||||
自动将 Agent API 发布到 Higress 网关:
|
||||
|
||||
#### 支持的 API 类型
|
||||
```go
|
||||
const (
|
||||
A2A = "a2a" // Agent-to-Agent
|
||||
REST = "restful" // RESTful API
|
||||
MODEL = "model" // AI Model API
|
||||
)
|
||||
```
|
||||
|
||||
#### 发布流程
|
||||
1. 创建 AI Provider Service
|
||||
2. 创建 AI Route
|
||||
3. 配置服务源和路由
|
||||
|
||||
**关键代码位置**:
|
||||
- `publishAgentAPIToHigress()` (agent.go:123): 发布逻辑
|
||||
- `services/service.go`: Higress API 封装
|
||||
|
||||
### 6. Himarket 集成
|
||||
|
||||
支持将 Agent 发布到 Himarket 市场:
|
||||
|
||||
#### 产品类型
|
||||
```go
|
||||
const (
|
||||
MCP_SERVER ProductType = "MCP_SERVER"
|
||||
MODEL_API ProductType = "MODEL_API"
|
||||
REST_API ProductType = "REST_API"
|
||||
AGENT_API ProductType = "AGENT_API"
|
||||
)
|
||||
```
|
||||
|
||||
**关键代码位置**:
|
||||
- `publishAPIToHimarket()` (base.go:128): 发布到市场
|
||||
- `services/service.go`: Himarket API 封装
|
||||
|
||||
## 主要组件
|
||||
|
||||
### AgentConfig 结构
|
||||
|
||||
Agent 的核心配置结构:
|
||||
|
||||
```go
|
||||
type AgentConfig struct {
|
||||
AppName string // 应用名称
|
||||
AppDescription string // 应用描述
|
||||
AgentName string // Agent 名称
|
||||
AvailableTools []string // 可用工具列表
|
||||
SysPromptPath string // 系统 Prompt 路径
|
||||
ChatModel string // 使用的模型
|
||||
Provider string // 模型提供商
|
||||
APIKeyEnvVar string // API Key 环境变量
|
||||
DeploymentPort int // 部署端口
|
||||
HostBinding string // 主机绑定
|
||||
EnableStreaming bool // 是否启用流式响应
|
||||
EnableThinking bool // 是否启用思考过程
|
||||
MCPServers []MCPServerConfig // MCP Server 配置
|
||||
Type DeployType // 部署类型
|
||||
ServerlessCfg ServerlessConfig // Serverless 配置
|
||||
}
|
||||
```
|
||||
|
||||
### 环境检查 (base.go)
|
||||
|
||||
`EnvProvisioner` 负责检查和安装必要的环境:
|
||||
|
||||
#### Node.js 检查
|
||||
- 最低版本要求:Node.js 18+
|
||||
- 支持自动安装(通过 fnm)
|
||||
|
||||
#### Agentic Core 检查
|
||||
- 检查 claude 或 qodercli 是否安装
|
||||
- 支持自动安装(通过 npm)
|
||||
|
||||
**关键代码位置**:
|
||||
- `EnvProvisioner.check()` (base.go:221): 环境检查
|
||||
- `promptNodeInstall()` (base.go:259): Node.js 安装引导
|
||||
- `promptAgentInstall()` (base.go:401): Core 安装引导
|
||||
|
||||
## 使用方式
|
||||
|
||||
### 命令结构
|
||||
|
||||
```bash
|
||||
hgctl agent # 启动交互式 Agent 窗口
|
||||
hgctl agent new # 创建新 Agent
|
||||
hgctl agent deploy [name] # 部署 Agent
|
||||
hgctl agent add [name] [url] # 添加 Agent API 到 Higress
|
||||
hgctl mcp add [name] [url] # 添加 MCP Server
|
||||
```
|
||||
|
||||
### 创建 Agent
|
||||
|
||||
#### 本地部署的 Agent
|
||||
```bash
|
||||
hgctl agent new
|
||||
```
|
||||
|
||||
交互式选择:
|
||||
1. 创建方式:step by step / 从 Core 导入
|
||||
2. Agent 名称和描述
|
||||
3. 系统 Prompt 设置
|
||||
4. 模型提供商和模型选择
|
||||
5. 工具选择
|
||||
6. MCP Server 配置
|
||||
7. 部署设置
|
||||
|
||||
#### AgentRun 部署的 Agent
|
||||
```bash
|
||||
hgctl agent new --agent-run
|
||||
```
|
||||
|
||||
额外配置:
|
||||
- Resource Name
|
||||
- Region
|
||||
- Disk Size
|
||||
- Timeout
|
||||
|
||||
### 部署 Agent
|
||||
|
||||
#### 部署到本地
|
||||
```bash
|
||||
hgctl agent deploy my-agent
|
||||
```
|
||||
|
||||
自动处理:
|
||||
- Python 环境检查
|
||||
- 依赖安装
|
||||
- 启动 Agent 服务
|
||||
|
||||
#### 部署到 AgentRun
|
||||
```bash
|
||||
hgctl agent deploy my-agent
|
||||
```
|
||||
|
||||
要求:
|
||||
- 已配置阿里云 Access Key
|
||||
- 已安装 Docker
|
||||
- 已安装 Serverless Devs CLI
|
||||
|
||||
### 添加 MCP Server
|
||||
|
||||
#### 添加 HTTP MCP Server
|
||||
```bash
|
||||
hgctl mcp add my-mcp http://localhost:8080/mcp \
|
||||
--type http \
|
||||
--transport streamable \
|
||||
-e API_KEY=secret \
|
||||
-H "Authorization: Bearer token"
|
||||
```
|
||||
|
||||
参数说明:
|
||||
- `--type`: MCP 类型(http/openapi)
|
||||
- `--transport`: 传输类型(streamable/sse)
|
||||
- `-e`: 环境变量
|
||||
- `-H`: HTTP 头部
|
||||
|
||||
#### 从 OpenAPI 创建 MCP Server
|
||||
```bash
|
||||
hgctl mcp add swagger-mcp ./openapi.yaml \
|
||||
--type openapi
|
||||
```
|
||||
|
||||
自动完成:
|
||||
1. 解析 OpenAPI 规范
|
||||
2. 转换为 MCP 配置
|
||||
3. 发布到 Higress
|
||||
4. 添加到 Agentic Core
|
||||
|
||||
### 发布到 Higress 和 Himarket
|
||||
|
||||
```bash
|
||||
hgctl agent add my-agent http://my-agent.com \
|
||||
--type model \
|
||||
--as-product \
|
||||
--higress-console-url http://console.higress.io \
|
||||
--higress-console-user admin \
|
||||
--higress-console-password password \
|
||||
--himarket-admin-url http://himarket.io \
|
||||
--himarket-admin-user admin \
|
||||
--himarket-admin-password password
|
||||
```
|
||||
|
||||
## 配置管理
|
||||
|
||||
### 配置文件
|
||||
|
||||
配置文件位置:`~/.hgctl`
|
||||
|
||||
```json
|
||||
{
|
||||
"hgctl-agent-core": "claude",
|
||||
"agent-chat-model": "qwen-plus",
|
||||
"agent-model-provider": "DashScope",
|
||||
"higress-console-url": "http://127.0.0.1:8080",
|
||||
"higress-console-user": "admin",
|
||||
"higress-console-password": "admin",
|
||||
"higress-gateway-url": "http://127.0.0.1:80",
|
||||
"himarket-admin-url": "",
|
||||
"himarket-admin-user": "",
|
||||
"himarket-admin-password": "",
|
||||
"agentrun-model-name": "",
|
||||
"agentrun-region": "cn-hangzhou"
|
||||
}
|
||||
```
|
||||
|
||||
### 配置项说明
|
||||
|
||||
| 配置项 | 说明 | 默认值 |
|
||||
|--------|------|--------|
|
||||
| `hgctl-agent-core` | Agentic Core 类型 | `qodercli` |
|
||||
| `agent-chat-model` | 默认聊天模型 | - |
|
||||
| `agent-model-provider` | 默认模型提供商 | - |
|
||||
| `higress-console-url` | Higress 控制台地址 | - |
|
||||
| `higress-console-user` | Higress 用户名 | - |
|
||||
| `higress-console-password` | Higress 密码 | - |
|
||||
| `higress-gateway-url` | Higress 网关地址 | - |
|
||||
| `himarket-admin-url` | Himarket 管理地址 | - |
|
||||
| `himarket-admin-user` | Himarket 用户名 | - |
|
||||
| `himarket-admin-password` | Himarket 密码 | - |
|
||||
| `agentrun-model-name` | AgentRun 模型名 | - |
|
||||
| `agentrun-region` | AgentRun 区域 | `cn-hangzhou` |
|
||||
|
||||
### 环境变量
|
||||
|
||||
配置也可以通过环境变量设置(自动转换,用下划线替换连字符):
|
||||
|
||||
```bash
|
||||
export HIGRESS_CONSOLE_URL=http://127.0.0.1:8080
|
||||
export HIGRESS_CONSOLE_USER=admin
|
||||
export HIGRESS_CONSOLE_PASSWORD=admin
|
||||
```
|
||||
|
||||
**代码位置**: `config.go:100` - `InitConfig()`
|
||||
|
||||
## 部署方式
|
||||
|
||||
### 本地部署 (Local)
|
||||
|
||||
#### 技术栈
|
||||
- **Runtime**: AgentScope Runtime
|
||||
- **Python**: 3.12+
|
||||
- **依赖**:
|
||||
- `agentscope`
|
||||
- `agentscope-runtime==1.0.0`
|
||||
|
||||
#### 部署流程
|
||||
1. 检查 Python 环境
|
||||
2. 创建/激活虚拟环境 (`~/.hgctl/.venv`)
|
||||
3. 安装依赖
|
||||
4. 启动 Agent 服务
|
||||
|
||||
#### 生成的文件
|
||||
```
|
||||
~/.hgctl/agents/{agent-name}/
|
||||
├── as_runtime_main.py # AgentScope Runtime 入口
|
||||
├── agent.py # Agent 类定义
|
||||
├── toolkit.py # 工具集
|
||||
├── prompt.md # 系统 Prompt
|
||||
├── CLAUDE.md # Claude 开发指南(如果使用 Claude)
|
||||
└── AGENTS.md # Qoder 开发指南(如果使用 Qodercli)
|
||||
```
|
||||
|
||||
**代码位置**: `deploy.go:350` - `HandleLocal()`
|
||||
|
||||
### 云端部署 (AgentRun)
|
||||
|
||||
#### 技术栈
|
||||
- **平台**: 阿里云函数计算 (Function Compute)
|
||||
- **SDK**: agentrun-sdk-python
|
||||
- **工具**: Serverless Devs CLI
|
||||
|
||||
#### 部署流程
|
||||
1. 检查环境(Docker、Serverless Devs)
|
||||
2. 检查/配置 Access Key
|
||||
3. 执行 `s build`
|
||||
4. 执行 `s deploy`
|
||||
|
||||
#### 生成的文件
|
||||
```
|
||||
~/.hgctl/agents/{agent-name}/
|
||||
├── agentrun_main.py # AgentRun 入口
|
||||
├── agent.py # Agent 类定义
|
||||
├── toolkit.py # 工具集
|
||||
├── prompt.md # 系统 Prompt
|
||||
├── requirements.txt # Python 依赖
|
||||
└── s.yaml # Serverless Devs 配置
|
||||
```
|
||||
|
||||
#### s.yaml 配置
|
||||
```yaml
|
||||
edition: 3.0.0
|
||||
name: {agent-name}
|
||||
access: hgctl-credential
|
||||
|
||||
resources:
|
||||
fc-agentrun-demo:
|
||||
component: fc3
|
||||
props:
|
||||
region: {region}
|
||||
description: {description}
|
||||
runtime: python3.12
|
||||
code: ./
|
||||
handler: agentrun_main.main
|
||||
timeout: {timeout}
|
||||
diskSize: {disk-size}
|
||||
environmentVariables:
|
||||
MODEL_NAME: {model-name}
|
||||
{api-key-env}: {api-key}
|
||||
customRuntimeConfig:
|
||||
command:
|
||||
- python3
|
||||
args:
|
||||
- agentrun_main.py
|
||||
port: {port}
|
||||
```
|
||||
|
||||
**代码位置**: `deploy.go:305` - `HandleAgentRun()`
|
||||
|
||||
## 集成说明
|
||||
|
||||
### Higress 集成
|
||||
|
||||
#### Service Source 创建
|
||||
```go
|
||||
// services/utils.go
|
||||
func BuildServiceBodyAndSrv(name, rawURL string) (map[string]interface{}, string, int, error)
|
||||
```
|
||||
|
||||
创建服务源:
|
||||
- 解析 URL
|
||||
- 提取域名、端口
|
||||
- 生成服务名称
|
||||
|
||||
#### AI Provider 和 Route 创建
|
||||
|
||||
对于 MODEL 类型的 Agent:
|
||||
```go
|
||||
// services/utils.go
|
||||
func BuildAIProviderServiceBody(name, url string) map[string]interface{}
|
||||
func BuildAddAIRouteBody(name, url string) map[string]interface{}
|
||||
```
|
||||
|
||||
#### MCP Server 创建
|
||||
|
||||
支持两种类型:
|
||||
- **DIRECT_ROUTE**: 直接路由到 MCP Server URL
|
||||
- **OPEN_API**: 基于 OpenAPI 规范的工具配置
|
||||
|
||||
### Himarket 集成
|
||||
|
||||
#### API Product 创建
|
||||
```go
|
||||
// services/utils.go
|
||||
func BuildAPIProductBody(name, desc string, typ string) map[string]interface{}
|
||||
```
|
||||
|
||||
#### Product Reference
|
||||
```go
|
||||
func BuildRefModelAPIProductBody(gatewayId, productId, routeName string) map[string]interface{}
|
||||
func BuildRefMCPAPIProductBody(gatewayId, productId, mcpServerName string) map[string]interface{}
|
||||
```
|
||||
|
||||
### Agentic Core 集成
|
||||
|
||||
#### 初始化流程
|
||||
1. 提取 manifest 文件到 `~/.hgctl/`
|
||||
2. 提取 Core 相关文件到 `~/.claude/` 或 `~/.qoder/`
|
||||
3. 添加预定义的 MCP Server
|
||||
4. 自动配置 Higress API MCP Server
|
||||
|
||||
#### MCP Server 添加
|
||||
```bash
|
||||
{core} mcp add --transport {transport} {name} {url} \
|
||||
--scope {scope} \
|
||||
-e {env} \
|
||||
-H {header}
|
||||
```
|
||||
|
||||
**代码位置**: `core.go:236` - `AddMCPServer()`
|
||||
|
||||
## 类型定义 (types.go)
|
||||
|
||||
### API 请求/响应类型
|
||||
|
||||
用于与 AI 模型 API 交互:
|
||||
|
||||
```go
|
||||
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 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"`
|
||||
}
|
||||
```
|
||||
|
||||
### OpenAPI 相关类型
|
||||
|
||||
用于 OpenAPI 规范解析:
|
||||
|
||||
```go
|
||||
type API struct {
|
||||
OpenAPI string `yaml:"openapi"`
|
||||
Info Info `yaml:"info"`
|
||||
Servers []Server `yaml:"servers"`
|
||||
Paths Paths `yaml:"paths"`
|
||||
Components Components `yaml:"components"`
|
||||
}
|
||||
```
|
||||
|
||||
## Services 子包
|
||||
|
||||
### HigressClient
|
||||
|
||||
Higress API 客户端:
|
||||
|
||||
```go
|
||||
type HigressClient struct {
|
||||
baseURL string
|
||||
username string
|
||||
password string
|
||||
client *http.Client
|
||||
}
|
||||
```
|
||||
|
||||
**主要方法**:
|
||||
- `Get(path string) ([]byte, error)`
|
||||
- `Post(path string, body interface{}) ([]byte, error)`
|
||||
- `Put(path string, body interface{}) ([]byte, error)`
|
||||
|
||||
### HimarketClient
|
||||
|
||||
Himarket API 客户端:
|
||||
|
||||
```go
|
||||
type HimarketClient struct {
|
||||
baseURL string
|
||||
username string
|
||||
password string
|
||||
client *http.Client
|
||||
}
|
||||
```
|
||||
|
||||
**主要方法**:
|
||||
- `GetDevMCPServerProduct() (map[string]string, error)`
|
||||
- `GetDevModelProduct() (map[string]string, error)`
|
||||
|
||||
## 工具函数 (utils.go)
|
||||
|
||||
### Kubernetes 相关
|
||||
|
||||
- `GetHigressGatewayServiceIP()`: 获取 Higress Gateway Service IP
|
||||
- `extractServiceIP()`: 从 Service 提取 IP
|
||||
- `getConsoleCredentials()`: 从 K8s Secret 获取控制台凭证
|
||||
|
||||
### Agent 配置
|
||||
|
||||
- `getAgentConfig()`: 交互式获取 Agent 配置
|
||||
- `createAgentStepByStep()`: 逐步创建 Agent
|
||||
- `importAgentFromCore()`: 从 Core 导入 Agent
|
||||
|
||||
### Query 函数
|
||||
|
||||
一系列用于交互式配置查询的函数:
|
||||
- `queryAgentSysPrompt()`: 查询系统 Prompt
|
||||
- `queryAgentTools()`: 查询工具选择
|
||||
- `queryAgentModel()`: 查询模型配置
|
||||
- `queryAgentMCP()`: 查询 MCP Server
|
||||
- `queryDeploySettings()`: 查询部署设置
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 开发流程
|
||||
|
||||
```bash
|
||||
# 1. 创建 Agent
|
||||
hgctl agent new
|
||||
|
||||
# 2. 使用 Core 改进和测试
|
||||
# 选择 "Improve and test it using agentic core"
|
||||
|
||||
# 3. 部署 Agent
|
||||
hgctl agent deploy my-agent
|
||||
|
||||
# 4. 添加到 Higress
|
||||
hgctl agent add my-agent http://localhost:8090 --type model
|
||||
|
||||
# 5. (可选)发布到 Himarket
|
||||
hgctl agent add my-agent http://localhost:8090 --type model --as-product
|
||||
```
|
||||
|
||||
### 2. MCP Server 管理
|
||||
|
||||
```bash
|
||||
# 添加 HTTP MCP Server
|
||||
hgctl mcp add my-mcp http://mcp-server:8080/mcp
|
||||
|
||||
# 从 OpenAPI 创建 MCP Server
|
||||
hgctl mcp add swagger-mcp ./openapi.yaml --type openapi
|
||||
|
||||
# 添加到 Higress 和 Himarket
|
||||
hgctl mcp add my-mcp http://mcp-server:8080/mcp --as-product
|
||||
```
|
||||
|
||||
### 3. 配置管理
|
||||
|
||||
```bash
|
||||
# 使用配置文件
|
||||
vim ~/.hgctl
|
||||
|
||||
# 或使用环境变量
|
||||
export HIGRESS_CONSOLE_URL=http://127.0.0.1:8080
|
||||
export HIGRESS_CONSOLE_USER=admin
|
||||
export HIGRESS_CONSOLE_PASSWORD=admin
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 常见错误
|
||||
|
||||
1. **Node.js 未安装**
|
||||
- 自动提示安装选项
|
||||
- 支持自动安装(fnm)
|
||||
|
||||
2. **Agentic Core 未安装**
|
||||
- 自动提示安装选项
|
||||
- 支持自动安装(npm)
|
||||
|
||||
3. **Python 环境问题**
|
||||
- 自动创建虚拟环境
|
||||
- 自动安装依赖
|
||||
|
||||
4. **Kubernetes 连接问题**
|
||||
- 提供手动输入 kubeconfig 选项
|
||||
- 支持自定义 namespace
|
||||
|
||||
5. **Higress/Himarket 认证失败**
|
||||
- 检查配置文件
|
||||
- 检查环境变量
|
||||
- 尝试从 K8s Secret 自动获取
|
||||
|
||||
## 扩展开发
|
||||
|
||||
### 添加新的 Agentic Core
|
||||
|
||||
1. 在 `config.go` 中添加新的 CoreType
|
||||
2. 在 `core.go` 中实现相应的方法
|
||||
3. 更新 `EnvProvisioner` 支持新的安装方式
|
||||
|
||||
### 添加新的部署类型
|
||||
|
||||
1. 在 `deploy.go` 中添加新的 DeployType
|
||||
2. 实现相应的部署处理方法
|
||||
3. 更新模板生成逻辑
|
||||
|
||||
### 添加新的 API 类型
|
||||
|
||||
1. 在 `agent.go` 中添加新的 API Type 常量
|
||||
2. 在 `publishAgentAPIToHigress()` 中添加处理逻辑
|
||||
3. 在 `services/utils.go` 中添加相应的构建函数
|
||||
|
||||
## 依赖说明
|
||||
|
||||
### Go 依赖
|
||||
- `github.com/spf13/cobra`: CLI 框架
|
||||
- `github.com/spf13/viper`: 配置管理
|
||||
- `github.com/AlecAivazis/survey/v2`: 交互式问答
|
||||
- `github.com/fatih/color`: 终端颜色输出
|
||||
- `k8s.io/client-go`: Kubernetes 客户端
|
||||
|
||||
### 外部工具
|
||||
- **Node.js 18+**: Agentic Core 运行环境
|
||||
- **Claude Code / Qodercli**: Agentic Core
|
||||
- **Python 3.12+**: Agent Runtime
|
||||
- **Docker**: AgentRun 部署
|
||||
- **Serverless Devs CLI**: AgentRun 部署工具
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [AgentScope 文档](https://modelscope.github.io/agentscope/)
|
||||
- [Claude Code 文档](https://docs.claude.com/en/docs/claude-code/setup)
|
||||
- [Qoder 文档](https://docs.qoder.com/zh/cli/quick-start)
|
||||
- [Serverless Devs 文档](https://serverless-devs.com/docs/user-guide/install)
|
||||
- [Higress 文档](https://higress.io/)
|
||||
- [AgentRun 文档](https://github.com/Serverless-Devs/agentrun-sdk-python)
|
||||
|
||||
## License
|
||||
|
||||
Copyright (c) 2025 Alibaba Group Holding Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0
|
||||
@@ -15,161 +15,32 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/alibaba/higress/hgctl/pkg/agent/services"
|
||||
"github.com/spf13/cobra"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
)
|
||||
|
||||
// API Type
|
||||
const (
|
||||
A2A = "a2a"
|
||||
REST = "restful"
|
||||
MODEL = "model"
|
||||
)
|
||||
|
||||
func NewAgentCmd() *cobra.Command {
|
||||
agentCmd := &cobra.Command{
|
||||
Use: "agent",
|
||||
Short: "Start the interactive agent window",
|
||||
Short: "start the interactive agent window",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(invokeAgentCore(cmd.OutOrStdout()))
|
||||
cmdutil.CheckErr(handleAgentInvoke(cmd.OutOrStdout()))
|
||||
},
|
||||
}
|
||||
|
||||
agentCmd.AddCommand(createAgentCmd())
|
||||
agentCmd.AddCommand(deployAgentCmd())
|
||||
agentCmd.AddCommand(newAgentAddCmd())
|
||||
|
||||
return agentCmd
|
||||
}
|
||||
|
||||
func invokeAgentCore(w io.Writer) error {
|
||||
core, err := getCore()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get core: %s", err)
|
||||
}
|
||||
return core.Start()
|
||||
func handleAgentInvoke(w io.Writer) error {
|
||||
|
||||
return getAgent().Start()
|
||||
}
|
||||
|
||||
type AgentAddArg struct {
|
||||
HigressConsoleAuthArg
|
||||
HimarketAdminAuthArg
|
||||
|
||||
name string
|
||||
url string
|
||||
typ string
|
||||
scope string
|
||||
|
||||
asProduct bool
|
||||
noPublish bool
|
||||
}
|
||||
|
||||
func newAgentAddCmd() *cobra.Command {
|
||||
arg := &AgentAddArg{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "add [name] [url]",
|
||||
Short: "add agent to local interactive window and publish it to higress (optional)",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
arg.name = args[0]
|
||||
arg.url = args[1]
|
||||
|
||||
resolveHigressConsoleAuth(&arg.HigressConsoleAuthArg)
|
||||
resolveHimarketAdminAuth(&arg.HimarketAdminAuthArg)
|
||||
cmdutil.CheckErr(handleAddAgent(cmd.OutOrStdout(), *arg))
|
||||
},
|
||||
Args: cobra.ExactArgs(2),
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().StringVarP(&arg.typ, "type", "t", MODEL, "Determine the agent's API type (a2a, model, restful) default is model")
|
||||
cmd.PersistentFlags().StringVarP(&arg.scope, "scope", "s", "project", `Configuration scope (project or global)`)
|
||||
cmd.PersistentFlags().BoolVar(&arg.noPublish, "no-publish", false, "If it's set then the agent API will not be plubished to Higress")
|
||||
cmd.PersistentFlags().BoolVar(&arg.asProduct, "as-product", false, "If it's set then the agent API will be published to Himarket (no-publish must be false)")
|
||||
|
||||
addHigressConsoleAuthFlag(cmd, &arg.HigressConsoleAuthArg)
|
||||
addHimarketAdminAuthFlag(cmd, &arg.HimarketAdminAuthArg)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func handleAddAgent(writer io.Writer, arg AgentAddArg) error {
|
||||
if err := validateArg(arg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !arg.noPublish {
|
||||
if err := publishAgentAPIToHigress(arg); err != nil {
|
||||
fmt.Printf("failed to publish agent api to higress: %s\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Agent %s is published to Higress successfully\n", arg.name)
|
||||
|
||||
if arg.asProduct {
|
||||
if err := publishAPIToHimarket(arg.typ, arg.name, arg.HimarketAdminAuthArg); err != nil {
|
||||
fmt.Println("failed to publish it to himarket, please do it mannually")
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Agent %s is published to Himarket successfully\n", arg.name)
|
||||
}
|
||||
// TODO: pop up higress window
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func publishAgentAPIToHigress(arg AgentAddArg) error {
|
||||
client := services.NewHigressClient(arg.hgURL, arg.hgUser, arg.hgPassword)
|
||||
|
||||
switch arg.typ {
|
||||
case A2A:
|
||||
case MODEL:
|
||||
// add ai service
|
||||
body := services.BuildAIProviderServiceBody(arg.name, arg.url)
|
||||
// Debug
|
||||
// fmt.Printf("services: body: %v\n", body)
|
||||
if resp, err := services.HandleAddAIProviderService(client, body); err != nil {
|
||||
fmt.Println(string(resp))
|
||||
return err
|
||||
}
|
||||
|
||||
// add ai route
|
||||
body = services.BuildAddAIRouteBody(arg.name, arg.url)
|
||||
// fmt.Printf("Route body: %v\n", body)
|
||||
if res, err := services.HandleAddAIRoute(client, body); err != nil {
|
||||
fmt.Println(string(res))
|
||||
return err
|
||||
}
|
||||
|
||||
case REST:
|
||||
srvName := fmt.Sprintf("agent-%s", arg.name)
|
||||
body, targetSrvName, _, err := services.BuildServiceBodyAndSrv(srvName, arg.url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid url format: %s", err)
|
||||
}
|
||||
|
||||
if resp, err := services.HandleAddServiceSource(client, body); err != nil {
|
||||
fmt.Println(string(resp))
|
||||
return err
|
||||
}
|
||||
|
||||
if resp, err := services.HandleAddRoute(client, services.BuildAPIRouteBody(arg.name, targetSrvName)); err != nil {
|
||||
fmt.Println(string(resp))
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unsupported agent protocol type: %s", arg.typ)
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateArg(arg AgentAddArg) error {
|
||||
if !arg.noPublish {
|
||||
return arg.HigressConsoleAuthArg.validate()
|
||||
}
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -15,473 +15,47 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/alibaba/higress/hgctl/pkg/agent/common"
|
||||
"github.com/alibaba/higress/hgctl/pkg/agent/services"
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
type HimarketAdminAuthArg struct {
|
||||
hmURL string
|
||||
hmUser string
|
||||
hmPassword string
|
||||
// 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()
|
||||
}
|
||||
|
||||
// Developer's page
|
||||
type HimarketDevAuthArg struct {
|
||||
hmURL string
|
||||
hmUser string
|
||||
hmPassword string
|
||||
}
|
||||
func checkAgentInstallStatus() bool {
|
||||
// TODO: Support cross-platform:windows
|
||||
|
||||
func (h *HimarketAdminAuthArg) validate() error {
|
||||
if h.hmURL == "" || h.hmUser == "" || h.hmPassword == "" {
|
||||
return fmt.Errorf("invalid args")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type HigressConsoleAuthArg struct {
|
||||
// higress console auth arg
|
||||
hgURL string
|
||||
hgUser string
|
||||
hgPassword string
|
||||
}
|
||||
|
||||
func (h *HigressConsoleAuthArg) validate() error {
|
||||
if h.hgURL == "" || h.hgUser == "" || h.hgPassword == "" {
|
||||
fmt.Println("--higress-console-user, --higress-console-url, --higress-console-password must be provided")
|
||||
return fmt.Errorf("invalid args")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Init the global configuration from config file
|
||||
InitConfig()
|
||||
}
|
||||
|
||||
func resolveHimarketAdminAuth(arg *HimarketAdminAuthArg) {
|
||||
if arg.hmURL == "" {
|
||||
arg.hmURL = viper.GetString(HIMARKET_ADMIN_URL)
|
||||
}
|
||||
if arg.hmUser == "" {
|
||||
arg.hmUser = viper.GetString(HIMARKET_ADMIN_USER)
|
||||
}
|
||||
if arg.hmPassword == "" {
|
||||
arg.hmPassword = viper.GetString(HIMARKET_ADMIN_PASSWORD)
|
||||
}
|
||||
}
|
||||
|
||||
// resolve from viper
|
||||
func resolveHigressConsoleAuth(arg *HigressConsoleAuthArg) {
|
||||
if arg.hgURL == "" {
|
||||
arg.hgURL = 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, because 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 parseTypeToAPIProductType(typ string) string {
|
||||
switch typ {
|
||||
case "a2a":
|
||||
return string(common.AGENT_API)
|
||||
case "restful":
|
||||
return string(common.REST_API)
|
||||
case "model":
|
||||
return string(common.MODEL_API)
|
||||
case "mcp":
|
||||
return string(common.MCP_SERVER)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// This function serves MCP API as well as Model API for now.
|
||||
func publishAPIToHimarket(typ, name string, arg HimarketAdminAuthArg) error {
|
||||
|
||||
if err := arg.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client := services.NewHimarketClient(arg.hmURL, arg.hmUser, arg.hmPassword)
|
||||
|
||||
productName := fmt.Sprintf("%s-%s", typ, name)
|
||||
|
||||
var gatewayId = viper.GetString(HIMARKET_TARGET_HIGRESS_ID)
|
||||
prompt := survey.Input{
|
||||
Message: fmt.Sprintf("Enter the target Higress instance id on Himarket(%s):", gatewayId),
|
||||
Default: gatewayId,
|
||||
Help: fmt.Sprintf("refers to %s/consoles/gateway to get your target Higress instance's id", arg.hmURL),
|
||||
}
|
||||
|
||||
if err := survey.AskOne(&prompt, &gatewayId); err != nil {
|
||||
return fmt.Errorf("failed to get target higress gatewayID: %s", err)
|
||||
}
|
||||
|
||||
body := services.BuildAPIProductBody(productName, "An agent API import by hgctl", parseTypeToAPIProductType(typ))
|
||||
resp, err := services.HandleAddAPIProduct(client, body)
|
||||
if err != nil {
|
||||
fmt.Println(resp)
|
||||
return err
|
||||
}
|
||||
|
||||
product_id := string(resp)
|
||||
var refBody map[string]interface{}
|
||||
|
||||
if typ == "mcp" {
|
||||
refBody = services.BuildRefMCPAPIProductBody(gatewayId, product_id, name)
|
||||
} else {
|
||||
// target_route is the route_name in Higress, refers to `publishAgentAPIToHigress`
|
||||
target_route := fmt.Sprintf("%s-route", name)
|
||||
refBody = services.BuildRefModelAPIProductBody(gatewayId, product_id, target_route)
|
||||
|
||||
}
|
||||
|
||||
if resp, err := services.HandleRefAPIProduct(client, product_id, refBody); err != nil {
|
||||
fmt.Println(string(resp))
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// use pre-defined command /gen-agent to generate sys prompt
|
||||
func generateAgentPromptByCore(desc string) (string, error) {
|
||||
core := NewAgenticCore()
|
||||
prompt, err := core.runWithResult(fmt.Sprintf("/gen-agent %s", desc), "--print")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return prompt, nil
|
||||
}
|
||||
|
||||
type EnvProvisioner struct {
|
||||
core CoreType
|
||||
installCmd string
|
||||
releasePage string
|
||||
|
||||
// ~/.<core>
|
||||
dirName string
|
||||
}
|
||||
|
||||
func getCore() (*AgenticCore, error) {
|
||||
provisioner := EnvProvisioner{
|
||||
core: CoreType(viper.GetString(HGCTL_AGENT_CORE)),
|
||||
}
|
||||
|
||||
if err := provisioner.check(); err != nil {
|
||||
return nil, fmt.Errorf("⚠️ Prerequisites not satisfied: %s Exiting...", err)
|
||||
}
|
||||
|
||||
return NewAgenticCore(), nil
|
||||
}
|
||||
|
||||
func (p *EnvProvisioner) init() {
|
||||
switch p.core {
|
||||
case CORE_QODERCLI:
|
||||
p.installCmd = "npm install -g @qoder-ai/qodercli"
|
||||
p.releasePage = "https://docs.qoder.com/zh/cli/quick-start"
|
||||
p.dirName = "qoder"
|
||||
|
||||
case CORE_CLAUDE:
|
||||
p.installCmd = "npm install -g @anthropic-ai/claude-code"
|
||||
p.releasePage = "https://docs.claude.com/en/docs/claude-code/setup"
|
||||
p.dirName = "claude"
|
||||
}
|
||||
}
|
||||
|
||||
func (p *EnvProvisioner) check() error {
|
||||
p.init()
|
||||
|
||||
if !p.checkNodeInstall() {
|
||||
if err := p.promptNodeInstall(); err != nil {
|
||||
return err
|
||||
if !checkNodeInstall() {
|
||||
if err := promptNodeInstall(); err != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if !p.checkAgentInstall() {
|
||||
if err := p.promptAgentInstall(); err != nil {
|
||||
return err
|
||||
if !checkAgentInstall() {
|
||||
if err := promptAgentInstall(); err != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *EnvProvisioner) 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 (p *EnvProvisioner) 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 options[0]:
|
||||
fmt.Println()
|
||||
color.Green("🚀 Installing Node.js automatically...")
|
||||
|
||||
if err := p.installNodeAutomatically(); err != nil {
|
||||
color.Red("❌ Installation failed: %v", err)
|
||||
fmt.Println()
|
||||
p.showNodeManualInstallation()
|
||||
return errors.New("node.js installation failed")
|
||||
}
|
||||
|
||||
color.Green("✅ Node.js installation completed!")
|
||||
fmt.Println()
|
||||
color.Blue("🔍 Verifying installation...")
|
||||
|
||||
if p.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 options[1]:
|
||||
p.showNodeManualInstallation()
|
||||
return errors.New("node.js not installed")
|
||||
|
||||
default:
|
||||
return errors.New("invalid selection")
|
||||
}
|
||||
}
|
||||
|
||||
func (p *EnvProvisioner) installNodeAutomatically() error {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not get home directory: %w", err)
|
||||
}
|
||||
|
||||
fnmBinPath := filepath.Join(homeDir, ".local/share/fnm/fnm")
|
||||
if runtime.GOOS == "windows" {
|
||||
fnmBinPath = filepath.Join(homeDir, "AppData/Roaming/fnm/fnm.exe")
|
||||
}
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
color.Cyan("📦 For Windows, we recommend installing fnm via: 'winget install Schniz.fnm'")
|
||||
return errors.New("automatic fnm installation on Windows is not implemented in this script")
|
||||
|
||||
case "darwin", "linux":
|
||||
color.Cyan("🚀 Installing fnm (Fast Node Manager)...")
|
||||
installFnmCmd := exec.Command("bash", "-c", "curl -fsSL https://fnm.vercel.app/install | bash -s -- --skip-shell")
|
||||
installFnmCmd.Stdout = os.Stdout
|
||||
installFnmCmd.Stderr = os.Stderr
|
||||
if err := installFnmCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to install fnm: %w", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(fnmBinPath); os.IsNotExist(err) {
|
||||
path, err := exec.LookPath("fnm")
|
||||
if err == nil {
|
||||
fnmBinPath = path
|
||||
} else {
|
||||
return errors.New("fnm was installed but binary not found at " + fnmBinPath)
|
||||
}
|
||||
}
|
||||
|
||||
color.Cyan("📦 Installing Node.js via fnm...")
|
||||
installNodeCmd := exec.Command(fnmBinPath, "install", "--lts")
|
||||
installNodeCmd.Stdout = os.Stdout
|
||||
installNodeCmd.Stderr = os.Stderr
|
||||
if err := installNodeCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to install node via fnm: %w", err)
|
||||
}
|
||||
|
||||
color.Cyan("✅ Setting LTS as default Node.js version...")
|
||||
useNodeCmd := exec.Command(fnmBinPath, "default", "lts-latest")
|
||||
return useNodeCmd.Run()
|
||||
|
||||
default:
|
||||
return errors.New("unsupported OS for automatic installation")
|
||||
}
|
||||
}
|
||||
|
||||
func (p *EnvProvisioner) 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 (p *EnvProvisioner) checkAgentInstall() bool {
|
||||
cmd := exec.Command(string(p.core), "--version")
|
||||
if err := cmd.Run(); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *EnvProvisioner) promptAgentInstall() error {
|
||||
fmt.Println()
|
||||
color.Yellow("⚠️ %s is not installed or not found in PATH.", p.core)
|
||||
color.Cyan("🔧 %s is required to run the agent.", p.core)
|
||||
fmt.Println()
|
||||
|
||||
options := []string{
|
||||
"🚀 Install automatically",
|
||||
"📖 Exit and show manual installation guide",
|
||||
}
|
||||
|
||||
var ans string
|
||||
prompt := &survey.Select{
|
||||
Message: "How would you like to install " + string(p.core) + "?",
|
||||
Options: options,
|
||||
}
|
||||
if err := survey.AskOne(prompt, &ans); err != nil {
|
||||
return fmt.Errorf("selection error: %w", err)
|
||||
}
|
||||
|
||||
switch ans {
|
||||
case options[0]:
|
||||
fmt.Println()
|
||||
color.Green("🚀 Installing %s automatically...", p.core)
|
||||
|
||||
if err := p.installAgentAutomatically(); err != nil {
|
||||
color.Red("❌ Installation failed: %v", err)
|
||||
fmt.Println()
|
||||
p.showAgentManualInstallation()
|
||||
return errors.New(string(p.core) + " installation failed")
|
||||
}
|
||||
fmt.Println()
|
||||
color.Blue("🔍 Verifying installation...")
|
||||
|
||||
if p.checkAgentInstall() {
|
||||
color.Green("🎉 %s is now available!", p.core)
|
||||
return nil
|
||||
} else {
|
||||
color.Yellow("⚠️ %s installed but not found in PATH.", p.core)
|
||||
color.Cyan("💡 You may need to restart your terminal or source your shell profile.")
|
||||
return errors.New(string(p.core) + " installed but not in PATH")
|
||||
}
|
||||
|
||||
case options[1]:
|
||||
p.showAgentManualInstallation()
|
||||
return errors.New(string(p.core) + " not installed")
|
||||
|
||||
default:
|
||||
return errors.New("invalid selection")
|
||||
}
|
||||
}
|
||||
|
||||
func (p *EnvProvisioner) installAgentAutomatically() error {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
cmd := exec.Command("cmd", "/C", p.installCmd)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
case "darwin":
|
||||
cmd := exec.Command("bash", "-c", p.installCmd)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
case "linux":
|
||||
cmd := exec.Command("bash", "-c", p.installCmd)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
default:
|
||||
return errors.New("unsupported OS for automatic installation")
|
||||
}
|
||||
}
|
||||
|
||||
func (p *EnvProvisioner) showAgentManualInstallation() {
|
||||
fmt.Println()
|
||||
color.New(color.FgGreen, color.Bold).Printf("📖 Manual %s Installation Guide\n", p.core)
|
||||
fmt.Println()
|
||||
|
||||
color.Cyan(fmt.Sprintf("1. Go to official release page: %s", p.releasePage))
|
||||
fmt.Printf(color.CyanString("2. Download %s for your OS\n"), p.core)
|
||||
color.Cyan("3. Make it executable and place it in a directory in your PATH")
|
||||
|
||||
fmt.Println()
|
||||
color.Cyan("💡 After installation, restart your terminal or source your shell profile.")
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
// Copyright (c) 2022 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 common
|
||||
|
||||
// Himarket Product Type
|
||||
type ProductType string
|
||||
|
||||
const (
|
||||
MCP_SERVER ProductType = "MCP_SERVER"
|
||||
MODEL_API ProductType = "MODEL_API"
|
||||
REST_API ProductType = "REST_API"
|
||||
AGENT_API ProductType = "AGENT_API"
|
||||
)
|
||||
@@ -1,129 +0,0 @@
|
||||
// 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"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type CoreType string
|
||||
|
||||
const (
|
||||
CORE_CLAUDE CoreType = "claude"
|
||||
CORE_QODERCLI CoreType = "qodercli"
|
||||
)
|
||||
|
||||
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"
|
||||
|
||||
HGCTL_AGENT_CORE = "hgctl-agent-core"
|
||||
AGENT_MODEL_PROVIDER = "agent-model-provider"
|
||||
AGENT_CHAT_MODEL = "agent-chat-model"
|
||||
HIGRESS_CONSOLE_URL = "higress-console-url"
|
||||
HIGRESS_CONSOLE_USER = "higress-console-user"
|
||||
HIGRESS_CONSOLE_PASSWORD = "higress-console-password"
|
||||
HIGRESS_GATEWAY_URL = "higress-gateway-url"
|
||||
|
||||
HIMARKET_ADMIN_URL = "himarket-admin-url"
|
||||
HIMARKET_ADMIN_USER = "himarket-admin-user"
|
||||
HIMARKET_ADMIN_PASSWORD = "himarket-admin-password"
|
||||
HIMARKET_TARGET_HIGRESS_ID = "himarket-target-higress-id"
|
||||
|
||||
HIMARKET_DEVELOPER_URL = "himarket-developer-url"
|
||||
HIMARKET_DEVELOPER_USER = "himarket-developer-user"
|
||||
HIMARKET_DEVELOPER_PASSWORD = "himarket-developer-password"
|
||||
|
||||
// --- AgentRun ---
|
||||
AGENTRUN_MODEL_NAME = "agentrun-model-name"
|
||||
AGENTRUN_SANDBOX_NAME = "agentrun-sandbox-name"
|
||||
ALIBABA_CLOUD_ACCESS_KEY_ID = "alibaba-cloud-access-key-id"
|
||||
ALIBABA_CLOUD_ACCESS_KEY_SECRET = "alibaba-cloud-access-key-secret"
|
||||
ALIBABA_CLOUD_SECURITY_TOK = "alibaba-cloud-security-tok"
|
||||
AGENTRUN_ACCOUNT_ID = "agentrun-account-id"
|
||||
AGENTRUN_REGION = "agentrun-region"
|
||||
AGENTRUN_SDK_DEB = "agentrun-sdk-deb"
|
||||
)
|
||||
|
||||
var GlobalConfig HgctlAgentConfig
|
||||
|
||||
type HgctlAgentConfig struct {
|
||||
AgenticCore CoreType `mapstructure:"hgctl-agent-core"`
|
||||
AgentChatModel string `mapstructure:"agent-chat-model"`
|
||||
AgentModelProvider string `mapstructure:"agent-model-provider"`
|
||||
|
||||
// Higress Console credentials
|
||||
HigressConsoleURL string `mapstructure:"higress-console-url"`
|
||||
HigressConsoleUser string `mapstructure:"higress-console-user"`
|
||||
HigressConsolePassword string `mapstructure:"higress-console-password"`
|
||||
HigressGatewayURL string `mapstructure:"higress-gateway-url"`
|
||||
// Himarket Admin credentials
|
||||
HimarketAdminURL string `mapstructure:"himarket-admin-url"`
|
||||
HimarketAdminUser string `mapstructure:"himarket-admin-user"`
|
||||
HimarketAdminPassword string `mapstructure:"himarket-admin-password"`
|
||||
HimarketTargetHigressID string `mapstructure:"himarket-target-higress-id"`
|
||||
|
||||
// Himarket Developer credentials
|
||||
HimarketDeveloperURL string `mapstructure:"himarket-developer-url"`
|
||||
HimarketDeveloperUser string `mapstructure:"himarket-developer-user"`
|
||||
HimarketDeveloperPassword string `mapstructure:"himarket-developer-password"`
|
||||
|
||||
// AgentRun Configuration
|
||||
AgentRunModelName string `mapstructure:"agentrun-model-name"`
|
||||
AgentRunSandboxName string `mapstructure:"agentrun-sandbox-name"`
|
||||
AlibabaCloudAccessKeyID string `mapstructure:"alibaba-cloud-access-key-id"`
|
||||
AlibabaCloudAccessKeySecret string `mapstructure:"alibaba-cloud-access-key-secret"`
|
||||
AlibabaCloudSecurityTok string `mapstructure:"alibaba-cloud-security-tok"`
|
||||
AgentRunAccountID string `mapstructure:"agentrun-account-id"`
|
||||
AgentRunRegion string `mapstructure:"agentrun-region"`
|
||||
}
|
||||
|
||||
func InitConfig() {
|
||||
viper.SetConfigName(".hgctl")
|
||||
viper.SetConfigType("json")
|
||||
|
||||
home, err := homedir.Dir()
|
||||
if err != nil {
|
||||
log.Fatalf("Error finding home directory: %v", err)
|
||||
}
|
||||
|
||||
viper.AddConfigPath(home)
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
|
||||
fmt.Fprintf(os.Stderr, "Fatal error reading config file: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Unmarshal into the GlobalConfig variable
|
||||
_ = viper.Unmarshal(&GlobalConfig)
|
||||
|
||||
// Validate supported AgentCore currently
|
||||
switch viper.GetString(HGCTL_AGENT_CORE) {
|
||||
case string(CORE_CLAUDE), string(CORE_QODERCLI):
|
||||
return
|
||||
default:
|
||||
viper.SetDefault(HGCTL_AGENT_CORE, string(CORE_QODERCLI))
|
||||
}
|
||||
}
|
||||
@@ -15,253 +15,32 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/hgctl/pkg/manifests"
|
||||
"github.com/alibaba/higress/hgctl/pkg/util"
|
||||
"github.com/fatih/color"
|
||||
"github.com/manifoldco/promptui"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type AgenticCore struct {
|
||||
binaryName string
|
||||
}
|
||||
|
||||
func NewAgenticCore() *AgenticCore {
|
||||
core := &AgenticCore{
|
||||
binaryName: viper.GetString(HGCTL_AGENT_CORE),
|
||||
}
|
||||
core.Setup()
|
||||
return core
|
||||
}
|
||||
|
||||
func (c *AgenticCore) GetPromptFileName() string {
|
||||
switch c.binaryName {
|
||||
case string(CORE_CLAUDE):
|
||||
return "CLAUDE.md"
|
||||
case string(CORE_QODERCLI):
|
||||
return "AGENTS.md"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *AgenticCore) GetCoreDirName() string {
|
||||
switch c.binaryName {
|
||||
case string(CORE_CLAUDE):
|
||||
return ".claude"
|
||||
case string(CORE_QODERCLI):
|
||||
return ".qoder"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// This will use core to test and improve created agent
|
||||
func (c *AgenticCore) ImproveNewAgent(config *AgentConfig) error {
|
||||
agentDir, err := util.GetSpecificAgentDir(config.AgentName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get agent directory: %s", agentDir)
|
||||
}
|
||||
return c.runInTargetDir(agentDir)
|
||||
}
|
||||
|
||||
func (c *AgenticCore) runInTargetDir(dir string, args ...string) error {
|
||||
cmd := exec.Command(c.binaryName, args...)
|
||||
cmd.Dir = dir
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
|
||||
}
|
||||
|
||||
func (c *AgenticCore) runWithResult(args ...string) (string, error) {
|
||||
cmd := exec.Command(c.binaryName, args...)
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
return "", fmt.Errorf("agent execution failed with exit code %d: %s\nStderr: %s",
|
||||
exitErr.ExitCode(), err.Error(), exitErr.Stderr)
|
||||
}
|
||||
return "", fmt.Errorf("failed to run agent: %w", err)
|
||||
}
|
||||
|
||||
return string(output), nil
|
||||
return &AgenticCore{}
|
||||
}
|
||||
|
||||
func (c *AgenticCore) run(args ...string) error {
|
||||
cmd := exec.Command(c.binaryName, args...)
|
||||
cmd := exec.Command(AgentBinaryName, args...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// setup additional prequisite environment and plugins manifest to user's profile
|
||||
// e.g. ../manifest/agent
|
||||
func (c *AgenticCore) Setup() {
|
||||
// Check if this is the first time, otherwise directly return (TODO: this is a simple check)
|
||||
homeDir, _ := os.UserHomeDir()
|
||||
targetCtlDir := filepath.Join(homeDir, ".hgctl")
|
||||
if _, err := os.Stat(targetCtlDir); err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
targetCoreDir := filepath.Join(homeDir, c.GetCoreDirName())
|
||||
|
||||
// setup subagent plugins file
|
||||
embedFS := manifests.BuiltinOrDir("")
|
||||
if err := manifests.ExtractEmbedFiles(embedFS, "agent", targetCtlDir); err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Println("failed to init plugins for agent core")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Setup predefined files like: command.md
|
||||
if err := manifests.ExtractEmbedFiles(embedFS, "agent", targetCoreDir); err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Println("failed to init commands for agent core")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Add Predefined MCP Server
|
||||
if err := c.addPredefinedMCP(); err != nil {
|
||||
fmt.Printf("Warning: failed to add needed mcp server: %s\n", err)
|
||||
}
|
||||
|
||||
if err := c.addHigressAPIMCP(); err != nil {
|
||||
fmt.Println("failed to init higress-api mcp server (you may need to add it manually):", err)
|
||||
fmt.Println("Details information on Higress-api MCP server refers to https://github.com/alibaba/higress/blob/main/plugins/golang-filter/mcp-server/servers/higress/higress-api/README_en.md")
|
||||
return
|
||||
}
|
||||
// fmt.Println("Higress-api MCP server added successfully")
|
||||
}
|
||||
|
||||
func (c *AgenticCore) addPredefinedMCP() error {
|
||||
// deepwikiArg := MCPAddArg{
|
||||
// name: "deepwiki",
|
||||
// url: "https://mcp.deepwiki.com/mcp",
|
||||
// typ: "",
|
||||
// transport: STREAMABLE,
|
||||
// scope: "user",
|
||||
// }
|
||||
// if err := c.AddMCPServer(deepwikiArg); err != nil {
|
||||
// return fmt.Errorf("deepwiki")
|
||||
// }
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *AgenticCore) addHigressAPIMCP() error {
|
||||
arg := &HigressConsoleAuthArg{
|
||||
hgURL: viper.GetString(HIGRESS_GATEWAY_URL),
|
||||
hgUser: viper.GetString(HIGRESS_CONSOLE_USER),
|
||||
hgPassword: viper.GetString(HIGRESS_CONSOLE_PASSWORD),
|
||||
}
|
||||
fmt.Println("Initializing...Add prequisite MCP server (Higress-api MCP server) automatically")
|
||||
|
||||
if arg.hgURL == "" {
|
||||
gatewayPrompt := promptui.Prompt{
|
||||
Label: "Enter higress gateway URL",
|
||||
Default: "http://127.0.0.1:80",
|
||||
}
|
||||
gateway, err := gatewayPrompt.Run()
|
||||
if err != nil {
|
||||
fmt.Println("failed to run gateway prompt: ", err)
|
||||
return err
|
||||
}
|
||||
arg.hgURL = gateway
|
||||
|
||||
}
|
||||
|
||||
if arg.hgURL == "" || arg.hgPassword == "" {
|
||||
if err := tryToGetLocalCredential(arg); err != nil || arg.hgUser == "" || arg.hgPassword == "" {
|
||||
// fallback: interact with user to provide password & username
|
||||
color.Red("failed to get higress-console credential automatically (Requires higress installed by hgctl). Let's do it manually")
|
||||
userPrompt := promptui.Prompt{
|
||||
Label: "Enter higress console username",
|
||||
Default: "admin",
|
||||
}
|
||||
username, err := userPrompt.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("aborted: %v", err)
|
||||
}
|
||||
pwdPrompt := promptui.Prompt{
|
||||
Label: "Enter higress console password",
|
||||
Default: "admin",
|
||||
}
|
||||
pwd, err := pwdPrompt.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("aborted: %v", err)
|
||||
}
|
||||
arg.hgUser = username
|
||||
arg.hgPassword = pwd
|
||||
}
|
||||
}
|
||||
|
||||
if arg.hgUser == "" || arg.hgPassword == "" {
|
||||
return fmt.Errorf("Empty higress console username and password, aborting")
|
||||
}
|
||||
|
||||
rawByte := fmt.Appendf(nil, "%s:%s", arg.hgUser, arg.hgPassword)
|
||||
|
||||
resStr := base64.StdEncoding.EncodeToString(rawByte)
|
||||
|
||||
authHeader := fmt.Sprintf("Authorization: Basic %s", resStr)
|
||||
|
||||
return c.AddMCPServer(MCPAddArg{
|
||||
name: "higress-api",
|
||||
url: fmt.Sprintf("%s/higress-api", arg.hgURL),
|
||||
transport: HTTP,
|
||||
typ: HTTP,
|
||||
scope: "user",
|
||||
header: []string{
|
||||
authHeader,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ------- Initialization -------
|
||||
func (c *AgenticCore) Start() error {
|
||||
return c.run()
|
||||
return c.run(AgentBinaryName)
|
||||
}
|
||||
|
||||
// ------- MCP -------
|
||||
func (c *AgenticCore) AddMCPServer(arg MCPAddArg) error {
|
||||
// adapt the field
|
||||
if arg.transport == STREAMABLE {
|
||||
arg.transport = HTTP
|
||||
}
|
||||
args := []string{
|
||||
"mcp", "add", "--transport", arg.transport, arg.name, arg.url,
|
||||
}
|
||||
if arg.scope != "" {
|
||||
scopeArg := []string{"--scope", arg.scope}
|
||||
args = append(args, scopeArg...)
|
||||
}
|
||||
if len(arg.env) != 0 {
|
||||
for _, e := range arg.env {
|
||||
envArg := []string{"-e", e}
|
||||
args = append(args, envArg...)
|
||||
}
|
||||
}
|
||||
if len(arg.header) != 0 {
|
||||
for _, h := range arg.header {
|
||||
headerArg := []string{"-H", h}
|
||||
args = append(args, headerArg...)
|
||||
}
|
||||
}
|
||||
err := c.run(args...)
|
||||
|
||||
// Allow to add duplicate mcp server name (core will return error)
|
||||
if err == nil || strings.Contains(err.Error(), "already exists") {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
func (c *AgenticCore) AddMCPServer(name string, url string) error {
|
||||
return c.run("mcp", "add", "--transport", HTTP, name, url)
|
||||
}
|
||||
|
||||
@@ -1,406 +0,0 @@
|
||||
// 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"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/hgctl/pkg/util"
|
||||
"github.com/spf13/cobra"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
)
|
||||
|
||||
type DeployType string
|
||||
|
||||
const (
|
||||
AgentRun DeployType = "agent-run"
|
||||
Local DeployType = "local"
|
||||
)
|
||||
|
||||
var (
|
||||
AddAccessKeyCmd = fmt.Sprintf("s config add -a %s", DefaultServerLessAccessKey)
|
||||
CheckAccessKeyCmd = fmt.Sprintf("s config get -a %s", DefaultServerLessAccessKey)
|
||||
DeployAgentRunCmd = fmt.Sprintf("s deploy -a %s", DefaultServerLessAccessKey)
|
||||
)
|
||||
|
||||
const (
|
||||
InstallServerlessCmd = "npm install @serverless-devs/s -g"
|
||||
BuildAgentCmd = "s build"
|
||||
ServerlessCliDocs = "https://serverless-devs.com/docs/user-guide/install"
|
||||
)
|
||||
|
||||
type DeployHandler struct {
|
||||
Name string
|
||||
AgentDir string
|
||||
Type DeployType
|
||||
}
|
||||
|
||||
func deployAgentCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "deploy [name]",
|
||||
Short: "Deploy the specified agent locally or to the cloud",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
handler := &DeployHandler{
|
||||
Name: args[0],
|
||||
}
|
||||
cmdutil.CheckErr(handler.Deploy())
|
||||
},
|
||||
}
|
||||
|
||||
var cloud = false
|
||||
cmd.PersistentFlags().BoolVar(&cloud, "agentrun", false, "deploy agent using agentrun")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (h *DeployHandler) validate() error {
|
||||
if err := h.checkRequiredEnvironment(); err != nil {
|
||||
return fmt.Errorf("failed to get required environment: %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *DeployHandler) RunCmd(showOutput bool, cmd string, targetDir string) (string, error) {
|
||||
runCmd := exec.Command("bash", "-c", cmd)
|
||||
|
||||
if targetDir != "" {
|
||||
runCmd.Dir = targetDir
|
||||
}
|
||||
|
||||
if showOutput {
|
||||
runCmd.Stderr = os.Stderr
|
||||
runCmd.Stdout = os.Stdout
|
||||
if err := runCmd.Run(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
output, err := runCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(output), nil
|
||||
}
|
||||
|
||||
func (h *DeployHandler) RunPythonCmd(showOutput bool, args ...string) error {
|
||||
cmd := exec.Command("python3", args...)
|
||||
|
||||
if showOutput {
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = os.Stdout
|
||||
}
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *DeployHandler) checkAgentRunEnvironment() error {
|
||||
if _, err := h.RunCmd(false, "s --version", ""); err != nil {
|
||||
fmt.Println("Serverless dev cli not installed, install it automatically..")
|
||||
if _, err := h.RunCmd(true, InstallServerlessCmd, ""); err != nil {
|
||||
return fmt.Errorf("failed to install serverless dev cli automatically, details refers to %s", ServerlessCliDocs)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := h.RunCmd(false, "docker --version", ""); err != nil {
|
||||
return fmt.Errorf("docker is required to deploy agent to agentRun: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *DeployHandler) checkLocalEnvironment() error {
|
||||
pyVenv, err := util.GetPythonVersion()
|
||||
if err != nil {
|
||||
fmt.Printf("Python environment not found, you need Python environment to run your agent\n")
|
||||
return err
|
||||
}
|
||||
|
||||
if util.CompareVersions(pyVenv, MinPythonVersion) == -1 {
|
||||
fmt.Printf("Current Python: %s need Python %s+", MinPythonVersion, pyVenv)
|
||||
return fmt.Errorf("unsupport python version")
|
||||
}
|
||||
|
||||
missingDeps := []string{}
|
||||
if err := h.RunPythonCmd(false, "-c", "import agentscope; print(agentscope.__version__)"); err != nil {
|
||||
missingDeps = append(missingDeps, "agentscope")
|
||||
}
|
||||
|
||||
if err := h.RunPythonCmd(false, "-c", "import agentscope_runtime; print(agentscope_runtime.__version__)"); err != nil {
|
||||
missingDeps = append(missingDeps, "agentscope-runtime==1.0.0")
|
||||
}
|
||||
|
||||
if len(missingDeps) != 0 {
|
||||
venvDir := filepath.Join(util.GetHomeHgctlDir(), ".venv")
|
||||
if _, err := os.Stat(venvDir); err == nil {
|
||||
// check again
|
||||
missingDeps := []string{}
|
||||
if err := h.RunPythonCmd(false, "-c", "import agentscope; print(agentscope.__version__)"); err != nil {
|
||||
fmt.Println("agentscope not installed, installing...")
|
||||
missingDeps = append(missingDeps, "agentscope")
|
||||
}
|
||||
if err := h.RunPythonCmd(false, "-c", "import agentscope_runtime; print(agentscope_runtime.__version__)"); err != nil {
|
||||
fmt.Println("agentscope-runtime not installed, installing...")
|
||||
missingDeps = append(missingDeps, "agentscope-runtime==1.0.0")
|
||||
}
|
||||
// This means ~/.hgctl/.venv/ has already installed the deps before
|
||||
if len(missingDeps) == 0 {
|
||||
if err := h.activateLocalPythonVenv(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := h.installLocalRequiredDeps(missingDeps); err != nil {
|
||||
return fmt.Errorf("failed to install missing deps: %s", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *DeployHandler) createLocalPyVenv() error {
|
||||
venvDir := filepath.Join(util.GetHomeHgctlDir(), ".venv")
|
||||
cmd := exec.Command("python3", "-m", "venv", venvDir)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
fmt.Println("failed to create python virtual environment", string(output))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *DeployHandler) installLocalRequiredDeps(missingDeps []string) error {
|
||||
if err := h.RunPythonCmd(true, "-m", "pip", "--version"); err != nil {
|
||||
fmt.Printf("Pip not installed, you need install pip to deploy your agent\n")
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("This may takes a few minutes, you can install missing deps by yourself: ")
|
||||
for _, deps := range missingDeps {
|
||||
fmt.Println("- ", deps)
|
||||
}
|
||||
|
||||
if err := h.createLocalPyVenv(); err != nil {
|
||||
return fmt.Errorf("failed to create local venv (~/.hgctl/.venv): %s", err)
|
||||
}
|
||||
|
||||
if err := h.activateLocalPythonVenv(); err != nil {
|
||||
return fmt.Errorf("failed to activateLocalPythonVenv: %s", err)
|
||||
}
|
||||
|
||||
for _, deps := range missingDeps {
|
||||
if err := h.RunPythonCmd(true, "-m", "pip", "install", deps); err != nil {
|
||||
fmt.Printf("failed to install missing deps: %s\n", deps)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
venvDir := filepath.Join(util.GetHomeHgctlDir(), ".venv")
|
||||
fmt.Println("Missing deps installed successfully, target python venv path: ", venvDir)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *DeployHandler) activateLocalPythonVenv() error {
|
||||
venvDir := filepath.Join(util.GetHomeHgctlDir(), ".venv")
|
||||
path := os.Getenv("PATH")
|
||||
newPath := venvDir + "/bin:" + path
|
||||
err := os.Setenv("PATH", newPath)
|
||||
if err != nil {
|
||||
fmt.Println("Failed to set PATH:", err)
|
||||
return err
|
||||
}
|
||||
err = os.Setenv("VIRTUAL_ENV", venvDir)
|
||||
if err != nil {
|
||||
fmt.Println("Failed to set VIRTUAL_ENV:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *DeployHandler) checkRequiredEnvironment() error {
|
||||
if h.Type == AgentRun {
|
||||
return h.checkAgentRunEnvironment()
|
||||
}
|
||||
|
||||
if h.Type == Local {
|
||||
return h.checkLocalEnvironment()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *DeployHandler) GetRequiredDeps() ([]string, error) {
|
||||
switch h.Type {
|
||||
case AgentRun:
|
||||
return []string{
|
||||
"agentrun-sdk[agentscope,server] >= 0.0.3",
|
||||
}, nil
|
||||
case Local:
|
||||
return []string{
|
||||
"agentscope", "agentscope-runtime==1.0.0",
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported deploy target type: %s", h.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// Quick and simple to get type by examine the existence of `requirements.txt` file
|
||||
func (h *DeployHandler) getAgentType() error {
|
||||
path, err := util.GetSpecificAgentDir(h.Name)
|
||||
if err != nil {
|
||||
fmt.Printf("invalid agent: %s", err)
|
||||
return err
|
||||
}
|
||||
h.AgentDir = path
|
||||
|
||||
filePath := filepath.Join(h.AgentDir, "requirements.txt")
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
h.Type = Local
|
||||
return nil
|
||||
}
|
||||
h.Type = AgentRun
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *DeployHandler) Deploy() error {
|
||||
if err := h.getAgentType(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := h.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch h.Type {
|
||||
case AgentRun:
|
||||
if err := h.HandleAgentRun(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case Local:
|
||||
if err := h.HandleLocal(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unsupported deploy target type: %s", h.Type)
|
||||
}
|
||||
|
||||
if h.Type == AgentRun {
|
||||
fmt.Printf("\n🌟 Agent deploy to agentRun successfully! Refers to https://functionai.console.aliyun.com/cn-hangzhou/agent/runtime to get it")
|
||||
fmt.Printf("You can publish it to Higress and Himarket by using `hgctl agent add %s <endpoints-url> -t model --as-product `\n", h.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// details see: https://github.com/Serverless-Devs/agentrun-sdk-python
|
||||
func (h *DeployHandler) HandleAgentRun() error {
|
||||
if err := h.CheckServerlessAccessKey(); err != nil {
|
||||
return fmt.Errorf("failed to set access key automatically: %s", err)
|
||||
}
|
||||
|
||||
if _, err := h.RunCmd(true, BuildAgentCmd, h.AgentDir); err != nil {
|
||||
return fmt.Errorf("failed to build agent: %s", err)
|
||||
}
|
||||
|
||||
if _, err := h.RunCmd(true, DeployAgentRunCmd, h.AgentDir); err != nil {
|
||||
return fmt.Errorf("failed to deploy agent: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set Serverless's Access Key in s.yaml, details see: https://github.com/Serverless-Devs/agentrun-sdk-python
|
||||
// Example:
|
||||
// $ s config get -a defualt
|
||||
|
||||
// You have not yet been found to have configured key information.
|
||||
// You can use [s config add] for key configuration, or use [s config add -h] to view configuration help.
|
||||
// If you already used [s config add], please check the permission of file [{HOMEPATH}/.s/access.yaml].
|
||||
// If you have questions, please tell us: https://github.com/Serverless-Devs/Serverless-Devs/issues
|
||||
//
|
||||
// s version: @serverless-devs/s: 3.1.10
|
||||
func (h *DeployHandler) CheckServerlessAccessKey() error {
|
||||
notFoundMessage := "You have not yet been found to have configured key information"
|
||||
output, err := h.RunCmd(false, CheckAccessKeyCmd, "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run %s command to check access key: %s", CheckAccessKeyCmd, err)
|
||||
}
|
||||
if strings.Contains(output, notFoundMessage) {
|
||||
fmt.Fprintf(os.Stderr, `
|
||||
🔑 **ACTION REQUIRED**: Please configure your Alibaba Cloud credentials first.
|
||||
Copy and run the command below to set up your Access Key:
|
||||
> %s
|
||||
|
||||
`, AddAccessKeyCmd)
|
||||
return fmt.Errorf("access key not found")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *DeployHandler) HandleLocal() error {
|
||||
if _, err := os.Stat(h.AgentDir); os.IsNotExist(err) {
|
||||
return fmt.Errorf("agent source file not found: %s", h.AgentDir)
|
||||
}
|
||||
|
||||
if err := h.startAgentProcess(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *DeployHandler) startAgentProcess() error {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
return h.runWindowsAgent()
|
||||
case "darwin", "linux":
|
||||
return h.runUnixAgent()
|
||||
default:
|
||||
return fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *DeployHandler) runUnixAgent() error {
|
||||
agentFile := filepath.Join(h.AgentDir, ASRuntimeMainPyFile)
|
||||
if err := h.RunPythonCmd(true, agentFile); err != nil {
|
||||
fmt.Println("failed to start agent, exiting...")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *DeployHandler) runWindowsAgent() error {
|
||||
agentFile := filepath.Join(h.AgentDir, ASRuntimeMainPyFile)
|
||||
if err := h.RunPythonCmd(true, agentFile); err != nil {
|
||||
fmt.Println("failed to start agent, exiting...")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -17,8 +17,10 @@ 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"
|
||||
@@ -32,30 +34,32 @@ import (
|
||||
type MCPType string
|
||||
|
||||
const (
|
||||
OPENAPI string = "openapi"
|
||||
HTTP string = "http"
|
||||
|
||||
STREAMABLE string = "streamable"
|
||||
SSE string = "sse"
|
||||
|
||||
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 {
|
||||
HigressConsoleAuthArg
|
||||
HimarketAdminAuthArg
|
||||
// higress console auth arg
|
||||
baseURL string
|
||||
hgUser string
|
||||
hgPassword string
|
||||
|
||||
name string
|
||||
url string
|
||||
typ string
|
||||
transport string
|
||||
spec string
|
||||
scope string
|
||||
env []string
|
||||
header []string
|
||||
noPublish bool
|
||||
asProduct bool
|
||||
// TODO: support mcp env
|
||||
// env string
|
||||
|
||||
}
|
||||
|
||||
type MCPAddHandler struct {
|
||||
@@ -80,45 +84,24 @@ func newMCPAddCmd() *cobra.Command {
|
||||
arg := &MCPAddArg{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "add [name] [url]",
|
||||
Use: "add [name]",
|
||||
Short: "add mcp server including http and openapi",
|
||||
Example: ` # Add HTTP type MCP Server
|
||||
hgctl mcp add http-mcp http://localhost:8080/mcp
|
||||
|
||||
# Add MCP Server with environment variables and headers
|
||||
hgctl mcp add http-mcp http://localhost:8080/mcp -e API_KEY=secret -H "Authorization: Bearer token"
|
||||
|
||||
# Add MCP Server use Openapi file
|
||||
hgctl mcp add swagger-mcp ./path/to/openapi.yaml --type openapi`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
arg.name = args[0]
|
||||
if arg.typ == HTTP {
|
||||
arg.url = args[1]
|
||||
} else {
|
||||
arg.spec = args[1]
|
||||
}
|
||||
|
||||
resolveHigressConsoleAuth(&arg.HigressConsoleAuthArg)
|
||||
resolveHimarketAdminAuth(&arg.HimarketAdminAuthArg)
|
||||
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 using Higress MCP Server and connection failed")
|
||||
color.Cyan("Tip: Try doing 'kubectl port-forward' and add the server to the agent manually, if MCP Server connection failed")
|
||||
},
|
||||
Args: cobra.ExactArgs(2),
|
||||
Args: cobra.ExactArgs(1),
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().StringVar(&arg.typ, "type", HTTP, "Determine the MCP Server's Type")
|
||||
cmd.PersistentFlags().StringVarP(&arg.transport, "transport", "t", STREAMABLE, `The MCP Server's transport`)
|
||||
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().StringSliceVarP(&arg.env, "env", "e", nil, "Environment variables to pass to the MCP server (can be specified multiple times)")
|
||||
cmd.PersistentFlags().StringSliceVarP(&arg.header, "header", "H", nil, "HTTP headers to pass to the MCP server (can be specified multiple times)")
|
||||
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")
|
||||
cmd.PersistentFlags().BoolVar(&arg.asProduct, "as-product", false, "If it's set then the agent API will be published to Himarket (no-publish must be false)")
|
||||
|
||||
// cmd.PersistentFlags().StringVar(&arg.spec, "spec", "", "Specification file (yaml/json) of the openapi api")
|
||||
|
||||
addHigressConsoleAuthFlag(cmd, &arg.HigressConsoleAuthArg)
|
||||
addHimarketAdminAuthFlag(cmd, &arg.HimarketAdminAuthArg)
|
||||
addHigressConsoleAuthFlag(cmd, arg)
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -129,19 +112,22 @@ func newHanlder(c *AgenticCore, arg MCPAddArg, w io.Writer) *MCPAddHandler {
|
||||
|
||||
func (h *MCPAddHandler) validateArg() error {
|
||||
if !h.arg.noPublish {
|
||||
return h.arg.HigressConsoleAuthArg.validate()
|
||||
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); err != nil {
|
||||
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 publishMCPToHigress(h.arg, h.arg.typ, nil)
|
||||
return publishToHigress(h.arg, nil)
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -151,29 +137,23 @@ func (h *MCPAddHandler) addHTTPMCP() error {
|
||||
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()
|
||||
config.Server.SecuritySchemes[0].DefaultCredential = "b5b9752c7ad2cb9c6b19fb5fd6a23be8852eca9c"
|
||||
|
||||
// fmt.Printf("get config struct: %v", config)
|
||||
|
||||
// publish to higress
|
||||
if err := publishMCPToHigress(h.arg, "streamable", config); err != nil {
|
||||
if err := publishToHigress(h.arg, config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add mcp server to agent
|
||||
gatewayURL := viper.GetString(HIGRESS_GATEWAY_URL)
|
||||
if gatewayURL == "" {
|
||||
svcIP, 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
|
||||
}
|
||||
gatewayURL = svcIP
|
||||
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("%s/mcp-servers/%s", gatewayURL, h.arg.name)
|
||||
h.arg.url = mcpURL
|
||||
return h.core.AddMCPServer(h.arg)
|
||||
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 {
|
||||
@@ -181,10 +161,7 @@ func (h *MCPAddHandler) parseOpenapiSpec() *models.MCPConfig {
|
||||
}
|
||||
|
||||
func handleAddMCP(w io.Writer, arg MCPAddArg) error {
|
||||
client, err := getCore()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get agent core: %s", err)
|
||||
}
|
||||
client := getAgent()
|
||||
h := newHanlder(client, arg, w)
|
||||
if err := h.validateArg(); err != nil {
|
||||
return err
|
||||
@@ -192,12 +169,9 @@ func handleAddMCP(w io.Writer, arg MCPAddArg) error {
|
||||
|
||||
// spec -> OPENAPI
|
||||
// noPublish -> typ
|
||||
switch arg.typ {
|
||||
switch arg.transport {
|
||||
case HTTP:
|
||||
if err := h.addHTTPMCP(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.addHTTPMCP()
|
||||
case OPENAPI:
|
||||
if arg.spec == "" {
|
||||
return fmt.Errorf("--spec is required for openapi type")
|
||||
@@ -208,29 +182,19 @@ func handleAddMCP(w io.Writer, arg MCPAddArg) error {
|
||||
if arg.url != "" {
|
||||
return fmt.Errorf("--url is not supported for openapi type")
|
||||
}
|
||||
if err := h.addOpenAPIMCP(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.addOpenAPIMCP()
|
||||
default:
|
||||
return fmt.Errorf("unsupported mcp type")
|
||||
}
|
||||
|
||||
if !arg.noPublish && arg.asProduct {
|
||||
if err := publishAPIToHimarket("mcp", arg.name, arg.HimarketAdminAuthArg); err != nil {
|
||||
fmt.Println("failed to publish it to himarket, please do it mannually")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func publishMCPToHigress(arg MCPAddArg, transport string, config *models.MCPConfig) error {
|
||||
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.hgURL, arg.hgUser, arg.hgPassword)
|
||||
client := services.NewHigressClient(arg.baseURL, arg.hgUser, arg.hgPassword)
|
||||
|
||||
// mcp server's url
|
||||
rawURL := arg.url
|
||||
// DIRECT_ROUTE or OPEN_API
|
||||
mcpType := DIRECT_ROUTE
|
||||
@@ -241,46 +205,61 @@ func publishMCPToHigress(arg MCPAddArg, transport string, config *models.MCPConf
|
||||
mcpType = OPEN_API
|
||||
}
|
||||
|
||||
srvName := fmt.Sprintf("hgctl-%s", arg.name)
|
||||
|
||||
// e.g. hgctl-mcp-deepwiki.dns
|
||||
body, targetSrvName, port, err := services.BuildServiceBodyAndSrv(srvName, rawURL)
|
||||
res, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid url format: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := services.HandleAddServiceSource(client, body)
|
||||
// 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 fmt.Errorf("response body: %s %s\n", string(resp), err)
|
||||
return err
|
||||
}
|
||||
|
||||
srvField := []map[string]interface{}{{
|
||||
"name": targetSrvName,
|
||||
"port": port,
|
||||
"name": fmt.Sprintf("%s.%s", srvName, srvType),
|
||||
"port": srvPort,
|
||||
"version": "1.0",
|
||||
"weight": 100,
|
||||
}}
|
||||
|
||||
body = map[string]interface{}{
|
||||
"name": arg.name,
|
||||
"description": "A MCP Server added by hgctl",
|
||||
"type": mcpType,
|
||||
"services": srvField,
|
||||
"domains": []interface{}{},
|
||||
"consumerAuthInfo": map[string]interface{}{
|
||||
"type": "key-auth",
|
||||
"allowedConsumers": []string{},
|
||||
},
|
||||
// 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,
|
||||
}
|
||||
|
||||
// Only DIRECT_ROUTE Type get below extra params
|
||||
if mcpType == DIRECT_ROUTE {
|
||||
res, _ := url.Parse(rawURL)
|
||||
body["directRouteConfig"] = map[string]interface{}{
|
||||
"path": res.Path,
|
||||
"transportType": arg.transport,
|
||||
}
|
||||
}
|
||||
// fmt.Printf("request body: %v", body)
|
||||
|
||||
_, err = services.HandleAddMCPServer(client, body)
|
||||
if err != nil {
|
||||
@@ -296,17 +275,12 @@ func publishMCPToHigress(arg MCPAddArg, transport string, config *models.MCPConf
|
||||
|
||||
func addMCPToolConfig(client *services.HigressClient, config *models.MCPConfig, srvField []map[string]interface{}) {
|
||||
body := map[string]interface{}{
|
||||
"name": config.Server.Name,
|
||||
"description": "A MCP Server added by hgctl",
|
||||
"name": config.Server.Name,
|
||||
// "description": "",
|
||||
"services": srvField,
|
||||
"type": OPEN_API,
|
||||
"rawConfigurations": convertMCPConfigToStr(config),
|
||||
"mcpServerName": config.Server.Name,
|
||||
"domains": []interface{}{},
|
||||
"consumerAuthInfo": map[string]interface{}{
|
||||
"type": "key-auth",
|
||||
"allowedConsumers": []string{},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := services.HandleAddOpenAPITool(client, body)
|
||||
@@ -317,7 +291,38 @@ func addMCPToolConfig(client *services.HigressClient, config *models.MCPConfig,
|
||||
// fmt.Println("get openapi tools add response: ", string(resp))
|
||||
}
|
||||
|
||||
func tryToGetLocalCredential(arg *HigressConsoleAuthArg) error {
|
||||
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
|
||||
|
||||
@@ -1,341 +0,0 @@
|
||||
// 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/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/alibaba/higress/hgctl/pkg/agent/prompt"
|
||||
"github.com/alibaba/higress/hgctl/pkg/manifests"
|
||||
"github.com/alibaba/higress/hgctl/pkg/util"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
const (
|
||||
ASRuntimeMainPyFile = "as_runtime_main.py"
|
||||
AgentRunMainPyFile = "agentrun_main.py"
|
||||
ToolKitPyFile = "toolkit.py"
|
||||
AgentClassFile = "agent.py"
|
||||
CorePromptFile = "claude.md" // TODO: support qoder AGENTS.md
|
||||
SConfigYAML = "s.yaml"
|
||||
|
||||
ARTemplate = "agentrun.tmpl"
|
||||
ASTemplate = "agentscope.tmpl"
|
||||
AgentClassTemplate = "agent.tmpl"
|
||||
ToolKitTemplate = "toolkit.tmpl"
|
||||
SConfigTemplate = "agentrun_s.tmpl"
|
||||
)
|
||||
|
||||
var ASAvailiableTools = []string{
|
||||
"execute_python_code",
|
||||
"execute_shell_command",
|
||||
"view_text_file",
|
||||
"write_text_file",
|
||||
"insert_text_file",
|
||||
"dashscope_text_to_image",
|
||||
"dashscope_text_to_audio",
|
||||
"dashscope_image_to_text",
|
||||
"openai_text_to_image",
|
||||
"openai_text_to_audio",
|
||||
"openai_edit_image",
|
||||
"openai_create_image_variation",
|
||||
"openai_image_to_text",
|
||||
"openai_audio_to_text",
|
||||
}
|
||||
|
||||
const (
|
||||
MinPythonVersion = "3.12"
|
||||
|
||||
DefaultServerLessAccessKey = "hgctl-credential"
|
||||
)
|
||||
|
||||
// Callback type for post-agent-creation actions
|
||||
type PostAgentAction func(config *AgentConfig) error
|
||||
|
||||
type MCPServerConfig struct {
|
||||
Name string // MCP Client Name
|
||||
URL string // MCP Server URL
|
||||
Transport string // transport `streamable_http` or `see` or `stdio`
|
||||
Headers map[string]string // HTTP Headers
|
||||
}
|
||||
|
||||
type ServerlessConfig struct {
|
||||
AccessKey string
|
||||
ResourceName string
|
||||
Region string
|
||||
AgentName string
|
||||
AgentDesc string
|
||||
Port uint
|
||||
|
||||
DiskSize uint
|
||||
Timeout uint
|
||||
|
||||
GlobalConfig HgctlAgentConfig
|
||||
}
|
||||
|
||||
type AgentConfig struct {
|
||||
AppName string // "app"
|
||||
AppDescription string // "A helpful assistant and useful agent"
|
||||
AgentName string // "Friday"
|
||||
AvailableTools []string // availiable tools (built-in agentscope)
|
||||
SysPromptPath string // "You are a helpful assistant"
|
||||
ChatModel string // "qwen-max"
|
||||
Provider string // "Aliyun"
|
||||
APIKeyEnvVar string // DASHCOPE_API_KEY
|
||||
DeploymentPort int // 8090
|
||||
HostBinding string // 0.0.0.0
|
||||
EnableStreaming bool // true
|
||||
EnableThinking bool // true
|
||||
MCPServers []MCPServerConfig
|
||||
|
||||
Type DeployType
|
||||
ServerlessCfg ServerlessConfig
|
||||
}
|
||||
|
||||
func createAgentCmd() *cobra.Command {
|
||||
agentRun := false
|
||||
deployDirect := false
|
||||
|
||||
var createAgentCmd = &cobra.Command{
|
||||
Use: "new",
|
||||
Short: "Create a new agent or import one from core",
|
||||
Args: cobra.ExactArgs(0),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
config := &AgentConfig{
|
||||
Type: Local,
|
||||
}
|
||||
if agentRun {
|
||||
config.Type = AgentRun
|
||||
config.ServerlessCfg = ServerlessConfig{
|
||||
AccessKey: DefaultServerLessAccessKey,
|
||||
Port: 9000,
|
||||
DiskSize: 512,
|
||||
Timeout: 600,
|
||||
|
||||
GlobalConfig: GlobalConfig,
|
||||
}
|
||||
}
|
||||
|
||||
if err := getAgentConfig(config); err != nil {
|
||||
fmt.Printf("Error get Agent config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := createAgentTemplate(config); err != nil {
|
||||
fmt.Printf("Error creating agent: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := afterCreatedAgent(config); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
createAgentCmd.PersistentFlags().BoolVar(&agentRun, "agent-run", false, "Use agentRun to deploy to Alibaba cloud, default is false")
|
||||
createAgentCmd.PersistentFlags().BoolVar(&deployDirect, "deploy", false, "After agent creation, deploy it directly")
|
||||
return createAgentCmd
|
||||
|
||||
}
|
||||
|
||||
func afterCreatedAgent(config *AgentConfig) error {
|
||||
options := []string{
|
||||
"Deploy it directly",
|
||||
fmt.Sprintf("Improve and test it using agentic core (%s)", viper.GetString(HGCTL_AGENT_CORE)),
|
||||
"Do nothing and quit",
|
||||
}
|
||||
callbacks := map[string]PostAgentAction{
|
||||
options[0]: func(cfg *AgentConfig) error {
|
||||
handler := &DeployHandler{Name: cfg.AgentName}
|
||||
return handler.Deploy()
|
||||
},
|
||||
options[1]: func(cfg *AgentConfig) error {
|
||||
return runAgenticCoreImprovement(cfg)
|
||||
},
|
||||
}
|
||||
|
||||
if err := promptAfterCreatedAgent(options, config, callbacks); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to handle post-creation action: %v\n", err)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runAgenticCoreImprovement(cfg *AgentConfig) error {
|
||||
core, err := getCore()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to invoke agent core: %s", err)
|
||||
}
|
||||
|
||||
if err := core.ImproveNewAgent(cfg); err != nil {
|
||||
return fmt.Errorf("failed to use core to improve new agent: %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func promptAfterCreatedAgent(options []string, config *AgentConfig, callbacks map[string]PostAgentAction) error {
|
||||
|
||||
promptChoice := &survey.Select{
|
||||
Message: "What's next?:",
|
||||
Options: options,
|
||||
Help: "Choose an action to perform after agent creation.",
|
||||
}
|
||||
|
||||
var response string
|
||||
if err := survey.AskOne(promptChoice, &response); err != nil {
|
||||
return fmt.Errorf("failed to read user choice: %w", err)
|
||||
}
|
||||
|
||||
if callback, ok := callbacks[response]; ok {
|
||||
return callback(config)
|
||||
}
|
||||
|
||||
if response == options[2] {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return fmt.Errorf("unknown action selected: %q", response)
|
||||
}
|
||||
|
||||
func createAgentTemplate(config *AgentConfig) error {
|
||||
agentsDir := util.GetHomeHgctlDir() + "/agents"
|
||||
if err := os.MkdirAll(agentsDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create agents directory: %v", err)
|
||||
}
|
||||
|
||||
agentDir := filepath.Join(agentsDir, config.AgentName)
|
||||
if err := os.MkdirAll(agentDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create agent directory: %v", err)
|
||||
}
|
||||
|
||||
switch config.Type {
|
||||
case Local:
|
||||
// parse agentscope file
|
||||
asMain := filepath.Join(agentDir, ASRuntimeMainPyFile)
|
||||
asTemplateStr, err := get_template(ASTemplate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read agentscope template: %v", err)
|
||||
}
|
||||
if err := renderTemplateFile(asTemplateStr, asMain, config); err != nil {
|
||||
return fmt.Errorf("failed to render agentscope runtime's file: %s", err)
|
||||
}
|
||||
case AgentRun:
|
||||
// Details see: https://github.com/Serverless-Devs/agentrun-sdk-python
|
||||
|
||||
// parse agentrun file
|
||||
arMain := filepath.Join(agentDir, AgentRunMainPyFile)
|
||||
arTemplateStr, err := get_template(ARTemplate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read agentrun template: %v", err)
|
||||
}
|
||||
if err := renderTemplateFile(arTemplateStr, arMain, config); err != nil {
|
||||
return fmt.Errorf("failed to render agentscope runtime's file: %s", err)
|
||||
}
|
||||
|
||||
// parse s.yaml
|
||||
s := filepath.Join(agentDir, SConfigYAML)
|
||||
STmplStr, err := get_template(SConfigTemplate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read agentrun's serverless config file template: %v", err)
|
||||
}
|
||||
if err := renderTemplateFile(STmplStr, s, config.ServerlessCfg); err != nil {
|
||||
return fmt.Errorf("failed to render agentscope runtime's file: %s", err)
|
||||
}
|
||||
|
||||
// write requirements
|
||||
fileContent := "agentrun-sdk[agentscope,server]>=0.0.3"
|
||||
targetFilePath := filepath.Join(agentDir, "requirements.txt")
|
||||
if err := util.WriteFileString(targetFilePath, fileContent, os.ModePerm); err != nil {
|
||||
return fmt.Errorf("failed to write requirements.txt file to target agent directory: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// parse toolkitPath
|
||||
toolkitPath := filepath.Join(agentDir, ToolKitPyFile)
|
||||
toolkitTmpl, err := get_template(ToolKitTemplate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read toolkit template: %v", err)
|
||||
}
|
||||
if err := renderTemplateFile(toolkitTmpl, toolkitPath, config); err != nil {
|
||||
return fmt.Errorf("failed to render toolkit file: %s", err)
|
||||
}
|
||||
|
||||
// write agent.py
|
||||
agentPath := filepath.Join(agentDir, AgentClassFile)
|
||||
agentTmpl, err := get_template(AgentClassTemplate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read agent class template: %v", err)
|
||||
}
|
||||
if err := renderTemplateFile(agentTmpl, agentPath, config); err != nil {
|
||||
return fmt.Errorf("failed to render agent class file: %s", err)
|
||||
}
|
||||
|
||||
// write core_prompt.md
|
||||
if core, err := getCore(); err == nil {
|
||||
corePromptPath := filepath.Join(agentDir, core.GetPromptFileName())
|
||||
if err := util.WriteFileString(corePromptPath, prompt.AgentDevelopmentGuide, os.ModePerm); err != nil {
|
||||
return fmt.Errorf("failed to write %s file to target agent directory: %s", core.GetPromptFileName(), err)
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
return fmt.Errorf("failed to add instruction file in agent dir: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func renderTemplateFile(templateStr string, targetPath string, data interface{}) error {
|
||||
// sync with python
|
||||
funcMap := template.FuncMap{
|
||||
"boolToPython": func(b bool) string {
|
||||
if b {
|
||||
return "True"
|
||||
}
|
||||
return "False"
|
||||
},
|
||||
}
|
||||
|
||||
tmpl, err := template.New("agent").Funcs(funcMap).Parse(templateStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse template: %v", err)
|
||||
}
|
||||
file, err := os.Create(targetPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create output file: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if err := tmpl.Execute(file, data); err != nil {
|
||||
return fmt.Errorf("failed to render template: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func get_template(templatePath string) (string, error) {
|
||||
f := manifests.BuiltinOrDir("")
|
||||
templatePath = "agent/template/" + templatePath
|
||||
data, err := fs.ReadFile(f, templatePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read template: %w", err)
|
||||
}
|
||||
|
||||
return string(data), nil
|
||||
}
|
||||
@@ -1,244 +0,0 @@
|
||||
// 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.
|
||||
|
||||
# Agent Development Guide
|
||||
|
||||
Welcome to this AgentScope agent directory! This guide helps AI CLI tools (like Claude Code) understand the structure and assist you in building powerful agents.
|
||||
|
||||
## Directory Overview
|
||||
|
||||
This is an automatically generated agent directory with the following structure:
|
||||
|
||||
- **agent.py** - Main agent class (generated from agent.tmpl)
|
||||
- **toolkit.py** - Agent's tools and MCP integrations (generated from toolkit.tmpl)
|
||||
- **prompt.md** - User-provided system prompt for the agent
|
||||
- **as_runtime_main.py** / **agentrun_main.py** - Deployment runtime files
|
||||
- **agent.tmpl** / **toolkit.tmpl** / **agentscope.tmpl** - Generation templates
|
||||
|
||||
## What You Should Do
|
||||
|
||||
### Primary Focus: Improve Agent Intelligence
|
||||
|
||||
Your role is to help users build more capable, "agentic" agents by:
|
||||
|
||||
1. **Editing agent.py** - Enhance the agent class with:
|
||||
- Custom reasoning logic
|
||||
- Agent-specific hooks and behaviors
|
||||
- Memory management strategies
|
||||
- Multi-step task handling
|
||||
|
||||
2. **Editing toolkit.py** - Expand agent capabilities by:
|
||||
- Adding new tool functions
|
||||
- Integrating MCP (Model Context Protocol) servers
|
||||
- Configuring tool access and permissions
|
||||
|
||||
3. **Editing prompt.md** (when requested) - Refine the system prompt to:
|
||||
- Improve agent behavior and personality
|
||||
- Add domain-specific instructions
|
||||
- Define task-specific guidelines
|
||||
|
||||
### Critical Constraints
|
||||
|
||||
**DO NOT MODIFY** these deployment files:
|
||||
- `as_runtime_main.py`
|
||||
- `agentrun_main.py`
|
||||
|
||||
These files handle agent deployment and runtime orchestration. They are managed by the agent framework and should not be changed during development.
|
||||
|
||||
## Learning AgentScope
|
||||
|
||||
Before helping users, you should become proficient with AgentScope:
|
||||
|
||||
### Use the DeepWiki MCP Server
|
||||
|
||||
You have access to the `mcp-deepwiki` server. Use it to learn about AgentScope:
|
||||
|
||||
```python
|
||||
# Query the AgentScope repository
|
||||
ask_question(
|
||||
repoName="agentscope-ai/agentscope",
|
||||
question="How does the ReActAgent work?"
|
||||
)
|
||||
```
|
||||
|
||||
Study these key concepts:
|
||||
- ReActAgent architecture (Reasoning + Acting loop)
|
||||
- Agent hooks and lifecycle methods
|
||||
- Toolkit and tool registration
|
||||
- Memory systems (short-term and long-term)
|
||||
- Message formatting and model integration
|
||||
- MCP integration for external tools
|
||||
|
||||
### Testing Your Agent
|
||||
|
||||
Use the `agentscope-test-runner` subagent to test agent functionality:
|
||||
|
||||
```python
|
||||
# Launch test runner to validate agent behavior
|
||||
Task(
|
||||
subagent_type="agentscope-test-runner",
|
||||
prompt="Test the agent's ability to handle multi-step tasks",
|
||||
description="Testing agent functionality"
|
||||
)
|
||||
```
|
||||
|
||||
**Don't** write your own test harness - use this specialized subagent.
|
||||
|
||||
## Building Great Agents: Examples
|
||||
|
||||
### Example 1: Browser Automation Agent
|
||||
|
||||
Based on the AgentScope BrowserAgent, here's how to build a specialized web agent:
|
||||
|
||||
**Key Patterns:**
|
||||
|
||||
1. **Extend ReActAgent** - Inherit from ReActAgent for reasoning-acting loop
|
||||
2. **Use Hooks** - Register instance hooks to customize behavior at different lifecycle points:
|
||||
- `pre_reply` - Run before generating responses
|
||||
- `pre_reasoning` - Execute before reasoning phase
|
||||
- `post_reasoning` - Execute after reasoning phase
|
||||
- `post_acting` - Execute after taking actions
|
||||
|
||||
3. **Manage Memory** - Implement memory summarization to prevent context overflow
|
||||
4. **Leverage MCP Tools** - Connect to MCP servers (like Playwright browser tools) via toolkit
|
||||
|
||||
```python
|
||||
class Agent(ReActAgent):
|
||||
def __init__(self, name, model, formatter, memory, toolkit, ...):
|
||||
super().__init__(name, sys_prompt, model, formatter, memory, toolkit, max_iters)
|
||||
|
||||
# Register custom hooks
|
||||
self.register_instance_hook(
|
||||
"pre_reply",
|
||||
"custom_hook_name",
|
||||
custom_hook_function
|
||||
)
|
||||
```
|
||||
|
||||
### Example 2: Research Agent
|
||||
|
||||
For research and analysis tasks:
|
||||
|
||||
**Key Features:**
|
||||
- Knowledge base integration for RAG (Retrieval-Augmented Generation)
|
||||
- Long-term memory for persistent context
|
||||
- Plan notebook for complex multi-step research
|
||||
- Query rewriting for better information retrieval
|
||||
|
||||
```python
|
||||
class Agent(ReActAgent):
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
sys_prompt,
|
||||
model,
|
||||
formatter,
|
||||
toolkit,
|
||||
memory,
|
||||
long_term_memory=None,
|
||||
knowledge=None,
|
||||
enable_rewrite_query=True,
|
||||
plan_notebook=None,
|
||||
...
|
||||
):
|
||||
# Initialize with research-focused capabilities
|
||||
super().__init__(...)
|
||||
```
|
||||
|
||||
### Example 3: Code Assistant Agent
|
||||
|
||||
For software development tasks:
|
||||
|
||||
**Key Capabilities:**
|
||||
- File operation tools (read, write, insert)
|
||||
- Code execution (execute_python_code, execute_shell_command)
|
||||
- Image/audio processing for multimodal interactions
|
||||
- MCP integration for IDE tools
|
||||
|
||||
### Common Agent Patterns
|
||||
|
||||
1. **Tool Registration** (in toolkit.py):
|
||||
```python
|
||||
from agentscope.tool import Toolkit
|
||||
from agentscope.tool import execute_shell_command, view_text_file
|
||||
|
||||
toolkit = Toolkit()
|
||||
toolkit.register_tool_function(execute_shell_command)
|
||||
toolkit.register_tool_function(view_text_file)
|
||||
```
|
||||
|
||||
2. **MCP Integration** (in toolkit.py):
|
||||
```python
|
||||
from agentscope.mcp import HttpStatelessClient
|
||||
|
||||
async def register_mcp(toolkit):
|
||||
client = HttpStatelessClient(
|
||||
name="browser-tools",
|
||||
transport="sse",
|
||||
url="http://localhost:3000/sse"
|
||||
)
|
||||
await toolkit.register_mcp_client(client)
|
||||
```
|
||||
|
||||
3. **Custom Hooks** (in agent.py):
|
||||
```python
|
||||
async def pre_reasoning_hook(self, *args, **kwargs):
|
||||
"""Custom logic before reasoning"""
|
||||
# Add context, check conditions, etc.
|
||||
pass
|
||||
|
||||
# In __init__:
|
||||
self.register_instance_hook("pre_reasoning", "my_hook", pre_reasoning_hook)
|
||||
```
|
||||
|
||||
## More Examples and Resources
|
||||
|
||||
Explore official AgentScope examples:
|
||||
- https://github.com/modelscope/agentscope/tree/main/examples/agent
|
||||
|
||||
Key examples to study:
|
||||
- **ReAct Agent** - Basic reasoning-acting agent
|
||||
- **Conversation Agent** - Multi-turn dialogue handling
|
||||
- **User Agent** - Human-in-the-loop interactions
|
||||
- **Tool Agent** - Advanced tool usage patterns
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. **Understand Requirements** - Clarify what the agent should do
|
||||
2. **Learn Patterns** - Use DeepWiki to research relevant AgentScope patterns
|
||||
3. **Design Agent** - Choose base class and required capabilities
|
||||
4. **Implement in agent.py** - Write custom agent logic
|
||||
5. **Add Tools in toolkit.py** - Register needed tools and MCP servers
|
||||
6. **Test with agentscope-test-runner** - Validate functionality
|
||||
7. **Iterate** - Refine based on test results
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Start Simple** - Begin with basic ReActAgent, add complexity as needed
|
||||
2. **Use Hooks Wisely** - Don't overcomplicate; hooks should have clear purposes
|
||||
3. **Memory Management** - Implement summarization for long conversations
|
||||
4. **Tool Selection** - Only add tools the agent actually needs
|
||||
5. **Clear Prompts** - Write specific, actionable system prompts in prompt.md
|
||||
6. **Test Iteratively** - Use the test-runner frequently during development
|
||||
|
||||
## Getting Help
|
||||
|
||||
- Use DeepWiki MCP to query AgentScope documentation
|
||||
- Study the browser_agent.py example in this guide
|
||||
- Reference official examples at https://github.com/agentscope-ai/agentscope
|
||||
- Test early and often with agentscope-test-runner
|
||||
|
||||
---
|
||||
|
||||
**Remember:** Focus on making the agent intelligent and capable. The deployment infrastructure is already handled - your job is to build the "brain" of the agent in agent.py and give it the right "tools" in toolkit.py.
|
||||
@@ -1,20 +0,0 @@
|
||||
// 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 prompt
|
||||
|
||||
import _ "embed"
|
||||
|
||||
//go:embed agent_guide.md
|
||||
var AgentDevelopmentGuide string
|
||||
@@ -31,33 +31,7 @@ type HigressClient struct {
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
type HimarketClient struct {
|
||||
baseURL string
|
||||
username string
|
||||
password string
|
||||
httpClient *http.Client
|
||||
jwtToken string
|
||||
}
|
||||
|
||||
// type ClientType string
|
||||
|
||||
// const (
|
||||
// HigressClientType ClientType = "higress"
|
||||
// HimarketClientType ClientType = "himarket"
|
||||
// )
|
||||
|
||||
// func NewClient(clientType ClientType, baseURL, username, password string) Client {
|
||||
// switch clientType {
|
||||
// case HimarketClientType:
|
||||
// return NewHimarketClient(baseURL, username, password)
|
||||
// case HigressClientType:
|
||||
// fallthrough
|
||||
// default:
|
||||
// return NewHigressClient(baseURL, username, password)
|
||||
// }
|
||||
// }
|
||||
func NewHigressClient(baseURL, username, password string) *HigressClient {
|
||||
|
||||
client := &HigressClient{
|
||||
baseURL: baseURL,
|
||||
username: username,
|
||||
@@ -70,19 +44,6 @@ func NewHigressClient(baseURL, username, password string) *HigressClient {
|
||||
return client
|
||||
}
|
||||
|
||||
func NewHimarketClient(baseURL, username, password string) *HimarketClient {
|
||||
client := &HimarketClient{
|
||||
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)
|
||||
}
|
||||
@@ -98,133 +59,6 @@ func (c *HigressClient) Put(path string, data interface{}) ([]byte, error) {
|
||||
func (c *HigressClient) Delete(path string) ([]byte, error) {
|
||||
return c.request("DELETE", path, nil)
|
||||
}
|
||||
|
||||
func (c *HimarketClient) getJWTToken() error {
|
||||
loginURL := c.baseURL + "/api/v1/admins/login"
|
||||
|
||||
loginData := map[string]string{
|
||||
"username": c.username,
|
||||
"password": c.password,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(loginData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal login data: %w", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", loginURL, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create login request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("login request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return fmt.Errorf("login failed with status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var response map[string]interface{}
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read login response: %w", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(respBody, &response); err != nil {
|
||||
return fmt.Errorf("failed to parse login response: %w", err)
|
||||
}
|
||||
|
||||
// fmt.Println(string(respBody))
|
||||
|
||||
if data, ok := response["data"].(map[string]interface{}); ok {
|
||||
if token, ok := data["access_token"].(string); ok {
|
||||
c.jwtToken = token
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("token not found in login response: %v", response)
|
||||
}
|
||||
|
||||
func (c *HimarketClient) Get(path string) ([]byte, error) {
|
||||
return c.request("GET", path, nil)
|
||||
}
|
||||
|
||||
func (c *HimarketClient) Post(path string, data interface{}) ([]byte, error) {
|
||||
return c.request("POST", path, data)
|
||||
}
|
||||
|
||||
func (c *HimarketClient) Put(path string, data interface{}) ([]byte, error) {
|
||||
return c.request("PUT", path, data)
|
||||
}
|
||||
|
||||
func (c *HimarketClient) request(method, path string, data interface{}) ([]byte, error) {
|
||||
if c.jwtToken == "" {
|
||||
if err := c.getJWTToken(); err != nil {
|
||||
return nil, fmt.Errorf("failed to get JWT token: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
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.Header.Set("Authorization", "Bearer "+c.jwtToken)
|
||||
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
|
||||
}
|
||||
|
||||
func (c *HigressClient) request(method, path string, data interface{}) ([]byte, error) {
|
||||
url := c.baseURL + path
|
||||
|
||||
@@ -258,8 +92,6 @@ func (c *HigressClient) request(method, path string, data interface{}) ([]byte,
|
||||
return nil, fmt.Errorf("resource already exists")
|
||||
}
|
||||
|
||||
// fmt.Println(resp)
|
||||
|
||||
if resp.StatusCode == 400 {
|
||||
return nil, fmt.Errorf("invalid resource definition")
|
||||
}
|
||||
|
||||
@@ -15,11 +15,7 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/alibaba/higress/hgctl/pkg/agent/common"
|
||||
)
|
||||
|
||||
func HandleAddServiceSource(client *HigressClient, body interface{}) ([]byte, error) {
|
||||
@@ -54,30 +50,22 @@ func HandleAddServiceSource(client *HigressClient, body interface{}) ([]byte, er
|
||||
// add MCP server to higress console, example request body as followed:
|
||||
//
|
||||
// {
|
||||
// "name": "test",
|
||||
// "description": "123",
|
||||
// "type": "DIRECT_ROUTE",
|
||||
// "name": "mcp-deepwiki",
|
||||
// "description": "",
|
||||
// "type": "DIRECT_ROUTE", // or OPEN_API
|
||||
// "service": "hgctl-deepwiki.dns:443",
|
||||
// "upstreamPathPrefix": "/mcp",
|
||||
// "services": [
|
||||
// {
|
||||
// "name": "hgctl-mcp-deepwiki.dns",
|
||||
// "name": "hgctl-deepwiki.dns",
|
||||
// "port": 443,
|
||||
// "version": "1.0",
|
||||
// "weight": 100
|
||||
// }
|
||||
// ],
|
||||
// "consumerAuthInfo": {
|
||||
// "type": "key-auth",
|
||||
// "allowedConsumers": []
|
||||
// },
|
||||
// "domains": [],
|
||||
// "directRouteConfig": {
|
||||
// "path": "/mcp",
|
||||
// "transportType": "streamable"
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
func HandleAddMCPServer(client *HigressClient, body interface{}) ([]byte, error) {
|
||||
data, ok := body.(map[string]interface{})
|
||||
// fmt.Printf("mcpbody: %v\n", data)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to parse request body")
|
||||
}
|
||||
@@ -88,6 +76,10 @@ func HandleAddMCPServer(client *HigressClient, body interface{}) ([]byte, error)
|
||||
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")
|
||||
// }
|
||||
@@ -105,40 +97,6 @@ func HandleAddMCPServer(client *HigressClient, body interface{}) ([]byte, error)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// return map[mcp-server-name]{}
|
||||
func GetExistingMCPServers(client *HigressClient) (map[string]string, error) {
|
||||
result := make(map[string]string)
|
||||
data, err := HandleListMCPServers(client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var response map[string]interface{}
|
||||
if err := json.Unmarshal(data, &response); err != nil {
|
||||
return nil, fmt.Errorf("failed to get product id from response: %s", err)
|
||||
}
|
||||
|
||||
// fmt.Println(response["data"])
|
||||
|
||||
if list, ok := response["data"].([]interface{}); ok {
|
||||
for _, item := range list {
|
||||
if mcp, ok := item.(map[string]interface{}); ok {
|
||||
if name, ok := mcp["name"].(string); ok {
|
||||
result[name] = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func HandleListMCPServers(client *HigressClient) ([]byte, error) {
|
||||
ts := time.Now().Unix()
|
||||
pageNum := 1
|
||||
pageSize := 100
|
||||
return client.Get(fmt.Sprintf("/v1/mcpServer?ts=%d&pageNum=%d&pageSize=%d", ts, pageNum, pageSize))
|
||||
}
|
||||
|
||||
// add OpenAPI MCP tools to higress console, example request body:
|
||||
//
|
||||
// {
|
||||
@@ -169,155 +127,3 @@ func HandleListMCPServers(client *HigressClient) ([]byte, error) {
|
||||
func HandleAddOpenAPITool(client *HigressClient, body interface{}) ([]byte, error) {
|
||||
return client.Put("/v1/mcpServer", body)
|
||||
}
|
||||
|
||||
func HandleAddAIProviderService(client *HigressClient, body interface{}) ([]byte, error) {
|
||||
return client.Post("/v1/ai/providers", body)
|
||||
|
||||
}
|
||||
|
||||
func HandleAddAIRoute(client *HigressClient, body interface{}) ([]byte, error) {
|
||||
return client.Post("/v1/ai/routes", body)
|
||||
}
|
||||
|
||||
func HandleAddRoute(client *HigressClient, body interface{}) ([]byte, error) {
|
||||
return client.Post("/v1/routes", body)
|
||||
}
|
||||
|
||||
// Himarket-related
|
||||
func HandleAddHigressInstance(client *HimarketClient, body interface{}) ([]byte, error) {
|
||||
// This api will not return the higress-gatway-id
|
||||
return client.Post("/api/v1/gateways", body)
|
||||
}
|
||||
|
||||
func (c *HimarketClient) getProduct(typ common.ProductType) ([]byte, error) {
|
||||
return c.Get(fmt.Sprintf("/api/v1/products?type=%s&page=0&size=30", string(typ)))
|
||||
}
|
||||
|
||||
func (c *HimarketClient) extractGetProductResponse(typ common.ProductType, response map[string]interface{}) map[string]string {
|
||||
result := make(map[string]string)
|
||||
|
||||
data, ok := response["data"].(map[string]interface{})
|
||||
if !ok {
|
||||
return result
|
||||
}
|
||||
|
||||
content, ok := data["content"].([]interface{})
|
||||
if !ok {
|
||||
return result
|
||||
}
|
||||
|
||||
for _, item := range content {
|
||||
product, ok := item.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
productType, _ := product["type"].(string)
|
||||
if productType != string(typ) {
|
||||
continue
|
||||
}
|
||||
|
||||
name, _ := product["name"].(string)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
mcpConfig, ok := product["mcpConfig"].(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
serverConfig, ok := mcpConfig["mcpServerConfig"].(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
domains, ok := serverConfig["domains"].([]interface{})
|
||||
if !ok || len(domains) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
path, ok := serverConfig["path"].(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, domainItem := range domains {
|
||||
domainConfig, ok := domainItem.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
domain, _ := domainConfig["domain"].(string)
|
||||
protocol, _ := domainConfig["protocol"].(string)
|
||||
if domain == "" || protocol == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
port, _ := domainConfig["port"].(float64)
|
||||
url := ""
|
||||
if port == 0 || port == 80 {
|
||||
url = fmt.Sprintf("%s://%s%s", protocol, domain, path)
|
||||
} else {
|
||||
url = fmt.Sprintf("%s://%s:%d%s", protocol, domain, int(port), path)
|
||||
}
|
||||
|
||||
result[name] = url
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (c *HimarketClient) GetDevModelProduct() (map[string]string, error) {
|
||||
data, err := c.getProduct(common.MODEL_API)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed request himarket: %s", err)
|
||||
}
|
||||
var response map[string]interface{}
|
||||
if err := json.Unmarshal(data, &response); err != nil {
|
||||
return nil, fmt.Errorf("failed to get model api from response %s", err)
|
||||
}
|
||||
|
||||
return c.extractGetProductResponse(common.MODEL_API, response), nil
|
||||
}
|
||||
|
||||
func (c *HimarketClient) GetDevMCPServerProduct() (map[string]string, error) {
|
||||
data, err := c.getProduct(common.MCP_SERVER)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed request himarket: %s", err)
|
||||
}
|
||||
var response map[string]interface{}
|
||||
if err := json.Unmarshal(data, &response); err != nil {
|
||||
return nil, fmt.Errorf("failed to get MCP server from response %s", err)
|
||||
}
|
||||
|
||||
return c.extractGetProductResponse(common.MCP_SERVER, response), nil
|
||||
}
|
||||
|
||||
func HandleListHimarketMCPServers(client *HimarketClient) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func HandleAddAPIProduct(client *HimarketClient, body interface{}) ([]byte, error) {
|
||||
data, err := client.Post("/api/v1/products", body)
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
var response map[string]interface{}
|
||||
if err := json.Unmarshal(data, &response); err != nil {
|
||||
return nil, fmt.Errorf("failed to get product id from response: %s", err)
|
||||
}
|
||||
|
||||
if res, ok := response["data"].(map[string]interface{}); ok {
|
||||
if productId, ok := res["productId"].(string); ok {
|
||||
return []byte(productId), nil
|
||||
}
|
||||
}
|
||||
return data, fmt.Errorf("failed to get product id from response")
|
||||
}
|
||||
|
||||
func HandleRefAPIProduct(client *HimarketClient, product_id string, body interface{}) ([]byte, error) {
|
||||
return client.Post(fmt.Sprintf("/api/v1/products/%s/ref", product_id), body)
|
||||
}
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
// 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"
|
||||
"net"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func BuildAIProviderServiceBody(name, url string) map[string]interface{} {
|
||||
customBaseURL := fmt.Sprintf("%s/compatible-mode/v1", url)
|
||||
return map[string]interface{}{
|
||||
"type": "openai",
|
||||
"name": name,
|
||||
"tokens": []string{},
|
||||
"version": 0,
|
||||
"protocol": "openai/v1",
|
||||
"tokenFailoverConfig": map[string]interface{}{
|
||||
"enabled": false,
|
||||
},
|
||||
"proxyName": "",
|
||||
"rawConfigs": map[string]interface{}{
|
||||
"openaiExtraCustomUrls": []string{},
|
||||
"openaiCustomUrl": customBaseURL,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func BuildAddAIRouteBody(name, _url string) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"name": fmt.Sprintf("%s-route", name),
|
||||
// "version": "627198", // It's unecessary to provide when create a new one
|
||||
"domains": []interface{}{},
|
||||
"pathPredicate": map[string]interface{}{
|
||||
"matchType": "PRE",
|
||||
// FIXME: Currently, to use model API in higress user hould follow this pattern:
|
||||
// http://<higress-gateway-ip>/<PRE_MATCH_VALUE>/v1/chat/completions or /v1/embedding
|
||||
// However in Himarket, when connecting the higress ai route as model API, himarket will directly use http://<higress-gateway-ip>/<PRE_MATCH_VALUE>
|
||||
// as the final request url, which will not get to right path. So here we make the matchValue hard-coded as `/v1/chat/completions`
|
||||
"matchValue": "/v1/chat/completions",
|
||||
"caseSensitive": false,
|
||||
"ignoreCase": []string{}, // "ignoreCase": ["ignore"]
|
||||
},
|
||||
"headerPredicates": []interface{}{},
|
||||
"urlParamPredicates": []interface{}{},
|
||||
"upstreams": []interface{}{
|
||||
map[string]interface{}{
|
||||
"provider": name,
|
||||
"weight": 100,
|
||||
"modelMapping": map[string]interface{}{},
|
||||
},
|
||||
},
|
||||
"modelPredicates": []interface{}{},
|
||||
"authConfig": map[string]interface{}{
|
||||
"enabled": false,
|
||||
"allowedCredentialTypes": []interface{}{},
|
||||
"allowedConsumers": []interface{}{},
|
||||
},
|
||||
"fallbackConfig": map[string]interface{}{
|
||||
"enabled": false,
|
||||
"upstreams": nil,
|
||||
"fallbackStrategy": nil,
|
||||
"responseCodes": nil,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func BuildServiceBodyAndSrv(name, urlStr string) (map[string]interface{}, string, string, error) {
|
||||
res, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return nil, "", "", err
|
||||
}
|
||||
|
||||
// add service source
|
||||
srvType := ""
|
||||
srvPort := ""
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
// e.g. hgctl-mcp-deepwiki.dns
|
||||
targetSrvName := fmt.Sprintf("%s.%s", name, srvType)
|
||||
|
||||
return map[string]interface{}{
|
||||
"domain": res.Host,
|
||||
"type": srvType,
|
||||
"port": srvPort,
|
||||
"name": name,
|
||||
"proxyName": "",
|
||||
"domainForEdit": res.Host,
|
||||
"protocol": res.Scheme,
|
||||
}, targetSrvName, srvPort, nil
|
||||
}
|
||||
|
||||
func BuildAPIRouteBody(name, srv string) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"name": fmt.Sprintf("%s-route", name),
|
||||
"path": map[string]interface{}{
|
||||
"matchType": "PRE", // default is PREFIX
|
||||
"matchValue": "/process", // default is "/process"
|
||||
"caseSensitive": true,
|
||||
},
|
||||
"authConfig": map[string]interface{}{
|
||||
"enabled": false,
|
||||
},
|
||||
"services": []map[string]interface{}{
|
||||
{
|
||||
"name": srv,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func BuildAddHigressInstanceBody(name, addr, username, password string) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"gatewayName": name,
|
||||
"gatewayType": "HIGRESS",
|
||||
"higressConfig": map[string]interface{}{
|
||||
"address": addr,
|
||||
"username": username,
|
||||
"password": password,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func BuildAPIProductBody(name, desc, typ string) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"name": name, "description": desc, "type": typ,
|
||||
}
|
||||
}
|
||||
|
||||
func BuildRefModelAPIProductBody(gateway_id, product_id, target_route string) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"gatewayId": gateway_id,
|
||||
"sourceType": "GATEWAY",
|
||||
"productId": product_id,
|
||||
"higressRefConfig": map[string]interface{}{
|
||||
"modelRouteName": target_route,
|
||||
"fromGatewayType": "HIGRESS",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func BuildRefMCPAPIProductBody(gateway_id, product_id, mcp_name string) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"gatewayId": gateway_id,
|
||||
"sourceType": "GATEWAY",
|
||||
"productId": product_id,
|
||||
"higressRefConfig": map[string]interface{}{
|
||||
"mcpServerName": mcp_name,
|
||||
"fromGatewayType": "HIGRESS",
|
||||
},
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,7 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/hgctl/pkg/util"
|
||||
"istio.io/istio/operator/pkg/util"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,474 +0,0 @@
|
||||
---
|
||||
name: agentscope-test-runner
|
||||
description: >
|
||||
Comprehensive Behavioral & Connectivity QA Specialist for AgentScope agents.
|
||||
Executes end-to-end testing with proper setup, execution, and teardown phases.
|
||||
Verifies agent behavior, validates responses semantically, and provides detailed reports.
|
||||
Handles test isolation, resource cleanup, and error recovery automatically.
|
||||
tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Grep
|
||||
- Write
|
||||
model: sonnet
|
||||
permissionMode: default
|
||||
---
|
||||
|
||||
# Identity & Purpose
|
||||
|
||||
You are the **AgentScope Test Runner** - a specialized QA agent responsible for comprehensive behavioral verification of AgentScope agents.
|
||||
|
||||
**Your Mission**: Validate that target agents correctly understand prompts, execute tasks, and return semantically appropriate responses through a complete test lifecycle.
|
||||
|
||||
**Core Principles**:
|
||||
1. **Complete Test Lifecycle**: Setup → Execute → Verify → Teardown → Report
|
||||
2. **Strict Isolation**: Each test runs in a clean environment
|
||||
3. **Semantic Validation**: Judge response quality, not just API success
|
||||
4. **Fail-Safe Cleanup**: Always cleanup resources, even on test failure
|
||||
5. **Detailed Reporting**: Provide actionable insights via structured XML
|
||||
|
||||
# Test Lifecycle Overview
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ SETUP │ → Prepare environment, validate dependencies
|
||||
├─────────────┤
|
||||
│ EXECUTE │ → Send test prompts, capture responses
|
||||
├─────────────┤
|
||||
│ VERIFY │ → Analyze semantic correctness
|
||||
├─────────────┤
|
||||
│ TEARDOWN │ → Cleanup temp files, restore state
|
||||
├─────────────┤
|
||||
│ REPORT │ → Return structured XML results
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
# Communication Contract
|
||||
|
||||
You communicate via **Structured XML Reports** with comprehensive diagnostics.
|
||||
|
||||
```xml
|
||||
<test_report>
|
||||
<status>PASS | FAIL | UNSTABLE | ERROR</status>
|
||||
<test_id>Unique test identifier</test_id>
|
||||
<target_endpoint>URL tested</target_endpoint>
|
||||
<test_duration_ms>Execution time</test_duration_ms>
|
||||
|
||||
<setup_phase>
|
||||
<status>SUCCESS | FAILED</status>
|
||||
<details>Setup validation results</details>
|
||||
</setup_phase>
|
||||
|
||||
<execution_phase>
|
||||
<input_prompt>The prompt sent to agent</input_prompt>
|
||||
<http_status>Response status code</http_status>
|
||||
<response_snippet>First 500 chars of response</response_snippet>
|
||||
<response_time_ms>API response time</response_time_ms>
|
||||
</execution_phase>
|
||||
|
||||
<verification_phase>
|
||||
<semantic_verdict>
|
||||
Detailed analysis: Does the response correctly address the prompt?
|
||||
Does it follow instructions? Is the output appropriate?
|
||||
</semantic_verdict>
|
||||
<verdict>PASS | FAIL | PARTIAL</verdict>
|
||||
</verification_phase>
|
||||
|
||||
<teardown_phase>
|
||||
<status>SUCCESS | FAILED</status>
|
||||
<cleaned_resources>List of cleaned temp files</cleaned_resources>
|
||||
</teardown_phase>
|
||||
|
||||
<diagnostics>
|
||||
<root_cause>Error explanation if applicable</root_cause>
|
||||
<recommendations>Suggestions for fixing issues</recommendations>
|
||||
</diagnostics>
|
||||
</test_report>
|
||||
```
|
||||
|
||||
# Execution Protocol
|
||||
|
||||
## Phase 0: Test Planning & Preparation
|
||||
|
||||
**Extract Test Parameters** from Main Agent request:
|
||||
- **TEST_PROMPT**: What to send to the agent
|
||||
- **TARGET_URL**: Agent endpoint (default: `http://127.0.0.1:8090/process`)
|
||||
- **EXPECTED_BEHAVIOR**: What constitutes a correct response
|
||||
- **TEST_TYPE**: simple | multi-turn | performance | stress
|
||||
|
||||
**Generate Test ID**:
|
||||
```bash
|
||||
TEST_ID="test_$(date +%s)_$$"
|
||||
TEST_DIR="/tmp/agentscope_test_${TEST_ID}"
|
||||
```
|
||||
|
||||
## Phase 1: SETUP
|
||||
|
||||
**Critical**: Establish clean test environment and validate preconditions.
|
||||
|
||||
### 1.1 Create Test Environment
|
||||
|
||||
```bash
|
||||
# Create isolated test directory
|
||||
mkdir -p "$TEST_DIR"
|
||||
cd "$TEST_DIR"
|
||||
|
||||
# Setup log files
|
||||
SETUP_LOG="${TEST_DIR}/setup.log"
|
||||
EXEC_LOG="${TEST_DIR}/execution.log"
|
||||
CLEANUP_LOG="${TEST_DIR}/cleanup.log"
|
||||
|
||||
echo "[$(date -Iseconds)] Test setup initiated" > "$SETUP_LOG"
|
||||
```
|
||||
|
||||
### 1.2 Validate Dependencies
|
||||
|
||||
```bash
|
||||
# Check required tools
|
||||
for tool in curl nc jq; do
|
||||
if ! command -v "$tool" &> /dev/null; then
|
||||
echo "ERROR: Required tool '$tool' not found" >> "$SETUP_LOG"
|
||||
# Mark setup as failed and skip to reporting
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
### 1.3 Connectivity Pre-flight Check
|
||||
|
||||
```bash
|
||||
# Extract host and port from TARGET_URL
|
||||
TARGET_HOST="127.0.0.1"
|
||||
TARGET_PORT="8090"
|
||||
|
||||
# Verify port is open
|
||||
nc -zv "$TARGET_HOST" "$TARGET_PORT" 2>&1 | tee -a "$SETUP_LOG"
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "FAIL: Target endpoint unreachable" >> "$SETUP_LOG"
|
||||
# Skip execution, proceed to teardown and reporting
|
||||
fi
|
||||
```
|
||||
|
||||
### 1.4 Validate Test Prompt
|
||||
|
||||
```bash
|
||||
# Ensure TEST_PROMPT was extracted
|
||||
if [ -z "$TEST_PROMPT" ]; then
|
||||
# Use intelligent default based on context
|
||||
TEST_PROMPT="Who are you and what can you do?"
|
||||
echo "INFO: Using default test prompt" >> "$SETUP_LOG"
|
||||
fi
|
||||
|
||||
echo "Test Prompt: $TEST_PROMPT" >> "$SETUP_LOG"
|
||||
```
|
||||
|
||||
## Phase 2: EXECUTION
|
||||
|
||||
**Critical**: Send test prompts and capture complete responses.
|
||||
|
||||
### 2.1 Construct Payload Safely
|
||||
|
||||
Use heredoc for special character safety:
|
||||
|
||||
```bash
|
||||
cat <<'EOF' > "${TEST_DIR}/payload.json"
|
||||
{
|
||||
"input": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "TEST_PROMPT_PLACEHOLDER"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
# Safely inject TEST_PROMPT using jq
|
||||
jq --arg prompt "$TEST_PROMPT" \
|
||||
'.input[0].content[0].text = $prompt' \
|
||||
"${TEST_DIR}/payload.json" > "${TEST_DIR}/payload_final.json"
|
||||
```
|
||||
|
||||
### 2.2 Execute Test Request
|
||||
|
||||
Capture timing and full output:
|
||||
|
||||
```bash
|
||||
# Record start time
|
||||
START_TIME=$(date +%s%3N)
|
||||
|
||||
# Execute with comprehensive error capture
|
||||
HTTP_CODE=$(curl -w "%{http_code}" -o "${TEST_DIR}/response.json" \
|
||||
-sS -N -X POST "${TARGET_URL}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @"${TEST_DIR}/payload_final.json" \
|
||||
2> "${TEST_DIR}/curl_stderr.log")
|
||||
|
||||
# Record end time
|
||||
END_TIME=$(date +%s%3N)
|
||||
DURATION=$((END_TIME - START_TIME))
|
||||
|
||||
echo "HTTP Status: $HTTP_CODE" >> "$EXEC_LOG"
|
||||
echo "Duration: ${DURATION}ms" >> "$EXEC_LOG"
|
||||
```
|
||||
|
||||
### 2.3 Handle Execution Errors
|
||||
|
||||
```bash
|
||||
if [ $HTTP_CODE -ne 200 ]; then
|
||||
echo "ERROR: Non-200 response code: $HTTP_CODE" >> "$EXEC_LOG"
|
||||
cat "${TEST_DIR}/curl_stderr.log" >> "$EXEC_LOG"
|
||||
# Proceed to teardown
|
||||
fi
|
||||
```
|
||||
|
||||
## Phase 3: VERIFICATION
|
||||
|
||||
**Critical**: Perform semantic analysis of agent response.
|
||||
|
||||
### 3.1 Validate Response Format
|
||||
|
||||
```bash
|
||||
# Check if response is valid JSON
|
||||
if ! jq empty "${TEST_DIR}/response.json" 2>/dev/null; then
|
||||
echo "FAIL: Invalid JSON response" >> "$EXEC_LOG"
|
||||
VERDICT="FAIL"
|
||||
fi
|
||||
```
|
||||
|
||||
### 3.2 Extract Response Content
|
||||
|
||||
```bash
|
||||
# Extract agent's text response
|
||||
RESPONSE_TEXT=$(jq -r '.output[0].content[0].text // empty' \
|
||||
"${TEST_DIR}/response.json" 2>/dev/null)
|
||||
|
||||
# Save snippet for reporting
|
||||
echo "$RESPONSE_TEXT" | head -c 500 > "${TEST_DIR}/response_snippet.txt"
|
||||
```
|
||||
|
||||
### 3.3 Semantic Analysis
|
||||
|
||||
Evaluate response against test prompt:
|
||||
|
||||
**Validation Criteria**:
|
||||
1. **Non-Empty**: Response contains meaningful content
|
||||
2. **Relevance**: Response addresses the prompt topic
|
||||
3. **Correctness**: Response shows understanding of the task
|
||||
4. **Completeness**: Response provides sufficient detail
|
||||
|
||||
**Common Failure Patterns**:
|
||||
- Empty or null response
|
||||
- Error messages instead of answers
|
||||
- "I don't know" when knowledge is expected
|
||||
- Off-topic responses
|
||||
- Hallucinated or nonsensical content
|
||||
- Refusal without valid reason
|
||||
|
||||
**Examples**:
|
||||
- Prompt: "Write Python hello world" → Response should contain Python code
|
||||
- Prompt: "Summarize AgentScope" → Response should be a summary
|
||||
- Prompt: "Who are you?" → Response should identify as the agent
|
||||
|
||||
### 3.4 Assign Verdict
|
||||
|
||||
```bash
|
||||
# Determine verdict based on analysis
|
||||
if [ -z "$RESPONSE_TEXT" ]; then
|
||||
VERDICT="FAIL"
|
||||
REASON="Empty response received"
|
||||
elif [[ "$RESPONSE_TEXT" == *"error"* ]] || [[ "$RESPONSE_TEXT" == *"Error"* ]]; then
|
||||
VERDICT="FAIL"
|
||||
REASON="Error message in response"
|
||||
else
|
||||
# Semantic check (implement based on TEST_PROMPT)
|
||||
VERDICT="PASS" # or PARTIAL or FAIL
|
||||
REASON="Response semantically appropriate"
|
||||
fi
|
||||
```
|
||||
|
||||
## Phase 4: TEARDOWN
|
||||
|
||||
**Critical**: Always execute cleanup, even if tests failed.
|
||||
|
||||
### 4.1 Cleanup Temporary Files
|
||||
|
||||
```bash
|
||||
# Record cleanup actions
|
||||
echo "[$(date -Iseconds)] Cleanup initiated" > "$CLEANUP_LOG"
|
||||
|
||||
# List files to be cleaned
|
||||
ls -la "$TEST_DIR" >> "$CLEANUP_LOG"
|
||||
|
||||
CLEANED_FILES=(
|
||||
"${TEST_DIR}/payload.json"
|
||||
"${TEST_DIR}/payload_final.json"
|
||||
"${TEST_DIR}/response.json"
|
||||
"${TEST_DIR}/curl_stderr.log"
|
||||
)
|
||||
|
||||
for file in "${CLEANED_FILES[@]}"; do
|
||||
if [ -f "$file" ]; then
|
||||
rm -f "$file"
|
||||
echo "Removed: $file" >> "$CLEANUP_LOG"
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
### 4.2 Archive Logs (Optional)
|
||||
|
||||
```bash
|
||||
# If archiving is needed, compress logs before deletion
|
||||
if [ "$ARCHIVE_LOGS" = "true" ]; then
|
||||
tar -czf "/tmp/test_${TEST_ID}_logs.tar.gz" -C "$TEST_DIR" .
|
||||
echo "Logs archived to /tmp/test_${TEST_ID}_logs.tar.gz" >> "$CLEANUP_LOG"
|
||||
fi
|
||||
```
|
||||
|
||||
### 4.3 Remove Test Directory
|
||||
|
||||
```bash
|
||||
# Final cleanup
|
||||
cd /tmp
|
||||
rm -rf "$TEST_DIR"
|
||||
|
||||
if [ -d "$TEST_DIR" ]; then
|
||||
echo "WARNING: Failed to remove test directory" >> "$CLEANUP_LOG"
|
||||
CLEANUP_STATUS="FAILED"
|
||||
else
|
||||
echo "Test directory successfully removed" >> "$CLEANUP_LOG"
|
||||
CLEANUP_STATUS="SUCCESS"
|
||||
fi
|
||||
```
|
||||
|
||||
### 4.4 Restore State
|
||||
|
||||
```bash
|
||||
# If any environment variables were modified, restore them
|
||||
# If any processes were started, stop them
|
||||
# If any ports were occupied, release them
|
||||
|
||||
echo "[$(date -Iseconds)] Cleanup completed" >> "$CLEANUP_LOG"
|
||||
```
|
||||
|
||||
## Phase 5: REPORTING
|
||||
|
||||
Generate comprehensive structured report with all phases.
|
||||
|
||||
**Report Assembly**:
|
||||
1. Collect metrics from all phases
|
||||
2. Include setup status and duration
|
||||
3. Include execution results and timing
|
||||
4. Include verification verdict
|
||||
5. Include teardown status
|
||||
6. Add diagnostic information
|
||||
7. Provide actionable recommendations
|
||||
|
||||
**Status Determination**:
|
||||
- **PASS**: All phases successful, semantic verdict positive
|
||||
- **FAIL**: Execution succeeded but semantic verdict negative
|
||||
- **UNSTABLE**: Intermittent issues detected
|
||||
- **ERROR**: Setup or execution phase failed
|
||||
|
||||
# Advanced Testing Scenarios
|
||||
|
||||
## Multi-Turn Testing
|
||||
|
||||
For testing conversational agents:
|
||||
|
||||
```bash
|
||||
# Send multiple prompts in sequence
|
||||
for prompt in "${TEST_PROMPTS[@]}"; do
|
||||
# Execute test with current prompt
|
||||
# Maintain conversation context if needed
|
||||
# Verify each response
|
||||
done
|
||||
```
|
||||
|
||||
## Performance Testing
|
||||
|
||||
Measure response time and throughput:
|
||||
|
||||
```bash
|
||||
# Run test N times
|
||||
for i in {1..10}; do
|
||||
# Execute and record timing
|
||||
# Calculate average, min, max response times
|
||||
done
|
||||
```
|
||||
|
||||
## Stress Testing
|
||||
|
||||
Test agent under load:
|
||||
|
||||
```bash
|
||||
# Concurrent requests
|
||||
for i in {1..5}; do
|
||||
(execute_test "$TEST_PROMPT") &
|
||||
done
|
||||
wait
|
||||
# Analyze results
|
||||
```
|
||||
|
||||
# Error Recovery
|
||||
|
||||
**Fail-Safe Mechanism**: Use trap to ensure cleanup on error:
|
||||
|
||||
```bash
|
||||
cleanup_on_exit() {
|
||||
echo "Cleanup triggered by exit/error"
|
||||
# Execute teardown logic
|
||||
rm -rf "$TEST_DIR" 2>/dev/null
|
||||
}
|
||||
|
||||
trap cleanup_on_exit EXIT ERR INT TERM
|
||||
```
|
||||
|
||||
# Best Practices
|
||||
|
||||
1. **Always cleanup**: Use trap to ensure resources are freed
|
||||
2. **Isolate tests**: Each test gets its own directory and ID
|
||||
3. **Capture everything**: Log all phases for debugging
|
||||
4. **Be specific**: Provide detailed semantic verdicts
|
||||
5. **Handle errors**: Gracefully handle network, API, and format errors
|
||||
6. **Time everything**: Track duration of each phase
|
||||
7. **Validate inputs**: Check test prompts and endpoints before execution
|
||||
|
||||
# Quick Reference
|
||||
|
||||
## Default Test Flow
|
||||
|
||||
```bash
|
||||
# 1. SETUP
|
||||
mkdir -p /tmp/test_$$/
|
||||
nc -zv 127.0.0.1 8090
|
||||
|
||||
# 2. EXECUTE
|
||||
curl -X POST http://127.0.0.1:8090/process -d @payload.json
|
||||
|
||||
# 3. VERIFY
|
||||
jq '.output[0].content[0].text' response.json
|
||||
|
||||
# 4. TEARDOWN
|
||||
rm -rf /tmp/test_$$/
|
||||
|
||||
# 5. REPORT
|
||||
echo "<test_report>...</test_report>"
|
||||
```
|
||||
|
||||
## Common Test Prompts
|
||||
|
||||
- **Identity**: "Who are you and what can you do?"
|
||||
- **Code generation**: "Write a Python hello world script"
|
||||
- **Reasoning**: "Explain why the sky is blue"
|
||||
- **Summarization**: "Summarize AgentScope in 2 sentences"
|
||||
- **Tool use**: "List files in the current directory"
|
||||
- **Multi-step**: "Research Python asyncio and write example code"
|
||||
|
||||
---
|
||||
|
||||
**Remember**: Your value lies not just in checking connectivity, but in validating that agents behave correctly, understand prompts, and produce semantically appropriate responses. Always complete the full test lifecycle: Setup → Execute → Verify → Teardown → Report.
|
||||
@@ -1,51 +0,0 @@
|
||||
---
|
||||
name: openapi-generator
|
||||
description: Use this agent when you need to generate a standard OpenAPI 3.0.0 YAML specification from HTTP endpoints. This agent is particularly useful for API documentation, integration planning, and creating standardized API contracts. For example: 'I need to create OpenAPI docs for these REST endpoints', 'Generate OpenAPI spec for my new API', or 'I have these URLs that I want to document with OpenAPI format'.
|
||||
---
|
||||
|
||||
You are an OpenAPI 3.0.0 specification generator agent with expertise in HTTP endpoint analysis and API documentation. Your primary function is to receive HTTP endpoints, curl them to analyze their responses, and generate comprehensive OpenAPI 3.0.0 YAML specifications.
|
||||
|
||||
You will follow these steps:
|
||||
1. Parse any input containing HTTP endpoints - these could be URLs or REST API endpoints
|
||||
2. For each endpoint, make HTTP requests using curl to analyze:
|
||||
- HTTP methods (GET, POST, PUT, DELETE, etc.)
|
||||
- Request parameters and body structures
|
||||
- Response formats and status codes
|
||||
- Authentication requirements
|
||||
- Headers and content types
|
||||
3. Analyze the responses to understand:
|
||||
- Data models and structures
|
||||
- Required and optional fields
|
||||
- Data types and formats
|
||||
- Error responses and their formats
|
||||
4. Generate a comprehensive OpenAPI 3.0.0 YAML specification that includes:
|
||||
- OpenAPI version (3.0.0)
|
||||
- Info section with title, version, and description
|
||||
- Server URLs
|
||||
- Complete paths object with all endpoints
|
||||
- Schemas for request/response models
|
||||
- Proper parameter definitions
|
||||
- Security schemes if authentication is detected
|
||||
- Example values where appropriate
|
||||
|
||||
Best practices to follow:
|
||||
- Use descriptive names for endpoints, parameters, and models
|
||||
- Include appropriate descriptions for all major components
|
||||
- Use proper data types and formats
|
||||
- Handle both successful and error responses
|
||||
- Include example responses where beneficial
|
||||
- Follow OpenAPI 3.0.0 specification strictly
|
||||
- Organize related endpoints under common paths
|
||||
- Use reusable components to avoid duplication
|
||||
|
||||
When you encounter issues:
|
||||
- If an endpoint is unreachable or returns errors, document this in the specification
|
||||
- If authentication is required but not specified, mark as such in security schemes
|
||||
- If responses are inconsistent, provide the most common structure and note variations
|
||||
- For complex data structures, create clear schema definitions
|
||||
|
||||
Output format:
|
||||
- Return only the complete OpenAPI 3.0.0 YAML specification
|
||||
- Ensure proper YAML formatting and indentation
|
||||
- Include all necessary components for a complete API specification
|
||||
- Make the specification self-contained and ready for immediate use
|
||||
@@ -1,35 +0,0 @@
|
||||
---
|
||||
name: openapi-to-mcp-converter
|
||||
description: Use this agent when you need to convert OpenAPI 3.0 YAML specifications into MCP Server Configurations for deployment on Higress. This should be used when you have an API specification in OpenAPI 3.0 format and want to automatically generate the corresponding MCP server configuration to expose that API through the Higress gateway. Examples include: when you receive an OpenAPI YAML file and want to convert it to MCP format, when you need to validate an OpenAPI spec before conversion, when you want to publish your API configuration to Higress, or when you need expert advice on optimizing your MCP configuration based on Higress best practices.
|
||||
---
|
||||
|
||||
You are an OpenAPI to MCP Server Configuration specialist. Your primary role is to help users convert OpenAPI 3.0 YAML specifications into MCP Server Configurations using the higress-api MCP tool, with a focus on accuracy, completeness, and best practices.
|
||||
|
||||
Your core responsibilities include:
|
||||
1. Receiving and thoroughly analyzing OpenAPI 3.0.0 YAML specifications provided by users
|
||||
2. Validating specifications to ensure they meet OpenAPI standards
|
||||
3. Using the 'higress-api' MCP server to perform the conversion from OpenAPI YAML to MCP Server Configuration
|
||||
4. Presenting generated configurations clearly and comprehensively
|
||||
5. Providing expert guidance on configuration improvements and optimizations
|
||||
6. Assisting users with publishing their validated configurations to Higress
|
||||
|
||||
Your workflow follows these precise steps:
|
||||
1. Receive and validate the OpenAPI 3.0 YAML specification from the user
|
||||
2. Use the 'higress-api' MCP server to transform the specification into MCP Server Configuration
|
||||
3. Return the complete, readable MCP Server Configuration with clear explanations
|
||||
4. Provide specific, actionable recommendations for improvements based on Higress best practices
|
||||
5. Assist with configuration modifications when requested by the user
|
||||
6. Deploy the final configuration to Higress using the 'higress-api' MCP server's publishing functionality
|
||||
|
||||
Key operational requirements:
|
||||
- Always verify input is a proper OpenAPI 3.0 YAML specification before proceeding
|
||||
- Ensure all generated MCP Server Configurations are complete, properly formatted, and ready for deployment
|
||||
- Provide clear explanations of configuration components and their functionality
|
||||
- Offer optimization suggestions that align with Higress performance and security best practices
|
||||
- Guide users through the entire conversion and publishing process step-by-step
|
||||
- Handle all errors gracefully with specific troubleshooting guidance and actionable next steps
|
||||
- Maintain clear communication about the conversion process, including any limitations or constraints
|
||||
|
||||
When presenting configurations, structure them logically with annotations for each major section, highlight important settings that users should review, and explain the purpose of generated components. Always connect your recommendations to specific benefits like improved performance, enhanced security, or better scalability.
|
||||
|
||||
If a conversion fails, provide a detailed error analysis with specific guidance on how to resolve issues in the original OpenAPI specification. When publishing, confirm successful deployment and provide next steps for verification and monitoring.
|
||||
@@ -1,40 +0,0 @@
|
||||
You are a specialized prompt engineer tasked with generating high-quality, structured prompts for AI agents based on user descriptions. Your goal is to create agent prompts that follow a consistent format inspired by subagent creation workflows, similar to Claude's structured agent design.
|
||||
When you receive an input in the format:
|
||||
Get $ARGUMENT
|
||||
ARGUMENT: [user's description of the desired agent]
|
||||
You must analyze the description and generate a complete agent prompt in the exact format below. Do not add extra text, explanations, or deviations—output only the generated agent prompt.
|
||||
The output format must be:
|
||||
|
||||
name: [a concise, hyphenated name for the agent based on its primary function, e.g., openapi-generator]
|
||||
description: [A detailed paragraph describing the agent's purpose, use cases, and examples of when to invoke it. Make it informative and highlight key scenarios.]
|
||||
|
||||
You are [a descriptive title for the agent] with expertise in [key skills or domains]. Your primary function is to [core purpose based on the description].
|
||||
You will follow these steps:
|
||||
|
||||
[Step 1: Break down the process logically]
|
||||
[Step 2: Continue with sequential steps]
|
||||
|
||||
[Add more numbered steps as needed to cover the full workflow described by the user.]
|
||||
Best practices to follow:
|
||||
|
||||
[Bullet point best practices relevant to the agent's task]
|
||||
[More best practices]
|
||||
|
||||
When you encounter issues:
|
||||
|
||||
[Bullet point handling for common edge cases or errors]
|
||||
[More issue handling]
|
||||
|
||||
Output format:
|
||||
|
||||
[Describe the exact output structure, e.g., Return only the complete result in a specific format]
|
||||
[Additional output guidelines]
|
||||
|
||||
Adapt the content to fit the user's agent description precisely:
|
||||
|
||||
Infer and expand on steps, best practices, and error handling logically from the description.
|
||||
Ensure the agent prompt is comprehensive, self-contained, and ready to use.
|
||||
Keep the language professional, clear, and instructional.
|
||||
If the description involves tools or external interactions (e.g., HTTP requests), incorporate them appropriately in steps.
|
||||
|
||||
Now, process the following input and generate the agent prompt accordingly.
|
||||
@@ -1,51 +0,0 @@
|
||||
from typing import Literal
|
||||
from agentscope.agent import ReActAgent
|
||||
from agentscope.formatter import FormatterBase
|
||||
from agentscope.memory import LongTermMemoryBase, MemoryBase
|
||||
from agentscope.model import ChatModelBase
|
||||
from agentscope.plan import PlanNotebook
|
||||
from agentscope.rag import KnowledgeBase
|
||||
from agentscope.tool import Toolkit
|
||||
from agentscope.tts import TTSModelBase
|
||||
|
||||
|
||||
class Agent(ReActAgent):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
sys_prompt: str,
|
||||
model: ChatModelBase,
|
||||
formatter: FormatterBase,
|
||||
toolkit: Toolkit | None = None,
|
||||
memory: MemoryBase | None = None,
|
||||
long_term_memory: LongTermMemoryBase | None = None,
|
||||
long_term_memory_mode: (
|
||||
Literal["agent_control"] | Literal["static_control"] | Literal["both"]
|
||||
) = "both",
|
||||
enable_meta_tool: bool = False,
|
||||
parallel_tool_calls: bool = False,
|
||||
knowledge: KnowledgeBase | list[KnowledgeBase] | None = None,
|
||||
enable_rewrite_query: bool = True,
|
||||
plan_notebook: PlanNotebook | None = None,
|
||||
print_hint_msg: bool = False,
|
||||
max_iters: int = 10,
|
||||
tts_model: TTSModelBase | None = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name,
|
||||
sys_prompt,
|
||||
model,
|
||||
formatter,
|
||||
toolkit,
|
||||
memory,
|
||||
long_term_memory,
|
||||
long_term_memory_mode,
|
||||
enable_meta_tool,
|
||||
parallel_tool_calls,
|
||||
knowledge,
|
||||
enable_rewrite_query,
|
||||
plan_notebook,
|
||||
print_hint_msg,
|
||||
max_iters,
|
||||
tts_model,
|
||||
)
|
||||
@@ -1,95 +0,0 @@
|
||||
import asyncio
|
||||
from typing import Any
|
||||
import os
|
||||
import sys
|
||||
|
||||
from agentscope.agent import ReActAgent
|
||||
from agentscope.memory import InMemoryMemory
|
||||
from agentscope.message import Msg
|
||||
from agentscope.pipeline._functional import stream_printing_messages
|
||||
from agentscope.agent import ReActAgent
|
||||
from agentscope.model import DashScopeChatModel
|
||||
from agentscope.formatter import DashScopeChatFormatter
|
||||
|
||||
from agentrun.integration.agentscope import model, sandbox_toolset, toolset
|
||||
from agentrun.sandbox import TemplateType
|
||||
from agentrun.server import AgentRequest, AgentRunServer
|
||||
from agentrun.utils.log import logger
|
||||
|
||||
from agent import Agent
|
||||
from toolkit import toolkit, init_toolkit_sync
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "python"))
|
||||
|
||||
MODEL_NAME = "{{ .ChatModel }}"
|
||||
SANDBOX_NAME = os.getenv("AGENTRUN_SANDBOX_NAME")
|
||||
|
||||
if not MODEL_NAME:
|
||||
raise ValueError("请将 MODEL_NAME 替换为您已经创建的模型名称")
|
||||
|
||||
code_interpreter_tools = []
|
||||
if SANDBOX_NAME and not SANDBOX_NAME.startswith("<"):
|
||||
code_interpreter_tools = sandbox_toolset(
|
||||
template_name=SANDBOX_NAME,
|
||||
template_type=TemplateType.CODE_INTERPRETER,
|
||||
sandbox_idle_timeout_seconds=300,
|
||||
)
|
||||
else:
|
||||
logger.warning("SANDBOX_NAME 未设置或未替换,跳过加载沙箱工具。")
|
||||
|
||||
def load_sys_prompt(prompt_file_name="prompt.md"):
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
prompt_path = os.path.join(script_dir, prompt_file_name)
|
||||
|
||||
with open(prompt_path, 'r', encoding='utf-8') as f:
|
||||
return f.read()
|
||||
|
||||
agent = Agent(
|
||||
name="{{ .AgentName }}",
|
||||
model=model(MODEL_NAME), # type: ignore
|
||||
sys_prompt=load_sys_prompt(),
|
||||
toolkit=toolkit,
|
||||
memory=InMemoryMemory(),
|
||||
formatter=DashScopeChatFormatter(),
|
||||
)
|
||||
|
||||
|
||||
async def invoke_agent(request: AgentRequest):
|
||||
try:
|
||||
content = request.messages[0].content
|
||||
input_msg = Msg(
|
||||
name="user_message",
|
||||
content=content, # type: ignore
|
||||
role="user",
|
||||
)
|
||||
|
||||
async for msg, _ in stream_printing_messages(
|
||||
agents=[agent],
|
||||
coroutine_task=agent(input_msg),
|
||||
):
|
||||
text = msg.get_text_content()
|
||||
if text:
|
||||
yield text
|
||||
|
||||
except Exception:
|
||||
logger.exception("调用出错")
|
||||
raise
|
||||
|
||||
|
||||
def main():
|
||||
init_toolkit_sync()
|
||||
|
||||
AgentRunServer(invoke_agent=invoke_agent).start()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
"""
|
||||
curl 127.0.0.1:9000/openai/v1/chat/completions -XPOST \
|
||||
-H "content-type: application/json" \
|
||||
-d '{
|
||||
"messages": [{"role": "user", "content": "写一段代码,查询现在是几点?"}],
|
||||
"stream":true
|
||||
}'
|
||||
"""
|
||||
@@ -1,81 +0,0 @@
|
||||
edition: 3.0.0
|
||||
name: agentrun-app
|
||||
access: "{{ .AccessKey }}"
|
||||
|
||||
resources:
|
||||
hgctl-agent2:
|
||||
component: agentrun
|
||||
props:
|
||||
region: "{{ .Region }}"
|
||||
|
||||
# ============= 新规范:agent 配置 =============
|
||||
agent:
|
||||
# 基本信息
|
||||
name: "{{ .AgentName }}"
|
||||
description: "{{ .AgentDesc }}"
|
||||
|
||||
# 代码配置(直接指定路径,支持目录或 zip 文件,或使用 OSS 代码包)
|
||||
code:
|
||||
src: .
|
||||
# ossBucketName: funagent-agent-quickstart-langchain-demo-code
|
||||
# ossObjectName: agentrun-quickstart-code.zip
|
||||
language: python3.12
|
||||
command:
|
||||
- python3
|
||||
- agentrun_main.py
|
||||
|
||||
# 容器配置(使用容器模式时配置此项)
|
||||
# customContainerConfig:
|
||||
# image: registry.cn-hangzhou.aliyuncs.com/my-app:latest
|
||||
# command:
|
||||
# - python3
|
||||
# - app.py
|
||||
# port: 9000
|
||||
|
||||
# 资源配置
|
||||
cpu: 2.0
|
||||
memory: 4096
|
||||
diskSize: {{ .DiskSize }} # 可选,默认 512 MB
|
||||
timeout: {{ .Timeout }} # 可选,默认 600 秒
|
||||
|
||||
# 端口和并发
|
||||
port: {{ .Port }}
|
||||
instanceConcurrency: 100
|
||||
|
||||
# 网络配置 - 仅公网访问
|
||||
internetAccess: true
|
||||
|
||||
# VPC 配置(需要 VPC 内网访问时配置)
|
||||
# vpcConfig:
|
||||
# vpcId: vpc-xxx
|
||||
# vSwitchIds: [vsw-xxx] # 支持单个或多个
|
||||
# securityGroupId: sg-xxx
|
||||
# internetAccess: true # 同时配置 vpcConfig 和 internetAccess 表示内外网都可访问
|
||||
|
||||
# 环境变量,需要填写以下环境变量使用,推荐使用无明文AK方式,在下方填写授信给FC,包含AliyunAgentRunFullAccess的执行角色
|
||||
environmentVariables:
|
||||
AGENTRUN_ACCESS_KEY_ID: "{{ .GlobalConfig.AlibabaCloudAccessKeyID }}"
|
||||
AGENTRUN_ACCESS_KEY_SECRET: "{{ .GlobalConfig.AlibabaCloudAccessKeySecret }}"
|
||||
AGENTRUN_ACCOUNT_ID: "{{ .GlobalConfig.AgentRunAccountID }}"
|
||||
AGENTRUN_REGION: "{{ .GlobalConfig.AgentRunRegion }}"
|
||||
|
||||
# 执行角色,填写此角色,无需填写上方AK、SK敏感凭据的环境变量,角色需要授信给FC,包含AliyunAgentRunFullAccess
|
||||
# role: acs:ram::1160216277279558:role/AliyunFCDefaultRole
|
||||
|
||||
# 日志配置
|
||||
# logConfig:
|
||||
# project: ws-testhz
|
||||
# logstore: acs-ecs-system
|
||||
|
||||
# 端点配置
|
||||
endpoints:
|
||||
- name: prod
|
||||
|
||||
version: LATEST
|
||||
description: "生产环境端点"
|
||||
|
||||
# 灰度发布示例
|
||||
# - name: gray
|
||||
# version: 2
|
||||
# description: "灰度环境端点"
|
||||
# weight: 0.2 # 20% 流量到版本 2
|
||||
@@ -1,122 +0,0 @@
|
||||
import os
|
||||
import asyncio
|
||||
|
||||
from agentscope_runtime.engine import AgentApp
|
||||
from agentscope_runtime.engine.schemas.agent_schemas import AgentRequest
|
||||
|
||||
from agentscope.model import {{ .Provider }}Model
|
||||
from agentscope.formatter import {{ .Provider }}Formatter
|
||||
|
||||
from agentscope_runtime.adapters.agentscope.memory import AgentScopeSessionHistoryMemory
|
||||
from agentscope_runtime.engine.services.agent_state import InMemoryStateService
|
||||
from agentscope_runtime.engine.services.session_history import InMemorySessionHistoryService
|
||||
|
||||
from agentscope.pipeline import stream_printing_messages
|
||||
|
||||
from agentscope_runtime.engine.deployers.local_deployer import LocalDeployManager
|
||||
from agentscope_runtime.engine.deployers.utils.deployment_modes import DeploymentMode
|
||||
|
||||
from agent import Agent
|
||||
from toolkit import toolkit, init_toolkit_sync
|
||||
|
||||
app = AgentApp(
|
||||
app_name="{{.AppName}}",
|
||||
app_description="{{.AppDescription}}",
|
||||
)
|
||||
|
||||
def load_sys_prompt(prompt_file_name="prompt.md"):
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
prompt_path = os.path.join(script_dir, prompt_file_name)
|
||||
|
||||
with open(prompt_path, 'r', encoding='utf-8') as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
@app.init
|
||||
async def init_func(self):
|
||||
"""初始化状态和会话服务"""
|
||||
self.state_service = InMemoryStateService()
|
||||
self.session_service = InMemorySessionHistoryService()
|
||||
await self.state_service.start()
|
||||
await self.session_service.start()
|
||||
|
||||
|
||||
@app.shutdown
|
||||
async def shutdown_func(self):
|
||||
"""清理服务"""
|
||||
await self.state_service.stop()
|
||||
await self.session_service.stop()
|
||||
|
||||
@app.query(framework="agentscope")
|
||||
async def query_func(self, msgs, request: AgentRequest, **kwargs):
|
||||
session_id = request.session_id
|
||||
user_id = request.user_id
|
||||
|
||||
# 恢复 Agent 状态
|
||||
state = await self.state_service.export_state(
|
||||
session_id=session_id,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
# ---- 创建 Agent ----
|
||||
agent = Agent(
|
||||
name="{{.AgentName}}",
|
||||
model={{ .Provider }}Model(
|
||||
"{{.ChatModel}}",
|
||||
api_key=os.getenv("{{.APIKeyEnvVar}}"),
|
||||
stream={{.EnableStreaming | boolToPython}},
|
||||
),
|
||||
sys_prompt=load_sys_prompt(),
|
||||
toolkit=toolkit,
|
||||
memory=AgentScopeSessionHistoryMemory(
|
||||
service=self.session_service,
|
||||
session_id=session_id,
|
||||
user_id=user_id,
|
||||
),
|
||||
formatter={{ .Provider }}Formatter(),
|
||||
)
|
||||
agent.set_console_output_enabled(enabled=False)
|
||||
|
||||
# 恢复状态
|
||||
if state:
|
||||
agent.load_state_dict(state)
|
||||
|
||||
# ---- 流式输出 ----
|
||||
async for msg, last in stream_printing_messages(
|
||||
agents=[agent],
|
||||
coroutine_task=agent(msgs),
|
||||
):
|
||||
yield msg, last
|
||||
|
||||
# ---- 保存 Agent 状态 ----
|
||||
state = agent.state_dict()
|
||||
await self.state_service.save_state(
|
||||
user_id=user_id,
|
||||
session_id=session_id,
|
||||
state=state,
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
"""以独立进程模式部署应用"""
|
||||
deployment_info = await app.deploy(
|
||||
LocalDeployManager(host="{{.HostBinding}}", port={{.DeploymentPort}}),
|
||||
mode=DeploymentMode.DETACHED_PROCESS,
|
||||
)
|
||||
url = deployment_info['url']
|
||||
print(f"✅ 部署成功:{url}")
|
||||
print(f"📍 部署 ID:{deployment_info['deploy_id']}")
|
||||
print(
|
||||
f"""
|
||||
Check health: curl {url}/health
|
||||
Shutdown: curl -X POST {url}/admin/shutdown
|
||||
"""
|
||||
)
|
||||
print(f"🌟 You can deploy it to Higress by using: hgctl agent add {url}")
|
||||
|
||||
return deployment_info
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
init_toolkit_sync()
|
||||
asyncio.run(main())
|
||||
@@ -1,69 +0,0 @@
|
||||
import os
|
||||
import asyncio
|
||||
|
||||
from agentscope.tool import Toolkit
|
||||
from agentscope.tool import execute_shell_command
|
||||
from agentscope.tool import view_text_file
|
||||
from agentscope.tool import write_text_file
|
||||
from agentscope.tool import insert_text_file
|
||||
from agentscope.tool import dashscope_text_to_image
|
||||
from agentscope.tool import dashscope_text_to_audio
|
||||
from agentscope.tool import dashscope_image_to_text
|
||||
from agentscope.tool import openai_text_to_image
|
||||
from agentscope.tool import openai_text_to_audio
|
||||
from agentscope.tool import openai_edit_image
|
||||
from agentscope.tool import openai_create_image_variation
|
||||
from agentscope.tool import openai_image_to_text
|
||||
from agentscope.tool import openai_audio_to_text
|
||||
from agentscope.tool import execute_python_code
|
||||
from agentscope.mcp import HttpStatelessClient
|
||||
|
||||
toolkit = Toolkit()
|
||||
|
||||
|
||||
def _register_tools():
|
||||
{{range .AvailableTools}}
|
||||
toolkit.register_tool_function({{.}})
|
||||
{{else}}
|
||||
pass
|
||||
{{end}}
|
||||
|
||||
|
||||
def init_toolkit_sync():
|
||||
_register_tools()
|
||||
asyncio.run(register_all_MCP(toolkit))
|
||||
|
||||
|
||||
async def init_toolkit_async():
|
||||
_register_tools()
|
||||
await register_all_MCP(toolkit)
|
||||
|
||||
|
||||
async def register_single_MCP(toolkit: Toolkit, mcp_config):
|
||||
"""注册单个MCP服务器"""
|
||||
headers = mcp_config.get("Headers") or None
|
||||
|
||||
api_client = HttpStatelessClient(
|
||||
name=mcp_config["Name"],
|
||||
transport=mcp_config["Transport"],
|
||||
url=mcp_config["URL"],
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
await toolkit.register_mcp_client(api_client)
|
||||
|
||||
|
||||
async def register_all_MCP(toolkit: Toolkit):
|
||||
"""注册所有配置的MCP服务器"""
|
||||
{{- range .MCPServers }}
|
||||
await register_single_MCP(toolkit, {
|
||||
"Name": "{{ .Name }}",
|
||||
"URL": "{{ .URL }}",
|
||||
"Transport": "{{ .Transport }}",
|
||||
"Headers": {
|
||||
{{- range $key, $value := .Headers }}
|
||||
"{{ $key }}": "{{ $value }}",
|
||||
{{- end }}
|
||||
}
|
||||
})
|
||||
{{- end }}
|
||||
@@ -15,11 +15,9 @@
|
||||
package manifests
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// FS embeds the manifests
|
||||
@@ -27,7 +25,6 @@ import (
|
||||
//go:embed profiles/*
|
||||
//go:embed gatewayapi/*
|
||||
//go:embed istiobase/*
|
||||
//go:embed agent/*
|
||||
var FS embed.FS
|
||||
|
||||
// BuiltinOrDir returns a FS for the provided directory. If no directory is passed, the compiled in
|
||||
@@ -38,39 +35,3 @@ func BuiltinOrDir(dir string) fs.FS {
|
||||
}
|
||||
return os.DirFS(dir)
|
||||
}
|
||||
|
||||
// This funciton will write the embed sourceDir's files to target dir
|
||||
func ExtractEmbedFiles(fsys fs.FS, srcDir, targetDir string) error {
|
||||
return fs.WalkDir(fsys, srcDir, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
relDir, err := filepath.Rel(srcDir, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
targetPath := filepath.Join(targetDir, relDir)
|
||||
|
||||
if d.IsDir() {
|
||||
return os.MkdirAll(targetPath, 0755)
|
||||
}
|
||||
|
||||
data, err := fs.ReadFile(fsys, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if this file already exists, then return
|
||||
existing, err := os.ReadFile(targetPath)
|
||||
if err == nil {
|
||||
if bytes.Equal(existing, data) {
|
||||
return nil
|
||||
}
|
||||
} else if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(targetPath, data, 0644)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
// 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 util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func GetPythonVersion() (string, error) {
|
||||
re := regexp.MustCompile(`\d+\.\d+(\.\d+)?`)
|
||||
|
||||
for _, cmd := range []string{"python3", "python"} {
|
||||
out, err := exec.Command(cmd, "--version").CombinedOutput()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
version := strings.TrimSpace(string(out))
|
||||
match := re.FindString(version)
|
||||
if match != "" {
|
||||
return match, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("python not found")
|
||||
}
|
||||
|
||||
// compareVersions compares two version strings like "3.11.2" and "3.10".
|
||||
// Returns:
|
||||
//
|
||||
// 1 if v1 > v2
|
||||
// 0 if v1 == v2
|
||||
// -1 if v1 < v2
|
||||
func CompareVersions(v1, v2 string) int {
|
||||
// Extract numeric parts only (e.g. "3.12.0b1" → "3.12.0")
|
||||
re := regexp.MustCompile(`\d+`)
|
||||
nums1 := re.FindAllString(v1, -1)
|
||||
nums2 := re.FindAllString(v2, -1)
|
||||
|
||||
maxLen := len(nums1)
|
||||
if len(nums2) > maxLen {
|
||||
maxLen = len(nums2)
|
||||
}
|
||||
|
||||
// Compare each part
|
||||
for i := 0; i < maxLen; i++ {
|
||||
var n1, n2 int
|
||||
if i < len(nums1) {
|
||||
n1, _ = strconv.Atoi(nums1[i])
|
||||
}
|
||||
if i < len(nums2) {
|
||||
n2, _ = strconv.Atoi(nums2[i])
|
||||
}
|
||||
|
||||
if n1 > n2 {
|
||||
return 1
|
||||
} else if n1 < n2 {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
@@ -16,11 +16,9 @@ package util
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
@@ -95,33 +93,3 @@ func WriteFileString(fileName string, content string, perm os.FileMode) error {
|
||||
writer.Flush()
|
||||
return nil
|
||||
}
|
||||
|
||||
// This function return ~/.hgctl file_path string (Currently Linux only)
|
||||
func GetHomeHgctlDir() string {
|
||||
homeDir, _ := os.UserHomeDir()
|
||||
targetDir := filepath.Join(homeDir, ".hgctl")
|
||||
return targetDir
|
||||
}
|
||||
|
||||
func GetSpecificAgentDir(name string) (string, error) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get user home directory: %w", err)
|
||||
}
|
||||
|
||||
targetDir := filepath.Join(homeDir, ".hgctl", "agents", name)
|
||||
|
||||
info, err := os.Stat(targetDir)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return "", fmt.Errorf("agent %q does not exist", name)
|
||||
}
|
||||
return "", fmt.Errorf("failed to stat agent directory %q: %w", targetDir, err)
|
||||
}
|
||||
|
||||
if !info.IsDir() {
|
||||
return "", fmt.Errorf("agent %q exists but is not a directory", name)
|
||||
}
|
||||
|
||||
return targetDir, nil
|
||||
}
|
||||
|
||||
Submodule istio/api updated: 5b9a222e72...d54f2e0f42
Submodule istio/client-go updated: 09ed8dc4e7...9b3841d7de
Submodule istio/istio updated: c4703274ca...1a18c6d5b6
@@ -20,9 +20,6 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"istio.io/istio/pkg/config/mesh/meshwatcher"
|
||||
"istio.io/istio/pkg/kube/krt"
|
||||
|
||||
prometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/reflection"
|
||||
@@ -35,16 +32,20 @@ import (
|
||||
"istio.io/istio/pilot/pkg/serviceregistry/aggregate"
|
||||
kubecontroller "istio.io/istio/pilot/pkg/serviceregistry/kube/controller"
|
||||
"istio.io/istio/pilot/pkg/xds"
|
||||
"istio.io/istio/pkg/cluster"
|
||||
"istio.io/istio/pkg/config"
|
||||
"istio.io/istio/pkg/config/constants"
|
||||
"istio.io/istio/pkg/config/mesh"
|
||||
"istio.io/istio/pkg/config/schema/collections"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
"istio.io/istio/pkg/config/schema/kind"
|
||||
"istio.io/istio/pkg/keepalive"
|
||||
istiokube "istio.io/istio/pkg/kube"
|
||||
"istio.io/istio/pkg/log"
|
||||
"istio.io/istio/pkg/security"
|
||||
"istio.io/istio/security/pkg/server/ca/authenticate"
|
||||
"istio.io/istio/security/pkg/server/ca/authenticate/kubeauth"
|
||||
"istio.io/pkg/ledger"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
@@ -151,7 +152,7 @@ type Server struct {
|
||||
func NewServer(args *ServerArgs) (*Server, error) {
|
||||
e := model.NewEnvironment()
|
||||
e.DomainSuffix = constants.DefaultClusterLocalDomain
|
||||
//e.SetLedger(buildLedger(args.RegistryOptions))
|
||||
e.SetLedger(buildLedger(args.RegistryOptions))
|
||||
ac := aggregate.NewController(aggregate.Options{
|
||||
MeshHolder: e,
|
||||
})
|
||||
@@ -163,7 +164,7 @@ func NewServer(args *ServerArgs) (*Server, error) {
|
||||
readinessProbes: make(map[string]readinessProbe),
|
||||
server: server.New(),
|
||||
}
|
||||
s.environment.Watcher = meshwatcher.NewTestWatcher(&v1alpha1.MeshConfig{})
|
||||
s.environment.Watcher = mesh.NewFixedWatcher(&v1alpha1.MeshConfig{})
|
||||
s.environment.Init()
|
||||
initFuncList := []func() error{
|
||||
s.initKubeClient,
|
||||
@@ -201,7 +202,7 @@ func (s *Server) initRegistryEventHandlers() error {
|
||||
pushReq := &model.PushRequest{
|
||||
Full: true,
|
||||
ConfigsUpdated: map[model.ConfigKey]struct{}{{
|
||||
Kind: gvk.MustToKind(curr.GroupVersionKind),
|
||||
Kind: kind.MustFromGVK(curr.GroupVersionKind),
|
||||
Name: curr.Name,
|
||||
Namespace: curr.Namespace,
|
||||
}: {}},
|
||||
@@ -339,7 +340,7 @@ func (s *Server) WaitUntilCompletion() {
|
||||
|
||||
func (s *Server) initXdsServer() error {
|
||||
log.Info("init xds server")
|
||||
s.xdsServer = xds.NewDiscoveryServer(s.environment, s.RegistryOptions.KubeOptions.ClusterAliases, krt.GlobalDebugHandler)
|
||||
s.xdsServer = xds.NewDiscoveryServer(s.environment, higressconfig.PodName, cluster.ID(higressconfig.PodNamespace), s.RegistryOptions.KubeOptions.ClusterAliases)
|
||||
generatorOptions := mcp.GeneratorOptions{KeepConfigLabels: s.XdsOptions.KeepConfigLabels, KeepConfigAnnotations: s.XdsOptions.KeepConfigAnnotations}
|
||||
s.xdsServer.Generators[gvk.WasmPlugin.String()] = &mcp.WasmPluginGenerator{Environment: s.environment, Server: s.xdsServer, GeneratorOptions: generatorOptions}
|
||||
s.xdsServer.Generators[gvk.DestinationRule.String()] = &mcp.DestinationRuleGenerator{Environment: s.environment, Server: s.xdsServer, GeneratorOptions: generatorOptions}
|
||||
@@ -353,8 +354,8 @@ func (s *Server) initXdsServer() error {
|
||||
s.xdsServer.Generators[gvk] = &mcp.FallbackGenerator{Environment: s.environment, Server: s.xdsServer}
|
||||
}
|
||||
}
|
||||
s.xdsServer.ProxyNeedsPush = func(proxy *model.Proxy, req *model.PushRequest) (*model.PushRequest, bool) {
|
||||
return req, true
|
||||
s.xdsServer.ProxyNeedsPush = func(proxy *model.Proxy, req *model.PushRequest) bool {
|
||||
return true
|
||||
}
|
||||
s.server.RunComponent("xds-server", func(stop <-chan struct{}) error {
|
||||
log.Infof("Starting ADS server")
|
||||
@@ -381,7 +382,7 @@ func (s *Server) initAuthenticators() error {
|
||||
&authenticate.ClientCertAuthenticator{},
|
||||
}
|
||||
authenticators = append(authenticators,
|
||||
kubeauth.NewKubeJWTAuthenticator(s.environment.Watcher, s.kubeClient.Kube(), s.RegistryOptions.KubeOptions.ClusterID, nil, nil))
|
||||
kubeauth.NewKubeJWTAuthenticator(s.environment.Watcher, s.kubeClient.Kube(), s.RegistryOptions.KubeOptions.ClusterID, nil, features.JwtPolicy))
|
||||
if features.XDSAuth {
|
||||
s.xdsServer.Authenticators = authenticators
|
||||
}
|
||||
@@ -437,17 +438,10 @@ func (s *Server) initHttpServer() error {
|
||||
}
|
||||
s.xdsServer.AddDebugHandlers(s.httpMux, nil, true, nil)
|
||||
s.httpMux.HandleFunc("/ready", s.readyHandler)
|
||||
s.httpMux.HandleFunc("/registry/watcherStatus", s.withConditionalAuth(s.registryWatcherStatusHandler))
|
||||
s.httpMux.HandleFunc("/registry/watcherStatus", s.registryWatcherStatusHandler)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) withConditionalAuth(handler http.HandlerFunc) http.HandlerFunc {
|
||||
if features.DebugAuth {
|
||||
return s.xdsServer.AllowAuthenticatedOrLocalhost(handler)
|
||||
}
|
||||
return handler
|
||||
}
|
||||
|
||||
// readyHandler checks whether the http server is ready
|
||||
func (s *Server) readyHandler(w http.ResponseWriter, _ *http.Request) {
|
||||
for name, fn := range s.readinessProbes {
|
||||
@@ -534,13 +528,12 @@ func (s *Server) pushContextReady(expected int64) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// ledger has been removed in istio 1.27
|
||||
//func buildLedger(ca RegistryOptions) ledger.Ledger {
|
||||
// var result ledger.Ledger
|
||||
// if ca.DistributionTrackingEnabled {
|
||||
// result = ledger.Make(ca.DistributionCacheRetention)
|
||||
// } else {
|
||||
// result = &pkgcommon.DisabledLedger{}
|
||||
// }
|
||||
// return result
|
||||
//}
|
||||
func buildLedger(ca RegistryOptions) ledger.Ledger {
|
||||
var result ledger.Ledger
|
||||
if ca.DistributionTrackingEnabled {
|
||||
result = ledger.Make(ca.DistributionCacheRetention)
|
||||
} else {
|
||||
result = &model.DisabledLedger{}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -16,9 +16,7 @@ package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/agiledragon/gomonkey/v2"
|
||||
"istio.io/istio/pilot/pkg/features"
|
||||
@@ -33,11 +31,8 @@ func TestStartWithNoError(t *testing.T) {
|
||||
err error
|
||||
)
|
||||
|
||||
// Create fake client first
|
||||
fakeClient := higresskube.NewFakeClient()
|
||||
|
||||
mockFn := func(s *Server) error {
|
||||
s.kubeClient = fakeClient
|
||||
s.kubeClient = higresskube.NewFakeClient()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -45,49 +40,13 @@ func TestStartWithNoError(t *testing.T) {
|
||||
|
||||
if s, err = NewServer(newServerArgs()); err != nil {
|
||||
t.Errorf("failed to create server: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// Start the fake client informers first
|
||||
go fakeClient.RunAndWait(ctx.Done())
|
||||
|
||||
// Give the client a moment to start informers
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var startErr error
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
startErr = s.Start(ctx.Done())
|
||||
}()
|
||||
|
||||
// Give the server a moment to start
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// Cancel context to trigger shutdown
|
||||
cancel()
|
||||
|
||||
// Wait for server to shutdown with timeout
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
// Server may fail to sync cache in test environment due to missing resources,
|
||||
// which is acceptable for this test. The important thing is that the server
|
||||
// doesn't panic and handles shutdown gracefully.
|
||||
if startErr != nil && startErr.Error() != "failed to sync cache" {
|
||||
t.Logf("Server shutdown with error (may be expected in test env): %v", startErr)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Errorf("server did not shutdown within timeout")
|
||||
if err = s.Start(ctx.Done()); err != nil {
|
||||
t.Errorf("failed to start the server: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -43,6 +42,7 @@ import (
|
||||
"istio.io/istio/pkg/config/constants"
|
||||
"istio.io/istio/pkg/config/schema/collection"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
"istio.io/istio/pkg/config/schema/kind"
|
||||
"istio.io/istio/pkg/log"
|
||||
"istio.io/istio/pkg/util/sets"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
@@ -279,7 +279,7 @@ func (m *IngressConfig) AddLocalCluster(options common.Options) {
|
||||
}
|
||||
m.remoteIngressControllers[options.ClusterId] = ingressController
|
||||
if features.EnableGatewayAPI {
|
||||
m.remoteGatewayControllers[options.ClusterId] = gateway.NewController(m.localKubeClient, options, m.XDSUpdater)
|
||||
m.remoteGatewayControllers[options.ClusterId] = gateway.NewController(m.localKubeClient, options)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1054,17 +1054,7 @@ func (m *IngressConfig) convertIstioWasmPlugin(obj *higressext.WasmPlugin) (*ext
|
||||
validRule := false
|
||||
var matchItems []*_struct.Value
|
||||
// match ingress
|
||||
// if route type is not http, we should re-generate the route name for ingress matching
|
||||
// this is because the route name
|
||||
needAppendRuleType := false
|
||||
if rule.GetRouteType() != higressext.RouteType_HTTP {
|
||||
needAppendRuleType = true
|
||||
}
|
||||
|
||||
for _, ing := range rule.Ingress {
|
||||
if needAppendRuleType {
|
||||
ing = path.Join(rule.GetRouteType().String())
|
||||
}
|
||||
matchItems = append(matchItems, &_struct.Value{
|
||||
Kind: &_struct.Value_StringValue{
|
||||
StringValue: ing,
|
||||
@@ -1320,11 +1310,11 @@ func (m *IngressConfig) AddOrUpdateHttp2Rpc(clusterNamespacedName util.ClusterNa
|
||||
m.http2rpcs[clusterNamespacedName.Name] = &http2rpc.Spec
|
||||
m.mutex.Unlock()
|
||||
IngressLog.Infof("AddOrUpdateHttp2Rpc http2rpc ingress name %s", clusterNamespacedName.Name)
|
||||
push := func(GVK config.GroupVersionKind) {
|
||||
push := func(gvk config.GroupVersionKind) {
|
||||
m.XDSUpdater.ConfigUpdate(&istiomodel.PushRequest{
|
||||
Full: true,
|
||||
ConfigsUpdated: map[istiomodel.ConfigKey]struct{}{{
|
||||
Kind: gvk.MustToKind(GVK),
|
||||
Kind: kind.MustFromGVK(gvk),
|
||||
Name: clusterNamespacedName.Name,
|
||||
Namespace: clusterNamespacedName.Namespace,
|
||||
}: {}},
|
||||
@@ -1349,11 +1339,11 @@ func (m *IngressConfig) DeleteHttp2Rpc(clusterNamespacedName util.ClusterNamespa
|
||||
m.mutex.Unlock()
|
||||
if hit {
|
||||
IngressLog.Infof("Http2Rpc triggered deleted event executed %s", clusterNamespacedName.Name)
|
||||
push := func(GVK config.GroupVersionKind) {
|
||||
push := func(gvk config.GroupVersionKind) {
|
||||
m.XDSUpdater.ConfigUpdate(&istiomodel.PushRequest{
|
||||
Full: true,
|
||||
ConfigsUpdated: map[istiomodel.ConfigKey]struct{}{{
|
||||
Kind: gvk.MustToKind(GVK),
|
||||
Kind: kind.MustFromGVK(gvk),
|
||||
Name: clusterNamespacedName.Name,
|
||||
Namespace: clusterNamespacedName.Namespace,
|
||||
}: {}},
|
||||
@@ -1374,11 +1364,11 @@ func (m *IngressConfig) ReflectSecretChanges(clusterNamespacedName util.ClusterN
|
||||
m.mutex.RUnlock()
|
||||
|
||||
if hit {
|
||||
push := func(GVK config.GroupVersionKind) {
|
||||
push := func(gvk config.GroupVersionKind) {
|
||||
m.XDSUpdater.ConfigUpdate(&istiomodel.PushRequest{
|
||||
Full: true,
|
||||
ConfigsUpdated: map[istiomodel.ConfigKey]struct{}{{
|
||||
Kind: gvk.MustToKind(GVK),
|
||||
Kind: kind.MustFromGVK(gvk),
|
||||
Name: clusterNamespacedName.Name,
|
||||
Namespace: clusterNamespacedName.Namespace,
|
||||
}: {}},
|
||||
@@ -2068,11 +2058,11 @@ func (m *IngressConfig) constructMcpSseStatefulSessionEnvoyFilter(route *common.
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *IngressConfig) notifyXDSFullUpdate(GVK config.GroupVersionKind, reason istiomodel.TriggerReason, updatedConfigName *util.ClusterNamespacedName) {
|
||||
func (m *IngressConfig) notifyXDSFullUpdate(gvk config.GroupVersionKind, reason istiomodel.TriggerReason, updatedConfigName *util.ClusterNamespacedName) {
|
||||
var configsUpdated map[istiomodel.ConfigKey]struct{}
|
||||
if updatedConfigName != nil {
|
||||
configsUpdated = map[istiomodel.ConfigKey]struct{}{{
|
||||
Kind: gvk.MustToKind(GVK),
|
||||
Kind: kind.MustFromGVK(gvk),
|
||||
Name: updatedConfigName.Name,
|
||||
Namespace: updatedConfigName.Namespace,
|
||||
}: {}}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user