diff --git a/FORK.md b/FORK.md index 96ee8e8..c4bb7c3 100644 --- a/FORK.md +++ b/FORK.md @@ -5,7 +5,7 @@ - Agent 自建定时任务(`cron_create` / `cron_list` / …) - 按创建来源默认投递:**WhatsApp 会话 → IM**;**Web/DSH 会话 → 侧栏新会话** - 与 [dsh-im-ops](https://github.com/hansjone/dsh-im-ops) 的 `ctx.dshIm.send(botId, targetId, text)` 打通 -- 跑完后把摘要**镜像**进创建时的有效会话(Web 来源会话 / WhatsApp 对端绑定会话),便于继续追问 +- 可选会话镜像(默认关闭):`mirrorToSession` / `mirror_to_session=true` 时才把摘要写入创建时的有效会话(Web / WhatsApp 绑定会话) 不要与上游 `@dsh-external/dsh-cron-tasks` 装进同一 profile(工具名冲突)。 @@ -29,17 +29,18 @@ dsh plugin --profile web add -w "D:\project\chatgpt\dsh-ops-cron" 1. 包名 / cordis id / HTTP 前缀仍为 `dsh-ops-cron`;侧栏产品文案为「定时任务」 2. Job 增加 `delivery: { kind: 'dsh'|'im', botId?, targetId? }` 3. Job 增加 `origin: { kind: 'web'|'im', sessionId?, peer? }`(创建时钉死;IM 开火时优先按 `conversationKey` 解析当前绑定会话) -4. `cron_create` 支持 `delivery` / `im_bot_id` / `im_target_id`;未显式指定时,若当前会话能 `resolveSessionPeer` 且已有匹配投递目标 → 默认 IM -5. 开火后若 `delivery.kind === 'im'`,把 assistant 摘要经 `ctx.dshIm.send` 投回 -6. 开火终态后把同构摘要镜像进有效会话(`agents.get` 或 `agents.resume` 后 `session.append`,不另开一轮模型;不用 `agents.create`,避免覆盖已有会话) +4. Job 增加 `mirrorToSession: boolean`(默认 `false`;侧栏勾选 / `cron_create` 的 `mirror_to_session`) +5. `cron_create` 支持 `delivery` / `im_bot_id` / `im_target_id`;未显式指定时,若当前会话能 `resolveSessionPeer` 且已有匹配投递目标 → 默认 IM +6. 开火后若 `delivery.kind === 'im'`,把 assistant 摘要经 `ctx.dshIm.send` 投回 +7. 开火终态且 `mirrorToSession === true` 时,把同构摘要镜像进有效会话(`agents.get` 或 `agents.resume` 后 `session.append`,不另开一轮模型;不用 `agents.create`,避免覆盖已有会话) ## 执行会话 vs 有效会话 | 角色 | 是什么 | 用途 | |------|--------|------| | 执行会话 | 每次开火新建的 root Session(侧栏历史可打开) | 跑 Agent、保留工具轨迹 | -| 有效会话 | 创建时的 Web 会话,或 WhatsApp/IM `conversationKey` 当前绑定的 Session | 用户日常追问所在处;摘要镜像写入这里 | +| 有效会话 | 创建时的 Web 会话,或 WhatsApp/IM `conversationKey` 当前绑定的 Session | 用户日常追问所在处;仅当 `mirrorToSession` 开启时才写入摘要 | -`dshIm.send` 只保证平台侧可见,不会自动写 Harness 会话日志;镜像是 cron 层额外一步。 +`dshIm.send` 只保证平台侧可见,不会自动写 Harness 会话日志;镜像是 cron 层可选的额外一步。 关键告警推送仍在 **netxops**,不经过本插件。 diff --git a/README.md b/README.md index 71fa138..01c6ba1 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Scheduled-task plugin for DeepSeek Harness (fork of [dsh-cron-tasks](https://git - Sidebar **定时任务** under New Session - Agent tools: `cron_create` / `cron_list` / `cron_pause` / `cron_resume` / `cron_delete` - Per-job delivery: **DSH** (new root session) or **IM** (`ctx.dshIm.send`) -- After each fire, mirrors the summary into the **origin effective session** (creator Web chat or WhatsApp peer binding) so follow-ups share context +- Optional per-job **session mirror** (default off): append the run summary into the origin Web/WhatsApp session for follow-ups See [FORK.md](FORK.md) for install notes. Do **not** install upstream `@dsh-external/dsh-cron-tasks` in the same profile. @@ -41,13 +41,13 @@ Create stamps the logged-in empNo. Legacy / unclaimed jobs stay `__unassigned__` | Created from | Default delivery | Effective-session mirror | |--------------|------------------|--------------------------| -| WhatsApp / IM session | `im` → reuse or **auto-create** a 投递目标 for that chat (group→group, DM→DM) | Live session for that `conversationKey` (latest binding) | -| Web / plain DSH session | `dsh` → sidebar history session | Creator session pinned as `origin.sessionId` | -| Explicit `delivery` / `im_bot_id`+`im_target_id` | as specified | Same mirror rules when `origin` / peer can be resolved | +| WhatsApp / IM session | `im` → reuse or **auto-create** a 投递目标 for that chat (group→group, DM→DM) | Off unless `mirrorToSession` / `mirror_to_session=true` | +| Web / plain DSH session | `dsh` → sidebar history session | Off unless opted in (origin still pinned as `origin.sessionId`) | +| Explicit `delivery` / `im_bot_id`+`im_target_id` | as specified | Same: opt-in mirror when `origin` / peer can be resolved | You usually do **not** need to paste botId/targetId when creating from WhatsApp — omit `delivery` and the job binds to the current chat. -**Execution session ≠ effective session.** Each fire still opens a fresh root Session for the Agent turn (visible under 定时任务 history). The mirrored notice is what makes the WhatsApp/Web chat you actually use able to continue from the result. +**Execution session ≠ effective session.** Each fire still opens a fresh root Session for the Agent turn (visible under 定时任务 history). Enable **Mirror into origin session** (sidebar) or `mirror_to_session=true` (`cron_create`) only when you want the summary injected into the WhatsApp/Web chat you actually use. ## Agent preset diff --git a/lib/client.js b/lib/client.js index 89214fc..8f2a666 100644 --- a/lib/client.js +++ b/lib/client.js @@ -313,6 +313,8 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba .dsh-ct-editorRow{display:flex;gap:12px} .dsh-ct-editorRow label{flex:1} .dsh-ct-cwdHint{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.4} +.dsh-ct-check{flex-direction:row!important;align-items:center;gap:8px!important} +.dsh-ct-check input{width:auto;margin:0} .dsh-ct-editorActs{display:flex;gap:8px;margin-top:8px;flex-wrap:wrap} .dsh-ct-primary{appearance:none;font:inherit;border:none;border-radius:8px;padding:8px 14px;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);cursor:pointer;font-size:13px} .dsh-ct-secondary{appearance:none;font:inherit;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:8px 14px;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;font-size:13px} @@ -328,7 +330,7 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba saveFailed: '本部署没有接受这些值,已保留供你修改。', enabled: '启用调度器', timezone: '默认时区', historyLimit: '历史保留条数', overlapPolicy: '重叠策略', misfirePolicy: '漏跑策略', policySkip: '跳过', - hint: '每次执行会新开会话跑任务;摘要会镜像回创建时的有效会话(WhatsApp/Web),便于继续追问。完整工具轨迹仍可从历史打开。', + hint: '每次执行会新开会话跑任务。默认不会把摘要注入创建时的有效会话;需要跟进时可在任务里勾选「镜像回原会话」。完整工具轨迹仍可从历史打开。', entry: '定时任务', entryLabel: '打开定时任务', backToWorkspace: '返回工作区', backLabel: '返回工作区', newJob: '新建任务', name: '名称', prompt: '提示词', kind: '日程', @@ -348,7 +350,7 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba agentPreset: 'Agent Preset', agentPresetDefault: '每次运行用当时的 Host 默认', agentPresetHint: '指定后每次触发都挂载该 Preset;留空则跟随 Host 默认(创建时若从 WhatsApp 继承会自动写入)。', - editorLead: '到点仍会新开会话执行。结束后摘要会镜像回创建时的有效会话;IM 投递还会经 WhatsApp/IM 发出。日常追问请用原会话;完整工具轨迹可从下方运行记录打开。', + editorLead: '到点仍会新开会话执行。摘要默认只留在运行历史;勾选「镜像回原会话」才会写入创建时的有效会话。IM 投递仍会经 WhatsApp/IM 发出。完整工具轨迹可从下方运行记录打开。', lastOutput: '上次输出', noOutput: '还没有输出。先立即运行一次。', delivery: '投递', deliveryDsh: 'DSH 侧栏会话', @@ -361,6 +363,8 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba imCatalogLoading: '正在加载投递目标…', imCatalogUnavailable: '无法加载投递目标(需 dsh-im-ops ≥ops.24,且已配置投递目标)', deliveryHint: '选 WhatsApp/IM 后从下拉选择已保存的投递目标;也可手动填写。目标在 IM 机器人 → 投递设置里创建。', + mirrorToSession: '镜像回原会话', + mirrorToSessionHint: '开启后把每次运行摘要写入创建时的 WhatsApp/Web 会话(不新开模型轮次)。默认关闭。', } const en = { title: 'Scheduled tasks', @@ -369,7 +373,7 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba 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: 'Each fire runs in a fresh session; the summary is mirrored into the origin WhatsApp/Web session so you can follow up there. Full tool traces stay in run history.', + hint: 'Each fire runs in a fresh session. By default the summary is not injected into the origin WhatsApp/Web chat; enable “Mirror into origin session” on a job when you want follow-up context there. Full tool traces stay in run history.', entry: 'Scheduled tasks', entryLabel: 'Open scheduled tasks', backToWorkspace: 'Back to workspace', backLabel: 'Back to workspace', newJob: 'New job', name: 'Name', prompt: 'Prompt', kind: 'Schedule', @@ -389,7 +393,7 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba agentPreset: 'Agent preset', agentPresetDefault: 'Use the Host default at fire time', agentPresetHint: 'Pin a preset for every fire. Leave empty to follow the Host default (WhatsApp-created jobs inherit the chat preset automatically).', - editorLead: 'Fires still run in a new session. Afterward the summary is mirrored into the origin session; IM delivery also sends via WhatsApp/IM. Follow up in the origin chat; open a run below for the full tool trace.', + editorLead: 'Fires still run in a new session. Summaries stay in run history by default; enable “Mirror into origin session” to append them to the origin chat. IM delivery still sends via WhatsApp/IM. Open a run below for the full tool trace.', lastOutput: 'Last output', noOutput: 'No output yet. Run it once.', delivery: 'Delivery', deliveryDsh: 'DSH sidebar session', @@ -402,6 +406,8 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba imCatalogLoading: 'Loading delivery targets…', imCatalogUnavailable: 'Cannot load targets (need dsh-im-ops ≥ops.24 with saved targets)', deliveryHint: 'Pick a saved IM delivery target from the list, or enter botId/targetId manually. Create targets under IM bot → Delivery settings.', + mirrorToSession: 'Mirror into origin session', + mirrorToSessionHint: 'When enabled, append each run summary into the creating WhatsApp/Web session (no new model turn). Off by default.', } function normalizePrefs(value) { @@ -651,6 +657,7 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba deliveryKind: 'dsh', imBotId: '', imTargetId: '', + mirrorToSession: false, } } @@ -671,6 +678,7 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba deliveryKind: job.delivery?.kind === 'im' ? 'im' : 'dsh', imBotId: job.delivery?.botId || '', imTargetId: job.delivery?.targetId || '', + mirrorToSession: job.mirrorToSession === true, } } @@ -1137,6 +1145,15 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba ? h(ImDeliveryFields, { t, form, setForm, imCatalog }) : null, h('span', { className: 'dsh-ct-cwdHint' }, t('deliveryHint')), + h('label', { className: 'dsh-ct-check' }, + h('input', { + type: 'checkbox', + checked: form.mirrorToSession === true, + onChange: (e) => setForm({ ...form, mirrorToSession: e.target.checked }), + }), + t('mirrorToSession'), + ), + h('span', { className: 'dsh-ct-cwdHint' }, t('mirrorToSessionHint')), 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, @@ -1383,6 +1400,7 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba delivery: form.deliveryKind === 'im' ? { kind: 'im', botId: form.imBotId, targetId: form.imTargetId } : { kind: 'dsh' }, + mirrorToSession: form.mirrorToSession === true, } if (selection.type === 'job' && selection.jobId) { await api(`/jobs/${selection.jobId}`, { method: 'PATCH', body: JSON.stringify(payload) }) diff --git a/lib/fire.js b/lib/fire.js index 625e2ad..fcb2d63 100644 --- a/lib/fire.js +++ b/lib/fire.js @@ -67,6 +67,7 @@ export function publicJob(job) { ...job.delivery.mentionName ? { mentionName: job.delivery.mentionName } : {}, } : { kind: 'dsh' }, + mirrorToSession: job.mirrorToSession === true, ...job.origin ? { origin: job.origin } : {}, schedule: job.schedule, createdAt: job.createdAt, diff --git a/lib/host.js b/lib/host.js index 7af3b50..4451271 100644 --- a/lib/host.js +++ b/lib/host.js @@ -281,6 +281,7 @@ export function createHostService(options = {}) { async function maybeMirrorResult(job, run) { if (!job || !run || run.status === 'skipped') return + if (job.mirrorToSession !== true) return try { const result = await mirrorRunToSession(job, run.summary || run.error || '', { dshIm: getDshIm(), @@ -362,6 +363,11 @@ export function createHostService(options = {}) { origin: patch.origin !== undefined ? (normalizeOrigin(patch.origin) || undefined) : job.origin, + mirrorToSession: patch.mirrorToSession !== undefined + ? patch.mirrorToSession === true + : (patch.mirror_to_session !== undefined + ? patch.mirror_to_session === true + : job.mirrorToSession === true), ownerEmpNo: job.ownerEmpNo || UNASSIGNED_OWNER, ownerDisplayName: job.ownerDisplayName || '', } diff --git a/lib/store.js b/lib/store.js index 5e97ac2..b53fcb7 100644 --- a/lib/store.js +++ b/lib/store.js @@ -100,6 +100,8 @@ export function createJobRecord(input, state, now) { const delivery = normalizeDelivery(input.delivery) const origin = normalizeOrigin(input.origin) const agentPreset = normalizeAgentPresetId(input.agentPreset ?? input.agent_preset) + const mirrorToSession = input.mirrorToSession === true + || input.mirror_to_session === true const ownerEmpNo = normalizeOwnerEmpNo(input.ownerEmpNo) const ownerDisplayName = typeof input.ownerDisplayName === 'string' ? input.ownerDisplayName.trim().slice(0, 80) @@ -118,6 +120,7 @@ export function createJobRecord(input, state, now) { reasoningEffort: model.reasoningEffort, agentPreset, delivery, + mirrorToSession, ownerEmpNo, ownerDisplayName, ...origin ? { origin } : {}, diff --git a/lib/tools.js b/lib/tools.js index 0ef28ed..a9e68df 100644 --- a/lib/tools.js +++ b/lib/tools.js @@ -35,6 +35,7 @@ const JOB_SCHEMA = { agentPreset: { type: 'string' }, delivery: { type: 'object', additionalProperties: true }, origin: { type: 'object', additionalProperties: true }, + mirrorToSession: { type: 'boolean' }, schedule: { type: 'object', additionalProperties: true }, createdAt: { type: 'number' }, updatedAt: { type: 'number' }, @@ -235,7 +236,7 @@ export function cronToolDefinitions(service, deps = {}) { return [ { name: 'cron_create', - description: 'Create a DSH 定时任务 job (sidebar scheduled job). NEVER use crontab. NOT schedule_create. Default timezone Asia/Shanghai. For "in N minutes / 一分钟后" pass only after_minutes. For "today/tonight at HH:MM" pass only hour+minute (24h; hour=0 is midnight). Do not also send at or expr. Extra schedule fields are ignored (after_minutes > hour > at > expr). cwd MUST be the current workspace path when chatting in a workspace; if omitted, uses the current session working directory. Pass provider+model for the job; if omitted, snapshots the current session model. Agent preset: omit to inherit (WhatsApp chat preset → session → Host default); or pass agent_preset. Delivery: omit for auto — on WhatsApp/IM creates/reuses a 投递目标 for the current chat (group→group, DM→DM) and sets delivery=im; on Web/DSH defaults to dsh. Or pass delivery=dsh|im with im_bot_id+im_target_id.', + description: 'Create a DSH 定时任务 job (sidebar scheduled job). NEVER use crontab. NOT schedule_create. Default timezone Asia/Shanghai. For "in N minutes / 一分钟后" pass only after_minutes. For "today/tonight at HH:MM" pass only hour+minute (24h; hour=0 is midnight). Do not also send at or expr. Extra schedule fields are ignored (after_minutes > hour > at > expr). cwd MUST be the current workspace path when chatting in a workspace; if omitted, uses the current session working directory. Pass provider+model for the job; if omitted, snapshots the current session model. Agent preset: omit to inherit (WhatsApp chat preset → session → Host default); or pass agent_preset. Delivery: omit for auto — on WhatsApp/IM creates/reuses a 投递目标 for the current chat (group→group, DM→DM) and sets delivery=im; on Web/DSH defaults to dsh. Or pass delivery=dsh|im with im_bot_id+im_target_id. Session mirror is OFF by default; pass mirror_to_session=true only when the user wants the run summary injected into the origin WhatsApp/Web chat.', parameters: { type: 'object', additionalProperties: false, @@ -261,6 +262,7 @@ export function cronToolDefinitions(service, deps = {}) { im_bot_id: { type: 'string', description: 'Opaque botId from IM 投递设置 when delivery=im (Web/sidebar only; IM chats cannot retarget).' }, im_target_id: { type: 'string', description: 'Opaque targetId from IM 投递设置 when delivery=im (Web/sidebar only; IM chats cannot retarget).' }, agent_preset: { type: 'string', description: 'Agent preset id for scheduled runs. Omit to inherit from the current WhatsApp/IM chat or Host default.' }, + mirror_to_session: { type: 'boolean', description: 'If true, after each fire append the run summary into the origin WhatsApp/Web session as a system-reminder (no new model turn). Default false — results stay in run history (and IM delivery when configured).' }, }, required: ['name', 'prompt'], }, @@ -308,6 +310,7 @@ export function cronToolDefinitions(service, deps = {}) { timeoutMinutes: Number.isFinite(timeout) && timeout > 0 ? timeout : undefined, enabled: args.enabled !== false, delivery, + mirrorToSession: args.mirror_to_session === true, ...origin ? { origin } : {}, agentPreset, }, identity) @@ -459,6 +462,7 @@ export function cronGuidanceText(nowMs = Date.now(), timeZone = 'Asia/Shanghai') 'Working directory: if the user is chatting in a workspace, pass cwd as that workspace filesystem path (the current session working directory). If they name another workspace, use that path. If cwd is omitted, cron_create uses the current session cwd.', 'Model: pass provider+model for the job. If omitted, cron_create stores the current session model. Scheduled runs consume that model\'s quota.', 'Delivery: when chatting on WhatsApp/IM, omit delivery so the job defaults to im for the current chat (group→same group, DM→same DM). A 投递目标 is reused or auto-created. On Web/DSH, default is dsh. Or pass delivery=im with im_bot_id+im_target_id.', + 'Session mirror: by default the run summary is NOT injected into the origin WhatsApp/Web chat. Pass mirror_to_session=true only when the user wants follow-up context in that chat. Full tool traces always stay in run history; IM delivery (when configured) is independent.', 'Agent preset: omit agent_preset to inherit (WhatsApp chat/group preset → creating session → Host default). Pass agent_preset to pin a preset for every scheduled run.', 'When the user asks to look at, create, pause, resume, or delete 定时任务 / scheduled tasks / cron jobs:', '1. If cron_list / cron_create / cron_pause / cron_resume / cron_delete are in your tool list, call them.', @@ -480,7 +484,7 @@ export function makeCronSkill() { Tools: - cron_list — list jobs -- cron_create — "一分钟后" → after_minutes=1. Clock time → hour+minute only. Do not send at/expr at the same time. Pass cwd as the current workspace path when creating from a workspace chat. Pass provider+model or inherit the current session model. Delivery and agent preset auto from session, or pass delivery=im / agent_preset explicitly. +- cron_create — "一分钟后" → after_minutes=1. Clock time → hour+minute only. Do not send at/expr at the same time. Pass cwd as the current workspace path when creating from a workspace chat. Pass provider+model or inherit the current session model. Delivery and agent preset auto from session, or pass delivery=im / agent_preset explicitly. Session mirror is off by default; pass mirror_to_session=true only if the user wants the summary injected into the origin chat. - cron_pause / cron_resume / cron_delete — by id from cron_list `, } diff --git a/test/host.test.js b/test/host.test.js index 8d5af11..d08b1f9 100644 --- a/test/host.test.js +++ b/test/host.test.js @@ -974,3 +974,58 @@ test('migrateJobOwners assigns unassigned for legacy jobs', async () => { assert.equal(next.jobs[1].ownerEmpNo, 'from-session') assert.equal(next.jobs[2].ownerEmpNo, 'u1') }) + +test('session mirror is off by default and runs only when mirrorToSession is true', async (t) => { + const dir = await mkdtemp(join(tmpdir(), 'dsh-ops-cron-')) + t.after(() => rm(dir, { recursive: true, force: true })) + + const appended = [] + const agents = { + get(sessionId) { + if (sessionId !== 'origin-web') return null + return { + session: { + append(...args) { appended.push(args) }, + }, + } + }, + } + const service = createTestHost({ + filePath: join(dir, 'store.json'), + now: () => Date.parse('2026-08-24T01:00:00.000Z'), + getAgents: () => agents, + sessionPort: { + async createAndPrompt({ job }) { + return { sessionId: `run-${job.id}`, status: 'succeeded', summary: `done ${job.name}` } + }, + async archiveSession() {}, + }, + }) + + const off = await service.createJob({ + name: 'no-mirror', + prompt: 'p', + schedule: { kind: 'at', at: '2099-01-01T00:00:00.000Z', timezone: 'UTC' }, + origin: { kind: 'web', sessionId: 'origin-web' }, + }) + assert.equal(off.mirrorToSession, false) + await service.dispatchRun(off.id, 'run-now') + assert.equal(appended.length, 0) + + const on = await service.createJob({ + name: 'with-mirror', + prompt: 'p', + schedule: { kind: 'at', at: '2099-01-02T00:00:00.000Z', timezone: 'UTC' }, + origin: { kind: 'web', sessionId: 'origin-web' }, + mirrorToSession: true, + }) + assert.equal(on.mirrorToSession, true) + await service.dispatchRun(on.id, 'run-now') + assert.equal(appended.length, 1) + assert.match(appended[0][1].content[0].text, /with-mirror/) + + const patched = await service.updateJob(off.id, { mirrorToSession: true }) + assert.equal(patched.mirrorToSession, true) + await service.dispatchRun(off.id, 'run-now') + assert.equal(appended.length, 2) +}) diff --git a/test/package.test.js b/test/package.test.js index c76b7b6..4f41663 100644 --- a/test/package.test.js +++ b/test/package.test.js @@ -45,6 +45,7 @@ test('installable bundle declares host apply, client half, unique id, and no @de assert.match(host, /agentDefaultModel/) assert.match(host, /deliverRunToIm/) assert.match(host, /mirrorRunToSession/) + assert.match(host, /mirrorToSession/) assert.match(host, /getAgents/) assert.match(host, /PLUGIN_NAME = 'dsh-ops-cron'/) assert.match(host, /API_PREFIX = '\/dsh-ops-cron'/) @@ -65,6 +66,7 @@ test('installable bundle declares host apply, client half, unique id, and no @de assert.match(client, /ModelField/) assert.match(client, /PresetField/) assert.match(client, /agentPreset/) + assert.match(client, /mirrorToSession/) assert.match(client, /inject = \['slots', 'locale', 'settingsScope'\]/) const preset = await readFile(join(root, 'lib/preset.js'), 'utf8') assert.match(preset, /resolveCreateAgentPreset/) diff --git a/test/preset.test.js b/test/preset.test.js index 98f59d1..5113260 100644 --- a/test/preset.test.js +++ b/test/preset.test.js @@ -19,6 +19,27 @@ test('createJobRecord persists agentPreset', () => { assert.equal(job.agentPreset, 'ops-desk') }) +test('createJobRecord defaults mirrorToSession off and accepts explicit true', () => { + const base = { + name: 'n', + prompt: 'p', + schedule: { kind: 'cron', expr: '0 9 * * *', timezone: 'Asia/Shanghai' }, + } + assert.equal(createJobRecord(base, emptyState(), Date.now()).mirrorToSession, false) + assert.equal( + createJobRecord({ ...base, mirrorToSession: true }, emptyState(), Date.now()).mirrorToSession, + true, + ) + assert.equal( + createJobRecord({ ...base, mirror_to_session: true }, emptyState(), Date.now()).mirrorToSession, + true, + ) + assert.equal( + createJobRecord({ ...base, mirrorToSession: 'yes' }, emptyState(), Date.now()).mirrorToSession, + false, + ) +}) + test('resolveCreateAgentPreset prefers explicit then peer then session then host default', async () => { assert.equal( await resolveCreateAgentPreset({ agent_preset: 'explicit' }, {}, {}),