Preserve partial creation replies on stream failure
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
@@ -51,3 +51,28 @@ test('readCreationAgentSessionFromSse flushes decoder tail and handles CRLF boun
|
||||
title: '世界共创',
|
||||
});
|
||||
});
|
||||
|
||||
test('readCreationAgentSessionFromSse keeps streamed updates before error event', async () => {
|
||||
const encoder = new TextEncoder();
|
||||
const response = createChunkedStreamResponse([
|
||||
encoder.encode(
|
||||
'event: reply_delta\ndata: {"text":"先把方洞万能的反差定住。"}\n\n',
|
||||
),
|
||||
encoder.encode(
|
||||
'event: error\ndata: {"message":"方洞挑战聊天生成失败:LLM 请求超时"}\n\n',
|
||||
),
|
||||
]);
|
||||
const updates: string[] = [];
|
||||
|
||||
await expect(
|
||||
readCreationAgentSessionFromSse(response, {
|
||||
fallbackMessage: '发送失败',
|
||||
incompleteMessage: '结果不完整',
|
||||
onUpdate: (text) => {
|
||||
updates.push(text);
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow('方洞挑战聊天生成失败:LLM 请求超时');
|
||||
|
||||
expect(updates).toEqual(['先把方洞万能的反差定住。']);
|
||||
});
|
||||
|
||||
@@ -29,6 +29,14 @@ export function buildMatch3DPublicWorkCode(profileId: string) {
|
||||
return `M3-${suffix}`;
|
||||
}
|
||||
|
||||
export function buildSquareHolePublicWorkCode(profileId: string) {
|
||||
const normalized = normalizePublicCodeText(profileId);
|
||||
const fallback = normalized || '00000000';
|
||||
const suffix = fallback.slice(-8).padStart(8, '0');
|
||||
|
||||
return `SH-${suffix}`;
|
||||
}
|
||||
|
||||
export function isSamePuzzlePublicWorkCode(keyword: string, profileId: string) {
|
||||
const normalizedKeyword = normalizePublicCodeText(keyword);
|
||||
|
||||
@@ -61,3 +69,16 @@ export function isSameMatch3DPublicWorkCode(keyword: string, profileId: string)
|
||||
normalizedKeyword === normalizePublicCodeText(profileId)
|
||||
);
|
||||
}
|
||||
|
||||
export function isSameSquareHolePublicWorkCode(
|
||||
keyword: string,
|
||||
profileId: string,
|
||||
) {
|
||||
const normalizedKeyword = normalizePublicCodeText(keyword);
|
||||
|
||||
return (
|
||||
normalizedKeyword ===
|
||||
normalizePublicCodeText(buildSquareHolePublicWorkCode(profileId)) ||
|
||||
normalizedKeyword === normalizePublicCodeText(profileId)
|
||||
);
|
||||
}
|
||||
|
||||
8
src/services/square-hole-creation/index.ts
Normal file
8
src/services/square-hole-creation/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export {
|
||||
createSquareHoleCreationSession,
|
||||
executeSquareHoleCreationAction,
|
||||
getSquareHoleCreationSession,
|
||||
sendSquareHoleCreationMessage,
|
||||
squareHoleCreationClient,
|
||||
streamSquareHoleCreationMessage,
|
||||
} from './squareHoleCreationClient';
|
||||
@@ -0,0 +1,87 @@
|
||||
import type {
|
||||
CreateSquareHoleSessionRequest,
|
||||
ExecuteSquareHoleActionRequest,
|
||||
SendSquareHoleMessageRequest,
|
||||
SquareHoleActionResponse,
|
||||
SquareHoleSessionResponse,
|
||||
SquareHoleSessionSnapshot,
|
||||
} from '../../../packages/shared/src/contracts/squareHoleAgent';
|
||||
import type { TextStreamOptions } from '../aiTypes';
|
||||
import { createCreationAgentClient } from '../creation-agent';
|
||||
|
||||
const SQUARE_HOLE_AGENT_API_BASE = '/api/creation/square-hole/sessions';
|
||||
|
||||
const squareHoleAgentHttpClient = createCreationAgentClient<
|
||||
CreateSquareHoleSessionRequest,
|
||||
SquareHoleSessionResponse,
|
||||
SquareHoleSessionResponse,
|
||||
SquareHoleSessionSnapshot,
|
||||
SendSquareHoleMessageRequest,
|
||||
SquareHoleSessionResponse,
|
||||
ExecuteSquareHoleActionRequest,
|
||||
SquareHoleActionResponse
|
||||
>({
|
||||
apiBase: SQUARE_HOLE_AGENT_API_BASE,
|
||||
messages: {
|
||||
createSession: '创建方洞挑战共创会话失败',
|
||||
getSession: '读取方洞挑战共创会话失败',
|
||||
sendMessage: '发送方洞挑战共创消息失败',
|
||||
streamIncomplete: '方洞挑战共创消息流式结果不完整',
|
||||
executeAction: '执行方洞挑战共创操作失败',
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 创建方洞挑战 Agent 共创会话。
|
||||
*/
|
||||
export function createSquareHoleCreationSession(
|
||||
payload: CreateSquareHoleSessionRequest = {},
|
||||
) {
|
||||
return squareHoleAgentHttpClient.createSession(payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取方洞挑战 Agent 会话快照。
|
||||
*/
|
||||
export function getSquareHoleCreationSession(sessionId: string) {
|
||||
return squareHoleAgentHttpClient.getSession(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 非流式发送方洞挑战 Agent 消息,保留为 SSE 降级入口。
|
||||
*/
|
||||
export function sendSquareHoleCreationMessage(
|
||||
sessionId: string,
|
||||
payload: SendSquareHoleMessageRequest,
|
||||
) {
|
||||
return squareHoleAgentHttpClient.sendMessage(sessionId, payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* 流式发送方洞挑战 Agent 消息。
|
||||
*/
|
||||
export function streamSquareHoleCreationMessage(
|
||||
sessionId: string,
|
||||
payload: SendSquareHoleMessageRequest,
|
||||
options: TextStreamOptions = {},
|
||||
) {
|
||||
return squareHoleAgentHttpClient.streamMessage(sessionId, payload, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行方洞挑战创作操作,例如生成草稿作品。
|
||||
*/
|
||||
export function executeSquareHoleCreationAction(
|
||||
sessionId: string,
|
||||
payload: ExecuteSquareHoleActionRequest,
|
||||
) {
|
||||
return squareHoleAgentHttpClient.executeAction(sessionId, payload);
|
||||
}
|
||||
|
||||
export const squareHoleCreationClient = {
|
||||
createSession: createSquareHoleCreationSession,
|
||||
getSession: getSquareHoleCreationSession,
|
||||
sendMessage: sendSquareHoleCreationMessage,
|
||||
streamMessage: streamSquareHoleCreationMessage,
|
||||
executeAction: executeSquareHoleCreationAction,
|
||||
};
|
||||
9
src/services/square-hole-runtime/index.ts
Normal file
9
src/services/square-hole-runtime/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export {
|
||||
dropSquareHoleShape,
|
||||
finishSquareHoleTimeUp,
|
||||
getSquareHoleRun,
|
||||
restartSquareHoleRun,
|
||||
squareHoleRuntimeClient,
|
||||
startSquareHoleRun,
|
||||
stopSquareHoleRun,
|
||||
} from './squareHoleRuntimeClient';
|
||||
123
src/services/square-hole-runtime/squareHoleRuntimeClient.ts
Normal file
123
src/services/square-hole-runtime/squareHoleRuntimeClient.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import type {
|
||||
DropSquareHoleShapeRequest,
|
||||
SquareHoleDropResponse,
|
||||
SquareHoleRunResponse,
|
||||
StopSquareHoleRunRequest,
|
||||
} from '../../../packages/shared/src/contracts/squareHoleRuntime';
|
||||
import { type ApiRetryOptions, requestJson } from '../apiClient';
|
||||
|
||||
const SQUARE_HOLE_RUNTIME_READ_RETRY: ApiRetryOptions = {
|
||||
maxRetries: 1,
|
||||
baseDelayMs: 120,
|
||||
maxDelayMs: 360,
|
||||
};
|
||||
const SQUARE_HOLE_RUNTIME_WRITE_RETRY: ApiRetryOptions = {
|
||||
maxRetries: 1,
|
||||
baseDelayMs: 120,
|
||||
maxDelayMs: 360,
|
||||
retryUnsafeMethods: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* 基于作品启动一局方洞挑战正式 run。
|
||||
*/
|
||||
export function startSquareHoleRun(profileId: string) {
|
||||
return requestJson<SquareHoleRunResponse>(
|
||||
`/api/runtime/square-hole/works/${encodeURIComponent(profileId)}/runs`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ profileId }),
|
||||
},
|
||||
'启动方洞挑战失败',
|
||||
{ retry: SQUARE_HOLE_RUNTIME_WRITE_RETRY },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取方洞挑战运行态快照。
|
||||
*/
|
||||
export function getSquareHoleRun(runId: string) {
|
||||
return requestJson<SquareHoleRunResponse>(
|
||||
`/api/runtime/square-hole/runs/${encodeURIComponent(runId)}`,
|
||||
{ method: 'GET' },
|
||||
'读取方洞挑战运行快照失败',
|
||||
{ retry: SQUARE_HOLE_RUNTIME_READ_RETRY },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交一次洞口选择,由后端做权威确认。
|
||||
*/
|
||||
export function dropSquareHoleShape(
|
||||
runId: string,
|
||||
payload: DropSquareHoleShapeRequest,
|
||||
) {
|
||||
return requestJson<SquareHoleDropResponse>(
|
||||
`/api/runtime/square-hole/runs/${encodeURIComponent(runId)}/drop`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
...payload,
|
||||
runId: payload.runId ?? runId,
|
||||
}),
|
||||
},
|
||||
'确认方洞挑战投入失败',
|
||||
{ retry: SQUARE_HOLE_RUNTIME_WRITE_RETRY },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止当前方洞挑战运行态。
|
||||
*/
|
||||
export function stopSquareHoleRun(
|
||||
runId: string,
|
||||
payload: StopSquareHoleRunRequest = {
|
||||
clientActionId: `square-hole-stop-${Date.now()}`,
|
||||
},
|
||||
) {
|
||||
return requestJson<SquareHoleRunResponse>(
|
||||
`/api/runtime/square-hole/runs/${encodeURIComponent(runId)}/stop`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
'停止方洞挑战失败',
|
||||
{ retry: SQUARE_HOLE_RUNTIME_WRITE_RETRY },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于当前 run 重开一局。
|
||||
*/
|
||||
export function restartSquareHoleRun(runId: string) {
|
||||
return requestJson<SquareHoleRunResponse>(
|
||||
`/api/runtime/square-hole/runs/${encodeURIComponent(runId)}/restart`,
|
||||
{ method: 'POST' },
|
||||
'重新开始方洞挑战失败',
|
||||
{ retry: SQUARE_HOLE_RUNTIME_WRITE_RETRY },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 前端倒计时归零后通知后端确认失败状态。
|
||||
*/
|
||||
export function finishSquareHoleTimeUp(runId: string) {
|
||||
return requestJson<SquareHoleRunResponse>(
|
||||
`/api/runtime/square-hole/runs/${encodeURIComponent(runId)}/time-up`,
|
||||
{ method: 'POST' },
|
||||
'同步方洞挑战倒计时失败',
|
||||
{ retry: SQUARE_HOLE_RUNTIME_WRITE_RETRY },
|
||||
);
|
||||
}
|
||||
|
||||
export const squareHoleRuntimeClient = {
|
||||
dropShape: dropSquareHoleShape,
|
||||
finishTimeUp: finishSquareHoleTimeUp,
|
||||
getRun: getSquareHoleRun,
|
||||
restartRun: restartSquareHoleRun,
|
||||
startRun: startSquareHoleRun,
|
||||
stopRun: stopSquareHoleRun,
|
||||
};
|
||||
9
src/services/square-hole-works/index.ts
Normal file
9
src/services/square-hole-works/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export {
|
||||
deleteSquareHoleWork,
|
||||
getSquareHoleWorkDetail,
|
||||
listSquareHoleGallery,
|
||||
listSquareHoleWorks,
|
||||
publishSquareHoleWork,
|
||||
squareHoleWorksClient,
|
||||
updateSquareHoleWork,
|
||||
} from './squareHoleWorksClient';
|
||||
113
src/services/square-hole-works/squareHoleWorksClient.ts
Normal file
113
src/services/square-hole-works/squareHoleWorksClient.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import type {
|
||||
PutSquareHoleWorkRequest,
|
||||
SquareHoleWorkDetailResponse,
|
||||
SquareHoleWorkMutationResponse,
|
||||
SquareHoleWorksResponse,
|
||||
} from '../../../packages/shared/src/contracts/squareHoleWorks';
|
||||
import { type ApiRetryOptions, requestJson } from '../apiClient';
|
||||
|
||||
const SQUARE_HOLE_WORKS_API_BASE = '/api/creation/square-hole/works';
|
||||
const SQUARE_HOLE_GALLERY_API_BASE = '/api/runtime/square-hole/gallery';
|
||||
const SQUARE_HOLE_WORKS_READ_RETRY: ApiRetryOptions = {
|
||||
maxRetries: 1,
|
||||
baseDelayMs: 120,
|
||||
maxDelayMs: 360,
|
||||
};
|
||||
const SQUARE_HOLE_WORKS_WRITE_RETRY: ApiRetryOptions = {
|
||||
maxRetries: 1,
|
||||
baseDelayMs: 120,
|
||||
maxDelayMs: 360,
|
||||
retryUnsafeMethods: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* 读取当前用户的方洞挑战作品列表。
|
||||
*/
|
||||
export function listSquareHoleWorks() {
|
||||
return requestJson<SquareHoleWorksResponse>(
|
||||
SQUARE_HOLE_WORKS_API_BASE,
|
||||
{ method: 'GET' },
|
||||
'读取方洞挑战作品列表失败',
|
||||
{ retry: SQUARE_HOLE_WORKS_READ_RETRY },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取公开方洞挑战作品列表。
|
||||
*/
|
||||
export function listSquareHoleGallery() {
|
||||
return requestJson<SquareHoleWorksResponse>(
|
||||
SQUARE_HOLE_GALLERY_API_BASE,
|
||||
{ method: 'GET' },
|
||||
'读取方洞挑战广场失败',
|
||||
{
|
||||
retry: SQUARE_HOLE_WORKS_READ_RETRY,
|
||||
skipAuth: true,
|
||||
skipRefresh: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取方洞挑战作品详情。
|
||||
*/
|
||||
export function getSquareHoleWorkDetail(profileId: string) {
|
||||
return requestJson<SquareHoleWorkDetailResponse>(
|
||||
`${SQUARE_HOLE_WORKS_API_BASE}/${encodeURIComponent(profileId)}`,
|
||||
{ method: 'GET' },
|
||||
'读取方洞挑战作品详情失败',
|
||||
{ retry: SQUARE_HOLE_WORKS_READ_RETRY },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存结果页可编辑字段。
|
||||
*/
|
||||
export function updateSquareHoleWork(
|
||||
profileId: string,
|
||||
payload: PutSquareHoleWorkRequest,
|
||||
) {
|
||||
return requestJson<SquareHoleWorkMutationResponse>(
|
||||
`${SQUARE_HOLE_WORKS_API_BASE}/${encodeURIComponent(profileId)}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
'更新方洞挑战作品失败',
|
||||
{ retry: SQUARE_HOLE_WORKS_WRITE_RETRY },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布方洞挑战作品。发布门槛由后端最终确认。
|
||||
*/
|
||||
export function publishSquareHoleWork(profileId: string) {
|
||||
return requestJson<SquareHoleWorkMutationResponse>(
|
||||
`${SQUARE_HOLE_WORKS_API_BASE}/${encodeURIComponent(profileId)}/publish`,
|
||||
{ method: 'POST' },
|
||||
'发布方洞挑战作品失败',
|
||||
{ retry: SQUARE_HOLE_WORKS_WRITE_RETRY },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除当前用户的方洞挑战作品,并返回删除后的列表。
|
||||
*/
|
||||
export function deleteSquareHoleWork(profileId: string) {
|
||||
return requestJson<SquareHoleWorksResponse>(
|
||||
`${SQUARE_HOLE_WORKS_API_BASE}/${encodeURIComponent(profileId)}`,
|
||||
{ method: 'DELETE' },
|
||||
'删除方洞挑战作品失败',
|
||||
{ retry: SQUARE_HOLE_WORKS_WRITE_RETRY },
|
||||
);
|
||||
}
|
||||
|
||||
export const squareHoleWorksClient = {
|
||||
delete: deleteSquareHoleWork,
|
||||
getDetail: getSquareHoleWorkDetail,
|
||||
listGallery: listSquareHoleGallery,
|
||||
list: listSquareHoleWorks,
|
||||
publish: publishSquareHoleWork,
|
||||
update: updateSquareHoleWork,
|
||||
};
|
||||
Reference in New Issue
Block a user