新增运行策略 agentMaxSessions(默认 30,范围 1-64),保存后同时下发到内嵌进程闸门与独立 yingce-agent 的 /v1/limit,降低上限不会中断已开始的对话。系统性能页展示 Agent 服务健康、进行中/排队数量,并可直接修改上限。
132 lines
4.1 KiB
Go
132 lines
4.1 KiB
Go
package yingceagent
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
agentruntime "infinite-canvas/backend/internal/agent/runtime"
|
|
)
|
|
|
|
type Status struct {
|
|
OK bool `json:"ok"`
|
|
Active int `json:"active"`
|
|
Limit int `json:"limit"`
|
|
Queued int `json:"queued"`
|
|
}
|
|
|
|
func Inspect(ctx context.Context, endpoint string) (Status, error) {
|
|
endpoint = strings.TrimRight(strings.TrimSpace(endpoint), "/")
|
|
if endpoint == "" {
|
|
return Status{}, fmt.Errorf("YINGCE_AGENT_URL is empty")
|
|
}
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint+"/healthz", nil)
|
|
if err != nil {
|
|
return Status{}, err
|
|
}
|
|
response, err := remoteClient.Do(request)
|
|
if err != nil {
|
|
return Status{}, err
|
|
}
|
|
defer response.Body.Close()
|
|
if response.StatusCode != http.StatusOK {
|
|
return Status{}, fmt.Errorf("yingce-agent health returned HTTP %d", response.StatusCode)
|
|
}
|
|
var status Status
|
|
if err := json.NewDecoder(io.LimitReader(response.Body, 4096)).Decode(&status); err != nil {
|
|
return Status{}, err
|
|
}
|
|
return status, nil
|
|
}
|
|
|
|
func SetLimit(ctx context.Context, endpoint, serviceToken string, limit int) error {
|
|
endpoint = strings.TrimRight(strings.TrimSpace(endpoint), "/")
|
|
serviceToken = strings.TrimSpace(serviceToken)
|
|
if endpoint == "" {
|
|
return fmt.Errorf("YINGCE_AGENT_URL is empty")
|
|
}
|
|
if len(serviceToken) < 32 {
|
|
return fmt.Errorf("YINGCE_AGENT_TOKEN must contain at least 32 characters")
|
|
}
|
|
payload, err := json.Marshal(map[string]int{"maxSessions": limit})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodPut, endpoint+"/v1/limit", bytes.NewReader(payload))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
request.Header.Set("Authorization", "Bearer "+serviceToken)
|
|
request.Header.Set("Content-Type", "application/json")
|
|
response, err := remoteClient.Do(request)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer response.Body.Close()
|
|
if response.StatusCode != http.StatusOK {
|
|
detail, _ := io.ReadAll(io.LimitReader(response.Body, 1024))
|
|
return fmt.Errorf("yingce-agent limit returned HTTP %d: %s", response.StatusCode, strings.TrimSpace(string(detail)))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Run executes one Agent session on the independent yingce-agent service.
|
|
// The Go process keeps the bridge, billing, and tool execution. A configured
|
|
// remote endpoint never falls back to an embedded Node process: doing so can
|
|
// run the same step twice.
|
|
func Run(ctx context.Context, endpoint, serviceToken, bridgeHost string, request agentruntime.ProcessRequest, bridge agentruntime.Bridge) error {
|
|
endpoint = strings.TrimRight(strings.TrimSpace(endpoint), "/")
|
|
serviceToken = strings.TrimSpace(serviceToken)
|
|
if endpoint == "" {
|
|
return fmt.Errorf("YINGCE_AGENT_URL is empty")
|
|
}
|
|
if len(serviceToken) < 32 {
|
|
return fmt.Errorf("YINGCE_AGENT_TOKEN must contain at least 32 characters")
|
|
}
|
|
server, err := agentruntime.StartBridge("0.0.0.0:0", bridgeHost, bridge)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer server.Close()
|
|
request.BridgeURL = server.URL
|
|
request.BridgeToken = server.Token
|
|
payload, err := json.Marshal(request)
|
|
if err != nil {
|
|
return fmt.Errorf("encode remote Agent request: %w", err)
|
|
}
|
|
httpRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint+"/v1/runs", bytes.NewReader(payload))
|
|
if err != nil {
|
|
return fmt.Errorf("create remote Agent request: %w", err)
|
|
}
|
|
httpRequest.Header.Set("Authorization", "Bearer "+serviceToken)
|
|
httpRequest.Header.Set("Content-Type", "application/json")
|
|
response, err := remoteClient.Do(httpRequest)
|
|
if err != nil {
|
|
return fmt.Errorf("call yingce-agent: %w", err)
|
|
}
|
|
defer response.Body.Close()
|
|
if response.StatusCode != http.StatusOK {
|
|
detail, _ := io.ReadAll(io.LimitReader(response.Body, 4096))
|
|
return fmt.Errorf("yingce-agent returned HTTP %d: %s", response.StatusCode, strings.TrimSpace(string(detail)))
|
|
}
|
|
return agentruntime.ReadOutput(response.Body)
|
|
}
|
|
|
|
var remoteClient = &http.Client{
|
|
Transport: &http.Transport{
|
|
Proxy: http.ProxyFromEnvironment,
|
|
DialContext: (&net.Dialer{
|
|
Timeout: 10 * time.Second,
|
|
KeepAlive: 30 * time.Second,
|
|
}).DialContext,
|
|
ForceAttemptHTTP2: true,
|
|
IdleConnTimeout: 90 * time.Second,
|
|
},
|
|
}
|