@@ -1,5 +1,5 @@
|
||||
import {validateWorldAttributeSchema} from '../data/attributeValidation';
|
||||
import {getPresetWorldAttributeSchema} from '../data/worldAttributeSchemas';
|
||||
import {getTemplateWorldAttributeSchema} from '../data/worldAttributeSchemas';
|
||||
import type {
|
||||
AttributeSchemaGenerationInput,
|
||||
WorldAttributeSchema,
|
||||
@@ -96,17 +96,17 @@ function buildCustomThemeSlots(input: AttributeSchemaGenerationInput) {
|
||||
|
||||
return {
|
||||
schemaName: '叙境六维',
|
||||
slots: getPresetWorldAttributeSchema(WorldType.WUXIA).slots,
|
||||
slots: getTemplateWorldAttributeSchema(WorldType.WUXIA).slots,
|
||||
};
|
||||
}
|
||||
|
||||
export function generateWorldAttributeSchema(input: AttributeSchemaGenerationInput) {
|
||||
if (input.worldType === WorldType.WUXIA) {
|
||||
return getPresetWorldAttributeSchema(WorldType.WUXIA);
|
||||
return getTemplateWorldAttributeSchema(WorldType.WUXIA);
|
||||
}
|
||||
|
||||
if (input.worldType === WorldType.XIANXIA) {
|
||||
return getPresetWorldAttributeSchema(WorldType.XIANXIA);
|
||||
return getTemplateWorldAttributeSchema(WorldType.XIANXIA);
|
||||
}
|
||||
|
||||
const generated = buildCustomThemeSlots(input);
|
||||
@@ -116,7 +116,7 @@ export function generateWorldAttributeSchema(input: AttributeSchemaGenerationInp
|
||||
if (issues.length > 0) {
|
||||
const fallbackWorldType = /仙|灵|宗门|秘境|裂界/u.test(input.settingText) ? WorldType.XIANXIA : WorldType.WUXIA;
|
||||
return {
|
||||
...getPresetWorldAttributeSchema(fallbackWorldType),
|
||||
...getTemplateWorldAttributeSchema(fallbackWorldType),
|
||||
id: `schema:custom-fallback:${input.worldName}`,
|
||||
worldId: `custom:${input.worldName}`,
|
||||
generatedFrom: {
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
createAutoAuthCredentials,
|
||||
ensureAutoAuthUser,
|
||||
getAuthAuditLogs,
|
||||
getAuthLoginOptions,
|
||||
getAuthRiskBlocks,
|
||||
getAuthSessions,
|
||||
getCaptchaChallengeFromError,
|
||||
@@ -339,6 +340,23 @@ describe('authService auto auth', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('loads available login methods for the unauthenticated login screen', async () => {
|
||||
requestJsonMock.mockResolvedValue({
|
||||
availableLoginMethods: ['phone', 'wechat'],
|
||||
});
|
||||
|
||||
const result = await getAuthLoginOptions();
|
||||
|
||||
expect(result.availableLoginMethods).toEqual(['phone', 'wechat']);
|
||||
expect(requestJsonMock).toHaveBeenCalledWith(
|
||||
'/api/auth/login-options',
|
||||
expect.objectContaining({
|
||||
method: 'GET',
|
||||
}),
|
||||
'读取登录方式失败',
|
||||
);
|
||||
});
|
||||
|
||||
it('consumes auth callback hash and stores token', () => {
|
||||
const replaceStateMock = vi.fn();
|
||||
vi.stubGlobal('window', {
|
||||
|
||||
@@ -3,8 +3,9 @@ import type {
|
||||
AuthAuditLogsResponse,
|
||||
AuthCaptchaChallenge,
|
||||
AuthEntryResponse,
|
||||
AuthLiftRiskBlockResponse,
|
||||
AuthLoginMethod,
|
||||
AuthLoginOptionsResponse,
|
||||
AuthLiftRiskBlockResponse,
|
||||
AuthLogoutAllResponse,
|
||||
AuthMeResponse,
|
||||
AuthPhoneChangeResponse,
|
||||
@@ -30,6 +31,7 @@ import {
|
||||
} from './apiClient';
|
||||
|
||||
export type { AuthUser } from '../../packages/shared/src/contracts/auth';
|
||||
export type { AuthLoginMethod } from '../../packages/shared/src/contracts/auth';
|
||||
|
||||
export type AutoAuthCredentials = {
|
||||
username: string;
|
||||
@@ -207,6 +209,16 @@ export async function startWechatLogin() {
|
||||
window.location.assign(response.authorizationUrl);
|
||||
}
|
||||
|
||||
export async function getAuthLoginOptions() {
|
||||
return requestJson<AuthLoginOptionsResponse>(
|
||||
'/api/auth/login-options',
|
||||
{
|
||||
method: 'GET',
|
||||
},
|
||||
'读取登录方式失败',
|
||||
);
|
||||
}
|
||||
|
||||
export async function authEntry(username: string, password: string) {
|
||||
const credentials = normalizeCredentials({ username, password });
|
||||
const response = await requestJson<AuthEntryResponse>(
|
||||
|
||||
@@ -59,8 +59,8 @@ export const CHARACTER_PANEL_CHAT_SUMMARY_SYSTEM_PROMPT = `总结玩家与这名
|
||||
包含当前关系气氛、态度变化,以及最近聊天里最重要的新信息、承诺、担忧或线索。`;
|
||||
|
||||
function describeWorld(world: WorldType) {
|
||||
if (world === WorldType.WUXIA) return '武侠';
|
||||
if (world === WorldType.XIANXIA) return '仙侠';
|
||||
if (world === WorldType.WUXIA) return '边城模板';
|
||||
if (world === WorldType.XIANXIA) return '灵潮模板';
|
||||
return '自定义世界';
|
||||
}
|
||||
|
||||
|
||||
@@ -126,6 +126,7 @@ export interface CustomWorldGenerationFramework {
|
||||
tone: string;
|
||||
playerGoal: string;
|
||||
templateWorldType: WorldType;
|
||||
compatibilityTemplateWorldType: WorldType;
|
||||
majorFactions: string[];
|
||||
coreConflicts: string[];
|
||||
camp: CustomWorldGenerationCampOutline;
|
||||
@@ -619,6 +620,7 @@ function buildBaseCustomWorldProfile(settingText: string): CustomWorldProfile {
|
||||
tone,
|
||||
playerGoal,
|
||||
templateWorldType,
|
||||
compatibilityTemplateWorldType: templateWorldType,
|
||||
majorFactions: [],
|
||||
coreConflicts: [summary],
|
||||
attributeSchema: generateWorldAttributeSchema({
|
||||
@@ -674,6 +676,8 @@ export function normalizeCustomWorldGenerationFramework(
|
||||
tone: fallback.tone,
|
||||
playerGoal: fallback.playerGoal,
|
||||
templateWorldType: fallback.templateWorldType,
|
||||
compatibilityTemplateWorldType:
|
||||
fallback.compatibilityTemplateWorldType ?? fallback.templateWorldType,
|
||||
majorFactions: [],
|
||||
coreConflicts: [fallback.summary],
|
||||
camp: {
|
||||
@@ -710,6 +714,7 @@ export function normalizeCustomWorldGenerationFramework(
|
||||
tone: toText(item.tone) || fallback.tone,
|
||||
playerGoal: toText(item.playerGoal) || fallback.playerGoal,
|
||||
templateWorldType,
|
||||
compatibilityTemplateWorldType: templateWorldType,
|
||||
majorFactions: normalizeTags(item.majorFactions, []),
|
||||
coreConflicts: normalizeTags(item.coreConflicts, [fallback.summary]),
|
||||
camp: normalizeCampOutline(item.camp, {
|
||||
@@ -744,6 +749,7 @@ export function buildCustomWorldRawProfileFromFramework(
|
||||
tone: framework.tone,
|
||||
playerGoal: framework.playerGoal,
|
||||
templateWorldType: framework.templateWorldType,
|
||||
compatibilityTemplateWorldType: framework.compatibilityTemplateWorldType,
|
||||
majorFactions: framework.majorFactions,
|
||||
coreConflicts: framework.coreConflicts,
|
||||
camp: {
|
||||
@@ -1136,6 +1142,7 @@ export function normalizeCustomWorldProfile(
|
||||
tone,
|
||||
playerGoal,
|
||||
templateWorldType,
|
||||
compatibilityTemplateWorldType: templateWorldType,
|
||||
majorFactions: normalizeTags(item.majorFactions, []),
|
||||
coreConflicts: normalizeTags(item.coreConflicts, [summary]),
|
||||
attributeSchema: coerceWorldAttributeSchema(
|
||||
|
||||
@@ -8,6 +8,7 @@ import { normalizeCustomWorldLandmarks } from '../data/customWorldSceneGraph';
|
||||
import { CustomWorldProfile, WorldType } from '../types';
|
||||
import { normalizeCustomWorldProfile } from './customWorld';
|
||||
import { normalizeCustomWorldOwnedSettingLayers } from './customWorldOwnedSettingLayers';
|
||||
import { resolveCustomWorldCompatibilityTemplateWorldType } from './customWorldTheme';
|
||||
import {
|
||||
buildFallbackActorNarrativeProfile,
|
||||
normalizeActorNarrativeProfile,
|
||||
@@ -161,7 +162,9 @@ export function buildExpandedCustomWorldProfile(
|
||||
description: clampText(landmark.description, 96),
|
||||
dangerLevel:
|
||||
landmark.dangerLevel ||
|
||||
(profile.templateWorldType === WorldType.XIANXIA ? 'high' : 'medium'),
|
||||
(resolveCustomWorldCompatibilityTemplateWorldType(profile) === WorldType.XIANXIA
|
||||
? 'high'
|
||||
: 'medium'),
|
||||
}));
|
||||
const landmarkIdByReference = new Map<string, string>();
|
||||
landmarkDrafts.forEach((landmark) => {
|
||||
|
||||
@@ -12,7 +12,11 @@ import {
|
||||
type SceneArchetypeBucket,
|
||||
WorldType,
|
||||
} from '../types';
|
||||
import { type CustomWorldThemeMode, detectCustomWorldThemeMode } from './customWorldTheme';
|
||||
import {
|
||||
type CustomWorldThemeMode,
|
||||
detectCustomWorldThemeMode,
|
||||
resolveCustomWorldCompatibilityTemplateWorldType,
|
||||
} from './customWorldTheme';
|
||||
import {
|
||||
buildThemePackFromWorldProfile,
|
||||
normalizeThemePack,
|
||||
@@ -407,7 +411,7 @@ function buildThemePackSeed(profile: CustomWorldProfile) {
|
||||
summary: profile.summary,
|
||||
tone: profile.tone,
|
||||
playerGoal: profile.playerGoal,
|
||||
templateWorldType: profile.templateWorldType,
|
||||
templateWorldType: resolveCustomWorldCompatibilityTemplateWorldType(profile),
|
||||
majorFactions: profile.majorFactions,
|
||||
coreConflicts: profile.coreConflicts,
|
||||
ownedSettingLayers: null,
|
||||
@@ -502,8 +506,12 @@ function compileReferenceProfile(
|
||||
}
|
||||
|
||||
function compileCompatibilityProfile(profile: CustomWorldProfile) {
|
||||
const compatibilityTemplateWorldType =
|
||||
resolveCustomWorldCompatibilityTemplateWorldType(profile);
|
||||
|
||||
return {
|
||||
legacyTemplateWorldType: profile.templateWorldType ?? WorldType.WUXIA,
|
||||
compatibilityTemplateWorldType,
|
||||
legacyTemplateWorldType: compatibilityTemplateWorldType,
|
||||
migrationVersion: OWNED_SETTING_LAYER_MIGRATION_VERSION,
|
||||
} satisfies CustomWorldCompatibilityProfile;
|
||||
}
|
||||
@@ -629,6 +637,8 @@ export function compileOwnedSettingLayersFromLegacyTemplate(
|
||||
tone: profile.tone,
|
||||
playerGoal: profile.playerGoal,
|
||||
templateWorldType: profile.templateWorldType,
|
||||
compatibilityTemplateWorldType:
|
||||
profile.compatibilityTemplateWorldType ?? profile.templateWorldType,
|
||||
ownedSettingLayers: null,
|
||||
});
|
||||
const semanticAnchor = compileSemanticAnchor(profile, mode);
|
||||
@@ -920,6 +930,12 @@ export function normalizeCustomWorldOwnedSettingLayers(
|
||||
),
|
||||
},
|
||||
compatibilityProfile: {
|
||||
compatibilityTemplateWorldType:
|
||||
compatibilityProfileItem.compatibilityTemplateWorldType === WorldType.XIANXIA
|
||||
? WorldType.XIANXIA
|
||||
: compatibilityProfileItem.compatibilityTemplateWorldType === WorldType.WUXIA
|
||||
? WorldType.WUXIA
|
||||
: fallback.compatibilityProfile?.compatibilityTemplateWorldType ?? null,
|
||||
legacyTemplateWorldType:
|
||||
compatibilityProfileItem.legacyTemplateWorldType === WorldType.XIANXIA
|
||||
? WorldType.XIANXIA
|
||||
|
||||
@@ -16,6 +16,7 @@ export function detectCustomWorldThemeMode(
|
||||
| 'tone'
|
||||
| 'playerGoal'
|
||||
| 'templateWorldType'
|
||||
| 'compatibilityTemplateWorldType'
|
||||
| 'ownedSettingLayers'
|
||||
>,
|
||||
): CustomWorldThemeMode {
|
||||
@@ -45,17 +46,36 @@ export function detectCustomWorldThemeMode(
|
||||
return 'mythic';
|
||||
}
|
||||
|
||||
export function resolveCustomWorldAnchorWorldType(
|
||||
export function resolveCustomWorldCompatibilityTemplateWorldType(
|
||||
profile: Pick<
|
||||
CustomWorldProfile,
|
||||
| 'settingText'
|
||||
| 'summary'
|
||||
| 'tone'
|
||||
| 'playerGoal'
|
||||
| 'templateWorldType'
|
||||
| 'compatibilityTemplateWorldType'
|
||||
| 'ownedSettingLayers'
|
||||
>,
|
||||
> &
|
||||
Partial<
|
||||
Pick<
|
||||
CustomWorldProfile,
|
||||
'settingText' | 'summary' | 'tone' | 'playerGoal'
|
||||
>
|
||||
>,
|
||||
): WorldTemplateType {
|
||||
if (
|
||||
profile.compatibilityTemplateWorldType === WorldType.WUXIA ||
|
||||
profile.compatibilityTemplateWorldType === WorldType.XIANXIA
|
||||
) {
|
||||
return profile.compatibilityTemplateWorldType;
|
||||
}
|
||||
|
||||
const compatibilityTemplateWorldType =
|
||||
profile.ownedSettingLayers?.compatibilityProfile?.compatibilityTemplateWorldType;
|
||||
if (
|
||||
compatibilityTemplateWorldType === WorldType.WUXIA ||
|
||||
compatibilityTemplateWorldType === WorldType.XIANXIA
|
||||
) {
|
||||
return compatibilityTemplateWorldType;
|
||||
}
|
||||
|
||||
const legacyTemplateWorldType =
|
||||
profile.ownedSettingLayers?.compatibilityProfile?.legacyTemplateWorldType;
|
||||
|
||||
@@ -66,6 +86,24 @@ export function resolveCustomWorldAnchorWorldType(
|
||||
return legacyTemplateWorldType;
|
||||
}
|
||||
|
||||
const themeMode = detectCustomWorldThemeMode(profile);
|
||||
if (
|
||||
profile.templateWorldType === WorldType.WUXIA ||
|
||||
profile.templateWorldType === WorldType.XIANXIA
|
||||
) {
|
||||
return profile.templateWorldType;
|
||||
}
|
||||
|
||||
const themeMode = detectCustomWorldThemeMode({
|
||||
settingText: profile.settingText ?? '',
|
||||
summary: profile.summary ?? '',
|
||||
tone: profile.tone ?? '',
|
||||
playerGoal: profile.playerGoal ?? '',
|
||||
templateWorldType: profile.templateWorldType ?? WorldType.WUXIA,
|
||||
compatibilityTemplateWorldType: profile.compatibilityTemplateWorldType,
|
||||
ownedSettingLayers: profile.ownedSettingLayers,
|
||||
});
|
||||
return themeMode === 'arcane' ? WorldType.XIANXIA : WorldType.WUXIA;
|
||||
}
|
||||
|
||||
export const resolveCustomWorldAnchorWorldType =
|
||||
resolveCustomWorldCompatibilityTemplateWorldType;
|
||||
|
||||
@@ -469,8 +469,8 @@ function describeAnimationLabel(animation: string | null | undefined) {
|
||||
}
|
||||
|
||||
export function describeWorld(world: WorldType) {
|
||||
if (world === WorldType.WUXIA) return '武侠';
|
||||
if (world === WorldType.XIANXIA) return '仙侠';
|
||||
if (world === WorldType.WUXIA) return '边城模板';
|
||||
if (world === WorldType.XIANXIA) return '灵潮模板';
|
||||
return '自定义世界';
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ import { buildQuestVisibilitySlice } from './storyEngine/visibilityEngine';
|
||||
function describeWorld(worldType: QuestGenerationContext['worldType']) {
|
||||
switch (worldType) {
|
||||
case 'WUXIA':
|
||||
return '武侠';
|
||||
return '边城模板';
|
||||
case 'XIANXIA':
|
||||
return '仙侠';
|
||||
return '灵潮模板';
|
||||
case 'CUSTOM':
|
||||
return '自定义世界';
|
||||
default:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user