feat(chat): 添加权限批准模式切换
This commit is contained in:
@@ -26,7 +26,9 @@ import KeyboardArrowUpRounded from "@mui/icons-material/KeyboardArrowUpRounded";
|
|||||||
import AttachFileRounded from "@mui/icons-material/AttachFileRounded";
|
import AttachFileRounded from "@mui/icons-material/AttachFileRounded";
|
||||||
import BoltRounded from "@mui/icons-material/BoltRounded";
|
import BoltRounded from "@mui/icons-material/BoltRounded";
|
||||||
import AutoAwesomeRounded from "@mui/icons-material/AutoAwesomeRounded";
|
import AutoAwesomeRounded from "@mui/icons-material/AutoAwesomeRounded";
|
||||||
import type { AgentModel } from "@/lib/chatStream";
|
import VerifiedUserRounded from "@mui/icons-material/VerifiedUserRounded";
|
||||||
|
import AdminPanelSettingsRounded from "@mui/icons-material/AdminPanelSettingsRounded";
|
||||||
|
import type { AgentApprovalMode, AgentModel } from "@/lib/chatStream";
|
||||||
|
|
||||||
export type AgentComposerHandle = {
|
export type AgentComposerHandle = {
|
||||||
focus: () => void;
|
focus: () => void;
|
||||||
@@ -48,6 +50,8 @@ type AgentComposerProps = {
|
|||||||
onStopListening: () => void;
|
onStopListening: () => void;
|
||||||
selectedModel: AgentModel;
|
selectedModel: AgentModel;
|
||||||
onModelChange: (model: AgentModel) => void;
|
onModelChange: (model: AgentModel) => void;
|
||||||
|
approvalMode: AgentApprovalMode;
|
||||||
|
onApprovalModeChange: (mode: AgentApprovalMode) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AgentComposer = React.forwardRef<AgentComposerHandle, AgentComposerProps>(function AgentComposer({
|
export const AgentComposer = React.forwardRef<AgentComposerHandle, AgentComposerProps>(function AgentComposer({
|
||||||
@@ -62,6 +66,8 @@ export const AgentComposer = React.forwardRef<AgentComposerHandle, AgentComposer
|
|||||||
onStopListening,
|
onStopListening,
|
||||||
selectedModel,
|
selectedModel,
|
||||||
onModelChange,
|
onModelChange,
|
||||||
|
approvalMode,
|
||||||
|
onApprovalModeChange,
|
||||||
}, ref) {
|
}, ref) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const inputRef = React.useRef<HTMLInputElement | HTMLTextAreaElement | null>(null);
|
const inputRef = React.useRef<HTMLInputElement | HTMLTextAreaElement | null>(null);
|
||||||
@@ -245,6 +251,96 @@ export const AgentComposer = React.forwardRef<AgentComposerHandle, AgentComposer
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
)
|
)
|
||||||
) : null}
|
) : null}
|
||||||
|
<FormControl size="small" sx={{ minWidth: 102 }}>
|
||||||
|
<Select
|
||||||
|
value={approvalMode}
|
||||||
|
onChange={(event) =>
|
||||||
|
onApprovalModeChange(event.target.value as AgentApprovalMode)
|
||||||
|
}
|
||||||
|
disabled={isHydrating || isStreaming}
|
||||||
|
aria-label="权限批准模式"
|
||||||
|
renderValue={(val) => (
|
||||||
|
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5 }}>
|
||||||
|
{val === "always" ? (
|
||||||
|
<AdminPanelSettingsRounded sx={{ fontSize: 16, color: "inherit" }} />
|
||||||
|
) : (
|
||||||
|
<VerifiedUserRounded sx={{ fontSize: 16, color: "inherit" }} />
|
||||||
|
)}
|
||||||
|
<Typography sx={{ fontSize: "0.78rem", fontWeight: 700, color: "inherit" }}>
|
||||||
|
{val === "always" ? "始终允许" : "请求批准"}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
MenuProps={{
|
||||||
|
anchorOrigin: { vertical: "top", horizontal: "left" },
|
||||||
|
transformOrigin: { vertical: "bottom", horizontal: "left" },
|
||||||
|
sx: { zIndex: (theme) => theme.zIndex.modal + 110 },
|
||||||
|
PaperProps: {
|
||||||
|
sx: {
|
||||||
|
mb: 1.5,
|
||||||
|
width: 210,
|
||||||
|
borderRadius: 4,
|
||||||
|
bgcolor: alpha("#fff", 0.9),
|
||||||
|
backdropFilter: "blur(24px)",
|
||||||
|
border: `1px solid ${alpha("#fff", 0.9)}`,
|
||||||
|
boxShadow: `0 -12px 40px ${alpha("#000", 0.08)}`,
|
||||||
|
"& .MuiList-root": { p: 1 },
|
||||||
|
"& .MuiMenuItem-root": {
|
||||||
|
px: 1.5,
|
||||||
|
py: 1.2,
|
||||||
|
mb: 0.5,
|
||||||
|
borderRadius: 3,
|
||||||
|
alignItems: "flex-start",
|
||||||
|
"&:last-child": { mb: 0 },
|
||||||
|
"&.Mui-selected": {
|
||||||
|
bgcolor: alpha("#00acc1", 0.08),
|
||||||
|
"&:hover": { bgcolor: alpha("#00acc1", 0.12) },
|
||||||
|
"& .title": { color: "#00838f" },
|
||||||
|
"& .icon": { color: "#00acc1" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
height: 36,
|
||||||
|
borderRadius: "18px",
|
||||||
|
bgcolor: alpha("#fff", 0.6),
|
||||||
|
color: "text.secondary",
|
||||||
|
".MuiOutlinedInput-notchedOutline": { border: "none" },
|
||||||
|
".MuiSelect-select": {
|
||||||
|
py: 0,
|
||||||
|
pl: 1,
|
||||||
|
pr: "28px !important",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
"&:hover, &:has(.MuiSelect-select[aria-expanded=\"true\"])": {
|
||||||
|
bgcolor: alpha("#000", 0.06),
|
||||||
|
color: "text.primary",
|
||||||
|
},
|
||||||
|
".MuiSelect-icon": {
|
||||||
|
color: "text.secondary",
|
||||||
|
right: 4,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem value="request">
|
||||||
|
<VerifiedUserRounded className="icon" sx={{ mr: 1.5, mt: 0.2, fontSize: 18, color: "text.secondary" }} />
|
||||||
|
<Box>
|
||||||
|
<Typography className="title" sx={{ fontSize: "0.85rem", fontWeight: 700, color: "text.primary", mb: 0.2 }}>请求批准</Typography>
|
||||||
|
<Typography sx={{ fontSize: "0.7rem", fontWeight: 500, color: "text.secondary", lineHeight: 1.3 }}>工具权限逐次确认</Typography>
|
||||||
|
</Box>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem value="always">
|
||||||
|
<AdminPanelSettingsRounded className="icon" sx={{ mr: 1.5, mt: 0.2, fontSize: 18, color: "text.secondary" }} />
|
||||||
|
<Box>
|
||||||
|
<Typography className="title" sx={{ fontSize: "0.85rem", fontWeight: 700, color: "text.primary", mb: 0.2 }}>始终允许</Typography>
|
||||||
|
<Typography sx={{ fontSize: "0.7rem", fontWeight: 500, color: "text.secondary", lineHeight: 1.3 }}>自动允许本轮权限请求</Typography>
|
||||||
|
</Box>
|
||||||
|
</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Stack direction="row" spacing={1} alignItems="center">
|
<Stack direction="row" spacing={1} alignItems="center">
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { Box, Drawer, alpha, useTheme } from "@mui/material";
|
|||||||
import { useNotification } from "@refinedev/core";
|
import { useNotification } from "@refinedev/core";
|
||||||
|
|
||||||
import { getAccessToken } from "@/lib/authToken";
|
import { getAccessToken } from "@/lib/authToken";
|
||||||
import type { AgentModel } from "@/lib/chatStream";
|
import type { AgentApprovalMode, AgentModel } from "@/lib/chatStream";
|
||||||
import { useProjectStore } from "@/store/projectStore";
|
import { useProjectStore } from "@/store/projectStore";
|
||||||
import { AgentComposer, type AgentComposerHandle } from "./AgentComposer";
|
import { AgentComposer, type AgentComposerHandle } from "./AgentComposer";
|
||||||
import { AgentHeader } from "./AgentHeader";
|
import { AgentHeader } from "./AgentHeader";
|
||||||
@@ -31,6 +31,8 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
|
|||||||
const [selectedModel, setSelectedModel] = useState<AgentModel>(
|
const [selectedModel, setSelectedModel] = useState<AgentModel>(
|
||||||
"deepseek/deepseek-v4-pro",
|
"deepseek/deepseek-v4-pro",
|
||||||
);
|
);
|
||||||
|
const [approvalMode, setApprovalMode] =
|
||||||
|
useState<AgentApprovalMode>("request");
|
||||||
|
|
||||||
const bottomRef = useRef<HTMLDivElement>(null);
|
const bottomRef = useRef<HTMLDivElement>(null);
|
||||||
const composerRef = useRef<AgentComposerHandle | null>(null);
|
const composerRef = useRef<AgentComposerHandle | null>(null);
|
||||||
@@ -85,6 +87,7 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
|
|||||||
onToolCall: handleToolCall,
|
onToolCall: handleToolCall,
|
||||||
onBeforeSend: stopListening,
|
onBeforeSend: stopListening,
|
||||||
getModel: () => selectedModel,
|
getModel: () => selectedModel,
|
||||||
|
getApprovalMode: () => approvalMode,
|
||||||
});
|
});
|
||||||
|
|
||||||
const scrollToBottom = useCallback((behavior: ScrollBehavior = "smooth") => {
|
const scrollToBottom = useCallback((behavior: ScrollBehavior = "smooth") => {
|
||||||
@@ -371,6 +374,8 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
|
|||||||
onStopListening={stopListening}
|
onStopListening={stopListening}
|
||||||
selectedModel={selectedModel}
|
selectedModel={selectedModel}
|
||||||
onModelChange={setSelectedModel}
|
onModelChange={setSelectedModel}
|
||||||
|
approvalMode={approvalMode}
|
||||||
|
onApprovalModeChange={setApprovalMode}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -9,7 +9,12 @@ import {
|
|||||||
resumeAgentChatStream,
|
resumeAgentChatStream,
|
||||||
streamAgentChat,
|
streamAgentChat,
|
||||||
} from "@/lib/chatStream";
|
} from "@/lib/chatStream";
|
||||||
import type { AgentModel, PermissionReply, StreamEvent } from "@/lib/chatStream";
|
import type {
|
||||||
|
AgentApprovalMode,
|
||||||
|
AgentModel,
|
||||||
|
PermissionReply,
|
||||||
|
StreamEvent,
|
||||||
|
} from "@/lib/chatStream";
|
||||||
import type {
|
import type {
|
||||||
AgentArtifact,
|
AgentArtifact,
|
||||||
AgentPermissionRequest,
|
AgentPermissionRequest,
|
||||||
@@ -45,6 +50,7 @@ type UseAgentChatSessionOptions = {
|
|||||||
) => void;
|
) => void;
|
||||||
onBeforeSend?: () => void;
|
onBeforeSend?: () => void;
|
||||||
getModel?: () => AgentModel;
|
getModel?: () => AgentModel;
|
||||||
|
getApprovalMode?: () => AgentApprovalMode;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PromptRunOptions = {
|
type PromptRunOptions = {
|
||||||
@@ -210,6 +216,7 @@ export const useAgentChatSession = ({
|
|||||||
onToolCall,
|
onToolCall,
|
||||||
onBeforeSend,
|
onBeforeSend,
|
||||||
getModel,
|
getModel,
|
||||||
|
getApprovalMode,
|
||||||
}: UseAgentChatSessionOptions) => {
|
}: UseAgentChatSessionOptions) => {
|
||||||
const hydrationCompletedRef = useRef(false);
|
const hydrationCompletedRef = useRef(false);
|
||||||
const hydrationNonceRef = useRef(0);
|
const hydrationNonceRef = useRef(0);
|
||||||
@@ -636,6 +643,7 @@ export const useAgentChatSession = ({
|
|||||||
message: prompt,
|
message: prompt,
|
||||||
sessionId: sessionIdOverride ?? sessionIdRef.current,
|
sessionId: sessionIdOverride ?? sessionIdRef.current,
|
||||||
model: getModel?.(),
|
model: getModel?.(),
|
||||||
|
approvalMode: getApprovalMode?.(),
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
onEvent: (event) =>
|
onEvent: (event) =>
|
||||||
applyStreamEvent(event, {
|
applyStreamEvent(event, {
|
||||||
@@ -686,7 +694,15 @@ export const useAgentChatSession = ({
|
|||||||
setIsStreaming(false);
|
setIsStreaming(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[applyStreamEvent, getModel, isHydrating, isStreaming, messages, onBeforeSend],
|
[
|
||||||
|
applyStreamEvent,
|
||||||
|
getApprovalMode,
|
||||||
|
getModel,
|
||||||
|
isHydrating,
|
||||||
|
isStreaming,
|
||||||
|
messages,
|
||||||
|
onBeforeSend,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const abort = useCallback(() => {
|
const abort = useCallback(() => {
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ describe("streamAgentChat", () => {
|
|||||||
message: "hi",
|
message: "hi",
|
||||||
session_id: undefined,
|
session_id: undefined,
|
||||||
model: "deepseek/deepseek-v4-pro",
|
model: "deepseek/deepseek-v4-pro",
|
||||||
|
approval_mode: undefined,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export type AgentModel =
|
|||||||
| "deepseek/deepseek-v4-pro";
|
| "deepseek/deepseek-v4-pro";
|
||||||
|
|
||||||
export type PermissionReply = "once" | "always" | "reject";
|
export type PermissionReply = "once" | "always" | "reject";
|
||||||
|
export type AgentApprovalMode = "request" | "always";
|
||||||
|
|
||||||
export type StreamEvent =
|
export type StreamEvent =
|
||||||
| {
|
| {
|
||||||
@@ -69,6 +70,7 @@ type StreamOptions = {
|
|||||||
message: string;
|
message: string;
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
model?: AgentModel;
|
model?: AgentModel;
|
||||||
|
approvalMode?: AgentApprovalMode;
|
||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
onEvent: (event: StreamEvent) => void;
|
onEvent: (event: StreamEvent) => void;
|
||||||
};
|
};
|
||||||
@@ -283,6 +285,7 @@ export const streamAgentChat = async ({
|
|||||||
message,
|
message,
|
||||||
sessionId,
|
sessionId,
|
||||||
model,
|
model,
|
||||||
|
approvalMode,
|
||||||
signal,
|
signal,
|
||||||
onEvent,
|
onEvent,
|
||||||
}: StreamOptions) => {
|
}: StreamOptions) => {
|
||||||
@@ -301,6 +304,7 @@ export const streamAgentChat = async ({
|
|||||||
message,
|
message,
|
||||||
session_id: sessionId,
|
session_id: sessionId,
|
||||||
model,
|
model,
|
||||||
|
approval_mode: approvalMode,
|
||||||
}),
|
}),
|
||||||
projectHeaderMode: "include",
|
projectHeaderMode: "include",
|
||||||
userHeaderMode: "include",
|
userHeaderMode: "include",
|
||||||
|
|||||||
Reference in New Issue
Block a user