{"record":{"id":"cae76dfdae294da5","repo":"siyuan-note/siyuan","slug":"unmarshal-legacy-ai-editor-actions-failed-w","errorCode":null,"errorMessage":"unmarshal legacy AI editor actions failed: %w","messagePattern":"unmarshal legacy AI editor actions failed: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/ai_editor.go","lineNumber":215,"sourceCode":"\t\treturn fmt.Errorf(\"marshal AI editor actions failed: %w\", err)\n\t}\n\tif err = filelock.WriteFile(aiEditorActionsPath(), bytes); err != nil {\n\t\treturn fmt.Errorf(\"write AI editor actions failed: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc migrateLegacyAIEditorActions(data *aiEditorActionsData, persist bool) (err error) {\n\tlocalStorage := GetLocalStorage()\n\tlegacyRaw, ok := localStorage[legacyAIEditorActionsStorageKey]\n\tif !ok {\n\t\treturn nil\n\t}\n\n\tvar legacyActions []*legacyAIEditorAction\n\tif legacyJSON, isString := legacyRaw.(string); isString {\n\t\tif err = gulu.JSON.UnmarshalJSON([]byte(legacyJSON), &legacyActions); err != nil {\n\t\t\treturn fmt.Errorf(\"unmarshal legacy AI editor actions failed: %w\", err)\n\t\t}\n\t} else {\n\t\tlegacyJSON, marshalErr := gulu.JSON.MarshalJSON(legacyRaw)\n\t\tif marshalErr != nil {\n\t\t\treturn fmt.Errorf(\"marshal legacy AI editor actions failed: %w\", marshalErr)\n\t\t}\n\t\tif err = gulu.JSON.UnmarshalJSON(legacyJSON, &legacyActions); err != nil {\n\t\t\treturn fmt.Errorf(\"unmarshal legacy AI editor actions failed: %w\", err)\n\t\t}\n\t}\n\n\ttype actionKey struct {\n\t\tname   string\n\t\taction string\n\t}\n\texisting := make(map[actionKey]struct{}, len(data.Actions))\n\tfor _, action := range data.Actions {\n\t\texisting[actionKey{name: action.Name, action: action.Action}] = struct{}{}","sourceCodeStart":197,"sourceCodeEnd":233,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/afa823b6b4e4f183511e0bc0a3be93caa94c7c97/kernel/model/ai_editor.go#L197-L233","documentation":"During legacy migration, the localStorage key \"local-ai\" exists and holds a STRING, but that string is not valid JSON for the expected shape []*legacyAIEditorAction{Name,Memo}. gulu.JSON.UnmarshalJSON returned a syntax or type error, so migration aborts and every Get/Save/Remove AI editor action call fails until the value is fixed.","triggerScenarios":"A pre-migration frontend stored custom AI actions as a string under local-ai in data/storage/local.json, and that string is truncated, hand-edited, corrupted by sync merge conflicts, or contains a different JSON shape (e.g. an object instead of an array of {name,memo}).","commonSituations":"Manual editing of storage/local.json, sync-tool merge conflicts producing half-written JSON, older SiYuan versions writing an incompatible local-ai schema, or crash during a previous localStorage write.","solutions":["Inspect data/storage/local.json and validate the value of key \"local-ai\" with any JSON parser","If it holds a string, decode/escape it and confirm it parses as [{\"name\":...,\"memo\":...}]","Repair the JSON in place, or delete the local-ai key (old custom AI actions are discarded; the migration then no-ops)","Back up local.json before editing, with SiYuan closed"],"exampleFix":"// before: corrupted value in data/storage/local.json\n\"local-ai\": \"[{name: 'translate', memo: 'Translate'}]\"  // single quotes, unquoted keys\n// after: strict JSON, memo field\n\"local-ai\": \"[{\\\"name\\\":\\\"translate\\\",\\\"memo\\\":\\\"Translate the current block\\\"}]\"","handlingStrategy":"validation","validationCode":"// Validate the legacy value before any AI editor action call triggers migration\nls := model.GetLocalStorage()\nif raw, ok := ls[\"local-ai\"]; ok {\n    if s, isStr := raw.(string); isStr {\n        var probe []*struct{ Name, Memo string }\n        if json.Unmarshal([]byte(s), &probe) != nil {\n            // repair or drop the key before proceeding\n        }\n    }\n}","typeGuard":"func isMigratableLegacyActions(raw any) bool {\n    s, ok := raw.(string)\n    if !ok { return false }\n    var probe []struct {\n        Name string `json:\"name\"`\n        Memo string `json:\"memo\"`\n    }\n    return json.Unmarshal([]byte(s), &probe) == nil\n}","tryCatchPattern":"if _, err := model.GetAIEditorActions(); err != nil && strings.Contains(err.Error(), \"legacy AI editor actions\") {\n    // stop calling in a loop; fix data/storage/local.json \"local-ai\" first, then retry once\n}","preventionTips":["Never hand-edit storage/local.json while the kernel is running","Validate local-ai JSON after restoring workspace from backup/sync","Back up local.json before SiYuan upgrades that include AI editor migration"],"tags":["json","migration","local-storage","data-corruption","go"],"backgroundTag":"json-unmarshal-type-mismatch","analyzedSha":"afa823b6b4e4f183511e0bc0a3be93caa94c7c97","analyzedAt":"2026-08-18T17:04:10.865Z","schemaVersion":2},"datasetVersion":"2026-08-31T22:30:34.772Z"}