1
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-11 15:43:32 +08:00
parent f19e482c8f
commit 0981d6ee1b
78 changed files with 1102 additions and 8510 deletions

View File

@@ -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: {

View File

@@ -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', {

View File

@@ -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>(

View File

@@ -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 '自定义世界';
}

View File

@@ -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(

View File

@@ -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) => {

View File

@@ -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

View File

@@ -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;

View File

@@ -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 '自定义世界';
}

View File

@@ -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