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

@@ -3,14 +3,18 @@ package app
import (
"context"
"errors"
"fmt"
"os"
"runtime"
"strconv"
"strings"
"time"
agentruntime "infinite-canvas/backend/internal/agent/runtime"
"infinite-canvas/backend/internal/agent/yingceagent"
"infinite-canvas/backend/internal/buildinfo"
"infinite-canvas/backend/internal/model"
"infinite-canvas/backend/internal/platform"
"infinite-canvas/backend/internal/repository"
"github.com/redis/go-redis/v9"
@@ -28,9 +32,23 @@ type AdminSystemPerformance struct {
Disk SystemPerformanceDisk `json:"disk"`
Database repository.DatabaseRuntimeStats `json:"database"`
Redis SystemPerformanceRedis `json:"redis"`
Agents []SystemPerformanceAgent `json:"agents"`
Build buildinfo.Info `json:"build"`
}
type SystemPerformanceAgent struct {
ID string `json:"id"`
Name string `json:"name"`
Mode string `json:"mode"`
Endpoint string `json:"endpoint"`
Healthy bool `json:"healthy"`
Active int `json:"active"`
Limit int `json:"limit"`
Queued int `json:"queued"`
ConfiguredLimit int `json:"configuredLimit"`
StatusMessage string `json:"statusMessage,omitempty"`
}
type SystemPerformanceHost struct {
Hostname string `json:"hostname"`
OS string `json:"os"`
@@ -149,17 +167,98 @@ func (s *Service) AdminSystemPerformance(ctx context.Context, actor *model.User)
disk := collectSystemDisk(s.dataDir)
databaseStats, databaseErr := s.repo.DatabaseRuntimeStats(ctx)
redisStats := s.collectRedisPerformance(ctx)
agents := s.collectAgentServices(ctx)
status := "healthy"
if databaseErr != nil || !databaseStats.Connected || !disk.Available || !disk.Writable || s.ValidateRuntime() != nil || (redisStats.Configured && !redisStats.Connected) {
if databaseErr != nil || !databaseStats.Connected || !disk.Available || !disk.Writable || s.ValidateRuntime() != nil || (redisStats.Configured && !redisStats.Connected) || agentServicesDegraded(agents) {
status = "degraded"
}
return &AdminSystemPerformance{
CollectedAt: time.Now(), Status: status,
Host: SystemPerformanceHost{Hostname: hostname, OS: runtime.GOOS, Arch: runtime.GOARCH, CPUCores: runtime.NumCPU(), GOMAXPROCS: runtime.GOMAXPROCS(0), ProcessID: os.Getpid(), UptimeSeconds: int64(time.Since(systemProcessStartedAt).Seconds()), Goroutines: runtime.NumGoroutine(), ActiveWorkerTasks: s.ActiveWorkerTasks(), LoadAverage: load, LoadAverageAvailable: loadAvailable},
Memory: memory, Disk: disk, Database: databaseStats, Redis: redisStats, Build: buildinfo.Current(),
Memory: memory, Disk: disk, Database: databaseStats, Redis: redisStats, Agents: agents, Build: buildinfo.Current(),
}, nil
}
type AgentSessionLimitRequest struct {
MaxSessions int `json:"maxSessions"`
}
func (s *Service) UpdateAgentSessionLimit(ctx context.Context, actor *model.User, maxSessions int) (*SystemPerformanceAgent, error) {
if err := s.RequireAdmin(actor); err != nil {
return nil, err
}
if maxSessions < platform.MinRuntimeAgentSessions || maxSessions > platform.MaxRuntimeAgentSessions {
return nil, BadAuthRequest(fmt.Sprintf("Agent 同时对话上限必须是 %d-%d 的整数", platform.MinRuntimeAgentSessions, platform.MaxRuntimeAgentSessions))
}
policy, err := s.RuntimePolicy()
if err != nil {
return nil, err
}
policy.Task.AgentMaxSessions = maxSessions
if _, err := s.UpdateRuntimePolicySetting(actor, policy); err != nil {
return nil, err
}
if err := s.applyAgentSessionLimit(ctx, maxSessions); err != nil {
return nil, err
}
agents := s.collectAgentServices(ctx)
if len(agents) == 0 {
return nil, fmt.Errorf("Agent 服务状态不可用")
}
return &agents[0], nil
}
func (s *Service) applyAgentSessionLimit(ctx context.Context, maxSessions int) error {
agentruntime.SetProcessLimit(maxSessions)
endpoint := strings.TrimSpace(os.Getenv("YINGCE_AGENT_URL"))
if endpoint == "" {
return nil
}
return yingceagent.SetLimit(ctx, endpoint, os.Getenv("YINGCE_AGENT_TOKEN"), maxSessions)
}
func (s *Service) collectAgentServices(ctx context.Context) []SystemPerformanceAgent {
configured := platform.DefaultRuntimeAgentSessions
if policy, err := s.RuntimePolicy(); err == nil && policy.Task.AgentMaxSessions > 0 {
configured = policy.Task.AgentMaxSessions
}
endpoint := strings.TrimSpace(os.Getenv("YINGCE_AGENT_URL"))
if endpoint == "" {
active, limit := agentruntime.ProcessUsage()
return []SystemPerformanceAgent{{
ID: "embedded", Name: "画布 Agent", Mode: "embedded", Endpoint: "本进程",
Healthy: true, Active: active, Limit: limit, ConfiguredLimit: configured,
StatusMessage: "本地开发由后端进程直接启动,不单独占用容器。",
}}
}
service := SystemPerformanceAgent{
ID: "yingce-agent", Name: "画布 Agent", Mode: "remote", Endpoint: endpoint,
ConfiguredLimit: configured,
}
status, err := yingceagent.Inspect(ctx, endpoint)
if err != nil {
service.StatusMessage = "独立 Agent 服务不可达。正在进行的对话会失败,不会自动改回本进程执行。"
return []SystemPerformanceAgent{service}
}
service.Healthy = status.OK
service.Active = status.Active
service.Limit = status.Limit
service.Queued = status.Queued
if status.Limit != configured {
service.StatusMessage = "运行中的上限与已保存配置不一致,重新保存一次即可应用。"
}
return []SystemPerformanceAgent{service}
}
func agentServicesDegraded(agents []SystemPerformanceAgent) bool {
for _, agent := range agents {
if !agent.Healthy {
return true
}
}
return false
}
func collectSystemMemory() SystemPerformanceMemory {
var stats runtime.MemStats
runtime.ReadMemStats(&stats)

View File

@@ -161,6 +161,7 @@ func TestRuntimePolicyBackfillsAgentStepLimitsForLegacyJSON(t *testing.T) {
task := value["task"].(map[string]any)
delete(task, "agentStepMaxOutputTokens")
delete(task, "agentStepTimeoutSeconds")
delete(task, "agentMaxSessions")
trimmed, err := json.Marshal(value)
if err != nil {
t.Fatal(err)
@@ -176,6 +177,9 @@ func TestRuntimePolicyBackfillsAgentStepLimitsForLegacyJSON(t *testing.T) {
if effective.Task.AgentStepMaxOutputTokens != platform.DefaultRuntimeAgentStepOutputTokens {
t.Fatalf("legacy json 未回填单步输出上限: %d", effective.Task.AgentStepMaxOutputTokens)
}
if effective.Task.AgentMaxSessions != platform.DefaultRuntimeAgentSessions {
t.Fatalf("legacy json 未回填同时对话上限: %d", effective.Task.AgentMaxSessions)
}
if effective.Task.AgentStepTimeoutSeconds != platform.DefaultRuntimeAgentStepTimeout {
t.Fatalf("legacy json 未回填单步超时: %d", effective.Task.AgentStepTimeoutSeconds)
}

View File

@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"log/slog"
"strings"
"sync"
"time"
@@ -166,6 +167,7 @@ func (s *Service) StartWorker() {
s.startResourceDeletionWorker(ctx)
s.startSkillSyncWorker(ctx)
s.startPaymentWorker(ctx)
go s.syncAgentSessionLimit()
s.runWorkerLoop(func(ctx context.Context) {
ticker := time.NewTicker(3 * time.Second)
defer ticker.Stop()
@@ -180,6 +182,19 @@ func (s *Service) StartWorker() {
})
}
func (s *Service) syncAgentSessionLimit() {
policy, err := s.RuntimePolicy()
if err != nil {
slog.Warn("agent session limit load failed", "error", err)
return
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
if err := s.applyAgentSessionLimit(ctx, policy.Task.AgentMaxSessions); err != nil {
slog.Warn("agent session limit apply failed", "error", err)
}
}
func (s *Service) BeginDrain() { s.backgroundWorkers().BeginDrain() }
func (s *Service) StopWorker(ctx context.Context) error { return s.backgroundWorkers().Stop(ctx) }