{"record":{"id":"7fae83b9c306f202","repo":"siyuan-note/siyuan","slug":"config-actioneffects-contains-an-empty-action","errorCode":null,"errorMessage":"config.actionEffects contains an empty action","messagePattern":"config\\.actionEffects contains an empty action","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/plugin/api_agent.go","lineNumber":293,"sourceCode":"\treturn\n}\n\nfunc jsCapabilityEffectsToGoEffects(rt *goja.Runtime, value goja.Value) (*tools.ToolEffects, error) {\n\teffects := &tools.ToolEffects{}\n\tif err := unmarshalCapabilityJSON(rt, value, effects, \"effects\"); err != nil {\n\t\treturn nil, err\n\t}\n\treturn effects, nil\n}\n\nfunc jsCapabilityActionEffectsToGoEffects(rt *goja.Runtime, value goja.Value) (map[string]tools.ToolEffects, error) {\n\tactionEffects := map[string]tools.ToolEffects{}\n\tif err := unmarshalCapabilityJSON(rt, value, &actionEffects, \"actionEffects\"); err != nil {\n\t\treturn nil, err\n\t}\n\tfor action := range actionEffects {\n\t\tif strings.TrimSpace(action) == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"config.actionEffects contains an empty action\")\n\t\t}\n\t}\n\treturn actionEffects, nil\n}\n\nfunc unmarshalCapabilityJSON(rt *goja.Runtime, value goja.Value, target any, field string) error {\n\tjsonValue, err := value.ToObject(rt).MarshalJSON()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to serialize config.%s: %v\", field, err)\n\t}\n\tif err = json.Unmarshal(jsonValue, target); err != nil {\n\t\treturn fmt.Errorf(\"invalid config.%s: %v\", field, err)\n\t}\n\treturn nil\n}\n","sourceCodeStart":275,"sourceCodeEnd":309,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/plugin/api_agent.go#L275-L309","documentation":"jsCapabilityActionEffectsToGoEffects converts the config.actionEffects map (action name -> effects) from JS. It rejects the configuration when any key (action name) is empty or whitespace-only. Effect keys are used to match tool invocations, so an empty key would be meaningless and likely a configuration mistake.","triggerScenarios":"A plugin defines config.actionEffects with an empty-string key, e.g. actionEffects: { \"\": {readOnlyHint:true} }, or a key of only whitespace like \"   \", produced by template literals or dynamic key construction from empty variables.","commonSituations":"Building keys programmatically (actionEffects[actionName] = ...) where actionName is empty; copy-paste leaving a blank key; destructuring mistakes creating a \"\" key.","solutions":["Inspect config.actionEffects keys and remove or fix the empty/whitespace key","Use the exact action name matching the tool's action identifier","Guard dynamic assignment: if (!actionName) throw/log before setting actionEffects[actionName]","Use Object.keys(config.actionEffects).some(k => !k.trim()) to self-check before registering"],"exampleFix":"// before\nconst actionEffects = {}; actionEffects[getName()] = { readOnlyHint: true }; // getName() returned \"\"\n\n// after\nconst name = getName();\nif (!name || !name.trim()) throw new Error(\"action name must not be empty\");\nconst actionEffects = { [name]: { readOnlyHint: true } };","handlingStrategy":"validation","validationCode":"function hasEmptyActionKey(actionEffects) {\n  return Object.keys(actionEffects || {}).some((k) => !k || !k.trim());\n}\nif (hasEmptyActionKey(config.actionEffects)) throw new Error(\"actionEffects has an empty action key\");","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Check dynamic action names for emptiness before assigning keys","Avoid building keys from possibly-empty template literals","Lint with Object.keys() assertions in plugin tests"],"tags":["plugin","validation","config"],"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"}