feat: QR login, AcFun settings panel, and 动态 (moment) tools

- scan.acfun.cn QR flow (start -> long-poll scanResult -> acceptResult) with
  rotated-signature handling, background poller, durable session in the Host
  credential service, and cookie handoff to the provider
- Client half: Settings -> AcFun page (status, QR code, poll, logout) over a
  same-origin /acfun-panel JSON API with cross-origin rejection
- acfun_moment / acfun_moment_comments / acfun_topic read tools, moment_like /
  moment_unlike / moment_banana actions, and comment posting on moments
- acfun_account status tool; docs for the QR contract, the 广场/动态 research
  verdict, and the client-composition restart requirement
This commit is contained in:
tamakyi
2026-10-27 11:50:44 +08:00
parent f9bedde19e
commit 97825f020d
19 changed files with 3723 additions and 189 deletions
+30
View File
@@ -11,6 +11,7 @@
import { AcfunProvider } from '../lib/provider.js'
import { resolveConfig } from '../lib/config.js'
import { createAcfunTools } from '../lib/tools.js'
import { AcfunQrFlow } from '../lib/qr-flow.js'
import { validateValue } from './schema-check.mjs'
const provider = new AcfunProvider({ requestTimeoutMs: 30_000 })
@@ -79,12 +80,41 @@ await run('acfun_comments', { acId, page: 1 })
await run('acfun_danmaku', { acId, limit: 50 })
await run('acfun_rank', { period: 'day', limit: 5 })
await run('acfun_playurl', { acId })
await run('acfun_account', {})
await run('acfun_moment', { momentId: process.env.ACFUN_MOMENT_ID ?? '5093831' })
await run('acfun_moment_comments', { momentId: process.env.ACFUN_MOMENT_ID ?? '5093831', page: 1 })
await run('acfun_topic', { tagId: process.env.ACFUN_TOPIC_ID ?? '488' })
if (video !== undefined && video.title.length === 0) failures.push('acfun_video: empty title')
// Interactions must fail closed without allowWriteActions + cookie.
await run('acfun_interact', { action: 'like', acId }, { expectError: 'WRITE_DISABLED' })
await run('acfun_interact', { action: 'moment_like', momentId: process.env.ACFUN_MOMENT_ID ?? '5093831' }, { expectError: 'WRITE_DISABLED' })
await run('acfun_post_comment', { acId, content: 'live-check should not post this' }, { expectError: 'WRITE_DISABLED' })
// QR login: ask AcFun for a real ticket and check the waiting state. No phone
// scans it, so this proves issuance + the long-poll waiting path only.
console.log('\n=== QR login (live, never scanned)')
try {
const flow = new AcfunQrFlow({ longPollTimeoutMs: 8_000 })
const started = await flow.start()
console.log(` ok issued ticket ${String(started.ticket).slice(0, 12)}… with QR ${started.qr.kind} (${started.qr.value.length} chars), ttl ${Math.round((started.expiresAt - Date.now()) / 1000)}s`)
const aborter = new AbortController()
const scan = await flow.waitForScan({ signal: aborter.signal })
console.log(` ok unscanned ticket reports state=${scan.state}`)
if (scan.state === 'waiting') {
const pending = flow.waitForScan({ signal: aborter.signal })
setTimeout(() => aborter.abort(), 400)
try {
await pending
failures.push('QR wait did not abort promptly')
} catch (error) {
console.log(` ok abort cancels the long poll (${error?.name})`)
}
}
} catch (error) {
failures.push(`QR login live check failed: ${error?.name}: ${error?.message}`)
}
if (failures.length > 0) {
console.error(`\n${failures.length} live check(s) failed`)
for (const failure of failures) console.error(`- ${failure}`)