@myassis/gateway 1.0.91 → 1.0.93
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/dist/routes/agent.js
CHANGED
|
@@ -393,6 +393,39 @@ router.get('/sessions/:sessionId/messages/sync', ensureAgentManager, async (req,
|
|
|
393
393
|
res.status(500).json({ success: false, error: 'Failed to sync messages' });
|
|
394
394
|
}
|
|
395
395
|
});
|
|
396
|
+
/**
|
|
397
|
+
* GET /api/agent/sessions/:sessionId/state
|
|
398
|
+
*
|
|
399
|
+
* 会话的后台运行态。终端在断线期间收不到任何流事件,重连后必须问一次
|
|
400
|
+
* 「这一轮到底还在跑吗」,否则只能二选一地猜:要么把仍在生成的会话当成
|
|
401
|
+
* 已结束(加载圆点与计划进度圈凭空消失),要么永远停在加载态。
|
|
402
|
+
*
|
|
403
|
+
* 注意要注册在 /messages/:messageId 之前,避免被当成 messageId 匹配。
|
|
404
|
+
*/
|
|
405
|
+
router.get('/sessions/:sessionId/state', ensureAgentManager, async (req, res) => {
|
|
406
|
+
try {
|
|
407
|
+
const userId = req.userId;
|
|
408
|
+
const { sessionId } = req.params;
|
|
409
|
+
const session = (0, index_js_2.getSessionManager)(userId).getSession(sessionId);
|
|
410
|
+
if (!session) {
|
|
411
|
+
return res.status(404).json({ success: false, error: 'Session not found' });
|
|
412
|
+
}
|
|
413
|
+
return res.json({
|
|
414
|
+
success: true,
|
|
415
|
+
data: {
|
|
416
|
+
isGenerating: session.isGenerating,
|
|
417
|
+
// 只在生成中才回消息 id:结束后回它会让终端以为还有流在推进
|
|
418
|
+
currentMessageId: session.isGenerating ? session.currentMessageId : null,
|
|
419
|
+
plan: session.plan,
|
|
420
|
+
planUpdatedAt: session.planUpdatedAt,
|
|
421
|
+
},
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
catch (error) {
|
|
425
|
+
logger.error(`Get session state error: ${error}`);
|
|
426
|
+
res.status(500).json({ success: false, error: 'Failed to get session state' });
|
|
427
|
+
}
|
|
428
|
+
});
|
|
396
429
|
/**
|
|
397
430
|
* DELETE /api/agent/sessions/:sessionId/messages/:messageId
|
|
398
431
|
* Delete a specific message
|
|
@@ -255,7 +255,9 @@ class AgentManager {
|
|
|
255
255
|
updatedAt: s.updatedAt,
|
|
256
256
|
messageQueue: s.messageQueue,
|
|
257
257
|
messageQueueAutoExecute: s.messageQueueAutoExecute,
|
|
258
|
-
plan: s.plan
|
|
258
|
+
plan: s.plan,
|
|
259
|
+
isGenerating: s.isGenerating,
|
|
260
|
+
currentMessageId: s.isGenerating ? s.currentMessageId : null
|
|
259
261
|
}));
|
|
260
262
|
}
|
|
261
263
|
/**
|
|
@@ -43,6 +43,7 @@ class RelayClient {
|
|
|
43
43
|
stopped = false;
|
|
44
44
|
/** Server 在 HELLO_ACK 中下发的参数,未收到前用协议默认值 */
|
|
45
45
|
initialWindowSize = protocol_js_1.INITIAL_WINDOW_SIZE;
|
|
46
|
+
wsInitialWindowSize = protocol_js_1.WS_INITIAL_WINDOW_SIZE;
|
|
46
47
|
maxConcurrentStreams = protocol_js_1.MAX_CONCURRENT_STREAMS;
|
|
47
48
|
/**
|
|
48
49
|
* 启动中继。
|
|
@@ -350,6 +351,11 @@ class RelayClient {
|
|
|
350
351
|
this.initialWindowSize = ack.initialWindowSize;
|
|
351
352
|
if (ack.maxConcurrentStreams > 0)
|
|
352
353
|
this.maxConcurrentStreams = ack.maxConcurrentStreams;
|
|
354
|
+
// 旧 Server 不下发该字段:回退到 HTTP 窗口,与改造前行为一致。
|
|
355
|
+
// 两侧必须用同一个值,否则接收方会把合法数据当成「对端超发」。
|
|
356
|
+
this.wsInitialWindowSize = ack.wsInitialWindowSize > 0
|
|
357
|
+
? ack.wsInitialWindowSize
|
|
358
|
+
: this.initialWindowSize;
|
|
353
359
|
this.failureCount = 0;
|
|
354
360
|
this.lastError = undefined;
|
|
355
361
|
this.connectedAt = Date.now();
|
|
@@ -461,8 +467,9 @@ class RelayClient {
|
|
|
461
467
|
return;
|
|
462
468
|
}
|
|
463
469
|
const open = (0, protocol_js_1.decodeJsonPayload)(payload);
|
|
464
|
-
|
|
465
|
-
const
|
|
470
|
+
// WS 没有逗条背压,窗口耗尽只能中止整条流,因此用更宽的专用窗口
|
|
471
|
+
const sendWindow = new protocol_js_1.SendWindow(this.wsInitialWindowSize);
|
|
472
|
+
const recvWindow = new protocol_js_1.ReceiveWindow(this.wsInitialWindowSize);
|
|
466
473
|
const socket = new LoopbackForwarder_js_1.LoopbackWebSocket(this.localPort, open.path, open.headers, {
|
|
467
474
|
onOpen: () => {
|
|
468
475
|
// 回环连上后无需额外通知:远端 WS 在 Server 侧已经 accept,
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* CI 会校验本文件与源文件的一致性,不一致将导致构建失败。
|
|
10
10
|
*/
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.stripHopByHopHeaders = exports.checkGatewayFeatures = exports.checkProtocolVersion = exports.ReceiveWindow = exports.SendWindow = exports.StreamIdAllocator = exports.decodeWsData = exports.encodeWsDataFrames = exports.MAX_WS_DATA_CHUNK = exports.WS_DATA_PREFIX_SIZE = exports.WsDataKind = exports.splitIntoFrames = exports.decodeJsonPayload = exports.decodeFrame = exports.encodeJsonFrame = exports.encodeFrame = exports.RelayProtocolError = exports.AbortReason = exports.isControlFrame = exports.isKnownFrameType = exports.frameTypeName = exports.FrameType = exports.CONTROL_STREAM_ID = exports.PING_TIMEOUT_MS = exports.PING_INTERVAL_MS = exports.MAX_CONCURRENT_STREAMS = exports.INITIAL_WINDOW_SIZE = exports.MAX_FRAME_PAYLOAD = exports.FRAME_HEADER_SIZE = exports.RELAY_REQUIRED_FEATURE = exports.RELAY_MIN_PROTOCOL_VERSION = exports.RELAY_PROTOCOL_VERSION = void 0;
|
|
12
|
+
exports.stripHopByHopHeaders = exports.checkGatewayFeatures = exports.checkProtocolVersion = exports.ReceiveWindow = exports.SendWindow = exports.StreamIdAllocator = exports.decodeWsData = exports.encodeWsDataFrames = exports.MAX_WS_DATA_CHUNK = exports.WS_DATA_PREFIX_SIZE = exports.WsDataKind = exports.splitIntoFrames = exports.decodeJsonPayload = exports.decodeFrame = exports.encodeJsonFrame = exports.encodeFrame = exports.RelayProtocolError = exports.AbortReason = exports.isControlFrame = exports.isKnownFrameType = exports.frameTypeName = exports.FrameType = exports.CONTROL_STREAM_ID = exports.PING_TIMEOUT_MS = exports.PING_INTERVAL_MS = exports.MAX_CONCURRENT_STREAMS = exports.WS_INITIAL_WINDOW_SIZE = exports.INITIAL_WINDOW_SIZE = exports.MAX_FRAME_PAYLOAD = exports.FRAME_HEADER_SIZE = exports.RELAY_REQUIRED_FEATURE = exports.RELAY_MIN_PROTOCOL_VERSION = exports.RELAY_PROTOCOL_VERSION = void 0;
|
|
13
13
|
/**
|
|
14
14
|
* 中继帧协议(单源实现)
|
|
15
15
|
* ============================================================
|
|
@@ -55,8 +55,29 @@ exports.FRAME_HEADER_SIZE = 5;
|
|
|
55
55
|
* 偏小是有意为之:帧越小,SSE/流式响应的转发粒度越细,首字延迟越低。
|
|
56
56
|
*/
|
|
57
57
|
exports.MAX_FRAME_PAYLOAD = 64 * 1024;
|
|
58
|
-
/**
|
|
58
|
+
/**
|
|
59
|
+
* HTTP 流的初始接收窗口(信用式流控)。
|
|
60
|
+
*
|
|
61
|
+
* 保持 256KB:HTTP 流有真正的背压——信用耗尽会 req.pause() 暂停
|
|
62
|
+
* 上游,等 WINDOW_UPDATE 回来再继续,窗口小只影响吞吐不会丢数据,
|
|
63
|
+
* 而帧细粒度对 SSE 首字延迟有利。
|
|
64
|
+
*/
|
|
59
65
|
exports.INITIAL_WINDOW_SIZE = 256 * 1024;
|
|
66
|
+
/**
|
|
67
|
+
* WebSocket 流的初始接收窗口。
|
|
68
|
+
*
|
|
69
|
+
* 必须远大于 HTTP 窗口,因为 WS 没有背压可用:ws 库不支持逗条
|
|
70
|
+
* 暂停,一条消息发不下去就只能 abortStream(见
|
|
71
|
+
* RelayHub.forwardWebSocket 与 RelayClient.onWsOpen 里的 QUOTA_EXCEEDED 分支)。
|
|
72
|
+
* 一旦中止,终端的通知通道就断了,那一段的会话镜像事件全丢。
|
|
73
|
+
*
|
|
74
|
+
* 现实触发条件:会话镜像会把含 base64 附件的用户消息整条下发,
|
|
75
|
+
* 一张 5MB 的图经 base64 膨胀约 4/3 后接近 7MB,瞬时打穿 256KB。
|
|
76
|
+
*
|
|
77
|
+
* 16MB 不是预分配:窗口只是「允许对端在未收到确认前先发多少」。
|
|
78
|
+
* 而且 WS 流数量等于在线终端数(个位数),不是 MAX_CONCURRENT_STREAMS。
|
|
79
|
+
*/
|
|
80
|
+
exports.WS_INITIAL_WINDOW_SIZE = 16 * 1024 * 1024;
|
|
60
81
|
/** 单条隧道允许的最大并发流数 */
|
|
61
82
|
exports.MAX_CONCURRENT_STREAMS = 64;
|
|
62
83
|
/** 心跳间隔与超时(毫秒) */
|
|
@@ -120,6 +120,17 @@ class Session {
|
|
|
120
120
|
isGenerating = false;
|
|
121
121
|
unreadCount = 0;
|
|
122
122
|
currentMessageId = null;
|
|
123
|
+
/**
|
|
124
|
+
* 镜像事件序号(会话级单调递增)。
|
|
125
|
+
*
|
|
126
|
+
* WebSocket 镜像是 fire-and-forget:连接不在 OPEN(重连窗口)或中继流
|
|
127
|
+
* 被中止时,sendToUser 会静默丢弃,接收端无从得知。带上序号后,
|
|
128
|
+
* 终端一旦发现跳号就能立即触发补拉,而不是靠「连接是否断过」猜。
|
|
129
|
+
*
|
|
130
|
+
* 不持久化:它只用于检测单次进程内的事件连续性,网关重启后
|
|
131
|
+
* 终端会因 WebSocket 重连而重置基准。
|
|
132
|
+
*/
|
|
133
|
+
mirrorSeq = 0;
|
|
123
134
|
abortController = null;
|
|
124
135
|
/** 后台预压缩的延迟定时器 */
|
|
125
136
|
precompressionTimer = null;
|
|
@@ -714,13 +725,36 @@ class Session {
|
|
|
714
725
|
// 这里预先取到 WebSocketService,把每个 SSE 事件同步给该用户的其他连接,
|
|
715
726
|
// 从而让所有终端看到同一份会话流。子 Agent 的内部流不需要同步。
|
|
716
727
|
const wsService = childAgent ? null : await this.getWebSocketService();
|
|
717
|
-
|
|
728
|
+
// SSE 通道是否还活着。
|
|
729
|
+
//
|
|
730
|
+
// 中继模式下隧道一断,这条回环响应会被直接销毁,而本轮生成仍在后台跑。
|
|
731
|
+
// 此时发起端拿不到任何后续事件:SSE 已断,而它又因 excludeClientId 被
|
|
732
|
+
// 排除在 WebSocket 镜像之外,界面上的加载圆点与计划进度圈会凭空消失。
|
|
733
|
+
// 所以 SSE 一断就取消排除,让发起端也从镜像里接着收。
|
|
734
|
+
let sseAlive = !!res;
|
|
735
|
+
if (res && typeof res.on === 'function') {
|
|
736
|
+
res.on('close', () => {
|
|
737
|
+
if (!sseAlive)
|
|
738
|
+
return;
|
|
739
|
+
sseAlive = false;
|
|
740
|
+
logger.warn(`会话 ${this.id} 的 SSE 已断开,本轮生成继续,后续事件改由 WebSocket 镜像下发`);
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* 把 SSE 事件镜像给当前用户的其他终端。
|
|
745
|
+
*
|
|
746
|
+
* SSE 还活着时排除发起端(它已从 SSE 收到同样的事件,不排除会渲染两遍);
|
|
747
|
+
* SSE 已断开时不再排除,发起端重连 WebSocket 后就能接着看完本轮。
|
|
748
|
+
*/
|
|
718
749
|
const mirrorToOtherClients = (data) => {
|
|
719
750
|
if (!wsService)
|
|
720
751
|
return;
|
|
721
752
|
if (NON_MIRRORED_SSE_EVENTS.has(data?.type))
|
|
722
753
|
return;
|
|
723
754
|
try {
|
|
755
|
+
// 序号在「决定要发」后才抬:被过滤掉的事件不占号,
|
|
756
|
+
// 否则接收端会把正常的过滤看成丢包。
|
|
757
|
+
this.mirrorSeq += 1;
|
|
724
758
|
wsService.sendToUser(String(this.userId), {
|
|
725
759
|
type: 'session_stream',
|
|
726
760
|
payload: {
|
|
@@ -728,9 +762,10 @@ class Session {
|
|
|
728
762
|
agentId: this.agentId,
|
|
729
763
|
userMessageId,
|
|
730
764
|
assistantMessageId,
|
|
765
|
+
streamSeq: this.mirrorSeq,
|
|
731
766
|
event: data,
|
|
732
767
|
},
|
|
733
|
-
}, { excludeClientId: clientId });
|
|
768
|
+
}, { excludeClientId: sseAlive ? clientId : undefined });
|
|
734
769
|
}
|
|
735
770
|
catch (error) {
|
|
736
771
|
logger.error('SSE mirror error:', error);
|
|
@@ -758,7 +793,8 @@ class Session {
|
|
|
758
793
|
// SSE 辅助方法:res 为 null 时跳过写入(本地执行模式),但仍同步给其他终端
|
|
759
794
|
const sendSSE = (res, data) => {
|
|
760
795
|
mirrorToOtherClients(data);
|
|
761
|
-
|
|
796
|
+
// 连接已断开时不再写入:写只会抛错刷日志,内容已改由镜像下发
|
|
797
|
+
if (!res || !sseAlive)
|
|
762
798
|
return;
|
|
763
799
|
try {
|
|
764
800
|
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
@@ -770,7 +806,7 @@ class Session {
|
|
|
770
806
|
};
|
|
771
807
|
// 心跳机制:每 15 秒发送一次心跳,防止连接超时
|
|
772
808
|
const heartbeatInterval = setInterval(() => {
|
|
773
|
-
if (res && !res.destroyed) {
|
|
809
|
+
if (res && sseAlive && !res.destroyed) {
|
|
774
810
|
sendSSE(res, { type: 'heartbeat' });
|
|
775
811
|
}
|
|
776
812
|
else {
|