@lofder/dsers-mcp-product 1.4.0 → 1.5.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.
Files changed (136) hide show
  1. package/CHANGELOG.md +264 -0
  2. package/README.md +65 -7
  3. package/dist/auth/oauth.d.ts.map +1 -1
  4. package/dist/auth/oauth.js +97 -57
  5. package/dist/auth/oauth.js.map +1 -1
  6. package/dist/auth/token-store.d.ts.map +1 -1
  7. package/dist/auth/token-store.js +80 -27
  8. package/dist/auth/token-store.js.map +1 -1
  9. package/dist/cli.js +39 -25
  10. package/dist/cli.js.map +1 -1
  11. package/dist/dsers/auth.d.ts +1 -0
  12. package/dist/dsers/auth.d.ts.map +1 -1
  13. package/dist/dsers/auth.js +56 -18
  14. package/dist/dsers/auth.js.map +1 -1
  15. package/dist/dsers/client.d.ts +9 -3
  16. package/dist/dsers/client.d.ts.map +1 -1
  17. package/dist/dsers/client.js +47 -20
  18. package/dist/dsers/client.js.map +1 -1
  19. package/dist/dsers/product.d.ts +60 -1
  20. package/dist/dsers/product.d.ts.map +1 -1
  21. package/dist/dsers/product.js +87 -13
  22. package/dist/dsers/product.js.map +1 -1
  23. package/dist/dsers/retry.d.ts +59 -0
  24. package/dist/dsers/retry.d.ts.map +1 -0
  25. package/dist/dsers/retry.js +214 -0
  26. package/dist/dsers/retry.js.map +1 -0
  27. package/dist/error-map.d.ts.map +1 -1
  28. package/dist/error-map.js +27 -8
  29. package/dist/error-map.js.map +1 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +7 -3
  32. package/dist/index.js.map +1 -1
  33. package/dist/instructions.d.ts.map +1 -1
  34. package/dist/instructions.js +23 -1
  35. package/dist/instructions.js.map +1 -1
  36. package/dist/job-store-memory.d.ts.map +1 -1
  37. package/dist/job-store-memory.js +2 -43
  38. package/dist/job-store-memory.js.map +1 -1
  39. package/dist/job-store.d.ts.map +1 -1
  40. package/dist/job-store.js +5 -1
  41. package/dist/job-store.js.map +1 -1
  42. package/dist/logger.d.ts +51 -0
  43. package/dist/logger.d.ts.map +1 -0
  44. package/dist/logger.js +125 -0
  45. package/dist/logger.js.map +1 -0
  46. package/dist/oauth/crypto.d.ts +4 -1
  47. package/dist/oauth/crypto.d.ts.map +1 -1
  48. package/dist/oauth/crypto.js +10 -1
  49. package/dist/oauth/crypto.js.map +1 -1
  50. package/dist/provider/helpers.d.ts +2 -2
  51. package/dist/provider/helpers.d.ts.map +1 -1
  52. package/dist/provider/helpers.js +28 -10
  53. package/dist/provider/helpers.js.map +1 -1
  54. package/dist/provider/import-ops.d.ts.map +1 -1
  55. package/dist/provider/import-ops.js +7 -4
  56. package/dist/provider/import-ops.js.map +1 -1
  57. package/dist/provider/index.d.ts +1 -0
  58. package/dist/provider/index.d.ts.map +1 -1
  59. package/dist/provider/index.js +3 -1
  60. package/dist/provider/index.js.map +1 -1
  61. package/dist/provider/normalize.d.ts.map +1 -1
  62. package/dist/provider/normalize.js +13 -9
  63. package/dist/provider/normalize.js.map +1 -1
  64. package/dist/provider/push.d.ts.map +1 -1
  65. package/dist/provider/push.js +4 -3
  66. package/dist/provider/push.js.map +1 -1
  67. package/dist/provider/sku-matcher.d.ts +157 -0
  68. package/dist/provider/sku-matcher.d.ts.map +1 -0
  69. package/dist/provider/sku-matcher.js +1206 -0
  70. package/dist/provider/sku-matcher.js.map +1 -0
  71. package/dist/provider/store.d.ts +1 -1
  72. package/dist/provider/store.d.ts.map +1 -1
  73. package/dist/provider/store.js +7 -5
  74. package/dist/provider/store.js.map +1 -1
  75. package/dist/rules.d.ts.map +1 -1
  76. package/dist/rules.js +60 -4
  77. package/dist/rules.js.map +1 -1
  78. package/dist/service/browse-shared.d.ts +1 -0
  79. package/dist/service/browse-shared.d.ts.map +1 -1
  80. package/dist/service/browse-shared.js +3 -1
  81. package/dist/service/browse-shared.js.map +1 -1
  82. package/dist/service/find-product.d.ts.map +1 -1
  83. package/dist/service/find-product.js +33 -6
  84. package/dist/service/find-product.js.map +1 -1
  85. package/dist/service/import-list.d.ts.map +1 -1
  86. package/dist/service/import-list.js +5 -2
  87. package/dist/service/import-list.js.map +1 -1
  88. package/dist/service/index.d.ts +2 -0
  89. package/dist/service/index.d.ts.map +1 -1
  90. package/dist/service/index.js +15 -5
  91. package/dist/service/index.js.map +1 -1
  92. package/dist/service/preview.d.ts +1 -1
  93. package/dist/service/preview.d.ts.map +1 -1
  94. package/dist/service/preview.js +53 -24
  95. package/dist/service/preview.js.map +1 -1
  96. package/dist/service/push-flow.d.ts.map +1 -1
  97. package/dist/service/push-flow.js +23 -15
  98. package/dist/service/push-flow.js.map +1 -1
  99. package/dist/service/sku-mapping.d.ts +674 -0
  100. package/dist/service/sku-mapping.d.ts.map +1 -0
  101. package/dist/service/sku-mapping.js +1879 -0
  102. package/dist/service/sku-mapping.js.map +1 -0
  103. package/dist/tools.d.ts +3 -0
  104. package/dist/tools.d.ts.map +1 -1
  105. package/dist/tools.js +329 -5
  106. package/dist/tools.js.map +1 -1
  107. package/dist/tsconfig.tsbuildinfo +1 -1
  108. package/dist/util.d.ts +6 -0
  109. package/dist/util.d.ts.map +1 -0
  110. package/dist/util.js +25 -0
  111. package/dist/util.js.map +1 -0
  112. package/package.json +10 -5
  113. package/dist/auth/cdp-session.d.ts +0 -6
  114. package/dist/auth/cdp-session.d.ts.map +0 -1
  115. package/dist/auth/cdp-session.js +0 -369
  116. package/dist/auth/cdp-session.js.map +0 -1
  117. package/dist/auth/safari-fallback.d.ts +0 -7
  118. package/dist/auth/safari-fallback.d.ts.map +0 -1
  119. package/dist/auth/safari-fallback.js +0 -73
  120. package/dist/auth/safari-fallback.js.map +0 -1
  121. package/dist/auth/terminal-prompt.d.ts +0 -6
  122. package/dist/auth/terminal-prompt.d.ts.map +0 -1
  123. package/dist/auth/terminal-prompt.js +0 -118
  124. package/dist/auth/terminal-prompt.js.map +0 -1
  125. package/dist/provider.d.ts +0 -90
  126. package/dist/provider.d.ts.map +0 -1
  127. package/dist/provider.js +0 -1577
  128. package/dist/provider.js.map +0 -1
  129. package/dist/service/browse.d.ts +0 -20
  130. package/dist/service/browse.d.ts.map +0 -1
  131. package/dist/service/browse.js +0 -159
  132. package/dist/service/browse.js.map +0 -1
  133. package/dist/service.d.ts +0 -27
  134. package/dist/service.d.ts.map +0 -1
  135. package/dist/service.js +0 -949
  136. package/dist/service.js.map +0 -1
