@yeaft/webchat-agent 1.0.186 → 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,254 @@
|
|
|
1
|
+
import { agents, userFileTabs } from '../context.js';
|
|
2
|
+
import {
|
|
3
|
+
sendToWebClient, forwardToAgent, broadcastAgentList
|
|
4
|
+
} from '../ws-utils.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Handle miscellaneous messages from web client.
|
|
8
|
+
* Types: ping, restart_agent, upgrade_agent,
|
|
9
|
+
* proxy_update_ports, update_file_tabs, restore_file_tabs
|
|
10
|
+
*/
|
|
11
|
+
export async function handleClientMisc(clientId, client, msg, checkAgentAccess) {
|
|
12
|
+
switch (msg.type) {
|
|
13
|
+
case 'ping':
|
|
14
|
+
await sendToWebClient(client, { type: 'pong' });
|
|
15
|
+
break;
|
|
16
|
+
|
|
17
|
+
case 'restart_agent': {
|
|
18
|
+
const restartAgentId = msg.agentId;
|
|
19
|
+
if (!restartAgentId) break;
|
|
20
|
+
if (!await checkAgentAccess(restartAgentId)) break;
|
|
21
|
+
await forwardToAgent(restartAgentId, { type: 'restart_agent' });
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
case 'upgrade_agent': {
|
|
26
|
+
const upgradeAgentId = msg.agentId;
|
|
27
|
+
if (!upgradeAgentId) break;
|
|
28
|
+
if (!await checkAgentAccess(upgradeAgentId)) break;
|
|
29
|
+
await forwardToAgent(upgradeAgentId, { type: 'upgrade_agent' });
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
case 'proxy_update_ports': {
|
|
34
|
+
const proxyAgentId = msg.agentId || client.currentAgent;
|
|
35
|
+
if (!proxyAgentId) break;
|
|
36
|
+
if (!await checkAgentAccess(proxyAgentId)) break;
|
|
37
|
+
const agent = agents.get(proxyAgentId);
|
|
38
|
+
if (agent) agent.proxyPorts = msg.ports || [];
|
|
39
|
+
await forwardToAgent(proxyAgentId, {
|
|
40
|
+
type: 'proxy_update_ports',
|
|
41
|
+
ports: msg.ports || []
|
|
42
|
+
});
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// File Tab 状态保存/恢复
|
|
47
|
+
case 'update_file_tabs': {
|
|
48
|
+
if (client.userId && client.currentAgent) {
|
|
49
|
+
const key = `${client.userId}:${client.currentAgent}`;
|
|
50
|
+
userFileTabs.set(key, {
|
|
51
|
+
files: (msg.openFiles || []).map(f => ({ path: f.path })),
|
|
52
|
+
activeIndex: msg.activeIndex || 0,
|
|
53
|
+
timestamp: Date.now()
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
case 'restore_file_tabs': {
|
|
60
|
+
const ftAgentId = msg.agentId || client.currentAgent;
|
|
61
|
+
if (client.userId && ftAgentId) {
|
|
62
|
+
if (!await checkAgentAccess(ftAgentId)) break;
|
|
63
|
+
const key = `${client.userId}:${ftAgentId}`;
|
|
64
|
+
const saved = userFileTabs.get(key);
|
|
65
|
+
await sendToWebClient(client, {
|
|
66
|
+
type: 'file_tabs_restored',
|
|
67
|
+
openFiles: saved?.files || [],
|
|
68
|
+
activeIndex: saved?.activeIndex || 0
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// MCP configuration
|
|
75
|
+
case 'get_mcp_servers': {
|
|
76
|
+
const mcpAgentId = msg.agentId || client.currentAgent;
|
|
77
|
+
if (!mcpAgentId) break;
|
|
78
|
+
if (!await checkAgentAccess(mcpAgentId)) break;
|
|
79
|
+
// If server already has cached list, return immediately
|
|
80
|
+
const mcpAgent = agents.get(mcpAgentId);
|
|
81
|
+
if (mcpAgent?.mcpServers?.length > 0) {
|
|
82
|
+
await sendToWebClient(client, {
|
|
83
|
+
type: 'mcp_servers_list',
|
|
84
|
+
agentId: mcpAgentId,
|
|
85
|
+
servers: mcpAgent.mcpServers
|
|
86
|
+
});
|
|
87
|
+
} else {
|
|
88
|
+
await forwardToAgent(mcpAgentId, { type: 'get_mcp_servers' });
|
|
89
|
+
}
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Expert roles definition (forward to agent)
|
|
94
|
+
case 'get_expert_roles': {
|
|
95
|
+
const expertAgentId = msg.agentId || client.currentAgent;
|
|
96
|
+
if (!expertAgentId) break;
|
|
97
|
+
if (!await checkAgentAccess(expertAgentId)) break;
|
|
98
|
+
await forwardToAgent(expertAgentId, { type: 'get_expert_roles' });
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
case 'update_mcp_config': {
|
|
103
|
+
const configAgentId = msg.agentId || client.currentAgent;
|
|
104
|
+
if (!configAgentId) break;
|
|
105
|
+
if (!await checkAgentAccess(configAgentId)) break;
|
|
106
|
+
await forwardToAgent(configAgentId, {
|
|
107
|
+
type: 'update_mcp_config',
|
|
108
|
+
config: msg.config || {}
|
|
109
|
+
});
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// LLM configuration — this writes only the selected agent's local ~/.yeaft/config.json.
|
|
114
|
+
case 'get_llm_config': {
|
|
115
|
+
const llmAgentId = msg.agentId || client.currentAgent;
|
|
116
|
+
if (!llmAgentId) break;
|
|
117
|
+
if (!await checkAgentAccess(llmAgentId)) break;
|
|
118
|
+
await forwardToAgent(llmAgentId, { type: 'get_llm_config' });
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
case 'discover_llm_models': {
|
|
123
|
+
const llmDiscoverAgentId = msg.agentId || client.currentAgent;
|
|
124
|
+
if (!llmDiscoverAgentId) break;
|
|
125
|
+
if (!await checkAgentAccess(llmDiscoverAgentId)) break;
|
|
126
|
+
await forwardToAgent(llmDiscoverAgentId, {
|
|
127
|
+
type: 'discover_llm_models',
|
|
128
|
+
agentId: llmDiscoverAgentId,
|
|
129
|
+
requestId: msg.requestId,
|
|
130
|
+
providerType: msg.providerType || msg.provider || msg.preset,
|
|
131
|
+
baseUrl: msg.baseUrl,
|
|
132
|
+
apiKey: msg.apiKey,
|
|
133
|
+
});
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
case 'update_llm_config': {
|
|
138
|
+
const llmUpdateAgentId = msg.agentId || client.currentAgent;
|
|
139
|
+
if (!llmUpdateAgentId) break;
|
|
140
|
+
if (!await checkAgentAccess(llmUpdateAgentId)) break;
|
|
141
|
+
await forwardToAgent(llmUpdateAgentId, {
|
|
142
|
+
type: 'update_llm_config',
|
|
143
|
+
config: msg.config || {}
|
|
144
|
+
});
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
case 'get_yeaft_settings': {
|
|
149
|
+
const targetAgentId = msg.agentId || client.currentAgent;
|
|
150
|
+
if (!targetAgentId) break;
|
|
151
|
+
if (!await checkAgentAccess(targetAgentId)) break;
|
|
152
|
+
await forwardToAgent(targetAgentId, { type: 'get_yeaft_settings' });
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
case 'update_yeaft_settings': {
|
|
157
|
+
const targetAgentId = msg.agentId || client.currentAgent;
|
|
158
|
+
if (!targetAgentId) break;
|
|
159
|
+
if (!await checkAgentAccess(targetAgentId)) break;
|
|
160
|
+
await forwardToAgent(targetAgentId, {
|
|
161
|
+
type: 'update_yeaft_settings',
|
|
162
|
+
settings: msg.settings || msg.config || {}
|
|
163
|
+
});
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Search settings (web-search backend + Tavily key + on-demand usage probe).
|
|
168
|
+
// Mirrors the get/update_yeaft_settings pair: the agent owns the
|
|
169
|
+
// config file, server is just a relay.
|
|
170
|
+
case 'get_search_settings': {
|
|
171
|
+
const a = msg.agentId || client.currentAgent;
|
|
172
|
+
if (!a) break;
|
|
173
|
+
if (!await checkAgentAccess(a)) break;
|
|
174
|
+
await forwardToAgent(a, { type: 'get_search_settings' });
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
case 'update_search_settings': {
|
|
179
|
+
const a = msg.agentId || client.currentAgent;
|
|
180
|
+
if (!a) break;
|
|
181
|
+
if (!await checkAgentAccess(a)) break;
|
|
182
|
+
await forwardToAgent(a, {
|
|
183
|
+
type: 'update_search_settings',
|
|
184
|
+
settings: msg.settings || msg.config || {}
|
|
185
|
+
});
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
case 'get_tavily_usage': {
|
|
190
|
+
const a = msg.agentId || client.currentAgent;
|
|
191
|
+
if (!a) break;
|
|
192
|
+
if (!await checkAgentAccess(a)) break;
|
|
193
|
+
await forwardToAgent(a, { type: 'get_tavily_usage' });
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Yeaft MCP CRUD (Claude-Code-style Settings → MCP tab).
|
|
198
|
+
// Server is a pure relay: agent owns the config file at
|
|
199
|
+
// `~/.yeaft/config.json` and the live MCPManager + ToolRegistry. We
|
|
200
|
+
// forward `yeaft_mcp_list/add/remove/reload` to the selected agent
|
|
201
|
+
// and the response (`yeaft_mcp_*_result` + broadcast
|
|
202
|
+
// `yeaft_mcp_updated`) flows back via agent-output.
|
|
203
|
+
case 'yeaft_mcp_list': {
|
|
204
|
+
const a = msg.agentId || client.currentAgent;
|
|
205
|
+
if (!a) break;
|
|
206
|
+
if (!await checkAgentAccess(a)) break;
|
|
207
|
+
await forwardToAgent(a, {
|
|
208
|
+
type: 'yeaft_mcp_list',
|
|
209
|
+
requestId: msg.requestId || null,
|
|
210
|
+
});
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
case 'yeaft_mcp_add': {
|
|
215
|
+
const a = msg.agentId || client.currentAgent;
|
|
216
|
+
if (!a) break;
|
|
217
|
+
if (!await checkAgentAccess(a)) break;
|
|
218
|
+
await forwardToAgent(a, {
|
|
219
|
+
type: 'yeaft_mcp_add',
|
|
220
|
+
requestId: msg.requestId || null,
|
|
221
|
+
server: msg.server || {},
|
|
222
|
+
});
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
case 'yeaft_mcp_remove': {
|
|
227
|
+
const a = msg.agentId || client.currentAgent;
|
|
228
|
+
if (!a) break;
|
|
229
|
+
if (!await checkAgentAccess(a)) break;
|
|
230
|
+
await forwardToAgent(a, {
|
|
231
|
+
type: 'yeaft_mcp_remove',
|
|
232
|
+
requestId: msg.requestId || null,
|
|
233
|
+
name: msg.name || '',
|
|
234
|
+
});
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
case 'yeaft_mcp_reload': {
|
|
239
|
+
const a = msg.agentId || client.currentAgent;
|
|
240
|
+
if (!a) break;
|
|
241
|
+
if (!await checkAgentAccess(a)) break;
|
|
242
|
+
await forwardToAgent(a, {
|
|
243
|
+
type: 'yeaft_mcp_reload',
|
|
244
|
+
requestId: msg.requestId || null,
|
|
245
|
+
name: msg.name || null,
|
|
246
|
+
});
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
default:
|
|
251
|
+
return false; // Not handled
|
|
252
|
+
}
|
|
253
|
+
return true; // Handled
|
|
254
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { CONFIG } from '../config.js';
|
|
3
|
+
import { agents, pendingFiles, previewFiles } from '../context.js';
|
|
4
|
+
import { forwardToAgent, sendToWebClient } from '../ws-utils.js';
|
|
5
|
+
import {
|
|
6
|
+
assertSupportedWorkItemAttachment,
|
|
7
|
+
assertWorkItemAttachmentSize,
|
|
8
|
+
MAX_WORK_ITEM_ATTACHMENTS,
|
|
9
|
+
MAX_WORK_ITEM_ATTACHMENT_BYTES,
|
|
10
|
+
MAX_WORK_ITEM_INLINE_BYTES,
|
|
11
|
+
} from '../work-item-attachment-policy.js';
|
|
12
|
+
|
|
13
|
+
const REQUEST_TIMEOUT_MS = 60_000;
|
|
14
|
+
const pendingRequests = new Map();
|
|
15
|
+
|
|
16
|
+
function decodePreviewBase64(value) {
|
|
17
|
+
const data = typeof value === 'string' ? value : '';
|
|
18
|
+
const maxEncodedLength = Math.ceil(MAX_WORK_ITEM_INLINE_BYTES / 3) * 4;
|
|
19
|
+
if (!data || data.length > maxEncodedLength || data.length % 4 !== 0
|
|
20
|
+
|| !/^[A-Za-z0-9+/]*={0,2}$/.test(data)) {
|
|
21
|
+
throw new Error('Attachment preview data is not valid base64');
|
|
22
|
+
}
|
|
23
|
+
const buffer = Buffer.from(data, 'base64');
|
|
24
|
+
if (buffer.length > MAX_WORK_ITEM_INLINE_BYTES) {
|
|
25
|
+
throw new Error(`Attachment preview exceeds ${MAX_WORK_ITEM_INLINE_BYTES} bytes`);
|
|
26
|
+
}
|
|
27
|
+
if (buffer.toString('base64') !== data) {
|
|
28
|
+
throw new Error('Attachment preview data is not canonical base64');
|
|
29
|
+
}
|
|
30
|
+
return buffer;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function hasPreviewSignature(buffer, mimeType) {
|
|
34
|
+
if (mimeType === 'image/png') {
|
|
35
|
+
return buffer.length >= 8 && buffer.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]));
|
|
36
|
+
}
|
|
37
|
+
if (mimeType === 'image/jpeg') {
|
|
38
|
+
return buffer.length >= 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff;
|
|
39
|
+
}
|
|
40
|
+
if (mimeType === 'image/gif') {
|
|
41
|
+
const signature = buffer.subarray(0, 6).toString('ascii');
|
|
42
|
+
return signature === 'GIF87a' || signature === 'GIF89a';
|
|
43
|
+
}
|
|
44
|
+
if (mimeType === 'image/webp') {
|
|
45
|
+
return buffer.length >= 12
|
|
46
|
+
&& buffer.subarray(0, 4).toString('ascii') === 'RIFF'
|
|
47
|
+
&& buffer.subarray(8, 12).toString('ascii') === 'WEBP';
|
|
48
|
+
}
|
|
49
|
+
if (mimeType === 'application/pdf') return buffer.subarray(0, 5).toString('ascii') === '%PDF-';
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function normalizeWorkItemPreview(previewData, attachment = {}) {
|
|
54
|
+
if (!previewData || typeof previewData !== 'object' || Array.isArray(previewData)) {
|
|
55
|
+
throw new Error('Attachment preview data is missing');
|
|
56
|
+
}
|
|
57
|
+
const filename = typeof previewData.filename === 'string' && previewData.filename.trim()
|
|
58
|
+
? previewData.filename.trim()
|
|
59
|
+
: typeof attachment.name === 'string' ? attachment.name.trim() : '';
|
|
60
|
+
const mimeType = typeof previewData.mimeType === 'string' && previewData.mimeType.trim()
|
|
61
|
+
? previewData.mimeType.trim().toLowerCase()
|
|
62
|
+
: typeof attachment.mimeType === 'string' ? attachment.mimeType.trim().toLowerCase() : '';
|
|
63
|
+
const kind = assertSupportedWorkItemAttachment(filename, mimeType);
|
|
64
|
+
const buffer = decodePreviewBase64(previewData.data);
|
|
65
|
+
if (!hasPreviewSignature(buffer, mimeType)) {
|
|
66
|
+
throw new Error('Attachment preview content does not match its declared type');
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
buffer,
|
|
70
|
+
filename,
|
|
71
|
+
mimeType: kind === 'text' ? 'text/plain; charset=utf-8' : mimeType,
|
|
72
|
+
kind,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function prunePendingRequests(now = Date.now()) {
|
|
77
|
+
for (const [requestId, pending] of pendingRequests) {
|
|
78
|
+
if (pending.expiresAt <= now) pendingRequests.delete(requestId);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function resolveWorkItemAttachments(client, payload) {
|
|
83
|
+
const attachments = Array.isArray(payload?.attachments) ? payload.attachments : [];
|
|
84
|
+
if (attachments.length === 0) return { payload, consumedIds: [] };
|
|
85
|
+
if (attachments.length > MAX_WORK_ITEM_ATTACHMENTS) {
|
|
86
|
+
throw new Error(`WorkItem supports at most ${MAX_WORK_ITEM_ATTACHMENTS} attachments`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const resolvedFiles = [];
|
|
90
|
+
const consumedIds = [];
|
|
91
|
+
const seen = new Set();
|
|
92
|
+
let totalBytes = 0;
|
|
93
|
+
for (const attachment of attachments) {
|
|
94
|
+
const fileId = typeof attachment?.fileId === 'string' ? attachment.fileId : '';
|
|
95
|
+
if (!fileId || seen.has(fileId)) throw new Error('Invalid WorkItem attachment reference');
|
|
96
|
+
seen.add(fileId);
|
|
97
|
+
const file = pendingFiles.get(fileId);
|
|
98
|
+
if (!file) throw new Error('WorkItem attachment expired; upload it again');
|
|
99
|
+
if (file.userId && !CONFIG.skipAuth && file.userId !== client.userId) {
|
|
100
|
+
throw new Error('WorkItem attachment access denied');
|
|
101
|
+
}
|
|
102
|
+
const buffer = Buffer.isBuffer(file.buffer) ? file.buffer : Buffer.from(file.buffer || '');
|
|
103
|
+
if (buffer.length > CONFIG.maxFileSize) throw new Error('WorkItem attachment exceeds the upload size limit');
|
|
104
|
+
totalBytes += buffer.length;
|
|
105
|
+
if (totalBytes > MAX_WORK_ITEM_ATTACHMENT_BYTES) {
|
|
106
|
+
throw new Error(`WorkItem attachments exceed ${MAX_WORK_ITEM_ATTACHMENT_BYTES} bytes`);
|
|
107
|
+
}
|
|
108
|
+
assertSupportedWorkItemAttachment(file.name, file.mimeType);
|
|
109
|
+
assertWorkItemAttachmentSize(buffer.length);
|
|
110
|
+
resolvedFiles.push({ file, buffer });
|
|
111
|
+
consumedIds.push(fileId);
|
|
112
|
+
}
|
|
113
|
+
const files = resolvedFiles.map(({ file, buffer }) => ({
|
|
114
|
+
name: file.name,
|
|
115
|
+
mimeType: file.mimeType,
|
|
116
|
+
data: buffer.toString('base64'),
|
|
117
|
+
isImage: String(file.mimeType || '').startsWith('image/'),
|
|
118
|
+
}));
|
|
119
|
+
return {
|
|
120
|
+
payload: { ...(payload || {}), files },
|
|
121
|
+
consumedIds,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Relay Work Center commands to the selected Agent.
|
|
127
|
+
*
|
|
128
|
+
* WorkItem data is Agent-local. The server owns authentication and routing.
|
|
129
|
+
* Request ownership stays server-side; an Agent can never choose which user
|
|
130
|
+
* receives a response by echoing or forging identity fields.
|
|
131
|
+
*/
|
|
132
|
+
export async function handleClientWorkCenter(client, msg, checkAgentAccess) {
|
|
133
|
+
if (msg?.type !== 'work_center_request') return false;
|
|
134
|
+
|
|
135
|
+
const agentId = typeof msg.agentId === 'string' && msg.agentId.trim()
|
|
136
|
+
? msg.agentId.trim()
|
|
137
|
+
: client.currentAgent;
|
|
138
|
+
if (!agentId) return true;
|
|
139
|
+
if (!await checkAgentAccess(agentId)) return true;
|
|
140
|
+
|
|
141
|
+
const op = typeof msg.op === 'string' ? msg.op : '';
|
|
142
|
+
const sourcePayload = msg.payload && typeof msg.payload === 'object' ? msg.payload : {};
|
|
143
|
+
if (['create', 'action_input', 'guide'].includes(op) && Object.hasOwn(sourcePayload, 'files')) {
|
|
144
|
+
await sendToWebClient(client, {
|
|
145
|
+
type: 'work_center_response',
|
|
146
|
+
requestId: typeof msg.requestId === 'string' ? msg.requestId : null,
|
|
147
|
+
agentId,
|
|
148
|
+
op,
|
|
149
|
+
ok: false,
|
|
150
|
+
error: 'WorkItem files are server-generated and cannot be supplied by the browser',
|
|
151
|
+
});
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const capabilities = agents.get(agentId)?.capabilities;
|
|
156
|
+
if (!Array.isArray(capabilities) || !capabilities.includes('work_center')) {
|
|
157
|
+
await sendToWebClient(client, {
|
|
158
|
+
type: 'work_center_response',
|
|
159
|
+
requestId: typeof msg.requestId === 'string' ? msg.requestId : null,
|
|
160
|
+
agentId,
|
|
161
|
+
op,
|
|
162
|
+
ok: false,
|
|
163
|
+
error: 'The selected Agent does not support Work Center; upgrade and restart the Agent',
|
|
164
|
+
});
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
prunePendingRequests();
|
|
169
|
+
const requestId = randomUUID();
|
|
170
|
+
let resolved = { payload: sourcePayload, consumedIds: [] };
|
|
171
|
+
try {
|
|
172
|
+
const attachments = Array.isArray(sourcePayload.attachments) ? sourcePayload.attachments : [];
|
|
173
|
+
if (['create', 'action_input', 'guide'].includes(op) && attachments.length > 0) {
|
|
174
|
+
const capabilities = agents.get(agentId)?.capabilities;
|
|
175
|
+
if (!Array.isArray(capabilities) || !capabilities.includes('work_item_attachments')) {
|
|
176
|
+
throw new Error('The selected Agent does not support WorkItem attachments');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (['create', 'action_input', 'guide'].includes(op)) resolved = resolveWorkItemAttachments(client, sourcePayload);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
await sendToWebClient(client, {
|
|
182
|
+
type: 'work_center_response',
|
|
183
|
+
requestId: typeof msg.requestId === 'string' ? msg.requestId : null,
|
|
184
|
+
agentId,
|
|
185
|
+
op,
|
|
186
|
+
ok: false,
|
|
187
|
+
error: error?.message || String(error),
|
|
188
|
+
});
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Store the browser correlation id before forwarding. Agent responses can
|
|
193
|
+
// arrive synchronously for an empty Work Center, so writing it after the
|
|
194
|
+
// awaited send creates a race where the response deletes this entry first.
|
|
195
|
+
pendingRequests.set(requestId, {
|
|
196
|
+
client,
|
|
197
|
+
agentId,
|
|
198
|
+
clientRequestId: typeof msg.requestId === 'string' ? msg.requestId : null,
|
|
199
|
+
attachmentFileIds: resolved.consumedIds,
|
|
200
|
+
expiresAt: Date.now() + REQUEST_TIMEOUT_MS,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
await forwardToAgent(agentId, {
|
|
205
|
+
type: 'work_center_request',
|
|
206
|
+
requestId,
|
|
207
|
+
op,
|
|
208
|
+
payload: resolved.payload,
|
|
209
|
+
});
|
|
210
|
+
} catch (error) {
|
|
211
|
+
pendingRequests.delete(requestId);
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export async function deliverWorkCenterResponse(agentId, msg) {
|
|
219
|
+
prunePendingRequests();
|
|
220
|
+
const pending = typeof msg?.requestId === 'string' ? pendingRequests.get(msg.requestId) : null;
|
|
221
|
+
if (!pending || pending.agentId !== agentId) return false;
|
|
222
|
+
pendingRequests.delete(msg.requestId);
|
|
223
|
+
if (msg.ok === true) {
|
|
224
|
+
for (const fileId of pending.attachmentFileIds || []) pendingFiles.delete(fileId);
|
|
225
|
+
}
|
|
226
|
+
const { agentId: _untrustedAgentId, requestId: _opaqueRequestId, _requestUserId, ...payload } = msg;
|
|
227
|
+
let response = payload;
|
|
228
|
+
if (msg.ok === true && msg.op === 'preview_attachment') {
|
|
229
|
+
try {
|
|
230
|
+
const data = payload.data;
|
|
231
|
+
const preview = normalizeWorkItemPreview(data?.previewData, data?.attachment);
|
|
232
|
+
const fileId = randomUUID();
|
|
233
|
+
const token = randomUUID();
|
|
234
|
+
previewFiles.set(fileId, {
|
|
235
|
+
...preview,
|
|
236
|
+
createdAt: Date.now(),
|
|
237
|
+
token,
|
|
238
|
+
});
|
|
239
|
+
const { previewData: _previewData, ...safeData } = data;
|
|
240
|
+
response = {
|
|
241
|
+
...payload,
|
|
242
|
+
data: { ...safeData, preview: `/api/preview/${fileId}?token=${encodeURIComponent(token)}` },
|
|
243
|
+
};
|
|
244
|
+
} catch (error) {
|
|
245
|
+
response = {
|
|
246
|
+
...payload,
|
|
247
|
+
ok: false,
|
|
248
|
+
error: error?.message || String(error),
|
|
249
|
+
data: undefined,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
await sendToWebClient(pending.client, {
|
|
254
|
+
...response,
|
|
255
|
+
agentId,
|
|
256
|
+
requestId: pending.clientRequestId,
|
|
257
|
+
});
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function clearWorkCenterRequestsForClient(client) {
|
|
262
|
+
for (const [requestId, pending] of pendingRequests) {
|
|
263
|
+
if (pending.client === client) pendingRequests.delete(requestId);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function __testResetWorkCenterRequests() {
|
|
268
|
+
pendingRequests.clear();
|
|
269
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { CONFIG } from '../config.js';
|
|
2
|
+
import {
|
|
3
|
+
sendToWebClient, forwardToAgent,
|
|
4
|
+
verifyConversationOwnership, getCachedDir
|
|
5
|
+
} from '../ws-utils.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Handle workbench messages from web client (terminal, file, git operations).
|
|
9
|
+
* Types: terminal_create, terminal_input, terminal_resize, terminal_close,
|
|
10
|
+
* read_file, write_file, list_directory,
|
|
11
|
+
* git_status, git_diff, git_add, git_reset, git_restore, git_commit, git_push,
|
|
12
|
+
* file_search, create_file, delete_files, move_files, copy_files, upload_to_dir
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Yeaft sessions use an agent-generated virtual conversationId
|
|
16
|
+
* ('yeaft-<timestamp>', see agent/yeaft/web-bridge.js) that exists neither in
|
|
17
|
+
* agent.conversations nor in the sessions DB table, so
|
|
18
|
+
* verifyConversationOwnership always falls through to "not found → deny".
|
|
19
|
+
* For these ids the ownership boundary is the Agent itself: by the time we
|
|
20
|
+
* get here the pro/admin workbench role gate (ws-client.js) and
|
|
21
|
+
* checkAgentAccess → verifyAgentOwnership have both passed — the same trust
|
|
22
|
+
* model already used by read_file and '_'-prefixed agent-level writes.
|
|
23
|
+
*/
|
|
24
|
+
function isYeaftVirtualConversation(conversationId) {
|
|
25
|
+
return typeof conversationId === 'string' && conversationId.startsWith('yeaft-');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function handleClientWorkbench(clientId, client, msg, checkAgentAccess) {
|
|
29
|
+
switch (msg.type) {
|
|
30
|
+
// Terminal messages (forward to agent)
|
|
31
|
+
case 'terminal_create':
|
|
32
|
+
case 'terminal_input':
|
|
33
|
+
case 'terminal_resize':
|
|
34
|
+
case 'terminal_close': {
|
|
35
|
+
const termAgentId = msg.agentId || client.currentAgent;
|
|
36
|
+
if (!termAgentId) return;
|
|
37
|
+
if (!await checkAgentAccess(termAgentId)) return;
|
|
38
|
+
const termConvId = msg.conversationId || client.currentConversation;
|
|
39
|
+
if (!termConvId) return;
|
|
40
|
+
if (!CONFIG.skipAuth && !isYeaftVirtualConversation(termConvId) && !verifyConversationOwnership(termConvId, client.userId)) {
|
|
41
|
+
console.warn(`[Security] User ${client.userId} terminal access denied for ${termConvId}`);
|
|
42
|
+
await sendToWebClient(client, { type: 'error', message: 'Permission denied' });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
await forwardToAgent(termAgentId, { ...msg, conversationId: termConvId });
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
case 'read_file': {
|
|
50
|
+
const fileAgentId = msg.agentId || client.currentAgent;
|
|
51
|
+
if (!fileAgentId) { console.warn('[Server] read_file: no agentId'); return; }
|
|
52
|
+
if (!await checkAgentAccess(fileAgentId)) return;
|
|
53
|
+
const fileConvId = msg.conversationId || client.currentConversation || '_explorer';
|
|
54
|
+
console.log(`[Server] Forwarding read_file to agent ${fileAgentId}, conv=${fileConvId}, path=${msg.filePath}`);
|
|
55
|
+
await forwardToAgent(fileAgentId, { ...msg, conversationId: fileConvId, _requestUserId: client.userId });
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
case 'write_file': {
|
|
60
|
+
const writeAgentId = msg.agentId || client.currentAgent;
|
|
61
|
+
if (!writeAgentId) return;
|
|
62
|
+
if (!await checkAgentAccess(writeAgentId)) return;
|
|
63
|
+
const writeConvId = msg.conversationId || client.currentConversation || '_explorer';
|
|
64
|
+
const isAgentLevelWrite = writeConvId.startsWith('_') || isYeaftVirtualConversation(writeConvId);
|
|
65
|
+
if (!isAgentLevelWrite) {
|
|
66
|
+
if (!CONFIG.skipAuth && !verifyConversationOwnership(writeConvId, client.userId)) {
|
|
67
|
+
console.warn(`[Security] User ${client.userId} file write denied for ${writeConvId}`);
|
|
68
|
+
await sendToWebClient(client, { type: 'error', message: 'Permission denied' });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
await forwardToAgent(writeAgentId, { ...msg, conversationId: writeConvId, _requestUserId: client.userId });
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
case 'list_directory': {
|
|
77
|
+
const dirAgentId = msg.agentId || client.currentAgent;
|
|
78
|
+
if (!dirAgentId) return;
|
|
79
|
+
if (!await checkAgentAccess(dirAgentId)) return;
|
|
80
|
+
|
|
81
|
+
// 先查缓存
|
|
82
|
+
const cached = getCachedDir(dirAgentId, msg.dirPath);
|
|
83
|
+
if (cached) {
|
|
84
|
+
await sendToWebClient(client, {
|
|
85
|
+
type: 'directory_listing',
|
|
86
|
+
conversationId: msg.conversationId,
|
|
87
|
+
requestId: msg.requestId,
|
|
88
|
+
dirPath: msg.dirPath,
|
|
89
|
+
entries: cached,
|
|
90
|
+
fromCache: true
|
|
91
|
+
});
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
await forwardToAgent(dirAgentId, {
|
|
96
|
+
type: 'list_directory',
|
|
97
|
+
dirPath: msg.dirPath,
|
|
98
|
+
workDir: msg.workDir,
|
|
99
|
+
conversationId: msg.conversationId || client.currentConversation,
|
|
100
|
+
requestId: msg.requestId,
|
|
101
|
+
_requestUserId: client.userId,
|
|
102
|
+
_requestClientId: clientId
|
|
103
|
+
});
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
case 'git_status':
|
|
108
|
+
case 'git_diff':
|
|
109
|
+
case 'git_add':
|
|
110
|
+
case 'git_reset':
|
|
111
|
+
case 'git_restore':
|
|
112
|
+
case 'git_commit':
|
|
113
|
+
case 'git_push':
|
|
114
|
+
case 'file_search': {
|
|
115
|
+
const gitAgentId = msg.agentId || client.currentAgent;
|
|
116
|
+
if (!gitAgentId) return;
|
|
117
|
+
if (!await checkAgentAccess(gitAgentId)) return;
|
|
118
|
+
await forwardToAgent(gitAgentId, {
|
|
119
|
+
...msg,
|
|
120
|
+
conversationId: msg.conversationId || client.currentConversation,
|
|
121
|
+
_requestUserId: client.userId
|
|
122
|
+
});
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
case 'create_file':
|
|
127
|
+
case 'delete_files':
|
|
128
|
+
case 'move_files':
|
|
129
|
+
case 'copy_files':
|
|
130
|
+
case 'upload_to_dir': {
|
|
131
|
+
const fopAgentId = msg.agentId || client.currentAgent;
|
|
132
|
+
if (!fopAgentId) return;
|
|
133
|
+
if (!await checkAgentAccess(fopAgentId)) return;
|
|
134
|
+
await forwardToAgent(fopAgentId, {
|
|
135
|
+
...msg,
|
|
136
|
+
conversationId: msg.conversationId || client.currentConversation,
|
|
137
|
+
_requestUserId: client.userId
|
|
138
|
+
});
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
default:
|
|
143
|
+
return false; // Not handled
|
|
144
|
+
}
|
|
145
|
+
return true; // Handled
|
|
146
|
+
}
|