diff --git a/app/(admin)/settings/admin-settings-client.tsx b/app/(admin)/settings/admin-settings-client.tsx index a415d432d..548f74a14 100644 --- a/app/(admin)/settings/admin-settings-client.tsx +++ b/app/(admin)/settings/admin-settings-client.tsx @@ -98,6 +98,11 @@ import { getCanvasSkillIconComponent, } from "@/components/canvas/canvas-skill-icons"; import { HomeAnimationEmbed } from "@/components/home/home-animation-embed"; +import { + DEFAULT_OPENCLAW_CHAT_BASE_URL, + OPENCLAW_CHAT_CONFIG_KEY, + type OpenClawChatConfig, +} from "@/lib/openclaw-chat/config"; import { Button } from "@/components/ui/button"; import { Card, @@ -1839,6 +1844,11 @@ export function AdminSettingsClient() { `/api/admin/settings?key=${HOME_CHAT_ANIMATION_CONFIG_KEY}`, fetcher ); + const { data: openClawChatSettingData, mutate: mutateOpenClawChatSetting } = + useSWR<{ setting?: { value?: unknown } }>( + `/api/admin/settings?key=${OPENCLAW_CHAT_CONFIG_KEY}`, + fetcher + ); // 🆕 生图记录数据 const imageQueryParams = new URLSearchParams({ @@ -2145,6 +2155,15 @@ export function AdminSettingsClient() { htmlContent: "", updatedAt: null, }); + const [openClawChatConfig, setOpenClawChatConfig] = + useState({ + enabled: false, + baseUrl: DEFAULT_OPENCLAW_CHAT_BASE_URL, + token: "", + timeoutMs: 180_000, + }); + const [openClawChatTokenConfigured, setOpenClawChatTokenConfigured] = + useState(false); const lastLoadedSiteSettingsRef = useRef(null); const lastLoadedHomeAnimationConfigRef = useRef( null @@ -2416,6 +2435,29 @@ export function AdminSettingsClient() { lastLoadedHomeAnimationConfigRef.current = nextHomeAnimationConfig; }, [homeAnimationSettingData]); + useEffect(() => { + const raw = + openClawChatSettingData?.setting?.value && + typeof openClawChatSettingData.setting.value === "object" && + !Array.isArray(openClawChatSettingData.setting.value) + ? (openClawChatSettingData.setting.value as Record) + : {}; + + setOpenClawChatConfig({ + enabled: raw.enabled === true, + baseUrl: + String(raw.baseUrl || "").trim() || DEFAULT_OPENCLAW_CHAT_BASE_URL, + token: "", + timeoutMs: Math.min( + 300_000, + Math.max(5_000, Math.round(Number(raw.timeoutMs) || 180_000)) + ), + }); + setOpenClawChatTokenConfigured( + raw.hasToken === true || Boolean(String(raw.token || "").trim()) + ); + }, [openClawChatSettingData]); + useEffect(() => { const normalized = normalizeDetailAgentLocalMoonshotConfig( moonshotDetailAgentSettingData?.setting?.value @@ -4242,6 +4284,68 @@ export function AdminSettingsClient() { } }; + const handleSaveOpenClawChatConfig = async () => { + try { + const normalizedConfig: OpenClawChatConfig = { + enabled: openClawChatConfig.enabled, + baseUrl: + openClawChatConfig.baseUrl.trim() || DEFAULT_OPENCLAW_CHAT_BASE_URL, + token: openClawChatConfig.token.trim(), + timeoutMs: Math.min( + 300_000, + Math.max(5_000, Math.round(Number(openClawChatConfig.timeoutMs) || 180_000)) + ), + }; + + if (normalizedConfig.enabled) { + if (!normalizedConfig.baseUrl) { + toast.error("请填写大龙虾聊天接口地址"); + return; + } + if (!normalizedConfig.token && !openClawChatTokenConfigured) { + toast.error("请填写大龙虾聊天接口 Token"); + return; + } + } + + const response = await fetch("/api/admin/settings", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + key: OPENCLAW_CHAT_CONFIG_KEY, + value: normalizedConfig, + }), + }); + const responseData = await response.json().catch(() => null); + if (!response.ok) { + throw new Error(responseData?.message || responseData?.error || "保存失败"); + } + + const savedValue = + responseData?.setting?.value && + typeof responseData.setting.value === "object" + ? (responseData.setting.value as OpenClawChatConfig) + : normalizedConfig; + setOpenClawChatConfig({ + enabled: savedValue.enabled, + baseUrl: savedValue.baseUrl, + token: "", + timeoutMs: savedValue.timeoutMs, + }); + setOpenClawChatTokenConfigured( + Boolean((responseData?.setting?.value as any)?.hasToken) || + Boolean(normalizedConfig.token) + ); + toast.success("大龙虾聊天接口配置已保存"); + mutateOpenClawChatSetting(); + mutateSiteSettings(); + } catch (error) { + toast.error( + error instanceof Error ? error.message : "保存大龙虾聊天接口配置失败" + ); + } + }; + // --- 网站设置逻辑 --- const handleSaveSiteSettings = async () => { try { @@ -12068,6 +12172,96 @@ export function AdminSettingsClient() { /> +
+
+
+ +
+ 开启并配置接口后,右侧对话框模式菜单显示 + Clawd模式;Token 仅保存在服务端,不会下发到前端。 +
+
+ + setOpenClawChatConfig((prev) => ({ + ...prev, + enabled: checked, + })) + } + /> +
+
+
+ + + setOpenClawChatConfig((prev) => ({ + ...prev, + baseUrl: event.target.value, + })) + } + placeholder={DEFAULT_OPENCLAW_CHAT_BASE_URL} + value={openClawChatConfig.baseUrl} + /> +
+
+ + + setOpenClawChatConfig((prev) => ({ + ...prev, + timeoutMs: Number(event.target.value) || 180000, + })) + } + type="number" + value={openClawChatConfig.timeoutMs} + /> +
+
+
+ + + setOpenClawChatConfig((prev) => ({ + ...prev, + token: event.target.value, + })) + } + placeholder={ + openClawChatTokenConfigured + ? "已配置,留空则保持不变" + : "只保存在服务端,禁止下发到浏览器" + } + type="password" + value={openClawChatConfig.token} + /> +
+
+ +
+
+ {siteSettings.agentEnabled && (
diff --git a/app/api/admin/settings/route.ts b/app/api/admin/settings/route.ts index f3d970b02..f25f19ee6 100644 --- a/app/api/admin/settings/route.ts +++ b/app/api/admin/settings/route.ts @@ -71,6 +71,11 @@ import { AGENT_WORKFLOW_MATERIAL_API_CONFIG_KEY, normalizeAgentWorkflowMaterialApiConfig, } from "@/lib/agents/workflow-material-config"; +import { + normalizeOpenClawChatConfig, + OPENCLAW_CHAT_CONFIG_KEY, + toAdminOpenClawChatConfig, +} from "@/lib/openclaw-chat/config"; import { clearScopedSiteSettingsCache } from "@/lib/site-settings/public-cache"; const settingSchema = z.object({ @@ -149,6 +154,12 @@ export async function GET(request: Request) { setting: setting ? { ...setting, value } : { key, value }, }); } + if (key === OPENCLAW_CHAT_CONFIG_KEY) { + const value = toAdminOpenClawChatConfig(setting?.value); + return Response.json({ + setting: setting ? { ...setting, value } : { key, value }, + }); + } return Response.json({ setting }); } @@ -301,6 +312,45 @@ export async function POST(request: Request) { }); } + if (key === OPENCLAW_CHAT_CONFIG_KEY) { + const existingSetting = await getSystemSetting({ + key: OPENCLAW_CHAT_CONFIG_KEY, + }).catch(() => null); + const existingValue = + existingSetting?.value && typeof existingSetting.value === "object" + ? (existingSetting.value as Record) + : {}; + const normalizedValue = normalizeOpenClawChatConfig( + value, + String(existingValue.token || "").trim() + ); + + if (normalizedValue.enabled) { + if (!normalizedValue.baseUrl) { + return new ChatSDKError("bad_request:api", "请填写大龙虾聊天接口地址").toResponse(); + } + if (!normalizedValue.token) { + return new ChatSDKError("bad_request:api", "请填写大龙虾聊天接口 Token").toResponse(); + } + } + + const saved = await upsertSystemSetting({ + key: OPENCLAW_CHAT_CONFIG_KEY, + value: normalizedValue, + updatedBy: session.user.id, + }); + clearScopedSiteSettingsCache(); + + return Response.json({ + setting: saved[0] + ? { + ...saved[0], + value: toAdminOpenClawChatConfig(saved[0].value), + } + : undefined, + }); + } + if (key === VIDEO_CANVAS_FUNCTION_CONFIG_KEY) { nextValue = normalizeVideoCanvasFunctionConfig(value); } diff --git a/app/api/canvas/clawd-chat/route.ts b/app/api/canvas/clawd-chat/route.ts new file mode 100644 index 000000000..14c458aaa --- /dev/null +++ b/app/api/canvas/clawd-chat/route.ts @@ -0,0 +1,212 @@ +import { NextResponse } from "next/server"; +import { z } from "zod"; +import { auth } from "@/app/(auth)/auth"; +import { + getChatById, + getSystemSetting, + saveChat, + saveMessages, +} from "@/lib/db/queries"; +import { + normalizeOpenClawChatConfig, + OPENCLAW_CHAT_CONFIG_KEY, +} from "@/lib/openclaw-chat/config"; +import { generateUUID } from "@/lib/utils"; + +const requestSchema = z.object({ + id: z.string().uuid(), + message: z.string().trim().min(1).max(8000), + userMessageId: z.string().uuid().optional(), + metadata: z.record(z.unknown()).optional(), +}); + +function buildChatTitle(message: string) { + const normalized = message.replace(/\s+/g, " ").trim(); + if (!normalized) return "Clawd 对话"; + return normalized.length > 80 ? `${normalized.slice(0, 80)}...` : normalized; +} + +function createConversationId(userId: string, chatId: string) { + return `site-user-${userId}-canvas-${chatId}`; +} + +async function readOpenClawConfig() { + const setting = await getSystemSetting({ key: OPENCLAW_CHAT_CONFIG_KEY }).catch( + () => null + ); + return normalizeOpenClawChatConfig(setting?.value); +} + +function getOpenClawErrorMessage(payload: unknown, fallback: string) { + if (payload && typeof payload === "object") { + const raw = payload as Record; + const nestedError = + raw.error && typeof raw.error === "object" + ? (raw.error as Record) + : null; + const message = nestedError?.message || raw.message || raw.error; + if (typeof message === "string" && message.trim()) { + return message.trim(); + } + } + return fallback; +} + +export async function POST(request: Request) { + const session = await auth().catch(() => null); + + if (!session?.user?.id) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const parsed = requestSchema.safeParse(await request.json().catch(() => null)); + if (!parsed.success) { + return NextResponse.json({ error: "请求参数不正确" }, { status: 400 }); + } + + const config = await readOpenClawConfig(); + if (!config.enabled) { + return NextResponse.json({ error: "Clawd 模式未启用" }, { status: 403 }); + } + if (!config.baseUrl || !config.token) { + return NextResponse.json( + { error: "Clawd 接口地址或 Token 未配置" }, + { status: 500 } + ); + } + + const existingChat = await getChatById({ id: parsed.data.id }).catch(() => null); + if (existingChat && existingChat.userId !== session.user.id) { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + if (!existingChat) { + await saveChat({ + id: parsed.data.id, + userId: session.user.id, + title: buildChatTitle(parsed.data.message), + visibility: "private", + }); + } + + const userMessageId = parsed.data.userMessageId || generateUUID(); + const assistantMessageId = generateUUID(); + const now = new Date(); + const conversationId = createConversationId(session.user.id, parsed.data.id); + const endpoint = `${config.baseUrl}/api/chat`; + + let upstreamPayload: unknown = null; + let upstreamStatus = 502; + + try { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), config.timeoutMs); + const response = await fetch(endpoint, { + method: "POST", + headers: { + Authorization: `Bearer ${config.token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + conversation_id: conversationId, + user_id: session.user.id, + user_name: session.user.name || session.user.email || "", + message: parsed.data.message, + metadata: { + source: "ai_chatbot_canvas", + page: "/canvas", + tenant: "default", + chat_id: parsed.data.id, + ...parsed.data.metadata, + }, + }), + signal: controller.signal, + }).finally(() => clearTimeout(timeout)); + + upstreamStatus = response.status; + upstreamPayload = await response.json().catch(() => null); + + if (!response.ok) { + return NextResponse.json( + { + error: getOpenClawErrorMessage(upstreamPayload, "大龙虾聊天接口调用失败"), + }, + { status: response.status } + ); + } + } catch (error) { + const isAbortError = + error instanceof Error && error.name === "AbortError"; + return NextResponse.json( + { + error: isAbortError + ? "大龙虾聊天接口请求超时" + : "大龙虾聊天接口暂时不可用", + }, + { status: isAbortError ? 504 : upstreamStatus } + ); + } + + const answer = + upstreamPayload && typeof upstreamPayload === "object" + ? String((upstreamPayload as Record).answer || "").trim() + : ""; + + if (!answer) { + return NextResponse.json( + { error: "大龙虾聊天接口未返回有效回答" }, + { status: 502 } + ); + } + + const userMessage = { + id: userMessageId, + chatId: parsed.data.id, + role: "user", + parts: [{ type: "text", text: parsed.data.message }], + attachments: [], + createdAt: now, + videoStorageStatus: null, + temporaryVideoUrl: null, + permanentVideoUrl: null, + archivedAt: null, + videoMetadata: null, + }; + const assistantMessage = { + id: assistantMessageId, + chatId: parsed.data.id, + role: "assistant", + parts: [{ type: "text", text: answer }], + attachments: [], + createdAt: new Date(now.getTime() + 1), + videoStorageStatus: null, + temporaryVideoUrl: null, + permanentVideoUrl: null, + archivedAt: null, + videoMetadata: null, + }; + + await saveMessages({ messages: [userMessage, assistantMessage] }); + + return NextResponse.json({ + conversationId, + requestId: + upstreamPayload && typeof upstreamPayload === "object" + ? (upstreamPayload as Record).request_id || null + : null, + messages: [ + { + id: userMessage.id, + role: "user", + parts: userMessage.parts, + metadata: { createdAt: userMessage.createdAt.toISOString() }, + }, + { + id: assistantMessage.id, + role: "assistant", + parts: assistantMessage.parts, + metadata: { createdAt: assistantMessage.createdAt.toISOString() }, + }, + ], + }); +} diff --git a/app/api/site-settings/route.ts b/app/api/site-settings/route.ts index 2ff5d1713..961334d99 100644 --- a/app/api/site-settings/route.ts +++ b/app/api/site-settings/route.ts @@ -125,6 +125,10 @@ import { HOME_CHAT_ANIMATION_MAX_BYTES, normalizeHomeChatAnimationConfig, } from "@/lib/home/home-animation-config"; +import { + normalizePublicOpenClawChatConfig, + OPENCLAW_CHAT_CONFIG_KEY, +} from "@/lib/openclaw-chat/config"; import { ENHANCE_PRO_KREA_SETTING_KEY, normalizeEnhanceProKreaConfig, @@ -837,6 +841,7 @@ async function getScopedPublicSiteSettings( LOVART_COMPOSER_PREVIEW_TEXTS_KEY, LOVART_COMPOSER_PREFIX_KEY, CANVAS_TOOLBAR_CONFIG_SETTING_KEY, + OPENCLAW_CHAT_CONFIG_KEY, ]); const typedSettings = settings as Record | undefined; @@ -864,6 +869,9 @@ async function getScopedPublicSiteSettings( canvasToolbarConfig: normalizeCanvasToolbarConfig( valueByKey.get(CANVAS_TOOLBAR_CONFIG_SETTING_KEY) ), + openClawChatConfig: normalizePublicOpenClawChatConfig( + valueByKey.get(OPENCLAW_CHAT_CONFIG_KEY) + ), }); } diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index c9522cc2e..0bf52c694 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -299,6 +299,8 @@ interface CanvasChatPanelProps { canvasToolbarConfig?: CanvasToolbarConfig; } +type CanvasComposerMode = "agent" | "image" | "video" | "detail" | "clawd"; + type VideoStoryboardStreamData = | { stage: "detecting"; @@ -1206,6 +1208,7 @@ export function CanvasChatPanel({ const [isDetailGenerationMode, setIsDetailGenerationMode] = useState(false); const [detailGenerationMode, setDetailGenerationMode] = useState("classic"); const [isDetailGenerating, setIsDetailGenerating] = useState(false); + const [isClawdSubmitting, setIsClawdSubmitting] = useState(false); const [detailSections, setDetailSections] = useState>({ required: true, strategy: false, @@ -2577,6 +2580,10 @@ export function CanvasChatPanel({ canvasAutoModeDefaultModelId?: string | null; canvasDetailDefaultModelId?: string | null; canvasToolbarConfig?: CanvasToolbarConfig; + openClawChatConfig?: { + enabled?: boolean; + baseUrl?: string; + }; }>("/api/site-settings?scope=canvas-chat", fetcher); const { data: canvasSkillTagsData } = useSWR<{ enabled?: boolean; @@ -2612,6 +2619,7 @@ export function CanvasChatPanel({ : normalizeCanvasToolbarConfig(siteSettings?.canvasToolbarConfig), [propCanvasToolbarConfig, rawCanvasToolbarConfig, siteSettings?.canvasToolbarConfig] ); + const isClawdModeEnabled = siteSettings?.openClawChatConfig?.enabled === true; const getToolbarItem = useCallback( (key: CanvasToolbarItemKey) => getCanvasToolbarItem(canvasToolbarConfig, key), [canvasToolbarConfig] @@ -3000,16 +3008,16 @@ export function CanvasChatPanel({ hasSelectedAgentPreset && !isDetailGenerationMode && (!selectedModelId || selectedModelId === "auto" || isConversationModel); - const computedComposerMode = useMemo<"agent" | "image" | "video" | "detail">(() => { + const computedComposerMode = useMemo(() => { if (isDetailGenerationMode) return "detail"; if (isVideoModel) return "video"; if (isAgentModel) return "agent"; return "image"; }, [isAgentModel, isDetailGenerationMode, isVideoModel]); - const [composerModeOverride, setComposerModeOverride] = useState< - "agent" | "image" | "video" | "detail" | null - >(null); + const [composerModeOverride, setComposerModeOverride] = + useState(null); const activeComposerMode = composerModeOverride || computedComposerMode; + const isClawdMode = activeComposerMode === "clawd" && isClawdModeEnabled; const agentSelectableModels = useMemo( () => effectiveAgentEnabled ? [SMART_AUTO_MODEL, ...conversationOnlyModels] : conversationOnlyModels, @@ -3348,7 +3356,9 @@ export function CanvasChatPanel({ [videoOnlyModels] ); const activeModeLabel = - activeComposerMode === "video" + activeComposerMode === "clawd" + ? "Clawd模式" + : activeComposerMode === "video" ? "视频" : activeComposerMode === "detail" ? getToolbarItem("chatModeDetailActive").label @@ -3655,12 +3665,21 @@ export function CanvasChatPanel({ ]); const handleSelectGenerationMode = useCallback( - (mode: "agent" | "image" | "video" | "detail") => { + (mode: CanvasComposerMode) => { hiddenPromptRef.current = null; autoContextRef.current = null; + if (mode === "clawd" && !isClawdModeEnabled) { + toast.error("Clawd模式未开启"); + return; + } activeComposerModeRef.current = mode; setComposerModeOverride(mode); + if (mode === "clawd") { + setIsDetailGenerationMode(false); + return; + } + if (mode === "agent") { if (!effectiveAgentEnabled) { return; @@ -3725,6 +3744,7 @@ export function CanvasChatPanel({ [ canUseDetailGeneration, effectiveAgentEnabled, + isClawdModeEnabled, allModels, computedComposerMode, firstImageModelId, @@ -3739,6 +3759,17 @@ export function CanvasChatPanel({ ] ); + useEffect(() => { + if (activeComposerMode === "clawd" && !isClawdModeEnabled) { + handleSelectGenerationMode(effectiveAgentEnabled ? "agent" : "image"); + } + }, [ + activeComposerMode, + effectiveAgentEnabled, + handleSelectGenerationMode, + isClawdModeEnabled, + ]); + useEffect(() => { if (!effectiveAgentEnabled || typeof window === "undefined") return; const targetWindow = window as CanvasAgentPendingRequestWindow; @@ -8459,6 +8490,108 @@ export function CanvasChatPanel({ return; } + if (isClawdMode) { + const textContent = input + .replace(/\u200b/g, " ") + .replace(/\s+/g, " ") + .trim(); + + if (!textContent) { + return; + } + if (attachments.length > 0) { + toast.error("Clawd模式当前仅支持文本对话"); + return; + } + + const sendSignature = JSON.stringify({ + chatId: activeChatId, + mode: "clawd", + text: textContent, + }); + + if (shouldBlockDuplicateSend(activeChatId, sendSignature)) { + toast.message("检测到重复发送,已自动忽略"); + return; + } + + const userMessageId = generateUUID(); + const optimisticUserMessage: ChatMessage = { + id: userMessageId, + role: "user", + parts: [{ type: "text", text: textContent }], + metadata: { createdAt: new Date().toISOString() }, + } as ChatMessage; + + setSendGuard(activeChatId, { + inFlight: true, + signature: sendSignature, + timestamp: Date.now(), + }); + setIsClawdSubmitting(true); + setMessages((previousMessages) => + sortCanvasMessagesByCreatedAt([ + ...previousMessages.filter((message) => message.id !== userMessageId), + optimisticUserMessage, + ]) + ); + + try { + const response = await fetch("/api/canvas/clawd-chat", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + id: rawChatId, + userMessageId, + message: textContent, + metadata: { + composerMode: "clawd", + activeChatId, + }, + }), + }); + const responseData = await response.json().catch(() => null); + if (!response.ok) { + throw new Error(responseData?.error || "Clawd 请求失败"); + } + + const returnedMessages = Array.isArray(responseData?.messages) + ? (responseData.messages as ChatMessage[]) + : []; + if (returnedMessages.length > 0) { + const returnedIds = new Set(returnedMessages.map((message) => message.id)); + setMessages((previousMessages) => + sortCanvasMessagesByCreatedAt([ + ...previousMessages.filter( + (message) => + message.id !== userMessageId && !returnedIds.has(message.id) + ), + ...returnedMessages, + ]) + ); + } + if (rawChatId) { + void mutate(buildHistoryMessagesUrl(rawChatId, { cacheBust: true })); + } + hasHydratedHistoryRef.current = true; + setInput(""); + setAttachments([]); + clearSelection(); + + const editableDiv = textareaRef.current as HTMLDivElement | null; + if (editableDiv) { + editableDiv.innerHTML = ""; + } + } catch (error) { + console.error("[CanvasChat] Clawd request failed:", error); + toast.error(error instanceof Error ? error.message : "Clawd 请求失败"); + } finally { + setIsClawdSubmitting(false); + releaseSendGuard(activeChatId); + } + return; + } + if (!input.trim() && attachments.length === 0 && !selectedSkillTag && !hasWorkflowContext) { return; } @@ -8770,6 +8903,7 @@ export function CanvasChatPanel({ imageParams, selectedImageNodes, isDetailGenerationMode, + isClawdMode, canUseDetailGeneration, handleDetailGeneration, ensureImagePointsBeforeSend, @@ -8781,8 +8915,11 @@ export function CanvasChatPanel({ effectiveAgentEnabled, releaseSendGuard, setSendGuard, + setMessages, shouldBlockDuplicateSend, status, + mutate, + buildHistoryMessagesUrl, workflowContext, ]); @@ -9271,13 +9408,15 @@ export function CanvasChatPanel({ ); const isLoading = status === 'streaming' || status === 'submitted'; - const isSubmitting = isLoading || isDetailGenerating; + const isSubmitting = isLoading || isDetailGenerating || isClawdSubmitting; const hasInputOrAttachments = Boolean( input.trim() || attachments.length > 0 || (!isDetailGenerationMode && selectedSkillTag) ); const hasEnoughAgentUploads = !hasAgentUploadSlots || attachments.length >= agentRequiredUploadCount; const composerPlaceholder = isWorkspacePresentation - ? isDetailGenerationMode + ? isClawdMode + ? "输入要交给 Clawd 的问题" + : isDetailGenerationMode ? "可选补充:风格、情绪氛围、禁用元素、期望生成张数(如:生成8张)" : isVideoWorkspace ? "描述镜头、主体和运动方式,也可以先切到首帧或首尾帧模式" @@ -9286,7 +9425,9 @@ export function CanvasChatPanel({ : hasAgentUploadSlots ? "可选补充本次生图要求,例如:高级感、暖灰色、近景特写、生成3张。" : "可选补充本次生成要求,例如:更高级、更生活化、暖调灯光。" - : isDetailGenerationMode + : isClawdMode + ? "输入要交给 Clawd 的问题" + : isDetailGenerationMode ? "可选补充:风格、情绪氛围、禁用元素、期望生成张数(如:生成8张)" : isVideoWorkspace ? isVideoModel && videoFrameSlots.length > 0 @@ -9309,10 +9450,12 @@ export function CanvasChatPanel({ detailProgress.total > 0 ? Math.min(100, Math.round((detailProgress.current / detailProgress.total) * 100)) : 0; - const canSend = isDetailGenerationMode - ? detailValidationIssues.length === 0 && !isDetailGenerating - : hasInputOrAttachments && - hasEnoughAgentUploads; + const canSend = isClawdMode + ? Boolean(input.trim()) && !isClawdSubmitting + : isDetailGenerationMode + ? detailValidationIssues.length === 0 && !isDetailGenerating + : hasInputOrAttachments && + hasEnoughAgentUploads; const currentVideoImageLimit = isVideoModel ? getVideoImageLimit(videoParams, currentModel?.providerType, currentModel?.videoToolCapability) : null; @@ -9604,13 +9747,17 @@ export function CanvasChatPanel({ !isLoading && !shouldShowImageLoading; const workspaceEmptyTitle = - activeComposerMode === "agent" && !isPresetAgentSelection + isClawdMode + ? "开始 Clawd 对话" + : activeComposerMode === "agent" && !isPresetAgentSelection ? "开始 Auto Agent 对话" : hasAgentUploadSlots && attachments.length < agentRequiredUploadCount ? `请先补齐图片槽位(${attachments.length}/${agentRequiredUploadCount})` : "准备开始生成"; const workspaceEmptyDescription = - activeComposerMode === "agent" && !isPresetAgentSelection + isClawdMode + ? "输入问题后会通过已配置的大龙虾 OpenClaw 聊天接口回答。" + : activeComposerMode === "agent" && !isPresetAgentSelection ? hasSelectedAgentPreset ? "" : "" @@ -12202,7 +12349,7 @@ export function CanvasChatPanel({ }} /> ))} - {!isWorkspacePresentation && !isDetailGenerationMode && activeComposerMode !== "image" && (!isVideoModel || showVideoAttachmentButton) && ( + {!isWorkspacePresentation && !isDetailGenerationMode && activeComposerMode !== "image" && activeComposerMode !== "clawd" && (!isVideoModel || showVideoAttachmentButton) && (