{"_rev":"8-c1976c40b1da61a5f69cbb891ce112d9","time":{"created":"2026-09-12T09:38:29.998Z","modified":"2026-09-12T09:38:30.549Z","0.1.0":"2026-09-12T07:06:49.747Z","0.1.1":"2026-09-12T07:11:12.955Z","0.1.2":"2026-09-12T09:38:30.303Z"},"_id":"@chengzzzi44/dsh-shopify","name":"@chengzzzi44/dsh-shopify","dist-tags":{"latest":"0.1.2"},"versions":{"0.1.2":{"name":"@chengzzzi44/dsh-shopify","version":"0.1.2","type":"module","description":"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.","license":"MIT","author":"chengzzzi44","repository":{"type":"git","url":"git+https://github.com/chengzzzi44/dsh-shopify.git"},"homepage":"https://github.com/chengzzzi44/dsh-shopify#readme","bugs":{"url":"https://github.com/chengzzzi44/dsh-shopify/issues"},"keywords":["deepseek-harness","dsh","dsh-plugin","shopify","ecommerce","cordis"],"engines":{"node":">=22.19"},"main":"lib/index.js","exports":{".":"./lib/index.js","./client":"./lib/client.js","./package.json":"./package.json"},"dsh":{"bundle":{"patch":"./cordis.patch.yml"},"client":{"platform":"web"}},"publishConfig":{"access":"public"},"dependencies":{"@deepseek-ai/schemastery":"^3.18.2"},"devDependencies":{"@types/node":"^22.10.0","@types/react":"^18.3.0","esbuild":"^0.25.0","typescript":"^5.7.0"},"scripts":{"build":"node build.mjs","typecheck":"tsc --noEmit","selftest":"node scripts/selftest.mjs && node scripts/client-selftest.mjs","test":"npm run selftest"},"_id":"@chengzzzi44/dsh-shopify@0.1.2","dist":{"integrity":"sha512-AOMjm0HQTgHLZ9j/2yd2DAC12SwRa7UPRdNdpqVTbypYo3JjJAxF/Pq9+nGMsMMrWRzcMxOgAaDRicPxtQ3XYQ==","shasum":"e1bb0b693d3a0fdf8d09d5a3913644c663c45c77","tarball":"https://registry.npmjs.org/@chengzzzi44/dsh-shopify/-/dsh-shopify-0.1.2.tgz","fileCount":7,"unpackedSize":190654,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDWtIE1u7YY9i9EC6UV1aqjJSI5uQUspwNRcx4m3w+Y4gIhAJRw+fp24fHCL6PkCaJMveByzTehSJw6FvjnMCG/Kx8d"}]},"_npmUser":{"name":"chengzzzi44","email":"17356938867@163.com"},"directories":{},"maintainers":[{"name":"chengzzzi44","email":"17356938867@163.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/dsh-shopify_0.1.2_1789205910184_0.4411493708822096"},"_hasShrinkwrap":false}},"maintainers":[{"name":"chengzzzi44","email":"17356938867@163.com"}],"description":"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.","homepage":"https://github.com/chengzzzi44/dsh-shopify#readme","keywords":["deepseek-harness","dsh","dsh-plugin","shopify","ecommerce","cordis"],"repository":{"type":"git","url":"git+https://github.com/chengzzzi44/dsh-shopify.git"},"author":"chengzzzi44","bugs":{"url":"https://github.com/chengzzzi44/dsh-shopify/issues"},"license":"MIT","readme":"# @chengzzzi44/dsh-shopify\n\nDeepSeek Harness 的 Shopify 插件：让模型直接读写你店铺的商品、订单、客户和库存，走 Shopify 官方的 **Admin GraphQL API**。\n\n- **18 个工具**：8 个只读 + 10 个写入\n- **写入默认关闭**：`enableWrites` 为 false 时，10 个写工具**直接拒绝、一个请求都不发**；要在设置卡片里打开「允许写入」才生效\n- **不碰的东西**：订单（发货、取消、退款）、客户、折扣、礼品卡——这些写操作一律没有\n- **商品侧闭环**：新建草稿 → 上架发布 → 写库存 → 传图/挂图，都在插件里\n\n## 工具\n\n### 只读工具（8 个，随时可用）\n\n| 工具 | 作用 | 需要的 Admin scope |\n|---|---|---|\n| `shopify_shop` | 店铺名称、myshopify 域名、主域名、币种、时区 | 无 |\n| `shopify_products` | 搜索/列商品（支持 Shopify 查询语法），返回 GID、价格区间、库存、状态、**上架时间** | `read_products` |\n| `shopify_products_summary` | 一次调用给出全店概览：各状态数量、上架但零库存的数量、最新上架与最新草稿 | `read_products` |\n| `shopify_product` | 单个商品详情：选项、变体（SKU/价格/库存）、SEO、所属系列 | `read_products` |\n| `shopify_orders` | 搜索/列订单（按支付状态、履约状态、时间、标签等） | `read_orders` |\n| `shopify_order` | 单个订单：行项目、金额拆分、客户、履约与物流单号 | `read_orders` |\n| `shopify_customers` | 搜索/列客户：邮箱、订单数、累计消费、标签 | `read_customers` |\n| `shopify_inventory` | 按 SKU 查各库位的可用/在库/占用/在途数量 | `read_inventory` + `read_locations` |\n\n### 写工具（10 个，默认全部关闭）\n\n| 工具 | 作用 | 需要的 Admin scope |\n|---|---|---|\n| `shopify_product_create` | 新建商品，一次带上选项、变体、价格、图片、初始库存；**状态默认 DRAFT** | `write_products`（带库存还要 `write_inventory`） |\n| `shopify_product_update` | 改状态（草稿上架/下架/归档）、标签、标题、供应商、类型，以及按变体改价 | `write_products` |\n| `shopify_product_publish` | 发布到销售渠道（\"顾客能看到\"的那一步），默认 Online Store | `write_products` |\n| `shopify_product_delete` | **永久删除**一个商品 | `write_products` |\n| `shopify_product_image_add` | 给商品加一张图——本地文件（先上传）或一个图片 URL | `write_products` + `write_files` |\n| `shopify_file_upload` | 本地图片（PNG/JPEG/WebP/GIF，≤20 MB）传进 Shopify Files | `write_files` |\n| `shopify_file_delete` | 按 GID 删除 Files 里的文件；删商品图对应的文件会同时把图从商品上摘掉 | `write_files` |\n| `shopify_inventory_set` | 把某 SKU 在某库位的数量**设成**绝对值 | `write_inventory` + `read_inventory` + `read_locations` |\n| `shopify_inventory_adjust` | 在某库位的现有数量上**加减**一个带符号的差值 | 同上 |\n| `shopify_inventory_activate` | 让某个变体在某库位首次入库（`set` 写不了没有库存记录的库位） | 同上 |\n\n三个列表工具（`shopify_products` / `shopify_orders` / `shopify_customers`）都会在**第一页**附带 `totalCount`——即当前筛选条件下一共多少条，不用自己翻页数。想看全部就在参数里加 `all: true`，插件会自己翻页读完（一次最多 1000 行，超了返回 `nextCursor` 让你接着读）：\n\n```jsonc\n// 一次读完所有已上架商品，按上架时间从新到旧\n{ \"query\": \"status:active\", \"sortKey\": \"PUBLISHED_AT\", \"reverse\": true, \"all\": true }\n// → products: 96 of 96\n```\n\n`shopify_orders` 的 `status` 过滤在 Shopify 那边执行（拼进搜索查询），所以返回的行数和 `totalCount` 永远是同一批订单。\n\n全店概览一次调用就够：\n\n```\nproducts: 128 total · 96 active · 32 draft · 0 archived\nactive out of stock: 3\nnewest published: 2026-07-30 · Silk Peony Stem … · gid://shopify/Product/1234567890\nnewest draft: 2026-08-04 · Faux Banana Tree …\n```\n\n## 安装\n\n前置条件：可用的 `dsh` + PATH 里有 `pnpm`。\n\n```sh\ndsh plugin --profile web add @chengzzzi44/dsh-shopify\n```\n\n从本仓库源码安装：\n\n```sh\ngit clone https://github.com/chengzzzi44/dsh-shopify.git\ncd dsh-shopify && npm install\ndsh plugin --profile web add \"$PWD\"\n```\n\n## 配置\n\n**推荐：在「设置 → 插件 → dsh-shopify」卡片里填**（店铺域名、client id、client secret 三项）。卡片把域名和 client id 写进该插件的设置节，secret 写进凭据域（`$DSH_HOME/.credentials.yaml`），都不回显密钥；保存后下一次工具调用立即生效，不用重启。\n\n也可以走配置文件，凭据有两种写法，任选，也可以混用（**字面值优先于同名的引用**）：\n\n**1. 直接写在插件配置里**（配置文件本身就在 `~/.dsh/profiles/<profile>/cordis.patch.yml`，不进任何仓库）：\n\n```yaml\n- id: dsh-shopify\n  config:\n    storeDomain: 'your-store.myshopify.com'\n    clientId: '你的 client id'\n    clientSecret: '你的 client secret'\n    apiVersion: '2026-01'\n    maxResults: 10\n```\n\n旧版静态 token 同理，写 `accessToken: 'shpat_...'` 即可，此时不会再走 client credentials 换 token。\n\n> 行里写了 `clientSecret` 字面值就会盖过凭据域里的那份；要在卡片里填 secret，就别在配置文件里留 `clientSecret`。\n\n**2. 只写引用名，值放环境/凭据域**（适合要提交配置文件的场景）：\n\n```sh\n# ~/.dsh/.env（或启动进程的环境变量）\nSHOPIFY_STORE_DOMAIN=your-store.myshopify.com\nSHOPIFY_CLIENT_ID=你的 client id\nSHOPIFY_CLIENT_SECRET=你的 client secret\n```\n\n```yaml\n- id: dsh-shopify\n  config:\n    storeDomain: 'your-store.myshopify.com'\n    clientIdRef: SHOPIFY_CLIENT_ID\n    clientSecretRef: SHOPIFY_CLIENT_SECRET\n    apiVersion: '2026-01'\n    maxResults: 10\n```\n\n> patch 会**整体替换** `config`，要保留的键都得重写。\n\n| 字段 | 默认 | 说明 |\n|---|---|---|\n| `storeDomain` | —（来自 `SHOPIFY_STORE_DOMAIN`） | 店铺的 `*.myshopify.com` 域名；未设置时工具仍注册，但每次调用都会明确报错 |\n| `clientId` | — | 直接写在配置里的 client id，优先于 `clientIdRef` |\n| `clientSecret` | — | 直接写在配置里的 client secret，优先于 `clientSecretRef` |\n| `accessToken` | — | 直接写在配置里的旧版静态 `shpat_` token；设了就不再换 token |\n| `clientIdRef` | `SHOPIFY_CLIENT_ID` | 凭据引用名（不是密钥本身），`clientId` 为空时使用 |\n| `clientSecretRef` | `SHOPIFY_CLIENT_SECRET` | 同上 |\n| `accessTokenRef` | — | 凭据引用名，`accessToken` 为空时使用 |\n| `apiVersion` | `2026-01` | Admin API 版本，写在 URL 路径里 |\n| `maxResults` | `10` | 列表工具的默认行数，同时是硬上限（最大 50） |\n| `requestTimeoutMs` | `30000` | 单次请求预算，含响应体 |\n| `enableWrites` | `false` | 是否允许写工具改动店铺；默认关闭，卡片上有开关 |\n\n设置节（命名空间 `dsh-shopify`）以插件行为基底：卡片里填过的字段进入用户层，没填的继续取 `cordis.patch.yml` / 环境变量。`clientSecret` 和 `accessToken` 标了 `role('secret')`，任何设置读取都拿不到它们的值。工具每次调用都会重读这份解析结果，所以保存后下一次查询就用新值，不用重启。\n\n### 凭据怎么来：建一个 Shopify 应用\n\n2026 年起新 app 在 **Dev Dashboard** 创建，用 **client credentials**：插件把 `client id` + `secret` 换成短期 access token（约 24 小时），缓存在内存里、过期前自动刷新，**token 不落盘**。\n\n1. 打开 <https://dev.shopify.com/dashboard> → Apps → 新建 app\n2. 在 **Configuration → Admin API access scopes** 里勾权限（见下）\n3. **发布（Release）这个版本**——只改不发布不生效\n4. 到店铺后台 → 设置 → 应用，**安装/授权**这个 app\n5. 把 **Client ID / Client secret** 填进插件设置卡片\n\n只要查询，勾这几个就够（复制即用）：\n\n```\nread_products, read_orders, read_customers, read_inventory, read_locations\n```\n\n要连写入一起用，再加：\n\n```\nwrite_products, write_files, write_inventory\n```\n\n旧版静态 token 也支持：老 custom app 的 `shpat_` token 写到 `accessToken` / `accessTokenRef` 即可，此时不再换 token。\n\n> scope 改过之后要**发布新版本并重新安装**，否则换出来的 token 还是旧权限——这是最常见的\"明明加了权限却还是 Access denied\"的原因。\n\n### 写入（默认关闭）\n\n**上面那 10 个写工具**在 `enableWrites` 为 false 时全部**直接拒绝、一个请求都不发**，报错里会告诉你去哪打开。打开方式：设置 → 插件 → `dsh-shopify` 卡片里的「允许写入」，或者在配置里写 `enableWrites: true`。\n\n打开之后可以：\n\n```jsonc\n// 把草稿上架并打标签\n{ \"id\": \"gid://shopify/Product/123\", \"status\": \"ACTIVE\", \"tags\": [\"spring\", \"sale\"] }\n// 批量改变体价格\n{ \"id\": \"gid://shopify/Product/123\", \"variants\": [{ \"id\": \"gid://shopify/ProductVariant/456\", \"price\": \"29.99\" }] }\n```\n\n几个刻意的设计：标签是**整体替换**（传什么就是什么，不传就不动）；`status` 只接受 `ACTIVE` / `DRAFT` / `ARCHIVED`；价格必须是十进制字符串；Shopify 返回的 `userErrors` 会原样抛给模型（而不是假装成功）；一次调用里改多个字段只发一条 mutation，价格另发一条。\n\n> 注意：`status: ACTIVE` 只改商品状态。要让它在店铺前台可见，还得发布到销售渠道——用 `shopify_product_publish`（默认 Online Store，也可以 `channels: [\"Shop\"]` 指名）。\n\n### 图片上传\n\n同样受「允许写入」开关控制，关着时一个请求都不发。\n\n```jsonc\n// 传本地图到 Shopify Files\n{ \"path\": \"generated-images/product-1.png\", \"alt\": \"Front view\" }\n// → uploaded product-1.png · image/png · 245123 bytes · gid://shopify/MediaImage/123 · UPLOADED\n\n// 直接给商品加图（本地文件会先自动上传）\n{ \"productId\": \"gid://shopify/Product/123\", \"path\": \"generated-images/product-1.png\", \"alt\": \"Front view\" }\n// 也可以传一个 Shopify 抓得到的图片 URL\n{ \"productId\": \"gid://shopify/Product/123\", \"url\": \"https://example.com/photo.jpg\" }\n```\n\n`path` 支持绝对路径，或相对**会话工作区**的路径——所以生图插件刚生成到工作区里的图片可以直接喂进来。传错了用 `shopify_file_delete` 删：商品媒体背后的文件是同一个 GID，删文件就同时把图从商品上摘掉（一次最多 50 个）。上传走 Shopify 官方三步：`stagedUploadsCreate` 拿签名目标 → multipart 把字节发到暂存服务 → `fileCreate` 落库；给商品加图走 `productUpdate` 的 `media` 参数。Shopify 处理图片是异步的，所以返回里 `status` 常常还是 `PROCESSING`，稍后自己会变 `READY`。\n\n### 库存写入\n\n`set` 和 `adjust` 是两个工具，不是同一个工具的两个参数——因为它们语义不同，混用会出事：`set` 会**覆盖**库位上的现有数字，`adjust` 才是在现有基础上加减。同步场景一般用 `adjust`，只有当你确定仓库真实数量时才用 `set`。\n\n```jsonc\n// 加减（推荐的同步方式）\n{ \"sku\": \"SKU-1234\", \"location\": \"主仓库\", \"delta\": 20 }\n// 设定绝对值（覆盖）\n{ \"sku\": \"SKU-1234\", \"location\": \"主仓库\", \"quantity\": 120 }\n// 带并发保护：只有当前值正好是 120 时才写，否则 Shopify 拒绝\n{ \"sku\": \"SKU-1234\", \"location\": \"主仓库\", \"quantity\": 100, \"expectedQuantity\": 120 }\n```\n\n- `location` 可以写库位名（中文也行，大小写不敏感）或 GID；写错会把这家店**实际有的库位名**列在报错里。\n- `sku` 和 `inventoryItemId` 二选一；两者都不给或都给都会被拒。\n- 数量名默认 `available`，可用 `name` 换成 `on_hand` 等；原因默认 `correction`。\n- `delta: 0`、负数 `quantity`、未知的数量名/原因都会在发请求前被拒。\n- 没写 `expectedQuantity` 时会显式告诉 Shopify「本次覆盖」，写了就带上比对值——并发改动会被 Shopify 挡下来，而不是把别人的改动冲掉。\n\n### 完整流水线：上传草稿 → 上架 → 库存\n\n这三步在插件里已经闭环，全部在同一个「允许写入」开关下：\n\n```jsonc\n// 1) 新建草稿：选项 + 两个变体 + 价格 + SKU + 初始库存，一次调用\n{\n  \"title\": \"Silk Peony Stem\",\n  \"vendor\": \"Example Vendor\",\n  \"tags\": [\"peony\", \"silk\"],\n  \"options\": [{ \"name\": \"Size\", \"values\": [\"S\", \"M\"] }],\n  \"variants\": [\n    { \"optionValues\": [{ \"optionName\": \"Size\", \"name\": \"S\" }], \"price\": \"27.99\", \"sku\": \"PEONY-S\",\n      \"quantities\": [{ \"locationId\": \"gid://shopify/Location/1\", \"quantity\": 100 }] },\n    { \"optionValues\": [{ \"optionName\": \"Size\", \"name\": \"M\" }], \"price\": \"29.99\", \"sku\": \"PEONY-M\" }\n  ],\n  \"images\": [{ \"url\": \"https://example.com/peony.jpg\", \"alt\": \"Peony stem\" }]\n}\n// → created … · DRAFT · /products/…   （默认草稿，不会自己跑到前台）\n\n// 2) 上架：先把状态改成 ACTIVE，再发布到渠道（两步都要）\n{ \"id\": \"gid://shopify/Product/123\", \"status\": \"ACTIVE\" }          // shopify_product_update\n{ \"productId\": \"gid://shopify/Product/123\" }                        // shopify_product_publish，默认 Online Store\n{ \"productId\": \"gid://shopify/Product/123\", \"channels\": [\"Shop\"] }  // 也可以指名渠道\n\n// 3) 库存：已有库存记录的库位直接 set/adjust；新变体首次入库先 activate\n{ \"sku\": \"PEONY-M\", \"location\": \"主仓库\", \"quantity\": 50 }\n{ \"sku\": \"PEONY-M\", \"location\": \"主仓库\" }        // shopify_inventory_activate\n```\n\n几个刻意的设计：\n\n- **新建商品默认 DRAFT**，要 ACTIVE 必须显式写 `status: \"ACTIVE\"`——上传流程不会不小心把东西推到前台。\n- **`status: ACTIVE` ≠ 顾客能看到**。Shopify 里这是两件事：状态 + 发布到渠道。所以上架是两步，`shopify_product_publish` 补的是后一半。\n- **声明 `options` 就必须给匹配的 `variants`**，否则 Shopify 只会生成一个价格为 0 的默认变体（这是 `productCreate` 的经典坑，`productSet` 配上完整变体才不会有）。\n- **`shopify_product_delete` 是永久删除**，Shopify 没有回收站；只是想下架请用 `status: DRAFT`。\n- 创建时带库存用的是 `productSet` 的 `inventoryQuantities`，和后面 `inventory_set` 写的是同一份数据。\n\n## 模型看到什么\n\n- 每个工具返回精简后的结构化 JSON（GID、标题、金额、状态、时间），`render` 再生成紧凑文本给模型看，例如：\n\n  ```\n  orders: 2\n  - #1001 · gid://shopify/Order/1001 · 2026-09-10 · PAID / UNFULFILLED · 59.00 USD · Alice <alice@example.com> · Dog Toy ×2 (DOG-S)\n  ```\n\n- 金额一律 `{ amount, currency }`（Shopify 的 Money 本来就是这个形状）；`numberOfOrders` 这类 Int64 标量 Shopify 会序列化成字符串，插件已做兼容。\n- UI 卡片走通用卡片（读 `kind: read`、写 `kind: edit`、删除 `kind: delete`）；`presentationMeta` 只带行数、是否还有下一页、改了几项这类计数，供将来做专用卡片。\n\n## 已知限制\n\n- **订单、客户、折扣、礼品卡没有写操作**：插件只能读它们。发货、取消订单、退款都做不到。\n- **`shopify_product_delete` 不可撤销**：Shopify 没有回收站。想下架用 `status: DRAFT`，别用删除。\n- **\"上架\"是两步**：`status: ACTIVE` 只改状态，还要 `shopify_product_publish` 落到渠道，顾客才看得到。\n- **`all: true` 有上限**：一次最多读 1000 行，超了返回 `nextCursor` 让你接着读；单页上限 250。\n- **受 Shopify 限流约束**：Admin GraphQL 按 query cost 计费的漏桶；一次请求太大或太频繁会被拒（错误信息里会带 `THROTTLED`），把 `maxResults` 调小或稍后重试。\n- **客户数据是 PII**：`shopify_customers` 会返回邮箱等个人信息，注意你的部署里谁能看到会话记录。\n- **只支持 myshopify 域名**：Admin API 不能直接用自定义域名访问，`storeDomain` 必须是 `xxx.myshopify.com`。\n- **没有工具结果卡片**：浏览器半边只注册「设置 → 插件」里的 `dsh-shopify` 配置卡片，工具结果仍走通用卡片。\n\n## 开发\n\n```sh\nnpm install     # 装依赖并执行 prepare 构建 lib/\nnpm test        # 无密钥自测：Host 半边跑本地假 Shopify 服务；浏览器半边装进 __ModuleLoader__ 驱动设置卡片\nnpm run typecheck\n```\n\n`npm test` 不起真实网络、不需要任何凭据：Host 侧把 `globalThis.fetch` 路由到本地假服务，客户端侧用无 DOM 的 React 替身驱动控制器。\n\n## License\n\n[MIT](LICENSE)\n","readmeFilename":""}