tags:

  • React
  • AirCode

categories: tools


在线文档:https://docs-cn.aircode.io/

AirCode 是一个在线开发和部署 Node.js 应用的平台,为全栈工程师量身定制,目标是让开发变得简单。

并且提供一个简易的 WebIDE 和开箱即用的云服务,让开发者无需再操心后端选型、环境搭建和线上运维等一系列繁琐之事,只需打开浏览器即可完成产品开发,并部署到全球节点。

但是 AirCode 有资源限制,不建议用于高频的服务,但是我们要开发的机器人接口调用频率不会特别高,因此正适合 AirCode。

效果

@机器人 告诉它你想听的歌曲名字,机器人会返回给你最匹配的三条,点击「打开播放器」,会在右侧弹出在线播放器。

音乐播放器

播放器使用 Github 上的开源项目「music-motion-x」,该项目是 SSR 服务端渲染项目,但是我们需要部署到 Github Pages 上,因此需要一些改造。

  1. 使用 Vite 新建个 react + ts 的模板:
1
yarn create vite my-vue-app --template react-ts
  1. 接下来安装并配置 eslintcommitlinthuskylint-staged等工具,配置细节本文就不赘述了,详细配置见 https://github.com/bijinfeng/music-motion-x

  2. 迁移代码至新项目中

  3. 添加 Github Action

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
name: Build and Deploy

on:
push:
branches:
- master

jobs:
build-and-deploy:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@master
with:
submodules: true

- name: Install Dependencies
run: yarn install

- name: Generate
run: yarn build

- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./dist
  1. 提交代码至 Github 就可以触发自动部署了

新建飞书应用

飞书开放平台新建个应用,图标和名称自定义,添加应用后添加「网页」和「机器人」应用:

网页应用将主页地址配置成 Github Page 的地址

机器人应用需要配置个消息请求地址,可以先不填,等到 AirCode 开发完成后回头再来填。

接下来我们还需要申请权限和订阅事件,用于机器人消息的收发:

  • 订阅事件:只需要「接收消息」的事件就行,请求地址配置成上面 👆 机器人请求地址

  • 申请权限:以下权限是必须要有的,其它权限按需申请

机器人 webhook 接口开发

上面应用和机器人配置好后,接下来就需要开发 webhook 接口供机器人订阅。

首先需要新建个应用,然后安装两个依赖:

  1. 新建 client.js 文件
1
2
3
4
5
6
7
const lark = require("@larksuiteoapi/node-sdk");

module.exports = new lark.Client({
appId: process.env.app_id,
appSecret: process.env.app_secret,
appType: lark.AppType.SelfBuild,
});

app_idapp_secret这两个环境变量从飞书开放平台获取后,配置到 AirCode 的 Enviroments Tab 下:

  1. 新建 image.js 文件

飞书的消息卡片要插入图片,必须要先将图片上传到飞书中才行,因此该文件的作用是将网络图片上传到飞书中,并返回图片的 image_key。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const aircode = require("aircode");
const axios = require("axios");
const querystring = require("querystring");
const client = require("./client");

module.exports = async ({ url }) => {
// 如果已经上传过了,从数据库中取出缓存
const ImagesTable = aircode.db.table("images");

// 查询数据库
const result = await ImagesTable.where({ url }).find();

if (result.length > 0) {
return result[0].imageKey;
}

// 添加 param=200y200 的参数,使用小尺寸图片
const splitUrl = url.split("?");
const querys = querystring.parse(splitUrl[1] || "");
const queryString = querystring.stringify({ ...querys, param: "200y200" });
const newUrl = `${splitUrl[0]}?${queryString}`;

// 上传图片到飞书中
const response = await axios.get(newUrl, { responseType: "arraybuffer" });
const { image_key } = await client.im.image.create({
data: {
image_type: "message",
image: Buffer.from(response.data, "utf-8"),
},
});

// 保存到数据库中
ImagesTable.save({
url: newUrl,
imageKey: image_key,
});

return image_key;
};
  1. 新建 search.js 文件

使用网易云音乐的搜索接口根据关键词搜索音乐,可以根据该文档自建 API,这里不多赘述了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
const axios = require("axios");
const imageUpload = require("./image");

const request = axios.create({
baseURL: process.env.music_api,
});

// 搜索歌曲
module.exports = async function (params) {
const { keyword } = params;

const result = await request.get(`/search?keywords=${keyword}&type=1018`);
// 取前三首
const songs = result.data.result.song.songs.slice(0, 3);

const _list = await songs.map((data) => {
const artistNames = data.ar.length
? [...data.ar]
.reverse()
.reduce((ac, a) => `${a.name} ${ac}`, "")
.trim()
: "";

return {
imgUrl: data.al.picUrl,
title: `${data.name}`,
desc: `${artistNames} · ${data.al.name}`,
artistId: data.ar[0].id,
albumId: data.al.id,
artistName: artistNames,
albumName: data.al.name,
id: data.id,
};
});

const imageKeys = await Promise.all(
_list.map((data) => imageUpload({ url: data.imgUrl }))
);
const list = _list.map((data, index) => ({
...data,
imageKey: imageKeys[index],
}));

return list;
};
  1. 新建 reaction.js 文件

该文件就是用来生成消息卡片富文本,富文本 JSON 可以先在飞书的消息卡片搭建工具搭建好,再复制到该文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// 生成消息卡片

const generatePath = (id) => {
return `https://applink.feishu.cn/client/web_app/open?appId=${process.env.app_id}&mode=sidebar&id=${id}`;
};

const generateHeader = (keyword) => {
return {
template: "turquoise",
title: {
content: `🎵 ${keyword}`,
tag: "plain_text",
},
};
};

const generateCard = (data) => {
return {
tag: "div",
text: {
tag: "lark_md",
content: `**${data.title}**\n${data.desc}\n[打开播放器](${generatePath(
data.id
)})`,
},
extra: {
tag: "img",
img_key: data.imageKey,
alt: {
tag: "plain_text",
content: data.title,
},
},
};
};

module.exports = ({ keyword, list }) => {
console.log("search keyword: ", keyword);
console.log("search result: ", list);

const header = generateHeader(keyword);

const elements = list.reduce((result, item, index) => {
const card = generateCard(item);
if (index > 0) {
result.push({ tag: "hr" });
}
return [...result, card];
}, []);

return {
config: {
wide_screen_mode: true,
},
elements,
header,
};
};
  1. 新建 index.js 文件

最后再新建个入口文件,集成下前面的几个模块:

  1. 首先飞书开放平台会向接口发送个验证请求,请求为 JSON 格式,带 challenge 参数。应用接收此请求后,需解析出 challenge 值,并在 1 秒内回复 challenge 值。可以通过 params.type === ‘url_verification’ 来判断请求是否是验证请求。
  2. 通过 register 方法注册事件的回调,目前我们只订阅了 im.message.receive_v1这一个事件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// @see https://docs.aircode.io/guide/functions/
const aircode = require("aircode");
const lark = require("@larksuiteoapi/node-sdk");

const client = require("./client");
const search = require("./search");
const reaction = require("./reaction");

const sendMessage = (chatId, content, type = "text") => {
return client.im.message.create({
params: {
receive_id_type: "chat_id",
},
data: {
receive_id: chatId,
content: JSON.stringify(content),
msg_type: type,
},
});
};

const eventDispatcher = new lark.EventDispatcher({}).register({
"im.message.receive_v1": async (data) => {
const open_chat_id = data.message.chat_id;
const msg = JSON.parse(data.message.content).text;
const keyword = msg.replace(/^@\w+/, "").trim();

let res;

if (!keyword) {
res = await sendMessage(open_chat_id, {
text: "请告诉我一些关键词,比如歌名,歌手名,歌单名,用户名等~",
});
} else {
const list = await search({ keyword });
const interactive = reaction({ keyword, list });
res = await sendMessage(open_chat_id, interactive, "interactive");
}

return res;
},
});

module.exports = async function (params, context) {
console.log("Received params:", params);

// url 验证
if (params.type === "url_verification") {
return { challenge: params.challenge };
}

// 根据 message_id 去重复消息
const message_id = params.event.message.message_id;
const LogsTable = aircode.db.table("logs");
const findResult = await LogsTable.where({ message_id }).find();
if (findResult.length > 0) return `重复消息 - ${message_id}`;
LogsTable.save({ message_id });

return await eventDispatcher.invoke(params);
};
  1. 点击 Deploy按钮,发布所有函数,复制 index.js 的公网请求路径到飞书应用设置中

总结

AirCode 用来开发机器人相当方面,替我们省掉了数据库,文件服务,线上运维等一系类繁琐的事情,使用一个浏览器就能快速的开发上线。再搭配上 Github Page 就能白嫖到底。