
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}`
});
}
});
成功/失敗通知
目前,您的機器人可能只會返回類似 Success 或 Failed的純文字。雖然可以正常工作,但效果平淡,無法幫助使用者理解成功的原因或失敗時應該採取的措施。
讓我們透過設定正確格式的成功和錯誤訊息以及實用的上下文、建議和簡潔的格式來解決這個問題。
將這些實用程式新增到您的 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 命令開始,該命令接受任務型別(backup, clear_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 將結構化日誌傳送到檔案,並可選地傳送到 Loggly 或 Datadog 等服務。使用以下命令安裝:
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.log 或 console.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 網站的團隊。

評論留言