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

118
lib/delivery.js Normal file
View file

@ -0,0 +1,118 @@
/**
* Job delivery: DSH sidebar session vs proactive IM (botId + targetId).
*/
export function normalizeDelivery(input = {}) {
const src = input && typeof input === 'object' ? input : {}
const kind = String(src.kind || 'dsh').trim().toLowerCase()
if (kind === 'im') {
const botId = typeof src.botId === 'string' ? src.botId.trim() : ''
const targetId = typeof src.targetId === 'string' ? src.targetId.trim() : ''
if (!botId || !targetId) {
const error = new Error('im delivery requires botId and targetId')
error.code = 'INVALID_DELIVERY'
throw error
}
return { kind: 'im', botId, targetId }
}
return { kind: 'dsh' }
}
export function deliveryLine(delivery) {
if (!delivery || delivery.kind !== 'im') return 'delivery=dsh'
return `delivery=im botId=${delivery.botId} targetId=${delivery.targetId}`
}
/**
* Match a delivery target whose route overlaps the channel peer conversation.
* @param {Array<object>} targets
* @param {object|null} peer - from dshIm.resolveSessionPeer
*/
export function matchTargetForPeer(targets, peer) {
if (!peer || !Array.isArray(targets) || targets.length === 0) return null
const key = String(peer.conversationKey || '').trim()
const jidFromKey = key.includes(':') ? key.slice(key.indexOf(':') + 1) : key
const peerJid = String(peer.jid || peer.route?.jid || jidFromKey || '').trim().toLowerCase()
if (!peerJid) return null
for (const target of targets) {
const route = target?.route && typeof target.route === 'object' ? target.route : {}
const jid = String(route.jid || route.chatId || route.openId || route.userId || '').trim().toLowerCase()
if (jid && (jid === peerJid || peerJid.endsWith(jid) || jid.endsWith(peerJid))) {
return target
}
}
return null
}
export function callerSessionId(exec) {
const session = exec?.agent?.session
return String(
session?.id
|| session?.sessionId
|| exec?.agent?.sessionId
|| exec?.sessionId
|| '',
).trim()
}
/**
* Resolve delivery for cron_create: explicit args win; else IM peer → matching target; else dsh.
* @param {object} args
* @param {object} exec
* @param {{ dshIm?: object }} deps
*/
export async function resolveCreateDelivery(args = {}, exec = {}, deps = {}) {
const explicit = typeof args.delivery === 'string' ? args.delivery.trim().toLowerCase() : ''
const botId = typeof args.im_bot_id === 'string' ? args.im_bot_id.trim()
: (typeof args.imBotId === 'string' ? args.imBotId.trim() : '')
const targetId = typeof args.im_target_id === 'string' ? args.im_target_id.trim()
: (typeof args.imTargetId === 'string' ? args.imTargetId.trim() : '')
if (explicit === 'dsh') return normalizeDelivery({ kind: 'dsh' })
if (explicit === 'im' || (botId && targetId)) {
return normalizeDelivery({ kind: 'im', botId, targetId })
}
const dshIm = deps.dshIm
const sessionId = callerSessionId(exec)
if (sessionId && dshIm && typeof dshIm.resolveSessionPeer === 'function') {
try {
const peer = await dshIm.resolveSessionPeer(sessionId)
if (peer?.botId && typeof dshIm.listTargets === 'function') {
const targets = await dshIm.listTargets(peer.botId)
const match = matchTargetForPeer(targets, peer)
if (match?.targetId) {
return normalizeDelivery({
kind: 'im',
botId: peer.botId,
targetId: String(match.targetId),
})
}
}
} catch {
// fall through to dsh
}
}
return normalizeDelivery({ kind: 'dsh' })
}
const IM_MAX_CHARS = 3500
/**
* Send run summary to IM when job.delivery.kind === 'im'.
*/
export async function deliverRunToIm(job, summary, deps = {}) {
const delivery = job?.delivery
if (!delivery || delivery.kind !== 'im') return { sent: false, skipped: true }
const dshIm = deps.dshIm
if (!dshIm || typeof dshIm.send !== 'function') {
const error = new Error('ctx.dshIm unavailable — install dsh-im-ops for IM delivery')
error.code = 'IM_UNAVAILABLE'
throw error
}
const text = String(summary || '').trim() || `(定时任务「${job.name}」已完成,无文本摘要)`
const clipped = text.length > IM_MAX_CHARS ? `${text.slice(0, IM_MAX_CHARS)}…` : text
const header = `【运维定时 · ${job.name}】\n`
await dshIm.send(delivery.botId, delivery.targetId, `${header}${clipped}`)
return { sent: true }
}