返回 DeepSeek-Reasonix
opencode_go_rewrite.go
根目录 / internal / config / opencode_go_rewrite.go
1 package config
2
3 import (
4 "fmt"
5 "reflect"
6 "slices"
7 "sort"
8 "strconv"
9 "strings"
10
11 "github.com/BurntSushi/toml"
12 )
13
14 // rawTOMLSet edits only the owning assignment. Unrelated text, including unknown
15 // keys and comments, is retained. Tables and quoted/dotted keys are supported.
16 func rawTOMLSet(body string, path []string, value any) (string, error) {
17 lines := strings.Split(body, "\n")
18 var section []string
19 insert := 0
20 parentSeen := len(path) == 1
21 for i := 0; i < len(lines); i++ {
22 line := strings.TrimSpace(lines[i])
23 if header := tomlSectionHeader(line); header != "" {
24 section = rawTOMLKeyPath(strings.Trim(header, "[]"))
25 if reflect.DeepEqual(section, path[:len(path)-1]) {
26 insert, parentSeen = i+1, true
27 }
28 continue
29 }
30 if line == "" || strings.HasPrefix(line, "#") {
31 continue
32 }
33 eq, err := findTOMLAssignmentEquals(lines[i], 0, len(lines[i]))
34 if err != nil || eq < 0 {
35 continue
36 }
37 key := rawTOMLKeyPath(strings.TrimSpace(lines[i][:eq]))
38 full := append(append([]string{}, section...), key...)
39 if len(section) == 0 && len(path) > 1 && path[0] == "providers" {
40 full = append([]string{"providers"}, full...)
41 }
42 // Consume an entire multiline value, so embedded fake headers/keys
43 // cannot be mistaken for configuration syntax.
44 end := i
45 var decoded map[string]any
46 for ; end < len(lines); end++ {
47 if _, e := toml.Decode("v = "+strings.Join(append([]string{lines[i][eq+1:]}, lines[i+1:end+1]...), "\n"), &decoded); e == nil {
48 break
49 }
50 }
51 if end == len(lines) {
52 return body, fmt.Errorf("cannot locate TOML value for %s", strings.Join(full, "."))
53 }
54 if len(full) <= len(path) && reflect.DeepEqual(full, path[:len(full)]) {
55 v := value
56 if len(full) < len(path) {
57 m, ok := decoded["v"].(map[string]any)
58 if !ok {
59 i = end
60 continue
61 }
62 root := m
63 for _, k := range path[len(full) : len(path)-1] {
64 nested, ok := m[k].(map[string]any)
65 if !ok {
66 nested = map[string]any{}
67 m[k] = nested
68 }
69 m = nested
70 }
71 m[path[len(path)-1]] = value
72 v = root
73 }
74 encoded, err := rawTOMLValue(v)
75 if err != nil {
76 return body, err
77 }
78 replacement := replaceTOMLScalarAssignment(lines[i], encoded)
79 // Retain comments inside an edited multiline array as adjacent
80 // comments; the rest of the document remains byte-for-byte intact.
81 var comments []string
82 for n := i + 1; n <= end; n++ {
83 if at := tomlInlineCommentIndex(lines[n]); at >= 0 {
84 comments = append(comments, lines[n][at:])
85 }
86 }
87 out := append([]string{}, lines[:i]...)
88 out = append(out, comments...)
89 out = append(out, replacement)
90 out = append(out, lines[end+1:]...)
91 return strings.Join(out, "\n"), nil
92 }
93 i = end
94 }
95 encoded, err := rawTOMLValue(value)
96 if err != nil {
97 return body, err
98 }
99 assignment := strconv.Quote(path[len(path)-1]) + " = " + encoded
100 if parentSeen {
101 lines = append(lines[:insert], append([]string{assignment}, lines[insert:]...)...)
102 return strings.Join(lines, "\n"), nil
103 }
104 keys := make([]string, len(path)-1)
105 for i, k := range path[:len(path)-1] {
106 keys[i] = strconv.Quote(k)
107 }
108 return strings.TrimRight(body, "\n") + "\n[" + strings.Join(keys, ".") + "]\n" + assignment + "\n", nil
109 }
110
111 func rawTOMLKeyPath(s string) []string {
112 // Let the validated TOML parser handle escapes and quoted dots.
113 var m map[string]any
114 if _, err := toml.Decode(s+" = 0", &m); err != nil {
115 return nil
116 }
117 var out []string
118 for len(m) == 1 {
119 for k, v := range m {
120 out = append(out, k)
121 m, _ = v.(map[string]any)
122 }
123 }
124 return out
125 }
126
127 func rawTOMLValue(v any) (string, error) {
128 if v == nil {
129 return "", fmt.Errorf("cannot encode absent TOML value")
130 }
131 rv := reflect.ValueOf(v)
132 if rv.Kind() == reflect.Pointer {
133 return rawTOMLValue(rv.Elem().Interface())
134 }
135 switch rv.Kind() {
136 case reflect.String:
137 return strconv.Quote(rv.String()), nil
138 case reflect.Bool:
139 return strconv.FormatBool(rv.Bool()), nil
140 case reflect.Int, reflect.Int64, reflect.Int32:
141 return strconv.FormatInt(rv.Int(), 10), nil
142 case reflect.Float64, reflect.Float32:
143 return strconv.FormatFloat(rv.Float(), 'g', -1, 64), nil
144 case reflect.Slice, reflect.Array:
145 var parts []string
146 for i := range rv.Len() {
147 s, err := rawTOMLValue(rv.Index(i).Interface())
148 if err != nil {
149 return "", err
150 }
151 parts = append(parts, s)
152 }
153 return "[" + strings.Join(parts, ", ") + "]", nil
154 case reflect.Map:
155 var keys []string
156 for _, k := range rv.MapKeys() {
157 keys = append(keys, k.String())
158 }
159 sort.Strings(keys)
160 var parts []string
161 for _, k := range keys {
162 s, err := rawTOMLValue(rv.MapIndex(reflect.ValueOf(k)).Interface())
163 if err != nil {
164 return "", err
165 }
166 parts = append(parts, strconv.Quote(k)+" = "+s)
167 }
168 return "{ " + strings.Join(parts, ", ") + " }", nil
169 }
170 return "", fmt.Errorf("unsupported TOML edit value %T", v)
171 }
172
173 func rewriteOpenCodeGoConfig(body string, before, after *Config, additions []openCodeGoGroup) (string, error) {
174 if reflect.DeepEqual(before.Providers, after.Providers) && reflect.DeepEqual(before.Agent, after.Agent) && reflect.DeepEqual(before.Bot, after.Bot) && reflect.DeepEqual(before.Desktop, after.Desktop) && before.DefaultModel == after.DefaultModel && len(additions) == 0 {
175 return rawTOMLSet(body, []string{"config_version"}, openCodeGoUpgradeVersion)
176 }
177 lines := strings.Split(body, "\n")
178 blocks := providerTOMLBlocks(lines)
179 if len(blocks) != len(before.Providers) {
180 expanded, err := expandOpenCodeGoInlineProviders(body)
181 if err != nil {
182 return body, err
183 }
184 return rewriteOpenCodeGoConfig(expanded, before, after, additions)
185 }
186 // Extend each provider span through its nested tables only.
187 for i := range blocks {
188 for blocks[i].end < len(lines) {
189 h := tomlSectionHeader(lines[blocks[i].end])
190 p := rawTOMLKeyPath(strings.Trim(h, "[]"))
191 if h != "" && (len(p) < 2 || p[0] != "providers") {
192 break
193 }
194 blocks[i].end++
195 }
196 }
197 var originals []string
198 for _, b := range blocks {
199 originals = append(originals, strings.Join(lines[b.start+1:b.end], "\n"))
200 }
201 // after.Providers holds each split group right behind its source, so the
202 // original index i maps to i plus the groups split from earlier sources.
203 afterIndex := make([]int, len(blocks))
204 for i := range blocks {
205 if i > 0 {
206 afterIndex[i] = afterIndex[i-1] + 1
207 for _, addition := range additions {
208 if addition.source == i-1 {
209 afterIndex[i]++
210 }
211 }
212 }
213 }
214 for i := range slices.Backward(blocks) {
215 next, err := patchOpenCodeGoProvider(originals[i], before.Providers[i], after.Providers[afterIndex[i]])
216 if err != nil {
217 return body, err
218 }
219 var siblings []string
220 for _, addition := range additions {
221 if addition.source != i {
222 continue
223 }
224 raw, err := patchOpenCodeGoProvider(originals[i], before.Providers[i], addition.entry)
225 if err != nil {
226 return body, err
227 }
228 siblings = append(siblings, "", "[[providers]]")
229 siblings = append(siblings, strings.Split(strings.TrimRight(raw, "\n"), "\n")...)
230 }
231 b := blocks[i]
232 replacement := strings.Split(next, "\n")
233 if len(siblings) > 0 {
234 replacement = append(append(trimTrailingBlankLines(replacement), siblings...), "")
235 }
236 lines = append(lines[:b.start+1], append(replacement, lines[b.end:]...)...)
237 }
238 body = strings.Join(lines, "\n")
239 return rewriteOpenCodeGoReferences(body, before, after)
240 }
241
242 func trimTrailingBlankLines(lines []string) []string {
243 for len(lines) > 0 && strings.TrimSpace(lines[len(lines)-1]) == "" {
244 lines = lines[:len(lines)-1]
245 }
246 return lines
247 }
248
249 // verifyOpenCodeGoRewrite proves the lexical edit still describes the planned
250 // configuration before any byte reaches disk. Every rewrite path ends here.
251 func verifyOpenCodeGoRewrite(body string, after *Config) error {
252 var check Config
253 if _, err := toml.Decode(body, &check); err != nil {
254 return fmt.Errorf("migration readback: %w", err)
255 }
256 if check.ConfigVersion != openCodeGoUpgradeVersion {
257 return fmt.Errorf("migration readback: config_version %d", check.ConfigVersion)
258 }
259 if check.DefaultModel != after.DefaultModel {
260 return fmt.Errorf("migration readback: default_model %q", check.DefaultModel)
261 }
262 if len(check.Providers) != len(after.Providers) {
263 return fmt.Errorf("provider count changed during lexical rewrite")
264 }
265 for i, p := range check.Providers {
266 want := after.Providers[i]
267 if p.Name != want.Name || p.Kind != want.Kind || p.BaseURL != want.BaseURL || !reflect.DeepEqual(p.ModelList(), want.ModelList()) {
268 return fmt.Errorf("provider %q failed migration readback", want.Name)
269 }
270 }
271 return nil
272 }
273
274 // Expand the uncommon inline-array form lexically. Only structural separators
275 // change; string contents, unknown fields and comments remain in the document.
276 func expandOpenCodeGoInlineProviders(body string) (string, error) {
277 start, end, err := providerTOMLInlineArrayRange(body)
278 if err != nil {
279 return body, err
280 }
281 blocks, err := providerTOMLInlineBlocks(body)
282 if err != nil {
283 return body, fmt.Errorf("cannot safely map inline providers: %w", err)
284 }
285 if len(blocks) == 0 {
286 return body, fmt.Errorf("cannot safely map inline providers: no provider blocks")
287 }
288 assignment := strings.LastIndex(body[:start], "\n") + 1
289 var tables strings.Builder
290 outside := body[start+1 : end]
291 for _, b := range slices.Backward(blocks) {
292 a, z := b.start-start-1, b.end-start
293 outside = outside[:a] + outside[z:]
294 }
295 var comments []string
296 for line := range strings.SplitSeq(outside, "\n") {
297 if at := tomlInlineCommentIndex(line); at >= 0 {
298 comments = append(comments, line[at:])
299 }
300 }
301 for _, b := range blocks {
302 chunk := []byte(body[b.start+1 : b.end])
303 depth := 0
304 if err := scanTOMLOutsideStrings(string(chunk), 0, len(chunk), func(pos int, ch byte) bool {
305 switch ch {
306 case '[', '{':
307 depth++
308 case ']', '}':
309 depth--
310 case ',':
311 if depth == 0 {
312 chunk[pos] = '\n'
313 }
314 }
315 return true
316 }); err != nil {
317 return body, err
318 }
319 tables.WriteString("\n[[providers]]\n" + string(chunk) + "\n")
320 }
321 return body[:assignment] + strings.Join(comments, "\n") + body[end+1:] + tables.String(), nil
322 }
323
324 func patchOpenCodeGoProvider(raw string, old, next ProviderEntry) (string, error) {
325 ov, nv := reflect.ValueOf(old), reflect.ValueOf(next)
326 for _, field := range []string{"Name", "Kind", "BaseURL", "RequestURL", "ChatURL", "Models", "Model", "Default", "PresetID", "PresetVersion", "ResponsesMode", "Effort"} {
327 a, b := ov.FieldByName(field).Interface(), nv.FieldByName(field).Interface()
328 if reflect.DeepEqual(a, b) {
329 continue
330 }
331 f, _ := ov.Type().FieldByName(field)
332 var err error
333 raw, err = rawTOMLSet(raw, []string{strings.Split(f.Tag.Get("toml"), ",")[0]}, b)
334 if err != nil {
335 return raw, err
336 }
337 }
338 for model, o := range next.ModelOverrides {
339 previous := old.ModelOverrides[model]
340 for _, pair := range []struct {
341 key string
342 a, b any
343 }{{"reasoning_protocol", previous.ReasoningProtocol, o.ReasoningProtocol}, {"supported_efforts", previous.SupportedEfforts, o.SupportedEfforts}, {"default_effort", previous.DefaultEffort, o.DefaultEffort}} {
344 if reflect.DeepEqual(pair.a, pair.b) {
345 continue
346 }
347 var err error
348 raw, err = rawTOMLSet(raw, []string{"providers", "model_overrides", model, pair.key}, pair.b)
349 if err != nil {
350 return raw, err
351 }
352 }
353 }
354 return raw, nil
355 }
356
357 func rewriteOpenCodeGoReferences(body string, before, after *Config) (string, error) {
358 // Update only reference fields whose resolved migration changed them.
359 var err error
360 for _, pair := range []struct {
361 path []string
362 a, b any
363 }{
364 {[]string{"config_version"}, before.ConfigVersion, openCodeGoUpgradeVersion},
365 {[]string{"default_model"}, before.DefaultModel, after.DefaultModel},
366 {[]string{"desktop", "provider_access"}, before.Desktop.ProviderAccess, after.Desktop.ProviderAccess},
367 {[]string{"agent", "planner_model"}, before.Agent.PlannerModel, after.Agent.PlannerModel},
368 {[]string{"agent", "vision_model"}, before.Agent.VisionModel, after.Agent.VisionModel},
369 {[]string{"agent", "guardian_model"}, before.Agent.GuardianModel, after.Agent.GuardianModel},
370 {[]string{"agent", "recovery_model"}, before.Agent.RecoveryModel, after.Agent.RecoveryModel},
371 {[]string{"agent", "subagent_model"}, before.Agent.SubagentModel, after.Agent.SubagentModel},
372 {[]string{"agent", "web_search_model"}, before.Agent.WebSearchModel, after.Agent.WebSearchModel},
373 {[]string{"bot", "model"}, before.Bot.Model, after.Bot.Model},
374 } {
375 if reflect.DeepEqual(pair.a, pair.b) {
376 continue
377 }
378 body, err = rawTOMLSet(body, pair.path, pair.b)
379 if err != nil {
380 return body, err
381 }
382 }
383 for name, ref := range after.Agent.SubagentModels {
384 if before.Agent.SubagentModels[name] != ref {
385 body, err = rawTOMLSet(body, []string{"agent", "subagent_models", name}, ref)
386 if err != nil {
387 return body, err
388 }
389 }
390 }
391 // Bot array tables need positional edits, never a global string replace.
392 botIndex, inBotConnection := -1, false
393 lines := strings.Split(body, "\n")
394 for i, line := range lines {
395 if tomlSectionHeader(line) == "bot.connections" && strings.HasPrefix(strings.TrimSpace(line), "[[") {
396 botIndex++
397 inBotConnection = true
398 continue
399 }
400 if inBotConnection && botIndex >= 0 && botIndex < len(after.Bot.Connections) && isTOMLKeyAssignment(line, "model") && before.Bot.Connections[botIndex].Model != after.Bot.Connections[botIndex].Model {
401 lines[i] = replaceTOMLStringAssignment(line, after.Bot.Connections[botIndex].Model)
402 }
403 if h := tomlSectionHeader(line); h != "" {
404 inBotConnection = false
405 }
406 }
407 body = strings.Join(lines, "\n")
408 return body, nil
409 }
410
410 lines GO