Files
Genarrative/src/services/creation-agent/creationAgentClientFactory.ts
2026-05-11 16:15:48 +08:00

169 lines
4.1 KiB
TypeScript

import { parseApiErrorMessage } from '../../../packages/shared/src/http';
import type { TextStreamOptions } from '../aiTypes';
import {
type ApiRetryOptions,
fetchWithApiAuth,
requestJson,
} from '../apiClient';
import { readCreationAgentSessionFromSse } from './creationAgentSse';
type CreationAgentClientMessages = {
createSession: string;
getSession: string;
sendMessage: string;
streamIncomplete: string;
executeAction: string;
};
type CreationAgentClientOptions = {
apiBase: string;
messages: CreationAgentClientMessages;
createSessionTimeoutMs?: number;
executeActionTimeoutMs?: number;
readRetry?: ApiRetryOptions;
writeRetry?: ApiRetryOptions;
};
const DEFAULT_CREATION_AGENT_READ_RETRY: ApiRetryOptions = {
maxRetries: 1,
baseDelayMs: 180,
maxDelayMs: 480,
};
const DEFAULT_CREATION_AGENT_WRITE_RETRY: ApiRetryOptions = {
maxRetries: 1,
baseDelayMs: 240,
maxDelayMs: 640,
retryUnsafeMethods: true,
};
function buildJsonPostInit(payload: unknown): RequestInit {
return {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
};
}
async function openCreationAgentSsePost(
url: string,
payload: unknown,
fallbackMessage: string,
signal?: AbortSignal,
) {
const response = await fetchWithApiAuth(url, {
...buildJsonPostInit(payload),
signal,
});
if (!response.ok) {
const responseText = await response.text();
throw new Error(parseApiErrorMessage(responseText, fallbackMessage));
}
if (!response.body) {
throw new Error('streaming response body is unavailable');
}
return response;
}
/**
* 三类作品创作 Agent 都遵循同一组 HTTP/SSE 端点形状。
* 这里统一请求骨架,玩法 client 只保留路径、类型与中文错误文案差异。
*/
export function createCreationAgentClient<
TCreateSessionPayload,
TCreateSessionResponse,
TGetSessionResponse,
TSession,
TSendMessagePayload,
TSendMessageResponse,
TExecuteActionPayload,
TExecuteActionResponse,
>({
apiBase,
messages,
createSessionTimeoutMs = 15000,
executeActionTimeoutMs,
readRetry = DEFAULT_CREATION_AGENT_READ_RETRY,
writeRetry = DEFAULT_CREATION_AGENT_WRITE_RETRY,
}: CreationAgentClientOptions) {
const createSession = (
payload: TCreateSessionPayload,
): Promise<TCreateSessionResponse> =>
requestJson<TCreateSessionResponse>(
apiBase,
buildJsonPostInit(payload),
messages.createSession,
{
retry: writeRetry,
timeoutMs: createSessionTimeoutMs,
},
);
const getSession = (sessionId: string): Promise<TGetSessionResponse> =>
requestJson<TGetSessionResponse>(
`${apiBase}/${encodeURIComponent(sessionId)}`,
{ method: 'GET' },
messages.getSession,
{
retry: readRetry,
},
);
const sendMessage = (
sessionId: string,
payload: TSendMessagePayload,
): Promise<TSendMessageResponse> =>
requestJson<TSendMessageResponse>(
`${apiBase}/${encodeURIComponent(sessionId)}/messages`,
buildJsonPostInit(payload),
messages.sendMessage,
{
retry: writeRetry,
},
);
const streamMessage = async (
sessionId: string,
payload: TSendMessagePayload,
options: TextStreamOptions = {},
): Promise<TSession> => {
const response = await openCreationAgentSsePost(
`${apiBase}/${encodeURIComponent(sessionId)}/messages/stream`,
payload,
messages.sendMessage,
options.signal,
);
return readCreationAgentSessionFromSse<TSession>(response, {
...options,
fallbackMessage: messages.sendMessage,
incompleteMessage: messages.streamIncomplete,
});
};
const executeAction = (
sessionId: string,
payload: TExecuteActionPayload,
): Promise<TExecuteActionResponse> =>
requestJson<TExecuteActionResponse>(
`${apiBase}/${encodeURIComponent(sessionId)}/actions`,
buildJsonPostInit(payload),
messages.executeAction,
{
retry: writeRetry,
timeoutMs: executeActionTimeoutMs,
},
);
return {
createSession,
getSession,
sendMessage,
streamMessage,
executeAction,
};
}