diff --git a/im/backend/internal/app/admin_ai_agents.go b/im/backend/internal/app/admin_ai_agents.go index 1290579..1ace85e 100644 --- a/im/backend/internal/app/admin_ai_agents.go +++ b/im/backend/internal/app/admin_ai_agents.go @@ -33,8 +33,14 @@ func (a *App) adminAIAgents(w http.ResponseWriter, r *http.Request) { fail(w, http.StatusBadRequest, 20001, err.Error()) return } - if strings.TrimSpace(r.URL.Query().Get("managed")) == "1" { + // "0" is not the absence of a filter here: an account is unmanaged both when + // it has no binding at all and when its binding is switched off, and the + // operator looking for accounts to turn on wants to see both. + switch strings.TrimSpace(r.URL.Query().Get("managed")) { + case "1": where += " AND g.user_id IS NOT NULL AND g.enabled=1" + case "0": + where += " AND (g.user_id IS NULL OR g.enabled=0)" } // Same keyword shape as the user list: nickname or 星遇号. if keyword := strings.TrimSpace(r.URL.Query().Get("keyword")); keyword != "" { diff --git a/im/backend/internal/app/ai_enqueue_integration_test.go b/im/backend/internal/app/ai_enqueue_integration_test.go index ede95a8..ffcfb02 100644 --- a/im/backend/internal/app/ai_enqueue_integration_test.go +++ b/im/backend/internal/app/ai_enqueue_integration_test.go @@ -246,3 +246,63 @@ func TestAIReplyRetriesAfterReasoningExhaustsBudgetMySQL(t *testing.T) { t.Fatalf("calls = %d, want 2", calls) } } + +// The managed filter has three states, and "0" is a real one: an account is +// unmanaged both when it was never bound and when its binding is switched off. +func TestAdminAIAgentsManagedFilterMySQL(t *testing.T) { + db := isolatedIMDatabase(t) + a := &App{db: db, hub: NewHub(), config: Config{Environment: "development", JWTSecret: "isolated-im-regression-secret-only"}} + for _, statement := range []string{ + `UPDATE users SET is_test=1,test_batch='regression' WHERE id IN (1,2,3)`, + `INSERT INTO ai_agents(user_id,model_id,enabled) VALUES(1,0,1),(2,0,0)`, + } { + if _, err := db.Exec(statement); err != nil { + t.Fatal(err) + } + } + list := func(query string) []int64 { + t.Helper() + var payload struct { + Items []struct { + UserID int64 `json:"userId"` + } `json:"items"` + Total int `json:"total"` + } + if err := json.Unmarshal(imTestCall(t, a.adminAIAgents, 1, "GET", "/admin/v1/ai/agents"+query, "", 200), &payload); err != nil { + t.Fatal(err) + } + ids := []int64{} + for _, item := range payload.Items { + ids = append(ids, item.UserID) + } + if payload.Total != len(ids) { + t.Fatalf("total=%d 与条目数 %d 不一致", payload.Total, len(ids)) + } + return ids + } + + if got := list("?managed=1"); len(got) != 1 || got[0] != 1 { + t.Fatalf("仅已托管 = %v, want [1]", got) + } + // Account 2 is bound but switched off, account 3 was never bound. + got := list("?managed=0") + if len(got) != 2 { + t.Fatalf("仅未托管 = %v, want 两个账号", got) + } + for _, want := range []int64{2, 3} { + found := false + for _, id := range got { + found = found || id == want + } + if !found { + t.Fatalf("仅未托管 = %v, 缺少 %d", got, want) + } + } + if all := list(""); len(all) != 3 { + t.Fatalf("全部账号 = %v, want 3 个", all) + } + // The two halves together are the whole list, with nothing counted twice. + if len(list("?managed=1"))+len(list("?managed=0")) != len(list("")) { + t.Fatal("两个筛选相加必须等于全部") + } +}