脚本撰写

脚本撰写指南 #

基于 Starlark 的语法和中控提供的运行时环境,我们可以撰写出各种各样的脚本,用于实现各种定制的功能。本文将介绍如何撰写脚本,以及脚本的配置方式。

设计思想 #

脚本的作用主要有两个:

  1. 为后续脚本提供运行时环境,例如定义一些全局变量,或者初始化一些资源。
  2. 对请求或结果进行特定的干涉,例如对请求进行预处理,或者对结果进行后处理。

具体脚本的运行时机和运行环境,需要考虑以下方面:

  • 入口点 (entrypoint):脚本的执行时机,即中控中对应的执行阶段,一个入口点可以有多个脚本顺序独立执行。
  • 全局环境 (global):脚本的默认执行环境,即所有脚本中都可以使用的变量和函数,不会因入口点变化而改变。
  • 执行环境 (environment):脚本的执行环境,即脚本中可以使用的变量和函数,会随着入口点的不同而不同。
  • 输出参数 (output):脚本执行后将作为输出结果继续使用的参数,修改会影响外部执行结果,会随着入口点的不同而不同。
  • 异常处理 (exception):脚本执行过程中可能发生异常的处理,处理方式会随着入口点的不同而不同。

因为中控脚本的设计是以干涉为目的,因此输出参数相对于传统执行环境而言,是比较特殊的,具体特点是:

  1. 参数都是直接在环境中作为变量可用,可以直接进行修改操作,而不需要实现一个特定签名的函数来处理输入和输出。
  2. 执行空白脚本不执行脚本的结果是一致的,即脚本的执行仅对外部参数进行修改,而不是从零构造参数本身。
  3. 复杂数据类型为 object,即 Go 类型指针在 Starlark 中的包壳,可以直接在 Starlark 中进行类似 dict 的修改操作,而不需要进行转换。

关于允许时环境的数据传递和覆盖:

  1. 相对于普通 Starlark 环境,中控开启了 --globalreassign 参数,即允许变量复用/覆盖,支持在顶层使用 if 语句和 for 循环。
  2. 不同 entrypoint 的执行结果会向后续层级传递,即 server -> session -> flow 等顺序,下级覆盖上级。
  3. 同一 entrypoint 下的多个脚本之间自身数据独立,即无法相互访问执行结果,后执行的结果会覆盖先执行的,但对输出参数的修改结果是有后效的。
  4. 不同 entrypoint 的脚本可以对传递的参数进行覆盖,结果会向后续层级传递;而对输出参数的直接覆盖,则无法影响外部执行结果,应选择原地修改。
  5. 对于同一 entrypoint 执行失败的情况,已经成功执行的脚本、部分的执行结果会被保持,如果流程没有退出,则会向后续层级传递。

配置方式 #

脚本的内容和执行条件,需要以列表的形式写入到中控的配置文件的 script 字段中。具体的脚本配置参数如下:

字段类型默认值说明
namestring空字符串表示脚本的名称。它可以是任何字符串,并不影响功能。
enabledbooleantrue表示脚本是否处于启用状态。如果未指定,默认会将脚本视为启用状态。
weightinteger0用于对同一入口点下的多个脚本进行排序。权重值较小的脚本会在权重值较大的脚本之前执行。
replbooleanfalse表示启用 REPL 调试模式。如果设置为 true,中控将在脚本执行后,进入类似 Python 交互的 REPL 模式,直到 Ctrl+D 退出。
timeoutstring10s指定脚本允许运行的最长持续时间。如果未提供,将默认为"10s"(10秒)。格式是持续时间的字符串表示形式,例如:“5s"表示5秒,“2m"表示2分钟等。
sourcestringtext表示脚本的来源。可以是 file(本地文件), url(在线文件) 或 text(文本)。
entrypointstring必填表示脚本的入口点,即中控中对应的执行阶段。
asyncbooleanfalse指定脚本是否应异步运行,异步执行的脚本的返回值将不会被记录。
codestring空字符串脚本的源代码。如果 sourcetext,则这个字段是必填的。
urlstring空字符串脚本的在线地址,需要 GET 方法直接可访问。如果 sourceurl,则这个字段是必填的。
pathstring空字符串脚本的本地路径,需要直接可访问,有读取权限。如果 sourcefile,则这个字段是必填的。
**注意**: - REPL 调试模式仅用于单并发情况下的测试,切勿用于上线,否则可能会导致中控无法正常工作。 - 所有启用的脚本需要内容非空,启动时会进行检查,如果发现空脚本或者获取失败,中控将报错并退出。

示例配置:

script:
  - name: ""
    weight: 1
    timeout: 1s
    entrypoint: server_start
    repl: true
    code: |
      print("Hello New Server! @", time.now())
      a = 1
      b = 2
      c = a + b
      print("EP:", entrypoint)
      r = http.get('https://dhpoc.softsugar.com/demo0/ping')
      print(r)
      rj = r.json()
      print("中控版本", rj['hostname'], hash.md5(rj['message']))      

  - name: test2
    enabled: true
    weight: 2
    timeout: 1s
    entrypoint: server_start
    async: true
    code: |
      print("Hello New Server! 2")
      x = [enabled_tts, enabled_asr, enabled_nlp]
      print(type(cs_config))
      print(dir(cs_config))
      sleep(0.5)
      print("Wake up ~~~ CS Config:", cs_config.DebugMode)      

  - name: test3
    weight: 10
    timeout: 1s
    entrypoint: session_start
    code: |
      print("Hello New Session!")      

全局环境 Globals #

全局环境变量 #

每个脚本无论入口点,都可以使用以下全局变量:

名称类型举例说明
hostnamestringst78中控运行机器或容器的 hostname
workdirstring/home/dev中控运行时的工作目录
os_platformstringdarwin中控运行时的操作系统平台
debug_modebooleantrue中控运行时的是否处于调试模式
enabled_asrlist[5, 6, 7]中控运行时的启用的 ASR 服务商 ID 列表
enabled_nlplist[2, 6]中控运行时的启用的 NLP 服务商 ID 列表
enabled_ttslist[3]中控运行时的启用的 TTS 服务商 ID 列表
entrypointstringserver_start当前脚本的入口点,具体取值会根据运行时的入口点改变
cron_cacheobject{"key": "value", ...}Cron Job 的存储信息,用于定时任务的存储
**注意**: 全局变量仅供参考,对它们的修改不会影响外部执行结果。

全局扩展模块 #

每个脚本无论入口点,都可以使用以下函数模块:

名称说明
go_idiomatic独立扩展方法合计,直接使用,调用时不需要写模块名称
base64Base64 编解码
hashlibMD5、SHA等哈希函数计算
httpHTTP 请求发送
jsonJSON 编解码
math常用数学计算函数
random常用随机变量生成
re正则表达式
structStarlark struct 构建
time常用时间变量操作函数

具体模块和函数的使用方法,请参考附录的相关文档。

入口点 Entrypoint #

定义好的脚本将在以下入口点(即 entrypoint)被中控调用执行,具体的执行时机和执行环境,请参考如下具体说明。

各个入口点之间的执行顺序和数据继承关系如下:

flowchart TD
    ServerS(server_start)
    SessionS(session_start)
    ASRA(asr_after)
    NLPB(nlp_before)
    NLPA(nlp_after)
    TTSB(tts_before)
    TTSA(tts_after)
    ServerS --> SessionS
    SessionS --> NLPB
    NLPB --> NLPA
    SessionS --> ASRA
    SessionS --> TTSB
    TTSB --> TTSA

server_start 中控启动 #

运行时机 #

中控进程启动过程中,当正确加载配置文件、显示了版本和配置信息后,将执行 server_start 入口点下的所有脚本。

即当脚本执行时,中控的授权校验、Web 服务启动尚未进行。

异常处理 #

如果脚本执行过程中发生异常,中控将报告错误信息并进程退出。

环境变量 #

无特定环境变量。

输出参数 #

名称类型举例说明
server_configobject{"endpoint": "/v1", "asr": {...}, ...}中控的配置文件的 JSON 形式

即可以通过对 server_config 的调整,来动态对中控的配置文件进行修改,修改仅执行一次,运行时全局有效。

它所设定的全局变量和函数,可以在后续的 session_start 等入口点的执行环境中被使用。

使用场景举例 #

可以参考但不限于以下场景:

  • 进行一些启动前的准备工作,例如:进行一些第三方接口的鉴权,或者进行记录日志,上报监控等。
  • 调用外部接口,获取中控的特定配置项并进行修改,例如:设置 WebHook 回调地址,控制授权时间等。
  • 根据特定条件,决定是否阻止中控启动,例如:检查是否联网,相关服务是否可用等。

server_cron 定时任务 #

运行时机 #

中控启动后,会根据配置文件中的 Cron Job 配置,定时执行 server_cron 入口点下的所有脚本。

异常处理 #

如果脚本执行过程中发生异常,中控将报告错误信息,但不会影响其他 Cron Job 的执行。

环境变量 #

server_start 脚本输出的变量基础上,无特定环境变量。

输出参数 #

无输出参数。

使用场景举例 #

可以参考但不限于以下场景:

  • 定时获取 CDN 上的配置文件,更新中控的配置。
  • 定时刷新第三方 Token,保证中控的鉴权信息有效。

session_start 会话建立 #

运行时机 #

客户端与中控建立 WebSocket 链接后,已经发送了格式正确的 Starter 包,并且中控已经成功解析了 Starter 包,确认了有效期和授权信息有效后,将执行 session_start 入口点下的所有脚本。

即当脚本执行时,鉴权通过的回复,尚未发送给客户端,客户端尚未收到任何业务数据。中控对应的 Driver 的 Client 也尚未建立。

异常处理 #

如果脚本执行过程中发生异常,中控将以 1011 错误码关闭当前 WebSocket 会话,其他会话不受影响。

环境变量 #

server_start 脚本输出的变量基础上,会根据具体会话设置以下环境变量:

名称类型举例说明
client_ipstring::1客户端真实 IP
remote_addrstring[::1]:56418客户端连接到服务器的 IP 地址
tenant_idstring123客户端指定的租户 ID,未指定时值为 None
device_idstringdevbox客户端指定的设备 ID,未指定时值为空字符串
session_idstring92e962d0-db14-4bd9-a65d-70f08d30a03e客户端指定的会话 ID,未指定时值为中控随机分配的 UUID
session_numint1自中控启动以来的会话序号

输出参数 #

名称类型举例说明
starter_requestobject{"type": "NLP2", ...}解析后的 Starter 包请求
starter_responseobject{"status":"ok", ...}拟定的 Starter 包回复
session_configobject{"endpoint": "/v1", ...}中控的配置文件的 JSON 形式

即可以通过对 session_config 的调整,来动态对中控的配置文件进行修改,修改仅对当前会话有效。

它所设定的全局变量和函数,可以在后续的 flow_start 等入口点的执行环境中被使用。

使用场景举例 #

可以参考但不限于以下场景:

  • 关闭中控自带鉴权,将 auth 信息发给第三方接口进行鉴权和身份确认。
  • 特定 Workflow Type 的修改,例如将所有 TTS4 的请求转发到 TTS5。
  • 对于 Starter 其他字段进行修正,例如替换 xiaoyi 为其他合理的 DeviceID。
  • 根据特定条件,修改对应的 VendorConfig,例如:根据用户请求的 VoiceID,使用私有的 TTS 配置信息。
  • 进行额外的第三方监控上报,例如:将会话信息上报到第三方监控平台。

nlp_before NLP 发送请求前 #

运行时机 #

客户端向中控发送了 NLP 请求,但中控尚未把请求发送给 NLP 服务商前,将执行 nlp_before 入口点下的所有脚本。

即当脚本执行时,中控尚未发送 NLP 请求,也未在缓存中进行查询,也没有按具体服务商构建报文,可以在此对请求进行修改。

异常处理 #

如果脚本执行过程中发生异常,中控将当前请求的状态标记为错误,不再发送给 NLP 服务商,并将错误信息返回给客户端。但 WebSocket 会话不会关闭,后续请求不受影响。

环境变量 #

server_start 脚本和 session_start 脚本输出的变量基础上,会根据具体请求设置以下环境变量:

名称类型举例说明
device_idstringdevbox客户端指定的设备 ID,未指定时值为空字符串
session_idstring92e962d0-db14-4bd9-a65d-70f08d30a03e客户端指定的会话 ID,未指定时值为中控随机分配的 UUID
trace_idstring55d98d63-c86c-423a-b015-0920614ad897当前请求的追踪 ID,为中控随机分配的 UUID
flow_namestringASR+NLP当前会话所处的工作流,即 Starter 中 type 的指定
vendor_idstringNLP6当前会话使用的服务商 ID
query_indexint1标识当前会话中第几条 NLP 请求,从1开始计数

输出参数 #

名称类型举例说明
nlp_requestobject{"query": "你好", ...}待处理的 NLP 请求
nlp_metaobject{"omit_error": false, ...}当前会话中 NLP 的 Starter 包的中 NLP 相关配置信息

可以通过对 nlp_request 的调整,来动态对中控的 NLP 请求进行修改,修改仅对当前请求有效;可以通过对 nlp_meta 的调整,来动态对中控的会话配置进行修改,修改仅对当前会话有效。

使用场景举例 #

可以参考但不限于以下场景:

  • 对请求进行预处理,例如:对请求中的敏感词进行过滤,或者对请求中的特定字段进行修改。
  • 对请求进行额外的第三方监控上报,例如:将请求信息上报到第三方监控平台。
  • 根据特定条件,修改会话对应的 NLP Meta 配置,例如:修改请求的 PromptHeader。

nlp_after NLP 收到回复后 #

运行时机 #

客户端向中控发送了 NLP 请求,中控已经把请求发送给 NLP 服务商,并收到了 NLP 服务商返回的结果,在将结果返回给客户端之前,将执行 nlp_after 入口点下的所有脚本。

即当脚本执行时,中控已经收到 NLP 服务商的结果并进行了归一化操作,但尚未发送给客户端,也未在缓存中存储查询,可以在此对结果进行修改。

异常处理 #

如果脚本执行过程中发生异常,中控将当前请求的状态标记为错误,并将错误信息返回给客户端。但 WebSocket 会话不会关闭,后续请求不受影响。

环境变量 #

server_start 脚本和 session_start 脚本输出的变量基础上,并结合当前请求的前序 nlp_before 的输出变量,会根据具体请求设置以下环境变量:

名称类型举例说明
device_idstringdevbox客户端指定的设备 ID,未指定时值为空字符串
session_idstring92e962d0-db14-4bd9-a65d-70f08d30a03e客户端指定的会话 ID,未指定时值为中控随机分配的 UUID
trace_idstring55d98d63-c86c-423a-b015-0920614ad897当前请求的追踪 ID,为中控随机分配的 UUID
flow_namestringASR+NLP当前会话所处的工作流,即 Starter 中 type 的指定
vendor_idstringNLP6当前会话使用的服务商 ID
query_indexint1标识当前会话中第几条 NLP 请求,从1开始计数
nlp_errorstringfailed to call ...当前请求收到的错误信息,空字符串表示未出错
is_cachedboolTrue当前回复内容是否来自缓存,True 表示为缓存结果

输出参数 #

