Prevents the hourly poller from re-classifying the same "not for us" tickets every hour and burning AI tokens on them forever. New SKIP_LABEL='bot-skipped' is applied whenever the AI classifier decides a ticket is out-of-scope (kind='skip', no store number, or an unroutable kind). The JQL now excludes both bot labels while preserving the `IS EMPTY OR` union so brand-new unlabeled tickets still match. Transient failures (AI down, collector 5xx, comment 5xx) intentionally stay unlabeled so they retry next poll. Labeling is wrapped in a non-throwing helper — a Jira 5xx on the label call can't abort the batch; the ticket just gets one duplicate classification next hour, which is far cheaper than dropping the poll.
272 lines
11 KiB
JavaScript
272 lines
11 KiB
JavaScript
// src/commands/help.js
|
|
//
|
|
// Help renderer.
|
|
//
|
|
// /help → short, scannable list of every command (one line each)
|
|
// with grouped headings.
|
|
// /help <command> → detailed usage + examples for one command.
|
|
//
|
|
// The previous version was a single 5KB blob of markdown with hard-to-read
|
|
// inline URLs. This split keeps the top-level message short for both 1:1 and
|
|
// group spaces, and pushes verbose detail behind `/help <command>`.
|
|
|
|
const SHORT_HELP = {
|
|
// Work orders
|
|
wohistory: 'Recent work order history for a store',
|
|
wosummary: 'AI summary of a specific work order',
|
|
woattachments: 'Download all attachments for a work order',
|
|
|
|
// AV / phones
|
|
avstatus: 'AV / device status for a store (alias: /wostatus)',
|
|
phonestatus: 'DECT + IP phone status for a store',
|
|
|
|
// Jira
|
|
jirahistory: 'Recent Jira tickets for a store (optionally filtered by component)',
|
|
jiraticket: 'Detailed AI summary of one Jira ticket',
|
|
jirapoll: 'Run the hourly Jira poller on demand (enrich unassigned AV/phone tickets)',
|
|
|
|
// Provisioning / mutating
|
|
'provision-dect': 'Interactive DECT provisioning card (add/remove bases & handsets)',
|
|
'provision-vc': 'Fully provision a video conferencing device',
|
|
vcmonitor: 'On-demand VC packet capture (start/stop/status)',
|
|
offboarduser: 'Offboard user: revoke Webex OAuth tokens + wipe MDM CORP devices',
|
|
webexhost: 'Check / assign Webex Meetings host license on aeo2go.webex.com',
|
|
|
|
// Bulk / utility
|
|
bulkavstatuscsv: 'Generate full AV devices report (CSV)',
|
|
bulkavswitchcsv: 'Generate rear-switch port report (CSV)',
|
|
devicesbymodel: 'Meraki devices grouped by model (CSV)',
|
|
};
|
|
|
|
const LONG_HELP = {
|
|
avstatus: {
|
|
title: '/avstatus',
|
|
usage: ['/avstatus <store>', '/avstatus <store> detailed'],
|
|
examples: ['/avstatus 782', '/avstatus 782 detailed'],
|
|
notes: [
|
|
'Aliases: `/wostatus`.',
|
|
'HTTP equivalent: `?storeNum=<n>&detailed=true`.',
|
|
'Includes Meraki deep links per device. For interactive topology, open `/av-store-dashboard.html`.',
|
|
],
|
|
},
|
|
phonestatus: {
|
|
title: '/phonestatus',
|
|
usage: ['/phonestatus <store>', '/phonestatus <store> detailed'],
|
|
examples: ['/phonestatus 782', '/phonestatus 782 detailed'],
|
|
notes: [
|
|
'Shows DECT basestations + IP phones with Meraki links.',
|
|
'Detailed mode adds firmware, serial, SIP details and errors.',
|
|
'Web dashboard: `/phone-store-dashboard.html`.',
|
|
],
|
|
},
|
|
'provision-dect': {
|
|
title: '/provision-dect',
|
|
usage: ['/provision-dect <store>'],
|
|
examples: ['/provision-dect 782'],
|
|
notes: [
|
|
'Aliases: `/provisiondect`.',
|
|
'Posts an interactive card to add/remove bases and handsets.',
|
|
'Looks up "Store XXXX" network using 5-digit person email; access codes are auto-generated.',
|
|
],
|
|
},
|
|
'provision-vc': {
|
|
title: '/provision-vc',
|
|
usage: ['/provision-vc <serial>', '/provision-vc <serial> <org>'],
|
|
examples: ['/provision-vc FOC2419NTN2', '/provision-vc FOC2419NTN2 todd'],
|
|
notes: [
|
|
'Aliases: `/vcprovision` (legacy).',
|
|
'`<org>` may be partial name (e.g. "todd", "american") or an org ID.',
|
|
],
|
|
},
|
|
vcmonitor: {
|
|
title: '/vcmonitor',
|
|
usage: ['/vcmonitor <serial> [start|stop|status] [Full|Limited|FullRotate]'],
|
|
examples: [
|
|
'/vcmonitor FOC2419NTN2',
|
|
'/vcmonitor FOC2419NTN2 Limited',
|
|
'/vcmonitor FOC2419NTN2 stop',
|
|
'/vcmonitor FOC2419NTN2 status',
|
|
],
|
|
notes: [
|
|
'Default action: `start Full` (~3 min capture including RTP).',
|
|
'PCAPs land in the System Log bundle downloadable from Control Hub diagnostics.',
|
|
],
|
|
},
|
|
wohistory: {
|
|
title: '/wohistory',
|
|
usage: ['/wohistory <store>'],
|
|
examples: ['/wohistory 782'],
|
|
notes: ['In a store-linked space, the store can be omitted.'],
|
|
},
|
|
wosummary: {
|
|
title: '/wosummary',
|
|
usage: ['/wosummary <work-order-id>'],
|
|
examples: ['/wosummary 12345678'],
|
|
},
|
|
woattachments: {
|
|
title: '/woattachments',
|
|
usage: ['/woattachments <work-order-id>'],
|
|
examples: ['/woattachments 12345678'],
|
|
},
|
|
jirahistory: {
|
|
title: '/jirahistory',
|
|
usage: [
|
|
'/jirahistory <store>',
|
|
'/jirahistory <store> <component>',
|
|
'/jirahistory <store> all',
|
|
],
|
|
examples: [
|
|
'/jirahistory 782',
|
|
'/jirahistory 782 phone',
|
|
'/jirahistory 782 av,voice,mobility',
|
|
],
|
|
notes: [
|
|
'Component shortcuts: `av` → Audio Visual, `phone`/`voice` → Communication Services, `mobility` → Mobility.',
|
|
],
|
|
},
|
|
jiraticket: {
|
|
title: '/jiraticket',
|
|
usage: ['/jiraticket <KEY>'],
|
|
examples: ['/jiraticket SUPPORT-817694'],
|
|
},
|
|
jirapoll: {
|
|
title: '/jirapoll',
|
|
usage: ['/jirapoll', '/jirapoll prime'],
|
|
examples: ['/jirapoll', '/jirapoll prime'],
|
|
notes: [
|
|
'Triggers the same Jira poller that normally runs at the top of every hour. Enriches any unlabeled matching tickets with a phone/av snapshot comment and labels them `bot-enriched`. Tickets the AI classifier decides are out-of-scope get labeled `bot-skipped` so they aren\'t re-classified every hour.',
|
|
'Idempotent — labels + JQL prevent double-processing, so running multiple times in a row is safe.',
|
|
'`/jirapoll prime` labels every matching ticket without enriching or notifying. Use once after adopting the poller to skip enriching the existing backlog. Same as `JIRA_POLLER_PRIME_ON_START=true` at startup.',
|
|
'A summary of enriched tickets goes to the configured `JIRA_POLLER_ROOM_ID`. The invoking chat also gets a compact result line.',
|
|
],
|
|
},
|
|
webexhost: {
|
|
title: '/webexhost',
|
|
usage: [
|
|
'/webexhost <email>',
|
|
'/webexhost list',
|
|
'/webexhost debug <email>',
|
|
],
|
|
examples: [
|
|
'/webexhost jdoe@company.com',
|
|
'/webexhost list',
|
|
'/webexhost debug jdoe@company.com',
|
|
],
|
|
notes: [
|
|
'`/webexhost <email>` checks whether the user holds any Webex Meetings host license on the configured site (default `aeo2go.webex.com`, override via `WEBEX_HOST_SITE_URL`).',
|
|
'If they already have one, the command reports which license. If not — and `WEBEX_HOST_LICENSE_ID` is set in `.env` — it posts a confirmation card to assign that license.',
|
|
'`/webexhost list` is a discovery helper: lists every meeting license on the site with id + remaining seats, marking the one currently configured for auto-assign.',
|
|
'`/webexhost debug <email>` dumps the raw Webex payload for the user (search-endpoint vs `GET /people/{id}` license counts, intersection with site licenses) — use this when Control Hub disagrees with what the bot says.',
|
|
'Requires service-app scopes: `spark-admin:licenses_read` + `spark-admin:people_write`. Scope/role failures surface inline.',
|
|
'There is no Webex API for the "host vs attendee" account flag itself — host status is determined entirely by holding a meeting license on the site (confirmed via Cisco docs + wxc_sdk source).',
|
|
'Audit log: requests + outcomes are emitted under the `webexhost:audit` scope with the requester identity (chat email or `via HTTP API`).',
|
|
],
|
|
},
|
|
offboarduser: {
|
|
title: '/offboarduser',
|
|
usage: ['/offboarduser <email>'],
|
|
examples: ['/offboarduser jdoe@company.com'],
|
|
notes: [
|
|
'Posts a confirmation card showing the Webex user + MDM CORP devices that will be acted on.',
|
|
'Confirming revokes every Webex OAuth authorization for the user (signs them out of all Webex clients) and enterprise-wipes each listed MDM CORP device — both run in parallel.',
|
|
'Requires the Webex service app to hold `identity:tokens_read` + `identity:tokens_write` scopes and an admin with Full / User / Device Admin role. Scope/role failures surface inline in the success message.',
|
|
'`Hide from search` is **not** part of this command — Cisco only exposes that toggle in Control Hub, not via any Webex API. Set it manually if your offboarding policy requires it.',
|
|
'Every request and outcome is logged under the `offboard:audit` scope with the requesting user (chat email or `via HTTP API`).',
|
|
],
|
|
},
|
|
bulkavstatuscsv: {
|
|
title: '/bulkavstatuscsv',
|
|
usage: ['/bulkavstatuscsv'],
|
|
notes: ['Long-running. Posts CSV attachment back to the calling room.'],
|
|
},
|
|
bulkavswitchcsv: {
|
|
title: '/bulkavswitchcsv',
|
|
usage: ['/bulkavswitchcsv'],
|
|
notes: [
|
|
'Long-running. Walks every rear switch in Meraki for VLAN 340/145 ports.',
|
|
'Posts CSV attachment back to the calling room.',
|
|
],
|
|
},
|
|
devicesbymodel: {
|
|
title: '/devicesbymodel',
|
|
usage: ['/devicesbymodel'],
|
|
notes: ['Posts a Meraki by-model count + CSV to the calling room.'],
|
|
},
|
|
};
|
|
|
|
const GROUPS = [
|
|
{ title: 'Work orders', keys: ['wohistory', 'wosummary', 'woattachments'] },
|
|
{ title: 'AV & phones', keys: ['avstatus', 'phonestatus'] },
|
|
{ title: 'Jira', keys: ['jirahistory', 'jiraticket', 'jirapoll'] },
|
|
{ title: 'Provisioning', keys: ['provision-dect', 'provision-vc', 'vcmonitor'] },
|
|
{ title: 'Admin & bulk', keys: ['offboarduser', 'webexhost', 'bulkavstatuscsv', 'bulkavswitchcsv', 'devicesbymodel'] },
|
|
];
|
|
|
|
function renderTopLevelHelp(isGroup) {
|
|
const lines = ['### CollabSupport Bot Help', ''];
|
|
for (const g of GROUPS) {
|
|
lines.push(`**${g.title}**`);
|
|
for (const key of g.keys) {
|
|
const short = SHORT_HELP[key];
|
|
if (!short) continue;
|
|
lines.push(`- \`/${key}\` — ${short}`);
|
|
}
|
|
lines.push('');
|
|
}
|
|
lines.push('Type `/help <command>` for usage and examples (e.g. `/help avstatus`).');
|
|
if (isGroup) {
|
|
lines.push('In a space linked to a store, most commands accept no arguments and pick up the store automatically.');
|
|
}
|
|
lines.push('Web dashboards: `/av-store-dashboard.html`, `/phone-store-dashboard.html`.');
|
|
return lines.join('\n');
|
|
}
|
|
|
|
function renderCommandHelp(name) {
|
|
const detail = LONG_HELP[name];
|
|
if (!detail) return null;
|
|
const lines = [`### ${detail.title}`];
|
|
if (SHORT_HELP[name]) {
|
|
lines.push('', SHORT_HELP[name]);
|
|
}
|
|
if (detail.usage?.length) {
|
|
lines.push('', '**Usage:**');
|
|
for (const u of detail.usage) lines.push(`- \`${u}\``);
|
|
}
|
|
if (detail.examples?.length) {
|
|
lines.push('', '**Examples:**');
|
|
for (const ex of detail.examples) lines.push(`- \`${ex}\``);
|
|
}
|
|
if (detail.notes?.length) {
|
|
lines.push('', '**Notes:**');
|
|
for (const n of detail.notes) lines.push(`- ${n}`);
|
|
}
|
|
return lines.join('\n');
|
|
}
|
|
|
|
export async function handleHelp(bot, trigger) {
|
|
const isGroup = trigger.message?.roomType === 'group';
|
|
|
|
// First arg (after `/help`) selects a specific command's detail page.
|
|
// Strip a leading slash so both `/help avstatus` and `/help /avstatus` work.
|
|
const args = trigger.args || [];
|
|
const requested = (args[0] || trigger.query?.command || '')
|
|
.toString()
|
|
.trim()
|
|
.toLowerCase()
|
|
.replace(/^\//, '');
|
|
|
|
if (requested) {
|
|
const detail = renderCommandHelp(requested);
|
|
if (detail) {
|
|
await bot.say('markdown', detail);
|
|
return;
|
|
}
|
|
await bot.say(
|
|
'markdown',
|
|
`Unknown command: \`${requested}\`.\n\n${renderTopLevelHelp(isGroup)}`,
|
|
);
|
|
return;
|
|
}
|
|
|
|
await bot.say('markdown', renderTopLevelHelp(isGroup));
|
|
}
|