feat: 插件管理及域名和智能体对应
This commit is contained in:
1 parent
eb61b332a2
commit
e9e4c181b9
9 files changed
+414
-27
No files matched your search
@@ -11,6 +11,7 @@ import {
|
||||
GaugeIcon,
|
||||
HistoryIcon,
|
||||
MessageCircleQuestionIcon,
|
||||
PlugIcon,
|
||||
ScrollTextIcon,
|
||||
Settings2Icon,
|
||||
SettingsIcon,
|
||||
@@ -218,6 +219,14 @@ export function WorkspaceNavMenu() {
|
||||
<TagsIcon />
|
||||
{t.adminTags.title}
|
||||
</DropdownMenuItem>
|
||||
{isAdmin && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => navigate("/page/workspace/admin/plugins")}
|
||||
>
|
||||
<PlugIcon />
|
||||
插件管理
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => navigate("/page/workspace/admin/positions")}
|
||||
|
||||
@@ -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<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> {
|
||||
const response = await apiFetch(
|
||||
`${getBackendBaseURL()}/api/system-settings/prompt-prefix`,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
options?: { additionalKwargs?: Record<string, unknown> };
|
||||
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<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 } =
|
||||
useAgentSelector();
|
||||
const { data: pluginManagementSettings } = usePluginManagementSettings();
|
||||
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();
|
||||
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)}`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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({
|
||||
</WorkspaceLayout>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="admin/plugins"
|
||||
element={
|
||||
getStoredAuth()?.system_role === 'admin' ? (
|
||||
<WorkspaceLayout {...layoutProps}>
|
||||
<PluginManagementPage />
|
||||
</WorkspaceLayout>
|
||||
) : (
|
||||
<Navigate to="/page/workspace/chats/new" replace />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="admin/positions"
|
||||
element={
|
||||
|
||||
@@ -33,3 +33,14 @@ test("falls back to lead-agent mode when no agent is selected or bound", () => {
|
||||
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 = {
|
||||
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)
|
||||
|
||||
Reference in new issue
Block a user