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