1
This commit is contained in:
@@ -6,76 +6,45 @@ import type {
|
||||
ExecuteBigFishActionRequest,
|
||||
SendBigFishMessageRequest,
|
||||
} from '../../../packages/shared/src/contracts/bigFish';
|
||||
import { parseApiErrorMessage } from '../../../packages/shared/src/http';
|
||||
import type { TextStreamOptions } from '../aiTypes';
|
||||
import {
|
||||
type ApiRetryOptions,
|
||||
fetchWithApiAuth,
|
||||
requestJson,
|
||||
} from '../apiClient';
|
||||
import { readCreationAgentSessionFromSse } from '../creation-agent';
|
||||
import { createCreationAgentClient } from '../creation-agent';
|
||||
|
||||
const BIG_FISH_AGENT_API_BASE = '/api/runtime/big-fish/agent/sessions';
|
||||
const BIG_FISH_SESSION_START_TIMEOUT_MS = 15000;
|
||||
const BIG_FISH_READ_RETRY: ApiRetryOptions = {
|
||||
maxRetries: 1,
|
||||
baseDelayMs: 180,
|
||||
maxDelayMs: 480,
|
||||
};
|
||||
const BIG_FISH_WRITE_RETRY: ApiRetryOptions = {
|
||||
maxRetries: 1,
|
||||
baseDelayMs: 240,
|
||||
maxDelayMs: 640,
|
||||
retryUnsafeMethods: true,
|
||||
};
|
||||
const bigFishAgentHttpClient = createCreationAgentClient<
|
||||
CreateBigFishSessionRequest,
|
||||
BigFishSessionResponse,
|
||||
BigFishSessionResponse,
|
||||
BigFishSessionSnapshotResponse,
|
||||
SendBigFishMessageRequest,
|
||||
BigFishSessionResponse,
|
||||
ExecuteBigFishActionRequest,
|
||||
BigFishActionResponse
|
||||
>({
|
||||
apiBase: BIG_FISH_AGENT_API_BASE,
|
||||
messages: {
|
||||
createSession: '创建大鱼吃小鱼共创会话失败',
|
||||
getSession: '读取大鱼吃小鱼共创会话失败',
|
||||
sendMessage: '发送大鱼吃小鱼共创消息失败',
|
||||
streamIncomplete: '大鱼吃小鱼共创消息流式结果不完整',
|
||||
executeAction: '执行大鱼吃小鱼共创操作失败',
|
||||
},
|
||||
});
|
||||
|
||||
export async function createBigFishCreationSession(
|
||||
payload: CreateBigFishSessionRequest = {},
|
||||
) {
|
||||
return requestJson<BigFishSessionResponse>(
|
||||
BIG_FISH_AGENT_API_BASE,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
'创建大鱼吃小鱼共创会话失败',
|
||||
{
|
||||
retry: BIG_FISH_WRITE_RETRY,
|
||||
timeoutMs: BIG_FISH_SESSION_START_TIMEOUT_MS,
|
||||
},
|
||||
);
|
||||
return bigFishAgentHttpClient.createSession(payload);
|
||||
}
|
||||
|
||||
export async function getBigFishCreationSession(sessionId: string) {
|
||||
return requestJson<BigFishSessionResponse>(
|
||||
`${BIG_FISH_AGENT_API_BASE}/${encodeURIComponent(sessionId)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
},
|
||||
'读取大鱼吃小鱼共创会话失败',
|
||||
{
|
||||
retry: BIG_FISH_READ_RETRY,
|
||||
},
|
||||
);
|
||||
return bigFishAgentHttpClient.getSession(sessionId);
|
||||
}
|
||||
|
||||
export async function sendBigFishCreationMessage(
|
||||
sessionId: string,
|
||||
payload: SendBigFishMessageRequest,
|
||||
) {
|
||||
return requestJson<BigFishSessionResponse>(
|
||||
`${BIG_FISH_AGENT_API_BASE}/${encodeURIComponent(sessionId)}/messages`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
'发送大鱼吃小鱼共创消息失败',
|
||||
{
|
||||
retry: BIG_FISH_WRITE_RETRY,
|
||||
},
|
||||
);
|
||||
return bigFishAgentHttpClient.sendMessage(sessionId, payload);
|
||||
}
|
||||
|
||||
export async function streamBigFishCreationMessage(
|
||||
@@ -83,61 +52,14 @@ export async function streamBigFishCreationMessage(
|
||||
payload: SendBigFishMessageRequest,
|
||||
options: TextStreamOptions = {},
|
||||
) {
|
||||
const response = await openBigFishCreationSsePost(
|
||||
`${BIG_FISH_AGENT_API_BASE}/${encodeURIComponent(sessionId)}/messages/stream`,
|
||||
payload,
|
||||
'发送大鱼吃小鱼共创消息失败',
|
||||
);
|
||||
|
||||
return readCreationAgentSessionFromSse<BigFishSessionSnapshotResponse>(
|
||||
response,
|
||||
{
|
||||
...options,
|
||||
fallbackMessage: '发送大鱼吃小鱼共创消息失败',
|
||||
incompleteMessage: '大鱼吃小鱼共创消息流式结果不完整',
|
||||
},
|
||||
);
|
||||
return bigFishAgentHttpClient.streamMessage(sessionId, payload, options);
|
||||
}
|
||||
|
||||
export async function executeBigFishCreationAction(
|
||||
sessionId: string,
|
||||
payload: ExecuteBigFishActionRequest,
|
||||
) {
|
||||
return requestJson<BigFishActionResponse>(
|
||||
`${BIG_FISH_AGENT_API_BASE}/${encodeURIComponent(sessionId)}/actions`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
'执行大鱼吃小鱼共创操作失败',
|
||||
{
|
||||
retry: BIG_FISH_WRITE_RETRY,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function openBigFishCreationSsePost(
|
||||
url: string,
|
||||
payload: unknown,
|
||||
fallbackMessage: string,
|
||||
) {
|
||||
const response = await fetchWithApiAuth(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
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;
|
||||
return bigFishAgentHttpClient.executeAction(sessionId, payload);
|
||||
}
|
||||
|
||||
export const bigFishCreationClient = {
|
||||
|
||||
165
src/services/creation-agent/creationAgentClientFactory.ts
Normal file
165
src/services/creation-agent/creationAgentClientFactory.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
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;
|
||||
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,
|
||||
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,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
createSession,
|
||||
getSession,
|
||||
sendMessage,
|
||||
streamMessage,
|
||||
executeAction,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { afterEach, expect, test, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
parseCreationAgentDocumentInput,
|
||||
validateCreationAgentDocumentInputFile,
|
||||
} from './creationAgentDocumentInput';
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
test('creation agent document input validation accepts supported text documents', () => {
|
||||
expect(() => {
|
||||
validateCreationAgentDocumentInputFile(
|
||||
new File(['世界设定'], '世界设定.MD', { type: 'text/markdown' }),
|
||||
);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
test('creation agent document input validation rejects unsupported documents', () => {
|
||||
expect(() => {
|
||||
validateCreationAgentDocumentInputFile(
|
||||
new File(['binary'], '世界设定.docx', {
|
||||
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
}),
|
||||
);
|
||||
}).toThrow('暂时只支持 txt、md、csv、json 文本文档。');
|
||||
});
|
||||
|
||||
test('creation agent document input validation rejects oversized documents', () => {
|
||||
const oversizedContent = new Uint8Array(256 * 1024 + 1);
|
||||
|
||||
expect(() => {
|
||||
validateCreationAgentDocumentInputFile(
|
||||
new File([oversizedContent], '世界设定.txt', { type: 'text/plain' }),
|
||||
);
|
||||
}).toThrow('文档过大,请上传 256KB 以内的文本文件。');
|
||||
});
|
||||
|
||||
test('creation agent document input parse skips network for unsupported files', async () => {
|
||||
const fetchSpy = vi.fn();
|
||||
vi.stubGlobal('fetch', fetchSpy);
|
||||
|
||||
await expect(
|
||||
parseCreationAgentDocumentInput(new File(['binary'], '世界设定.docx')),
|
||||
).rejects.toThrow('暂时只支持 txt、md、csv、json 文本文档。');
|
||||
|
||||
expect(fetchSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
82
src/services/creation-agent/creationAgentDocumentInput.ts
Normal file
82
src/services/creation-agent/creationAgentDocumentInput.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import type {
|
||||
ParseCreationAgentDocumentInputRequest,
|
||||
ParseCreationAgentDocumentInputResponse,
|
||||
} from '../../../packages/shared/src/contracts/creationAgentDocumentInput';
|
||||
import { requestJson } from '../apiClient';
|
||||
|
||||
const DOCUMENT_INPUT_PARSE_ENDPOINT =
|
||||
'/api/runtime/creation-agent/document-inputs/parse';
|
||||
const MAX_DOCUMENT_INPUT_BYTES = 256 * 1024;
|
||||
const SUPPORTED_DOCUMENT_INPUT_EXTENSIONS = new Set([
|
||||
'txt',
|
||||
'md',
|
||||
'markdown',
|
||||
'csv',
|
||||
'json',
|
||||
]);
|
||||
|
||||
export async function parseCreationAgentDocumentInput(
|
||||
file: File,
|
||||
): Promise<ParseCreationAgentDocumentInputResponse> {
|
||||
validateCreationAgentDocumentInputFile(file);
|
||||
|
||||
const contentBase64 = await readFileAsBase64(file);
|
||||
const payload: ParseCreationAgentDocumentInputRequest = {
|
||||
fileName: file.name,
|
||||
contentType: file.type || null,
|
||||
contentBase64,
|
||||
};
|
||||
|
||||
return requestJson<ParseCreationAgentDocumentInputResponse>(
|
||||
DOCUMENT_INPUT_PARSE_ENDPOINT,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
'解析文档失败',
|
||||
{
|
||||
retry: {
|
||||
maxRetries: 0,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function validateCreationAgentDocumentInputFile(file: File) {
|
||||
const fileName = file.name.trim();
|
||||
const extension = fileName.includes('.')
|
||||
? fileName.split('.').pop()?.trim().toLowerCase()
|
||||
: '';
|
||||
|
||||
if (!extension || !SUPPORTED_DOCUMENT_INPUT_EXTENSIONS.has(extension)) {
|
||||
throw new Error('暂时只支持 txt、md、csv、json 文本文档。');
|
||||
}
|
||||
|
||||
if (file.size <= 0) {
|
||||
throw new Error('文档内容为空,请选择有内容的文件。');
|
||||
}
|
||||
|
||||
if (file.size > MAX_DOCUMENT_INPUT_BYTES) {
|
||||
throw new Error('文档过大,请上传 256KB 以内的文本文件。');
|
||||
}
|
||||
}
|
||||
|
||||
function readFileAsBase64(file: File): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onerror = () => {
|
||||
reject(new Error('读取文档失败,请重新选择文件。'));
|
||||
};
|
||||
reader.onload = () => {
|
||||
const result = typeof reader.result === 'string' ? reader.result : '';
|
||||
const commaIndex = result.indexOf(',');
|
||||
resolve(commaIndex >= 0 ? result.slice(commaIndex + 1) : result);
|
||||
};
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
export * from './creationAgentClientFactory';
|
||||
export * from './creationAgentChat';
|
||||
export * from './creationAgentDocumentInput';
|
||||
export * from './creationAgentProgress';
|
||||
export * from './creationAgentSse';
|
||||
|
||||
@@ -185,11 +185,30 @@ test('keeps failed draft foundation progress on explicit failure state instead o
|
||||
expect(progress?.phaseId).toBe('failed');
|
||||
expect(progress?.phaseLabel).toBe('底稿生成失败');
|
||||
expect(progress?.phaseDetail).toContain('角色主形象补齐失败');
|
||||
expect(progress?.overallProgress).toBeLessThan(100);
|
||||
expect(progress?.estimatedRemainingMs).toBeNull();
|
||||
expect(progress?.steps.some((step) => step.label === '编译草稿卡')).toBe(true);
|
||||
expect(progress?.steps.some((step) => step.status === 'active')).toBe(false);
|
||||
expect(progress?.steps.filter((step) => step.status === 'completed').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('estimates draft generation wait time from phase duration model instead of linear progress', () => {
|
||||
const progress = buildAgentDraftFoundationGenerationProgress(
|
||||
{
|
||||
...baseOperation,
|
||||
phaseLabel: '生成幕背景图',
|
||||
phaseDetail: '正在生成幕背景图 1/6:潮汐码头。',
|
||||
progress: 98,
|
||||
updatedAt: '1970-01-01T00:00:01.000Z',
|
||||
},
|
||||
1_000,
|
||||
6_000,
|
||||
);
|
||||
|
||||
expect(progress?.estimatedRemainingMs).toBeGreaterThan(80_000);
|
||||
expect(progress?.estimatedRemainingMs).toBeLessThan(140_000);
|
||||
});
|
||||
|
||||
test('builds readable draft setting text from creator intent first', () => {
|
||||
const settingText = buildAgentDraftFoundationSettingText(baseSession);
|
||||
|
||||
|
||||
@@ -199,6 +199,7 @@ type AgentDraftFoundationStepDefinition = {
|
||||
detail: string;
|
||||
matchers: string[];
|
||||
minProgress: number;
|
||||
expectedDurationMs: number;
|
||||
};
|
||||
|
||||
type AgentDraftFoundationFailedStep = {
|
||||
@@ -215,6 +216,7 @@ const AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS = [
|
||||
detail: '正在校验当前锚点并准备底稿编译链路。',
|
||||
matchers: ['已接收请求'],
|
||||
minProgress: 0,
|
||||
expectedDurationMs: 3_000,
|
||||
},
|
||||
{
|
||||
id: 'framework',
|
||||
@@ -222,6 +224,7 @@ const AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS = [
|
||||
detail: '正在生成第一版世界框架、主题与核心冲突。',
|
||||
matchers: ['整理世界骨架', '生成世界底稿'],
|
||||
minProgress: 12,
|
||||
expectedDurationMs: 25_000,
|
||||
},
|
||||
{
|
||||
id: 'playable-outline',
|
||||
@@ -229,6 +232,7 @@ const AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS = [
|
||||
detail: '正在补出玩家视角角色的首轮名单与定位。',
|
||||
matchers: ['生成可扮演角色'],
|
||||
minProgress: 16,
|
||||
expectedDurationMs: 18_000,
|
||||
},
|
||||
{
|
||||
id: 'story-outline',
|
||||
@@ -236,6 +240,7 @@ const AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS = [
|
||||
detail: '正在整理关键 NPC、势力接口人与关系入口。',
|
||||
matchers: ['生成场景角色'],
|
||||
minProgress: 30,
|
||||
expectedDurationMs: 45_000,
|
||||
},
|
||||
{
|
||||
id: 'landmark-seed',
|
||||
@@ -243,6 +248,7 @@ const AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS = [
|
||||
detail: '正在补出第一批关键场景与地点骨架。',
|
||||
matchers: ['生成关键场景'],
|
||||
minProgress: 44,
|
||||
expectedDurationMs: 18_000,
|
||||
},
|
||||
{
|
||||
id: 'landmark-network',
|
||||
@@ -250,6 +256,7 @@ const AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS = [
|
||||
detail: '正在串联地点关系、线程挂钩与角色分布。',
|
||||
matchers: ['建立场景连接'],
|
||||
minProgress: 56,
|
||||
expectedDurationMs: 18_000,
|
||||
},
|
||||
{
|
||||
id: 'playable-detail',
|
||||
@@ -257,6 +264,7 @@ const AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS = [
|
||||
detail: '正在补全可扮演角色的叙事基础与档案细节。',
|
||||
matchers: ['补全可扮演角色'],
|
||||
minProgress: 66,
|
||||
expectedDurationMs: 32_000,
|
||||
},
|
||||
{
|
||||
id: 'story-detail',
|
||||
@@ -264,6 +272,7 @@ const AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS = [
|
||||
detail: '正在补全场景角色的叙事基础与档案细节。',
|
||||
matchers: ['补全场景角色'],
|
||||
minProgress: 84,
|
||||
expectedDurationMs: 65_000,
|
||||
},
|
||||
{
|
||||
id: 'finalize',
|
||||
@@ -271,6 +280,7 @@ const AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS = [
|
||||
detail: '正在把分批生成结果汇总成第一版可浏览的世界底稿。',
|
||||
matchers: ['编译世界底稿'],
|
||||
minProgress: 97,
|
||||
expectedDurationMs: 6_000,
|
||||
},
|
||||
{
|
||||
id: 'role-visuals',
|
||||
@@ -278,6 +288,7 @@ const AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS = [
|
||||
detail: '正在为关键角色补主形象预览资源。',
|
||||
matchers: ['生成角色主形象'],
|
||||
minProgress: 97,
|
||||
expectedDurationMs: 85_000,
|
||||
},
|
||||
{
|
||||
id: 'act-backgrounds',
|
||||
@@ -285,6 +296,7 @@ const AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS = [
|
||||
detail: '正在为场景章节的每一幕补背景图预览资源。',
|
||||
matchers: ['生成幕背景图'],
|
||||
minProgress: 98,
|
||||
expectedDurationMs: 85_000,
|
||||
},
|
||||
{
|
||||
id: 'cards',
|
||||
@@ -292,6 +304,7 @@ const AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS = [
|
||||
detail: '正在整理世界卡、角色卡、地点卡与详情结构。',
|
||||
matchers: ['编译草稿卡'],
|
||||
minProgress: 99,
|
||||
expectedDurationMs: 15_000,
|
||||
},
|
||||
{
|
||||
id: 'workspace',
|
||||
@@ -299,6 +312,7 @@ const AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS = [
|
||||
detail: '正在写回草稿数据,并打开可继续完善的结果页。',
|
||||
matchers: ['世界底稿已生成'],
|
||||
minProgress: 100,
|
||||
expectedDurationMs: 4_000,
|
||||
},
|
||||
] as const satisfies ReadonlyArray<AgentDraftFoundationStepDefinition>;
|
||||
|
||||
@@ -333,6 +347,39 @@ function resolveAgentDraftFoundationStepIndexByProgress(progress: number) {
|
||||
return matchedIndex;
|
||||
}
|
||||
|
||||
function resolveFailedProgress(
|
||||
operation: CustomWorldAgentOperationRecord,
|
||||
activeStepIndex: number,
|
||||
) {
|
||||
const progress = clampProgress(operation.progress);
|
||||
|
||||
if (operation.status !== 'failed') {
|
||||
return progress;
|
||||
}
|
||||
|
||||
if (progress < 100) {
|
||||
return progress;
|
||||
}
|
||||
|
||||
const activeStep =
|
||||
AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS[activeStepIndex] ??
|
||||
AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS[0];
|
||||
|
||||
return Math.max(0, Math.min(99, activeStep.minProgress));
|
||||
}
|
||||
|
||||
function parseOperationUpdatedAtMs(
|
||||
operation: CustomWorldAgentOperationRecord,
|
||||
) {
|
||||
const rawUpdatedAt = operation.updatedAt?.trim();
|
||||
if (!rawUpdatedAt) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsedMs = Date.parse(rawUpdatedAt);
|
||||
return Number.isFinite(parsedMs) ? parsedMs : null;
|
||||
}
|
||||
|
||||
function resolveAgentDraftFoundationStepIndex(
|
||||
operation: CustomWorldAgentOperationRecord,
|
||||
) {
|
||||
@@ -410,19 +457,47 @@ function resolveEstimatedRemainingMs(
|
||||
startedAtMs: number | null,
|
||||
nowMs: number,
|
||||
status: CustomWorldAgentOperationRecord['status'],
|
||||
activeStepIndex: number,
|
||||
operationUpdatedAtMs: number | null,
|
||||
) {
|
||||
if (status === 'completed') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!startedAtMs || progress <= 0 || progress >= 100) {
|
||||
if (status === 'failed' || progress >= 100) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const elapsedMs = Math.max(0, nowMs - startedAtMs);
|
||||
const progressFraction = progress / 100;
|
||||
const activeStep =
|
||||
AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS[activeStepIndex] ??
|
||||
AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS[0];
|
||||
const nextStep =
|
||||
AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS[activeStepIndex + 1] ??
|
||||
activeStep;
|
||||
const phaseProgressRange = Math.max(
|
||||
1,
|
||||
nextStep.minProgress - activeStep.minProgress,
|
||||
);
|
||||
const phaseProgressRatio = Math.max(
|
||||
0,
|
||||
Math.min(0.95, (progress - activeStep.minProgress) / phaseProgressRange),
|
||||
);
|
||||
const phaseStartedAtMs = operationUpdatedAtMs ?? startedAtMs;
|
||||
const currentPhaseElapsedMs = phaseStartedAtMs
|
||||
? Math.max(0, nowMs - phaseStartedAtMs)
|
||||
: 0;
|
||||
const currentPhaseRemainingMs = Math.max(
|
||||
0,
|
||||
Math.round(
|
||||
activeStep.expectedDurationMs * (1 - phaseProgressRatio) -
|
||||
currentPhaseElapsedMs,
|
||||
),
|
||||
);
|
||||
const followingStepsRemainingMs = AGENT_DRAFT_FOUNDATION_STEP_DEFINITIONS.slice(
|
||||
activeStepIndex + 1,
|
||||
).reduce((sum, step) => sum + step.expectedDurationMs, 0);
|
||||
|
||||
return Math.max(0, Math.round(elapsedMs / progressFraction - elapsedMs));
|
||||
return currentPhaseRemainingMs + followingStepsRemainingMs;
|
||||
}
|
||||
|
||||
export function isDraftFoundationOperation(
|
||||
@@ -449,14 +524,16 @@ export function buildAgentDraftFoundationGenerationProgress(
|
||||
return null;
|
||||
}
|
||||
|
||||
const overallProgress = clampProgress(operation.progress);
|
||||
const activeStepIndex = resolveAgentDraftFoundationStepIndex(operation);
|
||||
const overallProgress = resolveFailedProgress(operation, activeStepIndex);
|
||||
const elapsedMs = startedAtMs ? Math.max(0, nowMs - startedAtMs) : 0;
|
||||
const estimatedRemainingMs = resolveEstimatedRemainingMs(
|
||||
overallProgress,
|
||||
startedAtMs,
|
||||
nowMs,
|
||||
operation.status,
|
||||
activeStepIndex,
|
||||
parseOperationUpdatedAtMs(operation),
|
||||
);
|
||||
const failedStep = resolveAgentDraftFoundationFailedStep(operation);
|
||||
const activeStep =
|
||||
|
||||
350
src/services/customWorldFoundationEntries.ts
Normal file
350
src/services/customWorldFoundationEntries.ts
Normal file
@@ -0,0 +1,350 @@
|
||||
import type {
|
||||
EightAnchorContent,
|
||||
KeyRelationshipValue,
|
||||
} from '../../packages/shared/src/contracts/customWorldAgent';
|
||||
import { normalizeCustomWorldCreatorIntent } from './customWorldCreatorIntent';
|
||||
import type { CustomWorldProfile } from '../types';
|
||||
|
||||
export type CustomWorldFoundationEntryId =
|
||||
| 'world-promise'
|
||||
| 'player-fantasy'
|
||||
| 'theme-boundary'
|
||||
| 'player-entry-point'
|
||||
| 'core-conflict'
|
||||
| 'key-relationships'
|
||||
| 'hidden-lines'
|
||||
| 'iconic-elements';
|
||||
|
||||
export type CustomWorldFoundationEntry = {
|
||||
id: CustomWorldFoundationEntryId;
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export function compactFoundationTextList(
|
||||
values: Array<string | null | undefined>,
|
||||
) {
|
||||
return values.map((value) => value?.trim()).filter(Boolean) as string[];
|
||||
}
|
||||
|
||||
export function parseFoundationTagText(value: string) {
|
||||
return value
|
||||
.split(/[;;]/u)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function toText(value: unknown) {
|
||||
return typeof value === 'string' ? value.trim() : '';
|
||||
}
|
||||
|
||||
function toTextArray(value: unknown) {
|
||||
return Array.isArray(value)
|
||||
? value.map((item) => toText(item)).filter(Boolean)
|
||||
: [];
|
||||
}
|
||||
|
||||
function toRecord(value: unknown) {
|
||||
return value && typeof value === 'object' && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: null;
|
||||
}
|
||||
|
||||
function buildRelationshipSeedText(value: unknown) {
|
||||
const record = toRecord(value);
|
||||
if (!record) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return compactFoundationTextList([
|
||||
toText(record.name),
|
||||
toText(record.role),
|
||||
toText(record.relationToPlayer)
|
||||
? `与玩家:${toText(record.relationToPlayer)}`
|
||||
: '',
|
||||
toText(record.hiddenHook) ? `代价/暗线:${toText(record.hiddenHook)}` : '',
|
||||
]).join(';');
|
||||
}
|
||||
|
||||
function buildKeyRelationshipText(value: KeyRelationshipValue) {
|
||||
return compactFoundationTextList([
|
||||
value.pairs,
|
||||
value.relationshipType,
|
||||
value.secretOrCost ? `代价/秘密:${value.secretOrCost}` : '',
|
||||
]).join(';');
|
||||
}
|
||||
|
||||
function buildAnchorContentFromProfileFallback(
|
||||
profile: CustomWorldProfile,
|
||||
): EightAnchorContent {
|
||||
const creatorIntent = normalizeCustomWorldCreatorIntent(profile.creatorIntent);
|
||||
const relationshipSeed = creatorIntent?.keyCharacters[0] ?? null;
|
||||
|
||||
return {
|
||||
worldPromise: {
|
||||
hook:
|
||||
creatorIntent?.worldHook ||
|
||||
profile.anchorPack?.worldSummary ||
|
||||
profile.summary,
|
||||
differentiator: profile.subtitle || profile.settingText,
|
||||
desiredExperience:
|
||||
compactFoundationTextList([
|
||||
creatorIntent?.toneDirectives.join('、') || '',
|
||||
profile.tone,
|
||||
]).join(';') || profile.tone,
|
||||
},
|
||||
playerFantasy: {
|
||||
playerRole: creatorIntent?.playerPremise || profile.playerGoal,
|
||||
corePursuit: profile.playerGoal,
|
||||
fearOfLoss:
|
||||
relationshipSeed?.hiddenHook ||
|
||||
creatorIntent?.coreConflicts[0] ||
|
||||
profile.coreConflicts[0] ||
|
||||
'',
|
||||
},
|
||||
themeBoundary: {
|
||||
toneKeywords: compactFoundationTextList([
|
||||
creatorIntent?.themeKeywords.join('、') || '',
|
||||
creatorIntent?.toneDirectives.join('、') || '',
|
||||
]),
|
||||
aestheticDirectives: compactFoundationTextList([
|
||||
profile.tone,
|
||||
profile.subtitle,
|
||||
]),
|
||||
forbiddenDirectives: creatorIntent?.forbiddenDirectives ?? [],
|
||||
},
|
||||
playerEntryPoint: {
|
||||
openingIdentity: creatorIntent?.playerPremise || '',
|
||||
openingProblem:
|
||||
creatorIntent?.openingSituation || profile.coreConflicts[0] || '',
|
||||
entryMotivation: profile.playerGoal,
|
||||
},
|
||||
coreConflict: {
|
||||
surfaceConflicts:
|
||||
creatorIntent?.coreConflicts.length
|
||||
? creatorIntent.coreConflicts
|
||||
: profile.coreConflicts,
|
||||
hiddenCrisis:
|
||||
relationshipSeed?.hiddenHook ||
|
||||
profile.summary ||
|
||||
profile.settingText,
|
||||
firstTouchedConflict:
|
||||
creatorIntent?.openingSituation ||
|
||||
profile.coreConflicts[0] ||
|
||||
profile.playerGoal,
|
||||
},
|
||||
keyRelationships: relationshipSeed
|
||||
? [
|
||||
{
|
||||
pairs: compactFoundationTextList([
|
||||
relationshipSeed.name,
|
||||
relationshipSeed.role,
|
||||
]).join(' · '),
|
||||
relationshipType: relationshipSeed.relationToPlayer || '',
|
||||
secretOrCost: relationshipSeed.hiddenHook || '',
|
||||
},
|
||||
]
|
||||
: [],
|
||||
hiddenLines: {
|
||||
hiddenTruths: compactFoundationTextList([
|
||||
relationshipSeed?.hiddenHook || '',
|
||||
profile.summary,
|
||||
]),
|
||||
misdirectionHints: compactFoundationTextList([
|
||||
profile.subtitle,
|
||||
profile.majorFactions[0] || '',
|
||||
]),
|
||||
revealPacing:
|
||||
creatorIntent?.openingSituation ||
|
||||
profile.coreConflicts[0] ||
|
||||
profile.playerGoal,
|
||||
},
|
||||
iconicElements: {
|
||||
iconicMotifs:
|
||||
creatorIntent?.iconicElements.length
|
||||
? creatorIntent.iconicElements
|
||||
: compactFoundationTextList([
|
||||
profile.anchorPack?.motifDirectives.join('、') || '',
|
||||
profile.landmarks[0]?.name || '',
|
||||
]),
|
||||
institutionsOrArtifacts: compactFoundationTextList([
|
||||
profile.camp?.name || '',
|
||||
profile.majorFactions[0] || '',
|
||||
]),
|
||||
hardRules: compactFoundationTextList([
|
||||
profile.playerGoal,
|
||||
profile.coreConflicts[0] || '',
|
||||
]),
|
||||
},
|
||||
} satisfies EightAnchorContent;
|
||||
}
|
||||
|
||||
export function getCustomWorldFoundationAnchorContent(
|
||||
profile: CustomWorldProfile,
|
||||
) {
|
||||
const anchorContentRecord = profile.anchorContent;
|
||||
if (!anchorContentRecord) {
|
||||
return buildAnchorContentFromProfileFallback(profile);
|
||||
}
|
||||
|
||||
const worldPromiseRecord = toRecord(anchorContentRecord.worldPromise);
|
||||
const playerFantasyRecord = toRecord(anchorContentRecord.playerFantasy);
|
||||
const themeBoundaryRecord = toRecord(anchorContentRecord.themeBoundary);
|
||||
const playerEntryPointRecord = toRecord(anchorContentRecord.playerEntryPoint);
|
||||
const coreConflictRecord = toRecord(anchorContentRecord.coreConflict);
|
||||
const hiddenLinesRecord = toRecord(anchorContentRecord.hiddenLines);
|
||||
const iconicElementsRecord = toRecord(anchorContentRecord.iconicElements);
|
||||
|
||||
return {
|
||||
worldPromise: worldPromiseRecord
|
||||
? {
|
||||
hook: toText(worldPromiseRecord.hook),
|
||||
differentiator: toText(worldPromiseRecord.differentiator),
|
||||
desiredExperience: toText(worldPromiseRecord.desiredExperience),
|
||||
}
|
||||
: null,
|
||||
playerFantasy: playerFantasyRecord
|
||||
? {
|
||||
playerRole: toText(playerFantasyRecord.playerRole),
|
||||
corePursuit: toText(playerFantasyRecord.corePursuit),
|
||||
fearOfLoss: toText(playerFantasyRecord.fearOfLoss),
|
||||
}
|
||||
: null,
|
||||
themeBoundary: themeBoundaryRecord
|
||||
? {
|
||||
toneKeywords: toTextArray(themeBoundaryRecord.toneKeywords),
|
||||
aestheticDirectives: toTextArray(
|
||||
themeBoundaryRecord.aestheticDirectives,
|
||||
),
|
||||
forbiddenDirectives: toTextArray(themeBoundaryRecord.forbiddenDirectives),
|
||||
}
|
||||
: null,
|
||||
playerEntryPoint: playerEntryPointRecord
|
||||
? {
|
||||
openingIdentity: toText(playerEntryPointRecord.openingIdentity),
|
||||
openingProblem: toText(playerEntryPointRecord.openingProblem),
|
||||
entryMotivation: toText(playerEntryPointRecord.entryMotivation),
|
||||
}
|
||||
: null,
|
||||
coreConflict: coreConflictRecord
|
||||
? {
|
||||
surfaceConflicts: toTextArray(coreConflictRecord.surfaceConflicts),
|
||||
hiddenCrisis: toText(coreConflictRecord.hiddenCrisis),
|
||||
firstTouchedConflict: toText(coreConflictRecord.firstTouchedConflict),
|
||||
}
|
||||
: null,
|
||||
keyRelationships: Array.isArray(anchorContentRecord.keyRelationships)
|
||||
? anchorContentRecord.keyRelationships
|
||||
.map((entry) => toRecord(entry))
|
||||
.filter(Boolean)
|
||||
.map((entry) => ({
|
||||
pairs: toText(entry?.pairs),
|
||||
relationshipType: toText(entry?.relationshipType),
|
||||
secretOrCost: toText(entry?.secretOrCost),
|
||||
}))
|
||||
: [],
|
||||
hiddenLines: hiddenLinesRecord
|
||||
? {
|
||||
hiddenTruths: toTextArray(hiddenLinesRecord.hiddenTruths),
|
||||
misdirectionHints: toTextArray(hiddenLinesRecord.misdirectionHints),
|
||||
revealPacing: toText(hiddenLinesRecord.revealPacing),
|
||||
}
|
||||
: null,
|
||||
iconicElements: iconicElementsRecord
|
||||
? {
|
||||
iconicMotifs: toTextArray(iconicElementsRecord.iconicMotifs),
|
||||
institutionsOrArtifacts: toTextArray(
|
||||
iconicElementsRecord.institutionsOrArtifacts,
|
||||
),
|
||||
hardRules: toTextArray(iconicElementsRecord.hardRules),
|
||||
}
|
||||
: null,
|
||||
} satisfies EightAnchorContent;
|
||||
}
|
||||
|
||||
export function buildCustomWorldFoundationEntries(
|
||||
profile: CustomWorldProfile,
|
||||
): CustomWorldFoundationEntry[] {
|
||||
const creatorIntent = normalizeCustomWorldCreatorIntent(profile.creatorIntent);
|
||||
const anchorContent = getCustomWorldFoundationAnchorContent(profile);
|
||||
const fallbackRelationshipText =
|
||||
buildRelationshipSeedText(creatorIntent?.keyCharacters[0]) ||
|
||||
profile.playableNpcs[0]?.relationshipHooks.join(';') ||
|
||||
profile.storyNpcs[0]?.relationshipHooks.join(';') ||
|
||||
'';
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'world-promise',
|
||||
label: '世界承诺',
|
||||
value: compactFoundationTextList([
|
||||
anchorContent.worldPromise?.hook || '',
|
||||
anchorContent.worldPromise?.differentiator || '',
|
||||
anchorContent.worldPromise?.desiredExperience || '',
|
||||
]).join(';'),
|
||||
},
|
||||
{
|
||||
id: 'player-fantasy',
|
||||
label: '玩家幻想',
|
||||
value: compactFoundationTextList([
|
||||
anchorContent.playerFantasy?.playerRole || '',
|
||||
anchorContent.playerFantasy?.corePursuit || '',
|
||||
anchorContent.playerFantasy?.fearOfLoss || '',
|
||||
]).join(';'),
|
||||
},
|
||||
{
|
||||
id: 'theme-boundary',
|
||||
label: '主题边界',
|
||||
value: compactFoundationTextList([
|
||||
anchorContent.themeBoundary?.toneKeywords.join('、') || '',
|
||||
anchorContent.themeBoundary?.aestheticDirectives.join('、') || '',
|
||||
anchorContent.themeBoundary?.forbiddenDirectives.length
|
||||
? `避免:${anchorContent.themeBoundary.forbiddenDirectives.join('、')}`
|
||||
: '',
|
||||
]).join(';'),
|
||||
},
|
||||
{
|
||||
id: 'player-entry-point',
|
||||
label: '玩家切入口',
|
||||
value: compactFoundationTextList([
|
||||
anchorContent.playerEntryPoint?.openingIdentity || '',
|
||||
anchorContent.playerEntryPoint?.openingProblem || '',
|
||||
anchorContent.playerEntryPoint?.entryMotivation || '',
|
||||
]).join(';'),
|
||||
},
|
||||
{
|
||||
id: 'core-conflict',
|
||||
label: '核心冲突',
|
||||
value: compactFoundationTextList([
|
||||
anchorContent.coreConflict?.surfaceConflicts.join('、') || '',
|
||||
anchorContent.coreConflict?.hiddenCrisis || '',
|
||||
anchorContent.coreConflict?.firstTouchedConflict || '',
|
||||
]).join(';'),
|
||||
},
|
||||
{
|
||||
id: 'key-relationships',
|
||||
label: '关键关系',
|
||||
value:
|
||||
anchorContent.keyRelationships.map(buildKeyRelationshipText).join('\n') ||
|
||||
fallbackRelationshipText,
|
||||
},
|
||||
{
|
||||
id: 'hidden-lines',
|
||||
label: '暗线与揭示',
|
||||
value: compactFoundationTextList([
|
||||
anchorContent.hiddenLines?.hiddenTruths.join('、') || '',
|
||||
anchorContent.hiddenLines?.misdirectionHints.join('、') || '',
|
||||
anchorContent.hiddenLines?.revealPacing || '',
|
||||
]).join(';'),
|
||||
},
|
||||
{
|
||||
id: 'iconic-elements',
|
||||
label: '标志元素',
|
||||
value: compactFoundationTextList([
|
||||
anchorContent.iconicElements?.iconicMotifs.join('、') || '',
|
||||
anchorContent.iconicElements?.institutionsOrArtifacts.join('、') || '',
|
||||
anchorContent.iconicElements?.hardRules.join('、') || '',
|
||||
]).join(';'),
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -8,28 +8,29 @@ import type {
|
||||
PuzzleAgentSessionSnapshot,
|
||||
SendPuzzleAgentMessageRequest,
|
||||
} from '../../../packages/shared/src/contracts/puzzleAgentSession';
|
||||
import { parseApiErrorMessage } from '../../../packages/shared/src/http';
|
||||
import type { TextStreamOptions } from '../aiTypes';
|
||||
import {
|
||||
type ApiRetryOptions,
|
||||
fetchWithApiAuth,
|
||||
requestJson,
|
||||
} from '../apiClient';
|
||||
import { readCreationAgentSessionFromSse } from '../creation-agent';
|
||||
import { createCreationAgentClient } from '../creation-agent';
|
||||
|
||||
const PUZZLE_AGENT_API_BASE = '/api/runtime/puzzle/agent/sessions';
|
||||
const PUZZLE_AGENT_SESSION_START_TIMEOUT_MS = 15000;
|
||||
const PUZZLE_AGENT_READ_RETRY: ApiRetryOptions = {
|
||||
maxRetries: 1,
|
||||
baseDelayMs: 180,
|
||||
maxDelayMs: 480,
|
||||
};
|
||||
const PUZZLE_AGENT_WRITE_RETRY: ApiRetryOptions = {
|
||||
maxRetries: 1,
|
||||
baseDelayMs: 240,
|
||||
maxDelayMs: 640,
|
||||
retryUnsafeMethods: true,
|
||||
};
|
||||
const puzzleAgentHttpClient = createCreationAgentClient<
|
||||
CreatePuzzleAgentSessionRequest,
|
||||
CreatePuzzleAgentSessionResponse,
|
||||
CreatePuzzleAgentSessionResponse,
|
||||
PuzzleAgentSessionSnapshot,
|
||||
SendPuzzleAgentMessageRequest,
|
||||
{ session: PuzzleAgentSessionSnapshot },
|
||||
PuzzleAgentActionRequest,
|
||||
PuzzleAgentActionResponse
|
||||
>({
|
||||
apiBase: PUZZLE_AGENT_API_BASE,
|
||||
messages: {
|
||||
createSession: '创建拼图共创会话失败',
|
||||
getSession: '读取拼图共创会话失败',
|
||||
sendMessage: '发送拼图共创消息失败',
|
||||
streamIncomplete: '拼图共创消息流式结果不完整',
|
||||
executeAction: '执行拼图共创操作失败',
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 创建拼图 Agent 共创会话。
|
||||
@@ -38,35 +39,14 @@ const PUZZLE_AGENT_WRITE_RETRY: ApiRetryOptions = {
|
||||
export async function createPuzzleAgentSession(
|
||||
payload: CreatePuzzleAgentSessionRequest = {},
|
||||
) {
|
||||
return requestJson<CreatePuzzleAgentSessionResponse>(
|
||||
PUZZLE_AGENT_API_BASE,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
'创建拼图共创会话失败',
|
||||
{
|
||||
retry: PUZZLE_AGENT_WRITE_RETRY,
|
||||
timeoutMs: PUZZLE_AGENT_SESSION_START_TIMEOUT_MS,
|
||||
},
|
||||
);
|
||||
return puzzleAgentHttpClient.createSession(payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取拼图 Agent 会话快照。
|
||||
*/
|
||||
export async function getPuzzleAgentSession(sessionId: string) {
|
||||
return requestJson<CreatePuzzleAgentSessionResponse>(
|
||||
`${PUZZLE_AGENT_API_BASE}/${encodeURIComponent(sessionId)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
},
|
||||
'读取拼图共创会话失败',
|
||||
{
|
||||
retry: PUZZLE_AGENT_READ_RETRY,
|
||||
},
|
||||
);
|
||||
return puzzleAgentHttpClient.getSession(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,18 +57,7 @@ export async function sendPuzzleAgentMessage(
|
||||
sessionId: string,
|
||||
payload: SendPuzzleAgentMessageRequest,
|
||||
) {
|
||||
return requestJson<{ session: PuzzleAgentSessionSnapshot }>(
|
||||
`${PUZZLE_AGENT_API_BASE}/${encodeURIComponent(sessionId)}/messages`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
'发送拼图共创消息失败',
|
||||
{
|
||||
retry: PUZZLE_AGENT_WRITE_RETRY,
|
||||
},
|
||||
);
|
||||
return puzzleAgentHttpClient.sendMessage(sessionId, payload);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,20 +69,7 @@ export async function streamPuzzleAgentMessage(
|
||||
payload: SendPuzzleAgentMessageRequest,
|
||||
options: TextStreamOptions = {},
|
||||
) {
|
||||
const response = await openPuzzleAgentSsePost(
|
||||
`${PUZZLE_AGENT_API_BASE}/${encodeURIComponent(sessionId)}/messages/stream`,
|
||||
payload,
|
||||
'发送拼图共创消息失败',
|
||||
);
|
||||
|
||||
return readCreationAgentSessionFromSse<PuzzleAgentSessionSnapshot>(
|
||||
response,
|
||||
{
|
||||
...options,
|
||||
fallbackMessage: '发送拼图共创消息失败',
|
||||
incompleteMessage: '拼图共创消息流式结果不完整',
|
||||
},
|
||||
);
|
||||
return puzzleAgentHttpClient.streamMessage(sessionId, payload, options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,41 +80,7 @@ export async function executePuzzleAgentAction(
|
||||
sessionId: string,
|
||||
payload: PuzzleAgentActionRequest,
|
||||
) {
|
||||
return requestJson<PuzzleAgentActionResponse>(
|
||||
`${PUZZLE_AGENT_API_BASE}/${encodeURIComponent(sessionId)}/actions`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
'执行拼图共创操作失败',
|
||||
{
|
||||
retry: PUZZLE_AGENT_WRITE_RETRY,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function openPuzzleAgentSsePost(
|
||||
url: string,
|
||||
payload: unknown,
|
||||
fallbackMessage: string,
|
||||
) {
|
||||
const response = await fetchWithApiAuth(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
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;
|
||||
return puzzleAgentHttpClient.executeAction(sessionId, payload);
|
||||
}
|
||||
|
||||
export const puzzleAgentClient = {
|
||||
|
||||
@@ -11,8 +11,10 @@ export {
|
||||
} from './rpgEntryLibraryClient';
|
||||
export {
|
||||
clearRpgProfileBrowseHistory,
|
||||
createRpgProfileRechargeOrder,
|
||||
getRpgProfileDashboard,
|
||||
getRpgProfilePlayStats,
|
||||
getRpgProfileRechargeCenter,
|
||||
getRpgProfileSettings,
|
||||
getRpgProfileWalletLedger,
|
||||
listRpgProfileBrowseHistory,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import type {
|
||||
CreateProfileRechargeOrderResponse,
|
||||
PlatformBrowseHistoryBatchSyncRequest,
|
||||
PlatformBrowseHistoryResponse,
|
||||
PlatformBrowseHistoryWriteEntry,
|
||||
ProfileDashboardSummary,
|
||||
ProfilePlayStatsResponse,
|
||||
ProfileRechargeCenterResponse,
|
||||
ProfileSaveArchiveListResponse,
|
||||
ProfileSaveArchiveResumeResponse,
|
||||
ProfileWalletLedgerResponse,
|
||||
@@ -67,6 +69,33 @@ export function getRpgProfileWalletLedger(
|
||||
);
|
||||
}
|
||||
|
||||
export function getRpgProfileRechargeCenter(
|
||||
options: RuntimeRequestOptions = {},
|
||||
) {
|
||||
return requestRpgRuntimeJson<ProfileRechargeCenterResponse>(
|
||||
'/profile/recharge-center',
|
||||
{ method: 'GET' },
|
||||
'读取账户充值失败',
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
export function createRpgProfileRechargeOrder(
|
||||
productId: string,
|
||||
options: RuntimeRequestOptions = {},
|
||||
) {
|
||||
return requestRpgRuntimeJson<CreateProfileRechargeOrderResponse>(
|
||||
'/profile/recharge/orders',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ productId, paymentChannel: 'mock' }),
|
||||
},
|
||||
'充值失败',
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
export function getRpgProfilePlayStats(options: RuntimeRequestOptions = {}) {
|
||||
return requestRpgRuntimeJson<ProfilePlayStatsResponse>(
|
||||
'/profile/play-stats',
|
||||
@@ -176,6 +205,8 @@ export const rpgProfileClient = {
|
||||
getDashboard: getRpgProfileDashboard,
|
||||
getPlayStats: getRpgProfilePlayStats,
|
||||
getWalletLedger: getRpgProfileWalletLedger,
|
||||
getRechargeCenter: getRpgProfileRechargeCenter,
|
||||
createRechargeOrder: createRpgProfileRechargeOrder,
|
||||
getSettings: getRpgProfileSettings,
|
||||
putSettings: putRpgProfileSettings,
|
||||
listSaveArchives: listRpgProfileSaveArchives,
|
||||
|
||||
Reference in New Issue
Block a user