Files
tamakyi f9bedde19e 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.
2026-10-27 11:26:51 +08:00

141 lines
6.0 KiB
JavaScript

/**
* Minimal validator for the JSON Schema subset dsh-tools enforces:
* type / properties / required / additionalProperties / items / enum / const /
* oneOf, plus annotations. Used by the pre-install checks so a schema mistake
* fails locally instead of at plugin activation.
*/
const ANNOTATIONS = new Set(['description', 'title', 'default', 'examples'])
const CONSTRAINTS = new Set(['type', 'oneOf', 'properties', 'required', 'additionalProperties', 'items', 'enum', 'const'])
const TYPES = new Set(['object', 'array', 'string', 'number', 'integer', 'boolean', 'null'])
/** Check that one schema node stays inside the enforced keyword subset. */
export function assertSchemaSupported(schema, path = '$') {
const errors = []
if (schema === null || typeof schema !== 'object' || Array.isArray(schema)) {
return [`${path}: schema node must be an object`]
}
for (const key of Object.keys(schema)) {
if (!CONSTRAINTS.has(key) && !ANNOTATIONS.has(key)) errors.push(`${path}: unsupported keyword "${key}"`)
}
if (schema.type !== undefined && !TYPES.has(schema.type)) errors.push(`${path}: unsupported type "${schema.type}"`)
if (schema.enum !== undefined && !Array.isArray(schema.enum)) errors.push(`${path}.enum: must be an array`)
if (schema.oneOf !== undefined) {
if (!Array.isArray(schema.oneOf) || schema.oneOf.length < 2) errors.push(`${path}.oneOf: needs at least two branches`)
else schema.oneOf.forEach((branch, index) => errors.push(...assertSchemaSupported(branch, `${path}.oneOf[${index}]`)))
}
if (schema.type === 'object') {
if (schema.additionalProperties === undefined) errors.push(`${path}: object schema must declare additionalProperties`)
else if (typeof schema.additionalProperties !== 'boolean') errors.push(`${path}.additionalProperties: must be boolean`)
if (schema.properties !== undefined) {
if (typeof schema.properties !== 'object' || schema.properties === null) errors.push(`${path}.properties: must be an object`)
else {
for (const [key, child] of Object.entries(schema.properties)) {
errors.push(...assertSchemaSupported(child, `${path}.properties.${key}`))
}
}
}
if (schema.required !== undefined) {
if (!Array.isArray(schema.required)) errors.push(`${path}.required: must be an array`)
else {
const declared = schema.properties ?? {}
for (const name of schema.required) {
if (typeof name !== 'string') errors.push(`${path}.required: entries must be strings`)
else if (!(name in declared)) errors.push(`${path}.required: "${name}" is not declared in properties`)
}
}
}
}
if (schema.type === 'array' && schema.items !== undefined) {
errors.push(...assertSchemaSupported(schema.items, `${path}.items`))
}
return errors
}
/** Whether a JSON value matches one scalar type name. */
function matchesType(value, type) {
switch (type) {
case 'object':
return value !== null && typeof value === 'object' && !Array.isArray(value)
case 'array':
return Array.isArray(value)
case 'string':
return typeof value === 'string'
case 'number':
return typeof value === 'number' && Number.isFinite(value)
case 'integer':
return typeof value === 'number' && Number.isInteger(value)
case 'boolean':
return typeof value === 'boolean'
case 'null':
return value === null
default:
return false
}
}
/** Validate one value against the schema subset; returns path-qualified errors. */
export function validateValue(value, schema, path = '$') {
const errors = []
if (schema === null || typeof schema !== 'object') return [`${path}: invalid schema`]
if (schema.oneOf !== undefined) {
const branchResults = schema.oneOf.map((branch) => validateValue(value, branch, path))
const matching = branchResults.filter((result) => result.length === 0).length
if (matching !== 1) errors.push(`${path}: value must match exactly one oneOf branch (matched ${matching})`)
return errors
}
if (schema.const !== undefined && value !== schema.const) errors.push(`${path}: must equal ${JSON.stringify(schema.const)}`)
if (schema.enum !== undefined && !schema.enum.includes(value)) {
errors.push(`${path}: must be one of ${schema.enum.map((entry) => JSON.stringify(entry)).join(', ')}`)
}
if (schema.type !== undefined && !matchesType(value, schema.type)) {
errors.push(`${path}: expected ${schema.type}, got ${value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value}`)
return errors
}
if (schema.type === 'object') {
const properties = schema.properties ?? {}
for (const name of schema.required ?? []) {
if (!Object.prototype.hasOwnProperty.call(value, name)) errors.push(`${path}.${name}: missing required property`)
}
for (const [key, child] of Object.entries(value)) {
if (Object.prototype.hasOwnProperty.call(properties, key)) {
errors.push(...validateValue(child, properties[key], `${path}.${key}`))
} else if (schema.additionalProperties === false) {
errors.push(`${path}.${key}: unexpected property (additionalProperties: false)`)
}
}
}
if (schema.type === 'array') {
const items = schema.items
if (items !== undefined) value.forEach((entry, index) => errors.push(...validateValue(entry, items, `${path}[${index}]`)))
}
return errors
}
/** Build a minimal conforming sample value for schema-rendering checks. */
export function sampleValue(schema) {
if (schema.oneOf !== undefined) return sampleValue(schema.oneOf[0])
if (schema.const !== undefined) return schema.const
if (schema.enum !== undefined) return schema.enum[0]
switch (schema.type) {
case 'object': {
const out = {}
for (const [key, child] of Object.entries(schema.properties ?? {})) out[key] = sampleValue(child)
return out
}
case 'array':
return []
case 'string':
return 'sample'
case 'number':
case 'integer':
return 0
case 'boolean':
return false
case 'null':
return null
default:
return null
}
}