首页
/ Multica Hello Panel 插件剖析:一个 issue_panel Surface 如何演示 Action API 全契约

Multica Hello Panel 插件剖析:一个 issue_panel Surface 如何演示 Action API 全契约

2026-09-05 22:04:57作者:凤尚柏Louis

Hello Panel(examples/plugins/hello-panel/README.md)是 Multica 仓库中的参考插件:一个 issue_panel surface,用最小代码把 v1 surface 能够触及的 Action API 能力全部走了一遍。读透它,你就能掌握三件事:Multica 插件的清单(manifest)如何声明 scope 与 surface 贡献、surface 脚本如何通过宿主桥(host bridge)以"当前登录用户"的身份读写 issue 与评论、以及沙箱 iframe 的运行环境与发布/开发流程。本文以该插件文档为骨架,结合 manifest 文件surface 脚本 和宿主端实现源码,把文档中的每个断言落到可验证的代码事实上。

参考插件的定位:演示契约,而非框架

README 开宗明义:Hello Panel 是 "the reference Multica plugin",其唯一目的是演示 Action API 的完整契约,并且刻意保持"无聊"(boring)——因为它同时是 surface 端到端测试运行的 fixture,不应引入流行框架之类的额外变量。这一定位在测试代码中可以印证:服务端契约测试直接以 hello-panel/ 目录结构作为打包夹具,验证 manifest、ui/main.js 等文件被正确收录进插件包(见 server/pkg/plugincontract/bundle_test.go)。

五个能力点逐一对应源码

README 的 "What it shows" 一节列出了 Hello Panel 演示的五个 API。逐项对照 ui/main.js 可以看到它们全部通过同一套桥协议发出:

  1. multica.context.get() — 谁在看、面板挂在哪个 issue 上 脚本启动后第一件事是 call("GET", "/context")main.js 第 70 行),取回 context.user.namecontext.workspace.namecontext.issue.identifier 渲染页头。在正式 SDK 中对应 packages/plugin-sdk/index.tsmultica.context.get(),返回 PluginContext:workspace、user、当前挂载的 issue(可选),以及非密钥的安装配置 config 和经 net: scope 授权的域名列表。SDK 端对 context 做了缓存,get(true) 可强制刷新。

  2. multica.issue.get() — 在 issues:read 权限下读取 issue call("GET", /issues/${id}) 拉取 issue 标题(main.js 第 74 行)。这是声明 scope 的直接体现:manifest 中没有 issues:read,宿主会拒绝该请求。

  3. multica.issue.comment() — 以用户身份写入的评论 点击 "Post a test comment" 后,脚本 POST /issues/{id}/commentsmain.js 第 95 行)。README 强调这条写操作有三个语义:评论落库为当前登录用户所写、带 via_plugin_id 标记来自本插件、且受 comments:write scope 保护——若管理员未授予该 scope,返回的 403 错误信息会直接点名缺失的 scope。SDK 侧的封装见 packages/plugin-sdk/index.ts,且 SDK 文档明确指出:surface 发的评论不会触发 @mention 的任务分发,即 surface 不能通过发文字来间接触发 agent 运行。

  4. multica.storage.user — 按成员隔离的持久化 脚本用键 note 读写 GET/PUT /storage/user/notemain.js 第 79-84 行)。注释解释了 "user" 与 "workspace" 两种 scope 的区别:per-member 存储意味着另一位成员在同一 issue 上打开同一面板看到的是自己的笔记。SDK 中 storage.user.get() 还会把 404(键不存在)转换为正常的 null 返回,而不是抛错(index.ts 第 178-187 行)。

  5. multica.ui.resize() — 向宿主申请实际所需高度 脚本中每次状态变化都调用 resize(),请求高度为 document.body.scrollHeight + 16main.js 第 44-47 行)。注意 iframe 不会自动撑高,必须在内容稳定后主动上报,且宿主会对上报值做钳制(clamp)。SDK 实现同样如此:resize() 是 fire-and-forget 的 notify,宿主不回应(index.ts 第 261-269 行)。

此外脚本还处理了一类宿主主动推送的消息:kind === "theme" 的主题事件,把设计令牌写为 :root 的 CSS 自定义属性(main.js 第 20 行)。令牌清单与宿主同步逻辑见 packages/views/plugins/surface-document.ts--background--foreground--border--radius 等 11 个变量)。这也是为什么 main.js 里可以放心用 var(--muted-foreground) 这类变量而不用自带样式表。

Manifest 全字段走读

examples/plugins/hello-panel/multica.plugin.json 是一个可完整复制的最小清单:

{
  "manifest_version": 1,
  "key": "ai.multica.hello-panel",
  "name": "Hello Panel",
  "description": "The reference surface: reads the current issue, posts a comment, and stores a per-member note.",
  "version": "1.0.0",
  "author": { "name": "multica", "url": "https://multica.ai" },
  "scopes": ["issues:read", "comments:write", "storage:user"],
  "contributes": {
    "surfaces": [
      {
        "key": "hello",
        "type": "issue_panel",
        "name": "Hello",
        "entry": "ui/main.js",
        "platforms": ["web", "desktop"]
      }
    ]
  }
}
  • key 是插件的稳定身份(反向域名风格),version 是语义化版本。发布的版本不可变,安装会把工作区绑定到具体版本,只有管理员升级才会更换所运行的代码。
  • scopes 三项与 README "What it shows" 一一对应:读 issue、写评论、按成员存储。没有声明任何 net: scope——这正是 "Note on scopes" 一节的关键。
  • contributes.surfaces 声明一个 issue_panel 类型的表面:key 是插件内的标识,entry 指向打包产物中的脚本文件,platforms 限定在 web 与 desktop 平台挂载。

发布与开发流程:zip 上传、不可变版本与 MULTICA_PLUGIN_DIR

README 的 "Running it" 给出两条路径,均已在服务端实现中得到确认:

正式路径 — zip 上传。把 manifest 连同它命名的所有文件打包成 zip,在 Settings → Plugins 上传。作者无需自备服务器:Multica 存储产物,从专用插件内容源(plugin-content origin)直接发放面板脚本,并把安装绑定到该不可变版本。SDK 文档 packages/plugin-sdk/README.md 进一步说明:由于版本不可变、安装即绑定,用户在同意屏(consent screen)上批准的就是浏览器实际运行的代码。

开发路径 — MULTICA_PLUGIN_DIR 直发磁盘。迭代期间,设置环境变量 MULTICA_PLUGIN_DIR 指向插件源码目录,即可跳过 zip 与上传环节直接从磁盘发布。服务端实现见 server/internal/service/plugin_package.goPublishLocalBundle:它要求本地源码是 MULTICA_PLUGIN_DIR 下的单一目录名。关键细节是版本号处理——重新发布一个未改动的版本号会落成 1.0.0+dev.N 这样的开发构建后缀(plugin_package.go 第 196 行附近)。这样既保留了"每次运行的是有人同意过的某个版本"的审计属性,又不打断开发节奏。

单文件、无 import 是契约而非简化。README 特别强调 ui/main.jsimport 不是因为示例偷懒:Multica 在一份生成的文档中发放入口脚本,那里没有模块图,裸模块说明符(bare specifier)无处解析,因此依赖必须自行打包进单文件。Hello Panel 因此内联了一个最小 bridge 客户端代替 import { multica } from "@multica/plugin-sdk"(见 main.js 文件头注释);真实插件应把 SDK 与依赖一起 bundle 进一个文件。

沙箱执行环境:MessagePort 桥、opaque origin 与 CSP

