mirror of
https://github.com/alibaba/higress.git
synced 2026-02-24 20:50:51 +08:00
488 lines
13 KiB
Go
488 lines
13 KiB
Go
// 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 (
|
|
"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 (
|
|
NodeLeastVersion = 18
|
|
)
|
|
|
|
type HimarketAdminAuthArg struct {
|
|
hmURL string
|
|
hmUser string
|
|
hmPassword string
|
|
}
|
|
|
|
// Developer's page
|
|
type HimarketDevAuthArg struct {
|
|
hmURL string
|
|
hmUser string
|
|
hmPassword string
|
|
}
|
|
|
|
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 !p.checkAgentInstall() {
|
|
if err := p.promptAgentInstall(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
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()
|
|
}
|