@myassis/gateway 1.0.95 → 1.0.97

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,
@@ -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();
@@ -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
  /**
@@ -577,10 +578,14 @@ class Session {
577
578
  * 因此当会话已有计划、且连续 roundsSincePlanUpdate 轮工具调用都没有同步进度时,
578
579
  * 返回一条提醒文案注入上下文;无需提醒时返回 null。
579
580
  */
580
- buildPlanReminder(roundsSincePlanUpdate) {
581
+ buildPlanReminder(roundsSincePlanUpdate, finalizing) {
581
582
  // 没有计划或计划已完成,无需提醒
582
583
  if (this.plan.length === 0 || this.isPlanCompleted())
583
584
  return null;
585
+ if (finalizing) {
586
+ // Tools are stripped when finalizing, so skip the reminder instead of telling the model to continue
587
+ return null;
588
+ }
584
589
  // 允许模型连续做 1 轮工具调用后再要求同步,避免过于频繁地打断
585
590
  if (roundsSincePlanUpdate < 2)
586
591
  return null;
@@ -928,6 +933,8 @@ class Session {
928
933
  let roundsSincePlanUpdate = 0;
929
934
  // 计划模式:收尾时提醒模型补完计划的次数,避免反复提醒导致死循环
930
935
  let planFinalizeNudges = 0;
936
+ let pseudoToolCallRetries = 0;
937
+ let finalizeStatus = 'completed';
931
938
  // 当前裁剪级别,遇到上下文超限时逐级加重
932
939
  let trimLevel = ContextBuilder_js_1.TrimLevel.OldToolPayload;
933
940
  /** 判断错误是否为上下文超限 */
@@ -1070,6 +1077,7 @@ class Session {
1070
1077
  // 还没入队,插进去会破坏配对,被严格校验的厂商判为非法请求。
1071
1078
  logger.warn(`工具调用轮次超过上限 ${index_js_2.appConfig.maxToolRounds},转入收尾`);
1072
1079
  forceFinalize = true;
1080
+ finalizeStatus = 'error';
1073
1081
  }
1074
1082
  messages.push({
1075
1083
  role: 'assistant',
@@ -1356,7 +1364,7 @@ class Session {
1356
1364
  }
1357
1365
  // 计划模式:模型常常只在开头和结尾调用 updatePlan,中间过程不同步进度。
1358
1366
  // 这里在已有计划且连续多轮未同步时,主动注入一条提醒,迫使模型推进计划状态。
1359
- const reminder = this.buildPlanReminder(roundsSincePlanUpdate);
1367
+ const reminder = this.buildPlanReminder(roundsSincePlanUpdate, forceFinalize);
1360
1368
  if (reminder) {
1361
1369
  messages.push({
1362
1370
  role: 'assistant',
@@ -1381,7 +1389,7 @@ class Session {
1381
1389
  else {
1382
1390
  // ========== 没有工具调用,准备结束本轮 ==========
1383
1391
  // 计划模式:计划还没收尾时,先提醒模型补完状态(最多提醒一次,避免死循环)
1384
- if (planFinalizeNudges < 1) {
1392
+ if (!forceFinalize && planFinalizeNudges < 1) {
1385
1393
  const finalizeReminder = this.buildPlanFinalizeReminder();
1386
1394
  if (finalizeReminder) {
1387
1395
  planFinalizeNudges++;
@@ -1398,6 +1406,34 @@ class Session {
1398
1406
  return await processModelResponse();
1399
1407
  }
1400
1408
  }
1409
+ const _rawText = llmResult.content || llmResult.reasoningContent || '';
1410
+ const PSEUDO_TOOL_RE = /<\/?(exec|edit|file|search|fetch|webfetch|screenshot|keyboard|mouse|skill|task|model|sessionsspawn|setsessiontitle|updateplan|tool|tool_call|function)\b|exec\s+command=|\btoolname\b/i;
1411
+ if (_rawText && PSEUDO_TOOL_RE.test(_rawText)) {
1412
+ if (forceFinalize === false && pseudoToolCallRetries < 1) {
1413
+ pseudoToolCallRetries = 1;
1414
+ messages.push({
1415
+ role: 'user',
1416
+ content: 'Your previous reply wrote a tool call as text (e.g. an exec tag, exec command=, or a tool_call block). Such text is NOT executed. Call tools only through the function-calling tool_calls field. Never write tool-call tags in prose. If tools are unavailable, reply in plain language about progress and emit no tool-call tags. Retry using the correct format.',
1417
+ attachments: []
1418
+ });
1419
+ return await processModelResponse();
1420
+ }
1421
+ const _m = _rawText.match(PSEUDO_TOOL_RE);
1422
+ const _idx = _m && _m.index ? _m.index : 0;
1423
+ let _cleaned = _m ? _rawText.slice(0, _idx) : _rawText;
1424
+ _cleaned = _cleaned.trimEnd();
1425
+ if (!_cleaned.trim()) {
1426
+ _cleaned = '[Reached the tool-call limit for this turn. Partial progress is shown above; please continue remaining steps in a new message.]';
1427
+ }
1428
+ else {
1429
+ _cleaned = _cleaned + '\n\n[Reached the tool-call limit; partial progress above. Continue remaining steps in a new message.]';
1430
+ }
1431
+ finalizeStatus = 'error';
1432
+ if (llmResult.content)
1433
+ llmResult.content = _cleaned;
1434
+ else
1435
+ llmResult.reasoningContent = _cleaned;
1436
+ }
1401
1437
  if (llmResult.content || llmResult.reasoningContent) {
1402
1438
  // 分段发送内容,每段约50个字符
1403
1439
  const chunkSize = 1;
@@ -1412,18 +1448,21 @@ class Session {
1412
1448
  }
1413
1449
  }
1414
1450
  if (!childAgent) {
1415
- this.addAssistantMessage(llmResult.content || llmResult.reasoningContent, toolCalls, [...new Set(modelNames)].join(','), this.currentMessageId);
1451
+ const assistantMessage = this.addAssistantMessage(llmResult.content || llmResult.reasoningContent, toolCalls, [...new Set(modelNames)].join(','), this.currentMessageId);
1452
+ assistantMessage.status = finalizeStatus;
1416
1453
  // 正文已完整产出,先落库再发 complete:
1417
1454
  // 客户端收到 complete 后就认为这条消息定稿了,此时库里必须已经有它。
1418
1455
  await this.saveMessage();
1419
1456
  }
1420
- sendSSE(res, { type: 'complete', modelName: [...new Set(modelNames)].join(",") });
1457
+ sendSSE(res, { type: 'complete', modelName: [...new Set(modelNames)].join(","), status: finalizeStatus });
1421
1458
  sendSSE(res, { type: '[DONE]' });
1422
1459
  return llmResult.content;
1423
1460
  }
1424
1461
  };
1462
+ let completedContent;
1425
1463
  try {
1426
1464
  const result = await processModelResponse();
1465
+ completedContent = result;
1427
1466
  return { success: true, data: result };
1428
1467
  }
1429
1468
  catch (error) {
@@ -1432,7 +1471,8 @@ class Session {
1432
1471
  if (toolCalls) {
1433
1472
  // 用 '' 覆盖会把已经增量保存的正文清空,这里保留已有内容
1434
1473
  const saved = this.messages.find(m => m.id === this.currentMessageId && m.role === 'assistant');
1435
- this.addAssistantMessage(saved?.content || '', toolCalls, [...new Set(modelNames)].join(','), this.currentMessageId);
1474
+ const assistantMessage = this.addAssistantMessage(saved?.content || '', toolCalls, [...new Set(modelNames)].join(','), this.currentMessageId);
1475
+ assistantMessage.status = 'error';
1436
1476
  }
1437
1477
  if (error.message !== 'aborted') {
1438
1478
  try {
@@ -1470,6 +1510,10 @@ class Session {
1470
1510
  // 后台预压缩:本轮已结束,提前生成摘要让下一轮直接命中,降低首字延迟。
1471
1511
  // 延迟启动是为了给用户的连续追问让路(追问会取消它)。
1472
1512
  this.schedulePrecompression();
1513
+ // 会话正常结束:按用户设置推送消息到手机 App
1514
+ if (completedContent !== undefined) {
1515
+ void this.sendSessionCompletedNotification(completedContent);
1516
+ }
1473
1517
  }
1474
1518
  }
1475
1519
  }
@@ -1558,6 +1602,34 @@ class Session {
1558
1602
  logger.error('推送会话停止事件失败:', error);
1559
1603
  }
1560
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
+ }
1561
1633
  /**
1562
1634
  * 获取 AbortController 用于中断请求
1563
1635
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myassis/gateway",
3
- "version": "1.0.95",
3
+ "version": "1.0.97",
4
4
  "description": "我的助手 Gateway Service - 本地 AI 网关服务,支持认证、WebSocket 实时通信和任务调度",
5
5
  "main": "dist/index.js",
6
6
  "bin": {