package/CHANGELOG.md ADDED
@@ -0,0 +1,264 @@
1
+ # Changelog
2
+
3
+ > [English](#english) | [中文](#中文)
4
+
5
+ ---
6
+
7
+ <a id="english"></a>
8
+
9
+ ## English
10
+
11
+ ### 1.5.1 — 2026-04-10
12
+
13
+ Security hardening + 7 bugfixes from npm testing. No new tools, no algorithm changes — purely defensive.
14
+
15
+ #### Bugfixes
16
+
17
+ - **`dsers_find_product` — empty keyword rejected.** Passing `keyword: ""` or omitting both `keyword` and `image_url` used to return random products. Now throws a clear error.
18
+ - **`dsers_find_product` — image search respects `limit`.** DSers backend ignores `limit` on image search; we now truncate client-side and return `truncated_from` showing the original count.
19
+ - **`dsers_find_product` — pagination overlap note.** Response includes `pagination_note` when `search_after` is present, warning that pages may overlap by one item.
20
+ - **`dsers_find_product` — insufficient results note.** When fewer items than `limit` are returned and there are no more pages, a `note` field explains the shortfall.
21
+ - **`dsers_product_update_rules` — persist failure blocks push.** If `saveDraft()` fails, job status is set to `persist_failed` and `dsers_store_push` refuses to proceed. Previously the failure was a buried warning and push would silently use old rules.
22
+ - **`dsers_store_push` — pricing rule transparency.** When `apply_store_pricing_rule` is chosen, the response now shows the store's actual pricing rule configuration instead of silently overriding.
23
+ - **`dsers_rules_validate` — extreme pricing warnings.** `multiplier > 100`, `fixed_markup > $500`, and `fixed_price > $10,000` now produce warnings with concrete dollar examples.
24
+
25
+ #### Security
26
+
27
+ - **OAuth callback** — `redirect_uri` restricted to `localhost` / `127.0.0.1` / `[::1]`. Authorization code now includes a `nonce` for replay prevention and `client_id` binding.
28
+ - **Token endpoint** — nonce-based code replay prevention (in-memory Map with 10-min TTL). IP-based rate limit: 10 registrations/min.
29
+ - **Vercel route** — replaced unsigned `decodeJwtPayload` with `decryptAccessToken`. Legacy fallback gated by `LEGACY_JWT_FALLBACK=1` (default off, scheduled for removal). Added `assertEncryptionKey()` early check. `jobStores` Map capped at 100 entries with LRU eviction.
30
+ - **FileJobStore** — UUID format validation prevents path traversal.
31
+ - **MemoryJobStore** — `jobId` is now a plain UUID; no longer encodes payload state into the ID.
32
+ - **sku-matcher image fetch** — 10s timeout via `AbortController`, 10 MB size limit, LRU cache eviction (batch 50 at 500 cap), `pLimit(10)` concurrency cap.
33
+ - **Rules engine** — `description_override_html` and `description_append_html` validated against `<script>`, `<iframe>`, `<object>`, `<embed>`, `<form>`, `on*` event handlers, and `javascript:` URLs. Blocked at validation time; sanitized at runtime as defense-in-depth.
34
+ - **CLI OAuth** — callback server binds `127.0.0.1` only. `state` parameter verified on callback. Port fallback (3001 → 3002 → 3003). 5-minute timeout. `openBrowser` deduplicated via import from `browser-finder.ts`.
35
+ - **Token storage** — PBKDF2 (100k iterations) + random salt replaces bare SHA-256. Salt stored in `~/.dsers-mcp/key-salt`. Auto-migrates existing credentials on first load. File written with `mode: 0o600` directly (no post-hoc `chmod`).
36
+ - **Vercel headers** — `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, `X-XSS-Protection: 1; mode=block`, `Referrer-Policy: strict-origin-when-cross-origin`.
37
+ - **Error sanitization** — `err.body` in DSers API errors now strips tokens/keys/session IDs before truncation.
38
+
39
+ #### Improvements
40
+
41
+ - **DSers client** — 30s global request timeout via `AbortController`. Rate limiter promoted to module-level (shared across instances).
42
+ - **Auth** — `getSession()` refresh deduplicates concurrent callers via a shared promise. `invalidate()` clears all fields including `refreshToken`, `clientId`, `oauthBase`.
43
+ - **Push polling** — exponential backoff (1/2/4/8/15s) replaces fixed 10s intervals.
44
+ - **Multi-store push** — `Promise.all` with `pLimit(5)` replaces sequential loop.
45
+ - **Import list** — detail fetches capped at `pLimit(5)` concurrency.
46
+ - **Pricing rule cache** — 5-minute TTL prevents stale reads.
47
+ - **Helpers** — `findFirstValueByKeys` / `findAllValuesByKeys` add `maxDepth=20` + `WeakSet` cycle guard. `variantImages` uses `Set` instead of `Array.includes`.
48
+ - **Normalize** — pre-built `Map<optionId:valueName, valueId>` replaces nested variant loop.
49
+ - **`cleanNone`** — top-level only, no longer recursive (preserves deep null semantics).
50
+ - **`coerceIntId`** — always returns string (removed unsafe `parseInt` branch).
51
+ - **`deleteImportList`** — regex guard + `encodeURIComponent` prevents URL injection.
52
+ - **`checkAliExpressAuth`** — catch block now returns `valid: false` (was `true`).
53
+ - **`recoverImportItemId`** — structured field matching replaces `JSON.stringify + includes`.
54
+ - **Logger** — cached `LOG_LEVEL` read, `__testHooks.setWriter` gated by `NODE_ENV=test`, added `resetLevel()` hook.
55
+ - **eslint** — `no-console: "error"` (cli.ts exempted).
56
+ - **`mcp-handler`** moved from devDependencies to dependencies.
57
+ - **`prepublishOnly: "npm run build"`** added.
58
+ - **`version:sync`** script syncs package.json version to server.json and manifest.json.
59
+ - **`pLimit`** consolidated into `src/util.ts` (was duplicated in 3 files).
60
+ - **Preview warnings** — truncate-then-deduplicate (was deduplicate-then-truncate, which could produce dupes after truncation).
61
+ - **CLI** — graceful SIGTERM/SIGINT shutdown. Token hot-reload uses `fs.watchFile` instead of `statSync` polling.
62
+ - **`centsToDollars`** — `browse-shared.ts` now imports from `helpers.ts` (was duplicated).
63
+ - **Node** — minimum version lowered from >=22 to >=20.
64
+
65
+ #### Regression validation
66
+
67
+ Two independent test accounts (different stores, different product counts). 57 e2e scenarios total, 0 failures. Unit tests: 25 files, 618 tests, all green.
68
+
69
+ ---
70
+
71
+ ### 1.5.0 — 2026-04-10
72
+
73
+ Big release. Finally nailed down the "swap supplier" problem.
74
+
75
+ #### New tool: `dsers_sku_remap`
76
+
77
+ There was one scenario that kept nagging at me — the old supplier jacks up the price, runs out of stock, or gets delisted, and you want to swap to a new one. You can do this in the DSers dashboard, but clicking through variant by variant is painful, and you still have to figure out which seller variant corresponds to which new supplier SKU. I'd already built a separate sku-matcher engine (option alignment + unit normalization + synonym tables + dHash image similarity scoring) — originally as an internal tool. This release lifts the whole thing into the MCP interface so your AI agent can do the replacement in one sentence.
78
+
79
+ Two modes:
80
+
81
+ - **STRICT** — You already have the new supplier URL. Pass `new_supplier_url`, the tool uses sku-matcher to align variants on both sides, and honors `auto_confidence` to decide what auto-swaps, what keeps the old supplier, and what gets flagged as unmatched for manual review.
82
+ - **DISCOVER** — You only know "this supplier is dead, find me a replacement". Omit the URL. The tool reverse-image-searches the DSers pool from the current product's images, then ranks candidates via a multi-factor score (sku score + image frequency + product rating + store rating + price proximity + stock + order count) and auto-picks the best match.
83
+
84
+ Both modes are two-step: always call `mode='preview'` first (read-only) to inspect `diffs` and `pool_additions`, then call `mode='apply'` to actually write.
85
+
86
+ Key design decisions:
87
+
88
+ - Old suppliers get **archived into `mapping.pool[]` as history**, not thrown away. If the new supplier turns out to be a lemon, the next `dsers_sku_remap` run can reconsider the old supplier as a candidate.
89
+ - `pool` is monotonic (grow-only). There's a validator rule specifically enforcing this invariant.
90
+ - **6 structural checks run before any write**, covering `supplyProductId` numeric format, `supplyVariantId` format, `optionId` / `valueId` correspondence, and more — so DSers never gets written garbage.
91
+ - Auto-detects `MapBas` vs `MapSta` mapping type based on option alignment consistency. When alignment is incomplete, it force-downgrades to Standard and surfaces the reason in `warnings`.
92
+
93
+ #### Other tool cleanup
94
+
95
+ `manifest.json`'s tools array was missing 4 entries — the code had already shipped them but the metadata never caught up. Fixed in this release:
96
+
97
+ - `dsers_import_list` — browse the import staging list with cost / sell price / stock / markup status
98
+ - `dsers_my_products` — view products already pushed to a store (with supplier links for re-import)
99
+ - `dsers_find_product` — search the DSers product pool (keyword or image)
100
+ - `dsers_sku_remap` — the new one described above
101
+
102
+ Tool count goes from 9 listed to **13** (matches what `tools/list` has actually been returning since 1.4.x).
103
+
104
+ #### Stability fixes
105
+
106
+ `dsers_sku_remap` went through 15 fix rounds from round-1 to round-5.7.5. Headline items:
107
+
108
+ - **F1 — Path A candidate fetch now has a `product-pool/product/detail` fallback.** When the user-provided supplier URL is already mapped to another product in the account, DSers import-list reports "already exists" but doesn't return an `importListId`, which caused the old Path A to time out after 5–26 s. The fallback hits the pool endpoint directly and returns in a few hundred ms.
109
+ - **F2 — `skuRemap` entry point now runs a store-ownership check first.** A shape-valid but wrong `store_id` no longer travels all the way into Path B `rank_candidates` before bottoming out with a misleading "no viable match" error.
110
+ - **F3 — MCP-boundary input shape guard.** Empty strings / letters / 200-char strings / special symbols for `dsers_product_id` are now rejected in 0–1 ms without reaching the handler.
111
+ - **F4 — CODEC copy containment.** DSers' raw Go-map error text no longer leaks into user-visible errors; everything routes through a clean `store_id_mismatch` envelope with explicit `dsers_store_discover` / `dsers_my_products` remediation hints.
112
+ - **M1 — Production-grade int64 range convergence.** The 19-digit numeric validation for `store_id` / `dsers_product_id` now adds a BigInt `.refine()` at the MCP layer that checks against the signed int64 ceiling (`9223372036854775807`). Overflow values are rejected at the MCP boundary, avoiding the DSers int64 parser bug entirely.
113
+ - **O1 — URL scheme case canonicalization.** `HTTPS://` / `HttP://` etc. are now lowercased at the scheme prefix inside `SUPPLIER_URL_SCHEMA.transform()`. Host / path / query are left exactly as provided (RFC 3986: scheme is case-insensitive, path is case-sensitive; Alibaba URLs like `Widget_ABC_1600123456789.html` must survive intact).
114
+ - **O3 — Hard error text hygiene.** `sanitizePrimaryErrorForWarning` strips DSers JSON blobs / Go `<nil>` / `map[]` noise from Path A fetch errors and leaves clean prose with a compact `(DSers API HTTP NNN)` marker for the upstream status code.
115
+ - **O-R4-3 — `checkStoreOwnership` direct-lookup fast path.** Wrong `dsers_product_id` failure latency drops from ~1.3 s (list-scan) to under 500 ms (`getMyProductDetail` direct query), in line with wrong `store_id` latency.
116
+ - **O-R5-3 — Validator now accepts DSers' single-SKU `<none>` sentinel.** DSers natively stores `supplyVariantId: "<none>"` as a literal string for `Default Title` single-variant products. Before this fix the validator's `\d+:\d+[#...]` pattern was wrongly rejecting this real production data, which meant **every single-SKU product in the account would fail on apply**.
117
+ - **O-R55-2 / B-R57-1 — sku-matcher single-SKU seller shortcut.** Fixes a matcher blind spot where a seller is `Default Title` (1v) and the candidate is 1v with a real dimension name like `Color`. The new shortcut fires only when the seller is trivially single-SKU (`isTrivialSellerSingleSku`) and produces confidence 75 with reason `single_sku_seller_trivial`. **The existing algorithm core is untouched** — the shortcut is strictly an early-return at the top of `matchVariants`, preserving all multi-SKU paths and the fat-fat 1v↔1v exact-match path (which still scores 80).
118
+
119
+ #### File hygiene
120
+
121
+ - Fixed a hardcoded `version: "1.3.8"` in `app/dropshipping/[transport]/route.ts` — a legacy drop that had survived three releases without being noticed.
122
+ - `package.json` / `server.json` / `manifest.json` / README all synced to 1.5.0.
123
+ - Added `CHANGELOG.md`.
124
+ - npm package only ships `dist/` + `README` + `LICENSE` + `CHANGELOG` — 239 KB tarball. `src/` / `app/` / `test/` / `docs/` / `scripts/` are not in the published package.
125
+
126
+ #### Known limitations (deferred to later versions)
127
+
128
+ - `dsers_my_products` `page` pagination doesn't actually advance (DSers backend behavior, not a tool bug). Workaround: use `page_size:100` to pull everything at once. Accounts with >100 products will hit this; tracking separately.
129
+ - DSers GET mapping response returns a non-deterministic `mappings[]` array order for multi-supplier (`MapAgc`) products. Values are identical, only the index shifts. Affects byte-level diff workflows; the validator doesn't depend on byte-level comparison so it's not impacted.
130
+ - `dsers_sku_remap` discover mode (Path B) doesn't work on `vp*` virtual products — DSers-side limitation, no seed images available to reverse-search.
131
+
132
+ #### Regression validation
133
+
134
+ Every round from round-1 to round-5.7.5 was run by an **independent QA session** with no access to prior reports, HANDOFF docs, commit bodies, or test files. Full e2e regression + real DSers writes + byte-level rollback verification. The last round: **0 Blocker / 0 Major / 0 Minor / 0 Observation**. 585/585 vitest tests green.
135
+
136
+ ---
137
+
138
+ ### 1.4.x and earlier
139
+
140
+ Previous releases live in the git history. Mostly OAuth 2.1 + remote MCP + Vercel deployment work. No consolidated changelog; check `git log --oneline v1.4.0..v1.5.0` or individual commit messages.
141
+
142
+ ---
143
+
144
+ <a id="中文"></a>
145
+
146
+ ## 中文
147
+
148
+ ### 1.5.1 — 2026-04-10
149
+
150
+ 安全加固 + npm 测试发现的 7 个 bugfix。没有新工具,没有算法改动,纯防御性更新。
151
+
152
+ #### Bugfix
153
+
154
+ - **`dsers_find_product` — 空关键词拒绝**: `keyword: ""` 或不传 keyword 和 image_url 之前会返回随机产品,现在直接报错
155
+ - **`dsers_find_product` — 图片搜索尊重 limit**: DSers 后端对图搜忽略 limit,现在客户端强制截断,返回 `truncated_from` 字段
156
+ - **`dsers_find_product` — 分页重叠提示**: search_after 非空时返回 `pagination_note`,提示去重
157
+ - **`dsers_find_product` — 结果不足说明**: 返回少于 limit 且无更多页时加 `note` 字段
158
+ - **`dsers_product_update_rules` — 持久化失败阻断推送**: saveDraft 失败时 status 设为 `persist_failed`,push 直接拒绝
159
+ - **`dsers_store_push` — 定价规则透明化**: 选 `apply_store_pricing_rule` 时显示店铺实际定价规则配置
160
+ - **`dsers_rules_validate` — 极端定价 warning**: multiplier>100 / markup>$500 / price>$10000 带具体金额警告
161
+
162
+ #### 安全
163
+
164
+ - OAuth 回调限制 localhost、code 加 nonce 防重放、client_id 绑定
165
+ - Token 端点 nonce 防重放 + IP 限流 10 次/分
166
+ - Vercel 路由用 `decryptAccessToken` 替代无验签的 JWT 解码,legacy fallback 默认关闭
167
+ - FileJobStore UUID 校验防路径穿越
168
+ - MemoryJobStore jobId 不再编码 payload
169
+ - sku-matcher 图片拉取加 10s 超时 + 10MB 限制 + LRU 缓存驱逐 + 并发限制
170
+ - rules 引擎拦截 `<script>`/`<iframe>`/`on*` 事件/`javascript:` URL
171
+ - CLI OAuth 绑定 127.0.0.1、state 验证、端口回退、5 分钟超时
172
+ - Token 存储 PBKDF2 + salt 替代裸 SHA-256
173
+ - Vercel 安全响应头
174
+ - 错误信息自动脱敏(token/key/session)
175
+
176
+ #### 改进
177
+
178
+ - DSers 客户端 30s 全局超时,限流器改模块级共享
179
+ - Auth refresh 竞态去重,invalidate 完整清理
180
+ - Push 轮询指数退避(1/2/4/8/15s)
181
+ - 多店铺推送 pLimit(5) 并行
182
+ - Import list 详情请求 pLimit(5) 并发
183
+ - Pricing rule 缓存 5 分钟 TTL
184
+ - 递归查找加深度上限 + 循环引用检测
185
+ - `pLimit` 统一到 `src/util.ts`
186
+ - CLI 优雅退出 + watchFile 替代 statSync
187
+ - eslint `no-console: "error"`
188
+ - Node 最低版本降为 >=20
189
+
190
+ #### 回归验证
191
+
192
+ 两个独立测试账号(不同店铺、不同产品量),57 个 e2e 场景全过。单元测试 25 文件 618 用例全绿。
193
+
194
+ ---
195
+
196
+ ### 1.5.0 — 2026-04-10
197
+
198
+ 大版本。这版终于把换供应商这件事搞定了。
199
+
200
+ #### 新工具:`dsers_sku_remap`
201
+
202
+ 背景是之前一直有一个绕不过去的场景 —— 老供应商涨价 / 断货 / 被封了,你想换一个新的。DSers 后台手动点也能换,但是每个变体每个变体点下去非常反人类,更不用说你要把 seller 的 variant 精确对应到新 supplier 的哪个 SKU。我之前做了一个单独的 sku-matcher 引擎(用 option 对齐 + 单位规范化 + 同义词表 + dHash 图像相似度做打分),原本是内部工具,这一版整个搬进 MCP 接口,让 AI agent 可以一句话完成整个替换。
203
+
204
+ 用法:
205
+
206
+ - **STRICT 模式**: 你已经有了新供应商的 URL,直接传 `new_supplier_url`,工具用 sku-matcher 对齐两边的 variant,按 `auto_confidence` 阈值决定哪些自动换、哪些保留老的、哪些标记 unmatched 让你手动决定
207
+ - **DISCOVER 模式**: 你只知道"这个供应商不能用了,帮我找一个替代",不传 URL,工具反向图搜 DSers 池,按 `sku 分 + 图搜频次 + 商品评分 + 店铺评分 + 价格接近度 + 库存 + 订单数` 的多因子公式打分排序,自动挑最好的候选
208
+
209
+ 两种模式都是两步走:永远先 `mode='preview'`(只读)看 `diffs` 和 `pool_additions`,确认没问题再 `mode='apply'` 真正写入。
210
+
211
+ 几个关键设计决定:
212
+
213
+ - **老供应商会归档到 mapping.pool[] 历史池**,不是直接丢掉。万一新供应商有问题,`dsers_sku_remap` 下一次跑可以重新考虑老 supplier 作为候选
214
+ - **pool 只增不减**,有一个 validator 专门守着这条不变式
215
+ - **写入前做 6 条结构校验**,包括 supplyProductId 数字格式、supplyVariantId 格式、optionId/valueId 对应关系,防止把 DSers 写脏
216
+ - **自动识别 Basic vs Standard mapping type**,根据 variant 选项对齐情况决定用哪种 mapping schema。对齐不全时强制 degrade 到 Standard 并在 warnings 里说明原因
217
+
218
+ #### 其它工具完善
219
+
220
+ 顺手把之前零散的 3 个工具也正式加进 inputSchema / 描述里(之前 1.4.x 已经写过代码但 manifest / server.json 这些元数据文件没同步,这次补上):
221
+
222
+ - `dsers_import_list` — 浏览导入待推送列表,带成本 / 售价 / 库存 / 加价状态
223
+ - `dsers_my_products` — 查看已经推到店铺的商品(附供应商链接方便重新导入)
224
+ - `dsers_find_product` — DSers 商品池搜索,支持关键词和以图搜图,结果能直接导入
225
+
226
+ 加上 `dsers_sku_remap`,这版从 9 个工具扩到 13 个。manifest.json 的 tools 数组之前漏了这 4 个,这版一起补齐。
227
+
228
+ #### 稳定性修复
229
+
230
+ `dsers_sku_remap` 在 round-1 到 round-5.7.5 中一共发现并修了 15 个问题。按照发现轮次排列的关键几条:
231
+
232
+ - **F1 Path A 候选抓取加 pool-detail fallback** — 当用户传的 supplier URL 已经被账号里别的 product 占用时,DSers import-list 会直接报 "already exists" 但没给 importListId,导致原来的 Path A 走 5-26 秒的 timeout。现在会自动 fallback 到 `product-pool/product/detail` 直接查,几百 ms 返回
233
+ - **F2 入口加 store 归属校验** — shape 合法但不属于账号的 store_id 之前会一路走到 Path B rank_candidates 之后才报"no viable match",看不懂什么意思。现在先跑 `getMyProducts(page:1,size:100)` 做归属检查,几百 ms 内出干净的 `store_id_mismatch` envelope
234
+ - **F3 MCP 边界输入 shape 守卫** — 空字符串 / 字母 / 200 位长数字 / 特殊符号的 `dsers_product_id` 现在都在 MCP 层 0-1ms 被 refuse,不进 handler
235
+ - **F4 CODEC 文案收敛** — DSers 后端的 CODEC reason 码之前会透出到用户看到的错误里(内容是 Go map 的文字表示,不可读),现在统一包装成 `store_id_mismatch` envelope 带 `dsers_store_discover` 和 `dsers_my_products` 的明确修复指引
236
+ - **M1 int64 范围生产级收敛** — store_id / dsers_product_id 的 19 位数字现在不光用 regex 校验,MCP 层加了 BigInt `refine()` 检查 signed int64 上界(9223372036854775807)。超界值直接在 MCP 边界被拒,避免踩 DSers 后端的 int64 解析 bug
237
+ - **O1 URL scheme 大小写规范化** — `HTTPS://` / `HttP://` 这种混合大小写的 scheme 之前虽然运行时通过但回显给用户的 source_url 保留原样。现在在 SUPPLIER_URL_SCHEMA 的 `.transform()` 里统一 lowercase scheme 前缀,path / host / query 完全不动(RFC 3986 说 scheme case-insensitive 但 path case-sensitive,Alibaba 的 `Widget_ABC_1600123456789.html` 必须保真)
238
+ - **O3 hard error 文本清理** — Path A fetch failure 抛出的错误之前会带 DSers 原始 JSON blob / Go nil / map[] 这些 raw 文本。现在有一个 `sanitizePrimaryErrorForWarning` helper 剥掉 JSON 只保留人话,尾部统一附 `(DSers API HTTP NNN)` 标记上游状态码
239
+ - **O-R4-3 checkStoreOwnership 加直连快路径** — 之前 wrong dsers_product_id 要走 list-scan 扫一整页 100 个产品才能说"不存在",耗时 1.3s。新版先用 `getMyProductDetail` 直连查询,wrong-product 失败时间降到和 wrong-store 同数量级(< 500ms)
240
+ - **O-R5-3 validator 接受 DSers 单 SKU `<none>` 哨兵** — DSers 对"Default Title"单 SKU 产品的真实存储是 `supplyVariantId:"<none>"` 字面字符串,这不是 bug 是生产数据。validator 的 check 4 的 pattern `\d+:\d+[#...]` 之前会误拒这种数据,导致账号里**所有单 SKU 产品**(测试账号里有 9 个)的 apply 都会失败。现在 validator 接受 `<none>` 哨兵,前提是 candidate 也是单 SKU(防御性 guard:防止 matcher 把多 SKU candidate 硬塞到哨兵槽)
241
+ - **O-R55-2 / B-R57-1 sku-matcher 单 SKU seller 快路径** — matcher 的维度对齐路径对"seller 是单 SKU Default Title + candidate 是 1v 真实维度名(比如 `{Color, black}`)"这种退化场景会判 0 分 unmatched。新增一个 early-return 快路径: 当 seller 是 trivial 单 SKU(lean 或 Default Title 哨兵),candidate 不管什么形态,都按 `single_sku_seller_trivial` 策略配对,confidence 75。图像路径保留,candidate 多 SKU 时按 dHash 挑最接近的 variant,其它作为 alternatives 返回。**既有算法主体一行未动**(这点经过一周独立验证,不想动),shortcut 只是 early-return。fat-fat 场景(两边都是 Default Title 哨兵)还是走原算法的 exact 80 分,不被 shortcut 抢占
242
+
243
+ #### 文件更新
244
+
245
+ - 修了 `app/dropshipping/[transport]/route.ts` 里硬编码的 `version: "1.3.8"`(遗留 bug,历次发版没同步到)
246
+ - `manifest.json` 补齐 4 个漏掉的 tool 描述
247
+ - README 里 `test count (343)` 同步到实际的 585
248
+ - 新增 `CHANGELOG.md`
249
+
250
+ #### 没做的事(留给后续版本)
251
+
252
+ - `dsers_my_products` 的 `page` 分页参数现在实际不生效(DSers 后端行为,不是本工具的 bug),workaround 是用 `page_size:100` 一次拉完。账号产品超过 100 的用户会撞到这个,另开 issue 跟进
253
+ - DSers GET mapping 对多 supplier(MapAgc)产品返回的 `mappings[]` 数组顺序非确定(服务器行为,10 次里 9 次是顺序 A,1 次是顺序 B,值完全一样)。对做 byte-level diff 的工具链有影响,我们的 validator 不依赖 byte-level 所以不受影响,但使用者要注意
254
+ - `dsers_sku_remap` 的 discover 模式 Path B 对 vp* 虚拟产品(DSers 内部合成的)无效,因为 virtual product 没有 seed 图可以反搜。这是 DSers 侧的限制,不是我们能修的
255
+
256
+ #### 回归验证
257
+
258
+ 这一版从 round-1 做到 round-5.7.5,每轮都是独立 QA 会话(不读前面任何报告、HANDOFF、commit body、测试代码),跑完整 e2e 回归 + 真实 DSers 写入 + 字节级 rollback。585 个单元测试全过,最后一轮 0 Blocker / 0 Major / 0 Minor / 0 Observation。
259
+
260
+ ---
261
+
262
+ ### 1.4.x 之前
263
+
264
+ 之前的版本在 git history 里,主要是 OAuth 2.1 + 远程 MCP + Vercel 部署相关。没有集中的 changelog,看 `git log --oneline v1.4.0..v1.5.0` 或者各 commit message。
package/README.md CHANGED
@@ -76,9 +76,10 @@ This works for both AliExpress and Alibaba products found on Accio.
76
76
 
77
77
  ### What You Need
78
78
 
79
+ - **Node.js** >= 20.0.0
79
80
  - A [DSers](https://www.dsers.com/) account (free plan works)
80
81
  - A Shopify or Wix store already connected in DSers
81
- - An MCP-compatible AI client — [Cursor](https://cursor.sh/), [Claude Desktop](https://claude.ai/desktop), [Windsurf](https://codeium.com/windsurf), or any client that supports MCP
82
+ - An MCP-compatible AI client — [Cursor](https://cursor.sh/), [Claude Desktop](https://claude.ai/desktop), [Claude Managed Agents](https://platform.claude.com/docs/en/managed-agents/overview), [Windsurf](https://codeium.com/windsurf), or any client that supports MCP
82
83
 
83
84
  ### Quick Start
84
85
 
@@ -121,6 +122,39 @@ If you don't want to install anything locally, you can connect directly to the h
121
122
 
122
123
  Also listed on the official [MCP Registry](https://registry.modelcontextprotocol.io/servers/io.github.lofder/dsers-mcp-product).
123
124
 
125
+ **Claude Managed Agents ([docs](https://platform.claude.com/docs/en/managed-agents/overview)):**
126
+
127
+ Build autonomous dropshipping agents that run 24/7 in Anthropic's managed infrastructure. Connect this MCP server via the [Claude Agent SDK](https://platform.claude.com/docs/en/agent-sdk/mcp):
128
+
129
+ ```python
130
+ from claude_agent_sdk import query, ClaudeAgentOptions
131
+
132
+ async for message in query(
133
+ prompt="Find a cheaper supplier for product dp-123 in store st-456 and update the mapping",
134
+ options=ClaudeAgentOptions(
135
+ mcp_servers={
136
+ "dsers": {"command": "npx", "args": ["-y", "@lofder/dsers-mcp-product"]}
137
+ }
138
+ ),
139
+ ):
140
+ print(message)
141
+ ```
142
+
143
+ ```typescript
144
+ import { query } from "@anthropic-ai/claude-agent-sdk";
145
+
146
+ for await (const message of query({
147
+ prompt: "Find a cheaper supplier for product dp-123 in store st-456 and update the mapping",
148
+ options: {
149
+ mcpServers: {
150
+ dsers: { command: "npx", args: ["-y", "@lofder/dsers-mcp-product"] }
151
+ }
152
+ }
153
+ })) {
154
+ console.log(message);
155
+ }
156
+ ```
157
+
124
158
  ### Authentication — OAuth 2.1
125
159
 
126
160
  Your DSers password **never touches this tool**. Here's how login works:
@@ -142,6 +176,8 @@ npx @lofder/dsers-mcp-product login
142
176
 
143
177
  > **For developers:** The server also accepts `DSERS_ACCESS_TOKEN` and `DSERS_REFRESH_TOKEN` env vars for headless/CI environments.
144
178
 
179
+ > **Working from source (not the published npm package)?** Use `node dist/cli.js login` instead of `npx @lofder/dsers-mcp-product login`. The `npx` form requires the `dsers-mcp-product` binary to resolve through your `PATH`, which only happens after `npm link` (or after a real npm install). In a fresh local clone the `npx` command exits with `command not found` (exit code 127). Run `npm run build` first so `dist/cli.js` exists.
180
+
145
181
  ### Usage Examples
146
182
 
147
183
  Once set up, just talk to your AI agent in plain language:
@@ -190,7 +226,7 @@ dsers-mcp-product/
190
226
  │ ├── cli.ts # npx entry — stdio transport, login/logout
191
227
  │ ├── index.ts # MCP server init, tool registration
192
228
  │ ├── instructions.ts # Server-level prompts (agent instructions)
193
- │ ├── tools.ts # 12 MCP tools — schema + handler
229
+ │ ├── tools.ts # 13 MCP tools — schema + handler
194
230
  │ ├── rules.ts # Rule validation & application engine
195
231
  │ ├── push-guard.ts # Pre-push safety checks
196
232
  │ ├── push-options.ts # Push option normalization
@@ -219,12 +255,12 @@ dsers-mcp-product/
219
255
  │ │ ├── product.ts # Product & import APIs
220
256
  │ │ └── settings.ts # Shipping, pricing, billing APIs
221
257
  │ └── auth/ # Browser login (CDP)
222
- ├── test/ # Vitest unit tests (298 tests)
258
+ ├── test/ # Vitest unit tests (585 tests)
223
259
  ├── package.json
224
260
  └── tsconfig.json
225
261
  ```
226
262
 
227
- ### Twelve Tools
263
+ ### Thirteen Tools
228
264
 
229
265
  | # | Tool | What it does |
230
266
  |---|------|-------------|
@@ -240,6 +276,7 @@ dsers-mcp-product/
240
276
  | 10 | `dsers_import_list` | Browse your import staging list with cost & sell price, stock, markup status |
241
277
  | 11 | `dsers_my_products` | See products already pushed to a store, with supplier links for re-import |
242
278
  | 12 | `dsers_find_product` | Search the DSers product pool by keyword or image — results link directly to import |
279
+ | 13 | `dsers_sku_remap` | Replace the supplier on an existing store product at the SKU level. Two modes: provide `new_supplier_url` for a strict swap, or omit it to reverse-image-search the DSers pool and auto-pick the best replacement via multi-factor ranking. Always run `mode='preview'` first (read-only) and inspect `diffs` + `pool_additions` before `mode='apply'`. Requires the `product:mapping` OAuth scope. |
243
280
 
244
281
  All tools return clear error messages so your AI agent knows what went wrong and what to do next.
245
282
 
@@ -302,7 +339,7 @@ Yes. The tool is open-source (MIT license) and completely free to use. You only
302
339
  No passwords are stored or transmitted. Authentication uses a zero-password browser login — you log in on DSers's own website, and the tool picks up the session token. Your credentials never touch the MCP server. The project scored 92/100 on [SafeSkill](https://safeskill.dev/scan/@lofder/dsers-mcp-product) security scanning.
303
340
 
304
341
  **What AI clients does it support?**
305
- Cursor, Claude Desktop, Claude Code, Windsurf, and any MCP-compatible client that supports stdio transport.
342
+ Cursor, Claude Desktop, Claude Code, Claude Managed Agents, Windsurf, and any MCP-compatible client that supports stdio transport.
306
343
 
307
344
  **How is this different from AliDropify, AutoDS, or other dropshipping tools?**
308
345
  Most dropshipping tools have their own UI and require you to click through web interfaces. DSers MCP Product takes a fundamentally different approach — it connects directly to your AI agent, so you automate workflows through conversation instead of clicking buttons. It's also open-source and free, with no subscription tiers.
@@ -389,7 +426,7 @@ Accio 上搜出来的速卖通和阿里巴巴商品都能用。
389
426
 
390
427
  - 一个 [DSers](https://www.dsers.com/) 账号(免费版就行)
391
428
  - Shopify 或 Wix 店铺已经在 DSers 里绑定好了
392
- - 一个支持 MCP 的 AI 客户端 — [Cursor](https://cursor.sh/)、[Claude Desktop](https://claude.ai/desktop)、[Windsurf](https://codeium.com/windsurf) 或其他支持 MCP 的工具
429
+ - 一个支持 MCP 的 AI 客户端 — [Cursor](https://cursor.sh/)、[Claude Desktop](https://claude.ai/desktop)、[Claude Managed Agents](https://platform.claude.com/docs/en/managed-agents/overview)、[Windsurf](https://codeium.com/windsurf) 或其他支持 MCP 的工具
393
430
 
394
431
  ### 快速开始
395
432
 
@@ -432,6 +469,24 @@ npx @lofder/dsers-mcp-product login
432
469
 
433
470
  同时已收录到官方 [MCP Registry](https://registry.modelcontextprotocol.io/servers/io.github.lofder/dsers-mcp-product)。
434
471
 
472
+ **Claude Managed Agents ([文档](https://platform.claude.com/docs/en/managed-agents/overview)):**
473
+
474
+ 通过 [Claude Agent SDK](https://platform.claude.com/docs/en/agent-sdk/mcp) 构建 7×24 自主运行的 dropshipping agent:
475
+
476
+ ```python
477
+ from claude_agent_sdk import query, ClaudeAgentOptions
478
+
479
+ async for message in query(
480
+ prompt="帮商品 dp-123 在店铺 st-456 找个更便宜的供应商并更新映射",
481
+ options=ClaudeAgentOptions(
482
+ mcp_servers={
483
+ "dsers": {"command": "npx", "args": ["-y", "@lofder/dsers-mcp-product"]}
484
+ }
485
+ ),
486
+ ):
487
+ print(message)
488
+ ```
489
+
435
490
  ### 授权认证 — OAuth 2.1
436
491
 
437
492
  你的 DSers 密码**完全不经过本工具**。登录过程是这样的:
@@ -453,6 +508,8 @@ npx @lofder/dsers-mcp-product login
453
508
 
454
509
  > **开发者注:** headless / CI 环境支持通过 `DSERS_ACCESS_TOKEN` 和 `DSERS_REFRESH_TOKEN` 环境变量传入凭据。
455
510
 
511
+ > **从源码工作(不是从 npm 包跑)?** 登录用 `node dist/cli.js login`,**不是** `npx @lofder/dsers-mcp-product login`。后者要求 `dsers-mcp-product` binary 解析到 PATH,本地 workspace 没 `npm link`(或者没真正 npm install)的话会 `command not found` 直接退出码 127。先 `npm run build` 让 `dist/cli.js` 存在再跑。
512
+
456
513
  ### 使用示例
457
514
 
458
515
  装好之后,直接用自然语言跟 AI 助手说就行:
@@ -493,7 +550,7 @@ npm run build
493
550
  npm test
494
551
  ```
495
552
 
496
- ### 十二个工具
553
+ ### 十三个工具
497
554
 
498
555
  | # | 工具 | 干什么的 |
499
556
  |---|------|---------|
@@ -509,6 +566,7 @@ npm test
509
566
  | 10 | `dsers_import_list` | 浏览导入待推送列表,含成本价、售价、库存、加价状态 |
510
567
  | 11 | `dsers_my_products` | 查看已推到店铺的商品,带供应商链接方便重新导入 |
511
568
  | 12 | `dsers_find_product` | 在 DSers 商品池搜索,支持关键词和以图搜图,结果可直接导入 |
569
+ | 13 | `dsers_sku_remap` | SKU 级别替换已上架商品的供应商。两种模式:传 `new_supplier_url` 走精确替换,不传则反向图搜 DSers 池 + 多因子打分自动挑最佳替代。务必先用 `mode='preview'`(只读)看 `diffs` 和 `pool_additions`,确认无误后再 `mode='apply'`。需要 `product:mapping` OAuth scope。 |
512
570
 
513
571
  报错时会返回清晰的消息,AI 助手能看懂出了什么问题、该怎么办。
514
572
 
@@ -1 +1 @@
1
- {"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../../src/auth/oauth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAeH,MAAM,WAAW,WAAW;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,EAAE,EAAE,MAAM,CAAC;CACZ;AA+GD;;;GAGG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,EAC1B,aAAa,CAAC,EAAE,MAAM,EACtB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,WAAW,CAAC,CAwCtB;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,CAsB9E"}
1
+ {"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../../src/auth/oauth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAeH,MAAM,WAAW,WAAW;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,EAAE,EAAE,MAAM,CAAC;CACZ;AAiKD;;;GAGG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,EAC1B,aAAa,CAAC,EAAE,MAAM,EACtB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,WAAW,CAAC,CA+CtB;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,CAsB9E"}