feat(admin): Agent 并发 - 后台可调同时对话上限并展示 Agent 服务状态

新增运行策略 agentMaxSessions(默认 30,范围 1-64),保存后同时下发到内嵌进程闸门与独立 yingce-agent 的 /v1/limit,降低上限不会中断已开始的对话。系统性能页展示 Agent 服务健康、进行中/排队数量,并可直接修改上限。
This commit is contained in:
ddcat666
2026-09-30 03:37:54 +08:00
parent b59317415d
commit 0ac61434df
17 changed files with 408 additions and 29 deletions

View File

@@ -14,6 +14,68 @@ import (
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

View File

@@ -116,4 +116,14 @@ if (!response.ok) {
if !called {
t.Fatal("remote runtime did not call the Go tool bridge")
}
if err := SetLimit(ctx, endpoint, token, 16); err != nil {
t.Fatal(err)
}
status, err := Inspect(ctx, endpoint)
if err != nil {
t.Fatal(err)
}
if status.Limit != 16 {
t.Fatalf("limit = %d, want 16", status.Limit)
}
}