@agent-native/dispatch 0.13.12 → 0.14.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/dist/actions/delete-destination.d.ts +12 -12
- package/dist/actions/provider-api-audit.d.ts +7 -0
- package/dist/actions/provider-api-audit.d.ts.map +1 -0
- package/dist/actions/provider-api-audit.js +74 -0
- package/dist/actions/provider-api-audit.js.map +1 -0
- package/dist/actions/provider-api-catalog.d.ts +1 -1
- package/dist/actions/provider-api-request.js +10 -0
- package/dist/actions/provider-api-request.js.map +1 -1
- package/dist/actions/send-platform-message.d.ts +1 -0
- package/dist/actions/send-platform-message.js +43 -22
- package/dist/actions/send-platform-message.js.map +1 -1
- package/dist/components/dispatch-control-plane.d.ts.map +1 -1
- package/dist/components/dispatch-control-plane.js +1 -1
- package/dist/components/dispatch-control-plane.js.map +1 -1
- package/dist/components/messaging-setup-panel.d.ts.map +1 -1
- package/dist/components/messaging-setup-panel.js +203 -127
- package/dist/components/messaging-setup-panel.js.map +1 -1
- package/dist/hooks/use-navigation-state.d.ts.map +1 -1
- package/dist/hooks/use-navigation-state.js +1 -2
- package/dist/hooks/use-navigation-state.js.map +1 -1
- package/dist/routes/pages/$appId.d.ts.map +1 -1
- package/dist/routes/pages/$appId.js +3 -1
- package/dist/routes/pages/$appId.js.map +1 -1
- package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
- package/dist/routes/pages/apps.$appId.js +2 -2
- package/dist/routes/pages/apps.$appId.js.map +1 -1
- package/dist/routes/pages/apps.d.ts.map +1 -1
- package/dist/routes/pages/apps.js +1 -3
- package/dist/routes/pages/apps.js.map +1 -1
- package/dist/routes/pages/messaging.js +1 -1
- package/dist/routes/pages/messaging.js.map +1 -1
- package/dist/routes/pages/team.js +1 -1
- package/dist/routes/pages/team.js.map +1 -1
- package/dist/server/lib/dispatch-integrations.d.ts +7 -1
- package/dist/server/lib/dispatch-integrations.d.ts.map +1 -1
- package/dist/server/lib/dispatch-integrations.js +178 -8
- package/dist/server/lib/dispatch-integrations.js.map +1 -1
- package/dist/server/lib/dispatch-routing.d.ts +6 -0
- package/dist/server/lib/dispatch-routing.d.ts.map +1 -0
- package/dist/server/lib/dispatch-routing.js +30 -0
- package/dist/server/lib/dispatch-routing.js.map +1 -0
- package/dist/server/lib/env-config.d.ts.map +1 -1
- package/dist/server/lib/env-config.js +50 -0
- package/dist/server/lib/env-config.js.map +1 -1
- package/dist/server/lib/onboarding-steps.d.ts.map +1 -1
- package/dist/server/lib/onboarding-steps.js +25 -0
- package/dist/server/lib/onboarding-steps.js.map +1 -1
- package/dist/server/lib/provider-api.d.ts +2 -2
- package/dist/server/lib/provider-api.d.ts.map +1 -1
- package/dist/server/plugins/integrations.d.ts.map +1 -1
- package/dist/server/plugins/integrations.js +5 -2
- package/dist/server/plugins/integrations.js.map +1 -1
- package/package.json +2 -2
- package/src/actions/provider-api-audit.spec.ts +52 -0
- package/src/actions/provider-api-audit.ts +88 -0
- package/src/actions/provider-api-request.ts +10 -0
- package/src/actions/send-platform-message.spec.ts +108 -0
- package/src/actions/send-platform-message.ts +73 -25
- package/src/components/dispatch-control-plane.tsx +1 -5
- package/src/components/messaging-setup-panel.spec.tsx +191 -0
- package/src/components/messaging-setup-panel.tsx +472 -193
- package/src/hooks/use-navigation-state.ts +1 -2
- package/src/routes/pages/$appId.tsx +3 -5
- package/src/routes/pages/apps.$appId.tsx +3 -7
- package/src/routes/pages/apps.tsx +0 -3
- package/src/routes/pages/messaging.tsx +1 -1
- package/src/routes/pages/team.tsx +1 -1
- package/src/server/lib/dispatch-integrations.spec.ts +113 -0
- package/src/server/lib/dispatch-integrations.ts +239 -6
- package/src/server/lib/dispatch-routing.spec.ts +42 -0
- package/src/server/lib/dispatch-routing.ts +42 -0
- package/src/server/lib/env-config.ts +50 -0
- package/src/server/lib/onboarding-steps.ts +32 -0
- package/src/server/plugins/integrations.ts +5 -2
|
@@ -1,15 +1,46 @@
|
|
|
1
|
-
import { agentNativePath } from "@agent-native/core/client";
|
|
2
1
|
import {
|
|
2
|
+
disconnectManagedIntegrationInstallation,
|
|
3
|
+
listManagedIntegrationBudgets,
|
|
4
|
+
listManagedIntegrationInstallations,
|
|
5
|
+
listManagedIntegrationScopes,
|
|
6
|
+
listIntegrationEnvStatuses,
|
|
7
|
+
listIntegrationStatuses,
|
|
8
|
+
managedIntegrationOAuthUrl,
|
|
9
|
+
managedSlackAgentManifestUrl,
|
|
10
|
+
saveIntegrationEnvVars,
|
|
11
|
+
saveManagedIntegrationBudget,
|
|
12
|
+
saveManagedIntegrationScope,
|
|
13
|
+
setIntegrationEnabled,
|
|
14
|
+
setupIntegration,
|
|
15
|
+
testManagedIntegrationInstallation,
|
|
16
|
+
type ClientIntegrationInstallation,
|
|
17
|
+
type ClientIntegrationScope,
|
|
18
|
+
type ClientIntegrationUsageBudget,
|
|
19
|
+
type ClientIntegrationStatus,
|
|
20
|
+
type IntegrationEnvStatus,
|
|
21
|
+
useFormatters,
|
|
22
|
+
useT,
|
|
23
|
+
} from "@agent-native/core/client";
|
|
24
|
+
import {
|
|
25
|
+
listBuiltInChannelIntegrations,
|
|
26
|
+
type IntegrationCatalogEntry,
|
|
27
|
+
type IntegrationCredentialRequirement,
|
|
28
|
+
} from "@agent-native/core/integrations";
|
|
29
|
+
import {
|
|
30
|
+
IconBrandDiscord,
|
|
3
31
|
IconBrandSlack,
|
|
4
32
|
IconBrandTelegram,
|
|
33
|
+
IconBrandTeams,
|
|
5
34
|
IconBrandWhatsapp,
|
|
6
35
|
IconCheck,
|
|
7
36
|
IconChevronRight,
|
|
8
37
|
IconCopy,
|
|
9
38
|
IconExternalLink,
|
|
39
|
+
IconFileDescription,
|
|
10
40
|
IconInfoCircle,
|
|
11
41
|
IconLoader2,
|
|
12
42
|
IconMail,
|
|
43
|
+
IconPlug,
|
|
13
44
|
} from "@tabler/icons-react";
|
|
14
45
|
import { useEffect, useMemo, useState } from "react";
|
|
15
46
|
import { toast } from "sonner";
|
|
@@ -21,124 +52,46 @@ import {
|
|
|
21
52
|
CollapsibleTrigger,
|
|
22
53
|
} from "./ui/collapsible";
|
|
23
54
|
import { Input } from "./ui/input";
|
|
55
|
+
import { Switch } from "./ui/switch";
|
|
24
56
|
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
|
|
25
57
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
58
|
+
const CHANNELS = listBuiltInChannelIntegrations();
|
|
59
|
+
|
|
60
|
+
const PLATFORM_ICONS: Partial<Record<string, typeof IconBrandSlack>> = {
|
|
61
|
+
slack: IconBrandSlack,
|
|
62
|
+
"microsoft-teams": IconBrandTeams,
|
|
63
|
+
discord: IconBrandDiscord,
|
|
64
|
+
telegram: IconBrandTelegram,
|
|
65
|
+
whatsapp: IconBrandWhatsapp,
|
|
66
|
+
email: IconMail,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function hasMissingRequiredCredentials(
|
|
70
|
+
credentials: readonly IntegrationCredentialRequirement[],
|
|
71
|
+
envStatusByKey: Map<string, IntegrationEnvStatus>,
|
|
72
|
+
) {
|
|
73
|
+
const alternatives = new Map<
|
|
74
|
+
string,
|
|
75
|
+
readonly IntegrationCredentialRequirement[]
|
|
76
|
+
>();
|
|
77
|
+
|
|
78
|
+
for (const credential of credentials) {
|
|
79
|
+
if (!credential.required) continue;
|
|
80
|
+
if (!credential.alternativeGroup) {
|
|
81
|
+
if (!envStatusByKey.get(credential.key)?.configured) return true;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const group = alternatives.get(credential.alternativeGroup) ?? [];
|
|
85
|
+
alternatives.set(credential.alternativeGroup, [...group, credential]);
|
|
86
|
+
}
|
|
49
87
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
/** Our own docs anchor — keep these on /docs/messaging so users land on
|
|
56
|
-
* the page that explains the platform in plain English. */
|
|
57
|
-
docsUrl: string;
|
|
58
|
-
/** Optional external link (e.g. to the platform's developer console). */
|
|
59
|
-
externalUrl?: string;
|
|
60
|
-
externalLabel?: string;
|
|
61
|
-
setupSteps: string[];
|
|
62
|
-
/** Fallback env keys when the adapter doesn't surface them via
|
|
63
|
-
* `IntegrationStatus.requiredEnvKeys`. The panel prefers adapter-supplied
|
|
64
|
-
* keys when present so optional fields (webhook secrets, etc.) appear
|
|
65
|
-
* automatically. */
|
|
66
|
-
envKeys: string[];
|
|
88
|
+
return [...alternatives.values()].some((group) =>
|
|
89
|
+
group.every(
|
|
90
|
+
(credential) => !envStatusByKey.get(credential.key)?.configured,
|
|
91
|
+
),
|
|
92
|
+
);
|
|
67
93
|
}
|
|
68
94
|
|
|
69
|
-
const PLATFORM_DEFINITIONS: PlatformDefinition[] = [
|
|
70
|
-
{
|
|
71
|
-
id: "slack",
|
|
72
|
-
label: "Slack",
|
|
73
|
-
icon: IconBrandSlack,
|
|
74
|
-
description: "Receive mentions and DMs in one workspace-aware dispatch.",
|
|
75
|
-
docsUrl: "/docs/messaging#slack",
|
|
76
|
-
externalUrl: "https://api.slack.com/apps",
|
|
77
|
-
externalLabel: "Open Slack apps",
|
|
78
|
-
envKeys: ["SLACK_BOT_TOKEN", "SLACK_SIGNING_SECRET"],
|
|
79
|
-
setupSteps: [
|
|
80
|
-
"Create or open a Slack app at api.slack.com/apps.",
|
|
81
|
-
"Save the bot token and signing secret below — the webhook URL appears once they're saved.",
|
|
82
|
-
"Back in Slack, enable Event Subscriptions and paste the webhook URL.",
|
|
83
|
-
"Subscribe to app_mention and message.im events, then install the app.",
|
|
84
|
-
"Optional but recommended: Basic Information → Display Information → upload an app icon and pick a background color so the bot has a clean avatar in every channel.",
|
|
85
|
-
],
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
id: "telegram",
|
|
89
|
-
label: "Telegram",
|
|
90
|
-
icon: IconBrandTelegram,
|
|
91
|
-
description: "Chat with dispatch through a Telegram bot.",
|
|
92
|
-
docsUrl: "/docs/messaging#telegram",
|
|
93
|
-
externalUrl: "https://t.me/BotFather",
|
|
94
|
-
externalLabel: "Open BotFather",
|
|
95
|
-
envKeys: ["TELEGRAM_BOT_TOKEN"],
|
|
96
|
-
setupSteps: [
|
|
97
|
-
"Open @BotFather in Telegram and send /newbot.",
|
|
98
|
-
"Save the bot token here, then click Set up webhook below.",
|
|
99
|
-
"DM the bot in Telegram to test.",
|
|
100
|
-
],
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
id: "email",
|
|
104
|
-
label: "Email",
|
|
105
|
-
icon: IconMail,
|
|
106
|
-
description:
|
|
107
|
-
"Give your agent an email address. People can email it directly or CC it on threads.",
|
|
108
|
-
docsUrl: "/docs/messaging#email",
|
|
109
|
-
externalUrl: "https://resend.com/webhooks",
|
|
110
|
-
externalLabel: "Open Resend webhooks",
|
|
111
|
-
envKeys: ["EMAIL_AGENT_ADDRESS"],
|
|
112
|
-
setupSteps: [
|
|
113
|
-
"Save your Resend or SendGrid API key (Vault or onboarding).",
|
|
114
|
-
"Pick an email address — the easiest is a free <slug>.resend.app address.",
|
|
115
|
-
"If using your own domain, add MX records pointing to your provider.",
|
|
116
|
-
"Save the address here, then register the webhook URL below in Resend (event: email.received).",
|
|
117
|
-
],
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
id: "whatsapp",
|
|
121
|
-
label: "WhatsApp",
|
|
122
|
-
icon: IconBrandWhatsapp,
|
|
123
|
-
description:
|
|
124
|
-
"Receive WhatsApp messages and reply through a Meta-managed phone number.",
|
|
125
|
-
docsUrl: "/docs/messaging#whatsapp",
|
|
126
|
-
externalUrl: "https://developers.facebook.com/apps",
|
|
127
|
-
externalLabel: "Open Meta developer console",
|
|
128
|
-
envKeys: [
|
|
129
|
-
"WHATSAPP_ACCESS_TOKEN",
|
|
130
|
-
"WHATSAPP_VERIFY_TOKEN",
|
|
131
|
-
"WHATSAPP_PHONE_NUMBER_ID",
|
|
132
|
-
],
|
|
133
|
-
setupSteps: [
|
|
134
|
-
"Create a Meta app and add the WhatsApp product.",
|
|
135
|
-
"Save the access token, verify token, and phone number ID below.",
|
|
136
|
-
"In Meta's WhatsApp configuration, paste the webhook URL and your verify token.",
|
|
137
|
-
"Subscribe to the messages field, then enable here.",
|
|
138
|
-
],
|
|
139
|
-
},
|
|
140
|
-
];
|
|
141
|
-
|
|
142
95
|
function HelpTooltip({ content }: { content: string }) {
|
|
143
96
|
return (
|
|
144
97
|
<Tooltip>
|
|
@@ -211,24 +164,32 @@ function ConnectionStatus({
|
|
|
211
164
|
}
|
|
212
165
|
|
|
213
166
|
export function MessagingSetupPanel() {
|
|
214
|
-
const
|
|
167
|
+
const t = useT();
|
|
168
|
+
const { formatDate } = useFormatters();
|
|
169
|
+
const [statuses, setStatuses] = useState<ClientIntegrationStatus[]>([]);
|
|
215
170
|
const [loading, setLoading] = useState(true);
|
|
216
|
-
const [envStatuses, setEnvStatuses] = useState<
|
|
171
|
+
const [envStatuses, setEnvStatuses] = useState<IntegrationEnvStatus[]>([]);
|
|
217
172
|
const [envLoading, setEnvLoading] = useState(true);
|
|
218
173
|
const [envValues, setEnvValues] = useState<Record<string, string>>({});
|
|
219
174
|
const [savingKeysFor, setSavingKeysFor] = useState<string | null>(null);
|
|
220
175
|
const [togglingPlatform, setTogglingPlatform] = useState<string | null>(null);
|
|
221
176
|
const [setupPlatform, setSetupPlatform] = useState<string | null>(null);
|
|
222
177
|
const [copiedWebhook, setCopiedWebhook] = useState<string | null>(null);
|
|
178
|
+
const [installations, setInstallations] = useState<
|
|
179
|
+
ClientIntegrationInstallation[]
|
|
180
|
+
>([]);
|
|
181
|
+
const [installationAction, setInstallationAction] = useState<string | null>(
|
|
182
|
+
null,
|
|
183
|
+
);
|
|
184
|
+
const [scopes, setScopes] = useState<ClientIntegrationScope[]>([]);
|
|
185
|
+
const [budgets, setBudgets] = useState<ClientIntegrationUsageBudget[]>([]);
|
|
186
|
+
const [scopeBudget, setScopeBudget] = useState<Record<string, string>>({});
|
|
187
|
+
const [savingScope, setSavingScope] = useState<string | null>(null);
|
|
223
188
|
|
|
224
189
|
const refreshStatuses = async () => {
|
|
225
190
|
setLoading(true);
|
|
226
191
|
try {
|
|
227
|
-
|
|
228
|
-
agentNativePath("/_agent-native/integrations/status"),
|
|
229
|
-
);
|
|
230
|
-
const rows = res.ok ? await res.json() : [];
|
|
231
|
-
setStatuses(Array.isArray(rows) ? rows : []);
|
|
192
|
+
setStatuses(await listIntegrationStatuses());
|
|
232
193
|
} finally {
|
|
233
194
|
setLoading(false);
|
|
234
195
|
}
|
|
@@ -236,11 +197,10 @@ export function MessagingSetupPanel() {
|
|
|
236
197
|
|
|
237
198
|
useEffect(() => {
|
|
238
199
|
let active = true;
|
|
239
|
-
|
|
240
|
-
.then((res) => (res.ok ? res.json() : []))
|
|
200
|
+
listIntegrationStatuses()
|
|
241
201
|
.then((rows) => {
|
|
242
202
|
if (active) {
|
|
243
|
-
setStatuses(
|
|
203
|
+
setStatuses(rows);
|
|
244
204
|
setLoading(false);
|
|
245
205
|
}
|
|
246
206
|
})
|
|
@@ -254,11 +214,32 @@ export function MessagingSetupPanel() {
|
|
|
254
214
|
|
|
255
215
|
useEffect(() => {
|
|
256
216
|
let active = true;
|
|
257
|
-
|
|
258
|
-
.then((
|
|
217
|
+
listManagedIntegrationInstallations("slack")
|
|
218
|
+
.then((rows) => {
|
|
219
|
+
if (active) setInstallations(rows);
|
|
220
|
+
})
|
|
221
|
+
.catch(() => {});
|
|
222
|
+
listManagedIntegrationScopes("slack")
|
|
223
|
+
.then((rows) => {
|
|
224
|
+
if (active) setScopes(rows);
|
|
225
|
+
})
|
|
226
|
+
.catch(() => {});
|
|
227
|
+
listManagedIntegrationBudgets()
|
|
228
|
+
.then((rows) => {
|
|
229
|
+
if (active) setBudgets(rows);
|
|
230
|
+
})
|
|
231
|
+
.catch(() => {});
|
|
232
|
+
return () => {
|
|
233
|
+
active = false;
|
|
234
|
+
};
|
|
235
|
+
}, []);
|
|
236
|
+
|
|
237
|
+
useEffect(() => {
|
|
238
|
+
let active = true;
|
|
239
|
+
listIntegrationEnvStatuses()
|
|
259
240
|
.then((rows) => {
|
|
260
241
|
if (active) {
|
|
261
|
-
setEnvStatuses(
|
|
242
|
+
setEnvStatuses(rows);
|
|
262
243
|
setEnvLoading(false);
|
|
263
244
|
}
|
|
264
245
|
})
|
|
@@ -282,15 +263,16 @@ export function MessagingSetupPanel() {
|
|
|
282
263
|
const refreshEnvStatus = async () => {
|
|
283
264
|
setEnvLoading(true);
|
|
284
265
|
try {
|
|
285
|
-
|
|
286
|
-
const rows = res.ok ? await res.json() : [];
|
|
287
|
-
setEnvStatuses(Array.isArray(rows) ? rows : []);
|
|
266
|
+
setEnvStatuses(await listIntegrationEnvStatuses());
|
|
288
267
|
} finally {
|
|
289
268
|
setEnvLoading(false);
|
|
290
269
|
}
|
|
291
270
|
};
|
|
292
271
|
|
|
293
|
-
const saveEnvKeys = async (
|
|
272
|
+
const saveEnvKeys = async (
|
|
273
|
+
platform: IntegrationCatalogEntry,
|
|
274
|
+
keys: string[],
|
|
275
|
+
) => {
|
|
294
276
|
const vars = keys
|
|
295
277
|
.map((key) => ({ key, value: envValues[key]?.trim() || "" }))
|
|
296
278
|
.filter((item) => item.value);
|
|
@@ -302,18 +284,9 @@ export function MessagingSetupPanel() {
|
|
|
302
284
|
|
|
303
285
|
setSavingKeysFor(platform.id);
|
|
304
286
|
try {
|
|
305
|
-
|
|
306
|
-
method: "POST",
|
|
307
|
-
headers: { "Content-Type": "application/json" },
|
|
308
|
-
body: JSON.stringify({ vars }),
|
|
309
|
-
});
|
|
287
|
+
await saveIntegrationEnvVars(vars);
|
|
310
288
|
|
|
311
|
-
|
|
312
|
-
const payload = await res.json().catch(() => ({}));
|
|
313
|
-
throw new Error(payload.error || "Failed to save credentials");
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
toast.success(`${platform.label} credentials saved`);
|
|
289
|
+
toast.success(`${platform.name} credentials saved`);
|
|
317
290
|
setEnvValues((current) => {
|
|
318
291
|
const next = { ...current };
|
|
319
292
|
for (const key of keys) delete next[key];
|
|
@@ -331,28 +304,16 @@ export function MessagingSetupPanel() {
|
|
|
331
304
|
};
|
|
332
305
|
|
|
333
306
|
const togglePlatform = async (
|
|
334
|
-
platform:
|
|
307
|
+
platform: IntegrationCatalogEntry,
|
|
335
308
|
enabled: boolean,
|
|
336
309
|
) => {
|
|
337
310
|
setTogglingPlatform(platform.id);
|
|
338
311
|
try {
|
|
339
|
-
|
|
340
|
-
const res = await fetch(
|
|
341
|
-
agentNativePath(`/_agent-native/integrations/${platform.id}/${action}`),
|
|
342
|
-
{
|
|
343
|
-
method: "POST",
|
|
344
|
-
},
|
|
345
|
-
);
|
|
346
|
-
if (!res.ok) {
|
|
347
|
-
const payload = await res.json().catch(() => ({}));
|
|
348
|
-
throw new Error(
|
|
349
|
-
payload.error || `Failed to ${action} ${platform.label}`,
|
|
350
|
-
);
|
|
351
|
-
}
|
|
312
|
+
await setIntegrationEnabled(platform.id, !enabled);
|
|
352
313
|
toast.success(
|
|
353
314
|
enabled
|
|
354
|
-
? `${platform.
|
|
355
|
-
: `${platform.
|
|
315
|
+
? `${platform.name} disconnected`
|
|
316
|
+
: `${platform.name} connected`,
|
|
356
317
|
);
|
|
357
318
|
await refreshStatuses();
|
|
358
319
|
} catch (error) {
|
|
@@ -364,30 +325,21 @@ export function MessagingSetupPanel() {
|
|
|
364
325
|
}
|
|
365
326
|
};
|
|
366
327
|
|
|
367
|
-
const runSetup = async (platform:
|
|
328
|
+
const runSetup = async (platform: IntegrationCatalogEntry) => {
|
|
368
329
|
setSetupPlatform(platform.id);
|
|
369
330
|
try {
|
|
370
|
-
|
|
371
|
-
agentNativePath(`/_agent-native/integrations/${platform.id}/setup`),
|
|
372
|
-
{
|
|
373
|
-
method: "POST",
|
|
374
|
-
},
|
|
375
|
-
);
|
|
376
|
-
if (!res.ok) {
|
|
377
|
-
const payload = await res.json().catch(() => ({}));
|
|
378
|
-
throw new Error(payload.error || `Failed to set up ${platform.label}`);
|
|
379
|
-
}
|
|
331
|
+
await setupIntegration(platform.id);
|
|
380
332
|
toast.success(
|
|
381
333
|
platform.id === "telegram"
|
|
382
334
|
? "Telegram webhook registered"
|
|
383
|
-
: `${platform.
|
|
335
|
+
: `${platform.name} setup complete`,
|
|
384
336
|
);
|
|
385
337
|
await refreshStatuses();
|
|
386
338
|
} catch (error) {
|
|
387
339
|
toast.error(
|
|
388
340
|
error instanceof Error
|
|
389
341
|
? error.message
|
|
390
|
-
: `Failed to set up ${platform.
|
|
342
|
+
: `Failed to set up ${platform.name}`,
|
|
391
343
|
);
|
|
392
344
|
} finally {
|
|
393
345
|
setSetupPlatform(null);
|
|
@@ -401,24 +353,113 @@ export function MessagingSetupPanel() {
|
|
|
401
353
|
setTimeout(() => setCopiedWebhook(null), 1500);
|
|
402
354
|
};
|
|
403
355
|
|
|
356
|
+
const runInstallationAction = async (
|
|
357
|
+
installation: ClientIntegrationInstallation,
|
|
358
|
+
action: "test" | "disconnect",
|
|
359
|
+
) => {
|
|
360
|
+
setInstallationAction(`${action}:${installation.id}`);
|
|
361
|
+
try {
|
|
362
|
+
await (action === "test"
|
|
363
|
+
? testManagedIntegrationInstallation(installation.id)
|
|
364
|
+
: disconnectManagedIntegrationInstallation(installation.id));
|
|
365
|
+
setInstallations(await listManagedIntegrationInstallations("slack"));
|
|
366
|
+
toast.success(
|
|
367
|
+
action === "test"
|
|
368
|
+
? t("messaging.managed.connectionChecked")
|
|
369
|
+
: t("messaging.managed.workspaceDisconnected"),
|
|
370
|
+
);
|
|
371
|
+
} catch (error) {
|
|
372
|
+
toast.error(
|
|
373
|
+
error instanceof Error
|
|
374
|
+
? error.message
|
|
375
|
+
: t("messaging.managed.actionFailed"),
|
|
376
|
+
);
|
|
377
|
+
} finally {
|
|
378
|
+
setInstallationAction(null);
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
const updateScopePolicy = async (
|
|
383
|
+
scope: ClientIntegrationScope,
|
|
384
|
+
policy: Partial<ClientIntegrationScope["policy"]>,
|
|
385
|
+
) => {
|
|
386
|
+
setSavingScope(scope.id);
|
|
387
|
+
try {
|
|
388
|
+
await saveManagedIntegrationScope({
|
|
389
|
+
platform: scope.platform,
|
|
390
|
+
tenantId: scope.tenantId,
|
|
391
|
+
conversationId: scope.conversationId,
|
|
392
|
+
conversationType: scope.conversationType,
|
|
393
|
+
trust: scope.trust,
|
|
394
|
+
orgId: scope.orgId,
|
|
395
|
+
serviceOwnerEmail: scope.serviceOwnerEmail,
|
|
396
|
+
defaultModel: scope.defaultModel,
|
|
397
|
+
policy: { ...scope.policy, ...policy },
|
|
398
|
+
});
|
|
399
|
+
setScopes(await listManagedIntegrationScopes("slack"));
|
|
400
|
+
toast.success(t("messaging.managed.channelPolicyUpdated"));
|
|
401
|
+
} catch (error) {
|
|
402
|
+
toast.error(
|
|
403
|
+
error instanceof Error
|
|
404
|
+
? error.message
|
|
405
|
+
: t("messaging.managed.policyUpdateFailed"),
|
|
406
|
+
);
|
|
407
|
+
} finally {
|
|
408
|
+
setSavingScope(null);
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
const saveScopeBudget = async (scope: ClientIntegrationScope) => {
|
|
413
|
+
const dollars = Number(scopeBudget[scope.id]);
|
|
414
|
+
if (!Number.isFinite(dollars) || dollars <= 0) {
|
|
415
|
+
toast.error(t("messaging.managed.positiveMonthlyBudget"));
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
setSavingScope(`budget:${scope.id}`);
|
|
419
|
+
try {
|
|
420
|
+
await saveManagedIntegrationBudget({
|
|
421
|
+
subject: {
|
|
422
|
+
type: "scope",
|
|
423
|
+
scope: {
|
|
424
|
+
platform: scope.platform,
|
|
425
|
+
tenantId: scope.tenantId,
|
|
426
|
+
conversationId: scope.conversationId,
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
period: "month",
|
|
430
|
+
limitMicros: Math.round(dollars * 1_000_000),
|
|
431
|
+
});
|
|
432
|
+
setBudgets(await listManagedIntegrationBudgets());
|
|
433
|
+
toast.success(t("messaging.managed.channelBudgetSaved"));
|
|
434
|
+
} catch (error) {
|
|
435
|
+
toast.error(
|
|
436
|
+
error instanceof Error
|
|
437
|
+
? error.message
|
|
438
|
+
: t("messaging.managed.budgetSaveFailed"),
|
|
439
|
+
);
|
|
440
|
+
} finally {
|
|
441
|
+
setSavingScope(null);
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
|
|
404
445
|
return (
|
|
405
446
|
<div className="space-y-4">
|
|
406
447
|
<div className="grid gap-4 xl:grid-cols-2">
|
|
407
|
-
{
|
|
448
|
+
{CHANNELS.map((platform) => {
|
|
408
449
|
const status = statusByPlatform.get(platform.id);
|
|
409
|
-
const configured =
|
|
450
|
+
const configured =
|
|
451
|
+
!!status?.configured ||
|
|
452
|
+
(platform.id === "slack" &&
|
|
453
|
+
installations.some(
|
|
454
|
+
(installation) => installation.status === "connected",
|
|
455
|
+
));
|
|
410
456
|
const enabled = !!status?.enabled;
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
: platform.envKeys.map((key) => ({
|
|
418
|
-
key,
|
|
419
|
-
label: key,
|
|
420
|
-
required: true,
|
|
421
|
-
}));
|
|
457
|
+
const envKeys = platform.credentialRequirements;
|
|
458
|
+
const missingRequiredCredentials = hasMissingRequiredCredentials(
|
|
459
|
+
envKeys,
|
|
460
|
+
envStatusByKey,
|
|
461
|
+
);
|
|
462
|
+
const Icon = PLATFORM_ICONS[platform.iconKey] ?? IconPlug;
|
|
422
463
|
|
|
423
464
|
return (
|
|
424
465
|
<section
|
|
@@ -428,12 +469,12 @@ export function MessagingSetupPanel() {
|
|
|
428
469
|
<div className="flex items-start justify-between gap-4">
|
|
429
470
|
<div className="flex items-start gap-3">
|
|
430
471
|
<div className="flex h-10 w-10 items-center justify-center rounded-xl border bg-muted/30 text-foreground">
|
|
431
|
-
<
|
|
472
|
+
<Icon size={18} />
|
|
432
473
|
</div>
|
|
433
474
|
<div>
|
|
434
475
|
<div className="flex items-center gap-2">
|
|
435
476
|
<h3 className="text-base font-semibold text-foreground">
|
|
436
|
-
{platform.
|
|
477
|
+
{platform.name}
|
|
437
478
|
</h3>
|
|
438
479
|
<ConnectionStatus
|
|
439
480
|
configured={configured}
|
|
@@ -452,12 +493,16 @@ export function MessagingSetupPanel() {
|
|
|
452
493
|
size="sm"
|
|
453
494
|
className="h-7 px-2 text-xs text-muted-foreground"
|
|
454
495
|
>
|
|
455
|
-
<a
|
|
496
|
+
<a
|
|
497
|
+
href={platform.documentation.href}
|
|
498
|
+
target="_blank"
|
|
499
|
+
rel="noreferrer"
|
|
500
|
+
>
|
|
456
501
|
Docs
|
|
457
502
|
<IconExternalLink className="ml-1 h-3 w-3" />
|
|
458
503
|
</a>
|
|
459
504
|
</Button>
|
|
460
|
-
{platform.
|
|
505
|
+
{platform.documentation.externalHref ? (
|
|
461
506
|
<Button
|
|
462
507
|
asChild
|
|
463
508
|
variant="ghost"
|
|
@@ -465,11 +510,11 @@ export function MessagingSetupPanel() {
|
|
|
465
510
|
className="h-7 px-2 text-xs text-muted-foreground"
|
|
466
511
|
>
|
|
467
512
|
<a
|
|
468
|
-
href={platform.
|
|
513
|
+
href={platform.documentation.externalHref}
|
|
469
514
|
target="_blank"
|
|
470
515
|
rel="noreferrer"
|
|
471
516
|
>
|
|
472
|
-
{platform.externalLabel ?? "Open"}
|
|
517
|
+
{platform.documentation.externalLabel ?? "Open"}
|
|
473
518
|
<IconExternalLink className="ml-1 h-3 w-3" />
|
|
474
519
|
</a>
|
|
475
520
|
</Button>
|
|
@@ -477,6 +522,240 @@ export function MessagingSetupPanel() {
|
|
|
477
522
|
</div>
|
|
478
523
|
</div>
|
|
479
524
|
|
|
525
|
+
{platform.id === "slack" ? (
|
|
526
|
+
<div className="mt-5 space-y-3 rounded-xl border bg-muted/20 p-4">
|
|
527
|
+
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
528
|
+
<div>
|
|
529
|
+
<div className="text-sm font-medium text-foreground">
|
|
530
|
+
{t("messaging.managed.title")}
|
|
531
|
+
</div>
|
|
532
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
533
|
+
{t("messaging.managed.description")}
|
|
534
|
+
</p>
|
|
535
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
536
|
+
{t("messaging.managed.agentManifestDescription")}
|
|
537
|
+
</p>
|
|
538
|
+
</div>
|
|
539
|
+
<div className="space-y-2">
|
|
540
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
541
|
+
<Button asChild variant="outline" size="sm">
|
|
542
|
+
<a href={managedSlackAgentManifestUrl()}>
|
|
543
|
+
<IconFileDescription className="mr-2 h-4 w-4" />
|
|
544
|
+
{t("messaging.managed.agentManifest")}
|
|
545
|
+
</a>
|
|
546
|
+
</Button>
|
|
547
|
+
{missingRequiredCredentials ? (
|
|
548
|
+
<Button size="sm" disabled>
|
|
549
|
+
<IconBrandSlack className="mr-2 h-4 w-4" />
|
|
550
|
+
{t("messaging.managed.addToSlack")}
|
|
551
|
+
</Button>
|
|
552
|
+
) : (
|
|
553
|
+
<Button asChild size="sm">
|
|
554
|
+
<a href={managedIntegrationOAuthUrl("slack")}>
|
|
555
|
+
<IconBrandSlack className="mr-2 h-4 w-4" />
|
|
556
|
+
{t("messaging.managed.addToSlack")}
|
|
557
|
+
</a>
|
|
558
|
+
</Button>
|
|
559
|
+
)}
|
|
560
|
+
</div>
|
|
561
|
+
{missingRequiredCredentials ? (
|
|
562
|
+
<p className="max-w-72 text-xs text-amber-700 dark:text-amber-300">
|
|
563
|
+
{t("messaging.managed.requiredCredentials")}
|
|
564
|
+
</p>
|
|
565
|
+
) : null}
|
|
566
|
+
</div>
|
|
567
|
+
</div>
|
|
568
|
+
{installations.length ? (
|
|
569
|
+
<div className="space-y-2">
|
|
570
|
+
{installations.map((installation) => (
|
|
571
|
+
<div
|
|
572
|
+
key={installation.id}
|
|
573
|
+
className="flex flex-wrap items-center justify-between gap-3 rounded-lg border bg-background/60 px-3 py-2"
|
|
574
|
+
>
|
|
575
|
+
<div>
|
|
576
|
+
<div className="text-sm font-medium text-foreground">
|
|
577
|
+
{installation.teamName ||
|
|
578
|
+
installation.enterpriseName ||
|
|
579
|
+
installation.teamId ||
|
|
580
|
+
t("messaging.managed.workspaceFallback")}
|
|
581
|
+
</div>
|
|
582
|
+
<div className="mt-0.5 text-xs text-muted-foreground">
|
|
583
|
+
{t("messaging.managed.scopesUpdated", {
|
|
584
|
+
count: installation.scopes.length,
|
|
585
|
+
date: formatDate(installation.updatedAt, {
|
|
586
|
+
dateStyle: "medium",
|
|
587
|
+
}),
|
|
588
|
+
})}
|
|
589
|
+
</div>
|
|
590
|
+
</div>
|
|
591
|
+
<div className="flex items-center gap-2">
|
|
592
|
+
<StatusPill
|
|
593
|
+
tone={
|
|
594
|
+
installation.health === "healthy"
|
|
595
|
+
? "success"
|
|
596
|
+
: installation.health === "degraded" ||
|
|
597
|
+
installation.health === "revoked"
|
|
598
|
+
? "warning"
|
|
599
|
+
: "neutral"
|
|
600
|
+
}
|
|
601
|
+
label={t(
|
|
602
|
+
`messaging.managed.health.${installation.health}`,
|
|
603
|
+
)}
|
|
604
|
+
/>
|
|
605
|
+
<Button
|
|
606
|
+
variant="outline"
|
|
607
|
+
size="sm"
|
|
608
|
+
onClick={() =>
|
|
609
|
+
runInstallationAction(installation, "test")
|
|
610
|
+
}
|
|
611
|
+
disabled={
|
|
612
|
+
installationAction === `test:${installation.id}`
|
|
613
|
+
}
|
|
614
|
+
>
|
|
615
|
+
{t("messaging.managed.test")}
|
|
616
|
+
</Button>
|
|
617
|
+
<Button
|
|
618
|
+
variant="ghost"
|
|
619
|
+
size="sm"
|
|
620
|
+
onClick={() =>
|
|
621
|
+
runInstallationAction(
|
|
622
|
+
installation,
|
|
623
|
+
"disconnect",
|
|
624
|
+
)
|
|
625
|
+
}
|
|
626
|
+
disabled={
|
|
627
|
+
installationAction ===
|
|
628
|
+
`disconnect:${installation.id}`
|
|
629
|
+
}
|
|
630
|
+
>
|
|
631
|
+
{t("messaging.managed.disconnect")}
|
|
632
|
+
</Button>
|
|
633
|
+
</div>
|
|
634
|
+
</div>
|
|
635
|
+
))}
|
|
636
|
+
</div>
|
|
637
|
+
) : (
|
|
638
|
+
<p className="text-xs text-muted-foreground">
|
|
639
|
+
{t("messaging.managed.empty")}
|
|
640
|
+
</p>
|
|
641
|
+
)}
|
|
642
|
+
</div>
|
|
643
|
+
) : null}
|
|
644
|
+
|
|
645
|
+
{platform.id === "slack" && scopes.length ? (
|
|
646
|
+
<div className="mt-4 space-y-3 rounded-xl border bg-muted/20 p-4">
|
|
647
|
+
<div>
|
|
648
|
+
<div className="text-sm font-medium text-foreground">
|
|
649
|
+
{t("messaging.managed.channelAccessTitle")}
|
|
650
|
+
</div>
|
|
651
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
652
|
+
{t("messaging.managed.channelAccessDescription")}
|
|
653
|
+
</p>
|
|
654
|
+
</div>
|
|
655
|
+
{scopes.map((scope) => {
|
|
656
|
+
const subjectId = JSON.stringify([
|
|
657
|
+
scope.platform,
|
|
658
|
+
scope.tenantId,
|
|
659
|
+
scope.conversationId,
|
|
660
|
+
]);
|
|
661
|
+
const budget = budgets.find(
|
|
662
|
+
(item) =>
|
|
663
|
+
item.subjectType === "scope" &&
|
|
664
|
+
item.subjectId === subjectId &&
|
|
665
|
+
item.period === "month",
|
|
666
|
+
);
|
|
667
|
+
return (
|
|
668
|
+
<div
|
|
669
|
+
key={scope.id}
|
|
670
|
+
className="space-y-3 rounded-lg border bg-background/60 p-3"
|
|
671
|
+
>
|
|
672
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
673
|
+
<div>
|
|
674
|
+
<div className="font-mono text-xs text-foreground">
|
|
675
|
+
{scope.conversationId}
|
|
676
|
+
</div>
|
|
677
|
+
<div className="mt-0.5 text-xs text-muted-foreground">
|
|
678
|
+
{t("messaging.managed.isolatedIdentity", {
|
|
679
|
+
trust: t(
|
|
680
|
+
`messaging.managed.trust.${scope.trust}`,
|
|
681
|
+
),
|
|
682
|
+
})}
|
|
683
|
+
</div>
|
|
684
|
+
</div>
|
|
685
|
+
<StatusPill
|
|
686
|
+
tone={
|
|
687
|
+
scope.trust === "trusted" ? "success" : "warning"
|
|
688
|
+
}
|
|
689
|
+
label={t(`messaging.managed.trust.${scope.trust}`)}
|
|
690
|
+
/>
|
|
691
|
+
</div>
|
|
692
|
+
<div className="grid gap-2 sm:grid-cols-3">
|
|
693
|
+
{[
|
|
694
|
+
[
|
|
695
|
+
"requireMention",
|
|
696
|
+
t("messaging.managed.requireMention"),
|
|
697
|
+
],
|
|
698
|
+
["allowGuests", t("messaging.managed.allowGuests")],
|
|
699
|
+
[
|
|
700
|
+
"allowExternalShared",
|
|
701
|
+
t("messaging.managed.allowSlackConnect"),
|
|
702
|
+
],
|
|
703
|
+
].map(([key, label]) => (
|
|
704
|
+
<label
|
|
705
|
+
key={key}
|
|
706
|
+
className="flex items-center justify-between gap-2 rounded-md border px-2.5 py-2 text-xs"
|
|
707
|
+
>
|
|
708
|
+
{label}
|
|
709
|
+
<Switch
|
|
710
|
+
checked={
|
|
711
|
+
scope.policy[
|
|
712
|
+
key as keyof ClientIntegrationScope["policy"]
|
|
713
|
+
]
|
|
714
|
+
}
|
|
715
|
+
disabled={savingScope === scope.id}
|
|
716
|
+
onCheckedChange={(checked) =>
|
|
717
|
+
updateScopePolicy(scope, { [key]: checked })
|
|
718
|
+
}
|
|
719
|
+
/>
|
|
720
|
+
</label>
|
|
721
|
+
))}
|
|
722
|
+
</div>
|
|
723
|
+
<div className="flex flex-wrap items-end gap-2">
|
|
724
|
+
<div className="min-w-40 flex-1 space-y-1">
|
|
725
|
+
<label className="text-xs font-medium text-foreground">
|
|
726
|
+
{t("messaging.managed.monthlyBudgetUsd")}
|
|
727
|
+
</label>
|
|
728
|
+
<Input
|
|
729
|
+
inputMode="decimal"
|
|
730
|
+
value={
|
|
731
|
+
scopeBudget[scope.id] ??
|
|
732
|
+
(budget
|
|
733
|
+
? String(budget.limitMicros / 1_000_000)
|
|
734
|
+
: "")
|
|
735
|
+
}
|
|
736
|
+
onChange={(event) =>
|
|
737
|
+
setScopeBudget((current) => ({
|
|
738
|
+
...current,
|
|
739
|
+
[scope.id]: event.target.value,
|
|
740
|
+
}))
|
|
741
|
+
}
|
|
742
|
+
placeholder="25"
|
|
743
|
+
/>
|
|
744
|
+
</div>
|
|
745
|
+
<Button
|
|
746
|
+
variant="outline"
|
|
747
|
+
onClick={() => saveScopeBudget(scope)}
|
|
748
|
+
disabled={savingScope === `budget:${scope.id}`}
|
|
749
|
+
>
|
|
750
|
+
{t("messaging.managed.saveBudget")}
|
|
751
|
+
</Button>
|
|
752
|
+
</div>
|
|
753
|
+
</div>
|
|
754
|
+
);
|
|
755
|
+
})}
|
|
756
|
+
</div>
|
|
757
|
+
) : null}
|
|
758
|
+
|
|
480
759
|
<Collapsible className="mt-5">
|
|
481
760
|
<CollapsibleTrigger className="group flex w-full cursor-pointer items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-foreground">
|
|
482
761
|
<IconChevronRight className="h-3.5 w-3.5 transition-transform group-data-[state=open]:rotate-90" />
|
|
@@ -485,7 +764,7 @@ export function MessagingSetupPanel() {
|
|
|
485
764
|
<CollapsibleContent>
|
|
486
765
|
<div className="mt-2 rounded-xl border bg-muted/20 p-4">
|
|
487
766
|
<ol className="space-y-2 text-sm text-muted-foreground">
|
|
488
|
-
{platform.
|
|
767
|
+
{platform.setup.steps.map((step, index) => (
|
|
489
768
|
<li key={step} className="flex gap-2">
|
|
490
769
|
<span className="text-muted-foreground/60">
|
|
491
770
|
{index + 1}.
|
|
@@ -568,7 +847,7 @@ export function MessagingSetupPanel() {
|
|
|
568
847
|
);
|
|
569
848
|
})}
|
|
570
849
|
</div>
|
|
571
|
-
{
|
|
850
|
+
{missingRequiredCredentials ? (
|
|
572
851
|
<Button
|
|
573
852
|
variant="outline"
|
|
574
853
|
onClick={() =>
|
|
@@ -604,7 +883,7 @@ export function MessagingSetupPanel() {
|
|
|
604
883
|
variant="outline"
|
|
605
884
|
size="icon"
|
|
606
885
|
onClick={() => copyWebhook(status.webhookUrl!)}
|
|
607
|
-
aria-label={`Copy ${platform.
|
|
886
|
+
aria-label={`Copy ${platform.name} webhook URL`}
|
|
608
887
|
>
|
|
609
888
|
{copiedWebhook === status.webhookUrl ? (
|
|
610
889
|
<IconCheck className="h-4 w-4" />
|