@maixio/pstore 0.1.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.
@@ -0,0 +1,610 @@
1
+ # pstore
2
+
3
+ pstore 是一个非官方的 PlayStation Store Web API 客户端封装,提供 CLI 和 TypeScript SDK,用于查询公开可访问的商店游戏信息、搜索结果、分类列表和 PSN 服务状态。
4
+
5
+ > 免责声明: 本项目是个人维护的非官方工具,不隶属于 Sony Interactive Entertainment、PlayStation 或其关联公司,也未获得其认可、赞助或授权。PlayStation、PSN 及相关商标归其各自权利方所有。接口行为可能随 PlayStation Store Web 端变化而变化。
6
+
7
+ Unofficial PlayStation Store CLI and SDK for publicly available storefront metadata. This project is not affiliated with, endorsed by, sponsored by, or authorized by Sony Interactive Entertainment or PlayStation.
8
+
9
+ ## 快速开始
10
+
11
+ ```bash
12
+ # 查询游戏
13
+ pstore lookup 10014149
14
+
15
+ # 搜索
16
+ pstore search diablo
17
+
18
+ # 分类浏览
19
+ pstore category pre-orders
20
+ ```
21
+
22
+ ## 安装
23
+
24
+ ```bash
25
+ # 直接运行
26
+ bunx @maixio/pstore --help
27
+ npx @maixio/pstore --help
28
+
29
+ # 全局安装
30
+ bun add -g @maixio/pstore
31
+ npm i -g @maixio/pstore
32
+ pstore --help
33
+ ```
34
+
35
+ 作为库使用:
36
+
37
+ ```bash
38
+ bun add @maixio/pstore
39
+ ```
40
+
41
+ ```ts
42
+ import { createPstoreClient } from "@maixio/pstore";
43
+
44
+ const pstore = createPstoreClient({ locale: "zh-hans-HK" });
45
+
46
+ const game = await pstore.lookup("10014149", {
47
+ includeProducts: true,
48
+ });
49
+
50
+ const search = await pstore.search({
51
+ term: "elden ring",
52
+ locale: "en-US",
53
+ size: 10,
54
+ });
55
+
56
+ const upcoming = await pstore.browse({
57
+ window: "next-thirty-days",
58
+ sortBy: "conceptReleaseDate",
59
+ order: "asc",
60
+ size: 10,
61
+ });
62
+
63
+ const deals = await pstore.categoryGrid({
64
+ id: "all-deals",
65
+ size: 10,
66
+ });
67
+
68
+ const status = await pstore.status();
69
+ ```
70
+
71
+ ### Public API
72
+
73
+ | API | 说明 |
74
+ |-----|------|
75
+ | `createPstoreClient(options)` | 创建带默认 locale 的 SDK facade。 |
76
+ | `lookupGame(id, options)` | 查询单个 Concept ID 或 Product ID 的详情。 |
77
+ | `searchGames(options)` | 按关键词搜索商店,返回 PSN Search 原始结果中的轻量字段。 |
78
+ | `fetchBrowseGrid(query)` | 按 browse 时间窗、排序和筛选浏览列表。 |
79
+ | `fetchCategoryGridResult(params)` | 按 catalog UUID 或别名浏览分类,并返回分页、排序和 facet 元数据。 |
80
+ | `fetchPsnStatus(options)` | 查询 PlayStation Network 服务状态。 |
81
+ | `discoverCatalogs(options)` | 从当前商店页面实时发现 catalog UUID。 |
82
+ | `validateCatalogs(options)` | 验证内置 catalog UUID 是否仍可访问。 |
83
+ | `clearPstoreCaches()` | 清空进程内 lookup/search/category 缓存。 |
84
+
85
+ ### API Notes
86
+
87
+ - `lookupGame` 会根据 ID 形态自动区分 Concept ID 和 Product ID。
88
+ - `searchGames` 默认不做详情增强,不额外请求 publisher、releaseDate、genres 等详情字段。
89
+ - `fetchCategoryGridResult` 支持直接传 UUID;SDK facade 的 `category` / `categoryGrid` 也支持内置别名。
90
+ - `createPstoreClient({ locale })` 只保存默认 locale;缓存仍是进程级共享缓存。
91
+ - 上游 PSN Web API 没有公开稳定合约,字段和 persisted query hash 可能随商店前端更新而变化。
92
+
93
+ ## 使用
94
+
95
+ ### 命令总览
96
+
97
+ ```bash
98
+ pstore lookup --help
99
+ pstore search --help
100
+ pstore category --help
101
+ pstore browse --help
102
+ pstore catalogs
103
+ pstore status
104
+ pstore config
105
+ pstore init
106
+ pstore clear-cache
107
+ ```
108
+
109
+ ### 输出字段
110
+
111
+ 文本和 JSON 输出使用同一套英文字段选择。`--json` 只改变渲染格式,`--fields` 只裁剪输出字段,`--select` 会同时裁剪输出并尽量按字段选择请求计划。
112
+
113
+ 常用字段:
114
+
115
+ - 详情: `id`, `idType`, `conceptId`, `productId`, `name`, `locale`, `publisher`, `edition`, `releaseDate`, `price`, `rating`, `classification`, `topCategory`, `platforms`, `npTitleId`, `warnings`
116
+ - 完整详情: `productName`, `genres`, `skus`, `descriptions`, `media`, `products`, `contentRating`, `localizedTitles`, `concept`, `sources`, `timing`
117
+ - 搜索/列表默认: `id`, `name`, `type`, `price`, `platforms`, `classification`
118
+ - 可显式选择: `url`
119
+
120
+ `search`、`category`、`browse` 默认不输出 `url`,因为 `id` 已足够给 `lookup` 或后续脚本使用;需要商店链接时显式加 `--fields id,name,url`。
121
+
122
+ `lookup --select` 会根据字段生成更小的请求计划。例如 `--select id,name,price` 不会请求评分、关联版本和 Batarang 页面;`--select publisher`、`--select descriptions`、`--select contentRating` 会启用 Batarang。`search`、`category`、`browse` 的 `--select` 当前等同 `--fields`,只影响输出字段,不改变上游请求。
123
+
124
+ ```bash
125
+ pstore lookup 10014149 --json --fields id,name,price,platforms
126
+ pstore lookup 10014149 --json --select id,name,price
127
+ pstore lookup 10014149 --full --json
128
+ pstore search astro --json --fields id,name,price,url
129
+ pstore browse --window next-thirty-days --json --fields id,name,price,url
130
+ ```
131
+
132
+ ### lookup — 查询游戏详情
133
+
134
+ 查询游戏详情,支持 Concept ID 或 Product ID,也支持逗号分隔的批量查询。
135
+
136
+ 参数:
137
+ - `<ids>`: `Concept ID` 或 `Product ID`,多个值可用逗号分隔
138
+
139
+ 选项:
140
+ - `-l, --locale <locale>`: 默认语言区域,未指定时使用配置文件中的 `locale`
141
+ - `-f, --fields <fields>`: 仅输出指定英文字段,字段之间用逗号分隔
142
+ - `--select <fields>`: 字段驱动请求计划并输出指定字段,例如 `id,name,price`
143
+ - `--full`: 输出完整字段 profile,并启用关联版本、concept、Batarang 详情请求
144
+ - `-p, --products`: 包含关联版本
145
+ - `--with-concept`: Product ID 查询时额外返回所属 concept/default product 信息
146
+ - `-b, --batarang`: 强制爬取 Batarang HTML,以获取描述、发行商等信息
147
+ - `--title-locales <locales>`: 同时返回多区域标题;支持 `cjk-en` 或逗号分隔 locale 列表
148
+ - `-j, --json`: JSON 输出
149
+ - `--verbose`: 显示时间分布
150
+ - `--concurrency <count>`: 批量查询并发数,默认 `3`
151
+
152
+ 示例:
153
+
154
+ ```bash
155
+ # 按 Concept ID 查询
156
+ pstore lookup 10014149
157
+
158
+ # 按 Product ID 查询
159
+ pstore lookup UP1001-PPSA28420_00-NBA2K26000000000
160
+
161
+ # 多个 ID 批量查询
162
+ pstore lookup 10014149,231761
163
+
164
+ # 指定语言
165
+ pstore lookup 10014149 --locale en-HK
166
+
167
+ # JSON 输出
168
+ pstore lookup 10014149 --json
169
+
170
+ # 包含关联版本
171
+ pstore lookup 10014149 --products
172
+
173
+ # 同时返回简中、繁中、日文、英文标题
174
+ pstore lookup 10014149 --title-locales cjk-en --json
175
+
176
+ # 批量查询并控制并发
177
+ pstore lookup 10014149,10005069,10017939 --products --concurrency 2 --json
178
+
179
+ # 只看部分字段
180
+ pstore lookup 10014149 --fields id,name,price,rating
181
+
182
+ # 只请求和输出指定字段
183
+ pstore lookup 10014149 --select id,name,price --json
184
+ ```
185
+
186
+ lookup 会保留 PSN 官方 API 的对象语义:
187
+
188
+ - Product ID 走 `productRetrieve`,顶层 `price` 是该 product/edition 自己的价格。
189
+ - Concept ID 走 `conceptRetrieve`,顶层 `price` 是 concept/default product 的主价格。
190
+ - Product ID 查询会在 `conceptId` 字段暴露所属 concept;需要完整 concept/default product 数据时使用 `--with-concept`。
191
+
192
+ `--products` 会返回同 concept 下的关联版本:
193
+
194
+ - `products[].price` 是每个 edition/product 自己的价格。
195
+
196
+ 例如 `concept/10005069`:
197
+
198
+ ```json
199
+ {
200
+ "price": { "discountedPrice": "HK$568.00" },
201
+ "products": [
202
+ {
203
+ "id": "HP9000-PPSA07634_00-SAROS00000000000",
204
+ "edition": { "name": "普通版" },
205
+ "price": { "discountedPrice": "HK$568.00" }
206
+ },
207
+ {
208
+ "id": "HP9000-PPSA07634_00-SAROSDELUXE00000",
209
+ "edition": { "name": "数字豪华版" },
210
+ "price": { "discountedPrice": "HK$628.00" }
211
+ }
212
+ ]
213
+ }
214
+ ```
215
+
216
+ ### category — 分类浏览
217
+
218
+ 按分类 UUID 或内置别名浏览商品列表。
219
+
220
+ 参数:
221
+ - `<id>`: 分类 UUID 或别名,例如 `pre-orders`
222
+
223
+ 选项:
224
+ - `-l, --locale <locale>`: 默认语言区域
225
+ - `-p, --page <page>`: 页码,默认 `1`
226
+ - `-s, --size <size>`: 每页数量,默认 `24`
227
+ - `--sort <sort>`: 排序方式
228
+ - `--filter <facet:key>`: 通用筛选,可重复,例如 `productGenres:ACTION`
229
+ - `--platform <platform>`: 按平台筛选,可重复,例如 `PS5`
230
+ - `--genre <genre>`: 按类型筛选,可重复,例如 `ACTION`
231
+ - `--classification <classification>`: 按商品类型筛选,例如 `FULL_GAME`
232
+ - `--price <range>`: 按价格区间筛选,例如 `0-0`
233
+ - `--release <window>`: 按推出日期筛选,例如 `last_thirty_days`
234
+ - `--vr <compatibility>`: 按 VR 兼容筛选,例如 `PSVR2`
235
+ - `--facets`: 在 JSON/文本输出中包含当前分类支持的筛选项
236
+ - `--sorts`: 在 JSON/文本输出中包含当前分类支持的排序项
237
+ - `-f, --fields <fields>`: 仅输出指定英文字段,字段之间用逗号分隔
238
+ - `--select <fields>`: 字段驱动输出,当前等同 `--fields`
239
+ - `-j, --json`: JSON 输出
240
+
241
+ 支持的排序值:
242
+ - `best-selling`
243
+ - `downloads`
244
+ - `name`
245
+ - `name-desc`
246
+ - `price-asc`
247
+ - `price-desc`
248
+ - `release-new`
249
+ - `release-old`
250
+ - `field:asc` / `field:desc`
251
+
252
+ 示例:
253
+
254
+ ```bash
255
+ # 基本分类浏览
256
+ pstore category pre-orders
257
+ pstore category action
258
+ pstore category rpg
259
+ pstore category sports
260
+ pstore category simulation
261
+ pstore category all-deals
262
+
263
+ # 使用 UUID
264
+ pstore category 3bf499d7-7acf-4931-97dd-2667494ee2c9
265
+
266
+ # 排序
267
+ pstore category pre-orders --sort price-asc
268
+
269
+ # 查看当前分类支持哪些筛选/排序
270
+ pstore category all-deals --size 3 --facets --sorts
271
+
272
+ # 组合筛选
273
+ pstore category all-deals --platform PS5 --genre ACTION --price 0-0 --sort downloads
274
+
275
+ # 分页
276
+ pstore category pre-orders --page 2 --size 12
277
+
278
+ # JSON 输出
279
+ pstore category pre-orders --json
280
+
281
+ # 只输出 ID、标题、商店 URL
282
+ pstore category all-deals --fields id,name,url --json
283
+ ```
284
+
285
+ ### browse — 浏览首页/时间窗页面
286
+
287
+ 按 PlayStation Store 的 `browse` 页面语义浏览列表。当前支持时间窗、排序、分页、随机抽样,以及通用筛选。
288
+
289
+ 选项:
290
+ - `-w, --window <window>`: 时间窗口,可用值包括 `next-thirty-days`、`next_thirty_days`、`last-thirty-days`、`last_thirty_days`、`all`
291
+ - `--sort <sort>`: 排序字段,可用值包括 `conceptReleaseDate`、`sales30`、`downloads30`、`conceptName`、`webBasePrice`、`contentCollections.contentCollectionStartDate`
292
+ - `--order <order>`: 排序方向,可用值包括 `asc`、`desc`
293
+ - `-p, --page <page>`: 页码,默认 `1`
294
+ - `-s, --size <size>`: 每页数量,默认 `24`
295
+ - `--sample <count>`: 随机抽样条目数,默认不抽样
296
+ - `--filter <facet:value>`: 通用筛选,可重复,任意 `facet:value` 都会原样传给 PSN API
297
+ - `--platform <value>`: 平台筛选,例如 `PS5`、`PS4`
298
+ - `--genre <value>`: 类型筛选,例如 `ACTION`、`SPORTS`
299
+ - `--price <range>`: 价格筛选,例如 `0-0`、`10000-19999`
300
+ - `--classification <value>`: 分类筛选,例如 `FULL_GAME`
301
+ - `--age-rating <value>`: 年龄分级筛选
302
+ - `--vr <value>`: VR 兼容性筛选
303
+ - `--notice <value>`: 兼容性提示筛选
304
+ - `-f, --fields <fields>`: 仅输出指定英文字段,字段之间用逗号分隔
305
+ - `--select <fields>`: 字段驱动输出,当前等同 `--fields`
306
+ - `-l, --locale <locale>`: 默认语言区域
307
+ - `-j, --json`: JSON 输出
308
+
309
+ 示例:
310
+
311
+ ```bash
312
+ # 即将推出,按发布日期升序
313
+ pstore browse --window next-thirty-days --sort conceptReleaseDate --order asc --sample 3
314
+
315
+ # 即将推出,按热度/销量
316
+ pstore browse --window next-thirty-days --sort sales30 --order desc --sample 3
317
+
318
+ # 最新推出,按发布日期
319
+ pstore browse --window last-thirty-days --sort conceptReleaseDate --order desc --sample 3
320
+
321
+ # 最新推出,按热度/销量
322
+ pstore browse --window last-thirty-days --sort sales30 --order desc --sample 3
323
+
324
+ # 组合筛选
325
+ pstore browse --platform PS5 --genre ACTION --price 0-0
326
+
327
+ # 原样透传任意 facet:value
328
+ pstore browse --filter conceptGenres:SPORTS --filter webBasePrice:10000-19999
329
+
330
+ # 只输出 ID、标题、商店 URL
331
+ pstore browse --fields id,name,url --json
332
+ ```
333
+
334
+ ### search — 搜索游戏
335
+
336
+ 按关键词搜索 PSN 商店中的游戏。
337
+
338
+ `search` 默认只返回 PSN Search 原始结果中稳定可得的轻量字段,不会额外请求详情接口。需要发行商、发售日期、游戏类型、多语言标题等详情时,先取搜索结果的 `id`,再使用 `lookup <id>`。
339
+
340
+ 参数:
341
+ - `<term>`: 搜索关键词
342
+
343
+ 选项:
344
+ - `-l, --locale <locale>`: 默认语言区域
345
+ - `-p, --page <page>`: 页码,默认 `1`
346
+ - `-s, --size <size>`: 每页数量,默认 `24`
347
+ - `-f, --fields <fields>`: 仅输出指定英文字段,字段之间用逗号分隔
348
+ - `--select <fields>`: 字段驱动输出,当前等同 `--fields`
349
+ - `-j, --json`: JSON 输出
350
+
351
+ 示例:
352
+
353
+ ```bash
354
+ # 基本搜索
355
+ pstore search diablo
356
+
357
+ # 指定语言
358
+ pstore search 原神 --locale zh-hans-HK
359
+
360
+ # JSON 输出
361
+ pstore search "elden ring" --json
362
+
363
+ # JSON 输出并裁剪字段
364
+ pstore search "elden ring" --json --fields id,name,price,url
365
+
366
+ # 只输出 ID、标题、商店 URL
367
+ pstore search "elden ring" --fields id,name,url
368
+
369
+ # 分页
370
+ pstore search game --page 2
371
+ ```
372
+
373
+ ### catalogs — 列出内置分类别名 / 实时发现 catalog
374
+
375
+ 默认显示当前内置的分类别名和对应 UUID。内置表包含当前导航页可发现的 57 个 catalog,以及页面不可直接扫出的隐藏 `all-deals`,共 58 个。UUID 相对稳定,但它们仍属于 PSN 商店数据,严肃使用前建议用 `--validate` 抽样验证。
376
+
377
+ 也可以使用 `--live` 从当前 PSN 页面入口实时爬取 catalog UUID:
378
+
379
+ ```bash
380
+ pstore catalogs
381
+ pstore catalogs --live --depth 1 --limit 50
382
+ pstore catalogs --validate all-deals
383
+ pstore catalogs --validate --json
384
+ ```
385
+
386
+ `--live` 输出里的 `[from latest]` 表示该 UUID 是从 `latest` 导航入口页面发现的来源标记,不代表 catalog 的标题。
387
+
388
+ ### status — 查询 PlayStation Network 服务状态
389
+
390
+ `status` 命令读取 [status.playstation.com](https://status.playstation.com) 的公开配置和区域状态 JSON,用来判断当前 PSN 服务是否存在维护、降级或中断。
391
+
392
+ ```bash
393
+ pstore status
394
+ pstore status --locale en-US
395
+ pstore status --country JP --json
396
+ ```
397
+
398
+ ### config / init
399
+
400
+ ```bash
401
+ # 查看当前配置
402
+ pstore config
403
+
404
+ # 创建默认配置文件
405
+ pstore init
406
+
407
+ # 清空当前进程内缓存
408
+ pstore clear-cache
409
+
410
+ # 真实接口健康检查
411
+ bun run live-smoke
412
+ ```
413
+
414
+ ### 全局选项
415
+
416
+ - `--no-color`: 禁用彩色输出
417
+
418
+ ```bash
419
+ # 禁用彩色输出
420
+ pstore lookup 10014149 --no-color
421
+
422
+ # 或设置环境变量
423
+ export NO_COLOR=1
424
+ ```
425
+
426
+ ## 支持的区域
427
+
428
+ | Locale | 语言 | 地区 | 货币 |
429
+ |--------|------|------|------|
430
+ | `zh-hans-HK` | 简体中文 | 香港 | HK$ |
431
+ | `zh-hant-HK` | 繁體中文 | 香港 | HK$ |
432
+ | `en-HK` | English | 香港 | HK$ |
433
+ | `zh-hant-TW` | 繁體中文 | 台湾 | NT$ |
434
+ | `en-TW` | English | 台湾 | NT$ |
435
+ | `ja-JP` | 日本語 | 日本 | ¥ |
436
+ | `en-US` | English | 美国 | $ |
437
+ | `en-GB` | English | 英国 | £ |
438
+ | `zh-hans-CN` | 简体中文 | 中国大陆 | ¥ |
439
+
440
+ ## 字段说明
441
+
442
+ CLI 字段名统一使用英文 key,便于脚本和开发者消费。文本输出仍会显示中文标签,但 `--fields` / `--select` 应使用英文 key。
443
+
444
+ ### Lookup 默认字段
445
+
446
+ | 字段 | 类型 | 来源 | 说明 |
447
+ |------|------|------|------|
448
+ | `id` | string | derived | 当前结果主 ID。 |
449
+ | `idType` | `"concept"` / `"product"` | derived | 当前结果对象类型。 |
450
+ | `conceptId` | string | telemetry/price/rating/upsell | 所属 concept ID,Product ID 查询时也会尽量补齐。 |
451
+ | `productId` | string | telemetry/price/rating/upsell | 当前 product ID 或 concept 的 default product ID。 |
452
+ | `name` | string | telemetry/batarang | 游戏标题。 |
453
+ | `locale` | string | input | 当前语言区域。 |
454
+ | `publisher` | string | batarang | 发行商;默认 detail profile 会尝试获取。 |
455
+ | `edition` | string | telemetry | 版本名。 |
456
+ | `releaseDate` | string | telemetry/rating/batarang | ISO 8601 发售日期。 |
457
+ | `price` | object | price/telemetry | 顶层价格;Product ID 为该 product 价格,Concept ID 为 default product 价格。 |
458
+ | `rating` | object | rating/batarang | 星级评分摘要,不包含 `ratingsDistribution`。 |
459
+ | `platforms` | string[] | telemetry/upsell/batarang | 平台列表。 |
460
+ | `genres` | string[] | telemetry/batarang | 本地化游戏类型。 |
461
+ | `localizedTitles` | object | lookup by locale | 多区域标题;默认使用 `cjk-en` 预设。 |
462
+ | `warnings` | object[] | runtime | 非致命数据源失败。 |
463
+
464
+ ### Lookup 完整字段
465
+
466
+ | 字段 | 类型 | 来源 | 说明 |
467
+ |------|------|------|------|
468
+ | `productName` | string | telemetry | 商品级标题。 |
469
+ | `skus` | object[] | telemetry | SKU 信息。 |
470
+ | `descriptions` | object[] | batarang | 短描述、长描述、兼容性说明和法律文本。 |
471
+ | `media` | object[] | upsell/batarang | 截图、视频和商店图片。 |
472
+ | `products` | object[] | upsell | 同一 concept 下的关联版本;每个版本保留自己的 `price`。 |
473
+ | `contentRating` | object | telemetry/batarang | 年龄分级。 |
474
+ | `concept` | object | lookup | Product ID 查询时通过 `--with-concept` 返回的 concept/default product 补充数据。 |
475
+ | `sources` | object | runtime | 关键字段来源。 |
476
+ | `timing` | object | runtime | 各数据源耗时。 |
477
+ | `npTitleId` | string | telemetry/batarang | PSN title id;默认 detail profile 不输出,可用 `--fields npTitleId` 选择。 |
478
+
479
+ ### Search / Category / Browse 列表字段
480
+
481
+ | 字段 | 类型 | 默认 | 来源 | 说明 |
482
+ |------|------|------|------|------|
483
+ | `id` | string | 是 | search/category grid | Concept ID 或 Product ID;结合 `type` 判断。 |
484
+ | `name` | string | 是 | search/category grid | 本地化标题。 |
485
+ | `type` | `"concept"` / `"product"` | 是 | `__typename` | 列表项类型。 |
486
+ | `price` | string/null | 是 | search/category grid | 当前显示价格。 |
487
+ | `platforms` | string[]/null | 是 | search/category grid | 平台列表。 |
488
+ | `classification` | string/null | 是 | search/category grid | 本地化商品类型,例如制品版游戏、豪华版。 |
489
+ | `url` | string | 否 | derived | 商店链接;需要时显式 `--fields id,name,url`。 |
490
+
491
+ > 需要描述、发行商、内容分级等页面详情时使用 `lookup --batarang`、`lookup --full` 或对应 `lookup --select` 字段。`search` 默认不做详情增强,以避免 N+1 请求。
492
+
493
+ ## 分类别名
494
+
495
+ | 别名 | UUID | 说明 |
496
+ |------|------|------|
497
+ | `all-deals` | `3f772501-...` | 所有优惠,隐藏 catalog |
498
+ | `all-games` | `28c9c2b2-...` | 所有游戏 |
499
+ | `all-ps5-games` | `d0446d4b-...` | 所有 PS5 游戏 |
500
+ | `all-ps4-games` | `30e3fe35-...` | 所有 PS4 游戏 |
501
+ | `pre-orders` | `3bf499d7-...` | 预购游戏 |
502
+ | `add-ons` | `51c9aa7a-...` | 追加内容 |
503
+ | `free-to-play` | `4dfd67ab-...` | 基本免费游戏 |
504
+ | `best-sellers` | `132bb05e-...` | 最畅销 |
505
+ | `new-games` | `e1699f77-...` | 新的游戏 |
506
+ | `action` | `298b428c-...` | 动作游戏 |
507
+ | `rpg` | `e0b1cde3-...` | 角色扮演 |
508
+ | `shooter` | `64ee024b-...` | 射击游戏 |
509
+ | `sports` | `b86f0f65-...` | 体育游戏 |
510
+ | `racing` | `b78c6346-...` | 赛车游戏 |
511
+ | `fighting` | `0f6fc626-...` | 格斗游戏 |
512
+ | `simulation` | `bb42a4e0-...` | 模拟游戏 |
513
+
514
+ 完整列表以 `pstore catalogs` 和导出的 `BUILTIN_CATALOGS` 为准。
515
+
516
+ ## 配置文件
517
+
518
+ pstore 会读取 `pstore.config.json` 作为默认配置文件。
519
+
520
+ 查找顺序:
521
+ 1. 当前项目目录下的 `./pstore.config.json`
522
+ 2. 当前项目目录下的 `./psn-config.json`(旧配置兼容)
523
+ 3. 用户目录下的 `~/.config/pstore/config.json`
524
+ 4. 用户目录下的 `~/.config/psn-cli/config.json`(旧配置兼容)
525
+
526
+ 默认内容:
527
+
528
+ ```json
529
+ {
530
+ "locale": "zh-hans-HK",
531
+ "format": "text",
532
+ "pageSize": 24,
533
+ "verbose": false,
534
+ "color": true,
535
+ "cacheTtl": {
536
+ "lookup": 300000,
537
+ "search": 120000,
538
+ "category": 120000
539
+ }
540
+ }
541
+ ```
542
+
543
+ 字段说明:
544
+ - `locale`: 默认语言区域。
545
+ - `format`: 默认输出格式,`text` 或 `json`。
546
+ - `pageSize`: `search`、`category`、`browse` 的默认页大小,范围 `1-100`。
547
+ - `verbose`: lookup 文本输出是否默认展示耗时。
548
+ - `color`: 是否默认使用彩色输出。
549
+ - `cacheTtl.lookup/search/category`: 内存缓存 TTL,单位毫秒。
550
+
551
+ 生成配置文件:
552
+
553
+ ```bash
554
+ pstore init
555
+ ```
556
+
557
+ ## 技术说明
558
+
559
+ - 使用 PSN GraphQL persisted query API(无需登录)。
560
+ - 详情页由多个 operation 组合:telemetry、price、rating、upsell,以及可选 Batarang HTML。
561
+ - product ID 对应 PSN `productRetrieve`;顶层价格表示该 product/edition 的 CTA 价格。
562
+ - concept ID 对应 PSN `conceptRetrieve`;顶层价格表示 default product 主 CTA 价格。
563
+ - `conceptRetrieveForUpsellWithCtas` 返回多个 edition/product,项目会保留每个 `products[].price`。
564
+ - HTTP 超时 10s,默认重试 2 次;429/5xx 会退避,并尊重 `Retry-After`。
565
+ - 批量 lookup 默认 3 并发,可通过 `--concurrency` 调整。
566
+ - 内存 TTL 缓存覆盖 lookup、search、category。
567
+ - Batarang HTML 数据通过 SSR 页面提取(无需浏览器)。
568
+ - Product ID 结果会保留 product 作为主对象,并通过 `conceptId` 标明所属 concept;需要 concept/default product 详情时使用 `--with-concept`。
569
+
570
+ ## 给脚本使用的建议
571
+
572
+ 普通 CLI 也适合脚本调用,只要始终使用 JSON 输出:
573
+
574
+ ```bash
575
+ pstore lookup 10005069 --products --title-locales cjk-en --json
576
+ pstore search "Saros" --locale zh-hans-HK --json --fields id,name,price,url
577
+ pstore browse --window next-thirty-days --sort conceptReleaseDate --order asc --sample 3 --json --fields id,name,price,url
578
+ ```
579
+
580
+ 建议:
581
+
582
+ - 始终加 `--json`,并读取 `warnings` 字段判断结果是否部分缺失。
583
+ - 查 product 详情时,使用顶层 `price` 作为该 product 价格;查 concept 详情时,使用顶层 `price` 作为 default product 价格,使用 `products[].price` 作为各版本价格。
584
+ - 批量 lookup 使用 `--concurrency 2` 或 `--concurrency 3`,降低触发 429 的概率。
585
+ - 需要多语言标题时使用 `--title-locales cjk-en`。
586
+ - 需要描述、发行商、内容分级等页面详情时加 `--batarang`。
587
+ - 不要依赖文本字段顺序;以 JSON key 为准。
588
+
589
+ ## 日志
590
+
591
+ API 请求日志自动写入 `logs/pstore-YYYY-MM-DD.log`,包含每次请求的耗时和结果。
592
+
593
+ ## 测试
594
+
595
+ ```bash
596
+ bun test
597
+ bunx tsc --noEmit
598
+ bun run live-smoke
599
+ bun run test:live
600
+ bun run test:live:locales
601
+ bun run test:live:full
602
+ PSTORE_SMOKE_LOCALES=zh-hans-HK,en-US,ja-JP,zh-hant-TW,en-TW bun run live-smoke
603
+ PSTORE_SMOKE_FULL=1 bun run live-smoke
604
+ ```
605
+
606
+ 当前测试覆盖单元测试、API 响应结构验证、CLI/包契约、性能基准测试、极端场景测试。`live-smoke` 默认只检查默认区域;通过 `PSTORE_SMOKE_LOCALES` 和 `PSTORE_SMOKE_FULL=1` 可扩大线上覆盖,用于检查 persisted query hash 或字段结构是否漂移。
607
+
608
+ ## 许可
609
+
610
+ MIT
@@ -0,0 +1,25 @@
1
+ import type { ConceptId, BatarangData, PsnLocale } from "../types.js";
2
+ /**
3
+ * Fetch a PSN concept page HTML (raw) for concept ID extraction.
4
+ */
5
+ export declare function fetchBatarangHtml(productId: string, locale?: PsnLocale): Promise<string | null>;
6
+ /**
7
+ * Fetch a PSN concept page HTML and parse Batarang data.
8
+ */
9
+ export declare function fetchBatarang(conceptId: ConceptId, locale?: PsnLocale): Promise<BatarangData | null>;
10
+ /**
11
+ * Fetch a PSN product page HTML and parse Batarang data.
12
+ */
13
+ export declare function fetchBatarangByProduct(productId: string, locale?: PsnLocale): Promise<BatarangData | null>;
14
+ /**
15
+ * Extract concept ID from a product page HTML.
16
+ */
17
+ export declare function extractConceptId(html: string): string | null;
18
+ /**
19
+ * Extract all env script contents from HTML.
20
+ */
21
+ export declare function extractEnvScripts(html: string): string[];
22
+ /**
23
+ * Parse and merge all Batarang env scripts into a single data object.
24
+ */
25
+ export declare function parseBatarangAll(html: string): BatarangData;
@@ -0,0 +1,64 @@
1
+ import type { PsnLocale } from "../types.js";
2
+ import { type CategoryGridItem } from "./graphql.js";
3
+ export declare const BROWSE_ALIAS: "browse";
4
+ export declare const BROWSE_CLIENT_ID = "b6de8d4d-bf9b-11ee-ad2a-aea73dc1ea43";
5
+ export declare const BROWSE_EXPERIENCE_ID = "7bbceafe-bfa8-11ee-b375-5e45f4e139ac";
6
+ export declare const BROWSE_CATEGORY_ID = "28c9c2b2-cecc-415c-9a08-482a605cb104";
7
+ export declare const BROWSE_LOCALIZED_KEY_ID = "cat.gma.x_All_games";
8
+ export type BrowseWindow = "all" | "next-thirty-days" | "last-thirty-days";
9
+ export type BrowseSort = string;
10
+ export type BrowseOrder = "asc" | "desc";
11
+ export interface BrowseQuery {
12
+ window?: string;
13
+ sortBy?: string;
14
+ order?: string;
15
+ page?: number;
16
+ size?: number;
17
+ locale?: PsnLocale;
18
+ sample?: number;
19
+ filters?: string[];
20
+ platforms?: string[];
21
+ genres?: string[];
22
+ prices?: string[];
23
+ classifications?: string[];
24
+ ageRatings?: string[];
25
+ vrCompatibilities?: string[];
26
+ notices?: string[];
27
+ }
28
+ export interface BrowseResolvedQuery {
29
+ window: BrowseWindow;
30
+ sortBy: BrowseSort;
31
+ order: BrowseOrder;
32
+ page: number;
33
+ size: number;
34
+ sample?: number;
35
+ filters: string[];
36
+ }
37
+ export interface BrowseGridResult {
38
+ categoryId: string;
39
+ items: CategoryGridItem[];
40
+ query: BrowseResolvedQuery;
41
+ }
42
+ /**
43
+ * Normalize the browse window string.
44
+ */
45
+ export declare function normalizeBrowseWindow(input?: string): BrowseWindow;
46
+ /**
47
+ * Resolve the browse sort defaults for a given window.
48
+ */
49
+ export declare function resolveBrowseSortDefaults(window: BrowseWindow): {
50
+ sortBy: BrowseSort;
51
+ order: BrowseOrder;
52
+ };
53
+ /**
54
+ * Resolve a complete browse query from user input.
55
+ */
56
+ export declare function resolveBrowseQuery(query: BrowseQuery): BrowseResolvedQuery;
57
+ /**
58
+ * Randomly sample up to `count` items from a list.
59
+ */
60
+ export declare function sampleItems<T>(items: T[], count: number, rng?: () => number): T[];
61
+ /**
62
+ * Fetch browse results.
63
+ */
64
+ export declare function fetchBrowseGrid(query: BrowseQuery): Promise<BrowseGridResult | null>;