Huly 导入工具 Unified Format 实战解析以 Recipes 示例工作区的巧克力熔岩蛋糕卡片为例【免费下载链接】platformHuly — All-in-One Project Management Platform (alternative to Linear, Jira, Slack, Notion, Motion)项目地址: https://gitcode.com/GitHub_Trending/platform80/platformHuly 是面向项目与团队协作的全栈式工作区平台其配套的 Huly Import Tool 提供了一套以 YAML Markdown 为核心的 Unified Format 统一导入格式用于将任意系统的数据迁移进 Huly 工作区。本文以示例工作区中的 Chocolate Lava Cake.md 卡片文档为解剖样本完整讲解卡片 frontmatter 元数据、MasterTag/Tag 属性建模、N:N 关联关系、附件blobs挂载以及子文档结构并结合导入器源码揭示每条配置背后的底层处理逻辑。读完本文你将能够依据 Unified Format 亲手编写并校验可被 HulyFormatImporter 正确解析的卡片文档与工作区目录。一、Unified Format 与示例工作区一张卡片所处的完整上下文Unified Format 的核心理念是用人类可读的目录结构表达工作区数据根目录存放各空间的 YAML 配置每个空间目录下存放 Markdown 文档/事项及其子项子项位于与父文档同名的子目录中。完整的结构说明与导入命令见 导入指南而example-workspace目录则是一个可以直接照抄的成品示例。Chocolate Lava Cake.md位于示例工作区的Recipes/目录下该目录对应的空间配置是 Recipes.yaml文件结构如下example-workspace/ ├── Difficulty.yaml # 枚举定义被 Recipes.yaml 的 enumOf 引用 ├── Recipes.yaml # Recipe 空间配置card:class:MasterTag ├── RecipeAssociations.yaml # 卡片间 N:N 关联定义 ├── Recipes/ │ ├── DietaryType.yaml # 标签card:class:Tag │ ├── Chocolate Lava Cake.md # 本文主角卡片 │ ├── Chocolate Lava Cake/ # 子文档目录同名子文件夹 │ │ └── Chocolate Sauce.md # 子卡片相关菜谱 │ ├── Classic Margherita Pizza.md # 兄弟卡片关联 推荐甜点 │ ├── Vegan/ │ │ ├── Vegan Recipe.yaml # 子标签MasterTag │ │ └── Mushroom Risotto.md │ └── files/ │ └── cake.png # 附件被 frontmatter 的 blobs 引用从 导入指南 的规则可知所有空间配置必须是 YAML所有文档/事项文件必须包含 YAML frontmatter 后接 Markdown 正文缺少必需的class属性的文件会被跳过。Chocolate Lava Cake.md恰好是这一规则的完整样例——它由 frontmatter 元数据 正文 Markdown 两部分组成。二、卡片文档解剖frontmatter 元数据逐字段讲解Chocolate Lava Cake.md的完整 frontmatter 如下完整文件见 原文档--- title: Chocolate Lava Cake cookingTime: 25 minutes servings: 4 difficulty: Medium category: Dessert calories: 450 chef: Anna Smith blobs: - ./files/cake.png recommendedMainDishes: - ./Classic Margherita Pizza.md - ./Vegan/Mushroom Risotto.md ---2.1 定位字段title 与标识规则title是必需的定位字段。在导入器中卡片的_id由导入工具按文件路径统一生成源码中this.metadataRegistry.getRef(cardPath)见 cards.ts因此文件名即身份标识——同一空间内不要出现重名文件。与此对应导入指南规定 Tracker 事项的 ID 由项目identifier 文件名中的序号组成如项目ALPHA下的1.Setup Project.md导入后为ALPHA-1卡片类card 域则直接以文件路径作为引用锚点这点在后面的关联与refTo中至关重要。2.2 自定义属性字段来自 MasterTag 的属性建模cookingTime、servings、difficulty、category、calories、chef都不是 Huly 内置字段而是由 Recipes.yaml 定义的 MasterTag 属性class: card:class:MasterTag title: Recipe properties: - label: cookingTime type: TypeString - label: servings type: TypeNumber - label: difficulty enumOf: ./Difficulty.yaml # isArray: true # for multiple values - label: category type: TypeString - label: calories type: TypeNumber - label: chef type: TypeString - label: relatedRecipes refTo: ./Recipes.yaml isArray: true对照导入器源码 convertPropertyType属性类型映射规则如下属性写法底层类型说明type: TypeStringcore.class.TypeString字符串如cookingTime、category、cheftype: TypeNumbercore.class.TypeNumber数字如servings: 4、calories: 450type: TypeBooleancore.class.TypeBoolean布尔值见下文 Vegan Recipe.yamlenumOf: ./xxx.yamlcore.class.EnumOf引用另一个 YAML 定义枚举取值见difficultyrefTo: ./xxx.yamlcore.class.RefTo引用另一张卡片类型可配合isArray: trueisArray: truecore.class.ArrOf将上述类型包装为数组由 validateFormat 可知卡片 frontmatter 中出现但未在空间 MasterTag 属性中声明的键会被判定为非法抛出校验错误反之 MasterTag 声明的属性若卡片未提供则是可选的导入器不强制要求每个属性都有值除非属性定义了defaultValue。这正是示例中不同菜谱可以各自携带不同属性子集例如素食菜谱多出proteinSource、isGlutenFree仍能共处同一空间的原因。2.3 枚举字段difficulty 的取值约束difficulty: Medium的值并非任意字符串而是受 Difficulty.yaml 约束的枚举。该文件内容定义了枚举取值Easy/Medium/Hard 等具体以仓库内文件为准并通过enumOf: ./Difficulty.yaml挂到difficulty属性上。导入器在 convertPropertyType 中会先将enumOf指向的 YAML 解析注册进元数据注册表再以core.class.EnumOf类型创建属性——这意味着如果卡片里填了枚举之外的值导入时会被拒绝。编写自定义空间时建议把这类受控词表独立成 YAML 文件与菜谱示例保持同一模式。2.4 附件字段blobs 与 files/ 目录frontmatter 中blobs: - ./files/cake.png声明了卡片附件。导入器在 createCardWithRelations 中按相对路径解析附件文件将其构建为BlobType包含file、type、name、size并挂载到卡片的blobs属性上若路径对应的文件不存在会直接抛出Blob file not found错误cards.ts。两点实操要点附件必须放在空间目录内的files/子目录示例中为Recipes/files/cake.png并在卡片 frontmatter 中用相对路径引用也可以在 Markdown 正文中引用附件导入指南明确说明空间目录中的文件在 Markdown 内容中被引用时可用作附件。blobs支持字符串或字符串数组两种写法导入器通过Array.isArray(rawBlobs) ? rawBlobs : [rawBlobs]统一处理cards.ts即单附件可省略数组写法。三、卡片间的关联关系recommendedMainDishes 与 N:N AssociationChocolate Lava Cake.md的 frontmatter 里最有趣的是recommendedMainDishes: - ./Classic Margherita Pizza.md - ./Vegan/Mushroom Risotto.md这个属性既没有出现在 Recipes.yaml 的properties中也不是内置字段——它来自 RecipeAssociations.yaml 定义的卡片间关联class: core:class:Association typeA: ./Recipes.yaml typeB: ./Recipes.yaml nameA: recommendedDesserts nameB: recommendedMainDishes type: N:N # 1:1, 1:N, N:N3.1 Association 的语义该文件声明了两张Recipes类型卡片之间存在一个N:N多对多关联正向方向名为nameA: recommendedDesserts推荐甜点反向方向名为nameB: recommendedMainDishes推荐主菜。因此Chocolate Lava Cake.md里填recommendedMainDishes: [披萨, 意式烩饭]表达这道甜点推荐搭配这两道主菜兄弟卡片 Classic Margherita Pizza.md 与 Mushroom Risotto.md 中各自填写了对称方向的recommendedDesserts: - ./Chocolate Lava Cake.md二者在数据上互为印证。3.2 导入器如何把 YAML 关联变成双向关系导入器在处理class: core:class:Association的 YAML 文件时调用 createAssociation校验typeA、typeB引用的类型文件必须已注册在元数据注册表中为typeA挂上名为nameB的反向关联同时为typeB挂上名为nameA的正向关联最终把关联建模为core.class.Association文档。随后处理卡片时createCardWithRelations 会遍历 frontmatter 中所有未匹配到属性的键若该键命中了 MasterTag 或 Tag 的关联元数据masterTagAssociaions.has(key) || tagAssociations.has(key)则按关联方向建立关系若命中了属性则写入属性值。换言之卡片 frontmatter 中的关联字段名必须与 Association 中声明的nameA/nameB完全一致否则导入器会抛出Association not found: keycards.ts。由此可以总结关联建模的完整套路在空间级先写一个xxxAssociations.yamlclass: core:class:Association在卡片 frontmatter 里按声明的方向名填入对端卡片的相对路径列表导入器会自动建立双向引用。四、从一张卡片看标签、子卡片与继承4.1 标签Tag与继承属性示例中另有两类 YAML 用于扩展属性面DietaryType.yamlclass: card:class:Tag定义restrictionsTypeString与allergensTypeString。Classic Margherita Pizza.md与 Chocolate Sauce.md 通过 frontmatter 的tags: - ./DietaryType.yaml挂上该标签从而获得restrictions: Vegetarian、allergens: Gluten, Dairy等额外字段。Vegan Recipe.yamlclass: card:class:MasterTag定义proteinSourceTypeString、isGlutenFreeTypeBoolean、allergensTypeString由Mushroom Risotto.md使用。标签Tag与主标签MasterTag的区别在 createCardSchema 的处理中体现为属性/关联的来源不同MasterTag 的属性来自空间级 YAMLTags 的属性则来自卡片 frontmatter 中tags字段引用的 YAML。一张卡片可以同时享受 MasterTag 属性、多个 Tag 属性以及 Association 关联的叠加——这正是Chocolate Lava Cake.md虽然字段不多却完整演示了三种元数据来源的原因。4.2 子卡片同名子目录Chocolate Lava Cake.md存在一个同名子目录Chocolate Lava Cake/其中放有 Chocolate Sauce.md。按照导入指南的规则子文档/子事项位于与父文档同名的目录中。该子卡片本身也是一个完整的卡片文档frontmatter 声明了class: card:class:MasterTag空间属性、tags引用、relatedRecipes: - ../Chocolate Lava Cake.md使用../相对路径反向引用父卡片注意其refTo类型来自 Recipes.yaml 的relatedRecipes属性正文则是巧克力酱的制作步骤。这正是文档可以拥有子文档层级结构的实体示范——对应导入指南中子项位于同名子文件夹的通用规则也适用于 Tracker 的父任务/子任务与 QMS 的文档模板/受控文档。五、从卡片到工作区组装空间并执行导入5.1 卡片文档的必备骨架综合示例与导入指南一张可被 Unified Format 识别的卡片文档必须满足文件为.md首行是---包裹的 YAML frontmatter其后为 Markdown 正文如## Ingredients、## Instructions、## Notes这类普通 Markdown 即可Huly 会原样保留排版frontmatter 必须包含title位于 space 目录下的卡片通过该目录对应的 MasterTag YAML 提供class语义Chocolate Lava Cake.md本身未写class其类型由所属的Recipes.yaml空间决定这与导入指南中缺class的文件会被跳过的规则并不冲突——该规则针对的是独立空间配置文件涉及的自定义字段要么在空间 MasterTag 的properties中声明要么来自挂载 Tag 的属性要么是 Association 声明的方向名附件先放入空间files/目录再在blobs或 Markdown 正文中引用。5.2 执行导入按 导入指南 提供的命令将 workspace 目录挂载进容器执行docker run \ -e FRONT_URLhttps://huly.app \ -v /path/to/workspace:/data \ hardcoreeng/import-tool:latest \ -- bundle.js import /data \ --user your.emailcompany.com \ --password yourpassword \ --workspace workspace-id其中FRONT_URL对应 Huly 前端地址导入工具在 index.ts 中强制要求该环境变量缺失时直接报错退出。认证流程见 authorize工具会先拉取FRONT_URL/config.json获取ACCOUNTS_URL再用账号密码登录换取 token 后建立客户端连接随后由HulyFormatImporter逐目录解析空间配置、卡片、标签与关联并写入 Huly。5.3 导入限制务必提前知晓所有被引用的用户如chef、assignee、所有者必须在导入前已存在于系统中分配人按全名匹配受控文档QMS 场景只能以Draft状态导入且受控文档必须与其模板位于同一空间文档编号方括号内需全局唯一附件文件只有在 Markdown 中被引用或声明于blobs时才会被导入。六、小结一份菜谱卡片背后的建模方法论Chocolate Lava Cake.md表面是一份 25 分钟即可完成的甜点食谱实质上是 Unified Format 一张五脏俱全的示范卡片title定义身份MasterTag 属性定义可扩展的字段面enumOf约束受控取值blobs挂载图片附件Association 建立与其他卡片的 N:N 双向关联同名子目录承载子卡片。这套YAML 定义类型 Markdown 承载内容 相对路径建立关系的建模方式向上可扩展为 Tracker 项目/事项见 Project Alpha.yaml 与 导入指南 中的项目配置示例向下可复用于任意业务对象。若需要从 Notion、ClickUp 等平台直接迁移可参考 Notion 导入指南 与 ClickUp 导入指南但正如 README 所建议复杂场景与平台无关数据一律推荐先转成 Unified Format 再导入以享受可读、可校验、可先行修正的迁移流程。动手实践时直接以example-workspace/Recipes为蓝本复制改造是上手最快的方式。【免费下载链接】platformHuly — All-in-One Project Management Platform (alternative to Linear, Jira, Slack, Notion, Motion)项目地址: https://gitcode.com/GitHub_Trending/platform80/platform创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
