返回 DeepSeek-Reasonix
schema.go
根目录 / internal / browser / schema.go
1 package browser
2
3 import (
4 "bytes"
5 "encoding/json"
6 "fmt"
7 "regexp"
8 "sort"
9 "strings"
10 )
11
12 const operationIDPattern = `^[A-Za-z0-9_-]{1,100}$`
13
14 var operationIDRe = regexp.MustCompile(operationIDPattern)
15
16 // property is one schema entry. Keywords live in a map so json.Marshal emits
17 // them in sorted order, which is exactly provider.CanonicalizeSchema's form.
18 type property struct {
19 name string
20 schema map[string]any
21 }
22
23 func str(name, desc string) property {
24 return property{name, map[string]any{"type": "string", "description": desc}}
25 }
26
27 func boolean(name, desc string) property {
28 return property{name, map[string]any{"type": "boolean", "description": desc}}
29 }
30
31 func integer(name, desc string) property {
32 return property{name, map[string]any{"type": "integer", "description": desc}}
33 }
34
35 func bounded(p property, minimum, maximum int) property {
36 p.schema["minimum"] = minimum
37 p.schema["maximum"] = maximum
38 return p
39 }
40
41 func strList(name, desc string) property {
42 return property{name, map[string]any{"type": "array", "description": desc, "items": map[string]any{"type": "string"}, "minItems": 1}}
43 }
44
45 func enum(name, desc string, values ...string) property {
46 vals := make([]any, len(values))
47 for i, v := range values {
48 vals[i] = v
49 }
50 return property{name, map[string]any{"type": "string", "description": desc, "enum": vals}}
51 }
52
53 func pattern(p property, re string) property {
54 p.schema["pattern"] = re
55 return p
56 }
57
58 // objectSchema marshals a closed object schema whose bytes already equal the
59 // canonical form: map keys sort under json.Marshal and required sorts here.
60 func objectSchema(required []string, props ...property) json.RawMessage {
61 properties := make(map[string]any, len(props))
62 for _, p := range props {
63 properties[p.name] = p.schema
64 }
65 req := append([]string{}, required...)
66 sort.Strings(req)
67 b, err := json.Marshal(map[string]any{
68 "type": "object",
69 "additionalProperties": false,
70 "properties": properties,
71 "required": req,
72 })
73 if err != nil {
74 panic("browser: static schema failed to marshal: " + err.Error())
75 }
76 return b
77 }
78
79 func tabIDProp() property {
80 return str("tabId", "ID of the tab, as returned by browser_tabs or browser_open.")
81 }
82
83 func operationIDProp() property {
84 return pattern(str("operationId", "Unique ID for this attempt: letters, digits, '_' or '-', at most 100 characters. Mint a fresh one for every write call and never reuse it. A reused ID is rejected, and an ID whose outcome came back unknown must not be retried."), operationIDPattern)
85 }
86
87 func documentTokenProp() property {
88 return str("documentToken", "documentToken from the browser_snapshot this action was planned against. A navigation, page replacement, or user take-over invalidates it; when the call is blocked as stale, take a new snapshot instead of guessing.")
89 }
90
91 func refProp(desc string) property { return str("ref", desc) }
92
93 // decode parses args into dst, rejecting fields the schema does not declare
94 // so additionalProperties:false holds at runtime as well as on paper.
95 func decode(args json.RawMessage, dst any) error {
96 if len(bytes.TrimSpace(args)) == 0 {
97 args = json.RawMessage(`{}`)
98 }
99 dec := json.NewDecoder(bytes.NewReader(args))
100 dec.DisallowUnknownFields()
101 if err := dec.Decode(dst); err != nil {
102 return fmt.Errorf("invalid args: %w", err)
103 }
104 return nil
105 }
106
107 func requireTab(id string) error {
108 if strings.TrimSpace(id) == "" {
109 return fmt.Errorf("tabId is required; call browser_tabs to find one")
110 }
111 return nil
112 }
113
114 func requireOperationID(id string) error {
115 if !operationIDRe.MatchString(id) {
116 return fmt.Errorf("operationId must match %s; mint a fresh one per attempt", operationIDPattern)
117 }
118 return nil
119 }
120
121 func requireDocumentToken(token string) error {
122 if strings.TrimSpace(token) == "" {
123 return fmt.Errorf("documentToken is required; take a browser_snapshot and pass its documentToken")
124 }
125 return nil
126 }
127
128 func requireRef(ref string) error {
129 if strings.TrimSpace(ref) == "" {
130 return fmt.Errorf("ref is required; use an element ref from browser_snapshot")
131 }
132 return nil
133 }
134
135 func requireStrings(name string, values []string) error {
136 if len(values) == 0 {
137 return fmt.Errorf("%s must list at least one entry", name)
138 }
139 for _, v := range values {
140 if strings.TrimSpace(v) == "" {
141 return fmt.Errorf("%s must not contain empty entries", name)
142 }
143 }
144 return nil
145 }
146
146 lines GO