dsh-monkeyai-sessions 0.1.7 → 0.1.8
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 +1 -1
- package/README.zh.md +1 -1
- package/client.js +147 -91
- package/lib/client-runtime.cjs +147 -91
- package/package.json +1 -1
- package/src/client/index.js +140 -92
package/README.md
CHANGED
|
@@ -61,7 +61,7 @@ From a checkout:
|
|
|
61
61
|
npm install
|
|
62
62
|
npm run build
|
|
63
63
|
npm pack
|
|
64
|
-
dsh plugin --profile web add ./dsh-monkeyai-sessions-0.1.
|
|
64
|
+
dsh plugin --profile web add ./dsh-monkeyai-sessions-0.1.8.tgz
|
|
65
65
|
```
|
|
66
66
|
|
|
67
67
|
Verified against a real DSH `0.2.2-rc.1` web profile and a live MonkeyAI runtime: authorization, full history loading, prompt delivery, model switching, and turn cancellation all work end to end.
|
package/README.zh.md
CHANGED
|
@@ -28,7 +28,7 @@ https://artenx.github.io/dsh-monkeyai-sessions/index.json
|
|
|
28
28
|
npm install
|
|
29
29
|
npm run build
|
|
30
30
|
npm pack
|
|
31
|
-
dsh plugin --profile web add ./dsh-monkeyai-sessions-0.1.
|
|
31
|
+
dsh plugin --profile web add ./dsh-monkeyai-sessions-0.1.8.tgz
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
已在真实的 DSH `0.2.2-rc.1` web profile 与在线 MonkeyAI runtime 上端到端验证:授权、完整历史加载、指令下发、模型切换、回合取消均正常工作。
|
package/client.js
CHANGED
|
@@ -606,6 +606,24 @@ window.__ModuleLoader__.load({
|
|
|
606
606
|
function errorText(error) {
|
|
607
607
|
return typeof error?.message === "string" ? error.message : typeof error === "string" ? error : "\u8FDC\u7A0B\u64CD\u4F5C\u5931\u8D25";
|
|
608
608
|
}
|
|
609
|
+
function withTimeout(promise, timeoutMs, label) {
|
|
610
|
+
return new Promise((resolve, reject) => {
|
|
611
|
+
const timer = setTimeout(() => reject(new Error(`${label}\u8D85\u65F6\uFF0C\u8BF7\u91CD\u8BD5`)), timeoutMs);
|
|
612
|
+
Promise.resolve(promise).then(
|
|
613
|
+
(value) => {
|
|
614
|
+
clearTimeout(timer);
|
|
615
|
+
resolve(value);
|
|
616
|
+
},
|
|
617
|
+
(error) => {
|
|
618
|
+
clearTimeout(timer);
|
|
619
|
+
reject(error);
|
|
620
|
+
}
|
|
621
|
+
);
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
function isRemoteStreamCarrierError(error) {
|
|
625
|
+
return /api gateway: remote stream websocket (?:failed|closed)/i.test(errorText(error));
|
|
626
|
+
}
|
|
609
627
|
function renderMarkdown(text) {
|
|
610
628
|
if (typeof text !== "string" || text === "") return null;
|
|
611
629
|
const blocks = [];
|
|
@@ -829,6 +847,7 @@ window.__ModuleLoader__.load({
|
|
|
829
847
|
const authWindowRef = useRef(null);
|
|
830
848
|
const eventsRef = useRef(null);
|
|
831
849
|
const prevScrollHeightRef = useRef(null);
|
|
850
|
+
const streamGenerationRef = useRef(0);
|
|
832
851
|
const title = useMemo(() => {
|
|
833
852
|
const runtime = runtimeSessions.find((item) => item.id === sessionId);
|
|
834
853
|
const summary = summarySessions.find((item) => item.id === sessionId);
|
|
@@ -849,6 +868,7 @@ window.__ModuleLoader__.load({
|
|
|
849
868
|
});
|
|
850
869
|
return () => {
|
|
851
870
|
active = false;
|
|
871
|
+
streamGenerationRef.current += 1;
|
|
852
872
|
streamRef.current?.dispose();
|
|
853
873
|
streamRef.current = null;
|
|
854
874
|
};
|
|
@@ -1011,6 +1031,7 @@ window.__ModuleLoader__.load({
|
|
|
1011
1031
|
setError("");
|
|
1012
1032
|
setLoading(true);
|
|
1013
1033
|
try {
|
|
1034
|
+
streamGenerationRef.current += 1;
|
|
1014
1035
|
const result = await unwrap(api.listDevices({ accountId, page: 1 }));
|
|
1015
1036
|
setDevices(result.items);
|
|
1016
1037
|
const first = result.items.length > 0 ? result.items[0].machineId : "";
|
|
@@ -1034,24 +1055,32 @@ window.__ModuleLoader__.load({
|
|
|
1034
1055
|
setError("");
|
|
1035
1056
|
setLoading(true);
|
|
1036
1057
|
setSessionId("");
|
|
1058
|
+
streamGenerationRef.current += 1;
|
|
1037
1059
|
streamRef.current?.dispose();
|
|
1038
1060
|
streamRef.current = null;
|
|
1039
1061
|
setFrames([]);
|
|
1040
1062
|
setPendingRequests([]);
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1063
|
+
try {
|
|
1064
|
+
const results = await Promise.allSettled([
|
|
1065
|
+
withTimeout(Promise.resolve().then(() => unwrap(api.listSessions({ accountId, machineId: mid, page: 1, pageSize: 50 }))), 5e4, "\u4F1A\u8BDD\u6458\u8981\u8BFB\u53D6"),
|
|
1066
|
+
withTimeout(Promise.resolve().then(() => unwrap(api.listRuntimeSessions({ accountId, machineId: mid }))), 5e4, "ACP \u4F1A\u8BDD\u5217\u8868\u8BFB\u53D6")
|
|
1067
|
+
]);
|
|
1068
|
+
const errors = [];
|
|
1069
|
+
if (results[0].status === "fulfilled") setSummarySessions(results[0].value.items);
|
|
1070
|
+
else errors.push(`\u4F1A\u8BDD\u6458\u8981\u7D22\u5F15\uFF1A${errorText(results[0].reason)}`);
|
|
1071
|
+
if (results[1].status === "fulfilled") setRuntimeSessions(results[1].value.sessions);
|
|
1072
|
+
else errors.push(`ACP \u4F1A\u8BDD\u5217\u8868\uFF1A${errorText(results[1].reason)}`);
|
|
1073
|
+
setStatus(errors.length === 0 ? "\u4F1A\u8BDD\u5DF2\u5237\u65B0" : "\u90E8\u5206\u4F1A\u8BDD\u6765\u6E90\u4E0D\u53EF\u7528");
|
|
1074
|
+
setError(errors.join("\n"));
|
|
1075
|
+
} catch (reason) {
|
|
1076
|
+
setStatus("\u4F1A\u8BDD\u5237\u65B0\u5931\u8D25");
|
|
1077
|
+
setError(errorText(reason));
|
|
1078
|
+
} finally {
|
|
1079
|
+
setLoading(false);
|
|
1080
|
+
}
|
|
1053
1081
|
}
|
|
1054
1082
|
async function openSession(session) {
|
|
1083
|
+
const generation = ++streamGenerationRef.current;
|
|
1055
1084
|
streamRef.current?.dispose();
|
|
1056
1085
|
streamRef.current = null;
|
|
1057
1086
|
historyLoadedRef.current = false;
|
|
@@ -1068,90 +1097,116 @@ window.__ModuleLoader__.load({
|
|
|
1068
1097
|
setVisibleCount(RECENT_LIMIT);
|
|
1069
1098
|
setHistoryLoading(true);
|
|
1070
1099
|
setStatus("\u6B63\u5728\u8FDE\u63A5\u8FDC\u7A0B\u4F1A\u8BDD");
|
|
1071
|
-
let
|
|
1072
|
-
|
|
1073
|
-
stream
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
if (frame.type === "reconnecting") setStatus(frame.message || "\u6B63\u5728\u91CD\u8FDE\u8FDC\u7A0B runtime");
|
|
1080
|
-
if (frame.type === "history_loaded") {
|
|
1081
|
-
historyLoadedRef.current = true;
|
|
1082
|
-
setStatus("\u5386\u53F2\u5DF2\u52A0\u8F7D");
|
|
1083
|
-
setHistoryLoading(false);
|
|
1084
|
-
setSessionStatus((current) => current || "idle");
|
|
1085
|
-
if (frame.model) setCurrentModel(frame.model);
|
|
1086
|
-
}
|
|
1087
|
-
if (frame.type === "history_incomplete") {
|
|
1088
|
-
setStatus("\u5386\u53F2\u4E0D\u5B8C\u6574\uFF1A" + (frame.message || "\u56DE\u653E\u88AB\u4E2D\u65AD"));
|
|
1089
|
-
setHistoryLoading(false);
|
|
1090
|
-
}
|
|
1091
|
-
if (frame.type === "model_switched") {
|
|
1092
|
-
if (frame.data?.model) setCurrentModel(frame.data.model);
|
|
1093
|
-
setSwitchingModel(false);
|
|
1094
|
-
setStatus("\u6A21\u578B\u5DF2\u5207\u6362");
|
|
1095
|
-
}
|
|
1096
|
-
if (frame.type === "model_switch_failed") {
|
|
1097
|
-
setSwitchingModel(false);
|
|
1098
|
-
setError(frame.message || "\u6A21\u578B\u5207\u6362\u88AB\u62D2\u7EDD");
|
|
1099
|
-
setStatus("\u6A21\u578B\u5207\u6362\u5931\u8D25");
|
|
1100
|
-
}
|
|
1101
|
-
if (frame.type === "session_missing") {
|
|
1102
|
-
setError(frame.message || "\u8BE5\u4F1A\u8BDD\u5728\u6240\u9009\u8BBE\u5907\u4E0A\u5DF2\u4E0D\u5B58\u5728\u3002");
|
|
1103
|
-
setStatus("\u8BBE\u5907\u4E0A\u627E\u4E0D\u5230\u8BE5\u4F1A\u8BDD");
|
|
1104
|
-
setHistoryLoading(false);
|
|
1105
|
-
}
|
|
1106
|
-
if (frame.type === "request") {
|
|
1107
|
-
setPendingRequests((current) => [...current.filter((item) => item.requestId !== frame.requestId), frame]);
|
|
1100
|
+
let retries = 0;
|
|
1101
|
+
while (streamGenerationRef.current === generation) {
|
|
1102
|
+
let stream;
|
|
1103
|
+
try {
|
|
1104
|
+
stream = api.followSession({ accountId, machineId, sessionId: session.id, cwd: session.cwd });
|
|
1105
|
+
if (streamGenerationRef.current !== generation) {
|
|
1106
|
+
stream.dispose();
|
|
1107
|
+
return;
|
|
1108
1108
|
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1109
|
+
streamRef.current = stream;
|
|
1110
|
+
setStatus(retries > 0 ? "\u8FDC\u7A0B\u4F1A\u8BDD\u5DF2\u91CD\u65B0\u8FDE\u63A5" : "\u8FDC\u7A0B\u4F1A\u8BDD\u5DF2\u8FDE\u63A5");
|
|
1111
|
+
for await (const frame of stream) {
|
|
1112
|
+
setFrames((current) => [...current, frame]);
|
|
1113
|
+
if (frame.type === "ready" && !historyLoadedRef.current) setStatus("\u6B63\u5728\u56DE\u653E\u4F1A\u8BDD\u5386\u53F2");
|
|
1114
|
+
if (frame.type === "reconnecting") setStatus(frame.message || "\u6B63\u5728\u91CD\u8FDE\u8FDC\u7A0B runtime");
|
|
1115
|
+
if (frame.type === "history_loaded") {
|
|
1116
|
+
historyLoadedRef.current = true;
|
|
1117
|
+
setStatus("\u5386\u53F2\u5DF2\u52A0\u8F7D");
|
|
1118
|
+
setHistoryLoading(false);
|
|
1119
|
+
setSessionStatus((current) => current || "idle");
|
|
1120
|
+
if (frame.model) setCurrentModel(frame.model);
|
|
1115
1121
|
}
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1122
|
+
if (frame.type === "history_incomplete") {
|
|
1123
|
+
setStatus("\u5386\u53F2\u4E0D\u5B8C\u6574\uFF1A" + (frame.message || "\u56DE\u653E\u88AB\u4E2D\u65AD"));
|
|
1124
|
+
setHistoryLoading(false);
|
|
1125
|
+
}
|
|
1126
|
+
if (frame.type === "model_switched") {
|
|
1127
|
+
if (frame.data?.model) setCurrentModel(frame.data.model);
|
|
1128
|
+
setSwitchingModel(false);
|
|
1129
|
+
setStatus("\u6A21\u578B\u5DF2\u5207\u6362");
|
|
1130
|
+
}
|
|
1131
|
+
if (frame.type === "model_switch_failed") {
|
|
1132
|
+
setSwitchingModel(false);
|
|
1133
|
+
setError(frame.message || "\u6A21\u578B\u5207\u6362\u5931\u8D25");
|
|
1134
|
+
setStatus("\u6A21\u578B\u5207\u6362\u5931\u8D25");
|
|
1129
1135
|
}
|
|
1136
|
+
if (frame.type === "session_missing") {
|
|
1137
|
+
setError(frame.message || "\u8BE5\u4F1A\u8BDD\u5728\u6240\u9009\u8BBE\u5907\u4E0A\u5DF2\u4E0D\u5B58\u5728\u3002");
|
|
1138
|
+
setStatus("\u8BBE\u5907\u4E0A\u627E\u4E0D\u5230\u8BE5\u4F1A\u8BDD");
|
|
1139
|
+
setHistoryLoading(false);
|
|
1140
|
+
}
|
|
1141
|
+
if (frame.type === "request") {
|
|
1142
|
+
setPendingRequests((current) => [...current.filter((item) => item.requestId !== frame.requestId), frame]);
|
|
1143
|
+
}
|
|
1144
|
+
if (frame.type === "notification" && frame.method === "_monkeyai/session/state") {
|
|
1145
|
+
const status2 = frame.data?.state?.status;
|
|
1146
|
+
if (typeof status2 === "string" && SESSION_STATUS_LABEL[status2]) {
|
|
1147
|
+
setSessionStatus(status2);
|
|
1148
|
+
setTurnActive(status2 === "running");
|
|
1149
|
+
if (status2 !== "running") setStopping(false);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
if (frame.type === "delivery") {
|
|
1153
|
+
const accepted = Boolean(frame.data?.accepted);
|
|
1154
|
+
setLastDelivery({ accepted, code: frame.data?.code, frame: frame.type });
|
|
1155
|
+
setTurnActive(false);
|
|
1156
|
+
setStopping(false);
|
|
1157
|
+
if (accepted) {
|
|
1158
|
+
const cancelled = frame.data?.result?.stopReason === "cancelled";
|
|
1159
|
+
setStatus(cancelled ? "\u56DE\u5408\u5DF2\u505C\u6B62" : "\u56DE\u5408\u5DF2\u5B8C\u6210");
|
|
1160
|
+
} else {
|
|
1161
|
+
setPrompt((current) => current === "" ? lastDraftRef.current || "" : current);
|
|
1162
|
+
setStatus("MonkeyAI \u672A\u63A5\u53D7\u8BE5\u6307\u4EE4\uFF0C\u8349\u7A3F\u5DF2\u4FDD\u7559");
|
|
1163
|
+
if (frame.message) setError(frame.message);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
if (frame.type === "cancel_sent") {
|
|
1167
|
+
setStopping(false);
|
|
1168
|
+
setStatus("\u5DF2\u8BF7\u6C42\u505C\u6B62\uFF0C\u7B49\u5F85 runtime \u53D6\u6D88");
|
|
1169
|
+
}
|
|
1170
|
+
if (frame.type === "response_sent") {
|
|
1171
|
+
setPendingRequests((current) => current.filter((item) => item.requestId !== frame.requestId));
|
|
1172
|
+
}
|
|
1173
|
+
if (frame.type === "response_error") {
|
|
1174
|
+
setError(`\u8FDC\u7A0B\u56DE\u590D\u88AB\u62D2\u7EDD\uFF1A${frame.code || "\u672A\u77E5"}`);
|
|
1175
|
+
}
|
|
1176
|
+
if (frame.type === "error") setError(frame.message || frame.code || "\u8FDC\u7A0B\u4F1A\u8BDD\u9519\u8BEF");
|
|
1130
1177
|
}
|
|
1131
|
-
if (
|
|
1178
|
+
if (streamGenerationRef.current !== generation || streamRef.current !== stream) return;
|
|
1179
|
+
streamRef.current = null;
|
|
1180
|
+
setStatus("\u8FDC\u7A0B\u4F1A\u8BDD\u5DF2\u65AD\u5F00");
|
|
1181
|
+
setHistoryLoading(false);
|
|
1182
|
+
setTurnActive(false);
|
|
1183
|
+
setStopping(false);
|
|
1184
|
+
return;
|
|
1185
|
+
} catch (reason) {
|
|
1186
|
+
if (streamGenerationRef.current !== generation || stream && streamRef.current !== stream) return;
|
|
1187
|
+
if (isRemoteStreamCarrierError(reason)) {
|
|
1188
|
+
retries += 1;
|
|
1189
|
+
try {
|
|
1190
|
+
stream?.dispose();
|
|
1191
|
+
} catch {
|
|
1192
|
+
}
|
|
1193
|
+
streamRef.current = null;
|
|
1194
|
+
setError("");
|
|
1195
|
+
setTurnActive(false);
|
|
1132
1196
|
setStopping(false);
|
|
1133
|
-
setStatus("\
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
}
|
|
1138
|
-
if (frame.type === "response_error") {
|
|
1139
|
-
setError(`\u8FDC\u7A0B\u56DE\u590D\u88AB\u62D2\u7EDD\uFF1A${frame.code || "\u672A\u77E5"}`);
|
|
1197
|
+
setStatus("\u8FDC\u7A0B\u8FDE\u63A5\u4E2D\u65AD\uFF0C\u6B63\u5728\u91CD\u8FDE");
|
|
1198
|
+
const delay = Math.min(1e3 * 2 ** Math.min(retries - 1, 5), 3e4);
|
|
1199
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1200
|
+
continue;
|
|
1140
1201
|
}
|
|
1141
|
-
|
|
1202
|
+
streamRef.current = null;
|
|
1203
|
+
setStatus("\u8FDC\u7A0B\u4F1A\u8BDD\u4E0D\u53EF\u7528");
|
|
1204
|
+
setError(errorText(reason));
|
|
1205
|
+
setHistoryLoading(false);
|
|
1206
|
+
setTurnActive(false);
|
|
1207
|
+
setStopping(false);
|
|
1208
|
+
return;
|
|
1142
1209
|
}
|
|
1143
|
-
if (streamRef.current !== stream) return;
|
|
1144
|
-
setStatus("\u8FDC\u7A0B\u4F1A\u8BDD\u5DF2\u65AD\u5F00");
|
|
1145
|
-
setHistoryLoading(false);
|
|
1146
|
-
setTurnActive(false);
|
|
1147
|
-
setStopping(false);
|
|
1148
|
-
} catch (reason) {
|
|
1149
|
-
if (stream && streamRef.current !== stream) return;
|
|
1150
|
-
setStatus("\u8FDC\u7A0B\u4F1A\u8BDD\u4E0D\u53EF\u7528");
|
|
1151
|
-
setError(errorText(reason));
|
|
1152
|
-
setHistoryLoading(false);
|
|
1153
|
-
setTurnActive(false);
|
|
1154
|
-
setStopping(false);
|
|
1155
1210
|
}
|
|
1156
1211
|
}
|
|
1157
1212
|
function sendPrompt() {
|
|
@@ -1657,6 +1712,7 @@ window.__ModuleLoader__.load({
|
|
|
1657
1712
|
style: { ...style.button, flexShrink: 0 },
|
|
1658
1713
|
title: "\u5173\u95ED\u4F1A\u8BDD\u5E76\u8FD4\u56DE\u5217\u8868",
|
|
1659
1714
|
onClick: () => {
|
|
1715
|
+
streamGenerationRef.current += 1;
|
|
1660
1716
|
streamRef.current?.dispose();
|
|
1661
1717
|
streamRef.current = null;
|
|
1662
1718
|
setSessionId("");
|
|
@@ -1676,7 +1732,7 @@ window.__ModuleLoader__.load({
|
|
|
1676
1732
|
borderColor: sessionStatus === "running" ? t.accent : sessionStatus === "error" ? t.dangerBorder : t.border,
|
|
1677
1733
|
color: sessionStatus === "running" ? t.accent : sessionStatus === "error" ? t.danger : t.text2
|
|
1678
1734
|
},
|
|
1679
|
-
title: "\u4F1A\u8BDD\u72B6\u6001
|
|
1735
|
+
title: "\u4F1A\u8BDD\u72B6\u6001",
|
|
1680
1736
|
"aria-label": `\u4F1A\u8BDD\u72B6\u6001\uFF1A${SESSION_STATUS_LABEL[sessionStatus]}`
|
|
1681
1737
|
},
|
|
1682
1738
|
sessionStatus === "running" ? h("span", { className: "mai-pulse", style: { ...style.dot, background: t.accent } }) : null,
|
|
@@ -1865,7 +1921,7 @@ window.__ModuleLoader__.load({
|
|
|
1865
1921
|
"div",
|
|
1866
1922
|
{ style: { fontSize: 11.5, color: turnActive ? t.accent : lastDelivery?.accepted ? t.success : lastDelivery?.frame === "pending" ? t.text3 : t.danger, textAlign: "right", display: "flex", alignItems: "center", justifyContent: "flex-end", gap: 6 } },
|
|
1867
1923
|
turnActive ? h("span", { className: "mai-pulse", style: { ...style.dot, background: t.accent } }) : null,
|
|
1868
|
-
turnActive ? stopping ? "\u6B63\u5728\u505C\u6B62\u5F53\u524D\u56DE\u5408\u2026" :
|
|
1924
|
+
turnActive ? stopping ? "\u6B63\u5728\u505C\u6B62\u5F53\u524D\u56DE\u5408\u2026" : "MonkeyAI \u6B63\u5728\u5904\u7406\u2026" : lastDelivery?.accepted ? "\u56DE\u5408\u5DF2\u7ED3\u675F" : lastDelivery?.frame === "pending" ? "\u7B49\u5F85\u6295\u9012" : "\u6295\u9012\u88AB\u62D2\u7EDD"
|
|
1869
1925
|
) : null
|
|
1870
1926
|
) : null
|
|
1871
1927
|
);
|
package/lib/client-runtime.cjs
CHANGED
|
@@ -601,6 +601,24 @@ var PANEL_CSS = `
|
|
|
601
601
|
function errorText(error) {
|
|
602
602
|
return typeof error?.message === "string" ? error.message : typeof error === "string" ? error : "\u8FDC\u7A0B\u64CD\u4F5C\u5931\u8D25";
|
|
603
603
|
}
|
|
604
|
+
function withTimeout(promise, timeoutMs, label) {
|
|
605
|
+
return new Promise((resolve, reject) => {
|
|
606
|
+
const timer = setTimeout(() => reject(new Error(`${label}\u8D85\u65F6\uFF0C\u8BF7\u91CD\u8BD5`)), timeoutMs);
|
|
607
|
+
Promise.resolve(promise).then(
|
|
608
|
+
(value) => {
|
|
609
|
+
clearTimeout(timer);
|
|
610
|
+
resolve(value);
|
|
611
|
+
},
|
|
612
|
+
(error) => {
|
|
613
|
+
clearTimeout(timer);
|
|
614
|
+
reject(error);
|
|
615
|
+
}
|
|
616
|
+
);
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
function isRemoteStreamCarrierError(error) {
|
|
620
|
+
return /api gateway: remote stream websocket (?:failed|closed)/i.test(errorText(error));
|
|
621
|
+
}
|
|
604
622
|
function renderMarkdown(text) {
|
|
605
623
|
if (typeof text !== "string" || text === "") return null;
|
|
606
624
|
const blocks = [];
|
|
@@ -824,6 +842,7 @@ function RemoteSessionPage({ api }) {
|
|
|
824
842
|
const authWindowRef = useRef(null);
|
|
825
843
|
const eventsRef = useRef(null);
|
|
826
844
|
const prevScrollHeightRef = useRef(null);
|
|
845
|
+
const streamGenerationRef = useRef(0);
|
|
827
846
|
const title = useMemo(() => {
|
|
828
847
|
const runtime = runtimeSessions.find((item) => item.id === sessionId);
|
|
829
848
|
const summary = summarySessions.find((item) => item.id === sessionId);
|
|
@@ -844,6 +863,7 @@ function RemoteSessionPage({ api }) {
|
|
|
844
863
|
});
|
|
845
864
|
return () => {
|
|
846
865
|
active = false;
|
|
866
|
+
streamGenerationRef.current += 1;
|
|
847
867
|
streamRef.current?.dispose();
|
|
848
868
|
streamRef.current = null;
|
|
849
869
|
};
|
|
@@ -1006,6 +1026,7 @@ function RemoteSessionPage({ api }) {
|
|
|
1006
1026
|
setError("");
|
|
1007
1027
|
setLoading(true);
|
|
1008
1028
|
try {
|
|
1029
|
+
streamGenerationRef.current += 1;
|
|
1009
1030
|
const result = await unwrap(api.listDevices({ accountId, page: 1 }));
|
|
1010
1031
|
setDevices(result.items);
|
|
1011
1032
|
const first = result.items.length > 0 ? result.items[0].machineId : "";
|
|
@@ -1029,24 +1050,32 @@ function RemoteSessionPage({ api }) {
|
|
|
1029
1050
|
setError("");
|
|
1030
1051
|
setLoading(true);
|
|
1031
1052
|
setSessionId("");
|
|
1053
|
+
streamGenerationRef.current += 1;
|
|
1032
1054
|
streamRef.current?.dispose();
|
|
1033
1055
|
streamRef.current = null;
|
|
1034
1056
|
setFrames([]);
|
|
1035
1057
|
setPendingRequests([]);
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1058
|
+
try {
|
|
1059
|
+
const results = await Promise.allSettled([
|
|
1060
|
+
withTimeout(Promise.resolve().then(() => unwrap(api.listSessions({ accountId, machineId: mid, page: 1, pageSize: 50 }))), 5e4, "\u4F1A\u8BDD\u6458\u8981\u8BFB\u53D6"),
|
|
1061
|
+
withTimeout(Promise.resolve().then(() => unwrap(api.listRuntimeSessions({ accountId, machineId: mid }))), 5e4, "ACP \u4F1A\u8BDD\u5217\u8868\u8BFB\u53D6")
|
|
1062
|
+
]);
|
|
1063
|
+
const errors = [];
|
|
1064
|
+
if (results[0].status === "fulfilled") setSummarySessions(results[0].value.items);
|
|
1065
|
+
else errors.push(`\u4F1A\u8BDD\u6458\u8981\u7D22\u5F15\uFF1A${errorText(results[0].reason)}`);
|
|
1066
|
+
if (results[1].status === "fulfilled") setRuntimeSessions(results[1].value.sessions);
|
|
1067
|
+
else errors.push(`ACP \u4F1A\u8BDD\u5217\u8868\uFF1A${errorText(results[1].reason)}`);
|
|
1068
|
+
setStatus(errors.length === 0 ? "\u4F1A\u8BDD\u5DF2\u5237\u65B0" : "\u90E8\u5206\u4F1A\u8BDD\u6765\u6E90\u4E0D\u53EF\u7528");
|
|
1069
|
+
setError(errors.join("\n"));
|
|
1070
|
+
} catch (reason) {
|
|
1071
|
+
setStatus("\u4F1A\u8BDD\u5237\u65B0\u5931\u8D25");
|
|
1072
|
+
setError(errorText(reason));
|
|
1073
|
+
} finally {
|
|
1074
|
+
setLoading(false);
|
|
1075
|
+
}
|
|
1048
1076
|
}
|
|
1049
1077
|
async function openSession(session) {
|
|
1078
|
+
const generation = ++streamGenerationRef.current;
|
|
1050
1079
|
streamRef.current?.dispose();
|
|
1051
1080
|
streamRef.current = null;
|
|
1052
1081
|
historyLoadedRef.current = false;
|
|
@@ -1063,90 +1092,116 @@ function RemoteSessionPage({ api }) {
|
|
|
1063
1092
|
setVisibleCount(RECENT_LIMIT);
|
|
1064
1093
|
setHistoryLoading(true);
|
|
1065
1094
|
setStatus("\u6B63\u5728\u8FDE\u63A5\u8FDC\u7A0B\u4F1A\u8BDD");
|
|
1066
|
-
let
|
|
1067
|
-
|
|
1068
|
-
stream
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
if (frame.type === "reconnecting") setStatus(frame.message || "\u6B63\u5728\u91CD\u8FDE\u8FDC\u7A0B runtime");
|
|
1075
|
-
if (frame.type === "history_loaded") {
|
|
1076
|
-
historyLoadedRef.current = true;
|
|
1077
|
-
setStatus("\u5386\u53F2\u5DF2\u52A0\u8F7D");
|
|
1078
|
-
setHistoryLoading(false);
|
|
1079
|
-
setSessionStatus((current) => current || "idle");
|
|
1080
|
-
if (frame.model) setCurrentModel(frame.model);
|
|
1081
|
-
}
|
|
1082
|
-
if (frame.type === "history_incomplete") {
|
|
1083
|
-
setStatus("\u5386\u53F2\u4E0D\u5B8C\u6574\uFF1A" + (frame.message || "\u56DE\u653E\u88AB\u4E2D\u65AD"));
|
|
1084
|
-
setHistoryLoading(false);
|
|
1085
|
-
}
|
|
1086
|
-
if (frame.type === "model_switched") {
|
|
1087
|
-
if (frame.data?.model) setCurrentModel(frame.data.model);
|
|
1088
|
-
setSwitchingModel(false);
|
|
1089
|
-
setStatus("\u6A21\u578B\u5DF2\u5207\u6362");
|
|
1090
|
-
}
|
|
1091
|
-
if (frame.type === "model_switch_failed") {
|
|
1092
|
-
setSwitchingModel(false);
|
|
1093
|
-
setError(frame.message || "\u6A21\u578B\u5207\u6362\u88AB\u62D2\u7EDD");
|
|
1094
|
-
setStatus("\u6A21\u578B\u5207\u6362\u5931\u8D25");
|
|
1095
|
-
}
|
|
1096
|
-
if (frame.type === "session_missing") {
|
|
1097
|
-
setError(frame.message || "\u8BE5\u4F1A\u8BDD\u5728\u6240\u9009\u8BBE\u5907\u4E0A\u5DF2\u4E0D\u5B58\u5728\u3002");
|
|
1098
|
-
setStatus("\u8BBE\u5907\u4E0A\u627E\u4E0D\u5230\u8BE5\u4F1A\u8BDD");
|
|
1099
|
-
setHistoryLoading(false);
|
|
1100
|
-
}
|
|
1101
|
-
if (frame.type === "request") {
|
|
1102
|
-
setPendingRequests((current) => [...current.filter((item) => item.requestId !== frame.requestId), frame]);
|
|
1095
|
+
let retries = 0;
|
|
1096
|
+
while (streamGenerationRef.current === generation) {
|
|
1097
|
+
let stream;
|
|
1098
|
+
try {
|
|
1099
|
+
stream = api.followSession({ accountId, machineId, sessionId: session.id, cwd: session.cwd });
|
|
1100
|
+
if (streamGenerationRef.current !== generation) {
|
|
1101
|
+
stream.dispose();
|
|
1102
|
+
return;
|
|
1103
1103
|
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1104
|
+
streamRef.current = stream;
|
|
1105
|
+
setStatus(retries > 0 ? "\u8FDC\u7A0B\u4F1A\u8BDD\u5DF2\u91CD\u65B0\u8FDE\u63A5" : "\u8FDC\u7A0B\u4F1A\u8BDD\u5DF2\u8FDE\u63A5");
|
|
1106
|
+
for await (const frame of stream) {
|
|
1107
|
+
setFrames((current) => [...current, frame]);
|
|
1108
|
+
if (frame.type === "ready" && !historyLoadedRef.current) setStatus("\u6B63\u5728\u56DE\u653E\u4F1A\u8BDD\u5386\u53F2");
|
|
1109
|
+
if (frame.type === "reconnecting") setStatus(frame.message || "\u6B63\u5728\u91CD\u8FDE\u8FDC\u7A0B runtime");
|
|
1110
|
+
if (frame.type === "history_loaded") {
|
|
1111
|
+
historyLoadedRef.current = true;
|
|
1112
|
+
setStatus("\u5386\u53F2\u5DF2\u52A0\u8F7D");
|
|
1113
|
+
setHistoryLoading(false);
|
|
1114
|
+
setSessionStatus((current) => current || "idle");
|
|
1115
|
+
if (frame.model) setCurrentModel(frame.model);
|
|
1110
1116
|
}
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1117
|
+
if (frame.type === "history_incomplete") {
|
|
1118
|
+
setStatus("\u5386\u53F2\u4E0D\u5B8C\u6574\uFF1A" + (frame.message || "\u56DE\u653E\u88AB\u4E2D\u65AD"));
|
|
1119
|
+
setHistoryLoading(false);
|
|
1120
|
+
}
|
|
1121
|
+
if (frame.type === "model_switched") {
|
|
1122
|
+
if (frame.data?.model) setCurrentModel(frame.data.model);
|
|
1123
|
+
setSwitchingModel(false);
|
|
1124
|
+
setStatus("\u6A21\u578B\u5DF2\u5207\u6362");
|
|
1125
|
+
}
|
|
1126
|
+
if (frame.type === "model_switch_failed") {
|
|
1127
|
+
setSwitchingModel(false);
|
|
1128
|
+
setError(frame.message || "\u6A21\u578B\u5207\u6362\u5931\u8D25");
|
|
1129
|
+
setStatus("\u6A21\u578B\u5207\u6362\u5931\u8D25");
|
|
1124
1130
|
}
|
|
1131
|
+
if (frame.type === "session_missing") {
|
|
1132
|
+
setError(frame.message || "\u8BE5\u4F1A\u8BDD\u5728\u6240\u9009\u8BBE\u5907\u4E0A\u5DF2\u4E0D\u5B58\u5728\u3002");
|
|
1133
|
+
setStatus("\u8BBE\u5907\u4E0A\u627E\u4E0D\u5230\u8BE5\u4F1A\u8BDD");
|
|
1134
|
+
setHistoryLoading(false);
|
|
1135
|
+
}
|
|
1136
|
+
if (frame.type === "request") {
|
|
1137
|
+
setPendingRequests((current) => [...current.filter((item) => item.requestId !== frame.requestId), frame]);
|
|
1138
|
+
}
|
|
1139
|
+
if (frame.type === "notification" && frame.method === "_monkeyai/session/state") {
|
|
1140
|
+
const status2 = frame.data?.state?.status;
|
|
1141
|
+
if (typeof status2 === "string" && SESSION_STATUS_LABEL[status2]) {
|
|
1142
|
+
setSessionStatus(status2);
|
|
1143
|
+
setTurnActive(status2 === "running");
|
|
1144
|
+
if (status2 !== "running") setStopping(false);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
if (frame.type === "delivery") {
|
|
1148
|
+
const accepted = Boolean(frame.data?.accepted);
|
|
1149
|
+
setLastDelivery({ accepted, code: frame.data?.code, frame: frame.type });
|
|
1150
|
+
setTurnActive(false);
|
|
1151
|
+
setStopping(false);
|
|
1152
|
+
if (accepted) {
|
|
1153
|
+
const cancelled = frame.data?.result?.stopReason === "cancelled";
|
|
1154
|
+
setStatus(cancelled ? "\u56DE\u5408\u5DF2\u505C\u6B62" : "\u56DE\u5408\u5DF2\u5B8C\u6210");
|
|
1155
|
+
} else {
|
|
1156
|
+
setPrompt((current) => current === "" ? lastDraftRef.current || "" : current);
|
|
1157
|
+
setStatus("MonkeyAI \u672A\u63A5\u53D7\u8BE5\u6307\u4EE4\uFF0C\u8349\u7A3F\u5DF2\u4FDD\u7559");
|
|
1158
|
+
if (frame.message) setError(frame.message);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
if (frame.type === "cancel_sent") {
|
|
1162
|
+
setStopping(false);
|
|
1163
|
+
setStatus("\u5DF2\u8BF7\u6C42\u505C\u6B62\uFF0C\u7B49\u5F85 runtime \u53D6\u6D88");
|
|
1164
|
+
}
|
|
1165
|
+
if (frame.type === "response_sent") {
|
|
1166
|
+
setPendingRequests((current) => current.filter((item) => item.requestId !== frame.requestId));
|
|
1167
|
+
}
|
|
1168
|
+
if (frame.type === "response_error") {
|
|
1169
|
+
setError(`\u8FDC\u7A0B\u56DE\u590D\u88AB\u62D2\u7EDD\uFF1A${frame.code || "\u672A\u77E5"}`);
|
|
1170
|
+
}
|
|
1171
|
+
if (frame.type === "error") setError(frame.message || frame.code || "\u8FDC\u7A0B\u4F1A\u8BDD\u9519\u8BEF");
|
|
1125
1172
|
}
|
|
1126
|
-
if (
|
|
1173
|
+
if (streamGenerationRef.current !== generation || streamRef.current !== stream) return;
|
|
1174
|
+
streamRef.current = null;
|
|
1175
|
+
setStatus("\u8FDC\u7A0B\u4F1A\u8BDD\u5DF2\u65AD\u5F00");
|
|
1176
|
+
setHistoryLoading(false);
|
|
1177
|
+
setTurnActive(false);
|
|
1178
|
+
setStopping(false);
|
|
1179
|
+
return;
|
|
1180
|
+
} catch (reason) {
|
|
1181
|
+
if (streamGenerationRef.current !== generation || stream && streamRef.current !== stream) return;
|
|
1182
|
+
if (isRemoteStreamCarrierError(reason)) {
|
|
1183
|
+
retries += 1;
|
|
1184
|
+
try {
|
|
1185
|
+
stream?.dispose();
|
|
1186
|
+
} catch {
|
|
1187
|
+
}
|
|
1188
|
+
streamRef.current = null;
|
|
1189
|
+
setError("");
|
|
1190
|
+
setTurnActive(false);
|
|
1127
1191
|
setStopping(false);
|
|
1128
|
-
setStatus("\
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
}
|
|
1133
|
-
if (frame.type === "response_error") {
|
|
1134
|
-
setError(`\u8FDC\u7A0B\u56DE\u590D\u88AB\u62D2\u7EDD\uFF1A${frame.code || "\u672A\u77E5"}`);
|
|
1192
|
+
setStatus("\u8FDC\u7A0B\u8FDE\u63A5\u4E2D\u65AD\uFF0C\u6B63\u5728\u91CD\u8FDE");
|
|
1193
|
+
const delay = Math.min(1e3 * 2 ** Math.min(retries - 1, 5), 3e4);
|
|
1194
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1195
|
+
continue;
|
|
1135
1196
|
}
|
|
1136
|
-
|
|
1197
|
+
streamRef.current = null;
|
|
1198
|
+
setStatus("\u8FDC\u7A0B\u4F1A\u8BDD\u4E0D\u53EF\u7528");
|
|
1199
|
+
setError(errorText(reason));
|
|
1200
|
+
setHistoryLoading(false);
|
|
1201
|
+
setTurnActive(false);
|
|
1202
|
+
setStopping(false);
|
|
1203
|
+
return;
|
|
1137
1204
|
}
|
|
1138
|
-
if (streamRef.current !== stream) return;
|
|
1139
|
-
setStatus("\u8FDC\u7A0B\u4F1A\u8BDD\u5DF2\u65AD\u5F00");
|
|
1140
|
-
setHistoryLoading(false);
|
|
1141
|
-
setTurnActive(false);
|
|
1142
|
-
setStopping(false);
|
|
1143
|
-
} catch (reason) {
|
|
1144
|
-
if (stream && streamRef.current !== stream) return;
|
|
1145
|
-
setStatus("\u8FDC\u7A0B\u4F1A\u8BDD\u4E0D\u53EF\u7528");
|
|
1146
|
-
setError(errorText(reason));
|
|
1147
|
-
setHistoryLoading(false);
|
|
1148
|
-
setTurnActive(false);
|
|
1149
|
-
setStopping(false);
|
|
1150
1205
|
}
|
|
1151
1206
|
}
|
|
1152
1207
|
function sendPrompt() {
|
|
@@ -1652,6 +1707,7 @@ function RemoteSessionPage({ api }) {
|
|
|
1652
1707
|
style: { ...style.button, flexShrink: 0 },
|
|
1653
1708
|
title: "\u5173\u95ED\u4F1A\u8BDD\u5E76\u8FD4\u56DE\u5217\u8868",
|
|
1654
1709
|
onClick: () => {
|
|
1710
|
+
streamGenerationRef.current += 1;
|
|
1655
1711
|
streamRef.current?.dispose();
|
|
1656
1712
|
streamRef.current = null;
|
|
1657
1713
|
setSessionId("");
|
|
@@ -1671,7 +1727,7 @@ function RemoteSessionPage({ api }) {
|
|
|
1671
1727
|
borderColor: sessionStatus === "running" ? t.accent : sessionStatus === "error" ? t.dangerBorder : t.border,
|
|
1672
1728
|
color: sessionStatus === "running" ? t.accent : sessionStatus === "error" ? t.danger : t.text2
|
|
1673
1729
|
},
|
|
1674
|
-
title: "\u4F1A\u8BDD\u72B6\u6001
|
|
1730
|
+
title: "\u4F1A\u8BDD\u72B6\u6001",
|
|
1675
1731
|
"aria-label": `\u4F1A\u8BDD\u72B6\u6001\uFF1A${SESSION_STATUS_LABEL[sessionStatus]}`
|
|
1676
1732
|
},
|
|
1677
1733
|
sessionStatus === "running" ? h("span", { className: "mai-pulse", style: { ...style.dot, background: t.accent } }) : null,
|
|
@@ -1860,7 +1916,7 @@ function RemoteSessionPage({ api }) {
|
|
|
1860
1916
|
"div",
|
|
1861
1917
|
{ style: { fontSize: 11.5, color: turnActive ? t.accent : lastDelivery?.accepted ? t.success : lastDelivery?.frame === "pending" ? t.text3 : t.danger, textAlign: "right", display: "flex", alignItems: "center", justifyContent: "flex-end", gap: 6 } },
|
|
1862
1918
|
turnActive ? h("span", { className: "mai-pulse", style: { ...style.dot, background: t.accent } }) : null,
|
|
1863
|
-
turnActive ? stopping ? "\u6B63\u5728\u505C\u6B62\u5F53\u524D\u56DE\u5408\u2026" :
|
|
1919
|
+
turnActive ? stopping ? "\u6B63\u5728\u505C\u6B62\u5F53\u524D\u56DE\u5408\u2026" : "MonkeyAI \u6B63\u5728\u5904\u7406\u2026" : lastDelivery?.accepted ? "\u56DE\u5408\u5DF2\u7ED3\u675F" : lastDelivery?.frame === "pending" ? "\u7B49\u5F85\u6295\u9012" : "\u6295\u9012\u88AB\u62D2\u7EDD"
|
|
1864
1920
|
) : null
|
|
1865
1921
|
) : null
|
|
1866
1922
|
);
|
package/package.json
CHANGED
package/src/client/index.js
CHANGED
|
@@ -94,6 +94,20 @@ function errorText(error) {
|
|
|
94
94
|
return typeof error?.message === 'string' ? error.message : typeof error === 'string' ? error : '远程操作失败'
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
function withTimeout(promise, timeoutMs, label) {
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
const timer = setTimeout(() => reject(new Error(`${label}超时,请重试`)), timeoutMs)
|
|
100
|
+
Promise.resolve(promise).then(
|
|
101
|
+
value => { clearTimeout(timer); resolve(value) },
|
|
102
|
+
error => { clearTimeout(timer); reject(error) },
|
|
103
|
+
)
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isRemoteStreamCarrierError(error) {
|
|
108
|
+
return /api gateway: remote stream websocket (?:failed|closed)/i.test(errorText(error))
|
|
109
|
+
}
|
|
110
|
+
|
|
97
111
|
// Lightweight inline markdown: fenced code blocks, `code`, **bold**,
|
|
98
112
|
// headings and bullet lists. Everything else renders as plain text so
|
|
99
113
|
// remote content can never inject markup.
|
|
@@ -299,6 +313,7 @@ function RemoteSessionPage({ api }) {
|
|
|
299
313
|
const authWindowRef = useRef(null)
|
|
300
314
|
const eventsRef = useRef(null)
|
|
301
315
|
const prevScrollHeightRef = useRef(null)
|
|
316
|
+
const streamGenerationRef = useRef(0)
|
|
302
317
|
const title = useMemo(() => {
|
|
303
318
|
const runtime = runtimeSessions.find(item => item.id === sessionId)
|
|
304
319
|
const summary = summarySessions.find(item => item.id === sessionId)
|
|
@@ -320,6 +335,7 @@ function RemoteSessionPage({ api }) {
|
|
|
320
335
|
})
|
|
321
336
|
return () => {
|
|
322
337
|
active = false
|
|
338
|
+
streamGenerationRef.current += 1
|
|
323
339
|
streamRef.current?.dispose()
|
|
324
340
|
streamRef.current = null
|
|
325
341
|
}
|
|
@@ -489,6 +505,7 @@ function RemoteSessionPage({ api }) {
|
|
|
489
505
|
setError('')
|
|
490
506
|
setLoading(true)
|
|
491
507
|
try {
|
|
508
|
+
streamGenerationRef.current += 1
|
|
492
509
|
const result = await unwrap(api.listDevices({ accountId, page: 1 }))
|
|
493
510
|
setDevices(result.items)
|
|
494
511
|
const first = result.items.length > 0 ? result.items[0].machineId : ''
|
|
@@ -513,25 +530,33 @@ function RemoteSessionPage({ api }) {
|
|
|
513
530
|
setError('')
|
|
514
531
|
setLoading(true)
|
|
515
532
|
setSessionId('')
|
|
533
|
+
streamGenerationRef.current += 1
|
|
516
534
|
streamRef.current?.dispose()
|
|
517
535
|
streamRef.current = null
|
|
518
536
|
setFrames([])
|
|
519
537
|
setPendingRequests([])
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
538
|
+
try {
|
|
539
|
+
const results = await Promise.allSettled([
|
|
540
|
+
withTimeout(Promise.resolve().then(() => unwrap(api.listSessions({ accountId, machineId: mid, page: 1, pageSize: 50 }))), 50_000, '会话摘要读取'),
|
|
541
|
+
withTimeout(Promise.resolve().then(() => unwrap(api.listRuntimeSessions({ accountId, machineId: mid }))), 50_000, 'ACP 会话列表读取'),
|
|
542
|
+
])
|
|
543
|
+
const errors = []
|
|
544
|
+
if (results[0].status === 'fulfilled') setSummarySessions(results[0].value.items)
|
|
545
|
+
else errors.push(`会话摘要索引:${errorText(results[0].reason)}`)
|
|
546
|
+
if (results[1].status === 'fulfilled') setRuntimeSessions(results[1].value.sessions)
|
|
547
|
+
else errors.push(`ACP 会话列表:${errorText(results[1].reason)}`)
|
|
548
|
+
setStatus(errors.length === 0 ? '会话已刷新' : '部分会话来源不可用')
|
|
549
|
+
setError(errors.join('\n'))
|
|
550
|
+
} catch (reason) {
|
|
551
|
+
setStatus('会话刷新失败')
|
|
552
|
+
setError(errorText(reason))
|
|
553
|
+
} finally {
|
|
554
|
+
setLoading(false)
|
|
555
|
+
}
|
|
532
556
|
}
|
|
533
557
|
|
|
534
558
|
async function openSession(session) {
|
|
559
|
+
const generation = ++streamGenerationRef.current
|
|
535
560
|
streamRef.current?.dispose()
|
|
536
561
|
streamRef.current = null
|
|
537
562
|
historyLoadedRef.current = false
|
|
@@ -548,90 +573,113 @@ function RemoteSessionPage({ api }) {
|
|
|
548
573
|
setVisibleCount(RECENT_LIMIT)
|
|
549
574
|
setHistoryLoading(true)
|
|
550
575
|
setStatus('正在连接远程会话')
|
|
551
|
-
let
|
|
552
|
-
|
|
553
|
-
stream
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
if (frame.type === 'reconnecting') setStatus(frame.message || '正在重连远程 runtime')
|
|
560
|
-
if (frame.type === 'history_loaded') {
|
|
561
|
-
historyLoadedRef.current = true
|
|
562
|
-
setStatus('历史已加载')
|
|
563
|
-
setHistoryLoading(false)
|
|
564
|
-
setSessionStatus(current => current || 'idle')
|
|
565
|
-
if (frame.model) setCurrentModel(frame.model)
|
|
566
|
-
}
|
|
567
|
-
if (frame.type === 'history_incomplete') {
|
|
568
|
-
setStatus('历史不完整:' + (frame.message || '回放被中断'))
|
|
569
|
-
setHistoryLoading(false)
|
|
570
|
-
}
|
|
571
|
-
if (frame.type === 'model_switched') {
|
|
572
|
-
if (frame.data?.model) setCurrentModel(frame.data.model)
|
|
573
|
-
setSwitchingModel(false)
|
|
574
|
-
setStatus('模型已切换')
|
|
575
|
-
}
|
|
576
|
-
if (frame.type === 'model_switch_failed') {
|
|
577
|
-
setSwitchingModel(false)
|
|
578
|
-
setError(frame.message || '模型切换被拒绝')
|
|
579
|
-
setStatus('模型切换失败')
|
|
576
|
+
let retries = 0
|
|
577
|
+
while (streamGenerationRef.current === generation) {
|
|
578
|
+
let stream
|
|
579
|
+
try {
|
|
580
|
+
stream = api.followSession({ accountId, machineId, sessionId: session.id, cwd: session.cwd })
|
|
581
|
+
if (streamGenerationRef.current !== generation) {
|
|
582
|
+
stream.dispose()
|
|
583
|
+
return
|
|
580
584
|
}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
setTurnActive(status === 'running')
|
|
594
|
-
if (status !== 'running') setStopping(false)
|
|
585
|
+
streamRef.current = stream
|
|
586
|
+
setStatus(retries > 0 ? '远程会话已重新连接' : '远程会话已连接')
|
|
587
|
+
for await (const frame of stream) {
|
|
588
|
+
setFrames(current => [...current, frame])
|
|
589
|
+
if (frame.type === 'ready' && !historyLoadedRef.current) setStatus('正在回放会话历史')
|
|
590
|
+
if (frame.type === 'reconnecting') setStatus(frame.message || '正在重连远程 runtime')
|
|
591
|
+
if (frame.type === 'history_loaded') {
|
|
592
|
+
historyLoadedRef.current = true
|
|
593
|
+
setStatus('历史已加载')
|
|
594
|
+
setHistoryLoading(false)
|
|
595
|
+
setSessionStatus(current => current || 'idle')
|
|
596
|
+
if (frame.model) setCurrentModel(frame.model)
|
|
595
597
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
598
|
+
if (frame.type === 'history_incomplete') {
|
|
599
|
+
setStatus('历史不完整:' + (frame.message || '回放被中断'))
|
|
600
|
+
setHistoryLoading(false)
|
|
601
|
+
}
|
|
602
|
+
if (frame.type === 'model_switched') {
|
|
603
|
+
if (frame.data?.model) setCurrentModel(frame.data.model)
|
|
604
|
+
setSwitchingModel(false)
|
|
605
|
+
setStatus('模型已切换')
|
|
606
|
+
}
|
|
607
|
+
if (frame.type === 'model_switch_failed') {
|
|
608
|
+
setSwitchingModel(false)
|
|
609
|
+
setError(frame.message || '模型切换失败')
|
|
610
|
+
setStatus('模型切换失败')
|
|
611
|
+
}
|
|
612
|
+
if (frame.type === 'session_missing') {
|
|
613
|
+
setError(frame.message || '该会话在所选设备上已不存在。')
|
|
614
|
+
setStatus('设备上找不到该会话')
|
|
615
|
+
setHistoryLoading(false)
|
|
616
|
+
}
|
|
617
|
+
if (frame.type === 'request') {
|
|
618
|
+
setPendingRequests(current => [...current.filter(item => item.requestId !== frame.requestId), frame])
|
|
619
|
+
}
|
|
620
|
+
if (frame.type === 'notification' && frame.method === '_monkeyai/session/state') {
|
|
621
|
+
const status = frame.data?.state?.status
|
|
622
|
+
if (typeof status === 'string' && SESSION_STATUS_LABEL[status]) {
|
|
623
|
+
setSessionStatus(status)
|
|
624
|
+
setTurnActive(status === 'running')
|
|
625
|
+
if (status !== 'running') setStopping(false)
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
if (frame.type === 'delivery') {
|
|
629
|
+
const accepted = Boolean(frame.data?.accepted)
|
|
630
|
+
setLastDelivery({ accepted, code: frame.data?.code, frame: frame.type })
|
|
631
|
+
setTurnActive(false)
|
|
632
|
+
setStopping(false)
|
|
633
|
+
if (accepted) {
|
|
634
|
+
const cancelled = frame.data?.result?.stopReason === 'cancelled'
|
|
635
|
+
setStatus(cancelled ? '回合已停止' : '回合已完成')
|
|
636
|
+
} else {
|
|
637
|
+
setPrompt(current => current === '' ? (lastDraftRef.current || '') : current)
|
|
638
|
+
setStatus('MonkeyAI 未接受该指令,草稿已保留')
|
|
639
|
+
if (frame.message) setError(frame.message)
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
if (frame.type === 'cancel_sent') {
|
|
643
|
+
setStopping(false)
|
|
644
|
+
setStatus('已请求停止,等待 runtime 取消')
|
|
609
645
|
}
|
|
646
|
+
if (frame.type === 'response_sent') {
|
|
647
|
+
setPendingRequests(current => current.filter(item => item.requestId !== frame.requestId))
|
|
648
|
+
}
|
|
649
|
+
if (frame.type === 'response_error') {
|
|
650
|
+
setError(`远程回复被拒绝:${frame.code || '未知'}`)
|
|
651
|
+
}
|
|
652
|
+
if (frame.type === 'error') setError(frame.message || frame.code || '远程会话错误')
|
|
610
653
|
}
|
|
611
|
-
if (
|
|
654
|
+
if (streamGenerationRef.current !== generation || streamRef.current !== stream) return
|
|
655
|
+
streamRef.current = null
|
|
656
|
+
setStatus('远程会话已断开')
|
|
657
|
+
setHistoryLoading(false)
|
|
658
|
+
setTurnActive(false)
|
|
659
|
+
setStopping(false)
|
|
660
|
+
return
|
|
661
|
+
} catch (reason) {
|
|
662
|
+
if (streamGenerationRef.current !== generation || (stream && streamRef.current !== stream)) return
|
|
663
|
+
if (isRemoteStreamCarrierError(reason)) {
|
|
664
|
+
retries += 1
|
|
665
|
+
try { stream?.dispose() } catch {}
|
|
666
|
+
streamRef.current = null
|
|
667
|
+
setError('')
|
|
668
|
+
setTurnActive(false)
|
|
612
669
|
setStopping(false)
|
|
613
|
-
setStatus('
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
}
|
|
618
|
-
if (frame.type === 'response_error') {
|
|
619
|
-
setError(`远程回复被拒绝:${frame.code || '未知'}`)
|
|
670
|
+
setStatus('远程连接中断,正在重连')
|
|
671
|
+
const delay = Math.min(1000 * (2 ** Math.min(retries - 1, 5)), 30_000)
|
|
672
|
+
await new Promise(resolve => setTimeout(resolve, delay))
|
|
673
|
+
continue
|
|
620
674
|
}
|
|
621
|
-
|
|
675
|
+
streamRef.current = null
|
|
676
|
+
setStatus('远程会话不可用')
|
|
677
|
+
setError(errorText(reason))
|
|
678
|
+
setHistoryLoading(false)
|
|
679
|
+
setTurnActive(false)
|
|
680
|
+
setStopping(false)
|
|
681
|
+
return
|
|
622
682
|
}
|
|
623
|
-
if (streamRef.current !== stream) return
|
|
624
|
-
setStatus('远程会话已断开')
|
|
625
|
-
setHistoryLoading(false)
|
|
626
|
-
setTurnActive(false)
|
|
627
|
-
setStopping(false)
|
|
628
|
-
} catch (reason) {
|
|
629
|
-
if (stream && streamRef.current !== stream) return
|
|
630
|
-
setStatus('远程会话不可用')
|
|
631
|
-
setError(errorText(reason))
|
|
632
|
-
setHistoryLoading(false)
|
|
633
|
-
setTurnActive(false)
|
|
634
|
-
setStopping(false)
|
|
635
683
|
}
|
|
636
684
|
}
|
|
637
685
|
|
|
@@ -1028,7 +1076,7 @@ function RemoteSessionPage({ api }) {
|
|
|
1028
1076
|
h('div', { style: style.row },
|
|
1029
1077
|
h('button', {
|
|
1030
1078
|
type: 'button', style: { ...style.button, flexShrink: 0 }, title: '关闭会话并返回列表',
|
|
1031
|
-
onClick: () => { streamRef.current?.dispose(); streamRef.current = null; setSessionId(''); setTurnActive(false); setStopping(false); setSessionStatus(''); setStatus('远程会话已关闭') },
|
|
1079
|
+
onClick: () => { streamGenerationRef.current += 1; streamRef.current?.dispose(); streamRef.current = null; setSessionId(''); setTurnActive(false); setStopping(false); setSessionStatus(''); setStatus('远程会话已关闭') },
|
|
1032
1080
|
}, '←'),
|
|
1033
1081
|
h('strong', { style: { flex: 1, fontSize: 13.5, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title }, title),
|
|
1034
1082
|
sessionStatus ? h('span', {
|
|
@@ -1038,7 +1086,7 @@ function RemoteSessionPage({ api }) {
|
|
|
1038
1086
|
borderColor: sessionStatus === 'running' ? t.accent : sessionStatus === 'error' ? t.dangerBorder : t.border,
|
|
1039
1087
|
color: sessionStatus === 'running' ? t.accent : sessionStatus === 'error' ? t.danger : t.text2,
|
|
1040
1088
|
},
|
|
1041
|
-
title: '
|
|
1089
|
+
title: '会话状态',
|
|
1042
1090
|
'aria-label': `会话状态:${SESSION_STATUS_LABEL[sessionStatus]}`,
|
|
1043
1091
|
},
|
|
1044
1092
|
sessionStatus === 'running' ? h('span', { className: 'mai-pulse', style: { ...style.dot, background: t.accent } }) : null,
|
|
@@ -1160,7 +1208,7 @@ function RemoteSessionPage({ api }) {
|
|
|
1160
1208
|
h('path', { d: 'M8 12.5V3.5M8 3.5L4 7.5M8 3.5L12 7.5', stroke: 'currentColor', strokeWidth: 1.8, strokeLinecap: 'round', strokeLinejoin: 'round' })))),
|
|
1161
1209
|
lastDelivery || turnActive ? h('div', { style: { fontSize: 11.5, color: turnActive ? t.accent : lastDelivery?.accepted ? t.success : lastDelivery?.frame === 'pending' ? t.text3 : t.danger, textAlign: 'right', display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 6 } },
|
|
1162
1210
|
turnActive ? h('span', { className: 'mai-pulse', style: { ...style.dot, background: t.accent } }) : null,
|
|
1163
|
-
turnActive ? (stopping ? '正在停止当前回合…' :
|
|
1211
|
+
turnActive ? (stopping ? '正在停止当前回合…' : 'MonkeyAI 正在处理…') : lastDelivery?.accepted ? '回合已结束' : lastDelivery?.frame === 'pending' ? '等待投递' : '投递被拒绝') : null)
|
|
1164
1212
|
: null)
|
|
1165
1213
|
}
|
|
1166
1214
|
|