用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/KunanonJ/ai-skills-hub --skill aside-site-linkedin命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
正在显示 SKILL.md
基于 SOC 职业分类
| name | aside-site-linkedin |
| description | Read this when you need to use LinkedIn. |
| metadata | {"version":"0.1.0"} |
Use the linkedin global in the REPL tool. It allows you to control the LinkedIn website with API interface- no tab open needed.
// Current viewer profile. Response shape is Voyager-native:
// { data: { plainId, '*miniProfile' }, included: [MiniProfile, ...] }
const me = await linkedin.getMe();
const miniProfile = me.included?.find((item) => item.$type === 'com.linkedin.voyager.identity.shared.MiniProfile');
console.log(miniProfile?.publicIdentifier, me.data?.plainId);
// Public profile lookup (public identifier or full profile URL)
const profile = await linkedin.getProfile('johndoe');
console.log(profile.fullName, profile.headline);
// Search people / companies
const people = await linkedin.searchPeople('software engineer at openai');
console.log(people.results.map((item) => item.title));
const companies = await linkedin.searchCompanies('openai');
console.log(companies.results.map((item) => item.title));
// Company / job / posts
const company = await linkedin.getCompany('microsoft');
const job = await linkedin.getJob('4242424242');
const posts = await linkedin.getUserPosts('johndoe');
// Messaging — inbox + conversation history (paginate by timestamp, not offset)
const inbox = await linkedin.getInbox();
const convo = await linkedin.getConversation(inbox.conversations[0].threadId);
// Reply to an existing thread
await linkedin.sendMessage({ threadId: inbox.conversations[0].threadId, text: 'Hey!' });
// Start a new thread (accepts public identifiers OR profile URNs)
await linkedin.sendMessage({ recipients: ['johndoe'], text: 'Hi, nice to meet you.' });
// Connection requests
await linkedin.sendInvitation({ identifier: 'johndoe', customMessage: 'Would love to connect.' });
const received = await linkedin.getReceivedInvitations();
if (received.invitations[0]) {
await linkedin.acceptInvitation(received.invitations[0]);
// or: linkedin.ignoreInvitation(received.invitations[0])
}
// Withdrawing a previously-sent invitation (URN captured from sendInvitation's response)
// await linkedin.withdrawInvitation(invitationUrn);
// If LinkedIn rotates the session cookies:
linkedin.invalidateCache();
linkedin.getMe(): Promise<object>Fetch the authenticated viewer from /voyager/api/me. Returns the raw Voyager
payload { data: { plainId, '*miniProfile', ... }, included: [MiniProfile, ...] };
the viewer's publicIdentifier, firstName, lastName, etc. live on the
MiniProfile entry inside included.
linkedin.getProfile(identifier: string): Promise<LinkedInProfile>Get a public profile by LinkedIn public identifier or profile URL.
interface LinkedInProfile {
entityUrn?: string;
publicIdentifier?: string;
firstName?: string;
lastName?: string;
fullName: string;
headline?: string;
summary?: string;
location?: string;
industryName?: string;
occupation?: string;
profilePicture?: string;
backgroundPicture?: string;
experience: Array<{
title?: string;
companyName?: string;
description?: string;
location?: string;
startDate?: string;
endDate?: string;
}>;
education: Array<{
schoolName?: string;
degreeName?: string;
fieldOfStudy?: string;
startDate?: string;
endDate?: string;
}>;
raw: object;
}
linkedin.searchPeople(query: string, opts?: { offset?: number, limit?: number }): Promise<LinkedInSearchResponse>Search people results through Voyager search.
linkedin.searchCompanies(query: string, opts?: { offset?: number, limit?: number }): Promise<LinkedInSearchResponse>Search company results through Voyager search.
interface LinkedInSearchResponse {
paging: { offset: number; count: number; total: number };
results: Array<{
entityUrn: string;
title: string;
headline?: string;
subline?: string;
summary?: string;
navigationUrl?: string;
image?: string;
type?: string;
distance?: string;
}>;
}
linkedin.getCompany(slug: string): Promise<object | undefined>Fetch a company by its LinkedIn public slug (for example microsoft).
linkedin.getJob(jobId: string): Promise<object>Fetch a job posting by numeric job ID or urn:li:jobPosting:*.
linkedin.getUserPosts(identifier: string, opts?: { start?: number, count?: number }): Promise<LinkedInPost[]>Fetch posts authored by a user.
interface LinkedInPost {
urn?: string;
postUrl?: string;
text?: string;
authorName?: string;
authorHeadline?: string;
publishedAt?: string;
commentCount?: number;
likeCount?: number;
shareCount?: number;
}
linkedin.getInbox(opts?: { createdBefore?: number }): Promise<LinkedInInboxResponse>List the viewer's messaging inbox. LinkedIn paginates by timestamp cursor, not
offset: pass the previous page's nextCreatedBefore back in as createdBefore to
fetch older conversations. The response is intentionally compact: no raw Voyager dump,
just practical thread metadata plus a parsed preview of the latest message.
interface LinkedInParticipant {
profileUrn?: string;
profileUrl?: string;
fullName?: string;
headline?: string;
distance?: string;
verified?: boolean;
isSelf?: true;
}
type LinkedInMessageAttachmentKind =
| 'audio'
| 'conversation_ad'
| 'external_media'
| 'file'
| 'forwarded_message'
| 'image'
| 'inmail'
| 'message_ad'
| 'replied_message'
| 'unavailable'
| 'video'
| 'video_meeting';
interface LinkedInConversationMessagePreview {
messageId?: string;
sender?: LinkedInParticipant;
subject?: string;
text?: string;
sentAt?: string; // ISO string
format?: string;
attachmentKinds: LinkedInMessageAttachmentKind[];
}
interface LinkedInConversation {
threadId?: string;
?: ;
?: ;
?: ;
: [];
?: ;
?: ;
?: ;
?: ;
?: ;
: [];
?: ;
?: ;
: ;
?: ;
}
{
?: ;
: [];
}
linkedin.getConversation(threadIdOrConversationUrn: string, opts?: { createdBefore?: number }): Promise<LinkedInConversationResponse>Fetch messages in a single thread, newest first. You can pass either the full
conversation URN or the inbox threadId. Same timestamp-cursor pagination.
interface LinkedInTextLink {
url: string;
text?: string;
}
interface LinkedInMessageAttachmentAction {
label?: string;
type: 'external_website' | 'human_handoff' | 'lead_gen' | 'not_interested';
url?: string;
leadGenFormUrn?: string;
}
interface LinkedInMessageAttachment {
kind: LinkedInMessageAttachmentKind;
title?: string;
text?: string;
url?: string;
previewImageUrl?: string;
mediaType?: string;
sizeBytes?: number;
assetUrn?: string;
hostProfileUrn?: string;
inmailType?: string;
advertiserLabel?: string;
campaignUrn?: string;
status?: string;
actions?: LinkedInMessageAttachmentAction[];
}
interface LinkedInMessage {
messageId?: ;
?: ;
?: ;
?: ;
?: ;
?: ;
?: ;
: [];
: [];
}
{
: [];
?: ;
}
linkedin.sendMessage(opts: { threadId?: string; recipients?: string[]; text: string }): Promise<LinkedInSendMessageResult>Send a direct message. Provide EITHER threadId (reply to an existing thread)
OR recipients (array of public identifiers or profile URNs — starts a new thread).
Returns the new message ID plus thread identifier.
linkedin.subscribeMessages(onMessage, opts?: { pollIntervalMs?: number }): Promise<void>Poll-based subscription to new messages across all conversations. Calls onMessage(msg, conversation) once per new message. Returns when the REPL run aborts. Defaults to a
20s poll interval (minimum 5s — shorter intervals trigger throttling fast). LinkedIn's
SSE realtime/connect endpoint requires session-bound headers that are not recoverable
from cookies alone, so polling is the idiomatic approach here.
linkedin.sendInvitation(opts: { identifier: string; customMessage?: string }): Promise<object>Send a connection request. identifier can be a public identifier (johndoe), profile
URL, or urn:li:fsd_profile:* URN. customMessage is capped at 300 characters and
is Premium-only as of 2024 — free accounts that pass a note silently drop it.
linkedin.getReceivedInvitations(opts?: { start?: number; count?: number }): Promise<LinkedInInvitationListResponse>List pending invitations the viewer has received.
interface LinkedInInvitation {
entityUrn?: string;
invitationId?: string;
sharedSecret?: string; // required to accept/ignore — echo back unchanged
type?: 'sent' | 'received';
message?: string;
sentAt?: number;
counterpart: { profileUrn?: string; publicIdentifier?: string; firstName?: string; lastName?: string; headline?: string };
raw: object;
}
linkedin.acceptInvitation(invitation: LinkedInInvitation | string, sharedSecret?: string): Promise<void>Accept a received invitation. Pass the invitation object from getReceivedInvitations
directly (it carries the required sharedSecret). If you pass a string URN/id, you
MUST also provide sharedSecret as the second argument.
linkedin.ignoreInvitation(invitation: LinkedInInvitation | string, sharedSecret?: string): Promise<void>Reject a received invitation. Same argument shape as acceptInvitation.
linkedin.withdrawInvitation(invitation: LinkedInInvitation | string): Promise<void>Withdraw a previously-sent invitation. Accepts an invitation URN/id string. Capture
the URN from sendInvitation's response payload. Listing sent invitations is not
currently supported by Voyager — scrape
https://www.linkedin.com/mynetwork/invitation-manager/sent/ via the browser tools
if you need to enumerate pending sends.
linkedin.invalidateCache(): voidClear cached LinkedIn session cookies for the current Chrome profile.
If you have to manually open a browser tab, navigate to these URLs:
https://www.linkedin.com/feedhttps://www.linkedin.com/messaging/https://www.linkedin.com/search/results/${all|people|posts|companies|products|schools}/?keywords=${query}https://www.linkedin.com/mynetwork/invitation-manager/https://www.linkedin.com/notifications/?filter=allhttps://www.linkedin.com/sales/homehttps://www.linkedin.com/sales/accounts/dashboardhttps://www.linkedin.com/sales/lists/peoplehttps://www.linkedin.com/sales/inbox/LinkedIn aggressively throttles automation. The linkedin global already enforces a
~1–1.75s per-request floor with jitter per account, which is enough for read
traffic at interactive pace but not enough to hide bulk write traffic. The risk is
not just a rate-limit: sustained abuse can trigger CAPTCHA challenges, temporary
restrictions, or a permanent ban of the user's real account.
HIGH RISK — confirm explicitly with the user, then throttle hard:
sendInvitation — LinkedIn enforces ~100 invitations/week as a hard cap.
Acceptance rate below ~20% accelerates restrictions. Default to ≤15/day and
space them 2–5 min apart with jitter.sendMessage to recipients the user has never messaged before (cold outreach).
Default to ≤20/day with 3–10 min spacing and never send identical copy.withdrawInvitation in bulk (looks like spam cleanup; same cap applies).MEDIUM RISK — fine at interactive pace, throttle bulk jobs:
sendMessage replies in existing threads (warm conversations). Cap at ≤50/day.getProfile / searchPeople at scale. ≤100/day on free; ≤300/day Premium.subscribeMessages with pollIntervalMs < 10_000.LOW RISK — ambient use is fine:
getMe, getInbox, getConversation, getReceivedInvitations, getCompany,
getJob, getUserPosts, searchCompanies.| Operation | Min spacing | Daily cap (established account) | Weekly cap |
|---|---|---|---|
sendInvitation | 2–5 min | 15–25 | 80 hard (LinkedIn: 100) |
sendMessage (cold) | 3–10 min | 20–30 | — |
sendMessage (reply) | 1–3 min | 40–50 | — |
getProfile / search | 5–15 s | 100–150 | — |
subscribeMessages poll | ≥20 s | — | — |
Multiply caps by 0.5× for accounts <30 days old or dormant for 2+ weeks (LinkedIn flags "slide & spike": inactive → sudden burst is the single strongest automation signal). Multiply by 0× for brand-new accounts: do a week of manual-only activity first.
429 → back off exponentially (1s → 2s → 4s → 8s → 16s, max 5 retries), or
honor Retry-After if present. After 3 consecutive 429s: stop for 1–2 hours.999 → IP-level block from LinkedIn's WAF. Stop immediately, wait 45s minimum.401 / 403 → session invalidated. linkedin.invalidateCache() is called
automatically, but the user must re-authenticate before retrying.200 with no expected data, or a redirect to /checkpoint/... → account is
being challenged. Stop all writes for 24–48 hours.sendMessage bodies in a batch — LinkedIn has message-similarity
ML. Vary at least the greeting line.sendInvitation or bulk sendMessage right after the user logged back in
from a new device/IP — LinkedIn scrutinizes fresh sessions for several hours.LinkedInVoyagerError during writes: on 429/999/403 you MUST
stop the whole batch, not retry the next item.For any loop that does >3 write operations (sendInvitation, cold sendMessage),
confirm the full list with the user first, cap at the daily limit above, and emit
progress via replPrint so the user can interrupt.
sleep a bit to load more posts.linkedin REPL global for profile/search/company/job/post data before falling back to DOM scraping.sendMessage, sendInvitation), read the Bot detection & throttling section above and throttle accordingly.