@yeaft/webchat-agent 1.0.387 → 1.0.389

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.
@@ -25,6 +25,7 @@ export function connect(WebSocketImpl = WebSocket) {
25
25
  name: ctx.CONFIG.agentName,
26
26
  instanceId,
27
27
  workDir: ctx.CONFIG.workDir,
28
+ platform: process.platform,
28
29
  capabilities: ctx.agentCapabilities.join(',')
29
30
  });
30
31
 
@@ -72,7 +73,8 @@ export function connect(WebSocketImpl = WebSocket) {
72
73
  tempId: msg.tempId,
73
74
  secret: ctx.CONFIG.agentSecret,
74
75
  capabilities: ctx.agentCapabilities,
75
- version: ctx.agentVersion
76
+ version: ctx.agentVersion,
77
+ platform: process.platform
76
78
  }));
77
79
  return;
78
80
  }
@@ -37,6 +37,84 @@ export const userFileTabs = new Map();
37
37
  // fileId → { buffer, mimeType, filename, createdAt, token }
38
38
  export const previewFiles = new Map();
39
39
 
40
+ // Debug trace replies may come from an older Agent that does not echo the
41
+ // private browser client id. Keep the correlation on the Server, where the
42
+ // original Agent/Session ownership check happened, and consume it exactly once.
43
+ const YEAFT_DEBUG_REQUEST_TTL_MS = 30_000;
44
+ const YEAFT_DEBUG_REQUEST_MAX_PENDING = 2048;
45
+ export const pendingYeaftDebugRequests = new Map();
46
+
47
+ function yeaftDebugRequestKey(agentId, requestId) {
48
+ return `${String(agentId || '')}\u0000${String(requestId || '')}`;
49
+ }
50
+
51
+ function pruneYeaftDebugRequests(now = Date.now()) {
52
+ for (const [key, pending] of pendingYeaftDebugRequests) {
53
+ if (!pending || pending.expiresAt <= now || !webClients.has(pending.clientId)) {
54
+ pendingYeaftDebugRequests.delete(key);
55
+ }
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Register one owner-checked browser request before forwarding it to an Agent.
61
+ * @param {{agentId:string, requestId:string, sessionId:string, clientId:string, userId:string}} request
62
+ * @returns {boolean}
63
+ */
64
+ export function registerYeaftDebugRequest({ agentId, requestId, sessionId, clientId, userId }) {
65
+ if (!agentId || !requestId || !sessionId || !clientId || !userId) return false;
66
+ const now = Date.now();
67
+ pruneYeaftDebugRequests(now);
68
+ const key = yeaftDebugRequestKey(agentId, requestId);
69
+ if (pendingYeaftDebugRequests.has(key)) return false;
70
+ if (pendingYeaftDebugRequests.size >= YEAFT_DEBUG_REQUEST_MAX_PENDING) {
71
+ const oldestKey = pendingYeaftDebugRequests.keys().next().value;
72
+ if (oldestKey != null) pendingYeaftDebugRequests.delete(oldestKey);
73
+ }
74
+ pendingYeaftDebugRequests.set(key, {
75
+ agentId,
76
+ requestId,
77
+ sessionId,
78
+ clientId,
79
+ userId,
80
+ expiresAt: now + YEAFT_DEBUG_REQUEST_TTL_MS,
81
+ });
82
+ return true;
83
+ }
84
+
85
+ /**
86
+ * Consume a matching, unexpired correlation exactly once.
87
+ * @param {{agentId:string, requestId:string, sessionId?:string|null}} response
88
+ * @returns {{agentId:string, requestId:string, sessionId:string, clientId:string, userId:string, expiresAt:number}|null}
89
+ */
90
+ export function consumeYeaftDebugRequest({ agentId, requestId, sessionId }) {
91
+ if (!agentId || !requestId) return null;
92
+ const now = Date.now();
93
+ pruneYeaftDebugRequests(now);
94
+ const key = yeaftDebugRequestKey(agentId, requestId);
95
+ const pending = pendingYeaftDebugRequests.get(key);
96
+ if (!pending) return null;
97
+ if (sessionId && pending.sessionId !== sessionId) return null;
98
+ pendingYeaftDebugRequests.delete(key);
99
+ return pending;
100
+ }
101
+
102
+ /** Remove one correlation after forwarding fails. */
103
+ export function deleteYeaftDebugRequest({ agentId, requestId, clientId = null }) {
104
+ const key = yeaftDebugRequestKey(agentId, requestId);
105
+ const pending = pendingYeaftDebugRequests.get(key);
106
+ if (!pending || (clientId && pending.clientId !== clientId)) return false;
107
+ return pendingYeaftDebugRequests.delete(key);
108
+ }
109
+
110
+ /** Remove all correlations owned by a disconnected browser client. */
111
+ export function clearYeaftDebugRequestsForClient(clientId) {
112
+ if (!clientId) return;
113
+ for (const [key, pending] of pendingYeaftDebugRequests) {
114
+ if (pending?.clientId === clientId) pendingYeaftDebugRequests.delete(key);
115
+ }
116
+ }
117
+
40
118
  // Admin dashboard usage stats.
41
119
  // userId → { requests, bytesSent, bytesReceived, messages, sessions }
42
120
  // `messages` is user turn count. bytesSent/bytesReceived are message traffic
@@ -2,7 +2,7 @@ import { randomUUID } from 'crypto';
2
2
  import { messageDb, sessionUiMetadataDb, yeaftProjectDb, yeaftSessionDb } from '../database.js';
3
3
  import { transaction } from '../db/connection.js';
4
4
  import { broadcastAgentList, broadcastSessionCatalog, forwardToClients, sendToAgent, sendToWebClient } from '../ws-utils.js';
5
- import { webClients, previewFiles } from '../context.js';
5
+ import { consumeYeaftDebugRequest, webClients, previewFiles } from '../context.js';
6
6
  import { CONFIG } from '../config.js';
7
7
  import { yeaftAssetStore } from '../yeaft-asset-store.js';
8
8
  import { recordPerfTraceEvent } from '../perf-trace.js';
@@ -987,32 +987,43 @@ export async function handleAgentOutput(agentId, agent, msg) {
987
987
  break;
988
988
  }
989
989
 
990
- case 'yeaft_debug_history':
991
- // Relay the file-backed debug trace snapshot from the agent to the web
992
- // client that requested it via `yeaft_fetch_debug_history`. Keep the
993
- // paging/detail flags intact; otherwise the store cannot distinguish a
994
- // bounded index refresh from a single-request detail hydration.
995
- for (const [, c] of webClients) {
996
- if (c.authenticated && (CONFIG.skipAuth || c.userId === agent.ownerId)) {
997
- await sendToWebClient(c, {
998
- type: 'yeaft_debug_history',
999
- loops: Array.isArray(msg.loops) ? msg.loops : [],
1000
- turns: Array.isArray(msg.turns) ? msg.turns : [],
1001
- dreamEvents: Array.isArray(msg.dreamEvents) ? msg.dreamEvents : [],
1002
- ...(msg.sessionId != null ? { sessionId: msg.sessionId } : {}),
1003
- ...(msg.threadId != null ? { threadId: msg.threadId } : {}),
1004
- ...(msg.requestId != null ? { requestId: msg.requestId } : {}),
1005
- ...(msg.requestKind != null ? { requestKind: msg.requestKind } : {}),
1006
- ...(msg.search != null ? { search: msg.search } : {}),
1007
- ...(msg.hasMore != null ? { hasMore: !!msg.hasMore } : {}),
1008
- ...(msg.limit != null ? { limit: msg.limit } : {}),
1009
- ...(msg.indexOnly != null ? { indexOnly: !!msg.indexOnly } : {}),
1010
- ...(msg.detailTurnId != null ? { detailTurnId: msg.detailTurnId } : {}),
1011
- ...(msg.error != null ? { error: msg.error } : {}),
1012
- });
1013
- }
990
+ case 'yeaft_debug_history': {
991
+ // Debug history is request/Turn scoped and may contain raw provider or
992
+ // tool payloads. The Server registered this Agent + requestId after the
993
+ // compound Session ownership check, so rolling upgrades remain safe even
994
+ // when an older Agent does not echo the private browser client id.
995
+ const pending = consumeYeaftDebugRequest({
996
+ agentId,
997
+ requestId: msg.requestId,
998
+ sessionId: msg.sessionId,
999
+ });
1000
+ const targetClient = pending ? webClients.get(pending.clientId) : null;
1001
+ const ownerMatches = CONFIG.skipAuth || (
1002
+ pending?.userId === agent.ownerId
1003
+ && targetClient?.userId === pending.userId
1004
+ );
1005
+ if (targetClient?.authenticated && ownerMatches) {
1006
+ await sendToWebClient(targetClient, {
1007
+ type: 'yeaft_debug_history',
1008
+ agentId,
1009
+ loops: Array.isArray(msg.loops) ? msg.loops : [],
1010
+ turns: Array.isArray(msg.turns) ? msg.turns : [],
1011
+ dreamEvents: Array.isArray(msg.dreamEvents) ? msg.dreamEvents : [],
1012
+ ...(msg.projection && typeof msg.projection === 'object' ? { projection: msg.projection } : {}),
1013
+ ...(msg.sessionId != null ? { sessionId: msg.sessionId } : {}),
1014
+ ...(msg.threadId != null ? { threadId: msg.threadId } : {}),
1015
+ ...(msg.requestId != null ? { requestId: msg.requestId } : {}),
1016
+ ...(msg.requestKind != null ? { requestKind: msg.requestKind } : {}),
1017
+ ...(msg.search != null ? { search: msg.search } : {}),
1018
+ ...(msg.hasMore != null ? { hasMore: !!msg.hasMore } : {}),
1019
+ ...(msg.limit != null ? { limit: msg.limit } : {}),
1020
+ ...(msg.indexOnly != null ? { indexOnly: !!msg.indexOnly } : {}),
1021
+ ...(msg.detailTurnId != null ? { detailTurnId: msg.detailTurnId } : {}),
1022
+ ...(msg.error != null ? { error: msg.error } : {}),
1023
+ });
1014
1024
  }
1015
1025
  break;
1026
+ }
1016
1027
 
1017
1028
  default:
1018
1029
  return false; // Not handled
@@ -8,7 +8,14 @@ import {
8
8
  yeaftSessionDb,
9
9
  sessionUiMetadataDb,
10
10
  } from '../database.js';
11
- import { agents, pendingFiles, trackUserTurn, webClients } from '../context.js';
11
+ import {
12
+ agents,
13
+ deleteYeaftDebugRequest,
14
+ pendingFiles,
15
+ registerYeaftDebugRequest,
16
+ trackUserTurn,
17
+ webClients,
18
+ } from '../context.js';
12
19
  import {
13
20
  sendToWebClient, forwardToAgent,
14
21
  broadcastAgentList, broadcastSessionCatalog, buildSessionCatalog, buildHiddenSessionCatalog,
@@ -1288,6 +1295,45 @@ export async function handleClientConversation(clientId, client, msg, checkAgent
1288
1295
  break;
1289
1296
  }
1290
1297
 
1298
+ case 'yeaft_fetch_debug_history': {
1299
+ // Debug traces can contain raw prompts, provider payloads, and tool
1300
+ // output. Treat this as a precise Session-owned request instead of the
1301
+ // generic Yeaft relay: require the compound Agent + Session identity and
1302
+ // correlate the response back to the requesting browser tab.
1303
+ const debugAgentId = msg.agentId;
1304
+ const debugSessionId = typeof msg.sessionId === 'string' ? msg.sessionId : '';
1305
+ const debugRequestId = typeof msg.requestId === 'string' ? msg.requestId : '';
1306
+ if (!debugAgentId || !debugSessionId || !debugRequestId) return;
1307
+ if (!await checkAgentAccess(debugAgentId)) return;
1308
+ if (!CONFIG.skipAuth && !yeaftSessionDb.getForAgent(client.userId, debugAgentId, debugSessionId)) return;
1309
+ const registered = registerYeaftDebugRequest({
1310
+ agentId: debugAgentId,
1311
+ requestId: debugRequestId,
1312
+ sessionId: debugSessionId,
1313
+ clientId,
1314
+ userId: client.userId,
1315
+ });
1316
+ if (!registered) return;
1317
+ try {
1318
+ await forwardToAgent(debugAgentId, {
1319
+ type: 'yeaft_fetch_debug_history',
1320
+ sessionId: debugSessionId,
1321
+ requestId: debugRequestId,
1322
+ requestKind: msg.requestKind === 'detail' ? 'detail' : 'list',
1323
+ limit: typeof msg.limit === 'number' ? msg.limit : 10,
1324
+ dreamLimit: typeof msg.dreamLimit === 'number' ? msg.dreamLimit : 5,
1325
+ indexOnly: msg.indexOnly === true,
1326
+ detailTurnId: typeof msg.detailTurnId === 'string' ? msg.detailTurnId : null,
1327
+ search: typeof msg.search === 'string' ? msg.search.slice(0, 500) : '',
1328
+ _requestClientId: clientId,
1329
+ });
1330
+ } catch (err) {
1331
+ deleteYeaftDebugRequest({ agentId: debugAgentId, requestId: debugRequestId, clientId });
1332
+ throw err;
1333
+ }
1334
+ break;
1335
+ }
1336
+
1291
1337
  case 'yeaft_load_history_outline': {
1292
1338
  const outlineAgentId = msg.agentId;
1293
1339
  const outlineSessionId = typeof msg.sessionId === 'string' ? msg.sessionId : '';
@@ -8,8 +8,14 @@ import {
8
8
  // builds without this capability may still inherit the installed package cwd.
9
9
  export const SAFE_REMOTE_UPGRADE_CAPABILITY = 'remote_upgrade_safe';
10
10
 
11
- export function requiresManualUpgradeBridge(capabilities) {
12
- return !Array.isArray(capabilities) || !capabilities.includes(SAFE_REMOTE_UPGRADE_CAPABILITY);
11
+ export function requiresManualUpgradeBridge(capabilities, platform = null) {
12
+ if (Array.isArray(capabilities) && capabilities.includes(SAFE_REMOTE_UPGRADE_CAPABILITY)) return false;
13
+ const normalizedPlatform = typeof platform === 'string' ? platform.trim().toLowerCase() : '';
14
+ if (normalizedPlatform) return normalizedPlatform === 'win32';
15
+ // v1.0.373 predates explicit platform metadata but advertises this Linux-only
16
+ // capability, so it is safe to distinguish from the affected Windows build.
17
+ if (Array.isArray(capabilities) && capabilities.includes('work_item_attachments')) return false;
18
+ return true;
13
19
  }
14
20
 
15
21
  /**
@@ -36,7 +42,7 @@ export async function handleClientMisc(clientId, client, msg, checkAgentAccess)
36
42
  if (!upgradeAgentId) break;
37
43
  if (!await checkAgentAccess(upgradeAgentId)) break;
38
44
  const upgradeAgent = agents.get(upgradeAgentId);
39
- if (requiresManualUpgradeBridge(upgradeAgent?.capabilities)) {
45
+ if (requiresManualUpgradeBridge(upgradeAgent?.capabilities, upgradeAgent?.platform)) {
40
46
  await sendToWebClient(client, {
41
47
  type: 'upgrade_agent_ack',
42
48
  agentId: upgradeAgentId,
@@ -54,6 +54,7 @@ export function handleAgentConnection(ws, url) {
54
54
  const agentName = url.searchParams.get('name') || `Agent-${clientAgentId.slice(0, 8)}`;
55
55
  const instanceId = url.searchParams.get('instanceId') || clientAgentId;
56
56
  const workDir = url.searchParams.get('workDir') || '';
57
+ const urlPlatform = url.searchParams.get('platform') || null;
57
58
 
58
59
  // Both authenticated and SKIP_AUTH connections use the existing auth frame
59
60
  // for registration metadata. Released Agents already send their version there;
@@ -118,6 +119,9 @@ export function handleAgentConnection(ws, url) {
118
119
 
119
120
  const capabilities = Array.isArray(msg.capabilities) ? msg.capabilities : urlCapabilities;
120
121
  const agentVersion = msg.version || null;
122
+ const agentPlatform = typeof msg.platform === 'string' && msg.platform
123
+ ? msg.platform
124
+ : urlPlatform;
121
125
  // Local no-auth mode still has one durable browser owner. This makes
122
126
  // the server-side Session catalog persistent without changing generic
123
127
  // development-server behavior, which remains ownerless.
@@ -149,6 +153,7 @@ export function handleAgentConnection(ws, url) {
149
153
  ownerUsername,
150
154
  agentVersion,
151
155
  registeredInstanceId,
156
+ agentPlatform,
152
157
  );
153
158
  pruneAgentConnectionGenerations();
154
159
  }
@@ -233,7 +238,7 @@ function handleAgentDisconnect(agentId, agentName, ws) {
233
238
  broadcastAgentList();
234
239
  }
235
240
 
236
- function completeAgentRegistration(ws, agentId, agentName, workDir, sessionKey, capabilities = [], ownerId = null, ownerUsername = null, agentVersion = null, instanceId = null) {
241
+ function completeAgentRegistration(ws, agentId, agentName, workDir, sessionKey, capabilities = [], ownerId = null, ownerUsername = null, agentVersion = null, instanceId = null, agentPlatform = null) {
237
242
  // 如果是重连,保留 conversations;否则(server 重启)创建空 Map
238
243
  const existingAgent = agents.get(agentId);
239
244
  const conversations = existingAgent?.conversations || new Map();
@@ -270,6 +275,7 @@ function completeAgentRegistration(ws, agentId, agentName, workDir, sessionKey,
270
275
  ownerId,
271
276
  ownerUsername,
272
277
  version: agentVersion,
278
+ platform: agentPlatform,
273
279
  encryptOutbound
274
280
  });
275
281
 
@@ -5,7 +5,7 @@ import { generateSkipAuthSession } from './auth.js';
5
5
  import { authenticateRequest } from './auth/request-auth.js';
6
6
  import { encodeKey } from './encryption.js';
7
7
  import { userDb } from './database.js';
8
- import { agents, webClients, isHeartbeatMessageType, trackRequest } from './context.js';
8
+ import { agents, clearYeaftDebugRequestsForClient, webClients, isHeartbeatMessageType, trackRequest } from './context.js';
9
9
  import {
10
10
  parseMessage, sendToWebClient, sendToAgent,
11
11
  broadcastAgentList, resolveAgentAccessError
@@ -163,6 +163,7 @@ export function handleWebConnection(ws, url, req = {}) {
163
163
  }
164
164
  }
165
165
  clearWorkCenterRequestsForClient(client);
166
+ clearYeaftDebugRequestsForClient(clientId);
166
167
  webClients.delete(clientId);
167
168
  console.log(`Web client disconnected: ${clientId}`);
168
169
  });
@@ -1 +1 @@
1
- {"version":"1.0.387"}
1
+ {"version":"1.0.389"}