@myassis/gateway 1.0.34 → 1.0.36

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,
@@ -125,7 +125,10 @@ async function getSystemPromptAsync(agent, customPrompt) {
125
125
  1.逐步思考,分步骤调用工具,工具调用前尽量返回content,明确当前工具调用的目的。
126
126
  2.工具调用的参数全部采用JSON语法,调用前先验证JSON语法正确后,再回复,切记未验证JSON语法就直接回复。
127
127
  【命令执行】执行命令时一定要根据当前系统来执行命令
128
- 【文件操作】文件编辑后一定要验证编辑是否成功,文件内容与预期是否一致,文件工具目前未提供批量替换操作,如果需要批量修改,请自行创建脚本来完成。
128
+ 【文件操作】
129
+ 1.文件编辑后一定要验证编辑是否成功,文件内容与预期是否一致。
130
+ 2.文件工具目前未提供批量替换操作,如果需要批量修改,请自行创建脚本来完成。
131
+ 3.大文件写入如果无法识别,请先写入主体内容,再使用edit工具进行修改来操作
129
132
  【特殊语法】mermaid折线图需要使用xychart-beta,完整示例如下:
130
133
  xychart-beta
131
134
  title "2025年上半年产品销量对比"
@@ -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: '在本地计算机上执行命令行命令。适用于运行系统命令、脚本、查看文件、执行程序等场景。',
@@ -86,22 +146,22 @@ exports.execTool = {
86
146
  },
87
147
  handler: async (args, sessionId) => {
88
148
  return new Promise(async (resolve) => {
89
- const { cwd, timeout = 60000 } = args;
149
+ const { cwd, timeout = 60000, approved } = args;
90
150
  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: '危险命令已拦截' });
151
+ // 检查危险命令(如果已批准则跳过)
152
+ if (!approved && isDangerousCommand(command)) {
153
+ if (!sessionId) {
154
+ resolve({ success: false, errorMessage: '危险命令需要用户确认,但缺少会话ID' });
103
155
  return;
104
156
  }
157
+ const token = addPendingApproval(command, cwd, timeout, sessionId);
158
+ resolve({
159
+ success: false,
160
+ needsApproval: true,
161
+ approvalToken: token,
162
+ errorMessage: `危险命令需要用户确认:${command}`,
163
+ });
164
+ return;
105
165
  }
106
166
  const options = {
107
167
  cwd,
@@ -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.34",
3
+ "version": "1.0.36",
4
4
  "description": "我的助手 Gateway Service - 本地 AI 网关服务,支持认证、WebSocket 实时通信和任务调度",
5
5
  "main": "dist/index.js",
6
6
  "bin": {