Skip to content
On this page

自定义发包与安全边界

当内置动作无法表达服务器自己的业务时,可以使用 packet 把自定义事件交给服务端插件。ChaUI 负责验证页面会话并触发 API 事件,具体业务由监听 ChaUIPacketEvent 的插件决定。

编辑器的隔离预览不会发送 packet。只有正常打开的运行时页面会把自定义事件交给服务端,避免测试页面时误触发真实业务。

packet() 是普通脚本动作,不要求写在点击事件里。只要真实运行时确实执行到了这一句,就会发送,包括:

  • 页面 openclosekey.*mouse.* 事件;
  • 组件 create、三键按下、松开、点击、悬浮进入和悬浮离开事件;
  • 页面自定义方法;
  • timer 回调;
  • 其他插件通过 API 让当前页面执行的运行时脚本。

如果 packet() 写在 if 的未进入分支里,它不会发送。编辑器编辑状态和隔离预览也不会发送。

基本写法

只发送事件标识:

text
packet("shop:refresh")
发包("shop:refresh")

同时携带多项字符串数据:

text
packet("shop:select", "item_a", "2")
发包("dialog:reply", "accept", "chapter_1")

第一个参数是 packetId,其余参数会按顺序进入字符串列表 datapacketId 必须直接写成固定字符串,不会展开变量;数据参数会在发送前展开 {vars.xxx}{vals.xxx} 和花括号中的表达式。

text
packet("shop:select", "{vars.itemId}", "{vars.amount}")
发包("dialog:reply", "玩家生命:{vals.player.health}")

数据参数使用与 log() 相同的响应式规则。需要发送普通左花括号时,连续写两个左花括号;变量缺失或表达式无效时按安全空值处理,不会把未解析的模板原样冒充有效数据。

参数限制

项目限制说明
packetId 字符英文字母、数字、_-.:建议使用 系统:动作 形式,方便分类
数据类型字符串列表数字和复杂对象也需要转换成字符串
数据项数量最多 64 项超出限制的请求会被拒绝
单项长度最多 8192 个字符不适合传输大型文件或图片

需要发送复杂结构时,可以把 JSON 作为某一项字符串:

text
packet("shop:order", "{\"item\":\"item_a\",\"amount\":2}")

服务端插件应自行验证 JSON 字段、取值范围和玩家权限,不能因为数据来自 ChaUI 页面就直接信任。

服务端收到的上下文

通过 ChaUIPacketEvent,业务插件可以获得:

信息用途
玩家判断是谁触发了事件
页面 ID区分事件来自哪个页面
组件 ID组件事件中是组件 ID;页面级事件中为空字符串
触发类型原始组件事件或页面事件;方法和计时器继承创建它们的上下文,没有明确事件来源时为 script
packetId识别具体业务动作
data读取页面作者发送的字符串参数

ChaUI 不会自动解释 packetId,也不会自动修改经济、背包、任务或其他业务数据。没有插件监听时,自定义包不会产生默认业务效果。

例如,把发包写在页面打开事件中:

yaml
events:
  open: |-
    packet("example:page_opened", "welcome")

这时服务端事件的 elementId 是空字符串,triggeropen。如果 open 事件调用方法或启动计时器,其中的发包仍继承这个页面上下文;由没有明确事件名的运行时脚本执行时,triggerscript

页面示例

下面的确认按钮会把当前页面中的选择结果交给服务端业务插件:

yaml
elements:
  - id: submit_button
    type: button
    layout:
      x: 20
      y: 80
      width: 90
      height: 20
    button:
      label: "提交选择"
    events:
      leftClick: |
        packet("example:submit", "item_a", "1")
        component("status").text.value = "请求已发送"

packet 的参数按字符串传递。服务端仍需重新校验物品是否存在、数量是否合法、玩家是否满足条件,以及事件是否可以重复提交。动态值必须写在带引号的数据模板中,例如 "{vars.amount}";不能把未加引号的变量或整个组件对象直接作为参数。

component("id") 返回的是页面内的组件对象,不能把整个对象直接作为 packet 参数发送。需要检查组件状态时,先读取到页面变量用于本地判断或显示,再按业务协议发送明确的字符串数据:

yaml
events:
  leftClick: |-
    vars.buttonEnabled = component("submit_button").enabled
    log("提交按钮启用状态:{vars.buttonEnabled}")
    packet("example:submit", "{vars.itemId}", "{vars.amount}")

安全边界

  • 客户端提交的页面会话必须属于当前玩家,页面 ID 必须与会话一致。
  • packetIddata 都是玩家客户端提供的输入,业务插件必须验证。
  • 不要直接把 data 拼接成高权限指令、数据库语句或文件路径。
  • 涉及物品、货币、奖励和权限的结果必须由服务端计算并写入。
  • 对可能重复触发的按钮,应在业务层设计防重复提交或冷却机制。
  • 单个非法脚本或非法自定义包不应导致页面、客户端或服务端崩溃。

与内置服务端动作的区别

openclose 和指令类内置动作由服务端从已保存页面脚本中读取,客户端不能替换其中的动作内容。packet 则用于传递页面交互数据,服务端业务插件应把每个字段都视为待校验输入。

如果你刚开始使用脚本,可以返回ChaUI Script 简介,从第一个点击事件开始制作。