未命名 6

2025 年 10 月 31 日
0 次浏览
11679 字数

记录

  1. Obsidian 存在内部 api,例如 FileExplorerView 类型等。
  2. 已有排序项目的修改原理,猴子补丁技术 monkey-around 的了解。
  3. 更多关于内部 api d的信息,在 Obsidian Discard 论坛搜索 “FileExplorerView” 得到相关讨论:https://discord.com/channels/686053708261228577/840286264964022302/1377222124238016532
  4. Obsidian Typings 项目,用于扩展一些内部的 api 的 typescript 类型提示:Fevol/obsidian-typings:Obsidian API 中未记录部分的 Typescript 类型 — Fevol/obsidian-typings: Typescript typings for undocumented parts of the Obsidian API
  5. Halo 文档插件的 api 机制分析。由于内容比较多,单条讲述逻辑不清楚,单独开一个标题。

Halo 文档插件的 api 机制

基于浏览器 F12 抓包调试。

每一篇文档有 metadata.namespec.docName 两个 id 标识符。

获取文档

/apis/api.uc.doc.halo.run/v1alpha1/doctrees/{metadata.name} 作为文档树中节点。获取该节点在树上的相关信息,例如父子关系、排序优先级等。(包含 docName,需要某方面的完整信息则通过 docName 和对应 api 继续查询)。

/apis/api.uc.doc.halo.run/v1alpha1/docs/{spec.docName} 作为文档内容实体。获取 doc 相关的属性信息。

/apis/api.uc.doc.halo.run/v1alpha1/docs/{spec.docName}/head-content 获取文档正文信息。

整个流程就是通过 doctrees/{metadata.name} 获取 docName,再通过后两个 api 和 docName 获取完整的属性和内容信息。

创建文档

  • /apis/api.uc.doc.halo.run/v1alpha1/projectversions/{project-version-name}/tree

ProjectVersion: Create doc tree under project version

创建文档,本地生成并发送 metadata.name 和 spec.docName,文档类型。在文档树上创建节点 (关联 meadata.name),并创建空文档节点 (关联 spec.docName),仅添加 title、slug、描述属性,无正文类型和内容。

{
  "apiVersion": "doc.halo.run/v1alpha1",
  "kind": "DocTree",
  "metadata": {
    "name": "9ff5a81d-d06f-4b63-afe8-f806f6875627"
  },
  "spec": {
    "priority": 9,
    "projectVersionName": "project-version-o4a8ocii",
    "slug": "新建文档",
    "title": "xinjian",
    "type": "DOC",  // 文件夹此选项为 TREE
    "description": "新建一个测试文档描述",
    "docName": "d99f609a-92cb-49eb-a526-11c932b24aaa"
  },
  "status": {}
}
  • /apis/api.uc.doc.halo.run/v1alpha1/docs

Doc: Create or update a doc

通过 metadata.name 和 spec.docTreeName 创建文档类型和内容。其中 Post 数据中的 metadata.name 是上异步请求中的 docName,同时 docTreeName 是上一个请求中的 metadata.name

// Post 数据
{
  "doc": {
    "spec": {
      "docTreeName": "9ff5a81d-d06f-4b63-afe8-f806f6875627",
      "description": "",
      "owner": "holwell",
      "headSnapshot": "doc-snapshot-omztzkgb",
      "publish": false,
      "updatedAt": "2025-10-28T04:48:51.621807552Z",
      "updatedBy": "holwell"
    },
    "status": {
      "inProgress": false
    },
    "apiVersion": "doc.halo.run/v1alpha1",
    "kind": "Doc",
    "metadata": {
      "name": "d99f609a-92cb-49eb-a526-11c932b24aaa",
      "version": 0,
      "creationTimestamp": "2025-10-28T04:48:51.622347738Z"
    }
  },
  "content": {
    "raw": "",
    "content": "",
    "rawType": "markdown"
  },
  "published": false
}

综上,创建一个文档包含 “创建 DocTree" 和 ”创建 Doc“ 两个过程;同样地,创建一个文件夹仅包含 ”创建 DocTree"

创建文件夹

同上的第一个 api,请求数据如下:

{
  "apiVersion": "doc.halo.run/v1alpha1",
  "kind": "DocTree",
  "metadata": {
    "name": "86283fa9-5576-4b6d-aeed-9a88ea33c955"
  },
  "spec": {
    "priority": 10,
    "projectVersionName": "project-version-o4a8ocii",
    "slug": "wenjianjai",
    "title": "文件夹",
    "type": "TREE",  // 类型为文档则是 DOC
    "description": "文件夹描述"
  },
  "status": {}
}

修改文档

  • /apis/api.uc.doc.halo.run/v1alpha1/projects/{name}/markdown

Project: Render markdown content under a project

