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
+84 -17
View File
@@ -1,21 +1,28 @@
/**
* dsh-plugin-acfun — AcFun (A站) reads and interactions as DeepSeek Harness
* tools.
* dsh-plugin-acfun — AcFun (A站) reads, interactions, QR login and a settings
* panel for the DeepSeek Harness Web UI.
*
* Host-only bundle: no Client half, no runtime imports of `@deepseek-ai/*`.
* That is deliberate — a workspace-linked bundle resolves bare specifiers from
* its own directory, so importing `@deepseek-ai/dsh-tools` or schemastery
* would fail to activate. Tool definitions are therefore plain objects and the
* config schema is validated in `lib/config.js`.
* Host half: registers the model tools, the `/acfun-panel` HTTP surface used by
* the Client panel, and the QR-login state machine. No runtime dependency on
* `@deepseek-ai/*`: a workspace-linked bundle cannot resolve bare harness
* specifiers from its own directory, so tool definitions are plain objects and
* the config schema is validated locally.
*
* Reads work anonymously. Every mutating call additionally requires
* `allowWriteActions: true` plus a login cookie, so installing the bundle never
* gives the model write access to an AcFun account by accident.
* Browser half: `client.js` registers a Settings → AcFun page through the
* `settings.section` slot and talks to this half over same-origin HTTP.
*/
import { createRequire } from 'node:module'
import { resolveConfig } from './lib/config.js'
import { AcfunProvider } from './lib/provider.js'
import { ACFUN_GUIDANCE, createAcfunTools } from './lib/tools.js'
import { createPanelRoutes } from './lib/panel.js'
import { AcfunLoginManager, createCredentialSessionStore } from './lib/login.js'
import { AcfunQrFlow } from './lib/qr-flow.js'
const require = createRequire(import.meta.url)
const pkg = require('./package.json')
/** Cordis plugin name used by loader diagnostics. */
export const name = 'dsh-plugin-acfun'
@@ -36,22 +43,32 @@ function asEffect(ctx, factory) {
return
}
const dispose = factory()
if (typeof dispose === 'function' && typeof ctx.on === 'function') {
// Older Cordis minors without `effect` still expose the dispose event.
ctx.on('dispose', dispose)
if (typeof dispose === 'function' && typeof ctx.on === 'function') ctx.on('dispose', dispose)
}
/** Read one optional Host service across Cordis minors. */
function optionalService(ctx, key) {
if (typeof ctx.get === 'function') {
try {
const value = ctx.get(key)
if (value !== undefined) return value
} catch {
// Fall through to the direct property below.
}
}
return ctx[key]
}
/**
* Activate the plugin: register the seven AcFun tools plus one system-prompt
* guidance section.
* Activate the plugin: register the nine AcFun tools, one system-prompt
* guidance section, the QR-login manager, and — when this Host runs a Web
* carrier — the panel routes.
*
* @param {any} ctx - Cordis context with `tools` and `systemPrompt`.
* @param {unknown} config - the Loader row config (see cordis.patch.yml).
*/
export function apply(ctx, config) {
const resolved = resolveConfig(config)
if (resolved.warnings.length > 0 && typeof ctx.logger?.warn === 'function') {
for (const warning of resolved.warnings) ctx.logger.warn(`[dsh-plugin-acfun] ${warning}`)
}
@@ -62,8 +79,32 @@ export function apply(ctx, config) {
allowWriteActions: resolved.allowWriteActions,
})
// Durable session storage (Host credential service). Optional: without it a
// QR login only lives as long as this activation.
const credentials = optionalService(ctx, 'credentials')
const store = createCredentialSessionStore(credentials)
const login = new AcfunLoginManager({
provider,
store,
flow: new AcfunQrFlow(),
})
const sessionSource = () => {
if (login.authSource === 'qr') return 'qr'
return resolved.cookieSource === 'config' ? 'config' : resolved.cookieSource === 'env' ? 'env' : 'none'
}
// Restore a persisted QR session without delaying activation.
Promise.resolve()
.then(() => login.restore())
.catch((error) => {
if (typeof ctx.logger?.warn === 'function') ctx.logger.warn(`[dsh-plugin-acfun] could not restore session: ${String(error?.message ?? error)}`)
})
const tools = createAcfunTools(provider, resolved, { sessionSource, login })
asEffect(ctx, () => {
const disposers = createAcfunTools(provider, resolved).map((definition) => ctx.tools.register(definition))
const disposers = tools.map((definition) => ctx.tools.register(definition))
return () => {
for (const dispose of disposers) {
if (typeof dispose === 'function') dispose()
@@ -79,8 +120,34 @@ export function apply(ctx, config) {
})
return typeof dispose === 'function' ? dispose : undefined
})
// The panel is a Web-UI surface; a headless/SDK profile simply has no
// webServer and the tools stay available without it.
const webServer = optionalService(ctx, 'webServer')
if (webServer !== undefined && typeof webServer.register === 'function') {
asEffect(ctx, () => {
const routes = createPanelRoutes({
provider,
login,
version: String(pkg.version ?? '0.0.0'),
allowWriteActions: resolved.allowWriteActions,
cookieSource: sessionSource,
tools,
clientModules: optionalService(ctx, 'clientModules'),
})
const disposers = routes.map((route) => webServer.register(route))
return () => {
for (const dispose of disposers) {
if (typeof dispose === 'function') dispose()
}
}
})
}
}
export { AcfunProvider } from './lib/provider.js'
export { ACFUN_GUIDANCE, createAcfunTools } from './lib/tools.js'
export { resolveConfig } from './lib/config.js'
export { AcfunLoginManager, createCredentialSessionStore } from './lib/login.js'
export { AcfunQrFlow, QR_SERVICE_URL, QR_PATHS } from './lib/qr-flow.js'
export { createPanelRoutes, PANEL_PATH } from './lib/panel.js'