Mirror scheduled-run summaries into the origin effective session.

Pin origin at create time, keep isolated fire sessions for audit, and after IM/DSH delivery resume or open the origin chat to append the same summary for follow-ups.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-06 12:45:37 +08:00
parent 2e690c30c8
commit 29d946f3dd
12 changed files with 527 additions and 26 deletions

View file

@ -3,8 +3,13 @@ import test from 'node:test'
import {
matchTargetForPeer,
normalizeDelivery,
normalizeOrigin,
resolveCreateDelivery,
resolveCreateOrigin,
resolveMirrorSession,
formatRunResultBody,
deliverRunToIm,
mirrorRunToSession,
} from '../lib/delivery.js'
test('normalizeDelivery defaults to dsh', () => {
@ -19,6 +24,26 @@ test('normalizeDelivery requires botId+targetId for im', () => {
)
})
test('normalizeOrigin pins web and im shapes', () => {
assert.equal(normalizeOrigin(null), null)
assert.deepEqual(
normalizeOrigin({ kind: 'web', sessionId: 'sess-web' }),
{ kind: 'web', sessionId: 'sess-web' },
)
assert.deepEqual(
normalizeOrigin({
kind: 'im',
sessionId: 'sess-im',
peer: { botId: 'bot_1', conversationKey: 'direct:86138@s.whatsapp.net', kind: 'direct' },
}),
{
kind: 'im',
sessionId: 'sess-im',
peer: { botId: 'bot_1', conversationKey: 'direct:86138@s.whatsapp.net', kind: 'direct' },
},
)
})
test('matchTargetForPeer matches whatsapp jid from conversationKey', () => {
const peer = { botId: 'bot_1', conversationKey: 'direct:8613800000000@s.whatsapp.net' }
const targets = [
@ -55,6 +80,37 @@ test('resolveCreateDelivery auto-matches peer target', async () => {
assert.deepEqual(delivery, { kind: 'im', botId: 'bot_1', targetId: 'wa-dm' })
})
test('resolveCreateOrigin captures im peer from caller session', async () => {
const dshIm = {
resolveSessionPeer: async () => ({
botId: 'bot_1',
conversationKey: 'group:120363@g.us:user:86138@s.whatsapp.net',
conversationId: '120363@g.us',
kind: 'group',
}),
}
const origin = await resolveCreateOrigin({}, {
agent: { session: { id: 'sess-group' } },
}, { dshIm })
assert.deepEqual(origin, {
kind: 'im',
sessionId: 'sess-group',
peer: {
botId: 'bot_1',
conversationKey: 'group:120363@g.us:user:86138@s.whatsapp.net',
conversationId: '120363@g.us',
kind: 'group',
},
})
})
test('resolveCreateOrigin falls back to web when no peer', async () => {
const origin = await resolveCreateOrigin({}, {
agent: { session: { id: 'sess-web' } },
}, { dshIm: { resolveSessionPeer: async () => null } })
assert.deepEqual(origin, { kind: 'web', sessionId: 'sess-web' })
})
test('resolveCreateDelivery auto-creates group target when none exists', async () => {
const created = []
const dshIm = {
@ -151,7 +207,6 @@ test('resolveCreateDelivery pins group creator mention from peer', async () => {
kind: 'im',
botId: 'bot_wa',
targetId: 'ops-group',
// Prefer phone JID over LID so WhatsApp can show nickname + notify.
mentionJid: '8613800000000@s.whatsapp.net',
mentionName: 'Alice',
})
@ -196,3 +251,123 @@ test('deliverRunToIm sends via dshIm', async () => {
assert.match(sent[0][2], /定时任务/)
assert.match(sent[0][2], /hello world/)
})
test('resolveMirrorSession prefers live conversation binding over stale origin.sessionId', async () => {
const hit = await resolveMirrorSession({
origin: {
kind: 'im',
sessionId: 'stale-sess',
peer: { botId: 'bot_1', conversationKey: 'direct:86138@s.whatsapp.net' },
},
delivery: { kind: 'im', botId: 'bot_1', targetId: 'wa-dm' },
}, {
dshIm: {
resolveConversationSession: async () => ({
sessionId: 'live-sess',
botId: 'bot_1',
conversationKey: 'direct:86138@s.whatsapp.net',
}),
},
})
assert.deepEqual(hit, { sessionId: 'live-sess', via: 'conversation' })
})
test('resolveMirrorSession falls back to origin.sessionId', async () => {
const hit = await resolveMirrorSession({
origin: { kind: 'web', sessionId: 'origin-web' },
delivery: { kind: 'dsh' },
}, {})
assert.deepEqual(hit, { sessionId: 'origin-web', via: 'origin' })
})
test('mirrorRunToSession appends plugin notice without waking a turn', async () => {
const appended = []
const result = await mirrorRunToSession(
{
name: '日报',
origin: { kind: 'web', sessionId: 'origin-1' },
delivery: { kind: 'dsh' },
},
'line one',
{
getAgents: () => ({
get: () => ({
session: {
append: (...args) => { appended.push(args) },
},
}),
}),
newId: () => 'msg-1',
pluginName: 'dsh-ops-cron',
},
)
assert.equal(result.mirrored, true)
assert.equal(result.method, 'append')
assert.equal(appended.length, 1)
assert.equal(appended[0][0], 'user/message')
assert.match(appended[0][1].content[0].text, /日报/)
assert.match(appended[0][1].content[0].text, /line one/)
assert.equal(appended[0][1].source.kind, 'plugin')
assert.deepEqual(appended[0][2], { surfaceOp: 'append' })
})
test('mirrorRunToSession resumes a cold origin session instead of create', async () => {
const appended = []
const resumed = []
const result = await mirrorRunToSession(
{
name: '冷会话',
origin: { kind: 'web', sessionId: 'cold-1' },
},
'hello',
{
getAgents: () => ({
get: () => null,
create: async () => {
throw new Error('create must not be used for origin mirror')
},
resume: async (opts) => {
resumed.push(opts)
return {
agent: {
session: {
append: (...args) => { appended.push(args) },
},
},
}
},
}),
newId: () => 'msg-2',
},
)
assert.equal(result.mirrored, true)
assert.equal(result.resumed, true)
assert.deepEqual(resumed, [{ resumeSessionId: 'cold-1' }])
assert.equal(appended[0][0], 'user/message')
})
test('mirrorRunToSession skips when origin session is unavailable', async () => {
const result = await mirrorRunToSession(
{
name: 'gone',
origin: { kind: 'web', sessionId: 'missing' },
},
'x',
{
getAgents: () => ({
get: () => null,
resume: async () => {
throw new Error('not found')
},
}),
},
)
assert.equal(result.mirrored, false)
assert.equal(result.reason, 'session_unavailable')
})
test('formatRunResultBody matches IM delivery header', () => {
const body = formatRunResultBody({ name: '晨报' }, 'ok')
assert.match(body, /^【定时任务 · 晨报】\n/)
assert.match(body, /ok$/)
})

View file

@ -44,6 +44,8 @@ test('installable bundle declares host apply, client half, unique id, and no @de
const host = await readFile(join(root, 'lib/host.js'), 'utf8')
assert.match(host, /agentDefaultModel/)
assert.match(host, /deliverRunToIm/)
assert.match(host, /mirrorRunToSession/)
assert.match(host, /getAgents/)
assert.match(host, /PLUGIN_NAME = 'dsh-ops-cron'/)
assert.match(host, /API_PREFIX = '\/dsh-ops-cron'/)
assert.match(host, /resolveSessionPlacement/)
@ -68,7 +70,10 @@ test('installable bundle declares host apply, client half, unique id, and no @de
assert.match(preset, /resolveCreateAgentPreset/)
const delivery = await readFile(join(root, 'lib/delivery.js'), 'utf8')
assert.match(delivery, /resolveCreateDelivery/)
assert.match(delivery, /resolveCreateOrigin/)
assert.match(delivery, /mirrorRunToSession/)
assert.match(delivery, /deliverRunToIm/)
assert.match(client, /origin/)
const prd = await readFile(join(root, 'docs/PRD.md'), 'utf8')
assert.match(prd, /定时任务/)
assert.match(prd, /dsh-schedule/)