名称类型举例说明
nlp_requestobject{"query": "你好", ...}解析后的 NLP 请求
nlp_metaobject{"omit_error": false, ...}当前会话中 NLP 的 Starter 包的中 NLP 相关配置信息
nlp_responseobject{"text": "你好", ...}待处理的 NLP 回复

可以通过对 nlp_response 的调整,来动态对中控的 NLP 回复进行修改,修改仅对当前请求有效;可以通过对 nlp_meta 的调整,来动态对中控的会话配置进行修改,修改仅对当前会话有效;此处修改 nlp_request 意义不大,因为已经发送给 NLP 服务商,无法再修改。

使用场景举例 #

可以参考但不限于以下场景:

  • 对回复进行预处理,例如:对回复中的敏感词进行过滤,或者对回复中的特定字段进行修改。
  • 对回复内容和错误进行额外的第三方监控上报,例如:将回复信息上报到第三方监控平台。

asr_after ASR 收到结果后 #

运行时机 #

客户端向中控发送了 ASR 数据请求,中控已经把请求发送给 ASR 服务商,并收到了 ASR 服务商返回的结果后,在将结果返回给客户端之前,将执行 asr_after 入口点下的所有脚本。

即当脚本执行时,中控已经收到 ASR 服务商的结果并进行了归一化操作,但尚未发送给客户端,也未进行字幕等进一步处理,可以在此对结果进行修改。

异常处理 #

如果脚本执行过程中发生异常,中控将当前结果的状态标记为错误,并将错误信息返回给客户端。但 WebSocket 会话不会关闭,后续结果不受影响。

环境变量 #

server_start 脚本和 session_start 脚本输出的变量基础上,会根据当前请求的特征设置如下环境变量:

名称类型举例说明
device_idstringdevbox客户端指定的设备 ID,未指定时为空字符串
session_idstring92e962d0-db14-4bd9-a65d-70f08d30a03e客户端指定的会话 ID,未指定时为中控随机分配的 UUID
trace_idstring55d98d63-c86c-423a-b015-0920614ad897当前请求的追踪 ID,为中控随机分配的 UUID
flow_namestringASR+NLP当前会话所处的工作流,即 Starter 中 type 的指定
vendor_idstringASR5当前会话使用的服务商 ID
result_indexint1标识当前结果中第几条 ASR 请求,从1开始计数

输出参数 #

名称类型举例说明
asr_metaobject{"omit_error": false, ...}当前会话中 ASR 的 Starter 包的中 ASR 相关配置信息
asr_responseobject{"type": "text", ...}待处理的 ASR 结果

可以通过对 asr_response 的调整,来动态对中控的 ASR 结果进行修改,修改仅对当前结果有效;可以通过对 asr_meta 的调整,来动态对中控的会话配置进行修改,修改仅对当前会话有效。

使用场景举例 #

可以参考但不限于以下场景:

  • 对结果进行预处理,例如:对结果中的敏感词进行过滤,或者对结果中的特定字段进行修改。
  • 对结果内容和错误进行额外的第三方监控上报,例如:将结果信息上报到第三方监控平台。

tts_before TTS 发送请求前 #

运行时机 #

客户端向中控发送了 TTS 请求,但中控尚未把请求发送给 TTS 服务商前,将执行 tts_before 入口点下的所有脚本。

即当脚本执行时,中控尚未发送 TTS 请求,也未在缓存中进行查询,也没有按具体服务商构建报文,可以在此对请求进行修改。

异常处理 #

如果脚本执行过程中发生异常,中控将当前请求的状态标记为错误,不再发送给 TTS 服务商,并将错误信息返回给客户端。但 WebSocket 会话不会关闭,后续请求不受影响。

环境变量 #

server_start 脚本和 session_start 脚本输出的变量基础上,会根据具体请求设置以下环境变量:

名称类型举例说明
device_idstringdevbox客户端指定的设备 ID,未指定时值为空字符串
session_idstring92e962d0-db14-4bd9-a65d-70f08d30a03e客户端指定的会话 ID,未指定时值为中控随机分配的 UUID
trace_idstring55d98d63-c86c-423a-b015-0920614ad897当前请求的追踪 ID,为中控随机分配的 UUID
flow_namestringNLP+TTS当前会话所处的工作流,即 Starter 中 type 的指定
vendor_idstringTTS5当前会话使用的服务商 ID
request_idstringan5m178kx6wgsskg7zyl6sxlb当前请求使用的标识符 ID,未指定时值为中控随机分配
query_indexint1标识当前会话中第几条 TTS 请求,从1开始计数

输出参数 #

名称类型举例说明
tts_requestobject{"query": "你好", ...}待处理的 TTS 请求
tts_metaobject{"language": "zh-CN", ...}当前会话中 TTS 的 Starter 包的中 TTS 相关配置信息

可以通过对 tts_request 的调整,来动态对中控的 TTS 请求进行修改,修改仅对当前请求有效;可以通过对 tts_meta 的调整,来动态对中控的会话配置进行修改,修改仅对当前会话有效。

使用场景举例 #

可以参考但不限于以下场景:

  • 对请求进行预处理,例如:对请求中的特定字段进行修改。
  • 对请求进行额外的第三方监控上报,例如:将请求信息上报到第三方监控平台。
  • 根据特定条件,修改会话对应的 TTS Meta 配置,例如:修改请求的 voice 或 volume。

tts_after TTS 收到回复后 #

运行时机 #

客户端向中控发送了 TTS 请求,中控已经把请求发送给 TTS 服务商,并收到了 TTS 服务商返回的结果,在将结果返回给客户端之前,将执行 tts_after 入口点下的所有脚本。

即当脚本执行时,中控已经收到 TTS 服务商的结果并进行了归一化操作,但尚未发送给客户端,也未在缓存中存储查询,可以在此对结果进行修改。

异常处理 #

如果脚本执行过程中发生异常,中控将当前请求的状态标记为错误,并将错误信息返回给客户端。但 WebSocket 会话不会关闭,后续请求不受影响。

环境变量 #

server_start 脚本和 session_start 脚本输出的变量基础上,并结合当前请求的前序 tts_before 的输出变量,会根据具体请求设置以下环境变量:

名称类型举例说明
device_idstringdevbox客户端指定的设备 ID,未指定时值为空字符串
session_idstring92e962d0-db14-4bd9-a65d-70f08d30a03e客户端指定的会话 ID,未指定时值为中控随机分配的 UUID
trace_idstring55d98d63-c86c-423a-b015-0920614ad897当前请求的追踪 ID,为中控随机分配的 UUID
flow_namestringNLP+TTS当前会话所处的工作流,即 Starter 中 type 的指定
vendor_idstringTTS5当前会话使用的服务商 ID
request_idstringan5m178kx6wgsskg7zyl6sxlb当前请求使用的标识符 ID,未指定时值为中控随机分配
query_indexint1标识当前会话中第几条 TTS 请求,从1开始计数

输出参数 #

名称类型举例说明
tts_requestobject{"query": "你好", ...}待处理的 TTS 请求
tts_metaobject{"language": "zh-CN", ...}当前会话中 TTS 的 Starter 包的中 TTS 相关配置信息
tts_responseobject{"type": "subtitle", ...}待处理的 TTS 结果

可以通过对 tts_response 的调整,来动态对中控的 TTS 回复进行修改,修改仅对当前请求有效;此处修改 tts_request 意义不大,因为已经发送给 TTS 服务商,无法再修改。

使用场景举例 #

可以参考但不限于以下场景:

  • 对回复进行预处理,例如:对回复中的字幕、多音字字段进行繁简转换,或者对回复中的特定字段进行修改。
  • 对回复内容和错误进行额外的第三方监控上报,例如:将回复信息上报到第三方监控平台。