Merge branch 'gzy' into 'master'
Gzy See merge request knowledge-base-bj/deerflow-server!156
This commit is contained in:
commit
6157b42f5a
12 files changed
+927
-27
No files matched your search
@@ -11,6 +11,7 @@ import {
|
|||||||
GaugeIcon,
|
GaugeIcon,
|
||||||
HistoryIcon,
|
HistoryIcon,
|
||||||
MessageCircleQuestionIcon,
|
MessageCircleQuestionIcon,
|
||||||
|
PlugIcon,
|
||||||
ScrollTextIcon,
|
ScrollTextIcon,
|
||||||
Settings2Icon,
|
Settings2Icon,
|
||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
@@ -218,6 +219,14 @@ export function WorkspaceNavMenu() {
|
|||||||
<TagsIcon />
|
<TagsIcon />
|
||||||
{t.adminTags.title}
|
{t.adminTags.title}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
{isAdmin && (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => navigate("/page/workspace/admin/plugins")}
|
||||||
|
>
|
||||||
|
<PlugIcon />
|
||||||
|
插件管理
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => navigate("/page/workspace/admin/positions")}
|
onClick={() => navigate("/page/workspace/admin/positions")}
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
import {
|
||||||
|
normalizeDomainInput,
|
||||||
|
resolveIncomingIframeSelection,
|
||||||
|
resolveConfiguredIframeAgentId,
|
||||||
|
resolveDomainMappedAgentId,
|
||||||
|
} from "./domain-agent.ts";
|
||||||
|
|
||||||
|
test("normalizes full urls and bare hosts to comparable domains", () => {
|
||||||
|
assert.equal(
|
||||||
|
normalizeDomainInput(" https://Foo.Example.com/path?q=1 "),
|
||||||
|
"foo.example.com",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
normalizeDomainInput("foo.example.com:8080/landing"),
|
||||||
|
"foo.example.com:8080",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolves the configured agent for a matching pageContext url", () => {
|
||||||
|
assert.equal(
|
||||||
|
resolveDomainMappedAgentId({
|
||||||
|
pageUrl: "https://foo.example.com/article/123",
|
||||||
|
mappings: [
|
||||||
|
{
|
||||||
|
id: "m1",
|
||||||
|
domain: "foo.example.com",
|
||||||
|
agentId: "agent-1",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
availableAgentIds: ["agent-1", "agent-2"],
|
||||||
|
}),
|
||||||
|
"agent-1",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("ignores disabled mappings and unavailable agents", () => {
|
||||||
|
assert.equal(
|
||||||
|
resolveDomainMappedAgentId({
|
||||||
|
pageUrl: "https://foo.example.com/article/123",
|
||||||
|
mappings: [
|
||||||
|
{
|
||||||
|
id: "m1",
|
||||||
|
domain: "foo.example.com",
|
||||||
|
agentId: "agent-1",
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "m2",
|
||||||
|
domain: "bar.example.com",
|
||||||
|
agentId: "agent-2",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
availableAgentIds: ["agent-3"],
|
||||||
|
}),
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not auto-select after manual selection or once a conversation has started", () => {
|
||||||
|
assert.equal(
|
||||||
|
resolveConfiguredIframeAgentId({
|
||||||
|
pageUrl: "https://foo.example.com",
|
||||||
|
mappings: [
|
||||||
|
{
|
||||||
|
id: "m1",
|
||||||
|
domain: "foo.example.com",
|
||||||
|
agentId: "agent-1",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
availableAgentIds: ["agent-1"],
|
||||||
|
isNewThread: true,
|
||||||
|
hasStartedConversation: false,
|
||||||
|
manualSelectionLocked: true,
|
||||||
|
}),
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
resolveConfiguredIframeAgentId({
|
||||||
|
pageUrl: "https://foo.example.com",
|
||||||
|
mappings: [
|
||||||
|
{
|
||||||
|
id: "m1",
|
||||||
|
domain: "foo.example.com",
|
||||||
|
agentId: "agent-1",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
availableAgentIds: ["agent-1"],
|
||||||
|
isNewThread: true,
|
||||||
|
hasStartedConversation: true,
|
||||||
|
manualSelectionLocked: false,
|
||||||
|
}),
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("falls back to default assistant when url is unmapped and user has not manually selected an agent", () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
resolveIncomingIframeSelection({
|
||||||
|
pageUrl: "https://unknown.example.com",
|
||||||
|
mappings: [
|
||||||
|
{
|
||||||
|
id: "m1",
|
||||||
|
domain: "foo.example.com",
|
||||||
|
agentId: "agent-1",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
availableAgentIds: ["agent-1", "agent-2"],
|
||||||
|
manualSelectedAgentId: null,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
selectedAgentId: null,
|
||||||
|
forceDefaultAssistant: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("falls back to the user's manual selection when url is unmapped", () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
resolveIncomingIframeSelection({
|
||||||
|
pageUrl: "https://unknown.example.com",
|
||||||
|
mappings: [
|
||||||
|
{
|
||||||
|
id: "m1",
|
||||||
|
domain: "foo.example.com",
|
||||||
|
agentId: "agent-1",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
availableAgentIds: ["agent-1", "agent-2"],
|
||||||
|
manualSelectedAgentId: "agent-2",
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
selectedAgentId: "agent-2",
|
||||||
|
forceDefaultAssistant: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("mapped domain overrides the user's manual selection for that incoming message", () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
resolveIncomingIframeSelection({
|
||||||
|
pageUrl: "https://foo.example.com/article",
|
||||||
|
mappings: [
|
||||||
|
{
|
||||||
|
id: "m1",
|
||||||
|
domain: "foo.example.com",
|
||||||
|
agentId: "agent-1",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
availableAgentIds: ["agent-1", "agent-2"],
|
||||||
|
manualSelectedAgentId: "agent-2",
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
selectedAgentId: "agent-1",
|
||||||
|
forceDefaultAssistant: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
export type DomainAgentMapping = {
|
||||||
|
id: string;
|
||||||
|
domain: string;
|
||||||
|
agentId: string;
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function trimToUndefined(value: string | null | undefined): string | undefined {
|
||||||
|
const trimmed = value?.trim();
|
||||||
|
return trimmed ? trimmed : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeAgentId(agentId: string | null | undefined): string | undefined {
|
||||||
|
const trimmed = agentId?.trim();
|
||||||
|
return trimmed ? trimmed : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeDomainInput(
|
||||||
|
value: string | null | undefined,
|
||||||
|
): string | undefined {
|
||||||
|
const trimmed = trimToUndefined(value);
|
||||||
|
if (!trimmed) return undefined;
|
||||||
|
|
||||||
|
const candidate = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(trimmed)
|
||||||
|
? trimmed
|
||||||
|
: `https://${trimmed}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = new URL(candidate);
|
||||||
|
const host = parsed.host.trim().toLowerCase();
|
||||||
|
return host || undefined;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveDomainMappedAgentId({
|
||||||
|
pageUrl,
|
||||||
|
mappings,
|
||||||
|
availableAgentIds,
|
||||||
|
}: {
|
||||||
|
pageUrl: string | null | undefined;
|
||||||
|
mappings: DomainAgentMapping[];
|
||||||
|
availableAgentIds?: readonly string[] | null;
|
||||||
|
}): string | undefined {
|
||||||
|
const host = normalizeDomainInput(pageUrl);
|
||||||
|
if (!host) return undefined;
|
||||||
|
|
||||||
|
const allowedAgents =
|
||||||
|
availableAgentIds && availableAgentIds.length > 0
|
||||||
|
? new Set(availableAgentIds)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
for (const mapping of mappings) {
|
||||||
|
if (!mapping.enabled) continue;
|
||||||
|
if (normalizeDomainInput(mapping.domain) !== host) continue;
|
||||||
|
if (allowedAgents && !allowedAgents.has(mapping.agentId)) continue;
|
||||||
|
return mapping.agentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveConfiguredIframeAgentId({
|
||||||
|
pageUrl,
|
||||||
|
mappings,
|
||||||
|
availableAgentIds,
|
||||||
|
isNewThread,
|
||||||
|
hasStartedConversation,
|
||||||
|
manualSelectionLocked,
|
||||||
|
}: {
|
||||||
|
pageUrl: string | null | undefined;
|
||||||
|
mappings: DomainAgentMapping[];
|
||||||
|
availableAgentIds?: readonly string[] | null;
|
||||||
|
isNewThread: boolean;
|
||||||
|
hasStartedConversation: boolean;
|
||||||
|
manualSelectionLocked: boolean;
|
||||||
|
}): string | undefined {
|
||||||
|
if (!isNewThread || hasStartedConversation || manualSelectionLocked) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolveDomainMappedAgentId({
|
||||||
|
pageUrl,
|
||||||
|
mappings,
|
||||||
|
availableAgentIds,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveIncomingIframeSelection({
|
||||||
|
pageUrl,
|
||||||
|
mappings,
|
||||||
|
availableAgentIds,
|
||||||
|
manualSelectedAgentId,
|
||||||
|
}: {
|
||||||
|
pageUrl: string | null | undefined;
|
||||||
|
mappings: DomainAgentMapping[];
|
||||||
|
availableAgentIds?: readonly string[] | null;
|
||||||
|
manualSelectedAgentId: string | null;
|
||||||
|
}): {
|
||||||
|
selectedAgentId: string | null;
|
||||||
|
forceDefaultAssistant: boolean;
|
||||||
|
} {
|
||||||
|
const mappedAgentId = resolveDomainMappedAgentId({
|
||||||
|
pageUrl,
|
||||||
|
mappings,
|
||||||
|
availableAgentIds,
|
||||||
|
});
|
||||||
|
if (mappedAgentId) {
|
||||||
|
return {
|
||||||
|
selectedAgentId: mappedAgentId,
|
||||||
|
forceDefaultAssistant: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowedAgents =
|
||||||
|
availableAgentIds && availableAgentIds.length > 0
|
||||||
|
? new Set(availableAgentIds)
|
||||||
|
: null;
|
||||||
|
const normalizedManualAgentId = normalizeAgentId(manualSelectedAgentId);
|
||||||
|
if (
|
||||||
|
normalizedManualAgentId &&
|
||||||
|
(!allowedAgents || allowedAgents.has(normalizedManualAgentId))
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
selectedAgentId: normalizedManualAgentId,
|
||||||
|
forceDefaultAssistant: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectedAgentId: null,
|
||||||
|
forceDefaultAssistant: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -8,12 +8,115 @@ export interface PromptPrefixSettings {
|
|||||||
prompt_prefix: string;
|
prompt_prefix: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PluginDomainAgentMappingDto {
|
||||||
|
id: string;
|
||||||
|
domain: string;
|
||||||
|
agent_id: string;
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PluginManagementSettings {
|
||||||
|
domainAgentMappings: Array<{
|
||||||
|
id: string;
|
||||||
|
domain: string;
|
||||||
|
agentId: string;
|
||||||
|
enabled: boolean;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PluginManagementUpdate {
|
||||||
|
domainAgentMappings?: Array<{
|
||||||
|
id: string;
|
||||||
|
domain: string;
|
||||||
|
agentId: string;
|
||||||
|
enabled: boolean;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PromptPrefixUpdate {
|
export interface PromptPrefixUpdate {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
switch_label?: string;
|
switch_label?: string;
|
||||||
prompt_prefix?: string;
|
prompt_prefix?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fromPluginManagementDto(data: {
|
||||||
|
domain_agent_mappings: PluginDomainAgentMappingDto[];
|
||||||
|
}): PluginManagementSettings {
|
||||||
|
return {
|
||||||
|
domainAgentMappings: (data.domain_agent_mappings ?? []).map((item) => ({
|
||||||
|
id: item.id,
|
||||||
|
domain: item.domain,
|
||||||
|
agentId: item.agent_id,
|
||||||
|
enabled: item.enabled,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toPluginManagementDto(body: PluginManagementUpdate) {
|
||||||
|
return {
|
||||||
|
domain_agent_mappings: (body.domainAgentMappings ?? []).map((item) => ({
|
||||||
|
id: item.id,
|
||||||
|
domain: item.domain,
|
||||||
|
agent_id: item.agentId,
|
||||||
|
enabled: item.enabled,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadPluginManagementSettings(): Promise<PluginManagementSettings> {
|
||||||
|
const response = await apiFetch(
|
||||||
|
`${getBackendBaseURL()}/api/system-settings/plugin-management`,
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to load plugin management settings: ${response.status}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const data = (await response.json()) as {
|
||||||
|
domain_agent_mappings: PluginDomainAgentMappingDto[];
|
||||||
|
};
|
||||||
|
const mapped = fromPluginManagementDto(data);
|
||||||
|
writeYLog({
|
||||||
|
optType: OptType.view,
|
||||||
|
optDetail: "CM助手-全量-系统设置-插件管理查看",
|
||||||
|
url: "/api/system-settings/plugin-management",
|
||||||
|
returnParam: mapped,
|
||||||
|
});
|
||||||
|
return mapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updatePluginManagementSettings(
|
||||||
|
body: PluginManagementUpdate,
|
||||||
|
): Promise<PluginManagementSettings> {
|
||||||
|
const payload = toPluginManagementDto(body);
|
||||||
|
const response = await apiFetch(
|
||||||
|
`${getBackendBaseURL()}/api/system-settings/plugin-management`,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
const detail = await response
|
||||||
|
.json()
|
||||||
|
.catch(() => ({ detail: "Failed to update plugin management settings" }));
|
||||||
|
throw new Error(detail.detail ?? "Failed to update plugin management settings");
|
||||||
|
}
|
||||||
|
const data = (await response.json()) as {
|
||||||
|
domain_agent_mappings: PluginDomainAgentMappingDto[];
|
||||||
|
};
|
||||||
|
const mapped = fromPluginManagementDto(data);
|
||||||
|
writeYLog({
|
||||||
|
optType: OptType.edit,
|
||||||
|
optDetail: "CM助手-全量-系统设置-插件管理编辑",
|
||||||
|
url: "/api/system-settings/plugin-management",
|
||||||
|
reqParam: payload,
|
||||||
|
returnParam: mapped,
|
||||||
|
});
|
||||||
|
return mapped;
|
||||||
|
}
|
||||||
|
|
||||||
export async function loadPromptPrefixSettings(): Promise<PromptPrefixSettings> {
|
export async function loadPromptPrefixSettings(): Promise<PromptPrefixSettings> {
|
||||||
const response = await apiFetch(
|
const response = await apiFetch(
|
||||||
`${getBackendBaseURL()}/api/system-settings/prompt-prefix`,
|
`${getBackendBaseURL()}/api/system-settings/prompt-prefix`,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
loadCitationDisplaySettings,
|
loadCitationDisplaySettings,
|
||||||
loadLeaderboardSettings,
|
loadLeaderboardSettings,
|
||||||
loadModelConcurrencySettings,
|
loadModelConcurrencySettings,
|
||||||
|
loadPluginManagementSettings,
|
||||||
loadPrivateSquareStatus,
|
loadPrivateSquareStatus,
|
||||||
loadPromptPrefixSettings,
|
loadPromptPrefixSettings,
|
||||||
loadPublishSquares,
|
loadPublishSquares,
|
||||||
@@ -17,6 +18,7 @@ import {
|
|||||||
updateCitationDisplaySettings,
|
updateCitationDisplaySettings,
|
||||||
updateLeaderboardSettings,
|
updateLeaderboardSettings,
|
||||||
updateModelConcurrencySettings,
|
updateModelConcurrencySettings,
|
||||||
|
updatePluginManagementSettings,
|
||||||
updatePrivateSquareSettings,
|
updatePrivateSquareSettings,
|
||||||
updatePromptPrefixSettings,
|
updatePromptPrefixSettings,
|
||||||
updatePublishSquares,
|
updatePublishSquares,
|
||||||
@@ -25,6 +27,7 @@ import {
|
|||||||
type CitationDisplayUpdate,
|
type CitationDisplayUpdate,
|
||||||
type LeaderboardSettingsUpdate,
|
type LeaderboardSettingsUpdate,
|
||||||
type ModelConcurrencyUpdate,
|
type ModelConcurrencyUpdate,
|
||||||
|
type PluginManagementUpdate,
|
||||||
type PrivateSquareUpdate,
|
type PrivateSquareUpdate,
|
||||||
type PromptPrefixUpdate,
|
type PromptPrefixUpdate,
|
||||||
type PublishSquaresUpdate,
|
type PublishSquaresUpdate,
|
||||||
@@ -41,6 +44,7 @@ const CITATION_DISPLAY_KEY = ["systemSettings", "citationDisplay"] as const;
|
|||||||
const LEADERBOARD_KEY = ["systemSettings", "leaderboard"] as const;
|
const LEADERBOARD_KEY = ["systemSettings", "leaderboard"] as const;
|
||||||
const SKILL_COMPRESSION_KEY = ["systemSettings", "skillCompression"] as const;
|
const SKILL_COMPRESSION_KEY = ["systemSettings", "skillCompression"] as const;
|
||||||
const MODEL_CONCURRENCY_KEY = ["systemSettings", "modelConcurrency"] as const;
|
const MODEL_CONCURRENCY_KEY = ["systemSettings", "modelConcurrency"] as const;
|
||||||
|
const PLUGIN_MANAGEMENT_KEY = ["systemSettings", "pluginManagement"] as const;
|
||||||
|
|
||||||
export function usePromptPrefixSettings() {
|
export function usePromptPrefixSettings() {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
@@ -263,6 +267,27 @@ export function useUpdateModelConcurrencySettings() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function usePluginManagementSettings() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: PLUGIN_MANAGEMENT_KEY,
|
||||||
|
queryFn: () => loadPluginManagementSettings(),
|
||||||
|
staleTime: 60_000,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
retry: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdatePluginManagementSettings() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (body: PluginManagementUpdate) =>
|
||||||
|
updatePluginManagementSettings(body),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
queryClient.setQueryData(PLUGIN_MANAGEMENT_KEY, data);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function useLeaderboardSettings() {
|
export function useLeaderboardSettings() {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: LEADERBOARD_KEY,
|
queryKey: LEADERBOARD_KEY,
|
||||||
|
|||||||
@@ -26,18 +26,19 @@ import { Tooltip } from "@/components/workspace/tooltip";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import { useAgentSelector } from "@/core/agents";
|
import { useAgentSelector } from "@/core/agents";
|
||||||
|
import { resolveIncomingIframeSelection } from "@/core/plugin-management/domain-agent";
|
||||||
import { getAccessToken } from "@/core/auth";
|
import { getAccessToken } from "@/core/auth";
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
import { useModels } from "@/core/models/hooks";
|
import { useModels } from "@/core/models/hooks";
|
||||||
import { useNotification } from "@/core/notification/hooks";
|
import { useNotification } from "@/core/notification/hooks";
|
||||||
import { useRouteBase } from "@/core/route-base";
|
import { useRouteBase } from "@/core/route-base";
|
||||||
import {
|
import {
|
||||||
getLastAgentId,
|
|
||||||
isThreadRagModeLocked,
|
isThreadRagModeLocked,
|
||||||
saveLastAgentId,
|
saveLastAgentId,
|
||||||
useLocalSettings,
|
useLocalSettings,
|
||||||
useThreadSettings,
|
useThreadSettings,
|
||||||
} from "@/core/settings";
|
} from "@/core/settings";
|
||||||
|
import { usePluginManagementSettings } from "@/core/system-settings/hooks";
|
||||||
import { useThreadStream } from "@/core/threads/hooks";
|
import { useThreadStream } from "@/core/threads/hooks";
|
||||||
import { textOfMessage } from "@/core/threads/utils";
|
import { textOfMessage } from "@/core/threads/utils";
|
||||||
import { env } from "@/env";
|
import { env } from "@/env";
|
||||||
@@ -50,6 +51,22 @@ import { resolveIframeAssistantId } from "./iframe-chat-agent";
|
|||||||
import { buildIframeModeButtons } from "./iframe-chat-mode-buttons";
|
import { buildIframeModeButtons } from "./iframe-chat-mode-buttons";
|
||||||
|
|
||||||
type BrainyChatStatus = "started" | "finished";
|
type BrainyChatStatus = "started" | "finished";
|
||||||
|
type ResolvedIframeModeButton =
|
||||||
|
| {
|
||||||
|
key: "agent";
|
||||||
|
label: string;
|
||||||
|
active: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
key: "writing" | "memory" | "references" | "contextCompression" | "pro";
|
||||||
|
label: string;
|
||||||
|
active: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
icon: typeof FileTextIcon;
|
||||||
|
onClick: () => void;
|
||||||
|
activeClassName: string;
|
||||||
|
};
|
||||||
type BrainyAuthTokenMessage = {
|
type BrainyAuthTokenMessage = {
|
||||||
source: "brainy-iframe";
|
source: "brainy-iframe";
|
||||||
type: "brainy:auth-token";
|
type: "brainy:auth-token";
|
||||||
@@ -66,6 +83,13 @@ type BrainyChatInterruptMessage = {
|
|||||||
};
|
};
|
||||||
requestId?: string;
|
requestId?: string;
|
||||||
};
|
};
|
||||||
|
type PendingIframeSubmit = {
|
||||||
|
message: PromptInputMessage;
|
||||||
|
externalContext?: Record<string, unknown>;
|
||||||
|
options?: { additionalKwargs?: Record<string, unknown> };
|
||||||
|
targetSelectedAgentId: string | null;
|
||||||
|
targetForceDefaultAssistant: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
function postBrainyChatStatus(status: BrainyChatStatus, requestId: string) {
|
function postBrainyChatStatus(status: BrainyChatStatus, requestId: string) {
|
||||||
window.parent?.postMessage(
|
window.parent?.postMessage(
|
||||||
@@ -104,9 +128,22 @@ export default function IframeChatPage() {
|
|||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
const { tokenUsageEnabled } = useModels();
|
const { tokenUsageEnabled } = useModels();
|
||||||
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null);
|
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null);
|
||||||
|
const [manualSelectedAgentId, setManualSelectedAgentId] = useState<string | null>(null);
|
||||||
|
const [forceDefaultAssistant, setForceDefaultAssistant] = useState(true);
|
||||||
|
const [pendingIframeSubmit, setPendingIframeSubmit] =
|
||||||
|
useState<PendingIframeSubmit | null>(null);
|
||||||
const { agents: selectorAgents, isLoading: selectorLoading } =
|
const { agents: selectorAgents, isLoading: selectorLoading } =
|
||||||
useAgentSelector();
|
useAgentSelector();
|
||||||
|
const { data: pluginManagementSettings } = usePluginManagementSettings();
|
||||||
const pendingRequestIdRef = useRef<string | null>(null);
|
const pendingRequestIdRef = useRef<string | null>(null);
|
||||||
|
const handleSubmitRef = useRef<
|
||||||
|
| ((
|
||||||
|
message: PromptInputMessage,
|
||||||
|
externalContext?: Record<string, unknown>,
|
||||||
|
options?: { additionalKwargs?: Record<string, unknown> },
|
||||||
|
) => void)
|
||||||
|
| null
|
||||||
|
>(null);
|
||||||
const { selectedArtifact } = useArtifacts();
|
const { selectedArtifact } = useArtifacts();
|
||||||
useSpecificChatMode();
|
useSpecificChatMode();
|
||||||
|
|
||||||
@@ -139,23 +176,37 @@ export default function IframeChatPage() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isNewThread) {
|
if (isNewThread) {
|
||||||
setHasStartedConversation(false);
|
setHasStartedConversation(false);
|
||||||
setSelectedAgentId(getLastAgentId());
|
setSelectedAgentId(null);
|
||||||
|
setManualSelectedAgentId(null);
|
||||||
|
setForceDefaultAssistant(true);
|
||||||
|
setPendingIframeSubmit(null);
|
||||||
}
|
}
|
||||||
}, [isNewThread, threadId]);
|
}, [isNewThread, threadId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectorLoading || !selectedAgentId) {
|
if (selectorLoading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!selectorAgents.some((agent) => agent.id === selectedAgentId)) {
|
if (
|
||||||
|
selectedAgentId &&
|
||||||
|
!selectorAgents.some((agent) => agent.id === selectedAgentId)
|
||||||
|
) {
|
||||||
setSelectedAgentId(null);
|
setSelectedAgentId(null);
|
||||||
|
setForceDefaultAssistant(true);
|
||||||
}
|
}
|
||||||
}, [selectedAgentId, selectorAgents, selectorLoading]);
|
if (
|
||||||
|
manualSelectedAgentId &&
|
||||||
|
!selectorAgents.some((agent) => agent.id === manualSelectedAgentId)
|
||||||
|
) {
|
||||||
|
setManualSelectedAgentId(null);
|
||||||
|
}
|
||||||
|
}, [manualSelectedAgentId, selectedAgentId, selectorAgents, selectorLoading]);
|
||||||
|
|
||||||
const { showNotification } = useNotification();
|
const { showNotification } = useNotification();
|
||||||
const activeAssistantId = resolveIframeAssistantId({
|
const activeAssistantId = resolveIframeAssistantId({
|
||||||
selectedAgentId,
|
selectedAgentId,
|
||||||
threadMetadataAgentId: null,
|
threadMetadataAgentId: null,
|
||||||
|
preferDefaultAssistant: forceDefaultAssistant,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [thread, sendMessage, isUploading] = useThreadStream({
|
const [thread, sendMessage, isUploading] = useThreadStream({
|
||||||
@@ -209,14 +260,18 @@ export default function IframeChatPage() {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const resolvedAgentId = resolveIframeAssistantId({
|
if (!threadMetadataAgentId || isNewThread) return;
|
||||||
selectedAgentId,
|
if (manualSelectedAgentId) return;
|
||||||
threadMetadataAgentId,
|
if (forceDefaultAssistant) return;
|
||||||
});
|
if (selectedAgentId) return;
|
||||||
if (resolvedAgentId && resolvedAgentId !== selectedAgentId) {
|
setSelectedAgentId(threadMetadataAgentId);
|
||||||
setSelectedAgentId(resolvedAgentId);
|
}, [
|
||||||
}
|
forceDefaultAssistant,
|
||||||
}, [selectedAgentId, threadMetadataAgentId]);
|
isNewThread,
|
||||||
|
manualSelectedAgentId,
|
||||||
|
selectedAgentId,
|
||||||
|
threadMetadataAgentId,
|
||||||
|
]);
|
||||||
|
|
||||||
const writingArtifactPath = useMemo(() => {
|
const writingArtifactPath = useMemo(() => {
|
||||||
const isMarkdown = (path: string | null | undefined) =>
|
const isMarkdown = (path: string | null | undefined) =>
|
||||||
@@ -277,10 +332,43 @@ export default function IframeChatPage() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleAgentChange = useCallback((agentId: string | null) => {
|
const handleAgentChange = useCallback((agentId: string | null) => {
|
||||||
|
setManualSelectedAgentId(agentId);
|
||||||
setSelectedAgentId(agentId);
|
setSelectedAgentId(agentId);
|
||||||
|
setForceDefaultAssistant(!agentId);
|
||||||
saveLastAgentId(agentId);
|
saveLastAgentId(agentId);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleSubmitRef.current = handleSubmit;
|
||||||
|
}, [handleSubmit]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!pendingIframeSubmit) return;
|
||||||
|
const currentAssistantId = resolveIframeAssistantId({
|
||||||
|
selectedAgentId,
|
||||||
|
threadMetadataAgentId: null,
|
||||||
|
preferDefaultAssistant: forceDefaultAssistant,
|
||||||
|
});
|
||||||
|
const targetAssistantId = resolveIframeAssistantId({
|
||||||
|
selectedAgentId: pendingIframeSubmit.targetSelectedAgentId,
|
||||||
|
threadMetadataAgentId: null,
|
||||||
|
preferDefaultAssistant: pendingIframeSubmit.targetForceDefaultAssistant,
|
||||||
|
});
|
||||||
|
if (
|
||||||
|
currentAssistantId !== targetAssistantId ||
|
||||||
|
selectedAgentId !== pendingIframeSubmit.targetSelectedAgentId ||
|
||||||
|
forceDefaultAssistant !== pendingIframeSubmit.targetForceDefaultAssistant
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleSubmitRef.current?.(
|
||||||
|
pendingIframeSubmit.message,
|
||||||
|
pendingIframeSubmit.externalContext,
|
||||||
|
pendingIframeSubmit.options,
|
||||||
|
);
|
||||||
|
setPendingIframeSubmit(null);
|
||||||
|
}, [forceDefaultAssistant, pendingIframeSubmit, selectedAgentId]);
|
||||||
|
|
||||||
const ragModeLocked = isThreadRagModeLocked(threadId);
|
const ragModeLocked = isThreadRagModeLocked(threadId);
|
||||||
const ragModeEnabled =
|
const ragModeEnabled =
|
||||||
localSettings.display.ragModeEnabled || ragModeLocked;
|
localSettings.display.ragModeEnabled || ragModeLocked;
|
||||||
@@ -353,10 +441,22 @@ export default function IframeChatPage() {
|
|||||||
}
|
}
|
||||||
if (data?.type !== "brainy:chat-submit") return;
|
if (data?.type !== "brainy:chat-submit") return;
|
||||||
const submitMessage = data as BrainyChatSubmitMessage;
|
const submitMessage = data as BrainyChatSubmitMessage;
|
||||||
|
|
||||||
const { submitText, files, iframeContext, additionalKwargs } =
|
const { submitText, files, iframeContext, additionalKwargs } =
|
||||||
parseBrainyChatSubmitMessage(submitMessage);
|
parseBrainyChatSubmitMessage(submitMessage);
|
||||||
|
|
||||||
|
if (!submitText && files.length === 0) return;
|
||||||
|
|
||||||
|
const nextSelection = resolveIncomingIframeSelection({
|
||||||
|
pageUrl: submitMessage.payload?.pageContext?.url,
|
||||||
|
mappings: pluginManagementSettings?.domainAgentMappings ?? [],
|
||||||
|
availableAgentIds: selectorLoading
|
||||||
|
? undefined
|
||||||
|
: selectorAgents.map((agent) => agent.id),
|
||||||
|
manualSelectedAgentId,
|
||||||
|
});
|
||||||
|
setSelectedAgentId(nextSelection.selectedAgentId);
|
||||||
|
setForceDefaultAssistant(nextSelection.forceDefaultAssistant);
|
||||||
|
|
||||||
console.log("收到插件消息", {
|
console.log("收到插件消息", {
|
||||||
text: submitMessage.payload?.text,
|
text: submitMessage.payload?.text,
|
||||||
prompt: submitMessage.payload?.prompt,
|
prompt: submitMessage.payload?.prompt,
|
||||||
@@ -368,26 +468,31 @@ export default function IframeChatPage() {
|
|||||||
iframeContext,
|
iframeContext,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!submitText && files.length === 0) return;
|
|
||||||
|
|
||||||
pendingRequestIdRef.current = submitMessage.requestId ?? null;
|
pendingRequestIdRef.current = submitMessage.requestId ?? null;
|
||||||
|
setPendingIframeSubmit({
|
||||||
handleSubmit(
|
message: { text: submitText, files },
|
||||||
{ text: submitText, files },
|
externalContext: iframeContext,
|
||||||
iframeContext,
|
options: additionalKwargs ? { additionalKwargs } : undefined,
|
||||||
additionalKwargs ? { additionalKwargs } : undefined,
|
targetSelectedAgentId: nextSelection.selectedAgentId,
|
||||||
);
|
targetForceDefaultAssistant: nextSelection.forceDefaultAssistant,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener("message", handleMessage);
|
window.addEventListener("message", handleMessage);
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("message", handleMessage);
|
window.removeEventListener("message", handleMessage);
|
||||||
};
|
};
|
||||||
}, [handleInterrupt, handleSubmit]);
|
}, [
|
||||||
|
handleInterrupt,
|
||||||
|
manualSelectedAgentId,
|
||||||
|
pluginManagementSettings?.domainAgentMappings,
|
||||||
|
selectorAgents,
|
||||||
|
selectorLoading,
|
||||||
|
]);
|
||||||
|
|
||||||
const isLandingState = isNewThread && !hasStartedConversation;
|
const isLandingState = isNewThread && !hasStartedConversation;
|
||||||
const bottomBarHeightClass = "pb-24";
|
const bottomBarHeightClass = "pb-24";
|
||||||
const modeButtons = buildIframeModeButtons({
|
const modeButtons: ResolvedIframeModeButton[] = buildIframeModeButtons({
|
||||||
agentSelected: !!selectedAgentId,
|
agentSelected: !!selectedAgentId,
|
||||||
writingModeEnabled: !!settings.context.writing_mode,
|
writingModeEnabled: !!settings.context.writing_mode,
|
||||||
memoryInjectionEnabled: Boolean(
|
memoryInjectionEnabled: Boolean(
|
||||||
@@ -397,10 +502,15 @@ export default function IframeChatPage() {
|
|||||||
ragModeLocked,
|
ragModeLocked,
|
||||||
contextCompressionEnabled,
|
contextCompressionEnabled,
|
||||||
proModeEnabled,
|
proModeEnabled,
|
||||||
}).map((button) => {
|
}).map((button): ResolvedIframeModeButton => {
|
||||||
switch (button.key) {
|
switch (button.key) {
|
||||||
case "agent":
|
case "agent":
|
||||||
return button;
|
return {
|
||||||
|
key: "agent",
|
||||||
|
label: button.label,
|
||||||
|
active: button.active,
|
||||||
|
disabled: button.disabled,
|
||||||
|
};
|
||||||
case "writing":
|
case "writing":
|
||||||
return {
|
return {
|
||||||
...button,
|
...button,
|
||||||
@@ -437,7 +547,7 @@ export default function IframeChatPage() {
|
|||||||
activeClassName: "text-fuchsia-600 hover:text-fuchsia-700",
|
activeClassName: "text-fuchsia-600 hover:text-fuchsia-700",
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
return button;
|
throw new Error(`Unknown iframe mode button: ${String(button.key)}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Loader2Icon, PlusIcon, Trash2Icon } from "lucide-react";
|
||||||
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { useAgentSelector } from "@/core/agents";
|
||||||
|
import {
|
||||||
|
type DomainAgentMapping,
|
||||||
|
normalizeDomainInput,
|
||||||
|
} from "@/core/plugin-management/domain-agent";
|
||||||
|
import type { PluginManagementSettings } from "@/core/system-settings/api";
|
||||||
|
import {
|
||||||
|
usePluginManagementSettings,
|
||||||
|
useUpdatePluginManagementSettings,
|
||||||
|
} from "@/core/system-settings/hooks";
|
||||||
|
|
||||||
|
function makeRow(): DomainAgentMapping {
|
||||||
|
return {
|
||||||
|
id:
|
||||||
|
typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
|
||||||
|
? crypto.randomUUID()
|
||||||
|
: `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`,
|
||||||
|
domain: "",
|
||||||
|
agentId: "",
|
||||||
|
enabled: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeRows(
|
||||||
|
rows: DomainAgentMapping[],
|
||||||
|
): PluginManagementSettings["domainAgentMappings"] {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const next: PluginManagementSettings["domainAgentMappings"] = [];
|
||||||
|
for (const row of rows) {
|
||||||
|
const domain = normalizeDomainInput(row.domain);
|
||||||
|
const agentId = row.agentId.trim();
|
||||||
|
if (!domain || !agentId) continue;
|
||||||
|
if (seen.has(domain)) continue;
|
||||||
|
seen.add(domain);
|
||||||
|
next.push({
|
||||||
|
id: row.id || makeRow().id,
|
||||||
|
domain,
|
||||||
|
agentId,
|
||||||
|
enabled: row.enabled,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PluginManagementPage() {
|
||||||
|
const { data, isLoading, error } = usePluginManagementSettings();
|
||||||
|
const update = useUpdatePluginManagementSettings();
|
||||||
|
const { agents, isLoading: agentsLoading } = useAgentSelector();
|
||||||
|
const [rows, setRows] = useState<DomainAgentMapping[]>([]);
|
||||||
|
const initializedRef = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!data || initializedRef.current) return;
|
||||||
|
setRows(
|
||||||
|
data.domainAgentMappings.length > 0
|
||||||
|
? data.domainAgentMappings
|
||||||
|
: [makeRow()],
|
||||||
|
);
|
||||||
|
initializedRef.current = true;
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const agentOptions = useMemo(
|
||||||
|
() =>
|
||||||
|
[...agents]
|
||||||
|
.sort((a, b) => (a.name || a.id).localeCompare(b.name || b.id, "zh-CN"))
|
||||||
|
.map((agent) => ({
|
||||||
|
id: agent.id,
|
||||||
|
label: agent.name || agent.id,
|
||||||
|
})),
|
||||||
|
[agents],
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateRow = (id: string, patch: Partial<DomainAgentMapping>) => {
|
||||||
|
setRows((current) =>
|
||||||
|
current.map((row) => (row.id === id ? { ...row, ...patch } : row)),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeRow = (id: string) => {
|
||||||
|
setRows((current) => {
|
||||||
|
const next = current.filter((row) => row.id !== id);
|
||||||
|
return next.length > 0 ? next : [makeRow()];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
const domainAgentMappings = normalizeRows(rows);
|
||||||
|
try {
|
||||||
|
const saved = await update.mutateAsync({ domainAgentMappings });
|
||||||
|
setRows(
|
||||||
|
saved.domainAgentMappings.length > 0
|
||||||
|
? saved.domainAgentMappings
|
||||||
|
: [makeRow()],
|
||||||
|
);
|
||||||
|
toast.success("已保存");
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(e instanceof Error ? e.message : "保存失败");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto flex w-full max-w-5xl flex-col gap-6 px-6 py-10">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-xl font-semibold">插件管理</h1>
|
||||||
|
<p className="text-muted-foreground mt-1 text-sm">
|
||||||
|
管理插件传入页面地址的域名与默认智能体映射。命中后,插件ZNCM模式会按配置默认选择对应智能体。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex items-center justify-center py-16">
|
||||||
|
<Loader2Icon className="text-muted-foreground size-6 animate-spin" />
|
||||||
|
</div>
|
||||||
|
) : error ? (
|
||||||
|
<div className="text-destructive rounded-xl border border-dashed py-16 text-center text-sm">
|
||||||
|
加载失败:{error.message}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col gap-5 rounded-xl border bg-white px-5 py-5 dark:border-[#213D5E] dark:bg-[#02244C] dark:text-white">
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium">域名和智能体配置</div>
|
||||||
|
<div className="text-muted-foreground mt-0.5 text-xs">
|
||||||
|
输入域名或完整 URL,保存时会自动规范化为 host。只按域名精确匹配。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setRows((current) => [...current, makeRow()])}
|
||||||
|
>
|
||||||
|
<PlusIcon className="mr-1.5 size-4" />
|
||||||
|
新增配置
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)_120px_72px] gap-3 text-sm font-medium">
|
||||||
|
<div>域名</div>
|
||||||
|
<div>智能体</div>
|
||||||
|
<div>启用</div>
|
||||||
|
<div>操作</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
{rows.map((row) => (
|
||||||
|
<div
|
||||||
|
key={row.id}
|
||||||
|
className="grid grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)_120px_72px] gap-3"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={row.domain}
|
||||||
|
onChange={(e) => updateRow(row.id, { domain: e.target.value })}
|
||||||
|
placeholder="如:https://example.com/path 或 example.com"
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
value={row.agentId}
|
||||||
|
onChange={(e) => updateRow(row.id, { agentId: e.target.value })}
|
||||||
|
className="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 w-full rounded-md border px-3 py-1 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
disabled={agentsLoading}
|
||||||
|
>
|
||||||
|
<option value="">请选择智能体</option>
|
||||||
|
{agentOptions.map((agent) => (
|
||||||
|
<option key={agent.id} value={agent.id}>
|
||||||
|
{agent.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<div className="flex h-9 items-center">
|
||||||
|
<Switch
|
||||||
|
checked={row.enabled}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
updateRow(row.id, { enabled: checked })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex h-9 items-center">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => removeRow(row.id)}
|
||||||
|
aria-label="删除配置"
|
||||||
|
>
|
||||||
|
<Trash2Icon className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button onClick={handleSave} disabled={update.isPending}>
|
||||||
|
{update.isPending && (
|
||||||
|
<Loader2Icon className="mr-1.5 size-4 animate-spin" />
|
||||||
|
)}
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -42,6 +42,7 @@ import AdminMemoryPage from './AdminMemoryPage';
|
|||||||
import AdminUserHubPage from './AdminUserHubPage';
|
import AdminUserHubPage from './AdminUserHubPage';
|
||||||
import TagsPage from './TagsPage';
|
import TagsPage from './TagsPage';
|
||||||
import PositionsPage from './PositionsPage';
|
import PositionsPage from './PositionsPage';
|
||||||
|
import PluginManagementPage from './PluginManagementPage';
|
||||||
import RecommendedQuestionsPage from './RecommendedQuestionsPage';
|
import RecommendedQuestionsPage from './RecommendedQuestionsPage';
|
||||||
import AIWritingPage from '@/open-canvas/pages/AIWritingPage';
|
import AIWritingPage from '@/open-canvas/pages/AIWritingPage';
|
||||||
import StudioNotebooksPage from './StudioNotebooksPage';
|
import StudioNotebooksPage from './StudioNotebooksPage';
|
||||||
@@ -515,6 +516,18 @@ export default function WorkspaceRoutes({
|
|||||||
</WorkspaceLayout>
|
</WorkspaceLayout>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="admin/plugins"
|
||||||
|
element={
|
||||||
|
getStoredAuth()?.system_role === 'admin' ? (
|
||||||
|
<WorkspaceLayout {...layoutProps}>
|
||||||
|
<PluginManagementPage />
|
||||||
|
</WorkspaceLayout>
|
||||||
|
) : (
|
||||||
|
<Navigate to="/page/workspace/chats/new" replace />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="admin/positions"
|
path="admin/positions"
|
||||||
element={
|
element={
|
||||||
|
|||||||
@@ -33,3 +33,14 @@ test("falls back to lead-agent mode when no agent is selected or bound", () => {
|
|||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("can force lead-agent mode even when the thread metadata is bound to another agent", () => {
|
||||||
|
assert.equal(
|
||||||
|
resolveIframeAssistantId({
|
||||||
|
selectedAgentId: null,
|
||||||
|
threadMetadataAgentId: "roundtable-structure",
|
||||||
|
preferDefaultAssistant: true,
|
||||||
|
}),
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
type ResolveIframeAssistantIdInput = {
|
type ResolveIframeAssistantIdInput = {
|
||||||
selectedAgentId: string | null;
|
selectedAgentId: string | null;
|
||||||
threadMetadataAgentId: string | null;
|
threadMetadataAgentId: string | null;
|
||||||
|
preferDefaultAssistant?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function normalizeAgentId(agentId: string | null | undefined): string | undefined {
|
function normalizeAgentId(agentId: string | null | undefined): string | undefined {
|
||||||
@@ -11,7 +12,11 @@ function normalizeAgentId(agentId: string | null | undefined): string | undefine
|
|||||||
export function resolveIframeAssistantId({
|
export function resolveIframeAssistantId({
|
||||||
selectedAgentId,
|
selectedAgentId,
|
||||||
threadMetadataAgentId,
|
threadMetadataAgentId,
|
||||||
|
preferDefaultAssistant = false,
|
||||||
}: ResolveIframeAssistantIdInput): string | undefined {
|
}: ResolveIframeAssistantIdInput): string | undefined {
|
||||||
|
if (preferDefaultAssistant) {
|
||||||
|
return normalizeAgentId(selectedAgentId);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
normalizeAgentId(selectedAgentId) ??
|
normalizeAgentId(selectedAgentId) ??
|
||||||
normalizeAgentId(threadMetadataAgentId)
|
normalizeAgentId(threadMetadataAgentId)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, Request
|
from fastapi import APIRouter, HTTPException, Request
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
@@ -14,6 +15,8 @@ from deerflow.config import get_app_config
|
|||||||
from deerflow.config.system_settings import (
|
from deerflow.config.system_settings import (
|
||||||
LeaderboardSettings,
|
LeaderboardSettings,
|
||||||
ModelConcurrencySettings,
|
ModelConcurrencySettings,
|
||||||
|
PluginDomainAgentMapping,
|
||||||
|
PluginManagementSettings,
|
||||||
PromptPrefixSettings,
|
PromptPrefixSettings,
|
||||||
PublishSquare,
|
PublishSquare,
|
||||||
PublishSquaresSettings,
|
PublishSquaresSettings,
|
||||||
@@ -43,6 +46,21 @@ class PromptPrefixUpdateRequest(BaseModel):
|
|||||||
prompt_prefix: str | None = None
|
prompt_prefix: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class PluginDomainAgentMappingItem(BaseModel):
|
||||||
|
id: str = Field(..., description="Stable row id")
|
||||||
|
domain: str = Field(..., description="Normalized host matched against pageContext.url")
|
||||||
|
agent_id: str = Field(..., description="Agent id selected for the host")
|
||||||
|
enabled: bool = Field(default=True, description="Whether the mapping is active")
|
||||||
|
|
||||||
|
|
||||||
|
class PluginManagementResponse(BaseModel):
|
||||||
|
domain_agent_mappings: list[PluginDomainAgentMappingItem] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
class PluginManagementUpdateRequest(BaseModel):
|
||||||
|
domain_agent_mappings: list[PluginDomainAgentMappingItem] | None = None
|
||||||
|
|
||||||
|
|
||||||
class PrivateSquareStatusResponse(BaseModel):
|
class PrivateSquareStatusResponse(BaseModel):
|
||||||
"""Status of the private-square feature, readable by any logged-in user.
|
"""Status of the private-square feature, readable by any logged-in user.
|
||||||
|
|
||||||
@@ -91,6 +109,34 @@ def _to_response(settings: PromptPrefixSettings) -> PromptPrefixResponse:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_plugin_domain(value: str | None) -> str:
|
||||||
|
raw = (value or "").strip()
|
||||||
|
if not raw:
|
||||||
|
return ""
|
||||||
|
candidate = raw if "://" in raw else f"https://{raw}"
|
||||||
|
try:
|
||||||
|
parsed = urlparse(candidate)
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
return (parsed.netloc or parsed.path or "").strip().lower()
|
||||||
|
|
||||||
|
|
||||||
|
def _plugin_management_to_response(
|
||||||
|
settings: PluginManagementSettings,
|
||||||
|
) -> PluginManagementResponse:
|
||||||
|
return PluginManagementResponse(
|
||||||
|
domain_agent_mappings=[
|
||||||
|
PluginDomainAgentMappingItem(
|
||||||
|
id=item.id,
|
||||||
|
domain=item.domain,
|
||||||
|
agent_id=item.agent_id,
|
||||||
|
enabled=item.enabled,
|
||||||
|
)
|
||||||
|
for item in settings.domain_agent_mappings
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def _require_admin(request: Request) -> None:
|
async def _require_admin(request: Request) -> None:
|
||||||
user = await get_optional_user_from_request(request)
|
user = await get_optional_user_from_request(request)
|
||||||
if user is None:
|
if user is None:
|
||||||
@@ -117,6 +163,50 @@ async def update_prompt_prefix(body: PromptPrefixUpdateRequest, request: Request
|
|||||||
return _to_response(saved.prompt_prefix)
|
return _to_response(saved.prompt_prefix)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/plugin-management", response_model=PluginManagementResponse)
|
||||||
|
async def get_plugin_management() -> PluginManagementResponse:
|
||||||
|
"""Return the iframe plugin domain -> agent mappings."""
|
||||||
|
return _plugin_management_to_response(load_system_settings().plugin_management)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/plugin-management", response_model=PluginManagementResponse)
|
||||||
|
async def update_plugin_management(
|
||||||
|
body: PluginManagementUpdateRequest, request: Request
|
||||||
|
) -> PluginManagementResponse:
|
||||||
|
"""Replace the iframe plugin domain -> agent mappings. Admin only."""
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
await _require_admin(request)
|
||||||
|
|
||||||
|
current = load_system_settings()
|
||||||
|
mappings: list[PluginDomainAgentMapping] = []
|
||||||
|
seen_domains: set[str] = set()
|
||||||
|
for item in body.domain_agent_mappings or []:
|
||||||
|
domain = _normalize_plugin_domain(item.domain)
|
||||||
|
agent_id = (item.agent_id or "").strip()
|
||||||
|
if not domain or not agent_id:
|
||||||
|
continue
|
||||||
|
if domain in seen_domains:
|
||||||
|
continue
|
||||||
|
seen_domains.add(domain)
|
||||||
|
mappings.append(
|
||||||
|
PluginDomainAgentMapping(
|
||||||
|
id=(item.id or "").strip() or uuid4().hex,
|
||||||
|
domain=domain,
|
||||||
|
agent_id=agent_id,
|
||||||
|
enabled=bool(item.enabled),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
updated = current.plugin_management.model_copy(
|
||||||
|
update={"domain_agent_mappings": mappings}
|
||||||
|
)
|
||||||
|
saved = save_system_settings(
|
||||||
|
current.model_copy(update={"plugin_management": updated})
|
||||||
|
)
|
||||||
|
return _plugin_management_to_response(saved.plugin_management)
|
||||||
|
|
||||||
|
|
||||||
class SkillCompressionResponse(BaseModel):
|
class SkillCompressionResponse(BaseModel):
|
||||||
enabled: bool = Field(..., description="Whether skill compression is active")
|
enabled: bool = Field(..., description="Whether skill compression is active")
|
||||||
search_top_n: int = Field(..., description="Max skills returned by the search_skills tool")
|
search_top_n: int = Field(..., description="Max skills returned by the search_skills tool")
|
||||||
|
|||||||
@@ -206,6 +206,26 @@ class AgentModuleSettings(BaseModel):
|
|||||||
model_config = ConfigDict(extra="ignore")
|
model_config = ConfigDict(extra="ignore")
|
||||||
|
|
||||||
|
|
||||||
|
class PluginDomainAgentMapping(BaseModel):
|
||||||
|
"""One configured host -> agent mapping for iframe chat auto-selection."""
|
||||||
|
|
||||||
|
id: str = Field(..., description="Stable row id for client-side editing")
|
||||||
|
domain: str = Field(..., description="Normalized host matched against pageContext.url")
|
||||||
|
agent_id: str = Field(..., description="Agent id selected when the host matches")
|
||||||
|
enabled: bool = Field(default=True, description="Whether this mapping is active")
|
||||||
|
model_config = ConfigDict(extra="ignore")
|
||||||
|
|
||||||
|
|
||||||
|
class PluginManagementSettings(BaseModel):
|
||||||
|
"""Admin-configurable iframe plugin mappings."""
|
||||||
|
|
||||||
|
domain_agent_mappings: list[PluginDomainAgentMapping] = Field(
|
||||||
|
default_factory=list,
|
||||||
|
description="Ordered host -> agent mappings used by the iframe chat entry page.",
|
||||||
|
)
|
||||||
|
model_config = ConfigDict(extra="ignore")
|
||||||
|
|
||||||
|
|
||||||
def get_module_tag_ids(settings: AgentModuleSettings, key: str) -> list[str]:
|
def get_module_tag_ids(settings: AgentModuleSettings, key: str) -> list[str]:
|
||||||
"""Return the configured agent-tag ids for ``key`` (empty when unset)."""
|
"""Return the configured agent-tag ids for ``key`` (empty when unset)."""
|
||||||
return list(settings.modules.get(key, []))
|
return list(settings.modules.get(key, []))
|
||||||
@@ -280,6 +300,7 @@ class SystemSettings(BaseModel):
|
|||||||
citation_display: CitationDisplaySettings = Field(default_factory=CitationDisplaySettings)
|
citation_display: CitationDisplaySettings = Field(default_factory=CitationDisplaySettings)
|
||||||
leaderboard: LeaderboardSettings = Field(default_factory=LeaderboardSettings)
|
leaderboard: LeaderboardSettings = Field(default_factory=LeaderboardSettings)
|
||||||
skill_compression: SkillCompressionSettings = Field(default_factory=SkillCompressionSettings)
|
skill_compression: SkillCompressionSettings = Field(default_factory=SkillCompressionSettings)
|
||||||
|
plugin_management: PluginManagementSettings = Field(default_factory=PluginManagementSettings)
|
||||||
agent_modules: AgentModuleSettings = Field(default_factory=AgentModuleSettings)
|
agent_modules: AgentModuleSettings = Field(default_factory=AgentModuleSettings)
|
||||||
concurrency_monitor: ConcurrencyMonitorSettings = Field(default_factory=ConcurrencyMonitorSettings)
|
concurrency_monitor: ConcurrencyMonitorSettings = Field(default_factory=ConcurrencyMonitorSettings)
|
||||||
model_concurrency: ModelConcurrencySettings = Field(default_factory=ModelConcurrencySettings)
|
model_concurrency: ModelConcurrencySettings = Field(default_factory=ModelConcurrencySettings)
|
||||||
|
|||||||
Reference in new issue
Block a user