publishport-opencli 1.8.5-pp.21 → 1.8.5-pp.23
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 +302 -0
- package/clis/bilibili/coin.js +1 -0
- package/clis/bilibili/comment-del.js +1 -0
- package/clis/bilibili/comment-like.js +1 -0
- package/clis/bilibili/dm-list.js +1 -0
- package/clis/bilibili/dm-read.js +1 -0
- package/clis/bilibili/dm-send.js +1 -0
- package/clis/bilibili/favorite-add.js +1 -0
- package/clis/bilibili/like.js +1 -0
- package/clis/bilibili/utils.js +26 -7
- package/clis/jike/auth.js +4 -2
- package/clis/jike/feed.js +3 -3
- package/clis/jike/like.js +7 -11
- package/clis/jike/notifications.js +2 -2
- package/clis/jike/repost.js +22 -23
- package/clis/jike/search.js +3 -3
- package/dist/src/browser/cdp.js +1 -1
- package/package.json +1 -1
package/cli-manifest.json
CHANGED
|
@@ -3862,6 +3862,54 @@
|
|
|
3862
3862
|
"sourceFile": "bilibili/categories.js",
|
|
3863
3863
|
"navigateBefore": "https://member.bilibili.com"
|
|
3864
3864
|
},
|
|
3865
|
+
{
|
|
3866
|
+
"site": "bilibili",
|
|
3867
|
+
"name": "coin",
|
|
3868
|
+
"description": "给 B站视频投币(官方 API,需登录;消耗硬币不可撤销,需 --execute)",
|
|
3869
|
+
"access": "write",
|
|
3870
|
+
"domain": "www.bilibili.com",
|
|
3871
|
+
"strategy": "cookie",
|
|
3872
|
+
"browser": true,
|
|
3873
|
+
"args": [
|
|
3874
|
+
{
|
|
3875
|
+
"name": "bvid",
|
|
3876
|
+
"type": "str",
|
|
3877
|
+
"required": true,
|
|
3878
|
+
"positional": true,
|
|
3879
|
+
"help": "Video BV ID / URL / b23.tv short link"
|
|
3880
|
+
},
|
|
3881
|
+
{
|
|
3882
|
+
"name": "count",
|
|
3883
|
+
"type": "int",
|
|
3884
|
+
"default": 1,
|
|
3885
|
+
"required": false,
|
|
3886
|
+
"help": "投币数量,1 或 2(单视频累计上限 2 枚)"
|
|
3887
|
+
},
|
|
3888
|
+
{
|
|
3889
|
+
"name": "with-like",
|
|
3890
|
+
"type": "boolean",
|
|
3891
|
+
"required": false,
|
|
3892
|
+
"help": "投币同时点赞"
|
|
3893
|
+
},
|
|
3894
|
+
{
|
|
3895
|
+
"name": "execute",
|
|
3896
|
+
"type": "boolean",
|
|
3897
|
+
"required": false,
|
|
3898
|
+
"help": "Actually spend coins. Without it the command refuses to write."
|
|
3899
|
+
}
|
|
3900
|
+
],
|
|
3901
|
+
"columns": [
|
|
3902
|
+
"bvid",
|
|
3903
|
+
"coined",
|
|
3904
|
+
"total",
|
|
3905
|
+
"status",
|
|
3906
|
+
"url"
|
|
3907
|
+
],
|
|
3908
|
+
"type": "js",
|
|
3909
|
+
"modulePath": "bilibili/coin.js",
|
|
3910
|
+
"sourceFile": "bilibili/coin.js",
|
|
3911
|
+
"navigateBefore": "https://www.bilibili.com"
|
|
3912
|
+
},
|
|
3865
3913
|
{
|
|
3866
3914
|
"site": "bilibili",
|
|
3867
3915
|
"name": "comment",
|
|
@@ -3910,6 +3958,87 @@
|
|
|
3910
3958
|
"sourceFile": "bilibili/comment.js",
|
|
3911
3959
|
"navigateBefore": "https://www.bilibili.com"
|
|
3912
3960
|
},
|
|
3961
|
+
{
|
|
3962
|
+
"site": "bilibili",
|
|
3963
|
+
"name": "comment-del",
|
|
3964
|
+
"description": "删除自己发的 B站视频评论(官方 API,需登录;不可恢复,需 --execute)",
|
|
3965
|
+
"access": "write",
|
|
3966
|
+
"domain": "www.bilibili.com",
|
|
3967
|
+
"strategy": "cookie",
|
|
3968
|
+
"browser": true,
|
|
3969
|
+
"args": [
|
|
3970
|
+
{
|
|
3971
|
+
"name": "bvid",
|
|
3972
|
+
"type": "str",
|
|
3973
|
+
"required": true,
|
|
3974
|
+
"positional": true,
|
|
3975
|
+
"help": "Video BV ID / URL / b23.tv short link"
|
|
3976
|
+
},
|
|
3977
|
+
{
|
|
3978
|
+
"name": "rpid",
|
|
3979
|
+
"type": "str",
|
|
3980
|
+
"required": true,
|
|
3981
|
+
"positional": true,
|
|
3982
|
+
"help": "要删除的评论 rpid(comment 命令发布时返回的 rpid)"
|
|
3983
|
+
},
|
|
3984
|
+
{
|
|
3985
|
+
"name": "execute",
|
|
3986
|
+
"type": "boolean",
|
|
3987
|
+
"required": false,
|
|
3988
|
+
"help": "Actually delete the comment. Without it the command refuses to write."
|
|
3989
|
+
}
|
|
3990
|
+
],
|
|
3991
|
+
"columns": [
|
|
3992
|
+
"bvid",
|
|
3993
|
+
"rpid",
|
|
3994
|
+
"status"
|
|
3995
|
+
],
|
|
3996
|
+
"type": "js",
|
|
3997
|
+
"modulePath": "bilibili/comment-del.js",
|
|
3998
|
+
"sourceFile": "bilibili/comment-del.js",
|
|
3999
|
+
"navigateBefore": "https://www.bilibili.com"
|
|
4000
|
+
},
|
|
4001
|
+
{
|
|
4002
|
+
"site": "bilibili",
|
|
4003
|
+
"name": "comment-like",
|
|
4004
|
+
"description": "给 B站视频评论点赞 / 取消点赞(官方 API,需登录;rpid 从 comments 命令取)",
|
|
4005
|
+
"access": "write",
|
|
4006
|
+
"domain": "www.bilibili.com",
|
|
4007
|
+
"strategy": "cookie",
|
|
4008
|
+
"browser": true,
|
|
4009
|
+
"args": [
|
|
4010
|
+
{
|
|
4011
|
+
"name": "bvid",
|
|
4012
|
+
"type": "str",
|
|
4013
|
+
"required": true,
|
|
4014
|
+
"positional": true,
|
|
4015
|
+
"help": "Video BV ID / URL / b23.tv short link"
|
|
4016
|
+
},
|
|
4017
|
+
{
|
|
4018
|
+
"name": "rpid",
|
|
4019
|
+
"type": "str",
|
|
4020
|
+
"required": true,
|
|
4021
|
+
"positional": true,
|
|
4022
|
+
"help": "评论 rpid(bilibili comments 输出里的 id)"
|
|
4023
|
+
},
|
|
4024
|
+
{
|
|
4025
|
+
"name": "undo",
|
|
4026
|
+
"type": "boolean",
|
|
4027
|
+
"required": false,
|
|
4028
|
+
"help": "取消点赞(默认是点赞)"
|
|
4029
|
+
}
|
|
4030
|
+
],
|
|
4031
|
+
"columns": [
|
|
4032
|
+
"bvid",
|
|
4033
|
+
"rpid",
|
|
4034
|
+
"status",
|
|
4035
|
+
"url"
|
|
4036
|
+
],
|
|
4037
|
+
"type": "js",
|
|
4038
|
+
"modulePath": "bilibili/comment-like.js",
|
|
4039
|
+
"sourceFile": "bilibili/comment-like.js",
|
|
4040
|
+
"navigateBefore": "https://www.bilibili.com"
|
|
4041
|
+
},
|
|
3913
4042
|
{
|
|
3914
4043
|
"site": "bilibili",
|
|
3915
4044
|
"name": "comments",
|
|
@@ -3954,6 +4083,104 @@
|
|
|
3954
4083
|
"sourceFile": "bilibili/comments.js",
|
|
3955
4084
|
"navigateBefore": "https://www.bilibili.com"
|
|
3956
4085
|
},
|
|
4086
|
+
{
|
|
4087
|
+
"site": "bilibili",
|
|
4088
|
+
"name": "dm-list",
|
|
4089
|
+
"description": "B站私信会话列表(talker_id 即对方 UID,供 dm-read / dm-send 用)",
|
|
4090
|
+
"access": "read",
|
|
4091
|
+
"domain": "www.bilibili.com",
|
|
4092
|
+
"strategy": "cookie",
|
|
4093
|
+
"browser": true,
|
|
4094
|
+
"args": [
|
|
4095
|
+
{
|
|
4096
|
+
"name": "limit",
|
|
4097
|
+
"type": "int",
|
|
4098
|
+
"default": 20,
|
|
4099
|
+
"required": false,
|
|
4100
|
+
"help": "返回会话数上限"
|
|
4101
|
+
}
|
|
4102
|
+
],
|
|
4103
|
+
"columns": [
|
|
4104
|
+
"talker_id",
|
|
4105
|
+
"name",
|
|
4106
|
+
"last_content",
|
|
4107
|
+
"unread",
|
|
4108
|
+
"last_time"
|
|
4109
|
+
],
|
|
4110
|
+
"type": "js",
|
|
4111
|
+
"modulePath": "bilibili/dm-list.js",
|
|
4112
|
+
"sourceFile": "bilibili/dm-list.js",
|
|
4113
|
+
"navigateBefore": "https://www.bilibili.com"
|
|
4114
|
+
},
|
|
4115
|
+
{
|
|
4116
|
+
"site": "bilibili",
|
|
4117
|
+
"name": "dm-read",
|
|
4118
|
+
"description": "读取与某 B站用户的私信记录(近 30 条;target 为 UID / 用户名)",
|
|
4119
|
+
"access": "read",
|
|
4120
|
+
"domain": "www.bilibili.com",
|
|
4121
|
+
"strategy": "cookie",
|
|
4122
|
+
"browser": true,
|
|
4123
|
+
"args": [
|
|
4124
|
+
{
|
|
4125
|
+
"name": "target",
|
|
4126
|
+
"type": "str",
|
|
4127
|
+
"required": true,
|
|
4128
|
+
"positional": true,
|
|
4129
|
+
"help": "对方 UID / 用户名(dm-list 的 talker_id)"
|
|
4130
|
+
}
|
|
4131
|
+
],
|
|
4132
|
+
"columns": [
|
|
4133
|
+
"time",
|
|
4134
|
+
"direction",
|
|
4135
|
+
"sender_uid",
|
|
4136
|
+
"content",
|
|
4137
|
+
"msg_type"
|
|
4138
|
+
],
|
|
4139
|
+
"type": "js",
|
|
4140
|
+
"modulePath": "bilibili/dm-read.js",
|
|
4141
|
+
"sourceFile": "bilibili/dm-read.js",
|
|
4142
|
+
"navigateBefore": "https://www.bilibili.com"
|
|
4143
|
+
},
|
|
4144
|
+
{
|
|
4145
|
+
"site": "bilibili",
|
|
4146
|
+
"name": "dm-send",
|
|
4147
|
+
"description": "给 B站用户发私信(纯文本,官方 API,需登录;不可撤回,需 --execute)",
|
|
4148
|
+
"access": "write",
|
|
4149
|
+
"domain": "www.bilibili.com",
|
|
4150
|
+
"strategy": "cookie",
|
|
4151
|
+
"browser": true,
|
|
4152
|
+
"args": [
|
|
4153
|
+
{
|
|
4154
|
+
"name": "target",
|
|
4155
|
+
"type": "str",
|
|
4156
|
+
"required": true,
|
|
4157
|
+
"positional": true,
|
|
4158
|
+
"help": "对方 UID / 用户名(dm-list 的 talker_id)"
|
|
4159
|
+
},
|
|
4160
|
+
{
|
|
4161
|
+
"name": "message",
|
|
4162
|
+
"type": "str",
|
|
4163
|
+
"required": true,
|
|
4164
|
+
"positional": true,
|
|
4165
|
+
"help": "私信内容(纯文本)"
|
|
4166
|
+
},
|
|
4167
|
+
{
|
|
4168
|
+
"name": "execute",
|
|
4169
|
+
"type": "boolean",
|
|
4170
|
+
"required": false,
|
|
4171
|
+
"help": "Actually send the message. Without it the command refuses to write."
|
|
4172
|
+
}
|
|
4173
|
+
],
|
|
4174
|
+
"columns": [
|
|
4175
|
+
"receiver_id",
|
|
4176
|
+
"msg_key",
|
|
4177
|
+
"status"
|
|
4178
|
+
],
|
|
4179
|
+
"type": "js",
|
|
4180
|
+
"modulePath": "bilibili/dm-send.js",
|
|
4181
|
+
"sourceFile": "bilibili/dm-send.js",
|
|
4182
|
+
"navigateBefore": "https://www.bilibili.com"
|
|
4183
|
+
},
|
|
3957
4184
|
{
|
|
3958
4185
|
"site": "bilibili",
|
|
3959
4186
|
"name": "download",
|
|
@@ -4156,6 +4383,47 @@
|
|
|
4156
4383
|
"sourceFile": "bilibili/favorite.js",
|
|
4157
4384
|
"navigateBefore": "https://www.bilibili.com"
|
|
4158
4385
|
},
|
|
4386
|
+
{
|
|
4387
|
+
"site": "bilibili",
|
|
4388
|
+
"name": "favorite-add",
|
|
4389
|
+
"description": "把 B站视频加入 / 移出收藏夹(官方 API,需登录;--folder 缺省用默认收藏夹)",
|
|
4390
|
+
"access": "write",
|
|
4391
|
+
"domain": "www.bilibili.com",
|
|
4392
|
+
"strategy": "cookie",
|
|
4393
|
+
"browser": true,
|
|
4394
|
+
"args": [
|
|
4395
|
+
{
|
|
4396
|
+
"name": "bvid",
|
|
4397
|
+
"type": "str",
|
|
4398
|
+
"required": true,
|
|
4399
|
+
"positional": true,
|
|
4400
|
+
"help": "Video BV ID / URL / b23.tv short link"
|
|
4401
|
+
},
|
|
4402
|
+
{
|
|
4403
|
+
"name": "folder",
|
|
4404
|
+
"type": "str",
|
|
4405
|
+
"required": false,
|
|
4406
|
+
"help": "目标收藏夹 ID 或标题(缺省用默认收藏夹)"
|
|
4407
|
+
},
|
|
4408
|
+
{
|
|
4409
|
+
"name": "remove",
|
|
4410
|
+
"type": "boolean",
|
|
4411
|
+
"required": false,
|
|
4412
|
+
"help": "从收藏夹移出(默认是加入)"
|
|
4413
|
+
}
|
|
4414
|
+
],
|
|
4415
|
+
"columns": [
|
|
4416
|
+
"bvid",
|
|
4417
|
+
"folder_id",
|
|
4418
|
+
"folder",
|
|
4419
|
+
"status",
|
|
4420
|
+
"url"
|
|
4421
|
+
],
|
|
4422
|
+
"type": "js",
|
|
4423
|
+
"modulePath": "bilibili/favorite-add.js",
|
|
4424
|
+
"sourceFile": "bilibili/favorite-add.js",
|
|
4425
|
+
"navigateBefore": "https://www.bilibili.com"
|
|
4426
|
+
},
|
|
4159
4427
|
{
|
|
4160
4428
|
"site": "bilibili",
|
|
4161
4429
|
"name": "feed",
|
|
@@ -4365,6 +4633,40 @@
|
|
|
4365
4633
|
"sourceFile": "bilibili/hot.js",
|
|
4366
4634
|
"navigateBefore": "https://www.bilibili.com"
|
|
4367
4635
|
},
|
|
4636
|
+
{
|
|
4637
|
+
"site": "bilibili",
|
|
4638
|
+
"name": "like",
|
|
4639
|
+
"description": "给 B站视频点赞 / 取消点赞(官方 API,需登录;幂等,已点赞再点不会报错)",
|
|
4640
|
+
"access": "write",
|
|
4641
|
+
"domain": "www.bilibili.com",
|
|
4642
|
+
"strategy": "cookie",
|
|
4643
|
+
"browser": true,
|
|
4644
|
+
"args": [
|
|
4645
|
+
{
|
|
4646
|
+
"name": "bvid",
|
|
4647
|
+
"type": "str",
|
|
4648
|
+
"required": true,
|
|
4649
|
+
"positional": true,
|
|
4650
|
+
"help": "Video BV ID / URL / b23.tv short link"
|
|
4651
|
+
},
|
|
4652
|
+
{
|
|
4653
|
+
"name": "undo",
|
|
4654
|
+
"type": "boolean",
|
|
4655
|
+
"required": false,
|
|
4656
|
+
"help": "取消点赞(默认是点赞)"
|
|
4657
|
+
}
|
|
4658
|
+
],
|
|
4659
|
+
"columns": [
|
|
4660
|
+
"bvid",
|
|
4661
|
+
"action",
|
|
4662
|
+
"status",
|
|
4663
|
+
"url"
|
|
4664
|
+
],
|
|
4665
|
+
"type": "js",
|
|
4666
|
+
"modulePath": "bilibili/like.js",
|
|
4667
|
+
"sourceFile": "bilibili/like.js",
|
|
4668
|
+
"navigateBefore": "https://www.bilibili.com"
|
|
4669
|
+
},
|
|
4368
4670
|
{
|
|
4369
4671
|
"site": "bilibili",
|
|
4370
4672
|
"name": "login",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cli as W,Strategy as X}from"@jackwener/opencli/registry";import{ArgumentError as K,CommandExecutionError as N}from"@jackwener/opencli/errors";import{apiGet as Q,apiPost as Y,requireOkPayload as L,resolveVideoIds as Z}from"./utils.js";W({site:"bilibili",name:"coin",access:"write",description:"给 B站视频投币(官方 API,需登录;消耗硬币不可撤销,需 --execute)",domain:"www.bilibili.com",strategy:X.COOKIE,args:[{name:"bvid",required:!0,positional:!0,help:"Video BV ID / URL / b23.tv short link"},{name:"count",type:"int",default:1,help:"投币数量,1 或 2(单视频累计上限 2 枚)"},{name:"with-like",type:"boolean",help:"投币同时点赞"},{name:"execute",type:"boolean",help:"Actually spend coins. Without it the command refuses to write."}],columns:["bvid","coined","total","status","url"],func:async(D,F)=>{if(!D)throw new N("Browser session required for bilibili coin");const h=Number(F.count??1);if(h!==1&&h!==2)throw new K("bilibili coin --count must be 1 or 2");if(!F.execute)throw new K("Refusing to spend coins: pass --execute to actually pay coins");const{bvid:z,aid:H}=await Z(D,F.bvid),M=`https://www.bilibili.com/video/${z}`,R=await Q(D,"/x/web-interface/archive/coins",{params:{aid:H,bvid:z}}),B=Number(L(R,"archive coins")?.multiply??0);if(B>=2)return[{bvid:z,coined:0,total:B,status:"already-maxed",url:M}];if(B+h>2)throw new K(`已投 ${B} 枚,再投 ${h} 枚会超过单视频 2 枚上限;改用 --count ${2-B}`);const T=await Y(D,"/x/web-interface/coin/add",{params:{aid:H,bvid:z,multiply:h,like:F["with-like"]?1:0}});L(T,"coin add");const U=await Q(D,"/x/web-interface/archive/coins",{params:{aid:H,bvid:z}}),J=Number(L(U,"archive coins")?.multiply??0);if(J<B+h)throw new N(`Bilibili coin did not verify: multiply=${J} after paying ${h}`);return[{bvid:z,coined:h,total:J,status:"coined",url:M}]}});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cli as H,Strategy as J}from"@jackwener/opencli/registry";import{ArgumentError as B,CommandExecutionError as K}from"@jackwener/opencli/errors";import{apiPost as L,requireOkPayload as M,resolveVideoIds as N}from"./utils.js";function Q(h){const f=Number(h);if(!Number.isSafeInteger(f)||f<=0)throw new B("bilibili comment-del rpid must be a positive integer");return f}H({site:"bilibili",name:"comment-del",access:"write",description:"删除自己发的 B站视频评论(官方 API,需登录;不可恢复,需 --execute)",domain:"www.bilibili.com",strategy:J.COOKIE,args:[{name:"bvid",required:!0,positional:!0,help:"Video BV ID / URL / b23.tv short link"},{name:"rpid",required:!0,positional:!0,help:"要删除的评论 rpid(comment 命令发布时返回的 rpid)"},{name:"execute",type:"boolean",help:"Actually delete the comment. Without it the command refuses to write."}],columns:["bvid","rpid","status"],func:async(h,f)=>{if(!h)throw new K("Browser session required for bilibili comment-del");const z=Q(f.rpid);if(!f.execute)throw new B("Refusing to delete: pass --execute to actually delete this comment");const{bvid:D,aid:F}=await N(h,f.bvid),G=await L(h,"/x/v2/reply/del",{params:{oid:F,type:1,rpid:z}});M(G,"reply del");return[{bvid:D,rpid:String(z),status:"deleted"}]}});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cli as m,Strategy as d}from"@jackwener/opencli/registry";import{ArgumentError as a,CommandExecutionError as p}from"@jackwener/opencli/errors";import{apiPost as s,requireOkPayload as c,resolveVideoIds as b}from"./utils.js";function u(e){const i=Number(e);if(!Number.isSafeInteger(i)||i<=0)throw new a("bilibili comment-like rpid must be a positive integer");return i}m({site:"bilibili",name:"comment-like",access:"write",description:"给 B站视频评论点赞 / 取消点赞(官方 API,需登录;rpid 从 comments 命令取)",domain:"www.bilibili.com",strategy:d.COOKIE,args:[{name:"bvid",required:!0,positional:!0,help:"Video BV ID / URL / b23.tv short link"},{name:"rpid",required:!0,positional:!0,help:"评论 rpid(bilibili comments 输出里的 id)"},{name:"undo",type:"boolean",help:"取消点赞(默认是点赞)"}],columns:["bvid","rpid","status","url"],func:async(e,i)=>{if(!e)throw new p("Browser session required for bilibili comment-like");const o=u(i.rpid),r=Boolean(i.undo),{bvid:t,aid:n}=await b(e,i.bvid),l=await s(e,"/x/v2/reply/action",{params:{oid:n,type:1,rpid:o,action:r?0:1}});c(l,"reply action");return[{bvid:t,rpid:String(o),status:r?"unliked":"liked",url:`https://www.bilibili.com/video/${t}#reply${o}`}]}});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cli as R,Strategy as T}from"@jackwener/opencli/registry";import{CommandExecutionError as U}from"@jackwener/opencli/errors";import{apiGet as V,fetchJson as W,requireOkPayload as X}from"./utils.js";export function parseMsgContent(v){const z=String(v??"");try{const b=JSON.parse(z);if(b&&typeof b==="object"){if(typeof b.content==="string")return b.content;if(typeof b.title==="string"||typeof b.text==="string")return[b.title,b.text].filter(Boolean).join(":");return z}}catch{}return z}R({site:"bilibili",name:"dm-list",access:"read",description:"B站私信会话列表(talker_id 即对方 UID,供 dm-read / dm-send 用)",domain:"www.bilibili.com",strategy:T.COOKIE,args:[{name:"limit",type:"int",default:20,help:"返回会话数上限"}],columns:["talker_id","name","last_content","unread","last_time"],func:async(v,z)=>{if(!v)throw new U("Browser session required for bilibili dm-list");const b=Math.max(1,Number(z.limit??20)),L=new URLSearchParams({session_type:"1",group_fold:"1",unfollow_fold:"0",sort_rule:"2",build:"0",mobi_app:"web"}).toString(),N=await W(v,`https://api.vc.bilibili.com/session_svr/v1/session_svr/get_sessions?${L}`),Q=(X(N,"dm get_sessions")?.session_list??[]).slice(0,b),B=[];for(const A of Q){const D=String(A?.talker_id??"");let F="";try{F=(await V(v,"/x/web-interface/card",{params:{mid:D}}))?.data?.card?.name??""}catch{}const H=A?.last_msg??{},K=Number(H?.timestamp??0);B.push({talker_id:D,name:F,last_content:parseMsgContent(H?.content).replace(/\s+/g," ").slice(0,80),unread:Number(A?.unread_count??0),last_time:K?new Date(K*1000).toISOString():""})}return B}});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cli as T,Strategy as V}from"@jackwener/opencli/registry";import{CommandExecutionError as W,EmptyResultError as X}from"@jackwener/opencli/errors";import{fetchJson as Y,getSelfUid as Z,requireOkPayload as _,resolveUid as $,wbiSign as b}from"./utils.js";import{parseMsgContent as h}from"./dm-list.js";T({site:"bilibili",name:"dm-read",access:"read",description:"读取与某 B站用户的私信记录(近 30 条;target 为 UID / 用户名)",domain:"www.bilibili.com",strategy:V.COOKIE,args:[{name:"target",required:!0,positional:!0,help:"对方 UID / 用户名(dm-list 的 talker_id)"}],columns:["time","direction","sender_uid","content","msg_type"],func:async(z,H)=>{if(!z)throw new W("Browser session required for bilibili dm-read");const D=await $(z,String(H.target??"").trim()),K=await Z(z),L=await b(z,{talker_id:D,session_type:1,size:30,begin_seqno:0}),N=new URLSearchParams(L).toString().replace(/\+/g,"%20"),Q=await Y(z,`https://api.vc.bilibili.com/svr_sync/v1/svr_sync/fetch_session_msgs?${N}`),F=_(Q,"dm fetch_session_msgs")?.messages??[];if(!F.length)throw new X(`bilibili dm-read: ${D}`,"与该用户还没有私信往来。");return F.slice().reverse().map((B)=>{const G=Number(B?.timestamp??0);return{time:G?new Date(G*1000).toISOString():"",direction:String(B?.sender_uid??"")===K?"sent":"received",sender_uid:String(B?.sender_uid??""),content:h(B?.content).replace(/\s+/g," ").slice(0,200),msg_type:Number(B?.msg_type??0)}})}});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cli as T,Strategy as W}from"@jackwener/opencli/registry";import{ArgumentError as J,CommandExecutionError as N}from"@jackwener/opencli/errors";import{apiPostAbs as X,getSelfUid as Y,requireOkPayload as Z,resolveUid as $,wbiSign as f}from"./utils.js";import{assertLiteralContent as h}from"../_shared/content-guard.js";const L="A6716E9A-7CE3-47AF-994B-F0B34178D28D";T({site:"bilibili",name:"dm-send",access:"write",description:"给 B站用户发私信(纯文本,官方 API,需登录;不可撤回,需 --execute)",domain:"www.bilibili.com",strategy:W.COOKIE,args:[{name:"target",required:!0,positional:!0,help:"对方 UID / 用户名(dm-list 的 talker_id)"},{name:"message",required:!0,positional:!0,help:"私信内容(纯文本)"},{name:"execute",type:"boolean",help:"Actually send the message. Without it the command refuses to write."}],columns:["receiver_id","msg_key","status"],func:async(z,F)=>{if(!z)throw new N("Browser session required for bilibili dm-send");const G=String(F.message??"").trim();h(G,{label:"私信内容"});if(!G)throw new J("bilibili dm-send message cannot be empty");if(!F.execute)throw new J("Refusing to send: pass --execute to actually send this message");const B=Number(await $(z,String(F.target??"").trim())),H=Number(await Y(z));if(B===H)throw new J("Cannot send a private message to yourself");const Q=await f(z,{w_sender_uid:H,w_receiver_id:B}),R=await X(z,"https://api.vc.bilibili.com/web_im/v1/web_im/send_msg",{query:Q,params:{"msg[sender_uid]":H,"msg[receiver_id]":B,"msg[receiver_type]":1,"msg[msg_type]":1,"msg[msg_status]":0,"msg[content]":JSON.stringify({content:G}),"msg[dev_id]":L,"msg[new_face_version]":0,"msg[timestamp]":Math.floor(Date.now()/1000),from_filework:0,build:0,mobi_app:"web"}}),M=Z(R,"dm send_msg")?.msg_key;if(!M)throw new N("Bilibili send_msg API did not return msg_key for the sent message");return[{receiver_id:String(B),msg_key:String(M),status:"sent"}]}});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cli as G,Strategy as U}from"@jackwener/opencli/registry";import{ArgumentError as X,CommandExecutionError as Q}from"@jackwener/opencli/errors";import{apiGet as V,apiPost as q,getSelfUid as A,requireOkPayload as Y,resolveVideoIds as O}from"./utils.js";async function Z(z,K){const h=await A(z),L=await V(z,"/x/v3/fav/folder/created/list-all",{params:{up_mid:h,type:2,rid:K},signed:!0}),M=Y(L,"fav folder list")?.list??[];if(!Array.isArray(M)||M.length===0)throw new Q("Bilibili fav folder list returned no folders for this account");return M}function _(z,K){const h=String(K??"").trim();if(!h)return z[0];const L=z.find((J)=>String(J.id)===h);if(L)return L;const H=z.filter((J)=>String(J.title??"")===h);if(H.length===1)return H[0];if(H.length>1)throw new X(`收藏夹标题「${h}」有 ${H.length} 个同名,请改用 ID:${H.map((J)=>J.id).join(", ")}`);const M=z.map((J)=>`${J.id}=${J.title}`).join(", ");throw new X(`找不到收藏夹「${h}」;现有收藏夹:${M}`)}G({site:"bilibili",name:"favorite-add",access:"write",description:"把 B站视频加入 / 移出收藏夹(官方 API,需登录;--folder 缺省用默认收藏夹)",domain:"www.bilibili.com",strategy:U.COOKIE,args:[{name:"bvid",required:!0,positional:!0,help:"Video BV ID / URL / b23.tv short link"},{name:"folder",help:"目标收藏夹 ID 或标题(缺省用默认收藏夹)"},{name:"remove",type:"boolean",help:"从收藏夹移出(默认是加入)"}],columns:["bvid","folder_id","folder","status","url"],func:async(z,K)=>{if(!z)throw new Q("Browser session required for bilibili favorite-add");const h=Boolean(K.remove),{bvid:L,aid:H}=await O(z,K.bvid),M=`https://www.bilibili.com/video/${L}`,J=await Z(z,H),N=_(J,K.folder),$=Number(N.fav_state??0)===1,R={bvid:L,folder_id:String(N.id),folder:String(N.title??""),url:M};if($===!h)return[{...R,status:h?"not-in-folder":"already-favorited"}];const B=await q(z,"/x/v3/fav/resource/deal",{params:{rid:H,type:2,add_media_ids:h?"":String(N.id),del_media_ids:h?String(N.id):""}});Y(B,"fav resource deal");const W=(await Z(z,H)).find((D)=>String(D.id)===String(N.id));if(Number(W?.fav_state??0)!==(h?0:1))throw new Q(`Bilibili favorite did not verify: fav_state=${W?.fav_state} after ${h?"remove":"add"}`);return[{...R,status:h?"removed":"favorited"}]}});export const __test__={pickFolder:_};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cli as U,Strategy as q}from"@jackwener/opencli/registry";import{CommandExecutionError as X}from"@jackwener/opencli/errors";import{apiGet as A,apiPost as C,requireOkPayload as Z,resolveVideoIds as N}from"./utils.js";const Y=5000,h=500;async function Q(z,H,B){const D=await A(z,"/x/web-interface/archive/relation",{params:{aid:H,bvid:B}});return Z(D,"archive relation")}async function j(z,H,B,D){const K=Date.now()+Y;let J;while(Date.now()<=K){J=Boolean((await Q(z,H,B))?.like);if(J===D)return;if(typeof z.wait!=="function")break;await z.wait({time:h/1000})}throw new X(`Bilibili like did not verify: relation.like=${J}, expected ${D}`)}U({site:"bilibili",name:"like",access:"write",description:"给 B站视频点赞 / 取消点赞(官方 API,需登录;幂等,已点赞再点不会报错)",domain:"www.bilibili.com",strategy:q.COOKIE,args:[{name:"bvid",required:!0,positional:!0,help:"Video BV ID / URL / b23.tv short link"},{name:"undo",type:"boolean",help:"取消点赞(默认是点赞)"}],columns:["bvid","action","status","url"],func:async(z,H)=>{if(!z)throw new X("Browser session required for bilibili like");const B=Boolean(H.undo),{bvid:D,aid:K}=await N(z,H.bvid),J=`https://www.bilibili.com/video/${D}`,W=B?"unlike":"like",$=await Q(z,K,D);if(Boolean($?.like)===!B)return[{bvid:D,action:W,status:B?"not-liked":"already-liked",url:J}];const G=await C(z,"/x/web-interface/archive/like",{params:{aid:K,like:B?2:1}});Z(G,"archive like");await j(z,K,D,!B);return[{bvid:D,action:W,status:B?"unliked":"liked",url:J}]}});export const __test__={fetchVideoRelation:Q};
|
package/clis/bilibili/utils.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import
|
|
1
|
+
import B from"node:https";import{ArgumentError as G,AuthRequiredError as L,CommandExecutionError as W,EmptyResultError as _}from"@jackwener/opencli/errors";export function resolveBvid(z){const F=String(z).trim();if(/^BV[A-Za-z0-9]+$/i.test(F))return Promise.resolve(F);try{const Z=new URL(F);if(/(\.|^)bilibili\.com$/i.test(Z.hostname)){const V=Z.pathname.match(/\/(?:video|bangumi\/play)\/(BV[A-Za-z0-9]+)/i);if(V)return Promise.resolve(V[1])}}catch{}const Q=F.replace(/^https?:\/\//,"").replace(/^(www\.)?b23\.tv\//,"");if(!/^[A-Za-z0-9]+$/.test(Q))return Promise.reject(Error(`Cannot resolve BV ID from invalid b23.tv short code: ${F}`));const $="https://b23.tv/"+Q;return new Promise((Z,V)=>{const H=B.get($,(O)=>{const T=O.headers.location;if(T){const X=T.match(/\/video\/(BV[A-Za-z0-9]+)/);if(X){O.resume();Z(X[1]);return}}O.resume();V(Error(`Cannot resolve BV ID from short URL: ${F}`))});H.on("error",V);H.setTimeout(4000,()=>{H.destroy();V(Error(`Timeout resolving short URL: ${F}`))})})}export function parsePageArg(z){if(z==null||z==="")return null;if(typeof z==="number"){if(Number.isSafeInteger(z)&&z>=1)return z;throw new G(`--page must be a positive decimal integer, got: ${z}`)}if(typeof z!=="string"||!/^[1-9]\d*$/.test(z))throw new G(`--page must be a positive decimal integer, got: ${String(z)}`);const F=Number(z);if(!Number.isSafeInteger(F))throw new G(`--page is too large: ${z}`);return F}function M(z,F){if(typeof z==="number"&&Number.isSafeInteger(z)&&z>=1)return z;if(typeof z==="string"&&/^[1-9]\d*$/.test(z)){const Q=Number(z);if(Number.isSafeInteger(Q))return Q}throw new W(`Bilibili view API returned a malformed ${F}`)}export function selectVideoPart(z,F){const Q=Array.isArray(z?.pages)?z.pages:null;if(!Q||Q.length===0)throw new W("Bilibili view API did not return pages[] for --page selection");const $=[];for(const V of Q){if(!V||typeof V!=="object"||Array.isArray(V))throw new W("Bilibili view API returned a malformed pages[] entry");if(M(V.page,"page number")===F)$.push(V)}if($.length>1)throw new W(`Bilibili view API returned duplicate page entries for p=${F}`);const Z=$[0];if(!Z){const V=Q.length||z?.videos||1;throw new W(`分P 序号超出范围:p=${F}(该视频共 ${V} 集)`)}M(Z.cid,`cid for p=${F}`);return Z}const R=[46,47,18,2,53,8,23,32,15,50,10,31,58,3,45,35,27,43,5,49,33,9,42,19,29,28,14,39,12,38,41,13,37,48,7,16,24,55,40,61,26,17,0,1,60,51,30,4,22,25,54,21,56,59,6,63,57,62,11,36,20,34,44,52];export function stripHtml(z){return z.replace(/<[^>]+>/g,"").replace(/&[a-z]+;/gi," ").trim()}export function payloadData(z){return z?.data??z}async function S(z){return z.evaluate(`
|
|
2
2
|
async () => {
|
|
3
3
|
const res = await fetch('https://api.bilibili.com/x/web-interface/nav', { credentials: 'include' });
|
|
4
4
|
return await res.json();
|
|
5
5
|
}
|
|
6
|
-
`)}async function
|
|
6
|
+
`)}async function f(z){const Q=(await S(z))?.data?.wbi_img??{},$=Q.img_url??"",Z=Q.sub_url??"",V=$.split("/").pop()?.split(".")[0]??"",H=Z.split("/").pop()?.split(".")[0]??"";return{imgKey:V,subKey:H}}function J(z,F){const Q=z+F;return R.map(($)=>Q[$]||"").join("").slice(0,32)}async function P(z){const{createHash:F}=await import("node:crypto");return F("md5").update(z).digest("hex")}export async function wbiSign(z,F){const{imgKey:Q,subKey:$}=await f(z),Z=J(Q,$),V=Math.floor(Date.now()/1000),H={},O={...F,wts:String(V)};for(const Y of Object.keys(O).sort())H[Y]=String(O[Y]).replace(/[!'()*]/g,"");const T=new URLSearchParams(H).toString().replace(/\+/g,"%20"),X=await P(T+Z);H.w_rid=X;return H}export async function apiGet(z,F,Q={}){const $="https://api.bilibili.com";let Z=Q.params??{};if(Q.signed)Z=await wbiSign(z,Z);const V=new URLSearchParams(Object.fromEntries(Object.entries(Z).map(([O,T])=>[O,String(T)]))).toString().replace(/\+/g,"%20"),H=`${$}${F}?${V}`;return fetchJson(z,H)}export async function fetchJson(z,F){const Q=JSON.stringify(F);return z.evaluate(`
|
|
7
7
|
async () => {
|
|
8
|
-
const res = await fetch(${
|
|
8
|
+
const res = await fetch(${Q}, { credentials: "include" });
|
|
9
9
|
return await res.json();
|
|
10
10
|
}
|
|
11
|
-
`)}export function isAuthLikeBilibiliError(z,
|
|
11
|
+
`)}export function isAuthLikeBilibiliError(z,F){return z===-101||z===-111||z===-403||/csrf|登录|账号|权限|forbidden|permission|login/i.test(String(F??""))}export function requireOkPayload(z,F){if(!z||typeof z!=="object"||Array.isArray(z)||!Object.hasOwn(z,"code"))throw new W(`Bilibili ${F} API returned a malformed payload`);if(z.code!==0){const Q=z.message??"unknown error";if(isAuthLikeBilibiliError(z.code,Q))throw new L("bilibili.com",`Bilibili ${F} API requires login or permission: ${Q} (${z.code})`);throw new W(`Bilibili ${F} API failed: ${Q} (${z.code})`)}return z.data}export async function apiPost(z,F,Q={}){const $=Q.params??{},Z=Object.fromEntries(Object.entries($).map(([O,T])=>[O,String(T)])),V=JSON.stringify(Z),H=JSON.stringify(`https://api.bilibili.com${F}`);return z.evaluate(`
|
|
12
12
|
async () => {
|
|
13
13
|
const csrf = (document.cookie.match(/bili_jct=([^;]+)/) || [])[1] || "";
|
|
14
|
-
const body = new URLSearchParams(${
|
|
14
|
+
const body = new URLSearchParams(${V});
|
|
15
15
|
body.set("csrf", csrf);
|
|
16
|
-
const res = await fetch(${
|
|
16
|
+
const res = await fetch(${H}, {
|
|
17
17
|
method: "POST",
|
|
18
18
|
credentials: "include",
|
|
19
19
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
@@ -28,4 +28,23 @@ import f from"node:https";import{ArgumentError as X,AuthRequiredError as M,Comma
|
|
|
28
28
|
return { code: -1, message: "Non-JSON response (HTTP " + res.status + "): " + text.slice(0, 200) };
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
-
`)}export async function
|
|
31
|
+
`)}export async function apiPostAbs(z,F,Q={}){const $=Q.params??{},Z=Object.fromEntries(Object.entries($).map(([T,X])=>[T,String(X)])),V=JSON.stringify(Z);let H=F;if(Q.query&&Object.keys(Q.query).length>0){const T=new URLSearchParams(Object.fromEntries(Object.entries(Q.query).map(([X,Y])=>[X,String(Y)]))).toString().replace(/\+/g,"%20");H=`${F}?${T}`}const O=JSON.stringify(H);return z.evaluate(`
|
|
32
|
+
async () => {
|
|
33
|
+
const csrf = (document.cookie.match(/bili_jct=([^;]+)/) || [])[1] || "";
|
|
34
|
+
const body = new URLSearchParams(${V});
|
|
35
|
+
body.set("csrf", csrf);
|
|
36
|
+
body.set("csrf_token", csrf);
|
|
37
|
+
const res = await fetch(${O}, {
|
|
38
|
+
method: "POST",
|
|
39
|
+
credentials: "include",
|
|
40
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
41
|
+
body: body.toString(),
|
|
42
|
+
});
|
|
43
|
+
const text = await res.text();
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(text);
|
|
46
|
+
} catch {
|
|
47
|
+
return { code: -1, message: "Non-JSON response (HTTP " + res.status + "): " + text.slice(0, 200) };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
`)}export async function resolveVideoIds(z,F){let Q;try{Q=await resolveBvid(F)}catch(H){throw new G(`Cannot resolve Bilibili BV ID from input: ${String(F??"")}`,H instanceof Error?H.message:String(H))}const $=await apiGet(z,"/x/web-interface/view",{params:{bvid:Q}}),V=requireOkPayload($,"view")?.aid;if(!V)throw new W(`Cannot resolve aid for bvid: ${Q}`);return{bvid:Q,aid:Number(V)}}export async function getSelfUid(z){const Q=(await S(z))?.data?.mid;if(!Q)throw new L("bilibili.com");return String(Q)}export async function resolveUid(z,F){if(/^\d+$/.test(F))return F;const Q=await apiGet(z,"/x/web-interface/wbi/search/type",{params:{search_type:"bili_user",keyword:F},signed:!0});if(!Q||typeof Q!=="object"||Array.isArray(Q)||!Q.data||typeof Q.data!=="object"||Array.isArray(Q.data)||!Object.hasOwn(Q.data,"result"))throw new W(`Bilibili user search returned malformed result for ${F}`);const $=Q.data.result;if(!Array.isArray($))throw new W(`Bilibili user search returned malformed result for ${F}`);if($.length>0){const Z=String($[0]?.mid??"").trim();if(!Z)throw new W(`Bilibili user search returned malformed mid for ${F}`);return Z}throw new _(`bilibili user search: ${F}`,"User may not exist or username may have changed.")}
|
package/clis/jike/auth.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{AuthRequiredError as
|
|
1
|
+
import{AuthRequiredError as L,CommandExecutionError as K}from"@jackwener/opencli/errors";import{registerSiteAuthCommands as Q}from"../_shared/site-auth.js";const N=`(async () => {
|
|
2
2
|
try {
|
|
3
3
|
const token = localStorage.getItem('JK_ACCESS_TOKEN') || '';
|
|
4
4
|
if (!token) return { kind: 'auth', detail: 'Jike JK_ACCESS_TOKEN missing from localStorage (anonymous)' };
|
|
@@ -14,4 +14,6 @@ import{AuthRequiredError as N,CommandExecutionError as L}from"@jackwener/opencli
|
|
|
14
14
|
} catch (e) {
|
|
15
15
|
return { kind: 'exception', detail: String(e && e.message || e) };
|
|
16
16
|
}
|
|
17
|
-
})()`;async function
|
|
17
|
+
})()`;async function T(z){await z.goto("https://web.okjike.com/");await z.wait(2);let F="";for(let G=0;G<30;G++){F=await z.evaluate("() => location.href");if(F.includes("okjike.com"))break;await z.wait(0.5)}if(!F.includes("okjike.com")){await z.screenshot({path:"/tmp/jike_whoami_nav_debug.png"});throw new K(`Jike whoami: navigation never settled on okjike.com (landed on ${F}). Debug screenshot: /tmp/jike_whoami_nav_debug.png`)}let D=await z.evaluate(N);for(let G=0;G<30&&D?.kind==="auth";G++){await z.wait(0.5);D=await z.evaluate(N)}if(D?.kind==="auth")throw new L("web.okjike.com",D.detail);if(D?.kind==="http")throw new K(`HTTP ${D.httpStatus} from Jike users/profile`);if(D?.kind==="exception"){const G=await z.evaluate("() => location.href");if(/^data:/.test(G)||/\/login/.test(F))throw new L("web.okjike.com","Jike session anonymous (redirected to /login)");throw new K(`Jike whoami failed: ${D.detail}`)}if(!D?.ok)throw new K(`Unexpected Jike probe: ${JSON.stringify(D)}`);return{user_id:D.user_id,screen_name:D.screen_name,username:D.username}}Q({site:"jike",domain:"web.okjike.com",loginUrl:"https://web.okjike.com/login",columns:["user_id","screen_name","username"],verify:T,quickCheck:async(z)=>{await z.goto("https://web.okjike.com/robots.txt");return{logged_in:await z.evaluate(`(() => {
|
|
18
|
+
try { return !!localStorage.getItem('JK_ACCESS_TOKEN'); } catch { return false; }
|
|
19
|
+
})()`)===!0}},poll:async(z)=>{const F=await z.evaluate(N);if(!F?.ok)throw new L("web.okjike.com","Waiting for Jike login");return{user_id:F.user_id,screen_name:F.screen_name,username:F.username}}});
|
package/clis/jike/feed.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import{cli as
|
|
2
|
-
${
|
|
1
|
+
import{cli as F,Strategy as G}from"@jackwener/opencli/registry";import{AuthRequiredError as H,EmptyResultError as I}from"@jackwener/opencli/errors";import{getPostDataJs as K}from"./utils.js";F({site:"jike",name:"feed",access:"read",description:"即刻首页动态流",domain:"web.okjike.com",strategy:G.COOKIE,browser:!0,args:[{name:"limit",type:"int",default:20}],columns:["id","author","content","likes","comments","time","url"],func:async(f,C)=>{const z=C.limit||20;await f.goto("https://web.okjike.com");const B=async()=>{return await f.evaluate(`(() => {
|
|
2
|
+
${K}
|
|
3
3
|
|
|
4
4
|
const results = [];
|
|
5
5
|
const seen = new Set();
|
|
@@ -29,4 +29,4 @@ import{cli as v,Strategy as z}from"@jackwener/opencli/registry";import{getPostDa
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
return results;
|
|
32
|
-
})()`)};let f=await
|
|
32
|
+
})()`)};let b=await B();for(let v=0;v<30&&b.length===0;v++){await f.wait(0.5);b=await B()}if(b.length===0){const v=await f.evaluate("() => location.href");if(/\/login/.test(v)||/^data:/.test(v))throw new H("web.okjike.com","Jike feed requires login (redirected to /login)");throw new I("jike feed","No posts rendered on web.okjike.com within 15s. The page structure may have changed.")}if(b.length<z){await f.autoScroll({times:Math.ceil(z/10),delayMs:2000});b=await B()}return b.slice(0,z)}});
|
package/clis/jike/like.js
CHANGED
|
@@ -8,25 +8,21 @@ import{cli as m,Strategy as q}from"@jackwener/opencli/registry";m({site:"jike",n
|
|
|
8
8
|
return { ok: false, message: '未找到点赞按钮' };
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
// 已赞状态挂在内层 SVG 图标的类上(_likeIconLiked_),外层容器 div 的
|
|
12
|
+
// 类名点击前后不变——按容器类名判定会把成功点赞误报为失败(真机踩过)。
|
|
13
|
+
const isLiked = () => !!likeBtn.querySelector('[class*="_likeIconLiked_"]');
|
|
14
|
+
if (isLiked()) {
|
|
14
15
|
return { ok: true, message: '该帖子已赞过' };
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
const beforeCls = likeBtn.className;
|
|
19
|
-
|
|
18
|
+
const beforeCount = (likeBtn.textContent || '').trim();
|
|
20
19
|
likeBtn.click();
|
|
21
20
|
await new Promise(r => setTimeout(r, 1500));
|
|
22
21
|
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
if (afterCls !== beforeCls) {
|
|
22
|
+
// 验证:已赞图标出现或计数变化,任一即成功
|
|
23
|
+
if (isLiked() || (likeBtn.textContent || '').trim() !== beforeCount) {
|
|
26
24
|
return { ok: true, message: '点赞成功' };
|
|
27
25
|
}
|
|
28
|
-
|
|
29
|
-
// 类名未变化,无法确认点赞是否成功
|
|
30
26
|
return { ok: false, message: '点赞状态未确认,请手动检查' };
|
|
31
27
|
} catch (e) {
|
|
32
28
|
return { ok: false, message: e.toString() };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as D,Strategy as E}from"@jackwener/opencli/registry";function F(d){if(!d)return"通知";const h=d.toUpperCase();if(h.includes("LIKE"))return"赞了你";if(h.includes("COMMENT"))return"评论了你";if(h.includes("FOLLOW"))return"关注了你";if(h.includes("REPOST"))return"转发了你";if(h.includes("MENTION"))return"提到了你";if(h.includes("REPLY"))return"回复了你";return d}D({site:"jike",name:"notifications",access:"read",description:"即刻通知",domain:"web.okjike.com",strategy:E.COOKIE,browser:!0,args:[{name:"limit",type:"int",default:20}],columns:["type","user","content","time"],func:async(d,h)=>{const z=h.limit||20;await d.goto("https://web.okjike.com/notification");for(let B=0;B<30;B++){if(await d.evaluate(`(() => document.querySelectorAll('[class*="_item_"]').length > 0)()`))break;await d.wait(0.5)}const C=await d.evaluate(`(() => {
|
|
2
2
|
// 从 React fiber 树中提取通知数据,向上最多走 15 层
|
|
3
3
|
function getNotificationData(element) {
|
|
4
4
|
for (const key of Object.keys(element)) {
|
|
@@ -57,7 +57,7 @@ import{cli as C,Strategy as D}from"@jackwener/opencli/registry";function E(d){if
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
return results;
|
|
60
|
-
})()`);if(
|
|
60
|
+
})()`);if(C.length>0)return C.map((q)=>({type:F(q.actionType),user:q.fromUser,content:q.content.replace(/\n/g," ").slice(0,100),time:q.time})).slice(0,z);await d.autoScroll({times:Math.ceil(z/10),delayMs:2000});return(await d.evaluate(`(() => {
|
|
61
61
|
const results = [];
|
|
62
62
|
const items = document.querySelectorAll('[class*="_item_"]');
|
|
63
63
|
|
package/clis/jike/repost.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as E,Strategy as F}from"@jackwener/opencli/registry";E({site:"jike",name:"repost",access:"write",description:"转发即刻帖子",domain:"web.okjike.com",strategy:F.UI,browser:!0,args:[{name:"id",type:"string",required:!0,positional:!0,help:"帖子 ID"},{name:"text",positional:!0,type:"string",required:!1,help:"转发附言(可选)"}],columns:["status","message"],func:async(b,v)=>{await b.goto(`https://web.okjike.com/originalPost/${v.id}`);for(let h=0;h<30;h++){if(await b.evaluate(`(() => {
|
|
2
2
|
const actions = document.querySelector('[class*="_actions_"]');
|
|
3
3
|
if (!actions) return false;
|
|
4
4
|
const children = Array.from(actions.children).filter(c => c.offsetHeight > 0);
|
|
5
5
|
return !!children[2];
|
|
6
|
-
})()`))break;await b.wait(0.5)}const
|
|
6
|
+
})()`))break;await b.wait(0.5)}const A=await b.evaluate(`(async () => {
|
|
7
7
|
try {
|
|
8
8
|
const actions = document.querySelector('[class*="_actions_"]');
|
|
9
9
|
if (!actions) return { ok: false, message: '未找到操作栏' };
|
|
@@ -15,11 +15,11 @@ import{cli as B,Strategy as C}from"@jackwener/opencli/registry";B({site:"jike",n
|
|
|
15
15
|
} catch (e) {
|
|
16
16
|
return { ok: false, message: e.toString() };
|
|
17
17
|
}
|
|
18
|
-
})()`);if(!
|
|
18
|
+
})()`);if(!A.ok)return[{status:"failed",message:A.message}];await b.wait(1);for(let h=0;h<30;h++){if(await b.evaluate(`(() => {
|
|
19
19
|
return Array.from(document.querySelectorAll('button')).some(
|
|
20
20
|
b => b.textContent?.trim() === '转发动态'
|
|
21
21
|
);
|
|
22
|
-
})()`))break;await b.wait(0.5)}const
|
|
22
|
+
})()`))break;await b.wait(0.5)}const B=await b.evaluate(`(async () => {
|
|
23
23
|
try {
|
|
24
24
|
const btn = Array.from(document.querySelectorAll('button')).find(
|
|
25
25
|
b => b.textContent?.trim() === '转发动态'
|
|
@@ -30,13 +30,13 @@ import{cli as B,Strategy as C}from"@jackwener/opencli/registry";B({site:"jike",n
|
|
|
30
30
|
} catch (e) {
|
|
31
31
|
return { ok: false, message: e.toString() };
|
|
32
32
|
}
|
|
33
|
-
})()`);if(!
|
|
34
|
-
return !!document.querySelector('[contenteditable="true"]');
|
|
35
|
-
})()`))break;await b.wait(0.5)}const
|
|
33
|
+
})()`);if(!B.ok)return[{status:"failed",message:B.message}];await b.wait(2);if(v.text){for(let q=0;q<30;q++){if(await b.evaluate(`(() => {
|
|
34
|
+
return !!document.querySelector('[class*="Modal"] [contenteditable="true"], [class*="modal"] [contenteditable="true"]');
|
|
35
|
+
})()`))break;await b.wait(0.5)}const h=await b.evaluate(`(async () => {
|
|
36
36
|
try {
|
|
37
|
-
const textToInsert = ${JSON.stringify(
|
|
38
|
-
const editor = document.querySelector('[contenteditable="true"]');
|
|
39
|
-
if (!editor) return { ok: false, message: '
|
|
37
|
+
const textToInsert = ${JSON.stringify(v.text)};
|
|
38
|
+
const editor = document.querySelector('[class*="Modal"] [contenteditable="true"], [class*="modal"] [contenteditable="true"]');
|
|
39
|
+
if (!editor) return { ok: false, message: '未找到转发弹窗内的附言输入框' };
|
|
40
40
|
editor.focus();
|
|
41
41
|
const dt = new DataTransfer();
|
|
42
42
|
dt.setData('text/plain', textToInsert);
|
|
@@ -46,23 +46,22 @@ import{cli as B,Strategy as C}from"@jackwener/opencli/registry";B({site:"jike",n
|
|
|
46
46
|
await new Promise(r => setTimeout(r, 500));
|
|
47
47
|
return { ok: true };
|
|
48
48
|
} catch(e) { return { ok: false, message: '附言写入失败: ' + e.toString() }; }
|
|
49
|
-
})()`);if(!
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
})()`);if(!h.ok)return[{status:"failed",message:h.message}]}const D=`(() => {
|
|
50
|
+
return Array.from(document.querySelectorAll('button')).find(b => {
|
|
51
|
+
const text = b.textContent?.trim() || '';
|
|
52
|
+
if (text !== '转发动态' && text !== '发送' && text !== '发布') return false;
|
|
53
|
+
if (b.disabled) return false;
|
|
54
|
+
if (b.closest('[class*="Popover"]')) return false;
|
|
55
|
+
return !!(b.closest('[class*="Modal"], [class*="modal"], [class*="_submitButton_"]'));
|
|
56
|
+
});
|
|
57
|
+
})`;for(let h=0;h<30;h++){if(await b.evaluate(`(() => !!${D}())()`))break;await b.wait(0.5)}const z=await b.evaluate(`(async () => {
|
|
55
58
|
try {
|
|
56
59
|
await new Promise(r => setTimeout(r, 500));
|
|
57
|
-
const btn =
|
|
58
|
-
|
|
59
|
-
// 不匹配"转发动态",避免重复触发 Popover 菜单项
|
|
60
|
-
return (text === '发送' || text === '发布') && !b.disabled;
|
|
61
|
-
});
|
|
62
|
-
if (!btn) return { ok: false, message: '未找到发送按钮' };
|
|
60
|
+
const btn = ${D}();
|
|
61
|
+
if (!btn) return { ok: false, message: '未找到转发弹窗内的确认按钮' };
|
|
63
62
|
btn.click();
|
|
64
63
|
return { ok: true, message: '转发成功' };
|
|
65
64
|
} catch (e) {
|
|
66
65
|
return { ok: false, message: e.toString() };
|
|
67
66
|
}
|
|
68
|
-
})()`);if(
|
|
67
|
+
})()`);if(z.ok)await b.wait(3);return[{status:z.ok?"success":"failed",message:z.message}]}});
|
package/clis/jike/search.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import{cli as
|
|
2
|
-
${
|
|
1
|
+
import{cli as C,Strategy as F}from"@jackwener/opencli/registry";import{EmptyResultError as G}from"@jackwener/opencli/errors";import{getPostDataJs as H}from"./utils.js";C({site:"jike",name:"search",access:"read",description:"搜索即刻帖子",domain:"web.okjike.com",strategy:F.COOKIE,browser:!0,args:[{name:"query",type:"string",required:!0,positional:!0,help:"即刻搜索关键词"},{name:"limit",type:"int",default:20}],columns:["id","author","content","likes","comments","time","url"],func:async(f,v)=>{const z=v.query,h=v.limit||20,B=encodeURIComponent(z);await f.goto(`https://web.okjike.com/search?q=${B}`);const q=async()=>{return await f.evaluate(`(() => {
|
|
2
|
+
${H}
|
|
3
3
|
|
|
4
4
|
const results = [];
|
|
5
5
|
const seen = new Set();
|
|
@@ -26,4 +26,4 @@ import{cli as B,Strategy as C}from"@jackwener/opencli/registry";import{getPostDa
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
return results;
|
|
29
|
-
})()`)};let
|
|
29
|
+
})()`)};let b=await q();for(let A=0;A<30&&b.length===0;A++){await f.wait(0.5);b=await q()}if(b.length===0)throw new G("jike search",`No results rendered for "${z}" within 15s. The keyword may have no matches or the page structure changed.`);if(b.length<h){await f.autoScroll({times:Math.ceil(h/10),delayMs:2000});b=await q()}return b.slice(0,h)}});
|
package/dist/src/browser/cdp.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{WebSocket as I}from"ws";import{request as W}from"node:http";import{request as F}from"node:https";import{buildEvaluateExpression as _}from"./utils.js";import{generateStealthJs as z}from"./stealth.js";import{waitForDomStableJs as S}from"./dom-helpers.js";import{isRecord as H,saveBase64ToFile as M}from"../utils.js";import{getAllElectronApps as w}from"../electron-apps.js";import{BasePage as E}from"./base-page.js";const R=30000;export const CDP_RESPONSE_BODY_CAPTURE_LIMIT=8388608;export class CDPBridge{_ws=null;_idCounter=0;_pending=new Map;_eventListeners=new Map;async connect(G){if(this._ws)throw Error("CDPBridge is already connected. Call close() before reconnecting.");const V=G?.cdpEndpoint??process.env.OPENCLI_CDP_ENDPOINT;if(!V)throw Error("CDP endpoint not provided (pass cdpEndpoint or set OPENCLI_CDP_ENDPOINT)");let Q=V;if(V.startsWith("http")){const K=await j(`${V.replace(/\/$/,"")}/json`),X=U(K);if(!X||!X.webSocketDebuggerUrl)throw Error("No inspectable targets found at CDP endpoint");Q=X.webSocketDebuggerUrl}return new Promise((K,X)=>{const Z=new I(Q),$=(G?.timeout??10)*1000,J=setTimeout(()=>{this._ws=null;Z.close();X(Error("CDP connect timeout"))},$);Z.on("open",async()=>{clearTimeout(J);this._ws=Z;try{await this.send("Page.enable");await this.send("Page.addScriptToEvaluateOnNewDocument",{source:z()});if(G?.initScript)await this.send("Page.addScriptToEvaluateOnNewDocument",{source:G.initScript});if(G?.proxyAuth){this.on("Fetch.requestPaused",(L)=>{const Y=L;if(Y.requestId)this.send("Fetch.continueRequest",{requestId:Y.requestId}).catch(()=>{})});this.on("Fetch.authRequired",(L)=>{const Y=L;if(!Y.requestId)return;this.send("Fetch.continueWithAuth",{requestId:Y.requestId,authChallengeResponse:{response:"ProvideCredentials",username:G.proxyAuth.username,password:G.proxyAuth.password}}).catch(()=>{})});await this.send("Fetch.enable",{handleAuthRequests:!0})}}catch(L){Z.close();X(L instanceof Error?L:Error(String(L)));return}K(new O(this))});Z.on("error",(L)=>{clearTimeout(J);X(L)});Z.on("message",(L)=>{try{const Y=JSON.parse(L.toString());if(Y.id&&this._pending.has(Y.id)){const A=this._pending.get(Y.id);clearTimeout(A.timer);this._pending.delete(Y.id);if(Y.error)A.reject(Error(Y.error.message));else A.resolve(Y.result)}if(Y.method){const A=this._eventListeners.get(Y.method);if(A)for(const B of A)B(Y.params)}}catch(Y){if(process.env.OPENCLI_VERBOSE)console.error("[cdp] Failed to parse WebSocket message:",Y instanceof Error?Y.message:Y)}})})}async close(){if(this._ws){this._ws.close();this._ws=null}for(const G of this._pending.values()){clearTimeout(G.timer);G.reject(Error("CDP connection closed"))}this._pending.clear();this._eventListeners.clear()}async send(G,V={},Q=R){if(!this._ws||this._ws.readyState!==I.OPEN)throw Error("CDP connection is not open");const K=++this._idCounter;return new Promise((X,Z)=>{const $=setTimeout(()=>{this._pending.delete(K);Z(Error(`CDP command '${G}' timed out after ${Q/1000}s`))},Q);this._pending.set(K,{resolve:X,reject:Z,timer:$});this._ws.send(JSON.stringify({id:K,method:G,params:V}))})}on(G,V){let Q=this._eventListeners.get(G);if(!Q){Q=new Set;this._eventListeners.set(G,Q)}Q.add(V)}off(G,V){this._eventListeners.get(G)?.delete(V)}waitForEvent(G,V=15000){return new Promise((Q,K)=>{const X=setTimeout(()=>{this.off(G,Z);K(Error(`Timed out waiting for CDP event '${G}'`))},V),Z=($)=>{clearTimeout(X);this.off(G,Z);Q($)};this.on(G,Z)})}}class O extends E{bridge;_pageEnabled=!1;_networkCapturing=!1;_networkCapturePattern="";_networkEntries=[];_pendingRequests=new Map;_pendingBodyFetches=new Set;_consoleMessages=[];_consoleCapturing=!1;constructor(G){super();this.bridge=G}async goto(G,V){if(!this._pageEnabled){await this.bridge.send("Page.enable");this._pageEnabled=!0}const Q=this.bridge.waitForEvent("Page.loadEventFired",30000).catch(()=>{});await this.bridge.send("Page.navigate",{url:G});await Q;this._lastUrl=G;if(V?.waitUntil!=="none"){const K=V?.settleMs??1000;await this.evaluate(S(K,Math.min(500,K)))}}async evaluate(G,...V){const Q=_(G,V),K=await this.bridge.send("Runtime.evaluate",{expression:Q,returnByValue:!0,awaitPromise:!0});if(K.exceptionDetails)throw Error("Evaluate error: "+(K.exceptionDetails.exception?.description||"Unknown exception"));return K.result?.value}async getCookies(G={}){let V;try{const K=await this.bridge.send("Storage.getCookies");V=H(K)&&Array.isArray(K.cookies)?K.cookies:[]}catch{const K=await this.bridge.send("Network.getCookies",G.url?{urls:[G.url]}:{});V=H(K)&&Array.isArray(K.cookies)?K.cookies:[]}const Q=G.domain??(G.url?T(G.url):void 0);return Q?V.filter((K)=>N(K)&&C(K.domain,Q)):V.filter(N)}async screenshot(G={}){const V=G.fullPage===!0,Q=G.width&&G.width>0?Math.ceil(G.width):void 0,K=!V&&G.height&&G.height>0?Math.ceil(G.height):void 0,X=Q!==void 0||K!==void 0;if(X){if(Q!==void 0&&V)await this.bridge.send("Emulation.setDeviceMetricsOverride",{mobile:!1,width:Q,height:0,deviceScaleFactor:1});let Z=Q??0,$=K??0;if(V){const J=await this.bridge.send("Page.getLayoutMetrics"),L=H(J)?J:{},Y=H(L.cssContentSize)?L.cssContentSize:void 0,A=H(L.contentSize)?L.contentSize:void 0,B=Y??A;if(B&&typeof B.width==="number"&&typeof B.height==="number"){if(Z===0)Z=Math.ceil(B.width);$=Math.ceil(B.height)}}await this.bridge.send("Emulation.setDeviceMetricsOverride",{mobile:!1,width:Z,height:$,deviceScaleFactor:1})}try{const Z=await this.bridge.send("Page.captureScreenshot",{format:G.format??"png",quality:G.format==="jpeg"?G.quality??80:void 0,captureBeyondViewport:!X&&V}),$=H(Z)&&typeof Z.data==="string"?Z.data:"";if(G.path)await M($,G.path);return $}finally{if(X)await this.bridge.send("Emulation.clearDeviceMetricsOverride").catch(()=>{})}}async startNetworkCapture(G=""){this._networkCapturePattern=G;if(!this._networkCapturing){this._networkEntries=[];this._pendingRequests.clear();this._pendingBodyFetches.clear();await this.bridge.send("Network.enable");this.bridge.on("Network.requestWillBeSent",(V)=>{const Q=V;if(!this._networkCapturePattern||Q.request.url.includes(this._networkCapturePattern)){const K=this._networkEntries.push({url:Q.request.url,method:Q.request.method,timestamp:Date.now()})-1;this._pendingRequests.set(Q.requestId,K)}});this.bridge.on("Network.responseReceived",(V)=>{const Q=V,K=this._pendingRequests.get(Q.requestId);if(K!==void 0){this._networkEntries[K].responseStatus=Q.response.status;this._networkEntries[K].responseContentType=Q.response.mimeType||""}});this.bridge.on("Network.loadingFinished",(V)=>{const Q=V,K=this._pendingRequests.get(Q.requestId);if(K!==void 0){const X=this.bridge.send("Network.getResponseBody",{requestId:Q.requestId}).then((Z)=>{const $=Z;if(typeof $?.body==="string"){const J=$.body.length,L=J>CDP_RESPONSE_BODY_CAPTURE_LIMIT,Y=L?$.body.slice(0,CDP_RESPONSE_BODY_CAPTURE_LIMIT):$.body;this._networkEntries[K].responsePreview=$.base64Encoded?`base64:${Y}`:Y;this._networkEntries[K].responseBodyFullSize=J;this._networkEntries[K].responseBodyTruncated=L}}).catch((Z)=>{if(process.env.OPENCLI_VERBOSE)console.error(`[cdp] getResponseBody failed for ${Q.requestId}:`,Z instanceof Error?Z.message:Z)}).finally(()=>{this._pendingBodyFetches.delete(X)});this._pendingBodyFetches.add(X);this._pendingRequests.delete(Q.requestId)}});this._networkCapturing=!0}return!0}async readNetworkCapture(){if(this._pendingBodyFetches.size>0)await Promise.all([...this._pendingBodyFetches]);const G=[...this._networkEntries];this._networkEntries=[];return G}async consoleMessages(G="all"){if(!this._consoleCapturing){await this.bridge.send("Runtime.enable");this.bridge.on("Runtime.consoleAPICalled",(V)=>{const Q=V,K=(Q.args||[]).map((X)=>X.value!==void 0?String(X.value):X.description||"").join(" ");this._consoleMessages.push({type:Q.type,text:K,timestamp:Date.now()});if(this._consoleMessages.length>500)this._consoleMessages.shift()});this.bridge.on("Runtime.exceptionThrown",(V)=>{const Q=V,K=Q.exceptionDetails?.exception?.description||Q.exceptionDetails?.text||"Unknown exception";this._consoleMessages.push({type:"error",text:K,timestamp:Date.now()});if(this._consoleMessages.length>500)this._consoleMessages.shift()});this._consoleCapturing=!0}if(G==="all")return[...this._consoleMessages];if(G==="error")return this._consoleMessages.filter((V)=>V.type==="error"||V.type==="warning");return this._consoleMessages.filter((V)=>V.type===G)}async tabs(){return[]}async selectTab(G){}async cdp(G,V={}){return this.bridge.send(G,V)}async handleJavaScriptDialog(G,V){await this.cdp("Page.handleJavaScriptDialog",{accept:G,...V!==void 0&&{promptText:V}})}async nativeClick(G,V){await this.cdp("Input.dispatchMouseEvent",{type:"mouseMoved",x:G,y:V});await this.cdp("Input.dispatchMouseEvent",{type:"mousePressed",x:G,y:V,button:"left",clickCount:1});await this.cdp("Input.dispatchMouseEvent",{type:"mouseReleased",x:G,y:V,button:"left",clickCount:1})}async nativeType(G){await this.cdp("Input.insertText",{text:G})}async insertText(G){await this.nativeType(G)}async nativeKeyPress(G,V=[]){let Q=0;for(const K of V){if(K==="Alt")Q|=1;if(K==="Ctrl"||K==="Control")Q|=2;if(K==="Meta")Q|=4;if(K==="Shift")Q|=8}await this.cdp("Input.dispatchKeyEvent",{type:"keyDown",key:G,modifiers:Q});await this.cdp("Input.dispatchKeyEvent",{type:"keyUp",key:G,modifiers:Q})}}function T(G){try{return new URL(G).hostname}catch{return}}function N(G){return H(G)&&typeof G.name==="string"&&typeof G.value==="string"&&typeof G.domain==="string"}function C(G,V){const Q=G.replace(/^\./,"").toLowerCase(),K=V.replace(/^\./,"").toLowerCase();return K===Q||K.endsWith(`.${Q}`)}function U(G){const V=b(process.env.OPENCLI_CDP_TARGET);return G.map((K,X)=>({target:K,index:X,score:q(K,V)})).filter(({score:K})=>Number.isFinite(K)).sort((K,X)=>{if(X.score!==K.score)return X.score-K.score;return K.index-X.index})[0]?.target}function q(G,V){if(!G.webSocketDebuggerUrl)return Number.NEGATIVE_INFINITY;const Q=(G.type??"").toLowerCase(),K=(G.url??"").toLowerCase(),X=(G.title??"").toLowerCase(),Z=`${X} ${K}`;if(!Z.trim()&&!Q)return Number.NEGATIVE_INFINITY;if(Z.includes("devtools"))return Number.NEGATIVE_INFINITY;if(Q==="background_page"||Q==="service_worker")return Number.NEGATIVE_INFINITY;let $=0;if(V&&V.test(Z))$+=1000;if(Q==="app")$+=120;else if(Q==="webview")$+=100;else if(Q==="page")$+=80;else if(Q==="iframe")$+=20;if(K.startsWith("http://localhost")||K.startsWith("https://localhost"))$+=90;if(K.startsWith("file://"))$+=60;if(K.startsWith("http://127.0.0.1")||K.startsWith("https://127.0.0.1"))$+=50;if(K.startsWith("about:blank"))$-=120;if(K===""||K==="about:blank")$-=40;if(X&&X!=="devtools")$+=25;const J=Object.values(w()).map((L)=>(L.displayName??L.processName).toLowerCase());for(const L of J)if(X.includes(L)){$+=120;break}for(const L of J)if(K.includes(L)){$+=100;break}return $}function b(G){const V=G?.trim();if(!V)return;return new RegExp(D(V.toLowerCase()))}function D(G){return G.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}export const __test__={selectCDPTarget:U,scoreCDPTarget:q};function j(G){return new Promise((V,Q)=>{const K=new URL(G),X=(K.protocol==="https:"?F:W)(K,(Z)=>{const $=Z.statusCode??0;if($<200||$>=300){Z.resume();Q(Error(`Failed to fetch CDP targets: HTTP ${$}`));return}const J=[];Z.on("data",(L)=>J.push(Buffer.isBuffer(L)?L:Buffer.from(L)));Z.on("end",()=>{try{V(JSON.parse(Buffer.concat(J).toString("utf8")))}catch(L){Q(L instanceof Error?L:Error(String(L)))}})});X.on("error",Q);X.setTimeout(1e4,()=>X.destroy(Error("Timed out fetching CDP targets")));X.end()})}
|
|
1
|
+
import{WebSocket as I}from"ws";import{request as W}from"node:http";import{request as F}from"node:https";import{buildEvaluateExpression as _}from"./utils.js";import{generateStealthJs as z}from"./stealth.js";import{waitForDomStableJs as S}from"./dom-helpers.js";import{isRecord as H,saveBase64ToFile as M}from"../utils.js";import{getAllElectronApps as w}from"../electron-apps.js";import{BasePage as E}from"./base-page.js";const R=30000;export const CDP_RESPONSE_BODY_CAPTURE_LIMIT=8388608;export class CDPBridge{_ws=null;_idCounter=0;_pending=new Map;_eventListeners=new Map;async connect(G){if(this._ws)throw Error("CDPBridge is already connected. Call close() before reconnecting.");const V=G?.cdpEndpoint??process.env.OPENCLI_CDP_ENDPOINT;if(!V)throw Error("CDP endpoint not provided (pass cdpEndpoint or set OPENCLI_CDP_ENDPOINT)");let Q=V;if(V.startsWith("http")){const K=await j(`${V.replace(/\/$/,"")}/json`),X=U(K);if(!X||!X.webSocketDebuggerUrl)throw Error("No inspectable targets found at CDP endpoint");Q=X.webSocketDebuggerUrl}return new Promise((K,X)=>{const Z=new I(Q),$=(G?.timeout??10)*1000,J=setTimeout(()=>{this._ws=null;Z.close();X(Error("CDP connect timeout"))},$);Z.on("open",async()=>{clearTimeout(J);this._ws=Z;try{await this.send("Page.enable");this.on("Page.javascriptDialogOpening",(L)=>{if(L?.type==="beforeunload")this.send("Page.handleJavaScriptDialog",{accept:!0}).catch(()=>{})});await this.send("Page.addScriptToEvaluateOnNewDocument",{source:z()});if(G?.initScript)await this.send("Page.addScriptToEvaluateOnNewDocument",{source:G.initScript});if(G?.proxyAuth){this.on("Fetch.requestPaused",(L)=>{const Y=L;if(Y.requestId)this.send("Fetch.continueRequest",{requestId:Y.requestId}).catch(()=>{})});this.on("Fetch.authRequired",(L)=>{const Y=L;if(!Y.requestId)return;this.send("Fetch.continueWithAuth",{requestId:Y.requestId,authChallengeResponse:{response:"ProvideCredentials",username:G.proxyAuth.username,password:G.proxyAuth.password}}).catch(()=>{})});await this.send("Fetch.enable",{handleAuthRequests:!0})}}catch(L){Z.close();X(L instanceof Error?L:Error(String(L)));return}K(new O(this))});Z.on("error",(L)=>{clearTimeout(J);X(L)});Z.on("message",(L)=>{try{const Y=JSON.parse(L.toString());if(Y.id&&this._pending.has(Y.id)){const A=this._pending.get(Y.id);clearTimeout(A.timer);this._pending.delete(Y.id);if(Y.error)A.reject(Error(Y.error.message));else A.resolve(Y.result)}if(Y.method){const A=this._eventListeners.get(Y.method);if(A)for(const B of A)B(Y.params)}}catch(Y){if(process.env.OPENCLI_VERBOSE)console.error("[cdp] Failed to parse WebSocket message:",Y instanceof Error?Y.message:Y)}})})}async close(){if(this._ws){this._ws.close();this._ws=null}for(const G of this._pending.values()){clearTimeout(G.timer);G.reject(Error("CDP connection closed"))}this._pending.clear();this._eventListeners.clear()}async send(G,V={},Q=R){if(!this._ws||this._ws.readyState!==I.OPEN)throw Error("CDP connection is not open");const K=++this._idCounter;return new Promise((X,Z)=>{const $=setTimeout(()=>{this._pending.delete(K);const J=G.startsWith("Page.")?"(页面可能被 JS 弹窗挡住:试 `opencli browser dialog accept`;托管 profile 可 `opencli profile stop <name>` 关闭后重跑命令自动重启会话)":"";Z(Error(`CDP command '${G}' timed out after ${Q/1000}s${J}`))},Q);this._pending.set(K,{resolve:X,reject:Z,timer:$});this._ws.send(JSON.stringify({id:K,method:G,params:V}))})}on(G,V){let Q=this._eventListeners.get(G);if(!Q){Q=new Set;this._eventListeners.set(G,Q)}Q.add(V)}off(G,V){this._eventListeners.get(G)?.delete(V)}waitForEvent(G,V=15000){return new Promise((Q,K)=>{const X=setTimeout(()=>{this.off(G,Z);K(Error(`Timed out waiting for CDP event '${G}'`))},V),Z=($)=>{clearTimeout(X);this.off(G,Z);Q($)};this.on(G,Z)})}}class O extends E{bridge;_pageEnabled=!1;_networkCapturing=!1;_networkCapturePattern="";_networkEntries=[];_pendingRequests=new Map;_pendingBodyFetches=new Set;_consoleMessages=[];_consoleCapturing=!1;constructor(G){super();this.bridge=G}async goto(G,V){if(!this._pageEnabled){await this.bridge.send("Page.enable");this._pageEnabled=!0}const Q=this.bridge.waitForEvent("Page.loadEventFired",30000).catch(()=>{});await this.bridge.send("Page.navigate",{url:G});await Q;this._lastUrl=G;if(V?.waitUntil!=="none"){const K=V?.settleMs??1000;await this.evaluate(S(K,Math.min(500,K)))}}async evaluate(G,...V){const Q=_(G,V),K=await this.bridge.send("Runtime.evaluate",{expression:Q,returnByValue:!0,awaitPromise:!0});if(K.exceptionDetails)throw Error("Evaluate error: "+(K.exceptionDetails.exception?.description||"Unknown exception"));return K.result?.value}async getCookies(G={}){let V;try{const K=await this.bridge.send("Storage.getCookies");V=H(K)&&Array.isArray(K.cookies)?K.cookies:[]}catch{const K=await this.bridge.send("Network.getCookies",G.url?{urls:[G.url]}:{});V=H(K)&&Array.isArray(K.cookies)?K.cookies:[]}const Q=G.domain??(G.url?T(G.url):void 0);return Q?V.filter((K)=>N(K)&&C(K.domain,Q)):V.filter(N)}async screenshot(G={}){const V=G.fullPage===!0,Q=G.width&&G.width>0?Math.ceil(G.width):void 0,K=!V&&G.height&&G.height>0?Math.ceil(G.height):void 0,X=Q!==void 0||K!==void 0;if(X){if(Q!==void 0&&V)await this.bridge.send("Emulation.setDeviceMetricsOverride",{mobile:!1,width:Q,height:0,deviceScaleFactor:1});let Z=Q??0,$=K??0;if(V){const J=await this.bridge.send("Page.getLayoutMetrics"),L=H(J)?J:{},Y=H(L.cssContentSize)?L.cssContentSize:void 0,A=H(L.contentSize)?L.contentSize:void 0,B=Y??A;if(B&&typeof B.width==="number"&&typeof B.height==="number"){if(Z===0)Z=Math.ceil(B.width);$=Math.ceil(B.height)}}await this.bridge.send("Emulation.setDeviceMetricsOverride",{mobile:!1,width:Z,height:$,deviceScaleFactor:1})}try{const Z=await this.bridge.send("Page.captureScreenshot",{format:G.format??"png",quality:G.format==="jpeg"?G.quality??80:void 0,captureBeyondViewport:!X&&V}),$=H(Z)&&typeof Z.data==="string"?Z.data:"";if(G.path)await M($,G.path);return $}finally{if(X)await this.bridge.send("Emulation.clearDeviceMetricsOverride").catch(()=>{})}}async startNetworkCapture(G=""){this._networkCapturePattern=G;if(!this._networkCapturing){this._networkEntries=[];this._pendingRequests.clear();this._pendingBodyFetches.clear();await this.bridge.send("Network.enable");this.bridge.on("Network.requestWillBeSent",(V)=>{const Q=V;if(!this._networkCapturePattern||Q.request.url.includes(this._networkCapturePattern)){const K=this._networkEntries.push({url:Q.request.url,method:Q.request.method,timestamp:Date.now()})-1;this._pendingRequests.set(Q.requestId,K)}});this.bridge.on("Network.responseReceived",(V)=>{const Q=V,K=this._pendingRequests.get(Q.requestId);if(K!==void 0){this._networkEntries[K].responseStatus=Q.response.status;this._networkEntries[K].responseContentType=Q.response.mimeType||""}});this.bridge.on("Network.loadingFinished",(V)=>{const Q=V,K=this._pendingRequests.get(Q.requestId);if(K!==void 0){const X=this.bridge.send("Network.getResponseBody",{requestId:Q.requestId}).then((Z)=>{const $=Z;if(typeof $?.body==="string"){const J=$.body.length,L=J>CDP_RESPONSE_BODY_CAPTURE_LIMIT,Y=L?$.body.slice(0,CDP_RESPONSE_BODY_CAPTURE_LIMIT):$.body;this._networkEntries[K].responsePreview=$.base64Encoded?`base64:${Y}`:Y;this._networkEntries[K].responseBodyFullSize=J;this._networkEntries[K].responseBodyTruncated=L}}).catch((Z)=>{if(process.env.OPENCLI_VERBOSE)console.error(`[cdp] getResponseBody failed for ${Q.requestId}:`,Z instanceof Error?Z.message:Z)}).finally(()=>{this._pendingBodyFetches.delete(X)});this._pendingBodyFetches.add(X);this._pendingRequests.delete(Q.requestId)}});this._networkCapturing=!0}return!0}async readNetworkCapture(){if(this._pendingBodyFetches.size>0)await Promise.all([...this._pendingBodyFetches]);const G=[...this._networkEntries];this._networkEntries=[];return G}async consoleMessages(G="all"){if(!this._consoleCapturing){await this.bridge.send("Runtime.enable");this.bridge.on("Runtime.consoleAPICalled",(V)=>{const Q=V,K=(Q.args||[]).map((X)=>X.value!==void 0?String(X.value):X.description||"").join(" ");this._consoleMessages.push({type:Q.type,text:K,timestamp:Date.now()});if(this._consoleMessages.length>500)this._consoleMessages.shift()});this.bridge.on("Runtime.exceptionThrown",(V)=>{const Q=V,K=Q.exceptionDetails?.exception?.description||Q.exceptionDetails?.text||"Unknown exception";this._consoleMessages.push({type:"error",text:K,timestamp:Date.now()});if(this._consoleMessages.length>500)this._consoleMessages.shift()});this._consoleCapturing=!0}if(G==="all")return[...this._consoleMessages];if(G==="error")return this._consoleMessages.filter((V)=>V.type==="error"||V.type==="warning");return this._consoleMessages.filter((V)=>V.type===G)}async tabs(){return[]}async selectTab(G){}async cdp(G,V={}){return this.bridge.send(G,V)}async handleJavaScriptDialog(G,V){await this.cdp("Page.handleJavaScriptDialog",{accept:G,...V!==void 0&&{promptText:V}})}async nativeClick(G,V){await this.cdp("Input.dispatchMouseEvent",{type:"mouseMoved",x:G,y:V});await this.cdp("Input.dispatchMouseEvent",{type:"mousePressed",x:G,y:V,button:"left",clickCount:1});await this.cdp("Input.dispatchMouseEvent",{type:"mouseReleased",x:G,y:V,button:"left",clickCount:1})}async nativeType(G){await this.cdp("Input.insertText",{text:G})}async insertText(G){await this.nativeType(G)}async nativeKeyPress(G,V=[]){let Q=0;for(const K of V){if(K==="Alt")Q|=1;if(K==="Ctrl"||K==="Control")Q|=2;if(K==="Meta")Q|=4;if(K==="Shift")Q|=8}await this.cdp("Input.dispatchKeyEvent",{type:"keyDown",key:G,modifiers:Q});await this.cdp("Input.dispatchKeyEvent",{type:"keyUp",key:G,modifiers:Q})}}function T(G){try{return new URL(G).hostname}catch{return}}function N(G){return H(G)&&typeof G.name==="string"&&typeof G.value==="string"&&typeof G.domain==="string"}function C(G,V){const Q=G.replace(/^\./,"").toLowerCase(),K=V.replace(/^\./,"").toLowerCase();return K===Q||K.endsWith(`.${Q}`)}function U(G){const V=b(process.env.OPENCLI_CDP_TARGET);return G.map((K,X)=>({target:K,index:X,score:q(K,V)})).filter(({score:K})=>Number.isFinite(K)).sort((K,X)=>{if(X.score!==K.score)return X.score-K.score;return K.index-X.index})[0]?.target}function q(G,V){if(!G.webSocketDebuggerUrl)return Number.NEGATIVE_INFINITY;const Q=(G.type??"").toLowerCase(),K=(G.url??"").toLowerCase(),X=(G.title??"").toLowerCase(),Z=`${X} ${K}`;if(!Z.trim()&&!Q)return Number.NEGATIVE_INFINITY;if(Z.includes("devtools"))return Number.NEGATIVE_INFINITY;if(Q==="background_page"||Q==="service_worker")return Number.NEGATIVE_INFINITY;let $=0;if(V&&V.test(Z))$+=1000;if(Q==="app")$+=120;else if(Q==="webview")$+=100;else if(Q==="page")$+=80;else if(Q==="iframe")$+=20;if(K.startsWith("http://localhost")||K.startsWith("https://localhost"))$+=90;if(K.startsWith("file://"))$+=60;if(K.startsWith("http://127.0.0.1")||K.startsWith("https://127.0.0.1"))$+=50;if(K.startsWith("about:blank"))$-=120;if(K===""||K==="about:blank")$-=40;if(X&&X!=="devtools")$+=25;const J=Object.values(w()).map((L)=>(L.displayName??L.processName).toLowerCase());for(const L of J)if(X.includes(L)){$+=120;break}for(const L of J)if(K.includes(L)){$+=100;break}return $}function b(G){const V=G?.trim();if(!V)return;return new RegExp(D(V.toLowerCase()))}function D(G){return G.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}export const __test__={selectCDPTarget:U,scoreCDPTarget:q};function j(G){return new Promise((V,Q)=>{const K=new URL(G),X=(K.protocol==="https:"?F:W)(K,(Z)=>{const $=Z.statusCode??0;if($<200||$>=300){Z.resume();Q(Error(`Failed to fetch CDP targets: HTTP ${$}`));return}const J=[];Z.on("data",(L)=>J.push(Buffer.isBuffer(L)?L:Buffer.from(L)));Z.on("end",()=>{try{V(JSON.parse(Buffer.concat(J).toString("utf8")))}catch(L){Q(L instanceof Error?L:Error(String(L)))}})});X.on("error",Q);X.setTimeout(1e4,()=>X.destroy(Error("Timed out fetching CDP targets")));X.end()})}
|