@yeaft/webchat-agent 1.0.268 → 1.0.269

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.
@@ -1,7 +1,8 @@
1
1
  import { WebSocket } from 'ws';
2
2
  import { CONFIG } from './config.js';
3
3
  import { encrypt, decrypt, isEncrypted, encodeKey } from './encryption.js';
4
- import { sessionDb } from './database.js';
4
+ import { sessionDb, yeaftSessionDb, sessionUiMetadataDb } from './database.js';
5
+ import { projectSessionCatalog } from './session-catalog.js';
5
6
  import { agents, webClients, directoryCache, DIR_CACHE_TTL, DIR_CACHE_MAX_SIZE, trackMessageBytesSent } from './context.js';
6
7
 
7
8
  // Send message to web client.
@@ -75,6 +76,63 @@ export async function parseMessage(data, sessionKey) {
75
76
  }
76
77
  }
77
78
 
79
+ function onlineAgentIdsForUser(userId, role = null) {
80
+ return new Set(
81
+ Array.from(agents.entries())
82
+ .filter(([, agent]) => agent?.ws?.readyState === WebSocket.OPEN)
83
+ .filter(([, agent]) => CONFIG.skipAuth
84
+ || agent.ownerId === userId
85
+ || (!agent.ownerId && role === 'admin'))
86
+ .map(([agentId]) => agentId),
87
+ );
88
+ }
89
+
90
+ export function verifyPersistedAgentOwnership(agentId, userId, role = null) {
91
+ if (CONFIG.skipAuth) return true;
92
+ if (!agentId || !userId) return false;
93
+ const agent = agents.get(agentId);
94
+ if (agent) {
95
+ if (agent.ownerId === userId) return true;
96
+ return !agent.ownerId && role === 'admin';
97
+ }
98
+ return sessionDb.hasOwnedRoute(userId, agentId)
99
+ || sessionDb.getByUser(userId).some(row => row.user_id === userId && row.agent_id === agentId)
100
+ || yeaftSessionDb.getByUser(userId).some(row => row.agentId === agentId);
101
+ }
102
+
103
+ export function buildSessionCatalog(userId, role = null) {
104
+ const chatSessions = sessionDb.getActiveByUser(userId).filter((session) => {
105
+ if (CONFIG.skipAuth || session.user_id === userId) return true;
106
+ if (session.user_id != null) return false;
107
+ // Legacy NULL-owner rows require a durable route proof when their Agent is
108
+ // offline. This same predicate protects catalog mutations.
109
+ return verifyPersistedAgentOwnership(session.agent_id, userId, role);
110
+ });
111
+ return projectSessionCatalog({
112
+ chatSessions,
113
+ yeaftSessions: yeaftSessionDb.getByUser(userId),
114
+ metadata: sessionUiMetadataDb.getByUser(userId),
115
+ onlineAgentIds: onlineAgentIdsForUser(userId, role),
116
+ });
117
+ }
118
+
119
+ // Broadcast the owner-scoped read model after canonical Session state changes.
120
+ export async function broadcastSessionCatalog(userId) {
121
+ if (!userId) return;
122
+ for (const [, client] of webClients) {
123
+ if (!client?.authenticated) continue;
124
+ if (client.userId !== userId) continue;
125
+ try {
126
+ await sendToWebClient(client, {
127
+ type: 'session_catalog_snapshot',
128
+ catalog: buildSessionCatalog(userId, client.role),
129
+ });
130
+ } catch (e) {
131
+ console.warn('[Server] session catalog projection failed:', e?.message || e);
132
+ }
133
+ }
134
+ }
135
+
78
136
  // 广播 agent 列表给所有已认证的 web 客户端(按 owner 过滤)
79
137
  export async function broadcastAgentList() {
80
138
  for (const [, client] of webClients) {
@@ -125,6 +183,14 @@ export async function broadcastAgentList() {
125
183
  type: 'agent_list',
126
184
  agents: agentList
127
185
  });
186
+ try {
187
+ await sendToWebClient(client, {
188
+ type: 'session_catalog_snapshot',
189
+ catalog: buildSessionCatalog(client.userId, client.role),
190
+ });
191
+ } catch (e) {
192
+ console.warn('[Server] session catalog projection failed:', e?.message || e);
193
+ }
128
194
  }
129
195
  }
130
196
  }
@@ -305,7 +371,7 @@ export function findAgentByName(agentName) {
305
371
  /**
306
372
  * 检查用户是否拥有指定的会话
307
373
  */
308
- export function verifyConversationOwnership(conversationId, userId) {
374
+ export function verifyConversationOwnership(conversationId, userId, role = null) {
309
375
  if (!conversationId || !userId) {
310
376
  console.warn(`[Ownership] Check failed: conversationId=${conversationId}, userId=${userId} (missing parameter)`);
311
377
  return false;
@@ -315,8 +381,9 @@ export function verifyConversationOwnership(conversationId, userId) {
315
381
  for (const [agentId, agent] of agents) {
316
382
  const conv = agent.conversations.get(conversationId);
317
383
  if (conv) {
318
- // owner conversation(创建时未启用 auth)允许任何已认证用户访问
319
- if (!conv.userId) return true;
384
+ // Legacy ownerless conversations inherit the Agent route ACL; they are
385
+ // not public merely because the old row lacks user_id.
386
+ if (!conv.userId) return verifyPersistedAgentOwnership(agentId, userId, role);
320
387
  const match = conv.userId === userId;
321
388
  if (!match) {
322
389
  console.warn(`[Ownership] Mismatch for ${conversationId}: conv.userId=${conv.userId}, client.userId=${userId}, agent=${agentId}`);
@@ -329,7 +396,13 @@ export function verifyConversationOwnership(conversationId, userId) {
329
396
  try {
330
397
  const session = sessionDb.get(conversationId);
331
398
  if (session) {
332
- if (!session.user_id) return true;
399
+ if (!session.user_id) {
400
+ const allowed = verifyPersistedAgentOwnership(session.agent_id, userId, role);
401
+ if (!allowed) {
402
+ console.warn(`[Ownership] Legacy Session ${conversationId} has no durable owner proof for userId=${userId}`);
403
+ }
404
+ return allowed;
405
+ }
333
406
  const match = session.user_id === userId;
334
407
  if (!match) {
335
408
  console.warn(`[Ownership] DB mismatch for ${conversationId}: session.user_id=${session.user_id}, client.userId=${userId}`);
@@ -1 +1 @@
1
- {"version":"1.0.268"}
1
+ {"version":"1.0.269"}