From e3b6cca8bcd9a3aa97610fb80bb6178bb5e355b3 Mon Sep 17 00:00:00 2001 From: weichen Date: Tue, 13 Oct 2026 06:04:26 +0000 Subject: [PATCH] Add account name passthrough and EXT-19 account search MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why: P3 人员选择器需要按账号或姓名查找成员并区分同名——EXT-19 目录此前只有 owner+有效态;扩展申请四要素记入 Dev 任务卡(授权管理读面下发姓名,联系方式仍不下发;停用能力经 facts F6 核实为未实现,Valid 维持存在性口径)。 Evidence: go test ./... 全绿;TestInprocHostSearchAccounts 覆盖姓名/账号子串、大小写不敏感、空 query 首页、ListAccounts 姓名回归;facts F5/F6 登记随 Dev PR。 Impact: AccountEntry 加 Name 可选字段;新增可选接口 AccountSearcher 与 inprocHost 实现;既有消费者不受影响(字段可选、接口可选)。 Refs: 提案 r1 P3;Dev 任务卡 feature/rdm-increment-settings-duties --- internal/app/account_lister_test.go | 55 +++++++++++++++++++++++++++++ internal/app/hostcaps.go | 35 ++++++++++++++++-- pkg/extension/extension.go | 14 +++++++- 3 files changed, 101 insertions(+), 3 deletions(-) diff --git a/internal/app/account_lister_test.go b/internal/app/account_lister_test.go index 30821a7..6ee2898 100644 --- a/internal/app/account_lister_test.go +++ b/internal/app/account_lister_test.go @@ -51,3 +51,58 @@ func TestInprocHostListAccounts(t *testing.T) { t.Fatalf("limit 超限应钳制不报错: %v", err) } } + +// TestInprocHostSearchAccounts:EXT-19 扩展 SearchAccounts——账号/姓名子串 +// 检索、大小写不敏感、空 query 首页、limit 钳制(P3,2026-10-13)。 +func TestInprocHostSearchAccounts(t *testing.T) { + dir := t.TempDir() + st, err := store.OpenSQLite(dir + "/test.db") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { st.Close() }) + hash, err := store.HashPassword("init-pass-01") + if err != nil { + t.Fatal(err) + } + if err := st.CreateAccount("alice", "王小明", hash, false); err != nil { + t.Fatal(err) + } + if err := st.CreateAccount("bob", "李大明", hash, false); err != nil { + t.Fatal(err) + } + if err := st.CreateAccount("carol", "", hash, false); err != nil { + t.Fatal(err) + } + h := inprocHost{caps: hostcaps{st: st}} + + // 姓名子串(同名区分场景):两个"大明/小明"都以"明"命中。 + got, err := h.SearchAccounts("明", 50) + if err != nil { + t.Fatal(err) + } + if len(got) != 2 || got[0].Owner != "alice" || got[1].Owner != "bob" { + t.Fatalf("姓名子串命中不符: %+v", got) + } + if got[0].Name != "王小明" { + t.Fatalf("目录行应携带姓名: %+v", got[0]) + } + // 账号子串、大小写不敏感。 + got, _ = h.SearchAccounts("AL", 50) + if len(got) != 1 || got[0].Owner != "alice" { + t.Fatalf("账号子串应命中 alice: %+v", got) + } + // 未命中。 + if got, _ = h.SearchAccounts("不存在", 50); len(got) != 0 { + t.Fatalf("未命中应为空: %+v", got) + } + // 空 query=首页候选(owner 升序);姓名为空的账号 Name 为空串。 + if got, _ = h.SearchAccounts("", 2); len(got) != 2 || got[0].Owner != "alice" || got[1].Name != "李大明" { + t.Fatalf("空 query 首页不符: %+v", got) + } + // ListAccounts 回归:分页行同样携带姓名。 + page, _, _ := h.ListAccounts("", 2) + if page[0].Name != "王小明" { + t.Fatalf("ListAccounts 应携带姓名: %+v", page[0]) + } +} diff --git a/internal/app/hostcaps.go b/internal/app/hostcaps.go index 89c440e..9c75678 100644 --- a/internal/app/hostcaps.go +++ b/internal/app/hostcaps.go @@ -4,6 +4,8 @@ import ( "agent-system-backend/pkg/extension" "context" "fmt" + "sort" + "strings" "sync" "agent-system-backend/internal/ingest" @@ -173,7 +175,8 @@ func (h inprocHost) IsAdmin() bool { // ListAccounts 实现 EXT-19 AccountLister(最小账号目录):仅授权管理读面用。 // 有效态=账号存在且非停用(当前 users 表无停用位——must_change_password 不影响 -// 有效性;后续加停用位在此映射)。游标=owner 升序偏移。 +// 有效性;后续加停用位在此映射)。P3(2026-10-13):携带 Name 供同名区分。 +// 游标=owner 升序偏移。 func (h inprocHost) ListAccounts(cursor string, limit int) ([]extension.AccountEntry, string, error) { if limit <= 0 || limit > 200 { limit = 50 @@ -184,7 +187,7 @@ func (h inprocHost) ListAccounts(cursor string, limit int) ([]extension.AccountE } entries := make([]extension.AccountEntry, 0, len(users)) for _, u := range users { - entries = append(entries, extension.AccountEntry{Owner: u.Owner, Valid: true}) + entries = append(entries, extension.AccountEntry{Owner: u.Owner, Name: u.Name, Valid: true}) } // 排序+游标(owner 升序;cursor 为上一页末 owner,空=首页)。 for i := 0; i < len(entries); i++ { @@ -211,6 +214,34 @@ func (h inprocHost) ListAccounts(cursor string, limit int) ([]extension.AccountE return entries[start:end], next, nil } +// SearchAccounts 实现 EXT-19 扩展 AccountSearcher(P3,2026-10-13):账号或 +// 姓名子串检索(大小写不敏感;空 query = 首页候选)。结果按 owner 升序, +// 有效态口径与 ListAccounts 一致。 +func (h inprocHost) SearchAccounts(query string, limit int) ([]extension.AccountEntry, error) { + if limit <= 0 || limit > 200 { + limit = 50 + } + users, err := h.caps.st.ListUsersAdmin() + if err != nil { + return nil, err + } + q := strings.ToLower(strings.TrimSpace(query)) + entries := make([]extension.AccountEntry, 0, len(users)) + for _, u := range users { + if q != "" && + !strings.Contains(strings.ToLower(u.Owner), q) && + !strings.Contains(strings.ToLower(u.Name), q) { + continue + } + entries = append(entries, extension.AccountEntry{Owner: u.Owner, Name: u.Name, Valid: true}) + } + sort.Slice(entries, func(i, j int) bool { return entries[i].Owner < entries[j].Owner }) + if len(entries) > limit { + entries = entries[:limit] + } + return entries, nil +} + func (h inprocHost) KVPut(key, value string) error { return h.caps.st.PutPluginKV(h.pluginID, h.owner, key, value) } diff --git a/pkg/extension/extension.go b/pkg/extension/extension.go index 93939a0..976880d 100644 --- a/pkg/extension/extension.go +++ b/pkg/extension/extension.go @@ -83,9 +83,13 @@ type Registry interface { } // AccountEntry 最小账号目录行(U3/KTD2):owner+有效态——授权管理所需的最小 -// 身份;姓名/邮箱/联系方式一律不下发(隐私边界)。 +// 身份。2026-10-13 P3 扩展(提案 rdm-settings-members-duties-perms-increment): +// Name 向授权管理读面下发(同名区分与按姓名查找是成员管理必需;扩展申请四要素 +// 见 Dev 任务卡 feature/rdm-increment-settings-duties)。邮箱/联系方式仍一律 +// 不下发(隐私边界不变)。 type AccountEntry struct { Owner string `json:"owner"` + Name string `json:"name,omitempty"` Valid bool `json:"valid"` } @@ -96,6 +100,14 @@ type AccountLister interface { ListAccounts(cursor string, limit int) (entries []AccountEntry, next string, err error) } +// AccountSearcher EXT-19 扩展可选能力(P3,2026-10-13):按账号或姓名检索账号 +// 目录,供人员选择器的输入搜索。query 匹配账号子串或姓名子串(大小写不敏感; +// 空串=不限,返回首页候选)。仅授权管理读面使用;未实现时下游回落 +// AccountLister 分页浏览。 +type AccountSearcher interface { + SearchAccounts(query string, limit int) (entries []AccountEntry, err error) +} + // AdminZoneSpec 管理区分区声明(WP3-I3):ID 是分区身份(建议形如 // .
,前端按 ID 映射内置分区组件);Title/Icon/Desc 为展示 // 元数据(icon 取基座前端 IconName 词表,未知值前端回退通用图标);Order