publishport-opencli 1.0.29 → 1.0.30
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 +464 -0
- package/clis/douyin/_shared/browser-fetch.js +31 -13
- package/clis/douyin/_shared/comments.js +1 -0
- package/clis/douyin/comment-like.js +1 -0
- package/clis/douyin/comment.js +1 -0
- package/clis/douyin/comments.js +1 -0
- package/clis/xiaohongshu/_shared/comment-actions.js +9 -0
- package/clis/xiaohongshu/_shared/comment-state.js +1 -0
- package/clis/xiaohongshu/comment-like.js +1 -0
- package/clis/xiaohongshu/comment.js +1 -0
- package/clis/xiaohongshu/comments.js +33 -5
- package/clis/xiaohongshu/replies.js +1 -0
- package/clis/xiaohongshu/reply.js +1 -0
- package/dist/src/cli.js +27 -25
- package/package.json +1 -1
- package/skills/opencli-browser/SKILL.md +14 -3
package/cli-manifest.json
CHANGED
|
@@ -14340,6 +14340,190 @@
|
|
|
14340
14340
|
"sourceFile": "douyin/collections.js",
|
|
14341
14341
|
"navigateBefore": "https://creator.douyin.com"
|
|
14342
14342
|
},
|
|
14343
|
+
{
|
|
14344
|
+
"site": "douyin",
|
|
14345
|
+
"name": "comment",
|
|
14346
|
+
"description": "发表评论或回复主评论/楼中楼;默认只读预检,--execute 才提交一次并回查",
|
|
14347
|
+
"access": "write",
|
|
14348
|
+
"domain": "www.douyin.com",
|
|
14349
|
+
"strategy": "cookie",
|
|
14350
|
+
"browser": true,
|
|
14351
|
+
"args": [
|
|
14352
|
+
{
|
|
14353
|
+
"name": "aweme_id",
|
|
14354
|
+
"type": "str",
|
|
14355
|
+
"required": true,
|
|
14356
|
+
"positional": true,
|
|
14357
|
+
"help": "作品 ID 或完整链接"
|
|
14358
|
+
},
|
|
14359
|
+
{
|
|
14360
|
+
"name": "text",
|
|
14361
|
+
"type": "str",
|
|
14362
|
+
"required": false,
|
|
14363
|
+
"positional": true,
|
|
14364
|
+
"help": "字面评论内容,最多 500 个 UTF-16 字符;支持 --file",
|
|
14365
|
+
"fromFile": true,
|
|
14366
|
+
"requiredUnlessFile": true
|
|
14367
|
+
},
|
|
14368
|
+
{
|
|
14369
|
+
"name": "parent",
|
|
14370
|
+
"type": "string",
|
|
14371
|
+
"required": false,
|
|
14372
|
+
"help": "回复主评论时传其 cid"
|
|
14373
|
+
},
|
|
14374
|
+
{
|
|
14375
|
+
"name": "reply-to",
|
|
14376
|
+
"type": "string",
|
|
14377
|
+
"required": false,
|
|
14378
|
+
"help": "回复楼中楼的 cid,必须同时传 --parent"
|
|
14379
|
+
},
|
|
14380
|
+
{
|
|
14381
|
+
"name": "cursor",
|
|
14382
|
+
"type": "string",
|
|
14383
|
+
"default": "0",
|
|
14384
|
+
"required": false,
|
|
14385
|
+
"help": "被回复的楼中楼所在页 cursor(来自 replies 输出)"
|
|
14386
|
+
},
|
|
14387
|
+
{
|
|
14388
|
+
"name": "execute",
|
|
14389
|
+
"type": "boolean",
|
|
14390
|
+
"required": false,
|
|
14391
|
+
"help": "实际提交一次;默认仅验证账号、目标和原生提交入口"
|
|
14392
|
+
},
|
|
14393
|
+
{
|
|
14394
|
+
"name": "file",
|
|
14395
|
+
"type": "str",
|
|
14396
|
+
"required": false,
|
|
14397
|
+
"help": "从本机文件读取text(UTF-8),与 <text> 二选一。长正文建议走文件:命令行上只剩短参数,不受 shell 引号 / 换行 / 编码影响",
|
|
14398
|
+
"generatedFileFor": "text"
|
|
14399
|
+
}
|
|
14400
|
+
],
|
|
14401
|
+
"columns": [
|
|
14402
|
+
"aweme_id",
|
|
14403
|
+
"cid",
|
|
14404
|
+
"text",
|
|
14405
|
+
"parent_id",
|
|
14406
|
+
"reply_to_id",
|
|
14407
|
+
"status"
|
|
14408
|
+
],
|
|
14409
|
+
"type": "js",
|
|
14410
|
+
"modulePath": "douyin/comment.js",
|
|
14411
|
+
"sourceFile": "douyin/comment.js",
|
|
14412
|
+
"navigateBefore": false
|
|
14413
|
+
},
|
|
14414
|
+
{
|
|
14415
|
+
"site": "douyin",
|
|
14416
|
+
"name": "comment-like",
|
|
14417
|
+
"description": "点赞/取消点赞主评论或楼中楼;默认只读预检,--execute 才提交并回查",
|
|
14418
|
+
"access": "write",
|
|
14419
|
+
"domain": "www.douyin.com",
|
|
14420
|
+
"strategy": "cookie",
|
|
14421
|
+
"browser": true,
|
|
14422
|
+
"args": [
|
|
14423
|
+
{
|
|
14424
|
+
"name": "aweme_id",
|
|
14425
|
+
"type": "str",
|
|
14426
|
+
"required": true,
|
|
14427
|
+
"positional": true,
|
|
14428
|
+
"help": "作品 ID 或完整链接"
|
|
14429
|
+
},
|
|
14430
|
+
{
|
|
14431
|
+
"name": "cid",
|
|
14432
|
+
"type": "str",
|
|
14433
|
+
"required": true,
|
|
14434
|
+
"positional": true,
|
|
14435
|
+
"help": "目标评论 ID"
|
|
14436
|
+
},
|
|
14437
|
+
{
|
|
14438
|
+
"name": "parent",
|
|
14439
|
+
"type": "string",
|
|
14440
|
+
"required": false,
|
|
14441
|
+
"help": "目标为楼中楼时,传主评论 ID"
|
|
14442
|
+
},
|
|
14443
|
+
{
|
|
14444
|
+
"name": "cursor",
|
|
14445
|
+
"type": "string",
|
|
14446
|
+
"default": "0",
|
|
14447
|
+
"required": false,
|
|
14448
|
+
"help": "楼中楼所在页的 cursor(来自 replies 输出)"
|
|
14449
|
+
},
|
|
14450
|
+
{
|
|
14451
|
+
"name": "undo",
|
|
14452
|
+
"type": "boolean",
|
|
14453
|
+
"required": false,
|
|
14454
|
+
"help": "取消点赞"
|
|
14455
|
+
},
|
|
14456
|
+
{
|
|
14457
|
+
"name": "execute",
|
|
14458
|
+
"type": "boolean",
|
|
14459
|
+
"required": false,
|
|
14460
|
+
"help": "实际提交一次,随后回查点赞状态"
|
|
14461
|
+
}
|
|
14462
|
+
],
|
|
14463
|
+
"columns": [
|
|
14464
|
+
"aweme_id",
|
|
14465
|
+
"cid",
|
|
14466
|
+
"parent_id",
|
|
14467
|
+
"liked",
|
|
14468
|
+
"status"
|
|
14469
|
+
],
|
|
14470
|
+
"type": "js",
|
|
14471
|
+
"modulePath": "douyin/comment-like.js",
|
|
14472
|
+
"sourceFile": "douyin/comment-like.js",
|
|
14473
|
+
"navigateBefore": false
|
|
14474
|
+
},
|
|
14475
|
+
{
|
|
14476
|
+
"site": "douyin",
|
|
14477
|
+
"name": "comments",
|
|
14478
|
+
"description": "查看作品评论、点赞状态及回复数;使用 next_cursor 翻页",
|
|
14479
|
+
"access": "read",
|
|
14480
|
+
"domain": "www.douyin.com",
|
|
14481
|
+
"strategy": "cookie",
|
|
14482
|
+
"browser": true,
|
|
14483
|
+
"args": [
|
|
14484
|
+
{
|
|
14485
|
+
"name": "aweme_id",
|
|
14486
|
+
"type": "str",
|
|
14487
|
+
"required": true,
|
|
14488
|
+
"positional": true,
|
|
14489
|
+
"help": "作品 ID 或抖音作品完整链接"
|
|
14490
|
+
},
|
|
14491
|
+
{
|
|
14492
|
+
"name": "limit",
|
|
14493
|
+
"type": "int",
|
|
14494
|
+
"default": 20,
|
|
14495
|
+
"required": false,
|
|
14496
|
+
"help": "每页数量,1–20"
|
|
14497
|
+
},
|
|
14498
|
+
{
|
|
14499
|
+
"name": "cursor",
|
|
14500
|
+
"type": "string",
|
|
14501
|
+
"default": "0",
|
|
14502
|
+
"required": false,
|
|
14503
|
+
"help": "上一页 next_cursor,首次为 0"
|
|
14504
|
+
}
|
|
14505
|
+
],
|
|
14506
|
+
"columns": [
|
|
14507
|
+
"cid",
|
|
14508
|
+
"aweme_id",
|
|
14509
|
+
"text",
|
|
14510
|
+
"nickname",
|
|
14511
|
+
"uid",
|
|
14512
|
+
"digg_count",
|
|
14513
|
+
"liked",
|
|
14514
|
+
"reply_count",
|
|
14515
|
+
"parent_id",
|
|
14516
|
+
"reply_to_id",
|
|
14517
|
+
"create_time",
|
|
14518
|
+
"cursor",
|
|
14519
|
+
"next_cursor",
|
|
14520
|
+
"has_more"
|
|
14521
|
+
],
|
|
14522
|
+
"type": "js",
|
|
14523
|
+
"modulePath": "douyin/comments.js",
|
|
14524
|
+
"sourceFile": "douyin/comments.js",
|
|
14525
|
+
"navigateBefore": false
|
|
14526
|
+
},
|
|
14343
14527
|
{
|
|
14344
14528
|
"site": "douyin",
|
|
14345
14529
|
"name": "delete",
|
|
@@ -15157,6 +15341,65 @@
|
|
|
15157
15341
|
"navigateBefore": false,
|
|
15158
15342
|
"defaultWindowMode": "foreground"
|
|
15159
15343
|
},
|
|
15344
|
+
{
|
|
15345
|
+
"site": "douyin",
|
|
15346
|
+
"name": "replies",
|
|
15347
|
+
"description": "查看主评论下的楼中楼,含回复目标 ID;使用 next_cursor 翻页",
|
|
15348
|
+
"access": "read",
|
|
15349
|
+
"domain": "www.douyin.com",
|
|
15350
|
+
"strategy": "cookie",
|
|
15351
|
+
"browser": true,
|
|
15352
|
+
"args": [
|
|
15353
|
+
{
|
|
15354
|
+
"name": "aweme_id",
|
|
15355
|
+
"type": "str",
|
|
15356
|
+
"required": true,
|
|
15357
|
+
"positional": true,
|
|
15358
|
+
"help": "作品 ID 或抖音作品完整链接"
|
|
15359
|
+
},
|
|
15360
|
+
{
|
|
15361
|
+
"name": "cid",
|
|
15362
|
+
"type": "str",
|
|
15363
|
+
"required": true,
|
|
15364
|
+
"positional": true,
|
|
15365
|
+
"help": "主评论 ID(comments 输出的 cid)"
|
|
15366
|
+
},
|
|
15367
|
+
{
|
|
15368
|
+
"name": "limit",
|
|
15369
|
+
"type": "int",
|
|
15370
|
+
"default": 20,
|
|
15371
|
+
"required": false,
|
|
15372
|
+
"help": "每页数量,1–20"
|
|
15373
|
+
},
|
|
15374
|
+
{
|
|
15375
|
+
"name": "cursor",
|
|
15376
|
+
"type": "string",
|
|
15377
|
+
"default": "0",
|
|
15378
|
+
"required": false,
|
|
15379
|
+
"help": "上一页 next_cursor,首次为 0"
|
|
15380
|
+
}
|
|
15381
|
+
],
|
|
15382
|
+
"columns": [
|
|
15383
|
+
"cid",
|
|
15384
|
+
"aweme_id",
|
|
15385
|
+
"text",
|
|
15386
|
+
"nickname",
|
|
15387
|
+
"uid",
|
|
15388
|
+
"digg_count",
|
|
15389
|
+
"liked",
|
|
15390
|
+
"reply_count",
|
|
15391
|
+
"parent_id",
|
|
15392
|
+
"reply_to_id",
|
|
15393
|
+
"create_time",
|
|
15394
|
+
"cursor",
|
|
15395
|
+
"next_cursor",
|
|
15396
|
+
"has_more"
|
|
15397
|
+
],
|
|
15398
|
+
"type": "js",
|
|
15399
|
+
"modulePath": "douyin/comments.js",
|
|
15400
|
+
"sourceFile": "douyin/comments.js",
|
|
15401
|
+
"navigateBefore": false
|
|
15402
|
+
},
|
|
15160
15403
|
{
|
|
15161
15404
|
"site": "douyin",
|
|
15162
15405
|
"name": "search",
|
|
@@ -51255,6 +51498,120 @@
|
|
|
51255
51498
|
"sourceFile": "xiaohongshu/collections.js",
|
|
51256
51499
|
"navigateBefore": false
|
|
51257
51500
|
},
|
|
51501
|
+
{
|
|
51502
|
+
"site": "xiaohongshu",
|
|
51503
|
+
"name": "comment",
|
|
51504
|
+
"description": "发布小红书评论或定向回复;默认验证草稿,--execute 才发送",
|
|
51505
|
+
"access": "write",
|
|
51506
|
+
"domain": "www.xiaohongshu.com",
|
|
51507
|
+
"strategy": "cookie",
|
|
51508
|
+
"browser": true,
|
|
51509
|
+
"args": [
|
|
51510
|
+
{
|
|
51511
|
+
"name": "note-id",
|
|
51512
|
+
"type": "str",
|
|
51513
|
+
"required": true,
|
|
51514
|
+
"positional": true,
|
|
51515
|
+
"help": "Full signed note URL with xsec_token"
|
|
51516
|
+
},
|
|
51517
|
+
{
|
|
51518
|
+
"name": "text",
|
|
51519
|
+
"type": "str",
|
|
51520
|
+
"required": true,
|
|
51521
|
+
"positional": true,
|
|
51522
|
+
"help": "Exact plain-text comment"
|
|
51523
|
+
},
|
|
51524
|
+
{
|
|
51525
|
+
"name": "reply-to",
|
|
51526
|
+
"type": "str",
|
|
51527
|
+
"required": false,
|
|
51528
|
+
"help": "Exact comment ID to reply to, including a nested reply"
|
|
51529
|
+
},
|
|
51530
|
+
{
|
|
51531
|
+
"name": "root-id",
|
|
51532
|
+
"type": "str",
|
|
51533
|
+
"required": false,
|
|
51534
|
+
"help": "Top-level root_id from comments; use when targeting a nested reply"
|
|
51535
|
+
},
|
|
51536
|
+
{
|
|
51537
|
+
"name": "execute",
|
|
51538
|
+
"type": "boolean",
|
|
51539
|
+
"default": false,
|
|
51540
|
+
"required": false,
|
|
51541
|
+
"help": "Submit after verification (default: dry-run only)"
|
|
51542
|
+
}
|
|
51543
|
+
],
|
|
51544
|
+
"columns": [
|
|
51545
|
+
"status",
|
|
51546
|
+
"note_id",
|
|
51547
|
+
"id",
|
|
51548
|
+
"root_id",
|
|
51549
|
+
"parent_id",
|
|
51550
|
+
"text",
|
|
51551
|
+
"url"
|
|
51552
|
+
],
|
|
51553
|
+
"type": "js",
|
|
51554
|
+
"modulePath": "xiaohongshu/comment.js",
|
|
51555
|
+
"sourceFile": "xiaohongshu/comment.js",
|
|
51556
|
+
"navigateBefore": false
|
|
51557
|
+
},
|
|
51558
|
+
{
|
|
51559
|
+
"site": "xiaohongshu",
|
|
51560
|
+
"name": "comment-like",
|
|
51561
|
+
"description": "点赞/取消点赞小红书评论(含楼中楼);默认 dry-run,--execute 才操作",
|
|
51562
|
+
"access": "write",
|
|
51563
|
+
"domain": "www.xiaohongshu.com",
|
|
51564
|
+
"strategy": "cookie",
|
|
51565
|
+
"browser": true,
|
|
51566
|
+
"args": [
|
|
51567
|
+
{
|
|
51568
|
+
"name": "note-id",
|
|
51569
|
+
"type": "str",
|
|
51570
|
+
"required": true,
|
|
51571
|
+
"positional": true,
|
|
51572
|
+
"help": "Full signed note URL with xsec_token"
|
|
51573
|
+
},
|
|
51574
|
+
{
|
|
51575
|
+
"name": "comment-id",
|
|
51576
|
+
"type": "str",
|
|
51577
|
+
"required": true,
|
|
51578
|
+
"positional": true,
|
|
51579
|
+
"help": "Exact comment ID from comments/replies"
|
|
51580
|
+
},
|
|
51581
|
+
{
|
|
51582
|
+
"name": "undo",
|
|
51583
|
+
"type": "boolean",
|
|
51584
|
+
"default": false,
|
|
51585
|
+
"required": false,
|
|
51586
|
+
"help": "Ensure the comment is not liked"
|
|
51587
|
+
},
|
|
51588
|
+
{
|
|
51589
|
+
"name": "root-id",
|
|
51590
|
+
"type": "str",
|
|
51591
|
+
"required": false,
|
|
51592
|
+
"help": "Top-level root_id from comments; use when targeting a nested reply"
|
|
51593
|
+
},
|
|
51594
|
+
{
|
|
51595
|
+
"name": "execute",
|
|
51596
|
+
"type": "boolean",
|
|
51597
|
+
"default": false,
|
|
51598
|
+
"required": false,
|
|
51599
|
+
"help": "Submit after verification (default: dry-run only)"
|
|
51600
|
+
}
|
|
51601
|
+
],
|
|
51602
|
+
"columns": [
|
|
51603
|
+
"status",
|
|
51604
|
+
"note_id",
|
|
51605
|
+
"id",
|
|
51606
|
+
"root_id",
|
|
51607
|
+
"liked",
|
|
51608
|
+
"url"
|
|
51609
|
+
],
|
|
51610
|
+
"type": "js",
|
|
51611
|
+
"modulePath": "xiaohongshu/comment-like.js",
|
|
51612
|
+
"sourceFile": "xiaohongshu/comment-like.js",
|
|
51613
|
+
"navigateBefore": false
|
|
51614
|
+
},
|
|
51258
51615
|
{
|
|
51259
51616
|
"site": "xiaohongshu",
|
|
51260
51617
|
"name": "comments",
|
|
@@ -51288,14 +51645,20 @@
|
|
|
51288
51645
|
],
|
|
51289
51646
|
"columns": [
|
|
51290
51647
|
"rank",
|
|
51648
|
+
"id",
|
|
51649
|
+
"root_id",
|
|
51650
|
+
"parent_id",
|
|
51291
51651
|
"author",
|
|
51292
51652
|
"userId",
|
|
51293
51653
|
"profileUrl",
|
|
51294
51654
|
"text",
|
|
51295
51655
|
"likes",
|
|
51656
|
+
"liked",
|
|
51296
51657
|
"time",
|
|
51297
51658
|
"is_reply",
|
|
51298
51659
|
"reply_to",
|
|
51660
|
+
"reply_count",
|
|
51661
|
+
"replies_complete",
|
|
51299
51662
|
"images"
|
|
51300
51663
|
],
|
|
51301
51664
|
"type": "js",
|
|
@@ -52307,6 +52670,107 @@
|
|
|
52307
52670
|
"sourceFile": "xiaohongshu/publish-video.js",
|
|
52308
52671
|
"navigateBefore": false
|
|
52309
52672
|
},
|
|
52673
|
+
{
|
|
52674
|
+
"site": "xiaohongshu",
|
|
52675
|
+
"name": "replies",
|
|
52676
|
+
"description": "读取指定主评论下的楼中楼,返回精确回复关系与加载完整性",
|
|
52677
|
+
"access": "read",
|
|
52678
|
+
"domain": "www.xiaohongshu.com",
|
|
52679
|
+
"strategy": "cookie",
|
|
52680
|
+
"browser": true,
|
|
52681
|
+
"args": [
|
|
52682
|
+
{
|
|
52683
|
+
"name": "note-id",
|
|
52684
|
+
"type": "str",
|
|
52685
|
+
"required": true,
|
|
52686
|
+
"positional": true,
|
|
52687
|
+
"help": "Full signed note URL with xsec_token"
|
|
52688
|
+
},
|
|
52689
|
+
{
|
|
52690
|
+
"name": "root-id",
|
|
52691
|
+
"type": "str",
|
|
52692
|
+
"required": true,
|
|
52693
|
+
"positional": true,
|
|
52694
|
+
"help": "Top-level comment ID from comments"
|
|
52695
|
+
},
|
|
52696
|
+
{
|
|
52697
|
+
"name": "limit",
|
|
52698
|
+
"type": "int",
|
|
52699
|
+
"default": 50,
|
|
52700
|
+
"required": false,
|
|
52701
|
+
"help": "Maximum nested replies to return (1-200)"
|
|
52702
|
+
}
|
|
52703
|
+
],
|
|
52704
|
+
"columns": [
|
|
52705
|
+
"id",
|
|
52706
|
+
"reply_count",
|
|
52707
|
+
"returned",
|
|
52708
|
+
"complete",
|
|
52709
|
+
"replies"
|
|
52710
|
+
],
|
|
52711
|
+
"type": "js",
|
|
52712
|
+
"modulePath": "xiaohongshu/replies.js",
|
|
52713
|
+
"sourceFile": "xiaohongshu/replies.js",
|
|
52714
|
+
"navigateBefore": false
|
|
52715
|
+
},
|
|
52716
|
+
{
|
|
52717
|
+
"site": "xiaohongshu",
|
|
52718
|
+
"name": "reply",
|
|
52719
|
+
"description": "回复指定小红书评论(含楼中楼);默认 dry-run,--execute 才发送",
|
|
52720
|
+
"access": "write",
|
|
52721
|
+
"domain": "www.xiaohongshu.com",
|
|
52722
|
+
"strategy": "cookie",
|
|
52723
|
+
"browser": true,
|
|
52724
|
+
"args": [
|
|
52725
|
+
{
|
|
52726
|
+
"name": "note-id",
|
|
52727
|
+
"type": "str",
|
|
52728
|
+
"required": true,
|
|
52729
|
+
"positional": true,
|
|
52730
|
+
"help": "Full signed note URL with xsec_token"
|
|
52731
|
+
},
|
|
52732
|
+
{
|
|
52733
|
+
"name": "comment-id",
|
|
52734
|
+
"type": "str",
|
|
52735
|
+
"required": true,
|
|
52736
|
+
"positional": true,
|
|
52737
|
+
"help": "Exact comment ID from comments/replies"
|
|
52738
|
+
},
|
|
52739
|
+
{
|
|
52740
|
+
"name": "text",
|
|
52741
|
+
"type": "str",
|
|
52742
|
+
"required": true,
|
|
52743
|
+
"positional": true,
|
|
52744
|
+
"help": "Exact plain-text reply"
|
|
52745
|
+
},
|
|
52746
|
+
{
|
|
52747
|
+
"name": "root-id",
|
|
52748
|
+
"type": "str",
|
|
52749
|
+
"required": false,
|
|
52750
|
+
"help": "Top-level root_id from comments; use when targeting a nested reply"
|
|
52751
|
+
},
|
|
52752
|
+
{
|
|
52753
|
+
"name": "execute",
|
|
52754
|
+
"type": "boolean",
|
|
52755
|
+
"default": false,
|
|
52756
|
+
"required": false,
|
|
52757
|
+
"help": "Submit after verification (default: dry-run only)"
|
|
52758
|
+
}
|
|
52759
|
+
],
|
|
52760
|
+
"columns": [
|
|
52761
|
+
"status",
|
|
52762
|
+
"note_id",
|
|
52763
|
+
"id",
|
|
52764
|
+
"root_id",
|
|
52765
|
+
"parent_id",
|
|
52766
|
+
"text",
|
|
52767
|
+
"url"
|
|
52768
|
+
],
|
|
52769
|
+
"type": "js",
|
|
52770
|
+
"modulePath": "xiaohongshu/reply.js",
|
|
52771
|
+
"sourceFile": "xiaohongshu/reply.js",
|
|
52772
|
+
"navigateBefore": false
|
|
52773
|
+
},
|
|
52310
52774
|
{
|
|
52311
52775
|
"site": "xiaohongshu",
|
|
52312
52776
|
"name": "saved",
|
|
@@ -1,28 +1,46 @@
|
|
|
1
|
-
import{AuthRequiredError as
|
|
1
|
+
import{AuthRequiredError as l,CommandExecutionError as n}from"@jackwener/opencli/errors";import{unwrapEvaluateResult as u}from"./evaluate-result.js";function m(o,t){const r=String(t??"");return o===401||o===403||/login|cookie|auth|captcha|verify|forbidden|permission|登录|登陆|权限|验证|验证码/i.test(r)}export async function browserFetch(o,t,r,a={}){const c=`
|
|
2
2
|
(async () => {
|
|
3
3
|
const controller = new AbortController();
|
|
4
|
-
const timer = setTimeout(() => controller.abort(), ${Number(
|
|
4
|
+
const timer = setTimeout(() => controller.abort(), ${Number(a.timeoutMs??30000)});
|
|
5
|
+
let challengeTimer;
|
|
5
6
|
try {
|
|
6
|
-
|
|
7
|
+
${a.detectCaptcha?`
|
|
8
|
+
const captchaVisible = () => Array.from(document.querySelectorAll('iframe[src*="/verifycenter/captcha/"]')).some(frame => {
|
|
9
|
+
const rect = frame.getBoundingClientRect();
|
|
10
|
+
return rect.width > 0 && rect.height > 0 && getComputedStyle(frame).visibility !== 'hidden';
|
|
11
|
+
});
|
|
12
|
+
const challengeResult = { status_code: 403, status_msg: '抖音出现验证码;用 ppcli browser site:douyin captcha --adapter 截图,按图使用 captcha-click --adapter 点选并确认后再查询;不会自动重试。' };
|
|
13
|
+
if (captchaVisible()) return challengeResult;
|
|
14
|
+
const guard = new Promise(resolve => {
|
|
15
|
+
challengeTimer = setInterval(() => {
|
|
16
|
+
if (captchaVisible()) { resolve(challengeResult); controller.abort(); }
|
|
17
|
+
else if (controller.signal.aborted) resolve({ status_code: -1, status_msg: 'Douyin read request timed out' });
|
|
18
|
+
}, 200);
|
|
19
|
+
});`:""}
|
|
20
|
+
const request = (async () => {
|
|
21
|
+
const res = await fetch(${JSON.stringify(r)}, {
|
|
7
22
|
method: ${JSON.stringify(t)},
|
|
8
|
-
credentials: ${JSON.stringify(
|
|
23
|
+
credentials: ${JSON.stringify(a.credentials??"include")},
|
|
9
24
|
signal: controller.signal,
|
|
10
25
|
headers: {
|
|
11
26
|
'Content-Type': 'application/json',
|
|
12
|
-
...${JSON.stringify(
|
|
27
|
+
...${JSON.stringify(a.headers??{})}
|
|
13
28
|
},
|
|
14
|
-
${
|
|
29
|
+
${a.body?`body: JSON.stringify(${JSON.stringify(a.body)}),`:""}
|
|
15
30
|
});
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
31
|
+
const text = await res.text();
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(text);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
return { status_code: res.ok ? -2 : res.status, status_msg: \`JSON parse failed: \${text.slice(0, 500) || String(error && error.message || error)}\` };
|
|
36
|
+
}
|
|
37
|
+
})();
|
|
38
|
+
return await ${a.detectCaptcha?"Promise.race([request, guard])":"request"};
|
|
22
39
|
} catch (error) {
|
|
23
40
|
return { status_code: -1, status_msg: String(error && error.message || error) };
|
|
24
41
|
} finally {
|
|
25
42
|
clearTimeout(timer);
|
|
43
|
+
clearInterval(challengeTimer);
|
|
26
44
|
}
|
|
27
45
|
})()
|
|
28
|
-
`;let
|
|
46
|
+
`;let e;try{e=u(await o.evaluate(c))}catch(s){throw new n(`Douyin API request failed (${t} ${r}): ${s instanceof Error?s.message:String(s)}`)}if(e==null)throw new n(`Empty response from Douyin API (${t} ${r})`);if(Array.isArray(e)||typeof e!=="object")throw new n(`Malformed response from Douyin API (${t} ${r})`);if(e&&typeof e==="object"&&"status_code"in e){const s=e.status_code;if(s!==0){const i=e.status_msg??e.message??"unknown error";if(m(s,i))throw new l(new URL(r).hostname,`Douyin API auth/permission error ${s} at ${t} ${r}: ${i}`);throw new n(`Douyin API error ${s} at ${t} ${r}: ${i}`)}}return e}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{ArgumentError as w,AuthRequiredError as g,CommandExecutionError as l,EmptyResultError as p}from"@jackwener/opencli/errors";import{browserFetch as y}from"./browser-fetch.js";import{requireObjectEvaluateResult as h}from"./evaluate-result.js";export function commentId(n,r="评论 ID"){if(typeof n!=="string"||!/^[1-9]\d*$/.test(n))throw new w(`${r} 必须是十进制 ID 字符串,不能经过 Number 转换`);return n}export function videoId(n){if(typeof n==="string"&&/^[1-9]\d*$/.test(n))return n;try{const r=new URL(n),o=r.pathname.match(/^\/(?:video|note)\/([1-9]\d*)\/?$/);if(r.protocol==="https:"&&["www.douyin.com","douyin.com"].includes(r.hostname)&&o)return o[1]}catch{}throw new w("作品必须是抖音作品 ID 或 https://www.douyin.com/video/<ID> 链接")}export function commentCursor(n="0"){const r=String(n);if(!/^\d+$/.test(r)||!Number.isSafeInteger(Number(r)))throw new w("--cursor 必须是非负安全整数");return r}export function commentLimit(n=20){const r=Number(n);if(!Number.isInteger(r)||r<1||r>20)throw new w("--limit 必须在 1–20 之间");return r}export async function openComments(n,r){const o=`https://www.douyin.com/video/${r}`;if(await n.evaluate("location.origin + location.pathname")!==o)await n.goto(o)}export async function readCommentPage(n,r,{parent:o,cursor:u="0",limit:s=20,insertId:d}={}){const e=new URLSearchParams({aid:"6383",cursor:u,count:String(s)});if(o){e.set("item_id",r);e.set("comment_id",o);e.set("item_type","0");e.set("cut_version","1")}else{e.set("aweme_id",r);if(d)e.set("insert_ids",d)}const i=await y(n,"GET",`https://www.douyin.com/aweme/v1/web/comment/list/${o?"reply/":""}?${e}`,{detectCaptcha:!0});if(i.status_code!==0||!Array.isArray(i.comments)||![0,1,!1,!0].includes(i.has_more)||!Number.isSafeInteger(i.cursor)||i.cursor<0)throw new l("抖音评论接口返回了不完整的数据或分页信息");const c=i.comments.map((t)=>{if(typeof t.cid!=="string"||!/^[1-9]\d*$/.test(t.cid)||t.aweme_id!==r||typeof t.text!=="string"||typeof t.reply_id!=="string"||!/^\d+$/.test(t.reply_id)||(o?t.reply_id!==o:t.reply_id!=="0")||![0,1].includes(t.user_digged)||!Number.isSafeInteger(t.digg_count)||t.digg_count<0||typeof t.user?.uid!=="string"||typeof t.user?.nickname!=="string")throw new l("抖音评论字段或所属作品/楼层发生变化,已停止处理");const a=t.reply_to_reply_id??"0";if(typeof a!=="string"||!/^\d+$/.test(a))throw new l("抖音回复目标 ID 格式异常");return{cid:t.cid,aweme_id:r,text:t.text,nickname:t.user.nickname,uid:t.user.uid,digg_count:t.digg_count,liked:t.user_digged===1,reply_count:t.reply_comment_total??null,parent_id:t.reply_id==="0"?null:t.reply_id,reply_to_id:a==="0"?null:a,create_time:t.create_time??null,cursor:u,next_cursor:String(i.cursor),has_more:Boolean(i.has_more)}});if(i.has_more&&i.cursor<=Number(u))throw new l("抖音评论分页游标未前进,已停止翻页");return{rows:c,nextCursor:String(i.cursor),hasMore:Boolean(i.has_more)}}export const COMMENT_COLUMNS=["cid","aweme_id","text","nickname","uid","digg_count","liked","reply_count","parent_id","reply_to_id","create_time","cursor","next_cursor","has_more"];export async function readTargetComment(n,r,o,{parent:u,cursor:s="0"}={}){const e=(await readCommentPage(n,r,{parent:u,cursor:s,insertId:u?void 0:o})).rows.find((i)=>i.cid===o);if(!e)throw new p("douyin comment",`当前作品/评论页没有评论 ${o};楼中楼请传 --parent 和 replies 输出的 --cursor,不会替换为其他评论`);return e}export async function commentRuntime(n,r,o,u,s){const d=Date.now()+1e4;let e;while(Date.now()<d){if(Array.from(document.querySelectorAll('iframe[src*="/verifycenter/captcha/"]')).some((m)=>{const f=m.getBoundingClientRect();return f.width>0&&f.height>0&&getComputedStyle(m).visibility!=="hidden"}))return{error:"auth",message:"抖音出现验证码;用 ppcli browser site:douyin captcha --adapter 截图并点选确认,尚未提交"};const t=document.querySelector('[data-e2e="comment-list"]');let a=t?.[Object.keys(t).find((m)=>m.startsWith("__reactFiber"))];for(let m=0;a&&m<30;m++,a=a.return){const f=a.memoizedProps;if(f?.awemeInfo?.awemeId===n&&typeof f.puhlishCommentItem==="function"&&typeof f.diggCommentItem==="function"){e=f;break}}if(e)break;await new Promise((m)=>setTimeout(m,200))}if(!e)return{error:"runtime",message:"找不到当前作品的原生评论组件"};if(e.userInfo?.isLogin!==!0||!e.userInfo?.info?.uid)return{error:"auth",message:"抖音评论操作需要登录"};if(r==="publish"&&e.awemeInfo.awemeControl?.canComment!==!0)return{error:"runtime",message:"当前作品不允许评论或权限状态未知"};const i=String(e.userInfo.info.uid);if(s&&i!==s)return{error:"auth",message:"抖音账号在预检后已切换,已停止提交"};if(!u)return{ready:!0,uid:i};try{const c=r==="publish"?await e.puhlishCommentItem(o):await e.diggCommentItem(o),t=c?.comment;return{uid:i,status_code:c?.statusCode,status_msg:c?.statusMsg,comment:t?{cid:t.cid,text:t.text,reply_id:t.replyId,user_id:t.user?.uid}:null}}catch(c){return{error:"uncertain",message:String(c?.message??c)}}}export async function callCommentRuntime(n,r,o,u,s=!1,d){let e;try{e=h(await n.evaluate(`(${commentRuntime.toString()})(${JSON.stringify(r)},${JSON.stringify(o)},${JSON.stringify(u)},${s===!0},${JSON.stringify(d)})`),"抖音评论组件")}catch(i){throw new l(`抖音评论组件调用失败:${i.message}`,s?"提交结果未知;请先回查,不要直接重试。":"尚未提交。")}if(e.error==="auth")throw new g("www.douyin.com",e.message);if(e.error)throw new l(e.message,s?"可能已提交;请先回查,不要直接重试。":"尚未提交;页面组件需要重新适配。");if(s?e.status_code!==0:e.ready!==!0)throw new l(`抖音评论操作未成功确认:${e.status_code??"missing status"} ${e.status_msg??""}`,s?"请先回查,不要直接重试。":"尚未提交。");return e}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cli as h,Strategy as _}from"@jackwener/opencli/registry";import{CommandExecutionError as s}from"@jackwener/opencli/errors";import{callCommentRuntime as p,commentCursor as C,commentId as w,openComments as I,readTargetComment as k,videoId as b}from"./_shared/comments.js";h({site:"douyin",name:"comment-like",access:"write",description:"点赞/取消点赞主评论或楼中楼;默认只读预检,--execute 才提交并回查",domain:"www.douyin.com",strategy:_.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"aweme_id",required:!0,positional:!0,help:"作品 ID 或完整链接"},{name:"cid",required:!0,positional:!0,help:"目标评论 ID"},{name:"parent",type:"string",help:"目标为楼中楼时,传主评论 ID"},{name:"cursor",type:"string",default:"0",help:"楼中楼所在页的 cursor(来自 replies 输出)"},{name:"undo",type:"boolean",help:"取消点赞"},{name:"execute",type:"boolean",help:"实际提交一次,随后回查点赞状态"}],columns:["aweme_id","cid","parent_id","liked","status"],func:async(i,e)=>{const t=b(e.aweme_id),r=w(e.cid),o=e.parent==null?void 0:w(e.parent,"--parent"),u=C(e.cursor),n=e.undo!==!0;await I(i,t);const d=await k(i,t,r,{parent:o,cursor:u}),c={cid:r,diggType:n?1:2,level:o?3:1},f=await p(i,t,"like",c),a=(m,y)=>[{aweme_id:t,cid:r,parent_id:o??null,liked:m,status:y}];if(d.liked===n)return a(d.liked,"unchanged");if(e.execute!==!0)return a(d.liked,n?"would_like":"would_unlike");await p(i,t,"like",c,!0,f.uid);let l;try{l=await k(i,t,r,{parent:o,cursor:u})}catch(m){throw new s(`已提交点赞操作,但回查失败:${m.message}`,"先查看 comments/replies 的 liked,不要盲目重试。")}if(l.liked!==n)throw new s("已提交点赞操作,但回查状态不一致","先查看 comments/replies 的 liked,不要盲目重试。");return a(l.liked,n?"liked":"unliked")}});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cli as b,Strategy as E}from"@jackwener/opencli/registry";import{ArgumentError as y,CommandExecutionError as h}from"@jackwener/opencli/errors";import{assertLiteralContent as T}from"../_shared/content-guard.js";import{callCommentRuntime as x,commentCursor as F,commentId as _,openComments as D,readCommentPage as g,readTargetComment as m,videoId as q}from"./_shared/comments.js";b({site:"douyin",name:"comment",access:"write",description:"发表评论或回复主评论/楼中楼;默认只读预检,--execute 才提交一次并回查",domain:"www.douyin.com",strategy:E.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"aweme_id",required:!0,positional:!0,help:"作品 ID 或完整链接"},{name:"text",required:!0,positional:!0,fromFile:!0,help:"字面评论内容,最多 500 个 UTF-16 字符;支持 --file"},{name:"parent",type:"string",help:"回复主评论时传其 cid"},{name:"reply-to",type:"string",help:"回复楼中楼的 cid,必须同时传 --parent"},{name:"cursor",type:"string",default:"0",help:"被回复的楼中楼所在页 cursor(来自 replies 输出)"},{name:"execute",type:"boolean",help:"实际提交一次;默认仅验证账号、目标和原生提交入口"}],columns:["aweme_id","cid","text","parent_id","reply_to_id","status"],func:async(o,r)=>{const i=q(r.aweme_id),a=T(r.text,{label:"评论内容",fileFlag:"--file"});if(!a||a.length>500)throw new y("评论内容不能为空且不能超过 500 个 UTF-16 字符");const t=r.parent==null?void 0:_(r.parent,"--parent"),l=r["reply-to"]==null?void 0:_(r["reply-to"],"--reply-to");if(l&&!t)throw new y("--reply-to 必须同时指定 --parent 主评论 ID");const C=F(r.cursor);await D(o,i);if(t)await m(o,i,t);if(l)await m(o,i,l,{parent:t,cursor:C});const p={text:a,textExtra:[],...t?{replyId:t}:{},...l?{replyToReplyId:l}:{}},c=await x(o,i,"publish",p),s=(e,d)=>[{aweme_id:i,cid:e,text:a,parent_id:t??null,reply_to_id:l??null,status:d}];if(r.execute!==!0)return s(null,"prepared");const w=await x(o,i,"publish",p,!0,c.uid),n=w.comment;if(typeof n?.cid!=="string"||!/^[1-9]\d*$/.test(n.cid)||n.text!==a||n.reply_id!==(t??"0")||w.uid!==c.uid)throw new h("抖音提交已返回,但新评论 ID、内容或楼层无法确认","请先回查 comments/replies,不要直接重发。");try{let e;if(!t)e=await m(o,i,n.cid);else{let d="0";for(let f=0;f<5;f++){const u=await g(o,i,{parent:t,cursor:d});e=u.rows.find((I)=>I.cid===n.cid);if(e||!u.hasMore)break;d=u.nextCursor}}if(!e||e.text!==a||e.uid!==c.uid||e.parent_id!==(t??null)||e.reply_to_id!==(l??null))throw Error("未在回查范围(楼中楼前 5 页)读到匹配的新评论,或作者/内容/回复关系不一致")}catch(e){throw new h(`已提交评论 cid=${n.cid},但回查未通过:${e.message}`,"请按该 cid 核对,禁止直接重发。")}return s(n.cid,"posted")}});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cli as c,Strategy as u}from"@jackwener/opencli/registry";import{EmptyResultError as a}from"@jackwener/opencli/errors";import{COMMENT_COLUMNS as d,commentCursor as l,commentId as p,commentLimit as f,openComments as w,readCommentPage as y,videoId as h}from"./_shared/comments.js";for(const e of[!1,!0])c({site:"douyin",name:e?"replies":"comments",access:"read",description:e?"查看主评论下的楼中楼,含回复目标 ID;使用 next_cursor 翻页":"查看作品评论、点赞状态及回复数;使用 next_cursor 翻页",domain:"www.douyin.com",strategy:u.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"aweme_id",required:!0,positional:!0,help:"作品 ID 或抖音作品完整链接"},...e?[{name:"cid",required:!0,positional:!0,help:"主评论 ID(comments 输出的 cid)"}]:[],{name:"limit",type:"int",default:20,help:"每页数量,1–20"},{name:"cursor",type:"string",default:"0",help:"上一页 next_cursor,首次为 0"}],columns:d,func:async(r,t)=>{const o=h(t.aweme_id),n=e?p(t.cid):void 0,m=f(t.limit),s=l(t.cursor);await w(r,o);const i=await y(r,o,{parent:n,limit:m,cursor:s});if(!i.rows.length)throw new a(`douyin ${e?"replies":"comments"}`,"当前页没有评论;这不表示接口失败。");return i.rows}});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import{ArgumentError as v,CommandExecutionError as c}from"@jackwener/opencli/errors";import{commentId as p,commentNote as C,commentSnapshot as h,locateComment as _,openCommentPage as I,readCommentPage as T,reloadCommentPage as E}from"./comment-state.js";const g="#content-textarea",q=".engage-bar.active button.submit",R=".engage-bar.active button.cancel",S="The action may already have succeeded. Read comments/replies to verify before retrying; this command did not resubmit.";async function b(t,r,a){if(await t.evaluate(`(() => {
|
|
2
|
+
const state = (${T.toString()})(${JSON.stringify(r)});
|
|
3
|
+
if (state.error) return false;
|
|
4
|
+
const controls = document.querySelectorAll(${JSON.stringify(a)});
|
|
5
|
+
if (controls.length !== 1 || controls[0].disabled || !controls[0].getClientRects().length) return false;
|
|
6
|
+
controls[0].click();
|
|
7
|
+
return true;
|
|
8
|
+
})()`)!==!0)throw new c("Exact comment control is unavailable, disabled, or the note changed")}function s(t){return String(t).replace(/\r\n/g,`
|
|
9
|
+
`).replace(/\u00a0/g," ").trim()}function N(t,r,a){const e=t.editor;if(!e?.active||!e.canSend||s(e.text)!==s(a))throw new c("Comment draft did not retain the exact text or the send button is disabled");if(r?e.replyAuthor!==`回复 ${r.author}`||s(e.replyText)!==s(r.text):e.replyAuthor!==""||e.replyText!=="")throw new c("Reply target in the composer does not match the requested comment")}export const writeArgs=[{name:"root-id",help:"Top-level root_id from comments; use when targeting a nested reply"},{name:"execute",type:"boolean",default:!1,help:"Submit after verification (default: dry-run only)"}],writeColumns=["status","note_id","id","root_id","parent_id","text","url"];export async function runComment(t,r){const{url:a,noteId:e}=C(r["note-id"]),n=r.text;if(typeof n!=="string"||!n.trim())throw new v("Comment text must not be empty");const u=r["reply-to"]==null?null:p(r["reply-to"],"reply-to"),m=r["root-id"]==null?null:p(r["root-id"],"root-id");if(m&&!u)throw new v("--root-id requires --reply-to (or the reply command)");let i=await I(t,a,e);if(i.editor?.text?.trim())throw new c("Existing comment draft is not empty; refusing to overwrite it");let d=null;if(u)({state:i,target:d}=await _(t,e,u,m));if(!i.selfId)throw new c("Cannot identify the current account for comment verification");const y=i.rows.filter((o)=>o.userId===i.selfId&&o.parent_id===u&&s(o.text)===s(n));if(y.length){const o=y[0];return[{status:"already-present",note_id:e,id:o.id,root_id:o.root_id,parent_id:o.parent_id,text:o.text,url:a}]}const k=new Set(i.rows.map((o)=>o.id));if(d)await b(t,e,`#comment-${d.id} .reply`);else if(!i.editor?.active)await t.click(".engage-bar:has(#content-textarea) .not-active");let x=!1;for(let o=0;o<10;o++){await t.wait({time:0.2});if((await h(t,e)).editor?.active){x=!0;break}}if(!x)throw new c("Comment composer did not open");await t.typeText(g,n);await t.wait({time:0.2});i=await h(t,e);N(i,d,n);if(!r.execute){await b(t,e,R);let o=!1;for(let l=0;l<10;l++){await t.wait({time:0.3});const w=await h(t,e);if(w.editor?.text===""&&!w.editor.active){o=!0;break}}if(!o)throw new c("Dry-run draft was verified but could not be cleared");return[{status:"dry-run",note_id:e,id:null,root_id:d?.root_id??null,parent_id:u,text:n,url:a}]}N(await h(t,e),d,n);try{await b(t,e,q);let o;for(let w=0;w<15;w++){await t.wait({time:0.5});o=(await h(t,e)).rows.find((f)=>!k.has(f.id)&&f.userId===i.selfId&&f.parent_id===u&&(!d||f.root_id===d.root_id)&&s(f.text)===s(n));if(o)break}if(!o)throw Error("No new matching comment appeared");await E(t,e);const{target:l}=await _(t,e,o.id,d?.root_id??null);if(l.userId!==i.selfId||l.parent_id!==u||s(l.text)!==s(n))throw Error("Persisted comment does not match the requested author, text and reply target");return[{status:"posted",note_id:e,id:l.id,root_id:l.root_id,parent_id:l.parent_id,text:l.text,url:a}]}catch(o){throw new c(`Comment submission could not be confirmed: ${o.message}`,S)}}export async function runCommentLike(t,r){const{url:a,noteId:e}=C(r["note-id"]),n=p(r["comment-id"]),u=r["root-id"]==null?null:p(r["root-id"],"root-id"),m=!r.undo;await I(t,a,e);const{target:i}=await _(t,e,n,u),d=(o)=>[{status:o,note_id:e,id:n,root_id:i.root_id,liked:m,url:a}];if(i.liked===m)return d(m?"already-liked":"already-unliked");const y=`#comment-${n} .like-wrapper`;if(await t.evaluate(`document.querySelectorAll(${JSON.stringify(y)}).length`)!==1)throw new c("Exact comment like control is unavailable or ambiguous");if(!r.execute)return[{status:"dry-run",note_id:e,id:n,root_id:i.root_id,liked:i.liked,url:a}];const x=(await h(t,e)).rows.find((o)=>o.id===n);if(!x||x.liked!==i.liked)throw new c("Comment like state changed before the click; stopped");try{await b(t,e,y);let o=!1;for(let w=0;w<10;w++){await t.wait({time:0.5});if((await h(t,e)).rows.find((f)=>f.id===n)?.liked===m){o=!0;break}}if(!o)throw Error("Like state did not change");await E(t,e);const{target:l}=await _(t,e,n,i.root_id);if(l.liked!==m)throw Error("Like state did not persist after reload");return d(m?"liked":"unliked")}catch(o){throw new c(`Comment like could not be confirmed: ${o.message}`,S)}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{ArgumentError as h,AuthRequiredError as b,CommandExecutionError as f,EmptyResultError as g}from"@jackwener/opencli/errors";import{buildNoteUrl as _,parseNoteId as C}from"../note-helpers.js";export function commentId(e,o="comment-id"){if(typeof e!=="string"||!/^[a-f0-9]{24}$/i.test(e))throw new h(`${o} must be a 24-character comment ID from comments/replies`);return e.toLowerCase()}export function commentNote(e){const o=_(String(e??""),{commandName:"xiaohongshu comment operations"}),t=new URL(o);if(t.protocol!=="https:"||t.hostname!=="www.xiaohongshu.com"||t.port||t.username||t.password)throw new h("Comment operations require an https://www.xiaohongshu.com signed note URL");return{url:o,noteId:commentId(C(o),"note-id")}}export function readCommentPage(e){const o=document.querySelector("#app")?.__vue_app__?.config?.globalProperties?.$pinia,t=o?._s?.get("note"),n=o?._s?.get("user"),d=location.pathname.match(/\/(?:explore|note|search_result|discovery\/item)\/([a-f0-9]+)\/?$/i)?.[1]||location.pathname.match(/\/user\/profile\/[^/]+\/([a-f0-9]+)\/?$/i)?.[1];if(location.hostname!=="www.xiaohongshu.com"||location.protocol!=="https:")return{error:"wrong_host"};if(/website-login\/error|error_code=300017|error_code=300031/.test(location.href))return{error:"security_block"};if(n?.loggedIn===!1||/\/login(?:\/|$)/.test(location.pathname))return{error:"login"};if(d!==e||t?.currentNoteId!==e)return{error:"wrong_note"};const s=t.noteDetailMap?.[e]?.comments;if(!Array.isArray(s?.list)||typeof s.hasMore!=="boolean")return{error:"comments_unavailable"};const l=[],p=(r,m)=>{const a=document.getElementById("comment-"+r.id),w=m?.id||r.id,u=m?null:Number(r.subCommentCount);l.push({id:r.id,root_id:w,parent_id:m?r.targetComment?.id??null:null,author:r.userInfo?.nickname??"",userId:r.userInfo?.userId??"",profileUrl:r.userInfo?.userId?"https://www.xiaohongshu.com/user/profile/"+r.userInfo.userId:"",text:r.content,likes:Number(r.likeCount),liked:r.liked,time:a?.querySelector(".date, .time")?.textContent?.trim()??"",is_reply:Boolean(m),reply_to:m?r.targetComment?.userInfo?.nickname??"":"",reply_count:u,replies_complete:m?null:Number.isInteger(u)&&new Set((r.subComments||[]).map((x)=>x.id)).size>=u,rendered:Boolean(a)})};for(const r of s.list){p(r,null);for(const m of r.subComments||[])p(m,r)}const i=document.querySelector("#content-textarea"),c=i?.closest(".engage-bar"),y=Array.from(document.querySelectorAll('a[href*="/user/profile/"]')).find((r)=>r.textContent.trim()==="我");return{noteId:e,rows:l,hasMore:s.hasMore,loading:s.loading===!0,selfId:y?.getAttribute("href")?.match(/^\/user\/profile\/([a-f0-9]{24})(?:[/?]|$)/i)?.[1]??null,editor:i?{text:i.innerText??i.textContent,replyAuthor:c?.querySelector(".reply-content .reply")?.textContent?.trim()??"",replyText:c?.querySelector(".reply-content .content")?.textContent??"",active:c?.classList.contains("active")===!0,canSend:c?.querySelector("button.submit")?.disabled===!1}:null}}export function snapshotScript(e){return`(${readCommentPage.toString()})(${JSON.stringify(e)})`}export async function commentSnapshot(e,o){const t=await e.evaluate(snapshotScript(o));if(t?.error==="login")throw new b("www.xiaohongshu.com");if(!t||t.error||t.noteId!==o||!Array.isArray(t.rows)||typeof t.hasMore!=="boolean")throw new f(`Cannot inspect the requested comment page: ${t?.error||"malformed state"}`);for(const n of t.rows)if(!/^[a-f0-9]{24}$/i.test(n.id)||!/^[a-f0-9]{24}$/i.test(n.root_id)||n.is_reply&&!/^[a-f0-9]{24}$/i.test(n.parent_id)||typeof n.text!=="string"||typeof n.liked!=="boolean"||!Number.isInteger(n.likes)||n.likes<0)throw new f("Malformed comment identity/content/state; refusing to operate");return t}export async function openCommentPage(e,o,t){await e.goto(o);await e.wait({time:3});return commentSnapshot(e,t)}export async function reloadCommentPage(e,o){await e.cdp("Page.reload",{});await e.wait({time:3});return commentSnapshot(e,o)}export async function expandCommentThread(e,o,t,n=50,d=null){for(let s=0;s<50;s++){const l=await commentSnapshot(e,o),p=l.rows.find((a)=>a.id===t&&!a.is_reply);if(!p)throw new g("xiaohongshu replies","Root comment is not loaded");const i=l.rows.filter((a)=>a.is_reply&&a.root_id===t);if(d?i.some((a)=>a.id===d&&a.rendered):i.length>=n)return l;if(p.replies_complete)return l;const c=`.parent-comment:has(#comment-${t}) .show-more`;if(!await e.evaluate(`(() => { const e = document.querySelector(${JSON.stringify(c)}); return !!e && e.getClientRects().length > 0; })()`))throw new f("Reply list is incomplete but its expand control is unavailable");if(await e.evaluate(`(() => { const es = document.querySelectorAll(${JSON.stringify(c)}); if (es.length !== 1) return false; es[0].click(); return true; })()`)!==!0)throw new f("Reply expand control became ambiguous or disappeared");let m=!1;for(let a=0;a<10;a++){await e.wait({time:0.3});const w=await commentSnapshot(e,o);if(w.rows.filter((u)=>u.is_reply&&u.root_id===t).length>i.length||w.rows.find((u)=>u.id===t)?.replies_complete){m=!0;break}}if(!m)throw new f("Expanding replies produced no progress; stopped without repeating the click")}throw new f("Reply loading exceeded its bounded limit")}export async function locateComment(e,o,t,n=null){for(let d=0;d<50;d++){let s=await commentSnapshot(e,o);const l=s.rows.find((i)=>i.id===t&&i.rendered);if(l){if(n&&l.root_id!==n)throw new h("comment-id does not belong to root-id");return{state:s,target:l}}if(n&&s.rows.some((i)=>i.id===n&&!i.is_reply)){s=await expandCommentThread(e,o,n,50,t);const i=s.rows.find((c)=>c.id===t&&c.root_id===n&&c.rendered);if(i)return{state:s,target:i};break}if(!s.hasMore&&!s.loading)break;if(s.rows.filter((i)=>!i.is_reply).length>=50)break;await e.evaluate("(() => { const es = document.querySelectorAll('.parent-comment'); es[es.length - 1]?.scrollIntoView({block:'end'}); })()");await e.wait({time:1})}throw new g("xiaohongshu comment","Exact comment not found within 50 top-level comments. For a nested reply, supply its root_id via --root-id.")}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cli as e,Strategy as t}from"@jackwener/opencli/registry";import{runCommentLike as o,writeArgs as i}from"./_shared/comment-actions.js";e({site:"xiaohongshu",name:"comment-like",access:"write",description:"点赞/取消点赞小红书评论(含楼中楼);默认 dry-run,--execute 才操作",domain:"www.xiaohongshu.com",strategy:t.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"note-id",positional:!0,required:!0,help:"Full signed note URL with xsec_token"},{name:"comment-id",positional:!0,required:!0,help:"Exact comment ID from comments/replies"},{name:"undo",type:"boolean",default:!1,help:"Ensure the comment is not liked"},...i],columns:["status","note_id","id","root_id","liked","url"],func:o});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cli as e,Strategy as t}from"@jackwener/opencli/registry";import{runComment as o,writeArgs as r,writeColumns as n}from"./_shared/comment-actions.js";e({site:"xiaohongshu",name:"comment",access:"write",description:"发布小红书评论或定向回复;默认验证草稿,--execute 才发送",domain:"www.xiaohongshu.com",strategy:t.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"note-id",positional:!0,required:!0,help:"Full signed note URL with xsec_token"},{name:"text",positional:!0,required:!0,help:"Exact plain-text comment"},{name:"reply-to",help:"Exact comment ID to reply to, including a nested reply"},...r],columns:n,func:o});
|