fix(canvas): 创作助手 - 恢复画布 AI 问答卡片
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { nanoid } from "nanoid";
|
||||
import { type ResponseInputMessage, type ResponseToolCall } from "@/services/api/image";
|
||||
import { type AiTextMessage, type ResponseInputMessage, type ResponseToolCall } from "@/services/api/image";
|
||||
import { runBackendToolGenerationTask } from "@/services/api/generation-task";
|
||||
import { resourceIdFromStorageKey } from "@/services/api/resources";
|
||||
import { normalizeModelOptionValue, resolveModelRequestConfig, selectableModelsByCapability, type AiConfig } from "@/stores/use-config-store";
|
||||
import { modelCapabilityConfigFor } from "@/lib/model-capabilities";
|
||||
import { NODE_DEFAULT_SIZE } from "@/constant/canvas";
|
||||
import { CanvasNodeType, type CanvasAssistantMessage, type CanvasAssistantReference, type CanvasAssistantSession, type CanvasNodeData } from "@/types/canvas";
|
||||
import { previewCanvasAgentOps, type CanvasAgentOp, type CanvasAgentOperationImpact, type CanvasAgentSnapshot } from "@/lib/canvas/canvas-agent-ops";
|
||||
@@ -11,9 +12,35 @@ import { CANVAS_ONLINE_AGENT_TOOLS } from "@/lib/canvas/canvas-agent-tools";
|
||||
import { buildOrderedCanvasResourceReferences, canvasResourceMentionToken } from "@/lib/canvas/canvas-resource-references";
|
||||
import { buildCanvasWorkflowOps, looksLikeWorkflowRequest, type CanvasWorkflowInput } from "@/lib/canvas/canvas-agent-workflow";
|
||||
import type { Skill } from "@/services/api/skills";
|
||||
import { AGENT_CAPABILITY_GUIDANCE } from "@/services/agent-capabilities";
|
||||
import { budgetCanvasAgentHistory } from "@/lib/canvas/canvas-agent-context-budget";
|
||||
import { CREATIVE_AGENT_SYSTEM_PROMPT } from "@/lib/creation/creative-agent-tools";
|
||||
import { creativeScenarioPrompt, type CreativeBrief, type CreativeScenarioId } from "@/lib/creation/creative-agent-contract";
|
||||
import type { CreativeDynamicPlan } from "@/lib/creation/creative-plan";
|
||||
import type { CreativeReference } from "@/lib/creation/creative-agent-state";
|
||||
|
||||
export type OnlineToolResult = { ok: true; message: string; data?: unknown; waitForUser?: boolean } | { ok: false; message: string };
|
||||
|
||||
export type CreativeOnlineContext = {
|
||||
scene: CreativeScenarioId;
|
||||
brief: CreativeBrief;
|
||||
plan?: CreativeDynamicPlan;
|
||||
previousProposal?: unknown;
|
||||
rejectedProposal?: unknown;
|
||||
previousQuestions?: unknown;
|
||||
answers?: unknown;
|
||||
executionResults: unknown[];
|
||||
media?: unknown;
|
||||
references: CreativeReference[];
|
||||
sourceContext?: unknown[];
|
||||
};
|
||||
|
||||
export type BuildToolAgentMessagesOptions = {
|
||||
config?: AiConfig;
|
||||
confirmTools?: boolean;
|
||||
creative?: CreativeOnlineContext;
|
||||
};
|
||||
|
||||
export function objectDetail(value: unknown) {
|
||||
return value && typeof value === "object" ? (value as Record<string, unknown>) : {};
|
||||
}
|
||||
@@ -54,7 +81,7 @@ export function onlineToolToOps(name: string, input: Record<string, unknown>, sn
|
||||
if (name === "canvas_create_text_nodes") {
|
||||
const items = requireRecordArray(input.items, "items");
|
||||
const textBatch = items.map((item) => `${String(item.title || "")} ${String(item.text || "")}`).join(" ");
|
||||
if (looksLikeWorkflowRequest(textBatch)) throw new Error("检测到流水线/工作流意图,请使用 canvas_create_workflow 创建真实类型节点和连线。" );
|
||||
if (looksLikeWorkflowRequest(textBatch)) throw new Error("检测到流水线/工作流意图,请使用 canvas_create_workflow 创建真实类型节点和连线。");
|
||||
const x = numberOr(input.x, nextCanvasX(snapshot));
|
||||
const y = numberOr(input.y, 0);
|
||||
const gap = numberOr(input.gap, 40);
|
||||
@@ -474,7 +501,7 @@ export function buildAssistantReferences(nodes: CanvasNodeData[], selectedNodeId
|
||||
.filter((item): item is CanvasAssistantReference => Boolean(item));
|
||||
}
|
||||
|
||||
export async function buildToolAgentMessages(snapshot: CanvasAgentSnapshot, history: CanvasAssistantMessage[], userMessage: CanvasAssistantMessage, skills: Skill[] = []): Promise<ResponseInputMessage[]> {
|
||||
export async function buildToolAgentMessages(snapshot: CanvasAgentSnapshot, history: CanvasAssistantMessage[], userMessage: CanvasAssistantMessage, skills: Skill[] = [], options: BuildToolAgentMessagesOptions = {}): Promise<ResponseInputMessage[]> {
|
||||
const refs = userMessage.references || [];
|
||||
const imageRefs = refs.filter((item) => item.type === CanvasNodeType.Image);
|
||||
const unavailableImageRefs = imageRefs.filter((item) => !resourceIdFromStorageKey(item.storageKey));
|
||||
@@ -487,22 +514,54 @@ export async function buildToolAgentMessages(snapshot: CanvasAgentSnapshot, hist
|
||||
.slice(0, 40)
|
||||
.map((skill) => `- ${skill.skillName}(${skill.skillId},v${skill.version || "1"},${skill.fileCount || 1} 文件):${skill.description}`)
|
||||
.join("\n");
|
||||
const systemContent = buildCanvasOnlineAgentSystemContent(skillCatalog);
|
||||
return [
|
||||
{ role: "system", content: systemContent },
|
||||
...history
|
||||
.filter((message) => message.role === "user" || message.role === "assistant" || message.role === "system")
|
||||
.slice(-8)
|
||||
.map((message): ResponseInputMessage => ({ role: message.role as "system" | "user" | "assistant", content: message.text })),
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
...refs.flatMap((item) => (item.text ? [{ type: "text" as const, text: `选中节点 ${item.title}:${item.text}` }] : [])),
|
||||
{ type: "text", text: `当前画布:${JSON.stringify(compactSnapshot(snapshot))}\n\n用户需求:${userMessage.text}` },
|
||||
...imageRefs.map((item) => ({ type: "image_url" as const, image_url: { url: item.storageKey! } })),
|
||||
],
|
||||
},
|
||||
];
|
||||
const creative = options.creative;
|
||||
const executionGuidance =
|
||||
options.confirmTools === false
|
||||
? "当前普通画布工具的逐次确认已关闭。用户请求范围内的操作可以直接调用,不要先征求重复授权;这不代表可以擅自扩大删除范围或跳过结构化方案、具体费用的批准。"
|
||||
: "当前普通画布工具由程序展示执行确认。用户目标和操作范围明确时直接提交工具,程序会处理确认,不要在工具确认之前再用文字问一遍。";
|
||||
const systemContent = [buildCanvasOnlineAgentSystemContent(skillCatalog), AGENT_CAPABILITY_GUIDANCE, executionGuidance, creative ? CREATIVE_AGENT_SYSTEM_PROMPT : "", creative ? creativeScenarioPrompt(creative.scene) : ""].filter(Boolean).join("\n\n");
|
||||
const historyMessages: AiTextMessage[] = history
|
||||
.filter((message) => message.role === "user" || message.role === "assistant" || message.role === "system")
|
||||
.map((message) => ({ role: message.role as "system" | "user" | "assistant", content: message.text }));
|
||||
const creativeContextText = creative
|
||||
? `创作上下文:${JSON.stringify({
|
||||
brief: creative.brief,
|
||||
plan: creative.plan,
|
||||
previousProposal: creative.previousProposal,
|
||||
rejectedProposal: creative.rejectedProposal,
|
||||
previousQuestions: creative.previousQuestions,
|
||||
answers: creative.answers,
|
||||
executionResults: creative.executionResults,
|
||||
media: creative.media,
|
||||
references: creative.references,
|
||||
availableModels: options.config ? (["image", "video"] as const).flatMap((mode) => selectableModelsByCapability(options.config!, mode).map((model) => ({ mode, model, capability: modelCapabilityConfigFor(options.config!, model) }))) : [],
|
||||
})}\n已返回的执行状态不代表已观察画面。保留成功产物,若用户仅要求分析结果则只提供分析和下一步建议。新增或修改方案仍须确认,媒体生成仍须费用确认。`
|
||||
: "";
|
||||
const currentMessage: AiTextMessage = {
|
||||
role: "user",
|
||||
content: [
|
||||
...refs.flatMap((item) => (item.text ? [{ type: "text" as const, text: `选中节点 ${item.title}:${item.text}` }] : [])),
|
||||
...(creative?.sourceContext?.length
|
||||
? [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: `首页接续记录(任务事实和资料目录,不代表新的执行批准):${JSON.stringify(creative.sourceContext)}。沿用前序用户需求与技能引用;进入画布本身不要求重做作品,不把首页文本当成已获批准的画布操作。先检查当前真实节点,资源目录不等于已观察图片。`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{ type: "text", text: `当前画布:${JSON.stringify(compactSnapshot(snapshot))}\n${creativeContextText}\n用户需求:${userMessage.text}` },
|
||||
...(creative
|
||||
? [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: `本次引用的图片(仅素材目录,尚未观察图片):${JSON.stringify(refs.filter((item) => item.dataUrl || item.storageKey).map((item) => ({ id: item.id, title: item.title })))}。需要观察画面时调用 canvas_inspect_image;不能仅凭素材名称断言画面内容。`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...imageRefs.map((item) => ({ type: "image_url" as const, image_url: { url: item.storageKey! } })),
|
||||
],
|
||||
};
|
||||
return budgetCanvasAgentHistory({ role: "system", content: systemContent }, historyMessages, currentMessage);
|
||||
}
|
||||
|
||||
export function compactSnapshot(snapshot: CanvasAgentSnapshot) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { motion } from "motion/react";
|
||||
import { resolveModelRequestConfig, useConfigStore, useEffectiveConfig, type AiConfig } from "@/stores/use-config-store";
|
||||
import { nanoid } from "nanoid";
|
||||
import { type ResponseInputMessage, type ResponseToolCall } from "@/services/api/image";
|
||||
import { runBackendToolGenerationTask } from "@/services/api/generation-task";
|
||||
import { inspectAgentImage } from "@/services/agent-image-preview";
|
||||
import { listAgentCapabilities, readAgentPluginDocumentation, readAgentPluginNode } from "@/services/agent-capabilities";
|
||||
import { readAgentStoryboard } from "@/lib/canvas/canvas-agent-storyboard";
|
||||
@@ -39,11 +40,14 @@ import { isWritableToolCall } from "@/lib/canvas/canvas-agent-protocol";
|
||||
import { listAddedSkills, type Skill } from "@/services/api/skills";
|
||||
import { buildSkillMentionReferences, skillRuntime } from "@/services/skill-runtime";
|
||||
import { handleCinematicContinuationFailure, canvasCinematicContinuationEntryAdapters, type CinematicContinuationFailureDisposition, type CinematicContinuationLiveSessionState } from "./canvas-cinematic-continuation";
|
||||
import { CanvasCreativeInteraction, canvasCreativeDetail } from "./canvas-creative-interaction";
|
||||
import { creativePlan } from "@/lib/creation/creative-agent-state";
|
||||
import { CanvasCreativeInteraction, canvasCreativeDetail, creativeInteractionSeed, creativeProposalFailure, type CanvasCreativeDetail } from "./canvas-creative-interaction";
|
||||
import { creativePlan, type CreativeReference } from "@/lib/creation/creative-agent-state";
|
||||
import { CreativePlanBar } from "@/components/creation/creative-agent-cards";
|
||||
import { AgentTextModelPicker, AssistantHistory, AssistantReferenceChip, MessageReferences, assistantImageReferenceLabel, assistantMessageToChatMessage, assistantReferencesToMentionReferences, promptAlreadyHasMention } from "./canvas-assistant-panel-views";
|
||||
import { backendAgentProviderConfig, buildAssistantReferences, buildToolAgentMessages, capabilityBatchTitle, cinematicSessionMessageId, compactSnapshot, createSession, describeCanvasSnapshot, explainNoop, nodeToReference, objectDetail, onlineToolToOps, parseToolArguments, previewOnlineToolCalls, requestOnlineAgentModel, requireOps, requireString, snapshotSignature, summarizeToolCalls, toolCallToResponseInput, toolCallsFromDetail, toolResultText, upsertAssistantMessage, type OnlineToolResult } from "./canvas-assistant-online-tools";
|
||||
import { backendAgentProviderConfig, buildAssistantReferences, buildToolAgentMessages, capabilityBatchTitle, cinematicSessionMessageId, compactSnapshot, createSession, describeCanvasSnapshot, explainNoop, nodeToReference, objectDetail, onlineToolToOps, parseToolArguments, previewOnlineToolCalls, requestOnlineAgentModel, requireOps, requireString, snapshotSignature, summarizeToolCalls, toolCallToResponseInput, toolCallsFromDetail, toolResultText, upsertAssistantMessage, type CreativeOnlineContext, type OnlineToolResult } from "./canvas-assistant-online-tools";
|
||||
import { CREATIVE_AGENT_TOOLS } from "@/lib/creation/creative-agent-tools";
|
||||
import { resourceIdFromStorageKey } from "@/services/api/resources";
|
||||
import { recoverCreativeResponse } from "@/services/creative-agent-recovery";
|
||||
|
||||
export const CANVAS_AGENT_PANEL_MOTION_MS = 500;
|
||||
const PANEL_MOTION_SECONDS = CANVAS_AGENT_PANEL_MOTION_MS / 1000;
|
||||
@@ -80,6 +84,94 @@ type CanvasAssistantPanelProps = {
|
||||
resizing?: boolean;
|
||||
};
|
||||
|
||||
function buildCreativeOnlineContext(history: CanvasAssistantMessage[], snapshot: CanvasAgentSnapshot, config: AiConfig): CreativeOnlineContext {
|
||||
const seed = creativeInteractionSeed(history);
|
||||
const previous = history.findLast((message) => canvasCreativeDetail(message));
|
||||
const previousState = previous ? canvasCreativeDetail(previous)!.state : undefined;
|
||||
const previousDetail = previous ? canvasCreativeDetail(previous)! : undefined;
|
||||
const validationError = previousDetail ? creativeProposalFailure(previousDetail, config) : undefined;
|
||||
const rejectedProposal = validationError
|
||||
? {
|
||||
error: validationError,
|
||||
input: previousDetail!.input,
|
||||
instruction: "此方案校验失败,未获得用户批准,未创建节点或生成媒体。修正错误并重新用 creative_respond 返回完整方案;不得省略 generationItems 来规避校验。",
|
||||
}
|
||||
: undefined;
|
||||
const proposalMessage = history.findLast((message) => Boolean(canvasCreativeDetail(message)?.state.proposal));
|
||||
const proposalDetail = proposalMessage ? canvasCreativeDetail(proposalMessage)! : undefined;
|
||||
const proposal = proposalDetail?.state.proposal;
|
||||
const previousProposal =
|
||||
proposal && proposalDetail?.state.canvasApplied
|
||||
? {
|
||||
contextSummary: true,
|
||||
instruction: "已落地方案的结构摘要,未包含原方案正文和生成提示词。需要修改具体内容时先读取对应画布节点,不得用摘要覆盖正文。",
|
||||
id: proposal.id,
|
||||
version: proposal.version,
|
||||
title: proposal.title,
|
||||
summary: proposal.summary,
|
||||
deliverables: proposal.deliverables,
|
||||
generationItems: proposal.generationItems,
|
||||
nodes: proposal.workflow.nodes.map((node) => ({ ref: node.ref, kind: node.kind, title: node.title, referenceRefs: node.referenceRefs, referenceNodeIds: node.referenceNodeIds })),
|
||||
edges: proposal.workflow.edges,
|
||||
}
|
||||
: proposal;
|
||||
const executionByRun = new Map(
|
||||
history.flatMap((message) => {
|
||||
const detail = canvasCreativeDetail(message);
|
||||
return detail && (detail.state.canvasApplied || detail.state.media.some((item) => item.taskId || item.error))
|
||||
? [
|
||||
[
|
||||
detail.runId || message.id,
|
||||
{
|
||||
runId: detail.runId,
|
||||
status: detail.status,
|
||||
proposal: detail.state.proposal?.title,
|
||||
canvasApplied: detail.state.canvasApplied,
|
||||
approvedProposalVersion: detail.approvedProposalVersion,
|
||||
media: detail.state.media.map((item) => ({ ref: item.ref, nodeId: item.nodeId, taskId: item.taskId, status: item.status, error: item.error })),
|
||||
},
|
||||
] as const,
|
||||
]
|
||||
: [];
|
||||
}),
|
||||
);
|
||||
return {
|
||||
scene: seed.scene,
|
||||
brief: seed.brief,
|
||||
plan: seed.dynamicPlan,
|
||||
previousProposal,
|
||||
rejectedProposal,
|
||||
previousQuestions: previousState?.questions,
|
||||
answers: previousState?.answers,
|
||||
executionResults: [...executionByRun.values()],
|
||||
media: previousState?.media,
|
||||
references: creativeCanvasReferences(snapshot),
|
||||
sourceContext: history.flatMap((message) => {
|
||||
const detail = objectDetail(message.detail);
|
||||
return detail.kind === "creation-handoff" ? [{ messageId: message.id, ...detail }] : [];
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function creativeCanvasReferences(snapshot: CanvasAgentSnapshot): CreativeReference[] {
|
||||
return snapshot.nodes.flatMap((node): CreativeReference[] =>
|
||||
node.type === "image" && node.metadata?.storageKey && resourceIdFromStorageKey(node.metadata.storageKey)
|
||||
? [
|
||||
{
|
||||
id: node.id,
|
||||
title: node.title || "画布图片",
|
||||
kind: "image",
|
||||
storageKey: node.metadata.storageKey,
|
||||
assetId: node.metadata.assetId,
|
||||
mimeType: node.metadata.mimeType,
|
||||
width: node.metadata.naturalWidth,
|
||||
height: node.metadata.naturalHeight,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
);
|
||||
}
|
||||
|
||||
export { handleCinematicContinuationFailure, runCanvasCinematicContinuationBoundary, canvasCinematicContinuationEntryAdapters } from "./canvas-cinematic-continuation";
|
||||
export type { CinematicContinuationFailureDisposition } from "./canvas-cinematic-continuation";
|
||||
export function CanvasAssistantPanel({
|
||||
@@ -439,12 +531,18 @@ export function CanvasAssistantPanel({
|
||||
const requestConfig = { ...effectiveConfig, model: effectiveConfig.textModel || effectiveConfig.model };
|
||||
try {
|
||||
setIsRunning(true);
|
||||
const messages = await buildToolAgentMessages(snapshotRef.current, history, userMessage, composerSkills);
|
||||
const messages = await buildToolAgentMessages(snapshotRef.current, history, userMessage, composerSkills, {
|
||||
config: effectiveConfig,
|
||||
confirmTools,
|
||||
creative: buildCreativeOnlineContext(history, snapshotRef.current, effectiveConfig),
|
||||
});
|
||||
let streamed = "";
|
||||
const result = await requestOnlineAgentModel({ ...requestConfig, systemPrompt: "" }, messages, "required", userMessage.text, (text) => {
|
||||
const initial = await requestOnlineAgentModel({ ...requestConfig, systemPrompt: "" }, messages, "required", userMessage.text, (text) => {
|
||||
streamed = text;
|
||||
if (text.trim()) upsertMessage(sessionId, { id: assistantId, role: "assistant", text });
|
||||
});
|
||||
const result = await repairCreativeReply(initial, messages, userMessage.text, history);
|
||||
if (presentCreativeResponse(sessionId, assistantId, result)) return;
|
||||
if (result.toolCalls.length) {
|
||||
const writableCalls = result.toolCalls.filter(isWritableToolCall);
|
||||
if (confirmTools && writableCalls.length) {
|
||||
@@ -474,6 +572,44 @@ export function CanvasAssistantPanel({
|
||||
}
|
||||
};
|
||||
|
||||
const presentCreativeResponse = (sessionId: string, assistantId: string, result: { content: string; toolCalls: ResponseToolCall[] }) => {
|
||||
const call = result.toolCalls.find((item) => item.function.name === "creative_respond");
|
||||
if (!call) return false;
|
||||
const input = parseToolArguments(call.function.arguments);
|
||||
const history = localSessionsRef.current.find((session) => session.id === sessionId)?.messages || [];
|
||||
const state = creativeInteractionSeed(history.filter((message) => message.id !== assistantId));
|
||||
state.references = creativeCanvasReferences(snapshotRef.current);
|
||||
const detail: CanvasCreativeDetail = { kind: "creative-interaction", input, state };
|
||||
upsertMessage(sessionId, {
|
||||
id: assistantId,
|
||||
role: "assistant",
|
||||
text: typeof input.message === "string" ? input.message : result.content || "已整理当前需求。",
|
||||
detail,
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
const repairCreativeReply = async (reply: { content: string; toolCalls: ResponseToolCall[] }, protocol: ResponseInputMessage[], latestInput: string, history: CanvasAssistantMessage[]) => {
|
||||
const state = creativeInteractionSeed(history);
|
||||
state.references = creativeCanvasReferences(snapshotRef.current);
|
||||
const config = { ...effectiveConfig, model: effectiveConfig.textModel || effectiveConfig.model, systemPrompt: "" };
|
||||
return recoverCreativeResponse(
|
||||
reply,
|
||||
protocol,
|
||||
{ config, state, latestInput },
|
||||
(messages) =>
|
||||
runBackendToolGenerationTask({
|
||||
prompt: "根据程序校验反馈修正创作交互",
|
||||
config,
|
||||
messages,
|
||||
tools: CREATIVE_AGENT_TOOLS,
|
||||
toolChoice: "required",
|
||||
signal: generationConsumerControllerRef.current.signal,
|
||||
}),
|
||||
() => undefined,
|
||||
);
|
||||
};
|
||||
|
||||
const continueOnlineToolLoop = async (sessionId: string, assistantId: string, messages: ResponseInputMessage[], result: { content: string; toolCalls: ResponseToolCall[] }, step: number) => {
|
||||
const toolResults = await executeOnlineToolCalls(sessionId, result.toolCalls);
|
||||
appendMessage(sessionId, {
|
||||
@@ -487,6 +623,11 @@ export function CanvasAssistantPanel({
|
||||
};
|
||||
|
||||
const continueOnlineToolLoopAfterResults = async (sessionId: string, assistantId: string, messages: ResponseInputMessage[], toolCalls: ResponseToolCall[], toolResults: OnlineExecutedToolCall[], step: number) => {
|
||||
const waiting = toolResults.find((item) => item.result.ok && item.result.waitForUser);
|
||||
if (waiting) {
|
||||
upsertMessage(sessionId, { id: assistantId, role: "assistant", text: waiting.result.message });
|
||||
return;
|
||||
}
|
||||
const nextMessages: ResponseInputMessage[] = [...messages, ...toolCalls.map(toolCallToResponseInput), ...toolResults.map((item) => ({ role: "tool" as const, tool_call_id: item.toolCallId, content: JSON.stringify(item.result) }))];
|
||||
if (step >= ONLINE_AGENT_MAX_STEPS) {
|
||||
upsertMessage(sessionId, { id: assistantId, role: "assistant", text: toolResults.map((item) => toolResultText(item.result)).join("\n") || "工具已执行。" });
|
||||
@@ -498,25 +639,28 @@ export function CanvasAssistantPanel({
|
||||
streamed = text;
|
||||
if (text.trim()) upsertMessage(sessionId, { id: assistantId, role: "assistant", text });
|
||||
});
|
||||
if (next.toolCalls.length) {
|
||||
const writableCalls = next.toolCalls.filter(isWritableToolCall);
|
||||
const history = localSessionsRef.current.find((session) => session.id === sessionId)?.messages || [];
|
||||
const repaired = await repairCreativeReply(next, nextMessages, history.findLast((message) => message.role === "user")?.text || "", history);
|
||||
if (presentCreativeResponse(sessionId, assistantId, repaired)) return;
|
||||
if (repaired.toolCalls.length) {
|
||||
const writableCalls = repaired.toolCalls.filter(isWritableToolCall);
|
||||
if (confirmTools && writableCalls.length) {
|
||||
upsertMessage(sessionId, { id: assistantId, role: "assistant", text: next.content || streamed || "准备执行工具,等待确认。" });
|
||||
upsertMessage(sessionId, { id: assistantId, role: "assistant", text: repaired.content || streamed || "准备执行工具,等待确认。" });
|
||||
const toolMessageId = nanoid();
|
||||
pendingToolContextRef.current.set(toolMessageId, { messages: nextMessages, toolCalls: next.toolCalls, assistantId, step: step + 1 });
|
||||
pendingToolContextRef.current.set(toolMessageId, { messages: nextMessages, toolCalls: repaired.toolCalls, assistantId, step: step + 1 });
|
||||
appendMessage(sessionId, {
|
||||
id: toolMessageId,
|
||||
role: "tool",
|
||||
title: `确认${capabilityBatchTitle(next.toolCalls)}`,
|
||||
text: summarizeToolCalls(next.toolCalls),
|
||||
detail: { status: "pending", step: step + 1, toolCalls: next.toolCalls, impact: previewOnlineToolCalls(next.toolCalls, snapshotRef.current, effectiveConfig) },
|
||||
title: `确认${capabilityBatchTitle(repaired.toolCalls)}`,
|
||||
text: summarizeToolCalls(repaired.toolCalls),
|
||||
detail: { status: "pending", step: step + 1, toolCalls: repaired.toolCalls, impact: previewOnlineToolCalls(repaired.toolCalls, snapshotRef.current, effectiveConfig) },
|
||||
});
|
||||
return;
|
||||
}
|
||||
await continueOnlineToolLoop(sessionId, assistantId, nextMessages, next, step + 1);
|
||||
await continueOnlineToolLoop(sessionId, assistantId, nextMessages, repaired, step + 1);
|
||||
return;
|
||||
}
|
||||
upsertMessage(sessionId, { id: assistantId, role: "assistant", text: next.content || streamed || toolResults.map((item) => toolResultText(item.result)).join("\n") || "工具已执行。" });
|
||||
upsertMessage(sessionId, { id: assistantId, role: "assistant", text: repaired.content || streamed || toolResults.map((item) => toolResultText(item.result)).join("\n") || "工具已执行。" });
|
||||
};
|
||||
|
||||
const executeOps = async (ops: CanvasAgentOp[], context?: { conversationId?: string; messageId?: string; source?: "online" | "local" }) => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { type ResponseFunctionTool } from "@/services/api/image";
|
||||
import { skillRuntime } from "@/services/skill-runtime";
|
||||
import { CREATIVE_AGENT_TOOLS } from "@/lib/creation/creative-agent-tools";
|
||||
|
||||
const JSON_RECORD_SCHEMA = { type: "object", additionalProperties: true };
|
||||
const POSITION_SCHEMA = { type: "object", properties: { x: { type: "number" }, y: { type: "number" } }, required: ["x", "y"], additionalProperties: false };
|
||||
@@ -73,6 +74,7 @@ function generationToolDefinition(name: string, description: string, mode?: "tex
|
||||
}
|
||||
|
||||
export const CANVAS_ONLINE_AGENT_TOOLS: ResponseFunctionTool[] = [
|
||||
...CREATIVE_AGENT_TOOLS,
|
||||
...skillRuntime.agentTools("onlineAgent"),
|
||||
toolDefinition("canvas_get_state", "读取当前网页画布的节点、连线、选区和视口。", {}),
|
||||
toolDefinition("canvas_get_context", "读取语义化画布上下文、真实节点 id、连接关系、资源就绪状态和状态哈希。", {}),
|
||||
|
||||
37
web/test/canvas-assistant-creative.test.ts
Normal file
37
web/test/canvas-assistant-creative.test.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import { CANVAS_ONLINE_AGENT_TOOLS } from "@/lib/canvas/canvas-agent-tools";
|
||||
import { buildToolAgentMessages } from "@/components/canvas/canvas-assistant-online-tools";
|
||||
import type { CanvasAgentSnapshot } from "@/lib/canvas/canvas-agent-ops";
|
||||
import type { CanvasAssistantMessage } from "@/types/canvas";
|
||||
|
||||
const snapshot: CanvasAgentSnapshot = {
|
||||
projectId: "canvas",
|
||||
title: "测试画布",
|
||||
nodes: [],
|
||||
connections: [],
|
||||
selectedNodeIds: [],
|
||||
viewport: { x: 0, y: 0, k: 1 },
|
||||
};
|
||||
|
||||
describe("画布在线创作交互", () => {
|
||||
test("在线工具清单包含 creative_respond", () => {
|
||||
expect(CANVAS_ONLINE_AGENT_TOOLS.some((tool) => tool.function.name === "creative_respond")).toBe(true);
|
||||
});
|
||||
|
||||
test("创作请求携带创作指导和结构化上下文", async () => {
|
||||
const history: CanvasAssistantMessage[] = [{ id: "user-1", role: "user", text: "我想做一个短片" }];
|
||||
const messages = await buildToolAgentMessages(snapshot, history, { id: "user-2", role: "user", text: "先问我关键需求" }, [], {
|
||||
creative: {
|
||||
scene: "short-film",
|
||||
brief: {},
|
||||
executionResults: [],
|
||||
references: [{ id: "asset-1", title: "参考图", kind: "image" }],
|
||||
},
|
||||
});
|
||||
const system = messages.find((message) => message.role === "system");
|
||||
const user = messages.findLast((message) => message.role === "user");
|
||||
expect(system?.content).toContain("多步骤创作使用 creative_respond");
|
||||
expect(user?.content).toEqual(expect.arrayContaining([expect.objectContaining({ type: "text", text: expect.stringContaining("创作上下文") })]));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user