@swarmclawai/swarmclaw 0.7.1 → 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 +155 -150
- package/package.json +1 -1
- package/src/app/api/agents/[id]/route.ts +26 -0
- package/src/app/api/agents/[id]/thread/route.ts +37 -9
- package/src/app/api/agents/route.ts +13 -2
- package/src/app/api/auth/route.ts +76 -7
- package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
- package/src/app/api/{sessions → chats}/[id]/browser/route.ts +5 -1
- package/src/app/api/{sessions → chats}/[id]/chat/route.ts +7 -3
- package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
- package/src/app/api/chats/[id]/main-loop/route.ts +13 -0
- package/src/app/api/{sessions → chats}/[id]/messages/route.ts +19 -13
- package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
- package/src/app/api/{sessions → chats}/[id]/route.ts +22 -52
- package/src/app/api/{sessions → chats}/[id]/stop/route.ts +6 -1
- package/src/app/api/{sessions → chats}/route.ts +21 -7
- 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 +6 -26
- package/src/app/api/plugins/settings/route.ts +40 -0
- 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/usage/route.ts +30 -0
- package/src/app/api/webhooks/[id]/route.ts +8 -1
- package/src/app/page.tsx +9 -2
- package/src/cli/index.js +39 -33
- package/src/cli/index.ts +43 -49
- package/src/cli/spec.js +29 -27
- package/src/components/agents/agent-card.tsx +16 -13
- package/src/components/agents/agent-chat-list.tsx +104 -4
- package/src/components/agents/agent-list.tsx +54 -22
- package/src/components/agents/agent-sheet.tsx +209 -18
- package/src/components/agents/cron-job-form.tsx +3 -3
- package/src/components/agents/inspector-panel.tsx +110 -50
- package/src/components/auth/access-key-gate.tsx +36 -97
- package/src/components/auth/setup-wizard.tsx +5 -38
- package/src/components/chat/chat-area.tsx +39 -27
- package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +7 -23
- package/src/components/chat/chat-header.tsx +299 -314
- package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +11 -14
- package/src/components/chat/chat-tool-toggles.tsx +26 -17
- package/src/components/chat/checkpoint-timeline.tsx +4 -4
- package/src/components/chat/message-bubble.tsx +4 -1
- package/src/components/chat/message-list.tsx +5 -3
- package/src/components/chat/session-debug-panel.tsx +1 -1
- package/src/components/chat/tool-request-banner.tsx +3 -3
- package/src/components/chatrooms/agent-hover-card.tsx +3 -3
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
- 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 +218 -1
- package/src/components/home/home-view.tsx +129 -5
- package/src/components/layout/app-layout.tsx +392 -182
- package/src/components/layout/mobile-header.tsx +26 -8
- package/src/components/plugins/plugin-list.tsx +487 -254
- package/src/components/plugins/plugin-sheet.tsx +236 -13
- package/src/components/projects/project-detail.tsx +183 -0
- package/src/components/settings/gateway-connection-panel.tsx +1 -1
- package/src/components/shared/agent-picker-list.tsx +2 -2
- package/src/components/shared/command-palette.tsx +111 -25
- 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 +78 -1
- package/src/components/shared/settings/section-orchestrator.tsx +3 -3
- package/src/components/shared/settings/section-providers.tsx +1 -1
- 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 +244 -56
- 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 +147 -1
- package/src/components/wallets/wallet-panel.tsx +17 -5
- package/src/components/webhooks/webhook-sheet.tsx +8 -8
- 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/chat.ts +1 -1
- package/src/lib/{sessions.ts → chats.ts} +28 -18
- package/src/lib/openclaw-agent-id.test.ts +14 -0
- package/src/lib/openclaw-agent-id.ts +31 -0
- package/src/lib/providers/claude-cli.ts +1 -1
- 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/capability-router.ts +10 -8
- 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 +285 -165
- 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 +48 -8
- 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 +948 -112
- 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/cost.ts +34 -1
- package/src/lib/server/daemon-state.ts +61 -3
- 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 +14 -40
- 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 +28 -1103
- 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 +20 -9
- package/src/lib/server/orchestrator.ts +7 -7
- package/src/lib/server/playwright-proxy.mjs +27 -3
- package/src/lib/server/plugins.test.ts +207 -0
- package/src/lib/server/plugins.ts +927 -66
- package/src/lib/server/provider-health.ts +38 -6
- package/src/lib/server/queue.ts +13 -28
- 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 -82
- package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
- package/src/lib/server/session-tools/calendar.ts +366 -0
- package/src/lib/server/session-tools/canvas.ts +1 -1
- package/src/lib/server/session-tools/chatroom.ts +4 -2
- package/src/lib/server/session-tools/connector.ts +114 -10
- package/src/lib/server/session-tools/context.ts +21 -5
- package/src/lib/server/session-tools/crawl.ts +447 -0
- package/src/lib/server/session-tools/crud.ts +74 -28
- package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
- package/src/lib/server/session-tools/delegate.ts +497 -24
- package/src/lib/server/session-tools/discovery.ts +24 -6
- package/src/lib/server/session-tools/document.ts +283 -0
- package/src/lib/server/session-tools/edit_file.ts +4 -2
- package/src/lib/server/session-tools/email.ts +320 -0
- 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 +241 -25
- package/src/lib/server/session-tools/git.ts +1 -1
- package/src/lib/server/session-tools/http.ts +1 -1
- package/src/lib/server/session-tools/human-loop.ts +227 -0
- package/src/lib/server/session-tools/image-gen.ts +380 -0
- package/src/lib/server/session-tools/index.ts +130 -50
- package/src/lib/server/session-tools/mailbox.ts +276 -0
- package/src/lib/server/session-tools/memory.ts +172 -3
- package/src/lib/server/session-tools/monitor.ts +151 -8
- package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
- package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
- package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
- package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
- package/src/lib/server/session-tools/platform.ts +148 -7
- package/src/lib/server/session-tools/plugin-creator.ts +89 -26
- package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
- package/src/lib/server/session-tools/replicate.ts +301 -0
- package/src/lib/server/session-tools/sample-ui.ts +1 -1
- package/src/lib/server/session-tools/sandbox.ts +4 -2
- package/src/lib/server/session-tools/schedule.ts +24 -12
- package/src/lib/server/session-tools/session-info.ts +43 -7
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
- package/src/lib/server/session-tools/shell.ts +5 -2
- package/src/lib/server/session-tools/subagent.ts +194 -28
- package/src/lib/server/session-tools/table.ts +587 -0
- package/src/lib/server/session-tools/wallet.ts +42 -12
- package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
- package/src/lib/server/session-tools/web.ts +926 -91
- package/src/lib/server/storage.ts +255 -16
- package/src/lib/server/stream-agent-chat.ts +116 -268
- 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 +66 -18
- package/src/lib/server/tool-capability-policy.test.ts +9 -9
- package/src/lib/server/tool-capability-policy.ts +38 -27
- 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/tool-definitions.ts +4 -0
- package/src/lib/validation/schemas.test.ts +26 -0
- package/src/lib/validation/schemas.ts +10 -1
- package/src/lib/ws-client.ts +14 -12
- package/src/proxy.ts +5 -5
- package/src/stores/use-app-store.ts +5 -11
- package/src/stores/use-chat-store.ts +38 -9
- package/src/types/index.ts +352 -47
- package/src/app/api/sessions/[id]/main-loop/route.ts +0 -94
- package/src/components/sessions/new-session-sheet.tsx +0 -253
- package/src/lib/server/main-session.ts +0 -24
- package/src/lib/server/session-run-manager.test.ts +0 -23
- /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/heartbeat/route.ts +0 -0
|
@@ -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
|
+
}
|
package/src/lib/server/ws-hub.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { WebSocketServer, WebSocket } from 'ws'
|
|
2
2
|
import type { IncomingMessage } from 'http'
|
|
3
3
|
import { validateAccessKey } from './storage'
|
|
4
|
+
import { AUTH_COOKIE_NAME, getCookieValue } from '@/lib/auth'
|
|
4
5
|
|
|
5
6
|
interface WsClient {
|
|
6
7
|
ws: WebSocket
|
|
@@ -29,9 +30,10 @@ export function initWsServer() {
|
|
|
29
30
|
;(globalThis as any)[GK] = hub
|
|
30
31
|
|
|
31
32
|
wss.on('connection', (ws: WebSocket, req: IncomingMessage) => {
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
33
|
+
const headerKey = req.headers['x-access-key']
|
|
34
|
+
const key = (Array.isArray(headerKey) ? headerKey[0] : headerKey)
|
|
35
|
+
|| getCookieValue(req.headers.cookie, AUTH_COOKIE_NAME)
|
|
36
|
+
|| ''
|
|
35
37
|
if (!validateAccessKey(key)) {
|
|
36
38
|
ws.close(4001, 'Unauthorized')
|
|
37
39
|
return
|
|
@@ -25,6 +25,10 @@ export const AVAILABLE_TOOLS: ToolDefinition[] = [
|
|
|
25
25
|
{ id: 'monitor', label: 'Monitor', description: 'System observability: check resource usage, watch logs, and ping endpoints' },
|
|
26
26
|
{ id: 'plugin_creator', label: 'Plugin Creator', description: 'Design, write, and test custom SwarmClaw plugins dynamically' },
|
|
27
27
|
{ id: 'sample_ui', label: 'Sample UI', description: 'Demonstration of dynamic UI injection into Sidebar and Chat Header' },
|
|
28
|
+
{ id: 'image_gen', label: 'Image Generation', description: 'Generate images from text prompts using OpenAI, Stability AI, Replicate, fal.ai, and more' },
|
|
29
|
+
{ id: 'email', label: 'Email', description: 'Send emails via SMTP with plain text and HTML support' },
|
|
30
|
+
{ id: 'calendar', label: 'Calendar', description: 'Manage Google Calendar or Outlook events — list, create, update, delete' },
|
|
31
|
+
{ id: 'replicate', label: 'Replicate', description: 'Run any AI model on Replicate — image generation, LLMs, audio, video, and more' },
|
|
28
32
|
]
|
|
29
33
|
|
|
30
34
|
/**
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { AgentCreateSchema } from './schemas'
|
|
4
|
+
|
|
5
|
+
describe('AgentCreateSchema', () => {
|
|
6
|
+
it('defaults platformAssignScope to self', () => {
|
|
7
|
+
const parsed = AgentCreateSchema.parse({
|
|
8
|
+
name: 'Solo Agent',
|
|
9
|
+
provider: 'openai',
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
assert.equal(parsed.platformAssignScope, 'self')
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('accepts explicit all-scope delegation without relying on legacy orchestrator flags', () => {
|
|
16
|
+
const parsed = AgentCreateSchema.parse({
|
|
17
|
+
name: 'Coordinator',
|
|
18
|
+
provider: 'openai',
|
|
19
|
+
platformAssignScope: 'all',
|
|
20
|
+
isOrchestrator: false,
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
assert.equal(parsed.platformAssignScope, 'all')
|
|
24
|
+
assert.equal(parsed.isOrchestrator, false)
|
|
25
|
+
})
|
|
26
|
+
})
|
|
@@ -9,11 +9,20 @@ export const AgentCreateSchema = z.object({
|
|
|
9
9
|
credentialId: z.string().nullable().optional().default(null),
|
|
10
10
|
apiEndpoint: z.string().nullable().optional().default(null),
|
|
11
11
|
isOrchestrator: z.boolean().optional().default(false),
|
|
12
|
+
platformAssignScope: z.enum(['self', 'all']).optional().default('self'),
|
|
12
13
|
subAgentIds: z.array(z.string()).optional().default([]),
|
|
13
|
-
|
|
14
|
+
plugins: z.array(z.string()).optional().default([]),
|
|
15
|
+
/** @deprecated Use plugins */
|
|
16
|
+
tools: z.array(z.string()).optional(),
|
|
14
17
|
capabilities: z.array(z.string()).optional().default([]),
|
|
15
18
|
thinkingLevel: z.string().optional(),
|
|
16
19
|
soul: z.string().optional(),
|
|
20
|
+
identityState: z.record(z.string(), z.unknown()).nullable().optional().default(null),
|
|
21
|
+
sessionResetMode: z.enum(['idle', 'daily']).nullable().optional().default(null),
|
|
22
|
+
sessionIdleTimeoutSec: z.number().int().nonnegative().nullable().optional().default(null),
|
|
23
|
+
sessionMaxAgeSec: z.number().int().nonnegative().nullable().optional().default(null),
|
|
24
|
+
sessionDailyResetAt: z.string().nullable().optional().default(null),
|
|
25
|
+
sessionResetTimezone: z.string().nullable().optional().default(null),
|
|
17
26
|
autoRecovery: z.boolean().optional().default(false),
|
|
18
27
|
monthlyBudget: z.number().positive().nullable().optional().default(null),
|
|
19
28
|
dailyBudget: z.number().positive().nullable().optional().default(null),
|
package/src/lib/ws-client.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
type WsCallback = () => void
|
|
2
2
|
|
|
3
3
|
let ws: WebSocket | null = null
|
|
4
|
-
let
|
|
4
|
+
let wsEnabled = false
|
|
5
5
|
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
|
6
6
|
let reconnectDelay = 1000
|
|
7
7
|
const MAX_RECONNECT_DELAY = 30_000
|
|
8
8
|
const listeners = new Map<string, Set<WsCallback>>()
|
|
9
9
|
let connected = false
|
|
10
10
|
|
|
11
|
-
function getWsUrl(
|
|
12
|
-
if (typeof window === 'undefined') return
|
|
11
|
+
function getWsUrl(): string {
|
|
12
|
+
if (typeof window === 'undefined') return 'ws://localhost:3457/ws'
|
|
13
13
|
|
|
14
14
|
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
|
|
15
15
|
const pagePort = window.location.port
|
|
@@ -22,7 +22,7 @@ function getWsUrl(key: string): string {
|
|
|
22
22
|
const behindProxy = !pagePort || pagePort === '80' || pagePort === '443' || pagePort !== appPort
|
|
23
23
|
const wsHost = behindProxy ? window.location.host : `${window.location.hostname}:${buildPort}`
|
|
24
24
|
|
|
25
|
-
return `${protocol}://${wsHost}/ws
|
|
25
|
+
return `${protocol}://${wsHost}/ws`
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
function handleMessage(event: MessageEvent) {
|
|
@@ -44,17 +44,18 @@ function scheduleReconnect() {
|
|
|
44
44
|
const jitter = Math.random() * 2000
|
|
45
45
|
reconnectTimer = setTimeout(() => {
|
|
46
46
|
reconnectTimer = null
|
|
47
|
-
if (!
|
|
48
|
-
connect(
|
|
47
|
+
if (!wsEnabled) return
|
|
48
|
+
connect()
|
|
49
49
|
}, reconnectDelay + jitter)
|
|
50
50
|
reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY)
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
function connect(
|
|
53
|
+
function connect() {
|
|
54
|
+
if (!wsEnabled) return
|
|
54
55
|
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return
|
|
55
56
|
|
|
56
57
|
try {
|
|
57
|
-
ws = new WebSocket(getWsUrl(
|
|
58
|
+
ws = new WebSocket(getWsUrl())
|
|
58
59
|
} catch {
|
|
59
60
|
scheduleReconnect()
|
|
60
61
|
return
|
|
@@ -75,7 +76,7 @@ function connect(key: string) {
|
|
|
75
76
|
ws.onclose = () => {
|
|
76
77
|
connected = false
|
|
77
78
|
ws = null
|
|
78
|
-
if (
|
|
79
|
+
if (wsEnabled) scheduleReconnect()
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
ws.onerror = () => {
|
|
@@ -84,13 +85,14 @@ function connect(key: string) {
|
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
export function connectWs(key: string) {
|
|
87
|
-
|
|
88
|
+
void key
|
|
89
|
+
wsEnabled = true
|
|
88
90
|
reconnectDelay = 1000
|
|
89
|
-
connect(
|
|
91
|
+
connect()
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
export function disconnectWs() {
|
|
93
|
-
|
|
95
|
+
wsEnabled = false
|
|
94
96
|
if (reconnectTimer) {
|
|
95
97
|
clearTimeout(reconnectTimer)
|
|
96
98
|
reconnectTimer = null
|