@yeaft/webchat-agent 1.0.188 → 1.0.189
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/cli.js +12 -0
- package/index.js +10 -2
- package/local-run.js +218 -0
- package/local-runtime/server/.env.example +54 -0
- package/local-runtime/server/api.js +111 -0
- package/local-runtime/server/auth/aad.js +235 -0
- package/local-runtime/server/auth/login.js +156 -0
- package/local-runtime/server/auth/oauth-flow.js +277 -0
- package/local-runtime/server/auth/password-reset.js +134 -0
- package/local-runtime/server/auth/providers/alipay.js +125 -0
- package/local-runtime/server/auth/providers/github.js +82 -0
- package/local-runtime/server/auth/providers/google.js +60 -0
- package/local-runtime/server/auth/providers/microsoft.js +71 -0
- package/local-runtime/server/auth/providers/types.js +57 -0
- package/local-runtime/server/auth/providers/wechat.js +68 -0
- package/local-runtime/server/auth/register.js +91 -0
- package/local-runtime/server/auth/session-store.js +57 -0
- package/local-runtime/server/auth/token.js +85 -0
- package/local-runtime/server/auth/totp-auth.js +133 -0
- package/local-runtime/server/auth/utils.js +42 -0
- package/local-runtime/server/auth.js +8 -0
- package/local-runtime/server/check-node-version.js +74 -0
- package/local-runtime/server/config.js +298 -0
- package/local-runtime/server/context.js +140 -0
- package/local-runtime/server/create-user.js +59 -0
- package/local-runtime/server/database.js +12 -0
- package/local-runtime/server/db/connection.js +963 -0
- package/local-runtime/server/db/expert-db.js +171 -0
- package/local-runtime/server/db/identity-db.js +92 -0
- package/local-runtime/server/db/invitation-db.js +38 -0
- package/local-runtime/server/db/message-db.js +257 -0
- package/local-runtime/server/db/session-db.js +118 -0
- package/local-runtime/server/db/user-db.js +165 -0
- package/local-runtime/server/db/user-stats-db.js +185 -0
- package/local-runtime/server/db/yeaft-session-db.js +258 -0
- package/local-runtime/server/email.js +96 -0
- package/local-runtime/server/encryption.js +105 -0
- package/local-runtime/server/handlers/agent-conversation.js +347 -0
- package/local-runtime/server/handlers/agent-file-terminal.js +99 -0
- package/local-runtime/server/handlers/agent-output.js +854 -0
- package/local-runtime/server/handlers/agent-sync.js +399 -0
- package/local-runtime/server/handlers/agent-work-center.js +27 -0
- package/local-runtime/server/handlers/client-conversation.js +1182 -0
- package/local-runtime/server/handlers/client-misc.js +254 -0
- package/local-runtime/server/handlers/client-work-center.js +269 -0
- package/local-runtime/server/handlers/client-workbench.js +146 -0
- package/local-runtime/server/handlers/session-pin-router.js +61 -0
- package/local-runtime/server/heartbeat-policy.js +46 -0
- package/local-runtime/server/index.js +275 -0
- package/local-runtime/server/package.json +55 -0
- package/local-runtime/server/perf-trace.js +154 -0
- package/local-runtime/server/proxy.js +273 -0
- package/local-runtime/server/routes/admin-routes.js +207 -0
- package/local-runtime/server/routes/auth-routes.js +322 -0
- package/local-runtime/server/routes/expert-routes.js +117 -0
- package/local-runtime/server/routes/invitation-routes.js +60 -0
- package/local-runtime/server/routes/session-routes.js +112 -0
- package/local-runtime/server/routes/upload-routes.js +109 -0
- package/local-runtime/server/routes/user-routes.js +241 -0
- package/local-runtime/server/totp.js +74 -0
- package/local-runtime/server/work-item-attachment-policy.js +56 -0
- package/local-runtime/server/ws-agent.js +319 -0
- package/local-runtime/server/ws-client.js +214 -0
- package/local-runtime/server/ws-utils.js +394 -0
- package/local-runtime/server/yeaft-asset-store.js +339 -0
- package/local-runtime/version.json +1 -0
- package/local-runtime/web/app.bundle.js +7673 -0
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/assets/avatars/README.md +34 -0
- package/local-runtime/web/assets/avatars/ada.svg +1 -0
- package/local-runtime/web/assets/avatars/alan.svg +1 -0
- package/local-runtime/web/assets/avatars/alice.svg +1 -0
- package/local-runtime/web/assets/avatars/anders.svg +1 -0
- package/local-runtime/web/assets/avatars/bezos.svg +1 -0
- package/local-runtime/web/assets/avatars/borges.svg +1 -0
- package/local-runtime/web/assets/avatars/buffett.svg +1 -0
- package/local-runtime/web/assets/avatars/clausewitz.svg +1 -0
- package/local-runtime/web/assets/avatars/dalio.svg +1 -0
- package/local-runtime/web/assets/avatars/dieter.svg +1 -0
- package/local-runtime/web/assets/avatars/drucker.svg +1 -0
- package/local-runtime/web/assets/avatars/einstein.svg +1 -0
- package/local-runtime/web/assets/avatars/grace.svg +1 -0
- package/local-runtime/web/assets/avatars/harari.svg +1 -0
- package/local-runtime/web/assets/avatars/jung.svg +1 -0
- package/local-runtime/web/assets/avatars/kahneman.svg +1 -0
- package/local-runtime/web/assets/avatars/ken.svg +1 -0
- package/local-runtime/web/assets/avatars/kongzi.svg +1 -0
- package/local-runtime/web/assets/avatars/kubrick.svg +1 -0
- package/local-runtime/web/assets/avatars/linus.svg +1 -0
- package/local-runtime/web/assets/avatars/luxun.svg +1 -0
- package/local-runtime/web/assets/avatars/margaret.svg +1 -0
- package/local-runtime/web/assets/avatars/martin.svg +1 -0
- package/local-runtime/web/assets/avatars/miyazaki.svg +1 -0
- package/local-runtime/web/assets/avatars/munger.svg +1 -0
- package/local-runtime/web/assets/avatars/nietzsche.svg +1 -0
- package/local-runtime/web/assets/avatars/norman.svg +1 -0
- package/local-runtime/web/assets/avatars/shannon.svg +1 -0
- package/local-runtime/web/assets/avatars/simaqian.svg +1 -0
- package/local-runtime/web/assets/avatars/socrates.svg +1 -0
- package/local-runtime/web/assets/avatars/steve.svg +1 -0
- package/local-runtime/web/assets/avatars/sudongpo.svg +1 -0
- package/local-runtime/web/assets/avatars/sunzi.svg +1 -0
- package/local-runtime/web/docx-preview.min.js +2 -0
- package/local-runtime/web/docx-preview.min.js.gz +0 -0
- package/local-runtime/web/html-to-image.min.js +2 -0
- package/local-runtime/web/html-to-image.min.js.gz +0 -0
- package/local-runtime/web/index.html +21 -0
- package/local-runtime/web/jszip.min.js +13 -0
- package/local-runtime/web/jszip.min.js.gz +0 -0
- package/local-runtime/web/mermaid.min.js +2843 -0
- package/local-runtime/web/mermaid.min.js.gz +0 -0
- package/local-runtime/web/msal-browser.min.js +69 -0
- package/local-runtime/web/msal-browser.min.js.gz +0 -0
- package/local-runtime/web/style.bundle.css +10 -0
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/local-runtime/web/vendor.bundle.js +1522 -0
- package/local-runtime/web/vendor.bundle.js.gz +0 -0
- package/local-runtime/web/xlsx.min.js +24 -0
- package/local-runtime/web/xlsx.min.js.gz +0 -0
- package/package.json +13 -3
- package/scripts/prepare-local-runtime.js +25 -0
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import { WebSocket } from 'ws';
|
|
2
|
+
import { CONFIG } from './config.js';
|
|
3
|
+
import { encrypt, decrypt, isEncrypted, encodeKey } from './encryption.js';
|
|
4
|
+
import { sessionDb } from './database.js';
|
|
5
|
+
import { agents, webClients, directoryCache, DIR_CACHE_TTL, DIR_CACHE_MAX_SIZE, trackMessageBytesSent } from './context.js';
|
|
6
|
+
|
|
7
|
+
// Send message to web client.
|
|
8
|
+
// feat-ws-plaintext-negotiation: defaults to plaintext when the client
|
|
9
|
+
// advertised plaintext-ok capability via client_hello (encryptOutbound
|
|
10
|
+
// flipped to false). Falls back to ciphertext for old clients that
|
|
11
|
+
// haven't sent client_hello — back-compat. skipAuth still forces
|
|
12
|
+
// plaintext for local dev visibility.
|
|
13
|
+
export async function sendToWebClient(client, msg) {
|
|
14
|
+
if (client.ws.readyState !== WebSocket.OPEN) return;
|
|
15
|
+
|
|
16
|
+
// Plaintext path: dev mode OR new client that announced plaintext-ok.
|
|
17
|
+
if (CONFIG.skipAuth || client.encryptOutbound === false) {
|
|
18
|
+
const payload = JSON.stringify(msg);
|
|
19
|
+
trackMessageBytesSent(client.userId, payload.length, msg.type);
|
|
20
|
+
client.ws.send(payload);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Encrypted fallback for old clients that never sent client_hello.
|
|
25
|
+
if (!client.sessionKey) {
|
|
26
|
+
console.error('Cannot send to client: missing session key in production mode');
|
|
27
|
+
client.ws.close(1008, 'Encryption required');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const encrypted = await encrypt(msg, client.sessionKey);
|
|
32
|
+
const payload = JSON.stringify(encrypted);
|
|
33
|
+
trackMessageBytesSent(client.userId, payload.length, msg.type);
|
|
34
|
+
if (msg.type === 'file_content') console.log(`[sendToWebClient] file_content encrypted, payload size=${payload.length}, compressed=${encrypted.z}`);
|
|
35
|
+
client.ws.send(payload);
|
|
36
|
+
} catch (e) {
|
|
37
|
+
console.error(`[sendToWebClient] Error encrypting/sending ${msg.type}:`, e.message, e.stack);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Send message to agent.
|
|
42
|
+
// feat-ws-plaintext-negotiation: same shape as sendToWebClient — defaults
|
|
43
|
+
// to plaintext when the agent advertised the `plaintext-ok` capability.
|
|
44
|
+
// Falls back to ciphertext for old agents that haven't.
|
|
45
|
+
export async function sendToAgent(agent, msg) {
|
|
46
|
+
if (agent.ws.readyState !== WebSocket.OPEN) return;
|
|
47
|
+
|
|
48
|
+
if (CONFIG.skipAuth || agent.encryptOutbound === false) {
|
|
49
|
+
agent.ws.send(JSON.stringify(msg));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!agent.sessionKey) {
|
|
54
|
+
console.error('Cannot send to agent: missing session key in production mode');
|
|
55
|
+
agent.ws.close(1008, 'Encryption required');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const encrypted = await encrypt(msg, agent.sessionKey);
|
|
59
|
+
agent.ws.send(JSON.stringify(encrypted));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Parse incoming message (decrypt if needed)
|
|
63
|
+
export async function parseMessage(data, sessionKey) {
|
|
64
|
+
try {
|
|
65
|
+
const parsed = JSON.parse(data.toString());
|
|
66
|
+
|
|
67
|
+
if (sessionKey && isEncrypted(parsed)) {
|
|
68
|
+
return await decrypt(parsed, sessionKey);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return parsed;
|
|
72
|
+
} catch (e) {
|
|
73
|
+
console.error('Failed to parse message:', e.message);
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 广播 agent 列表给所有已认证的 web 客户端(按 owner 过滤)
|
|
79
|
+
export async function broadcastAgentList() {
|
|
80
|
+
for (const [, client] of webClients) {
|
|
81
|
+
if (client.authenticated) {
|
|
82
|
+
// 按 owner 过滤:只显示属于该用户的 agent,或 ownerId=null 的全局 agent(仅 admin 可见)
|
|
83
|
+
const agentList = Array.from(agents.entries())
|
|
84
|
+
.filter(([, agent]) =>
|
|
85
|
+
CONFIG.skipAuth ||
|
|
86
|
+
agent.ownerId === client.userId ||
|
|
87
|
+
(!agent.ownerId && client.role === 'admin')
|
|
88
|
+
)
|
|
89
|
+
.map(([id, agent]) => ({
|
|
90
|
+
id,
|
|
91
|
+
name: agent.name,
|
|
92
|
+
instanceId: agent.instanceId || null,
|
|
93
|
+
workDir: agent.workDir,
|
|
94
|
+
online: agent.ws.readyState === WebSocket.OPEN,
|
|
95
|
+
status: agent.status || 'ready',
|
|
96
|
+
latency: agent.latency || null,
|
|
97
|
+
capabilities: agent.capabilities || ['terminal', 'file_editor', 'background_tasks'],
|
|
98
|
+
version: agent.version || null,
|
|
99
|
+
yeaftStatus: agent.yeaftStatus || null,
|
|
100
|
+
proxyPorts: agent.proxyPorts || [],
|
|
101
|
+
conversations: Array.from(agent.conversations.values()).filter(c =>
|
|
102
|
+
CONFIG.skipAuth || !c.userId || c.userId === client.userId
|
|
103
|
+
).map(c => {
|
|
104
|
+
// fix-chat-title-sticky: lazy-hydrate `customTitle` from
|
|
105
|
+
// the DB whenever it's missing, so pre-fix in-memory
|
|
106
|
+
// convInfo objects (and any future rebuild path that
|
|
107
|
+
// forgets) still produce a correct broadcast.
|
|
108
|
+
if (!c.title || c.customTitle === undefined) {
|
|
109
|
+
const dbSession = sessionDb.get(c.id);
|
|
110
|
+
if (dbSession?.title) {
|
|
111
|
+
c.title = c.title || dbSession.title;
|
|
112
|
+
}
|
|
113
|
+
if (dbSession) {
|
|
114
|
+
c.pinned = !!dbSession.is_pinned;
|
|
115
|
+
if (c.customTitle === undefined) c.customTitle = !!dbSession.customTitle;
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
const dbSession = sessionDb.get(c.id);
|
|
119
|
+
if (dbSession) c.pinned = !!dbSession.is_pinned;
|
|
120
|
+
}
|
|
121
|
+
return c;
|
|
122
|
+
})
|
|
123
|
+
}));
|
|
124
|
+
await sendToWebClient(client, {
|
|
125
|
+
type: 'agent_list',
|
|
126
|
+
agents: agentList
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 发送 conversation 列表给特定客户端
|
|
133
|
+
export async function sendConversationList(clientId, agentId) {
|
|
134
|
+
const client = webClients.get(clientId);
|
|
135
|
+
const agent = agents.get(agentId);
|
|
136
|
+
|
|
137
|
+
if (client && agent) {
|
|
138
|
+
const filteredConvs = Array.from(agent.conversations.values()).filter(c =>
|
|
139
|
+
CONFIG.skipAuth || !c.userId || c.userId === client.userId
|
|
140
|
+
).map(c => {
|
|
141
|
+
// fix-chat-title-sticky: same lazy-hydrate as broadcastAgentList —
|
|
142
|
+
// ensure `customTitle` is reliably set on the wire so the
|
|
143
|
+
// per-message auto-title gate stays honest after restart / sync.
|
|
144
|
+
if (!c.title || c.customTitle === undefined) {
|
|
145
|
+
const dbSession = sessionDb.get(c.id);
|
|
146
|
+
if (dbSession?.title) {
|
|
147
|
+
c.title = c.title || dbSession.title;
|
|
148
|
+
}
|
|
149
|
+
if (dbSession) {
|
|
150
|
+
c.pinned = !!dbSession.is_pinned;
|
|
151
|
+
if (c.customTitle === undefined) c.customTitle = !!dbSession.customTitle;
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
const dbSession = sessionDb.get(c.id);
|
|
155
|
+
if (dbSession) c.pinned = !!dbSession.is_pinned;
|
|
156
|
+
}
|
|
157
|
+
return c;
|
|
158
|
+
});
|
|
159
|
+
await sendToWebClient(client, {
|
|
160
|
+
type: 'conversation_list',
|
|
161
|
+
agentId,
|
|
162
|
+
conversations: filteredConvs
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 通知会话更新给相关客户端
|
|
168
|
+
export async function notifyConversationUpdate(agentId, msg) {
|
|
169
|
+
// 对于 folders_list、history_sessions_list,优先定向发送给请求者
|
|
170
|
+
if (msg.type === 'folders_list' || msg.type === 'history_sessions_list' || msg.type === 'models_list') {
|
|
171
|
+
const targetClientId = msg._requestClientId;
|
|
172
|
+
if (targetClientId) {
|
|
173
|
+
const targetClient = webClients.get(targetClientId);
|
|
174
|
+
if (targetClient?.authenticated) {
|
|
175
|
+
const { _requestClientId, ...cleanMsg } = msg;
|
|
176
|
+
await sendToWebClient(targetClient, cleanMsg);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
// Target client disconnected/reconnected — fall through to broadcast
|
|
180
|
+
}
|
|
181
|
+
// Fallback: 如果没有 _requestClientId 或 target 不可用,发送给所有已认证客户端
|
|
182
|
+
const { _requestClientId: _, ...cleanMsg } = msg;
|
|
183
|
+
for (const [, client] of webClients) {
|
|
184
|
+
if (client.authenticated) {
|
|
185
|
+
await sendToWebClient(client, cleanMsg);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 对于 conversation_created 和 conversation_resumed,只发送给拥有该会话的用户的客户端
|
|
192
|
+
// 避免其他用户收到后误触发 select_conversation 导致 Permission denied
|
|
193
|
+
if (msg.type === 'conversation_created' || msg.type === 'conversation_resumed') {
|
|
194
|
+
const msgWithAgent = { ...msg, agentId };
|
|
195
|
+
const ownerId = msg.userId;
|
|
196
|
+
for (const [, client] of webClients) {
|
|
197
|
+
// 只发送给已认证且属于同一用户的客户端(或开发模式下所有客户端)
|
|
198
|
+
if (client.authenticated && (!ownerId || CONFIG.skipAuth || client.userId === ownerId)) {
|
|
199
|
+
await sendToWebClient(client, msgWithAgent);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
for (const [, client] of webClients) {
|
|
206
|
+
if (client.authenticated && client.currentAgent === agentId) {
|
|
207
|
+
await sendToWebClient(client, msg);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* 检查用户是否有权访问指定的 Agent
|
|
214
|
+
* @param {string} agentId
|
|
215
|
+
* @param {string} userId
|
|
216
|
+
* @param {string} [role] - 用户角色,ownerId=null 的全局 agent 仅 admin 可访问
|
|
217
|
+
* @returns {boolean}
|
|
218
|
+
*/
|
|
219
|
+
export function verifyAgentOwnership(agentId, userId, role = null) {
|
|
220
|
+
if (CONFIG.skipAuth) return true;
|
|
221
|
+
const agent = agents.get(agentId);
|
|
222
|
+
if (!agent) return false;
|
|
223
|
+
if (agent.ownerId === userId) return true;
|
|
224
|
+
// ownerId=null 表示通过全局 AGENT_SECRET 连接,仅 admin 可访问
|
|
225
|
+
if (!agent.ownerId && role === 'admin') return true;
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function resolveAgentAccessError(agentId, userId, role = null) {
|
|
230
|
+
if (!agentId) return 'Agent not found';
|
|
231
|
+
const agent = agents.get(agentId);
|
|
232
|
+
if (!agent) return 'Agent not found or offline';
|
|
233
|
+
if (!verifyAgentOwnership(agentId, userId, role)) return 'Agent access denied';
|
|
234
|
+
if (agent.ws?.readyState !== WebSocket.OPEN) return 'Agent not found or offline';
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Broadcast an Agent-level event to every authenticated browser that may
|
|
240
|
+
* access the Agent. Unlike conversation forwarding, this also supports
|
|
241
|
+
* ownerless global Agents, which are visible only to admins.
|
|
242
|
+
*/
|
|
243
|
+
export async function forwardAgentEvent(agentId, msg) {
|
|
244
|
+
for (const [, client] of webClients) {
|
|
245
|
+
if (!client.authenticated) continue;
|
|
246
|
+
if (!verifyAgentOwnership(agentId, client.userId, client.role)) continue;
|
|
247
|
+
await sendToWebClient(client, msg);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// 转发消息给拥有该会话的用户的所有客户端
|
|
252
|
+
export async function forwardToClients(agentId, conversationId, msg) {
|
|
253
|
+
// ★ Security: 从 agent.conversations 获取会话的 userId
|
|
254
|
+
const agent = agents.get(agentId);
|
|
255
|
+
const conv = agent?.conversations.get(conversationId);
|
|
256
|
+
// ★ Security: ownerId 优先级: conversation.userId > _requestUserId > agent.ownerId
|
|
257
|
+
// 确保生产模式下不会因 ownerId 缺失而向所有用户广播
|
|
258
|
+
const ownerId = conv?.userId || msg._requestUserId || agent?.ownerId || null;
|
|
259
|
+
|
|
260
|
+
let forwarded = false;
|
|
261
|
+
for (const [clientId, client] of webClients) {
|
|
262
|
+
// 只发送给已认证且属于同一用户的客户端
|
|
263
|
+
// 仅在开发模式下,无 ownerId 时允许广播给所有已认证客户端
|
|
264
|
+
if (client.authenticated && (CONFIG.skipAuth ? (!ownerId || client.userId === ownerId) : (ownerId && client.userId === ownerId))) {
|
|
265
|
+
try {
|
|
266
|
+
await sendToWebClient(client, msg);
|
|
267
|
+
forwarded = true;
|
|
268
|
+
if (msg.type === 'file_content') console.log(`[Forward] file_content sent to client ${clientId}, size=${JSON.stringify(msg).length}`);
|
|
269
|
+
} catch (e) {
|
|
270
|
+
console.error(`[Forward] Error sending ${msg.type} to client ${clientId}:`, e.message);
|
|
271
|
+
}
|
|
272
|
+
} else if (msg.type === 'file_content') {
|
|
273
|
+
console.log(`[Forward] file_content SKIPPED client ${clientId}: auth=${client.authenticated}, ownerId=${ownerId}, clientUserId=${client.userId}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (!forwarded) {
|
|
277
|
+
if (msg.type === 'file_content') {
|
|
278
|
+
console.warn(`[Forward] file_content NOT forwarded! conv=${conversationId}, agent=${agentId}, ownerId=${ownerId}, _reqUser=${msg._requestUserId}, webClients=${webClients.size}`);
|
|
279
|
+
} else if (msg.type === 'claude_output') {
|
|
280
|
+
console.warn(`[Forward] No authenticated clients for conv=${conversationId}, owner=${ownerId}`);
|
|
281
|
+
} else if (msg.type === 'directory_listing') {
|
|
282
|
+
console.warn(`[Forward] directory_listing NOT forwarded! conv=${conversationId}, agent=${agentId}, ownerId=${ownerId}, _reqUser=${msg._requestUserId}, webClients=${webClients.size}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// 转发消息给指定 agent
|
|
288
|
+
export async function forwardToAgent(agentId, msg) {
|
|
289
|
+
const agent = agents.get(agentId);
|
|
290
|
+
if (agent) {
|
|
291
|
+
await sendToAgent(agent, msg);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// 通过 agent name 查找 agent
|
|
296
|
+
export function findAgentByName(agentName) {
|
|
297
|
+
for (const [id, agent] of agents) {
|
|
298
|
+
if (agent.name === agentName || id === agentName) {
|
|
299
|
+
return { id, agent };
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* 检查用户是否拥有指定的会话
|
|
307
|
+
*/
|
|
308
|
+
export function verifyConversationOwnership(conversationId, userId) {
|
|
309
|
+
if (!conversationId || !userId) {
|
|
310
|
+
console.warn(`[Ownership] Check failed: conversationId=${conversationId}, userId=${userId} (missing parameter)`);
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// 先从内存中的 agent conversations 查找
|
|
315
|
+
for (const [agentId, agent] of agents) {
|
|
316
|
+
const conv = agent.conversations.get(conversationId);
|
|
317
|
+
if (conv) {
|
|
318
|
+
// 无 owner 的 conversation(创建时未启用 auth)允许任何已认证用户访问
|
|
319
|
+
if (!conv.userId) return true;
|
|
320
|
+
const match = conv.userId === userId;
|
|
321
|
+
if (!match) {
|
|
322
|
+
console.warn(`[Ownership] Mismatch for ${conversationId}: conv.userId=${conv.userId}, client.userId=${userId}, agent=${agentId}`);
|
|
323
|
+
}
|
|
324
|
+
return match;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// 再从数据库查找
|
|
329
|
+
try {
|
|
330
|
+
const session = sessionDb.get(conversationId);
|
|
331
|
+
if (session) {
|
|
332
|
+
if (!session.user_id) return true;
|
|
333
|
+
const match = session.user_id === userId;
|
|
334
|
+
if (!match) {
|
|
335
|
+
console.warn(`[Ownership] DB mismatch for ${conversationId}: session.user_id=${session.user_id}, client.userId=${userId}`);
|
|
336
|
+
}
|
|
337
|
+
return match;
|
|
338
|
+
}
|
|
339
|
+
} catch (e) {
|
|
340
|
+
console.error('Failed to verify conversation ownership:', e.message);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// conversation 在内存和数据库中都找不到 — 拒绝访问
|
|
344
|
+
console.warn(`[Ownership] Conversation ${conversationId} not found anywhere, userId=${userId}, denying access`);
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ★ Phase 4: Directory cache helpers
|
|
349
|
+
|
|
350
|
+
function normalizeDirPath(p) {
|
|
351
|
+
return (p || '').replace(/\\/g, '/').replace(/\/+$/, '');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function getDirCacheKey(agentId, dirPath) {
|
|
355
|
+
return `${agentId}:${normalizeDirPath(dirPath)}`;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export function getCachedDir(agentId, dirPath) {
|
|
359
|
+
const key = getDirCacheKey(agentId, dirPath);
|
|
360
|
+
const cached = directoryCache.get(key);
|
|
361
|
+
if (cached && Date.now() - cached.timestamp < DIR_CACHE_TTL) {
|
|
362
|
+
return cached.entries;
|
|
363
|
+
}
|
|
364
|
+
if (cached) directoryCache.delete(key);
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function setCachedDir(agentId, dirPath, entries) {
|
|
369
|
+
if (directoryCache.size >= DIR_CACHE_MAX_SIZE) {
|
|
370
|
+
const oldest = [...directoryCache.entries()].sort((a, b) => a[1].timestamp - b[1].timestamp)[0];
|
|
371
|
+
if (oldest) directoryCache.delete(oldest[0]);
|
|
372
|
+
}
|
|
373
|
+
directoryCache.set(getDirCacheKey(agentId, dirPath), {
|
|
374
|
+
entries, timestamp: Date.now()
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export function clearAgentDirCache(agentId) {
|
|
379
|
+
const prefix = `${agentId}:`;
|
|
380
|
+
const keysToDelete = [];
|
|
381
|
+
for (const key of directoryCache.keys()) {
|
|
382
|
+
if (key.startsWith(prefix)) keysToDelete.push(key);
|
|
383
|
+
}
|
|
384
|
+
for (const key of keysToDelete) directoryCache.delete(key);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export function invalidateParentDirCache(agentId, filePath) {
|
|
388
|
+
if (!filePath) return;
|
|
389
|
+
const parentDir = filePath.replace(/[\\\/][^\\\/]+$/, '');
|
|
390
|
+
if (parentDir) {
|
|
391
|
+
const key = getDirCacheKey(agentId, parentDir);
|
|
392
|
+
directoryCache.delete(key);
|
|
393
|
+
}
|
|
394
|
+
}
|