@beanx/cathygo-web-core 0.1.1 → 0.1.3
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/README.md +30 -0
- package/dist/index.d.ts +18 -3
- package/dist/index.js +392 -14
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# @beanx/cathygo-web-core
|
|
2
|
+
|
|
3
|
+
Shared CathyGO Web UI and state package.
|
|
4
|
+
|
|
5
|
+
Current active upgrade plan:
|
|
6
|
+
|
|
7
|
+
- [`../../docs/session-runtime-vnext-upgrade-plan.md`](../../docs/session-runtime-vnext-upgrade-plan.md)
|
|
8
|
+
|
|
9
|
+
## vNext Role
|
|
10
|
+
|
|
11
|
+
This package owns the selected-session view model for:
|
|
12
|
+
|
|
13
|
+
- single-session transcript rendering
|
|
14
|
+
- session event reduction
|
|
15
|
+
- optimistic user input reconciliation by `client_input_id`
|
|
16
|
+
- session-scoped stop state
|
|
17
|
+
- replay-safe hydration after refresh or reconnect
|
|
18
|
+
- selectors used by local Web and `beanx-home`
|
|
19
|
+
|
|
20
|
+
Host applications still own account routing, agent selection, transport setup,
|
|
21
|
+
selected session id, lightweight session lists, and product-specific shell UI.
|
|
22
|
+
They should not own a second session execution runtime.
|
|
23
|
+
|
|
24
|
+
## First vNext Deliverables
|
|
25
|
+
|
|
26
|
+
- Keep the current single-session `ChatState` reducer exported.
|
|
27
|
+
- Add `SessionViewState`, session event reducer, and current-session selectors.
|
|
28
|
+
- Add reducer tests for interrupt, replay, stop, and optimistic reconciliation.
|
|
29
|
+
- Prove the implementation in `cathygo-agent/web` before publishing
|
|
30
|
+
`@beanx/cathygo-web-core@0.2.0`.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { Dispatch, RefObject } from 'react';
|
|
3
|
-
import { AgentActivity, LearningTurnEvent, ConversationSummary, GatewayModelStatus } from '@beanx/cathygo-protocol';
|
|
3
|
+
import { AgentActivity, SessionRuntimeSnapshot, LearningTurnEvent, ConversationSummary, GatewayModelStatus } from '@beanx/cathygo-protocol';
|
|
4
4
|
|
|
5
5
|
type AgentIdentityStatus = {
|
|
6
6
|
agent_id?: string | null;
|
|
@@ -73,6 +73,12 @@ type ChatAttachment = {
|
|
|
73
73
|
width?: number | null;
|
|
74
74
|
height?: number | null;
|
|
75
75
|
created_at?: string | null;
|
|
76
|
+
thumbnail?: {
|
|
77
|
+
mime_type: string;
|
|
78
|
+
data_base64: string;
|
|
79
|
+
width?: number | null;
|
|
80
|
+
height?: number | null;
|
|
81
|
+
} | null;
|
|
76
82
|
};
|
|
77
83
|
type ChatMessagePart = {
|
|
78
84
|
type: 'text';
|
|
@@ -88,6 +94,8 @@ type ChatMessage$1 = {
|
|
|
88
94
|
content: string;
|
|
89
95
|
parts?: ChatMessagePart[];
|
|
90
96
|
turnId?: string;
|
|
97
|
+
clientInputId?: string;
|
|
98
|
+
clientTurnId?: string;
|
|
91
99
|
status?: 'streaming' | 'done' | 'error';
|
|
92
100
|
};
|
|
93
101
|
type ChatState = {
|
|
@@ -96,6 +104,7 @@ type ChatState = {
|
|
|
96
104
|
messages: ChatMessage$1[];
|
|
97
105
|
activitiesByTurnId: Record<string, AgentActivity[]>;
|
|
98
106
|
progressByTurnId: Record<string, AgentRunProgress>;
|
|
107
|
+
runtime?: SessionRuntimeSnapshot;
|
|
99
108
|
status: string;
|
|
100
109
|
contextMessageCount?: number;
|
|
101
110
|
error?: string;
|
|
@@ -131,6 +140,7 @@ type ChatAction = {
|
|
|
131
140
|
sessionId: string;
|
|
132
141
|
messages: ChatMessage$1[];
|
|
133
142
|
activities?: AgentActivity[];
|
|
143
|
+
runtime?: SessionRuntimeSnapshot;
|
|
134
144
|
} | {
|
|
135
145
|
type: 'session.load.failed';
|
|
136
146
|
sessionId: string;
|
|
@@ -141,10 +151,15 @@ type ChatAction = {
|
|
|
141
151
|
id: string;
|
|
142
152
|
content: string;
|
|
143
153
|
parts?: ChatMessagePart[];
|
|
154
|
+
clientInputId?: string;
|
|
155
|
+
clientTurnId?: string;
|
|
144
156
|
} | {
|
|
145
|
-
type: '
|
|
157
|
+
type: 'session.input.accepted';
|
|
146
158
|
sessionId: string;
|
|
147
159
|
turnId: string;
|
|
160
|
+
clientInputId?: string;
|
|
161
|
+
clientTurnId?: string;
|
|
162
|
+
idempotentReplay?: boolean;
|
|
148
163
|
} | {
|
|
149
164
|
type: 'send.failed';
|
|
150
165
|
id: string;
|
|
@@ -204,7 +219,7 @@ type UseCathyGOChatResult = {
|
|
|
204
219
|
dispatchChat: Dispatch<ChatAction>;
|
|
205
220
|
resetChat: () => void;
|
|
206
221
|
createSession: (sessionId: string) => void;
|
|
207
|
-
loadSession: (sessionId: string, messages: ChatMessage$1[], activities?: ChatState['activitiesByTurnId'][string]) => void;
|
|
222
|
+
loadSession: (sessionId: string, messages: ChatMessage$1[], activities?: ChatState['activitiesByTurnId'][string], runtime?: SessionRuntimeSnapshot) => void;
|
|
208
223
|
applyTurnEvent: (event: LearningTurnEvent) => void;
|
|
209
224
|
};
|
|
210
225
|
declare function useCathyGOChat(initialState?: ChatState): UseCathyGOChatResult;
|
package/dist/index.js
CHANGED
|
@@ -1459,7 +1459,7 @@ function ChatMessage({ message }) {
|
|
|
1459
1459
|
"img",
|
|
1460
1460
|
{
|
|
1461
1461
|
alt: part.attachment.original_name ?? "Attached image",
|
|
1462
|
-
src: part.attachment
|
|
1462
|
+
src: attachmentImageSrc(part.attachment)
|
|
1463
1463
|
},
|
|
1464
1464
|
part.attachment_id
|
|
1465
1465
|
) : null
|
|
@@ -1481,6 +1481,18 @@ function ChatMessage({ message }) {
|
|
|
1481
1481
|
}
|
|
1482
1482
|
);
|
|
1483
1483
|
}
|
|
1484
|
+
function attachmentImageSrc(attachment) {
|
|
1485
|
+
if (isRenderableUri(attachment.uri)) return attachment.uri;
|
|
1486
|
+
const thumbnail = attachment.thumbnail;
|
|
1487
|
+
if (thumbnail?.mime_type && thumbnail.data_base64) {
|
|
1488
|
+
return `data:${thumbnail.mime_type};base64,${thumbnail.data_base64}`;
|
|
1489
|
+
}
|
|
1490
|
+
return attachment.uri;
|
|
1491
|
+
}
|
|
1492
|
+
function isRenderableUri(uri) {
|
|
1493
|
+
if (!uri) return false;
|
|
1494
|
+
return uri.startsWith("blob:") || uri.startsWith("data:") || uri.startsWith("http://") || uri.startsWith("https://");
|
|
1495
|
+
}
|
|
1484
1496
|
|
|
1485
1497
|
// src/chat-ui/components/ScrollToBottomButton.tsx
|
|
1486
1498
|
import { useEffect as useEffect3, useState as useState5 } from "react";
|
|
@@ -1668,7 +1680,7 @@ function ChatView({
|
|
|
1668
1680
|
onSettingsOpen
|
|
1669
1681
|
}) {
|
|
1670
1682
|
const loadingSession = chat.status === "Loading";
|
|
1671
|
-
const busy = Boolean(
|
|
1683
|
+
const busy = Boolean(externalBusy) || loadingSession;
|
|
1672
1684
|
const ready = Boolean(chat.sessionId) && !loadingSession;
|
|
1673
1685
|
const usingMock = model?.provider === "mock";
|
|
1674
1686
|
const modelLabel = model ? model.model || model.display_name || model.provider : "loading model status";
|
|
@@ -1715,7 +1727,7 @@ function ChatView({
|
|
|
1715
1727
|
activitiesByTurnId: chat.activitiesByTurnId,
|
|
1716
1728
|
progressByTurnId: chat.progressByTurnId,
|
|
1717
1729
|
messages: chat.messages,
|
|
1718
|
-
onSuggest: ready && !busy ? onSuggest : void 0
|
|
1730
|
+
onSuggest: ready && !busy && !chat.activeTurnId ? onSuggest : void 0
|
|
1719
1731
|
}
|
|
1720
1732
|
),
|
|
1721
1733
|
!loadingSession && chat.status === "Error" && chat.error ? /* @__PURE__ */ jsx13("p", { className: "chat-session-error", children: chat.error }) : null,
|
|
@@ -1875,14 +1887,18 @@ function reduceChat(state, action) {
|
|
|
1875
1887
|
};
|
|
1876
1888
|
}
|
|
1877
1889
|
if (action.type === "session.loaded") {
|
|
1890
|
+
const runtime = action.runtime;
|
|
1891
|
+
const activeTurnId = optionalString(runtime?.active_turn_id);
|
|
1892
|
+
const messages = reconcileLoadedMessages(state, action.sessionId, action.messages);
|
|
1878
1893
|
return {
|
|
1879
1894
|
...state,
|
|
1880
1895
|
sessionId: action.sessionId,
|
|
1881
|
-
activeTurnId
|
|
1882
|
-
messages
|
|
1896
|
+
activeTurnId,
|
|
1897
|
+
messages,
|
|
1883
1898
|
activitiesByTurnId: groupActivitiesByTurn(action.activities ?? []),
|
|
1884
|
-
progressByTurnId: {},
|
|
1885
|
-
|
|
1899
|
+
progressByTurnId: activeTurnId ? setProgress({}, runtimeProgressFromSnapshot(runtime, activeTurnId)) : {},
|
|
1900
|
+
runtime,
|
|
1901
|
+
status: statusFromRuntime(runtime),
|
|
1886
1902
|
error: void 0,
|
|
1887
1903
|
errorCode: void 0
|
|
1888
1904
|
};
|
|
@@ -1908,6 +1924,8 @@ function reduceChat(state, action) {
|
|
|
1908
1924
|
role: "user",
|
|
1909
1925
|
content: action.content,
|
|
1910
1926
|
parts: action.parts,
|
|
1927
|
+
clientInputId: action.clientInputId,
|
|
1928
|
+
clientTurnId: action.clientTurnId,
|
|
1911
1929
|
status: "done"
|
|
1912
1930
|
}
|
|
1913
1931
|
],
|
|
@@ -1929,11 +1947,24 @@ function reduceChat(state, action) {
|
|
|
1929
1947
|
errorCode: action.code
|
|
1930
1948
|
};
|
|
1931
1949
|
}
|
|
1932
|
-
if (action.type === "
|
|
1950
|
+
if (action.type === "session.input.accepted") {
|
|
1951
|
+
const messages = attachClientIdsToPendingUser(state.messages, {
|
|
1952
|
+
clientInputId: action.clientInputId,
|
|
1953
|
+
clientTurnId: action.clientTurnId
|
|
1954
|
+
});
|
|
1933
1955
|
return {
|
|
1934
1956
|
...state,
|
|
1935
1957
|
sessionId: action.sessionId,
|
|
1936
1958
|
activeTurnId: action.turnId,
|
|
1959
|
+
messages,
|
|
1960
|
+
runtime: {
|
|
1961
|
+
...state.runtime ?? emptyRuntimeSnapshot(),
|
|
1962
|
+
status: "running",
|
|
1963
|
+
active_turn_id: action.turnId,
|
|
1964
|
+
active_turn_ids: [action.turnId],
|
|
1965
|
+
active_client_input_id: action.clientInputId ?? null,
|
|
1966
|
+
active_client_turn_id: action.clientTurnId ?? null
|
|
1967
|
+
},
|
|
1937
1968
|
progressByTurnId: setProgress(state.progressByTurnId, {
|
|
1938
1969
|
turnId: action.turnId,
|
|
1939
1970
|
phase: "queued",
|
|
@@ -1949,12 +1980,65 @@ function reduceChat(state, action) {
|
|
|
1949
1980
|
}
|
|
1950
1981
|
const event = action.event;
|
|
1951
1982
|
const payload = event.payload;
|
|
1983
|
+
if (isDuplicateEvent(state.runtime, event)) {
|
|
1984
|
+
return state;
|
|
1985
|
+
}
|
|
1952
1986
|
const next = {
|
|
1953
1987
|
...state,
|
|
1954
1988
|
eventCount: state.eventCount + 1,
|
|
1955
1989
|
sessionId: payload.session_id ?? state.sessionId,
|
|
1956
|
-
activeTurnId: payload.turn_id ?? state.activeTurnId
|
|
1990
|
+
activeTurnId: payload.turn_id ?? state.activeTurnId,
|
|
1991
|
+
runtime: runtimeFromEvent(state.runtime, event)
|
|
1957
1992
|
};
|
|
1993
|
+
if (event.event === "session.created") {
|
|
1994
|
+
return {
|
|
1995
|
+
...next,
|
|
1996
|
+
status: "Ready",
|
|
1997
|
+
error: void 0,
|
|
1998
|
+
errorCode: void 0
|
|
1999
|
+
};
|
|
2000
|
+
}
|
|
2001
|
+
if (event.event === "session.deleted") {
|
|
2002
|
+
return {
|
|
2003
|
+
...next,
|
|
2004
|
+
activeTurnId: void 0,
|
|
2005
|
+
progressByTurnId: {},
|
|
2006
|
+
status: "Archived"
|
|
2007
|
+
};
|
|
2008
|
+
}
|
|
2009
|
+
if (event.event === "session.input.accepted") {
|
|
2010
|
+
const turnId = optionalString(payload.turn_id);
|
|
2011
|
+
if (!turnId) return next;
|
|
2012
|
+
return {
|
|
2013
|
+
...next,
|
|
2014
|
+
activeTurnId: turnId,
|
|
2015
|
+
progressByTurnId: setProgress(next.progressByTurnId, {
|
|
2016
|
+
turnId,
|
|
2017
|
+
phase: "queued",
|
|
2018
|
+
status: "running",
|
|
2019
|
+
summary: "\u6B63\u5728\u63D0\u4EA4\u95EE\u9898",
|
|
2020
|
+
detail: "\u7B49\u5F85 CathyGO \u5F00\u59CB\u5904\u7406",
|
|
2021
|
+
startedAt: nowIso()
|
|
2022
|
+
}),
|
|
2023
|
+
status: "Thinking",
|
|
2024
|
+
error: void 0,
|
|
2025
|
+
errorCode: void 0
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
2028
|
+
if (event.event === "session.stop.requested") {
|
|
2029
|
+
return {
|
|
2030
|
+
...next,
|
|
2031
|
+
status: "Stopping"
|
|
2032
|
+
};
|
|
2033
|
+
}
|
|
2034
|
+
if (event.event === "user.message.created") {
|
|
2035
|
+
const message = userMessageFromEvent(event);
|
|
2036
|
+
if (!message) return next;
|
|
2037
|
+
return {
|
|
2038
|
+
...next,
|
|
2039
|
+
messages: dedupeMessages([...next.messages, message])
|
|
2040
|
+
};
|
|
2041
|
+
}
|
|
1958
2042
|
if (event.event === "agent.progress.delta") {
|
|
1959
2043
|
return {
|
|
1960
2044
|
...next,
|
|
@@ -2020,7 +2104,8 @@ function reduceChat(state, action) {
|
|
|
2020
2104
|
next.messages,
|
|
2021
2105
|
payload.message_id,
|
|
2022
2106
|
finalMessage,
|
|
2023
|
-
payload.turn_id
|
|
2107
|
+
payload.turn_id,
|
|
2108
|
+
optionalString(payload.persisted_message_id)
|
|
2024
2109
|
)
|
|
2025
2110
|
};
|
|
2026
2111
|
}
|
|
@@ -2029,6 +2114,7 @@ function reduceChat(state, action) {
|
|
|
2029
2114
|
...next,
|
|
2030
2115
|
activeTurnId: void 0,
|
|
2031
2116
|
progressByTurnId: removeProgress(next.progressByTurnId, payload.turn_id),
|
|
2117
|
+
runtime: markRuntimeIdle(next.runtime),
|
|
2032
2118
|
status: "Ready"
|
|
2033
2119
|
};
|
|
2034
2120
|
}
|
|
@@ -2037,6 +2123,7 @@ function reduceChat(state, action) {
|
|
|
2037
2123
|
...next,
|
|
2038
2124
|
activeTurnId: void 0,
|
|
2039
2125
|
progressByTurnId: removeProgress(next.progressByTurnId, payload.turn_id),
|
|
2126
|
+
runtime: markRuntimeIdle(next.runtime),
|
|
2040
2127
|
status: "Ready"
|
|
2041
2128
|
};
|
|
2042
2129
|
}
|
|
@@ -2046,6 +2133,7 @@ function reduceChat(state, action) {
|
|
|
2046
2133
|
...next,
|
|
2047
2134
|
activeTurnId: void 0,
|
|
2048
2135
|
progressByTurnId: removeProgress(next.progressByTurnId, payload.turn_id),
|
|
2136
|
+
runtime: markRuntimeIdle(next.runtime),
|
|
2049
2137
|
status: "Error",
|
|
2050
2138
|
error: error?.message ?? "CathyGO turn failed",
|
|
2051
2139
|
errorCode: error?.code
|
|
@@ -2068,6 +2156,10 @@ function appendAssistantDelta(messages, messageId, delta, turnId) {
|
|
|
2068
2156
|
}
|
|
2069
2157
|
];
|
|
2070
2158
|
}
|
|
2159
|
+
const existing = messages[index];
|
|
2160
|
+
if (existing?.role === "assistant" && existing.status === "done" && existing.content) {
|
|
2161
|
+
return messages;
|
|
2162
|
+
}
|
|
2071
2163
|
return messages.map(
|
|
2072
2164
|
(message, itemIndex) => itemIndex === index ? {
|
|
2073
2165
|
...message,
|
|
@@ -2077,20 +2169,34 @@ function appendAssistantDelta(messages, messageId, delta, turnId) {
|
|
|
2077
2169
|
} : message
|
|
2078
2170
|
);
|
|
2079
2171
|
}
|
|
2080
|
-
function finalizeAssistantMessage(messages, messageId, finalMessage, turnId) {
|
|
2172
|
+
function finalizeAssistantMessage(messages, messageId, finalMessage, turnId, persistedMessageId) {
|
|
2081
2173
|
const id = String(messageId || "");
|
|
2082
2174
|
const resolvedTurnId = optionalString(turnId);
|
|
2175
|
+
const existingTurnAssistantIndex = resolvedTurnId ? messages.findIndex(
|
|
2176
|
+
(message) => message.role === "assistant" && message.turnId === resolvedTurnId && message.status === "done" && Boolean(message.content)
|
|
2177
|
+
) : -1;
|
|
2083
2178
|
if (!id && !finalMessage) {
|
|
2084
2179
|
return messages.map(
|
|
2085
2180
|
(message) => message.role === "assistant" && message.status === "streaming" ? { ...message, turnId: message.turnId ?? resolvedTurnId, status: "done" } : message
|
|
2086
2181
|
);
|
|
2087
2182
|
}
|
|
2088
2183
|
const index = messages.findIndex((message) => message.id === id);
|
|
2184
|
+
if (index === -1 && existingTurnAssistantIndex !== -1) {
|
|
2185
|
+
return messages.map(
|
|
2186
|
+
(message, itemIndex) => itemIndex === existingTurnAssistantIndex ? {
|
|
2187
|
+
...message,
|
|
2188
|
+
id: persistedMessageId ?? (id || message.id),
|
|
2189
|
+
content: finalMessage || message.content,
|
|
2190
|
+
turnId: message.turnId ?? resolvedTurnId,
|
|
2191
|
+
status: "done"
|
|
2192
|
+
} : message
|
|
2193
|
+
);
|
|
2194
|
+
}
|
|
2089
2195
|
if (index === -1 && finalMessage) {
|
|
2090
2196
|
return [
|
|
2091
2197
|
...messages,
|
|
2092
2198
|
{
|
|
2093
|
-
id: id || `msg_done_${messages.length}
|
|
2199
|
+
id: persistedMessageId ?? (id || `msg_done_${messages.length}`),
|
|
2094
2200
|
role: "assistant",
|
|
2095
2201
|
content: finalMessage,
|
|
2096
2202
|
turnId: resolvedTurnId,
|
|
@@ -2101,12 +2207,212 @@ function finalizeAssistantMessage(messages, messageId, finalMessage, turnId) {
|
|
|
2101
2207
|
return messages.map(
|
|
2102
2208
|
(message, itemIndex) => itemIndex === index || message.role === "assistant" && message.status === "streaming" ? {
|
|
2103
2209
|
...message,
|
|
2210
|
+
id: itemIndex === index && persistedMessageId && message.id === id ? persistedMessageId : message.id,
|
|
2104
2211
|
content: message.content || finalMessage,
|
|
2105
2212
|
turnId: message.turnId ?? resolvedTurnId,
|
|
2106
2213
|
status: "done"
|
|
2107
2214
|
} : message
|
|
2108
2215
|
);
|
|
2109
2216
|
}
|
|
2217
|
+
function reconcileLoadedMessages(state, sessionId, loadedMessages) {
|
|
2218
|
+
if (state.sessionId !== sessionId || state.messages.length === 0) {
|
|
2219
|
+
return dedupeMessages(loadedMessages);
|
|
2220
|
+
}
|
|
2221
|
+
const localMessagesByClientId = /* @__PURE__ */ new Map();
|
|
2222
|
+
for (const message of state.messages) {
|
|
2223
|
+
if (message.role !== "user") continue;
|
|
2224
|
+
for (const key of messageClientKeys(message)) {
|
|
2225
|
+
localMessagesByClientId.set(key, message);
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
const reconciled = loadedMessages.map((message) => {
|
|
2229
|
+
if (message.role !== "user") return message;
|
|
2230
|
+
const local = messageClientKeys(message).map((key) => localMessagesByClientId.get(key)).find((item) => Boolean(item));
|
|
2231
|
+
if (!local) return message;
|
|
2232
|
+
return mergeLoadedUserMessage(local, message);
|
|
2233
|
+
});
|
|
2234
|
+
const loadedClientKeys = new Set(reconciled.flatMap(messageClientKeys));
|
|
2235
|
+
const pendingLocalMessages = state.messages.filter((message) => {
|
|
2236
|
+
if (message.role !== "user" || message.status === "error") return false;
|
|
2237
|
+
const keys = messageClientKeys(message);
|
|
2238
|
+
return keys.length > 0 && keys.every((key) => !loadedClientKeys.has(key));
|
|
2239
|
+
});
|
|
2240
|
+
return dedupeMessages([...reconciled, ...pendingLocalMessages]);
|
|
2241
|
+
}
|
|
2242
|
+
function mergeLoadedUserMessage(local, loaded) {
|
|
2243
|
+
return {
|
|
2244
|
+
...loaded,
|
|
2245
|
+
content: loaded.content || local.content,
|
|
2246
|
+
parts: mergeMessageParts(local.parts, loaded.parts),
|
|
2247
|
+
clientInputId: loaded.clientInputId ?? local.clientInputId,
|
|
2248
|
+
clientTurnId: loaded.clientTurnId ?? local.clientTurnId,
|
|
2249
|
+
status: loaded.status ?? "done"
|
|
2250
|
+
};
|
|
2251
|
+
}
|
|
2252
|
+
function mergeMessageParts(localParts, loadedParts) {
|
|
2253
|
+
if (!loadedParts?.length) return localParts;
|
|
2254
|
+
if (!localParts?.length) return loadedParts;
|
|
2255
|
+
return loadedParts.map((part) => {
|
|
2256
|
+
if (part.type !== "image") return part;
|
|
2257
|
+
const local = localParts.find(
|
|
2258
|
+
(item) => item.type === "image" && item.attachment_id === part.attachment_id
|
|
2259
|
+
);
|
|
2260
|
+
if (!local) return part;
|
|
2261
|
+
if (isLocalAttachmentPreview(local.attachment.uri) && !isReachableAttachmentUri(part.attachment.uri)) {
|
|
2262
|
+
return {
|
|
2263
|
+
...part,
|
|
2264
|
+
attachment: {
|
|
2265
|
+
...part.attachment,
|
|
2266
|
+
uri: local.attachment.uri
|
|
2267
|
+
}
|
|
2268
|
+
};
|
|
2269
|
+
}
|
|
2270
|
+
return part;
|
|
2271
|
+
});
|
|
2272
|
+
}
|
|
2273
|
+
function userMessageFromEvent(event) {
|
|
2274
|
+
const payload = event.payload;
|
|
2275
|
+
const messageId = optionalString(payload.message_id);
|
|
2276
|
+
if (!messageId) return void 0;
|
|
2277
|
+
const metadata = plainRecord(payload.metadata);
|
|
2278
|
+
return {
|
|
2279
|
+
id: messageId,
|
|
2280
|
+
role: "user",
|
|
2281
|
+
content: optionalString(payload.message) ?? "",
|
|
2282
|
+
parts: chatPartsFromPayload(payload.parts),
|
|
2283
|
+
turnId: optionalString(payload.turn_id),
|
|
2284
|
+
clientInputId: optionalString(payload.client_input_id) ?? metadataString(metadata, "client_input_id"),
|
|
2285
|
+
clientTurnId: optionalString(payload.client_turn_id) ?? metadataString(metadata, "client_turn_id"),
|
|
2286
|
+
status: "done"
|
|
2287
|
+
};
|
|
2288
|
+
}
|
|
2289
|
+
function chatPartsFromPayload(parts) {
|
|
2290
|
+
if (!Array.isArray(parts)) return void 0;
|
|
2291
|
+
const normalized = parts.map((part) => {
|
|
2292
|
+
const raw = plainRecord(part);
|
|
2293
|
+
if (!raw) return void 0;
|
|
2294
|
+
const type = optionalString(raw.type);
|
|
2295
|
+
if (type === "text") {
|
|
2296
|
+
const text = optionalString(raw.text);
|
|
2297
|
+
return text ? { type: "text", text } : void 0;
|
|
2298
|
+
}
|
|
2299
|
+
if (type === "image") {
|
|
2300
|
+
const attachmentId = optionalString(raw.attachment_id);
|
|
2301
|
+
const attachment = chatAttachmentFromPayload(raw.attachment, attachmentId);
|
|
2302
|
+
if (!attachmentId || !attachment) return void 0;
|
|
2303
|
+
return {
|
|
2304
|
+
type: "image",
|
|
2305
|
+
attachment_id: attachmentId,
|
|
2306
|
+
attachment
|
|
2307
|
+
};
|
|
2308
|
+
}
|
|
2309
|
+
return void 0;
|
|
2310
|
+
}).filter((part) => Boolean(part));
|
|
2311
|
+
return normalized.length ? normalized : void 0;
|
|
2312
|
+
}
|
|
2313
|
+
function chatAttachmentFromPayload(value, attachmentId) {
|
|
2314
|
+
const raw = plainRecord(value);
|
|
2315
|
+
if (!raw && !attachmentId) return void 0;
|
|
2316
|
+
return {
|
|
2317
|
+
id: optionalString(raw?.id) ?? attachmentId ?? "",
|
|
2318
|
+
kind: "image",
|
|
2319
|
+
uri: optionalString(raw?.uri) ?? "",
|
|
2320
|
+
original_name: optionalString(raw?.original_name) ?? null,
|
|
2321
|
+
mime_type: optionalString(raw?.mime_type) ?? null,
|
|
2322
|
+
size_bytes: optionalNumber(raw?.size_bytes),
|
|
2323
|
+
sha256: optionalString(raw?.sha256) ?? null,
|
|
2324
|
+
width: optionalNumber(raw?.width),
|
|
2325
|
+
height: optionalNumber(raw?.height),
|
|
2326
|
+
created_at: optionalString(raw?.created_at) ?? null,
|
|
2327
|
+
thumbnail: thumbnailFromPayload(raw?.thumbnail)
|
|
2328
|
+
};
|
|
2329
|
+
}
|
|
2330
|
+
function thumbnailFromPayload(value) {
|
|
2331
|
+
const raw = plainRecord(value);
|
|
2332
|
+
if (!raw) return null;
|
|
2333
|
+
const mimeType = optionalString(raw.mime_type);
|
|
2334
|
+
const dataBase64 = optionalString(raw.data_base64);
|
|
2335
|
+
if (!mimeType || !dataBase64) return null;
|
|
2336
|
+
return {
|
|
2337
|
+
mime_type: mimeType,
|
|
2338
|
+
data_base64: dataBase64,
|
|
2339
|
+
width: optionalNumber(raw.width),
|
|
2340
|
+
height: optionalNumber(raw.height)
|
|
2341
|
+
};
|
|
2342
|
+
}
|
|
2343
|
+
function dedupeMessages(messages) {
|
|
2344
|
+
const next = [];
|
|
2345
|
+
for (const message of messages) {
|
|
2346
|
+
const duplicateIndex = next.findIndex((item) => messagesMatch(item, message));
|
|
2347
|
+
if (duplicateIndex === -1) {
|
|
2348
|
+
next.push(message);
|
|
2349
|
+
continue;
|
|
2350
|
+
}
|
|
2351
|
+
next[duplicateIndex] = mergeDuplicateMessage(next[duplicateIndex], message);
|
|
2352
|
+
}
|
|
2353
|
+
return next;
|
|
2354
|
+
}
|
|
2355
|
+
function messagesMatch(left, right) {
|
|
2356
|
+
if (left.id === right.id) return true;
|
|
2357
|
+
if (left.role !== right.role) return false;
|
|
2358
|
+
if (left.role === "assistant" && left.turnId && left.turnId === right.turnId) return true;
|
|
2359
|
+
const leftKeys = new Set(messageClientKeys(left));
|
|
2360
|
+
return messageClientKeys(right).some((key) => leftKeys.has(key));
|
|
2361
|
+
}
|
|
2362
|
+
function mergeDuplicateMessage(base, update) {
|
|
2363
|
+
const preferUpdate = base.id.startsWith("user_") && !update.id.startsWith("user_") || base.id.startsWith("msg_") && !update.id.startsWith("msg_") || update.status === "done";
|
|
2364
|
+
const primary = preferUpdate ? update : base;
|
|
2365
|
+
const secondary = preferUpdate ? base : update;
|
|
2366
|
+
return {
|
|
2367
|
+
...primary,
|
|
2368
|
+
content: primary.content || secondary.content,
|
|
2369
|
+
parts: mergeMessageParts(secondary.parts, primary.parts),
|
|
2370
|
+
turnId: primary.turnId ?? secondary.turnId,
|
|
2371
|
+
clientInputId: primary.clientInputId ?? secondary.clientInputId,
|
|
2372
|
+
clientTurnId: primary.clientTurnId ?? secondary.clientTurnId,
|
|
2373
|
+
status: primary.status ?? secondary.status
|
|
2374
|
+
};
|
|
2375
|
+
}
|
|
2376
|
+
function attachClientIdsToPendingUser(messages, ids) {
|
|
2377
|
+
if (!ids.clientInputId && !ids.clientTurnId) return messages;
|
|
2378
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
2379
|
+
const message = messages[index];
|
|
2380
|
+
if (message.role !== "user") continue;
|
|
2381
|
+
if (message.clientInputId || message.clientTurnId) return messages;
|
|
2382
|
+
return messages.map(
|
|
2383
|
+
(item, itemIndex) => itemIndex === index ? {
|
|
2384
|
+
...item,
|
|
2385
|
+
clientInputId: ids.clientInputId,
|
|
2386
|
+
clientTurnId: ids.clientTurnId
|
|
2387
|
+
} : item
|
|
2388
|
+
);
|
|
2389
|
+
}
|
|
2390
|
+
return messages;
|
|
2391
|
+
}
|
|
2392
|
+
function messageClientKeys(message) {
|
|
2393
|
+
return [message.clientInputId, message.clientTurnId].map((value) => optionalString(value)).filter((value) => Boolean(value));
|
|
2394
|
+
}
|
|
2395
|
+
function metadataString(metadata, key) {
|
|
2396
|
+
return optionalString(metadata?.[key]);
|
|
2397
|
+
}
|
|
2398
|
+
function isDuplicateEvent(runtime, event) {
|
|
2399
|
+
if (!event.seq || event.seq <= 0) return false;
|
|
2400
|
+
return event.seq <= (runtime?.last_event_seq ?? 0);
|
|
2401
|
+
}
|
|
2402
|
+
function plainRecord(value) {
|
|
2403
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
2404
|
+
return value;
|
|
2405
|
+
}
|
|
2406
|
+
function optionalNumber(value) {
|
|
2407
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
2408
|
+
}
|
|
2409
|
+
function isLocalAttachmentPreview(uri) {
|
|
2410
|
+
return Boolean(uri?.startsWith("blob:") || uri?.startsWith("data:"));
|
|
2411
|
+
}
|
|
2412
|
+
function isReachableAttachmentUri(uri) {
|
|
2413
|
+
if (!uri) return false;
|
|
2414
|
+
return uri.startsWith("blob:") || uri.startsWith("data:") || uri.startsWith("http://") || uri.startsWith("https://");
|
|
2415
|
+
}
|
|
2110
2416
|
function isAgentActivityEvent(event) {
|
|
2111
2417
|
return event.event.startsWith("agent.activity.");
|
|
2112
2418
|
}
|
|
@@ -2217,6 +2523,78 @@ function progressFromActivity(progressByTurnId, activity, turnActivities, messag
|
|
|
2217
2523
|
}
|
|
2218
2524
|
return progressByTurnId;
|
|
2219
2525
|
}
|
|
2526
|
+
function runtimeProgressFromSnapshot(runtime, turnId) {
|
|
2527
|
+
return {
|
|
2528
|
+
turnId,
|
|
2529
|
+
phase: runtime?.status === "stopping" ? "model" : "queued",
|
|
2530
|
+
status: "running",
|
|
2531
|
+
summary: runtime?.status === "stopping" ? "\u6B63\u5728\u505C\u6B62" : "\u6B63\u5728\u6062\u590D\u4F1A\u8BDD\u72B6\u6001",
|
|
2532
|
+
detail: runtime?.active_client_input_id ? `client_input_id=${runtime.active_client_input_id}` : void 0,
|
|
2533
|
+
startedAt: nowIso()
|
|
2534
|
+
};
|
|
2535
|
+
}
|
|
2536
|
+
function runtimeFromEvent(current, event) {
|
|
2537
|
+
const payload = event.payload;
|
|
2538
|
+
const turnId = optionalString(payload.turn_id);
|
|
2539
|
+
const turnIds = eventTurnIds(payload, turnId);
|
|
2540
|
+
const base = {
|
|
2541
|
+
...current ?? emptyRuntimeSnapshot(),
|
|
2542
|
+
last_event_seq: Math.max(current?.last_event_seq ?? 0, event.seq)
|
|
2543
|
+
};
|
|
2544
|
+
if (event.event === "session.input.accepted" || event.event === "turn.started") {
|
|
2545
|
+
return {
|
|
2546
|
+
...base,
|
|
2547
|
+
status: "running",
|
|
2548
|
+
active_turn_id: turnId ?? base.active_turn_id ?? null,
|
|
2549
|
+
active_turn_ids: turnIds.length > 0 ? turnIds : base.active_turn_ids ?? [],
|
|
2550
|
+
active_client_input_id: optionalString(payload.client_input_id) ?? base.active_client_input_id ?? null,
|
|
2551
|
+
active_client_turn_id: optionalString(payload.client_turn_id) ?? base.active_client_turn_id ?? null
|
|
2552
|
+
};
|
|
2553
|
+
}
|
|
2554
|
+
if (event.event === "session.stop.requested") {
|
|
2555
|
+
return {
|
|
2556
|
+
...base,
|
|
2557
|
+
status: "stopping",
|
|
2558
|
+
active_turn_id: turnId ?? base.active_turn_id ?? null,
|
|
2559
|
+
active_turn_ids: turnIds.length > 0 ? turnIds : base.active_turn_ids ?? []
|
|
2560
|
+
};
|
|
2561
|
+
}
|
|
2562
|
+
if (event.event === "turn.completed" || event.event === "turn.cancelled" || event.event === "turn.failed" || event.event === "session.deleted") {
|
|
2563
|
+
return markRuntimeIdle(base);
|
|
2564
|
+
}
|
|
2565
|
+
return base;
|
|
2566
|
+
}
|
|
2567
|
+
function eventTurnIds(payload, turnId) {
|
|
2568
|
+
if (Array.isArray(payload.turn_ids)) {
|
|
2569
|
+
return payload.turn_ids.map((value) => optionalString(value)).filter((value) => Boolean(value));
|
|
2570
|
+
}
|
|
2571
|
+
return turnId ? [turnId] : [];
|
|
2572
|
+
}
|
|
2573
|
+
function emptyRuntimeSnapshot() {
|
|
2574
|
+
return {
|
|
2575
|
+
status: "idle",
|
|
2576
|
+
active_turn_id: null,
|
|
2577
|
+
active_turn_ids: [],
|
|
2578
|
+
active_client_input_id: null,
|
|
2579
|
+
active_client_turn_id: null,
|
|
2580
|
+
last_event_seq: 0
|
|
2581
|
+
};
|
|
2582
|
+
}
|
|
2583
|
+
function markRuntimeIdle(runtime) {
|
|
2584
|
+
return {
|
|
2585
|
+
...runtime ?? emptyRuntimeSnapshot(),
|
|
2586
|
+
status: "idle",
|
|
2587
|
+
active_turn_id: null,
|
|
2588
|
+
active_turn_ids: [],
|
|
2589
|
+
active_client_input_id: null,
|
|
2590
|
+
active_client_turn_id: null
|
|
2591
|
+
};
|
|
2592
|
+
}
|
|
2593
|
+
function statusFromRuntime(runtime) {
|
|
2594
|
+
if (runtime?.status === "running") return "Thinking";
|
|
2595
|
+
if (runtime?.status === "stopping") return "Stopping";
|
|
2596
|
+
return "Ready";
|
|
2597
|
+
}
|
|
2220
2598
|
function setProgress(progressByTurnId, progress) {
|
|
2221
2599
|
return {
|
|
2222
2600
|
...progressByTurnId,
|
|
@@ -2314,8 +2692,8 @@ function useCathyGOChat(initialState = initialChatState) {
|
|
|
2314
2692
|
dispatchChat({ type: "session.created", sessionId });
|
|
2315
2693
|
}, []);
|
|
2316
2694
|
const loadSession = useCallback(
|
|
2317
|
-
(sessionId, messages, activities) => {
|
|
2318
|
-
dispatchChat({ type: "session.loaded", sessionId, messages, activities });
|
|
2695
|
+
(sessionId, messages, activities, runtime) => {
|
|
2696
|
+
dispatchChat({ type: "session.loaded", sessionId, messages, activities, runtime });
|
|
2319
2697
|
},
|
|
2320
2698
|
[]
|
|
2321
2699
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beanx/cathygo-web-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"./styles.css": "./dist/styles.css"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@beanx/cathygo-protocol": "0.1.
|
|
19
|
+
"@beanx/cathygo-protocol": "0.1.2",
|
|
20
20
|
"@streamdown/math": "^1.0.2",
|
|
21
21
|
"katex": "^0.16.47",
|
|
22
22
|
"streamdown": "^2.5.0"
|