官网:https://workers.cloudflare.com/
我也不做过多介绍了,可以从官网上了解,简单来说它是个运行 javascript 的 serverless 平台,虽然还说支持 Rust 或者 C 等等,但实际上得将它们编译为 Wasm
有官方客户端 wrangler,但这客户端 bug 是真的多,请做好各种搜 issue 的准备(
以下描述针对免费版计划
为什么值得用:
局限性:
由于免费版套餐也可以白嫖 KV 了,这意味着你可以编写有状态的 bot 逻辑了,但 KV 的写入生效可能存在延迟,并且存在每秒一次(目前)的频率限制,不能与内存相提并论,因此请根据实际情况合理使用
实现 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()
函数,用于设置 headerheader
属性且为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 来建仓库,或者是根据下面的步骤自己动手
我将以上解决方案写了个 npm 包,可以直接作为 cfworker 中间件使用
在你的新项目中安装这些依赖:
npm i @cfworker/web cfworker-middware-telegraf telegraf
npm i -D webpack webpack-cli
主入口应该为这样的结构
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
要在 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,如果仍无法正常执行那就只能放弃使用该依赖了
接着我们设置 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);