@lofder/dsers-mcp-product 1.3.8 → 1.5.0
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/CHANGELOG.md +71 -0
- package/README.md +107 -26
- package/dist/auth/browser-finder.d.ts +5 -5
- package/dist/auth/browser-finder.d.ts.map +1 -1
- package/dist/auth/browser-finder.js +13 -100
- package/dist/auth/browser-finder.js.map +1 -1
- package/dist/auth/index.d.ts +3 -5
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +3 -5
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/oauth.d.ts +36 -0
- package/dist/auth/oauth.d.ts.map +1 -0
- package/dist/auth/oauth.js +173 -0
- package/dist/auth/oauth.js.map +1 -0
- package/dist/auth/token-store.d.ts +10 -2
- package/dist/auth/token-store.d.ts.map +1 -1
- package/dist/auth/token-store.js +27 -2
- package/dist/auth/token-store.js.map +1 -1
- package/dist/cli.js +38 -63
- package/dist/cli.js.map +1 -1
- package/dist/dsers/auth.d.ts +5 -6
- package/dist/dsers/auth.d.ts.map +1 -1
- package/dist/dsers/auth.js +72 -88
- package/dist/dsers/auth.js.map +1 -1
- package/dist/dsers/client.d.ts +9 -2
- package/dist/dsers/client.d.ts.map +1 -1
- package/dist/dsers/client.js +24 -10
- package/dist/dsers/client.js.map +1 -1
- package/dist/dsers/config.d.ts +19 -16
- package/dist/dsers/config.d.ts.map +1 -1
- package/dist/dsers/config.js +22 -31
- package/dist/dsers/config.js.map +1 -1
- package/dist/dsers/product.d.ts +56 -0
- package/dist/dsers/product.d.ts.map +1 -1
- package/dist/dsers/product.js +64 -0
- package/dist/dsers/product.js.map +1 -1
- package/dist/dsers/retry.d.ts +59 -0
- package/dist/dsers/retry.d.ts.map +1 -0
- package/dist/dsers/retry.js +214 -0
- package/dist/dsers/retry.js.map +1 -0
- package/dist/error-map.d.ts.map +1 -1
- package/dist/error-map.js +15 -3
- package/dist/error-map.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -10
- package/dist/index.js.map +1 -1
- package/dist/instructions.d.ts.map +1 -1
- package/dist/instructions.js +14 -2
- package/dist/instructions.js.map +1 -1
- package/dist/logger.d.ts +55 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +122 -0
- package/dist/logger.js.map +1 -0
- package/dist/provider/sku-matcher.d.ts +154 -0
- package/dist/provider/sku-matcher.d.ts.map +1 -0
- package/dist/provider/sku-matcher.js +1175 -0
- package/dist/provider/sku-matcher.js.map +1 -0
- package/dist/service/index.d.ts +2 -0
- package/dist/service/index.d.ts.map +1 -1
- package/dist/service/index.js +7 -0
- package/dist/service/index.js.map +1 -1
- package/dist/service/preview.d.ts +1 -1
- package/dist/service/preview.d.ts.map +1 -1
- package/dist/service/preview.js +48 -21
- package/dist/service/preview.js.map +1 -1
- package/dist/service/sku-mapping.d.ts +674 -0
- package/dist/service/sku-mapping.d.ts.map +1 -0
- package/dist/service/sku-mapping.js +1879 -0
- package/dist/service/sku-mapping.js.map +1 -0
- package/dist/tools.d.ts +3 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +322 -2
- package/dist/tools.js.map +1 -1
- package/package.json +5 -2
- package/dist/auth/cdp-session.d.ts +0 -6
- package/dist/auth/cdp-session.d.ts.map +0 -1
- package/dist/auth/cdp-session.js +0 -369
- package/dist/auth/cdp-session.js.map +0 -1
- package/dist/auth/safari-fallback.d.ts +0 -7
- package/dist/auth/safari-fallback.d.ts.map +0 -1
- package/dist/auth/safari-fallback.js +0 -73
- package/dist/auth/safari-fallback.js.map +0 -1
- package/dist/auth/terminal-prompt.d.ts +0 -6
- package/dist/auth/terminal-prompt.d.ts.map +0 -1
- package/dist/auth/terminal-prompt.js +0 -118
- package/dist/auth/terminal-prompt.js.map +0 -1
- package/dist/provider.d.ts +0 -90
- package/dist/provider.d.ts.map +0 -1
- package/dist/provider.js +0 -1577
- package/dist/provider.js.map +0 -1
- package/dist/service/browse.d.ts +0 -20
- package/dist/service/browse.d.ts.map +0 -1
- package/dist/service/browse.js +0 -159
- package/dist/service/browse.js.map +0 -1
- package/dist/service.d.ts +0 -27
- package/dist/service.d.ts.map +0 -1
- package/dist/service.js +0 -949
- package/dist/service.js.map +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.5.0 — 2026-04-10
|
|
4
|
+
|
|
5
|
+
大版本。这版终于把换供应商这件事搞定了。
|
|
6
|
+
|
|
7
|
+
### 新工具:`dsers_sku_remap`
|
|
8
|
+
|
|
9
|
+
背景是之前一直有一个绕不过去的场景 —— 老供应商涨价 / 断货 / 被封了,你想换一个新的。DSers 后台手动点也能换,但是每个变体每个变体点下去非常反人类,更不用说你要把 seller 的 variant 精确对应到新 supplier 的哪个 SKU。我之前做了一个单独的 sku-matcher 引擎(用 option 对齐 + 单位规范化 + 同义词表 + dHash 图像相似度做打分),原本是内部工具,这一版整个搬进 MCP 接口,让 AI agent 可以一句话完成整个替换。
|
|
10
|
+
|
|
11
|
+
用法:
|
|
12
|
+
|
|
13
|
+
- **STRICT 模式**: 你已经有了新供应商的 URL,直接传 `new_supplier_url`,工具用 sku-matcher 对齐两边的 variant,按 `auto_confidence` 阈值决定哪些自动换、哪些保留老的、哪些标记 unmatched 让你手动决定
|
|
14
|
+
- **DISCOVER 模式**: 你只知道"这个供应商不能用了,帮我找一个替代",不传 URL,工具反向图搜 DSers 池,按 `sku 分 + 图搜频次 + 商品评分 + 店铺评分 + 价格接近度 + 库存 + 订单数` 的多因子公式打分排序,自动挑最好的候选
|
|
15
|
+
|
|
16
|
+
两种模式都是两步走:永远先 `mode='preview'`(只读)看 `diffs` 和 `pool_additions`,确认没问题再 `mode='apply'` 真正写入。
|
|
17
|
+
|
|
18
|
+
几个关键设计决定:
|
|
19
|
+
|
|
20
|
+
- **老供应商会归档到 mapping.pool[] 历史池**,不是直接丢掉。万一新供应商有问题,`dsers_sku_remap` 下一次跑可以重新考虑老 supplier 作为候选
|
|
21
|
+
- **pool 只增不减**,有一个 validator 专门守着这条不变式
|
|
22
|
+
- **写入前做 6 条结构校验**,包括 supplyProductId 数字格式、supplyVariantId 格式、optionId/valueId 对应关系,防止把 DSers 写脏
|
|
23
|
+
- **自动识别 Basic vs Standard mapping type**,根据 variant 选项对齐情况决定用哪种 mapping schema。对齐不全时强制 degrade 到 Standard 并在 warnings 里说明原因
|
|
24
|
+
|
|
25
|
+
### 其它工具完善
|
|
26
|
+
|
|
27
|
+
顺手把之前零散的 3 个工具也正式加进 inputSchema / 描述里(之前 1.4.x 已经写过代码但 manifest / server.json 这些元数据文件没同步,这次补上):
|
|
28
|
+
|
|
29
|
+
- `dsers_import_list` — 浏览导入待推送列表,带成本 / 售价 / 库存 / 加价状态
|
|
30
|
+
- `dsers_my_products` — 查看已经推到店铺的商品(附供应商链接方便重新导入)
|
|
31
|
+
- `dsers_find_product` — DSers 商品池搜索,支持关键词和以图搜图,结果能直接导入
|
|
32
|
+
|
|
33
|
+
加上 `dsers_sku_remap`,这版从 9 个工具扩到 13 个。manifest.json 的 tools 数组之前漏了这 4 个,这版一起补齐。
|
|
34
|
+
|
|
35
|
+
### 稳定性修复
|
|
36
|
+
|
|
37
|
+
`dsers_sku_remap` 在 round-1 到 round-5.7.5 中一共发现并修了 15 个问题。按照发现轮次排列的关键几条:
|
|
38
|
+
|
|
39
|
+
- **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 返回
|
|
40
|
+
- **F2 入口加 store 归属校验** — shape 合法但不属于账号的 store_id 之前会一路走到 Path B rank_candidates 之后才报"no viable match",看不懂什么意思。现在先跑 `getMyProducts(page:1,size:100)` 做归属检查,几百 ms 内出干净的 `store_id_mismatch` envelope
|
|
41
|
+
- **F3 MCP 边界输入 shape 守卫** — 空字符串 / 字母 / 200 位长数字 / 特殊符号的 `dsers_product_id` 现在都在 MCP 层 0-1ms 被 refuse,不进 handler
|
|
42
|
+
- **F4 CODEC 文案收敛** — DSers 后端的 CODEC reason 码之前会透出到用户看到的错误里(内容是 Go map 的文字表示,不可读),现在统一包装成 `store_id_mismatch` envelope 带 `dsers_store_discover` 和 `dsers_my_products` 的明确修复指引
|
|
43
|
+
- **M1 int64 范围生产级收敛** — store_id / dsers_product_id 的 19 位数字现在不光用 regex 校验,MCP 层加了 BigInt `refine()` 检查 signed int64 上界(9223372036854775807)。超界值直接在 MCP 边界被拒,避免踩 DSers 后端的 int64 解析 bug
|
|
44
|
+
- **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` 必须保真)
|
|
45
|
+
- **O3 hard error 文本清理** — Path A fetch failure 抛出的错误之前会带 DSers 原始 JSON blob / Go nil / map[] 这些 raw 文本。现在有一个 `sanitizePrimaryErrorForWarning` helper 剥掉 JSON 只保留人话,尾部统一附 `(DSers API HTTP NNN)` 标记上游状态码
|
|
46
|
+
- **O-R4-3 checkStoreOwnership 加直连快路径** — 之前 wrong dsers_product_id 要走 list-scan 扫一整页 100 个产品才能说"不存在",耗时 1.3s。新版先用 `getMyProductDetail` 直连查询,wrong-product 失败时间降到和 wrong-store 同数量级(< 500ms)
|
|
47
|
+
- **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 硬塞到哨兵槽)
|
|
48
|
+
- **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 抢占
|
|
49
|
+
|
|
50
|
+
### 文件更新
|
|
51
|
+
|
|
52
|
+
- 修了 `app/dropshipping/[transport]/route.ts` 里硬编码的 `version: "1.3.8"`(遗留 bug,历次发版没同步到)
|
|
53
|
+
- `manifest.json` 补齐 4 个漏掉的 tool 描述
|
|
54
|
+
- README 里 `test count (343)` 同步到实际的 585
|
|
55
|
+
- 新增 `CHANGELOG.md`
|
|
56
|
+
|
|
57
|
+
### 没做的事(留给后续版本)
|
|
58
|
+
|
|
59
|
+
- `dsers_my_products` 的 `page` 分页参数现在实际不生效(DSers 后端行为,不是本工具的 bug),workaround 是用 `page_size:100` 一次拉完。账号产品超过 100 的用户会撞到这个,另开 issue 跟进
|
|
60
|
+
- DSers GET mapping 对多 supplier(MapAgc)产品返回的 `mappings[]` 数组顺序非确定(服务器行为,10 次里 9 次是顺序 A,1 次是顺序 B,值完全一样)。对做 byte-level diff 的工具链有影响,我们的 validator 不依赖 byte-level 所以不受影响,但使用者要注意
|
|
61
|
+
- `dsers_sku_remap` 的 discover 模式 Path B 对 vp* 虚拟产品(DSers 内部合成的)无效,因为 virtual product 没有 seed 图可以反搜。这是 DSers 侧的限制,不是我们能修的
|
|
62
|
+
|
|
63
|
+
### 回归验证
|
|
64
|
+
|
|
65
|
+
这一版从 round-1 做到 round-5.7.5,每轮都是独立 QA 会话(不读前面任何报告、HANDOFF、commit body、测试代码),跑完整 e2e 回归 + 真实 DSers 写入 + 字节级 rollback。585 个单元测试全过,最后一轮 0 Blocker / 0 Major / 0 Minor / 0 Observation。
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 1.4.x
|
|
70
|
+
|
|
71
|
+
之前的版本在 git history 里,主要是 OAuth 2.1 + 远程 MCP + Vercel 部署相关。没有集中的 changelog,看 `git log --oneline v1.4.0..v1.5.0` 或者各 commit message。
|
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
- **Safety checks** — automatically blocks pushes that would result in below-cost pricing, zero price, or zero stock
|
|
31
31
|
- **SEO optimization** — let AI rewrite the title and description for better search rankings before pushing
|
|
32
32
|
|
|
33
|
-
The server is hosted on [Vercel](https://
|
|
33
|
+
The server is hosted on [Vercel](https://ai.silentrillmcp.com/dropshipping/mcp) and published across multiple platforms:
|
|
34
34
|
|
|
35
35
|
### Available On
|
|
36
36
|
|
|
@@ -78,7 +78,7 @@ This works for both AliExpress and Alibaba products found on Accio.
|
|
|
78
78
|
|
|
79
79
|
- A [DSers](https://www.dsers.com/) account (free plan works)
|
|
80
80
|
- 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
|
|
81
|
+
- 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
82
|
|
|
83
83
|
### Quick Start
|
|
84
84
|
|
|
@@ -105,21 +105,66 @@ A browser window opens to the official DSers login page. You log in on DSers's o
|
|
|
105
105
|
|
|
106
106
|
That's it. No passwords in config files.
|
|
107
107
|
|
|
108
|
+
**Remote server (no install needed):**
|
|
109
|
+
|
|
110
|
+
If you don't want to install anything locally, you can connect directly to the hosted MCP server at `https://ai.silentrillmcp.com/dropshipping/mcp`. This works with any MCP client that supports Streamable HTTP transport. You'll be prompted to authorize with your DSers account on first connect.
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{
|
|
114
|
+
"mcpServers": {
|
|
115
|
+
"dropshipping": {
|
|
116
|
+
"url": "https://ai.silentrillmcp.com/dropshipping/mcp"
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
108
122
|
Also listed on the official [MCP Registry](https://registry.modelcontextprotocol.io/servers/io.github.lofder/dsers-mcp-product).
|
|
109
123
|
|
|
110
|
-
|
|
124
|
+
**Claude Managed Agents ([docs](https://platform.claude.com/docs/en/managed-agents/overview)):**
|
|
125
|
+
|
|
126
|
+
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):
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from claude_agent_sdk import query, ClaudeAgentOptions
|
|
130
|
+
|
|
131
|
+
async for message in query(
|
|
132
|
+
prompt="Find a cheaper supplier for product dp-123 in store st-456 and update the mapping",
|
|
133
|
+
options=ClaudeAgentOptions(
|
|
134
|
+
mcp_servers={
|
|
135
|
+
"dsers": {"command": "npx", "args": ["-y", "@lofder/dsers-mcp-product"]}
|
|
136
|
+
}
|
|
137
|
+
),
|
|
138
|
+
):
|
|
139
|
+
print(message)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
144
|
+
|
|
145
|
+
for await (const message of query({
|
|
146
|
+
prompt: "Find a cheaper supplier for product dp-123 in store st-456 and update the mapping",
|
|
147
|
+
options: {
|
|
148
|
+
mcpServers: {
|
|
149
|
+
dsers: { command: "npx", args: ["-y", "@lofder/dsers-mcp-product"] }
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
})) {
|
|
153
|
+
console.log(message);
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Authentication — OAuth 2.1
|
|
111
158
|
|
|
112
159
|
Your DSers password **never touches this tool**. Here's how login works:
|
|
113
160
|
|
|
114
161
|
1. You run `npx @lofder/dsers-mcp-product login`
|
|
115
|
-
2. Your browser opens the
|
|
116
|
-
3. You
|
|
117
|
-
4. The tool
|
|
118
|
-
5. Done —
|
|
119
|
-
|
|
120
|
-
Works with Chrome, Edge, Brave, and other Chromium browsers. On Mac, Safari works too.
|
|
162
|
+
2. Your browser opens the DSers OAuth authorization page
|
|
163
|
+
3. You authorize access on DSers's own website
|
|
164
|
+
4. The tool receives an OAuth token and encrypts it locally
|
|
165
|
+
5. Done — the MCP server uses the token automatically. No passwords in config files.
|
|
121
166
|
|
|
122
|
-
**
|
|
167
|
+
**Tokens refresh automatically** — a long-lived refresh token keeps your session active without manual re-login. You should rarely need to run `login` again.
|
|
123
168
|
|
|
124
169
|
**Switching accounts?**
|
|
125
170
|
|
|
@@ -128,7 +173,9 @@ npx @lofder/dsers-mcp-product logout
|
|
|
128
173
|
npx @lofder/dsers-mcp-product login
|
|
129
174
|
```
|
|
130
175
|
|
|
131
|
-
> **For developers:** The server also accepts
|
|
176
|
+
> **For developers:** The server also accepts `DSERS_ACCESS_TOKEN` and `DSERS_REFRESH_TOKEN` env vars for headless/CI environments.
|
|
177
|
+
|
|
178
|
+
> **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.
|
|
132
179
|
|
|
133
180
|
### Usage Examples
|
|
134
181
|
|
|
@@ -178,7 +225,7 @@ dsers-mcp-product/
|
|
|
178
225
|
│ ├── cli.ts # npx entry — stdio transport, login/logout
|
|
179
226
|
│ ├── index.ts # MCP server init, tool registration
|
|
180
227
|
│ ├── instructions.ts # Server-level prompts (agent instructions)
|
|
181
|
-
│ ├── tools.ts #
|
|
228
|
+
│ ├── tools.ts # 13 MCP tools — schema + handler
|
|
182
229
|
│ ├── rules.ts # Rule validation & application engine
|
|
183
230
|
│ ├── push-guard.ts # Pre-push safety checks
|
|
184
231
|
│ ├── push-options.ts # Push option normalization
|
|
@@ -207,12 +254,12 @@ dsers-mcp-product/
|
|
|
207
254
|
│ │ ├── product.ts # Product & import APIs
|
|
208
255
|
│ │ └── settings.ts # Shipping, pricing, billing APIs
|
|
209
256
|
│ └── auth/ # Browser login (CDP)
|
|
210
|
-
├── test/ # Vitest unit tests (
|
|
257
|
+
├── test/ # Vitest unit tests (585 tests)
|
|
211
258
|
├── package.json
|
|
212
259
|
└── tsconfig.json
|
|
213
260
|
```
|
|
214
261
|
|
|
215
|
-
###
|
|
262
|
+
### Thirteen Tools
|
|
216
263
|
|
|
217
264
|
| # | Tool | What it does |
|
|
218
265
|
|---|------|-------------|
|
|
@@ -228,6 +275,7 @@ dsers-mcp-product/
|
|
|
228
275
|
| 10 | `dsers_import_list` | Browse your import staging list with cost & sell price, stock, markup status |
|
|
229
276
|
| 11 | `dsers_my_products` | See products already pushed to a store, with supplier links for re-import |
|
|
230
277
|
| 12 | `dsers_find_product` | Search the DSers product pool by keyword or image — results link directly to import |
|
|
278
|
+
| 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. |
|
|
231
279
|
|
|
232
280
|
All tools return clear error messages so your AI agent knows what went wrong and what to do next.
|
|
233
281
|
|
|
@@ -290,7 +338,7 @@ Yes. The tool is open-source (MIT license) and completely free to use. You only
|
|
|
290
338
|
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.
|
|
291
339
|
|
|
292
340
|
**What AI clients does it support?**
|
|
293
|
-
Cursor, Claude Desktop, Claude Code, Windsurf, and any MCP-compatible client that supports stdio transport.
|
|
341
|
+
Cursor, Claude Desktop, Claude Code, Claude Managed Agents, Windsurf, and any MCP-compatible client that supports stdio transport.
|
|
294
342
|
|
|
295
343
|
**How is this different from AliDropify, AutoDS, or other dropshipping tools?**
|
|
296
344
|
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.
|
|
@@ -329,7 +377,7 @@ MIT
|
|
|
329
377
|
- **安全校验** — 推送前自动拦截低于成本价、零售价为零、库存为零的商品
|
|
330
378
|
- **SEO 优化** — 让 AI 重写标题和描述,提高搜索排名后再推送
|
|
331
379
|
|
|
332
|
-
服务已托管在 [Vercel](https://
|
|
380
|
+
服务已托管在 [Vercel](https://ai.silentrillmcp.com/dropshipping/mcp),并发布到多个平台:
|
|
333
381
|
|
|
334
382
|
### 发布平台
|
|
335
383
|
|
|
@@ -377,7 +425,7 @@ Accio 上搜出来的速卖通和阿里巴巴商品都能用。
|
|
|
377
425
|
|
|
378
426
|
- 一个 [DSers](https://www.dsers.com/) 账号(免费版就行)
|
|
379
427
|
- Shopify 或 Wix 店铺已经在 DSers 里绑定好了
|
|
380
|
-
- 一个支持 MCP 的 AI 客户端 — [Cursor](https://cursor.sh/)、[Claude Desktop](https://claude.ai/desktop)、[Windsurf](https://codeium.com/windsurf) 或其他支持 MCP 的工具
|
|
428
|
+
- 一个支持 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 的工具
|
|
381
429
|
|
|
382
430
|
### 快速开始
|
|
383
431
|
|
|
@@ -404,21 +452,51 @@ npx @lofder/dsers-mcp-product login
|
|
|
404
452
|
|
|
405
453
|
搞定。配置文件里不需要任何密码。
|
|
406
454
|
|
|
455
|
+
**在线版(免安装):**
|
|
456
|
+
|
|
457
|
+
如果不想在本地安装,可以直接连接托管的 MCP 服务端 `https://ai.silentrillmcp.com/dropshipping/mcp`。支持 Streamable HTTP transport 的 MCP 客户端都能用,首次连接会引导你授权 DSers 账号。
|
|
458
|
+
|
|
459
|
+
```json
|
|
460
|
+
{
|
|
461
|
+
"mcpServers": {
|
|
462
|
+
"dropshipping": {
|
|
463
|
+
"url": "https://ai.silentrillmcp.com/dropshipping/mcp"
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
```
|
|
468
|
+
|
|
407
469
|
同时已收录到官方 [MCP Registry](https://registry.modelcontextprotocol.io/servers/io.github.lofder/dsers-mcp-product)。
|
|
408
470
|
|
|
409
|
-
|
|
471
|
+
**Claude Managed Agents ([文档](https://platform.claude.com/docs/en/managed-agents/overview)):**
|
|
472
|
+
|
|
473
|
+
通过 [Claude Agent SDK](https://platform.claude.com/docs/en/agent-sdk/mcp) 构建 7×24 自主运行的 dropshipping agent:
|
|
474
|
+
|
|
475
|
+
```python
|
|
476
|
+
from claude_agent_sdk import query, ClaudeAgentOptions
|
|
477
|
+
|
|
478
|
+
async for message in query(
|
|
479
|
+
prompt="帮商品 dp-123 在店铺 st-456 找个更便宜的供应商并更新映射",
|
|
480
|
+
options=ClaudeAgentOptions(
|
|
481
|
+
mcp_servers={
|
|
482
|
+
"dsers": {"command": "npx", "args": ["-y", "@lofder/dsers-mcp-product"]}
|
|
483
|
+
}
|
|
484
|
+
),
|
|
485
|
+
):
|
|
486
|
+
print(message)
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
### 授权认证 — OAuth 2.1
|
|
410
490
|
|
|
411
491
|
你的 DSers 密码**完全不经过本工具**。登录过程是这样的:
|
|
412
492
|
|
|
413
493
|
1. 运行 `npx @lofder/dsers-mcp-product login`
|
|
414
|
-
2. 浏览器自动打开 DSers
|
|
415
|
-
3. 你在 DSers
|
|
416
|
-
4.
|
|
494
|
+
2. 浏览器自动打开 DSers OAuth 授权页
|
|
495
|
+
3. 你在 DSers 网站上授权
|
|
496
|
+
4. 工具拿到 OAuth token,加密存到本地
|
|
417
497
|
5. 搞定 — 之后 MCP 直接能用,配置文件里不需要写任何密码
|
|
418
498
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
**登录大约 6 小时有效。** 过期了 AI 助手会提醒你重新跑一下 `login`,10 秒的事。
|
|
499
|
+
**Token 自动续期** — refresh token 会自动保持登录状态,你基本不需要重新登录。
|
|
422
500
|
|
|
423
501
|
**换账号?**
|
|
424
502
|
|
|
@@ -427,7 +505,9 @@ npx @lofder/dsers-mcp-product logout
|
|
|
427
505
|
npx @lofder/dsers-mcp-product login
|
|
428
506
|
```
|
|
429
507
|
|
|
430
|
-
> **开发者注:** headless / CI
|
|
508
|
+
> **开发者注:** headless / CI 环境支持通过 `DSERS_ACCESS_TOKEN` 和 `DSERS_REFRESH_TOKEN` 环境变量传入凭据。
|
|
509
|
+
|
|
510
|
+
> **从源码工作(不是从 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` 存在再跑。
|
|
431
511
|
|
|
432
512
|
### 使用示例
|
|
433
513
|
|
|
@@ -469,7 +549,7 @@ npm run build
|
|
|
469
549
|
npm test
|
|
470
550
|
```
|
|
471
551
|
|
|
472
|
-
###
|
|
552
|
+
### 十三个工具
|
|
473
553
|
|
|
474
554
|
| # | 工具 | 干什么的 |
|
|
475
555
|
|---|------|---------|
|
|
@@ -485,6 +565,7 @@ npm test
|
|
|
485
565
|
| 10 | `dsers_import_list` | 浏览导入待推送列表,含成本价、售价、库存、加价状态 |
|
|
486
566
|
| 11 | `dsers_my_products` | 查看已推到店铺的商品,带供应商链接方便重新导入 |
|
|
487
567
|
| 12 | `dsers_find_product` | 在 DSers 商品池搜索,支持关键词和以图搜图,结果可直接导入 |
|
|
568
|
+
| 13 | `dsers_sku_remap` | SKU 级别替换已上架商品的供应商。两种模式:传 `new_supplier_url` 走精确替换,不传则反向图搜 DSers 池 + 多因子打分自动挑最佳替代。务必先用 `mode='preview'`(只读)看 `diffs` 和 `pool_additions`,确认无误后再 `mode='apply'`。需要 `product:mapping` OAuth scope。 |
|
|
488
569
|
|
|
489
570
|
报错时会返回清晰的消息,AI 助手能看懂出了什么问题、该怎么办。
|
|
490
571
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export declare function
|
|
1
|
+
/**
|
|
2
|
+
* Open a URL in the system's default browser.
|
|
3
|
+
* Uses spawn() with explicit arguments to avoid command injection.
|
|
4
|
+
*/
|
|
5
|
+
export declare function openBrowser(url: string): void;
|
|
6
6
|
//# sourceMappingURL=browser-finder.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser-finder.d.ts","sourceRoot":"","sources":["../../src/auth/browser-finder.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"browser-finder.d.ts","sourceRoot":"","sources":["../../src/auth/browser-finder.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAW7C"}
|
|
@@ -1,104 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
];
|
|
13
|
-
const LINUX_COMMANDS = [
|
|
14
|
-
["Google Chrome", "google-chrome-stable"],
|
|
15
|
-
["Google Chrome", "google-chrome"],
|
|
16
|
-
["Chromium", "chromium-browser"],
|
|
17
|
-
["Chromium", "chromium"],
|
|
18
|
-
["Microsoft Edge", "microsoft-edge-stable"],
|
|
19
|
-
["Microsoft Edge", "microsoft-edge"],
|
|
20
|
-
["Brave Browser", "brave-browser"],
|
|
21
|
-
["Brave Browser", "brave-browser-stable"],
|
|
22
|
-
["Opera", "opera"],
|
|
23
|
-
["Vivaldi", "vivaldi-stable"],
|
|
24
|
-
["Vivaldi", "vivaldi"],
|
|
25
|
-
];
|
|
26
|
-
const WIN_PATHS = [
|
|
27
|
-
["Google Chrome", "Google\\Chrome\\Application\\chrome.exe"],
|
|
28
|
-
["Microsoft Edge", "Microsoft\\Edge\\Application\\msedge.exe"],
|
|
29
|
-
["Brave Browser", "BraveSoftware\\Brave-Browser\\Application\\brave.exe"],
|
|
30
|
-
["Opera", "Opera\\opera.exe"],
|
|
31
|
-
["Vivaldi", "Vivaldi\\Application\\vivaldi.exe"],
|
|
32
|
-
["Chromium", "Chromium\\Application\\chrome.exe"],
|
|
33
|
-
];
|
|
34
|
-
function whichSync(cmd) {
|
|
35
|
-
if (!/^[a-z0-9._-]+$/i.test(cmd))
|
|
36
|
-
return null;
|
|
37
|
-
try {
|
|
38
|
-
return execSync(`which ${cmd} 2>/dev/null`, { encoding: "utf-8", timeout: 3000 }).trim() || null;
|
|
1
|
+
/**
|
|
2
|
+
* Open a URL in the system's default browser.
|
|
3
|
+
* Uses spawn() with explicit arguments to avoid command injection.
|
|
4
|
+
*/
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
export function openBrowser(url) {
|
|
7
|
+
const cmd = process.platform === "darwin" ? "open"
|
|
8
|
+
: process.platform === "win32" ? "cmd"
|
|
9
|
+
: "xdg-open";
|
|
10
|
+
if (process.platform === "win32") {
|
|
11
|
+
spawnSync(cmd, ["/c", "start", "", url], { stdio: "ignore" });
|
|
39
12
|
}
|
|
40
|
-
|
|
41
|
-
|
|
13
|
+
else {
|
|
14
|
+
spawnSync(cmd, [url], { stdio: "ignore" });
|
|
42
15
|
}
|
|
43
16
|
}
|
|
44
|
-
function findOnMacOS() {
|
|
45
|
-
for (const [name, p] of MACOS_BROWSERS) {
|
|
46
|
-
if (existsSync(p))
|
|
47
|
-
return { name, path: p };
|
|
48
|
-
}
|
|
49
|
-
// mdfind fallback for non-standard install locations
|
|
50
|
-
try {
|
|
51
|
-
const result = execSync('mdfind "kMDItemCFBundleIdentifier == com.google.Chrome" 2>/dev/null', {
|
|
52
|
-
encoding: "utf-8",
|
|
53
|
-
timeout: 3000,
|
|
54
|
-
}).trim();
|
|
55
|
-
if (result) {
|
|
56
|
-
const appPath = result.split("\n")[0];
|
|
57
|
-
const execPath = join(appPath, "Contents/MacOS/Google Chrome");
|
|
58
|
-
if (existsSync(execPath))
|
|
59
|
-
return { name: "Google Chrome", path: execPath };
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
catch {
|
|
63
|
-
/* mdfind unavailable or timed out */
|
|
64
|
-
}
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
function findOnLinux() {
|
|
68
|
-
for (const [name, cmd] of LINUX_COMMANDS) {
|
|
69
|
-
const p = whichSync(cmd);
|
|
70
|
-
if (p)
|
|
71
|
-
return { name, path: p };
|
|
72
|
-
}
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
function findOnWindows() {
|
|
76
|
-
const roots = [
|
|
77
|
-
process.env.PROGRAMFILES ?? "C:\\Program Files",
|
|
78
|
-
process.env["PROGRAMFILES(X86)"] ?? "C:\\Program Files (x86)",
|
|
79
|
-
process.env.LOCALAPPDATA ?? "",
|
|
80
|
-
].filter(Boolean);
|
|
81
|
-
for (const [name, rel] of WIN_PATHS) {
|
|
82
|
-
for (const root of roots) {
|
|
83
|
-
const full = join(root, rel);
|
|
84
|
-
if (existsSync(full))
|
|
85
|
-
return { name, path: full };
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
// Edge is pre-installed on Windows 10+ in a special location
|
|
89
|
-
const edgeSys = "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe";
|
|
90
|
-
if (existsSync(edgeSys))
|
|
91
|
-
return { name: "Microsoft Edge", path: edgeSys };
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
94
|
-
export function findChromiumBrowser() {
|
|
95
|
-
const platform = process.platform;
|
|
96
|
-
if (platform === "darwin")
|
|
97
|
-
return findOnMacOS();
|
|
98
|
-
if (platform === "linux")
|
|
99
|
-
return findOnLinux();
|
|
100
|
-
if (platform === "win32")
|
|
101
|
-
return findOnWindows();
|
|
102
|
-
return null;
|
|
103
|
-
}
|
|
104
17
|
//# sourceMappingURL=browser-finder.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser-finder.js","sourceRoot":"","sources":["../../src/auth/browser-finder.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"browser-finder.js","sourceRoot":"","sources":["../../src/auth/browser-finder.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAE/C,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,MAAM,GAAG,GACP,OAAO,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM;QACtC,CAAC,CAAC,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK;YACtC,CAAC,CAAC,UAAU,CAAC;IAEf,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QACjC,SAAS,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;IAChE,CAAC;SAAM,CAAC;QACN,SAAS,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC7C,CAAC;AACH,CAAC"}
|
package/dist/auth/index.d.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
export { loginViaTerminal, type TerminalLoginResult } from "./terminal-prompt.js";
|
|
5
|
-
export { saveToken, loadToken, clearToken, tokenFilePath, type StoredSession } from "./token-store.js";
|
|
1
|
+
export { openBrowser } from "./browser-finder.js";
|
|
2
|
+
export { authorizeWithPKCE, refreshAccessToken } from "./oauth.js";
|
|
3
|
+
export { saveToken, loadToken, clearToken, tokenFilePath, hasLegacyCredentials, isLegacySession, type StoredSession, } from "./token-store.js";
|
|
6
4
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/auth/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AACnE,OAAO,EACL,SAAS,EACT,SAAS,EACT,UAAU,EACV,aAAa,EACb,oBAAoB,EACpB,eAAe,EACf,KAAK,aAAa,GACnB,MAAM,kBAAkB,CAAC"}
|
package/dist/auth/index.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
export { loginViaTerminal } from "./terminal-prompt.js";
|
|
5
|
-
export { saveToken, loadToken, clearToken, tokenFilePath } from "./token-store.js";
|
|
1
|
+
export { openBrowser } from "./browser-finder.js";
|
|
2
|
+
export { authorizeWithPKCE, refreshAccessToken } from "./oauth.js";
|
|
3
|
+
export { saveToken, loadToken, clearToken, tokenFilePath, hasLegacyCredentials, isLegacySession, } from "./token-store.js";
|
|
6
4
|
//# sourceMappingURL=index.js.map
|
package/dist/auth/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AACnE,OAAO,EACL,SAAS,EACT,SAAS,EACT,UAAU,EACV,aAAa,EACb,oBAAoB,EACpB,eAAe,GAEhB,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth 2.1 PKCE authorization flow for DSers MCP.
|
|
3
|
+
*
|
|
4
|
+
* Flow:
|
|
5
|
+
* 1. Fetch AS metadata from /.well-known/oauth-authorization-server
|
|
6
|
+
* 2. Register client dynamically (or reuse saved client_id)
|
|
7
|
+
* 3. Open browser to authorization_endpoint with PKCE challenge
|
|
8
|
+
* 4. Listen on localhost for callback with authorization code
|
|
9
|
+
* 5. Exchange code for access_token + refresh_token
|
|
10
|
+
*
|
|
11
|
+
* Reference: client-simple/src/index.ts (verified working with DSers OAuth AS)
|
|
12
|
+
*/
|
|
13
|
+
export interface OAuthTokens {
|
|
14
|
+
access_token: string;
|
|
15
|
+
refresh_token: string;
|
|
16
|
+
expires_at: number;
|
|
17
|
+
client_id: string;
|
|
18
|
+
oauth_base: string;
|
|
19
|
+
base_url: string;
|
|
20
|
+
ts: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Full OAuth 2.1 PKCE authorization flow.
|
|
24
|
+
* Opens browser, waits for callback, exchanges code for tokens.
|
|
25
|
+
*/
|
|
26
|
+
export declare function authorizeWithPKCE(log: (msg: string) => void, savedClientId?: string, oauthBase?: string): Promise<OAuthTokens>;
|
|
27
|
+
/**
|
|
28
|
+
* Refresh an expired access_token using the refresh_token.
|
|
29
|
+
* Returns updated tokens or throws on failure.
|
|
30
|
+
*/
|
|
31
|
+
export declare function refreshAccessToken(refreshToken: string, clientId: string, oauthBase?: string): Promise<{
|
|
32
|
+
access_token: string;
|
|
33
|
+
refresh_token: string;
|
|
34
|
+
expires_at: number;
|
|
35
|
+
}>;
|
|
36
|
+
//# sourceMappingURL=oauth.d.ts.map
|
|
@@ -0,0 +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"}
|