{"record":{"id":"c8516b070ebbce4b","repo":"siyuan-note/siyuan","slug":"ai-editor-action-must-not-be-empty","errorCode":null,"errorMessage":"AI editor action must not be empty","messagePattern":"AI editor action must not be empty","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"kernel/model/ai_editor.go","lineNumber":72,"sourceCode":"\nfunc GetAIEditorActions() (ret []*AIEditorAction, err error) {\n\twaitForSyncingStorages()\n\taiEditorActionsLock.Lock()\n\tdefer aiEditorActionsLock.Unlock()\n\n\tdata, err := loadAIEditorActions()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err = migrateLegacyAIEditorActions(data, !util.ReadOnly); err != nil {\n\t\treturn nil, err\n\t}\n\treturn data.Actions, nil\n}\n\nfunc SaveAIEditorAction(action *AIEditorAction) (ret *AIEditorAction, err error) {\n\tif action == nil || (action.Name == \"\" && action.Action == \"\") {\n\t\treturn nil, errors.New(\"AI editor action must not be empty\")\n\t}\n\tif action.ID != \"\" && !ast.IsNodeIDPattern(action.ID) {\n\t\treturn nil, errors.New(\"invalid AI editor action ID\")\n\t}\n\n\twaitForSyncingStorages()\n\taiEditorActionsLock.Lock()\n\tdefer aiEditorActionsLock.Unlock()\n\n\tdata, err := loadAIEditorActions()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err = migrateLegacyAIEditorActions(data, true); err != nil {\n\t\treturn nil, err\n\t}\n\n\tret = &AIEditorAction{","sourceCodeStart":54,"sourceCodeEnd":90,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/model/ai_editor.go#L54-L90","documentation":"SaveAIEditorAction persists an AI editor action (custom prompt action for the editor). It rejects a nil action or one where both Name and Action are empty, since such an entry would be meaningless and unusable in the UI. This is a pure input-validation guard before any storage work.","triggerScenarios":"Calling SaveAIEditorAction (saveAIEditorActionContract API) with a null/empty body, or a JSON payload like {\"id\":\"...\"} with no name and no action text; an empty-string Name and Action after client-side trimming.","commonSituations":"Plugin or script calling the API with a partially built action object; frontend form submitted with all fields blank; JSON unmarshalling producing a zero-value struct.","solutions":["Provide a non-empty `name` and/or `action` prompt text in the request payload","Validate the payload client-side before POSTing (check at least one of name/action is non-empty)","If updating an existing action, load it first via the list API and modify fields rather than sending a bare {id}"],"exampleFix":"// before\nfetchPost(\"/api/ai/editor/action/save\", { id: existing.id })\n// after\nif (!existing.name && !existing.action) throw new Error(\"name or action is required\")\nfetchPost(\"/api/ai/editor/action/save\", { id: existing.id, name: existing.name, action: existing.action })","handlingStrategy":"validation","validationCode":"if (!action || (!action.name && !action.action)) {\n  throw new Error(\"AI editor action requires a non-empty name or action\");\n}","typeGuard":"const isValidAction = (a) => !!a && (typeof a.name === \"string\" && a.name.trim() !== \"\" || typeof a.action === \"string\" && a.action.trim() !== \"\");","tryCatchPattern":"try {\n  await fetchPost(\"/api/ai/editor/action/save\", action);\n} catch (e) {\n  if (e.message.includes(\"must not be empty\")) showMsg(\"Name or action prompt is required\");\n}","preventionTips":["Require at least the action prompt field in UI forms","Trim inputs before submitting","Never send a bare {id} payload for updates"],"tags":["validation","api","ai"],"backgroundTag":"empty-required-field","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}