in-page-playable-page
v0.1.8
Published
仓库 `in-page-playable` 的 **页内单例** 子目录:每 `window` 一个纯 JS 实例,`installPlayable` / `handle`,组合 `in-page-sense` / `in-page-bot` / `in-page-bom`。
Readme
in-page-playable-page
仓库 in-page-playable 的 页内单例 子目录:每 window 一个纯 JS 实例,installPlayable / handle,组合 in-page-sense / in-page-bot / in-page-bom。
与 React 无关。信封字段以仓库根协议为准,本目录不另写 cmd 表。
| 文档 | 用途 | |------|------| | 需求拟定 | 本包产品基线 | | 07 与插件交换 | 目标 page 的 postMessage;插件只传话 | | 根协议 | 跨子目录信封真源 | | AGENTS.md | 本包 Agent 入口 |
状态
- P0 + P1 已实现:
hello/snapshot/run/escape、单例、门闩、attachWindowBridge(down/up 携seqecho) - 三原子库已接线:
installPlayable默认createSense/createBot/createBom;islandSlots/captureScreenshot转交 sense - playground:
npm run playground(顶层 +shell.htmliframe 夹具;打点符合 sense 合同) - 协议类型由本包导出,供同仓 MCP
import type
给 MCP 的类型入口
import type {
IPlayableDownlink,
IPlayableUplink,
TPlayableErrorCode,
} from "in-page-playable-page";
// 源码侧:in-page-playable-page/src/index.ts
// 构建后:in-page-playable-page/dist/index.d.ts字段语义仍以根 docs/AI与人类/协议/ 为准;本包类型是同一份的实现侧导出。
脚本
npm install
npm run typecheck
npm run lint
npm run test
npm run build
npm run playgroundQuick Start
本包提供页内单例(每个 window/document 一个实例),用来在已打开的页面上:
- 接收信封(
hello / snapshot / run / escape) - 现读菜单(
in-page-sense)与必要时点击(in-page-bot)/刷新(in-page-bom) - 按协议返回上行信封(
ok:false + error.code诚实失败)
1) 安装并注入单例
宿主只需在目标页面里调用一次(例如 content script / iframe app 内):
import { installPlayable } from "in-page-playable-page";
const playable = installPlayable({
root: document, // 或 iframe 的 contentDocument
window, // 默认是 root 所属 window / globalThis
allowedOrigins: [window.location.origin], // 如需启用 attachWindowBridge
// islandSlots: { ... }, // 如有 island 合成 id(可选)
// captureScreenshot: async (scope) => ({ mime: "image/png", width, height, bytesBase64 }),
});
installPlayable会把该window标记为已安装(window.__IN_PAGE_PLAYABLE_V1__),但它不是 API、也不会提供可直接调用的全局入口。
2) 直接调用 handle(playground/宿主联调常用)
import { installPlayable } from "in-page-playable-page";
import type { IPlayableDownlink } from "in-page-playable-page";
const playable = installPlayable({ root: document, window });
const requestId = "req-1";
const down: IPlayableDownlink = {
pageId: null,
requestId,
cmd: "hello",
payload: {},
};
const up = await playable.handle(down);
// up.ok === true 时,up.payload.pageId 为新生成的页内 pageId3) 通过 postMessage 走插件通道(对打插件时常用)
插件脚本无法直接 import 并调用 handle,约定使用目标 page 的 window.postMessage。
在该 page 安装完成后显式开启监听:
playable.attachWindowBridge();桥会只接受同一 window 自语(event.source === window)且允许的 origin,并在收到有效 downlink 运输封装后回传 uplink 运输封装。
信封字段(cmd/requestId/pageId/payload)不在运输包装里做语义改写。
DOM 标记契约(与 in-page-sense 对齐)
编排层本质上依赖 in-page-sense 的“现读菜单 + resolve 规则”。你需要在页面里按 in-page-sense 标记点位:
- 页标题:放一个(或多个,但取第一个非空)
data-e2e-pagetitle - 可点击节点(playables):对可操作控件标记
data-e2e-kind="playable"data-e2e-id="..."(菜单 id,用于 run)data-e2e-event="click|input|drag|scroll"data-e2e-title="..."(菜单标题)data-e2e-desc="..."(菜单说明)
示例:
<div hidden data-e2e-pagetitle="ready-list"></div>
<button
data-e2e-kind="playable"
data-e2e-id="btn-save"
data-e2e-event="click"
data-e2e-title="Save"
data-e2e-desc="Save the current list"
>
Save
</button>Island 合成 id(可选)
若页面里存在复合体(data-e2e-kind="island"),可由宿主注入 islandSlots 让 sense 把岛展开成合成 playable id:
合成 id 形状:{islandId}:{slotName}(例如 list-pagination:next)
宿主需要提供:
- 岛:
data-e2e-kind="island" data-e2e-id="list-pagination" installPlayable({ islandSlots }):描述通过aria-label或role-name寻址岛内 slot 元素
协议字段语义仍以根协议真源为准;本包只做编排与门闩,不另写 cmd 表。
生命周期与失败语义(协议 00–03 对齐)
每个 window/document 的公开生命周期:
installed:已 install,但未 hello 成功(pageId === null)live:hello 成功后写入pageId(允许snapshot/run/escape)retired:escape.reload触发后退役(同一 pageId 的旧请求拒绝)
hello
- 成功:mint 新
pageId,进入live - 重复 hello:复用旧
pageId(不换代)
snapshot
- 只读:现读 sense snapshot
- 输出
payload.snapshot(sense 回包)与payload.escapes(由本页 snapshot 派生)
run
- 门闩:必须
live且 envelope 的pageId等于当前pageId - id 校验:
""或 generic ref(如g0)→NOT_IN_MENU,零副作用- 现读菜单中不存在该 id →
NOT_IN_MENU
- 命中后:交给
in-page-bot只进行click(targetElement)
escape(V1 仅支持 reload)
- 现读重校:必须由本页 sense snapshot 判定
reload仍允许 → 否则ESCAPE_NOT_ALLOWED - 调用
in-page-bom.run({ type: "reload" })bom返回retired:true才真正把本 pageId 标成 retiredbom返回ok:false且retired:false时:本包不假退役,并回DISPATCH_FAILED
因此该组件的失败路径在 state machine 上是“可解释、可复验”的(不依赖 Agent 记忆)。
API 说明
installPlayable(options?)
每个 window 一个实例;二次 install 不会换 pageId。
function installPlayable(options?: IInstallPlayableOptions): IPlayable;IInstallPlayableOptions:
root?: Document | ShadowRoot | Element:sense 的扫描根(默认document)window?: Window:绑定浏览上下文(默认root所属 window /globalThis)islandSlots?: IIslandSlotTable:岛展开槽位表(来自in-page-sense)captureScreenshot?: (scope: Element) => Promise<unknown>:可选截图注入(对齐 sense)allowedOrigins?: string[]:仅用于桥监听 origin 白名单ports?: IPlayablePorts:仅测试/联调注入,生产默认使用真三库
IPlayable
pageId: string | nullphase: "installed" | "live" | "retired"handle(request: IPlayableDownlink): Promise<IPlayableUplink>attachWindowBridge(): void
类型与协议真源
信封字段与失败码的真源在仓库根:
docs/AI与人类/协议/(见本目录 README 顶部链接)
本包导出与协议对齐的 TypeScript 类型,供同仓 MCP/插件 import type 使用:
import type { IPlayableDownlink, IPlayableUplink, TPlayableErrorCode } from "in-page-playable-page";导出项(概览)
installPlayable/resetPlayableForTestsderiveEscapes/isEscapeAllowedattachWindowBridge:通过playable.attachWindowBridge()启用
测试辅助(resetPlayableForTests / detachWindowBridgeForTests)不建议在生产逻辑里使用。
