@yeaft/webchat-agent 1.0.188 → 1.0.190

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.
Files changed (121) hide show
  1. package/cli.js +12 -0
  2. package/index.js +10 -2
  3. package/local-run.js +218 -0
  4. package/local-runtime/server/.env.example +54 -0
  5. package/local-runtime/server/api.js +111 -0
  6. package/local-runtime/server/auth/aad.js +235 -0
  7. package/local-runtime/server/auth/login.js +156 -0
  8. package/local-runtime/server/auth/oauth-flow.js +277 -0
  9. package/local-runtime/server/auth/password-reset.js +134 -0
  10. package/local-runtime/server/auth/providers/alipay.js +125 -0
  11. package/local-runtime/server/auth/providers/github.js +82 -0
  12. package/local-runtime/server/auth/providers/google.js +60 -0
  13. package/local-runtime/server/auth/providers/microsoft.js +71 -0
  14. package/local-runtime/server/auth/providers/types.js +57 -0
  15. package/local-runtime/server/auth/providers/wechat.js +68 -0
  16. package/local-runtime/server/auth/register.js +91 -0
  17. package/local-runtime/server/auth/session-store.js +57 -0
  18. package/local-runtime/server/auth/token.js +85 -0
  19. package/local-runtime/server/auth/totp-auth.js +133 -0
  20. package/local-runtime/server/auth/utils.js +42 -0
  21. package/local-runtime/server/auth.js +8 -0
  22. package/local-runtime/server/check-node-version.js +74 -0
  23. package/local-runtime/server/config.js +298 -0
  24. package/local-runtime/server/context.js +140 -0
  25. package/local-runtime/server/create-user.js +59 -0
  26. package/local-runtime/server/database.js +12 -0
  27. package/local-runtime/server/db/connection.js +963 -0
  28. package/local-runtime/server/db/expert-db.js +171 -0
  29. package/local-runtime/server/db/identity-db.js +92 -0
  30. package/local-runtime/server/db/invitation-db.js +38 -0
  31. package/local-runtime/server/db/message-db.js +257 -0
  32. package/local-runtime/server/db/session-db.js +118 -0
  33. package/local-runtime/server/db/user-db.js +165 -0
  34. package/local-runtime/server/db/user-stats-db.js +185 -0
  35. package/local-runtime/server/db/yeaft-session-db.js +258 -0
  36. package/local-runtime/server/email.js +96 -0
  37. package/local-runtime/server/encryption.js +105 -0
  38. package/local-runtime/server/handlers/agent-conversation.js +347 -0
  39. package/local-runtime/server/handlers/agent-file-terminal.js +99 -0
  40. package/local-runtime/server/handlers/agent-output.js +854 -0
  41. package/local-runtime/server/handlers/agent-sync.js +399 -0
  42. package/local-runtime/server/handlers/agent-work-center.js +27 -0
  43. package/local-runtime/server/handlers/client-conversation.js +1182 -0
  44. package/local-runtime/server/handlers/client-misc.js +254 -0
  45. package/local-runtime/server/handlers/client-work-center.js +269 -0
  46. package/local-runtime/server/handlers/client-workbench.js +146 -0
  47. package/local-runtime/server/handlers/session-pin-router.js +61 -0
  48. package/local-runtime/server/heartbeat-policy.js +46 -0
  49. package/local-runtime/server/index.js +275 -0
  50. package/local-runtime/server/package.json +55 -0
  51. package/local-runtime/server/perf-trace.js +154 -0
  52. package/local-runtime/server/proxy.js +273 -0
  53. package/local-runtime/server/routes/admin-routes.js +207 -0
  54. package/local-runtime/server/routes/auth-routes.js +322 -0
  55. package/local-runtime/server/routes/expert-routes.js +117 -0
  56. package/local-runtime/server/routes/invitation-routes.js +60 -0
  57. package/local-runtime/server/routes/session-routes.js +112 -0
  58. package/local-runtime/server/routes/upload-routes.js +109 -0
  59. package/local-runtime/server/routes/user-routes.js +241 -0
  60. package/local-runtime/server/totp.js +74 -0
  61. package/local-runtime/server/work-item-attachment-policy.js +56 -0
  62. package/local-runtime/server/ws-agent.js +319 -0
  63. package/local-runtime/server/ws-client.js +214 -0
  64. package/local-runtime/server/ws-utils.js +394 -0
  65. package/local-runtime/server/yeaft-asset-store.js +339 -0
  66. package/local-runtime/version.json +1 -0
  67. package/local-runtime/web/app.bundle.js +7673 -0
  68. package/local-runtime/web/app.bundle.js.gz +0 -0
  69. package/local-runtime/web/assets/avatars/README.md +34 -0
  70. package/local-runtime/web/assets/avatars/ada.svg +1 -0
  71. package/local-runtime/web/assets/avatars/alan.svg +1 -0
  72. package/local-runtime/web/assets/avatars/alice.svg +1 -0
  73. package/local-runtime/web/assets/avatars/anders.svg +1 -0
  74. package/local-runtime/web/assets/avatars/bezos.svg +1 -0
  75. package/local-runtime/web/assets/avatars/borges.svg +1 -0
  76. package/local-runtime/web/assets/avatars/buffett.svg +1 -0
  77. package/local-runtime/web/assets/avatars/clausewitz.svg +1 -0
  78. package/local-runtime/web/assets/avatars/dalio.svg +1 -0
  79. package/local-runtime/web/assets/avatars/dieter.svg +1 -0
  80. package/local-runtime/web/assets/avatars/drucker.svg +1 -0
  81. package/local-runtime/web/assets/avatars/einstein.svg +1 -0
  82. package/local-runtime/web/assets/avatars/grace.svg +1 -0
  83. package/local-runtime/web/assets/avatars/harari.svg +1 -0
  84. package/local-runtime/web/assets/avatars/jung.svg +1 -0
  85. package/local-runtime/web/assets/avatars/kahneman.svg +1 -0
  86. package/local-runtime/web/assets/avatars/ken.svg +1 -0
  87. package/local-runtime/web/assets/avatars/kongzi.svg +1 -0
  88. package/local-runtime/web/assets/avatars/kubrick.svg +1 -0
  89. package/local-runtime/web/assets/avatars/linus.svg +1 -0
  90. package/local-runtime/web/assets/avatars/luxun.svg +1 -0
  91. package/local-runtime/web/assets/avatars/margaret.svg +1 -0
  92. package/local-runtime/web/assets/avatars/martin.svg +1 -0
  93. package/local-runtime/web/assets/avatars/miyazaki.svg +1 -0
  94. package/local-runtime/web/assets/avatars/munger.svg +1 -0
  95. package/local-runtime/web/assets/avatars/nietzsche.svg +1 -0
  96. package/local-runtime/web/assets/avatars/norman.svg +1 -0
  97. package/local-runtime/web/assets/avatars/shannon.svg +1 -0
  98. package/local-runtime/web/assets/avatars/simaqian.svg +1 -0
  99. package/local-runtime/web/assets/avatars/socrates.svg +1 -0
  100. package/local-runtime/web/assets/avatars/steve.svg +1 -0
  101. package/local-runtime/web/assets/avatars/sudongpo.svg +1 -0
  102. package/local-runtime/web/assets/avatars/sunzi.svg +1 -0
  103. package/local-runtime/web/docx-preview.min.js +2 -0
  104. package/local-runtime/web/docx-preview.min.js.gz +0 -0
  105. package/local-runtime/web/html-to-image.min.js +2 -0
  106. package/local-runtime/web/html-to-image.min.js.gz +0 -0
  107. package/local-runtime/web/index.html +21 -0
  108. package/local-runtime/web/jszip.min.js +13 -0
  109. package/local-runtime/web/jszip.min.js.gz +0 -0
  110. package/local-runtime/web/mermaid.min.js +2843 -0
  111. package/local-runtime/web/mermaid.min.js.gz +0 -0
  112. package/local-runtime/web/msal-browser.min.js +69 -0
  113. package/local-runtime/web/msal-browser.min.js.gz +0 -0
  114. package/local-runtime/web/style.bundle.css +10 -0
  115. package/local-runtime/web/style.bundle.css.gz +0 -0
  116. package/local-runtime/web/vendor.bundle.js +1522 -0
  117. package/local-runtime/web/vendor.bundle.js.gz +0 -0
  118. package/local-runtime/web/xlsx.min.js +24 -0
  119. package/local-runtime/web/xlsx.min.js.gz +0 -0
  120. package/package.json +13 -3
  121. 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
+ }