npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@chengzzzi44/dsh-shopify

v0.1.2

Published

Shopify Admin GraphQL tools and a settings card for DeepSeek Harness: 8 read-only queries plus 10 opt-in writes covering the product, inventory, and image workflow.

Readme

@chengzzzi44/dsh-shopify

DeepSeek Harness 的 Shopify 插件:让模型直接读写你店铺的商品、订单、客户和库存,走 Shopify 官方的 Admin GraphQL API

  • 18 个工具:8 个只读 + 10 个写入
  • 写入默认关闭enableWrites 为 false 时,10 个写工具直接拒绝、一个请求都不发;要在设置卡片里打开「允许写入」才生效
  • 不碰的东西:订单(发货、取消、退款)、客户、折扣、礼品卡——这些写操作一律没有
  • 商品侧闭环:新建草稿 → 上架发布 → 写库存 → 传图/挂图,都在插件里

工具

只读工具(8 个,随时可用)

| 工具 | 作用 | 需要的 Admin scope | |---|---|---| | shopify_shop | 店铺名称、myshopify 域名、主域名、币种、时区 | 无 | | shopify_products | 搜索/列商品(支持 Shopify 查询语法),返回 GID、价格区间、库存、状态、上架时间 | read_products | | shopify_products_summary | 一次调用给出全店概览:各状态数量、上架但零库存的数量、最新上架与最新草稿 | read_products | | shopify_product | 单个商品详情:选项、变体(SKU/价格/库存)、SEO、所属系列 | read_products | | shopify_orders | 搜索/列订单(按支付状态、履约状态、时间、标签等) | read_orders | | shopify_order | 单个订单:行项目、金额拆分、客户、履约与物流单号 | read_orders | | shopify_customers | 搜索/列客户:邮箱、订单数、累计消费、标签 | read_customers | | shopify_inventory | 按 SKU 查各库位的可用/在库/占用/在途数量 | read_inventory + read_locations |

写工具(10 个,默认全部关闭)

| 工具 | 作用 | 需要的 Admin scope | |---|---|---| | shopify_product_create | 新建商品,一次带上选项、变体、价格、图片、初始库存;状态默认 DRAFT | write_products(带库存还要 write_inventory) | | shopify_product_update | 改状态(草稿上架/下架/归档)、标签、标题、供应商、类型,以及按变体改价 | write_products | | shopify_product_publish | 发布到销售渠道("顾客能看到"的那一步),默认 Online Store | write_products | | shopify_product_delete | 永久删除一个商品 | write_products | | shopify_product_image_add | 给商品加一张图——本地文件(先上传)或一个图片 URL | write_products + write_files | | shopify_file_upload | 本地图片(PNG/JPEG/WebP/GIF,≤20 MB)传进 Shopify Files | write_files | | shopify_file_delete | 按 GID 删除 Files 里的文件;删商品图对应的文件会同时把图从商品上摘掉 | write_files | | shopify_inventory_set | 把某 SKU 在某库位的数量设成绝对值 | write_inventory + read_inventory + read_locations | | shopify_inventory_adjust | 在某库位的现有数量上加减一个带符号的差值 | 同上 | | shopify_inventory_activate | 让某个变体在某库位首次入库(set 写不了没有库存记录的库位) | 同上 |

三个列表工具(shopify_products / shopify_orders / shopify_customers)都会在第一页附带 totalCount——即当前筛选条件下一共多少条,不用自己翻页数。想看全部就在参数里加 all: true,插件会自己翻页读完(一次最多 1000 行,超了返回 nextCursor 让你接着读):

// 一次读完所有已上架商品,按上架时间从新到旧
{ "query": "status:active", "sortKey": "PUBLISHED_AT", "reverse": true, "all": true }
// → products: 96 of 96

shopify_ordersstatus 过滤在 Shopify 那边执行(拼进搜索查询),所以返回的行数和 totalCount 永远是同一批订单。

全店概览一次调用就够:

products: 128 total · 96 active · 32 draft · 0 archived
active out of stock: 3
newest published: 2026-07-30 · Silk Peony Stem … · gid://shopify/Product/1234567890
newest draft: 2026-08-04 · Faux Banana Tree …

安装

前置条件:可用的 dsh + PATH 里有 pnpm

dsh plugin --profile web add @chengzzzi44/dsh-shopify

