用 vue 给女主播写了个工具,榜一大哥爱上了她,她爱上了我

21,800 阅读16分钟

这是一个什么样的程序?这是一个使用 sys-shim/vue3/vite 开发的一个 windows 程序。用于向网站注入自己的代码以实现一些自动化功能。

sys-shim 是什么?它是一个我开发的个人工具,力求前端人员无需了解其他语言的情况下快速制作轻量的 windows 程序,详情请移步 electron 和 tauri 都不想用,那就自己写个想用的吧

为什么要开发这样的程序

虽然已经过去了很久,但那天的场景还是历历在目。

那天是在周五晚上 23 点过,大楼的中央空调都关了,我搓了搓手,看着还未完成的工作,想了想再不回去公车就没了,到家的话饭店也关门了。

然后按了一下显示器的电源按钮,让电脑继续工作着,准备回家吃饭后远程继续工作。

在大楼电梯遇到一个长得很挺好看的女生,由于这一层我们公司,然后看样子好像是直播部门的同事,虽然平时也都不怎么遇见,更没什么交集,只是公司偶尔让大家去主播间刷下人气,有点印象,猜想应该是直播部门的吧啦吧啦**蕾

虽然是同事,却不熟悉,想打个招呼都不知道说啥,有点尴尬。然后我索性无所是事刷微信列表去了,虽然微信列表里一条消息也没有。。。

突然她给我来了句:“小哥哥你是我们公司的吧,你们平时下班都这么晚的吗?”一边哈气搓手。

我礼貌性笑了一下:“嗯,不是每天都这么晚。”,然后继续低头无所是事的刷微信列表。

大约一两秒之后,她说:“哦”。然后再停顿一会,好像又找到了什么话题:“那你们最近工作也很忙吗?但是我前两几天也没基本没遇到我们公司这么晚下班的人”。

这句话听起来好像传达了很多信息。但时间可不允许我慢慢思考每个信息应该如何正确应对,就像领导给的项目开发时间根本来不及完善好每一个细节一样。

我只能粗略回答:“没有特别忙,只是有时候我喜欢弄些小工具啥的,一不小心就已很晚了”。我心里想:感觉有点像面试,没有说公司不好,又显得自己爱学习,是一个能沉浸于思考的人,应该没问题吧。

“真好,看得出来你还十分热爱你这份职业。能够愿意花自己的时间去研究它们。”听语气好像是有一点羡慕我,感觉还有一点就是她不太喜欢她现在的工作。是呀,我也经常在想,做直播的那些人,有多少是喜欢整蛊自己,取悦别人,有多少是喜欢见人就哥哥好,哥哥帅,哥哥真的好可爱?

“只是觉得,能有一些工具,能帮助减少些重复劳动,就挺好”。

“对对对,小哥哥你说得太对了,就是因为有工具,减少了很多像机器一样的工作,人才可以去做更多有意义的,不像是机器那样的事情。”

当她说这句话的时候,我想,有一种不知道是不是错觉的错觉,真是个有想法的人!还有,这难道是在夸我们这些做工具的人吗?但是她说这句时的微笑,一下子就让人感到了她的热情和礼貌。

我心想,竟然这么有亲和力,很想有愿意继续沟通的想法。对!不然人家怎么能做主播?要换我上去做主播,绝对场也冷了,人也散了。

我一边告诉自己,因为能做主播,所以她本身就很有亲和力,所以你感觉她很热情,是因为这个热情是固有属性,并不是对于你热情。

一边竟开始好奇,这么漂亮又有亲和力的妹子,谁的下属?忍心让她上班这么晚?谁的女朋友?忍心让她上班这么晚?

好奇心害死猫。我竟然还是问出那句话:

“为什么你这么晚才下班呢?”

“最近销售量有点下滑,我想保住我销售额前一至少前二名的位置。”听到这句话的时候,我有点惊讶。我靠,居然是销冠。然后我不温不火的说到:“是啊,快过年了,得拿年终奖。”

“不是,就是想着让成绩保持着,马上快一年了。”尴尬,人家只是想保持成绩。是我肤浅了。等等!保持快一年?没记错的话她好像也才在公司直播一年不到吧!这就是传说中的入职即巅峰吗?我突然觉得我好菜!好想快点自觉走开,奈何地铁还没到!

“原来是销冠,这么厉害!我以为是年底为了冲年终奖,是我肤浅了~”我简单表达一下敬意和歉意。有颜值有能力,突然人与人之间的距离一下就拉开了。

“没有没有!钱也重要,钱也重要!”她噗呲一笑。然后用期盼的眼神看着我,“对了,你喜欢研究小工具来着,你有没有知道哪种可以在直播时做一些辅助的小工具?我网上找了好多,都是只能用在抖音斗鱼这些大公司的辅助工具,我们公司的这个直播平台的一直没有找到。哎呀!好烦~”

完犊子了,这题我不会。看着她好像是工具花了很久没有找到,焦急得好像就要跺脚的样子,我只感觉头皮发麻,要掉头发了!怎么办?怎么办?哪里有这种工具?

但话题还是要接着啊!我开始答非所问:“到没关注过这方面的小工具,但我听说现在有些自动直播的工具,可以克隆人像和声音二十四小时直播。”

“不需要不需要,我不需要这么高端的工具,而且那些自动播的很缺少粉丝互动的。我只要可以帮我定时上下架商品啥的就好。”

我心想,这不搞个脚本 setInterval 一下就行?虽然但是要做得方便他们使用的形式还是得折腾下。我们这个直播平台又不是大平台,网上肯定也没有现成的,不过要做一个没什么难度。

我回答说:“那我帮你找找。”

“谢谢谢谢小哥哥!你人真好!”看着她一边开心的笑着一边双手拜托的样子,我既感觉完犊子了入坑了,又恨不得现在就给她做一个出来!

车来了。她转头看了一下,然后又转过头来问我“小哥哥可以加下你微信吗?你有消息的话随随时通知我,我都在的。”

我:“行的。”

她:“我加你我加你~”

我竟然一下子没有找到我的微信二维码名片在哪,确实,从来就没有其他女生加过我,没什么经验倒也正常,是吧?她又转头看了看停车的车,我知道她是她的车,可她还告诉我没事的慢慢来。

她加上了我的微信,然后蹦上滴滴滴快要关门的列车,在窗口笑着向我挥手告别。在转角那一刻她指了指手机,示意我看微信。

“我叫李蕾^_^”。

“收到”。

alt

功能设计

在上一节为什么要开发这样的程序花费了一定量的与技术无关的笔墨,可能有些读者会反感。对此我表示:你以为呢?接到一个项目哪那么容易?手动狗头。

在功能方面,主要考虑以下特性:

开发时方便

不方便肯定会影响开发进度啦。热更新热部署啥的,如果没有这些开发体验那可是一点都不快乐~

使用时点开就能用

解压、下一步下一步安装了都不要了。

多设备下多平台下多配置支持

如果不做设备隔离,万一主播把这软件发给别人用,岂不是乱套了。多平台的考虑是因为反正都是注入脚本,就统一实现。多配置主要实现每个配置是不同的浏览器配置和数据隔离。

便于更新

减少文件发来发去、版本混乱等问题。

便于风控

如果改天主播说这软件不想用了,那我远程关闭就行。

看下总体界面

alt

一个设备支持多个主配置,每个主配置可以绑定密钥进行验证。

主配置验证通过之后,才是平台配置,平台配置表示系统已支持自动化的平台,例如疼训筷手这些平台。这些每个平台对应自己的 logo、自动化脚本文件和状态。

自动化脚本文件在开发过程中可以修改,用户侧不可见,直接使用即可。

每个平台下有多个配置,比如疼训这个平台下,创建配置A作为账号A的自动化程序,配置B作为账号B的自动化程序。因为每个配置启动的是不同的浏览器实例,所以疼训理论上不会认为多个账号在同一浏览器下交叉使用。反正我司的平台肯定不会认为~

然后配置下有一些通用的功能:例如智能客服可以按关键字进行文字或语音回复。

例如假设你配置了一个关键字列表为

keys = [`小*姐姐`, `漂亮`]
reply = [`谢谢大哥`, `大哥你好呀`, `你也好帅`]

当你进入直播间,发了一句小*姐姐真漂亮时,就可能会自动收到小*姐姐的语音谢谢大哥, 你也好帅

在场控助手这边,根据场控需求,直播间可以按指定规则进行自动发言,自动高亮评论(就是某个评论或系统设定的内容以很抢眼的形式展示在屏幕上),这是防止直播间被粉丝门把话题逐渐带偏的操作方法之一。

商品助手这边,有一些按指定规则、时间或顺序等配置展示商品的功能。

技术选型

  • 使用 vue3/vite 进行界面开发。这部分众所周知是热更新的,并且可以在浏览器中进行调试。
  • 使用 sys-shim 提供的 js api 进行浏览器窗口创建、读写操作系统的文件。当创建浏览器窗口后,需要关闭窗口。
  • 使用 mockm 进行接口开发,自动实现多设备、平台、配置的 crud 逻辑支持。

在 vue3 进行界面开发的过程中,这个过程可以在浏览器里面也可以 sys-shim 的 app 容器中。因为界面与 sys-shim 进行通信主要是通过 websocket 。前端调用某个函数,例如打开计算器,然后这个函数在内部构造成 websocket 的消息体传给 sys-shim 去调用操作系统的 api 打开计算器。就像是前端调用后端提供的 api 让后端调用数据库查询数据,返回数据给前端。

