merge: 合并 master,保留拖拽上传样式和工具面板样式
This commit is contained in:
@@ -64,6 +64,12 @@ import {
|
||||
import { renderMarkdownBlocks } from "./markdownRenderer";
|
||||
import { downloadResultAsset } from "./workbenchDownload";
|
||||
import { translateTaskError } from "../../utils/translateTaskError";
|
||||
import {
|
||||
buildLocalTimeoutMessage,
|
||||
formatTextTokenUsage,
|
||||
getTaskTimeoutPolicy,
|
||||
isTaskLocallyTimedOut,
|
||||
} from "../../utils/taskLifecycle";
|
||||
import { detectMentionTrigger } from "../../utils/mentionTrigger";
|
||||
import {
|
||||
isHappyHorseModel,
|
||||
@@ -106,6 +112,7 @@ import {
|
||||
VIDEO_MODEL_OPTIONS,
|
||||
RATIO_OPTIONS,
|
||||
GRID_MODE_OPTIONS,
|
||||
GRID_SUPPORTED_MODELS,
|
||||
VIDEO_FRAME_OPTIONS,
|
||||
VIDEO_DURATION_OPTIONS,
|
||||
MESSAGE_STORAGE_KEY,
|
||||
@@ -872,6 +879,9 @@ function WorkbenchPage({
|
||||
|
||||
let lastKnownProgress = Math.max(0, Number(task.progress || 0));
|
||||
let taskPollFailures = 0;
|
||||
let lastProgressAt = task.startedAt || Date.now();
|
||||
const taskKind = task.mode === "image" ? "image" : "video";
|
||||
const timeoutPolicy = getTaskTimeoutPolicy({ kind: taskKind, model: task.modelLabel, operation: task.operation });
|
||||
const abortController = new AbortController();
|
||||
taskAbortControllersRef.current.set(task.taskId, abortController);
|
||||
if (activeConversationIdRef.current === task.conversationId) {
|
||||
@@ -918,6 +928,9 @@ function WorkbenchPage({
|
||||
const progress = status.status === "completed"
|
||||
? 100
|
||||
: Math.min(99, Math.max(10, lastKnownProgress, currentMessageProgress, Math.round(baseProgress)));
|
||||
if (progress > lastKnownProgress || status.status === "completed") {
|
||||
lastProgressAt = Date.now();
|
||||
}
|
||||
lastKnownProgress = Math.max(lastKnownProgress, progress);
|
||||
const isSuperResolveTask = task.operation === "video-super-resolution";
|
||||
const statusLabel =
|
||||
@@ -942,6 +955,28 @@ function WorkbenchPage({
|
||||
setGenerationProgress(progress);
|
||||
}
|
||||
|
||||
const localTimeoutReason = status.status !== "completed" && status.status !== "failed" && status.status !== "cancelled"
|
||||
? isTaskLocallyTimedOut({
|
||||
startedAt: task.startedAt || Date.now(),
|
||||
lastProgressAt,
|
||||
progress,
|
||||
policy: timeoutPolicy,
|
||||
})
|
||||
: null;
|
||||
if (localTimeoutReason) {
|
||||
await patchConversationMessage(task.conversationId, task.assistantMessageId, {
|
||||
body: buildLocalTimeoutMessage(taskKind),
|
||||
status: "local_timeout",
|
||||
taskLifecycleStatus: "local_timeout",
|
||||
taskRefundStatus: "unknown",
|
||||
taskProgress: progress,
|
||||
taskStatusLabel: "本地等待超时",
|
||||
});
|
||||
removeKeepaliveTask(task.taskId);
|
||||
onRefreshUsage?.();
|
||||
return;
|
||||
}
|
||||
|
||||
if (status.status === "completed" && status.resultUrl) {
|
||||
const completedPatch: Partial<ChatMessage> = {
|
||||
body: isSuperResolveTask
|
||||
@@ -1989,6 +2024,7 @@ function WorkbenchPage({
|
||||
runKeepalivePoll(keepaliveTask);
|
||||
} else {
|
||||
let streamedText = "";
|
||||
let chatUsage: ChatMessage["taskUsage"] | undefined;
|
||||
setGenerationProgress(36);
|
||||
setGenerationStatus("正在回复");
|
||||
updateAssistantMessage(assistantMessageId, {
|
||||
@@ -2021,6 +2057,9 @@ function WorkbenchPage({
|
||||
});
|
||||
},
|
||||
abortController.signal,
|
||||
(usage) => {
|
||||
chatUsage = usage;
|
||||
},
|
||||
);
|
||||
|
||||
if (abortController.signal.aborted) return;
|
||||
@@ -2029,6 +2068,7 @@ function WorkbenchPage({
|
||||
const completedMessages = updateAssistantMessage(assistantMessageId, {
|
||||
body: streamedText.trim() || "收到。你可以继续补充目标,我会顺着当前上下文往下拆。",
|
||||
status: "completed",
|
||||
taskUsage: chatUsage,
|
||||
});
|
||||
if (!conversationId) {
|
||||
const conv = await conversationClient.create(
|
||||
@@ -2156,6 +2196,38 @@ function WorkbenchPage({
|
||||
}
|
||||
};
|
||||
|
||||
const handleReleaseStuckTask = (message: ChatMessage) => {
|
||||
if (message.taskId) {
|
||||
taskAbortControllersRef.current.get(message.taskId)?.abort();
|
||||
taskAbortControllersRef.current.delete(message.taskId);
|
||||
removeKeepaliveTask(message.taskId);
|
||||
}
|
||||
if (message.conversationId) {
|
||||
void patchConversationMessage(message.conversationId, message.id, {
|
||||
body: buildLocalTimeoutMessage(message.mode === "image" ? "image" : "video"),
|
||||
status: "local_timeout",
|
||||
taskLifecycleStatus: "local_timeout",
|
||||
taskRefundStatus: message.taskRefundStatus || "unknown",
|
||||
taskStatusLabel: "本地占用已释放",
|
||||
});
|
||||
}
|
||||
setMessages((current) =>
|
||||
current.map((item) =>
|
||||
item.id === message.id
|
||||
? {
|
||||
...item,
|
||||
body: buildLocalTimeoutMessage(item.mode === "image" ? "image" : "video"),
|
||||
status: "local_timeout",
|
||||
taskLifecycleStatus: "local_timeout",
|
||||
taskRefundStatus: item.taskRefundStatus || "unknown",
|
||||
taskStatusLabel: "本地占用已释放",
|
||||
}
|
||||
: item,
|
||||
),
|
||||
);
|
||||
syncActiveGenerationUi();
|
||||
};
|
||||
|
||||
const handleSuperResolveVideo = async (message: ChatMessage) => {
|
||||
if (!message.resultUrl || message.resultType !== "video") {
|
||||
setProjectError("仅支持对视频结果进行超分");
|
||||
@@ -2705,7 +2777,7 @@ function WorkbenchPage({
|
||||
isOpen={toolbarMenuId === "image-model"}
|
||||
onToggle={() => toggleToolbarMenu("image-model")}
|
||||
onClose={closeToolbarMenus}
|
||||
onChange={setImageModel}
|
||||
onChange={(v) => { setImageModel(v); if (!GRID_SUPPORTED_MODELS.has(v)) setImageGridMode("single"); }}
|
||||
direction={dropdownDirection}
|
||||
/>
|
||||
<CompoundSelectChip
|
||||
@@ -2717,6 +2789,7 @@ function WorkbenchPage({
|
||||
onToggle={() => toggleToolbarMenu("image-settings")}
|
||||
direction={dropdownDirection}
|
||||
/>
|
||||
{GRID_SUPPORTED_MODELS.has(imageModel) && (
|
||||
<SelectChip
|
||||
chipId="image-grid-mode"
|
||||
value={imageGridMode}
|
||||
@@ -2728,6 +2801,7 @@ function WorkbenchPage({
|
||||
onChange={setImageGridMode}
|
||||
direction={dropdownDirection}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{activeMode === "video" && (
|
||||
@@ -3054,7 +3128,7 @@ function WorkbenchPage({
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{message.status === "failed" && message.role === "assistant" && (message.mode === "image" || message.mode === "video") && (
|
||||
{(message.status === "failed" || message.status === "local_timeout") && message.role === "assistant" && (message.mode === "image" || message.mode === "video") && (
|
||||
<div className="ai-chat-failed-actions">
|
||||
<button type="button" className="ai-chat-failed-actions__retry" onClick={() => handleRegenerate(message)}>
|
||||
<ReloadOutlined /> 重试
|
||||
@@ -3062,9 +3136,12 @@ function WorkbenchPage({
|
||||
<button type="button" className="ai-chat-failed-actions__switch" onClick={() => { setToolbarMenuId(message.mode === "video" ? "video-model" : "image-model"); scrollMessagesSurface("bottom"); }}>
|
||||
<AppstoreOutlined /> 切换模型
|
||||
</button>
|
||||
<button type="button" className="ai-chat-failed-actions__release" onClick={() => handleReleaseStuckTask(message)}>
|
||||
<StopOutlined /> 释放卡住任务
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{message.status === "thinking" && !message.resultUrl && (message.mode === "image" || message.mode === "video") && (
|
||||
{(message.status === "thinking" || message.status === "stopping") && !message.resultUrl && (message.mode === "image" || message.mode === "video") && (
|
||||
<GenerationPendingCard message={message} onStop={() => handleStopSingleTask(message.id)} />
|
||||
)}
|
||||
{message.status === "thinking" && message.mode === "chat" && (
|
||||
@@ -3072,6 +3149,11 @@ function WorkbenchPage({
|
||||
<span>{message.taskStatusLabel || generationStatus}</span>
|
||||
</div>
|
||||
)}
|
||||
{message.role === "assistant" && message.mode === "chat" && message.status === "completed" && (
|
||||
<div className="ai-chat-task-billing-note">
|
||||
{formatTextTokenUsage(message.taskUsage)}
|
||||
</div>
|
||||
)}
|
||||
{(message.resultUrl || (message.result && message.status !== "thinking")) && (
|
||||
<ResultCard
|
||||
message={message}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { GenerationLifecycleStatus, TaskRefundStatus, TextTokenUsage } from "../../utils/taskLifecycle";
|
||||
|
||||
export type WorkbenchMode = "chat" | "image" | "video";
|
||||
|
||||
export interface WorkbenchChatAttachment {
|
||||
@@ -16,7 +18,10 @@ export interface WorkbenchChatMessage {
|
||||
body: string;
|
||||
prompt?: string;
|
||||
createdAt: string;
|
||||
status?: "thinking" | "queued" | "completed" | "failed";
|
||||
status?: "thinking" | "queued" | "completed" | "failed" | "stopping" | "local_timeout";
|
||||
taskLifecycleStatus?: GenerationLifecycleStatus;
|
||||
taskRefundStatus?: TaskRefundStatus;
|
||||
taskUsage?: TextTokenUsage;
|
||||
taskId?: string;
|
||||
conversationId?: number;
|
||||
taskProgress?: number;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { isServerRequestError } from "../../api/serverConnection";
|
||||
import { ENTERPRISE_VIDEO_MODEL_OPTIONS } from "../../utils/enterpriseVideoPolicy";
|
||||
import type { WebGenerationPreviewTask } from "../../types";
|
||||
import type { GenerationLifecycleStatus, TaskRefundStatus, TextTokenUsage } from "../../utils/taskLifecycle";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export type WorkbenchMode = "chat" | "image" | "video";
|
||||
@@ -74,7 +75,10 @@ export interface ChatMessage {
|
||||
body: string;
|
||||
prompt?: string;
|
||||
createdAt: string;
|
||||
status?: "thinking" | "queued" | "completed" | "failed";
|
||||
status?: "thinking" | "queued" | "completed" | "failed" | "stopping" | "local_timeout";
|
||||
taskLifecycleStatus?: GenerationLifecycleStatus;
|
||||
taskRefundStatus?: TaskRefundStatus;
|
||||
taskUsage?: TextTokenUsage;
|
||||
taskId?: string;
|
||||
conversationId?: number;
|
||||
taskProgress?: number;
|
||||
@@ -253,6 +257,13 @@ export const GRID_MODE_OPTIONS: WorkbenchOption[] = [
|
||||
{ value: "grid-25", label: "25 宫格" },
|
||||
];
|
||||
|
||||
export const GRID_SUPPORTED_MODELS = new Set([
|
||||
"wan2.7-image-pro",
|
||||
"wan2.7-image",
|
||||
"gpt-image-2",
|
||||
"gpt-image-2-vip",
|
||||
]);
|
||||
|
||||
export const VIDEO_FRAME_OPTIONS: WorkbenchOption[] = [
|
||||
{ value: "omni", label: "全能参考" },
|
||||
{ value: "start-end", label: "首尾帧" },
|
||||
@@ -386,11 +397,16 @@ export function shouldPersistPatch(patch: Partial<ChatMessage>): boolean {
|
||||
return (
|
||||
patch.status === "completed" ||
|
||||
patch.status === "failed" ||
|
||||
patch.status === "local_timeout" ||
|
||||
patch.status === "stopping" ||
|
||||
typeof patch.taskId === "string" ||
|
||||
typeof patch.resultUrl === "string" ||
|
||||
typeof patch.resultOssKey === "string" ||
|
||||
typeof patch.resultOriginalUrl === "string" ||
|
||||
typeof patch.resultMimeType === "string"
|
||||
typeof patch.resultMimeType === "string" ||
|
||||
typeof patch.taskRefundStatus === "string" ||
|
||||
typeof patch.taskLifecycleStatus === "string" ||
|
||||
typeof patch.taskUsage === "object"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -421,4 +437,4 @@ export function buildAssistantResult(
|
||||
summary: "我会把当前输入整理成脚本、分镜、素材需求和可复制的工作流节点。",
|
||||
specs: [model, ...specs],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user