index.html 是房间 UI,运行在一个 沙箱 iframe 里。它通过注入的全局对象
parti 与 Runtime 通信——不需要 import 任何东西,parti 直接可用。
源码:packages/client-sdk/src/bootstrap.ts。
interface Parti {
playerId: string | null;
getState(): unknown;
onState(handler: (state: unknown) => void): () => void;
onEvent(event: string, handler: (payload: unknown) => void): () => void;
action(action: string, payload?: unknown): Promise<{ ok: true }>;
ready(): void;
leave(): void;
log(...args: unknown[]): void;
// 可选:为 AI agent 提供"转述"(无障碍式),详见 §4 与 agent-access.md
exposeToAgent(describe: (state: unknown) => unknown): void;
}当前玩家的 id。初始为 null,房间初始化(收到 init)后才被赋值。第一次
onState 回调触发时它已就绪,所以在 onState 回调里读它是安全的;在顶层同步代码里
读可能还是 null。
订阅权威状态。state 每次变化时 handler(state) 被调用。
- 订阅时若已有 state,会立即同步回调一次(不必等下次变化)。
- 返回一个取消订阅函数,调用它即可移除该 handler。
- 典型用法:在 handler 里整体重渲染 UI。
const off = parti.onState((state) => render(state));
// 不再需要时: off();订阅由逻辑侧 ctx.broadcast(event, ...) / ctx.send(..., event, ...) 发出的一次性事件。
handler(payload) 收到事件 payload。返回取消订阅函数。
parti.onEvent('game:over', (p) => {
alert(p.winner === parti.playerId ? '你赢了' : '你输了');
});事件是一次性的,错过不补发;持久状态请从
onState拿。
提交一次玩家意图,触发逻辑侧对应的 actions[action] handler。
payload省略时按null处理。⚠️ 返回的 Promise 会立即 resolve 为{ ok: true },它只表示「已发出」, 不代表服务端已处理、更不代表操作成功/合法。 不要用它判断成败——成败要看 随后的onState(状态变了)或onEvent(收到结果事件)。
button.onclick = () => parti.action('mark', { cell: 4 });告诉 Runtime「本玩家已就绪」,触发逻辑侧 onReady(ctx, player)。幂等,重复调用无副作用。
许多简单房间在脚本末尾直接调用一次即可。
主动离开房间。
同步读取当前最新 state(无订阅)。大多数时候用 onState 即可。
把日志送到宿主页 DevTools,便于调试沙箱内代码。
Blob 与 filesystem package 统一运行在
sandbox="allow-scripts allow-same-origin" 的 iframe 中:
- 两种 package 都与宿主页保持同源;当前应仅运行可信 package,游戏通信仍应使用
parti。 packageMode只决定资源加载方式,不改变 iframe 权限。更严格的安全限制将由独立机制提供。- 沙箱不等于网络防火墙。外部请求仍服从浏览器 CORS,
permissions.network当前仅为声明。 packageMode: "filesystem"时可以用普通相对路径加载 package 内文件;blob模式不提供该能力。- 不需要、也不能
importSDK——parti是注入的全局对象。 - 普通的 DOM 操作、内联
<style>、<script>都正常可用。
// 1. 状态驱动:onState 里把整个界面按 state 重画
function render(state) {
// 根据 state.phase 切换界面、根据 state.board 画棋盘……
}
parti.onState(render);
// 2. 瞬时反馈:onEvent 处理「刚刚发生的事」(动画/提示音/弹窗)
parti.onEvent('guess:wrong', (p) => {
if (p.playerId === parti.playerId) flash('再试试');
});
// 3. 交互:按钮 -> action
submitBtn.onclick = () => parti.action('guess', { text: input.value });
// 4. 入场:标记就绪
parti.ready();完整的、可运行的 UI 范例见 示例:井字棋。
当有 AI agent 通过 agent 路由接入房间时,它默认只能读到 getState() 的原始
状态并靠推断游玩。你可以像写「无障碍说明」一样,注册一个把当前局面翻译成面向 AI 的
文字/结构化说明的函数,大幅降低 agent 的理解成本与试错:
parti.exposeToAgent((state) => ({
summary: '井字棋。X 先手,三连即胜。',
phase: state.phase, // 'playing' | 'finished'
yourMark: state.marks[parti.playerId],
yourTurn: state.turn === parti.playerId,
availableActions: [
{ name: 'mark', when: '轮到你且格子为空', payload: '{ cell: 0..8 }' },
],
}));- 完全可选:不注册也能被 agent 游玩(退化为读
state推断)。 - 只在 agent 模式下执行:普通人类玩家永不触发,对游戏流程零影响、零开销。
- 每次状态变化都会用最新
state重新调用,结果推送给 agent 的describe()。 - 运行在本玩家视角的 UI 里,因此只应描述该玩家可见的信息;由于 UI 本就拿不到 别人的隐藏状态(谜底、身份、底牌),转述天然不会泄露秘密——但仍请勿把通过私密 事件收到的他人信息写进去。
- 返回值必须可 JSON 序列化(对象或字符串皆可)。
agent 侧如何读取这份转述、以及房主如何邀请 AI,见 agent-access.md。