linco-connect 1.0.0
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 +426 -0
- package/bin/linco.js +465 -0
- package/package.json +25 -0
- package/public/index.html +1457 -0
- package/server.js +17 -0
- package/src/agentRunner.js +37 -0
- package/src/agents/claude.js +1 -0
- package/src/agents/codex.js +869 -0
- package/src/attachmentHandler.js +258 -0
- package/src/claudeRunner.js +564 -0
- package/src/config.js +371 -0
- package/src/danger.js +21 -0
- package/src/httpStatic.js +166 -0
- package/src/imConnector.js +488 -0
- package/src/imageHandler.js +38 -0
- package/src/lincoProtocol.js +209 -0
- package/src/localAuth.js +46 -0
- package/src/logger.js +137 -0
- package/src/outgoingAttachmentHandler.js +204 -0
- package/src/protocol.js +17 -0
- package/src/serverApp.js +61 -0
- package/src/session.js +359 -0
- package/src/slashCommands.js +349 -0
- package/src/streamBuffer.js +73 -0
- package/src/wsServer.js +293 -0
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
const { spawn } = require('child_process');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { isDangerousCommand } = require('./danger');
|
|
5
|
+
const { buildOutboxSystemPrompt, getOutboxDir } = require('./outgoingAttachmentHandler');
|
|
6
|
+
const { send, sendError, sendSystem } = require('./protocol');
|
|
7
|
+
const { appendTextStream, flushTextStream, resetTextStream } = require('./streamBuffer');
|
|
8
|
+
const { persistClaudeSessionId, stopAgentProcess, updateAgentSessionHistory } = require('./session');
|
|
9
|
+
|
|
10
|
+
function executeClaudeQuery(input, ws, session, config) {
|
|
11
|
+
const textForCheck = typeof input === 'string' ? input : extractText(input);
|
|
12
|
+
|
|
13
|
+
if (isDangerousCommand(textForCheck)) {
|
|
14
|
+
config.logger?.warn('dangerous command detected', { sessionId: session.id, chars: textForCheck.length });
|
|
15
|
+
const preview = textForCheck.slice(0, 200);
|
|
16
|
+
send(ws, 'danger_warning', {
|
|
17
|
+
text: `⚠️ 检测到可能的危险操作,请确认是否继续执行:\n\n"${preview}${textForCheck.length > 200 ? '...' : ''}"`
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
session.pendingDanger = {
|
|
21
|
+
kind: 'message',
|
|
22
|
+
resolve: () => enqueueClaudeQuery(input, ws, session, config),
|
|
23
|
+
};
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
enqueueClaudeQuery(input, ws, session, config);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function enqueueClaudeQuery(input, ws, session, config) {
|
|
31
|
+
if (session.isTurnActive) {
|
|
32
|
+
if (session.messageQueue.length >= config.maxMessageQueue) {
|
|
33
|
+
config.logger?.warn('message queue full', {
|
|
34
|
+
sessionId: session.id,
|
|
35
|
+
queueLength: session.messageQueue.length,
|
|
36
|
+
maxQueue: config.maxMessageQueue,
|
|
37
|
+
});
|
|
38
|
+
sendError(ws, '❌ 消息队列已满,请稍后再发送。');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
session.messageQueue.push({ input, ws, config });
|
|
42
|
+
config.logger?.info('message queued', {
|
|
43
|
+
sessionId: session.id,
|
|
44
|
+
queueLength: session.messageQueue.length,
|
|
45
|
+
maxQueue: config.maxMessageQueue,
|
|
46
|
+
});
|
|
47
|
+
sendSystem(ws, `⏳ Claude 正在回复,已加入队列(${session.messageQueue.length})`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
sendClaudeQuery(input, ws, session, config);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function sendClaudeQuery(input, ws, session, config) {
|
|
55
|
+
const child = ensureClaudeProcess(ws, session, config);
|
|
56
|
+
if (!child) return;
|
|
57
|
+
|
|
58
|
+
const payload = buildClaudePayload(input, session, config);
|
|
59
|
+
const resumeId = session.agentSessionId || session.claudeSessionId;
|
|
60
|
+
config.logger?.info('claude turn started', {
|
|
61
|
+
sessionId: session.id,
|
|
62
|
+
chars: extractText(input).length,
|
|
63
|
+
resumeId: resumeId || '(new session)',
|
|
64
|
+
workspace: session.workspace,
|
|
65
|
+
});
|
|
66
|
+
session.isTurnActive = true;
|
|
67
|
+
session.currentInputForNoOutput = input;
|
|
68
|
+
resetStreamForTurn(session);
|
|
69
|
+
sendSystem(ws, `🤔 Claude 正在思考... (resume: ${resumeId || 'new'})`);
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
writeClaudeJson(session, payload);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
config.logger?.error('claude stdin write failed', { sessionId: session.id, error: err.message });
|
|
75
|
+
session.isTurnActive = false;
|
|
76
|
+
session.currentInputForNoOutput = null;
|
|
77
|
+
sendError(ws, `❌ 发送消息到 Claude 失败: ${err.message}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function ensureClaudeProcess(ws, session, config) {
|
|
82
|
+
if (session.claudeProcess && session.claudeProcess.exitCode === null && !session.claudeProcess.killed) {
|
|
83
|
+
return session.claudeProcess;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const args = [
|
|
87
|
+
'--input-format', 'stream-json',
|
|
88
|
+
'--output-format', 'stream-json',
|
|
89
|
+
'--include-partial-messages',
|
|
90
|
+
'--permission-prompt-tool', 'stdio',
|
|
91
|
+
'--verbose',
|
|
92
|
+
'--append-system-prompt', buildOutboxSystemPrompt(session, config),
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
if (config.claudeAddRuntimeDir !== false && session.runtimeDir) {
|
|
96
|
+
args.push('--add-dir', session.runtimeDir);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const resumeSessionId = session.agentSessionId || session.claudeSessionId;
|
|
100
|
+
if (resumeSessionId) {
|
|
101
|
+
args.push('--resume', resumeSessionId);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const env = { ...process.env };
|
|
105
|
+
if (config.gitBashEnv) {
|
|
106
|
+
env.CLAUDE_CODE_GIT_BASH_PATH = config.gitBashEnv;
|
|
107
|
+
}
|
|
108
|
+
delete env.CLAUDECODE;
|
|
109
|
+
|
|
110
|
+
const spawnTarget = resolveClaudeSpawnTarget(config.claudeBin);
|
|
111
|
+
config.logger?.info('claude process starting', {
|
|
112
|
+
sessionId: session.id,
|
|
113
|
+
cwd: session.workspace,
|
|
114
|
+
command: config.claudeBin,
|
|
115
|
+
spawnCommand: spawnTarget.command,
|
|
116
|
+
resume: !!resumeSessionId,
|
|
117
|
+
resumeSessionId: resumeSessionId || '(none)',
|
|
118
|
+
fullCommand: `${spawnTarget.command} ${args.join(' ')}`,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const child = spawn(spawnTarget.command, args, {
|
|
122
|
+
cwd: session.workspace,
|
|
123
|
+
env,
|
|
124
|
+
shell: spawnTarget.shell,
|
|
125
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
126
|
+
windowsHide: true,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
session.claudeProcess = child;
|
|
130
|
+
session.agentProcess = child;
|
|
131
|
+
session.stdoutBuffer = '';
|
|
132
|
+
|
|
133
|
+
child.stdout.on('data', (chunk) => {
|
|
134
|
+
if (session.claudeProcess !== child) return;
|
|
135
|
+
|
|
136
|
+
// Check first line for session_id to verify resume worked
|
|
137
|
+
if (session._checkingResume) {
|
|
138
|
+
session._checkingResume = false;
|
|
139
|
+
} else {
|
|
140
|
+
const raw = chunk.toString();
|
|
141
|
+
const firstLine = raw.split('\n').find(l => l.trim());
|
|
142
|
+
if (firstLine) {
|
|
143
|
+
try {
|
|
144
|
+
const parsed = JSON.parse(firstLine);
|
|
145
|
+
if (parsed.type === 'system' && parsed.subtype === 'init') {
|
|
146
|
+
session._checkingResume = true;
|
|
147
|
+
const actualId = parsed.session_id;
|
|
148
|
+
const expectedId = resumeSessionId;
|
|
149
|
+
config.logger?.info('claude session initialized', {
|
|
150
|
+
sessionId: session.id,
|
|
151
|
+
expectedResumeId: expectedId || '(none)',
|
|
152
|
+
actualSessionId: actualId,
|
|
153
|
+
resumed: actualId === expectedId,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
} catch (e) {}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
handleClaudeStdoutData(chunk, ws, session, config);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
child.stderr.on('data', (data) => {
|
|
164
|
+
if (session.claudeProcess !== child) return;
|
|
165
|
+
const errText = data.toString().trim();
|
|
166
|
+
if (errText) {
|
|
167
|
+
config.logger?.warn('claude stderr', { sessionId: session.id, stderr: errText });
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
child.on('close', (code) => {
|
|
172
|
+
config.logger?.info('claude process closed', { sessionId: session.id, code });
|
|
173
|
+
if (session.claudeProcess !== child) return;
|
|
174
|
+
session.claudeProcess = null;
|
|
175
|
+
flushAssistantText(ws, session);
|
|
176
|
+
if (session.isTurnActive) {
|
|
177
|
+
session.isTurnActive = false;
|
|
178
|
+
session.currentInputForNoOutput = null;
|
|
179
|
+
sendError(ws, code === 0 || code === null ? '⚠️ Claude 会话已结束。' : `❌ Claude 进程退出,退出码: ${code}`);
|
|
180
|
+
drainMessageQueue(ws, session, config);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
child.on('error', (err) => {
|
|
185
|
+
config.logger?.error('claude process spawn error', { sessionId: session.id, error: err.message });
|
|
186
|
+
if (session.claudeProcess !== child) return;
|
|
187
|
+
session.claudeProcess = null;
|
|
188
|
+
session.isTurnActive = false;
|
|
189
|
+
session.currentInputForNoOutput = null;
|
|
190
|
+
flushAssistantText(ws, session);
|
|
191
|
+
sendError(ws, `❌ 无法启动 Claude: ${err.message}\n请确认已安装 Claude Code 并设置好 API 密钥。`);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
return child;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function resolveClaudeSpawnTarget(command) {
|
|
198
|
+
if (process.platform !== 'win32') {
|
|
199
|
+
return { command, shell: false };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const normalized = path.normalize(command || '');
|
|
203
|
+
if (normalized.toLowerCase().endsWith('.cmd')) {
|
|
204
|
+
const exe = path.join(path.dirname(normalized), 'node_modules', '@anthropic-ai', 'claude-code', 'bin', 'claude.exe');
|
|
205
|
+
if (fs.existsSync(exe)) {
|
|
206
|
+
return { command: exe, shell: false };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return { command, shell: true };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function writeClaudeJson(session, payload) {
|
|
214
|
+
const child = session.claudeProcess;
|
|
215
|
+
if (!child || child.killed || child.exitCode !== null || child.stdin.destroyed) {
|
|
216
|
+
throw new Error('Claude 进程未运行');
|
|
217
|
+
}
|
|
218
|
+
child.stdin.write(JSON.stringify(payload) + '\n');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function handleClaudeStdoutData(chunk, ws, session, config) {
|
|
222
|
+
session.stdoutBuffer += chunk.toString();
|
|
223
|
+
const lines = session.stdoutBuffer.split('\n');
|
|
224
|
+
session.stdoutBuffer = lines.pop() || '';
|
|
225
|
+
|
|
226
|
+
for (const line of lines) {
|
|
227
|
+
if (!line.trim()) continue;
|
|
228
|
+
try {
|
|
229
|
+
handleClaudeMessage(JSON.parse(line), ws, session, config);
|
|
230
|
+
} catch (err) {
|
|
231
|
+
config.logger?.warn('claude stream-json parse failed', { sessionId: session.id, error: err.message });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function buildClaudePayload(input, session, config) {
|
|
237
|
+
return {
|
|
238
|
+
type: 'user',
|
|
239
|
+
message: {
|
|
240
|
+
role: 'user',
|
|
241
|
+
content: addOutboxHint(addStyleHint(input, config), session, config),
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function addStyleHint(input, config) {
|
|
247
|
+
const text = extractText(input).trim();
|
|
248
|
+
if (!text || text.startsWith('/')) return input;
|
|
249
|
+
|
|
250
|
+
const hint = `系统样式要求:${config.systemPrompt}`;
|
|
251
|
+
|
|
252
|
+
if (Array.isArray(input)) {
|
|
253
|
+
return [...input, { type: 'text', text: hint }];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return `${String(input || '')}\n\n${hint}`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function addOutboxHint(input, session, config) {
|
|
260
|
+
const text = extractText(input);
|
|
261
|
+
if (!shouldAddOutboxHint(text)) return input;
|
|
262
|
+
|
|
263
|
+
const hint = `系统提示:用户正在要求发送文件/图片。请将要发送给用户的最终文件保存或复制到以下 outbox 目录,系统只会自动发送这个目录中的新文件:\n${getOutboxDir(session, config)}`;
|
|
264
|
+
|
|
265
|
+
if (Array.isArray(input)) {
|
|
266
|
+
return [...input, { type: 'text', text: hint }];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return `${String(input || '')}\n\n${hint}`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function shouldAddOutboxHint(text) {
|
|
273
|
+
return /发送.*(文件|图片)|把.*(文件|图片).*发|发给我|发送给我|文件发送|发文件|下载文件/.test(text);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function handleClaudeMessage(parsed, ws, session, config) {
|
|
277
|
+
updateClaudeSessionId(parsed, session);
|
|
278
|
+
|
|
279
|
+
switch (parsed.type) {
|
|
280
|
+
case 'stream_event':
|
|
281
|
+
handleStreamEvent(parsed, ws, session);
|
|
282
|
+
break;
|
|
283
|
+
case 'assistant':
|
|
284
|
+
handleAssistantMessage(parsed, ws, session, config);
|
|
285
|
+
break;
|
|
286
|
+
case 'user':
|
|
287
|
+
handleUserMessage(parsed, ws, session, config);
|
|
288
|
+
break;
|
|
289
|
+
case 'result':
|
|
290
|
+
flushAssistantText(ws, session);
|
|
291
|
+
if (session.streamState.assistantStarted) {
|
|
292
|
+
send(ws, 'assistant_end');
|
|
293
|
+
} else {
|
|
294
|
+
sendSystem(ws, noOutputMessage(session));
|
|
295
|
+
}
|
|
296
|
+
if (parsed.usage) {
|
|
297
|
+
const u = parsed.usage;
|
|
298
|
+
if (!session.usage) {
|
|
299
|
+
session.usage = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0 };
|
|
300
|
+
}
|
|
301
|
+
if (u.input_tokens) session.usage.inputTokens += u.input_tokens;
|
|
302
|
+
if (u.output_tokens) session.usage.outputTokens += u.output_tokens;
|
|
303
|
+
if (u.cache_read_input_tokens) session.usage.cacheReadTokens += u.cache_read_input_tokens;
|
|
304
|
+
if (u.cache_creation_input_tokens) session.usage.cacheCreationTokens += u.cache_creation_input_tokens;
|
|
305
|
+
}
|
|
306
|
+
session.messageCount = (session.messageCount || 0) + 1;
|
|
307
|
+
updateAgentSessionHistory(session);
|
|
308
|
+
config.logger?.info('claude turn completed', {
|
|
309
|
+
sessionId: session.id,
|
|
310
|
+
queueLength: session.messageQueue.length,
|
|
311
|
+
usage: session.usage,
|
|
312
|
+
messageCount: session.messageCount,
|
|
313
|
+
});
|
|
314
|
+
session.isTurnActive = false;
|
|
315
|
+
session.currentInputForNoOutput = null;
|
|
316
|
+
drainMessageQueue(ws, session, config);
|
|
317
|
+
break;
|
|
318
|
+
case 'control_request':
|
|
319
|
+
handleControlRequest(parsed, ws, session, config);
|
|
320
|
+
break;
|
|
321
|
+
case 'control_cancel_request':
|
|
322
|
+
if (session.pendingPermission?.requestId === parsed.request_id) {
|
|
323
|
+
session.pendingPermission = null;
|
|
324
|
+
}
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function noOutputMessage(session) {
|
|
330
|
+
const input = session.currentInputForNoOutput;
|
|
331
|
+
const text = extractText(input).trim();
|
|
332
|
+
if (text.startsWith('/')) {
|
|
333
|
+
return '✅ 操作完成,但 Claude Code 没有返回可展示内容。部分原生斜杠命令在 Linco 的 stream-json 模式下可能只在 CLI/TUI 内部生效。';
|
|
334
|
+
}
|
|
335
|
+
return '✅ 操作完成(无输出)';
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function updateClaudeSessionId(parsed, session) {
|
|
339
|
+
if (parsed.session_id) {
|
|
340
|
+
persistClaudeSessionId(session, parsed.session_id);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function handleStreamEvent(parsed, ws, session) {
|
|
345
|
+
const event = parsed.event || {};
|
|
346
|
+
if (event.type !== 'content_block_delta') return;
|
|
347
|
+
|
|
348
|
+
const delta = event.delta || {};
|
|
349
|
+
if (delta.type === 'text_delta' && delta.text) {
|
|
350
|
+
session.sawPartialAssistantText = true;
|
|
351
|
+
appendAssistantText(delta.text, ws, session);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function handleAssistantMessage(parsed, ws, session, config) {
|
|
356
|
+
const content = parsed.message?.content;
|
|
357
|
+
if (!Array.isArray(content)) return;
|
|
358
|
+
|
|
359
|
+
for (const block of content) {
|
|
360
|
+
if (block.type === 'text' && block.text) {
|
|
361
|
+
if (!session.sawPartialAssistantText) {
|
|
362
|
+
appendAssistantText(block.text, ws, session);
|
|
363
|
+
}
|
|
364
|
+
} else if (block.type === 'tool_use') {
|
|
365
|
+
config.logger?.info('claude tool use', {
|
|
366
|
+
sessionId: session.id,
|
|
367
|
+
toolName: block.name || 'tool',
|
|
368
|
+
toolUseId: block.id || '',
|
|
369
|
+
});
|
|
370
|
+
send(ws, 'tool_call', {
|
|
371
|
+
id: block.id || '',
|
|
372
|
+
name: block.name || 'tool',
|
|
373
|
+
input: formatJson(block.input),
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function handleUserMessage(parsed, ws, session, config) {
|
|
380
|
+
const content = parsed.message?.content;
|
|
381
|
+
if (!Array.isArray(content)) return;
|
|
382
|
+
|
|
383
|
+
for (const block of content) {
|
|
384
|
+
if (block.type === 'tool_result') {
|
|
385
|
+
config.logger?.info('claude tool result', {
|
|
386
|
+
sessionId: session.id,
|
|
387
|
+
toolUseId: block.tool_use_id || '',
|
|
388
|
+
isError: !!block.is_error,
|
|
389
|
+
});
|
|
390
|
+
send(ws, 'tool_result', {
|
|
391
|
+
toolUseId: block.tool_use_id || '',
|
|
392
|
+
isError: !!block.is_error,
|
|
393
|
+
output: formatToolResult(block.content),
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function appendAssistantText(text, ws, session) {
|
|
400
|
+
appendTextStream(text, ws, session.streamState);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function flushAssistantText(ws, session) {
|
|
404
|
+
flushTextStream(ws, session.streamState);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function resetStreamForTurn(session) {
|
|
408
|
+
session.streamState.onStart = (startWs) => {
|
|
409
|
+
send(startWs, 'thinking_clear');
|
|
410
|
+
send(startWs, 'assistant_start');
|
|
411
|
+
};
|
|
412
|
+
resetTextStream(session.streamState);
|
|
413
|
+
session.sawPartialAssistantText = false;
|
|
414
|
+
session._checkingResume = false;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function handleControlRequest(parsed, ws, session, config) {
|
|
418
|
+
const requestID = parsed.request_id;
|
|
419
|
+
const request = parsed.request || {};
|
|
420
|
+
if (request.subtype !== 'can_use_tool') return;
|
|
421
|
+
|
|
422
|
+
const toolName = request.tool_name || 'tool';
|
|
423
|
+
const input = sanitizeToolInput(toolName, request.input || {});
|
|
424
|
+
const inputText = summarizeInput(input);
|
|
425
|
+
|
|
426
|
+
session.pendingPermission = {
|
|
427
|
+
requestId: requestID,
|
|
428
|
+
toolName,
|
|
429
|
+
input,
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
config.logger?.info('permission request received', {
|
|
433
|
+
sessionId: session.id,
|
|
434
|
+
requestId: requestID,
|
|
435
|
+
toolName,
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
send(ws, 'permission_request', {
|
|
439
|
+
requestId: requestID,
|
|
440
|
+
toolName,
|
|
441
|
+
input: inputText,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function respondPermission(session, approved) {
|
|
446
|
+
const pending = session.pendingPermission;
|
|
447
|
+
if (!pending) return false;
|
|
448
|
+
|
|
449
|
+
const response = approved
|
|
450
|
+
? { behavior: 'allow', updatedInput: pending.input || {} }
|
|
451
|
+
: { behavior: 'deny', message: 'The user denied this tool use. Stop and wait for the user\'s instructions.' };
|
|
452
|
+
|
|
453
|
+
writeClaudeJson(session, {
|
|
454
|
+
type: 'control_response',
|
|
455
|
+
response: {
|
|
456
|
+
subtype: 'success',
|
|
457
|
+
request_id: pending.requestId,
|
|
458
|
+
response,
|
|
459
|
+
},
|
|
460
|
+
});
|
|
461
|
+
session.pendingPermission = null;
|
|
462
|
+
return true;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function resolvePendingPermission(approved, ws, session, config) {
|
|
466
|
+
if (!session.pendingPermission) return false;
|
|
467
|
+
|
|
468
|
+
try {
|
|
469
|
+
const pending = session.pendingPermission;
|
|
470
|
+
respondPermission(session, approved);
|
|
471
|
+
config.logger?.info('permission response sent', {
|
|
472
|
+
sessionId: session.id,
|
|
473
|
+
requestId: pending?.requestId,
|
|
474
|
+
toolName: pending?.toolName,
|
|
475
|
+
approved,
|
|
476
|
+
});
|
|
477
|
+
sendSystem(ws, approved ? '✅ 已批准工具使用。' : '🚫 已拒绝工具使用。');
|
|
478
|
+
} catch (err) {
|
|
479
|
+
sendError(ws, `❌ 回复权限请求失败: ${err.message}`);
|
|
480
|
+
}
|
|
481
|
+
return true;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function resolvePendingDanger(approved, ws, session, config) {
|
|
485
|
+
if (!session.pendingDanger) return false;
|
|
486
|
+
|
|
487
|
+
const { resolve } = session.pendingDanger;
|
|
488
|
+
session.pendingDanger = null;
|
|
489
|
+
|
|
490
|
+
config.logger?.info('danger confirmation resolved', { sessionId: session.id, approved });
|
|
491
|
+
|
|
492
|
+
if (approved) {
|
|
493
|
+
sendSystem(ws, '⚠️ 危险操作已批准,执行中...');
|
|
494
|
+
resolve();
|
|
495
|
+
} else {
|
|
496
|
+
sendSystem(ws, '🚫 已取消危险操作');
|
|
497
|
+
}
|
|
498
|
+
return true;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function drainMessageQueue(ws, session, config) {
|
|
502
|
+
if (session.isTurnActive || session.pendingPermission) return;
|
|
503
|
+
const next = session.messageQueue.shift();
|
|
504
|
+
if (!next) return;
|
|
505
|
+
config.logger?.info('message dequeued', { sessionId: session.id, queueLength: session.messageQueue.length });
|
|
506
|
+
sendClaudeQuery(next.input, next.ws || ws, session, next.config || config);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function extractText(input) {
|
|
510
|
+
if (!Array.isArray(input)) return String(input || '');
|
|
511
|
+
return input
|
|
512
|
+
.filter(block => block?.type === 'text')
|
|
513
|
+
.map(block => block.text || '')
|
|
514
|
+
.join('\n');
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function formatJson(value) {
|
|
518
|
+
if (value === undefined) return '';
|
|
519
|
+
try {
|
|
520
|
+
return JSON.stringify(value, null, 2);
|
|
521
|
+
} catch {
|
|
522
|
+
return String(value);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function formatToolResult(content) {
|
|
527
|
+
if (typeof content === 'string') return content;
|
|
528
|
+
if (Array.isArray(content)) {
|
|
529
|
+
return content.map(item => {
|
|
530
|
+
if (typeof item === 'string') return item;
|
|
531
|
+
if (item?.type === 'text') return item.text || '';
|
|
532
|
+
return formatJson(item);
|
|
533
|
+
}).filter(Boolean).join('\n');
|
|
534
|
+
}
|
|
535
|
+
return formatJson(content);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function sanitizeToolInput(toolName, input) {
|
|
539
|
+
if (toolName !== 'Read' || !input || typeof input !== 'object' || Array.isArray(input)) return input;
|
|
540
|
+
if (input.pages !== '') return input;
|
|
541
|
+
|
|
542
|
+
const sanitized = { ...input };
|
|
543
|
+
delete sanitized.pages;
|
|
544
|
+
return sanitized;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function summarizeInput(input) {
|
|
548
|
+
try {
|
|
549
|
+
const text = JSON.stringify(input, null, 2);
|
|
550
|
+
return text.length > 1200 ? text.slice(0, 1200) + '\n...' : text;
|
|
551
|
+
} catch {
|
|
552
|
+
return String(input);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
module.exports = {
|
|
557
|
+
buildClaudePayload,
|
|
558
|
+
execute: executeClaudeQuery,
|
|
559
|
+
executeClaudeQuery,
|
|
560
|
+
flushAssistantText,
|
|
561
|
+
resolvePendingDanger,
|
|
562
|
+
resolvePendingPermission,
|
|
563
|
+
stop: stopAgentProcess,
|
|
564
|
+
};
|