feat: AcFun tools plugin for DeepSeek Harness

Eight tools (search, video, comments, danmaku, rank, playurl, interact,
post_comment) over acfun.cn's public web API, a model-facing skill, a
bundle patch with a skill-filesystem row, and offline/live checks.

Reads are anonymous; writes require allowWriteActions plus a login cookie.
This commit is contained in:
tamakyi
2026-10-27 11:26:51 +08:00
commit f9bedde19e
19 changed files with 3530 additions and 0 deletions
+83
View File
@@ -0,0 +1,83 @@
/**
* Plugin config resolution.
*
* The plugin deliberately uses no runtime imports (no schemastery, no
* dsh-tools): a workspace-linked bundle resolves bare `@deepseek-ai/*`
* specifiers from its own directory, so it cannot see the dsh installation's
* packages. Everything here is plain ESM so `node scripts/live-check.mjs` can
* exercise it before the bundle is installed.
*/
/** Environment variable read when `cookie` is empty. */
export const DEFAULT_COOKIE_ENV = 'ACFUN_COOKIE'
/** Per-request timeout in milliseconds. */
export const DEFAULT_REQUEST_TIMEOUT_MS = 30_000
/** Cooperative per-tool-call budget in milliseconds. */
export const DEFAULT_TOOL_TIMEOUT_MS = 45_000
/** Cooperative budget for a write call (login + token + action). */
export const DEFAULT_WRITE_TIMEOUT_MS = 60_000
/** Largest list size the rank tool accepts (server caps around this). */
export const RANK_MAX_LIMIT = 50
const DEFAULTS = Object.freeze({
cookie: '',
cookieEnv: DEFAULT_COOKIE_ENV,
allowWriteActions: false,
requestTimeoutMs: DEFAULT_REQUEST_TIMEOUT_MS,
timeoutMs: DEFAULT_TOOL_TIMEOUT_MS,
})
/** A non-empty trimmed string, or undefined. */
function text(value) {
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined
}
/** A positive finite number, or undefined. */
function positive(value) {
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : undefined
}
/**
* Resolve the row config into a plain settings object.
*
* Unknown keys are ignored on purpose: the profile patch may carry comments or
* keys from an older version, and a stale key must not block activation.
*
* @param {unknown} raw - the Loader row `config` object, possibly undefined.
* @param {NodeJS.ProcessEnv | Record<string, string | undefined>} [env] - environment for `cookieEnv`.
* @returns {{cookie: string, cookieEnv: string, allowWriteActions: boolean, requestTimeoutMs: number, timeoutMs: number, warnings: string[]}}
*/
export function resolveConfig(raw, env = process.env) {
const source = raw !== null && typeof raw === 'object' && !Array.isArray(raw) ? raw : {}
const warnings = []
if (raw !== undefined && (raw === null || typeof raw !== 'object' || Array.isArray(raw))) {
warnings.push('config must be an object; using defaults')
}
const cookieEnv = text(source.cookieEnv) ?? DEFAULT_COOKIE_ENV
const literalCookie = text(source.cookie)
const envCookie = text(env?.[cookieEnv])
const cookie = literalCookie ?? envCookie ?? ''
if (source.cookie !== undefined && source.cookie !== '' && literalCookie === undefined) {
warnings.push('config.cookie is set but is not a non-empty string; ignoring it')
}
if (typeof source.allowWriteActions !== 'undefined' && typeof source.allowWriteActions !== 'boolean') {
warnings.push('config.allowWriteActions is not a boolean; using false')
}
return {
cookie,
cookieEnv,
cookieSource: literalCookie !== undefined ? 'config' : envCookie !== undefined ? 'env' : 'none',
allowWriteActions: source.allowWriteActions === true,
requestTimeoutMs: positive(source.requestTimeoutMs) ?? DEFAULT_REQUEST_TIMEOUT_MS,
timeoutMs: positive(source.timeoutMs) ?? DEFAULT_TOOL_TIMEOUT_MS,
warnings,
}
}