@swarmclawai/swarmclaw 0.7.2 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -22
- package/package.json +1 -1
- package/src/app/api/agents/[id]/route.ts +26 -0
- package/src/app/api/agents/[id]/thread/route.ts +36 -7
- package/src/app/api/agents/route.ts +12 -1
- package/src/app/api/auth/route.ts +76 -7
- package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
- package/src/app/api/chats/[id]/browser/route.ts +5 -1
- package/src/app/api/chats/[id]/chat/route.ts +7 -3
- package/src/app/api/chats/[id]/main-loop/route.ts +7 -88
- package/src/app/api/chats/[id]/messages/route.ts +19 -13
- package/src/app/api/chats/[id]/route.ts +18 -0
- package/src/app/api/chats/[id]/stop/route.ts +6 -1
- package/src/app/api/chats/route.ts +16 -0
- package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
- package/src/app/api/connectors/doctor/route.ts +13 -0
- package/src/app/api/files/open/route.ts +16 -14
- package/src/app/api/memory/maintenance/route.ts +11 -1
- package/src/app/api/openclaw/agent-files/route.ts +27 -4
- package/src/app/api/openclaw/skills/route.ts +11 -3
- package/src/app/api/plugins/dependencies/route.ts +24 -0
- package/src/app/api/plugins/install/route.ts +15 -92
- package/src/app/api/plugins/route.ts +3 -26
- package/src/app/api/plugins/settings/route.ts +17 -12
- package/src/app/api/plugins/ui/route.ts +1 -0
- package/src/app/api/settings/route.ts +49 -7
- package/src/app/api/tasks/[id]/route.ts +15 -6
- package/src/app/api/tasks/bulk/route.ts +2 -2
- package/src/app/api/tasks/route.ts +9 -4
- package/src/app/api/webhooks/[id]/route.ts +8 -1
- package/src/app/page.tsx +9 -2
- package/src/cli/index.js +4 -0
- package/src/cli/index.ts +3 -10
- package/src/components/agents/agent-card.tsx +15 -12
- package/src/components/agents/agent-chat-list.tsx +101 -1
- package/src/components/agents/agent-list.tsx +46 -9
- package/src/components/agents/agent-sheet.tsx +207 -16
- package/src/components/agents/inspector-panel.tsx +108 -48
- package/src/components/auth/access-key-gate.tsx +36 -97
- package/src/components/chat/chat-area.tsx +29 -13
- package/src/components/chat/chat-card.tsx +4 -20
- package/src/components/chat/chat-header.tsx +255 -353
- package/src/components/chat/chat-list.tsx +7 -9
- package/src/components/chat/checkpoint-timeline.tsx +1 -1
- package/src/components/chat/message-list.tsx +3 -1
- package/src/components/chatrooms/chatroom-view.tsx +347 -205
- package/src/components/connectors/connector-list.tsx +265 -127
- package/src/components/connectors/connector-sheet.tsx +217 -0
- package/src/components/home/home-view.tsx +128 -4
- package/src/components/layout/app-layout.tsx +383 -194
- package/src/components/layout/mobile-header.tsx +26 -8
- package/src/components/plugins/plugin-list.tsx +15 -3
- package/src/components/plugins/plugin-sheet.tsx +118 -9
- package/src/components/projects/project-detail.tsx +183 -0
- package/src/components/shared/agent-picker-list.tsx +2 -2
- package/src/components/shared/command-palette.tsx +111 -24
- package/src/components/shared/settings/plugin-manager.tsx +20 -4
- package/src/components/shared/settings/section-capability-policy.tsx +105 -0
- package/src/components/shared/settings/section-heartbeat.tsx +77 -0
- package/src/components/shared/settings/section-orchestrator.tsx +3 -3
- package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
- package/src/components/shared/settings/section-secrets.tsx +6 -6
- package/src/components/shared/settings/section-user-preferences.tsx +1 -1
- package/src/components/shared/settings/section-voice.tsx +5 -1
- package/src/components/shared/settings/section-web-search.tsx +10 -2
- package/src/components/shared/settings/settings-page.tsx +245 -46
- package/src/components/tasks/approvals-panel.tsx +205 -18
- package/src/components/tasks/task-board.tsx +242 -46
- package/src/components/usage/metrics-dashboard.tsx +74 -1
- package/src/components/wallets/wallet-panel.tsx +17 -5
- package/src/components/webhooks/webhook-sheet.tsx +7 -7
- package/src/lib/auth.ts +17 -0
- package/src/lib/chat-streaming-state.test.ts +108 -0
- package/src/lib/chat-streaming-state.ts +108 -0
- package/src/lib/openclaw-agent-id.test.ts +14 -0
- package/src/lib/openclaw-agent-id.ts +31 -0
- package/src/lib/server/agent-assignment.test.ts +112 -0
- package/src/lib/server/agent-assignment.ts +169 -0
- package/src/lib/server/approval-connector-notify.test.ts +253 -0
- package/src/lib/server/approvals-auto-approve.test.ts +205 -0
- package/src/lib/server/approvals.ts +483 -75
- package/src/lib/server/autonomy-runtime.test.ts +341 -0
- package/src/lib/server/browser-state.test.ts +118 -0
- package/src/lib/server/browser-state.ts +123 -0
- package/src/lib/server/build-llm.test.ts +36 -0
- package/src/lib/server/build-llm.ts +11 -4
- package/src/lib/server/builtin-plugins.ts +34 -0
- package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
- package/src/lib/server/chat-execution.ts +250 -61
- package/src/lib/server/chatroom-health.test.ts +26 -0
- package/src/lib/server/chatroom-health.ts +2 -3
- package/src/lib/server/chatroom-helpers.test.ts +67 -2
- package/src/lib/server/chatroom-helpers.ts +45 -5
- package/src/lib/server/connectors/discord.ts +175 -11
- package/src/lib/server/connectors/doctor.test.ts +80 -0
- package/src/lib/server/connectors/doctor.ts +116 -0
- package/src/lib/server/connectors/manager.ts +946 -110
- package/src/lib/server/connectors/policy.test.ts +222 -0
- package/src/lib/server/connectors/policy.ts +452 -0
- package/src/lib/server/connectors/slack.ts +188 -9
- package/src/lib/server/connectors/telegram.ts +65 -15
- package/src/lib/server/connectors/thread-context.test.ts +44 -0
- package/src/lib/server/connectors/thread-context.ts +72 -0
- package/src/lib/server/connectors/types.ts +41 -11
- package/src/lib/server/daemon-state.ts +59 -1
- package/src/lib/server/data-dir.ts +13 -0
- package/src/lib/server/delegation-jobs.test.ts +140 -0
- package/src/lib/server/delegation-jobs.ts +248 -0
- package/src/lib/server/document-utils.test.ts +47 -0
- package/src/lib/server/document-utils.ts +397 -0
- package/src/lib/server/heartbeat-service.ts +13 -39
- package/src/lib/server/heartbeat-source.test.ts +22 -0
- package/src/lib/server/heartbeat-source.ts +7 -0
- package/src/lib/server/identity-continuity.test.ts +77 -0
- package/src/lib/server/identity-continuity.ts +127 -0
- package/src/lib/server/mailbox-utils.ts +347 -0
- package/src/lib/server/main-agent-loop.ts +27 -967
- package/src/lib/server/memory-db.ts +4 -6
- package/src/lib/server/memory-tiers.ts +40 -0
- package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
- package/src/lib/server/openclaw-agent-resolver.ts +128 -0
- package/src/lib/server/openclaw-exec-config.ts +5 -6
- package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
- package/src/lib/server/openclaw-skills-normalize.ts +136 -0
- package/src/lib/server/openclaw-sync.ts +3 -2
- package/src/lib/server/orchestrator-lg.ts +17 -6
- package/src/lib/server/orchestrator.ts +2 -2
- package/src/lib/server/playwright-proxy.mjs +27 -3
- package/src/lib/server/plugins.test.ts +207 -0
- package/src/lib/server/plugins.ts +822 -69
- package/src/lib/server/provider-health.ts +33 -3
- package/src/lib/server/queue.ts +3 -20
- package/src/lib/server/scheduler.ts +2 -0
- package/src/lib/server/session-archive-memory.test.ts +85 -0
- package/src/lib/server/session-archive-memory.ts +230 -0
- package/src/lib/server/session-mailbox.ts +8 -18
- package/src/lib/server/session-reset-policy.test.ts +99 -0
- package/src/lib/server/session-reset-policy.ts +311 -0
- package/src/lib/server/session-run-manager.ts +33 -80
- package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
- package/src/lib/server/session-tools/calendar.ts +2 -12
- package/src/lib/server/session-tools/connector.ts +109 -8
- package/src/lib/server/session-tools/context.ts +14 -2
- package/src/lib/server/session-tools/crawl.ts +447 -0
- package/src/lib/server/session-tools/crud.ts +70 -32
- package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
- package/src/lib/server/session-tools/delegate.ts +406 -20
- package/src/lib/server/session-tools/discovery.ts +22 -4
- package/src/lib/server/session-tools/document.ts +283 -0
- package/src/lib/server/session-tools/email.ts +1 -3
- package/src/lib/server/session-tools/extract.ts +137 -0
- package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
- package/src/lib/server/session-tools/file-send.test.ts +84 -1
- package/src/lib/server/session-tools/file.ts +237 -24
- package/src/lib/server/session-tools/human-loop.ts +227 -0
- package/src/lib/server/session-tools/image-gen.ts +1 -3
- package/src/lib/server/session-tools/index.ts +56 -1
- package/src/lib/server/session-tools/mailbox.ts +276 -0
- package/src/lib/server/session-tools/memory.ts +35 -3
- package/src/lib/server/session-tools/monitor.ts +150 -7
- package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
- package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
- package/src/lib/server/session-tools/platform.ts +142 -4
- package/src/lib/server/session-tools/plugin-creator.ts +86 -23
- package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
- package/src/lib/server/session-tools/replicate.ts +1 -3
- package/src/lib/server/session-tools/schedule.ts +20 -10
- package/src/lib/server/session-tools/session-info.ts +36 -3
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
- package/src/lib/server/session-tools/subagent.ts +193 -27
- package/src/lib/server/session-tools/table.ts +587 -0
- package/src/lib/server/session-tools/wallet.ts +13 -10
- package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
- package/src/lib/server/session-tools/web.ts +896 -100
- package/src/lib/server/storage.ts +226 -7
- package/src/lib/server/stream-agent-chat.ts +46 -21
- package/src/lib/server/structured-extract.test.ts +72 -0
- package/src/lib/server/structured-extract.ts +373 -0
- package/src/lib/server/task-mention.test.ts +16 -2
- package/src/lib/server/task-mention.ts +61 -10
- package/src/lib/server/tool-aliases.ts +44 -7
- package/src/lib/server/tool-capability-policy.ts +6 -0
- package/src/lib/server/tool-retry.ts +2 -0
- package/src/lib/server/watch-jobs.test.ts +173 -0
- package/src/lib/server/watch-jobs.ts +532 -0
- package/src/lib/server/ws-hub.ts +5 -3
- package/src/lib/validation/schemas.test.ts +26 -0
- package/src/lib/validation/schemas.ts +7 -0
- package/src/lib/ws-client.ts +14 -12
- package/src/proxy.ts +5 -5
- package/src/stores/use-app-store.ts +0 -6
- package/src/stores/use-chat-store.ts +31 -2
- package/src/types/index.ts +287 -44
- package/src/components/chat/new-chat-sheet.tsx +0 -253
- package/src/lib/server/main-session.ts +0 -17
- package/src/lib/server/session-run-manager.test.ts +0 -26
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { after, before, describe, it } from 'node:test'
|
|
6
|
+
|
|
7
|
+
const originalEnv = {
|
|
8
|
+
DATA_DIR: process.env.DATA_DIR,
|
|
9
|
+
WORKSPACE_DIR: process.env.WORKSPACE_DIR,
|
|
10
|
+
SWARMCLAW_BUILD_MODE: process.env.SWARMCLAW_BUILD_MODE,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let tempDir = ''
|
|
14
|
+
let watchJobs: typeof import('./watch-jobs')
|
|
15
|
+
let storage: typeof import('./storage')
|
|
16
|
+
|
|
17
|
+
before(async () => {
|
|
18
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-watch-jobs-'))
|
|
19
|
+
process.env.DATA_DIR = path.join(tempDir, 'data')
|
|
20
|
+
process.env.WORKSPACE_DIR = path.join(tempDir, 'workspace')
|
|
21
|
+
process.env.SWARMCLAW_BUILD_MODE = '1'
|
|
22
|
+
watchJobs = await import('./watch-jobs')
|
|
23
|
+
storage = await import('./storage')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
after(() => {
|
|
27
|
+
if (originalEnv.DATA_DIR === undefined) delete process.env.DATA_DIR
|
|
28
|
+
else process.env.DATA_DIR = originalEnv.DATA_DIR
|
|
29
|
+
if (originalEnv.WORKSPACE_DIR === undefined) delete process.env.WORKSPACE_DIR
|
|
30
|
+
else process.env.WORKSPACE_DIR = originalEnv.WORKSPACE_DIR
|
|
31
|
+
if (originalEnv.SWARMCLAW_BUILD_MODE === undefined) delete process.env.SWARMCLAW_BUILD_MODE
|
|
32
|
+
else process.env.SWARMCLAW_BUILD_MODE = originalEnv.SWARMCLAW_BUILD_MODE
|
|
33
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
describe('watch-jobs', () => {
|
|
37
|
+
it('validates required targets for durable watches', async () => {
|
|
38
|
+
await assert.rejects(
|
|
39
|
+
watchJobs.createWatchJob({
|
|
40
|
+
type: 'http',
|
|
41
|
+
resumeMessage: 'resume',
|
|
42
|
+
target: {},
|
|
43
|
+
condition: {},
|
|
44
|
+
}),
|
|
45
|
+
/url target/,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
await assert.rejects(
|
|
49
|
+
watchJobs.createWatchJob({
|
|
50
|
+
type: 'time',
|
|
51
|
+
resumeMessage: 'resume',
|
|
52
|
+
target: { source: 'test' },
|
|
53
|
+
condition: {},
|
|
54
|
+
}),
|
|
55
|
+
/runAt or delayMinutes/,
|
|
56
|
+
)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('triggers time and task watches durably', async () => {
|
|
60
|
+
const tasks = storage.loadTasks()
|
|
61
|
+
tasks.task_done = {
|
|
62
|
+
id: 'task_done',
|
|
63
|
+
title: 'done',
|
|
64
|
+
status: 'completed',
|
|
65
|
+
result: 'ok',
|
|
66
|
+
createdAt: Date.now(),
|
|
67
|
+
updatedAt: Date.now(),
|
|
68
|
+
}
|
|
69
|
+
storage.saveTasks(tasks)
|
|
70
|
+
|
|
71
|
+
const timeJob = await watchJobs.createWatchJob({
|
|
72
|
+
type: 'time',
|
|
73
|
+
resumeMessage: 'wake up',
|
|
74
|
+
target: { source: 'schedule_wake' },
|
|
75
|
+
condition: {},
|
|
76
|
+
runAt: Date.now() - 1000,
|
|
77
|
+
})
|
|
78
|
+
const taskJob = await watchJobs.createWatchJob({
|
|
79
|
+
type: 'task',
|
|
80
|
+
resumeMessage: 'task finished',
|
|
81
|
+
target: { taskId: 'task_done' },
|
|
82
|
+
condition: { statusIn: ['completed'] },
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const outcome = await watchJobs.processDueWatchJobs(Date.now())
|
|
86
|
+
|
|
87
|
+
assert.equal(outcome.triggered >= 2, true)
|
|
88
|
+
assert.equal(watchJobs.getWatchJob(timeJob.id)?.status, 'triggered')
|
|
89
|
+
assert.equal(watchJobs.getWatchJob(taskJob.id)?.status, 'triggered')
|
|
90
|
+
assert.equal(watchJobs.getWatchJob(taskJob.id)?.result?.status, 'completed')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('captures file changes and webhook triggers', async () => {
|
|
94
|
+
const watchedFile = path.join(tempDir, 'watch.txt')
|
|
95
|
+
fs.writeFileSync(watchedFile, 'alpha')
|
|
96
|
+
|
|
97
|
+
const fileJob = await watchJobs.createWatchJob({
|
|
98
|
+
type: 'file',
|
|
99
|
+
resumeMessage: 'file changed',
|
|
100
|
+
target: { path: watchedFile },
|
|
101
|
+
condition: { changed: true },
|
|
102
|
+
})
|
|
103
|
+
const webhookJob = await watchJobs.createWatchJob({
|
|
104
|
+
type: 'webhook',
|
|
105
|
+
resumeMessage: 'webhook arrived',
|
|
106
|
+
target: { webhookId: 'wh_1' },
|
|
107
|
+
condition: { event: 'build.finished' },
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
fs.writeFileSync(watchedFile, 'beta')
|
|
111
|
+
await watchJobs.processDueWatchJobs(Date.now())
|
|
112
|
+
const webhookMatches = watchJobs.triggerWebhookWatchJobs({
|
|
113
|
+
webhookId: 'wh_1',
|
|
114
|
+
event: 'build.finished',
|
|
115
|
+
payloadPreview: '{"ok":true}',
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
assert.equal(watchJobs.getWatchJob(fileJob.id)?.status, 'triggered')
|
|
119
|
+
assert.match(String(watchJobs.getWatchJob(fileJob.id)?.result?.preview || ''), /beta/)
|
|
120
|
+
assert.equal(webhookMatches.length, 1)
|
|
121
|
+
assert.equal(watchJobs.getWatchJob(webhookJob.id)?.status, 'triggered')
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('wakes mailbox and approval watches from event triggers', async () => {
|
|
125
|
+
storage.upsertApproval('approval_1', {
|
|
126
|
+
id: 'approval_1',
|
|
127
|
+
category: 'human_loop',
|
|
128
|
+
title: 'Need approval',
|
|
129
|
+
description: 'Approve the action',
|
|
130
|
+
data: {},
|
|
131
|
+
createdAt: Date.now(),
|
|
132
|
+
updatedAt: Date.now(),
|
|
133
|
+
status: 'pending',
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
const mailboxJob = await watchJobs.createWatchJob({
|
|
137
|
+
type: 'mailbox',
|
|
138
|
+
resumeMessage: 'human replied',
|
|
139
|
+
target: { sessionId: 'session_1' },
|
|
140
|
+
condition: { type: 'human_reply', correlationId: 'corr_1' },
|
|
141
|
+
})
|
|
142
|
+
const approvalJob = await watchJobs.createWatchJob({
|
|
143
|
+
type: 'approval',
|
|
144
|
+
resumeMessage: 'approval updated',
|
|
145
|
+
target: { approvalId: 'approval_1' },
|
|
146
|
+
condition: { statusIn: ['approved'] },
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const mailboxMatches = watchJobs.triggerMailboxWatchJobs({
|
|
150
|
+
sessionId: 'session_1',
|
|
151
|
+
envelope: {
|
|
152
|
+
id: 'env_1',
|
|
153
|
+
type: 'human_reply',
|
|
154
|
+
payload: 'approved',
|
|
155
|
+
toSessionId: 'session_1',
|
|
156
|
+
correlationId: 'corr_1',
|
|
157
|
+
status: 'new',
|
|
158
|
+
createdAt: Date.now(),
|
|
159
|
+
},
|
|
160
|
+
})
|
|
161
|
+
const approvalMatches = watchJobs.triggerApprovalWatchJobs({
|
|
162
|
+
approvalId: 'approval_1',
|
|
163
|
+
status: 'approved',
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
assert.equal(mailboxMatches.length, 1)
|
|
167
|
+
assert.equal(approvalMatches.length, 1)
|
|
168
|
+
assert.equal(watchJobs.getWatchJob(mailboxJob.id)?.status, 'triggered')
|
|
169
|
+
assert.equal(watchJobs.getWatchJob(approvalJob.id)?.status, 'triggered')
|
|
170
|
+
assert.equal(watchJobs.getWatchJob(mailboxJob.id)?.result?.correlationId, 'corr_1')
|
|
171
|
+
assert.equal(watchJobs.getWatchJob(approvalJob.id)?.result?.status, 'approved')
|
|
172
|
+
})
|
|
173
|
+
})
|
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
import crypto from 'crypto'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import * as cheerio from 'cheerio'
|
|
4
|
+
import { genId } from '@/lib/id'
|
|
5
|
+
import type { MailboxEnvelope, WatchJob } from '@/types'
|
|
6
|
+
import { requestHeartbeatNow } from './heartbeat-wake'
|
|
7
|
+
import { enqueueSystemEvent } from './system-events'
|
|
8
|
+
import { loadApprovals, loadTasks, loadWatchJobs, upsertWatchJob, upsertWatchJobs } from './storage'
|
|
9
|
+
import { notify } from './ws-hub'
|
|
10
|
+
import { fetchMailboxMessages, getMailboxHighwaterUid } from './mailbox-utils'
|
|
11
|
+
|
|
12
|
+
export interface CreateWatchJobInput {
|
|
13
|
+
type: WatchJob['type']
|
|
14
|
+
sessionId?: string | null
|
|
15
|
+
agentId?: string | null
|
|
16
|
+
createdByAgentId?: string | null
|
|
17
|
+
browserProfileId?: string | null
|
|
18
|
+
description?: string | null
|
|
19
|
+
resumeMessage: string
|
|
20
|
+
target: Record<string, unknown>
|
|
21
|
+
condition: Record<string, unknown>
|
|
22
|
+
runAt?: number | null
|
|
23
|
+
intervalMs?: number | null
|
|
24
|
+
timeoutAt?: number | null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function now() {
|
|
28
|
+
return Date.now()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function hashContent(value: string): string {
|
|
32
|
+
return crypto.createHash('sha1').update(value).digest('hex')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function cleanHtmlToText(html: string): string {
|
|
36
|
+
const $ = cheerio.load(html)
|
|
37
|
+
$('script, style, noscript').remove()
|
|
38
|
+
return $('body').text().replace(/\s+/g, ' ').trim()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function matchesRegex(body: string, pattern: unknown): boolean {
|
|
42
|
+
if (typeof pattern !== 'string' || !pattern.trim()) return false
|
|
43
|
+
try {
|
|
44
|
+
return new RegExp(pattern, 'i').test(body)
|
|
45
|
+
} catch {
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function scheduleNextCheck(job: WatchJob, at = now()): WatchJob {
|
|
51
|
+
const intervalMs = typeof job.intervalMs === 'number' && job.intervalMs > 0 ? job.intervalMs : 60_000
|
|
52
|
+
return {
|
|
53
|
+
...job,
|
|
54
|
+
nextCheckAt: at + intervalMs,
|
|
55
|
+
updatedAt: at,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function notifyWatchJobsChanged() {
|
|
60
|
+
notify('watch_jobs')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function finalizeWatchJob(job: WatchJob, status: WatchJob['status'], result?: Record<string, unknown> | null, error?: string | null): WatchJob {
|
|
64
|
+
const updated: WatchJob = {
|
|
65
|
+
...job,
|
|
66
|
+
status,
|
|
67
|
+
result: result ?? job.result ?? null,
|
|
68
|
+
lastError: error ?? null,
|
|
69
|
+
lastTriggeredAt: status === 'triggered' ? now() : job.lastTriggeredAt ?? null,
|
|
70
|
+
updatedAt: now(),
|
|
71
|
+
}
|
|
72
|
+
upsertWatchJob(updated.id, updated)
|
|
73
|
+
notifyWatchJobsChanged()
|
|
74
|
+
return updated
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function wakeFromWatch(job: WatchJob, result?: Record<string, unknown> | null) {
|
|
78
|
+
const summary = job.description || `Watch ${job.id}`
|
|
79
|
+
const detail = result ? JSON.stringify(result).slice(0, 1200) : ''
|
|
80
|
+
if (job.sessionId) {
|
|
81
|
+
enqueueSystemEvent(
|
|
82
|
+
job.sessionId,
|
|
83
|
+
`[Watch Triggered] ${summary}\n${job.resumeMessage}${detail ? `\n\nObserved:\n${detail}` : ''}`,
|
|
84
|
+
)
|
|
85
|
+
requestHeartbeatNow({ sessionId: job.sessionId, reason: 'watch_job' })
|
|
86
|
+
} else if (job.agentId) {
|
|
87
|
+
requestHeartbeatNow({ agentId: job.agentId, reason: 'watch_job' })
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function createWatchJob(input: CreateWatchJobInput): Promise<WatchJob> {
|
|
92
|
+
if (input.type === 'time' && typeof input.runAt !== 'number') {
|
|
93
|
+
throw new Error('Time watches require runAt or delayMinutes.')
|
|
94
|
+
}
|
|
95
|
+
if ((input.type === 'http' || input.type === 'page') && typeof input.target?.url !== 'string') {
|
|
96
|
+
throw new Error(`${input.type} watches require a url target.`)
|
|
97
|
+
}
|
|
98
|
+
if (input.type === 'file' && typeof input.target?.path !== 'string') {
|
|
99
|
+
throw new Error('File watches require a path target.')
|
|
100
|
+
}
|
|
101
|
+
if (input.type === 'task' && typeof input.target?.taskId !== 'string') {
|
|
102
|
+
throw new Error('Task watches require a taskId target.')
|
|
103
|
+
}
|
|
104
|
+
if (input.type === 'webhook' && typeof input.target?.webhookId !== 'string') {
|
|
105
|
+
throw new Error('Webhook watches require a webhookId target.')
|
|
106
|
+
}
|
|
107
|
+
if (input.type === 'email' && typeof input.target?.folder !== 'string' && typeof input.target?.folder !== 'undefined') {
|
|
108
|
+
throw new Error('Email watches expect a string folder when provided.')
|
|
109
|
+
}
|
|
110
|
+
if (input.type === 'mailbox' && typeof input.target?.sessionId !== 'string') {
|
|
111
|
+
throw new Error('Mailbox watches require a sessionId target.')
|
|
112
|
+
}
|
|
113
|
+
if (input.type === 'approval' && typeof input.target?.approvalId !== 'string') {
|
|
114
|
+
throw new Error('Approval watches require an approvalId target.')
|
|
115
|
+
}
|
|
116
|
+
const createdAt = now()
|
|
117
|
+
const job: WatchJob = {
|
|
118
|
+
id: genId(10),
|
|
119
|
+
type: input.type,
|
|
120
|
+
status: 'active',
|
|
121
|
+
description: input.description || null,
|
|
122
|
+
sessionId: input.sessionId || null,
|
|
123
|
+
agentId: input.agentId || null,
|
|
124
|
+
createdByAgentId: input.createdByAgentId || null,
|
|
125
|
+
browserProfileId: input.browserProfileId || null,
|
|
126
|
+
resumeMessage: input.resumeMessage,
|
|
127
|
+
target: { ...(input.target || {}) },
|
|
128
|
+
condition: { ...(input.condition || {}) },
|
|
129
|
+
runAt: input.runAt ?? null,
|
|
130
|
+
nextCheckAt: input.runAt ?? createdAt,
|
|
131
|
+
intervalMs: input.intervalMs ?? (input.type === 'time' ? null : 60_000),
|
|
132
|
+
timeoutAt: input.timeoutAt ?? null,
|
|
133
|
+
lastCheckedAt: null,
|
|
134
|
+
lastTriggeredAt: null,
|
|
135
|
+
lastError: null,
|
|
136
|
+
result: null,
|
|
137
|
+
createdAt,
|
|
138
|
+
updatedAt: createdAt,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Capture initial baselines for change watches.
|
|
142
|
+
if ((job.type === 'http' || job.type === 'page') && typeof job.target.url === 'string' && job.condition.changed === true) {
|
|
143
|
+
try {
|
|
144
|
+
const res = await fetch(job.target.url, { signal: AbortSignal.timeout(15_000) })
|
|
145
|
+
if (res.ok) {
|
|
146
|
+
const text = job.type === 'page'
|
|
147
|
+
? cleanHtmlToText(await res.text())
|
|
148
|
+
: await res.text()
|
|
149
|
+
job.target = { ...job.target, baselineHash: hashContent(text) }
|
|
150
|
+
}
|
|
151
|
+
} catch {
|
|
152
|
+
// Baseline creation is best-effort; the watch can still run later.
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (job.type === 'file' && typeof job.target.path === 'string' && job.condition.changed === true) {
|
|
157
|
+
try {
|
|
158
|
+
if (fs.existsSync(job.target.path)) {
|
|
159
|
+
const text = fs.readFileSync(job.target.path, 'utf8')
|
|
160
|
+
job.target = { ...job.target, baselineHash: hashContent(text) }
|
|
161
|
+
}
|
|
162
|
+
} catch {
|
|
163
|
+
// Best-effort baseline only.
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (job.type === 'email') {
|
|
168
|
+
try {
|
|
169
|
+
const baselineUid = await getMailboxHighwaterUid(undefined, typeof job.target.folder === 'string' ? job.target.folder : undefined)
|
|
170
|
+
job.target = {
|
|
171
|
+
...job.target,
|
|
172
|
+
baselineUid,
|
|
173
|
+
}
|
|
174
|
+
} catch {
|
|
175
|
+
// best-effort baseline only
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
upsertWatchJob(job.id, job)
|
|
180
|
+
notifyWatchJobsChanged()
|
|
181
|
+
return job
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function cancelWatchJob(id: string): WatchJob | null {
|
|
185
|
+
const all = loadWatchJobs()
|
|
186
|
+
const current = all[id]
|
|
187
|
+
if (!current || typeof current !== 'object') return null
|
|
188
|
+
return finalizeWatchJob(current as WatchJob, 'cancelled', null, null)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function getWatchJob(id: string): WatchJob | null {
|
|
192
|
+
const all = loadWatchJobs()
|
|
193
|
+
const current = all[id]
|
|
194
|
+
if (!current || typeof current !== 'object') return null
|
|
195
|
+
return current as WatchJob
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function listWatchJobs(filter?: { sessionId?: string | null; status?: WatchJob['status'] | null }): WatchJob[] {
|
|
199
|
+
return Object.values(loadWatchJobs())
|
|
200
|
+
.filter((job): job is WatchJob => !!job && typeof job === 'object')
|
|
201
|
+
.filter((job) => !filter?.sessionId || job.sessionId === filter.sessionId)
|
|
202
|
+
.filter((job) => !filter?.status || job.status === filter.status)
|
|
203
|
+
.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0))
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function evaluateHttpLikeJob(job: WatchJob, asPage: boolean): Promise<{ triggered: boolean; result: Record<string, unknown> }> {
|
|
207
|
+
const url = typeof job.target.url === 'string' ? job.target.url : ''
|
|
208
|
+
if (!url) return { triggered: false, result: { error: 'Missing url' } }
|
|
209
|
+
const startedAt = Date.now()
|
|
210
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(15_000) })
|
|
211
|
+
const latencyMs = Date.now() - startedAt
|
|
212
|
+
const raw = await res.text()
|
|
213
|
+
const body = asPage ? cleanHtmlToText(raw) : raw
|
|
214
|
+
const bodyHash = hashContent(body)
|
|
215
|
+
const containsText = typeof job.condition.containsText === 'string' ? job.condition.containsText : ''
|
|
216
|
+
const textGone = typeof job.condition.textGone === 'string' ? job.condition.textGone : ''
|
|
217
|
+
const statusEquals = typeof job.condition.status === 'number' ? job.condition.status : null
|
|
218
|
+
const statusIn = Array.isArray(job.condition.statusIn)
|
|
219
|
+
? job.condition.statusIn.filter((value): value is number => typeof value === 'number')
|
|
220
|
+
: []
|
|
221
|
+
const changed = job.condition.changed === true
|
|
222
|
+
const latencyThreshold = typeof job.condition.threshold === 'number' ? job.condition.threshold : null
|
|
223
|
+
const baselineHash = typeof job.target.baselineHash === 'string' ? job.target.baselineHash : ''
|
|
224
|
+
const regexMatched = matchesRegex(body, job.condition.regex)
|
|
225
|
+
const triggered =
|
|
226
|
+
(statusEquals !== null && res.status === statusEquals)
|
|
227
|
+
|| (statusIn.length > 0 && statusIn.includes(res.status))
|
|
228
|
+
|| (!!containsText && body.includes(containsText))
|
|
229
|
+
|| (!!textGone && !body.includes(textGone))
|
|
230
|
+
|| regexMatched
|
|
231
|
+
|| (!!changed && !!baselineHash && baselineHash !== bodyHash)
|
|
232
|
+
|| (latencyThreshold !== null && latencyMs >= latencyThreshold)
|
|
233
|
+
return {
|
|
234
|
+
triggered,
|
|
235
|
+
result: {
|
|
236
|
+
url,
|
|
237
|
+
status: res.status,
|
|
238
|
+
latencyMs,
|
|
239
|
+
containsText: containsText || undefined,
|
|
240
|
+
regex: typeof job.condition.regex === 'string' ? job.condition.regex : undefined,
|
|
241
|
+
changed: changed || undefined,
|
|
242
|
+
bodyHash,
|
|
243
|
+
preview: body.slice(0, 1200),
|
|
244
|
+
},
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function evaluateFileJob(job: WatchJob): { triggered: boolean; result: Record<string, unknown> } {
|
|
249
|
+
const targetPath = typeof job.target.path === 'string' ? job.target.path : ''
|
|
250
|
+
if (!targetPath) return { triggered: false, result: { error: 'Missing path' } }
|
|
251
|
+
const exists = fs.existsSync(targetPath)
|
|
252
|
+
const expectExists = job.condition.exists !== false
|
|
253
|
+
const containsText = typeof job.condition.containsText === 'string' ? job.condition.containsText : ''
|
|
254
|
+
const changed = job.condition.changed === true
|
|
255
|
+
let text = ''
|
|
256
|
+
let bodyHash = ''
|
|
257
|
+
try {
|
|
258
|
+
if (exists) {
|
|
259
|
+
text = fs.readFileSync(targetPath, 'utf8')
|
|
260
|
+
bodyHash = hashContent(text)
|
|
261
|
+
}
|
|
262
|
+
} catch {
|
|
263
|
+
text = ''
|
|
264
|
+
}
|
|
265
|
+
const baselineHash = typeof job.target.baselineHash === 'string' ? job.target.baselineHash : ''
|
|
266
|
+
const triggered =
|
|
267
|
+
exists === expectExists
|
|
268
|
+
&& (!containsText || text.includes(containsText))
|
|
269
|
+
&& (!job.condition.regex || matchesRegex(text, job.condition.regex))
|
|
270
|
+
&& (!changed || (!!baselineHash && baselineHash !== bodyHash))
|
|
271
|
+
return {
|
|
272
|
+
triggered,
|
|
273
|
+
result: {
|
|
274
|
+
path: targetPath,
|
|
275
|
+
exists,
|
|
276
|
+
regex: typeof job.condition.regex === 'string' ? job.condition.regex : undefined,
|
|
277
|
+
bodyHash: bodyHash || undefined,
|
|
278
|
+
preview: text.slice(0, 1200),
|
|
279
|
+
},
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function evaluateTaskJob(job: WatchJob): { triggered: boolean; result: Record<string, unknown> } {
|
|
284
|
+
const taskId = typeof job.target.taskId === 'string' ? job.target.taskId : ''
|
|
285
|
+
if (!taskId) return { triggered: false, result: { error: 'Missing taskId' } }
|
|
286
|
+
const tasks = loadTasks()
|
|
287
|
+
const task = tasks[taskId] as Record<string, unknown> | undefined
|
|
288
|
+
const statuses = Array.isArray(job.condition.statusIn)
|
|
289
|
+
? job.condition.statusIn.filter((value): value is string => typeof value === 'string')
|
|
290
|
+
: ['completed', 'failed']
|
|
291
|
+
const currentStatus = typeof task?.status === 'string' ? task.status : 'missing'
|
|
292
|
+
return {
|
|
293
|
+
triggered: statuses.includes(currentStatus),
|
|
294
|
+
result: {
|
|
295
|
+
taskId,
|
|
296
|
+
status: currentStatus,
|
|
297
|
+
title: typeof task?.title === 'string' ? task.title : null,
|
|
298
|
+
result: typeof task?.result === 'string' ? task.result.slice(0, 1000) : null,
|
|
299
|
+
error: typeof task?.error === 'string' ? task.error : null,
|
|
300
|
+
},
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function evaluateWatchJob(job: WatchJob): Promise<{ triggered: boolean; result: Record<string, unknown> }> {
|
|
305
|
+
if (job.type === 'time') {
|
|
306
|
+
const runAt = typeof job.runAt === 'number' ? job.runAt : 0
|
|
307
|
+
return { triggered: runAt > 0 && runAt <= now(), result: { runAt } }
|
|
308
|
+
}
|
|
309
|
+
if (job.type === 'http') return evaluateHttpLikeJob(job, false)
|
|
310
|
+
if (job.type === 'page') return evaluateHttpLikeJob(job, true)
|
|
311
|
+
if (job.type === 'file') return evaluateFileJob(job)
|
|
312
|
+
if (job.type === 'task') return evaluateTaskJob(job)
|
|
313
|
+
if (job.type === 'email') {
|
|
314
|
+
const folder = typeof job.target.folder === 'string' ? job.target.folder : undefined
|
|
315
|
+
const messages = await fetchMailboxMessages({
|
|
316
|
+
folder,
|
|
317
|
+
from: typeof job.condition.from === 'string' ? job.condition.from : undefined,
|
|
318
|
+
subjectContains: typeof job.condition.subjectContains === 'string' ? job.condition.subjectContains : undefined,
|
|
319
|
+
bodyContains: typeof job.condition.containsText === 'string' ? job.condition.containsText : undefined,
|
|
320
|
+
query: typeof job.condition.query === 'string' ? job.condition.query : undefined,
|
|
321
|
+
unreadOnly: job.condition.unreadOnly === true,
|
|
322
|
+
hasAttachments: job.condition.hasAttachments === true,
|
|
323
|
+
uidGreaterThan: typeof job.target.baselineUid === 'number' ? job.target.baselineUid : undefined,
|
|
324
|
+
limit: 20,
|
|
325
|
+
})
|
|
326
|
+
const match = messages[0]
|
|
327
|
+
return {
|
|
328
|
+
triggered: !!match,
|
|
329
|
+
result: match
|
|
330
|
+
? {
|
|
331
|
+
uid: match.uid,
|
|
332
|
+
from: match.from,
|
|
333
|
+
subject: match.subject,
|
|
334
|
+
snippet: match.snippet,
|
|
335
|
+
attachmentCount: match.attachments.length,
|
|
336
|
+
messageId: match.messageId,
|
|
337
|
+
}
|
|
338
|
+
: {
|
|
339
|
+
folder: folder || 'INBOX',
|
|
340
|
+
baselineUid: typeof job.target.baselineUid === 'number' ? job.target.baselineUid : null,
|
|
341
|
+
},
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return { triggered: false, result: { note: 'Webhook waits are triggered by inbound webhook delivery.' } }
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export async function processDueWatchJobs(timestamp = now()): Promise<{ checked: number; triggered: number; failed: number }> {
|
|
348
|
+
const all = listWatchJobs({ status: 'active' })
|
|
349
|
+
let checked = 0
|
|
350
|
+
let triggered = 0
|
|
351
|
+
let failed = 0
|
|
352
|
+
const updates: Array<[string, WatchJob]> = []
|
|
353
|
+
|
|
354
|
+
for (const job of all) {
|
|
355
|
+
if (typeof job.timeoutAt === 'number' && job.timeoutAt > 0 && job.timeoutAt <= timestamp) {
|
|
356
|
+
failed += 1
|
|
357
|
+
updates.push([job.id, {
|
|
358
|
+
...job,
|
|
359
|
+
status: 'failed',
|
|
360
|
+
lastError: 'Watch timed out before condition was met.',
|
|
361
|
+
updatedAt: timestamp,
|
|
362
|
+
}])
|
|
363
|
+
continue
|
|
364
|
+
}
|
|
365
|
+
if (typeof job.nextCheckAt === 'number' && job.nextCheckAt > timestamp) continue
|
|
366
|
+
if (job.type === 'webhook' || job.type === 'mailbox' || job.type === 'approval') continue
|
|
367
|
+
|
|
368
|
+
checked += 1
|
|
369
|
+
try {
|
|
370
|
+
const evaluation = await evaluateWatchJob(job)
|
|
371
|
+
if (evaluation.triggered) {
|
|
372
|
+
triggered += 1
|
|
373
|
+
const completed = {
|
|
374
|
+
...job,
|
|
375
|
+
status: 'triggered' as const,
|
|
376
|
+
result: evaluation.result,
|
|
377
|
+
lastError: null,
|
|
378
|
+
lastCheckedAt: timestamp,
|
|
379
|
+
lastTriggeredAt: timestamp,
|
|
380
|
+
updatedAt: timestamp,
|
|
381
|
+
}
|
|
382
|
+
updates.push([job.id, completed])
|
|
383
|
+
wakeFromWatch(completed, evaluation.result)
|
|
384
|
+
} else {
|
|
385
|
+
updates.push([job.id, scheduleNextCheck({
|
|
386
|
+
...job,
|
|
387
|
+
lastCheckedAt: timestamp,
|
|
388
|
+
result: evaluation.result,
|
|
389
|
+
}, timestamp)])
|
|
390
|
+
}
|
|
391
|
+
} catch (err: unknown) {
|
|
392
|
+
failed += 1
|
|
393
|
+
updates.push([job.id, scheduleNextCheck({
|
|
394
|
+
...job,
|
|
395
|
+
lastCheckedAt: timestamp,
|
|
396
|
+
lastError: err instanceof Error ? err.message : String(err),
|
|
397
|
+
}, timestamp)])
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (updates.length > 0) {
|
|
402
|
+
upsertWatchJobs(updates)
|
|
403
|
+
notifyWatchJobsChanged()
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return { checked, triggered, failed }
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export function triggerWebhookWatchJobs(params: {
|
|
410
|
+
webhookId: string
|
|
411
|
+
event: string
|
|
412
|
+
payloadPreview?: string
|
|
413
|
+
}): WatchJob[] {
|
|
414
|
+
const matches = listWatchJobs({ status: 'active' }).filter((job) => {
|
|
415
|
+
if (job.type !== 'webhook') return false
|
|
416
|
+
const watchWebhookId = typeof job.target.webhookId === 'string' ? job.target.webhookId : ''
|
|
417
|
+
if (watchWebhookId !== params.webhookId) return false
|
|
418
|
+
const expectedEvent = typeof job.condition.event === 'string' ? job.condition.event.trim() : ''
|
|
419
|
+
return !expectedEvent || expectedEvent === params.event
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
const updated = matches.map((job) => {
|
|
423
|
+
const next: WatchJob = {
|
|
424
|
+
...job,
|
|
425
|
+
status: 'triggered',
|
|
426
|
+
result: {
|
|
427
|
+
webhookId: params.webhookId,
|
|
428
|
+
event: params.event,
|
|
429
|
+
payloadPreview: params.payloadPreview?.slice(0, 1200) || '',
|
|
430
|
+
},
|
|
431
|
+
lastTriggeredAt: now(),
|
|
432
|
+
updatedAt: now(),
|
|
433
|
+
}
|
|
434
|
+
wakeFromWatch(next, next.result || null)
|
|
435
|
+
return [next.id, next] as [string, WatchJob]
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
if (updated.length > 0) {
|
|
439
|
+
upsertWatchJobs(updated)
|
|
440
|
+
notifyWatchJobsChanged()
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return updated.map(([, job]) => job)
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export function triggerMailboxWatchJobs(params: {
|
|
447
|
+
sessionId: string
|
|
448
|
+
envelope: MailboxEnvelope
|
|
449
|
+
}): WatchJob[] {
|
|
450
|
+
const matches = listWatchJobs({ status: 'active' }).filter((job) => {
|
|
451
|
+
if (job.type !== 'mailbox') return false
|
|
452
|
+
const targetSessionId = typeof job.target.sessionId === 'string' ? job.target.sessionId : ''
|
|
453
|
+
if (targetSessionId !== params.sessionId) return false
|
|
454
|
+
const expectedType = typeof job.condition.type === 'string' ? job.condition.type.trim() : ''
|
|
455
|
+
const correlationId = typeof job.condition.correlationId === 'string' ? job.condition.correlationId.trim() : ''
|
|
456
|
+
const fromSessionId = typeof job.condition.fromSessionId === 'string' ? job.condition.fromSessionId.trim() : ''
|
|
457
|
+
const payloadContains = typeof job.condition.containsText === 'string' ? job.condition.containsText.trim() : ''
|
|
458
|
+
if (expectedType && params.envelope.type !== expectedType) return false
|
|
459
|
+
if (correlationId && params.envelope.correlationId !== correlationId) return false
|
|
460
|
+
if (fromSessionId && params.envelope.fromSessionId !== fromSessionId) return false
|
|
461
|
+
if (payloadContains && !params.envelope.payload.includes(payloadContains)) return false
|
|
462
|
+
return true
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
const updated = matches.map((job) => {
|
|
466
|
+
const next: WatchJob = {
|
|
467
|
+
...job,
|
|
468
|
+
status: 'triggered',
|
|
469
|
+
result: {
|
|
470
|
+
envelopeId: params.envelope.id,
|
|
471
|
+
type: params.envelope.type,
|
|
472
|
+
correlationId: params.envelope.correlationId || null,
|
|
473
|
+
payload: params.envelope.payload.slice(0, 1200),
|
|
474
|
+
fromSessionId: params.envelope.fromSessionId || null,
|
|
475
|
+
},
|
|
476
|
+
lastTriggeredAt: now(),
|
|
477
|
+
updatedAt: now(),
|
|
478
|
+
}
|
|
479
|
+
wakeFromWatch(next, next.result || null)
|
|
480
|
+
return [next.id, next] as [string, WatchJob]
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
if (updated.length > 0) {
|
|
484
|
+
upsertWatchJobs(updated)
|
|
485
|
+
notifyWatchJobsChanged()
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return updated.map(([, job]) => job)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export function triggerApprovalWatchJobs(params: {
|
|
492
|
+
approvalId: string
|
|
493
|
+
status: 'approved' | 'rejected'
|
|
494
|
+
title?: string
|
|
495
|
+
description?: string
|
|
496
|
+
}): WatchJob[] {
|
|
497
|
+
const approvals = loadApprovals()
|
|
498
|
+
const approval = approvals[params.approvalId] as Record<string, unknown> | undefined
|
|
499
|
+
const matches = listWatchJobs({ status: 'active' }).filter((job) => {
|
|
500
|
+
if (job.type !== 'approval') return false
|
|
501
|
+
const targetApprovalId = typeof job.target.approvalId === 'string' ? job.target.approvalId : ''
|
|
502
|
+
if (targetApprovalId !== params.approvalId) return false
|
|
503
|
+
const statuses = Array.isArray(job.condition.statusIn)
|
|
504
|
+
? job.condition.statusIn.filter((value): value is string => typeof value === 'string')
|
|
505
|
+
: ['approved']
|
|
506
|
+
return statuses.includes(params.status)
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
const updated = matches.map((job) => {
|
|
510
|
+
const next: WatchJob = {
|
|
511
|
+
...job,
|
|
512
|
+
status: 'triggered',
|
|
513
|
+
result: {
|
|
514
|
+
approvalId: params.approvalId,
|
|
515
|
+
status: params.status,
|
|
516
|
+
title: params.title || (typeof approval?.title === 'string' ? approval.title : null),
|
|
517
|
+
description: params.description || (typeof approval?.description === 'string' ? approval.description : null),
|
|
518
|
+
},
|
|
519
|
+
lastTriggeredAt: now(),
|
|
520
|
+
updatedAt: now(),
|
|
521
|
+
}
|
|
522
|
+
wakeFromWatch(next, next.result || null)
|
|
523
|
+
return [next.id, next] as [string, WatchJob]
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
if (updated.length > 0) {
|
|
527
|
+
upsertWatchJobs(updated)
|
|
528
|
+
notifyWatchJobsChanged()
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return updated.map(([, job]) => job)
|
|
532
|
+
}
|