@frockbot/plugin-routines 0.3.45 → 0.3.47
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/package.json +11 -9
- package/src/agent.ts +65 -11
- package/src/backend.ts +26 -2
- package/src/client/RoutinesSection.vue +266 -13
- package/src/client/RoutinesSummary.vue +3 -1
- package/src/client/index.ts +18 -0
- package/src/client/state.ts +3 -0
- package/src/records.ts +29 -4
- package/src/scheduler.ts +6 -0
- package/src/shared.ts +35 -1
- package/src/store.ts +75 -26
- package/src/subscriptions.test.ts +159 -0
- package/src/subscriptions.ts +185 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/plugin-routines",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.47",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
"./shared": "./src/shared.ts",
|
|
22
22
|
"./storage-keys": "./src/storage-keys.ts",
|
|
23
23
|
"./store": "./src/store.ts",
|
|
24
|
-
"./testing": "./src/testing.ts"
|
|
24
|
+
"./testing": "./src/testing.ts",
|
|
25
|
+
"./subscriptions": "./src/subscriptions.ts"
|
|
25
26
|
},
|
|
26
27
|
"frockbot": {
|
|
27
28
|
"manifest": "./frockbot.json"
|
|
@@ -32,15 +33,16 @@
|
|
|
32
33
|
"typecheck": "vue-tsc --noEmit -p tsconfig.json"
|
|
33
34
|
},
|
|
34
35
|
"dependencies": {
|
|
35
|
-
"@frockbot/client-core": "0.3.
|
|
36
|
-
"@frockbot/client-ui": "0.3.
|
|
37
|
-
"@frockbot/configuration-core": "0.3.
|
|
38
|
-
"@frockbot/kernel-agent-loop": "0.3.
|
|
39
|
-
"@frockbot/kernel-contracts": "0.3.
|
|
40
|
-
"@frockbot/plugin-shell": "0.3.
|
|
36
|
+
"@frockbot/client-core": "0.3.47",
|
|
37
|
+
"@frockbot/client-ui": "0.3.47",
|
|
38
|
+
"@frockbot/configuration-core": "0.3.47",
|
|
39
|
+
"@frockbot/kernel-agent-loop": "0.3.47",
|
|
40
|
+
"@frockbot/kernel-contracts": "0.3.47",
|
|
41
|
+
"@frockbot/plugin-shell": "0.3.47",
|
|
41
42
|
"cordis": "4.0.0-rc.8",
|
|
42
43
|
"croner": "10.0.1",
|
|
43
|
-
"vue": "3.5.41"
|
|
44
|
+
"vue": "3.5.41",
|
|
45
|
+
"@frockbot/connection-core": "0.3.47"
|
|
44
46
|
},
|
|
45
47
|
"devDependencies": {
|
|
46
48
|
"@types/bun": "1.4.0",
|
package/src/agent.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
decodeConnectionTriggerV1,
|
|
3
|
+
type ConnectionTriggerV1,
|
|
4
|
+
type ConnectionTriggerCatalogV1,
|
|
5
|
+
} from "@frockbot/connection-core";
|
|
1
6
|
// The Routines runtime Contribution: one tool, `routine_manage`.
|
|
2
7
|
//
|
|
3
8
|
// GrokBot's `update_state target=routine {create,update,pause,resume,delete}`
|
|
@@ -50,6 +55,7 @@ export interface RoutinesRuntimeHostV1 {
|
|
|
50
55
|
botId: string;
|
|
51
56
|
writer?: RoutineWriterIdentityV1;
|
|
52
57
|
list(): Promise<RoutineListViewV1>;
|
|
58
|
+
listTriggers?(): Promise<ConnectionTriggerCatalogV1>;
|
|
53
59
|
execute(
|
|
54
60
|
command: RoutineCommandV1,
|
|
55
61
|
writer: RoutineWriterV1,
|
|
@@ -63,6 +69,7 @@ export const ROUTINE_MANAGE_ACTIONS = [
|
|
|
63
69
|
"resume",
|
|
64
70
|
"delete",
|
|
65
71
|
"run_now",
|
|
72
|
+
"list_triggers",
|
|
66
73
|
] as const;
|
|
67
74
|
|
|
68
75
|
export type RoutineManageActionV1 = (typeof ROUTINE_MANAGE_ACTIONS)[number];
|
|
@@ -92,10 +99,28 @@ const ROUTINE_MANAGE_INPUT_SCHEMA = {
|
|
|
92
99
|
"A five-field cron expression, or @hourly, @daily, @weekly, @monthly, or @every 15m. Optionally prefixed with CRON_TZ=<zone>. A Routine has a schedule or a webhook trigger, never both.",
|
|
93
100
|
},
|
|
94
101
|
trigger: {
|
|
95
|
-
|
|
96
|
-
|
|
102
|
+
oneOf: [
|
|
103
|
+
{ type: "string", enum: ["webhook"] },
|
|
104
|
+
{
|
|
105
|
+
type: "object",
|
|
106
|
+
properties: {
|
|
107
|
+
composio: {
|
|
108
|
+
type: "object",
|
|
109
|
+
properties: {
|
|
110
|
+
connectionId: { type: "string" },
|
|
111
|
+
triggerType: { type: "string" },
|
|
112
|
+
config: { type: "object" },
|
|
113
|
+
},
|
|
114
|
+
required: ["connectionId", "triggerType", "config"],
|
|
115
|
+
additionalProperties: false,
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
required: ["composio"],
|
|
119
|
+
additionalProperties: false,
|
|
120
|
+
},
|
|
121
|
+
],
|
|
97
122
|
description:
|
|
98
|
-
"Fire on
|
|
123
|
+
"Fire on an event from an existing connected account. Use list_triggers first to find the account, event type and configuration schema. A Routine has a schedule or a trigger, never both.",
|
|
99
124
|
},
|
|
100
125
|
timezone: {
|
|
101
126
|
type: "string",
|
|
@@ -125,7 +150,7 @@ interface RoutineManageInputV1 {
|
|
|
125
150
|
name?: string;
|
|
126
151
|
prompt?: string;
|
|
127
152
|
schedule?: string;
|
|
128
|
-
trigger?: "webhook";
|
|
153
|
+
trigger?: "webhook" | { composio: ConnectionTriggerV1 };
|
|
129
154
|
timezone?: string;
|
|
130
155
|
userAsked?: boolean;
|
|
131
156
|
}
|
|
@@ -164,8 +189,18 @@ function decodeRoutineManageInputV1(input: unknown): RoutineManageInputV1 {
|
|
|
164
189
|
}
|
|
165
190
|
return candidate;
|
|
166
191
|
};
|
|
167
|
-
|
|
168
|
-
|
|
192
|
+
let trigger: RoutineManageInputV1["trigger"];
|
|
193
|
+
if (value.trigger === "webhook") trigger = "webhook";
|
|
194
|
+
else if (value.trigger !== undefined) {
|
|
195
|
+
if (
|
|
196
|
+
!value.trigger ||
|
|
197
|
+
typeof value.trigger !== "object" ||
|
|
198
|
+
Array.isArray(value.trigger) ||
|
|
199
|
+
Object.keys(value.trigger).some((key) => key !== "composio") ||
|
|
200
|
+
!("composio" in value.trigger)
|
|
201
|
+
)
|
|
202
|
+
throw new RoutineDecodeError("Choose a service event or webhook trigger");
|
|
203
|
+
trigger = { composio: decodeConnectionTriggerV1(value.trigger.composio) };
|
|
169
204
|
}
|
|
170
205
|
if (value.userAsked !== undefined && typeof value.userAsked !== "boolean") {
|
|
171
206
|
throw new RoutineDecodeError("routine_manage userAsked must be a boolean");
|
|
@@ -182,7 +217,7 @@ function decodeRoutineManageInputV1(input: unknown): RoutineManageInputV1 {
|
|
|
182
217
|
...(optional("schedule") === undefined
|
|
183
218
|
? {}
|
|
184
219
|
: { schedule: optional("schedule")! }),
|
|
185
|
-
...(
|
|
220
|
+
...(trigger === undefined ? {} : { trigger }),
|
|
186
221
|
...(optional("timezone") === undefined
|
|
187
222
|
? {}
|
|
188
223
|
: { timezone: optional("timezone")! }),
|
|
@@ -228,7 +263,12 @@ export function routineManageCommandV1(
|
|
|
228
263
|
...(input.schedule === undefined ? {} : { schedule: input.schedule }),
|
|
229
264
|
...(input.trigger === undefined
|
|
230
265
|
? {}
|
|
231
|
-
: {
|
|
266
|
+
: {
|
|
267
|
+
trigger:
|
|
268
|
+
input.trigger === "webhook"
|
|
269
|
+
? { kind: "webhook" }
|
|
270
|
+
: { kind: "connection", ...input.trigger.composio },
|
|
271
|
+
}),
|
|
232
272
|
...(input.timezone === undefined ? {} : { timezone: input.timezone }),
|
|
233
273
|
});
|
|
234
274
|
}
|
|
@@ -247,7 +287,12 @@ export function routineManageCommandV1(
|
|
|
247
287
|
...(input.schedule === undefined ? {} : { schedule: input.schedule }),
|
|
248
288
|
...(input.trigger === undefined
|
|
249
289
|
? {}
|
|
250
|
-
: {
|
|
290
|
+
: {
|
|
291
|
+
trigger:
|
|
292
|
+
input.trigger === "webhook"
|
|
293
|
+
? { kind: "webhook" }
|
|
294
|
+
: { kind: "connection", ...input.trigger.composio },
|
|
295
|
+
}),
|
|
251
296
|
...(input.timezone === undefined ? {} : { timezone: input.timezone }),
|
|
252
297
|
});
|
|
253
298
|
}
|
|
@@ -295,7 +340,7 @@ export function createRoutineManageTool(
|
|
|
295
340
|
// video roles. See `@frockbot/plugin-subagents` `SUBAGENT_TOOL_REACH_V1`.
|
|
296
341
|
admission: { subagentRoles: ["executor"] },
|
|
297
342
|
description: [
|
|
298
|
-
"Create, edit, pause, resume, delete, or immediately run one of your own Routines.",
|
|
343
|
+
"Create, edit, pause, resume, delete, or immediately run one of your own Routines. Use list_triggers to list events and configuration schemas on the User’s existing connected accounts.",
|
|
299
344
|
"A Routine is a standing instruction that fires on a schedule or on a delivered webhook,",
|
|
300
345
|
`as its own Turn rather than inside this conversation. Names are at most ${ROUTINE_NAME_MAX_LENGTH}`,
|
|
301
346
|
`characters and prompts at most ${ROUTINE_PROMPT_MAX_LENGTH}.`,
|
|
@@ -322,6 +367,13 @@ export function createRoutineManageTool(
|
|
|
322
367
|
let command: RoutineCommandV1;
|
|
323
368
|
try {
|
|
324
369
|
decoded = decodeRoutineManageInputV1(input);
|
|
370
|
+
if (decoded.action === "list_triggers")
|
|
371
|
+
return {
|
|
372
|
+
content: JSON.stringify(
|
|
373
|
+
(await host.listTriggers?.()) ?? { schemaVersion: 1, items: [] },
|
|
374
|
+
),
|
|
375
|
+
isError: false,
|
|
376
|
+
};
|
|
325
377
|
command = routineManageCommandV1(decoded, {
|
|
326
378
|
botId: host.botId,
|
|
327
379
|
commandId: routineToolCommandIdV1(context.effectId),
|
|
@@ -374,7 +426,9 @@ export function createRoutineManageTool(
|
|
|
374
426
|
const routine = receipt.routine;
|
|
375
427
|
const timing = routine.schedule
|
|
376
428
|
? `schedule ${routine.schedule} (${routine.timezone})`
|
|
377
|
-
:
|
|
429
|
+
: routine.trigger?.kind === "connection"
|
|
430
|
+
? (routine.eventName ?? "a service event")
|
|
431
|
+
: "webhook trigger";
|
|
378
432
|
return {
|
|
379
433
|
content: [
|
|
380
434
|
`Routine "${routine.name}" (${routine.routineId}) is ${
|
package/src/backend.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
decodeConnectionTriggerCatalogV1,
|
|
3
|
+
type ConnectionTriggerCatalogV1,
|
|
4
|
+
} from "@frockbot/connection-core";
|
|
1
5
|
// The Routines gateway Contribution: the authenticated HTTP surface.
|
|
2
6
|
//
|
|
3
7
|
// Three routes, all Bot-scoped and all beside `/api/bots/:id/settings`:
|
|
@@ -73,6 +77,10 @@ export interface RoutinesGatewayHost {
|
|
|
73
77
|
},
|
|
74
78
|
): Promise<RoutineHookDeliveryReceiptV1>;
|
|
75
79
|
listRoutines(userId: string, botId: string): Promise<RoutineListViewV1>;
|
|
80
|
+
listTriggers?(
|
|
81
|
+
userId: string,
|
|
82
|
+
botId: string,
|
|
83
|
+
): Promise<ConnectionTriggerCatalogV1>;
|
|
76
84
|
executeRoutineCommand(
|
|
77
85
|
userId: string,
|
|
78
86
|
botId: string,
|
|
@@ -117,6 +125,7 @@ export interface RoutinesBackendRouteContribution {
|
|
|
117
125
|
}
|
|
118
126
|
|
|
119
127
|
const ROUTINES = /^\/api\/bots\/([^/]+)\/routines$/;
|
|
128
|
+
const ROUTINE_TRIGGERS = /^\/api\/bots\/([^/]+)\/routines\/triggers$/;
|
|
120
129
|
const ROUTINE_INBOX = /^\/api\/bots\/([^/]+)\/routines\/inbox$/;
|
|
121
130
|
const ROUTINE_RUNS = /^\/api\/bots\/([^/]+)\/routines\/([^/]+)\/runs$/;
|
|
122
131
|
const ROUTINE_HOOK = /^\/api\/bots\/([^/]+)\/routines\/([^/]+)\/hook$/;
|
|
@@ -270,16 +279,31 @@ export function createRoutinesBackendContribution(
|
|
|
270
279
|
packageId: "routines",
|
|
271
280
|
async route(request, url, context) {
|
|
272
281
|
if (!context.userId) return undefined;
|
|
282
|
+
const triggers = ROUTINE_TRIGGERS.exec(url.pathname);
|
|
273
283
|
const list = ROUTINES.exec(url.pathname);
|
|
274
284
|
const inbox = ROUTINE_INBOX.exec(url.pathname);
|
|
275
285
|
const runs = ROUTINE_RUNS.exec(url.pathname);
|
|
276
286
|
const run = ROUTINE_RUN.exec(url.pathname);
|
|
277
|
-
if (!list && !inbox && !runs && !run) return undefined;
|
|
287
|
+
if (!triggers && !list && !inbox && !runs && !run) return undefined;
|
|
278
288
|
if ([...url.searchParams.keys()].length > 0) {
|
|
279
289
|
return jsonError(400, "Routine routes take no query parameters");
|
|
280
290
|
}
|
|
281
291
|
try {
|
|
282
|
-
const botId = pathSegment(
|
|
292
|
+
const botId = pathSegment(
|
|
293
|
+
(triggers ?? list ?? inbox ?? runs ?? run)![1]!,
|
|
294
|
+
);
|
|
295
|
+
if (triggers) {
|
|
296
|
+
if (request.method !== "GET")
|
|
297
|
+
return jsonError(405, "method not allowed");
|
|
298
|
+
return Response.json(
|
|
299
|
+
decodeConnectionTriggerCatalogV1(
|
|
300
|
+
(await host.listTriggers?.(context.userId, botId)) ?? {
|
|
301
|
+
schemaVersion: 1,
|
|
302
|
+
items: [],
|
|
303
|
+
},
|
|
304
|
+
),
|
|
305
|
+
);
|
|
306
|
+
}
|
|
283
307
|
if (run) {
|
|
284
308
|
if (request.method !== "GET") {
|
|
285
309
|
return jsonError(405, "method not allowed");
|
|
@@ -18,7 +18,8 @@ import {
|
|
|
18
18
|
} from "@frockbot/client-core";
|
|
19
19
|
import { frockBotWebDataKey } from "@frockbot/plugin-shell/shared";
|
|
20
20
|
import { settingsLinkV1 } from "@frockbot/plugin-shell/settings-links";
|
|
21
|
-
import { computed, inject, reactive, ref, watch } from "vue";
|
|
21
|
+
import { computed, inject, reactive, ref, toRaw, watch } from "vue";
|
|
22
|
+
import { triggerObject } from "@frockbot/connection-core";
|
|
22
23
|
import type { RoutineViewV1 } from "../shared.js";
|
|
23
24
|
import { routinesStateKey } from "./state.js";
|
|
24
25
|
|
|
@@ -56,11 +57,83 @@ const form = reactive({
|
|
|
56
57
|
routineId: undefined as string | undefined,
|
|
57
58
|
name: "",
|
|
58
59
|
prompt: "",
|
|
59
|
-
timing: "schedule" as "schedule" | "webhook",
|
|
60
|
+
timing: "schedule" as "schedule" | "webhook" | "connection",
|
|
60
61
|
schedule: "",
|
|
62
|
+
connectionId: "",
|
|
63
|
+
triggerType: "",
|
|
64
|
+
config: {} as Record<string, unknown>,
|
|
61
65
|
timezone: "UTC",
|
|
62
66
|
});
|
|
63
67
|
|
|
68
|
+
const accounts = computed(() => [
|
|
69
|
+
...new Map(
|
|
70
|
+
(routines.value.triggers ?? []).map((item) => [item.connectionId, item]),
|
|
71
|
+
).values(),
|
|
72
|
+
]);
|
|
73
|
+
const events = computed(() =>
|
|
74
|
+
(routines.value.triggers ?? []).filter(
|
|
75
|
+
(item) => item.connectionId === form.connectionId,
|
|
76
|
+
),
|
|
77
|
+
);
|
|
78
|
+
const selectedEvent = computed(() =>
|
|
79
|
+
events.value.find((item) => item.triggerType === form.triggerType),
|
|
80
|
+
);
|
|
81
|
+
const eventFields = computed(() => {
|
|
82
|
+
const schema = selectedEvent.value?.configSchema;
|
|
83
|
+
if (!schema || !triggerObject(schema.properties)) return [];
|
|
84
|
+
return Object.entries(schema.properties).flatMap(([key, value]) =>
|
|
85
|
+
triggerObject(value)
|
|
86
|
+
? [
|
|
87
|
+
{
|
|
88
|
+
key,
|
|
89
|
+
label: (typeof value.title === "string" ? value.title : key)
|
|
90
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
91
|
+
.replaceAll("_", " ")
|
|
92
|
+
.replace(/^./, (letter) => letter.toUpperCase()),
|
|
93
|
+
hint:
|
|
94
|
+
typeof value.description === "string"
|
|
95
|
+
? value.description
|
|
96
|
+
: undefined,
|
|
97
|
+
type: value.type,
|
|
98
|
+
options: Array.isArray(value.enum)
|
|
99
|
+
? value.enum.filter(
|
|
100
|
+
(item): item is string => typeof item === "string",
|
|
101
|
+
)
|
|
102
|
+
: [],
|
|
103
|
+
required:
|
|
104
|
+
Array.isArray(schema.required) && schema.required.includes(key),
|
|
105
|
+
},
|
|
106
|
+
]
|
|
107
|
+
: [],
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
function selectEvent(): void {
|
|
111
|
+
form.config = {};
|
|
112
|
+
const properties = selectedEvent.value?.configSchema.properties;
|
|
113
|
+
if (triggerObject(properties))
|
|
114
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
115
|
+
if (triggerObject(value) && value.default !== undefined)
|
|
116
|
+
form.config[key] = structuredClone(toRaw(value.default));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function selectAccount(): void {
|
|
120
|
+
form.triggerType = events.value[0]?.triggerType ?? "";
|
|
121
|
+
selectEvent();
|
|
122
|
+
}
|
|
123
|
+
function eventState(routine: RoutineViewV1): string {
|
|
124
|
+
return (
|
|
125
|
+
{
|
|
126
|
+
active: "Listening",
|
|
127
|
+
starting: "Starting…",
|
|
128
|
+
paused: "Paused",
|
|
129
|
+
missing: "Listening stopped — edit and save to restart",
|
|
130
|
+
failed: "Could not start listening — review the event settings",
|
|
131
|
+
unavailable: "Connection unavailable — check Connectors",
|
|
132
|
+
}[routine.eventStatus ?? "unavailable"] ??
|
|
133
|
+
"Connection unavailable — check Connectors"
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
64
137
|
/**
|
|
65
138
|
* The reason the last save was refused, held beside the form rather than in
|
|
66
139
|
* the section header. The header sits above every Routine card, so on a Bot
|
|
@@ -125,7 +198,9 @@ async function copyHook(): Promise<void> {
|
|
|
125
198
|
function summary(routine: RoutineViewV1): string {
|
|
126
199
|
return routine.schedule
|
|
127
200
|
? `${routine.schedule} · ${routine.timezone}`
|
|
128
|
-
:
|
|
201
|
+
: routine.trigger?.kind === "connection"
|
|
202
|
+
? (routine.eventName ?? "Connected account event")
|
|
203
|
+
: "Webhook trigger";
|
|
129
204
|
}
|
|
130
205
|
|
|
131
206
|
/** A durable moment, read in the Routine's own zone — the one it fires on. */
|
|
@@ -134,10 +209,13 @@ function moment(routine: RoutineViewV1, iso: string): string {
|
|
|
134
209
|
}
|
|
135
210
|
|
|
136
211
|
function startCreate(): void {
|
|
212
|
+
if (botId.value) void routines.value.load(botId.value);
|
|
137
213
|
form.routineId = undefined;
|
|
138
214
|
form.name = "";
|
|
139
215
|
form.prompt = "";
|
|
140
216
|
form.timing = "schedule";
|
|
217
|
+
form.connectionId = accounts.value[0]?.connectionId ?? "";
|
|
218
|
+
selectAccount();
|
|
141
219
|
form.schedule = "0 9 * * *";
|
|
142
220
|
// The reader's own zone, not UTC: a schedule is almost always meant in the
|
|
143
221
|
// day the person writing it is living in, and the Bot picks the same when it
|
|
@@ -151,7 +229,16 @@ function startEdit(routine: RoutineViewV1): void {
|
|
|
151
229
|
form.routineId = routine.routineId;
|
|
152
230
|
form.name = routine.name;
|
|
153
231
|
form.prompt = routine.prompt;
|
|
154
|
-
form.timing = routine.schedule
|
|
232
|
+
form.timing = routine.schedule
|
|
233
|
+
? "schedule"
|
|
234
|
+
: routine.trigger?.kind === "connection"
|
|
235
|
+
? "connection"
|
|
236
|
+
: "webhook";
|
|
237
|
+
if (routine.trigger?.kind === "connection") {
|
|
238
|
+
form.connectionId = routine.trigger.connectionId;
|
|
239
|
+
form.triggerType = routine.trigger.triggerType;
|
|
240
|
+
form.config = structuredClone(toRaw(routine.trigger.config));
|
|
241
|
+
}
|
|
155
242
|
form.schedule = routine.schedule ?? "";
|
|
156
243
|
form.timezone = routine.timezone;
|
|
157
244
|
saveError.value = undefined;
|
|
@@ -163,13 +250,46 @@ async function submit(): Promise<void> {
|
|
|
163
250
|
if (!id) return;
|
|
164
251
|
saveError.value = undefined;
|
|
165
252
|
try {
|
|
253
|
+
if (form.timing === "connection" && !selectedEvent.value)
|
|
254
|
+
throw new Error("Choose an available account and event");
|
|
255
|
+
const config = Object.fromEntries(
|
|
256
|
+
Object.entries(form.config).filter(
|
|
257
|
+
([, value]) => value !== "" && value !== undefined,
|
|
258
|
+
),
|
|
259
|
+
);
|
|
260
|
+
for (const field of eventFields.value) {
|
|
261
|
+
if (field.required && config[field.key] === undefined)
|
|
262
|
+
throw new Error(`Enter ${field.label.toLowerCase()}`);
|
|
263
|
+
if (
|
|
264
|
+
config[field.key] !== undefined &&
|
|
265
|
+
(field.type === "array" || field.type === "object") &&
|
|
266
|
+
typeof config[field.key] === "string"
|
|
267
|
+
) {
|
|
268
|
+
try {
|
|
269
|
+
config[field.key] = JSON.parse(String(config[field.key]));
|
|
270
|
+
} catch {
|
|
271
|
+
throw new Error(
|
|
272
|
+
`Check ${field.label.toLowerCase()}: enter valid JSON`,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
166
277
|
await routines.value.save(id, {
|
|
167
278
|
...(form.routineId ? { routineId: form.routineId } : {}),
|
|
168
279
|
name: form.name.trim(),
|
|
169
280
|
prompt: form.prompt.trim(),
|
|
170
281
|
...(form.timing === "schedule"
|
|
171
282
|
? { schedule: form.schedule.trim() }
|
|
172
|
-
:
|
|
283
|
+
: form.timing === "connection"
|
|
284
|
+
? {
|
|
285
|
+
trigger: {
|
|
286
|
+
kind: "connection" as const,
|
|
287
|
+
connectionId: form.connectionId,
|
|
288
|
+
triggerType: form.triggerType,
|
|
289
|
+
config,
|
|
290
|
+
},
|
|
291
|
+
}
|
|
292
|
+
: { trigger: { kind: "webhook" as const } }),
|
|
173
293
|
timezone: form.timezone.trim(),
|
|
174
294
|
});
|
|
175
295
|
formOpen.value = false;
|
|
@@ -185,7 +305,9 @@ async function submit(): Promise<void> {
|
|
|
185
305
|
saveError.value =
|
|
186
306
|
serverRefusalMessageV1(error) ??
|
|
187
307
|
routines.value.error ??
|
|
188
|
-
|
|
308
|
+
(error instanceof Error
|
|
309
|
+
? error.message
|
|
310
|
+
: presentClientFailureV1(error, "save the Routine"));
|
|
189
311
|
}
|
|
190
312
|
}
|
|
191
313
|
|
|
@@ -232,8 +354,8 @@ async function toggleLog(routineId: string): Promise<void> {
|
|
|
232
354
|
<span class="routines__intro">
|
|
233
355
|
<strong>Routines</strong>
|
|
234
356
|
<small>
|
|
235
|
-
Standing instructions this Bot runs on a schedule
|
|
236
|
-
|
|
357
|
+
Standing instructions this Bot runs on a schedule or when something
|
|
358
|
+
happens.
|
|
237
359
|
</small>
|
|
238
360
|
</span>
|
|
239
361
|
<UiButton type="button" :disabled="!botId" @click="startCreate">
|
|
@@ -291,6 +413,13 @@ async function toggleLog(routineId: string): Promise<void> {
|
|
|
291
413
|
</UiButton>
|
|
292
414
|
</div>
|
|
293
415
|
</div>
|
|
416
|
+
<p
|
|
417
|
+
v-if="routine.trigger?.kind === 'connection'"
|
|
418
|
+
class="routines__note"
|
|
419
|
+
role="status"
|
|
420
|
+
>
|
|
421
|
+
{{ eventState(routine) }}
|
|
422
|
+
</p>
|
|
294
423
|
<dl class="routine-card__facts">
|
|
295
424
|
<div>
|
|
296
425
|
<dt>Next run</dt>
|
|
@@ -301,7 +430,7 @@ async function toggleLog(routineId: string): Promise<void> {
|
|
|
301
430
|
<template v-else>—</template>
|
|
302
431
|
</dd>
|
|
303
432
|
</div>
|
|
304
|
-
<div v-if="routine.trigger">
|
|
433
|
+
<div v-if="routine.trigger?.kind === 'webhook'">
|
|
305
434
|
<dt>Webhook key</dt>
|
|
306
435
|
<dd>
|
|
307
436
|
{{
|
|
@@ -354,7 +483,7 @@ async function toggleLog(routineId: string): Promise<void> {
|
|
|
354
483
|
Run now
|
|
355
484
|
</UiButton>
|
|
356
485
|
<UiButton
|
|
357
|
-
v-if="routine.trigger"
|
|
486
|
+
v-if="routine.trigger?.kind === 'webhook'"
|
|
358
487
|
type="button"
|
|
359
488
|
:disabled="routines.busy"
|
|
360
489
|
@click="botId && routines.rotateKey(botId, routine.routineId)"
|
|
@@ -362,7 +491,7 @@ async function toggleLog(routineId: string): Promise<void> {
|
|
|
362
491
|
{{ routine.hookKeyVersion ? "Rotate key" : "Mint key" }}
|
|
363
492
|
</UiButton>
|
|
364
493
|
<UiButton
|
|
365
|
-
v-if="routine.trigger && routine.hookKeyVersion"
|
|
494
|
+
v-if="routine.trigger?.kind === 'webhook' && routine.hookKeyVersion"
|
|
366
495
|
type="button"
|
|
367
496
|
:disabled="routines.busy"
|
|
368
497
|
@click="botId && routines.revokeKey(botId, routine.routineId)"
|
|
@@ -469,7 +598,111 @@ async function toggleLog(routineId: string): Promise<void> {
|
|
|
469
598
|
<input v-model="form.timing" type="radio" value="webhook" />
|
|
470
599
|
<span>A webhook</span>
|
|
471
600
|
</label>
|
|
601
|
+
<label>
|
|
602
|
+
<input v-model="form.timing" type="radio" value="connection" />
|
|
603
|
+
<span>An event in a connected account</span>
|
|
604
|
+
</label>
|
|
472
605
|
</fieldset>
|
|
606
|
+
<template v-if="form.timing === 'connection'">
|
|
607
|
+
<p
|
|
608
|
+
v-if="routines.triggerError"
|
|
609
|
+
class="routine-form__error"
|
|
610
|
+
role="alert"
|
|
611
|
+
>
|
|
612
|
+
{{ routines.triggerError }}
|
|
613
|
+
</p>
|
|
614
|
+
<p v-else-if="!accounts.length" class="routines__note">
|
|
615
|
+
No events are available for your connected accounts yet.
|
|
616
|
+
</p>
|
|
617
|
+
<template v-else>
|
|
618
|
+
<UiField label="Account">
|
|
619
|
+
<select v-model="form.connectionId" @change="selectAccount">
|
|
620
|
+
<option disabled value="">Choose an account</option>
|
|
621
|
+
<option
|
|
622
|
+
v-for="account in accounts"
|
|
623
|
+
:key="account.connectionId"
|
|
624
|
+
:value="account.connectionId"
|
|
625
|
+
>
|
|
626
|
+
{{ account.connectorName }} · {{ account.accountName }}
|
|
627
|
+
</option>
|
|
628
|
+
</select>
|
|
629
|
+
</UiField>
|
|
630
|
+
<UiField label="When" :hint="selectedEvent?.description">
|
|
631
|
+
<select v-model="form.triggerType" @change="selectEvent">
|
|
632
|
+
<option disabled value="">Choose an event</option>
|
|
633
|
+
<option
|
|
634
|
+
v-for="event in events"
|
|
635
|
+
:key="event.triggerType"
|
|
636
|
+
:value="event.triggerType"
|
|
637
|
+
>
|
|
638
|
+
{{ event.name }}
|
|
639
|
+
</option>
|
|
640
|
+
</select>
|
|
641
|
+
</UiField>
|
|
642
|
+
<details
|
|
643
|
+
v-if="eventFields.length"
|
|
644
|
+
:key="form.triggerType"
|
|
645
|
+
class="routine-form__options"
|
|
646
|
+
:open="eventFields.some((field) => field.required)"
|
|
647
|
+
>
|
|
648
|
+
<summary>Event options</summary>
|
|
649
|
+
<UiField
|
|
650
|
+
v-for="field in eventFields"
|
|
651
|
+
:key="field.key"
|
|
652
|
+
:label="field.label"
|
|
653
|
+
:hint="field.hint"
|
|
654
|
+
>
|
|
655
|
+
<select
|
|
656
|
+
v-if="field.options.length"
|
|
657
|
+
v-model="form.config[field.key]"
|
|
658
|
+
:required="field.required"
|
|
659
|
+
>
|
|
660
|
+
<option v-if="!field.required" value="">Any</option>
|
|
661
|
+
<option
|
|
662
|
+
v-for="option in field.options"
|
|
663
|
+
:key="option"
|
|
664
|
+
:value="option"
|
|
665
|
+
>
|
|
666
|
+
{{ option }}
|
|
667
|
+
</option>
|
|
668
|
+
</select>
|
|
669
|
+
<input
|
|
670
|
+
v-else-if="field.type === 'boolean'"
|
|
671
|
+
v-model="form.config[field.key]"
|
|
672
|
+
type="checkbox"
|
|
673
|
+
/>
|
|
674
|
+
<input
|
|
675
|
+
v-else-if="field.type === 'number' || field.type === 'integer'"
|
|
676
|
+
v-model.number="form.config[field.key]"
|
|
677
|
+
type="number"
|
|
678
|
+
:step="field.type === 'integer' ? 1 : 'any'"
|
|
679
|
+
:required="field.required"
|
|
680
|
+
/>
|
|
681
|
+
<textarea
|
|
682
|
+
v-else-if="field.type === 'array' || field.type === 'object'"
|
|
683
|
+
:value="
|
|
684
|
+
typeof form.config[field.key] === 'string'
|
|
685
|
+
? String(form.config[field.key])
|
|
686
|
+
: JSON.stringify(form.config[field.key], null, 2)
|
|
687
|
+
"
|
|
688
|
+
@input="
|
|
689
|
+
form.config[field.key] = (
|
|
690
|
+
$event.target as HTMLTextAreaElement
|
|
691
|
+
).value
|
|
692
|
+
"
|
|
693
|
+
:required="field.required"
|
|
694
|
+
rows="3"
|
|
695
|
+
/>
|
|
696
|
+
<input
|
|
697
|
+
v-else
|
|
698
|
+
v-model="form.config[field.key]"
|
|
699
|
+
:required="field.required"
|
|
700
|
+
maxlength="8000"
|
|
701
|
+
/>
|
|
702
|
+
</UiField>
|
|
703
|
+
</details>
|
|
704
|
+
</template>
|
|
705
|
+
</template>
|
|
473
706
|
<UiField
|
|
474
707
|
v-if="form.timing === 'schedule'"
|
|
475
708
|
label="Schedule"
|
|
@@ -493,7 +726,7 @@ async function toggleLog(routineId: string): Promise<void> {
|
|
|
493
726
|
>
|
|
494
727
|
{{ scheduleError }}
|
|
495
728
|
</p>
|
|
496
|
-
<p v-
|
|
729
|
+
<p v-if="form.timing === 'webhook'" class="routines__note">
|
|
497
730
|
A delivery key is minted when the Routine is saved, and shown once.
|
|
498
731
|
</p>
|
|
499
732
|
<UiField label="Time zone">
|
|
@@ -754,7 +987,8 @@ async function toggleLog(routineId: string): Promise<void> {
|
|
|
754
987
|
|
|
755
988
|
.routine-form__timing {
|
|
756
989
|
display: flex;
|
|
757
|
-
|
|
990
|
+
flex-direction: column;
|
|
991
|
+
gap: 8px;
|
|
758
992
|
border: 0;
|
|
759
993
|
margin: 0;
|
|
760
994
|
padding: 0;
|
|
@@ -773,4 +1007,23 @@ async function toggleLog(routineId: string): Promise<void> {
|
|
|
773
1007
|
align-items: center;
|
|
774
1008
|
gap: 6px;
|
|
775
1009
|
}
|
|
1010
|
+
|
|
1011
|
+
.routine-form__options {
|
|
1012
|
+
color: var(--frock-text-muted);
|
|
1013
|
+
font-size: var(--frock-text-sm);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
.routine-form__options summary {
|
|
1017
|
+
cursor: pointer;
|
|
1018
|
+
font-weight: 600;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
.routine-form__options :deep(.ui-field) {
|
|
1022
|
+
margin-top: 12px;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
.routine-form__options input[type="checkbox"] {
|
|
1026
|
+
width: auto;
|
|
1027
|
+
align-self: flex-start;
|
|
1028
|
+
}
|
|
776
1029
|
</style>
|
|
@@ -63,7 +63,9 @@ watch(
|
|
|
63
63
|
<li v-for="routine in rows" :key="routine.routineId">
|
|
64
64
|
<span>
|
|
65
65
|
<strong>{{ routine.name }}</strong>
|
|
66
|
-
<small>{{
|
|
66
|
+
<small>{{
|
|
67
|
+
routine.schedule ?? routine.eventName ?? "Webhook"
|
|
68
|
+
}}</small>
|
|
67
69
|
</span>
|
|
68
70
|
<small>{{ routine.enabled ? "On" : "Paused" }}</small>
|
|
69
71
|
</li>
|