fix(doc): distinguish identical pagination pages

This commit is contained in:
Dennis
2026-08-17 14:48:00 +08:00
parent a62332be93
commit 2db73a8185
4 changed files with 125 additions and 10 deletions
+46 -5
View File
@@ -755,7 +755,7 @@ func readAllDocBlocksForVerification(ctx context.Context, nodeID string) ([]any,
const pageSize = 50
const maxItems = 5000
all := make([]any, 0, pageSize)
seen := map[string]bool{}
seenPageIdentities := map[string]bool{}
for start := 0; start < maxItems; start += pageSize {
text, err := callMCPToolReturnTextOnServer(ctx, "doc", "list_document_blocks", map[string]any{
"nodeId": nodeID, "format": "jsonml", "startIndex": start, "endIndex": start + pageSize - 1,
@@ -772,12 +772,13 @@ func readAllDocBlocksForVerification(ctx context.Context, nodeID string) ([]any,
if !ok {
return nil, fmt.Errorf("list_document_blocks 回读缺少 blocks 数组")
}
encoded, _ := json.Marshal(blocks)
key := string(encoded)
if key != "[]" && seen[key] {
pageIdentity := docBlockPageIdentity(blocks)
if pageIdentity != "" && seenPageIdentities[pageIdentity] {
return nil, fmt.Errorf("list_document_blocks 分页停滞")
}
seen[key] = true
if pageIdentity != "" {
seenPageIdentities[pageIdentity] = true
}
all = append(all, blocks...)
hasMore, hasMoreKnown := payload["hasMore"].(bool)
if hasMoreKnown && !hasMore {
@@ -796,6 +797,46 @@ func readAllDocBlocksForVerification(ctx context.Context, nodeID string) ([]any,
return nil, fmt.Errorf("文档块超过安全回读上限")
}
func docBlockPageIdentity(blocks []any) string {
if len(blocks) == 0 {
return ""
}
ids := make([]string, 0, len(blocks))
for _, value := range blocks {
id := ""
switch block := value.(type) {
case map[string]any:
id = directDocBlockIdentity(block)
if id == "" {
if element, ok := block["element"].(map[string]any); ok {
id = directDocBlockIdentity(element)
}
}
case []any:
if len(block) > 1 {
if attributes, ok := block[1].(map[string]any); ok {
id = directDocBlockIdentity(attributes)
}
}
}
if id == "" {
return ""
}
ids = append(ids, id)
}
encoded, _ := json.Marshal(ids)
return string(encoded)
}
func directDocBlockIdentity(block map[string]any) string {
for _, key := range []string{"blockId", "id", "uuid", "elementId"} {
if text, ok := block[key].(string); ok && strings.TrimSpace(text) != "" {
return strings.TrimSpace(text)
}
}
return ""
}
func nestedDocMap(data map[string]any) map[string]any {
for _, key := range []string{"result", "data"} {
if nested, ok := data[key].(map[string]any); ok {
@@ -3,6 +3,7 @@ package helpers
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
@@ -318,6 +319,27 @@ func TestCrossPlatformCoverageDocMediaReadbackDefensiveEdges(t *testing.T) {
}
})
t.Run("identical adjacent pages advance by requested indexes", func(t *testing.T) {
blocks := make([]any, 50)
for index := range blocks {
blocks[index] = map[string]any{"blockType": "paragraph"}
}
first, err := json.Marshal(map[string]any{"blocks": blocks, "hasMore": true, "totalCount": 100})
if err != nil {
t.Fatal(err)
}
second, err := json.Marshal(map[string]any{"blocks": blocks, "hasMore": false, "totalCount": 100})
if err != nil {
t.Fatal(err)
}
caller := &scriptedToolCaller{steps: []scriptedToolStep{{text: string(first)}, {text: string(second)}}}
installScriptedCaller(t, caller)
got, err := readAllDocBlocksForVerification(ctx, "node")
if err != nil || len(got) != 100 || caller.calls != 2 {
t.Fatalf("blocks=%d calls=%d err=%v, want 100 blocks from two pages", len(got), caller.calls, err)
}
})
t.Run("unknown pagination short page", func(t *testing.T) {
installScriptedCaller(t, &scriptedToolCaller{steps: []scriptedToolStep{{text: `{"blocks":[{"id":"a"}]}`}}})
if blocks, err := readAllDocBlocksForVerification(ctx, "node"); err != nil || len(blocks) != 1 {
+33 -5
View File
@@ -808,7 +808,7 @@ func readDocVerification(rt *shortcut.RuntimeContext, tool string, rawParams map
func readAllDocumentBlocks(rt *shortcut.RuntimeContext, base map[string]any) (map[string]any, error) {
all := make([]any, 0, docBlockReadPageSize)
seenPages := map[string]bool{}
seenPageIdentities := map[string]bool{}
for start := 0; start < docBlockReadMaxItems; start += docBlockReadPageSize {
params := cloneMap(base)
params["startIndex"] = start
@@ -821,12 +821,13 @@ func readAllDocumentBlocks(rt *shortcut.RuntimeContext, base map[string]any) (ma
if !ok {
return nil, fmt.Errorf("list_document_blocks 回读缺少 blocks 数组")
}
encoded, _ := json.Marshal(blocks)
pageKey := string(encoded)
if pageKey != "[]" && seenPages[pageKey] {
pageIdentity := documentBlockPageIdentity(blocks)
if pageIdentity != "" && seenPageIdentities[pageIdentity] {
return nil, fmt.Errorf("list_document_blocks 分页停滞,无法证明回读完整")
}
seenPages[pageKey] = true
if pageIdentity != "" {
seenPageIdentities[pageIdentity] = true
}
all = append(all, blocks...)
hasMore, known, _ := docPageState(page)
if known && !hasMore {
@@ -845,6 +846,33 @@ func readAllDocumentBlocks(rt *shortcut.RuntimeContext, base map[string]any) (ma
return nil, fmt.Errorf("list_document_blocks 超过 %d 个块,无法在安全上限内完成回读", docBlockReadMaxItems)
}
func documentBlockPageIdentity(blocks []any) string {
if len(blocks) == 0 {
return ""
}
ids := make([]string, 0, len(blocks))
for _, value := range blocks {
id := ""
switch block := value.(type) {
case map[string]any:
id = blockIdentity(block, "")
if id == "" {
if element, ok := block["element"].(map[string]any); ok {
id = blockIdentity(element, "")
}
}
case []any:
id = jsonMLBlockIdentity(block)
}
if id == "" {
return ""
}
ids = append(ids, id)
}
encoded, _ := json.Marshal(ids)
return string(encoded)
}
func documentBlockEntries(value any) ([]any, bool) {
switch typed := value.(type) {
case map[string]any:
@@ -211,6 +211,30 @@ func TestCrossPlatformCoverageDocReadbackDefensiveEdges(t *testing.T) {
})
}
t.Run("identical adjacent pages advance by requested indexes", func(t *testing.T) {
testseam.Swap(t, &docVerifyDelays, []time.Duration{})
blocks := make([]any, docBlockReadPageSize)
for index := range blocks {
blocks[index] = map[string]any{"blockType": "paragraph"}
}
caller := &docCoverageCaller{responses: map[string][]map[string]any{"list_document_blocks": {
{"blocks": blocks, "hasMore": true, "totalCount": 2 * docBlockReadPageSize},
{"blocks": blocks, "hasMore": false, "totalCount": 2 * docBlockReadPageSize},
}}}
if err := runDocCoverage(t, Update, caller, "--node", "n", "--command", "block_delete", "--block-id", "target", "--yes"); err != nil {
t.Fatal(err)
}
starts := []int{}
for _, call := range caller.history {
if call.tool == "list_document_blocks" {
starts = append(starts, call.params["startIndex"].(int))
}
}
if fmt.Sprint(starts) != "[0 50]" {
t.Fatalf("pagination starts = %v, want [0 50]", starts)
}
})
t.Run("total count terminates pagination", func(t *testing.T) {
testseam.Swap(t, &docVerifyDelays, []time.Duration{})
caller := &docCoverageCaller{responses: map[string][]map[string]any{"list_document_blocks": {{"blocks": []any{map[string]any{"id": "other"}}, "totalCount": 1}}}}