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.
@@ -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
- const body = JSON.parse(await this.readBody(req));
712
- const { messages = [] } = body;
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 filePath = join(this.getAgentsDir(), `${agentId}.json`);
725
- writeFileSync(filePath, JSON.stringify(agent, null, 2));
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
- // Read OAD config for provider info
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 oadPath = join(this.config.agentDir, 'oad.yaml');
747
- if (existsSync(oadPath)) {
748
- const yaml = require('js-yaml');
749
- const oad = yaml.load(readFileSync(oadPath, 'utf-8'));
750
- providerName = oad?.spec?.provider?.default;
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 || 'openai';
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(allMsgs, agent.systemPrompt)) {
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
- // No content streamed yet, send error
769
- const errData = JSON.stringify({ error: streamErr.message });
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
- // Fallback: try simulated response
777
- this.sendSimulatedResponse(res, lastMsg, agent);
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
 
@@ -1317,26 +1317,61 @@
1317
1317
 
1318
1318
  agentChatHistory.push({ role: 'user', content: msg });
1319
1319
 
1320
- // Send to API
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
- const data = await res.json();
1328
- const reply = data.reply || data.message || data.content || JSON.stringify(data);
1329
- const assistantDiv = document.createElement('div');
1330
- assistantDiv.className = 'agent-chat-msg assistant';
1331
- assistantDiv.textContent = reply;
1332
- messagesEl.appendChild(assistantDiv);
1333
- agentChatHistory.push({ role: 'assistant', content: reply });
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
- const errDiv = document.createElement('div');
1336
- errDiv.className = 'agent-chat-msg assistant';
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
  }