mirror of
https://github.com/hansjone/dsh-ops-cron.git
synced 2026-10-10 19:50:52 +08:00
Fork dsh-cron-tasks as dsh-ops-cron with DSH/IM delivery.
Agent cron_* jobs default to IM when created from a WhatsApp session with a matching delivery target; otherwise DSH sidebar sessions. Fire path soft-calls ctx.dshIm.send for summaries. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
4447404361
commit
2f08136adf
17 changed files with 472 additions and 285 deletions
57
lib/tools.js
57
lib/tools.js
|
|
@ -4,6 +4,7 @@
|
|||
*/
|
||||
|
||||
import { formatInZone, resolveTodayAt } from './scheduler.js'
|
||||
import { deliveryLine, resolveCreateDelivery } from './delivery.js'
|
||||
|
||||
const JOB_SCHEMA = {
|
||||
type: 'object',
|
||||
|
|
@ -18,6 +19,7 @@ const JOB_SCHEMA = {
|
|||
provider: { type: 'string' },
|
||||
model: { type: 'string' },
|
||||
reasoningEffort: { type: 'string' },
|
||||
delivery: { type: 'object', additionalProperties: true },
|
||||
schedule: { type: 'object', additionalProperties: true },
|
||||
createdAt: { type: 'number' },
|
||||
updatedAt: { type: 'number' },
|
||||
|
|
@ -93,7 +95,8 @@ function jobLine(job) {
|
|||
? `at ${job.schedule.at}`
|
||||
: (job.schedule?.expr || 'cron')
|
||||
const model = job.provider && job.model ? `${job.provider}/${job.model}` : 'default-model'
|
||||
return `${job.name} [${state}] ${sched} tz=${tz} cwd=${job.cwd || '(recent workspace)'} model=${model} next=${when} id=${job.id}`
|
||||
const delivery = deliveryLine(job.delivery)
|
||||
return `${job.name} [${state}] ${sched} tz=${tz} cwd=${job.cwd || '(recent workspace)'} model=${model} ${delivery} next=${when} id=${job.id}`
|
||||
}
|
||||
|
||||
export function callerWorkingDirectory(exec) {
|
||||
|
|
@ -137,16 +140,17 @@ function requireId(args) {
|
|||
return id
|
||||
}
|
||||
|
||||
export function cronToolDefinitions(service) {
|
||||
export function cronToolDefinitions(service, deps = {}) {
|
||||
const getDshIm = typeof deps.getDshIm === 'function' ? deps.getDshIm : () => undefined
|
||||
return [
|
||||
{
|
||||
name: 'cron_create',
|
||||
description: 'Create a DSH 定时任务 (sidebar scheduled job). NEVER use crontab. NOT schedule_create. Default timezone Asia/Shanghai. For "in N minutes / 一分钟后" pass only after_minutes. For "today/tonight at HH:MM" pass only hour+minute (24h; hour=0 is midnight). Do not also send at or expr. Extra schedule fields are ignored (after_minutes > hour > at > expr). cwd MUST be the current workspace path when chatting in a workspace; if omitted, uses the current session working directory. Pass provider+model for the job; if omitted, snapshots the current session model. Scheduled runs consume that model\'s quota, not whatever the user happens to have selected later.',
|
||||
description: 'Create a DSH 运维定时 job (sidebar scheduled job). NEVER use crontab. NOT schedule_create. Default timezone Asia/Shanghai. For "in N minutes / 一分钟后" pass only after_minutes. For "today/tonight at HH:MM" pass only hour+minute (24h; hour=0 is midnight). Do not also send at or expr. Extra schedule fields are ignored (after_minutes > hour > at > expr). cwd MUST be the current workspace path when chatting in a workspace; if omitted, uses the current session working directory. Pass provider+model for the job; if omitted, snapshots the current session model. Delivery: omit for auto (WhatsApp/IM session → im if a matching 投递目标 exists; else dsh). Or pass delivery=dsh|im with im_bot_id+im_target_id from IM 投递设置.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
name: { type: 'string', description: 'Short display name in the 定时任务 list.' },
|
||||
name: { type: 'string', description: 'Short display name in the 运维定时 list.' },
|
||||
prompt: { type: 'string', description: 'Self-contained instructions for the new session when the job fires.' },
|
||||
expr: { type: 'string', description: '5-field cron in the given timezone: "minute hour day-of-month month day-of-week". Recurring jobs only. 22:55 every day = "55 22 * * *". Do not send expr together with after_minutes/hour/at — extras are ignored.' },
|
||||
cron: { type: 'string', description: 'Alias of expr.' },
|
||||
|
|
@ -163,6 +167,9 @@ export function cronToolDefinitions(service) {
|
|||
reasoning_effort: { type: 'string', description: 'Optional reasoning effort for this job.' },
|
||||
timeout_minutes: { type: 'integer', description: 'Per-run timeout in minutes, 1-240.' },
|
||||
enabled: { type: 'boolean', description: 'If false, create paused. Default true.' },
|
||||
delivery: { type: 'string', description: 'dsh (sidebar session) or im (proactive WhatsApp/IM via botId+targetId). Omit to auto-detect from the current session.' },
|
||||
im_bot_id: { type: 'string', description: 'Opaque botId from IM 投递设置 when delivery=im.' },
|
||||
im_target_id: { type: 'string', description: 'Opaque targetId from IM 投递设置 when delivery=im.' },
|
||||
},
|
||||
required: ['name', 'prompt'],
|
||||
},
|
||||
|
|
@ -183,14 +190,16 @@ export function cronToolDefinitions(service) {
|
|||
const model = job?.provider && job?.model
|
||||
? ` model=${job.provider}/${job.model}`
|
||||
: ' model=(new-session default at fire time)'
|
||||
return text(`Created "${job?.name}" (${job?.id}) ${sched} in ${tz}.${cwd}.${model}. Next run: ${when}. If that local time is wrong, delete this job and recreate with 24h hours (22=10pm).`)
|
||||
const delivery = ` ${deliveryLine(job?.delivery)}`
|
||||
return text(`Created "${job?.name}" (${job?.id}) ${sched} in ${tz}.${cwd}.${model}.${delivery}. Next run: ${when}. If that local time is wrong, delete this job and recreate with 24h hours (22=10pm).`)
|
||||
},
|
||||
},
|
||||
presentCall: (args) => ({ card: 'generic', title: '创建定时任务', content: String(args?.name || '') }),
|
||||
presentCall: (args) => ({ card: 'generic', title: '创建运维定时', content: String(args?.name || '') }),
|
||||
async execute(args, exec) {
|
||||
aborted(exec)
|
||||
const timeout = Number(args.timeout_minutes)
|
||||
try {
|
||||
const delivery = await resolveCreateDelivery(args, exec, { dshIm: getDshIm() })
|
||||
const job = await service.createJob({
|
||||
name: args.name,
|
||||
prompt: args.prompt,
|
||||
|
|
@ -199,6 +208,7 @@ export function cronToolDefinitions(service) {
|
|||
...resolveCreateModel(args, exec),
|
||||
timeoutMinutes: Number.isFinite(timeout) && timeout > 0 ? timeout : undefined,
|
||||
enabled: args.enabled !== false,
|
||||
delivery,
|
||||
})
|
||||
return { job }
|
||||
} catch (error) {
|
||||
|
|
@ -215,7 +225,7 @@ export function cronToolDefinitions(service) {
|
|||
},
|
||||
{
|
||||
name: 'cron_list',
|
||||
description: 'List DSH 定时任务 (sidebar scheduled jobs). Use this whenever the user asks what scheduled tasks exist. NEVER use crontab -l or /etc/cron* — those are OS crontabs, not this plugin.',
|
||||
description: 'List DSH 运维定时 jobs (sidebar scheduled jobs). Use this whenever the user asks what scheduled tasks exist. NEVER use crontab -l or /etc/cron* — those are OS crontabs, not this plugin.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
|
|
@ -238,7 +248,7 @@ export function cronToolDefinitions(service) {
|
|||
return text(rows.length ? `Scheduled tasks (${value.count}):\n${rows.join('\n')}` : 'No scheduled tasks yet.')
|
||||
},
|
||||
},
|
||||
presentCall: () => ({ card: 'generic', title: '列出定时任务' }),
|
||||
presentCall: () => ({ card: 'generic', title: '列出运维定时' }),
|
||||
async execute(args, exec) {
|
||||
aborted(exec)
|
||||
let jobs = await service.listJobs()
|
||||
|
|
@ -248,7 +258,7 @@ export function cronToolDefinitions(service) {
|
|||
},
|
||||
{
|
||||
name: 'cron_pause',
|
||||
description: 'Pause a scheduled task by id from cron_list / cron_create. It stays in 定时任务 but will not fire until resumed.',
|
||||
description: 'Pause a scheduled task by id from cron_list / cron_create. It stays in 运维定时 but will not fire until resumed.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
|
|
@ -259,7 +269,7 @@ export function cronToolDefinitions(service) {
|
|||
schema: { type: 'object', additionalProperties: true, properties: { job: JOB_SCHEMA } },
|
||||
render: (_args, value) => text(`Paused "${value.job?.name}" (${value.job?.id}).`),
|
||||
},
|
||||
presentCall: (args) => ({ card: 'generic', title: '暂停定时任务', content: String(args?.id || '') }),
|
||||
presentCall: (args) => ({ card: 'generic', title: '暂停运维定时', content: String(args?.id || '') }),
|
||||
async execute(args, exec) {
|
||||
aborted(exec)
|
||||
const job = await service.pauseJob(requireId(args), false)
|
||||
|
|
@ -279,7 +289,7 @@ export function cronToolDefinitions(service) {
|
|||
schema: { type: 'object', additionalProperties: true, properties: { job: JOB_SCHEMA } },
|
||||
render: (_args, value) => text(`Resumed "${value.job?.name}" (${value.job?.id}).`),
|
||||
},
|
||||
presentCall: (args) => ({ card: 'generic', title: '恢复定时任务', content: String(args?.id || '') }),
|
||||
presentCall: (args) => ({ card: 'generic', title: '恢复运维定时', content: String(args?.id || '') }),
|
||||
async execute(args, exec) {
|
||||
aborted(exec)
|
||||
const job = await service.pauseJob(requireId(args), true)
|
||||
|
|
@ -307,7 +317,7 @@ export function cronToolDefinitions(service) {
|
|||
},
|
||||
render: (_args, value) => text(value.deleted ? `Deleted scheduled task ${value.id}.` : `Scheduled task ${value.id} was not found.`),
|
||||
},
|
||||
presentCall: (args) => ({ card: 'generic', title: '删除定时任务', content: String(args?.id || '') }),
|
||||
presentCall: (args) => ({ card: 'generic', title: '删除运维定时', content: String(args?.id || '') }),
|
||||
async execute(args, exec) {
|
||||
aborted(exec)
|
||||
const id = requireId(args)
|
||||
|
|
@ -326,16 +336,17 @@ export function cronToolDefinitions(service) {
|
|||
export function cronGuidanceText(nowMs = Date.now(), timeZone = 'Asia/Shanghai') {
|
||||
const now = formatInZone(nowMs, timeZone)
|
||||
return [
|
||||
'This deployment has a DSH sidebar feature named 定时任务 (scheduled tasks).',
|
||||
'Jobs live in that list and fire a fresh session. They are NOT OS crontab and NOT in-chat schedule_create reminders.',
|
||||
'This deployment has a DSH sidebar feature named 运维定时 (ops scheduled tasks / dsh-ops-cron).',
|
||||
'Jobs live in that list and fire a fresh session (and optionally deliver a summary to WhatsApp/IM). They are NOT OS crontab and NOT in-chat schedule_create reminders.',
|
||||
`Current local time for scheduling: ${now}. Use this calendar date for "today"/"tonight". Never invent another month or day.`,
|
||||
'For "in N minutes / 一分钟后", call cron_create with after_minutes=N only (do not also pass at, hour, or expr).',
|
||||
'For a clock time tonight, pass only hour and minute in 24h (晚上11点34 → hour=23, minute=34; 零点33 → hour=0, minute=33). Extra at/expr fields are ignored.',
|
||||
'Working directory: if the user is chatting in a workspace, pass cwd as that workspace filesystem path (the current session working directory). If they name another workspace, use that path. If cwd is omitted, cron_create uses the current session cwd. Do not invent ~/.dsh/cron-tasks/workspace unless they asked for an isolated folder.',
|
||||
'Model: pass provider+model for the job. If omitted, cron_create stores the current session model. Scheduled runs consume that model\'s quota. Ask which model if the user cares about billing.',
|
||||
'When the user asks to look at, create, pause, resume, or delete 定时任务 / scheduled tasks / cron jobs:',
|
||||
'Working directory: if the user is chatting in a workspace, pass cwd as that workspace filesystem path (the current session working directory). If they name another workspace, use that path. If cwd is omitted, cron_create uses the current session cwd.',
|
||||
'Model: pass provider+model for the job. If omitted, cron_create stores the current session model. Scheduled runs consume that model\'s quota.',
|
||||
'Delivery: when chatting on WhatsApp/IM, omit delivery so the job defaults to im (requires a matching 投递目标 for this peer). On Web/DSH, default is dsh. Or pass delivery=im with im_bot_id+im_target_id from IM 投递设置 → 复制调用参数.',
|
||||
'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.',
|
||||
'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.',
|
||||
].join('\n')
|
||||
}
|
||||
|
|
@ -345,15 +356,15 @@ export const CRON_GUIDANCE = cronGuidanceText()
|
|||
export function makeCronSkill() {
|
||||
return {
|
||||
name: 'scheduled-tasks',
|
||||
description: '定时任务: 查看、创建、暂停、恢复、删除 DSH 侧栏定时任务(今天晚上几点、一次性执行、scheduled job、cron)。不要用系统 crontab,也不是会话内 reminder。',
|
||||
whenToUse: 'User asks to list or create 定时任务, schedule something for tonight/today, pause a job, or mentions cron / scheduled tasks in DeepSeek Harness.',
|
||||
description: '运维定时: 查看、创建、暂停、恢复、删除 DSH 侧栏定时任务(今天晚上几点、一次性执行、scheduled job、cron)。WhatsApp 里创建默认回投 IM;Web 里创建进侧栏会话。不要用系统 crontab,也不是会话内 reminder。',
|
||||
whenToUse: 'User asks to list or create 运维定时 / 定时任务, schedule something for tonight/today, pause a job, or mentions cron / scheduled tasks in DeepSeek Harness.',
|
||||
source: 'runtime',
|
||||
provider: 'runtime',
|
||||
content: `${cronGuidanceText()}
|
||||
|
||||
Tools:
|
||||
- cron_list — list jobs
|
||||
- cron_create — "一分钟后" → after_minutes=1. Clock time → hour+minute only. Do not send at/expr at the same time. Pass cwd as the current workspace path when creating from a workspace chat. Pass provider+model or inherit the current session model.
|
||||
- cron_create — "一分钟后" → after_minutes=1. Clock time → hour+minute only. Do not send at/expr at the same time. Pass cwd as the current workspace path when creating from a workspace chat. Pass provider+model or inherit the current session model. Delivery auto from session, or delivery=im + im_bot_id + im_target_id.
|
||||
- cron_pause / cron_resume / cron_delete — by id from cron_list
|
||||
`,
|
||||
}
|
||||
|
|
@ -361,10 +372,10 @@ Tools:
|
|||
|
||||
export const CRON_SKILL = makeCronSkill()
|
||||
|
||||
export function registerCronTools(ctx, service) {
|
||||
export function registerCronTools(ctx, service, deps = {}) {
|
||||
const tools = ctx?.tools
|
||||
if (!tools || typeof tools.register !== 'function') return () => {}
|
||||
const offs = cronToolDefinitions(service).map((definition) => tools.register(definition))
|
||||
const offs = cronToolDefinitions(service, deps).map((definition) => tools.register(definition))
|
||||
return () => {
|
||||
for (const off of offs) {
|
||||
if (typeof off === 'function') off()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue