Commit Graph
18 Commits
Author SHA1 Message Date
DrSmoothl 7c0519f75f lint 2026-10-12 15:43:34 +08:00
H3CoF6andClaude Opus 4.8 0d03b00643 Fix(Qzone) -10000风控修复 && Fix(port-probe) PID探测多pid逻辑修复和偶数端口 (#192)
* 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>
2026-07-04 14:33:02 +08:00
DrSmoothl 63ab7a1846 fix(protocol): parse Qzone feeds without eval + stop getCookies hanging (#182)
真机抓包定位到 get_qzone_feeds 的两个问题:

1) feeds3_html_more 的响应体是**JS 对象字面量**(键无引号、单引号字符串、\xNN
   转义、数组含 undefined),不是 JSON → JSON.parse 报 "Expected property name
   at position 66"(报告者看到的「响应结构问题」)。实测调参数(outputhtmlfeed/
   format)改不掉这个格式,且 feeds 是纯远程 H5(native i64 里无相关 CGI),没有
   干净 JSON 端点。改用**非执行的递归下降解析器** parseJsLiteral 把它当数据解析——
   绝不 eval 远程内容(vm 沙箱不是安全边界,被篡改的响应可逃逸到 RCE)。解析器只认
   对象/数组/字符串/数字/true|false|null|undefined,丢弃 __proto__ 键防原型污染;
   mapFeeds 过滤数组尾部的 undefined 洞。

2) getCookies 卡死:HttpsGetCookies 无超时、无深度上限地递归跟随 302;当某跳目标
   不可达(数据中心/Docker 上 qzone.qq.com 裸域超时)→ 永久挂死所有 cookie-backed
   web 动作。加 8s 超时 + 深度上限(5) + 失败/超时保留已收集 cookie(不再 reject)。

两处修复均在 tempserver 真机验证:getCookies 从卡死 3 分钟变正常返回;抓到的真实
feeds 响应体做成测试 fixture,非执行解析器正确解出 uin/nickname/html(\x3C→<)。
读接口加了 TRACE 原始体日志 + 结构失败错误带响应片段,方便后续排查。
(msglist 的 -10000「使用人数过多」判定为 taotao 服务端风控——同 cookie 下 feeds
能成功、msglist 单独挂,非代码 bug,代码已正确透传。)

protocol 572 全绿、typecheck 干净;两轮对抗性 Agent:先揪出 vm 版可 RCE 逼我换非执行
解析器,再确认新解析器构造上不执行代码、无原型污染、真机数据解析正确。
2026-07-01 21:56:28 +08:00
Airline233 2f374ea8c9 feat(qzone): support post visibility rights when publishing qzone posts (#178)
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.
2026-06-30 23:57:39 +08:00
Airline233 62eb8fd763 feat: Qzone支持上传图片(发说说、评论附图) (#152)
* feat(core,onebot,protocol): support publish shuoshuo and comment with image

* Fix: Accidentally changed notes
2026-06-30 01:47:21 +08:00
DrSmoothl b9d8247309 fix(qzone): complete re_feeds param set + correct qzreferrer/topicId honesty
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.
2026-06-20 02:10:12 +08:00
DrSmoothl 95f24ffc4a feat(qzone): comment_qzone — comment on a 说说 via web API
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.
2026-06-20 02:05:56 +08:00
DrSmoothl 870504ae3a fix(qzone): thread abstime into like, flag unlike endpoint as unverified
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.
2026-06-20 02:02:25 +08:00
DrSmoothl d497f1e6c7 feat(qzone): like_qzone / unlike_qzone — like/unlike a 说说 via web API
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.
2026-06-20 01:56:23 +08:00
DrSmoothl 4d75dc9ae3 fix(qzone): delete_v6 uses format=fs, drop bogus json param, harden success check
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.
2026-06-20 01:52:47 +08:00
DrSmoothl a704f12945 feat(qzone): delete_qzone_msg — delete a 说说 by tid via web API
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.
2026-06-20 01:47:50 +08:00
DrSmoothl 9e21c3a7ef fix(qzone): read real t1_tid/t1_time publish fields — was false-throwing every success
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.
2026-06-20 01:44:50 +08:00
DrSmoothl 9f2eb37232 feat(qzone): send_qzone_msg — publish a text 说说 via web API
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.
2026-06-20 01:40:06 +08:00
DrSmoothl b00f5ab759 test(qzone): align feeds JSONP fixture to requested _preloadCallback name
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.
2026-06-20 01:37:55 +08:00
DrSmoothl 5164ba9e39 fix(qzone): route feeds3_html_more through proxy + pin JSONP format
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.
2026-06-20 01:36:04 +08:00
DrSmoothl 094dc12f0c feat(qzone): get_qzone_feeds — read friend feeds (好友动态) via web API
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).
2026-06-20 01:31:36 +08:00
DrSmoothl 0083a8fb07 test(qzone): cover empty-space happy path + bridge target_uin defaulting
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.
2026-06-20 01:28:57 +08:00
DrSmoothl 61b4bd04a6 feat(qzone): get_qzone_msg_list — read personal Qzone 说说 list via web API
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).
2026-06-20 01:11:07 +08:00