mirror of
https://github.com/hansjone/dsh-ops-cron.git
synced 2026-10-16 02:43:16 +08:00
Inherit agent presets and auto-bind IM delivery targets (0.1.7).
Cron jobs created from WhatsApp can omit bot/target ids and reuse chat overrides; sidebar/agent preset pins persist on fire. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
2f08136adf
commit
2e690c30c8
15 changed files with 830 additions and 82 deletions
|
|
@ -42,6 +42,8 @@ test('resolveCreateDelivery auto-matches peer target', async () => {
|
|||
resolveSessionPeer: async () => ({
|
||||
botId: 'bot_1',
|
||||
conversationKey: 'direct:86138@s.whatsapp.net',
|
||||
conversationId: '86138@s.whatsapp.net',
|
||||
kind: 'direct',
|
||||
}),
|
||||
listTargets: async () => ([
|
||||
{ targetId: 'wa-dm', route: { jid: '86138@s.whatsapp.net' } },
|
||||
|
|
@ -53,6 +55,134 @@ test('resolveCreateDelivery auto-matches peer target', async () => {
|
|||
assert.deepEqual(delivery, { kind: 'im', botId: 'bot_1', targetId: 'wa-dm' })
|
||||
})
|
||||
|
||||
test('resolveCreateDelivery auto-creates group target when none exists', async () => {
|
||||
const created = []
|
||||
const dshIm = {
|
||||
resolveSessionPeer: async () => ({
|
||||
botId: 'bot_wa',
|
||||
kind: 'group',
|
||||
conversationKey: 'group:120363@g.us:user:86138@s.whatsapp.net',
|
||||
conversationId: '120363@g.us',
|
||||
label: '运维群',
|
||||
}),
|
||||
listTargets: async () => ([]),
|
||||
createTarget: async (botId, target) => {
|
||||
created.push({ botId, target })
|
||||
return target
|
||||
},
|
||||
}
|
||||
const delivery = await resolveCreateDelivery({}, {
|
||||
agent: { session: { id: 'sess-group' } },
|
||||
}, { dshIm })
|
||||
assert.equal(created.length, 1)
|
||||
assert.equal(created[0].botId, 'bot_wa')
|
||||
assert.equal(created[0].target.kind, 'group')
|
||||
assert.equal(created[0].target.route.jid, '120363@g.us')
|
||||
assert.deepEqual(delivery, {
|
||||
kind: 'im',
|
||||
botId: 'bot_wa',
|
||||
targetId: created[0].target.targetId,
|
||||
})
|
||||
})
|
||||
|
||||
test('matchTargetForPeer matches phone-mapped DM against phone JID target', () => {
|
||||
const peer = {
|
||||
botId: 'bot_1',
|
||||
conversationKey: 'direct:910xxx@lid',
|
||||
conversationId: '910xxx@lid',
|
||||
phone: '8613800000000',
|
||||
}
|
||||
const targets = [
|
||||
{ targetId: 'alerts', kind: 'user', route: { jid: '8613800000000@s.whatsapp.net' } },
|
||||
]
|
||||
assert.equal(matchTargetForPeer(targets, peer).targetId, 'alerts')
|
||||
})
|
||||
|
||||
test('matchTargetForPeer does not bind group peer to a DM via sender phone', () => {
|
||||
const peer = {
|
||||
botId: 'bot_1',
|
||||
kind: 'group',
|
||||
conversationKey: 'group:120363999@g.us:user:8613800000000@s.whatsapp.net',
|
||||
conversationId: '120363999@g.us',
|
||||
phone: '8613800000000',
|
||||
label: 'Ops Room · Alice · 8613800000000',
|
||||
}
|
||||
const targets = [
|
||||
{ targetId: 'my-dm', kind: 'user', route: { jid: '8613800000000@s.whatsapp.net' } },
|
||||
{ targetId: 'ops-group', kind: 'group', route: { jid: '120363999@g.us' } },
|
||||
]
|
||||
assert.equal(matchTargetForPeer(targets, peer).targetId, 'ops-group')
|
||||
})
|
||||
|
||||
test('matchTargetForPeer ignores DM-only catalog when peer is a group', () => {
|
||||
const peer = {
|
||||
botId: 'bot_1',
|
||||
kind: 'group',
|
||||
conversationKey: 'group:120363999@g.us:user:8613800000000@s.whatsapp.net',
|
||||
conversationId: '120363999@g.us',
|
||||
phone: '8613800000000',
|
||||
}
|
||||
const targets = [
|
||||
{ targetId: 'my-dm', kind: 'user', route: { jid: '8613800000000@s.whatsapp.net' } },
|
||||
]
|
||||
assert.equal(matchTargetForPeer(targets, peer), null)
|
||||
})
|
||||
|
||||
test('resolveCreateDelivery pins group creator mention from peer', async () => {
|
||||
const dshIm = {
|
||||
resolveSessionPeer: async () => ({
|
||||
botId: 'bot_wa',
|
||||
kind: 'group',
|
||||
conversationKey: 'group:120363@g.us:user:910xxx@lid',
|
||||
conversationId: '120363@g.us',
|
||||
senderId: '910xxx@lid',
|
||||
phone: '8613800000000',
|
||||
pushName: 'Alice',
|
||||
label: 'Ops · Alice · 8613800000000',
|
||||
}),
|
||||
listTargets: async () => ([
|
||||
{ targetId: 'ops-group', kind: 'group', route: { jid: '120363@g.us' } },
|
||||
]),
|
||||
}
|
||||
const delivery = await resolveCreateDelivery({}, {
|
||||
agent: { session: { id: 'sess-group' } },
|
||||
}, { dshIm })
|
||||
assert.deepEqual(delivery, {
|
||||
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',
|
||||
})
|
||||
})
|
||||
|
||||
test('deliverRunToIm @mentions the creator in group deliveries', async () => {
|
||||
const sent = []
|
||||
await deliverRunToIm(
|
||||
{
|
||||
name: 'ping',
|
||||
delivery: {
|
||||
kind: 'im',
|
||||
botId: 'b',
|
||||
targetId: 't',
|
||||
mentionJid: '8613800000000@s.whatsapp.net',
|
||||
mentionName: 'Alice',
|
||||
},
|
||||
},
|
||||
'hello world',
|
||||
{
|
||||
dshIm: {
|
||||
send: async (...args) => { sent.push(args) },
|
||||
},
|
||||
},
|
||||
)
|
||||
assert.equal(sent.length, 1)
|
||||
assert.match(sent[0][2], /^@8613800000000\n/)
|
||||
assert.match(sent[0][2], /定时任务/)
|
||||
assert.deepEqual(sent[0][3], { mentions: ['8613800000000@s.whatsapp.net'] })
|
||||
})
|
||||
|
||||
test('deliverRunToIm sends via dshIm', async () => {
|
||||
const sent = []
|
||||
await deliverRunToIm(
|
||||
|
|
@ -63,6 +193,6 @@ test('deliverRunToIm sends via dshIm', async () => {
|
|||
assert.equal(sent.length, 1)
|
||||
assert.equal(sent[0][0], 'b')
|
||||
assert.equal(sent[0][1], 't')
|
||||
assert.match(sent[0][2], /运维定时/)
|
||||
assert.match(sent[0][2], /定时任务/)
|
||||
assert.match(sent[0][2], /hello world/)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ test('installable bundle declares host apply, client half, unique id, and no @de
|
|||
assert.match(tools, /resolveCreateCwd/)
|
||||
assert.match(tools, /after_minutes/)
|
||||
assert.match(tools, /Extra schedule fields are ignored/)
|
||||
assert.match(tools, /agent_preset/)
|
||||
assert.match(tools, /resolveCreateAgentPreset/)
|
||||
assert.doesNotMatch(tools, /pass only one of expr, at, or hour/)
|
||||
assert.match(index, /makeCronSkill/)
|
||||
assert.doesNotMatch(index, /agent\/session-start/)
|
||||
|
|
@ -46,9 +48,12 @@ test('installable bundle declares host apply, client half, unique id, and no @de
|
|||
assert.match(host, /API_PREFIX = '\/dsh-ops-cron'/)
|
||||
assert.match(host, /resolveSessionPlacement/)
|
||||
assert.match(host, /\/models/)
|
||||
assert.match(host, /\/presets/)
|
||||
assert.match(host, /composeCronAgent/)
|
||||
assert.match(host, /listPresetChoices/)
|
||||
assert.doesNotMatch(host, /from ['"]@deepseek-ai\//)
|
||||
const client = await readFile(join(root, 'lib/client.js'), 'utf8')
|
||||
assert.match(client, /运维定时/)
|
||||
assert.match(client, /定时任务/)
|
||||
assert.match(client, /dsh-ops-cron/)
|
||||
assert.match(client, /settings\.plugin\.item/)
|
||||
assert.match(client, /deliveryKind/)
|
||||
|
|
@ -56,7 +61,11 @@ test('installable bundle declares host apply, client half, unique id, and no @de
|
|||
assert.match(client, /imTargetId/)
|
||||
assert.match(client, /CwdField/)
|
||||
assert.match(client, /ModelField/)
|
||||
assert.match(client, /PresetField/)
|
||||
assert.match(client, /agentPreset/)
|
||||
assert.match(client, /inject = \['slots', 'locale', 'settingsScope'\]/)
|
||||
const preset = await readFile(join(root, 'lib/preset.js'), 'utf8')
|
||||
assert.match(preset, /resolveCreateAgentPreset/)
|
||||
const delivery = await readFile(join(root, 'lib/delivery.js'), 'utf8')
|
||||
assert.match(delivery, /resolveCreateDelivery/)
|
||||
assert.match(delivery, /deliverRunToIm/)
|
||||
|
|
|
|||
46
test/preset.test.js
Normal file
46
test/preset.test.js
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
import { normalizeAgentPresetId, resolveCreateAgentPreset } from '../lib/preset.js'
|
||||
import { createJobRecord, emptyState } from '../lib/store.js'
|
||||
|
||||
test('normalizeAgentPresetId accepts empty and valid ids', () => {
|
||||
assert.equal(normalizeAgentPresetId(''), '')
|
||||
assert.equal(normalizeAgentPresetId(' ops-desk '), 'ops-desk')
|
||||
assert.throws(() => normalizeAgentPresetId('bad preset'), /agentPreset/)
|
||||
})
|
||||
|
||||
test('createJobRecord persists agentPreset', () => {
|
||||
const job = createJobRecord({
|
||||
name: 'n',
|
||||
prompt: 'p',
|
||||
schedule: { kind: 'cron', expr: '0 9 * * *', timezone: 'Asia/Shanghai' },
|
||||
agentPreset: 'ops-desk',
|
||||
}, emptyState(), Date.now())
|
||||
assert.equal(job.agentPreset, 'ops-desk')
|
||||
})
|
||||
|
||||
test('resolveCreateAgentPreset prefers explicit then peer then session then host default', async () => {
|
||||
assert.equal(
|
||||
await resolveCreateAgentPreset({ agent_preset: 'explicit' }, {}, {}),
|
||||
'explicit',
|
||||
)
|
||||
|
||||
const fromPeer = await resolveCreateAgentPreset({}, { agent: { session: { id: 's1' } } }, {
|
||||
dshIm: {
|
||||
resolveSessionPeer: async () => ({ agentPreset: 'from-peer' }),
|
||||
},
|
||||
})
|
||||
assert.equal(fromPeer, 'from-peer')
|
||||
|
||||
const fromMeta = await resolveCreateAgentPreset({}, {
|
||||
agent: { session: { meta: { agentPreset: 'from-meta' } } },
|
||||
}, {})
|
||||
assert.equal(fromMeta, 'from-meta')
|
||||
|
||||
const fromHost = await resolveCreateAgentPreset({}, {}, {
|
||||
agentPresets: {
|
||||
resolve: async () => ({ id: 'host-default' }),
|
||||
},
|
||||
})
|
||||
assert.equal(fromHost, 'host-default')
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue