开发者指南

插件开发文档

为 AnBoWork 桌面客户端开发插件:给 AI 增加新工具、在会话旁边挂一块自己的面板、发系统通知、解析新的文件格式。一个 manifest.json 加一个入口脚本就能跑起来。

01概览

AnBoWork 插件是一个普通的 Node.js 程序。安装后,客户端把它 fork 成一个独立的子进程运行(纯 Node 环境,没有 Electron API),通过进程间消息跟宿主通信。插件不需要依赖任何 SDK 包,只要会 process.on('message') 和 process.send()。

插件“有哪些能力”全部写在 manifest.json 里,是静态声明:安装前的知情同意页、市场审核,都不需要运行插件代码就能知道它会装哪些工具、要什么权限。所有 UI(顶栏图标、设置项、命令面板、右键菜单)都由宿主按声明统一渲染,插件代码不会跑进主窗口。只有停靠视图和浮动面板是插件自己的 HTML,也运行在独立沙箱页面里。

能力一览

manifest 字段能力用户在哪里看到
tools给 AI 增加可调用的工具AI 在对话中自动调用;工具调用记录显示在会话里
views停靠在会话右侧栏的常驻视图会话顶栏多一个图标,点开在右侧展示你的页面
ui独立浮动窗口插件列表的“⋯”菜单里打开
sessionAction会话顶栏常驻操作图标点一下调用你指定的工具
configuration设置页的配置项设置 → 插件配置,值随每次调用传给你
commandsCtrl/Cmd+K 命令面板条目命令面板
keybindings全局快捷键按键触发某条命令
menus会话右键菜单项会话列表行的“⋯”/右键菜单
documentParsers解析拖进输入框的文件用户拖入对应格式文件时自动调用
activationEvents随应用启动拉起无感知;用于需要后台定时器的插件
permissions声明需要的权限安装前的知情同意页

02快速开始

一个最小插件只需要两个文件。

my-plugin/
├── manifest.json   # plugin manifest (required)
├── index.js        # entry script, referenced by manifest.main (required)
├── view.html       # docked view page (optional, manifest.views[].entry)
└── ui.html         # floating panel page (optional, manifest.ui.entry)

1. 写 manifest.json

{
  "id": "com.example.hello",
  "name": "Hello Plugin",
  "version": "1.0.0",
  "description": "Say hello to the user.",
  "main": "index.js",
  "permissions": [],
  "tools": [
    {
      "name": "say_hello",
      "summary": "Greet the user by name",
      "description": "Return a greeting for the given name. Call this when the user explicitly asks the plugin to say hello.",
      "parameters": {
        "type": "object",
        "properties": {
          "name": { "type": "string", "description": "Who to greet." }
        },
        "required": ["name"]
      }
    }
  ]
}

2. 写入口脚本

// The entry script runs in a standalone Node.js child process (ELECTRON_RUN_AS_NODE=1).
// Only Node built-ins and dependencies shipped inside the plugin folder are available;
// there is no Electron API.
process.on('message', (msg) => {
  if (!msg || msg.type !== 'call_tool') return;
  const { requestId, toolName, params } = msg;
  try {
    if (toolName !== 'say_hello') throw new Error(`unknown tool: ${toolName}`);
    process.send({
      type: 'tool_result',
      requestId,
      content: [{ type: 'text', text: `Hello, ${params.name}!` }],
    });
  } catch (err) {
    process.send({ type: 'tool_error', requestId, message: err.message });
  }
});

3. 安装到客户端

打开 AnBoWork → 顶部「工具」→「插件」→ 右上角「本地安装」,选择插件目录(或打成 zip 后选 zip 文件)。安装前会弹出知情同意页,列出这个插件声明的权限和全部工具,确认后启用。

4. 试一下

在任意会话里对 AI 说“让插件跟小明打个招呼”,AI 会根据工具描述决定调用 say_hello,会话里会显示这次调用和返回结果。

提示
AnBoWork 里的 AI 本身也能替你生成插件(对它说“帮我写一个 xxx 插件”,它会调用 create_local_plugin),生成的插件默认禁用,需要你在插件页看过代码后手动启用。这份文档同样适用于阅读和修改 AI 生成的插件。

03manifest 字段

字段类型必填说明
idstring是反向域名风格,如 com.example.my-plugin。只允许字母、数字、点、短横线。创建后不可改,安装同 id 视为覆盖重装。
namestring是插件名。这个字段和 description 会作为上下文喂给模型,建议用英文。
versionstring是语义化版本,市场按它判断更新。
descriptionstring是一句话说明插件做什么(英文)。
authorstring否作者。
readmestring否市场详情页展示的长文说明,支持 Markdown。图片只能用 https 链接,不支持引用 zip 内的相对路径图片。
iconstring否市场列表/详情页展示的图标,填 zip 内一个相对路径(如 icon.png)。必须是 PNG,不支持 SVG,尺寸至少 128×128。
mainstring是入口脚本,相对插件目录的路径,不允许 .. 跳出目录。
permissionsstring[]是network / filesystem / notifications,可为空数组。见「权限模型」。
toolsobject[]是工具声明,可为空数组。见「工具与调用协议」。
documentParsersobject[]否文件解析能力。
uiobject否浮动面板:{ entry, title }
viewsobject[]否停靠视图:{ id, title, icon, entry, width? }
sessionActionobject否顶栏图标,每个插件最多一个。
configurationobject[]否设置项列表。
commandsobject[]否命令面板条目。
keybindingsobject[]否快捷键,command 必须引用 commands[] 里某条的 tool,否则安装被拒绝。
menusobject否目前只开放 session/context 一个位置。
activationEventsstring[]否目前只有 onStartupFinished。

文案语言约定

manifest 里的文本分两类受众。给模型看的:name、description、tools[].description、tools[].parameters.*.description,请用英文,模型对英文工具描述的理解和参数填写更稳定。给人看的:tools[].summary(知情同意页的一句话摘要)、ui.title、views[].title、sessionAction.tooltip、configuration[].label、commands[].title、menus 里的 title,用你的目标用户的语言。

{
  "id": "com.example.full",
  "name": "Full Example",
  "version": "1.2.0",
  "description": "Every contribution point in one manifest.",
  "author": "Example Inc.",
  "readme": "## Full Example\n\nLonger Markdown shown on the marketplace detail page. Images must be https URLs.",
  "icon": "icon.png",
  "main": "index.js",
  "permissions": ["filesystem", "network", "notifications"],
  "activationEvents": ["onStartupFinished"],
  "tools": [
    {
      "name": "do_something",
      "summary": "Do one thing to the current project",
      "description": "…English. State clearly when to call this and when not to…",
      "parameters": { "type": "object", "properties": {}, "required": [] },
      "renderAs": "checklist"
    },
    { "name": "is_available", "summary": "Whether the current folder applies", "description": "…", "parameters": { "type": "object", "properties": {} } },
    { "name": "list_modes", "summary": "List selectable modes", "description": "…", "parameters": { "type": "object", "properties": {} } }
  ],
  "documentParsers": [
    { "mimeType": "text/markdown", "toolName": "parse_markdown" }
  ],
  "ui": { "entry": "ui.html", "title": "Example Panel" },
  "views": [
    { "id": "board", "title": "Example Board", "icon": "checklist", "entry": "view.html", "width": 380 }
  ],
  "sessionAction": {
    "tool": "do_something",
    "icon": "git-branch",
    "tooltip": "Do one thing to the current project",
    "visibleTool": "is_available"
  },
  "configuration": [
    { "id": "mode", "label": "Mode", "type": "select", "optionsTool": "list_modes", "defaultValue": "auto" },
    { "id": "verbose", "label": "Verbose output", "type": "boolean", "defaultValue": false },
    { "id": "prefix", "label": "Prefix", "type": "string", "defaultValue": "" },
    { "id": "limit", "label": "Max items", "type": "number", "defaultValue": 20 }
  ],
  "commands": [
    { "tool": "do_something", "icon": "git-branch", "title": "Do one thing to the current project" }
  ],
  "keybindings": [
    { "command": "do_something", "key": "mod+shift+d" }
  ],
  "menus": {
    "session/context": [
      { "tool": "do_something", "icon": "git-branch", "title": "Do one thing to this session" }
    ]
  }
}

04权限模型

permissions 必须如实反映代码实际做的事。安装前的知情同意页会逐条展示,用户据此决定要不要装;市场审核也会对照代码检查声明是否属实。

权限什么时候必须声明宿主如何对待
filesystem读写用户文件(读项目代码、生成文档、在用户目录存数据)知情同意,向用户展示。
network发起任何网络请求(调第三方 API、下载资源)知情同意,向用户展示。
notifications通过反向 RPC show_notification 弹系统通知硬校验:没有声明就调用,宿主直接返回 host_error,通知不会弹出。
注意
插件子进程是完整的 Node.js 环境,filesystem 和 network 目前是知情同意层面的声明,不是运行时沙箱:宿主不会拦截 fs 或 fetch。这正是为什么声明必须诚实——漏报权限的插件在市场审核时会被拒绝,被用户发现后会失去信任。不要多报(用不到的权限会让用户犹豫),也不要少报。

隐私与数据

context.cwd 是用户的项目目录,里面可能有源码和敏感配置;工具参数里可能有用户的对话内容。除非功能必需,不要把这些数据发到网络;需要发送时在 description 和 summary 里说清楚发给谁、发什么。插件自己的持久化数据建议放在用户主目录下一个以插件 id 命名的文件里,卸载时用户能一眼找到。

05工具与调用协议

声明

字段说明
name工具名,小写字母加下划线,在整个客户端里要唯一(会和内置工具、其它插件的工具一起注册给模型)。
description给模型的说明(英文)。写清楚三件事:这个工具做什么、用户说什么话时应该调用、什么情况下不要调用。这是决定 AI 会不会用你插件的最关键字段。
summary给人看的一句话摘要,知情同意页展示。可选但强烈建议提供,否则会退回展示裁剪过的 description。
parameters标准 JSON Schema 对象,每个属性都写 description。模型按它填参数,宿主原样传给你,不做额外校验,代码里要自己检查必填项。
renderAs可选,目前只有 checklist:宿主把这次调用参数里的 todos 数组渲染成清单卡片,见下方。
注意
模型能调用的工具只来自这里声明的 tools[]——sessionAction.tool、commands[].tool、menus 里的 tool 字段只在安装时校验是否为非空字符串,不会校验这个名字有没有在 tools[] 里声明过,点击/触发时宿主照样按这个名字发起调用。所以一个纯 UI 触发的按钮可以只在你自己的 index.js 里实现处理逻辑、完全不出现在 tools[] 里——但这样 AI 永远看不到也调用不了它。想让同一个功能既能点击触发、也能被 AI 在对话里直接调用,就把这个名字也声明进 tools[],两条入口共用同一份处理逻辑;确定只要 UI 触发、不希望 AI 插手,不声明即可,不用为了“齐全”硬加一条。

消息协议

宿主发 call_tool,插件必须用同一个 requestId 回一条 tool_result 或 tool_error。单次调用 30 秒内没有回应会被判为超时,模型收到错误。

// host → plugin
{
  "type": "call_tool",
  "requestId": "0b2d…",          // unique per call, echo it back unchanged
  "toolName": "do_something",
  "params": { "name": "…" },     // arguments validated against tools[].parameters
  "context": {
    "cwd": "/Users/me/project",  // project folder of the calling session; a temp folder for chat sessions; empty string for document parsing
    "config": { "mode": "auto", "verbose": false }   // present only when configuration is declared; keys are configuration[].id
  }
}

content 只认 text 和 image 两种块(image 用 base64 的 data 加 mimeType),其它类型会被丢弃。details 是任意结构化数据,不会喂给模型,用于宿主 UI 渲染和你自己的停靠视图。

产物是文件:文件卡片

// When the result is a file: include the absolute path and file name. The session
// renders a clickable file card that previews in-app (docx/xlsx/pdf/markdown/code…).
process.send({
  type: 'tool_result',
  requestId,
  content: [{ type: 'text', text: `Generated [${fileName}](${filePath})` }],
  details: { filePath, fileName },
});

清单渲染:renderAs checklist

// For tools with renderAs: "checklist", the host renders the todos array from the call arguments:
{
  "todos": [
    { "id": "1", "content": "Initialize repository", "status": "completed" },
    { "id": "2", "content": "Write unit tests", "status": "in_progress" },
    { "id": "3", "content": "Publish", "status": "pending" }
  ]
}

写好 description 的要点

用祈使句描述触发条件(“Call this when the user asks to…”),明确排除项(“Do not call this for…”)。如果产物是文件,要求模型在回复里引用返回的文件链接、不要凭空声称已生成。如果参数会被转发给外部生成类 API,说明参数要用什么语言。避免只写“manage todos”这类一句话,模型会不知道什么时候该用。

06停靠视图 views

停靠视图是一张挂在会话右侧栏的常驻页面:会话顶栏多出一个图标,点开后你的 HTML 显示在右侧,切换会话、收起再展开都不会丢失页面状态(滚动位置、草稿),跟 VS Code 侧栏视图同一个语义。

字段说明
id插件内唯一。
title图标的提示文字(人类可读)。
icon从宿主白名单里选:git-branch、checklist。保持全应用图标风格统一,不接受自带图片。
entryHTML 文件,相对插件目录。
width可选,面板默认宽度(像素),240 到 640,默认 360。用户之后可以拖动调整并被记住。

页面运行环境

页面在沙箱里运行(sandbox + contextIsolation,没有 Node、没有宿主应用的任何数据),只能通过 window.pluginHost 这几个方法跟自己的插件进程通信。样式自己写,建议按下面示例跟随应用的亮/暗色。

interface PluginHost {
  // Call a tool declared in this plugin's manifest.tools; same call_tool channel the AI uses
  invoke(toolName: string, params: unknown): Promise<
    { content: unknown[]; details?: unknown } | { error: string }
  >;
  // Subscribe to ui_push messages from the plugin process; returns an unsubscribe function
  onPush(callback: (channel: string, payload: unknown) => void): () => void;
  // Current light/dark scheme and subsequent changes
  getColorScheme(): Promise<'light' | 'dark'>;
  onColorSchemeChange(callback: (scheme: 'light' | 'dark') => void): () => void;
}
declare const pluginHost: PluginHost; // exposed as window.pluginHost

推荐模式:页面首次加载用 invoke() 调自己的一个查询工具,并让工具在 details 里返回结构化数据;之后每次数据变化由插件进程 ui_push 推送,页面不轮询。AI 在对话里改了数据,面板会即时刷新。

07浮动面板 ui

ui: { entry, title } 声明一个独立的浮动窗口(900×700),用户在插件列表的“⋯”菜单里打开,关掉即销毁。页面环境和 window.pluginHost API 跟停靠视图完全一样,ui_push 也会同时送到浮动面板和停靠视图。适合“偶尔打开看一眼”的大面板;需要常驻在会话旁边的内容用 views。

08会话顶栏图标 sessionAction

每个插件最多一个常驻图标,点击时用空参数调用 tool,所以那个工具必须在没有参数时也能有意义地执行。

字段说明
tool点击调用的工具名。
icon白名单图标名。
tooltip悬停提示。
visibleTool可选。给了就在渲染前先用空参数调用它,按返回的 details.visible(布尔)决定要不要显示图标。适合“当前目录是不是 git 仓库”这类依赖运行时状态的判断。
// The host calls visibleTool with empty params and shows the icon based on details.visible
process.send({
  type: 'tool_result',
  requestId,
  content: [{ type: 'text', text: isGitRepo ? 'git repo' : 'not a git repo' }],
  details: { visible: isGitRepo },
});

09设置项 configuration

声明后出现在设置页的插件配置里。用户改的值由宿主保存,每次 call_tool 都会以 context.config 传给你(键是 id,没改过的用 defaultValue),不管这次调用来自 AI、顶栏图标还是你的面板,你不需要自己去问。

type渲染成defaultValue 类型
select单选列表,候选项来自 optionsToolstring
boolean开关boolean
string文本输入框,失焦提交string
number数字输入框number
// For type: "select" settings, the host calls optionsTool with empty params to fetch options
process.send({
  type: 'tool_result',
  requestId,
  content: [{ type: 'text', text: '2 options' }],
  details: {
    options: [
      { value: 'fast', label: 'Fast' },
      { value: 'accurate', label: 'Accurate' },
    ],
  },
});
// The host automatically prepends an "Auto" option whose value equals this
// configuration entry's defaultValue; the plugin must not return it again.

10命令面板 / 快捷键 / 右键菜单

三者都是“用空参数调用某个工具”的不同入口,一个插件可以声明多条。

字段形状说明
commands{ tool, icon, title }[]出现在 Ctrl/Cmd+K 命令面板,用户搜索后触发。
keybindings{ command, key }[]command 必须等于 commands[] 里某条的 tool。key 用 mod+shift+g 这种写法,mod 在 macOS 是 Cmd、其它平台是 Ctrl。跟其它快捷键冲突不做静态检查,选不常用的组合。
menus{ "session/context": { tool, icon, title }[] }挂进会话列表行的菜单。目前只开放这一个位置。

11文档解析 documentParsers

用户把文件拖进输入框时,宿主按 MIME 类型找已启用插件里声明的解析器,把文件内容 base64 传给对应的 toolName。这不是 AI 决定调用的工具,所以不要把解析工具也写进 tools,否则会被当成 AI 可调用工具暴露出去。

// The toolName declared in documentParsers is called when the user drops a file into the input:
// params = { base64: "<file content as base64>" }
// Return: details must be { success: true, text } or { success: false, reason }
process.on('message', (msg) => {
  if (msg.type !== 'call_tool' || msg.toolName !== 'parse_markdown') return;
  const text = Buffer.from(msg.params.base64, 'base64').toString('utf8');
  process.send({
    type: 'tool_result',
    requestId: msg.requestId,
    content: [{ type: 'text', text: 'parsed' }],
    details: { success: true, text },
  });
});

12反向 RPC host_request

插件进程没有 Electron API,需要“真实系统能力”时向宿主发 host_request,宿主回 host_response 或 host_error。这条通道和 call_tool 完全独立,可以在处理一次工具调用的过程中发起,也可以在定时器里发起。

capabilityparamsresult前提
show_notification{ title, body?, viewId? }{ shown: true } / { shown: false, reason: "disabled" | "unsupported" }声明 notifications 权限;用户在设置里关闭桌面通知时返回 disabled 而不是报错。viewId 指向自己的停靠视图时,点击通知会把应用拉到前台并展开该视图。
render_pdf{ outputPath, title, content }{}content 是 Markdown,宿主用内置渲染器生成 PDF 到 outputPath。
const crypto = require('crypto');
const pending = new Map();

function requestHost(capability, params) {
  const requestId = crypto.randomUUID();
  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => {
      pending.delete(requestId);
      reject(new Error(`host capability "${capability}" timed out`));
    }, 30_000);
    pending.set(requestId, { resolve, reject, timer });
    process.send({ type: 'host_request', requestId, capability, params });
  });
}

process.on('message', (msg) => {
  if (msg.type === 'host_response' || msg.type === 'host_error') {
    const p = pending.get(msg.requestId);
    if (!p) return;
    pending.delete(msg.requestId);
    clearTimeout(p.timer);
    if (msg.type === 'host_error') p.reject(new Error(msg.message));
    else p.resolve(msg.result);
  }
});

// usage
await requestHost('show_notification', {
  title: '3 todos for today',
  body: '• Write weekly report\n• Reply to emails',
  viewId: 'board',      // optional: expand this docked view when the notification is clicked
});
// => { shown: true } or { shown: false, reason: 'disabled' | 'unsupported' }

await requestHost('render_pdf', {
  outputPath: '/abs/path/report.pdf',
  title: 'Report',
  content: '# Title\n\nBody (Markdown)',
});

13生命周期

阶段行为
启动默认懒启动:应用启动时不拉起,第一次有人调用你的工具(AI、顶栏图标、面板、命令、文档解析)时才 fork。声明 activationEvents: ["onStartupFinished"] 的插件在应用启动完成后立即拉起,给需要后台定时器(到点提醒、轮询)的插件用;普通工具型插件不要声明。
常驻进程启动后一直存活,跨多次工具调用复用,可以在内存里缓存状态,但要假设随时可能被重启。
崩溃进程意外退出时宿主自动重启:立即一次,再等 5 秒、15 秒,连续 3 次失败后标记为“崩溃”不再重试,用户在插件页能看到状态。进行中的调用全部以错误返回。
禁用 / 卸载 / 重装进程被终止,停靠视图和浮动面板销毁;重装是整个目录推倒重来。
日志你的 stdout/stderr 会带着插件 id 前缀写进应用日志文件。用 console.log 调试即可,见「调试与常见问题」。
环境
子进程以 ELECTRON_RUN_AS_NODE=1 启动,cwd 是插件安装目录,Node 版本跟客户端内置的 Electron 一致。不要依赖全局安装的任何包,也不要假设有 npx。

