Store associates share accounts and iPads, so the existing findMyTickets (keyed by reporter email) misses tickets a coworker opened for the same store earlier in the shift. This adds a store-keyed lookup so a store caller can see everything open at their location. - New service function searchOpenTicketsByStoreNumber(): normalizes to 5 digits, validates against the stores cache (warn-only on miss), runs the same Grok-enriched search as the reporter path. - New route GET /wxccai/open-tickets-by-store?storeNumber=782 — accepts padded or unpadded input, degrade-gracefully 200 on runtime failures to match the sibling reporter route. - JQL note: the CMDB "Store Number" field only matches on the object *label* (the 5-digit padded string). Neither the objectId, ASSET-<id> objectKey, workspace-qualified id, nor raw digits match. Probed all variants before landing. - WxCC AI Agent docs updated: findMyStoreTickets promoted to the primary lookup for store callers; findMyTickets reframed as the corporate-caller path. System prompt Step 1 rewritten to route store vs corporate callers to the right tool. Verified end-to-end against store 782 — returns 5 open tickets including two from other reporters (SS-20380 wireless phone, SS-11943 Zipline training) that email-based lookup misses. Co-authored-by: Cursor <cursoragent@cursor.com>
204 lines
11 KiB
Markdown
204 lines
11 KiB
Markdown
# wxcc-ai
|
|
|
|
Node/Express service that wires Webex Contact Center summaries, Jira tickets (core + JSM Store Support), and xAI Grok summarization into a single set of internal HTTP endpoints.
|
|
|
|
## Setup
|
|
|
|
```bash
|
|
npm install
|
|
cp .env.example .env
|
|
# fill in JIRA_*, XAI_*, and optionally the Assets values
|
|
npm run dev
|
|
```
|
|
|
|
The server listens on `PORT` (default `1866`).
|
|
|
|
## Environment variables
|
|
|
|
See [`.env.example`](./.env.example) for the full list. The important ones:
|
|
|
|
| Var | Purpose |
|
|
| --- | --- |
|
|
| `JIRA_CLOUD_ID` | If set, requests use `https://api.atlassian.com/ex/jira/{cloudId}`. |
|
|
| `JIRA_BASE_URL` | Fallback for legacy site URLs (`https://your-site.atlassian.net`). |
|
|
| `JIRA_EMAIL`, `JIRA_API_TOKEN` | Basic auth to Jira. `JIRA_AUTH_TYPE=bearer` switches to bearer. |
|
|
| `JIRA_SERVICE_DESK_ID` | Numeric JSM service desk id for Store Support. |
|
|
| `JIRA_ASSETS_WORKSPACE_ID` | Assets workspace id (auto-discovered if omitted). |
|
|
| `JIRA_ASSETS_STORE_SCHEMA_ID`, `_OBJECT_TYPE_ID` | Locate the Store schema/type. |
|
|
| `JIRA_ASSETS_STORE_NUMBER_ATTRIBUTE[_ID]` | Attribute name (or id) holding the store number in Assets. |
|
|
| `JIRA_STORE_CUSTOM_FIELD_ID` | Custom field on the JSM request that holds the Store Assets reference. |
|
|
| `XAI_API_KEY`, `XAI_BASE_URL` | Grok credentials for summary generation. |
|
|
|
|
## Endpoints
|
|
|
|
Base path: `/api/wxccai`.
|
|
|
|
### Read
|
|
|
|
- `GET /getticket?jiraKey=CS-1234` — Grok-summarized single ticket.
|
|
- `GET /open-tickets-by-reporter?email=user@example.com` — Grok-summarized list of open tickets a person reported. Best for corporate callers (unique per-user emails).
|
|
- `GET /open-tickets-by-store?storeNumber=00782` — Grok-summarized list of open SS tickets filed for a given store, regardless of reporter. Best for store callers (shared accounts). Store number can be unpadded; service pads to 5 digits.
|
|
- `GET /ticket/:key/status` — raw status fields (no Grok).
|
|
- `GET /ticket/:key/transitions` — available workflow transitions.
|
|
|
|
### Write
|
|
|
|
- `PATCH /ticket/:key` — body: `{ summary?, description?, priority?, labels?, assigneeAccountId?, additional? }`
|
|
- `POST /ticket/:key/comment` — body: `{ text, internal? }`
|
|
- `POST /ticket/:key/close` — body: `{ transitionName?, resolution?, comment?, internal?, component?, businessService?, system?, cause?, subType?, preserveExistingClassification?, skipValidatorFields? }`. Auto-picks the first "done" transition when `transitionName` is omitted. When the chosen transition is done-category, the four SS workflow-validator fields (`components`, `Business Service`, `System`, `Cause`) are populated first (see "Closing SS tickets" below). Non-done transitions skip the resolution field (fixes issue #9).
|
|
- `POST /ticket/:key/confirmFixed` — CC-agent convenience. Body: `{ subType?, comment?, component?, businessService?, system?, cause?, internal? }`. Closes with `resolution=Done`.
|
|
- `POST /ticket/:key/customerCancelled` — CC-agent convenience. Body: `{ subType?, reason?, component?, businessService?, system?, cause?, internal? }`. Closes with `resolution=Won't Do`.
|
|
- `POST /ticket/:key/duplicate` — CC-agent convenience. Body: `{ primaryKey (required), subType?, comment?, component?, businessService?, system?, cause?, internal? }`. Creates a formal `Duplicate` issue link to `primaryKey`, then closes with `resolution=Duplicate`.
|
|
|
|
### Store Support (JSM requests)
|
|
|
|
- `GET /ssRequestTypes` — supported `subType` values + Assets config summary.
|
|
- `POST /createSSRequest` — body includes `subType`, `summary`, `storeNumber` (auto-resolved via Assets), `onBehalfOf`, `description`, `additional`.
|
|
|
|
### Webex webhook
|
|
|
|
- `POST /issueTranscript/:jiraKey` — attaches audio + JSON transcript + human-readable transcript, then posts a restricted-visibility summary comment.
|
|
|
|
### Admin
|
|
|
|
- `GET /admin/caches` — snapshot of all four Assets object caches (stores, businessServices, systems, causes): `{ caches: [{ name, count, lastSyncAt, ageSeconds, syncing, assetsSyncConfigured, ... }] }`. Safe for health checks.
|
|
- `GET /admin/caches/:name/status` — same shape, one cache.
|
|
- `POST /admin/caches/:name/refresh` — force an immediate resync of one cache via the personal PAT (see below). Takes a few seconds.
|
|
- `POST /admin/caches/refreshAll` — refresh every cache in parallel.
|
|
- `GET /admin/storesCache/status` and `POST /admin/storesCache/refresh` — backward-compat aliases for the stores cache endpoints above.
|
|
|
|
### Debug (non-production only)
|
|
|
|
- `GET /debug/assetsProbe?storeNumber=305` — runs several AQL variants against Jira Assets and returns visible schemas + object-type detail + a computed diagnosis. Returns 404 when `NODE_ENV=production`.
|
|
|
|
## Assets object caches (Assets workaround)
|
|
|
|
Several parts of the SS lifecycle need to translate a human-readable name (a store number, a Business Service name, a System name, a Cause code) into a Jira Assets **object id** before the value can be written to a CMDB custom field. The shared service account is silently filtered out of the underlying object schema (68), so the app maintains four local caches that are populated from a **personal Atlassian PAT** (a real human account with the right Assets role):
|
|
|
|
| Cache | Object type | Used for |
|
|
| ------------------ | ----------- | ------------------------------------------------------------------- |
|
|
| `stores` | 109 | `customfield_10261 Store Number` on new SS tickets |
|
|
| `businessServices` | 100 | `customfield_10224 Business Service` workflow validator on close |
|
|
| `systems` | 103 | `customfield_10225 System` workflow validator on close |
|
|
| `causes` | 107 | `customfield_10233 Cause` workflow validator on close |
|
|
|
|
All four are backed by the same shared factory (`services/jira/assetsObjectCache.js`) and use the same PAT credentials. The service account is still used for everything else (creating tickets, comments, attachments, transitions).
|
|
|
|
Both storage patterns end up in the same place — `process.env.ASSETS_SYNC_TOKEN` — so the runtime code path is identical. Pick whichever fits the host.
|
|
|
|
### Setup A — production / Linux host (`.env`)
|
|
|
|
Put the values directly in `.env` (which is gitignored) and lock the file down:
|
|
|
|
```bash
|
|
cat >> .env <<'EOF'
|
|
ASSETS_SYNC_EMAIL=you@ae.com
|
|
ASSETS_SYNC_TOKEN=<paste-your-atlassian-api-token>
|
|
EOF
|
|
|
|
chmod 600 .env # only the bot user can read it
|
|
```
|
|
|
|
Then start normally:
|
|
|
|
```bash
|
|
npm start
|
|
```
|
|
|
|
Rotate the token in Atlassian → Account → Security → API tokens whenever the trust boundary on the host changes (new operator, offboarding, suspected leak). The app reloads it on the next process start.
|
|
|
|
### Setup B — local dev on macOS (Keychain)
|
|
|
|
If you're running the app on a Mac and would rather not keep the PAT in `.env`, use the wrapper script — it pulls the token from Keychain into `ASSETS_SYNC_TOKEN` before exec'ing the process:
|
|
|
|
```bash
|
|
security add-generic-password \
|
|
-s jira-assets-sync \
|
|
-a you@ae.com \
|
|
-w '<paste-your-atlassian-api-token>' \
|
|
-U
|
|
|
|
echo 'ASSETS_SYNC_EMAIL=you@ae.com' >> .env # email in .env, token stays in Keychain
|
|
|
|
./bin/load-assets-sync-secret.sh npm start
|
|
```
|
|
|
|
If `ASSETS_SYNC_TOKEN` is already in the process env (Setup A above), the wrapper is a no-op and skips the Keychain lookup.
|
|
|
|
### Runtime behavior
|
|
|
|
On boot the app loads each on-disk cache from `$CACHES_DIR/{name}.json` (default `./data/`), kicks off a background refresh for any snapshot missing or older than `CACHES_STALE_AFTER_HOURS`, and schedules a periodic full resync every `CACHES_REFRESH_HOURS`. `resolveStoreAssetReference` serves lookups from memory (sub-ms) with a live PAT lookup as fallback for brand-new stores. The three close-time caches (business services, systems, causes) are much smaller (dozens to a few hundred entries) and rarely change.
|
|
|
|
Force a refresh at any time:
|
|
|
|
```bash
|
|
curl -X POST http://localhost:1866/api/wxccai/admin/caches/stores/refresh
|
|
curl -X POST http://localhost:1866/api/wxccai/admin/caches/refreshAll
|
|
```
|
|
|
|
## Closing SS tickets
|
|
|
|
The `Resolved` transition on SS tickets fires a workflow validator that requires four fields to be populated:
|
|
|
|
- `components` — Jira native (63 options in the SS project)
|
|
- `customfield_10224` Business Service — Assets CMDB
|
|
- `customfield_10225` System — Assets CMDB
|
|
- `customfield_10233` Cause — Assets CMDB (the value "Unknown" exists as a designed catch-all)
|
|
|
|
`closeTicket` fills these in **before** calling the Resolved transition. Resolution order per field:
|
|
|
|
1. Explicit value in the request body (`component`, `businessService`, `system`, `cause`)
|
|
2. Whatever is already on the ticket (if `preserveExistingClassification` is `true`, the default — respects human triage)
|
|
3. The per-subType default from `src/config/ssCloseDefaults.js`
|
|
4. The `__default__` entry (Help Desk / Store Technology / I can't find my option - Misc / Unknown)
|
|
|
|
For CC-agent-driven closes, the shortest path is to send just `subType` and `comment`; everything else is defaulted. Use the convenience routes:
|
|
|
|
```bash
|
|
# Caller confirms the issue is resolved
|
|
curl -X POST http://localhost:1866/api/wxccai/ticket/SS-20948/confirmFixed \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"subType":"Report a Technology issue"}'
|
|
|
|
# Caller wants to cancel
|
|
curl -X POST http://localhost:1866/api/wxccai/ticket/SS-20949/customerCancelled \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"subType":"Broken device / hardware","reason":"changed their mind"}'
|
|
|
|
# Duplicate of an earlier ticket (creates a formal Duplicate issueLink)
|
|
curl -X POST http://localhost:1866/api/wxccai/ticket/SS-20950/duplicate \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"primaryKey":"SS-20948"}'
|
|
```
|
|
|
|
To override the auto-detected classification (for a subType not in the defaults map, or when the caller volunteers specific context), pass any of the four fields explicitly. CMDB names are matched case-insensitively; you can also pass a raw Assets objectId as a shortcut for `businessService` / `system` / `cause`:
|
|
|
|
```bash
|
|
curl -X POST http://localhost:1866/api/wxccai/ticket/SS-XXXXX/confirmFixed \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"subType":"Broken device / hardware","system":"Printer","cause":"Broken Equipment"}'
|
|
```
|
|
|
|
## Jira Assets gotcha
|
|
|
|
`createSSRequest` resolves a store number to an Assets object reference (`customfield_10261`). Two independent permission layers must both grant access, or every AQL query silently returns `total: 0`:
|
|
|
|
1. **OAuth scopes** on the API token: `read:cmdb-schema:jira`, `read:cmdb-type:jira`, `read:cmdb-object:jira`, `read:cmdb-attribute:jira` (and the `write:` equivalents for updates).
|
|
2. **Object Schema role membership** in Jira Assets itself — the underlying user needs to be added to a role on the Store schema in Jira → Assets → Object schemas → Configure → Roles.
|
|
|
|
If AQL keeps returning `total: 0` with HTTP 200, run the probe endpoint above; the `diagnosis` field will tell you exactly which layer is missing.
|
|
|
|
## Docker
|
|
|
|
```bash
|
|
npm run docker:build
|
|
npm run docker:run
|
|
```
|
|
|
|
The image runs as a non-root user and exposes `1866`. `HEALTHCHECK` pings `GET /health`.
|
|
|
|
## Repo hygiene
|
|
|
|
- Secrets live only in `.env`, which is `.gitignore`d.
|
|
- The two `discover-ss-*.js` helper scripts read from `.env` — never hardcode credentials in them.
|
|
- The debug logger no longer echoes the outbound `Authorization` header. Rotate any token that appears in older `logs/*.log` files.
|