Skip to content

Latest commit

 

History

History
151 lines (108 loc) · 5.8 KB

File metadata and controls

151 lines (108 loc) · 5.8 KB

Room UI API(全局 parti

index.html 是房间 UI,运行在一个 沙箱 iframe 里。它通过注入的全局对象 parti 与 Runtime 通信——不需要 import 任何东西,parti 直接可用

源码:packages/client-sdk/src/bootstrap.ts

1. 完整 API

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;
}

parti.playerId: string | null

当前玩家的 id。初始为 null,房间初始化(收到 init)后才被赋值。第一次 onState 回调触发时它已就绪,所以在 onState 回调里读它是安全的;在顶层同步代码里 读可能还是 null

parti.onState(handler) => unsubscribe

订阅权威状态。state 每次变化时 handler(state) 被调用。

  • 订阅时若已有 state,会立即同步回调一次(不必等下次变化)。
  • 返回一个取消订阅函数,调用它即可移除该 handler。
  • 典型用法:在 handler 里整体重渲染 UI。
const off = parti.onState((state) => render(state));
// 不再需要时: off();

parti.onEvent(event, handler) => unsubscribe

订阅由逻辑侧 ctx.broadcast(event, ...) / ctx.send(..., event, ...) 发出的一次性事件。 handler(payload) 收到事件 payload。返回取消订阅函数。

parti.onEvent('game:over', (p) => {
  alert(p.winner === parti.playerId ? '你赢了' : '你输了');
});

事件是一次性的,错过不补发;持久状态请从 onState 拿。

parti.action(action, payload?) => Promise<{ ok: true }>

提交一次玩家意图,触发逻辑侧对应的 actions[action] handler。

  • payload 省略时按 null 处理。
  • ⚠️ 返回的 Promise 会立即 resolve 为 { ok: true },它只表示「已发出」, 不代表服务端已处理、更不代表操作成功/合法。 不要用它判断成败——成败要看 随后的 onState(状态变了)或 onEvent(收到结果事件)。
button.onclick = () => parti.action('mark', { cell: 4 });

parti.ready(): void

告诉 Runtime「本玩家已就绪」,触发逻辑侧 onReady(ctx, player)幂等,重复调用无副作用。 许多简单房间在脚本末尾直接调用一次即可。

parti.leave(): void

主动离开房间。

parti.getState(): unknown

同步读取当前最新 state(无订阅)。大多数时候用 onState 即可。

parti.log(...args): void

把日志送到宿主页 DevTools,便于调试沙箱内代码。

2. 沙箱限制

Blob 与 filesystem package 统一运行在 sandbox="allow-scripts allow-same-origin" 的 iframe 中:

  • 两种 package 都与宿主页保持同源;当前应仅运行可信 package,游戏通信仍应使用 parti
  • packageMode 只决定资源加载方式,不改变 iframe 权限。更严格的安全限制将由独立机制提供。
  • 沙箱不等于网络防火墙。外部请求仍服从浏览器 CORS,permissions.network 当前仅为声明。
  • packageMode: "filesystem" 时可以用普通相对路径加载 package 内文件;blob 模式不提供该能力。
  • 不需要、也不能 import SDK——parti 是注入的全局对象。
  • 普通的 DOM 操作、内联 <style><script> 都正常可用。

3. 推荐写法:onState 重渲染 + onEvent 处理瞬时反馈

// 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 范例见 示例:井字棋

4. parti.exposeToAgent(describe) —— 为 AI agent 提供转述(可选)

当有 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