publishport-opencli 1.8.5-pp.34 → 1.8.5-pp.36
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 +369 -10
- package/clis/dianping/auth.js +8 -9
- package/clis/dianping/cityResolver.js +4 -1
- package/clis/dianping/search.js +1 -1
- package/clis/discord-web/auth.js +10 -0
- package/clis/discord-web/send.js +13 -0
- package/clis/facebook/comment.js +18 -0
- package/clis/facebook/like.js +29 -0
- package/clis/hackernews/auth.js +9 -0
- package/clis/hackernews/comment.js +61 -0
- package/clis/hackernews/submit.js +65 -0
- package/clis/jimeng/auth.js +2 -2
- package/clis/jimeng/generate.js +53 -46
- package/clis/jimeng/history.js +22 -13
- package/clis/pixiv/auth.js +7 -16
- package/clis/reddit/submit.js +42 -0
- package/clis/suno/auth.js +13 -17
- package/package.json +1 -1
package/cli-manifest.json
CHANGED
|
@@ -11864,6 +11864,92 @@
|
|
|
11864
11864
|
"sourceFile": "discord-app/threads.js",
|
|
11865
11865
|
"navigateBefore": true
|
|
11866
11866
|
},
|
|
11867
|
+
{
|
|
11868
|
+
"site": "discord-web",
|
|
11869
|
+
"name": "login",
|
|
11870
|
+
"description": "Open discord-web login and wait until the browser session is authenticated",
|
|
11871
|
+
"access": "write",
|
|
11872
|
+
"domain": "discord.com",
|
|
11873
|
+
"strategy": "cookie",
|
|
11874
|
+
"browser": true,
|
|
11875
|
+
"args": [
|
|
11876
|
+
{
|
|
11877
|
+
"name": "timeout",
|
|
11878
|
+
"type": "int",
|
|
11879
|
+
"default": 300,
|
|
11880
|
+
"required": false,
|
|
11881
|
+
"help": "Maximum seconds to wait for the user to finish login"
|
|
11882
|
+
}
|
|
11883
|
+
],
|
|
11884
|
+
"columns": [
|
|
11885
|
+
"status",
|
|
11886
|
+
"logged_in",
|
|
11887
|
+
"site",
|
|
11888
|
+
"username",
|
|
11889
|
+
"display_name"
|
|
11890
|
+
],
|
|
11891
|
+
"type": "js",
|
|
11892
|
+
"modulePath": "discord-web/auth.js",
|
|
11893
|
+
"sourceFile": "discord-web/auth.js",
|
|
11894
|
+
"navigateBefore": false,
|
|
11895
|
+
"siteSession": "persistent",
|
|
11896
|
+
"defaultWindowMode": "foreground"
|
|
11897
|
+
},
|
|
11898
|
+
{
|
|
11899
|
+
"site": "discord-web",
|
|
11900
|
+
"name": "send",
|
|
11901
|
+
"description": "Send a message to a Discord channel (discord.com). Pass the channel URL or \"<guild>/<channel>\". Multi-line text is sent as one message.",
|
|
11902
|
+
"access": "write",
|
|
11903
|
+
"domain": "discord.com",
|
|
11904
|
+
"strategy": "cookie",
|
|
11905
|
+
"browser": true,
|
|
11906
|
+
"args": [
|
|
11907
|
+
{
|
|
11908
|
+
"name": "channel",
|
|
11909
|
+
"type": "string",
|
|
11910
|
+
"required": true,
|
|
11911
|
+
"positional": true,
|
|
11912
|
+
"help": "Channel URL (https://discord.com/channels/<guild>/<channel>) or \"<guild>/<channel>\""
|
|
11913
|
+
},
|
|
11914
|
+
{
|
|
11915
|
+
"name": "text",
|
|
11916
|
+
"type": "string",
|
|
11917
|
+
"required": true,
|
|
11918
|
+
"positional": true,
|
|
11919
|
+
"help": "Message text (multi-line allowed)"
|
|
11920
|
+
}
|
|
11921
|
+
],
|
|
11922
|
+
"columns": [
|
|
11923
|
+
"status",
|
|
11924
|
+
"channel_id",
|
|
11925
|
+
"message"
|
|
11926
|
+
],
|
|
11927
|
+
"type": "js",
|
|
11928
|
+
"modulePath": "discord-web/send.js",
|
|
11929
|
+
"sourceFile": "discord-web/send.js",
|
|
11930
|
+
"navigateBefore": "https://discord.com"
|
|
11931
|
+
},
|
|
11932
|
+
{
|
|
11933
|
+
"site": "discord-web",
|
|
11934
|
+
"name": "whoami",
|
|
11935
|
+
"description": "Show the current logged-in discord-web account",
|
|
11936
|
+
"access": "read",
|
|
11937
|
+
"domain": "discord.com",
|
|
11938
|
+
"strategy": "cookie",
|
|
11939
|
+
"browser": true,
|
|
11940
|
+
"args": [],
|
|
11941
|
+
"columns": [
|
|
11942
|
+
"logged_in",
|
|
11943
|
+
"site",
|
|
11944
|
+
"username",
|
|
11945
|
+
"display_name"
|
|
11946
|
+
],
|
|
11947
|
+
"type": "js",
|
|
11948
|
+
"modulePath": "discord-web/auth.js",
|
|
11949
|
+
"sourceFile": "discord-web/auth.js",
|
|
11950
|
+
"navigateBefore": false,
|
|
11951
|
+
"siteSession": "persistent"
|
|
11952
|
+
},
|
|
11867
11953
|
{
|
|
11868
11954
|
"site": "dockerhub",
|
|
11869
11955
|
"name": "image",
|
|
@@ -14603,6 +14689,40 @@
|
|
|
14603
14689
|
"sourceFile": "facebook/add-friend.js",
|
|
14604
14690
|
"navigateBefore": "https://www.facebook.com"
|
|
14605
14691
|
},
|
|
14692
|
+
{
|
|
14693
|
+
"site": "facebook",
|
|
14694
|
+
"name": "comment",
|
|
14695
|
+
"description": "Post a comment on a Facebook post. Pass the post URL and the comment text (multi-line allowed). The comment is published from your logged-in account — write a genuine, on-topic remark, not filler.",
|
|
14696
|
+
"access": "write",
|
|
14697
|
+
"domain": "facebook.com",
|
|
14698
|
+
"strategy": "cookie",
|
|
14699
|
+
"browser": true,
|
|
14700
|
+
"args": [
|
|
14701
|
+
{
|
|
14702
|
+
"name": "url",
|
|
14703
|
+
"type": "string",
|
|
14704
|
+
"required": true,
|
|
14705
|
+
"positional": true,
|
|
14706
|
+
"help": "Post URL (e.g. https://www.facebook.com/<user>/posts/<id> or a permalink)"
|
|
14707
|
+
},
|
|
14708
|
+
{
|
|
14709
|
+
"name": "text",
|
|
14710
|
+
"type": "string",
|
|
14711
|
+
"required": true,
|
|
14712
|
+
"positional": true,
|
|
14713
|
+
"help": "Comment text (multi-line allowed)"
|
|
14714
|
+
}
|
|
14715
|
+
],
|
|
14716
|
+
"columns": [
|
|
14717
|
+
"status",
|
|
14718
|
+
"url",
|
|
14719
|
+
"message"
|
|
14720
|
+
],
|
|
14721
|
+
"type": "js",
|
|
14722
|
+
"modulePath": "facebook/comment.js",
|
|
14723
|
+
"sourceFile": "facebook/comment.js",
|
|
14724
|
+
"navigateBefore": "https://facebook.com"
|
|
14725
|
+
},
|
|
14606
14726
|
{
|
|
14607
14727
|
"site": "facebook",
|
|
14608
14728
|
"name": "events",
|
|
@@ -14740,6 +14860,41 @@
|
|
|
14740
14860
|
"sourceFile": "facebook/join-group.js",
|
|
14741
14861
|
"navigateBefore": "https://www.facebook.com"
|
|
14742
14862
|
},
|
|
14863
|
+
{
|
|
14864
|
+
"site": "facebook",
|
|
14865
|
+
"name": "like",
|
|
14866
|
+
"description": "Like a Facebook post (ends in the liked state; no-op if already liked). Pass the post URL. Reacts as a genuine like from your logged-in account — use it to engage with content you actually mean to endorse.",
|
|
14867
|
+
"access": "write",
|
|
14868
|
+
"domain": "facebook.com",
|
|
14869
|
+
"strategy": "cookie",
|
|
14870
|
+
"browser": true,
|
|
14871
|
+
"args": [
|
|
14872
|
+
{
|
|
14873
|
+
"name": "url",
|
|
14874
|
+
"type": "string",
|
|
14875
|
+
"required": true,
|
|
14876
|
+
"positional": true,
|
|
14877
|
+
"help": "Post URL (e.g. https://www.facebook.com/<user>/posts/<id> or a permalink)"
|
|
14878
|
+
},
|
|
14879
|
+
{
|
|
14880
|
+
"name": "remove",
|
|
14881
|
+
"type": "bool",
|
|
14882
|
+
"default": false,
|
|
14883
|
+
"required": false,
|
|
14884
|
+
"help": "Remove your like instead (end in the un-liked state)"
|
|
14885
|
+
}
|
|
14886
|
+
],
|
|
14887
|
+
"columns": [
|
|
14888
|
+
"status",
|
|
14889
|
+
"action",
|
|
14890
|
+
"url",
|
|
14891
|
+
"message"
|
|
14892
|
+
],
|
|
14893
|
+
"type": "js",
|
|
14894
|
+
"modulePath": "facebook/like.js",
|
|
14895
|
+
"sourceFile": "facebook/like.js",
|
|
14896
|
+
"navigateBefore": "https://facebook.com"
|
|
14897
|
+
},
|
|
14743
14898
|
{
|
|
14744
14899
|
"site": "facebook",
|
|
14745
14900
|
"name": "login",
|
|
@@ -17244,6 +17399,42 @@
|
|
|
17244
17399
|
"modulePath": "hackernews/best.js",
|
|
17245
17400
|
"sourceFile": "hackernews/best.js"
|
|
17246
17401
|
},
|
|
17402
|
+
{
|
|
17403
|
+
"site": "hackernews",
|
|
17404
|
+
"name": "comment",
|
|
17405
|
+
"description": "Post a comment on a Hacker News item. Pass the parent id (a story to comment on it, or a comment to reply). Contribute a substantive, on-topic comment — HN flags low-effort or off-topic replies.",
|
|
17406
|
+
"access": "write",
|
|
17407
|
+
"domain": "news.ycombinator.com",
|
|
17408
|
+
"strategy": "cookie",
|
|
17409
|
+
"browser": true,
|
|
17410
|
+
"args": [
|
|
17411
|
+
{
|
|
17412
|
+
"name": "parent",
|
|
17413
|
+
"type": "string",
|
|
17414
|
+
"required": true,
|
|
17415
|
+
"positional": true,
|
|
17416
|
+
"help": "Parent item id (story or comment) to reply to"
|
|
17417
|
+
},
|
|
17418
|
+
{
|
|
17419
|
+
"name": "text",
|
|
17420
|
+
"type": "string",
|
|
17421
|
+
"required": true,
|
|
17422
|
+
"positional": true,
|
|
17423
|
+
"help": "Comment text (HN markup: blank line = paragraph)"
|
|
17424
|
+
}
|
|
17425
|
+
],
|
|
17426
|
+
"columns": [
|
|
17427
|
+
"status",
|
|
17428
|
+
"id",
|
|
17429
|
+
"url",
|
|
17430
|
+
"parent",
|
|
17431
|
+
"message"
|
|
17432
|
+
],
|
|
17433
|
+
"type": "js",
|
|
17434
|
+
"modulePath": "hackernews/comment.js",
|
|
17435
|
+
"sourceFile": "hackernews/comment.js",
|
|
17436
|
+
"navigateBefore": "https://news.ycombinator.com"
|
|
17437
|
+
},
|
|
17247
17438
|
{
|
|
17248
17439
|
"site": "hackernews",
|
|
17249
17440
|
"name": "jobs",
|
|
@@ -17272,6 +17463,37 @@
|
|
|
17272
17463
|
"modulePath": "hackernews/jobs.js",
|
|
17273
17464
|
"sourceFile": "hackernews/jobs.js"
|
|
17274
17465
|
},
|
|
17466
|
+
{
|
|
17467
|
+
"site": "hackernews",
|
|
17468
|
+
"name": "login",
|
|
17469
|
+
"description": "Open hackernews login and wait until the browser session is authenticated",
|
|
17470
|
+
"access": "write",
|
|
17471
|
+
"domain": "news.ycombinator.com",
|
|
17472
|
+
"strategy": "cookie",
|
|
17473
|
+
"browser": true,
|
|
17474
|
+
"args": [
|
|
17475
|
+
{
|
|
17476
|
+
"name": "timeout",
|
|
17477
|
+
"type": "int",
|
|
17478
|
+
"default": 300,
|
|
17479
|
+
"required": false,
|
|
17480
|
+
"help": "Maximum seconds to wait for the user to finish login"
|
|
17481
|
+
}
|
|
17482
|
+
],
|
|
17483
|
+
"columns": [
|
|
17484
|
+
"status",
|
|
17485
|
+
"logged_in",
|
|
17486
|
+
"site",
|
|
17487
|
+
"username",
|
|
17488
|
+
"karma"
|
|
17489
|
+
],
|
|
17490
|
+
"type": "js",
|
|
17491
|
+
"modulePath": "hackernews/auth.js",
|
|
17492
|
+
"sourceFile": "hackernews/auth.js",
|
|
17493
|
+
"navigateBefore": false,
|
|
17494
|
+
"siteSession": "persistent",
|
|
17495
|
+
"defaultWindowMode": "foreground"
|
|
17496
|
+
},
|
|
17275
17497
|
{
|
|
17276
17498
|
"site": "hackernews",
|
|
17277
17499
|
"name": "new",
|
|
@@ -17435,6 +17657,47 @@
|
|
|
17435
17657
|
"modulePath": "hackernews/show.js",
|
|
17436
17658
|
"sourceFile": "hackernews/show.js"
|
|
17437
17659
|
},
|
|
17660
|
+
{
|
|
17661
|
+
"site": "hackernews",
|
|
17662
|
+
"name": "submit",
|
|
17663
|
+
"description": "Submit a story to Hacker News. Link post by default (--url), or an Ask/text post with --text. HN gates submissions on account age/karma and dedupes URLs — rejections are surfaced verbatim. Post genuinely worthwhile content; low-effort submissions hurt the account.",
|
|
17664
|
+
"access": "write",
|
|
17665
|
+
"domain": "news.ycombinator.com",
|
|
17666
|
+
"strategy": "cookie",
|
|
17667
|
+
"browser": true,
|
|
17668
|
+
"args": [
|
|
17669
|
+
{
|
|
17670
|
+
"name": "title",
|
|
17671
|
+
"type": "string",
|
|
17672
|
+
"required": true,
|
|
17673
|
+
"positional": true,
|
|
17674
|
+
"help": "Story title (max 80 chars)"
|
|
17675
|
+
},
|
|
17676
|
+
{
|
|
17677
|
+
"name": "url",
|
|
17678
|
+
"type": "string",
|
|
17679
|
+
"required": false,
|
|
17680
|
+
"help": "External URL for a link post (mutually exclusive with --text)"
|
|
17681
|
+
},
|
|
17682
|
+
{
|
|
17683
|
+
"name": "text",
|
|
17684
|
+
"type": "string",
|
|
17685
|
+
"required": false,
|
|
17686
|
+
"help": "Body text for an Ask HN / text post (mutually exclusive with --url)"
|
|
17687
|
+
}
|
|
17688
|
+
],
|
|
17689
|
+
"columns": [
|
|
17690
|
+
"status",
|
|
17691
|
+
"id",
|
|
17692
|
+
"url",
|
|
17693
|
+
"title",
|
|
17694
|
+
"message"
|
|
17695
|
+
],
|
|
17696
|
+
"type": "js",
|
|
17697
|
+
"modulePath": "hackernews/submit.js",
|
|
17698
|
+
"sourceFile": "hackernews/submit.js",
|
|
17699
|
+
"navigateBefore": "https://news.ycombinator.com"
|
|
17700
|
+
},
|
|
17438
17701
|
{
|
|
17439
17702
|
"site": "hackernews",
|
|
17440
17703
|
"name": "top",
|
|
@@ -17492,6 +17755,27 @@
|
|
|
17492
17755
|
"modulePath": "hackernews/user.js",
|
|
17493
17756
|
"sourceFile": "hackernews/user.js"
|
|
17494
17757
|
},
|
|
17758
|
+
{
|
|
17759
|
+
"site": "hackernews",
|
|
17760
|
+
"name": "whoami",
|
|
17761
|
+
"description": "Show the current logged-in hackernews account",
|
|
17762
|
+
"access": "read",
|
|
17763
|
+
"domain": "news.ycombinator.com",
|
|
17764
|
+
"strategy": "cookie",
|
|
17765
|
+
"browser": true,
|
|
17766
|
+
"args": [],
|
|
17767
|
+
"columns": [
|
|
17768
|
+
"logged_in",
|
|
17769
|
+
"site",
|
|
17770
|
+
"username",
|
|
17771
|
+
"karma"
|
|
17772
|
+
],
|
|
17773
|
+
"type": "js",
|
|
17774
|
+
"modulePath": "hackernews/auth.js",
|
|
17775
|
+
"sourceFile": "hackernews/auth.js",
|
|
17776
|
+
"navigateBefore": false,
|
|
17777
|
+
"siteSession": "persistent"
|
|
17778
|
+
},
|
|
17495
17779
|
{
|
|
17496
17780
|
"site": "hashnode",
|
|
17497
17781
|
"name": "login",
|
|
@@ -20613,31 +20897,26 @@
|
|
|
20613
20897
|
"positional": true,
|
|
20614
20898
|
"help": "图片描述 prompt"
|
|
20615
20899
|
},
|
|
20616
|
-
{
|
|
20617
|
-
"name": "model",
|
|
20618
|
-
"type": "string",
|
|
20619
|
-
"default": "high_aes_general_v50",
|
|
20620
|
-
"required": false,
|
|
20621
|
-
"help": "模型: high_aes_general_v50 (5.0 Lite), high_aes_general_v42 (4.6), high_aes_general_v40 (4.0)"
|
|
20622
|
-
},
|
|
20623
20900
|
{
|
|
20624
20901
|
"name": "wait",
|
|
20625
20902
|
"type": "int",
|
|
20626
|
-
"default":
|
|
20903
|
+
"default": 60,
|
|
20627
20904
|
"required": false,
|
|
20628
|
-
"help": "
|
|
20905
|
+
"help": "等待生成完成的最长秒数(每 3 秒轮询一次,超时会返回 pending 并提示用 history 回查)"
|
|
20629
20906
|
}
|
|
20630
20907
|
],
|
|
20631
20908
|
"columns": [
|
|
20632
20909
|
"status",
|
|
20633
20910
|
"prompt",
|
|
20911
|
+
"model",
|
|
20634
20912
|
"image_count",
|
|
20635
20913
|
"image_urls"
|
|
20636
20914
|
],
|
|
20637
20915
|
"type": "js",
|
|
20638
20916
|
"modulePath": "jimeng/generate.js",
|
|
20639
20917
|
"sourceFile": "jimeng/generate.js",
|
|
20640
|
-
"navigateBefore":
|
|
20918
|
+
"navigateBefore": false,
|
|
20919
|
+
"siteSession": "persistent"
|
|
20641
20920
|
},
|
|
20642
20921
|
{
|
|
20643
20922
|
"site": "jimeng",
|
|
@@ -32186,6 +32465,86 @@
|
|
|
32186
32465
|
"sourceFile": "reddit/search.js",
|
|
32187
32466
|
"navigateBefore": "https://reddit.com"
|
|
32188
32467
|
},
|
|
32468
|
+
{
|
|
32469
|
+
"site": "reddit",
|
|
32470
|
+
"name": "submit",
|
|
32471
|
+
"description": "Submit a new post to a subreddit. Text post by default (--text); pass --url for a link post. Note: many subreddits gate submissions behind karma/age requirements or flair — a rejection is surfaced verbatim.",
|
|
32472
|
+
"access": "write",
|
|
32473
|
+
"domain": "reddit.com",
|
|
32474
|
+
"strategy": "cookie",
|
|
32475
|
+
"browser": true,
|
|
32476
|
+
"args": [
|
|
32477
|
+
{
|
|
32478
|
+
"name": "subreddit",
|
|
32479
|
+
"type": "string",
|
|
32480
|
+
"required": true,
|
|
32481
|
+
"positional": true,
|
|
32482
|
+
"help": "Target subreddit, e.g. \"test\" or \"r/test\""
|
|
32483
|
+
},
|
|
32484
|
+
{
|
|
32485
|
+
"name": "title",
|
|
32486
|
+
"type": "string",
|
|
32487
|
+
"required": true,
|
|
32488
|
+
"positional": true,
|
|
32489
|
+
"help": "Post title (max 300 chars)"
|
|
32490
|
+
},
|
|
32491
|
+
{
|
|
32492
|
+
"name": "text",
|
|
32493
|
+
"type": "string",
|
|
32494
|
+
"required": false,
|
|
32495
|
+
"help": "Body text (markdown) for a self/text post"
|
|
32496
|
+
},
|
|
32497
|
+
{
|
|
32498
|
+
"name": "url",
|
|
32499
|
+
"type": "string",
|
|
32500
|
+
"required": false,
|
|
32501
|
+
"help": "External URL for a link post (mutually exclusive with --text)"
|
|
32502
|
+
},
|
|
32503
|
+
{
|
|
32504
|
+
"name": "flair-id",
|
|
32505
|
+
"type": "string",
|
|
32506
|
+
"required": false,
|
|
32507
|
+
"help": "Flair template id, if the subreddit requires one"
|
|
32508
|
+
},
|
|
32509
|
+
{
|
|
32510
|
+
"name": "flair-text",
|
|
32511
|
+
"type": "string",
|
|
32512
|
+
"required": false,
|
|
32513
|
+
"help": "Flair text to accompany --flair-id"
|
|
32514
|
+
},
|
|
32515
|
+
{
|
|
32516
|
+
"name": "nsfw",
|
|
32517
|
+
"type": "bool",
|
|
32518
|
+
"default": false,
|
|
32519
|
+
"required": false,
|
|
32520
|
+
"help": "Mark the post as NSFW"
|
|
32521
|
+
},
|
|
32522
|
+
{
|
|
32523
|
+
"name": "spoiler",
|
|
32524
|
+
"type": "bool",
|
|
32525
|
+
"default": false,
|
|
32526
|
+
"required": false,
|
|
32527
|
+
"help": "Mark the post as a spoiler"
|
|
32528
|
+
},
|
|
32529
|
+
{
|
|
32530
|
+
"name": "sendreplies",
|
|
32531
|
+
"type": "bool",
|
|
32532
|
+
"default": true,
|
|
32533
|
+
"required": false,
|
|
32534
|
+
"help": "Receive inbox replies for this post (default: true)"
|
|
32535
|
+
}
|
|
32536
|
+
],
|
|
32537
|
+
"columns": [
|
|
32538
|
+
"status",
|
|
32539
|
+
"url",
|
|
32540
|
+
"id",
|
|
32541
|
+
"message"
|
|
32542
|
+
],
|
|
32543
|
+
"type": "js",
|
|
32544
|
+
"modulePath": "reddit/submit.js",
|
|
32545
|
+
"sourceFile": "reddit/submit.js",
|
|
32546
|
+
"navigateBefore": "https://reddit.com"
|
|
32547
|
+
},
|
|
32189
32548
|
{
|
|
32190
32549
|
"site": "reddit",
|
|
32191
32550
|
"name": "subreddit",
|
package/clis/dianping/auth.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import{AuthRequiredError as
|
|
1
|
+
import{AuthRequiredError as F,CommandExecutionError as K}from"@jackwener/opencli/errors";import{registerSiteAuthCommands as L}from"../_shared/site-auth.js";async function H(z){return(await z.getCookies({url:"https://www.dianping.com"})).some((B)=>B.name==="dper"&&B.value)}async function J(z){if(!await H(z))throw new F("dianping.com","Dianping dper cookie missing");await z.goto("https://www.dianping.com/citylist");await z.wait(2);const G=await z.evaluate("location.href");if(/account\.dianping\.com\/(pc)?login/.test(String(G||"")))throw new F("dianping.com",`Dianping page redirected to login: ${G}`);const B=await z.evaluate(`
|
|
2
2
|
(() => {
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
return { user_id: uidMatch?.[1] || '', nickname };
|
|
3
|
+
const trigger = document.querySelector('.userinfo-container .J-user-trigger, .userinfo-container a.username');
|
|
4
|
+
if (!trigger) return { loggedOut: true };
|
|
5
|
+
const nickname = (trigger.textContent || '').trim();
|
|
6
|
+
const href = trigger.getAttribute('href') || '';
|
|
7
|
+
const uid = (String(href).match(/\\/member\\/(\\d+)/) || [])[1] || '';
|
|
8
|
+
return { user_id: uid, nickname };
|
|
10
9
|
})()
|
|
11
|
-
`);if(
|
|
10
|
+
`);if(B?.loggedOut)throw new F("dianping.com","Dianping session anonymous — no logged-in user trigger in top nav");if(!B?.user_id)throw new K("Dianping user trigger found but no /member/<id> href — layout drift");return{user_id:String(B.user_id),nickname:String(B.nickname||"")}}L({site:"dianping",domain:"dianping.com",loginUrl:"https://account.dianping.com/pclogin",columns:["user_id","nickname"],quickCheck:H,verify:J,poll:async(z)=>{if(!await H(z))throw new F("dianping.com","Waiting for Dianping dper cookie");return J(z)}});
|
|
@@ -1 +1,4 @@
|
|
|
1
|
-
import{ArgumentError as
|
|
1
|
+
import{ArgumentError as B,CommandExecutionError as J}from"@jackwener/opencli/errors";import{CITY_ID as b,resolveCityId as j}from"./utils.js";const $=/^[一-龥]+$/,F=/^[a-z]+$/,Z=new Map;export function clearCityResolverCache(){Z.clear()}export async function resolveCityIdAsync(z,G){if(G==null||G==="")return null;const q=String(G).trim();if(!q)return null;if(/^\d+$/.test(q))return Number(q);const W=q.toLowerCase();try{const Q=j(q);if(Q!=null)return Q}catch(Q){if(Q?.code!=="ARGUMENT")throw Q}if(Z.has(W))return Z.get(W);if(Z.has(q))return Z.get(q);let K=null;if(F.test(W))K=W;else if($.test(q)){K=await T(z,q);if(!K){const Q=Object.keys(b).filter((X)=>/^[a-z]+$/.test(X)).join(", ");throw new B("city",`unknown city '${G}'. pass a numeric cityId, a pinyin slug (e.g. shantou), a Chinese name listed on dianping.com/citylist, or one of: ${Q}`)}}else{const Q=Object.keys(b).filter((X)=>/^[a-z]+$/.test(X)).join(", ");throw new B("city",`unknown city '${G}'. pass a numeric cityId, a pinyin slug (e.g. shantou), a Chinese name listed on dianping.com/citylist, or one of: ${Q}`)}const U=await v(z,K);if(!U)throw new J(`dianping could not resolve cityId for '${G}' (pinyin=${K}); the city page rendered without a /search/keyword/{id}/ link`);Z.set(W,U);Z.set(K,U);if($.test(q))Z.set(q,U);return U}export async function resolveDefaultCityId(z){await z.goto("https://www.dianping.com/");await z.wait(1);const G=await z.evaluate(`(() => {
|
|
2
|
+
const el = document.querySelector('.city-select-text, .city-select-content');
|
|
3
|
+
return el ? (el.textContent || '').trim() : '';
|
|
4
|
+
})()`);if(!G||!$.test(G))return null;return resolveCityIdAsync(z,G)}async function T(z,G){await z.goto("https://www.dianping.com/citylist");const q=await z.evaluate(`(${buildCitylistMap.toString()})()`);if(!q||typeof q!=="object"||Object.keys(q).length===0)throw new J("dianping citylist did not render any city anchors; cannot resolve Chinese city names");if(q&&typeof q==="object"&&q[G])return String(q[G]).toLowerCase();return null}export function buildCitylistMap(){const z={};document.querySelectorAll("a").forEach((q)=>{const W=q.getAttribute("href")||"",K=(q.textContent||"").trim();if(!K||!/^[一-龥]+$/.test(K))return;const U=W.replace(/^https?:/,""),Q=U.match(/^\/\/(?:www\.)?dianping\.com\/([a-z]+)\/?$/)||U.match(/^\/([a-z]+)\/?$/);if(!Q)return;const X=Q[1].toLowerCase();if(X==="citylist"||X==="promo"||X==="events")return;if(!z[K])z[K]=X});return z}async function v(z,G){await z.goto(`https://www.dianping.com/${G}`);const q=await z.evaluate(`(${extractCityIdFromPage.toString()})()`);return Number.isInteger(q)&&q>0?q:null}export function extractCityIdFromPage(){const z=typeof location<"u"&&location.href||"https://www.dianping.com/",G=document.querySelectorAll("a[href]");for(const q of G){const W=q.getAttribute("href")||"";let K;try{K=new URL(W,z)}catch{continue}if(K.protocol!=="https:")continue;if(K.hostname!=="www.dianping.com"&&K.hostname!=="dianping.com")continue;const U=K.pathname.match(/^\/search\/keyword\/(\d+)(?:\/|$)/);if(!U)continue;const Q=Number(U[1]);if(Number.isInteger(Q)&&Q>0)return Q}return null}
|
package/clis/dianping/search.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cli as
|
|
1
|
+
import{cli as L,Strategy as O}from"@jackwener/opencli/registry";import{ArgumentError as W,CommandExecutionError as F}from"@jackwener/opencli/errors";import{SEARCH_COLUMNS as b,detectAuthOrPageFailure as P,parsePrice as j,parseReviewCount as x,requireSearchLimit as f,wrapDianpingStep as $}from"./utils.js";import{resolveCityIdAsync as D,resolveDefaultCityId as A}from"./cityResolver.js";export function extractSearchRows(){const J=document.querySelectorAll("#shop-all-list li");if(J.length===0){const z=document.body;return{ok:!1,sample:(z&&(z.innerText||z.textContent)||"").slice(0,800),url:location.href}}const Q=[];J.forEach((z,Z)=>{if(!(z.querySelector(".tit h4, .review-num, .mean-price, .tag-addr")||z.querySelector('.tit a, a[data-shopid], a[href*="/shop/"]')))return;const K=z.querySelector('.tit a[href*="/shop/"]')||z.querySelector("a[data-shopid]")||z.querySelector('a[href*="/shop/"]'),T=K&&K.getAttribute("data-shopid")||K&&(K.getAttribute("href")||"").match(/\/shop\/([^?#/]+)/)?.[1]||"",G=z.querySelector(".tit h4"),Y=G&&G.textContent&&G.textContent.trim()||K&&K.getAttribute("title")||"",B=z.querySelector(".review-num b"),U=B&&B.textContent&&B.textContent.trim()||"",q=z.querySelector(".mean-price b"),_=q&&q.textContent&&q.textContent.trim()||"",H=Array.from(z.querySelectorAll(".tag-addr .tag")).map((V)=>V.textContent.trim()).filter(Boolean);let M="";const N=z.querySelector(".nebula_star");if(N){const V=N.outerHTML.match(/star_(\d{2})/g);if(V&&V.length)M=V[V.length-1].match(/star_(\d{2})/)[1]}Q.push({rank:Z+1,shop_id:T,name:Y,starClass:M,reviewsRaw:U,priceRaw:_,cuisine:H[0]||"",district:H[1]||"",url:T?"https://www.dianping.com/shop/"+T:""})});return{ok:!0,rows:Q}}L({site:"dianping",name:"search",access:"read",description:"大众点评店铺搜索(按关键词 + 城市)",domain:"www.dianping.com",strategy:O.COOKIE,args:[{name:"keyword",required:!0,positional:!0,help:'搜索关键词,例如 "火锅"'},{name:"city",help:"城市名(北京/上海/汕头/beijing/shantou/...)或 cityId 数字。未在静态表中的城市会通过 dianping.com 在线解析。不传则使用 cookie 默认城市"},{name:"limit",type:"int",default:15,help:"返回的店铺数量(最多 15,dianping 单页固定 15 条)"}],columns:b,func:async(J,Q)=>{const z=String(Q.keyword||"").trim();if(!z)throw new W("keyword","must be a non-empty string");const Z=f(Q.limit);let X=await $("city resolve",()=>D(J,Q.city));if(X==null)X=await $("default city resolve",()=>A(J));if(X==null)throw new F("dianping search 需要城市:未传 --city 且无法确定默认城市。请显式指定,例如 --city 上海");const T=`https://www.dianping.com${`/search/keyword/${X}/0_${encodeURIComponent(z)}`}`;await $(`search "${z}" navigation`,async()=>{await J.goto(T);await J.wait(2)});const G=await $(`search "${z}" extraction`,()=>J.evaluate(`(${extractSearchRows.toString()})()`));if(!G||!G.ok)P({text:String(G?.sample||""),url:String(G?.url||T)},`search "${z}"`,{emptyPatterns:[/没有找到|暂无结果|暂无商户|换个关键词|未找到相关/i]});const Y=(G.rows||[]).slice(0,Z);if(Y.length===0)throw new F("dianping search parser found no result-shaped shop cards");if(Y.some((B)=>!B?.shop_id))throw new F("dianping search parser found result cards without shop_id values");return Y.map((B)=>({rank:B.rank,shop_id:B.shop_id,name:B.name,rating:B.starClass?Number((Number(B.starClass)/10).toFixed(1)):null,reviews:x(B.reviewsRaw),price:j(B.priceRaw),cuisine:B.cuisine,district:B.district,url:B.url}))}});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import{AuthRequiredError as z}from"@jackwener/opencli/errors";import{registerSiteAuthCommands as H}from"../_shared/site-auth.js";const J="https://discord.com/channels/@me";async function K(b){return b.evaluate(`(() => {
|
|
2
|
+
if (/\\/login(\\b|\\/|\\?|$)/.test(location.pathname)) return null;
|
|
3
|
+
const panel = document.querySelector('[class*=panelTitleContainer]');
|
|
4
|
+
if (!panel) return null;
|
|
5
|
+
const display = (panel.textContent || '').trim();
|
|
6
|
+
const handleEl = document.querySelector('[class*=panelSubtextContainer] [class*=hovered]');
|
|
7
|
+
const handle = handleEl ? (handleEl.textContent || '').trim() : '';
|
|
8
|
+
if (!display && !handle) return null;
|
|
9
|
+
return { display_name: display, username: handle || display };
|
|
10
|
+
})()`)}async function B(b){await b.goto(J);for(let F=0;F<8;F++){const G=await K(b);if(G)return G;await b.wait(1)}const x=await b.evaluate("location.href");if(/\/login/.test(String(x||"")))throw new z("discord.com","Discord redirected to /login — not signed in");throw new z("discord.com","Discord account panel not found — not signed in or app failed to load")}H({site:"discord-web",domain:"discord.com",loginUrl:"https://discord.com/login",columns:["username","display_name"],quickCheck:async(b)=>{try{return{logged_in:!0,...await B(b)}}catch(x){if(x instanceof z)return{logged_in:!1};throw x}},verify:B,poll:async(b)=>B(b)});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import{ArgumentError as K,AuthRequiredError as $,CommandExecutionError as P}from"@jackwener/opencli/errors";import{cli as q,Strategy as z}from"@jackwener/opencli/registry";const G=/^(?:@me|\d{3,})\/\d{3,}$/;export function normalizeChannelUrl(b){const f=String(b||"").trim();if(!f)throw new K("Channel is required.",'Pass a channel URL like https://discord.com/channels/<guild>/<channel>, or "<guild>/<channel>".');if(/^https?:\/\//i.test(f)){let J;try{J=new URL(f)}catch{throw new K(`Invalid channel URL: ${f}`)}const M=J.hostname.toLowerCase();if(M!=="discord.com"&&!M.endsWith(".discord.com"))throw new K("Channel URL must be a discord.com URL.");const F=J.pathname.split("/").filter(Boolean);if(F[0]!=="channels"||F.length<3)throw new K("Channel URL must look like /channels/<guild>/<channel>.");return`https://discord.com/channels/${F[1]}/${F[2]}`}if(!G.test(f))throw new K(`Invalid channel reference: ${JSON.stringify(f)}`,'Use "<guild>/<channel>" (numeric ids) or "@me/<dm-channel>", or a full channel URL.');return`https://discord.com/channels/${f}`}q({site:"discord-web",name:"send",access:"write",description:'Send a message to a Discord channel (discord.com). Pass the channel URL or "<guild>/<channel>". Multi-line text is sent as one message.',domain:"discord.com",strategy:z.COOKIE,browser:!0,args:[{name:"channel",type:"string",required:!0,positional:!0,help:'Channel URL (https://discord.com/channels/<guild>/<channel>) or "<guild>/<channel>"'},{name:"text",type:"string",required:!0,positional:!0,help:"Message text (multi-line allowed)"}],columns:["status","channel_id","message"],func:async(b,f)=>{if(!b)throw new P("Browser session required");const J=normalizeChannelUrl(f.channel),M=J.split("/").pop(),F=String(f.text??"");if(!F.trim())throw new K("Message text cannot be empty.");await b.goto(J);let W=!1;for(let B=0;B<12;B++){const Z=await b.evaluate(`(() => {
|
|
2
|
+
if (/\\/login(\\b|\\/|\\?|$)/.test(location.pathname)) return 'login';
|
|
3
|
+
const ed = document.querySelector('[role="textbox"][data-slate-editor="true"]');
|
|
4
|
+
if (!ed) return 'no-editor';
|
|
5
|
+
return 'ready';
|
|
6
|
+
})()`);if(Z==="login")throw new $("discord.com","Discord redirected to /login — not signed in");if(Z==="ready"){W=!0;break}await b.wait(1)}if(!W)throw new P("Discord message box not found — the channel may not exist, or you lack access to it.");const X='[role="textbox"][data-slate-editor="true"]';await b.click(X);await b.pressKey("Control+A");await b.pressKey("Delete");const Q=F.split(`
|
|
7
|
+
`);for(let B=0;B<Q.length;B++){if(B>0)await b.pressKey("Shift+Enter");if(Q[B].length)await b.typeText(X,Q[B])}if(await b.evaluate(`(() => !!document.querySelector('[class*="autocomplete"]'))()`))await b.pressKey("Escape");const V=await b.evaluate(`(() => {
|
|
8
|
+
const ed = document.querySelector('[role="textbox"][data-slate-editor="true"]');
|
|
9
|
+
return ed ? (ed.textContent || '').replace(/\\uFEFF/g, '') : null;
|
|
10
|
+
})()`),Y=(B)=>String(B||"").replace(/\s+/g," ").trim();if(V===null||Y(V)!==Y(F))throw new P(`Composed text did not match before send (got ${JSON.stringify((V||"").slice(0,60))}).`);await b.pressKey("Enter");await b.wait(1);if((await b.evaluate(`(() => {
|
|
11
|
+
const ed = document.querySelector('[role="textbox"][data-slate-editor="true"]');
|
|
12
|
+
return ed ? (ed.textContent || '').replace(/\\uFEFF/g, '') : '';
|
|
13
|
+
})()`)).trim().length>0)throw new P("Message still in the box after Enter — send blocked (slow-mode, rate limit, or no permission to post here).");return[{status:"success",channel_id:M,message:"Message sent"}]}});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import{ArgumentError as H,AuthRequiredError as Z,CommandExecutionError as J}from"@jackwener/opencli/errors";import{cli as _,Strategy as $}from"@jackwener/opencli/registry";const Y='[data-lexical-editor="true"][contenteditable="true"]',K='[data-pp-fb-composer="1"]';_({site:"facebook",name:"comment",access:"write",description:"Post a comment on a Facebook post. Pass the post URL and the comment text (multi-line allowed). The comment is published from your logged-in account — write a genuine, on-topic remark, not filler.",domain:"facebook.com",strategy:$.COOKIE,browser:!0,args:[{name:"url",type:"string",required:!0,positional:!0,help:"Post URL (e.g. https://www.facebook.com/<user>/posts/<id> or a permalink)"},{name:"text",type:"string",required:!0,positional:!0,help:"Comment text (multi-line allowed)"}],columns:["status","url","message"],func:async(v,U)=>{if(!v)throw new J("Browser session required");const B=String(U.url??"").trim();if(!B)throw new H("Post URL is required.");let L;try{L=new URL(B)}catch{throw new H(`Invalid post URL: ${B}`)}if(!/facebook\.com$/.test(L.hostname)&&!L.hostname.endsWith(".facebook.com"))throw new H("URL must be a facebook.com post URL.");const G=String(U.text??"");if(!G.trim())throw new H("Comment text cannot be empty.");await v.goto(B,{waitUntil:"none"});let V=!1;for(let z=0;z<15;z++){const X=await v.evaluate(`(() => {
|
|
2
|
+
const path = location.pathname || '';
|
|
3
|
+
if (/^\\/(login|checkpoint)(\\/|$|\\.php)/.test(path)) return 'login';
|
|
4
|
+
const dialog = document.querySelector('[role="dialog"]');
|
|
5
|
+
const scope = dialog || document;
|
|
6
|
+
const ed = scope.querySelector('${Y}') || document.querySelector('${Y}');
|
|
7
|
+
if (!ed) return 'wait';
|
|
8
|
+
ed.setAttribute('data-pp-fb-composer', '1');
|
|
9
|
+
ed.scrollIntoView({ block: 'center' });
|
|
10
|
+
return 'ready';
|
|
11
|
+
})()`);if(X==="login")throw new Z("www.facebook.com","Facebook redirected to login/checkpoint — not signed in");if(X==="ready"){V=!0;break}await v.wait(1)}if(!V)throw new J("Comment box not found — the post may be unavailable, or commenting is disabled.");await v.click(K);await v.wait(1);await v.pressKey("Control+A");await v.pressKey("Delete");const N=G.split(`
|
|
12
|
+
`);for(let z=0;z<N.length;z++){if(z>0)await v.pressKey("Shift+Enter");if(N[z].length)await v.typeText(K,N[z])}if(await v.evaluate(`(() => !!document.querySelector('[role="listbox"][aria-label], ul[role="listbox"]'))()`))await v.pressKey("Escape");const F=(z)=>String(z||"").replace(/\s+/g," ").trim(),Q=await v.evaluate(`(() => {
|
|
13
|
+
const ed = document.querySelector('${K}');
|
|
14
|
+
return ed ? (ed.textContent || '') : null;
|
|
15
|
+
})()`);if(Q===null||F(Q)!==F(G))throw new J(`Composed text did not match before send (got ${JSON.stringify((Q||"").slice(0,60))}).`);await v.pressKey("Enter");await v.wait(2);const W=await v.evaluate(`(() => {
|
|
16
|
+
const ed = document.querySelector('${K}');
|
|
17
|
+
return ed ? (ed.textContent || '') : '';
|
|
18
|
+
})()`);if(F(W).length>0&&F(W)===F(G))throw new J("Comment still in the box after Enter — submit blocked (rate limit, restricted post, or no permission to comment).");return[{status:"success",url:B,message:"Comment posted."}]}});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import{ArgumentError as Z,AuthRequiredError as y,CommandExecutionError as V}from"@jackwener/opencli/errors";import{cli as N,Strategy as R}from"@jackwener/opencli/registry";const $="data-pp-fb-like";function U(){return`(() => {
|
|
2
|
+
const path = location.pathname || '';
|
|
3
|
+
if (/^\\/(login|checkpoint)(\\/|$|\\.php)/.test(path)) return { auth: true, detail: 'on login/checkpoint page' };
|
|
4
|
+
// A permalink often renders as a modal dialog over the feed. Scope to the
|
|
5
|
+
// dialog so we don't grab a background feed post's Like button.
|
|
6
|
+
const dialog = document.querySelector('[role="dialog"]');
|
|
7
|
+
const art = (dialog && dialog.querySelector('div[role="article"]'))
|
|
8
|
+
|| dialog
|
|
9
|
+
|| document.querySelector('div[role="article"]');
|
|
10
|
+
if (!art) return { notfound: true, detail: 'no post found on page' };
|
|
11
|
+
// Action buttons = focusable role=button carrying a css-img icon, not part
|
|
12
|
+
// of the reaction-count toolbar. The first one is Like.
|
|
13
|
+
const btns = [...art.querySelectorAll('[role="button"][tabindex="0"]')]
|
|
14
|
+
.filter(b => !b.closest('[role="toolbar"]') && b.querySelector('i[data-visualcompletion="css-img"]'));
|
|
15
|
+
const like = btns[0];
|
|
16
|
+
if (!like) return { notfound: true, detail: 'like button not located' };
|
|
17
|
+
like.setAttribute(${JSON.stringify($)}, '1');
|
|
18
|
+
like.scrollIntoView({ block: 'center' });
|
|
19
|
+
const icon = like.querySelector('i[data-visualcompletion="css-img"]');
|
|
20
|
+
const y = parseFloat((getComputedStyle(icon).backgroundPosition.split(' ')[1] || '0').replace('px','')) || 0;
|
|
21
|
+
return { y, label: like.getAttribute('aria-label') || '' };
|
|
22
|
+
})()`}function G(){return`(() => {
|
|
23
|
+
const like = document.querySelector('[${$}="1"]');
|
|
24
|
+
if (!like) return null;
|
|
25
|
+
const icon = like.querySelector('i[data-visualcompletion="css-img"]');
|
|
26
|
+
if (!icon) return null;
|
|
27
|
+
const y = parseFloat((getComputedStyle(icon).backgroundPosition.split(' ')[1] || '0').replace('px','')) || 0;
|
|
28
|
+
return { y, label: like.getAttribute('aria-label') || '' };
|
|
29
|
+
})()`}async function v(z,O,H){for(let J=0;J<H;J++){await z.click(`[${$}="1"]`);await z.wait(1);const M=await z.evaluate(G());if(M&&M.y!==O)return M}return null}N({site:"facebook",name:"like",access:"write",description:"Like a Facebook post (ends in the liked state; no-op if already liked). Pass the post URL. Reacts as a genuine like from your logged-in account — use it to engage with content you actually mean to endorse.",domain:"facebook.com",strategy:R.COOKIE,browser:!0,args:[{name:"url",type:"string",required:!0,positional:!0,help:"Post URL (e.g. https://www.facebook.com/<user>/posts/<id> or a permalink)"},{name:"remove",type:"bool",default:!1,help:"Remove your like instead (end in the un-liked state)"}],columns:["status","action","url","message"],func:async(z,O)=>{if(!z)throw new V("Browser session required");const H=String(O.url??"").trim();if(!H)throw new Z("Post URL is required.");let J;try{J=new URL(H)}catch{throw new Z(`Invalid post URL: ${H}`)}if(!/facebook\.com$/.test(J.hostname)&&!J.hostname.endsWith(".facebook.com"))throw new Z("URL must be a facebook.com post URL.");const M=O.remove===!0;await z.goto(H,{waitUntil:"none"});let B=null;for(let X=0;X<15;X++){await z.wait(1);B=await z.evaluate(U());if(B?.auth)throw new y("www.facebook.com",`Facebook ${B.detail} — not signed in`);if(typeof B?.y==="number")break}if(typeof B?.y!=="number")throw new V(`Could not act on this post: ${B?.detail||"like button never appeared"}.`);const P=await v(z,B.y,5);if(!P)throw new V("Like did not register — Facebook ignored the click (post may be unavailable or reactions disabled).");const q=Math.max(B.y,P.y),D=P.y===q,j=B.y===q,W=!M;let Q;if(D===W)Q=W?j?"already-liked":"liked":"unliked";else{if(!await v(z,P.y,3))throw new V("Could not settle the reaction to the desired state (second toggle ignored).");Q=W?"already-liked":"already-unliked"}return[{status:"success",action:Q,url:H,message:{liked:"Post liked.","already-liked":"Post was already liked (left liked).",unliked:"Like removed.","already-unliked":"Post was not liked (left un-liked)."}[Q]||"Reaction updated."}]}});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import{AuthRequiredError as n,CommandExecutionError as c}from"@jackwener/opencli/errors";import{registerSiteAuthCommands as i}from"../_shared/site-auth.js";const t="https://news.ycombinator.com";async function r(e){return(await e.getCookies({url:t})).some((a)=>a.name==="user"&&a.value)}async function s(e){if(!await r(e))throw new n("news.ycombinator.com","Hacker News `user` cookie missing — not signed in");await e.goto(`${t}/news`);await e.wait(1);const o=await e.evaluate(`(() => {
|
|
2
|
+
// The top-right nav shows "<username> (karma) | logout" only when signed in.
|
|
3
|
+
const userLink = document.querySelector('span.pagetop a[href^="user?id="]');
|
|
4
|
+
const logout = document.querySelector('a[href^="logout"]');
|
|
5
|
+
if (!userLink || !logout) return { anon: true };
|
|
6
|
+
const username = (userLink.textContent || '').trim();
|
|
7
|
+
const karmaMatch = (document.querySelector('span.pagetop')?.textContent || '').match(/\\((\\d+)\\)/);
|
|
8
|
+
return { username, karma: karmaMatch ? karmaMatch[1] : '' };
|
|
9
|
+
})()`);if(o?.anon||!o?.username)throw new n("news.ycombinator.com","Hacker News shows no signed-in user — cookie stale");return{username:o.username,karma:o.karma||""}}i({site:"hackernews",domain:"news.ycombinator.com",loginUrl:`${t}/login`,columns:["username","karma"],quickCheck:r,verify:s,poll:async(e)=>{if(!await r(e))throw new n("news.ycombinator.com","Waiting for Hacker News `user` cookie");return s(e)}});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import{ArgumentError as Q,AuthRequiredError as T,CommandExecutionError as L}from"@jackwener/opencli/errors";import{cli as U,Strategy as V}from"@jackwener/opencli/registry";const M="https://news.ycombinator.com";export function normalizeItemId(F){const J=String(F||"").trim();if(!J)throw new Q("Item id is required.","Pass an HN item id (story or comment), e.g. 48811591.");if(/^\d+$/.test(J))return J;const B=J.match(/[?&]id=(\d+)/);if(B)return B[1];throw new Q(`Invalid HN item reference: ${JSON.stringify(J)}`,"Pass a numeric id or an item?id=… URL.")}U({site:"hackernews",name:"comment",access:"write",description:"Post a comment on a Hacker News item. Pass the parent id (a story to comment on it, or a comment to reply). Contribute a substantive, on-topic comment — HN flags low-effort or off-topic replies.",domain:"news.ycombinator.com",strategy:V.COOKIE,browser:!0,args:[{name:"parent",type:"string",required:!0,positional:!0,help:"Parent item id (story or comment) to reply to"},{name:"text",type:"string",required:!0,positional:!0,help:"Comment text (HN markup: blank line = paragraph)"}],columns:["status","id","url","parent","message"],func:async(F,J)=>{if(!F)throw new L("Browser session required");const B=normalizeItemId(J.parent),P=String(J.text??"");if(!P.trim())throw new Q("Comment text cannot be empty.");await F.goto(`${M}/item?id=${B}`);await F.wait(1);const D=await F.evaluate(`(async () => {
|
|
2
|
+
try {
|
|
3
|
+
if (/\\/login/.test(location.pathname)) return { kind: 'auth', detail: 'redirected to /login' };
|
|
4
|
+
// The reply form posts to "comment" (relative). Match it by the hidden
|
|
5
|
+
// fields it carries rather than the action attribute, which HN writes
|
|
6
|
+
// relative ("comment") so /comment suffix selectors miss it.
|
|
7
|
+
const forms = [...document.querySelectorAll('form')].filter(f => f.querySelector('input[name="hmac"]') && f.querySelector('input[name="parent"]'));
|
|
8
|
+
const form = forms.find(f => (f.querySelector('input[name="parent"]')?.value) === ${JSON.stringify(B)}) || forms[0];
|
|
9
|
+
if (!form) return { kind: 'notfound', detail: 'No comment form on this item — it may be closed, deleted, or you are signed out.' };
|
|
10
|
+
const hmac = form.querySelector('input[name="hmac"]')?.value;
|
|
11
|
+
const goto = form.querySelector('input[name="goto"]')?.value || ('item?id=' + ${JSON.stringify(B)});
|
|
12
|
+
const parentVal = form.querySelector('input[name="parent"]')?.value || ${JSON.stringify(B)};
|
|
13
|
+
if (!hmac) return { kind: 'auth', detail: 'comment hmac token missing — likely signed out' };
|
|
14
|
+
|
|
15
|
+
const p = new URLSearchParams();
|
|
16
|
+
p.set('parent', parentVal);
|
|
17
|
+
p.set('goto', goto);
|
|
18
|
+
p.set('hmac', hmac);
|
|
19
|
+
p.set('text', ${JSON.stringify(P)});
|
|
20
|
+
|
|
21
|
+
const res = await fetch('/comment', {
|
|
22
|
+
method: 'POST',
|
|
23
|
+
credentials: 'include',
|
|
24
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
25
|
+
body: p.toString(),
|
|
26
|
+
redirect: 'manual',
|
|
27
|
+
});
|
|
28
|
+
if (res.type === 'opaqueredirect' || res.status === 302 || res.status === 0) {
|
|
29
|
+
return { ok: true };
|
|
30
|
+
}
|
|
31
|
+
const body = await res.text();
|
|
32
|
+
let msg = '';
|
|
33
|
+
if (/Please try again/i.test(body)) msg = 'HN asked to try again (possible rate limit or expired token).';
|
|
34
|
+
else if (/You're posting too fast|too fast/i.test(body)) msg = 'Posting too fast — HN rate limited the comment.';
|
|
35
|
+
else {
|
|
36
|
+
const td = body.match(/<td[^>]*>\\s*([^<][^<]{0,200}?)\\s*<\\/td>/i);
|
|
37
|
+
if (td) msg = td[1].replace(/\\s+/g, ' ').trim();
|
|
38
|
+
}
|
|
39
|
+
return { kind: 'reject', detail: msg || ('HTTP ' + res.status + ' without redirect'), status: res.status };
|
|
40
|
+
} catch (e) {
|
|
41
|
+
return { kind: 'exception', detail: String((e && e.message) || e) };
|
|
42
|
+
}
|
|
43
|
+
})()`);if(D?.kind==="auth")throw new T("news.ycombinator.com",D.detail);if(D?.kind==="notfound")throw new L(D.detail);if(D?.kind==="exception")throw new L(`HN comment failed: ${D.detail}`);if(D?.kind==="reject")return[{status:"failed",id:"",url:"",parent:B,message:D.detail}];if(!D?.ok)throw new L(`Unexpected HN comment result: ${JSON.stringify(D)}`);await F.goto(`${M}/item?id=${B}`);await F.wait(1);const K=(await F.evaluate(`(() => {
|
|
44
|
+
try {
|
|
45
|
+
let username = '';
|
|
46
|
+
const navLink = document.querySelector('span.pagetop a[href^="user?id="]');
|
|
47
|
+
if (navLink) username = (navLink.getAttribute('href') || '').replace('user?id=', '');
|
|
48
|
+
const want = ${JSON.stringify(P)}.replace(/\\s+/g, ' ').trim();
|
|
49
|
+
const rows = [...document.querySelectorAll('tr.athing.comtr')];
|
|
50
|
+
// Newest first: our fresh comment is usually near the top of its thread.
|
|
51
|
+
for (const row of rows) {
|
|
52
|
+
const by = (row.querySelector('a.hnuser')?.textContent || '').trim();
|
|
53
|
+
if (username && by !== username) continue;
|
|
54
|
+
const commentText = (row.querySelector('.commtext')?.textContent || '').replace(/\\s+/g, ' ').trim();
|
|
55
|
+
if (commentText.startsWith(want.slice(0, 60))) {
|
|
56
|
+
return { id: row.id };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
} catch { return null; }
|
|
61
|
+
})()`))?.id||"";return[{status:"success",id:K,url:K?`${M}/item?id=${K}`:`${M}/item?id=${B}`,parent:B,message:K?"Comment posted":"Comment posted (id not confirmed on reload)"}]}});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import{ArgumentError as F,AuthRequiredError as Q,CommandExecutionError as L}from"@jackwener/opencli/errors";import{cli as T,Strategy as V}from"@jackwener/opencli/registry";const M="https://news.ycombinator.com";T({site:"hackernews",name:"submit",access:"write",description:"Submit a story to Hacker News. Link post by default (--url), or an Ask/text post with --text. HN gates submissions on account age/karma and dedupes URLs — rejections are surfaced verbatim. Post genuinely worthwhile content; low-effort submissions hurt the account.",domain:"news.ycombinator.com",strategy:V.COOKIE,browser:!0,args:[{name:"title",type:"string",required:!0,positional:!0,help:"Story title (max 80 chars)"},{name:"url",type:"string",help:"External URL for a link post (mutually exclusive with --text)"},{name:"text",type:"string",help:"Body text for an Ask HN / text post (mutually exclusive with --url)"}],columns:["status","id","url","title","message"],func:async(v,D)=>{if(!v)throw new L("Browser session required");const z=String(D.title??"").trim();if(!z)throw new F("Title is required.");if(z.length>80)throw new F("HN titles must be at most 80 characters.");const B=typeof D.url==="string"?D.url.trim():"",K=typeof D.text==="string"?D.text:"";if(B&&K)throw new F("Pass either --url (link post) or --text (text post), not both.");if(!B&&!K)throw new F("A story needs either --url or --text.","Give a link to submit, or --text for an Ask HN post.");if(B)try{new URL(B)}catch{throw new F(`Invalid --url: ${B}`)}await v.goto(`${M}/submit`);await v.wait(1);const f=await v.evaluate(`(async () => {
|
|
2
|
+
try {
|
|
3
|
+
// The submit form only renders for a signed-in user; if HN bounced us
|
|
4
|
+
// to /login the form (and its fnid) will be absent.
|
|
5
|
+
if (/\\/login/.test(location.pathname)) return { kind: 'auth', detail: 'redirected to /login' };
|
|
6
|
+
const form = document.querySelector('form[action$="/r"], form[action="/r"], form[action="https://news.ycombinator.com/r"]')
|
|
7
|
+
|| [...document.querySelectorAll('form')].find(f => f.querySelector('input[name="fnid"]'));
|
|
8
|
+
if (!form) return { kind: 'auth', detail: 'submit form not found — not signed in' };
|
|
9
|
+
const fnid = form.querySelector('input[name="fnid"]')?.value;
|
|
10
|
+
if (!fnid) return { kind: 'auth', detail: 'fnid token missing' };
|
|
11
|
+
|
|
12
|
+
const p = new URLSearchParams();
|
|
13
|
+
p.set('fnid', fnid);
|
|
14
|
+
p.set('fnop', 'submit-page');
|
|
15
|
+
p.set('title', ${JSON.stringify(z)});
|
|
16
|
+
p.set('url', ${JSON.stringify(B)});
|
|
17
|
+
p.set('text', ${JSON.stringify(K)});
|
|
18
|
+
|
|
19
|
+
const res = await fetch('/r', {
|
|
20
|
+
method: 'POST',
|
|
21
|
+
credentials: 'include',
|
|
22
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
23
|
+
body: p.toString(),
|
|
24
|
+
redirect: 'manual',
|
|
25
|
+
});
|
|
26
|
+
// A same-origin 302 comes back as an opaque redirect (status 0) — that
|
|
27
|
+
// is the success path. A 200 means HN re-rendered the form with an error.
|
|
28
|
+
if (res.type === 'opaqueredirect' || res.status === 302 || res.status === 0) {
|
|
29
|
+
return { ok: true };
|
|
30
|
+
}
|
|
31
|
+
const body = await res.text();
|
|
32
|
+
// Pull the first human-readable error HN prints above the form.
|
|
33
|
+
let msg = '';
|
|
34
|
+
const td = body.match(/<td[^>]*>\\s*([^<][^<]{0,200}?)\\s*<\\/td>/i);
|
|
35
|
+
if (/Please try again/i.test(body)) msg = 'HN asked to try again (possible rate limit or expired token).';
|
|
36
|
+
else if (/already been submitted/i.test(body)) msg = 'That URL has already been submitted.';
|
|
37
|
+
else if (td) msg = td[1].replace(/\\s+/g, ' ').trim();
|
|
38
|
+
return { kind: 'reject', detail: msg || ('HTTP ' + res.status + ' without redirect'), status: res.status };
|
|
39
|
+
} catch (e) {
|
|
40
|
+
return { kind: 'exception', detail: String((e && e.message) || e) };
|
|
41
|
+
}
|
|
42
|
+
})()`);if(f?.kind==="auth")throw new Q("news.ycombinator.com",f.detail);if(f?.kind==="exception")throw new L(`HN submit failed: ${f.detail}`);if(f?.kind==="reject")return[{status:"failed",id:"",url:"",title:z,message:f.detail}];if(!f?.ok)throw new L(`Unexpected HN submit result: ${JSON.stringify(f)}`);await v.wait(1);const J=(await v.evaluate(`(async () => {
|
|
43
|
+
try {
|
|
44
|
+
const uc = document.cookie.match(/(?:^|; )user=([^;]+)/);
|
|
45
|
+
// Cookie value is "<username>&<hash>"; the nav link is more reliable.
|
|
46
|
+
let username = '';
|
|
47
|
+
const navLink = document.querySelector('span.pagetop a[href^="user?id="]');
|
|
48
|
+
if (navLink) username = (navLink.getAttribute('href') || '').replace('user?id=', '');
|
|
49
|
+
if (!username && uc) username = decodeURIComponent(uc[1]).split('&')[0];
|
|
50
|
+
if (!username) return null;
|
|
51
|
+
const res = await fetch('/submitted?id=' + encodeURIComponent(username), { credentials: 'include' });
|
|
52
|
+
const html = await res.text();
|
|
53
|
+
const doc = new DOMParser().parseFromString(html, 'text/html');
|
|
54
|
+
const rows = [...doc.querySelectorAll('tr.athing')];
|
|
55
|
+
const want = ${JSON.stringify(z)}.replace(/\\s+/g, ' ').trim();
|
|
56
|
+
for (const row of rows.slice(0, 5)) {
|
|
57
|
+
const link = row.querySelector('.titleline a, a.storylink, .title a');
|
|
58
|
+
const t = (link?.textContent || '').replace(/\\s+/g, ' ').trim();
|
|
59
|
+
if (t === want) {
|
|
60
|
+
return { id: row.id, href: link?.getAttribute('href') || '' };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { id: rows[0]?.id || '', href: '' };
|
|
64
|
+
} catch { return null; }
|
|
65
|
+
})()`))?.id||"",P=J?`${M}/item?id=${J}`:"";return[{status:"success",id:J,url:P,title:z,message:J?"Story submitted":"Story submitted (item id not confirmed; check /newest)"}]}});
|
package/clis/jimeng/auth.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{AuthRequiredError as
|
|
1
|
+
import{AuthRequiredError as L,CommandExecutionError as F}from"@jackwener/opencli/errors";import{registerSiteAuthCommands as T}from"../_shared/site-auth.js";const Q=`(async () => {
|
|
2
2
|
try {
|
|
3
3
|
const r = await fetch('/passport/account/info/v2/?aid=513695', { credentials: 'include', headers: { Accept: 'application/json' } });
|
|
4
4
|
if (r.status === 401 || r.status === 403) return { kind: 'auth', detail: 'Jimeng passport HTTP ' + r.status };
|
|
@@ -10,4 +10,4 @@ import{AuthRequiredError as G,CommandExecutionError as F}from"@jackwener/opencli
|
|
|
10
10
|
} catch (e) {
|
|
11
11
|
return { kind: 'exception', detail: String(e && e.message || e) };
|
|
12
12
|
}
|
|
13
|
-
})()
|
|
13
|
+
})()`,U=new Set(["sessionid","sessionid_ss","sid_tt"]);async function V(D){return{logged_in:(await D.getCookies({url:"https://jimeng.jianying.com"})).some((G)=>G.value&&U.has(G.name))}}async function X(D){await D.goto("https://jimeng.jianying.com/ai-tool/generate?type=image&workspace=0");await D.wait(2);const z=await D.evaluate(Q);if(z?.kind==="auth")throw new L("jimeng.jianying.com",z.detail);if(z?.kind==="http")throw new F(`HTTP ${z.httpStatus} from Jimeng passport`);if(z?.kind==="exception")throw new F(`Jimeng whoami failed: ${z.detail}`);if(!z?.ok)throw new F(`Unexpected Jimeng probe: ${JSON.stringify(z)}`);return{user_id:z.user_id,screen_name:z.screen_name}}T({site:"jimeng",domain:"jimeng.jianying.com",loginUrl:"https://jimeng.jianying.com/",columns:["user_id","screen_name"],verify:X,quickCheck:V,poll:async(D)=>{const z=await D.evaluate(Q);if(!z?.ok)throw new L("jimeng.jianying.com","Waiting for Jimeng login");return{user_id:z.user_id,screen_name:z.screen_name}}});
|
package/clis/jimeng/generate.js
CHANGED
|
@@ -1,53 +1,60 @@
|
|
|
1
|
-
import{cli as
|
|
2
|
-
const prompt =
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
import{cli as h,Strategy as G}from"@jackwener/opencli/registry";import{CommandExecutionError as F}from"@jackwener/opencli/errors";const M="https://jimeng.jianying.com/ai-tool/generate?type=image&workspace=0",V="/mweb/v1/get_history?aid=513695&device_platform=web®ion=cn&da_version=3.3.11&web_version=7.5.0&aigc_features=app_lip_sync",W=JSON.stringify({cursor:"",count:5,need_page_item:!0,need_aigc_data:!0,aigc_mode_list:["workbench"]}),N=(q)=>`(async () => {
|
|
2
|
+
const prompt = ${JSON.stringify(q)};
|
|
3
|
+
let existing = [];
|
|
4
|
+
try {
|
|
5
|
+
const r = await fetch(${JSON.stringify(V)}, {
|
|
6
|
+
method: 'POST', credentials: 'include',
|
|
7
|
+
headers: { 'Content-Type': 'application/json' },
|
|
8
|
+
body: ${JSON.stringify(W)},
|
|
9
|
+
});
|
|
10
|
+
const d = await r.json();
|
|
11
|
+
existing = (d && d.data && d.data.records_list || []).map((x) => x.submit_id).filter(Boolean);
|
|
12
|
+
} catch (e) { /* 基线拿不到就当空,轮询取最新一条 */ }
|
|
13
|
+
|
|
14
|
+
const editor = document.querySelector('[contenteditable="true"]');
|
|
15
|
+
if (!editor) return { ok: false, error: 'editor_not_found' };
|
|
13
16
|
editor.focus();
|
|
14
|
-
await new Promise(r => setTimeout(r, 200));
|
|
17
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
15
18
|
document.execCommand('selectAll');
|
|
16
|
-
await new Promise(r => setTimeout(r,
|
|
19
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
17
20
|
document.execCommand('delete');
|
|
18
|
-
await new Promise(r => setTimeout(r,
|
|
21
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
19
22
|
document.execCommand('insertText', false, prompt);
|
|
20
|
-
await new Promise(r => setTimeout(r,
|
|
21
|
-
|
|
22
|
-
// Step 3: Click generate
|
|
23
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
24
|
+
|
|
23
25
|
const btn = document.querySelector('.lv-btn.lv-btn-primary[class*="circle"]');
|
|
24
|
-
if (!btn) return
|
|
26
|
+
if (!btn) return { ok: false, error: 'button_not_found' };
|
|
27
|
+
if (btn.disabled || btn.getAttribute('aria-disabled') === 'true') return { ok: false, error: 'button_disabled' };
|
|
25
28
|
btn.click();
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
return { ok: true, existing };
|
|
30
|
+
})()`,j=(q)=>`(async () => {
|
|
31
|
+
const existing = ${JSON.stringify(q)};
|
|
32
|
+
const r = await fetch(${JSON.stringify(V)}, {
|
|
33
|
+
method: 'POST', credentials: 'include',
|
|
34
|
+
headers: { 'Content-Type': 'application/json' },
|
|
35
|
+
body: ${JSON.stringify(W)},
|
|
36
|
+
});
|
|
37
|
+
const d = await r.json();
|
|
38
|
+
const recs = (d && d.data && d.data.records_list) || [];
|
|
39
|
+
const fresh = recs.find((x) => x.submit_id && existing.indexOf(x.submit_id) === -1);
|
|
40
|
+
if (!fresh) return { state: 'pending' };
|
|
41
|
+
|
|
42
|
+
const failMsg = fresh.fail_starling_message || '';
|
|
43
|
+
const items = fresh.item_list || [];
|
|
44
|
+
const done = items
|
|
45
|
+
.map((it) => it && it.image && it.image.large_images && it.image.large_images[0] && it.image.large_images[0].image_url)
|
|
46
|
+
.filter(Boolean);
|
|
47
|
+
const total = fresh.total_image_count || 0;
|
|
48
|
+
|
|
49
|
+
if (done.length && (!total || done.length >= total)) {
|
|
50
|
+
const p = items[0] && items[0].aigc_image_params && items[0].aigc_image_params.text2image_params;
|
|
51
|
+
return {
|
|
52
|
+
state: 'success',
|
|
53
|
+
urls: done,
|
|
54
|
+
model: (fresh.model_info && fresh.model_info.model_name) || (p && p.model_config && p.model_config.model_name) || '',
|
|
55
|
+
};
|
|
37
56
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
// Step 5: Extract image URLs (use thumbnail URLs which are accessible)
|
|
44
|
-
const urls = newImgs.map(img => img.src);
|
|
45
|
-
|
|
46
|
-
return [{
|
|
47
|
-
status: 'success',
|
|
48
|
-
prompt: prompt.substring(0, 80),
|
|
49
|
-
image_count: urls.length,
|
|
50
|
-
image_urls: urls.join('\\n')
|
|
51
|
-
}];
|
|
52
|
-
})()
|
|
53
|
-
`},{map:{status:"${{ item.status }}",prompt:"${{ item.prompt }}",image_count:"${{ item.image_count }}",image_urls:"${{ item.image_urls }}"}}]});
|
|
57
|
+
if (!done.length && failMsg) return { state: 'failed', error: failMsg };
|
|
58
|
+
return { state: 'pending', got: done.length, total };
|
|
59
|
+
})()`;h({site:"jimeng",name:"generate",access:"write",description:"即梦AI 文生图 — 输入 prompt 生成图片",domain:"jimeng.jianying.com",strategy:G.COOKIE,browser:!0,navigateBefore:!1,siteSession:"persistent",args:[{name:"prompt",type:"string",required:!0,positional:!0,help:"图片描述 prompt"},{name:"wait",type:"int",default:60,help:"等待生成完成的最长秒数(每 3 秒轮询一次,超时会返回 pending 并提示用 history 回查)"}],columns:["status","prompt","model","image_count","image_urls"],func:async(q,K)=>{const z=String(K.prompt??"").trim();if(!z)throw new F("prompt is required");const Q=Number(K.wait??60);await q.goto(M);await q.wait(3);const v=await q.evaluate(N(z));if(!v?.ok){const $={editor_not_found:"找不到 prompt 输入框(页面结构可能已变)",button_not_found:"找不到生成按钮(页面结构可能已变)",button_disabled:"生成按钮不可点击(可能未登录、额度用尽或 prompt 为空)"}[v?.error]||`提交失败:${v?.error||"unknown"}`;throw new F($)}const X=Array.isArray(v.existing)?v.existing:[],Z=Date.now()+Q*1000;let f=null;while(Date.now()<Z){await q.wait(3);f=await q.evaluate(j(X));if(f?.state==="success")return[{status:"success",prompt:z.slice(0,80),model:f.model||"",image_count:f.urls.length,image_urls:f.urls.join(`
|
|
60
|
+
`)}];if(f?.state==="failed")throw new F(`即梦生成失败:${f.error}`)}return[{status:"pending",prompt:z.slice(0,80),model:f&&f.model||"",image_count:f&&f.got||0,image_urls:`生成仍在进行(已 ${Q}s);稍后用 \`opencli jimeng history\` 回查结果`}]}});
|
package/clis/jimeng/history.js
CHANGED
|
@@ -1,23 +1,32 @@
|
|
|
1
1
|
import{cli as e,Strategy as t}from"@jackwener/opencli/registry";e({site:"jimeng",name:"history",access:"read",description:"即梦AI 查看最近生成的作品",domain:"jimeng.jianying.com",strategy:t.COOKIE,browser:!0,args:[{name:"limit",type:"int",default:5}],columns:["prompt","model","status","image_url","created_at"],pipeline:[{navigate:"https://jimeng.jianying.com/ai-tool/generate?type=image&workspace=0"},{evaluate:`(async () => {
|
|
2
2
|
const limit = \${{ args.limit }};
|
|
3
|
+
// get_history 的响应结构已从 history_list 改版为 records_list:每条 record 是一次
|
|
4
|
+
// 生成任务,其 item_list 才是单张图片;prompt/model/image 仍在 item 层的老字段里
|
|
5
|
+
// (aigc_image_params / image.large_images / common_attr)。完成态以「拿到 large_images
|
|
6
|
+
// 的图片 URL」判定(状态码 102→144/50 已漂移,别再按魔法数字判),出图即完成。
|
|
3
7
|
const res = await fetch('/mweb/v1/get_history?aid=513695&device_platform=web®ion=cn&da_version=3.3.11&web_version=7.5.0&aigc_features=app_lip_sync', {
|
|
4
8
|
method: 'POST',
|
|
5
9
|
credentials: 'include',
|
|
6
10
|
headers: { 'Content-Type': 'application/json' },
|
|
7
|
-
body: JSON.stringify({ cursor: '', count: limit, need_page_item: true, need_aigc_data: true, aigc_mode_list: ['workbench'] })
|
|
11
|
+
body: JSON.stringify({ cursor: '', count: Math.max(limit, 10), need_page_item: true, need_aigc_data: true, aigc_mode_list: ['workbench'] })
|
|
8
12
|
});
|
|
9
13
|
const data = await res.json();
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
const records = data?.data?.records_list || [];
|
|
15
|
+
const rows = [];
|
|
16
|
+
for (const rec of records) {
|
|
17
|
+
for (const item of (rec.item_list || [])) {
|
|
18
|
+
const params = item.aigc_image_params?.text2image_params || {};
|
|
19
|
+
const imageUrl = item.image?.large_images?.[0]?.image_url || item.common_attr?.cover_url || '';
|
|
20
|
+
rows.push({
|
|
21
|
+
prompt: params.prompt || item.common_attr?.title || rec.fail_starling_message || 'N/A',
|
|
22
|
+
model: (rec.model_info?.model_name) || params.model_config?.model_name || 'unknown',
|
|
23
|
+
status: imageUrl ? 'completed' : 'pending',
|
|
24
|
+
image_url: imageUrl,
|
|
25
|
+
created_at: new Date((item.common_attr?.create_time || rec.finish_time || 0) * 1000).toLocaleString('zh-CN'),
|
|
26
|
+
});
|
|
27
|
+
if (rows.length >= limit) return rows;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return rows;
|
|
22
31
|
})()
|
|
23
32
|
`},{map:{prompt:"${{ item.prompt }}",model:"${{ item.model }}",status:"${{ item.status }}",image_url:"${{ item.image_url }}",created_at:"${{ item.created_at }}"}},{limit:"${{ args.limit }}"}]});
|
package/clis/pixiv/auth.js
CHANGED
|
@@ -1,25 +1,16 @@
|
|
|
1
1
|
import{AuthRequiredError as D,CommandExecutionError as F}from"@jackwener/opencli/errors";import{registerSiteAuthCommands as K}from"../_shared/site-auth.js";async function G(B){return(await B.getCookies({url:"https://www.pixiv.net"})).some((H)=>H.name==="PHPSESSID"&&/^\d+_/.test(H.value||""))}async function J(B){if(!await G(B))throw new D("pixiv.net","Pixiv PHPSESSID cookie missing");await B.goto("https://www.pixiv.net/");await B.wait(2);const z=await B.evaluate(`(async () => {
|
|
2
2
|
try {
|
|
3
|
-
const
|
|
4
|
-
let userData = null;
|
|
5
|
-
if (meta) { try { userData = JSON.parse(meta); } catch {} }
|
|
6
|
-
const u = userData?.userData;
|
|
7
|
-
if (u?.id) {
|
|
8
|
-
return { ok: true, user_id: String(u.id), name: String(u.name || u.account || '') };
|
|
9
|
-
}
|
|
10
|
-
const r = await fetch('/ajax/user/extra', { credentials: 'include', headers: { Accept: 'application/json' } });
|
|
3
|
+
const r = await fetch('/touch/ajax/user/self/status', { credentials: 'include', headers: { Accept: 'application/json' } });
|
|
11
4
|
if (r.status === 401 || r.status === 403) {
|
|
12
|
-
return { kind: 'auth', detail: 'Pixiv /
|
|
5
|
+
return { kind: 'auth', detail: 'Pixiv self/status HTTP ' + r.status };
|
|
13
6
|
}
|
|
14
7
|
if (!r.ok) return { kind: 'http', httpStatus: r.status };
|
|
15
|
-
const d = await r.json();
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (!uid) {
|
|
20
|
-
return { kind: 'auth', detail: 'Pixiv PHPSESSID prefix unparseable' };
|
|
8
|
+
const d = await r.json().catch(() => null);
|
|
9
|
+
const u = d?.body?.user_status;
|
|
10
|
+
if (d?.error || !u || u.is_loggedin === false || !u.user_id) {
|
|
11
|
+
return { kind: 'auth', detail: 'Pixiv self/status reports anonymous' };
|
|
21
12
|
}
|
|
22
|
-
return { ok: true, user_id:
|
|
13
|
+
return { ok: true, user_id: String(u.user_id), name: String(u.user_name || u.user_account || '') };
|
|
23
14
|
} catch (e) {
|
|
24
15
|
return { kind: 'exception', detail: String(e && e.message || e) };
|
|
25
16
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import{ArgumentError as H,AuthRequiredError as V,CommandExecutionError as P}from"@jackwener/opencli/errors";import{cli as W,Strategy as X}from"@jackwener/opencli/registry";const Y=/^[A-Za-z0-9][A-Za-z0-9_]{1,20}$/;export function normalizeSubreddit(M){let F=String(M||"").trim();if(!F)throw new H("Subreddit is required.",'Pass a subreddit like "test" or "r/test".');if(/^https?:\/\//i.test(F)){let N;try{N=new URL(F)}catch{throw new H(`Invalid subreddit URL: ${F}`)}const J=N.pathname.split("/").filter(Boolean),K=J.indexOf("r");if(K<0||J.length<=K+1)throw new H("Subreddit URL must contain /r/<name>.");F=J[K+1]}F=F.replace(/^\/?r\//i,"").replace(/^\/+/,"");if(!Y.test(F))throw new H(`Invalid subreddit name: ${JSON.stringify(F)}`,'Use the bare name, e.g. "test" (2–21 chars, letters/digits/underscore).');return F}W({site:"reddit",name:"submit",access:"write",description:"Submit a new post to a subreddit. Text post by default (--text); pass --url for a link post. Note: many subreddits gate submissions behind karma/age requirements or flair — a rejection is surfaced verbatim.",domain:"reddit.com",strategy:X.COOKIE,browser:!0,args:[{name:"subreddit",type:"string",required:!0,positional:!0,help:'Target subreddit, e.g. "test" or "r/test"'},{name:"title",type:"string",required:!0,positional:!0,help:"Post title (max 300 chars)"},{name:"text",type:"string",help:"Body text (markdown) for a self/text post"},{name:"url",type:"string",help:"External URL for a link post (mutually exclusive with --text)"},{name:"flair-id",type:"string",help:"Flair template id, if the subreddit requires one"},{name:"flair-text",type:"string",help:"Flair text to accompany --flair-id"},{name:"nsfw",type:"bool",default:!1,help:"Mark the post as NSFW"},{name:"spoiler",type:"bool",default:!1,help:"Mark the post as a spoiler"},{name:"sendreplies",type:"bool",default:!0,help:"Receive inbox replies for this post (default: true)"}],columns:["status","url","id","message"],func:async(M,F)=>{if(!M)throw new P("Browser session required");const N=normalizeSubreddit(F.subreddit),J=String(F.title??"").trim();if(!J)throw new H("Title is required.");if(J.length>300)throw new H("Title must be at most 300 characters.");const K=typeof F.text==="string"?F.text:"",O=typeof F.url==="string"?F.url.trim():"";if(O&&K)throw new H("Pass either --text (self post) or --url (link post), not both.");const Q=O?"link":"self";if(Q==="link")try{new URL(O)}catch{throw new H(`Invalid --url: ${O}`)}await M.goto("https://www.reddit.com");const G=await M.evaluate(`(async () => {
|
|
2
|
+
try {
|
|
3
|
+
const meRes = await fetch('/api/me.json', { credentials: 'include' });
|
|
4
|
+
if (meRes.status === 401 || meRes.status === 403) {
|
|
5
|
+
return { kind: 'auth', detail: 'Reddit /api/me.json HTTP ' + meRes.status };
|
|
6
|
+
}
|
|
7
|
+
const me = await meRes.json();
|
|
8
|
+
const modhash = me && me.data && me.data.modhash;
|
|
9
|
+
if (!modhash) return { kind: 'auth', detail: 'No modhash — anonymous or expired session' };
|
|
10
|
+
|
|
11
|
+
const p = new URLSearchParams();
|
|
12
|
+
p.set('sr', ${JSON.stringify(N)});
|
|
13
|
+
p.set('title', ${JSON.stringify(J)});
|
|
14
|
+
p.set('kind', ${JSON.stringify(Q)});
|
|
15
|
+
${Q==="link"?`p.set('url', ${JSON.stringify(O)}); p.set('resubmit', 'true');`:`p.set('text', ${JSON.stringify(K)});`}
|
|
16
|
+
p.set('api_type', 'json');
|
|
17
|
+
p.set('validate_on_submit', 'true');
|
|
18
|
+
p.set('sendreplies', ${F.sendreplies===!1?"'false'":"'true'"});
|
|
19
|
+
${F.nsfw?"p.set('nsfw', 'true');":""}
|
|
20
|
+
${F.spoiler?"p.set('spoiler', 'true');":""}
|
|
21
|
+
${F["flair-id"]?`p.set('flair_id', ${JSON.stringify(String(F["flair-id"]))});`:""}
|
|
22
|
+
${F["flair-text"]?`p.set('flair_text', ${JSON.stringify(String(F["flair-text"]))});`:""}
|
|
23
|
+
p.set('uh', modhash);
|
|
24
|
+
|
|
25
|
+
const res = await fetch('/api/submit', {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
credentials: 'include',
|
|
28
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
29
|
+
body: p.toString(),
|
|
30
|
+
});
|
|
31
|
+
if (!res.ok) return { kind: 'http', httpStatus: res.status };
|
|
32
|
+
const data = await res.json();
|
|
33
|
+
const errors = data && data.json && data.json.errors;
|
|
34
|
+
if (errors && errors.length > 0) {
|
|
35
|
+
return { kind: 'reject', detail: errors.map(e => e.join(': ')).join('; ') };
|
|
36
|
+
}
|
|
37
|
+
const d = (data && data.json && data.json.data) || {};
|
|
38
|
+
return { ok: true, url: d.url || '', id: d.id || (d.name || '') };
|
|
39
|
+
} catch (e) {
|
|
40
|
+
return { kind: 'exception', detail: String((e && e.message) || e) };
|
|
41
|
+
}
|
|
42
|
+
})()`);if(G?.kind==="auth")throw new V("reddit.com",G.detail);if(G?.kind==="http")throw new P(`HTTP ${G.httpStatus} from /api/submit`);if(G?.kind==="exception")throw new P(`Reddit submit failed: ${G.detail}`);if(G?.kind==="reject")return[{status:"failed",url:"",id:"",message:G.detail}];if(!G?.ok)throw new P(`Unexpected Reddit submit result: ${JSON.stringify(G)}`);return[{status:"success",url:G.url,id:G.id,message:"Post submitted to r/"+N}]}});
|
package/clis/suno/auth.js
CHANGED
|
@@ -1,24 +1,20 @@
|
|
|
1
1
|
import{AuthRequiredError as D,CommandExecutionError as F}from"@jackwener/opencli/errors";import{registerSiteAuthCommands as K}from"../_shared/site-auth.js";async function G(B){return(await B.getCookies({url:"https://clerk.suno.com"})).some((H)=>H.name==="__session"&&H.value)}async function J(B){if(!await G(B))throw new D("suno.com","Suno Clerk __session/__client cookie missing");await B.goto("https://suno.com/");await B.wait(2);const z=await B.evaluate(`(async () => {
|
|
2
2
|
try {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
});
|
|
7
|
-
if (r.status === 401 || r.status === 403) {
|
|
8
|
-
return { kind: 'auth', detail: 'clerk.suno.com /v1/client HTTP ' + r.status };
|
|
3
|
+
for (let i = 0; i < 20; i += 1) {
|
|
4
|
+
if (window.Clerk && window.Clerk.loaded) break;
|
|
5
|
+
await new Promise(r => setTimeout(r, 250));
|
|
9
6
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
7
|
+
const C = window.Clerk;
|
|
8
|
+
if (!C) return { kind: 'exception', detail: 'window.Clerk never mounted' };
|
|
9
|
+
const user = C.user;
|
|
10
|
+
const session = C.session;
|
|
11
|
+
if (!user || !session) {
|
|
12
|
+
return { kind: 'auth', detail: 'Suno Clerk mounted but no active user/session — anonymous' };
|
|
15
13
|
}
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
return { ok: true, user_id: String(user.id), name: String(user.username || '') };
|
|
14
|
+
const email = user.primaryEmailAddress?.emailAddress
|
|
15
|
+
|| user.emailAddresses?.[0]?.emailAddress
|
|
16
|
+
|| '';
|
|
17
|
+
return { ok: true, user_id: String(user.id), name: String(user.username || email || '') };
|
|
22
18
|
} catch (e) {
|
|
23
19
|
return { kind: 'exception', detail: String(e && e.message || e) };
|
|
24
20
|
}
|