diff --git a/frontend-web/src/components/workspace/workspace-nav-menu.tsx b/frontend-web/src/components/workspace/workspace-nav-menu.tsx
index 73a56ac4..5df42f84 100644
--- a/frontend-web/src/components/workspace/workspace-nav-menu.tsx
+++ b/frontend-web/src/components/workspace/workspace-nav-menu.tsx
@@ -11,6 +11,7 @@ import {
GaugeIcon,
HistoryIcon,
MessageCircleQuestionIcon,
+ PlugIcon,
ScrollTextIcon,
Settings2Icon,
SettingsIcon,
@@ -218,6 +219,14 @@ export function WorkspaceNavMenu() {
{t.adminTags.title}
+ {isAdmin && (
+ navigate("/page/workspace/admin/plugins")}
+ >
+
+ 插件管理
+
+ )}
{isAdmin && (
navigate("/page/workspace/admin/positions")}
diff --git a/frontend-web/src/core/system-settings/api.ts b/frontend-web/src/core/system-settings/api.ts
index 778b273a..dacfa808 100644
--- a/frontend-web/src/core/system-settings/api.ts
+++ b/frontend-web/src/core/system-settings/api.ts
@@ -8,12 +8,115 @@ export interface PromptPrefixSettings {
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 {
enabled?: boolean;
switch_label?: 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 {
+ 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 {
+ 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 {
const response = await apiFetch(
`${getBackendBaseURL()}/api/system-settings/prompt-prefix`,
diff --git a/frontend-web/src/core/system-settings/hooks.ts b/frontend-web/src/core/system-settings/hooks.ts
index 5ae171df..b21902f7 100644
--- a/frontend-web/src/core/system-settings/hooks.ts
+++ b/frontend-web/src/core/system-settings/hooks.ts
@@ -8,6 +8,7 @@ import {
loadCitationDisplaySettings,
loadLeaderboardSettings,
loadModelConcurrencySettings,
+ loadPluginManagementSettings,
loadPrivateSquareStatus,
loadPromptPrefixSettings,
loadPublishSquares,
@@ -17,6 +18,7 @@ import {
updateCitationDisplaySettings,
updateLeaderboardSettings,
updateModelConcurrencySettings,
+ updatePluginManagementSettings,
updatePrivateSquareSettings,
updatePromptPrefixSettings,
updatePublishSquares,
@@ -25,6 +27,7 @@ import {
type CitationDisplayUpdate,
type LeaderboardSettingsUpdate,
type ModelConcurrencyUpdate,
+ type PluginManagementUpdate,
type PrivateSquareUpdate,
type PromptPrefixUpdate,
type PublishSquaresUpdate,
@@ -41,6 +44,7 @@ const CITATION_DISPLAY_KEY = ["systemSettings", "citationDisplay"] as const;
const LEADERBOARD_KEY = ["systemSettings", "leaderboard"] as const;
const SKILL_COMPRESSION_KEY = ["systemSettings", "skillCompression"] as const;
const MODEL_CONCURRENCY_KEY = ["systemSettings", "modelConcurrency"] as const;
+const PLUGIN_MANAGEMENT_KEY = ["systemSettings", "pluginManagement"] as const;
export function usePromptPrefixSettings() {
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() {
return useQuery({
queryKey: LEADERBOARD_KEY,
diff --git a/frontend-web/src/pages/IframeChatPage.tsx b/frontend-web/src/pages/IframeChatPage.tsx
index ea526b4d..b847d3a0 100644
--- a/frontend-web/src/pages/IframeChatPage.tsx
+++ b/frontend-web/src/pages/IframeChatPage.tsx
@@ -26,18 +26,19 @@ import { Tooltip } from "@/components/workspace/tooltip";
import { Button } from "@/components/ui/button";
import { TooltipProvider } from "@/components/ui/tooltip";
import { useAgentSelector } from "@/core/agents";
+import { resolveIncomingIframeSelection } from "@/core/plugin-management/domain-agent";
import { getAccessToken } from "@/core/auth";
import { useI18n } from "@/core/i18n/hooks";
import { useModels } from "@/core/models/hooks";
import { useNotification } from "@/core/notification/hooks";
import { useRouteBase } from "@/core/route-base";
import {
- getLastAgentId,
isThreadRagModeLocked,
saveLastAgentId,
useLocalSettings,
useThreadSettings,
} from "@/core/settings";
+import { usePluginManagementSettings } from "@/core/system-settings/hooks";
import { useThreadStream } from "@/core/threads/hooks";
import { textOfMessage } from "@/core/threads/utils";
import { env } from "@/env";
@@ -50,6 +51,22 @@ import { resolveIframeAssistantId } from "./iframe-chat-agent";
import { buildIframeModeButtons } from "./iframe-chat-mode-buttons";
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 = {
source: "brainy-iframe";
type: "brainy:auth-token";
@@ -66,6 +83,13 @@ type BrainyChatInterruptMessage = {
};
requestId?: string;
};
+type PendingIframeSubmit = {
+ message: PromptInputMessage;
+ externalContext?: Record;
+ options?: { additionalKwargs?: Record };
+ targetSelectedAgentId: string | null;
+ targetForceDefaultAssistant: boolean;
+};
function postBrainyChatStatus(status: BrainyChatStatus, requestId: string) {
window.parent?.postMessage(
@@ -104,9 +128,22 @@ export default function IframeChatPage() {
const [mounted, setMounted] = useState(false);
const { tokenUsageEnabled } = useModels();
const [selectedAgentId, setSelectedAgentId] = useState(null);
+ const [manualSelectedAgentId, setManualSelectedAgentId] = useState(null);
+ const [forceDefaultAssistant, setForceDefaultAssistant] = useState(true);
+ const [pendingIframeSubmit, setPendingIframeSubmit] =
+ useState(null);
const { agents: selectorAgents, isLoading: selectorLoading } =
useAgentSelector();
+ const { data: pluginManagementSettings } = usePluginManagementSettings();
const pendingRequestIdRef = useRef(null);
+ const handleSubmitRef = useRef<
+ | ((
+ message: PromptInputMessage,
+ externalContext?: Record,
+ options?: { additionalKwargs?: Record },
+ ) => void)
+ | null
+ >(null);
const { selectedArtifact } = useArtifacts();
useSpecificChatMode();
@@ -139,23 +176,37 @@ export default function IframeChatPage() {
useEffect(() => {
if (isNewThread) {
setHasStartedConversation(false);
- setSelectedAgentId(getLastAgentId());
+ setSelectedAgentId(null);
+ setManualSelectedAgentId(null);
+ setForceDefaultAssistant(true);
+ setPendingIframeSubmit(null);
}
}, [isNewThread, threadId]);
useEffect(() => {
- if (selectorLoading || !selectedAgentId) {
+ if (selectorLoading) {
return;
}
- if (!selectorAgents.some((agent) => agent.id === selectedAgentId)) {
+ if (
+ selectedAgentId &&
+ !selectorAgents.some((agent) => agent.id === selectedAgentId)
+ ) {
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 activeAssistantId = resolveIframeAssistantId({
selectedAgentId,
threadMetadataAgentId: null,
+ preferDefaultAssistant: forceDefaultAssistant,
});
const [thread, sendMessage, isUploading] = useThreadStream({
@@ -209,14 +260,18 @@ export default function IframeChatPage() {
})();
useEffect(() => {
- const resolvedAgentId = resolveIframeAssistantId({
- selectedAgentId,
- threadMetadataAgentId,
- });
- if (resolvedAgentId && resolvedAgentId !== selectedAgentId) {
- setSelectedAgentId(resolvedAgentId);
- }
- }, [selectedAgentId, threadMetadataAgentId]);
+ if (!threadMetadataAgentId || isNewThread) return;
+ if (manualSelectedAgentId) return;
+ if (forceDefaultAssistant) return;
+ if (selectedAgentId) return;
+ setSelectedAgentId(threadMetadataAgentId);
+ }, [
+ forceDefaultAssistant,
+ isNewThread,
+ manualSelectedAgentId,
+ selectedAgentId,
+ threadMetadataAgentId,
+ ]);
const writingArtifactPath = useMemo(() => {
const isMarkdown = (path: string | null | undefined) =>
@@ -277,10 +332,43 @@ export default function IframeChatPage() {
);
const handleAgentChange = useCallback((agentId: string | null) => {
+ setManualSelectedAgentId(agentId);
setSelectedAgentId(agentId);
+ setForceDefaultAssistant(!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 ragModeEnabled =
localSettings.display.ragModeEnabled || ragModeLocked;
@@ -353,10 +441,22 @@ export default function IframeChatPage() {
}
if (data?.type !== "brainy:chat-submit") return;
const submitMessage = data as BrainyChatSubmitMessage;
-
const { submitText, files, iframeContext, additionalKwargs } =
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("收到插件消息", {
text: submitMessage.payload?.text,
prompt: submitMessage.payload?.prompt,
@@ -368,26 +468,31 @@ export default function IframeChatPage() {
iframeContext,
});
- if (!submitText && files.length === 0) return;
-
pendingRequestIdRef.current = submitMessage.requestId ?? null;
-
- handleSubmit(
- { text: submitText, files },
- iframeContext,
- additionalKwargs ? { additionalKwargs } : undefined,
- );
+ setPendingIframeSubmit({
+ message: { text: submitText, files },
+ externalContext: iframeContext,
+ options: additionalKwargs ? { additionalKwargs } : undefined,
+ targetSelectedAgentId: nextSelection.selectedAgentId,
+ targetForceDefaultAssistant: nextSelection.forceDefaultAssistant,
+ });
};
window.addEventListener("message", handleMessage);
return () => {
window.removeEventListener("message", handleMessage);
};
- }, [handleInterrupt, handleSubmit]);
+ }, [
+ handleInterrupt,
+ manualSelectedAgentId,
+ pluginManagementSettings?.domainAgentMappings,
+ selectorAgents,
+ selectorLoading,
+ ]);
const isLandingState = isNewThread && !hasStartedConversation;
const bottomBarHeightClass = "pb-24";
- const modeButtons = buildIframeModeButtons({
+ const modeButtons: ResolvedIframeModeButton[] = buildIframeModeButtons({
agentSelected: !!selectedAgentId,
writingModeEnabled: !!settings.context.writing_mode,
memoryInjectionEnabled: Boolean(
@@ -397,10 +502,15 @@ export default function IframeChatPage() {
ragModeLocked,
contextCompressionEnabled,
proModeEnabled,
- }).map((button) => {
+ }).map((button): ResolvedIframeModeButton => {
switch (button.key) {
case "agent":
- return button;
+ return {
+ key: "agent",
+ label: button.label,
+ active: button.active,
+ disabled: button.disabled,
+ };
case "writing":
return {
...button,
@@ -437,7 +547,7 @@ export default function IframeChatPage() {
activeClassName: "text-fuchsia-600 hover:text-fuchsia-700",
};
default:
- return button;
+ throw new Error(`Unknown iframe mode button: ${String(button.key)}`);
}
});
diff --git a/frontend-web/src/pages/WorkspaceRoutes.tsx b/frontend-web/src/pages/WorkspaceRoutes.tsx
index bcc3bda5..1d58ac9f 100644
--- a/frontend-web/src/pages/WorkspaceRoutes.tsx
+++ b/frontend-web/src/pages/WorkspaceRoutes.tsx
@@ -42,6 +42,7 @@ import AdminMemoryPage from './AdminMemoryPage';
import AdminUserHubPage from './AdminUserHubPage';
import TagsPage from './TagsPage';
import PositionsPage from './PositionsPage';
+import PluginManagementPage from './PluginManagementPage';
import RecommendedQuestionsPage from './RecommendedQuestionsPage';
import AIWritingPage from '@/open-canvas/pages/AIWritingPage';
import StudioNotebooksPage from './StudioNotebooksPage';
@@ -515,6 +516,18 @@ export default function WorkspaceRoutes({
}
/>
+
+
+
+ ) : (
+
+ )
+ }
+ />
{
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,
+ );
+});
diff --git a/frontend-web/src/pages/iframe-chat-agent.ts b/frontend-web/src/pages/iframe-chat-agent.ts
index f10fdfa1..2ddb4b0b 100644
--- a/frontend-web/src/pages/iframe-chat-agent.ts
+++ b/frontend-web/src/pages/iframe-chat-agent.ts
@@ -1,6 +1,7 @@
type ResolveIframeAssistantIdInput = {
selectedAgentId: string | null;
threadMetadataAgentId: string | null;
+ preferDefaultAssistant?: boolean;
};
function normalizeAgentId(agentId: string | null | undefined): string | undefined {
@@ -11,7 +12,11 @@ function normalizeAgentId(agentId: string | null | undefined): string | undefine
export function resolveIframeAssistantId({
selectedAgentId,
threadMetadataAgentId,
+ preferDefaultAssistant = false,
}: ResolveIframeAssistantIdInput): string | undefined {
+ if (preferDefaultAssistant) {
+ return normalizeAgentId(selectedAgentId);
+ }
return (
normalizeAgentId(selectedAgentId) ??
normalizeAgentId(threadMetadataAgentId)
diff --git a/offline-backend-20260512/backend/app/gateway/routers/system_settings.py b/offline-backend-20260512/backend/app/gateway/routers/system_settings.py
index 079fc15f..baf5fceb 100644
--- a/offline-backend-20260512/backend/app/gateway/routers/system_settings.py
+++ b/offline-backend-20260512/backend/app/gateway/routers/system_settings.py
@@ -4,6 +4,7 @@ from __future__ import annotations
import logging
from typing import Literal
+from urllib.parse import urlparse
from fastapi import APIRouter, HTTPException, Request
from pydantic import BaseModel, Field
@@ -14,6 +15,8 @@ from deerflow.config import get_app_config
from deerflow.config.system_settings import (
LeaderboardSettings,
ModelConcurrencySettings,
+ PluginDomainAgentMapping,
+ PluginManagementSettings,
PromptPrefixSettings,
PublishSquare,
PublishSquaresSettings,
@@ -43,6 +46,21 @@ class PromptPrefixUpdateRequest(BaseModel):
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):
"""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:
user = await get_optional_user_from_request(request)
if user is None:
@@ -117,6 +163,50 @@ async def update_prompt_prefix(body: PromptPrefixUpdateRequest, request: Request
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):
enabled: bool = Field(..., description="Whether skill compression is active")
search_top_n: int = Field(..., description="Max skills returned by the search_skills tool")
diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/config/system_settings.py b/offline-backend-20260512/backend/packages/harness/deerflow/config/system_settings.py
index eac22b77..214f84c2 100644
--- a/offline-backend-20260512/backend/packages/harness/deerflow/config/system_settings.py
+++ b/offline-backend-20260512/backend/packages/harness/deerflow/config/system_settings.py
@@ -206,6 +206,26 @@ class AgentModuleSettings(BaseModel):
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]:
"""Return the configured agent-tag ids for ``key`` (empty when unset)."""
return list(settings.modules.get(key, []))
@@ -280,6 +300,7 @@ class SystemSettings(BaseModel):
citation_display: CitationDisplaySettings = Field(default_factory=CitationDisplaySettings)
leaderboard: LeaderboardSettings = Field(default_factory=LeaderboardSettings)
skill_compression: SkillCompressionSettings = Field(default_factory=SkillCompressionSettings)
+ plugin_management: PluginManagementSettings = Field(default_factory=PluginManagementSettings)
agent_modules: AgentModuleSettings = Field(default_factory=AgentModuleSettings)
concurrency_monitor: ConcurrencyMonitorSettings = Field(default_factory=ConcurrencyMonitorSettings)
model_concurrency: ModelConcurrencySettings = Field(default_factory=ModelConcurrencySettings)