This commit is contained in:
2026-04-22 20:14:15 +08:00
parent 0773a0d0ca
commit 0e9c286a57
205 changed files with 25790 additions and 1623 deletions

View File

@@ -0,0 +1,147 @@
import type {
BigFishActionResponse,
BigFishSessionResponse,
BigFishSessionSnapshotResponse,
CreateBigFishSessionRequest,
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';
const BIG_FISH_AGENT_API_BASE = '/api/runtime/big-fish/agent/sessions';
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,
};
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,
},
);
}
export async function getBigFishCreationSession(sessionId: string) {
return requestJson<BigFishSessionResponse>(
`${BIG_FISH_AGENT_API_BASE}/${encodeURIComponent(sessionId)}`,
{
method: 'GET',
},
'读取大鱼吃小鱼共创会话失败',
{
retry: BIG_FISH_READ_RETRY,
},
);
}
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,
},
);
}
export async function streamBigFishCreationMessage(
sessionId: string,
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: '大鱼吃小鱼共创消息流式结果不完整',
},
);
}
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;
}
export const bigFishCreationClient = {
createSession: createBigFishCreationSession,
getSession: getBigFishCreationSession,
sendMessage: sendBigFishCreationMessage,
streamMessage: streamBigFishCreationMessage,
executeAction: executeBigFishCreationAction,
};

View File

@@ -0,0 +1,8 @@
export {
bigFishCreationClient,
createBigFishCreationSession,
executeBigFishCreationAction,
getBigFishCreationSession,
sendBigFishCreationMessage,
streamBigFishCreationMessage,
} from './bigFishCreationClient';

View File

@@ -0,0 +1,68 @@
import type {
BigFishRunResponse,
SubmitBigFishInputRequest,
} from '../../../packages/shared/src/contracts/bigFish';
import { type ApiRetryOptions, requestJson } from '../apiClient';
const BIG_FISH_RUNTIME_API_BASE = '/api/runtime/big-fish';
const BIG_FISH_RUNTIME_READ_RETRY: ApiRetryOptions = {
maxRetries: 1,
baseDelayMs: 120,
maxDelayMs: 360,
};
const BIG_FISH_RUNTIME_WRITE_RETRY: ApiRetryOptions = {
maxRetries: 1,
baseDelayMs: 120,
maxDelayMs: 360,
retryUnsafeMethods: true,
};
export async function startBigFishRuntimeRun(sessionId: string) {
return requestJson<BigFishRunResponse>(
`${BIG_FISH_RUNTIME_API_BASE}/sessions/${encodeURIComponent(sessionId)}/runs`,
{
method: 'POST',
},
'启动大鱼吃小鱼测试玩法失败',
{
retry: BIG_FISH_RUNTIME_WRITE_RETRY,
},
);
}
export async function getBigFishRuntimeRun(runId: string) {
return requestJson<BigFishRunResponse>(
`${BIG_FISH_RUNTIME_API_BASE}/runs/${encodeURIComponent(runId)}`,
{
method: 'GET',
},
'读取大鱼吃小鱼运行快照失败',
{
retry: BIG_FISH_RUNTIME_READ_RETRY,
},
);
}
export async function submitBigFishRuntimeInput(
runId: string,
payload: SubmitBigFishInputRequest,
) {
return requestJson<BigFishRunResponse>(
`${BIG_FISH_RUNTIME_API_BASE}/runs/${encodeURIComponent(runId)}/input`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
},
'提交大鱼吃小鱼移动输入失败',
{
retry: BIG_FISH_RUNTIME_WRITE_RETRY,
},
);
}
export const bigFishRuntimeClient = {
startRun: startBigFishRuntimeRun,
getRun: getBigFishRuntimeRun,
submitInput: submitBigFishRuntimeInput,
};

View File

@@ -0,0 +1,6 @@
export {
bigFishRuntimeClient,
getBigFishRuntimeRun,
startBigFishRuntimeRun,
submitBigFishRuntimeInput,
} from './bigFishRuntimeClient';

View File