POST 方法:先将 markdown 渲染成 HTML 格式 。

  • /apis/api.uc.doc.halo.run/v1alpha1/docs/{name}/head-content

Doc: Update doc content by doc name.

PUT 方法:再将原始数据和渲染后的数据更新到文档中。

{
  "rawType": "markdown",
  "content": "<p>费水电费</p>\n",
  "raw": "费水电费"
}

metadata 和 spec 的区别——以 metadata.name 和 spec.docName、spec.docTreeName 的关系为例

在文档树 docTree 上,节点与节点间的关系需要维护,例如父子关系、次序关系等,它们在树里会有一个 metadata.name 映射。而每个节点类型可能是文件夹或文档,对应的属性信息和存储信息的节点也不一样,这些就信息就存储在 spec 中。

也就是说,我们会通过 “在 docTree 上通过 metadata 查询节点在树上的信息以及 spec 信息” => “通过 spec 信息查询完整的存储信息” 这样一个查询链条。

换言之,每一个 api 对应的是一个类型的数据管理,其中 metadata.name 表示在这个 api 中的唯一资源符,如果这个资源还附带其他类型的数据,则通过 spec.xxxName 进行记录和对应的 api 查询。

移动文档——调整优先级 priority

移动某篇文档时,实际会对该文档当前位置之后与移动新位置之后的所有文档进行 prioriy 的调整,即使部分文档不受调整排序影响,也发起了相关 patch 请求。

这种复杂的排序更新逻辑,这种批量修改在 Obsidian 中可能比较复杂。

排序

  1. 文章排序:前置数据“发布时间”
  2. 文档排序:前置数据“priority”
  3. 文档文件夹排序:给文件夹添加一个 priority属性,获取该属性进行排序。
  4. 文章分类排序:为了和 3 统一,也可以添加 priority 属性,获取该属性进行排序。

文件夹属性的读取

存储的数据结构由读取的需求决定。所以,在确定存储结构之前,我们分析一下读取需求:

  1. 文件夹排序时,需要用到 priority 属性。
  2. Obsidian 客户端在某文档文件夹下,发布文档时,需要知道这个文档文件夹的唯一 id,也就是 project-version-name 属性。
  3. 基于 2 的文档文件夹,类比文章分类文件夹——Obsidian 客户端发布文章时,如果需要携带分类信息,那么也应该知道分类的唯一 id,也就是 categoryName

文件夹唯一 id

  1. 不能是路径,文件夹被移动后,路径发生变化。
  2. 唯一 id 必须关联文件夹,是文件夹名或在文件夹内,可以跟随文件夹移动并表示文件夹 id 的对象。
  3. 文件夹名具有表意作用,作为 id 需要不重复,不合适。
  4. 在文件夹内可能比较合理。json 文件或 folder.md 前置数据。

以上是基于文件夹被移动后,路径变化无法作为文件夹唯一 id 的假设。那么,如果禁止本地文件夹的移动呢?又或是本地文件夹的移动同步修改路径和文件夹 id 映射关系呢?

  1. 本地文件夹应该被移动吗?不应该,移动文件夹对应Halo 文件树管理关系的变化,而文件树管理关系是 Halo 的职责,本地无权执行这样的操作,应该被禁止。
  2. 禁止本地文件夹移动,决定了无需考虑移动文件夹同步修改映射 Id 的方案。
  3. 远程 Halo 文件分类目录树变化后,如何在本地找到未变化之前的文件夹。需要建立唯一 id 和路径的映射,通过 id 寻找路径。
  4. 那目前就有两个需求。第一,远程 Halo 寻址,通过 id 寻找文件夹路径,存储在各文件夹之外。第二,本地文件夹排序,通过文件夹路径获取其 priority 等相关属性,可储存在文件夹之内;如存储在文件夹之外,则需处理文件夹移动时的路径映射变化。
  5. 最终数据结构可能是两个:第一,id 作为 key,与文件夹路径、priority 属性等对象作为 value 的 map。第二,文件夹路径作为 key 和 id 的 map 隐射关系。因为Obsidian 在内部处理时,并不知道有 id、属性等关系,它只认路径。
  6. 数据更新。同样的,如果 Halo 远程文件分类树结构发生变化,需要同时更新这两个 map。
  7. 这两个 map 始终仅 Halo 端的写入权限,Obsidian 端读取权限。由此决定了,本地端无需进行分类文件夹维护,随时重建更新。

文件夹属性的存储——数据结构设计

第一个 map,分类文件夹、文档文件夹的唯一 id 及其对象属性,例如文件夹路径、priority等属性,也就是 “id => 属性”。

第二个 map,由于各个文件夹每次排序都要用到其属性 priority,而 Obsidian 只能直接获取文件夹路径,如果仅基于第一个 map,则每次都需要遍历判断。由于高频使用 “文件夹路径 => id => 属性”,所以我们决定额外维护一个 map。

