bug修改

This commit is contained in:
董宏凯 committed 2026-08-26 15:46:49 +08:00
1 parent 910cee9e67
commit 704438b3d2
6 files changed
+278 -36

No files matched your search

+8 -17
View File
@@ -3,8 +3,6 @@ import { useEffect, useMemo, useRef, useState } from "react";
import { useThread } from "@/components/workspace/messages/context";
import { isImageFile } from "../utils/files";
import {
loadArtifactContent,
loadArtifactContentFromLatestWriteFile,
@@ -15,22 +13,13 @@ import {
isSandboxStreamPerfEnabled,
} from "./sandbox-stream-perf";
import { isArtifactApiPath } from "./streaming-write-preview";
import { isWriteFileInFlight, isWriteFileStreamingUrl } from "./write-file-in-flight";
import { updateArtifactContent } from "./api";
function isWriteFileStreamingUrl(filepath: string): boolean {
if (!filepath.startsWith("write-file:")) {
return false;
}
try {
const url = new URL(filepath);
const path = decodeURIComponent(url.pathname);
return !isImageFile(path);
} catch {
return true;
}
}
function hasToolResult(thread: ReturnType<typeof useThread>["thread"], toolCallId?: string) {
function hasToolResult(
thread: ReturnType<typeof useThread>["thread"],
toolCallId?: string,
) {
if (!toolCallId) {
return false;
}
@@ -144,11 +133,13 @@ export function useArtifactContent({
const liveWriteFileContent = useWriteFileStreaming
? loadArtifactContentFromToolCall({ url: filepath, thread })
: undefined;
const writeFileInFlight =
useWriteFileStreaming && isWriteFileInFlight(thread, filepath);
const displayedWriteFileContent = useThrottledWriteFileDisplay({
filepath,
liveContent: liveWriteFileContent,
enabled: streamPerfEnabled && useWriteFileStreaming,
streaming: thread.isLoading,
streaming: writeFileInFlight,
});
const content = useWriteFileStreaming ? displayedWriteFileContent : null;
const toolCallFallbackContent = useMemo(() => {
@@ -0,0 +1,65 @@
import assert from "node:assert/strict";
import test from "node:test";
import { isWriteFileInFlight, resolveWriteFilePath } from "./write-file-in-flight";
test("resolveWriteFilePath reads the virtual URL pathname", () => {
assert.equal(
resolveWriteFilePath("write-file:report.md?message_id=m1&tool_call_id=t1"),
"report.md",
);
assert.equal(resolveWriteFilePath("mnt/report.md"), "mnt/report.md");
});
test("isWriteFileInFlight is true only until that file's tool result arrives", () => {
const inFlight = {
messages: [
{
type: "ai",
tool_calls: [
{ id: "w1", name: "write_file", args: { path: "report.md" } },
],
},
],
};
assert.equal(isWriteFileInFlight(inFlight, "write-file:report.md?tool_call_id=w1"), true);
const completed = {
messages: [
{
type: "ai",
tool_calls: [
{ id: "w1", name: "write_file", args: { path: "report.md" } },
],
},
{ type: "tool", tool_call_id: "w1", name: "write_file" },
],
};
assert.equal(
isWriteFileInFlight(completed, "write-file:report.md?tool_call_id=w1"),
false,
);
});
test("a later thread turn does not treat a finished write as in-flight", () => {
const laterTurn = {
messages: [
{
type: "ai",
tool_calls: [
{ id: "w1", name: "write_file", args: { path: "report.md" } },
],
},
{ type: "tool", tool_call_id: "w1", name: "write_file" },
{ type: "human", content: "按方案 A" },
{
type: "ai",
tool_calls: [{ id: "c2", name: "web_search", args: { query: "x" } }],
},
],
};
assert.equal(
isWriteFileInFlight(laterTurn, "write-file:report.md?tool_call_id=w1"),
false,
);
});
@@ -0,0 +1,77 @@
import { isImageFile } from "../utils/files";
export function isWriteFileStreamingUrl(filepath: string): boolean {
if (!filepath.startsWith("write-file:")) {
return false;
}
try {
const url = new URL(filepath);
const path = decodeURIComponent(url.pathname);
return !isImageFile(path);
} catch {
return true;
}
}
export function resolveWriteFilePath(filepath: string): string {
if (filepath.startsWith("write-file:")) {
try {
return decodeURIComponent(new URL(filepath).pathname).trim();
} catch {
const rest = filepath.slice("write-file:".length);
return rest.split(/[?#]/, 1)[0]?.trim() ?? filepath;
}
}
return filepath.trim();
}
type WriteFileThreadLike = {
messages: Array<{
type?: string;
tool_call_id?: string;
tool_calls?: Array<{
id?: string;
name?: string;
args?: unknown;
}>;
}>;
};
function hasToolResult(thread: WriteFileThreadLike, toolCallId?: string) {
if (!toolCallId) {
return false;
}
return thread.messages.some(
(message) => message.type === "tool" && message.tool_call_id === toolCallId,
);
}
/** True only while THIS file's write_file call has not yet returned a tool result. */
export function isWriteFileInFlight(
thread: WriteFileThreadLike,
filepath: string,
): boolean {
const path = resolveWriteFilePath(filepath);
if (!path) {
return false;
}
for (const message of thread.messages) {
if (message.type !== "ai" || !message.tool_calls) {
continue;
}
for (const toolCall of message.tool_calls) {
if (toolCall.name !== "write_file") {
continue;
}
const args = toolCall.args as { path?: unknown } | undefined;
const callPath = typeof args?.path === "string" ? args.path.trim() : "";
if (callPath !== path) {
continue;
}
if (!hasToolResult(thread, toolCall.id)) {
return true;
}
}
}
return false;
}
@@ -0,0 +1,77 @@
import assert from "node:assert/strict";
import test from "node:test";
import type { Message } from "@langchain/langgraph-sdk";
import { groupMessages } from "./utils";
function groupTypes(messages: Message[]) {
const types: string[] = [];
groupMessages(messages, (group) => {
types.push(group.type);
return group.type;
});
return types;
}
test("write_file tool after 用户协助 opens a processing group instead of erroring", () => {
const types = groupTypes([
{ type: "human", id: "h1", content: "写一份报告" } as Message,
{
type: "ai",
id: "a1",
content: "",
tool_calls: [{ id: "c1", name: "ask_clarification", args: {} }],
} as Message,
{
type: "tool",
id: "t1",
name: "ask_clarification",
content: "请选择范围",
tool_call_id: "c1",
} as Message,
{ type: "human", id: "h2", content: "按方案 A" } as Message,
{
type: "tool",
id: "t2",
name: "write_file",
content: "OK",
tool_call_id: "w1",
} as Message,
{
type: "tool",
id: "t3",
name: "write_file",
content: "OK",
tool_call_id: "w2",
} as Message,
]);
assert.deepEqual(types, [
"human",
"assistant:processing",
"assistant:clarification",
"human",
"assistant:processing",
]);
});
test("tool messages still attach to an open processing group", () => {
const types = groupTypes([
{
type: "ai",
id: "a1",
content: "",
tool_calls: [{ id: "w1", name: "write_file", args: { path: "a.md" } }],
} as Message,
{
type: "tool",
id: "t1",
name: "write_file",
content: "OK",
tool_call_id: "w1",
} as Message,
]);
assert.deepEqual(types, ["assistant:processing"]);
});
+9 -4
View File
@@ -119,10 +119,15 @@ export function groupMessages<T>(
if (open) {
open.messages.push(message);
} else {
console.error(
"Unexpected tool message outside a processing group",
message,
);
// After 用户协助 the last group is human/clarification, so later
// write_file results have nowhere to attach. Opening a processing
// group keeps them renderable and avoids console.error(full message)
// on every MessageList render (that froze the SSE thread).
groups.push({
id: message.id,
type: "assistant:processing",
messages: [message],
});
}
}
continue;