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/dist/studio/server.js
CHANGED
|
@@ -37,22 +37,56 @@ exports.StudioServer = void 0;
|
|
|
37
37
|
const http_1 = require("http");
|
|
38
38
|
const fs_1 = require("fs");
|
|
39
39
|
const path_1 = require("path");
|
|
40
|
+
const os = __importStar(require("os"));
|
|
40
41
|
const net = __importStar(require("net"));
|
|
42
|
+
const templates_data_1 = require("./templates-data");
|
|
43
|
+
const marketplace_1 = require("../skills/marketplace");
|
|
44
|
+
const cron_engine_1 = require("../scheduler/cron-engine");
|
|
45
|
+
const image_generator_1 = require("../tools/image-generator");
|
|
46
|
+
const document_processor_1 = require("../tools/document-processor");
|
|
41
47
|
const MODULE_REGISTRY = [
|
|
42
48
|
{ name: 'DeepBrain', path: 'brain', port: 4001, icon: '🧠' },
|
|
43
49
|
{ name: 'AgentKits', path: 'kits', port: 4002, icon: '📊' },
|
|
44
50
|
{ name: 'Workstation', path: 'workstation', port: 4003, icon: '👤' },
|
|
45
51
|
];
|
|
52
|
+
// Settings config helpers
|
|
53
|
+
function getSettingsConfigPath() {
|
|
54
|
+
const dir = (0, path_1.join)(os.homedir(), '.opc');
|
|
55
|
+
if (!(0, fs_1.existsSync)(dir))
|
|
56
|
+
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
57
|
+
return (0, path_1.join)(dir, 'config.json');
|
|
58
|
+
}
|
|
59
|
+
function loadSettingsConfig() {
|
|
60
|
+
const p = getSettingsConfigPath();
|
|
61
|
+
if ((0, fs_1.existsSync)(p)) {
|
|
62
|
+
try {
|
|
63
|
+
return JSON.parse((0, fs_1.readFileSync)(p, 'utf-8'));
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return {};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return {};
|
|
70
|
+
}
|
|
71
|
+
function saveSettingsConfig(config) {
|
|
72
|
+
(0, fs_1.writeFileSync)(getSettingsConfigPath(), JSON.stringify(config, null, 2));
|
|
73
|
+
}
|
|
46
74
|
class StudioServer {
|
|
47
75
|
server;
|
|
48
76
|
config;
|
|
49
77
|
tracer;
|
|
78
|
+
skillMarketplace;
|
|
79
|
+
cronEngine;
|
|
80
|
+
imageGenerator;
|
|
50
81
|
constructor(config = {}) {
|
|
51
82
|
this.config = {
|
|
52
83
|
port: config.port || 4000,
|
|
53
84
|
agentDir: config.agentDir || process.cwd(),
|
|
54
85
|
staticDir: config.staticDir || (0, path_1.join)(__dirname, '../studio-ui'),
|
|
55
86
|
};
|
|
87
|
+
this.cronEngine = new cron_engine_1.CronEngine();
|
|
88
|
+
this.imageGenerator = new image_generator_1.ImageGenerator();
|
|
89
|
+
this.skillMarketplace = new marketplace_1.SkillMarketplace();
|
|
56
90
|
}
|
|
57
91
|
setTracer(tracer) {
|
|
58
92
|
this.tracer = tracer;
|
|
@@ -64,11 +98,19 @@ class StudioServer {
|
|
|
64
98
|
return { ...this.config };
|
|
65
99
|
}
|
|
66
100
|
async start() {
|
|
101
|
+
const opcDir = (0, path_1.join)(os.homedir(), '.opc');
|
|
102
|
+
if (!(0, fs_1.existsSync)(opcDir))
|
|
103
|
+
(0, fs_1.mkdirSync)(opcDir, { recursive: true });
|
|
104
|
+
const cfgPath = (0, path_1.join)(opcDir, 'config.json');
|
|
105
|
+
if (!(0, fs_1.existsSync)(cfgPath))
|
|
106
|
+
(0, fs_1.writeFileSync)(cfgPath, JSON.stringify({}, null, 2));
|
|
67
107
|
this.server = (0, http_1.createServer)((req, res) => this.handleRequest(req, res));
|
|
68
108
|
this.server.listen(this.config.port);
|
|
109
|
+
this.cronEngine.start();
|
|
69
110
|
console.log(`🎨 OPC Studio: http://localhost:${this.config.port}`);
|
|
70
111
|
}
|
|
71
112
|
async stop() {
|
|
113
|
+
this.cronEngine.stop();
|
|
72
114
|
return new Promise((resolve) => {
|
|
73
115
|
if (this.server) {
|
|
74
116
|
this.server.close(() => resolve());
|
|
@@ -107,6 +149,195 @@ class StudioServer {
|
|
|
107
149
|
const route = url.pathname.replace('/api/', '');
|
|
108
150
|
try {
|
|
109
151
|
let data;
|
|
152
|
+
// Dynamic agent routes
|
|
153
|
+
if (route === 'agents' && req.method === 'POST') {
|
|
154
|
+
data = await this.createAgent(req);
|
|
155
|
+
res.writeHead(201, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
156
|
+
res.end(JSON.stringify(data));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (route === 'agents' && req.method === 'GET') {
|
|
160
|
+
data = this.listAgents();
|
|
161
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
162
|
+
res.end(JSON.stringify(data));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (route === 'templates' && req.method === 'GET') {
|
|
166
|
+
const industry = url.searchParams.get('industry') || '';
|
|
167
|
+
const search = url.searchParams.get('q') || '';
|
|
168
|
+
data = this.getTemplates(industry, search);
|
|
169
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
170
|
+
res.end(JSON.stringify(data));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (route.match(/^templates\/[^/]+$/) && req.method === 'GET') {
|
|
174
|
+
const tplId = route.split('/')[1];
|
|
175
|
+
data = this.getTemplateById(tplId);
|
|
176
|
+
res.writeHead(data.error ? 404 : 200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
177
|
+
res.end(JSON.stringify(data));
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (route.match(/^agents\/[^/]+\/memory$/) && req.method === 'GET') {
|
|
181
|
+
const agentId = route.split('/')[1];
|
|
182
|
+
data = this.getAgentMemory(agentId);
|
|
183
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
184
|
+
res.end(JSON.stringify(data));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (route.match(/^agents\/[^/]+\/chat$/) && req.method === 'POST') {
|
|
188
|
+
const agentId = route.split('/')[1];
|
|
189
|
+
return this.handleAgentChat(req, res, agentId);
|
|
190
|
+
}
|
|
191
|
+
if (route.match(/^agents\/[^/]+$/) && req.method === 'GET') {
|
|
192
|
+
const agentId = route.split('/')[1];
|
|
193
|
+
data = this.getAgentById(agentId);
|
|
194
|
+
res.writeHead(data.error ? 404 : 200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
195
|
+
res.end(JSON.stringify(data));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (route.match(/^agents\/[^/]+$/) && req.method === 'PUT') {
|
|
199
|
+
const agentId = route.split('/')[1];
|
|
200
|
+
data = await this.updateAgent(agentId, req);
|
|
201
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
202
|
+
res.end(JSON.stringify(data));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (route.match(/^agents\/[^/]+$/) && req.method === 'DELETE') {
|
|
206
|
+
const agentId = route.split('/')[1];
|
|
207
|
+
data = this.deleteAgent(agentId);
|
|
208
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
209
|
+
res.end(JSON.stringify(data));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
// --- Document upload routes ---
|
|
213
|
+
if (route.match(/^agents\/[^/]+\/upload$/) && req.method === 'POST') {
|
|
214
|
+
const agentId = route.split('/')[1];
|
|
215
|
+
return this.handleDocumentUpload(req, res, agentId);
|
|
216
|
+
}
|
|
217
|
+
if (route.match(/^agents\/[^/]+\/documents$/) && req.method === 'GET') {
|
|
218
|
+
const agentId = route.split('/')[1];
|
|
219
|
+
data = this.getDocumentList(agentId);
|
|
220
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
221
|
+
res.end(JSON.stringify(data));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (route.match(/^agents\/[^/]+\/documents\/[^/]+$/) && req.method === 'DELETE') {
|
|
225
|
+
const parts = route.split('/');
|
|
226
|
+
const agentId = parts[1];
|
|
227
|
+
const docId = parts[3];
|
|
228
|
+
data = this.deleteDocument(agentId, docId);
|
|
229
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
230
|
+
res.end(JSON.stringify(data));
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
// --- Settings API routes ---
|
|
234
|
+
if (route === 'settings/models' && req.method === 'GET') {
|
|
235
|
+
const cfg = loadSettingsConfig();
|
|
236
|
+
data = cfg.models || { mode: 'local', provider: 'ollama', chatModel: 'qwen2.5:7b', embeddingModel: 'nomic-embed-text', providers: {} };
|
|
237
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
238
|
+
res.end(JSON.stringify(data));
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (route === 'settings/models' && req.method === 'PUT') {
|
|
242
|
+
const body = JSON.parse(await this.readBody(req));
|
|
243
|
+
const cfg = loadSettingsConfig();
|
|
244
|
+
cfg.models = { ...(cfg.models || {}), ...body };
|
|
245
|
+
saveSettingsConfig(cfg);
|
|
246
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
247
|
+
res.end(JSON.stringify({ success: true, models: cfg.models }));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (route === 'settings/models/test' && req.method === 'POST') {
|
|
251
|
+
const body = JSON.parse(await this.readBody(req));
|
|
252
|
+
const { provider, apiKey, baseUrl } = body;
|
|
253
|
+
data = await this.testModelConnection(provider, apiKey, baseUrl);
|
|
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/local' && req.method === 'GET') {
|
|
259
|
+
data = await this.detectLocalOllama();
|
|
260
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
261
|
+
res.end(JSON.stringify(data));
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (route === 'settings/channels' && req.method === 'GET') {
|
|
265
|
+
const cfg = loadSettingsConfig();
|
|
266
|
+
data = cfg.channels || {};
|
|
267
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
268
|
+
res.end(JSON.stringify(data));
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (route.match(/^settings\/channels\/[^/]+$/) && req.method === 'PUT') {
|
|
272
|
+
const channelName = route.split('/')[2];
|
|
273
|
+
const body = JSON.parse(await this.readBody(req));
|
|
274
|
+
const cfg = loadSettingsConfig();
|
|
275
|
+
if (!cfg.channels)
|
|
276
|
+
cfg.channels = {};
|
|
277
|
+
cfg.channels[channelName] = { ...(cfg.channels[channelName] || {}), ...body, updated: new Date().toISOString() };
|
|
278
|
+
saveSettingsConfig(cfg);
|
|
279
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
280
|
+
res.end(JSON.stringify({ success: true, channel: cfg.channels[channelName] }));
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
// Web Search settings
|
|
284
|
+
if (route === 'settings/search' && req.method === 'GET') {
|
|
285
|
+
const cfg = loadSettingsConfig();
|
|
286
|
+
data = cfg.webSearch || { defaultEngine: 'duckduckgo', enabled: true, engines: { duckduckgo: { enabled: true } } };
|
|
287
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
288
|
+
res.end(JSON.stringify(data));
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (route === 'settings/search' && req.method === 'PUT') {
|
|
292
|
+
const body = JSON.parse(await this.readBody(req));
|
|
293
|
+
const cfg = loadSettingsConfig();
|
|
294
|
+
cfg.webSearch = { ...(cfg.webSearch || {}), ...body, updated: new Date().toISOString() };
|
|
295
|
+
saveSettingsConfig(cfg);
|
|
296
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
297
|
+
res.end(JSON.stringify({ success: true, config: cfg.webSearch }));
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (route === 'settings/search/test' && req.method === 'POST') {
|
|
301
|
+
try {
|
|
302
|
+
const { webSearch: doSearch } = require('../tools/web-search');
|
|
303
|
+
const body = JSON.parse(await this.readBody(req));
|
|
304
|
+
const query = body.query || 'test search';
|
|
305
|
+
const cfg = loadSettingsConfig();
|
|
306
|
+
const searchCfg = { ...(cfg.webSearch || { defaultEngine: 'duckduckgo', enabled: true, engines: { duckduckgo: { enabled: true } } }), ...body.config };
|
|
307
|
+
const results = await doSearch(query, searchCfg, { maxResults: 3 });
|
|
308
|
+
data = { success: true, results, engine: searchCfg.defaultEngine };
|
|
309
|
+
}
|
|
310
|
+
catch (e) {
|
|
311
|
+
data = { success: false, error: e.message };
|
|
312
|
+
}
|
|
313
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
314
|
+
res.end(JSON.stringify(data));
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (route === 'settings/status' && req.method === 'GET') {
|
|
318
|
+
data = await this.getSettingsStatus();
|
|
319
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
320
|
+
res.end(JSON.stringify(data));
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (route === 'settings/status/start' && req.method === 'POST') {
|
|
324
|
+
data = { success: true, status: 'running', message: 'Agent started' };
|
|
325
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
326
|
+
res.end(JSON.stringify(data));
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (route === 'settings/status/stop' && req.method === 'POST') {
|
|
330
|
+
data = { success: true, status: 'stopped', message: 'Agent stopped' };
|
|
331
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
332
|
+
res.end(JSON.stringify(data));
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (route === 'settings/usage' && req.method === 'GET') {
|
|
336
|
+
data = await this.getUsageStats();
|
|
337
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
338
|
+
res.end(JSON.stringify(data));
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
110
341
|
// Dynamic workflow routes (parameterized)
|
|
111
342
|
if (route.match(/^workflows\/[^/]+\/run$/) && req.method === 'POST') {
|
|
112
343
|
const wfId = route.split('/')[1];
|
|
@@ -129,6 +360,119 @@ class StudioServer {
|
|
|
129
360
|
res.end(JSON.stringify(data));
|
|
130
361
|
return;
|
|
131
362
|
}
|
|
363
|
+
// --- Schedules API ---
|
|
364
|
+
if (route === 'schedules' && req.method === 'GET') {
|
|
365
|
+
data = this.cronEngine.listTasks();
|
|
366
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
367
|
+
res.end(JSON.stringify(data));
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (route === 'schedules' && req.method === 'POST') {
|
|
371
|
+
const body = JSON.parse(await this.readBody(req));
|
|
372
|
+
data = this.cronEngine.createTask(body);
|
|
373
|
+
res.writeHead(201, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
374
|
+
res.end(JSON.stringify(data));
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (route.match(/^schedules\/[^/]+$/) && req.method === 'PUT') {
|
|
378
|
+
const id = route.split('/')[1];
|
|
379
|
+
const body = JSON.parse(await this.readBody(req));
|
|
380
|
+
data = this.cronEngine.updateTask(id, body);
|
|
381
|
+
res.writeHead(data ? 200 : 404, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
382
|
+
res.end(JSON.stringify(data || { error: 'Schedule not found' }));
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (route.match(/^schedules\/[^/]+$/) && req.method === 'DELETE') {
|
|
386
|
+
const id = route.split('/')[1];
|
|
387
|
+
const success = this.cronEngine.deleteTask(id);
|
|
388
|
+
res.writeHead(success ? 200 : 404, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
389
|
+
res.end(JSON.stringify({ success }));
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
if (route.match(/^schedules\/[^/]+\/run$/) && req.method === 'POST') {
|
|
393
|
+
const id = route.split('/')[1];
|
|
394
|
+
const success = await this.cronEngine.runTask(id);
|
|
395
|
+
res.writeHead(success ? 200 : 404, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
396
|
+
res.end(JSON.stringify({ success }));
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
// --- Image Generation API ---
|
|
400
|
+
if (route === 'image-gen/status' && req.method === 'GET') {
|
|
401
|
+
data = this.imageGenerator.getStatus();
|
|
402
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
403
|
+
res.end(JSON.stringify(data));
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (route === 'image-gen/generate' && req.method === 'POST') {
|
|
407
|
+
const body = JSON.parse(await this.readBody(req));
|
|
408
|
+
data = await this.imageGenerator.generate(body.prompt, body);
|
|
409
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
410
|
+
res.end(JSON.stringify(data));
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
if (route === 'image-gen/config' && req.method === 'PUT') {
|
|
414
|
+
const body = JSON.parse(await this.readBody(req));
|
|
415
|
+
const cfg = loadSettingsConfig();
|
|
416
|
+
cfg.imageGen = { ...(cfg.imageGen || {}), ...body };
|
|
417
|
+
saveSettingsConfig(cfg);
|
|
418
|
+
this.imageGenerator = new image_generator_1.ImageGenerator({
|
|
419
|
+
openaiApiKey: body.openaiApiKey,
|
|
420
|
+
replicateApiKey: body.replicateApiKey,
|
|
421
|
+
sdApiUrl: body.sdApiUrl,
|
|
422
|
+
});
|
|
423
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
424
|
+
res.end(JSON.stringify({ success: true }));
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
if (route === 'first-run/status' && req.method === 'GET') {
|
|
428
|
+
data = await this.getFirstRunStatus();
|
|
429
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
430
|
+
res.end(JSON.stringify(data));
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (route === 'first-run/complete' && req.method === 'POST') {
|
|
434
|
+
const body = JSON.parse(await this.readBody(req));
|
|
435
|
+
data = await this.completeFirstRun(body);
|
|
436
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
437
|
+
res.end(JSON.stringify(data));
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
// === Skill Marketplace API ===
|
|
441
|
+
if (route === 'skills/marketplace' && req.method === 'GET') {
|
|
442
|
+
const category = url.searchParams.get('category') || undefined;
|
|
443
|
+
const search = url.searchParams.get('q') || undefined;
|
|
444
|
+
data = this.skillMarketplace.listAll(category, search);
|
|
445
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
446
|
+
res.end(JSON.stringify(data));
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
if (route === 'skills/installed' && req.method === 'GET') {
|
|
450
|
+
data = this.skillMarketplace.getInstalled();
|
|
451
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
452
|
+
res.end(JSON.stringify(data));
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (route.match(/^skills\/marketplace\/[^/]+$/) && req.method === 'GET') {
|
|
456
|
+
const skillId = route.split('/')[2];
|
|
457
|
+
data = this.skillMarketplace.getSkill(skillId);
|
|
458
|
+
res.writeHead(data ? 200 : 404, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
459
|
+
res.end(JSON.stringify(data || { error: 'Skill not found' }));
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
if (route.match(/^skills\/marketplace\/[^/]+\/install$/) && req.method === 'POST') {
|
|
463
|
+
const skillId = route.split('/')[2];
|
|
464
|
+
data = this.skillMarketplace.install(skillId);
|
|
465
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
466
|
+
res.end(JSON.stringify(data));
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (route.match(/^skills\/marketplace\/[^/]+\/uninstall$/) && req.method === 'DELETE') {
|
|
470
|
+
const skillId = route.split('/')[2];
|
|
471
|
+
data = this.skillMarketplace.uninstall(skillId);
|
|
472
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
473
|
+
res.end(JSON.stringify(data));
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
132
476
|
switch (route) {
|
|
133
477
|
case 'modules':
|
|
134
478
|
data = await this.getModulesStatus();
|
|
@@ -266,6 +610,254 @@ class StudioServer {
|
|
|
266
610
|
res.end(JSON.stringify({ error: e.message }));
|
|
267
611
|
}
|
|
268
612
|
}
|
|
613
|
+
// --- Agent CRUD & Templates ---
|
|
614
|
+
getAgentsDir() {
|
|
615
|
+
const dir = (0, path_1.join)(os.homedir(), '.opc', 'agents');
|
|
616
|
+
if (!(0, fs_1.existsSync)(dir))
|
|
617
|
+
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
618
|
+
return dir;
|
|
619
|
+
}
|
|
620
|
+
async createAgent(req) {
|
|
621
|
+
const body = await this.readBody(req);
|
|
622
|
+
const { name, templateId, description, model, language } = JSON.parse(body);
|
|
623
|
+
const template = templates_data_1.TEMPLATES.find(t => t.id === templateId);
|
|
624
|
+
const id = `agent-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
625
|
+
const agent = {
|
|
626
|
+
id,
|
|
627
|
+
name: name || template?.name || 'My Agent',
|
|
628
|
+
templateId: templateId || null,
|
|
629
|
+
templateName: template?.name || 'Custom',
|
|
630
|
+
templateIcon: template?.icon || '🤖',
|
|
631
|
+
description: description || template?.description || '',
|
|
632
|
+
model: model || template?.suggestedModel || 'gpt-4o-mini',
|
|
633
|
+
language: language || 'en',
|
|
634
|
+
systemPrompt: template?.systemPrompt || 'You are a helpful assistant.',
|
|
635
|
+
industry: template?.industry || 'general',
|
|
636
|
+
created: new Date().toISOString(),
|
|
637
|
+
updated: new Date().toISOString(),
|
|
638
|
+
messageCount: 0,
|
|
639
|
+
lastActive: new Date().toISOString(),
|
|
640
|
+
};
|
|
641
|
+
const filePath = (0, path_1.join)(this.getAgentsDir(), `${id}.json`);
|
|
642
|
+
(0, fs_1.writeFileSync)(filePath, JSON.stringify(agent, null, 2));
|
|
643
|
+
return agent;
|
|
644
|
+
}
|
|
645
|
+
listAgents() {
|
|
646
|
+
const dir = this.getAgentsDir();
|
|
647
|
+
const files = (0, fs_1.readdirSync)(dir).filter(f => f.endsWith('.json'));
|
|
648
|
+
const agents = files.map(f => {
|
|
649
|
+
try {
|
|
650
|
+
return JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(dir, f), 'utf-8'));
|
|
651
|
+
}
|
|
652
|
+
catch {
|
|
653
|
+
return null;
|
|
654
|
+
}
|
|
655
|
+
}).filter(Boolean).sort((a, b) => new Date(b.updated).getTime() - new Date(a.updated).getTime());
|
|
656
|
+
return { agents };
|
|
657
|
+
}
|
|
658
|
+
getAgentById(id) {
|
|
659
|
+
const filePath = (0, path_1.join)(this.getAgentsDir(), `${id}.json`);
|
|
660
|
+
if (!(0, fs_1.existsSync)(filePath))
|
|
661
|
+
return { error: 'Agent not found' };
|
|
662
|
+
return JSON.parse((0, fs_1.readFileSync)(filePath, 'utf-8'));
|
|
663
|
+
}
|
|
664
|
+
async updateAgent(id, req) {
|
|
665
|
+
const filePath = (0, path_1.join)(this.getAgentsDir(), `${id}.json`);
|
|
666
|
+
if (!(0, fs_1.existsSync)(filePath))
|
|
667
|
+
return { error: 'Agent not found' };
|
|
668
|
+
const existing = JSON.parse((0, fs_1.readFileSync)(filePath, 'utf-8'));
|
|
669
|
+
const body = await this.readBody(req);
|
|
670
|
+
const updates = JSON.parse(body);
|
|
671
|
+
const updated = { ...existing, ...updates, id, updated: new Date().toISOString() };
|
|
672
|
+
(0, fs_1.writeFileSync)(filePath, JSON.stringify(updated, null, 2));
|
|
673
|
+
return updated;
|
|
674
|
+
}
|
|
675
|
+
deleteAgent(id) {
|
|
676
|
+
const filePath = (0, path_1.join)(this.getAgentsDir(), `${id}.json`);
|
|
677
|
+
if ((0, fs_1.existsSync)(filePath))
|
|
678
|
+
(0, fs_1.unlinkSync)(filePath);
|
|
679
|
+
return { success: true };
|
|
680
|
+
}
|
|
681
|
+
getTemplates(industry, search) {
|
|
682
|
+
let filtered = templates_data_1.TEMPLATES;
|
|
683
|
+
if (industry)
|
|
684
|
+
filtered = filtered.filter(t => t.industry === industry);
|
|
685
|
+
if (search) {
|
|
686
|
+
const q = search.toLowerCase();
|
|
687
|
+
filtered = filtered.filter(t => 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
|
+
return { templates: filtered, industries: templates_data_1.INDUSTRIES };
|
|
692
|
+
}
|
|
693
|
+
getTemplateById(id) {
|
|
694
|
+
const tpl = templates_data_1.TEMPLATES.find(t => t.id === id);
|
|
695
|
+
return tpl || { error: 'Template not found' };
|
|
696
|
+
}
|
|
697
|
+
getAgentMemory(agentId) {
|
|
698
|
+
const memDir = (0, path_1.join)(this.getAgentsDir(), agentId + '-memory');
|
|
699
|
+
if (!(0, fs_1.existsSync)(memDir))
|
|
700
|
+
return { entries: [], timeline: [] };
|
|
701
|
+
const files = (0, fs_1.readdirSync)(memDir).filter(f => f.endsWith('.json'));
|
|
702
|
+
const entries = files.map(f => {
|
|
703
|
+
try {
|
|
704
|
+
return JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(memDir, f), 'utf-8'));
|
|
705
|
+
}
|
|
706
|
+
catch {
|
|
707
|
+
return null;
|
|
708
|
+
}
|
|
709
|
+
}).filter(Boolean).sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
710
|
+
return { entries, timeline: entries.map((e) => ({ date: e.timestamp, summary: e.summary || e.content?.slice(0, 100) })) };
|
|
711
|
+
}
|
|
712
|
+
async handleAgentChat(req, res, agentId) {
|
|
713
|
+
const body = JSON.parse(await this.readBody(req));
|
|
714
|
+
const { messages = [] } = body;
|
|
715
|
+
const agent = this.getAgentById(agentId);
|
|
716
|
+
if (agent.error) {
|
|
717
|
+
res.writeHead(404, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
718
|
+
res.end(JSON.stringify(agent));
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
// Update message count
|
|
722
|
+
agent.messageCount = (agent.messageCount || 0) + 1;
|
|
723
|
+
agent.lastActive = new Date().toISOString();
|
|
724
|
+
agent.updated = new Date().toISOString();
|
|
725
|
+
const filePath = (0, path_1.join)(this.getAgentsDir(), `${agentId}.json`);
|
|
726
|
+
(0, fs_1.writeFileSync)(filePath, JSON.stringify(agent, null, 2));
|
|
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
|
+
const allMsgs = [{ role: 'system', content: agent.systemPrompt }, ...messages];
|
|
735
|
+
const lastMsg = allMsgs[allMsgs.length - 1]?.content || '';
|
|
736
|
+
// Try to call the real /v1/chat/completions endpoint
|
|
737
|
+
try {
|
|
738
|
+
const completionReq = (0, http_1.request)({
|
|
739
|
+
hostname: 'localhost',
|
|
740
|
+
port: this.config.port,
|
|
741
|
+
path: '/v1/chat/completions',
|
|
742
|
+
method: 'POST',
|
|
743
|
+
headers: { 'Content-Type': 'application/json' },
|
|
744
|
+
}, (completionRes) => {
|
|
745
|
+
if (completionRes.statusCode === 200) {
|
|
746
|
+
completionRes.pipe(res);
|
|
747
|
+
}
|
|
748
|
+
else {
|
|
749
|
+
// Fallback to simulated response
|
|
750
|
+
this.sendSimulatedResponse(res, lastMsg, agent);
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
completionReq.on('error', () => {
|
|
754
|
+
this.sendSimulatedResponse(res, lastMsg, agent);
|
|
755
|
+
});
|
|
756
|
+
completionReq.write(JSON.stringify({
|
|
757
|
+
model: agent.model,
|
|
758
|
+
messages: allMsgs,
|
|
759
|
+
stream: true,
|
|
760
|
+
}));
|
|
761
|
+
completionReq.end();
|
|
762
|
+
}
|
|
763
|
+
catch {
|
|
764
|
+
this.sendSimulatedResponse(res, lastMsg, agent);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
sendSimulatedResponse(res, lastMsg, agent) {
|
|
768
|
+
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)`;
|
|
769
|
+
const words = response.split(' ');
|
|
770
|
+
let i = 0;
|
|
771
|
+
const interval = setInterval(() => {
|
|
772
|
+
if (i >= words.length) {
|
|
773
|
+
res.write('data: [DONE]\n\n');
|
|
774
|
+
res.end();
|
|
775
|
+
clearInterval(interval);
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
const chunk = (i === 0 ? '' : ' ') + words[i];
|
|
779
|
+
res.write(`data: ${JSON.stringify({ choices: [{ delta: { content: chunk } }] })}\n\n`);
|
|
780
|
+
i++;
|
|
781
|
+
}, 50);
|
|
782
|
+
}
|
|
783
|
+
// --- Settings Implementations ---
|
|
784
|
+
async detectLocalOllama() {
|
|
785
|
+
return new Promise((resolve) => {
|
|
786
|
+
const req = (0, http_1.request)({ hostname: 'localhost', port: 11434, path: '/api/tags', method: 'GET', timeout: 3000 }, (res) => {
|
|
787
|
+
let body = '';
|
|
788
|
+
res.on('data', (c) => body += c);
|
|
789
|
+
res.on('end', () => {
|
|
790
|
+
try {
|
|
791
|
+
const data = JSON.parse(body);
|
|
792
|
+
const models = (data.models || []).map((m) => ({
|
|
793
|
+
name: m.name, size: m.size, modified: m.modified_at,
|
|
794
|
+
details: m.details || {},
|
|
795
|
+
}));
|
|
796
|
+
resolve({ running: true, models });
|
|
797
|
+
}
|
|
798
|
+
catch {
|
|
799
|
+
resolve({ running: true, models: [] });
|
|
800
|
+
}
|
|
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
|
+
async testModelConnection(provider, apiKey, baseUrl) {
|
|
809
|
+
const endpoints = {
|
|
810
|
+
openai: { url: 'api.openai.com', path: '/v1/models' },
|
|
811
|
+
deepseek: { url: 'api.deepseek.com', path: '/v1/models' },
|
|
812
|
+
anthropic: { url: 'api.anthropic.com', path: '/v1/models' },
|
|
813
|
+
openrouter: { url: 'openrouter.ai', path: '/api/v1/models' },
|
|
814
|
+
};
|
|
815
|
+
const ep = endpoints[provider];
|
|
816
|
+
if (!ep && !baseUrl)
|
|
817
|
+
return { success: false, error: 'Unknown provider' };
|
|
818
|
+
const hostname = baseUrl ? new URL(baseUrl).hostname : ep.url;
|
|
819
|
+
const path = baseUrl ? '/v1/models' : ep.path;
|
|
820
|
+
const headers = { 'Authorization': `Bearer ${apiKey}` };
|
|
821
|
+
if (provider === 'anthropic') {
|
|
822
|
+
headers['x-api-key'] = apiKey;
|
|
823
|
+
headers['anthropic-version'] = '2023-06-01';
|
|
824
|
+
delete headers['Authorization'];
|
|
825
|
+
}
|
|
826
|
+
return new Promise((resolve) => {
|
|
827
|
+
const https = require('https');
|
|
828
|
+
const req = https.request({ hostname, path, method: 'GET', headers, timeout: 10000 }, (res) => {
|
|
829
|
+
resolve({ success: res.statusCode === 200, statusCode: res.statusCode });
|
|
830
|
+
});
|
|
831
|
+
req.on('error', (e) => resolve({ success: false, error: e.message }));
|
|
832
|
+
req.on('timeout', () => { req.destroy(); resolve({ success: false, error: 'Timeout' }); });
|
|
833
|
+
req.end();
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
async getSettingsStatus() {
|
|
837
|
+
const uptime = process.uptime();
|
|
838
|
+
const mem = process.memoryUsage();
|
|
839
|
+
const modules = await this.getModulesStatus();
|
|
840
|
+
const logPath = (0, path_1.join)(this.config.agentDir, '.opc', 'agent.log');
|
|
841
|
+
let recentLogs = [];
|
|
842
|
+
if ((0, fs_1.existsSync)(logPath)) {
|
|
843
|
+
const content = (0, fs_1.readFileSync)(logPath, 'utf-8');
|
|
844
|
+
recentLogs = content.split('\n').slice(-50);
|
|
845
|
+
}
|
|
846
|
+
return {
|
|
847
|
+
status: 'running',
|
|
848
|
+
uptime,
|
|
849
|
+
memory: { rss: mem.rss, heapUsed: mem.heapUsed, heapTotal: mem.heapTotal },
|
|
850
|
+
cpu: os.loadavg(),
|
|
851
|
+
modules: modules.modules,
|
|
852
|
+
logs: recentLogs,
|
|
853
|
+
startedAt: new Date(Date.now() - uptime * 1000).toISOString(),
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
async getUsageStats() {
|
|
857
|
+
const cfg = loadSettingsConfig();
|
|
858
|
+
const usage = cfg.usage || { totalTokens: 0, totalCost: 0, daily: [], byModel: {} };
|
|
859
|
+
return usage;
|
|
860
|
+
}
|
|
269
861
|
// --- API Implementations ---
|
|
270
862
|
async getAgentInfo() {
|
|
271
863
|
const oad = this.loadOAD();
|
|
@@ -640,6 +1232,32 @@ class StudioServer {
|
|
|
640
1232
|
return { error: err.message };
|
|
641
1233
|
}
|
|
642
1234
|
}
|
|
1235
|
+
async getFirstRunStatus() {
|
|
1236
|
+
const configPath = (0, path_1.join)(os.homedir(), '.opc', 'config.json');
|
|
1237
|
+
const ollamaStatus = await this.detectLocalOllama();
|
|
1238
|
+
if (!(0, fs_1.existsSync)(configPath)) {
|
|
1239
|
+
return { firstRun: true, ollamaDetected: ollamaStatus.running, ollamaModels: ollamaStatus.models || [] };
|
|
1240
|
+
}
|
|
1241
|
+
try {
|
|
1242
|
+
const config = JSON.parse((0, fs_1.readFileSync)(configPath, 'utf-8'));
|
|
1243
|
+
return { firstRun: !config.firstRunComplete, ollamaDetected: ollamaStatus.running, ollamaModels: ollamaStatus.models || [] };
|
|
1244
|
+
}
|
|
1245
|
+
catch {
|
|
1246
|
+
return { firstRun: true, ollamaDetected: ollamaStatus.running, ollamaModels: ollamaStatus.models || [] };
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
async completeFirstRun(choices) {
|
|
1250
|
+
const cfg = loadSettingsConfig();
|
|
1251
|
+
cfg.firstRunComplete = true;
|
|
1252
|
+
if (choices) {
|
|
1253
|
+
if (choices.templateId)
|
|
1254
|
+
cfg.firstRunTemplate = choices.templateId;
|
|
1255
|
+
if (choices.model)
|
|
1256
|
+
cfg.models = { ...(cfg.models || {}), chatModel: choices.model };
|
|
1257
|
+
}
|
|
1258
|
+
saveSettingsConfig(cfg);
|
|
1259
|
+
return { success: true };
|
|
1260
|
+
}
|
|
643
1261
|
// --- Module Proxy & Health ---
|
|
644
1262
|
proxyToModule(req, res, mod, url) {
|
|
645
1263
|
const targetPath = url.pathname.slice(`/${mod.path}`.length) || '/';
|
|
@@ -763,6 +1381,168 @@ class StudioServer {
|
|
|
763
1381
|
res.write('data: [DONE]\n\n');
|
|
764
1382
|
res.end();
|
|
765
1383
|
}
|
|
1384
|
+
// --- Document upload handlers ---
|
|
1385
|
+
getDocumentsDir(agentId) {
|
|
1386
|
+
const dir = (0, path_1.join)(this.getAgentsDir(), agentId + '-documents');
|
|
1387
|
+
if (!(0, fs_1.existsSync)(dir))
|
|
1388
|
+
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
1389
|
+
return dir;
|
|
1390
|
+
}
|
|
1391
|
+
async handleDocumentUpload(req, res, agentId) {
|
|
1392
|
+
const corsHeaders = { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' };
|
|
1393
|
+
try {
|
|
1394
|
+
// Parse multipart form data manually
|
|
1395
|
+
const { buffer, filename } = await this.parseMultipart(req);
|
|
1396
|
+
if (!filename) {
|
|
1397
|
+
res.writeHead(400, corsHeaders);
|
|
1398
|
+
res.end(JSON.stringify({ error: 'No file uploaded' }));
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
if (buffer.length > 50 * 1024 * 1024) {
|
|
1402
|
+
res.writeHead(413, corsHeaders);
|
|
1403
|
+
res.end(JSON.stringify({ error: 'File too large (max 50MB)' }));
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
// Process document
|
|
1407
|
+
const processor = new document_processor_1.DocumentProcessor();
|
|
1408
|
+
const doc = await processor.process(buffer, filename);
|
|
1409
|
+
// Store chunks via DeepBrain learn()
|
|
1410
|
+
let learnedCount = 0;
|
|
1411
|
+
try {
|
|
1412
|
+
const { Brain } = require('deepbrain');
|
|
1413
|
+
const oad = this.loadOAD();
|
|
1414
|
+
const dbPath = oad?.spec?.memory?.longTerm?.database || './data/brain.db';
|
|
1415
|
+
const brain = new Brain({ database: dbPath, embedding_provider: 'ollama' });
|
|
1416
|
+
await brain.connect();
|
|
1417
|
+
for (const chunk of doc.chunks) {
|
|
1418
|
+
const content = `[Source: ${filename}] ${chunk.title}\n\n${chunk.content}`;
|
|
1419
|
+
if (typeof brain.store === 'function') {
|
|
1420
|
+
await brain.store('documents', `${doc.id}-${chunk.metadata.chunkIndex}`, content, {
|
|
1421
|
+
source: filename,
|
|
1422
|
+
docId: doc.id,
|
|
1423
|
+
chunkIndex: chunk.metadata.chunkIndex,
|
|
1424
|
+
tags: ['document-upload', filename],
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
else if (typeof brain.learn === 'function') {
|
|
1428
|
+
await brain.learn(content, {
|
|
1429
|
+
tags: ['document-upload', filename],
|
|
1430
|
+
slug: `${doc.id}-${chunk.metadata.chunkIndex}`,
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
learnedCount++;
|
|
1434
|
+
}
|
|
1435
|
+
await brain.disconnect();
|
|
1436
|
+
}
|
|
1437
|
+
catch {
|
|
1438
|
+
// If DeepBrain is not available, store in local memory files
|
|
1439
|
+
const memDir = (0, path_1.join)(this.getAgentsDir(), agentId + '-memory');
|
|
1440
|
+
if (!(0, fs_1.existsSync)(memDir))
|
|
1441
|
+
(0, fs_1.mkdirSync)(memDir, { recursive: true });
|
|
1442
|
+
for (const chunk of doc.chunks) {
|
|
1443
|
+
const entry = {
|
|
1444
|
+
id: `${doc.id}-${chunk.metadata.chunkIndex}`,
|
|
1445
|
+
content: chunk.content,
|
|
1446
|
+
summary: `[${filename}] ${chunk.title}`,
|
|
1447
|
+
timestamp: new Date().toISOString(),
|
|
1448
|
+
source: filename,
|
|
1449
|
+
docId: doc.id,
|
|
1450
|
+
tags: ['document-upload'],
|
|
1451
|
+
};
|
|
1452
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(memDir, `${entry.id}.json`), JSON.stringify(entry, null, 2));
|
|
1453
|
+
learnedCount++;
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
// Save document metadata
|
|
1457
|
+
const docsDir = this.getDocumentsDir(agentId);
|
|
1458
|
+
const docMeta = {
|
|
1459
|
+
id: doc.id,
|
|
1460
|
+
filename: doc.filename,
|
|
1461
|
+
format: doc.format,
|
|
1462
|
+
size: doc.size,
|
|
1463
|
+
chunks: doc.chunks.length,
|
|
1464
|
+
processedAt: doc.processedAt,
|
|
1465
|
+
};
|
|
1466
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(docsDir, `${doc.id}.json`), JSON.stringify(docMeta, null, 2));
|
|
1467
|
+
res.writeHead(200, corsHeaders);
|
|
1468
|
+
res.end(JSON.stringify({ success: true, document: docMeta, learnedCount }));
|
|
1469
|
+
}
|
|
1470
|
+
catch (e) {
|
|
1471
|
+
res.writeHead(500, corsHeaders);
|
|
1472
|
+
res.end(JSON.stringify({ error: e.message || 'Upload failed' }));
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
async parseMultipart(req) {
|
|
1476
|
+
return new Promise((resolve, reject) => {
|
|
1477
|
+
const contentType = req.headers['content-type'] || '';
|
|
1478
|
+
const boundaryMatch = contentType.match(/boundary=(.+)/);
|
|
1479
|
+
if (!boundaryMatch) {
|
|
1480
|
+
reject(new Error('Missing multipart boundary'));
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
const boundary = boundaryMatch[1];
|
|
1484
|
+
const chunks = [];
|
|
1485
|
+
req.on('data', (chunk) => chunks.push(chunk));
|
|
1486
|
+
req.on('error', reject);
|
|
1487
|
+
req.on('end', () => {
|
|
1488
|
+
const body = Buffer.concat(chunks);
|
|
1489
|
+
const bodyStr = body.toString('latin1');
|
|
1490
|
+
const parts = bodyStr.split('--' + boundary).filter(p => p.trim() && p.trim() !== '--');
|
|
1491
|
+
for (const part of parts) {
|
|
1492
|
+
const headerEnd = part.indexOf('\r\n\r\n');
|
|
1493
|
+
if (headerEnd === -1)
|
|
1494
|
+
continue;
|
|
1495
|
+
const headers = part.slice(0, headerEnd);
|
|
1496
|
+
const filenameMatch = headers.match(/filename="([^"]+)"/);
|
|
1497
|
+
if (!filenameMatch)
|
|
1498
|
+
continue;
|
|
1499
|
+
const filename = filenameMatch[1];
|
|
1500
|
+
// Extract binary content properly
|
|
1501
|
+
const contentStart = body.indexOf('\r\n\r\n', body.indexOf(Buffer.from(headers.slice(0, 40), 'latin1'))) + 4;
|
|
1502
|
+
const nextBoundary = body.indexOf(Buffer.from('\r\n--' + boundary, 'latin1'), contentStart);
|
|
1503
|
+
const fileBuffer = body.slice(contentStart, nextBoundary);
|
|
1504
|
+
resolve({ buffer: fileBuffer, filename });
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
reject(new Error('No file found in upload'));
|
|
1508
|
+
});
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
getDocumentList(agentId) {
|
|
1512
|
+
const docsDir = this.getDocumentsDir(agentId);
|
|
1513
|
+
const files = (0, fs_1.readdirSync)(docsDir).filter(f => f.endsWith('.json'));
|
|
1514
|
+
const documents = files.map(f => {
|
|
1515
|
+
try {
|
|
1516
|
+
return JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(docsDir, f), 'utf-8'));
|
|
1517
|
+
}
|
|
1518
|
+
catch {
|
|
1519
|
+
return null;
|
|
1520
|
+
}
|
|
1521
|
+
}).filter(Boolean).sort((a, b) => new Date(b.processedAt).getTime() - new Date(a.processedAt).getTime());
|
|
1522
|
+
return { documents };
|
|
1523
|
+
}
|
|
1524
|
+
deleteDocument(agentId, docId) {
|
|
1525
|
+
const docsDir = this.getDocumentsDir(agentId);
|
|
1526
|
+
const docPath = (0, path_1.join)(docsDir, `${docId}.json`);
|
|
1527
|
+
if (!(0, fs_1.existsSync)(docPath)) {
|
|
1528
|
+
return { error: 'Document not found' };
|
|
1529
|
+
}
|
|
1530
|
+
// Delete document metadata
|
|
1531
|
+
(0, fs_1.unlinkSync)(docPath);
|
|
1532
|
+
// Try to delete from DeepBrain
|
|
1533
|
+
try {
|
|
1534
|
+
// Remove memory entries with this docId
|
|
1535
|
+
const memDir = (0, path_1.join)(this.getAgentsDir(), agentId + '-memory');
|
|
1536
|
+
if ((0, fs_1.existsSync)(memDir)) {
|
|
1537
|
+
const files = (0, fs_1.readdirSync)(memDir).filter(f => f.startsWith(docId));
|
|
1538
|
+
for (const f of files) {
|
|
1539
|
+
(0, fs_1.unlinkSync)((0, path_1.join)(memDir, f));
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
catch { /* best effort */ }
|
|
1544
|
+
return { success: true, deleted: docId };
|
|
1545
|
+
}
|
|
766
1546
|
readBody(req) {
|
|
767
1547
|
return new Promise((resolve, reject) => {
|
|
768
1548
|
let body = '';
|