publishport-opencli 1.0.8 → 1.0.9
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/cli-manifest.json +442 -1
- package/clis/facebook/feed.js +78 -5
- package/clis/facebook/search.js +83 -22
- package/clis/xianyu/collect.js +1 -0
- package/clis/xianyu/location.js +1 -0
- package/clis/xianyu/manage.js +1 -0
- package/clis/xianyu/mtop.js +47 -0
- package/clis/xianyu/order.js +1 -0
- package/clis/xianyu/rate.js +1 -0
- package/clis/xianyu/user.js +1 -0
- package/dist/src/browser/base-page.d.ts +19 -0
- package/dist/src/browser/base-page.js +1 -1
- package/dist/src/browser/target-resolver.d.ts +1 -0
- package/dist/src/browser/target-resolver.js +100 -6
- package/dist/src/cli.js +1 -1
- package/dist/src/external.js +1 -1
- package/dist/src/types.d.ts +3 -0
- package/package.json +1 -1
package/cli-manifest.json
CHANGED
|
@@ -15215,7 +15215,7 @@
|
|
|
15215
15215
|
"type": "js",
|
|
15216
15216
|
"modulePath": "facebook/search.js",
|
|
15217
15217
|
"sourceFile": "facebook/search.js",
|
|
15218
|
-
"navigateBefore":
|
|
15218
|
+
"navigateBefore": false
|
|
15219
15219
|
},
|
|
15220
15220
|
{
|
|
15221
15221
|
"site": "facebook",
|
|
@@ -47183,6 +47183,125 @@
|
|
|
47183
47183
|
"sourceFile": "xianyu/chat.js",
|
|
47184
47184
|
"navigateBefore": false
|
|
47185
47185
|
},
|
|
47186
|
+
{
|
|
47187
|
+
"site": "xianyu",
|
|
47188
|
+
"name": "collect",
|
|
47189
|
+
"description": "列出我收藏的商品(分页)",
|
|
47190
|
+
"access": "read",
|
|
47191
|
+
"domain": "www.goofish.com",
|
|
47192
|
+
"strategy": "cookie",
|
|
47193
|
+
"browser": true,
|
|
47194
|
+
"args": [
|
|
47195
|
+
{
|
|
47196
|
+
"name": "page",
|
|
47197
|
+
"type": "int",
|
|
47198
|
+
"default": 1,
|
|
47199
|
+
"required": false,
|
|
47200
|
+
"help": "页码,从 1 开始"
|
|
47201
|
+
},
|
|
47202
|
+
{
|
|
47203
|
+
"name": "page_size",
|
|
47204
|
+
"type": "int",
|
|
47205
|
+
"default": 20,
|
|
47206
|
+
"required": false,
|
|
47207
|
+
"help": "每页条数"
|
|
47208
|
+
}
|
|
47209
|
+
],
|
|
47210
|
+
"columns": [
|
|
47211
|
+
"item_id",
|
|
47212
|
+
"title",
|
|
47213
|
+
"price",
|
|
47214
|
+
"seller_nick",
|
|
47215
|
+
"want_cnt",
|
|
47216
|
+
"status_str"
|
|
47217
|
+
],
|
|
47218
|
+
"type": "js",
|
|
47219
|
+
"modulePath": "xianyu/collect.js",
|
|
47220
|
+
"sourceFile": "xianyu/collect.js",
|
|
47221
|
+
"navigateBefore": false
|
|
47222
|
+
},
|
|
47223
|
+
{
|
|
47224
|
+
"site": "xianyu",
|
|
47225
|
+
"name": "delete",
|
|
47226
|
+
"description": "下架/删除商品",
|
|
47227
|
+
"access": "write",
|
|
47228
|
+
"domain": "www.goofish.com",
|
|
47229
|
+
"strategy": "cookie",
|
|
47230
|
+
"browser": true,
|
|
47231
|
+
"args": [
|
|
47232
|
+
{
|
|
47233
|
+
"name": "item_id",
|
|
47234
|
+
"type": "str",
|
|
47235
|
+
"required": true,
|
|
47236
|
+
"positional": true,
|
|
47237
|
+
"help": "商品 itemId"
|
|
47238
|
+
}
|
|
47239
|
+
],
|
|
47240
|
+
"columns": [
|
|
47241
|
+
"item_id",
|
|
47242
|
+
"ok"
|
|
47243
|
+
],
|
|
47244
|
+
"type": "js",
|
|
47245
|
+
"modulePath": "xianyu/manage.js",
|
|
47246
|
+
"sourceFile": "xianyu/manage.js",
|
|
47247
|
+
"navigateBefore": false
|
|
47248
|
+
},
|
|
47249
|
+
{
|
|
47250
|
+
"site": "xianyu",
|
|
47251
|
+
"name": "edit",
|
|
47252
|
+
"description": "编辑商品(标题/详情/价格/划线价/库存,按需传)",
|
|
47253
|
+
"access": "write",
|
|
47254
|
+
"domain": "www.goofish.com",
|
|
47255
|
+
"strategy": "cookie",
|
|
47256
|
+
"browser": true,
|
|
47257
|
+
"args": [
|
|
47258
|
+
{
|
|
47259
|
+
"name": "item_id",
|
|
47260
|
+
"type": "str",
|
|
47261
|
+
"required": true,
|
|
47262
|
+
"positional": true,
|
|
47263
|
+
"help": "商品 itemId"
|
|
47264
|
+
},
|
|
47265
|
+
{
|
|
47266
|
+
"name": "title",
|
|
47267
|
+
"type": "str",
|
|
47268
|
+
"required": false,
|
|
47269
|
+
"help": "新标题"
|
|
47270
|
+
},
|
|
47271
|
+
{
|
|
47272
|
+
"name": "desc",
|
|
47273
|
+
"type": "str",
|
|
47274
|
+
"required": false,
|
|
47275
|
+
"help": "新详情描述"
|
|
47276
|
+
},
|
|
47277
|
+
{
|
|
47278
|
+
"name": "price",
|
|
47279
|
+
"type": "float",
|
|
47280
|
+
"required": false,
|
|
47281
|
+
"help": "新价格(元)"
|
|
47282
|
+
},
|
|
47283
|
+
{
|
|
47284
|
+
"name": "original_price",
|
|
47285
|
+
"type": "float",
|
|
47286
|
+
"required": false,
|
|
47287
|
+
"help": "新划线价(元),传 0 清除"
|
|
47288
|
+
},
|
|
47289
|
+
{
|
|
47290
|
+
"name": "quantity",
|
|
47291
|
+
"type": "int",
|
|
47292
|
+
"required": false,
|
|
47293
|
+
"help": "新库存数量"
|
|
47294
|
+
}
|
|
47295
|
+
],
|
|
47296
|
+
"columns": [
|
|
47297
|
+
"item_id",
|
|
47298
|
+
"ok"
|
|
47299
|
+
],
|
|
47300
|
+
"type": "js",
|
|
47301
|
+
"modulePath": "xianyu/manage.js",
|
|
47302
|
+
"sourceFile": "xianyu/manage.js",
|
|
47303
|
+
"navigateBefore": false
|
|
47304
|
+
},
|
|
47186
47305
|
{
|
|
47187
47306
|
"site": "xianyu",
|
|
47188
47307
|
"name": "inbox",
|
|
@@ -47263,6 +47382,42 @@
|
|
|
47263
47382
|
"sourceFile": "xianyu/item.js",
|
|
47264
47383
|
"navigateBefore": false
|
|
47265
47384
|
},
|
|
47385
|
+
{
|
|
47386
|
+
"site": "xianyu",
|
|
47387
|
+
"name": "location",
|
|
47388
|
+
"description": "获取账号默认发布地址",
|
|
47389
|
+
"access": "read",
|
|
47390
|
+
"domain": "www.goofish.com",
|
|
47391
|
+
"strategy": "cookie",
|
|
47392
|
+
"browser": true,
|
|
47393
|
+
"args": [
|
|
47394
|
+
{
|
|
47395
|
+
"name": "longitude",
|
|
47396
|
+
"type": "float",
|
|
47397
|
+
"default": 121.4737,
|
|
47398
|
+
"required": false,
|
|
47399
|
+
"help": "经度(默认上海)"
|
|
47400
|
+
},
|
|
47401
|
+
{
|
|
47402
|
+
"name": "latitude",
|
|
47403
|
+
"type": "float",
|
|
47404
|
+
"default": 31.2304,
|
|
47405
|
+
"required": false,
|
|
47406
|
+
"help": "纬度(默认上海)"
|
|
47407
|
+
}
|
|
47408
|
+
],
|
|
47409
|
+
"columns": [
|
|
47410
|
+
"prov",
|
|
47411
|
+
"city",
|
|
47412
|
+
"area",
|
|
47413
|
+
"poi",
|
|
47414
|
+
"division_id"
|
|
47415
|
+
],
|
|
47416
|
+
"type": "js",
|
|
47417
|
+
"modulePath": "xianyu/location.js",
|
|
47418
|
+
"sourceFile": "xianyu/location.js",
|
|
47419
|
+
"navigateBefore": false
|
|
47420
|
+
},
|
|
47266
47421
|
{
|
|
47267
47422
|
"site": "xianyu",
|
|
47268
47423
|
"name": "login",
|
|
@@ -47346,6 +47501,220 @@
|
|
|
47346
47501
|
"sourceFile": "xianyu/messages.js",
|
|
47347
47502
|
"navigateBefore": false
|
|
47348
47503
|
},
|
|
47504
|
+
{
|
|
47505
|
+
"site": "xianyu",
|
|
47506
|
+
"name": "mine",
|
|
47507
|
+
"description": "列出当前账号在售/已发布商品(分页)",
|
|
47508
|
+
"access": "read",
|
|
47509
|
+
"domain": "www.goofish.com",
|
|
47510
|
+
"strategy": "cookie",
|
|
47511
|
+
"browser": true,
|
|
47512
|
+
"args": [
|
|
47513
|
+
{
|
|
47514
|
+
"name": "page",
|
|
47515
|
+
"type": "int",
|
|
47516
|
+
"default": 1,
|
|
47517
|
+
"required": false,
|
|
47518
|
+
"help": "页码,从 1 开始"
|
|
47519
|
+
},
|
|
47520
|
+
{
|
|
47521
|
+
"name": "page_size",
|
|
47522
|
+
"type": "int",
|
|
47523
|
+
"default": 20,
|
|
47524
|
+
"required": false,
|
|
47525
|
+
"help": "每页条数"
|
|
47526
|
+
}
|
|
47527
|
+
],
|
|
47528
|
+
"columns": [
|
|
47529
|
+
"item_id",
|
|
47530
|
+
"title",
|
|
47531
|
+
"price",
|
|
47532
|
+
"status",
|
|
47533
|
+
"main_pic"
|
|
47534
|
+
],
|
|
47535
|
+
"type": "js",
|
|
47536
|
+
"modulePath": "xianyu/manage.js",
|
|
47537
|
+
"sourceFile": "xianyu/manage.js",
|
|
47538
|
+
"navigateBefore": false
|
|
47539
|
+
},
|
|
47540
|
+
{
|
|
47541
|
+
"site": "xianyu",
|
|
47542
|
+
"name": "order",
|
|
47543
|
+
"description": "列出卖家已售订单(可按状态筛选)",
|
|
47544
|
+
"access": "read",
|
|
47545
|
+
"domain": "www.goofish.com",
|
|
47546
|
+
"strategy": "cookie",
|
|
47547
|
+
"browser": true,
|
|
47548
|
+
"args": [
|
|
47549
|
+
{
|
|
47550
|
+
"name": "page",
|
|
47551
|
+
"type": "int",
|
|
47552
|
+
"default": 1,
|
|
47553
|
+
"required": false,
|
|
47554
|
+
"help": "页码,从 1 开始"
|
|
47555
|
+
},
|
|
47556
|
+
{
|
|
47557
|
+
"name": "page_size",
|
|
47558
|
+
"type": "int",
|
|
47559
|
+
"default": 20,
|
|
47560
|
+
"required": false,
|
|
47561
|
+
"help": "每页条数"
|
|
47562
|
+
},
|
|
47563
|
+
{
|
|
47564
|
+
"name": "status",
|
|
47565
|
+
"type": "str",
|
|
47566
|
+
"default": "ALL",
|
|
47567
|
+
"required": false,
|
|
47568
|
+
"help": "订单状态筛选:ALL / WAITING_PAY / WAITING_SEND / WAITING_RECEIVE / WAITING_RATE / SUCCESS / REFUND"
|
|
47569
|
+
}
|
|
47570
|
+
],
|
|
47571
|
+
"columns": [
|
|
47572
|
+
"order_id",
|
|
47573
|
+
"item_id",
|
|
47574
|
+
"buyer_nick",
|
|
47575
|
+
"status_str",
|
|
47576
|
+
"total_price",
|
|
47577
|
+
"create_time"
|
|
47578
|
+
],
|
|
47579
|
+
"type": "js",
|
|
47580
|
+
"modulePath": "xianyu/order.js",
|
|
47581
|
+
"sourceFile": "xianyu/order.js",
|
|
47582
|
+
"navigateBefore": false
|
|
47583
|
+
},
|
|
47584
|
+
{
|
|
47585
|
+
"site": "xianyu",
|
|
47586
|
+
"name": "order-get",
|
|
47587
|
+
"description": "查询单个闲鱼订单详情",
|
|
47588
|
+
"access": "read",
|
|
47589
|
+
"domain": "www.goofish.com",
|
|
47590
|
+
"strategy": "cookie",
|
|
47591
|
+
"browser": true,
|
|
47592
|
+
"args": [
|
|
47593
|
+
{
|
|
47594
|
+
"name": "order_id",
|
|
47595
|
+
"type": "str",
|
|
47596
|
+
"required": true,
|
|
47597
|
+
"positional": true,
|
|
47598
|
+
"help": "订单号 orderId"
|
|
47599
|
+
},
|
|
47600
|
+
{
|
|
47601
|
+
"name": "role",
|
|
47602
|
+
"type": "str",
|
|
47603
|
+
"default": "seller",
|
|
47604
|
+
"required": false,
|
|
47605
|
+
"help": "视角:seller / buyer"
|
|
47606
|
+
}
|
|
47607
|
+
],
|
|
47608
|
+
"columns": [
|
|
47609
|
+
"order_id",
|
|
47610
|
+
"item_id",
|
|
47611
|
+
"status_str",
|
|
47612
|
+
"total_price",
|
|
47613
|
+
"buyer_nick"
|
|
47614
|
+
],
|
|
47615
|
+
"type": "js",
|
|
47616
|
+
"modulePath": "xianyu/order.js",
|
|
47617
|
+
"sourceFile": "xianyu/order.js",
|
|
47618
|
+
"navigateBefore": false
|
|
47619
|
+
},
|
|
47620
|
+
{
|
|
47621
|
+
"site": "xianyu",
|
|
47622
|
+
"name": "order-ship-dummy",
|
|
47623
|
+
"description": "虚拟发货(仅虚拟商品,无需物流单号)",
|
|
47624
|
+
"access": "write",
|
|
47625
|
+
"domain": "www.goofish.com",
|
|
47626
|
+
"strategy": "cookie",
|
|
47627
|
+
"browser": true,
|
|
47628
|
+
"args": [
|
|
47629
|
+
{
|
|
47630
|
+
"name": "order_id",
|
|
47631
|
+
"type": "str",
|
|
47632
|
+
"required": true,
|
|
47633
|
+
"positional": true,
|
|
47634
|
+
"help": "订单号 orderId"
|
|
47635
|
+
},
|
|
47636
|
+
{
|
|
47637
|
+
"name": "text",
|
|
47638
|
+
"type": "str",
|
|
47639
|
+
"default": "",
|
|
47640
|
+
"required": false,
|
|
47641
|
+
"help": "发货附言(可选)"
|
|
47642
|
+
}
|
|
47643
|
+
],
|
|
47644
|
+
"columns": [
|
|
47645
|
+
"order_id",
|
|
47646
|
+
"ok"
|
|
47647
|
+
],
|
|
47648
|
+
"type": "js",
|
|
47649
|
+
"modulePath": "xianyu/order.js",
|
|
47650
|
+
"sourceFile": "xianyu/order.js",
|
|
47651
|
+
"navigateBefore": false
|
|
47652
|
+
},
|
|
47653
|
+
{
|
|
47654
|
+
"site": "xianyu",
|
|
47655
|
+
"name": "order-ship-freeshipping",
|
|
47656
|
+
"description": "包邮发货(拼团活动专用)",
|
|
47657
|
+
"access": "write",
|
|
47658
|
+
"domain": "www.goofish.com",
|
|
47659
|
+
"strategy": "cookie",
|
|
47660
|
+
"browser": true,
|
|
47661
|
+
"args": [
|
|
47662
|
+
{
|
|
47663
|
+
"name": "order_id",
|
|
47664
|
+
"type": "str",
|
|
47665
|
+
"required": true,
|
|
47666
|
+
"positional": true,
|
|
47667
|
+
"help": "订单号 bizOrderId"
|
|
47668
|
+
},
|
|
47669
|
+
{
|
|
47670
|
+
"name": "item_id",
|
|
47671
|
+
"type": "str",
|
|
47672
|
+
"required": true,
|
|
47673
|
+
"help": "商品 itemId"
|
|
47674
|
+
},
|
|
47675
|
+
{
|
|
47676
|
+
"name": "buyer_id",
|
|
47677
|
+
"type": "str",
|
|
47678
|
+
"required": true,
|
|
47679
|
+
"help": "买家 userId"
|
|
47680
|
+
}
|
|
47681
|
+
],
|
|
47682
|
+
"columns": [
|
|
47683
|
+
"order_id",
|
|
47684
|
+
"ok"
|
|
47685
|
+
],
|
|
47686
|
+
"type": "js",
|
|
47687
|
+
"modulePath": "xianyu/order.js",
|
|
47688
|
+
"sourceFile": "xianyu/order.js",
|
|
47689
|
+
"navigateBefore": false
|
|
47690
|
+
},
|
|
47691
|
+
{
|
|
47692
|
+
"site": "xianyu",
|
|
47693
|
+
"name": "polish",
|
|
47694
|
+
"description": "擦亮商品(刷新曝光排序,不改内容)",
|
|
47695
|
+
"access": "write",
|
|
47696
|
+
"domain": "www.goofish.com",
|
|
47697
|
+
"strategy": "cookie",
|
|
47698
|
+
"browser": true,
|
|
47699
|
+
"args": [
|
|
47700
|
+
{
|
|
47701
|
+
"name": "item_id",
|
|
47702
|
+
"type": "str",
|
|
47703
|
+
"required": true,
|
|
47704
|
+
"positional": true,
|
|
47705
|
+
"help": "商品 itemId"
|
|
47706
|
+
}
|
|
47707
|
+
],
|
|
47708
|
+
"columns": [
|
|
47709
|
+
"item_id",
|
|
47710
|
+
"ok",
|
|
47711
|
+
"already_polished"
|
|
47712
|
+
],
|
|
47713
|
+
"type": "js",
|
|
47714
|
+
"modulePath": "xianyu/manage.js",
|
|
47715
|
+
"sourceFile": "xianyu/manage.js",
|
|
47716
|
+
"navigateBefore": false
|
|
47717
|
+
},
|
|
47349
47718
|
{
|
|
47350
47719
|
"site": "xianyu",
|
|
47351
47720
|
"name": "publish",
|
|
@@ -47423,6 +47792,48 @@
|
|
|
47423
47792
|
"sourceFile": "xianyu/publish.js",
|
|
47424
47793
|
"navigateBefore": false
|
|
47425
47794
|
},
|
|
47795
|
+
{
|
|
47796
|
+
"site": "xianyu",
|
|
47797
|
+
"name": "rate",
|
|
47798
|
+
"description": "拉某用户收到的信用评价列表",
|
|
47799
|
+
"access": "read",
|
|
47800
|
+
"domain": "www.goofish.com",
|
|
47801
|
+
"strategy": "cookie",
|
|
47802
|
+
"browser": true,
|
|
47803
|
+
"args": [
|
|
47804
|
+
{
|
|
47805
|
+
"name": "user_id",
|
|
47806
|
+
"type": "str",
|
|
47807
|
+
"required": true,
|
|
47808
|
+
"positional": true,
|
|
47809
|
+
"help": "被评价用户 userId(明文数字 uid)"
|
|
47810
|
+
},
|
|
47811
|
+
{
|
|
47812
|
+
"name": "page",
|
|
47813
|
+
"type": "int",
|
|
47814
|
+
"default": 1,
|
|
47815
|
+
"required": false,
|
|
47816
|
+
"help": "页码,从 1 开始"
|
|
47817
|
+
},
|
|
47818
|
+
{
|
|
47819
|
+
"name": "page_size",
|
|
47820
|
+
"type": "int",
|
|
47821
|
+
"default": 20,
|
|
47822
|
+
"required": false,
|
|
47823
|
+
"help": "每页条数"
|
|
47824
|
+
}
|
|
47825
|
+
],
|
|
47826
|
+
"columns": [
|
|
47827
|
+
"rate_id",
|
|
47828
|
+
"rater_nick",
|
|
47829
|
+
"rate_content",
|
|
47830
|
+
"create_time"
|
|
47831
|
+
],
|
|
47832
|
+
"type": "js",
|
|
47833
|
+
"modulePath": "xianyu/rate.js",
|
|
47834
|
+
"sourceFile": "xianyu/rate.js",
|
|
47835
|
+
"navigateBefore": false
|
|
47836
|
+
},
|
|
47426
47837
|
{
|
|
47427
47838
|
"site": "xianyu",
|
|
47428
47839
|
"name": "reply",
|
|
@@ -47538,6 +47949,36 @@
|
|
|
47538
47949
|
"sourceFile": "xianyu/search.js",
|
|
47539
47950
|
"navigateBefore": false
|
|
47540
47951
|
},
|
|
47952
|
+
{
|
|
47953
|
+
"site": "xianyu",
|
|
47954
|
+
"name": "user",
|
|
47955
|
+
"description": "查询闲鱼用户主页信息(不传 user_id 看自己)",
|
|
47956
|
+
"access": "read",
|
|
47957
|
+
"domain": "www.goofish.com",
|
|
47958
|
+
"strategy": "cookie",
|
|
47959
|
+
"browser": true,
|
|
47960
|
+
"args": [
|
|
47961
|
+
{
|
|
47962
|
+
"name": "user_id",
|
|
47963
|
+
"type": "str",
|
|
47964
|
+
"required": false,
|
|
47965
|
+
"positional": true,
|
|
47966
|
+
"help": "目标用户 userId;留空看自己"
|
|
47967
|
+
}
|
|
47968
|
+
],
|
|
47969
|
+
"columns": [
|
|
47970
|
+
"user_id",
|
|
47971
|
+
"nick",
|
|
47972
|
+
"city",
|
|
47973
|
+
"credit_level",
|
|
47974
|
+
"fans_count",
|
|
47975
|
+
"follow_count"
|
|
47976
|
+
],
|
|
47977
|
+
"type": "js",
|
|
47978
|
+
"modulePath": "xianyu/user.js",
|
|
47979
|
+
"sourceFile": "xianyu/user.js",
|
|
47980
|
+
"navigateBefore": false
|
|
47981
|
+
},
|
|
47541
47982
|
{
|
|
47542
47983
|
"site": "xianyu",
|
|
47543
47984
|
"name": "whoami",
|
package/clis/facebook/feed.js
CHANGED
|
@@ -1,8 +1,27 @@
|
|
|
1
|
-
import{ArgumentError as
|
|
2
|
-
const limit = ${
|
|
1
|
+
import{ArgumentError as K,AuthRequiredError as P,CommandExecutionError as Q,EmptyResultError as U}from"@jackwener/opencli/errors";import{cli as h,Strategy as j}from"@jackwener/opencli/registry";const T="https://www.facebook.com/",V=50;function W(f){const J=Number(f);if(!Number.isInteger(J)||J<1||J>V)throw new K(`facebook feed --limit must be an integer between 1 and ${V}`);return J}function Y(f){if(f&&typeof f==="object"&&"data"in f)return f.data;return f}function Z(f){return`(() => {
|
|
2
|
+
const limit = ${f};
|
|
3
3
|
|
|
4
4
|
function clean(value) {
|
|
5
|
-
|
|
5
|
+
// Strip zero-width / bidi control chars first: Facebook injects them into
|
|
6
|
+
// decoy nodes to poison scrapers. See issue #2089.
|
|
7
|
+
return String(value || '')
|
|
8
|
+
.replace(/[\\u200b-\\u200f\\u202a-\\u202e\\u2060\\ufeff]/g, '')
|
|
9
|
+
.replace(/\\s+/g, ' ')
|
|
10
|
+
.trim();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// FB anti-scrape decoys: long spaceless digit runs, spaced single-char
|
|
14
|
+
// strings ("a b c d e"), and hidden-domain .com spam. Real author/content
|
|
15
|
+
// text never looks like this.
|
|
16
|
+
function isDecoyText(text) {
|
|
17
|
+
if (!text) return true;
|
|
18
|
+
if (/^\\d{8,}$/.test(text)) return true;
|
|
19
|
+
if (/^(?:\\S ){4,}\\S$/.test(text) && text.replace(/\\s/g, '').length <= 12) return true;
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isReelsOrCarouselChrome(text) {
|
|
24
|
+
return /^(Reels|Reels and short videos|Reels 和短视频|快拍|短视频|People you may know)/i.test(text);
|
|
6
25
|
}
|
|
7
26
|
|
|
8
27
|
function textOf(el) {
|
|
@@ -94,6 +113,8 @@ import{ArgumentError as m,AuthRequiredError as h,CommandExecutionError as s,Empt
|
|
|
94
113
|
&& !isActionText(text)
|
|
95
114
|
&& !isMetricText(text)
|
|
96
115
|
&& !isTimestampText(text)
|
|
116
|
+
&& !isDecoyText(text)
|
|
117
|
+
&& !/^[\\d\\s.,-]+$/.test(text) // reject all-digit decoy names, but keep "Class of 2024" (#2089)
|
|
97
118
|
&& !/\\/groups\\/|\\/watch\\/|\\/reel\\/|\\/events\\/|\\/friends\\//i.test(href)) {
|
|
98
119
|
return text;
|
|
99
120
|
}
|
|
@@ -107,6 +128,7 @@ import{ArgumentError as m,AuthRequiredError as h,CommandExecutionError as s,Empt
|
|
|
107
128
|
if (text.length <= 10) return false;
|
|
108
129
|
if (isSuggestionOrChrome(text) || isSponsored(text)) return false;
|
|
109
130
|
if (isActionText(text) || isMetricText(text) || isTimestampText(text)) return false;
|
|
131
|
+
if (isDecoyText(text) || isReelsOrCarouselChrome(text)) return false;
|
|
110
132
|
if (/^(See more|查看更多|更多)$/i.test(text)) return false;
|
|
111
133
|
return true;
|
|
112
134
|
});
|
|
@@ -148,6 +170,47 @@ import{ArgumentError as m,AuthRequiredError as h,CommandExecutionError as s,Empt
|
|
|
148
170
|
.filter((el) => textOf(el).length > 30);
|
|
149
171
|
}
|
|
150
172
|
|
|
173
|
+
// Modern Facebook no longer wraps posts in [role="article"] nor exposes the
|
|
174
|
+
// Like/Comment/Share aria-labels the fallback keyed on. Every post still
|
|
175
|
+
// carries one "Actions for this post" control (the ⋯ menu). Anchor on it and
|
|
176
|
+
// walk up to the HIGHEST ancestor that still contains exactly one such
|
|
177
|
+
// control — that bounds the node to a single post. See issue #2089.
|
|
178
|
+
function actionMenuAnchors() {
|
|
179
|
+
// Post menus only — NOT "Actions for this comment", so the anchor set,
|
|
180
|
+
// countMenus, and loadFeedPosts all key on the same thing (#2089).
|
|
181
|
+
return Array.from(document.querySelectorAll('[aria-label]')).filter((el) =>
|
|
182
|
+
/^(Actions for this post|此帖子的操作|针对此帖子的操作|贴文的操作)$/i.test(labelOf(el)));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function actionAnchoredContainers() {
|
|
186
|
+
const menus = actionMenuAnchors();
|
|
187
|
+
const seen = new WeakSet();
|
|
188
|
+
const containers = [];
|
|
189
|
+
const countMenus = (node) => {
|
|
190
|
+
let n = 0;
|
|
191
|
+
for (const el of node.querySelectorAll('[aria-label]')) {
|
|
192
|
+
if (/^(Actions for this post|此帖子的操作|针对此帖子的操作|贴文的操作)$/i.test(labelOf(el))) n += 1;
|
|
193
|
+
}
|
|
194
|
+
return n;
|
|
195
|
+
};
|
|
196
|
+
const LANDMARK_ROLES = new Set(['main', 'feed', 'banner', 'navigation', 'complementary', 'contentinfo', 'region']);
|
|
197
|
+
for (const menu of menus) {
|
|
198
|
+
let node = menu.parentElement;
|
|
199
|
+
let best = null;
|
|
200
|
+
for (let depth = 0; depth < 22 && node && node !== document.body && node !== document.documentElement; depth += 1, node = node.parentElement) {
|
|
201
|
+
// Never let a single-post page climb the container up to a page
|
|
202
|
+
// landmark (role=main/feed/...) and emit the whole region as a post.
|
|
203
|
+
if (LANDMARK_ROLES.has(node.getAttribute('role') || '')) break;
|
|
204
|
+
if (countMenus(node) === 1) best = node; else break;
|
|
205
|
+
}
|
|
206
|
+
if (best && !seen.has(best) && textOf(best).length > 30) {
|
|
207
|
+
seen.add(best);
|
|
208
|
+
containers.push(best);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return containers;
|
|
212
|
+
}
|
|
213
|
+
|
|
151
214
|
function fallbackContainers() {
|
|
152
215
|
const main = document.querySelector('[role="main"]');
|
|
153
216
|
if (!main) return [];
|
|
@@ -188,7 +251,8 @@ import{ArgumentError as m,AuthRequiredError as h,CommandExecutionError as s,Empt
|
|
|
188
251
|
if (isAuthPage()) return { status: 'auth', rows: [], diagnostics: {} };
|
|
189
252
|
|
|
190
253
|
const primary = primaryContainers();
|
|
191
|
-
const
|
|
254
|
+
const actionAnchored = actionAnchoredContainers();
|
|
255
|
+
const combined = dedupe([...primary, ...actionAnchored, ...fallbackContainers()]);
|
|
192
256
|
const rows = [];
|
|
193
257
|
for (const container of combined) {
|
|
194
258
|
const row = extractPost(container, rows.length + 1);
|
|
@@ -202,8 +266,17 @@ import{ArgumentError as m,AuthRequiredError as h,CommandExecutionError as s,Empt
|
|
|
202
266
|
diagnostics: {
|
|
203
267
|
articleCount: document.querySelectorAll('[role="article"]').length,
|
|
204
268
|
primaryCount: primary.length,
|
|
269
|
+
actionMenuCount: actionMenuAnchors().length,
|
|
205
270
|
fallbackActionCount: document.querySelectorAll('[role="main"] [aria-label="Like"], [role="main"] [aria-label="赞"], [role="main"] [aria-label="Comment"], [role="main"] [aria-label="评论"]').length,
|
|
206
271
|
mainTextLength: textOf(document.querySelector('[role="main"]')).length,
|
|
207
272
|
},
|
|
208
273
|
};
|
|
209
|
-
})()`}async function
|
|
274
|
+
})()`}async function X(f,J){const N=`(async () => {
|
|
275
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
276
|
+
window.scrollTo(0, document.body.scrollHeight);
|
|
277
|
+
await sleep(800);
|
|
278
|
+
return document.querySelectorAll('[aria-label]').length
|
|
279
|
+
? document.querySelectorAll('[role="article"]').length
|
|
280
|
+
+ Array.from(document.querySelectorAll('[aria-label]')).filter((el) => /^(Actions for this post|此帖子的操作|针对此帖子的操作|贴文的操作)$/i.test((el.getAttribute('aria-label') || '').trim())).length
|
|
281
|
+
: 0;
|
|
282
|
+
})()`;let z=-1;for(let G=0;G<8;G+=1){let D=0;try{D=Number(Y(await f.evaluate(N)))||0}catch{break}if(D>=J||D===z)break;z=D}}async function $(f,J){const N=W(J.limit??10);try{await f.goto(T,{settleMs:4000})}catch(D){throw new Q(`Failed to navigate to facebook feed: ${D instanceof Error?D.message:D}`,"Check that facebook.com is reachable and the browser extension is connected.")}await X(f,N);let z;try{z=Y(await f.evaluate(Z(N)))}catch(D){throw new Q(`Failed to read facebook feed: ${D instanceof Error?D.message:D}`,"Facebook may not have rendered or the feed markup may have changed.")}if(!z||typeof z!=="object"||!Array.isArray(z.rows))throw new Q("facebook feed returned malformed extraction payload");if(z.status==="auth")throw new P("www.facebook.com","Open Chrome and log in to Facebook before retrying.");if(z.rows.length>0)return z.rows;if(z.status==="empty")throw new U("facebook feed","Facebook did not show any feed posts for this account.");const G=z.diagnostics||{};if(G.articleCount||G.actionMenuCount||G.fallbackActionCount||G.mainTextLength>200)throw new Q("facebook feed page rendered but no feed rows could be extracted",`Diagnostics: articles=${G.articleCount||0}, actions=${G.fallbackActionCount||0}, mainTextLength=${G.mainTextLength||0}.`);throw new U("facebook feed","No Facebook feed content was visible in the current browser session.")}const H={site:"facebook",name:"feed",access:"read",description:"Get your Facebook news feed",domain:"www.facebook.com",strategy:j.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"limit",type:"int",default:10,help:"Number of posts"}],columns:["index","author","content","likes","comments","shares"],func:$};h(H);export const __test__={buildFeedExtractScript:Z,command:H,getFacebookFeed:$,requireLimit:W};
|
package/clis/facebook/search.js
CHANGED
|
@@ -1,22 +1,83 @@
|
|
|
1
|
-
import{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
1
|
+
import{ArgumentError as V,AuthRequiredError as j,CommandExecutionError as P,EmptyResultError as B}from"@jackwener/opencli/errors";import{cli as Q,Strategy as T}from"@jackwener/opencli/registry";const X="https://www.facebook.com",W=50;function Y(z){const D=Number(z);if(!Number.isInteger(D)||D<1||D>W)throw new V(`facebook search --limit must be an integer between 1 and ${W}`);return D}function Z(z){const D=String(z??"").trim();if(!D)throw new V("facebook search requires a non-empty query");return D}function F(z){if(z&&typeof z==="object"&&"data"in z)return z.data;return z}function $(z){return`(() => {
|
|
2
|
+
const limit = ${z};
|
|
3
|
+
|
|
4
|
+
function clean(value) {
|
|
5
|
+
return String(value || '')
|
|
6
|
+
.replace(/[\\u200b-\\u200f\\u202a-\\u202e\\u2060\\ufeff]/g, '')
|
|
7
|
+
.replace(/\\s+/g, ' ')
|
|
8
|
+
.trim();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// FB anti-scrape obfuscation: long spaceless digit tokens and spaced
|
|
12
|
+
// single-char strings ("a b c d e"). Real titles never look like this.
|
|
13
|
+
function isObfuscated(text) {
|
|
14
|
+
if (!text) return true;
|
|
15
|
+
if (/^\\d{8,}$/.test(text)) return true;
|
|
16
|
+
if (/^(?:\\S ){4,}\\S$/.test(text) && text.replace(/\\s/g, '').length <= 12) return true;
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isEntityHref(href) {
|
|
21
|
+
if (!href) return false;
|
|
22
|
+
let u;
|
|
23
|
+
try { u = new URL(href, 'https://www.facebook.com'); } catch (e) { return false; }
|
|
24
|
+
// drop hidden-domain .com spam — real results stay on facebook.com
|
|
25
|
+
if (!/(^|\\.)facebook\\.com$/i.test(u.hostname)) return false;
|
|
26
|
+
const p = u.pathname;
|
|
27
|
+
if (/^\\/search(\\/|$)/i.test(p)) return false; // decoy links back to search (incl. bare /search)
|
|
28
|
+
// chrome / non-result destinations that the catch-all below would keep
|
|
29
|
+
if (/^\\/(login|checkpoint|help|policies|privacy|settings|bookmarks|messages|notifications|marketplace|gaming|friends|requests|saved|me)\\b/i.test(p)) return false;
|
|
30
|
+
return /^\\/(profile\\.php|groups\\/|events\\/|watch\\/|reel\\/|pages\\/|permalink\\.php|story\\.php|[^/]+\\/posts\\/|[^/]+\\/videos\\/|[A-Za-z0-9.\\-]{2,}\\/?$)/i.test(p);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isAuthPage() {
|
|
34
|
+
const path = window.location && window.location.pathname ? window.location.pathname : '';
|
|
35
|
+
const body = clean(document.body && document.body.textContent);
|
|
36
|
+
return /^\\/(login|checkpoint)(\\/|$|\\.php)/.test(path)
|
|
37
|
+
|| /^(Log in to Facebook|Facebook登录|登录 Facebook)/i.test(body)
|
|
38
|
+
|| /You must log in to continue/i.test(body);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (isAuthPage()) return { status: 'auth', rows: [], diagnostics: {} };
|
|
42
|
+
|
|
43
|
+
const feed = document.querySelector('[role="feed"]')
|
|
44
|
+
|| document.querySelector('[role="main"]')
|
|
45
|
+
|| document.body;
|
|
46
|
+
const anchors = Array.from(feed.querySelectorAll('a[href]'));
|
|
47
|
+
const seen = new Set();
|
|
48
|
+
const rows = [];
|
|
49
|
+
for (const a of anchors) {
|
|
50
|
+
const rawHref = a.href || a.getAttribute('href') || '';
|
|
51
|
+
if (!isEntityHref(rawHref)) continue;
|
|
52
|
+
let key;
|
|
53
|
+
try { const u = new URL(rawHref, 'https://www.facebook.com'); key = u.origin + u.pathname; }
|
|
54
|
+
catch (e) { key = rawHref.split('?')[0].split('#')[0]; }
|
|
55
|
+
if (seen.has(key)) continue;
|
|
56
|
+
|
|
57
|
+
const title = clean(a.textContent).substring(0, 80);
|
|
58
|
+
if (!title || isObfuscated(title)) continue;
|
|
59
|
+
|
|
60
|
+
// climb a few levels for the surrounding card text
|
|
61
|
+
let card = a;
|
|
62
|
+
for (let i = 0; i < 4 && card.parentElement; i += 1) {
|
|
63
|
+
card = card.parentElement;
|
|
64
|
+
if (clean(card.textContent).length > title.length + 20) break;
|
|
65
|
+
}
|
|
66
|
+
const text = clean(card.textContent).substring(0, 150);
|
|
67
|
+
if (isObfuscated(text)) continue;
|
|
68
|
+
|
|
69
|
+
seen.add(key);
|
|
70
|
+
rows.push({ index: rows.length + 1, title, text, url: key });
|
|
71
|
+
if (rows.length >= limit) break;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
status: rows.length ? 'ok' : 'no_rows',
|
|
76
|
+
rows,
|
|
77
|
+
diagnostics: {
|
|
78
|
+
feedFound: !!document.querySelector('[role="feed"]'),
|
|
79
|
+
anchorCount: anchors.length,
|
|
80
|
+
mainTextLength: clean((document.querySelector('[role="main"]') || {}).textContent).length,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
})()`}async function H(z,D){const U=Z(D.query),f=Y(D.limit??10);try{await z.goto(X);await z.goto(`https://www.facebook.com/search/top?q=${encodeURIComponent(U)}`,{settleMs:4000})}catch(J){throw new P(`Failed to open facebook search: ${J instanceof Error?J.message:J}`,"Check that facebook.com is reachable and the browser extension is connected.")}let G;try{G=F(await z.evaluate($(f)))}catch(J){throw new P(`Failed to read facebook search results: ${J instanceof Error?J.message:J}`,"Facebook may not have rendered or the search markup may have changed.")}if(!G||typeof G!=="object"||!Array.isArray(G.rows))throw new P("facebook search returned malformed extraction payload");if(G.status==="auth")throw new j("www.facebook.com","Open Chrome and log in to Facebook before retrying.");if(G.rows.length>0)return G.rows;const N=G.diagnostics||{};if(N.anchorCount||N.mainTextLength>200)throw new P("facebook search page rendered but no entity results could be extracted",`Diagnostics: feed=${!!N.feedFound}, anchors=${N.anchorCount||0}, mainTextLength=${N.mainTextLength||0}.`);throw new B("facebook search",`No Facebook results were visible for "${U}".`)}const K={site:"facebook",name:"search",access:"read",description:"Search Facebook for people, pages, or posts",domain:"www.facebook.com",strategy:T.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"query",required:!0,positional:!0,help:"Search query"},{name:"limit",type:"int",default:10,help:"Number of results"}],columns:["index","title","text","url"],func:H};Q(K);export const __test__={buildSearchExtractScript:$,command:K,searchFacebook:H,requireLimit:Y,requireQuery:Z};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{ArgumentError as J}from"@jackwener/opencli/errors";import{cli as K,Strategy as Q}from"@jackwener/opencli/registry";import{HOME_URL as T,mtopRequest as V}from"./mtop.js";function F(C,D){const x=String(C??"").trim();if(!x)return 1;if(!/^\d+$/.test(x)||Number(x)<1)throw new J(`xianyu collect ${D} must be a positive integer`);return Number(x)}K({site:"xianyu",name:"collect",access:"read",description:"列出我收藏的商品(分页)",domain:"www.goofish.com",strategy:Q.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"page",type:"int",default:1,help:"页码,从 1 开始"},{name:"page_size",type:"int",default:20,help:"每页条数"}],columns:["item_id","title","price","seller_nick","want_cnt","status_str"],func:async(C,D)=>{const x=F(D.page,"page"),I=F(D.page_size,"page_size");await C.goto(T);await C.wait(1);const B=await V(C,"collect",{api:"mtop.taobao.idle.web.favor.item.list",data:{pageNumber:x,pageSize:I},version:"1.0"}),G=(Array.isArray(B.items)?B.items:Array.isArray(B.itemList)?B.itemList:Array.isArray(B.list)?B.list:[]).filter((h)=>h&&typeof h==="object").map((h)=>({item_id:String(h.itemId||h.id||""),title:h.title||"",price:String(h.price||h.soldPrice||""),main_pic:h.picUrl||h.mainPic||"",seller_id:String(h.sellerId||h.userId||""),seller_nick:h.sellerNick||h.userNick||"",want_cnt:Number(h.wantCnt||0),status_str:h.itemStatusStr||""}));return G.length?G:[{item_id:"",title:"(无收藏)",price:"",seller_nick:"",want_cnt:0,status_str:""}]}});export const __test__={normalizePage:F};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cli as D,Strategy as F}from"@jackwener/opencli/registry";import{HOME_URL as G,mtopRequest as I}from"./mtop.js";D({site:"xianyu",name:"location",access:"read",description:"获取账号默认发布地址",domain:"www.goofish.com",strategy:F.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"longitude",type:"float",default:121.4737,help:"经度(默认上海)"},{name:"latitude",type:"float",default:31.2304,help:"纬度(默认上海)"}],columns:["prov","city","area","poi","division_id"],func:async(h,z)=>{const B=Number(z.longitude??121.4737),C=Number(z.latitude??31.2304);await h.goto(G);await h.wait(1);const x=await I(h,"location",{api:"mtop.taobao.idle.local.poi.get",data:{longitude:B,latitude:C},version:"1.0"}),A=Array.isArray(x.commonAddresses)?x.commonAddresses:[],f=x.selectedPoi||(A.length?A[0]:null);if(!f)return[{prov:"",city:"",area:"",poi:"",division_id:""}];return[{prov:f.prov||"",city:f.city||"",area:f.area||"",poi:f.poi||"",division_id:String(f.divisionId||"")}]}});export const __test__={};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{ArgumentError as P,CommandExecutionError as j}from"@jackwener/opencli/errors";import{cli as Z,Strategy as $}from"@jackwener/opencli/registry";import{HOME_URL as f,mtopRequest as W}from"./mtop.js";import{normalizeNumericId as A}from"./utils.js";function C(F,B){const G=String(F??"").trim();if(!G)return 1;if(!/^\d+$/.test(G)||Number(G)<1)throw new P(`xianyu ${B} must be a positive integer`);return Number(G)}function H(F){const B=F.cardData||{},G=B.priceInfo||{},J=B.picInfo||{},K=B.detailParams||{};return{item_id:String(B.id||K.itemId||""),title:B.title||K.title||"",price:G.price||K.soldPrice||"",status:B.itemStatus,main_pic:J.picUrl||K.picUrl||"",post_info:K.postInfo||""}}Z({site:"xianyu",name:"mine",access:"read",description:"列出当前账号在售/已发布商品(分页)",domain:"www.goofish.com",strategy:$.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"page",type:"int",default:1,help:"页码,从 1 开始"},{name:"page_size",type:"int",default:20,help:"每页条数"}],columns:["item_id","title","price","status","main_pic"],func:async(F,B)=>{const G=C(B.page,"page"),J=C(B.page_size,"page_size");await F.goto(f);await F.wait(1);const K=await W(F,"item mine",{api:"mtop.idle.web.user.page.head",data:{self:!0},version:"1.0"}),Q=String((K.base||K.baseInfo||{}).kcUserId||"").trim();if(!Q)throw new j("无法解析当前账号 userId,请确认已登录闲鱼");const V=await W(F,"item mine",{api:"mtop.idle.web.xyh.item.list",data:{userId:Q,pageNumber:G,pageSize:J},version:"1.0"}),X=(Array.isArray(V.cardList)?V.cardList:Array.isArray(V.itemList)?V.itemList:[]).filter((Y)=>Y&&typeof Y==="object").map(H);return X.length?X:[{item_id:"",title:"(无在售商品)",price:"",status:"",main_pic:""}]}});Z({site:"xianyu",name:"polish",access:"write",description:"擦亮商品(刷新曝光排序,不改内容)",domain:"www.goofish.com",strategy:$.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"item_id",required:!0,positional:!0,help:"商品 itemId"}],columns:["item_id","ok","already_polished"],func:async(F,B)=>{const G=A(B.item_id,"item_id","1017530128619");await F.goto(f);await F.wait(1);let J;try{J=await W(F,"item polish",{api:"mtop.taobao.idle.item.polish",data:{itemId:G},version:"1.0"})}catch(K){const Q=String(K?.message||K);if(/POLISH_AGAIN|已经擦亮/.test(Q))return[{item_id:G,ok:!0,already_polished:!0,note:"今天已擦亮过,明天再来"}];throw K}return[{item_id:G,ok:!0,already_polished:!1,data:J}]}});Z({site:"xianyu",name:"delete",access:"write",description:"下架/删除商品",domain:"www.goofish.com",strategy:$.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"item_id",required:!0,positional:!0,help:"商品 itemId"}],columns:["item_id","ok"],func:async(F,B)=>{const G=A(B.item_id,"item_id","1017530128619");await F.goto(f);await F.wait(1);const J=await W(F,"item delete",{api:"com.taobao.idle.item.delete",data:{itemId:G},version:"1.1"});return[{item_id:G,ok:!0,data:J}]}});Z({site:"xianyu",name:"edit",access:"write",description:"编辑商品(标题/详情/价格/划线价/库存,按需传)",domain:"www.goofish.com",strategy:$.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"item_id",required:!0,positional:!0,help:"商品 itemId"},{name:"title",help:"新标题"},{name:"desc",help:"新详情描述"},{name:"price",type:"float",help:"新价格(元)"},{name:"original_price",type:"float",help:"新划线价(元),传 0 清除"},{name:"quantity",type:"int",help:"新库存数量"}],columns:["item_id","ok"],func:async(F,B)=>{const G=A(B.item_id,"item_id","1017530128619"),J=(T)=>T!=null&&String(T).trim()!=="";if(![B.title,B.desc,B.price,B.original_price,B.quantity].some(J))throw new P("至少传一个待改字段:--title / --desc / --price / --original-price / --quantity");await F.goto(f);await F.wait(1);const K=await W(F,"item edit",{api:"mtop.idle.pc.idleitem.editDetail",data:{itemId:G},version:"1.0"});if(!K||!Object.keys(K).length)throw new j(`editDetail 返回为空:itemId=${G}(可能已下架 / 不属于当前账号 / 风控)`);const Q={...K,itemId:G};if(J(B.title)||J(B.desc)){const T={...K.itemTextDTO||{}};if(J(B.title))T.title=String(B.title);if(J(B.desc))T.desc=String(B.desc);if(T.titleDescSeparate==null)T.titleDescSeparate=!0;Q.itemTextDTO=T}if(J(B.price)||J(B.original_price)){const T={...K.itemPriceDTO||{}},X=Number(B.price),Y=Number(B.original_price);if(J(B.price)&&X>0)T.priceInCent=String(Math.round(X*100));if(J(B.original_price))if(Y>0)T.origPriceInCent=String(Math.round(Y*100));else delete T.origPriceInCent;Q.itemPriceDTO=T}if(J(B.quantity))Q.quantity=String(Number(B.quantity));if(Q.uniqueCode==null)Q.uniqueCode=String(Date.now()*1000);if(Q.sourceId==null)Q.sourceId="pcMainPublish";if(Q.bizcode==null)Q.bizcode="pcMainPublish";if(Q.publishScene==null)Q.publishScene="pcMainPublish";const V=await W(F,"item edit",{api:"mtop.idle.pc.idleitem.edit",data:Q,version:"1.0"});return[{item_id:G,ok:!0,data:V}]}});export const __test__={normalizeMyItem:H,normalizePage:C};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import{AuthRequiredError as P,CommandExecutionError as $,EmptyResultError as N,selectorError as j}from"@jackwener/opencli/errors";export const HOME_URL="https://www.goofish.com";const Q=["RGV587_ERROR","FAIL_SYS_USER_VALIDATE","哎哟喂","/punish"],V=["FAIL_SYS_SESSION_EXPIRED","FAIL_SYS_TOKEN_EXOIRED","FAIL_SYS_TOKEN_EMPTY","FAIL_SYS_ILLEGAL_ACCESS","令牌过期","NEED_LOGIN"];function X(B,z,F){return`
|
|
2
|
+
(async () => {
|
|
3
|
+
const clean = (v) => String(v ?? '').replace(/\\s+/g, ' ').trim();
|
|
4
|
+
const extractRetCode = (ret) => clean(Array.isArray(ret) ? ret[0] : '').split('::')[0] || '';
|
|
5
|
+
const waitFor = async (pred, t = 6000) => {
|
|
6
|
+
const s = Date.now();
|
|
7
|
+
while (Date.now() - s < t) { if (pred()) return true; await new Promise((r) => setTimeout(r, 150)); }
|
|
8
|
+
return false;
|
|
9
|
+
};
|
|
10
|
+
const bodyText = document.body?.innerText || '';
|
|
11
|
+
if (/请先登录|扫码登录|登录后/.test(bodyText)) return { error: 'auth-required' };
|
|
12
|
+
if (/验证码|安全验证|异常访问/.test(bodyText)) return { error: 'blocked' };
|
|
13
|
+
await waitFor(() => window.lib?.mtop?.request);
|
|
14
|
+
if (typeof window.lib?.mtop?.request !== 'function') return { error: 'mtop-not-ready' };
|
|
15
|
+
let response;
|
|
16
|
+
try {
|
|
17
|
+
response = await window.lib.mtop.request({
|
|
18
|
+
api: ${JSON.stringify(B)},
|
|
19
|
+
data: ${JSON.stringify(z)},
|
|
20
|
+
type: 'POST',
|
|
21
|
+
v: ${JSON.stringify(String(F))},
|
|
22
|
+
dataType: 'json',
|
|
23
|
+
needLogin: false,
|
|
24
|
+
needLoginPC: false,
|
|
25
|
+
sessionOption: 'AutoLoginOnly',
|
|
26
|
+
ecode: 0,
|
|
27
|
+
});
|
|
28
|
+
} catch (error) {
|
|
29
|
+
const ret = error?.ret || [];
|
|
30
|
+
return {
|
|
31
|
+
error: 'mtop-request-failed',
|
|
32
|
+
error_code: extractRetCode(ret),
|
|
33
|
+
error_message: clean(Array.isArray(ret) ? ret.join(' | ') : error?.message || error),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const retCode = extractRetCode(response?.ret || []);
|
|
37
|
+
if (retCode && retCode !== 'SUCCESS') {
|
|
38
|
+
return {
|
|
39
|
+
error: 'mtop-response-error',
|
|
40
|
+
error_code: retCode,
|
|
41
|
+
error_message: clean((response?.ret || []).join(' | ')),
|
|
42
|
+
ret: response?.ret || [],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return { ok: true, ret: response?.ret || [], data: response?.data || {} };
|
|
46
|
+
})()
|
|
47
|
+
`}function Z(B,z){if(z?.error==="auth-required")throw new P("www.goofish.com",`闲鱼 ${B} 需要已登录的浏览器会话`);if(z?.error==="blocked")throw new N(`xianyu ${B}`,"闲鱼页面被验证码 / 安全验证拦截,请人工处理后重试");if(z?.error==="mtop-not-ready")throw j("window.lib.mtop","闲鱼页面未完成初始化,无法调用 mtop 接口");const F=String(z?.error_code||""),J=String(z?.error_message||""),L=`${F} ${J}`;if(V.some((G)=>L.includes(G)))throw new P("www.goofish.com",`闲鱼 ${B} 登录态失效:${J||F}`);if(Q.some((G)=>L.includes(G)))throw new N(`xianyu ${B}`,`闲鱼 ${B} 触发风控:${J||F},请人工处理`);if(z?.error)throw new $(`闲鱼 ${B} 接口调用失败:${J||F||z.error}`)}export async function mtopRequest(B,z,{api:F,data:J,version:L="1.0"}){const G=await B.evaluate(X(F,J,L));Z(z,G);if(!G||typeof G!=="object")throw new N(`xianyu ${z}`,"闲鱼接口未返回有效数据");return G.data||{}}export function retOk(B){return Array.isArray(B)&&B.some((z)=>String(z).includes("SUCCESS"))}export const __test__={buildMtopEvaluate:X,classifyAndThrow:Z,RISK_KEYWORDS:Q,AUTH_KEYWORDS:V};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{ArgumentError as M}from"@jackwener/opencli/errors";import{cli as $,Strategy as C}from"@jackwener/opencli/registry";import{HOME_URL as h,mtopRequest as f}from"./mtop.js";import{normalizeNumericId as Y}from"./utils.js";const j=["ALL","WAITING_PAY","WAITING_SEND","WAITING_RECEIVE","WAITING_RATE","SUCCESS","REFUND"];function q(x){const B=String(x||"ALL").trim().toUpperCase();if(!j.includes(B))throw new M(`xianyu order --status must be one of: ${j.join(", ")}`);return B}function L(x,B){const F=String(x??"").trim();if(!F)return 1;if(!/^\d+$/.test(F)||Number(F)<1)throw new M(`xianyu order ${B} must be a positive integer`);return Number(F)}$({site:"xianyu",name:"order",access:"read",description:"列出卖家已售订单(可按状态筛选)",domain:"www.goofish.com",strategy:C.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"page",type:"int",default:1,help:"页码,从 1 开始"},{name:"page_size",type:"int",default:20,help:"每页条数"},{name:"status",default:"ALL",help:`订单状态筛选:${j.join(" / ")}`}],columns:["order_id","item_id","buyer_nick","status_str","total_price","create_time"],func:async(x,B)=>{const F=L(B.page,"page"),Q=L(B.page_size,"page_size"),J=q(B.status);await x.goto(h);await x.wait(1);const X=(await f(x,"order list",{api:"mtop.taobao.idle.trade.merchant.sold.get",data:{pageNumber:F,rowsPerPage:Q,orderIds:"",queryCode:J,orderSearchParam:"{}"},version:"1.0"})).module||{},W=(Array.isArray(X.items)?X.items:[]).map((K)=>{const G=K.order||K.orderInfo||K,Z=K.buyer||K.buyerInfo||{},H=K.item||K.itemInfo||{};return{order_id:String(G.orderId||G.bizOrderId||G.id||""),item_id:String(H.itemId||H.id||""),item_title:H.title||"",buyer_id:String(Z.userId||Z.buyerId||""),buyer_nick:Z.nick||Z.userNick||"",status:G.status??G.orderStatus??"",status_str:G.statusStr||G.orderStatusStr||"",total_price:String(G.totalPrice||G.actualPrice||""),create_time:String(G.createTime||G.gmtCreate||"")}});return W.length?W:[{order_id:"",status_str:"(无已售订单)",total_price:"",buyer_nick:"",item_id:"",create_time:""}]}});$({site:"xianyu",name:"order-get",access:"read",description:"查询单个闲鱼订单详情",domain:"www.goofish.com",strategy:C.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"order_id",required:!0,positional:!0,help:"订单号 orderId"},{name:"role",default:"seller",help:"视角:seller / buyer"}],columns:["order_id","item_id","status_str","total_price","buyer_nick"],func:async(x,B)=>{const F=Y(B.order_id,"order_id","2800000000000000000"),Q=String(B.role||"seller").trim()==="buyer"?"buyer":"seller";await x.goto(h);await x.wait(1);const J=await f(x,"order get",{api:"mtop.idle.web.trade.order.detail",data:{orderId:F,role:Q},version:"1.0"}),V=J.itemInfoDTO||{},X=J.buyerInfoDTO||{},D=J.sellerInfoDTO||{},W=J.priceInfoDTO||{},K=J.logisticsInfoDTO||{},G=J.statusInfoDTO||{};return[{order_id:F,role:Q,item_id:String(V.itemId||""),title:V.title||"",main_pic:V.picUrl||"",quantity:Number(V.quantity||1),buyer_id:String(X.buyerId||""),buyer_nick:X.userNick||"",seller_id:String(D.sellerId||""),seller_nick:D.userNick||"",total_price:String(W.totalPrice||""),auction_price:String(W.auctionPrice||""),post_fee:String(W.postFee||""),status:G.orderStatus,status_str:G.orderStatusStr||"",logistic_no:K.logisticsNo||"",logistic_company:K.logisticsCompany||""}]}});$({site:"xianyu",name:"order-ship-dummy",access:"write",description:"虚拟发货(仅虚拟商品,无需物流单号)",domain:"www.goofish.com",strategy:C.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"order_id",required:!0,positional:!0,help:"订单号 orderId"},{name:"text",default:"",help:"发货附言(可选)"}],columns:["order_id","ok"],func:async(x,B)=>{const F=Y(B.order_id,"order_id","2800000000000000000");await x.goto(h);await x.wait(1);const Q=await f(x,"order ship-dummy",{api:"mtop.taobao.idle.logistic.consign.dummy",data:{orderId:F,tradeText:String(B.text||""),picList:[],newUnconsign:!0},version:"1.0"});return[{order_id:F,ok:!0,data:Q}]}});$({site:"xianyu",name:"order-ship-freeshipping",access:"write",description:"包邮发货(拼团活动专用)",domain:"www.goofish.com",strategy:C.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"order_id",required:!0,positional:!0,help:"订单号 bizOrderId"},{name:"item_id",required:!0,help:"商品 itemId"},{name:"buyer_id",required:!0,help:"买家 userId"}],columns:["order_id","ok"],func:async(x,B)=>{const F=Y(B.order_id,"order_id","2800000000000000000"),Q=Y(B.item_id,"item_id","1017530128619"),J=Y(B.buyer_id,"buyer_id","2208502750464");await x.goto(h);await x.wait(1);const V=await f(x,"order ship-freeshipping",{api:"mtop.idle.groupon.activity.seller.freeshipping",data:{bizOrderId:F,itemId:Number(Q),buyerId:Number(J)},version:"1.0"});return[{order_id:F,ok:!0,data:V}]}});export const __test__={normalizeStatus:q,normalizePage:L,ORDER_STATUS:j};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{ArgumentError as h}from"@jackwener/opencli/errors";import{cli as Q,Strategy as V}from"@jackwener/opencli/registry";import{HOME_URL as W,mtopRequest as X}from"./mtop.js";import{normalizeNumericId as Y}from"./utils.js";Q({site:"xianyu",name:"rate",access:"read",description:"拉某用户收到的信用评价列表",domain:"www.goofish.com",strategy:V.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"user_id",required:!0,positional:!0,help:"被评价用户 userId(明文数字 uid)"},{name:"page",type:"int",default:1,help:"页码,从 1 开始"},{name:"page_size",type:"int",default:20,help:"每页条数"}],columns:["rate_id","rater_nick","rate_content","create_time"],func:async(A,B)=>{const G=Y(B.user_id,"user_id","2208502750464"),J=Math.max(1,Number(B.page)||1),K=Math.max(1,Number(B.page_size)||20);await A.goto(W);await A.wait(1);const C=await X(A,"rate",{api:"mtop.idle.web.trade.rate.list",data:{rateType:0,ratedUid:G,raterType:0,rowsPerPage:K,pageNumber:J,foldFlag:0,fishAdCode:"",extraTag:""},version:"1.0"}),D=(Array.isArray(C.cardList)?C.cardList:[]).filter((x)=>x&&typeof x==="object").map((x)=>{const v=x.cardData||x;return{rate_id:String(v.rateId||v.id||""),rater_id:String(v.raterUid||v.userId||""),rater_nick:v.raterNick||v.userNick||v.nick||"",rate_content:v.content||v.rateContent||"",rate_type:v.rateType??v.attitude??"",item_id:String(v.itemId||""),item_title:v.itemTitle||"",create_time:String(v.gmtCreate||v.createTime||v.rateTime||""),reply:v.reply||v.sellerReply||""}});return D.length?D:[{rate_id:"",rater_nick:"(该用户暂无评价或评价不可见)",rate_content:"",create_time:""}]}});export const __test__={};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cli as P,Strategy as Q}from"@jackwener/opencli/registry";import{HOME_URL as V,mtopRequest as W}from"./mtop.js";import{normalizeNumericId as X}from"./utils.js";P({site:"xianyu",name:"user",access:"read",description:"查询闲鱼用户主页信息(不传 user_id 看自己)",domain:"www.goofish.com",strategy:Q.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"user_id",positional:!0,help:"目标用户 userId;留空看自己"}],columns:["user_id","nick","city","credit_level","fans_count","follow_count"],func:async(A,B)=>{const G=B.user_id!=null&&String(B.user_id).trim()!=="",J=G?X(B.user_id,"user_id","2208502750464"):"";await A.goto(V);await A.wait(1);const C=await W(A,"user",{api:"mtop.idle.web.user.page.head",data:G?{userId:J,self:!1}:{self:!0},version:"1.0"}),x=C.baseInfo||C.base||{},D=C.module||{},v=D.base||{},F=D.social||{},K=(D.tabs||{}).rate||{};return[{user_id:String(x.kcUserId||x.userId||J||""),nick:v.displayName||x.userNick||"",avatar:v.avatar&&v.avatar.avatar||v.avatar||"",location:v.ipLocation||"",signature:v.introduction||"",followers:Number(F.followers||0),following:Number(F.following||0),follow_status:F.followStatus??"",rate_count:Number(K.number||0),tags:x.tags||{}}]}});export const __test__={};
|
|
@@ -18,6 +18,25 @@ export interface ResolveSuccess {
|
|
|
18
18
|
* clean `exact` match — the page changed, the action still succeeded.
|
|
19
19
|
*/
|
|
20
20
|
match_level: TargetMatchLevel;
|
|
21
|
+
/**
|
|
22
|
+
* Which physical click path executed: `cdp` (trusted Input.dispatchMouseEvent),
|
|
23
|
+
* `js` (el.click() fallback), or `ax` (accessibility-node click). Surfaced so
|
|
24
|
+
* agents can tell a trusted synthetic click from the untrusted JS fallback.
|
|
25
|
+
*/
|
|
26
|
+
click_method?: 'cdp' | 'js' | 'ax';
|
|
27
|
+
/**
|
|
28
|
+
* Result of hit-testing the click point against the resolved element:
|
|
29
|
+
* `target` (the point lands on the element or a descendant — trustworthy),
|
|
30
|
+
* `other` (an overlay/sibling covers it — the CDP click would miss), or
|
|
31
|
+
* `none` (nothing at the point). Only set on click().
|
|
32
|
+
*/
|
|
33
|
+
hit?: 'target' | 'ancestor' | 'other' | 'none';
|
|
34
|
+
/**
|
|
35
|
+
* True when the click was retargeted from the resolved element to a nearby
|
|
36
|
+
* clickable ancestor (e.g. an <svg> icon whose click handler lives on the
|
|
37
|
+
* wrapping <div>), so the handler actually fires. See issue #2071.
|
|
38
|
+
*/
|
|
39
|
+
retargeted?: boolean;
|
|
21
40
|
}
|
|
22
41
|
export interface FillTextResult extends ResolveSuccess {
|
|
23
42
|
filled: boolean;
|
|
@@ -44,7 +44,7 @@ ${G}
|
|
|
44
44
|
clearTimeout(timer);
|
|
45
45
|
}
|
|
46
46
|
})()
|
|
47
|
-
`,{request:W}),Z=Q.url||G;if(Q.error)throw new M("FETCH_ERROR",`Browser fetch failed for ${Z}: ${Q.error}`,"Check that the page is reachable and the current browser profile has access.");if(!Q.ok)throw new M("FETCH_ERROR",`HTTP ${Q.status??0}${Q.statusText?` ${Q.statusText}`:""} from ${Z}`,A(Q.text));const _=Q.text??"";if(!_.trim())return null;try{return JSON.parse(_)}catch{const $=Q.contentType?` (${Q.contentType})`:"";throw new M("FETCH_ERROR",`Expected JSON from ${Z}${$}`,A(_))}}async annotatedScreenshot(G={}){await this.snapshot({source:"dom",viewportExpand:0});try{await this.evaluate(m());return await this.screenshot({...G,annotate:!1})}finally{await this.evaluate(u()).catch(()=>{})}}async click(G,H={}){const W=await this.tryClickAxRef(G);if(W)return W;const Q=await Y(this,G,H),Z=await this.tryCdpOnResolvedElement("DOM.scrollIntoViewIfNeeded"),_=await this.evaluate(P({skipScroll:Z}));if(_?.visible===!0){if(await this.tryNativeClick(_.x,_.y))return
|
|
47
|
+
`,{request:W}),Z=Q.url||G;if(Q.error)throw new M("FETCH_ERROR",`Browser fetch failed for ${Z}: ${Q.error}`,"Check that the page is reachable and the current browser profile has access.");if(!Q.ok)throw new M("FETCH_ERROR",`HTTP ${Q.status??0}${Q.statusText?` ${Q.statusText}`:""} from ${Z}`,A(Q.text));const _=Q.text??"";if(!_.trim())return null;try{return JSON.parse(_)}catch{const $=Q.contentType?` (${Q.contentType})`:"";throw new M("FETCH_ERROR",`Expected JSON from ${Z}${$}`,A(_))}}async annotatedScreenshot(G={}){await this.snapshot({source:"dom",viewportExpand:0});try{await this.evaluate(m());return await this.screenshot({...G,annotate:!1})}finally{await this.evaluate(u()).catch(()=>{})}}async click(G,H={}){const W=await this.tryClickAxRef(G);if(W)return W;const Q=await Y(this,G,H),Z=await this.tryCdpOnResolvedElement("DOM.scrollIntoViewIfNeeded"),_=await this.evaluate(P({skipScroll:Z,forClick:!0})),$={hit:_?.hit,retargeted:_?.retargeted};if(_?.visible===!0&&(_.hit==="target"||_.hit==="ancestor")){if(await this.tryNativeClick(_.x,_.y))return{...Q,click_method:"cdp",...$}}const D=await this.evaluate(x({skipScroll:Z}));if(typeof D==="string"||D==null)return{...Q,click_method:"js",...$};if(D.status==="clicked")return{...Q,click_method:"js",...$};if(D.x!=null&&D.y!=null){if(await this.tryNativeClick(D.x,D.y))return{...Q,click_method:"cdp",...$}}throw Error(`Click failed: ${D.error??"JS click and CDP fallback both failed"}`)}async tryNativeClick(G,H){const W=this.nativeClick;if(typeof W!=="function")return!1;try{await W.call(this,G,H);return!0}catch{return!1}}async tryNativeMouseMove(G,H){const W=this.cdp;if(typeof W!=="function")return!1;try{await W.call(this,"Input.dispatchMouseEvent",{type:"mouseMoved",x:G,y:H});return!0}catch{return!1}}async tryNativeDoubleClick(G,H){const W=this.cdp;if(typeof W!=="function")return!1;try{await W.call(this,"Input.dispatchMouseEvent",{type:"mouseMoved",x:G,y:H});await W.call(this,"Input.dispatchMouseEvent",{type:"mousePressed",x:G,y:H,button:"left",clickCount:1});await W.call(this,"Input.dispatchMouseEvent",{type:"mouseReleased",x:G,y:H,button:"left",clickCount:1});await W.call(this,"Input.dispatchMouseEvent",{type:"mousePressed",x:G,y:H,button:"left",clickCount:2});await W.call(this,"Input.dispatchMouseEvent",{type:"mouseReleased",x:G,y:H,button:"left",clickCount:2});return!0}catch{return!1}}async tryNativeDrag(G,H){const W=this.cdp;if(typeof W!=="function")return!1;const Q=Math.round((G.x+H.x)/2),Z=Math.round((G.y+H.y)/2);try{await W.call(this,"Input.dispatchMouseEvent",{type:"mouseMoved",x:G.x,y:G.y});await W.call(this,"Input.dispatchMouseEvent",{type:"mousePressed",x:G.x,y:G.y,button:"left",clickCount:1});await W.call(this,"Input.dispatchMouseEvent",{type:"mouseMoved",x:Q,y:Z,button:"left",buttons:1});await W.call(this,"Input.dispatchMouseEvent",{type:"mouseMoved",x:H.x,y:H.y,button:"left",buttons:1});await W.call(this,"Input.dispatchMouseEvent",{type:"mouseReleased",x:H.x,y:H.y,button:"left",clickCount:1});return!0}catch{return!1}}async tryClickAxRef(G){if(!/^\d+$/.test(G))return null;const H=this._axRefs.get(G);if(!H)return null;const W=this.nativeClick;if(typeof W!=="function")return null;const Q=await this.resolveAxRefPoint(H);if(!Q)return null;try{await W.call(this,Q.x,Q.y);return{matches_n:1,match_level:Q.matchLevel,click_method:"ax"}}catch{return null}}async resolveAxRefPoint(G){const H=this.cdp;if(typeof H!=="function")return null;if(G.backendNodeId!=null){const _=await this.axBoxCenter(G.backendNodeId,G.frame).catch(()=>null);if(_)return{..._,matchLevel:"exact"}}await H.call(this,"Accessibility.enable",C(G.frame));const W=await H.call(this,"Accessibility.getFullAXTree",j(G.frame)).catch(()=>null),Q=w(W,G);if(!Q?.backendNodeId)return null;this._axRefs.set(G.ref,Q);const Z=await this.axBoxCenter(Q.backendNodeId,Q.frame).catch(()=>null);return Z?{...Z,matchLevel:"reidentified"}:null}async axBoxCenter(G,H){const W=this.cdp;if(typeof W!=="function")return null;const Q=await W.call(this,"DOM.getBoxModel",{backendNodeId:G,...H?.sessionId?{frameId:H.frameId,sessionId:H.sessionId,...H.targetUrl?{targetUrl:H.targetUrl}:{}}:{}}),Z=Array.isArray(Q?.model?.content)&&Q.model.content.length>=8?Q.model.content:Array.isArray(Q?.model?.border)&&Q.model.border.length>=8?Q.model.border:null;if(!Z)return null;const _=Z.slice(0,8).map(($)=>typeof $==="number"?$:Number($));if(_.some(($)=>!Number.isFinite($)))return null;return{x:Math.round((_[0]+_[2]+_[4]+_[6])/4),y:Math.round((_[1]+_[3]+_[5]+_[7])/4)}}async tryNativeType(G){const H=this.nativeType;if(typeof H==="function")try{await H.call(this,G);return!0}catch{}const W=this.insertText;if(typeof W!=="function")return!1;try{await W.call(this,G);return!0}catch{return!1}}async tryNativeKeyPress(G,H){const W=this.nativeKeyPress;if(typeof W!=="function")return!1;try{await W.call(this,G,H);return!0}catch{return!1}}async isResolvedFocused(){try{return await this.evaluate(`
|
|
48
48
|
(() => {
|
|
49
49
|
const el = window.__resolved;
|
|
50
50
|
return !!el && (document.activeElement === el || (typeof el.matches === 'function' && el.matches(':focus')));
|
|
@@ -77,6 +77,7 @@ export declare function resolveTargetJs(ref: string, opts?: ResolveOptions): str
|
|
|
77
77
|
*/
|
|
78
78
|
export declare function boundingRectResolvedJs(opts?: {
|
|
79
79
|
skipScroll?: boolean;
|
|
80
|
+
forClick?: boolean;
|
|
80
81
|
}): string;
|
|
81
82
|
/**
|
|
82
83
|
* Generate JS for click that uses the unified resolver.
|
|
@@ -215,18 +215,112 @@ export function resolveTargetJs(j,g={}){const q=JSON.stringify(j),w=g.nth!==void
|
|
|
215
215
|
return { ok: true, matches_n: matches.length, match_level: 'exact' };
|
|
216
216
|
}
|
|
217
217
|
})()
|
|
218
|
-
`}export function boundingRectResolvedJs(j={}){return`
|
|
218
|
+
`}export function boundingRectResolvedJs(j={}){const g=j.skipScroll?"false":"true",q=j.forClick?"true":"false";return`
|
|
219
219
|
(() => {
|
|
220
220
|
const el = window.__resolved;
|
|
221
221
|
if (!el) throw new Error('No resolved element');
|
|
222
|
-
if (${
|
|
223
|
-
|
|
222
|
+
if (${g}) el.scrollIntoView({ behavior: 'instant', block: 'center' });
|
|
223
|
+
|
|
224
|
+
const FOR_CLICK = ${q};
|
|
225
|
+
// hover()/dblClick() want the plain element centre — the retarget + hit-test
|
|
226
|
+
// below are click-only so those actions keep their original behaviour.
|
|
227
|
+
if (!FOR_CLICK) {
|
|
228
|
+
const r0 = el.getBoundingClientRect();
|
|
229
|
+
return {
|
|
230
|
+
x: Math.round(r0.left + r0.width / 2),
|
|
231
|
+
y: Math.round(r0.top + r0.height / 2),
|
|
232
|
+
w: Math.round(r0.width),
|
|
233
|
+
h: Math.round(r0.height),
|
|
234
|
+
visible: Math.round(r0.width) > 0 && Math.round(r0.height) > 0,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Does this node OWN a click handler? Deliberately excludes cursor:pointer,
|
|
239
|
+
// which is an *inherited* CSS property — an <svg> icon inside a clickable
|
|
240
|
+
// <div> inherits the pointer cursor but owns no handler, so cursor can't
|
|
241
|
+
// decide whether a node is the real click target. See issue #2071.
|
|
242
|
+
const ownsClickHandler = (node) => {
|
|
243
|
+
if (!node || node.nodeType !== 1) return false;
|
|
244
|
+
const tag = node.tagName.toLowerCase();
|
|
245
|
+
if (['a','button','input','select','textarea','label','summary'].includes(tag)) return true;
|
|
246
|
+
const role = node.getAttribute && node.getAttribute('role');
|
|
247
|
+
if (role && ['button','link','menuitem','menuitemcheckbox','menuitemradio','tab','option','checkbox','radio','switch'].includes(role)) return true;
|
|
248
|
+
if (typeof node.onclick === 'function') return true;
|
|
249
|
+
if (node.hasAttribute && (node.hasAttribute('onclick') || node.hasAttribute('jsaction'))) return true;
|
|
250
|
+
try {
|
|
251
|
+
for (const k in node) {
|
|
252
|
+
if (k.charCodeAt(0) === 95 && (k.indexOf('__reactProps$') === 0 || k.indexOf('__reactEventHandlers$') === 0)) {
|
|
253
|
+
const p = node[k];
|
|
254
|
+
if (p && (p.onClick || p.onMouseDown || p.onMouseUp)) return true;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
} catch (e) {}
|
|
258
|
+
return false;
|
|
259
|
+
};
|
|
260
|
+
// A retarget destination just needs to look clickable; here cursor:pointer
|
|
261
|
+
// IS a useful signal (the ancestor is where it was set).
|
|
262
|
+
const isClickableAncestor = (node) => {
|
|
263
|
+
if (ownsClickHandler(node)) return true;
|
|
264
|
+
try { return window.getComputedStyle(node).cursor === 'pointer'; } catch (e) { return false; }
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// #2071: if the resolved node owns no click handler but a nearby ancestor
|
|
268
|
+
// is clickable, aim at that ancestor so the handler fires.
|
|
269
|
+
let target = el;
|
|
270
|
+
let retargeted = false;
|
|
271
|
+
if (!ownsClickHandler(el)) {
|
|
272
|
+
let a = el.parentElement, hops = 0;
|
|
273
|
+
while (a && hops < 4) {
|
|
274
|
+
if (isClickableAncestor(a)) { target = a; retargeted = true; break; }
|
|
275
|
+
a = a.parentElement; hops++;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const rect = target.getBoundingClientRect();
|
|
224
280
|
const w = Math.round(rect.width);
|
|
225
281
|
const h = Math.round(rect.height);
|
|
226
|
-
|
|
227
|
-
|
|
282
|
+
let x = Math.round(rect.left + rect.width / 2);
|
|
283
|
+
let y = Math.round(rect.top + rect.height / 2);
|
|
228
284
|
const visible = w > 0 && h > 0;
|
|
229
|
-
|
|
285
|
+
|
|
286
|
+
// #2076: verify the click point actually lands on the target. A trusted
|
|
287
|
+
// CDP click hit-tests at (x,y) and delivers the event to whatever is
|
|
288
|
+
// topmost there — an overlay/sibling can silently swallow it. Classify:
|
|
289
|
+
// 'target' — the point is the target or a light-DOM descendant.
|
|
290
|
+
// 'ancestor' — the point is an ANCESTOR of the target. This is the
|
|
291
|
+
// open-shadow-DOM case (elementFromPoint returns the shadow
|
|
292
|
+
// *host*, not the shadow content) and the "point sits over
|
|
293
|
+
// the target's own wrapper background" case. In both a CDP
|
|
294
|
+
// click at (x,y) still reaches the target — CDP hit-testing
|
|
295
|
+
// pierces shadow roots, and light-DOM clicks bubble up — so
|
|
296
|
+
// it is trustworthy, unlike an unrelated overlay.
|
|
297
|
+
// 'other' — an unrelated element covers the point (the real overlay
|
|
298
|
+
// bug); the caller then dispatches a direct DOM click.
|
|
299
|
+
const hitClass = (px, py) => {
|
|
300
|
+
let at = null;
|
|
301
|
+
try { at = document.elementFromPoint(px, py); } catch (e) { return 'none'; }
|
|
302
|
+
if (!at) return 'none';
|
|
303
|
+
if (at === target || target.contains(at)) return 'target';
|
|
304
|
+
if (at.contains && at.contains(target)) return 'ancestor';
|
|
305
|
+
return 'other';
|
|
306
|
+
};
|
|
307
|
+
let hit = visible ? hitClass(x, y) : 'none';
|
|
308
|
+
if (visible && hit !== 'target' && hit !== 'ancestor') {
|
|
309
|
+
const cands = [
|
|
310
|
+
[rect.left + rect.width * 0.5, rect.top + rect.height * 0.25],
|
|
311
|
+
[rect.left + rect.width * 0.25, rect.top + rect.height * 0.5],
|
|
312
|
+
[rect.left + rect.width * 0.75, rect.top + rect.height * 0.5],
|
|
313
|
+
[rect.left + rect.width * 0.5, rect.top + rect.height * 0.75],
|
|
314
|
+
[rect.left + 3, rect.top + 3],
|
|
315
|
+
[rect.right - 3, rect.bottom - 3],
|
|
316
|
+
];
|
|
317
|
+
for (const c of cands) {
|
|
318
|
+
const px = Math.round(c[0]), py = Math.round(c[1]);
|
|
319
|
+
const hc = hitClass(px, py);
|
|
320
|
+
if (hc === 'target' || hc === 'ancestor') { x = px; y = py; hit = hc; break; }
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return { x, y, w, h, visible, hit, retargeted };
|
|
230
324
|
})()
|
|
231
325
|
`}export function clickResolvedJs(j={}){return`
|
|
232
326
|
(() => {
|
package/dist/src/cli.js
CHANGED
|
@@ -93,7 +93,7 @@ Examples:
|
|
|
93
93
|
}
|
|
94
94
|
return { kind: 'ok', html: document.documentElement ? document.documentElement.outerHTML : null };
|
|
95
95
|
})()`);if(N.kind==="invalid_selector"){console.log(JSON.stringify({error:{code:"invalid_selector",message:`Selector "${K.selector}" is not a valid CSS selector: ${N.reason}`}},null,2));process.exitCode=J.USAGE_ERROR;return}const U=N.html;if(U===null){if(K.selector){console.log(JSON.stringify({error:{code:"selector_not_found",message:`Selector "${K.selector}" matched 0 elements.`}},null,2));process.exitCode=J.USAGE_ERROR;return}console.log("(empty)");return}if(Y>0&&U.length>Y){console.log(`<!-- ppcli: truncated ${Y} of ${U.length} chars; re-run without --max (or --max 0) for full -->
|
|
96
|
-
${U.slice(0,Y)}`);return}console.log(U)}));E(m(Hq.command("attributes")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","Pick the nth match (0-based) when <target> is a multi-match CSS selector").description("Element attributes — JSON envelope {value, matches_n}")).action(D(async(q,K,X)=>Tq(q,K,X??{},FQ(),"attributes")));function t(q){const K=P(q);if(K&&typeof K==="object"&&"error"in K)return K;if(typeof K==="number")return{opts:{nth:K}};return{opts:{}}}function dK(q){const K=Array.isArray(q)?q:[];if(K.length===0)return{error:{code:"usage_error",message:"At least one file path is required.",hint:'Example: ppcli browser upload "input[type=file]" ./receipt.pdf'}};const X=[];for(const Q of K){const Y=String(Q),Z=Y==="~"||Y.startsWith(`~${M.sep}`)?M.join(Nq.homedir(),Y.slice(2)):Y,N=M.resolve(Z);if(!I.existsSync(N))return{error:{code:"file_not_found",message:`File not found: ${N}`}};if(!I.statSync(N).isFile())return{error:{code:"not_a_file",message:`Not a regular file: ${N}`}};X.push(N)}return{files:X}}function QK(q,K){const X=P(q);if(X&&typeof X==="object"&&"error"in X)return{error:X.error.replace("--nth",K)};if(typeof X==="number")return{opts:{nth:X}};return{opts:{}}}E(m(j.command("click")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Click element — JSON envelope {clicked, target, matches_n}")).action(D(async(q,K,X)=>{const Q=await bq(q,K,X??{},"write");if(typeof Q!=="string"){console.log(JSON.stringify(Q,null,2));process.exitCode=J.USAGE_ERROR;return}const Y=t(X?.nth);if("error"in Y){console.log(JSON.stringify({error:{code:"usage_error",message:Y.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const{matches_n:Z,match_level:N}=await q.click(Q,Y.opts);console.log(JSON.stringify({clicked:!0,target:Q,matches_n:Z,match_level:N},null,2))}));E(m(j.command("type")).argument("[targetOrText]","Numeric ref/CSS target, or text when using --role/--name/etc.").argument("[text]","Text to type").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Click element, then type text — JSON envelope {typed, text, target, matches_n, autocomplete}")).action(D(async(q,K,X,Q)=>{const Y=await Cq(q,K,X,Q??{},"text");if(!Y)return;const Z=t(Q?.nth);if("error"in Z){console.log(JSON.stringify({error:{code:"usage_error",message:Z.error}},null,2));process.exitCode=J.USAGE_ERROR;return}await q.click(Y.target,Z.opts);await q.wait(0.3);const{matches_n:N,match_level:U}=await q.typeText(Y.target,Y.value,Z.opts),G=await q.evaluate(EQ());if(G)await q.wait(0.4);console.log(JSON.stringify({typed:!0,text:Y.value,target:Y.target,matches_n:N,match_level:U,autocomplete:!!G},null,2))}));E(m(j.command("hover")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Move the mouse over an element — JSON envelope {hovered, target, matches_n}")).action(D(async(q,K,X)=>{if(typeof q.hover!=="function")throw Error("browser hover is not supported by this browser backend");const Q=await $q(q,K,X??{});if(!Q)return;const Y=t(X?.nth);if("error"in Y){console.log(JSON.stringify({error:{code:"usage_error",message:Y.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const{matches_n:Z,match_level:N}=await q.hover(Q,Y.opts);console.log(JSON.stringify({hovered:!0,target:Q,matches_n:Z,match_level:N},null,2))}));E(m(j.command("focus")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Focus an element — JSON envelope {focused, target, matches_n}")).action(D(async(q,K,X)=>{if(typeof q.focus!=="function")throw Error("browser focus is not supported by this browser backend");const Q=await $q(q,K,X??{});if(!Q)return;const Y=t(X?.nth);if("error"in Y){console.log(JSON.stringify({error:{code:"usage_error",message:Y.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const{focused:Z,matches_n:N,match_level:U}=await q.focus(Q,Y.opts);console.log(JSON.stringify({focused:Z,target:Q,matches_n:N,match_level:U},null,2))}));E(m(j.command("dblclick")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Double-click element — JSON envelope {dblclicked, target, matches_n}")).action(D(async(q,K,X)=>{if(typeof q.dblClick!=="function")throw Error("browser dblclick is not supported by this browser backend");const Q=await $q(q,K,X??{});if(!Q)return;const Y=t(X?.nth);if("error"in Y){console.log(JSON.stringify({error:{code:"usage_error",message:Y.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const{matches_n:Z,match_level:N}=await q.dblClick(Q,Y.opts);console.log(JSON.stringify({dblclicked:!0,target:Q,matches_n:Z,match_level:N},null,2))}));const XK=async(q,K,X,Q)=>{if(typeof q.setChecked!=="function")throw Error(`browser ${Q?"check":"uncheck"} is not supported by this browser backend`);const Y=await $q(q,K,X);if(!Y)return;const Z=t(X?.nth);if("error"in Z){console.log(JSON.stringify({error:{code:"usage_error",message:Z.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const N=await q.setChecked(Y,Q,Z.opts);console.log(JSON.stringify({checked:N.checked,changed:N.changed,target:Y,matches_n:N.matches_n,match_level:N.match_level,...N.kind?{kind:N.kind}:{}},null,2))};E(m(j.command("check")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Ensure a checkbox/radio/aria-checked control is checked — JSON envelope {checked, changed, target, matches_n}")).action(D(async(q,K,X)=>{await XK(q,K,X??{},!0)}));E(m(j.command("uncheck")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Ensure a checkbox/aria-checked control is unchecked — JSON envelope {checked, changed, target, matches_n}")).action(D(async(q,K,X)=>{await XK(q,K,X??{},!1)}));E(m(j.command("upload")).argument("[targetOrFile]","Numeric ref/CSS target, or first file when using --role/--name/etc.").argument("[files...]","Local file path(s) to attach").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Attach local files to a file input — JSON envelope {uploaded, files, file_names, target, matches_n}")).action(D(async(q,K,X,Q)=>{if(typeof q.uploadFiles!=="function")throw Error("browser upload is not supported by this browser backend");const Y=!!Lq(Q??{}),N=await $q(q,Y?void 0:K,Q??{});if(!N)return;const U=t(Q?.nth);if("error"in U){console.log(JSON.stringify({error:{code:"usage_error",message:U.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const G=Y?[K,...Array.isArray(X)?X:[]].filter((F)=>F!==void 0):X,W=dK(G);if("error"in W){console.log(JSON.stringify({error:W.error},null,2));process.exitCode=J.USAGE_ERROR;return}const _=await q.uploadFiles(N,W.files,U.opts);console.log(JSON.stringify(_,null,2))}));E(eq(eq(j.command("drag"),"from"),"to").argument("[source]","Numeric ref/CSS selector to drag from, or omit with --from-role/--from-name/etc.").argument("[target]","Numeric ref/CSS selector to drop onto, or omit with --to-role/--to-name/etc.").option("--from-nth <n>","When <source> is a multi-match CSS selector, pick the nth match (0-based)").option("--to-nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Drag one element to another — JSON envelope {dragged, source, target, source_matches_n, target_matches_n}")).action(D(async(q,K,X,Q)=>{if(typeof q.drag!=="function")throw Error("browser drag is not supported by this browser backend");const Y=await KK(q,K,Q??{},"from","source");if(!Y)return;const Z=await KK(q,X,Q??{},"to","target");if(!Z)return;const N=QK(Q?.fromNth,"--from-nth");if("error"in N){console.log(JSON.stringify({error:{code:"usage_error",message:N.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const U=QK(Q?.toNth,"--to-nth");if("error"in U){console.log(JSON.stringify({error:{code:"usage_error",message:U.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const G=await q.drag(Y,Z,{from:N.opts,to:U.opts});console.log(JSON.stringify(G,null,2))}));E(m(j.command("fill")).argument("[targetOrText]","Numeric ref/CSS target, or text when using --role/--name/etc.").argument("[text]","Text to set exactly").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Set input/textarea/contenteditable text exactly and verify the value — JSON envelope {filled, verified, text, actual}")).action(D(async(q,K,X,Q)=>{const Y=await Cq(q,K,X,Q??{},"text");if(!Y)return;const Z=t(Q?.nth);if("error"in Z){console.log(JSON.stringify({error:{code:"usage_error",message:Z.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const N=await q.fillText(Y.target,Y.value,Z.opts);if(!N.verified)process.exitCode=J.GENERIC_ERROR;console.log(JSON.stringify({filled:N.filled,verified:N.verified,target:Y.target,text:Y.value,actual:N.actual,length:N.length,matches_n:N.matches_n,match_level:N.match_level,...N.mode?{mode:N.mode}:{}},null,2))}));E(m(j.command("select")).argument("[targetOrOption]","Numeric ref/CSS target, or option text when using --role/--name/etc.").argument("[option]","Option text (or value) to select").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Select dropdown option — JSON envelope {selected, target, matches_n}")).action(D(async(q,K,X,Q)=>{const Y=await Cq(q,K,X,Q??{},"option");if(!Y)return;const Z=t(Q?.nth);if("error"in Z){console.log(JSON.stringify({error:{code:"usage_error",message:Z.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const{matches_n:N,match_level:U}=await R(q,Y.target,Z.opts),G=await q.evaluate(RQ(Y.value));if(G?.error){console.log(JSON.stringify({error:{code:G.error==="Not a <select>"?"not_a_select":"option_not_found",message:G.error,...G.available&&{available:G.available},matches_n:N}},null,2));process.exitCode=J.GENERIC_ERROR;return}console.log(JSON.stringify({selected:G?.selected??Y.value,target:Y.target,matches_n:N,match_level:U},null,2))}));E(j.command("keys").argument("<key>","Key to press (Enter, Escape, Tab, Control+a)")).description("Press keyboard key").action(D(async(q,K)=>{await q.pressKey(K);console.log(`Pressed: ${K}`)}));const YK=j.command("dialog").description("Handle a blocking JavaScript alert/confirm/prompt dialog");E(YK.command("accept").option("--text <text>","Prompt text to submit for prompt() dialogs").description("Accept the currently open JavaScript dialog")).action(D(async(q,K)=>{if(!q.handleJavaScriptDialog)throw Error("This browser session does not support JavaScript dialog handling");try{await q.handleJavaScriptDialog(!0,K?.text)}catch(X){if(h(X).toLowerCase().includes("no dialog")){console.log(JSON.stringify({error:{code:"no_javascript_dialog",message:"No JavaScript dialog is currently open."}},null,2));process.exitCode=J.USAGE_ERROR;return}throw X}console.log(JSON.stringify({handled:!0,action:"accept",...K?.text!==void 0&&{text:K.text}},null,2))}));E(YK.command("dismiss").description("Dismiss the currently open JavaScript dialog")).action(D(async(q)=>{if(!q.handleJavaScriptDialog)throw Error("This browser session does not support JavaScript dialog handling");try{await q.handleJavaScriptDialog(!1)}catch(K){if(h(K).toLowerCase().includes("no dialog")){console.log(JSON.stringify({error:{code:"no_javascript_dialog",message:"No JavaScript dialog is currently open."}},null,2));process.exitCode=J.USAGE_ERROR;return}throw K}console.log(JSON.stringify({handled:!0,action:"dismiss"},null,2))}));E(j.command("wait")).argument("<type>","selector, text, time, xhr, or download").argument("[value]","CSS selector, text string, seconds, XHR URL regex, or download filename/URL pattern").option("--timeout <ms>","Timeout in milliseconds","10000").description('Wait for selector, text, time, matching XHR, or browser download (e.g. wait selector ".loaded", wait text "Success", wait time 3, wait xhr "/api/search", wait download receipt.pdf)').action(D(async(q,K,X,Q)=>{const Y=parseInt(Q.timeout,10);if(K==="time"){const Z=parseFloat(X??"2");await q.wait(Z);console.log(`Waited ${Z}s`)}else if(K==="selector"){if(!X){console.error("Missing CSS selector");process.exitCode=J.USAGE_ERROR;return}await q.wait({selector:X,timeout:Y/1000});console.log(`Element "${X}" appeared`)}else if(K==="text"){if(!X){console.error("Missing text");process.exitCode=J.USAGE_ERROR;return}await q.wait({text:X,timeout:Y/1000});console.log(`Text "${X}" appeared`)}else if(K==="xhr"){if(!X){console.error("Missing XHR URL regex");process.exitCode=J.USAGE_ERROR;return}let Z;try{Z=new RegExp(X)}catch(_){console.error(`Invalid regex "${X}": ${_ instanceof Error?_.message:String(_)}`);process.exitCode=J.USAGE_ERROR;return}if(!(await q.startNetworkCapture?.()??!1))try{await q.evaluate(Kq)}catch{}await Wq(q);const U=Date.now()+Y,G=400;let W=null;while(Date.now()<U&&!W){W=(await Wq(q)).find((F)=>Z.test(F.url))??null;if(!W)await new Promise((F)=>setTimeout(F,G))}if(!W){console.log(JSON.stringify({error:{code:"xhr_not_seen",message:`No captured XHR matched /${X}/ within ${Y}ms`,hint:"Check the pattern against `browser network` output; the endpoint may not have fired yet, or capture is disabled."}},null,2));process.exitCode=J.GENERIC_ERROR;return}console.log(JSON.stringify({matched:{url:W.url,status:W.status,contentType:W.ct}},null,2))}else if(K==="download"){if(typeof q.waitForDownload!=="function"){console.log(JSON.stringify({error:{code:"download_wait_unavailable",message:"The active browser backend does not support download lifecycle waits.",hint:"Use the Browser Bridge extension version 1.0.8 or newer, then retry the command."}},null,2));process.exitCode=J.GENERIC_ERROR;return}const Z=await q.waitForDownload(String(X??""),Y);if(!Z.downloaded){const N=Z.state==="interrupted"&&Z.id!==void 0?"download_failed":"download_not_seen";console.log(JSON.stringify({error:{code:N,message:Z.error??`No download matched "${X??"*"}" within ${Y}ms`,hint:"Check the pattern against the expected filename or URL; use a longer --timeout if the download starts slowly."},download:Z},null,2));process.exitCode=J.GENERIC_ERROR;return}console.log(JSON.stringify(Z,null,2))}else{console.error(`Unknown wait type "${K}". Use: selector, text, time, xhr, or download`);process.exitCode=J.USAGE_ERROR}}));E(j.command("eval").argument("<js>","JavaScript code").option("--frame <index>",'Cross-origin iframe index from "browser frames"').description("Execute JS in page context, return result")).action(D(async(q,K,X)=>{let Q;if(X.frame!==void 0){const Y=Number.parseInt(X.frame,10);if(!Number.isInteger(Y)||Y<0){console.error(`Invalid frame index "${X.frame}". Use a 0-based index from "browser frames".`);process.exitCode=J.USAGE_ERROR;return}if(!q.evaluateInFrame)throw Error("This browser session does not support frame-targeted evaluation");Q=await q.evaluateInFrame(K,Y)}else Q=await q.evaluate(K);if(typeof Q==="string")console.log(Q);else console.log(JSON.stringify(Q,null,2))}));E(j.command("extract").option("--selector <css>","CSS selector scope; defaults to <main>/<article>/<body>").option("--chunk-size <chars>","Target chunk size in chars","20000").option("--start <char>","Start offset (use next_start_char from a previous extract)","0").description("Extract page content as markdown, paragraph-aware chunks for long pages")).action(D(async(q,K)=>{const X=String(K.chunkSize??"20000");if(!/^\d+$/.test(X)||Number.parseInt(X,10)<=0){console.log(JSON.stringify({error:{code:"invalid_chunk_size",message:`--chunk-size must be a positive integer, got "${K.chunkSize}"`}},null,2));process.exitCode=J.USAGE_ERROR;return}const Q=String(K.start??"0");if(!/^\d+$/.test(Q)){console.log(JSON.stringify({error:{code:"invalid_start",message:`--start must be a non-negative integer, got "${K.start}"`}},null,2));process.exitCode=J.USAGE_ERROR;return}const Y=Number.parseInt(X,10),Z=Number.parseInt(Q,10),N=typeof K.selector==="string"&&K.selector.length>0?K.selector:null,U=CQ(N),G=await q.evaluate(U);if(!G){console.log(JSON.stringify({error:{code:"extract_failed",message:"Page returned no root element."}},null,2));process.exitCode=J.USAGE_ERROR;return}if("invalidSelector"in G){console.log(JSON.stringify({error:{code:"invalid_selector",message:`Selector "${N}" is not a valid CSS selector: ${G.reason}`}},null,2));process.exitCode=J.USAGE_ERROR;return}if("notFound"in G){console.log(JSON.stringify({error:{code:"selector_not_found",message:N?`Selector "${N}" matched 0 elements.`:"Page has no body/main/article element."}},null,2));process.exitCode=J.USAGE_ERROR;return}const W=TQ({html:G.html,url:G.url,title:G.title,selector:N,start:Z,chunkSize:Y});console.log(JSON.stringify(W,null,2))}));E(j.command("network")).option("--detail <key>","Emit full body for the entry with this key").option("--all","Include static resources (js/css/images/telemetry)").option("--raw","Emit full bodies for every entry (skip shape preview)").option("--filter <fields>","Comma-separated field names; keep only entries whose body shape has ALL names as path segments").option("--since <duration>","Only include entries from the last duration (for example: 30s, 2m)").option("--until <duration>","Only include entries older than the duration from now").option("--follow","Continuously print new matching entries as JSON lines",!1).option("--failed","Only include failed HTTP requests (status 0 or >= 400)",!1).option("--max-body <chars>","With --detail: cap the emitted body at N chars (0 = unlimited, default)","0").option("--ttl <ms>","Cache TTL in ms for --detail lookups",String(OK)).description("Capture network requests as shape previews; retrieve full bodies by key").action(D(async(q,K)=>{const X=xK(K.ttl,"ttl",OK),Q=tq(q),Y=typeof K.detail==="string"&&K.detail.length>0,Z=typeof K.filter==="string",N=kq(K.since,"since"),U=kq(K.until,"until");if(N&&typeof N==="object"){p("invalid_since",N.error);return}if(U&&typeof U==="object"){p("invalid_until",U.error);return}if(Y&&Z){p("invalid_args","--filter and --detail cannot be used together (one narrows a list, the other fetches a specific entry).");return}let G=null;if(Z){const L=MQ(K.filter);if("reason"in L){p("invalid_filter",L.reason);return}G=L.fields}if(Y&&K.follow){p("invalid_args","--follow cannot be used with --detail.");return}if(Y){const L=kQ(Q,{ttlMs:X});if(L.status==="missing"){p("cache_missing",`No cached capture. Run "browser network" first (in session "${Q}").`);return}if(L.status==="expired"){p("cache_expired",`Cache is stale (age ${L.ageMs}ms > ttl ${X}ms). Re-run "browser network" to refresh.`);return}if(L.status==="corrupt"||!L.file){p("cache_corrupt",'Cache file is malformed; re-run "browser network" to regenerate.');return}const O=OQ(L.file,K.detail);if(!O){p("key_not_found",`Key "${K.detail}" not in cache.`,{available_keys:L.file.entries.map((a)=>a.key)});return}const s=String(K.maxBody??"0");if(!/^\d+$/.test(s)){p("invalid_max_body",`--max-body must be a non-negative integer, got "${K.maxBody}"`);return}const A=Number.parseInt(s,10);let b=O.body,x=!1;if(A>0&&typeof O.body==="string"&&O.body.length>A){b=O.body.slice(0,A);x=!0}const u=O.body_truncated===!0,c={key:O.key,url:O.url,method:O.method,status:O.status,ct:O.ct,size:O.size,...typeof O.timestamp==="number"?{timestamp:Gq(O.timestamp)}:{},shape:EK(O.body),body:b};if(u||x){c.body_truncated=!0;c.body_full_size=O.body_full_size??O.size;c.body_truncation_reason=u?"capture-limit":"max-body"}console.log(JSON.stringify(c,null,2));return}if(K.follow){if(!await q.startNetworkCapture?.())try{await q.evaluate(Kq)}catch{}while(!0){const L=await Wq(q).catch((A)=>{p("capture_failed",`Could not read network capture: ${A.message}`);return[]});let O=K.all?L:SK(L);O=iq(O,{sinceMs:N,untilMs:U});if(K.failed)O=O.filter((A)=>A.status===0||A.status>=400);const s=PK(O);for(const A of s)console.log(JSON.stringify({key:A.key,timestamp:Gq(A.timestamp),method:A.method,status:A.status,url:A.url,ct:A.ct,size:A.size,...A.bodyTruncated?{body_truncated:!0}:{}}));await new Promise((A)=>setTimeout(A,MK))}}let W;try{W=await Wq(q)}catch(L){p("capture_failed",`Could not read network capture: ${L.message}`);return}let _=K.all?W:SK(W);_=iq(_,{sinceMs:N,untilMs:U});if(K.failed)_=_.filter((L)=>L.status===0||L.status>=400);const F=W.length-_.length,f=PK(_).map((L)=>({key:L.key,url:L.url,method:L.method,status:L.status,size:L.size,ct:L.ct,body:L.body,...typeof L.timestamp==="number"?{timestamp:L.timestamp}:{},...L.bodyTruncated?{body_truncated:!0}:{},...L.bodyTruncated&&typeof L.bodyFullSize==="number"?{body_full_size:L.bodyFullSize}:{}}));let y=null;try{IQ(Q,f)}catch(L){y=`Could not persist capture cache: ${L.message}. --detail lookups may miss this capture.`}const w=f.map((L)=>({entry:L,shape:EK(L.body)})),T=G?w.filter((L)=>SQ(L.shape,G)):w,d=G?w.length-T.length:0,g={session:Q,captured_at:new Date().toISOString(),count:T.length,filtered_out:F};if(G){g.filter=G;g.filter_dropped=d}if(y)g.cache_warning=y;const Qq=T.filter((L)=>L.entry.body_truncated).length;if(Qq>0){g.body_truncated_count=Qq;g.body_truncated_hint="Some bodies exceeded the capture limit; their `shape` reflects only the captured prefix."}if(K.raw)g.entries=T.map((L)=>({...L.entry,...typeof L.entry.timestamp==="number"?{timestamp:Gq(L.entry.timestamp)}:{}}));else{g.entries=T.map((L)=>({key:L.entry.key,method:L.entry.method,...typeof L.entry.timestamp==="number"?{timestamp:Gq(L.entry.timestamp)}:{},status:L.entry.status,url:L.entry.url,ct:L.entry.ct,size:L.entry.size,shape:L.shape,...L.entry.body_truncated?{body_truncated:!0}:{}}));g.detail_hint='Run "browser network --detail <key>" for full body.'}console.log(JSON.stringify(g,null,2))}));j.command("init").argument("<name>","Adapter name in site/command format (e.g. hn/top)").description("Generate adapter scaffold in ~/.ppcli/clis/").action(async(q)=>{try{const K=q.split("/");if(K.length!==2||!K[0]||!K[1]){console.error("Name must be site/command format (e.g. hn/top)");process.exitCode=J.USAGE_ERROR;return}const[X,Q]=K;if(!/^[a-zA-Z0-9_-]+$/.test(X)||!/^[a-zA-Z0-9_-]+$/.test(Q)){console.error("Name parts must be alphanumeric/dash/underscore only");process.exitCode=J.USAGE_ERROR;return}const Y=await import("node:os"),Z=await import("node:fs"),N=await import("node:path"),U=N.join(Y.homedir(),".ppcli","clis",X),G=N.join(U,`${Q}.js`);if(Z.existsSync(G)){console.log(`Adapter already exists: ${G}`);return}const _=`import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
96
|
+
${U.slice(0,Y)}`);return}console.log(U)}));E(m(Hq.command("attributes")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","Pick the nth match (0-based) when <target> is a multi-match CSS selector").description("Element attributes — JSON envelope {value, matches_n}")).action(D(async(q,K,X)=>Tq(q,K,X??{},FQ(),"attributes")));function t(q){const K=P(q);if(K&&typeof K==="object"&&"error"in K)return K;if(typeof K==="number")return{opts:{nth:K}};return{opts:{}}}function dK(q){const K=Array.isArray(q)?q:[];if(K.length===0)return{error:{code:"usage_error",message:"At least one file path is required.",hint:'Example: ppcli browser upload "input[type=file]" ./receipt.pdf'}};const X=[];for(const Q of K){const Y=String(Q),Z=Y==="~"||Y.startsWith(`~${M.sep}`)?M.join(Nq.homedir(),Y.slice(2)):Y,N=M.resolve(Z);if(!I.existsSync(N))return{error:{code:"file_not_found",message:`File not found: ${N}`}};if(!I.statSync(N).isFile())return{error:{code:"not_a_file",message:`Not a regular file: ${N}`}};X.push(N)}return{files:X}}function QK(q,K){const X=P(q);if(X&&typeof X==="object"&&"error"in X)return{error:X.error.replace("--nth",K)};if(typeof X==="number")return{opts:{nth:X}};return{opts:{}}}E(m(j.command("click")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Click element — JSON envelope {clicked, target, matches_n}")).action(D(async(q,K,X)=>{const Q=await bq(q,K,X??{},"write");if(typeof Q!=="string"){console.log(JSON.stringify(Q,null,2));process.exitCode=J.USAGE_ERROR;return}const Y=t(X?.nth);if("error"in Y){console.log(JSON.stringify({error:{code:"usage_error",message:Y.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const{matches_n:Z,match_level:N,click_method:U,hit:G,retargeted:W}=await q.click(Q,Y.opts);console.log(JSON.stringify({clicked:!0,target:Q,matches_n:Z,match_level:N,...U&&{click_method:U},...G&&{hit:G},...W&&{retargeted:W}},null,2))}));E(m(j.command("type")).argument("[targetOrText]","Numeric ref/CSS target, or text when using --role/--name/etc.").argument("[text]","Text to type").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Click element, then type text — JSON envelope {typed, text, target, matches_n, autocomplete}")).action(D(async(q,K,X,Q)=>{const Y=await Cq(q,K,X,Q??{},"text");if(!Y)return;const Z=t(Q?.nth);if("error"in Z){console.log(JSON.stringify({error:{code:"usage_error",message:Z.error}},null,2));process.exitCode=J.USAGE_ERROR;return}await q.click(Y.target,Z.opts);await q.wait(0.3);const{matches_n:N,match_level:U}=await q.typeText(Y.target,Y.value,Z.opts),G=await q.evaluate(EQ());if(G)await q.wait(0.4);console.log(JSON.stringify({typed:!0,text:Y.value,target:Y.target,matches_n:N,match_level:U,autocomplete:!!G},null,2))}));E(m(j.command("hover")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Move the mouse over an element — JSON envelope {hovered, target, matches_n}")).action(D(async(q,K,X)=>{if(typeof q.hover!=="function")throw Error("browser hover is not supported by this browser backend");const Q=await $q(q,K,X??{});if(!Q)return;const Y=t(X?.nth);if("error"in Y){console.log(JSON.stringify({error:{code:"usage_error",message:Y.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const{matches_n:Z,match_level:N}=await q.hover(Q,Y.opts);console.log(JSON.stringify({hovered:!0,target:Q,matches_n:Z,match_level:N},null,2))}));E(m(j.command("focus")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Focus an element — JSON envelope {focused, target, matches_n}")).action(D(async(q,K,X)=>{if(typeof q.focus!=="function")throw Error("browser focus is not supported by this browser backend");const Q=await $q(q,K,X??{});if(!Q)return;const Y=t(X?.nth);if("error"in Y){console.log(JSON.stringify({error:{code:"usage_error",message:Y.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const{focused:Z,matches_n:N,match_level:U}=await q.focus(Q,Y.opts);console.log(JSON.stringify({focused:Z,target:Q,matches_n:N,match_level:U},null,2))}));E(m(j.command("dblclick")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Double-click element — JSON envelope {dblclicked, target, matches_n}")).action(D(async(q,K,X)=>{if(typeof q.dblClick!=="function")throw Error("browser dblclick is not supported by this browser backend");const Q=await $q(q,K,X??{});if(!Q)return;const Y=t(X?.nth);if("error"in Y){console.log(JSON.stringify({error:{code:"usage_error",message:Y.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const{matches_n:Z,match_level:N}=await q.dblClick(Q,Y.opts);console.log(JSON.stringify({dblclicked:!0,target:Q,matches_n:Z,match_level:N},null,2))}));const XK=async(q,K,X,Q)=>{if(typeof q.setChecked!=="function")throw Error(`browser ${Q?"check":"uncheck"} is not supported by this browser backend`);const Y=await $q(q,K,X);if(!Y)return;const Z=t(X?.nth);if("error"in Z){console.log(JSON.stringify({error:{code:"usage_error",message:Z.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const N=await q.setChecked(Y,Q,Z.opts);console.log(JSON.stringify({checked:N.checked,changed:N.changed,target:Y,matches_n:N.matches_n,match_level:N.match_level,...N.kind?{kind:N.kind}:{}},null,2))};E(m(j.command("check")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Ensure a checkbox/radio/aria-checked control is checked — JSON envelope {checked, changed, target, matches_n}")).action(D(async(q,K,X)=>{await XK(q,K,X??{},!0)}));E(m(j.command("uncheck")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Ensure a checkbox/aria-checked control is unchecked — JSON envelope {checked, changed, target, matches_n}")).action(D(async(q,K,X)=>{await XK(q,K,X??{},!1)}));E(m(j.command("upload")).argument("[targetOrFile]","Numeric ref/CSS target, or first file when using --role/--name/etc.").argument("[files...]","Local file path(s) to attach").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Attach local files to a file input — JSON envelope {uploaded, files, file_names, target, matches_n}")).action(D(async(q,K,X,Q)=>{if(typeof q.uploadFiles!=="function")throw Error("browser upload is not supported by this browser backend");const Y=!!Lq(Q??{}),N=await $q(q,Y?void 0:K,Q??{});if(!N)return;const U=t(Q?.nth);if("error"in U){console.log(JSON.stringify({error:{code:"usage_error",message:U.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const G=Y?[K,...Array.isArray(X)?X:[]].filter((F)=>F!==void 0):X,W=dK(G);if("error"in W){console.log(JSON.stringify({error:W.error},null,2));process.exitCode=J.USAGE_ERROR;return}const _=await q.uploadFiles(N,W.files,U.opts);console.log(JSON.stringify(_,null,2))}));E(eq(eq(j.command("drag"),"from"),"to").argument("[source]","Numeric ref/CSS selector to drag from, or omit with --from-role/--from-name/etc.").argument("[target]","Numeric ref/CSS selector to drop onto, or omit with --to-role/--to-name/etc.").option("--from-nth <n>","When <source> is a multi-match CSS selector, pick the nth match (0-based)").option("--to-nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Drag one element to another — JSON envelope {dragged, source, target, source_matches_n, target_matches_n}")).action(D(async(q,K,X,Q)=>{if(typeof q.drag!=="function")throw Error("browser drag is not supported by this browser backend");const Y=await KK(q,K,Q??{},"from","source");if(!Y)return;const Z=await KK(q,X,Q??{},"to","target");if(!Z)return;const N=QK(Q?.fromNth,"--from-nth");if("error"in N){console.log(JSON.stringify({error:{code:"usage_error",message:N.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const U=QK(Q?.toNth,"--to-nth");if("error"in U){console.log(JSON.stringify({error:{code:"usage_error",message:U.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const G=await q.drag(Y,Z,{from:N.opts,to:U.opts});console.log(JSON.stringify(G,null,2))}));E(m(j.command("fill")).argument("[targetOrText]","Numeric ref/CSS target, or text when using --role/--name/etc.").argument("[text]","Text to set exactly").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Set input/textarea/contenteditable text exactly and verify the value — JSON envelope {filled, verified, text, actual}")).action(D(async(q,K,X,Q)=>{const Y=await Cq(q,K,X,Q??{},"text");if(!Y)return;const Z=t(Q?.nth);if("error"in Z){console.log(JSON.stringify({error:{code:"usage_error",message:Z.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const N=await q.fillText(Y.target,Y.value,Z.opts);if(!N.verified)process.exitCode=J.GENERIC_ERROR;console.log(JSON.stringify({filled:N.filled,verified:N.verified,target:Y.target,text:Y.value,actual:N.actual,length:N.length,matches_n:N.matches_n,match_level:N.match_level,...N.mode?{mode:N.mode}:{}},null,2))}));E(m(j.command("select")).argument("[targetOrOption]","Numeric ref/CSS target, or option text when using --role/--name/etc.").argument("[option]","Option text (or value) to select").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Select dropdown option — JSON envelope {selected, target, matches_n}")).action(D(async(q,K,X,Q)=>{const Y=await Cq(q,K,X,Q??{},"option");if(!Y)return;const Z=t(Q?.nth);if("error"in Z){console.log(JSON.stringify({error:{code:"usage_error",message:Z.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const{matches_n:N,match_level:U}=await R(q,Y.target,Z.opts),G=await q.evaluate(RQ(Y.value));if(G?.error){console.log(JSON.stringify({error:{code:G.error==="Not a <select>"?"not_a_select":"option_not_found",message:G.error,...G.available&&{available:G.available},matches_n:N}},null,2));process.exitCode=J.GENERIC_ERROR;return}console.log(JSON.stringify({selected:G?.selected??Y.value,target:Y.target,matches_n:N,match_level:U},null,2))}));E(j.command("keys").argument("<key>","Key to press (Enter, Escape, Tab, Control+a)")).description("Press keyboard key").action(D(async(q,K)=>{await q.pressKey(K);console.log(`Pressed: ${K}`)}));const YK=j.command("dialog").description("Handle a blocking JavaScript alert/confirm/prompt dialog");E(YK.command("accept").option("--text <text>","Prompt text to submit for prompt() dialogs").description("Accept the currently open JavaScript dialog")).action(D(async(q,K)=>{if(!q.handleJavaScriptDialog)throw Error("This browser session does not support JavaScript dialog handling");try{await q.handleJavaScriptDialog(!0,K?.text)}catch(X){if(h(X).toLowerCase().includes("no dialog")){console.log(JSON.stringify({error:{code:"no_javascript_dialog",message:"No JavaScript dialog is currently open."}},null,2));process.exitCode=J.USAGE_ERROR;return}throw X}console.log(JSON.stringify({handled:!0,action:"accept",...K?.text!==void 0&&{text:K.text}},null,2))}));E(YK.command("dismiss").description("Dismiss the currently open JavaScript dialog")).action(D(async(q)=>{if(!q.handleJavaScriptDialog)throw Error("This browser session does not support JavaScript dialog handling");try{await q.handleJavaScriptDialog(!1)}catch(K){if(h(K).toLowerCase().includes("no dialog")){console.log(JSON.stringify({error:{code:"no_javascript_dialog",message:"No JavaScript dialog is currently open."}},null,2));process.exitCode=J.USAGE_ERROR;return}throw K}console.log(JSON.stringify({handled:!0,action:"dismiss"},null,2))}));E(j.command("wait")).argument("<type>","selector, text, time, xhr, or download").argument("[value]","CSS selector, text string, seconds, XHR URL regex, or download filename/URL pattern").option("--timeout <ms>","Timeout in milliseconds","10000").description('Wait for selector, text, time, matching XHR, or browser download (e.g. wait selector ".loaded", wait text "Success", wait time 3, wait xhr "/api/search", wait download receipt.pdf)').action(D(async(q,K,X,Q)=>{const Y=parseInt(Q.timeout,10);if(K==="time"){const Z=parseFloat(X??"2");await q.wait(Z);console.log(`Waited ${Z}s`)}else if(K==="selector"){if(!X){console.error("Missing CSS selector");process.exitCode=J.USAGE_ERROR;return}await q.wait({selector:X,timeout:Y/1000});console.log(`Element "${X}" appeared`)}else if(K==="text"){if(!X){console.error("Missing text");process.exitCode=J.USAGE_ERROR;return}await q.wait({text:X,timeout:Y/1000});console.log(`Text "${X}" appeared`)}else if(K==="xhr"){if(!X){console.error("Missing XHR URL regex");process.exitCode=J.USAGE_ERROR;return}let Z;try{Z=new RegExp(X)}catch(_){console.error(`Invalid regex "${X}": ${_ instanceof Error?_.message:String(_)}`);process.exitCode=J.USAGE_ERROR;return}if(!(await q.startNetworkCapture?.()??!1))try{await q.evaluate(Kq)}catch{}await Wq(q);const U=Date.now()+Y,G=400;let W=null;while(Date.now()<U&&!W){W=(await Wq(q)).find((F)=>Z.test(F.url))??null;if(!W)await new Promise((F)=>setTimeout(F,G))}if(!W){console.log(JSON.stringify({error:{code:"xhr_not_seen",message:`No captured XHR matched /${X}/ within ${Y}ms`,hint:"Check the pattern against `browser network` output; the endpoint may not have fired yet, or capture is disabled."}},null,2));process.exitCode=J.GENERIC_ERROR;return}console.log(JSON.stringify({matched:{url:W.url,status:W.status,contentType:W.ct}},null,2))}else if(K==="download"){if(typeof q.waitForDownload!=="function"){console.log(JSON.stringify({error:{code:"download_wait_unavailable",message:"The active browser backend does not support download lifecycle waits.",hint:"Use the Browser Bridge extension version 1.0.8 or newer, then retry the command."}},null,2));process.exitCode=J.GENERIC_ERROR;return}const Z=await q.waitForDownload(String(X??""),Y);if(!Z.downloaded){const N=Z.state==="interrupted"&&Z.id!==void 0?"download_failed":"download_not_seen";console.log(JSON.stringify({error:{code:N,message:Z.error??`No download matched "${X??"*"}" within ${Y}ms`,hint:"Check the pattern against the expected filename or URL; use a longer --timeout if the download starts slowly."},download:Z},null,2));process.exitCode=J.GENERIC_ERROR;return}console.log(JSON.stringify(Z,null,2))}else{console.error(`Unknown wait type "${K}". Use: selector, text, time, xhr, or download`);process.exitCode=J.USAGE_ERROR}}));E(j.command("eval").argument("<js>","JavaScript code").option("--frame <index>",'Cross-origin iframe index from "browser frames"').description("Execute JS in page context, return result")).action(D(async(q,K,X)=>{let Q;if(X.frame!==void 0){const Y=Number.parseInt(X.frame,10);if(!Number.isInteger(Y)||Y<0){console.error(`Invalid frame index "${X.frame}". Use a 0-based index from "browser frames".`);process.exitCode=J.USAGE_ERROR;return}if(!q.evaluateInFrame)throw Error("This browser session does not support frame-targeted evaluation");Q=await q.evaluateInFrame(K,Y)}else Q=await q.evaluate(K);if(typeof Q==="string")console.log(Q);else console.log(JSON.stringify(Q,null,2))}));E(j.command("extract").option("--selector <css>","CSS selector scope; defaults to <main>/<article>/<body>").option("--chunk-size <chars>","Target chunk size in chars","20000").option("--start <char>","Start offset (use next_start_char from a previous extract)","0").description("Extract page content as markdown, paragraph-aware chunks for long pages")).action(D(async(q,K)=>{const X=String(K.chunkSize??"20000");if(!/^\d+$/.test(X)||Number.parseInt(X,10)<=0){console.log(JSON.stringify({error:{code:"invalid_chunk_size",message:`--chunk-size must be a positive integer, got "${K.chunkSize}"`}},null,2));process.exitCode=J.USAGE_ERROR;return}const Q=String(K.start??"0");if(!/^\d+$/.test(Q)){console.log(JSON.stringify({error:{code:"invalid_start",message:`--start must be a non-negative integer, got "${K.start}"`}},null,2));process.exitCode=J.USAGE_ERROR;return}const Y=Number.parseInt(X,10),Z=Number.parseInt(Q,10),N=typeof K.selector==="string"&&K.selector.length>0?K.selector:null,U=CQ(N),G=await q.evaluate(U);if(!G){console.log(JSON.stringify({error:{code:"extract_failed",message:"Page returned no root element."}},null,2));process.exitCode=J.USAGE_ERROR;return}if("invalidSelector"in G){console.log(JSON.stringify({error:{code:"invalid_selector",message:`Selector "${N}" is not a valid CSS selector: ${G.reason}`}},null,2));process.exitCode=J.USAGE_ERROR;return}if("notFound"in G){console.log(JSON.stringify({error:{code:"selector_not_found",message:N?`Selector "${N}" matched 0 elements.`:"Page has no body/main/article element."}},null,2));process.exitCode=J.USAGE_ERROR;return}const W=TQ({html:G.html,url:G.url,title:G.title,selector:N,start:Z,chunkSize:Y});console.log(JSON.stringify(W,null,2))}));E(j.command("network")).option("--detail <key>","Emit full body for the entry with this key").option("--all","Include static resources (js/css/images/telemetry)").option("--raw","Emit full bodies for every entry (skip shape preview)").option("--filter <fields>","Comma-separated field names; keep only entries whose body shape has ALL names as path segments").option("--since <duration>","Only include entries from the last duration (for example: 30s, 2m)").option("--until <duration>","Only include entries older than the duration from now").option("--follow","Continuously print new matching entries as JSON lines",!1).option("--failed","Only include failed HTTP requests (status 0 or >= 400)",!1).option("--max-body <chars>","With --detail: cap the emitted body at N chars (0 = unlimited, default)","0").option("--ttl <ms>","Cache TTL in ms for --detail lookups",String(OK)).description("Capture network requests as shape previews; retrieve full bodies by key").action(D(async(q,K)=>{const X=xK(K.ttl,"ttl",OK),Q=tq(q),Y=typeof K.detail==="string"&&K.detail.length>0,Z=typeof K.filter==="string",N=kq(K.since,"since"),U=kq(K.until,"until");if(N&&typeof N==="object"){p("invalid_since",N.error);return}if(U&&typeof U==="object"){p("invalid_until",U.error);return}if(Y&&Z){p("invalid_args","--filter and --detail cannot be used together (one narrows a list, the other fetches a specific entry).");return}let G=null;if(Z){const L=MQ(K.filter);if("reason"in L){p("invalid_filter",L.reason);return}G=L.fields}if(Y&&K.follow){p("invalid_args","--follow cannot be used with --detail.");return}if(Y){const L=kQ(Q,{ttlMs:X});if(L.status==="missing"){p("cache_missing",`No cached capture. Run "browser network" first (in session "${Q}").`);return}if(L.status==="expired"){p("cache_expired",`Cache is stale (age ${L.ageMs}ms > ttl ${X}ms). Re-run "browser network" to refresh.`);return}if(L.status==="corrupt"||!L.file){p("cache_corrupt",'Cache file is malformed; re-run "browser network" to regenerate.');return}const O=OQ(L.file,K.detail);if(!O){p("key_not_found",`Key "${K.detail}" not in cache.`,{available_keys:L.file.entries.map((a)=>a.key)});return}const s=String(K.maxBody??"0");if(!/^\d+$/.test(s)){p("invalid_max_body",`--max-body must be a non-negative integer, got "${K.maxBody}"`);return}const A=Number.parseInt(s,10);let b=O.body,x=!1;if(A>0&&typeof O.body==="string"&&O.body.length>A){b=O.body.slice(0,A);x=!0}const u=O.body_truncated===!0,c={key:O.key,url:O.url,method:O.method,status:O.status,ct:O.ct,size:O.size,...typeof O.timestamp==="number"?{timestamp:Gq(O.timestamp)}:{},shape:EK(O.body),body:b};if(u||x){c.body_truncated=!0;c.body_full_size=O.body_full_size??O.size;c.body_truncation_reason=u?"capture-limit":"max-body"}console.log(JSON.stringify(c,null,2));return}if(K.follow){if(!await q.startNetworkCapture?.())try{await q.evaluate(Kq)}catch{}while(!0){const L=await Wq(q).catch((A)=>{p("capture_failed",`Could not read network capture: ${A.message}`);return[]});let O=K.all?L:SK(L);O=iq(O,{sinceMs:N,untilMs:U});if(K.failed)O=O.filter((A)=>A.status===0||A.status>=400);const s=PK(O);for(const A of s)console.log(JSON.stringify({key:A.key,timestamp:Gq(A.timestamp),method:A.method,status:A.status,url:A.url,ct:A.ct,size:A.size,...A.bodyTruncated?{body_truncated:!0}:{}}));await new Promise((A)=>setTimeout(A,MK))}}let W;try{W=await Wq(q)}catch(L){p("capture_failed",`Could not read network capture: ${L.message}`);return}let _=K.all?W:SK(W);_=iq(_,{sinceMs:N,untilMs:U});if(K.failed)_=_.filter((L)=>L.status===0||L.status>=400);const F=W.length-_.length,f=PK(_).map((L)=>({key:L.key,url:L.url,method:L.method,status:L.status,size:L.size,ct:L.ct,body:L.body,...typeof L.timestamp==="number"?{timestamp:L.timestamp}:{},...L.bodyTruncated?{body_truncated:!0}:{},...L.bodyTruncated&&typeof L.bodyFullSize==="number"?{body_full_size:L.bodyFullSize}:{}}));let y=null;try{IQ(Q,f)}catch(L){y=`Could not persist capture cache: ${L.message}. --detail lookups may miss this capture.`}const w=f.map((L)=>({entry:L,shape:EK(L.body)})),T=G?w.filter((L)=>SQ(L.shape,G)):w,d=G?w.length-T.length:0,g={session:Q,captured_at:new Date().toISOString(),count:T.length,filtered_out:F};if(G){g.filter=G;g.filter_dropped=d}if(y)g.cache_warning=y;const Qq=T.filter((L)=>L.entry.body_truncated).length;if(Qq>0){g.body_truncated_count=Qq;g.body_truncated_hint="Some bodies exceeded the capture limit; their `shape` reflects only the captured prefix."}if(K.raw)g.entries=T.map((L)=>({...L.entry,...typeof L.entry.timestamp==="number"?{timestamp:Gq(L.entry.timestamp)}:{}}));else{g.entries=T.map((L)=>({key:L.entry.key,method:L.entry.method,...typeof L.entry.timestamp==="number"?{timestamp:Gq(L.entry.timestamp)}:{},status:L.entry.status,url:L.entry.url,ct:L.entry.ct,size:L.entry.size,shape:L.shape,...L.entry.body_truncated?{body_truncated:!0}:{}}));g.detail_hint='Run "browser network --detail <key>" for full body.'}console.log(JSON.stringify(g,null,2))}));j.command("init").argument("<name>","Adapter name in site/command format (e.g. hn/top)").description("Generate adapter scaffold in ~/.ppcli/clis/").action(async(q)=>{try{const K=q.split("/");if(K.length!==2||!K[0]||!K[1]){console.error("Name must be site/command format (e.g. hn/top)");process.exitCode=J.USAGE_ERROR;return}const[X,Q]=K;if(!/^[a-zA-Z0-9_-]+$/.test(X)||!/^[a-zA-Z0-9_-]+$/.test(Q)){console.error("Name parts must be alphanumeric/dash/underscore only");process.exitCode=J.USAGE_ERROR;return}const Y=await import("node:os"),Z=await import("node:fs"),N=await import("node:path"),U=N.join(Y.homedir(),".ppcli","clis",X),G=N.join(U,`${Q}.js`);if(Z.existsSync(G)){console.log(`Adapter already exists: ${G}`);return}const _=`import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
97
97
|
|
|
98
98
|
cli({
|
|
99
99
|
site: '${X}',
|
package/dist/src/external.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import*as
|
|
1
|
+
import*as V from"node:fs";import*as Z from"node:path";import*as Y from"node:os";import{fileURLToPath as F}from"node:url";import{spawnSync as M,execFileSync as L}from"node:child_process";import q from"js-yaml";import{log as N}from"./logger.js";import{EXIT_CODES as v,getErrorMessage as W}from"./errors.js";import{enforceRateLimit as O}from"./rate-limit.js";const X=Z.dirname(F(import.meta.url));function U(){const z=Y.homedir();return Z.join(z,".ppcli","external-clis.yaml")}let $=null;export function loadExternalClis(){if($)return $;const z=new Map,A=Z.resolve(X,"external-clis.yaml");try{if(V.existsSync(A)){const J=V.readFileSync(A,"utf8"),G=q.load(J)||[];for(const K of G)z.set(K.name,K)}}catch(J){N.warn(`Failed to parse built-in external-clis.yaml: ${W(J)}`)}const H=U();try{if(V.existsSync(H)){const J=V.readFileSync(H,"utf8"),G=q.load(J)||[];for(const K of G)z.set(K.name,K)}}catch(J){N.warn(`Failed to parse user external-clis.yaml: ${W(J)}`)}$=Array.from(z.values()).sort((J,G)=>J.name.localeCompare(G.name));return $}export function isBinaryInstalled(z){try{const A=Y.platform()==="win32";L(A?"where":"which",[z],{stdio:"ignore"});return!0}catch{return!1}}export function getInstallCmd(z){if(!z)return null;const A=Y.platform();if(A==="darwin"&&z.mac)return z.mac;if(A==="linux"&&z.linux)return z.linux;if(A==="win32"&&z.windows)return z.windows;if(z.default)return z.default;return null}export function formatExternalCliLabel(z){return z.package&&z.package!==z.name?`${z.name}(${z.package})`:z.name}export function parseCommand(z){if(/&&|\|\|?|;|[><`$#\n\r]|\$\(/.test(z))throw Error(`Install command contains unsafe shell operators and cannot be executed securely: "${z}". Please install the tool manually.`);const H=[],J=/(?:"([^"]*)")|(?:'([^']*)')|(\S+)/g;let G;while((G=J.exec(z))!==null)H.push(G[1]??G[2]??G[3]);if(H.length===0)throw Error("Install command is empty.");const[K,...Q]=H;return{binary:K,args:Q}}function R(z,A){const H=A instanceof Error?A.code:void 0;return Y.platform()==="win32"&&!Z.extname(z)&&H==="ENOENT"}function T(z){const{binary:A,args:H}=parseCommand(z);try{L(A,H,{stdio:"inherit"})}catch(J){if(R(A,J)){L(`${A}.cmd`,H,{stdio:"inherit"});return}throw J}}export function installExternalCli(z){if(!z.install){N.error(`No auto-install command configured for '${z.name}'.`);N.info(`Please install '${z.binary}' manually.`);return!1}const A=getInstallCmd(z.install);if(!A){N.error(`No install command for your platform (${Y.platform()}) for '${z.name}'.`);if(z.homepage)N.info(`See: ${z.homepage}`);return!1}N.info(`'${z.name}' is not installed. Auto-installing...`);N.verbose(`$ ${A}`);try{T(A);N.success(`Installed '${z.name}' successfully.`);return!0}catch(H){N.error(`Failed to install '${z.name}': ${W(H)}`);return!1}}export function executeExternalCli(z,A,H){const G=(H??loadExternalClis()).find((Q)=>Q.name===z);if(!G)throw Error(`External CLI '${z}' not found in registry.`);O(z);if(!isBinaryInstalled(G.binary)){if(!installExternalCli(G)){process.exitCode=v.SERVICE_UNAVAIL;return}}const K=k(G.binary,A);if(K.error){N.error(`Failed to execute '${G.binary}': ${K.error.message}`);process.exitCode=v.GENERIC_ERROR;return}if(K.status!==null)process.exitCode=K.status;else if(K.signal){const Q=K.signal;process.exitCode=Q==="SIGINT"?v.INTERRUPTED:Y.constants.signals[Q]?128+Y.constants.signals[Q]:v.GENERIC_ERROR}}function D(z){if(z!==""&&!/[\s"^&|<>%()]/.test(z))return z;return`"${z.replace(/"/g,'""')}"`}function k(z,A){const H=M(z,A,{stdio:"inherit"}),J=H.error?.code;if(Y.platform()==="win32"&&(J==="EINVAL"||J==="ENOENT")){const G=[z,...A].map(D).join(" ");return M(G,{stdio:"inherit",shell:!0})}return H}export function registerExternalCli(z,A){const H=U(),J=Z.dirname(H);if(!V.existsSync(J))V.mkdirSync(J,{recursive:!0});let G=[];if(V.existsSync(H))try{const B=V.readFileSync(H,"utf8");G=q.load(B)||[]}catch{}const K=G.findIndex((B)=>B.name===z),Q={name:z,binary:A?.binary||z};if(A?.description)Q.description=A.description;if(A?.install)Q.install={default:A.install};if(K>=0){G[K]={...G[K],...Q};N.success(`Updated '${z}' in user registry.`)}else{G.push(Q);N.success(`Registered '${z}' in user registry.`)}const j=q.dump(G,{indent:2,sortKeys:!0});V.writeFileSync(H,j,"utf8");$=null;N.verbose(H)}
|
package/dist/src/types.d.ts
CHANGED
|
@@ -87,6 +87,9 @@ export interface IPage {
|
|
|
87
87
|
}): Promise<{
|
|
88
88
|
matches_n: number;
|
|
89
89
|
match_level: 'exact' | 'stable' | 'reidentified';
|
|
90
|
+
click_method?: 'cdp' | 'js' | 'ax';
|
|
91
|
+
hit?: 'target' | 'ancestor' | 'other' | 'none';
|
|
92
|
+
retargeted?: boolean;
|
|
90
93
|
}>;
|
|
91
94
|
dblClick?(ref: string, opts?: {
|
|
92
95
|
nth?: number;
|