自定义主页开发文档

1. 主题令牌总览

启动器与官网统一使用一套 CSS 自定义属性(design tokens)。:root 为浅色,[data-theme='dark'] 为深色;两侧必须成对提供同名变量,切换主题时只需切换 data-theme,无需改动任何组件样式。

文字 --text-*

令牌暗色值浅色值用途
--text-primaryrgba(255,255,255,0.92)rgba(0,0,0,0.9)正文、标题等主要文字
--text-secondaryrgba(255,255,255,0.62)rgba(0,0,0,0.58)次要说明、标签、注释
--text-tertiaryrgba(255,255,255,0.4)rgba(0,0,0,0.38)占位符、禁用态、极弱信息

玻璃材质 --glass-*

令牌暗色值浅色值用途
--glass-bgrgba(28,28,36,0.5)rgba(255,255,255,0.52)标准玻璃卡片底色(配 blur(36px) saturate(180%))
--glass-bg-strongrgba(32,32,42,0.62)rgba(255,255,255,0.64)强调层 / 首屏 hero / 不支持磨砂时的回退底色
--glass-bg-softrgba(24,24,32,0.34)rgba(255,255,255,0.34)轻量玻璃层(配 blur(24px))
--glass-borderrgba(255,255,255,0.14)rgba(255,255,255,0.6)玻璃边缘描边
--glass-highlightrgba(255,255,255,0.2)rgba(255,255,255,0.8)顶部高光渐变起点(环境光反射)
--glass-shadow0 24px 60px -24px rgba(0,0,0,0.65)0 24px 60px -24px rgba(15,23,42,0.35)玻璃悬空投影

填充与状态色 --fill-*

令牌暗色值浅色值用途
--fill-primary#0a84ff主按钮、选中态、焦点描边(明暗共用,iOS 蓝)
--fill-primary-hover#0a78f0主按钮 hover
--fill-secondaryrgba(255,255,255,0.08)rgba(0,0,0,0.06)次要按钮 / 输入框 / 列表行底色
--fill-secondary-hoverrgba(255,255,255,0.14)rgba(0,0,0,0.1)次要按钮 / 列表行 hover
--fill-danger#ff453a危险操作(删除 / 拒绝)与错误文案
--fill-success#30d158成功提示 / 通过状态

分隔与遮罩

令牌暗色值浅色值用途
--chip-bgrgba(255,255,255,0.08)rgba(0,0,0,0.05)标签 / 徽章 / 分段控件底色
--dividerrgba(255,255,255,0.1)rgba(0,0,0,0.08)细分隔线、输入框描边
--scrimrgba(0,0,0,0.55)rgba(0,0,0,0.4)弹窗 / 抽屉背后的遮罩层

补充:另有 --surface-flat、--bg-flat 两个纯色表面令牌,供「极简(二维化)」模式关闭玻璃材质时使用;自定义主题可按需覆盖,不影响本页其余令牌。

2. 背景预设与光斑规范

背景由「三色线性渐变」(--bg-1/2/3)+「三个漂移光斑(blob)」构成,由 <html data-bg="…"> 切换预设。每个预设都必须同时给出暗色与浅色两套值,避免浅色主题配深色底。

渐变变量

预设暗色 --bg-1/2/3浅色 --bg-1/2/3
midnight(默认)#04060f → #0b1330 → #10101c#eef1fa → #e8ecf8 → #f2f0fb
sunset#2a0a14 → #7a2a1e → #d4762a#fff4ec → #ffe9d9 → #ffe2cf
forest#04120f → #0a2e24 → #144d3a#eefaf3 → #e4f6ec → #eef9e8
rose#2a0a1c → #6b1740 → #c94d6e#fff0f6 → #ffe6f0 → #fbeafc
mono#0d0d10 → #1c1c22 → #2a2a30#f4f4f6 → #ededf0 → #e6e6ea
.app-background {
  position: fixed; inset: 0; z-index: -1; overflow: hidden;
  /* 暗色 160deg;浅色 165deg */
  background: linear-gradient(160deg, var(--bg-1) 0%, var(--bg-2) 45%, var(--bg-3) 100%);
}

光斑(blob)规范

光斑尺寸定位动画
.blob-152vw × 52vwleft:-14vw; top:-16vwdrift1 26s ease-in-out infinite alternate
.blob-244vw × 44vwright:-12vw; top:-8vwdrift2 32s ease-in-out infinite alternate
.blob-340vw × 40vwleft:24vw; bottom:-20vwdrift3 38s ease-in-out infinite alternate

通用样式:position:absolute; border-radius:50%; filter:blur(90px); will-change:transform;,不透明度由 --blob-opacity 控制(暗色 0.4 / 浅色 0.55)。关键帧终态位移/缩放为:drift1 translate3d(10vw,8vh,0) scale(1.18)、drift2 translate3d(-8vw,12vh,0) scale(0.96)、drift3 translate3d(-6vw,-10vh,0) scale(1.16)。

每个预设可覆盖三个光斑的 background(radial-gradient(circle, 主色 0%, 次色 60%+, transparent 72%~76%))以与背景基调协调。例如 sunset 为 #ff9f45→#ff375f / #ffd23f→#ff7a00 / #ff5c8a→#a855f7;forest 为 #34d399→#0ea5e9 / #22c55e→#14b8a6 / #84cc16→#10b981;rose 为 #fb7185→#f43f5e / #ec4899→#d946ef / #f472b6→#a855f7;mono 为 #64748b→#475569 / #94a3b8→#64748b / #cbd5e1→#94a3b8;默认 midnight 为 #4f7cff→#7a5cff / #ff5c8a→#c84dff / #2fd0c0→#2f7bff。

3. 如何做一套自定义主题

最小令牌集合

一套可用主题至少需要覆盖以下令牌(其余可留空继承默认值):文字 --text-primary/secondary/tertiary;玻璃 --glass-bg、--glass-border;填充 --fill-primary、--fill-secondary;分隔 --divider。背景预设的 --bg-1/2/3 决定整体氛围。

明暗成对提供

同一套主题必须同时提供 :root(浅色)与 [data-theme='dark'](深色)两组值,命名一一对应。切勿只做单侧,否则切换模式会出现低对比度或白底白字。

对比度与命名建议

  • 正文文字与所在背景的对比度建议 ≥ 4.5:1,次要文字 ≥ 3:1。
  • 玻璃底色保持半透明(alpha 0.34 ~ 0.64),过实会失去 Liquid Glass 质感,过虚则看不清内容。
  • --fill-primary 用于主操作,务必与白色文字形成足够对比(深色系主题可沿用 iOS 蓝或换成更亮的强调色)。
  • 令牌命名沿用既有前缀(--text- / --glass- / --fill-),不要自造新前缀,以保证组件样式可复用。

减弱动效

当用户开启「减弱动效」(prefers-reduced-motion: reduce 或 <html data-reduced-motion>)时,请关闭光斑漂移与入场动画,并取消按钮缩放:

@media (prefers-reduced-motion: reduce) {
  .app-background .blob { animation: none !important; }
  .hero, .timeline { animation: none !important; }
  .btn { transition: opacity 160ms ease; }
  .btn:active { transform: none; }
}

同理,prefers-reduced-transparency: reduce 时应关闭 backdrop-filter 并回退到 --glass-bg-strong 实色。

4. 主页脚本如何消费主题

主页脚本运行在宿主注入的 <iframe> 沙箱中。宿主会通过白名单 API hc.theme.get() 暴露当前主题,并把主题令牌以 CSS 变量注入到 iframe 的根元素上,脚本直接使用即可自动适配明暗与背景预设。

hc.theme.get() 返回字段

字段类型含义
mode'light' | 'dark'当前实际生效的明暗模式(已根据偏好与系统解析后的结果)
setting'light' | 'dark' | 'system'用户在设置中选择的主题偏好;system 表示跟随系统
accentColorstring用户自定义主题色(十六进制,如 #0a84ff)
backgroundstring背景预设键:midnight / sunset / forest / rose / mono
reducedMotionboolean用户是否启用「减弱动效」;为 true 时脚本应关闭装饰性动画
debugboolean启动器是否处于 Debug 模式(该模式下才会推送运行日志)

宿主注入 iframe 的 CSS 变量

以下变量名必须与宿主完全一致,脚本应直接引用(它们由宿主写入 iframe 根元素):

变量对应语义
--hc-text-primary主文字色
--hc-text-secondary次要文字色
--hc-text-tertiary极弱文字色
--hc-fill-primary主强调色 / 主按钮底色
--hc-fill-secondary次级填充(标签底、悬停底色)
--hc-fill-danger危险操作色(删除 / 结束游戏)
--hc-fill-success成功状态色
--hc-glass-bg玻璃卡片底色
--hc-glass-border玻璃边缘描边
--hc-divider分隔线色
--hc-scrim遮罩层色
--hc-accent用户自定义主题色(等价于 hc.theme.get().accentColor)
:root {
  color: var(--hc-text-primary);
  background: transparent;        /* 透出宿主的渐变背景 */
}
.card {
  background: var(--hc-glass-bg);
  border: 1px solid var(--hc-glass-border);
  backdrop-filter: blur(36px) saturate(180%);
}
.btn { background: var(--hc-fill-primary); color: #fff; }
hr { border: 0; border-top: 1px solid var(--hc-divider); }

建议:主页脚本所有颜色优先取自 --hc-* 变量,不要写死明暗固定色;这样用户切换明暗模式或更换背景预设时无需刷新即可正确呈现。

形状与材质令牌(--hc-radius-* / --hc-surface-*)

除颜色外,宿主还把当前皮肤的圆角与材质也作为令牌注入。它们与颜色令牌一样由宿主写入 iframe 根元素,脚本用 var() 引用即可——用户换皮肤(Material / GitHub / WinUI3 / WinUI2 / Minecraft…)时无需刷新、无需改代码,圆角与磨砂/实色自动跟着变。

变量含义
--hc-radius-control控件圆角(按钮 / 输入框 / 芯片 / 下拉)
--hc-radius-card卡片与浮层圆角
--hc-radius-full全圆角(药丸 / 开关滑块 / 状态点)
--hc-surface-material材质关键字:glass(磨砂)/ mica / acrylic / solid(实色)。可作为给 backdrop-filter 兜底的开关
--hc-surface-blur卡片背景的实际模糊值(含饱和),如 blur(36px) saturate(180%);实色皮肤为 none
--hc-surface-border-width卡片描边宽度;立体倒角皮肤(如 Minecraft)为 2px,其余多为 1px 或 0px

各皮肤的实际取值(脚本无需硬记,直接 var() 即可):

皮肤--hc-radius-control--hc-radius-card--hc-surface-material--hc-surface-blur--hc-surface-border-width
Material(默认)12px24pxglassblur(36px) saturate(180%)1px
Win1012px26pxsolidnone1px
GitHub6px12pxsolidnone1px
WinUI34px8pxmicanone1px
WinUI20px2pxacrylicnone1px
Minecraft0px0pxsolidnone2px
.card {
  border-radius: var(--hc-radius-card, 22px);          /* 跟随皮肤圆角 */
  border: var(--hc-surface-border-width, 1px) solid var(--hc-glass-border);
  background: var(--hc-glass-bg);
  backdrop-filter: var(--hc-surface-blur, blur(36px) saturate(180%)); /* 实色皮肤为 none,天然失效 */
}
.btn { border-radius: var(--hc-radius-control, 12px); } /* 跟随控件圆角 */
.pill { border-radius: var(--hc-radius-full, 999px); }

兜底:宿主令牌缺失时沙箱会注入中性默认值(控件 12px / 卡片 24px / 全圆角 999px、实色材质),所以没写 var() 的旧脚本与新皮肤都不会「开天窗」,但也不会自适应——新脚本请一律走 var(--hc-…, 回退值)。

5. 主页脚本开发要点

单文件与元信息块

主页脚本是单个 HTML 文件,元信息块 <!--@hcpage … --> 位于文件开头(约定位置,实际可出现在任意位置),块内为一段 JSON:

<!--@hcpage
{
  "id": "",
  "name": "示例主页",
  "author": "作者",
  "version": "1.0.0",
  "description": "一句话描述",
  "minLauncher": "0.4.0"
}
-->
字段说明
id编号。提交时留空或任意值均可;服务端会原字节替换为分配到的 HC-XXXXXX。若块内没有该字段,服务端会在 { 后插入。
name脚本名称(市场展示用)
author作者名
version语义化版本号,如 1.0.0
description一句话描述
minLauncher要求的最低启动器版本(如 0.4.0),低于该版本的启动器将拒绝运行

API 白名单

沙箱只向脚本暴露以下经宿主审核的 window.hc 接口(全部为 Promise,10 秒无响应即超时)。白名单之外的宿主能力一律不可用;脚本不应假设存在其他 hc.* 接口。需要网络等额外能力时,必须由用户显式授权。

方法返回说明
hc.state()object宿主最近一次推送的快照(与 hc.onUpdate 收到的对象相同,见下表)
hc.onReady(fn)取消订阅函数SDK 就绪时触发一次
hc.onUpdate(fn)取消订阅函数宿主快照变化时触发(内存 / 账号 / 版本 / 运行状态 / 主题)
hc.onLog(fn)取消订阅函数接收运行日志增量;仅 Debug 模式下有数据
hc.system.memory(){ total, used, free }系统内存(MB)
hc.settings.memory.get()number当前分配给游戏的内存(MB)
hc.settings.memory.set(mb)number修改分配内存,返回宿主夹取后的实际值
hc.account.current(){ name, id, avatarUrl, authType, siteName } | null当前账号;未登录为 null。siteName 为账号的站点归属名:第三方登录时自动获取(如 LittleSkin、馋猫认证中心,取不到时回落认证域名),非第三方账号为空串
hc.account.avatar()string头像地址;无头像为空串
hc.versions.list()InstalledVersion[]当前版本目录下的已安装实例列表,见下方字段表
hc.versions.selected()string当前选中版本的 id
hc.versions.select(id)string切换选中版本(并持久化);id 不在当前版本目录的列表中时抛错
hc.versions.info(){ id, number, loader, loaderName } | null选中版本的加载器与版本号摘要;无版本为 null
hc.versions.loader()string选中版本的加载器显示名(Fabric / Quilt / Forge / NeoForge);无加载器返回「原版」,无选中版本同样返回「原版」
hc.versions.number()string选中版本的版本号(Minecraft 版本,如 1.21.4);无选中版本返回空串
hc.versionDirs.list()VersionDir[]版本目录列表(默认目录在最前),见下方字段表
hc.versionDirs.selected()string当前生效的版本目录 id(默认目录为 'default')
hc.versionDirs.select(id)string切换版本目录(并持久化),返回切换后的 id;id 不在列表中时抛错。切换后 hc.versions.list() / 快照 versions 会收敛到新目录
hc.launcher.version()string启动器版本号(如 0.4.11);取不到时为空串
hc.game.launch(){ ok, versionId }启动选中版本;无账号 / 无已安装版本时抛错
hc.game.stop(){ ok, stopped }结束当前运行的游戏;无运行中进程时 stopped 为 false
hc.game.state(){ state, running, starting, busy, pid, versionId, debug }运行状态
hc.log.info(msg) / .warn(msg) / .error(msg)null写入启动器日志(仅 Debug 模式可见,单条截断 4000 字符)
hc.theme.get()object见上一节「hc.theme.get() 返回字段」
hc.openExternal(url)string用系统浏览器打开链接;需脚本已获联网授权,且仅允许 http(s)
hc.model3d.skin(){ skinUrl, capeUrl, skinModel } | null当前账号的 3D 模型皮肤数据;离线账号 / 未设置皮肤为 null。见「3D 玩家模型容器」
hc.model3d.mount(target, options)控制器对象在目标元素内挂载可自定义尺寸、自带旋转的 3D 玩家模型。见「3D 玩家模型容器」

快照字段(hc.state() / hc.onUpdate)

字段类型含义
memory{ total, used, free } | null系统内存(MB)
allocatedMemorynumber分配给游戏的内存(MB)
account{ name, id, avatarUrl, authType, siteName } | null当前账号;siteName 为账号的站点归属名(第三方自动获取,非第三方为空串)
versionsInstalledVersion[]当前版本目录下的已安装实例列表
selectedVersionIdstring选中版本 id
selectedVersion{ id, number, loader, loaderName } | null选中版本的加载器与版本号摘要
versionDirsVersionDir[]版本目录列表(默认目录在最前)
selectedVersionDirIdstring当前生效的版本目录 id(默认目录为 'default')
launcherVersionstring启动器版本号
launch{ state, running, starting, busy, pid, debug }运行状态与是否 Debug 模式
theme{ mode, setting, accentColor, background, reducedMotion }当前主题(强调色 / 背景预设 / 减弱动效)

InstalledVersion 字段(hc.versions.list())

字段类型含义
idstring实例 id(选中版本使用此值)
mcVersionstring对应的 Minecraft 版本号
loaderstring | null加载器标识;原版为 null
worldsstring[]存档目录名
servers{ name, address }[]服务器列表

VersionDir 字段(hc.versionDirs.list())

字段类型含义
idstring目录唯一 id(切换版本目录、写入 hc.versionDirs.select 时使用此值);默认目录固定为 'default'
pathstring目录绝对路径(默认目录即设置里的「默认版本列表目录」)
aliasstring用户设置的别名;未设置为空串
labelstring展示名:优先别名,默认目录无别名时为「默认版本列表目录」,其余回退目录名;可直接用于渲染
isDefaultboolean是否为默认目录(默认目录不可删除)
hc.versions.loader().then(function (loader) {
  document.getElementById('loader').textContent = loader;   // 「原版」 / 「Fabric」 …
});

hc.versions.number().then(function (ver) {
  document.getElementById('ver').textContent = ver;         // 「1.21.4」
});

hc.launcher.version().then(function (v) {
  document.getElementById('launcher').textContent = '启动器 ' + v;  // 「启动器 0.4.11」
});

// 或直接从快照取,无需再次请求:
hc.onUpdate(function (s) {
  var info = s.selectedVersion;   // null 表示没有已安装版本
  if (info) console.log(info.loaderName, info.number);
  console.log('启动器版本', s.launcherVersion);
});

版本列表与选中版本

版本(实例)列表由启动器扫描当前版本目录得到,脚本只能读取列表与切换选中项,不能安装或删除实例(安装请引导用户去「资源下载」页)。列表内容随当前版本目录收敛——切换版本目录后,hc.versions.list() 与快照 versions 都会变成新目录下的实例;详见下一节「版本目录」。

接口行为
hc.versions.list()返回当前版本目录下全部已安装实例(InstalledVersion[],字段见上表),按实例 id 字典序排列,与内置页一致;空数组表示该目录尚未安装任何实例
hc.versions.selected()返回当前选中实例的 id;当前版本目录下没有任何已安装实例时返回空串
hc.versions.select(id)切换到指定实例并立即持久化,返回切换后的 id;id 不在当前版本目录的已安装列表中时抛错 版本不可用:<id>
  • 与内置界面共享:选中版本保存在启动器设置里,脚本调用 select 后,内置「启动游戏」界面同样会切到该实例;反之亦然。
  • 自动回落:若持久化的 id 已不在当前版本目录的已安装列表中(例如切换了目录或实例被卸载),宿主会自动回落到列表第一项并写回设置,因此 selected() 不会返回一个不存在的 id。
  • hc.versions.select('') 一定失败(空串不在列表中),切换前请先用 list() 校验。
// 渲染版本下拉框,并让用户切换
hc.versions.list().then(function (list) {
  var sel = document.getElementById('ver');
  sel.innerHTML = list.map(function (v) {
    return '<option value="' + v.id + '">' + v.mcVersion + ' · ' + (v.loader || '原版') + '</option>';
  }).join('');
  return hc.versions.selected();
}).then(function (current) {
  document.getElementById('ver').value = current;   // 与启动器保持一致
});

function onPick(id) {
  hc.versions.select(id)
    .then(function (next) { console.log('已切换选中版本:', next); })
    .catch(function (e) { console.error(e.message); });
}

// 快照里已带列表与摘要,也可不调用接口:
hc.onUpdate(function (s) {
  console.log('已安装', s.versions.length, '个实例,当前选中', s.selectedVersionId);
});

版本目录(多目录切换)

启动器支持多个版本目录:每个目录都是一份独立的「版本列表根目录」(其下各有自己的 versions/)。脚本可以通过 hc.versionDirs.* 列出这些目录并切换当前目录;切换后,hc.versions.list()、hc.versions.selected() 以及快照 versions / selectedVersionId 都会立即收敛到新目录,hc.versions.select() 也只能指定当前目录内的实例。

接口行为
hc.versionDirs.list()返回版本目录列表(VersionDir[],字段见上表),默认目录始终在最前;默认目录的 id 固定为 'default'
hc.versionDirs.selected()返回当前生效的版本目录 id
hc.versionDirs.select(id)切换到指定版本目录并立即持久化,返回切换后的 id;id 不在列表中时抛错 版本目录不可用:<id>。切换后宿主会重新扫描新目录,versions 快照随之刷新
  • 与内置界面共享:当前版本目录保存在启动器设置里,脚本调用 select 后,内置「启动游戏」与「版本」界面同样切到该目录;反之亦然。
  • 版本列表从属于目录:不存在「跨目录的全量版本列表」。要启动某个实例,需先切到它所在的版本目录,再通过 hc.versions.select(id) 选中它。
  • 只读能力:脚本不能新增 / 删除版本目录或修改别名(这些只能在「设置」与「版本」界面由用户操作),hc.versionDirs.* 仅提供列表与切换。
  • 展示名:优先使用 label 字段渲染(已按别名 / 默认名 / 目录名算好),需要更细的信息时再用 path 与 alias。
// 渲染版本目录下拉框,并让用户切换
hc.versionDirs.list().then(function (dirs) {
  var sel = document.getElementById('dir');
  sel.innerHTML = dirs.map(function (d) {
    return '<option value="' + d.id + '">' + d.label + '</option>';
  }).join('');
  return hc.versionDirs.selected();
}).then(function (current) {
  document.getElementById('dir').value = current;
});

function onPickDir(id) {
  // 切换目录后,版本列表会随快照更新到新目录
  hc.versionDirs.select(id)
    .then(function (next) { console.log('已切换版本目录:', next); })
    .catch(function (e) { console.error(e.message); });
}

// 快照里同时带目录列表、当前目录与当前目录下的版本
hc.onUpdate(function (s) {
  console.log('当前目录', s.selectedVersionDirId, '共', s.versionDirs.length, '个目录');
  console.log('该目录下有', s.versions.length, '个实例');
});

启动与结束游戏

启动器对游戏进程只提供「启动」与「结束」两个动作,另有运行状态查询。

接口返回说明
hc.game.launch(){ ok, versionId }启动当前选中版本(使用的内存 / 当前版本目录 / Java 路径均取用户设置);见下方前置条件与注意事项
hc.game.stop(){ ok, stopped }结束当前运行中的游戏进程;没有运行中进程时返回 { ok: true, stopped: false },不报错,便于幂等调用
hc.game.state(){ state, running, starting, busy, pid, versionId, debug }运行状态;state 取值见下表
state含义
starting正在准备启动(校验 / 补齐资源等)
downloading正在下载启动所需文件
launching已拉起进程,等待进入游戏
running游戏正在运行(running 为 true)
exited游戏已退出
error启动或运行出错
null本次会话尚未启动过
  • 前置条件:未登录账号时 launch() 抛 尚未登录账号,请先在「账号」页登录;没有任何已安装实例时抛 没有已安装的游戏版本,请先在「资源下载」页安装。
  • Java 检查:若所选实例要求的 Java 不兼容,启动器会先弹出「安装 Java」提示,此时 launch() 会提前返回 { ok: true, versionId } 但游戏并未开始。请以 launch.state 是否变为 running 作为「真的启动成功」的判据,不要只看返回值。
  • 结束是强制终止:stop() 直接结束进程,游戏不会走正常的退出保存流程;请在界面上给出明确提示,避免玩家丢失进度。
  • 状态推送:优先用 hc.onUpdate(function (s) { s.launch }) 被动接收变化(内容为 { state, running, starting, busy, pid, debug }),比轮询更省资源。

关于「暂停」:启动器不提供暂停 / 恢复游戏进程的能力(Minecraft 进程无法可靠挂起)。只有「启动」与「结束」两个动作;需要停止游戏的脚本请调用 hc.game.stop(),并注意它是强制终止。

// 启动按钮:按状态禁用 + 给出可读错误
function startGame() {
  hc.game.launch()
    .then(function () { /* 仅表示请求已受理,真正开跑见 s.launch.state === 'running' */ })
    .catch(function (e) { alert(e.message); });
}

function stopGame() {
  hc.game.stop().then(function (r) {
    if (!r.stopped) console.log('当前没有运行中的游戏');
  });
}

// 用状态驱动按钮
hc.onUpdate(function (s) {
  var run = s.launch.running;
  var starting = s.launch.starting;
  document.getElementById('start').disabled = run || starting;
  document.getElementById('stop').disabled = !run && !starting;
  document.getElementById('state').textContent = s.launch.state || '未启动';
});

3D 玩家模型容器

宿主提供一个纯 CSS 3D 玩家模型容器:脚本可把它挂载到自己的任意元素上,并自定义尺寸比例;模型自带旋转——静止时缓慢自转,按住可像转盘一样拖动旋转到任意角度。模型在脚本自己的 DOM 内构建,不依赖任何外部 3D 库,也不访问宿主 DOM。

接口返回说明
hc.model3d.skin(){ skinUrl, capeUrl, skinModel } | null当前账号的皮肤贴图地址、披风贴图地址与模型类型('classic' | 'slim');离线账号或账号未设置皮肤时为 null
hc.model3d.mount(target, options)控制器对象(同步返回)在 target(元素或选择器字符串)内挂载 3D 模型;options 见下表,返回控制器用于改尺寸 / 刷新 / 销毁

options 字段:

字段类型默认含义
sizenumber200模型容器边长(像素),决定整体比例;允许 40–1024,越界自动夹取
autoSpinbooleantrue是否自动缓慢自转(拖动时自动暂停,松手继续)
draggablebooleantrue是否允许按住拖动旋转(俯仰限制 ±89°)
showCapebooleantrue是否显示披风(账号有披风贴图时才可见)

返回的控制器对象:

成员说明
el模型根元素
setSize(n)就地改尺寸(返回夹取后的实际值),用于实现「缩放滑块」等
refresh()重新拉取皮肤数据并重建(账号切换 / 换肤后调用)
destroy()销毁模型并从 DOM 移除
  • 仅正版 / 第三方账号有皮肤:离线账号或账号未设置皮肤时 hc.model3d.skin() 返回 null,模型显示「暂无皮肤」占位。
  • 皮肤贴图自动放行:皮肤 / 披风来自启动器自身使用的官方或认证站源,宿主会自动放行其 CSP img-src 并在运行时探针中豁免——脚本无需为此申请联网授权,也不会被判定为外链。
  • 兼容现代与旧版皮肤:自动识别 64×64 现代皮肤(含细手臂 slim 与各部位外层)与 64×32 旧版皮肤,脚本无需处理。
  • 尺寸自定义:初始用 options.size,运行期用 setSize() 调整,两者按同一套比例缩放,观感一致。
// 在 #skin 容器内挂一个 220px、自带旋转的 3D 模型(无皮肤时显示占位)
var model = hc.model3d.mount('#skin', { size: 220, showCape: true });

// 用滑块实时改大小(例如实现「模型缩放」)
document.getElementById('zoom').addEventListener('input', function (e) {
  model.setSize(Number(e.target.value));   // 40 ~ 1024
});

// 想先判断是否有皮肤再决定布局:
hc.model3d.skin().then(function (skin) {
  document.getElementById('tip').textContent = skin ? '按住拖动旋转' : '当前账号没有皮肤';
});

// 账号切换 / 换肤后刷新
hc.onUpdate(function () { model.refresh(); });

沙箱限制

  • 无 Node:没有 require/process/fs 等 Node 能力。
  • 无同源:iframe 与宿主不同源,无法访问宿主 DOM、Cookie、本地存储。
  • 默认无网络:默认禁止网络请求,需用户授权后才可发起。
  • 仅可渲染单文件内的 HTML/CSS/JS,无法加载本地文件系统资源。

安全红线(一律拒绝运行)

含有以下任一行为的脚本将被直接拒绝,且账号/投稿可能被限制:

  • 使用 eval 执行动态字符串,或对代码进行混淆 / 伪装以规避审核;
  • 删除文件、格式化磁盘等破坏性命令;
  • 下载可执行文件、压缩包或 .svg 等潜在可执行内容。

编号与 SHA256 联网校验

服务端为脚本分配唯一编号(HC- + 6 位大写十六进制),并对注入编号后的最终文件计算 SHA256。启动器运行前会联网校验:

GET api.php?action=script_verify&id=HC-3F9A2C&sha256=<64位小写hex>

// 编号存在且哈希全等(大小写不敏感)
{ "ok": true, "verified": true, "script": { "id": "HC-3F9A2C", "name": "…", "author": "…", "version": "…" } }
// 编号不存在或哈希不匹配
{ "ok": true, "verified": false, "script": null }

注意:校验基于「注入编号后」的最终文件哈希,故请务必以服务端返回的已编号脚本为准(见下节)。

6. 投稿与审核流程

流程

  1. 在官网「投稿」区或启动器「主页 → 投稿」选择脚本文件,填写名称 / 作者 / 描述 / 版本、开发者邮箱与邮箱验证码,选择可见性;邮箱必须先点「发送验证码」并填入收到的 6 位数字才能真正提交(验证码 10 分钟内有效、单次使用);
  2. 服务端校验(≤512KB、.html/.htm、含可解析的 <!--@hcpage 元信息块),分配编号并注入,重算 SHA256,保存为待审核记录;
  3. 服务端静态安全检测:投稿时先按启动器同款规则扫一遍源码(见上节「安全红线」)。命中拒绝级规则的脚本会被直接退回(返回 400 并说明原因,不分配编号、不落盘);只是链接了外部服务的脚本允许投稿,但外链清单会随记录保存,供后台审核核对;
  4. 响应返回编号、SHA256、risk(safe / warn)与外链清单,以及「已编号脚本」的 content_base64,请保存该脚本用于本地与联网校验;
  5. 后台人工审核:通过 或 拒绝,两者都可以填写备注;审核备注会随结果邮件一并发送给投稿时绑定的邮箱。待审核记录在后台可直接「下载」查看源码;
  6. 审核结束后系统立即向该邮箱发送结果邮件(含审核备注);通过时邮件里还附带一条永久管理链接,用于后续自助管理。公开脚本通过后上架主页市场,可被下载;私密脚本通过后仅保留记录与哈希,文件随即删除。

同一 IP 每小时最多投稿 5 次,超出将返回 429 限流提示;验证码另有「同邮箱 60 秒冷却 / 每小时 5 封」与「同 IP 每小时 10 封」的限制。

服务端只做静态文本检测,不会取回外链脚本正文(避免 SSRF 与慢请求);外链 .js 的深度核验由启动器在运行时完成。

开发者后台(管理链接)

审核结束后,系统会向投稿邮箱发送结果邮件,无论通过还是拒绝都会带上后台填写的审核备注。审核通过时,邮件里还会包含一条形如 manage.php?token=… 的永久管理链接。

  • 妥善保存这条链接:它是唯一凭据,不依赖登录,也不必再经过后台;
  • 凭它可以随时:下架 / 重新上架(立即生效,无需再审)、变更公开 · 私密、提交更新(需重新审核)、删除脚本;
  • 变更公开 · 私密:改为私密会立即删除服务端脚本文件(不再托管),之后要重新公开必须走「提交更新」重新上传并等待审核;
  • 提交更新:新版本先进入待审核,线上旧版本在此之前保持可用;审核通过后自动切换到新版本并删除旧文件,被拒则线上版本不受影响;
  • 链接等同于密码,请勿转发给他人;管理链接只在「首次通过审核」那封邮件里给出(后续的更新审核结果邮件不再重复附上),请务必当时就保存好;若邮件丢失或链接失效,请联系站长为你「重置并重发管理链接」——重置后旧链接会立即失效。

脚本下载入口

网站与启动器都通过 script.php?id=<编号> 下载脚本:该入口以附件形式返回内容(浏览器不会把脚本当页面渲染),并顺带把该脚本的下载次数 +1。请勿直接把 scripts/ 下的文件当作网页地址分发。

该入口只对「已上架 + 公开」的脚本开放;下架、私密、待审核与被拒的脚本一律返回 403,即使拿到直链也无法下载。

公开 vs 私密

可见性文件处理适用场景
public 公开文件长期保留在服务端(scripts/<编号>.html),审核通过后出现在主页市场可被下载愿意分享给社区的脚本
private 私密仅用于获取编号与联网校验哈希;审核后文件立即从服务端删除;审核通过后由你自行通过管理链接变更为私密时,同样会立即删除服务端文件自用脚本,仅需编号与联网校验

私密投稿请务必保存服务端返回的「已编号脚本」(content_base64 解码后的文件)。文件删除后无法再从服务端取回,但记录与 SHA256 仍保留,可用于 script_verify 联网校验。

审核标准

  • 元信息块完整、JSON 合法,name/author/version 等信息真实不误导;
  • 无上节「安全红线」所列行为(eval / 混淆、删除文件、下载可执行内容等);
  • UI 使用主题令牌(--hc-*)以适配明暗与背景预设,不写死对比度过低的颜色;卡片圆角与材质应使用 --hc-radius-* / --hc-surface-* 跟随当前皮肤(至少在写死固定圆角/模糊时要说明理由);
  • 不采集用户隐私、不诱导跳转外部站点、不含违规或侵权内容;
  • 功能可正常加载运行,不与宿主沙箱限制冲突。

前往投稿