从本仓库源码安装:

git clone https://github.com/chengzzzi44/dsh-shopify.git
cd dsh-shopify && npm install
dsh plugin --profile web add "$PWD"

配置

推荐:在「设置 → 插件 → dsh-shopify」卡片里填(店铺域名、client id、client secret 三项)。卡片把域名和 client id 写进该插件的设置节,secret 写进凭据域($DSH_HOME/.credentials.yaml),都不回显密钥;保存后下一次工具调用立即生效,不用重启。

也可以走配置文件,凭据有两种写法,任选,也可以混用(字面值优先于同名的引用):

1. 直接写在插件配置里(配置文件本身就在 ~/.dsh/profiles/<profile>/cordis.patch.yml,不进任何仓库):

- id: dsh-shopify
  config:
    storeDomain: 'your-store.myshopify.com'
    clientId: '你的 client id'
    clientSecret: '你的 client secret'
    apiVersion: '2026-01'
    maxResults: 10

旧版静态 token 同理,写 accessToken: 'shpat_...' 即可,此时不会再走 client credentials 换 token。

行里写了 clientSecret 字面值就会盖过凭据域里的那份;要在卡片里填 secret,就别在配置文件里留 clientSecret

2. 只写引用名,值放环境/凭据域(适合要提交配置文件的场景):

# ~/.dsh/.env(或启动进程的环境变量)
SHOPIFY_STORE_DOMAIN=your-store.myshopify.com
SHOPIFY_CLIENT_ID=你的 client id
SHOPIFY_CLIENT_SECRET=你的 client secret
- id: dsh-shopify
  config:
    storeDomain: 'your-store.myshopify.com'
    clientIdRef: SHOPIFY_CLIENT_ID
    clientSecretRef: SHOPIFY_CLIENT_SECRET
    apiVersion: '2026-01'
    maxResults: 10

patch 会整体替换 config,要保留的键都得重写。

| 字段 | 默认 | 说明 | |---|---|---| | storeDomain | —(来自 SHOPIFY_STORE_DOMAIN) | 店铺的 *.myshopify.com 域名;未设置时工具仍注册,但每次调用都会明确报错 | | clientId | — | 直接写在配置里的 client id,优先于 clientIdRef | | clientSecret | — | 直接写在配置里的 client secret,优先于 clientSecretRef | | accessToken | — | 直接写在配置里的旧版静态 shpat_ token;设了就不再换 token | | clientIdRef | SHOPIFY_CLIENT_ID | 凭据引用名(不是密钥本身),clientId 为空时使用 | | clientSecretRef | SHOPIFY_CLIENT_SECRET | 同上 | | accessTokenRef | — | 凭据引用名,accessToken 为空时使用 | | apiVersion | 2026-01 | Admin API 版本,写在 URL 路径里 | | maxResults | 10 | 列表工具的默认行数,同时是硬上限(最大 50) | | requestTimeoutMs | 30000 | 单次请求预算,含响应体 | | enableWrites | false | 是否允许写工具改动店铺;默认关闭,卡片上有开关 |

设置节(命名空间 dsh-shopify)以插件行为基底:卡片里填过的字段进入用户层,没填的继续取 cordis.patch.yml / 环境变量。clientSecretaccessToken 标了 role('secret'),任何设置读取都拿不到它们的值。工具每次调用都会重读这份解析结果,所以保存后下一次查询就用新值,不用重启。

凭据怎么来:建一个 Shopify 应用

2026 年起新 app 在 Dev Dashboard 创建,用 client credentials:插件把 client id + secret 换成短期 access token(约 24 小时),缓存在内存里、过期前自动刷新,token 不落盘

  1. 打开 https://dev.shopify.com/dashboard → Apps → 新建 app
  2. Configuration → Admin API access scopes 里勾权限(见下)
  3. 发布(Release)这个版本——只改不发布不生效
  4. 到店铺后台 → 设置 → 应用,安装/授权这个 app
  5. Client ID / Client secret 填进插件设置卡片

只要查询,勾这几个就够(复制即用):

read_products, read_orders, read_customers, read_inventory, read_locations

要连写入一起用,再加:

write_products, write_files, write_inventory

旧版静态 token 也支持:老 custom app 的 shpat_ token 写到 accessToken / accessTokenRef 即可,此时不再换 token。

scope 改过之后要发布新版本并重新安装,否则换出来的 token 还是旧权限——这是最常见的"明明加了权限却还是 Access denied"的原因。

写入(默认关闭)

上面那 10 个写工具enableWrites 为 false 时全部直接拒绝、一个请求都不发,报错里会告诉你去哪打开。打开方式:设置 → 插件 → dsh-shopify 卡片里的「允许写入」,或者在配置里写 enableWrites: true

打开之后可以:

// 把草稿上架并打标签
{ "id": "gid://shopify/Product/123", "status": "ACTIVE", "tags": ["spring", "sale"] }
// 批量改变体价格
{ "id": "gid://shopify/Product/123", "variants": [{ "id": "gid://shopify/ProductVariant/456", "price": "29.99" }] }

几个刻意的设计:标签是整体替换(传什么就是什么,不传就不动);status 只接受 ACTIVE / DRAFT / ARCHIVED;价格必须是十进制字符串;Shopify 返回的 userErrors 会原样抛给模型(而不是假装成功);一次调用里改多个字段只发一条 mutation,价格另发一条。

注意:status: ACTIVE 只改商品状态。要让它在店铺前台可见,还得发布到销售渠道——用 shopify_product_publish(默认 Online Store,也可以 channels: ["Shop"] 指名)。

图片上传

同样受「允许写入」开关控制,关着时一个请求都不发。

// 传本地图到 Shopify Files
{ "path": "generated-images/product-1.png", "alt": "Front view" }
// → uploaded product-1.png · image/png · 245123 bytes · gid://shopify/MediaImage/123 · UPLOADED

// 直接给商品加图(本地文件会先自动上传)
{ "productId": "gid://shopify/Product/123", "path": "generated-images/product-1.png", "alt": "Front view" }
// 也可以传一个 Shopify 抓得到的图片 URL
{ "productId": "gid://shopify/Product/123", "url": "https://example.com/photo.jpg" }

path 支持绝对路径,或相对会话工作区的路径——所以生图插件刚生成到工作区里的图片可以直接喂进来。传错了用 shopify_file_delete 删:商品媒体背后的文件是同一个 GID,删文件就同时把图从商品上摘掉(一次最多 50 个)。上传走 Shopify 官方三步:stagedUploadsCreate 拿签名目标 → multipart 把字节发到暂存服务 → fileCreate 落库;给商品加图走 productUpdatemedia 参数。Shopify 处理图片是异步的,所以返回里 status 常常还是 PROCESSING,稍后自己会变 READY

库存写入

setadjust 是两个工具,不是同一个工具的两个参数——因为它们语义不同,混用会出事:set覆盖库位上的现有数字,adjust 才是在现有基础上加减。同步场景一般用 adjust,只有当你确定仓库真实数量时才用 set

// 加减(推荐的同步方式)
{ "sku": "SKU-1234", "location": "主仓库", "delta": 20 }
// 设定绝对值(覆盖)
{ "sku": "SKU-1234", "location": "主仓库", "quantity": 120 }
// 带并发保护:只有当前值正好是 120 时才写,否则 Shopify 拒绝
{ "sku": "SKU-1234", "location": "主仓库", "quantity": 100, "expectedQuantity": 120 }
  • location 可以写库位名(中文也行,大小写不敏感)或 GID;写错会把这家店实际有的库位名列在报错里。
  • skuinventoryItemId 二选一;两者都不给或都给都会被拒。
  • 数量名默认 available,可用 name 换成 on_hand 等;原因默认 correction
  • delta: 0、负数 quantity、未知的数量名/原因都会在发请求前被拒。
  • 没写 expectedQuantity 时会显式告诉 Shopify「本次覆盖」,写了就带上比对值——并发改动会被 Shopify 挡下来,而不是把别人的改动冲掉。

完整流水线:上传草稿 → 上架 → 库存

这三步在插件里已经闭环,全部在同一个「允许写入」开关下:

// 1) 新建草稿:选项 + 两个变体 + 价格 + SKU + 初始库存,一次调用
{
  "title": "Silk Peony Stem",
  "vendor": "Example Vendor",
  "tags": ["peony", "silk"],
  "options": [{ "name": "Size", "values": ["S", "M"] }],
  "variants": [
    { "optionValues": [{ "optionName": "Size", "name": "S" }], "price": "27.99", "sku": "PEONY-S",
      "quantities": [{ "locationId": "gid://shopify/Location/1", "quantity": 100 }] },
    { "optionValues": [{ "optionName": "Size", "name": "M" }], "price": "29.99", "sku": "PEONY-M" }
  ],
  "images": [{ "url": "https://example.com/peony.jpg", "alt": "Peony stem" }]
}
// → created … · DRAFT · /products/…   (默认草稿,不会自己跑到前台)

// 2) 上架:先把状态改成 ACTIVE,再发布到渠道(两步都要)
{ "id": "gid://shopify/Product/123", "status": "ACTIVE" }          // shopify_product_update
{ "productId": "gid://shopify/Product/123" }                        // shopify_product_publish,默认 Online Store
{ "productId": "gid://shopify/Product/123", "channels": ["Shop"] }  // 也可以指名渠道

// 3) 库存:已有库存记录的库位直接 set/adjust;新变体首次入库先 activate
{ "sku": "PEONY-M", "location": "主仓库", "quantity": 50 }
{ "sku": "PEONY-M", "location": "主仓库" }        // shopify_inventory_activate

几个刻意的设计:

  • 新建商品默认 DRAFT,要 ACTIVE 必须显式写 status: "ACTIVE"——上传流程不会不小心把东西推到前台。
  • status: ACTIVE ≠ 顾客能看到。Shopify 里这是两件事:状态 + 发布到渠道。所以上架是两步,shopify_product_publish 补的是后一半。
  • 声明 options 就必须给匹配的 variants,否则 Shopify 只会生成一个价格为 0 的默认变体(这是 productCreate 的经典坑,productSet 配上完整变体才不会有)。
  • shopify_product_delete 是永久删除,Shopify 没有回收站;只是想下架请用 status: DRAFT
  • 创建时带库存用的是 productSetinventoryQuantities,和后面 inventory_set 写的是同一份数据。

模型看到什么

  • 每个工具返回精简后的结构化 JSON(GID、标题、金额、状态、时间),render 再生成紧凑文本给模型看,例如:

    orders: 2
    - #1001 · gid://shopify/Order/1001 · 2026-09-10 · PAID / UNFULFILLED · 59.00 USD · Alice <[email protected]> · Dog Toy ×2 (DOG-S)
  • 金额一律 { amount, currency }(Shopify 的 Money 本来就是这个形状);numberOfOrders 这类 Int64 标量 Shopify 会序列化成字符串,插件已做兼容。

  • UI 卡片走通用卡片(读 kind: read、写 kind: edit、删除 kind: delete);presentationMeta 只带行数、是否还有下一页、改了几项这类计数,供将来做专用卡片。

已知限制

  • 订单、客户、折扣、礼品卡没有写操作:插件只能读它们。发货、取消订单、退款都做不到。
  • shopify_product_delete 不可撤销:Shopify 没有回收站。想下架用 status: DRAFT,别用删除。
  • "上架"是两步status: ACTIVE 只改状态,还要 shopify_product_publish 落到渠道,顾客才看得到。
  • all: true 有上限:一次最多读 1000 行,超了返回 nextCursor 让你接着读;单页上限 250。
  • 受 Shopify 限流约束:Admin GraphQL 按 query cost 计费的漏桶;一次请求太大或太频繁会被拒(错误信息里会带 THROTTLED),把 maxResults 调小或稍后重试。
  • 客户数据是 PIIshopify_customers 会返回邮箱等个人信息,注意你的部署里谁能看到会话记录。
  • 只支持 myshopify 域名:Admin API 不能直接用自定义域名访问,storeDomain 必须是 xxx.myshopify.com
  • 没有工具结果卡片:浏览器半边只注册「设置 → 插件」里的 dsh-shopify 配置卡片,工具结果仍走通用卡片。

开发

npm install     # 装依赖并执行 prepare 构建 lib/
npm test        # 无密钥自测:Host 半边跑本地假 Shopify 服务;浏览器半边装进 __ModuleLoader__ 驱动设置卡片
npm run typecheck

npm test 不起真实网络、不需要任何凭据:Host 侧把 globalThis.fetch 路由到本地假服务,客户端侧用无 DOM 的 React 替身驱动控制器。

License

MIT