@yeaft/webchat-agent 1.0.414 → 1.0.415
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/connection/index.js +12 -0
- package/index.js +1 -1
- package/local-runtime/server/client-protocol.js +14 -0
- package/local-runtime/server/context.js +3 -2
- package/local-runtime/server/handlers/agent-file-terminal.js +185 -115
- package/local-runtime/server/handlers/agent-output.js +3 -0
- package/local-runtime/server/handlers/client-misc.js +21 -4
- package/local-runtime/server/handlers/client-workbench.js +222 -41
- package/local-runtime/server/workbench-correlation.js +184 -0
- package/local-runtime/server/workbench-route.js +180 -0
- package/local-runtime/server/ws-agent.js +4 -0
- package/local-runtime/server/ws-client.js +25 -3
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +191 -135
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +2 -2
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/package.json +1 -1
- package/terminal.js +167 -30
- package/workbench/file-ops.js +21 -20
- package/workbench/file-search.js +4 -3
- package/workbench/git-ops.js +23 -22
- package/workbench/request-routing.js +16 -0
- package/yeaft/cli.js +57 -1
- package/yeaft/sessions/session-manifest.js +114 -10
- package/yeaft/stdio-protocol.js +57 -0
package/connection/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import ctx from '../context.js';
|
|
|
3
3
|
import { sendToServer, parseMessage } from './buffer.js';
|
|
4
4
|
import { startAgentHeartbeat, stopAgentHeartbeat, scheduleReconnect } from './heartbeat.js';
|
|
5
5
|
import { handleMessage } from './message-router.js';
|
|
6
|
+
import { cleanupTerminalsForDisconnect } from '../terminal.js';
|
|
6
7
|
|
|
7
8
|
export function resetConnectionTransport() {
|
|
8
9
|
ctx.sessionKey = null;
|
|
@@ -35,6 +36,7 @@ export function connect(WebSocketImpl = WebSocket) {
|
|
|
35
36
|
console.log(`Disallowed tools: ${ctx.CONFIG.disallowedTools.join(', ')}`);
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
const previousSocket = ctx.ws;
|
|
38
40
|
const socket = new WebSocketImpl(url, {
|
|
39
41
|
// Match server's permessage-deflate config (bounded memory,
|
|
40
42
|
// skip compression for small frames). The `ws` library handles
|
|
@@ -46,6 +48,12 @@ export function connect(WebSocketImpl = WebSocket) {
|
|
|
46
48
|
threshold: 1024
|
|
47
49
|
}
|
|
48
50
|
});
|
|
51
|
+
if (previousSocket && previousSocket !== socket) {
|
|
52
|
+
const closedTerminals = cleanupTerminalsForDisconnect();
|
|
53
|
+
if (closedTerminals > 0) {
|
|
54
|
+
console.log(`[PTY] Closed ${closedTerminals} terminal(s) before Agent transport replacement`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
49
57
|
ctx.ws = socket;
|
|
50
58
|
|
|
51
59
|
socket.on('open', () => {
|
|
@@ -99,6 +107,10 @@ export function connect(WebSocketImpl = WebSocket) {
|
|
|
99
107
|
ctx.sessionKey = null;
|
|
100
108
|
ctx.pendingAuthTempId = null;
|
|
101
109
|
stopAgentHeartbeat();
|
|
110
|
+
const closedTerminals = cleanupTerminalsForDisconnect();
|
|
111
|
+
if (closedTerminals > 0) {
|
|
112
|
+
console.log(`[PTY] Closed ${closedTerminals} terminal(s) after Agent transport disconnect`);
|
|
113
|
+
}
|
|
102
114
|
|
|
103
115
|
if (code === 1008) {
|
|
104
116
|
console.error('Authentication failed. Check AGENT_SECRET configuration.');
|
package/index.js
CHANGED
|
@@ -160,7 +160,7 @@ async function detectCapabilities() {
|
|
|
160
160
|
// agent build can speak plaintext WS frames. New servers see this and
|
|
161
161
|
// flip `agent.encryptOutbound = false`, stopping outbound encryption
|
|
162
162
|
// to this peer. Old servers ignore the unknown capability token.
|
|
163
|
-
const capabilities = ['background_tasks', 'file_editor', 'ping_session', 'plaintext-ok', SAFE_REMOTE_UPGRADE_CAPABILITY, 'work_center', 'work_center_message_v2', 'session_history_search', 'session_history_outline', 'session_history_window_prefetch'];
|
|
163
|
+
const capabilities = ['background_tasks', 'file_editor', 'ping_session', 'plaintext-ok', 'workbench_session_routes', SAFE_REMOTE_UPGRADE_CAPABILITY, 'work_center', 'work_center_message_v2', 'session_history_search', 'session_history_outline', 'session_history_window_prefetch'];
|
|
164
164
|
if (process.platform === 'linux') capabilities.push('work_item_attachments');
|
|
165
165
|
const pty = await loadNodePty();
|
|
166
166
|
if (pty) capabilities.push('terminal');
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const WORKBENCH_ROUTE_PROTOCOL = 1;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Apply the explicit browser protocol hello to one Server-owned client record.
|
|
5
|
+
* Unknown or omitted fields leave legacy defaults unchanged.
|
|
6
|
+
*/
|
|
7
|
+
export function applyClientHello(client, message) {
|
|
8
|
+
if (!client || message?.type !== 'client_hello') return false;
|
|
9
|
+
if (message.plaintextOk === true) client.encryptOutbound = false;
|
|
10
|
+
if (message.workbenchRouteProtocol === WORKBENCH_ROUTE_PROTOCOL) {
|
|
11
|
+
client.workbenchRouteProtocol = WORKBENCH_ROUTE_PROTOCOL;
|
|
12
|
+
}
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
@@ -29,8 +29,9 @@ export const directoryCache = new Map();
|
|
|
29
29
|
export const DIR_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
30
30
|
export const DIR_CACHE_MAX_SIZE = 500;
|
|
31
31
|
|
|
32
|
-
//
|
|
33
|
-
//
|
|
32
|
+
// Workbench Files tab state. Route-aware writers use
|
|
33
|
+
// `${userId}:${routeKey}\0${workspaceGeneration}`; legacy pairs keep the
|
|
34
|
+
// historical `${userId}:${agentId}` key.
|
|
34
35
|
export const userFileTabs = new Map();
|
|
35
36
|
|
|
36
37
|
// Preview file cache for binary file preview (Office/PDF/Image)
|
|
@@ -1,130 +1,200 @@
|
|
|
1
1
|
import { randomUUID } from 'crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { CONFIG } from '../config.js';
|
|
3
|
+
import { agents, previewFiles, webClients } from '../context.js';
|
|
3
4
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
sendToAgent,
|
|
6
|
+
sendToWebClient,
|
|
7
|
+
setCachedDir,
|
|
8
|
+
invalidateParentDirCache,
|
|
9
|
+
clearAgentDirCache,
|
|
6
10
|
} from '../ws-utils.js';
|
|
11
|
+
import {
|
|
12
|
+
currentWorkbenchWorkspaceGeneration,
|
|
13
|
+
workbenchRouteKeyFromConversationId,
|
|
14
|
+
} from '../workbench-route.js';
|
|
15
|
+
import {
|
|
16
|
+
consumeWorkbenchRequest,
|
|
17
|
+
deleteWorkbenchTerminalOwner,
|
|
18
|
+
getWorkbenchTerminalOwner,
|
|
19
|
+
registerWorkbenchTerminalOwner,
|
|
20
|
+
} from '../workbench-correlation.js';
|
|
7
21
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
case 'terminal_closed':
|
|
20
|
-
case 'terminal_error': {
|
|
21
|
-
const targetClient = msg._requestClientId ? webClients.get(msg._requestClientId) : null;
|
|
22
|
-
const targetMatchesUser = !msg._requestUserId || targetClient?.userId === msg._requestUserId;
|
|
23
|
-
if (targetClient?.authenticated && targetMatchesUser) {
|
|
24
|
-
const { _requestClientId, _requestUserId, ...cleanMsg } = msg;
|
|
25
|
-
await sendToWebClient(targetClient, cleanMsg);
|
|
26
|
-
break;
|
|
27
|
-
}
|
|
28
|
-
const { _requestClientId, ...fallbackMsg } = msg;
|
|
29
|
-
await forwardToClients(agentId, msg.conversationId, fallbackMsg);
|
|
30
|
-
break;
|
|
31
|
-
}
|
|
22
|
+
function stripAgentRouting(msg) {
|
|
23
|
+
const {
|
|
24
|
+
_requestClientId: _ignoredClientId,
|
|
25
|
+
_requestUserId: _ignoredUserId,
|
|
26
|
+
_workbenchRequestId: _ignoredRequestId,
|
|
27
|
+
workbenchRouteKey: _ignoredRouteKey,
|
|
28
|
+
workbenchWorkspaceGeneration: _ignoredGeneration,
|
|
29
|
+
...visible
|
|
30
|
+
} = msg || {};
|
|
31
|
+
return visible;
|
|
32
|
+
}
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
34
|
+
function pendingResponse(agentId, msg, pending) {
|
|
35
|
+
const { requestId: _agentRequestId, ...visible } = stripAgentRouting(msg);
|
|
36
|
+
return {
|
|
37
|
+
...visible,
|
|
38
|
+
agentId,
|
|
39
|
+
conversationId: pending.conversationId,
|
|
40
|
+
...(pending.publicRequestId ? { requestId: pending.publicRequestId } : {}),
|
|
41
|
+
workbenchRouteKey: pending.routeKey,
|
|
42
|
+
workbenchWorkspaceGeneration: pending.workspaceGeneration,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function sendToPendingClient(agentId, msg, pending) {
|
|
47
|
+
const client = webClients.get(pending?.clientId);
|
|
48
|
+
if (!client?.authenticated || client.userId !== pending?.userId) return false;
|
|
49
|
+
await sendToWebClient(client, pendingResponse(agentId, msg, pending));
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function forwardLegacyResponse(agentId, msg) {
|
|
54
|
+
const visible = { ...stripAgentRouting(msg), agentId };
|
|
55
|
+
const agent = agents.get(agentId);
|
|
56
|
+
const conversation = agent?.conversations?.get?.(visible.conversationId);
|
|
57
|
+
const ownerId = conversation?.userId || agent?.ownerId || null;
|
|
58
|
+
for (const [, client] of webClients) {
|
|
59
|
+
if (!client?.authenticated) continue;
|
|
60
|
+
const allowed = CONFIG.skipAuth
|
|
61
|
+
|| (ownerId ? client.userId === ownerId : client.role === 'admin');
|
|
62
|
+
if (allowed) await sendToWebClient(client, visible);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function cacheBinaryPreview(msg) {
|
|
67
|
+
const fileId = randomUUID();
|
|
68
|
+
const token = randomUUID();
|
|
69
|
+
const filename = msg.filePath.split('/').pop() || 'file';
|
|
70
|
+
previewFiles.set(fileId, {
|
|
71
|
+
buffer: Buffer.from(msg.content, 'base64'),
|
|
72
|
+
mimeType: msg.mimeType,
|
|
73
|
+
filename,
|
|
74
|
+
createdAt: Date.now(),
|
|
75
|
+
token,
|
|
76
|
+
});
|
|
77
|
+
const { content: _binaryContent, ...projected } = msg;
|
|
78
|
+
return {
|
|
79
|
+
...projected,
|
|
80
|
+
binary: true,
|
|
81
|
+
fileId,
|
|
82
|
+
previewToken: token,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function handleTerminalResponse(agentId, msg, routeKey) {
|
|
87
|
+
const terminalId = msg.terminalId || null;
|
|
88
|
+
if (msg.type === 'terminal_created') {
|
|
89
|
+
const pending = consumeWorkbenchRequest({
|
|
90
|
+
agentId,
|
|
91
|
+
requestId: msg._workbenchRequestId,
|
|
92
|
+
responseType: msg.type,
|
|
93
|
+
routeKey,
|
|
94
|
+
});
|
|
95
|
+
if (!pending) {
|
|
96
|
+
const agentRecord = agents.get(agentId);
|
|
97
|
+
if (agentRecord && terminalId && msg.workbenchWorkspaceGeneration) {
|
|
98
|
+
await sendToAgent(agentRecord, {
|
|
99
|
+
type: 'terminal_close',
|
|
52
100
|
conversationId: msg.conversationId,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
requestedFilePath: msg.requestedFilePath,
|
|
58
|
-
binary: true,
|
|
59
|
-
fileId,
|
|
60
|
-
previewToken: token,
|
|
61
|
-
mimeType: msg.mimeType
|
|
62
|
-
};
|
|
63
|
-
} else {
|
|
64
|
-
console.log(`[Server] Forwarding file_content to clients, conv=${msg.conversationId}, path=${msg.filePath}`);
|
|
65
|
-
}
|
|
66
|
-
const targetClient = fwdMsg._requestClientId ? webClients.get(fwdMsg._requestClientId) : null;
|
|
67
|
-
const targetMatchesUser = !fwdMsg._requestUserId || targetClient?.userId === fwdMsg._requestUserId;
|
|
68
|
-
if (targetClient?.authenticated && targetMatchesUser) {
|
|
69
|
-
const { _requestClientId, _requestUserId, ...cleanMsg } = fwdMsg;
|
|
70
|
-
await sendToWebClient(targetClient, cleanMsg);
|
|
71
|
-
} else {
|
|
72
|
-
const { _requestClientId, ...fallbackMsg } = fwdMsg;
|
|
73
|
-
await forwardToClients(agentId, msg.conversationId, fallbackMsg);
|
|
101
|
+
terminalId,
|
|
102
|
+
workbenchRouteKey: routeKey,
|
|
103
|
+
workbenchWorkspaceGeneration: msg.workbenchWorkspaceGeneration,
|
|
104
|
+
});
|
|
74
105
|
}
|
|
75
|
-
|
|
106
|
+
return;
|
|
76
107
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
invalidateParentDirCache(agentId, msg.filePath);
|
|
81
|
-
const fwdMsg = { ...msg, agentId };
|
|
82
|
-
const targetClient = msg._requestClientId ? webClients.get(msg._requestClientId) : null;
|
|
83
|
-
const targetMatchesUser = !msg._requestUserId || targetClient?.userId === msg._requestUserId;
|
|
84
|
-
if (targetClient?.authenticated && targetMatchesUser) {
|
|
85
|
-
const { _requestClientId, _requestUserId, ...cleanMsg } = fwdMsg;
|
|
86
|
-
await sendToWebClient(targetClient, cleanMsg);
|
|
87
|
-
} else {
|
|
88
|
-
const { _requestClientId, ...fallbackMsg } = fwdMsg;
|
|
89
|
-
await forwardToClients(agentId, msg.conversationId, fallbackMsg);
|
|
90
|
-
}
|
|
91
|
-
break;
|
|
108
|
+
if (pending.routeKey !== routeKey || pending.terminalId !== terminalId) {
|
|
109
|
+
deleteWorkbenchTerminalOwner(agentId, pending.terminalId);
|
|
110
|
+
return;
|
|
92
111
|
}
|
|
112
|
+
if (msg.success !== false) registerWorkbenchTerminalOwner({ ...pending, terminalId });
|
|
113
|
+
else deleteWorkbenchTerminalOwner(agentId, terminalId);
|
|
114
|
+
await sendToPendingClient(agentId, msg, pending);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
93
117
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
118
|
+
// Create errors carry the one-shot create correlation even though a
|
|
119
|
+
// terminal-id reservation already exists. Consume and release it first.
|
|
120
|
+
if (msg.type === 'terminal_error' && msg._workbenchRequestId) {
|
|
121
|
+
const pending = consumeWorkbenchRequest({
|
|
122
|
+
agentId,
|
|
123
|
+
requestId: msg._workbenchRequestId,
|
|
124
|
+
responseType: msg.type,
|
|
125
|
+
routeKey,
|
|
126
|
+
});
|
|
127
|
+
if (pending?.terminalId) deleteWorkbenchTerminalOwner(agentId, pending.terminalId);
|
|
128
|
+
if (pending?.routeKey === routeKey) await sendToPendingClient(agentId, msg, pending);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const owner = terminalId ? getWorkbenchTerminalOwner(agentId, terminalId) : null;
|
|
133
|
+
if (owner) {
|
|
134
|
+
if (owner.routeKey !== routeKey) return;
|
|
135
|
+
await sendToPendingClient(agentId, msg, owner);
|
|
136
|
+
if (msg.type === 'terminal_closed') deleteWorkbenchTerminalOwner(agentId, terminalId);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
112
139
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
140
|
+
async function handleOneShotResponse(agentId, msg, routeKey) {
|
|
141
|
+
const pending = consumeWorkbenchRequest({
|
|
142
|
+
agentId,
|
|
143
|
+
requestId: msg._workbenchRequestId,
|
|
144
|
+
responseType: msg.type,
|
|
145
|
+
routeKey,
|
|
146
|
+
});
|
|
147
|
+
if (!pending) return;
|
|
148
|
+
const currentGeneration = currentWorkbenchWorkspaceGeneration({
|
|
149
|
+
route: pending.route,
|
|
150
|
+
userId: pending.userId,
|
|
151
|
+
role: pending.role,
|
|
152
|
+
});
|
|
153
|
+
if (!currentGeneration || currentGeneration !== pending.workspaceGeneration) return;
|
|
154
|
+
const projected = msg.type === 'file_content' && msg.binary
|
|
155
|
+
? cacheBinaryPreview(msg)
|
|
156
|
+
: msg;
|
|
157
|
+
await sendToPendingClient(agentId, projected, pending);
|
|
158
|
+
}
|
|
118
159
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
160
|
+
/**
|
|
161
|
+
* Handle file, terminal, and git messages from an Agent. Route-scoped replies
|
|
162
|
+
* are delivered only through Server-owned correlations. Agent-supplied client
|
|
163
|
+
* or user ids never select a browser recipient.
|
|
164
|
+
*/
|
|
165
|
+
export async function handleAgentFileTerminal(agentId, agent, rawMsg) {
|
|
166
|
+
const msg = rawMsg || {};
|
|
167
|
+
const routeKey = workbenchRouteKeyFromConversationId(msg.conversationId, agentId);
|
|
168
|
+
const terminalTypes = new Set([
|
|
169
|
+
'terminal_created', 'terminal_output', 'terminal_closed', 'terminal_error',
|
|
170
|
+
]);
|
|
171
|
+
const oneShotTypes = new Set([
|
|
172
|
+
'file_content', 'file_saved', 'directory_listing', 'file_op_result',
|
|
173
|
+
'git_status_result', 'git_diff_result', 'git_op_result', 'file_search_result',
|
|
174
|
+
]);
|
|
175
|
+
if (!terminalTypes.has(msg.type) && !oneShotTypes.has(msg.type)) return false;
|
|
125
176
|
|
|
126
|
-
|
|
127
|
-
|
|
177
|
+
if (msg.type === 'file_saved') invalidateParentDirCache(agentId, msg.filePath);
|
|
178
|
+
if (msg.type === 'file_op_result') clearAgentDirCache(agentId);
|
|
179
|
+
if (msg.type === 'directory_listing' && msg.dirPath && msg.entries && !msg.error) {
|
|
180
|
+
setCachedDir(agentId, msg.dirPath, msg.entries);
|
|
128
181
|
}
|
|
129
|
-
|
|
182
|
+
|
|
183
|
+
if (routeKey) {
|
|
184
|
+
if (terminalTypes.has(msg.type)) await handleTerminalResponse(agentId, msg, routeKey);
|
|
185
|
+
else await handleOneShotResponse(agentId, msg, routeKey);
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// `_workbench:` is reserved for Server-authored route conversations. An
|
|
190
|
+
// invalid or cross-Agent value is not a legacy conversation.
|
|
191
|
+
if (typeof msg.conversationId === 'string' && msg.conversationId.startsWith('_workbench:')) {
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const projected = msg.type === 'file_content' && msg.binary
|
|
196
|
+
? cacheBinaryPreview(msg)
|
|
197
|
+
: msg;
|
|
198
|
+
await forwardLegacyResponse(agentId, projected);
|
|
199
|
+
return true;
|
|
130
200
|
}
|
|
@@ -53,6 +53,9 @@ function syncYeaftSessionMetadata(agentId, agent, event) {
|
|
|
53
53
|
|
|
54
54
|
if (event.type === 'session_list_updated') {
|
|
55
55
|
const rows = Array.isArray(event.sessions) ? event.sessions : [];
|
|
56
|
+
agent.yeaftSessions = new Map(rows
|
|
57
|
+
.filter(session => session?.id)
|
|
58
|
+
.map(session => [session.id, { ...session }]));
|
|
56
59
|
try {
|
|
57
60
|
if (ownerId) {
|
|
58
61
|
reconcileAuthoritativeSessionSnapshot(ownerId, agentId, rows);
|
|
@@ -2,6 +2,7 @@ import { agents, userFileTabs } from '../context.js';
|
|
|
2
2
|
import {
|
|
3
3
|
sendToWebClient, forwardToAgent, broadcastAgentList
|
|
4
4
|
} from '../ws-utils.js';
|
|
5
|
+
import { resolveWorkbenchRequest } from '../workbench-route.js';
|
|
5
6
|
|
|
6
7
|
// Only Agents that explicitly advertise the package-replacement-safe updater
|
|
7
8
|
// may receive remote upgrade commands. Version thresholds are insufficient:
|
|
@@ -73,11 +74,18 @@ export async function handleClientMisc(clientId, client, msg, checkAgentAccess)
|
|
|
73
74
|
|
|
74
75
|
// File Tab 状态保存/恢复
|
|
75
76
|
case 'update_file_tabs': {
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
const ftAgentId = msg.agentId || client.currentAgent;
|
|
78
|
+
if (client.userId && ftAgentId) {
|
|
79
|
+
if (!await checkAgentAccess(ftAgentId)) break;
|
|
80
|
+
const resolved = resolveWorkbenchRequest(client, msg, ftAgentId);
|
|
81
|
+
if (!resolved) break;
|
|
82
|
+
const identity = resolved.routeKey
|
|
83
|
+
? `${resolved.routeKey}\u0000${resolved.workspaceGeneration}`
|
|
84
|
+
: ftAgentId;
|
|
85
|
+
const key = `${client.userId}:${identity}`;
|
|
78
86
|
userFileTabs.set(key, {
|
|
79
87
|
files: (msg.openFiles || []).map(f => ({ path: f.path })),
|
|
80
|
-
activeIndex: msg.activeIndex
|
|
88
|
+
activeIndex: Number.isFinite(msg.activeIndex) ? msg.activeIndex : 0,
|
|
81
89
|
timestamp: Date.now()
|
|
82
90
|
});
|
|
83
91
|
}
|
|
@@ -88,10 +96,19 @@ export async function handleClientMisc(clientId, client, msg, checkAgentAccess)
|
|
|
88
96
|
const ftAgentId = msg.agentId || client.currentAgent;
|
|
89
97
|
if (client.userId && ftAgentId) {
|
|
90
98
|
if (!await checkAgentAccess(ftAgentId)) break;
|
|
91
|
-
const
|
|
99
|
+
const resolved = resolveWorkbenchRequest(client, msg, ftAgentId);
|
|
100
|
+
if (!resolved) break;
|
|
101
|
+
const identity = resolved.routeKey
|
|
102
|
+
? `${resolved.routeKey}\u0000${resolved.workspaceGeneration}`
|
|
103
|
+
: ftAgentId;
|
|
104
|
+
const key = `${client.userId}:${identity}`;
|
|
92
105
|
const saved = userFileTabs.get(key);
|
|
93
106
|
await sendToWebClient(client, {
|
|
94
107
|
type: 'file_tabs_restored',
|
|
108
|
+
agentId: ftAgentId,
|
|
109
|
+
conversationId: resolved.conversationId || msg.conversationId || client.currentConversation,
|
|
110
|
+
workbenchRouteKey: resolved.routeKey,
|
|
111
|
+
workbenchWorkspaceGeneration: resolved.workspaceGeneration,
|
|
95
112
|
openFiles: saved?.files || [],
|
|
96
113
|
activeIndex: saved?.activeIndex || 0
|
|
97
114
|
});
|