@yeaft/webchat-agent 1.0.413 → 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/browser-runtime/browser-install.js +497 -0
- package/browser-runtime/cli.js +88 -0
- package/browser-runtime/config.js +116 -0
- package/browser-runtime/errors.js +8 -0
- package/browser-runtime/extension/manifest.json +18 -0
- package/browser-runtime/extension/offscreen.html +5 -0
- package/browser-runtime/extension/offscreen.js +101 -0
- package/browser-runtime/extension/popup.html +5 -0
- package/browser-runtime/extension/popup.js +1 -0
- package/browser-runtime/extension/service-worker.js +48 -0
- package/browser-runtime/extension.js +45 -0
- package/browser-runtime/index.js +5 -0
- package/browser-runtime/probe.js +427 -0
- package/browser-runtime/protocol.js +71 -0
- package/browser-runtime/service.js +132 -0
- package/browser-runtime/windows-version-job.ps1 +233 -0
- package/browser-runtime/windows-version-worker.js +75 -0
- package/browser-runtime/windows-version.js +85 -0
- package/cli.js +24 -7
- package/connection/index.js +12 -0
- package/context.js +1 -0
- package/index.js +19 -2
- package/llm-config-cli.js +24 -21
- 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 +5 -1
- package/service/config.js +23 -2
- package/service/index.js +1 -0
- package/service/linux.js +3 -2
- 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/config-api.js +138 -192
- package/yeaft/config-store.js +192 -0
- package/yeaft/config.js +3 -0
- package/yeaft/init.js +20 -7
- package/yeaft/sessions/feature-flag.js +15 -33
- package/yeaft/sessions/session-manifest.js +114 -10
- package/yeaft/stdio-protocol.js +57 -0
- package/yeaft/storage/atomic.js +43 -17
- package/yeaft/tools/process-runner.js +86 -13
package/cli.js
CHANGED
|
@@ -61,6 +61,14 @@ const SERVICE_COMMANDS = ['install', 'uninstall', 'start', 'stop', 'restart', 's
|
|
|
61
61
|
|
|
62
62
|
if (command === 'doctor') {
|
|
63
63
|
await handleDoctorCommand(subArgs);
|
|
64
|
+
} else if (command === 'browser') {
|
|
65
|
+
try {
|
|
66
|
+
const { handleBrowserCommand } = await import('./browser-runtime/cli.js');
|
|
67
|
+
await handleBrowserCommand(subArgs);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error(`Browser Runtime command failed: ${error.code || error.message}`);
|
|
70
|
+
process.exitCode = 1;
|
|
71
|
+
}
|
|
64
72
|
} else if (command === 'llm') {
|
|
65
73
|
await handleLlmCommand(subArgs);
|
|
66
74
|
} else if (command === 'local') {
|
|
@@ -137,6 +145,10 @@ function printHelp() {
|
|
|
137
145
|
yeaft-agent status [options] Show service status
|
|
138
146
|
yeaft-agent logs [options] View service logs (follow mode)
|
|
139
147
|
yeaft-agent doctor Diagnose service configuration
|
|
148
|
+
yeaft-agent browser install Install the pinned Chrome for Testing build
|
|
149
|
+
yeaft-agent browser probe Validate tabCapture → offscreen → WebRTC
|
|
150
|
+
yeaft-agent browser enable|disable Change the Browser Runtime feature flag
|
|
151
|
+
yeaft-agent browser status Show config and managed browser install status
|
|
140
152
|
yeaft-agent llm <command> Configure local Yeaft LLM providers/models
|
|
141
153
|
yeaft-agent container <command> Manage a Dockerized yeaft-agent
|
|
142
154
|
yeaft-agent upgrade [--name <id>] Upgrade and restart the selected service
|
|
@@ -153,6 +165,11 @@ function printHelp() {
|
|
|
153
165
|
--yeaft-dir <dir> Yeaft data directory for this instance
|
|
154
166
|
--auto-upgrade Check for updates on startup
|
|
155
167
|
|
|
168
|
+
Browser options:
|
|
169
|
+
--executable <path> Explicit compatible Chrome for Testing executable
|
|
170
|
+
--headful Run the Browser Runtime probe with a visible browser
|
|
171
|
+
--name <id> Select the named Agent instance
|
|
172
|
+
|
|
156
173
|
Environment variables (alternative to flags):
|
|
157
174
|
YEAFT_AGENT_INSTANCE Deprecated local service instance id override
|
|
158
175
|
SERVER_URL WebSocket server URL
|
|
@@ -174,7 +191,7 @@ function printHelp() {
|
|
|
174
191
|
`);
|
|
175
192
|
}
|
|
176
193
|
|
|
177
|
-
function printLlmHelp() {
|
|
194
|
+
export function printLlmHelp() {
|
|
178
195
|
console.log(`
|
|
179
196
|
Configure local Yeaft LLM providers/models in ~/.yeaft/config.json.
|
|
180
197
|
|
|
@@ -257,7 +274,7 @@ async function handleLlmCommand(args) {
|
|
|
257
274
|
const preset = args[1];
|
|
258
275
|
if (preset === 'github-copilot') {
|
|
259
276
|
result = await useGitHubCopilot(current, options);
|
|
260
|
-
writeLocalLlmConfig(result.config, configPath);
|
|
277
|
+
writeLocalLlmConfig(result.config, configPath, current);
|
|
261
278
|
console.log(`Configured GitHub Copilot provider with ${result.discovery.models.length} ${result.discovery.source} models.`);
|
|
262
279
|
if (result.discovery.warning) console.log(`Warning: ${result.discovery.warning}`);
|
|
263
280
|
console.log(`Primary model: ${result.config.primaryModel}`);
|
|
@@ -266,7 +283,7 @@ async function handleLlmCommand(args) {
|
|
|
266
283
|
}
|
|
267
284
|
if (preset === 'openai-compatible') {
|
|
268
285
|
result = await useOpenAICompatible(current, options, process.env);
|
|
269
|
-
writeLocalLlmConfig(result.config, configPath);
|
|
286
|
+
writeLocalLlmConfig(result.config, configPath, current);
|
|
270
287
|
console.log(`Configured ${result.provider.name} with ${result.discovery.models.length} live models.`);
|
|
271
288
|
console.log(`Primary model: ${result.config.primaryModel}`);
|
|
272
289
|
if (result.config.fastModel) console.log(`Fast model: ${result.config.fastModel}`);
|
|
@@ -277,7 +294,7 @@ async function handleLlmCommand(args) {
|
|
|
277
294
|
|
|
278
295
|
if (subcommand === 'add-provider') {
|
|
279
296
|
result = addOrUpdateProvider(current, options, process.env);
|
|
280
|
-
writeLocalLlmConfig(result.config, configPath);
|
|
297
|
+
writeLocalLlmConfig(result.config, configPath, current);
|
|
281
298
|
console.log(`${result.replaced ? 'Updated' : 'Added'} provider: ${result.provider.name}`);
|
|
282
299
|
if (result.config.primaryModel) console.log(`Primary model: ${result.config.primaryModel}`);
|
|
283
300
|
if (result.config.fastModel) console.log(`Fast model: ${result.config.fastModel}`);
|
|
@@ -286,7 +303,7 @@ async function handleLlmCommand(args) {
|
|
|
286
303
|
|
|
287
304
|
if (subcommand === 'set-model') {
|
|
288
305
|
result = setLocalModels(current, options);
|
|
289
|
-
writeLocalLlmConfig(result.config, configPath);
|
|
306
|
+
writeLocalLlmConfig(result.config, configPath, current);
|
|
290
307
|
if (result.config.primaryModel) console.log(`Primary model: ${result.config.primaryModel}`);
|
|
291
308
|
if (result.config.fastModel) console.log(`Fast model: ${result.config.fastModel}`);
|
|
292
309
|
return;
|
|
@@ -294,7 +311,7 @@ async function handleLlmCommand(args) {
|
|
|
294
311
|
|
|
295
312
|
if (subcommand === 'remove-provider') {
|
|
296
313
|
result = removeProvider(current, options);
|
|
297
|
-
writeLocalLlmConfig(result.config, configPath);
|
|
314
|
+
writeLocalLlmConfig(result.config, configPath, current);
|
|
298
315
|
console.log(result.removed ? `Removed provider: ${options.name}` : `Provider not found: ${options.name}`);
|
|
299
316
|
if (result.cleared.length) {
|
|
300
317
|
console.log(`Cleared ${result.cleared.join(', ')} because it referenced ${options.name}`);
|
|
@@ -426,7 +443,7 @@ async function runLlmSetup(current, configPath) {
|
|
|
426
443
|
const fastAnswer = (await rl.question('Fast model number or id (optional): ')).trim();
|
|
427
444
|
const fast = fastAnswer ? (ids[Number(fastAnswer) - 1] || fastAnswer) : null;
|
|
428
445
|
const result = await useGitHubCopilot(current, { model: primary, fast, allowUnknownModel: false });
|
|
429
|
-
writeLocalLlmConfig(result.config, configPath);
|
|
446
|
+
writeLocalLlmConfig(result.config, configPath, current);
|
|
430
447
|
console.log(`Configured GitHub Copilot with ${result.discovery.models.length} ${result.discovery.source} models.`);
|
|
431
448
|
if (result.discovery.warning) console.log(`Warning: ${result.discovery.warning}`);
|
|
432
449
|
console.log(`Primary model: ${result.config.primaryModel}`);
|
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/context.js
CHANGED
|
@@ -20,6 +20,7 @@ export default {
|
|
|
20
20
|
CONFIG: null,
|
|
21
21
|
managedCliReady: Promise.resolve([]),
|
|
22
22
|
agentCapabilities: [],
|
|
23
|
+
browserRuntime: null,
|
|
23
24
|
// Agent 级别的 slash commands 缓存(所有 conversation 共用)
|
|
24
25
|
slashCommands: [],
|
|
25
26
|
// Slash command 描述映射: { commandName: description } — 从 plugin commands/*.md 提取
|
package/index.js
CHANGED
|
@@ -23,6 +23,7 @@ import { connect } from './connection.js';
|
|
|
23
23
|
import { loadMcpServers } from './mcp.js';
|
|
24
24
|
import { SAFE_REMOTE_UPGRADE_CAPABILITY } from './upgrade-command.js';
|
|
25
25
|
import { loadConfig as loadYeaftConfig } from './yeaft/config.js';
|
|
26
|
+
import { bootBrowserRuntime, shutdownBrowserRuntime } from './browser-runtime/index.js';
|
|
26
27
|
import {
|
|
27
28
|
ensureManagedCliTools,
|
|
28
29
|
prepareManagedCliToolEnvironment,
|
|
@@ -104,7 +105,7 @@ const { agentName: AGENT_NAME, instanceId: INSTANCE_ID } = resolveRuntimeIdentit
|
|
|
104
105
|
const YEAFT_DIR = process.env.YEAFT_DIR || fileConfig.yeaftDir || getDefaultYeaftDir(INSTANCE_ID);
|
|
105
106
|
try {
|
|
106
107
|
if (!existsSync(YEAFT_DIR)) {
|
|
107
|
-
mkdirSync(YEAFT_DIR, { recursive: true });
|
|
108
|
+
mkdirSync(YEAFT_DIR, { recursive: true, mode: 0o700 });
|
|
108
109
|
console.log(`[Agent] Created yeaft dir: ${YEAFT_DIR}`);
|
|
109
110
|
}
|
|
110
111
|
} catch (err) {
|
|
@@ -159,7 +160,7 @@ async function detectCapabilities() {
|
|
|
159
160
|
// agent build can speak plaintext WS frames. New servers see this and
|
|
160
161
|
// flip `agent.encryptOutbound = false`, stopping outbound encryption
|
|
161
162
|
// to this peer. Old servers ignore the unknown capability token.
|
|
162
|
-
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'];
|
|
163
164
|
if (process.platform === 'linux') capabilities.push('work_item_attachments');
|
|
164
165
|
const pty = await loadNodePty();
|
|
165
166
|
if (pty) capabilities.push('terminal');
|
|
@@ -356,6 +357,7 @@ function cleanup() {
|
|
|
356
357
|
const { shutdownWorkCenter } = await import('./yeaft/work-center/bridge.js');
|
|
357
358
|
await shutdownWorkCenter();
|
|
358
359
|
} catch {}
|
|
360
|
+
try { await shutdownBrowserRuntime(); } catch {}
|
|
359
361
|
if (ctx.ws) ctx.ws.close();
|
|
360
362
|
});
|
|
361
363
|
}
|
|
@@ -396,6 +398,21 @@ process.on('SIGTERM', async () => {
|
|
|
396
398
|
} catch (error) {
|
|
397
399
|
console.warn(`[Startup] managed rg environment setup failed; using built-in fallback: ${error?.message || error}`);
|
|
398
400
|
}
|
|
401
|
+
try {
|
|
402
|
+
const runtimeConfig = loadYeaftConfig({ dir: YEAFT_DIR }).browserRuntime;
|
|
403
|
+
ctx.browserRuntime = await bootBrowserRuntime({
|
|
404
|
+
yeaftDir: YEAFT_DIR,
|
|
405
|
+
config: runtimeConfig,
|
|
406
|
+
});
|
|
407
|
+
const probe = ctx.browserRuntime.snapshot().probe;
|
|
408
|
+
if (probe?.ok) {
|
|
409
|
+
console.log(`[BrowserRuntime] ready: capture=${probe.captureMode} build=${probe.actualBuildId || probe.expectedBuildId}`);
|
|
410
|
+
} else if (runtimeConfig?.enabled) {
|
|
411
|
+
console.warn(`[BrowserRuntime] unavailable: ${probe?.code || 'probe_failed'}`);
|
|
412
|
+
}
|
|
413
|
+
} catch (err) {
|
|
414
|
+
console.warn(`[BrowserRuntime] startup probe failed: ${err?.message || err}`);
|
|
415
|
+
}
|
|
399
416
|
ctx.agentCapabilities = await detectCapabilities();
|
|
400
417
|
// Prime the models.dev community catalog so the Yeaft engine's *synchronous*
|
|
401
418
|
// hot path (engine.js / config.js / cli.js all read context-window inline)
|
package/llm-config-cli.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { existsSync, readFileSync
|
|
2
|
-
import {
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
4
|
import { normalizePluginConfig } from './yeaft/plugins.js';
|
|
5
|
+
import { mutateAgentConfigPath } from './yeaft/config-store.js';
|
|
5
6
|
import {
|
|
6
7
|
discoverGitHubCopilotModels,
|
|
7
8
|
discoverOpenAICompatibleModels,
|
|
@@ -40,31 +41,33 @@ export function readLocalLlmConfig(configPath = getDefaultYeaftConfigPath()) {
|
|
|
40
41
|
return parsed;
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
export function writeLocalLlmConfig(config, configPath = getDefaultYeaftConfigPath()) {
|
|
44
|
-
// The CLI exposes this writer publicly, so do not rely on callers having
|
|
45
|
-
// already used readLocalLlmConfig(). An existing invalid Plugins policy must
|
|
46
|
-
// remain on disk and keep runtime fail-closed until the user repairs it.
|
|
47
|
-
const existing = existsSync(configPath) ? readLocalLlmConfig(configPath) : null;
|
|
44
|
+
export function writeLocalLlmConfig(config, configPath = getDefaultYeaftConfigPath(), baseConfig = null) {
|
|
48
45
|
if (!config || typeof config !== 'object' || Array.isArray(config)
|
|
49
46
|
|| Object.getPrototypeOf(config) !== Object.prototype) {
|
|
50
47
|
throw new Error(`Invalid config file: expected JSON object at ${configPath}`);
|
|
51
48
|
}
|
|
52
|
-
|
|
53
|
-
if (next.plugins === undefined
|
|
54
|
-
&& Object.prototype.hasOwnProperty.call(existing || {}, 'plugins')) {
|
|
55
|
-
// LLM commands change provider/model fields. They must not remove a
|
|
56
|
-
// separately-managed Plugin allowlist merely because their payload omits it.
|
|
57
|
-
next.plugins = existing.plugins;
|
|
58
|
-
}
|
|
59
|
-
if (next.plugins !== undefined) {
|
|
49
|
+
if (config.plugins !== undefined) {
|
|
60
50
|
try {
|
|
61
|
-
normalizePluginConfig(
|
|
62
|
-
} catch (
|
|
63
|
-
throw new Error(`Invalid config file: ${
|
|
51
|
+
normalizePluginConfig(config.plugins);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
throw new Error(`Invalid config file: ${error?.message || error}`);
|
|
64
54
|
}
|
|
65
55
|
}
|
|
66
|
-
|
|
67
|
-
|
|
56
|
+
|
|
57
|
+
mutateAgentConfigPath(configPath, current => {
|
|
58
|
+
const baseline = baseConfig && typeof baseConfig === 'object' && !Array.isArray(baseConfig)
|
|
59
|
+
? baseConfig
|
|
60
|
+
: {};
|
|
61
|
+
const keys = new Set([...Object.keys(baseline), ...Object.keys(config)]);
|
|
62
|
+
for (const key of keys) {
|
|
63
|
+
if (key === 'plugins' && !Object.prototype.hasOwnProperty.call(config, key)) continue;
|
|
64
|
+
const before = baseline[key];
|
|
65
|
+
const after = config[key];
|
|
66
|
+
if (JSON.stringify(before) === JSON.stringify(after)) continue;
|
|
67
|
+
if (Object.prototype.hasOwnProperty.call(config, key)) current[key] = after;
|
|
68
|
+
else delete current[key];
|
|
69
|
+
}
|
|
70
|
+
});
|
|
68
71
|
}
|
|
69
72
|
|
|
70
73
|
export function parseModelsCsv(value) {
|
|
@@ -313,7 +316,7 @@ export async function tryAutoConfigureGitHubCopilot(configPath = getDefaultYeaft
|
|
|
313
316
|
...options,
|
|
314
317
|
model: options.model || DEFAULT_GITHUB_COPILOT_MODEL,
|
|
315
318
|
});
|
|
316
|
-
writeLocalLlmConfig(result.config, configPath);
|
|
319
|
+
writeLocalLlmConfig(result.config, configPath, current);
|
|
317
320
|
return { configured: true, reason: 'configured', ...result };
|
|
318
321
|
} catch (error) {
|
|
319
322
|
return {
|
|
@@ -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);
|