Skip to content

Commit 812faad

Browse files
moidreamzhangmengqiu.zmq
andauthored
feat(mpa): complete Studio details and managed account provisioning (#1160)
* feat(mpa): harden managed account provisioning * fix(studio): complete upstream MPA details integration * feat(studio): add multi-bot channels and account Ark key discovery Resolve existing Ark model keys with deployment credentials and select the newest eligible key by creation time. Add capability-gated Feishu bot account management and scheduled-task delivery selection, retaining legacy single-bot behavior. Include bilingual contracts, regression tests, the current lockfile changes, and rebuilt Studio assets. Pre-commit, Pyright and frontend gates pass. The Python repository regression has 6933 passing tests and one Markdown MIME failure reproduced on the unmodified baseline; record the result in the paired designs. * feat(mpa): support BytePlus creation and recover failed PG bootstrap * fix(frontend): restore complete esbuild lock entries --------- Co-authored-by: zhangmengqiu.zmq <zhangmengqiu.zmq@bytedance.com>
1 parent 171d8d8 commit 812faad

187 files changed

Lines changed: 9920 additions & 939 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎frontend/README.md‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1824,3 +1824,31 @@ MPA creation defaults to A2A discovery (`ENABLE_A2A=true`, `DISABLE_JWT_AUTH=fal
18241824
MPA name validation highlights invalid input and its message in the theme danger color, offers a valid-name placeholder, and visibly disables Next until corrected. Shared Button disabled states are dimmed consistently; loading retains its own appearance.
18251825

18261826
MPA 名称校验使用主题错误色显示非法输入及错误提示,并提供合法名称示例;名称修正前“下一步”明显置灰。共享 Button 的禁用态统一降低透明度,加载态保留原有外观。
1827+
1828+
MPA Agent details currently expose only Basic information. Other tabs and the Profile configuration action are hidden; this page does not query Profile status or submit Profile operations. General Agent pages and chat session protocols are unchanged.
1829+
1830+
MPA 智能体详情当前仅显示基本信息。其他页面及 Profile 配置操作隐藏;本页不查询 Profile 状态或提交 Profile 操作。普通智能体页面与聊天会话协议保持不变。
1831+
1832+
### MPA Feishu multi-bot accounts / MPA 飞书多机器人
1833+
1834+
Runtimes advertising `multiBotChannels: ["feishu"]` expose a robot selector in
1835+
Automations → Messaging channels → MPA agent messaging channels. Add bots with
1836+
QR or manual binding, then manage enablement, unbinding, diagnostics and group
1837+
permissions for the selected `appId`. Adding a different bot preserves existing
1838+
accounts. Feishu scheduled tasks select an enabled delivery bot; switching to
1839+
Web delivery removes `delivery.appId`. Channel management requires Studio
1840+
administrator access. A failed or unauthorized account lookup blocks Feishu
1841+
task submission instead of selecting an arbitrary bot. Older runtimes keep the
1842+
single-bot interface. Compatible MPA images must be deployed separately.
1843+
1844+
Runtime 声明 `multiBotChannels: ["feishu"]` 后,可在“自动化 → 消息渠道 →
1845+
MPA 智能体消息渠道”中选择机器人。扫码或手动添加后,启停、解绑、诊断和群权限
1846+
均按选中的 `appId` 管理;新增不同机器人保留既有绑定。飞书定时任务须选择已启用
1847+
的投递机器人,切换 Web 投递会清除 `delivery.appId`。渠道管理需要 Studio 管理员
1848+
权限;账号读取失败或无权限时阻止飞书任务提交,不任意选择机器人。旧 Runtime
1849+
保持单机器人界面,兼容的 MPA 镜像需另行部署。
1850+
1851+
Contracts and verification / 契约与验证:
1852+
[channels](../specs/mpa-channels/README.md),
1853+
[tasks](../specs/studio-mpa-cron-tasks/README.md),
1854+
[design](../prd-spec/features/mpa-feishu-multi-bot/2026-10-09-studio-multi-bot.md).

‎frontend/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"dev": "vite",
99
"build": "node scripts/build.mjs",
1010
"test": "node --test tests/*.test.mjs && npm run test:mpa-agent-info",
11-
"test:mpa-agent-info": "vitest run tests/mpaAgentInfoRail.test.tsx tests/skillDocumentEditor.test.tsx tests/mpaCreation.test.tsx",
11+
"test:mpa-agent-info": "vitest run tests/mpaAgentInfoRail.test.tsx tests/skillDocumentEditor.test.tsx tests/mpaCreation.test.tsx tests/mpaBasicDetails.test.tsx",
1212
"check:i18n": "node scripts/check-i18n.mjs",
1313
"test:sandbox-download-coverage": "vitest run --coverage -c vitest.sandbox-download.config.ts",
1414
"test:mpa-cron-coverage": "vitest run --coverage -c vitest.mpa-cron.config.ts",

‎frontend/server/mpa_creation.py‎

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,22 +37,27 @@
3737
ConfigurationError,
3838
load_studio_profile,
3939
validate_creation_resources,
40+
validate_creation_tos,
4041
validate_image_reference,
4142
validate_runtime_name,
42-
validate_creation_tos,
4343
with_creation_images,
4444
with_creation_resources,
4545
with_creation_tos,
4646
)
47-
from veadk.integrations.mpa.managed.credentials import load_volcengine_credentials
47+
from veadk.integrations.mpa.managed.credentials import (
48+
load_provider_credentials,
49+
load_volcengine_credentials,
50+
)
4851
from veadk.integrations.mpa.managed.tasks import CreationTasks, TaskError, owner_key
4952

5053

51-
def _studio_task_path() -> Path:
54+
def _studio_task_path(provider="volcengine") -> Path:
5255
return Path(
5356
os.getenv(
5457
"VEADK_MPA_TASK_DB",
55-
"/tmp/veadk-studio/mpa-creation.sqlite3",
58+
"/tmp/veadk-studio/mpa-creation-byteplus.sqlite3"
59+
if provider == "byteplus"
60+
else "/tmp/veadk-studio/mpa-creation.sqlite3",
5661
)
5762
)
5863

@@ -63,7 +68,7 @@ class CreationRequest(BaseModel):
6368
agentId: str = Field(default="", pattern=r"^[a-z0-9][a-z0-9_-]{0,63}$")
6469
name: str = ""
6570
description: str = Field(default="", max_length=512)
66-
region: str = Field(pattern=r"^cn-[a-z]+$", max_length=32)
71+
region: str = Field(pattern=r"^(cn-[a-z]+|ap-southeast-1)$", max_length=32)
6772
runtimeImage: str = Field(default="", max_length=1024)
6873
workerImage: str = Field(default="", max_length=1024)
6974
pgHost: str = Field(default="", max_length=255)
@@ -95,26 +100,36 @@ def validate_resources(self):
95100

96101

97102
def mount_mpa_creation_routes(
98-
app: FastAPI, *, owner, service: CreationTasks | None = None, supported=True
103+
app: FastAPI,
104+
*,
105+
owner,
106+
service: CreationTasks | None = None,
107+
supported=True,
108+
provider="volcengine",
99109
):
100110
tasks = service
101111

102112
def get_tasks():
103113
nonlocal tasks
104114
if tasks is None:
105-
task_path = _studio_task_path()
115+
task_path = _studio_task_path(provider)
106116
tasks = CreationTasks(task_path)
107117
return tasks
108118

109119
def profile(region):
110120
if not supported:
111-
raise ConfigurationError("MPA creation requires the Volcengine provider")
112-
result = load_studio_profile(region=region)
121+
raise ConfigurationError("MPA creation is unavailable for this provider")
122+
result = load_studio_profile(region=region, provider=provider)
113123
try:
114-
load_volcengine_credentials(result.managed.credential_file)
124+
if provider == "volcengine":
125+
load_volcengine_credentials(result.managed.credential_file)
126+
else:
127+
load_provider_credentials(provider, result.managed.credential_file)
115128
except ValueError:
116129
raise ConfigurationError(
117-
"Configure server deployment credentials"
130+
"Configure server BytePlus deployment credentials (BYTEPLUS_ACCESS_KEY/BYTEPLUS_SECRET_KEY or VEADK_MPA_BYTEPLUS_CREDENTIAL_FILE)"
131+
if provider == "byteplus"
132+
else "Configure server deployment credentials"
118133
) from None
119134
return None, result
120135

@@ -143,6 +158,8 @@ async def create(request: Request):
143158
try:
144159
path, config = profile(body.region)
145160
payload = body.model_dump(mode="json")
161+
if provider == "byteplus":
162+
payload["provider"] = provider
146163
if not payload["agentId"]:
147164
payload["agentId"] = (
148165
"mi-"

‎frontend/src/adk/client.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6566,19 +6566,19 @@ export async function updateSandboxTool(kind: SandboxToolKind): Promise<{
65666566

65676567

65686568
export type ChannelScope = "group" | "group_sender" | "group_topic" | "group_topic_sender";
6569-
export interface ChannelCapabilities { channels?: string[]; credentialBindingChannels?: string[]; serverSideBinding: boolean; bindingReady: boolean; bindingError?: string | null; }
6569+
export interface ChannelCapabilities { channels?: string[]; credentialBindingChannels?: string[]; serverSideBinding: boolean; bindingReady: boolean; bindingError?: string | null; multiBotChannels?: string[]; accountScopedPermissions?: boolean; }
65706570
export interface FeishuBinding {
65716571
id: string;
65726572
status: "PENDING" | "SCANNED" | "AUTHORIZED" | "REGISTERING" | "BOUND" | "FAILED" | "EXPIRED";
65736573
loginUrl: string; qrCodeImage: string; expiresAt: string; pollAfterSeconds: number;
65746574
appId?: string | null; lastErrorCode?: string | null; lastErrorMessage?: string | null;
65756575
}
65766576
export interface ChannelDiagnostics {
6577-
configured: boolean; appId?: string | null; appName?: string | null;
6577+
configured: boolean; enabled?: boolean; appId?: string | null; appName?: string | null;
65786578
gatewayConfigured: boolean; routeConfigured: boolean; missingConfiguration: string[];
65796579
deliveryHealth: string; lastInboundAt?: number | null; lastDeliveryAt?: number | null;
65806580
}
6581-
export interface ChannelPermission { channel: string; chatId: string; chatName?: string; groupSessionScope?: ChannelScope | null; }
6581+
export interface ChannelPermission { appId?: string; channel: string; chatId: string; chatName?: string; groupSessionScope?: ChannelScope | null; }
65826582
export class ChannelApiError extends Error {
65836583
constructor(public status: number) { super(`Channel request failed (${status})`); }
65846584
}

‎frontend/src/adk/feishuAccounts.ts‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { channelRequest, ChannelApiError, type AdkEndpoint } from "./client";
2+
3+
export interface FeishuAccount {
4+
channel: "feishu";
5+
appId: string;
6+
appName?: string;
7+
enabled: boolean;
8+
}
9+
10+
export async function listFeishuAccounts(
11+
ep: AdkEndpoint,
12+
signal: AbortSignal,
13+
): Promise<FeishuAccount[]> {
14+
const data = await channelRequest<{ channels: unknown[] }>(ep, "", {
15+
signal,
16+
});
17+
if (!data || !Array.isArray(data.channels)) throw new ChannelApiError(502);
18+
const accounts: FeishuAccount[] = [];
19+
const ids = new Set<string>();
20+
for (const value of data.channels) {
21+
if (!value || typeof value !== "object" || !("channel" in value))
22+
throw new ChannelApiError(502);
23+
if (value.channel !== "feishu") continue;
24+
if (
25+
!("appId" in value) ||
26+
typeof value.appId !== "string" ||
27+
!value.appId.trim() ||
28+
ids.has(value.appId)
29+
)
30+
throw new ChannelApiError(502);
31+
if (!("enabled" in value) || typeof value.enabled !== "boolean")
32+
throw new ChannelApiError(502);
33+
ids.add(value.appId);
34+
accounts.push({
35+
channel: "feishu",
36+
appId: value.appId,
37+
enabled: value.enabled,
38+
...("appName" in value && typeof value.appName === "string"
39+
? { appName: value.appName }
40+
: {}),
41+
});
42+
}
43+
return accounts;
44+
}

‎frontend/src/cronjobs/MpaCronTasks.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,7 @@ function RuntimeTasks({ runtime }: { runtime: MpaRuntime }) {
560560
)}
561561
{editor && (
562562
<MpaTaskEditor
563+
runtime={runtime}
563564
task={editor.task}
564565
copy={editor.copy}
565566
busy={busy}

‎frontend/src/cronjobs/MpaTaskEditor.tsx‎

Lines changed: 129 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,29 @@
1-
import { useRef, useState } from "react";
1+
import { useEffect, useRef, useState } from "react";
22
import { useTranslation } from "react-i18next";
33
import { DialogShell } from "../ui/SandboxControls";
4+
import { Select } from "../components/primitives/Select/Select";
45
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";
613
import { scheduleTypes } from "./mpaSchedule";
714
import { MpaIcon } from "./MpaTaskIcons";
815

916
export function MpaTaskEditor({
1017
task,
18+
runtime,
1119
copy,
1220
busy,
1321
error,
1422
onClose,
1523
onSave,
1624
}: {
1725
task?: MpaCronTask;
26+
runtime?: MpaRuntime;
1827
copy: boolean;
1928
busy: boolean;
2029
error: string;
@@ -53,11 +62,75 @@ export function MpaTaskEditor({
5362
String(task?.delivery?.channel || "Web"),
5463
);
5564
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]);
56122
const [validation, setValidation] = useState("");
57123
const composing = useRef(false);
58124
const nameRef = useRef<HTMLInputElement>(null);
59125
function save() {
60126
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+
}
61134
try {
62135
new Intl.DateTimeFormat("en-US", { timeZone: zone });
63136
} catch {
@@ -115,10 +188,15 @@ export function MpaTaskEditor({
115188
}
116189
const delivery =
117190
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)
119196
? task.delivery
120197
: {
121198
channel,
199+
...(channel === "Feishu" && multiBot ? { appId: botId } : {}),
122200
targetId: channel === "Web" ? null : target.trim(),
123201
receiveIdType: "chat_id",
124202
bestEffort: true,
@@ -296,6 +374,48 @@ export function MpaTaskEditor({
296374
onChange={setChannel}
297375
/>
298376
</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+
)}
299419
{channel === "Feishu" && (
300420
<label>
301421
{label("target")}
@@ -334,7 +454,12 @@ export function MpaTaskEditor({
334454
</button>
335455
<button
336456
className="cw-btn cw-btn-primary is-primary"
337-
disabled={busy}
457+
disabled={
458+
busy ||
459+
(channel === "Feishu" &&
460+
!!runtime &&
461+
(botsLoading || !!botsError))
462+
}
338463
type="submit"
339464
>
340465
{label(busy ? "saving" : "save")}

0 commit comments

Comments
 (0)