重开仓库内容:改推五个技能(browser-harness / humanizer / humanizer-zh / product-planning / session-mechanism)
按授权清空原有内容后重新提交(原 oil-ui-pro 一并移出,可从历史恢复)。 browser-harness 剔除 .venv 等运行环境;根 .gitignore 补记 .venv/ 与 node_modules/。
This commit is contained in:
1 parent
9a152a1952
commit
777f7fe5d0
240 files changed
+52518
-4409
No files matched your search
@@ -0,0 +1,109 @@
|
||||
# LinkedIn — Invitation Manager
|
||||
|
||||
Accept or ignore pending connection invitations in bulk from
|
||||
`https://www.linkedin.com/mynetwork/invitation-manager/received/<FILTER>/`.
|
||||
|
||||
## URL filters
|
||||
|
||||
The trailing slug pre-filters the received invitations. Observed slugs:
|
||||
|
||||
- `PEOPLE_WITH_MUTUAL_CONNECTION` — people who share a mutual connection
|
||||
- `PEOPLE_WITH_MUTUAL_SCHOOL` — people who share a school
|
||||
- omit the slug (`.../received/`) for all pending invitations
|
||||
|
||||
The filter chip at the top of the page mirrors the URL and also renders
|
||||
`All (N)`, `Mutual Connections (N)`, `Your School (N)` — the `(N)` is the
|
||||
authoritative remaining-count for the active filter and is what you loop on.
|
||||
|
||||
## Button selectors
|
||||
|
||||
Each pending-invitation card contains an Accept and an Ignore control.
|
||||
**The aria-label formats are different** for the two buttons — don't derive
|
||||
one from the other:
|
||||
|
||||
- Accept: `aria-label = "Accept <Name>'s invitation"` (note: curly `’`, not ASCII `'`)
|
||||
- Ignore: `aria-label = "Ignore an invitation to connect from <Name>"`
|
||||
|
||||
```python
|
||||
# Match either — both are unique per card
|
||||
accepts = js("Array.from(document.querySelectorAll('button, a')).filter(b => (b.getAttribute('aria-label')||'').startsWith('Accept ')).length")
|
||||
ignores = js("Array.from(document.querySelectorAll('button')).filter(b => (b.getAttribute('aria-label')||'').toLowerCase().startsWith('ignore')).length")
|
||||
```
|
||||
|
||||
## Trap: "follows you" cards render Accept as `<a>`, not `<button>`
|
||||
|
||||
For invitations labeled `<Name> follows you and is inviting you to connect`
|
||||
(typically Premium users' auto-invites), the Accept control is an `<a href>`,
|
||||
not a `<button>` — and the `href` points at the **current page URL**.
|
||||
|
||||
`<a>.click()` follows the href → same-URL soft-nav → accept never fires.
|
||||
Dispatched `MouseEvent`s and coordinate `Input.dispatchMouseEvent` clicks
|
||||
also land on the element (you can see the focus ring appear) but do not
|
||||
trigger the underlying accept handler. **There is no known way to accept
|
||||
these via CDP.** Click the Ignore button instead (Ignore is always a
|
||||
`<button>` and works with a normal coordinate click), or skip the row.
|
||||
|
||||
Detect with `element.tagName === 'A'` on the Accept element.
|
||||
|
||||
```python
|
||||
# In your extractor, capture the tag so downstream logic can route these
|
||||
rows = js(r"""
|
||||
(() => {
|
||||
const accepts = Array.from(document.querySelectorAll('button, a'))
|
||||
.filter(b => (b.getAttribute('aria-label')||'').startsWith('Accept ') && !b.disabled);
|
||||
return accepts.map(a => ({aria: a.getAttribute('aria-label'), tag: a.tagName}));
|
||||
})()
|
||||
""")
|
||||
```
|
||||
|
||||
## Pagination — reload, don't scroll
|
||||
|
||||
The list only renders ~10 cards at a time. After you click Accept on the
|
||||
visible batch, LinkedIn replaces the pending section with a "X is now a
|
||||
connection" acknowledgment list + "Suggestions for you" — the next batch of
|
||||
pending invites does **not** auto-mount. Window-scroll does not trigger
|
||||
lazy-load either.
|
||||
|
||||
Pattern:
|
||||
|
||||
1. Navigate to the filter URL, `wait_for_load()`, sleep ~2.5s.
|
||||
2. Extract visible rows, decide, click Accept/Ignore for each (`.click()` via
|
||||
JS works for `<button>` Accept and Ignore; coordinate click via
|
||||
`Input.dispatchMouseEvent` also works).
|
||||
3. Reload the URL (`cdp("Page.navigate", url=...)`). Do **not** rely on
|
||||
scrolling or clicking a "show more" control.
|
||||
4. Repeat until the filter chip shows `(0)` or no Accept buttons remain.
|
||||
|
||||
Chip count decreases by the number of successful accepts + ignores per
|
||||
cycle — use it as the loop guard.
|
||||
|
||||
## Safety modal: "Take care when connecting"
|
||||
|
||||
LinkedIn occasionally interposes a `"Take care when connecting"` dialog
|
||||
when you click Accept on a connection it considers unfamiliar. The dialog
|
||||
has `View profile` and `Accept invite` buttons — click `Accept invite` to
|
||||
proceed. Watch for it between accepts; it's intermittent, not per-row.
|
||||
|
||||
## Quick sketch
|
||||
|
||||
```python
|
||||
import time
|
||||
|
||||
def chip():
|
||||
return js(r"""(() => {
|
||||
const el = Array.from(document.querySelectorAll('button, a')).map(e => (e.textContent||'').trim())
|
||||
.find(t => /^Mutual Connections \(/.test(t));
|
||||
return el || '';
|
||||
})()""")
|
||||
|
||||
while True:
|
||||
cdp("Page.navigate", url="https://www.linkedin.com/mynetwork/invitation-manager/received/PEOPLE_WITH_MUTUAL_CONNECTION/")
|
||||
wait_for_load()
|
||||
time.sleep(2.5)
|
||||
n = int(js(r"""(() => Array.from(document.querySelectorAll('button, a'))
|
||||
.filter(b => (b.getAttribute('aria-label')||'').startsWith('Accept ') && !b.disabled).length)()"""))
|
||||
if n == 0:
|
||||
break
|
||||
# click each Accept (route tag === 'A' rows to Ignore — see trap above)
|
||||
...
|
||||
```
|
||||
Reference in new issue
Block a user