修复主站外部生成任务泥点刷新时机
Project CI / Repository checks (pull_request) Failing after 11s
Project CI / Backend tests (pull_request) Failing after 11s
Project CI / Frontend tests (pull_request) Successful in 2m56s
Project CI / Native shell tests (pull_request) Failing after 5m48s

复用主站任务轮询推动共享钱包余额收敛
覆盖运行中扣费与失败退款后的余额刷新
支持其他项目任务驱动账号级钱包更新
补充主站外部生成任务回归测试
同步更新项目基线与共享决策记录
This commit is contained in:
2026-08-05 19:25:21 +08:00
parent 02e4e18068
commit 1f3d3f1aa1
6 changed files with 179 additions and 14 deletions
@@ -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(