|
1 | | -import { useRef, useState } from "react"; |
| 1 | +import { useEffect, useRef, useState } from "react"; |
2 | 2 | import { useTranslation } from "react-i18next"; |
3 | 3 | import { DialogShell } from "../ui/SandboxControls"; |
| 4 | +import { Select } from "../components/primitives/Select/Select"; |
4 | 5 | import { DeploymentSelect } from "../ui/DeploymentSelect"; |
5 | | -import type { MpaCronTask, TaskFields } from "../adk/mpaCronTasks"; |
| 6 | +import { |
| 7 | + channelRequest, |
| 8 | + ChannelApiError, |
| 9 | + type ChannelCapabilities, |
| 10 | +} from "../adk/client"; |
| 11 | +import { listFeishuAccounts, type FeishuAccount } from "../adk/feishuAccounts"; |
| 12 | +import type { MpaRuntime, MpaCronTask, TaskFields } from "../adk/mpaCronTasks"; |
6 | 13 | import { scheduleTypes } from "./mpaSchedule"; |
7 | 14 | import { MpaIcon } from "./MpaTaskIcons"; |
8 | 15 |
|
9 | 16 | export function MpaTaskEditor({ |
10 | 17 | task, |
| 18 | + runtime, |
11 | 19 | copy, |
12 | 20 | busy, |
13 | 21 | error, |
14 | 22 | onClose, |
15 | 23 | onSave, |
16 | 24 | }: { |
17 | 25 | task?: MpaCronTask; |
| 26 | + runtime?: MpaRuntime; |
18 | 27 | copy: boolean; |
19 | 28 | busy: boolean; |
20 | 29 | error: string; |
@@ -53,11 +62,75 @@ export function MpaTaskEditor({ |
53 | 62 | String(task?.delivery?.channel || "Web"), |
54 | 63 | ); |
55 | 64 | const [target, setTarget] = useState(String(task?.delivery?.targetId || "")); |
| 65 | + const [botId, setBotId] = useState(String(task?.delivery?.appId || "")); |
| 66 | + const [bots, setBots] = useState<FeishuAccount[]>([]); |
| 67 | + const [multiBot, setMultiBot] = useState(false); |
| 68 | + const [botsLoading, setBotsLoading] = useState(false); |
| 69 | + const [botsError, setBotsError] = useState(""); |
| 70 | + const [botsReload, setBotsReload] = useState(0); |
| 71 | + useEffect(() => { |
| 72 | + if (!runtime || channel !== "Feishu") { |
| 73 | + setBotsLoading(false); |
| 74 | + return; |
| 75 | + } |
| 76 | + const controller = new AbortController(); |
| 77 | + const ep = { runtimeId: runtime.runtimeId, region: runtime.region }; |
| 78 | + setBotsLoading(true); |
| 79 | + setBotsError(""); |
| 80 | + void (async () => { |
| 81 | + let caps: ChannelCapabilities; |
| 82 | + try { |
| 83 | + caps = await channelRequest<ChannelCapabilities>(ep, "/capabilities", { |
| 84 | + signal: controller.signal, |
| 85 | + }); |
| 86 | + } catch (reason) { |
| 87 | + if (reason instanceof ChannelApiError && reason.status === 404) { |
| 88 | + if (!controller.signal.aborted) setMultiBot(false); |
| 89 | + return; |
| 90 | + } |
| 91 | + throw reason; |
| 92 | + } |
| 93 | + if (controller.signal.aborted) return; |
| 94 | + const multiple = !!caps.multiBotChannels?.includes("feishu"); |
| 95 | + setMultiBot(multiple); |
| 96 | + if (!multiple) return; |
| 97 | + const accounts = await listFeishuAccounts(ep, controller.signal); |
| 98 | + if (controller.signal.aborted) return; |
| 99 | + setBots(accounts); |
| 100 | + setBotId( |
| 101 | + (current) => |
| 102 | + current || |
| 103 | + (accounts.length === 1 && accounts[0].enabled |
| 104 | + ? accounts[0].appId |
| 105 | + : ""), |
| 106 | + ); |
| 107 | + })() |
| 108 | + .catch((reason) => { |
| 109 | + if (!controller.signal.aborted) |
| 110 | + setBotsError( |
| 111 | + reason instanceof ChannelApiError && |
| 112 | + [401, 403].includes(reason.status) |
| 113 | + ? "botsUnauthorized" |
| 114 | + : "botsFailed", |
| 115 | + ); |
| 116 | + }) |
| 117 | + .finally(() => { |
| 118 | + if (!controller.signal.aborted) setBotsLoading(false); |
| 119 | + }); |
| 120 | + return () => controller.abort(); |
| 121 | + }, [runtime?.runtimeId, runtime?.region, channel, botsReload]); |
56 | 122 | const [validation, setValidation] = useState(""); |
57 | 123 | const composing = useRef(false); |
58 | 124 | const nameRef = useRef<HTMLInputElement>(null); |
59 | 125 | function save() { |
60 | 126 | if (busy || composing.current) return; |
| 127 | + if (channel === "Feishu" && runtime) { |
| 128 | + if (botsLoading || botsError) return; |
| 129 | + if (multiBot && !bots.some((bot) => bot.appId === botId && bot.enabled)) { |
| 130 | + setValidation(label("selectBot")); |
| 131 | + return; |
| 132 | + } |
| 133 | + } |
61 | 134 | try { |
62 | 135 | new Intl.DateTimeFormat("en-US", { timeZone: zone }); |
63 | 136 | } catch { |
@@ -115,10 +188,15 @@ export function MpaTaskEditor({ |
115 | 188 | } |
116 | 189 | const delivery = |
117 | 190 | task?.delivery?.channel === channel && |
118 | | - String(task.delivery.targetId || "") === target |
| 191 | + String(task.delivery.targetId || "") === target && |
| 192 | + (channel !== "Feishu" || |
| 193 | + !multiBot || |
| 194 | + String(task.delivery.appId || "") === botId) && |
| 195 | + !(channel === "Web" && task.delivery.appId) |
119 | 196 | ? task.delivery |
120 | 197 | : { |
121 | 198 | channel, |
| 199 | + ...(channel === "Feishu" && multiBot ? { appId: botId } : {}), |
122 | 200 | targetId: channel === "Web" ? null : target.trim(), |
123 | 201 | receiveIdType: "chat_id", |
124 | 202 | bestEffort: true, |
@@ -296,6 +374,48 @@ export function MpaTaskEditor({ |
296 | 374 | onChange={setChannel} |
297 | 375 | /> |
298 | 376 | </div> |
| 377 | + {channel === "Feishu" && runtime && ( |
| 378 | + <> |
| 379 | + {botsLoading && <p role="status">{label("botsLoading")}</p>} |
| 380 | + {botsError && ( |
| 381 | + <div role="alert"> |
| 382 | + <p>{label(botsError)}</p> |
| 383 | + <button |
| 384 | + type="button" |
| 385 | + className="cw-btn cw-btn-ghost" |
| 386 | + disabled={busy || botsLoading} |
| 387 | + onClick={() => setBotsReload((value) => value + 1)} |
| 388 | + > |
| 389 | + {label("retryBots")} |
| 390 | + </button> |
| 391 | + </div> |
| 392 | + )} |
| 393 | + {multiBot && ( |
| 394 | + <div> |
| 395 | + <span>{label("botAccount")}</span> |
| 396 | + <Select |
| 397 | + aria-label={label("botAccount")} |
| 398 | + value={botId} |
| 399 | + disabled={busy || botsLoading || !!botsError} |
| 400 | + options={[ |
| 401 | + { value: "", label: label("selectBot") }, |
| 402 | + ...bots.map((bot) => ({ |
| 403 | + value: bot.appId, |
| 404 | + label: `${bot.appName || bot.appId} | ${bot.appId}${bot.enabled ? "" : ` | ${label("botDisabled")}`}`, |
| 405 | + disabled: !bot.enabled, |
| 406 | + })), |
| 407 | + ]} |
| 408 | + onChange={(event) => setBotId(event.target.value)} |
| 409 | + /> |
| 410 | + {!botsLoading && |
| 411 | + !botsError && |
| 412 | + !bots.some( |
| 413 | + (bot) => bot.appId === botId && bot.enabled, |
| 414 | + ) && <p role="status">{label("selectBot")}</p>} |
| 415 | + </div> |
| 416 | + )} |
| 417 | + </> |
| 418 | + )} |
299 | 419 | {channel === "Feishu" && ( |
300 | 420 | <label> |
301 | 421 | {label("target")} |
@@ -334,7 +454,12 @@ export function MpaTaskEditor({ |
334 | 454 | </button> |
335 | 455 | <button |
336 | 456 | className="cw-btn cw-btn-primary is-primary" |
337 | | - disabled={busy} |
| 457 | + disabled={ |
| 458 | + busy || |
| 459 | + (channel === "Feishu" && |
| 460 | + !!runtime && |
| 461 | + (botsLoading || !!botsError)) |
| 462 | + } |
338 | 463 | type="submit" |
339 | 464 | > |
340 | 465 | {label(busy ? "saving" : "save")} |
|
0 commit comments