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 +5 -2
- package/package.json +1 -1
- package/src/agentRunner.js +2 -2
- package/src/agents/codex.js +79 -19
- package/src/agents/hermes.js +56 -16
- package/src/claudeRunner.js +62 -13
- package/src/imConnector.js +1 -1
- package/src/permissionState.js +83 -0
- package/src/session.js +6 -0
- package/src/slashCommands.js +43 -2
- package/src/wsServer.js +2 -2
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
|
-
-
|
|
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
package/src/agentRunner.js
CHANGED
|
@@ -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 = {}) {
|
package/src/agents/codex.js
CHANGED
|
@@ -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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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 —
|
|
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
|
|
398
|
+
if (getPendingPermission(session, String(message.id), 'codex')) return;
|
|
361
399
|
|
|
362
|
-
session.
|
|
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
|
|
587
|
+
// final content fallback if this item did not emit deltas
|
|
537
588
|
const text = extractFinalText(params);
|
|
538
|
-
|
|
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
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
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.
|
|
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('用户已停止'));
|
package/src/agents/hermes.js
CHANGED
|
@@ -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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
363
|
-
if (!pending
|
|
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.
|
|
408
|
+
removePendingPermission(session, pending.requestId);
|
|
369
409
|
|
|
370
410
|
try {
|
|
371
411
|
await fetchJson(`${gatewayUrl}/v1/runs/${encodeURIComponent(runId)}/approval`, agentConfig, {
|
package/src/claudeRunner.js
CHANGED
|
@@ -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', {
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
503
|
+
removePendingPermission(session, pending.requestId);
|
|
462
504
|
return true;
|
|
463
505
|
}
|
|
464
506
|
|
|
465
|
-
function resolvePendingPermission(approved, ws, session, config) {
|
|
466
|
-
|
|
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
|
-
|
|
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
|
|
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 });
|
package/src/imConnector.js
CHANGED
|
@@ -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
|
}
|
package/src/slashCommands.js
CHANGED
|
@@ -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
|
|
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;
|