@sea-ecommerce/admin-mcp-server 0.1.0 → 0.1.2

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/README.md CHANGED
@@ -11,9 +11,15 @@
11
11
  | 工具 | 说明 | 写操作 |
12
12
  |---|---|---|
13
13
  | `list_products` / `get_product` | 查商品 | 读 |
14
- | `create_product` / `update_product` / `deactivate_product` | 增 / 改 / 下线商品 | 写(dry_run 默认) |
14
+ | `list_sales_channels` / `list_collections` | 列渠道 / 集合 | 读 |
15
+ | `get_translations` | 查多语言翻译(核对翻译用) | 读 |
16
+ | `create_product` / `update_product` / `deactivate_product` | 增 / 改 / 下线商品(含图片、SKU、渠道、物理/海关字段、metadata 合并) | 写(dry_run 默认) |
17
+ | `update_product_prices` | 替换变体价格(多币种) | 写(dry_run 默认) |
18
+ | `upload_product_image` / `add_product_to_sales_channel` | 追加图片 / 挂渠道 | 写(dry_run 默认) |
19
+ | `create_product_content` / `get_product_content` | 详情页富内容块(hero/richtext/specs/图集/faq/video) | 写 / 读 |
20
+ | `set_product_stock` | 给商品建仓/改库存(自动建库存条目,幂等) | 写(dry_run 默认) |
15
21
  | `list_stock_locations` / `create_stock_location` | 列 / 增仓库 | 写(dry_run 默认) |
16
- | `get_inventory_levels` / `update_inventory_level` | 查 / 改库存 | 写(dry_run 默认) |
22
+ | `get_inventory_levels` / `update_inventory_level` | 查 / 改库存水平 | 写(dry_run 默认) |
17
23
  | `list_orders` / `get_order` | 查订单 | 读 |
18
24
  | `list_warehouse_tasks` / `list_return_requests` | 查履约 / 退货 | 读 |
19
25
  | `search_docs` / `ask_usage` | 基于 `docs/` 回答后台使用问题 | 读 |
@@ -21638,6 +21638,15 @@ var AuthManager = class {
21638
21638
  };
21639
21639
 
21640
21640
  // src/client.ts
