Files
tutti/packages/agent/gui/agent-gui/agentGuiNode/controller/AgentGUIConversationRailTargetedPageRefresher.ts
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

187 lines
5.8 KiB
TypeScript

import type { AgentConversationRailRuntimePort } from "../../../agentConversationRailContracts";
import type { AgentGuiScheduler } from "../agentGuiScheduler";
import type { ConversationRailRefreshedPage } from "./agentGuiConversationRailQueryCache";
import {
conversationRailRetryMode,
isConversationRailAbortError,
requestConversationRailWithRetry,
type ConversationRailRetryMode
} from "./agentGuiConversationRailRequestRetry";
type TargetedPageRuntime = Pick<
AgentConversationRailRuntimePort,
"listPinnedSessionsPage" | "listSessionSectionPage"
>;
export class AgentGUIConversationRailTargetedPageRefresher {
private abortController: AbortController | null = null;
private readonly pendingPageIds = new Set<string>();
private requestSequence = 0;
constructor(
private readonly input: {
onFailed?(): void;
onResolved(pages: readonly ConversationRailRefreshedPage[]): void;
onRetryScheduled?(mode: ConversationRailRetryMode): void;
pageSize: number;
runtime: TargetedPageRuntime;
scheduler: AgentGuiScheduler;
workspaceId: string;
}
) {}
refresh(input: { agentTargetId: string; pageIds: readonly string[] }): void {
for (const pageId of input.pageIds) this.pendingPageIds.add(pageId);
if (this.pendingPageIds.size === 0) return;
this.abortController?.abort();
const abortController = new AbortController();
this.abortController = abortController;
const requestSequence = ++this.requestSequence;
const pageIds = [...this.pendingPageIds];
const createRequests = (
signal: AbortSignal
): Promise<ConversationRailRefreshedPage>[] =>
pageIds.flatMap((id) => {
if (id === "pinned") {
const listPage = this.input.runtime.listPinnedSessionsPage;
return listPage
? [
listPage({
agentTargetId: input.agentTargetId || undefined,
limit: this.input.pageSize,
signal,
workspaceId: this.input.workspaceId
}).then(
(page): ConversationRailRefreshedPage => ({
kind: "pinned",
page
})
)
]
: [];
}
const listPage = this.input.runtime.listSessionSectionPage;
return listPage
? [
listPage({
agentTargetId: input.agentTargetId || undefined,
limit: this.input.pageSize,
sectionKey: id,
signal,
workspaceId: this.input.workspaceId
}).then(
(page): ConversationRailRefreshedPage => ({
id,
kind: "section",
page
})
)
]
: [];
});
const hasAllPageReaders = pageIds.every((id) =>
id === "pinned"
? Boolean(this.input.runtime.listPinnedSessionsPage)
: Boolean(this.input.runtime.listSessionSectionPage)
);
if (!hasAllPageReaders) {
for (const id of pageIds) this.pendingPageIds.delete(id);
this.input.onFailed?.();
return;
}
void requestConversationRailWithRetry({
onRetryScheduled: ({ mode }) => this.input.onRetryScheduled?.(mode),
request: () =>
requestTargetedPages({
createRequests,
signal: abortController.signal
}),
retryKey: JSON.stringify([
this.input.workspaceId,
input.agentTargetId,
pageIds
]),
scheduler: this.input.scheduler,
signal: abortController.signal
})
.then((pages) => {
if (
abortController.signal.aborted ||
requestSequence !== this.requestSequence
) {
return;
}
for (const id of pageIds) this.pendingPageIds.delete(id);
this.input.onResolved(pages);
})
.catch(() => {
if (
abortController.signal.aborted ||
requestSequence !== this.requestSequence
) {
return;
}
for (const id of pageIds) this.pendingPageIds.delete(id);
this.input.onFailed?.();
});
}
cancel(): void {
this.requestSequence += 1;
this.abortController?.abort();
this.abortController = null;
this.pendingPageIds.clear();
}
}
async function requestTargetedPages(input: {
createRequests(signal: AbortSignal): Promise<ConversationRailRefreshedPage>[];
signal: AbortSignal;
}): Promise<ConversationRailRefreshedPage[]> {
const attemptAbortController = new AbortController();
const abortFromParent = (): void => {
attemptAbortController.abort(input.signal.reason);
};
input.signal.addEventListener("abort", abortFromParent, { once: true });
if (input.signal.aborted) abortFromParent();
const failures: unknown[] = [];
try {
const requests = input
.createRequests(attemptAbortController.signal)
.map((request) =>
request.catch((error: unknown) => {
failures.push(error);
attemptAbortController.abort(error);
throw error;
})
);
try {
return await Promise.all(requests);
} catch (error) {
attemptAbortController.abort(error);
await Promise.resolve();
throw preferredTargetedPageFailure(failures, error);
}
} finally {
input.signal.removeEventListener("abort", abortFromParent);
}
}
function preferredTargetedPageFailure(
failures: readonly unknown[],
fallback: unknown
): unknown {
const eligible = failures.filter(
(error) => !isConversationRailAbortError(error)
);
return (
eligible.find((error) => conversationRailRetryMode(error) === null) ??
eligible.find(
(error) => conversationRailRetryMode(error) === "background"
) ??
eligible[0] ??
fallback
);
}