x
This commit is contained in:
1 parent
94cbb52348
commit
167d07f0c2
7 files changed
+1284
-21
No files matched your search
@@ -32,7 +32,37 @@ bun run dev
|
||||
|
||||
- `update_brand_name` เปลี่ยนชื่อแบรนด์กลางที่ใช้ใน header/footer ของ `apps/spa` และ `apps/website`
|
||||
- `update_store_settings_translation` อัปเดตข้อความแปลของ store settings
|
||||
- `update_footer_content` อัปเดตเนื้อหา footer และลิงก์ที่เกี่ยวข้อง
|
||||
- `update_footer_content` อัปเดตคำอธิบายแบรนด์และ social links เท่านั้น
|
||||
|
||||
## Footer Menu LLM Workflow
|
||||
|
||||
Footer Explore and Footer Support are flat item menus, not Mega Menu payload trees. Supported menu names are:
|
||||
|
||||
```text
|
||||
footer-explore
|
||||
footer-support
|
||||
```
|
||||
|
||||
Use this workflow for every Footer menu edit:
|
||||
|
||||
1. Call `get_footer_menu` with the target language.
|
||||
2. Keep the returned `version`.
|
||||
3. Call one item-level write tool with `dryRun: true` and `expectedVersion`.
|
||||
4. Review the compact preview.
|
||||
5. Apply the same operation with `dryRun: false` and the same version.
|
||||
6. Call `get_footer_menu` again to verify and obtain the next version.
|
||||
|
||||
Available item-level tools:
|
||||
|
||||
- `add_footer_menu_item`
|
||||
- `update_footer_menu_item`
|
||||
- `delete_footer_menu_item`
|
||||
- `reorder_footer_menu_items`
|
||||
- `update_footer_menu_item_translation`
|
||||
|
||||
Every reorder request must include every current item ID exactly once. A `MENU_VERSION_CONFLICT` response means Admin or another agent changed the menu after it was read; fetch it again before retrying.
|
||||
|
||||
Do not pass `exploreLinks`, `collectionLinks`, or `supportLinks` to legacy `update_footer_content`. Full menu-array replacement is rejected because it can delete item IDs and translations. Website Collections are populated from Categories rather than a `footer-collections` menu.
|
||||
|
||||
## Mega Menu LLM Workflow
|
||||
|
||||
|
||||
+16
-20
@@ -6,7 +6,7 @@ import {
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import axios from "axios";
|
||||
import FormData from "form-data";
|
||||
import { MENU_TOOL_DEFINITIONS, handleMenuTool } from "./menu-toolkit";
|
||||
import { MENU_TOOL_DEFINITIONS, getLegacyFooterMenuDeprecation, handleMenuTool } from "./menu-toolkit";
|
||||
import { SECTION_TOOL_DEFINITIONS, buildCmsPreview, handleSectionTool } from "./section-toolkit";
|
||||
const LOCAL_BACKEND_URL = process.env.MCP_BACKEND_URL_LOCAL || "http://localhost:3051/api";
|
||||
const PRODUCTION_BACKEND_URL = process.env.MCP_BACKEND_URL_PRODUCTION || "https://api.homaroma.shop/api";
|
||||
@@ -949,7 +949,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
},
|
||||
{
|
||||
name: "update_footer_content",
|
||||
description: "Update the content and links of the 4 Footer Blocks (Brand, Explore, Collections, Newsletter)",
|
||||
description: "Legacy tool for Footer brand description and social links only. Footer menu arrays are rejected; use the Footer-specific item tools instead.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -965,7 +965,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
},
|
||||
exploreLinks: {
|
||||
type: "array",
|
||||
description: "Array of { label, url, order } for Block 2 (Explore)",
|
||||
description: "Deprecated and rejected. Use add_footer_menu_item, update_footer_menu_item, delete_footer_menu_item, or reorder_footer_menu_items.",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: { label: { type: "string" }, url: { type: "string" }, order: { type: "number" } }
|
||||
@@ -973,7 +973,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
},
|
||||
collectionLinks: {
|
||||
type: "array",
|
||||
description: "Array of { label, url, order } for Block 3 (Collections)",
|
||||
description: "Deprecated and rejected. Website Collections come from Categories, not a footer menu.",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: { label: { type: "string" }, url: { type: "string" }, order: { type: "number" } }
|
||||
@@ -981,7 +981,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
},
|
||||
supportLinks: {
|
||||
type: "array",
|
||||
description: "Array of { label, url, order } for Block 4 (Support/Legal)",
|
||||
description: "Deprecated and rejected. Use the Footer-specific item tools with menuName=footer-support.",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: { label: { type: "string" }, url: { type: "string" }, order: { type: "number" } }
|
||||
@@ -1661,6 +1661,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
return { content: [{ type: "text", text: JSON.stringify(normalized, null, 2) }] };
|
||||
}
|
||||
case "update_footer_content": {
|
||||
const deprecation = getLegacyFooterMenuDeprecation(params);
|
||||
if (deprecation) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(deprecation, null, 2),
|
||||
}],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const results = [];
|
||||
|
||||
if (params.brandDescription !== undefined || params.socialLinks) {
|
||||
@@ -1687,21 +1698,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (params.exploreLinks) {
|
||||
await api.put("/menus/footer-explore", { items: params.exploreLinks });
|
||||
results.push({ type: "explore_links", status: "success" });
|
||||
}
|
||||
|
||||
if (params.collectionLinks) {
|
||||
await api.put("/menus/footer-collections", { items: params.collectionLinks });
|
||||
results.push({ type: "collection_links", status: "success" });
|
||||
}
|
||||
|
||||
if (params.supportLinks) {
|
||||
await api.put("/menus/footer-support", { items: params.supportLinks });
|
||||
results.push({ type: "support_links", status: "success" });
|
||||
}
|
||||
|
||||
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
|
||||
}
|
||||
default:
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
MENU_TOOL_DEFINITIONS,
|
||||
getLegacyFooterMenuDeprecation,
|
||||
handleMenuTool,
|
||||
} from "./menu-toolkit";
|
||||
|
||||
type Call = {
|
||||
method: string;
|
||||
url: string;
|
||||
body?: unknown;
|
||||
config?: unknown;
|
||||
};
|
||||
|
||||
function createApi(response: unknown = { success: true, version: "v2" }) {
|
||||
const calls: Call[] = [];
|
||||
const api = {
|
||||
get: async (url: string) => {
|
||||
calls.push({ method: "get", url });
|
||||
return { data: response };
|
||||
},
|
||||
post: async (url: string, body: unknown) => {
|
||||
calls.push({ method: "post", url, body });
|
||||
return { data: response };
|
||||
},
|
||||
patch: async (url: string, body: unknown) => {
|
||||
calls.push({ method: "patch", url, body });
|
||||
return { data: response };
|
||||
},
|
||||
put: async (url: string, body: unknown) => {
|
||||
calls.push({ method: "put", url, body });
|
||||
return { data: response };
|
||||
},
|
||||
delete: async (url: string, config: unknown) => {
|
||||
calls.push({ method: "delete", url, config });
|
||||
return { data: response };
|
||||
},
|
||||
};
|
||||
return { api: api as any, calls };
|
||||
}
|
||||
|
||||
function output(result: any) {
|
||||
return JSON.parse(result.content[0].text);
|
||||
}
|
||||
|
||||
describe("Footer MCP tool definitions", () => {
|
||||
test("registers six Footer-specific tools with constrained menu names", () => {
|
||||
const names = [
|
||||
"get_footer_menu",
|
||||
"add_footer_menu_item",
|
||||
"update_footer_menu_item",
|
||||
"delete_footer_menu_item",
|
||||
"reorder_footer_menu_items",
|
||||
"update_footer_menu_item_translation",
|
||||
];
|
||||
|
||||
for (const name of names) {
|
||||
const definition = MENU_TOOL_DEFINITIONS.find((tool) => tool.name === name) as any;
|
||||
expect(definition).toBeDefined();
|
||||
expect(definition.inputSchema.properties.menuName.enum).toEqual([
|
||||
"footer-explore",
|
||||
"footer-support",
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
test("all Footer writes expose dryRun and expectedVersion", () => {
|
||||
const writes = MENU_TOOL_DEFINITIONS.filter((tool) =>
|
||||
[
|
||||
"add_footer_menu_item",
|
||||
"update_footer_menu_item",
|
||||
"delete_footer_menu_item",
|
||||
"reorder_footer_menu_items",
|
||||
"update_footer_menu_item_translation",
|
||||
].includes(tool.name),
|
||||
) as any[];
|
||||
|
||||
for (const tool of writes) {
|
||||
expect(tool.inputSchema.properties.dryRun).toBeDefined();
|
||||
expect(tool.inputSchema.properties.expectedVersion).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
test("update_footer_menu_item exposes only supported mutable item fields", () => {
|
||||
const definition = MENU_TOOL_DEFINITIONS.find(
|
||||
(tool) => tool.name === "update_footer_menu_item",
|
||||
) as any;
|
||||
|
||||
expect(Object.keys(definition.inputSchema.properties).sort()).toEqual([
|
||||
"dryRun",
|
||||
"expectedVersion",
|
||||
"itemId",
|
||||
"label",
|
||||
"menuName",
|
||||
"order",
|
||||
"url",
|
||||
]);
|
||||
expect(definition.inputSchema.properties.isActive).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Footer MCP proxy handlers", () => {
|
||||
test("reads a compact localized Footer menu", async () => {
|
||||
const { api, calls } = createApi({
|
||||
success: true,
|
||||
menuName: "footer-explore",
|
||||
lang: "zh",
|
||||
version: "v1",
|
||||
items: [{ id: 1, label: "商店", url: "/shop", order: 1 }],
|
||||
internalMetadata: "must not leak",
|
||||
});
|
||||
const result = await handleMenuTool(api, "get_footer_menu", {
|
||||
menuName: "footer-explore",
|
||||
lang: "zh",
|
||||
fallback: false,
|
||||
});
|
||||
|
||||
expect(calls).toEqual([{
|
||||
method: "get",
|
||||
url: "/menus/footer/footer-explore?lang=zh&fallback=false",
|
||||
}]);
|
||||
expect(output(result)).toEqual({
|
||||
success: true,
|
||||
operation: "get_footer_menu",
|
||||
menuName: "footer-explore",
|
||||
version: "v1",
|
||||
lang: "zh",
|
||||
items: [{ id: 1, label: "商店", url: "/shop", order: 1 }],
|
||||
});
|
||||
});
|
||||
|
||||
test("adds an item with write controls", async () => {
|
||||
const { api, calls } = createApi();
|
||||
await handleMenuTool(api, "add_footer_menu_item", {
|
||||
menuName: "footer-explore",
|
||||
label: "Shop",
|
||||
url: "/shop",
|
||||
order: 2,
|
||||
translations: { zh: "商店" },
|
||||
dryRun: true,
|
||||
expectedVersion: "v1",
|
||||
});
|
||||
|
||||
expect(calls[0]).toEqual({
|
||||
method: "post",
|
||||
url: "/menus/footer/footer-explore/items",
|
||||
body: {
|
||||
label: "Shop",
|
||||
url: "/shop",
|
||||
order: 2,
|
||||
translations: { zh: "商店" },
|
||||
dryRun: true,
|
||||
expectedVersion: "v1",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("partially updates an item without forwarding unrelated fields", async () => {
|
||||
const { api, calls } = createApi();
|
||||
await handleMenuTool(api, "update_footer_menu_item", {
|
||||
menuName: "footer-support",
|
||||
itemId: 9,
|
||||
label: "Contact",
|
||||
dryRun: false,
|
||||
expectedVersion: "v4",
|
||||
translations: { zh: "ignored" },
|
||||
isActive: true,
|
||||
});
|
||||
|
||||
expect(calls[0]).toEqual({
|
||||
method: "patch",
|
||||
url: "/menus/footer/footer-support/items/9",
|
||||
body: { label: "Contact", dryRun: false, expectedVersion: "v4" },
|
||||
});
|
||||
});
|
||||
|
||||
test("deletes an item with Axios DELETE body config", async () => {
|
||||
const { api, calls } = createApi();
|
||||
await handleMenuTool(api, "delete_footer_menu_item", {
|
||||
menuName: "footer-support",
|
||||
itemId: 7,
|
||||
dryRun: true,
|
||||
expectedVersion: "v3",
|
||||
});
|
||||
|
||||
expect(calls[0]).toEqual({
|
||||
method: "delete",
|
||||
url: "/menus/footer/footer-support/items/7",
|
||||
config: { data: { dryRun: true, expectedVersion: "v3" } },
|
||||
});
|
||||
});
|
||||
|
||||
test("reorders using the complete item ID list", async () => {
|
||||
const { api, calls } = createApi();
|
||||
await handleMenuTool(api, "reorder_footer_menu_items", {
|
||||
menuName: "footer-explore",
|
||||
itemIds: [3, 1, 2],
|
||||
expectedVersion: "v8",
|
||||
});
|
||||
|
||||
expect(calls[0]).toEqual({
|
||||
method: "put",
|
||||
url: "/menus/footer/footer-explore/items/reorder",
|
||||
body: { itemIds: [3, 1, 2], dryRun: false, expectedVersion: "v8" },
|
||||
});
|
||||
});
|
||||
|
||||
test("updates one item translation through the language endpoint", async () => {
|
||||
const { api, calls } = createApi();
|
||||
await handleMenuTool(api, "update_footer_menu_item_translation", {
|
||||
menuName: "footer-explore",
|
||||
itemId: 12,
|
||||
lang: "zh",
|
||||
label: "商店",
|
||||
dryRun: true,
|
||||
expectedVersion: "v5",
|
||||
});
|
||||
|
||||
expect(calls[0]).toEqual({
|
||||
method: "patch",
|
||||
url: "/menus/footer/footer-explore/items/12/translations/zh",
|
||||
body: { label: "商店", dryRun: true, expectedVersion: "v5" },
|
||||
});
|
||||
});
|
||||
|
||||
test("preserves compact mutation, preview, and conflict response keys", async () => {
|
||||
const { api } = createApi({
|
||||
success: false,
|
||||
operation: "update_footer_menu_item",
|
||||
menuName: "footer-support",
|
||||
itemId: 18,
|
||||
itemIds: [18, 20],
|
||||
label: "Help",
|
||||
before: { label: "Support" },
|
||||
after: { label: "Help" },
|
||||
code: "MENU_VERSION_CONFLICT",
|
||||
error: "The menu changed after it was read.",
|
||||
partialWrite: false,
|
||||
version: "v9",
|
||||
databaseRecord: { mustNotLeak: true },
|
||||
});
|
||||
const result = await handleMenuTool(api, "update_footer_menu_item", {
|
||||
menuName: "footer-support",
|
||||
itemId: 18,
|
||||
label: "Help",
|
||||
expectedVersion: "v8",
|
||||
});
|
||||
|
||||
expect(output(result)).toEqual({
|
||||
success: false,
|
||||
operation: "update_footer_menu_item",
|
||||
menuName: "footer-support",
|
||||
version: "v9",
|
||||
itemId: 18,
|
||||
itemIds: [18, 20],
|
||||
label: "Help",
|
||||
before: { label: "Support" },
|
||||
after: { label: "Help" },
|
||||
code: "MENU_VERSION_CONFLICT",
|
||||
error: "The menu changed after it was read.",
|
||||
partialWrite: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Legacy Footer mutation guard", () => {
|
||||
test("rejects every legacy menu array before any partial write", () => {
|
||||
expect(getLegacyFooterMenuDeprecation({
|
||||
brandDescription: "Must not be written",
|
||||
exploreLinks: [],
|
||||
collectionLinks: [],
|
||||
supportLinks: [],
|
||||
})).toEqual({
|
||||
success: false,
|
||||
code: "FOOTER_MENU_REPLACEMENT_DEPRECATED",
|
||||
partialWrite: false,
|
||||
rejectedFields: ["exploreLinks", "collectionLinks", "supportLinks"],
|
||||
message: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
test("retains legacy brand and social settings support", () => {
|
||||
expect(getLegacyFooterMenuDeprecation({
|
||||
brandDescription: "Brand",
|
||||
socialLinks: { instagramUrl: "https://instagram.com/example" },
|
||||
})).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -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) }] };
|
||||
|
||||
Reference in new issue
Block a user