diff --git a/lib/store.js b/lib/store.js index 0467927..bf20a7a 100644 --- a/lib/store.js +++ b/lib/store.js @@ -222,10 +222,10 @@ export function createStore(options = {}) { error.code = 'STORE_CORRUPT' try { const bak = `${filePath}.corrupt.${Date.now()}.bak` - await writeFile(bak, raw, 'utf8') + await rename(filePath, bak) error.message = `${error.message} (backup: ${bak})` } catch { - // best-effort backup + // best-effort quarantine; persist() below overwrites either way } console.warn(`[dsh-ops-cron] ${error.message}`) state = emptyState() diff --git a/lib/tools.js b/lib/tools.js index 9ca9d19..a229f9d 100644 --- a/lib/tools.js +++ b/lib/tools.js @@ -41,9 +41,10 @@ const JOB_SCHEMA = { schedule: { type: 'object', additionalProperties: true }, createdAt: { type: 'number' }, updatedAt: { type: 'number' }, - lastRunAt: { type: ['number', 'null'] }, - lastStatus: { type: ['string', 'null'] }, - nextRunAt: { type: ['number', 'null'] }, + // Host assertSupportedJsonSchema rejects type arrays — use oneOf for nullables. + lastRunAt: { oneOf: [{ type: 'number' }, { type: 'null' }] }, + lastStatus: { oneOf: [{ type: 'string' }, { type: 'null' }] }, + nextRunAt: { oneOf: [{ type: 'number' }, { type: 'null' }] }, }, } @@ -497,8 +498,9 @@ export function cronGuidanceText(nowMs = Date.now(), timeZone = 'Asia/Shanghai') 'Agent preset: omit agent_preset to inherit (WhatsApp chat/group preset → creating session → Host default). Pass agent_preset to pin a preset for every scheduled run.', '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 are in your tool list, call them.', - '2. If they are not listed, search/unlock tools or skills with query "定时任务" or "cron" (skill_search, skill_load, or dev_tool_search) and then call them.', - '3. Never run crontab, never read /etc/cron*, and never say there are no tasks until cron_list has returned.', + '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).', + '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.', + '4. Never run crontab, never read /etc/cron*, and never say there are no tasks until cron_list has returned.', ].join('\n') } diff --git a/package.json b/package.json index f696f7a..0d02a93 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "dsh-ops-cron", "description": "Scheduled tasks for DeepSeek Harness: Agent cron_* tools + sidebar 定时任务, with DSH or WhatsApp/IM delivery per job.", - "version": "0.1.12", + "version": "0.1.13", "private": false, "type": "module", "repository": { diff --git a/test/tools.test.js b/test/tools.test.js index f67473e..bb6bbad 100644 --- a/test/tools.test.js +++ b/test/tools.test.js @@ -435,3 +435,25 @@ test('registerCronTools registers each definition and disposer unregisters', () off() assert.deepEqual(registered, []) }) + +test('cron tool output schemas never use type arrays (Host rejects them)', () => { + const defs = cronToolDefinitions({ + async createJob() { return {} }, + async listJobs() { return [] }, + async pauseJob() { return {} }, + async resumeJob() { return {} }, + async deleteJob() { return {} }, + }) + const bad = [] + function walk(node, path) { + if (!node || typeof node !== 'object') return + if (Array.isArray(node.type)) bad.push(`${path}.type=${JSON.stringify(node.type)}`) + if (Array.isArray(node.oneOf)) node.oneOf.forEach((branch, i) => walk(branch, `${path}.oneOf[${i}]`)) + if (node.properties && typeof node.properties === 'object') { + for (const [key, child] of Object.entries(node.properties)) walk(child, `${path}.properties.${key}`) + } + if (node.items) walk(node.items, `${path}.items`) + } + for (const def of defs) walk(def.output?.schema, def.name) + assert.deepEqual(bad, []) +})