mirror of
https://github.com/hansjone/dsh-ops-cron.git
synced 2026-10-09 03:03:18 +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
118
lib/delivery.js
Normal file
118
lib/delivery.js
Normal 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 }
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue