Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,12 @@ server that `veadk frontend` launches — no separate backend.
input. Changing an authenticated MCP
URL requires the user to enter a replacement Token, explicitly confirm reuse
of the previous credential, or mark the new endpoint as unauthenticated;
Studio never silently replays a credential to a different endpoint. Long descriptions and prompts
Studio never silently replays a credential to a different endpoint. Feishu
App ID and App Secret are restored from the selected Runtime into the masked
update form and remain in the signed-in user's browser draft so a resumed
draft shows the same editable values. Disabling Feishu during an update
removes both Runtime variables; leaving it enabled preserves or replaces
them with the submitted values. Long descriptions and prompts
scroll within bounded editors, while the sidebar stays pinned to the
viewport. On narrow desktop windows, the structure, configuration, and debug
panels stack vertically instead of squeezing the form. The deployment page
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2083,6 +2083,7 @@ export default function App() {
etag?: string;
editMode?: "source-preserving" | "regenerate";
configuredMcpEnvKeys?: string[];
configuredRuntimeEnvKeys?: string[];
} | null>(null);
const [newRuntimeRegion, setNewRuntimeRegion] = useState<string>(
defaultCloudRegion(cloudProvider),
Expand Down Expand Up @@ -6966,6 +6967,10 @@ export default function App() {
const runtimeModel = modelConfigurationFromRuntime(
runtimeAgent.model,
);
const feishuConfigured =
runtimeDraft.deployment?.feishuEnabled === true ||
(runtimeEnv.has("FEISHU_APP_ID") &&
runtimeEnv.has("FEISHU_APP_SECRET"));
const hydratedDraft = hydrateA2aRegistryFromRuntime(
hydrateRuntimeModelSelection(
{
Expand All @@ -6981,6 +6986,7 @@ export default function App() {
...(runtimeDraft.deployment ?? {
feishuEnabled: false,
}),
feishuEnabled: feishuConfigured,
network: capability.runtime.network,
envValues: runtimeEnvValues,
},
Expand Down Expand Up @@ -7037,6 +7043,8 @@ export default function App() {
? "source-preserving"
: "regenerate",
configuredMcpEnvKeys: configuredMcpEnvKeys(classifiedDraft),
configuredRuntimeEnvKeys:
capability.runtime.configuredEnvKeys,
});
setCreateView("custom");
setError("");
Expand Down
23 changes: 18 additions & 5 deletions frontend/src/create/CustomCreate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3593,6 +3593,7 @@ interface CustomCreateProps extends CreateModeProps {
etag?: string;
editMode?: "source-preserving" | "regenerate";
configuredMcpEnvKeys?: string[];
configuredRuntimeEnvKeys?: string[];
};
/** Region selected before entering the create flow. */
initialDeployRegion?: string;
Expand Down Expand Up @@ -4652,10 +4653,15 @@ export function CustomCreate({
? mcpCredentialReuseValues(draft)
: undefined,
removeRuntimeEnvKeys: deploymentTarget
? removedConfiguredMcpEnvKeys(
deploymentTarget.configuredMcpEnvKeys ?? [],
draft,
)
? [
...removedConfiguredMcpEnvKeys(
deploymentTarget.configuredMcpEnvKeys ?? [],
draft,
),
...(!draft.deployment?.feishuEnabled
? ["FEISHU_APP_ID", "FEISHU_APP_SECRET"]
: []),
]
: undefined,
description: draft.description,
harnessSidecar: draft.harnessSidecar,
Expand Down Expand Up @@ -6056,15 +6062,22 @@ export function CustomCreate({
onDeploymentStarted={onDeploymentStarted}
onDeploymentComplete={onDeploymentComplete}
feishuEnabled={!!draft.deployment?.feishuEnabled}
onFeishuEnabledChange={(feishuEnabled) => {
configuredRuntimeEnvKeys={
deploymentTarget?.configuredRuntimeEnvKeys
}
onFeishuEnabledChange={async (feishuEnabled) => {
const nextDraft: AgentDraft = {
...draft,
deployment: {
...(draft.deployment ?? { feishuEnabled: false }),
feishuEnabled,
},
};
const generated = await generateAgentProject(
codegenDraft(nextDraft),
);
setDraft(nextDraft);
setProject(generated);
}}
deploymentEnv={deploymentEnv.specs}
requiredSecretEnv={customModelCredentials}
Expand Down
1 change: 1 addition & 0 deletions frontend/src/create/agentDraftStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface WorkspaceAgentDraft {
etag?: string;
editMode?: "source-preserving" | "regenerate";
configuredMcpEnvKeys?: string[];
configuredRuntimeEnvKeys?: string[];
};
}

Expand Down
1 change: 1 addition & 0 deletions frontend/src/create/veadkCatalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ export const FEISHU_ENV: EnvVar[] = [
required: true,
placeholder: "输入 App Secret",
comment: "飞书应用 App Secret",
secret: true,
},
];

Expand Down
21 changes: 16 additions & 5 deletions frontend/src/ui/ProjectPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ import {
} from "../create/veadkCatalog";
import {
firstInvalidRuntimeEnv,
firstMissingRuntimeEnv,
missingRuntimeEnvs,
runtimeEnvDisplayRows,
runtimeEnvJsonError,
Expand Down Expand Up @@ -695,6 +694,8 @@ export interface ProjectPreviewProps {
feishuEnabled?: boolean;
/** Update the Feishu channel selection from the deploy page. */
onFeishuEnabledChange?: (enabled: boolean) => void | Promise<void>;
/** Runtime keys whose values remain configured but are not returned to the browser. */
configuredRuntimeEnvKeys?: readonly string[];
/** Environment variables required by the selected memory/knowledge backends. */
deploymentEnv?: RuntimeEnvSpec[];
/** Required deployment secrets kept only in this mounted publish page. */
Expand Down Expand Up @@ -840,6 +841,7 @@ export function ProjectPreview({
onDeploymentTaskChange,
feishuEnabled = false,
onFeishuEnabledChange,
configuredRuntimeEnvKeys = [],
deploymentEnv = [],
requiredSecretEnv = [],
requiredSecretEnvValues,
Expand All @@ -864,6 +866,10 @@ export function ProjectPreview({
}: ProjectPreviewProps) {
const editable = typeof onChange === "function";
const isRuntimeUpdate = Boolean(deploymentRuntimeId);
const configuredRuntimeEnvKeySet = useMemo(
() => new Set(configuredRuntimeEnvKeys),
[configuredRuntimeEnvKeys],
);
const inMemorySession = usesInMemorySession(agentDraft);
const runtimeNameSource =
agentName?.trim() || agentDraft?.name || project.name;
Expand Down Expand Up @@ -1505,9 +1511,10 @@ export function ProjectPreview({
return;
}
if (feishuEnabled) {
const missingFeishuEnv = firstMissingRuntimeEnv(
FEISHU_ENV,
deploymentEnvValues,
const missingFeishuEnv = FEISHU_ENV.find(
(env) =>
!String(deploymentEnvValues[env.key] ?? "").trim() &&
!configuredRuntimeEnvKeySet.has(env.key),
);
if (missingFeishuEnv) {
const env = FEISHU_ENV.find((item) => item.key === missingFeishuEnv.key);
Expand Down Expand Up @@ -2628,7 +2635,11 @@ export function ProjectPreview({
env.key.includes("SECRET") ? "password" : "text"
}
value={deploymentEnvValues[env.key] ?? ""}
placeholder={env.placeholder}
placeholder={
configuredRuntimeEnvKeySet.has(env.key)
? "已配置,留空沿用"
: env.placeholder
}
tabIndex={feishuEnabled ? 0 : -1}
disabled={
!feishuEnabled ||
Expand Down
135 changes: 134 additions & 1 deletion frontend/tests/agentDraftStorage.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ function memoryStorage(initial = {}) {
};
}

test("keeps MCP credentials ephemeral while preserving non-MCP deployment values", () => {
test("keeps MCP credentials ephemeral while preserving deployment values", () => {
const sourceDraft = draft({
mcpTools: [{ name: "root", transport: "http", authToken: "root-secret" }],
deployment: { feishuEnabled: true, envValues: { FEISHU_APP_SECRET: "secret" } },
Expand Down Expand Up @@ -108,6 +108,139 @@ test("keeps MCP credentials ephemeral while preserving non-MCP deployment values
);
});

test("persists every editable draft property including Feishu credentials", () => {
const storage = memoryStorage();
const completeDraft = draft({
description: "complete description",
instruction: "complete instruction",
dynamicAgentDelegation: true,
agentType: "llm",
cloudProvider: "byteplus",
maxIterations: 7,
a2aUrl: "https://agent.example.com",
model: "legacy-model",
modelSource: "custom",
modelName: "custom-model",
modelProvider: "openai",
modelApiBase: "https://model.example.com/v1",
tools: ["legacy-tool"],
skills: ["legacy-skill"],
memory: { shortTerm: true, longTerm: true },
knowledgebase: true,
tracing: true,
builtinTools: ["web_search"],
customTools: [{ name: "lookup", description: "lookup records" }],
mcpTools: [
{
name: "orders",
transport: "http",
url: "https://mcp.example.com/mcp",
authTokenEnv: "MCP_ORDERS_TOKEN",
credentialConfigured: true,
credentialSourceUrl: "https://mcp.example.com/mcp",
credentialSourceAuthTokenEnv: "MCP_ORDERS_TOKEN",
},
],
a2aRegistry: {
enabled: true,
registrySpaceId: "space-1",
registryTopK: "5",
registryRegion: "ap-southeast-1",
registryEndpoint: "https://registry.example.com",
},
shortTermBackend: "redis",
longTermBackend: "viking",
longTermMemoryIndex: "memory-index",
autoSaveSession: true,
knowledgebaseBackend: "viking",
knowledgebaseIndex: "knowledge-index",
tracingExporters: ["tls"],
selectedSkills: [
{
source: "runtime",
folder: "ops",
name: "ops",
description: "operations",
},
],
cloudEnvironment: {
environmentId: "environment-1",
environmentVersionId: "version-2",
cliTools: ["lark-cli"],
dockerfile: "RUN echo ready",
},
harnessSidecar: {
enabled: true,
profile: "default",
componentOverrides: {
context_engine: true,
compressor: false,
verifier: true,
long_run_control: false,
mcp_resilience: true,
},
catalogVersion: "catalog-1",
planHash: "sha256:plan",
},
deployment: {
feishuEnabled: true,
runtimeName: "runtime-name",
runtimeNameCustomized: true,
network: {
mode: "both",
vpcId: "vpc-1",
subnetIds: "subnet-1,subnet-2",
enableSharedInternetAccess: true,
},
modelApiKeyId: "key-id",
modelApiKeyName: "key-name",
envValues: {
FEISHU_APP_ID: "cli_test",
FEISHU_APP_SECRET: "persisted-feishu-secret",
CUSTOM_SETTING: "custom-value",
},
},
});

writeWorkspaceDrafts(storage, "complete-builder", [
{
id: "complete-draft",
updatedAt: 123,
creationMode: "quick",
deploymentTarget: {
runtimeId: "runtime-1",
name: "runtime-name",
region: "ap-southeast-1",
appName: "complete_app",
currentVersion: 3,
etag: "etag-1",
editMode: "source-preserving",
configuredMcpEnvKeys: ["MCP_ORDERS_TOKEN"],
configuredRuntimeEnvKeys: ["OPAQUE_RUNTIME_SECRET"],
},
draft: completeDraft,
},
]);

const [loaded] = loadWorkspaceDrafts(storage, "complete-builder");
assert.equal(loaded.creationMode, "quick");
assert.deepEqual(loaded.deploymentTarget, {
runtimeId: "runtime-1",
name: "runtime-name",
region: "ap-southeast-1",
appName: "complete_app",
currentVersion: 3,
etag: "etag-1",
editMode: "source-preserving",
configuredMcpEnvKeys: ["MCP_ORDERS_TOKEN"],
configuredRuntimeEnvKeys: ["OPAQUE_RUNTIME_SECRET"],
});
const expectedDraft = structuredClone(completeDraft);
delete expectedDraft.mcpTools[0].credentialSourceUrl;
delete expectedDraft.mcpTools[0].credentialSourceAuthTokenEnv;
assert.deepEqual(loaded.draft, expectedDraft);
});

test("writes a versioned user-scoped payload without transient MCP values", () => {
const storage = memoryStorage();
writeWorkspaceDrafts(storage, "alice@example.com", [
Expand Down
8 changes: 8 additions & 0 deletions frontend/tests/agentWorkspace.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,10 @@ test("runtime updates use the Agent selected in management instead of the active
/capability\.runtime\.envs[\s\S]*?filter\(\(\{ key \}\) => !isRuntimeModelSelectionEnv\(key\)\)[\s\S]*?\.map/,
);
assert.match(handler, /envValues:\s*runtimeEnvValues/);
assert.match(
handler,
/runtimeEnv\.has\("FEISHU_APP_ID"\)[\s\S]*?runtimeEnv\.has\("FEISHU_APP_SECRET"\)/,
);
assert.doesNotMatch(handler, /draftEnvValues|selectedAgentUpdateDraft\?\.draft/);
assert.match(handler, /hydrateRuntimeModelSelection\(/);
assert.match(
Expand All @@ -743,6 +747,10 @@ test("runtime updates use the Agent selected in management instead of the active
assert.match(handler, /network:\s*capability\.runtime\.network/);
assert.match(handler, /etag:\s*capability\.etag/);
assert.match(handler, /editMode:\s*capability\.editMode/);
assert.match(
handler,
/configuredRuntimeEnvKeys:\s*capability\.runtime\.configuredEnvKeys/,
);
assert.match(
handler,
/setRuntimeUpdateTarget\(\{[\s\S]*?runtimeId:\s*capability\.runtime\.runtimeId,[\s\S]*?currentVersion:\s*capability\.runtime\.currentVersion,[\s\S]*?etag:\s*capability\.etag/,
Expand Down
24 changes: 21 additions & 3 deletions frontend/tests/deploymentEnv.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -790,12 +790,11 @@ test("shows configured database and Feishu values in the runtime env summary", (
]);
});

test("keeps the generated project stable when only deployment channel settings change", () => {
test("regenerates the project when deployment channel settings change", () => {
assert.match(
customCreateSource,
/onFeishuEnabledChange=\{\(feishuEnabled\) => \{[\s\S]*?setDraft\(nextDraft\);/,
/onFeishuEnabledChange=\{async \(feishuEnabled\) => \{[\s\S]*?generateAgentProject\([\s\S]*?codegenDraft\(nextDraft\)[\s\S]*?setDraft\(nextDraft\);[\s\S]*?setProject\(generated\);/,
);
assert.doesNotMatch(customCreateSource, /buildPreviewProject/);
assert.match(
customCreateSource,
/const releaseDraft = releaseVariant[\s\S]*?releaseDraftFromDebugVariant\(providerDraft, releaseVariant\)[\s\S]*?generateAgentProject\(codegenDraft\(releaseDraft\)\)/,
Expand All @@ -804,6 +803,25 @@ test("keeps the generated project stable when only deployment channel settings c
assert.match(projectPreviewSource, /deploying \|\| feishuUpdating/);
});

test("restores Feishu credentials into Runtime updates and reuses opaque values", () => {
assert.match(
projectPreviewSource,
/value=\{deploymentEnvValues\[env\.key\] \?\? ""\}/,
);
assert.match(
projectPreviewSource,
/configuredRuntimeEnvKeySet\.has\(env\.key\)[\s\S]*?已配置,留空沿用/,
);
assert.match(
customCreateSource,
/deploymentEnvValues=\{\{[\s\S]*?\.\.\.providerDraft\.deployment\?\.envValues/,
);
assert.match(
customCreateSource,
/removedConfiguredMcpEnvKeys\([\s\S]*?FEISHU_APP_ID[\s\S]*?FEISHU_APP_SECRET/,
);
});

test("normalizes generated project drafts to the selected cloud provider", () => {
assert.match(customCreateSource, /function draftForCloudProvider/);
assert.match(
Expand Down
Loading
Loading