x
This commit is contained in:
1 parent
0ceee3dbff
commit
2d39edbd57
18 files changed
+804
-153
No files matched your search
@@ -129,11 +129,16 @@ th, en, ja, zh, vi, ko, lo, it
|
||||
LLM agents should use this workflow for category edits:
|
||||
|
||||
1. `list_categories` with `lang: "all"` to discover category IDs and current translations
|
||||
2. `create_category`, `update_category`, `update_category_translations`, or `delete_category` with `dryRun: true`
|
||||
2. `create_category`, `update_category`, `update_category_media`, `update_category_translations`, or `delete_category` with `dryRun: true`
|
||||
3. Review the preview and product counts
|
||||
4. Apply only after the intended change is clear
|
||||
5. Run `list_categories` again to verify
|
||||
|
||||
Category media uses semantic roles: `thumbnail`, `hero_desktop`, `hero_mobile`,
|
||||
`card_portrait`, and `social_share`. `update_category_media` replaces the complete
|
||||
media set, so omitted roles are removed. `update_category_image` remains available
|
||||
only as a legacy thumbnail compatibility tool.
|
||||
|
||||
Backend category support currently covers:
|
||||
|
||||
- base `name`
|
||||
|
||||
@@ -104,11 +104,31 @@ function compactCategory(category: any) {
|
||||
id: category.id,
|
||||
name: category.name,
|
||||
imageUrl: category.imageUrl ?? null,
|
||||
media: category.media ?? [],
|
||||
layoutType: category.layoutType ?? undefined,
|
||||
productCount: category._count?.products ?? category.productCount ?? undefined,
|
||||
translations: category.translations ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const categoryMediaSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
role: {
|
||||
type: "string",
|
||||
enum: ["thumbnail", "hero_desktop", "hero_mobile", "card_portrait", "social_share"],
|
||||
description: "Semantic image usage; do not encode a layout name in this field."
|
||||
},
|
||||
url: { type: "string", description: "Image URL or website-relative media path." },
|
||||
width: { type: ["number", "null"], description: "Optional intrinsic width in pixels." },
|
||||
height: { type: ["number", "null"], description: "Optional intrinsic height in pixels." },
|
||||
focalX: { type: "number", minimum: 0, maximum: 1, description: "Horizontal focal point from 0 to 1." },
|
||||
focalY: { type: "number", minimum: 0, maximum: 1, description: "Vertical focal point from 0 to 1." },
|
||||
sortOrder: { type: "number", minimum: 0 }
|
||||
},
|
||||
required: ["role", "url"]
|
||||
};
|
||||
|
||||
function compactProduct(product: any) {
|
||||
if (!product) return null;
|
||||
const images = Array.isArray(product.images) ? product.images : [];
|
||||
@@ -211,12 +231,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
},
|
||||
{
|
||||
name: "create_category",
|
||||
description: "Create a product category. Backend supports base name, imageUrl, and translations. It does not support slug, hierarchy, sort order, or active state for categories.",
|
||||
description: "Create a product category with semantic responsive media and translations. imageUrl remains a legacy thumbnail fallback.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string", description: "Base category name. Use Thai or the primary display name." },
|
||||
imageUrl: { type: ["string", "null"], description: "Optional image URL." },
|
||||
layoutType: { type: "string", enum: ["split_showcase", "editorial_hero", "dense_catalog"] },
|
||||
media: { type: "array", items: categoryMediaSchema, maxItems: 5 },
|
||||
translations: {
|
||||
type: "object",
|
||||
description: "Optional locale map for category names. Supported locales: th, en, ja, zh, vi, ko, lo, it."
|
||||
@@ -228,13 +250,15 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
},
|
||||
{
|
||||
name: "update_category",
|
||||
description: "Update category base name, image URL, and/or translations. Use dryRun first. Backend supports name and imageUrl only for base category fields.",
|
||||
description: "Update category base fields, layout, semantic responsive media, and/or translations. Use dryRun first.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "number", description: "Category ID" },
|
||||
name: { type: "string", description: "Optional base category name." },
|
||||
imageUrl: { type: ["string", "null"], description: "Optional image URL. Pass null to remove." },
|
||||
layoutType: { type: "string", enum: ["split_showcase", "editorial_hero", "dense_catalog"] },
|
||||
media: { type: "array", items: categoryMediaSchema, maxItems: 5 },
|
||||
translations: {
|
||||
type: "object",
|
||||
description: "Optional locale map for category names. Supported locales: th, en, ja, zh, vi, ko, lo, it."
|
||||
@@ -324,9 +348,22 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
required: ["id", "translations"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "update_category_media",
|
||||
description: "Replace the complete semantic media set for a category. Omitted roles are removed. Use dryRun first.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "number", description: "Category ID" },
|
||||
media: { type: "array", items: categoryMediaSchema, maxItems: 5 },
|
||||
dryRun: { type: "boolean", description: "Preview only. No media is changed." }
|
||||
},
|
||||
required: ["id", "media"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "update_category_image",
|
||||
description: "Update the image URL for a specific category.",
|
||||
description: "Legacy compatibility tool. Update the category thumbnail only. Prefer update_category_media for responsive assets.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -1145,13 +1182,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||
}
|
||||
case "create_category": {
|
||||
const { name, imageUrl = null, translations, dryRun } = params as any;
|
||||
const { name, imageUrl = null, layoutType, media, translations, dryRun } = params as any;
|
||||
if (typeof name !== "string" || name.trim().length === 0) {
|
||||
throw new Error("name must be a non-empty string.");
|
||||
}
|
||||
if (translations !== undefined) validateCategoryLocaleMap(translations);
|
||||
|
||||
const payload = { name: name.trim(), imageUrl };
|
||||
const payload = { name: name.trim(), imageUrl, layoutType, media };
|
||||
if (dryRun) {
|
||||
return {
|
||||
content: [{
|
||||
@@ -1164,7 +1201,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
create: payload,
|
||||
translations: translations ?? null,
|
||||
backendSupport: {
|
||||
fields: ["name", "imageUrl", "translations"],
|
||||
fields: ["name", "imageUrl", "layoutType", "media", "translations"],
|
||||
unsupportedFields: ["slug", "parentId", "sortOrder", "isActive"],
|
||||
},
|
||||
},
|
||||
@@ -1193,7 +1230,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
};
|
||||
}
|
||||
case "update_category": {
|
||||
const { id, name, imageUrl, translations, dryRun } = params as any;
|
||||
const { id, name, imageUrl, layoutType, media, translations, dryRun } = params as any;
|
||||
if (name !== undefined && (typeof name !== "string" || name.trim().length === 0)) {
|
||||
throw new Error("name must be a non-empty string when provided.");
|
||||
}
|
||||
@@ -1210,6 +1247,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const baseUpdate: any = {};
|
||||
if (name !== undefined) baseUpdate.name = name.trim();
|
||||
if (imageUrl !== undefined) baseUpdate.imageUrl = imageUrl;
|
||||
if (layoutType !== undefined) baseUpdate.layoutType = layoutType;
|
||||
if (media !== undefined) baseUpdate.media = media;
|
||||
|
||||
if (dryRun) {
|
||||
return {
|
||||
@@ -1225,7 +1264,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
baseUpdate,
|
||||
translations: translations ?? null,
|
||||
backendSupport: {
|
||||
fields: ["name", "imageUrl", "translations"],
|
||||
fields: ["name", "imageUrl", "layoutType", "media", "translations"],
|
||||
unsupportedFields: ["slug", "parentId", "sortOrder", "isActive"],
|
||||
},
|
||||
},
|
||||
@@ -1392,6 +1431,29 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }]
|
||||
};
|
||||
}
|
||||
case "update_category_media": {
|
||||
const { id, media, dryRun } = params as any;
|
||||
if (!Array.isArray(media)) throw new Error("media must be an array.");
|
||||
if (dryRun) {
|
||||
const before = await api.get(`/categories/${id}?lang=all`);
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
dryRun: true,
|
||||
operation: "update_category_media",
|
||||
categoryId: id,
|
||||
before: compactCategory(before.data),
|
||||
media,
|
||||
note: "This is a full replacement; omitted roles will be removed."
|
||||
}, null, 2)
|
||||
}]
|
||||
};
|
||||
}
|
||||
const res = await api.put(`/categories/${id}/media`, { media });
|
||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||
}
|
||||
case "update_product_brand_translation": {
|
||||
const { id, lang, name } = params as any;
|
||||
const res = await api.patch(`/products/brands/${id}/translations`, {
|
||||
|
||||
Reference in new issue
Block a user