@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,273 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { WebSocket, WebSocketServer } from 'ws';
|
|
3
|
+
import { agents, pendingProxyRequests, proxyWsConnections } from './context.js';
|
|
4
|
+
import { sendToAgent, findAgentByName } from './ws-utils.js';
|
|
5
|
+
|
|
6
|
+
export function registerProxyRoutes(app) {
|
|
7
|
+
app.all('/agent/:agentName/:port/*', handleProxyRequest);
|
|
8
|
+
app.all('/agent/:agentName/:port', handleProxyRequest);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function handleProxyRequest(req, res) {
|
|
12
|
+
// No JWT authentication for proxy routes — external services need direct access.
|
|
13
|
+
// Access control is enforced by the port enable/disable mechanism in ProxyTab.
|
|
14
|
+
|
|
15
|
+
const { agentName, port } = req.params;
|
|
16
|
+
const portNum = parseInt(port);
|
|
17
|
+
if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
|
|
18
|
+
return res.status(400).send('Invalid port');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const found = findAgentByName(agentName);
|
|
22
|
+
if (!found || found.agent.ws.readyState !== WebSocket.OPEN) {
|
|
23
|
+
return res.status(502).send('Agent not connected');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Check if this port is enabled for proxy
|
|
27
|
+
const proxyPorts = found.agent.proxyPorts || [];
|
|
28
|
+
const portConfig = proxyPorts.find(p => p.port === portNum);
|
|
29
|
+
if (!portConfig || !portConfig.enabled) {
|
|
30
|
+
return res.status(403).send('Port not enabled for proxy');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Handle CORS preflight — respond immediately without forwarding to agent
|
|
34
|
+
if (req.method === 'OPTIONS') {
|
|
35
|
+
const origin = req.headers.origin || '*';
|
|
36
|
+
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
37
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
|
|
38
|
+
res.setHeader('Access-Control-Allow-Headers', req.headers['access-control-request-headers'] || 'Content-Type, Authorization');
|
|
39
|
+
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
|
40
|
+
res.setHeader('Access-Control-Max-Age', '86400');
|
|
41
|
+
return res.status(204).end();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const requestId = randomUUID();
|
|
45
|
+
const requestOrigin = req.headers.origin || null;
|
|
46
|
+
|
|
47
|
+
// Collect raw body
|
|
48
|
+
const bodyChunks = [];
|
|
49
|
+
req.on('data', chunk => bodyChunks.push(chunk));
|
|
50
|
+
req.on('end', async () => {
|
|
51
|
+
const body = Buffer.concat(bodyChunks);
|
|
52
|
+
|
|
53
|
+
// Build proxy path: strip /agent/:name/:port prefix
|
|
54
|
+
const proxyPath = req.params[0] ? ('/' + req.params[0]) : '/';
|
|
55
|
+
const queryString = req.url.includes('?') ? req.url.substring(req.url.indexOf('?')) : '';
|
|
56
|
+
|
|
57
|
+
// Clean headers for forwarding
|
|
58
|
+
const fwdHeaders = { ...req.headers };
|
|
59
|
+
delete fwdHeaders['host'];
|
|
60
|
+
const proxyHost = portConfig.host || 'localhost';
|
|
61
|
+
fwdHeaders['host'] = `${proxyHost}:${portNum}`;
|
|
62
|
+
delete fwdHeaders['connection'];
|
|
63
|
+
|
|
64
|
+
// Build publicOrigin so agent can rewrite localhost URLs in responses
|
|
65
|
+
const protocol = req.headers['x-forwarded-proto'] || req.protocol || 'https';
|
|
66
|
+
const publicOrigin = `${protocol}://${req.headers.host}`;
|
|
67
|
+
|
|
68
|
+
await sendToAgent(found.agent, {
|
|
69
|
+
type: 'proxy_request',
|
|
70
|
+
requestId,
|
|
71
|
+
port: portNum,
|
|
72
|
+
host: portConfig.host || 'localhost',
|
|
73
|
+
scheme: portConfig.scheme || 'http',
|
|
74
|
+
basePath: `/agent/${agentName}/${portNum}`,
|
|
75
|
+
publicOrigin,
|
|
76
|
+
method: req.method,
|
|
77
|
+
path: proxyPath + queryString,
|
|
78
|
+
headers: fwdHeaders,
|
|
79
|
+
body: body.length > 0 ? body.toString('base64') : null
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const timeout = setTimeout(() => {
|
|
83
|
+
pendingProxyRequests.delete(requestId);
|
|
84
|
+
if (!res.headersSent) res.status(504).send('Proxy timeout');
|
|
85
|
+
}, 60000);
|
|
86
|
+
|
|
87
|
+
pendingProxyRequests.set(requestId, { res, timeout, streaming: false, origin: requestOrigin });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
req.on('error', () => {
|
|
91
|
+
pendingProxyRequests.delete(requestId);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Overwrite CORS headers so browser accepts proxied responses
|
|
96
|
+
function applyCorsHeaders(headers, origin) {
|
|
97
|
+
if (origin) {
|
|
98
|
+
headers['access-control-allow-origin'] = origin;
|
|
99
|
+
headers['access-control-allow-credentials'] = 'true';
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Handle proxy response messages from agent
|
|
104
|
+
export function handleProxyResponse(msg) {
|
|
105
|
+
const pending = pendingProxyRequests.get(msg.requestId);
|
|
106
|
+
if (!pending) return;
|
|
107
|
+
clearTimeout(pending.timeout);
|
|
108
|
+
pendingProxyRequests.delete(msg.requestId);
|
|
109
|
+
|
|
110
|
+
const headers = msg.headers || {};
|
|
111
|
+
delete headers['transfer-encoding'];
|
|
112
|
+
delete headers['connection'];
|
|
113
|
+
applyCorsHeaders(headers, pending.origin);
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
pending.res.status(msg.statusCode || 500);
|
|
117
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
118
|
+
try { pending.res.setHeader(k, v); } catch(e) {}
|
|
119
|
+
}
|
|
120
|
+
if (msg.body) {
|
|
121
|
+
pending.res.end(Buffer.from(msg.body, 'base64'));
|
|
122
|
+
} else {
|
|
123
|
+
pending.res.end();
|
|
124
|
+
}
|
|
125
|
+
} catch(e) {
|
|
126
|
+
console.error('[Proxy] Error sending response:', e.message);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function handleProxyResponseChunk(msg) {
|
|
131
|
+
const pending = pendingProxyRequests.get(msg.requestId);
|
|
132
|
+
if (!pending) return;
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
if (!pending.streaming) {
|
|
136
|
+
pending.streaming = true;
|
|
137
|
+
clearTimeout(pending.timeout);
|
|
138
|
+
// Set a longer timeout for streaming responses
|
|
139
|
+
pending.timeout = setTimeout(() => {
|
|
140
|
+
pendingProxyRequests.delete(msg.requestId);
|
|
141
|
+
try { pending.res.end(); } catch(e) {}
|
|
142
|
+
}, 300000); // 5 minutes for streaming
|
|
143
|
+
|
|
144
|
+
pending.res.status(msg.statusCode || 200);
|
|
145
|
+
const headers = msg.headers || {};
|
|
146
|
+
delete headers['transfer-encoding'];
|
|
147
|
+
delete headers['connection'];
|
|
148
|
+
applyCorsHeaders(headers, pending.origin);
|
|
149
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
150
|
+
try { pending.res.setHeader(k, v); } catch(e) {}
|
|
151
|
+
}
|
|
152
|
+
pending.res.setHeader('X-Accel-Buffering', 'no');
|
|
153
|
+
pending.res.flushHeaders();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (msg.chunk) {
|
|
157
|
+
pending.res.write(Buffer.from(msg.chunk, 'base64'));
|
|
158
|
+
}
|
|
159
|
+
} catch(e) {
|
|
160
|
+
console.error('[Proxy] Error writing chunk:', e.message);
|
|
161
|
+
pendingProxyRequests.delete(msg.requestId);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function handleProxyResponseEnd(msg) {
|
|
166
|
+
const pending = pendingProxyRequests.get(msg.requestId);
|
|
167
|
+
if (!pending) return;
|
|
168
|
+
clearTimeout(pending.timeout);
|
|
169
|
+
pendingProxyRequests.delete(msg.requestId);
|
|
170
|
+
try { pending.res.end(); } catch(e) {}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Handle proxy WebSocket messages from agent to browser
|
|
174
|
+
export function handleProxyWsAgentMessage(msg) {
|
|
175
|
+
const conn = proxyWsConnections.get(msg.proxyWsId);
|
|
176
|
+
if (!conn || !conn.browserWs) return;
|
|
177
|
+
|
|
178
|
+
switch (msg.type) {
|
|
179
|
+
case 'proxy_ws_opened':
|
|
180
|
+
break;
|
|
181
|
+
case 'proxy_ws_message':
|
|
182
|
+
try {
|
|
183
|
+
if (msg.isBinary) {
|
|
184
|
+
conn.browserWs.send(Buffer.from(msg.data, 'base64'));
|
|
185
|
+
} else {
|
|
186
|
+
conn.browserWs.send(msg.data);
|
|
187
|
+
}
|
|
188
|
+
} catch(e) {}
|
|
189
|
+
break;
|
|
190
|
+
case 'proxy_ws_closed':
|
|
191
|
+
try { conn.browserWs.close(msg.code || 1000); } catch(e) {}
|
|
192
|
+
proxyWsConnections.delete(msg.proxyWsId);
|
|
193
|
+
break;
|
|
194
|
+
case 'proxy_ws_error':
|
|
195
|
+
try { conn.browserWs.close(1011, msg.error); } catch(e) {}
|
|
196
|
+
proxyWsConnections.delete(msg.proxyWsId);
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Handle WebSocket upgrade for proxy connections
|
|
202
|
+
export async function handleProxyWebSocketUpgrade(req, socket, head, match) {
|
|
203
|
+
const agentName = match[1];
|
|
204
|
+
const port = parseInt(match[2]);
|
|
205
|
+
const path = match[3] || '/';
|
|
206
|
+
|
|
207
|
+
const found = findAgentByName(agentName);
|
|
208
|
+
if (!found || found.agent.ws.readyState !== WebSocket.OPEN) {
|
|
209
|
+
socket.destroy();
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Check if port is enabled
|
|
214
|
+
const proxyPorts = found.agent.proxyPorts || [];
|
|
215
|
+
const portConfig = proxyPorts.find(p => p.port === port);
|
|
216
|
+
if (!portConfig || !portConfig.enabled) {
|
|
217
|
+
socket.destroy();
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Create a temporary WSS for this upgrade
|
|
222
|
+
const proxyWss = new WebSocketServer({ noServer: true });
|
|
223
|
+
proxyWss.handleUpgrade(req, socket, head, async (browserWs) => {
|
|
224
|
+
const proxyWsId = randomUUID();
|
|
225
|
+
|
|
226
|
+
proxyWsConnections.set(proxyWsId, {
|
|
227
|
+
browserWs,
|
|
228
|
+
agentId: found.id
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Forward browser messages to agent
|
|
232
|
+
browserWs.on('message', async (data, isBinary) => {
|
|
233
|
+
await sendToAgent(found.agent, {
|
|
234
|
+
type: 'proxy_ws_message',
|
|
235
|
+
proxyWsId,
|
|
236
|
+
data: isBinary ? data.toString('base64') : data.toString(),
|
|
237
|
+
isBinary
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
browserWs.on('close', async (code) => {
|
|
242
|
+
proxyWsConnections.delete(proxyWsId);
|
|
243
|
+
await sendToAgent(found.agent, {
|
|
244
|
+
type: 'proxy_ws_close',
|
|
245
|
+
proxyWsId,
|
|
246
|
+
code
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
browserWs.on('error', () => {
|
|
251
|
+
proxyWsConnections.delete(proxyWsId);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Tell agent to open WS connection to localhost:port
|
|
255
|
+
const fwdHeaders = { ...req.headers };
|
|
256
|
+
delete fwdHeaders['host'];
|
|
257
|
+
delete fwdHeaders['upgrade'];
|
|
258
|
+
delete fwdHeaders['connection'];
|
|
259
|
+
delete fwdHeaders['sec-websocket-key'];
|
|
260
|
+
delete fwdHeaders['sec-websocket-version'];
|
|
261
|
+
delete fwdHeaders['sec-websocket-extensions'];
|
|
262
|
+
|
|
263
|
+
await sendToAgent(found.agent, {
|
|
264
|
+
type: 'proxy_ws_open',
|
|
265
|
+
proxyWsId,
|
|
266
|
+
port,
|
|
267
|
+
host: portConfig.host || 'localhost',
|
|
268
|
+
scheme: portConfig.scheme || 'http',
|
|
269
|
+
path,
|
|
270
|
+
headers: fwdHeaders
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { WebSocket } from 'ws';
|
|
2
|
+
import { userStatsDb } from '../database.js';
|
|
3
|
+
import { agents, webClients, userStatsDeltas } from '../context.js';
|
|
4
|
+
|
|
5
|
+
const toNumber = (value) => Number(value) || 0;
|
|
6
|
+
|
|
7
|
+
function emptyAgentMetrics() {
|
|
8
|
+
return {
|
|
9
|
+
totalTurns: 0,
|
|
10
|
+
chatTurns: 0,
|
|
11
|
+
yeaftTurns: 0,
|
|
12
|
+
sessionsCreated: 0,
|
|
13
|
+
inputTokens: 0,
|
|
14
|
+
outputTokens: 0,
|
|
15
|
+
cacheReadTokens: 0,
|
|
16
|
+
cacheWriteTokens: 0,
|
|
17
|
+
totalTokens: 0,
|
|
18
|
+
lastUpdatedAt: null,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function safeAgentMetrics(metrics = {}) {
|
|
23
|
+
const out = emptyAgentMetrics();
|
|
24
|
+
for (const key of Object.keys(out)) {
|
|
25
|
+
if (key === 'lastUpdatedAt') continue;
|
|
26
|
+
out[key] = toNumber(metrics[key]);
|
|
27
|
+
}
|
|
28
|
+
out.lastUpdatedAt = metrics.lastUpdatedAt || null;
|
|
29
|
+
if (!out.totalTurns) out.totalTurns = out.chatTurns + out.yeaftTurns;
|
|
30
|
+
if (!out.totalTokens) out.totalTokens = out.inputTokens + out.outputTokens + out.cacheReadTokens + out.cacheWriteTokens;
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function sumAgentMetrics() {
|
|
35
|
+
const totals = emptyAgentMetrics();
|
|
36
|
+
for (const [, agent] of agents) {
|
|
37
|
+
const metrics = safeAgentMetrics(agent.metrics || {});
|
|
38
|
+
totals.totalTurns += metrics.totalTurns;
|
|
39
|
+
totals.chatTurns += metrics.chatTurns;
|
|
40
|
+
totals.yeaftTurns += metrics.yeaftTurns;
|
|
41
|
+
totals.sessionsCreated += metrics.sessionsCreated;
|
|
42
|
+
totals.inputTokens += metrics.inputTokens;
|
|
43
|
+
totals.outputTokens += metrics.outputTokens;
|
|
44
|
+
totals.cacheReadTokens += metrics.cacheReadTokens;
|
|
45
|
+
totals.cacheWriteTokens += metrics.cacheWriteTokens;
|
|
46
|
+
totals.totalTokens += metrics.totalTokens;
|
|
47
|
+
if (metrics.lastUpdatedAt && (!totals.lastUpdatedAt || metrics.lastUpdatedAt > totals.lastUpdatedAt)) {
|
|
48
|
+
totals.lastUpdatedAt = metrics.lastUpdatedAt;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return totals;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function mergePendingUserStats(stats) {
|
|
55
|
+
const byUser = new Map(stats.map(row => [row.user_id, { ...row }]));
|
|
56
|
+
for (const [userId, delta] of userStatsDeltas) {
|
|
57
|
+
const row = byUser.get(userId);
|
|
58
|
+
if (!row) {
|
|
59
|
+
byUser.set(userId, {
|
|
60
|
+
user_id: userId,
|
|
61
|
+
username: userId,
|
|
62
|
+
display_name: userId,
|
|
63
|
+
role: 'pro',
|
|
64
|
+
last_login_at: null,
|
|
65
|
+
updated_at: Date.now(),
|
|
66
|
+
message_count: toNumber(delta.messages),
|
|
67
|
+
session_count: toNumber(delta.sessions),
|
|
68
|
+
request_count: toNumber(delta.requests),
|
|
69
|
+
bytes_sent: toNumber(delta.bytesSent),
|
|
70
|
+
bytes_received: toNumber(delta.bytesReceived),
|
|
71
|
+
});
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
row.message_count = toNumber(row.message_count) + toNumber(delta.messages);
|
|
75
|
+
row.session_count = toNumber(row.session_count) + toNumber(delta.sessions);
|
|
76
|
+
row.request_count = toNumber(row.request_count) + toNumber(delta.requests);
|
|
77
|
+
row.bytes_sent = toNumber(row.bytes_sent) + toNumber(delta.bytesSent);
|
|
78
|
+
row.bytes_received = toNumber(row.bytes_received) + toNumber(delta.bytesReceived);
|
|
79
|
+
}
|
|
80
|
+
return Array.from(byUser.values());
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function pendingTodayMessages() {
|
|
84
|
+
let count = 0;
|
|
85
|
+
for (const delta of userStatsDeltas.values()) count += toNumber(delta.messages);
|
|
86
|
+
return count;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Register admin-only REST API routes for the dashboard.
|
|
91
|
+
*/
|
|
92
|
+
export function registerAdminRoutes(app, { requireAuth, requireAdmin }) {
|
|
93
|
+
// GET /api/admin/dashboard — aggregated overview
|
|
94
|
+
app.get('/api/admin/dashboard', requireAuth, requireAdmin, (req, res) => {
|
|
95
|
+
try {
|
|
96
|
+
const totals = userStatsDb.getDashboardTotals();
|
|
97
|
+
const onlineUsers = new Set();
|
|
98
|
+
for (const [, client] of webClients) {
|
|
99
|
+
if (client.authenticated && client.userId) {
|
|
100
|
+
onlineUsers.add(client.userId);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
let onlineAgents = 0;
|
|
104
|
+
for (const [, agent] of agents) {
|
|
105
|
+
if (agent.ws.readyState === WebSocket.OPEN) onlineAgents++;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const agentMetrics = sumAgentMetrics();
|
|
109
|
+
const tokenTotals = userStatsDb.getDashboardTokenTotals();
|
|
110
|
+
res.json({
|
|
111
|
+
totalUsers: totals.total_users,
|
|
112
|
+
totalSessions: totals.total_sessions,
|
|
113
|
+
totalMessages: totals.total_messages,
|
|
114
|
+
onlineUsers: onlineUsers.size,
|
|
115
|
+
onlineAgents,
|
|
116
|
+
todayActiveUsers: userStatsDb.getTodayActiveUsers(),
|
|
117
|
+
todayMessages: userStatsDb.getTodayMessages() + pendingTodayMessages(),
|
|
118
|
+
agentMetrics,
|
|
119
|
+
totalAgentTurns: agentMetrics.totalTurns,
|
|
120
|
+
totalTokens: toNumber(tokenTotals.total_tokens)
|
|
121
|
+
});
|
|
122
|
+
} catch (e) {
|
|
123
|
+
console.error('[Admin] Dashboard error:', e.message);
|
|
124
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// GET /api/admin/user-stats — per-user stats list (supports ?period=today|week|month|all)
|
|
129
|
+
app.get('/api/admin/user-stats', requireAuth, requireAdmin, (req, res) => {
|
|
130
|
+
try {
|
|
131
|
+
const period = req.query.period || 'all';
|
|
132
|
+
const validPeriods = ['today', 'week', 'month', 'all'];
|
|
133
|
+
const stats = mergePendingUserStats(userStatsDb.getByPeriod(validPeriods.includes(period) ? period : 'all'));
|
|
134
|
+
res.json(stats.map(s => ({
|
|
135
|
+
userId: s.user_id,
|
|
136
|
+
username: s.username,
|
|
137
|
+
displayName: s.display_name,
|
|
138
|
+
role: s.role,
|
|
139
|
+
messageCount: s.message_count,
|
|
140
|
+
sessionCount: s.session_count,
|
|
141
|
+
requestCount: s.request_count,
|
|
142
|
+
bytesSent: s.bytes_sent,
|
|
143
|
+
bytesReceived: s.bytes_received,
|
|
144
|
+
inputTokens: s.input_tokens,
|
|
145
|
+
outputTokens: s.output_tokens,
|
|
146
|
+
cacheReadTokens: s.cache_read_tokens,
|
|
147
|
+
cacheWriteTokens: s.cache_write_tokens,
|
|
148
|
+
totalTokens: s.total_tokens,
|
|
149
|
+
lastLoginAt: s.last_login_at,
|
|
150
|
+
updatedAt: s.updated_at
|
|
151
|
+
})));
|
|
152
|
+
} catch (e) {
|
|
153
|
+
console.error('[Admin] User stats error:', e.message);
|
|
154
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// GET /api/admin/agents — full agent list (no owner filtering)
|
|
159
|
+
app.get('/api/admin/agents', requireAuth, requireAdmin, (req, res) => {
|
|
160
|
+
try {
|
|
161
|
+
const agentList = Array.from(agents.entries()).map(([id, agent]) => ({
|
|
162
|
+
id,
|
|
163
|
+
name: agent.name,
|
|
164
|
+
workDir: agent.workDir,
|
|
165
|
+
online: agent.ws.readyState === WebSocket.OPEN,
|
|
166
|
+
status: agent.status || 'ready',
|
|
167
|
+
latency: agent.latency || null,
|
|
168
|
+
version: agent.version || null,
|
|
169
|
+
ownerId: agent.ownerId || null,
|
|
170
|
+
ownerUsername: agent.ownerUsername || null,
|
|
171
|
+
capabilities: agent.capabilities || [],
|
|
172
|
+
conversationCount: agent.conversations?.size || 0,
|
|
173
|
+
metrics: safeAgentMetrics(agent.metrics || {}),
|
|
174
|
+
metricsUpdatedAt: agent.metricsUpdatedAt || null
|
|
175
|
+
}));
|
|
176
|
+
res.json(agentList);
|
|
177
|
+
} catch (e) {
|
|
178
|
+
console.error('[Admin] Agents error:', e.message);
|
|
179
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// GET /api/admin/online-users — currently connected web clients
|
|
184
|
+
app.get('/api/admin/online-users', requireAuth, requireAdmin, (req, res) => {
|
|
185
|
+
try {
|
|
186
|
+
// Deduplicate by userId, keep the most active connection
|
|
187
|
+
const userMap = new Map();
|
|
188
|
+
for (const [, client] of webClients) {
|
|
189
|
+
if (!client.authenticated || !client.userId) continue;
|
|
190
|
+
const existing = userMap.get(client.userId);
|
|
191
|
+
if (!existing || client.currentAgent) {
|
|
192
|
+
userMap.set(client.userId, {
|
|
193
|
+
userId: client.userId,
|
|
194
|
+
username: client.username,
|
|
195
|
+
role: client.role,
|
|
196
|
+
currentAgent: client.currentAgent || null,
|
|
197
|
+
currentAgentName: client.currentAgent ? (agents.get(client.currentAgent)?.name || null) : null
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
res.json(Array.from(userMap.values()));
|
|
202
|
+
} catch (e) {
|
|
203
|
+
console.error('[Admin] Online users error:', e.message);
|
|
204
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|