mirror of
https://github.com/hansjone/dsh-ops-cron.git
synced 2026-10-09 04:40:48 +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
108
lib/client.js
108
lib/client.js
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue