mirror of
https://github.com/hansjone/dsh-ops-cron.git
synced 2026-10-08 23:20:45 +08:00
Fix cron_* Host registration: additionalProperties must be boolean.
labels schemas used additionalProperties:{type:string}, which assertSupportedJsonSchema rejects and aborted the whole tools.register batch.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
2e13432b0a
commit
6ed7187754
2 changed files with 14 additions and 8 deletions
14
lib/tools.js
14
lib/tools.js
|
|
@ -38,7 +38,7 @@ const JOB_SCHEMA = {
|
||||||
delivery: { type: 'object', additionalProperties: true },
|
delivery: { type: 'object', additionalProperties: true },
|
||||||
origin: { type: 'object', additionalProperties: true },
|
origin: { type: 'object', additionalProperties: true },
|
||||||
mirrorToSession: { type: 'boolean' },
|
mirrorToSession: { type: 'boolean' },
|
||||||
labels: { type: 'object', additionalProperties: { type: 'string' } },
|
labels: { type: 'object', additionalProperties: true },
|
||||||
watch: { type: 'object', additionalProperties: true },
|
watch: { type: 'object', additionalProperties: true },
|
||||||
progress: { type: 'object', additionalProperties: true },
|
progress: { type: 'object', additionalProperties: true },
|
||||||
report: { type: 'object', additionalProperties: true },
|
report: { type: 'object', additionalProperties: true },
|
||||||
|
|
@ -388,7 +388,7 @@ export function cronToolDefinitions(service, deps = {}) {
|
||||||
im_target_id: { type: 'string', description: 'Opaque targetId from IM 投递设置 when delivery=im (Web/sidebar only; IM chats cannot retarget).' },
|
im_target_id: { type: 'string', description: 'Opaque targetId from IM 投递设置 when delivery=im (Web/sidebar only; IM chats cannot retarget).' },
|
||||||
agent_preset: { type: 'string', description: 'Agent preset id for scheduled runs. Omit to inherit from the current WhatsApp/IM chat or Host default.' },
|
agent_preset: { type: 'string', description: 'Agent preset id for scheduled runs. Omit to inherit from the current WhatsApp/IM chat or Host default.' },
|
||||||
mirror_to_session: { type: 'boolean', description: 'If true, after each fire append the run summary into the origin WhatsApp/Web session as a system-reminder (no new model turn). Default false — results stay in run history (and IM delivery when configured).' },
|
mirror_to_session: { type: 'boolean', description: 'If true, after each fire append the run summary into the origin WhatsApp/Web session as a system-reminder (no new model turn). Default false — results stay in run history (and IM delivery when configured).' },
|
||||||
labels: { type: 'object', additionalProperties: { type: 'string' }, description: 'String labels for discovery/monitoring, e.g. {"role":"worker","task":"theory","owner":"alice"}.' },
|
labels: { type: 'object', additionalProperties: true, description: 'String labels for discovery/monitoring, e.g. {"role":"worker","task":"theory","owner":"alice"}.' },
|
||||||
watch: { type: 'object', additionalProperties: true, description: 'Listener binding: {"taskId":"..."} and/or {"labels":{...},"match":"any|all","timeout_min":30}.' },
|
watch: { type: 'object', additionalProperties: true, description: 'Listener binding: {"taskId":"..."} and/or {"labels":{...},"match":"any|all","timeout_min":30}.' },
|
||||||
progress: { type: 'object', additionalProperties: true, description: 'Progress declaration: {"channel":{"file":"progress.json"},"metrics":[{"key":"done","label":"已完成"}]}.' },
|
progress: { type: 'object', additionalProperties: true, description: 'Progress declaration: {"channel":{"file":"progress.json"},"metrics":[{"key":"done","label":"已完成"}]}.' },
|
||||||
report: { type: 'object', additionalProperties: true, description: 'Watcher report: {"mode":"on_change|on_complete|periodic","delivery":"dsh|im|none","interval_min":15}.' },
|
report: { type: 'object', additionalProperties: true, description: 'Watcher report: {"mode":"on_change|on_complete|periodic","delivery":"dsh|im|none","interval_min":15}.' },
|
||||||
|
|
@ -472,7 +472,7 @@ export function cronToolDefinitions(service, deps = {}) {
|
||||||
enabled_only: { type: 'boolean', description: 'If true, omit paused jobs.' },
|
enabled_only: { type: 'boolean', description: 'If true, omit paused jobs.' },
|
||||||
state: { type: 'string', description: 'Lifecycle filter: idle|pending|running|succeeded|failed|paused.' },
|
state: { type: 'string', description: 'Lifecycle filter: idle|pending|running|succeeded|failed|paused.' },
|
||||||
task_id: { type: 'string', description: 'Exact job id.' },
|
task_id: { type: 'string', description: 'Exact job id.' },
|
||||||
labels: { type: 'object', additionalProperties: { type: 'string' }, description: 'Label filter (all keys must match unless used with cron_query match=any).' },
|
labels: { type: 'object', additionalProperties: true, description: 'Label filter (all keys must match unless used with cron_query match=any).' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
|
|
@ -514,7 +514,7 @@ export function cronToolDefinitions(service, deps = {}) {
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
properties: {
|
properties: {
|
||||||
task_id: { type: 'string', description: 'Watch a single job id.' },
|
task_id: { type: 'string', description: 'Watch a single job id.' },
|
||||||
labels: { type: 'object', additionalProperties: { type: 'string' }, description: 'Label selector.' },
|
labels: { type: 'object', additionalProperties: true, description: 'Label selector.' },
|
||||||
match: { type: 'string', description: 'Label match mode: all (default) or any.' },
|
match: { type: 'string', description: 'Label match mode: all (default) or any.' },
|
||||||
state: { type: 'string', description: 'Filter projected lifecycle state.' },
|
state: { type: 'string', description: 'Filter projected lifecycle state.' },
|
||||||
include_runs: { type: 'boolean', description: 'Include run instances per job.' },
|
include_runs: { type: 'boolean', description: 'Include run instances per job.' },
|
||||||
|
|
@ -619,7 +619,7 @@ export function cronToolDefinitions(service, deps = {}) {
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
properties: {
|
properties: {
|
||||||
task_id: { type: 'string', description: 'Job id.' },
|
task_id: { type: 'string', description: 'Job id.' },
|
||||||
labels: { type: 'object', additionalProperties: { type: 'string' }, description: 'Label selector (by_watch).' },
|
labels: { type: 'object', additionalProperties: true, description: 'Label selector (by_watch).' },
|
||||||
match: { type: 'string', description: 'any|all for labels.' },
|
match: { type: 'string', description: 'any|all for labels.' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -763,8 +763,8 @@ export function cronGuidanceText(nowMs = Date.now(), timeZone = 'Asia/Shanghai')
|
||||||
'Labels/monitor: pass labels={"role":"worker","task":"theory"} on workers; listeners pass watch={"taskId":"..."} or watch={"labels":{...},"match":"all"}. Use cron_query / cron_progress / cron_runs to poll state and progress.',
|
'Labels/monitor: pass labels={"role":"worker","task":"theory"} on workers; listeners pass watch={"taskId":"..."} or watch={"labels":{...},"match":"all"}. Use cron_query / cron_progress / cron_runs to poll state and progress.',
|
||||||
'When the user asks to look at, create, pause, resume, or delete 定时任务 / scheduled tasks / cron jobs:',
|
'When the user asks to look at, create, pause, resume, or delete 定时任务 / scheduled tasks / cron jobs:',
|
||||||
'1. If cron_list / cron_create / cron_pause / cron_resume / cron_delete / cron_query / cron_runs / cron_progress are in your tool list, call them.',
|
'1. If cron_list / cron_create / cron_pause / cron_resume / cron_delete / cron_query / cron_runs / cron_progress are in your tool list, call them.',
|
||||||
'2. If they are not listed, load skill "scheduled-tasks" via the skill tool (exact name), then retry the cron_* tools. Those tools are registered by the dsh-ops-cron plugin — they are not unlocked by skill_search/dev_tool_search (those names are obsolete).',
|
'2. If they are not listed, load skill "scheduled-tasks" for usage guidance, then look again. skill_load does NOT inject tools — cron_* are registered by the dsh-ops-cron Host plugin at boot.',
|
||||||
'3. If cron_* still return unknown tool after loading the skill, the plugin failed to register (check Host logs for unsupported JSON schema). Tell the user; do not invent crontab workarounds.',
|
'3. If cron_* are still missing after a Host restart with the latest dsh-ops-cron, the plugin failed to register (check Host logs for unsupported JSON schema / tools.register). Tell the user; do not invent crontab workarounds.',
|
||||||
'4. Never run crontab, never read /etc/cron*, and never say there are no tasks until cron_list has returned.',
|
'4. Never run crontab, never read /etc/cron*, and never say there are no tasks until cron_list has returned.',
|
||||||
].join('\n')
|
].join('\n')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -460,12 +460,18 @@ test('cron tool output schemas never use type arrays (Host rejects them)', () =>
|
||||||
function walk(node, path) {
|
function walk(node, path) {
|
||||||
if (!node || typeof node !== 'object') return
|
if (!node || typeof node !== 'object') return
|
||||||
if (Array.isArray(node.type)) bad.push(`${path}.type=${JSON.stringify(node.type)}`)
|
if (Array.isArray(node.type)) bad.push(`${path}.type=${JSON.stringify(node.type)}`)
|
||||||
|
if (Object.hasOwn(node, 'additionalProperties') && typeof node.additionalProperties !== 'boolean') {
|
||||||
|
bad.push(`${path}.additionalProperties must be boolean (got ${typeof node.additionalProperties})`)
|
||||||
|
}
|
||||||
if (Array.isArray(node.oneOf)) node.oneOf.forEach((branch, i) => walk(branch, `${path}.oneOf[${i}]`))
|
if (Array.isArray(node.oneOf)) node.oneOf.forEach((branch, i) => walk(branch, `${path}.oneOf[${i}]`))
|
||||||
if (node.properties && typeof node.properties === 'object') {
|
if (node.properties && typeof node.properties === 'object') {
|
||||||
for (const [key, child] of Object.entries(node.properties)) walk(child, `${path}.properties.${key}`)
|
for (const [key, child] of Object.entries(node.properties)) walk(child, `${path}.properties.${key}`)
|
||||||
}
|
}
|
||||||
if (node.items) walk(node.items, `${path}.items`)
|
if (node.items) walk(node.items, `${path}.items`)
|
||||||
}
|
}
|
||||||
for (const def of defs) walk(def.output?.schema, def.name)
|
for (const def of defs) {
|
||||||
|
walk(def.output?.schema, `${def.name}.output`)
|
||||||
|
walk(def.parameters, `${def.name}.parameters`)
|
||||||
|
}
|
||||||
assert.deepEqual(bad, [])
|
assert.deepEqual(bad, [])
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue