@nuxtagentic/automations 0.0.0-stage → 0.1.0
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/LICENSE.md +9 -0
- package/README.md +173 -2
- package/client.ts +24 -0
- package/module.ts +55 -0
- package/package.json +59 -3
- package/runtime/components/AutomationCanvas.vue +488 -0
- package/runtime/components/AutomationInspector.vue +436 -0
- package/runtime/components/AutomationNodeIcon.vue +27 -0
- package/runtime/components/AutomationRunsPanel.vue +475 -0
- package/runtime/components/AutomationSidebar.vue +253 -0
- package/runtime/components/AutomationStatusIcon.vue +44 -0
- package/runtime/pages/automations.vue +1181 -0
- package/runtime/server/api/automations/[id]/endpoint.get.ts +12 -0
- package/runtime/server/api/automations/[id]/run.post.ts +30 -0
- package/runtime/server/api/automations/events.post.ts +37 -0
- package/runtime/server/api/automations/runs/[id]/resume.post.ts +23 -0
- package/runtime/server/handlers/automation-hook.post.ts +51 -0
- package/runtime/server/handlers/connect-hook.post.ts +54 -0
- package/runtime/server/handlers/integration-hook.post.ts +86 -0
- package/runtime/server/plugins/events.ts +79 -0
- package/runtime/server/plugins/maintenance.ts +20 -0
- package/runtime/server/utils/agent-runner.ts +15 -0
- package/runtime/server/utils/automation-context.ts +77 -0
- package/runtime/server/utils/automation-engine.ts +697 -0
- package/runtime/server/utils/automation-launch.ts +205 -0
- package/runtime/server/utils/automation-schedule.ts +31 -0
- package/runtime/server/utils/connection-events.ts +114 -0
- package/runtime/server/utils/maintenance.ts +255 -0
- package/runtime/server/utils/schedule-sync.ts +32 -0
- package/runtime/server/utils/transient-retry.ts +34 -0
- package/runtime/server/workflows/runner.ts +39 -0
- package/runtime/shared/agent.ts +5 -0
- package/runtime/shared/automation-run.ts +61 -0
- package/runtime/shared/automation.ts +202 -0
- package/runtime/workspace.ts +56 -0
- package/schema.ts +222 -0
- package/server.ts +2 -0
- package/workflow.ts +2 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineEventHandler, getRequestURL, getRouterParam } from 'h3'
|
|
2
|
+
import { getAutomationContext } from '../../../utils/automation-context'
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
const context = await getAutomationContext(event)
|
|
5
|
+
const automation = await context.api.automation.get(Number(getRouterParam(event, 'id')))
|
|
6
|
+
if (automation.workspace !== context.workspace) {
|
|
7
|
+
throw new Error('Choose an automation in this workspace.')
|
|
8
|
+
}
|
|
9
|
+
return {
|
|
10
|
+
url: `${getRequestURL(event).origin}/api/automation-hooks/${context.ownerId}/${automation.id}/${automation.webhookToken}/${(automation.trigger as { slug: string }).slug}`,
|
|
11
|
+
}
|
|
12
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { readCommandBody } from '@nuxtagentic/integrations/runtime/server/utils/request-body'
|
|
2
|
+
import { defineEventHandler, getRouterParam, setResponseStatus } from 'h3'
|
|
3
|
+
import { assertGoogleMutation } from '@nuxtagentic/google/runtime/server/utils/google'
|
|
4
|
+
import { toActionError } from '@nuxtagentic/integrations/runtime/server/utils/integration-context'
|
|
5
|
+
import { getAutomationContext } from '../../../utils/automation-context'
|
|
6
|
+
import { launchAutomation } from '../../../utils/automation-launch'
|
|
7
|
+
export default defineEventHandler(async (event) => {
|
|
8
|
+
assertGoogleMutation(event)
|
|
9
|
+
try {
|
|
10
|
+
const context = await getAutomationContext(event)
|
|
11
|
+
const automation = await context.api.automation.get(Number(getRouterParam(event, 'id')))
|
|
12
|
+
const body = await readCommandBody<{ payload: unknown; errorPath?: boolean; eventId?: string }>(
|
|
13
|
+
event,
|
|
14
|
+
100000,
|
|
15
|
+
)
|
|
16
|
+
if (body.payload === undefined || JSON.stringify(body.payload).length > 100000) {
|
|
17
|
+
throw new Error('Provide a sample payload up to 100 KB.')
|
|
18
|
+
}
|
|
19
|
+
const result = await launchAutomation(
|
|
20
|
+
event,
|
|
21
|
+
context,
|
|
22
|
+
{ automation, payload: body.payload, eventId: `test:${body.eventId || crypto.randomUUID()}` },
|
|
23
|
+
{ test: true, forceError: !!body.errorPath },
|
|
24
|
+
)
|
|
25
|
+
setResponseStatus(event, 202)
|
|
26
|
+
return result
|
|
27
|
+
} catch (cause) {
|
|
28
|
+
throw toActionError(cause)
|
|
29
|
+
}
|
|
30
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { readCommandBody } from '@nuxtagentic/integrations/runtime/server/utils/request-body'
|
|
2
|
+
import { defineEventHandler, setResponseStatus } from 'h3'
|
|
3
|
+
import { assertGoogleMutation } from '@nuxtagentic/google/runtime/server/utils/google'
|
|
4
|
+
import { toActionError } from '@nuxtagentic/integrations/runtime/server/utils/integration-context'
|
|
5
|
+
import { getAutomationContext } from '../../utils/automation-context'
|
|
6
|
+
import { emitAutomationEvent } from '../../utils/automation-launch'
|
|
7
|
+
import { LOCAL_EVENTS } from '../../../shared/automation'
|
|
8
|
+
import { INTEGRATION_CATALOG } from '@nuxtagentic/integrations/runtime/shared/catalog'
|
|
9
|
+
export default defineEventHandler(async (event) => {
|
|
10
|
+
assertGoogleMutation(event)
|
|
11
|
+
try {
|
|
12
|
+
const context = await getAutomationContext(event)
|
|
13
|
+
const body = await readCommandBody<{ event: string; payload: unknown; id: string }>(
|
|
14
|
+
event,
|
|
15
|
+
100000,
|
|
16
|
+
)
|
|
17
|
+
if (
|
|
18
|
+
![
|
|
19
|
+
...LOCAL_EVENTS.map((entry) => entry.id),
|
|
20
|
+
...INTEGRATION_CATALOG.flatMap((entry) => entry.triggers.map((trigger) => trigger.id)),
|
|
21
|
+
].includes(body.event) ||
|
|
22
|
+
!body.id ||
|
|
23
|
+
body.id.length > 200 ||
|
|
24
|
+
JSON.stringify(body.payload ?? null).length > 100000
|
|
25
|
+
) {
|
|
26
|
+
throw new Error('Provide a supported event, unique id and payload up to 100 KB.')
|
|
27
|
+
}
|
|
28
|
+
setResponseStatus(event, 202)
|
|
29
|
+
return await emitAutomationEvent(event, context, {
|
|
30
|
+
name: body.event,
|
|
31
|
+
payload: body.payload ?? {},
|
|
32
|
+
id: body.id,
|
|
33
|
+
})
|
|
34
|
+
} catch (cause) {
|
|
35
|
+
throw toActionError(cause)
|
|
36
|
+
}
|
|
37
|
+
})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { defineEventHandler, getRouterParam } from 'h3'
|
|
2
|
+
import { assertGoogleMutation } from '@nuxtagentic/google/runtime/server/utils/google'
|
|
3
|
+
import { getAutomationContext } from '../../../../utils/automation-context'
|
|
4
|
+
import { executeAutomation } from '../../../../utils/automation-engine'
|
|
5
|
+
|
|
6
|
+
export default defineEventHandler(async (event) => {
|
|
7
|
+
assertGoogleMutation(event)
|
|
8
|
+
const context = await getAutomationContext(event)
|
|
9
|
+
const run = await context.api.automationRun.get(Number(getRouterParam(event, 'id')))
|
|
10
|
+
if (run.workspace !== context.workspace || run.status !== 'waiting') {
|
|
11
|
+
throw new Error('This run is not waiting in this workspace.')
|
|
12
|
+
}
|
|
13
|
+
if (context.env?.AUTOMATION_RUNNER) {
|
|
14
|
+
throw new Error('Cloudflare resumes this run through its Workflow approval event.')
|
|
15
|
+
}
|
|
16
|
+
event.waitUntil(
|
|
17
|
+
executeAutomation(
|
|
18
|
+
{ runId: run.id, ownerId: context.ownerId, workspace: context.workspace },
|
|
19
|
+
context,
|
|
20
|
+
),
|
|
21
|
+
)
|
|
22
|
+
return { status: 'queued', runId: run.id }
|
|
23
|
+
})
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createError,
|
|
3
|
+
defineEventHandler,
|
|
4
|
+
getHeader,
|
|
5
|
+
getRouterParam,
|
|
6
|
+
setHeader,
|
|
7
|
+
setResponseStatus,
|
|
8
|
+
} from 'h3'
|
|
9
|
+
import { tokenMatches } from '@nuxtagentic/integrations/runtime/server/utils/integration-crypto'
|
|
10
|
+
import { toActionError } from '@nuxtagentic/integrations/runtime/server/utils/integration-context'
|
|
11
|
+
import { getWebhookContext } from '../utils/automation-context'
|
|
12
|
+
import { launchAutomation } from '../utils/automation-launch'
|
|
13
|
+
import { readCommandBody } from '@nuxtagentic/integrations/runtime/server/utils/request-body'
|
|
14
|
+
export default defineEventHandler(async (event) => {
|
|
15
|
+
setHeader(event, 'Cache-Control', 'no-store')
|
|
16
|
+
const ownerId = Number(getRouterParam(event, 'owner'))
|
|
17
|
+
const id = Number(getRouterParam(event, 'id'))
|
|
18
|
+
if (!Number.isSafeInteger(ownerId) || ownerId <= 0 || !Number.isSafeInteger(id) || id <= 0) {
|
|
19
|
+
throw createError({ statusCode: 404, statusMessage: 'Unknown webhook endpoint.' })
|
|
20
|
+
}
|
|
21
|
+
const context = await getWebhookContext(event, ownerId)
|
|
22
|
+
const automation = await context.api.automation.get(id).catch(() => null)
|
|
23
|
+
const trigger = automation?.trigger as { kind: string; slug: string } | undefined
|
|
24
|
+
if (
|
|
25
|
+
!automation ||
|
|
26
|
+
trigger?.kind !== 'webhook' ||
|
|
27
|
+
trigger.slug !== getRouterParam(event, 'slug') ||
|
|
28
|
+
!(await tokenMatches(getRouterParam(event, 'token') ?? '', automation.webhookToken))
|
|
29
|
+
) {
|
|
30
|
+
throw createError({ statusCode: 404, statusMessage: 'Unknown webhook endpoint.' })
|
|
31
|
+
}
|
|
32
|
+
context.workspace = automation.workspace
|
|
33
|
+
try {
|
|
34
|
+
const payload = await readCommandBody<Record<string, unknown>>(event)
|
|
35
|
+
const eventId = getHeader(event, 'Idempotency-Key')
|
|
36
|
+
if (!eventId || eventId.length > 200) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
'Send an Idempotency-Key header with a unique event ID (up to 200 characters).',
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
const result = await launchAutomation(event, context, {
|
|
42
|
+
automation,
|
|
43
|
+
payload,
|
|
44
|
+
eventId: `webhook:${eventId}`,
|
|
45
|
+
})
|
|
46
|
+
setResponseStatus(event, 202)
|
|
47
|
+
return result
|
|
48
|
+
} catch (cause) {
|
|
49
|
+
throw toActionError(cause)
|
|
50
|
+
}
|
|
51
|
+
})
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { defineEventHandler, getRouterParam, setResponseStatus, toWebRequest } from 'h3'
|
|
2
|
+
import { ConnectError } from '@cloudconfig/core/connect'
|
|
3
|
+
import {
|
|
4
|
+
connectOf,
|
|
5
|
+
getDatabaseProvider,
|
|
6
|
+
} from '@nuxtagentic/integrations/runtime/server/utils/integration-context'
|
|
7
|
+
import { useRuntimeConfig } from '#imports'
|
|
8
|
+
import { catalogTrigger, emitConnectionEvent } from '../utils/connection-events'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* App-level vendor webhooks (GitHub App, Slack, Shopify, …) at the URL
|
|
12
|
+
* `cloudconfig connect add` prints. Connect verifies the signature and finds
|
|
13
|
+
* the connections for the vendor account; this starts their automations.
|
|
14
|
+
*/
|
|
15
|
+
export default defineEventHandler(async (event) => {
|
|
16
|
+
const provider = getRouterParam(event, 'provider') ?? ''
|
|
17
|
+
const env = event.context.cloudflare?.env as Record<string, unknown> | undefined
|
|
18
|
+
const settings = useRuntimeConfig(event).cloudconfig as { d1Binding?: string; localDb?: string }
|
|
19
|
+
const database = await getDatabaseProvider(env, settings)
|
|
20
|
+
const connect = await connectOf({
|
|
21
|
+
database,
|
|
22
|
+
env,
|
|
23
|
+
request: new Request('https://events.internal'),
|
|
24
|
+
})
|
|
25
|
+
try {
|
|
26
|
+
const result = await connect.receiveWebhook(provider, toWebRequest(event))
|
|
27
|
+
if (result.response) {
|
|
28
|
+
return result.response
|
|
29
|
+
}
|
|
30
|
+
for (const delivery of result.deliveries) {
|
|
31
|
+
const name = catalogTrigger(provider, delivery.event, delivery.payload)
|
|
32
|
+
if (!name) {
|
|
33
|
+
continue
|
|
34
|
+
}
|
|
35
|
+
await emitConnectionEvent(event, connect, {
|
|
36
|
+
connection: delivery.connection,
|
|
37
|
+
context: { database, env },
|
|
38
|
+
delivery: {
|
|
39
|
+
name,
|
|
40
|
+
payload: delivery.payload,
|
|
41
|
+
id: delivery.deliveryId || crypto.randomUUID(),
|
|
42
|
+
},
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
setResponseStatus(event, 202)
|
|
46
|
+
return { received: result.deliveries.length }
|
|
47
|
+
} catch (cause) {
|
|
48
|
+
if (cause instanceof ConnectError) {
|
|
49
|
+
setResponseStatus(event, cause.status)
|
|
50
|
+
return { error: cause.message }
|
|
51
|
+
}
|
|
52
|
+
throw cause
|
|
53
|
+
}
|
|
54
|
+
})
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createError,
|
|
3
|
+
defineEventHandler,
|
|
4
|
+
getHeader,
|
|
5
|
+
getRouterParam,
|
|
6
|
+
setHeader,
|
|
7
|
+
setResponseStatus,
|
|
8
|
+
toWebRequest,
|
|
9
|
+
} from 'h3'
|
|
10
|
+
import { ConnectError } from '@cloudconfig/core/connect'
|
|
11
|
+
import {
|
|
12
|
+
connectOf,
|
|
13
|
+
getDatabaseProvider,
|
|
14
|
+
toActionError,
|
|
15
|
+
} from '@nuxtagentic/integrations/runtime/server/utils/integration-context'
|
|
16
|
+
import { getIntegrationDefinition } from '@nuxtagentic/integrations/runtime/shared/catalog'
|
|
17
|
+
import { useRuntimeConfig } from '#imports'
|
|
18
|
+
import { catalogTrigger, emitConnectionEvent } from '../utils/connection-events'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Events for one connection, authenticated with that connection's own webhook
|
|
22
|
+
* secret: Connect's signature, a GitHub repository webhook signed with it, or
|
|
23
|
+
* the secret as a bearer token. The secret never appears in the URL.
|
|
24
|
+
*/
|
|
25
|
+
export default defineEventHandler(async (event) => {
|
|
26
|
+
setHeader(event, 'Cache-Control', 'no-store')
|
|
27
|
+
const uid = getRouterParam(event, 'uid') ?? ''
|
|
28
|
+
const env = event.context.cloudflare?.env as Record<string, unknown> | undefined
|
|
29
|
+
const settings = useRuntimeConfig(event).cloudconfig as { d1Binding?: string; localDb?: string }
|
|
30
|
+
const database = await getDatabaseProvider(env, settings)
|
|
31
|
+
const connect = await connectOf({
|
|
32
|
+
database,
|
|
33
|
+
env,
|
|
34
|
+
request: new Request('https://events.internal'),
|
|
35
|
+
})
|
|
36
|
+
let received
|
|
37
|
+
try {
|
|
38
|
+
received = await connect.receiveConnectionEvent(uid, toWebRequest(event))
|
|
39
|
+
} catch (cause) {
|
|
40
|
+
if (cause instanceof ConnectError) {
|
|
41
|
+
throw createError({
|
|
42
|
+
statusCode: cause.status === 401 ? 404 : cause.status,
|
|
43
|
+
statusMessage: 'Unknown event endpoint.',
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
throw cause
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
const body = (received.payload ?? {}) as Record<string, unknown>
|
|
50
|
+
const githubEvent = getHeader(event, 'X-GitHub-Event')
|
|
51
|
+
const name = githubEvent
|
|
52
|
+
? catalogTrigger(
|
|
53
|
+
'github',
|
|
54
|
+
body.action ? `${githubEvent}.${String(body.action)}` : githubEvent,
|
|
55
|
+
body,
|
|
56
|
+
)
|
|
57
|
+
: String(body.event ?? '')
|
|
58
|
+
// GitHub sends every subscribed event; ignore the ones no trigger listens for.
|
|
59
|
+
if (githubEvent && !name) {
|
|
60
|
+
return { runs: [] }
|
|
61
|
+
}
|
|
62
|
+
const provider = name.split('.')[0] ?? ''
|
|
63
|
+
if (!getIntegrationDefinition(provider)?.triggers.some((entry) => entry.id === name)) {
|
|
64
|
+
throw new Error('Choose an event supported by this integration.')
|
|
65
|
+
}
|
|
66
|
+
const delivery =
|
|
67
|
+
getHeader(event, 'X-GitHub-Delivery') ??
|
|
68
|
+
getHeader(event, 'Idempotency-Key') ??
|
|
69
|
+
String(body.id ?? '')
|
|
70
|
+
if (!delivery || delivery.length > 200) {
|
|
71
|
+
throw new Error('Send a unique event id or Idempotency-Key header, up to 200 characters.')
|
|
72
|
+
}
|
|
73
|
+
setResponseStatus(event, 202)
|
|
74
|
+
return await emitConnectionEvent(event, connect, {
|
|
75
|
+
connection: received.connection,
|
|
76
|
+
context: { database, env },
|
|
77
|
+
delivery: {
|
|
78
|
+
name,
|
|
79
|
+
payload: githubEvent ? body : (body.payload ?? {}),
|
|
80
|
+
id: delivery,
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
} catch (cause) {
|
|
84
|
+
throw toActionError(cause)
|
|
85
|
+
}
|
|
86
|
+
})
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { defineNitroPlugin } from 'nitropack/runtime'
|
|
2
|
+
import { getRequestURL, getResponseStatus, readBody } from 'h3'
|
|
3
|
+
import { getAutomationContext } from '../utils/automation-context'
|
|
4
|
+
import { emitAutomationEvent } from '../utils/automation-launch'
|
|
5
|
+
import { readJson } from '@nuxtagentic/integrations/runtime/server/utils/integration-adapters'
|
|
6
|
+
|
|
7
|
+
/** Successful API calls that raise a built-in automation event. */
|
|
8
|
+
const automationEventRoutes = [
|
|
9
|
+
{ method: 'POST', path: /\/auth\/manage\/users$/, event: 'user.created' },
|
|
10
|
+
{ method: 'POST', path: /\/auth\/ban-user$/, event: 'user.deactivated' },
|
|
11
|
+
{ method: 'POST', path: /\/(?:files|file-content)$/, event: 'file.uploaded' },
|
|
12
|
+
{ method: 'PATCH', path: /\/kanbanCards\/\d+$/, event: 'task.moved' },
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
async function readResponseBody(body: unknown) {
|
|
16
|
+
if (!(body instanceof Response)) {
|
|
17
|
+
return body
|
|
18
|
+
}
|
|
19
|
+
return body.ok ? readJson(body.clone()) : null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function eventPayload(name: string, row: Record<string, unknown>) {
|
|
23
|
+
if (name.startsWith('user.')) {
|
|
24
|
+
return { user: row.user ?? row }
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
record: { model: name === 'task.moved' ? 'kanbanCard' : 'file', id: row.id },
|
|
28
|
+
data: row,
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default defineNitroPlugin((nitro) => {
|
|
33
|
+
nitro.hooks.hook('beforeResponse', async (event, response) => {
|
|
34
|
+
const path = getRequestURL(event).pathname
|
|
35
|
+
const name = automationEventRoutes.find(
|
|
36
|
+
(route) => route.method === event.method && route.path.test(path),
|
|
37
|
+
)?.event
|
|
38
|
+
if (!name) {
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
const body = await readResponseBody(response.body)
|
|
42
|
+
if (!body || getResponseStatus(event) >= 400) {
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
const row = body as Record<string, unknown>
|
|
46
|
+
if (name === 'task.moved') {
|
|
47
|
+
const input = await readBody<Record<string, unknown>>(event)
|
|
48
|
+
if (!Object.hasOwn(input ?? {}, 'status')) {
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
event.context.automationEvent = {
|
|
53
|
+
name,
|
|
54
|
+
payload: eventPayload(name, row),
|
|
55
|
+
id: crypto.randomUUID(),
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
nitro.hooks.hook('afterResponse', (event) => {
|
|
59
|
+
const candidate = event.context.automationEvent as
|
|
60
|
+
{ name: string; payload: unknown; id: string } | undefined
|
|
61
|
+
if (!candidate) {
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
event.waitUntil(
|
|
65
|
+
(async () => {
|
|
66
|
+
const context = await getAutomationContext(event)
|
|
67
|
+
await emitAutomationEvent(event, context, {
|
|
68
|
+
name: candidate.name,
|
|
69
|
+
payload: candidate.payload,
|
|
70
|
+
id: candidate.id,
|
|
71
|
+
})
|
|
72
|
+
})().catch(() => {
|
|
73
|
+
console.warn(
|
|
74
|
+
'[automations] Could not dispatch a workspace event. Check the automation configuration.',
|
|
75
|
+
)
|
|
76
|
+
}),
|
|
77
|
+
)
|
|
78
|
+
})
|
|
79
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { defineNitroPlugin, useRuntimeConfig } from 'nitropack/runtime'
|
|
2
|
+
import { runAutomationMaintenance, type MaintenanceDatabase } from '../utils/maintenance'
|
|
3
|
+
|
|
4
|
+
/** Runs on the Worker's cron trigger; see `triggers.crons` in wrangler.jsonc. */
|
|
5
|
+
export default defineNitroPlugin((nitro) => {
|
|
6
|
+
nitro.hooks.hook('cloudflare:scheduled', async ({ env }) => {
|
|
7
|
+
const bindings = env as Record<string, unknown>
|
|
8
|
+
const settings = useRuntimeConfig().cloudconfig as { d1Binding?: string } | undefined
|
|
9
|
+
const database = bindings[settings?.d1Binding ?? 'DB'] as MaintenanceDatabase | undefined
|
|
10
|
+
if (!database) {
|
|
11
|
+
console.error('[automations] Maintenance needs the D1 binding; it was skipped.')
|
|
12
|
+
return
|
|
13
|
+
}
|
|
14
|
+
const retentionDays = Number(bindings.NUXT_AUTOMATIONS_RUN_RETENTION_DAYS ?? 0)
|
|
15
|
+
const report = await runAutomationMaintenance(bindings, database, { retentionDays })
|
|
16
|
+
if (Object.values(report).some(Boolean)) {
|
|
17
|
+
console.log('[automations] Maintenance', JSON.stringify(report))
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
runSavedAgent as runApiAgent,
|
|
3
|
+
type AgentInvocation,
|
|
4
|
+
} from '@cloudconfig/core/agents/runtime'
|
|
5
|
+
import { createRetryingModelFetch } from '@nuxtagentic/agents/runtime/server/utils/model-fetch'
|
|
6
|
+
export type { RunnableTool, AgentProviderKeys } from '@cloudconfig/core/agents/runtime'
|
|
7
|
+
|
|
8
|
+
export async function runSavedAgent(options: AgentInvocation) {
|
|
9
|
+
if (import.meta.dev && options.agent.runtime && options.agent.runtime !== 'api') {
|
|
10
|
+
return (
|
|
11
|
+
await import('@nuxtagentic/agents/runtime/server/utils/run-local-agent')
|
|
12
|
+
).runLocalAgent(options)
|
|
13
|
+
}
|
|
14
|
+
return runApiAgent({ ...options, fetch: createRetryingModelFetch(options.fetch) })
|
|
15
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { useCloudConfigServer } from '#cloudconfig/server'
|
|
2
|
+
import { useRuntimeConfig } from '#imports'
|
|
3
|
+
import type { H3Event } from 'h3'
|
|
4
|
+
import { createServerClient } from '@cloudconfig/core/server'
|
|
5
|
+
import type { AgentsConfig } from '@cloudconfig/core/agents'
|
|
6
|
+
import {
|
|
7
|
+
getAgentProviderKeys,
|
|
8
|
+
getAgentProviderKeysForEvent,
|
|
9
|
+
} from '@nuxtagentic/agents/runtime/server/utils/agent-provider-keys'
|
|
10
|
+
import {
|
|
11
|
+
backendConfig,
|
|
12
|
+
getDatabaseProvider,
|
|
13
|
+
getIntegrationContext,
|
|
14
|
+
} from '@nuxtagentic/integrations/runtime/server/utils/integration-context'
|
|
15
|
+
import type { IntegrationsConfig } from '@nuxtagentic/integrations/schema'
|
|
16
|
+
import type { WorkspaceConfig } from '@nuxtagentic/workspace/schema'
|
|
17
|
+
import type { AutomationsConfig } from '../../../schema'
|
|
18
|
+
import type { AgentProviderKeys } from './agent-runner'
|
|
19
|
+
|
|
20
|
+
export type AutomationBackend = {
|
|
21
|
+
models: AutomationsConfig['models'] &
|
|
22
|
+
IntegrationsConfig['models'] &
|
|
23
|
+
AgentsConfig['models'] &
|
|
24
|
+
WorkspaceConfig['models']
|
|
25
|
+
}
|
|
26
|
+
export interface AutomationJob {
|
|
27
|
+
ownerId: number
|
|
28
|
+
runId: number
|
|
29
|
+
workspace: string
|
|
30
|
+
forceError?: boolean
|
|
31
|
+
}
|
|
32
|
+
/** Who an automation acts as: its owner (their and the workspace's connections) or only the workspace. */
|
|
33
|
+
export type AutomationRunAs = 'owner' | 'workspace'
|
|
34
|
+
export function providerKeys(values: Record<string, unknown>): AgentProviderKeys {
|
|
35
|
+
return getAgentProviderKeys(values)
|
|
36
|
+
}
|
|
37
|
+
export async function getAutomationContext(event: H3Event) {
|
|
38
|
+
const context = await getIntegrationContext(event)
|
|
39
|
+
const api = await useCloudConfigServer<AutomationBackend>(event)
|
|
40
|
+
const keys = getAgentProviderKeysForEvent(event)
|
|
41
|
+
return { ...context, api, keys }
|
|
42
|
+
}
|
|
43
|
+
export async function createJobContext(
|
|
44
|
+
job: AutomationJob,
|
|
45
|
+
env: Record<string, unknown> | undefined,
|
|
46
|
+
options: {
|
|
47
|
+
keys?: AgentProviderKeys
|
|
48
|
+
localDb?: string
|
|
49
|
+
d1Binding?: string
|
|
50
|
+
runAs?: AutomationRunAs
|
|
51
|
+
} = {},
|
|
52
|
+
) {
|
|
53
|
+
const database = await getDatabaseProvider(env, options)
|
|
54
|
+
const request = new Request('https://automation.internal')
|
|
55
|
+
const api = createServerClient(
|
|
56
|
+
backendConfig as AutomationBackend,
|
|
57
|
+
{ request, env },
|
|
58
|
+
{ db: database, getUser: () => ({ id: job.ownerId }) },
|
|
59
|
+
)
|
|
60
|
+
return {
|
|
61
|
+
api,
|
|
62
|
+
database,
|
|
63
|
+
request,
|
|
64
|
+
env,
|
|
65
|
+
ownerId: job.ownerId,
|
|
66
|
+
workspace: job.workspace,
|
|
67
|
+
...(options.runAs ? { runAs: options.runAs } : {}),
|
|
68
|
+
keys: options.keys ?? providerKeys({ ...process.env, ...env }),
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export async function getWebhookContext(event: H3Event, ownerId: number) {
|
|
72
|
+
const runtime = useRuntimeConfig(event)
|
|
73
|
+
const env = event.context.cloudflare?.env as Record<string, unknown> | undefined
|
|
74
|
+
const settings = runtime.cloudconfig as { d1Binding?: string; localDb?: string }
|
|
75
|
+
const keys = getAgentProviderKeysForEvent(event)
|
|
76
|
+
return createJobContext({ ownerId, workspace: '', runId: 0 }, env, { ...settings, keys })
|
|
77
|
+
}
|