/** * dsh-plugin-acfun — AcFun (A站) reads, interactions, QR login and a settings * panel for the DeepSeek Harness Web UI. * * 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. * * 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' /** Services required before the plugin starts. */ export const inject = ['tools', 'systemPrompt'] /** * Register one setup factory as a plugin-owned effect when the context offers * `effect`, falling back to a direct call for older Cordis minors. * * @param {any} ctx - plugin context. * @param {() => (() => void) | void} factory - performs registrations and returns a disposer. */ function asEffect(ctx, factory) { if (typeof ctx.effect === 'function') { ctx.effect(factory) return } const dispose = factory() 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 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}`) } const provider = new AcfunProvider({ cookie: resolved.cookie, requestTimeoutMs: resolved.requestTimeoutMs, 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 = tools.map((definition) => ctx.tools.register(definition)) return () => { for (const dispose of disposers) { if (typeof dispose === 'function') dispose() } } }) asEffect(ctx, () => { const dispose = ctx.systemPrompt.section({ name: 'tool:acfun', order: 112, text: ACFUN_GUIDANCE, }) 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'