# hxh-dev-docs
**Repository Path**: t_tgl/hxh-dev-docs
## Basic Information
- **Project Name**: hxh-dev-docs
- **Description**: No description available
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 1
- **Forks**: 2
- **Created**: 2025-09-18
- **Last Updated**: 2026-09-02
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# 彗星号创意工坊开发文档
创意工坊的每个插件均为一个静态网页,既可以使用原生静态网页技术进行开发,也可以使用任意可编译为静态网页的语言开发,网页之间通过`window.postMessage`进行跨域交互
[全屏特效示例](全屏特效.html)
**若需使用彗星号内置字体,可在`
`中添加以下代码,然后根据option更新事件提供的`fontStyle`修改字体**
```html
```
如果想要覆盖默认字体,参考下面示例。
```css
@font-face {
font-family: 'my-font';
src: url("将上传字体文件后生成的链接放这里");
}
/* 指定元素并修改CSS变量 */
.header {
--font-family: 'my-font';
}
```
## 环境判断
```javascript
const query = new URLSearchParams(location.search)
// 是否处于配置状态
const isSetup = query.get('env') === '1'
if(isSetup) {
//配置时使用模拟数据展示效果
}
```
## 共用类型
* 彗星号事件
```typescript
export interface ApiMessage {
/** 消息分类 */
category: string
/** 消息类型 */
type: string
/** 消息参数 */
args: any[]
}
```
* 创意工坊插件事件
```typescript
export interface PlugMessage {
/** 调用的API名称 */
fun: string
/** API参数,不同API对应不同结构 */
args: unknown
}
```
## API
* ##### 控制插件
```typescript
export interface Attr {
/** 与左侧横向距离 */
x: number
/** 与顶部纵向距离 */
y: number
/** 宽度 */
w: number
/** 高度 */
h: number
}
```
```javascript
//全屏
const args = {
x: 0,
y: 0,
w: 1920,
h: 1080,
}
parent.postMessage({
fun: 'attr',
args,
}, '*')
```
* 更新配置
```javascript
/*
假设插件配置结构为
{
a: {
b: 1,
c: 2,
}
}
*/
//只修改b的值
const newOption = {
a: {
b: 111
}
}
parent.postMessage({
fun: 'option',
args: newOption,
}, '*')
```
> 此操作会与当前配置进行深度合并,不会整体替换配置。每个插件的当前配置结构可通过上传index.html并使用`console.log`输出到控制台获取。
## 更新事件(update)
**所有更新事件将于页面加载完成之后触发一次,以便初始化数据**
> 用户信息(user)
* 示例
```typescript
window.addEventListener('message', e => {
if (e.source !== parent) return
const { category, type, args:[userInfoMap] } = e.data
if(category === 'update' && type === 'user'){
console.log('用户信息更新了', userInfoMap)
}
})
```
* 参数
```typescript
interface PlatformUserInfo {
/** 房间号 */
room_id: string | number
/** 昵称 */
name: string
}
/** 平台标识到用户信息的映射,未绑定的平台不会出现 */
type UserInfoMap = Partial>
```
> 插件配置(option)
* 示例
```typescript
window.addEventListener('message', e => {
if (e.source !== parent) return
const { category, type, args:[option] } = e.data
if(category === 'update' && type === 'option'){
console.log('插件配置更新了', option)
}
})
```
其中`option._`保存“自定义作品配置”产生的值。
> CSS样式(css)
```typescript
window.addEventListener('message', e => {
if (e.source !== parent) return
const { category, type, args: [css] } = e.data
if (category === 'update' && type === 'css') {
console.log('插件CSS更新了', css)
}
})
```
> 插件状态(config)
```typescript
interface PluginConfig {
/** 插件实例唯一ID */
id: string
/** 插件位置和尺寸 */
attr: Attr & Record
/** 插件样式配置 */
styles: Record
/** 插件锁定、隐藏等状态 */
status: Record
}
window.addEventListener('message', e => {
if (e.source !== parent) return
const { category, type, args: [config] } = e.data
if (category === 'update' && type === 'config') {
console.log('插件状态更新了', config)
}
})
```
* 全屏特效
```typescript
export enum Guard {
NOTHING = 0, //普通观众
GOVERNOR = 1, //总督
SUPERVISOR = 2, //提督
CAPTAIN = 3, //舰长
}
export interface Option {
// 蒙层颜色
maskColor: string,
// 上舰特效
guard: {
/* 感谢文案
{
1:"感谢老板开通总督",
2:"感谢老板开通提督",
3:"感谢老板开通舰长"
}
*/
banner: { [T in Exclude]: string }
// 是否启用
enable: boolean
}
// 送礼特效
gift: {
// 最低显示价格(RMB)
min: number
// 感谢文案
banner: string
// 是否启用
enable: boolean
}
// 醒目留言特效
superChat: {
// 感谢文案
banner: string
// 是否启用
enable: boolean
}
}
```
# 直播间事件('event')
插件可以通过此API获取直播间的各种事件,执行对应的变化:
```javascript
//监听事件
window.addEventListener('message', e => {
if (e.source !== parent) return
const { category, type, args:[dm] } = e.data
if(category === 'event' && type === 'LIVE_OPEN_PLATFORM_DM') console.log(dm.message)
})
// 订阅直播间事件
parent.postMessage({
fun: 'subscribe',
args: 'LIVE_OPEN_PLATFORM_DM' //使用数组可同时订阅多个事件
}, '*')
```
**常用订阅事件名如下;B站新增事件可参考[哔哩哔哩直播开放文档](https://open-live.bilibili.com/document/f9ce25be-312e-1f4a-85fd-fef21f1637f8)中的`CMD`字段。**
| 事件 | 订阅事件名 | 备注 |
| ---- | ---------- | ---------- |
| 弹幕 | `LIVE_OPEN_PLATFORM_DM` |
| 礼物 | `LIVE_OPEN_PLATFORM_SEND_GIFT` |
| 醒目留言 | `LIVE_OPEN_PLATFORM_SUPER_CHAT` |
| 身份开通 | `LIVE_OPEN_PLATFORM_GUARD` | B站大航海、抖音星守护和会员开通/续费 |
| 进房 | `LIVE_OPEN_PLATFORM_LIVE_ROOM_ENTER` | 部分平台不支持 |
| 点赞 | `LIVE_OPEN_PLATFORM_LIKE` | 部分平台不支持 |
| 直播开始 | `LIVE_OPEN_PLATFORM_LIVE_START` | 仅支持B站 |
| 直播结束 | `LIVE_OPEN_PLATFORM_LIVE_END` | 仅支持B站 |
| 重置插件 | `CLEAR_DATA` |
> 通用字段
| 字段名 | 类型 | 描述 |
| ---------------------- | ------ | ---------------------------------- |
| id | string | 消息唯一id |
| type | string | 事件类型 |
| uname | string | 用户昵称 |
| uid | string | 用户唯一标识 |
| face | string | 用户头像 |
| timestamp | number | 弹幕发送时间秒级时间戳 |
| fansMedalWearingStatus | boolean | 该房间粉丝勋章佩戴情况 |
| fansMedalName | string | 粉丝勋章名 |
| fansMedalLevel | number | 对应房间勋章等级 |
| guardLevel | number | 权限等级:B站1总督、2提督、3舰长;抖音星守护及各平台会员为3 |
| guardIcon | string | 平台守护身份图标 |
| guardName | string | 平台守护身份名称,如总督、提督、舰长、星守护、会员 |
| vipLevel | number | 会员等级:0非会员、1普通会员、2年度会员 |
| vipImg | string | 会员图标 |
| isModerator | boolean | 是否为房管 |
| isAnchor | boolean | 是否为主播 |
| platform | string | 平台标识 |
> 事件类型
| 事件名称 | 标识符 |
| -------- | ---- |
| 弹幕 | message |
| 礼物 | gift |
| 醒目留言 | superChat |
| 大航海/守护/会员 | guard |
| 进房 | enter |
| 点赞 | like |
| 直播开始 | start |
| 直播结束 | end |
> 平台标识
| 平台名称 | 标识符 |
| -------- | ---- |
| B站 | bili |
| 抖音 | dou_yin |
| 视频号 | shi_pin_hao |
| 快手 | kuai_shou |
| 虎牙 | hu_ya |
| 斗鱼 | dou_yu |
| 花椒 | hua_jiao |
| 小红书 | red_note |
> 弹幕(message)
| 字段名 | 类型 | 描述 |
| -------- | ---- | ---------- |
| dmType | number | 弹幕类型 0:普通弹幕 1:表情包弹幕 |
| message | string | 弹幕内容 |
| emojiImgUrl | string | 表情包图片地址 |
| emoticon | string[] | 表情列表 |
| replyUid | string | 被at用户唯一标识 |
| replyUname | string | 被at的用户昵称 |
| gloryLevel | number | 直播荣耀等级 |
> 礼物(gift)
| 字段名 | 类型 | 描述 |
| ---------------------- | ---------------- | --------------------------------------- |
| giftId | string | 道具ID |
| giftName | string | 道具名 |
| giftNum | number | 赠送道具数量 |
| price | number | 礼物单价(单位:RMB) |
| paid | boolean | 是否是付费道具 |
| giftIcon | string | 道具图标 |
| comboGift | boolean | 是否是combo道具 |
| comboInfo | combo_info结构体 | 连击信息 |
| blindGift | blind_gift结构体(可选) | 盲盒爆出礼物信息 |
| combo_info | 类型 | 描述 |
| :------------- | :----- | :--------------------- |
| combo_base_num | number | 每次连击赠送的道具数量 |
| combo_count | number | 连击次数 |
| combo_id | string | 连击id |
| combo_timeout | number | 连击有效期秒 |
| blind_gift | 类型 | 描述 |
| ---------- | ---- | ---- |
| giftId | string | 爆出礼物ID |
| giftName | string | 爆出礼物名称 |
| giftIcon | string | 爆出礼物图标 |
| price | number | 爆出礼物价值 |
> 付费留言(superChat)
| 字段名 | 类型 | 描述 |
| ---------------------- | ------ | ---------------------- |
| message | string | 留言内容 |
| rmb | number | 支付金额(元) |
| startTime | number | 生效开始时间 |
| endTime | number | 生效结束时间 |
> 大航海/守护/会员(guard)
处理该事件时应先通过`platform`区分直播平台,再在对应平台分支中根据`guardName`、`guardLevel`和`vipLevel`判断具体身份。不同平台未来可能提供名称或等级相同的会员、守护身份,不应只凭单个身份字段推断所属平台。
| 字段名 | 类型 | 描述 |
| ---------------------- | ------ | ---------------------- |
| price | number | 开通价格(单位:RMB) |
| guardLevel | number | B站1总督、2提督、3舰长;抖音星守护及会员为3 |
| guardNum | number | 开通数量 |
| guardUnit | string | 开通时长单位,如月、季、年或3天等 |
| guardName | string | 平台守护身份名称,如总督、提督、舰长、星守护、会员 |
| guardIcon | string | 平台守护身份图标 |
| action | string | 如开通、续费 |
会员开通或续费同样使用该事件。此时`guardName`为`会员`、`guardLevel`为`3`、`guardNum`为`1`,会员周期通过`guardUnit`获取;`vipLevel`和`vipImg`用于识别会员身份及展示会员图标。普通会员的`vipLevel`为`1`,年度会员为`2`。价格无法获取时`price`为`0`。
> 进房(enter)
| 字段名 | 类型 | 描述 |
| --------- | ------ | ----------- |
> 点赞(like)
| 字段名 | 类型 | 描述 |
| ---------------------- | ------ | ---------------------- |
| likeText | string | 点赞文案 |
| likeCount | number | 点赞次数 |
> 重置插件
* 事件名:`CLEAR_DATA`
* 作用:点击配置中的清空数据按钮后,通知直播间中的插件清空数据
* **收到此事件后,若此插件有数据,则应重置插件状态**
```javascript
parent.postMessage({
fun: 'subscribe',
args: 'CLEAR_DATA'
}, '*')
```
## 代码示例
```js
//关于B站身份区分
if (platform === 'bili' && guardLevel > 0) {
//舰长
if (guardLevel === 3) {}
//提督
if (guardLevel === 2) {}
//总督
if (guardLevel === 1) {}
}
//关于抖音身份区分
if (platform === 'dou_yin' && guardLevel > 0) {
//星守护
if (guardName === '星守护') {}
//会员
if (vipLevel > 0) {}
//同时是星守护和会员
if (guardName === '星守护' && vipLevel > 0) {}
}
```
# 自定义作品配置
如果作品有额外配置参数需求,可以通过指定的json格式,在画布右侧配置栏创建额外的选项
## 基本格式
| 字段名 | 类型 | 必须 | 描述 |
| ------ | ------------------------------------ | ---- | ------------------------ |
| key | string | 是 | ID (不能重复) |
| name | string | 是 | 标题 |
| type | string | 是 | 类型 (输入框、下拉框...) |
| value | string \| number \| boolean \| array | 是 | 默认值 |
```json
{
"settings": [
{
"key": "foo",
"name": "自定义颜色",
"type": "color",
"value": "#ffffff"
}
]
}
```
## CSS中使用
变量格式:`--workshop-`
```css
.foo {
color: var(--workshop-foo)
}
```
## 模板中使用
```javascript
const handlers = {
update: {
option(newValue) {
//控制台输出自定义配置foo的值
console.log(newValue._.foo)
},
},
event: {
LIVE_OPEN_PLATFORM_DM(dm) {
console.log(dm.message)
}
}
}
//监听事件
window.addEventListener('message', e => {
if (e.source !== parent) return
const { category, type, args } = e.data
handlers[category]?.[type]?.apply(e, args)
})
// 订阅事件
parent.postMessage({
fun: 'subscribe',
args: Object.keys(handlers.event)
}, '*')
```
## 组件库
* 输入框(text, textarea)
| 字段 | 类型 | 必须 | 描述 |
| ----------- | ------ | ---- | -------------------- |
| placeholder | string | 否 | 无内容时的提示 |
| prefix | string | 否 | 前缀文本 |
| suffix | string | 否 | 后缀文本 |
| minlength | number | 否 | 限制内容最小字符长度 |
| maxlength | number | 否 | 限制内容最大字符长度 |
| rows | number | 否 | 输入框展示行数 |
```json
{
"settings": [
{
"key": "text",
"name": "输入框",
"type": "text",
"value": "默认内容",
"prefix": "前缀内容",
"suffix": "后缀内容"
},
{
"key": "textarea",
"name": "多行输入框",
"type": "textarea",
"value": "默认内容",
"placeholder": "请输入xxx",
"maxlength": 30,
"rows": 5
}
]
}
```
* 数字输入框(decimal)
| 字段 | 类型 | 必须 | 描述 |
| ----------- | ------ | ---- | -------------------- |
| placeholder | string | 否 | 无内容时的提示 |
| prefix | string | 否 | 前缀文本 |
| suffix | string | 否 | 后缀文本 |
| precision | number | 否 | 小数位数,默认为0 |
| step | number | 否 | 每次加减的值 |
| min | number | 否 | 最小值 |
| max | number | 否 | 最大值 |
```json
{
"settings": [
{
"key": "integer",
"name": "整数输入框",
"type": "decimal",
"value": 0
},
{
"key": "decimal",
"name": "小数输入框",
"type": "decimal",
"value": 0,
"placeholder": "请输入xxx",
"prefix": "前缀内容",
"suffix": "后缀内容",
"precision": 2,
"step": 0.01,
"min": -999.99,
"max": 999.99
}
]
}
```
```css
/* CSS示例 */
.foo {
font-size: calc(var(--workshop-integer) * 1px);
}
```
* 滑动选择(slider)
| 字段 | 类型 | 必须 | 描述 |
| ---- | ------ | ---- | --------------------- |
| step | number | 否 | 每次加减的值,默认为1 |
| min | number | 否 | 最小值 |
| max | number | 否 | 最大值 |
```json
{
"settings": [
{
"key": "integer",
"name": "整数滑块",
"type": "slider",
"value": 0
},
{
"key": "decimal",
"name": "小数滑块",
"type": "slider",
"value": 0,
"step": 0.01,
"min": -1,
"max": 1
}
]
}
```
* 复选框(checkbox)
```json
{
"settings": [
{
"key": "checkbox",
"name": "复选框",
"type": "checkbox",
"value": true
}
]
}
```
* 开关(switch)
```json
{
"settings": [
{
"key": "switch",
"name": "开关",
"type": "switch",
"value": true
}
]
}
```
* 选择器(select)
| 字段 | 类型 | 必须 | 描述 |
| -------- | -------------------------------- | ---- | ---------- |
| options | {label: string, value: string}[] | 是 | 选项组 |
| multiple | boolean | 否 | 是否可多选 |
```json
{
"settings": [
{
"key": "select",
"name": "选择器",
"type": "select",
"options": [
{
"label": "我选A",
"value": "A"
},
{
"label": "我选B",
"value": "B"
}
],
"value": "B"
},
{
"key": "multipleSelect",
"name": "多项选择器",
"type": "select",
"multiple": true,
"options": [
{
"label": "我选A",
"value": "A"
},
{
"label": "我选B",
"value": "B"
}
],
"value": [
"A",
"B"
]
}
]
}
```
* 颜色选择器(color)
```json
{
"settings": [
{
"key": "color",
"name": "颜色选择",
"type": "color",
"value": "#ffffffff"
}
]
}
```
* 图片上传(image)
图片地址转换为CSS变量时会自动包装为`url(...)`,可直接写`background-image: var(--workshop-background)`。
```json
{
"settings": [
{
"key": "background",
"name": "背景图片",
"type": "image",
"value": ""
}
]
}
```
* 自定义页面(iframe)
| 字段 | 类型 | 必须 | 描述 |
| ---- | ---- | ---- | ---- |
| src | string | 否 | 页面地址 |
| srcdoc | string | 否 | HTML,会覆盖src |
| width | string | 否 | 页面宽度,默认为`100%` |
| height | string | 否 | 页面高度,默认为`150px` |
| loading | string(eager,lazy) | 否 | 加载方式,默认为`eager` |
| scrolling | string(auto,yes,no) | 否 | 是否显示滚动条 |
```json
{
"settings": [
{
"key": "setting",
"name": "自定义设置",
"type": "iframe",
"value": "",
"src": "https://xxx.com/setting.html",
"width": "100%",
"height": "200px"
}
]
}
```
> 复杂对象请将`value`保存为JSON字符串;页面收到后使用`JSON.parse`解析。`src`和`srcdoc`至少填写一个。
```html
```
* 礼物选择(gift)
| 字段 | 类型 | 必须 | 描述 |
| -------- | -------------------------------- | ---- | ---------- |
| multiple | boolean | 否 | 是否可多选 |
```json
{
"settings": [
{
"key": "gift",
"name": "礼物选择",
"type": "gift",
"multiple": false,
"value": []
}
]
}
```
# 自定义插件
作品类型选择`自定插件`,然后上传模板,代码逻辑和其他插件的模板上传一致
# VTS 接口文档
VTS能力主要用于“自定插件”及其配套本地服务。接口定义与消息格式请参考[VTube Studio官方API文档](https://github.com/DenchiSoft/VTubeStudio)。
# 麦克风语音识别
创意工坊插件可以订阅客户端麦克风的语音识别结果。首次订阅时客户端会申请麦克风权限,识别由客户端处理,插件只接收识别事件,不会直接获得麦克风音频流。
## 订阅麦克风
```javascript
parent.postMessage({
fun: 'subscribe',
args: {
eventName: 'MICROPHONE',
lang: 'zh-CN'
}
}, '*')
```
| 字段名 | 类型 | 必须 | 描述 |
| ------ | ------ | ---- | ---- |
| eventName | string | 是 | 固定为`MICROPHONE` |
| lang | string | 是 | 语音识别语言,使用BCP 47语言标签,如`zh-CN`、`en-US` |
`subscribe`支持数组参数,因此麦克风事件可以和直播间事件一起订阅:
```javascript
parent.postMessage({
fun: 'subscribe',
args: [
'LIVE_OPEN_PLATFORM_DM',
{ eventName: 'MICROPHONE', lang: 'zh-CN' }
]
}, '*')
```
同一个插件实例只保留一个麦克风语言订阅。重复订阅相同语言不会重复创建会话,订阅其他语言会先释放原语言会话。
## 接收识别事件
```javascript
window.addEventListener('message', e => {
if (e.source !== parent) return
const { category, type, args: [event] } = e.data
if (category !== 'event' || type !== 'MICROPHONE') return
if (event.type === 'ready') {
console.log('麦克风识别已开始', event.lang)
} else if (event.type === 'result') {
console.log('识别结果', event.text, event.isFinal, event.confidence)
// 临时结果可能继续变化,建议仅使用最终结果触发业务逻辑
if (event.isFinal) {
console.log('最终结果', event.text)
}
} else if (event.type === 'stopped') {
console.log('麦克风识别已停止', event.lang)
}
})
```
## 事件格式
| 事件类型 | 字段 | 描述 |
| -------- | ---- | ---- |
| `ready` | `type: 'ready'`、`lang: string` | 识别会话已开始或自动恢复后重新开始 |
| `result` | `type: 'result'`、`lang: string`、`text: string`、`isFinal: boolean`、`confidence: number` | 收到临时或最终识别文本,`confidence`为客户端返回的置信度 |
| `stopped` | `type: 'stopped'`、`lang: string` | 识别因不支持、权限、设备或其他不可恢复问题终止 |
使用时注意:
- 麦克风功能依赖客户端环境的Web Speech语音识别支持及识别服务可用性。
- 网络类临时故障由客户端自动重试,恢复后可能再次收到`ready`事件。
- 权限被拒绝、没有可用麦克风、语言不受支持等不可恢复问题会触发`stopped`,具体错误由客户端提示,不会发送给插件。
- 插件页面卸载、来源重置或插件实例销毁时,客户端会自动释放对应的麦克风订阅。