Merge pull request #150 from Autumn-27/feat/broadcast-search-node-id
feat(web): 播报板搜索支持按节点 id 筛选
This commit is contained in:
4 files changed
+65
-3
No files matched your search
+10
-1
@@ -7,6 +7,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -872,7 +873,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 ")
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -573,7 +573,7 @@ export function BroadcastTab({ taskId }: { taskId: string }) {
|
||||
<Input
|
||||
value={queryInput}
|
||||
onChange={(e) => setQueryInput(e.target.value)}
|
||||
placeholder="搜索播报内容 / 来源"
|
||||
placeholder="搜索内容 / 来源 / 节点 id"
|
||||
className="h-8 pl-8"
|
||||
aria-label="搜索播报"
|
||||
/>
|
||||
|
||||
@@ -2168,7 +2168,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;
|
||||
|
||||
Reference in new issue
Block a user