From e2ada7e4949d3873e93e15fa0b63202020115d41 Mon Sep 17 00:00:00 2001 From: Autumn-27 <53844290+Autumn-27@users.noreply.github.com> Date: Thu, 17 Sep 2026 11:31:27 -0400 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E6=92=AD=E6=8A=A5=E6=9D=BF?= =?UTF-8?q?=E6=90=9C=E7=B4=A2=E6=94=AF=E6=8C=81=E6=8C=89=E8=8A=82=E7=82=B9?= =?UTF-8?q?=20id=20=E7=AD=9B=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 搜索框在内容/来源之外,新增节点 id 匹配:纯数字或 UI 展示的「#41」形式 都会精确命中该节点。服务端 NodesPage 在 payload/origin ILIKE 的 OR 组里 追加 id 精确匹配;mock 同步;补 NodesPage id 搜索测试。 --- db/exploration.go | 11 ++++- db/exploration_test.go | 48 +++++++++++++++++++ .../tasks/detail/_tabs/broadcast-tab.tsx | 2 +- web/src/lib/mock/handler.ts | 7 ++- 4 files changed, 65 insertions(+), 3 deletions(-) diff --git a/db/exploration.go b/db/exploration.go index c7f4228..9865c26 100644 --- a/db/exploration.go +++ b/db/exploration.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "sort" + "strconv" "strings" "time" ) @@ -828,7 +829,15 @@ func (s *ExplorationStore) NodesPage(f NodeFilter, page, size int) ([]*Node, int if q := strings.TrimSpace(f.Query); q != "" { args = append(args, "%"+q+"%") mark := "$" + fmt.Sprint(len(args)) - conds = append(conds, "(payload::text ILIKE "+mark+" OR COALESCE(origin,'') ILIKE "+mark+")") + ors := []string{"payload::text ILIKE " + mark, "COALESCE(origin,'') ILIKE " + mark} + // 纯数字(或 UI 里带 # 前缀的形式,如「#41」)当作节点 id 精确匹配,方便直接定位某个节点。 + if idStr := strings.TrimPrefix(q, "#"); idStr != "" { + if id, err := strconv.ParseInt(idStr, 10, 64); err == nil { + args = append(args, id) + ors = append(ors, "id = $"+fmt.Sprint(len(args))) + } + } + conds = append(conds, "("+strings.Join(ors, " OR ")+")") } where := " WHERE " + strings.Join(conds, " AND ") diff --git a/db/exploration_test.go b/db/exploration_test.go index 97d9f62..6e21a78 100644 --- a/db/exploration_test.go +++ b/db/exploration_test.go @@ -246,3 +246,51 @@ func TestIntentPauseResumeAndCancelCleanup(t *testing.T) { assertCount(`SELECT COUNT(*) FROM exploration_anchors WHERE node_id=$1`, 0, directFact) assertCount(`SELECT COUNT(*) FROM assets WHERE id=$1`, 1, assetID) } + +// TestNodesPageQueryMatchesID verifies the 播报板 search filters on node id (both +// the bare number and the「#id」form the UI shows) in addition to payload/origin. +func TestNodesPageQueryMatchesID(t *testing.T) { + d, err := Open(testDSN(t)) + if err != nil { + t.Skipf("postgres unavailable (%v) — skipping", err) + } + defer d.Close() + + expID, err := d.CreateExploration("test", "id 搜索") + if err != nil { + t.Fatal(err) + } + defer d.Exec(`DELETE FROM explorations WHERE id=$1`, expID) + es := d.Exploration(expID) + + target, err := es.AddNode("fact", map[string]any{"summary": "needle-alpha"}, 0, "confirmed", "worker-a", nil) + if err != nil { + t.Fatal(err) + } + other, err := es.AddNode("fact", map[string]any{"summary": "unrelated-beta"}, 0, "confirmed", "worker-b", nil) + if err != nil { + t.Fatal(err) + } + + onlyTarget := func(label, q string) { + t.Helper() + nodes, total, err := es.NodesPage(NodeFilter{Query: q}, 1, 50) + if err != nil { + t.Fatalf("%s: %v", label, err) + } + if total != 1 || len(nodes) != 1 || nodes[0].ID != target { + t.Fatalf("%s: q=%q total=%d nodes=%+v, want single node %d", label, q, total, nodes, target) + } + } + + onlyTarget("bare id", fmt.Sprint(target)) + onlyTarget("hash id", "#"+fmt.Sprint(target)) + onlyTarget("payload still works", "needle-alpha") + + // A non-matching numeric id returns nothing (and does not accidentally match other). + if nodes, total, err := es.NodesPage(NodeFilter{Query: fmt.Sprint(target + other + 1000)}, 1, 50); err != nil { + t.Fatal(err) + } else if total != 0 || len(nodes) != 0 { + t.Fatalf("non-existent id: total=%d nodes=%+v, want empty", total, nodes) + } +} diff --git a/web/src/app/(main)/function/tasks/detail/_tabs/broadcast-tab.tsx b/web/src/app/(main)/function/tasks/detail/_tabs/broadcast-tab.tsx index 8b9fd12..f9449da 100644 --- a/web/src/app/(main)/function/tasks/detail/_tabs/broadcast-tab.tsx +++ b/web/src/app/(main)/function/tasks/detail/_tabs/broadcast-tab.tsx @@ -567,7 +567,7 @@ export function BroadcastTab({ taskId }: { taskId: string }) { setQueryInput(e.target.value)} - placeholder="搜索播报内容 / 来源" + placeholder="搜索内容 / 来源 / 节点 id" className="h-8 pl-8" aria-label="搜索播报" /> diff --git a/web/src/lib/mock/handler.ts b/web/src/lib/mock/handler.ts index affac15..e8b8490 100644 --- a/web/src/lib/mock/handler.ts +++ b/web/src/lib/mock/handler.ts @@ -2156,7 +2156,12 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re const rank = (id: string) => all.findIndex((n) => n.id === id); const matched = all .filter((n) => (kinds.size === 0 || kinds.has(n.type)) && (states.size === 0 || states.has(n.state))) - .filter((n) => !needle || `${n.payload ?? ""} ${n.origin}`.toLowerCase().includes(needle)) + .filter( + (n) => + !needle || + // 内容 / 来源 / 节点 id 任一命中即可(id 兼容「#41」写法)。 + `${n.payload ?? ""} ${n.origin} ${n.id}`.toLowerCase().includes(needle.replace(/^#/, "")), + ) .sort((a, b) => { const d = Date.parse(a.ts) - Date.parse(b.ts) || rank(a.id) - rank(b.id); return asc ? d : -d;