@myassis/gateway 1.0.96 → 1.0.98

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.
@@ -87,6 +87,12 @@ const defaultServerBaseUrl = resolvedNodeEnv === 'production' ? PROD_SERVER_BASE
87
87
  // 环境变量配置
88
88
  exports.appConfig = {
89
89
  serverBaseUrl: (process.env.SERVER_BASE_URL || defaultServerBaseUrl).replace(/\/+$/, ''),
90
+ /**
91
+ * Desktop Web 站点地址。会话结束推送到手机 App 的通知点击后打开该地址。
92
+ * 可通过 DESKTOP_WEB_URL 覆盖(私有化部署)。
93
+ */
94
+ desktopWebUrl: (process.env.DESKTOP_WEB_URL
95
+ || (resolvedNodeEnv === 'production' ? 'https://web.my-assis.com' : 'http://192.168.1.36:5005')).replace(/\/+$/, ''),
90
96
  port: parseInt(process.env.PORT || '3001', 10),
91
97
  clientUrl: process.env.CLIENT_URL || 'http://localhost:3000',
92
98
  nodeEnv: resolvedNodeEnv,
@@ -94,7 +100,7 @@ exports.appConfig = {
94
100
  /** 摘要后保留的原始历史消息条数 */
95
101
  historyKeep: parseInt(process.env.HISTORY_KEEP || '4', 10),
96
102
  /** 保留完整输出的最近工具调用轮数 */
97
- toolPairKeep: parseInt(process.env.TOOL_PAIR_KEEP || '8', 10),
103
+ toolPairKeep: parseInt(process.env.TOOL_PAIR_KEEP || '2', 10),
98
104
  /** 极端裁剪时至少保留的工具调用轮数 */
99
105
  minToolPairKeep: 2,
100
106
  /** 单次请求上下文字符预算(LLMClient 硬上限为 200000,此处留出余量) */
@@ -112,7 +118,7 @@ exports.appConfig = {
112
118
  */
113
119
  turnCompactTriggerChars: parseInt(process.env.TURN_COMPACT_TRIGGER_CHARS || '144000', 10),
114
120
  /** 轮内压缩时保留原文的最近工具调用轮数 */
115
- turnCompactKeepPairs: parseInt(process.env.TURN_COMPACT_KEEP_PAIRS || '3', 10),
121
+ turnCompactKeepPairs: parseInt(process.env.TURN_COMPACT_KEEP_PAIRS || '2', 10),
116
122
  /** 单轮请求内最多做几次轮内压缩,防止摘要模型被反复调用 */
117
123
  maxInTurnCompactions: parseInt(process.env.MAX_IN_TURN_COMPACTIONS || '4', 10),
118
124
  /** 模型瞬时故障(空回复 / 超时 / 网络抖动)的自动重试次数 */
package/dist/main.js CHANGED
@@ -168,26 +168,26 @@ if (cliCommand) {
168
168
  }
169
169
  }
170
170
  else if (cliCommand === '--help' || cliCommand === '-h') {
171
- console.log(`我的助手 Gateway CLI
172
-
173
- 用法: gateway <命令>
174
-
175
- 服务管理命令:
176
- install 安装 Gateway 服务(后台运行)
177
- uninstall 卸载 Gateway 服务
178
- start 启动 Gateway 服务
179
- stop 停止 Gateway 服务
180
- restart 重启 Gateway 服务
181
- update 更新 Gateway(需重新安装)
182
- status 查看服务状态
183
-
184
- CORS 管理命令:
185
- addCors <域名> 添加允许的跨域域名
186
- removeCors <域名> 移除允许的跨域域名
187
- listCors 列出所有跨域域名(含内置域名)
188
-
189
- 其他:
190
- --help, -h 显示本帮助信息
171
+ console.log(`我的助手 Gateway CLI
172
+
173
+ 用法: gateway <命令>
174
+
175
+ 服务管理命令:
176
+ install 安装 Gateway 服务(后台运行)
177
+ uninstall 卸载 Gateway 服务
178
+ start 启动 Gateway 服务
179
+ stop 停止 Gateway 服务
180
+ restart 重启 Gateway 服务
181
+ update 更新 Gateway(需重新安装)
182
+ status 查看服务状态
183
+
184
+ CORS 管理命令:
185
+ addCors <域名> 添加允许的跨域域名
186
+ removeCors <域名> 移除允许的跨域域名
187
+ listCors 列出所有跨域域名(含内置域名)
188
+
189
+ 其他:
190
+ --help, -h 显示本帮助信息
191
191
  `);
192
192
  }
193
193
  else {
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ /**
3
+ * 会话结束消息通知服务
4
+ *
5
+ * Desktop agent 会话结束后,调用 Server 的推送接口向用户手机 App 推送消息。
6
+ * 点击通知会打开 Desktop Web 站点(由 Server / Gateway 配置决定)。
7
+ *
8
+ * 是否真正推送由用户设置 sessionNotificationEnabled 控制(Server 端校验)。
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.sessionNotificationService = void 0;
12
+ const index_js_1 = require("../config/index.js");
13
+ const authStore_js_1 = require("../stores/authStore.js");
14
+ const shared_1 = require("@myassis/shared");
15
+ const logger = (0, shared_1.getLogger)('SessionNotificationService');
16
+ class SessionNotificationService {
17
+ /**
18
+ * 通知 Server 推送「会话已结束」消息到用户手机 App(fire-and-forget)。
19
+ */
20
+ async notifySessionCompleted(userId, payload) {
21
+ try {
22
+ const auth = authStore_js_1.authStore.get(userId);
23
+ const token = auth?.accessToken;
24
+ if (!token) {
25
+ logger.debug('未找到用户 accessToken,跳过会话结束推送', { userId });
26
+ return;
27
+ }
28
+ const response = await fetch(`${index_js_1.appConfig.serverBaseUrl}/api/v1/push-tokens/session-completed`, {
29
+ method: 'POST',
30
+ headers: {
31
+ 'Content-Type': 'application/json',
32
+ Authorization: `Bearer ${token}`,
33
+ },
34
+ body: JSON.stringify({
35
+ title: payload.title,
36
+ body: payload.body || '',
37
+ sessionId: payload.sessionId,
38
+ agentId: payload.agentId,
39
+ url: index_js_1.appConfig.desktopWebUrl,
40
+ }),
41
+ });
42
+ if (!response.ok) {
43
+ const text = await response.text().catch(() => '');
44
+ logger.warn('会话结束推送接口返回异常', { status: response.status, body: text });
45
+ }
46
+ }
47
+ catch (error) {
48
+ logger.error('会话结束推送请求失败', { error: error?.message });
49
+ }
50
+ }
51
+ }
52
+ exports.sessionNotificationService = new SessionNotificationService();
@@ -43,7 +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
+ wsInitialWindowSize = protocol_js_1.INITIAL_WINDOW_SIZE;
47
47
  maxConcurrentStreams = protocol_js_1.MAX_CONCURRENT_STREAMS;
48
48
  /**
49
49
  * 启动中继。
@@ -353,8 +353,8 @@ class RelayClient {
353
353
  this.maxConcurrentStreams = ack.maxConcurrentStreams;
354
354
  // 旧 Server 不下发该字段:回退到 HTTP 窗口,与改造前行为一致。
355
355
  // 两侧必须用同一个值,否则接收方会把合法数据当成「对端超发」。
356
- this.wsInitialWindowSize = ack.wsInitialWindowSize > 0
357
- ? ack.wsInitialWindowSize
356
+ this.wsInitialWindowSize = ack.initialWindowSize > 0
357
+ ? ack.initialWindowSize
358
358
  : this.initialWindowSize;
359
359
  this.failureCount = 0;
360
360
  this.lastError = undefined;
@@ -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.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;
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;
13
13
  /**
14
14
  * 中继帧协议(单源实现)
15
15
  * ============================================================
@@ -55,29 +55,8 @@ exports.FRAME_HEADER_SIZE = 5;
55
55
  * 偏小是有意为之:帧越小,SSE/流式响应的转发粒度越细,首字延迟越低。
56
56
  */
57
57
  exports.MAX_FRAME_PAYLOAD = 64 * 1024;
58
- /**
59
- * HTTP 流的初始接收窗口(信用式流控)。
60
- *
61
- * 保持 256KB:HTTP 流有真正的背压——信用耗尽会 req.pause() 暂停
62
- * 上游,等 WINDOW_UPDATE 回来再继续,窗口小只影响吞吐不会丢数据,
63
- * 而帧细粒度对 SSE 首字延迟有利。
64
- */
58
+ /** 每条流的初始接收窗口(信用式流控) */
65
59
  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;
81
60
  /** 单条隧道允许的最大并发流数 */
82
61
  exports.MAX_CONCURRENT_STREAMS = 64;
83
62
  /** 心跳间隔与超时(毫秒) */
@@ -38,6 +38,7 @@ const MemoryManager_js_1 = require("../memory/MemoryManager.js");
38
38
  const index_js_2 = require("../../config/index.js");
39
39
  const ContextBuilder_js_1 = require("../memory/ContextBuilder.js");
40
40
  const SessionManager_js_1 = require("./SessionManager.js");
41
+ const SessionNotificationService_js_1 = require("../SessionNotificationService.js");
41
42
  const logger = (0, shared_1.getLogger)('Session');
42
43
  const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
43
44
  /**
@@ -1458,8 +1459,10 @@ class Session {
1458
1459
  return llmResult.content;
1459
1460
  }
1460
1461
  };
1462
+ let completedContent;
1461
1463
  try {
1462
1464
  const result = await processModelResponse();
1465
+ completedContent = result;
1463
1466
  return { success: true, data: result };
1464
1467
  }
1465
1468
  catch (error) {
@@ -1507,6 +1510,10 @@ class Session {
1507
1510
  // 后台预压缩:本轮已结束,提前生成摘要让下一轮直接命中,降低首字延迟。
1508
1511
  // 延迟启动是为了给用户的连续追问让路(追问会取消它)。
1509
1512
  this.schedulePrecompression();
1513
+ // 会话正常结束:按用户设置推送消息到手机 App
1514
+ if (completedContent !== undefined) {
1515
+ void this.sendSessionCompletedNotification(completedContent);
1516
+ }
1510
1517
  }
1511
1518
  }
1512
1519
  }
@@ -1595,6 +1602,34 @@ class Session {
1595
1602
  logger.error('推送会话停止事件失败:', error);
1596
1603
  }
1597
1604
  }
1605
+ /**
1606
+ * 会话正常结束后,推送消息通知到用户手机 App。
1607
+ * 具体是否推送由 Server 根据用户设置 sessionNotificationEnabled 决定。
1608
+ */
1609
+ sendSessionCompletedNotification(content) {
1610
+ try {
1611
+ let title = this.title;
1612
+ if (this.agentId) {
1613
+ const agent = getAgentStore().findById(this.agentId);
1614
+ if (agent?.name)
1615
+ title = agent.name;
1616
+ }
1617
+ const snippet = (content || '')
1618
+ .replace(/[\r\n]+/g, ' ')
1619
+ .replace(/[#*`>\[\]]/g, '')
1620
+ .trim()
1621
+ .slice(0, 80);
1622
+ void SessionNotificationService_js_1.sessionNotificationService.notifySessionCompleted(String(this.userId), {
1623
+ title,
1624
+ body: snippet,
1625
+ sessionId: this.id,
1626
+ agentId: this.agentId,
1627
+ });
1628
+ }
1629
+ catch (error) {
1630
+ logger.error('发送会话结束通知失败:', error);
1631
+ }
1632
+ }
1598
1633
  /**
1599
1634
  * 获取 AbortController 用于中断请求
1600
1635
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myassis/gateway",
3
- "version": "1.0.96",
3
+ "version": "1.0.98",
4
4
  "description": "我的助手 Gateway Service - 本地 AI 网关服务,支持认证、WebSocket 实时通信和任务调度",
5
5
  "main": "dist/index.js",
6
6
  "bin": {