修复泥点消耗刷新不及时 (#138)
Project CI / Repository checks (push) Successful in 50s
Project CI / Frontend tests (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled

refactor:
- 主站复用game agent的zustand store

fix:
- 修复旧请求覆盖问题
- 修复账号切换问题

- 主站原本已经每 4 秒轮询 external generation 任务状态, 在这里补充触发余额刷新的时机

---------

Co-authored-by: 段舒康 <kdletters@qq.com>
Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/138
Co-authored-by: 王德宇 <kvtodev@outlook.com>
Co-committed-by: 王德宇 <kvtodev@outlook.com>
This commit was merged in pull request #138.
This commit is contained in:
2026-08-08 10:25:02 +08:00
committed by 段舒康
parent 44de26d204
commit fc0dcb0782
36 changed files with 4160 additions and 491 deletions
@@ -26,6 +26,7 @@ const COMPLETED_TASK_LIST_LIMIT = 20;
const COMPLETED_TASK_LIST_MAX_LIMIT = 100;
const ACTIVE_TASK_LIST_LIMIT = 100;
const ACTIVE_TASK_POLL_INTERVAL_MS = 4000;
const TASK_LIST_BOOTSTRAP_RETRY_DELAYS_MS = [1000, 2000, 4000] as const;
type TaskSidebarTab = 'active' | 'completed';
@@ -49,6 +50,7 @@ type ImageCanvasTaskSidebarViewProps = {
onToggleOpen: () => void;
onFocusExternalTask: (task: ExternalGenerationTaskRecord) => void;
onExternalTasksCompleted?: (tasks: ExternalGenerationTaskRecord[]) => void;
onExternalTaskWalletMayHaveChanged?: () => void;
};
function parseTaskTimeMs(value?: string | null) {
@@ -280,6 +282,7 @@ export function ImageCanvasTaskSidebarView({
onToggleOpen,
onFocusExternalTask,
onExternalTasksCompleted,
onExternalTaskWalletMayHaveChanged,
}: ImageCanvasTaskSidebarViewProps) {
const { refreshCanvas } = useImageCanvasActions();
const projectId = useImageCanvasContextStore((state) => state.projectId);
@@ -289,6 +292,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,
@@ -353,54 +358,123 @@ export function ImageCanvasTaskSidebarView({
useEffect(() => {
let disposed = false;
const controller = new AbortController();
Promise.all([
listExternalGenerationTasks({
let controller: AbortController | null = null;
let retryTimerId: number | null = null;
let retryIndex = 0;
let hasLoadedActiveTasks = false;
let hasLoadedCompletedTasks = false;
const loadTaskLists = () => {
const attemptController = new AbortController();
controller = attemptController;
const activeTasksRequest = listExternalGenerationTasks({
limit: ACTIVE_TASK_LIST_LIMIT,
includeAcknowledgedTerminal: false,
statuses: ['running', 'queued'],
signal: controller.signal,
}),
listExternalGenerationTasks({
limit: completedListLimit,
includeAcknowledgedTerminal: true,
statuses: ['completed', 'failed'],
signal: controller.signal,
}),
])
.then(([activeResponse, completedResponse]) => {
if (disposed) {
signal: attemptController.signal,
}).then((activeResponse) => {
if (disposed || attemptController.signal.aborted) {
return;
}
const visibleActiveTasks = filterVisibleExternalTasks(
activeResponse.tasks,
);
const visibleCompletedTasks = filterVisibleExternalTasks(
completedResponse.tasks,
);
notifyCompletedExternalTasks(visibleCompletedTasks, {
unacknowledgedOnly: true,
});
setExternalTasks(
hasLoadedActiveTasks = true;
const activeTasks = activeResponse.tasks.filter(isActiveExternalTask);
const visibleActiveTasks = filterVisibleExternalTasks(activeTasks);
setWalletActiveExternalTaskIds(activeTasks.map((task) => task.jobId));
if (activeTasks.length > 0) {
onExternalTaskWalletMayHaveChanged?.();
}
setExternalTasks((currentTasks) =>
trimStoredExternalTasks(
mergeExternalTasks(visibleActiveTasks, visibleCompletedTasks),
mergeExternalTasks(
visibleActiveTasks,
currentTasks.filter(isTerminalExternalTask),
),
completedListLimit,
),
);
})
.catch(() => {
if (!disposed) {
setExternalTasks([]);
}
});
const completedTasksRequest = listExternalGenerationTasks({
limit: completedListLimit,
includeAcknowledgedTerminal: true,
statuses: ['completed', 'failed'],
signal: attemptController.signal,
}).then((completedResponse) => {
if (disposed || attemptController.signal.aborted) {
return;
}
hasLoadedCompletedTasks = true;
const visibleCompletedTasks = filterVisibleExternalTasks(
completedResponse.tasks,
);
if (
completedResponse.tasks.some(
(task) =>
isTerminalExternalTask(task) && !task.notificationAcknowledgedAt,
)
) {
onExternalTaskWalletMayHaveChanged?.();
}
notifyCompletedExternalTasks(visibleCompletedTasks, {
unacknowledgedOnly: true,
});
setExternalTasks((currentTasks) =>
trimStoredExternalTasks(
mergeExternalTasks(
currentTasks.filter(isActiveExternalTask),
visibleCompletedTasks,
),
completedListLimit,
),
);
});
Promise.all([activeTasksRequest, completedTasksRequest])
.catch(() => {
attemptController.abort();
if (controller === attemptController) {
controller = null;
}
if (disposed) {
return;
}
if (retryIndex >= TASK_LIST_BOOTSTRAP_RETRY_DELAYS_MS.length) {
setExternalTasks((currentTasks) =>
trimStoredExternalTasks(
currentTasks.filter(
(task) =>
(hasLoadedActiveTasks && isActiveExternalTask(task)) ||
(hasLoadedCompletedTasks && isTerminalExternalTask(task)),
),
completedListLimit,
),
);
if (!hasLoadedActiveTasks) {
setWalletActiveExternalTaskIds([]);
}
return;
}
const retryDelayMs = TASK_LIST_BOOTSTRAP_RETRY_DELAYS_MS[retryIndex];
retryIndex += 1;
retryTimerId = window.setTimeout(loadTaskLists, retryDelayMs);
})
.finally(() => {
if (controller === attemptController) {
controller = null;
}
});
};
loadTaskLists();
return () => {
disposed = true;
controller.abort();
controller?.abort();
if (retryTimerId !== null) {
window.clearTimeout(retryTimerId);
}
};
}, [
completedListLimit,
filterVisibleExternalTasks,
notifyCompletedExternalTasks,
onExternalTaskWalletMayHaveChanged,
refreshKey,
]);
@@ -412,13 +486,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 +523,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 +567,11 @@ export function ImageCanvasTaskSidebarView({
}
};
}, [
activeExternalTaskIds,
activeExternalTaskKey,
completedListLimit,
filterVisibleExternalTasks,
notifyCompletedExternalTasks,
onExternalTaskWalletMayHaveChanged,
walletActiveExternalTaskKey,
]);
const handleTaskListScroll = useCallback(