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 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.7.tgz
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.7.tgz
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
- const results = await Promise.allSettled([
1042
- unwrap(api.listSessions({ accountId, machineId: mid, page: 1, pageSize: 50 })),
1043
- unwrap(api.listRuntimeSessions({ accountId, machineId: mid }))
1044
- ]);
1045
- const errors = [];
1046
- if (results[0].status === "fulfilled") setSummarySessions(results[0].value.items);
1047
- else errors.push(`\u4F1A\u8BDD\u6458\u8981\u7D22\u5F15\uFF1A${errorText(results[0].reason)}`);
1048
- if (results[1].status === "fulfilled") setRuntimeSessions(results[1].value.sessions);
1049
- else errors.push(`ACP \u4F1A\u8BDD\u5217\u8868\uFF1A${errorText(results[1].reason)}`);
1050
- setStatus(errors.length === 0 ? "\u4F1A\u8BDD\u5DF2\u5237\u65B0" : "\u90E8\u5206\u4F1A\u8BDD\u6765\u6E90\u4E0D\u53EF\u7528");
1051
- setError(errors.join("\n"));
1052
- setLoading(false);
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 stream;
1072
- try {
1073
- stream = api.followSession({ accountId, machineId, sessionId: session.id, cwd: session.cwd });
1074
- streamRef.current = stream;
1075
- setStatus("\u8FDC\u7A0B\u4F1A\u8BDD\u5DF2\u8FDE\u63A5");
1076
- for await (const frame of stream) {
1077
- setFrames((current) => [...current, frame]);
1078
- if (frame.type === "ready" && !historyLoadedRef.current) setStatus("\u6B63\u5728\u56DE\u653E\u4F1A\u8BDD\u5386\u53F2");
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
- if (frame.type === "notification" && frame.method === "_monkeyai/session/state") {
1110
- const status2 = frame.data?.state?.status;
1111
- if (typeof status2 === "string" && SESSION_STATUS_LABEL[status2]) {
1112
- setSessionStatus(status2);
1113
- setTurnActive(status2 === "running");
1114
- if (status2 !== "running") setStopping(false);
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
- if (frame.type === "delivery") {
1118
- const accepted = Boolean(frame.data?.accepted);
1119
- setLastDelivery({ accepted, code: frame.data?.code, frame: frame.type });
1120
- setTurnActive(false);
1121
- setStopping(false);
1122
- if (accepted) {
1123
- const cancelled = frame.data?.result?.stopReason === "cancelled";
1124
- setStatus(cancelled ? "\u56DE\u5408\u5DF2\u505C\u6B62" : "\u56DE\u5408\u5DF2\u5B8C\u6210");
1125
- } else {
1126
- setPrompt((current) => current === "" ? lastDraftRef.current || "" : current);
1127
- setStatus("MonkeyAI \u672A\u63A5\u53D7\u8BE5\u6307\u4EE4\uFF0C\u8349\u7A3F\u5DF2\u4FDD\u7559");
1128
- if (frame.message) setError(frame.message);
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 (frame.type === "cancel_sent") {
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("\u5DF2\u8BF7\u6C42\u505C\u6B62\uFF0C\u7B49\u5F85 runtime \u53D6\u6D88");
1134
- }
1135
- if (frame.type === "response_sent") {
1136
- setPendingRequests((current) => current.filter((item) => item.requestId !== frame.requestId));
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
- if (frame.type === "error") setError(frame.message || frame.code || "\u8FDC\u7A0B\u4F1A\u8BDD\u9519\u8BEF");
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\uFF08\u542B\u8FDC\u7AEF\u5BA2\u6237\u7AEF\u53D1\u8D77\u7684\u56DE\u5408\uFF09",
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" : sessionStatus === "running" ? "MonkeyAI \u6B63\u5728\u5904\u7406\uFF08\u8FDC\u7AEF\u5BA2\u6237\u7AEF\u53D1\u8D77\uFF09\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"
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
  );
@@ -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
- const results = await Promise.allSettled([
1037
- unwrap(api.listSessions({ accountId, machineId: mid, page: 1, pageSize: 50 })),
1038
- unwrap(api.listRuntimeSessions({ accountId, machineId: mid }))
1039
- ]);
1040
- const errors = [];
1041
- if (results[0].status === "fulfilled") setSummarySessions(results[0].value.items);
1042
- else errors.push(`\u4F1A\u8BDD\u6458\u8981\u7D22\u5F15\uFF1A${errorText(results[0].reason)}`);
1043
- if (results[1].status === "fulfilled") setRuntimeSessions(results[1].value.sessions);
1044
- else errors.push(`ACP \u4F1A\u8BDD\u5217\u8868\uFF1A${errorText(results[1].reason)}`);
1045
- setStatus(errors.length === 0 ? "\u4F1A\u8BDD\u5DF2\u5237\u65B0" : "\u90E8\u5206\u4F1A\u8BDD\u6765\u6E90\u4E0D\u53EF\u7528");
1046
- setError(errors.join("\n"));
1047
- setLoading(false);
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 stream;
1067
- try {
1068
- stream = api.followSession({ accountId, machineId, sessionId: session.id, cwd: session.cwd });
1069
- streamRef.current = stream;
1070
- setStatus("\u8FDC\u7A0B\u4F1A\u8BDD\u5DF2\u8FDE\u63A5");
1071
- for await (const frame of stream) {
1072
- setFrames((current) => [...current, frame]);
1073
- if (frame.type === "ready" && !historyLoadedRef.current) setStatus("\u6B63\u5728\u56DE\u653E\u4F1A\u8BDD\u5386\u53F2");
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
- if (frame.type === "notification" && frame.method === "_monkeyai/session/state") {
1105
- const status2 = frame.data?.state?.status;
1106
- if (typeof status2 === "string" && SESSION_STATUS_LABEL[status2]) {
1107
- setSessionStatus(status2);
1108
- setTurnActive(status2 === "running");
1109
- if (status2 !== "running") setStopping(false);
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
- if (frame.type === "delivery") {
1113
- const accepted = Boolean(frame.data?.accepted);
1114
- setLastDelivery({ accepted, code: frame.data?.code, frame: frame.type });
1115
- setTurnActive(false);
1116
- setStopping(false);
1117
- if (accepted) {
1118
- const cancelled = frame.data?.result?.stopReason === "cancelled";
1119
- setStatus(cancelled ? "\u56DE\u5408\u5DF2\u505C\u6B62" : "\u56DE\u5408\u5DF2\u5B8C\u6210");
1120
- } else {
1121
- setPrompt((current) => current === "" ? lastDraftRef.current || "" : current);
1122
- setStatus("MonkeyAI \u672A\u63A5\u53D7\u8BE5\u6307\u4EE4\uFF0C\u8349\u7A3F\u5DF2\u4FDD\u7559");
1123
- if (frame.message) setError(frame.message);
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 (frame.type === "cancel_sent") {
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("\u5DF2\u8BF7\u6C42\u505C\u6B62\uFF0C\u7B49\u5F85 runtime \u53D6\u6D88");
1129
- }
1130
- if (frame.type === "response_sent") {
1131
- setPendingRequests((current) => current.filter((item) => item.requestId !== frame.requestId));
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
- if (frame.type === "error") setError(frame.message || frame.code || "\u8FDC\u7A0B\u4F1A\u8BDD\u9519\u8BEF");
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\uFF08\u542B\u8FDC\u7AEF\u5BA2\u6237\u7AEF\u53D1\u8D77\u7684\u56DE\u5408\uFF09",
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" : sessionStatus === "running" ? "MonkeyAI \u6B63\u5728\u5904\u7406\uFF08\u8FDC\u7AEF\u5BA2\u6237\u7AEF\u53D1\u8D77\uFF09\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"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-monkeyai-sessions",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "在 DeepSeek Harness 中读取和继续 MonkeyAI 会话:查看完整对话历史、下发新指令,无缝接续之前在 MonkeyAI 客户端上的工作",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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
- const results = await Promise.allSettled([
521
- unwrap(api.listSessions({ accountId, machineId: mid, page: 1, pageSize: 50 })),
522
- unwrap(api.listRuntimeSessions({ accountId, machineId: mid })),
523
- ])
524
- const errors = []
525
- if (results[0].status === 'fulfilled') setSummarySessions(results[0].value.items)
526
- else errors.push(`会话摘要索引:${errorText(results[0].reason)}`)
527
- if (results[1].status === 'fulfilled') setRuntimeSessions(results[1].value.sessions)
528
- else errors.push(`ACP 会话列表:${errorText(results[1].reason)}`)
529
- setStatus(errors.length === 0 ? '会话已刷新' : '部分会话来源不可用')
530
- setError(errors.join('\n'))
531
- setLoading(false)
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 stream
552
- try {
553
- stream = api.followSession({ accountId, machineId, sessionId: session.id, cwd: session.cwd })
554
- streamRef.current = stream
555
- setStatus('远程会话已连接')
556
- for await (const frame of stream) {
557
- setFrames(current => [...current, frame])
558
- if (frame.type === 'ready' && !historyLoadedRef.current) setStatus('正在回放会话历史')
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
- if (frame.type === 'session_missing') {
582
- setError(frame.message || '该会话在所选设备上已不存在。')
583
- setStatus('设备上找不到该会话')
584
- setHistoryLoading(false)
585
- }
586
- if (frame.type === 'request') {
587
- setPendingRequests(current => [...current.filter(item => item.requestId !== frame.requestId), frame])
588
- }
589
- if (frame.type === 'notification' && frame.method === '_monkeyai/session/state') {
590
- const status = frame.data?.state?.status
591
- if (typeof status === 'string' && SESSION_STATUS_LABEL[status]) {
592
- setSessionStatus(status)
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
- if (frame.type === 'delivery') {
598
- const accepted = Boolean(frame.data?.accepted)
599
- setLastDelivery({ accepted, code: frame.data?.code, frame: frame.type })
600
- setTurnActive(false)
601
- setStopping(false)
602
- if (accepted) {
603
- const cancelled = frame.data?.result?.stopReason === 'cancelled'
604
- setStatus(cancelled ? '回合已停止' : '回合已完成')
605
- } else {
606
- setPrompt(current => current === '' ? (lastDraftRef.current || '') : current)
607
- setStatus('MonkeyAI 未接受该指令,草稿已保留')
608
- if (frame.message) setError(frame.message)
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 (frame.type === 'cancel_sent') {
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('已请求停止,等待 runtime 取消')
614
- }
615
- if (frame.type === 'response_sent') {
616
- setPendingRequests(current => current.filter(item => item.requestId !== frame.requestId))
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
- if (frame.type === 'error') setError(frame.message || frame.code || '远程会话错误')
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 ? '正在停止当前回合…' : sessionStatus === 'running' ? 'MonkeyAI 正在处理(远端客户端发起)…' : 'MonkeyAI 正在处理…') : lastDelivery?.accepted ? '回合已结束' : lastDelivery?.frame === 'pending' ? '等待投递' : '投递被拒绝') : null)
1211
+ turnActive ? (stopping ? '正在停止当前回合…' : 'MonkeyAI 正在处理…') : lastDelivery?.accepted ? '回合已结束' : lastDelivery?.frame === 'pending' ? '等待投递' : '投递被拒绝') : null)
1164
1212
  : null)
1165
1213
  }
1166
1214