Obsidian 小红书同步插件(Xiaohongshu Sync)二次开发完整教程
基于开源插件 bnchiang96/xiaohongshu-importer 二次开发,打造一个功能完整、全中文化、支持手机短链、自动排版、属性丰富的小红书笔记导入插件。目录
- 项目背景与目标
- 准备工作
- 获取源码
- 项目结构说明
- 核心改造点
- 完整开发流程
- 构建与安装
- 功能清单
- 进阶扩展方向
- 常见问题
一、项目背景与目标
原版插件的问题
原版 Xiaohongshu Importer 虽然能用,但存在以下不足:
| 问题 | 影响 |
|---|---|
| 只支持 http://xhslink.com 短链 | 手机 App 分享的 https://xhslink.cn 链接无法识别 |
| 界面部分英文 | 中文用户体验不友好 |
| 文件存储路径固定 | 无法自定义文件夹结构 |
| 无作者、发布时间等属性 | 不利于后续数据检索 |
| 排版粗糙 | 下载后是一堆文字,标签、图片堆在一起 |
| 标签在正文末尾 | 无法用 Dataview 按标签检索 |
| 图片文件名带时间戳 | 冗长且无意义 |
二次开发目标
- 改名:插件 ID 改为 xiaohongshu-sync,名称改为 Xiaohongshu Sync
- 全中文化:设置面板、弹窗、通知、报错全部中文
- 适配手机短链:兼容 xhslink.com 和 xhslink.cn
- 智能存储:有附件建同名文件夹,无附件直接建文件
- 日期前缀:文件夹和文件名加下载日期
- 丰富属性:新增作者、作者主页、发布时间
- 标签入属性:标签作为 frontmatter 最后一项,YAML 列表格式
- 基础排版:段落整理、图片独立区块、去掉冗余空行
- 快速新增分类:导入弹窗内即可新增分类,持久化到设置
二、准备工作
环境要求
| 工具 | 版本 | 说明 |
|---|---|---|
| Node.js | 18.x 或更高 | 用于安装依赖和构建 |
| npm | 随 Node.js 安装 | 包管理器 |
| Obsidian | 0.15.0 或更高 | 测试插件 |
| Git(可选) | 最新版 | 克隆仓库 |
| 代码编辑器 | VS Code 推荐 | 修改源码 |
检查环境
node -v
npm -v
清理原项目文件
克隆或下载原仓库后,保留以下 6 个文件即可,其余可删除:
xiaohongshu-sync/
├── main.ts ← 核心源码
├── manifest.json ← 插件身份
├── package.json ← 依赖和构建脚本
├── esbuild.config.mjs ← 构建配置
├── tsconfig.json ← TS 编译配置
└── styles.css ← 样式
可删除的文件:
- .editorconfig、.eslintignore、.eslintrc、.gitignore
- .npmrc、LICENSE(自己用可删)
- README.md、version-bump.mjs、versions.json
三、获取源码
原版仓库地址
- 普通版:https://github.com/bnchiang96/xiaohongshu-importer
- Plus 版:https://github.com/lxl448080113/ob-Plugin
注意:Plus 版只提供编译后的 main.js,没有 .ts 源码,属于源码不公开。二次开发请使用普通版。
克隆到本地
git clone https://github.com/bnchiang96/xiaohongshu-importer.git xiaohongshu-sync
cd xiaohongshu-sync
四、项目结构说明
核心文件职责
| 文件 | 职责 |
|---|---|
| main.ts | 插件全部逻辑:导入、解析、存储、设置面板、弹窗 |
| manifest.json | 插件元信息:ID、名称、版本、最低 Obsidian 版本 |
| package.json | 依赖列表、构建脚本、插件元信息 |
| esbuild.config.mjs | 构建配置:入口、输出、外部依赖、banner |
| tsconfig.json | TypeScript 编译选项 |
| styles.css | 弹窗和分类 chip 的样式 |
关键类结构(改造后)
XHSSyncPlugin ← 主插件类
├── onload() ← 注册 ribbon、命令、设置面板
├── extractURL() ← 提取小红书链接
├── importXHSNote() ← 主导入流程
├── extractTitle() ← 提取标题
├── extractImages() ← 提取图片列表
├── extractVideoUrl() ← 提取视频地址
├── extractContent() ← 提取正文
├── isVideoNote() ← 判断是否为视频笔记
├── extractAuthor() ← 提取作者信息(新增)
├── extractPublishTime() ← 提取发布时间(新增)
├── extractTags() ← 提取标签
├── formatContent() ← 正文排版(新增)
└── formatDateTime() ← 时间格式化(新增)
XHSSyncSettingTab ← 设置面板
XHSInputModal ← 导入弹窗
五、核心改造点
1. 插件改名
manifest.json:
{
"id": "xiaohongshu-sync",
"name": "Xiaohongshu Sync",
"version": "1.0.0",
"minAppVersion": "0.15.0",
"description": "导入小红书笔记,支持媒体下载、分类、作者信息和自动排版。",
"author": "你的名字",
"authorUrl": "https://github.com/你的用户名",
"isDesktopOnly": false
}
package.json:
{
"name": "xiaohongshu-sync",
"version": "1.0.0",
"description": "Import Xiaohongshu notes into Obsidian.",
...
}
esbuild.config.mjs banner:
const banner =
`/*
* Xiaohongshu Sync plugin
* Based on Xiaohongshu Importer by bnchiang96 (MIT)
* Version: 1.0.0
*/
`;
main.ts 类名和引用:
- XHSImporterPlugin 改为 XHSSyncPlugin
- XHSImporterSettings 改为 XHSSyncSettings
- XHSImporterSettingTab 改为 XHSSyncSettingTab
2. 适配手机短链
原版正则只匹配 http://xhslink.com/a?o?/,改为:
extractURL(shareText: string): string | null {
const mobileUrlMatch = shareText.match(/https?:\/\/xhslink\.(?:com|cn)\/[a-zA-Z0-9/?=&._-]+/);
if (mobileUrlMatch) {
return mobileUrlMatch[0];
}
const webUrlMatch = shareText.match(/https?:\/\/www\.xiaohongshu\.com\/(?:discovery\/item|explore)\/[a-zA-Z0-9]+(?:\?[^\s,,]*)?/);
if (webUrlMatch) {
return webUrlMatch[0].replace('/explore/', '/discovery/item/');
}
return null;
}
兼容的链接格式:
- https://xhslink.cn/o/xxxxx
- http://xhslink.com/a/xxxxx
- https://www.xiaohongshu.com/discovery/item/xxxxx
- https://www.xiaohongshu.com/explore/xxxxx
3. 智能存储逻辑
规则:
| 情况 | 存储路径 |
|---|---|
| 有图片/视频 | 默认文件夹/分类/日期_标题/日期_标题.md + 图片同文件夹 |
| 无图片/视频 | 默认文件夹/分类/日期_标题.md |
核心代码:
const hasMedia = (images.length > 0 || (isVideo && videoUrl)) && downloadMedia;
if (hasMedia) {
const noteFolder = `${folderPath}/${fileBaseName}`;
if (!await this.app.vault.adapter.exists(noteFolder)) {
await this.app.vault.createFolder(noteFolder);
}
notePath = `${noteFolder}/${fileBaseName}.md`;
mediaFolder = noteFolder;
} else {
if (!await this.app.vault.adapter.exists(folderPath)) {
await this.app.vault.createFolder(folderPath);
}
notePath = `${folderPath}/${fileBaseName}.md`;
mediaFolder = folderPath;
}
4. 日期前缀
文件夹和文件名统一用 日期_标题 格式:
const datePrefix = noteDate; // 2026-09-16
const fileBaseName = `${datePrefix}_${safeTitle}`;
5. 图片和视频文件名简化
改前:
标题-0-1758000000000.jpg
标题-1-1758000000001.jpg
标题-1758000000000.mp4
改后:
1.jpg
2.jpg
video.mp4
改动点:
// 图文笔记
const imageFilename = `${i + 1}.jpg`;
// 视频笔记封面
const imageFilename = `1.jpg`;
// 视频
const videoFilename = `video.mp4`;
6. 新增作者和发布时间属性
新增两个方法:
extractAuthor(html: string): { nickname: string; profileUrl: string } {
const stateMatch = html.match(/window\.__INITIAL_STATE__=(.*?)<\/script>/s);
if (!stateMatch) return { nickname: "", profileUrl: "" };
try {
const jsonStr = stateMatch[1].trim();
const cleanedJson = jsonStr.replace(/undefined/g, "null");
const state = JSON.parse(cleanedJson);
const noteId = Object.keys(state.note.noteDetailMap)[0];
const note = state.note.noteDetailMap[noteId].note;
const user = note.user || {};
const nickname = user.nickname || user.nickName || "";
const userId = user.userId || user.user_id || "";
const profileUrl = userId ? `https://www.xiaohongshu.com/user/profile/${userId}` : "";
return { nickname, profileUrl };
} catch (e) {
console.log(`解析作者信息失败:${e.message}`);
return { nickname: "", profileUrl: "" };
}
}
extractPublishTime(html: string): string {
const stateMatch = html.match(/window\.__INITIAL_STATE__=(.*?)<\/script>/s);
if (!stateMatch) return "";
try {
const jsonStr = stateMatch[1].trim();
const cleanedJson = jsonStr.replace(/undefined/g, "null");
const state = JSON.parse(cleanedJson);
const noteId = Object.keys(state.note.noteDetailMap)[0];
const note = state.note.noteDetailMap[noteId].note;
const rawTime = note.time || note.publishTime || "";
if (!rawTime) return "";
const date = new Date(Number(rawTime));
if (isNaN(date.getTime())) return "";
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, "0");
const d = String(date.getDate()).padStart(2, "0");
return `${y}-${m}-${d}`;
} catch (e) {
console.log(`解析发布时间失败:${e.message}`);
return "";
}
}
frontmatter 增加:
作者: 林间自然
作者主页: https://www.xiaohongshu.com/user/profile/xxxxx
发布时间: 2026-09-10
7. 标签移入属性
改前:标签在正文末尾,代码块形式
改后:标签作为 frontmatter 最后一项,YAML 列表
let frontmatterTags = "tags: []\n";
if (tags.length > 0) {
frontmatterTags = "tags:\n" + tags.map((tag) => ` - ${tag}`).join("\n") + "\n";
}
标签提取正则修正(避免把标点符号也抓进来):
extractTags(content: string): string[] {
const tagMatches = content.match(/#[\w\u4e00-\u9fa5-]+/g) || [];
return tagMatches.map((tag) => tag.replace("#", "").trim());
}
8. 正文排版
新增 formatContent 方法:
formatContent(rawContent: string): string {
let text = rawContent;
text = text.replace(/#[^#\s]*(?:\s+#[^#\s]*)*\s*/g, "");
const lines = text.split("\n").map((line) => line.trim());
const paragraphs: string[] = [];
let buffer: string[] = [];
for (const line of lines) {
if (line === "") {
if (buffer.length > 0) {
paragraphs.push(buffer.join(" "));
buffer = [];
}
} else {
buffer.push(line);
}
}
if (buffer.length > 0) {
paragraphs.push(buffer.join(" "));
}
return paragraphs.join("\n\n");
}
排版效果:
- 去掉标签和话题标记
- 段落之间空一行
- 图片独立成 ## 图片 区块,每张图前加 图 N
9. 快速新增分类
在导入弹窗的分类 chip 下方加一行输入框和按钮:
const addCategoryRow = contentEl.createEl("div", { cls: "xhs-add-category-row" });
const newCategoryInput = addCategoryRow.createEl("input", {
cls: "xhs-add-category-input",
attr: { type: "text", placeholder: "新增分类名称" },
});
const addCategoryButton = addCategoryRow.createEl("button", {
text: "新增分类",
cls: "xhs-add-category-button",
});
const handleAddCategory = async () => {
const name = newCategoryInput.value.trim();
if (!name) {
new Notice("请输入分类名称。");
return;
}
if (name === "其他") {
new Notice("“其他”是保留分类,请换一个名称。");
return;
}
if (this.plugin.settings.categories.includes(name)) {
new Notice("该分类已存在。");
return;
}
this.plugin.settings.categories.push(name);
await this.plugin.saveSettings();
this.selectedCategory = name;
newCategoryInput.value = "";
renderChips();
new Notice(`已新增分类:${name}`);
};
10. 全中文化
设置面板:
- Default folder 改为 默认存储文件夹
- Download media 改为 默认下载媒体
- Categories 改为 分类管理
- Add category 改为 添加分类
- Remove 改为 删除
- Move up 改为 上移
- Move down 改为 下移
弹窗:
- Import Xiaohongshu note 改为 导入小红书笔记
- Paste the share text below: 改为 粘贴分享文本:
- Select a category: 改为 选择分类:
- Download media locally for this import 改为 本次导入下载媒体到本地
- Import 改为 导入
通知和报错:
- No valid Xiaohongshu URL found in the text. 改为 未在文本中找到有效的小红书链接。
- Failed to download media: 改为 下载媒体失败:
- Imported Xiaohongshu note as 改为 已导入小红书笔记:
- Failed to import note: 改为 导入笔记失败:
11. 删除冗余字段
原版 frontmatter 里 date 和 导入时间 重复,删除 date,只保留 导入时间,格式统一为 YYYY-MM-DD HH:mm:ss。
formatDateTime(date: Date): string {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, "0");
const d = String(date.getDate()).padStart(2, "0");
const hh = String(date.getHours()).padStart(2, "0");
const mm = String(date.getMinutes()).padStart(2, "0");
const ss = String(date.getSeconds()).padStart(2, "0");
return `${y}-${m}-${d} ${hh}:${mm}:${ss}`;
}
六、完整开发流程
步骤 1:安装依赖
npm install
步骤 2:开发模式构建
npm run dev
开发模式会监听 main.ts 变化,自动重新编译。
步骤 3:生产模式构建
npm run build
生产模式会压缩代码,生成 main.js。
步骤 4:复制到插件目录
把以下三个文件复制到 Obsidian 库的插件目录:
main.js
manifest.json
styles.css
目标路径:
<你的库>/.obsidian/plugins/xiaohongshu-sync/
步骤 5:启用插件
- 重启 Obsidian
- 进入 设置 - 第三方插件
- 找到 Xiaohongshu Sync,打开开关
步骤 6:测试导入
- 点击左侧边栏的书本图标,或按 Ctrl/Cmd + P 搜索“导入小红书笔记”
- 粘贴小红书分享文本
- 选择分类(可现场新增)
- 勾选是否下载媒体
- 点击“导入”
七、构建与安装
完整命令序列
# 1. 进入项目目录
cd xiaohongshu-sync
# 2. 安装依赖
npm install
# 3. 生产构建
npm run build
# 4. 复制到插件目录(手动操作)
# 把 main.js、manifest.json、styles.css 复制到
# <你的库>/.obsidian/plugins/xiaohongshu-sync/
最终插件目录结构
<你的库>/.obsidian/plugins/xiaohongshu-sync/
├── main.js ← 编译产物
├── manifest.json ← 插件元信息
└── styles.css ← 样式
导入后的文件结构
xhs/
└── 美食/
└── 2026-09-16_某篇笔记/
├── 2026-09-16_某篇笔记.md
├── 1.jpg
├── 2.jpg
└── 3.jpg
无附件的笔记:
xhs/
└── 知识/
└── 2026-09-15_无图笔记.md
八、功能清单
已实现功能
| 功能 | 状态 | 说明 |
|---|---|---|
| 手机短链适配 | 已完成 | 兼容 xhslink.com 和 xhslink.cn |
| 网页链接适配 | 已完成 | 兼容 discovery/item 和 explore |
| 全中文化 | 已完成 | 设置、弹窗、通知、报错 |
| 智能存储 | 已完成 | 有附件建文件夹,无附件直接建文件 |
| 日期前缀 | 已完成 | 文件夹和文件名加 YYYY-MM-DD_ |
| 图片序号 | 已完成 | 1.jpg、2.jpg 从 1 开始 |
| 视频处理 | 已完成 | 下载为 video.mp4 |
| 作者昵称 | 已完成 | frontmatter 作者 字段 |
| 作者主页 | 已完成 | frontmatter 作者主页 字段 |
| 发布时间 | 已完成 | frontmatter 发布时间 字段 |
| 标签入属性 | 已完成 | YAML 列表格式,最后一项 |
| 正文排版 | 已完成 | 段落整理,图片独立区块 |
| 快速新增分类 | 已完成 | 弹窗内即时新增,持久化 |
| 分类管理 | 已完成 | 设置面板可增删改、排序 |
frontmatter 最终格式
---
title: 想做自然教育,第一场活动怎么落地?
作者: 林间自然
作者主页: https://www.xiaohongshu.com/user/profile/xxxxx
发布时间: 2026-09-10
source: https://xhslink.cn/o/6oa52Edo8CC
导入时间: 2026-09-16 14:30:00
分类: 知识
tags:
- 自然教育创业
- 自然教育
- 林间自然
- 自然体验活动
- 自然探索之旅
- 户外自然教育
---
Dataview 检索示例
TABLE 作者, 发布时间, 分类
FROM #自然教育
SORT 发布时间 DESC
TABLE 作者, 发布时间
FROM "xhs"
WHERE 分类 = "知识"
SORT 导入时间 DESC
九、进阶扩展方向
1. 互动数据入属性
小红书页面的 INITIAL_STATE 里包含点赞、收藏、评论数,可以提取后写入 frontmatter:
点赞: 1234
收藏: 567
评论: 89
配合 Dataview 可以按热度排序筛选。
2. URI Protocol Handler
注册 obsidian://xiaohongshu-sync 协议,手机端配合 iOS 快捷指令或 Android Tasker,在小红书 App 里分享后直接跳转导入。
this.registerObsidianProtocolHandler("xiaohongshu-sync", async (params) => {
const url = params.url;
// 触发导入
});
3. 图片横排显示
配合 Image Grid 插件,把图片区块输出改成:
![[1.jpg]]
![[2.jpg]]
![[3.jpg]]4. 图片轮播切换
配合 Simple Image Slider 插件,输出:
![[1.jpg]]
![[2.jpg]]
![[3.jpg]]5. 批量导入收藏夹
扩展插件,支持读取用户收藏夹列表,批量导入多篇笔记。
6. AI 自动分类
接入 AI 模型,根据笔记内容自动推荐分类,减少手动选择。
7. 发布到七牛云
配合七牛云插件,增加“上传当前笔记所有本地图片并替换链接”命令,方便发布到网站。
十、常见问题
Q1:构建时报错 Cannot find module 'obsidian'
原因:依赖未安装。
解决:
npm install
Q2:npm install 时出现 allow-scripts 警告
原因:npm 的安全提示,esbuild 的 postinstall 脚本未自动执行。
解决:不影响构建,忽略即可。如果构建报错找不到 esbuild,执行:
npm approve-scripts esbuild
Q3:插件启用后没反应
排查:
- 确认 main.js、manifest.json、styles.css 三个文件都在同一个文件夹
- 确认文件夹名和 manifest.json 里的 id 一致(xiaohongshu-sync)
- 重启 Obsidian
- 在 设置 - 第三方插件 里确认插件已启用
Q4:导入时提示“未在文本中找到有效的小红书链接”
排查:
- 确认链接格式是 xhslink.com、xhslink.cn 或 xiaohongshu.com
- 确认链接是公开笔记,不是私密或已删除
- 检查分享文本里是否包含完整 URL
Q5:图片下载失败
排查:
- 确认“下载媒体”选项已勾选
- 检查网络连接
- 确认图片 URL 可访问
- 部分图片可能有防盗链,需要检查 Referer
Q6:标签在 Obsidian 里显示被划线
原因:标签名包含 Obsidian 不支持的字符(如标点符号)。
解决:已修正 extractTags 正则,只匹配字母、数字、下划线、连字符、正斜杠和中文:
const tagMatches = content.match(/#[\w\u4e00-\u9fa5-]+/g) || [];
Q7:Windows 下看不到 .obsidian 文件夹
解决:在资源管理器里勾选 查看 - 显示 - 隐藏的项目。
Q8:构建产物 main.js 在哪里
答:在项目根目录,和 main.ts 同级。npm run build 后会生成或覆盖。
Q9:能否同时保留原版插件和二次开发版
答:可以,只要 manifest.json 里的 id 不同即可。原版是 xiaohongshu-importer,二次开发版是 xiaohongshu-sync,两者互不冲突。
Q10:MIT 许可证要求
答:原版采用 MIT 许可证,允许修改和分发。如果公开发布二次开发版,建议在 README 里注明:
Based on Xiaohongshu Importer by bnchiang96 (MIT License)
https://github.com/bnchiang96/xiaohongshu-importer
自己用则没有这个约束。
附录:关键代码文件
manifest.json 最终版
{
"id": "xiaohongshu-sync",
"name": "Xiaohongshu Sync",
"version": "1.0.0",
"minAppVersion": "0.15.0",
"description": "导入小红书笔记,支持媒体下载、分类、作者信息和自动排版。",
"author": "你的名字",
"authorUrl": "https://github.com/你的用户名",
"isDesktopOnly": false
}
styles.css 关键样式
/* 弹窗内容容器 */
.xhs-modal-content {
display: flex;
flex-direction: column;
gap: 10px;
padding: 10px;
}
/* 分类 chip */
.xhs-chip {
padding: 4px 8px;
border-radius: 12px;
border: 1px solid #ccc;
background-color: #f0f0f0;
color: #000;
cursor: pointer;
transition: background-color 0.2s;
}
.xhs-chip.xhs-chip--selected {
background-color: #FF2442;
color: #fff;
}
/* 新增分类行 */
.xhs-add-category-row {
display: flex;
gap: 8px;
align-items: center;
margin-top: 4px;
}
/* 去掉设置页面分类管理标题的左边距 */
.xhs-setting-heading {
margin-left: 0;
padding-left: 0;
}
总结
通过这次二次开发,我们把一个基础的小红书导入插件,改造成了一个功能完整、体验友好、便于检索的知识管理工具。核心思路是:
- 兼容性优先:适配手机短链,覆盖更多用户场景
- 中文化:降低中文用户的使用门槛
- 结构化存储:日期前缀 + 智能文件夹,文件管理更清晰
- 元数据丰富:作者、发布时间、标签入属性,方便 Dataview 检索
- 排版优化:正文段落整理,图片独立区块,阅读体验更好
- 交互便捷:弹窗内快速新增分类,减少设置切换
这套流程不仅适用于小红书插件,也可以作为其他 Obsidian 插件二次开发的参考模板。
暂无评论
还没有评论,来说点什么吧