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.
134 lines
5.8 KiB
JavaScript
134 lines
5.8 KiB
JavaScript
/**
|
|
* Offline pre-install check: load the bundle exactly as the Loader would
|
|
* (`apply()` on a mock context), verify every tool definition, render each one
|
|
* from a schema-shaped sample, and prove the write guard fails closed.
|
|
*
|
|
* Run with: node scripts/self-check.mjs
|
|
*/
|
|
|
|
import { apply, inject, name } from '../index.js'
|
|
import { resolveConfig } from '../lib/config.js'
|
|
import { createAcfunTools, ACFUN_GUIDANCE } from '../lib/tools.js'
|
|
import { AcfunProvider } from '../lib/provider.js'
|
|
import { assertSchemaSupported, sampleValue, validateValue } from './schema-check.mjs'
|
|
|
|
const failures = []
|
|
const check = (label, condition, detail = '') => {
|
|
if (condition) {
|
|
console.log(` ok ${label}`)
|
|
} else {
|
|
failures.push(`${label}${detail ? `: ${detail}` : ''}`)
|
|
console.log(` FAIL ${label}${detail ? `: ${detail}` : ''}`)
|
|
}
|
|
}
|
|
|
|
console.log('plugin exports')
|
|
check('name is dsh-plugin-acfun', name === 'dsh-plugin-acfun', String(name))
|
|
check('inject lists tools and systemPrompt', inject.includes('tools') && inject.includes('systemPrompt'), JSON.stringify(inject))
|
|
check('guidance is non-empty prose', typeof ACFUN_GUIDANCE === 'string' && ACFUN_GUIDANCE.length > 80, String(ACFUN_GUIDANCE.length))
|
|
|
|
console.log('\nmock activation')
|
|
const registered = []
|
|
const sections = []
|
|
const effects = []
|
|
const mockCtx = {
|
|
effect(factory) {
|
|
effects.push(factory)
|
|
factory()
|
|
},
|
|
tools: {
|
|
register(definition) {
|
|
registered.push(definition)
|
|
return () => {}
|
|
},
|
|
},
|
|
systemPrompt: {
|
|
section(section) {
|
|
sections.push(section)
|
|
return () => {}
|
|
},
|
|
},
|
|
}
|
|
apply(mockCtx, {})
|
|
|
|
check('one system-prompt section registered', sections.length === 1, JSON.stringify(sections))
|
|
check('section name is tool:acfun', sections[0]?.name === 'tool:acfun', String(sections[0]?.name))
|
|
check('eight tools registered', registered.length === 8, String(registered.length))
|
|
check(
|
|
'tool names are unique acfun_* names',
|
|
new Set(registered.map((tool) => tool.name)).size === 8 && registered.every((tool) => tool.name.startsWith('acfun_')),
|
|
registered.map((tool) => tool.name).join(','),
|
|
)
|
|
|
|
console.log('\ntool definitions')
|
|
for (const tool of registered) {
|
|
check(`${tool.name}: has a description`, typeof tool.description === 'string' && tool.description.length > 40)
|
|
check(`${tool.name}: parameters stay in the schema subset`, assertSchemaSupported(tool.parameters, `${tool.name}.parameters`).length === 0, assertSchemaSupported(tool.parameters, `${tool.name}.parameters`).join('; '))
|
|
check(`${tool.name}: parameters are an object root`, tool.parameters.type === 'object' && tool.parameters.additionalProperties === false)
|
|
const outputErrors = assertSchemaSupported(tool.output.schema, `${tool.name}.output`)
|
|
check(`${tool.name}: output stays in the schema subset`, outputErrors.length === 0, outputErrors.join('; '))
|
|
check(`${tool.name}: output is an object root`, tool.output.schema.type === 'object')
|
|
check(`${tool.name}: execute is a function`, typeof tool.execute === 'function')
|
|
|
|
const sample = sampleValue(tool.output.schema)
|
|
const valueErrors = validateValue(sample, tool.output.schema, `${tool.name}.sample`)
|
|
check(`${tool.name}: schema sample validates`, valueErrors.length === 0, valueErrors.join('; '))
|
|
let rendered
|
|
try {
|
|
rendered = tool.output.render({}, sample)
|
|
} catch (error) {
|
|
check(`${tool.name}: render does not throw`, false, String(error?.message ?? error))
|
|
continue
|
|
}
|
|
check(
|
|
`${tool.name}: render returns text content blocks`,
|
|
Array.isArray(rendered) && rendered.length > 0 && rendered.every((block) => block?.type === 'text' && typeof block.text === 'string'),
|
|
)
|
|
check(`${tool.name}: render uses its value`, rendered.some((block) => block.text.includes('sample')))
|
|
}
|
|
|
|
console.log('\nwrite guard fails closed')
|
|
const provider = new AcfunProvider({})
|
|
const tools = createAcfunTools(provider, resolveConfig({}))
|
|
const interact = tools.find((tool) => tool.name === 'acfun_interact')
|
|
const postComment = tools.find((tool) => tool.name === 'acfun_post_comment')
|
|
const exec = { signal: new AbortController().signal }
|
|
|
|
for (const [tool, args] of [
|
|
[interact, { action: 'like', acId: '42588725' }],
|
|
[postComment, { action: 'post', acId: '42588725', content: 'hello' }],
|
|
]) {
|
|
try {
|
|
await tool.execute(args, exec)
|
|
check(`${tool.name}: refuses to write without allowWriteActions`, false, 'execute resolved unexpectedly')
|
|
} catch (error) {
|
|
check(`${tool.name}: refuses to write without allowWriteActions`, error?.code === 'WRITE_DISABLED', String(error?.code))
|
|
}
|
|
}
|
|
|
|
const enabledProvider = new AcfunProvider({ allowWriteActions: true })
|
|
const enabledTools = createAcfunTools(enabledProvider, resolveConfig({ allowWriteActions: true }))
|
|
try {
|
|
await enabledTools.find((tool) => tool.name === 'acfun_interact').execute({ action: 'follow', userId: '1' }, exec)
|
|
check('write tools require a cookie even when enabled', false, 'execute resolved unexpectedly')
|
|
} catch (error) {
|
|
check('write tools require a cookie even when enabled', error?.code === 'LOGIN_REQUIRED', String(error?.code))
|
|
}
|
|
|
|
console.log('\nconfig resolution')
|
|
const withEnv = resolveConfig({}, { ACFUN_COOKIE: 'auth_key=abc; _did=xyz' })
|
|
check('env cookie is read through cookieEnv', withEnv.cookie === 'auth_key=abc; _did=xyz' && withEnv.cookieSource === 'env')
|
|
const literal = resolveConfig({ cookie: 'auth_key=literal', cookieEnv: 'OTHER' }, { OTHER: 'ignored' })
|
|
check('literal cookie wins over env', literal.cookie === 'auth_key=literal' && literal.cookieSource === 'config')
|
|
const defaults = resolveConfig(undefined)
|
|
check('defaults keep writes disabled', defaults.allowWriteActions === false && defaults.cookie === '')
|
|
check('bad config values fall back with warnings', resolveConfig(null).warnings.length > 0)
|
|
|
|
if (failures.length > 0) {
|
|
console.error(`\n${failures.length} check(s) failed`)
|
|
process.exitCode = 1
|
|
} else {
|
|
console.log('\nall offline checks passed')
|
|
}
|
|
|