@ finamx/ 主机桥
将 FinamX 扩展嵌入到主机应用程序中的初级程序——web iframes,React apps,以及MCP代理.
安装
npm install @finamx/host-bridge @finamx/bridge-protocol
快速启动——新主机集成
将 FinamX 扩展与平台能力代理嵌入到 \\ 20 行的最快方式 :
npm install @finamx/host-bridge @finamx/bridge-protocol
import {
fetchEmbedLaunch,
launchToBridgeSession,
resolvePlatformBase,
useExtensionEmbed,
} from '@finamx/host-bridge';
export function MyExtensionEmbed({ extensionId }: { extensionId: string }) {
const [state, setState] = React.useState<{
launchUrl: string;
session: ReturnType<typeof launchToBridgeSession>;
} | null>(null);
React.useEffect(() => {
fetchEmbedLaunch({ extensionId, platformBase: resolvePlatformBase() }).then((launch) => {
setState({ launchUrl: launch.launchUrl, session: launchToBridgeSession(launch) });
});
}, [extensionId]);
const bridge = useExtensionEmbed({
launchUrl: state?.launchUrl ?? '',
allowedOrigin: state?.session?.allowedOrigin ?? '',
session: state?.session ?? { sessionId: '', extensionId, allowedOrigin: '', grantedScopes: [] },
// Declarative proxy: both fields → createCapabilityRouterWithPlatformProxy auto-created
platformBase: resolvePlatformBase(),
sessionJwt: state?.session?.sessionJwt ?? '',
});
if (!state) return <p>Loading…</p>;
return (
<div ref={bridge.containerRef} style={{ height: 400 }}>
<iframe ref={bridge.iframeRef} {...bridge.frameProps} style={{ height: '100%' }} />
</div>
);
}
扩展调用 portfolio.summary 接收真正的平台数据,因为 platformBase编号 :sessionJwt
触发 createCapabilityRouterWithPlatformProxy 自动——无手动 CapabilityRouter 需要设置.
方法再绘图(海关方法+平台倒置)
当您的主机自己处理一些方法
import { createCapabilityRouterWithPlatformProxy, resolvePlatformBase, CapabilityRouter } from '@finamx/host-bridge';
const platformRouter = createCapabilityRouterWithPlatformProxy({
platformBase: resolvePlatformBase(),
sessionJwt: session.sessionJwt ?? '',
});
const myRouter = new CapabilityRouter({
invokeHandler: async (req) => {
if (req.method === 'my.custom.method') {
return { ok: true, data: await myBackend.get() };
}
return platformRouter.invoke(req); // fallback to platform
},
});
// Then pass myRouter explicitly:
useExtensionEmbed({ ..., capabilityRouter: myRouter });
快速启动 — 类似本地嵌入( 反应)
import {
ExtensionEmbedFrame,
fetchEmbedLaunch,
launchToBridgeSession,
} from '@finamx/host-bridge';
const launch = await fetchEmbedLaunch({
extensionId: 'com.example.my-extension',
platformBase: process.env.NEXT_PUBLIC_CMS_URL!,
userId: 'poc-user-1',
});
<ExtensionEmbedFrame
session={launchToBridgeSession(launch)}
launchUrl={launch.launchUrl}
/>;
ExtensionEmbedFrame 应用服务器驱动 embedLayout (高下限、滚动策略、手势模式),手柄 ui:resize, (中文(简体) ), ui:scroll-lock,以及 ui:gesture-claim。见 文献/档案/内嵌/内嵌/泥塑。 。 。 .
快速启动 — Web 主机
import { createWebHostBridge, CapabilityRouter, ExtensionSessionConfig } from '@finamx/host-bridge';
const config: ExtensionSessionConfig = {
sessionId: crypto.randomUUID(),
extensionId: 'com.example.my-extension',
allowedOrigin: 'https://my-extension.example.com',
grantedScopes: ['market.read', 'ui.toast'],
};
const bridge = createWebHostBridge({
iframe: document.getElementById('extension-frame') as HTMLIFrameElement,
allowedOrigin: config.allowedOrigin,
session: config,
});
// Tear down (removes window message listener):
bridge.destroy();
快速启动-填充容器布局 (D-04)
对于嵌入式应该填入卡片或模式而不是使用固定高度的宿主UI:
import { ExtensionEmbedFrame } from '@finamx/host-bridge';
// The container div controls the available height; ExtensionEmbedFrame
// fills it and sends host:layout with measured px dimensions via ResizeObserver
// so the extension can respond with appropriate layout (not minHeight: 100vh).
<div style={{ height: 320 }}>
<ExtensionEmbedFrame
session={session}
launchUrl={launch.launchUrl}
fillContainer // iframe height = 100% of container; ResizeObserver pushes dimensions
/>
</div>
与 useExtensionEmbed,挂载 containerRef 在包装上显示:
const bridge = useExtensionEmbed({ ..., fillContainer: true });
<div ref={bridge.containerRef} style={{ height: '100%' }}>
<iframe ref={bridge.iframeRef} {...bridge.frameProps} />
</div>
大小偏好(SIZE-02)
ui:sizePreference 没有 ui:resize当出现偏好时 fillContainer 实值
假的,容器高度变成被夹住 resolveSizePreference 结果。 何时
fillContainer 是真实的,没有设置偏好, 高度停留 100%两者设定后,
容器使用 CSS aspect-ratio (单位:千美元)W / H 从已解析的整数)到 height: auto
因此,康迪特牌可以尊重 优先,没有 ui:resize。 。 。 .
墙上覆盖(WALL-02)
主机可以通过 renderWall 替换 DefaultWallOverlay。如果省略,则默认值
说明性铬仍然有效。 同意重叠仍然是一个单独的主机应用程序部分.
reason: payment 仅复制( e)Требуется оплата()——并不意味着开账单
供应商或退货.
<ExtensionEmbedFrame
session={session}
launchUrl={launch.launchUrl}
renderWall={({ visible, reason }) =>
visible ? <MyWall reason={reason} /> : null
}
/>
虎克用户得到 wall, (中文(简体) ), setWall (代表控制员)和 wallOverlay数字 :
const bridge = useExtensionEmbed({ launchUrl, allowedOrigin, session });
<div ref={bridge.containerRef} {...bridge.containerProps}>
<iframe ref={bridge.iframeRef} {...bridge.frameProps} />
{bridge.wallOverlay}
</div>
快速启动 — 设计令牌+调试工具
传出真实的设计符号, 这样嵌入式扩展可以继承您的主机的主题 通用阴影默认,并选择桥面交通/标志检查面板.
统一调试包(D-05,D-06)
BridgeHostDebugKit 组成日志面板、 符号预览和范围撤销控制
变成单一的选入组件。 总是把门锁在门后 debug 旗帜 —— 绝不屈服
生产嵌入(T-48-02):
import {
useExtensionEmbed,
BridgeHostDebugKit,
CapabilityRouter,
} from '@finamx/host-bridge';
const [debug, setDebug] = useState(false);
const [grantedScopes, setGrantedScopes] = useState(session.grantedScopes);
const bridge = useExtensionEmbed({ ..., debug });
// Render conditionally — never default on in production
{debug && (
<BridgeHostDebugKit
entries={bridge.bridgeLog}
onClear={bridge.clearBridgeLog}
tokens={uiTokens}
grantedScopes={grantedScopes}
onScopesChange={setGrantedScopes} // simulate scope revoke (dev-only)
uiTokensEnabled={tokensEnabled}
onUiTokensEnabledChange={setTokensEnabled}
/>
)}
个别小组(人员组成)
import {
useExtensionEmbed,
BridgeDebugLogPanel,
BridgeTokenPreviewPanel,
CapabilityRouter,
} from '@finamx/host-bridge';
import { mergeUITokens, type UITokens } from '@finamx/bridge-protocol';
function readHostUITokens(): UITokens {
const style = getComputedStyle(document.documentElement);
const colors = { ...mergeUITokens({}).colors };
for (const key of Object.keys(colors) as (keyof UITokens['colors'])[]) {
const v = style.getPropertyValue(`--${key}`).trim();
if (v) colors[key] = v;
}
return mergeUITokens({ colors });
}
const capabilityRouter = useMemo(() => new CapabilityRouter(), []);
const uiTokens = useMemo(() => readHostUITokens(), []);
const [debugEnabled, setDebugEnabled] = useState(false);
const bridge = useExtensionEmbed({
launchUrl: launch.launchUrl,
allowedOrigin: session.allowedOrigin,
session,
capabilityRouter,
uiTokens,
debug: debugEnabled,
});
// ...render the iframe via bridge.iframeRef / bridge.frameProps as usual, then:
{debugEnabled && (
<BridgeDebugLogPanel entries={bridge.bridgeLog} onClear={bridge.clearBridgeLog} />
)}
<BridgeTokenPreviewPanel tokens={uiTokens} />
bridgeLog编号 :clearBridgeLog 仅在插入时 debug: true - 默认放弃
锁定内部/ dev 旗后切换, 不暴露给终端用户 .
快速启动—— MCP 探员
import { createMcpWidgetTools } from '@finamx/host-bridge/mcp';
const tools = createMcpWidgetTools({
widgetCatalog: [
{
extensionId: 'com.example.widgets',
name: 'Portfolio',
description: 'Portfolio summary widget',
routes: ['/widgets/portfolio-summary'],
},
],
launchWidget: async (extensionId, route, params) => {
// Return { launchUrl, sessionId } from your launch service
return {
launchUrl: `https://app.example.com/embed?ext=${extensionId}&route=${route}`,
sessionId: crypto.randomUUID(),
};
},
});
// Register with your MCP server:
// tools.forEach(t => server.tool(t.name, t.description, t.inputSchema, t.handler));
出口
主要软件包( E)@finamx/host-bridge(中文(简体) )
| 导出 | 说明 |
|---|---|
createWebHostBridge(options) | 自动浏览器 iframe 桥 window 消息倾听器管理 |
createHostBridge(options) | 低级工厂——呼叫员管理 window 事件 |
createCapabilityRouter(options?) | 工厂助手 CapabilityRouter |
CapabilityRouter | 验证范围、发送 platform:invoke 到处理器 |
ExtensionEmbedFrame | Drop- in React iframe 有嵌入式布局 + 滚动锁副作用 |
DefaultWallOverlay | 演示墙铬( C)auth 编号 : payment [原件:英 通过 renderWall |
resolveEmbedLayout() | 合并会话 embedLayout 包含默认和超标 |
getEmbedFrameStyles() | Iframe 从布局 + 滚动/ 地貌状态获取 CSS |
runScrollLockSideEffects() | 当扩展发送时应用父滚动锁 ui:scroll-lock |
sendHostLayoutEvent() 编号 : sendHostScrollStateEvent() | 调度 platform:event 嵌入 |
useExtensionEmbed(options) | 反应钩-iframe ref, frameProps, (中文(简体) ), layout,锁/要求状态, bridgeLog, (中文(简体) ), bridgeReady |
fetchEmbedLaunch(options) | 电话 POST /api/v1/embed/launch返回 LaunchResponse |
launchToBridgeSession(launch, userId?) | 转换 LaunchResponse → BridgeSessionConfig |
ExtensionSessionConfig | 会话配置为可选 uiTokens 财务报告和审定财务报表 embedLayout |
UITokens | 设计符号形状( 从 @finamx/bridge-protocol(中文(简体) ) |
BridgeLogEntry | 1个桥梁交通记录条目的形状(useExtensionEmbed({ debug: true })(中文(简体) ) |
BridgeDebugLogPanel | Opt-in 组件——桥的现场景观 send编号 :recv 交通、时间、行情 |
BridgeTokenPreviewPanel | 切入组件——监视预览 UITokens 实际发送到扩展名 |
BridgeHostDebugKit | 统一 dev- only 调试 UI:日志+令牌预览+范围撤销+令牌切换(D-05/D-06) |
部件子包件( S)@finamx/host-bridge/widgets(中文(简体) )
DivKit + 自定义部件主机帮助器用于路由 CMS 授权部件 (D-07) .
需要可选对等点 @divkitframework/react 页:1 DivKitWidgetHost 翻译.
DivKit 自定义组件——阴影DOM绕行工作
DivKit 调制 type: "custom" 插入 a 的元素 <template shadowrootmode="open"> 通过 JavaScript 输入 DOM(insertBefore) (中文(简体) ). 浏览器只在HTML解析过程中处理声明式阴影DOM——动态JS插入被默默忽略. 模板中的 SVG/HTML 最终变成惰性 #document-fragment 永远不要做.
DivKitWidgetHost 财务报告和审定财务报表 WidgetBySlugEmbed 自动处理 : 在 DivKit 挂载后, 它们扫描 <template shadowrootmode> 元素, 调用 host.attachShadow() ,并将模板内容移动到真正的阴影根中。 A 类 MutationObserver 为 DivKit DOM 之后的任何更新重复此内容.
已知的限制——反应性 custom_props
工作绕道去除 <template> 处理后从DOM(tmpl.remove()) (中文(简体) ). DivKit仍然提到这一分离要素。 如果 DivKit 被动更新 custom_props (例如,CMS推动新的 chartPoints 它通过活绑,将新的 SVG 内容写到 r.content.innerHTML 在已解开的模板上——阴影根不更新并显示陈旧的内容.
目前缓解措施:
- 所有现有的自定义组件( E)
finamSparkLine, (中文(简体) ),finamNewsCard, (中文(简体) ),webEmbed接收静态custom_props从CMS——没有活绑,因此限制没有被触发.
添加被动自定义组件时需要的固定 :
与其删除模板,请保留在DOM中,并用一秒钟观察 MutationObserver 瞄准模板的内容。 每次更改时, 替换阴影根的内容 :
// pseudo-code — implement in activateShadowRoots() in each DivKit host component
const shadow = host.attachShadow({ mode });
const sync = () => {
shadow.replaceChildren(document.importNode(tmpl.content, true));
};
sync();
// Do NOT remove the template — keep it for DivKit to update
const contentObserver = new MutationObserver(sync);
contentObserver.observe(tmpl.content, { subtree: true, childList: true, characterData: true });
在运送任何 DivKit 部件之前,需要进行这一修改 custom_props 运行时更改.
import {
resolveWidgetRenderMode,
fetchDivKitWidget,
DivKitWidgetHost,
CustomIdEmbedHost,
} from '@finamx/host-bridge/widgets';
// Route a board card to the correct renderer
const mode = resolveWidgetRenderMode(card.meta);
// → 'divkit' | 'extension-embed' | 'external-embed' | 'custom-id'
// Fetch DivKit JSON from CMS (host passes its own auth headers — T-48-05)
const widget = await fetchDivKitWidget('my-slug', {
platformBase: 'http://localhost:3001',
headers: { 'X-User-Id': 'poc-user-1' },
});
// All-in-one routing component (render props per path)
<CustomIdEmbedHost
meta={card.meta}
renderDivKit={(slug) => (
<DivKitWidgetHost
slug={slug}
fetchOptions={{ platformBase: 'http://localhost:3001', headers: { 'X-User-Id': userId } }}
/>
)}
renderExtension={(extensionId) => <ExtensionEmbed extensionId={extensionId} />}
renderExternalEmbed={(slug) => <ExternalEmbedContent slug={slug} />}
/>
| 导出 | 说明 |
|---|---|
resolveWidgetRenderMode(meta) | 返回“ divkit ” |
fetchDivKitWidget(slug, options) | 获取 /api/v1/divkit/widgets/:slug — 东道国通行证 platformBase + 认证信头 |
DivKitWidgetResponse | 答复类型 fetchDivKitWidget |
DivKitWidgetHost | 反应组件 — 获取 + 使 DivKit JSON; 动态导入 @divkitframework/react |
CustomIdEmbedHost | 为divkit/扩展/外部路径提供道具的路由卡元数据 |
MCP子包件(@finamx/host-bridge/mcp(中文(简体) )
| 导出 | 说明 |
|---|---|
createMcpWidgetTools(options) | 为部件渲染创建平面对象 MCP 工具定义 |
renderWidgetForAgUi(options) | 构建 ag-ui widget_render 事件有效载荷 |
createWidgetAdapter(options) | 通用发射-非MCP系统适配器 |
McpToolDefinition | Plain-object MCP 工具类型( 无) @modelcontextprotocol/sdk 硬度计) |
AgUiWidgetEvent | ag-ui 部件渲染事件类型 |
推广自主专题(D-874-23)
LaunchResponse.extensionTopics (单位:千美元){ publishes: [{ topicType, schema?, legacy? }], subscribes: [] }旅行费
结束 launchToBridgeSession 输入 BridgeSessionConfig 财务报告和审定财务报表 PubSubBroker.register,所以主机使用
useExtensionEmbed 不需要更改代码 。 扩展主题声明于 publishes 之间通过
无需主机赠款的扩展,并对照其计划进行验证;外部订阅者必须声明为
subscribes。主机域中的名称总是遵循主机规则.
平台目录客户端( 任何主机)
import {
createPlatformClient,
createHostCapabilityRouter,
fetchEmbedLaunch,
launchToBridgeSession,
useExtensionEmbed,
} from '@finamx/host-bridge';
const client = createPlatformClient({
platformBase: 'http://127.0.0.1:3001',
userId: 'user-1',
});
const catalog = await client.fetchExtensionCatalog();
const installed = await client.fetchInstalledExtensions();
// Launch + embed + local invoke overrides:
const launch = await fetchEmbedLaunch({ extensionId: 'com.example.widget', userId: 'user-1' });
const session = launchToBridgeSession(launch);
const router = createHostCapabilityRouter({
platformBase: 'http://127.0.0.1:3001',
sessionJwt: session.sessionJwt ?? '',
localHandler: async (req) => {
if (req.method === 'my.host.method') return { ok: true, data: {} };
return null; // fall through to platform proxy
},
});
DivKit 部件 : fetchDivKitWidget 从 @finamx/host-bridge/widgets。 。 。 .
主机签名身份(3.0.0)
签名仅为服务器 : signHostIdentity 从 @finamx/host-bridge/server 和标志 platform.identity.v1 (ES256, (英语), iss = 主机编号, sub = 主机-本地用户id, aud = 扩展编号, kid, TTL 最多300 s),由主机秘密商店出品的私人JWK. 浏览器捆绑不签名,也从来不持有密钥. 只在扩展名宣布时才调用 requiresIdentityContext,然后将结果交给iframe作为 identityToken 内 widget.identity.grantCMS只储存公众的JWK;没有HMAC倒置. sessionJwt 不得将核心/盖特韦标志置于框架或身份证书中.
import { signHostIdentity } from '@finamx/host-bridge/server';
const identityToken = await signHostIdentity({ hostId, userId, extensionId, kid, privateKey });
构建
npm run build # tsc → dist/ (includes dist/mcp/)
npm test # build + run capability-router tests
产出仅为无害环境管理("type": "module") (中文(简体) ). 部分 FinamX 移动单曲。 。 。 .
启动全新的主机应用程序(代理技能)
此 README 中的所有内容都被包装为插件技能
openplatform-host-init (插头技能) /openplatform:openplatform-host-init, (中文(简体) ), https://github.com/尼基塔-Efimenko/开平平台-插图因此,一个新的项目
不需要人类手动重放这些步骤. 它包括:安装
@finamx/bridge-protocol + 键 @finamx/host-bridge 从 npm 获取发射,渲染通过
钩子或 ExtensionEmbedFrame由主机自己主题制作的电线设计符号,
选入调试工具,能力代理,主机域,和赠款. 模板存在于
skills/openplatform-host-init/resources/ 文件夹
打开平台插件。 。 。 .
运行它,在 Claude 代码、代码或光标中安装插件并运行 :
/openplatform:openplatform-host-init
或用普通语言描述目标(例如"在本app中线上扩展主机")-
技能自动匹配。 技能的第一步总是会重新检查
npm view @finamx/host-bridge version 在做任何事情之前,因为这个软件包的发布
状态可以在会话之间发生变化.
如果您不使用代理, 请遵循上面的“ 快速启动” 部分, 顺序为: 安装 ~ • 调试工具 * 能力代理 这和技能自动化的顺序一样.
为了一个完全的穿行,它也覆盖了从当地开始的平台,选择一个种子测试
和具体的核查清单,见
docs/guides/application-layer/connect-a-new-host.md。 。 。 .
代理制成的嵌入物(第58阶段,AGW-04)
MCP 磁共振 compose 和彩绘 workspace.launchWidget({ divkitJson }) 返回已建服务器
embedUrl 指向中央管理系统 /widget/inline?json=…&trust=agent-generated。东道主必须 没有
从原始 DivKit JSON 构造此 URL 客户端 – CMS 内置路由通过
validateAgentGeneratedDivKit。 。 。 .
当挂起编程框时,通过桥接会话 trustClass: 'agent-generated'数字 :
import {
buildAgentGeneratedEmbedSession,
useExtensionEmbed,
} from '@finamx/host-bridge';
const session = buildAgentGeneratedEmbedSession({
sessionId: bridgeSessionId,
embedUrl, // from MCP compose structuredContent or LAUNCH_WIDGET SSE payload
extensionId: 'agent://compose/<widgetId>', // optional; defaults to agent://compose/{sessionId}
});
const bridge = useExtensionEmbed({
launchUrl: embedUrl,
allowedOrigin: session.allowedOrigin,
session, // trustClass: 'agent-generated' gates pubsub / MCP-UI sensitivity (Phase 54)
});
审计: 作曲(MCP)、内置渲染(CMS)和图例发射(平台MCP) graph.addInstance每页
/api/v1/embed/audit 与 method: agent-generated.v1, (中文(简体) ), extensionId,以及 sessionId。 。 。 .
主机执行者应当将自己的会话/用户ID与这些审计领域联系起来.
网络聊天 : 接线推迟到子模块恢复; 使用 buildAgentGeneratedEmbedSession
在任何装有 Iframes 的反应主机中.
流程通知和人类决定(仅服务器)
createHostS2sEndpoint 检查呼叫者的注册范围 并验证方法的机身计划
装入 createCachedDomainLoader声明 requestBody 编号 : responseBody 作为
{ "$ref": "#/topics/<topicType>" } 并在同一主机域中提供这些主题计划.
在引用处理器前检查参数, 解包后结果 hostResult。 。 。 .
缺少或错误的参考文献在援引处理器之前拒绝拨打. 遗留问题
paramsSchema 编号 : resultSchema 继续描述,如浏览器路径.
METHOD_PARAMS_INVALID 财务报告和审定财务报表 METHOD_SCHEMA_UNRESOLVED 也就是说处理器没有被调用.
METHOD_RESULT_INVALID 表示结果被扣; 处理者可能已经执行
一个效果。 效应方法仍然需要在主机中持久的一能和调节.
服务器子路径导出 createSelfNotificationSender, (中文(简体) ), flowNotificationKey, (中文(简体) ),
createFlowDecisionStore 财务报告和审定财务报表 createFlowDecisionService。通知发送者
在经过验证的 " 外联 " HTTP 202接受后完成,并配有确定性钥匙
每个主机/扩展/ rpcId。 来电者必须解决收件人和来自
一个经认证的主机上下文; 图表不能选择收件人.
决策存储需要宿主拥有的原子持久性适配器(Mongo)
收藏匹配 DecisionCollection) (中文(简体) ). 待决请求无终止期。 这个
回答和补全的发件箱是用解剖法写入的;回调由
东道工人。 东道主会议/CSRF检查、目前的同意/资格、文物进入、,
排出箱排出和主机UI仍然是集成责任. 这些帮手做的
不安装主机主题/方法, 不授予背景访问权限, 或认证一个原始
请求身体。 绝对不能从浏览器或Temperal工作流程代码调用它们.
CMS 签名信托( 3. 9.1)
部署 /op/jwks 财务报告和审定财务报表 /host/jwks 在升级前在CMS中。 标准 CMS 代号
校验者在配置后选择此专用源
JWKS URL,无倒置. 自定义 JWKS 来源并注入 getKey编号 :cmsGetKey
解析器必须只包含 CMS 签名密钥 。 主机身份保留其单独的主机
密钥来源( e)/host/jwks)和CMS签名的安装装订. 当前/重叠主机密钥
保留在遗产中 /.well-known/jwks.json 兼容性的终点;
会员总数从未授予CMS授权.
AG- UI 聊天传输
使用 @finamx/host-bridge/ag-ui 在浏览器和 @finamx/host-bridge/server 用于节点流、范围收发和耐用ACK/Cancel生命周期。 支持无害环境管理和通用JS服务器消费者。 主机认证、端点路由和聊天/板的持久性作为端口提供。 见 主机集成指南 寿命周期和交易日记的切入点。 预览补丁是惰性的;流承诺从不赋予动作能力.