flare-chat-core 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/chat-core/messages/buildTimelineItems.js +3 -0
- package/src/chat-core/session/useChatSessionReducer.js +71 -1
- package/src/chat-core/stream/sse-client.js +109 -7
- package/src/chat-core/stream/sse-events.js +269 -0
- package/src/chat-core/stream/useSSEStream.js +91 -8
package/package.json
CHANGED
|
@@ -21,6 +21,9 @@ export function buildTimelineItems(state = {}, options = {}) {
|
|
|
21
21
|
role: msg.role,
|
|
22
22
|
content: msg.content,
|
|
23
23
|
createdAt: msg.created_at,
|
|
24
|
+
client_request_id: msg.client_request_id || '',
|
|
25
|
+
intake_session_id: msg.intake_session_id || '',
|
|
26
|
+
roundKey: msg.round_key || '',
|
|
24
27
|
highlights: msg.highlights ?? [],
|
|
25
28
|
sourceTypes: msg.sourceTypes ?? [],
|
|
26
29
|
attachments: Array.isArray(msg.attachments) ? msg.attachments : [],
|
|
@@ -8,6 +8,7 @@ const MESSAGES_REFRESHED = 'MESSAGES_REFRESHED';
|
|
|
8
8
|
const MESSAGE_APPENDED = 'MESSAGE_APPENDED';
|
|
9
9
|
const STREAMING_RESET = 'STREAMING_RESET';
|
|
10
10
|
const STREAMING_CHUNK = 'STREAMING_CHUNK';
|
|
11
|
+
const STREAMING_REPLACE = 'STREAMING_REPLACE';
|
|
11
12
|
const AGENT_STATUS_SET = 'AGENT_STATUS_SET';
|
|
12
13
|
const THINKING_TRACE_SET = 'THINKING_TRACE_SET';
|
|
13
14
|
const EXECUTION_TRACE_SET = 'EXECUTION_TRACE_SET';
|
|
@@ -19,6 +20,7 @@ const UI_CARDS_SET = 'UI_CARDS_SET';
|
|
|
19
20
|
const LAST_USER_MESSAGE_SET = 'LAST_USER_MESSAGE_SET';
|
|
20
21
|
const UI_CARD_UPDATED = 'UI_CARD_UPDATED';
|
|
21
22
|
const INSTANCE_PROFILE_SET = 'INSTANCE_PROFILE_SET';
|
|
23
|
+
const SESSION_PROMOTED = 'SESSION_PROMOTED';
|
|
22
24
|
|
|
23
25
|
const initialStreaming = {
|
|
24
26
|
content: '',
|
|
@@ -31,6 +33,7 @@ export const initialState = {
|
|
|
31
33
|
sessionId: null,
|
|
32
34
|
sessionTitle: '',
|
|
33
35
|
sessionStatus: 'active',
|
|
36
|
+
sessionDetail: null,
|
|
34
37
|
instanceProfile: null,
|
|
35
38
|
messages: [],
|
|
36
39
|
executionCards: [],
|
|
@@ -48,12 +51,19 @@ export const initialState = {
|
|
|
48
51
|
function reducer(state, { type, payload }) {
|
|
49
52
|
switch (type) {
|
|
50
53
|
case SESSION_LOADED: {
|
|
51
|
-
const {
|
|
54
|
+
const {
|
|
55
|
+
sessionId,
|
|
56
|
+
title,
|
|
57
|
+
status,
|
|
58
|
+
messages,
|
|
59
|
+
detail,
|
|
60
|
+
} = payload;
|
|
52
61
|
return {
|
|
53
62
|
...state,
|
|
54
63
|
sessionId,
|
|
55
64
|
sessionTitle: title,
|
|
56
65
|
sessionStatus: status || 'active',
|
|
66
|
+
sessionDetail: (detail && typeof detail === 'object' && !Array.isArray(detail)) ? detail : null,
|
|
57
67
|
instanceProfile: state.instanceProfile,
|
|
58
68
|
messages,
|
|
59
69
|
executionCards: [],
|
|
@@ -103,6 +113,40 @@ function reducer(state, { type, payload }) {
|
|
|
103
113
|
case INSTANCE_PROFILE_SET:
|
|
104
114
|
return { ...state, instanceProfile: payload };
|
|
105
115
|
|
|
116
|
+
case SESSION_PROMOTED: {
|
|
117
|
+
const sessionId = String(payload?.sessionId || '').trim();
|
|
118
|
+
if (!sessionId) {
|
|
119
|
+
return state;
|
|
120
|
+
}
|
|
121
|
+
const title = String(payload?.title || '').trim();
|
|
122
|
+
const detailPayload = (
|
|
123
|
+
payload?.detail
|
|
124
|
+
&& typeof payload.detail === 'object'
|
|
125
|
+
&& !Array.isArray(payload.detail)
|
|
126
|
+
) ? payload.detail : null;
|
|
127
|
+
const nextDetail = detailPayload
|
|
128
|
+
? {
|
|
129
|
+
...(state.sessionDetail && typeof state.sessionDetail === 'object' ? state.sessionDetail : {}),
|
|
130
|
+
...detailPayload,
|
|
131
|
+
sessionId,
|
|
132
|
+
}
|
|
133
|
+
: (
|
|
134
|
+
state.sessionDetail && typeof state.sessionDetail === 'object'
|
|
135
|
+
? {
|
|
136
|
+
...state.sessionDetail,
|
|
137
|
+
sessionId,
|
|
138
|
+
}
|
|
139
|
+
: null
|
|
140
|
+
);
|
|
141
|
+
return {
|
|
142
|
+
...state,
|
|
143
|
+
sessionId,
|
|
144
|
+
sessionTitle: title || state.sessionTitle,
|
|
145
|
+
sessionStatus: String(payload?.status || state.sessionStatus || 'active'),
|
|
146
|
+
sessionDetail: nextDetail,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
106
150
|
case EXECUTION_CARD_UPSERTED: {
|
|
107
151
|
const nextCard = payload;
|
|
108
152
|
const cardKey = nextCard.step_id || `${nextCard.agent}-${nextCard.label}-${nextCard.stage}`;
|
|
@@ -135,6 +179,15 @@ function reducer(state, { type, payload }) {
|
|
|
135
179
|
},
|
|
136
180
|
};
|
|
137
181
|
|
|
182
|
+
case STREAMING_REPLACE:
|
|
183
|
+
return {
|
|
184
|
+
...state,
|
|
185
|
+
streaming: {
|
|
186
|
+
...state.streaming,
|
|
187
|
+
content: String(payload || ''),
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
|
|
138
191
|
case AGENT_STATUS_SET:
|
|
139
192
|
return {
|
|
140
193
|
...state,
|
|
@@ -223,8 +276,10 @@ export default function useChatSessionReducer(deps = {}) {
|
|
|
223
276
|
title: detail.title,
|
|
224
277
|
status: detail.status || 'active',
|
|
225
278
|
messages: msgs.messages,
|
|
279
|
+
detail,
|
|
226
280
|
},
|
|
227
281
|
});
|
|
282
|
+
return detail;
|
|
228
283
|
}, [sessionAPI, messageAPI]);
|
|
229
284
|
|
|
230
285
|
const createSession = useCallback(async ({
|
|
@@ -242,6 +297,7 @@ export default function useChatSessionReducer(deps = {}) {
|
|
|
242
297
|
title,
|
|
243
298
|
status: response.status || 'active',
|
|
244
299
|
messages: [],
|
|
300
|
+
detail: response,
|
|
245
301
|
},
|
|
246
302
|
});
|
|
247
303
|
|
|
@@ -295,6 +351,7 @@ export default function useChatSessionReducer(deps = {}) {
|
|
|
295
351
|
title,
|
|
296
352
|
status: detail.status || 'active',
|
|
297
353
|
messages: msgs.messages,
|
|
354
|
+
detail,
|
|
298
355
|
},
|
|
299
356
|
});
|
|
300
357
|
|
|
@@ -311,6 +368,10 @@ export default function useChatSessionReducer(deps = {}) {
|
|
|
311
368
|
dispatch({ type: TITLE_UPDATED, payload: title });
|
|
312
369
|
}, [sessionAPI]);
|
|
313
370
|
|
|
371
|
+
const promoteSession = useCallback((payload = {}) => {
|
|
372
|
+
dispatch({ type: SESSION_PROMOTED, payload });
|
|
373
|
+
}, []);
|
|
374
|
+
|
|
314
375
|
const refreshMessages = useCallback(async (sessionId) => {
|
|
315
376
|
const messageApi = requireAPI(messageAPI, 'messageAPI');
|
|
316
377
|
const response = await messageApi.list(sessionId);
|
|
@@ -361,6 +422,10 @@ export default function useChatSessionReducer(deps = {}) {
|
|
|
361
422
|
dispatch({ type: STREAMING_CHUNK, payload: chunk });
|
|
362
423
|
}, []);
|
|
363
424
|
|
|
425
|
+
const replaceStreamContent = useCallback((content) => {
|
|
426
|
+
dispatch({ type: STREAMING_REPLACE, payload: content });
|
|
427
|
+
}, []);
|
|
428
|
+
|
|
364
429
|
const setAgentStatus = useCallback((agentStatus) => {
|
|
365
430
|
dispatch({ type: AGENT_STATUS_SET, payload: agentStatus });
|
|
366
431
|
}, []);
|
|
@@ -397,6 +462,7 @@ export default function useChatSessionReducer(deps = {}) {
|
|
|
397
462
|
return useMemo(() => ({
|
|
398
463
|
sessionId: state.sessionId,
|
|
399
464
|
sessionTitle: state.sessionTitle,
|
|
465
|
+
sessionDetail: state.sessionDetail,
|
|
400
466
|
instanceProfile: state.instanceProfile,
|
|
401
467
|
messages: state.messages,
|
|
402
468
|
executionCards: state.executionCards,
|
|
@@ -411,6 +477,7 @@ export default function useChatSessionReducer(deps = {}) {
|
|
|
411
477
|
createOrLoadSession,
|
|
412
478
|
resetSession,
|
|
413
479
|
updateTitle,
|
|
480
|
+
promoteSession,
|
|
414
481
|
refreshMessages,
|
|
415
482
|
setSessionError,
|
|
416
483
|
appendUserMessage,
|
|
@@ -419,6 +486,7 @@ export default function useChatSessionReducer(deps = {}) {
|
|
|
419
486
|
setLastUserMessage,
|
|
420
487
|
resetStreaming,
|
|
421
488
|
appendStreamChunk,
|
|
489
|
+
replaceStreamContent,
|
|
422
490
|
setAgentStatus,
|
|
423
491
|
setThinkingTrace,
|
|
424
492
|
setExecutionTrace,
|
|
@@ -433,6 +501,7 @@ export default function useChatSessionReducer(deps = {}) {
|
|
|
433
501
|
createOrLoadSession,
|
|
434
502
|
resetSession,
|
|
435
503
|
updateTitle,
|
|
504
|
+
promoteSession,
|
|
436
505
|
refreshMessages,
|
|
437
506
|
setSessionError,
|
|
438
507
|
appendUserMessage,
|
|
@@ -441,6 +510,7 @@ export default function useChatSessionReducer(deps = {}) {
|
|
|
441
510
|
setLastUserMessage,
|
|
442
511
|
resetStreaming,
|
|
443
512
|
appendStreamChunk,
|
|
513
|
+
replaceStreamContent,
|
|
444
514
|
setAgentStatus,
|
|
445
515
|
setThinkingTrace,
|
|
446
516
|
setExecutionTrace,
|
|
@@ -13,11 +13,19 @@ function joinUrl(baseUrl, endpoint) {
|
|
|
13
13
|
return `${baseUrl.replace(/\/+$/, '')}/${endpoint.replace(/^\/+/, '')}`;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
function buildClientRequestId() {
|
|
17
|
+
const random = Math.random().toString(36).slice(2, 10);
|
|
18
|
+
return `req_${Date.now()}_${random}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
16
21
|
export class SSEClient {
|
|
17
22
|
constructor({ endpoint = DEFAULT_ENDPOINT, baseUrl = API_BASE_URL } = {}) {
|
|
18
23
|
this.abortController = null;
|
|
19
24
|
this.endpoint = endpoint;
|
|
20
25
|
this.baseUrl = baseUrl;
|
|
26
|
+
this.activeRequestId = '';
|
|
27
|
+
this.authReadyPromise = null;
|
|
28
|
+
this.authReadyAtMs = 0;
|
|
21
29
|
}
|
|
22
30
|
|
|
23
31
|
async sendMessage(sessionOrParams, contentArg, onEventArg, onCompleteArg, onErrorArg, optionsArg = {}) {
|
|
@@ -35,7 +43,6 @@ export class SSEClient {
|
|
|
35
43
|
const enabledCapabilities = Array.isArray(params.enabledCapabilities) ? params.enabledCapabilities : [];
|
|
36
44
|
const modeKey = typeof params.modeKey === 'string' ? params.modeKey.trim() : '';
|
|
37
45
|
const manualModeKey = typeof params.manualModeKey === 'string' ? params.manualModeKey.trim() : '';
|
|
38
|
-
const currentModeKey = typeof params.currentModeKey === 'string' ? params.currentModeKey.trim() : '';
|
|
39
46
|
const payloadExtra = (
|
|
40
47
|
params.payloadExtra
|
|
41
48
|
&& typeof params.payloadExtra === 'object'
|
|
@@ -47,32 +54,111 @@ export class SSEClient {
|
|
|
47
54
|
const onComplete = isObjectCall ? onEventArg : onCompleteArg;
|
|
48
55
|
const onError = isObjectCall ? onCompleteArg : onErrorArg;
|
|
49
56
|
const url = params.url || joinUrl(params.baseUrl ?? this.baseUrl, params.endpoint || this.endpoint);
|
|
50
|
-
|
|
57
|
+
const command = typeof params.command === 'string' && params.command.trim()
|
|
58
|
+
? params.command.trim()
|
|
59
|
+
: (typeof payloadExtra.command === 'string' && payloadExtra.command.trim() ? payloadExtra.command.trim() : 'send_message');
|
|
60
|
+
const clientRequestId = typeof params.clientRequestId === 'string' && params.clientRequestId.trim()
|
|
61
|
+
? params.clientRequestId.trim()
|
|
62
|
+
: buildClientRequestId();
|
|
63
|
+
const contractVersion = typeof params.contractVersion === 'string' && params.contractVersion.trim()
|
|
64
|
+
? params.contractVersion.trim()
|
|
65
|
+
: 'flare.v1';
|
|
66
|
+
const lastTurnId = typeof params.lastTurnId === 'string' ? params.lastTurnId.trim() : '';
|
|
67
|
+
const token = typeof params.token === 'string' ? params.token.trim() : '';
|
|
68
|
+
const onTiming = typeof params.onTiming === 'function' ? params.onTiming : null;
|
|
69
|
+
const authProvider = (
|
|
70
|
+
params.auth
|
|
71
|
+
&& typeof params.auth === 'object'
|
|
72
|
+
&& !Array.isArray(params.auth)
|
|
73
|
+
) ? params.auth : null;
|
|
74
|
+
|
|
75
|
+
const nowMs = Date.now();
|
|
76
|
+
onTiming?.({
|
|
77
|
+
point: 't_submit',
|
|
78
|
+
at_ms: nowMs,
|
|
79
|
+
request_id: clientRequestId,
|
|
80
|
+
session_id: String(sessionId || '').trim(),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const waitAuthReady = async () => {
|
|
84
|
+
if (!authProvider || typeof authProvider.ensureReady !== 'function') {
|
|
85
|
+
return {
|
|
86
|
+
authorizationToken: token,
|
|
87
|
+
authReadyAtMs: nowMs,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (!this.authReadyPromise) {
|
|
91
|
+
this.authReadyPromise = Promise.resolve()
|
|
92
|
+
.then(() => authProvider.ensureReady())
|
|
93
|
+
.then((resolved) => {
|
|
94
|
+
this.authReadyAtMs = Date.now();
|
|
95
|
+
return resolved || null;
|
|
96
|
+
})
|
|
97
|
+
.finally(() => {
|
|
98
|
+
this.authReadyPromise = null;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
const authResult = await this.authReadyPromise;
|
|
102
|
+
const tokenFromAuthResult = typeof authResult?.token === 'string' ? authResult.token.trim() : '';
|
|
103
|
+
const tokenFromProvider = typeof authProvider.getToken === 'function'
|
|
104
|
+
? String(authProvider.getToken() || '').trim()
|
|
105
|
+
: '';
|
|
106
|
+
return {
|
|
107
|
+
authorizationToken: tokenFromAuthResult || tokenFromProvider || token,
|
|
108
|
+
authReadyAtMs: this.authReadyAtMs || Date.now(),
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const currentRequestId = clientRequestId;
|
|
113
|
+
if (this.abortController) {
|
|
114
|
+
this.abortController.abort();
|
|
115
|
+
this.abortController = null;
|
|
116
|
+
}
|
|
51
117
|
this.abortController = new AbortController();
|
|
118
|
+
this.activeRequestId = currentRequestId;
|
|
52
119
|
|
|
53
120
|
try {
|
|
121
|
+
const authResolved = await waitAuthReady();
|
|
122
|
+
onTiming?.({
|
|
123
|
+
point: 't_auth_ready',
|
|
124
|
+
at_ms: authResolved.authReadyAtMs || Date.now(),
|
|
125
|
+
request_id: currentRequestId,
|
|
126
|
+
session_id: String(sessionId || '').trim(),
|
|
127
|
+
});
|
|
128
|
+
if (this.activeRequestId !== currentRequestId) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
54
131
|
const response = await fetch(url, {
|
|
55
132
|
method: 'POST',
|
|
56
133
|
headers: {
|
|
57
134
|
'Content-Type': 'application/json',
|
|
135
|
+
...(authResolved.authorizationToken ? { Authorization: `Bearer ${authResolved.authorizationToken}` } : {}),
|
|
58
136
|
},
|
|
59
137
|
body: JSON.stringify({
|
|
138
|
+
contract_version: contractVersion,
|
|
139
|
+
client_request_id: clientRequestId,
|
|
140
|
+
command,
|
|
141
|
+
last_turn_id: lastTurnId || null,
|
|
60
142
|
message: content,
|
|
61
143
|
session_id: sessionId,
|
|
62
144
|
enabled_capabilities: enabledCapabilities,
|
|
63
145
|
...(modeKey ? { mode: modeKey } : {}),
|
|
64
146
|
...(manualModeKey ? { manual_mode: manualModeKey } : {}),
|
|
65
|
-
...(currentModeKey ? { current_mode: currentModeKey } : {}),
|
|
66
147
|
payload: {
|
|
67
148
|
message: content,
|
|
68
149
|
...(modeKey ? { mode: modeKey } : {}),
|
|
69
150
|
...(manualModeKey ? { manual_mode: manualModeKey } : {}),
|
|
70
|
-
...(currentModeKey ? { current_mode: currentModeKey } : {}),
|
|
71
151
|
...payloadExtra,
|
|
72
152
|
},
|
|
73
153
|
}),
|
|
74
154
|
signal: this.abortController.signal,
|
|
75
155
|
});
|
|
156
|
+
onTiming?.({
|
|
157
|
+
point: 't_stream_connected',
|
|
158
|
+
at_ms: Date.now(),
|
|
159
|
+
request_id: currentRequestId,
|
|
160
|
+
session_id: String(sessionId || '').trim(),
|
|
161
|
+
});
|
|
76
162
|
|
|
77
163
|
if (!response.ok) {
|
|
78
164
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
@@ -108,13 +194,20 @@ export class SSEClient {
|
|
|
108
194
|
|
|
109
195
|
try {
|
|
110
196
|
const eventData = JSON.parse(data);
|
|
197
|
+
if (this.activeRequestId !== currentRequestId) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
111
200
|
onEvent?.({
|
|
112
201
|
type: currentEvent,
|
|
113
202
|
data: eventData,
|
|
114
203
|
});
|
|
115
204
|
|
|
116
|
-
if (currentEvent === 'complete') {
|
|
117
|
-
onComplete?.();
|
|
205
|
+
if (currentEvent === 'complete' || currentEvent === 'done') {
|
|
206
|
+
await onComplete?.();
|
|
207
|
+
if (this.activeRequestId === currentRequestId) {
|
|
208
|
+
this.abortController = null;
|
|
209
|
+
this.activeRequestId = '';
|
|
210
|
+
}
|
|
118
211
|
return;
|
|
119
212
|
}
|
|
120
213
|
} catch (error) {
|
|
@@ -123,13 +216,21 @@ export class SSEClient {
|
|
|
123
216
|
}
|
|
124
217
|
}
|
|
125
218
|
|
|
126
|
-
onComplete?.();
|
|
219
|
+
await onComplete?.();
|
|
220
|
+
if (this.activeRequestId === currentRequestId) {
|
|
221
|
+
this.abortController = null;
|
|
222
|
+
this.activeRequestId = '';
|
|
223
|
+
}
|
|
127
224
|
} catch (error) {
|
|
128
225
|
if (error?.name === 'AbortError') {
|
|
129
226
|
return;
|
|
130
227
|
}
|
|
131
228
|
|
|
132
229
|
onError?.(error instanceof Error ? error.message : '发送消息失败');
|
|
230
|
+
if (this.activeRequestId === currentRequestId) {
|
|
231
|
+
this.abortController = null;
|
|
232
|
+
this.activeRequestId = '';
|
|
233
|
+
}
|
|
133
234
|
}
|
|
134
235
|
}
|
|
135
236
|
|
|
@@ -138,5 +239,6 @@ export class SSEClient {
|
|
|
138
239
|
this.abortController.abort();
|
|
139
240
|
this.abortController = null;
|
|
140
241
|
}
|
|
242
|
+
this.activeRequestId = '';
|
|
141
243
|
}
|
|
142
244
|
}
|
|
@@ -145,6 +145,49 @@ export const parseContent = (raw) => ({
|
|
|
145
145
|
chunk: raw?.content ?? '',
|
|
146
146
|
});
|
|
147
147
|
|
|
148
|
+
export const parseAck = (raw) => ({
|
|
149
|
+
trace_id: String(raw?.trace_id || '').trim(),
|
|
150
|
+
session_id: String(raw?.session_id || '').trim(),
|
|
151
|
+
request_id: String(raw?.request_id || '').trim(),
|
|
152
|
+
mode: String(raw?.mode || '').trim(),
|
|
153
|
+
ts: Number.isFinite(Number(raw?.ts)) ? Number(raw.ts) : null,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
export const parsePhaseEvent = (raw) => ({
|
|
157
|
+
phase: String(raw?.phase || '').trim(),
|
|
158
|
+
label: String(raw?.label || '').trim(),
|
|
159
|
+
status: String(raw?.status || '').trim(),
|
|
160
|
+
progress: Number.isFinite(Number(raw?.progress)) ? Number(raw.progress) : null,
|
|
161
|
+
meta: raw?.meta && typeof raw.meta === 'object' && !Array.isArray(raw.meta) ? raw.meta : {},
|
|
162
|
+
ts: Number.isFinite(Number(raw?.ts)) ? Number(raw.ts) : null,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
export const parsePatch = (raw) => ({
|
|
166
|
+
scope: String(raw?.scope || '').trim(),
|
|
167
|
+
payload: raw?.payload && typeof raw.payload === 'object' && !Array.isArray(raw.payload) ? raw.payload : {},
|
|
168
|
+
ts: Number.isFinite(Number(raw?.ts)) ? Number(raw.ts) : null,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
export const parseTextDelta = (raw) => ({
|
|
172
|
+
channel: String(raw?.channel || 'assistant').trim() || 'assistant',
|
|
173
|
+
delta: String(raw?.delta || ''),
|
|
174
|
+
ts: Number.isFinite(Number(raw?.ts)) ? Number(raw.ts) : null,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
export const parseTextReplace = (raw) => ({
|
|
178
|
+
channel: String(raw?.channel || 'assistant').trim() || 'assistant',
|
|
179
|
+
content: String(raw?.content || ''),
|
|
180
|
+
ts: Number.isFinite(Number(raw?.ts)) ? Number(raw.ts) : null,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
export const parseDone = (raw) => ({
|
|
184
|
+
trace_id: String(raw?.trace_id || '').trim(),
|
|
185
|
+
session_id: String(raw?.session_id || '').trim(),
|
|
186
|
+
request_id: String(raw?.request_id || '').trim(),
|
|
187
|
+
status: String(raw?.status || 'done').trim() || 'done',
|
|
188
|
+
ts: Number.isFinite(Number(raw?.ts)) ? Number(raw.ts) : null,
|
|
189
|
+
});
|
|
190
|
+
|
|
148
191
|
export const parseTrace = (raw) => ({
|
|
149
192
|
trace: raw?.trace ?? raw?.detail ?? '',
|
|
150
193
|
summary: raw?.summary ?? raw?.label ?? '',
|
|
@@ -169,9 +212,48 @@ function pickFirstArray(payload, keys) {
|
|
|
169
212
|
return [];
|
|
170
213
|
}
|
|
171
214
|
|
|
215
|
+
function parseGuidedSession(rawGuidedSession) {
|
|
216
|
+
const payload = (rawGuidedSession && typeof rawGuidedSession === 'object' && !Array.isArray(rawGuidedSession))
|
|
217
|
+
? rawGuidedSession
|
|
218
|
+
: {};
|
|
219
|
+
const questionQueue = (payload.question_queue && typeof payload.question_queue === 'object' && !Array.isArray(payload.question_queue))
|
|
220
|
+
? payload.question_queue
|
|
221
|
+
: {};
|
|
222
|
+
const summary = (payload.summary && typeof payload.summary === 'object' && !Array.isArray(payload.summary))
|
|
223
|
+
? payload.summary
|
|
224
|
+
: {};
|
|
225
|
+
const decisionPoint = (payload.decision_point && typeof payload.decision_point === 'object' && !Array.isArray(payload.decision_point))
|
|
226
|
+
? payload.decision_point
|
|
227
|
+
: {};
|
|
228
|
+
return {
|
|
229
|
+
state: payload?.state ?? '',
|
|
230
|
+
active: payload?.active === true,
|
|
231
|
+
current_question: payload?.current_question && typeof payload.current_question === 'object' && !Array.isArray(payload.current_question)
|
|
232
|
+
? payload.current_question
|
|
233
|
+
: null,
|
|
234
|
+
question_queue: {
|
|
235
|
+
total: Number.isFinite(Number(questionQueue?.total)) ? Number(questionQueue.total) : 0,
|
|
236
|
+
answered: Number.isFinite(Number(questionQueue?.answered)) ? Number(questionQueue.answered) : 0,
|
|
237
|
+
remaining: Number.isFinite(Number(questionQueue?.remaining)) ? Number(questionQueue.remaining) : 0,
|
|
238
|
+
pending_fields: Array.isArray(questionQueue?.pending_fields) ? questionQueue.pending_fields : [],
|
|
239
|
+
},
|
|
240
|
+
summary: {
|
|
241
|
+
text: summary?.text ?? '',
|
|
242
|
+
ready: summary?.ready === true,
|
|
243
|
+
},
|
|
244
|
+
decision_point: {
|
|
245
|
+
active: decisionPoint?.active === true,
|
|
246
|
+
actions: Array.isArray(decisionPoint?.actions) ? decisionPoint.actions : [],
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
172
251
|
export const parseFieldProgress = (raw) => {
|
|
173
252
|
const payload = resolveModeEventPayload(raw);
|
|
174
253
|
return {
|
|
254
|
+
intake_session_id: payload?.intake_session_id ?? '',
|
|
255
|
+
intake_session_state: payload?.intake_session_state ?? '',
|
|
256
|
+
intake_session_completed: payload?.intake_session_completed === true,
|
|
175
257
|
flow_state: payload?.flow_state ?? payload?.collection_phase ?? '',
|
|
176
258
|
fields: payload?.fields ?? {},
|
|
177
259
|
sources: payload?.sources ?? {},
|
|
@@ -212,6 +294,15 @@ export const parseFieldProgress = (raw) => {
|
|
|
212
294
|
: { current: 0, total: 0 },
|
|
213
295
|
collection_phase: payload?.collection_phase ?? '',
|
|
214
296
|
required_missing_count: Number.isFinite(payload?.required_missing_count) ? payload.required_missing_count : null,
|
|
297
|
+
required_coverage: Number.isFinite(Number(payload?.required_coverage)) ? Number(payload.required_coverage) : 0,
|
|
298
|
+
total_coverage: Number.isFinite(Number(payload?.total_coverage)) ? Number(payload.total_coverage) : 0,
|
|
299
|
+
analysis_entry_threshold: Number.isFinite(Number(payload?.analysis_entry_threshold)) ? Number(payload.analysis_entry_threshold) : 0.8,
|
|
300
|
+
analysis_entry_eligible: payload?.analysis_entry_eligible === true,
|
|
301
|
+
active_collecting: payload?.active_collecting === true,
|
|
302
|
+
ready_for_submit: payload?.ready_for_submit === true,
|
|
303
|
+
decision_point_active: payload?.decision_point_active === true,
|
|
304
|
+
has_active_question: payload?.has_active_question === true,
|
|
305
|
+
guided_session: parseGuidedSession(payload?.guided_session),
|
|
215
306
|
};
|
|
216
307
|
};
|
|
217
308
|
|
|
@@ -234,6 +325,9 @@ export const parseRequirementDraft = (raw) => {
|
|
|
234
325
|
: {};
|
|
235
326
|
|
|
236
327
|
return {
|
|
328
|
+
intake_session_id: payload?.intake_session_id ?? '',
|
|
329
|
+
intake_session_state: payload?.intake_session_state ?? '',
|
|
330
|
+
intake_session_completed: payload?.intake_session_completed === true,
|
|
237
331
|
mode_key: payload?.mode_key ?? '',
|
|
238
332
|
project_id: payload?.project_id ?? null,
|
|
239
333
|
message_excerpt: payload?.message_excerpt ?? '',
|
|
@@ -251,6 +345,9 @@ export const parseNextActions = (raw) => {
|
|
|
251
345
|
? payload.actions
|
|
252
346
|
: (Array.isArray(payload?.next_actions) ? payload.next_actions : []);
|
|
253
347
|
return {
|
|
348
|
+
intake_session_id: payload?.intake_session_id ?? '',
|
|
349
|
+
intake_session_state: payload?.intake_session_state ?? '',
|
|
350
|
+
intake_session_completed: payload?.intake_session_completed === true,
|
|
254
351
|
mode_key: payload?.mode_key ?? '',
|
|
255
352
|
flow_state: payload?.flow_state ?? payload?.collection_phase ?? '',
|
|
256
353
|
project_id: payload?.project_id ?? null,
|
|
@@ -282,6 +379,15 @@ export const parseNextActions = (raw) => {
|
|
|
282
379
|
collection_phase: payload?.collection_phase ?? '',
|
|
283
380
|
required_missing_count: Number.isFinite(payload?.required_missing_count) ? payload.required_missing_count : null,
|
|
284
381
|
render_hint: payload?.render_hint ?? 'next_actions',
|
|
382
|
+
required_coverage: Number.isFinite(Number(payload?.required_coverage)) ? Number(payload.required_coverage) : 0,
|
|
383
|
+
total_coverage: Number.isFinite(Number(payload?.total_coverage)) ? Number(payload.total_coverage) : 0,
|
|
384
|
+
analysis_entry_threshold: Number.isFinite(Number(payload?.analysis_entry_threshold)) ? Number(payload.analysis_entry_threshold) : 0.8,
|
|
385
|
+
analysis_entry_eligible: payload?.analysis_entry_eligible === true,
|
|
386
|
+
active_collecting: payload?.active_collecting === true,
|
|
387
|
+
ready_for_submit: payload?.ready_for_submit === true,
|
|
388
|
+
decision_point_active: payload?.decision_point_active === true,
|
|
389
|
+
has_active_question: payload?.has_active_question === true,
|
|
390
|
+
guided_session: parseGuidedSession(payload?.guided_session),
|
|
285
391
|
};
|
|
286
392
|
};
|
|
287
393
|
|
|
@@ -473,8 +579,14 @@ export const parseKnowledgeCitation = (raw) => {
|
|
|
473
579
|
|
|
474
580
|
export const parseOrchestrationStatus = (raw) => {
|
|
475
581
|
const payload = resolveModeEventPayload(raw);
|
|
582
|
+
const activeWorkItem = payload?.active_work_item && typeof payload.active_work_item === 'object' && !Array.isArray(payload.active_work_item)
|
|
583
|
+
? payload.active_work_item
|
|
584
|
+
: null;
|
|
476
585
|
return {
|
|
477
586
|
trace_id: payload?.trace_id ?? '',
|
|
587
|
+
intake_session_id: payload?.intake_session_id ?? '',
|
|
588
|
+
intake_session_state: payload?.intake_session_state ?? '',
|
|
589
|
+
intake_session_completed: payload?.intake_session_completed === true,
|
|
478
590
|
current_stage: payload?.current_stage ?? '',
|
|
479
591
|
detected_intent: payload?.detected_intent && typeof payload.detected_intent === 'object' && !Array.isArray(payload.detected_intent)
|
|
480
592
|
? payload.detected_intent
|
|
@@ -489,6 +601,21 @@ export const parseOrchestrationStatus = (raw) => {
|
|
|
489
601
|
? payload.next_action
|
|
490
602
|
: {},
|
|
491
603
|
next_actions: Array.isArray(payload?.next_actions) ? payload.next_actions : [],
|
|
604
|
+
active_collecting: payload?.active_collecting === true,
|
|
605
|
+
ready_for_submit: payload?.ready_for_submit === true,
|
|
606
|
+
required_coverage: Number.isFinite(Number(payload?.required_coverage)) ? Number(payload.required_coverage) : 0,
|
|
607
|
+
total_coverage: Number.isFinite(Number(payload?.total_coverage)) ? Number(payload.total_coverage) : 0,
|
|
608
|
+
analysis_entry_threshold: Number.isFinite(Number(payload?.analysis_entry_threshold)) ? Number(payload.analysis_entry_threshold) : 0.8,
|
|
609
|
+
analysis_entry_eligible: payload?.analysis_entry_eligible === true,
|
|
610
|
+
decision_point_active: payload?.decision_point_active === true,
|
|
611
|
+
has_active_question: payload?.has_active_question === true,
|
|
612
|
+
guided_session: parseGuidedSession(payload?.guided_session),
|
|
613
|
+
active_work_item: activeWorkItem,
|
|
614
|
+
active_fields: Array.isArray(payload?.active_fields) ? payload.active_fields : [],
|
|
615
|
+
hypotheses: Array.isArray(payload?.hypotheses) ? payload.hypotheses : [],
|
|
616
|
+
planner: payload?.planner && typeof payload.planner === 'object' && !Array.isArray(payload.planner)
|
|
617
|
+
? payload.planner
|
|
618
|
+
: {},
|
|
492
619
|
chooser: payload?.chooser && typeof payload.chooser === 'object' && !Array.isArray(payload.chooser)
|
|
493
620
|
? payload.chooser
|
|
494
621
|
: null,
|
|
@@ -506,6 +633,26 @@ export const parseOrchestrationStatus = (raw) => {
|
|
|
506
633
|
node_state: payload?.node_state ?? '',
|
|
507
634
|
node_reason: payload?.node_reason ?? '',
|
|
508
635
|
needs_user_choice: payload?.needs_user_choice === true,
|
|
636
|
+
conversation_intent_state: payload?.conversation_intent_state ?? '',
|
|
637
|
+
session_intent_stage: payload?.session_intent_stage ?? '',
|
|
638
|
+
intent_type: payload?.intent_type ?? '',
|
|
639
|
+
procurement_relevance: payload?.procurement_relevance === true,
|
|
640
|
+
consultative_procurement_qa: payload?.consultative_procurement_qa === true,
|
|
641
|
+
escalation_target: payload?.escalation_target ?? '',
|
|
642
|
+
intake_candidate: payload?.intake_candidate === true,
|
|
643
|
+
clarification_recommended: payload?.clarification_recommended === true,
|
|
644
|
+
analysis_ready: payload?.analysis_ready === true,
|
|
645
|
+
draft_seeded: payload?.draft_seeded === true,
|
|
646
|
+
missing_key_fields: Array.isArray(payload?.missing_key_fields) ? payload.missing_key_fields : [],
|
|
647
|
+
recommended_next_step: payload?.recommended_next_step ?? '',
|
|
648
|
+
intent_and_escalation_confidence: Number.isFinite(Number(payload?.intent_and_escalation_confidence))
|
|
649
|
+
? Number(payload.intent_and_escalation_confidence)
|
|
650
|
+
: 0,
|
|
651
|
+
intent_and_escalation_reason: payload?.intent_and_escalation_reason ?? '',
|
|
652
|
+
intent_escalation_policy: payload?.intent_escalation_policy && typeof payload.intent_escalation_policy === 'object' && !Array.isArray(payload.intent_escalation_policy)
|
|
653
|
+
? payload.intent_escalation_policy
|
|
654
|
+
: {},
|
|
655
|
+
intent_escalation_policy_source: payload?.intent_escalation_policy_source ?? '',
|
|
509
656
|
degrade_reason: Array.isArray(payload?.degrade_reason) ? payload.degrade_reason : [],
|
|
510
657
|
content_policy: payload?.content_policy && typeof payload.content_policy === 'object' && !Array.isArray(payload.content_policy)
|
|
511
658
|
? payload.content_policy
|
|
@@ -596,3 +743,125 @@ export const parseModeSwitchReason = (raw) => ({
|
|
|
596
743
|
export const parseSSEError = (raw) => ({
|
|
597
744
|
message: raw?.message ?? '发生错误',
|
|
598
745
|
});
|
|
746
|
+
|
|
747
|
+
export const PATCH_SCOPE_AUTHORITATIVE = Object.freeze([
|
|
748
|
+
'session',
|
|
749
|
+
'assistant_reply',
|
|
750
|
+
'primary_flow',
|
|
751
|
+
'checkpoint',
|
|
752
|
+
'intent_state',
|
|
753
|
+
'question',
|
|
754
|
+
'confirmed_fields',
|
|
755
|
+
'missing_fields',
|
|
756
|
+
'next_actions',
|
|
757
|
+
'analysis',
|
|
758
|
+
'observation',
|
|
759
|
+
'capabilities.field_retrieval',
|
|
760
|
+
'errors',
|
|
761
|
+
]);
|
|
762
|
+
|
|
763
|
+
export const PATCH_SCOPE_LEGACY_COMPAT = Object.freeze([
|
|
764
|
+
'current_question',
|
|
765
|
+
'recommendation_summary',
|
|
766
|
+
]);
|
|
767
|
+
|
|
768
|
+
const PATCH_SCOPE_ALLOWLIST = new Set([
|
|
769
|
+
...PATCH_SCOPE_AUTHORITATIVE,
|
|
770
|
+
...PATCH_SCOPE_LEGACY_COMPAT,
|
|
771
|
+
]);
|
|
772
|
+
|
|
773
|
+
function getByScopePath(payload, scope) {
|
|
774
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
775
|
+
return undefined;
|
|
776
|
+
}
|
|
777
|
+
const segments = String(scope || '').split('.').filter(Boolean);
|
|
778
|
+
if (!segments.length) {
|
|
779
|
+
return undefined;
|
|
780
|
+
}
|
|
781
|
+
let current = payload;
|
|
782
|
+
for (const segment of segments) {
|
|
783
|
+
if (!current || typeof current !== 'object' || Array.isArray(current) || !(segment in current)) {
|
|
784
|
+
return undefined;
|
|
785
|
+
}
|
|
786
|
+
current = current[segment];
|
|
787
|
+
}
|
|
788
|
+
return current;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
export const parsePatchEvent = (raw) => {
|
|
792
|
+
const envelope = (raw && typeof raw === 'object' && !Array.isArray(raw)) ? raw : {};
|
|
793
|
+
const patchScope = Array.isArray(envelope.patch_scope)
|
|
794
|
+
? envelope.patch_scope.map((item) => String(item || '').trim()).filter(Boolean)
|
|
795
|
+
: [];
|
|
796
|
+
const payload = (envelope.payload && typeof envelope.payload === 'object' && !Array.isArray(envelope.payload))
|
|
797
|
+
? envelope.payload
|
|
798
|
+
: {};
|
|
799
|
+
const hasPayload = Object.keys(payload).length > 0;
|
|
800
|
+
const hasUnknownScope = patchScope.some((item) => !(PATCH_SCOPE_ALLOWLIST.has(item) || item.startsWith('confirmed_fields.')));
|
|
801
|
+
const hasScopePayloadMismatch = patchScope.some((scope) => {
|
|
802
|
+
const direct = payload[scope];
|
|
803
|
+
const byPath = getByScopePath(payload, scope);
|
|
804
|
+
const lastSegment = String(scope).split('.').slice(-1)[0];
|
|
805
|
+
const leaf = payload[lastSegment];
|
|
806
|
+
return direct === undefined && byPath === undefined && leaf === undefined;
|
|
807
|
+
});
|
|
808
|
+
const legacyPatchScope = patchScope.filter((scope) => PATCH_SCOPE_LEGACY_COMPAT.includes(scope));
|
|
809
|
+
const authoritativePatchScope = patchScope.filter(
|
|
810
|
+
(scope) => PATCH_SCOPE_AUTHORITATIVE.includes(scope) || scope.startsWith('confirmed_fields.')
|
|
811
|
+
);
|
|
812
|
+
const invalidReason = (() => {
|
|
813
|
+
if (envelope.message_type !== 'patch_event') {
|
|
814
|
+
return 'invalid_message_type';
|
|
815
|
+
}
|
|
816
|
+
if (!['ack', 'patch', 'error', 'final'].includes(String(envelope.event_type || '').trim())) {
|
|
817
|
+
return 'invalid_event_type';
|
|
818
|
+
}
|
|
819
|
+
if (hasPayload && patchScope.length === 0) {
|
|
820
|
+
return 'payload_without_patch_scope';
|
|
821
|
+
}
|
|
822
|
+
if (hasUnknownScope) {
|
|
823
|
+
return 'unknown_patch_scope';
|
|
824
|
+
}
|
|
825
|
+
if (patchScope.length > 0 && hasScopePayloadMismatch) {
|
|
826
|
+
return 'patch_scope_payload_mismatch';
|
|
827
|
+
}
|
|
828
|
+
return '';
|
|
829
|
+
})();
|
|
830
|
+
return {
|
|
831
|
+
contract_version: String(envelope.contract_version || 'flare.v1'),
|
|
832
|
+
message_type: 'patch_event',
|
|
833
|
+
event_type: String(envelope.event_type || '').trim(),
|
|
834
|
+
sequence: Number.isFinite(Number(envelope.sequence)) ? Number(envelope.sequence) : 0,
|
|
835
|
+
trace_id: String(envelope.trace_id || '').trim(),
|
|
836
|
+
client_request_id: String(envelope.client_request_id || '').trim(),
|
|
837
|
+
intake_session_id: String(
|
|
838
|
+
payload?.primary_flow?.intake_session_id
|
|
839
|
+
|| payload?.checkpoint?.intake_session_id
|
|
840
|
+
|| payload?.next_actions?.intake_session_id
|
|
841
|
+
|| payload?.question?.intake_session_id
|
|
842
|
+
|| payload?.intake_session_id
|
|
843
|
+
|| ''
|
|
844
|
+
).trim(),
|
|
845
|
+
intake_session_state: String(
|
|
846
|
+
payload?.primary_flow?.intake_session_state
|
|
847
|
+
|| payload?.next_actions?.intake_session_state
|
|
848
|
+
|| payload?.intake_session_state
|
|
849
|
+
|| ''
|
|
850
|
+
).trim(),
|
|
851
|
+
intake_session_completed: Boolean(
|
|
852
|
+
payload?.primary_flow?.intake_session_completed
|
|
853
|
+
|| payload?.next_actions?.intake_session_completed
|
|
854
|
+
|| payload?.intake_session_completed
|
|
855
|
+
),
|
|
856
|
+
session: (
|
|
857
|
+
envelope.session && typeof envelope.session === 'object' && !Array.isArray(envelope.session)
|
|
858
|
+
) ? envelope.session : { session_id: '', turn_id: '' },
|
|
859
|
+
patch_scope: patchScope,
|
|
860
|
+
authoritative_patch_scope: authoritativePatchScope,
|
|
861
|
+
legacy_patch_scope: legacyPatchScope,
|
|
862
|
+
payload,
|
|
863
|
+
final: envelope.final === true,
|
|
864
|
+
invalid: Boolean(invalidReason),
|
|
865
|
+
invalid_reason: invalidReason,
|
|
866
|
+
};
|
|
867
|
+
};
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
import { useRef, useState, useCallback } from 'react';
|
|
1
|
+
import { useRef, useState, useCallback, useEffect } from 'react';
|
|
2
2
|
import { SSEClient } from './sse-client.js';
|
|
3
3
|
import {
|
|
4
|
+
parseAck,
|
|
4
5
|
parseAgentStatus,
|
|
5
6
|
parseThinkingTrace,
|
|
6
7
|
parseExecutionTrace,
|
|
7
8
|
parseContent,
|
|
9
|
+
parseDone,
|
|
10
|
+
parsePatch,
|
|
11
|
+
parsePhaseEvent,
|
|
12
|
+
parseTextDelta,
|
|
13
|
+
parseTextReplace,
|
|
8
14
|
parseTrace,
|
|
9
15
|
parseFieldProgress,
|
|
10
16
|
parseDoc,
|
|
@@ -31,6 +37,7 @@ import {
|
|
|
31
37
|
parseCanvasState,
|
|
32
38
|
parseCanvasRevision,
|
|
33
39
|
parsePlanBlock,
|
|
40
|
+
parsePatchEvent,
|
|
34
41
|
parseSSEError,
|
|
35
42
|
} from './sse-events.js';
|
|
36
43
|
|
|
@@ -52,19 +59,28 @@ export function useSSEStream(sessionId) {
|
|
|
52
59
|
const lastMessageRef = useRef({ content: '', handlers: {} });
|
|
53
60
|
|
|
54
61
|
const send = useCallback(async (content, handlers = {}, options = {}) => {
|
|
55
|
-
const
|
|
56
|
-
|
|
62
|
+
const hasSessionOverride = Object.prototype.hasOwnProperty.call(options, 'sessionIdOverride');
|
|
63
|
+
const resolvedSessionId = hasSessionOverride ? options.sessionIdOverride : sessionId;
|
|
64
|
+
if (!content.trim()) return;
|
|
57
65
|
|
|
58
66
|
lastMessageRef.current = { content, handlers, options };
|
|
59
67
|
setError(null);
|
|
60
68
|
accumulatedRef.current = '';
|
|
61
69
|
setLoading(true);
|
|
70
|
+
let latestSessionId = String(resolvedSessionId || '').trim();
|
|
62
71
|
|
|
63
72
|
const {
|
|
73
|
+
onAck,
|
|
64
74
|
onAgentStatus,
|
|
65
75
|
onThinkingTrace,
|
|
66
76
|
onExecutionTrace,
|
|
67
77
|
onContent,
|
|
78
|
+
onTextReplace,
|
|
79
|
+
onPhaseStart,
|
|
80
|
+
onPhaseUpdate,
|
|
81
|
+
onPhaseEnd,
|
|
82
|
+
onPatch,
|
|
83
|
+
onDone,
|
|
68
84
|
onTrace,
|
|
69
85
|
onWorkspaceActivation,
|
|
70
86
|
onUICards,
|
|
@@ -91,12 +107,28 @@ export function useSSEStream(sessionId) {
|
|
|
91
107
|
onPlanBlock,
|
|
92
108
|
onDoc,
|
|
93
109
|
onCapabilitySuggestion,
|
|
110
|
+
onPatchEvent,
|
|
94
111
|
onError,
|
|
95
112
|
onComplete,
|
|
96
113
|
} = handlers;
|
|
97
114
|
|
|
98
115
|
const eventHandler = (event) => {
|
|
99
116
|
switch (event.type) {
|
|
117
|
+
case 'ack':
|
|
118
|
+
onAck?.(parseAck(event.data));
|
|
119
|
+
break;
|
|
120
|
+
case 'phase.start':
|
|
121
|
+
onPhaseStart?.(parsePhaseEvent(event.data));
|
|
122
|
+
break;
|
|
123
|
+
case 'phase.update':
|
|
124
|
+
onPhaseUpdate?.(parsePhaseEvent(event.data));
|
|
125
|
+
break;
|
|
126
|
+
case 'phase.end':
|
|
127
|
+
onPhaseEnd?.(parsePhaseEvent(event.data));
|
|
128
|
+
break;
|
|
129
|
+
case 'patch':
|
|
130
|
+
onPatch?.(parsePatch(event.data));
|
|
131
|
+
break;
|
|
100
132
|
case 'agent_status':
|
|
101
133
|
onAgentStatus?.(parseAgentStatus(event.data));
|
|
102
134
|
break;
|
|
@@ -116,6 +148,25 @@ export function useSSEStream(sessionId) {
|
|
|
116
148
|
onContent?.(chunk);
|
|
117
149
|
break;
|
|
118
150
|
}
|
|
151
|
+
case 'text.delta': {
|
|
152
|
+
const parsed = parseTextDelta(event.data);
|
|
153
|
+
if (parsed.channel === 'assistant' && parsed.delta) {
|
|
154
|
+
accumulatedRef.current += parsed.delta;
|
|
155
|
+
onContent?.(parsed.delta);
|
|
156
|
+
}
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
case 'text.replace': {
|
|
160
|
+
const parsed = parseTextReplace(event.data);
|
|
161
|
+
if (parsed.channel === 'assistant') {
|
|
162
|
+
accumulatedRef.current = parsed.content;
|
|
163
|
+
onTextReplace?.(parsed.content);
|
|
164
|
+
}
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
case 'done':
|
|
168
|
+
onDone?.(parseDone(event.data));
|
|
169
|
+
break;
|
|
119
170
|
case 'trace':
|
|
120
171
|
onTrace?.(parseTrace(event.data));
|
|
121
172
|
break;
|
|
@@ -194,6 +245,16 @@ export function useSSEStream(sessionId) {
|
|
|
194
245
|
case 'capability_suggestion':
|
|
195
246
|
onCapabilitySuggestion?.(parseCapabilitySuggestion(event.data));
|
|
196
247
|
break;
|
|
248
|
+
case 'patch_event':
|
|
249
|
+
{
|
|
250
|
+
const parsedPatchEvent = parsePatchEvent(event.data);
|
|
251
|
+
const patchSessionId = String(parsedPatchEvent?.session?.session_id || '').trim();
|
|
252
|
+
if (patchSessionId) {
|
|
253
|
+
latestSessionId = patchSessionId;
|
|
254
|
+
}
|
|
255
|
+
onPatchEvent?.(parsedPatchEvent);
|
|
256
|
+
}
|
|
257
|
+
break;
|
|
197
258
|
case 'error':
|
|
198
259
|
onError?.(parseSSEError(event.data));
|
|
199
260
|
break;
|
|
@@ -202,9 +263,11 @@ export function useSSEStream(sessionId) {
|
|
|
202
263
|
}
|
|
203
264
|
};
|
|
204
265
|
|
|
205
|
-
const completeHandler = () => {
|
|
266
|
+
const completeHandler = async () => {
|
|
206
267
|
setLoading(false);
|
|
207
|
-
onComplete?.(accumulatedRef.current
|
|
268
|
+
await onComplete?.(accumulatedRef.current, {
|
|
269
|
+
sessionId: latestSessionId || null,
|
|
270
|
+
});
|
|
208
271
|
accumulatedRef.current = '';
|
|
209
272
|
};
|
|
210
273
|
|
|
@@ -217,12 +280,15 @@ export function useSSEStream(sessionId) {
|
|
|
217
280
|
};
|
|
218
281
|
|
|
219
282
|
const requestOptions = {
|
|
220
|
-
sessionId: resolvedSessionId,
|
|
283
|
+
sessionId: String(resolvedSessionId || '').trim() || null,
|
|
221
284
|
content,
|
|
222
285
|
enabledCapabilities: Array.isArray(options.enabledCapabilities) ? options.enabledCapabilities : [],
|
|
223
286
|
modeKey: typeof options.modeKey === 'string' ? options.modeKey : '',
|
|
224
287
|
manualModeKey: typeof options.manualModeKey === 'string' ? options.manualModeKey : '',
|
|
225
|
-
|
|
288
|
+
command: typeof options.command === 'string' ? options.command : '',
|
|
289
|
+
clientRequestId: typeof options.clientRequestId === 'string' ? options.clientRequestId : '',
|
|
290
|
+
contractVersion: typeof options.contractVersion === 'string' ? options.contractVersion : '',
|
|
291
|
+
lastTurnId: typeof options.lastTurnId === 'string' ? options.lastTurnId : '',
|
|
226
292
|
payloadExtra: (
|
|
227
293
|
options.payloadExtra
|
|
228
294
|
&& typeof options.payloadExtra === 'object'
|
|
@@ -233,6 +299,13 @@ export function useSSEStream(sessionId) {
|
|
|
233
299
|
endpoint: options.endpoint,
|
|
234
300
|
baseUrl: options.baseUrl,
|
|
235
301
|
url: options.url,
|
|
302
|
+
token: typeof options.token === 'string' ? options.token : '',
|
|
303
|
+
auth: (
|
|
304
|
+
options.auth
|
|
305
|
+
&& typeof options.auth === 'object'
|
|
306
|
+
&& !Array.isArray(options.auth)
|
|
307
|
+
) ? options.auth : undefined,
|
|
308
|
+
onTiming: typeof options.onTiming === 'function' ? options.onTiming : undefined,
|
|
236
309
|
};
|
|
237
310
|
|
|
238
311
|
if (!requestOptions.enabledCapabilities.length) {
|
|
@@ -240,12 +313,18 @@ export function useSSEStream(sessionId) {
|
|
|
240
313
|
}
|
|
241
314
|
if (!requestOptions.modeKey) delete requestOptions.modeKey;
|
|
242
315
|
if (!requestOptions.manualModeKey) delete requestOptions.manualModeKey;
|
|
243
|
-
if (!requestOptions.
|
|
316
|
+
if (!requestOptions.command) delete requestOptions.command;
|
|
317
|
+
if (!requestOptions.clientRequestId) delete requestOptions.clientRequestId;
|
|
318
|
+
if (!requestOptions.contractVersion) delete requestOptions.contractVersion;
|
|
319
|
+
if (!requestOptions.lastTurnId) delete requestOptions.lastTurnId;
|
|
244
320
|
if (!Object.keys(requestOptions.payloadExtra || {}).length) delete requestOptions.payloadExtra;
|
|
245
321
|
|
|
246
322
|
if (!requestOptions.endpoint) delete requestOptions.endpoint;
|
|
247
323
|
if (!requestOptions.baseUrl) delete requestOptions.baseUrl;
|
|
248
324
|
if (!requestOptions.url) delete requestOptions.url;
|
|
325
|
+
if (!requestOptions.token) delete requestOptions.token;
|
|
326
|
+
if (!requestOptions.auth) delete requestOptions.auth;
|
|
327
|
+
if (!requestOptions.onTiming) delete requestOptions.onTiming;
|
|
249
328
|
|
|
250
329
|
await clientRef.current.sendMessage(
|
|
251
330
|
requestOptions,
|
|
@@ -267,6 +346,10 @@ export function useSSEStream(sessionId) {
|
|
|
267
346
|
accumulatedRef.current = '';
|
|
268
347
|
}, []);
|
|
269
348
|
|
|
349
|
+
useEffect(() => () => {
|
|
350
|
+
clientRef.current.abort();
|
|
351
|
+
}, []);
|
|
352
|
+
|
|
270
353
|
return { send, loading, error, retry, abort };
|
|
271
354
|
}
|
|
272
355
|
|