@movevom/dangan
v0.1.0
Published
DangAn sidecar NDJSON (four-line) format utilities
Readme
@movevom/dangan
DangAn(档案)是一个“sidecar NDJSON(四行 JSON Lines)”格式与工具集,用于为任意内容文件在同目录维护一个同名 .json 侧车文件:
a.jpg→a.jpg.jsonb.md→b.md.jsonc.tar.gz→c.tar.gz.json
本包只提供纯逻辑(解析/格式化/合并/历史策略/sidecar key 规则)与可选的流程/Node 便捷入口:
- 默认入口
@movevom/dangan:跨平台纯逻辑(不绑定 Node/浏览器/小程序) - 子入口
@movevom/dangan/node:Node/Electron/脚本可用的 fs adapter(核心入口仍无 Node 依赖)
核心原则:
- DangAn 只负责“内容与规则”(四行 NDJSON 的结构、合并策略、历史策略、sidecar 命名规则)
- DangAn 不负责“存储位置与权限”(文件系统 / IndexedDB / 云端 / 小程序沙盒等由宿主决定)
- sidecar 是一个逻辑概念:它可以是一个真实的
*.json文件,也可以是一个“同名 key”的独立存储记录
安装
npm i @movevom/danganDangAn 文件格式(四行 NDJSON)
DangAn 文件内容是 4 行,每行都是一个 JSON object(不是一个“大 JSON”),顺序固定:
auto:自动生成/确定性字段(作者、时间、字数、文件指纹等)ai:AI(或其他非确定性生成器)写入的“当前有效版本”aiHistory:ai的版本历史列表nameHistory:文件名/路径历史(create/rename/move)
读取方式必须按“逐行 JSON.parse”处理,不要对整个文件内容做一次 JSON.parse(wholeText)。
不同运行环境的“sidecar”落地方式
DangAn 的标准命名是“同名 + .json”。落地时分两种情况:
A. 有真实文件系统(Desktop / Node / Electron / Tauri)
目标:在同目录创建一个真实侧车文件 original.ext.json。
- 读取:先读
original.ext.json,再按需读original.ext - 写入:原文件保存/更新后,同步更新其侧车文件
- 更名/移动:更新原文件名/路径的同时,更新侧车文件名/路径,并追加一条
nameHistory事件 - 删除:原文件与侧车文件一起删除
在 Node/Electron/脚本场景可以使用子入口提供的 fs adapter(见文末)。
Tauri App(更详细)
Tauri 的关键点是:前端不是 Node 运行时,所以不要导入 @movevom/dangan/node。你仍然使用默认入口 @movevom/dangan,并在“宿主层”用 Tauri 的文件 API 实现存储。
推荐的两种组织方式:
- App 管理的内容目录(推荐)
- 把“主内容文件 + sidecar”统一放进 app 的数据目录(例如 AppData / Application Support)
- 好处:权限简单、跨平台一致、迁移与备份更可控
- 用户选择的外部目录
- 主内容文件位于用户选择的文件夹(例如文库目录)
- sidecar 与主内容同目录:
original.ext.json - 需要处理:权限授权、路径变化、同名冲突、移动/重命名
实现要点(指导性):
- 用 adapter 把 DangAn 接到 Tauri 的读写能力上
- 实现
DangAnTextAdapter:readText(key)/writeText(key, text) key可以是“绝对路径”,也可以是你自己的“逻辑 key(再映射到路径)”
- 实现
- 写入要尽量原子化
- 推荐:写到
*.json.tmp,再 rename 成*.json - 避免:直接覆盖写导致半写入、或 app 崩溃留下损坏文件
- 推荐:写到
- 重命名/移动需要同时迁移 sidecar
- 文件系统改名/移动后:sidecar 同步改名/移动
- 追加
nameHistory事件,保留可追溯链路
- 跨平台路径与大小写
- Windows/macOS 常见是大小写不敏感;path normalization 与冲突处理要在宿主层完成
- 尽量不要把“路径字符串”当作唯一稳定 id;建议另存一个稳定
id(例如 uuid/哈希)写入 auto.id
- 扫描策略(性能)
- AI 先扫 sidecar:遍历目录时优先读
*.json,避免无谓读取大正文文件 - 如果 sidecar 缺失:按需补齐(lazy ensure)
- AI 先扫 sidecar:遍历目录时优先读
B. 没有真实文件系统或权限受限(Web / 移动端 / 小程序)
目标:用“同名 key”来模拟 sidecar:
- 主文件 key:
fileKey - 侧车 key:
toDangAnSidecarKey(fileKey),等价于${fileKey}.json
常见存储选择:
- IndexedDB:为 DangAn 单独建一个 store(推荐),value 保存四行文本或结构化对象
- KV 存储:例如 localStorage(不推荐大内容)、小程序 storage、移动端 KV
- 云端:用对象存储/数据库的 key 体系映射 sidecar key
重要:在这类环境里,“sidecar 是否对用户可见”完全由宿主 UI 决定。推荐做法是让 sidecar 只存在于专用存储域,不参与用户文件列表的枚举。
快速开始(纯逻辑)
import {
createEmptyDangAn,
parseDangAnText,
formatDangAnText,
updateAi,
updateAuto,
appendNameEvent
} from '@movevom/dangan'
} from '@movevom/dangan'
let doc = createEmptyDangAn({ now: Date.now(), name: 'hello.md' })
doc = updateAuto(doc, { updatedAt: Date.now(), wordCount: 123 })
doc = updateAi(
doc,
{ model: 'my-model', keywords: ['dangAn'] },
{ now: Date.now(), maxHistoryItems: 20 }
)
doc = appendNameEvent(doc, { op: 'rename', from: 'hello.md', name: 'hello-2.md', at: Date.now() })
const text = formatDangAnText(doc)
const parsed = parseDangAnText(text)推荐的宿主集成流程(指导性)
1) 创建(Create)
当宿主创建一个新内容文件/文档时:
- 生成主文件 key(路径、ID、URL、对象 key 都可以)
- 用
createEmptyDangAn({ now, name })初始化 - 把确定性字段写进
auto(例如 createdAt/updatedAt/作者/字数/指纹等) - 把四行文本保存到 sidecar(真实文件
*.json或 sidecarKey)
2) 更新(Update)
当主文件内容变化时:
- 重新计算确定性字段(例如 updatedAt、wordCount、fingerprint)
- 只用
updateAuto更新auto行,不要覆盖ai行 - 保存 sidecar
3) AI 写入(AI Update)
当 AI 产生新的 tags/keywords/description/links 等时:
- 只用
updateAi(doc, nextAi, { now, maxHistoryItems })更新ai行 - 让库自动追加版本到
aiHistory - 保存 sidecar
4) 重命名/移动(Rename/Move)
当主文件发生 rename/move 时:
- 先迁移 sidecar(文件系统改名;或更新主 key / sidecar key 的映射)
- 用
appendNameEvent追加一条 nameHistory 事件 - 保存 sidecar
5) 读取与检索(Scan -> Decide -> Read)
当你需要“让 AI 先扫元信息再决定读正文”:
- 枚举候选集合(按目录、标签、时间等)
- 对每个候选先读取 sidecar,拿到
auto/ai的轻量信息 - 只对被选中的少数候选,再读取其主文件正文内容
这个流程能显著降低 token/IO,并让 AI 的选择更可控。
核心 API
解析与格式化
parseDangAnText(text): DangAnDoc:容错解析(缺行/乱序/空行均可)formatDangAnText(doc): string:输出标准四行顺序migrateDangAn(doc): { doc; changed }:把结构规范化到当前 schema(用于长期演进)
Sidecar 命名规则
toDangAnSidecarPath(path): string:${path}.jsontoDangAnSidecarKey(fileKey): string:${fileKey}.json(适用于虚拟 key/IDB key 等)
合并与历史策略
createEmptyDangAn({ now?, auto?, ai?, name? }): DangAnDocupdateAuto(doc, patch, policy?): DangAnDoc- 默认不允许在 auto 行覆盖 AI 字段;如确需覆盖可传
policy.allowAiFieldsInAuto = true
- 默认不允许在 auto 行覆盖 AI 字段;如确需覆盖可传
updateAi(doc, next, policy): DangAnDocpolicy.now必填(由宿主提供时间源)- 自动将新版本写入
aiHistory.items并按maxHistoryItems裁剪(默认 20)
appendNameEvent(doc, event): DangAnDoc
流程 API(带 adapter,不绑定环境)
本包提供了一个最小 “文本存储 adapter” 接口,用于把 core 逻辑串成 load/save/ensure 流程:
import type { DangAnTextAdapter } from '@movevom/dangan'
import { ensureDangAnForFile, loadDangAn, saveDangAn } from '@movevom/dangan'DangAnTextAdapter 只要求:
readText(key): Promise<string | null>writeText(key, text): Promise<void>
你可以在 Web/小程序/移动端用 IndexedDB/KV/沙盒文件 API 实现它,在桌面端用 Tauri/Electron/Node 实现它。
Node 子入口(可选)
在 Node/Electron/脚本场景,可直接使用内置的 fs adapter:
import { createNodeFsAdapter } from '@movevom/dangan/node'
import { ensureDangAnForFile } from '@movevom/dangan'
const adapter = createNodeFsAdapter()
const { doc } = await ensureDangAnForFile(adapter, '/abs/path/to/a.jpg', {
now: Date.now(),
name: 'a.jpg'
})注意:浏览器/小程序/受限容器不要导入 @movevom/dangan/node。
本地测试(不发布)
如果你想在另一个项目里本地测试本包而不发布到 npm,推荐使用 npm pack 的 tgz 安装方式。见:
