tags:
categories: tools
在线文档:https://docs-cn.aircode.io/
AirCode 是一个在线开发和部署 Node.js 应用的平台,为全栈工程师量身定制,目标是让开发变得简单。
并且提供一个简易的 WebIDE 和开箱即用的云服务,让开发者无需再操心后端选型、环境搭建和线上运维等一系列繁琐之事,只需打开浏览器即可完成产品开发,并部署到全球节点。
但是 AirCode 有资源限制,不建议用于高频的服务,但是我们要开发的机器人接口调用频率不会特别高,因此正适合 AirCode。
效果

@机器人 告诉它你想听的歌曲名字,机器人会返回给你最匹配的三条,点击「打开播放器」,会在右侧弹出在线播放器。
音乐播放器
播放器使用 Github 上的开源项目「music-motion-x」,该项目是 SSR 服务端渲染项目,但是我们需要部署到 Github Pages 上,因此需要一些改造。
- 使用 Vite 新建个 react + ts 的模板:
1
| yarn create vite my-vue-app --template react-ts
|
接下来安装并配置 eslint
、commitlint
、husky
和lint-staged
等工具,配置细节本文就不赘述了,详细配置见 https://github.com/bijinfeng/music-motion-x
迁移代码至新项目中
添加 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
|
- 提交代码至 Github 就可以触发自动部署了
新建飞书应用
在飞书开放平台新建个应用,图标和名称自定义,添加应用后添加「网页」和「机器人」应用:

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

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

接下来我们还需要申请权限和订阅事件,用于机器人消息的收发:
- 订阅事件:只需要「接收消息」的事件就行,请求地址配置成上面 👆 机器人请求地址


机器人 webhook 接口开发
上面应用和机器人配置好后,接下来就需要开发 webhook 接口供机器人订阅。
首先需要新建个应用,然后安装两个依赖:
- 新建 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_id
和 app_secret
这两个环境变量从飞书开放平台获取后,配置到 AirCode 的 Enviroments Tab 下:

- 新建 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; }
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; };
|
- 新建 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; };
|
- 新建 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, }; };
|
- 新建 index.js 文件
最后再新建个入口文件,集成下前面的几个模块:
- 首先飞书开放平台会向接口发送个验证请求,请求为 JSON 格式,带 challenge 参数。应用接收此请求后,需解析出 challenge 值,并在 1 秒内回复 challenge 值。可以通过 params.type === ‘url_verification’ 来判断请求是否是验证请求。
- 通过 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
| 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);
if (params.type === "url_verification") { return { challenge: params.challenge }; }
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); };
|
- 点击
Deploy
按钮,发布所有函数,复制 index.js 的公网请求路径到飞书应用设置中

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