@mpelka/aliorders 1.0.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/LICENSE +21 -0
- package/README.md +83 -0
- package/package.json +36 -0
- package/src/AliExpressClient.ts +216 -0
- package/src/cli.ts +327 -0
- package/src/types.ts +127 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 mpelka
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# @mpelka/aliorders
|
|
2
|
+
|
|
3
|
+
CLI tool to fetch and display your AliExpress order history, including tracking info, by using the internal web API.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- **macOS** (cookie extraction uses Chrome's local database)
|
|
8
|
+
- **Google Chrome** with an active AliExpress login
|
|
9
|
+
- **[Bun](https://bun.sh/)** runtime
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# From npm
|
|
15
|
+
npm install -g @mpelka/aliorders
|
|
16
|
+
|
|
17
|
+
# Or clone and run directly
|
|
18
|
+
git clone https://github.com/mpelka/aliorders.git
|
|
19
|
+
cd aliorders
|
|
20
|
+
bun install
|
|
21
|
+
bun link
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
aliorders # Show active orders (default)
|
|
28
|
+
aliorders -s all # Show all orders including completed
|
|
29
|
+
aliorders -s shipped # Filter by status
|
|
30
|
+
aliorders -s completed # Show completed orders
|
|
31
|
+
aliorders -n 20 # Fetch up to 20 orders
|
|
32
|
+
aliorders -p 2 # Show page 2
|
|
33
|
+
aliorders --json # Output as JSON for scripting/agents
|
|
34
|
+
aliorders --no-details # Skip tracking lookups (faster)
|
|
35
|
+
aliorders statuses # List all status filters
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
By default, only active orders are shown (awaiting payment, awaiting shipment, in transit). Use `-s all` to include completed and cancelled orders.
|
|
39
|
+
|
|
40
|
+
### Status filters
|
|
41
|
+
|
|
42
|
+
| Filter | Aliases | Description |
|
|
43
|
+
|-------------|----------------------|------------------------|
|
|
44
|
+
| `all` | | All orders (no filter) |
|
|
45
|
+
| `waitpay` | `paying` | Awaiting payment |
|
|
46
|
+
| `waitsend` | `shipping` | Awaiting shipment |
|
|
47
|
+
| `waitaccept`| `shipped`, `transit` | Shipped / In transit |
|
|
48
|
+
| `finish` | `completed`, `done` | Completed |
|
|
49
|
+
| `close` | `cancelled` | Cancelled / Closed |
|
|
50
|
+
|
|
51
|
+
### JSON output
|
|
52
|
+
|
|
53
|
+
Use `--json` to get structured output, useful for piping to `jq` or feeding to LLM agents:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
aliorders --json | jq '.[].status'
|
|
57
|
+
aliorders --json --no-details # Faster, skips tracking API calls
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Exit codes
|
|
61
|
+
|
|
62
|
+
| Code | Meaning |
|
|
63
|
+
|------|-----------------|
|
|
64
|
+
| `0` | Success |
|
|
65
|
+
| `1` | Error |
|
|
66
|
+
| `2` | No orders found |
|
|
67
|
+
|
|
68
|
+
## How it works
|
|
69
|
+
|
|
70
|
+
Every run extracts fresh authentication cookies directly from Chrome's local database for `aliexpress.com`. These cookies are used to sign requests to AliExpress's internal API endpoints. Nothing is persisted to disk — cookies are always read fresh from Chrome.
|
|
71
|
+
|
|
72
|
+
## Troubleshooting
|
|
73
|
+
|
|
74
|
+
**Token expired / empty response:**
|
|
75
|
+
Visit aliexpress.com in Chrome and browse around (this refreshes the auth token), then run again.
|
|
76
|
+
|
|
77
|
+
## Security
|
|
78
|
+
|
|
79
|
+
Cookies are read directly from Chrome's database and held in memory only for the duration of the command. Nothing is written to disk.
|
|
80
|
+
|
|
81
|
+
## License
|
|
82
|
+
|
|
83
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mpelka/aliorders",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI tool to fetch and display your AliExpress order history using browser cookies",
|
|
5
|
+
"main": "src/cli.ts",
|
|
6
|
+
"bin": {
|
|
7
|
+
"aliorders": "src/cli.ts"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src/**/*.ts",
|
|
11
|
+
"!src/**/*.test.ts",
|
|
12
|
+
"LICENSE",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"start": "bun src/cli.ts",
|
|
17
|
+
"test": "bun test",
|
|
18
|
+
"lint": "biome check src/",
|
|
19
|
+
"typecheck": "tsc --noEmit"
|
|
20
|
+
},
|
|
21
|
+
"keywords": ["aliexpress", "orders", "cli", "tracking"],
|
|
22
|
+
"author": "mpelka",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"type": "module",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@mpelka/get-cookies": "^1.0.2",
|
|
27
|
+
"commander": "^14.0.3",
|
|
28
|
+
"ky": "^1.14.3"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@biomejs/biome": "^2.4.6",
|
|
32
|
+
"@types/bun": "^1.3.10",
|
|
33
|
+
"@types/node": "^25.4.0",
|
|
34
|
+
"typescript": "^5.9.3"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import * as crypto from "node:crypto";
|
|
2
|
+
import type { Cookie } from "@mpelka/get-cookies";
|
|
3
|
+
import ky, { type AfterResponseHook, type BeforeRequestHook } from "ky";
|
|
4
|
+
import type {
|
|
5
|
+
AliExpressApiResponse,
|
|
6
|
+
OrderDetailApiResponse,
|
|
7
|
+
OrderDetailRequestData,
|
|
8
|
+
OrderListOptions,
|
|
9
|
+
RequestConfig,
|
|
10
|
+
} from "./types.ts";
|
|
11
|
+
|
|
12
|
+
export class AliExpressClient {
|
|
13
|
+
private kyInstance: typeof ky;
|
|
14
|
+
private cookies: Map<string, string>;
|
|
15
|
+
|
|
16
|
+
private readonly config = {
|
|
17
|
+
tokenCookieName: "_m_h5_tk",
|
|
18
|
+
appKey: "12574478",
|
|
19
|
+
baseUrl: "https://acs.aliexpress.com/h5/",
|
|
20
|
+
endpoints: {
|
|
21
|
+
orderList: "mtop.aliexpress.trade.buyer.order.list/1.0/",
|
|
22
|
+
orderDetail: "mtop.aliexpress.trade.buyer.order.detail/1.0/",
|
|
23
|
+
},
|
|
24
|
+
defaultParams: {
|
|
25
|
+
jsv: "2.5.1",
|
|
26
|
+
v: "1.0",
|
|
27
|
+
post: "1",
|
|
28
|
+
timeout: 15000,
|
|
29
|
+
dataType: "originaljsonp",
|
|
30
|
+
type: "originaljson",
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
constructor(browserCookies: Cookie[]) {
|
|
35
|
+
this.cookies = new Map();
|
|
36
|
+
for (const cookie of browserCookies) {
|
|
37
|
+
this.cookies.set(cookie.name, cookie.value);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const beforeRequestHook: BeforeRequestHook = async (request) => {
|
|
41
|
+
const cookieHeader = [...this.cookies.entries()].map(([k, v]) => `${k}=${v}`).join("; ");
|
|
42
|
+
if (cookieHeader) {
|
|
43
|
+
request.headers.set("Cookie", cookieHeader);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const afterResponseHook: AfterResponseHook = async (_request, _options, response) => {
|
|
48
|
+
const setCookieHeader = response.headers.get("set-cookie");
|
|
49
|
+
if (setCookieHeader) {
|
|
50
|
+
for (const part of setCookieHeader.split(",")) {
|
|
51
|
+
const match = part.trim().match(/^([^=]+)=([^;]*)/);
|
|
52
|
+
if (match) {
|
|
53
|
+
this.cookies.set(match[1].trim(), match[2].trim());
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
this.kyInstance = ky.create({
|
|
60
|
+
hooks: {
|
|
61
|
+
beforeRequest: [beforeRequestHook],
|
|
62
|
+
afterResponse: [afterResponseHook],
|
|
63
|
+
},
|
|
64
|
+
headers: {
|
|
65
|
+
accept: "application/json, text/plain, */*",
|
|
66
|
+
"accept-language": "en,pl;q=0.9",
|
|
67
|
+
referer: "https://www.aliexpress.com/",
|
|
68
|
+
"sec-ch-ua": '"Not)A;Brand";v="8", "Chromium";v="138", "Google Chrome";v="138"',
|
|
69
|
+
"sec-ch-ua-mobile": "?0",
|
|
70
|
+
"sec-ch-ua-platform": '"macOS"',
|
|
71
|
+
"sec-fetch-dest": "empty",
|
|
72
|
+
"sec-fetch-mode": "cors",
|
|
73
|
+
"sec-fetch-site": "same-site",
|
|
74
|
+
"user-agent":
|
|
75
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
|
|
76
|
+
},
|
|
77
|
+
timeout: 15000,
|
|
78
|
+
retry: 0,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private parseJsonpResponse(responseText: string, callbackName: string) {
|
|
83
|
+
const trimmed = responseText.trim();
|
|
84
|
+
|
|
85
|
+
if (trimmed.startsWith(`${callbackName}(`) && trimmed.endsWith(")")) {
|
|
86
|
+
const jsonString = trimmed.slice(callbackName.length + 1, -1);
|
|
87
|
+
return JSON.parse(jsonString);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return JSON.parse(responseText);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private generateSignature(token: string, timestamp: number, appKey: string, dataString: string): string {
|
|
94
|
+
const signString = `${token}&${timestamp}&${appKey}&${dataString}`;
|
|
95
|
+
return crypto.createHash("md5").update(signString).digest("hex");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
public getTokenInfo(): { expired: boolean; ageHours: number } {
|
|
99
|
+
const tokenValue = this.cookies.get(this.config.tokenCookieName);
|
|
100
|
+
if (!tokenValue) return { expired: true, ageHours: Infinity };
|
|
101
|
+
|
|
102
|
+
const parts = tokenValue.split("_");
|
|
103
|
+
if (parts.length < 2) return { expired: true, ageHours: Infinity };
|
|
104
|
+
|
|
105
|
+
const tokenTimestamp = parseInt(parts[1], 10);
|
|
106
|
+
if (Number.isNaN(tokenTimestamp)) return { expired: true, ageHours: Infinity };
|
|
107
|
+
|
|
108
|
+
const ageHours = (Date.now() - tokenTimestamp) / (1000 * 60 * 60);
|
|
109
|
+
return { expired: ageHours > 24, ageHours };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private getToken(): string | null {
|
|
113
|
+
const tokenValue = this.cookies.get(this.config.tokenCookieName);
|
|
114
|
+
if (!tokenValue) return null;
|
|
115
|
+
return tokenValue.split("_")[0];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async getOrders({
|
|
119
|
+
pageIndex = 1,
|
|
120
|
+
pageSize = 10,
|
|
121
|
+
statusTab = null,
|
|
122
|
+
country = "PL",
|
|
123
|
+
language = "pl_PL",
|
|
124
|
+
}: OrderListOptions): Promise<AliExpressApiResponse | null> {
|
|
125
|
+
const dataObject = {
|
|
126
|
+
params: {
|
|
127
|
+
data: {
|
|
128
|
+
pc_om_list_body_109702: { fields: { pageIndex, pageSize } },
|
|
129
|
+
pc_om_list_header_action_110846: {
|
|
130
|
+
fields: {
|
|
131
|
+
statusTab: statusTab ?? "all",
|
|
132
|
+
timeOption: "all",
|
|
133
|
+
searchOption: "order",
|
|
134
|
+
searchInput: "",
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
shipToCountry: country,
|
|
140
|
+
_lang: language,
|
|
141
|
+
};
|
|
142
|
+
return this.request({
|
|
143
|
+
endpoint: this.config.endpoints.orderList,
|
|
144
|
+
api: "mtop.aliexpress.trade.buyer.order.list",
|
|
145
|
+
data: dataObject,
|
|
146
|
+
method: "POST",
|
|
147
|
+
callbackName: "mtopjsonp1",
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async getOrderDetails(
|
|
152
|
+
orderId: string,
|
|
153
|
+
{ country = "PL", language = "pl_PL" }: OrderListOptions,
|
|
154
|
+
): Promise<OrderDetailApiResponse | null> {
|
|
155
|
+
const dataObject: OrderDetailRequestData = {
|
|
156
|
+
tradeOrderId: orderId,
|
|
157
|
+
timeZone: "GMT+0200",
|
|
158
|
+
clientPlatform: "pc",
|
|
159
|
+
channel: "tracking",
|
|
160
|
+
shipToCountry: country,
|
|
161
|
+
_lang: language,
|
|
162
|
+
};
|
|
163
|
+
return this.request({
|
|
164
|
+
endpoint: this.config.endpoints.orderDetail,
|
|
165
|
+
api: "mtop.aliexpress.trade.buyer.order.detail",
|
|
166
|
+
data: dataObject,
|
|
167
|
+
callbackName: "mtopjsonp3",
|
|
168
|
+
needLogin: true,
|
|
169
|
+
method: "GET",
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private async request(config: RequestConfig) {
|
|
174
|
+
const token = this.getToken();
|
|
175
|
+
if (!token) throw new Error("No authentication token found. Are you logged in to AliExpress in Chrome?");
|
|
176
|
+
|
|
177
|
+
const timestamp = Date.now();
|
|
178
|
+
const dataString = JSON.stringify(config.data);
|
|
179
|
+
const signature = this.generateSignature(token, timestamp, this.config.appKey, dataString);
|
|
180
|
+
|
|
181
|
+
const method = config.method || "GET";
|
|
182
|
+
const urlParams = new URLSearchParams({
|
|
183
|
+
jsv: this.config.defaultParams.jsv,
|
|
184
|
+
appKey: this.config.appKey,
|
|
185
|
+
t: timestamp.toString(),
|
|
186
|
+
sign: signature,
|
|
187
|
+
api: config.api,
|
|
188
|
+
v: this.config.defaultParams.v,
|
|
189
|
+
type: this.config.defaultParams.type,
|
|
190
|
+
dataType: this.config.defaultParams.dataType,
|
|
191
|
+
timeout: this.config.defaultParams.timeout.toString(),
|
|
192
|
+
method: method,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
if (method === "POST") {
|
|
196
|
+
urlParams.set("post", "1");
|
|
197
|
+
urlParams.set("Content-Type", "application/x-www-form-urlencoded");
|
|
198
|
+
}
|
|
199
|
+
if (method === "GET") {
|
|
200
|
+
urlParams.set("callback", config.callbackName);
|
|
201
|
+
urlParams.set("data", dataString);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const url = `${this.config.baseUrl}${config.endpoint}?${urlParams.toString()}`;
|
|
205
|
+
|
|
206
|
+
const response =
|
|
207
|
+
method === "POST"
|
|
208
|
+
? await this.kyInstance.post(url, { body: new URLSearchParams({ data: dataString }) })
|
|
209
|
+
: await this.kyInstance.get(url);
|
|
210
|
+
|
|
211
|
+
const responseText = await response.text();
|
|
212
|
+
if (responseText.startsWith("{")) return JSON.parse(responseText);
|
|
213
|
+
|
|
214
|
+
return this.parseJsonpResponse(responseText, config.callbackName);
|
|
215
|
+
}
|
|
216
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import type { Cookie } from "@mpelka/get-cookies";
|
|
4
|
+
import { getChromiumCookiesMacOS } from "@mpelka/get-cookies";
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import { AliExpressClient } from "./AliExpressClient.ts";
|
|
7
|
+
import type {
|
|
8
|
+
LogisticPackageBlock,
|
|
9
|
+
OrderComponentData,
|
|
10
|
+
OrderDetailApiResponse,
|
|
11
|
+
OrderStatusBlock,
|
|
12
|
+
TrackingInfo,
|
|
13
|
+
} from "./types.ts";
|
|
14
|
+
|
|
15
|
+
const STATUS_ALIASES: Record<string, string> = {
|
|
16
|
+
paying: "waitpay",
|
|
17
|
+
shipping: "waitsend",
|
|
18
|
+
shipped: "waitaccept",
|
|
19
|
+
transit: "waitaccept",
|
|
20
|
+
completed: "finish",
|
|
21
|
+
done: "finish",
|
|
22
|
+
cancelled: "close",
|
|
23
|
+
canceled: "close",
|
|
24
|
+
all: "all",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const STATUS_NAMES: Record<string, string> = {
|
|
28
|
+
all: "All orders (no filter)",
|
|
29
|
+
waitpay: "Awaiting payment",
|
|
30
|
+
waitsend: "Awaiting shipment",
|
|
31
|
+
waitaccept: "Shipped / In transit",
|
|
32
|
+
finish: "Completed",
|
|
33
|
+
close: "Cancelled / Closed",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function resolveStatus(input: string): string | null {
|
|
37
|
+
if (input in STATUS_NAMES) return input;
|
|
38
|
+
if (input in STATUS_ALIASES) return STATUS_ALIASES[input];
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function truncate(str: string, max: number): string {
|
|
43
|
+
return str.length > max ? `${str.slice(0, max - 1)}…` : str;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isActiveOrder(order: OrderComponentData): boolean {
|
|
47
|
+
const status = order.fields.statusText?.toLowerCase() ?? "";
|
|
48
|
+
const completedKeywords = ["completed", "finished", "closed", "cancelled", "canceled", "ukończony", "zamknięty"];
|
|
49
|
+
return !completedKeywords.some((kw) => status.includes(kw));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface OrderOutput {
|
|
53
|
+
orderId: string;
|
|
54
|
+
date: string;
|
|
55
|
+
store: string;
|
|
56
|
+
total: string;
|
|
57
|
+
currency: string;
|
|
58
|
+
status: string;
|
|
59
|
+
tracking?: {
|
|
60
|
+
number: string;
|
|
61
|
+
estimatedDelivery?: string;
|
|
62
|
+
history?: { date: string; action: string; address?: string }[];
|
|
63
|
+
};
|
|
64
|
+
items: { title: string; price: string; quantity: number }[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function extractOrderData(order: OrderComponentData, orderDetails: OrderDetailApiResponse | null): OrderOutput {
|
|
68
|
+
const f = order.fields;
|
|
69
|
+
|
|
70
|
+
let status = f.statusText;
|
|
71
|
+
let trackingNumber: string | null = null;
|
|
72
|
+
let estimatedDelivery: string | null = null;
|
|
73
|
+
let trackingHistory: TrackingInfo[] | null = null;
|
|
74
|
+
|
|
75
|
+
if (orderDetails?.data?.data) {
|
|
76
|
+
const statusBlock = Object.values(orderDetails.data.data).find(
|
|
77
|
+
(block) => block.tag === "detail_order_status_block",
|
|
78
|
+
) as OrderStatusBlock | undefined;
|
|
79
|
+
if (statusBlock?.fields?.title) {
|
|
80
|
+
status = statusBlock.fields.title;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const logisticBlock = Object.values(orderDetails.data.data).find(
|
|
84
|
+
(block) => block.tag === "detail_logistic_package_block",
|
|
85
|
+
) as LogisticPackageBlock | undefined;
|
|
86
|
+
const packages = logisticBlock?.fields?.packageInfoList;
|
|
87
|
+
if (packages && packages.length > 0) {
|
|
88
|
+
trackingNumber = packages[0].trackingNumber;
|
|
89
|
+
estimatedDelivery = packages[0].expectDeliveryDate;
|
|
90
|
+
trackingHistory = packages[0].trackInfoList;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const output: OrderOutput = {
|
|
95
|
+
orderId: f.orderId,
|
|
96
|
+
date: f.orderDateText,
|
|
97
|
+
store: f.storeName,
|
|
98
|
+
total: f.totalPriceText,
|
|
99
|
+
currency: f.currencyCode,
|
|
100
|
+
status,
|
|
101
|
+
items: (f.orderLines ?? []).map((item) => ({
|
|
102
|
+
title: item.itemTitle,
|
|
103
|
+
price: item.itemPriceText,
|
|
104
|
+
quantity: item.quantity,
|
|
105
|
+
})),
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
if (trackingNumber) {
|
|
109
|
+
output.tracking = {
|
|
110
|
+
number: trackingNumber,
|
|
111
|
+
estimatedDelivery: estimatedDelivery ?? undefined,
|
|
112
|
+
history: trackingHistory?.slice(0, 5).map((e) => ({
|
|
113
|
+
date: e.date,
|
|
114
|
+
action: e.action.trim(),
|
|
115
|
+
address: e.address || undefined,
|
|
116
|
+
})),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return output;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function printOrder(data: OrderOutput, index: number, total: number) {
|
|
124
|
+
const header = `[${index + 1}/${total}] ${data.orderId}`;
|
|
125
|
+
console.log(`\n${header}`);
|
|
126
|
+
console.log("-".repeat(header.length));
|
|
127
|
+
console.log(` ${data.date} | ${data.store} | ${data.total} ${data.currency} | ${data.status}`);
|
|
128
|
+
|
|
129
|
+
if (data.tracking) {
|
|
130
|
+
const parts = [` Tracking: ${data.tracking.number}`];
|
|
131
|
+
if (data.tracking.estimatedDelivery) parts.push(`ETA: ${data.tracking.estimatedDelivery}`);
|
|
132
|
+
console.log(parts.join(" | "));
|
|
133
|
+
|
|
134
|
+
if (data.tracking.history) {
|
|
135
|
+
for (const event of data.tracking.history.slice(0, 3)) {
|
|
136
|
+
const location = event.address ? ` (${event.address})` : "";
|
|
137
|
+
console.log(` ${event.date} - ${event.action}${location}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
for (const item of data.items) {
|
|
143
|
+
console.log(` - ${truncate(item.title, 80)} ${item.price} x${item.quantity}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function createClient(): Promise<AliExpressClient | null> {
|
|
148
|
+
if (os.platform() !== "darwin") {
|
|
149
|
+
console.error("Cookie extraction is currently macOS-only.");
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const cookies: Cookie[] = await getChromiumCookiesMacOS("chrome", "Default", "aliexpress.com");
|
|
155
|
+
if (cookies.length === 0) {
|
|
156
|
+
console.error("No AliExpress cookies found in Chrome. Log in to aliexpress.com first.");
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const tokenCookie = cookies.find((c) => c.name === "_m_h5_tk");
|
|
161
|
+
if (!tokenCookie) {
|
|
162
|
+
console.error("Auth cookie '_m_h5_tk' not found. Visit aliexpress.com in Chrome and browse around.");
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return new AliExpressClient(cookies);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.error("Failed to get cookies from Chrome:", error);
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
interface RunOptions {
|
|
174
|
+
status?: string;
|
|
175
|
+
limit: number;
|
|
176
|
+
page: number;
|
|
177
|
+
details: boolean;
|
|
178
|
+
json: boolean;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function run(options: RunOptions): Promise<number> {
|
|
182
|
+
const client = await createClient();
|
|
183
|
+
if (!client) return 1;
|
|
184
|
+
|
|
185
|
+
const tokenInfo = client.getTokenInfo();
|
|
186
|
+
if (tokenInfo.expired) {
|
|
187
|
+
console.warn(`Warning: Token is ${tokenInfo.ageHours.toFixed(1)}h old and likely expired.`);
|
|
188
|
+
console.warn(" Visit aliexpress.com in Chrome to refresh your session, then retry.");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const showAll = options.status === "all";
|
|
192
|
+
const serverFilter = options.status && !showAll ? options.status : null;
|
|
193
|
+
|
|
194
|
+
const ordersRes = await client.getOrders({
|
|
195
|
+
pageIndex: options.page,
|
|
196
|
+
pageSize: options.limit,
|
|
197
|
+
statusTab: serverFilter,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
if (ordersRes?.ret?.some((r) => r.startsWith("FAIL_") || r.includes("ERROR") || r.includes("SESSION_EXPIRED"))) {
|
|
201
|
+
console.error(`API error: ${ordersRes.ret.join(", ")}`);
|
|
202
|
+
console.error(" Visit aliexpress.com in Chrome to refresh your session, then retry.");
|
|
203
|
+
return 1;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const ordersData = ordersRes?.data?.data;
|
|
207
|
+
if (!ordersData) {
|
|
208
|
+
console.error("Failed to retrieve orders. Response was empty or malformed.");
|
|
209
|
+
if (ordersRes) {
|
|
210
|
+
console.error("Response:", JSON.stringify(ordersRes, null, 2));
|
|
211
|
+
}
|
|
212
|
+
return 1;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let orders = Object.keys(ordersData)
|
|
216
|
+
.filter((key) => key.startsWith("pc_om_list_order_"))
|
|
217
|
+
.map((key) => ordersData[key])
|
|
218
|
+
.filter((order) => order?.fields) as OrderComponentData[];
|
|
219
|
+
|
|
220
|
+
// Default: show only active orders. -s all or -s <specific> skips this filter.
|
|
221
|
+
if (!showAll && !options.status) {
|
|
222
|
+
orders = orders.filter(isActiveOrder);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (orders.length === 0) {
|
|
226
|
+
if (options.json) {
|
|
227
|
+
console.log(JSON.stringify([]));
|
|
228
|
+
} else {
|
|
229
|
+
const context = options.status
|
|
230
|
+
? ` with status "${STATUS_NAMES[options.status] ?? options.status}"`
|
|
231
|
+
: " (active only -- use -s all to include completed)";
|
|
232
|
+
console.log(`No orders found${context}.`);
|
|
233
|
+
}
|
|
234
|
+
return 2;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const results: OrderOutput[] = [];
|
|
238
|
+
|
|
239
|
+
for (let i = 0; i < orders.length; i++) {
|
|
240
|
+
const order = orders[i];
|
|
241
|
+
let orderDetails: OrderDetailApiResponse | null = null;
|
|
242
|
+
|
|
243
|
+
if (options.details) {
|
|
244
|
+
orderDetails = await client.getOrderDetails(order.fields.orderId, {});
|
|
245
|
+
if (i < orders.length - 1) {
|
|
246
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const data = extractOrderData(order, orderDetails);
|
|
251
|
+
results.push(data);
|
|
252
|
+
|
|
253
|
+
if (!options.json) {
|
|
254
|
+
printOrder(data, i, orders.length);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (options.json) {
|
|
259
|
+
console.log(JSON.stringify(results, null, 2));
|
|
260
|
+
} else {
|
|
261
|
+
const label = options.status ? ` (${STATUS_NAMES[options.status] ?? options.status})` : " (active)";
|
|
262
|
+
console.log(`\n${results.length} order${results.length === 1 ? "" : "s"}${label}, page ${options.page}`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return 0;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const allStatuses = [...new Set([...Object.keys(STATUS_NAMES), ...Object.keys(STATUS_ALIASES)])].sort();
|
|
269
|
+
|
|
270
|
+
const program = new Command()
|
|
271
|
+
.name("aliorders")
|
|
272
|
+
.description("Fetch and display AliExpress order history from your browser session")
|
|
273
|
+
.version("1.0.0")
|
|
274
|
+
.option("-s, --status <name>", `Filter by status: ${allStatuses.join(", ")}`)
|
|
275
|
+
.option("-n, --limit <number>", "Number of orders to fetch per page", "10")
|
|
276
|
+
.option("-p, --page <number>", "Page number", "1")
|
|
277
|
+
.option("-j, --json", "Output as JSON (useful for scripting and LLM agents)", false)
|
|
278
|
+
.option("--no-details", "Skip fetching detailed tracking info (faster)")
|
|
279
|
+
.addHelpText(
|
|
280
|
+
"after",
|
|
281
|
+
`
|
|
282
|
+
Examples:
|
|
283
|
+
aliorders Show active orders (default)
|
|
284
|
+
aliorders -s all Show all orders including completed
|
|
285
|
+
aliorders -s shipped Show shipped/in-transit orders
|
|
286
|
+
aliorders -s completed Show completed orders
|
|
287
|
+
aliorders -n 20 Fetch up to 20 orders
|
|
288
|
+
aliorders -p 2 Show page 2
|
|
289
|
+
aliorders --json Output as JSON for scripting/agents
|
|
290
|
+
aliorders --no-details Skip tracking lookups (faster)
|
|
291
|
+
aliorders statuses List all status filters`,
|
|
292
|
+
)
|
|
293
|
+
.action(async (opts) => {
|
|
294
|
+
if (opts.status) {
|
|
295
|
+
const resolved = resolveStatus(opts.status);
|
|
296
|
+
if (!resolved) {
|
|
297
|
+
console.error(`Unknown status "${opts.status}". Available: ${allStatuses.join(", ")}`);
|
|
298
|
+
process.exit(1);
|
|
299
|
+
}
|
|
300
|
+
opts.status = resolved;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const exitCode = await run({
|
|
304
|
+
status: opts.status,
|
|
305
|
+
limit: parseInt(opts.limit, 10),
|
|
306
|
+
page: parseInt(opts.page, 10),
|
|
307
|
+
details: opts.details,
|
|
308
|
+
json: opts.json,
|
|
309
|
+
});
|
|
310
|
+
process.exit(exitCode);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
program
|
|
314
|
+
.command("statuses")
|
|
315
|
+
.description("List available order status filters")
|
|
316
|
+
.action(() => {
|
|
317
|
+
console.log("Available status filters:\n");
|
|
318
|
+
for (const [tab, name] of Object.entries(STATUS_NAMES)) {
|
|
319
|
+
const aliases = Object.entries(STATUS_ALIASES)
|
|
320
|
+
.filter(([, v]) => v === tab)
|
|
321
|
+
.map(([k]) => k);
|
|
322
|
+
const aliasStr = aliases.length > 0 ? ` (aliases: ${aliases.join(", ")})` : "";
|
|
323
|
+
console.log(` ${tab.padEnd(14)} ${name}${aliasStr}`);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
program.parse();
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Main API response wrapper
|
|
2
|
+
export interface AliExpressApiResponse {
|
|
3
|
+
api: string;
|
|
4
|
+
data: AliExpressResponseData;
|
|
5
|
+
ret: string[];
|
|
6
|
+
v: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Container for all response data
|
|
10
|
+
export interface AliExpressResponseData {
|
|
11
|
+
data: Record<string, ComponentData>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Generic component data structure
|
|
15
|
+
export interface ComponentData {
|
|
16
|
+
fields: any;
|
|
17
|
+
id: string;
|
|
18
|
+
tag: string;
|
|
19
|
+
type: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Order-specific component data
|
|
23
|
+
export interface OrderComponentData extends ComponentData {
|
|
24
|
+
fields: OrderFields;
|
|
25
|
+
tag: "pc_om_list_order";
|
|
26
|
+
type: "pc_om_list_order";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Order fields containing all order details
|
|
30
|
+
export interface OrderFields {
|
|
31
|
+
buttons: OrderButton[];
|
|
32
|
+
currencyCode: string;
|
|
33
|
+
orderDateText: string;
|
|
34
|
+
orderId: string;
|
|
35
|
+
orderLines: OrderLine[];
|
|
36
|
+
statusText: string;
|
|
37
|
+
storeName: string;
|
|
38
|
+
totalPriceText: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Individual order line (product)
|
|
42
|
+
export interface OrderLine {
|
|
43
|
+
itemPriceText: string;
|
|
44
|
+
itemTitle: string;
|
|
45
|
+
quantity: number;
|
|
46
|
+
skuAttrs?: SkuAttribute[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// SKU attributes for product variants
|
|
50
|
+
export interface SkuAttribute {
|
|
51
|
+
name: string;
|
|
52
|
+
text: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Action buttons available for each order
|
|
56
|
+
export interface OrderButton {
|
|
57
|
+
text: string;
|
|
58
|
+
type: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Detailed tracking information
|
|
62
|
+
export interface TrackingInfo {
|
|
63
|
+
action: string;
|
|
64
|
+
address?: string;
|
|
65
|
+
date: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Package tracking details
|
|
69
|
+
export interface PackageInfo {
|
|
70
|
+
expectDeliveryDate: string;
|
|
71
|
+
trackInfoList: TrackingInfo[];
|
|
72
|
+
trackingNumber: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Logistic package block from order details
|
|
76
|
+
export interface LogisticPackageBlock extends ComponentData {
|
|
77
|
+
fields: {
|
|
78
|
+
packageInfoList: PackageInfo[];
|
|
79
|
+
};
|
|
80
|
+
tag: "detail_logistic_package_block";
|
|
81
|
+
type: "detail_logistic_package_block";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Order status block from order details
|
|
85
|
+
export interface OrderStatusBlock extends ComponentData {
|
|
86
|
+
fields: {
|
|
87
|
+
title: string;
|
|
88
|
+
};
|
|
89
|
+
tag: "detail_order_status_block";
|
|
90
|
+
type: "detail_order_status_block";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Order detail API response
|
|
94
|
+
export interface OrderDetailApiResponse extends AliExpressApiResponse {
|
|
95
|
+
api: "mtop.aliexpress.trade.buyer.order.detail";
|
|
96
|
+
data: {
|
|
97
|
+
data: Record<string, ComponentData>;
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Request data for order details API
|
|
102
|
+
export interface OrderDetailRequestData {
|
|
103
|
+
tradeOrderId: string;
|
|
104
|
+
timeZone: string;
|
|
105
|
+
clientPlatform: "pc";
|
|
106
|
+
channel: "tracking";
|
|
107
|
+
shipToCountry: string;
|
|
108
|
+
_lang: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface RequestConfig {
|
|
112
|
+
endpoint: string;
|
|
113
|
+
api: string;
|
|
114
|
+
data: any;
|
|
115
|
+
callbackName: string;
|
|
116
|
+
needLogin?: boolean;
|
|
117
|
+
method?: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface OrderListOptions {
|
|
121
|
+
statusTab?: string | null;
|
|
122
|
+
country?: string;
|
|
123
|
+
language?: string;
|
|
124
|
+
timezone?: string;
|
|
125
|
+
pageIndex?: number;
|
|
126
|
+
pageSize?: number;
|
|
127
|
+
}
|