在Slackbots中实现交互性、调度和监控以管理WordPress网站

在Slackbots中实现交互性、调度和监控以管理WordPress网站

文章目录

  • 先决条件
  • 入门指南
  • 为您的Slackbot增添交互性
  • 使用下拉菜单选择站点
  • 确认对话框
  • 进度指示器
  • 成功/失败通知
  • 使用计划任务自动执行WordPress任务
  • 创建计划任务命令
  • 取消计划任务
  • 列出所有计划任务
  • 定期维护
  • 自动报告
  • 错误处理和监控
  • 使用Winston进行结构化日志记录
  • 通过Slack向管理员发送警报
  • 使用基本指标跟踪性能
  • 可选:定义恢复步骤
  • 部署和管理您的Slackbot
  • 小结

在Slackbots中实现交互性、调度和监控以管理WordPress网站

Slack 机器人无需等待您输入命令。正确设置后,您的机器人可以通过提供交互式按钮、下拉菜单、计划任务和智能提醒来帮助您管理 WordPress 网站——所有这些都在 Slack 内部完成。

本文将向您展示如何为您的 Slack 机器人添加交互性、自动化和监控功能。

先决条件

开始之前,请确保您已具备:

  • 一个拥有机器人权限和斜线命令的 Slack 应用。
  • 一个拥有 API 访问权限的服务器帐户以及一个用于测试的网站。
  • 本地安装了 Node.js 和 NPM。
  • 熟悉 JavaScript 的基本知识(或至少能够熟练地复制和调整代码)。
  • Slack 和网站服务器的 API 密钥。

入门指南

要构建此 Slack 机器人,需要使用 Node.js 和 Slack 的 Bolt 框架来连接通过服务器 API 触发操作的斜线命令。

本指南不对创建 Slack 应用或获取服务器 API 访问权限展开。您需要通过网上资料了解如何创建 Slack 应用、获取机器人令牌和签名密钥,以及获取服务器 API 密钥。

为您的Slackbot增添交互性

Slackbot 不必仅依赖斜线命令。借助按钮、菜单和模态框等交互式组件,您可以将机器人变成更加直观、用户友好的工具。

想象一下,在检查站点状态后立即点击标有“Clear Cache”的按钮,而不是输入 /clear_cache environment_id。为此,您需要 Slack 的 Web API 客户端。使用以下命令将其安装到您的项目中:

npm install @slack/web-api

然后在你的 app.js 中初始化它:

const { WebClient } = require('@slack/web-api');
const web = new WebClient(process.env.SLACK_BOT_TOKEN);

确保已在 .env 文件中设置 SLACK_BOT_TOKEN。现在,让我们增强上一篇文章中的 /site_status 命令。我们不再只是发送文本,而是附加一些按钮,用于执行快速操作,例如清除缓存创建备份检查详细状态

更新后的处理程序如下所示:

app.command('/site_status', async ({ command, ack, say }) => {
await ack();
const environmentId = command.text.trim();
if (!environmentId) {
await say('Please provide an environment ID. Usage: `/site_status [environment-id]`');
return;
}
try {
// Get environment status
const response = await kinstaRequest(`/sites/environments/${environmentId}`);
if (response && response.site && response.site.environments && response.site.environments.length > 0) {
const env = response.site.environments[0];
// Format the status message
let statusMessage = formatSiteStatus(env);
// Send message with interactive buttons
await web.chat.postMessage({
channel: command.channel_id,
text: statusMessage,
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: statusMessage
}
},
{
type: 'actions',
elements: [
{
type: 'button',
text: {
type: 'plain_text',
text: ' Clear Cache',
emoji: true
},
value: environmentId,
action_id: 'clear_cache_button'
},
{
type: 'button',
text: {
type: 'plain_text',
text: ' Detailed Status',
emoji: true
},
value: environmentId,
action_id: 'detailed_status_button'
},
{
type: 'button',
text: {
type: 'plain_text',
text: ' Create Backup',
emoji: true
},
value: environmentId,
action_id: 'create_backup_button'
}
]
}
]
});
} else {
await say(` No environment found with ID: \`${environmentId}\``);
}
} catch (error) {
console.error('Error checking site status:', error);
await say(` Error checking site status: ${error.message}`);
}
});

每次点击按钮都会触发一个操作。以下是我们对 Clear Cache 按钮的处理方式:

// Add action handlers for the buttons
app.action('clear_cache_button', async ({ body, ack, respond }) => {
await ack();
const environmentId = body.actions[0].value;
await respond(` Clearing cache for environment \`${environmentId}\`...`);
try {
// Call Kinsta API to clear cache
const response = await kinstaRequest(
`/sites/environments/${environmentId}/clear-cache`,
'POST'
);
if (response && response.operation_id) {
await respond(` Cache clearing operation started! Operation ID: \`${response.operation_id}\``);
} else {
await respond(' Cache clearing request was sent, but no operation ID was returned.');
}
} catch (error) {
console.error('Cache clearing error:', error);
await respond(` Error clearing cache: ${error.message}`);
}
});

您可以对备份和状态按钮遵循相同的模式,只需将每个按钮链接到适当的 API 端点或命令逻辑即可。

// Handlers for other buttons
app.action('detailed_status_button', async ({ body, ack, respond }) => {
await ack();
const environmentId = body.actions[0].value;
// Implement detailed status check similar to the /detailed_status command
// ...
});
app.action('create_backup_button', async ({ body, ack, respond }) => {
await ack();
const environmentId = body.actions[0].value;
// Implement backup creation similar to the /create_backup command
// ...
});

使用下拉菜单选择站点

输入环境 ID 可不是件容易的事。而且,还指望每个团队成员都记住哪个 ID 属于哪个环境?这可不现实。

让我们让它更直观一些。我们不再要求用户输入 /site_status [environment-id],而是提供一个 Slack 下拉菜单,让他们可以从列表中选择站点。一旦用户选择站点,机器人就会显示状态,并附加我们之前实现的快速操作按钮。

为此,我们:

  • 从 API 获取所有站点
  • 获取每个站点的环境
  • 使用以下选项构建下拉菜单
  • 处理用户的选择并显示站点状态

以下是显示下拉菜单的命令:

app.command('/select_site', async ({ command, ack, say }) => {
await ack();
try {
// Get all sites
const response = await kinstaRequest('/sites');
if (response && response.company && response.company.sites) {
const sites = response.company.sites;
// Create options for each site
const options = [];
for (const site of sites) {
// Get environments for this site
const envResponse = await kinstaRequest(`/sites/${site.id}/environments`);
if (envResponse && envResponse.site && envResponse.site.environments) {
for (const env of envResponse.site.environments) {
options.push({
text: {
type: 'plain_text',
text: `${site.name} (${env.name})`
},
value: env.id
});
}
}
}
// Send message with dropdown
await web.chat.postMessage({
channel: command.channel_id,
text: 'Select a site to manage:',
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: '*Select a site to manage:*'
},
accessory: {
type: 'static_select',
placeholder: {
type: 'plain_text',
text: 'Select a site'
},
options: options.slice(0, 100), // Slack has a limit of 100 options
action_id: 'site_selected'
}
}
]
});
} else {
await say(' Error retrieving sites. Please check your API credentials.');
}
} catch (error) {
console.error('Error:', error);
await say(` Error retrieving sites: ${error.message}`);
}
});

当用户选择一个站点时,我们使用此操作处理程序来处理:

// Handle the site selection
app.action('site_selected', async ({ body, ack, respond }) => {
await ack();
const environmentId = body.actions[0].selected_option.value;
const siteName = body.actions[0].selected_option.text.text;
// Get environment status
try {
const response = await kinstaRequest(`/sites/environments/${environmentId}`);
if (response && response.site && response.site.environments && response.site.environments.length > 0) {
const env = response.site.environments[0];
// Format the status message
let statusMessage = `*${siteName}* (ID: \`${environmentId}\`)\n\n${formatSiteStatus(env)}`;
// Send message with interactive buttons (similar to the site_status command)
// ...
} else {
await respond(` No environment found with ID: \`${environmentId}\``);
}
} catch (error) {
console.error('Error:', error);
await respond(` Error retrieving environment: ${error.message}`);
}
});

现在我们的机器人可以通过按钮触发操作并从列表中选择网站,让我们确保不会意外运行危险操作。

确认对话框

有些操作绝对不应该意外运行。清除缓存听起来可能无伤大雅,但如果您正在开发生产网站,您可能不想通过单击操作来完成——尤其是在您只是检查网站状态时。这时,Slack 模态框(对话框)就派上用场了。

点击 clear_cache_button 按钮时,我们不会立即清除缓存,而是显示一个确认模态框。具体方法如下:

app.action('clear_cache_button', async ({ body, ack, context }) => {
await ack();
const environmentId = body.actions[0].value;
// Open a confirmation dialog
try {
await web.views.open({
trigger_id: body.trigger_id,
view: {
type: 'modal',
callback_id: 'clear_cache_confirmation',
private_metadata: environmentId,
title: {
type: 'plain_text',
text: 'Confirm Cache Clearing'
},
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `Are you sure you want to clear the cache for environment \`${environmentId}\`?`
}
}
],
submit: {
type: 'plain_text',
text: 'Clear Cache'
},
close: {
type: 'plain_text',
text: 'Cancel'
}
}
});
} catch (error) {
console.error('Error opening confirmation dialog:', error);
}
});

在上面的代码中,我们使用 web.views.open() 启动一个带有清晰标题、警告消息和两个按钮(“Clear Cache”和“Cancel”)的模态窗口,并将 environmentId 存储在 private_metadata 中,以便在用户点击“Clear Cache”时获取该 ID。

一旦用户点击模态窗口中的“Clear Cache”按钮,Slack 就会发送一个 view_submission 事件。以下是如何处理该事件并进行实际操作:

// Handle the confirmation dialog submission
app.view('clear_cache_confirmation', async ({ ack, body, view }) => {
await ack();
const environmentId = view.private_metadata;
const userId = body.user.id;
// Find a DM channel with the user to respond to
const result = await web.conversations.open({
users: userId
});
const channel = result.channel.id;
await web.chat.postMessage({
channel,
text: ` Clearing cache for environment \`${environmentId}\`...`
});
try {
// Call Kinsta API to clear cache
const response = await kinstaRequest(
`/sites/environments/${environmentId}/clear-cache`,
'POST'
);
if (response && response.operation_id) {
await web.chat.postMessage({
channel,
text: ` Cache clearing operation started! Operation ID: \`${response.operation_id}\``
});
} else {
await web.chat.postMessage({
channel,
text: ' Cache clearing request was sent, but no operation ID was returned.'
});
}
} catch (error) {
console.error('Cache clearing error:', error);
await web.chat.postMessage({
channel,
text: ` Error clearing cache: ${error.message}`
});
}
});

在此代码中,在用户确认后,我们从 private_metadata 中获取 environmentId,使用 web.conversations.open() 打开私人 DM 以避免混乱公共频道,运行 API 请求以清除缓存,并根据结果跟进成功或错误消息。

进度指示器

有些 Slack 命令是即时执行的,例如清除缓存或检查状态。但其他命令呢?则不然。

创建备份或部署文件可能需要几秒钟甚至几分钟。如果你的机器人在这段时间内一直处于静默状态,用户可能会认为出现了问题。

Slack 没有原生进度条,但我们可以通过一些创意来伪造一个。以下是一个辅助函数,它使用 Block Kit 为消息添加可视化进度条:

async function updateProgress(channel, messageTs, text, percentage) {
// Create a progress bar
const barLength = 20;
const filledLength = Math.round(barLength * (percentage / 100));
const bar = '█'.repeat(filledLength) + '░'.repeat(barLength - filledLength);
await web.chat.update({
channel,
ts: messageTs,
text: `${text} [${percentage}%]`,
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `${text} [${percentage}%]\n\`${bar}\``
}
}
]
});
}

让我们将其集成到 /create_backup 命令中。我们不会等待整个操作完成后再回复,而是在每个步骤中与用户进行核对。

app.command('/create_backup', async ({ command, ack, say }) => {
await ack();
const args = command.text.split(' ');
const environmentId = args[0];
const tag = args.length > 1 ? args.slice(1).join(' ') : `Manual backup ${new Date().toISOString()}`;
if (!environmentId) {
await say('Please provide an environment ID. Usage: `/create_backup [environment-id] [optional-tag]`');
return;
}
// Post initial message and get its timestamp for updates
const initial = await say(' Initiating backup...');
const messageTs = initial.ts;
try {
// Update progress to 10%
await updateProgress(command.channel_id, messageTs, ' Creating backup...', 10);
// Call Kinsta API to create a backup
const response = await kinstaRequest(
`/sites/environments/${environmentId}/manual-backups`,
'POST',
{ tag }
);
if (response && response.operation_id) {
await updateProgress(command.channel_id, messageTs, ' Backup in progress...', 30);
// Poll the operation status
let completed = false;
let percentage = 30;
while (!completed && percentage  setTimeout(resolve, 3000));
// Check operation status
const statusResponse = await kinstaRequest(`/operations/${response.operation_id}`);
if (statusResponse && statusResponse.operation) {
const operation = statusResponse.operation;
if (operation.status === 'completed') {
completed = true;
percentage = 100;
} else if (operation.status === 'failed') {
await web.chat.update({
channel: command.channel_id,
ts: messageTs,
text: ` Backup failed! Error: ${operation.error || 'Unknown error'}`
});
return;
} else {
// Increment progress
percentage += 10;
if (percentage > 95) percentage = 95;
await updateProgress(
command.channel_id, 
messageTs, 
' Backup in progress...', 
percentage
);
}
}
}
// Final update
await web.chat.update({
channel: command.channel_id,
ts: messageTs,
text: ` Backup completed successfully!`,
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: ` Backup completed successfully!\n*Tag:* ${tag}\n*Operation ID:* \`${response.operation_id}\``
}
}
]
});
} else {
await web.chat.update({
channel: command.channel_id,
ts: messageTs,
text: ' Backup request was sent, but no operation ID was returned.'
});
}
} catch (error) {
console.error('Backup creation error:', error);
await web.chat.update({
channel: command.channel_id,
ts: messageTs,
text: ` Error creating backup: ${error.message}`
});
}
});

成功/失败通知

目前,您的机器人可能只会返回类似 SuccessFailed的纯文本。虽然可以正常工作,但效果平淡,无法帮助用户理解成功的原因或失败时应该采取的措施。

让我们通过设置正确格式的成功和错误消息以及实用的上下文、建议和简洁的格式来解决这个问题。

将这些实用程序添加到您的 utils.js 中,以便您可以在所有命令中重复使用它们:

function formatSuccessMessage(title, details = []) {
let message = ` *${title}*\n\n`;
if (details.length > 0) {
details.forEach(detail => {
message += `• ${detail.label}: ${detail.value}\n`;
});
}
return message;
}
function formatErrorMessage(title, error, suggestions = []) {
let message = ` *${title}*\n\n`;
message += `*Error:* ${error}\n\n`;
if (suggestions.length > 0) {
message += '*Suggestions:*\n';
suggestions.forEach(suggestion => {
message += `• ${suggestion}\n`;
});
}
return message;
}
module.exports = {
connectToSite,
logCommand,
formatSuccessMessage,
formatErrorMessage
};

这些函数接受结构化输入,并将其转换为 Slack 友好的 Markdown 格式,并带有表情符号、标签和换行符。在繁忙的 Slack 线程中,扫描起来更加方便。以下是实际命令处理程序内部的样子。我们以 /clear_cache 为例:

app.command('/clear_cache', async ({ command, ack, say }) => {
await ack();
const environmentId = command.text.trim();
if (!environmentId) {
await say('Please provide an environment ID. Usage: `/clear_cache [environment-id]`');
return;
}
try {
await say(' Processing...');
// Call Kinsta API to clear cache
const response = await kinstaRequest(
`/sites/environments/${environmentId}/clear-cache`,
'POST'
);
if (response && response.operation_id) {
const { formatSuccessMessage } = require('./utils');
await say(formatSuccessMessage('Cache Clearing Started', [
{ label: 'Environment ID', value: `\`${environmentId}\`` },
{ label: 'Operation ID', value: `\`${response.operation_id}\`` },
{ label: 'Status', value: 'In Progress' }
]));
} else {
const { formatErrorMessage } = require('./utils');
await say(formatErrorMessage(
'Cache Clearing Error',
'No operation ID returned',
[
'Check your environment ID',
'Verify your API credentials',
'Try again later'
]
));
}
} catch (error) {
console.error('Cache clearing error:', error);
const { formatErrorMessage } = require('./utils');
await say(formatErrorMessage(
'Cache Clearing Error',
error.message,
[
'Check your environment ID',
'Verify your API credentials',
'Try again later'
]
));
}
});

使用计划任务自动执行WordPress任务

到目前为止,您的 Slackbot 所做的一切都是在有人明确触发命令时发生的。但并非所有任务都应该依赖于有人记得运行它。

如果您的机器人可以每晚自动备份您的网站,那会怎样?或者每天早上在团队起床之前检查是否有任何网站宕机?

我们将使用 node-schedule 库根据 cron 表达式运行任务。首先,安装它:

npm install node-schedule

现在,将其设置在 app.js 的顶部:

const schedule = require('node-schedule');

我们还需要一种方法来跟踪活动的计划作业,以便用户可以稍后列出或取消它们:

const scheduledJobs = {};

创建计划任务命令

我们将从一个基本的 /schedule_task 命令开始,该命令接受任务类型(backupclear_cache, 或 status_check)、环境 ID 和 cron 表达式。

/schedule_task backup 12345 0 0 * * *

这将安排在午夜进行每日备份。完整的命令处理程序如下:

app.command('/schedule_task', async ({ command, ack, say }) => {
await ack();
const args = command.text.split(' ');
if (args.length  {
console.log(`Running scheduled ${taskType} for environment ${environmentId}`);
try {
switch (taskType) {
case 'backup':
await kinstaRequest(`/sites/environments/${environmentId}/manual-backups`, 'POST', {
tag: `Scheduled backup ${new Date().toISOString()}`
});
break;
case 'clear_cache':
await kinstaRequest(`/sites/environments/${environmentId}/clear-cache`, 'POST');
break;
case 'status_check':
const response = await kinstaRequest(`/sites/environments/${environmentId}`);
const env = response?.site?.environments?.[0];
if (env) {
console.log(`Status: ${env.display_name} is ${env.is_blocked ? 'blocked' : 'running'}`);
}
break;
}
} catch (err) {
console.error(`Scheduled ${taskType} failed for ${environmentId}:`, err.message);
}
});
scheduledJobs[jobId] = {
job,
taskType,
environmentId,
cronSchedule,
userId: command.user_id,
createdAt: new Date().toISOString()
};
await say(` Scheduled task created!
*Task:* ${taskType}
*Environment:* \`${environmentId}\`
*Cron:* \`${cronSchedule}\`
*Job ID:* \`${jobId}\`
To cancel this task, run \`/cancel_task ${jobId}\``);
} catch (err) {
console.error('Error creating scheduled job:', err);
await say(` Failed to create scheduled task: ${err.message}`);
}
});

取消计划任务

如果情况发生变化或不再需要该任务,用户可以使用以下方式取消:

/cancel_task

具体实现如下:

app.command('/cancel_task', async ({ command, ack, say }) => {
await ack();
const jobId = command.text.trim();
if (!scheduledJobs[jobId]) {
await say(` No task found with ID: \`${jobId}\``);
return;
}
scheduledJobs[jobId].job.cancel();
delete scheduledJobs[jobId];
await say(` Task \`${jobId}\` has been cancelled.`);
});

列出所有计划任务

我们还允许用户查看所有已计划的作业:

app.command('/list_tasks', async ({ command, ack, say }) => {
await ack();
const tasks = Object.entries(scheduledJobs);
if (tasks.length === 0) {
await say('No scheduled tasks found.');
return;
}
let message = '*Scheduled Tasks:*\n\n';
for (const [jobId, job] of tasks) {
message += `• *Job ID:* \`${jobId}\`\n`;
message += `  - Task: ${job.taskType}\n`;
message += `  - Environment: \`${job.environmentId}\`\n`;
message += `  - Cron: \`${job.cronSchedule}\`\n`;
message += `  - Created by: \n\n`;
}
message += '_Use `/cancel_task [job_id]` to cancel a task._';
await say(message);
});

这赋予了你的 Slackbot 全新级别的自主性。备份、缓存清除和状态检查不再需要人工干预。它们会安静、可靠、按时地运行。

定期维护

有时,您需要定期运行一组维护任务,例如每周备份和周日晚上清除缓存。这时,维护窗口就派上用场了。

维护窗口是指机器人自动运行预定义任务的预定时间段,例如:

  • 创建备份
  • 清除缓存
  • 发送开始和完成通知

格式很简单:

/maintenance_window [environment_id] [day_of_week] [hour] [duration_hours]

例如:

/maintenance_window 12345 Sunday 2 3

这意味着每周日凌晨 2 点,都会运行 3 小时的维护任务。完整的实现如下:

// Add a command to create a maintenance window
app.command('/maintenance_window', async ({ command, ack, say }) => {
await ack();
// Expected format: environment_id day_of_week hour duration
// Example: /maintenance_window 12345 Sunday 2 3
const args = command.text.split(' ');
if (args.length < 4) {
await say('Please provide all required parameters. Usage: `/maintenance_window [environment_id] [day_of_week] [hour] [duration_hours]`');
return;
}
const [environmentId, dayOfWeek, hour, duration] = args;
// Validate inputs
const validDays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
if (!validDays.includes(dayOfWeek)) {
await say(`Invalid day of week. Please choose from: ${validDays.join(', ')}`);
return;
}
const hourInt = parseInt(hour, 10);
if (isNaN(hourInt) || hourInt  23) {
await say('Hour must be a number between 0 and 23.');
return;
}
const durationInt = parseInt(duration, 10);
if (isNaN(durationInt) || durationInt  12) {
await say('Duration must be a number between 1 and 12 hours.');
return;
}
// Convert day of week to cron format
const dayMap = {
'Sunday': 0,
'Monday': 1,
'Tuesday': 2,
'Wednesday': 3,
'Thursday': 4,
'Friday': 5,
'Saturday': 6
};
const cronDay = dayMap[dayOfWeek];
// Create cron schedule for the start of the maintenance window
const cronSchedule = `0 ${hourInt} * * ${cronDay}`;
// Generate a unique job ID
const jobId = `maintenance_${environmentId}_${Date.now()}`;
// Schedule the job
try {
const job = schedule.scheduleJob(cronSchedule, async function() {
// Start of maintenance window
await web.chat.postMessage({
channel: command.channel_id,
text: ` *Maintenance Window Started*\n*Environment:* \`${environmentId}\`\n*Duration:* ${durationInt} hours\n\nAutomatic maintenance tasks are now running.`
});
// Perform maintenance tasks
try {
// 1. Create a backup
const backupResponse = await kinstaRequest(
`/sites/environments/${environmentId}/manual-backups`,
'POST',
{ tag: `Maintenance backup ${new Date().toISOString()}` }
);
if (backupResponse && backupResponse.operation_id) {
await web.chat.postMessage({
channel: command.channel_id,
text: ` Maintenance backup created. Operation ID: \`${backupResponse.operation_id}\``
});
}
// 2. Clear cache
const cacheResponse = await kinstaRequest(
`/sites/environments/${environmentId}/clear-cache`,
'POST'
);
if (cacheResponse && cacheResponse.operation_id) {
await web.chat.postMessage({
channel: command.channel_id,
text: ` Cache cleared. Operation ID: \`${cacheResponse.operation_id}\``
});
}
// 3. Schedule end of maintenance window notification
setTimeout(async () => {
await web.chat.postMessage({
channel: command.channel_id,
text: ` *Maintenance Window Completed*\n*Environment:* \`${environmentId}\`\n\nAll maintenance tasks have been completed.`
});
}, durationInt * 60 * 60 * 1000); // Convert hours to milliseconds
} catch (error) {
console.error('Maintenance tasks error:', error);
await web.chat.postMessage({
channel: command.channel_id,
text: ` Error during maintenance: ${error.message}`
});
}
});
// Store the job for later cancellation
scheduledJobs[jobId] = {
job,
taskType: 'maintenance',
environmentId,
cronSchedule,
dayOfWeek,
hour: hourInt,
duration: durationInt,
userId: command.user_id,
createdAt: new Date().toISOString()
};
await say(` Maintenance window scheduled!
*Environment:* \`${environmentId}\`
*Schedule:* Every ${dayOfWeek} at ${hourInt}:00 for ${durationInt} hours
*Job ID:* \`${jobId}\`
To cancel this maintenance window, use \`/cancel_task ${jobId}\``);
} catch (error) {
console.error('Error scheduling maintenance window:', error);
await say(` Error scheduling maintenance window: ${error.message}`);
}
});

自动报告

您肯定不想每个星期一醒来都疑惑您的 WordPress 网站是否已备份,或者是否已经宕机数小时。借助自动报告功能,您的 Slack 机器人可以按计划为您和您的团队提供快速的性能摘要。

此类报告非常适合监控以下内容:

  • 当前网站状态
  • 过去 7 天的备份活动
  • PHP 版本和主域名
  • 任何危险信号,例如被阻止的环境或缺少备份

让我们构建一个 /schedule_report 命令来自动执行此操作。

