opc-agent 4.0.43 → 4.1.0
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/dist/cli.js +2 -2
- package/dist/core/runtime.js +18 -0
- package/dist/providers/index.d.ts +6 -0
- package/dist/providers/index.js +100 -10
- package/dist/schema/oad.d.ts +34 -34
- package/dist/studio/server.js +181 -19
- package/dist/studio-ui/index.html +49 -14
- package/package.json +1 -1
- package/src/cli.ts +2 -2
- package/src/core/runtime.ts +18 -0
- package/src/providers/index.ts +95 -10
- package/src/studio/server.ts +178 -21
- package/src/studio-ui/index.html +49 -14
package/src/studio/server.ts
CHANGED
|
@@ -181,12 +181,91 @@ class StudioServer {
|
|
|
181
181
|
const industry = url.searchParams.get('industry') || '';
|
|
182
182
|
const search = url.searchParams.get('q') || '';
|
|
183
183
|
data = this.getTemplates(industry, search);
|
|
184
|
+
// Merge with real workstation templates
|
|
185
|
+
try {
|
|
186
|
+
const ws = require('agent-workstation');
|
|
187
|
+
const categories = ws.getCategories();
|
|
188
|
+
const wsTemplates: any[] = [];
|
|
189
|
+
for (const cat of categories) {
|
|
190
|
+
for (const roleName of cat.roles) {
|
|
191
|
+
const role = ws.getRole(cat.name, roleName);
|
|
192
|
+
if (!role) continue;
|
|
193
|
+
let oad: any = {};
|
|
194
|
+
try {
|
|
195
|
+
if (role.files?.['oad.yaml']) {
|
|
196
|
+
const yaml = require('js-yaml');
|
|
197
|
+
oad = yaml.load(role.files['oad.yaml']) || {};
|
|
198
|
+
}
|
|
199
|
+
} catch {}
|
|
200
|
+
const tpl = {
|
|
201
|
+
id: `ws-${cat.name}-${roleName}`,
|
|
202
|
+
name: oad.name || roleName.replace(/-/g, ' ').replace(/\b\w/g, (c: string) => c.toUpperCase()),
|
|
203
|
+
nameZh: oad.nameZh || '',
|
|
204
|
+
icon: oad.icon || '🤖',
|
|
205
|
+
description: oad.description || '',
|
|
206
|
+
descriptionZh: oad.descriptionZh || '',
|
|
207
|
+
industry: cat.name,
|
|
208
|
+
industryZh: cat.name,
|
|
209
|
+
tags: [cat.name, 'workstation'],
|
|
210
|
+
suggestedModel: 'auto',
|
|
211
|
+
systemPrompt: oad.systemPrompt || role.files?.['brain-seed.md'] || '',
|
|
212
|
+
source: 'workstation',
|
|
213
|
+
ego: oad.ego || null,
|
|
214
|
+
mission: oad.mission || null,
|
|
215
|
+
skills: oad.skills || [],
|
|
216
|
+
};
|
|
217
|
+
if (!search || tpl.name.toLowerCase().includes(search.toLowerCase()) || tpl.nameZh.includes(search)) {
|
|
218
|
+
if (!industry || tpl.industry === industry) {
|
|
219
|
+
wsTemplates.push(tpl);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
data.templates = [...data.templates, ...wsTemplates];
|
|
225
|
+
// Add workstation industries to list
|
|
226
|
+
const existingIds = new Set(data.industries.map((i: any) => i.id));
|
|
227
|
+
for (const cat of categories) {
|
|
228
|
+
if (!existingIds.has(cat.name)) {
|
|
229
|
+
data.industries.push({ id: cat.name, name: cat.name, nameZh: cat.name });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
} catch (wsErr: any) {
|
|
233
|
+
// workstation not available, use built-in templates only
|
|
234
|
+
}
|
|
184
235
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
185
236
|
res.end(JSON.stringify(data));
|
|
186
237
|
return;
|
|
187
238
|
}
|
|
188
239
|
if (route.match(/^templates\/[^/]+$/) && req.method === 'GET') {
|
|
189
240
|
const tplId = route.split('/')[1];
|
|
241
|
+
// Check workstation first
|
|
242
|
+
if (tplId.startsWith('ws-')) {
|
|
243
|
+
const parts = tplId.replace('ws-', '').split('-');
|
|
244
|
+
const catName = parts[0];
|
|
245
|
+
const roleName = parts.slice(1).join('-');
|
|
246
|
+
try {
|
|
247
|
+
const ws = require('agent-workstation');
|
|
248
|
+
const role = ws.getRole(catName, roleName);
|
|
249
|
+
if (role) {
|
|
250
|
+
let oad: any = {};
|
|
251
|
+
try {
|
|
252
|
+
if (role.files?.['oad.yaml']) {
|
|
253
|
+
const yaml = require('js-yaml');
|
|
254
|
+
oad = yaml.load(role.files['oad.yaml']) || {};
|
|
255
|
+
}
|
|
256
|
+
} catch {}
|
|
257
|
+
data = {
|
|
258
|
+
id: tplId, name: oad.name || roleName, source: 'workstation',
|
|
259
|
+
category: catName, role: roleName, files: role.files,
|
|
260
|
+
ego: oad.ego, mission: oad.mission, skills: oad.skills,
|
|
261
|
+
systemPrompt: oad.systemPrompt || role.files?.['brain-seed.md'] || '',
|
|
262
|
+
};
|
|
263
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
264
|
+
res.end(JSON.stringify(data));
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
} catch {}
|
|
268
|
+
}
|
|
190
269
|
data = this.getTemplateById(tplId);
|
|
191
270
|
res.writeHead(data.error ? 404 : 200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
192
271
|
res.end(JSON.stringify(data));
|
|
@@ -494,6 +573,65 @@ class StudioServer {
|
|
|
494
573
|
return;
|
|
495
574
|
}
|
|
496
575
|
|
|
576
|
+
// === Global config API (reads/writes ~/.opc/config.json) ===
|
|
577
|
+
if (route === 'config' && req.method === 'GET') {
|
|
578
|
+
data = loadSettingsConfig();
|
|
579
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
580
|
+
res.end(JSON.stringify(data));
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
if (route === 'config' && req.method === 'PUT') {
|
|
584
|
+
const body = JSON.parse(await this.readBody(req));
|
|
585
|
+
const cfg = loadSettingsConfig();
|
|
586
|
+
Object.assign(cfg, body);
|
|
587
|
+
saveSettingsConfig(cfg);
|
|
588
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
589
|
+
res.end(JSON.stringify({ success: true, config: cfg }));
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// === Models API (real agentkits integration) ===
|
|
594
|
+
if (route === 'models' && req.method === 'GET') {
|
|
595
|
+
try {
|
|
596
|
+
const ak = await import('agentkits');
|
|
597
|
+
const providers = ak.listLLMProviders();
|
|
598
|
+
data = { providers };
|
|
599
|
+
} catch (e: any) {
|
|
600
|
+
data = { providers: [], error: 'agentkits not available: ' + e.message };
|
|
601
|
+
}
|
|
602
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
603
|
+
res.end(JSON.stringify(data));
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// === Memory stats API (real deepbrain integration) ===
|
|
608
|
+
if (route === 'memory/stats' && req.method === 'GET') {
|
|
609
|
+
try {
|
|
610
|
+
const { Brain } = require('deepbrain');
|
|
611
|
+
const oad = this.loadOAD();
|
|
612
|
+
const dbPath = oad?.spec?.memory?.longTerm?.database || './data/brain.db';
|
|
613
|
+
const brain = new Brain({ database: dbPath, embedding_provider: 'ollama' });
|
|
614
|
+
await brain.connect();
|
|
615
|
+
const stats = await brain.stats();
|
|
616
|
+
await brain.disconnect();
|
|
617
|
+
data = { connected: true, ...stats };
|
|
618
|
+
} catch {
|
|
619
|
+
data = { connected: false, pages: 0, chunks: 0, error: 'DeepBrain not installed or not configured. Install with: npm i deepbrain' };
|
|
620
|
+
}
|
|
621
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
622
|
+
res.end(JSON.stringify(data));
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
if (route.match(/^memory\/[^/]+$/) && req.method === 'GET') {
|
|
626
|
+
const agentId = route.split('/')[1];
|
|
627
|
+
if (agentId !== 'stats' && agentId !== 'list' && agentId !== 'search') {
|
|
628
|
+
data = this.getAgentMemory(agentId);
|
|
629
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
630
|
+
res.end(JSON.stringify(data));
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
497
635
|
switch (route) {
|
|
498
636
|
case 'modules':
|
|
499
637
|
data = await this.getModulesStatus();
|
|
@@ -708,8 +846,22 @@ class StudioServer {
|
|
|
708
846
|
}
|
|
709
847
|
|
|
710
848
|
private async handleAgentChat(req: IncomingMessage, res: ServerResponse, agentId: string): Promise<void> {
|
|
711
|
-
|
|
712
|
-
|
|
849
|
+
let body: any;
|
|
850
|
+
try {
|
|
851
|
+
body = JSON.parse(await this.readBody(req));
|
|
852
|
+
} catch {
|
|
853
|
+
res.writeHead(400, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
854
|
+
res.end(JSON.stringify({ error: 'Invalid JSON body' }));
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Accept both { messages: [...] } and { message: "...", history: [...] }
|
|
859
|
+
let messages: any[] = body.messages || [];
|
|
860
|
+
if (body.message) {
|
|
861
|
+
// Frontend sends { message, history }
|
|
862
|
+
messages = [...(body.history || []), { role: 'user', content: body.message }];
|
|
863
|
+
}
|
|
864
|
+
|
|
713
865
|
const agent = this.getAgentById(agentId);
|
|
714
866
|
if (agent.error) {
|
|
715
867
|
res.writeHead(404, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
@@ -721,8 +873,8 @@ class StudioServer {
|
|
|
721
873
|
agent.messageCount = (agent.messageCount || 0) + 1;
|
|
722
874
|
agent.lastActive = new Date().toISOString();
|
|
723
875
|
agent.updated = new Date().toISOString();
|
|
724
|
-
const
|
|
725
|
-
writeFileSync(
|
|
876
|
+
const agentFilePath = join(this.getAgentsDir(), `${agentId}.json`);
|
|
877
|
+
writeFileSync(agentFilePath, JSON.stringify(agent, null, 2));
|
|
726
878
|
|
|
727
879
|
// SSE streaming response
|
|
728
880
|
res.writeHead(200, {
|
|
@@ -732,31 +884,30 @@ class StudioServer {
|
|
|
732
884
|
'Access-Control-Allow-Origin': '*',
|
|
733
885
|
});
|
|
734
886
|
|
|
735
|
-
const allMsgs = [{ role: 'system', content: agent.systemPrompt }, ...messages];
|
|
736
|
-
const lastMsg = allMsgs[allMsgs.length - 1]?.content || '';
|
|
737
|
-
|
|
738
887
|
// Use createProvider directly to call LLM
|
|
739
888
|
try {
|
|
740
889
|
const { createProvider } = require('../providers');
|
|
741
|
-
//
|
|
890
|
+
// Determine provider: agent config > OAD yaml > env > auto
|
|
742
891
|
let providerName = agent.provider || process.env.OPC_LLM_PROVIDER;
|
|
743
892
|
if (!providerName) {
|
|
744
|
-
// Try reading from oad.yaml
|
|
745
893
|
try {
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
894
|
+
for (const fname of ['oad.yaml', 'agent.yaml']) {
|
|
895
|
+
const oadPath = join(this.config.agentDir, fname);
|
|
896
|
+
if (existsSync(oadPath)) {
|
|
897
|
+
const yaml = require('js-yaml');
|
|
898
|
+
const oad = yaml.load(readFileSync(oadPath, 'utf-8'));
|
|
899
|
+
providerName = oad?.spec?.provider?.default;
|
|
900
|
+
if (providerName) break;
|
|
901
|
+
}
|
|
751
902
|
}
|
|
752
903
|
} catch {}
|
|
753
904
|
}
|
|
754
|
-
providerName = providerName || '
|
|
905
|
+
providerName = providerName || 'auto';
|
|
755
906
|
const provider = createProvider(providerName, agent.model);
|
|
756
|
-
|
|
907
|
+
|
|
757
908
|
let fullText = '';
|
|
758
909
|
try {
|
|
759
|
-
for await (const chunk of provider.chatStream(
|
|
910
|
+
for await (const chunk of provider.chatStream(messages, agent.systemPrompt)) {
|
|
760
911
|
const sseData = JSON.stringify({
|
|
761
912
|
choices: [{ delta: { content: chunk }, index: 0 }],
|
|
762
913
|
});
|
|
@@ -765,16 +916,22 @@ class StudioServer {
|
|
|
765
916
|
}
|
|
766
917
|
} catch (streamErr: any) {
|
|
767
918
|
if (!fullText) {
|
|
768
|
-
|
|
769
|
-
|
|
919
|
+
const errData = JSON.stringify({
|
|
920
|
+
choices: [{ delta: { content: `⚠️ LLM Error: ${streamErr.message}` }, index: 0 }],
|
|
921
|
+
});
|
|
770
922
|
res.write(`data: ${errData}\n\n`);
|
|
771
923
|
}
|
|
772
924
|
}
|
|
773
925
|
res.write('data: [DONE]\n\n');
|
|
774
926
|
res.end();
|
|
775
927
|
} catch (err: any) {
|
|
776
|
-
//
|
|
777
|
-
|
|
928
|
+
// Provider creation failed — send error as SSE so frontend can display it
|
|
929
|
+
const errData = JSON.stringify({
|
|
930
|
+
choices: [{ delta: { content: `⚠️ Provider error: ${err.message}\n\nTip: Install Claude CLI (npm i -g @anthropic-ai/claude-code) or set OPENAI_API_KEY.` }, index: 0 }],
|
|
931
|
+
});
|
|
932
|
+
res.write(`data: ${errData}\n\n`);
|
|
933
|
+
res.write('data: [DONE]\n\n');
|
|
934
|
+
res.end();
|
|
778
935
|
}
|
|
779
936
|
}
|
|
780
937
|
|
package/src/studio-ui/index.html
CHANGED
|
@@ -1317,26 +1317,61 @@
|
|
|
1317
1317
|
|
|
1318
1318
|
agentChatHistory.push({ role: 'user', content: msg });
|
|
1319
1319
|
|
|
1320
|
-
//
|
|
1320
|
+
// Add assistant message placeholder
|
|
1321
|
+
const assistantDiv = document.createElement('div');
|
|
1322
|
+
assistantDiv.className = 'agent-chat-msg assistant';
|
|
1323
|
+
assistantDiv.textContent = '';
|
|
1324
|
+
messagesEl.appendChild(assistantDiv);
|
|
1325
|
+
|
|
1326
|
+
// Send to API and parse SSE stream
|
|
1321
1327
|
try {
|
|
1322
1328
|
const res = await fetch(`/api/agents/${selectedAgentId}/chat`, {
|
|
1323
1329
|
method: 'POST',
|
|
1324
1330
|
headers: { 'Content-Type': 'application/json' },
|
|
1325
|
-
body: JSON.stringify({ message: msg, history: agentChatHistory })
|
|
1331
|
+
body: JSON.stringify({ message: msg, history: agentChatHistory.slice(0, -1) })
|
|
1326
1332
|
});
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1333
|
+
|
|
1334
|
+
if (!res.ok) {
|
|
1335
|
+
let errMsg = `HTTP ${res.status}`;
|
|
1336
|
+
try { const ej = await res.json(); errMsg = ej.error || errMsg; } catch {}
|
|
1337
|
+
throw new Error(errMsg);
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
const reader = res.body.getReader();
|
|
1341
|
+
const decoder = new TextDecoder();
|
|
1342
|
+
let fullReply = '';
|
|
1343
|
+
let buffer = '';
|
|
1344
|
+
|
|
1345
|
+
while (true) {
|
|
1346
|
+
const { done, value } = await reader.read();
|
|
1347
|
+
if (done) break;
|
|
1348
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1349
|
+
const lines = buffer.split('\n');
|
|
1350
|
+
buffer = lines.pop() || '';
|
|
1351
|
+
for (const line of lines) {
|
|
1352
|
+
const trimmed = line.trim();
|
|
1353
|
+
if (!trimmed || !trimmed.startsWith('data: ')) continue;
|
|
1354
|
+
const data = trimmed.slice(6);
|
|
1355
|
+
if (data === '[DONE]') continue;
|
|
1356
|
+
try {
|
|
1357
|
+
const parsed = JSON.parse(data);
|
|
1358
|
+
const content = parsed.choices?.[0]?.delta?.content;
|
|
1359
|
+
if (content) {
|
|
1360
|
+
fullReply += content;
|
|
1361
|
+
assistantDiv.textContent = fullReply;
|
|
1362
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
1363
|
+
}
|
|
1364
|
+
} catch {}
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
if (!fullReply) {
|
|
1369
|
+
assistantDiv.textContent = '(No response received)';
|
|
1370
|
+
}
|
|
1371
|
+
agentChatHistory.push({ role: 'assistant', content: fullReply });
|
|
1334
1372
|
} catch(e) {
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
errDiv.style.borderColor = 'var(--red)';
|
|
1338
|
-
errDiv.textContent = `⚠️ 发送失败: ${e.message}`;
|
|
1339
|
-
messagesEl.appendChild(errDiv);
|
|
1373
|
+
assistantDiv.style.borderColor = 'var(--red)';
|
|
1374
|
+
assistantDiv.textContent = `⚠️ 发送失败: ${e.message}`;
|
|
1340
1375
|
}
|
|
1341
1376
|
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
1342
1377
|
}
|