feat: Slack alerts for chat widget escalations + support@ email polling

- notifications/slack.ts: added sendChatEscalationAlert (fires when visitor
  asks about pricing, demo, help, or a human) and sendSupportEmailAlert
- email-poller.ts: polls support@squaremcp.com every 5 min via IMAP,
  deduplicates with Redis (support📧alerted_uids), fires Slack alert
  for each new unseen message
- index.ts: detectEscalation() scans last user message for trigger phrases;
  chat endpoint fires alert fire-and-forget after responding; startEmailPoller()
  called on server boot

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Garfield
2026-05-15 21:47:43 -04:00
parent d613119d55
commit adebe29ca0
4 changed files with 163 additions and 21 deletions

View File

@@ -37,6 +37,8 @@ import { deliverWebhook, isValidWebhookUrl } from './webhooks/delivery.js';
import { notifyNewPilotRequest } from './notifications/index.js';
import redis from './redis.js';
import { handleChat, type ChatMessage } from './chat.js';
import { startEmailPoller } from './email-poller.js';
import { sendChatEscalationAlert } from './notifications/slack.js';
process.on('uncaughtException', (err) => {
console.error('FATAL uncaughtException:', err);
@@ -1997,6 +1999,24 @@ app.post('/api/facebook/video', requireAuth, async (req, res) => {
});
// ── Chat widget endpoint ────────────────────────────────────────
const ESCALATION_PATTERNS = [
/\b(human|person|agent|someone|anybody|real person)\b/i,
/\bspeak\s+to\b|\btalk\s+to\b|\bcontact\s+(you|someone|support|sales)\b/i,
/\b(pricing|price|cost|how much|plans?|subscribe|sign\s*up|get\s+started|trial|demo)\b/i,
/\b(help me|need help|can you help|support)\b/i,
/\bemail\s+(you|garfield|support|sales)\b/i,
];
function detectEscalation(messages: ChatMessage[]): string | null {
const lastUser = [...messages].reverse().find(m => m.role === 'user');
if (!lastUser) return null;
for (const pattern of ESCALATION_PATTERNS) {
const match = lastUser.content.match(pattern);
if (match) return match[0];
}
return null;
}
app.post('/api/chat', async (req, res) => {
const { messages } = req.body as { messages?: ChatMessage[] };
if (!Array.isArray(messages) || messages.length === 0) {
@@ -2006,6 +2026,15 @@ app.post('/api/chat', async (req, res) => {
try {
const { reply, toolsUsed } = await handleChat(messages);
res.json({ reply, toolsUsed });
const trigger = detectEscalation(messages);
if (trigger) {
sendChatEscalationAlert({
trigger,
conversation: messages,
userAgent: (req as { get: (h: string) => string | undefined }).get('user-agent'),
}).catch(err => console.error('[chat] escalation alert error:', err));
}
} catch (err) {
console.error('[chat] error:', (err as Error).message);
res.status(500).json({ error: 'Chat unavailable' });
@@ -2153,6 +2182,8 @@ async function main() {
]);
}
startEmailPoller();
app.listen(PORT, () => {
console.log(`Hermes MCP server running on port ${PORT}`);
console.log(` Streamable HTTP: ${SERVER_URL}/mcp`);