feat(*): 云端 Agent - 增强画布上下文读取并接入 Gemini 缓存
This commit is contained in:
@@ -39,6 +39,8 @@ export type CloudAgentChatMessage = {
|
||||
streaming?: boolean;
|
||||
reasoning?: boolean;
|
||||
planItems?: CloudAgentPlanItem[];
|
||||
/** 运行已进入终态,但计划项仍未全部完成;用于历史恢复时停止显示 loading。 */
|
||||
planTerminal?: boolean;
|
||||
question?: CloudAgentUserQuestion;
|
||||
meta?: string;
|
||||
detail?: unknown;
|
||||
@@ -417,87 +419,95 @@ export function AgentToolCard({
|
||||
const visibleActions = actions.slice(0, isNodeRead && !readExpanded ? 1 : 8);
|
||||
const collapsedReadNodeCount = isNodeRead ? Math.max(0, actions.length - visibleActions.length) : 0;
|
||||
const isPlain = !actions.length && !state.isError;
|
||||
const hasDetails = actions.length > 0 || state.isError;
|
||||
const conciseError = text.length > 180 ? `${text.slice(0, 180)}…` : text;
|
||||
const categoryIcon = category === "read" ? <Eye className="size-3.5" /> : category === "create" ? <Plus className="size-3.5" /> : <Pencil className="size-3.5" />;
|
||||
const retry = agentToolRetry(detail);
|
||||
const attempts = objectField(detail, "retryAttempts");
|
||||
if (retry && Array.isArray(attempts)) {
|
||||
const label = retry.status === "recovered" ? "自动纠正后已恢复" : retry.status === "exhausted" ? "自动纠正未完成" : "自动纠正记录";
|
||||
return (
|
||||
<details data-agent-tool-retry className="min-w-0 flex-1 text-xs leading-5" style={{ color: theme.node.muted }}>
|
||||
<summary className="cursor-pointer rounded-sm focus-visible:outline focus-visible:outline-2" style={{ outlineColor: theme.node.muted }}>
|
||||
{label} · {retry.attempt}/{retry.maxAttempts} 次尝试未通过
|
||||
</summary>
|
||||
<ol className="mt-2 space-y-1 pl-4" aria-label="自动纠正详情">
|
||||
{(attempts as AgentToolRetryAttempt[]).map((attempt, index) => (
|
||||
<li key={attempt.id} className="whitespace-pre-wrap break-words">
|
||||
第 {index + 1} 次:{attempt.text}
|
||||
</li>
|
||||
const header = (
|
||||
<div className="agent-tool-header">
|
||||
<span className="agent-tool-category">
|
||||
{categoryIcon}
|
||||
<span>{categoryLabel}</span>
|
||||
</span>
|
||||
<span className="agent-tool-summary">{summary}</span>
|
||||
{state.label !== "已完成" ? (
|
||||
<span className="agent-tool-label" style={{ color: state.color }}>
|
||||
{state.label}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
const detailBody = (
|
||||
<>
|
||||
{actions.length ? (
|
||||
<div className="agent-tool-action-list">
|
||||
{visibleActions.map((action) => (
|
||||
<button
|
||||
key={`${action.action}-${action.nodeId}`}
|
||||
type="button"
|
||||
data-agent-node-id={action.nodeId}
|
||||
disabled={!onFocusNode}
|
||||
onClick={() => onFocusNode?.(action.nodeId)}
|
||||
aria-label={`在画布中定位${action.title}`}
|
||||
className="agent-tool-action-link"
|
||||
>
|
||||
{agentCanvasActionLabel(action)}
|
||||
</button>
|
||||
))}
|
||||
</ol>
|
||||
{isNodeRead && (collapsedReadNodeCount > 0 || readExpanded) ? (
|
||||
<button type="button" className="agent-tool-more" aria-expanded={readExpanded} onClick={() => setReadExpanded((current) => !current)}>
|
||||
{readExpanded ? `收起其余 ${Math.max(0, actions.length - 1)} 个节点` : `已折叠 ${collapsedReadNodeCount} 个节点,展开查看`}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{state.isError && text && text !== summary ? (
|
||||
<span className="mt-1 block whitespace-pre-wrap break-words" style={{ color: theme.node.muted }}>
|
||||
{conciseError}
|
||||
</span>
|
||||
) : null}
|
||||
{state.isError && text.length > 180 ? (
|
||||
<details className="mt-1" style={{ color: theme.node.muted }}>
|
||||
<summary className="cursor-pointer">查看完整错误详情</summary>
|
||||
<p className="whitespace-pre-wrap break-words">{text}</p>
|
||||
</details>
|
||||
) : null}
|
||||
{state.isError && objectField(objectField(detail, "result"), "taskId") ? (
|
||||
<span className="mt-1 block break-all" style={{ color: theme.node.muted }}>
|
||||
任务 ID:{String(objectField(objectField(detail, "result"), "taskId"))}
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
if (hasDetails) {
|
||||
return (
|
||||
<details data-agent-tool-card className={`agent-tool-details agent-tool-row--${category}${isPlain ? " agent-tool-row--plain" : ""}`} style={{ color: theme.node.text }}>
|
||||
<summary className="agent-tool-summary-toggle agent-tool-row flex min-w-0 items-start gap-2.5 text-left">
|
||||
<span className="agent-tool-status shrink-0" style={{ color: state.color }} aria-hidden="true">
|
||||
{state.icon}
|
||||
</span>
|
||||
<div className="min-w-0 flex-1 break-words text-xs leading-5" style={{ color: state.isError ? state.color : theme.node.muted }}>
|
||||
{header}
|
||||
</div>
|
||||
<ChevronDown className="agent-tool-chevron mt-1 size-3.5 shrink-0" aria-hidden="true" />
|
||||
</summary>
|
||||
<div className="agent-tool-detail-body ml-6 break-words text-xs leading-5" style={{ color: theme.node.muted }}>
|
||||
{detailBody}
|
||||
</div>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div data-agent-tool-card className={`agent-tool-row agent-tool-row--${category}${isPlain ? " agent-tool-row--plain" : ""} flex min-w-0 flex-1 items-start gap-2.5 text-left`} style={{ color: theme.node.text }}>
|
||||
<div data-agent-tool-card className="agent-tool-row agent-tool-row--plain flex min-w-0 flex-1 items-start gap-2.5 text-left" style={{ color: theme.node.text }}>
|
||||
<span className="agent-tool-status shrink-0" style={{ color: state.color }} aria-hidden="true">
|
||||
{state.icon}
|
||||
</span>
|
||||
<div className="min-w-0 flex-1 break-words text-xs leading-5" style={{ color: state.isError ? state.color : theme.node.muted }}>
|
||||
<div className="agent-tool-header">
|
||||
<span className="agent-tool-category">
|
||||
{categoryIcon}
|
||||
<span>{categoryLabel}</span>
|
||||
</span>
|
||||
<span className="agent-tool-summary">{summary}</span>
|
||||
{state.label !== "已完成" ? (
|
||||
<span className="agent-tool-label" style={{ color: state.color }}>
|
||||
{state.label}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{actions.length ? (
|
||||
<div className="agent-tool-action-list">
|
||||
{visibleActions.map((action) => (
|
||||
<button
|
||||
key={`${action.action}-${action.nodeId}`}
|
||||
type="button"
|
||||
data-agent-node-id={action.nodeId}
|
||||
disabled={!onFocusNode}
|
||||
onClick={() => onFocusNode?.(action.nodeId)}
|
||||
aria-label={`在画布中定位${action.title}`}
|
||||
className="agent-tool-action-link"
|
||||
>
|
||||
{agentCanvasActionLabel(action)}
|
||||
</button>
|
||||
))}
|
||||
{isNodeRead && (collapsedReadNodeCount > 0 || readExpanded) ? (
|
||||
<button type="button" className="agent-tool-more" aria-expanded={readExpanded} onClick={() => setReadExpanded((current) => !current)}>
|
||||
{readExpanded ? `收起其余 ${Math.max(0, actions.length - 1)} 个节点` : `已折叠 ${collapsedReadNodeCount} 个节点,展开查看`}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{state.isError && text && text !== summary ? (
|
||||
<span className="mt-1 block whitespace-pre-wrap break-words" style={{ color: theme.node.muted }}>
|
||||
{conciseError}
|
||||
</span>
|
||||
) : null}
|
||||
{state.isError && text.length > 180 ? (
|
||||
<details className="mt-1" style={{ color: theme.node.muted }}>
|
||||
<summary className="cursor-pointer">查看完整错误详情</summary>
|
||||
<p className="whitespace-pre-wrap break-words">{text}</p>
|
||||
</details>
|
||||
) : null}
|
||||
{state.isError && objectField(objectField(detail, "result"), "taskId") ? (
|
||||
<span className="mt-1 block break-all" style={{ color: theme.node.muted }}>
|
||||
任务 ID:{String(objectField(objectField(detail, "result"), "taskId"))}
|
||||
</span>
|
||||
) : null}
|
||||
{header}
|
||||
</div>
|
||||
{state.label === "已完成" ? <span className="sr-only">已完成</span> : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
export function AgentWorkingMessage({ theme, label = WORKING_TEXT }: { theme: (typeof canvasThemes)[keyof typeof canvasThemes]; label?: string }) {
|
||||
@@ -512,7 +522,7 @@ export function AgentWorkingMessage({ theme, label = WORKING_TEXT }: { theme: (t
|
||||
);
|
||||
}
|
||||
|
||||
export function AgentPlanBar({ items, theme, minimized, onToggle }: { items: CloudAgentPlanItem[]; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; minimized: boolean; onToggle: () => void }) {
|
||||
export function AgentPlanBar({ items, theme, minimized, onToggle, terminal = false }: { items: CloudAgentPlanItem[]; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; minimized: boolean; onToggle: () => void; terminal?: boolean }) {
|
||||
const doneCount = items.filter((entry) => entry.status === "done").length;
|
||||
const allDone = doneCount === items.length;
|
||||
return (
|
||||
@@ -523,6 +533,7 @@ export function AgentPlanBar({ items, theme, minimized, onToggle }: { items: Clo
|
||||
<span className="text-[11px] tabular-nums opacity-50">
|
||||
{doneCount}/{items.length}
|
||||
</span>
|
||||
{terminal && !allDone ? <span className="shrink-0 text-[10px] opacity-55">本轮已结束,未完成项已停止</span> : null}
|
||||
<span className="min-w-0 flex-1" />
|
||||
<span className="shrink-0 text-[11px] opacity-50">{minimized ? "展开" : "收起"}</span>
|
||||
{minimized ? <ChevronDown className="size-3.5 shrink-0 opacity-50" /> : <ChevronUp className="size-3.5 shrink-0 opacity-50" />}
|
||||
@@ -532,11 +543,11 @@ export function AgentPlanBar({ items, theme, minimized, onToggle }: { items: Clo
|
||||
{items.map((entry) => {
|
||||
const done = entry.status === "done";
|
||||
const doing = entry.status === "doing";
|
||||
const Icon = done ? CheckCircle2 : doing ? LoaderCircle : CircleDot;
|
||||
const Icon = done ? CheckCircle2 : terminal ? CircleAlert : doing ? LoaderCircle : CircleDot;
|
||||
return (
|
||||
<li key={entry.id} className="flex min-w-0 items-start gap-1.5 text-xs">
|
||||
<Icon className={doing ? "mt-[3px] size-3 shrink-0 animate-spin" : "mt-[3px] size-3 shrink-0"} style={{ color: done ? "#429477" : doing ? theme.accent.primary : theme.node.muted }} />
|
||||
<span className={done ? "min-w-0 break-words line-through opacity-50" : "min-w-0 break-words"}>{entry.title}</span>
|
||||
<Icon className={doing && !terminal ? "mt-[3px] size-3 shrink-0 animate-spin" : "mt-[3px] size-3 shrink-0"} style={{ color: done ? "#429477" : terminal ? theme.node.muted : doing ? theme.accent.primary : theme.node.muted }} />
|
||||
<span className={done ? "min-w-0 break-words line-through opacity-50" : terminal ? "min-w-0 break-words opacity-55" : "min-w-0 break-words"}>{entry.title}</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { saveAs } from "file-saver";
|
||||
import { buildAgentDebugExport } from "@/lib/canvas/agent-debug-export";
|
||||
import { markdownPlainText } from "@/lib/markdown-plain-text";
|
||||
import { agentToolRetry, mergeAgentToolRetry } from "@/lib/canvas/agent-tool-retry";
|
||||
import { agentPlanVisible, latestAgentPlanItems, pendingAgentQuestion } from "@/lib/canvas/cloud-agent-plan";
|
||||
import { agentPlanVisible, latestAgentPlanItems, latestAgentPlanTerminal, pendingAgentQuestion } from "@/lib/canvas/cloud-agent-plan";
|
||||
import { emptyAgentContextUsage, presentAgentContextUsage, reduceAgentContextUsage, type AgentContextPhase, type AgentContextUsage, type AgentContextUsageView } from "@/lib/canvas/agent-context-usage";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
@@ -43,11 +43,11 @@ import { live2DModelURL } from "@/services/api/appearance";
|
||||
import { Live2DAvatar } from "./live2d-avatar";
|
||||
import "./canvas-cloud-agent.css";
|
||||
|
||||
type CloudAgentPanelProps = { canvasId: string; domainProjectId?: string; nodeCount: number; references: CanvasResourceReference[]; open: boolean; prefillPrompt?: string; onOpen: () => void; onCollapse: () => void; onFocusNode?: (nodeId: string) => void };
|
||||
type CloudAgentPanelProps = { canvasId: string; domainProjectId?: string; nodeCount: number; selectedNodeIds: string[]; references: CanvasResourceReference[]; open: boolean; prefillPrompt?: string; onOpen: () => void; onCollapse: () => void; onFocusNode?: (nodeId: string) => void };
|
||||
type ApprovalState = { approvalId: string; detail: Record<string, unknown>; reason: string };
|
||||
type AgentPanelView = "chat" | "history" | "settings";
|
||||
|
||||
export function CanvasCloudAgentPanel({ canvasId, domainProjectId, nodeCount, references, open, prefillPrompt, onOpen, onCollapse, onFocusNode }: CloudAgentPanelProps) {
|
||||
export function CanvasCloudAgentPanel({ canvasId, domainProjectId, nodeCount, selectedNodeIds, references, open, prefillPrompt, onOpen, onCollapse, onFocusNode }: CloudAgentPanelProps) {
|
||||
const userId = useUserStore((state) => state.user?.id);
|
||||
const appearance = useAppearanceStore((state) => state.appearance.canvas) || DEFAULT_CANVAS_APPEARANCE;
|
||||
const theme = canvasThemes[useActiveTheme()];
|
||||
@@ -95,6 +95,7 @@ export function CanvasCloudAgentPanel({ canvasId, domainProjectId, nodeCount, re
|
||||
const [pendingHydrated, setPendingHydrated] = useState(false);
|
||||
const [planMinimized, setPlanMinimized] = useState(false);
|
||||
const planItems = useMemo(() => latestAgentPlanItems(messages), [messages]);
|
||||
const planTerminal = useMemo(() => latestAgentPlanTerminal(messages), [messages]);
|
||||
const planVisible = agentPlanVisible(planItems);
|
||||
const pendingQuestion = useMemo(() => pendingAgentQuestion(messages), [messages]);
|
||||
const [scenePresets, setScenePresets] = useState<SkillPreset[]>([]);
|
||||
@@ -543,6 +544,7 @@ export function CanvasCloudAgentPanel({ canvasId, domainProjectId, nodeCount, re
|
||||
model: modelOptionName(selectedModel) || undefined,
|
||||
...(logicalModelId ? { logicalModelId } : requestConfig.channelId ? { channelId: requestConfig.channelId, channelModelKey: modelOptionName(selectedModel) || undefined } : {}),
|
||||
skillIds: [...new Set([...selectedSkillIds, ...resolveSkillMentions(value, installedSkills).map((skill) => skill.skillId)])],
|
||||
focusNodeIds: selectedNodeIds.length <= 8 ? selectedNodeIds : [],
|
||||
permissionMode, contextScope,
|
||||
budget: { maxCredits: positiveNumber(maxCredits), maxGenerationTasks: permissionMode === "read_only" ? 0 : Number(maxGenerationTasks), maxVideoSeconds: permissionMode === "read_only" ? 0 : Number(maxVideoSeconds) },
|
||||
};
|
||||
@@ -871,7 +873,7 @@ export function CanvasCloudAgentPanel({ canvasId, domainProjectId, nodeCount, re
|
||||
onApprove={(settings) => void submitApproval("approve", settings)}
|
||||
onReject={() => void submitApproval("reject")}
|
||||
/>
|
||||
{planVisible ? <AgentPlanBar items={planItems} theme={theme} minimized={planMinimized} onToggle={() => setPlanMinimized((value) => !value)} /> : null}
|
||||
{planVisible ? <AgentPlanBar items={planItems} theme={theme} minimized={planMinimized} terminal={planTerminal || Boolean(run && ["completed", "failed", "cancelled", "rejected"].includes(run.status))} onToggle={() => setPlanMinimized((value) => !value)} /> : null}
|
||||
{historyHydrated && !messages.some((message) => message.role === "user" || message.role === "assistant") && !run ? (
|
||||
<AgentSceneCapsules
|
||||
buckets={sceneBuckets}
|
||||
@@ -1457,7 +1459,12 @@ function applyAgentEvent(event: AgentEvent, setMessages: Dispatch<SetStateAction
|
||||
const text = String(payload.text || payload.summary || payload.message || "");
|
||||
if (event.type === "run_status") {
|
||||
const snapshotApproval = payload.approval && typeof payload.approval === "object" ? payload.approval as AgentRun["approval"] : undefined;
|
||||
setRun((current) => (current ? { ...current, status: String(payload.status || current.status) as AgentRun["status"], updatedAt: event.createdAt, revision: Number(payload.revision || 0), cleanupPending: Boolean(payload.cleanupPending), failureMessage: String(payload.failureMessage || ""), skills: payload.skills as AgentRun["skills"], spentCredits: Number(payload.spentCredits || 0), step: Number(payload.step || 0), approval: snapshotApproval } : current));
|
||||
const nextStatus = String(payload.status || "") as AgentRun["status"];
|
||||
const terminal = ["completed", "failed", "cancelled", "rejected"].includes(nextStatus);
|
||||
setRun((current) => (current ? { ...current, status: nextStatus || current.status, updatedAt: event.createdAt, revision: Number(payload.revision || 0), cleanupPending: Boolean(payload.cleanupPending), failureMessage: String(payload.failureMessage || ""), skills: payload.skills as AgentRun["skills"], spentCredits: Number(payload.spentCredits || 0), step: Number(payload.step || 0), approval: snapshotApproval } : current));
|
||||
if (terminal) {
|
||||
setMessages((current) => current.map((message) => message.id === `plan-${event.runId}` && message.planItems?.length ? { ...message, planTerminal: true, streaming: false } : message));
|
||||
}
|
||||
if (payload.failureMessage) setMessages((current) => appendAgentError(current, `terminal-${event.runId}`, String(payload.failureMessage)));
|
||||
if (snapshotApproval && !snapshotApproval.decision && snapshotApproval.approvalId) {
|
||||
setApproval((current) => ({ approvalId: snapshotApproval.approvalId, detail: snapshotApproval, reason: current?.approvalId === snapshotApproval.approvalId ? current.reason : snapshotApproval.reason || "" }));
|
||||
|
||||
@@ -787,6 +787,36 @@
|
||||
transition: transform 160ms ease;
|
||||
}
|
||||
|
||||
/* Tool calls are secondary activity, not chat content. Keep the receipt row
|
||||
scannable and reveal node links / diagnostics only on demand. */
|
||||
.agent-tool-details {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.agent-tool-summary-toggle {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.agent-tool-summary-toggle::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.agent-tool-chevron {
|
||||
flex: 0 0 auto;
|
||||
color: color-mix(in srgb, currentColor 48%, transparent);
|
||||
transition: transform 160ms ease;
|
||||
}
|
||||
|
||||
.agent-tool-details[open] .agent-tool-chevron {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.agent-tool-detail-body {
|
||||
margin: -1px 0 4px 31px;
|
||||
padding: 0 2px 4px;
|
||||
}
|
||||
|
||||
/* 没有节点明细的“读取信息 / 操作画布”只保留时间线语义,不再撑成一张空卡片。 */
|
||||
.agent-tool-row--plain {
|
||||
min-height: 28px;
|
||||
@@ -899,6 +929,7 @@
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.agent-tool-row,
|
||||
.agent-tool-chevron,
|
||||
.agent-tool-action-link { transition: none; }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user