원클릭으로
use-ui-sdk
在 Spherse 的 HTML 内容中嵌入 postMessage 调用,实现 iframe 与 App 的交互(如创建会话、打开文件、key-value 数据读写)
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
在 Spherse 的 HTML 内容中嵌入 postMessage 调用,实现 iframe 与 App 的交互(如创建会话、打开文件、key-value 数据读写)
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Use when creating or editing an agent-level Spherse chat window theme.css for custom chat backgrounds, headers, message bubbles, avatars, composer inputs, markdown blocks, or placeholder text.
指导用户创建自定义 UI 主题,通过 .spherse/theme.css 覆盖 CSS 变量实现视觉定制
为任意《哈利·波特》角色生成「魔法世界风格的手机界面」——包括聊天记录、朋友圈动态和羊皮笔记。数据与 UI 分离,只需编写 JSON 数据文件即可创作新角色。
当用户想要创建自定义 skill 时使用,涵盖 skill 的两个层级(project-level 与 agent-level)、标准目录结构与 SKILL.md 格式规范
在 Spherse 中产出任何 HTML 之前必须先阅读本 skill。当用户要求创建或修改 HTML 页面、生成网页、制作可视化展示(欢迎页、导览主页、内容卡片、预览页等任意 HTML 交付物)时,务必在写出 HTML 代码前先读本 skill,了解 charset、数据与渲染分离的决策、数据加载模式与 App 能力调用(含交互式卡片回传会话)的约定;切勿未经阅读直接输出 HTML
Guide for coding agents to migrate user-visible strings to the @spherse/i18n package and update locale catalogs
| name | use-ui-sdk |
| description | 在 Spherse 的 HTML 内容中嵌入 postMessage 调用,实现 iframe 与 App 的交互(如创建会话、打开文件、key-value 数据读写) |
Spherse 中的 HTML 文件(欢迎页、Content Browser 预览、聊天 HtmlCard)通过 iframe 展示。你可以使用浏览器原生 postMessage API 从 iframe 内触发 App 操作,无需引入任何脚本或依赖。
所有交互通过 window.parent.postMessage 发送,消息必须包含以下结构:
window.parent.postMessage({
type: "spherse:action",
action: "<action-name>",
params: { /* action 参数 */ }
}, "*");
type 必须为 "spherse:action",其他值会被忽略action 为操作名称,见下方可用 action 列表params 为操作参数对象当 HTML 作为聊天 HtmlCard 渲染时,App 会向卡片所在的 iframe 注入当前会话的运行时信息,卡片无需硬编码任何 ID 即可向「当前会话」发消息或读取上下文。Welcome Page 与 Content Browser 预览不注入运行时上下文。
注入方式有两种(同时提供,任选其一读取):
全局变量:window.__SPHERSE__,在 iframe 加载后由 App 写入。
const { sessionId, agentId, projectId } = window.__SPHERSE__;
postMessage 通知:App 在 iframe 加载时发送 { type: "spherse:runtime", sessionId, agentId, projectId }。在卡片脚本最早期注册监听器即可竞态安全地拿到(推荐用于需要在脚本初始化阶段就使用的场景):
window.addEventListener("message", (e) => {
if (e.data?.type === "spherse:runtime") {
window.__SPHERSE__ = e.data;
init();
}
});
对「用户点击按钮才触发」的交互式卡片,直接读
window.__SPHERSE__即可(此时必然已注入);对「加载时立即使用」的场景,使用 postMessage 监听更稳妥。
function getSpherseRuntime() {
return new Promise((resolve) => {
if (window.__SPHERSE__) return resolve(window.__SPHERSE__);
const handler = (e) => {
if (e.data?.type === "spherse:runtime") {
window.removeEventListener("message", handler);
resolve(e.data);
}
};
window.addEventListener("message", handler);
});
}
创建新会话并导航到聊天页面,可选附带初始消息。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| agentId | string | 否 | 目标 agent 的 ID(UUID,与 agentSlug 二选一,同时提供时以 agentId 为准) |
| agentSlug | string | 否 | 目标 agent 的 slug(即 agent 目录名,形如 writer-a1b2c3,可在 agent 右键菜单「复制 ID」获取),作为 agentId 的替代 |
| message | string | 否 | 初始消息内容 |
| float | boolean | 否 | 为 true 时在浮窗中打开新会话,而非导航到聊天页 |
// 通过 agent ID 创建会话
window.parent.postMessage({
type: "spherse:action",
action: "createSession",
params: {
agentId: "my-writer",
message: "请帮我扩展这段世界观设定"
}
}, "*");
也可以用人类可读的 agent slug(即 agent 目录名,形如 writer-a1b2c3,可在 agent 右键菜单「复制 ID」获取)替代 ID(二者二选一):
// 通过 agent slug 创建会话
window.parent.postMessage({
type: "spherse:action",
action: "createSession",
params: {
agentSlug: "writer-a1b2c3",
message: "请帮我扩展这段世界观设定"
}
}, "*");
在 Content Browser 中打开指定项目文件。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| path | string | 是 | 项目内相对文件路径 |
window.parent.postMessage({
type: "spherse:action",
action: "openFile",
params: {
path: "world/characters/主角设定.md"
}
}, "*");
向已有会话发送消息并导航到聊天页面。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| sessionId | string | 是 | 目标会话 ID(HtmlCard 中可用 window.__SPHERSE__.sessionId 获取当前会话) |
| message | string | 是 | 消息内容 |
| float | boolean | 否 | 为 true 时确保该会话在浮窗中显示再发送消息 |
sendMessage 支持 request-response 模式:传入 requestId(配合下文 Promise wrapper)可获取发送结果。
{ ok: true }agent_end):{ ok: false, data: { error: "session_busy" } }。此时消息不会被发送,卡片应提示用户稍后重试,或在会话空闲后再发。// 向当前会话发消息(HtmlCard 内)
const rt = window.__SPHERSE__;
window.parent.postMessage({
type: "spherse:action",
action: "sendMessage",
params: {
sessionId: rt.sessionId,
message: "请继续分析这个角色的动机"
}
}, "*");
带结果反馈的写法(推荐,可感知 busy 状态):
try {
await spherseCall("sendMessage", { sessionId: rt.sessionId, message: "继续" });
// 发送成功
} catch (e) {
// 会话忙碌或发送失败,提示用户稍后重试
}
将会话显示为浮窗。一次只能有一个浮窗,新的浮窗会自动替换旧的。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| sessionId | string | 是 | 要浮窗的会话 ID |
window.parent.postMessage({
type: "spherse:action",
action: "floatSession",
params: {
sessionId: "session-abc123"
}
}, "*");
关闭当前浮窗。
无需参数。
window.parent.postMessage({
type: "spherse:action",
action: "unfloatSession",
params: {}
}, "*");
触发一个自定义事件,用于激活配置了「事件触发器」的 agent。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| eventName | string | 是 | 自定义事件名(不能以 sp: 开头,该前缀为系统保留) |
| payload | string | 否 | 事件附带的数据,会通过 {{payload}} 注入触发器的消息模板(是否使用取决于 trigger 配置) |
window.parent.postMessage({
type: "spherse:action",
action: "emitAgentTriggerEvent",
params: {
eventName: "daily-review",
payload: "第3章"
}
}, "*");
例如 trigger 配置了消息模板 "请回顾 {{payload}} 的写作进度",触发时会拼接为 "请回顾 第3章 的写作进度"。
事件触发后,所有匹配该事件名且启用的触发器将自动执行(创建会话或向已有会话发送消息)。此操作为单向触发,无返回值。
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: sans-serif; padding: 2rem; }
.card { padding: 1rem; border: 1px solid #e5e5e5; border-radius: 0.5rem; margin-bottom: 1rem; cursor: pointer; }
.card:hover { background: #f5f5f5; }
</style>
</head>
<body>
<h1>我的世界观</h1>
<div class="card" onclick="openCharacters()">角色档案</div>
<div class="card" onclick="startWriting()">开始写作</div>
<script>
function openCharacters() {
window.parent.postMessage({
type: "spherse:action",
action: "openFile",
params: { path: "world/characters.md" }
}, "*");
}
function startWriting() {
window.parent.postMessage({
type: "spherse:action",
action: "createSession",
params: { agentId: "writer", message: "开始新的写作会话" }
}, "*");
}
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<body>
<h2>势力关系图</h2>
<p>北境王国与南方联盟之间维持着脆弱的和平。</p>
<button onclick="analyzeWith('historian')">历史分析</button>
<button onclick="analyzeWith('strategist')">战略推演</button>
<script>
function analyzeWith(agentId) {
window.parent.postMessage({
type: "spherse:action",
action: "createSession",
params: {
agentId: agentId,
message: "请分析北境王国与南方联盟的关系动态"
}
}, "*");
}
</script>
</body>
</html>
Data action 支持在 HTML 内读写持久化的 key-value 数据。数据存储在与 HTML 文件同级的 .data.json 文件中。
Data action 使用 request-response 模式,通过 requestId 匹配响应。
将以下代码嵌入 HTML <script> 中即可使用 await 方式调用:
function spherseCall(action, params) {
return new Promise((resolve, reject) => {
const requestId = "r" + Date.now() + Math.random().toString(36).slice(2);
const timeout = setTimeout(() => { cleanup(); reject(new Error("spherse timeout")); }, 10000);
const handler = (e) => {
if (e.data?.type === "spherse:response" && e.data.requestId === requestId) {
cleanup();
e.data.ok ? resolve(e.data.data) : reject(new Error(e.data.data?.error || "spherse data error"));
}
};
function cleanup() { clearTimeout(timeout); window.removeEventListener("message", handler); }
window.addEventListener("message", handler);
window.parent.postMessage({ type: "spherse:action", action, params, requestId }, "*");
});
}
读取指定 key 的值。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| file | string | 是 | 数据文件路径(项目内相对路径,如 world/game.data.json) |
| key | string | 是 | 要读取的 key |
返回值:对应的 value(任意 JSON 类型),key 不存在时返回 null。
const score = await spherseCall("data.get", { file: "world/game.data.json", key: "score" });
写入 key-value,已存在的 key 覆盖。文件不存在时自动创建。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| file | string | 是 | 数据文件路径 |
| key | string | 是 | key 名 |
| value | any | 是 | 任意 JSON 可序列化值 |
返回值:写入后的 value。
await spherseCall("data.set", { file: "world/game.data.json", key: "score", value: 100 });
await spherseCall("data.set", { file: "world/game.data.json", key: "player", value: { name: "Alice", hp: 80 } });
删除指定 key。key 不存在时也返回成功。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| file | string | 是 | 数据文件路径 |
| key | string | 是 | 要删除的 key |
返回值:true。
await spherseCall("data.delete", { file: "world/game.data.json", key: "score" });
{HTML文件名}.data.json,放在 HTML 文件的同级目录
world/game.html → world/game.data.jsonwelcome.html → welcome.data.json{ "key1": value1, "key2": value2 }a.b.c)假设 HTML 文件为 world/game.html,数据文件约定为 world/game.data.json。
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body>
<h1>游戏存档</h1>
<p>当前分数:<span id="score">--</span></p>
<button onclick="loadScore()">读取存档</button>
<button onclick="saveScore()">保存分数</button>
<script>
const DATA_FILE = "world/game.data.json";
function spherseCall(action, params) {
return new Promise((resolve, reject) => {
const requestId = "r" + Date.now() + Math.random().toString(36).slice(2);
const timeout = setTimeout(() => { cleanup(); reject(new Error("spherse timeout")); }, 10000);
const handler = (e) => {
if (e.data?.type === "spherse:response" && e.data.requestId === requestId) {
cleanup();
e.data.ok ? resolve(e.data.data) : reject(new Error(e.data.data?.error || "spherse data error"));
}
};
function cleanup() { clearTimeout(timeout); window.removeEventListener("message", handler); }
window.addEventListener("message", handler);
window.parent.postMessage({ type: "spherse:action", action, params, requestId }, "*");
});
}
async function loadScore() {
const score = await spherseCall("data.get", { file: DATA_FILE, key: "score" });
document.getElementById("score").textContent = score ?? "无存档";
}
async function saveScore() {
await spherseCall("data.set", { file: DATA_FILE, key: "score", value: 42 });
document.getElementById("score").textContent = "42(已保存)";
}
</script>
</body>
</html>
data.get)位于白名单内,不受频率限制,便于交互式页面频繁读取状态postMessage,零依赖sendMessage 与 Data action(data.get/set/delete)例外,支持通过 requestId 获取返回值(sendMessage 在目标会话忙碌时返回 { ok: false, data: { error: "session_busy" } }).data.json 文件中