@fruggr/zendesk-mcp-server 2.9.0 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +282 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -909,6 +909,24 @@ const formatTicketField = (field) => {
|
|
|
909
909
|
...options.map((o) => ` - ${o.name} → ${o.value}`)
|
|
910
910
|
].filter(Boolean).join("\n");
|
|
911
911
|
};
|
|
912
|
+
const formatFieldValue = (value) => Array.isArray(value) ? value.map(formatFieldValue).join(", ") : formatConditionValue(value);
|
|
913
|
+
const MACRO_VALUE_PREVIEW = 120;
|
|
914
|
+
const formatMacroActionValue = (value) => {
|
|
915
|
+
const oneLine = formatFieldValue(value).replace(/\s+/g, " ").trim();
|
|
916
|
+
return oneLine.length > MACRO_VALUE_PREVIEW ? `${oneLine.slice(0, MACRO_VALUE_PREVIEW)}…` : oneLine;
|
|
917
|
+
};
|
|
918
|
+
const formatMacroAction = (action) => ` - ${action.field} → ${formatMacroActionValue(action.value)}`;
|
|
919
|
+
const formatMacro = (macro) => {
|
|
920
|
+
const scope = macro.restriction?.type ? "restricted" : "shared";
|
|
921
|
+
const actions = macro.actions ?? [];
|
|
922
|
+
return [
|
|
923
|
+
`## ${macro.title} (id ${macro.id})`,
|
|
924
|
+
`- **${macro.active ? "active" : "inactive"}** | **Scope**: ${scope}`,
|
|
925
|
+
macro.description ? `- **Description**: ${macro.description}` : "",
|
|
926
|
+
actions.length > 0 ? "- **Actions**:" : "- **Actions**: none",
|
|
927
|
+
...actions.map(formatMacroAction)
|
|
928
|
+
].filter(Boolean).join("\n");
|
|
929
|
+
};
|
|
912
930
|
const minutesUntil = (iso) => {
|
|
913
931
|
const t = Date.parse(iso);
|
|
914
932
|
return Number.isNaN(t) ? null : Math.round((t - Date.now()) / 6e4);
|
|
@@ -982,6 +1000,11 @@ const formatTranslation = (translation) => [
|
|
|
982
1000
|
].join("\n");
|
|
983
1001
|
const formatCategory = (category) => `- **${category.name}** (${category.id}) — ${category.description || "No description"}`;
|
|
984
1002
|
const formatSection = (section) => `- **${section.name}** (${section.id}) — Category: ${section.category_id} — ${section.description || "No description"}`;
|
|
1003
|
+
const formatView = (view, count) => {
|
|
1004
|
+
const countText = count ? ` — ${count.pretty} ticket(s)${count.fresh ? "" : " (count updating)"}` : "";
|
|
1005
|
+
const description = view.description ? ` — ${view.description}` : "";
|
|
1006
|
+
return `- **${view.title}** (id ${view.id})${countText}${description}`;
|
|
1007
|
+
};
|
|
985
1008
|
const formatPermissionGroup = (group) => `- **${group.name}** (${group.id})${group.built_in ? " — Built-in" : ""}`;
|
|
986
1009
|
const formatContentTag = (tag) => `- **${tag.name}** (${tag.id})`;
|
|
987
1010
|
const formatLabel = (label) => `- **${label.name}** (${label.id})`;
|
|
@@ -1010,6 +1033,11 @@ const extractPaginationMeta = (response, itemCount) => ({
|
|
|
1010
1033
|
after_cursor: response.meta?.after_cursor ?? null,
|
|
1011
1034
|
count: response.count ?? itemCount
|
|
1012
1035
|
});
|
|
1036
|
+
const extractOffsetPaginationMeta = (response, itemCount, perPage, page) => response.count != null ? extractSearchPaginationMeta(response, perPage, page) : {
|
|
1037
|
+
count: itemCount,
|
|
1038
|
+
has_more: false,
|
|
1039
|
+
after_cursor: null
|
|
1040
|
+
};
|
|
1013
1041
|
const extractSearchPaginationMeta = (response, perPage, page) => {
|
|
1014
1042
|
const count = response.count ?? 0;
|
|
1015
1043
|
const has_more = count > page * perPage;
|
|
@@ -2091,6 +2119,124 @@ const fetchTicketSla = async (subdomain, token, ticket) => {
|
|
|
2091
2119
|
return;
|
|
2092
2120
|
}
|
|
2093
2121
|
};
|
|
2122
|
+
const VIEW_COUNT_BATCH = 20;
|
|
2123
|
+
const chunk = (items, size) => {
|
|
2124
|
+
const groups = [];
|
|
2125
|
+
for (let i = 0; i < items.length; i += size) groups.push(items.slice(i, i + size));
|
|
2126
|
+
return groups;
|
|
2127
|
+
};
|
|
2128
|
+
const fetchViewCounts = async (subdomain, token, viewIds) => {
|
|
2129
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2130
|
+
for (const group of chunk(viewIds, VIEW_COUNT_BATCH)) try {
|
|
2131
|
+
const { view_counts } = await zendeskGet(subdomain, token, "/views/count_many", { ids: group.join(",") });
|
|
2132
|
+
for (const c of view_counts ?? []) counts.set(c.view_id, c);
|
|
2133
|
+
} catch {}
|
|
2134
|
+
return counts;
|
|
2135
|
+
};
|
|
2136
|
+
const resolveViewId = async (subdomain, token, view) => {
|
|
2137
|
+
if (typeof view === "number") return { id: view };
|
|
2138
|
+
const target = view.trim().toLowerCase();
|
|
2139
|
+
const available = [];
|
|
2140
|
+
let cursor;
|
|
2141
|
+
do {
|
|
2142
|
+
const response = await zendeskGet(subdomain, token, "/views", {
|
|
2143
|
+
active: "true",
|
|
2144
|
+
...buildCursorParams(100, cursor)
|
|
2145
|
+
});
|
|
2146
|
+
const views = response.views ?? [];
|
|
2147
|
+
const match = views.find((v) => v.title.trim().toLowerCase() === target);
|
|
2148
|
+
if (match) return { id: match.id };
|
|
2149
|
+
available.push(...views.map((v) => v.title));
|
|
2150
|
+
cursor = response.meta?.has_more ? response.meta.after_cursor ?? void 0 : void 0;
|
|
2151
|
+
} while (cursor);
|
|
2152
|
+
return { available };
|
|
2153
|
+
};
|
|
2154
|
+
const extractRowTicketId = (row) => {
|
|
2155
|
+
if (typeof row.ticket?.id === "number") return row.ticket.id;
|
|
2156
|
+
for (const key of ["id", "ticket_id"]) if (typeof row[key] === "number") return row[key];
|
|
2157
|
+
};
|
|
2158
|
+
const executeView = async (subdomain, token, viewId, opts) => {
|
|
2159
|
+
const params = buildCursorParams(opts.page_size, opts.cursor);
|
|
2160
|
+
if (opts.sort_by) params["sort_by"] = opts.sort_by;
|
|
2161
|
+
if (opts.sort_order) params["sort_order"] = opts.sort_order;
|
|
2162
|
+
const response = await zendeskGet(subdomain, token, `/views/${viewId}/execute`, params);
|
|
2163
|
+
const rows = response.rows ?? [];
|
|
2164
|
+
return {
|
|
2165
|
+
rows,
|
|
2166
|
+
meta: extractPaginationMeta(response, rows.length)
|
|
2167
|
+
};
|
|
2168
|
+
};
|
|
2169
|
+
const hydrateViewTickets = async (subdomain, token, ids) => {
|
|
2170
|
+
if (ids.length === 0) return [];
|
|
2171
|
+
const { tickets } = await zendeskGet(subdomain, token, "/tickets/show_many", { ids: ids.join(",") });
|
|
2172
|
+
const byId = new Map((tickets ?? []).map((t) => [t.id, t]));
|
|
2173
|
+
return ids.map((id) => byId.get(id)).filter((t) => t !== void 0);
|
|
2174
|
+
};
|
|
2175
|
+
const DIFF_SKIP_KEYS = /* @__PURE__ */ new Set([
|
|
2176
|
+
"comment",
|
|
2177
|
+
"fields",
|
|
2178
|
+
"custom_fields",
|
|
2179
|
+
"id",
|
|
2180
|
+
"url",
|
|
2181
|
+
"created_at",
|
|
2182
|
+
"updated_at",
|
|
2183
|
+
"generated_timestamp",
|
|
2184
|
+
"encoded_id"
|
|
2185
|
+
]);
|
|
2186
|
+
const valuesEqual = (a, b) => a === b || JSON.stringify(a) === JSON.stringify(b);
|
|
2187
|
+
const shownValue = (v) => {
|
|
2188
|
+
const s = formatFieldValue(v);
|
|
2189
|
+
return s === "" ? "(empty)" : s;
|
|
2190
|
+
};
|
|
2191
|
+
const diffLine = (label, before, after) => {
|
|
2192
|
+
const b = shownValue(before);
|
|
2193
|
+
const a = shownValue(after);
|
|
2194
|
+
return b === a ? null : `- **${label}**: ${b} → ${a}`;
|
|
2195
|
+
};
|
|
2196
|
+
const formatTagDiff = (before, after) => {
|
|
2197
|
+
const b = new Set(Array.isArray(before) ? before.map(String) : []);
|
|
2198
|
+
const a = new Set(Array.isArray(after) ? after.map(String) : []);
|
|
2199
|
+
const added = [...a].filter((t) => !b.has(t)).map((t) => `+${t}`);
|
|
2200
|
+
const removed = [...b].filter((t) => !a.has(t)).map((t) => `-${t}`);
|
|
2201
|
+
return added.length + removed.length === 0 ? null : `- **tags**: ${[...added, ...removed].join(", ")}`;
|
|
2202
|
+
};
|
|
2203
|
+
const formatMacroPreviewDiff = (ticketId, macroId, before, result) => {
|
|
2204
|
+
const after = result?.ticket ?? {};
|
|
2205
|
+
const beforeObj = before ?? {};
|
|
2206
|
+
const comment = after.comment ?? result?.comment;
|
|
2207
|
+
const changes = [];
|
|
2208
|
+
for (const [key, afterVal] of Object.entries(after)) {
|
|
2209
|
+
if (DIFF_SKIP_KEYS.has(key)) continue;
|
|
2210
|
+
const beforeVal = beforeObj[key];
|
|
2211
|
+
if (valuesEqual(beforeVal, afterVal)) continue;
|
|
2212
|
+
if (key === "tags") {
|
|
2213
|
+
const tagLine = formatTagDiff(beforeVal, afterVal);
|
|
2214
|
+
if (tagLine) changes.push(tagLine);
|
|
2215
|
+
continue;
|
|
2216
|
+
}
|
|
2217
|
+
if (afterVal !== null && typeof afterVal === "object" && !Array.isArray(afterVal)) continue;
|
|
2218
|
+
const line = diffLine(key, beforeVal, afterVal);
|
|
2219
|
+
if (line) changes.push(line);
|
|
2220
|
+
}
|
|
2221
|
+
const afterFields = [after.fields ?? after.custom_fields ?? []].flat();
|
|
2222
|
+
const beforeById = new Map((before?.custom_fields ?? []).map((f) => [f.id, f.value]));
|
|
2223
|
+
for (const f of afterFields) {
|
|
2224
|
+
const line = diffLine(`custom field ${f.id}`, beforeById.get(f.id), f.value);
|
|
2225
|
+
if (line) changes.push(line);
|
|
2226
|
+
}
|
|
2227
|
+
const lines = [
|
|
2228
|
+
`# Macro #${macroId} preview on ticket #${ticketId} (diff — nothing saved yet)`,
|
|
2229
|
+
"",
|
|
2230
|
+
"## Field changes",
|
|
2231
|
+
...changes.length > 0 ? changes : ["- none"]
|
|
2232
|
+
];
|
|
2233
|
+
if (comment?.body) {
|
|
2234
|
+
const visibility = comment.public === false ? "internal note" : "public comment";
|
|
2235
|
+
lines.push("", `## Reply (${visibility})`, "", comment.body);
|
|
2236
|
+
} else lines.push("", "## Reply", "- none");
|
|
2237
|
+
lines.push("", "## To apply these changes", "Nothing has been committed. Persist the field changes with `update_ticket` (or `manage_tags` for incremental tag edits), and post the reply with `add_public_comment` (public) or `add_private_note` (internal). Edit the reply text first if needed.");
|
|
2238
|
+
return lines.join("\n");
|
|
2239
|
+
};
|
|
2094
2240
|
const createTicketTools = (ctx) => {
|
|
2095
2241
|
const { subdomain, getToken } = ctx;
|
|
2096
2242
|
const attachmentSchema = z.object({
|
|
@@ -2491,11 +2637,7 @@ const createTicketTools = (ctx) => {
|
|
|
2491
2637
|
const policies = response.sla_policies ?? [];
|
|
2492
2638
|
return { content: [{
|
|
2493
2639
|
type: "text",
|
|
2494
|
-
text: formatList(policies, formatSlaPolicy,
|
|
2495
|
-
count: policies.length,
|
|
2496
|
-
has_more: false,
|
|
2497
|
-
after_cursor: null
|
|
2498
|
-
})
|
|
2640
|
+
text: formatList(policies, formatSlaPolicy, extractOffsetPaginationMeta(response, policies.length, per_page, page))
|
|
2499
2641
|
}] };
|
|
2500
2642
|
}
|
|
2501
2643
|
},
|
|
@@ -2525,6 +2667,141 @@ const createTicketTools = (ctx) => {
|
|
|
2525
2667
|
text: formatList(fields, formatTicketField, extractPaginationMeta(response, fields.length))
|
|
2526
2668
|
}] };
|
|
2527
2669
|
}
|
|
2670
|
+
},
|
|
2671
|
+
{
|
|
2672
|
+
name: "list_views",
|
|
2673
|
+
namespace: "tickets",
|
|
2674
|
+
readOnly: true,
|
|
2675
|
+
title: "List Zendesk Views",
|
|
2676
|
+
description: "List the agent's active Zendesk views — the saved ticket queues (\"Unassigned tickets\", \"My open tickets\", \"Breaching today\") the agent sees in the Zendesk UI — each with its current ticket count so you can tell at a glance where the workload sits. Views are per-agent scoped, so per-user auth returns exactly the queues this agent can see, with no shared key. Counts come from Zendesk's cache and can lag by up to about an hour (shown as \"(count updating)\" while a fresh value is still being computed); pass a view's title or id to get_view_tickets to read the tickets inside it.",
|
|
2677
|
+
inputSchema: z.object({
|
|
2678
|
+
page_size: z.number().int().min(1).max(100).default(100).describe("Views per page (1-100, default 100)."),
|
|
2679
|
+
cursor: z.string().optional().describe("Pagination cursor from a previous response; omit for the first page.")
|
|
2680
|
+
}),
|
|
2681
|
+
annotations: {
|
|
2682
|
+
readOnlyHint: true,
|
|
2683
|
+
destructiveHint: false,
|
|
2684
|
+
idempotentHint: true,
|
|
2685
|
+
openWorldHint: true
|
|
2686
|
+
},
|
|
2687
|
+
handler: async (params) => {
|
|
2688
|
+
const { page_size, cursor } = params;
|
|
2689
|
+
const token = await getToken();
|
|
2690
|
+
const response = await zendeskGet(subdomain, token, "/views", {
|
|
2691
|
+
active: "true",
|
|
2692
|
+
...buildCursorParams(page_size, cursor)
|
|
2693
|
+
});
|
|
2694
|
+
const views = response.views ?? [];
|
|
2695
|
+
const counts = await fetchViewCounts(subdomain, token, views.map((v) => v.id));
|
|
2696
|
+
return { content: [{
|
|
2697
|
+
type: "text",
|
|
2698
|
+
text: formatList(views, (view) => formatView(view, counts.get(view.id)), extractPaginationMeta(response, views.length))
|
|
2699
|
+
}] };
|
|
2700
|
+
}
|
|
2701
|
+
},
|
|
2702
|
+
{
|
|
2703
|
+
name: "get_view_tickets",
|
|
2704
|
+
namespace: "tickets",
|
|
2705
|
+
readOnly: true,
|
|
2706
|
+
title: "Get Tickets In A View",
|
|
2707
|
+
description: "Read the tickets inside a Zendesk view, in the view's own configured sort order — the same order the agent sees in the Zendesk UI — which is the natural way to work a named queue like \"Unassigned tickets\" or \"Breaching today\". Accepts the view by title or by numeric id (discover both with list_views); a title is matched case-insensitively against the agent's active views, and on no match the available titles are returned so you can retry in one step. Tickets come back with the same fields as list_tickets and are cursor-paginated; there is no live SLA block here (use search_tickets when you need per-ticket SLA state), and sort_by/sort_order override the view's order when you want a different cut.",
|
|
2708
|
+
inputSchema: z.object({
|
|
2709
|
+
view: z.union([z.string().min(1), z.number().int().positive()]).describe("The view to read: its exact title as shown in Zendesk (e.g. \"Unassigned tickets\") or its numeric id from list_views. A title is matched case-insensitively against your active views; on no match the tool returns the available titles instead of erroring, so you can retry with a correct one."),
|
|
2710
|
+
sort_by: z.string().optional().describe("Optional column to sort by, overriding the view's own sort. Must be one of the view's columns (e.g. \"status\", \"priority\", \"updated_at\", or a custom field id); \"subject\" and \"submitter\" are not sortable. Omit to keep the view's configured order."),
|
|
2711
|
+
sort_order: z.enum(["asc", "desc"]).optional().describe("Sort direction applied to sort_by: \"asc\" (oldest/lowest first) or \"desc\" (newest/highest first). Only meaningful together with sort_by; omit to keep the view's configured direction."),
|
|
2712
|
+
page_size: z.number().int().min(1).max(100).default(100).describe("Tickets per page (1-100, default 100)."),
|
|
2713
|
+
cursor: z.string().optional().describe("Pagination cursor from a previous response; omit for the first page.")
|
|
2714
|
+
}),
|
|
2715
|
+
annotations: {
|
|
2716
|
+
readOnlyHint: true,
|
|
2717
|
+
destructiveHint: false,
|
|
2718
|
+
idempotentHint: true,
|
|
2719
|
+
openWorldHint: true
|
|
2720
|
+
},
|
|
2721
|
+
handler: async (params) => {
|
|
2722
|
+
const { view, sort_by, sort_order, page_size, cursor } = params;
|
|
2723
|
+
const token = await getToken();
|
|
2724
|
+
const resolved = await resolveViewId(subdomain, token, view);
|
|
2725
|
+
if ("available" in resolved) return { content: [{
|
|
2726
|
+
type: "text",
|
|
2727
|
+
text: resolved.available.length > 0 ? `No active view matches "${view}". Available views: ${resolved.available.join(", ")}.` : `No active view matches "${view}", and no active views were found for this agent.`
|
|
2728
|
+
}] };
|
|
2729
|
+
let rows;
|
|
2730
|
+
let meta;
|
|
2731
|
+
try {
|
|
2732
|
+
({rows, meta} = await executeView(subdomain, token, resolved.id, {
|
|
2733
|
+
sort_by,
|
|
2734
|
+
sort_order,
|
|
2735
|
+
page_size,
|
|
2736
|
+
cursor
|
|
2737
|
+
}));
|
|
2738
|
+
} catch (error) {
|
|
2739
|
+
if (error instanceof ZendeskApiError && error.status === 403) throw new Error(`Access denied to view ${resolved.id} (HTTP 403). Zendesk views can be restricted to specific groups, and this agent is not allowed to read this one. Call list_views to see the queues available to this agent.`, { cause: error });
|
|
2740
|
+
throw error;
|
|
2741
|
+
}
|
|
2742
|
+
const ids = rows.map(extractRowTicketId).filter((id) => typeof id === "number");
|
|
2743
|
+
const tickets = await hydrateViewTickets(subdomain, token, ids);
|
|
2744
|
+
return { content: [{
|
|
2745
|
+
type: "text",
|
|
2746
|
+
text: formatList(tickets, formatTicket, {
|
|
2747
|
+
...meta,
|
|
2748
|
+
count: tickets.length
|
|
2749
|
+
})
|
|
2750
|
+
}] };
|
|
2751
|
+
}
|
|
2752
|
+
},
|
|
2753
|
+
{
|
|
2754
|
+
name: "list_macros",
|
|
2755
|
+
namespace: "tickets",
|
|
2756
|
+
readOnly: true,
|
|
2757
|
+
title: "List Zendesk Macros",
|
|
2758
|
+
description: "List the active macros available to the authenticated user. A macro bundles a canned reply and/or a set of field changes (status, priority, assignee, group, tags, custom fields) an agent applies to a ticket in one gesture; this returns each macro id, title, description, availability scope, and its ordered list of actions, offset-paginated. Results are scoped by per-user OAuth to what the current user can see, so no shared admin key is needed. Pass a macro id from here to preview_macro_diff to preview its effect on a specific ticket.",
|
|
2759
|
+
inputSchema: z.object({
|
|
2760
|
+
per_page: z.number().int().min(1).max(100).default(100).describe(PER_PAGE_DESC),
|
|
2761
|
+
page: z.number().int().min(1).default(1).describe(PAGE_DESC)
|
|
2762
|
+
}),
|
|
2763
|
+
annotations: {
|
|
2764
|
+
readOnlyHint: true,
|
|
2765
|
+
destructiveHint: false,
|
|
2766
|
+
idempotentHint: true,
|
|
2767
|
+
openWorldHint: true
|
|
2768
|
+
},
|
|
2769
|
+
handler: async (params) => {
|
|
2770
|
+
const { per_page, page } = params;
|
|
2771
|
+
const token = await getToken();
|
|
2772
|
+
const response = await zendeskGet(subdomain, token, "/macros/active", buildOffsetParams(per_page, page));
|
|
2773
|
+
const macros = response.macros ?? [];
|
|
2774
|
+
return { content: [{
|
|
2775
|
+
type: "text",
|
|
2776
|
+
text: formatList(macros, formatMacro, extractOffsetPaginationMeta(response, macros.length, per_page, page))
|
|
2777
|
+
}] };
|
|
2778
|
+
}
|
|
2779
|
+
},
|
|
2780
|
+
{
|
|
2781
|
+
name: "preview_macro_diff",
|
|
2782
|
+
namespace: "tickets",
|
|
2783
|
+
readOnly: false,
|
|
2784
|
+
title: "Preview a Macro Diff on a Ticket",
|
|
2785
|
+
description: "Preview the exact changes a macro would make to a specific ticket, as a before → after diff, WITHOUT saving anything. Orchestrates two reads — the ticket's current state and Zendesk's macro-apply preview (which returns the whole resulting ticket) — and returns only the fields the macro actually changes (status, priority, assignee, group, tags, custom fields) plus the canned reply with its public/internal flag; unchanged and identity fields are omitted. Nothing is committed: to apply it, follow up with update_ticket for the field changes and add_public_comment or add_private_note for the reply. This deliberate two-step keeps the mutation explicit and reviewable rather than hidden. Find macro ids via list_macros and the ticket id via search_tickets or list_tickets.",
|
|
2786
|
+
inputSchema: z.object({
|
|
2787
|
+
ticket_id: z.number().int().describe("Ticket ID — the numeric id of the ticket to preview the macro against. Obtain it from search_tickets or list_tickets."),
|
|
2788
|
+
macro_id: z.number().int().describe("Macro ID — the numeric id of the macro to preview. Obtain it from list_macros.")
|
|
2789
|
+
}),
|
|
2790
|
+
annotations: {
|
|
2791
|
+
readOnlyHint: false,
|
|
2792
|
+
destructiveHint: false,
|
|
2793
|
+
idempotentHint: true,
|
|
2794
|
+
openWorldHint: true
|
|
2795
|
+
},
|
|
2796
|
+
handler: async (params) => {
|
|
2797
|
+
const { ticket_id, macro_id } = params;
|
|
2798
|
+
const token = await getToken();
|
|
2799
|
+
const [{ ticket: before }, { result }] = await Promise.all([zendeskGet(subdomain, token, `/tickets/${ticket_id}`), zendeskGet(subdomain, token, `/tickets/${ticket_id}/macros/${macro_id}/apply`)]);
|
|
2800
|
+
return { content: [{
|
|
2801
|
+
type: "text",
|
|
2802
|
+
text: truncateIfNeeded(formatMacroPreviewDiff(ticket_id, macro_id, before, result))
|
|
2803
|
+
}] };
|
|
2804
|
+
}
|
|
2528
2805
|
}
|
|
2529
2806
|
];
|
|
2530
2807
|
};
|