Skip to content

Commit 9ca2728

Browse files
authored
Merge pull request #13 from whitelonng/fix/05-appinvoke-type-safety
refactor: add type-safe command interface to appInvoke
2 parents 7d8ca8b + 2b9801f commit 9ca2728

9 files changed

Lines changed: 186 additions & 78 deletions

File tree

.github/workflows/test.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ jobs:
2525
- name: Run frontend tests
2626
run: npm run test
2727

28+
- name: Build frontend
29+
run: npm run build
30+
2831
rust-test:
2932
name: Rust Tests
3033
runs-on: ubuntu-22.04

src/__tests__/runtime.test.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ describe('Runtime Utility & Bridge', () => {
110110
});
111111
globalThis.fetch = mockFetch;
112112

113-
const result = await appInvoke<any>('get_mobile_service_status');
113+
const result = await appInvoke('get_mobile_service_status');
114114
expect(result.isRunning).toBe(true);
115115
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/mobile/status', {
116116
cache: 'no-store',
@@ -128,7 +128,7 @@ describe('Runtime Utility & Bridge', () => {
128128
});
129129
globalThis.fetch = mockFetch;
130130

131-
const result = await appInvoke<any>('list_agent_sessions', { prefix: 'partner-session-' });
131+
const result = await appInvoke('list_agent_sessions', { prefix: 'partner-session-' });
132132
expect(result).toEqual([]);
133133
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/mobile/sessions?prefix=partner-session-', {
134134
cache: 'no-store',
@@ -146,7 +146,7 @@ describe('Runtime Utility & Bridge', () => {
146146
});
147147
globalThis.fetch = mockFetch;
148148

149-
await appInvoke<any>('list_agent_sessions', {
149+
await appInvoke('list_agent_sessions', {
150150
prefix: 'story-session-',
151151
sessionKind: 'story',
152152
});
@@ -164,8 +164,16 @@ describe('Runtime Utility & Bridge', () => {
164164
});
165165
globalThis.fetch = mockFetch;
166166

167-
const sessionObj = { id: 's1', title: 'test session' };
168-
const result = await appInvoke<any>('save_agent_session', { session: sessionObj });
167+
const sessionObj = {
168+
id: 's1',
169+
title: 'test session',
170+
savedAt: 0,
171+
messages: [],
172+
selectedReferenceFiles: [],
173+
selectedOutlineFile: null,
174+
todos: [],
175+
};
176+
const result = await appInvoke('save_agent_session', { session: sessionObj });
169177
expect(result.id).toBe('s1');
170178
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/mobile/sessions', {
171179
method: 'POST',
@@ -198,7 +206,7 @@ describe('Runtime Utility & Bridge', () => {
198206
characterCardId: 'card-1',
199207
selectedWorldBookId: null,
200208
};
201-
await appInvoke<any>('save_agent_session', { session: sessionObj });
209+
await appInvoke('save_agent_session', { session: sessionObj });
202210

203211
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/mobile/sessions', expect.objectContaining({
204212
method: 'POST',
@@ -214,7 +222,7 @@ describe('Runtime Utility & Bridge', () => {
214222
globalThis.fetch = mockFetch;
215223

216224
const reqBody = { messages: [] };
217-
const result = await appInvoke<any>('start_chat_completion_stream', { request: reqBody });
225+
const result = await appInvoke('start_chat_completion_stream', { request: reqBody });
218226
expect(result.runId).toBe('run-123');
219227
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/mobile/chat/start', {
220228
method: 'POST',

src/pages/MobileBond.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ const MobileBond: React.FC = () => {
138138

139139
// Reload partner store from backend to ensure latest character cards
140140
try {
141-
const partnerStoreContent = await appInvoke<string>('load_app_state', { name: 'partner-store' });
141+
const partnerStoreContent = await appInvoke('load_app_state', { name: 'partner-store' });
142142
if (partnerStoreContent) {
143143
const parsed = JSON.parse(partnerStoreContent);
144144
if (parsed.state) {
@@ -150,7 +150,7 @@ const MobileBond: React.FC = () => {
150150
}
151151

152152
try {
153-
const summaries = await appInvoke<AgentSessionSummary[]>('list_agent_sessions', { prefix: 'partner-session-' });
153+
const summaries = await appInvoke('list_agent_sessions', { prefix: 'partner-session-' });
154154
patchUiState({ sessions: summaries });
155155
} catch (err) {
156156
console.error('加载会话足迹失败:', err);
@@ -159,7 +159,7 @@ const MobileBond: React.FC = () => {
159159
}
160160

161161
try {
162-
const summaries = await appInvoke<AgentSessionSummary[]>('list_agent_sessions', {
162+
const summaries = await appInvoke('list_agent_sessions', {
163163
prefix: 'story-session-',
164164
sessionKind: 'story',
165165
});
@@ -193,7 +193,7 @@ const MobileBond: React.FC = () => {
193193
setExpandedSessionId(id);
194194
setLoadingRecord(true);
195195
try {
196-
const record = await appInvoke<AgentSessionRecord>('load_agent_session', { id });
196+
const record = await appInvoke('load_agent_session', { id });
197197
setExpandedRecord(record);
198198
} catch (err) {
199199
console.error('加载会话详情失败:', err);
@@ -211,7 +211,7 @@ const MobileBond: React.FC = () => {
211211
setExpandedAdventureId(id);
212212
setLoadingAdventureRecord(true);
213213
try {
214-
const record = await appInvoke<AgentSessionRecord>('load_agent_session', { id });
214+
const record = await appInvoke('load_agent_session', { id });
215215
setExpandedAdventureRecord(record);
216216
} catch (err) {
217217
console.error('加载冒险详情失败:', err);

src/pages/MobileChat.tsx

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { createStableContentKey } from '../utils/renderKeys';
2222
import { useStateGroup } from '../utils/reducerState';
2323
import { ensureSessionId } from '../utils/sessionIds';
2424
import { resolveSessionTitle } from '../utils/sessionTitle';
25-
import type { Message, AgentSessionSummary } from '../stores/useAgentStore';
25+
import type { AgentSessionRecord, Message } from '../stores/useAgentStore';
2626

2727
interface MobileChatUiState {
2828
isArchiveModalOpen: boolean;
@@ -172,7 +172,7 @@ const useMobileChatView = () => {
172172

173173
// Load session list on mount
174174
useEffect(() => {
175-
appInvoke<AgentSessionSummary[]>('list_agent_sessions', { prefix: 'partner-session-' })
175+
appInvoke('list_agent_sessions', { prefix: 'partner-session-' })
176176
.then((list) => setSessions(list))
177177
.catch((e) => console.error('加载会话列表失败:', e));
178178
}, [setSessions]);
@@ -185,7 +185,7 @@ const useMobileChatView = () => {
185185
setSessionId(currentSessionId);
186186
}
187187
try {
188-
const record = {
188+
const record: AgentSessionRecord = {
189189
id: currentSessionId,
190190
title,
191191
messages: list,
@@ -198,10 +198,10 @@ const useMobileChatView = () => {
198198
characterCardId: selectedCharacterCardId,
199199
selectedWorldBookId,
200200
};
201-
await appInvoke<AgentSessionSummary>('save_agent_session', { session: record });
201+
await appInvoke('save_agent_session', { session: record });
202202

203203
// Update session summary list
204-
const listRes = await appInvoke<AgentSessionSummary[]>('list_agent_sessions', { prefix: 'partner-session-' });
204+
const listRes = await appInvoke('list_agent_sessions', { prefix: 'partner-session-' });
205205
setSessions(listRes);
206206
return true;
207207
} catch (e) {
@@ -230,10 +230,10 @@ const useMobileChatView = () => {
230230
messages,
231231
finalFallback: '未命名会话',
232232
summarize: async () => {
233-
const res = await appInvoke<{ title: string }>('summarize_text', {
233+
const res = await appInvoke('summarize_text', {
234234
request: { text: chatHistoryText },
235235
});
236-
return res.title;
236+
return typeof res === 'string' ? res : res.title;
237237
},
238238
});
239239
setSessionTitle(finalTitle);
@@ -257,7 +257,7 @@ const useMobileChatView = () => {
257257
return;
258258
}
259259
try {
260-
const record = await appInvoke<any>('load_agent_session', { id });
260+
const record = await appInvoke('load_agent_session', { id });
261261
setSessionId(record.id);
262262
setSessionTitle(record.title);
263263
setMessages(record.messages || []);
@@ -285,7 +285,7 @@ const useMobileChatView = () => {
285285
if (sessionId === id) {
286286
createNewSession();
287287
}
288-
const listRes = await appInvoke<AgentSessionSummary[]>('list_agent_sessions', { prefix: 'partner-session-' });
288+
const listRes = await appInvoke('list_agent_sessions', { prefix: 'partner-session-' });
289289
setSessions(listRes);
290290
} catch (e) {
291291
message.error('删除会话失败');
@@ -371,7 +371,7 @@ const useMobileChatView = () => {
371371
}));
372372

373373
try {
374-
const { runId } = await appInvoke<{ runId: string }>('start_chat_completion_stream', {
374+
const { runId } = await appInvoke('start_chat_completion_stream', {
375375
request: {
376376
agentId: 'partnerChat',
377377
modelInterface: settings.modelInterface,
@@ -502,7 +502,7 @@ const useMobileChatView = () => {
502502
return;
503503
}
504504

505-
const result = await appInvoke<string | Record<string, any>>('analyze_character_memory', { sessionId });
505+
const result = await appInvoke('analyze_character_memory', { sessionId });
506506
const parsed = parseArchiveAnalysisResponse(result);
507507
setArchiveAnalysis(parsed);
508508
setEditedTitle(parsed.sessionTitle || parsed.recommendedSessionTitle || sessionTitle);
@@ -536,11 +536,11 @@ const useMobileChatView = () => {
536536
message.success('伴侣记忆封存成功!该会话已归档锁定。');
537537

538538
// Reload sessions
539-
const sessList = await appInvoke<AgentSessionSummary[]>('list_agent_sessions', { prefix: 'partner-session-' });
539+
const sessList = await appInvoke('list_agent_sessions', { prefix: 'partner-session-' });
540540
setSessions(sessList);
541541

542542
// Reload partner store
543-
const partnerStoreContent = await appInvoke<string>('load_app_state', { name: 'partner-store' });
543+
const partnerStoreContent = await appInvoke('load_app_state', { name: 'partner-store' });
544544
if (partnerStoreContent) {
545545
const parsed = JSON.parse(partnerStoreContent);
546546
if (parsed.state) {

src/pages/MobileHome.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { appInvoke, clearMobileToken, getMobileToken, setMobileToken } from '../
55
import { usePartnerChatStore } from '../stores/usePartnerChatStore';
66
import { usePartnerStore } from '../stores/usePartnerStore';
77
import { useStoryStore } from '../stores/useStoryStore';
8-
import type { AgentSessionSummary } from '../stores/useAgentStore';
98

109
type ConnectionStatus = 'waiting' | 'verifying' | 'verified' | 'invalid';
1110

@@ -59,10 +58,10 @@ const MobileHome: React.FC = () => {
5958
useStoryStore.persist.rehydrate(),
6059
]);
6160
const [chatSessions, storySessions] = await Promise.all([
62-
appInvoke<AgentSessionSummary[]>('list_agent_sessions', {
61+
appInvoke('list_agent_sessions', {
6362
prefix: 'partner-session-',
6463
}),
65-
appInvoke<AgentSessionSummary[]>('list_agent_sessions', {
64+
appInvoke('list_agent_sessions', {
6665
prefix: 'story-session-',
6766
sessionKind: 'story',
6867
}),

src/pages/MobileStory.tsx

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import {
2929
getStoryAllowedTools,
3030
getRolePlayCharacterName,
3131
} from './storyAgent';
32-
import type { Message, AgentSessionSummary, AgentToolEntry } from '../stores/useAgentStore';
32+
import type { AgentSessionRecord, AgentToolEntry, Message } from '../stores/useAgentStore';
3333

3434
interface MobileStoryUiState {
3535
isArchiveModalOpen: boolean;
@@ -196,7 +196,7 @@ const useMobileStoryView = () => {
196196

197197
// Load session list on mount
198198
useEffect(() => {
199-
appInvoke<AgentSessionSummary[]>('list_agent_sessions', {
199+
appInvoke('list_agent_sessions', {
200200
prefix: 'story-session-',
201201
sessionKind: 'story',
202202
})
@@ -212,7 +212,7 @@ const useMobileStoryView = () => {
212212
setSessionId(currentSessionId);
213213
}
214214
try {
215-
const record = {
215+
const record: AgentSessionRecord = {
216216
id: currentSessionId,
217217
title,
218218
messages: list,
@@ -227,10 +227,10 @@ const useMobileStoryView = () => {
227227
selectedWorldBookId,
228228
dynamicRoleLoadingEnabled,
229229
};
230-
await appInvoke<AgentSessionSummary>('save_agent_session', { session: record });
231-
230+
await appInvoke('save_agent_session', { session: record });
231+
232232
// Update session summary list
233-
const listRes = await appInvoke<AgentSessionSummary[]>('list_agent_sessions', {
233+
const listRes = await appInvoke('list_agent_sessions', {
234234
prefix: 'story-session-',
235235
sessionKind: 'story',
236236
});
@@ -262,10 +262,10 @@ const useMobileStoryView = () => {
262262
messages,
263263
finalFallback: '未命名故事',
264264
summarize: async () => {
265-
const res = await appInvoke<{ title: string }>('summarize_text', {
265+
const res = await appInvoke('summarize_text', {
266266
request: { text: chatHistoryText },
267267
});
268-
return res.title;
268+
return typeof res === 'string' ? res : res.title;
269269
},
270270
});
271271
setSessionTitle(finalTitle);
@@ -289,7 +289,7 @@ const useMobileStoryView = () => {
289289
return;
290290
}
291291
try {
292-
const record = await appInvoke<any>('load_agent_session', { id });
292+
const record = await appInvoke('load_agent_session', { id });
293293
setSessionId(record.id);
294294
setSessionTitle(record.title);
295295
setMessages(record.messages || []);
@@ -318,7 +318,7 @@ const useMobileStoryView = () => {
318318
if (sessionId === id) {
319319
createNewSession();
320320
}
321-
const listRes = await appInvoke<AgentSessionSummary[]>('list_agent_sessions', {
321+
const listRes = await appInvoke('list_agent_sessions', {
322322
prefix: 'story-session-',
323323
sessionKind: 'story',
324324
});
@@ -408,7 +408,7 @@ const useMobileStoryView = () => {
408408
const storyAgentConfig = settings.agentConfigs?.[storyAgentConfigId] || {};
409409

410410
try {
411-
const { runId } = await appInvoke<{ runId: string }>('start_chat_completion_stream', {
411+
const { runId } = await appInvoke('start_chat_completion_stream', {
412412
request: {
413413
agentId: storyAgentConfigId,
414414
modelInterface: settings.modelInterface,
@@ -630,7 +630,7 @@ const useMobileStoryView = () => {
630630

631631
const filteredCards = selectedCards.filter(card => tempSelectedCardIds.includes(card.id));
632632
const results = await Promise.all(filteredCards.map(async (card) => {
633-
const result = await appInvoke<string | Record<string, any>>('analyze_character_memory', {
633+
const result = await appInvoke('analyze_character_memory', {
634634
sessionId,
635635
characterCardId: card.id,
636636
});
@@ -692,14 +692,14 @@ const useMobileStoryView = () => {
692692
message.success('记忆封存成功!故事已锁定归档。');
693693

694694
// Reload sessions
695-
const listRes = await appInvoke<AgentSessionSummary[]>('list_agent_sessions', {
695+
const listRes = await appInvoke('list_agent_sessions', {
696696
prefix: 'story-session-',
697697
sessionKind: 'story',
698698
});
699699
setSessions(listRes);
700700

701701
// Reload partner store
702-
const partnerStoreContent = await appInvoke<string>('load_app_state', { name: 'partner-store' });
702+
const partnerStoreContent = await appInvoke('load_app_state', { name: 'partner-store' });
703703
if (partnerStoreContent) {
704704
const parsed = JSON.parse(partnerStoreContent);
705705
if (parsed.state) {

src/stores/diskStorage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export function createDiskStorage(
88
return {
99
getItem: async () => {
1010
try {
11-
const content = await appInvoke<string>('load_app_state', { name });
11+
const content = await appInvoke('load_app_state', { name });
1212
return content;
1313
} catch {
1414
if (localStorageKey) {

src/utils/bookTravelMaterials.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const fileNameFromPath = (path: string) => {
88
};
99

1010
export const resolveOutlineMaterial = async (path: string): Promise<BookTravelMaterial> => {
11-
const content = await appInvoke<string>('read_file', { path });
11+
const content = await appInvoke('read_file', { path });
1212
return {
1313
id: path,
1414
title: fileNameFromPath(path),

0 commit comments

Comments
 (0)