Merge branch 'master-nj' of http://192.168.0.200/knowledge-base-bj/deerflow-code into master-zl
This commit is contained in:
commit
1c0e4e9723
63 files changed
+4113
-929
No files matched your search
@@ -5,7 +5,11 @@ import { useThread } from "@/components/workspace/messages/context";
|
||||
|
||||
import { isImageFile } from "../utils/files";
|
||||
|
||||
import { loadArtifactContent, loadArtifactContentFromToolCall } from "./loader";
|
||||
import {
|
||||
loadArtifactContent,
|
||||
loadArtifactContentFromLatestWriteFile,
|
||||
loadArtifactContentFromToolCall,
|
||||
} from "./loader";
|
||||
import { isArtifactApiPath } from "./streaming-write-preview";
|
||||
import { updateArtifactContent } from "./api";
|
||||
|
||||
@@ -96,8 +100,14 @@ export function useArtifactContent({
|
||||
}
|
||||
return null;
|
||||
}, [filepath, useWriteFileStreaming, thread]);
|
||||
const toolCallFallbackContent = useMemo(() => {
|
||||
if (useWriteFileStreaming || !isArtifactApiPath(filepath)) {
|
||||
return undefined;
|
||||
}
|
||||
return loadArtifactContentFromLatestWriteFile({ filepath, thread });
|
||||
}, [filepath, thread, useWriteFileStreaming]);
|
||||
|
||||
const { data, isLoading, error } = useQuery({
|
||||
const { data, isLoading, error, refetch } = useQuery({
|
||||
queryKey: ["artifact", filepath, threadId, isMock, publicShareCode],
|
||||
queryFn: () => {
|
||||
return loadArtifactContent({ filepath, threadId, isMock, publicShareCode });
|
||||
@@ -121,11 +131,12 @@ export function useArtifactContent({
|
||||
return {
|
||||
content: useWriteFileStreaming
|
||||
? content
|
||||
: (pendingEdit?.content ?? data?.content),
|
||||
: (pendingEdit?.content ?? data?.content ?? toolCallFallbackContent),
|
||||
url: useWriteFileStreaming ? undefined : data?.url,
|
||||
isLoading,
|
||||
isApplyingEdit: pendingEdit?.kind === "editing",
|
||||
error,
|
||||
refetch,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ export async function loadArtifactContent({
|
||||
return { content: text, url };
|
||||
}
|
||||
|
||||
export function loadArtifactContentFromToolCall({
|
||||
export function loadArtifactContentFromToolCall({
|
||||
url: urlString,
|
||||
thread,
|
||||
}: {
|
||||
@@ -66,5 +66,58 @@ export function loadArtifactContentFromToolCall({
|
||||
return toolCall.args.content;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeArtifactPath(path: string): string {
|
||||
let normalized = path.trim();
|
||||
const first = normalized[0];
|
||||
const last = normalized.at(-1);
|
||||
if (
|
||||
normalized.length >= 2 &&
|
||||
((first === '"' && last === '"') ||
|
||||
(first === "'" && last === "'") ||
|
||||
(first === "`" && last === "`"))
|
||||
) {
|
||||
normalized = normalized.slice(1, -1).trim();
|
||||
}
|
||||
return normalized.replace(/\\/g, "/").replace(/^\/+/, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Last-resort preview for an output whose artifact HTTP request is unavailable.
|
||||
*
|
||||
* A completed `write_file` can be selected before its backing sandbox file is
|
||||
* visible through the artifacts endpoint (or when a deployment temporarily
|
||||
* serves a stale mount). The original tool arguments still contain the
|
||||
* Markdown, so use the newest matching write as a read-only display fallback.
|
||||
*/
|
||||
export function loadArtifactContentFromLatestWriteFile({
|
||||
filepath,
|
||||
thread,
|
||||
}: {
|
||||
filepath: string;
|
||||
thread: BaseStream<AgentThreadState>;
|
||||
}): string | undefined {
|
||||
const normalizedFilepath = normalizeArtifactPath(filepath);
|
||||
if (!normalizedFilepath) return undefined;
|
||||
|
||||
for (const message of [...thread.messages].reverse()) {
|
||||
if (message.type !== "ai" || !message.tool_calls) continue;
|
||||
for (const toolCall of [...message.tool_calls].reverse()) {
|
||||
if (toolCall.name !== "write_file") continue;
|
||||
if (!toolCall.args || typeof toolCall.args !== "object" || Array.isArray(toolCall.args)) {
|
||||
continue;
|
||||
}
|
||||
const args = toolCall.args as { path?: unknown; content?: unknown };
|
||||
if (
|
||||
typeof args.path === "string" &&
|
||||
typeof args.content === "string" &&
|
||||
normalizeArtifactPath(args.path) === normalizedFilepath
|
||||
) {
|
||||
return args.content;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -11,6 +11,8 @@ import { createClientId } from "@/lib/client-id";
|
||||
export type DocumentRewritePresentation = {
|
||||
id: string;
|
||||
threadId: string;
|
||||
/** A report regeneration is a new document, not an edit of the current one. */
|
||||
operation: "rewrite" | "generate";
|
||||
source: {
|
||||
kind: "artifact" | "deep_research_report";
|
||||
id: string;
|
||||
@@ -37,6 +39,10 @@ export type DocumentRewritePresentation = {
|
||||
originalContent: string;
|
||||
rewrittenContent: string | null;
|
||||
versionId: string | null;
|
||||
/** Hash of the currently committed rewrite, used by the local undo fallback. */
|
||||
committedHash: string | null;
|
||||
/** A browser-only undo backup is available because the server snapshot failed. */
|
||||
localFallbackAvailable: boolean;
|
||||
/** Inverse snapshot created by a safe undo; enables a one-click reapply. */
|
||||
redoVersionId: string | null;
|
||||
revertedAt: string | null;
|
||||
@@ -49,6 +55,7 @@ type CreatePresentationInput = Omit<
|
||||
DocumentRewritePresentation,
|
||||
| "id"
|
||||
| "stage"
|
||||
| "operation"
|
||||
| "generateImages"
|
||||
| "maxGeneratedImages"
|
||||
| "generatedImageCount"
|
||||
@@ -62,14 +69,19 @@ type CreatePresentationInput = Omit<
|
||||
| "originalContent"
|
||||
| "rewrittenContent"
|
||||
| "versionId"
|
||||
| "committedHash"
|
||||
| "localFallbackAvailable"
|
||||
| "redoVersionId"
|
||||
| "revertedAt"
|
||||
| "error"
|
||||
| "createdAt"
|
||||
| "updatedAt"
|
||||
> & {
|
||||
operation?: DocumentRewritePresentation["operation"];
|
||||
originalContent?: string;
|
||||
rewrittenContent?: string | null;
|
||||
committedHash?: string | null;
|
||||
localFallbackAvailable?: boolean;
|
||||
requirementsPlan?: string;
|
||||
generateImages?: boolean;
|
||||
maxGeneratedImages?: number;
|
||||
@@ -107,6 +119,7 @@ export function DocumentRewritePresentationProvider({
|
||||
...input,
|
||||
id,
|
||||
stage: "snapshot",
|
||||
operation: input.operation ?? "rewrite",
|
||||
generateImages: input.generateImages === true,
|
||||
maxGeneratedImages: Math.min(Math.max(input.maxGeneratedImages ?? 2, 1), 4),
|
||||
generatedImageCount: 0,
|
||||
@@ -120,6 +133,8 @@ export function DocumentRewritePresentationProvider({
|
||||
originalContent: input.originalContent ?? "",
|
||||
rewrittenContent: input.rewrittenContent ?? null,
|
||||
versionId: null,
|
||||
committedHash: input.committedHash ?? null,
|
||||
localFallbackAvailable: input.localFallbackAvailable === true,
|
||||
redoVersionId: null,
|
||||
revertedAt: null,
|
||||
error: null,
|
||||
@@ -180,3 +195,12 @@ export function useDocumentRewritePresentation() {
|
||||
|
||||
/** Browser event consumed by the open sandbox after an in-feed undo succeeds. */
|
||||
export const DOCUMENT_REWRITE_UNDONE_EVENT = "deerflow:document-rewrite-undone";
|
||||
|
||||
/** Synchronous guard used before a browser-only undo writes back the original text. */
|
||||
export const DOCUMENT_REWRITE_LOCAL_UNDO_GUARD_EVENT = "deerflow:document-rewrite-local-undo-guard";
|
||||
|
||||
/** Sent after the user confirms a rewrite so the conversation can reveal its new step card. */
|
||||
export const DOCUMENT_REWRITE_STARTED_EVENT = "deerflow:document-rewrite-started";
|
||||
|
||||
/** A completed structural-regeneration card requests an in-place rewrite of that new report. */
|
||||
export const DEEP_RESEARCH_REPORT_REWRITE_REQUESTED_EVENT = "deerflow:deep-research-report-rewrite-requested";
|
||||
@@ -0,0 +1,133 @@
|
||||
import { getAccountIdentityKey } from "@/core/auth";
|
||||
import { authStorage } from "@/core/auth/scoped-storage";
|
||||
|
||||
const STORAGE_PREFIX = "deerflow.document-rewrite.local-fallback:v1";
|
||||
// Keep the browser fallback comfortably below the typical 5 MB storage quota.
|
||||
const MAX_BACKUP_CHARACTERS = 1_000_000;
|
||||
|
||||
type PendingArtifactRewriteFallback = {
|
||||
threadId: string;
|
||||
path: string;
|
||||
originalContent: string;
|
||||
committedHash: string | null;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
export type ArtifactRewriteFallback = PendingArtifactRewriteFallback & {
|
||||
committedHash: string;
|
||||
};
|
||||
|
||||
function storageKey(threadId: string, path: string) {
|
||||
const account = getAccountIdentityKey() ?? "anonymous";
|
||||
return [STORAGE_PREFIX, account, threadId, path]
|
||||
.map((part) => encodeURIComponent(part))
|
||||
.join(":");
|
||||
}
|
||||
|
||||
function getStorage(): Storage | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
return authStorage();
|
||||
}
|
||||
|
||||
function canPersist(originalContent: string): boolean {
|
||||
if (originalContent.length <= MAX_BACKUP_CHARACTERS) return true;
|
||||
console.error("AI 全文改写的本地撤销备份过大,未写入浏览器存储。");
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the original text before the durable rewrite starts. It is completed
|
||||
* with the rewritten file hash only if the server-side undo snapshot fails.
|
||||
*/
|
||||
export function primeArtifactRewriteFallback({
|
||||
threadId,
|
||||
path,
|
||||
originalContent,
|
||||
}: Omit<PendingArtifactRewriteFallback, "committedHash" | "createdAt">): void {
|
||||
if (!canPersist(originalContent)) return;
|
||||
try {
|
||||
getStorage()?.setItem(
|
||||
storageKey(threadId, path),
|
||||
JSON.stringify({
|
||||
threadId,
|
||||
path,
|
||||
originalContent,
|
||||
committedHash: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
} satisfies PendingArtifactRewriteFallback),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("保存 AI 全文改写的本地撤销备份失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/** Mark a primed backup as safe to use for the server snapshot failure path. */
|
||||
export function completeArtifactRewriteFallback({
|
||||
threadId,
|
||||
path,
|
||||
originalContent,
|
||||
committedHash,
|
||||
}: Omit<ArtifactRewriteFallback, "createdAt">): boolean {
|
||||
if (!committedHash || !canPersist(originalContent)) return false;
|
||||
try {
|
||||
const storage = getStorage();
|
||||
if (!storage) return false;
|
||||
storage.setItem(
|
||||
storageKey(threadId, path),
|
||||
JSON.stringify({
|
||||
threadId,
|
||||
path,
|
||||
originalContent,
|
||||
committedHash,
|
||||
createdAt: new Date().toISOString(),
|
||||
} satisfies ArtifactRewriteFallback),
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("完成 AI 全文改写的本地撤销备份失败:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function loadArtifactRewriteFallback({
|
||||
threadId,
|
||||
path,
|
||||
committedHash,
|
||||
}: Pick<ArtifactRewriteFallback, "threadId" | "path" | "committedHash">): ArtifactRewriteFallback | null {
|
||||
try {
|
||||
const raw = getStorage()?.getItem(storageKey(threadId, path));
|
||||
if (!raw) return null;
|
||||
const parsed: unknown = JSON.parse(raw);
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
||||
const fallback = parsed as Partial<ArtifactRewriteFallback>;
|
||||
if (
|
||||
fallback.threadId !== threadId ||
|
||||
fallback.path !== path ||
|
||||
fallback.committedHash !== committedHash ||
|
||||
typeof fallback.originalContent !== "string"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return fallback as ArtifactRewriteFallback;
|
||||
} catch (error) {
|
||||
console.error("读取 AI 全文改写的本地撤销备份失败:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function clearArtifactRewriteFallback(threadId: string, path: string): void {
|
||||
try {
|
||||
getStorage()?.removeItem(storageKey(threadId, path));
|
||||
} catch (error) {
|
||||
console.error("清理 AI 全文改写的本地撤销备份失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function sha256DocumentContent(content: string): Promise<string | null> {
|
||||
if (!globalThis.crypto?.subtle) return null;
|
||||
const digest = await globalThis.crypto.subtle.digest(
|
||||
"SHA-256",
|
||||
new TextEncoder().encode(content),
|
||||
);
|
||||
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
@@ -118,10 +118,10 @@ export function groupMessages<T>(
|
||||
}
|
||||
|
||||
if (message.type === "ai") {
|
||||
// Deep-research interaction card: virtual ai messages injected by the
|
||||
// workbench (additional_kwargs.setup_research_approval) render through
|
||||
// the host-provided slot. Writing phases use ordinary virtual tool calls
|
||||
// and completed summaries remain normal assistant messages.
|
||||
// Deep-research interaction card: virtual ai messages injected by the
|
||||
// workbench (additional_kwargs.setup_research_approval) render through
|
||||
// the host-provided slot. Writing phases use ordinary virtual tool calls
|
||||
// and completed summaries remain normal assistant messages.
|
||||
if (hasResearchApproval(message)) {
|
||||
groups.push({
|
||||
id: message.id,
|
||||
@@ -136,6 +136,15 @@ export function groupMessages<T>(
|
||||
type: "assistant:present-files",
|
||||
messages: [message],
|
||||
});
|
||||
} else if (hasOnlyDeepResearchProgress(message)) {
|
||||
// Each deep-research writing bar is its own group. Merging consecutive
|
||||
// processing messages would pin a new report's in-progress steps onto
|
||||
// the parent bar, which sits above the original file card.
|
||||
groups.push({
|
||||
id: message.id,
|
||||
type: "assistant:processing",
|
||||
messages: [message],
|
||||
});
|
||||
} else if (hasSubagent(message)) {
|
||||
groups.push({
|
||||
id: message.id,
|
||||
@@ -380,6 +389,14 @@ export function hasPresentFiles(message: Message) {
|
||||
);
|
||||
}
|
||||
|
||||
export function hasOnlyDeepResearchProgress(message: Message) {
|
||||
return (
|
||||
message.type === "ai" &&
|
||||
Boolean(message.tool_calls?.length) &&
|
||||
message.tool_calls!.every((toolCall) => toolCall.name === "deep_research_progress")
|
||||
);
|
||||
}
|
||||
|
||||
export function isClarificationToolMessage(message: Message) {
|
||||
return message.type === "tool" && message.name === "ask_clarification";
|
||||
}
|
||||
|
||||
@@ -103,6 +103,16 @@ export async function deleteManagedModel(name: string): Promise<void> {
|
||||
if (!response.ok) return readError(response, "删除模型失败");
|
||||
}
|
||||
|
||||
export async function reorderManagedModels(names: string[]): Promise<ManagedModel[]> {
|
||||
const response = await apiFetch(`${getBackendBaseURL()}/api/admin/models/order`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ names }),
|
||||
});
|
||||
if (!response.ok) return readError(response, "调整模型顺序失败");
|
||||
return response.json() as Promise<ManagedModel[]>;
|
||||
}
|
||||
|
||||
export async function testManagedModel(model: ModelSavePayload): Promise<ModelConnectionTestResult> {
|
||||
const response = await apiFetch(`${getBackendBaseURL()}/api/admin/models/test`, {
|
||||
method: "POST",
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
createManagedModel,
|
||||
deleteManagedModel,
|
||||
loadManagedModels,
|
||||
reorderManagedModels,
|
||||
testManagedModel,
|
||||
updateManagedModel,
|
||||
type ModelSavePayload,
|
||||
@@ -50,3 +51,16 @@ export function useDeleteManagedModel() {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useReorderManagedModels() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: reorderManagedModels,
|
||||
onSuccess: async () => {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: MANAGED_MODELS_QUERY_KEY }),
|
||||
queryClient.invalidateQueries({ queryKey: ["models"] }),
|
||||
]);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -100,12 +100,12 @@ export const sidebarMenuItems: MenuItem[] = [
|
||||
icon: Telescope,
|
||||
path: "/page/strategy/qa/deep-research",
|
||||
},
|
||||
{
|
||||
id: "qa-enterprise-research",
|
||||
label: "企业深度研究",
|
||||
icon: FileSearch,
|
||||
path: "/page/strategy/qa/enterprise-research",
|
||||
},
|
||||
// {
|
||||
// id: "qa-enterprise-research",
|
||||
// label: "企业深度研究",
|
||||
// icon: FileSearch,
|
||||
// path: "/page/strategy/qa/enterprise-research",
|
||||
// },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -314,7 +314,7 @@ export interface CustomThemePreset {
|
||||
}
|
||||
export type ViewMode = "card" | "list";
|
||||
export type LayoutMode = "full" | "compact";
|
||||
export type CompactShortcutParamType = "login" | "theme";
|
||||
export type CompactShortcutParamType = "login" | "theme" | "path";
|
||||
|
||||
export interface CompactShortcutRouteParam {
|
||||
key: string;
|
||||
|
||||
Reference in new issue
Block a user