diff --git a/web/src/App.vue b/web/src/App.vue
index 7d46744..4e580c1 100644
--- a/web/src/App.vue
+++ b/web/src/App.vue
@@ -1,11 +1,9 @@
@@ -385,4 +231,4 @@ watch([showMenu, isManualPaused], ([menuVisible, manualPaused]) => {
flex-direction: column;
z-index: 20;
}
-
\ No newline at end of file
+
diff --git a/web/src/api/game.ts b/web/src/api/game.ts
deleted file mode 100644
index 0588074..0000000
--- a/web/src/api/game.ts
+++ /dev/null
@@ -1,277 +0,0 @@
-import { httpClient } from './http';
-import type {
- InitialStateDTO,
- MapResponseDTO,
- DetailResponseDTO,
- SaveFileDTO
-} from '../types/api';
-
-export interface HoverParams {
- type: string;
- id: string;
-}
-
-// --- New Types ---
-
-export interface GameDataDTO {
- sects: Array<{ id: number; name: string; alignment: string }>;
- personas: Array<{ id: number; name: string; desc: string; rarity: string }>;
- realms: string[];
- techniques: Array<{ id: number; name: string; grade: string; attribute: string; sect: string | null }>;
- weapons: Array<{ id: number; name: string; grade: string; type: string }>;
- auxiliaries: Array<{ id: number; name: string; grade: string }>;
- alignments: Array<{ value: string; label: string }>;
-}
-
-export interface SimpleAvatarDTO {
- id: string;
- name: string;
- sect_name: string;
- realm: string;
- gender: string;
- age: number;
-}
-
-export interface CreateAvatarParams {
- surname?: string;
- given_name?: string;
- gender?: string;
- age?: number;
- level?: number;
- sect_id?: number;
- persona_ids?: number[];
- pic_id?: number;
- technique_id?: number;
- weapon_id?: number;
- auxiliary_id?: number;
- alignment?: string;
- appearance?: number;
- relations?: Array<{ target_id: string; relation: string }>;
-}
-
-export interface PhenomenonDTO {
- id: number;
- name: string;
- desc: string;
- rarity: string;
- duration_years: number;
- effect_desc: string;
-}
-
-export interface LLMConfigDTO {
- base_url: string;
- api_key: string;
- model_name: string;
- fast_model_name: string;
- mode: string;
-}
-
-export interface GameStartConfigDTO {
- init_npc_num: number;
- sect_num: number;
- protagonist: string;
- npc_awakening_rate_per_month: number;
-}
-
-export interface CurrentConfigDTO {
- game: {
- init_npc_num: number;
- sect_num: number;
- npc_awakening_rate_per_month: number;
- };
- avatar: {
- protagonist: string;
- };
-}
-
-// --- Events Pagination ---
-
-export interface EventDTO {
- id: string;
- text: string;
- content: string;
- year: number;
- month: number;
- month_stamp: number;
- related_avatar_ids: string[];
- is_major: boolean;
- is_story: boolean;
- created_at: number;
-}
-
-export interface EventsResponseDTO {
- events: EventDTO[];
- next_cursor: string | null;
- has_more: boolean;
-}
-
-export interface FetchEventsParams {
- avatar_id?: string;
- avatar_id_1?: string;
- avatar_id_2?: string;
- cursor?: string;
- limit?: number;
-}
-
-export interface InitStatusDTO {
- status: 'idle' | 'pending' | 'in_progress' | 'ready' | 'error';
- phase: number;
- phase_name: string;
- progress: number;
- elapsed_seconds: number;
- error: string | null;
- llm_check_failed: boolean;
- llm_error_message: string;
-}
-
-export const gameApi = {
- // --- World State ---
-
- fetchInitialState() {
- return httpClient.get('/api/state');
- },
-
- fetchMap() {
- return httpClient.get('/api/map');
- },
-
- fetchAvatarMeta() {
- // Add timestamp to prevent caching
- return httpClient.get<{ males: number[]; females: number[] }>(`/api/meta/avatars?t=${Date.now()}`);
- },
-
- fetchPhenomenaList() {
- return httpClient.get<{ phenomena: PhenomenonDTO[] }>('/api/meta/phenomena');
- },
-
- setPhenomenon(id: number) {
- return httpClient.post('/api/control/set_phenomenon', { id });
- },
-
- // --- Information ---
-
- fetchDetailInfo(params: HoverParams) {
- const query = new URLSearchParams(Object.entries(params));
- return httpClient.get(`/api/detail?${query}`);
- },
-
- // --- Actions ---
-
- setLongTermObjective(avatarId: string, content: string) {
- return httpClient.post('/api/action/set_long_term_objective', {
- avatar_id: avatarId,
- content
- });
- },
-
- clearLongTermObjective(avatarId: string) {
- return httpClient.post('/api/action/clear_long_term_objective', {
- avatar_id: avatarId
- });
- },
-
- // --- Controls ---
-
- pauseGame() {
- return httpClient.post('/api/control/pause', {});
- },
-
- resumeGame() {
- return httpClient.post('/api/control/resume', {});
- },
-
- // --- Saves ---
-
- fetchSaves() {
- return httpClient.get<{ saves: SaveFileDTO[] }>('/api/saves');
- },
-
- saveGame(filename?: string) {
- return httpClient.post<{ status: string; filename: string }>('/api/game/save', { filename });
- },
-
- loadGame(filename: string) {
- return httpClient.post<{ status: string; message: string }>('/api/game/load', { filename });
- },
-
- // --- Avatar Management ---
-
- fetchGameData() {
- return httpClient.get('/api/meta/game_data');
- },
-
- fetchAvatarList() {
- return httpClient.get<{ avatars: SimpleAvatarDTO[] }>('/api/meta/avatar_list');
- },
-
- createAvatar(params: CreateAvatarParams) {
- return httpClient.post<{ status: string; message: string; avatar_id: string }>('/api/action/create_avatar', params);
- },
-
- deleteAvatar(avatarId: string) {
- return httpClient.post<{ status: string; message: string }>('/api/action/delete_avatar', { avatar_id: avatarId });
- },
-
- // --- LLM Config ---
-
- fetchLLMConfig() {
- return httpClient.get('/api/config/llm');
- },
-
- testLLMConnection(config: LLMConfigDTO) {
- return httpClient.post<{ status: string; message: string }>('/api/config/llm/test', config);
- },
-
- saveLLMConfig(config: LLMConfigDTO) {
- return httpClient.post<{ status: string; message: string }>('/api/config/llm/save', config);
- },
-
- fetchLLMStatus() {
- return httpClient.get<{ configured: boolean }>('/api/config/llm/status');
- },
-
- // --- Events Pagination ---
-
- fetchEvents(params: FetchEventsParams = {}) {
- const query = new URLSearchParams();
- if (params.avatar_id) query.set('avatar_id', params.avatar_id);
- if (params.avatar_id_1) query.set('avatar_id_1', params.avatar_id_1);
- if (params.avatar_id_2) query.set('avatar_id_2', params.avatar_id_2);
- if (params.cursor) query.set('cursor', params.cursor);
- if (params.limit) query.set('limit', String(params.limit));
- const qs = query.toString();
- return httpClient.get(`/api/events${qs ? '?' + qs : ''}`);
- },
-
- cleanupEvents(keepMajor = true, beforeMonthStamp?: number) {
- const query = new URLSearchParams();
- query.set('keep_major', String(keepMajor));
- if (beforeMonthStamp !== undefined) query.set('before_month_stamp', String(beforeMonthStamp));
- return httpClient.delete<{ deleted: number }>(`/api/events/cleanup?${query}`);
- },
-
- // --- Init Status ---
-
- fetchInitStatus() {
- return httpClient.get('/api/init-status');
- },
-
- startNewGame() {
- // Legacy: replaced by startGame logic usually, but kept for compatibility if needed
- return httpClient.post<{ status: string; message: string }>('/api/game/new', {});
- },
-
- reinitGame() {
- return httpClient.post<{ status: string; message: string }>('/api/control/reinit', {});
- },
-
- // --- Game Start Config ---
-
- fetchCurrentConfig() {
- return httpClient.get('/api/config/current');
- },
-
- startGame(config: GameStartConfigDTO) {
- return httpClient.post<{ status: string; message: string }>('/api/game/start', config);
- }
-};
diff --git a/web/src/api/index.ts b/web/src/api/index.ts
new file mode 100644
index 0000000..a03f31c
--- /dev/null
+++ b/web/src/api/index.ts
@@ -0,0 +1,9 @@
+// 导出子模块
+export { worldApi } from './modules/world';
+export { avatarApi, type HoverParams } from './modules/avatar';
+export { systemApi } from './modules/system';
+export { llmApi } from './modules/llm';
+export { eventApi } from './modules/event';
+
+// 保持向后兼容的聚合对象 (Optional, for transition)
+// 但这次我们直接重构,不再保留大对象,鼓励按需引用
diff --git a/web/src/api/modules/avatar.ts b/web/src/api/modules/avatar.ts
new file mode 100644
index 0000000..9152ceb
--- /dev/null
+++ b/web/src/api/modules/avatar.ts
@@ -0,0 +1,53 @@
+import { httpClient } from '../http';
+import type {
+ DetailResponseDTO,
+ SimpleAvatarDTO,
+ CreateAvatarParams,
+ GameDataDTO
+} from '../../types/api';
+
+export interface HoverParams {
+ type: string;
+ id: string;
+}
+
+export const avatarApi = {
+ fetchAvatarMeta() {
+ // Add timestamp to prevent caching
+ return httpClient.get<{ males: number[]; females: number[] }>(`/api/meta/avatars?t=${Date.now()}`);
+ },
+
+ fetchDetailInfo(params: HoverParams) {
+ const query = new URLSearchParams(Object.entries(params));
+ return httpClient.get(`/api/detail?${query}`);
+ },
+
+ setLongTermObjective(avatarId: string, content: string) {
+ return httpClient.post('/api/action/set_long_term_objective', {
+ avatar_id: avatarId,
+ content
+ });
+ },
+
+ clearLongTermObjective(avatarId: string) {
+ return httpClient.post('/api/action/clear_long_term_objective', {
+ avatar_id: avatarId
+ });
+ },
+
+ fetchGameData() {
+ return httpClient.get('/api/meta/game_data');
+ },
+
+ fetchAvatarList() {
+ return httpClient.get<{ avatars: SimpleAvatarDTO[] }>('/api/meta/avatar_list');
+ },
+
+ createAvatar(params: CreateAvatarParams) {
+ return httpClient.post<{ status: string; message: string; avatar_id: string }>('/api/action/create_avatar', params);
+ },
+
+ deleteAvatar(avatarId: string) {
+ return httpClient.post<{ status: string; message: string }>('/api/action/delete_avatar', { avatar_id: avatarId });
+ }
+};
diff --git a/web/src/api/modules/event.ts b/web/src/api/modules/event.ts
new file mode 100644
index 0000000..fcc4afe
--- /dev/null
+++ b/web/src/api/modules/event.ts
@@ -0,0 +1,25 @@
+import { httpClient } from '../http';
+import type {
+ EventsResponseDTO,
+ FetchEventsParams
+} from '../../types/api';
+
+export const eventApi = {
+ fetchEvents(params: FetchEventsParams = {}) {
+ const query = new URLSearchParams();
+ if (params.avatar_id) query.set('avatar_id', params.avatar_id);
+ if (params.avatar_id_1) query.set('avatar_id_1', params.avatar_id_1);
+ if (params.avatar_id_2) query.set('avatar_id_2', params.avatar_id_2);
+ if (params.cursor) query.set('cursor', params.cursor);
+ if (params.limit) query.set('limit', String(params.limit));
+ const qs = query.toString();
+ return httpClient.get(`/api/events${qs ? '?' + qs : ''}`);
+ },
+
+ cleanupEvents(keepMajor = true, beforeMonthStamp?: number) {
+ const query = new URLSearchParams();
+ query.set('keep_major', String(keepMajor));
+ if (beforeMonthStamp !== undefined) query.set('before_month_stamp', String(beforeMonthStamp));
+ return httpClient.delete<{ deleted: number }>(`/api/events/cleanup?${query}`);
+ }
+};
diff --git a/web/src/api/modules/llm.ts b/web/src/api/modules/llm.ts
new file mode 100644
index 0000000..b2997fe
--- /dev/null
+++ b/web/src/api/modules/llm.ts
@@ -0,0 +1,20 @@
+import { httpClient } from '../http';
+import type { LLMConfigDTO } from '../../types/api';
+
+export const llmApi = {
+ fetchConfig() {
+ return httpClient.get('/api/config/llm');
+ },
+
+ testConnection(config: LLMConfigDTO) {
+ return httpClient.post<{ status: string; message: string }>('/api/config/llm/test', config);
+ },
+
+ saveConfig(config: LLMConfigDTO) {
+ return httpClient.post<{ status: string; message: string }>('/api/config/llm/save', config);
+ },
+
+ fetchStatus() {
+ return httpClient.get<{ configured: boolean }>('/api/config/llm/status');
+ }
+};
diff --git a/web/src/api/modules/system.ts b/web/src/api/modules/system.ts
new file mode 100644
index 0000000..636bbff
--- /dev/null
+++ b/web/src/api/modules/system.ts
@@ -0,0 +1,49 @@
+import { httpClient } from '../http';
+import type {
+ SaveFileDTO,
+ InitStatusDTO,
+ GameStartConfigDTO,
+ CurrentConfigDTO
+} from '../../types/api';
+
+export const systemApi = {
+ pauseGame() {
+ return httpClient.post('/api/control/pause', {});
+ },
+
+ resumeGame() {
+ return httpClient.post('/api/control/resume', {});
+ },
+
+ fetchSaves() {
+ return httpClient.get<{ saves: SaveFileDTO[] }>('/api/saves');
+ },
+
+ saveGame(filename?: string) {
+ return httpClient.post<{ status: string; filename: string }>('/api/game/save', { filename });
+ },
+
+ loadGame(filename: string) {
+ return httpClient.post<{ status: string; message: string }>('/api/game/load', { filename });
+ },
+
+ fetchInitStatus() {
+ return httpClient.get('/api/init-status');
+ },
+
+ startNewGame() {
+ return httpClient.post<{ status: string; message: string }>('/api/game/new', {});
+ },
+
+ reinitGame() {
+ return httpClient.post<{ status: string; message: string }>('/api/control/reinit', {});
+ },
+
+ fetchCurrentConfig() {
+ return httpClient.get('/api/config/current');
+ },
+
+ startGame(config: GameStartConfigDTO) {
+ return httpClient.post<{ status: string; message: string }>('/api/game/start', config);
+ }
+};
diff --git a/web/src/api/modules/world.ts b/web/src/api/modules/world.ts
new file mode 100644
index 0000000..a136d00
--- /dev/null
+++ b/web/src/api/modules/world.ts
@@ -0,0 +1,24 @@
+import { httpClient } from '../http';
+import type {
+ InitialStateDTO,
+ MapResponseDTO,
+ PhenomenonDTO
+} from '../../types/api';
+
+export const worldApi = {
+ fetchInitialState() {
+ return httpClient.get('/api/state');
+ },
+
+ fetchMap() {
+ return httpClient.get('/api/map');
+ },
+
+ fetchPhenomenaList() {
+ return httpClient.get<{ phenomena: PhenomenonDTO[] }>('/api/meta/phenomena');
+ },
+
+ setPhenomenon(id: number) {
+ return httpClient.post('/api/control/set_phenomenon', { id });
+ }
+};
diff --git a/web/src/components/LoadingOverlay.vue b/web/src/components/LoadingOverlay.vue
index 3fb0406..d88ed60 100644
--- a/web/src/components/LoadingOverlay.vue
+++ b/web/src/components/LoadingOverlay.vue
@@ -1,6 +1,6 @@