Files
ttstd-infinite-canvas/backend/internal/agent/yingceagent/client.go
ddcat666 0ac61434df feat(admin): Agent 并发 - 后台可调同时对话上限并展示 Agent 服务状态
新增运行策略 agentMaxSessions(默认 30,范围 1-64),保存后同时下发到内嵌进程闸门与独立 yingce-agent 的 /v1/limit,降低上限不会中断已开始的对话。系统性能页展示 Agent 服务健康、进行中/排队数量,并可直接修改上限。
2026-09-30 03:37:54 +08:00

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,
},
}