linco-connect 1.1.0 → 1.1.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linco-connect",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "自研 IM 桥接多 Agent 服务",
5
5
  "main": "server.js",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  const { spawn } = require('child_process');
2
2
  const { isDangerousCommand } = require('../danger');
3
- const { send, sendError, sendSystem } = require('../protocol');
3
+ const { send, sendError, sendSystem, sendTurnEnd } = 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');
@@ -603,6 +603,7 @@ function handleAppServerMessage(message, session) {
603
603
  } else {
604
604
  sendSystem(ws, 'Codex 本次执行没有输出。');
605
605
  }
606
+ sendTurnEnd(ws, session, 'timeout');
606
607
  const cfg = session._lastConfig;
607
608
  if (cfg) drainQueue(ws, session, cfg);
608
609
  }
@@ -622,6 +623,7 @@ function handleAppServerMessage(message, session) {
622
623
  } else {
623
624
  sendSystem(ws, 'Codex 本次执行没有输出。');
624
625
  }
626
+ sendTurnEnd(ws, session);
625
627
  const cfg = session._lastConfig;
626
628
  if (cfg) {
627
629
  drainQueue(ws, session, cfg);
@@ -631,6 +633,7 @@ function handleAppServerMessage(message, session) {
631
633
 
632
634
  if (method === 'error' || method.includes('error')) {
633
635
  sendError(ws, params.message || JSON.stringify(params));
636
+ sendTurnEnd(ws, session, 'error', { error: params.message || JSON.stringify(params) });
634
637
  clearTurnState(session);
635
638
  return;
636
639
  }
@@ -861,6 +864,7 @@ function runExecTurn(input, ws, session, config) {
861
864
  if (code === 0) {
862
865
  updateCodexSessionStats(session);
863
866
  }
867
+ sendTurnEnd(ws, session, code === 0 ? 'completed' : 'error', code === 0 ? {} : { error: stderr.trim() || `Codex 退出,状态码: ${code}` });
864
868
  drainQueue(ws, session, config);
865
869
  });
866
870
 
@@ -1,5 +1,5 @@
1
1
  const { isDangerousCommand } = require('../danger');
2
- const { send, sendError, sendSystem } = require('../protocol');
2
+ const { send, sendError, sendSystem, sendTurnEnd } = require('../protocol');
3
3
  const { persistAgentSessionId, stopAgentProcess: stopSessionProcess, updateAgentSessionHistory, createAgentSessionEntry, saveSessionMetadata } = require('../session');
4
4
  const { getOutboxDir } = require('../outgoingAttachmentHandler');
5
5
  const { createTextStreamBuffer, appendTextStream, flushTextStream, resetTextStream } = require('../streamBuffer');
@@ -177,10 +177,12 @@ function handleHermesEvent(event, ws, session, config) {
177
177
  return;
178
178
  case 'run.failed':
179
179
  sendError(ws, event.error || 'Hermes run 执行失败');
180
+ sendTurnEnd(ws, session, 'error', { error: event.error || 'Hermes run 执行失败' });
180
181
  finishTurn(ws, session, config);
181
182
  return;
182
183
  case 'run.cancelled':
183
184
  sendSystem(ws, 'Hermes run 已停止。');
185
+ sendTurnEnd(ws, session, 'cancelled');
184
186
  finishTurn(ws, session, config);
185
187
  return;
186
188
  default:
@@ -240,6 +242,7 @@ function completeRun(event, ws, session, config) {
240
242
  sendSystem(ws, 'Hermes 本次执行没有输出。');
241
243
  }
242
244
  updateHermesSessionStats(session, event.usage);
245
+ sendTurnEnd(ws, session);
243
246
  finishTurn(ws, session, config);
244
247
  }
245
248
 
@@ -1,7 +1,7 @@
1
1
  const crypto = require('crypto');
2
2
  const path = require('path');
3
3
  const { isDangerousCommand } = require('../danger');
4
- const { send, sendError, sendSystem } = require('../protocol');
4
+ const { send, sendError, sendSystem, sendTurnEnd } = require('../protocol');
5
5
  const {
6
6
  createAgentSessionEntry,
7
7
  persistAgentSessionId,
@@ -211,12 +211,14 @@ function handleChatEvent(payload, ws, session, config) {
211
211
  flushOpenClawAssistantText(ws, session);
212
212
  if (session.streamState?.assistantStarted) send(ws, 'assistant_end', {});
213
213
  sendSystem(ws, 'OpenClaw run stopped.');
214
+ sendTurnEnd(ws, session, 'cancelled');
214
215
  finishTurn(ws, session, config, { drain: false });
215
216
  return;
216
217
  }
217
218
 
218
219
  if (payload.state === 'error') {
219
220
  sendError(ws, payload.errorMessage || 'OpenClaw run failed.');
221
+ sendTurnEnd(ws, session, 'error', { error: payload.errorMessage || 'OpenClaw run failed.' });
220
222
  finishTurn(ws, session, config);
221
223
  }
222
224
  }
@@ -227,6 +229,7 @@ function completeRun(payload, ws, session, config) {
227
229
  if (hadOutput) send(ws, 'assistant_end', {});
228
230
  else sendSystem(ws, 'OpenClaw returned no output.');
229
231
  updateOpenClawSessionStats(session, payload.usage);
232
+ sendTurnEnd(ws, session);
230
233
  finishTurn(ws, session, config);
231
234
  }
232
235
 
@@ -2,7 +2,7 @@ const crypto = require('crypto');
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
4
  const { ensureDir } = require('./config');
5
- const { sendError, sendSystem } = require('./protocol');
5
+ const { sendError, sendSystem, sendTurnEnd } = require('./protocol');
6
6
 
7
7
  const RESERVED_WINDOWS_NAMES = new Set(['CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9']);
8
8
  const SUPPORTED_IMAGE_MEDIA_TYPES = new Set(['image/png', 'image/jpeg', 'image/gif', 'image/webp']);
@@ -25,6 +25,7 @@ function handleMessageWithAttachments(msg, ws, session, config, executeAgentQuer
25
25
  savedAttachments = saveAttachments(session, attachments, config);
26
26
  } catch (err) {
27
27
  sendError(ws, `❌ 附件处理失败: ${err.message}`);
28
+ sendTurnEnd(ws, session, 'error', { error: err.message });
28
29
  return;
29
30
  }
30
31
 
@@ -3,7 +3,7 @@ const fs = require('fs');
3
3
  const path = require('path');
4
4
  const { isDangerousCommand } = require('./danger');
5
5
  const { buildOutboxSystemPrompt, getOutboxDir } = require('./outgoingAttachmentHandler');
6
- const { send, sendError, sendSystem } = require('./protocol');
6
+ const { send, sendError, sendSystem, sendTurnEnd } = require('./protocol');
7
7
  const { appendTextStream, flushTextStream, resetTextStream } = require('./streamBuffer');
8
8
  const { persistClaudeSessionId, stopAgentProcess, updateAgentSessionHistory } = require('./session');
9
9
  const {
@@ -193,7 +193,9 @@ function ensureClaudeProcess(ws, session, config) {
193
193
  if (session.isTurnActive) {
194
194
  session.isTurnActive = false;
195
195
  session.currentInputForNoOutput = null;
196
- sendError(ws, code === 0 || code === null ? '⚠️ Claude 会话已结束。' : `❌ Claude 进程退出,退出码: ${code}`);
196
+ const message = code === 0 || code === null ? '⚠️ Claude 会话已结束。' : `❌ Claude 进程退出,退出码: ${code}`;
197
+ sendError(ws, message);
198
+ sendTurnEnd(ws, session, 'error', { error: message });
197
199
  drainMessageQueue(ws, session, config);
198
200
  }
199
201
  });
@@ -205,7 +207,9 @@ function ensureClaudeProcess(ws, session, config) {
205
207
  session.isTurnActive = false;
206
208
  session.currentInputForNoOutput = null;
207
209
  flushAssistantText(ws, session);
208
- sendError(ws, `❌ 无法启动 Claude: ${err.message}\n请确认已安装 Claude Code 并设置好 API 密钥。`);
210
+ const message = `❌ 无法启动 Claude: ${err.message}\n请确认已安装 Claude Code 并设置好 API 密钥。`;
211
+ sendError(ws, message);
212
+ sendTurnEnd(ws, session, 'error', { error: message });
209
213
  });
