mirror of
https://github.com/basketikun/infinite-canvas.git
synced 2026-08-02 15:01:14 +08:00
feat(agent): implement tool confirmation mode in chat input with automatic confirmation by default
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useRef, type ReactNode } from "react";
|
||||
import { Button, Tooltip } from "antd";
|
||||
import { ArrowUp, ImagePlus, LoaderCircle, Square, X } from "lucide-react";
|
||||
import { Button, Dropdown, Tooltip } from "antd";
|
||||
import { ArrowUp, Check, ChevronUp, Hand, ImagePlus, LoaderCircle, RefreshCw, Square, X } from "lucide-react";
|
||||
|
||||
import { canvasThemes } from "@/lib/canvas-theme";
|
||||
import { isPlainEnterKey } from "@/lib/keyboard-event";
|
||||
@@ -18,6 +18,8 @@ export function AgentChatComposer({
|
||||
onStop,
|
||||
onAddFiles,
|
||||
onRemoveAttachment,
|
||||
confirmTools,
|
||||
onConfirmToolsChange,
|
||||
left,
|
||||
}: {
|
||||
prompt: string;
|
||||
@@ -31,6 +33,8 @@ export function AgentChatComposer({
|
||||
onStop?: () => void;
|
||||
onAddFiles?: (files: FileList | File[] | null) => void | Promise<void>;
|
||||
onRemoveAttachment?: (id: string) => void;
|
||||
confirmTools?: boolean;
|
||||
onConfirmToolsChange?: (confirmTools: boolean) => void;
|
||||
left?: ReactNode;
|
||||
}) {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -84,6 +88,7 @@ export function AgentChatComposer({
|
||||
</Tooltip>
|
||||
</>
|
||||
) : null}
|
||||
{onConfirmToolsChange ? <ToolConfirmationMenu confirmTools={Boolean(confirmTools)} theme={theme} onChange={onConfirmToolsChange} /> : null}
|
||||
{left}
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-1.5">
|
||||
@@ -98,3 +103,45 @@ export function AgentChatComposer({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolConfirmationMenu({ confirmTools, theme, onChange }: { confirmTools: boolean; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; onChange: (confirmTools: boolean) => void }) {
|
||||
return (
|
||||
<Dropdown
|
||||
trigger={["click"]}
|
||||
placement="topLeft"
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: "manual",
|
||||
label: <ConfirmationOption icon={<Hand className="size-4" />} title="手动确认" description="Agent 执行画布写入前会请求确认" selected={confirmTools} />,
|
||||
onClick: () => onChange(true),
|
||||
},
|
||||
{
|
||||
key: "automatic",
|
||||
label: <ConfirmationOption icon={<RefreshCw className="size-4" />} title="自动确认" description="Agent 会自动执行画布写入操作" selected={!confirmTools} />,
|
||||
onClick: () => onChange(false),
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
<button type="button" className="flex h-9 shrink-0 items-center gap-1.5 rounded-full px-2.5 text-xs font-medium transition" style={{ background: theme.toolbar.activeBg, color: theme.node.text }} aria-label="选择工具确认模式">
|
||||
{confirmTools ? <Hand className="size-3.5" /> : <RefreshCw className="size-3.5" />}
|
||||
<span>{confirmTools ? "手动确认" : "自动确认"}</span>
|
||||
<ChevronUp className="size-3 opacity-50" />
|
||||
</button>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
function ConfirmationOption({ icon, title, description, selected }: { icon: ReactNode; title: string; description: string; selected: boolean }) {
|
||||
return (
|
||||
<div className="flex min-w-64 items-start gap-3 py-1">
|
||||
<span className="mt-0.5 shrink-0">{icon}</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block text-sm font-medium">{title}</span>
|
||||
<span className="mt-0.5 block text-xs leading-5 opacity-60">{description}</span>
|
||||
</span>
|
||||
{selected ? <Check className="mt-0.5 size-4 shrink-0" /> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { useEffect, useState, type ReactNode } from "react";
|
||||
import { Button, Image } from "antd";
|
||||
import { Brain, CheckCircle2, ChevronDown, Circle, CircleAlert, FilePenLine, FileText, ListChecks, LoaderCircle, Search, TerminalSquare, UserRound, Wrench, XCircle } from "lucide-react";
|
||||
import { Brain, CheckCircle2, ChevronDown, Circle, CircleAlert, FilePenLine, FileText, ListChecks, LoaderCircle, Search, TerminalSquare, Wrench, XCircle } from "lucide-react";
|
||||
import { Streamdown } from "streamdown";
|
||||
|
||||
import { canvasThemes } from "@/lib/canvas-theme";
|
||||
import type { LocalUser } from "@/stores/use-user-store";
|
||||
|
||||
export type AgentChatAttachment = { id: string; name: string; url: string };
|
||||
export type AgentChatMessageItem = {
|
||||
@@ -19,7 +18,7 @@ export type AgentChatMessageItem = {
|
||||
streamId?: string;
|
||||
};
|
||||
|
||||
export function AgentChatMessage({ item, theme, user, onRejectTool, onApproveTool }: { item: AgentChatMessageItem; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; user: LocalUser | null; onRejectTool?: (id: string) => void; onApproveTool?: (id: string) => void }) {
|
||||
export function AgentChatMessage({ item, theme, onRejectTool, onApproveTool }: { item: AgentChatMessageItem; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; onRejectTool?: (id: string) => void; onApproveTool?: (id: string) => void }) {
|
||||
const isUser = item.role === "user";
|
||||
const isSystem = item.role === "system";
|
||||
const color = item.role === "error" ? "#dc2626" : item.role === "tool" ? "#2563eb" : theme.node.text;
|
||||
@@ -35,18 +34,12 @@ export function AgentChatMessage({ item, theme, user, onRejectTool, onApproveToo
|
||||
}
|
||||
if (item.role === "tool") {
|
||||
if (objectField(item.detail, "status") === "pending") return <AgentPendingToolCard summary={item.text} detail={item.detail} theme={theme} onReject={() => onRejectTool?.(item.id)} onApprove={() => onApproveTool?.(item.id)} />;
|
||||
return (
|
||||
<div className="flex items-start gap-3">
|
||||
<AgentAvatar theme={theme} />
|
||||
<AgentToolCard title={item.title || "工具调用"} text={item.text} detail={item.detail} theme={theme} />
|
||||
</div>
|
||||
);
|
||||
return <AgentToolCard title={item.title || "工具调用"} text={item.text} detail={item.detail} theme={theme} />;
|
||||
}
|
||||
return (
|
||||
<div className={`flex items-start gap-3 ${isUser ? "justify-end" : "justify-start"}`}>
|
||||
{!isUser ? <AgentAvatar theme={theme} /> : null}
|
||||
<div className={`flex ${isUser ? "justify-end" : "justify-start"}`}>
|
||||
<div
|
||||
className={isUser ? "min-w-0 max-w-[82%] py-1 text-right text-sm leading-6" : "min-w-0 flex-1 text-left text-sm leading-6"}
|
||||
className={isUser ? "min-w-0 max-w-[82%] py-1 text-right text-sm leading-6" : "min-w-0 w-full text-left text-sm leading-6"}
|
||||
style={{ color }}
|
||||
>
|
||||
{isUser ? (
|
||||
@@ -57,7 +50,6 @@ export function AgentChatMessage({ item, theme, user, onRejectTool, onApproveToo
|
||||
{item.attachments?.length ? <AgentMessageAttachments attachments={item.attachments} alignRight={isUser} /> : null}
|
||||
{item.meta ? <div className={`mt-1 text-[11px] tabular-nums opacity-55 ${isUser ? "text-right" : ""}`}>{item.meta}</div> : null}
|
||||
</div>
|
||||
{isUser ? <AgentUserAvatar user={user} theme={theme} /> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -65,33 +57,28 @@ export function AgentChatMessage({ item, theme, user, onRejectTool, onApproveToo
|
||||
export function AgentPendingToolCard({ summary, detail, theme, onReject, onApprove }: { summary: string; detail?: unknown; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; onReject?: () => void; onApprove?: () => void }) {
|
||||
const view = userDetail(detail);
|
||||
return (
|
||||
<div className="flex items-start gap-3">
|
||||
<AgentAvatar theme={theme} />
|
||||
<div className="min-w-0 flex-1 rounded-xl border px-3 py-3" style={{ borderColor: "rgba(217,119,6,.28)", background: "rgba(217,119,6,.025)", color: theme.node.text }}>
|
||||
<details className="group">
|
||||
<summary className={`flex list-none items-start gap-2.5 ${view ? "cursor-pointer" : "cursor-default"}`} onClick={(event) => { if (!view) event.preventDefault(); }}>
|
||||
<CircleAlert className="mt-0.5 size-4 shrink-0 text-amber-600" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2 text-sm font-medium leading-5">
|
||||
<span>等待确认</span>
|
||||
{view ? <ChevronDown className="ml-auto size-3.5 transition-transform group-open:rotate-180" style={{ color: theme.node.muted }} /> : null}
|
||||
</div>
|
||||
<div className="mt-1 text-sm leading-5" style={{ color: theme.node.muted }}>{summary}</div>
|
||||
</div>
|
||||
</summary>
|
||||
{view ? <AgentDetailBlock detail={view} theme={theme} /> : null}
|
||||
</details>
|
||||
{onReject || onApprove ? (
|
||||
<div className="mt-3 flex justify-end gap-2 border-t pt-3" style={{ borderColor: theme.node.stroke }}>
|
||||
<Button danger type="text" className="!h-8" icon={<XCircle className="size-3.5" />} onClick={() => onReject?.()}>
|
||||
拒绝执行
|
||||
</Button>
|
||||
<Button type="text" className="!h-8" icon={<CheckCircle2 className="size-3.5" />} style={{ color: "#16a34a" }} onClick={() => onApprove?.()}>
|
||||
批准执行
|
||||
</Button>
|
||||
<div className="min-w-0 rounded-xl border px-3 py-3" style={{ borderColor: "rgba(217,119,6,.28)", background: "rgba(217,119,6,.025)", color: theme.node.text }}>
|
||||
<details className="group">
|
||||
<summary className={`list-none ${view ? "cursor-pointer" : "cursor-default"}`} onClick={(event) => { if (!view) event.preventDefault(); }}>
|
||||
<div className="flex min-w-0 items-center gap-2 text-sm font-medium leading-5">
|
||||
<CircleAlert className="size-4 shrink-0 text-amber-600" />
|
||||
<span className="min-w-0 flex-1">等待确认</span>
|
||||
{view ? <ChevronDown className="size-3.5 shrink-0 transition-transform group-open:rotate-180" style={{ color: theme.node.muted }} /> : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-1 pl-6 text-sm leading-5" style={{ color: theme.node.muted }}>{summary}</div>
|
||||
</summary>
|
||||
{view ? <div className="ml-6"><AgentDetailBlock detail={view} theme={theme} /></div> : null}
|
||||
</details>
|
||||
{onReject || onApprove ? (
|
||||
<div className="mt-3 flex justify-end gap-2 border-t pt-3" style={{ borderColor: theme.node.stroke }}>
|
||||
<Button danger type="text" className="!h-8" icon={<XCircle className="size-3.5" />} onClick={() => onReject?.()}>
|
||||
拒绝执行
|
||||
</Button>
|
||||
<Button type="text" className="!h-8" icon={<CheckCircle2 className="size-3.5" />} style={{ color: "#16a34a" }} onClick={() => onApprove?.()}>
|
||||
批准执行
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -103,23 +90,19 @@ export function AgentToolCard({ title, text, detail, theme }: { title: string; t
|
||||
const view = userDetail(detail);
|
||||
const kind = String(objectField(detail, "kind") || "");
|
||||
return (
|
||||
<details className="group min-w-0 flex-1 rounded-xl border px-3 py-2.5 text-left" style={{ borderColor: theme.node.stroke, background: "transparent", color: theme.node.text }}>
|
||||
<summary className={`flex list-none items-start gap-2.5 ${view ? "cursor-pointer" : "cursor-default"}`} onClick={(event) => { if (!view) event.preventDefault(); }}>
|
||||
<span className="mt-0.5 shrink-0" style={{ color: state.color }}>{toolIcon(kind, state.icon)}</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex min-w-0 items-center gap-2 text-sm font-medium leading-5">
|
||||
<span className="min-w-0 truncate">{title}</span>
|
||||
<span className="inline-flex shrink-0 items-center gap-1 text-[11px] font-normal" style={{ color: state.color }}>
|
||||
{state.label}
|
||||
</span>
|
||||
{view ? <ChevronDown className="ml-auto size-3.5 shrink-0 transition-transform group-open:rotate-180" style={{ color: theme.node.muted }} /> : null}
|
||||
</div>
|
||||
<div className={`mt-1 whitespace-pre-wrap break-words text-sm leading-5 ${kind === "command" ? "font-mono text-[12px]" : ""}`} style={{ color: state.isError ? state.color : theme.node.muted }}>
|
||||
{text}
|
||||
</div>
|
||||
<details className="group min-w-0 rounded-xl border px-3 py-2.5 text-left" style={{ borderColor: theme.node.stroke, background: "transparent", color: theme.node.text }}>
|
||||
<summary className={`list-none ${view ? "cursor-pointer" : "cursor-default"}`} onClick={(event) => { if (!view) event.preventDefault(); }}>
|
||||
<div className="flex min-w-0 items-center gap-2 text-sm leading-5">
|
||||
<span className="shrink-0" style={{ color: state.color }}>{toolIcon(kind, state.icon)}</span>
|
||||
<span className="min-w-0 truncate font-medium">{title}</span>
|
||||
<span className="shrink-0 text-[11px]" style={{ color: state.color }}>{state.label}</span>
|
||||
{view ? <ChevronDown className="ml-auto size-3.5 shrink-0 transition-transform group-open:rotate-180" style={{ color: theme.node.muted }} /> : null}
|
||||
</div>
|
||||
<div className={`mt-1 whitespace-pre-wrap break-words pl-6 text-sm leading-5 ${kind === "command" ? "font-mono text-[12px]" : ""}`} style={{ color: state.isError ? state.color : theme.node.muted }}>
|
||||
{text}
|
||||
</div>
|
||||
</summary>
|
||||
{view ? <AgentDetailBlock detail={view} theme={theme} /> : null}
|
||||
{view ? <div className="ml-6"><AgentDetailBlock detail={view} theme={theme} /></div> : null}
|
||||
</details>
|
||||
);
|
||||
}
|
||||
@@ -163,16 +146,13 @@ export function AgentWorkingMessage({ text, activityKey, theme }: { text: string
|
||||
return () => window.clearInterval(timer);
|
||||
}, [activityKey]);
|
||||
return (
|
||||
<div className="flex items-start gap-3">
|
||||
<AgentAvatar theme={theme} />
|
||||
<div className="min-w-0 flex-1 py-1" aria-live="polite">
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-sm" style={{ color: theme.node.muted }}>
|
||||
<LoaderCircle className="size-3.5 shrink-0 animate-spin" />
|
||||
<span className="min-w-0">{text}</span>
|
||||
{elapsed >= 5 ? <span className="shrink-0 text-[11px] tabular-nums opacity-60">{waitingTime(elapsed)}</span> : null}
|
||||
</div>
|
||||
{elapsed >= 30 ? <div className="mt-1 text-xs leading-5 opacity-65" style={{ color: theme.node.muted }}>响应时间较长,但任务仍在运行。可以继续等待,或点击输入框右侧的停止按钮结束本轮。</div> : null}
|
||||
<div className="min-w-0 py-1" aria-live="polite">
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-sm" style={{ color: theme.node.muted }}>
|
||||
<LoaderCircle className="size-3.5 shrink-0 animate-spin" />
|
||||
<span className="min-w-0">{text}</span>
|
||||
{elapsed >= 5 ? <span className="shrink-0 text-[11px] tabular-nums opacity-60">{waitingTime(elapsed)}</span> : null}
|
||||
</div>
|
||||
{elapsed >= 30 ? <div className="mt-1 text-xs leading-5 opacity-65" style={{ color: theme.node.muted }}>响应时间较长,但任务仍在运行。可以继续等待,或点击输入框右侧的停止按钮结束本轮。</div> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -222,23 +202,6 @@ function AgentDetailBlock({ detail, theme }: { detail: UserDetail; theme: (typeo
|
||||
);
|
||||
}
|
||||
|
||||
function AgentAvatar({ theme }: { theme: (typeof canvasThemes)[keyof typeof canvasThemes] }) {
|
||||
return (
|
||||
<span className="grid size-8 shrink-0 place-items-center" role="img" aria-label="OpenAI">
|
||||
<span className="size-5 opacity-80" style={{ background: theme.node.text, WebkitMask: "url(/icons/openai.svg) center / contain no-repeat", mask: "url(/icons/openai.svg) center / contain no-repeat" }} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function AgentUserAvatar({ user, theme }: { user: LocalUser | null; theme: (typeof canvasThemes)[keyof typeof canvasThemes] }) {
|
||||
const avatarUrl = user?.avatarUrl?.trim();
|
||||
return (
|
||||
<span className="grid size-8 shrink-0 place-items-center overflow-hidden rounded-full" style={{ color: theme.node.text }}>
|
||||
{avatarUrl ? <img src={avatarUrl} alt="" className="size-full object-cover" referrerPolicy="no-referrer" /> : <UserRound className="size-4" />}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function AgentMessageAttachments({ attachments, alignRight }: { attachments: AgentChatAttachment[]; alignRight?: boolean }) {
|
||||
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||
return (
|
||||
|
||||
@@ -6,7 +6,6 @@ import { motion, useSpring, useTransform } from "motion/react";
|
||||
import { canvasThemes } from "@/lib/canvas-theme";
|
||||
import { summarizeCanvasAgentOps } from "@/lib/canvas/canvas-agent-ops";
|
||||
import { useAgentStore, type AgentChatItem, type AgentPendingToolCall, type AgentTokenUsage } from "@/stores/use-agent-store";
|
||||
import { useUserStore, type LocalUser } from "@/stores/use-user-store";
|
||||
import { AgentChatMessage, AgentPendingToolCard, AgentToolCard, AgentWorkingMessage } from "./agent-chat-message";
|
||||
import { agentMessageToChatMessage, currentPlanMessage, isPlanMessage, latestPlanMessage, toolCallDetail, toolName, workingActivity } from "./agent-event-formatters";
|
||||
|
||||
@@ -28,7 +27,6 @@ export function AgentChatTimeline({
|
||||
onApproveTool: () => void;
|
||||
}) {
|
||||
const messages = useAgentStore((state) => state.messages);
|
||||
const user = useUserStore((state) => state.user);
|
||||
const listRef = useRef<HTMLDivElement>(null);
|
||||
const followMessagesRef = useRef(true);
|
||||
const [showScrollToBottom, setShowScrollToBottom] = useState(false);
|
||||
@@ -56,7 +54,7 @@ export function AgentChatTimeline({
|
||||
<div className="relative min-h-0 flex-1">
|
||||
<div ref={listRef} className="thin-scrollbar h-full select-text space-y-4 overflow-y-auto px-4 pb-12 pt-4" onScroll={updateScrollState}>
|
||||
{messages.map((item) => (
|
||||
isPlanMessage(item) ? null : <AgentChatMessageRow key={item.id} item={item} theme={theme} user={user} />
|
||||
isPlanMessage(item) ? null : <AgentChatMessageRow key={item.id} item={item} theme={theme} />
|
||||
))}
|
||||
{pendingTool ? (
|
||||
<AgentPendingToolCard
|
||||
@@ -96,10 +94,10 @@ export function AgentTaskProgress({ theme, busy }: { theme: (typeof canvasThemes
|
||||
);
|
||||
}
|
||||
|
||||
const AgentChatMessageRow = memo(function AgentChatMessageRow({ item, theme, user }: { item: AgentChatItem; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; user: LocalUser | null }) {
|
||||
const AgentChatMessageRow = memo(function AgentChatMessageRow({ item, theme }: { item: AgentChatItem; theme: (typeof canvasThemes)[keyof typeof canvasThemes] }) {
|
||||
return (
|
||||
<div style={{ contentVisibility: "auto", containIntrinsicSize: "0 80px" }}>
|
||||
<AgentChatMessage item={agentMessageToChatMessage(item)} theme={theme} user={user} />
|
||||
<AgentChatMessage item={agentMessageToChatMessage(item)} theme={theme} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, type PointerEvent as ReactPointerEvent } from "react";
|
||||
import { Bot, PanelRightClose } from "lucide-react";
|
||||
import { Button, Switch, Tooltip } from "antd";
|
||||
import { Button, Tooltip } from "antd";
|
||||
import { motion } from "motion/react";
|
||||
|
||||
import { LocalAgentPanel } from "./local-agent-panel";
|
||||
@@ -17,7 +17,6 @@ export function AgentPanel() {
|
||||
const panelMounted = useAgentStore((state) => state.panelMounted);
|
||||
const panelOpen = useAgentStore((state) => state.panelOpen);
|
||||
const panelClosing = useAgentStore((state) => state.panelClosing);
|
||||
const confirmTools = useAgentStore((state) => state.confirmTools);
|
||||
const setAgentState = useAgentStore((state) => state.setAgentState);
|
||||
const closePanel = useAgentStore((state) => state.closePanel);
|
||||
|
||||
@@ -72,10 +71,6 @@ export function AgentPanel() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<label className="flex items-center gap-1.5 text-xs" style={{ color: theme.node.muted }}>
|
||||
<Switch size="small" checked={confirmTools} onChange={(confirmTools) => setAgentState({ confirmTools })} />
|
||||
工具确认
|
||||
</label>
|
||||
<Tooltip title="收起对话">
|
||||
<Button type="text" shape="circle" className="!h-8 !w-8 !min-w-8" style={{ color: theme.node.muted }} icon={<PanelRightClose className="size-4" />} onClick={closePanel} />
|
||||
</Tooltip>
|
||||
|
||||
@@ -806,6 +806,8 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded?
|
||||
onStop={stopTurn}
|
||||
onAddFiles={addAttachments}
|
||||
onRemoveAttachment={removeAttachment}
|
||||
confirmTools={confirmTools}
|
||||
onConfirmToolsChange={(confirmTools) => setAgentState({ confirmTools })}
|
||||
left={
|
||||
attachments.length ? (
|
||||
<span className="text-[11px]" style={{ color: theme.node.muted }}>
|
||||
|
||||
@@ -80,7 +80,7 @@ export const useAgentStore = create<AgentStore>((set, get) => ({
|
||||
workspacePath: "",
|
||||
loadingThreads: false,
|
||||
activeTab: "setup",
|
||||
confirmTools: true,
|
||||
confirmTools: false,
|
||||
activity: "就绪",
|
||||
connectError: "",
|
||||
pendingTool: null,
|
||||
|
||||
Reference in New Issue
Block a user