# 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`,具体错误由客户端提示,不会发送给插件。 - 插件页面卸载、来源重置或插件实例销毁时,客户端会自动释放对应的麦克风订阅。