三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

我给 WorkBuddy 接上 Obsidian,被 401 和中文乱码折腾了大半天

我给 WorkBuddy 接上 Obsidian,被 401 和中文乱码折腾了大半天

大家好,我是喻勇。

前两天,我盯着 WorkBuddy 左侧那排连接器看了一会儿,忽然觉得有件事挺别扭。

我每天都在 Obsidian 里记东西。技术知识、故障分析、公众号草稿、读书摘记,零零散散攒了不少。可到了 WorkBuddy 里,这些笔记跟不存在一样。每次聊到以前处理过的问题,我还得自己翻一遍,再把相关内容贴进去。

既然 WorkBuddy 支持 MCP,我就想给它接一个 Obsidian 连接器。动手前估了估,一两个小时应该够。后来这件事折腾了大半天。中间重启了好几次,有两回我已经准备收工,下一次调用又把我打了回来。

最后卡住我的,是两个很普通的问题。一个是 401 鉴权失败,一个是 Windows 管道里的中文编码。它们单独看都不复杂,叠上常驻进程、宿主配置和 Obsidian 的运行状态以后,排查起来就很会绕人。

我为什么选 Local REST API

Obsidian 接外部工具,常见做法有两种。

最省事的是把 vault 当成普通文件夹,MCP 服务器直接读写里面的 Markdown 文件。安装简单,也不用额外启动服务。日常需求只涉及读文件、写文件,这条路已经够用。

我想要的能力更多。除了读写笔记,我还想全文搜索、列标签、执行 Obsidian 命令、打开指定笔记,也希望以后能操作当前聚焦的笔记。于是我选了 Obsidian 的 Local REST API 社区插件。

插件启用以后,会在本机提供 HTTP 或 HTTPS 接口。我的配置走 HTTPS,使用 27124 端口,通过 API Key 鉴权。这样能调用的能力更完整,代价也很明确。Obsidian 必须保持运行,插件也得处于启用状态。程序退出,接口就跟着消失。

沙箱没网,我写了一个零依赖版本

连接器用 Python 写,MCP 走 stdio,消息采用 JSON-RPC 2.0。我原本准备直接安装 MCP SDK,结果运行环境在沙箱里,没有外网,依赖下载一直超时。

我索性用标准库实现了这次需要的最小协议子集。进程从 stdin 接收消息,完成初始化握手,再按工具名分发请求,最后把结果写回 stdout。服务器主体一百来行,不需要虚拟环境,也省掉了部署第三方依赖的麻烦。

这套做法适合能力范围明确、只在自己机器上用的小连接器。MCP 还包含能力协商、错误处理和协议演进等细节。要做成长期维护或分发给别人使用的产品,SDK 仍然更省心。我这次手写,是沙箱条件下的一次取舍。

握手和工具路由写完后,我先在命令行测试。手动读取mcp.json里的 Key,启动子进程,列目录、读标签都能返回。看到这里,我以为最麻烦的部分已经过去了。

第一个坑是 401

我从 WorkBuddy 的连接器入口发起了一次搜索,请求马上返回[HTTP 401] Authorization required

有个状态接口还能勉强返回,搜索接口的鉴权更严格,缺少有效的 Authorization 请求头就直接拒绝。顺着日志往前查,我发现 WorkBuddy 在这次启动 MCP 子进程时,没有把OBSIDIAN_API_KEY注入子进程环境。

我把配置读取顺序改了。服务器启动后,先读取~/.workbuddy/mcp.jsonobsidian-local-rest这一项,拿到 host、api_key 和 verify_tls。文件里缺少某个字段时,再去读环境变量。

def _load_cfg(): cfg = {"host": "", "api_key": "", "verify_tls": ""} try: mcp_path = os.path.expanduser("~/.workbuddy/mcp.json") with open(mcp_path, "r", encoding="utf-8") as f: data = json.load(f) env = ( data.get("mcpServers", {}) .get("obsidian-local-rest", {}) .get("env", {}) ) cfg["host"] = env.get("OBSIDIAN_HOST", "") cfg["api_key"] = env.get("OBSIDIAN_API_KEY", "") cfg["verify_tls"] = env.get("OBSIDIAN_VERIFY_TLS", "") except Exception as e: log(f"mcp.json load failed: {e}") if not cfg["host"]: cfg["host"] = os.environ.get("OBSIDIAN_HOST", "") if not cfg["api_key"]: cfg["api_key"] = os.environ.get("OBSIDIAN_API_KEY", "") if not cfg["verify_tls"]: cfg["verify_tls"] = os.environ.get("OBSIDIAN_VERIFY_TLS", "") return cfg

我测了三种情况。子进程没有任何相关环境变量,环境变量里放一个故意写错的 Key,以及环境变量与配置文件都正常。三次都按预期读取了mcp.json中的值,搜索接口不再返回 401。

这套优先级是按我的使用环境定的,因为当前这份mcp.json才是连接器配置的实际来源。换到别的宿主,环境变量或系统密钥存储可能更合适。API Key 既然写在本地文件里,就要限制文件权限,日志也只能记录配置来源和状态码,不能把 Key 原文打出来。

代码改完,我让 WorkBuddy 重新加载连接器。再搜一次,还是 401。

这一下很容易把人带回代码里继续查。我加了脱敏诊断日志,才看出新写的配置读取逻辑根本没有执行。WorkBuddy 在会话开始时已经拉起了一个常驻 MCP 进程,连接器界面里的关闭和开启,只让前端重新连接到原来的进程,没有重新启动服务器。

彻底退出 WorkBuddy,再打开,新的代码才真正加载。

这次经历让我多记了两项检查。宿主有没有把配置传给子进程,要在真实调用链里验证。服务器代码改动以后,也要确认旧进程已经退出。界面显示重新连接,不等于操作系统里的进程换过一轮。

第二个坑是中文全成了问号

401 解决后,我搜索了一次K8S。结果能返回,文件名却碎成了一串问号。原本的中文目录和笔记标题几乎没法辨认。

问题出在 stdio 两端对字符编码的理解不一致。服务器输出经过 Windows 文本层,宿主按 UTF-8 读取,中文就在管道中损坏了。

我先试了sys.stdout.reconfigure(encoding="utf-8")。手动启动时显示正常,换回 WorkBuddy 的实际启动路径,乱码仍然存在。仅靠调整 Python 文本包装层,没能把这条调用链里的编码约定统一起来。

最后我绕开文本层,直接向sys.stdout.buffer写 UTF-8 字节。

def send(obj): data = (json.dumps(obj, ensure_ascii=False) + "\n").encode("utf-8") try: sys.stdout.buffer.write(data) sys.stdout.buffer.flush() except Exception: sys.stdout.write(data.decode("utf-8", "replace")) sys.stdout.flush()

输入也做了同样处理。我从sys.stdin.buffer按行读取,再显式用 UTF-8 解码。这样请求中的中文搜索词和响应中的中文路径都走同一套编码,不再依赖 Windows 当前代码页。

重启 WorkBuddy 后,文件名终于完整显示出来。

04_文章/运维技术小记/待发表文章/03 K8s入门与提高/03 K8s入门与提高.md

我刚松口气,后面两次调用又报连接被拒绝。检查本机端口,27124 和 27123 都没有监听。查到这里,原因朴素得让人没脾气。Obsidian 当时没开。

把 Obsidian 启动起来,再搜一次,中文路径干干净净地回来了。

所以我现在排查这套连接器,顺序很固定。先看 Obsidian 和插件有没有运行,再看端口是否监听,随后看 HTTP 状态码,最后才查 MCP 进程和代码。这个顺序能省掉不少无用功。

接好以后,我每天怎么用

现在用起来很简单,我只管说正常的话。

想读笔记,就说"读一下07_日记/2026-08-08.md"。想找旧记录,就说"搜所有提到 K8S 调度的笔记"。刚处理完一次 OOM,也可以让它把排查结论追加到今天的日记末尾。

连接器背后放了十三个工具,覆盖列目录、读写文件、全文搜索、打开笔记、列标签、执行命令和操作焦点笔记。平时不用记这些工具的名字,WorkBuddy 会按需求选择。

接通以后,变化很直接。以前聊到 K8S,它只能根据当前对话和已有知识回答。我自己写过的排障过程,它看不到。现在它能先搜 vault,再把旧记录拿出来接着用。那些散在日记和技术笔记里的经验,终于进入了日常对话。

最后再记几句

这次折腾留下的代码不多,排查过程倒是很值钱。

401 出现时,先确认鉴权信息有没有沿着宿主、子进程和 HTTP 请求一路传下去。改完 MCP 服务器,要确认宿主启动的是新进程。Windows 上走 stdio,最好在协议边界明确使用 UTF-8 字节输入输出,别让系统代码页替你做决定。

还有最容易漏掉的一项。Obsidian Local REST API 依赖 Obsidian 进程和插件本身。Obsidian 关了,端口自然没人监听。遇到连接被拒绝,先把它打开,再考虑改代码。

服务器旁边那份脱敏日志我保留了下来。它只记录配置取自哪里、请求到了哪个接口、返回什么状态码。以后再出问题,我能先判断 Key 有没有读到,端口有没有响应,不必每次从头猜。

← 返回列表