// Add a command to schedule weekly reporting
app.command('/schedule_report', async ({ command, ack, say }) => {
await ack();
// Expected format: environment_id day_of_week hour
// Example: /schedule_report 12345 Monday 9
const args = command.text.split(' ');
if (args.length < 3) {
await say('Please provide all required parameters. Usage: `/schedule_report [environment_id] [day_of_week] [hour]`');
return;
}
const [environmentId, dayOfWeek, hour] = args;
// Validate inputs
const validDays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
if (!validDays.includes(dayOfWeek)) {
await say(`Invalid day of week. Please choose from: ${validDays.join(', ')}`);
return;
}
const hourInt = parseInt(hour, 10);
if (isNaN(hourInt) || hourInt  23) {
await say('Hour must be a number between 0 and 23.');
return;
}
// Convert day of week to cron format
const dayMap = {
'Sunday': 0,
'Monday': 1,
'Tuesday': 2,
'Wednesday': 3,
'Thursday': 4,
'Friday': 5,
'Saturday': 6
};
const cronDay = dayMap[dayOfWeek];
// Create cron schedule for the report
const cronSchedule = `0 ${hourInt} * * ${cronDay}`;
// Generate a unique job ID
const jobId = `report_${environmentId}_${Date.now()}`;
// Schedule the job
try {
const job = schedule.scheduleJob(cronSchedule, async function() {
// Generate and send the report
await generateWeeklyReport(environmentId, command.channel_id);
});
// Store the job for later cancellation
scheduledJobs[jobId] = {
job,
taskType: 'report',
environmentId,
cronSchedule,
dayOfWeek,
hour: hourInt,
userId: command.user_id,
createdAt: new Date().toISOString()
};
await say(` Weekly report scheduled!
*Environment:* \`${environmentId}\`
*Schedule:* Every ${dayOfWeek} at ${hourInt}:00
*Job ID:* \`${jobId}\`
To cancel this report, use \`/cancel_task ${jobId}\``);
} catch (error) {
console.error('Error scheduling report:', error);
await say(` Error scheduling report: ${error.message}`);
}
});
// Function to generate weekly report
async function generateWeeklyReport(environmentId, channelId) {
try {
// Get environment details
const response = await kinstaRequest(`/sites/environments/${environmentId}`);
if (!response || !response.site || !response.site.environments || !response.site.environments.length) {
await web.chat.postMessage({
channel: channelId,
text: ` Weekly Report Error: No environment found with ID: \`${environmentId}\``
});
return;
}
const env = response.site.environments[0];
// Get backups for the past week
const backupsResponse = await kinstaRequest(`/sites/environments/${environmentId}/backups`);
let backupsCount = 0;
let latestBackup = null;
if (backupsResponse && backupsResponse.environment && backupsResponse.environment.backups) {
const oneWeekAgo = new Date();
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
const recentBackups = backupsResponse.environment.backups.filter(backup => {
const backupDate = new Date(backup.created_at);
return backupDate >= oneWeekAgo;
});
backupsCount = recentBackups.length;
if (recentBackups.length > 0) {
latestBackup = recentBackups.sort((a, b) => b.created_at - a.created_at)[0];
}
}
// Get environment status
const statusEmoji = env.is_blocked ? '' : '';
const statusText = env.is_blocked ? 'Blocked' : 'Running';
// Create report message
const reportDate = new Date().toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
});
const reportMessage = ` *Weekly Report - ${reportDate}*
*Site:* ${env.display_name}
*Environment ID:* \`${environmentId}\`
*Status Summary:*
• Current Status: ${statusEmoji} ${statusText}
• PHP Version: ${env.container_info?.php_engine_version || 'Unknown'}
• Primary Domain: ${env.primaryDomain?.name || env.domains?.[0]?.name || 'N/A'}
*Backup Summary:*
• Total Backups (Last 7 Days): ${backupsCount}
• Latest Backup: ${latestBackup ? new Date(latestBackup.created_at).toLocaleString() : 'N/A'}
• Latest Backup Type: ${latestBackup ? latestBackup.type : 'N/A'}
*Recommendations:*
• ${backupsCount === 0 ? ' No recent backups found. Consider creating a manual backup.' : ' Regular backups are being created.'}
• ${env.is_blocked ? ' Site is currently blocked. Check for issues.' : ' Site is running normally.'}
_This is an automated report. For detailed information, use the \`/site_status ${environmentId}\` command._`;
await web.chat.postMessage({
channel: channelId,
text: reportMessage
});
} catch (error) {
console.error('Report generation error:', error);
await web.chat.postMessage({
channel: channelId,
text: ` Error generating weekly report: ${error.message}`
});
}
}

错误处理和监控

一旦您的机器人开始执行实际操作(例如修改环境或触发计划任务),您需要的不仅仅是 console.log() 来跟踪幕后发生的情况。

让我们将其分解为简洁易维护的几个层:

使用Winston进行结构化日志记录

与其将日志打印到控制台,不如使用 winston 将结构化日志发送到文件,并可选地发送到 LogglyDatadog 等服务。使用以下命令安装:

npm install winston

接下来,设置 logger.js

