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:
@@ -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}`)
|
||||
|
||||
Reference in New Issue
Block a user