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:
oliver 2026-09-05 21:48:17 +08:00
parent 4447404361
commit 2f08136adf
17 changed files with 472 additions and 285 deletions

View file

@ -1,5 +1,5 @@
window.__ModuleLoader__.load({
id: '@dsh-external/dsh-cron-tasks',
id: 'dsh-ops-cron',
factory: (require) => {
const module = { exports: {} }
const exports = module.exports
@ -156,16 +156,16 @@ window.__ModuleLoader__.load({
return undefined
}
const name = 'dsh-cron-tasks'
const NS = 'dsh-cron-tasks'
const name = 'dsh-ops-cron'
const NS = 'dsh-ops-cron'
const inject = ['slots', 'locale', 'settingsScope']
const API = '/dsh-cron-tasks'
const API = '/dsh-ops-cron'
const LOCALE_NS = 'settings.dshCronTasks'
const TITLE_PREFIX = '定时任务 · '
const TITLE_PREFIX = '运维定时 · '
const listSnapshot = { jobs: [], runs: [], workspaces: [], catalog: { groups: [], current: null } }
function jobStamp(rows) {
return (rows || []).map((row) => `${row.id}:${row.updatedAt}:${row.nextRunAt}:${row.enabled}:${row.lastStatus}:${row.model}:${row.provider}`).join('|')
return (rows || []).map((row) => `${row.id}:${row.updatedAt}:${row.nextRunAt}:${row.enabled}:${row.lastStatus}:${row.model}:${row.provider}:${row.delivery?.kind}:${row.delivery?.targetId}`).join('|')
}
function runStamp(rows) {
@ -291,19 +291,19 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
`
const zh = {
title: '定时任务',
title: '运维定时',
description: '调度策略:启用、默认时区、历史保留、重叠与漏跑。',
expand: '展开设置', collapse: '收起设置', save: '保存', saving: '保存中…', discard: '放弃修改',
saveFailed: '本部署没有接受这些值,已保留供你修改。',
enabled: '启用调度器', timezone: '默认时区', historyLimit: '历史保留条数',
overlapPolicy: '重叠策略', misfirePolicy: '漏跑策略', policySkip: '跳过',
hint: '任务在左侧「新会话」下方进入。运行结果会出现在历史行里,点击记录会打开和普通会话一样的对话页,可以继续聊。',
entry: '定时任务', entryLabel: '打开定时任务',
entry: '运维定时', entryLabel: '打开运维定时',
backToWorkspace: '返回工作区', backLabel: '返回工作区',
newJob: '新建任务', name: '名称', prompt: '提示词', kind: '日程',
cron: 'Cron(循环)', at: '一次性时间', expr: 'Cron 表达式', atTime: '时间',
create: '创建', pause: '暂停', resume: '恢复', runNow: '立即运行', remove: '删除',
emptyJobs: '还没有定时任务。点右上角 + 新建。', next: '下次', last: '上次',
emptyJobs: '还没有运维定时。点右上角 + 新建。', next: '下次', last: '上次',
expandRuns: '展开记录', collapseRuns: '收起记录',
search: '搜索', searchPlaceholder: '搜索任务', searchClear: '清除搜索', searchEmpty: '没有匹配的任务。',
paused: '已暂停',
@ -313,19 +313,25 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
cwdHint: '运行会在这个目录对应的工作区里开新会话。留空则用最近工作区。',
model: '模型',
modelDefault: '每次运行用当时的新会话默认',
modelHint: '定时任务会消耗这个模型的额度。指定后不会跟着聊天模型变。',
editorLead: '保存后按日程在新会话里执行。点击下方某次运行,右侧会打开那次对话,可以继续。',
modelHint: '运维定时会消耗这个模型的额度。指定后不会跟着聊天模型变。',
editorLead: '保存后按日程在新会话里执行;若投递为 IM,结束后还会发摘要到 WhatsApp/IM。点击下方某次运行,右侧会打开那次对话,可以继续。',
lastOutput: '上次输出', noOutput: '还没有输出。先立即运行一次。',
delivery: '投递',
deliveryDsh: 'DSH 侧栏会话',
deliveryIm: 'WhatsApp / IM',
imBotId: 'IM Bot ID',
imTargetId: 'IM Target ID',
deliveryHint: 'Web 默认 DSH;WhatsApp 会话里由 Agent 创建时可自动匹配投递目标。也可粘贴 IM「投递设置」里的 botId/targetId。',
}
const en = {
title: 'Scheduled tasks',
title: 'Ops schedule',
description: 'Scheduler policy: enable, default time zone, history retention, overlap and misfire.',
expand: 'Show settings', collapse: 'Hide settings', save: 'Save', saving: 'Saving…', discard: 'Discard',
saveFailed: 'The deployment did not accept these values; they were left for you to correct.',
enabled: 'Enable scheduler', timezone: 'Default time zone', historyLimit: 'History retention',
overlapPolicy: 'Overlap policy', misfirePolicy: 'Misfire policy', policySkip: 'Skip',
hint: 'Open from under New Session. Run output appears in history; click a run to continue in the normal chat view.',
entry: 'Scheduled tasks', entryLabel: 'Open scheduled tasks',
entry: 'Ops schedule', entryLabel: 'Open ops schedule',
backToWorkspace: 'Back to workspace', backLabel: 'Back to workspace',
newJob: 'New job', name: 'Name', prompt: 'Prompt', kind: 'Schedule',
cron: 'Cron (recurring)', at: 'One-shot time', expr: 'Cron expression', atTime: 'Time',
@ -341,8 +347,14 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
model: 'Model',
modelDefault: 'Use the New Session default at fire time',
modelHint: 'Scheduled runs consume this model\'s quota. A pinned model will not follow the chat selector.',
editorLead: 'Runs start a new session. Click a run below to open that conversation and continue it.',
editorLead: 'Runs start a new session. IM delivery also sends a summary via dshIm after the turn. Click a run below to open that conversation.',
lastOutput: 'Last output', noOutput: 'No output yet. Run it once.',
delivery: 'Delivery',
deliveryDsh: 'DSH sidebar session',
deliveryIm: 'WhatsApp / IM',
imBotId: 'IM Bot ID',
imTargetId: 'IM Target ID',
deliveryHint: 'Web defaults to DSH. WhatsApp sessions can auto-match a delivery target. Or paste botId/targetId from IM delivery settings.',
}
function normalizePrefs(value) {
@ -585,6 +597,9 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
timezone: timezone || DEFAULTS.timezone, cwd: '', timeoutMinutes: 10, enabled: true,
provider: current?.provider || '',
model: current?.model || '',
deliveryKind: 'dsh',
imBotId: '',
imTargetId: '',
}
}
@ -601,9 +616,19 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
enabled: job.enabled !== false,
provider: job.provider || '',
model: job.model || '',
deliveryKind: job.delivery?.kind === 'im' ? 'im' : 'dsh',
imBotId: job.delivery?.botId || '',
imTargetId: job.delivery?.targetId || '',
}
}
function jobDeliveryLabel(job, t) {
if (job?.delivery?.kind === 'im') {
return `${t('deliveryIm')}:${job.delivery.targetId || '?'}`
}
return t('deliveryDsh')
}
function modelKey(provider, model) {
if (!provider || !model) return ''
return `${provider}::${model}`
@ -723,7 +748,7 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
h('span', { className: skin.projectText },
h('span', { className: skin.title }, paused
? `${job.name} · ${t('paused')}`
: (jobModelLabel(job) ? `${job.name} · ${jobModelLabel(job)}` : job.name))),
: `${job.name} · ${jobDeliveryLabel(job, t)}${jobModelLabel(job) ? ` · ${jobModelLabel(job)}` : ''}`)),
h('span', { className: skin.rowActs, onClick: (e) => e.stopPropagation() },
h('button', { type: 'button', className: skin.rowIcon, title: t('runNow'), onClick: () => onRun(job.id) },
playIcon() || '▶'),
@ -865,6 +890,28 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
),
h(CwdField, { t, form, setForm, workspaces }),
h(ModelField, { t, form, setForm, catalog }),
h('label', null, t('delivery'),
h('select', {
value: form.deliveryKind || 'dsh',
onChange: (e) => setForm({ ...form, deliveryKind: e.target.value }),
},
h('option', { value: 'dsh' }, t('deliveryDsh')),
h('option', { value: 'im' }, t('deliveryIm')),
),
),
form.deliveryKind === 'im'
? h('div', { className: 'dsh-ct-editorRow' },
h('label', null, t('imBotId'), h('input', {
value: form.imBotId || '',
onChange: (e) => setForm({ ...form, imBotId: e.target.value }),
})),
h('label', null, t('imTargetId'), h('input', {
value: form.imTargetId || '',
onChange: (e) => setForm({ ...form, imTargetId: e.target.value }),
})),
)
: null,
h('span', { className: 'dsh-ct-cwdHint' }, t('deliveryHint')),
h('div', { className: 'dsh-ct-editorActs' },
h('button', { type: 'button', className: 'dsh-ct-primary', onClick: onSave }, t('save')),
!isNew ? h('button', { type: 'button', className: 'dsh-ct-secondary', onClick: onRun }, t('runNow')) : null,
@ -1037,6 +1084,9 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
name: form.name, prompt: form.prompt, schedule,
cwd: form.cwd, timeoutMinutes: form.timeoutMinutes, enabled: form.enabled,
provider: form.provider, model: form.model,
delivery: form.deliveryKind === 'im'
? { kind: 'im', botId: form.imBotId, targetId: form.imTargetId }
: { kind: 'dsh' },
}
if (selection.type === 'job' && selection.jobId) {
await api(`/jobs/${selection.jobId}`, { method: 'PATCH', body: JSON.stringify(payload) })
@ -1147,14 +1197,14 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
function isCronTitle(text) {
const value = String(text || '').trim()
return value.startsWith('定时任务') || value.startsWith(TITLE_PREFIX.trim()) || value.includes(TITLE_PREFIX)
return value.startsWith('运维定时') || value.startsWith(TITLE_PREFIX.trim()) || value.includes(TITLE_PREFIX)
}
function workspaceTitleFromCronFork(title) {
const value = String(title || '').trim()
if (value.startsWith(TITLE_PREFIX)) return value.slice(TITLE_PREFIX.length).trim() || value
if (value.startsWith('定时任务')) {
const stripped = value.replace(/^定时任务(?:\s*·\s*|\s+)/, '').trim()
if (value.startsWith('运维定时')) {
const stripped = value.replace(/^运维定时(?:\s*·\s*|\s+)/, '').trim()
return stripped || value
}
return value
@ -1169,7 +1219,7 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
const parent = String(row.parentId || row.parentSessionId || '').trim()
if (parent && ids.has(parent)) return true
const title = String(row.title || row.displayTitle || '')
const looksCron = title.startsWith(TITLE_PREFIX) || title.startsWith('定时任务')
const looksCron = title.startsWith(TITLE_PREFIX) || title.startsWith('运维定时')
if (previousId && ids.has(String(previousId)) && looksCron) return true
return false
}
@ -1411,7 +1461,7 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
tag.textContent = CSS
document.head.appendChild(tag)
return () => tag.remove()
}, 'dsh-cron-tasks: styles')
}, 'dsh-ops-cron: styles')
ctx.effect(() => {
try {
@ -1421,7 +1471,7 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
const offEn = ctx.locale.register(LOCALE_NS, 'en', en)
return () => { offZh && offZh(); offEn && offEn() }
}
}, 'dsh-cron-tasks: locale')
}, 'dsh-ops-cron: locale')
const scope = ctx.settingsScope.bind({ namespace: NS })
const form = createForm(scope)
@ -1473,7 +1523,7 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
sessions.open(sessionId)
} catch (error) {
lastError = error
console.warn('[dsh-cron-tasks] open session failed', sessionId, error)
console.warn('[dsh-ops-cron] open session failed', sessionId, error)
try { await sessions.refresh?.() } catch { /* ignore */ }
await sleep(80)
continue
@ -1484,8 +1534,8 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
if (sessions.list?.getSnapshot?.()?.current === sessionId) return true
}
}
if (lastError) console.warn('[dsh-cron-tasks] giving up open', sessionId, lastError)
else console.warn('[dsh-cron-tasks] open session timed out', sessionId)
if (lastError) console.warn('[dsh-ops-cron] giving up open', sessionId, lastError)
else console.warn('[dsh-ops-cron] open session timed out', sessionId)
return false
},
clearSession() {
@ -1519,7 +1569,7 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
if (typeof off === 'function') offs.push(off)
} catch { /* optional */ }
return () => { for (const off of offs) off() }
}, 'dsh-cron-tasks: session faces')
}, 'dsh-ops-cron: session faces')
ctx.effect(() => {
const promoted = new Set()
@ -1543,12 +1593,12 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
if (session && typeof session.rename === 'function') await session.rename(nextTitle)
}
} catch (error) {
console.warn('[dsh-cron-tasks] fork rename failed', row.id, error)
console.warn('[dsh-ops-cron] fork rename failed', row.id, error)
}
try {
await api(`/sessions/${encodeURIComponent(row.id)}/adopt`, { method: 'POST' })
} catch (error) {
console.warn('[dsh-cron-tasks] fork adopt failed', row.id, error)
console.warn('[dsh-ops-cron] fork adopt failed', row.id, error)
}
try { faces.leaveCronMode?.() } catch { /* ignore */ }
}
@ -1573,14 +1623,14 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
clearInterval(timer)
if (typeof unsub === 'function') unsub()
}
}, 'dsh-cron-tasks: fork promote')
}, 'dsh-ops-cron: fork promote')
ctx.effect(() => {
const lang = ctx.locale?.locale || ctx.locale?.current || 'zh'
const dict = String(lang).toLowerCase().startsWith('zh') ? zh : en
const t = (key) => dict[key] || zh[key] || key
return installChrome(t, faces)
}, 'dsh-cron-tasks: chrome')
}, 'dsh-ops-cron: chrome')
}
exports.name = name