@sea-ecommerce/admin-mcp-server 0.1.1 → 0.1.3

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/` 回答后台使用问题 | 读 |
@@ -21718,6 +21718,17 @@ var MedusaAdminClient = class {
21718
21718
  async batchTranslations(create, update = []) {
21719
21719
  return this.request("POST", "/admin/translations/batch", { create, update });
21720
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
+ }
21721
21732
  /**
21722
21733
  * 把一组商品加入销售渠道(POST /admin/sales-channels/{id}/products)。
21723
21734
  * ⚠️ 注意:Medusa v2 该端点挂在「渠道」下,不是商品下;body 为 { add:[productIds], remove:[] }。
@@ -21806,25 +21817,79 @@ var MedusaAdminClient = class {
21806
21817
  createStockLocation(body) {
21807
21818
  return this.request("POST", "/admin/stock-locations", body);
21808
21819
  }
21809
- /** 某仓库下的库存条目及库存水平 */
21810
- 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 = {}) {
21811
21840
  return this.request(
21812
21841
  "GET",
21813
- `/admin/stock-locations/${encodeURIComponent(locationId)}/inventory-items${buildQuery(params)}`
21842
+ `/admin/inventory-items/${encodeURIComponent(inventoryItemId)}/location-levels${buildQuery(params)}`
21814
21843
  );
21815
21844
  }
21816
- /**
21817
- * 设置/更新某库存条目在指定仓库的库存水平。
21818
- * ⚠️ 实施时按 Medusa 2.21 官方契约核对端点与字段
21819
- * (v2 常见为 POST /admin/inventory-items/{id}/location-levels,body 含 location_id + stocked_quantity)。
21820
- */
21821
- updateInventoryLevel(inventoryItemId, body) {
21845
+ /** 新建某库存条目在某仓库的库存水平(POST .../location-levels,body: { location_id, stocked_quantity? }) */
21846
+ createInventoryItemLevel(inventoryItemId, body) {
21822
21847
  return this.request(
21823
21848
  "POST",
21824
21849
  `/admin/inventory-items/${encodeURIComponent(inventoryItemId)}/location-levels`,
21825
21850
  body
21826
21851
  );
21827
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
+ * 把库存条目关联到变体(POST /admin/products/{pid}/variants/{vid}/inventory-items)。
21863
+ * ⚠️ 关联是「变体能查到库存」的关键:只有关联后,变体的 manage_inventory 才指向该条目,
21864
+ * 且设置 manage_inventory=true 时 Medusa 不会因「无关联条目」而另建同 SKU 条目(撞唯一约束)。
21865
+ * 重复关联会报已存在,调用方应 try/catch 吞掉「已关联」类错误。
21866
+ */
21867
+ linkVariantInventoryItem(productId, variantId, inventoryItemId) {
21868
+ return this.request(
21869
+ "POST",
21870
+ `/admin/products/${encodeURIComponent(productId)}/variants/${encodeURIComponent(variantId)}/inventory-items`,
21871
+ { inventory_item_id: inventoryItemId, required_quantity: 1 }
21872
+ );
21873
+ }
21874
+ /**
21875
+ * 幂等地设置某库存条目在指定仓库的库存数量:
21876
+ * 已有 (item, location) 水平 → 更新;没有 → 创建。
21877
+ */
21878
+ async upsertInventoryLevel(inventoryItemId, locationId, stockedQuantity) {
21879
+ const existing = await this.getInventoryItemLevels(inventoryItemId, {
21880
+ location_id: locationId
21881
+ });
21882
+ const found = (existing?.inventory_levels || []).find((l) => l.location_id === locationId);
21883
+ if (found) {
21884
+ await this.updateInventoryItemLevel(inventoryItemId, locationId, { stocked_quantity: stockedQuantity });
21885
+ return { created: false, stocked_quantity: stockedQuantity };
21886
+ }
21887
+ await this.createInventoryItemLevel(inventoryItemId, {
21888
+ location_id: locationId,
21889
+ stocked_quantity: stockedQuantity
21890
+ });
21891
+ return { created: true, stocked_quantity: stockedQuantity };
21892
+ }
21828
21893
  // ─── 订单 ───
21829
21894
  listOrders(params = {}) {
21830
21895
  return this.request("GET", `/admin/orders${buildQuery(params)}`);
@@ -21841,6 +21906,210 @@ var MedusaAdminClient = class {
21841
21906
  }
21842
21907
  };
21843
21908
 
21909
+ // src/tools/stock.ts
21910
+ async function applyProductStock(ctx2, opts) {
21911
+ const { client: client2 } = ctx2;
21912
+ const prod = await client2.getProduct(opts.productId);
21913
+ const product = prod?.product ?? prod;
21914
+ if (!product?.variants?.length) throw new Error(`\u5546\u54C1 ${opts.productId} \u6CA1\u6709\u53D8\u4F53\uFF0C\u65E0\u6CD5\u5EFA\u4ED3`);
21915
+ const variant = opts.sku ? product.variants.find((v) => v.sku === opts.sku) : product.variants[0];
21916
+ if (!variant) throw new Error(`\u627E\u4E0D\u5230\u76EE\u6807\u53D8\u4F53\uFF08sku=${opts.sku || "\u7B2C\u4E00\u4E2A"}\uFF09`);
21917
+ let location = null;
21918
+ if (opts.locationId) {
21919
+ const locs = await client2.listStockLocations({ limit: 100 });
21920
+ location = (locs?.stock_locations || []).find((l) => l.id === opts.locationId);
21921
+ if (!location) throw new Error(`\u627E\u4E0D\u5230\u4ED3\u5E93 ${opts.locationId}`);
21922
+ } else {
21923
+ const locs = await client2.listStockLocations({ limit: 100 });
21924
+ const list = locs?.stock_locations || [];
21925
+ location = list.find((l) => /main warehouse/i.test(l.name)) || list[0];
21926
+ if (!location) throw new Error("\u6CA1\u6709\u4EFB\u4F55\u53EF\u7528\u4ED3\u5E93\uFF0C\u8BF7\u5148 create_stock_location");
21927
+ }
21928
+ const findItem = async () => {
21929
+ if (!variant.sku) return null;
21930
+ const items = await client2.listInventoryItems({ sku: variant.sku, limit: 10 });
21931
+ return (items?.inventory_items || []).find((i) => i.sku === variant.sku) || null;
21932
+ };
21933
+ if (opts.manageInventory) {
21934
+ if (!variant.sku) {
21935
+ 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`);
21936
+ }
21937
+ let iitem2 = await findItem();
21938
+ if (!iitem2) {
21939
+ const created = await client2.createInventoryItem({
21940
+ sku: variant.sku,
21941
+ title: variant.title || void 0
21942
+ });
21943
+ iitem2 = created?.inventory_item ?? created;
21944
+ }
21945
+ try {
21946
+ await client2.linkVariantInventoryItem(product.id, variant.id, iitem2.id);
21947
+ } catch {
21948
+ }
21949
+ if (Boolean(variant.manage_inventory) !== opts.manageInventory) {
21950
+ await client2.updateProduct(product.id, {
21951
+ variants: [{ id: variant.id, manage_inventory: opts.manageInventory }]
21952
+ });
21953
+ }
21954
+ iitem2 = await findItem() || iitem2;
21955
+ const level = await client2.upsertInventoryLevel(iitem2.id, location.id, opts.quantity);
21956
+ return {
21957
+ product_id: product.id,
21958
+ variant_id: variant.id,
21959
+ sku: variant.sku,
21960
+ location_id: location.id,
21961
+ location_name: location.name,
21962
+ inventory_item_id: iitem2.id,
21963
+ manage_inventory: opts.manageInventory,
21964
+ stocked_quantity: level.stocked_quantity,
21965
+ level_created: level.created
21966
+ };
21967
+ }
21968
+ if (Boolean(variant.manage_inventory) !== opts.manageInventory) {
21969
+ await client2.updateProduct(product.id, {
21970
+ variants: [{ id: variant.id, manage_inventory: opts.manageInventory }]
21971
+ });
21972
+ }
21973
+ const iitem = await findItem();
21974
+ if (iitem) {
21975
+ await client2.upsertInventoryLevel(iitem.id, location.id, 0);
21976
+ }
21977
+ return {
21978
+ product_id: product.id,
21979
+ variant_id: variant.id,
21980
+ sku: variant.sku,
21981
+ location_id: location.id,
21982
+ location_name: location.name,
21983
+ inventory_item_id: iitem?.id ?? null,
21984
+ manage_inventory: false,
21985
+ stocked_quantity: 0,
21986
+ note: "\u5DF2\u5173\u95ED\u5E93\u5B58\u7BA1\u7406\u5E76\u6E05\u96F6\u5E93\u5B58"
21987
+ };
21988
+ }
21989
+ var stockTools = [
21990
+ {
21991
+ name: "list_stock_locations",
21992
+ description: "\u5217\u51FA\u4ED3\u5E93\uFF08Medusa StockLocation\uFF09\u3002\u53EF\u9009\u5173\u952E\u8BCD q \u8FC7\u6EE4\u540D\u79F0\u3002",
21993
+ schema: {
21994
+ q: external_exports.string().optional(),
21995
+ limit: external_exports.number().int().positive().max(100).default(50)
21996
+ },
21997
+ async run(input, { client: client2 }) {
21998
+ return client2.listStockLocations({ q: input.q, limit: input.limit });
21999
+ }
22000
+ },
22001
+ {
22002
+ name: "create_stock_location",
22003
+ description: "\u589E\u52A0\u4E00\u4E2A\u4ED3\u5E93\uFF08StockLocation\uFF09\u3002\u9ED8\u8BA4 dry_run\u3002",
22004
+ schema: {
22005
+ name: external_exports.string(),
22006
+ address: external_exports.object({
22007
+ address_1: external_exports.string().optional(),
22008
+ address_2: external_exports.string().optional(),
22009
+ city: external_exports.string().optional(),
22010
+ country_code: external_exports.string().optional(),
22011
+ province: external_exports.string().optional(),
22012
+ postal_code: external_exports.string().optional(),
22013
+ phone: external_exports.string().optional()
22014
+ }).optional(),
22015
+ metadata: external_exports.record(external_exports.string(), external_exports.string()).optional(),
22016
+ confirm: external_exports.boolean().default(false)
22017
+ },
22018
+ async run(input, { client: client2 }) {
22019
+ const payload = { name: input.name };
22020
+ if (input.address) payload.address = input.address;
22021
+ if (input.metadata) payload.metadata = input.metadata;
22022
+ if (!input.confirm) return { dry_run: true, will_create: payload };
22023
+ return client2.createStockLocation(payload);
22024
+ }
22025
+ },
22026
+ {
22027
+ name: "get_inventory_levels",
22028
+ description: `\u67E5\u8BE2\u5E93\u5B58\u6761\u76EE\u53CA\u5E93\u5B58\u6C34\u5E73\uFF08GET /admin/inventory-items\uFF0CMedusa 2.21 \u5951\u7EA6\uFF09\u3002
22029
+ 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`,
22030
+ schema: {
22031
+ location_id: external_exports.string().optional().describe("\u4ED3\u5E93 id\uFF1B\u7701\u7565\u5219\u4E0D\u9650\u4ED3\u5E93"),
22032
+ q: external_exports.string().optional(),
22033
+ limit: external_exports.number().int().positive().max(100).default(50)
22034
+ },
22035
+ async run(input, { client: client2 }) {
22036
+ const params = { limit: input.limit };
22037
+ if (input.q) params.q = input.q;
22038
+ if (input.location_id) params["location_levels.location_id"] = input.location_id;
22039
+ return client2.listInventoryItems(params);
22040
+ }
22041
+ },
22042
+ {
22043
+ name: "set_product_stock",
22044
+ 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
22045
+ \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
22046
+ \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
22047
+ \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
22048
+ \u9ED8\u8BA4 dry_run\uFF08confirm=false\uFF09\u4EC5\u9884\u89C8\uFF1Bconfirm=true \u624D\u5199\u5165\u3002`,
22049
+ schema: {
22050
+ product_id: external_exports.string().describe("\u5546\u54C1 id \u6216 handle"),
22051
+ sku: external_exports.string().optional().describe("\u76EE\u6807\u53D8\u4F53 SKU\uFF1B\u7701\u7565\u5219\u53D6\u7B2C\u4E00\u4E2A\u53D8\u4F53"),
22052
+ location_id: external_exports.string().optional().describe("\u4ED3\u5E93 id\uFF1B\u7701\u7565\u5219 Main Warehouse/\u7B2C\u4E00\u4E2A"),
22053
+ quantity: external_exports.number().int().nonnegative().describe("\u8BE5\u4ED3\u5E93\u5E93\u5B58\u6570\u91CF\uFF08\u7EDD\u5BF9\u503C\uFF0C\u975E\u589E\u91CF\uFF09"),
22054
+ manage_inventory: external_exports.boolean().default(true).describe("\u53D8\u4F53\u662F\u5426\u542F\u7528\u5E93\u5B58\u7BA1\u7406\uFF0C\u9ED8\u8BA4 true"),
22055
+ confirm: external_exports.boolean().default(false)
22056
+ },
22057
+ async run(input, ctx2) {
22058
+ const plan = {
22059
+ product_id: input.product_id,
22060
+ sku: input.sku || "(\u7B2C\u4E00\u4E2A\u53D8\u4F53)",
22061
+ location_id: input.location_id || "(Main Warehouse/\u7B2C\u4E00\u4E2A\u4ED3\u5E93)",
22062
+ quantity: input.quantity,
22063
+ manage_inventory: input.manage_inventory
22064
+ };
22065
+ if (!input.confirm) {
22066
+ return { dry_run: true, will_set_stock: plan };
22067
+ }
22068
+ try {
22069
+ const result = await applyProductStock(ctx2, {
22070
+ productId: input.product_id,
22071
+ sku: input.sku,
22072
+ locationId: input.location_id,
22073
+ quantity: input.quantity,
22074
+ manageInventory: input.manage_inventory
22075
+ });
22076
+ return { set_stock: result };
22077
+ } catch (e) {
22078
+ return `\u9519\u8BEF\uFF1A\u5EFA\u4ED3\u5931\u8D25 \u2014\u2014 ${e?.message || e}`;
22079
+ }
22080
+ }
22081
+ },
22082
+ {
22083
+ name: "update_inventory_level",
22084
+ 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
22085
+ \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`,
22086
+ schema: {
22087
+ inventory_item_id: external_exports.string(),
22088
+ location_id: external_exports.string(),
22089
+ stocked_quantity: external_exports.number().int().nonnegative(),
22090
+ confirm: external_exports.boolean().default(false)
22091
+ },
22092
+ async run(input, { client: client2 }) {
22093
+ if (!input.confirm) {
22094
+ return {
22095
+ dry_run: true,
22096
+ will_update: {
22097
+ inventory_item_id: input.inventory_item_id,
22098
+ location_id: input.location_id,
22099
+ stocked_quantity: input.stocked_quantity
22100
+ }
22101
+ };
22102
+ }
22103
+ const level = await client2.upsertInventoryLevel(
22104
+ input.inventory_item_id,
22105
+ input.location_id,
22106
+ input.stocked_quantity
22107
+ );
22108
+ return { updated: { ...level, inventory_item_id: input.inventory_item_id, location_id: input.location_id } };
22109
+ }
22110
+ }
22111
+ ];
22112
+
21844
22113
  // src/tools/products.ts
21845
22114
  var PRODUCT_STATUS = [
21846
22115
  "draft",
@@ -21884,6 +22153,66 @@ var productTools = [
21884
22153
  return client2.getProduct(input.id_or_handle);
21885
22154
  }
21886
22155
  },
22156
+ {
22157
+ name: "list_sales_channels",
22158
+ 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",
22159
+ schema: {
22160
+ q: external_exports.string().optional(),
22161
+ limit: external_exports.number().int().positive().max(100).default(50)
22162
+ },
22163
+ async run(input, { client: client2 }) {
22164
+ return client2.listSalesChannels({ q: input.q, limit: input.limit });
22165
+ }
22166
+ },
22167
+ {
22168
+ name: "list_collections",
22169
+ 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",
22170
+ schema: {
22171
+ q: external_exports.string().optional(),
22172
+ limit: external_exports.number().int().positive().max(100).default(50)
22173
+ },
22174
+ async run(input, { client: client2 }) {
22175
+ return client2.listCollections({ q: input.q, limit: input.limit });
22176
+ }
22177
+ },
22178
+ {
22179
+ name: "get_translations",
22180
+ 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
22181
+ \uFF08get_product \u9ED8\u8BA4\u5B57\u6BB5\u4E0D\u542B translations\uFF09\u3002reference \u4E00\u822C\u4E3A product\uFF0Creference_id \u4F20\u5546\u54C1 id \u6216 handle\u3002
22182
+ \u8FD4\u56DE\u6BCF\u6761 { locale_code, title, description }\u3002`,
22183
+ schema: {
22184
+ reference: external_exports.string().default("product"),
22185
+ reference_id: external_exports.string().describe("\u5546\u54C1 id \u6216 handle"),
22186
+ locale_code: external_exports.string().optional().describe("\u53EA\u53D6\u67D0\u4E00\u8BED\u8A00\uFF0C\u5982 zh"),
22187
+ limit: external_exports.number().int().positive().max(100).default(50)
22188
+ },
22189
+ async run(input, { client: client2 }) {
22190
+ let refId = input.reference_id;
22191
+ if (!/^[a-z]+_[a-z0-9]+$/i.test(refId)) {
22192
+ const prod = await client2.getProduct(refId).catch(() => null);
22193
+ const id = prod?.product?.id || prod?.id;
22194
+ if (!id) return `\u9519\u8BEF\uFF1A\u627E\u4E0D\u5230 ${input.reference} "${refId}"\uFF08\u8BF7\u4F20 id \u6216\u6709\u6548 handle\uFF09`;
22195
+ refId = id;
22196
+ }
22197
+ const params = {
22198
+ reference: input.reference,
22199
+ reference_id: refId,
22200
+ limit: input.limit
22201
+ };
22202
+ if (input.locale_code) params.locale_code = input.locale_code;
22203
+ const data = await client2.listTranslations(params);
22204
+ const rows = (data?.translations || []).map((t) => {
22205
+ const tr = t.translations || {};
22206
+ return {
22207
+ locale_code: t.locale_code,
22208
+ title: tr.title ?? null,
22209
+ description: tr.description ? tr.description.slice(0, 120) + "\u2026" : null,
22210
+ has_full_description: Boolean(tr.description)
22211
+ };
22212
+ });
22213
+ return { reference: input.reference, reference_id: input.reference_id, translations: rows, count: rows.length };
22214
+ }
22215
+ },
21887
22216
  {
21888
22217
  name: "create_product",
21889
22218
  description: `\u5728\u540E\u53F0\u589E\u52A0\u4E00\u4E2A\u5546\u54C1\u3002
@@ -21891,7 +22220,10 @@ var productTools = [
21891
22220
  \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
21892
22221
  \u9ED8\u8BA4 dry_run\uFF08confirm=false\uFF09\u53EA\u8FD4\u56DE\u9884\u89C8\uFF0C\u4E0D\u771F\u6B63\u521B\u5EFA\u3002
21893
22222
  \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
21894
- 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`,
22223
+ 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
22224
+ \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
22225
+ \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
22226
+ \u7269\u7406/\u6D77\u5173\u5B57\u6BB5\uFF1Aweight(\u514B)/length/height/width(\u5398\u7C73)/origin_country/hs_code/mid_code/material \u5747\u53EF\u9009\u3002`,
21895
22227
  schema: {
21896
22228
  title: external_exports.string(),
21897
22229
  handle: external_exports.string().optional().describe("\u4E0D\u586B\u5219\u7528 title \u5F52\u4E00\u5316\u751F\u6210"),
@@ -21909,6 +22241,21 @@ SKU / \u89C4\u683C\uFF1Asku \u5199\u5165\u9ED8\u8BA4\u53D8\u4F53\uFF1Bmetadata \
21909
22241
  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"),
21910
22242
  sku: external_exports.string().optional().describe("\u5199\u5165\u9ED8\u8BA4\u53D8\u4F53\u7684 SKU"),
21911
22243
  metadata: external_exports.record(external_exports.string(), external_exports.string()).optional().describe("\u4EFB\u610F\u952E\u503C\uFF0C\u5982\u89C4\u683C\u53C2\u6570 / source_url"),
22244
+ 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'),
22245
+ collection_id: external_exports.string().optional().describe('\u96C6\u5408 id \u6216\u540D\u79F0\uFF08\u5982 "\u53CC11\u6D3B\u52A8"\uFF09'),
22246
+ weight: external_exports.number().nonnegative().optional().describe("\u91CD\u91CF\uFF0C\u5355\u4F4D\u514B"),
22247
+ length: external_exports.number().nonnegative().optional().describe("\u957F\uFF0C\u5355\u4F4D\u5398\u7C73"),
22248
+ height: external_exports.number().nonnegative().optional().describe("\u9AD8\uFF0C\u5355\u4F4D\u5398\u7C73"),
22249
+ width: external_exports.number().nonnegative().optional().describe("\u5BBD\uFF0C\u5355\u4F4D\u5398\u7C73"),
22250
+ origin_country: external_exports.string().optional().describe("\u539F\u4EA7\u56FD ISO \u7801\uFF0C\u5982 KR/MY"),
22251
+ hs_code: external_exports.string().optional(),
22252
+ mid_code: external_exports.string().optional(),
22253
+ material: external_exports.string().optional(),
22254
+ manage_inventory: external_exports.boolean().default(true).describe("\u53D8\u4F53\u662F\u5426\u542F\u7528\u5E93\u5B58\u7BA1\u7406\uFF0C\u9ED8\u8BA4 true"),
22255
+ initial_stock: external_exports.object({
22256
+ location_id: external_exports.string().optional().describe("\u4ED3\u5E93 id\uFF1B\u7701\u7565\u5219 Main Warehouse/\u7B2C\u4E00\u4E2A\u4ED3\u5E93"),
22257
+ quantity: external_exports.number().int().nonnegative()
22258
+ }).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"),
21912
22259
  confirm: external_exports.boolean().default(false),
21913
22260
  acknowledge_missing_languages: external_exports.boolean().default(false)
21914
22261
  },
@@ -21922,6 +22269,29 @@ SKU / \u89C4\u683C\uFF1Asku \u5199\u5165\u9ED8\u8BA4\u53D8\u4F53\uFF1Bmetadata \
21922
22269
  const missing = config3.requiredLocales.filter(
21923
22270
  (l) => !provided.includes(l) || !input.translations?.[l]?.title
21924
22271
  );
22272
+ let channelIds = [];
22273
+ if (input.sales_channels?.length) {
22274
+ const ch = await client2.listSalesChannels({ limit: 100 });
22275
+ const list = ch?.sales_channels || [];
22276
+ channelIds = input.sales_channels.map((name) => list.find((c) => c.id === name || c.name === name)?.id).filter(Boolean);
22277
+ const unknown2 = input.sales_channels.filter(
22278
+ (name) => !list.some((c) => c.id === name || c.name === name)
22279
+ );
22280
+ 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`;
22281
+ }
22282
+ let collectionId = input.collection_id;
22283
+ if (collectionId && !collectionId.startsWith("pcol_")) {
22284
+ const cols = await client2.listCollections({ limit: 100 });
22285
+ const hit = (cols?.collections || []).find((c) => c.id === collectionId || c.title === collectionId);
22286
+ if (!hit) return `\u9519\u8BEF\uFF1A\u627E\u4E0D\u5230\u96C6\u5408 "${collectionId}"\uFF08\u7528 list_collections \u67E5\u770B\u53EF\u7528\u96C6\u5408\uFF09`;
22287
+ collectionId = hit.id;
22288
+ }
22289
+ const warnings = [];
22290
+ if (input.manage_inventory !== false && !input.initial_stock) {
22291
+ warnings.push(
22292
+ "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"
22293
+ );
22294
+ }
21925
22295
  if (!input.confirm) {
21926
22296
  return {
21927
22297
  dry_run: true,
@@ -21935,8 +22305,14 @@ SKU / \u89C4\u683C\uFF1Asku \u5199\u5165\u9ED8\u8BA4\u53D8\u4F53\uFF1Bmetadata \
21935
22305
  sku: input.sku,
21936
22306
  images: input.images,
21937
22307
  metadata: input.metadata,
22308
+ sales_channels: channelIds,
22309
+ collection_id: collectionId || null,
22310
+ weight: input.weight,
22311
+ manage_inventory: input.manage_inventory,
22312
+ initial_stock: input.initial_stock,
21938
22313
  missing_translations: missing
21939
22314
  },
22315
+ warnings,
21940
22316
  note: "\u672A confirm\uFF0C\u4EC5\u9884\u89C8\u3002\u786E\u8BA4\u8BF7\u8BBE confirm=true\u3002"
21941
22317
  };
21942
22318
  }
@@ -21961,41 +22337,66 @@ SKU / \u89C4\u683C\uFF1Asku \u5199\u5165\u9ED8\u8BA4\u53D8\u4F53\uFF1Bmetadata \
21961
22337
  };
21962
22338
  if (input.metadata) payload.metadata = input.metadata;
21963
22339
  if (imageUrls.length) payload.images = imageUrls.map((url) => ({ url }));
22340
+ if (channelIds.length) payload.sales_channels = channelIds.map((id) => ({ id }));
22341
+ if (collectionId) payload.collection_id = collectionId;
22342
+ for (const k of ["weight", "length", "height", "width"]) {
22343
+ if (input[k] != null) payload[k] = input[k];
22344
+ }
22345
+ for (const k of ["origin_country", "hs_code", "mid_code", "material"]) {
22346
+ if (input[k]) payload[k] = input[k];
22347
+ }
21964
22348
  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;
21965
22349
  if (variantPrices) {
21966
22350
  const variant = {
21967
22351
  title: "Default",
21968
22352
  options: { Default: "Default" },
21969
22353
  prices: variantPrices,
21970
- manage_inventory: false
22354
+ manage_inventory: input.manage_inventory
21971
22355
  };
21972
22356
  if (input.sku) variant.sku = input.sku;
21973
22357
  payload.variants = [variant];
21974
22358
  }
21975
22359
  const data = await client2.createProduct(payload);
22360
+ const productId = data?.product?.id || data?.id;
21976
22361
  let translationResult = null;
21977
22362
  if (input.translations && Object.keys(input.translations).length) {
21978
- const productId = data?.product?.id || data?.id;
21979
- if (productId) {
21980
- const create = Object.entries(input.translations).map(([locale, t]) => ({
21981
- reference: "product",
21982
- reference_id: productId,
21983
- locale_code: locale,
21984
- translations: { title: t?.title, description: t?.description }
21985
- }));
21986
- try {
21987
- translationResult = await client2.batchTranslations(create);
21988
- } catch (e) {
21989
- return { created: data, warning_translations: `\u7FFB\u8BD1\u5199\u5165\u5931\u8D25 \u2014\u2014 ${e?.message || e}` };
21990
- }
22363
+ const create = Object.entries(input.translations).map(([locale, t]) => ({
22364
+ reference: "product",
22365
+ reference_id: productId,
22366
+ locale_code: locale,
22367
+ translations: { title: t?.title, description: t?.description }
22368
+ }));
22369
+ try {
22370
+ translationResult = await client2.batchTranslations(create);
22371
+ } catch (e) {
22372
+ return { created: data, warning_translations: `\u7FFB\u8BD1\u5199\u5165\u5931\u8D25 \u2014\u2014 ${e?.message || e}` };
21991
22373
  }
21992
22374
  }
21993
- return { created: data, translations: translationResult, warning_missing_languages: missing };
22375
+ let stock = null;
22376
+ if (input.initial_stock && productId) {
22377
+ try {
22378
+ stock = await applyProductStock(
22379
+ { client: client2, config: config3 },
22380
+ {
22381
+ productId,
22382
+ sku: input.sku,
22383
+ locationId: input.initial_stock.location_id,
22384
+ quantity: input.initial_stock.quantity,
22385
+ manageInventory: input.manage_inventory
22386
+ }
22387
+ );
22388
+ } catch (e) {
22389
+ 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}`);
22390
+ }
22391
+ }
22392
+ return { created: data, translations: translationResult, stock, warnings, warning_missing_languages: missing };
21994
22393
  }