在界面完成之后,把界面部署在服务器上,这样如果有更新的话,就像普通的前端项目一样上传 dist 中内容在服务器上即可。发给主播的 app 读取服务器内容进行界面展示和功能调用。

计划安排

  • 周五加加班,用两小时完成数据模型、API实现
  • 周六完成主要功能界面、交互开发
  • 周日上午进行体验完善、发布测试

开发过程

由于我只是个做前端的,并且只是个实习生。所以用到的技术都很简单,下面是具体实现:

数据模型、API实现

由于是多设备、多平台、多配置,所以数据模型如下:

const db = util.libObj.mockjs.mock({
  // 设备
  'device|3-5': [
    {
      'id|+1': 1,
      电脑名: `@cname`,
    },
  ],
  // 主配置
  'config|10': [
    {
      'id|+1': 1,
      deviceId() {
        const max = 3
        return this.id % max || 3
      },
      名称: `@ctitle`,
      卡密: `@uuid`,
      激活时间: `@date`,
      过期时间: `@date`,
    },
  ],
  // 平台
  platform: [
    {
      id: 1,
      封面: [
        {
          label: `@ctitle`,
          value: `@image().jpg`,
        },
      ],
      网址: `https://example.com/`,
      状态: `可使用`,
      脚本文件: [
        {
          label: `@ctitle().js`,
          value: `@url().js`,
        },
      ],
      名称: `豆印`,
    },
  ],
  'devicePlatformConfig|1-3': [
    {
      'id|+1': 1,
      名称: `默认`,
      deviceId() {
        const max = 3
        return this.id % max || 3
      },
      platformId() {
        const max = 3
        return this.id % max || 3
      },
      configId() {
        const max = 3
        return this.id % max || 3
      },
      数据目录() {
        return `data/${this.id}`
      },
      // 功能配置
      action: {
        智能客服: {
          文字回复: {
            频率: `@integer(1, 5)-@integer(6, 10)`,
            启用: `@boolean`,
            '配置|1-5': [
              {
                关键词: `@ctitle`,
                回复: `@ctitle`,
              },
            ],
          },
          // ... 省略更多配置
        },
        // ... 省略更多配置
      },
    },
  ],
}),

观察上面的数据模型, 例如主配置中有一个 deviceId,由于这个字段是以驼峰后缀的 Id 结尾,所以会自动与 device 表进行关联。

platform 这张表由于没有与其他表有关联关系,所以无需添加含有 ...Id 的字段。

devicePlatformConfig 平台设备配置这张表,是某设备创建的针对于某一主配置下的某平台下的某一配置,所以会有 deviceId / platformId / configId

这样如何要查某设备下的所有平台的配置,直接 /devicePlatformConfig?deviceId=查某设备ID 即可。

由于上面这些表声明关联关系之后,模拟数据和接口都是自动生成的,所以这一块并没有其他步骤。

在 api 层面,有一个需要处理的小地方,就是类似于登录(token/用户标识)的功能。由于这个程序并不需要登录功能,所以使用设备ID作为用户标记。

const api = {
  async 'use /'(req, res, next) {
    // 不用自动注入用户信息的接口, 一般是系统接口, 例如公用字典
    const publicList = [`/platform`]
    const defaultObj =
      !publicList.includes(req.path) &&
      Object.entries({ ...req.headers }).reduce((acc, [key, value]) => {
        const [, name] = key.match(/^default-(.*)$/) || []
        if (name) {
          const k2 = name.replace(/-([a-z])/g, (match, group) => group.toUpperCase())
          acc[k2] = value
        }
        return acc
      }, {})
    if (req.method !== `GET`) {
      req.body = {
        ...defaultObj,
        ...req.body,
      }
    }
    req.query = {
      ...defaultObj,
      ...req.query,
    }
    next()
  },
}

在后端 api 入口上,我们添加了一个拦截器,含有 default- 开头的键会被当成接口的默认参数。如果传设备 id 就相当于给每个接口带上设备标记,后面这个设备创建和修改、查询配置都会被限定在改设备下,实现了类似某用户只能或修改查看某用户的数据的功能。对于之前提到的公用数据,例如 /platform 这个接口的数据是所有用户都能看到,那直接配置到上面的 publicList 中即可。

前端的请求拦截器是这样的:

http.interceptors.request.use(
  (options) => {
    options.headers[`default-device-id`] = globalThis.userId
    return options
  },
  (error) => {
    Promise.reject(error)
  },
)

什么?并不严谨?啊对对对!

界面实现:首先做一个浏览器

