This commit is contained in:
Your Name committed 2026-07-25 00:54:35 -04:00
1 parent 94cbb52348
commit 167d07f0c2
7 files changed
+1284 -21

No files matched your search

+228
View File
@@ -25,6 +25,23 @@ const localizedTextSchema = {
additionalProperties: false,
} as const;
const footerMenuNameSchema = {
type: "string",
enum: ["footer-explore", "footer-support"],
description: "Footer menu managed by apps/admin Menu Manager.",
} as const;
const footerWriteControlSchema = {
dryRun: {
type: "boolean",
description: "Validate and preview the operation without saving. Defaults to false.",
},
expectedVersion: {
type: "string",
description: "Version returned by get_footer_menu. The backend rejects stale writes.",
},
} as const;
const menuLinkNodeSchema = {
type: "object",
description: "Mega Menu Link node. Valid under a Column or another Link. Renders as a clickable link; child links are shown inline up to the desktop limit.",
@@ -88,6 +105,119 @@ const rootMenuNodeSchema = {
} as const;
export const MENU_TOOL_DEFINITIONS = [
{
name: "get_footer_menu",
description: "Read footer-explore or footer-support in a compact, language-specific format. Use lang=all only when every translation is required.",
inputSchema: {
type: "object",
properties: {
menuName: footerMenuNameSchema,
lang: {
type: "string",
enum: [...SUPPORTED_MENU_LOCALES, "all"],
description: "Language to resolve. Defaults to th; all returns all translations.",
},
fallback: {
type: "boolean",
description: "Allow the backend to fall back to the base label when the requested translation is missing. Defaults to true.",
},
},
required: ["menuName"],
additionalProperties: false,
},
},
{
name: "add_footer_menu_item",
description: "Add one item to footer-explore or footer-support without replacing the menu. Use dryRun first and pass the version from get_footer_menu.",
inputSchema: {
type: "object",
properties: {
menuName: footerMenuNameSchema,
label: { type: "string", description: "Base label for the item." },
url: { type: "string", description: "Internal path or supported external URL." },
order: { type: "number", description: "Display order." },
translations: {
type: "object",
properties: Object.fromEntries(SUPPORTED_MENU_LOCALES.map((locale) => [locale, { type: "string" }])),
additionalProperties: false,
description: "Optional translated labels keyed by locale.",
},
...footerWriteControlSchema,
},
required: ["menuName", "label", "url"],
additionalProperties: false,
},
},
{
name: "update_footer_menu_item",
description: "Partially update one footer menu item while preserving omitted fields and translations.",
inputSchema: {
type: "object",
properties: {
menuName: footerMenuNameSchema,
itemId: { type: "number", description: "Existing footer menu item ID." },
label: { type: "string" },
url: { type: "string" },
order: { type: "number" },
...footerWriteControlSchema,
},
required: ["menuName", "itemId"],
additionalProperties: false,
},
},
{
name: "delete_footer_menu_item",
description: "Delete one item from footer-explore or footer-support. Use dryRun first and pass expectedVersion.",
inputSchema: {
type: "object",
properties: {
menuName: footerMenuNameSchema,
itemId: { type: "number", description: "Existing footer menu item ID." },
...footerWriteControlSchema,
},
required: ["menuName", "itemId"],
additionalProperties: false,
},
},
{
name: "reorder_footer_menu_items",
description: "Reorder a footer menu using the complete ordered list of its item IDs.",
inputSchema: {
type: "object",
properties: {
menuName: footerMenuNameSchema,
itemIds: {
type: "array",
items: { type: "number" },
minItems: 1,
description: "Every current item ID, exactly once, in the desired order.",
},
...footerWriteControlSchema,
},
required: ["menuName", "itemIds"],
additionalProperties: false,
},
},
{
name: "update_footer_menu_item_translation",
description: "Update one translated label on a footer menu item without changing its base label, URL, order, or other languages.",
inputSchema: {
type: "object",
properties: {
menuName: footerMenuNameSchema,
itemId: { type: "number", description: "Existing footer menu item ID." },
lang: {
type: "string",
enum: SUPPORTED_MENU_LOCALES,
description: "Translation locale.",
},
label: { type: "string", description: "Translated label." },
...footerWriteControlSchema,
},
required: ["menuName", "itemId", "lang", "label"],
additionalProperties: false,
},
},
{
name: "get_menu",
description: "Fetch a specific menu and its items (including translations)",
@@ -281,10 +411,108 @@ export const MENU_TOOL_DEFINITIONS = [
export const MENU_TOOL_NAMES = new Set<string>(MENU_TOOL_DEFINITIONS.map((tool) => tool.name));
export function getLegacyFooterMenuDeprecation(params: any) {
const rejectedFields = ["exploreLinks", "collectionLinks", "supportLinks"]
.filter((field) => params?.[field] !== undefined);
if (rejectedFields.length === 0) return null;
return {
success: false,
code: "FOOTER_MENU_REPLACEMENT_DEPRECATED",
partialWrite: false,
rejectedFields,
message: "Footer menu arrays cannot be replaced through update_footer_content. Use get_footer_menu and the Footer-specific item tools. Website Collections are managed through Categories.",
};
}
function compactFooterResponse(data: any, operation: string, params: any) {
const source = data?.data ?? data ?? {};
const compact: Record<string, unknown> = {
success: source.success ?? true,
operation: source.operation ?? operation,
menuName: source.menuName ?? source.name ?? params.menuName,
};
for (const key of [
"dryRun",
"version",
"lang",
"item",
"items",
"itemId",
"itemIds",
"itemCount",
"label",
"before",
"after",
"deletedItemId",
"translatedItemId",
"translatedPaths",
"changes",
"warnings",
"code",
"error",
"partialWrite",
]) {
if (source[key] !== undefined) compact[key] = source[key];
}
if (compact.dryRun === undefined && params.dryRun !== undefined) {
compact.dryRun = Boolean(params.dryRun);
}
return { content: [{ type: "text", text: JSON.stringify(compact, null, 2) }] };
}
function footerWriteBody(params: any, fields: string[]) {
const body: Record<string, unknown> = {};
for (const field of fields) {
if (params[field] !== undefined) body[field] = params[field];
}
body.dryRun = Boolean(params.dryRun);
if (params.expectedVersion !== undefined) body.expectedVersion = params.expectedVersion;
return body;
}
export async function handleMenuTool(api: AxiosInstance, name: string, params: any) {
if (!MENU_TOOL_NAMES.has(name)) return null;
switch (name) {
case "get_footer_menu": {
const query = new URLSearchParams();
query.set("lang", params.lang || "th");
query.set("fallback", params.fallback === false ? "false" : "true");
const res = await api.get(`/menus/footer/${params.menuName}?${query.toString()}`);
return compactFooterResponse(res.data, name, params);
}
case "add_footer_menu_item": {
const body = footerWriteBody(params, ["label", "url", "order", "translations"]);
const res = await api.post(`/menus/footer/${params.menuName}/items`, body);
return compactFooterResponse(res.data, name, params);
}
case "update_footer_menu_item": {
const body = footerWriteBody(params, ["label", "url", "order"]);
const res = await api.patch(`/menus/footer/${params.menuName}/items/${params.itemId}`, body);
return compactFooterResponse(res.data, name, params);
}
case "delete_footer_menu_item": {
const body = footerWriteBody(params, []);
const res = await api.delete(`/menus/footer/${params.menuName}/items/${params.itemId}`, { data: body });
return compactFooterResponse(res.data, name, params);
}
case "reorder_footer_menu_items": {
const body = footerWriteBody(params, ["itemIds"]);
const res = await api.put(`/menus/footer/${params.menuName}/items/reorder`, body);
return compactFooterResponse(res.data, name, params);
}
case "update_footer_menu_item_translation": {
const body = footerWriteBody(params, ["label"]);
const res = await api.patch(
`/menus/footer/${params.menuName}/items/${params.itemId}/translations/${params.lang}`,
body,
);
return compactFooterResponse(res.data, name, params);
}
case "get_menu": {
const res = await api.get(`/menus/${params.name}`);
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };