feat: add Claw skill shortcuts and isolate mode chats
This commit is contained in:
parent
80f6827375
commit
d8af4bb48d
@ -334,7 +334,10 @@ export async function POST(request: Request) {
|
||||
temporaryVideoUrl: null,
|
||||
permanentVideoUrl: null,
|
||||
archivedAt: null,
|
||||
videoMetadata: null,
|
||||
videoMetadata: {
|
||||
composerMode: "clawd",
|
||||
source: "openclaw",
|
||||
},
|
||||
};
|
||||
const assistantMessage = {
|
||||
id: assistantMessageId,
|
||||
@ -347,7 +350,14 @@ export async function POST(request: Request) {
|
||||
temporaryVideoUrl: null,
|
||||
permanentVideoUrl: null,
|
||||
archivedAt: null,
|
||||
videoMetadata: null,
|
||||
videoMetadata: {
|
||||
composerMode: "clawd",
|
||||
source: "openclaw",
|
||||
requestId:
|
||||
upstreamPayload && typeof upstreamPayload === "object"
|
||||
? (upstreamPayload as Record<string, unknown>).request_id || null
|
||||
: null,
|
||||
},
|
||||
};
|
||||
|
||||
await saveMessages({ messages: [userMessage, assistantMessage] });
|
||||
@ -364,14 +374,22 @@ export async function POST(request: Request) {
|
||||
role: "user",
|
||||
parts: userMessage.parts,
|
||||
createdAt: userMessage.createdAt.toISOString(),
|
||||
metadata: { createdAt: userMessage.createdAt.toISOString() },
|
||||
metadata: {
|
||||
createdAt: userMessage.createdAt.toISOString(),
|
||||
composerMode: "clawd",
|
||||
},
|
||||
videoMetadata: userMessage.videoMetadata,
|
||||
},
|
||||
{
|
||||
id: assistantMessage.id,
|
||||
role: "assistant",
|
||||
parts: assistantMessage.parts,
|
||||
createdAt: assistantMessage.createdAt.toISOString(),
|
||||
metadata: { createdAt: assistantMessage.createdAt.toISOString() },
|
||||
metadata: {
|
||||
createdAt: assistantMessage.createdAt.toISOString(),
|
||||
composerMode: "clawd",
|
||||
},
|
||||
videoMetadata: assistantMessage.videoMetadata,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@ -1133,8 +1133,7 @@ function normalizeImportedCanvasState(payload: unknown): {
|
||||
}
|
||||
|
||||
const FLOATING_PANEL_WIDTH = 400;
|
||||
const FLOATING_PANEL_MAX_WIDTH = 760;
|
||||
const FLOATING_PANEL_MIN_CANVAS_WIDTH = 520;
|
||||
const FLOATING_PANEL_MAX_VIEWPORT_RATIO = 0.75;
|
||||
const FLOATING_PANEL_WIDTH_STORAGE_KEY = "canvas.chatPanel.width.v1";
|
||||
const FLOATING_PANEL_INSET = 16;
|
||||
const DOCKED_PANEL_INSET = 0;
|
||||
@ -1142,6 +1141,7 @@ const FLOATING_UI_GAP = 16;
|
||||
const FLOATING_BOTTOM_INSET = 20;
|
||||
const FLOATING_TOOLBAR_ESTIMATED_WIDTH = 680;
|
||||
const FLOATING_TOOLBAR_SIDE_DOCK_CLEARANCE = 48;
|
||||
const FLOATING_TOOLBAR_SIDE_DOCK_VISIBLE_CANVAS_RATIO = 0.58;
|
||||
const WORKFLOW_AGENT_CONTEXT_START = "【视频画布同步上下文】";
|
||||
const WORKFLOW_AGENT_CONTEXT_END = "【/视频画布同步上下文】";
|
||||
const AGENT_RESULT_MODE_LABELS: Record<AgentGenerationResultModeType, string> =
|
||||
@ -1154,17 +1154,19 @@ const AGENT_RESULT_MODE_LABELS: Record<AgentGenerationResultModeType, string> =
|
||||
|
||||
function getMaxFloatingPanelWidth(viewportWidth: number) {
|
||||
if (viewportWidth <= 0) {
|
||||
return FLOATING_PANEL_MAX_WIDTH;
|
||||
return FLOATING_PANEL_WIDTH;
|
||||
}
|
||||
|
||||
const viewportLimitedMax = Math.max(
|
||||
FLOATING_PANEL_WIDTH,
|
||||
viewportWidth - FLOATING_PANEL_MIN_CANVAS_WIDTH - DOCKED_PANEL_INSET
|
||||
0,
|
||||
viewportWidth - DOCKED_PANEL_INSET
|
||||
);
|
||||
const ratioLimitedMax =
|
||||
viewportLimitedMax * FLOATING_PANEL_MAX_VIEWPORT_RATIO;
|
||||
|
||||
return Math.max(
|
||||
FLOATING_PANEL_WIDTH,
|
||||
Math.min(FLOATING_PANEL_MAX_WIDTH, viewportLimitedMax)
|
||||
Math.min(viewportLimitedMax, ratioLimitedMax)
|
||||
);
|
||||
}
|
||||
|
||||
@ -3012,13 +3014,20 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
|
||||
viewportWidth > 0 ? Math.max(viewportWidth - panelOcclusionRight, 0) : 0;
|
||||
const horizontalFloatingToolbarWidthForDocking =
|
||||
horizontalFloatingToolbarWidth || FLOATING_TOOLBAR_ESTIMATED_WIDTH;
|
||||
const shouldDockFloatingToolbarToSide =
|
||||
!isVideoMode &&
|
||||
isFloatingSidebarVisible &&
|
||||
const isFloatingToolbarWidthCrowded =
|
||||
visibleCanvasWidth > 0 &&
|
||||
visibleCanvasWidth <
|
||||
horizontalFloatingToolbarWidthForDocking +
|
||||
FLOATING_TOOLBAR_SIDE_DOCK_CLEARANCE * 2;
|
||||
const isFloatingToolbarCanvasCrowded =
|
||||
viewportWidth > 0 &&
|
||||
visibleCanvasWidth > 0 &&
|
||||
visibleCanvasWidth / viewportWidth <=
|
||||
FLOATING_TOOLBAR_SIDE_DOCK_VISIBLE_CANVAS_RATIO;
|
||||
const shouldDockFloatingToolbarToSide =
|
||||
!isVideoMode &&
|
||||
isFloatingSidebarVisible &&
|
||||
(isFloatingToolbarWidthCrowded || isFloatingToolbarCanvasCrowded);
|
||||
const floatingToolbarTooltipSide = shouldDockFloatingToolbarToSide
|
||||
? "right"
|
||||
: "top";
|
||||
|
||||
@ -326,6 +326,47 @@ function getCanvasComposerModeSessionLabel(mode: CanvasComposerMode) {
|
||||
return "Agent";
|
||||
}
|
||||
|
||||
function normalizeCanvasComposerSessionMode(
|
||||
value: unknown,
|
||||
fallback: CanvasComposerMode = "image"
|
||||
): CanvasComposerMode {
|
||||
return value === "agent" ||
|
||||
value === "image" ||
|
||||
value === "video" ||
|
||||
value === "detail" ||
|
||||
value === "clawd"
|
||||
? value
|
||||
: fallback;
|
||||
}
|
||||
|
||||
function getCanvasChatSessionMode(
|
||||
session: Pick<CanvasChatSession, "mode" | "clawdSkillTagId"> | null | undefined,
|
||||
fallback: CanvasComposerMode = "image"
|
||||
) {
|
||||
if (!session) return fallback;
|
||||
if (session.mode) {
|
||||
return normalizeCanvasComposerSessionMode(session.mode, fallback);
|
||||
}
|
||||
return session.clawdSkillTagId ? "clawd" : fallback;
|
||||
}
|
||||
|
||||
function isClawdChatMessage(message: unknown) {
|
||||
if (!message || typeof message !== "object") return false;
|
||||
const raw = message as Record<string, any>;
|
||||
const metadata =
|
||||
raw.metadata && typeof raw.metadata === "object" && !Array.isArray(raw.metadata)
|
||||
? raw.metadata
|
||||
: {};
|
||||
const videoMetadata =
|
||||
raw.videoMetadata &&
|
||||
typeof raw.videoMetadata === "object" &&
|
||||
!Array.isArray(raw.videoMetadata)
|
||||
? raw.videoMetadata
|
||||
: {};
|
||||
|
||||
return metadata.composerMode === "clawd" || videoMetadata.composerMode === "clawd";
|
||||
}
|
||||
|
||||
const CLAWD_TABLE_ACCEPT =
|
||||
".xlsx,.csv,.tsv,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,text/csv,text/tab-separated-values";
|
||||
const CLAWD_TABLE_ALLOWED_EXTENSIONS = new Set(["xlsx", "csv", "tsv"]);
|
||||
@ -1600,6 +1641,7 @@ type MessageVote = {
|
||||
interface CanvasChatSession {
|
||||
id: string;
|
||||
title: string;
|
||||
mode: CanvasComposerMode;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
clawdSkillTagId?: string | null;
|
||||
@ -1899,6 +1941,7 @@ export function CanvasChatPanel({
|
||||
const [downloadProgress, setDownloadProgress] =
|
||||
useState<CanvasImageDownloadProgress | null>(null);
|
||||
const [downloadingOriginalUrl, setDownloadingOriginalUrl] = useState("");
|
||||
const [addingClawdImageUrl, setAddingClawdImageUrl] = useState("");
|
||||
const downloadAbortControllerRef = useRef<AbortController | null>(null);
|
||||
const fullscreenPreviewSrc = useMemo(() => {
|
||||
if (!fullscreenPreview?.url) return "";
|
||||
@ -2045,13 +2088,29 @@ export function CanvasChatPanel({
|
||||
}, [getSendGuard]);
|
||||
|
||||
const startNewChatSession = useCallback(
|
||||
(options?: { title?: string; toastMessage?: string }) => {
|
||||
(options?: {
|
||||
mode?: CanvasComposerMode;
|
||||
title?: string;
|
||||
toastMessage?: string;
|
||||
}) => {
|
||||
const now = Date.now();
|
||||
const newChatSessionId = `canvas-${crypto.randomUUID()}`;
|
||||
const fallbackTitle = `对话 ${chatSessions.length + 1}`;
|
||||
const sessionMode = normalizeCanvasComposerSessionMode(
|
||||
options?.mode,
|
||||
"image"
|
||||
);
|
||||
const sameModeSessionCount = chatSessions.filter(
|
||||
(session) => getCanvasChatSessionMode(session) === sessionMode
|
||||
).length;
|
||||
const modeLabel = getCanvasComposerModeSessionLabel(sessionMode);
|
||||
const fallbackTitle =
|
||||
sessionMode === "image"
|
||||
? `对话 ${sameModeSessionCount + 1}`
|
||||
: `${modeLabel} 对话 ${sameModeSessionCount + 1}`;
|
||||
const newSession: CanvasChatSession = {
|
||||
id: newChatSessionId,
|
||||
title: options?.title || fallbackTitle,
|
||||
mode: sessionMode,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
@ -2064,7 +2123,7 @@ export function CanvasChatPanel({
|
||||
toast.success(options?.toastMessage || "已新建聊天窗口");
|
||||
return newChatSessionId;
|
||||
},
|
||||
[activeChatId, chatSessions.length, releaseSendGuard, replaceChatSessionQuery]
|
||||
[activeChatId, chatSessions, releaseSendGuard, replaceChatSessionQuery]
|
||||
);
|
||||
|
||||
const setSessionClawdSkillTag = useCallback(
|
||||
@ -2390,6 +2449,7 @@ export function CanvasChatPanel({
|
||||
const fallbackSession: CanvasChatSession = {
|
||||
id: rootChatId,
|
||||
title: "对话 1",
|
||||
mode: "image",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
@ -2398,6 +2458,7 @@ export function CanvasChatPanel({
|
||||
? {
|
||||
id: requestedChatSessionId,
|
||||
title: "新对话",
|
||||
mode: "image",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}
|
||||
@ -2411,18 +2472,26 @@ export function CanvasChatPanel({
|
||||
|
||||
const sanitized: CanvasChatSession[] = parsed
|
||||
.filter((session) => session?.id)
|
||||
.map((session): CanvasChatSession => ({
|
||||
id: session.id,
|
||||
title: (session.title || "").trim() || "新对话",
|
||||
createdAt: Number(session.createdAt) || now,
|
||||
updatedAt: Number(session.updatedAt) || now,
|
||||
clawdSkillTagId:
|
||||
.map((session): CanvasChatSession => {
|
||||
const clawdSkillTagId =
|
||||
typeof (session as CanvasChatSession).clawdSkillTagId === "string"
|
||||
? (session as CanvasChatSession).clawdSkillTagId || null
|
||||
: null,
|
||||
clawdSkillTagPromptApplied:
|
||||
(session as CanvasChatSession).clawdSkillTagPromptApplied === true,
|
||||
}));
|
||||
: null;
|
||||
|
||||
return {
|
||||
id: session.id,
|
||||
title: (session.title || "").trim() || "新对话",
|
||||
mode: normalizeCanvasComposerSessionMode(
|
||||
(session as CanvasChatSession).mode,
|
||||
clawdSkillTagId ? "clawd" : "image"
|
||||
),
|
||||
createdAt: Number(session.createdAt) || now,
|
||||
updatedAt: Number(session.updatedAt) || now,
|
||||
clawdSkillTagId,
|
||||
clawdSkillTagPromptApplied:
|
||||
(session as CanvasChatSession).clawdSkillTagPromptApplied === true,
|
||||
};
|
||||
});
|
||||
|
||||
let nextSessions = sanitized;
|
||||
const requiredSessions = requestedSession
|
||||
@ -2712,6 +2781,83 @@ export function CanvasChatPanel({
|
||||
[focusCanvasOnNode, setTransientViewport]
|
||||
);
|
||||
|
||||
const handleAddClawdImageToCanvas = useCallback(
|
||||
async (imageUrl: string) => {
|
||||
const normalizedUrl = String(imageUrl || "").trim();
|
||||
if (!/^https?:\/\//i.test(normalizedUrl)) {
|
||||
toast.error("图片地址无效");
|
||||
return;
|
||||
}
|
||||
|
||||
const imageKey = normalizeImageIdentityUrl(normalizedUrl);
|
||||
const existingNode = useCanvasStore.getState().nodes.find((node) => {
|
||||
if (node.type !== "image") return false;
|
||||
const src = String(((node.data as any) || {}).src || "").trim();
|
||||
return imageKey && normalizeImageIdentityUrl(src) === imageKey;
|
||||
});
|
||||
|
||||
if (existingNode) {
|
||||
selectNodes([existingNode.id]);
|
||||
focusCanvasOnNode(existingNode);
|
||||
toast.message("这张图片已在画布中,已为你定位");
|
||||
return;
|
||||
}
|
||||
|
||||
setAddingClawdImageUrl(normalizedUrl);
|
||||
try {
|
||||
const dimensions = await getImageDimensions(normalizedUrl);
|
||||
const currentNodes = useCanvasStore.getState().nodes;
|
||||
const anchorNode = findCanvasImageAnchorNode(currentNodes, {
|
||||
selectedNodes:
|
||||
generationAnchorNodesRef.current.length > 0
|
||||
? generationAnchorNodesRef.current
|
||||
: selectedImageNodesRef.current,
|
||||
});
|
||||
const size = { width: dimensions.width, height: dimensions.height };
|
||||
const position = getAnchoredGenerationPosition({
|
||||
anchorNode,
|
||||
existingNodes: currentNodes,
|
||||
fallbackNodeCount: currentNodes.length,
|
||||
size,
|
||||
viewport: useCanvasStore.getState().viewport,
|
||||
});
|
||||
|
||||
const nodeId = addNode({
|
||||
type: "image",
|
||||
position,
|
||||
size,
|
||||
data: {
|
||||
src: normalizedUrl,
|
||||
status: "completed",
|
||||
title: "Claw 图片",
|
||||
prompt: "Claw 对话图片",
|
||||
metadata: {
|
||||
originalWidth: dimensions.originalWidth,
|
||||
originalHeight: dimensions.originalHeight,
|
||||
source: "clawd-chat",
|
||||
sourceChatId: activeChatId,
|
||||
sourceImageKey: imageKey || undefined,
|
||||
placementMode: "manual-clawd-image",
|
||||
manuallyAddedAt: Date.now(),
|
||||
},
|
||||
},
|
||||
connections: anchorNode?.id ? [anchorNode.id] : [],
|
||||
});
|
||||
|
||||
selectNodes([nodeId]);
|
||||
focusCanvasOnNode({ position, size });
|
||||
void autoNameGeneratedImageNode(nodeId, normalizedUrl);
|
||||
toast.success("已添加到画布");
|
||||
} catch (error) {
|
||||
console.error("[CanvasChat] Failed to add Claw image to canvas:", error);
|
||||
toast.error("添加到画布失败");
|
||||
} finally {
|
||||
setAddingClawdImageUrl("");
|
||||
}
|
||||
},
|
||||
[activeChatId, addNode, autoNameGeneratedImageNode, focusCanvasOnNode, selectNodes]
|
||||
);
|
||||
|
||||
const animateCanvasAgentViewport = useCallback(
|
||||
(
|
||||
targetViewport: { x: number; y: number; zoom: number },
|
||||
@ -3901,6 +4047,17 @@ export function CanvasChatPanel({
|
||||
const activeComposerMode = composerModeOverride || computedComposerMode;
|
||||
const isVideoToolbarDark = isVideoWorkspace || activeComposerMode === "video";
|
||||
const isClawdMode = activeComposerMode === "clawd" && isClawdModeEnabled;
|
||||
const activeChatSessionMode = getCanvasChatSessionMode(
|
||||
activeChatSession,
|
||||
activeComposerMode
|
||||
);
|
||||
const currentModeChatSessions = useMemo(
|
||||
() =>
|
||||
chatSessions.filter(
|
||||
(session) => getCanvasChatSessionMode(session) === activeComposerMode
|
||||
),
|
||||
[activeComposerMode, chatSessions]
|
||||
);
|
||||
const agentSelectableModels = useMemo(
|
||||
() =>
|
||||
effectiveAgentEnabled ? [SMART_AUTO_MODEL, ...conversationOnlyModels] : conversationOnlyModels,
|
||||
@ -4319,22 +4476,6 @@ export function CanvasChatPanel({
|
||||
: isPresetAgentSelection
|
||||
? getToolbarItem("chatModePresetAgent").label
|
||||
: "Agent";
|
||||
const hasActiveChatContext = useCallback(() => {
|
||||
const editorText = (textareaRef.current?.textContent || "")
|
||||
.replace(/\u200b/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
const hasDetailSlotContent = detailSlots.some((slot) => Boolean(slot?.url));
|
||||
return (
|
||||
activeMessagesRef.current.length > 0 ||
|
||||
Boolean(input.trim()) ||
|
||||
Boolean(editorText) ||
|
||||
attachments.length > 0 ||
|
||||
clawdTableFiles.length > 0 ||
|
||||
Boolean(selectedSkillTagId) ||
|
||||
hasDetailSlotContent
|
||||
);
|
||||
}, [attachments.length, clawdTableFiles.length, detailSlots, input, selectedSkillTagId]);
|
||||
const getComposerModeMenuItemClass = useCallback(
|
||||
(mode: CanvasComposerMode) =>
|
||||
cn(
|
||||
@ -4692,7 +4833,12 @@ export function CanvasChatPanel({
|
||||
return null;
|
||||
}
|
||||
|
||||
const isSwitchingMode = mode !== activeComposerMode;
|
||||
const activeSessionMode = getCanvasChatSessionMode(
|
||||
chatSessions.find((session) => session.id === activeChatId) || null,
|
||||
activeComposerMode
|
||||
);
|
||||
const isSwitchingMode =
|
||||
mode !== activeComposerMode || activeSessionMode !== mode;
|
||||
let targetChatSessionId = activeChatId;
|
||||
if (
|
||||
isSwitchingMode &&
|
||||
@ -4702,12 +4848,28 @@ export function CanvasChatPanel({
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isSwitchingMode && hasActiveChatContext()) {
|
||||
if (isSwitchingMode) {
|
||||
const modeLabel = getCanvasComposerModeSessionLabel(mode);
|
||||
targetChatSessionId = startNewChatSession({
|
||||
title: `${modeLabel} 对话`,
|
||||
toastMessage: `已为 ${modeLabel} 新建聊天窗口`,
|
||||
});
|
||||
const targetSession = chatSessions
|
||||
.filter((session) => getCanvasChatSessionMode(session) === mode)
|
||||
.sort((a, b) => b.updatedAt - a.updatedAt)[0];
|
||||
|
||||
if (targetSession) {
|
||||
targetChatSessionId = targetSession.id;
|
||||
if (targetSession.id !== activeChatId) {
|
||||
forceHistoryHydrationRef.current = toRawCanvasChatId(targetSession.id);
|
||||
releaseSendGuard(activeChatId);
|
||||
setActiveChatId(targetSession.id);
|
||||
replaceChatSessionQuery(targetSession.id);
|
||||
toast.success(`已切换到 ${modeLabel} 对话`);
|
||||
}
|
||||
} else {
|
||||
targetChatSessionId = startNewChatSession({
|
||||
mode,
|
||||
title: `${modeLabel} 对话`,
|
||||
toastMessage: `已为 ${modeLabel} 新建聊天窗口`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
activeComposerModeRef.current = mode;
|
||||
@ -4764,7 +4926,6 @@ export function CanvasChatPanel({
|
||||
firstImageModelId,
|
||||
firstVideoModelId,
|
||||
getSendGuard,
|
||||
hasActiveChatContext,
|
||||
isClawdSubmitting,
|
||||
isDetailGenerating,
|
||||
lastConversationModelId,
|
||||
@ -4773,6 +4934,9 @@ export function CanvasChatPanel({
|
||||
isCanvasImageModel,
|
||||
isCanvasVideoModel,
|
||||
lastVideoModelId,
|
||||
chatSessions,
|
||||
releaseSendGuard,
|
||||
replaceChatSessionQuery,
|
||||
startNewChatSession,
|
||||
]
|
||||
);
|
||||
@ -6454,6 +6618,10 @@ export function CanvasChatPanel({
|
||||
const created: CanvasChatSession = {
|
||||
id: activeChatId,
|
||||
title: derivedTitle || fallbackTitle,
|
||||
mode: normalizeCanvasComposerSessionMode(
|
||||
activeComposerModeRef.current,
|
||||
"image"
|
||||
),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
@ -6479,6 +6647,7 @@ export function CanvasChatPanel({
|
||||
}, [activeChatId, messages]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeChatSessionMode === "clawd") return;
|
||||
if (!hasHydratedHistoryRef.current) return;
|
||||
const activeStreamState = streamImageGenerationStateByChatId[activeChatId];
|
||||
const shouldDeferAssistantMediaAutosync =
|
||||
@ -6530,6 +6699,10 @@ export function CanvasChatPanel({
|
||||
if (message.role !== "assistant" || processedMessageIds.current.has(message.id)) {
|
||||
continue;
|
||||
}
|
||||
if (isClawdChatMessage(message)) {
|
||||
processedMessageIds.current.add(message.id);
|
||||
continue;
|
||||
}
|
||||
|
||||
const nearestUserImageKeys = getNearestUserImageUrlKeys(messages, messageIndex);
|
||||
const nearestUserImageUrls = getNearestUserImageUrls(messages, messageIndex);
|
||||
@ -6827,6 +7000,7 @@ export function CanvasChatPanel({
|
||||
};
|
||||
}, [
|
||||
activeChatId,
|
||||
activeChatSessionMode,
|
||||
messages,
|
||||
addNode,
|
||||
nodes,
|
||||
@ -6865,11 +7039,13 @@ export function CanvasChatPanel({
|
||||
|
||||
// 为历史节点补全可编辑的原始提示词(旧数据里可能只有“对话生成”占位文案)
|
||||
useEffect(() => {
|
||||
if (activeChatSessionMode === "clawd") return;
|
||||
if (!messages.length || !nodes.length) return;
|
||||
|
||||
const promptByImageUrl = new Map<string, string>();
|
||||
messages.forEach((message, messageIndex) => {
|
||||
if (message.role !== "assistant") return;
|
||||
if (isClawdChatMessage(message)) return;
|
||||
const content = message.parts?.map((p: any) => p.text || "").join("") || "";
|
||||
const imageUrls = extractImageUrls(content);
|
||||
if (!imageUrls.length) return;
|
||||
@ -6905,7 +7081,7 @@ export function CanvasChatPanel({
|
||||
},
|
||||
});
|
||||
});
|
||||
}, [messages, nodes, updateNode]);
|
||||
}, [activeChatSessionMode, messages, nodes, updateNode]);
|
||||
|
||||
// 滚动到底部
|
||||
useEffect(() => {
|
||||
@ -9741,9 +9917,23 @@ export function CanvasChatPanel({
|
||||
role: "user",
|
||||
parts: [{ type: "text", text: optimisticMessageText }],
|
||||
createdAt: optimisticCreatedAt,
|
||||
metadata: { createdAt: optimisticCreatedAt.toISOString() },
|
||||
metadata: {
|
||||
createdAt: optimisticCreatedAt.toISOString(),
|
||||
composerMode: "clawd",
|
||||
},
|
||||
} as ChatMessage;
|
||||
|
||||
setChatSessions((prevSessions) =>
|
||||
prevSessions.map((session) =>
|
||||
session.id === activeChatId
|
||||
? {
|
||||
...session,
|
||||
mode: "clawd",
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
: session
|
||||
)
|
||||
);
|
||||
setSendGuard(activeChatId, {
|
||||
inFlight: true,
|
||||
signature: sendSignature,
|
||||
@ -10370,6 +10560,62 @@ export function CanvasChatPanel({
|
||||
}
|
||||
}, [addNode]);
|
||||
|
||||
const uploadImageFileForComposerAttachment = useCallback(async (
|
||||
file: File
|
||||
): Promise<{
|
||||
imageUrl: string;
|
||||
name: string;
|
||||
contentType: string;
|
||||
assetId?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
} | null> => {
|
||||
if (!file.type.startsWith("image/")) {
|
||||
toast.error("请选择图片文件");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (file.size > 50 * 1024 * 1024) {
|
||||
toast.error("图片大小不能超过 50MB");
|
||||
return null;
|
||||
}
|
||||
|
||||
const ext = file.type.split("/")[1] || "png";
|
||||
const fileName =
|
||||
(file.name || "").trim() || `上传图片-${Date.now()}.${ext}`;
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
formData.append("purpose", "canvas-upload");
|
||||
|
||||
const response = await fetchUploadWithRetry(formData);
|
||||
if (!response.ok) {
|
||||
throw new Error("上传失败");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const imageUrl = data.url as string;
|
||||
const assetId =
|
||||
typeof data.assetId === "string" && data.assetId.trim()
|
||||
? data.assetId.trim()
|
||||
: undefined;
|
||||
const dimensions = await getImageDimensions(imageUrl);
|
||||
|
||||
return {
|
||||
imageUrl,
|
||||
name: fileName,
|
||||
contentType: file.type || "image/jpeg",
|
||||
assetId,
|
||||
width: dimensions.originalWidth || dimensions.width,
|
||||
height: dimensions.originalHeight || dimensions.height,
|
||||
};
|
||||
} catch (_error) {
|
||||
toast.error("图片上传失败");
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handlePastedImages = useCallback(async (files: File[]) => {
|
||||
if (files.length === 0) return;
|
||||
|
||||
@ -10377,6 +10623,44 @@ export function CanvasChatPanel({
|
||||
const baseNodeCount = nodes.length;
|
||||
let uploadIndex = 0;
|
||||
|
||||
if (isClawdMode) {
|
||||
let handledCount = 0;
|
||||
for (const file of files) {
|
||||
const uploaded = await uploadImageFileForComposerAttachment(file);
|
||||
if (!uploaded) continue;
|
||||
|
||||
handledCount += 1;
|
||||
setAttachments((prev) => {
|
||||
const newIndex = prev.length;
|
||||
setTimeout(() => {
|
||||
insertImageTagInEditor(uploaded.imageUrl, uploaded.name, newIndex);
|
||||
(textareaRef.current as HTMLDivElement | null)?.focus();
|
||||
}, 0);
|
||||
|
||||
return [
|
||||
...prev,
|
||||
{
|
||||
url: uploaded.imageUrl,
|
||||
name: uploaded.name,
|
||||
contentType: uploaded.contentType,
|
||||
assetId: uploaded.assetId,
|
||||
width: uploaded.width,
|
||||
height: uploaded.height,
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
if (handledCount > 0) {
|
||||
toast.success(
|
||||
handledCount === 1
|
||||
? `图片已添加为 ${CLAW_MODE_LABEL} 附件`
|
||||
: `已添加 ${handledCount} 张图片为 ${CLAW_MODE_LABEL} 附件`
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasAgentUploadSlots) {
|
||||
const slotCount = agentUploadSlots.length;
|
||||
const currentSlots = Array.from({ length: slotCount }, (_, index) => attachments[index] ?? null);
|
||||
@ -10500,11 +10784,13 @@ export function CanvasChatPanel({
|
||||
attachments,
|
||||
hasAgentUploadSlots,
|
||||
insertImageTagInEditor,
|
||||
isClawdMode,
|
||||
isVideoModel,
|
||||
maybeAutoSetCanvasTitle,
|
||||
nodes.length,
|
||||
updateAttachmentAtAgentSlot,
|
||||
updateAttachmentAtVideoSlot,
|
||||
uploadImageFileForComposerAttachment,
|
||||
uploadImageFileAndAddToCanvas,
|
||||
videoFrameSlots.length,
|
||||
]);
|
||||
@ -10584,11 +10870,42 @@ export function CanvasChatPanel({
|
||||
addClawdTableFiles(tableFiles);
|
||||
}
|
||||
|
||||
selectedFiles = imageFiles;
|
||||
if (selectedFiles.length === 0) {
|
||||
e.target.value = "";
|
||||
return;
|
||||
let handledImageCount = 0;
|
||||
for (const file of imageFiles) {
|
||||
const uploaded = await uploadImageFileForComposerAttachment(file);
|
||||
if (!uploaded) continue;
|
||||
|
||||
handledImageCount += 1;
|
||||
setAttachments((prev) => {
|
||||
const newIndex = prev.length;
|
||||
setTimeout(() => {
|
||||
insertImageTagInEditor(uploaded.imageUrl, uploaded.name, newIndex);
|
||||
(textareaRef.current as HTMLDivElement | null)?.focus();
|
||||
}, 0);
|
||||
|
||||
return [
|
||||
...prev,
|
||||
{
|
||||
url: uploaded.imageUrl,
|
||||
name: uploaded.name,
|
||||
contentType: uploaded.contentType,
|
||||
assetId: uploaded.assetId,
|
||||
width: uploaded.width,
|
||||
height: uploaded.height,
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
if (handledImageCount > 0) {
|
||||
toast.success(
|
||||
handledImageCount === 1
|
||||
? `图片已添加为 ${CLAW_MODE_LABEL} 附件`
|
||||
: `已添加 ${handledImageCount} 张图片为 ${CLAW_MODE_LABEL} 附件`
|
||||
);
|
||||
}
|
||||
e.target.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
let firstImageUrl: string | null = null;
|
||||
@ -11415,8 +11732,8 @@ export function CanvasChatPanel({
|
||||
);
|
||||
|
||||
const createNewChatSession = useCallback(() => {
|
||||
startNewChatSession();
|
||||
}, [startNewChatSession]);
|
||||
startNewChatSession({ mode: activeComposerMode });
|
||||
}, [activeComposerMode, startNewChatSession]);
|
||||
|
||||
const switchToSession = useCallback((sessionId: string) => {
|
||||
if (!sessionId || sessionId === activeChatId) {
|
||||
@ -11582,10 +11899,10 @@ export function CanvasChatPanel({
|
||||
>
|
||||
<div className="px-2 py-1 text-xs text-muted-foreground">聊天窗口记录</div>
|
||||
<div className="max-h-56 overflow-y-auto space-y-1">
|
||||
{chatSessions.length === 0 ? (
|
||||
{currentModeChatSessions.length === 0 ? (
|
||||
<div className="px-2 py-2 text-xs text-muted-foreground">暂无记录</div>
|
||||
) : (
|
||||
chatSessions.map((session, index) => {
|
||||
currentModeChatSessions.map((session, index) => {
|
||||
const isActive = session.id === activeChatId;
|
||||
const timeLabel = new Date(session.updatedAt).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
@ -11824,7 +12141,7 @@ export function CanvasChatPanel({
|
||||
预设快捷功能
|
||||
</h3>
|
||||
<div className="mt-6 flex flex-wrap justify-center gap-2">
|
||||
{canvasSkillTags.slice(0, 4).map((tag) => {
|
||||
{canvasSkillTags.map((tag) => {
|
||||
const isActive = selectedSkillTag?.id === tag.id;
|
||||
const TagIcon = getCanvasSkillIconComponent(tag.iconName);
|
||||
return (
|
||||
@ -11930,6 +12247,9 @@ export function CanvasChatPanel({
|
||||
message.role === "assistant" &&
|
||||
!hasAssistantMedia &&
|
||||
Boolean(clawdTypewriterMessageIds[message.id]);
|
||||
const isClawAssistantMessage =
|
||||
message.role === "assistant" &&
|
||||
(isClawdMode || isClawdChatMessage(message));
|
||||
|
||||
const messageTimestamp = getMessageTimestamp(message);
|
||||
const prevMessageTimestamp =
|
||||
@ -12136,6 +12456,7 @@ export function CanvasChatPanel({
|
||||
shouldTypeClawAssistantMessage);
|
||||
const shouldShowDoneSummary =
|
||||
!isDetailGenerationMode &&
|
||||
!isClawAssistantMessage &&
|
||||
imageUrls.length > 0 &&
|
||||
!plainText.includes(CANVAS_IMAGE_DONE_SUMMARY) &&
|
||||
!isLatestAssistantDuringGeneration;
|
||||
@ -12279,24 +12600,46 @@ export function CanvasChatPanel({
|
||||
setFullscreenPreview({ url });
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute bottom-2 right-2 inline-flex items-center gap-1 rounded-md bg-black/70 px-2 py-1 text-[11px] font-medium text-white hover:bg-black/80 disabled:cursor-not-allowed disabled:opacity-65"
|
||||
disabled={isDownloadingOriginalImage}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
void handleDownloadOriginalImage(url);
|
||||
}}
|
||||
title="下载原图"
|
||||
>
|
||||
{downloadingOriginalUrl === url ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<Download className="h-3.5 w-3.5" />
|
||||
)}
|
||||
{downloadingOriginalUrl === url ? "下载中" : "原图"}
|
||||
</button>
|
||||
<div className="absolute bottom-2 right-2 flex items-center gap-1.5">
|
||||
{isClawAssistantMessage ? (
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-1 rounded-md bg-black/70 px-2 py-1 text-[11px] font-medium text-white hover:bg-black/80 disabled:cursor-not-allowed disabled:opacity-65"
|
||||
disabled={addingClawdImageUrl === url}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
void handleAddClawdImageToCanvas(url);
|
||||
}}
|
||||
title="添加到画布"
|
||||
>
|
||||
{addingClawdImageUrl === url ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<ImagePlus className="h-3.5 w-3.5" />
|
||||
)}
|
||||
{addingClawdImageUrl === url ? "添加中" : "画布"}
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-1 rounded-md bg-black/70 px-2 py-1 text-[11px] font-medium text-white hover:bg-black/80 disabled:cursor-not-allowed disabled:opacity-65"
|
||||
disabled={isDownloadingOriginalImage}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
void handleDownloadOriginalImage(url);
|
||||
}}
|
||||
title="下载原图"
|
||||
>
|
||||
{downloadingOriginalUrl === url ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<Download className="h-3.5 w-3.5" />
|
||||
)}
|
||||
{downloadingOriginalUrl === url ? "下载中" : "原图"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -13309,6 +13652,48 @@ export function CanvasChatPanel({
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
{isClawdMode && activeClawdSkillTag && !activeClawdSkillTagPromptApplied ? (
|
||||
<div className="mb-2 flex px-1 animate-in fade-in-0 slide-in-from-bottom-2 zoom-in-95 duration-300">
|
||||
<div
|
||||
className="inline-flex max-w-full items-center gap-2 rounded-full border border-violet-200 bg-violet-50 px-3 py-1.5 text-sm font-medium text-violet-700 shadow-sm dark:border-violet-400/25 dark:bg-violet-500/15 dark:text-violet-100"
|
||||
title={`首次发送会带入「${activeClawdSkillTag.name}」快捷标签提示词`}
|
||||
>
|
||||
{activeClawdSkillTag.iconUrl ? (
|
||||
<img
|
||||
src={buildPreviewImageUrl(activeClawdSkillTag.iconUrl, 40)}
|
||||
alt={activeClawdSkillTag.name}
|
||||
className="h-4 w-4 shrink-0 rounded-sm object-cover"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
onError={(event) => {
|
||||
if (event.currentTarget.src !== activeClawdSkillTag.iconUrl) {
|
||||
event.currentTarget.src = activeClawdSkillTag.iconUrl || "";
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ActiveClawdSkillTagIcon className="h-4 w-4 shrink-0 text-violet-600 dark:text-violet-200" />
|
||||
)}
|
||||
<span className="truncate">{activeClawdSkillTag.name}</span>
|
||||
<span className="shrink-0 rounded-full bg-white/70 px-1.5 py-0.5 text-[10px] font-semibold text-violet-500 dark:bg-white/12 dark:text-violet-100">
|
||||
待发送
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-full p-0.5 text-violet-400 transition-colors hover:bg-violet-100 hover:text-violet-700 disabled:cursor-not-allowed disabled:opacity-50 dark:text-violet-200 dark:hover:bg-white/12 dark:hover:text-white"
|
||||
onClick={() => {
|
||||
setSessionClawdSkillTag(activeChatId, null);
|
||||
setSelectedSkillTagId(null);
|
||||
}}
|
||||
disabled={isSubmitting}
|
||||
aria-label={`移除 ${activeClawdSkillTag.name} 快捷标签`}
|
||||
title="取消本次快捷标签"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{!isDetailGenerationMode && hasWorkflowContext && workflowContext && (
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
@ -65,6 +65,142 @@ export const DEFAULT_CANVAS_SKILL_TAGS: CanvasSkillTag[] = [
|
||||
prompt:
|
||||
"请按电商平台高转化标准输出主图+副图方案,强调产品主体清晰、卖点可视化、场景可信和对比信息明确。保持产品外观一致,不虚构结构细节。",
|
||||
},
|
||||
{
|
||||
id: "video-gateway",
|
||||
name: "视频理解",
|
||||
iconName: "clapperboard",
|
||||
placeholder: "例如:分析这个短视频链接,总结分镜、口播脚本和可复用卖点",
|
||||
prompt:
|
||||
"【技能路由:video-gateway】用途:视频链接/附件理解。请优先按视频理解任务处理,支持 mp4/mov/webm/m3u8 和抖音/小红书/B站等链接。主要触发关键词:视频、短视频、总结、分析、拆解、分镜、口播、脚本。",
|
||||
},
|
||||
{
|
||||
id: "image-search-api",
|
||||
name: "以图搜图",
|
||||
iconName: "image",
|
||||
placeholder: "例如:用这张图片找市场同款,并识别是否为公司 SKU",
|
||||
prompt:
|
||||
"【技能路由:image-search-api】用途:以图搜图/公司 SKU 图片识别。请优先按图片搜索任务处理。主要触发关键词:以图搜图、图片搜索、找同款、相似款、市场同款、公司SKU、内部SKU、货号。需要图片 URL、附件或 base64。",
|
||||
},
|
||||
{
|
||||
id: "product-asset-api",
|
||||
name: "商品素材下载",
|
||||
iconName: "shopping-bag",
|
||||
placeholder: "例如:SKU: ABC123,下载主图、白底图、SKU 图和详情页素材",
|
||||
prompt:
|
||||
"【技能路由:product-asset-api】用途:商品详情页/主图/SKU图/白底图下载。请优先按商品素材查询下载任务处理。主要触发关键词:详情页、详情素材、任务详情、sku图、主图、白底图、白底。需要 SKU: 或 SPU:。",
|
||||
},
|
||||
{
|
||||
id: "install-media-db",
|
||||
name: "安装资料查询",
|
||||
iconName: "book-open",
|
||||
placeholder: "例如:帮我查这个订单/货号的安装图纸、安装视频和说明书",
|
||||
prompt:
|
||||
"【技能路由:install-media-db】用途:安装图纸/安装视频/说明书查询。请优先按安装资料查询任务处理。主要触发关键词:安装图纸、安装视频、安装说明书、安装资料、图纸地址、视频地址、订单号查安装。",
|
||||
},
|
||||
{
|
||||
id: "product-region-logistics-db",
|
||||
name: "仓配快递策略",
|
||||
iconName: "message-circle",
|
||||
placeholder: "例如:货号 ABC123 发浙江省默认走什么快递、哪个仓库",
|
||||
prompt:
|
||||
"【技能路由:product-region-logistics-db】用途:货号 + 区域仓配/快递策略。请优先按仓配物流策略查询任务处理。主要触发关键词:发什么快递、什么快递、物流策略、仓库策略、仓配策略、默认快递、默认仓库。需要货号和省份。",
|
||||
},
|
||||
{
|
||||
id: "product-retention-db",
|
||||
name: "退补留客决策",
|
||||
iconName: "badge-percent",
|
||||
placeholder: "例如:这个售后场景应该退货还是补偿,补偿区间多少",
|
||||
prompt:
|
||||
"【技能路由:product-retention-db】用途:退货/补偿/留客决策。请优先按退补留客决策任务处理。主要触发关键词:退货还是补偿、退回还是补偿、补偿区间、优惠留客、留客、挽留、放货、退补决策。",
|
||||
},
|
||||
{
|
||||
id: "company-aftersale-db",
|
||||
name: "售后退款分析",
|
||||
iconName: "message-circle",
|
||||
placeholder: "例如:汇总 6 月售后分析,拆解退款原因和风险类型",
|
||||
prompt:
|
||||
"【技能路由:company-aftersale-db】用途:公司售后/退款原因分析。请优先按售后退款原因分析任务处理。主要触发关键词:售后分析、退款原因、售后原因、退货退款风险、售后类型、售后结果。",
|
||||
},
|
||||
{
|
||||
id: "business-db",
|
||||
name: "销售业绩汇总",
|
||||
iconName: "badge-percent",
|
||||
placeholder: "例如:汇总 2026 年 6 月全平台整体销售业绩和 GMV",
|
||||
prompt:
|
||||
"【技能路由:business-db】用途:整体销售业绩汇总。请优先按销售业绩汇总任务处理。触发条件:销售/业绩/GMV/成交/营业额 + 总/整体/全平台/汇总 + 日期。",
|
||||
},
|
||||
{
|
||||
id: "market-db",
|
||||
name: "市场大盘分析",
|
||||
iconName: "layout-grid",
|
||||
placeholder: "例如:分析书桌类目 2026 年 6 月市场大盘、价格带和品牌榜",
|
||||
prompt:
|
||||
"【技能路由:market-db】用途:市场大盘分析。请优先按市场大盘/行业类目分析任务处理。主要触发关键词:市场分析、市场大盘、行业分析、类目分析、市场趋势、价格带、品牌榜、店铺榜、热搜词等 + 类目词。",
|
||||
},
|
||||
{
|
||||
id: "market-insight-db",
|
||||
name: "市场洞察表",
|
||||
iconName: "layout-grid",
|
||||
placeholder: "例如:生成鞋柜市场洞察表,并支持完整 Excel 导出",
|
||||
prompt:
|
||||
"【技能路由:market-insight-db】用途:市场洞察表/完整 Excel 导出。请优先按市场洞察表任务处理。主要触发关键词:市场洞察、洞察表、洞察报告、洞察分析;后续支持按推荐继续、看SKU、看关键词、确认导出完整表。",
|
||||
},
|
||||
{
|
||||
id: "rank-board-db",
|
||||
name: "市场榜单分析",
|
||||
iconName: "layout-grid",
|
||||
placeholder: "例如:分析目标商品 ID 的市场榜单结构、价格带和材质分布",
|
||||
prompt:
|
||||
"【技能路由:rank-board-db】用途:市场榜单/商品ID榜单分析。请优先按榜单分析任务处理。主要触发关键词:榜单、排行榜、渠道榜、目标ID、市场ID、商品ID计算、ID明细、价格带、材质、店铺、榜单结构。",
|
||||
},
|
||||
{
|
||||
id: "market-id-detail-db",
|
||||
name: "市场ID详情",
|
||||
iconName: "shopping-bag",
|
||||
placeholder: "例如:补全这些市场商品 ID 的主图、SKU 图、详情页、标题和价格",
|
||||
prompt:
|
||||
"【技能路由:market-id-detail-db】用途:市场商品 ID 详情补全。请优先按市场商品 ID 详情任务处理。主要触发关键词:市场ID详情、市场商品ID、市场商品详情、商品ID + 主图/sku图/详情页/标题/价格/链接。",
|
||||
},
|
||||
{
|
||||
id: "pdd-goods-db",
|
||||
name: "拼多多经营分析",
|
||||
iconName: "shopping-bag",
|
||||
placeholder: "例如:分析这个拼多多商品 ID 的经营表现、排行、转化和价格",
|
||||
prompt:
|
||||
"【技能路由:pdd-goods-db】用途:拼多多商品经营分析。请优先按拼多多商品经营分析任务处理。主要触发关键词:拼多多、PDD、datagoods + 商品ID或经营/表现/数据/排行/转化/推广/价格/SKU 等意图。",
|
||||
},
|
||||
{
|
||||
id: "channel-id-health-db",
|
||||
name: "渠道ID健康诊断",
|
||||
iconName: "badge-percent",
|
||||
placeholder: "例如:诊断这个京东/天猫商品 ID 的费比、SKU 利润和优化动作",
|
||||
prompt:
|
||||
"【技能路由:channel-id-health-db】用途:京东/天猫商品ID健康诊断。请优先按渠道商品 ID 健康诊断任务处理。主要触发关键词:京东/JD/天猫/tmall + 健康诊断、ID健康、商品经营、费比、SKU利润、放大、控投、优化。",
|
||||
},
|
||||
{
|
||||
id: "operator-health-db",
|
||||
name: "运营健康诊断",
|
||||
iconName: "badge-percent",
|
||||
placeholder: "例如:给我的团队做运营健康诊断,包含业绩、费比、SKU 和优化建议",
|
||||
prompt:
|
||||
"【技能路由:operator-health-db】用途:运营健康诊断。请优先按运营健康诊断任务处理。主要触发关键词:运营健康、健康诊断、健康分析、放大、控投、优化 + 我的/团队/运营/商品ID/SKU/费比/业绩。",
|
||||
},
|
||||
{
|
||||
id: "business-operator-dimension-db",
|
||||
name: "运营维度排行",
|
||||
iconName: "layout-grid",
|
||||
placeholder: "例如:按运营维度汇总 6 月销售数据,输出排行和差异",
|
||||
prompt:
|
||||
"【技能路由:business-operator-dimension-db】用途:团队/运营维度销售排行。请优先按运营维度销售排行任务处理。主要触发关键词:运营维度、按运营、各运营、每个运营、运营排行、运营排名、运营汇总、运营销售数据。",
|
||||
},
|
||||
{
|
||||
id: "business-operator-db",
|
||||
name: "单运营业绩费比",
|
||||
iconName: "badge-percent",
|
||||
placeholder: "例如:张三 2026 年 6 月业绩和费比情况",
|
||||
prompt:
|
||||
"【技能路由:business-operator-db】用途:单个运营业绩与费比。请优先按单个运营业绩费比任务处理。触发条件:人名 + 业绩/费比/销售/成交/GMV + 月份,例如“张三2026年6月业绩”。",
|
||||
},
|
||||
];
|
||||
|
||||
function normalizeString(value: unknown) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user