@@ -0,0 +1,77 @@
export type CreationAgentOperationLike = {
status?: string | null;
};
export type CreationAgentProgressCopy = {
completed?: string;
high?: string;
medium?: string;
low?: string;
initial?: string;
};
export function normalizeCreationAgentProgress(progressPercent: number) {
if (!Number.isFinite(progressPercent)) {
return 0;
}
return Math.max(0, Math.min(100, Math.round(progressPercent)));
}
export function isCreationAgentOperationBusy(
operation: CreationAgentOperationLike | null | undefined,
) {
return operation?.status === 'queued' || operation?.status === 'running';
}
export function resolveCreationAgentProgressHint(
progressPercent: number,
copy: CreationAgentProgressCopy = {},
) {
const normalizedProgress = normalizeCreationAgentProgress(progressPercent);
if (normalizedProgress >= 100) {
return copy.completed || '当前设定已经收束完成,可以进入结果页生成';
}
if (normalizedProgress >= 75) {
return copy.high || '关键锚点基本成形,正在收束成可生成草稿的版本';
}
if (normalizedProgress >= 45) {
return copy.medium || '方向已经成形,继续补齐会影响体验的关键锚点';
}
if (normalizedProgress >= 15) {
return copy.low || '先把玩家一眼能感知的核心体验钉稳';
}
return copy.initial || '先抓住这个创作品类最关键的方向';
}
export function resolveCreationAnchorStatusLabel(status: string) {
if (status === 'locked') {
return '已锁定';
}
if (status === 'confirmed') {
return '已确认';
}
if (status === 'inferred') {
return '推断中';
}
return '待补充';
}
export function createCreationAgentClientMessageId(prefix: string) {
if (
typeof crypto !== 'undefined' &&
typeof crypto.randomUUID === 'function'
) {
return crypto.randomUUID();
}
return `${prefix}-client-message-${Date.now()}`;
}

View File

@@ -0,0 +1,105 @@
import type { TextStreamOptions } from '../aiTypes';
type CreationAgentSseOptions<TSession> = TextStreamOptions & {
fallbackMessage: string;
incompleteMessage: string;
resolveSession?: (rawSession: unknown) => TSession | null;
};
function parseSseEventBlock(eventBlock: string) {
let eventName = 'message';
const dataLines: string[] = [];
for (const rawLine of eventBlock.split(/\r?\n/u)) {
const line = rawLine.trim();
if (line.startsWith('event:')) {
eventName = line.slice(6).trim() || 'message';
continue;
}
if (line.startsWith('data:')) {
dataLines.push(line.slice(5).trim());
}
}
return {
eventName,
data: dataLines.join('\n'),
};
}
function parseJsonObject(data: string) {
try {
return JSON.parse(data) as Record<string, unknown>;
} catch {
return null;
}
}
export async function readCreationAgentSessionFromSse<TSession>(
response: Response,
options: CreationAgentSseOptions<TSession>,
) {
const streamBody = response.body;
if (!streamBody) {
throw new Error('streaming response body is unavailable');
}
const reader = streamBody.getReader();
const decoder = new TextDecoder('utf-8');
const resolveSession =
options.resolveSession ??
((rawSession: unknown) => (rawSession as TSession | null) ?? null);
let buffer = '';
let finalSession: TSession | null = null;
for (;;) {
const { done, value } = await reader.read();
if (done) {
break;
}
buffer += decoder.decode(value, { stream: true });
while (buffer.includes('\n\n')) {
const boundary = buffer.indexOf('\n\n');
const eventBlock = buffer.slice(0, boundary);
buffer = buffer.slice(boundary + 2);
const { eventName, data } = parseSseEventBlock(eventBlock);
if (!data) {
continue;
}
const parsed = parseJsonObject(data);
if (eventName === 'reply_delta' && parsed) {
const text = parsed.text;
if (typeof text === 'string') {
options.onUpdate?.(text);
}
continue;
}
if (eventName === 'session' && parsed?.session) {
finalSession = resolveSession(parsed.session);
continue;
}
if (eventName === 'error' && parsed) {
const message =
typeof parsed.message === 'string' && parsed.message.trim()
? parsed.message.trim()
: options.fallbackMessage;
throw new Error(message);
}
}
}
if (!finalSession) {
throw new Error(options.incompleteMessage);
}
return finalSession;
}

View File

@@ -0,0 +1,2 @@
export * from './creationAgentProgress';
export * from './creationAgentSse';

View File

