Group albums already list videos, but upload only accepted images.
Add a dedicated video action that extracts a cover and returns the new video id.
Fixed#331
Return an empty result when a successful, completed group essence response omits its message list instead of reporting a malformed response.
Keep upstream errors and incomplete pagination responses explicit, and cover omitted and null list variants with regression tests.
Fixes#320.
Translate group essence responses into the established OneBot shape instead of exposing the upstream digest payload.
Return validated sender, operator, timestamp, message identifier, and message segment fields for text, face, image, and video entries. Retain the authoritative message metadata in one transaction so returned identifiers can be used by later essence actions without partial cache state.
Reject malformed, cross-group, incomplete, or unknown digest entries, and surface pagination or upstream failures instead of returning truncated success results.
Fixes#300.
Align existing managed source with the repository lint rules so the workspace can adopt one reliable root validation command.
This normalizes the private file upload block, removes stale test suppressions, makes an error boundary explicit, and records a stable settings dependency without changing user-facing behavior.
The cleanup creates a deterministic baseline before lint becomes a required pull request gate.
Require the dress page to identify the requested account and every returned item before exposing the result. Reject malformed nested media metadata instead of silently degrading it.
Bound the HTML request to a 10-second redirect-chain deadline and a 2 MiB response body. Secure redirects by closing previous connections, stripping cross-origin credentials, rejecting cross-origin write forwarding, and surfacing malformed locations.
Replace the captured page with a minimal sanitized fixture and add protocol plus OneBot Action regression coverage for resource limits, redirect safety, response ownership, and structured failures.
Add a read-only action for querying QQ profile decorations from the authenticated web page.
Validate and classify page responses, expose structured errors, and cover parsing with a sanitized fixture.
Correct type 2 link and type 8 rich-media summary field mappings so collection responses no longer fail with unterminated groups.
Add wire-level regression coverage for both layouts.
Replace the get_collection_list placeholder with authenticated, read-only collection retrieval through the existing bridge API.
Add strict binary envelope validation, bounded pagination, accurate category filtering, preserved 64-bit identifiers, and explicit ticket, transport, service, and malformed-response failures. Cap item counts, page traversal, response bytes, per-request waits, and the total operation time.
Expose the compatible response model through Core, OneBot, the SDK, and the generated MCP catalog. Cover protocol framing, cursor behavior, filtered hasMore semantics, cleanup failures, bridge ticket acquisition, and Action validation with regression tests.
Verified with the live test bot without changing native components or adding any collection write behavior.
* fix(protocol): retry qzone msglist via user.qzone route on -10000
emotion_cgi_msglist_v6 through the legacy h5.qzone.qq.com →
taotao.qzone.qq.com gateway can be rejected with `-10000 使用人数过多`
for some accounts (issue #182). Real-device captures show the same CGI
via user.qzone.qq.com → taotao.qq.com is not rate-limited, so retry once
through that route when the legacy route returns -10000.
Behaviour-preserving for accounts the legacy route already serves: the
retry only fires on a -10000 envelope; all other codes, transport
errors, and the missing-msglist case are unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(port-probe): fix port probe logic (parse accounts more than 2) and add description for 4302-4310 port.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(qzone): add set_qzone_msg_right to change a feed's visibility
Add updateQzoneMsgRight in the protocol layer, porting php-qzone's
updateRight: fetch the feed via emotion_cgi_msgdetail_v6, rebuild the
full publish-shaped payload (richval/pic_bo/con), then override
ugc_right/allow_uins and POST to emotion_cgi_update. Expose it through
QzoneApi.updateRight and the OneBot set_qzone_msg_right action, with the
same ugc_right/target_uins validation as send_qzone_msg. Covered by
protocol, core and onebot tests.
* fix(qzone): make qzoneParse more stable and fixed a test example
Add ugc_right and target_uins to the qzone publish path across protocol,
bridge, and the send_qzone_msg OneBot action, regenerate the MCP catalog,
and cover restricted visibility (16/128) with validation tests.
Adversarial review (NEEDS-FIXES) — the high-risk parts (topicId base
hostUin_tid, uin/hostUin not swapped, format=fs, platformid=52) verified
correct against 3 community impls; the gaps:
- Added the re_feeds param family all 3 impls send and the publish sibling
already sends for this CGI family: feedsType=100, private=0, paramstr=1,
richtype/richval/isSignIn=''. Tests assert the load-bearing ones.
- qzreferrer used hostUin; all impls use the commenter's own uin → selfUin.
- topicId __1 suffix is impl-dependent (2/3 include it, 1 omits); kept as
default but the comment now flags the suffix specifically as the
unverified piece (base shape is confirmed), and the test no longer
presents it as canonical.
commentQzoneMsg POSTs a form body to taotao.qzone.qq.com's
emotion_cgi_re_feeds (proxied through h5.qzone), keyed by topicId
<hostUin>_<tid>__1, as the bot (uin=self) on target_uin's feed (defaults
to the bot's own space). Returns the new comment id best-effort — the
response field name varies (commentid/commentId) so a missing id with
code 0 is still success. Throws on non-zero code/subcode; rejects empty
tid/content pre-flight. WRITE — rate-limit (风控).
topicId shape + comment-id field are extrapolated from community impls,
pending a live capture (labeled in the helper comment).
- core: QzoneApi.comment; onebot: comment_qzone action; regen catalog.
- tests: re_feeds form lock (topicId/uin/hostUin/content/format=fs),
commentid + camelCase commentId fallback + missing-id success, empty
tid/content preflight, non-zero code throw, bridge owner-defaulting.
Adversarial review (NEEDS-FIXES) of the like/unlike slice:
- Missing abstime: every real dolike impl (QLiker.py, CSDN) sends the
target feed's post time. Threaded abstime through setQzoneLike →
QzoneApi.like → like_qzone/unlike_qzone actions (optional, default 0;
pass the feed's real time for reliability). Test asserts it.
- internal_unlike_app is the conventional paired CGI but NO public bot
impl exercises unlike, so it's best-guess. Labeled UNVERIFIED/pending a
live capture in the helper comment, the unlike action summary, and the
unlike test — instead of pinning it as confirmed truth (avoiding the
re-codify-a-wrong-constant trap the last 3 slices hit).
- Documented the success-signal (code/subcode) as extrapolated from
sibling CGIs (dolike may use a succ/fail token instead).
The confirmed mechanics (dolike CGI, opuin=liker, unikey/curkey mood
shape, appid=311, fid=tid) are noted as such.
setQzoneLike POSTs a form body to w.qzone.qq.com's internal_dolike_app
(like) or internal_unlike_app (unlike), proxied through h5.qzone, keyed by
the feed's mood unikey/curkey (http://user.qzone.qq.com/<uin>/mood/<tid>)
and fid=tid, as the bot (opUin). target_uin selects whose 说说 (defaults
to the bot's own); scope is 说说 (mood, appid 311). Throws on non-zero
code/subcode. Rejects empty tid pre-flight. WRITE — rate-limit (风控).
- core: QzoneApi.like(tid, targetUin?, like); onebot: like_qzone +
unlike_qzone actions; regen catalog.
- tests: dolike vs unlike CGI selection, unikey/curkey/fid/opuin form
lock, empty-tid preflight, code+subcode throws, and bridge owner-
defaulting (self vs friend) + like-flag threading.
Adversarial review (NEEDS-FIXES) — two wrong-constant defects (same family
as the publish t1_tid bug) + a destructive-write hardening:
- format=json → format=fs. Every working reference (silica-github/
qq_zone_delete, zhihu, daimagongfang) uses fs for the delete CGI; the
tests re-codified the wrong value.
- Removed the invented json=1 body param (no source includes it); test now
asserts it's absent.
- Also throw on a non-zero subcode (not just code), and documented that the
code/subcode success signal is EXTRAPOLATED from sibling CGIs pending a
live capture (public delete impls only check HTTP 2xx). Added subcode-throw
and code-absent-resolves tests to lock the branch.
deleteQzoneMsg POSTs a form-urlencoded tid to taotao.qzone.qq.com's
emotion_cgi_delete_v6 (proxied through h5.qzone), on the bot's own space.
Success has no positive payload beyond code 0, so the contract is
throw-on-non-zero-code (foreign/unknown tid or auth failure); the server
only deletes feeds the hostUin owns. Rejects empty tid pre-flight.
- core: QzoneApi.delete; onebot: delete_qzone_msg action; regen catalog.
- tests: tid form-body/url/headers lock, empty-tid preflight, non-zero
code throw, and bridge self-space threading.
Adversarial review (verdict NEEDS-FIXES, CRITICAL): emotion_cgi_publish_v6's
success envelope names the new feed id `t1_tid` and post time `t1_time`
(a STRING), NOT `tid`/`now`. So every successful publish hit the
'缺少 tid' guard and was reported as a failure (risking caller-retry
duplicate 说说). The tests passed only because they mocked the wrong field
names — they codified the bug.
- Read t1_tid (primary) with tid as a defensive fallback; t1_time||now
via Number() (t1_time is a string on the wire). Verified against the
Go reference lib starim00/AnimeAPI EmotionPublishVo.
- Added who=1 to the form body (both working references send it).
- Tests now encode the real contract (t1_tid/t1_time string), plus a
tid/now fallback case and a no-feed-id throw case.
First Qzone WRITE path. publishQzoneMsg POSTs a form-urlencoded body to
taotao.qzone.qq.com's emotion_cgi_publish_v6 (proxied through h5.qzone),
g_tk in the query, on the bot's own space. Returns the server-assigned
tid (the handle for later delete/comment/like) + post time.
- Throws on transport failure, non-zero Qzone code (content rejected /
rate-limited), or a success body with no tid — never reports a publish
as succeeded without the feed id. Rejects empty content pre-flight.
- 带图 (image) publishing is a later slice (needs the upload CGI first).
- Action carries a风控/限流 caveat: publishing is an active action and
high frequency is rate-limited by Qzone, same as sending messages.
- core: QzoneApi.publish; onebot: send_qzone_msg action; regen catalog.
- tests: form-body/url/headers lock, empty-content preflight, non-zero
code throw, missing-tid throw, and bridge self-space threading.
Cosmetic fidelity nit from re-review: the fixture used _Callback(...) while
the request pins callback=_preloadCallback. Parser is name-agnostic so
behavior is unchanged; this just makes the fixture mirror slice-1.
Adversarial review of the get_qzone_feeds slice (verdict NEEDS-FIXES):
- Host: hit ic2.qzone.qq.com directly, bypassing the proxy gateway that
slice-1/group-album use. The qzone.qq.com cookie jar only authenticates
against the proxy origin, so the direct call would fail referer/origin.
Now routed through h5.qzone.qq.com/proxy/domain/ic2.qzone.qq.com/...
- Format: requested format=json but the test fed a JSONP body, so the
requested format was never pinned. Aligned to slice-1's
format=jsonp + callback=_preloadCallback; the test now asserts format.
- Pagination: documented honestly that page_num is reliable only for the
first page (deep paging needs a time cursor, deferred) — in the
docstring and the action summary/param desc.
- Clarified the key/feedskey fallback is the per-feed handle, not a cursor.
Second read-only Qzone slice. getQzoneFeeds over ic2.qzone.qq.com's
feeds3_html_more CGI, same cookie/g_tk plumbing + JSONP-tolerant parse +
throw-on-auth-failure contract as get_qzone_msg_list. Surfaces the stable
structured fields (uin/nickname/time/appid/key) and passes the
pre-rendered html blob through verbatim — deep HTML→segment parsing is
left to callers. Paginated via page_num (1-based) + count.
- protocol/web/qzone.ts: getQzoneFeeds + mapFeeds.
- core: QzoneApi.getFeeds (always the bot's own identity).
- onebot: get_qzone_feeds action; regenerated MCP catalog.
- tests: feeds mapping, HTTP url/params, empty-feed→[], auth-throw,
and bridge pageNum/count threading.
Exact CGI field names / pagination cursor to be re-confirmed against a
live capture (same posture as the msglist helper).
Adversarial review of the get_qzone_msg_list slice flagged two real
coverage gaps (verdict was SHIP, no correctness defects):
- protocol: empty space ({code:0,msglist:[]}) must return [] not throw —
the exact distinction the throw-on-auth-failure contract promises.
- core: QzoneApi.getMsgList target_uin defaulting + the >0 guard, the
only place the string(identity.uin)/number(param) boundary is crossed.
First slice of personal QQ-Zone (个人空间) support, an exclusive feature
vs NapCat/SL (both only do group albums). Read-only to prove out the
auth+parse chain before write ops (publish/like/comment).
- protocol/web/qzone.ts: getQzoneMsgList over taotao.qzone.qq.com's
emotion_cgi_msglist_v6 (h5.qzone proxy), reusing the same cookie/g_tk
plumbing as group-album. JSONP-tolerant parse; throws on auth failure
rather than swallowing to [] (mirrors group-signin contract).
- core: QzoneApi bridge class wired into ApiHub.
- onebot: get_qzone_msg_list action (target_uin/pos/num), regenerated
MCP catalog + new 空间 doc category.
- HTTP-layer tests lock URL/params, JSONP stripping, field mapping, and
the throw-on-auth-failure paths (9 cases, all green).
Exact CGI params/response fields to be re-confirmed against a live
capture when extending (same maintenance posture as group-album).
Adversarial review: swallowing transport/auth errors to [] made an expired
cookie indistinguishable from a group with zero sign-ins, and left the
onebot action's try/catch as dead code. Now getDaySignedListWebAPI
propagates transport errors and throws on a page-less response (matches
NapCat, which throws on missing page) — so auth failure becomes a proper
'failed' OneBot response; a genuinely empty group still maps to [].
Also: cover the previously-untested HTTP layer (exact URL + g_tk + json
body + error propagation), harden the rank de-skew with Number(), and
document the deliberate local-date (vs NapCat UTC) choice for dayYmd.
Group sign-in (打卡) list is NapCat's one kernel-free item here: it's a
plain HTTP POST to qun.qq.com/v2/signin/trpc/GetDaySignedList with g_tk
from p_skey, not OIDB. SnowLuma already has the cookie/bkn infra (same as
group notice/essence/honor), so this is a straight port — no RE.
- protocol/web/group-signin.ts: getDaySignedListWebAPI + pure
mapDaySignedList (de-skews signInRank via (r-1)/2+1, [] when no page).
- core WebApi.getSignedList(groupId) wires the qun.qq.com cookie jar.
- onebot get_group_signed_list groupAction (readOnly), [] on no sign-ins.
- Tests pin the rank/field transform and the action wiring.