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
182 changes: 122 additions & 60 deletions frontend/src/create/CustomCreate.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import {
type CSSProperties,
type ComponentType,
Fragment,
lazy,
type ReactNode,
Suspense,
useEffect,
useMemo,
Expand Down Expand Up @@ -506,10 +508,12 @@ function RuntimeEnvFields({
env,
values,
onChange,
renderAfterField,
}: {
env: EnvVar[];
values: Record<string, string>;
onChange: (key: string, value: string) => void;
renderAfterField?: (item: EnvVar) => ReactNode;
}) {
if (env.length === 0) {
return <p className="cw-env-empty">此后端无需额外运行参数。</p>;
Expand All @@ -521,73 +525,117 @@ function RuntimeEnvFields({
const jsonError = runtimeEnvJsonError(item, values);
const controlId = `cw-env-${item.key}`;
return (
<label className="cw-env-field" key={item.key} htmlFor={controlId}>
<span className="cw-env-field-head">
<span className="cw-env-field-title">
<span className="cw-env-field-label">
{item.comment || item.key}
{item.required && <span className="cw-req">*</span>}
</span>
{item.help && (
<span
className="cw-env-help"
tabIndex={0}
data-help={item.help}
aria-label={`${item.comment || item.key}说明:${item.help}`}
>
?
<span className="cw-env-help-popover" role="tooltip">
{item.help}
</span>
<Fragment key={item.key}>
<label className="cw-env-field" htmlFor={controlId}>
<span className="cw-env-field-head">
<span className="cw-env-field-title">
<span className="cw-env-field-label">
{item.comment || item.key}
{item.required && <span className="cw-req">*</span>}
</span>
)}
{item.link && (
<a
className="cw-env-link"
href={item.link.url}
target="_blank"
rel="noopener noreferrer"
title={`打开 OpenViking ${item.link.label}`}
aria-label={`打开 OpenViking ${item.link.label}`}
onClick={(event) => event.stopPropagation()}
>
<ExternalLink aria-hidden="true" />
</a>
)}
{item.help && (
<span
className="cw-env-help"
tabIndex={0}
data-help={item.help}
aria-label={`${item.comment || item.key}说明:${item.help}`}
>
?
<span className="cw-env-help-popover" role="tooltip">
{item.help}
</span>
</span>
)}
{item.link && (
<a
className="cw-env-link"
href={item.link.url}
target="_blank"
rel="noopener noreferrer"
title={`打开 OpenViking ${item.link.label}`}
aria-label={`打开 OpenViking ${item.link.label}`}
onClick={(event) => event.stopPropagation()}
>
<ExternalLink aria-hidden="true" />
</a>
)}
</span>
{item.comment && <code title={item.key}>{item.key}</code>}
</span>
{item.comment && <code title={item.key}>{item.key}</code>}
</span>
{item.multiline || item.format === "json" ? (
<textarea
id={controlId}
className="cw-input cw-env-textarea"
value={value}
placeholder={item.placeholder || "请输入参数值"}
autoComplete="off"
spellCheck={false}
aria-invalid={!!jsonError}
onChange={(event) => onChange(item.key, event.currentTarget.value)}
/>
) : (
<input
id={controlId}
className="cw-input"
type={isSensitiveEnv(item.key) ? "password" : "text"}
value={value}
placeholder={item.placeholder || "请输入参数值"}
autoComplete="off"
aria-invalid={!!jsonError}
onChange={(event) => onChange(item.key, event.currentTarget.value)}
/>
)}
{jsonError && <span className="cw-env-error">{jsonError}</span>}
</label>
{item.multiline || item.format === "json" ? (
<textarea
id={controlId}
className="cw-input cw-env-textarea"
value={value}
placeholder={item.placeholder || "请输入参数值"}
autoComplete="off"
spellCheck={false}
aria-invalid={!!jsonError}
onChange={(event) => onChange(item.key, event.currentTarget.value)}
/>
) : (
<input
id={controlId}
className="cw-input"
type={isSensitiveEnv(item.key) ? "password" : "text"}
value={value}
placeholder={item.placeholder || "请输入参数值"}
autoComplete="off"
aria-invalid={!!jsonError}
onChange={(event) => onChange(item.key, event.currentTarget.value)}
/>
)}
{jsonError && <span className="cw-env-error">{jsonError}</span>}
</label>
{renderAfterField?.(item)}
</Fragment>
);
})}
</div>
);
}

const OPENVIKING_KNOWLEDGE_INDEX_HELP =
"默认值:留空;生成项目时使用 Agent 名自动生成,例如 my_agent_kb。未配置 DATABASE_OPENVIKING_TARGET_URI 时,默认 URI 拼接为 viking://user/{知识库归属 ID,未填则 default}/resources/{资源索引}/;如果填写了 DATABASE_OPENVIKING_TARGET_URI,则直接使用该完整 URI。";

function OpenVikingKnowledgeIndexField({
value,
onChange,
}: {
value: string;
onChange: (index: string) => void;
}) {
const controlId = "cw-openviking-knowledge-index";
return (
<label className="cw-env-field" htmlFor={controlId}>
<span className="cw-env-field-head">
<span className="cw-env-field-title">
<span className="cw-env-field-label">OpenViking 资源索引</span>
<span
className="cw-env-help"
tabIndex={0}
data-help={OPENVIKING_KNOWLEDGE_INDEX_HELP}
aria-label={`OpenViking 资源索引说明:${OPENVIKING_KNOWLEDGE_INDEX_HELP}`}
>
?
<span className="cw-env-help-popover" role="tooltip">
{OPENVIKING_KNOWLEDGE_INDEX_HELP}
</span>
</span>
</span>
</span>
<input
id={controlId}
className="cw-input"
value={value}
placeholder=""
autoComplete="off"
onChange={(event) => onChange(event.currentTarget.value)}
/>
</label>
);
}

function a2aSpaceDisplayName(space: A2aSpaceRef): string {
return space.name.trim() || "未命名智能体中心";
}
Expand Down Expand Up @@ -4006,7 +4054,7 @@ export function CustomCreate({
patch({
knowledgebaseBackend: id,
knowledgebaseIndex:
id === "viking"
id === "viking" || id === "openviking"
? node.knowledgebaseIndex
: "",
})
Expand Down Expand Up @@ -4059,6 +4107,20 @@ export function CustomCreate({
}
values={draft.deployment?.envValues ?? {}}
onChange={patchDeploymentEnv}
renderAfterField={
(node.knowledgebaseBackend ?? DEFAULT_KB_BACKEND) ===
"openviking"
? (item) =>
item.key === "DATABASE_OPENVIKING_USER_ID" ? (
<OpenVikingKnowledgeIndexField
value={node.knowledgebaseIndex ?? ""}
onChange={(knowledgebaseIndex) =>
patch({ knowledgebaseIndex })
}
/>
) : null
: undefined
}
/>
</div>
)}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/create/normalizeDraft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const LTM_IDS = new Set([
"openviking",
"mem0",
]);
const KB_IDS = new Set(["opensearch", "viking", "context_search"]);
const KB_IDS = new Set(["opensearch", "viking", "context_search", "openviking"]);
const EXPORTER_IDS = new Set(["apmplus", "cozeloop", "tls"]);
const TOOL_IDS = new Set([
"web_search",
Expand Down
34 changes: 34 additions & 0 deletions frontend/src/create/veadkCatalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,40 @@ export const KB_BACKENDS: BackendOption[] = [
{ key: "DATABASE_CONTEXT_SEARCH_ENGINE_APIKEY", required: true },
],
},
{
id: "openviking",
label: "OpenViking Knowledge",
desc: "OpenViking 资源目录知识库,无需向量化模型配置。",
env: [
{
key: "DATABASE_OPENVIKING_URL",
required: true,
placeholder: OPENVIKING_DEFAULT_URL,
comment: "OpenViking 服务地址",
link: OPENVIKING_CONSOLE_LINK,
},
{
key: "DATABASE_OPENVIKING_API_KEY",
required: true,
comment: "OpenViking API Key",
link: OPENVIKING_CONSOLE_LINK,
},
{
key: "DATABASE_OPENVIKING_USER_ID",
required: false,
placeholder: "default",
comment: "知识库归属 ID",
help: "未配置资源目录时用于默认路径 viking://user/<此值>/resources/<知识库索引>/,默认 default。",
},
{
key: "DATABASE_OPENVIKING_TARGET_URI",
required: false,
placeholder: "viking://user/default/resources/<index>/",
comment: "知识库资源目录",
help: "留空时由 KnowledgeBase index 自动生成;填写后直接检索该 OpenViking 资源目录,优先级最高。",
},
],
},
];

/* ------------------------------------------------------------------ *
Expand Down
69 changes: 69 additions & 0 deletions frontend/tests/deploymentEnv.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,75 @@ test("declares the OpenViking long-term memory runtime configuration", () => {
assert.match(projectPreviewStyles, /\.pp-env-help:hover \.pp-env-help-popover/);
});

test("declares the OpenViking knowledge runtime configuration", () => {
const openviking = KB_BACKENDS.find((option) => option.id === "openviking");

assert.ok(openviking);
assert.equal(openviking.label, "OpenViking Knowledge");
assert.equal(openviking.pipExtra, undefined);
assert.deepEqual(
openviking.env.map((env) => [env.key, env.required, env.placeholder ?? ""]),
[
[
"DATABASE_OPENVIKING_URL",
true,
"https://api.vikingdb.cn-beijing.volces.com/openviking",
],
["DATABASE_OPENVIKING_API_KEY", true, ""],
["DATABASE_OPENVIKING_USER_ID", false, "default"],
[
"DATABASE_OPENVIKING_TARGET_URI",
false,
"viking://user/default/resources/<index>/",
],
],
);
assert.match(
openviking.env.find((env) => env.key === "DATABASE_OPENVIKING_USER_ID")
?.help ?? "",
/viking:\/\/user\/<此值>\/resources\/<知识库索引>\//,
);
assert.match(
openviking.env.find((env) => env.key === "DATABASE_OPENVIKING_TARGET_URI")
?.help ?? "",
/KnowledgeBase index/,
);
assert.equal(
firstMissingRuntimeEnv(openviking.env, {
DATABASE_OPENVIKING_URL: "https://openviking.local",
})?.key,
"DATABASE_OPENVIKING_API_KEY",
);
assert.deepEqual(
runtimeEnvVars(openviking.env, {
DATABASE_OPENVIKING_URL: "https://openviking.local",
DATABASE_OPENVIKING_API_KEY: "test-api-key",
DATABASE_OPENVIKING_TARGET_URI: "viking://user/team/resources/faq/",
}),
[
{
key: "DATABASE_OPENVIKING_URL",
value: "https://openviking.local",
},
{ key: "DATABASE_OPENVIKING_API_KEY", value: "test-api-key" },
{
key: "DATABASE_OPENVIKING_TARGET_URI",
value: "viking://user/team/resources/faq/",
},
],
);
assert.match(customCreateSource, /OpenViking 资源索引/);
assert.match(customCreateSource, /id === "viking" \|\| id === "openviking"/);
assert.match(
customCreateSource,
/item\.key === "DATABASE_OPENVIKING_USER_ID"[\s\S]*<OpenVikingKnowledgeIndexField/,
);
assert.match(
customCreateSource,
/默认值:留空[\s\S]*viking:\/\/user\/\{知识库归属 ID,未填则 default\}\/resources\/\{资源索引\}\//,
);
});

test("does not request auto-resolved credentials per component", () => {
const envKeys = [
...BUILTIN_TOOLS,
Expand Down
7 changes: 7 additions & 0 deletions frontend/tests/generatedAgentPlanner.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ test("feeds provider-supported generated tool ids into the checklist selection",
assert.match(createSource, /selected=\{builtinTools\}/);
});

test("keeps OpenViking knowledge when normalizing imported drafts", () => {
assert.match(
normalizeSource,
/const KB_IDS = new Set\(\["opensearch", "viking", "context_search", "openviking"\]\)/,
);
});

test("allows Agent generation to outlive the default request timeout", () => {
assert.match(clientSource, /GENERATED_AGENT_DRAFT_TIMEOUT_MS = 190_000/);
assert.match(
Expand Down
Loading
Loading