由于只会一些简单的跑在浏览器里的 js/css ,所以我们要先做一个浏览器来显示我们的软件界面。

经常用 google chrome,用习惯了,并且听说它还不错。所以打算做一个和它差不多的浏览器。

它封装了 chromium 作为内核,那我们也封装 chromium 吧。

微软听说大家都想要做个基于 chromium 的的界面渲染程序,于是微软就给我们做好了,叫 microsoft-edge/webview2

听说大家都在用这个渲染引擎,那么微软干脆把它内置于操作系统中,目前 win10/win11 都有,win7/8 也可以在程序内自动在线安装或引用安装程序离线安装。

不知不觉的浏览器就做好了。

如何使用这个做好的浏览器

由于只会 js ,所以目前我使用 js 创建这个 webview 实例是这样的:

const hwnd = await hook.openUrl({
  url: platformInfo.value.网址,
  preloadScript,
  userDataDir: row.数据目录 || `default`,
})

可以看到,上面的 js 方法支持传入一个网址、预加载脚本和数据目录。

在这个方法的内部,我们通过构造一个 aardio 代码片段来创建 winform 窗口嵌入 webview 实例。

至于要构造什么 aardio 片段,是 aardio 已经做好相关示例了。复制粘贴就能跑,需要传参的地方,底层是使用 ipc 或 rpc 进行通信的。

ipc 是进程之前通知,可以简单的理解为一个基于事件的发布订阅程序。

rpc 是远程调用,可以简单理解为我们前端经常调用的 api。服务端封装好的 api,暴露给前端,直接调用就好了。

aardio示意片段

var winform = win.form({text: `sys-shim-app`}) // 创建一个 windows 窗口
var wbPage = web.view(winform, arg.userDataDir, arg.browserArguments) // 使用指定配置启动一个浏览器示例
wbPage.external = { // 向浏览器注入全局变量
  wsUrl: global.G.wsUrl;
}
wbPage.preloadScript(arg.preloadScript) // 向浏览器注入 js 脚本
wbPage.go(arg.url) // 使用创建的浏览器打开指定 url
winform.show() // 显示窗口

有了上面的代码,已经可以做很多事情了。因为向浏览器注入了一个全局变量 wsUrl,这是服务端的接口地址。然后在注入的脚本里去连接这个接口地址。

脚本由于是先于 url 被加载的,所以首先可以对页面上的 fetch 或者页面元素这些进行监听,实现拦截或代理。另外 webview 也提供了 cdp 层面实现的数据监听。

功能实现:让宿主与实现分离

这里的宿主是指除开 注入自定义脚本 的所有功能。根据之前的设计,网站地址是用户配置的,脚本也是用户上传的。所以一切都是用户行为,与平台无关?

啊对对对就这样!

把自动化这块功能分离出去,让其他人写(我不会!手动狗头)。然后我们在程序里为现有功能做一个事件发布。当用户开启了某个功能,脚本可以知道,并可以得到对应配置的值,然后去做对应功能的事。

const keyList = Object.keys(flatObj(getBase()))
keyList.forEach((key) => {
  watch(
    () => {
      return deepGet(devicePlatformConfig.value, key)
    },
    (newVal, oldVal) => {
      keyByValueUpdate(key, newVal, oldVal)
    },
    {
      immediate: true,
    },
  )
})

getBase 是一个配置的基础结构对象。把这个对象扁平化,就能等到每个对象的 key,使用 vue 的 watch 监听每个 key 的变化,变化后分别发布 [key, 当前值, 占值, 整个配置对象]

这样在自动化脚本那边只需要订阅一下他关心的 key 即可。

例如:当 场控助手.直播间发言.频率 从 2 变成 6 。

alt

ws.on(`action.场控助手.直播间发言.频率`, (...arg) => {
  console.log(`变化了`, ...arg)
})

好了,接下来的内容就是在群里 v50 找人写写 js 模拟事件点击、dom监听啥的了(具体自动化脚本略,你懂的~手动狗头)。

alt

测试过程

总算赶在了周一完成了功能,终于可以进行测试啦~

alt

她同事进行功能测试的时候,提出了一些修改意见(还好是自己写的,不然真改不动一点),然后有个比较折腾的是原来我的配置窗口和平台直播页面是分别在不同的 windows 窗口下的,可以相互独立进行拖拽、最小化等控制,因为想着独立开来的话配置窗口就不会挡住直播页面的窗口了。

没想到她希望配置窗口可以悬浮在直播平台的页面上,并且可以展开折叠拖动。这对于之前设计的架构有一些差异,修改花了点时间。

alt

alt

alt

最终结果

alt

我很满意,手动狗头。

相关内容

声明:本文仅作为 sys-shim 的程序开发技术交流,本人没有也不提供可以自动化操作某直播平台的脚本。