@@ -118,12 +118,17 @@ describe('resolveCustomWorldCoverPresentation', () => {
it('当第一幕图片缺失时按营地图与地标图顺序回退', () => {
const profile = createBaseProfile();
const firstSceneChapter = profile.sceneChapterBlueprints?.[0];
const firstSceneAct = firstSceneChapter?.acts[0];
if (!firstSceneChapter || !firstSceneAct) {
throw new Error('expected base profile to provide an opening scene chapter');
}
profile.sceneChapterBlueprints = [
{
...profile.sceneChapterBlueprints![0],
...firstSceneChapter,
acts: [
{
...profile.sceneChapterBlueprints![0]!.acts[0]!,
...firstSceneAct,
backgroundImageSrc: null,
backgroundAssetId: null,
},

View File

@@ -0,0 +1,5 @@
/**
* 平台入口服务通用封装。
* 先复用既有资料看板读取逻辑,但对 `platform-entry` 暴露通用命名。
*/
export { getRpgProfileDashboard as getPlatformProfileDashboard } from '../rpg-entry';

View File

@@ -0,0 +1,8 @@
export {
createPuzzleAgentSession,
executePuzzleAgentAction,
getPuzzleAgentSession,
puzzleAgentClient,
sendPuzzleAgentMessage,
streamPuzzleAgentMessage,
} from './puzzleAgentClient';

View File

@@ -0,0 +1,168 @@
import type {
PuzzleAgentActionRequest,
PuzzleAgentActionResponse,
} from '../../../packages/shared/src/contracts/puzzleAgentActions';
import type {
CreatePuzzleAgentSessionRequest,
CreatePuzzleAgentSessionResponse,
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';
const PUZZLE_AGENT_API_BASE = '/api/runtime/puzzle/agent/sessions';
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,
};
/**
* 创建拼图 Agent 共创会话。
* 首版继续走 Axum facade前端不直连 SpacetimeDB。
*/
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,
},
);
}
/**
* 读取拼图 Agent 会话快照。
*/
export async function getPuzzleAgentSession(sessionId: string) {
return requestJson<CreatePuzzleAgentSessionResponse>(
`${PUZZLE_AGENT_API_BASE}/${encodeURIComponent(sessionId)}`,
{
method: 'GET',
},
'读取拼图共创会话失败',
{
retry: PUZZLE_AGENT_READ_RETRY,
},
);
}
/**
* 非流式发送拼图 Agent 消息。
* 当前 UI 主链使用 SSE但保留普通接口便于后续降级。
*/
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,
},
);
}
/**
* 流式发送拼图 Agent 消息。
* 后端当前会先回传一段 assistant 文本,再附上最新 session 快照。
*/
export async function streamPuzzleAgentMessage(
sessionId: string,
payload: SendPuzzleAgentMessageRequest,
options: TextStreamOptions = {},
) {
const response = await openPuzzleAgentSsePost(
`${PUZZLE_AGENT_API_BASE}/${encodeURIComponent(sessionId)}/messages/stream`,
payload,
'发送拼图共创消息失败',
);
return readCreationAgentSessionFromSse<PuzzleAgentSessionSnapshot>(
response,
{
...options,
fallbackMessage: '发送拼图共创消息失败',
incompleteMessage: '拼图共创消息流式结果不完整',
},
);
}
/**
* 执行拼图结果页相关操作。
* 后端会返回 operation 记录,前端再按需刷新 session 或 works/gallery。
*/
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;
}
export const puzzleAgentClient = {
createSession: createPuzzleAgentSession,
getSession: getPuzzleAgentSession,
sendMessage: sendPuzzleAgentMessage,
streamMessage: streamPuzzleAgentMessage,
executeAction: executePuzzleAgentAction,
};

View File

@@ -0,0 +1,5 @@
export {
getPuzzleGalleryDetail,
listPuzzleGallery,
puzzleGalleryClient,
} from './puzzleGalleryClient';

View File

@@ -0,0 +1,49 @@
import type {
PuzzleWorksResponse,
PuzzleWorkSummary,
} from '../../../packages/shared/src/contracts/puzzleWorkSummary';
import { type ApiRetryOptions, requestJson } from '../apiClient';
const PUZZLE_GALLERY_API_BASE = '/api/runtime/puzzle/gallery';
const PUZZLE_GALLERY_READ_RETRY: ApiRetryOptions = {
maxRetries: 1,
baseDelayMs: 120,
maxDelayMs: 360,
};
/**
* 读取拼图广场列表。
*/
export async function listPuzzleGallery() {
return requestJson<PuzzleWorksResponse>(
PUZZLE_GALLERY_API_BASE,
{
method: 'GET',
},
'读取拼图广场失败',
{
retry: PUZZLE_GALLERY_READ_RETRY,
},
);
}
/**
* 读取拼图广场详情。
*/
export async function getPuzzleGalleryDetail(profileId: string) {
return requestJson<{ item: PuzzleWorkSummary }>(
`${PUZZLE_GALLERY_API_BASE}/${encodeURIComponent(profileId)}`,
{
method: 'GET',
},
'读取拼图广场详情失败',
{
retry: PUZZLE_GALLERY_READ_RETRY,
},
);
}
export const puzzleGalleryClient = {
getDetail: getPuzzleGalleryDetail,
list: listPuzzleGallery,
};

View File

@@ -0,0 +1,8 @@
export {
advancePuzzleNextLevel,
dragPuzzlePieceOrGroup,
getPuzzleRun,
puzzleRuntimeClient,
startPuzzleRun,
swapPuzzlePieces,
} from './puzzleRuntimeClient';

View File

@@ -0,0 +1,120 @@
import type {
DragPuzzlePieceRequest,
PuzzleRunResponse,
StartPuzzleRunRequest,
SwapPuzzlePiecesRequest,
} from '../../../packages/shared/src/contracts/puzzleRuntimeSession';
import { type ApiRetryOptions, requestJson } from '../apiClient';
const PUZZLE_RUNTIME_API_BASE = '/api/runtime/puzzle/runs';
const PUZZLE_RUNTIME_READ_RETRY: ApiRetryOptions = {
maxRetries: 1,
baseDelayMs: 120,
maxDelayMs: 360,
};
const PUZZLE_RUNTIME_WRITE_RETRY: ApiRetryOptions = {
maxRetries: 1,
baseDelayMs: 120,
maxDelayMs: 360,
retryUnsafeMethods: true,
};
/**
* 从某个已发布拼图作品开始一次 run。
*/
export async function startPuzzleRun(payload: StartPuzzleRunRequest) {
return requestJson<PuzzleRunResponse>(
PUZZLE_RUNTIME_API_BASE,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
},
'启动拼图玩法失败',
{
retry: PUZZLE_RUNTIME_WRITE_RETRY,
},
);
}
/**
* 读取拼图运行态快照。
*/
export async function getPuzzleRun(runId: string) {
return requestJson<PuzzleRunResponse>(
`${PUZZLE_RUNTIME_API_BASE}/${encodeURIComponent(runId)}`,
{
method: 'GET',
},
'读取拼图运行快照失败',
{
retry: PUZZLE_RUNTIME_READ_RETRY,
},
);
}
/**
* 提交两块交换请求。
*/
export async function swapPuzzlePieces(
runId: string,
payload: SwapPuzzlePiecesRequest,
) {
return requestJson<PuzzleRunResponse>(
`${PUZZLE_RUNTIME_API_BASE}/${encodeURIComponent(runId)}/swap`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
},
'交换拼图块失败',
{
retry: PUZZLE_RUNTIME_WRITE_RETRY,
},
);
}
/**
* 提交单块或合并块拖动请求。
*/
export async function dragPuzzlePieceOrGroup(
runId: string,
payload: DragPuzzlePieceRequest,
) {
return requestJson<PuzzleRunResponse>(
`${PUZZLE_RUNTIME_API_BASE}/${encodeURIComponent(runId)}/drag`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
},
'拖动拼图块失败',
{
retry: PUZZLE_RUNTIME_WRITE_RETRY,
},
);
}
/**
* 进入推荐出的下一关。
*/
export async function advancePuzzleNextLevel(runId: string) {
return requestJson<PuzzleRunResponse>(
`${PUZZLE_RUNTIME_API_BASE}/${encodeURIComponent(runId)}/next-level`,
{
method: 'POST',
},
'进入下一关失败',
{
retry: PUZZLE_RUNTIME_WRITE_RETRY,
},
);
}
export const puzzleRuntimeClient = {
advanceNextLevel: advancePuzzleNextLevel,
drag: dragPuzzlePieceOrGroup,
getRun: getPuzzleRun,
startRun: startPuzzleRun,
swap: swapPuzzlePieces,
};

View File

@@ -0,0 +1,6 @@
export {
getPuzzleWorkDetail,
listPuzzleWorks,
puzzleWorksClient,
updatePuzzleWork,
} from './puzzleWorksClient';

View File