21995
22394
  },
21996
22395
  {
21997
22396
  name: "update_product",
21998
- 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",
22397
+ 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
22398
+ weight \u5355\u4F4D\u514B\uFF1Blength/height/width \u5355\u4F4D\u5398\u7C73\uFF1Borigin_country \u7528 ISO \u7801\uFF08\u5982 KR\uFF09\u3002
22399
+ 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`,
21999
22400
  schema: {
22000
22401
  id: external_exports.string(),
22001
22402
  title: external_exports.string().optional(),
@@ -22004,6 +22405,16 @@ SKU / \u89C4\u683C\uFF1Asku \u5199\u5165\u9ED8\u8BA4\u53D8\u4F53\uFF1Bmetadata \
22004
22405
  subtitle: external_exports.string().optional(),
22005
22406
  description: external_exports.string().optional(),
22006
22407
  translations: external_exports.record(external_exports.string(), translationShape).optional(),
22408
+ collection_id: external_exports.string().optional().describe("\u96C6\u5408 id \u6216\u540D\u79F0"),
22409
+ weight: external_exports.number().nonnegative().optional().describe("\u91CD\u91CF\uFF0C\u5355\u4F4D\u514B"),
22410
+ length: external_exports.number().nonnegative().optional().describe("\u957F\uFF0C\u5355\u4F4D\u5398\u7C73"),
22411
+ height: external_exports.number().nonnegative().optional().describe("\u9AD8\uFF0C\u5355\u4F4D\u5398\u7C73"),
22412
+ width: external_exports.number().nonnegative().optional().describe("\u5BBD\uFF0C\u5355\u4F4D\u5398\u7C73"),
22413
+ origin_country: external_exports.string().optional(),
22414
+ hs_code: external_exports.string().optional(),
22415
+ mid_code: external_exports.string().optional(),
22416
+ material: external_exports.string().optional(),
22417
+ 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"),
22007
22418
  confirm: external_exports.boolean().default(false)
22008
22419
  },
22009
22420
  async run(input, { client: client2 }) {
@@ -22017,6 +22428,27 @@ SKU / \u89C4\u683C\uFF1Asku \u5199\u5165\u9ED8\u8BA4\u53D8\u4F53\uFF1Bmetadata \
22017
22428
  if (input.status) patch.status = input.status;
22018
22429
  if (input.subtitle) patch.subtitle = input.subtitle;
22019
22430
  if (input.description) patch.description = input.description;
22431
+ if (input.collection_id) {
22432
+ let cid = input.collection_id;
22433
+ if (!cid.startsWith("pcol_")) {
22434
+ const cols = await client2.listCollections({ limit: 100 });
22435
+ const hit = (cols?.collections || []).find((c) => c.id === cid || c.title === cid);
22436
+ if (!hit) return `\u9519\u8BEF\uFF1A\u627E\u4E0D\u5230\u96C6\u5408 "${cid}"\uFF08\u7528 list_collections \u67E5\u770B\u53EF\u7528\u96C6\u5408\uFF09`;
22437
+ cid = hit.id;
22438
+ }
22439
+ patch.collection_id = cid;
22440
+ }
22441
+ for (const k of ["weight", "length", "height", "width"]) {
22442
+ if (input[k] != null) patch[k] = input[k];
22443
+ }
22444
+ for (const k of ["origin_country", "hs_code", "mid_code", "material"]) {
22445
+ if (input[k]) patch[k] = input[k];
22446
+ }
22447
+ if (input.metadata && Object.keys(input.metadata).length) {
22448
+ const cur = await client2.getProduct(input.id).catch(() => null);
22449
+ const existingMeta = (cur?.product ?? cur)?.metadata || {};
22450
+ patch.metadata = { ...existingMeta, ...input.metadata };
22451
+ }
22020
22452
  if (!input.confirm) return { dry_run: true, will_update: patch };
22021
22453
  const data = await client2.updateProduct(input.id, patch);
22022
22454
  let translationResult = null;
@@ -22211,77 +22643,6 @@ storefront \u8BE6\u60C5\u9875\u300C\u5BCC\u5185\u5BB9\u300D\u533A\u7531\u8FD9\u4
22211
22643
  }
22212
22644
  ];
22213
22645
 
22214
- // src/tools/stock.ts
22215
- var stockTools = [
22216
- {
22217
- name: "list_stock_locations",
22218
- description: "\u5217\u51FA\u4ED3\u5E93\uFF08Medusa StockLocation\uFF09\u3002\u53EF\u9009\u5173\u952E\u8BCD q \u8FC7\u6EE4\u540D\u79F0\u3002",
22219
- schema: {
22220
- q: external_exports.string().optional(),
22221
- limit: external_exports.number().int().positive().max(100).default(50)
22222
- },
22223
- async run(input, { client: client2 }) {
22224
- return client2.listStockLocations({ q: input.q, limit: input.limit });
22225
- }
22226
- },
22227
- {
22228
- name: "create_stock_location",
22229
- description: "\u589E\u52A0\u4E00\u4E2A\u4ED3\u5E93\uFF08StockLocation\uFF09\u3002\u9ED8\u8BA4 dry_run\u3002",
22230
- schema: {
22231
- name: external_exports.string(),
22232
- address: external_exports.object({
22233
- address_1: external_exports.string().optional(),
22234
- address_2: external_exports.string().optional(),
22235
- city: external_exports.string().optional(),
22236
- country_code: external_exports.string().optional(),
22237
- province: external_exports.string().optional(),
22238
- postal_code: external_exports.string().optional(),
22239
- phone: external_exports.string().optional()
22240
- }).optional(),
22241
- metadata: external_exports.record(external_exports.string(), external_exports.string()).optional(),
22242
- confirm: external_exports.boolean().default(false)
22243
- },
22244
- async run(input, { client: client2 }) {
22245
- const payload = { name: input.name };
22246
- if (input.address) payload.address = input.address;
22247
- if (input.metadata) payload.metadata = input.metadata;
22248
- if (!input.confirm) return { dry_run: true, will_create: payload };
22249
- return client2.createStockLocation(payload);
22250
- }
22251
- },
22252
- {
22253
- name: "get_inventory_levels",
22254
- description: "\u67E5\u8BE2\u67D0\u4ED3\u5E93\u4E0B\u7684\u5E93\u5B58\u6761\u76EE\u53CA\u5E93\u5B58\u6C34\u5E73\uFF08\u6309 location_id\uFF09\u3002",
22255
- schema: {
22256
- location_id: external_exports.string(),
22257
- q: external_exports.string().optional(),
22258
- limit: external_exports.number().int().positive().max(100).default(50)
22259
- },
22260
- async run(input, { client: client2 }) {
22261
- return client2.listInventoryItems(input.location_id, { q: input.q, limit: input.limit });
22262
- }
22263
- },
22264
- {
22265
- name: "update_inventory_level",
22266
- description: `\u4FEE\u6539\u67D0\u5E93\u5B58\u6761\u76EE\u5728\u6307\u5B9A\u4ED3\u5E93\u7684\u5E93\u5B58\u6570\u91CF\uFF08stocked_quantity\uFF09\u3002
22267
- \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`,
22268
- schema: {
22269
- inventory_item_id: external_exports.string(),
22270
- location_id: external_exports.string(),
22271
- stocked_quantity: external_exports.number().int().nonnegative(),
22272
- confirm: external_exports.boolean().default(false)
22273
- },
22274
- async run(input, { client: client2 }) {
22275
- const payload = {
22276
- location_id: input.location_id,
22277
- stocked_quantity: input.stocked_quantity
22278
- };
22279
- if (!input.confirm) return { dry_run: true, will_update: payload };
22280
- return client2.updateInventoryLevel(input.inventory_item_id, payload);
22281
- }
22282
- }
22283
- ];
22284
-
22285
22646
  // src/tools/ops.ts
22286
22647
  var opsTools = [
22287
22648
  {
@@ -22461,7 +22822,7 @@ var config2 = loadConfig();
22461
22822
  var auth = new AuthManager(config2);
22462
22823
  var client = new MedusaAdminClient(config2, auth);
22463
22824
  var ctx = { client, config: config2 };
22464
- var server = new McpServer({ name: "derlux-admin", version: "0.1.1" });
22825
+ var server = new McpServer({ name: "derlux-admin", version: "0.1.3" });
22465
22826
  for (const def of toolDefs) {
22466
22827
  server.tool(def.name, def.description, def.schema, async (args) => {
22467
22828
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sea-ecommerce/admin-mcp-server",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "让运营同学用本地 AI agent(Codex / Workbuddy / Claude / Cursor / CodeBuddy)运营 Medusa 后台的 MCP server。",
package/skill/SKILL.md CHANGED
@@ -31,6 +31,8 @@ description: 运营使用本地 AI agent(derlux-admin MCP server)操作 DERL
31
31
  - 商品标题/描述的**翻译**(`create_product` 的 `translations`、后端 `/admin/translations/batch`):用**简码** `en` / `zh` / `ms` / `id`。store `/store/products?locale=zh` 即回退到 zh 翻译。
32
32
  - 商品详情页**富内容块**(`create_product_content` 的 `locale`):必须用**区域码** `en-US` / `zh-CN` / `ms-MY` / `id-ID`(与前端 `useProductContent` 的 localeMap 一致)。用简码会导致该语言详情页读不到内容。
33
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。
34
36
 
35
37
  ## 工具速查表
36
38
 
@@ -38,8 +40,12 @@ description: 运营使用本地 AI agent(derlux-admin MCP server)操作 DERL
38
40
  |---|---|---|---|
39
41
  | `list_products` | 读 | 否 | 直接返回 |
40
42
  | `get_product` | 读 | 否 | 直接返回 |
43
+ | `list_sales_channels` | 读 | 否 | 直接返回(渠道 id/名称,建商品前确认用)|
44
+ | `list_collections` | 读 | 否 | 直接返回 |
45
+ | `get_translations` | 读 | 否 | 直接返回(某商品各语言的 title/description,多语言核对唯一可靠途径)|
41
46
  | `list_stock_locations` | 读 | 否 | 直接返回 |
42
- | `get_inventory_levels` | 读 | 否 | 直接返回(按 location_id 列库存条目与水平)|
47
+ | `get_inventory_levels` | 读 | 否 | 直接返回(location_id 可选:按仓库列库存条目与水平;q 按 sku/title 过滤)|
48
+ | `set_product_stock` | 写 | 是(`confirm=true` 才写)| 幂等建仓:自动找/建库存条目 + 写仓库数量 + manage_inventory;`confirm=false` 仅预览 |
43
49
  | `list_orders` | 读 | 否 | 直接返回 |
44
50
  | `get_order` | 读 | 否 | 直接返回 |
45
51
  | `list_warehouse_tasks` | 读 | 否 | 直接返回(自定义端点)|
@@ -63,12 +69,18 @@ description: 运营使用本地 AI agent(derlux-admin MCP server)操作 DERL
63
69
  - `q`(string,可选)关键词;`status`(enum:`draft|published|proposed|rejected|archived`,可选)状态过滤;`handle`(string,可选);`limit`(int 1–100,默认 20)。
64
70
  - **`get_product`**:按商品 id 或 handle 获取详情。
65
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)。
66
78
 
67
79
  ### 读 —— 仓库与库存
68
80
  - **`list_stock_locations`**:列出仓库(StockLocation)。
69
81
  - `q`(string,可选)按名称过滤;`limit`(int 1–100,默认 50)。
70
- - **`get_inventory_levels`**:列出某仓库下的库存条目及库存水平。
71
- - `location_id`(string,**必填**);`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)。
72
84
 
73
85
  ### 读 —— 订单 / 履约 / 退货
74
86
  - **`list_orders`**:查询订单列表。
@@ -88,10 +100,12 @@ description: 运营使用本地 AI agent(derlux-admin MCP server)操作 DERL
88
100
 
89
101
  ### 写 —— 商品
90
102
  - **`create_product`**:新增商品。
91
- - `title`(string,**必填**);`handle`(string,可选,缺省由 title 归一化);`status`(enum,默认 `draft`);`subtitle`(可选);`description`(可选);`price`(number ≥0,主货币单位,**切勿 ×100**);`currency_code`(默认 `MYR`);`translations`(`record<locale,{title?,description?}>`,建议覆盖 `en/zh/ms/id`);`images`(string[],可选,本地图片路径或图片 URL,工具自动上传并挂接);`sku`(string,可选,写入默认变体 SKU);`metadata`(`record<string,string>`,可选,存规格参数/来源链接等任意键值);`confirm`(bool,默认 false);`acknowledge_missing_languages`(bool,默认 false)。
92
- - 行为:`confirm=false` 返回预览(含缺语言清单);`confirm=true` 才真正创建;缺语言时须同时 `acknowledge_missing_languages=true`。创建时**自动**补上 Medusa v2 必需的 `options`(单一 Default)、把图片上传到文件服务并挂接、把 `sku` 写入默认变体、把 `translations` 写入 v2.21 专用的 `/admin/translations/batch` 端点。
93
- - **`update_product`**:修改商品字段(标题/handle/状态/描述/多语言)。
94
- - `id`(string,**必填**);`title`/`handle`/`status`/`subtitle`/`description`/`translations`(均可选);`confirm`(默认 false)。`confirm=false` 返回预览。
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` 返回合并后的完整预览。
95
109
  - **`deactivate_product`**:下架商品(status 置 `archived`)。
96
110
  - `id`(string,**必填**);`confirm`(默认 false)。`confirm=false` 返回预览。
97
111
  - **`upload_product_image`**:给**已存在**的商品追加图片(与已有图片合并,不覆盖)。
@@ -114,39 +128,44 @@ description: 运营使用本地 AI agent(derlux-admin MCP server)操作 DERL
114
128
  ### 写 —— 仓库与库存
115
129
  - **`create_stock_location`**:新增仓库(StockLocation)。
116
130
  - `name`(string,**必填**);`address`(可选:`{address_1?,address_2?,city?,country_code?,province?,postal_code?,phone?}`);`metadata`(`record<string,string>`,可选);`confirm`(默认 false)。
117
- - **`update_inventory_level`**:修改某库存条目在指定仓库的库存数量(`stocked_quantity`)。
118
- - `inventory_item_id`(string,**必填**);`location_id`(string,**必填**);`stocked_quantity`(int ≥0,**必填**);`confirm`(默认 false)。底层走 `POST /admin/inventory-items/{id}/location-levels`,字段/端点以 Medusa 2.21 契约为准。
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
+ - 行为(`manage_inventory=true`):按变体 SKU 查库存条目(缺失则自动创建)→ **关联条目到变体**(`POST .../variants/{id}/inventory-items`,已关联则忽略)→ 置变体 `manage_inventory=true` → 幂等写 (条目, 仓库) 库存水平。重复调用安全。
134
+ - ⚠️ 「关联」是变体能查到库存的关键:只建条目不关联,前台仍显示缺货。本工具已自动处理关联,运营无需关心。
135
+ - (`manage_inventory=false`)关闭库存管理并把库存清零。
136
+ - **`update_inventory_level`**:直接改某**库存条目**(`inventory_item_id`)在某仓库的数量(upsert,幂等)。
137
+ - `inventory_item_id`(string,**必填**);`location_id`(string,**必填**);`stocked_quantity`(int ≥0,**必填**);`confirm`(默认 false)。已知 `inventory_item_id` 时用;否则优先 `set_product_stock`。
119
138
 
120
139
  ## 推荐操作流程(SOP)
121
140
 
122
141
  **创建商品**
123
- 1. 先收集:标题、handle(或告知由标题生成)、价格(主单位)、四语言标题、状态、图片、规格、详情页富文本内容(按语言)。
124
- 2. 调用 `create_product`(`confirm=false`)→ 展示预览与「缺语言」告警。
125
- 3. 提醒运营补全缺失语言;确认无误后调用 `create_product`(`confirm=true`,必要时 `acknowledge_missing_languages=true`)。
126
- 4. **`add_product_to_sales_channel`(`confirm=true`)** → 把商品加入销售渠道,否则前端看不到。
127
- 5. **`create_product_content`(`confirm=true`,逐语言 `en-US/zh-CN/ms-MY/id-ID`)** → 写详情页富内容块(richtext 描述 + specs 规格 + image_gallery 图集)。否则详情页主体空白。
128
- 6. 把返回的新商品 id/handle 告知运营。
142
+ 1. 先收集:标题、handle(或告知由标题生成)、价格(主单位)、四语言标题、状态、图片、规格、重量、初始库存数量、目标渠道、详情页富文本内容(按语言)。
143
+ 2. 调用 `create_product`(`confirm=false`)→ 展示预览与「缺语言 / 缺货」告警。
144
+ 3. 提醒运营补全缺失语言与 `initial_stock`;确认无误后调用 `create_product`(`confirm=true`,`sales_channels` 里带上目标渠道,必要时 `acknowledge_missing_languages=true`)。创建即完成:图片上传、SKU、翻译、渠道挂载、建库存。
145
+ 4. **`create_product_content`(`confirm=true`,逐语言 `en-US/zh-CN/ms-MY/id-ID`)** → 写详情页富内容块(richtext 描述 + specs 规格 + image_gallery 图集)。否则详情页主体空白。
146
+ 5. 用 `get_translations` + `get_product_content` 核对多语言与富内容;把新商品 id/handle 告知运营。
129
147
 
130
148
  **修改 / 下架商品**
131
149
  1. `get_product` 或 `list_products` 确认目标 id。
132
150
  2. `update_product` / `deactivate_product`(`confirm=false`)→ 展示预览。
133
151
  3. 确认后同名工具 `confirm=true` 执行。
134
152
 
135
- **调整库存**
136
- 1. `list_stock_locations` 拿到 `location_id`;`get_inventory_levels` 拿到 `inventory_item_id`。
137
- 2. `update_inventory_level`(`confirm=false`)→ 展示将写入的数量。
138
- 3. 确认后 `confirm=true` 执行。
153
+ **建仓 / 调整库存**
154
+ 1. 直接 `set_product_stock`(`product_id` + `quantity`,`confirm=false` 预览 → 确认后 `confirm=true`)——自动处理库存条目与仓库。
155
+ 2. 查现状:`get_inventory_levels`(`location_id` 可选、`q` 按 sku 过滤)。
156
+ 3. 已知 `inventory_item_id` 的精细调整:`update_inventory_level`(upsert)。
139
157
 
140
158
  **查询类**:直接调用对应读工具即可,无需 `confirm`。
141
159
 
142
160
  ## 常见任务示例(运营怎么说 / 你怎么调)
143
161
 
144
162
  - 「下架 handle 为 `summer-serum` 的商品」→ `get_product`(id_or_handle=`summer-serum`) 取 id → `deactivate_product`(id, confirm=false 预览 → 确认后 confirm=true)。
145
- - 「给马来西亚仓的 A 商品加 50 件库存」→ `list_stock_locations` 找 MY 仓 → `get_inventory_levels`(location_id) 找 A 的 inventory_item_id → `update_inventory_level`(confirm=false 预览 → 确认后 confirm=true)。
163
+ - 「给 A 商品补 100 件库存」→ `set_product_stock`(product_id=A 的 id, quantity=100, confirm=false 预览 → 确认后 confirm=true;默认写 Main Warehouse,指定仓传 location_id)。
146
164
  - 「查一下最近 10 个待发货订单」→ `list_orders`(status=`pending`, limit=10)。
147
165
  - 「后台怎么新建优惠券」→ `ask_usage`(question=`如何新建优惠券`)。
148
166
  - 「列出所有退货请求」→ `list_return_requests`()。
149
- - 「把这份抓取的 Lazada 商品录入:标题 DR.CYJ Hair Filler、价格 420 MYR、4 张本地图 `/Users/fire/temp/images/product_1..4.webp`、SKU `4820...`、规格存 metadata、并补全中/马来/印尼语翻译」→ 先整理数据,再 `create_product`(`confirm=false` 看预览含 images/sku/metadata/缺语言清单 → 确认后 `confirm=true`,必要时 `acknowledge_missing_languages=true`)。工具会自动上传图片、写 SKU/metadata、并把四语言简码翻译写入翻译端点。随后 `add_product_to_sales_channel`(`confirm=true`) 让前端可见,再 `create_product_content`(`locale=en-US/zh-CN/ms-MY/id-ID`, `confirm=true`) 逐语言写富内容块(richtext 描述 + specs 规格 + image_gallery 图集)。
167
+ - 「把这份抓取的 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` 核对。
168
+ - 「核对某商品多语言是否齐全」→ `get_translations`(reference_id=商品 id 或 handle),看 4 条 locale 是否都有 title/description。
150
169
  - 「让这个商品在前端能搜到」→ `get_product` 取 id → `add_product_to_sales_channel`(`product_ids=[id]`, `confirm=true`)。
151
170
  - 「补全某商品中文详情页内容」→ `create_product_content`(`product_id`, `locale=zh-CN`, `blocks=[{type:'richtext',data:{html:'...'}},{type:'specs',data:{rows:[...]}}]`, `confirm=true`)。可用 `get_product_content` 先核对现状。
152
171
 
@@ -189,9 +208,10 @@ agent 客户端(Codex / Claude Desktop / Cursor / Workbuddy / CodeBuddy 等)
189
208
  - 登录走**用户账号**而非 API Key(现有 RBAC 依赖 `user.metadata.rbac_roles`)。
190
209
  - 金额以**主货币单位**存储(见上「硬规则 2」),不要换算成分。
191
210
  - **Medusa v2.21 差异(重要)**:
192
- - 有变体就必须有 product options,工具已自动补一个 `Default` 选项,你无需关心。
193
- - 商品创建/更新体**不接受 `translations` 字段**;翻译由工具自动走 `/admin/translations/batch`。`get_product` 默认字段不含 `translations` 关系,想核对翻译请用 `ask_usage` 或后台「多语言」面板,不要凭 `get_product` 返回为空就误判没写入。
211
+ - 有变体就必须有 product options,工具已自动补一个 `Default` 选项,你无需关心;**更新**商品时 `options` 字段被移除(2.16+),工具不会误传。
212
+ - 商品创建/更新体**不接受 `translations` 字段**;翻译由工具自动走 `/admin/translations/batch`。`get_product` 默认字段不含 `translations` 关系,**核对翻译请用 `get_translations`**,不要凭 `get_product` 返回为空就误判没写入。
194
213
  - 图片走 `/admin/uploads` 上传后再挂到商品(无独立 `/admin/products/{id}/images` 端点)。
195
- - 库存写入的端点/字段以 Medusa 2.21 契约为准;自定义端点 `warehouse-tasks` / `return-requests` 为本项目扩展。
214
+ - 库存查询/写入端点已按 2.21 契约校准(`/admin/inventory-items` + `location-levels`,sku 唯一约束);`set_product_stock` 会**关联条目↔变体**并幂等可重放(变体只有关联到库存条目,前台才显示有货;只建条目不关联会撞唯一约束或前台缺货,工具已自动处理)。
215
+ - 自定义端点 `warehouse-tasks` / `return-requests` / `product-contents` 为本项目扩展。
196
216
  - 文档问答(`search_docs` / `ask_usage`)依赖知识源目录;npx 形态下默认 `./docs`,若问不到相关内容,提示运营换关键词或查后台手册。
197
217
  - 用受限的 `csr` 账号即可完成日常运营;敏感动作由后端 RBAC 拦截。