linco-connect 1.0.7 → 1.0.9

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/README.md CHANGED
@@ -32,11 +32,11 @@ Codex: wss://chat.ddjf.info/socket/ai/codex
32
32
  - 会话隔离:每个 Agent 和远端 session 都创建独立 session、workspace、attachments、outbox。
33
33
  - Markdown 展示:支持 GFM Markdown、代码块、表格、引用和安全链接。
34
34
  - 工具调用展示:Agent 工具输入、输出和错误以可折叠卡片展示。
35
- - 权限确认:Agent 请求工具权限时可在前端批准或拒绝。
35
+ - 权限确认:工具/命令权限请求和危险操作确认默认自动允许;可用 `/approve off` 为当前会话恢复手动确认。
36
36
  - 附件上传:支持普通文件和图片;图片作为多模态内容发送,普通文件保存为本地路径引用。
37
37
  - 图片粘贴:支持从剪贴板粘贴图片。
38
38
  - 文件下发:Agent 生成文件放入 outbox 后,前端自动展示下载/预览卡片。
39
- - 斜杠命令:内置 `/help`、`/pwd`、`/cd`、`/new`、`/stop`、`/base`、`/status`、`/list`。
39
+ - 斜杠命令:内置 `/help`、`/pwd`、`/cd`、`/new`、`/stop`、`/base`、`/status`、`/list`、`/approve`。
40
40
  - Windows Git Bash 检测:Windows 下自动查找 Git Bash,也可手动指定。
41
41
 
42
42
  ## 前置条件
@@ -377,6 +377,9 @@ outbox 目录: .../outbox
377
377
  | `/base` | 显示 Linco 运行目录、附件目录和 outbox 目录 |
378
378
  | `/list` | 列出当前 IM 会话最近 10 条 Agent Session 历史 |
379
379
  | `/list <条数>` | 按指定条数列出最近的 Agent Session 历史 |
380
+ | `/approve` | 显示当前自动审批状态 |
381
+ | `/approve on` | 开启当前会话自动审批,后续工具/命令权限请求和危险操作确认自动允许(默认) |
382
+ | `/approve off` | 关闭当前会话自动审批,恢复手动确认 |
380
383
 
381
384
  除上述本地命令外,其他 `/xxx` 会透传给当前 Agent CLI。
382
385
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linco-connect",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "自研 IM 桥接多 Agent 服务",
5
5
  "main": "server.js",
6
6
  "bin": {
@@ -23,8 +23,8 @@ function resolvePendingDanger(confirmed, ws, session, config) {
23
23
  return providerFor(session).resolvePendingDanger?.(confirmed, ws, session, config) || false;
24
24
  }
25
25
 
26
- function resolvePendingPermission(allowed, ws, session, config) {
27
- return providerFor(session).resolvePendingPermission?.(allowed, ws, session, config) || false;
26
+ function resolvePendingPermission(allowed, ws, session, config, requestId) {
27
+ return providerFor(session).resolvePendingPermission?.(allowed, ws, session, config, requestId) || false;
28
28
  }
29
29
 
30
30
  function stopAgentProcess(session, options = {}) {
@@ -4,18 +4,29 @@ const { send, sendError, sendSystem } = require('../protocol');
4
4
  const { persistAgentSessionId, stopAgentProcess: stopSessionProcess, updateAgentSessionHistory } = require('../session');
5
5
  const { getOutboxDir } = require('../outgoingAttachmentHandler');
6
6
  const { createTextStreamBuffer, appendTextStream, flushTextStream, resetTextStream } = require('../streamBuffer');
7
+ const {
8
+ clearPendingPermissions,
9
+ getPendingPermission,
10
+ pendingPermissionIds,
11
+ removePendingPermission,
12
+ setPendingPermission,
13
+ } = require('../permissionState');
7
14
 
8
15
  function execute(input, ws, session, config) {
9
16
  const textForCheck = stringifyInput(input);
10
17
  if (isDangerousCommand(textForCheck)) {
11
- const preview = textForCheck.slice(0, 200);
12
- session.pendingDanger = { input };
13
- send(ws, 'danger_warning', {
14
- text: `⚠️ 检测到可能的危险操作,请确认是否继续执行:
18
+ if (session.autoApprove === true) {
19
+ sendSystem(ws, '✅ 已自动批准危险操作确认。');
20
+ } else {
21
+ const preview = textForCheck.slice(0, 200);
22
+ session.pendingDanger = { input };
23
+ send(ws, 'danger_warning', {
24
+ text: `⚠️ 检测到可能的危险操作,请确认是否继续执行:
15
25
 
16
26
  "${preview}${textForCheck.length > 200 ? '...' : ''}"`,
17
- });
18
- return;
27
+ });
28
+ return;
29
+ }
19
30
  }
20
31
 
21
32
  if (session.isTurnActive) {
@@ -44,6 +55,7 @@ function runAppServerTurn(input, ws, session, config) {
44
55
  session.currentInputForNoOutput = input;
45
56
  session.sawPartialAssistantText = false;
46
57
  session.codexAssistantEnded = false;
58
+ session.codexEmittedAgentMessageIds = new Set();
47
59
  resetCodexAssistantText(session);
48
60
  session._lastWs = ws;
49
61
  session._lastConfig = config;
@@ -165,6 +177,7 @@ function ensureAppServer(session, config) {
165
177
  session.codexAppServer = null;
166
178
  }
167
179
  clearTurnState(session);
180
+ clearPendingPermissions(session, 'codex');
168
181
  // drain pending requests
169
182
  for (const [, pending] of session.codexPendingRequests) {
170
183
  pending.reject(new Error(`app-server 已退出,code=${code}`));
@@ -298,6 +311,31 @@ function resetCodexAssistantText(session) {
298
311
  resetTextStream(ensureCodexStreamState(session));
299
312
  }
300
313
 
314
+ function codexAgentMessageId(params) {
315
+ return String(params.item?.id || params.itemId || params.id || '').trim();
316
+ }
317
+
318
+ function markCodexAgentMessageEmitted(session, itemId) {
319
+ const id = String(itemId || '').trim();
320
+ if (!id) return;
321
+ if (!(session.codexEmittedAgentMessageIds instanceof Set)) {
322
+ session.codexEmittedAgentMessageIds = new Set();
323
+ }
324
+ session.codexEmittedAgentMessageIds.add(id);
325
+ }
326
+
327
+ function hasCodexAgentMessageEmitted(session, itemId) {
328
+ const id = String(itemId || '').trim();
329
+ return Boolean(id && session.codexEmittedAgentMessageIds?.has(id));
330
+ }
331
+
332
+ function shouldAppendCompletedAgentMessage(session, params) {
333
+ const itemId = codexAgentMessageId(params);
334
+ if (itemId) return !hasCodexAgentMessageEmitted(session, itemId);
335
+ if (!session.sawPartialAssistantText) return true;
336
+ return params.item?.phase === 'final_answer';
337
+ }
338
+
301
339
  function nextRpcId(session) {
302
340
  session.codexRpcId = (session.codexRpcId || 0) + 1;
303
341
  return session.codexRpcId;
@@ -352,21 +390,33 @@ function handleServerRequest(message, session) {
352
390
  return;
353
391
  }
354
392
 
355
- // Command execution approval — send to frontend for user approval
393
+ // Command execution approval — auto-approve by default, or ask the user when disabled.
356
394
  if (method === 'item/commandExecution/requestApproval' || method === 'execCommandApproval') {
357
395
  const cmd = params.command || params.tool || '';
358
- session._log?.info('codex command execution approval requested', { method, command: cmd });
396
+ session._log?.info('codex command execution approval requested', { method, command: cmd, autoApprove: session.autoApprove === true });
359
397
 
360
- if (session.pendingPermission?.requestId === String(message.id)) return;
398
+ if (getPendingPermission(session, String(message.id), 'codex')) return;
361
399
 
362
- session.pendingPermission = {
400
+ if (session.autoApprove === true) {
401
+ sendJsonRpc(session.codexAppServer, {
402
+ jsonrpc: '2.0',
403
+ id: message.id,
404
+ result: { decision: 'accept' },
405
+ });
406
+ if (ws) {
407
+ sendSystem(ws, `✅ 已自动批准命令执行:${cmd}`);
408
+ }
409
+ return;
410
+ }
411
+
412
+ setPendingPermission(session, {
363
413
  provider: 'codex',
364
414
  requestId: String(message.id),
365
415
  toolName: 'exec',
366
416
  input: cmd,
367
417
  _codexMethod: method,
368
418
  _rpcId: message.id,
369
- };
419
+ });
370
420
 
371
421
  if (ws) {
372
422
  send(ws, 'permission_request', {
@@ -509,6 +559,7 @@ function handleAppServerMessage(message, session) {
509
559
  const delta = params.delta || '';
510
560
  if (delta) {
511
561
  appendCodexAssistantText(delta, ws, session, () => send(ws, 'assistant_start', {}));
562
+ markCodexAgentMessageEmitted(session, codexAgentMessageId(params));
512
563
  }
513
564
  return;
514
565
  }
@@ -533,10 +584,13 @@ function handleAppServerMessage(message, session) {
533
584
  return;
534
585
  }
535
586
 
536
- // final content fallback if no deltas were emitted
587
+ // final content fallback if this item did not emit deltas
537
588
  const text = extractFinalText(params);
538
- if (text && !session.sawPartialAssistantText) {
589
+ const agentMessageId = codexAgentMessageId(params);
590
+ if (text && shouldAppendCompletedAgentMessage(session, params)) {
591
+ if (session.sawPartialAssistantText) appendCodexAssistantText('\n\n', ws, session, () => send(ws, 'assistant_start', {}));
539
592
  appendCodexAssistantText(text, ws, session, () => send(ws, 'assistant_start', {}));
593
+ markCodexAgentMessageEmitted(session, agentMessageId);
540
594
  }
541
595
  // Safety fallback: if turn/completed never arrives, clear isTurnActive so the next message doesn't get stuck.
542
596
  if (session.turnCompletedTimerId) clearTimeout(session.turnCompletedTimerId);
@@ -820,6 +874,7 @@ function buildExecArgs(session, agentConfig) {
820
874
  args.push('--json');
821
875
  if (!session.agentSessionId) args.push('--cd', session.workspace);
822
876
  if (agentConfig.model) args.push('--model', agentConfig.model);
877
+ if (session.autoApprove === true) args.push('--dangerously-bypass-approvals-and-sandbox');
823
878
  if (session.agentSessionId) args.push(session.agentSessionId);
824
879
  args.push('-');
825
880
  return args;
@@ -890,13 +945,17 @@ function resolvePendingDanger(confirmed, ws, session, config) {
890
945
  return true;
891
946
  }
892
947
 
893
- function resolvePendingPermission(approved, ws, session, config) {
894
- if (!session.pendingPermission) return false;
895
-
896
- const pending = session.pendingPermission;
897
- if (pending.provider !== 'codex') return false;
948
+ function resolvePendingPermission(approved, ws, session, config, requestId) {
949
+ const pending = getPendingPermission(session, requestId, 'codex');
950
+ if (!pending) {
951
+ session._log?.warn('codex permission response without pending request', {
952
+ requestId: requestId || '',
953
+ pendingRequestIds: pendingPermissionIds(session, 'codex'),
954
+ });
955
+ return false;
956
+ }
898
957
 
899
- session.pendingPermission = null;
958
+ removePendingPermission(session, pending.requestId);
900
959
  session._log?.info('codex permission response', { approved, toolName: pending.toolName, rpcId: pending._rpcId });
901
960
 
902
961
  // Respond to Codex RPC
@@ -932,6 +991,7 @@ function stop(session, options = {}) {
932
991
  }
933
992
  stopSessionProcess(session, options);
934
993
  clearTurnState(session);
994
+ clearPendingPermissions(session, 'codex');
935
995
  if (session.codexPendingRequests) {
936
996
  for (const [, pending] of session.codexPendingRequests) {
937
997
  pending.reject(new Error('用户已停止'));
@@ -4,6 +4,13 @@ const { persistAgentSessionId, stopAgentProcess: stopSessionProcess, updateAgent
4
4
  const { getOutboxDir } = require('../outgoingAttachmentHandler');
5
5
  const { createTextStreamBuffer, appendTextStream, flushTextStream, resetTextStream } = require('../streamBuffer');
6
6
  const { DEFAULT_GATEWAY_URL, ensureHermesGateway, normalizeGatewayUrl } = require('../hermesGateway');
7
+ const {
8
+ clearPendingPermissions,
9
+ getPendingPermission,
10
+ pendingPermissionIds,
11
+ removePendingPermission,
12
+ setPendingPermission,
13
+ } = require('../permissionState');
7
14
 
8
15
  function extractText(input) {
9
16
  if (!Array.isArray(input)) return String(input || '');
@@ -16,12 +23,16 @@ function extractText(input) {
16
23
  function execute(input, ws, session, config) {
17
24
  const textForCheck = stringifyInput(input);
18
25
  if (isDangerousCommand(textForCheck)) {
19
- const preview = textForCheck.slice(0, 200);
20
- session.pendingDanger = { input };
21
- send(ws, 'danger_warning', {
22
- text: `⚠️ 检测到可能的危险操作,请确认是否继续执行:\n\n"${preview}${textForCheck.length > 200 ? '...' : ''}"`,
23
- });
24
- return;
26
+ if (session.autoApprove === true) {
27
+ sendSystem(ws, '✅ 已自动批准危险操作确认。');
28
+ } else {
29
+ const preview = textForCheck.slice(0, 200);
30
+ session.pendingDanger = { input };
31
+ send(ws, 'danger_warning', {
32
+ text: `⚠️ 检测到可能的危险操作,请确认是否继续执行:\n\n"${preview}${textForCheck.length > 200 ? '...' : ''}"`,
33
+ });
34
+ return;
35
+ }
25
36
  }
26
37
 
27
38
  if (session.isTurnActive) {
@@ -156,7 +167,7 @@ function handleHermesEvent(event, ws, session, config) {
156
167
  send(ws, 'thinking', { text: event.text || '' });
157
168
  return;
158
169
  case 'approval.request':
159
- handleApprovalRequest(event, ws, session);
170
+ handleApprovalRequest(event, ws, session, config);
160
171
  return;
161
172
  case 'approval.responded':
162
173
  sendSystem(ws, event.choice === 'deny' ? '🚫 已拒绝 Hermes 操作。' : '✅ 已批准 Hermes 操作。');
@@ -177,17 +188,39 @@ function handleHermesEvent(event, ws, session, config) {
177
188
  }
178
189
  }
179
190
 
180
- function handleApprovalRequest(event, ws, session) {
181
- const requestId = `hermes-${event.run_id || session.hermesRunId}-${Date.now()}`;
191
+ function handleApprovalRequest(event, ws, session, config) {
192
+ const requestId = String(
193
+ event.requestId ||
194
+ event.request_id ||
195
+ event.approvalId ||
196
+ event.approval_id ||
197
+ event.id ||
198
+ `hermes-${event.run_id || session.hermesRunId}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
199
+ );
182
200
  const input = event.command || event.description || event.preview || '';
183
- session.pendingPermission = {
201
+
202
+ if (getPendingPermission(session, requestId, 'hermes')) return;
203
+
204
+ setPendingPermission(session, {
184
205
  provider: 'hermes',
185
206
  requestId,
186
207
  runId: event.run_id || session.hermesRunId,
187
208
  toolName: 'exec',
188
209
  input,
189
210
  choices: event.choices || [],
190
- };
211
+ });
212
+
213
+ if (session.autoApprove === true) {
214
+ config.logger?.info('hermes permission auto-approved', {
215
+ sessionId: session.id,
216
+ requestId,
217
+ });
218
+ resolvePendingPermission(true, ws, session, config, requestId).catch(err => {
219
+ sendError(ws, `Hermes 自动审批失败: ${err.message}`);
220
+ });
221
+ return;
222
+ }
223
+
191
224
  send(ws, 'permission_request', {
192
225
  requestId,
193
226
  toolName: 'exec',
@@ -222,7 +255,7 @@ function finishTurn(ws, session, config, options = {}) {
222
255
  session.hermesGatewayUrl = null;
223
256
  session.isTurnActive = false;
224
257
  session.currentInputForNoOutput = null;
225
- session.pendingPermission = null;
258
+ clearPendingPermissions(session, 'hermes');
226
259
  flushHermesAssistantText(ws, session);
227
260
  resetHermesAssistantText(session);
228
261
  if (drain) drainQueue(ws, session, config);
@@ -358,14 +391,21 @@ async function resolvePendingDanger(confirmed, ws, session, config) {
358
391
  return true;
359
392
  }
360
393
 
361
- async function resolvePendingPermission(approved, ws, session, config) {
362
- const pending = session.pendingPermission;
363
- if (!pending || pending.provider !== 'hermes') return false;
394
+ async function resolvePendingPermission(approved, ws, session, config, requestId) {
395
+ const pending = getPendingPermission(session, requestId, 'hermes');
396
+ if (!pending) {
397
+ config.logger?.warn('hermes permission response without pending request', {
398
+ sessionId: session.id,
399
+ requestId: requestId || '',
400
+ pendingRequestIds: pendingPermissionIds(session, 'hermes'),
401
+ });
402
+ return false;
403
+ }
364
404
 
365
405
  const agentConfig = config.agents?.hermes || {};
366
406
  const gatewayUrl = session.hermesGatewayUrl || normalizeGatewayUrl(agentConfig.gatewayUrl || agentConfig.baseUrl || DEFAULT_GATEWAY_URL);
367
407
  const runId = pending.runId || session.hermesRunId;
368
- session.pendingPermission = null;
408
+ removePendingPermission(session, pending.requestId);
369
409
 
370
410
  try {
371
411
  await fetchJson(`${gatewayUrl}/v1/runs/${encodeURIComponent(runId)}/approval`, agentConfig, {
@@ -6,12 +6,29 @@ const { buildOutboxSystemPrompt, getOutboxDir } = require('./outgoingAttachmentH
6
6
  const { send, sendError, sendSystem } = require('./protocol');
7
7
  const { appendTextStream, flushTextStream, resetTextStream } = require('./streamBuffer');
8
8
  const { persistClaudeSessionId, stopAgentProcess, updateAgentSessionHistory } = require('./session');
9
+ const {
10
+ getPendingPermission,
11
+ hasPendingPermissions,
12
+ pendingPermissionIds,
13
+ removePendingPermission,
14
+ setPendingPermission,
15
+ } = require('./permissionState');
9
16
 
10
17
  function executeClaudeQuery(input, ws, session, config) {
11
18
  const textForCheck = typeof input === 'string' ? input : extractText(input);
12
19
 
13
20
  if (isDangerousCommand(textForCheck)) {
14
- config.logger?.warn('dangerous command detected', { sessionId: session.id, chars: textForCheck.length });
21
+ config.logger?.warn('dangerous command detected', {
22
+ sessionId: session.id,
23
+ chars: textForCheck.length,
24
+ autoApprove: session.autoApprove === true,
25
+ });
26
+ if (session.autoApprove === true) {
27
+ sendSystem(ws, '✅ 已自动批准危险操作确认。');
28
+ enqueueClaudeQuery(input, ws, session, config);
29
+ return;
30
+ }
31
+
15
32
  const preview = textForCheck.slice(0, 200);
16
33
  send(ws, 'danger_warning', {
17
34
  text: `⚠️ 检测到可能的危险操作,请确认是否继续执行:\n\n"${preview}${textForCheck.length > 200 ? '...' : ''}"`
@@ -319,9 +336,7 @@ function handleClaudeMessage(parsed, ws, session, config) {
319
336
  handleControlRequest(parsed, ws, session, config);
320
337
  break;
321
338
  case 'control_cancel_request':
322
- if (session.pendingPermission?.requestId === parsed.request_id) {
323
- session.pendingPermission = null;
324
- }
339
+ removePendingPermission(session, parsed.request_id);
325
340
  break;
326
341
  }
327
342
  }
@@ -423,18 +438,45 @@ function handleControlRequest(parsed, ws, session, config) {
423
438
  const input = sanitizeToolInput(toolName, request.input || {});
424
439
  const inputText = summarizeInput(input);
425
440
 
426
- session.pendingPermission = {
441
+ if (requestID && getPendingPermission(session, requestID, 'claude')) {
442
+ config.logger?.info('duplicate permission request ignored', {
443
+ sessionId: session.id,
444
+ requestId: requestID,
445
+ toolName,
446
+ });
447
+ return;
448
+ }
449
+
450
+ const pending = {
451
+ provider: 'claude',
427
452
  requestId: requestID,
428
453
  toolName,
429
454
  input,
430
455
  };
456
+ setPendingPermission(session, pending);
431
457
 
432
458
  config.logger?.info('permission request received', {
433
459
  sessionId: session.id,
434
460
  requestId: requestID,
435
461
  toolName,
462
+ autoApprove: session.autoApprove === true,
436
463
  });
437
464
 
465
+ if (session.autoApprove === true) {
466
+ try {
467
+ respondPermission(session, true, requestID);
468
+ config.logger?.info('permission auto-approved', {
469
+ sessionId: session.id,
470
+ requestId: requestID,
471
+ toolName,
472
+ });
473
+ sendSystem(ws, `✅ 已自动批准工具使用:${toolName}`);
474
+ } catch (err) {
475
+ sendError(ws, `❌ 自动批准工具权限失败: ${err.message}`);
476
+ }
477
+ return;
478
+ }
479
+
438
480
  send(ws, 'permission_request', {
439
481
  requestId: requestID,
440
482
  toolName,
@@ -442,8 +484,8 @@ function handleControlRequest(parsed, ws, session, config) {
442
484
  });
443
485
  }
444
486
 
445
- function respondPermission(session, approved) {
446
- const pending = session.pendingPermission;
487
+ function respondPermission(session, approved, requestId) {
488
+ const pending = getPendingPermission(session, requestId, 'claude');
447
489
  if (!pending) return false;
448
490
 
449
491
  const response = approved
@@ -458,16 +500,23 @@ function respondPermission(session, approved) {
458
500
  response,
459
501
  },
460
502
  });
461
- session.pendingPermission = null;
503
+ removePendingPermission(session, pending.requestId);
462
504
  return true;
463
505
  }
464
506
 
465
- function resolvePendingPermission(approved, ws, session, config) {
466
- if (!session.pendingPermission) return false;
507
+ function resolvePendingPermission(approved, ws, session, config, requestId) {
508
+ const pending = getPendingPermission(session, requestId, 'claude');
509
+ if (!pending) {
510
+ config.logger?.warn('permission response without pending request', {
511
+ sessionId: session.id,
512
+ requestId: requestId || '',
513
+ pendingRequestIds: pendingPermissionIds(session, 'claude'),
514
+ });
515
+ return false;
516
+ }
467
517
 
468
518
  try {
469
- const pending = session.pendingPermission;
470
- respondPermission(session, approved);
519
+ respondPermission(session, approved, pending.requestId);
471
520
  config.logger?.info('permission response sent', {
472
521
  sessionId: session.id,
473
522
  requestId: pending?.requestId,
@@ -499,7 +548,7 @@ function resolvePendingDanger(approved, ws, session, config) {
499
548
  }
500
549
 
501
550
  function drainMessageQueue(ws, session, config) {
502
- if (session.isTurnActive || session.pendingPermission) return;
551
+ if (session.isTurnActive || hasPendingPermissions(session, 'claude')) return;
503
552
  const next = session.messageQueue.shift();
504
553
  if (!next) return;
505
554
  config.logger?.info('message dequeued', { sessionId: session.id, queueLength: session.messageQueue.length });
@@ -222,7 +222,7 @@ class ImConnector {
222
222
  return;
223
223
  }
224
224
 
225
- if (!resolvePendingPermission(!!msg.approved, session.ws, session, this.config)) {
225
+ if (!resolvePendingPermission(!!msg.approved, session.ws, session, this.config, msg.requestId)) {
226
226
  sendError(session.ws, '没有待确认的工具权限请求');
227
227
  }
228
228
  }
@@ -0,0 +1,83 @@
1
+ function ensurePendingPermissionMap(session) {
2
+ if (session.pendingPermissions instanceof Map) return session.pendingPermissions;
3
+
4
+ const map = new Map();
5
+ if (session.pendingPermission?.requestId) {
6
+ map.set(String(session.pendingPermission.requestId), session.pendingPermission);
7
+ }
8
+ session.pendingPermissions = map;
9
+ return map;
10
+ }
11
+
12
+ function setPendingPermission(session, pending) {
13
+ if (!pending?.requestId) return;
14
+ const map = ensurePendingPermissionMap(session);
15
+ map.set(String(pending.requestId), pending);
16
+ syncLegacyPendingPermission(session);
17
+ }
18
+
19
+ function getPendingPermission(session, requestId, provider) {
20
+ const map = ensurePendingPermissionMap(session);
21
+ const normalized = String(requestId || '').trim();
22
+
23
+ if (normalized) {
24
+ const pending = map.get(normalized) || null;
25
+ return matchesProvider(pending, provider) ? pending : null;
26
+ }
27
+
28
+ const matching = Array.from(map.values()).filter(pending => matchesProvider(pending, provider));
29
+ return matching.length === 1 ? matching[0] : null;
30
+ }
31
+
32
+ function removePendingPermission(session, requestId) {
33
+ const map = ensurePendingPermissionMap(session);
34
+ const normalized = String(requestId || '').trim();
35
+ if (normalized) map.delete(normalized);
36
+ syncLegacyPendingPermission(session);
37
+ }
38
+
39
+ function clearPendingPermissions(session, provider) {
40
+ const map = ensurePendingPermissionMap(session);
41
+ if (!provider) {
42
+ map.clear();
43
+ syncLegacyPendingPermission(session);
44
+ return;
45
+ }
46
+
47
+ for (const [requestId, pending] of map) {
48
+ if (matchesProvider(pending, provider)) map.delete(requestId);
49
+ }
50
+ syncLegacyPendingPermission(session);
51
+ }
52
+
53
+ function hasPendingPermissions(session, provider) {
54
+ const map = ensurePendingPermissionMap(session);
55
+ if (!provider) return map.size > 0;
56
+ return Array.from(map.values()).some(pending => matchesProvider(pending, provider));
57
+ }
58
+
59
+ function pendingPermissionIds(session, provider) {
60
+ return Array.from(ensurePendingPermissionMap(session).values())
61
+ .filter(pending => matchesProvider(pending, provider))
62
+ .map(pending => String(pending.requestId));
63
+ }
64
+
65
+ function matchesProvider(pending, provider) {
66
+ if (!pending) return false;
67
+ return !provider || pending.provider === provider;
68
+ }
69
+
70
+ function syncLegacyPendingPermission(session) {
71
+ const remaining = Array.from(ensurePendingPermissionMap(session).values());
72
+ session.pendingPermission = remaining.length ? remaining[remaining.length - 1] : null;
73
+ }
74
+
75
+ module.exports = {
76
+ clearPendingPermissions,
77
+ ensurePendingPermissionMap,
78
+ getPendingPermission,
79
+ hasPendingPermissions,
80
+ pendingPermissionIds,
81
+ removePendingPermission,
82
+ setPendingPermission,
83
+ };
package/src/session.js CHANGED
@@ -33,6 +33,7 @@ function createSession(config, { externalSessionId, agentType = 'claude' } = {})
33
33
  const workspace = resolveSavedWorkspace(metadata.workspace || legacyMetadata.workspace, defaultWorkspace);
34
34
  const agentSessionId = metadata.agentSessionId || metadata.claudeSessionId || legacyMetadata.agentSessionId || legacyMetadata.claudeSessionId || null;
35
35
  const agentSessionHistory = metadata.agentSessionHistory || [];
36
+ const autoApprove = metadata.autoApprove !== false;
36
37
 
37
38
  const activeEntry = agentSessionHistory.find(e => e.id === agentSessionId);
38
39
  const messageCount = activeEntry?.messageCount ?? 0;
@@ -60,7 +61,9 @@ function createSession(config, { externalSessionId, agentType = 'claude' } = {})
60
61
  currentInputForNoOutput: null,
61
62
  messageQueue: [],
62
63
  pendingDanger: null,
64
+ autoApprove,
63
65
  pendingPermission: null,
66
+ pendingPermissions: new Map(),
64
67
  streamState: createStreamState(),
65
68
  sawPartialAssistantText: false,
66
69
  outgoingAttachments: new Map(),
@@ -169,6 +172,7 @@ function saveSessionMetadata(session) {
169
172
  agentSessionId: session.agentSessionId || session.claudeSessionId || null,
170
173
  claudeSessionId: session.agentType === 'claude' ? (session.agentSessionId || session.claudeSessionId || null) : null,
171
174
  agentSessionHistory: session.agentSessionHistory || [],
175
+ autoApprove: session.autoApprove === true,
172
176
  updatedAt: new Date().toISOString(),
173
177
  };
174
178
  fs.writeFileSync(sessionMetadataPath(session), `${JSON.stringify(metadata, null, 2)}\n`);
@@ -294,6 +298,8 @@ function resetConversationState(session, { clearAgentSession = true, clearClaude
294
298
  session.messageQueue = [];
295
299
  session.pendingDanger = null;
296
300
  session.pendingPermission = null;
301
+ if (session.pendingPermissions?.clear) session.pendingPermissions.clear();
302
+ else session.pendingPermissions = new Map();
297
303
  session.sawPartialAssistantText = false;
298
304
  clearStreamState(session);
299
305
  }
@@ -3,7 +3,8 @@ const path = require('path');
3
3
  const { resetOutgoingAttachments, startOutboxWatcher, stopOutboxWatcher } = require('./outgoingAttachmentHandler');
4
4
  const { sendError, sendSystem } = require('./protocol');
5
5
  const { removeAgentSessionFromHistory, saveSessionMetadata } = require('./session');
6
- const { stopAgentProcess } = require('./agentRunner');
6
+ const { resolvePendingDanger, resolvePendingPermission, stopAgentProcess } = require('./agentRunner');
7
+ const { clearPendingPermissions, pendingPermissionIds } = require('./permissionState');
7
8
 
8
9
  function localCommandsHelp() {
9
10
  return `📋 Linco 本地命令:
@@ -19,6 +20,7 @@ function localCommandsHelp() {
19
20
  /list [条数] - 列出当前 IM 会话下最近的 Agent Session 历史(默认 10 条)
20
21
  /switch <序号> - 切换到指定 Agent Session(恢复上下文)
21
22
  /delete <序号> - 从历史记录中删除指定 Agent Session
23
+ /approve on/off - 开启或关闭自动审批(默认开启)
22
24
  /usage - 显示 Token 用量统计`;
23
25
  }
24
26
 
@@ -96,6 +98,10 @@ outbox 目录: ${session.outboxDir}`);
96
98
  handleDelete(parts[1], ws, session);
97
99
  return true;
98
100
 
101
+ case '/approve':
102
+ handleApprove(parts[1], ws, session, config);
103
+ return true;
104
+
99
105
  case '/usage':
100
106
  handleUsage(ws, session);
101
107
  return true;
@@ -126,6 +132,7 @@ Agent session: ${session.agentSessionId || session.claudeSessionId || '(尚未
126
132
  Agent 进程: ${processRunning ? '运行中' : '未运行'}
127
133
  当前 turn: ${session.isTurnActive ? '进行中' : '空闲'}
128
134
  排队消息: ${session.messageQueue.length}
135
+ 自动审批: ${session.autoApprove ? '开启' : '关闭'}
129
136
  待确认: ${session.pendingPermission ? '工具权限' : session.pendingDanger ? '危险操作' : '无'}`);
130
137
  }
131
138
 
@@ -276,7 +283,7 @@ function handleSwitch(indexOrId, ws, session, config) {
276
283
  session.currentInputForNoOutput = null;
277
284
  session.messageQueue = [];
278
285
  session.pendingDanger = null;
279
- session.pendingPermission = null;
286
+ clearPendingPermissions(session);
280
287
  }
281
288
 
282
289
  // Verify the change persisted correctly
@@ -361,6 +368,40 @@ function handleUsage(ws, session) {
361
368
  sendSystem(ws, text);
362
369
  }
363
370
 
371
+ function handleApprove(mode, ws, session, config) {
372
+ const value = String(mode || '').trim().toLowerCase();
373
+
374
+ if (!value || value === 'status') {
375
+ sendSystem(ws, `自动审批当前为:${session.autoApprove ? '开启' : '关闭'}。\n使用 /approve on 开启,/approve off 关闭。默认开启。`);
376
+ return;
377
+ }
378
+
379
+ if (!['on', 'off'].includes(value)) {
380
+ sendError(ws, '❌ /approve 参数只能是 on 或 off,例如 /approve on。');
381
+ return;
382
+ }
383
+
384
+ session.autoApprove = value === 'on';
385
+ saveSessionMetadata(session);
386
+
387
+ let approvedPending = 0;
388
+ let approvedDanger = false;
389
+ if (session.autoApprove) {
390
+ const provider = session.agentType || 'claude';
391
+ for (const requestId of pendingPermissionIds(session, provider)) {
392
+ const resolved = resolvePendingPermission(true, ws, session, config, requestId);
393
+ if (resolved) approvedPending += 1;
394
+ }
395
+ if (session.pendingDanger) {
396
+ approvedDanger = !!resolvePendingDanger(true, ws, session, config);
397
+ }
398
+ }
399
+
400
+ sendSystem(ws, session.autoApprove
401
+ ? `✅ 自动审批已开启:后续工具/命令权限请求和危险操作确认会自动允许。${approvedPending ? `\n已自动批准当前等待中的 ${approvedPending} 个权限请求。` : ''}${approvedDanger ? '\n已自动批准当前等待中的危险操作确认。' : ''}`
402
+ : '✅ 自动审批已关闭:后续工具/命令权限请求和危险操作确认会回到手动确认。');
403
+ }
404
+
364
405
  module.exports = {
365
406
  handleSlashCommand,
366
407
  };
package/src/wsServer.js CHANGED
@@ -173,7 +173,7 @@ function handleMessage(data, ws, session, config) {
173
173
  }
174
174
  if (msg.type === 'permission_response') {
175
175
  config.logger?.info('permission response received (linco)', { sessionKey: msg.sessionKey, approved: !!msg.approved });
176
- if (!resolvePendingPermission(!!msg.approved, session.ws, session, config)) {
176
+ if (!resolvePendingPermission(!!msg.approved, session.ws, session, config, msg.requestId)) {
177
177
  sendError(session.ws, '❌ 没有待确认的工具权限请求');
178
178
  }
179
179
  return;
@@ -192,7 +192,7 @@ function handleMessage(data, ws, session, config) {
192
192
 
193
193
  if (msg.type === 'permission_response') {
194
194
  config.logger?.info('permission response received', { sessionId: session.id, approved: !!msg.approved });
195
- if (!resolvePendingPermission(!!msg.approved, ws, session, config)) {
195
+ if (!resolvePendingPermission(!!msg.approved, ws, session, config, msg.requestId)) {
196
196
  sendError(ws, '❌ 没有待确认的工具权限请求');
197
197
  }
198
198
  return;