同步和冲突

考虑以下几种冲突情况:

  1. 远程有本地也有。
  2. 远程删除本地存在。

考虑以下冲突类型和来源:

  1. 文章分类文件夹 “存在性” 冲突。远程删除或移动了分类结构。
  2. 文章分类文件夹 “名称” 和 “属性” 冲突。远程修改了名称和属性。
  3. 文章 “存在性” 冲突。远程删除了某篇文章而本地仍然存在;同标题即同文件名文章。
  4. 文章 “内容” 和 “属性冲突”
  5. 文档和文档文件夹冲突同上。

为了减少不必要的冲突,本地操作原则如下:

  1. 本地不对分类文件夹进行任何移动、修改的操作。第一,因为本地分类文件夹是远程 Halo 分类组织结构的映射,我们无法在本地执行移动、修改操作后同步到 Halo,所以这是一个单向的 “Halo=>本地” 的映射,它应该是只读的。第二,分类文件夹的属性存储和读取是基于路径的。在本地存储的数据读取是基于 “path=>id=>属性” 的,但移动、修改文件夹的操作无法同步更新本地存储数据的映射,所以在本地禁止这些操作更便于维护。
  2. 禁止本地新建文章分类文件夹和文档分类文件夹。原因同上。
  3. 本地文章修改后,必须及时同步至 Halo,保证 Halo 版本不落后于本地文章。
  4. 本地仅有新增文章、新建文档,以及更新文章内容和部分属性(发布时间等需要自定义的属性),和更新文档内容和部分属性(优先级)

针对上述可能存在的冲突情况和冲突类型,设计如下更新原则和冲突检测原则:

  1. 分类文件夹的组织结构和位置是由远程 Halo 决定,所以每次更新都应该重建文件夹组织结构。换言之,分类文件夹的的冲突处理方式完全是 “Halo=>本地” 的单向覆盖。
  2. 文档文件夹同上。
  3. 理论上,文章文档版本均落后于 Halo 版本,也应该遵循单向覆盖和重建原则。

最终设计

基于上述的分析,最终设计如下。

Markdown 文件

markdown 文件作为 Obsidian 主要的内容载体形式,它在我们当前设计中被用来放 ”文章“ 和 ”文档“,除了给它们添加特定属性外,还应添加一些通用属性。具体如下:

Md 文件通用属性

  1. type:用于区分文件类型,例如 POST/DOC 等。

文章

前置数据:

  1. _name:唯一 id
  2. title:原始 title,区别于文件名,后者需要做字符替换和冲突检测。(可提交)
  3. _category:文章所属分类名称。
  4. _tags。
  5. publishTime:发布时间,排序属性。(可提交)
  6. _mtime
  7. _url:完整的文章链接,方便访问,也包含 slug。Halo 引用链接转 Obsidian 本地引用时得寻找依据。
  8. commited:布尔值,该属性的设计用于标记是否已提交 Halo。在对本地文章进行修改之前,先将该值设置为 false,在之后的提交成功后该值会自动修改为 true。方便从远程同步文章到本地时,避免覆盖未提交文章,也大大简化了冲突检测的机制和难度。

文件名:

  1. 用 title 正则替换字符。
  2. 文件名重复,添加创建日期。

正文转换:

  1. Halo 站内文章链接转 Obsidian 本地文件引用。url => 前置数据生成 map<url, path> => path。如果在 map 中未查询到 url 对应的文档,则先拉取该 url 对应的文章,并重建 map。
  2. Obsidian 本地文件引用转 Halo 站内文章链接。path => 前置数据生成 map<path, url> => url

文档

前置数据:

  1. _name
  2. title
  3. priority:优先级,排序用。
  4. _ctime
  5. _mtime
  6. _url
  7. commited

文件名、正文转换,同上。

文件夹

Obsidian 中的文件夹,对应 Halo 文章中的分类、文档中的项目、项目中的子目录等。

文件夹通用属性

  1. type:用于区分文件夹的类型,例如 categoryprojecttree

分类文件夹

额外属性:

  1. categoryName:发布分类文章用
  2. priority:

文档文件夹(project-Version-name)

额外属性:

  1. projectVersionName:发布文档用
  2. priority

文档子文件夹 (docTree-Tree)

额外属性:

  1. docTreeName
  2. priority

额外的数据存储

在上述分析中,Markdown 文件的属性可以存储在 前置数据中,但是文件夹的属性只能存储在额外的 json 文件中。

在 Obsidian 中,一个文件夹的唯一属性是它的路径。所以,本地查询某个文件夹的额外属性的 key 就应该是路径 path。所以,大致数据结构如下:

{
	folderPath: {
		type: 'catecory/project/docTree',
		name: 'metadata.name',
		priority: '8'
	}
}

开发中随时产生的新想法记录

暂无标签