{"record":{"id":"2a4cee63545cd0a9","repo":"siyuan-note/siyuan","slug":"invalid-skill-path-component","errorCode":null,"errorMessage":"invalid skill path component","messagePattern":"invalid skill path component","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/util/skill_manage.go","lineNumber":64,"sourceCode":"\tEditable bool\n}\n\ntype SkillFileData struct {\n\tEntries        []SkillFileEntry\n\tContent        *string\n\tRevision       string\n\tReadOnlyReason string\n}\n\n// 管理操作使用真实的相对路径，不使用技能正文中的名称作为文件标识。\nfunc validateManagedSkillPath(p string) error {\n\tif p == \"\" || !fs.ValidPath(p) || strings.ContainsAny(p, \"\\\\:\") {\n\t\treturn errors.New(\"invalid skill path\")\n\t}\n\tfor _, part := range strings.Split(p, \"/\") {\n\t\tif strings.TrimSpace(part) != part || strings.HasSuffix(part, \".\") ||\n\t\t\tstrings.ContainsAny(part, \"<>\\\"|?*~\") || strings.ContainsFunc(part, unicode.IsControl) {\n\t\t\treturn errors.New(\"invalid skill path component\")\n\t\t}\n\t\tdevice := strings.ToUpper(strings.TrimRight(strings.SplitN(part, \".\", 2)[0], \" .\"))\n\t\tif device == \"CON\" || device == \"PRN\" || device == \"AUX\" || device == \"NUL\" || device == \"CONIN$\" || device == \"CONOUT$\" {\n\t\t\treturn errors.New(\"reserved skill file name\")\n\t\t}\n\t\tif strings.HasPrefix(device, \"COM\") || strings.HasPrefix(device, \"LPT\") {\n\t\t\tnumber := strings.TrimPrefix(strings.TrimPrefix(device, \"COM\"), \"LPT\")\n\t\t\tif len([]rune(number)) == 1 && strings.ContainsAny(number, \"0123456789¹²³\") {\n\t\t\t\treturn errors.New(\"reserved skill file name\")\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n// 从工作空间目录逐层打开根目录，拒绝把管理操作重定向到链接目标。\nfunc openManagedSkillsRoot() (*os.Root, error) {\n\tworkspace, err := os.OpenRoot(DataDir)","sourceCodeStart":46,"sourceCodeEnd":82,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/util/skill_manage.go#L46-L82","documentation":"Each path component in a managed skill path must be filesystem-safe across platforms: no leading/trailing whitespace, no trailing dot, no characters in <>\"|?*~, and no Unicode control runes. A component violating these per-segment rules produces 'invalid skill path component'.","triggerScenarios":"Path segments containing spaces at edges, a trailing dot ('file.'), wildcards ('*'), quotes, '~', '?', '<', '>', '|', or control characters anywhere in a segment passed to ManageSkillFiles.","commonSituations":"Filenames copied from search results with trailing spaces; names containing globs or template markers; files renamed on one OS with characters reserved on another.","solutions":["Rename the target to remove the offending characters (trailing dots/spaces, <>\"|?*~)","Trim whitespace from each path segment before sending","Replace control characters (e.g. from copy-paste) with safe characters"],"exampleFix":"// before\npath := \"my skill /notes?.md\"\n\n// after\npath := \"my-skill/notes.md\"\n","handlingStrategy":"validation","validationCode":"function segmentOk(part) {\n  return part === part.trim() && !part.endsWith('.') &&\n    !/[<>\"|?*~]/.test(part) && ![...part].some(c => c.charCodeAt(0) < 32 || c.charCodeAt(0) === 127);\n}\nconst ok = path.split('/').every(segmentOk);","typeGuard":null,"tryCatchPattern":"try {\n  await api.manageSkillFiles({action: 'move', path, target});\n} catch (e) {\n  if (e.message === 'invalid skill path component') {\n    // sanitize the offending segment and retry\n  }\n}","preventionTips":["Sanitize filenames (trim, strip reserved chars) before any file API call","Avoid copying filenames verbatim from clipboard/search output","Test names for cross-platform safety"],"tags":["path-validation","filename","cross-platform","go"],"backgroundTag":"invalid-argument-format","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"}