Hello Panel 的一切行为都发生在沙箱 iframe 里,README 末节的 "Note on scopes" 是这段安全设计的浓缩表述:由于未声明 net: scope,surface 的 CSP 得到 connect-src 'none'——它字面意义上无法向任何地方发数据,所有操作必须经由宿主桥完成。源码里可以逐层核对这一设计:

  • 桥的接线。宿主在插件代码执行前注入 globalThis.__multicaPluginBridgePortV2(一个 MessagePort),surface 侧脚本读取后立即删除该全局,保证"单通道"不变式(main.js 第 13-17 行)。协议版本与消息形态定义在 packages/plugin-sdk/protocol.tsBRIDGE_PROTOCOL_VERSION = 2,请求为 { id, kind: "action", method, path, body },另有 kind: "ui.resize" 与宿主主动的 { kind: "theme" } 事件。Hello Panel 的 call() 与 SDK 的 Bridge.request() 是完全同构的实现:生成 r{seq} 请求号、把 { id, kind: "action", method, path, body } post 到端口、按 id 匹配响应,ok 时 resolve,否则以带 HTTP 状态码的错误 reject(SDK 侧为 MulticaPluginError,默认 15 秒超时,见 index.ts 第 155-167 行)。
  • 身份绑定在端口上,而非 origin。因为 iframe 以 sandbox="allow-scripts" 挂载且没有 allow-same-origin,其 origin 是不透明的 "null",任何 surface 的 event.origin 看起来都一样。因此"谁发来的消息"由"消息从哪条私有通道到达"来回答——这是 protocol.ts 文件头注释 明确的设计决策。
  • opaque origin 的实际后果。没有 cookie、localStorage/sessionStorage 都不可用,所以脚本里没有任何浏览器存储,状态全部走 multica.storage(服务端持久化、按工作区或按成员隔离);若 surface 直连自家后端,后端要接受 Origin: null 的 CORS 请求(packages/plugin-sdk/README.md 的 "What a surface is" 一节)。
  • CSP 由宿主生成。外层框架文档由 packages/views/plugins/surface-document.tsbuildSurfaceFrameDocument() 构造:外层文档是宿主自己的、与 app 同源,其 frame-src 只允许回到发放了该插件的确切内容源;内层 iframe 仍保持 sandbox="allow-scripts" 且无 allow-same-origin,使两个插件各拿到不同的 opaque origin、互相不可见。connect-src 则依据 manifest 的 net: scope 推导(服务端在 server/internal/handler/plugin_surface.go 生成 guest 文档):Hello Panel 零 net: scope,于是得到 connect-src 'none',连回自身源都不行——因为发放代码的源现在属于 Multica,不再属于作者。

权限模型:scope 与用户自身权限的双重约束

SDK 文档概括了两条同时生效的边界(packages/plugin-sdk/README.md 的 "What you can do, and what bounds it" 一节):一是管理员授予插件的 scope,二是该用户自己本来能做什么。因此没有 issue 访问权限的成员即便通过 surface 也只会收到 404;管理员拒绝的 scope 则是点名 scope 名的 403。每一次调用都是"surface 请求、宿主以登录用户自身会话执行"的间接调用——帧内不存在任何可泄露的凭据,这正是 Hello Panel 用 via_plugin_id 而非独立 bot 身份发评论的底层原因。

小结

Hello Panel 用约一百行的单文件脚本覆盖了 Multica v1 surface 的全部关键契约:manifest 声明 issues:read / comments:write / storage:user 三类 scope 并贡献一个 web/desktop 双平台的 issue_panel;脚本经 __multicaPluginBridgePortV2 端口完成 context 读取、issue 读取、以用户身份发评论、按成员存储与高度上报五类操作;零 net: scope 换来 connect-src 'none' 的绝对网络静默。配合 zip 上传发布(不可变版本 + 安装绑定)与 MULTICA_PLUGIN_DIR 开发通道(1.0.0+dev.N 构建后缀),这套参考插件既是入门样例,也是编写自己插件 surface 时可以直接对照的行为基准。

登录后查看全文
热门项目推荐
相关项目推荐