diff --git a/app/(admin)/settings/admin-settings-client.tsx b/app/(admin)/settings/admin-settings-client.tsx
index ac6b03e4e..169063a20 100644
--- a/app/(admin)/settings/admin-settings-client.tsx
+++ b/app/(admin)/settings/admin-settings-client.tsx
@@ -12345,11 +12345,11 @@ export function AdminSettingsClient() {
开启并配置接口后,右侧对话框模式菜单显示
- Clawd;Token 仅保存在服务端,不会下发到前端。
+ Claw;Token 仅保存在服务端,不会下发到前端。
& {
function buildChatTitle(message: string) {
const normalized = message.replace(/\s+/g, " ").trim();
- if (!normalized) return "Clawd 对话";
+ if (!normalized) return "Claw 对话";
return normalized.length > 80 ? `${normalized.slice(0, 80)}...` : normalized;
}
@@ -144,11 +144,11 @@ export async function POST(request: Request) {
const config = await readOpenClawConfig();
if (!config.enabled) {
- return NextResponse.json({ error: "Clawd 未启用" }, { status: 403 });
+ return NextResponse.json({ error: "Claw 未启用" }, { status: 403 });
}
if (!config.baseUrl || !config.token) {
return NextResponse.json(
- { error: "Clawd 接口地址或 Token 未配置" },
+ { error: "Claw 接口地址或 Token 未配置" },
{ status: 500 }
);
}
diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx
index 924889e78..cc384e868 100644
--- a/components/canvas/CanvasChatPanel.tsx
+++ b/components/canvas/CanvasChatPanel.tsx
@@ -311,8 +311,10 @@ interface CanvasChatPanelProps {
type CanvasComposerMode = "agent" | "image" | "video" | "detail" | "clawd";
+const CLAW_MODE_LABEL = "Claw";
+
function getCanvasComposerModeSessionLabel(mode: CanvasComposerMode) {
- if (mode === "clawd") return "Clawd";
+ if (mode === "clawd") return CLAW_MODE_LABEL;
if (mode === "video") return "视频";
if (mode === "detail") return "AI详情";
if (mode === "image") return "图像";
@@ -952,16 +954,125 @@ type CanvasTypewriterTextProps = {
enabled: boolean;
className?: string;
renderMarkdown?: boolean;
+ preserveMarkdownTables?: boolean;
onActiveChange?: (typingKey: string, active: boolean) => void;
onComplete?: (typingKey: string) => void;
};
+type MarkdownTableBlockRange = {
+ start: number;
+ end: number;
+};
+
+function isLikelyMarkdownTableRow(line: string) {
+ const trimmed = line.trim();
+ if (!trimmed || !trimmed.includes("|")) return false;
+
+ const cells = trimmed
+ .replace(/^\|/, "")
+ .replace(/\|$/, "")
+ .split("|");
+ return cells.length >= 2;
+}
+
+function isMarkdownTableSeparatorRow(line: string) {
+ if (!isLikelyMarkdownTableRow(line)) return false;
+
+ const cells = line
+ .trim()
+ .replace(/^\|/, "")
+ .replace(/\|$/, "")
+ .split("|")
+ .map((cell) => cell.trim().replace(/\s+/g, ""));
+
+ return cells.length >= 2 && cells.every((cell) => /^:?-{3,}:?$/.test(cell));
+}
+
+function getMarkdownTableBlockRanges(content: string): MarkdownTableBlockRange[] {
+ if (!content.includes("|")) return [];
+
+ const rawLines = content.split("\n");
+ const lines = rawLines.map((line, index) => {
+ const hasNewline = index < rawLines.length - 1;
+ const text = hasNewline ? `${line}\n` : line;
+ return {
+ line: line.replace(/\r$/, ""),
+ text,
+ };
+ });
+
+ let offset = 0;
+ const indexedLines = lines.map((line) => {
+ const start = offset;
+ offset += line.text.length;
+ return {
+ ...line,
+ start,
+ end: offset,
+ };
+ });
+
+ const ranges: MarkdownTableBlockRange[] = [];
+ for (let index = 0; index < indexedLines.length - 1; index += 1) {
+ const header = indexedLines[index];
+ const separator = indexedLines[index + 1];
+ if (
+ !header ||
+ !separator ||
+ !isLikelyMarkdownTableRow(header.line) ||
+ !isMarkdownTableSeparatorRow(separator.line)
+ ) {
+ continue;
+ }
+
+ let endIndex = index + 2;
+ while (
+ endIndex < indexedLines.length &&
+ isLikelyMarkdownTableRow(indexedLines[endIndex]?.line || "")
+ ) {
+ endIndex += 1;
+ }
+
+ ranges.push({
+ start: header.start,
+ end: indexedLines[endIndex - 1]?.end || separator.end,
+ });
+ index = endIndex - 1;
+ }
+
+ return ranges;
+}
+
+function advanceTypewriterCursor(
+ contentLength: number,
+ targetCursor: number,
+ tableRanges: MarkdownTableBlockRange[]
+) {
+ const clampedCursor = Math.min(contentLength, targetCursor);
+ for (const range of tableRanges) {
+ if (clampedCursor >= range.start && clampedCursor < range.end) {
+ return Math.min(contentLength, range.end);
+ }
+ }
+ return clampedCursor;
+}
+
+function CanvasTypewriterCursor() {
+ return (
+
+ );
+}
+
function CanvasTypewriterText({
text,
typingKey,
enabled,
className,
renderMarkdown = false,
+ preserveMarkdownTables = false,
onActiveChange,
onComplete,
}: CanvasTypewriterTextProps) {
@@ -980,12 +1091,15 @@ function CanvasTypewriterText({
}
let cursor = 0;
+ const tableRanges = preserveMarkdownTables
+ ? getMarkdownTableBlockRanges(content)
+ : [];
activeRef.current = true;
onActiveChange?.(typingKey, true);
setDisplayText("");
const timer = window.setInterval(() => {
- cursor += 1;
+ cursor = advanceTypewriterCursor(content.length, cursor + 1, tableRanges);
if (cursor >= content.length) {
setDisplayText(content);
window.clearInterval(timer);
@@ -1006,13 +1120,25 @@ function CanvasTypewriterText({
onActiveChange?.(typingKey, false);
}
};
- }, [content, enabled, onActiveChange, onComplete, typingKey]);
+ }, [content, enabled, onActiveChange, onComplete, preserveMarkdownTables, typingKey]);
+
+ const showCursor = enabled && Boolean(content) && displayText.length < content.length;
if (renderMarkdown) {
- return {displayText};
+ return (
+
+ {displayText}
+ {showCursor ? : null}
+
+ );
}
- return {displayText}
;
+ return (
+
+ {displayText}
+ {showCursor ? : null}
+
+ );
}
function CanvasAssistantTypingPlaceholder({ hint }: { hint?: string | null }) {
@@ -1037,7 +1163,7 @@ function CanvasAssistantTypingPlaceholder({ hint }: { hint?: string | null }) {
) : null}
{[0, 1, 2].map((index) => (
@@ -1553,6 +1679,7 @@ export function CanvasChatPanel({
const [analysisCollapseState, setAnalysisCollapseState] = useState>({});
const [typedAssistantSections, setTypedAssistantSections] = useState>({});
const [activeTypingSections, setActiveTypingSections] = useState>({});
+ const [clawdTypewriterMessageIds, setClawdTypewriterMessageIds] = useState>({});
const [insufficientPointsState, setInsufficientPointsState] = useState<{
balance: number;
expectedCost: number;
@@ -3633,7 +3760,7 @@ export function CanvasChatPanel({
);
const activeModeLabel =
activeComposerMode === "clawd"
- ? "Clawd"
+ ? CLAW_MODE_LABEL
: activeComposerMode === "video"
? "视频"
: activeComposerMode === "detail"
@@ -3978,7 +4105,7 @@ export function CanvasChatPanel({
hiddenPromptRef.current = null;
autoContextRef.current = null;
if (mode === "clawd" && !isClawdModeEnabled) {
- toast.error("Clawd 未开启");
+ toast.error(`${CLAW_MODE_LABEL} 未开启`);
return;
}
if (mode === "agent") {
@@ -8991,7 +9118,7 @@ export function CanvasChatPanel({
});
const responseData = await response.json().catch(() => null);
if (!response.ok) {
- throw new Error(responseData?.error || "Clawd 请求失败");
+ throw new Error(responseData?.error || `${CLAW_MODE_LABEL} 请求失败`);
}
const returnedMessages = Array.isArray(responseData?.messages)
@@ -8999,6 +9126,19 @@ export function CanvasChatPanel({
: [];
if (returnedMessages.length > 0) {
const returnedIds = new Set(returnedMessages.map((message) => message.id));
+ const returnedAssistantIds = returnedMessages
+ .filter((message) => message.role === "assistant")
+ .map((message) => message.id)
+ .filter(Boolean);
+ if (returnedAssistantIds.length > 0) {
+ setClawdTypewriterMessageIds((previousIds) => {
+ const nextIds = { ...previousIds };
+ returnedAssistantIds.forEach((messageId) => {
+ nextIds[messageId] = true;
+ });
+ return nextIds;
+ });
+ }
setMessages((previousMessages) =>
sortCanvasMessagesByCreatedAt([
...previousMessages.filter(
@@ -9014,11 +9154,11 @@ export function CanvasChatPanel({
}
hasHydratedHistoryRef.current = true;
} catch (error) {
- console.error("[CanvasChat] Clawd request failed:", error);
+ console.error("[CanvasChat] Claw request failed:", error);
setMessages((previousMessages) =>
previousMessages.filter((message) => message.id !== userMessageId)
);
- toast.error(error instanceof Error ? error.message : "Clawd 请求失败");
+ toast.error(error instanceof Error ? error.message : `${CLAW_MODE_LABEL} 请求失败`);
setClawdTableFiles(tableFilesSnapshot);
setAttachments(attachmentsSnapshot);
const currentEditorText = (editableDiv?.textContent || "")
@@ -9956,7 +10096,7 @@ export function CanvasChatPanel({
const hasEnoughAgentUploads = !hasAgentUploadSlots || attachments.length >= agentRequiredUploadCount;
const composerPlaceholder = isWorkspacePresentation
? isClawdMode
- ? "输入要交给 Clawd 的问题,可上传图片或表格"
+ ? `输入要交给 ${CLAW_MODE_LABEL} 的问题,可上传图片或表格`
: isDetailGenerationMode
? "可选补充:风格、情绪氛围、禁用元素、期望生成张数(如:生成8张)"
: isVideoWorkspace
@@ -9967,7 +10107,7 @@ export function CanvasChatPanel({
? "可选补充本次生图要求,例如:高级感、暖灰色、近景特写、生成3张。"
: "可选补充本次生成要求,例如:更高级、更生活化、暖调灯光。"
: isClawdMode
- ? "输入要交给 Clawd 的问题,可上传图片或表格"
+ ? `输入要交给 ${CLAW_MODE_LABEL} 的问题,可上传图片或表格`
: isDetailGenerationMode
? "可选补充:风格、情绪氛围、禁用元素、期望生成张数(如:生成8张)"
: isVideoWorkspace
@@ -10289,7 +10429,7 @@ export function CanvasChatPanel({
!shouldShowImageLoading;
const workspaceEmptyTitle =
isClawdMode
- ? "开始 Clawd 对话"
+ ? `开始 ${CLAW_MODE_LABEL} 对话`
: activeComposerMode === "agent" && !isPresetAgentSelection
? "开始 Auto Agent 对话"
: hasAgentUploadSlots && attachments.length < agentRequiredUploadCount
@@ -10961,6 +11101,10 @@ export function CanvasChatPanel({
isLoading &&
!hasAssistantMedia &&
index === renderableVisibleMessages.length - 1;
+ const shouldTypeClawAssistantMessage =
+ message.role === "assistant" &&
+ !hasAssistantMedia &&
+ Boolean(clawdTypewriterMessageIds[message.id]);
const messageTimestamp = getMessageTimestamp(message);
const prevMessageTimestamp =
@@ -11137,16 +11281,18 @@ export function CanvasChatPanel({
const analysisTypingKey = `${message.id}-${i}-analysis-typing`;
const plainTextTypingKey = `${message.id}-${i}-plain-typing`;
const shouldAnimateAnalysis =
- shouldTypeAssistantMessage &&
Boolean(analysisSection?.analysis) &&
- !hasCanvasRenderableMarkdown(analysisSection?.analysis || "") &&
- !typedAssistantSections[analysisTypingKey];
+ !typedAssistantSections[analysisTypingKey] &&
+ ((shouldTypeAssistantMessage &&
+ !hasCanvasRenderableMarkdown(analysisSection?.analysis || "")) ||
+ shouldTypeClawAssistantMessage);
const shouldAnimatePlainText =
- shouldTypeAssistantMessage &&
!analysisSection &&
Boolean(plainText) &&
- !hasCanvasRenderableMarkdown(plainText) &&
- !typedAssistantSections[plainTextTypingKey];
+ !typedAssistantSections[plainTextTypingKey] &&
+ ((shouldTypeAssistantMessage &&
+ !hasCanvasRenderableMarkdown(plainText)) ||
+ shouldTypeClawAssistantMessage);
const shouldShowDoneSummary =
!isDetailGenerationMode &&
imageUrls.length > 0 &&
@@ -11216,6 +11362,7 @@ export function CanvasChatPanel({
enabled={shouldAnimateAnalysis}
className="break-words [overflow-wrap:anywhere]"
renderMarkdown
+ preserveMarkdownTables={shouldTypeClawAssistantMessage}
onActiveChange={handleTypewriterActiveChange}
onComplete={handleTypewriterComplete}
/>
@@ -11232,6 +11379,7 @@ export function CanvasChatPanel({
enabled={shouldAnimatePlainText}
className="break-words [overflow-wrap:anywhere]"
renderMarkdown
+ preserveMarkdownTables={shouldTypeClawAssistantMessage}
onActiveChange={handleTypewriterActiveChange}
onComplete={handleTypewriterComplete}
/>
@@ -13023,7 +13171,7 @@ export function CanvasChatPanel({
className={getComposerModeMenuItemClass("clawd")}
>
- Clawd
+ {CLAW_MODE_LABEL}
{activeComposerMode === "clawd" ? (
) : null}
diff --git a/lib/openclaw-chat/spreadsheet-markdown.ts b/lib/openclaw-chat/spreadsheet-markdown.ts
index babae2791..03f902083 100644
--- a/lib/openclaw-chat/spreadsheet-markdown.ts
+++ b/lib/openclaw-chat/spreadsheet-markdown.ts
@@ -336,7 +336,7 @@ export async function spreadsheetFileToOpenClawMarkdown(file: File) {
const extension = getSpreadsheetExtension(file.name);
if (!SUPPORTED_EXTENSIONS.has(extension)) {
- throw new Error("Clawd 目前支持 .xlsx、.csv、.tsv 表格文件");
+ throw new Error("Claw 目前支持 .xlsx、.csv、.tsv 表格文件");
}
if (extension === "csv") {