接通custom world session的STDB请求级bridge

This commit is contained in:
2026-04-20 13:13:11 +00:00
parent 3f92380ec9
commit eadfd785d1
7 changed files with 677 additions and 13 deletions

View File

@@ -79,7 +79,7 @@
### 4.2 Custom World
1. 先把 `session / works / profile` 各自 capability 固化
2.`custom world session` 提供 STDB provider
2.`custom world session` 提供 STDB provider,并采用“按请求切换 provider”的桥接方式不把 STDB token 依赖塞回全局 context
3.`agent session` 的会话存取提供 STDB provider
4. 保留 `LLM / asset / stream` 在 orchestrator 边界
5. 等会话和作品真源稳定后,再搬纯状态推导逻辑

View File

@@ -11,6 +11,10 @@ import { createApp } from './app.ts';
import type { AppConfig } from './config.ts';
import { prepareEventStreamResponse } from './http.ts';
import { requestIdMiddleware } from './middleware/requestId.ts';
import {
resetCustomWorldSessionBridgeConnectorForTest,
setCustomWorldSessionBridgeConnectorForTest,
} from './modules/runtime/customWorldSessionSpacetimeBridge.js';
import { createAppContext } from './server.ts';
import { createCustomWorldRuntimeProvider } from './services/customWorldRuntimeProvider.js';
import { createTestCustomWorldAgentSingleTurnLlmClient } from './services/customWorldAgentTestHelpers.js';
@@ -524,6 +528,19 @@ function withBearer(token: string, init: TestRequestInit = {}) {
} satisfies TestRequestInit;
}
function withCustomWorldSpacetimeBearer(
token: string,
init: TestRequestInit = {},
) {
return {
...withBearer(token, init),
headers: {
...(withBearer(token, init).headers ?? {}),
'x-genarrative-custom-world-auth': 'spacetime-token',
},
} satisfies TestRequestInit;
}
test('legacy json responses remain compatible and include response metadata headers', async () => {
await withTestServer('legacy-http', async ({ baseUrl }) => {
const requestId = 'req-legacy-http';
@@ -1890,6 +1907,94 @@ test('runtime persistence is isolated by user', async () => {
});
});
test('custom world session supports spacetime auth header over http', async () => {
await withTestServer('custom-world-session-spacetime-http', async ({ baseUrl }) => {
const storedSessions = new Map<
string,
{
sessionId: string;
payloadJson: string;
createdAtMs: bigint;
updatedAtMs: bigint;
}
>();
setCustomWorldSessionBridgeConnectorForTest(async () => ({
db: {
my_auth_state: {
iter: () =>
[
{
accountId: 'user_1',
},
][Symbol.iterator](),
},
my_custom_world_sessions: {
iter: () => [...storedSessions.values()][Symbol.iterator](),
},
},
procedures: {
async upsertCustomWorldSession(input) {
storedSessions.set(String(input.sessionId), {
sessionId: String(input.sessionId),
payloadJson: String(input.payloadJson),
createdAtMs: input.createdAtMs as bigint,
updatedAtMs: input.updatedAtMs as bigint,
});
return {
ok: true,
message: 'ok',
};
},
},
disconnect() {},
}));
const entry = await authEntry(
baseUrl,
'custom_world_stdb_user',
'secret123',
);
const createResponse = await httpRequest(
`${baseUrl}/api/runtime/custom-world/sessions`,
withCustomWorldSpacetimeBearer(entry.token, {
method: 'POST',
body: JSON.stringify({
settingText: '一个被潮雾切开的列岛世界。',
creatorIntent: {
worldHook: '潮雾会改变海图与记忆。',
},
generationMode: 'fast',
}),
}),
);
const created = (await createResponse.json()) as {
sessionId: string;
status: string;
questions: Array<{ id: string }>;
};
assert.equal(createResponse.status, 200);
assert.ok(created.sessionId);
const getResponse = await httpRequest(
`${baseUrl}/api/runtime/custom-world/sessions/${encodeURIComponent(created.sessionId)}`,
withCustomWorldSpacetimeBearer(entry.token),
);
const session = (await getResponse.json()) as {
sessionId: string;
settingText: string;
generationMode: string;
};
assert.equal(getResponse.status, 200);
assert.equal(session.sessionId, created.sessionId);
assert.equal(session.settingText, '一个被潮雾切开的列岛世界。');
assert.equal(session.generationMode, 'fast');
});
resetCustomWorldSessionBridgeConnectorForTest();
});
test('profile dashboard aggregates wallet, play time and played works at the account level', async () => {
await withTestServer('profile-dashboard', async ({ baseUrl }) => {
const user = await authEntry(baseUrl, 'dashboard_user', 'secret123');

View File

@@ -0,0 +1,252 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import type { Request } from 'express';
import type { AppConfig } from '../../config.js';
import type { CustomWorldSessionRecord } from '../../../../packages/shared/src/contracts/runtime.js';
import {
authenticateCustomWorldSessionViaSpacetime,
createCustomWorldSessionRepository,
resetCustomWorldSessionBridgeConnectorForTest,
setCustomWorldSessionBridgeConnectorForTest,
shouldUseSpacetimeCustomWorldSessionAuth,
} from './customWorldSessionSpacetimeBridge.js';
function createTestConfig(): AppConfig {
return {
nodeEnv: 'test',
projectRoot: '/tmp/genarrative-test',
publicDir: '/tmp/genarrative-test/public',
logsDir: '/tmp/genarrative-test/logs',
dataDir: '/tmp/genarrative-test/data',
rawEnv: {},
databaseUrl: 'pg-mem://genarrative-test',
serverAddr: ':0',
logLevel: 'silent',
editorApiEnabled: true,
assetsApiEnabled: true,
jwtSecret: 'test-secret',
jwtExpiresIn: '7d',
jwtIssuer: 'genarrative-test',
llm: {
baseUrl: 'https://example.invalid',
apiKey: '',
model: 'test-model',
},
dashScope: {
baseUrl: 'https://example.invalid',
apiKey: '',
imageModel: 'test-image-model',
requestTimeoutMs: 1000,
},
smsAuth: {
enabled: true,
provider: 'mock',
endpoint: 'dypnsapi.aliyuncs.com',
accessKeyId: '',
accessKeySecret: '',
signName: 'Test Sign',
templateCode: '100001',
templateParamKey: 'code',
countryCode: '86',
schemeName: '',
codeLength: 6,
codeType: 1,
validTimeSeconds: 300,
intervalSeconds: 60,
duplicatePolicy: 1,
caseAuthPolicy: 1,
returnVerifyCode: false,
mockVerifyCode: '123456',
maxSendPerPhonePerDay: 20,
maxSendPerIpPerHour: 30,
maxVerifyFailuresPerPhonePerHour: 12,
maxVerifyFailuresPerIpPerHour: 24,
captchaTtlSeconds: 180,
captchaTriggerVerifyFailuresPerPhone: 3,
captchaTriggerVerifyFailuresPerIp: 5,
blockPhoneFailureThreshold: 6,
blockIpFailureThreshold: 10,
blockPhoneDurationMinutes: 30,
blockIpDurationMinutes: 30,
},
wechatAuth: {
enabled: true,
provider: 'mock',
appId: '',
appSecret: '',
authorizeEndpoint: 'https://open.weixin.qq.com/connect/qrconnect',
accessTokenEndpoint: 'https://api.weixin.qq.com/sns/oauth2/access_token',
userInfoEndpoint: 'https://api.weixin.qq.com/sns/userinfo',
callbackPath: '/api/auth/wechat/callback',
defaultRedirectPath: '/',
mockUserId: 'mock_wechat_user',
mockUnionId: 'mock_wechat_union',
mockDisplayName: '微信旅人',
mockAvatarUrl: '',
},
authSession: {
refreshCookieName: 'genarrative_refresh_session',
refreshSessionTtlDays: 30,
refreshCookieSecure: false,
refreshCookieSameSite: 'Lax',
refreshCookiePath: '/api/auth',
},
spacetime: {
uri: 'ws://127.0.0.1:3000',
databaseName: 'genarrative-test',
},
};
}
function createRequest(headers: Record<string, string> = {}) {
const normalizedHeaders = Object.fromEntries(
Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value]),
);
return {
header(name: string) {
return normalizedHeaders[name.toLowerCase()] ?? undefined;
},
ip: '127.0.0.1',
userId: undefined,
customWorldSessionAuthMode: undefined,
} as unknown as Request;
}
function createSessionRecord(): CustomWorldSessionRecord {
return {
sessionId: 'cw-session-01',
status: 'ready_to_generate',
settingText: '潮雾列岛',
creatorIntent: null,
generationMode: 'fast',
questions: [],
createdAt: '2026-04-20T00:00:00.000Z',
updatedAt: '2026-04-20T00:01:00.000Z',
};
}
test('shouldUseSpacetimeCustomWorldSessionAuth only accepts the custom world spacetime marker', () => {
assert.equal(
shouldUseSpacetimeCustomWorldSessionAuth(
createRequest({
'x-genarrative-custom-world-auth': 'spacetime-token',
}),
),
true,
);
assert.equal(
shouldUseSpacetimeCustomWorldSessionAuth(
createRequest({
'x-genarrative-custom-world-auth': 'http-access-token',
}),
),
false,
);
});
test('authenticateCustomWorldSessionViaSpacetime resolves account id from bridge connection', async () => {
setCustomWorldSessionBridgeConnectorForTest(async () => ({
db: {
my_auth_state: {
iter: () =>
[
{
accountId: 'stdb-account-02',
},
][Symbol.iterator](),
},
},
disconnect() {},
}));
const request = createRequest({
authorization: 'Bearer stdb-token',
});
await authenticateCustomWorldSessionViaSpacetime(request, {
config: createTestConfig(),
});
assert.equal(request.userId, 'stdb-account-02');
assert.equal(request.customWorldSessionAuthMode, 'spacetime');
resetCustomWorldSessionBridgeConnectorForTest();
});
test('createCustomWorldSessionRepository uses STDB rows when request is marked as spacetime', async () => {
let savedInput: Record<string, unknown> | null = null;
const sessionRecord = createSessionRecord();
const payloadJson = JSON.stringify(sessionRecord);
setCustomWorldSessionBridgeConnectorForTest(async () => ({
db: {
my_custom_world_sessions: {
iter: () =>
[
{
sessionId: sessionRecord.sessionId,
payloadJson,
createdAtMs: BigInt(Date.parse(sessionRecord.createdAt)),
updatedAtMs: BigInt(Date.parse(sessionRecord.updatedAt)),
},
][Symbol.iterator](),
},
},
procedures: {
async upsertCustomWorldSession(input: Record<string, unknown>) {
savedInput = input;
return {
ok: true,
message: 'ok',
};
},
},
disconnect() {},
}));
const request = createRequest({
authorization: 'Bearer stdb-token',
'user-agent': 'test-agent',
});
request.customWorldSessionAuthMode = 'spacetime';
const repository = createCustomWorldSessionRepository({
request,
config: createTestConfig(),
runtimeRepository: {
async listCustomWorldSessions() {
throw new Error('should not hit legacy repository');
},
async getCustomWorldSession() {
throw new Error('should not hit legacy repository');
},
async upsertCustomWorldSession() {
throw new Error('should not hit legacy repository');
},
},
});
const sessions = await repository.listCustomWorldSessions('ignored-user-id');
assert.equal(sessions.length, 1);
assert.equal(sessions[0]?.sessionId, sessionRecord.sessionId);
const loaded = await repository.getCustomWorldSession(
'ignored-user-id',
sessionRecord.sessionId,
);
assert.equal(loaded?.settingText, sessionRecord.settingText);
const persisted = await repository.upsertCustomWorldSession(
'ignored-user-id',
sessionRecord.sessionId,
sessionRecord,
);
assert.equal(persisted.sessionId, sessionRecord.sessionId);
assert.equal(savedInput?.sessionId, sessionRecord.sessionId);
assert.equal(savedInput?.payloadJson, payloadJson);
resetCustomWorldSessionBridgeConnectorForTest();
});

View File

@@ -0,0 +1,222 @@
import type { Request } from 'express';
import { DbConnection } from '../../../../src/spacetime/generated/index.ts';
import type {
CustomWorldSessionView,
RequestMeta,
} from '../../../../src/spacetime/generated/types.ts';
import { conflict, unauthorized } from '../../errors.js';
import type { AppConfig } from '../../config.js';
import type { CustomWorldSessionRecord } from '../../../../packages/shared/src/contracts/runtime.js';
import type { CustomWorldSessionCapability } from '../../services/runtimeCapabilities.js';
const CUSTOM_WORLD_SESSION_STDB_AUTH_HEADER =
'x-genarrative-custom-world-auth';
type CustomWorldSessionStdbBridgeContext = {
config: AppConfig;
};
type CustomWorldSessionBridgeDbConnection = Awaited<
ReturnType<typeof connectWithToken>
>;
function readBearerToken(request: Request) {
const authorization = request.header('authorization')?.trim() || '';
if (!authorization.startsWith('Bearer ')) {
return '';
}
return authorization.slice('Bearer '.length).trim();
}
function buildRequestMeta(request: Request): RequestMeta {
const userAgent = request.header('user-agent')?.trim() || undefined;
const forwardedFor = request.header('x-forwarded-for')?.split(',')[0]?.trim();
const ip = forwardedFor || request.ip || undefined;
return {
clientType: 'server-node-custom-world-session-bridge',
userAgent,
ip,
};
}
function parseCustomWorldSessionRow(
row: CustomWorldSessionView,
): CustomWorldSessionRecord | null {
try {
const payload = JSON.parse(row.payloadJson) as CustomWorldSessionRecord;
return {
...payload,
sessionId: row.sessionId,
createdAt: new Date(Number(row.createdAtMs)).toISOString(),
updatedAt: new Date(Number(row.updatedAtMs)).toISOString(),
};
} catch {
return null;
}
}
async function connectWithToken(config: AppConfig, token: string) {
return new Promise<DbConnection>((resolve, reject) => {
let settled = false;
const finish = (callback: () => void) => {
if (settled) {
return;
}
settled = true;
callback();
};
DbConnection.builder()
.withUri(config.spacetime.uri)
.withDatabaseName(config.spacetime.databaseName)
.withLightMode(true)
.withToken(token)
.onConnect((connection) => {
connection
.subscriptionBuilder()
.onApplied(() => {
finish(() => {
resolve(connection);
});
})
.onError((_ctx, error) => {
finish(() => {
connection.disconnect();
reject(error);
});
})
.subscribeToAllTables();
})
.onConnectError((_ctx, error) => {
finish(() => reject(error));
})
.onDisconnect((_ctx, error) => {
if (!settled) {
finish(() => reject(error ?? new Error('Spacetime 连接已断开')));
}
})
.build();
});
}
async function withBridgeConnection<T>(
config: AppConfig,
token: string,
run: (connection: CustomWorldSessionBridgeDbConnection) => Promise<T>,
) {
const connection = await customWorldSessionBridgeConnector(config, token);
try {
return await run(connection);
} finally {
connection.disconnect();
}
}
let customWorldSessionBridgeConnector = connectWithToken;
export function setCustomWorldSessionBridgeConnectorForTest(
connector: typeof connectWithToken,
) {
customWorldSessionBridgeConnector = connector;
}
export function resetCustomWorldSessionBridgeConnectorForTest() {
customWorldSessionBridgeConnector = connectWithToken;
}
export function shouldUseSpacetimeCustomWorldSessionAuth(request: Request) {
return (
request.header(CUSTOM_WORLD_SESSION_STDB_AUTH_HEADER)?.trim() ===
'spacetime-token'
);
}
export async function authenticateCustomWorldSessionViaSpacetime(
request: Request,
context: CustomWorldSessionStdbBridgeContext,
) {
const token = readBearerToken(request);
if (!token) {
throw unauthorized('缺少 custom world session 兼容认证 token');
}
const accountId = await withBridgeConnection(
context.config,
token,
async (connection) => {
const row = Array.from(connection.db.my_auth_state.iter())[0] ?? null;
const nextAccountId = row?.accountId?.trim() || '';
if (!nextAccountId) {
throw unauthorized('custom world session STDB 账号态不存在');
}
return nextAccountId;
},
);
request.userId = accountId;
request.customWorldSessionAuthMode = 'spacetime';
}
export function createCustomWorldSessionRepository(params: {
request: Request;
runtimeRepository: CustomWorldSessionCapability;
config: AppConfig;
}): CustomWorldSessionCapability {
if (params.request.customWorldSessionAuthMode !== 'spacetime') {
return params.runtimeRepository;
}
const token = readBearerToken(params.request);
if (!token) {
throw unauthorized('缺少 custom world session STDB token');
}
const requestMeta = buildRequestMeta(params.request);
return {
async listCustomWorldSessions() {
return withBridgeConnection(params.config, token, async (connection) => {
return Array.from(connection.db.my_custom_world_sessions.iter())
.map(parseCustomWorldSessionRow)
.filter((row): row is CustomWorldSessionRecord => Boolean(row))
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
});
},
async getCustomWorldSession(_userId: string, sessionId: string) {
return withBridgeConnection(params.config, token, async (connection) => {
const row =
Array.from(connection.db.my_custom_world_sessions.iter()).find(
(entry) => entry.sessionId === sessionId,
) ?? null;
return row ? parseCustomWorldSessionRow(row) : null;
});
},
async upsertCustomWorldSession(_userId: string, sessionId: string, session) {
return withBridgeConnection(params.config, token, async (connection) => {
const result = await connection.procedures.upsertCustomWorldSession({
meta: requestMeta,
sessionId,
payloadJson: JSON.stringify(session),
createdAtMs: BigInt(Date.parse(session.createdAt) || Date.now()),
updatedAtMs: BigInt(Date.parse(session.updatedAt) || Date.now()),
});
if (!result.ok) {
throw conflict(result.message || 'custom world session STDB 写入失败');
}
const row =
Array.from(connection.db.my_custom_world_sessions.iter()).find(
(entry) => entry.sessionId === sessionId,
) ?? null;
const parsedRow = row ? parseCustomWorldSessionRow(row) : null;
if (!parsedRow) {
throw conflict('custom world session STDB 写入后未返回最新会话');
}
return parsedRow;
});
},
};
}

View File

@@ -1,4 +1,4 @@
import { Router } from 'express';
import { Router, type Request } from 'express';
import { z } from 'zod';
import type { ListCustomWorldWorksResponse } from '../../../packages/shared/src/contracts/customWorldAgent.js';
@@ -58,6 +58,11 @@ import {
hydrateSavedSnapshot,
normalizeSavedSnapshotPayload,
} from '../modules/runtime/runtimeSnapshotHydration.js';
import {
authenticateCustomWorldSessionViaSpacetime,
createCustomWorldSessionRepository,
shouldUseSpacetimeCustomWorldSessionAuth,
} from '../modules/runtime/customWorldSessionSpacetimeBridge.js';
import {
characterChatReplyRequestSchema,
characterChatSuggestionsRequestSchema,
@@ -87,6 +92,7 @@ import {
generateHighQualityNextStory,
parseStoryRequest,
} from '../services/storyService.js';
import { createCustomWorldRuntimeProvider } from '../services/customWorldRuntimeProvider.js';
import { createCustomWorldAgentRoutes } from './customWorldAgent.js';
const jsonObjectSchema = z.record(z.string(), z.unknown());
@@ -195,6 +201,17 @@ export function createRuntimeRoutes(context: AppContext) {
npc: await generateSceneNpcForLandmark(context.llmClient, payload),
});
});
const createRequestScopedCustomWorldRuntime = (request: Request) =>
createCustomWorldRuntimeProvider({
customWorldSessionCapability: createCustomWorldSessionRepository({
request,
runtimeRepository: context.customWorldSessionCapability,
config: context.config,
}),
customWorldProfileCapability: context.customWorldProfileCapability,
llmClient: context.llmClient,
runtimeLlmClient: context.llmClient,
});
router.get(
'/runtime/custom-world-gallery',
@@ -231,7 +248,18 @@ export function createRuntimeRoutes(context: AppContext) {
}),
);
router.use(requireAuth);
router.use(
asyncHandler(async (request, response, next) => {
if (shouldUseSpacetimeCustomWorldSessionAuth(request)) {
await authenticateCustomWorldSessionViaSpacetime(request, context);
next();
return;
}
request.customWorldSessionAuthMode = 'jwt';
await requireAuth(request, response, next);
}),
);
router.use(
'/runtime/custom-world/agent',
createCustomWorldAgentRoutes(context),
@@ -795,12 +823,13 @@ export function createRuntimeRoutes(context: AppContext) {
'/runtime/custom-world/sessions',
routeMeta({ operation: 'runtime.customWorldSession.create' }),
asyncHandler(async (request, response) => {
const customWorldRuntime = createRequestScopedCustomWorldRuntime(request);
const payload = customWorldSessionSchema.parse(
request.body,
) as CreateCustomWorldSessionRequest;
sendApiResponse(
response,
await context.customWorldSessions.create(
await customWorldRuntime.customWorldSessions.create(
request.userId!,
payload.settingText,
payload.creatorIntent,
@@ -814,7 +843,9 @@ export function createRuntimeRoutes(context: AppContext) {
'/runtime/custom-world/sessions/:sessionId',
routeMeta({ operation: 'runtime.customWorldSession.get' }),
asyncHandler(async (request, response) => {
const session = await context.customWorldSessions.get(
const session = await createRequestScopedCustomWorldRuntime(
request,
).customWorldSessions.get(
request.userId!,
readParam(request.params.sessionId),
);
@@ -829,10 +860,11 @@ export function createRuntimeRoutes(context: AppContext) {
'/runtime/custom-world/sessions/:sessionId/answers',
routeMeta({ operation: 'runtime.customWorldSession.answer' }),
asyncHandler(async (request, response) => {
const customWorldRuntime = createRequestScopedCustomWorldRuntime(request);
const payload = customWorldAnswerSchema.parse(
request.body,
) as AnswerCustomWorldSessionQuestionRequest;
const session = await context.customWorldSessions.answer(
const session = await customWorldRuntime.customWorldSessions.answer(
request.userId!,
readParam(request.params.sessionId),
payload.questionId,
@@ -849,7 +881,8 @@ export function createRuntimeRoutes(context: AppContext) {
'/runtime/custom-world/sessions/:sessionId/generate/stream',
routeMeta({ operation: 'runtime.customWorldSession.generateStream' }),
asyncHandler(async (request, response) => {
const session = await context.customWorldSessions.get(
const customWorldRuntime = createRequestScopedCustomWorldRuntime(request);
const session = await customWorldRuntime.customWorldSessions.get(
request.userId!,
readParam(request.params.sessionId),
);
@@ -870,7 +903,7 @@ export function createRuntimeRoutes(context: AppContext) {
};
writeEvent('progress', { phase: 'preparing', progress: 10 });
await context.customWorldSessions.updateStatus(
await customWorldRuntime.customWorldSessions.updateStatus(
request.userId!,
readParam(request.params.sessionId),
'generating',
@@ -887,7 +920,7 @@ export function createRuntimeRoutes(context: AppContext) {
);
},
});
await context.customWorldSessions.setResult(
await customWorldRuntime.customWorldSessions.setResult(
request.userId!,
readParam(request.params.sessionId),
profile,
@@ -900,7 +933,7 @@ export function createRuntimeRoutes(context: AppContext) {
error instanceof Error
? error.message
: 'custom world generation failed';
await context.customWorldSessions.updateStatus(
await customWorldRuntime.customWorldSessions.updateStatus(
request.userId!,
readParam(request.params.sessionId),
'generation_error',

View File

@@ -5,6 +5,7 @@ declare global {
requestStartedAt: number;
userId?: string;
runtimeStoryAuthMode?: 'jwt' | 'spacetime';
customWorldSessionAuthMode?: 'jwt' | 'spacetime';
auth?: {
userId: string;
tokenVersion: number;

View File

@@ -51,12 +51,18 @@ import type {
StoryRequestOptions,
TextStreamOptions,
} from './aiTypes';
import { fetchWithApiAuth, requestJson } from './apiClient';
import {
fetchWithApiAuth,
getStoredAccessToken,
getStoredSpacetimeToken,
requestJson,
} from './apiClient';
import { type CharacterChatTargetStatus } from './characterChatPrompt';
import { parseLineListContent } from './llmParsers';
const RUNTIME_API_BASE = '/api/runtime';
const CUSTOM_WORLD_API_BASE = '/api';
const CUSTOM_WORLD_STDB_AUTH_HEADER = 'x-genarrative-custom-world-auth';
type LegacyAiModule = typeof import('./ai');
@@ -86,6 +92,29 @@ async function requestPostJson<T>(
);
}
function withCustomWorldAuthHeaders(headers?: HeadersInit) {
const nextHeaders =
headers instanceof Headers
? Object.fromEntries(headers.entries())
: Array.isArray(headers)
? Object.fromEntries(headers)
: { ...(headers ?? {}) };
const httpAccessToken = getStoredAccessToken();
const spacetimeToken = getStoredSpacetimeToken();
const bearerToken = spacetimeToken || httpAccessToken;
if (bearerToken) {
nextHeaders.Authorization = `Bearer ${bearerToken}`;
}
if (spacetimeToken) {
nextHeaders[CUSTOM_WORLD_STDB_AUTH_HEADER] = 'spacetime-token';
} else if (httpAccessToken) {
nextHeaders[CUSTOM_WORLD_STDB_AUTH_HEADER] = 'http-access-token';
}
return nextHeaders;
}
async function requestPlainText(
url: string,
payload: unknown,
@@ -434,6 +463,11 @@ export async function streamCustomWorldSessionGeneration(
{
method: 'GET',
signal: options.signal,
headers: withCustomWorldAuthHeaders(),
},
{
skipAuth: true,
skipRefresh: true,
},
);
if (!response.ok) {
@@ -634,10 +668,16 @@ export async function createCustomWorldSession(payload: {
`${RUNTIME_API_BASE}/custom-world/sessions`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: withCustomWorldAuthHeaders({
'Content-Type': 'application/json',
}),
body: JSON.stringify(payload satisfies CreateCustomWorldSessionRequest),
},
'创建自定义世界会话失败',
{
skipAuth: true,
skipRefresh: true,
},
);
}
@@ -847,8 +887,13 @@ export async function getCustomWorldSession(sessionId: string) {
`${RUNTIME_API_BASE}/custom-world/sessions/${encodeURIComponent(sessionId)}`,
{
method: 'GET',
headers: withCustomWorldAuthHeaders(),
},
'读取自定义世界会话失败',
{
skipAuth: true,
skipRefresh: true,
},
);
}
@@ -860,12 +905,18 @@ export async function answerCustomWorldSessionQuestion(
`${RUNTIME_API_BASE}/custom-world/sessions/${encodeURIComponent(sessionId)}/answers`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: withCustomWorldAuthHeaders({
'Content-Type': 'application/json',
}),
body: JSON.stringify(
payload satisfies AnswerCustomWorldSessionQuestionRequest,
),
},
'提交自定义世界补充设定失败',
{
skipAuth: true,
skipRefresh: true,
},
);
}