بنقرة واحدة
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 文件中