@@ -0,0 +1,85 @@
import type {
PuzzleWorkDetailResponse,
PuzzleWorkMutationResponse,
PuzzleWorksResponse,
} from '../../../packages/shared/src/contracts/puzzleWorkSummary';
import { type ApiRetryOptions, requestJson } from '../apiClient';
const PUZZLE_WORKS_API_BASE = '/api/runtime/puzzle/works';
const PUZZLE_WORKS_READ_RETRY: ApiRetryOptions = {
maxRetries: 1,
baseDelayMs: 120,
maxDelayMs: 360,
};
const PUZZLE_WORKS_WRITE_RETRY: ApiRetryOptions = {
maxRetries: 1,
baseDelayMs: 120,
maxDelayMs: 360,
retryUnsafeMethods: true,
};
/**
* 读取当前用户的拼图作品列表。
*/
export async function listPuzzleWorks() {
return requestJson<PuzzleWorksResponse>(
PUZZLE_WORKS_API_BASE,
{
method: 'GET',
},
'读取拼图作品列表失败',
{
retry: PUZZLE_WORKS_READ_RETRY,
},
);
}
/**
* 读取拼图作品详情。
*/
export async function getPuzzleWorkDetail(profileId: string) {
return requestJson<PuzzleWorkDetailResponse>(
`${PUZZLE_WORKS_API_BASE}/${encodeURIComponent(profileId)}`,
{
method: 'GET',
},
'读取拼图作品详情失败',
{
retry: PUZZLE_WORKS_READ_RETRY,
},
);
}
/**
* 更新已发布或草稿态拼图作品的轻量字段。
* 只覆盖结果页约定的标题、摘要、标签与正式图。
*/
export async function updatePuzzleWork(
profileId: string,
payload: {
levelName: string;
summary: string;
themeTags: string[];
coverImageSrc?: string | null;
coverAssetId?: string | null;
},
) {
return requestJson<PuzzleWorkMutationResponse>(
`${PUZZLE_WORKS_API_BASE}/${encodeURIComponent(profileId)}`,
{
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
},
'更新拼图作品失败',
{
retry: PUZZLE_WORKS_WRITE_RETRY,
},
);
}
export const puzzleWorksClient = {
getDetail: getPuzzleWorkDetail,
list: listPuzzleWorks,
update: updatePuzzleWork,
};

View File

@@ -9,6 +9,7 @@ import type {
} from '../../../packages/shared/src';
import type { RpgAgentActionRequest } from '../../../packages/shared/src';
import type { TextStreamOptions } from '../aiTypes';
import { readCreationAgentSessionFromSse } from '../creation-agent';
import {
openRpgCreationSsePost,
requestRpgCreationPostJson,
@@ -63,84 +64,11 @@ export async function streamRpgCreationMessage(
'发送共创消息失败',
);
const streamBody = response.body;
if (!streamBody) {
throw new Error('streaming response body is unavailable');
}
const reader = streamBody.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
let finalSession: RpgAgentSessionSnapshot | null = null;
for (;;) {
const { done, value } = await reader.read();
if (done) {
break;
}
buffer += decoder.decode(value, { stream: true });
while (buffer.includes('\n\n')) {
const boundary = buffer.indexOf('\n\n');
const eventBlock = buffer.slice(0, boundary);
buffer = buffer.slice(boundary + 2);
let eventName = 'message';
const dataLines: string[] = [];
for (const rawLine of eventBlock.split(/\r?\n/u)) {
const line = rawLine.trim();
if (line.startsWith('event:')) {
eventName = line.slice(6).trim() || 'message';
continue;
}
if (line.startsWith('data:')) {
dataLines.push(line.slice(5).trim());
}
}
if (dataLines.length === 0) {
continue;
}
const data = dataLines.join('\n');
let parsed: Record<string, unknown> | null = null;
try {
parsed = JSON.parse(data) as Record<string, unknown>;
} catch {
parsed = null;
}
if (eventName === 'reply_delta' && parsed) {
const text = parsed.text;
if (typeof text === 'string') {
options.onUpdate?.(text);
}
continue;
}
if (eventName === 'session' && parsed?.session) {
finalSession = parsed.session as RpgAgentSessionSnapshot;
continue;
}
if (eventName === 'error' && parsed) {
const message =
typeof parsed.message === 'string' && parsed.message.trim()
? parsed.message.trim()
: '发送共创消息失败';
throw new Error(message);
}
}
}
if (!finalSession) {
throw new Error('共创消息流式结果不完整');
}
return finalSession;
return readCreationAgentSessionFromSse<RpgAgentSessionSnapshot>(response, {
...options,
fallbackMessage: '发送共创消息失败',
incompleteMessage: '共创消息流式结果不完整',
});
}
export async function executeRpgCreationAction(