Skip to content

Latest commit

 

History

History
315 lines (222 loc) · 8.9 KB

File metadata and controls

315 lines (222 loc) · 8.9 KB

基于 Local API 开发插件

语言版本: English

相关文档: 总览 · 快速开始 · 接口参考

目标场景

Local API 不只适合脚本,也适合“本地托管的插件前端”:

  • 文档编辑器加载项
  • Obsidian 辅助面板
  • 本地 Web 控制台
  • 自动化工具配置页
  • 任何需要在本机展示 UI、同时访问 Lattice 文献数据的插件

插件托管是什么

Lattice 除了 /api/v1/... 之外,还会暴露:

/plugins/{name}/...

这意味着你可以把一个插件当作“纯静态网站”部署到 Lattice 的插件目录里,然后由 Lattice 自己通过本地 HTTP 服务把它托管出来。

示例:

http://127.0.0.1:29467/plugins/my-plugin/index.html
http://127.0.0.1:29467/plugins/my-plugin/app.js
http://127.0.0.1:29467/plugins/my-plugin/assets/icon.png

为什么推荐这种方式

好处很直接:

  • 插件页面和 /api/v1 同源
  • 不需要额外起一个本地 Web 服务器
  • 不需要处理额外的跨域问题
  • 打包和分发方式简单
  • 更容易做成“用户装完就能跑”的桌面插件

插件文件放在哪里

对沙盒版 Lattice 来说,插件会被放进应用容器里的 Application Support 目录。典型路径如下:

~/Library/Containers/com.aurelian.Lattice/Data/Library/Application Support/Plugins/<plugin-name>/

部署完成后,对应访问地址就是:

http://127.0.0.1:<port>/plugins/<plugin-name>/...

建议的插件目录结构

Lattice 对插件内容没有强制的框架要求。你只需要准备静态资源即可。

一个简单插件可以长这样:

my-plugin/
  index.html
  app.js
  styles.css
  assets/
    icon.png

如果你的源码仓库里还保留了构建源文件、模板、脚本等,建议在安装阶段把“最终可直接访问的静态文件”复制到插件目录根部。

支持托管的常见静态资源类型

Local API 已经覆盖插件常用的资源类型,包括:

  • .html
  • .js
  • .css
  • .json
  • .png
  • .svg
  • .xml
  • .csl

因此一个带前端、样式、图标、CSL 模板的插件可以直接跑起来。

最小可用插件调用方式

如果插件页面本身就是从 /plugins/{name}/... 打开的,推荐始终使用相对路径调用 API:

async function requestJson(path, init = {}) {
  const headers = {
    Accept: "application/json",
    ...(init.headers ?? {})
  };

  const response = await fetch(`/api/v1${path}`, {
    ...init,
    method: init.method ?? "GET",
    headers
  });

  if (!response.ok) {
    const payload = await response.json().catch(() => null);
    throw new Error(payload?.error || `${response.status} ${response.statusText}`);
  }

  return await response.json();
}

export async function getBridgeStatus() {
  return requestJson("/status");
}

export async function searchLattice(query, limit = 10) {
  return requestJson(`/search?q=${encodeURIComponent(query ?? "")}&limit=${encodeURIComponent(String(limit))}`);
}

export async function fetchPaperSnapshot(id) {
  return requestJson(`/papers/${encodeURIComponent(id)}`);
}

export async function getCollections() {
  return requestJson("/collections");
}

export async function getTags() {
  return requestJson("/tags");
}

export async function lookupMetadata(params) {
  const query = new URLSearchParams();
  for (const [key, value] of Object.entries(params ?? {})) {
    if (value) {
      query.set(key, value);
    }
  }
  return requestJson(`/metadata?${query.toString()}`);
}

export async function createPaper(body) {
  return requestJson("/papers", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body)
  });
}

export async function uploadPaperPdf(id, pdfBytes, { force = false } = {}) {
  const suffix = force ? "?force=true" : "";
  const response = await fetch(`/api/v1/papers/${encodeURIComponent(id)}/pdf${suffix}`, {
    method: "PUT",
    headers: { "Content-Type": "application/pdf" },
    body: pdfBytes
  });

  if (!response.ok) {
    const payload = await response.json().catch(() => null);
    throw new Error(payload?.error || payload?.reason || `${response.status} ${response.statusText}`);
  }

  return await response.json();
}

这也是推荐的标准接入方式。

处理 capability-gated 写入能力

不要假设所有写接口在所有用户机器上都可用。

推荐模式:

  1. 先调 /api/v1/status
  2. 检查 capabilities
  3. 只有当其中包含 create-paper 时,才展示创建 UI 或发送创建请求
  4. 只有当其中包含 pdf-upload 时,才展示原始 PDF 字节上传 UI
  5. 如果你依赖重复策略与更丰富的成功响应,检查 create-paper-v2

示例:

export async function getCapabilities() {
  const status = await getBridgeStatus();
  return new Set(status.capabilities ?? []);
}

export async function canCreatePapers() {
  return (await getCapabilities()).has("create-paper");
}

export async function canUploadPdfBytes() {
  return (await getCapabilities()).has("pdf-upload");
}

用服务端数据驱动选择器与预填表单

对于写入型插件,公开 Local API 已经暴露了常用基础能力:

  • /api/v1/collections 填充文献集选择器
  • /api/v1/tags 填充标签选择器或自动补全
  • /api/v1/metadata,传入 doiarxivIdisbntitle 之一,先预填创建或替换表单

推荐开发流程

1. 先把 API 用通

先用 curl 或浏览器验证:

curl http://127.0.0.1:29467/api/v1/status

2. 本地开发你的前端

你可以用任意技术栈:

  • 原生 HTML/CSS/JS
  • React / Vue / Svelte 构建后的静态产物
  • Office Add-in 前端
  • 其他可以产出静态文件的方案

3. 构建并复制到插件目录

把最终产物放到:

.../Application Support/Plugins/<plugin-name>/

4. 通过插件 URL 打开

http://127.0.0.1:29467/plugins/<plugin-name>/index.html

5. 如果宿主程序需要注册入口,再生成它自己的 manifest / loader

例如:

  • 某些宿主需要 manifest 或注册文件
  • 某些桌面工具可能需要自定义协议或 WebView 入口

开发本地外部扩展时的推荐架构

一个非常稳的模式是:

  1. UI 页面由 Lattice 托管在 /plugins/<name>/...
  2. 页面通过相对路径访问 /api/v1/...
  3. 宿主程序只负责打开该页面
  4. 插件内部自己缓存必要的文献详情与样式配置

这种模式的优点是耦合简单:

  • Lattice 只负责本地数据与静态资源托管
  • 插件自己负责 UI、状态、渲染和宿主交互

理解数据边界

当前读接口是面向引用场景设计的,返回的数据刻意比 Lattice 内部完整文献模型更小。

读接口当前不会暴露:

  • abstract
  • collections
  • tags
  • pdfPath
  • pdfURL
  • latticeURL

这带来几个直接结论:

  • 如果插件只需要在 Lattice 中打开一篇文献,可以根据 id 自行拼出 lattice://paper/{id}
  • 如果插件需要真实的文件系统 PDF 路径,当前 Local API 不提供
  • pdfPath 目前是 POST /api/v1/papers 接受的输入字段,不是 GET /api/v1/papers/{id} 返回的字段
  • 原始 PDF 字节上传是单独的 PUT /api/v1/papers/{id}/pdf 流程,并由 pdf-upload capability 控制,不属于文献详情负载本身

不建议的方式

远程 Web 站点直接调用 Local API

默认不适合。Local API 设计成“本地来源、本地集成”的模型,不是给远程网页公开调用的。

假设写入接口始终可用

不适合。写入能力是可选的,用户必须显式关闭 Read-Only Mode,客户端也应该以 /status.capabilities 为准做能力判断。

把读接口当成完整文献序列化接口

也不适合。当前 Local API 仍然以引用、搜索、选文献和受控创建文献为核心目标。

调试建议

  • 先用 /api/v1/status 判断是不是连通性问题
  • 搜索不符合预期时,先直接请求 /api/v1/search
  • 插件页面打不开时,先检查插件资源是否真的复制到了插件目录
  • 静态页面能打开但 API 失败时,优先检查当前端口是否和 Lattice 设置页一致
  • 如果 POST /api/v1/papers 返回 403,优先检查用户是否已关闭 Read-Only Mode
  • 如果创建成功但 pdfAttachedfalse,查看返回的 warnings 数组里是否有 Trusted Folders 或 PDF 校验失败原因
  • 如果 PUT /api/v1/papers/{id}/pdf 返回 409,决定你的 UI 是否应使用 ?force=true 重试
  • 如果 PUT /api/v1/papers/{id}/pdf 返回 503,先让用户配置可用的 PDF 上传目标
  • 如果宿主程序缓存了入口地址,修改端口后通常需要重新注册或重新安装插件

相关文档