210
214
 
211
215
  return child;
@@ -328,6 +332,7 @@ function handleClaudeMessage(parsed, ws, session, config) {
328
332
  usage: session.usage,
329
333
  messageCount: session.messageCount,
330
334
  });
335
+ sendTurnEnd(ws, session);
331
336
  session.isTurnActive = false;
332
337
  session.currentInputForNoOutput = null;
333
338
  drainMessageQueue(ws, session, config);
@@ -132,6 +132,17 @@ function mapLocalEventToLinco(event, session, config, linco) {
132
132
  };
133
133
  case 'outgoing_attachment':
134
134
  return mapOutgoingAttachment(event, base, session);
135
+ case 'turn_end':
136
+ return {
137
+ ...base,
138
+ ...event,
139
+ type: 'turn_end',
140
+ requestId: event.requestId || event.request_id || base.messageId,
141
+ streamId: event.streamId || event.stream_id || linco.streamId || base.streamId,
142
+ sessionKey: event.sessionKey || event.session_key || session.id,
143
+ reason: event.reason || 'completed',
144
+ ts: event.ts || Date.now(),
145
+ };
135
146
  default:
136
147
  return {
137
148
  ...base,
package/src/protocol.js CHANGED
@@ -10,8 +10,39 @@ function sendError(ws, text) {
10
10
  send(ws, 'error', { text });
11
11
  }
12
12
 
13
+ function buildTurnEndPayload(session, reason = 'completed', payload = {}) {
14
+ const linco = session?.linco || {};
15
+ return {
16
+ requestId: payload.requestId || payload.request_id || linco.messageId,
17
+ streamId: payload.streamId || payload.stream_id || linco.streamId,
18
+ sessionKey: payload.sessionKey || payload.session_key || session?.id,
19
+ reason,
20
+ ts: Date.now(),
21
+ ...payload,
22
+ };
23
+ }
24
+
25
+ function turnEndKey(payload) {
26
+ return [
27
+ payload.requestId || '',
28
+ payload.streamId || '',
29
+ payload.sessionKey || '',
30
+ ].join('|');
31
+ }
32
+
33
+ function sendTurnEnd(ws, session, reason = 'completed', payload = {}) {
34
+ const turnEndPayload = buildTurnEndPayload(session, reason, payload);
35
+ const key = turnEndKey(turnEndPayload);
36
+ if (session && session.lastTurnEndKey === key) return false;
37
+ if (session) session.lastTurnEndKey = key;
38
+ send(ws, 'turn_end', turnEndPayload);
39
+ return true;
40
+ }
41
+
13
42
  module.exports = {
14
43
  send,
15
44
  sendError,
16
45
  sendSystem,
46
+ sendTurnEnd,
47
+ buildTurnEndPayload,
17
48
  };
@@ -1,7 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { resetOutgoingAttachments, startOutboxWatcher, stopOutboxWatcher } = require('./outgoingAttachmentHandler');
4
- const { sendError, sendSystem } = require('./protocol');
4
+ const { sendError, sendSystem, sendTurnEnd } = require('./protocol');
5
5
  const { removeAgentSessionFromHistory, saveSessionMetadata } = require('./session');
6
6
  const { resolvePendingDanger, resolvePendingPermission, stopAgentProcess } = require('./agentRunner');
7
7
  const { clearPendingPermissions, pendingPermissionIds } = require('./permissionState');
@@ -35,7 +35,7 @@ function handleSlashCommand(text, ws, session, config) {
35
35
  📎 支持附件:默认允许普通文件(如 csv、xlsx、sql、txt、md、pdf、docx 等),高风险可执行/脚本文件默认拦截
36
36
  📤 Agent 生成文件后可放入当前会话 outbox 目录自动发送到对话框
37
37
  🔁 其他 /xxx 命令会透传给当前 Agent,优先使用当前 Agent 原生斜杠命令能力`);
38
- return true;
38
+ return completeLocalCommand(ws, session);
39
39
 
40
40
  case '/commands':
41
41
  sendSystem(ws, `${localCommandsHelp()}
@@ -44,38 +44,38 @@ function handleSlashCommand(text, ws, session, config) {
44
44
  除上述本地命令外,其他 /xxx 会直接发送给当前 Agent,例如 /model、/status、/memory、/compact 等。
45
45
 
46
46
  ⚠️ 注意:部分 Agent 原生命令只在交互式 CLI/TUI 中更新界面;在 Linco 的 stream-json 模式下可能返回“无输出”或提示当前环境不可用。遇到这种情况不是连接失败,而是该命令不适合当前桥接模式。`);
47
- return true;
47
+ return completeLocalCommand(ws, session);
48
48
 
49
49
  case '/status':
50
50
  sendStatus(ws, session);
51
- return true;
51
+ return completeLocalCommand(ws, session);
52
52
 
53
53
  case '/pwd':
54
54
  if (isHermesSession(session)) {
55
55
  sendHermesWorkspaceNotice(ws);
56
- return true;
56
+ return completeLocalCommand(ws, session);
57
57
  }
58
58
  sendSystem(ws, `📂 ${session.workspace}`);
59
- return true;
59
+ return completeLocalCommand(ws, session);
60
60
 
61
61
  case '/cd':
62
62
  if (isHermesSession(session)) {
63
63
  sendHermesWorkspaceNotice(ws);
64
- return true;
64
+ return completeLocalCommand(ws, session);
65
65
  }
66
66
  handleCd(parts[1], ws, session, config);
67
- return true;
67
+ return completeLocalCommand(ws, session);
68
68
 
69
69
  case '/new':
70
70
  stopAgentProcess(session, { clearAgentSession: true });
71
71
  resetOutgoingAttachments(session, config);
72
72
  sendSystem(ws, '🆕 已开启新会话,之前上下文已清除。');
73
- return true;
73
+ return completeLocalCommand(ws, session);
74
74
 
75
75
  case '/stop':
76
76
  stopAgentProcess(session, { clearAgentSession: false });
77
77
  sendSystem(ws, '⏹️ 已停止当前 Agent 进程,下次消息会尝试恢复当前会话。');
78
- return true;
78
+ return completeLocalCommand(ws, session);
79
79
 
80
80
  case '/base':
81
81
  sendSystem(ws, `🗄️ Linco 运行信息:
@@ -84,33 +84,38 @@ Linco Home: ${config.lincoHome}
84
84
  会话运行目录: ${session.runtimeDir}
85
85
  附件目录: ${session.attachmentsDir}
86
86
  outbox 目录: ${session.outboxDir}`);
87
- return true;
87
+ return completeLocalCommand(ws, session);
88
88
 
89
89
  case '/list':
90
90
  handleList(parts[1], ws, session);
91
- return true;
91
+ return completeLocalCommand(ws, session);
92
92
 
93
93
  case '/switch':
94
94
  handleSwitch(parts[1], ws, session, config);
95
- return true;
95
+ return completeLocalCommand(ws, session);
96
96
 
97
97
  case '/delete':
98
98
  handleDelete(parts[1], ws, session);
99
- return true;
99
+ return completeLocalCommand(ws, session);
100
100
 
101
101
  case '/approve':
102
102
  handleApprove(parts[1], ws, session, config);
103
- return true;
103
+ return completeLocalCommand(ws, session);
104
104
 
105
105
  case '/usage':
106
106
  handleUsage(ws, session);
107
- return true;
107
+ return completeLocalCommand(ws, session);
108
108
 
109
109
  default:
110
110
  return false;
111
111
  }
112
112
  }
113
113
 
114
+ function completeLocalCommand(ws, session) {
115
+ sendTurnEnd(ws, session);
116
+ return true;
117
+ }
118
+
114
119
  function sendStatus(ws, session) {
115
120
  const processRunning = !!(
116
121
  (session.agentProcess || session.claudeProcess) &&