Multica Hello Panel 插件剖析:一个 issue_panel Surface 如何演示 Action API 全契约
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 可以看到它们全部通过同一套桥协议发出:
-
multica.context.get()— 谁在看、面板挂在哪个 issue 上 脚本启动后第一件事是call("GET", "/context")(main.js 第 70 行),取回context.user.name、context.workspace.name与context.issue.identifier渲染页头。在正式 SDK 中对应 packages/plugin-sdk/index.ts 的multica.context.get(),返回PluginContext:workspace、user、当前挂载的 issue(可选),以及非密钥的安装配置config和经net:scope 授权的域名列表。SDK 端对 context 做了缓存,get(true)可强制刷新。 -
multica.issue.get()— 在issues:read权限下读取 issuecall("GET", /issues/${id}) 拉取 issue 标题(main.js 第 74 行)。这是声明 scope 的直接体现:manifest 中没有issues:read,宿主会拒绝该请求。 -
multica.issue.comment()— 以用户身份写入的评论 点击 "Post a test comment" 后,脚本POST /issues/{id}/comments(main.js 第 95 行)。README 强调这条写操作有三个语义:评论落库为当前登录用户所写、带via_plugin_id标记来自本插件、且受comments:writescope 保护——若管理员未授予该 scope,返回的 403 错误信息会直接点名缺失的 scope。SDK 侧的封装见 packages/plugin-sdk/index.ts,且 SDK 文档明确指出:surface 发的评论不会触发@mention的任务分发,即 surface 不能通过发文字来间接触发 agent 运行。 -
multica.storage.user— 按成员隔离的持久化 脚本用键note读写GET/PUT /storage/user/note(main.js 第 79-84 行)。注释解释了 "user" 与 "workspace" 两种 scope 的区别:per-member 存储意味着另一位成员在同一 issue 上打开同一面板看到的是自己的笔记。SDK 中storage.user.get()还会把 404(键不存在)转换为正常的null返回,而不是抛错(index.ts 第 178-187 行)。 -
multica.ui.resize()— 向宿主申请实际所需高度 脚本中每次状态变化都调用resize(),请求高度为document.body.scrollHeight + 16(main.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.go 的 PublishLocalBundle:它要求本地源码是 MULTICA_PLUGIN_DIR 下的单一目录名。关键细节是版本号处理——重新发布一个未改动的版本号会落成 1.0.0+dev.N 这样的开发构建后缀(plugin_package.go 第 196 行附近)。这样既保留了"每次运行的是有人同意过的某个版本"的审计属性,又不打断开发节奏。
单文件、无 import 是契约而非简化。README 特别强调 ui/main.js 无 import 不是因为示例偷懒: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.ts:BRIDGE_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.ts 的
buildSurfaceFrameDocument()构造:外层文档是宿主自己的、与 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 时可以直接对照的行为基准。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00