修复主站外部生成任务泥点刷新时机
复用主站任务轮询推动共享钱包余额收敛 覆盖运行中扣费与失败退款后的余额刷新 支持其他项目任务驱动账号级钱包更新 补充主站外部生成任务回归测试 同步更新项目基线与共享决策记录
This commit is contained in:
@@ -1463,13 +1463,8 @@ export function ImageCanvasEditorView({
|
||||
if (warning) {
|
||||
showGenerationWarning(warning);
|
||||
}
|
||||
refreshEditorWalletState();
|
||||
},
|
||||
[
|
||||
projectId,
|
||||
refreshEditorWalletState,
|
||||
showGenerationWarning,
|
||||
],
|
||||
[projectId, showGenerationWarning],
|
||||
);
|
||||
const effectiveIsAgentConversationOpen =
|
||||
isAgentConversationEnabled && isAgentConversationOpen;
|
||||
@@ -2374,6 +2369,7 @@ export function ImageCanvasEditorView({
|
||||
onActivateGenerationDialog: activateCanvasGenerationDialog,
|
||||
onFocusExternalTask: focusExternalGenerationTask,
|
||||
onExternalTasksCompleted: handleExternalGenerationTasksCompleted,
|
||||
onExternalTaskWalletMayHaveChanged: refreshEditorWalletState,
|
||||
onEditorAgentConfirmSent: handleEditorAgentConfirmSent,
|
||||
onToggleTaskSidebar: toggleTaskSidebar,
|
||||
onToggleAgentConversation: toggleAgentConversation,
|
||||
|
||||
@@ -134,6 +134,7 @@ export type ImageCanvasStageViewProps = {
|
||||
onActivateGenerationDialog: (dialog: CanvasGenerationDialogState) => void;
|
||||
onFocusExternalTask: (task: ExternalGenerationTaskRecord) => void;
|
||||
onExternalTasksCompleted?: (tasks: ExternalGenerationTaskRecord[]) => void;
|
||||
onExternalTaskWalletMayHaveChanged?: () => void;
|
||||
onEditorAgentConfirmSent?: () => void;
|
||||
onToggleTaskSidebar: () => void;
|
||||
onToggleAgentConversation: () => void;
|
||||
@@ -270,6 +271,7 @@ export function ImageCanvasStageView({
|
||||
onActivateGenerationDialog,
|
||||
onFocusExternalTask,
|
||||
onExternalTasksCompleted,
|
||||
onExternalTaskWalletMayHaveChanged,
|
||||
onEditorAgentConfirmSent,
|
||||
onToggleTaskSidebar,
|
||||
onToggleAgentConversation,
|
||||
@@ -397,7 +399,7 @@ export function ImageCanvasStageView({
|
||||
selectedToolbarStyle={selectedToolbarStyle}
|
||||
isSplittingIconSpritesheet={Boolean(
|
||||
selectedLayer &&
|
||||
splittingIconSpritesheetLayerIds.has(selectedLayer.id),
|
||||
splittingIconSpritesheetLayerIds.has(selectedLayer.id),
|
||||
)}
|
||||
isPersistingAssetKind={Boolean(
|
||||
selectedLayer && persistingAssetKindLayerIds.has(selectedLayer.id),
|
||||
@@ -502,6 +504,7 @@ export function ImageCanvasStageView({
|
||||
onToggleOpen={onToggleTaskSidebar}
|
||||
onFocusExternalTask={onFocusExternalTask}
|
||||
onExternalTasksCompleted={onExternalTasksCompleted}
|
||||
onExternalTaskWalletMayHaveChanged={onExternalTaskWalletMayHaveChanged}
|
||||
/>
|
||||
|
||||
{isAgentConversationEnabled ? (
|
||||
|
||||
@@ -474,6 +474,147 @@ describe('ImageCanvasTaskSidebarView', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('refreshes the wallet while an external task is running and after a failed refund settles', async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const onExternalTasksCompleted = vi.fn();
|
||||
const onExternalTaskWalletMayHaveChanged = vi.fn();
|
||||
let activeRequestCount = 0;
|
||||
const runningTask = createExternalTask({
|
||||
jobId: 'wallet-running-task',
|
||||
requestLabel: '长耗时图片生成',
|
||||
status: 'running',
|
||||
priceMudPoints: 20,
|
||||
});
|
||||
const failedTask = createExternalTask({
|
||||
...runningTask,
|
||||
status: 'failed',
|
||||
progress: 0,
|
||||
phaseDetail: '生成失败。',
|
||||
error: '生成失败,泥点已退回。',
|
||||
completedAt: new Date().toISOString(),
|
||||
});
|
||||
listExternalGenerationTasksMock.mockImplementation(
|
||||
(options: Parameters<typeof listExternalGenerationTasks>[0] = {}) => {
|
||||
if (options.statuses?.includes('running')) {
|
||||
activeRequestCount += 1;
|
||||
return Promise.resolve({
|
||||
overview: {
|
||||
pendingCount: 0,
|
||||
runningCount: activeRequestCount === 1 ? 1 : 0,
|
||||
unacknowledgedTerminalCount: activeRequestCount === 1 ? 0 : 1,
|
||||
updatedAtMicros: activeRequestCount,
|
||||
},
|
||||
tasks: activeRequestCount === 1 ? [runningTask] : [],
|
||||
});
|
||||
}
|
||||
return Promise.resolve({
|
||||
overview: {
|
||||
pendingCount: 0,
|
||||
runningCount: 0,
|
||||
unacknowledgedTerminalCount: activeRequestCount > 1 ? 1 : 0,
|
||||
updatedAtMicros: activeRequestCount,
|
||||
},
|
||||
tasks: activeRequestCount > 1 ? [failedTask] : [],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
render(
|
||||
<ImageCanvasTaskSidebarView
|
||||
open
|
||||
onToggleOpen={vi.fn()}
|
||||
onFocusExternalTask={vi.fn()}
|
||||
onExternalTasksCompleted={onExternalTasksCompleted}
|
||||
onExternalTaskWalletMayHaveChanged={
|
||||
onExternalTaskWalletMayHaveChanged
|
||||
}
|
||||
/>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
expect(onExternalTaskWalletMayHaveChanged).toHaveBeenCalledTimes(1);
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(4000);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(onExternalTaskWalletMayHaveChanged).toHaveBeenCalledTimes(2);
|
||||
expect(onExternalTasksCompleted).not.toHaveBeenCalled();
|
||||
expect(refreshCanvasMock).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps wallet polling global when the active external task belongs to another project', async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const onExternalTaskWalletMayHaveChanged = vi.fn();
|
||||
let activeRequestCount = 0;
|
||||
const otherProjectTask = createExternalTask({
|
||||
jobId: 'other-project-wallet-task',
|
||||
sourceEntityId: 'project-other',
|
||||
status: 'running',
|
||||
});
|
||||
listExternalGenerationTasksMock.mockImplementation(
|
||||
(options: Parameters<typeof listExternalGenerationTasks>[0] = {}) => {
|
||||
if (options.statuses?.includes('running')) {
|
||||
activeRequestCount += 1;
|
||||
return Promise.resolve({
|
||||
overview: {
|
||||
pendingCount: 0,
|
||||
runningCount: activeRequestCount === 1 ? 1 : 0,
|
||||
unacknowledgedTerminalCount: 0,
|
||||
updatedAtMicros: activeRequestCount,
|
||||
},
|
||||
tasks: activeRequestCount === 1 ? [otherProjectTask] : [],
|
||||
});
|
||||
}
|
||||
return Promise.resolve({
|
||||
overview: {
|
||||
pendingCount: 0,
|
||||
runningCount: 0,
|
||||
unacknowledgedTerminalCount: 0,
|
||||
updatedAtMicros: activeRequestCount,
|
||||
},
|
||||
tasks: [],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
render(
|
||||
<ImageCanvasTaskSidebarView
|
||||
open
|
||||
onToggleOpen={vi.fn()}
|
||||
onFocusExternalTask={vi.fn()}
|
||||
onExternalTaskWalletMayHaveChanged={
|
||||
onExternalTaskWalletMayHaveChanged
|
||||
}
|
||||
/>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
expect(screen.queryByText('发光猫咪主视觉')).toBeNull();
|
||||
expect(onExternalTaskWalletMayHaveChanged).toHaveBeenCalledTimes(1);
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(4000);
|
||||
await Promise.resolve();
|
||||
});
|
||||
expect(onExternalTaskWalletMayHaveChanged).toHaveBeenCalledTimes(2);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it('loads more completed tasks while scrolling and keeps the request capped', async () => {
|
||||
const completedTasks = Array.from({ length: 120 }, (_, index) => {
|
||||
const completedAt = new Date(Date.now() - index * 1000).toISOString();
|
||||
|
||||
@@ -49,6 +49,7 @@ type ImageCanvasTaskSidebarViewProps = {
|
||||
onToggleOpen: () => void;
|
||||
onFocusExternalTask: (task: ExternalGenerationTaskRecord) => void;
|
||||
onExternalTasksCompleted?: (tasks: ExternalGenerationTaskRecord[]) => void;
|
||||
onExternalTaskWalletMayHaveChanged?: () => void;
|
||||
};
|
||||
|
||||
function parseTaskTimeMs(value?: string | null) {
|
||||
@@ -280,6 +281,7 @@ export function ImageCanvasTaskSidebarView({
|
||||
onToggleOpen,
|
||||
onFocusExternalTask,
|
||||
onExternalTasksCompleted,
|
||||
onExternalTaskWalletMayHaveChanged,
|
||||
}: ImageCanvasTaskSidebarViewProps) {
|
||||
const { refreshCanvas } = useImageCanvasActions();
|
||||
const projectId = useImageCanvasContextStore((state) => state.projectId);
|
||||
@@ -289,6 +291,8 @@ export function ImageCanvasTaskSidebarView({
|
||||
const [externalTasks, setExternalTasks] = useState<
|
||||
ExternalGenerationTaskRecord[]
|
||||
>([]);
|
||||
const [walletActiveExternalTaskIds, setWalletActiveExternalTaskIds] =
|
||||
useState<string[]>([]);
|
||||
const [completedListState, setCompletedListState] = useState(() => ({
|
||||
limit: COMPLETED_TASK_LIST_LIMIT,
|
||||
projectId: normalizedProjectId,
|
||||
@@ -372,12 +376,21 @@ export function ImageCanvasTaskSidebarView({
|
||||
if (disposed) {
|
||||
return;
|
||||
}
|
||||
const visibleActiveTasks = filterVisibleExternalTasks(
|
||||
activeResponse.tasks,
|
||||
);
|
||||
const activeTasks = activeResponse.tasks.filter(isActiveExternalTask);
|
||||
const visibleActiveTasks = filterVisibleExternalTasks(activeTasks);
|
||||
const visibleCompletedTasks = filterVisibleExternalTasks(
|
||||
completedResponse.tasks,
|
||||
);
|
||||
setWalletActiveExternalTaskIds(activeTasks.map((task) => task.jobId));
|
||||
if (
|
||||
activeTasks.length > 0 ||
|
||||
completedResponse.tasks.some(
|
||||
(task) =>
|
||||
isTerminalExternalTask(task) && !task.notificationAcknowledgedAt,
|
||||
)
|
||||
) {
|
||||
onExternalTaskWalletMayHaveChanged?.();
|
||||
}
|
||||
notifyCompletedExternalTasks(visibleCompletedTasks, {
|
||||
unacknowledgedOnly: true,
|
||||
});
|
||||
@@ -401,6 +414,7 @@ export function ImageCanvasTaskSidebarView({
|
||||
completedListLimit,
|
||||
filterVisibleExternalTasks,
|
||||
notifyCompletedExternalTasks,
|
||||
onExternalTaskWalletMayHaveChanged,
|
||||
refreshKey,
|
||||
]);
|
||||
|
||||
@@ -412,13 +426,14 @@ export function ImageCanvasTaskSidebarView({
|
||||
[externalTasks],
|
||||
);
|
||||
const activeExternalTaskKey = activeExternalTaskIds.join('|');
|
||||
const walletActiveExternalTaskKey = walletActiveExternalTaskIds.join('|');
|
||||
|
||||
useEffect(() => {
|
||||
activeExternalTaskIdsRef.current = new Set(activeExternalTaskIds);
|
||||
}, [activeExternalTaskKey, activeExternalTaskIds]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeExternalTaskIds.length) {
|
||||
if (!walletActiveExternalTaskKey) {
|
||||
return undefined;
|
||||
}
|
||||
let disposed = false;
|
||||
@@ -448,6 +463,15 @@ export function ImageCanvasTaskSidebarView({
|
||||
filterVisibleExternalTasks(activeResponse.tasks),
|
||||
filterVisibleExternalTasks(completedResponse.tasks),
|
||||
);
|
||||
setWalletActiveExternalTaskIds(
|
||||
activeResponse.tasks
|
||||
.filter(isActiveExternalTask)
|
||||
.map((task) => task.jobId),
|
||||
);
|
||||
// external job 在 worker 领取后才预扣泥点,running 可能先于扣费落账。
|
||||
// 只要仍有全局 active task,每轮成功轮询都推动钱包收敛;终态这一轮
|
||||
// 同时覆盖成功结算或失败退款。共享 Store 会合并并发刷新。
|
||||
onExternalTaskWalletMayHaveChanged?.();
|
||||
notifyCompletedExternalTasks(refreshedTasks, {
|
||||
activeTaskIds: activeExternalTaskIdsRef.current,
|
||||
});
|
||||
@@ -483,11 +507,11 @@ export function ImageCanvasTaskSidebarView({
|
||||
}
|
||||
};
|
||||
}, [
|
||||
activeExternalTaskIds,
|
||||
activeExternalTaskKey,
|
||||
completedListLimit,
|
||||
filterVisibleExternalTasks,
|
||||
notifyCompletedExternalTasks,
|
||||
onExternalTaskWalletMayHaveChanged,
|
||||
walletActiveExternalTaskKey,
|
||||
]);
|
||||
|
||||
const handleTaskListScroll = useCallback(
|
||||
|
||||
Reference in New Issue
Block a user