Skip to content

本地网络服务 + vixel:// 深链接 + MCP(HTTP 与 stdio):搜索结果可链接进笔记、AI 客户端可查询 - #6

Merged
RGB-loop merged 5 commits into
mainfrom
feat-local-api-mcp
Oct 17, 2026
Merged

RGB-loop merged 5 commits into
mainfrom
feat-local-api-mcp

Conversation

@RGB-loop

Copy link
Copy Markdown
Owner

Closes #3(客户端版本增加本地网络服务,把搜索到的文件链接集成到笔记里),并给 AI 客户端(MCP)提供同一套只读查询。三个里程碑各自独立可用、按序提交。

1. 深链接(无需任何网络服务)

  • 新链接形态 vixel://open?hash=<内容哈希>&id=<photoId>(图片)/ vixel://open?hash=…&video=<videoId>&t=<秒>(音视频),构建与解析在 src/shared/deeplink.ts(纯函数 + 单测)。
  • 链接活过重建索引:定位以内容哈希(xxHash64)优先 —— 清库重建、移动文件、换文件夹后 id 都会变,哈希只随内容变;id 只作内容已变 / 旧链接的兜底。
  • 与内部协议不冲突:host 固定 open。protocol.handle('vixel') 只响应渲染进程发起的 thumb/image/media/sprite/face 请求;OS 转交的外部链接走 open-url / argv,两条路径互不相见,所以沿用 vixel:// 而不是再立一个 scheme(笔记里的链接长得和应用内资源一致,也少占一个系统 scheme)。
  • OS 注册:app.setAsDefaultProtocolClient + electron-builder protocols(打包版 Info.plist / 注册表);macOS open-url(含 app ready 之前的冷启动队列),Windows/Linux 单实例锁 + second-instance argv 转交(拿不到锁的新实例递完链接即退出)。开发期 scripts/dev-bundle.mjs 顺手给 Electron.app 写 CFBundleURLTypes,npm run dev 下也能点链接。
  • 命中后复用快速搜索面板的"在主窗口打开结果"通道(含主窗口未建 / 未加载完的暂存),视频 / 音频直接落到详情并 seek:t 恰为片段起点("拷贝链接"的产物)时用整段(时间轴整段高亮),任意时间点则精确 seek。链接指向的内容不在库里 → 聚焦主窗口 + 双语说明对话框。
  • UI:右键菜单与检查器新增「拷贝链接」「拷贝 Markdown 链接」([文件名 @ 0:42](vixel://…)),网页版隐藏(等价物是下面的 http 链接)。

2. 本地接口(桌面内嵌服务器栈)+ /mcp

  • src/server/index.ts 的 HTTP 层抽成 src/server/app.ts 的 createServerApp(services, 配置):认证、/api/*、/media/*、网页前端、WebSocket 原样搬移,服务器版与桌面共用,无复制代码。行为未变:scripts/server-security.mjs 16 项全过。
  • 设置 › 通用 › 本地接口(默认关闭):开启后在 Electron 主进程里跑同一套栈、复用桌面正在跑的 services/库实例。默认只监听 127.0.0.1:41721(端口可配);「允许局域网访问」绑定 0.0.0.0 且强制要求口令(没口令拒绝开启,开启后本机也要登录)—— 手机浏览器即可访问桌面资料库(设置页列出各网卡地址)。dist/web 进打包 extraResources(prepackage 构建)。
  • /mcp(桌面与 Docker 同一实现):MCP Streamable HTTP(规范 2025-06-18)最小合规实现 —— POST JSON-RPC、通知 202、GET 405(无状态服务器不开 SSE 流,规范允许)。认证 Bearer 令牌(设置页生成/拷贝/重新生成;Docker 用 VIXEL_MCP_TOKEN,≥16 位),常数时间比较 + Origin 校验(防 DNS rebinding),与会话 cookie 无关、天然免 CSRF。
  • 不用官方 SDK 的原因:@modelcontextprotocol/sdk 是 ESM-only 且带运输层依赖,与 electron-vite 主进程包和 CommonJS 的 tsconfig.server/cli 都不合;我们只需要无状态 initialize/tools,自写核心约 120 行、全部只读、可审计(src/core/mcp/)。
  • 工具(只读,三个宿主共用):search_media(四路融合搜索;返回 id、name、源文件 path、type、score、matchedBy、命中片段时间段、vixel:// 深链接、http 媒体链接)、get_media(详情含 OCR 文字、标签、人物、片段表)、find_similar。不暴露任何写操作。
  • 安全小结:默认关;/api 的 CSRF 头与登录限速原样保留;LAN 必须口令;/mcp 必须令牌;服务只监听、零出站请求,渲染进程离线白名单不受影响。设置页注明 MCP 结果会发给所连接的 AI 客户端。

3. vixel mcp(stdio)

  • src/cli/mcp.ts:stdin 每行一条 JSON-RPC、响应写 stdout、日志全走 stderr(mcp 命令下 console.log 重定向),库按 CLI 惯例只读打开,语义模型尽力加载(没有也能按 OCR/文件名/标签搜)。
  • Claude Desktop / Cursor 配置片段写进 README(zh + en):以仓库自带 Electron 为运行时 + ELECTRON_RUN_AS_NODE=1(better-sqlite3 按 Electron ABI 构建,普通 node 加载不了;该变量下 Electron 即纯 Node、不弹窗)。打包安装器不含 CLI,文档注明需要源码 checkout(已安装 Vixel.app 的模型会被自动找到)。

验证(均实测)

  • npm run typecheck、npm test(385 项,新增 25 项:链接构建/解析、MCP 协议核心、HTTP 传输令牌/Origin/批量/通知)、npm run test:db、npx tsc --noEmit -p tsconfig.server.json、tsconfig.cli.json 全过;npm run test:server:security 16/16(锁定重构后行为不变)。
  • 真机(demo 库拷贝到临时目录 + --user-data-dir):
    • 本地接口开启后 curl /api/search(带 X-Vixel)返回瀑布视频;不带头 403;LAN 无口令拒绝启动(日志),设口令后绑定 0.0.0.0、未登录 401、登录后正常、/login 页正常。
    • /mcp:无令牌 401;initialize → 协议版本/能力正确;notifications/initialized → 202;tools/list → 三个工具;tools/call search_media("瀑布") → waterfall.mp4,含时间段 0–10.36s、vixel://open?hash=…&video=2、httpUrl。
    • stdio:脚本化 JSON-RPC 四连(initialize/initialized/tools/list/tools/call)输出与 HTTP 一致。
    • 深链接:open 'vixel://open?hash=…&video=2&t=8' → 主进程日志 [deeplink] opening …,CDP 查渲染进程确认 <video src="vixel://media/2"> 已按 t seek(3 秒后 currentTime=10.36=片尾,自 0 播放只会到 ~3s);hash-only 图片链接同样导航;伪造 hash → "不在库中"对话框路径命中。

已知边界

  • macOS 开发机上若有多个同 bundle id 的 Electron.app,open vixel:// 可能被路由到别的 Electron(LaunchServices 按 bundle id 挑);打包版 bundle id 唯一,不受影响。
  • /mcp 的 http 媒体链接在浏览器里打开仍需会话登录(LAN 模式);MCP 客户端一般只用 link(深链接)。
  • 服务器版 serverInfo.version 读仓库 package.json;桌面未打包、直接 electron out/... 跑时 app.getVersion() 是 Electron 版本(dev/打包版正常)。

…注册,hash 定位活过重建索引

- shared/deeplink.ts:构建/解析纯函数(hash 优先,id 兜底;t 秒级时间点),单元测试
- main/deeplink.ts:macOS open-url(含冷启动队列)+ Windows/Linux 单实例锁 second-instance argv
- 解析命中复用快速面板的"在主窗口打开结果"通道(含未加载完的暂存);时间点落到包含它的已索引片段
- 右键菜单 / 检查器新增"拷贝链接""拷贝 Markdown 链接"(网页版隐藏)
- electron-builder protocols + dev-bundle.mjs 给开发期 Electron.app 写 CFBundleURLTypes
- src/server/app.ts:把 HTTP 应用从 server/index.ts 抽成 createServerApp(services, 配置),
  认证/API/媒体/网页/WS 原样搬移(scripts/server-security.mjs 16 项全过,行为未变)
- src/core/mcp/*:无依赖的 MCP 核心(JSON-RPC + initialize/tools),三个只读工具
  search_media / get_media / find_similar(结果带 vixel:// 深链接与 http 媒体链接),单元测试
- src/server/mcp.ts:Streamable HTTP 传输(POST JSON、通知 202、GET 405),Bearer 常数时间
  比较 + Origin 校验(防 DNS rebinding),单元测试
- 服务器版:VIXEL_MCP_TOKEN(≥16 位)开启 /mcp,docs/server/README.md 中英文档
- 桌面:设置 › 通用 › 本地接口(默认关、127.0.0.1、端口可配;允许局域网访问需口令、绑定 0.0.0.0),
  MCP 令牌生成/拷贝/重新生成;LAN 地址列表;i18n zh/en
- 打包:dist/web 进 extraResources(prepackage 构建网页前端)
- src/cli/mcp.ts:stdin 每行一条 JSON-RPC,响应写 stdout;日志全走 stderr(mcp 命令下
  console.log 重定向),库按 CLI 惯例只读打开,工具与 /mcp 完全相同(core/mcp 共用)
- README zh/en:Claude Desktop / Cursor 配置片段(Electron 作运行时 + ELECTRON_RUN_AS_NODE=1,
  解释 better-sqlite3 ABI 的原因),新特性条目;CHANGELOG Unreleased
实测桌面全链路后修正两处:
- core/mcp/tools.ts:音视频的 photo 行是代表帧,path/name 改查 videos 表给源文件
- main/index.ts:t 恰为片段起点(拷贝链接的产物)才用整段(时间轴整段高亮),
  其余时间点起点保持精确,播放器按 t seek(之前被吸附到片段起点,t 丢失)
- deeplink 成功打开记一条日志;localapi 无口令拒开 LAN 时记警告
…binding)

The desktop local API binds 127.0.0.1 without a password and relied on the X-Vixel
header against CSRF. A page that rebinds its own domain to 127.0.0.1 is same-origin,
can send that header and would read the whole library. Without a password only accept
Host localhost / 127.0.0.1 / [::1] (HTTP and WebSocket). Verified live: normal request
200, forged Host 403 for /api and /media.
@RGB-loop
RGB-loop merged commit c74976e into main Oct 17, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

图片文字OCR的问题,以及能否支持局域网服务

1 participant