@myassis/gateway 1.0.35 → 1.0.37

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.
@@ -8,6 +8,7 @@ exports.webSocketService = void 0;
8
8
  const ws_1 = require("ws");
9
9
  const index_js_1 = require("../stores/index.js");
10
10
  const shared_1 = require("@myassis/shared");
11
+ const Session_js_1 = require("./session/Session.js");
11
12
  const logger = (0, shared_1.getLogger)('WebSocketService');
12
13
  class WebSocketService {
13
14
  wss = null;
@@ -127,6 +128,13 @@ class WebSocketService {
127
128
  case 'subscribe':
128
129
  logger.debug(`用户 ${userId} 订阅: ${message.payload?.channel || 'all'}`);
129
130
  break;
131
+ case 'approval_response':
132
+ // 处理批准响应
133
+ const { token, approved } = message.payload || {};
134
+ if (token) {
135
+ (0, Session_js_1.handleApprovalResponse)(token, approved === true);
136
+ }
137
+ break;
130
138
  default:
131
139
  logger.debug(`收到用户 ${userId} 的消息: ${message.type}`);
132
140
  }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Session = void 0;
3
+ exports.Session = exports.handleApprovalResponse = exports.registerApprovalWaiter = void 0;
4
4
  const uuid_1 = require("uuid");
5
5
  const shared_1 = require("@myassis/shared");
6
6
  const SessionStore_js_1 = require("./SessionStore.js");
@@ -16,6 +16,40 @@ const index_js_2 = require("../../config/index.js");
16
16
  const SessionManager_js_1 = require("./SessionManager.js");
17
17
  const logger = (0, shared_1.getLogger)('Session');
18
18
  const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
19
+ // 批准等待缓存:token -> { resolve, reject, expiresAt }
20
+ const approvalWaiters = new Map();
21
+ /**
22
+ * 注册批准等待者
23
+ */
24
+ function registerApprovalWaiter(token, timeout = 5 * 60 * 1000) {
25
+ return new Promise((resolve, reject) => {
26
+ approvalWaiters.set(token, {
27
+ resolve,
28
+ reject,
29
+ expiresAt: Date.now() + timeout,
30
+ });
31
+ // 超时自动拒绝
32
+ setTimeout(() => {
33
+ const waiter = approvalWaiters.get(token);
34
+ if (waiter) {
35
+ approvalWaiters.delete(token);
36
+ reject(new Error('批准请求超时'));
37
+ }
38
+ }, timeout);
39
+ });
40
+ }
41
+ exports.registerApprovalWaiter = registerApprovalWaiter;
42
+ /**
43
+ * 处理批准响应(由 WebSocket 调用)
44
+ */
45
+ function handleApprovalResponse(token, approved) {
46
+ const waiter = approvalWaiters.get(token);
47
+ if (waiter) {
48
+ approvalWaiters.delete(token);
49
+ waiter.resolve(approved);
50
+ }
51
+ }
52
+ exports.handleApprovalResponse = handleApprovalResponse;
19
53
  // AgentStore 单例 - 需要通过 SessionStore 获取数据库实例
20
54
  let agentStoreInstance = null;
21
55
  function getAgentStore() {
@@ -639,6 +673,141 @@ class Session {
639
673
  sendSSE(res, toolStartEvent);
640
674
  try {
641
675
  const result = await (0, index_js_1.executeTool)(pendingToolCall.toolName, (0, LLMClient_js_1.parseAruments)(pendingToolCall.input), this.id, this.currentMessageId, this.userId);
676
+ // 检查是否需要用户批准
677
+ if (result.needsApproval) {
678
+ // 发送需要批准的事件
679
+ const approvalEvent = {
680
+ type: 'approval_pending',
681
+ id: pendingToolCall.id,
682
+ toolName: pendingToolCall.toolName,
683
+ actionName: pendingToolCall.actionName,
684
+ input: pendingToolCall.input,
685
+ approvalToken: result.approvalToken,
686
+ errorMessage: result.errorMessage,
687
+ toolCallId: toolCall.id,
688
+ modelName,
689
+ sessionId: this.id
690
+ };
691
+ sendSSE(res, approvalEvent);
692
+ // 等待 Desktop 批准
693
+ try {
694
+ const approved = await registerApprovalWaiter(result.approvalToken);
695
+ if (approved) {
696
+ // 用户批准,重新调用 executeTool 并传入 approved=true
697
+ const pending = (0, index_js_1.getAndRemovePendingApproval)(result.approvalToken);
698
+ if (pending) {
699
+ // 重新执行工具调用,传入 approved 参数
700
+ const approvedResult = await (0, index_js_1.executeTool)(pendingToolCall.toolName, { ...(0, LLMClient_js_1.parseAruments)(pendingToolCall.input), approved: true }, this.id, this.currentMessageId, this.userId);
701
+ toolResults.push({
702
+ id: pendingToolCall.id,
703
+ name: pendingToolCall.toolName,
704
+ output: approvedResult.output || approvedResult.errorMessage || '',
705
+ success: approvedResult.success,
706
+ });
707
+ toolCall.toolCalls.push({
708
+ id: pendingToolCall.id,
709
+ toolName: pendingToolCall.toolName,
710
+ input: pendingToolCall.input,
711
+ output: approvedResult.output || approvedResult.errorMessage || '',
712
+ status: approvedResult.success ? 'success' : 'error',
713
+ actionName: pendingToolCall.actionName
714
+ });
715
+ sendSSE(res, {
716
+ type: 'tool_call_result',
717
+ id: pendingToolCall.id,
718
+ toolName: pendingToolCall.toolName,
719
+ actionName: pendingToolCall.actionName,
720
+ output: JSON.stringify(approvedResult),
721
+ status: approvedResult.success ? 'success' : 'error',
722
+ toolCallId: toolCall.id,
723
+ modelName
724
+ });
725
+ }
726
+ else {
727
+ // 批准 token 已过期或不存在
728
+ toolResults.push({
729
+ id: pendingToolCall.id,
730
+ name: pendingToolCall.toolName,
731
+ output: '批准请求已过期',
732
+ success: false,
733
+ });
734
+ toolCall.toolCalls.push({
735
+ id: pendingToolCall.id,
736
+ toolName: pendingToolCall.toolName,
737
+ input: pendingToolCall.input,
738
+ output: '批准请求已过期',
739
+ status: 'error',
740
+ actionName: pendingToolCall.actionName
741
+ });
742
+ sendSSE(res, {
743
+ type: 'tool_call_result',
744
+ id: pendingToolCall.id,
745
+ toolName: pendingToolCall.toolName,
746
+ output: '批准请求已过期',
747
+ status: 'error',
748
+ toolCallId: toolCall.id,
749
+ actionName: pendingToolCall.actionName,
750
+ modelName
751
+ });
752
+ }
753
+ }
754
+ else {
755
+ // 用户拒绝
756
+ toolResults.push({
757
+ id: pendingToolCall.id,
758
+ name: pendingToolCall.toolName,
759
+ output: '用户拒绝执行',
760
+ success: false,
761
+ });
762
+ toolCall.toolCalls.push({
763
+ id: pendingToolCall.id,
764
+ toolName: pendingToolCall.toolName,
765
+ input: pendingToolCall.input,
766
+ output: '用户拒绝执行',
767
+ status: 'error',
768
+ actionName: pendingToolCall.actionName
769
+ });
770
+ sendSSE(res, {
771
+ type: 'tool_call_result',
772
+ id: pendingToolCall.id,
773
+ toolName: pendingToolCall.toolName,
774
+ output: '用户拒绝执行',
775
+ status: 'error',
776
+ toolCallId: toolCall.id,
777
+ actionName: pendingToolCall.actionName,
778
+ modelName
779
+ });
780
+ }
781
+ }
782
+ catch (waitError) {
783
+ // 等待超时或其他错误
784
+ toolResults.push({
785
+ id: pendingToolCall.id,
786
+ name: pendingToolCall.toolName,
787
+ output: waitError.message,
788
+ success: false,
789
+ });
790
+ toolCall.toolCalls.push({
791
+ id: pendingToolCall.id,
792
+ toolName: pendingToolCall.toolName,
793
+ input: pendingToolCall.input,
794
+ output: waitError.message,
795
+ status: 'error',
796
+ actionName: pendingToolCall.actionName
797
+ });
798
+ sendSSE(res, {
799
+ type: 'tool_call_result',
800
+ id: pendingToolCall.id,
801
+ toolName: pendingToolCall.toolName,
802
+ output: waitError.message,
803
+ status: 'error',
804
+ toolCallId: toolCall.id,
805
+ actionName: pendingToolCall.actionName,
806
+ modelName
807
+ });
808
+ }
809
+ return;
810
+ }
642
811
  toolResults.push({
643
812
  id: pendingToolCall.id,
644
813
  name: pendingToolCall.toolName,
@@ -3,14 +3,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.execTool = exports.clearSessionCwd = exports.setSessionCwd = exports.getSessionCwd = void 0;
6
+ exports.execTool = exports.cleanupExpiredApprovals = exports.getAndRemovePendingApproval = exports.addPendingApproval = exports.clearSessionCwd = exports.setSessionCwd = exports.getSessionCwd = void 0;
7
7
  const child_process_1 = require("child_process");
8
8
  const util_1 = require("util");
9
9
  const path_1 = __importDefault(require("path"));
10
10
  const os_1 = __importDefault(require("os"));
11
11
  const shared_1 = require("@myassis/shared");
12
+ const crypto_1 = __importDefault(require("crypto"));
12
13
  const logger = (0, shared_1.getLogger)('exec');
13
14
  const execAsync = (0, util_1.promisify)(child_process_1.exec);
15
+ // 待批准的命令缓存:token -> { command, cwd, timeout, sessionId, expiresAt }
16
+ const pendingApprovals = new Map();
14
17
  // 会话级工作目录状态:sessionId -> cwd
15
18
  // 让 cd 命令能跨多次 exec 调用持久化工作目录
16
19
  const sessionCwdMap = new Map();
@@ -62,6 +65,63 @@ function clearSessionCwd(sessionId) {
62
65
  sessionCwdMap.delete(sessionId);
63
66
  }
64
67
  exports.clearSessionCwd = clearSessionCwd;
68
+ /** 生成批准 token */
69
+ function generateApprovalToken() {
70
+ return crypto_1.default.randomBytes(16).toString('hex');
71
+ }
72
+ /** 检查命令是否危险 */
73
+ function isDangerousCommand(command) {
74
+ const dangerousPatterns = [
75
+ /rm\s+-rf\s+\//,
76
+ /format\s+[a-z]:/i,
77
+ /del\s+\/[sfq]\s+\*/i,
78
+ /shutdown/i,
79
+ /reboot/i,
80
+ /mkfs/i,
81
+ /dd\s+if=.*of=\/dev\//i,
82
+ ];
83
+ for (const reg of dangerousPatterns) {
84
+ if (reg.test(command))
85
+ return true;
86
+ }
87
+ return false;
88
+ }
89
+ /** 添加待批准命令,返回 token */
90
+ function addPendingApproval(command, cwd, timeout, sessionId) {
91
+ const token = generateApprovalToken();
92
+ pendingApprovals.set(token, {
93
+ command,
94
+ cwd,
95
+ timeout,
96
+ sessionId,
97
+ expiresAt: Date.now() + 5 * 60 * 1000, // 5分钟过期
98
+ });
99
+ return token;
100
+ }
101
+ exports.addPendingApproval = addPendingApproval;
102
+ /** 验证并获取待批准命令 */
103
+ function getAndRemovePendingApproval(token) {
104
+ const pending = pendingApprovals.get(token);
105
+ if (!pending)
106
+ return null;
107
+ if (Date.now() > pending.expiresAt) {
108
+ pendingApprovals.delete(token);
109
+ return null;
110
+ }
111
+ pendingApprovals.delete(token);
112
+ return pending;
113
+ }
114
+ exports.getAndRemovePendingApproval = getAndRemovePendingApproval;
115
+ /** 清理过期的待批准命令 */
116
+ function cleanupExpiredApprovals() {
117
+ const now = Date.now();
118
+ for (const [token, pending] of pendingApprovals.entries()) {
119
+ if (now > pending.expiresAt) {
120
+ pendingApprovals.delete(token);
121
+ }
122
+ }
123
+ }
124
+ exports.cleanupExpiredApprovals = cleanupExpiredApprovals;
65
125
  exports.execTool = {
66
126
  name: 'exec',
67
127
  description: '在本地计算机上执行命令行命令。适用于运行系统命令、脚本、查看文件、执行程序等场景。',
@@ -80,26 +140,37 @@ exports.execTool = {
80
140
  type: 'number',
81
141
  description: '命令超时时间(毫秒),默认 60000',
82
142
  default: 60000,
143
+ },
144
+ isUserAsk: {
145
+ type: 'boolean',
146
+ description: '是否为用户主动要求的命令(用于绕过危险命令拦截)。如果用户明确要求执行此命令(如"帮我执行shutdown"),设置为true。',
147
+ default: false,
83
148
  }
84
149
  },
85
150
  required: ['command'],
86
151
  },
87
152
  handler: async (args, sessionId) => {
88
153
  return new Promise(async (resolve) => {
89
- const { cwd, timeout = 60000 } = args;
154
+ const { cwd, timeout = 60000, approved, isUserAsk = false } = args;
90
155
  let command = args.command;
91
- const dangerousPatterns = [
92
- /rm\s+-rf\s+\//,
93
- /format\s+[a-z]:/i,
94
- /del\s+\/[sfq]\s+\*/i,
95
- /shutdown/i,
96
- /reboot/i,
97
- /mkfs/i,
98
- /dd\s+if=.*of=\/dev\//i,
99
- ];
100
- for (const reg of dangerousPatterns) {
101
- if (reg.test(command)) {
102
- resolve({ success: false, errorMessage: '危险命令已拦截' });
156
+ // 检查危险命令(如果已批准或用户主动要求则跳过拦截)
157
+ if (!approved && isDangerousCommand(command)) {
158
+ if (isUserAsk) {
159
+ // 用户主动要求,跳过拦截
160
+ logger.info(`用户主动要求执行危险命令,跳过拦截: ${command}`);
161
+ }
162
+ else {
163
+ if (!sessionId) {
164
+ resolve({ success: false, errorMessage: '危险命令需要用户确认,但缺少会话ID' });
165
+ return;
166
+ }
167
+ const token = addPendingApproval(command, cwd, timeout, sessionId);
168
+ resolve({
169
+ success: false,
170
+ needsApproval: true,
171
+ approvalToken: token,
172
+ errorMessage: `危险命令需要用户确认:${command}`,
173
+ });
103
174
  return;
104
175
  }
105
176
  }
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.getToolDefinitions = exports.executeTool = exports.getToolByName = exports.tools = void 0;
17
+ exports.getToolDefinitions = exports.executeTool = exports.cleanupExpiredApprovals = exports.getAndRemovePendingApproval = exports.addPendingApproval = exports.getToolByName = exports.tools = void 0;
18
18
  /**
19
19
  * 工具定义集合
20
20
  */
@@ -27,6 +27,9 @@ const mouse_js_1 = require("./mouse.js");
27
27
  const fetch_js_1 = require("./fetch.js");
28
28
  const skill_js_1 = require("./skill.js");
29
29
  const exec_js_1 = require("./exec.js");
30
+ Object.defineProperty(exports, "addPendingApproval", { enumerable: true, get: function () { return exec_js_1.addPendingApproval; } });
31
+ Object.defineProperty(exports, "getAndRemovePendingApproval", { enumerable: true, get: function () { return exec_js_1.getAndRemovePendingApproval; } });
32
+ Object.defineProperty(exports, "cleanupExpiredApprovals", { enumerable: true, get: function () { return exec_js_1.cleanupExpiredApprovals; } });
30
33
  const file_js_1 = require("./file.js");
31
34
  const task_js_1 = require("./task.js");
32
35
  const model_js_1 = require("./model.js");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myassis/gateway",
3
- "version": "1.0.35",
3
+ "version": "1.0.37",
4
4
  "description": "我的助手 Gateway Service - 本地 AI 网关服务,支持认证、WebSocket 实时通信和任务调度",
5
5
  "main": "dist/index.js",
6
6
  "bin": {