publishport-opencli 1.0.32 → 1.0.34
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 +278 -0
- package/clis/_shared/watch.js +1 -1
- package/clis/taobao/im.js +13 -0
- package/clis/taobao/inbox.js +1 -0
- package/clis/taobao/messages.js +1 -0
- package/clis/taobao/send.js +3 -0
- package/clis/taobao/watch.js +1 -0
- package/clis/xianyu/auth.js +2 -2
- package/clis/xianyu/captcha.js +20 -0
- package/clis/xianyu/chat.js +1 -1
- package/clis/xianyu/inbox.js +1 -1
- package/clis/xianyu/item-stats.js +2 -2
- package/clis/xianyu/item.js +2 -2
- package/clis/xianyu/messages.js +1 -1
- package/clis/xianyu/mtop.js +2 -2
- package/clis/xianyu/promote-stats.js +2 -2
- package/clis/xianyu/publish.js +7 -7
- package/clis/xianyu/reply.js +1 -1
- package/clis/xianyu/search.js +2 -2
- package/clis/xianyu/seller-data.js +1 -1
- package/clis/xianyu/seller-list.js +1 -1
- package/clis/xianyu/system-mouse.js +23 -0
- package/clis/xianyu/watch.js +2 -2
- package/dist/src/build-manifest.js +1 -1
- package/dist/src/discovery.js +2 -2
- package/dist/src/manifest-types.d.ts +2 -0
- package/package.json +1 -1
package/cli-manifest.json
CHANGED
|
@@ -5526,6 +5526,7 @@
|
|
|
5526
5526
|
"sourceFile": "bilibili/watch.js",
|
|
5527
5527
|
"navigateBefore": false,
|
|
5528
5528
|
"siteSession": "persistent",
|
|
5529
|
+
"sessionKey": "watch",
|
|
5529
5530
|
"defaultWindowMode": "background"
|
|
5530
5531
|
},
|
|
5531
5532
|
{
|
|
@@ -18328,6 +18329,7 @@
|
|
|
18328
18329
|
"sourceFile": "github/watch.js",
|
|
18329
18330
|
"navigateBefore": false,
|
|
18330
18331
|
"siteSession": "persistent",
|
|
18332
|
+
"sessionKey": "watch",
|
|
18331
18333
|
"defaultWindowMode": "background"
|
|
18332
18334
|
},
|
|
18333
18335
|
{
|
|
@@ -31381,6 +31383,7 @@
|
|
|
31381
31383
|
"sourceFile": "nowcoder/watch.js",
|
|
31382
31384
|
"navigateBefore": false,
|
|
31383
31385
|
"siteSession": "persistent",
|
|
31386
|
+
"sessionKey": "watch",
|
|
31384
31387
|
"defaultWindowMode": "background"
|
|
31385
31388
|
},
|
|
31386
31389
|
{
|
|
@@ -37152,6 +37155,7 @@
|
|
|
37152
37155
|
"sourceFile": "reddit/watch.js",
|
|
37153
37156
|
"navigateBefore": false,
|
|
37154
37157
|
"siteSession": "persistent",
|
|
37158
|
+
"sessionKey": "watch",
|
|
37155
37159
|
"defaultWindowMode": "background"
|
|
37156
37160
|
},
|
|
37157
37161
|
{
|
|
@@ -41767,6 +41771,56 @@
|
|
|
41767
41771
|
"sourceFile": "taobao/detail.js",
|
|
41768
41772
|
"navigateBefore": false
|
|
41769
41773
|
},
|
|
41774
|
+
{
|
|
41775
|
+
"site": "taobao",
|
|
41776
|
+
"name": "inbox",
|
|
41777
|
+
"description": "列出淘宝旺旺最近的会话(对方 userId、我方是卖家还是买家、最后一条消息、商品 id)",
|
|
41778
|
+
"access": "read",
|
|
41779
|
+
"domain": "market.m.taobao.com",
|
|
41780
|
+
"strategy": "cookie",
|
|
41781
|
+
"browser": true,
|
|
41782
|
+
"args": [
|
|
41783
|
+
{
|
|
41784
|
+
"name": "limit",
|
|
41785
|
+
"type": "int",
|
|
41786
|
+
"default": 20,
|
|
41787
|
+
"required": false,
|
|
41788
|
+
"help": "返回多少个会话(最多 100)"
|
|
41789
|
+
},
|
|
41790
|
+
{
|
|
41791
|
+
"name": "as-seller",
|
|
41792
|
+
"type": "bool",
|
|
41793
|
+
"default": false,
|
|
41794
|
+
"required": false,
|
|
41795
|
+
"help": "只看买家找店铺的会话(我方是卖家)"
|
|
41796
|
+
},
|
|
41797
|
+
{
|
|
41798
|
+
"name": "unread-only",
|
|
41799
|
+
"type": "bool",
|
|
41800
|
+
"default": false,
|
|
41801
|
+
"required": false,
|
|
41802
|
+
"help": "只看有未读的会话"
|
|
41803
|
+
}
|
|
41804
|
+
],
|
|
41805
|
+
"columns": [
|
|
41806
|
+
"rank",
|
|
41807
|
+
"peer_nick",
|
|
41808
|
+
"peer_user_id",
|
|
41809
|
+
"self_role",
|
|
41810
|
+
"last_message",
|
|
41811
|
+
"last_from",
|
|
41812
|
+
"last_time",
|
|
41813
|
+
"unread",
|
|
41814
|
+
"item_id",
|
|
41815
|
+
"conversation_id"
|
|
41816
|
+
],
|
|
41817
|
+
"type": "js",
|
|
41818
|
+
"modulePath": "taobao/inbox.js",
|
|
41819
|
+
"sourceFile": "taobao/inbox.js",
|
|
41820
|
+
"navigateBefore": false,
|
|
41821
|
+
"sessionKey": "watch",
|
|
41822
|
+
"defaultWindowMode": "background"
|
|
41823
|
+
},
|
|
41770
41824
|
{
|
|
41771
41825
|
"site": "taobao",
|
|
41772
41826
|
"name": "login",
|
|
@@ -41798,6 +41852,46 @@
|
|
|
41798
41852
|
"siteSession": "persistent",
|
|
41799
41853
|
"defaultWindowMode": "foreground"
|
|
41800
41854
|
},
|
|
41855
|
+
{
|
|
41856
|
+
"site": "taobao",
|
|
41857
|
+
"name": "messages",
|
|
41858
|
+
"description": "读取淘宝旺旺某个会话最近的聊天记录(按会话 id 或对方 userId)",
|
|
41859
|
+
"access": "read",
|
|
41860
|
+
"domain": "market.m.taobao.com",
|
|
41861
|
+
"strategy": "cookie",
|
|
41862
|
+
"browser": true,
|
|
41863
|
+
"args": [
|
|
41864
|
+
{
|
|
41865
|
+
"name": "target",
|
|
41866
|
+
"type": "str",
|
|
41867
|
+
"required": true,
|
|
41868
|
+
"positional": true,
|
|
41869
|
+
"help": "会话 id(…#11001@cntaobao)或对方 userId"
|
|
41870
|
+
},
|
|
41871
|
+
{
|
|
41872
|
+
"name": "limit",
|
|
41873
|
+
"type": "int",
|
|
41874
|
+
"default": 20,
|
|
41875
|
+
"required": false,
|
|
41876
|
+
"help": "最近多少条(最多 50)"
|
|
41877
|
+
}
|
|
41878
|
+
],
|
|
41879
|
+
"columns": [
|
|
41880
|
+
"index",
|
|
41881
|
+
"time",
|
|
41882
|
+
"from",
|
|
41883
|
+
"sender_nick",
|
|
41884
|
+
"text",
|
|
41885
|
+
"item_id",
|
|
41886
|
+
"message_id"
|
|
41887
|
+
],
|
|
41888
|
+
"type": "js",
|
|
41889
|
+
"modulePath": "taobao/messages.js",
|
|
41890
|
+
"sourceFile": "taobao/messages.js",
|
|
41891
|
+
"navigateBefore": false,
|
|
41892
|
+
"sessionKey": "watch",
|
|
41893
|
+
"defaultWindowMode": "background"
|
|
41894
|
+
},
|
|
41801
41895
|
{
|
|
41802
41896
|
"site": "taobao",
|
|
41803
41897
|
"name": "reviews",
|
|
@@ -41885,6 +41979,153 @@
|
|
|
41885
41979
|
"sourceFile": "taobao/search.js",
|
|
41886
41980
|
"navigateBefore": false
|
|
41887
41981
|
},
|
|
41982
|
+
{
|
|
41983
|
+
"site": "taobao",
|
|
41984
|
+
"name": "send",
|
|
41985
|
+
"description": "给淘宝旺旺里已有的会话发一条文字消息,发完回读确认送达(--dry-run 只核对会话和发送通道,不发)",
|
|
41986
|
+
"access": "write",
|
|
41987
|
+
"domain": "market.m.taobao.com",
|
|
41988
|
+
"strategy": "cookie",
|
|
41989
|
+
"browser": true,
|
|
41990
|
+
"args": [
|
|
41991
|
+
{
|
|
41992
|
+
"name": "target",
|
|
41993
|
+
"type": "str",
|
|
41994
|
+
"required": true,
|
|
41995
|
+
"positional": true,
|
|
41996
|
+
"help": "会话 id(…#11001@cntaobao)或对方 userId"
|
|
41997
|
+
},
|
|
41998
|
+
{
|
|
41999
|
+
"name": "text",
|
|
42000
|
+
"type": "str",
|
|
42001
|
+
"required": false,
|
|
42002
|
+
"help": "消息正文(或用 --file 指向正文文件)",
|
|
42003
|
+
"fromFile": true
|
|
42004
|
+
},
|
|
42005
|
+
{
|
|
42006
|
+
"name": "dry-run",
|
|
42007
|
+
"type": "bool",
|
|
42008
|
+
"default": false,
|
|
42009
|
+
"required": false,
|
|
42010
|
+
"help": "只核对会话、登录和发送通道,不发送"
|
|
42011
|
+
},
|
|
42012
|
+
{
|
|
42013
|
+
"name": "timeout",
|
|
42014
|
+
"type": "int",
|
|
42015
|
+
"default": 20,
|
|
42016
|
+
"required": false,
|
|
42017
|
+
"help": "等送达回读的秒数"
|
|
42018
|
+
},
|
|
42019
|
+
{
|
|
42020
|
+
"name": "file",
|
|
42021
|
+
"type": "str",
|
|
42022
|
+
"required": false,
|
|
42023
|
+
"help": "从本机文件读取text(UTF-8),与 --text 二选一。长正文建议走文件:命令行上只剩短参数,不受 shell 引号 / 换行 / 编码影响",
|
|
42024
|
+
"generatedFileFor": "text"
|
|
42025
|
+
}
|
|
42026
|
+
],
|
|
42027
|
+
"columns": [
|
|
42028
|
+
"ok",
|
|
42029
|
+
"status",
|
|
42030
|
+
"reason",
|
|
42031
|
+
"peer_nick",
|
|
42032
|
+
"message_id",
|
|
42033
|
+
"tail"
|
|
42034
|
+
],
|
|
42035
|
+
"type": "js",
|
|
42036
|
+
"modulePath": "taobao/send.js",
|
|
42037
|
+
"sourceFile": "taobao/send.js",
|
|
42038
|
+
"navigateBefore": false,
|
|
42039
|
+
"sessionKey": "watch",
|
|
42040
|
+
"defaultWindowMode": "background"
|
|
42041
|
+
},
|
|
42042
|
+
{
|
|
42043
|
+
"site": "taobao",
|
|
42044
|
+
"name": "watch",
|
|
42045
|
+
"description": "淘宝旺旺私信监听:旁听网页版旺旺自己的 WebSocket 推送,一有新消息就读已打开的会话,按 NDJSON 逐条输出(带对方 userId、商品 id、我方是买家还是卖家)",
|
|
42046
|
+
"access": "read",
|
|
42047
|
+
"domain": "market.m.taobao.com",
|
|
42048
|
+
"strategy": "cookie",
|
|
42049
|
+
"browser": true,
|
|
42050
|
+
"args": [
|
|
42051
|
+
{
|
|
42052
|
+
"name": "interval",
|
|
42053
|
+
"type": "int",
|
|
42054
|
+
"default": 1000,
|
|
42055
|
+
"required": false,
|
|
42056
|
+
"help": "本机取帧间隔毫秒(默认 1000)。纯本机 IPC,不发平台请求"
|
|
42057
|
+
},
|
|
42058
|
+
{
|
|
42059
|
+
"name": "reconcile-interval",
|
|
42060
|
+
"type": "int",
|
|
42061
|
+
"default": 0,
|
|
42062
|
+
"required": false,
|
|
42063
|
+
"help": "对账间隔秒(0 = 用该平台的默认值:本平台 60 秒);-1 = 彻底关闭对账"
|
|
42064
|
+
},
|
|
42065
|
+
{
|
|
42066
|
+
"name": "heartbeat",
|
|
42067
|
+
"type": "int",
|
|
42068
|
+
"default": 30,
|
|
42069
|
+
"required": false,
|
|
42070
|
+
"help": "心跳间隔秒(默认 30),无事件时输出一行 status 表示还活着"
|
|
42071
|
+
},
|
|
42072
|
+
{
|
|
42073
|
+
"name": "events",
|
|
42074
|
+
"type": "str",
|
|
42075
|
+
"required": false,
|
|
42076
|
+
"help": "只输出这些事件类型,逗号分隔(如 dm,comment);默认全部"
|
|
42077
|
+
},
|
|
42078
|
+
{
|
|
42079
|
+
"name": "once",
|
|
42080
|
+
"type": "bool",
|
|
42081
|
+
"default": false,
|
|
42082
|
+
"required": false,
|
|
42083
|
+
"help": "只跑一轮对账后退出,不长驻。用于验证解析器和补历史"
|
|
42084
|
+
},
|
|
42085
|
+
{
|
|
42086
|
+
"name": "replay-seen",
|
|
42087
|
+
"type": "bool",
|
|
42088
|
+
"default": false,
|
|
42089
|
+
"required": false,
|
|
42090
|
+
"help": "忽略去重表,把解析到的事件全部重新输出(调试用,会导致重复触发)"
|
|
42091
|
+
},
|
|
42092
|
+
{
|
|
42093
|
+
"name": "backfill",
|
|
42094
|
+
"type": "bool",
|
|
42095
|
+
"default": false,
|
|
42096
|
+
"required": false,
|
|
42097
|
+
"help": "首次运行时把已存在的历史消息也当成新事件输出(默认只静默登记,不触发)"
|
|
42098
|
+
},
|
|
42099
|
+
{
|
|
42100
|
+
"name": "dump-frames",
|
|
42101
|
+
"type": "bool",
|
|
42102
|
+
"default": false,
|
|
42103
|
+
"required": false,
|
|
42104
|
+
"help": "侦察模式:把收到的每一条原始帧(url / 通道 / 内容片段)也打出来,用来给新平台摸清事件通道"
|
|
42105
|
+
},
|
|
42106
|
+
{
|
|
42107
|
+
"name": "include-own",
|
|
42108
|
+
"type": "bool",
|
|
42109
|
+
"default": false,
|
|
42110
|
+
"required": false,
|
|
42111
|
+
"help": "连自己发出去的消息也当成事件(默认不算——否则自动回复会追着自己的回复自我触发)。调试解析用"
|
|
42112
|
+
}
|
|
42113
|
+
],
|
|
42114
|
+
"columns": [
|
|
42115
|
+
"event",
|
|
42116
|
+
"id",
|
|
42117
|
+
"sender",
|
|
42118
|
+
"text",
|
|
42119
|
+
"ts"
|
|
42120
|
+
],
|
|
42121
|
+
"type": "js",
|
|
42122
|
+
"modulePath": "taobao/watch.js",
|
|
42123
|
+
"sourceFile": "taobao/watch.js",
|
|
42124
|
+
"navigateBefore": false,
|
|
42125
|
+
"siteSession": "persistent",
|
|
42126
|
+
"sessionKey": "watch",
|
|
42127
|
+
"defaultWindowMode": "background"
|
|
42128
|
+
},
|
|
41888
42129
|
{
|
|
41889
42130
|
"site": "taobao",
|
|
41890
42131
|
"name": "whoami",
|
|
@@ -46753,6 +46994,7 @@
|
|
|
46753
46994
|
"sourceFile": "twitter/watch.js",
|
|
46754
46995
|
"navigateBefore": false,
|
|
46755
46996
|
"siteSession": "persistent",
|
|
46997
|
+
"sessionKey": "watch",
|
|
46756
46998
|
"defaultWindowMode": "background"
|
|
46757
46999
|
},
|
|
46758
47000
|
{
|
|
@@ -47528,6 +47770,7 @@
|
|
|
47528
47770
|
"sourceFile": "v2ex/watch.js",
|
|
47529
47771
|
"navigateBefore": false,
|
|
47530
47772
|
"siteSession": "persistent",
|
|
47773
|
+
"sessionKey": "watch",
|
|
47531
47774
|
"defaultWindowMode": "background"
|
|
47532
47775
|
},
|
|
47533
47776
|
{
|
|
@@ -48452,6 +48695,7 @@
|
|
|
48452
48695
|
"sourceFile": "weibo/watch.js",
|
|
48453
48696
|
"navigateBefore": false,
|
|
48454
48697
|
"siteSession": "persistent",
|
|
48698
|
+
"sessionKey": "watch",
|
|
48455
48699
|
"defaultWindowMode": "background"
|
|
48456
48700
|
},
|
|
48457
48701
|
{
|
|
@@ -50319,6 +50563,37 @@
|
|
|
50319
50563
|
"modulePath": "wttr/forecast.js",
|
|
50320
50564
|
"sourceFile": "wttr/forecast.js"
|
|
50321
50565
|
},
|
|
50566
|
+
{
|
|
50567
|
+
"site": "xianyu",
|
|
50568
|
+
"name": "captcha",
|
|
50569
|
+
"description": "检测闲鱼滑块验证码,自动拖动并确认验证已解除",
|
|
50570
|
+
"access": "write",
|
|
50571
|
+
"domain": "www.goofish.com",
|
|
50572
|
+
"strategy": "cookie",
|
|
50573
|
+
"browser": true,
|
|
50574
|
+
"args": [
|
|
50575
|
+
{
|
|
50576
|
+
"name": "input",
|
|
50577
|
+
"type": "str",
|
|
50578
|
+
"default": "auto",
|
|
50579
|
+
"required": false,
|
|
50580
|
+
"help": "鼠标方式:auto 自动选择,system 系统鼠标,cdp 浏览器鼠标",
|
|
50581
|
+
"choices": [
|
|
50582
|
+
"auto",
|
|
50583
|
+
"system",
|
|
50584
|
+
"cdp"
|
|
50585
|
+
]
|
|
50586
|
+
}
|
|
50587
|
+
],
|
|
50588
|
+
"columns": [
|
|
50589
|
+
"status",
|
|
50590
|
+
"attempts"
|
|
50591
|
+
],
|
|
50592
|
+
"type": "js",
|
|
50593
|
+
"modulePath": "xianyu/auth.js",
|
|
50594
|
+
"sourceFile": "xianyu/auth.js",
|
|
50595
|
+
"navigateBefore": false
|
|
50596
|
+
},
|
|
50322
50597
|
{
|
|
50323
50598
|
"site": "xianyu",
|
|
50324
50599
|
"name": "chat",
|
|
@@ -51409,6 +51684,7 @@
|
|
|
51409
51684
|
"sourceFile": "xianyu/watch.js",
|
|
51410
51685
|
"navigateBefore": false,
|
|
51411
51686
|
"siteSession": "persistent",
|
|
51687
|
+
"sessionKey": "watch",
|
|
51412
51688
|
"defaultWindowMode": "background"
|
|
51413
51689
|
},
|
|
51414
51690
|
{
|
|
@@ -53290,6 +53566,7 @@
|
|
|
53290
53566
|
"sourceFile": "xiaohongshu/watch.js",
|
|
53291
53567
|
"navigateBefore": false,
|
|
53292
53568
|
"siteSession": "persistent",
|
|
53569
|
+
"sessionKey": "watch",
|
|
53293
53570
|
"defaultWindowMode": "background"
|
|
53294
53571
|
},
|
|
53295
53572
|
{
|
|
@@ -56919,6 +57196,7 @@
|
|
|
56919
57196
|
"sourceFile": "zhihu/watch.js",
|
|
56920
57197
|
"navigateBefore": false,
|
|
56921
57198
|
"siteSession": "persistent",
|
|
57199
|
+
"sessionKey": "watch",
|
|
56922
57200
|
"defaultWindowMode": "background"
|
|
56923
57201
|
},
|
|
56924
57202
|
{
|
package/clis/_shared/watch.js
CHANGED
|
@@ -56,4 +56,4 @@ import{existsSync as ee,mkdirSync as te,readFileSync as re,writeFileSync as ne}f
|
|
|
56
56
|
WrappedES.prototype = NativeES.prototype;
|
|
57
57
|
window.EventSource = WrappedES;
|
|
58
58
|
}
|
|
59
|
-
})()`}async function me(e){let t;try{t=await e.evaluate("(function(){ var o = window.__pp_stream || []; window.__pp_stream = []; return JSON.stringify(o); })()")}catch{return[]}let r;try{r=JSON.parse(typeof t==="string"?t:"[]")}catch{return[]}if(!Array.isArray(r))return[];return r.map((i)=>({kind:"page-hook",channel:i.channel,direction:i.direction,url:i.url,method:i.channel==="websocket"?"WS":"SSE",eventName:i.eventName||void 0,responseStatus:200,responseContentType:i.binary?"application/octet-stream":"text/plain",responsePreview:i.binary?"":String(i.data??""),timestamp:i.ts||Date.now()}))}export function registerWatch(e){if(!e?.site||!e?.domain||!e?.watchUrl||typeof e.parse!=="function")throw Error("registerWatch 需要 site、domain、watchUrl 和 parse");const t=e.name??"watch",r=e.site,i=Q(e.watchUrl);se({site:r,name:t,access:"read",description:e.description??`长驻监听${r}的新消息,旁听页面自身的实时通道并按 NDJSON 逐条输出(不对平台发轮询请求)`,domain:e.domain,strategy:oe.COOKIE,browser:!0,navigateBefore:!1,sessionKey:"watch",siteSession:"persistent",defaultWindowMode:"background",args:[{name:"interval",type:"int",required:!1,default:P,help:`本机取帧间隔毫秒(默认 ${P})。纯本机 IPC,不发平台请求`},{name:"reconcile-interval",type:"int",required:!1,default:0,help:`对账间隔秒(0 =
|
|
59
|
+
})()`}async function me(e){let t;try{t=await e.evaluate("(function(){ var o = window.__pp_stream || []; window.__pp_stream = []; return JSON.stringify(o); })()")}catch{return[]}let r;try{r=JSON.parse(typeof t==="string"?t:"[]")}catch{return[]}if(!Array.isArray(r))return[];return r.map((i)=>({kind:"page-hook",channel:i.channel,direction:i.direction,url:i.url,method:i.channel==="websocket"?"WS":"SSE",eventName:i.eventName||void 0,responseStatus:200,responseContentType:i.binary?"application/octet-stream":"text/plain",responsePreview:i.binary?"":String(i.data??""),timestamp:i.ts||Date.now()}))}export function registerWatch(e){if(!e?.site||!e?.domain||!e?.watchUrl||typeof e.parse!=="function")throw Error("registerWatch 需要 site、domain、watchUrl 和 parse");const t=e.name??"watch",r=e.site,i=Q(e.watchUrl);se({site:r,name:t,access:"read",description:e.description??`长驻监听${r}的新消息,旁听页面自身的实时通道并按 NDJSON 逐条输出(不对平台发轮询请求)`,domain:e.domain,strategy:oe.COOKIE,browser:!0,navigateBefore:!1,sessionKey:e.sessionKey??"watch",siteSession:"persistent",defaultWindowMode:"background",args:[{name:"interval",type:"int",required:!1,default:P,help:`本机取帧间隔毫秒(默认 ${P})。纯本机 IPC,不发平台请求`},{name:"reconcile-interval",type:"int",required:!1,default:0,help:`对账间隔秒(0 = 用该平台的默认值:${Number(e.reconcileSeconds)>0?`本平台 ${Number(e.reconcileSeconds)} 秒`:`可旁听实时通道的平台 ${B} 秒兜底,只能靠拉取的平台 ${j} 秒一轮`});-1 = 彻底关闭对账`},{name:"heartbeat",type:"int",required:!1,default:D,help:`心跳间隔秒(默认 ${D}),无事件时输出一行 status 表示还活着`},{name:"events",type:"str",required:!1,help:"只输出这些事件类型,逗号分隔(如 dm,comment);默认全部"},{name:"once",type:"bool",required:!1,default:!1,help:"只跑一轮对账后退出,不长驻。用于验证解析器和补历史"},{name:"replay-seen",type:"bool",required:!1,default:!1,help:"忽略去重表,把解析到的事件全部重新输出(调试用,会导致重复触发)"},{name:"backfill",type:"bool",required:!1,default:!1,help:"首次运行时把已存在的历史消息也当成新事件输出(默认只静默登记,不触发)"},{name:"dump-frames",type:"bool",required:!1,default:!1,help:"侦察模式:把收到的每一条原始帧(url / 通道 / 内容片段)也打出来,用来给新平台摸清事件通道"},{name:"include-own",type:"bool",required:!1,default:!1,help:"连自己发出去的消息也当成事件(默认不算——否则自动回复会追着自己的回复自我触发)。调试解析用"}],columns:["event","id","sender","text","ts"],func:async(o,c)=>{const X=Math.max(200,Number(c.interval??P)),k=e.mode==="reconcile"?"reconcile":"stream",b=Number(c["reconcile-interval"]??0),_=b<0?0:b>0?b:Number(e.reconcileSeconds)>0?Number(e.reconcileSeconds):k==="reconcile"?j:B,U=Math.max(0,Number(c.heartbeat??D)),x=c.once===!0||c.once==="true",S=c["replay-seen"]===!0||c["replay-seen"]==="true",T=c.backfill===!0||c.backfill==="true",Y=c["dump-frames"]===!0||c["dump-frames"]==="true",h=String(c.events??"").split(",").map((l)=>l.trim()).filter(Boolean);if(typeof o.startNetworkCapture!=="function"||typeof o.readNetworkCapture!=="function")throw new ce("当前浏览器通道不支持网络捕获,watch 无法工作(需要 CDP 或扩展路径)");const d=de(r,t),C={site:r,page:o,state:{},includeOwn:c["include-own"]===!0||c["include-own"]==="true"};let p=0,W=0,y=!1;const O=[];let m=d.firstRun&&!T&&!S,I=!1;const w=d.lastTs,F=(l)=>{const a=K(r,l);if(!a)return!1;if(h.length&&!h.includes(a.event))return!1;if(!S&&!fe(d,`${a.event}:${a.id}`))return!1;if(a.ts>d.lastTs)d.lastTs=a.ts;if(m){W++;return!1}if(!T&&!S&&w>0&&a.ts>0&&a.ts<=w){W++;return!1}if(!x)R(a);O.push(a);p++;return!0},Z=(l)=>{let a=0;for(const n of l){const s=K(r,n);if(!s)continue;if(h.length&&!h.includes(s.event))continue;if(d.set.has(`${s.event}:${s.id}`))continue;if(w>0&&s.ts>0&&s.ts<=w)continue;a++}return a},v=async(l,a=!1)=>{if(typeof e.reconcile!=="function")return;try{const n=V(await e.reconcile(o,C));if(a&&!m&&!T&&!S&&Z(n)>le){m=!0;I=!0}let s=0;for(const f of n)if(F(f))s++;u(r,"reconciled",{reason:l,scanned:n.length,new:s})}catch(n){if(n instanceof J)throw n;u(r,"reconcile_failed",{reason:l,error:n?.message??String(n)})}},g=async(l)=>{let a=!1;try{a=await o.startNetworkCapture(e.pattern??"")!==!1}catch{a=!1}const n=pe(e.pattern??"");y=!1;let s=!1;if(typeof o.cdp==="function")try{await o.cdp("Page.enable",{});await o.cdp("Page.addScriptToEvaluateOnNewDocument",{source:n});s=!0;y=!0}catch{}await o.goto(e.watchUrl,{waitUntil:s?"load":"none"});if(!s)try{await o.evaluate(n);y=!0;u(r,"page_hook_late",{note:"浏览器扩展版本较旧,旁听脚本只能在导航后注入;页面在此之前已建立的连接要等它重连才能被看到"})}catch(f){u(r,"page_hook_unavailable",{error:f?.message??String(f)})}if(!a){await o.startNetworkCapture(e.pattern??"");await o.goto(e.watchUrl,{waitUntil:"load"})}if(typeof e.onAttach==="function")await e.onAttach(o,C);u(r,"watching",{reason:l,mode:k,url:e.watchUrl,pattern:e.pattern??"",reconcile_seconds:_})};await g("start");await v("startup",!0);if(m){E(r,t,d);u(r,"seeded",{seeded:W,desynced:I,note:I?"去重表和收件箱对不上(上次没跑完 / 换了账号 / 表被裁剪),本轮把当前收件箱重新登记为「已见过」,不触发任何事件。从现在起只有新消息才会上报":"首次运行:已把当前收件箱登记为「已见过」,不触发任何事件。从现在起只有新消息才会上报(要补历史请加 --backfill)"});m=!1}if(x){E(r,t,d);return O}let A=!1;const N=()=>{A=!0};process.on("SIGINT",N);process.on("SIGTERM",N);let M=Date.now(),L=Date.now(),q=p;try{while(!A){await new Promise((n)=>setTimeout(n,X));if(A)break;let l=[];try{l=await o.readNetworkCapture()??[];if(y)l=l.concat(await me(o))}catch(n){u(r,"reattaching",{error:n?.message??String(n)});try{await g("capture_lost");await v("reattach")}catch(s){if(s instanceof J)throw s;u(r,"reattach_failed",{error:s?.message??String(s)})}continue}for(const n of l){if(Y)R({type:"frame",site:r,via:n?.kind==="page-hook"?"page-hook":"cdp",channel:n?.channel??"http",direction:n?.direction??"",method:n?.method??"",url:String(n?.url??"").slice(0,200),size:n?.responseBodyFullSize??String(n?.responsePreview??"").length,preview:String(n?.responsePreview??"").slice(0,600)});if(n?.direction==="sent")continue;let s;try{s=await e.parse(n,C)}catch(f){u(r,"parse_failed",{url:n?.url??"",error:f?.message??String(f)});continue}for(const f of V(s))F(f)}if(p!==q){E(r,t,d);q=p;L=Date.now()}if(i&&typeof o.getCurrentUrl==="function")try{const n=await o.getCurrentUrl();if(n&&Q(n)!==i){u(r,"page_drifted",{url:n});await g("drift");await v("drift")}}catch{}const a=Date.now();if(_&&a-M>=_*1000){M=a;await v("interval")}if(U&&a-L>=U*1000){L=a;u(r,"alive",{emitted:p})}}}finally{process.off("SIGINT",N);process.off("SIGTERM",N);E(r,t,d);u(r,"stopped",{emitted:p})}return O}})}export function frameJson(e){const t=e?.responsePreview;if(typeof t!=="string"||!t)return null;if(t.startsWith("base64:"))return null;try{return JSON.parse(t)}catch{return null}}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import{ArgumentError as C,AuthRequiredError as E,CommandExecutionError as b}from"@jackwener/opencli/errors";export const IM_URL="https://market.m.taobao.com/app/im/chat/index.html",IM_DOMAIN="market.m.taobao.com",IM_SESSION_KEY="watch";const L=(t)=>new Promise((a)=>setTimeout(a,t));function v(){const t=(e)=>{if(e&&typeof e==="object")return e;if(typeof e!=="string"||!e)return null;try{return JSON.parse(e)}catch(m){return null}},a=Array.from(document.querySelectorAll("iframe")).find((e)=>/chat-core/.test(e.src||""));let s=null;try{s=a?a.contentWindow:null;if(s)s.document}catch(e){s=null}const c=s?s.document:null,f=s?s._imsdk:null,_=()=>{if(!c)return null;const e=(o)=>{const g=o&&Object.keys(o).find((n)=>n.startsWith("__reactFiber"));let i=g?o[g]:null;for(let n=0;i&&n<40;n++,i=i.return){const d=i.memoizedProps;if(d&&d.dataManager&&d.dataManager.store)return d.dataManager}return null},m=c.querySelector(".conversation-item"),r=m?e(m):null;if(r)return r;const u=Array.from(c.querySelectorAll("body > div"));for(const o of u){const g=Object.keys(o).find((n)=>n.startsWith("__reactContainer"));if(!g)continue;const i=[o[g]];for(let n=0;n<i.length&&n<20000;n++){const d=i[n],y=d&&d.memoizedProps;if(y&&y.dataManager&&y.dataManager.store)return y.dataManager;if(d&&d.child)i.push(d.child);if(d&&d.sibling)i.push(d.sibling)}}return null},p=()=>{try{return String(f.getAuthService().getLoginUid()||"")}catch(e){return""}},l=(e)=>{const m={},r=/"?(\d+)"?\s*:\s*"(\w+)"/g;let u;while(u=r.exec(String(e||"")))m[u[1]]=u[2];return m},x=(e,m)=>{const r=e&&e.message?e.message:e;if(!r||!r.messageId)return null;const u=r.extension||{},o=r.content||{},g=o.contentType,i=o.custom||null;let n="";if(o.text&&typeof o.text.content==="string")n=o.text.content;else if(i)n=String(i.summary||i.title||"");else if(o.image||g===2)n="[图片]";else if(o.audio||g===3)n="[语音]";else if(o.video)n="[视频]";n=String(n).replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f]/g,"").trim();if(!n)n=String(r.searchableContent&&r.searchableContent.summary||"")||`[消息类型 ${g}]`;const d=String(r.sender&&r.sender.uid||"").split("@")[0],y=o.text&&o.text.extension||{},w=t(u.bizDataExt)||t(y.bizDataExt)||{},N=t(w.template_material_summary)||{},A=t(w.enter_shop_info)||{};let k=String(N.itemId||A.itemId||""),D=k?"card":"";if(!k){const O=JSON.stringify(o).match(/(?:item\.taobao\.com|detail\.tmall\.com|item\.htm)[^"\s]*?[?&]id=(\d{6,})/);if(O){k=O[1];D="link"}}return{message_id:String(r.messageId),ts:Number(r.createAt||0),cid:String(r.cid||""),sender_id:d,sender_nick:String(u.sender_nick||"").replace(/^cntaobao/,""),from_self:!!m&&d===m,content_type:g==null?null:Number(g),custom_type:i&&i.type!=null?Number(i.type):null,text:n,item_id:k,item_source:D,order_id:String(N.orderId||A.orderId||""),status:e&&e.msgStatus!=null?Number(e.msgStatus):null}},S=(e,m)=>{const r=e.conversationContent||{},u=r.lastMessageSummary||{},o=u.messageData&&u.messageData.originPaasMessageData;let g=o?x(o,m):null;if(!g&&u.code&&u.code.messageId){const w=String(u.sender&&u.sender.targetId||"");g={message_id:String(u.code.messageId),ts:Number(u.sendTime||0),cid:String(e.conversationCode||""),sender_id:w,sender_nick:"",from_self:!!m&&w===m,content_type:null,custom_type:null,text:String(u.content||""),item_id:"",item_source:"",order_id:"",status:null}}const i=e.ext||{},n=t(i.target)||{},d=l(i.accountRoleMap),y=String(e.conversationIdentifier&&e.conversationIdentifier.target&&e.conversationIdentifier.target.targetId||n.id||"");return{cid:String(e.conversationCode||""),peer_id:y,peer_nick:String(r.conversationName||n.dnick||""),self_role:d[m]||"",peer_role:d[y]||"",unread:Number(r.unReadNumber||0),modify_time:Number(e.modifyTime||0),last:g}};return{win:s,doc:c,sdk:f,parseJson:t,normMsg:x,normConv:S,readState:()=>{const e=String(location.href||""),m=c&&c.body?c.body.innerText:"",r={href:e,login_page:/login\.taobao\.com|login\.tmall\.com/.test(e),frame:!!c,sdk:!!f,login:!1,connection:null,self_id:"",self_nick:"",disconnected:/连接断开|连接中断/.test(m),store:!1,conversations:[]};if(!f)return r;try{const n=f.getAuthService();r.login=!!n.isLogin();r.connection=n.connectionStatus;r.self_nick=String(n.getLoginNick()||"")}catch(n){}r.self_id=p();const u=_();if(!u)return r;r.store=!0;const o=u.store,g=Array.isArray(o.conversationIdList)?o.conversationIdList:[],i=o.conversationMap&&o.conversationMap.store;for(const n of g){const d=i&&typeof i.get==="function"?i.get(n):null;if(d)r.conversations.push(S(d,r.self_id))}return r},selfId:p,findDataManager:_}}function I(t,a={}){return`(() => { const L = (${v.toString()})(); return (${t.toString()})(L, ${JSON.stringify(a)}); })()`}function M(t,a){if(!t||typeof t!=="object"||Array.isArray(t))throw new b(`淘宝旺旺 ${a}:页面返回的数据不对(${JSON.stringify(t)?.slice(0,200)})`);return t}export async function readState(t){return M(await t.evaluate(I((a)=>a.readState())),"readState")}export async function fetchMessages(t,a,s=10){const c=`(async () => { const L = (${v.toString()})(); const a = ${JSON.stringify({cid:a,count:s})};
|
|
2
|
+
if (!L.sdk) return { error: 'IM SDK 不在(页面没加载完或不是旺旺页)' };
|
|
3
|
+
const me = L.selfId();
|
|
4
|
+
try {
|
|
5
|
+
const r = await L.sdk.getOriginSdk().getMsgService().listPrevMsgs(a.cid, String(Number.MAX_SAFE_INTEGER), a.count);
|
|
6
|
+
const list = ((r && r.userMessageModels) || []).map((um) => L.normMsg(um, me)).filter(Boolean);
|
|
7
|
+
list.sort((x, y) => x.ts - y.ts);
|
|
8
|
+
return { messages: list, has_more: !!(r && r.hasMore) };
|
|
9
|
+
} catch (e) {
|
|
10
|
+
let detail = '';
|
|
11
|
+
try { detail = JSON.stringify(e); } catch (x) { detail = String(e); }
|
|
12
|
+
return { error: (e && e.message) || detail.slice(0, 500) || String(e) };
|
|
13
|
+
} })()`;return M(await t.evaluate(c),"listPrevMsgs")}export async function ensureImPage(t,{timeoutMs:a=20000,allowReload:s=!0}={}){let c="";try{c=String(await t.getCurrentUrl?.()||"")}catch{c=""}if(!c.startsWith(IM_URL))await t.goto(IM_URL);let f=!1;const _=()=>Date.now()+a;let p=_(),l=await readState(t);for(;;){if(l.login_page)throw new E("taobao.com","淘宝网页旺旺跳到了登录页:浏览器里的淘宝登录态失效了");if(l.sdk&&l.login&&l.store&&!l.disconnected)return l;if(l.disconnected&&s&&!f){f=!0;await t.goto(IM_URL);p=_()}else if(Date.now()>p)break;await L(500);l=await readState(t)}if(l.disconnected)throw new b("淘宝网页旺旺「连接断开」:同一个账号在别处又开了一个网页旺旺页面(只能有一个在线),刷新后仍然连不上");throw new b(`淘宝网页旺旺 ${Math.round(a/1000)} 秒内没有就绪:${JSON.stringify({frame:l.frame,sdk:l.sdk,login:l.login,connection:l.connection,store:l.store,href:l.href})}`)}export function resolveConversation(t,a){const s=String(a??"").trim();if(!s)throw new C("要给会话 id(如 2208502750464.1-123.1#11001@cntaobao)或对方 userId");if(s.includes("#")){const f=t.conversations.find((_)=>_.cid===s);if(!f)throw new b(`会话 ${s} 不在当前旺旺的会话列表里`);return f}if(!/^\d+$/.test(s))throw new C(`「${s}」既不是会话 id 也不是数字 userId`);const c=t.conversations.find((f)=>f.peer_id===s);if(!c)throw new b(`旺旺会话列表里没有和 userId ${s} 的会话(只回复已有会话,不新开会话)`);return c}export async function startSend(t,a,s,c){const f=I((_,p)=>{if(!_.sdk)return{ok:!1,reason:"IM SDK 不在"};const l=_.win.__pp_tb_send=_.win.__pp_tb_send||{};l[p.token]={state:"sending",at:Date.now()};const x=(S)=>{try{return JSON.parse(JSON.stringify(S))}catch(h){return String(S)}};try{_.sdk.getMessageService().sendMessages({sendMessages:[{conversationCode:p.cid,msgType:101,originalData:{text:p.text},summary:"",ext:{},localExt:{}}],dataCallback:(S)=>{const h=Array.isArray(S)?S[0]:S;l[p.token]={state:"sent",message_id:String(h&&h.code&&h.code.messageId||""),at:Date.now()}},errorCallBack:(S)=>{l[p.token]={state:"failed",error:x(S),at:Date.now()}}})}catch(S){l[p.token]={state:"failed",error:String(S&&S.message||S),at:Date.now()}}return{ok:!0}},{cid:a,text:s,token:c});return M(await t.evaluate(f),"send")}export async function readSendResult(t,a){const s=I((c,f)=>{return{result:(c.win&&c.win.__pp_tb_send||{})[f.token]||null}},{token:a});return M(await t.evaluate(s),"send-result")}export function fmtTime(t){if(!t)return"";return new Date(Number(t)+28800000).toISOString().replace("T"," ").slice(0,19)}export const __test__={pageLib:v,pageCall:I};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cli as i,Strategy as n}from"@jackwener/opencli/registry";import{clampInt as o}from"../_shared/common.js";import{IM_DOMAIN as m,IM_SESSION_KEY as d,ensureImPage as _,fmtTime as f}from"./im.js";i({site:"taobao",name:"inbox",access:"read",description:"列出淘宝旺旺最近的会话(对方 userId、我方是卖家还是买家、最后一条消息、商品 id)",domain:m,strategy:n.COOKIE,browser:!0,navigateBefore:!1,sessionKey:d,defaultWindowMode:"background",args:[{name:"limit",type:"int",default:20,help:"返回多少个会话(最多 100)"},{name:"as-seller",type:"bool",default:!1,help:"只看买家找店铺的会话(我方是卖家)"},{name:"unread-only",type:"bool",default:!1,help:"只看有未读的会话"}],columns:["rank","peer_nick","peer_user_id","self_role","last_message","last_from","last_time","unread","item_id","conversation_id"],func:async(r,t)=>{const l=o(t.limit,20,1,100);let a=(await _(r)).conversations;if(t["as-seller"]===!0||t["as-seller"]==="true")a=a.filter((e)=>e.self_role==="seller");if(t["unread-only"]===!0||t["unread-only"]==="true")a=a.filter((e)=>e.unread>0);return a.slice(0,l).map((e,s)=>({rank:s+1,peer_nick:e.peer_nick,peer_user_id:e.peer_id,self_role:e.self_role,last_message:e.last?.text??"",last_from:e.last?e.last.from_self?"me":"peer":"",last_time:f(e.last?.ts),unread:e.unread,item_id:e.last?.item_id??"",conversation_id:e.cid}))}});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{CommandExecutionError as m}from"@jackwener/opencli/errors";import{cli as d,Strategy as c}from"@jackwener/opencli/registry";import{clampInt as _}from"../_shared/common.js";import{IM_DOMAIN as f,IM_SESSION_KEY as l,ensureImPage as u,fetchMessages as g,fmtTime as p,resolveConversation as I}from"./im.js";d({site:"taobao",name:"messages",access:"read",description:"读取淘宝旺旺某个会话最近的聊天记录(按会话 id 或对方 userId)",domain:f,strategy:c.COOKIE,browser:!0,navigateBefore:!1,sessionKey:l,defaultWindowMode:"background",args:[{name:"target",positional:!0,required:!0,help:"会话 id(…#11001@cntaobao)或对方 userId"},{name:"limit",type:"int",default:20,help:"最近多少条(最多 50)"}],columns:["index","time","from","sender_nick","text","item_id","message_id"],func:async(i,s)=>{const o=_(s.limit,20,1,50),a=await u(i),t=I(a,s.target),r=await g(i,t.cid,o);if(!Array.isArray(r.messages))throw new m(`读会话 ${t.cid} 的消息失败:${r.error||"页面没返回消息列表"}`);return r.messages.map((e,n)=>({index:n+1,time:p(e.ts),from:e.from_self?"me":"peer",sender_nick:e.sender_nick,text:e.text,item_id:e.item_id,message_id:e.message_id,conversation_id:t.cid,peer_user_id:t.peer_id,self_role:t.self_role}))}});
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{ArgumentError as h,CommandExecutionError as k}from"@jackwener/opencli/errors";import{cli as E,Strategy as I}from"@jackwener/opencli/registry";import{IM_DOMAIN as O,IM_SESSION_KEY as v,ensureImPage as N,fetchMessages as c,fmtTime as D,readSendResult as C,resolveConversation as K,startSend as P}from"./im.js";const b=500,x=(t)=>new Promise((o)=>setTimeout(o,t)),A=(t)=>String(t??"").replace(/\s+/g," ").trim();E({site:"taobao",name:"send",access:"write",description:"给淘宝旺旺里已有的会话发一条文字消息,发完回读确认送达(--dry-run 只核对会话和发送通道,不发)",domain:O,strategy:I.COOKIE,browser:!0,navigateBefore:!1,sessionKey:v,defaultWindowMode:"background",args:[{name:"target",positional:!0,required:!0,help:"会话 id(…#11001@cntaobao)或对方 userId"},{name:"text",fromFile:!0,help:"消息正文(或用 --file 指向正文文件)"},{name:"dry-run",type:"bool",default:!1,help:"只核对会话、登录和发送通道,不发送"},{name:"timeout",type:"int",default:20,help:"等送达回读的秒数"}],columns:["ok","status","reason","peer_nick","message_id","tail"],func:async(t,o)=>{const d=String(o.text??"").replace(/\r\n/g,`
|
|
2
|
+
`).trim(),f=o["dry-run"]===!0||o["dry-run"]==="true";if(!d&&!f)throw new h("正文是空的(--text 或 --file)");if(d.length>b)throw new h(`正文 ${d.length} 字,超过旺旺单条上限 ${b} 字,拆成几条发`);const S=await N(t),r=K(S,o.target),l={peer_nick:r.peer_nick,peer_user_id:r.peer_id,self_role:r.self_role,conversation_id:r.cid},m=(e)=>e.slice(-4).map((n)=>`${D(n.ts)} ${n.from_self?"我":"对方"}: ${n.text}`).join(`
|
|
3
|
+
`);if(f){const e=await c(t,r.cid,4);if(!Array.isArray(e.messages))throw new k(`读会话失败:${e.error}`);return[{ok:!0,status:"dry-run",reason:"会话、登录、发送通道都就绪,未发送",message_id:"",tail:m(e.messages),...l}]}const g=`pp${Date.now()}${Math.random().toString(36).slice(2,8)}`,u=Date.now(),_=await P(t,r.cid,d,g);if(!_.ok)throw new k(`发送没发起来:${_.reason}`);const w=u+Math.max(5,Number(o.timeout)||20)*1000;let i=null,s=null,a=[];while(Date.now()<w){await x(1000);i=(await C(t,g)).result||i;if(i?.state==="failed")return[{ok:!1,status:"failed",reason:`平台回发送失败:${JSON.stringify(i.error).slice(0,400)}`,message_id:"",tail:"",...l}];const e=await c(t,r.cid,6);if(!Array.isArray(e.messages))continue;a=e.messages;s=a.find((n)=>n.from_self&&(i?.message_id&&n.message_id===i.message_id||A(n.text)===A(d)&&n.ts>=u-60000));if(s)break}if(!s)return[{ok:!1,status:"unconfirmed",reason:`${Math.round((w-u)/1000)} 秒内没回读到这条消息(发送回调:${i?.state??"无"})`,message_id:i?.message_id||"",tail:m(a),...l}];await x(1500);const y=await c(t,r.cid,6);if(Array.isArray(y.messages))a=y.messages;const M=a.find((e)=>e.message_id===s.message_id)||s,p=a.filter((e)=>e.ts>=s.ts&&e.message_id!==s.message_id).find((e)=>/违规|违禁|发送失败|未发送|不当|拦截|无法发送/.test(e.text));if(M.status===-1)return[{ok:!1,status:"failed",reason:"平台把这条消息标成发送失败",message_id:s.message_id,tail:m(a),...l}];if(p)return[{ok:!1,status:"blocked",reason:`发出后平台提示:${p.text}`,message_id:s.message_id,tail:m(a),...l}];return[{ok:!0,status:"sent",reason:"已送达(回读到这条消息)",message_id:s.message_id,tail:m(a),...l}]}});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{CommandExecutionError as y}from"@jackwener/opencli/errors";import{registerWatch as g}from"../_shared/watch.js";import{IM_DOMAIN as C,IM_SESSION_KEY as S,IM_URL as u,ensureImPage as E,fetchMessages as v,readState as I}from"./im.js";const h=/"lwp"\s*:\s*"\/s\//,k=[300,700,1000,1500],M=10,N=10,T=60;function l(t){return t.map((e)=>`${e.cid}:${e.last?.message_id??""}`).join("|")}function A(t,e,i,n){const s=!!e.sender_id&&e.sender_id!==i.self_id,_=n.state.itemByCid.get(t.cid),a=e.item_id||_?.item_id||"";return{event:"dm",id:e.message_id,ts:e.ts,conversation_id:t.cid,sender:{id:e.sender_id,name:s?e.sender_nick||t.peer_nick:e.sender_nick||i.self_nick},text:e.text,url:u,raw:{cid:t.cid,peer_user_id:t.peer_id,peer_nick:t.peer_nick,self_user_id:i.self_id,self_role:t.self_role,peer_role:t.peer_role,from_peer:s,sender_nick:e.sender_nick,item_id:a,item_id_source:e.item_id?`message:${e.item_source||"card"}`:a?"conversation":"",order_id:e.order_id||"",content_type:e.content_type,custom_type:e.custom_type,unread_count:t.unread}}}function B(t,e,i){for(const n of i){if(!n.item_id)continue;const s=t.state.itemByCid.get(e);if(!s||n.ts>=s.ts)t.state.itemByCid.set(e,{item_id:n.item_id,ts:n.ts})}}async function p(t,e,{fetch:i}){const n=t.state.lastByCid,s=[];let _=0;const a=[];for(const r of e.conversations){const d=r.last;if(!d||!d.message_id)continue;const c=n.get(r.cid);if(c&&c.message_id===d.message_id)continue;let f=[d];if(i&&_<N){_++;const o=await v(t.page,r.cid,M);if(Array.isArray(o.messages)&&o.messages.length)f=o.messages;else if(o.error)a.push(`${r.cid}: ${o.error}`)}B(t,r.cid,f);const m=c?c.ts:t.state.viewTs;for(const o of f){if(i&&m&&o.ts<m)continue;if(!i&&o.message_id!==d.message_id)continue;s.push(A(r,o,e,t))}n.set(r.cid,{message_id:d.message_id,ts:d.ts})}for(const r of e.conversations)if(r.last?.ts&&r.last.ts>t.state.viewTs)t.state.viewTs=r.last.ts;if(a.length&&!s.length)throw new y(`读会话消息失败:${a.join("; ").slice(0,500)}`);return s}function w(t){if(!t.state.lastByCid){t.state.lastByCid=new Map;t.state.itemByCid=new Map;t.state.viewTs=0}}g({site:"taobao",name:"watch",domain:C,description:"淘宝旺旺私信监听:旁听网页版旺旺自己的 WebSocket 推送,一有新消息就读已打开的会话,按 NDJSON 逐条输出(带对方 userId、商品 id、我方是买家还是卖家)",watchUrl:u,pattern:"wss-cntaobao.dingtalk.com",sessionKey:S,reconcileSeconds:T,parse:async(t,e)=>{if(!h.test(String(t?.responsePreview??"")))return null;w(e);const i=Number(t?.timestamp??0);if(i&&e.state.readStartedAt&&i<e.state.readStartedAt)return null;e.state.readStartedAt=Date.now();const n=e.state.fingerprint;let s=null;for(const _ of k){await new Promise((a)=>setTimeout(a,_));s=await I(e.page);if(s.store&&l(s.conversations)!==n)break}if(!s.store||!s.login)return null;e.state.fingerprint=l(s.conversations);return p(e,s,{fetch:e.state.lastByCid.size>0})},reconcile:async(t,e)=>{w(e);const i=await E(t);e.state.fingerprint=l(i.conversations);return p(e,i,{fetch:e.state.lastByCid.size>0})}});
|
package/clis/xianyu/auth.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{AuthRequiredError as
|
|
1
|
+
import{evaluateWithCaptcha as d}from"./captcha.js";import{AuthRequiredError as n,CommandExecutionError as l}from"@jackwener/opencli/errors";import{registerSiteAuthCommands as w}from"../_shared/site-auth.js";async function a(o){return(await o.getCookies({url:"https://www.goofish.com"})).some((t)=>(t.name==="unb"||t.name==="tracknick")&&t.value)}async function h(o){if(!await a(o))throw new n("goofish.com","Xianyu unb/tracknick cookie missing — anonymous");await o.goto("https://www.goofish.com/personal");await o.wait(2);const e=await d(o,"location.href");if(/passport\.(taobao|goofish)\.com\/(member\/login|login)/.test(String(e||"")))throw new n("goofish.com",`Xianyu /personal redirected to login: ${e}`);const t=await o.getCookies({url:"https://www.goofish.com"}),r=t.find((s)=>s.name==="tracknick")?.value||"",k=t.find((s)=>s.name==="unb")?.value||"",i=await d(o,`
|
|
2
2
|
(() => {
|
|
3
3
|
const bodyText = document.body?.innerText || '';
|
|
4
4
|
const requiresAuth = /请先登录|登录后/.test(bodyText);
|
|
@@ -8,4 +8,4 @@ import{AuthRequiredError as t,CommandExecutionError as k}from"@jackwener/opencli
|
|
|
8
8
|
const userIdMatch = html.match(/['"]?userId['"]?\\s*[:=]\\s*['"]?(\\d+)/i);
|
|
9
9
|
return { requiresAuth, blocked, domNick: nick, domUserId: userIdMatch?.[1] || '' };
|
|
10
10
|
})()
|
|
11
|
-
`);if(i.blocked)throw new
|
|
11
|
+
`);if(i.blocked)throw new n("goofish.com","Xianyu blocked by verification / risk control");if(i.requiresAuth)throw new n("goofish.com","Xianyu /personal shows login prompt — anonymous");const u=i.domUserId||k;let c="";if(r)try{c=JSON.parse('"'+r.replace(/\\/g,"\\\\")+'"')}catch{c=r}const m=i.domNick||c;if(!u&&!m)throw new l("Xianyu /personal rendered but no user identity extractable — stale unb or layout drift");return{user_id:String(u),nickname:String(m)}}w({site:"xianyu",domain:"goofish.com",loginUrl:"https://www.goofish.com/login",columns:["user_id","nickname"],quickCheck:a,verify:h,poll:async(o)=>{if(!await a(o))throw new n("goofish.com","Waiting for Xianyu unb/tracknick cookie");return h(o)}});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import{cli as x,Strategy as p}from"@jackwener/opencli/registry";import{CliError as v,TimeoutError as _}from"@jackwener/opencli/errors";import{createSystemMouse as b,systemMouseAvailable as h}from"./system-mouse.js";const m="/_____tmd_____/punish",l=(t)=>new Promise((e)=>setTimeout(e,t));export function captchaError(t){return new v("CAPTCHA",`闲鱼验证码:${t}`)}const w=`(() => {
|
|
2
|
+
const visible = (e) => !!e && e.getBoundingClientRect().width > 0 && e.getBoundingClientRect().height > 0 && getComputedStyle(e).visibility !== 'hidden' && getComputedStyle(e).display !== 'none';
|
|
3
|
+
const box = (e) => { const r = e.getBoundingClientRect(); return { x: r.x, y: r.y, width: r.width, height: r.height }; };
|
|
4
|
+
const container = document.querySelector('.nc-container, #nocaptcha, #baxia-punish');
|
|
5
|
+
const track = document.querySelector('.nc_scale');
|
|
6
|
+
const handle = track?.querySelector('.btn_slide');
|
|
7
|
+
const retry = document.querySelector('.nc_wrapper .errloading, .nc_wrapper .nc-lang-cnt a');
|
|
8
|
+
const text = container?.innerText?.replace(/\\s+/g, ' ').trim() || '';
|
|
9
|
+
const expired = location.pathname.includes('/_____tmd_____/') && /抱歉,页面访问出现了问题/.test(document.body?.innerText || '');
|
|
10
|
+
const success = visible(document.querySelector('.nc-success, .nc_scale .btn_ok')) || /验证通过|验证成功/.test(text);
|
|
11
|
+
const blocked = !success && (expired || visible(handle) || visible(retry) || location.pathname.includes(${JSON.stringify(m)}));
|
|
12
|
+
return { blocked, success, expired, text, handle: visible(handle) ? box(handle) : null, track: visible(track) ? box(track) : null, retry: visible(retry) ? box(retry) : null, viewport: { width: innerWidth, height: innerHeight } };
|
|
13
|
+
})()`;async function d(t){const e=await t.evaluate(`(() => {
|
|
14
|
+
const widget = ${w};
|
|
15
|
+
const frames = Array.from(document.querySelectorAll('iframe')).filter(f => f.src.includes(${JSON.stringify(m)})).map(f => {
|
|
16
|
+
const r = f.getBoundingClientRect();
|
|
17
|
+
return { url: f.src, x: r.x + f.clientLeft, y: r.y + f.clientTop, scaleX: f.clientWidth ? (r.width / f.offsetWidth) : 1, scaleY: f.clientHeight ? (r.height / f.offsetHeight) : 1, visible: r.width > 0 && r.height > 0 && getComputedStyle(f).display !== 'none' && getComputedStyle(f).visibility !== 'hidden' };
|
|
18
|
+
}).filter(f => f.visible);
|
|
19
|
+
return { widget, frames };
|
|
20
|
+
})()`);if(e.widget.blocked||e.widget.success)return{...e.widget,offset:{x:0,y:0,scaleX:1,scaleY:1},viewport:e.widget.viewport};if(!e.frames.length)return null;if(typeof t.frames!=="function"||typeof t.evaluateInFrame!=="function")throw captchaError("当前浏览器无法读取跨域滑块,验证尚未解除");const r=(await t.frames()).find((s)=>e.frames.some((c)=>c.url===s.url));if(!r)throw captchaError("验证码框架尚未就绪,验证尚未解除");return{...await t.evaluateInFrame(w,r.index),frameIndex:r.index,frameId:r.frameId,offset:e.frames.find((s)=>s.url===r.url),viewport:e.widget.viewport}}function y(t,e){return{x:t.offset.x+(e.x+e.width/2)*t.offset.scaleX,y:t.offset.y+(e.y+e.height/2)*t.offset.scaleY}}async function u(t,e,o,r=!1){await t.cdp("Input.dispatchMouseEvent",{type:e,x:o.x,y:o.y,button:e==="mouseMoved"&&!r?"none":"left",buttons:r?1:0,...e!=="mouseMoved"?{clickCount:1}:{}})}async function k(t,e,o){const r=y(e,e.retry);if(o)await o.click(r);else{await u(t,"mouseMoved",r);await u(t,"mousePressed",r,!0);await u(t,"mouseReleased",r)}await l(650)}async function S(t,e,o){const r=y(e,e.handle),a=(e.track.x+e.track.width-e.handle.x-e.handle.width+3)*e.offset.scaleX,s={x:r.x+a,y:r.y};if(!Number.isFinite(a)||a<=0||r.x<0||r.y<0||s.x>=e.viewport.width||s.y>=e.viewport.height)throw captchaError("滑块位置不可用,尚未拖动");const c=[r];for(let i=1;i<=36;i++){const n=i/36,f=n<0.65?0.88*(n/0.65)**1.3:0.88+0.12*(1-((1-n)/0.35)**2);c.push({x:r.x+a*f,y:r.y+Math.sin(n*Math.PI*2)*1.5})}if(o){await o.drag(c);return}for(let i=1;i<=10;i++){const n=i/10;await u(t,"mouseMoved",{x:r.x-45*(1-n),y:r.y-20*(1-n)});await l(10)}await l(90);await u(t,"mousePressed",r,!0);try{for(const i of c.slice(1)){await u(t,"mouseMoved",i,!0);await l(12+Math.random()*16)}}finally{await u(t,"mouseReleased",s)}}export async function solveCaptcha(t,{input:e="auto"}={}){let o=await d(t);if(!o?.blocked)return{status:"clear",attempts:0};if(o.expired)throw captchaError("验证地址已失效,需要由原操作取得当前验证码");const r=e==="system"||e==="auto"&&h(t);if(r&&!h(t))throw captchaError("系统鼠标需要 Linux KDE 桌面、ydotool、qdbus6、journalctl 和可写的 /dev/uinput");if(!r&&typeof t.cdp!=="function")throw captchaError("当前浏览器不支持原生鼠标拖动");console.error("[xianyu-captcha] solving verification",{input:r?"system":"cdp"});const a=(await t.getCookies({domain:"goofish.com"})).filter((i)=>i.name==="x5sec").map((i)=>i.value),s=async(i)=>{if(!(await t.getCookies({domain:"goofish.com"})).filter((f)=>f.name==="x5sec").some((f)=>f.value&&!a.includes(f.value)))throw captchaError("滑块已消失或显示通过,但未确认新的 x5sec 通行凭证");return{status:"solved",attempts:i}},c=r?await b(t,()=>d(t)):null;try{for(let i=1;i<=2;i++){if(o.retry){await k(t,o,c);o=await d(t)}for(let n=0;o?.blocked&&!o.handle&&!o.retry&&n<12;n++){await l(200);o=await d(t)}if(!o?.blocked)return await s(i-1);if(!o.handle||!o.track)throw captchaError(`不是可拖动的滑块或滑块未加载:${o.text}`);await S(t,o,c);for(let n=0;n<20;n++){await l(200);o=await d(t);if(!o?.blocked)return await s(i);if(o.retry)break}console.error("[xianyu-captcha] verification rejected",{attempt:i,status:o?.text})}throw captchaError(`自动拖动未通过,验证仍未解除:${o?.text||"验证结果未确认"}`)}finally{if(c)await c.close()}}export function isCaptchaRejection(t){return t?.error==="blocked"||/RGV587_ERROR|FAIL_SYS_USER_VALIDATE|FAIL_SYS_USER_INPUT|\/punish/.test(`${t?.error_code||""} ${t?.error_message||""}`)}export async function evaluateWithCaptcha(t,...e){await solveCaptcha(t);const o=Date.now();for(let r=0;r<2;r++){let a,s=!1;const c=t.evaluate(...e).then((n)=>{a={value:n}},(n)=>{a={error:n}});for(let n=0;!a&&n<120;n++){await Promise.race([c,l(400)]);if(!a)s=(await solveCaptcha(t)).status==="solved"||s}if(!a)throw new _("xianyu operation",Math.ceil((Date.now()-o)/1000),"原操作的结果尚未确认,请先回查;尚未重发原请求");if(a.error)throw a.error;if(!isCaptchaRejection(a.value))return a.value;const i=await solveCaptcha(t);if(!s&&i.status!=="solved"||r===1)throw captchaError("接口仍被验证码拦截,原操作未完成")}}x({site:"xianyu",name:"captcha",access:"write",description:"检测闲鱼滑块验证码,自动拖动并确认验证已解除",domain:"www.goofish.com",strategy:p.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"input",default:"auto",choices:["auto","system","cdp"],help:"鼠标方式:auto 自动选择,system 系统鼠标,cdp 浏览器鼠标"}],columns:["status","attempts"],func:async(t,e)=>{const o=await t.getCurrentUrl();await t.goto(!o||o==="about:blank"?"https://www.goofish.com":o);return[await solveCaptcha(t,{input:e.input})]}});
|
package/clis/xianyu/chat.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{AuthRequiredError as
|
|
1
|
+
import{evaluateWithCaptcha as u}from"./captcha.js";import{AuthRequiredError as p,CommandExecutionError as h,selectorError as w}from"@jackwener/opencli/errors";import{cli as f,Strategy as g}from"@jackwener/opencli/registry";import{buildChatUrl as c,buildExtractChatStateEvaluate as l,buildSendMessageEvaluate as _,requireEvaluateObject as d}from"./im.js";import{normalizeNumericId as n}from"./utils.js";f({site:"xianyu",name:"chat",access:"write",description:"打开闲鱼聊一聊会话,并可选发送消息",domain:"www.goofish.com",strategy:g.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"item_id",required:!0,positional:!0,help:"闲鱼商品 item_id"},{name:"user_id",required:!0,positional:!0,help:"聊一聊对方的 user_id / peerUserId"},{name:"text",fromFile:!0,help:"Message to send after opening the chat"}],columns:["status","peer_name","item_title","price","location","message"],func:async(t,i)=>{const r=n(i.item_id,"item_id","1038951278192"),a=n(i.user_id,"user_id","3650092411"),o=c(r,a),s=String(i.text||"").trim();await t.goto(o);await t.wait(2);const e=d(await u(t,l()),"chat");if(e?.requiresAuth)throw new p("www.goofish.com","Xianyu chat requires a logged-in browser session");if(!e?.can_input)throw w("闲鱼聊天输入框","未找到可用的聊天输入框,请确认该会话页已正确加载");if(!s)return[{status:"ready",peer_name:e.peer_name||"",item_title:e.item_title||"",price:e.price||"",location:e.location||"",message:(e.visible_messages||[]).slice(-1)[0]||"",peer_user_id:a,item_id:r,url:o,item_url:e.item_url||""}];const m=d(await u(t,_(s)),"chat send");if(!m?.ok)throw new h(`Xianyu chat did not observe the sent message: ${m?.reason||"unknown-reason"}`);await t.wait(1);return[{status:"sent",peer_name:e.peer_name||"",item_title:e.item_title||"",price:e.price||"",location:e.location||"",message:s,peer_user_id:a,item_id:r,url:o,item_url:e.item_url||""}]}});export const __test__={normalizeNumericId:n,buildChatUrl:c,buildExtractChatStateEvaluate:l,buildSendMessageEvaluate:_};
|
package/clis/xianyu/inbox.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{AuthRequiredError as
|
|
1
|
+
import{evaluateWithCaptcha as l}from"./captcha.js";import{AuthRequiredError as d,CommandExecutionError as y,EmptyResultError as v}from"@jackwener/opencli/errors";import{cli as x,Strategy as I}from"@jackwener/opencli/registry";import{buildClickInboxConversationEvaluate as E,buildExtractInboxEvaluate as m,buildInboxUrl as _,buildReadCurrentConversationUrlEvaluate as C,DEFAULT_INBOX_LIMIT as f,MAX_INBOX_LIMIT as g,normalizeLimit as w,requireClickResult as k,requireEvaluateObject as b}from"./im.js";x({site:"xianyu",name:"inbox",access:"read",description:"列出闲鱼最近私信会话",domain:"www.goofish.com",strategy:I.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"limit",type:"int",default:f,help:"Number of conversations to return"},{name:"unread-only",type:"bool",default:!1,help:"Return only conversations with unread messages"},{name:"resolve-ids",type:"bool",default:!1,help:"Click each visible conversation to resolve item_id and peer_user_id from the chat URL"}],columns:["rank","peer_name","peer_user_id","item_id","item_title","price","last_message","unread","unread_count","url"],func:async(r,n)=>{const u=w(n.limit,f,g,"inbox --limit"),p=Boolean(n["unread-only"]),h=Boolean(n["resolve-ids"]);let a="";if(r.getCurrentUrl)try{a=await r.getCurrentUrl()}catch{a=""}if(!/https:\/\/www\.goofish\.com\/im\b/.test(a))await r.goto(_());await r.wait(4);const t=b(await l(r,m(u)),"inbox");if(t?.requiresAuth)throw new d("www.goofish.com","Xianyu inbox requires a logged-in browser session");if(t?.blocked)throw new d("www.goofish.com","Xianyu inbox is blocked by verification or risk control");if(!Array.isArray(t.items))throw new y("Xianyu inbox returned malformed conversation list");const c=t.items;if(!c.length)throw new v("xianyu inbox","No Xianyu inbox conversations were found");let o=c.slice(0,u);if(p)o=o.filter((e)=>Boolean(e.unread));if(h)for(const e of o){if(e.item_id&&e.peer_user_id)continue;const i=Number(e.row_index);if(!Number.isInteger(i)||i<0)continue;k(await l(r,E(i)),"inbox resolve-ids click");await r.wait(1);const s=b(await l(r,C()),"inbox current-url");e.item_id=s?.item_id||e.item_id||"";e.peer_user_id=s?.peer_user_id||e.peer_user_id||"";e.url=s?.url||e.url||""}return o.map((e,i)=>({rank:i+1,peer_name:e.peer_name||"",peer_user_id:e.peer_user_id||"",item_id:e.item_id||"",item_title:e.item_title||"",price:e.price||"",last_message:e.last_message||"",unread:Boolean(e.unread),unread_count:Number(e.unread_count||0),url:e.url||""}))}});export const __test__={buildInboxUrl:_,buildExtractInboxEvaluate:m,normalizeLimit:w};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{CommandExecutionError as
|
|
2
|
-
`).map((c)=>c.trim()).filter(Boolean)[0]||"";if(!n)return;const
|
|
1
|
+
import{evaluateWithCaptcha as y}from"./captcha.js";import{CommandExecutionError as w,ArgumentError as A}from"@jackwener/opencli/errors";import{cli as x,Strategy as g}from"@jackwener/opencli/registry";const d={recent1d:"近1天",recent7d:"近7天",recent30d:"近30天"},U="https://seller.goofish.com/?site=COMMONPRO#/seller-data/commodity",h={商品曝光次数:"exposureCount",商品曝光人数:"exposureUsers",商品浏览次数:"viewCount",商品浏览人数:"viewUsers",询单人数:"inquiryUsers",支付人数:"payUsers",支付订单数:"payOrders",支付金额:"payAmount",浏览支付转化率:"viewPayCvr",发起退款人数:"refundUsers",发起退款订单数:"refundOrders",发起退款金额:"refundAmount",成功退款人数:"refundOkUsers",成功退款订单数:"refundOkOrders",成功退款金额:"refundOkAmount"};function O(e){const a=Array.from(document.querySelectorAll("thead th")).map((t)=>(t.innerText||"").trim());if(!a.length)return{error:"no-table"};const o={};a.forEach((t,r)=>{if(e[t]!=null)o[e[t]]=r});const s=[];document.querySelectorAll("tbody tr").forEach((t)=>{const r=Array.from(t.querySelectorAll("td"));if(!r.length)return;if(r.every((c)=>!(c.innerText||"").trim()))return;const i=(r[0].innerText||"").replace(/\r/g,""),n=i.match(/(\d{10,})/),l=i.match(/¥\s*([\d.]+)/),p=i.split(`
|
|
2
|
+
`).map((c)=>c.trim()).filter(Boolean)[0]||"";if(!n)return;const m={itemId:n[1],title:p,price:l?l[1]:""};for(const c of Object.keys(e)){const f=e[c],u=o[f];m[f]=u!=null&&r[u]?(r[u].innerText||"").trim():""}s.push(m)});return{rows:s}}x({site:"xianyu",name:"item-stats",access:"read",description:"读每个在售商品的经营数据(曝光/浏览/询单/支付/退款,逐商品一行)",domain:"seller.goofish.com",strategy:g.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"range",default:"recent1d",help:"时间范围:recent1d / recent7d / recent30d"}],columns:["itemId","title","price","exposureCount","exposureUsers","viewCount","viewUsers","inquiryUsers","payUsers","payOrders","payAmount","viewPayCvr","refundUsers","refundOrders","refundAmount","refundOkUsers","refundOkOrders","refundOkAmount"],func:async(e,a)=>{const o=String(a.range||"recent1d").trim(),s=d[o];if(!s)throw new A(`--range 只能是 ${Object.keys(d).join(" / ")},收到 "${o}"`);await e.goto(U);await e.wait(3);if(o!=="recent1d"){await y(e,(i)=>{Array.from(document.querySelectorAll("label,span,div,button")).filter((n)=>n.children.length===0&&(n.innerText||"").trim()===i).forEach((n)=>n.click())},s);await e.wait(2)}const t=await y(e,O,h);if(t.error==="no-table")throw new w("商品数据表未渲染 —— 可能未登录卖家中心 / 页面改版 / 无权限");const r=t.rows||[];if(r.length===0)throw new w("商品数据表为空 —— 该账号可能无在售商品或当前范围无数据");return r}});export const __test__={readItemTable:O,HEADER_MAP:h,RANGE_TEXT:d};
|
package/clis/xianyu/item.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{AuthRequiredError as a,EmptyResultError as r,selectorError as
|
|
1
|
+
import{evaluateWithCaptcha as m}from"./captcha.js";import{AuthRequiredError as a,EmptyResultError as r,selectorError as d}from"@jackwener/opencli/errors";import{cli as u,Strategy as w}from"@jackwener/opencli/registry";import{normalizeNumericId as s}from"./utils.js";function i(t){return`https://www.goofish.com/item?id=${encodeURIComponent(t)}`}function p(t){return`
|
|
2
2
|
(async () => {
|
|
3
3
|
const clean = (value) => String(value ?? '').replace(/\\s+/g, ' ').trim();
|
|
4
4
|
const extractRetCode = (ret) => {
|
|
@@ -94,4 +94,4 @@ import{AuthRequiredError as a,EmptyResultError as r,selectorError as m}from"@jac
|
|
|
94
94
|
image_urls: images,
|
|
95
95
|
};
|
|
96
96
|
})()
|
|
97
|
-
`}
|
|
97
|
+
`}u({site:"xianyu",name:"item",access:"read",description:"查看闲鱼商品详情",domain:"www.goofish.com",strategy:w.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"item_id",required:!0,positional:!0,help:"闲鱼商品 item_id"}],columns:["item_id","title","price","condition","brand","location","seller_name","want_count"],func:async(t,l)=>{const o=s(l.item_id,"item_id","1040754408976");await t.goto(i(o));await t.wait(2);const e=await m(t,p(o));if(e?.error==="auth-required")throw new a("www.goofish.com","Xianyu item detail requires a logged-in browser session");if(e?.error==="blocked")throw new r("xianyu item","Xianyu item detail is blocked by verification or risk control");if(e?.error==="mtop-not-ready")throw d("window.lib.mtop","闲鱼页面未完成初始化,无法调用商品详情接口");if(!e||typeof e!=="object")throw new r("xianyu item","闲鱼商品详情接口未返回有效数据");const c=String(e.error_code||""),n=String(e.error_message||"");if(/FAIL_SYS_SESSION_EXPIRED|SESSION_EXPIRED|FAIL_SYS/.test(c)||/FAIL_SYS_SESSION_EXPIRED|SESSION_EXPIRED/.test(n))throw new a("www.goofish.com","Xianyu item detail requires a logged-in browser session");if(e.error)throw new r("xianyu item",n||`Xianyu item detail request failed: ${e.error}`);if(!String(e.title||"").trim())throw new r("xianyu item","No item detail was returned for the specified item_id");return[e]}});export const __test__={normalizeNumericId:s,buildItemUrl:i};
|
package/clis/xianyu/messages.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{ArgumentError as m,AuthRequiredError as
|
|
1
|
+
import{evaluateWithCaptcha as c}from"./captcha.js";import{ArgumentError as m,AuthRequiredError as E,CommandExecutionError as I,EmptyResultError as v}from"@jackwener/opencli/errors";import{cli as C,Strategy as A}from"@jackwener/opencli/registry";import{buildChatUrl as h,buildClickInboxConversationEvaluate as q,buildExtractChatStateEvaluate as f,DEFAULT_MESSAGE_LIMIT as w,MAX_MESSAGE_LIMIT as k,normalizeLimit as g,normalizeRank as p,requireClickResult as S,requireEvaluateObject as U}from"./im.js";import{normalizeNumericId as y}from"./utils.js";C({site:"xianyu",name:"messages",access:"read",description:"读取指定闲鱼私信会话的最近聊天内容",domain:"www.goofish.com",strategy:A.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"item_id",positional:!0,help:"闲鱼商品 item_id"},{name:"user_id",positional:!0,help:"聊一聊对方的 user_id / peerUserId"},{name:"limit",type:"int",default:w,help:"Number of visible messages to return"},{name:"rank",type:"int",default:0,help:"Conversation rank from xianyu inbox; clicks the visible row instead of requiring IDs"}],columns:["index","peer_name","item_title","message","item_id","peer_user_id","url"],func:async(e,i)=>{const s=i.item_id!=null&&i.item_id!=="",a=i.user_id!=null&&i.user_id!=="",t=p(i.rank);if(t>0&&(s||a))throw new m("xianyu messages accepts either item_id/user_id or --rank, not both");if(t===0&&s!==a)throw new m("xianyu messages requires both item_id and user_id, or --rank from xianyu inbox");if(t===0&&!s&&!a)throw new m("xianyu messages requires item_id/user_id or --rank from xianyu inbox");const o=s&&a,u=o?y(i.item_id,"item_id","1038951278192"):"",l=o?y(i.user_id,"user_id","3650092411"):"",d=g(i.limit,w,k,"messages --limit");let n="";if(o){n=h(u,l);await e.goto(n)}else if(!e.getCurrentUrl||!/https:\/\/www\.goofish\.com\/im\b/.test(await e.getCurrentUrl()))await e.goto("https://www.goofish.com/im");await e.wait(2);if(t>0){S(await c(e,q(t-1)),"messages rank click");await e.wait(2)}const r=U(await c(e,f(d)),"messages");if(r?.requiresAuth)throw new E("www.goofish.com","Xianyu messages requires a logged-in browser session");if(!Array.isArray(r.messages))throw new I("Xianyu messages returned malformed message list");const _=r.messages;if(!_.length)throw new v("xianyu messages","No visible messages were found in this Xianyu conversation");return _.slice(-d).map((x,b)=>({index:b+1,peer_name:r.peer_name||"",item_title:r.item_title||"",message:x.text||"",item_id:u,peer_user_id:l,url:n||""}))}});export const __test__={buildChatUrl:h,buildExtractChatStateEvaluate:f,normalizeLimit:g,normalizeRank:p};
|
package/clis/xianyu/mtop.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{AuthRequiredError as a,CommandExecutionError as
|
|
1
|
+
import{evaluateWithCaptcha as d}from"./captcha.js";import{AuthRequiredError as a,CommandExecutionError as S,EmptyResultError as i,selectorError as f}from"@jackwener/opencli/errors";export const HOME_URL="https://www.goofish.com";const c=["RGV587_ERROR","FAIL_SYS_USER_VALIDATE","哎哟喂","/punish"],w=["FAIL_SYS_SESSION_EXPIRED","FAIL_SYS_TOKEN_EXOIRED","FAIL_SYS_TOKEN_EMPTY","FAIL_SYS_ILLEGAL_ACCESS","令牌过期","NEED_LOGIN"];function p(e,r,t){return`
|
|
2
2
|
(async () => {
|
|
3
3
|
const clean = (v) => String(v ?? '').replace(/\\s+/g, ' ').trim();
|
|
4
4
|
const extractRetCode = (ret) => clean(Array.isArray(ret) ? ret[0] : '').split('::')[0] || '';
|
|
@@ -44,4 +44,4 @@ import{AuthRequiredError as a,CommandExecutionError as p,EmptyResultError as i,s
|
|
|
44
44
|
}
|
|
45
45
|
return { ok: true, ret: response?.ret || [], data: response?.data || {} };
|
|
46
46
|
})()
|
|
47
|
-
`}function
|
|
47
|
+
`}function u(e,r){if(r?.error==="auth-required")throw new a("www.goofish.com",`闲鱼 ${e} 需要已登录的浏览器会话`);if(r?.error==="blocked")throw new i(`xianyu ${e}`,"闲鱼页面被验证码 / 安全验证拦截,请人工处理后重试");if(r?.error==="mtop-not-ready")throw f("window.lib.mtop","闲鱼页面未完成初始化,无法调用 mtop 接口");const t=String(r?.error_code||""),n=String(r?.error_message||""),s=`${t} ${n}`;if(w.some((o)=>s.includes(o)))throw new a("www.goofish.com",`闲鱼 ${e} 登录态失效:${n||t}`);if(c.some((o)=>s.includes(o)))throw new i(`xianyu ${e}`,`闲鱼 ${e} 触发风控:${n||t},请人工处理`);if(r?.error)throw new S(`闲鱼 ${e} 接口调用失败:${n||t||r.error}`)}export async function mtopRequest(e,r,{api:t,data:n,version:s="1.0"}){const o=await d(e,p(t,n,s));u(r,o);if(!o||typeof o!=="object")throw new i(`xianyu ${r}`,"闲鱼接口未返回有效数据");return o.data||{}}export function retOk(e){return Array.isArray(e)&&e.some((r)=>String(r).includes("SUCCESS"))}export const __test__={buildMtopEvaluate:p,classifyAndThrow:u,RISK_KEYWORDS:c,AUTH_KEYWORDS:w};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{CommandExecutionError as
|
|
2
|
-
`).map((d)=>d.trim()).filter(Boolean);if(e.length<2)return;const t=e[0],s=e[1],u=e.slice(2).join(" ");a(t,s,u)});const i=(r)=>{const e=Array.from(document.querySelectorAll("div,span")).filter((t)=>t.children.length===0&&(t.innerText||"").trim().replace(/(.*)|\(.*\)/g,"")===r);for(const t of e){let s=t.parentElement;for(let u=0;u<3&&s;u++){const d=(s.innerText||"").match(/-?[\d,]+(\.\d+)?%?/);if(d&&d[0]!==r)return d[0];s=s.parentElement}}return null};[["擦亮中计划","擦亮中计划"],["已暂停擦亮计划","已暂停擦亮计划"],["即将消耗完成计划","即将消耗完成计划"],["已完成擦亮计划","已完成擦亮计划"]].forEach(([r,e])=>{const t=i(e);if(t!=null)a(r,t,"")});const l=document.body?document.body.innerText.replace(/\s+/g," "):"",c=/[^\d-]*(-?[\d,]+(?:\s*\.\s*\d+)?)/,m=l.match(new RegExp("可用余额[((]元[))]"+c.source));if(m)a("可用余额(元)",m[1].replace(/\s+/g,""),"");const o=l.match(new RegExp("今日花费[((]元[))]"+c.source));if(o)a("今日花费(元)",o[1].replace(/\s+/g,""),"");return n}
|
|
1
|
+
import{evaluateWithCaptcha as y}from"./captcha.js";import{CommandExecutionError as p,ArgumentError as g}from"@jackwener/opencli/errors";import{cli as w,Strategy as E}from"@jackwener/opencli/registry";const f={today:"今日",yesterday:"昨日",recent7d:"近7日",recent30d:"近30日",recent90d:"近90日"},x="https://seller.goofish.com/?site=COMMONPRO#/seller-ad/home";function h(){const n=[],a=(r,e,t)=>{if(r&&e!==""&&e!=null)n.push({metric:r,value:e,compare:t||""})};document.querySelectorAll('[class*="metricCard--"]').forEach((r)=>{const e=(r.innerText||"").split(`
|
|
2
|
+
`).map((d)=>d.trim()).filter(Boolean);if(e.length<2)return;const t=e[0],s=e[1],u=e.slice(2).join(" ");a(t,s,u)});const i=(r)=>{const e=Array.from(document.querySelectorAll("div,span")).filter((t)=>t.children.length===0&&(t.innerText||"").trim().replace(/(.*)|\(.*\)/g,"")===r);for(const t of e){let s=t.parentElement;for(let u=0;u<3&&s;u++){const d=(s.innerText||"").match(/-?[\d,]+(\.\d+)?%?/);if(d&&d[0]!==r)return d[0];s=s.parentElement}}return null};[["擦亮中计划","擦亮中计划"],["已暂停擦亮计划","已暂停擦亮计划"],["即将消耗完成计划","即将消耗完成计划"],["已完成擦亮计划","已完成擦亮计划"]].forEach(([r,e])=>{const t=i(e);if(t!=null)a(r,t,"")});const l=document.body?document.body.innerText.replace(/\s+/g," "):"",c=/[^\d-]*(-?[\d,]+(?:\s*\.\s*\d+)?)/,m=l.match(new RegExp("可用余额[((]元[))]"+c.source));if(m)a("可用余额(元)",m[1].replace(/\s+/g,""),"");const o=l.match(new RegExp("今日花费[((]元[))]"+c.source));if(o)a("今日花费(元)",o[1].replace(/\s+/g,""),"");return n}w({site:"xianyu",name:"promote-stats",access:"read",description:"读超级擦亮(付费推广)投放数据:花费/曝光/点击/询单/下单/单次成本 + 账户余额 + 计划状态",domain:"seller.goofish.com",strategy:E.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"range",default:"today",help:"时间范围:today / yesterday / recent7d / recent30d / recent90d"}],columns:["metric","value","compare"],func:async(n,a)=>{const i=String(a.range||"today").trim(),l=f[i];if(!l)throw new g(`--range 只能是 ${Object.keys(f).join(" / ")},收到 "${i}"`);await n.goto(x);await n.wait(4);if(i!=="today"){await y(n,(m)=>{Array.from(document.querySelectorAll("label,span,div,button")).filter((o)=>o.children.length===0&&(o.innerText||"").trim()===m).forEach((o)=>o.click())},l);await n.wait(2)}const c=await y(n,h);if(!Array.isArray(c)||c.length===0)throw new p("未读到擦亮数据 —— 可能未开通超级擦亮 / 未登录 / 页面改版");return c}});export const __test__={readAdDashboard:h,RANGE_TEXT:f};
|
package/clis/xianyu/publish.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import*as
|
|
1
|
+
import{evaluateWithCaptcha as s}from"./captcha.js";import*as P from"node:fs";import*as x from"node:path";import{ArgumentError as c,AuthRequiredError as N,CommandExecutionError as u}from"@jackwener/opencli/errors";import{cli as C,Strategy as M}from"@jackwener/opencli/registry";import{setInputFiles as _}from"../_shared/file-inject.js";const j=new Set([".jpg",".jpeg",".png",".webp"]),f=9,h=["全新","几乎全新","轻微使用","明显使用","老旧"];function w(){return"https://www.goofish.com/publish"}async function b(t){if(t.getCurrentUrl)try{const n=await t.getCurrentUrl();if(n)return n}catch{}return w()}function p(t,n){const e=String(t??"").replace(/\s+/g," ").trim();if(!e)throw new c(`xianyu publish ${n} cannot be empty`);return e}function E(t,n){if(t==null||String(t).trim()==="")return null;const e=String(t).trim();if(!/^\d+(?:\.\d{1,2})?$/.test(e))throw new c(`xianyu publish ${n} must be a positive price with at most 2 decimals`);const o=Number(e);if(!Number.isFinite(o)||o<=0)throw new c(`xianyu publish ${n} must be a positive price`);return e}function B(t){const n=p(t,"condition");if(!h.includes(n))throw new c(`xianyu publish condition must be one of: ${h.join(", ")}`);return n}function k(t){if(!t)return[];const n=String(t).split(",").map((e)=>e.trim()).filter(Boolean);if(n.length===0)return[];if(n.length>f)throw new c(`xianyu publish images supports at most ${f} files`);return n.map((e)=>{const o=x.resolve(e),l=x.extname(o).toLowerCase();if(!j.has(l))throw new c(`Unsupported image format "${l}". Supported: jpg, jpeg, png, webp`);const a=P.statSync(o,{throwIfNoEntry:!1});if(!a||!a.isFile())throw new c(`Not a valid image file: ${o}`);return o})}function q(t){const n=E(t.price,"price");if(n==null)throw new c("xianyu publish price cannot be empty");const e={};e.title=p(t.title,"title");e.description=p(t.description,"description");e.price=n;e.condition=B(t.condition);e.category=p(t.category,"category");e.original_price=E(t.original_price,"original_price");e.location=t.location?p(t.location,"location"):"";e.images=k(t.images);return e}function g(t){return`
|
|
2
2
|
(() => {
|
|
3
3
|
const clean = (value) => String(value ?? '').replace(/\\s+/g, ' ').trim();
|
|
4
4
|
|
|
@@ -103,7 +103,7 @@ import*as T from"node:fs";import*as S from"node:path";import{ArgumentError as a,
|
|
|
103
103
|
|
|
104
104
|
return { ok: missing.length === 0, filled, missing };
|
|
105
105
|
})()
|
|
106
|
-
`}function
|
|
106
|
+
`}function y(t){return`
|
|
107
107
|
(async () => {
|
|
108
108
|
const clean = (value) => String(value ?? '').replace(/\\s+/g, ' ').trim();
|
|
109
109
|
|
|
@@ -147,7 +147,7 @@ import*as T from"node:fs";import*as S from"node:path";import{ArgumentError as a,
|
|
|
147
147
|
}
|
|
148
148
|
return { ok: false, reason: 'category-match-failed' };
|
|
149
149
|
})()
|
|
150
|
-
`}function
|
|
150
|
+
`}function I(){return`
|
|
151
151
|
(() => {
|
|
152
152
|
// 找图片上传相关的 file input
|
|
153
153
|
const fileInput = document.querySelector('input[type="file"]');
|
|
@@ -162,7 +162,7 @@ import*as T from"node:fs";import*as S from"node:path";import{ArgumentError as a,
|
|
|
162
162
|
|
|
163
163
|
return { ok: true, selector, hasMultiple: fileInput.multiple };
|
|
164
164
|
})()
|
|
165
|
-
`}function
|
|
165
|
+
`}function A(){return`
|
|
166
166
|
(() => {
|
|
167
167
|
const clean = (value) => String(value ?? '').replace(/\\s+/g, ' ').trim();
|
|
168
168
|
|
|
@@ -181,7 +181,7 @@ import*as T from"node:fs";import*as S from"node:path";import{ArgumentError as a,
|
|
|
181
181
|
submitBtn.click();
|
|
182
182
|
return { ok: true };
|
|
183
183
|
})()
|
|
184
|
-
`}function
|
|
184
|
+
`}function O(){return`
|
|
185
185
|
(() => {
|
|
186
186
|
const clean = (value) => String(value ?? '').replace(/\\s+/g, ' ').trim();
|
|
187
187
|
const bodyText = document.body?.innerText || '';
|
|
@@ -209,7 +209,7 @@ import*as T from"node:fs";import*as S from"node:path";import{ArgumentError as a,
|
|
|
209
209
|
|
|
210
210
|
return { ok: false, reason: 'unknown-state' };
|
|
211
211
|
})()
|
|
212
|
-
`}function
|
|
212
|
+
`}function T(){return`
|
|
213
213
|
(() => {
|
|
214
214
|
const clean = (value) => String(value ?? '').replace(/\\s+/g, ' ').trim();
|
|
215
215
|
const bodyText = document.body?.innerText || '';
|
|
@@ -242,4 +242,4 @@ import*as T from"node:fs";import*as S from"node:path";import{ArgumentError as a,
|
|
|
242
242
|
bodySnippet: bodyText.slice(0, 500),
|
|
243
243
|
};
|
|
244
244
|
})()
|
|
245
|
-
`}export const publishCommand=
|
|
245
|
+
`}export const publishCommand=C({site:"xianyu",name:"publish",access:"write",description:"发布闲鱼宝贝(需先在浏览器中登录闲鱼)",domain:"www.goofish.com",strategy:M.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"title",required:!0,positional:!0,help:"商品标题"},{name:"description",fromFile:!0,required:!0,positional:!0,help:"商品描述/详情"},{name:"price",required:!0,positional:!0,type:"float",help:"出售价格(元)"},{name:"condition",required:!0,positional:!0,help:"成色:全新 / 几乎全新 / 轻微使用 / 明显使用 / 老旧"},{name:"category",required:!0,positional:!0,help:"商品分类关键词(如:手机、衣服、图书)"},{name:"original_price",type:"float",help:"原价(选填,用于显示折扣)"},{name:"location",help:"所在地区(选填,如:杭州)"},{name:"images",help:"本地图片路径,多张用逗号分隔(选填,如:/tmp/a.jpg,/tmp/b.jpg)"}],columns:["status","item_id","title","price","condition","url","message"],func:async(t,n)=>{const e=q(n);await t.goto(w());await t.wait(3);let o=await s(t,T());for(let i=0;i<30;i++){if(o?.requiresAuth||o?.hasPublishForm)break;await t.wait(0.5);o=await s(t,T())}if(o?.requiresAuth)throw new N("www.goofish.com","发布闲鱼需要先登录,请在 Chrome 中打开 goofish.com 并完成登录");if(!o?.hasPublishForm)throw new u("Xianyu publish form was not detected","Confirm goofish.com is logged in and the publish page finished loading.");let l=await s(t,y(e.category));for(let i=0;i<30;i++){if(l?.ok||l?.reason!=="category-trigger-not-found")break;await t.wait(0.5);l=await s(t,y(e.category))}if(!l?.ok)throw new u(`Xianyu category selection failed: ${l?.reason||"unknown-reason"}`);await t.wait(1.5);let a=await s(t,g(e));for(let i=0;i<30;i++){if(a?.ok)break;await t.wait(0.5);a=await s(t,g(e))}if(!a?.ok){const i=Array.isArray(a?.missing)?a.missing.join(", "):"unknown";throw new u(`Xianyu publish form fill failed; missing fields: ${i}`)}await t.wait(1);if(e.images.length>0){let i=await s(t,I());for(let r=0;r<30;r++){if(i?.ok)break;await t.wait(0.5);i=await s(t,I())}if(!i?.ok)throw new u(`Xianyu image upload input was not found: ${i?.reason||"unknown-reason"}`);try{await _(t,i.selector||'input[type="file"]',e.images);await t.wait(3)}catch(r){throw new u(`Xianyu image upload failed: ${r?.message||r}`)}}let d=await s(t,A());for(let i=0;i<30;i++){if(d?.ok)break;await t.wait(0.5);d=await s(t,A())}if(!d?.ok)throw new u(`Xianyu publish submit failed: ${d?.reason||"unknown-reason"}`);await t.wait(2);let S="",m=await b(t),v="";for(let i=0;i<10;i++){await t.wait(1.5);const r=await s(t,O());m=await b(t);if(r?.status==="published"){S=String(r.item_id||"").replace(/\D/g,"");return[{status:"published",item_id:S,title:e.title.slice(0,50),price:`¥${e.price}`,condition:e.condition,url:r.url||m,message:"发布成功"}]}if(r?.status==="failed"){v=r.message||"发布失败";break}}throw new u(v||"Xianyu publish result was not confirmed before timeout",`Open ${m} and verify whether the listing was published.`)}}),__test__={CONDITION_CHOICES:h,MAX_IMAGES:f,validateImagePaths:k,normalizePublishArgs:q,buildPublishUrl:w,getCurrentPageUrl:b,buildFillFormEvaluate:g,buildSelectCategoryEvaluate:y,buildFindFileInputSelectorEvaluate:I,buildDetectSuccessEvaluate:O};
|
package/clis/xianyu/reply.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{ArgumentError as
|
|
1
|
+
import{evaluateWithCaptcha as s}from"./captcha.js";import{ArgumentError as u,AuthRequiredError as y,CommandExecutionError as x,selectorError as b}from"@jackwener/opencli/errors";import{cli as C,Strategy as E}from"@jackwener/opencli/registry";import{buildChatUrl as w,buildClickInboxConversationEvaluate as I,buildExtractChatStateEvaluate as q,buildSendMessageEvaluate as h,normalizeRank as v,requireClickResult as k,requireEvaluateObject as p,requireText as g}from"./im.js";import{normalizeNumericId as f}from"./utils.js";C({site:"xianyu",name:"reply",access:"write",description:"回复指定闲鱼私信会话",domain:"www.goofish.com",strategy:E.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"item_id",positional:!0,help:"闲鱼商品 item_id"},{name:"user_id",positional:!0,help:"聊一聊对方的 user_id / peerUserId"},{name:"text",fromFile:!0,required:!0,help:"Message text to send"},{name:"rank",type:"int",default:0,help:"Conversation rank from xianyu inbox; clicks the visible row instead of requiring IDs"}],columns:["status","peer_name","item_title","price","location","message"],func:async(e,t)=>{const o=t.item_id!=null&&t.item_id!=="",n=t.user_id!=null&&t.user_id!=="",r=v(t.rank);if(r>0&&(o||n))throw new u("xianyu reply accepts either item_id/user_id or --rank, not both");if(r===0&&o!==n)throw new u("xianyu reply requires both item_id and user_id, or --rank from xianyu inbox");if(r===0&&!o&&!n)throw new u("xianyu reply requires item_id/user_id or --rank from xianyu inbox");const a=o&&n,l=a?f(t.item_id,"item_id","1038951278192"):"",m=a?f(t.user_id,"user_id","3650092411"):"",d=g(t.text,"xianyu reply --text"),c=a?w(l,m):"";if(a)await e.goto(c);else if(!e.getCurrentUrl||!/https:\/\/www\.goofish\.com\/im\b/.test(await e.getCurrentUrl()))await e.goto("https://www.goofish.com/im");await e.wait(2);if(r>0){k(await s(e,I(r-1)),"reply rank click");await e.wait(2)}const i=p(await s(e,q()),"reply");if(i?.requiresAuth)throw new y("www.goofish.com","Xianyu reply requires a logged-in browser session");if(!i?.can_input)throw b("闲鱼聊天输入框","未找到可用的聊天输入框,请确认该会话页已正确加载");const _=p(await s(e,h(d)),"reply send");if(!_?.ok)throw new x(`Xianyu reply did not observe the sent message: ${_?.reason||"unknown-reason"}`);await e.wait(1);return[{status:"sent",peer_name:i.peer_name||"",item_title:i.item_title||"",price:i.price||"",location:i.location||"",message:d,peer_user_id:m,item_id:l,url:c||"",item_url:i.item_url||""}]}});export const __test__={buildChatUrl:w,buildSendMessageEvaluate:h};
|
package/clis/xianyu/search.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{ArgumentError as f,AuthRequiredError as w,CommandExecutionError as s,EmptyResultError as
|
|
1
|
+
import{evaluateWithCaptcha as P}from"./captcha.js";import{ArgumentError as f,AuthRequiredError as w,CommandExecutionError as s,EmptyResultError as A,selectorError as N}from"@jackwener/opencli/errors";import{cli as v,Strategy as C}from"@jackwener/opencli/registry";const g=30,c=60;function y(e){const r=Number(e);if(!Number.isFinite(r))return 20;return Math.min(c,Math.max(1,Math.floor(r)))}function h(e){return`https://www.goofish.com/search?q=${encodeURIComponent(e)}`}function l(e,r){if(e===void 0||e===null||e==="")return null;const n=Number(String(e).trim());if(!Number.isFinite(n)||n<0)throw new f(`xianyu search ${r} must be a non-negative number`,`For example: --${r} 100000`);return n}function x(e,r){if(e==null&&r==null)return"";const n=e!=null?e:0,o=r!=null?r:99999999;return`priceRange:${n},${o};`}function b(e,r){if(!e&&!r)return"{}";return JSON.stringify({divisionList:[{province:e||"",city:r||""}],excludeMultiPlacesSellers:"0",extraDivision:""})}function q({keyword:e,searchFilter:r,extraFilterValue:n,fromFilter:o,maxItems:i}){return`
|
|
2
2
|
(async () => {
|
|
3
3
|
const clean = (value) => String(value ?? '').replace(/\\s+/g, ' ').trim();
|
|
4
4
|
const cleanFirst = (...values) => values.map(clean).find(Boolean) || '';
|
|
@@ -138,4 +138,4 @@ import{ArgumentError as f,AuthRequiredError as w,CommandExecutionError as s,Empt
|
|
|
138
138
|
|
|
139
139
|
return { items: collected };
|
|
140
140
|
})()
|
|
141
|
-
`}
|
|
141
|
+
`}v({site:"xianyu",name:"search",access:"read",description:"搜索闲鱼商品(支持服务端价格区间 / 地区筛选)",domain:"www.goofish.com",strategy:C.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"query",required:!0,positional:!0,help:"搜索关键词"},{name:"limit",type:"int",default:20,help:`返回结果数(最多 ${c},自动翻页)`},{name:"min-price",type:"float",help:"最低价格(元),服务端筛选"},{name:"max-price",type:"float",help:"最高价格(元),服务端筛选"},{name:"province",type:"string",help:"省份名(如 广东),服务端按地区筛选"},{name:"city",type:"string",help:"城市名(如 深圳 / 湛江),可单独使用,服务端按地区筛选"}],columns:["item_id","rank","title","price","condition","brand","location","badge","want","url"],func:async(e,r)=>{const n=String(r.query||"").trim(),o=y(r.limit),i=l(r["min-price"],"min-price"),a=l(r["max-price"],"max-price");if(i!=null&&a!=null&&i>a)throw new f("xianyu search min-price cannot be greater than max-price",`Received --min-price ${i} and --max-price ${a}`);const S=String(r.province||"").trim(),_=String(r.city||"").trim(),m=x(i,a),u=b(S,_),E=Boolean(m)||u!=="{}";await e.goto(h(n));await e.wait(2);const t=await P(e,q({keyword:n,searchFilter:m,extraFilterValue:u,fromFilter:E,maxItems:o}));if(t?.error==="auth-required")throw new w("www.goofish.com","Xianyu search requires a logged-in browser session");if(t?.error==="blocked")throw new s("Xianyu returned a verification page or blocked the current browser session");if(t?.error==="mtop-not-ready")throw N("window.lib.mtop","闲鱼页面未完成初始化,无法调用搜索接口");if(!t||typeof t!=="object")throw new s("Xianyu search returned a malformed response");const F=String(t?.error_code||""),d=String(t?.error_message||"");if(/FAIL_SYS_SESSION_EXPIRED|SESSION_EXPIRED|FAIL_SYS_TOKEN/.test(F)||/FAIL_SYS_SESSION_EXPIRED|SESSION_EXPIRED/.test(d))throw new w("www.goofish.com","Xianyu search requires a logged-in browser session");if(t?.error)throw new s(d||`Xianyu search request failed: ${t.error}`);if(!Array.isArray(t.items))throw new s("Xianyu search response did not include an items array");const p=t.items;if(!p.length)throw new A("xianyu search","没有匹配的商品(筛选条件可能过窄,或当前关键词无结果)");return p.map((I,R)=>({rank:R+1,...I}))}});export const __test__={ROWS_PER_PAGE:g,MAX_LIMIT:c,normalizeLimit:y,buildSearchUrl:h,parsePriceArg:l,buildSearchFilter:x,buildExtraFilterValue:b};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{ArgumentError as
|
|
1
|
+
import{evaluateWithCaptcha as f}from"./captcha.js";import{ArgumentError as g,CommandExecutionError as y}from"@jackwener/opencli/errors";import{cli as p,Strategy as E}from"@jackwener/opencli/registry";const d={overview:{hash:"#/seller-data/data",label:"数据总览"},commodity:{hash:"#/seller-data/commodity",label:"商品数据"},fans:{hash:"#/seller-data/fanData",label:"粉丝数据"},service:{hash:"#/seller-data/customerService",label:"客服数据"}},m={recent1d:"近1天",recent7d:"近7天",recent30d:"近30天"},v="https://seller.goofish.com/?site=COMMONPRO";function w(){const t=[],c=new Set;document.querySelectorAll('[class*="statValue--"]').forEach((n)=>{let e=n,r=null,o=null;for(let h=0;h<5&&e;h++){e=e.parentElement;if(!e)break;r=e.querySelector('[class*="statHeader--"]');if(r){o=e;break}}if(!r||!o)return;const a=(r.innerText||"").trim(),l=(n.innerText||"").trim();if(!a)return;const i=o.querySelector('[class*="statPrevious--"]'),s=i?(i.innerText||"").replace(/\s+/g," ").trim():"",u=a+"|"+l;if(c.has(u))return;c.add(u);t.push({metric:a,value:l,compare:s})});return t}p({site:"xianyu",name:"seller-data",access:"read",description:"读卖家工作台经营数据(曝光/访问/浏览/转化/复购/售后等指标卡)",domain:"seller.goofish.com",strategy:E.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"page",default:"overview",help:`数据页:${Object.keys(d).join(" / ")}`},{name:"range",default:"recent1d",help:"时间范围:recent1d / recent7d / recent30d"}],columns:["metric","value","compare"],func:async(t,c)=>{const n=String(c.page||"overview").trim(),e=d[n];if(!e)throw new g(`--page 只能是 ${Object.keys(d).join(" / ")},收到 "${n}"`);const r=String(c.range||"recent1d").trim(),o=m[r];if(!o)throw new g(`--range 只能是 ${Object.keys(m).join(" / ")},收到 "${r}"`);await t.goto(`${v}${e.hash}`);await t.wait(3);if(r!=="recent1d"){await f(t,(l)=>{const i=Array.from(document.querySelectorAll("label,span,div,button")).filter((s)=>s.children.length===0&&(s.innerText||"").trim()===l);i.forEach((s)=>s.click());return i.length},o);await t.wait(2)}const a=await f(t,w);if(!Array.isArray(a)||a.length===0)throw new y(`未读到任何指标卡(page=${n})—— 可能未登录卖家中心、页面结构改版或该账号无权限`);return a}});export const __test__={readCards:w,PAGE_ROUTES:d,RANGE_TEXT:m};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{CommandExecutionError as h,ArgumentError as
|
|
1
|
+
import{evaluateWithCaptcha as f}from"./captcha.js";import{CommandExecutionError as h,ArgumentError as g}from"@jackwener/opencli/errors";import{cli as p,Strategy as d}from"@jackwener/opencli/registry";const a={refund:{hash:"#/seller-trade/refund-manage",label:"退款管理"},review:{hash:"#/seller-trade/evaluation-manage",label:"评价管理"},complaint:{hash:"#/seller-trade/complaint-manage",label:"投诉管理"},income:{hash:"#/seller-finance/income-bill",label:"收入账单"},expense:{hash:"#/seller-finance/expense-bill",label:"支出账单"},"shipping-template":{hash:"#/seller-item/post-temple",label:"运费模版"}};function m(){const t=Array.from(document.querySelectorAll("thead th")).map((r)=>(r.innerText||"").trim()).filter(Boolean);if(!t.length)return{error:"no-table"};const n=[];let l=!1;document.querySelectorAll("tbody tr").forEach((r)=>{const e=Array.from(r.querySelectorAll("td"));if(!e.length)return;if(e.every((s)=>!(s.innerText||"").trim()))return;if(e.length===1)return;if(e.length!==t.length){l=!0;return}const o={};e.forEach((s,c)=>{const u=t[c]||`col${c+1}`;o[u]=(s.innerText||"").replace(/\s+/g," ").trim()});const i=Object.values(o).filter(Boolean);if(i.length<=1&&/^(暂无数据|没有数据|暂无记录|空空如也)$/.test(i[0]||""))return;n.push(o)});return{heads:t,rows:n,misaligned:l}}p({site:"xianyu",name:"seller-list",access:"read",description:`读卖家工作台列表页(${Object.keys(a).join(" / ")})—— 表头即列,动态返回`,domain:"seller.goofish.com",strategy:d.COOKIE,navigateBefore:!1,browser:!0,args:[{name:"page",required:!0,positional:!0,help:`列表页:${Object.keys(a).join(" / ")}`}],func:async(t,n)=>{const l=String(n.page||"").trim(),r=a[l];if(!r)throw new g(`page 只能是 ${Object.keys(a).join(" / ")},收到 "${l}"`);await t.goto(`https://seller.goofish.com/?site=COMMONPRO${r.hash}`);await t.wait(4);const e=await f(t,m);if(e.error==="no-table")throw new h(`${r.label} 未渲染出表格 —— 可能未登录 / 页面改版 / 无权限`);if(e.misaligned&&(e.rows||[]).length===0)throw new h(`${r.label} 表格单元格数与表头(${(e.heads||[]).length}列)不一致,疑似带选择/展开列,需为该页单独适配以免列错位`);return e.rows||[]}});export const __test__={readTable:m,PAGE_ROUTES:a};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import{accessSync as D,constants as H,existsSync as N,mkdtempSync as j,rmSync as F,writeFileSync as X}from"node:fs";import{tmpdir as Y}from"node:os";import{delimiter as G,join as k}from"node:path";import{spawn as R,spawnSync as W}from"node:child_process";import{CliError as z}from"@jackwener/opencli/errors";const d=(u)=>new Promise((c)=>setTimeout(c,u)),s=(u)=>new z("CAPTCHA",`闲鱼验证码:系统鼠标 ${u}`);function U(u){return(process.env.PATH||"").split(G).some((c)=>N(k(c,u)))}export function systemMouseAvailable(u){if(process.platform!=="linux"||!u.getActivePage?.()||!["ydotool","ydotoold","qdbus6","journalctl"].every(U)||!process.env.XDG_CURRENT_DESKTOP?.includes("KDE"))return!1;try{D("/dev/uinput",H.W_OK);return!0}catch(c){if(c.code!=="EACCES"&&c.code!=="ENOENT")throw c;return!1}}export async function createSystemMouse(u,c){const v=j(k(Y(),"ppcli-mouse-")),y=k(v,"input.sock"),M=`__ppcli_mouse_${process.pid}_${Date.now()}`,f=u,b=`(() => {
|
|
2
|
+
const key = ${JSON.stringify(M)};
|
|
3
|
+
if (!window[key]) {
|
|
4
|
+
const state = { point: null };
|
|
5
|
+
state.listener = e => { state.point = { x: e.clientX, y: e.clientY, time: performance.timeOrigin + e.timeStamp }; };
|
|
6
|
+
document.addEventListener('mousemove', state.listener, true);
|
|
7
|
+
window[key] = state;
|
|
8
|
+
}
|
|
9
|
+
return window[key].point;
|
|
10
|
+
})()`,J=`(() => {
|
|
11
|
+
const key = ${JSON.stringify(M)}, state = window[key];
|
|
12
|
+
if (state) document.removeEventListener('mousemove', state.listener, true);
|
|
13
|
+
delete window[key];
|
|
14
|
+
return true;
|
|
15
|
+
})()`;let O;const a=R("ydotoold",[`--socket-path=${y}`],{stdio:["ignore","ignore","pipe"]});a.on("error",(t)=>{O=t});a.stderr.on("data",(t)=>console.error("[xianyu-captcha] mouse daemon",{message:String(t).trim()}));const A={...process.env,YDOTOOL_SOCKET:y};let T,g=2,h=2;const _=new Map;let q=0,K,C;function x(t,e,i=!1){const r=W(t,e,{encoding:"utf8",timeout:3000});if(r.error)throw r.error;if(r.status!==0&&!(i&&r.status===1&&!r.stderr.trim()))throw s(`${t} 调用失败:${r.stderr||r.stdout||r.status}`);return r.stdout.trim()}async function P(t=!1){const e=`ppcli-pointer-${M}-${++q}`,i=`${e}:`,r=k(v,"cursor.js");X(r,`
|
|
16
|
+
var matches = workspace.windowList().filter(function(w) { return ${C?`String(w.internalId) === ${JSON.stringify(C)}`:`String(w.resourceClass).toLowerCase().indexOf('chrome') >= 0 && String(w.caption).indexOf(${JSON.stringify(K)}) >= 0`}; });
|
|
17
|
+
var target = matches.length === 1 ? matches[0] : null;
|
|
18
|
+
if (!target) print(${JSON.stringify(i)} + JSON.stringify({error: 'Verification window is unavailable or ambiguous'}));
|
|
19
|
+
else {
|
|
20
|
+
if (${t} || !target.active) { target.minimized = false; workspace.activeWindow = target; }
|
|
21
|
+
print(${JSON.stringify(i)} + JSON.stringify({cursor: workspace.cursorPos, geometry: target.frameGeometry, active: target.active, windowId: String(target.internalId)}));
|
|
22
|
+
}
|
|
23
|
+
`);const o=x("qdbus6",["org.kde.KWin","/Scripting","org.kde.kwin.Scripting.loadScript",r,e]);try{x("qdbus6",["org.kde.KWin",`/Scripting/Script${o}`,"org.kde.kwin.Script.run"]);for(let n=0;n<5;n++){await d(40);const l=x("journalctl",["--user","--since","10 seconds ago","-o","cat","--grep",i,"-n","1"],!0),m=l.indexOf(i);if(m>=0){const w=JSON.parse(l.slice(m+i.length));if(w.error)throw s(w.error);return w}}throw s("读取桌面鼠标位置超时")}finally{x("qdbus6",["org.kde.KWin","/Scripting","org.kde.kwin.Scripting.unloadScript",e])}}function p(t){const e=W("ydotool",t,{env:A,encoding:"utf8",timeout:2000});if(e.error)throw e.error;if(e.status!==0)throw s(`输入失败:${e.stderr||e.stdout||e.status}`)}async function S(){const t=await f.evaluate(b),e=await c();let i;if(e?.frameIndex!==void 0){_.set(e.frameId,e.frameIndex);const o=await f.evaluateInFrame(b,e.frameIndex);if(o)i={...o,x:e.offset.x+o.x*e.offset.scaleX,y:e.offset.y+o.y*e.offset.scaleY}}const r=i&&(!t||i.time>t.time)?i:t;if(r)T=r;return T}async function I(t){for(let e=0;e<40;e++){const i=await S();if(!i)throw s("没有收到桌面鼠标事件,尚未按下滑块");const r=t.x-i.x,o=t.y-i.y;if(Math.hypot(r,o)<=2)return;const n=Math.round(Math.max(-30,Math.min(30,r/g))),l=Math.round(Math.max(-30,Math.min(30,o/h)));p(["mousemove","-x",String(n),"-y",String(l)]);await d(35);const m=await S();if(m&&m.time>i.time){const w=n?(m.x-i.x)/n:g,E=l?(m.y-i.y)/l:h;if(w>=0.25&&w<=4)g=w;if(E>=0.25&&E<=4)h=E}else throw s("鼠标离开了目标页面或事件未送达,停止拖动")}throw s("无法校准滑块位置,尚未完成拖动")}async function L(){if(a.exitCode===null&&a.signalCode===null){const t=new Promise((e)=>a.once("exit",e));a.kill();await Promise.race([t,d(500)]);if(a.exitCode===null&&a.signalCode===null)a.kill("SIGKILL");await Promise.race([t,d(500)])}await f.evaluate(J).catch((t)=>console.error("[xianyu-captcha] pointer cleanup failed",{error:t}));for(const t of await f.frames())if(_.has(t.frameId))await f.evaluateInFrame(J,t.index).catch((e)=>console.error("[xianyu-captcha] frame cleanup failed",{error:e}));F(v,{recursive:!0,force:!0})}try{for(let r=0;!N(y)&&r<25;r++){if(O)throw O;if(a.exitCode!==null)throw s(`虚拟鼠标启动失败(${a.exitCode})`);await d(40)}if(!N(y))throw s("虚拟鼠标启动超时");await d(1000);K=await f.evaluate("document.title");const t=await P(!0);if(!t.active)throw s("验证窗口尚未激活");C=t.windowId;const e=await f.evaluate("({ top: outerHeight - innerHeight, width: innerWidth, height: innerHeight })");await S();const i={x:t.geometry.x+e.width/3,y:t.geometry.y+e.top+e.height/3};for(let r=0;r<60;r++){const{cursor:o}=await P(),n=i.x-o.x,l=i.y-o.y;if(Math.hypot(n,l)<4)break;p(["mousemove","-x",String(Math.round(Math.max(-100,Math.min(100,n/2)))),"-y",String(Math.round(Math.max(-100,Math.min(100,l/2))))]);await d(30)}await d(100);if(!await S())throw s("目标 Chrome 未收到鼠标事件");return{async click(r){await I(r);p(["click","0xC0"])},async drag(r){await I(r[0]);p(["click","0x40"]);try{let o=r[0];for(const n of r.slice(1)){p(["mousemove","-x",String(Math.round((n.x-o.x)/g)),"-y",String(Math.round((n.y-o.y)/h))]);o=n;await d(16+Math.random()*12)}if((await c())?.blocked)await I(r.at(-1))}finally{p(["click","0x80"])}},close:L}}catch(t){console.error("[xianyu-captcha] system input failed",{error:t});await L();throw t}}
|
package/clis/xianyu/watch.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{AuthRequiredError as
|
|
1
|
+
import{evaluateWithCaptcha as w}from"./captcha.js";import{AuthRequiredError as d,CommandExecutionError as u}from"@jackwener/opencli/errors";import{registerWatch as _}from"../_shared/watch.js";import{buildChatUrl as p,buildInboxUrl as o,requireEvaluateObject as g}from"./im.js";const y=/"lwp"\s*:\s*"\/s\//,S=[300,700,1000,1500],c=1e4,h=`
|
|
2
2
|
(() => {
|
|
3
3
|
const rows = Array.from(document.querySelectorAll('#conv-list-scrollable [class*="conversation-item"]'));
|
|
4
4
|
const sessions = [];
|
|
@@ -40,4 +40,4 @@ import{AuthRequiredError as m,CommandExecutionError as u}from"@jackwener/opencli
|
|
|
40
40
|
blocked: /验证码|安全验证|异常访问/.test(bodyText),
|
|
41
41
|
};
|
|
42
42
|
})()
|
|
43
|
-
`;async function a(t){const s=
|
|
43
|
+
`;async function a(t){const s=g(await w(t,h),"watch sessions");if(!s.rows&&s.requiresAuth)throw new d("www.goofish.com","Xianyu IM requires a logged-in browser session");if(!s.rows&&s.blocked)throw new d("www.goofish.com","Xianyu IM is blocked by verification or risk control");if(s.rows&&!s.sessions.length)throw new u("闲鱼会话列表渲染出来了,但行上读不到 sessionInfo:页面结构可能改版了");return s}function l(t,s){const n=[];for(const e of t){if(!e.message_id||!e.peer_user_id)continue;const i=e.sender_id===e.peer_user_id;if(!i&&!s?.includeOwn)continue;n.push({event:"dm",id:e.message_id,ts:e.ts,conversation_id:e.session_id,sender:{id:e.sender_id,name:i?e.peer_name:""},text:e.text,url:e.item_id?p(e.item_id,e.peer_user_id):o(),raw:{session_id:e.session_id,session_type:e.session_type,peer_user_id:e.peer_user_id,item_id:e.item_id,item_title:e.item_title,price:e.price,seller_id:e.seller_id,content_type:e.content_type,unread_count:e.unread,from_peer:i}})}return n}function m(t){return t.map((s)=>`${s.session_id}:${s.message_id}`).join("|")}_({site:"xianyu",name:"watch",domain:"www.goofish.com",description:"闲鱼私信监听:旁听网页版 IM 自己的 WebSocket 推送,一有新消息就读已打开的会话列表,按 NDJSON 逐条输出",watchUrl:o(),pattern:"wss-goofish.dingtalk.com",parse:async(t,s)=>{if(!y.test(String(t?.responsePreview??"")))return null;const n=Number(t?.timestamp??0);if(n&&s.state.readStartedAt&&n<s.state.readStartedAt)return null;s.state.readStartedAt=Date.now();const e=s.state.fingerprint;let i=null;for(const r of S){await new Promise((f)=>setTimeout(f,r));i=await a(s.page);if(m(i.sessions)!==e)break}s.state.fingerprint=m(i.sessions);return l(i.sessions,s)},reconcile:async(t,s)=>{const n=await(t.getCurrentUrl?t.getCurrentUrl().catch(()=>""):Promise.resolve(""));if(!/https:\/\/www\.goofish\.com\/im\b/.test(String(n||"")))await t.goto(o());const e=Date.now()+c;let i=await a(t);while(!i.rows&&Date.now()<e){await new Promise((r)=>setTimeout(r,500));i=await a(t)}if(!i.rows)throw new u(`闲鱼会话列表 ${c/1000} 秒内没有渲染出来(收件箱为空,或页面没加载完)`);s.state.fingerprint=m(i.sessions);return l(i.sessions,s)}});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import*as c from"node:fs";import*as a from"node:path";import{fileURLToPath as x,pathToFileURL as S}from"node:url";import{getErrorMessage as F}from"./errors.js";import{fullName as R,getRegistry as A}from"./registry.js";import{findPackageRoot as C,getCliManifestPath as j}from"./package-paths.js";import{isRecord as M}from"./utils.js";const y=C(x(import.meta.url)),w=a.join(y,"clis"),h=j(w),k=/\bcli\s*\(|\bregisterSiteAuthCommands\s*\(|\bregisterArticleLogin\s*\(|\bregisterTokenAuth\s*\(|\bregisterWatch\s*\(|\bmake[A-Z]\w*Command\s*\(/;export class ManifestImportError extends Error{filePath;cause;constructor(e,t){super(`failed to scan ${e}: ${F(t)}`);this.filePath=e;this.cause=t;this.name="ManifestImportError"}}function I(e){return e.map((t)=>({name:t.name,type:t.type??"str",default:t.default,required:!!t.required,valueRequired:!!t.valueRequired||void 0,positional:t.positional||void 0,help:t.help??"",choices:t.choices,fromFile:t.fromFile||void 0,requiredUnlessFile:t.requiredUnlessFile||void 0,generatedFileFor:t.generatedFileFor||void 0}))}function N(e,t){const r=a.basename(e,a.extname(e));return`${t}/${r}.js`}export function normalizeManifestPath(e){return e.replace(/\\/g,"/")}function O(e,t){return normalizeManifestPath(a.relative(t,e))}function T(e,t){return M(e)&&typeof e.site==="string"&&e.site===t&&typeof e.name==="string"&&(e.access==="read"||e.access==="write")&&Array.isArray(e.args)}function E(e,t,r){return{site:e.site,name:e.name,aliases:e.aliases,description:e.description??"",access:e.access,example:e.example,domain:e.domain,strategy:(e.strategy??"public").toString().toLowerCase(),browser:e.browser??!0,args:I(e.args),columns:e.columns,defaultFormat:e.defaultFormat,type:"js",modulePath:t,sourceFile:r,navigateBefore:e.navigateBefore,siteSession:e.siteSession,defaultWindowMode:e.defaultWindowMode}}export async function loadManifestEntries(e,t,r=(u)=>import(u),n=w){let u;try{u=c.readFileSync(e,"utf-8")}catch(s){throw new ManifestImportError(e,s)}if(!k.test(u))return[];try{const s=N(e,t),o=A(),i=new Map(o.entries()),d=await r(S(e).href),l=Object.values(M(d)?d:{}).filter((f)=>T(f,t)),p=l.length>0?l:[...o.entries()].filter(([f,m])=>{if(m.site!==t)return!1;const v=i.get(f);return!v||v!==m}).map(([,f])=>f),g=O(e,n),b=new Set;return p.filter((f)=>{const m=R(f);if(b.has(m))return!1;b.add(m);return!0}).sort((f,m)=>f.name.localeCompare(m.name)).map((f)=>E(f,s,g))}catch(s){throw new ManifestImportError(e,s)}}export async function scanClisDir(e,t=(r)=>import(r)){const r=new Map,n=[];if(!c.existsSync(e))return{entries:[],failures:n};for(const s of c.readdirSync(e)){const o=a.join(e,s);if(!c.statSync(o).isDirectory())continue;for(const i of c.readdirSync(o))if(i.endsWith(".js")&&!i.endsWith(".d.js")&&!i.endsWith(".test.js")&&i!=="index.js"){const d=a.join(o,i);try{const l=await loadManifestEntries(d,s,t,e);for(const p of l){const g=`${p.site}/${p.name}`;r.set(g,p)}}catch(l){if(l instanceof ManifestImportError){n.push(l);continue}throw l}}}return{entries:[...r.values()].sort((s,o)=>s.site.localeCompare(o.site)||s.name.localeCompare(o.name)),failures:n}}export async function buildManifest(){return scanClisDir(w)}export function serializeManifest(e){return`${JSON.stringify(e,null,2)}
|
|
2
|
+
import*as c from"node:fs";import*as a from"node:path";import{fileURLToPath as x,pathToFileURL as S}from"node:url";import{getErrorMessage as F}from"./errors.js";import{fullName as R,getRegistry as A}from"./registry.js";import{findPackageRoot as C,getCliManifestPath as j}from"./package-paths.js";import{isRecord as M}from"./utils.js";const y=C(x(import.meta.url)),w=a.join(y,"clis"),h=j(w),k=/\bcli\s*\(|\bregisterSiteAuthCommands\s*\(|\bregisterArticleLogin\s*\(|\bregisterTokenAuth\s*\(|\bregisterWatch\s*\(|\bmake[A-Z]\w*Command\s*\(/;export class ManifestImportError extends Error{filePath;cause;constructor(e,t){super(`failed to scan ${e}: ${F(t)}`);this.filePath=e;this.cause=t;this.name="ManifestImportError"}}function I(e){return e.map((t)=>({name:t.name,type:t.type??"str",default:t.default,required:!!t.required,valueRequired:!!t.valueRequired||void 0,positional:t.positional||void 0,help:t.help??"",choices:t.choices,fromFile:t.fromFile||void 0,requiredUnlessFile:t.requiredUnlessFile||void 0,generatedFileFor:t.generatedFileFor||void 0}))}function N(e,t){const r=a.basename(e,a.extname(e));return`${t}/${r}.js`}export function normalizeManifestPath(e){return e.replace(/\\/g,"/")}function O(e,t){return normalizeManifestPath(a.relative(t,e))}function T(e,t){return M(e)&&typeof e.site==="string"&&e.site===t&&typeof e.name==="string"&&(e.access==="read"||e.access==="write")&&Array.isArray(e.args)}function E(e,t,r){return{site:e.site,name:e.name,aliases:e.aliases,description:e.description??"",access:e.access,example:e.example,domain:e.domain,strategy:(e.strategy??"public").toString().toLowerCase(),browser:e.browser??!0,args:I(e.args),columns:e.columns,defaultFormat:e.defaultFormat,type:"js",modulePath:t,sourceFile:r,navigateBefore:e.navigateBefore,siteSession:e.siteSession,sessionKey:e.sessionKey,defaultWindowMode:e.defaultWindowMode}}export async function loadManifestEntries(e,t,r=(u)=>import(u),n=w){let u;try{u=c.readFileSync(e,"utf-8")}catch(s){throw new ManifestImportError(e,s)}if(!k.test(u))return[];try{const s=N(e,t),o=A(),i=new Map(o.entries()),d=await r(S(e).href),l=Object.values(M(d)?d:{}).filter((f)=>T(f,t)),p=l.length>0?l:[...o.entries()].filter(([f,m])=>{if(m.site!==t)return!1;const v=i.get(f);return!v||v!==m}).map(([,f])=>f),g=O(e,n),b=new Set;return p.filter((f)=>{const m=R(f);if(b.has(m))return!1;b.add(m);return!0}).sort((f,m)=>f.name.localeCompare(m.name)).map((f)=>E(f,s,g))}catch(s){throw new ManifestImportError(e,s)}}export async function scanClisDir(e,t=(r)=>import(r)){const r=new Map,n=[];if(!c.existsSync(e))return{entries:[],failures:n};for(const s of c.readdirSync(e)){const o=a.join(e,s);if(!c.statSync(o).isDirectory())continue;for(const i of c.readdirSync(o))if(i.endsWith(".js")&&!i.endsWith(".d.js")&&!i.endsWith(".test.js")&&i!=="index.js"){const d=a.join(o,i);try{const l=await loadManifestEntries(d,s,t,e);for(const p of l){const g=`${p.site}/${p.name}`;r.set(g,p)}}catch(l){if(l instanceof ManifestImportError){n.push(l);continue}throw l}}}return{entries:[...r.values()].sort((s,o)=>s.site.localeCompare(o.site)||s.name.localeCompare(o.name)),failures:n}}export async function buildManifest(){return scanClisDir(w)}export function serializeManifest(e){return`${JSON.stringify(e,null,2)}
|
|
3
3
|
`}export function findManifestMetadataIssues(e){const t=[];for(const r of e){if(!Array.isArray(r.args))continue;for(const n of r.args){if(!n.positional)continue;if((typeof n.help==="string"?n.help.trim():"")==="")t.push({site:r.site,command:r.name,arg:n.name,sourceFile:r.sourceFile,reason:"positional arg missing non-empty `help` text"})}}return t}export function diffRemovedEntries(e,t){const r=new Set(t.map((n)=>`${n.site}/${n.name}`));return e.map((n)=>`${n.site}/${n.name}`).filter((n)=>!r.has(n)).sort()}export function parseBuildManifestArgs(e){let t=0;for(const r of e){if(r==="--allow-removals"){t=Number.POSITIVE_INFINITY;continue}const n=r.match(/^--allow-removals=(\d+)$/);if(n){t=Number.parseInt(n[1],10);continue}}return{allowRemovals:t}}function P(e){try{if(!c.existsSync(e))return null;const t=c.readFileSync(e,"utf-8"),r=JSON.parse(t);return Array.isArray(r)?r:null}catch{return null}}async function L(){if(x(import.meta.url).includes(`${a.sep}dist${a.sep}`)){process.stderr.write(`❌ Refusing to run build-manifest from dist/.
|
|
4
4
|
`+" Stale compiled output silently drops adapters that import renamed/removed exports.\n Run `npm run build-manifest` (or `tsx src/build-manifest.ts`) from the source tree instead.\n");process.exit(1)}const e=parseBuildManifestArgs(process.argv.slice(2)),{entries:t,failures:r}=await buildManifest();if(r.length>0){process.stderr.write(`❌ ${r.length} adapter(s) failed to load:
|
|
5
5
|
`);for(const s of r){const o=a.relative(y,s.filePath)||s.filePath;process.stderr.write(` - ${o}: ${F(s.cause)}
|
package/dist/src/discovery.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import*as i from"node:fs";import*as R from"node:os";import*as c from"node:path";import{fileURLToPath as S,pathToFileURL as p}from"node:url";import A from"js-yaml";import{Strategy as y,registerCommand as E}from"./registry.js";import{getErrorMessage as m}from"./errors.js";import{log as u}from"./logger.js";import{findPackageRoot as k,getCliManifestPath as I}from"./package-paths.js";import{migrateLegacyUserRuntime as x}from"./user-runtime.js";x();export const USER_OPENCLI_DIR=c.join(R.homedir(),".ppcli"),USER_CLIS_DIR=c.join(USER_OPENCLI_DIR,"clis"),PLUGINS_DIR=c.join(USER_OPENCLI_DIR,"plugins");const M=/\b(?:cli|registerSiteAuthCommands|onStartup|onBeforeExecute|onAfterExecute)\s*\(/,h=/\.ya?ml$/i,g=new Set,F=new Map;function T(e,r=y.COOKIE){if(!e)return r;const t=e.toUpperCase();return y[t]??r}const D=k(S(import.meta.url));export async function ensureUserCliCompatShims(e=USER_OPENCLI_DIR){await i.promises.mkdir(e,{recursive:!0});const r=c.join(e,"package.json"),t=`${JSON.stringify({name:"opencli-user-runtime",private:!0,type:"module"},null,2)}
|
|
2
|
-
`;try{if(await i.promises.readFile(r,"utf-8")!==t)await i.promises.writeFile(r,t,"utf-8")}catch{await i.promises.writeFile(r,t,"utf-8")}const a=D,n=c.join(e,"node_modules","@jackwener"),s=c.join(n,"opencli");try{let o=!0;try{if(await i.promises.readlink(s)===a)o=!1}catch{}if(o){await i.promises.mkdir(n,{recursive:!0});try{await i.promises.rm(s,{recursive:!0,force:!0})}catch{}const l=process.platform==="win32"?"junction":"dir";await i.promises.symlink(a,s,l)}}catch(o){u.warn(`Could not create symlink at ${s}: ${m(o)}`)}}export async function ensureUserAdapters(){await i.promises.mkdir(USER_CLIS_DIR,{recursive:!0})}export async function discoverClis(...e){for(const r of e){const t=I(r);try{await i.promises.access(t);const a=await
|
|
1
|
+
import*as i from"node:fs";import*as R from"node:os";import*as c from"node:path";import{fileURLToPath as S,pathToFileURL as p}from"node:url";import A from"js-yaml";import{Strategy as y,registerCommand as E}from"./registry.js";import{getErrorMessage as m}from"./errors.js";import{log as u}from"./logger.js";import{findPackageRoot as k,getCliManifestPath as I}from"./package-paths.js";import{migrateLegacyUserRuntime as x}from"./user-runtime.js";x();export const USER_OPENCLI_DIR=c.join(R.homedir(),".ppcli"),USER_CLIS_DIR=c.join(USER_OPENCLI_DIR,"clis"),PLUGINS_DIR=c.join(USER_OPENCLI_DIR,"plugins");const M=/\b(?:cli|registerSiteAuthCommands|registerWatch|onStartup|onBeforeExecute|onAfterExecute)\s*\(/,h=/\.ya?ml$/i,g=new Set,F=new Map;function T(e,r=y.COOKIE){if(!e)return r;const t=e.toUpperCase();return y[t]??r}const D=k(S(import.meta.url));export async function ensureUserCliCompatShims(e=USER_OPENCLI_DIR){await i.promises.mkdir(e,{recursive:!0});const r=c.join(e,"package.json"),t=`${JSON.stringify({name:"opencli-user-runtime",private:!0,type:"module"},null,2)}
|
|
2
|
+
`;try{if(await i.promises.readFile(r,"utf-8")!==t)await i.promises.writeFile(r,t,"utf-8")}catch{await i.promises.writeFile(r,t,"utf-8")}const a=D,n=c.join(e,"node_modules","@jackwener"),s=c.join(n,"opencli");try{let o=!0;try{if(await i.promises.readlink(s)===a)o=!1}catch{}if(o){await i.promises.mkdir(n,{recursive:!0});try{await i.promises.rm(s,{recursive:!0,force:!0})}catch{}const l=process.platform==="win32"?"junction":"dir";await i.promises.symlink(a,s,l)}}catch(o){u.warn(`Could not create symlink at ${s}: ${m(o)}`)}}export async function ensureUserAdapters(){await i.promises.mkdir(USER_CLIS_DIR,{recursive:!0})}export async function discoverClis(...e){for(const r of e){const t=I(r);try{await i.promises.access(t);const a=await W(t,r);if(a){await j(r,a.manifestFiles);continue}}catch{}await L(r)}}async function W(e,r){try{const t=await i.promises.readFile(e,"utf-8"),a=JSON.parse(t),n=new Set;for(const s of a){if(!s.modulePath)continue;const o=c.resolve(r,s.modulePath);n.add(o);if(s.sourceFile)n.add(c.resolve(r,s.sourceFile));const l={site:s.site,name:s.name,aliases:s.aliases,description:s.description??"",access:s.access,example:s.example,domain:s.domain,strategy:T(s.strategy),browser:s.browser,args:s.args??[],columns:s.columns,defaultFormat:s.defaultFormat,pipeline:s.pipeline,source:s.sourceFile?c.resolve(r,s.sourceFile):o,navigateBefore:s.navigateBefore,siteSession:s.siteSession,sessionKey:s.sessionKey,defaultWindowMode:s.defaultWindowMode,_lazy:!0,_modulePath:o};E(l)}return{manifestFiles:n}}catch(t){u.warn(`Failed to load manifest ${e}: ${m(t)}`);return null}}async function L(e){try{await i.promises.access(e)}catch{return}const r=await i.promises.readdir(e,{withFileTypes:!0});await j(e);const t=r.filter((a)=>a.isDirectory()).map(async(a)=>{const n=a.name,s=c.join(e,n),o=await i.promises.readdir(s);await Promise.all(o.map(async(l)=>{const d=c.join(s,l);if(f(l))return;if(l.endsWith(".ts")&&!l.endsWith(".d.ts")&&!l.endsWith(".test.ts")){u.warn(`Ignoring TypeScript adapter ${d} — .ts adapters are no longer loaded. Rename to .js or convert to JavaScript.`);return}if(l.endsWith(".js")&&!l.endsWith(".d.js")&&!l.endsWith(".test.js")){if(!await w(d))return;await import(p(d).href).catch((C)=>{u.warn(`Failed to load module ${d}: ${m(C)}`)})}}))});await Promise.all(t)}export async function discoverPlugins(){try{await i.promises.access(PLUGINS_DIR)}catch{return}const e=(await i.promises.readdir(PLUGINS_DIR,{withFileTypes:!0})).sort((t,a)=>t.name.localeCompare(a.name)),r=[];for(const t of e){const a=c.join(PLUGINS_DIR,t.name);if(await b(t,a))r.push({entry:t,pluginDir:a})}for(const{entry:t,pluginDir:a}of r)await P(a,{owner:`plugin ${t.name}`,pluginMode:!0});await Promise.all(r.map(async({entry:t,pluginDir:a})=>{await _(a,t.name)}))}async function _(e,r){let t;try{t=await i.promises.readdir(e)}catch(n){const s=n.code;if(s!=="ENOENT"&&s!=="ENOTDIR")u.warn(`Failed to scan plugin directory ${e}: ${m(n)}`);return}const a=new Set(t);await Promise.all(t.map(async(n)=>{const s=c.join(e,n);if(f(n))return;if(n.endsWith(".js")&&!n.endsWith(".d.js")){if(!await w(s))return;await import(p(s).href).catch((o)=>{u.warn(`Plugin ${r}/${n}: ${m(o)}`)})}else if(n.endsWith(".ts")&&!n.endsWith(".d.ts")&&!n.endsWith(".test.ts")){const o=n.replace(/\.ts$/,".js");if(a.has(o))return;u.warn(`Plugin ${r}/${n}: no compiled .js found. Run "ppcli plugin update ${r}" to re-transpile, or install esbuild.`)}}))}async function j(e,r){let t;try{t=await i.promises.readdir(e,{withFileTypes:!0})}catch{return}for(const a of t.filter((n)=>n.isDirectory()).sort((n,s)=>n.name.localeCompare(s.name))){const n=c.join(e,a.name);await P(n,{owner:`site ${a.name}`,manifestFiles:r})}}async function P(e,r){let t;try{t=await i.promises.readdir(e)}catch(s){u.warn(`Could not inspect YAML adapters for ${r.owner}: ${m(s)}`);return}const a=new Set(t),n=[];for(const s of t.filter(f).sort()){const o=c.join(e,s);if(!await N(o,r.pluginMode===!0))continue;if(await U(e,s,a,r.manifestFiles))continue;const l=await v(o);if(g.has(l))continue;g.add(l);n.push(s)}if(n.length===0)return;u.warn(`Ignoring ${n.length} YAML adapter${n.length===1?"":"s"} for ${r.owner}: ${n.join(", ")}. .yaml/.yml adapters are no longer loaded; convert them to .js with the cli() API.`)}function f(e){return h.test(e)}async function U(e,r,t,a){const n=r.replace(h,".js");if(!t.has(n))return!1;const s=c.resolve(e,n);if(a)return a.has(s);return K(s)}async function N(e,r){let t;try{t=await i.promises.readFile(e,"utf-8")}catch{return!r}let a;try{a=A.load(t)}catch{return!r}if(!a||typeof a!=="object"||Array.isArray(a))return!1;const n=a;if(typeof n.name!=="string"||!n.name.trim())return!1;return["site","description","access","strategy","browser","domain","args","columns","pipeline","url","request","steps","extract","output","func","method","headers"].some((o)=>(o in n))}async function v(e){try{return await i.promises.realpath(e)}catch{return c.resolve(e)}}async function O(e){const r=await v(e);try{const t=await i.promises.stat(e);return`${r}:${t.mtimeMs}:${t.size}`}catch{return r}}async function K(e){const r=await O(e);let t=F.get(r);if(!t){t=(async()=>{if(!await w(e))return!1;try{await import(p(e).href);return!0}catch(a){u.warn(`Failed to load module ${e}: ${m(a)}`);return!1}})();F.set(r,t)}return t}async function w(e){try{const r=await i.promises.readFile(e,"utf-8");return M.test(r)}catch(r){u.warn(`Failed to inspect module ${e}: ${m(r)}`);return!1}}async function b(e,r){if(e.isDirectory())return!0;if(!e.isSymbolicLink())return!1;try{return(await i.promises.stat(r)).isDirectory()}catch(t){const a=t.code;if(a!=="ENOENT"&&a!=="ENOTDIR")u.warn(`Failed to inspect plugin link ${r}: ${m(t)}`);return!1}}
|
|
@@ -46,6 +46,8 @@ export interface ManifestEntry {
|
|
|
46
46
|
navigateBefore?: boolean | string;
|
|
47
47
|
/** Site session lifecycle defaults — see CliCommand.siteSession */
|
|
48
48
|
siteSession?: 'ephemeral' | 'persistent';
|
|
49
|
+
/** 标签页会话名后缀 — see CliCommand.sessionKey(不进清单的话,懒加载的内置命令会丢掉它) */
|
|
50
|
+
sessionKey?: string;
|
|
49
51
|
/** Default browser window visibility — see CliCommand.defaultWindowMode */
|
|
50
52
|
defaultWindowMode?: 'foreground' | 'background';
|
|
51
53
|
}
|