opc-agent 3.0.1 → 4.0.1
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/README.md +404 -74
- package/README.zh-CN.md +82 -0
- package/dist/channels/dingtalk.d.ts +17 -0
- package/dist/channels/dingtalk.js +38 -0
- package/dist/channels/googlechat.d.ts +14 -0
- package/dist/channels/googlechat.js +37 -0
- package/dist/channels/imessage.d.ts +13 -0
- package/dist/channels/imessage.js +28 -0
- package/dist/channels/irc.d.ts +20 -0
- package/dist/channels/irc.js +71 -0
- package/dist/channels/line.d.ts +14 -0
- package/dist/channels/line.js +28 -0
- package/dist/channels/matrix.d.ts +15 -0
- package/dist/channels/matrix.js +28 -0
- package/dist/channels/mattermost.d.ts +18 -0
- package/dist/channels/mattermost.js +49 -0
- package/dist/channels/msteams.d.ts +14 -0
- package/dist/channels/msteams.js +28 -0
- package/dist/channels/nostr.d.ts +14 -0
- package/dist/channels/nostr.js +28 -0
- package/dist/channels/qq.d.ts +15 -0
- package/dist/channels/qq.js +28 -0
- package/dist/channels/signal.d.ts +14 -0
- package/dist/channels/signal.js +28 -0
- package/dist/channels/sms.d.ts +15 -0
- package/dist/channels/sms.js +28 -0
- package/dist/channels/twitch.d.ts +17 -0
- package/dist/channels/twitch.js +59 -0
- package/dist/channels/voice-call.d.ts +27 -0
- package/dist/channels/voice-call.js +82 -0
- package/dist/channels/whatsapp.d.ts +14 -0
- package/dist/channels/whatsapp.js +28 -0
- package/dist/cli/chat.d.ts +2 -0
- package/dist/cli/chat.js +134 -0
- package/dist/cli/setup.d.ts +4 -0
- package/dist/cli/setup.js +303 -0
- package/dist/cli.js +142 -6
- package/dist/core/api-server.d.ts +25 -0
- package/dist/core/api-server.js +286 -0
- package/dist/core/audio.d.ts +50 -0
- package/dist/core/audio.js +68 -0
- package/dist/core/context-discovery.d.ts +16 -0
- package/dist/core/context-discovery.js +107 -0
- package/dist/core/context-refs.d.ts +29 -0
- package/dist/core/context-refs.js +162 -0
- package/dist/core/gateway.d.ts +53 -0
- package/dist/core/gateway.js +80 -0
- package/dist/core/heartbeat.d.ts +19 -0
- package/dist/core/heartbeat.js +50 -0
- package/dist/core/hooks.d.ts +28 -0
- package/dist/core/hooks.js +82 -0
- package/dist/core/ide-bridge.d.ts +53 -0
- package/dist/core/ide-bridge.js +97 -0
- package/dist/core/node-network.d.ts +23 -0
- package/dist/core/node-network.js +77 -0
- package/dist/core/profiles.d.ts +27 -0
- package/dist/core/profiles.js +131 -0
- package/dist/core/sandbox.d.ts +25 -0
- package/dist/core/sandbox.js +84 -1
- package/dist/core/session-manager.d.ts +33 -0
- package/dist/core/session-manager.js +157 -0
- package/dist/core/vision.d.ts +45 -0
- package/dist/core/vision.js +177 -0
- package/dist/hub/brain-seed.d.ts +14 -0
- package/dist/hub/brain-seed.js +77 -0
- package/dist/hub/client.d.ts +25 -0
- package/dist/hub/client.js +44 -0
- package/dist/index.d.ts +66 -1
- package/dist/index.js +95 -3
- package/dist/memory/context-compressor.d.ts +43 -0
- package/dist/memory/context-compressor.js +167 -0
- package/dist/memory/index.d.ts +4 -0
- package/dist/memory/index.js +5 -1
- package/dist/memory/user-profiler.d.ts +50 -0
- package/dist/memory/user-profiler.js +201 -0
- package/dist/providers/index.d.ts +1 -1
- package/dist/providers/index.js +54 -1
- package/dist/scheduler/cron-engine.d.ts +41 -0
- package/dist/scheduler/cron-engine.js +200 -0
- package/dist/scheduler/index.d.ts +3 -0
- package/dist/scheduler/index.js +7 -0
- package/dist/schema/oad.d.ts +12 -12
- package/dist/security/approvals.d.ts +53 -0
- package/dist/security/approvals.js +115 -0
- package/dist/security/elevated.d.ts +41 -0
- package/dist/security/elevated.js +89 -0
- package/dist/security/index.d.ts +6 -0
- package/dist/security/index.js +7 -1
- package/dist/security/secrets.d.ts +34 -0
- package/dist/security/secrets.js +115 -0
- package/dist/skills/builtin/index.d.ts +6 -0
- package/dist/skills/builtin/index.js +402 -0
- package/dist/skills/marketplace.d.ts +30 -0
- package/dist/skills/marketplace.js +142 -0
- package/dist/skills/types.d.ts +34 -0
- package/dist/skills/types.js +16 -0
- package/dist/studio/server.d.ts +25 -0
- package/dist/studio/server.js +780 -0
- package/dist/studio/templates-data.d.ts +21 -0
- package/dist/studio/templates-data.js +148 -0
- package/dist/studio-ui/index.html +2502 -1073
- package/dist/tools/builtin/browser.d.ts +47 -0
- package/dist/tools/builtin/browser.js +284 -0
- package/dist/tools/builtin/home-assistant.d.ts +12 -0
- package/dist/tools/builtin/home-assistant.js +126 -0
- package/dist/tools/builtin/index.d.ts +7 -1
- package/dist/tools/builtin/index.js +23 -2
- package/dist/tools/builtin/rl-tools.d.ts +13 -0
- package/dist/tools/builtin/rl-tools.js +228 -0
- package/dist/tools/builtin/vision.d.ts +6 -0
- package/dist/tools/builtin/vision.js +61 -0
- package/dist/tools/builtin/web-search.d.ts +9 -0
- package/dist/tools/builtin/web-search.js +150 -0
- package/dist/tools/document-processor.d.ts +39 -0
- package/dist/tools/document-processor.js +188 -0
- package/dist/tools/image-generator.d.ts +42 -0
- package/dist/tools/image-generator.js +136 -0
- package/dist/tools/web-scraper.d.ts +20 -0
- package/dist/tools/web-scraper.js +148 -0
- package/dist/tools/web-search.d.ts +51 -0
- package/dist/tools/web-search.js +152 -0
- package/install.ps1 +154 -0
- package/install.sh +164 -0
- package/package.json +63 -52
- package/src/channels/dingtalk.ts +46 -0
- package/src/channels/googlechat.ts +42 -0
- package/src/channels/imessage.ts +32 -0
- package/src/channels/irc.ts +82 -0
- package/src/channels/line.ts +33 -0
- package/src/channels/matrix.ts +34 -0
- package/src/channels/mattermost.ts +57 -0
- package/src/channels/msteams.ts +33 -0
- package/src/channels/nostr.ts +33 -0
- package/src/channels/qq.ts +34 -0
- package/src/channels/signal.ts +33 -0
- package/src/channels/sms.ts +34 -0
- package/src/channels/twitch.ts +65 -0
- package/src/channels/voice-call.ts +100 -0
- package/src/channels/whatsapp.ts +33 -0
- package/src/cli/chat.ts +99 -0
- package/src/cli/setup.ts +314 -0
- package/src/cli.ts +148 -6
- package/src/core/api-server.ts +277 -0
- package/src/core/audio.ts +98 -0
- package/src/core/context-discovery.ts +85 -0
- package/src/core/context-refs.ts +140 -0
- package/src/core/gateway.ts +106 -0
- package/src/core/heartbeat.ts +51 -0
- package/src/core/hooks.ts +105 -0
- package/src/core/ide-bridge.ts +133 -0
- package/src/core/node-network.ts +86 -0
- package/src/core/profiles.ts +122 -0
- package/src/core/sandbox.ts +100 -0
- package/src/core/session-manager.ts +137 -0
- package/src/core/vision.ts +180 -0
- package/src/hub/brain-seed.ts +54 -0
- package/src/hub/client.ts +60 -0
- package/src/index.ts +86 -1
- package/src/memory/context-compressor.ts +189 -0
- package/src/memory/index.ts +4 -0
- package/src/memory/user-profiler.ts +215 -0
- package/src/providers/index.ts +64 -1
- package/src/scheduler/cron-engine.ts +191 -0
- package/src/scheduler/index.ts +2 -0
- package/src/security/approvals.ts +143 -0
- package/src/security/elevated.ts +105 -0
- package/src/security/index.ts +6 -0
- package/src/security/secrets.ts +129 -0
- package/src/skills/builtin/index.ts +408 -0
- package/src/skills/marketplace.ts +113 -0
- package/src/skills/types.ts +42 -0
- package/src/studio/server.ts +1591 -791
- package/src/studio/templates-data.ts +178 -0
- package/src/studio-ui/index.html +2502 -1073
- package/src/tools/builtin/browser.ts +299 -0
- package/src/tools/builtin/home-assistant.ts +116 -0
- package/src/tools/builtin/index.ts +37 -28
- package/src/tools/builtin/rl-tools.ts +243 -0
- package/src/tools/builtin/vision.ts +64 -0
- package/src/tools/builtin/web-search.ts +126 -0
- package/src/tools/document-processor.ts +213 -0
- package/src/tools/image-generator.ts +150 -0
- package/src/tools/web-scraper.ts +179 -0
- package/src/tools/web-search.ts +180 -0
- package/tests/api-server.test.ts +148 -0
- package/tests/approvals.test.ts +89 -0
- package/tests/audio.test.ts +40 -0
- package/tests/browser.test.ts +179 -0
- package/tests/builtin-tools.test.ts +83 -83
- package/tests/channels-extra.test.ts +45 -0
- package/tests/context-compressor.test.ts +172 -0
- package/tests/context-refs.test.ts +121 -0
- package/tests/cron-engine.test.ts +101 -0
- package/tests/document-processor.test.ts +69 -0
- package/tests/e2e-nocode.test.ts +442 -0
- package/tests/elevated.test.ts +69 -0
- package/tests/gateway.test.ts +63 -71
- package/tests/home-assistant.test.ts +40 -0
- package/tests/hooks.test.ts +79 -0
- package/tests/ide-bridge.test.ts +38 -0
- package/tests/image-generator.test.ts +84 -0
- package/tests/node-network.test.ts +74 -0
- package/tests/profiles.test.ts +61 -0
- package/tests/rl-tools.test.ts +93 -0
- package/tests/sandbox-manager.test.ts +46 -0
- package/tests/secrets.test.ts +107 -0
- package/tests/settings-api.test.ts +148 -0
- package/tests/setup.test.ts +73 -0
- package/tests/studio.test.ts +402 -229
- package/tests/tools/builtin-extended.test.ts +138 -138
- package/tests/user-profiler.test.ts +169 -0
- package/tests/v090-features.test.ts +254 -0
- package/tests/vision.test.ts +61 -0
- package/tests/voice-call.test.ts +47 -0
- package/tests/voice-interaction.test.ts +38 -0
- package/tests/web-search.test.ts +155 -0
package/src/studio/server.ts
CHANGED
|
@@ -1,791 +1,1591 @@
|
|
|
1
|
-
import { createServer, IncomingMessage, ServerResponse, request as httpRequest } from 'http';
|
|
2
|
-
import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'fs';
|
|
3
|
-
import { join, extname } from 'path';
|
|
4
|
-
import * as
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
interface
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
);
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
return
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
const
|
|
770
|
-
const
|
|
771
|
-
|
|
772
|
-
const
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
|
|
1
|
+
import { createServer, IncomingMessage, ServerResponse, request as httpRequest } from 'http';
|
|
2
|
+
import { readFileSync, existsSync, writeFileSync, mkdirSync, readdirSync, unlinkSync } from 'fs';
|
|
3
|
+
import { join, extname } from 'path';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
import * as net from 'net';
|
|
6
|
+
import { Tracer } from '../telemetry';
|
|
7
|
+
import { TEMPLATES, INDUSTRIES, AgentTemplate } from './templates-data';
|
|
8
|
+
import { SkillMarketplace } from '../skills/marketplace';
|
|
9
|
+
import { CronEngine } from '../scheduler/cron-engine';
|
|
10
|
+
import { ImageGenerator } from '../tools/image-generator';
|
|
11
|
+
import { DocumentProcessor, ProcessedDocument } from '../tools/document-processor';
|
|
12
|
+
|
|
13
|
+
export interface WorkflowNode {
|
|
14
|
+
id: string;
|
|
15
|
+
type: 'agent' | 'tool' | 'condition' | 'loop' | 'parallel' | 'input' | 'output';
|
|
16
|
+
name: string;
|
|
17
|
+
x: number;
|
|
18
|
+
y: number;
|
|
19
|
+
config: Record<string, any>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface WorkflowEdge {
|
|
23
|
+
id: string;
|
|
24
|
+
from: string;
|
|
25
|
+
to: string;
|
|
26
|
+
fromPort: string;
|
|
27
|
+
toPort: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface WorkflowDefinition {
|
|
31
|
+
id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
nodes: WorkflowNode[];
|
|
34
|
+
edges: WorkflowEdge[];
|
|
35
|
+
created: string;
|
|
36
|
+
updated: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface StudioConfig {
|
|
40
|
+
port: number;
|
|
41
|
+
agentDir: string;
|
|
42
|
+
staticDir: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface ModuleInfo {
|
|
46
|
+
name: string;
|
|
47
|
+
path: string;
|
|
48
|
+
port: number;
|
|
49
|
+
icon: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const MODULE_REGISTRY: ModuleInfo[] = [
|
|
53
|
+
{ name: 'DeepBrain', path: 'brain', port: 4001, icon: '🧠' },
|
|
54
|
+
{ name: 'AgentKits', path: 'kits', port: 4002, icon: '📊' },
|
|
55
|
+
{ name: 'Workstation', path: 'workstation', port: 4003, icon: '👤' },
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
// Settings config helpers
|
|
59
|
+
function getSettingsConfigPath(): string {
|
|
60
|
+
const dir = join(os.homedir(), '.opc');
|
|
61
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
62
|
+
return join(dir, 'config.json');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function loadSettingsConfig(): any {
|
|
66
|
+
const p = getSettingsConfigPath();
|
|
67
|
+
if (existsSync(p)) {
|
|
68
|
+
try { return JSON.parse(readFileSync(p, 'utf-8')); } catch { return {}; }
|
|
69
|
+
}
|
|
70
|
+
return {};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function saveSettingsConfig(config: any): void {
|
|
74
|
+
writeFileSync(getSettingsConfigPath(), JSON.stringify(config, null, 2));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
class StudioServer {
|
|
78
|
+
private server: any;
|
|
79
|
+
private config: StudioConfig;
|
|
80
|
+
private tracer?: Tracer;
|
|
81
|
+
private skillMarketplace: SkillMarketplace;
|
|
82
|
+
private cronEngine: CronEngine;
|
|
83
|
+
private imageGenerator: ImageGenerator;
|
|
84
|
+
|
|
85
|
+
constructor(config: Partial<StudioConfig> = {}) {
|
|
86
|
+
this.config = {
|
|
87
|
+
port: config.port || 4000,
|
|
88
|
+
agentDir: config.agentDir || process.cwd(),
|
|
89
|
+
staticDir: config.staticDir || join(__dirname, '../studio-ui'),
|
|
90
|
+
};
|
|
91
|
+
this.cronEngine = new CronEngine();
|
|
92
|
+
this.imageGenerator = new ImageGenerator();
|
|
93
|
+
this.skillMarketplace = new SkillMarketplace();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
setTracer(tracer: Tracer): void {
|
|
97
|
+
this.tracer = tracer;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
getTracer(): Tracer | undefined {
|
|
101
|
+
return this.tracer;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
getConfig(): StudioConfig {
|
|
105
|
+
return { ...this.config };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async start(): Promise<void> {
|
|
109
|
+
const opcDir = join(os.homedir(), '.opc');
|
|
110
|
+
if (!existsSync(opcDir)) mkdirSync(opcDir, { recursive: true });
|
|
111
|
+
const cfgPath = join(opcDir, 'config.json');
|
|
112
|
+
if (!existsSync(cfgPath)) writeFileSync(cfgPath, JSON.stringify({}, null, 2));
|
|
113
|
+
|
|
114
|
+
this.server = createServer((req, res) => this.handleRequest(req, res));
|
|
115
|
+
this.server.listen(this.config.port);
|
|
116
|
+
this.cronEngine.start();
|
|
117
|
+
console.log(`🎨 OPC Studio: http://localhost:${this.config.port}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async stop(): Promise<void> {
|
|
121
|
+
this.cronEngine.stop();
|
|
122
|
+
return new Promise((resolve) => {
|
|
123
|
+
if (this.server) {
|
|
124
|
+
this.server.close(() => resolve());
|
|
125
|
+
} else {
|
|
126
|
+
resolve();
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async handleRequest(req: IncomingMessage, res: ServerResponse) {
|
|
132
|
+
const url = new URL(req.url || '/', `http://localhost`);
|
|
133
|
+
|
|
134
|
+
// Handle CORS preflight
|
|
135
|
+
if (req.method === 'OPTIONS') {
|
|
136
|
+
res.writeHead(204, {
|
|
137
|
+
'Access-Control-Allow-Origin': '*',
|
|
138
|
+
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
|
139
|
+
'Access-Control-Allow-Headers': 'Content-Type',
|
|
140
|
+
});
|
|
141
|
+
res.end();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// API routes
|
|
146
|
+
if (url.pathname.startsWith('/api/')) {
|
|
147
|
+
return this.handleAPI(req, res, url);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Module proxy routes
|
|
151
|
+
for (const mod of MODULE_REGISTRY) {
|
|
152
|
+
if (url.pathname.startsWith(`/${mod.path}/`) || url.pathname === `/${mod.path}`) {
|
|
153
|
+
return this.proxyToModule(req, res, mod, url);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Static files
|
|
158
|
+
return this.serveStatic(req, res, url);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private async handleAPI(req: IncomingMessage, res: ServerResponse, url: URL) {
|
|
162
|
+
const route = url.pathname.replace('/api/', '');
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
let data: any;
|
|
166
|
+
|
|
167
|
+
// Dynamic agent routes
|
|
168
|
+
if (route === 'agents' && req.method === 'POST') {
|
|
169
|
+
data = await this.createAgent(req);
|
|
170
|
+
res.writeHead(201, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
171
|
+
res.end(JSON.stringify(data));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (route === 'agents' && req.method === 'GET') {
|
|
175
|
+
data = this.listAgents();
|
|
176
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
177
|
+
res.end(JSON.stringify(data));
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (route === 'templates' && req.method === 'GET') {
|
|
181
|
+
const industry = url.searchParams.get('industry') || '';
|
|
182
|
+
const search = url.searchParams.get('q') || '';
|
|
183
|
+
data = this.getTemplates(industry, search);
|
|
184
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
185
|
+
res.end(JSON.stringify(data));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (route.match(/^templates\/[^/]+$/) && req.method === 'GET') {
|
|
189
|
+
const tplId = route.split('/')[1];
|
|
190
|
+
data = this.getTemplateById(tplId);
|
|
191
|
+
res.writeHead(data.error ? 404 : 200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
192
|
+
res.end(JSON.stringify(data));
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (route.match(/^agents\/[^/]+\/memory$/) && req.method === 'GET') {
|
|
196
|
+
const agentId = route.split('/')[1];
|
|
197
|
+
data = this.getAgentMemory(agentId);
|
|
198
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
199
|
+
res.end(JSON.stringify(data));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (route.match(/^agents\/[^/]+\/chat$/) && req.method === 'POST') {
|
|
203
|
+
const agentId = route.split('/')[1];
|
|
204
|
+
return this.handleAgentChat(req, res, agentId);
|
|
205
|
+
}
|
|
206
|
+
if (route.match(/^agents\/[^/]+$/) && req.method === 'GET') {
|
|
207
|
+
const agentId = route.split('/')[1];
|
|
208
|
+
data = this.getAgentById(agentId);
|
|
209
|
+
res.writeHead(data.error ? 404 : 200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
210
|
+
res.end(JSON.stringify(data));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (route.match(/^agents\/[^/]+$/) && req.method === 'PUT') {
|
|
214
|
+
const agentId = route.split('/')[1];
|
|
215
|
+
data = await this.updateAgent(agentId, req);
|
|
216
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
217
|
+
res.end(JSON.stringify(data));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (route.match(/^agents\/[^/]+$/) && req.method === 'DELETE') {
|
|
221
|
+
const agentId = route.split('/')[1];
|
|
222
|
+
data = this.deleteAgent(agentId);
|
|
223
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
224
|
+
res.end(JSON.stringify(data));
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// --- Document upload routes ---
|
|
229
|
+
if (route.match(/^agents\/[^/]+\/upload$/) && req.method === 'POST') {
|
|
230
|
+
const agentId = route.split('/')[1];
|
|
231
|
+
return this.handleDocumentUpload(req, res, agentId);
|
|
232
|
+
}
|
|
233
|
+
if (route.match(/^agents\/[^/]+\/documents$/) && req.method === 'GET') {
|
|
234
|
+
const agentId = route.split('/')[1];
|
|
235
|
+
data = this.getDocumentList(agentId);
|
|
236
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
237
|
+
res.end(JSON.stringify(data));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (route.match(/^agents\/[^/]+\/documents\/[^/]+$/) && req.method === 'DELETE') {
|
|
241
|
+
const parts = route.split('/');
|
|
242
|
+
const agentId = parts[1];
|
|
243
|
+
const docId = parts[3];
|
|
244
|
+
data = this.deleteDocument(agentId, docId);
|
|
245
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
246
|
+
res.end(JSON.stringify(data));
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// --- Settings API routes ---
|
|
251
|
+
if (route === 'settings/models' && req.method === 'GET') {
|
|
252
|
+
const cfg = loadSettingsConfig();
|
|
253
|
+
data = cfg.models || { mode: 'local', provider: 'ollama', chatModel: 'qwen2.5:7b', embeddingModel: 'nomic-embed-text', providers: {} };
|
|
254
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
255
|
+
res.end(JSON.stringify(data));
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (route === 'settings/models' && req.method === 'PUT') {
|
|
259
|
+
const body = JSON.parse(await this.readBody(req));
|
|
260
|
+
const cfg = loadSettingsConfig();
|
|
261
|
+
cfg.models = { ...(cfg.models || {}), ...body };
|
|
262
|
+
saveSettingsConfig(cfg);
|
|
263
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
264
|
+
res.end(JSON.stringify({ success: true, models: cfg.models }));
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (route === 'settings/models/test' && req.method === 'POST') {
|
|
268
|
+
const body = JSON.parse(await this.readBody(req));
|
|
269
|
+
const { provider, apiKey, baseUrl } = body;
|
|
270
|
+
data = await this.testModelConnection(provider, apiKey, baseUrl);
|
|
271
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
272
|
+
res.end(JSON.stringify(data));
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (route === 'settings/models/local' && req.method === 'GET') {
|
|
276
|
+
data = await this.detectLocalOllama();
|
|
277
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
278
|
+
res.end(JSON.stringify(data));
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (route === 'settings/channels' && req.method === 'GET') {
|
|
282
|
+
const cfg = loadSettingsConfig();
|
|
283
|
+
data = cfg.channels || {};
|
|
284
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
285
|
+
res.end(JSON.stringify(data));
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (route.match(/^settings\/channels\/[^/]+$/) && req.method === 'PUT') {
|
|
289
|
+
const channelName = route.split('/')[2];
|
|
290
|
+
const body = JSON.parse(await this.readBody(req));
|
|
291
|
+
const cfg = loadSettingsConfig();
|
|
292
|
+
if (!cfg.channels) cfg.channels = {};
|
|
293
|
+
cfg.channels[channelName] = { ...(cfg.channels[channelName] || {}), ...body, updated: new Date().toISOString() };
|
|
294
|
+
saveSettingsConfig(cfg);
|
|
295
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
296
|
+
res.end(JSON.stringify({ success: true, channel: cfg.channels[channelName] }));
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
// Web Search settings
|
|
300
|
+
if (route === 'settings/search' && req.method === 'GET') {
|
|
301
|
+
const cfg = loadSettingsConfig();
|
|
302
|
+
data = cfg.webSearch || { defaultEngine: 'duckduckgo', enabled: true, engines: { duckduckgo: { enabled: true } } };
|
|
303
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
304
|
+
res.end(JSON.stringify(data));
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (route === 'settings/search' && req.method === 'PUT') {
|
|
308
|
+
const body = JSON.parse(await this.readBody(req));
|
|
309
|
+
const cfg = loadSettingsConfig();
|
|
310
|
+
cfg.webSearch = { ...(cfg.webSearch || {}), ...body, updated: new Date().toISOString() };
|
|
311
|
+
saveSettingsConfig(cfg);
|
|
312
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
313
|
+
res.end(JSON.stringify({ success: true, config: cfg.webSearch }));
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (route === 'settings/search/test' && req.method === 'POST') {
|
|
317
|
+
try {
|
|
318
|
+
const { webSearch: doSearch } = require('../tools/web-search');
|
|
319
|
+
const body = JSON.parse(await this.readBody(req));
|
|
320
|
+
const query = body.query || 'test search';
|
|
321
|
+
const cfg = loadSettingsConfig();
|
|
322
|
+
const searchCfg = { ...(cfg.webSearch || { defaultEngine: 'duckduckgo', enabled: true, engines: { duckduckgo: { enabled: true } } }), ...body.config };
|
|
323
|
+
const results = await doSearch(query, searchCfg, { maxResults: 3 });
|
|
324
|
+
data = { success: true, results, engine: searchCfg.defaultEngine };
|
|
325
|
+
} catch (e: any) {
|
|
326
|
+
data = { success: false, error: e.message };
|
|
327
|
+
}
|
|
328
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
329
|
+
res.end(JSON.stringify(data));
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (route === 'settings/status' && req.method === 'GET') {
|
|
333
|
+
data = await this.getSettingsStatus();
|
|
334
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
335
|
+
res.end(JSON.stringify(data));
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (route === 'settings/status/start' && req.method === 'POST') {
|
|
339
|
+
data = { success: true, status: 'running', message: 'Agent started' };
|
|
340
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
341
|
+
res.end(JSON.stringify(data));
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (route === 'settings/status/stop' && req.method === 'POST') {
|
|
345
|
+
data = { success: true, status: 'stopped', message: 'Agent stopped' };
|
|
346
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
347
|
+
res.end(JSON.stringify(data));
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (route === 'settings/usage' && req.method === 'GET') {
|
|
351
|
+
data = await this.getUsageStats();
|
|
352
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
353
|
+
res.end(JSON.stringify(data));
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Dynamic workflow routes (parameterized)
|
|
358
|
+
if (route.match(/^workflows\/[^/]+\/run$/) && req.method === 'POST') {
|
|
359
|
+
const wfId = route.split('/')[1];
|
|
360
|
+
data = await this.runWorkflow(wfId);
|
|
361
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
362
|
+
res.end(JSON.stringify(data));
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (route.match(/^workflows\/[^/]+$/) && req.method === 'GET') {
|
|
366
|
+
const wfId = route.split('/')[1];
|
|
367
|
+
data = this.getWorkflowById(wfId);
|
|
368
|
+
res.writeHead(data.error ? 404 : 200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
369
|
+
res.end(JSON.stringify(data));
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
if (route.match(/^workflows\/[^/]+$/) && req.method === 'DELETE') {
|
|
373
|
+
const wfId = route.split('/')[1];
|
|
374
|
+
data = this.deleteWorkflow(wfId);
|
|
375
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
376
|
+
res.end(JSON.stringify(data));
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// --- Schedules API ---
|
|
381
|
+
if (route === 'schedules' && req.method === 'GET') {
|
|
382
|
+
data = this.cronEngine.listTasks();
|
|
383
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
384
|
+
res.end(JSON.stringify(data));
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (route === 'schedules' && req.method === 'POST') {
|
|
388
|
+
const body = JSON.parse(await this.readBody(req));
|
|
389
|
+
data = this.cronEngine.createTask(body);
|
|
390
|
+
res.writeHead(201, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
391
|
+
res.end(JSON.stringify(data));
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (route.match(/^schedules\/[^/]+$/) && req.method === 'PUT') {
|
|
395
|
+
const id = route.split('/')[1];
|
|
396
|
+
const body = JSON.parse(await this.readBody(req));
|
|
397
|
+
data = this.cronEngine.updateTask(id, body);
|
|
398
|
+
res.writeHead(data ? 200 : 404, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
399
|
+
res.end(JSON.stringify(data || { error: 'Schedule not found' }));
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
if (route.match(/^schedules\/[^/]+$/) && req.method === 'DELETE') {
|
|
403
|
+
const id = route.split('/')[1];
|
|
404
|
+
const success = this.cronEngine.deleteTask(id);
|
|
405
|
+
res.writeHead(success ? 200 : 404, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
406
|
+
res.end(JSON.stringify({ success }));
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (route.match(/^schedules\/[^/]+\/run$/) && req.method === 'POST') {
|
|
410
|
+
const id = route.split('/')[1];
|
|
411
|
+
const success = await this.cronEngine.runTask(id);
|
|
412
|
+
res.writeHead(success ? 200 : 404, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
413
|
+
res.end(JSON.stringify({ success }));
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// --- Image Generation API ---
|
|
418
|
+
if (route === 'image-gen/status' && req.method === 'GET') {
|
|
419
|
+
data = this.imageGenerator.getStatus();
|
|
420
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
421
|
+
res.end(JSON.stringify(data));
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (route === 'image-gen/generate' && req.method === 'POST') {
|
|
425
|
+
const body = JSON.parse(await this.readBody(req));
|
|
426
|
+
data = await this.imageGenerator.generate(body.prompt, body);
|
|
427
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
428
|
+
res.end(JSON.stringify(data));
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
if (route === 'image-gen/config' && req.method === 'PUT') {
|
|
432
|
+
const body = JSON.parse(await this.readBody(req));
|
|
433
|
+
const cfg = loadSettingsConfig();
|
|
434
|
+
cfg.imageGen = { ...(cfg.imageGen || {}), ...body };
|
|
435
|
+
saveSettingsConfig(cfg);
|
|
436
|
+
this.imageGenerator = new ImageGenerator({
|
|
437
|
+
openaiApiKey: body.openaiApiKey,
|
|
438
|
+
replicateApiKey: body.replicateApiKey,
|
|
439
|
+
sdApiUrl: body.sdApiUrl,
|
|
440
|
+
});
|
|
441
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
442
|
+
res.end(JSON.stringify({ success: true }));
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (route === 'first-run/status' && req.method === 'GET') {
|
|
447
|
+
data = await this.getFirstRunStatus();
|
|
448
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
449
|
+
res.end(JSON.stringify(data));
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
if (route === 'first-run/complete' && req.method === 'POST') {
|
|
453
|
+
const body = JSON.parse(await this.readBody(req));
|
|
454
|
+
data = await this.completeFirstRun(body);
|
|
455
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
456
|
+
res.end(JSON.stringify(data));
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// === Skill Marketplace API ===
|
|
461
|
+
if (route === 'skills/marketplace' && req.method === 'GET') {
|
|
462
|
+
const category = url.searchParams.get('category') || undefined;
|
|
463
|
+
const search = url.searchParams.get('q') || undefined;
|
|
464
|
+
data = this.skillMarketplace.listAll(category, search);
|
|
465
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
466
|
+
res.end(JSON.stringify(data));
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (route === 'skills/installed' && req.method === 'GET') {
|
|
470
|
+
data = this.skillMarketplace.getInstalled();
|
|
471
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
472
|
+
res.end(JSON.stringify(data));
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
if (route.match(/^skills\/marketplace\/[^/]+$/) && req.method === 'GET') {
|
|
476
|
+
const skillId = route.split('/')[2];
|
|
477
|
+
data = this.skillMarketplace.getSkill(skillId);
|
|
478
|
+
res.writeHead(data ? 200 : 404, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
479
|
+
res.end(JSON.stringify(data || { error: 'Skill not found' }));
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
if (route.match(/^skills\/marketplace\/[^/]+\/install$/) && req.method === 'POST') {
|
|
483
|
+
const skillId = route.split('/')[2];
|
|
484
|
+
data = this.skillMarketplace.install(skillId);
|
|
485
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
486
|
+
res.end(JSON.stringify(data));
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (route.match(/^skills\/marketplace\/[^/]+\/uninstall$/) && req.method === 'DELETE') {
|
|
490
|
+
const skillId = route.split('/')[2];
|
|
491
|
+
data = this.skillMarketplace.uninstall(skillId);
|
|
492
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
493
|
+
res.end(JSON.stringify(data));
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
switch (route) {
|
|
498
|
+
case 'modules':
|
|
499
|
+
data = await this.getModulesStatus();
|
|
500
|
+
break;
|
|
501
|
+
case 'agent/info':
|
|
502
|
+
data = await this.getAgentInfo();
|
|
503
|
+
break;
|
|
504
|
+
case 'agent/config':
|
|
505
|
+
if (req.method === 'GET') data = await this.getAgentConfig();
|
|
506
|
+
else if (req.method === 'PUT') data = await this.saveConfig(req);
|
|
507
|
+
break;
|
|
508
|
+
case 'agent/chat':
|
|
509
|
+
data = await this.handleChat(req);
|
|
510
|
+
break;
|
|
511
|
+
case 'memory/list':
|
|
512
|
+
data = await this.getMemoryList();
|
|
513
|
+
break;
|
|
514
|
+
case 'memory/search':
|
|
515
|
+
data = await this.searchMemory(url.searchParams.get('q') || '');
|
|
516
|
+
break;
|
|
517
|
+
case 'memory/stats':
|
|
518
|
+
data = await this.getMemoryStats();
|
|
519
|
+
break;
|
|
520
|
+
case 'skills/list':
|
|
521
|
+
data = await this.getSkills();
|
|
522
|
+
break;
|
|
523
|
+
case 'tools/list':
|
|
524
|
+
data = await this.getTools();
|
|
525
|
+
break;
|
|
526
|
+
case 'workflows/list':
|
|
527
|
+
data = this.listWorkflows();
|
|
528
|
+
break;
|
|
529
|
+
case 'workflows':
|
|
530
|
+
if (req.method === 'POST') data = await this.saveWorkflow(req);
|
|
531
|
+
else if (req.method === 'GET') data = this.listWorkflows();
|
|
532
|
+
else { res.writeHead(405); res.end(); return; }
|
|
533
|
+
break;
|
|
534
|
+
case 'jobs/list':
|
|
535
|
+
data = await this.getJobs();
|
|
536
|
+
break;
|
|
537
|
+
case 'logs/recent':
|
|
538
|
+
data = await this.getRecentLogs();
|
|
539
|
+
break;
|
|
540
|
+
case 'analytics/overview':
|
|
541
|
+
data = await this.getAnalytics();
|
|
542
|
+
break;
|
|
543
|
+
case 'doctor/check':
|
|
544
|
+
data = await this.runDoctor();
|
|
545
|
+
break;
|
|
546
|
+
case 'channels/list':
|
|
547
|
+
data = await this.getChannels();
|
|
548
|
+
break;
|
|
549
|
+
case 'plugins/list':
|
|
550
|
+
data = await this.getPlugins();
|
|
551
|
+
break;
|
|
552
|
+
case 'security/approvals':
|
|
553
|
+
data = await this.getPendingApprovals();
|
|
554
|
+
break;
|
|
555
|
+
case 'eval/suites':
|
|
556
|
+
data = await this.getEvalSuites();
|
|
557
|
+
break;
|
|
558
|
+
case 'eval/run':
|
|
559
|
+
if (req.method === 'POST') data = await this.runEvalSuite(req);
|
|
560
|
+
else { res.writeHead(405); res.end(); return; }
|
|
561
|
+
break;
|
|
562
|
+
case 'a2a/card':
|
|
563
|
+
data = this.getA2ACard();
|
|
564
|
+
break;
|
|
565
|
+
case 'a2a/tasks':
|
|
566
|
+
data = this.getA2ATasks();
|
|
567
|
+
break;
|
|
568
|
+
case 'a2a/discover':
|
|
569
|
+
if (req.method === 'POST') data = await this.discoverA2AAgent(req);
|
|
570
|
+
else { res.writeHead(405); res.end(); return; }
|
|
571
|
+
break;
|
|
572
|
+
case 'protocols':
|
|
573
|
+
data = await this.getProtocols();
|
|
574
|
+
break;
|
|
575
|
+
case 'protocols/mcp':
|
|
576
|
+
data = this.getMCPServerStatus();
|
|
577
|
+
break;
|
|
578
|
+
case 'eval/reports':
|
|
579
|
+
data = await this.getEvalReports();
|
|
580
|
+
break;
|
|
581
|
+
case 'telemetry/stats':
|
|
582
|
+
data = this.tracer ? this.tracer.getStats() : { error: 'Telemetry not enabled' };
|
|
583
|
+
break;
|
|
584
|
+
case 'telemetry/traces':
|
|
585
|
+
data = this.getTelemetryTraces(url);
|
|
586
|
+
break;
|
|
587
|
+
case 'telemetry/metrics':
|
|
588
|
+
data = this.tracer ? this.tracer.getMetrics() : [];
|
|
589
|
+
break;
|
|
590
|
+
case 'playground/chat':
|
|
591
|
+
if (req.method === 'POST') {
|
|
592
|
+
return this.handlePlaygroundChat(req, res);
|
|
593
|
+
}
|
|
594
|
+
res.writeHead(405); res.end(); return;
|
|
595
|
+
case 'playground/models':
|
|
596
|
+
data = { models: ['gpt-4o', 'gpt-4o-mini', 'claude-sonnet-4', 'claude-haiku', 'gemini-2.0-flash', 'deepseek-v3'] };
|
|
597
|
+
break;
|
|
598
|
+
default:
|
|
599
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
600
|
+
res.end(JSON.stringify({ error: 'Not found' }));
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
res.writeHead(200, {
|
|
605
|
+
'Content-Type': 'application/json',
|
|
606
|
+
'Access-Control-Allow-Origin': '*',
|
|
607
|
+
});
|
|
608
|
+
res.end(JSON.stringify(data));
|
|
609
|
+
} catch (e: any) {
|
|
610
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
611
|
+
res.end(JSON.stringify({ error: e.message }));
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// --- Agent CRUD & Templates ---
|
|
616
|
+
|
|
617
|
+
private getAgentsDir(): string {
|
|
618
|
+
const dir = join(os.homedir(), '.opc', 'agents');
|
|
619
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
620
|
+
return dir;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
private async createAgent(req: IncomingMessage): Promise<any> {
|
|
624
|
+
const body = await this.readBody(req);
|
|
625
|
+
const { name, templateId, description, model, language } = JSON.parse(body);
|
|
626
|
+
const template = TEMPLATES.find(t => t.id === templateId);
|
|
627
|
+
const id = `agent-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
628
|
+
const agent = {
|
|
629
|
+
id,
|
|
630
|
+
name: name || template?.name || 'My Agent',
|
|
631
|
+
templateId: templateId || null,
|
|
632
|
+
templateName: template?.name || 'Custom',
|
|
633
|
+
templateIcon: template?.icon || '🤖',
|
|
634
|
+
description: description || template?.description || '',
|
|
635
|
+
model: model || template?.suggestedModel || 'gpt-4o-mini',
|
|
636
|
+
language: language || 'en',
|
|
637
|
+
systemPrompt: template?.systemPrompt || 'You are a helpful assistant.',
|
|
638
|
+
industry: template?.industry || 'general',
|
|
639
|
+
created: new Date().toISOString(),
|
|
640
|
+
updated: new Date().toISOString(),
|
|
641
|
+
messageCount: 0,
|
|
642
|
+
lastActive: new Date().toISOString(),
|
|
643
|
+
};
|
|
644
|
+
const filePath = join(this.getAgentsDir(), `${id}.json`);
|
|
645
|
+
writeFileSync(filePath, JSON.stringify(agent, null, 2));
|
|
646
|
+
return agent;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
private listAgents(): { agents: any[] } {
|
|
650
|
+
const dir = this.getAgentsDir();
|
|
651
|
+
const files = readdirSync(dir).filter(f => f.endsWith('.json'));
|
|
652
|
+
const agents = files.map(f => {
|
|
653
|
+
try { return JSON.parse(readFileSync(join(dir, f), 'utf-8')); } catch { return null; }
|
|
654
|
+
}).filter(Boolean).sort((a: any, b: any) => new Date(b.updated).getTime() - new Date(a.updated).getTime());
|
|
655
|
+
return { agents };
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
private getAgentById(id: string): any {
|
|
659
|
+
const filePath = join(this.getAgentsDir(), `${id}.json`);
|
|
660
|
+
if (!existsSync(filePath)) return { error: 'Agent not found' };
|
|
661
|
+
return JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
private async updateAgent(id: string, req: IncomingMessage): Promise<any> {
|
|
665
|
+
const filePath = join(this.getAgentsDir(), `${id}.json`);
|
|
666
|
+
if (!existsSync(filePath)) return { error: 'Agent not found' };
|
|
667
|
+
const existing = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
668
|
+
const body = await this.readBody(req);
|
|
669
|
+
const updates = JSON.parse(body);
|
|
670
|
+
const updated = { ...existing, ...updates, id, updated: new Date().toISOString() };
|
|
671
|
+
writeFileSync(filePath, JSON.stringify(updated, null, 2));
|
|
672
|
+
return updated;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
private deleteAgent(id: string): { success: boolean } {
|
|
676
|
+
const filePath = join(this.getAgentsDir(), `${id}.json`);
|
|
677
|
+
if (existsSync(filePath)) unlinkSync(filePath);
|
|
678
|
+
return { success: true };
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
private getTemplates(industry: string, search: string): { templates: AgentTemplate[]; industries: typeof INDUSTRIES } {
|
|
682
|
+
let filtered = TEMPLATES;
|
|
683
|
+
if (industry) filtered = filtered.filter(t => t.industry === industry);
|
|
684
|
+
if (search) {
|
|
685
|
+
const q = search.toLowerCase();
|
|
686
|
+
filtered = filtered.filter(t =>
|
|
687
|
+
t.name.toLowerCase().includes(q) || t.nameZh.includes(q) ||
|
|
688
|
+
t.description.toLowerCase().includes(q) || t.descriptionZh.includes(q) ||
|
|
689
|
+
t.tags.some(tag => tag.includes(q))
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
return { templates: filtered, industries: INDUSTRIES };
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
private getTemplateById(id: string): AgentTemplate | { error: string } {
|
|
696
|
+
const tpl = TEMPLATES.find(t => t.id === id);
|
|
697
|
+
return tpl || { error: 'Template not found' };
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
private getAgentMemory(agentId: string): any {
|
|
701
|
+
const memDir = join(this.getAgentsDir(), agentId + '-memory');
|
|
702
|
+
if (!existsSync(memDir)) return { entries: [], timeline: [] };
|
|
703
|
+
const files = readdirSync(memDir).filter(f => f.endsWith('.json'));
|
|
704
|
+
const entries = files.map(f => {
|
|
705
|
+
try { return JSON.parse(readFileSync(join(memDir, f), 'utf-8')); } catch { return null; }
|
|
706
|
+
}).filter(Boolean).sort((a: any, b: any) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
707
|
+
return { entries, timeline: entries.map((e: any) => ({ date: e.timestamp, summary: e.summary || e.content?.slice(0, 100) })) };
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
private async handleAgentChat(req: IncomingMessage, res: ServerResponse, agentId: string): Promise<void> {
|
|
711
|
+
const body = JSON.parse(await this.readBody(req));
|
|
712
|
+
const { messages = [] } = body;
|
|
713
|
+
const agent = this.getAgentById(agentId);
|
|
714
|
+
if (agent.error) {
|
|
715
|
+
res.writeHead(404, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
716
|
+
res.end(JSON.stringify(agent));
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Update message count
|
|
721
|
+
agent.messageCount = (agent.messageCount || 0) + 1;
|
|
722
|
+
agent.lastActive = new Date().toISOString();
|
|
723
|
+
agent.updated = new Date().toISOString();
|
|
724
|
+
const filePath = join(this.getAgentsDir(), `${agentId}.json`);
|
|
725
|
+
writeFileSync(filePath, JSON.stringify(agent, null, 2));
|
|
726
|
+
|
|
727
|
+
// SSE streaming response
|
|
728
|
+
res.writeHead(200, {
|
|
729
|
+
'Content-Type': 'text/event-stream',
|
|
730
|
+
'Cache-Control': 'no-cache',
|
|
731
|
+
'Connection': 'keep-alive',
|
|
732
|
+
'Access-Control-Allow-Origin': '*',
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
const allMsgs = [{ role: 'system', content: agent.systemPrompt }, ...messages];
|
|
736
|
+
const lastMsg = allMsgs[allMsgs.length - 1]?.content || '';
|
|
737
|
+
|
|
738
|
+
// Try to call the real /v1/chat/completions endpoint
|
|
739
|
+
try {
|
|
740
|
+
const completionReq = httpRequest({
|
|
741
|
+
hostname: 'localhost',
|
|
742
|
+
port: this.config.port,
|
|
743
|
+
path: '/v1/chat/completions',
|
|
744
|
+
method: 'POST',
|
|
745
|
+
headers: { 'Content-Type': 'application/json' },
|
|
746
|
+
}, (completionRes) => {
|
|
747
|
+
if (completionRes.statusCode === 200) {
|
|
748
|
+
completionRes.pipe(res);
|
|
749
|
+
} else {
|
|
750
|
+
// Fallback to simulated response
|
|
751
|
+
this.sendSimulatedResponse(res, lastMsg, agent);
|
|
752
|
+
}
|
|
753
|
+
});
|
|
754
|
+
completionReq.on('error', () => {
|
|
755
|
+
this.sendSimulatedResponse(res, lastMsg, agent);
|
|
756
|
+
});
|
|
757
|
+
completionReq.write(JSON.stringify({
|
|
758
|
+
model: agent.model,
|
|
759
|
+
messages: allMsgs,
|
|
760
|
+
stream: true,
|
|
761
|
+
}));
|
|
762
|
+
completionReq.end();
|
|
763
|
+
} catch {
|
|
764
|
+
this.sendSimulatedResponse(res, lastMsg, agent);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
private sendSimulatedResponse(res: ServerResponse, lastMsg: string, agent: any): void {
|
|
769
|
+
const response = `Hello! I'm ${agent.name}. You said: "${lastMsg}"\n\nI'm ready to help you. (Note: Connect a model provider for real AI responses)`;
|
|
770
|
+
const words = response.split(' ');
|
|
771
|
+
let i = 0;
|
|
772
|
+
const interval = setInterval(() => {
|
|
773
|
+
if (i >= words.length) {
|
|
774
|
+
res.write('data: [DONE]\n\n');
|
|
775
|
+
res.end();
|
|
776
|
+
clearInterval(interval);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
const chunk = (i === 0 ? '' : ' ') + words[i];
|
|
780
|
+
res.write(`data: ${JSON.stringify({ choices: [{ delta: { content: chunk } }] })}\n\n`);
|
|
781
|
+
i++;
|
|
782
|
+
}, 50);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// --- Settings Implementations ---
|
|
786
|
+
|
|
787
|
+
private async detectLocalOllama(): Promise<any> {
|
|
788
|
+
return new Promise((resolve) => {
|
|
789
|
+
const req = httpRequest({ hostname: 'localhost', port: 11434, path: '/api/tags', method: 'GET', timeout: 3000 }, (res) => {
|
|
790
|
+
let body = '';
|
|
791
|
+
res.on('data', (c: any) => body += c);
|
|
792
|
+
res.on('end', () => {
|
|
793
|
+
try {
|
|
794
|
+
const data = JSON.parse(body);
|
|
795
|
+
const models = (data.models || []).map((m: any) => ({
|
|
796
|
+
name: m.name, size: m.size, modified: m.modified_at,
|
|
797
|
+
details: m.details || {},
|
|
798
|
+
}));
|
|
799
|
+
resolve({ running: true, models });
|
|
800
|
+
} catch { resolve({ running: true, models: [] }); }
|
|
801
|
+
});
|
|
802
|
+
});
|
|
803
|
+
req.on('error', () => resolve({ running: false, models: [] }));
|
|
804
|
+
req.on('timeout', () => { req.destroy(); resolve({ running: false, models: [] }); });
|
|
805
|
+
req.end();
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
private async testModelConnection(provider: string, apiKey: string, baseUrl?: string): Promise<any> {
|
|
810
|
+
const endpoints: Record<string, { url: string; path: string }> = {
|
|
811
|
+
openai: { url: 'api.openai.com', path: '/v1/models' },
|
|
812
|
+
deepseek: { url: 'api.deepseek.com', path: '/v1/models' },
|
|
813
|
+
anthropic: { url: 'api.anthropic.com', path: '/v1/models' },
|
|
814
|
+
openrouter: { url: 'openrouter.ai', path: '/api/v1/models' },
|
|
815
|
+
};
|
|
816
|
+
const ep = endpoints[provider];
|
|
817
|
+
if (!ep && !baseUrl) return { success: false, error: 'Unknown provider' };
|
|
818
|
+
|
|
819
|
+
const hostname = baseUrl ? new URL(baseUrl).hostname : ep.url;
|
|
820
|
+
const path = baseUrl ? '/v1/models' : ep.path;
|
|
821
|
+
const headers: Record<string, string> = { 'Authorization': `Bearer ${apiKey}` };
|
|
822
|
+
if (provider === 'anthropic') {
|
|
823
|
+
headers['x-api-key'] = apiKey;
|
|
824
|
+
headers['anthropic-version'] = '2023-06-01';
|
|
825
|
+
delete headers['Authorization'];
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
return new Promise((resolve) => {
|
|
829
|
+
const https = require('https');
|
|
830
|
+
const req = https.request({ hostname, path, method: 'GET', headers, timeout: 10000 }, (res: any) => {
|
|
831
|
+
resolve({ success: res.statusCode === 200, statusCode: res.statusCode });
|
|
832
|
+
});
|
|
833
|
+
req.on('error', (e: any) => resolve({ success: false, error: e.message }));
|
|
834
|
+
req.on('timeout', () => { req.destroy(); resolve({ success: false, error: 'Timeout' }); });
|
|
835
|
+
req.end();
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
private async getSettingsStatus(): Promise<any> {
|
|
840
|
+
const uptime = process.uptime();
|
|
841
|
+
const mem = process.memoryUsage();
|
|
842
|
+
const modules = await this.getModulesStatus();
|
|
843
|
+
const logPath = join(this.config.agentDir, '.opc', 'agent.log');
|
|
844
|
+
let recentLogs: string[] = [];
|
|
845
|
+
if (existsSync(logPath)) {
|
|
846
|
+
const content = readFileSync(logPath, 'utf-8');
|
|
847
|
+
recentLogs = content.split('\n').slice(-50);
|
|
848
|
+
}
|
|
849
|
+
return {
|
|
850
|
+
status: 'running',
|
|
851
|
+
uptime,
|
|
852
|
+
memory: { rss: mem.rss, heapUsed: mem.heapUsed, heapTotal: mem.heapTotal },
|
|
853
|
+
cpu: os.loadavg(),
|
|
854
|
+
modules: modules.modules,
|
|
855
|
+
logs: recentLogs,
|
|
856
|
+
startedAt: new Date(Date.now() - uptime * 1000).toISOString(),
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
private async getUsageStats(): Promise<any> {
|
|
861
|
+
const cfg = loadSettingsConfig();
|
|
862
|
+
const usage = cfg.usage || { totalTokens: 0, totalCost: 0, daily: [], byModel: {} };
|
|
863
|
+
return usage;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// --- API Implementations ---
|
|
867
|
+
|
|
868
|
+
private async getAgentInfo() {
|
|
869
|
+
const oad = this.loadOAD();
|
|
870
|
+
const pkg = this.loadPackageJson();
|
|
871
|
+
return {
|
|
872
|
+
name: oad?.metadata?.name || pkg?.name || 'unknown',
|
|
873
|
+
version: oad?.metadata?.version || pkg?.version || '0.0.0',
|
|
874
|
+
description: oad?.metadata?.description || pkg?.description || '',
|
|
875
|
+
model: oad?.spec?.model || 'unknown',
|
|
876
|
+
provider: oad?.spec?.provider?.default || 'unknown',
|
|
877
|
+
channels: oad?.spec?.channels?.map((c: any) => c.type) || [],
|
|
878
|
+
skills: oad?.spec?.skills?.map((s: any) => s.name) || [],
|
|
879
|
+
status: 'running',
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
private async getAgentConfig() {
|
|
884
|
+
const yamlPath = join(this.config.agentDir, 'agent.yaml');
|
|
885
|
+
if (existsSync(yamlPath)) {
|
|
886
|
+
return { content: readFileSync(yamlPath, 'utf-8') };
|
|
887
|
+
}
|
|
888
|
+
return { content: '', error: 'agent.yaml not found' };
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
private async saveConfig(req: IncomingMessage) {
|
|
892
|
+
const body = await this.readBody(req);
|
|
893
|
+
const { content } = JSON.parse(body);
|
|
894
|
+
const yamlPath = join(this.config.agentDir, 'agent.yaml');
|
|
895
|
+
const { writeFileSync } = require('fs');
|
|
896
|
+
writeFileSync(yamlPath, content, 'utf-8');
|
|
897
|
+
return { success: true };
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
private async handleChat(req: IncomingMessage) {
|
|
901
|
+
const body = await this.readBody(req);
|
|
902
|
+
const { message, sessionId } = JSON.parse(body);
|
|
903
|
+
try {
|
|
904
|
+
const { BaseAgent, InMemoryStore } = require('../index');
|
|
905
|
+
const oad = this.loadOAD();
|
|
906
|
+
const agent = new BaseAgent({
|
|
907
|
+
name: oad?.metadata?.name || 'studio-agent',
|
|
908
|
+
systemPrompt: oad?.spec?.systemPrompt || 'You are a helpful assistant.',
|
|
909
|
+
provider: oad?.spec?.provider?.default || 'ollama',
|
|
910
|
+
model: oad?.spec?.model || 'qwen2.5',
|
|
911
|
+
memory: new InMemoryStore(),
|
|
912
|
+
});
|
|
913
|
+
await agent.init();
|
|
914
|
+
const response = await agent.handleMessage({
|
|
915
|
+
id: String(Date.now()),
|
|
916
|
+
content: message,
|
|
917
|
+
sender: 'studio-user',
|
|
918
|
+
channel: 'studio',
|
|
919
|
+
sessionId: sessionId || 'studio-session',
|
|
920
|
+
timestamp: new Date(),
|
|
921
|
+
});
|
|
922
|
+
return { response: response.content };
|
|
923
|
+
} catch (e: any) {
|
|
924
|
+
return { response: `Error: ${e.message}` };
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
private async getMemoryList() {
|
|
929
|
+
try {
|
|
930
|
+
const { Brain } = require('deepbrain');
|
|
931
|
+
const oad = this.loadOAD();
|
|
932
|
+
const dbPath = oad?.spec?.memory?.longTerm?.database || './data/brain.db';
|
|
933
|
+
const brain = new Brain({ database: dbPath, embedding_provider: 'ollama' });
|
|
934
|
+
await brain.connect();
|
|
935
|
+
const pages = await brain.list({ limit: 50 });
|
|
936
|
+
await brain.disconnect();
|
|
937
|
+
return { pages };
|
|
938
|
+
} catch {
|
|
939
|
+
return { pages: [], error: 'DeepBrain not available' };
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
private async searchMemory(query: string) {
|
|
944
|
+
try {
|
|
945
|
+
const { Brain } = require('deepbrain');
|
|
946
|
+
const oad = this.loadOAD();
|
|
947
|
+
const dbPath = oad?.spec?.memory?.longTerm?.database || './data/brain.db';
|
|
948
|
+
const brain = new Brain({ database: dbPath, embedding_provider: 'ollama' });
|
|
949
|
+
await brain.connect();
|
|
950
|
+
const results = await brain.search(query);
|
|
951
|
+
await brain.disconnect();
|
|
952
|
+
return { results };
|
|
953
|
+
} catch {
|
|
954
|
+
return { results: [], error: 'Search failed' };
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
private async getMemoryStats() {
|
|
959
|
+
try {
|
|
960
|
+
const { Brain } = require('deepbrain');
|
|
961
|
+
const oad = this.loadOAD();
|
|
962
|
+
const dbPath = oad?.spec?.memory?.longTerm?.database || './data/brain.db';
|
|
963
|
+
const brain = new Brain({ database: dbPath, embedding_provider: 'ollama' });
|
|
964
|
+
await brain.connect();
|
|
965
|
+
const stats = await brain.stats();
|
|
966
|
+
await brain.disconnect();
|
|
967
|
+
return stats;
|
|
968
|
+
} catch {
|
|
969
|
+
return { pages: 0, chunks: 0, error: 'Stats unavailable' };
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
private async getSkills() {
|
|
974
|
+
try {
|
|
975
|
+
const { SkillLearner } = require('../index');
|
|
976
|
+
const learner = new SkillLearner('.opc/skills');
|
|
977
|
+
const skills = learner.loadSkills();
|
|
978
|
+
return { skills };
|
|
979
|
+
} catch {
|
|
980
|
+
return { skills: [] };
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
private async getTools() {
|
|
985
|
+
try {
|
|
986
|
+
const { getBuiltinTools } = require('../index');
|
|
987
|
+
const tools = getBuiltinTools(this.config.agentDir);
|
|
988
|
+
return { tools: tools.map((t: any) => ({ name: t.definition.name, description: t.definition.description })) };
|
|
989
|
+
} catch {
|
|
990
|
+
return { tools: [] };
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
private getWorkflowsDir(): string {
|
|
995
|
+
const dir = join(this.config.agentDir, '.opc', 'workflows');
|
|
996
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
997
|
+
return dir;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
private listWorkflows(): { workflows: WorkflowDefinition[] } {
|
|
1001
|
+
const dir = this.getWorkflowsDir();
|
|
1002
|
+
const files = require('fs').readdirSync(dir).filter((f: string) => f.endsWith('.json'));
|
|
1003
|
+
const workflows = files.map((f: string) => {
|
|
1004
|
+
try { return JSON.parse(readFileSync(join(dir, f), 'utf-8')); } catch { return null; }
|
|
1005
|
+
}).filter(Boolean);
|
|
1006
|
+
// Also include OAD-defined workflows
|
|
1007
|
+
const oad = this.loadOAD();
|
|
1008
|
+
const oadWorkflows = (oad?.spec?.workflows || []).map((w: any, i: number) => ({
|
|
1009
|
+
id: `oad-${i}`,
|
|
1010
|
+
name: w.name || `Workflow ${i + 1}`,
|
|
1011
|
+
nodes: [],
|
|
1012
|
+
edges: [],
|
|
1013
|
+
steps: w.steps,
|
|
1014
|
+
source: 'oad',
|
|
1015
|
+
}));
|
|
1016
|
+
return { workflows: [...workflows, ...oadWorkflows] };
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
private getWorkflowById(id: string): WorkflowDefinition | { error: string } {
|
|
1020
|
+
const filePath = join(this.getWorkflowsDir(), `${id}.json`);
|
|
1021
|
+
if (!existsSync(filePath)) return { error: 'Workflow not found' };
|
|
1022
|
+
return JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
private async saveWorkflow(req: IncomingMessage): Promise<{ success: boolean; id: string }> {
|
|
1026
|
+
const body = await this.readBody(req);
|
|
1027
|
+
const workflow = JSON.parse(body) as WorkflowDefinition;
|
|
1028
|
+
if (!workflow.id) workflow.id = `wf-${Date.now()}`;
|
|
1029
|
+
workflow.updated = new Date().toISOString();
|
|
1030
|
+
if (!workflow.created) workflow.created = workflow.updated;
|
|
1031
|
+
const filePath = join(this.getWorkflowsDir(), `${workflow.id}.json`);
|
|
1032
|
+
writeFileSync(filePath, JSON.stringify(workflow, null, 2));
|
|
1033
|
+
return { success: true, id: workflow.id };
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
private deleteWorkflow(id: string): { success: boolean } {
|
|
1037
|
+
const filePath = join(this.getWorkflowsDir(), `${id}.json`);
|
|
1038
|
+
if (existsSync(filePath)) require('fs').unlinkSync(filePath);
|
|
1039
|
+
return { success: true };
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
private async runWorkflow(id: string): Promise<any> {
|
|
1043
|
+
const wf = this.getWorkflowById(id);
|
|
1044
|
+
if ('error' in wf) return wf;
|
|
1045
|
+
// Basic topological execution simulation
|
|
1046
|
+
const results: Record<string, any> = {};
|
|
1047
|
+
const sorted = this.topoSort(wf.nodes, wf.edges);
|
|
1048
|
+
for (const node of sorted) {
|
|
1049
|
+
results[node.id] = { type: node.type, name: node.name, status: 'completed', output: `[simulated output for ${node.name}]` };
|
|
1050
|
+
}
|
|
1051
|
+
return { workflowId: id, status: 'completed', results };
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
private topoSort(nodes: WorkflowNode[], edges: WorkflowEdge[]): WorkflowNode[] {
|
|
1055
|
+
const nodeMap = new Map(nodes.map(n => [n.id, n]));
|
|
1056
|
+
const inDegree = new Map<string, number>();
|
|
1057
|
+
const adj = new Map<string, string[]>();
|
|
1058
|
+
for (const n of nodes) { inDegree.set(n.id, 0); adj.set(n.id, []); }
|
|
1059
|
+
for (const e of edges) { adj.get(e.from)?.push(e.to); inDegree.set(e.to, (inDegree.get(e.to) || 0) + 1); }
|
|
1060
|
+
const queue = nodes.filter(n => (inDegree.get(n.id) || 0) === 0);
|
|
1061
|
+
const result: WorkflowNode[] = [];
|
|
1062
|
+
while (queue.length > 0) {
|
|
1063
|
+
const node = queue.shift()!;
|
|
1064
|
+
result.push(node);
|
|
1065
|
+
for (const next of (adj.get(node.id) || [])) {
|
|
1066
|
+
const d = (inDegree.get(next) || 1) - 1;
|
|
1067
|
+
inDegree.set(next, d);
|
|
1068
|
+
if (d === 0) queue.push(nodeMap.get(next)!);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
return result;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
private async getJobs() {
|
|
1075
|
+
const oad = this.loadOAD();
|
|
1076
|
+
return { jobs: oad?.spec?.scheduler?.jobs || [] };
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
private async getRecentLogs() {
|
|
1080
|
+
const logPath = join(this.config.agentDir, '.opc', 'agent.log');
|
|
1081
|
+
if (existsSync(logPath)) {
|
|
1082
|
+
const content = readFileSync(logPath, 'utf-8');
|
|
1083
|
+
const lines = content.split('\n').slice(-100);
|
|
1084
|
+
return { lines };
|
|
1085
|
+
}
|
|
1086
|
+
return { lines: [] };
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
private async getAnalytics() {
|
|
1090
|
+
return {
|
|
1091
|
+
totalMessages: 0,
|
|
1092
|
+
totalSessions: 0,
|
|
1093
|
+
avgResponseTime: 0,
|
|
1094
|
+
topSkills: [],
|
|
1095
|
+
note: 'Analytics tracking starts when agent is running via opc run/start',
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
private async runDoctor() {
|
|
1100
|
+
try {
|
|
1101
|
+
const { runDoctor } = require('../doctor');
|
|
1102
|
+
const results = await runDoctor();
|
|
1103
|
+
return results;
|
|
1104
|
+
} catch {
|
|
1105
|
+
return { error: 'Doctor not available' };
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
private async getChannels() {
|
|
1110
|
+
const oad = this.loadOAD();
|
|
1111
|
+
return { channels: oad?.spec?.channels || [] };
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
private async getPlugins() {
|
|
1115
|
+
const oad = this.loadOAD();
|
|
1116
|
+
return { plugins: oad?.spec?.plugins || [] };
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
private async getProtocols() {
|
|
1120
|
+
const oad = this.loadOAD();
|
|
1121
|
+
const protocols = (oad?.spec as any)?.protocols || {};
|
|
1122
|
+
return {
|
|
1123
|
+
protocols: [
|
|
1124
|
+
{ name: 'a2a', description: 'Agent-to-Agent', enabled: !!protocols.a2a?.enabled, config: protocols.a2a || {} },
|
|
1125
|
+
{ name: 'agui', description: 'AG-UI — Agent-User Interaction (SSE)', enabled: !!protocols.agui?.enabled, config: protocols.agui || {} },
|
|
1126
|
+
{ name: 'mcp', description: 'MCP Server — Expose as MCP tools', enabled: !!protocols.mcp?.enabled, config: protocols.mcp || {} },
|
|
1127
|
+
],
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
private async getPendingApprovals() {
|
|
1132
|
+
return { approvals: [] };
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
private getMCPServerStatus() {
|
|
1136
|
+
const oad = this.loadOAD();
|
|
1137
|
+
const mcpConfig = (oad?.spec as any)?.protocols?.mcp;
|
|
1138
|
+
const { agentToMCPTools } = require('../protocols/mcp/agent-tools');
|
|
1139
|
+
const agentName = oad?.metadata?.name || 'opc-agent';
|
|
1140
|
+
const tools = agentToMCPTools({ name: agentName });
|
|
1141
|
+
return {
|
|
1142
|
+
enabled: !!mcpConfig?.enabled,
|
|
1143
|
+
mode: mcpConfig?.mode || 'stdio',
|
|
1144
|
+
port: mcpConfig?.port || 3002,
|
|
1145
|
+
tools: tools.map((t: any) => ({ name: t.name, description: t.description })),
|
|
1146
|
+
toolCount: tools.length,
|
|
1147
|
+
exposedTools: mcpConfig?.exposedTools || tools.map((t: any) => t.name),
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
private getTelemetryTraces(url: URL) {
|
|
1152
|
+
if (!this.tracer) return { traces: [] };
|
|
1153
|
+
const traceId = url.searchParams.get('id');
|
|
1154
|
+
if (traceId) {
|
|
1155
|
+
return { spans: this.tracer.getTrace(traceId) };
|
|
1156
|
+
}
|
|
1157
|
+
const limit = parseInt(url.searchParams.get('limit') || '50');
|
|
1158
|
+
const spans = this.tracer.getSpans({ limit });
|
|
1159
|
+
// Group by traceId for trace list
|
|
1160
|
+
const traceMap = new Map<string, { traceId: string; rootSpan: string; startTime: number; spanCount: number; status: string }>();
|
|
1161
|
+
for (const s of spans) {
|
|
1162
|
+
if (!traceMap.has(s.traceId)) {
|
|
1163
|
+
traceMap.set(s.traceId, { traceId: s.traceId, rootSpan: s.name, startTime: s.startTime, spanCount: 0, status: s.status });
|
|
1164
|
+
}
|
|
1165
|
+
traceMap.get(s.traceId)!.spanCount++;
|
|
1166
|
+
}
|
|
1167
|
+
return { traces: Array.from(traceMap.values()) };
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
private async getEvalSuites() {
|
|
1171
|
+
const { AgentEvaluator } = require('../eval');
|
|
1172
|
+
return { suites: AgentEvaluator.builtinSuites() };
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
private async runEvalSuite(req: IncomingMessage): Promise<any> {
|
|
1176
|
+
const body = await this.readBody(req);
|
|
1177
|
+
const { suite: suiteName } = JSON.parse(body || '{}');
|
|
1178
|
+
const { AgentEvaluator } = require('../eval');
|
|
1179
|
+
const suite = AgentEvaluator.loadBuiltinSuite(suiteName || 'basic');
|
|
1180
|
+
// Use a mock agent for studio eval (no real agent loaded)
|
|
1181
|
+
const mockAgent = { chat: async (input: string) => `[mock response to: ${input}]` };
|
|
1182
|
+
const evaluator = new AgentEvaluator(mockAgent);
|
|
1183
|
+
const report = await evaluator.evalSuite(suite);
|
|
1184
|
+
// Save report
|
|
1185
|
+
const reportsDir = join(this.config.agentDir, '.eval-reports');
|
|
1186
|
+
const reportPath = join(reportsDir, `${suiteName || 'basic'}-${Date.now()}.json`);
|
|
1187
|
+
AgentEvaluator.saveReport(report, reportPath);
|
|
1188
|
+
return report;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
private async getEvalReports() {
|
|
1192
|
+
const reportsDir = join(this.config.agentDir, '.eval-reports');
|
|
1193
|
+
if (!existsSync(reportsDir)) return { reports: [] };
|
|
1194
|
+
const files = require('fs').readdirSync(reportsDir).filter((f: string) => f.endsWith('.json'));
|
|
1195
|
+
return {
|
|
1196
|
+
reports: files.map((f: string) => {
|
|
1197
|
+
try {
|
|
1198
|
+
return JSON.parse(readFileSync(join(reportsDir, f), 'utf-8'));
|
|
1199
|
+
} catch { return null; }
|
|
1200
|
+
}).filter(Boolean)
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// --- A2A Protocol ---
|
|
1205
|
+
|
|
1206
|
+
private getA2ACard() {
|
|
1207
|
+
try {
|
|
1208
|
+
const { oadToAgentCard } = require('../protocols/a2a');
|
|
1209
|
+
const yaml = require('js-yaml');
|
|
1210
|
+
for (const name of ['agent.yaml', 'agent.yml']) {
|
|
1211
|
+
const p = join(this.config.agentDir, name);
|
|
1212
|
+
if (existsSync(p)) {
|
|
1213
|
+
const oad = yaml.load(readFileSync(p, 'utf-8'));
|
|
1214
|
+
return oadToAgentCard(oad, `http://localhost:${this.config.port}`);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
return { error: 'No agent.yaml found' };
|
|
1218
|
+
} catch { return { error: 'Failed to generate agent card' }; }
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
private getA2ATasks() {
|
|
1222
|
+
// In-memory tasks from A2A server if running
|
|
1223
|
+
return { tasks: [] };
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
private async discoverA2AAgent(req: IncomingMessage): Promise<any> {
|
|
1227
|
+
const body = await this.readBody(req);
|
|
1228
|
+
const { url } = JSON.parse(body || '{}');
|
|
1229
|
+
if (!url) return { error: 'url required' };
|
|
1230
|
+
try {
|
|
1231
|
+
const { A2AClient } = require('../protocols/a2a');
|
|
1232
|
+
const client = new A2AClient(url);
|
|
1233
|
+
return await client.getAgentCard();
|
|
1234
|
+
} catch (err: any) {
|
|
1235
|
+
return { error: err.message };
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
private async getFirstRunStatus(): Promise<any> {
|
|
1240
|
+
const configPath = join(os.homedir(), '.opc', 'config.json');
|
|
1241
|
+
const ollamaStatus = await this.detectLocalOllama();
|
|
1242
|
+
if (!existsSync(configPath)) {
|
|
1243
|
+
return { firstRun: true, ollamaDetected: ollamaStatus.running, ollamaModels: ollamaStatus.models || [] };
|
|
1244
|
+
}
|
|
1245
|
+
try {
|
|
1246
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
1247
|
+
return { firstRun: !config.firstRunComplete, ollamaDetected: ollamaStatus.running, ollamaModels: ollamaStatus.models || [] };
|
|
1248
|
+
} catch {
|
|
1249
|
+
return { firstRun: true, ollamaDetected: ollamaStatus.running, ollamaModels: ollamaStatus.models || [] };
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
private async completeFirstRun(choices: any): Promise<any> {
|
|
1254
|
+
const cfg = loadSettingsConfig();
|
|
1255
|
+
cfg.firstRunComplete = true;
|
|
1256
|
+
if (choices) {
|
|
1257
|
+
if (choices.templateId) cfg.firstRunTemplate = choices.templateId;
|
|
1258
|
+
if (choices.model) cfg.models = { ...(cfg.models || {}), chatModel: choices.model };
|
|
1259
|
+
}
|
|
1260
|
+
saveSettingsConfig(cfg);
|
|
1261
|
+
return { success: true };
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// --- Module Proxy & Health ---
|
|
1265
|
+
|
|
1266
|
+
private proxyToModule(req: IncomingMessage, res: ServerResponse, mod: ModuleInfo, url: URL) {
|
|
1267
|
+
const targetPath = url.pathname.slice(`/${mod.path}`.length) || '/';
|
|
1268
|
+
const proxyReq = httpRequest(
|
|
1269
|
+
{
|
|
1270
|
+
hostname: 'localhost',
|
|
1271
|
+
port: mod.port,
|
|
1272
|
+
path: targetPath + (url.search || ''),
|
|
1273
|
+
method: req.method,
|
|
1274
|
+
headers: { ...req.headers, host: `localhost:${mod.port}` },
|
|
1275
|
+
},
|
|
1276
|
+
(proxyRes) => {
|
|
1277
|
+
res.writeHead(proxyRes.statusCode || 502, proxyRes.headers);
|
|
1278
|
+
proxyRes.pipe(res, { end: true });
|
|
1279
|
+
},
|
|
1280
|
+
);
|
|
1281
|
+
proxyReq.on('error', () => {
|
|
1282
|
+
res.writeHead(502, { 'Content-Type': 'text/html' });
|
|
1283
|
+
res.end(`<html><body style="font-family:system-ui;padding:40px;color:#999;background:#1a1a2e;text-align:center"><h2>${mod.icon} ${mod.name}</h2><p>Module not running on port ${mod.port}</p></body></html>`);
|
|
1284
|
+
});
|
|
1285
|
+
req.pipe(proxyReq, { end: true });
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
private checkPort(port: number): Promise<boolean> {
|
|
1289
|
+
return new Promise((resolve) => {
|
|
1290
|
+
const sock = new net.Socket();
|
|
1291
|
+
sock.setTimeout(500);
|
|
1292
|
+
sock.once('connect', () => { sock.destroy(); resolve(true); });
|
|
1293
|
+
sock.once('error', () => { sock.destroy(); resolve(false); });
|
|
1294
|
+
sock.once('timeout', () => { sock.destroy(); resolve(false); });
|
|
1295
|
+
sock.connect(port, 'localhost');
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
async getModulesStatus() {
|
|
1300
|
+
const modules = await Promise.all(
|
|
1301
|
+
MODULE_REGISTRY.map(async (mod) => ({
|
|
1302
|
+
name: mod.name,
|
|
1303
|
+
path: `/${mod.path}/`,
|
|
1304
|
+
port: mod.port,
|
|
1305
|
+
icon: mod.icon,
|
|
1306
|
+
running: await this.checkPort(mod.port),
|
|
1307
|
+
})),
|
|
1308
|
+
);
|
|
1309
|
+
return { modules };
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
// --- Helpers ---
|
|
1313
|
+
|
|
1314
|
+
private loadOAD(): any {
|
|
1315
|
+
try {
|
|
1316
|
+
const yamlPath = join(this.config.agentDir, 'agent.yaml');
|
|
1317
|
+
if (!existsSync(yamlPath)) return null;
|
|
1318
|
+
const content = readFileSync(yamlPath, 'utf-8');
|
|
1319
|
+
try {
|
|
1320
|
+
const { loadOAD } = require('../index');
|
|
1321
|
+
return loadOAD(yamlPath);
|
|
1322
|
+
} catch {
|
|
1323
|
+
// Fallback: simple yaml parse
|
|
1324
|
+
const yaml = require('js-yaml');
|
|
1325
|
+
return yaml.load(content);
|
|
1326
|
+
}
|
|
1327
|
+
} catch {
|
|
1328
|
+
return null;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
private loadPackageJson(): any {
|
|
1333
|
+
try {
|
|
1334
|
+
const pkgPath = join(this.config.agentDir, 'package.json');
|
|
1335
|
+
if (!existsSync(pkgPath)) return null;
|
|
1336
|
+
return JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
1337
|
+
} catch {
|
|
1338
|
+
return null;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
serveStatic(req: IncomingMessage, res: ServerResponse, url: URL) {
|
|
1343
|
+
let filePath = url.pathname === '/' ? '/index.html' : url.pathname;
|
|
1344
|
+
const fullPath = join(this.config.staticDir, filePath);
|
|
1345
|
+
|
|
1346
|
+
if (!existsSync(fullPath)) {
|
|
1347
|
+
// SPA fallback
|
|
1348
|
+
const indexPath = join(this.config.staticDir, 'index.html');
|
|
1349
|
+
if (existsSync(indexPath)) {
|
|
1350
|
+
const content = readFileSync(indexPath, 'utf-8');
|
|
1351
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
1352
|
+
res.end(content);
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
res.writeHead(404);
|
|
1356
|
+
res.end('Not found');
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
const mimeTypes: Record<string, string> = {
|
|
1361
|
+
'.html': 'text/html',
|
|
1362
|
+
'.css': 'text/css',
|
|
1363
|
+
'.js': 'application/javascript',
|
|
1364
|
+
'.json': 'application/json',
|
|
1365
|
+
'.png': 'image/png',
|
|
1366
|
+
'.svg': 'image/svg+xml',
|
|
1367
|
+
'.ico': 'image/x-icon',
|
|
1368
|
+
};
|
|
1369
|
+
const ext = extname(fullPath);
|
|
1370
|
+
const contentType = mimeTypes[ext] || 'application/octet-stream';
|
|
1371
|
+
|
|
1372
|
+
const content = readFileSync(fullPath);
|
|
1373
|
+
res.writeHead(200, { 'Content-Type': contentType });
|
|
1374
|
+
res.end(content);
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
private async handlePlaygroundChat(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
1378
|
+
const body = JSON.parse(await this.readBody(req));
|
|
1379
|
+
const { messages = [], model = 'gpt-4o', temperature = 0.7, systemPrompt } = body;
|
|
1380
|
+
|
|
1381
|
+
res.writeHead(200, {
|
|
1382
|
+
'Content-Type': 'text/event-stream',
|
|
1383
|
+
'Cache-Control': 'no-cache',
|
|
1384
|
+
'Connection': 'keep-alive',
|
|
1385
|
+
'Access-Control-Allow-Origin': '*',
|
|
1386
|
+
});
|
|
1387
|
+
|
|
1388
|
+
// Simulated streaming response for playground demo
|
|
1389
|
+
const allMsgs = systemPrompt ? [{ role: 'system', content: systemPrompt }, ...messages] : messages;
|
|
1390
|
+
const lastMsg = allMsgs[allMsgs.length - 1]?.content || '';
|
|
1391
|
+
const response = `This is a playground demo response to: "${lastMsg}"\n\nModel: ${model}, Temperature: ${temperature}\nMessages in context: ${allMsgs.length}`;
|
|
1392
|
+
|
|
1393
|
+
const words = response.split(' ');
|
|
1394
|
+
for (let i = 0; i < words.length; i++) {
|
|
1395
|
+
const chunk = (i === 0 ? '' : ' ') + words[i];
|
|
1396
|
+
res.write(`data: ${JSON.stringify({ content: chunk })}\n\n`);
|
|
1397
|
+
}
|
|
1398
|
+
res.write('data: [DONE]\n\n');
|
|
1399
|
+
res.end();
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
// --- Document upload handlers ---
|
|
1403
|
+
|
|
1404
|
+
private getDocumentsDir(agentId: string): string {
|
|
1405
|
+
const dir = join(this.getAgentsDir(), agentId + '-documents');
|
|
1406
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
1407
|
+
return dir;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
private async handleDocumentUpload(req: IncomingMessage, res: ServerResponse, agentId: string): Promise<void> {
|
|
1411
|
+
const corsHeaders = { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' };
|
|
1412
|
+
|
|
1413
|
+
try {
|
|
1414
|
+
// Parse multipart form data manually
|
|
1415
|
+
const { buffer, filename } = await this.parseMultipart(req);
|
|
1416
|
+
|
|
1417
|
+
if (!filename) {
|
|
1418
|
+
res.writeHead(400, corsHeaders);
|
|
1419
|
+
res.end(JSON.stringify({ error: 'No file uploaded' }));
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
if (buffer.length > 50 * 1024 * 1024) {
|
|
1424
|
+
res.writeHead(413, corsHeaders);
|
|
1425
|
+
res.end(JSON.stringify({ error: 'File too large (max 50MB)' }));
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
// Process document
|
|
1430
|
+
const processor = new DocumentProcessor();
|
|
1431
|
+
const doc = await processor.process(buffer, filename);
|
|
1432
|
+
|
|
1433
|
+
// Store chunks via DeepBrain learn()
|
|
1434
|
+
let learnedCount = 0;
|
|
1435
|
+
try {
|
|
1436
|
+
const { Brain } = require('deepbrain');
|
|
1437
|
+
const oad = this.loadOAD();
|
|
1438
|
+
const dbPath = oad?.spec?.memory?.longTerm?.database || './data/brain.db';
|
|
1439
|
+
const brain = new Brain({ database: dbPath, embedding_provider: 'ollama' });
|
|
1440
|
+
await brain.connect();
|
|
1441
|
+
|
|
1442
|
+
for (const chunk of doc.chunks) {
|
|
1443
|
+
const content = `[Source: ${filename}] ${chunk.title}\n\n${chunk.content}`;
|
|
1444
|
+
if (typeof brain.store === 'function') {
|
|
1445
|
+
await brain.store('documents', `${doc.id}-${chunk.metadata.chunkIndex}`, content, {
|
|
1446
|
+
source: filename,
|
|
1447
|
+
docId: doc.id,
|
|
1448
|
+
chunkIndex: chunk.metadata.chunkIndex,
|
|
1449
|
+
tags: ['document-upload', filename],
|
|
1450
|
+
});
|
|
1451
|
+
} else if (typeof brain.learn === 'function') {
|
|
1452
|
+
await brain.learn(content, {
|
|
1453
|
+
tags: ['document-upload', filename],
|
|
1454
|
+
slug: `${doc.id}-${chunk.metadata.chunkIndex}`,
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
learnedCount++;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
await brain.disconnect();
|
|
1461
|
+
} catch {
|
|
1462
|
+
// If DeepBrain is not available, store in local memory files
|
|
1463
|
+
const memDir = join(this.getAgentsDir(), agentId + '-memory');
|
|
1464
|
+
if (!existsSync(memDir)) mkdirSync(memDir, { recursive: true });
|
|
1465
|
+
|
|
1466
|
+
for (const chunk of doc.chunks) {
|
|
1467
|
+
const entry = {
|
|
1468
|
+
id: `${doc.id}-${chunk.metadata.chunkIndex}`,
|
|
1469
|
+
content: chunk.content,
|
|
1470
|
+
summary: `[${filename}] ${chunk.title}`,
|
|
1471
|
+
timestamp: new Date().toISOString(),
|
|
1472
|
+
source: filename,
|
|
1473
|
+
docId: doc.id,
|
|
1474
|
+
tags: ['document-upload'],
|
|
1475
|
+
};
|
|
1476
|
+
writeFileSync(join(memDir, `${entry.id}.json`), JSON.stringify(entry, null, 2));
|
|
1477
|
+
learnedCount++;
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
// Save document metadata
|
|
1482
|
+
const docsDir = this.getDocumentsDir(agentId);
|
|
1483
|
+
const docMeta = {
|
|
1484
|
+
id: doc.id,
|
|
1485
|
+
filename: doc.filename,
|
|
1486
|
+
format: doc.format,
|
|
1487
|
+
size: doc.size,
|
|
1488
|
+
chunks: doc.chunks.length,
|
|
1489
|
+
processedAt: doc.processedAt,
|
|
1490
|
+
};
|
|
1491
|
+
writeFileSync(join(docsDir, `${doc.id}.json`), JSON.stringify(docMeta, null, 2));
|
|
1492
|
+
|
|
1493
|
+
res.writeHead(200, corsHeaders);
|
|
1494
|
+
res.end(JSON.stringify({ success: true, document: docMeta, learnedCount }));
|
|
1495
|
+
} catch (e: any) {
|
|
1496
|
+
res.writeHead(500, corsHeaders);
|
|
1497
|
+
res.end(JSON.stringify({ error: e.message || 'Upload failed' }));
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
private async parseMultipart(req: IncomingMessage): Promise<{ buffer: Buffer; filename: string }> {
|
|
1502
|
+
return new Promise((resolve, reject) => {
|
|
1503
|
+
const contentType = req.headers['content-type'] || '';
|
|
1504
|
+
const boundaryMatch = contentType.match(/boundary=(.+)/);
|
|
1505
|
+
|
|
1506
|
+
if (!boundaryMatch) {
|
|
1507
|
+
reject(new Error('Missing multipart boundary'));
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
const boundary = boundaryMatch[1];
|
|
1512
|
+
const chunks: Buffer[] = [];
|
|
1513
|
+
|
|
1514
|
+
req.on('data', (chunk: Buffer) => chunks.push(chunk));
|
|
1515
|
+
req.on('error', reject);
|
|
1516
|
+
req.on('end', () => {
|
|
1517
|
+
const body = Buffer.concat(chunks);
|
|
1518
|
+
const bodyStr = body.toString('latin1');
|
|
1519
|
+
const parts = bodyStr.split('--' + boundary).filter(p => p.trim() && p.trim() !== '--');
|
|
1520
|
+
|
|
1521
|
+
for (const part of parts) {
|
|
1522
|
+
const headerEnd = part.indexOf('\r\n\r\n');
|
|
1523
|
+
if (headerEnd === -1) continue;
|
|
1524
|
+
|
|
1525
|
+
const headers = part.slice(0, headerEnd);
|
|
1526
|
+
const filenameMatch = headers.match(/filename="([^"]+)"/);
|
|
1527
|
+
if (!filenameMatch) continue;
|
|
1528
|
+
|
|
1529
|
+
const filename = filenameMatch[1];
|
|
1530
|
+
// Extract binary content properly
|
|
1531
|
+
const contentStart = body.indexOf('\r\n\r\n', body.indexOf(Buffer.from(headers.slice(0, 40), 'latin1'))) + 4;
|
|
1532
|
+
const nextBoundary = body.indexOf(Buffer.from('\r\n--' + boundary, 'latin1'), contentStart);
|
|
1533
|
+
const fileBuffer = body.slice(contentStart, nextBoundary);
|
|
1534
|
+
|
|
1535
|
+
resolve({ buffer: fileBuffer, filename });
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
reject(new Error('No file found in upload'));
|
|
1540
|
+
});
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
private getDocumentList(agentId: string): any {
|
|
1545
|
+
const docsDir = this.getDocumentsDir(agentId);
|
|
1546
|
+
const files = readdirSync(docsDir).filter(f => f.endsWith('.json'));
|
|
1547
|
+
const documents = files.map(f => {
|
|
1548
|
+
try { return JSON.parse(readFileSync(join(docsDir, f), 'utf-8')); } catch { return null; }
|
|
1549
|
+
}).filter(Boolean).sort((a: any, b: any) =>
|
|
1550
|
+
new Date(b.processedAt).getTime() - new Date(a.processedAt).getTime()
|
|
1551
|
+
);
|
|
1552
|
+
return { documents };
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
private deleteDocument(agentId: string, docId: string): any {
|
|
1556
|
+
const docsDir = this.getDocumentsDir(agentId);
|
|
1557
|
+
const docPath = join(docsDir, `${docId}.json`);
|
|
1558
|
+
|
|
1559
|
+
if (!existsSync(docPath)) {
|
|
1560
|
+
return { error: 'Document not found' };
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
// Delete document metadata
|
|
1564
|
+
unlinkSync(docPath);
|
|
1565
|
+
|
|
1566
|
+
// Try to delete from DeepBrain
|
|
1567
|
+
try {
|
|
1568
|
+
// Remove memory entries with this docId
|
|
1569
|
+
const memDir = join(this.getAgentsDir(), agentId + '-memory');
|
|
1570
|
+
if (existsSync(memDir)) {
|
|
1571
|
+
const files = readdirSync(memDir).filter(f => f.startsWith(docId));
|
|
1572
|
+
for (const f of files) {
|
|
1573
|
+
unlinkSync(join(memDir, f));
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
} catch { /* best effort */ }
|
|
1577
|
+
|
|
1578
|
+
return { success: true, deleted: docId };
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
private readBody(req: IncomingMessage): Promise<string> {
|
|
1582
|
+
return new Promise((resolve, reject) => {
|
|
1583
|
+
let body = '';
|
|
1584
|
+
req.on('data', (chunk: any) => (body += chunk));
|
|
1585
|
+
req.on('end', () => resolve(body));
|
|
1586
|
+
req.on('error', reject);
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
export { StudioServer, StudioConfig };
|