const winston = require('winston');
const fs = require('fs');
const path = require('path');
const logsDir = path.join(__dirname, '../logs');
if (!fs.existsSync(logsDir)) fs.mkdirSync(logsDir);
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
defaultMeta: { service: 'wordpress-slack-bot' },
transports: [
new winston.transports.Console({ format: winston.format.simple() }),
new winston.transports.File({ filename: path.join(logsDir, 'error.log'), level: 'error' }),
new winston.transports.File({ filename: path.join(logsDir, 'combined.log') })
]
});
module.exports = logger;

然后,在您的 app.js 中,将所有 console.logconsole.error 调用替换为:

const logger = require('./logger');
logger.info('Cache clear initiated', { userId: command.user_id });
logger.error('API failure', { error: err.message });

通过Slack向管理员发送警报

您已拥有 ADMIN_USERS 环境变量,当出现严重故障时,您可以使用它在 Slack 中直接通知您的团队:

async function alertAdmins(message, metadata = {}) {
for (const userId of ADMIN_USERS) {
const dm = await web.conversations.open({ users: userId });
const channel = dm.channel.id;
let alert = ` *${message}*\n`;
for (const [key, value] of Object.entries(metadata)) {
alert += `• *${key}:* ${value}\n`;
}
await web.chat.postMessage({ channel, text: alert });
}
}

像这样使用:

await alertAdmins('Backup Failed', {
environmentId,
error: error.message,
user: ``
});

使用基本指标跟踪性能

如果您只是想了解机器人的健康状况,请不要完全使用 Prometheus。请保留一个轻量级的性能对象:

const metrics = {
apiCalls: 0,
errors: 0,
commands: 0,
totalTime: 0,
get avgResponseTime() {
return this.apiCalls === 0 ? 0 : this.totalTime / this.apiCalls;
}
};

在您的 kinstaRequest() 助手中更新此内容:

const start = Date.now();
try {
metrics.apiCalls++;
const res = await fetch(...);
return await res.json();
} catch (err) {
metrics.errors++;
throw err;
} finally {
metrics.totalTime += Date.now() - start;
}

通过类似 /bot_performance 的命令公开它:

app.command('/bot_performance', async ({ command, ack, say }) => {
await ack();
if (!ADMIN_USERS.includes(command.user_id)) {
return await say(' Not authorized.');
}
const msg = ` *Bot Metrics*
• API Calls: ${metrics.apiCalls}
• Errors: ${metrics.errors}
• Avg Response Time: ${metrics.avgResponseTime.toFixed(2)}ms
• Commands Run: ${metrics.commands}`;
await say(msg);
});

可选:定义恢复步骤

如果您想实现恢复逻辑(例如通过 SSH 重试缓存清除),只需创建一个辅助函数,如下所示:

async function attemptRecovery(environmentId, issue) {
logger.warn('Attempting recovery', { environmentId, issue });
if (issue === 'cache_clear_failure') {
// fallback logic here
}
// Return a recovery status object
return { success: true, message: 'Fallback ran.' };
}

除非是关键路径,否则不要将其包含在主命令逻辑中。很多情况下,最好记录错误,提醒管理员,然后由人工决定如何处理。

部署和管理您的Slackbot

您的机器人功能完善后,您应该将其部署到可以全天候运行的生产环境中。

您可以使用 Docker 容器化您的机器人,或将其部署到任何支持 Node.js 和后台服务的云平台。

上线前,请注意以下几点:

  • 所有密钥(Slack 令牌、API 密钥、SSH 密钥)均使用环境变量。
  • 设置日志记录和正常运行时间监控,以便在出现故障时及时了解情况。
  • 使用进程管理器(例如 PM2)或 Docker 的 restart: always 策略运行您的机器人,以确保其在崩溃或重启后仍保持活动状态。
  • 确保您的 SSH 密钥安全无虞,尤其是在将其用于自动化操作时。

小结

现在,您已将 Slackbot 从一个简单的命令处理程序升级为一个功能强大的工具,它具备真正的交互性、定时自动化和可靠的监控功能。这些功能使您的机器人更加实用、可靠,并且使用体验更加愉悦,尤其适合管理多个 WordPress 网站的团队。

评论留言

闪电侠

(工作日 10:00 - 18:30 为您服务)

2025-12-05 11:32:51

您好,无论是售前、售后、意见建议……均可通过联系工单与我们取得联系。

您也可选择聊天工具与我们即时沟通或点击查看:

您的工单我们已经收到,我们将会尽快跟您联系!
取消
选择聊天工具: