编写 HTML 页面 — 数据读写与 App 能力调用

SkillWeb & browsing

You must read this skill before producing any HTML in Spherse. Whenever the user asks to create or modify HTML pages, generate web pages, or build visual presentations (welcome pages, guide homepages, content cards, preview pages, or any other HTML deliverable), be sure to read this skill before wri

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the 编写 HTML 页面 — 数据读写与 App 能力调用 skill

What this skill tells your AI

The instructions your AI receives, as published by mengrru/spherse in packages/presets/skills/spherse-write-html/SKILL.md and read by ahel’s review.

本 skill 只规范 HTML 页面如何读取/写入数据、如何调用 App 内能力,不约束页面视觉风格、布局或代码风格。

强制:声明字符编码

所有 HTML 页面必须在 <head> 中添加 <meta charset="UTF-8">,且尽量靠近 <head> 起始位置,避免中文乱码。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>页面标题</title>
</head>
<body>
  <!-- ... -->
</body>
</html>

强制:不要禁止页面滚动

HtmlCard 在固定高度的 iframe 中渲染(默认 400px,上限 600px)。当页面内容超出卡片高度时,必须能滚动查看全部内容——切勿设置 body { overflow: hidden }html { overflow: hidden },否则超出部分被裁剪、无法滚动,用户看不到完整内容。

/* ❌ 错误:卡片内内容会被裁剪,无滚动条 */
body { overflow: hidden; }

/* ✅ 正确:保持默认(visible)或显式允许滚动 */
body { overflow-y: auto; }

即便不加 overflow:hidden,App 也会在渲染时强制注入 html,body{overflow-y:auto!important} 作为兜底;但请勿依赖兜底,页面自身就应保持可滚动。

区分两种渲染模式

Spherse 中的 HTML 有两种加载方式,决定了数据能否通过 fetch 访问同目录文件:

模式加载方式能否 fetch 同目录文件典型场景
文件模式HTML 落在项目目录,经 preview 路由以 src 加载(真实 origin)✅ 可以Welcome Page、Content Browser 预览、HtmlCard 带 file_path
字符串模式纯 HTML 字符串经 srcDoc 加载(无真实 origin)❌ 不可以Chat HtmlCard 无 file_path

判断方法:如果页面是作为文件写入用户项目目录(而非内联字符串),按「文件模式」处理。

何时将数据与渲染分离

实现一个 HTML 页面前,先判断它属于哪类,决定数据是「外置 JSON」还是「内联进 HTML」:

倾向适用场景做法
外置 JSON(推荐)信息量较大(多条目列表/表格/清单)、预计需要长期维护与更新、数据可能被其它页面复用数据存成独立的 {页面名}.data.json 文件,HTML 只负责渲染并用 fetch 加载数据
内联进 HTML少量一次性展示内容、纯结构展示、字符串模式且无需持久化数据直接写进 <script> 中的 JS 对象或 DOM

外置的好处:数据与结构解耦,后续只改 JSON 即可更新内容,无需触碰 HTML;JSON 也可被其它页面 fetch 复用。

实现时务必用 write 工具分别落盘两个文件:HTML 页面(如 world/atlas.html)和数据文件(如 world/atlas.data.json),不要只写 HTML。

只读展示数据:外置为同目录 JSON(文件模式推荐)

文件模式下,把页面需要的展示数据外置到同目录的 .json 文件,用 fetch 加载。这样数据与结构分离,便于维护和更新,也避免把大段数据塞进 HTML。

约定:数据文件名形如 {页面名}.data.json,与 HTML 同级。

假设页面为 world/atlas.html,数据文件为 world/atlas.data.json

{
  "regions": [
    { "name": "北境", "climate": "寒带", "faction": "守夜人" },
    { "name": "南境", "climate": "亚热带", "faction": "商会联盟" }
  ]
}
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>世界 atlas</title>
</head>
<body>
  <ul id="list"></ul>
  <script>
    // 相对路径基于 HTML 自身 URL 解析,指向同目录的 atlas.data.json
    async function render() {
      const data = await fetch("./atlas.data.json").then((r) => r.json());
      document.getElementById("list").innerHTML = data.regions
        .map((r) => `<li>${r.name}(${r.climate})</li>`)
        .join("");
    }

    spherse.events.on(
      "file:update",
      { path: "./atlas.data.json" },
      render,
    );
    render();
  </script>
</body>
</html>
  • 字符串模式fetch 相对路径会解析到 about:srcdoc 而失败。此时把数据直接内联进 HTML(如 <script> 中的 JS 对象),或改用 ui-sdk 的 data action(见下文)。
  • preview 路由支持 jsoncssjs、图片、字体等常见静态资源类型,同目录的 CSS / JS / 图片同样可用相对路径引用。
  • 需要在 JSON 被外部工具或 agent 修改后自动刷新页面时,用 spherse.events.on("file:update", { path: "./atlas.data.json" }, handler) 订阅。./ / ../ 路径与 fetch 一样基于当前 HTML 的 base 解析;也可以传项目根目录相对路径。完整说明见 spherse-use-ui-sdk skill。

持久化读写:使用 ui-sdk data action

页面需要写入/持久化数据(如表单、进度、勾选状态)时,使用注入的 window.spherse.data.* key-value 接口。请读取 spherse-use-ui-sdk skill 获取完整 API。

要点速览(详情见 spherse-use-ui-sdk):

  • spherse.data.get / spherse.data.set / spherse.data.delete:对 key-value 数据的读写删(均返回 Promise)
  • spherse.data.mutate:执行数据文件 $manifest 声明的业务 mutation 入口(结构性写入用,见下文 $manifest 一节)
  • 数据文件路径通过 file 参数显式指定;推荐命名为 {HTML文件名}.data.json 并与 HTML 同级(如 world/atlas.htmlworld/atlas.data.json),多个页面也可共用一个显式指定的数据文件;字符串模式下 HTML 不是文件,需自行指定一个项目内的 .data.json 路径
  • 数据文件不能放在 .spherse/ 目录下
  • 两种渲染模式都可用(经 App 注入的 SDK,不依赖 fetch

强制:会增长/需要 agent 互动的数据文件必须内嵌 $manifest

判断:数据是静态展示(一次性内容,页面只 fetch 渲染)→ 无需 manifest;数据会随使用增长(清单、记录、游戏存档)或需要 agent 与页面互动读写 → 先加载 spherse-build-data-app skill 设计数据模型、命名 query/mutation 与协作流程,再生成 HTML。

HTML 落地时遵守:

  • 同源生成 HTML、业务数据和 $manifest,字段名与 enum 必须和页面渲染逻辑一致。
  • 页面会执行的每种结构性写入都必须有对应 mutation,并通过 spherse.data.mutate({ file, name, args }) 调用。
  • data.set 仅用于单值、标量或低冲突数据,不得整体回写增长型数组。
  • 页面订阅数据文件的 file:update,在 Agent 或其他页面写入后重新读取并渲染。
  • 业务数据键不得以平台保留的 $ 开头。

跳转到项目内其它文件:openFile

页面中点击跳转/打开项目内的其它文件时,使用 spherse.openFile(在 Content Browser 中打开)。不要用 <a href="..."> 直接链接(iframe 内的链接不会触发 App 导航)。

spherse.openFile("world/characters/主角设定.md");
// 直接以浮窗打开(仅 desktop,web 端回退到主面板)
spherse.openFile({ path: "world/characters/主角设定.md", float: true });

path 为项目内相对路径。spherse 全局对象由 App 自动注入,无需自己引入脚本。完整 API 见 spherse-use-ui-sdk skill。

打开外部链接:openExternalLink

页面中需要打开外部网页(http/https/mailto/tel)时,使用 spherse.openExternalLink 在系统默认浏览器中打开。不要用 <a href="https://..."> 直接链接——iframe 中的原生外链只会在 iframe 内原地跳转,无法跳出 App。

spherse.openExternalLink("https://example.com");

仅 http/https/mailto/tel 协议生效,其它协议会被静默忽略。指向项目内文件请用 spherse.openFile。完整 API 见 spherse-use-ui-sdk skill。

交互式 HtmlCard:将用户选择回传当前会话

当 HTML 作为聊天 HtmlCard 渲染时,可以为用户制作带交互性的卡片——例如让用户在多个选项中勾选,点击「提交」后把选择结果直接作为一条消息发回当前会话,驱动后续对话或 agent 行为。

实现要点:

  1. App 会自动向每个 HTML 注入 window.spherse SDK。
  2. 当 HTML 作为聊天 HtmlCard 渲染时,spherse.runtime 携带当前会话上下文(sessionId/agentId/projectId);加载即使用时用 await spherse.getRuntime()(内部已处理竞态)。
  3. 用户点击提交时,读取 sessionId,组装消息文本,通过 spherse.sendMessage 发送。
  4. spherse.sendMessage 是请求型,会话忙碌时会 reject(session_busy),消息不会发出,应提示用户稍后重试。

spherse.sendMessage / spherse.getRuntime 的完整签名见 spherse-use-ui-sdk skill。

示例:选项卡片,用户勾选后提交,结果回传当前会话。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>选择推进方向</title>
  <style>
    body { font-family: sans-serif; padding: 1rem; }
    label { display: block; margin: 0.4rem 0; cursor: pointer; }
    button { margin-top: 0.8rem; padding: 0.4rem 1rem; }
    #tip { margin-top: 0.5rem; color: #888; min-height: 1.2em; }
  </style>
</head>
<body>
  <h3>请选择接下来要展开的方向</h3>
  <label><input type="checkbox" value="角色背景"> 角色背景</label>
  <label><input type="checkbox" value="势力关系"> 势力关系</label>
  <label><input type="checkbox" value="历史时间线"> 历史时间线</label>
  <button onclick="submit()">提交选择</button>
  <div id="tip"></div>

  <script>
    async function submit() {
      const picks = [...document.querySelectorAll("input:checked")].map((i) => i.value);
      if (picks.length === 0) {
        document.getElementById("tip").textContent = "请至少选择一项";
        return;
      }
      const rt = spherse.runtime;
      if (!rt?.sessionId) {
        document.getElementById("tip").textContent = "未找到当前会话,无法提交";
        return;
      }
      const message = "我选择了展开以下方向:" + picks.join("、");
      try {
        await spherse.sendMessage({ sessionId: rt.sessionId, message });
        document.getElementById("tip").textContent = "已发送";
      } catch (e) {
        document.getElementById("tip").textContent =
          e.message === "session_busy" ? "会话正在生成,请稍后重试" : "发送失败,请重试";
      }
    }
  </script>
</body>
</html>
  • 此模式仅适用于聊天 HtmlCard(Welcome Page / Content Browser 预览中 spherse.runtimenull)。
  • 提交内容应是有意义的、可被会话/agent 理解的自然语言,而非原始参数。

其它 App 能力调用

需要触发 App 内其它能力时,阅读 spherse-use-ui-sdk skill,通过 window.spherse.* 调用。可用方法包括:

  • spherse.createSession(params)Promise<{ sessionId }> — 创建新会话并导航到聊天页;open: false 时只创建不跳转,resolve 返回新会话 ID;可选 name 参数为会话命名(显示在会话列表)
  • spherse.openSession(sessionId) — 打开已有会话并导航,不发消息(只跳转用这个)
  • spherse.sendMessage(params)Promise — 向已有会话发送消息(message 必填;会话忙碌时 reject session_busyopen: false 时静默发送——不跳转,未打开的会话由 server 直接执行)
  • spherse.toast(params) — 弹一条 toast 提示({ variant, message, description? }
  • spherse.emitAgentTriggerEvent(params) — 触发自定义事件,激活匹配的事件触发器
  • spherse.floatSession(id) / spherse.unfloatSession() — 浮窗显示/关闭会话
  • spherse.openFile(path | { path, float }) — 在 Content Browser 打开项目文件;float:true 以浮窗打开(desktop)
  • spherse.openExternalLink(url) — 在系统默认浏览器打开外部链接(http/https/mailto/tel)
  • spherse.data.get/set/delete(params)Promise — key-value 数据读写
  • spherse.api.* — 只读查询项目信息(agents / sessions / content / fileTree,完整 op 清单见 spherse-use-ui-sdk skill)
  • spherse.events.on("file:update", filter, handler) — 订阅指定项目文件的变化信号

spherse 全局对象由 App 自动注入到每个 HTML,无需自己写 <script> 加载或内联 wrapper。

向当前会话发消息:当 HTML 作为聊天 HtmlCard 渲染时,spherse.runtime(或 await spherse.getRuntime())携带当前会话上下文(sessionId/agentId/projectId)。详见 spherse-use-ui-sdk skill 的「运行时上下文」一节。

速查:场景 → 方案

需求方案
声明字符编码<head> 内加 <meta charset="UTF-8">
保持卡片内容可滚动切勿设 body{overflow:hidden};保持默认或 overflow-y:auto(超长内容需可滚动)
信息量大 / 需长期维护的页面数据与渲染分离:外置 {页面名}.data.json,HTML 用 fetch() 读取,分两个文件落盘
文件模式下加载展示数据外置同目录 .json,用 fetch()
字符串模式下加载展示数据数据内联进 HTML,或用 ui-sdk data.get
持久化读写数据ui-sdk data.get / data.set / data.delete;结构性集合写入用 data.mutate
枚举数据文件 key / 批量读取ui-sdk data.keys / data.entries
列出目录内容ui-sdk api.content.listDir
获取文件大小/类型/修改时间ui-sdk api.content.stat
播放音视频HTML <audio src="music.mp3"> / <video src="clip.mp4">(相对路径,支持拖动进度条)
点击打开项目内文件ui-sdk openFile
点击打开外部链接(网页/邮箱)ui-sdk openExternalLink(http/https/mailto/tel),勿用 <a href>
打开/发送 chat 会话ui-sdk createSession(新建,返回 sessionId)/ openSession(只打开已有会话,不发消息)/ sendMessage(发消息)/ floatSession
后台静默创建/驱动会话await createSession({ ..., open: false }) 拿到 sessionId,再 await sendMessage({ sessionId, message, open: false }) 静默执行
弹 toast 提示ui-sdk toast({ variant, message, description? })
触发事件驱动 agent 执行ui-sdk emitAgentTriggerEvent(配合 agent 触发器配置)
交互式卡片 / 向当前会话发消息spherse.runtime.sessionId,调 spherse.sendMessage(如收集用户选择后提交回传,会话忙碌 reject session_busy

Signals

GitHub stars
73
Forks
8
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
spherse-write-html
Source
github.com/mengrru/spherse