@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
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { CONFIG } from './config.js';
|
|
2
|
+
import { agents } from './context.js';
|
|
3
|
+
import { sessionDb } from './db/session-db.js';
|
|
4
|
+
import { yeaftSessionDb } from './db/yeaft-session-db.js';
|
|
5
|
+
|
|
6
|
+
export const WORKBENCH_SESSION_ROUTE_CAPABILITY = 'workbench_session_routes';
|
|
7
|
+
|
|
8
|
+
const PROVIDERS = new Set(['yeaft', 'claude-code', 'copilot']);
|
|
9
|
+
const SCOPES = new Set(['main', 'files-folder-picker', 'git-folder-picker']);
|
|
10
|
+
|
|
11
|
+
function clean(value, maxLength = 300) {
|
|
12
|
+
if (typeof value !== 'string') return '';
|
|
13
|
+
const result = value.trim();
|
|
14
|
+
return result && result.length <= maxLength ? result : '';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function workbenchRouteKey(route) {
|
|
18
|
+
const runtimeProvider = clean(route?.runtimeProvider, 32);
|
|
19
|
+
const agentId = clean(route?.agentId);
|
|
20
|
+
const sessionId = clean(route?.sessionId);
|
|
21
|
+
if (!PROVIDERS.has(runtimeProvider) || !agentId || !sessionId) return '';
|
|
22
|
+
return [runtimeProvider, agentId, sessionId]
|
|
23
|
+
.map(part => encodeURIComponent(part))
|
|
24
|
+
.join(':');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function stableWorkspaceHash(value) {
|
|
28
|
+
let hash = 0xcbf29ce484222325n;
|
|
29
|
+
for (const char of String(value || '')) {
|
|
30
|
+
hash ^= BigInt(char.codePointAt(0));
|
|
31
|
+
hash = BigInt.asUintN(64, hash * 0x100000001b3n);
|
|
32
|
+
}
|
|
33
|
+
return hash.toString(16).padStart(16, '0');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function workbenchWorkspaceGeneration(routeKey, workDir) {
|
|
37
|
+
const normalizedRouteKey = clean(routeKey, 1200);
|
|
38
|
+
const normalizedWorkDir = clean(workDir, 4096);
|
|
39
|
+
if (!normalizedRouteKey || !normalizedWorkDir) return '';
|
|
40
|
+
return `${normalizedRouteKey}@${stableWorkspaceHash(normalizedWorkDir)}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function workbenchConversationId(route, scope = 'main') {
|
|
44
|
+
const routeKey = workbenchRouteKey(route);
|
|
45
|
+
if (!routeKey) return '';
|
|
46
|
+
const normalizedScope = SCOPES.has(scope) ? scope : 'main';
|
|
47
|
+
return normalizedScope === 'main'
|
|
48
|
+
? `_workbench:${routeKey}`
|
|
49
|
+
: `_workbench:${routeKey}:${normalizedScope}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function workbenchRouteKeyFromConversationId(conversationId, expectedAgentId = '') {
|
|
53
|
+
const raw = clean(conversationId, 4096);
|
|
54
|
+
if (!raw.startsWith('_workbench:')) return '';
|
|
55
|
+
const parts = raw.slice('_workbench:'.length).split(':');
|
|
56
|
+
if (parts.length < 3) return '';
|
|
57
|
+
const routeKey = parts.slice(0, 3).join(':');
|
|
58
|
+
try {
|
|
59
|
+
const decodedRoute = {
|
|
60
|
+
runtimeProvider: decodeURIComponent(parts[0]),
|
|
61
|
+
agentId: decodeURIComponent(parts[1]),
|
|
62
|
+
sessionId: decodeURIComponent(parts[2]),
|
|
63
|
+
};
|
|
64
|
+
if (expectedAgentId && decodedRoute.agentId !== expectedAgentId) return '';
|
|
65
|
+
return workbenchRouteKey(decodedRoute) === routeKey ? routeKey : '';
|
|
66
|
+
} catch {
|
|
67
|
+
return '';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function resolveYeaftRow(client, route) {
|
|
72
|
+
if (client?.userId) {
|
|
73
|
+
const owned = yeaftSessionDb.getForAgent(client.userId, route.agentId, route.sessionId);
|
|
74
|
+
if (owned) return owned;
|
|
75
|
+
}
|
|
76
|
+
const agent = agents.get(route.agentId);
|
|
77
|
+
if (!CONFIG.skipAuth) {
|
|
78
|
+
if (client?.role !== 'admin' || agent?.ownerId) return null;
|
|
79
|
+
return agent?.yeaftSessions?.get(route.sessionId) || null;
|
|
80
|
+
}
|
|
81
|
+
return agent?.yeaftSessions?.get(route.sessionId)
|
|
82
|
+
|| yeaftSessionDb.getByAgent(route.agentId).find(row => row?.id === route.sessionId)
|
|
83
|
+
|| null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function resolveChatRow(client, route) {
|
|
87
|
+
const row = sessionDb.get(route.sessionId);
|
|
88
|
+
if (!row || row.agent_id !== route.agentId) return null;
|
|
89
|
+
const provider = row.provider === 'copilot' ? 'copilot' : 'claude-code';
|
|
90
|
+
if (provider !== route.runtimeProvider) return null;
|
|
91
|
+
if (!CONFIG.skipAuth) {
|
|
92
|
+
if (!client?.userId) return null;
|
|
93
|
+
if (row.user_id) {
|
|
94
|
+
if (row.user_id !== client.userId) return null;
|
|
95
|
+
} else if (client.role !== 'admin' && agents.get(route.agentId)?.ownerId !== client.userId) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return row;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Validate a browser-provided Workbench route against Server-owned Session
|
|
104
|
+
* metadata and return canonical execution fields. Browser cwd and synthetic
|
|
105
|
+
* conversation ids are never authoritative.
|
|
106
|
+
*
|
|
107
|
+
* `legacy: true` preserves old clients that predate route-scoped Workbench.
|
|
108
|
+
*/
|
|
109
|
+
export function currentWorkbenchWorkspaceGeneration({ route, userId, role }) {
|
|
110
|
+
if (!route || !userId) return '';
|
|
111
|
+
const client = { userId, role };
|
|
112
|
+
const row = route.runtimeProvider === 'yeaft'
|
|
113
|
+
? resolveYeaftRow(client, route)
|
|
114
|
+
: resolveChatRow(client, route);
|
|
115
|
+
if (!row || row.isArchived) return '';
|
|
116
|
+
const workDir = clean(route.runtimeProvider === 'yeaft' ? row.workDir : row.work_dir, 4096);
|
|
117
|
+
return workbenchWorkspaceGeneration(workbenchRouteKey(route), workDir);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function resolveWorkbenchRequest(client, msg, targetAgentId, { allowMissingSession = false } = {}) {
|
|
121
|
+
const agent = agents.get(targetAgentId);
|
|
122
|
+
const agentSupportsRoutes = Array.isArray(agent?.capabilities)
|
|
123
|
+
&& agent.capabilities.includes(WORKBENCH_SESSION_ROUTE_CAPABILITY);
|
|
124
|
+
const clientSupportsRoutes = client?.workbenchRouteProtocol === 1;
|
|
125
|
+
|
|
126
|
+
if (!msg?.workbenchRoute) {
|
|
127
|
+
// Legacy is a negotiated pairing, not a caller-selected downgrade. Once
|
|
128
|
+
// either side speaks the route protocol, route-less Workbench is invalid.
|
|
129
|
+
if (clientSupportsRoutes || agentSupportsRoutes) return null;
|
|
130
|
+
return {
|
|
131
|
+
legacy: true,
|
|
132
|
+
agentId: targetAgentId,
|
|
133
|
+
conversationId: clean(msg?.conversationId) || null,
|
|
134
|
+
workDir: clean(msg?.workDir, 4096),
|
|
135
|
+
routeKey: '',
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!clientSupportsRoutes || !agentSupportsRoutes) return null;
|
|
140
|
+
const route = {
|
|
141
|
+
runtimeProvider: clean(msg.workbenchRoute.runtimeProvider, 32),
|
|
142
|
+
agentId: clean(msg.workbenchRoute.agentId),
|
|
143
|
+
sessionId: clean(msg.workbenchRoute.sessionId),
|
|
144
|
+
};
|
|
145
|
+
const routeKey = workbenchRouteKey(route);
|
|
146
|
+
if (!routeKey || route.agentId !== targetAgentId) return null;
|
|
147
|
+
|
|
148
|
+
const row = route.runtimeProvider === 'yeaft'
|
|
149
|
+
? resolveYeaftRow(client, route)
|
|
150
|
+
: resolveChatRow(client, route);
|
|
151
|
+
if (row?.isArchived && !allowMissingSession) return null;
|
|
152
|
+
if (!row && !allowMissingSession) return null;
|
|
153
|
+
|
|
154
|
+
let scope = SCOPES.has(msg.workbenchScope) ? msg.workbenchScope : 'main';
|
|
155
|
+
if (scope === 'main') {
|
|
156
|
+
if (msg.conversationId === '_folder_picker') scope = 'files-folder-picker';
|
|
157
|
+
else if (msg.conversationId === '_git_folder_picker') scope = 'git-folder-picker';
|
|
158
|
+
}
|
|
159
|
+
const sessionWorkDir = clean(row
|
|
160
|
+
? (route.runtimeProvider === 'yeaft' ? row.workDir : row.work_dir)
|
|
161
|
+
: '', 4096);
|
|
162
|
+
const workspaceGeneration = sessionWorkDir
|
|
163
|
+
? workbenchWorkspaceGeneration(routeKey, sessionWorkDir)
|
|
164
|
+
: clean(msg.workbenchWorkspaceGeneration, 1600);
|
|
165
|
+
if (!workspaceGeneration && !allowMissingSession) return null;
|
|
166
|
+
return {
|
|
167
|
+
legacy: false,
|
|
168
|
+
route,
|
|
169
|
+
routeKey,
|
|
170
|
+
scope,
|
|
171
|
+
agentId: route.agentId,
|
|
172
|
+
conversationId: workbenchConversationId(route, scope),
|
|
173
|
+
// Terminal is pinned to this Server-owned cwd. Git and Files retain their
|
|
174
|
+
// existing Agent-path picker and use requestedWorkDir after route auth.
|
|
175
|
+
workDir: sessionWorkDir,
|
|
176
|
+
requestedWorkDir: clean(msg.workDir, 4096) || sessionWorkDir,
|
|
177
|
+
workspaceGeneration,
|
|
178
|
+
archived: row?.isArchived === true,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
@@ -15,6 +15,7 @@ import { handleAgentWorkCenter } from './handlers/agent-work-center.js';
|
|
|
15
15
|
import { handleAgentFileTerminal } from './handlers/agent-file-terminal.js';
|
|
16
16
|
import { handleAgentSync } from './handlers/agent-sync.js';
|
|
17
17
|
import { recordPerfTraceEvent } from './perf-trace.js';
|
|
18
|
+
import { clearWorkbenchCorrelationsForAgent } from './workbench-correlation.js';
|
|
18
19
|
import { markAgentHeartbeatSeen } from './heartbeat-policy.js';
|
|
19
20
|
|
|
20
21
|
/**
|
|
@@ -227,6 +228,7 @@ function handleAgentDisconnect(agentId, agentName, ws) {
|
|
|
227
228
|
if (!agent || agent.ws !== ws) return;
|
|
228
229
|
// Phase 4: 清理目录缓存
|
|
229
230
|
clearAgentDirCache(agentId);
|
|
231
|
+
clearWorkbenchCorrelationsForAgent(agentId);
|
|
230
232
|
// Phase 1: 清理同步超时
|
|
231
233
|
if (agent._syncTimeout) {
|
|
232
234
|
clearTimeout(agent._syncTimeout);
|
|
@@ -245,6 +247,7 @@ function completeAgentRegistration(ws, agentId, agentName, workDir, sessionKey,
|
|
|
245
247
|
const proxyPorts = (existingAgent?.proxyPorts || []).map(p => ({ ...p, enabled: false }));
|
|
246
248
|
const slashCommands = existingAgent?.slashCommands || [];
|
|
247
249
|
const slashCommandDescriptions = existingAgent?.slashCommandDescriptions || {};
|
|
250
|
+
const yeaftSessions = existingAgent?.yeaftSessions || new Map();
|
|
248
251
|
if (existingAgent?._syncTimeout) clearTimeout(existingAgent._syncTimeout);
|
|
249
252
|
|
|
250
253
|
// 兼容旧版 agent:未上报 capabilities 时默认全部开启
|
|
@@ -271,6 +274,7 @@ function completeAgentRegistration(ws, agentId, agentName, workDir, sessionKey,
|
|
|
271
274
|
proxyPorts,
|
|
272
275
|
slashCommands,
|
|
273
276
|
slashCommandDescriptions,
|
|
277
|
+
yeaftSessions,
|
|
274
278
|
status: 'syncing',
|
|
275
279
|
ownerId,
|
|
276
280
|
ownerUsername,
|
|
@@ -6,6 +6,8 @@ import { authenticateRequest } from './auth/request-auth.js';
|
|
|
6
6
|
import { encodeKey } from './encryption.js';
|
|
7
7
|
import { userDb } from './database.js';
|
|
8
8
|
import { agents, clearYeaftDebugRequestsForClient, webClients, isHeartbeatMessageType, trackRequest } from './context.js';
|
|
9
|
+
import { applyClientHello, WORKBENCH_ROUTE_PROTOCOL } from './client-protocol.js';
|
|
10
|
+
import { clearWorkbenchCorrelationsForClient } from './workbench-correlation.js';
|
|
9
11
|
import {
|
|
10
12
|
parseMessage, sendToWebClient, sendToAgent,
|
|
11
13
|
broadcastAgentList, resolveAgentAccessError
|
|
@@ -76,7 +78,9 @@ export function handleWebConnection(ws, url, req = {}) {
|
|
|
76
78
|
// (= old client, encrypt outbound for back-compat). Flipped to
|
|
77
79
|
// `false` when the client sends `client_hello { plaintextOk: true }`
|
|
78
80
|
// — see early dispatch in handleWebMessage.
|
|
79
|
-
encryptOutbound: true
|
|
81
|
+
encryptOutbound: true,
|
|
82
|
+
// Explicit Workbench protocol negotiation. Zero means legacy Web.
|
|
83
|
+
workbenchRouteProtocol: 0,
|
|
80
84
|
});
|
|
81
85
|
|
|
82
86
|
// 心跳响应处理
|
|
@@ -103,6 +107,7 @@ export function handleWebConnection(ws, url, req = {}) {
|
|
|
103
107
|
role,
|
|
104
108
|
acceptPlaintext: true,
|
|
105
109
|
yeaftSessionInventoryComplete: true,
|
|
110
|
+
workbenchRouteProtocol: WORKBENCH_ROUTE_PROTOCOL,
|
|
106
111
|
}));
|
|
107
112
|
setTimeout(() => broadcastAgentList(), 100);
|
|
108
113
|
} else {
|
|
@@ -164,6 +169,18 @@ export function handleWebConnection(ws, url, req = {}) {
|
|
|
164
169
|
}
|
|
165
170
|
clearWorkCenterRequestsForClient(client);
|
|
166
171
|
clearYeaftDebugRequestsForClient(clientId);
|
|
172
|
+
const ownedTerminals = clearWorkbenchCorrelationsForClient(clientId);
|
|
173
|
+
for (const owner of ownedTerminals) {
|
|
174
|
+
const agent = agents.get(owner.agentId);
|
|
175
|
+
if (!agent) continue;
|
|
176
|
+
void sendToAgent(agent, {
|
|
177
|
+
type: 'terminal_close',
|
|
178
|
+
terminalId: owner.terminalId,
|
|
179
|
+
conversationId: owner.conversationId,
|
|
180
|
+
workbenchRouteKey: owner.routeKey,
|
|
181
|
+
workbenchWorkspaceGeneration: owner.workspaceGeneration,
|
|
182
|
+
}).catch(error => console.warn('[Workbench] PTY disconnect cleanup failed:', error.message));
|
|
183
|
+
}
|
|
167
184
|
webClients.delete(clientId);
|
|
168
185
|
console.log(`Web client disconnected: ${clientId}`);
|
|
169
186
|
});
|
|
@@ -195,10 +212,15 @@ async function handleWebMessage(clientId, msg) {
|
|
|
195
212
|
// Old clients never send this; their per-client `encryptOutbound` flag
|
|
196
213
|
// stays `true` and we keep the ciphertext path.
|
|
197
214
|
if (msg.type === 'client_hello') {
|
|
198
|
-
|
|
199
|
-
|
|
215
|
+
const wasEncrypted = client.encryptOutbound;
|
|
216
|
+
applyClientHello(client, msg);
|
|
217
|
+
if (wasEncrypted && client.encryptOutbound === false) {
|
|
200
218
|
console.log(`[WS] Client ${clientId} negotiated plaintext mode`);
|
|
201
219
|
}
|
|
220
|
+
await sendToWebClient(client, {
|
|
221
|
+
type: 'client_hello_ack',
|
|
222
|
+
workbenchRouteProtocol: client.workbenchRouteProtocol,
|
|
223
|
+
});
|
|
202
224
|
return;
|
|
203
225
|
}
|
|
204
226
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.0.
|
|
1
|
+
{"version":"1.0.415"}
|