用 serverless 解放你的服务器
Head Pic: #アークナイツ timeout - Alcxome - pixiv
Cloudflare Workers
官网:https://workers.cloudflare.com/
我也不做过多介绍了,可以从官网上了解,简单来说它是个运行 javascript 的 serverless 平台,虽然还说支持 Rust 或者 C 等等,但实际上得将它们编译为 Wasm
有官方客户端 wrangler,但这客户端 bug 是真的多,请做好各种搜 issue 的准备(
以下描述针对免费版计划
为什么值得用:
- 每天免费调用十万次,超出只会终止而不扣费
- 最多创建 30 个 worker,管够
- 赠送一个专门用于调用 worker 的 workers.dev 子域,子域名自定义
- 可以自定义路由来使用你自己的域名
- 没有流量限制
- 可以使用 KV(cf 提供的键值数据库,2020年11月起对免费套餐开放使用了)
局限性:
- Serverless 的特性所决定的局限性
- 每个请求限制 10ms CPU 时间(网络 IO 时间不计),无法完成某些太耗算力的任务,例如较复杂的图片处理或者使用 cheerio 这类效率不高的 HTML 解析库
- (目前)不允许使用 websocket
关于 KV
由于免费版套餐也可以白嫖 KV 了,这意味着你可以编写有状态的 bot 逻辑了,但 KV 的写入生效可能存在延迟,并且存在每秒一次(目前)的频率限制,不能与内存相提并论,因此请根据实际情况合理使用
Telegram Bot
实现 Telegram Bot 的处理消息有两种方式,长轮询和 webhook,想依靠 serverless 处理则选用 webhook 方式
注意,webhook 每一次被 tg 服务器调用都只能返回一次,因此没办法做到“等待某条消息发送后再xxxx”,只能一次性发送完所有操作
我一般用 Telegraf 框架开发,所以这里主要说下怎么将使用该框架开发的 bot 迁移至 cfworker
迁移思路
如果只想知道最终该怎么做可以直接跳过这一节
首先 Telegraf 的文档有说明如何处理 webhook
我一般使用 cf 官方的框架来开发 cfworker:
这是一个类 Koa 框架,并包含了 Router,但 cfworker 的Request
和Response
的 API 和 Koa 略有不同,具体可以见官方文档
综合考虑决定参考 Telegraf 文档中 Koa 的示例,使用handleUpdate
方法来处理 webhook
这个函数接受两个参数,第一个是 webhook 请求的 payload,也就是JSON.parse
body 所得到的内容,这个只要await req.json()
就可以得到
而第二个参数是一个 http.ServerResponse,但 web-router 的ResponseBuilder
由于 API 不同所以不能直接拿来用,需要改造一下
Telegraf 框架的这段代码作用就是产生 webhook 的应答,因此我从这里入手进行分析
function isKoaResponse (response) {
return typeof response.set === 'function' && typeof response.header === 'object'
}
function answerToWebhook (response, payload = {}, options) {
if (!includesMedia(payload)) {
if (isKoaResponse(response)) {
response.body = payload
return Promise.resolve(WEBHOOK_REPLY_STUB)
}
if (!response.headersSent) {
response.setHeader('content-type', 'application/json')
}
return new Promise((resolve) => {
if (response.end.length === 2) {
response.end(JSON.stringify(payload), 'utf-8')
return resolve(WEBHOOK_REPLY_STUB)
}
response.end(JSON.stringify(payload), 'utf-8', () => resolve(WEBHOOK_REPLY_STUB))
})
}
return buildFormDataConfig(payload, options.agent)
.then(({ headers, body }) => {
if (isKoaResponse(response)) {
Object.keys(headers).forEach(key => response.set(key, headers[key]))
response.body = body
return Promise.resolve(WEBHOOK_REPLY_STUB)
}
if (!response.headersSent) {
Object.keys(headers).forEach(key => response.setHeader(key, headers[key]))
}
return new Promise((resolve) => {
response.on('finish', () => resolve(WEBHOOK_REPLY_STUB))
body.pipe(response)
})
})
}
根据 cfworker 中Response
的 API,其body
是支持被赋值Stream
的,因此向 KoaResponse 靠拢会比较好处理,不需要考虑 pipe 的问题
那么就需要使isKoaResponse(response)
为true
,根据这段代码我们需要对 web-router 的ResponseBuilder
做的适配有以下几点:
- 实现
set()
函数,用于设置 header - 有
header
属性且为Object
,由于没有实际使用所以随便糊弄也可以 body
支持被赋值一个Object
(但Array
最好也考虑进去,与 Koa API 一致),相当于构造一个 JSON Response
考虑到第三点,做一个 Proxy 就可以实现
function getKoaLikeResponse(res) {
return new Proxy(
Object.assign(res, {
set: (...args) => res.headers.set(...args),
header: res.headers,
}),
{
set(obj, prop, value) {
if (prop === 'body' && ['Object', 'Array'].includes(Object.getPrototypeOf(value).constructor.name)) {
obj.body = JSON.stringify(value);
obj.headers.set('content-type', 'application/json');
return true;
}
return Reflect.set(...arguments);
},
}
);
}
迁移流程
你可以选择直接使用我写好的模板 Tsuk1ko/cfworker-telegraf-template 来建仓库,或者是根据下面的步骤自己动手
0. 安装依赖
我将以上解决方案写了个 npm 包,可以直接作为 cfworker 中间件使用
在你的新项目中安装这些依赖:
npm i @cfworker/web cfworker-middware-telegraf telegraf
npm i -D webpack webpack-cli
1. 写代码
主入口应该为这样的结构
const { Telegraf } = require('telegraf');
const { Application, Router } = require('@cfworker/web');
const createTelegrafMiddware = require('cfworker-middware-telegraf');
const bot = new Telegraf('BOT_TOKEN');
// 写 bot 逻辑,但不要 bot.launch()
const router = new Router();
// `/SECRET_PATH` 指的是一个不容易猜到的路径,以防止他人访问你的 webhook
// 可以滚键盘或者用 UUID 之类的生成,例如 '/d4507ff0-08d1-4160-bad8-1addf587374a'
router.post('/SECRET_PATH', createTelegrafMiddware(bot));
new Application().use(router.middleware).listen();
所以很简单,把这段代码抄走然后补上你的 bot 逻辑即可
顺带一提,cfworker 支持设置 secret,它们会成为代码中的全局变量,所以像TG_BOT_TOKEN
和SECRET_PATH
这类不宜写入代码的变量可以直接设置成 secret
2. webpack
要在 cfworker 上运行需要 webpack 一下,把依赖全打进一个文件中,然后……粗暴的复制粘贴到 cfworker 编辑器中保存就行
当然你也可以用 wrangler,可通过配置实现自动 webpack 并部署
下面是一份示例配置
// webpack.config.js
module.exports = {
target: 'webworker',
entry: './index.js',
mode: 'production',
node: {
fs: 'empty',
},
module: {
rules: [
{
test: /\.mjs$/,
include: /node_modules/,
type: 'javascript/auto',
},
],
},
};
至于其中的 node 配置是干嘛的,你可以看 webpack 文档
如果在打包时出现找不到模块错误,就说明你的依赖中用到了 webworker 不支持的 node 模块或全局变量,需要配置上面所说的 node
如果代码实际运行时不会用到这些模块或全局变量,则 empty 就可以解决;如果实际用到则需要尝试 webpack 提供的 polyfill 或 mock,如果仍无法正常执行那就只能放弃使用该依赖了
3. 设置 webhook
接着我们设置 bot 的 webhook,另起一个 js 在本地执行一下即可
只用执行一次即可,这段不需要放进 bot 代码中
const Telegraf = require('telegraf');
const bot = new Telegraf(TG_BOT_TOKEN);
// 设置 webhook,请改成你自己的回调地址
bot.telegram.setWebhook('https://name.subdomain.workers.dev/SECRET_PATH');
// 删除 webhook
// bot.telegram.deleteWebhook();
// 查看当前 webhook
// bot.telegram.getWebhookInfo().then(console.log);
版权声明:本文为原创文章,版权归 神代綺凜 所有。
本文链接:https://moe.best/tutorial/cfworker-telegraf-tgbot.html
所有原创文章采用 知识共享署名-非商业性使用 4.0 国际许可协议 进行许可。
您可以自由的转载和修改,但请务必注明文章来源并且不可用于商业目的。
https://s2.loli.net/2021/12/16/bShyZfFRjtzdOXW.png
tg 对我来说不是主力聊天软件,这些 bot 也很少用