21641
+ import { readFileSync as readFileSync2, existsSync as existsSync2 } from "node:fs";
21642
+ function mimeOf(path) {
21643
+ const ext = path.split(".").pop()?.toLowerCase() || "";
21644
+ if (ext === "webp") return "image/webp";
21645
+ if (ext === "png") return "image/png";
21646
+ if (ext === "jpg" || ext === "jpeg") return "image/jpeg";
21647
+ if (ext === "gif") return "image/gif";
21648
+ return "application/octet-stream";
21649
+ }
21641
21650
  var MedusaAdminError = class extends Error {
21642
21651
  constructor(status, message) {
21643
21652
  super(message);
@@ -21685,12 +21694,122 @@ var MedusaAdminClient = class {
21685
21694
  getProduct(id) {
21686
21695
  return this.request("GET", `/admin/products/${encodeURIComponent(id)}`);
21687
21696
  }
21697
+ /** 列出销售渠道(用于默认渠道解析) */
21698
+ listSalesChannels(params = {}) {
21699
+ return this.request("GET", `/admin/sales-channels${buildQuery(params)}`);
21700
+ }
21688
21701
  createProduct(body) {
21689
21702
  return this.request("POST", "/admin/products", body);
21690
21703
  }
21691
21704
  updateProduct(id, body) {
21692
21705
  return this.request("POST", `/admin/products/${encodeURIComponent(id)}`, body);
21693
21706
  }
21707
+ /**
21708
+ * 整体替换某变体的价格(多币种)。Medusa v2 价格挂在「变体」上,
21709
+ * POST /admin/products/{id} 携带 variants:[{id, prices}] 即整体替换该变体价格(不会累加)。
21710
+ * prices 数组形如 [{ currency_code, amount }, ...],amount 为主货币单位(元),切勿 ×100。
21711
+ */
21712
+ updateProductPrices(productId, variantId, prices) {
21713
+ return this.request("POST", `/admin/products/${encodeURIComponent(productId)}`, {
21714
+ variants: [{ id: variantId, prices }]
21715
+ });
21716
+ }
21717
+ /** 批量写入翻译(POST /admin/translations/batch)。reference 一般为 "product"。 */
21718
+ async batchTranslations(create, update = []) {
21719
+ return this.request("POST", "/admin/translations/batch", { create, update });
21720
+ }
21721
+ /**
21722
+ * 查询翻译(GET /admin/translations)——用于核对多语言是否写入成功。
21723
+ * ?reference=product&reference_id=<productId>[&locale_code=zh]
21724
+ */
21725
+ listTranslations(params = {}) {
21726
+ return this.request("GET", `/admin/translations${buildQuery(params)}`);
21727
+ }
21728
+ /** 列出集合(GET /admin/collections),用于按名称找 collection_id。 */
21729
+ listCollections(params = {}) {
21730
+ return this.request("GET", `/admin/collections${buildQuery(params)}`);
21731
+ }
21732
+ /**
21733
+ * 把一组商品加入销售渠道(POST /admin/sales-channels/{id}/products)。
21734
+ * ⚠️ 注意:Medusa v2 该端点挂在「渠道」下,不是商品下;body 为 { add:[productIds], remove:[] }。
21735
+ * 商品只有挂到渠道,storefront(按 publishable key → 渠道)才能查到,否则详情页拉不到。
21736
+ */
21737
+ addProductsToSalesChannel(channelId, productIds) {
21738
+ return this.request("POST", `/admin/sales-channels/${encodeURIComponent(channelId)}/products`, {
21739
+ add: productIds,
21740
+ remove: []
21741
+ });
21742
+ }
21743
+ // ─── 商品详情页富内容(product_content 模块)───
21744
+ /**
21745
+ * 读取某商品某语言的富内容块(GET /admin/product-contents)。
21746
+ * locale 用区域码:en-US / zh-CN / ms-MY / id-ID(与 storefront useProductContent 的 localeMap 一致)。
21747
+ * 返回 { product_content: { id, product_id, locale, status, blocks(JSON 字符串) } | null }。
21748
+ */
21749
+ getProductContent(productId, locale = "en-US", status = "published") {
21750
+ return this.request(
21751
+ "GET",
21752
+ `/admin/product-contents${buildQuery({ product_id: productId, locale, status })}`
21753
+ );
21754
+ }
21755
+ /**
21756
+ * 创建/更新某商品某语言的富内容块(POST /admin/product-contents)。
21757
+ * body: { product_id, locale, status?, blocks };blocks 为 ContentBlock[] 数组
21758
+ * (type ∈ hero/richtext/image_gallery/video/faq/specs)。按 (product_id, locale) upsert。
21759
+ */
21760
+ createProductContent(body) {
21761
+ return this.request("POST", "/admin/product-contents", body);
21762
+ }
21763
+ /**
21764
+ * 上传一组文件到 Medusa 文件服务(POST /admin/uploads,multipart 字段名 files),
21765
+ * 返回上传后的公开 URL 列表。
21766
+ */
21767
+ async uploadFiles(files) {
21768
+ if (!files.length) return [];
21769
+ const form = new FormData();
21770
+ for (const f of files) {
21771
+ form.append("files", new Blob([new Uint8Array(f.buffer)], { type: f.mimetype }), f.filename);
21772
+ }
21773
+ const doPost = async () => {
21774
+ const res = await fetch(`${this.config.backendUrl}/admin/uploads`, {
21775
+ method: "POST",
21776
+ headers: this.auth.headers(),
21777
+ body: form
21778
+ });
21779
+ if (res.status === 401) {
21780
+ this.auth.clear();
21781
+ await this.auth.login();
21782
+ return doPost();
21783
+ }
21784
+ if (!res.ok) {
21785
+ const text = await res.text();
21786
+ throw new MedusaAdminError(res.status, text || res.statusText);
21787
+ }
21788
+ return await res.json();
21789
+ };
21790
+ const data = await doPost();
21791
+ return (data.files || []).map((f) => f.url);
21792
+ }
21793
+ /**
21794
+ * 把一组「本地文件路径 或 图片 URL」解析为可挂到商品的 URL:
21795
+ * - http(s) URL 原样透传;
21796
+ * - 本地路径读取后走 /admin/uploads 上传,返回 URL。
21797
+ */
21798
+ async uploadProductAssets(pathsOrUrls) {
21799
+ const urls = [];
21800
+ const toUpload = [];
21801
+ for (const p of pathsOrUrls) {
21802
+ if (/^https?:\/\//i.test(p)) {
21803
+ urls.push(p);
21804
+ continue;
21805
+ }
21806
+ if (!existsSync2(p)) throw new MedusaAdminError(0, `\u56FE\u7247\u6587\u4EF6\u4E0D\u5B58\u5728: ${p}`);
21807
+ const buf = readFileSync2(p);
21808
+ toUpload.push({ filename: p.split("/").pop() || "image", mimetype: mimeOf(p), buffer: buf });
21809
+ }
21810
+ if (toUpload.length) urls.push(...await this.uploadFiles(toUpload));
21811
+ return urls;
21812
+ }
21694
21813
  // ─── 仓库(StockLocation)/ 库存 ───
21695
21814
  listStockLocations(params = {}) {
21696
21815
  return this.request("GET", `/admin/stock-locations${buildQuery(params)}`);
@@ -21698,25 +21817,66 @@ var MedusaAdminClient = class {
21698
21817
  createStockLocation(body) {
21699
21818
  return this.request("POST", "/admin/stock-locations", body);
21700
21819
  }
21701
- /** 某仓库下的库存条目及库存水平 */
21702
- listInventoryItems(locationId, params = {}) {
21820
+ /**
21821
+ * 库存条目列表(GET /admin/inventory-items,Medusa 2.21 契约)。
21822
+ * ⚠️ 2.21 没有 GET /admin/stock-locations/{id}/inventory-items 路由;
21823
+ * 按仓库过滤用查询参数:`location_levels.location_id=<sloc_xxx>`(点号写法)。
21824
+ * 常用参数:q(匹配 sku/title)、sku(精确)、location_levels.location_id。
21825
+ */
21826
+ listInventoryItems(params = {}) {
21827
+ return this.request("GET", `/admin/inventory-items${buildQuery(params)}`);
21828
+ }
21829
+ /**
21830
+ * 创建库存条目(POST /admin/inventory-items)。
21831
+ * body: { sku, title?, weight?, length?, height?, width?, requires_shipping?, ... },
21832
+ * 可附带 location_levels: [{ location_id, stocked_quantity }] 一步建仓。
21833
+ * sku 有唯一约束(未删除项),同 sku 重复创建会 409。
21834
+ */
21835
+ createInventoryItem(body) {
21836
+ return this.request("POST", "/admin/inventory-items", body);
21837
+ }
21838
+ /** 某库存条目已有的库存水平(GET /admin/inventory-items/{id}/location-levels[?location_id=]) */
21839
+ getInventoryItemLevels(inventoryItemId, params = {}) {
21703
21840
  return this.request(
21704
21841
  "GET",
21705
- `/admin/stock-locations/${encodeURIComponent(locationId)}/inventory-items${buildQuery(params)}`
21842
+ `/admin/inventory-items/${encodeURIComponent(inventoryItemId)}/location-levels${buildQuery(params)}`
21706
21843
  );
21707
21844
  }
21708
- /**
21709
- * 设置/更新某库存条目在指定仓库的库存水平。
21710
- * ⚠️ 实施时按 Medusa 2.21 官方契约核对端点与字段
21711
- * (v2 常见为 POST /admin/inventory-items/{id}/location-levels,body 含 location_id + stocked_quantity)。
21712
- */
21713
- updateInventoryLevel(inventoryItemId, body) {
21845
+ /** 新建某库存条目在某仓库的库存水平(POST .../location-levels,body: { location_id, stocked_quantity? }) */
21846
+ createInventoryItemLevel(inventoryItemId, body) {
21714
21847
  return this.request(
21715
21848
  "POST",
21716
21849
  `/admin/inventory-items/${encodeURIComponent(inventoryItemId)}/location-levels`,
21717
21850
  body
21718
21851
  );
21719
21852
  }
21853
+ /** 更新某库存条目在指定仓库的库存水平(POST .../location-levels/{location_id},body: { stocked_quantity? }) */
21854
+ updateInventoryItemLevel(inventoryItemId, locationId, body) {
21855
+ return this.request(
21856
+ "POST",
21857
+ `/admin/inventory-items/${encodeURIComponent(inventoryItemId)}/location-levels/${encodeURIComponent(locationId)}`,
21858
+ body
21859
+ );
21860
+ }
21861
+ /**
21862
+ * 幂等地设置某库存条目在指定仓库的库存数量:
21863
+ * 已有 (item, location) 水平 → 更新;没有 → 创建。
21864
+ */
21865
+ async upsertInventoryLevel(inventoryItemId, locationId, stockedQuantity) {
21866
+ const existing = await this.getInventoryItemLevels(inventoryItemId, {
21867
+ location_id: locationId
21868
+ });
21869
+ const found = (existing?.inventory_levels || []).find((l) => l.location_id === locationId);
21870
+ if (found) {
21871
+ await this.updateInventoryItemLevel(inventoryItemId, locationId, { stocked_quantity: stockedQuantity });
21872
+ return { created: false, stocked_quantity: stockedQuantity };
21873
+ }
21874
+ await this.createInventoryItemLevel(inventoryItemId, {
21875
+ location_id: locationId,
21876
+ stocked_quantity: stockedQuantity
21877
+ });
21878
+ return { created: true, stocked_quantity: stockedQuantity };
21879
+ }
21720
21880
  // ─── 订单 ───
21721
21881
  listOrders(params = {}) {
21722
21882
  return this.request("GET", `/admin/orders${buildQuery(params)}`);
@@ -21733,6 +21893,180 @@ var MedusaAdminClient = class {
21733
21893
  }
21734
21894
  };
21735
21895
 
21896
+ // src/tools/stock.ts
21897
+ async function applyProductStock(ctx2, opts) {
21898
+ const { client: client2 } = ctx2;
21899
+ const prod = await client2.getProduct(opts.productId);
21900
+ const product = prod?.product ?? prod;
21901
+ if (!product?.variants?.length) throw new Error(`\u5546\u54C1 ${opts.productId} \u6CA1\u6709\u53D8\u4F53\uFF0C\u65E0\u6CD5\u5EFA\u4ED3`);
21902
+ const variant = opts.sku ? product.variants.find((v) => v.sku === opts.sku) : product.variants[0];
21903
+ if (!variant) throw new Error(`\u627E\u4E0D\u5230\u76EE\u6807\u53D8\u4F53\uFF08sku=${opts.sku || "\u7B2C\u4E00\u4E2A"}\uFF09`);
21904
+ let location = null;
21905
+ if (opts.locationId) {
21906
+ const locs = await client2.listStockLocations({ limit: 100 });
21907
+ location = (locs?.stock_locations || []).find((l) => l.id === opts.locationId);
21908
+ if (!location) throw new Error(`\u627E\u4E0D\u5230\u4ED3\u5E93 ${opts.locationId}`);
21909
+ } else {
21910
+ const locs = await client2.listStockLocations({ limit: 100 });
21911
+ const list = locs?.stock_locations || [];
21912
+ location = list.find((l) => /main warehouse/i.test(l.name)) || list[0];
21913
+ if (!location) throw new Error("\u6CA1\u6709\u4EFB\u4F55\u53EF\u7528\u4ED3\u5E93\uFF0C\u8BF7\u5148 create_stock_location");
21914
+ }
21915
+ let iitem = null;
21916
+ if (variant.sku) {
21917
+ const items = await client2.listInventoryItems({ sku: variant.sku, limit: 10 });
21918
+ iitem = (items?.inventory_items || []).find((i) => i.sku === variant.sku) || null;
21919
+ }
21920
+ if (!iitem) {
21921
+ if (!variant.sku) throw new Error(`\u53D8\u4F53\u6CA1\u6709 SKU\uFF0C\u65E0\u6CD5\u5173\u8054\u5E93\u5B58\u6761\u76EE\uFF08\u8BF7\u5148 update_product \u8865 sku\uFF09`);
21922
+ const created = await client2.createInventoryItem({
21923
+ sku: variant.sku,
21924
+ title: variant.title || void 0
21925
+ });
21926
+ iitem = created?.inventory_item ?? created;
21927
+ }
21928
+ const level = await client2.upsertInventoryLevel(iitem.id, location.id, opts.quantity);
21929
+ if (Boolean(variant.manage_inventory) !== opts.manageInventory) {
21930
+ await client2.updateProduct(product.id, {
21931
+ variants: [{ id: variant.id, manage_inventory: opts.manageInventory }]
21932
+ });
21933
+ }
21934
+ return {
21935
+ product_id: product.id,
21936
+ variant_id: variant.id,
21937
+ sku: variant.sku,
21938
+ location_id: location.id,
21939
+ location_name: location.name,
21940
+ inventory_item_id: iitem.id,
21941
+ manage_inventory: opts.manageInventory,
21942
+ stocked_quantity: level.stocked_quantity,
21943
+ level_created: level.created
21944
+ };
21945
+ }
21946
+ var stockTools = [
21947
+ {
21948
+ name: "list_stock_locations",
21949
+ description: "\u5217\u51FA\u4ED3\u5E93\uFF08Medusa StockLocation\uFF09\u3002\u53EF\u9009\u5173\u952E\u8BCD q \u8FC7\u6EE4\u540D\u79F0\u3002",
21950
+ schema: {
21951
+ q: external_exports.string().optional(),
21952
+ limit: external_exports.number().int().positive().max(100).default(50)
21953
+ },
21954
+ async run(input, { client: client2 }) {
21955
+ return client2.listStockLocations({ q: input.q, limit: input.limit });
21956
+ }
21957
+ },
21958
+ {
21959
+ name: "create_stock_location",
21960
+ description: "\u589E\u52A0\u4E00\u4E2A\u4ED3\u5E93\uFF08StockLocation\uFF09\u3002\u9ED8\u8BA4 dry_run\u3002",
21961
+ schema: {
21962
+ name: external_exports.string(),
21963
+ address: external_exports.object({
21964
+ address_1: external_exports.string().optional(),
21965
+ address_2: external_exports.string().optional(),
21966
+ city: external_exports.string().optional(),
21967
+ country_code: external_exports.string().optional(),
21968
+ province: external_exports.string().optional(),
21969
+ postal_code: external_exports.string().optional(),
21970
+ phone: external_exports.string().optional()
21971
+ }).optional(),
21972
+ metadata: external_exports.record(external_exports.string(), external_exports.string()).optional(),
21973
+ confirm: external_exports.boolean().default(false)
21974
+ },
21975
+ async run(input, { client: client2 }) {
21976
+ const payload = { name: input.name };
21977
+ if (input.address) payload.address = input.address;
21978
+ if (input.metadata) payload.metadata = input.metadata;
21979
+ if (!input.confirm) return { dry_run: true, will_create: payload };
21980
+ return client2.createStockLocation(payload);
21981
+ }
21982
+ },
21983
+ {
21984
+ name: "get_inventory_levels",
21985
+ description: `\u67E5\u8BE2\u5E93\u5B58\u6761\u76EE\u53CA\u5E93\u5B58\u6C34\u5E73\uFF08GET /admin/inventory-items\uFF0CMedusa 2.21 \u5951\u7EA6\uFF09\u3002
21986
+ location_id \u53EF\u9009\uFF1A\u4F20\u4E86\u5219\u53EA\u56DE\u8BE5\u4ED3\u5E93\u4E0B\u6709\u6C34\u5E73\u7684\u6761\u76EE\uFF1B\u4E0D\u4F20\u5219\u5217\u5168\u90E8\u3002q \u6309 sku/title \u6A21\u7CCA\u8FC7\u6EE4\u3002`,
21987
+ schema: {
21988
+ location_id: external_exports.string().optional().describe("\u4ED3\u5E93 id\uFF1B\u7701\u7565\u5219\u4E0D\u9650\u4ED3\u5E93"),
21989
+ q: external_exports.string().optional(),
21990
+ limit: external_exports.number().int().positive().max(100).default(50)
21991
+ },
21992
+ async run(input, { client: client2 }) {
21993
+ const params = { limit: input.limit };
21994
+ if (input.q) params.q = input.q;
21995
+ if (input.location_id) params["location_levels.location_id"] = input.location_id;
21996
+ return client2.listInventoryItems(params);
21997
+ }
21998
+ },
21999
+ {
22000
+ name: "set_product_stock",
22001
+ description: `\u7ED9\u5546\u54C1\u5EFA\u4ED3/\u6539\u5E93\u5B58\uFF08\u5E42\u7B49\uFF09\uFF1A\u786E\u4FDD\u53D8\u4F53\u5BF9\u5E94\u7684\u5E93\u5B58\u6761\u76EE\u5B58\u5728\uFF08\u7F3A\u5931\u5219\u6309\u53D8\u4F53 SKU \u81EA\u52A8\u521B\u5EFA\uFF09\uFF0C
22002
+ \u518D\u628A\u6307\u5B9A\u4ED3\u5E93\u7684\u5E93\u5B58\u6570\u91CF\u8BBE\u4E3A quantity\uFF0C\u5E76\u628A\u53D8\u4F53 manage_inventory \u7F6E\u4E3A true\u3002
22003
+ \u26A0\uFE0F \u4E0D\u6307\u5B9A location_id \u65F6\u9ED8\u8BA4\u5199\u5165\u540D\u79F0\u542B "Main Warehouse" \u7684\u4ED3\u5E93\uFF08\u5426\u5219\u53D6\u7B2C\u4E00\u4E2A\u4ED3\u5E93\uFF09\u3002
22004
+ \u26A0\uFE0F \u672A\u5EFA\u4ED3\uFF08manage_inventory=false \u6216 0 \u5E93\u5B58\uFF09\u7684\u5546\u54C1\u5728\u524D\u7AEF\u8868\u73B0\u4E3A\u300C\u7F3A\u8D27\u300D\uFF0C\u6B63\u5F0F\u4E0A\u67B6\u524D\u5FC5\u987B\u5EFA\u4ED3\u3002
22005
+ \u9ED8\u8BA4 dry_run\uFF08confirm=false\uFF09\u4EC5\u9884\u89C8\uFF1Bconfirm=true \u624D\u5199\u5165\u3002`,
22006
+ schema: {
22007
+ product_id: external_exports.string().describe("\u5546\u54C1 id \u6216 handle"),
22008
+ sku: external_exports.string().optional().describe("\u76EE\u6807\u53D8\u4F53 SKU\uFF1B\u7701\u7565\u5219\u53D6\u7B2C\u4E00\u4E2A\u53D8\u4F53"),
22009
+ location_id: external_exports.string().optional().describe("\u4ED3\u5E93 id\uFF1B\u7701\u7565\u5219 Main Warehouse/\u7B2C\u4E00\u4E2A"),
22010
+ quantity: external_exports.number().int().nonnegative().describe("\u8BE5\u4ED3\u5E93\u5E93\u5B58\u6570\u91CF\uFF08\u7EDD\u5BF9\u503C\uFF0C\u975E\u589E\u91CF\uFF09"),
22011
+ manage_inventory: external_exports.boolean().default(true).describe("\u53D8\u4F53\u662F\u5426\u542F\u7528\u5E93\u5B58\u7BA1\u7406\uFF0C\u9ED8\u8BA4 true"),
22012
+ confirm: external_exports.boolean().default(false)
22013
+ },
22014
+ async run(input, ctx2) {
22015
+ const plan = {
22016
+ product_id: input.product_id,
22017
+ sku: input.sku || "(\u7B2C\u4E00\u4E2A\u53D8\u4F53)",
22018
+ location_id: input.location_id || "(Main Warehouse/\u7B2C\u4E00\u4E2A\u4ED3\u5E93)",
22019
+ quantity: input.quantity,
22020
+ manage_inventory: input.manage_inventory
22021
+ };
22022
+ if (!input.confirm) {
22023
+ return { dry_run: true, will_set_stock: plan };
22024
+ }
22025
+ try {
22026
+ const result = await applyProductStock(ctx2, {
22027
+ productId: input.product_id,
22028
+ sku: input.sku,
22029
+ locationId: input.location_id,
22030
+ quantity: input.quantity,
22031
+ manageInventory: input.manage_inventory
22032
+ });
22033
+ return { set_stock: result };
22034
+ } catch (e) {
22035
+ return `\u9519\u8BEF\uFF1A\u5EFA\u4ED3\u5931\u8D25 \u2014\u2014 ${e?.message || e}`;
22036
+ }
22037
+ }
22038
+ },
22039
+ {
22040
+ name: "update_inventory_level",
22041
+ description: `\u4FEE\u6539\u67D0\u5E93\u5B58\u6761\u76EE\u5728\u6307\u5B9A\u4ED3\u5E93\u7684\u5E93\u5B58\u6570\u91CF\uFF08\u7EDD\u5BF9\u503C\uFF0C\u5E42\u7B49 upsert\uFF09\u3002
22042
+ \u76F4\u63A5\u7ED9\u5546\u54C1\u5EFA\u4ED3\u8BF7\u4F18\u5148\u7528 set_product_stock\uFF08\u4F1A\u81EA\u52A8\u627E/\u5EFA\u5E93\u5B58\u6761\u76EE\uFF09\u3002\u9ED8\u8BA4 dry_run\u3002`,
22043
+ schema: {
22044
+ inventory_item_id: external_exports.string(),
22045
+ location_id: external_exports.string(),
22046
+ stocked_quantity: external_exports.number().int().nonnegative(),
22047
+ confirm: external_exports.boolean().default(false)
22048
+ },
22049
+ async run(input, { client: client2 }) {
22050
+ if (!input.confirm) {
22051
+ return {
22052
+ dry_run: true,
22053
+ will_update: {
22054
+ inventory_item_id: input.inventory_item_id,
22055
+ location_id: input.location_id,
22056
+ stocked_quantity: input.stocked_quantity
22057
+ }
22058
+ };
22059
+ }
22060
+ const level = await client2.upsertInventoryLevel(
22061
+ input.inventory_item_id,
22062
+ input.location_id,
22063
+ input.stocked_quantity
22064
+ );
22065
+ return { updated: { ...level, inventory_item_id: input.inventory_item_id, location_id: input.location_id } };
22066
+ }
22067
+ }
22068
+ ];
22069
+
21736
22070
  // src/tools/products.ts
21737
22071
  var PRODUCT_STATUS = [
21738
22072
  "draft",
@@ -21776,12 +22110,77 @@ var productTools = [
21776
22110
  return client2.getProduct(input.id_or_handle);
21777
22111
  }
21778
22112
  },
22113
+ {
22114
+ name: "list_sales_channels",
22115
+ description: "\u5217\u51FA\u9500\u552E\u6E20\u9053\uFF08GET /admin/sales-channels\uFF09\u3002\u65B0\u589E\u5546\u54C1\u524D\u7528\u5B83\u786E\u8BA4\u6E20\u9053 id/\u540D\u79F0\u3002",
22116
+ schema: {
22117
+ q: external_exports.string().optional(),
22118
+ limit: external_exports.number().int().positive().max(100).default(50)
22119
+ },
22120
+ async run(input, { client: client2 }) {
22121
+ return client2.listSalesChannels({ q: input.q, limit: input.limit });
22122
+ }
22123
+ },
22124
+ {
22125
+ name: "list_collections",
22126
+ description: "\u5217\u51FA\u96C6\u5408\uFF08GET /admin/collections\uFF09\uFF0C\u7528\u4E8E\u628A\u5546\u54C1\u6302\u8FDB\u96C6\u5408\uFF08\u5982\u6D3B\u52A8\u96C6\u5408\uFF09\u3002",
22127
+ schema: {
22128
+ q: external_exports.string().optional(),
22129
+ limit: external_exports.number().int().positive().max(100).default(50)
22130
+ },
22131
+ async run(input, { client: client2 }) {
22132
+ return client2.listCollections({ q: input.q, limit: input.limit });
22133
+ }
22134
+ },
22135
+ {
22136
+ name: "get_translations",
22137
+ description: `\u8BFB\u53D6\u67D0\u5B9E\u4F53\u7684\u591A\u8BED\u8A00\u7FFB\u8BD1\uFF08GET /admin/translations\uFF09\u2014\u2014\u6838\u5BF9\u5546\u54C1\u56DB\u8BED\u8A00\u662F\u5426\u5199\u5165\u6210\u529F\u7684\u552F\u4E00\u53EF\u9760\u9014\u5F84
22138
+ \uFF08get_product \u9ED8\u8BA4\u5B57\u6BB5\u4E0D\u542B translations\uFF09\u3002reference \u4E00\u822C\u4E3A product\uFF0Creference_id \u4F20\u5546\u54C1 id \u6216 handle\u3002
22139
+ \u8FD4\u56DE\u6BCF\u6761 { locale_code, title, description }\u3002`,
22140
+ schema: {
22141
+ reference: external_exports.string().default("product"),
22142
+ reference_id: external_exports.string().describe("\u5546\u54C1 id \u6216 handle"),
22143
+ locale_code: external_exports.string().optional().describe("\u53EA\u53D6\u67D0\u4E00\u8BED\u8A00\uFF0C\u5982 zh"),
22144
+ limit: external_exports.number().int().positive().max(100).default(50)
22145
+ },
22146
+ async run(input, { client: client2 }) {
22147
+ let refId = input.reference_id;
22148
+ if (!/^[a-z]+_[a-z0-9]+$/i.test(refId)) {
22149
+ const prod = await client2.getProduct(refId).catch(() => null);
22150
+ const id = prod?.product?.id || prod?.id;
22151
+ if (!id) return `\u9519\u8BEF\uFF1A\u627E\u4E0D\u5230 ${input.reference} "${refId}"\uFF08\u8BF7\u4F20 id \u6216\u6709\u6548 handle\uFF09`;
22152
+ refId = id;
22153
+ }
22154
+ const params = {
22155
+ reference: input.reference,
22156
+ reference_id: refId,
22157
+ limit: input.limit
22158
+ };
22159
+ if (input.locale_code) params.locale_code = input.locale_code;
22160
+ const data = await client2.listTranslations(params);
22161
+ const rows = (data?.translations || []).map((t) => {
22162
+ const tr = t.translations || {};
22163
+ return {
22164
+ locale_code: t.locale_code,
22165
+ title: tr.title ?? null,
22166
+ description: tr.description ? tr.description.slice(0, 120) + "\u2026" : null,
22167
+ has_full_description: Boolean(tr.description)
22168
+ };
22169
+ });
22170
+ return { reference: input.reference, reference_id: input.reference_id, translations: rows, count: rows.length };
22171
+ }
22172
+ },
21779
22173
  {
21780
22174
  name: "create_product",
21781
22175
  description: `\u5728\u540E\u53F0\u589E\u52A0\u4E00\u4E2A\u5546\u54C1\u3002
21782
22176
  \u89C4\u5219\uFF08\xA716.1\uFF09\uFF1AHandle \u53EA\u80FD\u542B\u5C0F\u5199\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u8FDE\u5B57\u7B26(-)\uFF0C\u5DE5\u5177\u4F1A\u81EA\u52A8\u5F52\u4E00\u5316\uFF0C\u975E\u6CD5\u5219\u62A5\u9519\u3002
21783
22177
  \u591A\u8BED\u8A00\uFF08\xA716.2\uFF09\uFF1Atranslations \u5FC5\u987B\u5305\u542B en/zh/ms/id \u56DB\u79CD\u8BED\u8A00\u7684 title\uFF1B\u7F3A\u5931\u65F6\u9700 confirm=true \u4E14 acknowledge_missing_languages=true\u3002
21784
- \u9ED8\u8BA4 dry_run\uFF08confirm=false\uFF09\u53EA\u8FD4\u56DE\u9884\u89C8\uFF0C\u4E0D\u771F\u6B63\u521B\u5EFA\u3002`,
22178
+ \u9ED8\u8BA4 dry_run\uFF08confirm=false\uFF09\u53EA\u8FD4\u56DE\u9884\u89C8\uFF0C\u4E0D\u771F\u6B63\u521B\u5EFA\u3002
22179
+ \u56FE\u7247\uFF1Aimages \u53EF\u4F20\u672C\u5730\u6587\u4EF6\u8DEF\u5F84\uFF08\u81EA\u52A8\u4E0A\u4F20\u5230\u6587\u4EF6\u670D\u52A1\uFF09\u6216\u56FE\u7247 URL\uFF0C\u4EC5\u5728 confirm=true \u65F6\u624D\u771F\u6B63\u4E0A\u4F20/\u6302\u63A5\u3002
22180
+ SKU / \u89C4\u683C\uFF1Asku \u5199\u5165\u9ED8\u8BA4\u53D8\u4F53\uFF1Bmetadata \u53EF\u5B58\u4EFB\u610F\u952E\u503C\uFF08\u5982\u89C4\u683C\u53C2\u6570\u3001\u6765\u6E90\u94FE\u63A5\uFF09\u3002
22181
+ \u6E20\u9053\uFF1Asales_channels \u4F20\u6E20\u9053 id \u6216\u540D\u79F0\uFF08\u5982 "MY Storefront"\uFF09\uFF0C\u521B\u5EFA\u5373\u6302\u8F7D\uFF0C\u5426\u5219\u524D\u7AEF\u4E0D\u53EF\u89C1\u3002
22182
+ \u5E93\u5B58\uFF1Amanage_inventory \u9ED8\u8BA4 true\uFF1B\u6B63\u5F0F\u4E0A\u67B6\u5FC5\u987B\u7ED9 initial_stock\uFF08\u5982 {quantity:100}\uFF0C\u9ED8\u8BA4\u4E3B\u4ED3\uFF09\uFF0C\u5426\u5219 0 \u5E93\u5B58=\u524D\u7AEF\u7F3A\u8D27\u3002
22183
+ \u7269\u7406/\u6D77\u5173\u5B57\u6BB5\uFF1Aweight(\u514B)/length/height/width(\u5398\u7C73)/origin_country/hs_code/mid_code/material \u5747\u53EF\u9009\u3002`,
21785
22184
  schema: {
21786
22185
  title: external_exports.string(),
21787
22186
  handle: external_exports.string().optional().describe("\u4E0D\u586B\u5219\u7528 title \u5F52\u4E00\u5316\u751F\u6210"),
@@ -21792,9 +22191,28 @@ var productTools = [
21792
22191
  // 并非最小单位(分)。官方 ESLint 规则 prices-in-major-units 明确:
21793
22192
  // "Medusa stores prices in major units",期望 19.99 而非 1999。
21794
22193
  // 本项目 seed 与 storefront 均遵循此官方约定,故直接透传,不要 ×100。
21795
- price: external_exports.number().nonnegative().optional().describe("\u4E3B\u8D27\u5E01\u5355\u4F4D\u91D1\u989D\uFF0C\u5982 200.51 \u5373 RM200.51\uFF1BMedusa \u4EE5\u4E3B\u5355\u4F4D\u5B58\u50A8\uFF0C\u5207\u52FF\u6362\u7B97"),
22194
+ price: external_exports.number().nonnegative().optional().describe("\u4E3B\u8D27\u5E01\u5355\u4F4D\u91D1\u989D\uFF0C\u5982 200.51 \u5373 RM200.51\uFF1BMedusa \u4EE5\u4E3B\u5355\u4F4D\u5B58\u50A8\uFF0C\u5207\u52FF\u6362\u7B97\u3002\u4E0E prices \u4E8C\u9009\u4E00\uFF0Cprices \u4F18\u5148"),
21796
22195
  currency_code: external_exports.string().default("MYR"),
22196
+ prices: external_exports.array(external_exports.object({ currency_code: external_exports.string(), amount: external_exports.number().nonnegative() })).optional().describe('\u591A\u5E01\u79CD\u4EF7\u683C\uFF08\u4E3B\u8D27\u5E01\u5355\u4F4D\uFF09\uFF0C\u5982 [{currency_code:"myr",amount:420},{currency_code:"sgd",amount:127}]\u3002\u4E1C\u5357\u4E9A\u5E97\u901A\u5E38\u9700\u8986\u76D6 myr/sgd/idr/thb/php/cny\u3002\u4F18\u5148\u4E8E\u5355\u4E00 price'),
21797
22197
  translations: external_exports.record(external_exports.string(), translationShape).optional(),
22198
+ images: external_exports.array(external_exports.string()).optional().describe("\u672C\u5730\u56FE\u7247\u8DEF\u5F84\u6216\u56FE\u7247 URL\uFF0Cconfirm=true \u65F6\u4E0A\u4F20\u5E76\u6302\u5230\u5546\u54C1"),
22199
+ sku: external_exports.string().optional().describe("\u5199\u5165\u9ED8\u8BA4\u53D8\u4F53\u7684 SKU"),
22200
+ metadata: external_exports.record(external_exports.string(), external_exports.string()).optional().describe("\u4EFB\u610F\u952E\u503C\uFF0C\u5982\u89C4\u683C\u53C2\u6570 / source_url"),
22201
+ sales_channels: external_exports.array(external_exports.string()).optional().describe('\u9500\u552E\u6E20\u9053 id \u6216\u540D\u79F0\uFF08\u5982 "MY Storefront"\uFF09\uFF0C\u521B\u5EFA\u5373\u6302\u8F7D\uFF0C\u5426\u5219\u524D\u7AEF\u4E0D\u53EF\u89C1'),
22202
+ collection_id: external_exports.string().optional().describe('\u96C6\u5408 id \u6216\u540D\u79F0\uFF08\u5982 "\u53CC11\u6D3B\u52A8"\uFF09'),
22203
+ weight: external_exports.number().nonnegative().optional().describe("\u91CD\u91CF\uFF0C\u5355\u4F4D\u514B"),
22204
+ length: external_exports.number().nonnegative().optional().describe("\u957F\uFF0C\u5355\u4F4D\u5398\u7C73"),
22205
+ height: external_exports.number().nonnegative().optional().describe("\u9AD8\uFF0C\u5355\u4F4D\u5398\u7C73"),
22206
+ width: external_exports.number().nonnegative().optional().describe("\u5BBD\uFF0C\u5355\u4F4D\u5398\u7C73"),
22207
+ origin_country: external_exports.string().optional().describe("\u539F\u4EA7\u56FD ISO \u7801\uFF0C\u5982 KR/MY"),
22208
+ hs_code: external_exports.string().optional(),
22209
+ mid_code: external_exports.string().optional(),
22210
+ material: external_exports.string().optional(),
22211
+ manage_inventory: external_exports.boolean().default(true).describe("\u53D8\u4F53\u662F\u5426\u542F\u7528\u5E93\u5B58\u7BA1\u7406\uFF0C\u9ED8\u8BA4 true"),
22212
+ initial_stock: external_exports.object({
22213
+ location_id: external_exports.string().optional().describe("\u4ED3\u5E93 id\uFF1B\u7701\u7565\u5219 Main Warehouse/\u7B2C\u4E00\u4E2A\u4ED3\u5E93"),
22214
+ quantity: external_exports.number().int().nonnegative()
22215
+ }).optional().describe("\u5EFA\u5E93\u5B58\uFF1A\u6309\u53D8\u4F53 SKU \u81EA\u52A8\u521B\u5EFA\u5E93\u5B58\u6761\u76EE\u5E76\u5199\u5165\u8BE5\u4ED3\u5E93\u6570\u91CF"),
21798
22216
  confirm: external_exports.boolean().default(false),
21799
22217
  acknowledge_missing_languages: external_exports.boolean().default(false)
21800
22218
  },
@@ -21808,6 +22226,29 @@ var productTools = [
21808
22226
  const missing = config3.requiredLocales.filter(
21809
22227
  (l) => !provided.includes(l) || !input.translations?.[l]?.title
21810
22228
  );
22229
+ let channelIds = [];
22230
+ if (input.sales_channels?.length) {
22231
+ const ch = await client2.listSalesChannels({ limit: 100 });
22232
+ const list = ch?.sales_channels || [];
22233
+ channelIds = input.sales_channels.map((name) => list.find((c) => c.id === name || c.name === name)?.id).filter(Boolean);
22234
+ const unknown2 = input.sales_channels.filter(
22235
+ (name) => !list.some((c) => c.id === name || c.name === name)
22236
+ );
22237
+ if (unknown2.length) return `\u9519\u8BEF\uFF1A\u627E\u4E0D\u5230\u9500\u552E\u6E20\u9053\uFF1A${unknown2.join(", ")}\uFF08\u7528 list_sales_channels \u67E5\u770B\u53EF\u7528\u6E20\u9053\uFF09`;
22238
+ }
22239
+ let collectionId = input.collection_id;
22240
+ if (collectionId && !collectionId.startsWith("pcol_")) {
22241
+ const cols = await client2.listCollections({ limit: 100 });
22242
+ const hit = (cols?.collections || []).find((c) => c.id === collectionId || c.title === collectionId);
22243
+ if (!hit) return `\u9519\u8BEF\uFF1A\u627E\u4E0D\u5230\u96C6\u5408 "${collectionId}"\uFF08\u7528 list_collections \u67E5\u770B\u53EF\u7528\u96C6\u5408\uFF09`;
22244
+ collectionId = hit.id;
22245
+ }
22246
+ const warnings = [];
22247
+ if (input.manage_inventory !== false && !input.initial_stock) {
22248
+ warnings.push(
22249
+ "manage_inventory=true \u4F46\u672A\u7ED9 initial_stock\uFF1A\u521B\u5EFA\u540E\u4E3A 0 \u5E93\u5B58\uFF0C\u524D\u7AEF\u5C06\u663E\u793A\u7F3A\u8D27\u3002\u6B63\u5F0F\u4E0A\u67B6\u524D\u5FC5\u987B\u8C03\u7528 set_product_stock \u5EFA\u4ED3\u3002"
22250
+ );
22251
+ }
21811
22252
  if (!input.confirm) {
21812
22253
  return {
21813
22254
  dry_run: true,
@@ -21817,38 +22258,102 @@ var productTools = [
21817
22258
  status: input.status,
21818
22259
  price: input.price,
21819
22260
  currency_code: input.currency_code,
22261
+ prices: input.prices,
22262
+ sku: input.sku,
22263
+ images: input.images,
22264
+ metadata: input.metadata,
22265
+ sales_channels: channelIds,
22266
+ collection_id: collectionId || null,
22267
+ weight: input.weight,
22268
+ manage_inventory: input.manage_inventory,
22269
+ initial_stock: input.initial_stock,
21820
22270
  missing_translations: missing
21821
22271
  },
22272
+ warnings,
21822
22273
  note: "\u672A confirm\uFF0C\u4EC5\u9884\u89C8\u3002\u786E\u8BA4\u8BF7\u8BBE confirm=true\u3002"
21823
22274
  };
21824
22275
  }
21825
22276
  if (missing.length > 0 && !input.acknowledge_missing_languages) {
21826
22277
  return `\u9519\u8BEF\uFF1A\u7F3A\u5C11\u4EE5\u4E0B\u8BED\u8A00\u7684 title\uFF1A${missing.join(", ")}\u3002\u5982\u786E\u8BA4\u4ECD\u8981\u521B\u5EFA\uFF0C\u8BF7\u8BBE acknowledge_missing_languages=true\u3002`;
21827
22278
  }
22279
+ let imageUrls = [];
22280
+ if (input.images?.length) {
22281
+ try {
22282
+ imageUrls = await client2.uploadProductAssets(input.images);
22283
+ } catch (e) {
22284
+ return `\u9519\u8BEF\uFF1A\u56FE\u7247\u4E0A\u4F20\u5931\u8D25 \u2014\u2014 ${e?.message || e}`;
22285
+ }
22286
+ }
21828
22287
  const payload = {
21829
22288
  title: input.title,
21830
22289
  handle,
21831
22290
  status: input.status,
21832
22291
  subtitle: input.subtitle,
21833
- description: input.description
22292
+ description: input.description,
22293
+ options: [{ title: "Default", values: ["Default"] }]
21834
22294
  };
21835
- if (input.translations) payload.translations = input.translations;
21836
- if (input.price != null) {
21837
- payload.variants = [
21838
- {
21839
- title: "Default",
21840
- prices: [{ amount: input.price, currency_code: input.currency_code }],
21841
- manage_inventory: false
21842
- }
21843
- ];
22295
+ if (input.metadata) payload.metadata = input.metadata;
22296
+ if (imageUrls.length) payload.images = imageUrls.map((url) => ({ url }));
22297
+ if (channelIds.length) payload.sales_channels = channelIds.map((id) => ({ id }));
22298
+ if (collectionId) payload.collection_id = collectionId;
22299
+ for (const k of ["weight", "length", "height", "width"]) {
22300
+ if (input[k] != null) payload[k] = input[k];
22301
+ }
22302
+ for (const k of ["origin_country", "hs_code", "mid_code", "material"]) {
22303
+ if (input[k]) payload[k] = input[k];
22304
+ }
22305
+ const variantPrices = input.prices && input.prices.length ? input.prices.map((p) => ({ amount: p.amount, currency_code: p.currency_code })) : input.price != null ? [{ amount: input.price, currency_code: input.currency_code }] : void 0;
22306
+ if (variantPrices) {
22307
+ const variant = {
22308
+ title: "Default",
22309
+ options: { Default: "Default" },
22310
+ prices: variantPrices,
22311
+ manage_inventory: input.manage_inventory
22312
+ };
22313
+ if (input.sku) variant.sku = input.sku;
22314
+ payload.variants = [variant];
21844
22315
  }
21845
22316
  const data = await client2.createProduct(payload);
21846
- return { created: data, warning_missing_languages: missing };
22317
+ const productId = data?.product?.id || data?.id;
22318
+ let translationResult = null;
22319
+ if (input.translations && Object.keys(input.translations).length) {
22320
+ const create = Object.entries(input.translations).map(([locale, t]) => ({
22321
+ reference: "product",
22322
+ reference_id: productId,
22323
+ locale_code: locale,
22324
+ translations: { title: t?.title, description: t?.description }
22325
+ }));
22326
+ try {
22327
+ translationResult = await client2.batchTranslations(create);
22328
+ } catch (e) {
22329
+ return { created: data, warning_translations: `\u7FFB\u8BD1\u5199\u5165\u5931\u8D25 \u2014\u2014 ${e?.message || e}` };
22330
+ }
22331
+ }
22332
+ let stock = null;
22333
+ if (input.initial_stock && productId) {
22334
+ try {
22335
+ stock = await applyProductStock(
22336
+ { client: client2, config: config3 },
22337
+ {
22338
+ productId,
22339
+ sku: input.sku,
22340
+ locationId: input.initial_stock.location_id,
22341
+ quantity: input.initial_stock.quantity,
22342
+ manageInventory: input.manage_inventory
22343
+ }
22344
+ );
22345
+ } catch (e) {
22346
+ warnings.push(`\u5EFA\u5E93\u5B58\u5931\u8D25\uFF08\u5546\u54C1\u5DF2\u521B\u5EFA\uFF0C\u8BF7\u8865\u8C03 set_product_stock\uFF09\u2014\u2014 ${e?.message || e}`);
22347
+ }
22348
+ }
22349
+ return { created: data, translations: translationResult, stock, warnings, warning_missing_languages: missing };
21847
22350
  }
21848
22351
  },
21849
22352
  {
21850
22353
  name: "update_product",
21851
- description: "\u4FEE\u6539\u5546\u54C1\u5B57\u6BB5\uFF08\u6807\u9898/handle/\u72B6\u6001/\u63CF\u8FF0/\u591A\u8BED\u8A00\uFF09\u3002Handle \u81EA\u52A8\u5F52\u4E00\u5316\u3002\u9ED8\u8BA4 dry_run\u3002",
22354
+ description: `\u4FEE\u6539\u5546\u54C1\u5B57\u6BB5\uFF08\u6807\u9898/handle/\u72B6\u6001/\u63CF\u8FF0/\u591A\u8BED\u8A00/\u7269\u7406\u5C3A\u5BF8/\u6D77\u5173/\u96C6\u5408/metadata\uFF09\u3002Handle \u81EA\u52A8\u5F52\u4E00\u5316\u3002
22355
+ weight \u5355\u4F4D\u514B\uFF1Blength/height/width \u5355\u4F4D\u5398\u7C73\uFF1Borigin_country \u7528 ISO \u7801\uFF08\u5982 KR\uFF09\u3002
22356
+ metadata \u4E3A**\u5408\u5E76**\u8BED\u4E49\uFF1A\u65B0\u952E\u8986\u76D6\u540C\u540D\u952E\u3001\u672A\u63D0\u53CA\u7684\u65E7\u952E\u4FDD\u7559\uFF08\u4E0D\u652F\u6301\u5220\u9664\u952E\uFF09\u3002\u9ED8\u8BA4 dry_run\u3002`,
21852
22357
  schema: {
21853
22358
  id: external_exports.string(),
21854
22359
  title: external_exports.string().optional(),
@@ -21857,6 +22362,16 @@ var productTools = [
21857
22362
  subtitle: external_exports.string().optional(),
21858
22363
  description: external_exports.string().optional(),
21859
22364
  translations: external_exports.record(external_exports.string(), translationShape).optional(),
22365
+ collection_id: external_exports.string().optional().describe("\u96C6\u5408 id \u6216\u540D\u79F0"),
22366
+ weight: external_exports.number().nonnegative().optional().describe("\u91CD\u91CF\uFF0C\u5355\u4F4D\u514B"),
22367
+ length: external_exports.number().nonnegative().optional().describe("\u957F\uFF0C\u5355\u4F4D\u5398\u7C73"),
22368
+ height: external_exports.number().nonnegative().optional().describe("\u9AD8\uFF0C\u5355\u4F4D\u5398\u7C73"),
22369
+ width: external_exports.number().nonnegative().optional().describe("\u5BBD\uFF0C\u5355\u4F4D\u5398\u7C73"),
22370
+ origin_country: external_exports.string().optional(),
22371
+ hs_code: external_exports.string().optional(),
22372
+ mid_code: external_exports.string().optional(),
22373
+ material: external_exports.string().optional(),
22374
+ metadata: external_exports.record(external_exports.string(), external_exports.string()).optional().describe("\u5408\u5E76\u8FDB\u73B0\u6709 metadata\uFF08\u4E0D\u6574\u4F53\u8986\u76D6\uFF09"),
21860
22375
  confirm: external_exports.boolean().default(false)
21861
22376
  },
21862
22377
  async run(input, { client: client2 }) {
@@ -21870,9 +22385,79 @@ var productTools = [
21870
22385
  if (input.status) patch.status = input.status;
21871
22386
  if (input.subtitle) patch.subtitle = input.subtitle;
21872
22387
  if (input.description) patch.description = input.description;
21873
- if (input.translations) patch.translations = input.translations;
22388
+ if (input.collection_id) {
22389
+ let cid = input.collection_id;
22390
+ if (!cid.startsWith("pcol_")) {
22391
+ const cols = await client2.listCollections({ limit: 100 });
22392
+ const hit = (cols?.collections || []).find((c) => c.id === cid || c.title === cid);
22393
+ if (!hit) return `\u9519\u8BEF\uFF1A\u627E\u4E0D\u5230\u96C6\u5408 "${cid}"\uFF08\u7528 list_collections \u67E5\u770B\u53EF\u7528\u96C6\u5408\uFF09`;
22394
+ cid = hit.id;
22395
+ }
22396
+ patch.collection_id = cid;
22397
+ }
22398
+ for (const k of ["weight", "length", "height", "width"]) {
22399
+ if (input[k] != null) patch[k] = input[k];
22400
+ }
22401
+ for (const k of ["origin_country", "hs_code", "mid_code", "material"]) {
22402
+ if (input[k]) patch[k] = input[k];
22403
+ }
22404
+ if (input.metadata && Object.keys(input.metadata).length) {
22405
+ const cur = await client2.getProduct(input.id).catch(() => null);
22406
+ const existingMeta = (cur?.product ?? cur)?.metadata || {};
22407
+ patch.metadata = { ...existingMeta, ...input.metadata };
22408
+ }
21874
22409
  if (!input.confirm) return { dry_run: true, will_update: patch };
21875
- return client2.updateProduct(input.id, patch);
22410
+ const data = await client2.updateProduct(input.id, patch);
22411
+ let translationResult = null;
22412
+ if (input.translations && Object.keys(input.translations).length) {
22413
+ const create = Object.entries(input.translations).map(([locale, t]) => ({
22414
+ reference: "product",
22415
+ reference_id: input.id,
22416
+ locale_code: locale,
22417
+ translations: { title: t?.title, description: t?.description }
22418
+ }));
22419
+ try {
22420
+ translationResult = await client2.batchTranslations(create);
22421
+ } catch (e) {
22422
+ return { updated: data, warning_translations: `\u7FFB\u8BD1\u5199\u5165\u5931\u8D25 \u2014\u2014 ${e?.message || e}` };
22423
+ }
22424
+ }
22425
+ return { updated: data, translations: translationResult };
22426
+ }
22427
+ },
22428
+ {
22429
+ name: "update_product_prices",
22430
+ description: `\u8BBE\u7F6E/\u66FF\u6362\u67D0\u5546\u54C1\u7684\uFF08\u53D8\u4F53\uFF09\u4EF7\u683C\uFF0C\u652F\u6301\u591A\u5E01\u79CD\u3002Medusa \u4EF7\u683C\u6302\u5728\u300C\u53D8\u4F53\u300D\u4E0A\uFF0C\u5199\u5165\u5373\u6574\u4F53\u66FF\u6362\u8BE5\u53D8\u4F53\u7684\u5168\u90E8\u4EF7\u683C\uFF08\u4E0D\u4F1A\u7D2F\u52A0\u65E7\u4EF7\uFF09\u3002
22431
+ \u4EF7\u683C\u4EE5\u4E3B\u8D27\u5E01\u5355\u4F4D\uFF08\u5143\uFF09\u5B58\u50A8\uFF0C\u5207\u52FF \xD7100\u3002\u4E1C\u5357\u4E9A\u8DE8\u5883\u5E97\u901A\u5E38\u9700\u8986\u76D6 myr/sgd/idr/thb/php/cny\u3002
22432
+ \u4E0D\u6307\u5B9A variant_id \u65F6\u4F5C\u7528\u4E8E\u7B2C\u4E00\u4E2A\u53D8\u4F53\uFF08\u5355 SKU \u5546\u54C1\u5373\u552F\u4E00\u53D8\u4F53\uFF09\u3002\u9ED8\u8BA4 dry_run\uFF08confirm=false\uFF09\u4EC5\u9884\u89C8\u3002`,
22433
+ schema: {
22434
+ product_id: external_exports.string(),
22435
+ prices: external_exports.array(external_exports.object({ currency_code: external_exports.string(), amount: external_exports.number().nonnegative() })).describe('\u591A\u5E01\u79CD\u4EF7\u683C\u6570\u7EC4\uFF08\u4E3B\u8D27\u5E01\u5355\u4F4D\uFF09\uFF0C\u5982 [{currency_code:"myr",amount:420},{currency_code:"sgd",amount:127}]'),
22436
+ variant_id: external_exports.string().optional().describe("\u76EE\u6807\u53D8\u4F53 id\uFF1B\u7701\u7565\u5219\u53D6\u7B2C\u4E00\u4E2A\u53D8\u4F53"),
22437
+ confirm: external_exports.boolean().default(false)
22438
+ },
22439
+ async run(input, { client: client2 }) {
22440
+ if (!input.confirm) {
22441
+ return {
22442
+ dry_run: true,
22443
+ product_id: input.product_id,
22444
+ will_set_prices: input.prices,
22445
+ variant_id: input.variant_id || "(\u7B2C\u4E00\u4E2A\u53D8\u4F53)"
22446
+ };
22447
+ }
22448
+ const prod = await client2.getProduct(input.product_id).catch(() => null);
22449
+ const product = prod?.product ?? prod;
22450
+ if (!product) return `\u9519\u8BEF\uFF1A\u627E\u4E0D\u5230\u5546\u54C1 ${input.product_id}`;
22451
+ const variants = product.variants || [];
22452
+ if (!variants.length) return `\u9519\u8BEF\uFF1A\u5546\u54C1\u6CA1\u6709\u53D8\u4F53\uFF0C\u65E0\u6CD5\u8BBE\u7F6E\u4EF7\u683C\u3002`;
22453
+ const target = input.variant_id ? variants.find((v) => v.id === input.variant_id) : variants[0];
22454
+ if (!target) return `\u9519\u8BEF\uFF1A\u627E\u4E0D\u5230\u76EE\u6807\u53D8\u4F53\uFF08\u6307\u5B9A ${input.variant_id} \u6216\u7B2C\u4E00\u4E2A\u53D8\u4F53\uFF09\u3002`;
22455
+ const data = await client2.updateProductPrices(
22456
+ input.product_id,
22457
+ target.id,
22458
+ input.prices.map((p) => ({ amount: p.amount, currency_code: p.currency_code }))
22459
+ );
22460
+ return { updated: data };
21876
22461
  }
21877
22462
  },
21878
22463
  {
@@ -21886,76 +22471,131 @@ var productTools = [
21886
22471
  if (!input.confirm) return { dry_run: true, will_set_status: "archived" };
21887
22472
  return client2.updateProduct(input.id, { status: "archived" });
21888
22473
  }
21889
- }
21890
- ];
21891
-
21892
- // src/tools/stock.ts
21893
- var stockTools = [
22474
+ },
21894
22475
  {
21895
- name: "list_stock_locations",
21896
- description: "\u5217\u51FA\u4ED3\u5E93\uFF08Medusa StockLocation\uFF09\u3002\u53EF\u9009\u5173\u952E\u8BCD q \u8FC7\u6EE4\u540D\u79F0\u3002",
22476
+ name: "upload_product_image",
22477
+ description: `\u7ED9\u5DF2\u5B58\u5728\u7684\u5546\u54C1\u8FFD\u52A0\u56FE\u7247\u3002\u652F\u6301\u672C\u5730\u6587\u4EF6\u8DEF\u5F84\uFF08\u81EA\u52A8\u4E0A\u4F20\u5230\u6587\u4EF6\u670D\u52A1\uFF09\u6216\u56FE\u7247 URL\u3002
22478
+ \u9ED8\u8BA4 dry_run\uFF08confirm=false\uFF09\u4EC5\u9884\u89C8\uFF1Bconfirm=true \u624D\u4E0A\u4F20\u5E76\u6302\u5230\u5546\u54C1\uFF08\u4E0E\u73B0\u6709\u56FE\u7247\u5408\u5E76\uFF0C\u4E0D\u4F1A\u8986\u76D6\u5DF2\u6709\u56FE\u7247\uFF09\u3002`,
21897
22479
  schema: {
21898
- q: external_exports.string().optional(),
21899
- limit: external_exports.number().int().positive().max(100).default(50)
22480
+ product_id: external_exports.string(),
22481
+ images: external_exports.array(external_exports.string()).describe("\u672C\u5730\u56FE\u7247\u8DEF\u5F84\u6216\u56FE\u7247 URL"),
22482
+ confirm: external_exports.boolean().default(false)
21900
22483
  },
21901
22484
  async run(input, { client: client2 }) {
21902
- return client2.listStockLocations({ q: input.q, limit: input.limit });
22485
+ if (!input.confirm) {
22486
+ return { dry_run: true, product_id: input.product_id, will_upload: input.images };
22487
+ }
22488
+ let urls = [];
22489
+ try {
22490
+ urls = await client2.uploadProductAssets(input.images);
22491
+ } catch (e) {
22492
+ return `\u9519\u8BEF\uFF1A\u56FE\u7247\u4E0A\u4F20\u5931\u8D25 \u2014\u2014 ${e?.message || e}`;
22493
+ }
22494
+ const existing = await client2.getProduct(input.product_id).catch(() => null);
22495
+ const prod = existing?.product ?? existing ?? {};
22496
+ const existingUrls = (prod.images || []).map((i) => i.url).filter(Boolean);
22497
+ const merged = Array.from(/* @__PURE__ */ new Set([...existingUrls, ...urls]));
22498
+ const data = await client2.updateProduct(input.product_id, {
22499
+ images: merged.map((u) => ({ url: u }))
22500
+ });
22501
+ return { updated: data };
21903
22502
  }
21904
22503
  },
21905
22504
  {
21906
- name: "create_stock_location",
21907
- description: "\u589E\u52A0\u4E00\u4E2A\u4ED3\u5E93\uFF08StockLocation\uFF09\u3002\u9ED8\u8BA4 dry_run\u3002",
22505
+ name: "add_product_to_sales_channel",
22506
+ description: `\u628A\u5546\u54C1\u52A0\u5165\u9500\u552E\u6E20\u9053\uFF0C\u4F7F\u5176\u5728\u524D\u7AEF\u5546\u57CE\uFF08\u6309 publishable key \u2192 \u6E20\u9053\u8FC7\u6EE4\uFF09\u53EF\u89C1\u3002
22507
+ \u26A0\uFE0F Medusa v2 \u7AEF\u70B9\u6302\u5728\u6E20\u9053\u4E0B\uFF1APOST /admin/sales-channels/{id}/products\uFF0C\u800C\u975E\u5546\u54C1\u4E0B\u3002
22508
+ \u4E0D\u6307\u5B9A channel_id \u65F6\u9ED8\u8BA4\u52A0\u5165\u7B2C\u4E00\u4E2A\u542F\u7528\u7684\u6E20\u9053\u3002\u9ED8\u8BA4 dry_run\uFF08confirm=false\uFF09\u4EC5\u9884\u89C8\u3002`,
21908
22509
  schema: {
21909
- name: external_exports.string(),
21910
- address: external_exports.object({
21911
- address_1: external_exports.string().optional(),
21912
- address_2: external_exports.string().optional(),
21913
- city: external_exports.string().optional(),
21914
- country_code: external_exports.string().optional(),
21915
- province: external_exports.string().optional(),
21916
- postal_code: external_exports.string().optional(),
21917
- phone: external_exports.string().optional()
21918
- }).optional(),
21919
- metadata: external_exports.record(external_exports.string(), external_exports.string()).optional(),
22510
+ product_ids: external_exports.array(external_exports.string()).describe("\u8981\u52A0\u5165\u6E20\u9053\u7684\u5546\u54C1 id \u5217\u8868"),
22511
+ channel_id: external_exports.string().optional().describe("\u9500\u552E\u6E20\u9053 id\uFF1B\u7701\u7565\u5219\u53D6\u7B2C\u4E00\u4E2A\u542F\u7528\u7684\u6E20\u9053"),
21920
22512
  confirm: external_exports.boolean().default(false)
21921
22513
  },
21922
- async run(input, { client: client2 }) {
21923
- const payload = { name: input.name };
21924
- if (input.address) payload.address = input.address;
21925
- if (input.metadata) payload.metadata = input.metadata;
21926
- if (!input.confirm) return { dry_run: true, will_create: payload };
21927
- return client2.createStockLocation(payload);
22514
+ async run(input, { client: client2, config: config3 }) {
22515
+ if (!input.confirm) {
22516
+ return { dry_run: true, will_add: input.product_ids, channel_id: input.channel_id || "(\u9ED8\u8BA4\u7B2C\u4E00\u4E2A\u542F\u7528\u6E20\u9053)" };
22517
+ }
22518
+ let channelId = input.channel_id;
22519
+ if (!channelId) {
22520
+ const ch = await client2.listSalesChannels?.();
22521
+ const list = ch?.sales_channels || [];
22522
+ const firstEnabled = list.find((c) => !c.is_disabled) || list[0];
22523
+ if (!firstEnabled) return "\u9519\u8BEF\uFF1A\u627E\u4E0D\u5230\u53EF\u7528\u7684\u9500\u552E\u6E20\u9053\u3002";
22524
+ channelId = firstEnabled.id;
22525
+ }
22526
+ const data = await client2.addProductsToSalesChannel(channelId, input.product_ids);
22527
+ return { added_to_channel: channelId, result: data };
21928
22528
  }
21929
22529
  },
21930
22530
  {
21931
- name: "get_inventory_levels",
21932
- description: "\u67E5\u8BE2\u67D0\u4ED3\u5E93\u4E0B\u7684\u5E93\u5B58\u6761\u76EE\u53CA\u5E93\u5B58\u6C34\u5E73\uFF08\u6309 location_id\uFF09\u3002",
22531
+ name: "get_product_content",
22532
+ description: `\u8BFB\u53D6\u5546\u54C1\u8BE6\u60C5\u9875\u5BCC\u5185\u5BB9\u5757\uFF08GET /admin/product-contents\uFF09\u3002
22533
+ locale \u7528\u533A\u57DF\u7801\uFF1Aen-US / zh-CN / ms-MY / id-ID\uFF08\u4E0E\u524D\u7AEF useProductContent \u7684 localeMap \u4E00\u81F4\uFF09\u3002
22534
+ \u8FD4\u56DE { product_content: { id, product_id, locale, status, blocks(\u5DF2\u89E3\u6790\u6570\u7EC4) } | null }\u3002`,
21933
22535
  schema: {
21934
- location_id: external_exports.string(),
21935
- q: external_exports.string().optional(),
21936
- limit: external_exports.number().int().positive().max(100).default(50)
22536
+ product_id: external_exports.string(),
22537
+ locale: external_exports.enum(["en-US", "zh-CN", "ms-MY", "id-ID"]).default("en-US"),
22538
+ status: external_exports.enum(["draft", "published"]).default("published")
21937
22539
  },
21938
22540
  async run(input, { client: client2 }) {
21939
- return client2.listInventoryItems(input.location_id, { q: input.q, limit: input.limit });
22541
+ const data = await client2.getProductContent(input.product_id, input.locale, input.status);
22542
+ const pc = data?.product_content;
22543
+ if (!pc) return { product_content: null };
22544
+ let blocks = [];
22545
+ try {
22546
+ blocks = pc.blocks ? JSON.parse(pc.blocks) : [];
22547
+ } catch {
22548
+ blocks = [];
22549
+ }
22550
+ return { product_content: { ...pc, blocks } };
21940
22551
  }
21941
22552
  },
21942
22553
  {
21943
- name: "update_inventory_level",
21944
- description: `\u4FEE\u6539\u67D0\u5E93\u5B58\u6761\u76EE\u5728\u6307\u5B9A\u4ED3\u5E93\u7684\u5E93\u5B58\u6570\u91CF\uFF08stocked_quantity\uFF09\u3002
21945
- \u26A0\uFE0F \u7AEF\u70B9/\u5B57\u6BB5\u4EE5 Medusa 2.21 \u5951\u7EA6\u4E3A\u51C6\uFF08POST /admin/inventory-items/{id}/location-levels\uFF09\u3002\u9ED8\u8BA4 dry_run\u3002`,
22554
+ name: "create_product_content",
22555
+ description: `\u521B\u5EFA/\u66F4\u65B0\u5546\u54C1\u8BE6\u60C5\u9875\u5BCC\u5185\u5BB9\u5757\uFF08POST /admin/product-contents\uFF0C\u6309 product_id+locale upsert\uFF09\u3002
22556
+ storefront \u8BE6\u60C5\u9875\u300C\u5BCC\u5185\u5BB9\u300D\u533A\u7531\u8FD9\u4E9B\u5757\u6E32\u67D3\u2014\u2014\u4EC5\u586B product.description \u4E0D\u4F1A\u663E\u793A\u5728\u8FD9\u91CC\uFF0C\u5FC5\u987B\u5EFA\u5185\u5BB9\u5757\uFF0C\u5426\u5219\u8BE6\u60C5\u9875\u4E3B\u4F53\u7A7A\u767D\u3002
22557
+ \u533A\u5757\u7C7B\u578B type \u2208 hero / richtext / image_gallery / video / faq / specs\uFF1A
22558
+ - richtext: data.{ html }\uFF08\u76F4\u63A5\u6E32\u67D3 HTML\uFF0C\u53EF\u542B <p>/<h2>/<ul> \u7B49\uFF09
22559
+ - image_gallery: data.{ images:string[], caption? }
22560
+ - hero: data.{ image?, title?, subtitle? }
22561
+ - video: data.{ url, poster? }\uFF08\u652F\u6301 YouTube\uFF09
22562
+ - faq: data.{ items: {question,answer}[] }
22563
+ - specs: data.{ rows: {label,value}[] }
22564
+ \u26A0\uFE0F locale \u5FC5\u987B\u7528\u533A\u57DF\u7801 en-US / zh-CN / ms-MY / id-ID\uFF08\u4E0E\u524D\u7AEF\u4E00\u81F4\uFF09\uFF1B\u5546\u54C1\u6807\u9898/\u63CF\u8FF0\u7684\u7FFB\u8BD1\u7528\u77ED\u7801 en/zh/ms/id\uFF0C\u4E8C\u8005\u4E0D\u540C\u3002
22565
+ \u9ED8\u8BA4 dry_run\uFF08confirm=false\uFF09\u4EC5\u9884\u89C8\u3002`,
21946
22566
  schema: {
21947
- inventory_item_id: external_exports.string(),
21948
- location_id: external_exports.string(),
21949
- stocked_quantity: external_exports.number().int().nonnegative(),
22567
+ product_id: external_exports.string(),
22568
+ locale: external_exports.enum(["en-US", "zh-CN", "ms-MY", "id-ID"]).default("en-US"),
22569
+ status: external_exports.enum(["draft", "published"]).default("published"),
22570
+ blocks: external_exports.array(
22571
+ external_exports.object({
22572
+ id: external_exports.string().optional(),
22573
+ type: external_exports.enum(["hero", "richtext", "image_gallery", "video", "faq", "specs"]),
22574
+ data: external_exports.record(external_exports.string(), external_exports.any())
22575
+ })
22576
+ ).describe("ContentBlock[] \u6570\u7EC4"),
21950
22577
  confirm: external_exports.boolean().default(false)
21951
22578
  },
21952
22579
  async run(input, { client: client2 }) {
21953
- const payload = {
21954
- location_id: input.location_id,
21955
- stocked_quantity: input.stocked_quantity
21956
- };
21957
- if (!input.confirm) return { dry_run: true, will_update: payload };
21958
- return client2.updateInventoryLevel(input.inventory_item_id, payload);
22580
+ if (!input.confirm) {
22581
+ return {
22582
+ dry_run: true,
22583
+ will_create: {
22584
+ product_id: input.product_id,
22585
+ locale: input.locale,
22586
+ status: input.status,
22587
+ block_count: input.blocks.length,
22588
+ types: input.blocks.map((b) => b.type)
22589
+ }
22590
+ };
22591
+ }
22592
+ const data = await client2.createProductContent({
22593
+ product_id: input.product_id,
22594
+ locale: input.locale,
22595
+ status: input.status,
22596
+ blocks: input.blocks
22597
+ });
22598
+ return { created: data };
21959
22599
  }
21960
22600
  }
21961
22601
  ];
@@ -22012,11 +22652,11 @@ var opsTools = [
22012
22652
  ];
22013
22653
 
22014
22654
  // src/resources/docs.ts
22015
- import { readdirSync, readFileSync as readFileSync2, existsSync as existsSync2, statSync } from "node:fs";
22655
+ import { readdirSync, readFileSync as readFileSync3, existsSync as existsSync3, statSync } from "node:fs";
22016
22656
  import { join as join2 } from "node:path";
22017
22657
  var cache = null;
22018
22658
  function walk(dir, out = []) {
22019
- if (!existsSync2(dir)) return out;
22659
+ if (!existsSync3(dir)) return out;
22020
22660
  for (const entry of readdirSync(dir)) {
22021
22661
  const full = join2(dir, entry);
22022
22662
  try {
@@ -22028,7 +22668,7 @@ function walk(dir, out = []) {
22028
22668
  return out;
22029
22669
  }
22030
22670
  function parseFile(path) {
22031
- const raw = readFileSync2(path, "utf8");
22671
+ const raw = readFileSync3(path, "utf8");
22032
22672
  const lines = raw.split("\n");
22033
22673
  const title = lines.find((l) => l.startsWith("# "))?.slice(2).trim() || path;
22034
22674
  const sections = [];
@@ -22139,7 +22779,7 @@ var config2 = loadConfig();
22139
22779
  var auth = new AuthManager(config2);
22140
22780
  var client = new MedusaAdminClient(config2, auth);
22141
22781
  var ctx = { client, config: config2 };
22142
- var server = new McpServer({ name: "derlux-admin", version: "0.1.0" });
22782
+ var server = new McpServer({ name: "derlux-admin", version: "0.1.2" });
22143
22783
  for (const def of toolDefs) {
22144
22784
  server.tool(def.name, def.description, def.schema, async (args) => {
22145
22785
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sea-ecommerce/admin-mcp-server",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "让运营同学用本地 AI agent(Codex / Workbuddy / Claude / Cursor / CodeBuddy)运营 Medusa 后台的 MCP server。",
@@ -9,7 +9,8 @@
9
9
  },
10
10
  "files": [
11
11
  "dist",
12
- "README.md"
12
+ "README.md",
13
+ "skill"
13
14
  ],
14
15
  "scripts": {
15
16
  "dev": "tsx src/index.ts",
package/skill/SKILL.md ADDED
@@ -0,0 +1,215 @@
1
+ ---
2
+ name: derlux-admin-mcp-ops
3
+ description: 运营使用本地 AI agent(derlux-admin MCP server)操作 DERLUX Store 后台的指南。覆盖商品创建/修改/下架、仓库与库存调整、订单/退货/仓库作业查询、后台使用问答。当用户要让 agent 创建、修改或下架商品、调整某仓库库存、查询订单或退货、或询问「后台怎么操作某功能」时使用。
4
+ ---
5
+
6
+ # DERLUX 运营后台 MCP 操作指南(derlux-admin)
7
+
8
+ > 本文件随 npm 包 `@sea-ecommerce/admin-mcp-server` 一同发布,运营/agent 可经 npm 公开文件链接读取,
9
+ > 例如:`https://unpkg.com/@sea-ecommerce/admin-mcp-server/skill/SKILL.md`
10
+ > 或 `https://cdn.jsdelivr.net/npm/@sea-ecommerce/admin-mcp-server/skill/SKILL.md`
11
+
12
+ ## 你是谁 / 这个 skill 是做什么的
13
+
14
+ 你已接入一个名为 **`derlux-admin`** 的 MCP server。它把 DERLUX Store(面向马来西亚/印尼的东南亚电商,法律主体「香港德樂仕股份有限公司」)Medusa 后台的运营动作封装成一批语义化工具(tool),由你直接调用,代替运营同学在后台界面手动点选。
15
+
16
+ 关键前提(务必记牢):
17
+
18
+ - **认证复用真实后台管理员账号**(须持有 `csr` 或 `super_admin` 角色),权限受现有 RBAC 约束。敏感动作若账号无权限,后端会直接拦截。
19
+ - **所有写操作默认是「预览(dry_run)」**,只有显式设 `confirm=true` 才会真正落到后台。这是给运营的兜底保护,不要跳过预览。
20
+ - 每次调用都会写入审计日志 `~/.derlux/mcp-audit.log`,操作可追溯。
21
+
22
+ ## ⚠️ 必须遵守的硬规则(每次调用写工具前自查)
23
+
24
+ 1. **预览优先,确认后执行**:任何写工具先以 `confirm=false`(默认)跑一次,把返回的预览读给运营确认;得到明确确认后,再用 `confirm=true` 真正执行。绝不在未确认时直接写库。
25
+ 2. **价格单位 = 主货币单位(元),不要 ×100**:金额直接透传主单位,例如 `200.51` 就是 RM200.51 / MYR200.51。Medusa v2.21 以主单位存储金额,**绝不要写成 20051**。
26
+ 3. **Handle 自动归一化**:商品 `handle` 会被自动转小写、空格/下划线/点→连字符、剔除其余非法字符。归一化后仍非法(空或全部被剔除)工具会直接报错,你无需手工处理,但应避免给无意义输入;未填时由 `title` 自动生成。
27
+ 4. **多语言必填**:`create_product` / `update_product` 的 `translations` 应覆盖 `en` / `zh` / `ms` / `id` 四种语言的 `title`(建议也带 `description`)。缺语言时:预览会告警;真正写入需 `confirm=true` **且** `acknowledge_missing_languages=true`。创建前主动提醒运营补全翻译,不要默默放行。工具会**自动**把翻译写入 Medusa v2.21 专用的 `/admin/translations/batch` 端点(商品创建体本身不接受 `translations` 字段),你无需手动调用该端点。
28
+ 5. **审计可追溯**:无需你处理,但要知道每次动作都在 `~/.derlux/mcp-audit.log` 留痕。
29
+ 6. **商品必须挂销售渠道才在前端可见**:新建商品默认**不在任何销售渠道**,storefront(按 publishable key → 渠道过滤)拉不到它,详情页表现成「空/404」。录入后用 `add_product_to_sales_channel`(`confirm=true`)把它加入渠道(默认第一个启用渠道;马来西亚站用 `sc_01M4K4F0D6HBHZ0NH6WB7H5KPT`「MY Storefront」)。
30
+ 7. **两套 locale 码,别混**:
31
+ - 商品标题/描述的**翻译**(`create_product` 的 `translations`、后端 `/admin/translations/batch`):用**简码** `en` / `zh` / `ms` / `id`。store `/store/products?locale=zh` 即回退到 zh 翻译。
32
+ - 商品详情页**富内容块**(`create_product_content` 的 `locale`):必须用**区域码** `en-US` / `zh-CN` / `ms-MY` / `id-ID`(与前端 `useProductContent` 的 localeMap 一致)。用简码会导致该语言详情页读不到内容。
33
+ 8. **详情页「富内容」区靠 `create_product_content`,不是 `description`**:storefront 详情页下方大块内容由 `product_content` 模块渲染(`richtext`/`specs`/`image_gallery` 等)。只填 `product.description` 不会显示在那里——录入完整商品时必须再建一份 `product_content`(建议每语言一份)。`get_product` 默认不含该字段,核对请用 `get_product_content`。
34
+ 9. **不建库存 = 前端缺货**:变体 `manage_inventory` 默认 `true`,但新商品默认 0 库存——`create_product` 时必须给 `initial_stock`(如 `{quantity:100}`,默认写 Main Warehouse),或对已有商品调 `set_product_stock`,否则前台显示缺货、无法下单。
35
+ 10. **多语言核对用 `get_translations`**:`get_product` 默认字段**不含** translations,不要凭它为空就判断没翻译。用 `get_translations`(reference_id=商品id或handle) 读取实际写入的各语言 title/description。
36
+
37
+ ## 工具速查表
38
+
39
+ | 工具 | 类型 | 写库? | 默认行为 |
40
+ |---|---|---|---|
41
+ | `list_products` | 读 | 否 | 直接返回 |
42
+ | `get_product` | 读 | 否 | 直接返回 |
43
+ | `list_sales_channels` | 读 | 否 | 直接返回(渠道 id/名称,建商品前确认用)|
44
+ | `list_collections` | 读 | 否 | 直接返回 |
45
+ | `get_translations` | 读 | 否 | 直接返回(某商品各语言的 title/description,多语言核对唯一可靠途径)|
46
+ | `list_stock_locations` | 读 | 否 | 直接返回 |
47
+ | `get_inventory_levels` | 读 | 否 | 直接返回(location_id 可选:按仓库列库存条目与水平;q 按 sku/title 过滤)|
48
+ | `set_product_stock` | 写 | 是(`confirm=true` 才写)| 幂等建仓:自动找/建库存条目 + 写仓库数量 + manage_inventory;`confirm=false` 仅预览 |
49
+ | `list_orders` | 读 | 否 | 直接返回 |
50
+ | `get_order` | 读 | 否 | 直接返回 |
51
+ | `list_warehouse_tasks` | 读 | 否 | 直接返回(自定义端点)|
52
+ | `list_return_requests` | 读 | 否 | 直接返回(自定义端点)|
53
+ | `search_docs` | 读 | 否 | 检索后台知识库 |
54
+ | `ask_usage` | 读 | 否 | 回答「后台怎么操作」类问题 |
55
+ | `create_product` | 写 | 是(`confirm=true` 才写)| `confirm=false` → 仅预览 |
56
+ | `update_product` | 写 | 是(`confirm=true` 才写)| `confirm=false` → 仅预览 |
57
+ | `deactivate_product` | 写 | 是(`confirm=true` 才写)| `confirm=false` → 仅预览,置 `archived` |
58
+ | `upload_product_image` | 写 | 是(`confirm=true` 才写)| `confirm=false` → 仅预览,给已有商品追加图片 |
59
+ | `add_product_to_sales_channel` | 写 | 是(`confirm=true` 才写)| `confirm=false` → 仅预览,把商品加入销售渠道使其在前端可见 |
60
+ | `create_product_content` | 写 | 是(`confirm=true` 才写)| `confirm=false` → 仅预览,写商品详情页富内容块(hero/richtext/image_gallery/video/faq/specs)|
61
+ | `get_product_content` | 读 | 否 | 读取某商品某语言的富内容块 |
62
+ | `create_stock_location` | 写 | 是(`confirm=true` 才写)| `confirm=false` → 仅预览 |
63
+ | `update_inventory_level` | 写 | 是(`confirm=true` 才写)| `confirm=false` → 仅预览 |
64
+
65
+ ## 各工具参数与说明
66
+
67
+ ### 读 —— 商品
68
+ - **`list_products`**:查询商品列表。
69
+ - `q`(string,可选)关键词;`status`(enum:`draft|published|proposed|rejected|archived`,可选)状态过滤;`handle`(string,可选);`limit`(int 1–100,默认 20)。
70
+ - **`get_product`**:按商品 id 或 handle 获取详情。
71
+ - `id_or_handle`(string,**必填**)。
72
+ - **`list_sales_channels`**:列出销售渠道(含 id / 名称 / 启用状态)。
73
+ - `q`(string,可选)按名称过滤;`limit`(int 1–100,默认 50)。
74
+ - **`list_collections`**:列出集合(如「双11活动」)。
75
+ - `q`(string,可选);`limit`(int 1–100,默认 50)。
76
+ - **`get_translations`**:读取多语言翻译(GET /admin/translations)。
77
+ - `reference_id`(string,**必填**,商品 id 或 handle);`reference`(默认 `product`);`locale_code`(可选,如 `zh`);`limit`(默认 50)。
78
+
79
+ ### 读 —— 仓库与库存
80
+ - **`list_stock_locations`**:列出仓库(StockLocation)。
81
+ - `q`(string,可选)按名称过滤;`limit`(int 1–100,默认 50)。
82
+ - **`get_inventory_levels`**:列出库存条目及库存水平(Medusa 2.21 走 `GET /admin/inventory-items`)。
83
+ - `location_id`(string,**可选**:只回该仓库有水平的条目);`q`(string,可选,按 sku/title 过滤);`limit`(int 1–100,默认 50)。
84
+
85
+ ### 读 —— 订单 / 履约 / 退货
86
+ - **`list_orders`**:查询订单列表。
87
+ - `q`(string,可选);`status`(string,可选);`limit`(int 1–100,默认 20)。
88
+ - **`get_order`**:按订单 id 或 `display_id` 获取详情。
89
+ - `id`(string,**必填**)。
90
+ - **`list_warehouse_tasks`**:查询仓库作业任务(自定义端点 `/admin/warehouse-tasks`)。
91
+ - `order_id`(string,可选);`stage`(string,可选);`limit`(int 1–200,默认 200)。
92
+ - **`list_return_requests`**:查询退货/售后请求(自定义端点 `/admin/return-requests`)。
93
+ - `status`(string,可选);`limit`(int 1–100,默认 50)。
94
+
95
+ ### 读 —— 后台知识问答
96
+ - **`search_docs`**:在知识库(docs)中检索相关段落,返回文件、章节与摘要。
97
+ - `query`(string,**必填**);`limit`(int 1–20,默认 5)。
98
+ - **`ask_usage`**:回答「后台怎么操作」类问题,返回最相关段落与出处文件。
99
+ - `question`(string,**必填**);`limit`(int 1–10,默认 5)。若搜不到,提示运营换关键词或查后台手册。
100
+
101
+ ### 写 —— 商品
102
+ - **`create_product`**:新增商品。
103
+ - `title`(string,**必填**);`handle`(string,可选,缺省由 title 归一化);`status`(enum,默认 `draft`);`subtitle`(可选);`description`(可选);`price`(number ≥0,主货币单位,**切勿 ×100**);`currency_code`(默认 `MYR`);`prices`(`{currency_code,amount}[]`,可选,多币种,优先于单一 price);`translations`(`record<locale,{title?,description?}>`,建议覆盖 `en/zh/ms/id`);`images`(string[],可选,本地图片路径或图片 URL,工具自动上传并挂接);`sku`(string,可选,写入默认变体 SKU);`metadata`(`record<string,string>`,可选,存规格参数/来源链接等任意键值);`sales_channels`(string[],可选,渠道 id 或名称如 `"MY Storefront"`,创建即挂载);`collection_id`(string,可选,集合 id 或名称);`weight`/`length`/`height`/`width`(number,可选,克/厘米);`origin_country`/`hs_code`/`mid_code`/`material`(string,可选);`manage_inventory`(bool,**默认 true**);`initial_stock`(`{location_id?, quantity}`,可选,建库存;**正式上架必给**,见硬规则 9);`confirm`(bool,默认 false);`acknowledge_missing_languages`(bool,默认 false)。
104
+ - 行为:`confirm=false` 返回预览(含缺语言清单、缺货告警);`confirm=true` 才真正创建;缺语言时须同时 `acknowledge_missing_languages=true`。创建时**自动**补上 Medusa v2 必需的 `options`(单一 Default)、图片上传并挂接、`sku` 写入默认变体、`translations` 写入 v2.21 专用的 `/admin/translations/batch` 端点、按 `initial_stock` 建库存条目并写仓库。
105
+ - **`update_product`**:修改商品字段(标题/handle/状态/描述/多语言/物理尺寸/海关/集合/metadata)。
106
+ - `id`(string,**必填**);`title`/`handle`/`status`/`subtitle`/`description`/`translations`/`collection_id`(集合 id 或名称)/`weight`/`length`/`height`/`width`/`origin_country`/`hs_code`/`mid_code`/`material`/`metadata`(均可选)。
107
+ - `metadata` 为**合并**语义:新键覆盖同名键、未提及的旧键保留(不支持删除键)。改库存请用 `set_product_stock`,改价格请用 `update_product_prices`。
108
+ - `confirm`(默认 false)。`confirm=false` 返回合并后的完整预览。
109
+ - **`deactivate_product`**:下架商品(status 置 `archived`)。
110
+ - `id`(string,**必填**);`confirm`(默认 false)。`confirm=false` 返回预览。
111
+ - **`upload_product_image`**:给**已存在**的商品追加图片(与已有图片合并,不覆盖)。
112
+ - `product_id`(string,**必填**);`images`(string[],本地图片路径或图片 URL);`confirm`(默认 false)。`confirm=false` 仅预览;`confirm=true` 才上传并挂接。
113
+ - **`add_product_to_sales_channel`**:把商品加入销售渠道,使其在前端商城可见。
114
+ - `product_ids`(string[],**必填**,商品 id 列表);`channel_id`(string,可选,省略则取第一个启用渠道);`confirm`(默认 false)。`confirm=false` 仅预览。
115
+ - **`get_product_content`**:读取商品详情页富内容块。
116
+ - `product_id`(string,**必填**);`locale`(`en-US` / `zh-CN` / `ms-MY` / `id-ID`,默认 `en-US`);`status`(`draft` / `published`,默认 `published`)。返回 `{ product_content: { id, product_id, locale, status, blocks(已解析数组) } | null }`。
117
+ - **`create_product_content`**:创建/更新商品详情页富内容块(按 `product_id` + `locale` upsert)。**这是填充详情页主体内容的工具**——只填 `description` 详情页大块区域仍是空白。
118
+ - `product_id`(string,**必填**);`locale`(区域码,默认 `en-US`,见硬规则 7);`status`(默认 `published`);`blocks`(ContentBlock[],**必填**);`confirm`(默认 false)。
119
+ - 区块 `type` 与 `data`:
120
+ - `richtext`:`data.{ html }`(直接渲染 HTML,可含 `<p>/<h2>/<ul>`)
121
+ - `image_gallery`:`data.{ images:string[], caption? }`
122
+ - `hero`:`data.{ image?, title?, subtitle? }`
123
+ - `video`:`data.{ url, poster? }`(支持 YouTube)
124
+ - `faq`:`data.{ items: {question,answer}[] }`
125
+ - `specs`:`data.{ rows: {label,value}[] }`
126
+ - `confirm=false` 返回预览(含块数量与类型);`confirm=true` 才写入。
127
+
128
+ ### 写 —— 仓库与库存
129
+ - **`create_stock_location`**:新增仓库(StockLocation)。
130
+ - `name`(string,**必填**);`address`(可选:`{address_1?,address_2?,city?,country_code?,province?,postal_code?,phone?}`);`metadata`(`record<string,string>`,可选);`confirm`(默认 false)。
131
+ - **`set_product_stock`**:给商品建仓/改库存(**运营最常用的库存工具**,幂等)。
132
+ - `product_id`(string,**必填**,商品 id 或 handle);`sku`(string,可选,目标变体;省略取第一个变体);`location_id`(string,可选,省略=Main Warehouse/第一个仓库);`quantity`(int ≥0,**必填**,绝对值非增量);`manage_inventory`(bool,默认 true);`confirm`(默认 false)。
133
+ - 行为:按变体 SKU 查找库存条目(缺失则自动 `POST /admin/inventory-items` 创建)→ 幂等写 (条目, 仓库) 库存水平(有则更新、无则创建)→ 确保变体 `manage_inventory` 为 true。重复调用安全。
134
+ - **`update_inventory_level`**:直接改某**库存条目**(`inventory_item_id`)在某仓库的数量(upsert,幂等)。
135
+ - `inventory_item_id`(string,**必填**);`location_id`(string,**必填**);`stocked_quantity`(int ≥0,**必填**);`confirm`(默认 false)。已知 `inventory_item_id` 时用;否则优先 `set_product_stock`。
136
+
137
+ ## 推荐操作流程(SOP)
138
+
139
+ **创建商品**
140
+ 1. 先收集:标题、handle(或告知由标题生成)、价格(主单位)、四语言标题、状态、图片、规格、重量、初始库存数量、目标渠道、详情页富文本内容(按语言)。
141
+ 2. 调用 `create_product`(`confirm=false`)→ 展示预览与「缺语言 / 缺货」告警。
142
+ 3. 提醒运营补全缺失语言与 `initial_stock`;确认无误后调用 `create_product`(`confirm=true`,`sales_channels` 里带上目标渠道,必要时 `acknowledge_missing_languages=true`)。创建即完成:图片上传、SKU、翻译、渠道挂载、建库存。
143
+ 4. **`create_product_content`(`confirm=true`,逐语言 `en-US/zh-CN/ms-MY/id-ID`)** → 写详情页富内容块(richtext 描述 + specs 规格 + image_gallery 图集)。否则详情页主体空白。
144
+ 5. 用 `get_translations` + `get_product_content` 核对多语言与富内容;把新商品 id/handle 告知运营。
145
+
146
+ **修改 / 下架商品**
147
+ 1. `get_product` 或 `list_products` 确认目标 id。
148
+ 2. `update_product` / `deactivate_product`(`confirm=false`)→ 展示预览。
149
+ 3. 确认后同名工具 `confirm=true` 执行。
150
+
151
+ **建仓 / 调整库存**
152
+ 1. 直接 `set_product_stock`(`product_id` + `quantity`,`confirm=false` 预览 → 确认后 `confirm=true`)——自动处理库存条目与仓库。
153
+ 2. 查现状:`get_inventory_levels`(`location_id` 可选、`q` 按 sku 过滤)。
154
+ 3. 已知 `inventory_item_id` 的精细调整:`update_inventory_level`(upsert)。
155
+
156
+ **查询类**:直接调用对应读工具即可,无需 `confirm`。
157
+
158
+ ## 常见任务示例(运营怎么说 / 你怎么调)
159
+
160
+ - 「下架 handle 为 `summer-serum` 的商品」→ `get_product`(id_or_handle=`summer-serum`) 取 id → `deactivate_product`(id, confirm=false 预览 → 确认后 confirm=true)。
161
+ - 「给 A 商品补 100 件库存」→ `set_product_stock`(product_id=A 的 id, quantity=100, confirm=false 预览 → 确认后 confirm=true;默认写 Main Warehouse,指定仓传 location_id)。
162
+ - 「查一下最近 10 个待发货订单」→ `list_orders`(status=`pending`, limit=10)。
163
+ - 「后台怎么新建优惠券」→ `ask_usage`(question=`如何新建优惠券`)。
164
+ - 「列出所有退货请求」→ `list_return_requests`()。
165
+ - 「把这份抓取的 Lazada 商品录入:标题 DR.CYJ Hair Filler、价格 420 MYR、4 张本地图、SKU `4820...`、规格存 metadata、补全中/马来/印尼语翻译」→ 先整理数据,`create_product`(`confirm=false` 看预览含 images/sku/metadata/缺语言/缺货告警 → 确认后 `confirm=true`,带 `sales_channels: ["MY Storefront"]`、`initial_stock: {quantity: 100}`、`weight: 150`,必要时 `acknowledge_missing_languages=true`)。工具会自动上传图片、写 SKU/metadata/物理字段、四语言翻译、渠道挂载并建库存。再 `create_product_content`(`locale=en-US/zh-CN/ms-MY/id-ID`, `confirm=true`) 逐语言写富内容块(richtext 描述 + specs 规格 + image_gallery 图集),最后 `get_translations` + `get_product_content` 核对。
166
+ - 「核对某商品多语言是否齐全」→ `get_translations`(reference_id=商品 id 或 handle),看 4 条 locale 是否都有 title/description。
167
+ - 「让这个商品在前端能搜到」→ `get_product` 取 id → `add_product_to_sales_channel`(`product_ids=[id]`, `confirm=true`)。
168
+ - 「补全某商品中文详情页内容」→ `create_product_content`(`product_id`, `locale=zh-CN`, `blocks=[{type:'richtext',data:{html:'...'}},{type:'specs',data:{rows:[...]}}]`, `confirm=true`)。可用 `get_product_content` 先核对现状。
169
+
170
+ ## 给运营的一次性安装配置
171
+
172
+ agent 客户端(Codex / Claude Desktop / Cursor / Workbuddy / CodeBuddy 等)的 MCP 配置里加一条即可(机器只需 Node ≥ 18,无需 clone 仓库、无需 build):
173
+
174
+ ```json
175
+ {
176
+ "mcpServers": {
177
+ "derlux-admin": {
178
+ "command": "npx",
179
+ "args": ["-y", "@sea-ecommerce/admin-mcp-server"],
180
+ "env": {
181
+ "MEDUSA_BACKEND_URL": "https://admin.internal.derlux.com",
182
+ "DERLUX_ADMIN_EMAIL": "你的后台账号",
183
+ "DERLUX_ADMIN_PASSWORD": "你的密码"
184
+ }
185
+ }
186
+ }
187
+ }
188
+ ```
189
+
190
+ ### 环境变量说明
191
+
192
+ | 变量 | 必填 | 默认 | 说明 |
193
+ |---|---|---|---|
194
+ | `MEDUSA_BACKEND_URL` | 否 | `http://localhost:9000` | 后端地址(生产填内部域名)|
195
+ | `DERLUX_ADMIN_EMAIL` | **是** | — | 后台管理员账号(须持 `csr`/`super_admin`)|
196
+ | `DERLUX_ADMIN_PASSWORD` | **是** | — | 该账号密码 |
197
+ | `DERLUX_AUTH_PATH` | 否 | `/auth/user/emailpass` | 登录端点 |
198
+ | `DERLUX_SESSION_FILE` | 否 | `~/.derlux/mcp-session` | 会话持久化文件(权限 0600)|
199
+ | `DERLUX_AUDIT_FILE` | 否 | `~/.derlux/mcp-audit.log` | 审计日志 |
200
+ | `DERLUX_DOCS_DIR` | 否 | `./docs` | 问答知识源目录(npx 形态下通常留默认或指向本地 docs 副本)|
201
+ | `DERLUX_REQUIRED_LOCALES` | 否 | `en,zh,ms,id` | 商品必须覆盖的语言 |
202
+
203
+ ## 已知限制 / 注意事项
204
+
205
+ - 本 MCP **不改动任何数据库 schema**,仅复用现有 `/admin/*` 接口;运营动作本身按既有逻辑写库。
206
+ - 登录走**用户账号**而非 API Key(现有 RBAC 依赖 `user.metadata.rbac_roles`)。
207
+ - 金额以**主货币单位**存储(见上「硬规则 2」),不要换算成分。
208
+ - **Medusa v2.21 差异(重要)**:
209
+ - 有变体就必须有 product options,工具已自动补一个 `Default` 选项,你无需关心;**更新**商品时 `options` 字段被移除(2.16+),工具不会误传。
210
+ - 商品创建/更新体**不接受 `translations` 字段**;翻译由工具自动走 `/admin/translations/batch`。`get_product` 默认字段不含 `translations` 关系,**核对翻译请用 `get_translations`**,不要凭 `get_product` 返回为空就误判没写入。
211
+ - 图片走 `/admin/uploads` 上传后再挂到商品(无独立 `/admin/products/{id}/images` 端点)。
212
+ - 库存查询/写入端点已按 2.21 契约校准(`/admin/inventory-items` + `location-levels`,sku 唯一约束);`set_product_stock` 全程幂等可重放。
213
+ - 自定义端点 `warehouse-tasks` / `return-requests` / `product-contents` 为本项目扩展。
214
+ - 文档问答(`search_docs` / `ask_usage`)依赖知识源目录;npx 形态下默认 `./docs`,若问不到相关内容,提示运营换关键词或查后台手册。
215
+ - 用受限的 `csr` 账号即可完成日常运营;敏感动作由后端 RBAC 拦截。