

《喜相逢》
蓝天白云青草地,
萱花摇曳笑盈盈。
月寒日暖添人寿,
野汀新绿鹿呦呦。
一眨眼,椂宝满月了。我也顺理成章地以家庭为重,把公众号断更了一个多月。
福宝在客厅嬉闹,椂宝在卧室啼哭。两股声音交叠在一起,有时竟生出移形换位的错觉——恍惚间,仿佛有一个娃娃从卧室里爬出来,撑起手臂,朝我跑来。
我不知道他们长大后会是什么样子。他们有自由的意志、个性的喜好,还有尚不可知的拐点,足以在某一天扭转人生。
我便想起自己的公众号来。过去半年它停停续续,灵感与激情渐渐淡去,却仍是这两年半一路走来的见证。趁此契机,我也在重想:插件、Obsidian,乃至整个知识管理系统,应当走向何方。
我盯着 Obsidian 的名字出神。它陪了我太久,像一位沉默的老友——见证过笔记、脚本、插件的一路生长,却从不回话。那一刻,我希望它开口说话。
于是有了 OldBuddy。
NoteChain 1.4.1 把它带进插件:在 Obsidian 里用微信风格界面聊天,文字、语音、图片、文件都能发;历史可写进日记,回复逻辑由 Templater 脚本定制,外部自动化也能通过 HTTP 推送消息。它不是要取代 Obsidian,而是给这位「老朋友」一张嘴、一扇窗。

为什么需要这样一个聊天窗口?
椂宝满月这阵子,我写字少了、更新慢了,却更清楚自己缺什么:不是更复杂的面板,而是一个会回应的入口。
Obsidian 擅长写笔记、链知识、跑脚本,但不擅长「像聊天一样随手说一句话」。你可能也熟悉这些时刻:
走路时想记一句灵感,却不想打开整库找笔记、切面板、找模板 脚本或 AI 跑完了,结果只出现在控制台或某张笔记里,没有「有人回你一句」的反馈 WebView LLM、MCP、定时任务各自为政,缺少一个统一的入口:发一条、收一条、历史可查 想把对话写进当天日记,又不想手动复制粘贴
OldBuddy 要补的就是这块:一个轻量、熟悉、可编程的聊天面——像微信那样顺手,像日记那样留痕,像 Templater 那样听你的话。
它和「在 Obsidian 里嵌一个 ChatGPT 网页」不一样:
- 界面像微信
:手机浏览器打开就能用,语音、图片、位置都顺手 - 逻辑在你 vault 里
:回复、存盘、@ 列表、头像全是 Templater 脚本,改脚本即改行为 - 和 Note-Chain 一体
:HTTP 服务、 、日记 infield、Dataview 查询,和现有工作流接得上ea.web.post 对外可接入:自动化脚本 推通知,你在聊天里立刻看到push_message
换句话说:OldBuddy 不是又一个聊天机器人,而是 Obsidian 的「对话层」——
你说话,脚本和外部系统回应,重要内容落进日记;平时当快捷输入,需要时再深度写笔记。
快速开始
1. 开启 HTTP 服务
设置 → Note Chain → 启用 HTTP Server(默认 )。127.0.0.1:3000
2. 打开聊天页
浏览器访问:
http://127.0.0.1:3000/oldbuddy端口以设置里的 为准。手机同一局域网可访问电脑 IP,例如 httpServerPort。http://192.168.1.10:3000/oldbuddy
3. 准备 Templater 脚本(可选但推荐)
notes/OldBuddy/ 下七个脚本复制到 vault 的 Templater 脚本目录,basename 必须一致(如 nochain_oldbuddy_reply.md)。脚本不存在时,OldBuddy 仍可用内置默认行为。下文逐一说明;更完整的示例见 [[OldBuddy]] 系列笔记。
Templater 脚本详解
脚本块用 或```js tpl<%* … -%>列表类脚本返回 或 JSON 字符串[{ label, text }, …]插件通过 (Templater 里也可用extra.oldbuddy)传入上下文tp.config.extra.oldbuddy
nochain_oldbuddy_targets — 聊天对象
返回格式:
return [{ label: '本地', text: 'local' },{ label: 'Obsidian', text: 'obsidian' },];
label:界面显示名 :内部 id(写入消息的text字段)target
默认: 脚本不存在时仅 。[{ label: 'local', text: 'local' }]
nochain_oldbuddy_quick_commands — 快捷命令 ⚡
quick_cmd_id 传给 reply)。返回格式(按 target 分组):
return {local: [{ label: '你是谁', text: '你是谁' },],obsidian: [{ label: 'Who u are', text: 'Who u are' },],'*': [ // 通用兜底{ label: '通用命令', text: '通用命令' },],};
默认: 脚本不存在时, 下仅「你是谁」。local
nochain_oldbuddy_reply — 自动回复
作用: 决定「老友」如何回应——纯文本、Markdown 卡片,或通过 自行推送多条消息。push_message
传入 :extra.oldbuddy
message | |
history | |
target | |
quick_cmd_id |
| 字符串 | card: true 时可渲染 Markdown 卡片) |
true | 不再push_message 推过 |
const ob = tp.config?.extra?.oldbuddy || extra?.oldbuddy;return`收到(${ob?.target}):${ob?.message?.content}`;
示例(push_message + return true):
//javascriptconst ob = tp.config?.extra?.oldbuddy || extra?.oldbuddy;const s = app.plugins.plugins['note-chain'].settings.notechain;const base = `http://${s.httpServerHost}:${s.httpServerPort}`;await ea.web.post(`${base}/oldbuddy/push_message`, {content: '## 分析结果\n\n……',sender: 'buddy',target: ob?.target || 'local',card: true,});returntrue;
nochain_oldbuddy_save — 消息写入日记
YYYY-MM-DD.md 的 callout 下,用 Dataview infield 一行一条(与瞬时笔记同类写法)。传入 :extra.oldbuddy
action | 'save' |
message | |
messages | |
data_dirmessages_file |
单行示例:
//markdown- (id:: xxx) (sender:: user) (target:: local) (type:: text) (timestamp:: …) (content:: 你好) (s::ob)
return true日记写入成功后 → 该条不再写入return true,长期历史交给 messages.json 从日记读。未返回 nochain_oldbuddy_query 则 JSON 与日记双份保存。true
nochain_oldbuddy_query — 从日记读历史
YYYY-MM-DD 日记中带 (s::ob) 的列表行,解析为消息数组,与本地 messages.json按 id 合并后分页返回。传入 :extra.oldbuddy
action | 'query' |
limitbefore / target | |
messages |
OldBuddyMessage[] 或 JSON.stringify([...])。与 save 成对使用:save 写日志、query 读日志,即可让 Obsidian 日记成为唯一真相来源nochain_oldbuddy_reference — 输入 @ 引用
作用: 微信式 @ 浮层——选人/选笔记/选角色,选中后插入 。@显示名
返回格式:
return [{ label: '老友', text: 'buddy' },{ label: 'Obsidian', text: 'obsidian' },];
label:浮层显示、插入到输入框的文字 :可选副标题或实际 id(reply 里可解析text)message.content
默认: 脚本不存在或返回空数组 → 不显示浮层。
nochain_oldbuddy_avatar — 头像与昵称
作用: 为每条消息配置发送者昵称与头像——用户气泡在右、老友在左,类似微信。
返回格式:
//javascriptreturnJSON.stringify({user: ['我', 'OldBuddy/avatars/user.png'],user_script: ['脚本', 'OldBuddy/avatars/script.png'],buddy: ['老友', 'OldBuddy/avatars/buddy.png'],});
key:与消息的 对应;sender可单独配置,否则回退到user_*user值: [昵称, 头像路径]头像路径:vault 相对路径(经 读取)、http(s) URL,或/oldbuddy/api/vault_asset?path=/oldbuddy/uploads/...
默认: 脚本不存在 → 无头像,仅显示昵称首字占位。
脚本协作关系(一图流)
//texttargets ──→ 选和谁聊├── quick_commands / reference / avatar(按 target 变化)├── 用户发消息│ ├── save ──→ 日记 (s::ob) returntrue → 跳过 JSON│ └── reply ──→ 文本 或 push_message + returntrue└── query ──→ 读日记 + 合并 JSON ──→ 聊天历史
聊天界面
- 文字
:输入框发送;桌面 Enter 发送,Shift+Enter 换行 - 语音 / 图片 / 文件
:底栏 📁 菜单 - 位置
:可读取 GPS 写入输入框 - @ 引用
:由 提供浮层nochain_oldbuddy_reference - 实时
:WebSocket 推送新消息/oldbuddy/ws - 历史
:上滑加载; 与本地 JSON 合并nochain_oldbuddy_query 筛选:状态栏可「仅当前对象」「隐藏旧记录」
消息布局:用户( / user)绿色气泡在右,老友在左;头像由 user_* 配置。nochain_oldbuddy_avatar
消息存哪里?
oldbuddy-data/messages.json | true 时跳过 | |
(s::ob) | nochain_oldbuddy_savenochain_oldbuddy_query |
两层可叠加,也可只用其一。推荐:save 写日记并 ,query 读日记。return true
第三方推送:/oldbuddy/push_message
JSON 或表单,向聊天窗口注入消息(含 WebSocket 实时显示)。POST
//javascriptconst s = app.plugins.plugins['note-chain'].settings.notechain;const res = await ea.web.post(`http://${s.httpServerHost}:${s.httpServerPort}/oldbuddy/push_message`,{content: '来自脚本的通知',sender: 'buddy', // user / user_* / buddy 等target: 'local',card: true,skip_reply: true, // sender 为 user 时可禁止自动 reply});
content | |
sender | buddy;user / user_* 视为用户侧(右侧气泡) |
target | local |
type | textimage / audio / file |
card | |
skip_reply | true |
sender 约定
user | ||
user_*user_script) | ||
buddy |
夜雨聆风