Reuses the existing LinkedIn shared evaluate envelope unwrap helper across general messaging commands while preserving subsystem-local copies for later batches.
Moves the duplicate Node-side LinkedIn safety URL decoder into the site shared helper while preserving page-realm copies and their browser-local contract.
Fixes#2334, #2335, and #2336.\n\nRepairs Twitter block/unblock profile-state scoping and localized block menu matching, and makes hide-reply retry from the parent conversation using only the preceding article time permalink.\n\nLocal gates: focused block/unblock/hide-reply tests 20/20, full twitter tests 531/531, typecheck, build, validate twitter, typed-error lint new=0, silent-column-drop new=0, diff-check. Hosted checks terminal green on exact head 57d1927d.
Add twitter mute-word <keyword> as a UI write command against the visible Twitter/X muted-word settings form. Confirmation only accepts click-after route transition, new success toast, or new exact muted-word row; pre-write targeting stays scoped to the settings surface.\n\nLocal gates: focused twitter write/block/unblock tests 21/21, full twitter tests 523/523, typecheck, build, validate twitter, diff-check. Hosted checks terminal green on exact head 114a7c7f.
Share duplicated Bilibili follow/unfollow relation helpers in a site-local relation module while preserving command-specific validation text and the existing utils.js mock boundary.
* fix(completion): fall back on invalid manifests
* test(completion): cover all manifest fallback paths
---------
Co-authored-by: OpenCLI-sol <opencli-sol@users.noreply.github.com>
`chatgpt image` with 2+ --image attachments could return the just-uploaded
reference thumbnails instead of the actual generated image.
isUserUploadPreview() classified an <img> as a user upload (to exclude it
from waitForChatGPTImages' before/after diff) using two signals, both
broken against ChatGPT's current DOM:
- turn.querySelector('h4')?.innerText: the heading is visually hidden, so
real Chrome's innerText resolves to '' (layout-dependent) even though
.textContent correctly reads "You said:" / "ChatGPT said:". jsdom's
innerText is always undefined, so the test suite never exercised this
path either - it happened to pass via the aria-label/alt fallback below.
- button[aria-label^="Open image:"]: ChatGPT's current label for a
multi-file attachment reads "Open image N of M: <name>", which no
longer starts with "Open image:", so this selector stopped matching.
With both signals dead, classification fell through to alt-text sniffing.
Right after upload, an attachment thumbnail's alt/aria-label haven't
populated yet, so for a poll or two every uploaded image is misclassified
as "new". waitForChatGPTImages returns as soon as two consecutive polls
agree on a URL set - long enough for that transient window to win when
multiple attachments are involved, so it can return the uploads instead of
the real result.
Fix: check the turn <section>'s own data-turn="user"|"assistant"
attribute first. It's set structurally as soon as the turn mounts, not
tied to the attachment's async metadata, so it isn't subject to the race.
Keep the heading/aria-label checks as a fallback (now using textContent
and a substring aria-label match) for markup that lacks data-turn.
Verified live against chatgpt.com: reproduced the bug with 3 reference
images, then confirmed the patched build returns exactly the one real
generated image instead of the 3 uploaded thumbnails.
Adds regression tests for both the data-turn race and the aria-label
format change; confirmed both fail against the pre-fix code.
Claude-Session: https://claude.ai/code/session_01L29nrhaeQ4W5rjNr27z47h
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* fix(xiaohongshu): scope note fields to #noteContainer
`#detail-title, .title` was queried against the whole document. A note
detail page also renders a recommendation feed whose cards each carry a
`.title`, and `querySelector` returns the first match in document order.
For a note with no title of its own (`#detail-title` absent) the selector
fell through to that feed and reported an unrelated card's title as the
note's title -- on one real note, two consecutive runs returned two
different unrelated titles while the note itself has no title at all.
Scope title/desc/author to `#noteContainer` (falling back to `document`
for older layouts). This is the same class of fix already applied to the
`.interact-container` counts a few lines below.
Also adds JSDOM regression tests for NOTE_EXTRACT_JS, following the
pattern used in clis/aibase/news.test.js.
* fix(xiaohongshu): tighten note fallback scope
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(xiaohongshu): detect composer media from document.body
opencli's currentComposerMediaCount() picked the composer root via
titleEl.closest('form, [class*=publish], ...'), but Xiaohongshu's new
React DOM renders the image/card editor in a different subtree, so the
matched root never contained the generated media and the count was
always 0. That broke the native '--card-text' (文字生成图片) flow with
'expected at least N visible media item(s), got 0'.
- Use document.body as the scan root so generated cards are found.
- Add 'image, svg' to the media selector for completeness.
This is the maintained fork of @jackwener/opencli (liuxinyea/OpenCLI).
* fix(xiaohongshu): scope text-card media count
* fix(xiaohongshu): keep publish media scan scoped
---------
Co-authored-by: liuxinye <liuxinye@zingfront.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(twitter): fail typed when a write command does not go through
* fix(twitter): preserve uncertain write outcomes
---------
Co-authored-by: OpenCLI-sol <opencli-sol@users.noreply.github.com>
* fix(chatgpt): filter user-uploaded images in Chinese UI and allow large image payloads
- chatgpt image adapter: the attachment filter in getChatGPTVisibleImageUrls
only matched the English 'Open image:' button label and English keywords
(upload/uploaded/attachment). In the Chinese ChatGPT UI the button is
labeled '打开图片:用户上传的图片' (Open image: user uploaded image) and
the image alt is empty, so user-uploaded reference images escaped the
filter and were reported as generated results (the original photo was
downloaded instead of the generated image). Add the Chinese button label
prefix and the '上传' keyword to the filter.
- daemon: raise MAX_BODY from 1 MB to 32 MB. The chatgpt image upload
fallback (base64-in-evaluate) serializes the image into the command body;
a typical 2 MB photo becomes a >1 MB base64 payload and the daemon
rejected it with a connection reset ('fetch failed').
* test(chatgpt): cover Chinese upload previews
---------
Co-authored-by: OpenCLI-sol <opencli-sol@users.noreply.github.com>
* fix(1688): extract detail images from shadow DOM with lazy-render scrolling
The product detail section lives inside the shadow root of a custom
element (v-detail-e with class html-description). Plain CSS selectors
like `.html-description img` cannot pierce shadowRoot, so the detail
group never matched any element and detail_images was always empty.
Fix by collecting all img/source elements (walking shadow roots) and
checking ancestry through the shadow host chain with closest(), plus
scrolling further and settling on the detail container so its lazy
content renders before extraction.
Adds a jsdom regression test covering shadow-root detail images,
light-DOM main gallery images, and plain-class detail containers.
* fix(1688): address review — restore deleted tests, dedupe the traversal, poll instead of sleep
Review fixes on top of the shadow-DOM detail extraction:
- Restore the two tests this PR replaced. `normalizeAssets` (grouping,
counts, blob: filtering) and `normalizeMediaUrl` both lost all coverage;
the 14 -> 15 test count hid that, since three new cases were added while
two existing ones were removed. Now 17, with the new jsdom cases
alongside the originals rather than in place of them.
- Inject the module-level `inDetailContainer` via toString() instead of
keeping a hand-copied twin inside the evaluated script. The copy meant
the unit tests exercised code that was not what ran in the page, and the
two could drift silently. This is the convention already used in
clis/gov-policy/search.js and clis/codex/sidebar.js.
- Check `node.closest(selector)` at each level of the walk, not only
`host.matches(...)`. A detail container that is a plain element inside a
shadow root rather than the host itself was previously missed.
- Replace `autoScroll(6) + autoScroll(4) + wait(3)` with one autoScroll,
a scrollIntoView, and a bounded poll on the deep detail-image count.
autoScroll keeps no state between calls, so 6+4 was identical to a
single 10 and the comment about a "second confirmation pass" described
something that did not happen; the fixed 3s wait was then paid on every
call even when the content had already rendered. The poll returns as
soon as the count is stable, capped at ~5s.
- Use an <img> rather than a <source srcset> in the shadow fixture:
defaultSrcProps does not read srcset, so asserting on it implied
coverage the adapter does not actually have.
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(extension): omit credentials from daemon ping (fixes#2278)
A large localhost cookie jar can push the extension ping past the Node default header limit. The daemon then responds 431, but the extension silently retries and never reaches the WebSocket connection.
Send the ping without credentials so browser cookies are not attached, and log non-OK HTTP statuses so future probe failures remain visible. Keep connection errors quiet because a stopped daemon is the expected idle state.
* build(extension): rebuild dist for daemon ping credentials:omit
extension/dist/background.js is a tracked artifact (.gitignore un-ignores
it via !extension/dist/), so the source-only change in 62d1f202 never
reached the bundle Chrome actually loads — the #2278 431 wedge would have
persisted in production despite the fix being merged.
Rebuild only; no source change. Diff is exactly the credentials:'omit'
and the non-OK warn from the parent commit.
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(boss): read current search and detail pages
* fix(boss): harden read-only job discovery
* fix(boss): address review — drop the site-auth fork, flatten detail columns
Review fixes on top of the read-only search/detail restore:
- Drop the adapter-local fork of `_shared/site-auth.js`. The fork had
already diverged — it lost `normalizeRefreshResult` and the
`config.refresh` branch, which silently removes `opencli auth refresh`
support for boss. `clis/_shared/site-auth.js` is imported by 65
adapters; `adapter eject` not copying `_shared/` is a real bug, but it
affects every one of them and belongs in `src/cli.ts` eject, not in a
per-adapter copy. Also removes the tautological test that only
`readFileSync`'d auth.js and asserted on its own import string.
- Flatten `detail`'s row and `columns` back to scalars. The nested
`location` / `recruiter` / `companyInfo` objects rendered as
`[object Object]` in table, plain, csv and markdown output, because
every renderer coerces cells with `String(v)` (`src/output.ts`) and
none resolves dotted paths — only `-f json/yaml` was usable. Field
names match the previous flat contract.
- Fix `stageText`, which matched `/融资|上市|不需要融资/` and therefore
never matched the common `D轮及以上` / `天使轮` forms, leaving `stage`
permanently empty. The industry filter directly above already excluded
`轮`.
- Prefer BOSS's semantic `.text-city` / `.text-experiece` / `.text-degree`
classes over positional `limits[0..2]`, which shifted every field when
the header gained or lost a node. Positional order remains a fallback.
- Drop the `district` column instead of shipping one that is always null:
the extractor hardcoded `districtText: ''` and the rendered page
exposes no district anywhere in the captured fixture.
- Classify a login bounce as `AuthRequiredError`. The retry loop swallows
every read error, so a session pushed to the login wall previously
surfaced as "did not expose a complete job posting" — the API path this
replaced got that classification for free via `assertOk`.
Regenerates `cli-manifest.json` for the new columns.
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(ke,douban): update auth anchors for 2026-08 site redesign
- ke: username moved to .typeShowUser (masked phone e.g. 15****93) in new
SSR header; legacy .userNick/.user-name/.myInfo anchors no longer render,
causing false AUTH_REQUIRED despite a valid lianjia_token cookie.
- douban: .bn-more href changed from /people/<id>/ to /passport/setting/,
breaking the user_id regex. Login detection now keys on account element +
ck cookie; user_id falls back to any /people/<id>/ link or the dbcl2
cookie, and may be empty on the new homepage without misreporting auth.
Verified locally via shadow adapters in ~/.opencli/clis: both whoami
commands return logged_in:true, and downstream commands (ke zufang,
douban search) return live data again.
* fix(ke): keep only the ke auth anchor widening; drop the douban change
The douban half of this PR is superseded by #2293, which rewrites
verifyDoubanIdentity with a strictly better mechanism, and it introduced
three problems of its own:
- It read `document.cookie` inside the page, but this file already reads
cookies at CDP level in hasDoubanSessionCookie and discards them. dbcl2
is HttpOnly, so the in-page path can never see it — the PR's own comment
admits this ("HttpOnly 时 JS 取不到,静默跳过").
- `document.querySelector('a[href*="/people/"]')` takes the first
/people/ link anywhere on the douban homepage, which renders a friends'
activity feed full of other users' profile links, so whoami could
silently report a stranger's user_id. #2293 removed this exact selector
for this exact reason.
- The `ck` guard was unreachable in practice: verifyDoubanIdentity
already throws upstream when neither dbcl2 nor ck exists, so the new
branch only added another false AUTH_REQUIRED path to a change whose
stated purpose was removing false AUTH_REQUIRED.
The ke half stands on its own: prepending `.typeShowUser a span,
.typeShowUser a` while keeping every previous anchor is purely additive
and cannot regress a profile where the old anchors still resolve.
---------
Co-authored-by: fanxiaoyu0 <fanxiaoyu0@users.noreply.github.com>
Co-authored-by: jackwener <jakevingoo@gmail.com>
* fix(douyin): restore stats via creator item list after metrics_trend retired
`item_analysis/metrics_trend` now answers every request with `status_code 4`,
so `douyin stats` fails for all works regardless of age or account (#2197).
Replaying the endpoint with the old unix-timestamp params, with `start_date`/
`end_date`, and with `item_id` instead of `aweme_id` all return the same code,
so the endpoint is gone rather than re-shaped.
The creator item list still serves the full per-work metric set the creator
dashboard renders — 26 fields including view_count, bounce_rate_2s,
completion_rate_5s, avg_view_second, cover_show, cover_click_rate,
fan_view_proportion and subscribe_count — which is a superset of the four
counters metrics_trend used to return. Walk its cursor and pick the requested
work out of the page.
Two details worth keeping:
- The endpoint serializes the work id as a JSON number, so the browser has
already rounded it past IEEE-754 integer precision before the adapter sees
it. `sameAwemeId` compares numerically as a fallback, otherwise every lookup
misses.
- A work that exists but carries no metrics is reported with a distinct hint
from a work that is absent from the account, so callers can tell "not yours /
wrong id" apart from "no data yet".
Verified live against a logged-in creator account: 26 metrics returned for a
published work, EMPTY_RESULT for an unknown id, ARGUMENT for a malformed id.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(douyin): state what the item list provides instead of naming the dead endpoint
A comment that names a retired endpoint puts it back into the reader's choice
space. The PR description carries the history; the source should carry the
current contract.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore: rebuild cli-manifest for the douyin stats description
The adapter description changed, and cli-manifest.json is generated and checked
in, so CI's freshness gate fails until it is rebuilt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: ele-yufo <gentanaka606@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The trailing rows of CITY_ID had incorrect cityIds, causing dianping
search to silently fall back to the cookie's default city for these
cities. Verified against live https://www.dianping.com/<slug> resolution:
- kunming 昆明: 25 -> 267
- fuzhou 福州: 110 -> 14
- xiamen 厦门: 14 -> 15
- hefei 合肥: 26 -> 110
(fuzhou and hefei previously shared the same id 110, indicating the
last few rows were transposed when the table was hand-written.)
Co-authored-by: fanxiaoyu0 <fanxiaoyu0@users.noreply.github.com>