publishport-opencli 1.8.5-pp.42 → 1.8.5-pp.44
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 +327 -0
- package/clis/linkedin/comment.js +1 -0
- package/clis/linkedin/follow.js +1 -0
- package/clis/linkedin/like.js +1 -0
- package/clis/linkedin/notifications.js +1 -0
- package/clis/linkedin/post-comments.js +1 -0
- package/clis/linkedin/post-detail.js +1 -0
- package/clis/linkedin/reply.js +1 -0
- package/clis/linkedin/repost.js +1 -0
- package/clis/linkedin/social-api.js +260 -0
- package/dist/src/browser/daemon-client.js +1 -1
- package/package.json +1 -1
package/cli-manifest.json
CHANGED
|
@@ -23502,6 +23502,47 @@
|
|
|
23502
23502
|
"modulePath": "lichess/user.js",
|
|
23503
23503
|
"sourceFile": "lichess/user.js"
|
|
23504
23504
|
},
|
|
23505
|
+
{
|
|
23506
|
+
"site": "linkedin",
|
|
23507
|
+
"name": "comment",
|
|
23508
|
+
"description": "Post a top-level comment on a LinkedIn post via the Voyager API. Dry-run by default; pass --send true to actually publish. Takes a post URL or activity/ugcPost urn.",
|
|
23509
|
+
"access": "write",
|
|
23510
|
+
"domain": "www.linkedin.com",
|
|
23511
|
+
"strategy": "cookie",
|
|
23512
|
+
"browser": true,
|
|
23513
|
+
"args": [
|
|
23514
|
+
{
|
|
23515
|
+
"name": "post",
|
|
23516
|
+
"type": "string",
|
|
23517
|
+
"required": true,
|
|
23518
|
+
"positional": true,
|
|
23519
|
+
"help": "Post to comment on: a LinkedIn post URL (…/feed/update/… or …/posts/…) or a raw urn:li:activity:… / urn:li:ugcPost:… urn."
|
|
23520
|
+
},
|
|
23521
|
+
{
|
|
23522
|
+
"name": "text",
|
|
23523
|
+
"type": "string",
|
|
23524
|
+
"required": true,
|
|
23525
|
+
"help": "Comment body (max 1250 chars)."
|
|
23526
|
+
},
|
|
23527
|
+
{
|
|
23528
|
+
"name": "send",
|
|
23529
|
+
"type": "bool",
|
|
23530
|
+
"default": false,
|
|
23531
|
+
"required": false,
|
|
23532
|
+
"help": "Actually post the comment. Default false = dry-run (validates the session only; nothing is posted)."
|
|
23533
|
+
}
|
|
23534
|
+
],
|
|
23535
|
+
"columns": [
|
|
23536
|
+
"status",
|
|
23537
|
+
"post_urn",
|
|
23538
|
+
"comment_urn",
|
|
23539
|
+
"text_chars"
|
|
23540
|
+
],
|
|
23541
|
+
"type": "js",
|
|
23542
|
+
"modulePath": "linkedin/comment.js",
|
|
23543
|
+
"sourceFile": "linkedin/comment.js",
|
|
23544
|
+
"navigateBefore": "https://www.linkedin.com"
|
|
23545
|
+
},
|
|
23505
23546
|
{
|
|
23506
23547
|
"site": "linkedin",
|
|
23507
23548
|
"name": "connect",
|
|
@@ -23609,6 +23650,39 @@
|
|
|
23609
23650
|
"sourceFile": "linkedin/create.js",
|
|
23610
23651
|
"navigateBefore": "https://www.linkedin.com"
|
|
23611
23652
|
},
|
|
23653
|
+
{
|
|
23654
|
+
"site": "linkedin",
|
|
23655
|
+
"name": "follow",
|
|
23656
|
+
"description": "Follow or unfollow a LinkedIn person or company page via the Voyager API. Following subscribes to their posts without a connection request (use `linkedin connect` for that). Takes a profile/company URL or urn.",
|
|
23657
|
+
"access": "write",
|
|
23658
|
+
"domain": "www.linkedin.com",
|
|
23659
|
+
"strategy": "cookie",
|
|
23660
|
+
"browser": true,
|
|
23661
|
+
"args": [
|
|
23662
|
+
{
|
|
23663
|
+
"name": "target",
|
|
23664
|
+
"type": "string",
|
|
23665
|
+
"required": true,
|
|
23666
|
+
"positional": true,
|
|
23667
|
+
"help": "Who to follow. A person MUST be given as a profile URL (…/in/<handle>) or urn:li:fsd_profile:… urn. A company can be a company URL (…/company/<name>), a bare company name (resolved as a company, never a person), or a urn:li:fsd_company:… urn."
|
|
23668
|
+
},
|
|
23669
|
+
{
|
|
23670
|
+
"name": "unfollow",
|
|
23671
|
+
"type": "bool",
|
|
23672
|
+
"default": false,
|
|
23673
|
+
"required": false,
|
|
23674
|
+
"help": "Unfollow instead of follow."
|
|
23675
|
+
}
|
|
23676
|
+
],
|
|
23677
|
+
"columns": [
|
|
23678
|
+
"status",
|
|
23679
|
+
"entity_urn"
|
|
23680
|
+
],
|
|
23681
|
+
"type": "js",
|
|
23682
|
+
"modulePath": "linkedin/follow.js",
|
|
23683
|
+
"sourceFile": "linkedin/follow.js",
|
|
23684
|
+
"navigateBefore": "https://www.linkedin.com"
|
|
23685
|
+
},
|
|
23612
23686
|
{
|
|
23613
23687
|
"site": "linkedin",
|
|
23614
23688
|
"name": "inbox",
|
|
@@ -23707,6 +23781,48 @@
|
|
|
23707
23781
|
"sourceFile": "linkedin/jobs-preferences.js",
|
|
23708
23782
|
"navigateBefore": "https://www.linkedin.com"
|
|
23709
23783
|
},
|
|
23784
|
+
{
|
|
23785
|
+
"site": "linkedin",
|
|
23786
|
+
"name": "like",
|
|
23787
|
+
"description": "React to a LinkedIn post OR comment (like by default; also celebrate/support/love/insightful/funny), or remove your reaction. Takes a post URL / activity urn, or a comment_urn from `linkedin post-comments`.",
|
|
23788
|
+
"access": "write",
|
|
23789
|
+
"domain": "www.linkedin.com",
|
|
23790
|
+
"strategy": "cookie",
|
|
23791
|
+
"browser": true,
|
|
23792
|
+
"args": [
|
|
23793
|
+
{
|
|
23794
|
+
"name": "target",
|
|
23795
|
+
"type": "string",
|
|
23796
|
+
"required": true,
|
|
23797
|
+
"positional": true,
|
|
23798
|
+
"help": "What to react to: a LinkedIn post URL (…/feed/update/…) or activity/ugcPost urn, OR a comment_urn (urn:li:comment:(…)) from `linkedin post-comments`."
|
|
23799
|
+
},
|
|
23800
|
+
{
|
|
23801
|
+
"name": "reaction",
|
|
23802
|
+
"type": "string",
|
|
23803
|
+
"default": "like",
|
|
23804
|
+
"required": false,
|
|
23805
|
+
"help": "Reaction type: like (default), celebrate, support, love, insightful, funny. Ignored when --remove true."
|
|
23806
|
+
},
|
|
23807
|
+
{
|
|
23808
|
+
"name": "remove",
|
|
23809
|
+
"type": "bool",
|
|
23810
|
+
"default": false,
|
|
23811
|
+
"required": false,
|
|
23812
|
+
"help": "Remove your existing reaction instead of adding one."
|
|
23813
|
+
}
|
|
23814
|
+
],
|
|
23815
|
+
"columns": [
|
|
23816
|
+
"status",
|
|
23817
|
+
"target_urn",
|
|
23818
|
+
"target_type",
|
|
23819
|
+
"reaction"
|
|
23820
|
+
],
|
|
23821
|
+
"type": "js",
|
|
23822
|
+
"modulePath": "linkedin/like.js",
|
|
23823
|
+
"sourceFile": "linkedin/like.js",
|
|
23824
|
+
"navigateBefore": "https://www.linkedin.com"
|
|
23825
|
+
},
|
|
23710
23826
|
{
|
|
23711
23827
|
"site": "linkedin",
|
|
23712
23828
|
"name": "login",
|
|
@@ -23739,6 +23855,50 @@
|
|
|
23739
23855
|
"siteSession": "persistent",
|
|
23740
23856
|
"defaultWindowMode": "foreground"
|
|
23741
23857
|
},
|
|
23858
|
+
{
|
|
23859
|
+
"site": "linkedin",
|
|
23860
|
+
"name": "notifications",
|
|
23861
|
+
"description": "Read your LinkedIn notifications via the Voyager API: who reacted to / commented on / mentioned your posts, profile views, follows, and more. Read-only.",
|
|
23862
|
+
"access": "read",
|
|
23863
|
+
"domain": "www.linkedin.com",
|
|
23864
|
+
"strategy": "cookie",
|
|
23865
|
+
"browser": true,
|
|
23866
|
+
"args": [
|
|
23867
|
+
{
|
|
23868
|
+
"name": "limit",
|
|
23869
|
+
"type": "int",
|
|
23870
|
+
"default": 20,
|
|
23871
|
+
"required": false,
|
|
23872
|
+
"help": "Maximum notifications to return (1-100)."
|
|
23873
|
+
},
|
|
23874
|
+
{
|
|
23875
|
+
"name": "filter",
|
|
23876
|
+
"type": "string",
|
|
23877
|
+
"default": "all",
|
|
23878
|
+
"required": false,
|
|
23879
|
+
"help": "Which notifications: all (default), mentions, posts (activity on your posts), jobs."
|
|
23880
|
+
},
|
|
23881
|
+
{
|
|
23882
|
+
"name": "unread-only",
|
|
23883
|
+
"type": "bool",
|
|
23884
|
+
"default": false,
|
|
23885
|
+
"required": false,
|
|
23886
|
+
"help": "Only return unread notifications."
|
|
23887
|
+
}
|
|
23888
|
+
],
|
|
23889
|
+
"columns": [
|
|
23890
|
+
"rank",
|
|
23891
|
+
"unread",
|
|
23892
|
+
"headline",
|
|
23893
|
+
"content",
|
|
23894
|
+
"when",
|
|
23895
|
+
"target"
|
|
23896
|
+
],
|
|
23897
|
+
"type": "js",
|
|
23898
|
+
"modulePath": "linkedin/notifications.js",
|
|
23899
|
+
"sourceFile": "linkedin/notifications.js",
|
|
23900
|
+
"navigateBefore": "https://www.linkedin.com"
|
|
23901
|
+
},
|
|
23742
23902
|
{
|
|
23743
23903
|
"site": "linkedin",
|
|
23744
23904
|
"name": "people-search",
|
|
@@ -23890,6 +24050,91 @@
|
|
|
23890
24050
|
"sourceFile": "linkedin/post-analytics.js",
|
|
23891
24051
|
"navigateBefore": "https://www.linkedin.com"
|
|
23892
24052
|
},
|
|
24053
|
+
{
|
|
24054
|
+
"site": "linkedin",
|
|
24055
|
+
"name": "post-comments",
|
|
24056
|
+
"description": "List the comments (and nested replies) on a LinkedIn post via the Voyager API. Emits each comment_urn so you can reply to it. Takes a post URL or activity/ugcPost urn.",
|
|
24057
|
+
"access": "read",
|
|
24058
|
+
"domain": "www.linkedin.com",
|
|
24059
|
+
"strategy": "cookie",
|
|
24060
|
+
"browser": true,
|
|
24061
|
+
"args": [
|
|
24062
|
+
{
|
|
24063
|
+
"name": "post",
|
|
24064
|
+
"type": "string",
|
|
24065
|
+
"required": true,
|
|
24066
|
+
"positional": true,
|
|
24067
|
+
"help": "Post to read comments from: a LinkedIn post URL (…/feed/update/… or …/posts/…) or a raw urn:li:activity:… / urn:li:ugcPost:… urn."
|
|
24068
|
+
},
|
|
24069
|
+
{
|
|
24070
|
+
"name": "limit",
|
|
24071
|
+
"type": "int",
|
|
24072
|
+
"default": 20,
|
|
24073
|
+
"required": false,
|
|
24074
|
+
"help": "Maximum comments to return (1-100)."
|
|
24075
|
+
},
|
|
24076
|
+
{
|
|
24077
|
+
"name": "sort",
|
|
24078
|
+
"type": "string",
|
|
24079
|
+
"default": "relevance",
|
|
24080
|
+
"required": false,
|
|
24081
|
+
"help": "Comment order: relevance (default) or recent (chronological)."
|
|
24082
|
+
}
|
|
24083
|
+
],
|
|
24084
|
+
"columns": [
|
|
24085
|
+
"rank",
|
|
24086
|
+
"comment_urn",
|
|
24087
|
+
"author",
|
|
24088
|
+
"headline",
|
|
24089
|
+
"text",
|
|
24090
|
+
"created_at",
|
|
24091
|
+
"is_reply",
|
|
24092
|
+
"parent_comment_urn",
|
|
24093
|
+
"reactions",
|
|
24094
|
+
"permalink"
|
|
24095
|
+
],
|
|
24096
|
+
"type": "js",
|
|
24097
|
+
"modulePath": "linkedin/post-comments.js",
|
|
24098
|
+
"sourceFile": "linkedin/post-comments.js",
|
|
24099
|
+
"navigateBefore": "https://www.linkedin.com"
|
|
24100
|
+
},
|
|
24101
|
+
{
|
|
24102
|
+
"site": "linkedin",
|
|
24103
|
+
"name": "post-detail",
|
|
24104
|
+
"description": "Read one LinkedIn post via the Voyager API: author, body text, reactions/comments/reposts/impressions, and whether it is a repost. Takes a post URL or activity/ugcPost urn.",
|
|
24105
|
+
"access": "read",
|
|
24106
|
+
"domain": "www.linkedin.com",
|
|
24107
|
+
"strategy": "cookie",
|
|
24108
|
+
"browser": true,
|
|
24109
|
+
"args": [
|
|
24110
|
+
{
|
|
24111
|
+
"name": "post",
|
|
24112
|
+
"type": "string",
|
|
24113
|
+
"required": true,
|
|
24114
|
+
"positional": true,
|
|
24115
|
+
"help": "Post to read: a LinkedIn post URL (…/feed/update/… or …/posts/…) or a raw urn:li:activity:… / urn:li:ugcPost:… urn."
|
|
24116
|
+
}
|
|
24117
|
+
],
|
|
24118
|
+
"columns": [
|
|
24119
|
+
"post_urn",
|
|
24120
|
+
"author",
|
|
24121
|
+
"author_type",
|
|
24122
|
+
"author_url",
|
|
24123
|
+
"posted",
|
|
24124
|
+
"text",
|
|
24125
|
+
"content_type",
|
|
24126
|
+
"is_repost",
|
|
24127
|
+
"reactions",
|
|
24128
|
+
"comments",
|
|
24129
|
+
"reposts",
|
|
24130
|
+
"impressions",
|
|
24131
|
+
"url"
|
|
24132
|
+
],
|
|
24133
|
+
"type": "js",
|
|
24134
|
+
"modulePath": "linkedin/post-detail.js",
|
|
24135
|
+
"sourceFile": "linkedin/post-detail.js",
|
|
24136
|
+
"navigateBefore": "https://www.linkedin.com"
|
|
24137
|
+
},
|
|
23893
24138
|
{
|
|
23894
24139
|
"site": "linkedin",
|
|
23895
24140
|
"name": "posts",
|
|
@@ -24069,6 +24314,88 @@
|
|
|
24069
24314
|
"sourceFile": "linkedin/profile-read.js",
|
|
24070
24315
|
"navigateBefore": "https://www.linkedin.com"
|
|
24071
24316
|
},
|
|
24317
|
+
{
|
|
24318
|
+
"site": "linkedin",
|
|
24319
|
+
"name": "reply",
|
|
24320
|
+
"description": "Reply to a comment on a LinkedIn post via the Voyager API. Dry-run by default; pass --send true to actually publish. The comment urn comes from `linkedin post-comments`.",
|
|
24321
|
+
"access": "write",
|
|
24322
|
+
"domain": "www.linkedin.com",
|
|
24323
|
+
"strategy": "cookie",
|
|
24324
|
+
"browser": true,
|
|
24325
|
+
"args": [
|
|
24326
|
+
{
|
|
24327
|
+
"name": "comment",
|
|
24328
|
+
"type": "string",
|
|
24329
|
+
"required": true,
|
|
24330
|
+
"positional": true,
|
|
24331
|
+
"help": "The comment to reply to: a comment_urn from `linkedin post-comments` (urn:li:comment:(activity:…,…) or urn:li:fsd_comment:(…))."
|
|
24332
|
+
},
|
|
24333
|
+
{
|
|
24334
|
+
"name": "text",
|
|
24335
|
+
"type": "string",
|
|
24336
|
+
"required": true,
|
|
24337
|
+
"help": "Reply body (max 1250 chars)."
|
|
24338
|
+
},
|
|
24339
|
+
{
|
|
24340
|
+
"name": "send",
|
|
24341
|
+
"type": "bool",
|
|
24342
|
+
"default": false,
|
|
24343
|
+
"required": false,
|
|
24344
|
+
"help": "Actually post the reply. Default false = dry-run (validates the session only; nothing is posted)."
|
|
24345
|
+
}
|
|
24346
|
+
],
|
|
24347
|
+
"columns": [
|
|
24348
|
+
"status",
|
|
24349
|
+
"parent_comment_urn",
|
|
24350
|
+
"reply_urn",
|
|
24351
|
+
"text_chars"
|
|
24352
|
+
],
|
|
24353
|
+
"type": "js",
|
|
24354
|
+
"modulePath": "linkedin/reply.js",
|
|
24355
|
+
"sourceFile": "linkedin/reply.js",
|
|
24356
|
+
"navigateBefore": "https://www.linkedin.com"
|
|
24357
|
+
},
|
|
24358
|
+
{
|
|
24359
|
+
"site": "linkedin",
|
|
24360
|
+
"name": "repost",
|
|
24361
|
+
"description": "Repost (reshare) a LinkedIn post to your own feed via the Voyager API. Plain repost by default; pass --text to quote-repost with your commentary. Dry-run unless --send true.",
|
|
24362
|
+
"access": "write",
|
|
24363
|
+
"domain": "www.linkedin.com",
|
|
24364
|
+
"strategy": "cookie",
|
|
24365
|
+
"browser": true,
|
|
24366
|
+
"args": [
|
|
24367
|
+
{
|
|
24368
|
+
"name": "post",
|
|
24369
|
+
"type": "string",
|
|
24370
|
+
"required": true,
|
|
24371
|
+
"positional": true,
|
|
24372
|
+
"help": "Post to repost: a LinkedIn post URL (…/feed/update/… or …/posts/…) or a raw urn:li:activity:… / urn:li:ugcPost:… urn."
|
|
24373
|
+
},
|
|
24374
|
+
{
|
|
24375
|
+
"name": "text",
|
|
24376
|
+
"type": "string",
|
|
24377
|
+
"required": false,
|
|
24378
|
+
"help": "Optional commentary to quote-repost with (max 3000 chars). Omit for a plain repost."
|
|
24379
|
+
},
|
|
24380
|
+
{
|
|
24381
|
+
"name": "send",
|
|
24382
|
+
"type": "bool",
|
|
24383
|
+
"default": false,
|
|
24384
|
+
"required": false,
|
|
24385
|
+
"help": "Actually repost. Default false = dry-run (validates the session only; nothing is posted)."
|
|
24386
|
+
}
|
|
24387
|
+
],
|
|
24388
|
+
"columns": [
|
|
24389
|
+
"status",
|
|
24390
|
+
"shared_urn",
|
|
24391
|
+
"repost_urn",
|
|
24392
|
+
"quote"
|
|
24393
|
+
],
|
|
24394
|
+
"type": "js",
|
|
24395
|
+
"modulePath": "linkedin/repost.js",
|
|
24396
|
+
"sourceFile": "linkedin/repost.js",
|
|
24397
|
+
"navigateBefore": "https://www.linkedin.com"
|
|
24398
|
+
},
|
|
24072
24399
|
{
|
|
24073
24400
|
"site": "linkedin",
|
|
24074
24401
|
"name": "safe-send",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cli as Z,Strategy as $}from"@jackwener/opencli/registry";import{ArgumentError as Q,AuthRequiredError as b,CommandExecutionError as V}from"@jackwener/opencli/errors";import{assertLinkedInAuthenticated as q,LINKEDIN_DOMAIN as W,unwrapEvaluateResult as v}from"./shared.js";import{buildCommentScript as z,NORM_COMMENT_DECORATION as K,normalizeThreadUrn as k}from"./social-api.js";const P=1250;Z({site:"linkedin",name:"comment",access:"write",description:"Post a top-level comment on a LinkedIn post via the Voyager API. Dry-run by default; pass --send true to actually publish. Takes a post URL or activity/ugcPost urn.",domain:W,strategy:$.COOKIE,browser:!0,args:[{name:"post",type:"string",required:!0,positional:!0,help:"Post to comment on: a LinkedIn post URL (…/feed/update/… or …/posts/…) or a raw urn:li:activity:… / urn:li:ugcPost:… urn."},{name:"text",type:"string",required:!0,help:`Comment body (max ${P} chars).`},{name:"send",type:"bool",required:!1,default:!1,help:"Actually post the comment. Default false = dry-run (validates the session only; nothing is posted)."}],columns:["status","post_urn","comment_urn","text_chars"],func:async(F,G)=>{if(!F)throw new V("Browser session required for linkedin comment");const J=k(G?.post),B=String(G?.text??"").replace(/ /g," ").trim();if(!B)throw new Q("--text is required");if(B.length>P)throw new Q(`--text must be ${P} characters or fewer (got ${B.length})`);const Y=G?.send===!0||G?.send==="true";await F.goto(`https://www.linkedin.com/feed/update/${J}/`);await F.wait(2);if(!Y){await q(F,"LinkedIn comment");return[{status:"dry_run",post_urn:J,comment_urn:"(not posted — pass --send true to publish)",text_chars:B.length}]}const f=v(await F.evaluate(z({threadUrn:J,text:B,decorationId:K})));if(!f?.ok){if(f?.stage==="auth"||f?.httpStatus===401||f?.httpStatus===403)throw new b(W,`LinkedIn comment requires an active signed-in LinkedIn browser session (${f?.error||"auth"}).`);throw new V(`LinkedIn comment failed: stage=${f?.stage||"unknown"} http=${f?.httpStatus||"?"} ${f?.error||""}`.trim())}return[{status:"commented",post_urn:J,comment_urn:f.commentUrn||"",text_chars:B.length}]}});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cli as T,Strategy as U}from"@jackwener/opencli/registry";import{ArgumentError as V,AuthRequiredError as W,CommandExecutionError as J}from"@jackwener/opencli/errors";import{LINKEDIN_DOMAIN as Q,unwrapEvaluateResult as X}from"./shared.js";import{buildFollowScript as Y}from"./social-api.js";T({site:"linkedin",name:"follow",access:"write",description:"Follow or unfollow a LinkedIn person or company page via the Voyager API. Following subscribes to their posts without a connection request (use `linkedin connect` for that). Takes a profile/company URL or urn.",domain:Q,strategy:U.COOKIE,browser:!0,args:[{name:"target",type:"string",required:!0,positional:!0,help:"Who to follow. A person MUST be given as a profile URL (…/in/<handle>) or urn:li:fsd_profile:… urn. A company can be a company URL (…/company/<name>), a bare company name (resolved as a company, never a person), or a urn:li:fsd_company:… urn."},{name:"unfollow",type:"bool",required:!1,default:!1,help:"Unfollow instead of follow."}],columns:["status","entity_urn"],func:async(B,G)=>{if(!B)throw new J("Browser session required for linkedin follow");const H=String(G?.target??"").trim();if(!H)throw new V("target is required (profile/company URL or urn)");const P=G?.unfollow===!0||G?.unfollow==="true";await B.goto("https://www.linkedin.com/feed/");await B.wait(2);const z=X(await B.evaluate(Y({target:H,following:!P})));if(!z?.ok){if(z?.stage==="auth"||z?.httpStatus===401||z?.httpStatus===403)throw new W(Q,`LinkedIn follow requires an active signed-in LinkedIn browser session (${z?.error||"auth"}).`);if(z?.stage==="resolve")throw new J(`LinkedIn follow could not resolve "${H}" to a person or company. Pass a /in/<handle> URL, a /company/<name> URL, or a urn.`);throw new J(`LinkedIn follow failed: stage=${z?.stage||"unknown"} http=${z?.httpStatus||"?"} ${z?.error||""}`.trim())}return[{status:P?"unfollowed":"followed",entity_urn:z.entity_urn||""}]}});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cli as Z,Strategy as $}from"@jackwener/opencli/registry";import{AuthRequiredError as q,CommandExecutionError as W}from"@jackwener/opencli/errors";import{LINKEDIN_DOMAIN as X,unwrapEvaluateResult as G}from"./shared.js";import{buildCommentReactScript as b,buildReactScript as x,looksLikeCommentUrn as z,normalizeCommentUrn as K,normalizeThreadUrn as M,parseReactionType as w,REACTION_CREATE_QID as Q,REACTION_DELETE_QID as h}from"./social-api.js";Z({site:"linkedin",name:"like",access:"write",description:"React to a LinkedIn post OR comment (like by default; also celebrate/support/love/insightful/funny), or remove your reaction. Takes a post URL / activity urn, or a comment_urn from `linkedin post-comments`.",domain:X,strategy:$.COOKIE,browser:!0,args:[{name:"target",type:"string",required:!0,positional:!0,help:"What to react to: a LinkedIn post URL (…/feed/update/…) or activity/ugcPost urn, OR a comment_urn (urn:li:comment:(…)) from `linkedin post-comments`."},{name:"reaction",type:"string",required:!1,default:"like",help:"Reaction type: like (default), celebrate, support, love, insightful, funny. Ignored when --remove true."},{name:"remove",type:"bool",required:!1,default:!1,help:"Remove your existing reaction instead of adding one."}],columns:["status","target_urn","target_type","reaction"],func:async(H,B)=>{if(!H)throw new W("Browser session required for linkedin like");const F=B?.remove===!0||B?.remove==="true",J=F?"":w(B?.reaction),P=z(B?.target),V=P?K(B?.target):M(B?.target);await H.goto("https://www.linkedin.com/feed/");await H.wait(2);const Y=P?b({commentUrn:V,reactionType:J,remove:F,createQid:Q,deleteQid:h}):x({threadUrn:V,reactionType:J,remove:F}),f=G(await H.evaluate(Y));if(!f?.ok){if(f?.stage==="auth"||f?.httpStatus===401||f?.httpStatus===403)throw new q(X,`LinkedIn like requires an active signed-in LinkedIn browser session (${f?.error||"auth"}).`);throw new W(`LinkedIn like failed: stage=${f?.stage||"unknown"} http=${f?.httpStatus||"?"} ${f?.error||""}`.trim())}return[{status:F?"reaction_removed":"reacted",target_urn:V,target_type:P?"comment":"post",reaction:F?"":J}]}});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cli as X,Strategy as Z}from"@jackwener/opencli/registry";import{ArgumentError as b,AuthRequiredError as f,CommandExecutionError as G,EmptyResultError as j}from"@jackwener/opencli/errors";import{LINKEDIN_DOMAIN as K,normalizeWhitespace as C,parseLimit as x,unwrapEvaluateResult as z}from"./shared.js";import{buildNotificationsScript as O}from"./social-api.js";const P=20,U=100,k=20,R={all:"all",mentions:"mentions",posts:"my_posts_all",jobs:"job_alerts"};X({site:"linkedin",name:"notifications",access:"read",description:"Read your LinkedIn notifications via the Voyager API: who reacted to / commented on / mentioned your posts, profile views, follows, and more. Read-only.",domain:K,strategy:Z.COOKIE,browser:!0,args:[{name:"limit",type:"int",required:!1,default:P,help:`Maximum notifications to return (1-${U}).`},{name:"filter",type:"string",required:!1,default:"all",help:"Which notifications: all (default), mentions, posts (activity on your posts), jobs."},{name:"unread-only",type:"bool",required:!1,default:!1,help:"Only return unread notifications."}],columns:["rank","unread","headline","content","when","target"],func:async(Q,H)=>{if(!Q)throw new G("Browser session required for linkedin notifications");const V=x(H?.limit,P,U),W=String(H?.filter??"all").trim().toLowerCase(),v=R[W];if(!v)throw new b(`--filter 只能是 all / mentions / posts / jobs(收到 "${H?.filter}")`);const F=H?.["unread-only"]===!0||H?.["unread-only"]==="true";await Q.goto("https://www.linkedin.com/feed/");await Q.wait(2);const $=[];for(let Y=0;$.length<V&&Y<U;Y+=k){const B=z(await Q.evaluate(O({count:k,start:Y,filter:v})));if(!B?.ok){if(B?.stage==="auth"||B?.httpStatus===401||B?.httpStatus===403)throw new f(K,`LinkedIn notifications requires an active signed-in LinkedIn browser session (${B?.error||"auth"}).`);throw new G(`LinkedIn notifications failed: stage=${B?.stage||"read"} http=${B?.httpStatus||"?"} ${B?.error||""}`.trim())}const q=Array.isArray(B.rows)?B.rows:[];if(q.length===0)break;for(const J of q){if(F&&J.read)continue;$.push({unread:J.read?"":"yes",headline:C(J.headline),content:C(J.content),when:J.when?new Date(J.when).toISOString():"",target:y(J.target)})}if(q.length<k)break}if($.length===0)throw new j("linkedin notifications",F?"No unread notifications.":"No notifications were returned for this filter.");return $.slice(0,V).map((Y,B)=>({rank:B+1,...Y}))}});function y(Q){const H=C(Q);if(!H)return"";try{const V=new URL(H,"https://www.linkedin.com");if(V.protocol!=="https:")return"";return V.toString()}catch{return""}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cli as R,Strategy as S}from"@jackwener/opencli/registry";import{ArgumentError as W,AuthRequiredError as K,CommandExecutionError as X,EmptyResultError as k}from"@jackwener/opencli/errors";import{LINKEDIN_DOMAIN as b,normalizeWhitespace as q,parseLimit as z,unwrapEvaluateResult as D}from"./shared.js";import{buildReadCommentsScript as N,normalizeThreadUrn as T}from"./social-api.js";const j=20,v=100,x=50;R({site:"linkedin",name:"post-comments",access:"read",description:"List the comments (and nested replies) on a LinkedIn post via the Voyager API. Emits each comment_urn so you can reply to it. Takes a post URL or activity/ugcPost urn.",domain:b,strategy:S.COOKIE,browser:!0,args:[{name:"post",type:"string",required:!0,positional:!0,help:"Post to read comments from: a LinkedIn post URL (…/feed/update/… or …/posts/…) or a raw urn:li:activity:… / urn:li:ugcPost:… urn."},{name:"limit",type:"int",required:!1,default:j,help:`Maximum comments to return (1-${v}).`},{name:"sort",type:"string",required:!1,default:"relevance",help:"Comment order: relevance (default) or recent (chronological)."}],columns:["rank","comment_urn","author","headline","text","created_at","is_reply","parent_comment_urn","reactions","permalink"],func:async(J,Q)=>{if(!J)throw new X("Browser session required for linkedin post-comments");const V=T(Q?.post),F=z(Q?.limit,j,v),G=String(Q?.sort??"relevance").trim().toLowerCase();if(G!=="relevance"&&G!=="recent")throw new W("--sort 只能是 relevance 或 recent");const C=G==="recent"?"CHRONOLOGICAL":"RELEVANCE";await J.goto(`https://www.linkedin.com/feed/update/${V}/`);await J.wait(2);const Y=[],O=new Set;for(let $=0;Y.length<F&&$<v;$+=x){const H=Math.min(x,F-Y.length),B=D(await J.evaluate(N({activityUrn:V,count:H,start:$,sortOrder:C})));if(!B?.ok){if(B?.stage==="auth"||B?.httpStatus===401||B?.httpStatus===403)throw new K(b,`LinkedIn post-comments requires an active signed-in LinkedIn browser session (${B?.error||"auth"}).`);throw new X(`LinkedIn post-comments failed: stage=${B?.stage||"read"} http=${B?.httpStatus||"?"} ${B?.error||""}`.trim())}const Z=Array.isArray(B.rows)?B.rows:[];if(Z.length===0)break;for(const f of Z){const P=f.comment_urn||`${f.author}::${f.created_at}::${(f.text||"").slice(0,40)}`;if(O.has(P))continue;O.add(P);Y.push({comment_urn:f.comment_urn||"",author:q(f.author),headline:q(f.headline),text:q(f.text),created_at:f.created_at?new Date(f.created_at).toISOString():"",is_reply:f.is_reply?"yes":"",parent_comment_urn:f.parent_comment_urn||"",reactions:Number(f.reactions)||0,permalink:U(f.permalink)})}if(Z.length<H)break}if(Y.length===0)throw new k("linkedin post-comments","This post has no comments, or comments are not visible to your account.");return Y.slice(0,F).map(($,H)=>({rank:H+1,...$}))}});function U(J){const Q=q(J);if(!Q)return"";try{const V=new URL(Q,"https://www.linkedin.com");if(V.protocol!=="https:")return"";return V.toString()}catch{return""}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cli as X,Strategy as Y}from"@jackwener/opencli/registry";import{AuthRequiredError as Z,CommandExecutionError as J,EmptyResultError as $}from"@jackwener/opencli/errors";import{LINKEDIN_DOMAIN as Q,normalizeWhitespace as F,unwrapEvaluateResult as b}from"./shared.js";import{buildPostDetailScript as q,normalizeThreadUrn as P}from"./social-api.js";X({site:"linkedin",name:"post-detail",access:"read",description:"Read one LinkedIn post via the Voyager API: author, body text, reactions/comments/reposts/impressions, and whether it is a repost. Takes a post URL or activity/ugcPost urn.",domain:Q,strategy:Y.COOKIE,browser:!0,args:[{name:"post",type:"string",required:!0,positional:!0,help:"Post to read: a LinkedIn post URL (…/feed/update/… or …/posts/…) or a raw urn:li:activity:… / urn:li:ugcPost:… urn."}],columns:["post_urn","author","author_type","author_url","posted","text","content_type","is_repost","reactions","comments","reposts","impressions","url"],func:async(G,V)=>{if(!G)throw new J("Browser session required for linkedin post-detail");const H=P(V?.post);await G.goto("https://www.linkedin.com/feed/");await G.wait(2);const B=b(await G.evaluate(q({activityUrn:H})));if(!B?.ok){if(B?.stage==="auth"||B?.httpStatus===401||B?.httpStatus===403)throw new Z(Q,`LinkedIn post-detail requires an active signed-in LinkedIn browser session (${B?.error||"auth"}).`);throw new J(`LinkedIn post-detail failed: stage=${B?.stage||"read"} http=${B?.httpStatus||"?"} ${B?.error||""}`.trim())}const f=B.post||{};if(!f.author&&!f.text)throw new $("linkedin post-detail","The post was not found or is not visible to your account.");return[{post_urn:f.post_urn||H,author:F(f.author),author_type:f.author_type||"",author_url:F(f.author_url),posted:F(f.posted).replace(/[•·|\s]+$/,""),text:F(f.text),content_type:f.content_type||"",is_repost:f.is_repost||"",reactions:Number(f.reactions)||0,comments:Number(f.comments)||0,reposts:Number(f.reposts)||0,impressions:Number(f.impressions)||0,url:F(f.url)}]}});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cli as b,Strategy as q}from"@jackwener/opencli/registry";import{ArgumentError as W,AuthRequiredError as v,CommandExecutionError as Y}from"@jackwener/opencli/errors";import{assertLinkedInAuthenticated as z,LINKEDIN_DOMAIN as Z,unwrapEvaluateResult as K}from"./shared.js";import{buildCommentScript as k,NORM_COMMENT_DECORATION as w,normalizeCommentUrn as y}from"./social-api.js";const Q=1250;b({site:"linkedin",name:"reply",access:"write",description:"Reply to a comment on a LinkedIn post via the Voyager API. Dry-run by default; pass --send true to actually publish. The comment urn comes from `linkedin post-comments`.",domain:Z,strategy:q.COOKIE,browser:!0,args:[{name:"comment",type:"string",required:!0,positional:!0,help:"The comment to reply to: a comment_urn from `linkedin post-comments` (urn:li:comment:(activity:…,…) or urn:li:fsd_comment:(…))."},{name:"text",type:"string",required:!0,help:`Reply body (max ${Q} chars).`},{name:"send",type:"bool",required:!1,default:!1,help:"Actually post the reply. Default false = dry-run (validates the session only; nothing is posted)."}],columns:["status","parent_comment_urn","reply_urn","text_chars"],func:async(F,G)=>{if(!F)throw new Y("Browser session required for linkedin reply");const J=y(G?.comment),B=String(G?.text??"").replace(/ /g," ").trim();if(!B)throw new W("--text is required");if(B.length>Q)throw new W(`--text must be ${Q} characters or fewer (got ${B.length})`);const $=G?.send===!0||G?.send==="true",P=J.match(/urn:li:comment:\((activity|ugcPost):(\d+),/),V=P?`urn:li:${P[1]}:${P[2]}`:"";await F.goto(V?`https://www.linkedin.com/feed/update/${V}/`:"https://www.linkedin.com/feed/");await F.wait(2);if(!$){await z(F,"LinkedIn reply");return[{status:"dry_run",parent_comment_urn:J,reply_urn:"(not posted — pass --send true to publish)",text_chars:B.length}]}const f=K(await F.evaluate(k({threadUrn:J,text:B,decorationId:w})));if(!f?.ok){if(f?.stage==="auth"||f?.httpStatus===401||f?.httpStatus===403)throw new v(Z,`LinkedIn reply requires an active signed-in LinkedIn browser session (${f?.error||"auth"}).`);throw new Y(`LinkedIn reply failed: stage=${f?.stage||"unknown"} http=${f?.httpStatus||"?"} ${f?.error||""}`.trim())}return[{status:"replied",parent_comment_urn:J,reply_urn:f.commentUrn||"",text_chars:B.length}]}});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cli as $,Strategy as b}from"@jackwener/opencli/registry";import{ArgumentError as z,AuthRequiredError as P,CommandExecutionError as W}from"@jackwener/opencli/errors";import{assertLinkedInAuthenticated as Q,LINKEDIN_DOMAIN as Y,unwrapEvaluateResult as q}from"./shared.js";import{buildRepostScript as x,normalizeThreadUrn as K,REPOST_CREATE_QID as k}from"./social-api.js";const J=3000;$({site:"linkedin",name:"repost",access:"write",description:"Repost (reshare) a LinkedIn post to your own feed via the Voyager API. Plain repost by default; pass --text to quote-repost with your commentary. Dry-run unless --send true.",domain:Y,strategy:b.COOKIE,browser:!0,args:[{name:"post",type:"string",required:!0,positional:!0,help:"Post to repost: a LinkedIn post URL (…/feed/update/… or …/posts/…) or a raw urn:li:activity:… / urn:li:ugcPost:… urn."},{name:"text",type:"string",required:!1,help:`Optional commentary to quote-repost with (max ${J} chars). Omit for a plain repost.`},{name:"send",type:"bool",required:!1,default:!1,help:"Actually repost. Default false = dry-run (validates the session only; nothing is posted)."}],columns:["status","shared_urn","repost_urn","quote"],func:async(B,F)=>{if(!B)throw new W("Browser session required for linkedin repost");const V=K(F?.post),G=String(F?.text??"").replace(/ /g," ").trim();if(G.length>J)throw new z(`--text must be ${J} characters or fewer (got ${G.length})`);const Z=F?.send===!0||F?.send==="true";await B.goto("https://www.linkedin.com/feed/");await B.wait(2);if(!Z){await Q(B,"LinkedIn repost");return[{status:"dry_run",shared_urn:V,repost_urn:"(not posted — pass --send true to repost)",quote:G?"yes":""}]}const f=q(await B.evaluate(x({activityUrn:V,commentary:G,repostQid:k})));if(!f?.ok){if(f?.stage==="auth"||f?.httpStatus===401||f?.httpStatus===403)throw new P(Y,`LinkedIn repost requires an active signed-in LinkedIn browser session (${f?.error||"auth"}).`);throw new W(`LinkedIn repost failed: stage=${f?.stage||"unknown"} http=${f?.httpStatus||"?"} ${f?.error||""}`.trim())}return[{status:"reposted",shared_urn:f.shared_urn||"",repost_urn:f.repost_urn||"",quote:f.quote?"yes":""}]}});
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import{ArgumentError as F}from"@jackwener/opencli/errors";export const NORM_COMMENT_DECORATION="com.linkedin.voyager.dash.deco.social.NormComment-43",REACTION_CREATE_QID="voyagerSocialDashReactions.b731222600772fd42464c0fe19bd722b",REACTION_DELETE_QID="voyagerSocialDashReactions.f68b48ae5bc0085d7a45c7003b772a39",REPOST_CREATE_QID="voyagerFeedDashReposts.a0663ae5c654123343da36617d2dbfde";export function looksLikeCommentUrn(q){const b=String(q??"");return/urn:li:(fsd_)?comment:\(/.test(b)}export const REACTION_TYPES=["LIKE","CELEBRATE","SUPPORT","LOVE","INSIGHTFUL","FUNNY"];const H={like:"LIKE",celebrate:"PRAISE",praise:"PRAISE",support:"EMPATHY",empathy:"EMPATHY",love:"APPRECIATION",appreciation:"APPRECIATION",insightful:"INTEREST",interest:"INTEREST",funny:"ENTERTAINMENT",entertainment:"ENTERTAINMENT",haha:"ENTERTAINMENT"};export function parseReactionType(q){const b=String(q??"like").trim().toLowerCase(),z=H[b];if(!z)throw new F(`--reaction 只能是 like / celebrate / support / love / insightful / funny(收到 "${q}")`);return z}export function normalizeThreadUrn(q){const b=String(q??"").trim();if(!b)throw new F("post 标识必填(帖子 URL 或 urn:li:activity:… / urn:li:ugcPost:…)");const z=b.match(/urn:li:(activity|ugcPost|share):(\d+)/);if(z)return`urn:li:${z[1]}:${z[2]}`;const D=b.match(/activity[:\-](\d{6,})/i);if(D)return`urn:li:activity:${D[1]}`;const G=b.match(/ugcPost[:\-](\d{6,})/i);if(G)return`urn:li:ugcPost:${G[1]}`;throw new F(`无法从 "${b}" 解析出帖子 urn(需 activity/ugcPost urn 或含 activity-<id> 的 LinkedIn 链接)`)}export function normalizeCommentUrn(q){const b=String(q??"").trim();if(!b)throw new F("comment 标识必填(urn:li:comment:(…) 或 urn:li:fsd_comment:(…))");const z=b.match(/urn:li:comment:\((activity|ugcPost):(\d+),(\d+)\)/);if(z)return`urn:li:comment:(${z[1]}:${z[2]},${z[3]})`;const D=b.match(/urn:li:fsd_comment:\((\d+),urn:li:(activity|ugcPost):(\d+)\)/);if(D)return`urn:li:comment:(${D[2]}:${D[3]},${D[1]})`;throw new F(`无法解析评论 urn "${b}"(需 urn:li:comment:(…) 或 urn:li:fsd_comment:(…);可从 linkedin post-comments 的 comment_urn 列直接取)`)}export const SOCIAL_PREAMBLE=String.raw`
|
|
2
|
+
const jsessionRaw = (document.cookie.split('; ').find((c) => c.startsWith('JSESSIONID=')) || '').split('=')[1] || '';
|
|
3
|
+
const csrf = jsessionRaw.replace(/^"|"$/g, '');
|
|
4
|
+
if (!csrf) return { ok: false, stage: 'auth', error: 'JSESSIONID_missing' };
|
|
5
|
+
const H = {
|
|
6
|
+
'csrf-token': csrf,
|
|
7
|
+
'accept': 'application/vnd.linkedin.normalized+json+2.1',
|
|
8
|
+
'x-restli-protocol-version': '2.0.0',
|
|
9
|
+
'x-li-lang': 'en_US',
|
|
10
|
+
'content-type': 'application/json; charset=UTF-8',
|
|
11
|
+
};
|
|
12
|
+
`;export function buildReactScript({threadUrn:q,reactionType:b,remove:z}){return`(async () => {
|
|
13
|
+
${SOCIAL_PREAMBLE}
|
|
14
|
+
const THREAD = ${JSON.stringify(q)};
|
|
15
|
+
const REMOVE = ${JSON.stringify(Boolean(z))};
|
|
16
|
+
const REACTION = ${JSON.stringify(b)};
|
|
17
|
+
if (REMOVE) {
|
|
18
|
+
const res = await fetch('/voyager/api/voyagerSocialDashReactions?action=deleteReaction', {
|
|
19
|
+
method: 'POST', credentials: 'include', headers: H,
|
|
20
|
+
body: JSON.stringify({ threadUrn: THREAD }),
|
|
21
|
+
});
|
|
22
|
+
if (res.status === 204 || res.status === 200) return { ok: true, removed: true, httpStatus: res.status };
|
|
23
|
+
return { ok: false, stage: 'unreact', httpStatus: res.status, error: (await res.text()).slice(0, 300) };
|
|
24
|
+
}
|
|
25
|
+
const res = await fetch('/voyager/api/voyagerSocialDashReactions?threadUrn=' + encodeURIComponent(THREAD), {
|
|
26
|
+
method: 'POST', credentials: 'include', headers: H,
|
|
27
|
+
body: JSON.stringify({ reactionType: REACTION }),
|
|
28
|
+
});
|
|
29
|
+
if (res.status === 201 || res.status === 200) return { ok: true, removed: false, reaction: REACTION, httpStatus: res.status };
|
|
30
|
+
return { ok: false, stage: 'react', httpStatus: res.status, error: (await res.text()).slice(0, 300) };
|
|
31
|
+
})()`}export function buildCommentScript({threadUrn:q,text:b,decorationId:z}){return`(async () => {
|
|
32
|
+
${SOCIAL_PREAMBLE}
|
|
33
|
+
const THREAD = ${JSON.stringify(q)};
|
|
34
|
+
const TEXT = ${JSON.stringify(b)};
|
|
35
|
+
const payload = {
|
|
36
|
+
commentary: { text: TEXT, attributesV2: [], $type: 'com.linkedin.voyager.dash.common.text.TextViewModel' },
|
|
37
|
+
threadUrn: THREAD,
|
|
38
|
+
};
|
|
39
|
+
const res = await fetch('/voyager/api/voyagerSocialDashNormComments?decorationId=' + ${JSON.stringify(z)}, {
|
|
40
|
+
method: 'POST', credentials: 'include', headers: H,
|
|
41
|
+
body: JSON.stringify(payload),
|
|
42
|
+
});
|
|
43
|
+
const body = await res.text();
|
|
44
|
+
if (res.status !== 201 && res.status !== 200) {
|
|
45
|
+
return { ok: false, stage: 'comment', httpStatus: res.status, error: body.slice(0, 400) };
|
|
46
|
+
}
|
|
47
|
+
// 从响应里捞新评论 urn(fsd_comment(commentId, urn:li:activity:postId) → 归一成 comment:(activity:postId,commentId))
|
|
48
|
+
let commentUrn = '';
|
|
49
|
+
const m = body.match(/urn:li:fsd_comment:\\((\\d+),urn:li:(activity|ugcPost):(\\d+)\\)/);
|
|
50
|
+
if (m) commentUrn = 'urn:li:comment:(' + m[2] + ':' + m[3] + ',' + m[1] + ')';
|
|
51
|
+
return { ok: true, commentUrn, httpStatus: res.status };
|
|
52
|
+
})()`}export function buildPostDetailScript({activityUrn:q}){return`(async () => {
|
|
53
|
+
${SOCIAL_PREAMBLE}
|
|
54
|
+
const ACTIVITY = ${JSON.stringify(q)};
|
|
55
|
+
const res = await fetch('/voyager/api/feed/updates/' + encodeURIComponent(ACTIVITY) + '?commentsCount=0&likesCount=0', { credentials: 'include', headers: H });
|
|
56
|
+
if (!res.ok) return { ok: false, stage: 'read', httpStatus: res.status, error: (await res.text()).slice(0, 300) };
|
|
57
|
+
const j = await res.json();
|
|
58
|
+
const inc = j.included || [];
|
|
59
|
+
const uv2 = inc.find((e) => /feed\\.render\\.UpdateV2$/.test(e.$type || '')) || {};
|
|
60
|
+
const actor = uv2.actor || {};
|
|
61
|
+
const commentary = uv2.commentary || {};
|
|
62
|
+
const sac = inc.find((e) => /SocialActivityCounts$/.test(e.$type || '')) || {};
|
|
63
|
+
const content = uv2.content || {};
|
|
64
|
+
const actorName = (actor.name && (actor.name.text || actor.name)) || '';
|
|
65
|
+
const actorUrl = (actor.navigationContext && actor.navigationContext.actionTarget) || '';
|
|
66
|
+
const actorType = /company/i.test(actor.urn || '') ? 'company' : (/member|fsd_profile|miniProfile/i.test(actor.urn || actor['*miniProfile'] || '') ? 'person' : '');
|
|
67
|
+
const text = (commentary.text && (commentary.text.text
|
|
68
|
+
|| (Array.isArray(commentary.text.values) ? commentary.text.values.map((v) => v.value || '').join('') : ''))) || '';
|
|
69
|
+
const perm = (inc.find((e) => e.permalink) || {}).permalink || '';
|
|
70
|
+
return {
|
|
71
|
+
ok: true,
|
|
72
|
+
post: {
|
|
73
|
+
post_urn: ACTIVITY,
|
|
74
|
+
author: actorName,
|
|
75
|
+
author_type: actorType,
|
|
76
|
+
author_url: actorUrl,
|
|
77
|
+
posted: (actor.subDescription && (actor.subDescription.text || actor.subDescription)) || '',
|
|
78
|
+
text,
|
|
79
|
+
content_type: (content.$type || '').split('.').pop(),
|
|
80
|
+
is_repost: Boolean(uv2.resharedUpdate) ? 'yes' : '',
|
|
81
|
+
reactions: sac.numLikes || 0,
|
|
82
|
+
comments: sac.numComments || 0,
|
|
83
|
+
reposts: sac.numShares || 0,
|
|
84
|
+
impressions: sac.numImpressions || 0,
|
|
85
|
+
url: perm,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
})()`}export function buildReadCommentsScript({activityUrn:q,count:b,start:z,sortOrder:D}){return`(async () => {
|
|
89
|
+
${SOCIAL_PREAMBLE}
|
|
90
|
+
const ACTIVITY = ${JSON.stringify(q)};
|
|
91
|
+
const url = '/voyager/api/feed/comments?count=' + ${JSON.stringify(b)} + '&start=' + ${JSON.stringify(z)}
|
|
92
|
+
+ '&q=comments&sortOrder=' + ${JSON.stringify(D)} + '&updateId=' + encodeURIComponent(ACTIVITY);
|
|
93
|
+
const res = await fetch(url, { credentials: 'include', headers: H });
|
|
94
|
+
if (!res.ok) return { ok: false, stage: 'read', httpStatus: res.status, error: (await res.text()).slice(0, 300) };
|
|
95
|
+
const j = await res.json();
|
|
96
|
+
const inc = j.included || [];
|
|
97
|
+
const comments = inc.filter((e) => /\\.feed\\.Comment$/.test(e.$type || ''));
|
|
98
|
+
// MiniProfile 名字表:entityUrn → 全名
|
|
99
|
+
const minis = {};
|
|
100
|
+
for (const e of inc) {
|
|
101
|
+
if (/MiniProfile$/.test(e.$type || '')) {
|
|
102
|
+
minis[e.entityUrn] = ((e.firstName || '') + ' ' + (e.lastName || '')).trim();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// 评论级点赞数:SocialActivityCounts 按 urn 里的 commentId 匹配
|
|
106
|
+
const countsByComment = {};
|
|
107
|
+
for (const e of inc) {
|
|
108
|
+
if (/SocialActivityCounts$/.test(e.$type || '') && /comment/i.test(e.urn || e.entityUrn || '')) {
|
|
109
|
+
countsByComment[e.urn || e.entityUrn] = e.numLikes || 0;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const rows = comments.map((c) => {
|
|
113
|
+
const commenterRef = c['*commenter'] || (c.commenter && c.commenter['*miniProfile']) || '';
|
|
114
|
+
const author = minis[commenterRef] || (c.commenter && c.commenter.urn) || '';
|
|
115
|
+
const text = c.comment && Array.isArray(c.comment.values)
|
|
116
|
+
? c.comment.values.map((v) => v.value || '').join('')
|
|
117
|
+
: (c.comment && c.comment.text) || '';
|
|
118
|
+
const isReply = /replyUrn|dashReplyUrn/.test(c.permalink || '');
|
|
119
|
+
const sdRef = c['*socialDetail'] || '';
|
|
120
|
+
let reactions = 0;
|
|
121
|
+
for (const k in countsByComment) { if (sdRef && k && sdRef.indexOf(k.split(':').pop()) >= 0) { reactions = countsByComment[k]; break; } }
|
|
122
|
+
return {
|
|
123
|
+
comment_urn: c.urn || '',
|
|
124
|
+
author,
|
|
125
|
+
headline: (c.headline && (c.headline.text || c.headline)) || '',
|
|
126
|
+
text,
|
|
127
|
+
created_at: c.createdTime || 0,
|
|
128
|
+
is_reply: isReply,
|
|
129
|
+
parent_comment_urn: c.parentCommentUrn || '',
|
|
130
|
+
reactions,
|
|
131
|
+
permalink: c.permalink || '',
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
const total = (j.data && j.data.paging && j.data.paging.total) || rows.length;
|
|
135
|
+
return { ok: true, rows, total };
|
|
136
|
+
})()`}export function buildCommentReactScript({commentUrn:q,reactionType:b,remove:z,createQid:D,deleteQid:G}){return`(async () => {
|
|
137
|
+
${SOCIAL_PREAMBLE}
|
|
138
|
+
const GH = Object.assign({}, H, { 'x-li-graphql-pegasus-client': 'true' });
|
|
139
|
+
const COMMENT = ${JSON.stringify(q)};
|
|
140
|
+
const REMOVE = ${JSON.stringify(Boolean(z))};
|
|
141
|
+
const REACTION = ${JSON.stringify(b)};
|
|
142
|
+
if (REMOVE) {
|
|
143
|
+
const qid = ${JSON.stringify(G)};
|
|
144
|
+
const res = await fetch('/voyager/api/graphql?action=execute&queryId=' + qid, {
|
|
145
|
+
method: 'POST', credentials: 'include', headers: GH,
|
|
146
|
+
body: JSON.stringify({ variables: { threadUrn: COMMENT }, queryId: qid, includeWebMetadata: true }),
|
|
147
|
+
});
|
|
148
|
+
if (res.status === 200 || res.status === 201) return { ok: true, removed: true };
|
|
149
|
+
return { ok: false, stage: 'unreact_comment', httpStatus: res.status, error: (await res.text()).slice(0, 300) };
|
|
150
|
+
}
|
|
151
|
+
const qid = ${JSON.stringify(D)};
|
|
152
|
+
const res = await fetch('/voyager/api/graphql?action=execute&queryId=' + qid, {
|
|
153
|
+
method: 'POST', credentials: 'include', headers: GH,
|
|
154
|
+
body: JSON.stringify({ variables: { entity: { reactionType: REACTION }, threadUrn: COMMENT }, queryId: qid, includeWebMetadata: true }),
|
|
155
|
+
});
|
|
156
|
+
if (res.status === 200 || res.status === 201) return { ok: true, removed: false, reaction: REACTION };
|
|
157
|
+
return { ok: false, stage: 'react_comment', httpStatus: res.status, error: (await res.text()).slice(0, 300) };
|
|
158
|
+
})()`}export function buildRepostScript({activityUrn:q,commentary:b,repostQid:z}){return`(async () => {
|
|
159
|
+
${SOCIAL_PREAMBLE}
|
|
160
|
+
const ACTIVITY = ${JSON.stringify(q)};
|
|
161
|
+
const COMMENTARY = ${JSON.stringify(b||"")};
|
|
162
|
+
// 1. 解析 shareUrn
|
|
163
|
+
const u = await fetch('/voyager/api/feed/updates/' + encodeURIComponent(ACTIVITY) + '?commentsCount=0&likesCount=0', { credentials: 'include', headers: H });
|
|
164
|
+
if (!u.ok) return { ok: false, stage: 'resolve', httpStatus: u.status, error: (await u.text()).slice(0, 200) };
|
|
165
|
+
const j = await u.json();
|
|
166
|
+
const uv2 = (j.included || []).find((e) => /feed\\.render\\.UpdateV2$/.test(e.$type || '')) || {};
|
|
167
|
+
const shareUrn = (uv2.updateMetadata && uv2.updateMetadata.shareUrn) || '';
|
|
168
|
+
if (!shareUrn) return { ok: false, stage: 'resolve', error: 'shareUrn_not_found' };
|
|
169
|
+
// 2a. 带评语 → normShares + resharedUpdate
|
|
170
|
+
if (COMMENTARY) {
|
|
171
|
+
const payload = { visibleToConnectionsOnly: false, externalAudienceProviders: [], commentaryV2: { text: COMMENTARY, attributes: [] }, origin: 'FEED', allowedCommentersScope: 'ALL', postState: 'PUBLISHED', resharedUpdate: shareUrn };
|
|
172
|
+
const s = await fetch('/voyager/api/contentcreation/normShares', { method: 'POST', credentials: 'include', headers: H, body: JSON.stringify(payload) });
|
|
173
|
+
const st = await s.text();
|
|
174
|
+
if (s.status !== 200 && s.status !== 201) return { ok: false, stage: 'quote', httpStatus: s.status, error: st.slice(0, 300) };
|
|
175
|
+
const m = st.match(/urn:li:(share|ugcPost|activity):[0-9]+/g) || [];
|
|
176
|
+
const nu = [...new Set(m)].filter((x) => x !== shareUrn);
|
|
177
|
+
return { ok: true, quote: true, shared_urn: shareUrn, repost_urn: nu.find((x) => /activity/.test(x)) || nu[0] || '' };
|
|
178
|
+
}
|
|
179
|
+
// 2b. 纯转发 → GraphQL Reposts
|
|
180
|
+
const GH = Object.assign({}, H, { 'x-li-graphql-pegasus-client': 'true' });
|
|
181
|
+
const qid = ${JSON.stringify(z)};
|
|
182
|
+
const r = await fetch('/voyager/api/graphql?action=execute&queryId=' + qid, {
|
|
183
|
+
method: 'POST', credentials: 'include', headers: GH,
|
|
184
|
+
body: JSON.stringify({ variables: { entity: { rootContentUrn: shareUrn } }, queryId: qid, includeWebMetadata: true }),
|
|
185
|
+
});
|
|
186
|
+
const rt = await r.text();
|
|
187
|
+
if (r.status !== 200 && r.status !== 201) return { ok: false, stage: 'repost', httpStatus: r.status, error: rt.slice(0, 300) };
|
|
188
|
+
// 新转发 urn:优先响应里的 activity urn,回退到 createFeedDashReposts.resourceKey(instantRepost)。
|
|
189
|
+
const act = (rt.match(/urn:li:activity:[0-9]+/g) || [])[0] || '';
|
|
190
|
+
const rk = (rt.match(/urn:li:fsd_repost:[^"\\\\]+/) || [])[0] || '';
|
|
191
|
+
return { ok: true, quote: false, shared_urn: shareUrn, repost_urn: act || rk };
|
|
192
|
+
})()`}export function buildNotificationsScript({count:q,start:b,filter:z}){return`(async () => {
|
|
193
|
+
${SOCIAL_PREAMBLE}
|
|
194
|
+
const url = '/voyager/api/voyagerIdentityDashNotificationCards?q=filterVanityName&filterVanityName=' + ${JSON.stringify(z)}
|
|
195
|
+
+ '&count=' + ${JSON.stringify(q)} + '&start=' + ${JSON.stringify(b)};
|
|
196
|
+
const res = await fetch(url, { credentials: 'include', headers: H });
|
|
197
|
+
if (!res.ok) return { ok: false, stage: 'read', httpStatus: res.status, error: (await res.text()).slice(0, 300) };
|
|
198
|
+
const j = await res.json();
|
|
199
|
+
const inc = j.included || [];
|
|
200
|
+
const cards = inc.filter((e) => /notifications\\.Card$/.test(e.$type || ''));
|
|
201
|
+
const textOf = (f) => { if (!f) return ''; if (typeof f === 'string') return f; if (f.text) return f.text; if (f.attributedText && f.attributedText.text) return f.attributedText.text; return ''; };
|
|
202
|
+
const rows = cards.map((c) => {
|
|
203
|
+
const target = (c.cardAction && (c.cardAction.actionTarget
|
|
204
|
+
|| (c.cardAction.navigationContext && c.cardAction.navigationContext.actionTarget))) || '';
|
|
205
|
+
return {
|
|
206
|
+
read: c.read === true,
|
|
207
|
+
headline: textOf(c.headline),
|
|
208
|
+
content: Array.isArray(c.contentPrimaryText) ? textOf(c.contentPrimaryText[0]) : textOf(c.contentPrimaryText),
|
|
209
|
+
when: c.publishedAt || 0,
|
|
210
|
+
target,
|
|
211
|
+
};
|
|
212
|
+
});
|
|
213
|
+
const meta = j.data && j.data.metadata;
|
|
214
|
+
return { ok: true, rows, numUnseen: (meta && meta.numUnseen) || 0, nextStart: (meta && meta.nextStart) || 0 };
|
|
215
|
+
})()`}export function buildFollowScript({target:q,following:b}){return`(async () => {
|
|
216
|
+
${SOCIAL_PREAMBLE}
|
|
217
|
+
const TARGET = ${JSON.stringify(String(q||"").trim())};
|
|
218
|
+
const FOLLOWING = ${JSON.stringify(Boolean(b))};
|
|
219
|
+
|
|
220
|
+
async function resolveEntityUrn() {
|
|
221
|
+
// 1. 原生 urn 直接用
|
|
222
|
+
const rawProfile = TARGET.match(/urn:li:fsd?_?profile:([A-Za-z0-9_-]+)/) || TARGET.match(/urn:li:fs_miniProfile:([A-Za-z0-9_-]+)/);
|
|
223
|
+
if (rawProfile) return 'urn:li:fsd_profile:' + rawProfile[1];
|
|
224
|
+
const rawCompany = TARGET.match(/urn:li:fs?d?_?(?:normalized_)?company:(\\d+)/);
|
|
225
|
+
if (rawCompany) return 'urn:li:fsd_company:' + rawCompany[1];
|
|
226
|
+
// 2. /in/<handle> → 人(memberIdentity 解析 fsd_profile)
|
|
227
|
+
const inMatch = TARGET.match(/linkedin\\.com\\/in\\/([^/?#]+)/i);
|
|
228
|
+
if (inMatch) {
|
|
229
|
+
const handle = decodeURIComponent(inMatch[1]);
|
|
230
|
+
const r = await fetch('/voyager/api/identity/dash/profiles?q=memberIdentity&memberIdentity=' + encodeURIComponent(handle), { credentials: 'include', headers: H });
|
|
231
|
+
if (!r.ok) return '';
|
|
232
|
+
const j = await r.json();
|
|
233
|
+
const p = (j.included || []).find((e) => /dash\\.identity\\.profile\\.Profile$/.test(e.$type || '')) || {};
|
|
234
|
+
return p.entityUrn || ((j.data && j.data.elements && j.data.elements[0]) || '');
|
|
235
|
+
}
|
|
236
|
+
// 3. /company/<name> URL 或裸公司名 → 公司(universalName 解析 fsd_company)
|
|
237
|
+
// 裸字符串一律按公司名解析(人有歧义,必须给 /in/ URL),避免误把公司名当成 member handle。
|
|
238
|
+
const companyMatch = TARGET.match(/linkedin\\.com\\/company\\/([^/?#]+)/i);
|
|
239
|
+
const name = companyMatch ? decodeURIComponent(companyMatch[1]) : (/^[^/\\s]+$/.test(TARGET) ? TARGET : '');
|
|
240
|
+
if (name) {
|
|
241
|
+
const r = await fetch('/voyager/api/organization/companies?q=universalName&universalName=' + encodeURIComponent(name), { credentials: 'include', headers: H });
|
|
242
|
+
if (!r.ok) return '';
|
|
243
|
+
const j = await r.json();
|
|
244
|
+
const c = (j.included || []).find((e) => /organization\\.Company$/.test(e.$type || '')) || {};
|
|
245
|
+
const m = (c.entityUrn || '').match(/company:(\\d+)/);
|
|
246
|
+
return m ? 'urn:li:fsd_company:' + m[1] : '';
|
|
247
|
+
}
|
|
248
|
+
return '';
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const entityUrn = await resolveEntityUrn();
|
|
252
|
+
if (!entityUrn) return { ok: false, stage: 'resolve', error: 'could_not_resolve_target' };
|
|
253
|
+
const fsUrn = 'urn:li:fsd_followingState:' + entityUrn;
|
|
254
|
+
const res = await fetch('/voyager/api/feed/dash/followingStates/' + encodeURIComponent(fsUrn), {
|
|
255
|
+
method: 'POST', credentials: 'include', headers: H,
|
|
256
|
+
body: JSON.stringify({ patch: { $set: { following: FOLLOWING } } }),
|
|
257
|
+
});
|
|
258
|
+
if (res.status === 200 || res.status === 201) return { ok: true, entity_urn: entityUrn, following: FOLLOWING };
|
|
259
|
+
return { ok: false, stage: 'follow', httpStatus: res.status, error: (await res.text()).slice(0, 300) };
|
|
260
|
+
})()`}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{sleep as
|
|
1
|
+
import{sleep as L}from"../utils.js";import{BrowserConnectError as D}from"../errors.js";import{classifyBrowserError as S}from"./errors.js";import{resolveProfileContextId as M}from"./profile.js";import{DEFAULT_BROWSER_CONNECT_TIMEOUT as $}from"./config.js";import{ensureBrowserBridgeReady as H}from"./daemon-lifecycle.js";import{isPreDispatchError as O}from"./bridge-readiness.js";import{fetchDaemonStatus,getDaemonHealth,requestDaemon as _,requestDaemonShutdown}from"./daemon-transport.js";let g=0;function N(){return`cmd_${process.pid}_${Date.now()}_${++g}`}export class BrowserCommandError extends Error{code;hint;constructor(K,J,Q){super(K);this.code=J;this.hint=Q;this.name="BrowserCommandError"}}export{fetchDaemonStatus,getDaemonHealth,requestDaemonShutdown};async function f(K,J){const Q=4;let X=!1,P=!1,Y=!1;const b=K==="navigate";let Z=!1;for(let F=1;F<=Q;F++){const j=N(),q=process.env.OPENCLI_WINDOW,x=q==="foreground"||q==="background"?q:void 0,V=J.contextId??M(),k=J.windowMode??x,W={id:j,action:K,...J,...V&&{contextId:V},...k&&{windowMode:k}};try{const G=await _("/command",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(W),timeout:30000}),z=await G.json();if(z.ok)return z;if(z.errorCode==="command_result_unknown"){if(b&&!Z){Z=!0;await H({timeoutSeconds:$,contextId:V,verbose:!1});continue}throw new BrowserCommandError(z.error??"Browser command result is unknown",z.errorCode,z.errorHint)}if(!X&&O(z.errorCode)){X=!0;await H({timeoutSeconds:$,contextId:V,verbose:!1});continue}if(G.status===409&&!z.errorCode&&(z.error??"").includes("Duplicate command id")&&!P){P=!0;continue}const A=S(Error(z.error??""));if(A.retryable&&!Y){Y=!0;await L(A.delayMs);continue}throw new BrowserCommandError(z.error??"Daemon command failed",z.errorCode,z.errorHint)}catch(G){if(G instanceof BrowserCommandError||G instanceof D)throw G;if(G instanceof Error&&G.name==="AbortError"){if(b&&!Z){Z=!0;await H({timeoutSeconds:$,contextId:V,verbose:!1});continue}throw new BrowserCommandError("Browser command timed out client-side; the page may still have applied it.","command_result_unknown","Inspect the page state before retrying. Idempotent reads are safe to retry; non-idempotent writes may have already happened.")}if(!X&&G instanceof TypeError){X=!0;await H({timeoutSeconds:$,contextId:V,verbose:!1});continue}if(G instanceof Error){const z=S(G);if(z.retryable&&!Y){Y=!0;await L(z.delayMs);continue}}throw G}}throw new BrowserCommandError("sendCommand: max attempts exhausted","max_attempts_exhausted")}export async function sendCommand(K,J={}){return(await f(K,J)).data}export async function sendCommandFull(K,J={}){const Q=await f(K,J);return{data:Q.data,page:Q.page}}export async function bindTab(K,J={}){return sendCommand("bind",{session:K,surface:"browser",...J})}
|