14打包与发布

打包

把插件目录打成 zip,manifest.json 必须在压缩包根目录。有第三方依赖时建议先用 esbuild/webpack 打成单文件,不要把整个 node_modules 塞进去。

my-plugin.zip
├── manifest.json     # must be at the archive root
├── icon.png          # optional, referenced by manifest.icon
├── index.js
├── view.html
└── node_modules/…    # include dependencies if any (bundling into a single file is recommended)

发布到市场

AnBoWork → 工具 → 插件 → 「市场」标签 → 右上角「发布插件」,选择 zip 上传。提交后进入待审核状态,审核会核对 manifest 声明与代码是否一致(尤其是权限)。通过后其他用户可以在市场里搜索安装,AI 也可以在判断需要时自动为用户安装已认证的插件。

更新

改 version 后重新发布。客户端按已安装版本和市场版本比对提示更新;重装会保留用户的启用状态和设置项的值。

15完整示例:四象限待办

一个真实可用的插件(精简版):AI 可以增删待办,右侧栏有实时刷新的看板,可以弹系统通知提醒。把三个能力串起来正好覆盖了工具协议、停靠视图推送和反向 RPC。

{
  "id": "local.todo-quadrant",
  "name": "Todo Quadrant",
  "version": "1.2.0",
  "description": "Daily todo list organized by the Eisenhower matrix. Install this whenever the user wants to track tasks or asks to be reminded about something…",
  "main": "index.js",
  "permissions": ["filesystem", "notifications"],
  "activationEvents": ["onStartupFinished"],
  "tools": [{
    "name": "manage_todo_quadrant",
    "summary": "Add, list, complete, delete and update todos in an urgent/important matrix",
    "description": "Manage the user's daily todo list… Actions: add / list / complete / delete / update / set_reminder / remind_now. add/update also accept remind_at to schedule a one-time reminder for that specific todo. Call this whenever the user talks about their todos…",
    "parameters": {
      "type": "object",
      "properties": {
        "action": { "type": "string", "enum": ["add", "list", "complete", "delete", "update", "set_reminder", "remind_now"] },
        "id": { "type": "string" },
        "content": { "type": "string" },
        "quadrant": { "type": "string", "enum": ["urgent_important", "important_not_urgent", "urgent_not_important", "not_urgent_not_important"] },
        "due_date": { "type": "string", "description": "YYYY-MM-DD" },
        "remind_at": { "type": "string", "description": "YYYY-MM-DD HH:MM — a one-time reminder for this todo, independent of the daily digest" }
      },
      "required": ["action"]
    }
  }],
  "views": [{ "id": "quadrant", "title": "Todo Quadrant", "icon": "checklist", "entry": "view.html", "width": 380 }]
}

16调试与常见问题

现象原因与解法
安装时提示 manifest 校验失败按提示逐字段对照「manifest 字段」表:id 格式、main/entry 不能带 ..、icon 必须在白名单、keybindings 的 command 必须指向 commands 里存在的 tool、width 在 240 到 640 之间。
AI 从不调用我的工具几乎都是 description 的问题:没写清楚触发条件,或者用了非英文。参考「写好 description 的要点」。也检查工具名是否和别的工具重名。
调用超时(30 秒)入口脚本没有回 tool_result,通常是 requestId 没原样带回、消息 type 拼错,或者脚本启动时就抛异常退出了。看应用日志里带 [plugin:你的id] 前缀的行。
插件状态显示“崩溃”连续 3 次启动失败。常见于 require 了不存在的包;在插件目录里直接 node index.js 能复现。修好后在插件页关掉再打开即可。
面板打开是空白entry 路径不对,或者页面 JS 报错。页面运行在沙箱里,没有 Node 的 require,只能用 window.pluginHost。
通知没有弹出依次检查:manifest 声明了 notifications;返回值不是 shown:false(用户关了桌面通知);系统设置里允许了 AnBoWork 的通知。
设置项的值读不到值在 message.context.config[id],不在 params 里。

日志文件位置

系统路径
macOS~/Library/Logs/AnBoWork/main.log
Windows%USERPROFILE%\AppData\Roaming\AnBoWork\logs\main.log
查看 API 接入文档