{"record":{"id":"4ba4e18055013639","repo":"siyuan-note/siyuan","slug":"invalid-ai-editor-action-id","errorCode":null,"errorMessage":"invalid AI editor action ID","messagePattern":"invalid AI editor action ID","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/ai_editor.go","lineNumber":75,"sourceCode":"\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{\n\t\tID:     action.ID,\n\t\tName:   action.Name,\n\t\tAction: action.Action,","sourceCodeStart":57,"sourceCodeEnd":93,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/kernel/model/ai_editor.go#L57-L93","documentation":"When updating an existing AI editor action, the ID must match the SiYuan node-ID pattern validated by ast.IsNodeIDPattern. A non-empty ID that fails this pattern is rejected before touching storage, protecting stored action records from malformed identifiers.","triggerScenarios":"Calling SaveAIEditorAction with an action whose ID is non-empty but not a valid node ID (e.g. random string, URL-safe slug, or truncated ID).","commonSituations":"Client generating its own IDs instead of letting the backend assign them; corrupted local storage returning malformed IDs; hand-edited action config.","solutions":["Omit the ID field when creating a new action so the backend generates a valid one","Only pass back IDs previously returned by the backend (valid 20-char node IDs)","Fix corrupted stored actions by removing and recreating them"],"exampleFix":"// before\nawait fetchPost('/api/ai/saveAIEditorAction', {id: 'my-action-1', name: 'Summarize', action: '...'})\n// after\nawait fetchPost('/api/ai/saveAIEditorAction', {name: 'Summarize', action: '...'}) // let backend assign ID","handlingStrategy":"validation","validationCode":"const isNodeID = id => typeof id === 'string' && /^[0-9]{20}-[a-z0-9]{7}$/.test(id); if (a.id && !isNodeID(a.id)) throw new Error('invalid id')","typeGuard":"const isValidActionID = (id) => id === '' || (typeof id === 'string' && /^[0-9]{14}-[a-z0-9]{7}$/.test(id))","tryCatchPattern":"try { await saveAction(a) } catch (e) { if (e.message.includes('invalid AI editor action ID')) { delete a.id; await saveAction(a) } }","preventionTips":["Never fabricate IDs client-side; omit id for new actions","Echo back only server-issued IDs","Sanitize imported/stored action configs"],"tags":["validation","identifier","ai-editor"],"backgroundTag":"invalid-identifier-format","analyzedSha":"8641553a1f07374001902d3ce773285db1292b2d","analyzedAt":"2026-09-11T16:08:28.414Z","contentChangedAt":"2026-09-11T16:08:28.414Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}