Add account name passthrough and EXT-19 account search
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
This commit is contained in:
@@ -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])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 是分区身份(建议形如
|
||||
// <plugin>.<section>,前端按 ID 映射内置分区组件);Title/Icon/Desc 为展示
|
||||
// 元数据(icon 取基座前端 IconName 词表,未知值前端回退通用图标);Order
|
||||
|
||||
Reference in New Issue
Block a user