opc-agent 4.0.44 → 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.js +39 -13
- 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 +37 -13
- package/src/studio/server.ts +178 -21
- package/src/studio-ui/index.html +49 -14
package/dist/cli.js
CHANGED
|
@@ -680,7 +680,7 @@ program
|
|
|
680
680
|
let model;
|
|
681
681
|
let agentName = 'Agent';
|
|
682
682
|
let agentVersion = '1.0.0';
|
|
683
|
-
let providerName = '
|
|
683
|
+
let providerName = 'auto';
|
|
684
684
|
let skillNames = [];
|
|
685
685
|
// Try loading SOUL.md and CONTEXT.md for enriched system prompt
|
|
686
686
|
const soulPath = path.resolve('SOUL.md');
|
|
@@ -708,7 +708,7 @@ program
|
|
|
708
708
|
}
|
|
709
709
|
// Prepend SOUL.md and CONTEXT.md to system prompt
|
|
710
710
|
systemPrompt = [soulContent, contextContent, systemPrompt].filter(Boolean).join('\n\n');
|
|
711
|
-
const provider = (0, providers_1.createProvider)(
|
|
711
|
+
const provider = (0, providers_1.createProvider)(providerName, model);
|
|
712
712
|
const history = [];
|
|
713
713
|
// Print startup banner
|
|
714
714
|
const bannerLines = [
|
package/dist/core/runtime.js
CHANGED
|
@@ -73,6 +73,24 @@ class AgentRuntime {
|
|
|
73
73
|
agentBrain = null;
|
|
74
74
|
evolveScheduler = null;
|
|
75
75
|
async loadConfig(filePath) {
|
|
76
|
+
const fs = require('fs');
|
|
77
|
+
if (!fs.existsSync(filePath)) {
|
|
78
|
+
// Auto-create a minimal oad.yaml with auto-detect provider
|
|
79
|
+
const yaml = require('js-yaml');
|
|
80
|
+
const defaultOAD = {
|
|
81
|
+
apiVersion: 'opc/v1',
|
|
82
|
+
kind: 'Agent',
|
|
83
|
+
metadata: { name: 'my-agent', version: '1.0.0', description: 'OPC Agent' },
|
|
84
|
+
spec: {
|
|
85
|
+
model: 'auto',
|
|
86
|
+
provider: { default: 'auto' },
|
|
87
|
+
systemPrompt: 'You are a helpful AI assistant.',
|
|
88
|
+
channels: [{ type: 'web', config: { port: 3000 } }],
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
fs.writeFileSync(filePath, yaml.dump(defaultOAD, { lineWidth: 120 }));
|
|
92
|
+
this.logger.info('Created default oad.yaml (no config file found)');
|
|
93
|
+
}
|
|
76
94
|
this.config = (0, config_1.loadOAD)(filePath);
|
|
77
95
|
this.logger.info('Config loaded', { name: this.config.metadata.name });
|
|
78
96
|
return this.config;
|
package/dist/providers/index.js
CHANGED
|
@@ -328,7 +328,21 @@ class ClaudeCLIProvider {
|
|
|
328
328
|
name = 'claude-cli';
|
|
329
329
|
model;
|
|
330
330
|
constructor(model) {
|
|
331
|
-
|
|
331
|
+
// Claude CLI uses short model names; don't pass API-style model names
|
|
332
|
+
// Let Claude CLI use its default model unless explicitly set to a known CLI model
|
|
333
|
+
const cliModels = ['sonnet', 'opus', 'haiku', 'claude-sonnet-4-20250514', 'claude-opus-4-20250514'];
|
|
334
|
+
if (model && !cliModels.includes(model)) {
|
|
335
|
+
// Map common patterns
|
|
336
|
+
if (model.includes('opus'))
|
|
337
|
+
this.model = 'opus';
|
|
338
|
+
else if (model.includes('haiku'))
|
|
339
|
+
this.model = 'haiku';
|
|
340
|
+
else
|
|
341
|
+
this.model = ''; // let CLI choose default
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
this.model = model || '';
|
|
345
|
+
}
|
|
332
346
|
}
|
|
333
347
|
async chat(messages, systemPrompt, options) {
|
|
334
348
|
const { writeFileSync, unlinkSync, mkdtempSync } = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
@@ -343,7 +357,7 @@ class ClaudeCLIProvider {
|
|
|
343
357
|
if (options?.tools && options.tools.length > 0) {
|
|
344
358
|
prompt += buildToolPrompt(options.tools);
|
|
345
359
|
}
|
|
346
|
-
const args = ['-p'
|
|
360
|
+
const args = ['-p'];
|
|
347
361
|
// Write system prompt to temp file to avoid shell escaping issues
|
|
348
362
|
let tmpFile;
|
|
349
363
|
if (systemPrompt) {
|
|
@@ -407,7 +421,7 @@ class ClaudeCLIProvider {
|
|
|
407
421
|
}
|
|
408
422
|
}
|
|
409
423
|
async *chatStream(messages, systemPrompt) {
|
|
410
|
-
const args = ['-p', '--
|
|
424
|
+
const args = ['-p', '--verbose', '--output-format', 'stream-json'];
|
|
411
425
|
if (this.model) {
|
|
412
426
|
args.push('--model', this.model);
|
|
413
427
|
}
|
|
@@ -442,14 +456,25 @@ class ClaudeCLIProvider {
|
|
|
442
456
|
continue;
|
|
443
457
|
try {
|
|
444
458
|
const event = JSON.parse(trimmed);
|
|
445
|
-
//
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
459
|
+
// Claude CLI stream-json format:
|
|
460
|
+
// {"type":"assistant","message":{"content":[{"type":"text","text":"..."}]}}
|
|
461
|
+
if (event.type === 'assistant' && event.message?.content) {
|
|
462
|
+
for (const block of event.message.content) {
|
|
463
|
+
if (block.type === 'text' && block.text) {
|
|
464
|
+
const newContent = block.text;
|
|
465
|
+
if (newContent.length > lastContent.length) {
|
|
466
|
+
yield newContent.slice(lastContent.length);
|
|
467
|
+
lastContent = newContent;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
451
470
|
}
|
|
452
471
|
}
|
|
472
|
+
// Also handle result type for final content
|
|
473
|
+
if (event.type === 'result' && event.result) {
|
|
474
|
+
const remaining = event.result.slice(lastContent.length);
|
|
475
|
+
if (remaining)
|
|
476
|
+
yield remaining;
|
|
477
|
+
}
|
|
453
478
|
// Handle assistant message with content array
|
|
454
479
|
if (event.type === 'assistant' && event.message?.content) {
|
|
455
480
|
for (const block of event.message.content) {
|
|
@@ -520,10 +545,11 @@ function detectClaudeCLI() {
|
|
|
520
545
|
}
|
|
521
546
|
function detectOllama() {
|
|
522
547
|
try {
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
548
|
+
// Use node http instead of curl for Windows compatibility
|
|
549
|
+
const { execSync: es } = require('child_process');
|
|
550
|
+
// Quick check: try to connect to Ollama API via node
|
|
551
|
+
const result = es(`node -e "const h=require('http');const r=h.get('http://localhost:11434/api/tags',{timeout:2000},s=>{let d='';s.on('data',c=>d+=c);s.on('end',()=>{process.stdout.write(d.includes('models')?'1':'0')})});r.on('error',()=>process.stdout.write('0'));r.on('timeout',()=>{r.destroy();process.stdout.write('0')})"`, { stdio: 'pipe', timeout: 5000 });
|
|
552
|
+
return result.toString().trim() === '1';
|
|
527
553
|
}
|
|
528
554
|
catch {
|
|
529
555
|
return false;
|
package/dist/studio/server.js
CHANGED
|
@@ -166,12 +166,96 @@ class StudioServer {
|
|
|
166
166
|
const industry = url.searchParams.get('industry') || '';
|
|
167
167
|
const search = url.searchParams.get('q') || '';
|
|
168
168
|
data = this.getTemplates(industry, search);
|
|
169
|
+
// Merge with real workstation templates
|
|
170
|
+
try {
|
|
171
|
+
const ws = require('agent-workstation');
|
|
172
|
+
const categories = ws.getCategories();
|
|
173
|
+
const wsTemplates = [];
|
|
174
|
+
for (const cat of categories) {
|
|
175
|
+
for (const roleName of cat.roles) {
|
|
176
|
+
const role = ws.getRole(cat.name, roleName);
|
|
177
|
+
if (!role)
|
|
178
|
+
continue;
|
|
179
|
+
let oad = {};
|
|
180
|
+
try {
|
|
181
|
+
if (role.files?.['oad.yaml']) {
|
|
182
|
+
const yaml = require('js-yaml');
|
|
183
|
+
oad = yaml.load(role.files['oad.yaml']) || {};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch { }
|
|
187
|
+
const tpl = {
|
|
188
|
+
id: `ws-${cat.name}-${roleName}`,
|
|
189
|
+
name: oad.name || roleName.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()),
|
|
190
|
+
nameZh: oad.nameZh || '',
|
|
191
|
+
icon: oad.icon || '🤖',
|
|
192
|
+
description: oad.description || '',
|
|
193
|
+
descriptionZh: oad.descriptionZh || '',
|
|
194
|
+
industry: cat.name,
|
|
195
|
+
industryZh: cat.name,
|
|
196
|
+
tags: [cat.name, 'workstation'],
|
|
197
|
+
suggestedModel: 'auto',
|
|
198
|
+
systemPrompt: oad.systemPrompt || role.files?.['brain-seed.md'] || '',
|
|
199
|
+
source: 'workstation',
|
|
200
|
+
ego: oad.ego || null,
|
|
201
|
+
mission: oad.mission || null,
|
|
202
|
+
skills: oad.skills || [],
|
|
203
|
+
};
|
|
204
|
+
if (!search || tpl.name.toLowerCase().includes(search.toLowerCase()) || tpl.nameZh.includes(search)) {
|
|
205
|
+
if (!industry || tpl.industry === industry) {
|
|
206
|
+
wsTemplates.push(tpl);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
data.templates = [...data.templates, ...wsTemplates];
|
|
212
|
+
// Add workstation industries to list
|
|
213
|
+
const existingIds = new Set(data.industries.map((i) => i.id));
|
|
214
|
+
for (const cat of categories) {
|
|
215
|
+
if (!existingIds.has(cat.name)) {
|
|
216
|
+
data.industries.push({ id: cat.name, name: cat.name, nameZh: cat.name });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch (wsErr) {
|
|
221
|
+
// workstation not available, use built-in templates only
|
|
222
|
+
}
|
|
169
223
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
170
224
|
res.end(JSON.stringify(data));
|
|
171
225
|
return;
|
|
172
226
|
}
|
|
173
227
|
if (route.match(/^templates\/[^/]+$/) && req.method === 'GET') {
|
|
174
228
|
const tplId = route.split('/')[1];
|
|
229
|
+
// Check workstation first
|
|
230
|
+
if (tplId.startsWith('ws-')) {
|
|
231
|
+
const parts = tplId.replace('ws-', '').split('-');
|
|
232
|
+
const catName = parts[0];
|
|
233
|
+
const roleName = parts.slice(1).join('-');
|
|
234
|
+
try {
|
|
235
|
+
const ws = require('agent-workstation');
|
|
236
|
+
const role = ws.getRole(catName, roleName);
|
|
237
|
+
if (role) {
|
|
238
|
+
let oad = {};
|
|
239
|
+
try {
|
|
240
|
+
if (role.files?.['oad.yaml']) {
|
|
241
|
+
const yaml = require('js-yaml');
|
|
242
|
+
oad = yaml.load(role.files['oad.yaml']) || {};
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
catch { }
|
|
246
|
+
data = {
|
|
247
|
+
id: tplId, name: oad.name || roleName, source: 'workstation',
|
|
248
|
+
category: catName, role: roleName, files: role.files,
|
|
249
|
+
ego: oad.ego, mission: oad.mission, skills: oad.skills,
|
|
250
|
+
systemPrompt: oad.systemPrompt || role.files?.['brain-seed.md'] || '',
|
|
251
|
+
};
|
|
252
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
253
|
+
res.end(JSON.stringify(data));
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
catch { }
|
|
258
|
+
}
|
|
175
259
|
data = this.getTemplateById(tplId);
|
|
176
260
|
res.writeHead(data.error ? 404 : 200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
177
261
|
res.end(JSON.stringify(data));
|
|
@@ -473,6 +557,64 @@ class StudioServer {
|
|
|
473
557
|
res.end(JSON.stringify(data));
|
|
474
558
|
return;
|
|
475
559
|
}
|
|
560
|
+
// === Global config API (reads/writes ~/.opc/config.json) ===
|
|
561
|
+
if (route === 'config' && req.method === 'GET') {
|
|
562
|
+
data = loadSettingsConfig();
|
|
563
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
564
|
+
res.end(JSON.stringify(data));
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
if (route === 'config' && req.method === 'PUT') {
|
|
568
|
+
const body = JSON.parse(await this.readBody(req));
|
|
569
|
+
const cfg = loadSettingsConfig();
|
|
570
|
+
Object.assign(cfg, body);
|
|
571
|
+
saveSettingsConfig(cfg);
|
|
572
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
573
|
+
res.end(JSON.stringify({ success: true, config: cfg }));
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
// === Models API (real agentkits integration) ===
|
|
577
|
+
if (route === 'models' && req.method === 'GET') {
|
|
578
|
+
try {
|
|
579
|
+
const ak = await Promise.resolve().then(() => __importStar(require('agentkits')));
|
|
580
|
+
const providers = ak.listLLMProviders();
|
|
581
|
+
data = { providers };
|
|
582
|
+
}
|
|
583
|
+
catch (e) {
|
|
584
|
+
data = { providers: [], error: 'agentkits not available: ' + e.message };
|
|
585
|
+
}
|
|
586
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
587
|
+
res.end(JSON.stringify(data));
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
// === Memory stats API (real deepbrain integration) ===
|
|
591
|
+
if (route === 'memory/stats' && req.method === 'GET') {
|
|
592
|
+
try {
|
|
593
|
+
const { Brain } = require('deepbrain');
|
|
594
|
+
const oad = this.loadOAD();
|
|
595
|
+
const dbPath = oad?.spec?.memory?.longTerm?.database || './data/brain.db';
|
|
596
|
+
const brain = new Brain({ database: dbPath, embedding_provider: 'ollama' });
|
|
597
|
+
await brain.connect();
|
|
598
|
+
const stats = await brain.stats();
|
|
599
|
+
await brain.disconnect();
|
|
600
|
+
data = { connected: true, ...stats };
|
|
601
|
+
}
|
|
602
|
+
catch {
|
|
603
|
+
data = { connected: false, pages: 0, chunks: 0, error: 'DeepBrain not installed or not configured. Install with: npm i deepbrain' };
|
|
604
|
+
}
|
|
605
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
606
|
+
res.end(JSON.stringify(data));
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
if (route.match(/^memory\/[^/]+$/) && req.method === 'GET') {
|
|
610
|
+
const agentId = route.split('/')[1];
|
|
611
|
+
if (agentId !== 'stats' && agentId !== 'list' && agentId !== 'search') {
|
|
612
|
+
data = this.getAgentMemory(agentId);
|
|
613
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
614
|
+
res.end(JSON.stringify(data));
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
476
618
|
switch (route) {
|
|
477
619
|
case 'modules':
|
|
478
620
|
data = await this.getModulesStatus();
|
|
@@ -710,8 +852,21 @@ class StudioServer {
|
|
|
710
852
|
return { entries, timeline: entries.map((e) => ({ date: e.timestamp, summary: e.summary || e.content?.slice(0, 100) })) };
|
|
711
853
|
}
|
|
712
854
|
async handleAgentChat(req, res, agentId) {
|
|
713
|
-
|
|
714
|
-
|
|
855
|
+
let body;
|
|
856
|
+
try {
|
|
857
|
+
body = JSON.parse(await this.readBody(req));
|
|
858
|
+
}
|
|
859
|
+
catch {
|
|
860
|
+
res.writeHead(400, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
861
|
+
res.end(JSON.stringify({ error: 'Invalid JSON body' }));
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
// Accept both { messages: [...] } and { message: "...", history: [...] }
|
|
865
|
+
let messages = body.messages || [];
|
|
866
|
+
if (body.message) {
|
|
867
|
+
// Frontend sends { message, history }
|
|
868
|
+
messages = [...(body.history || []), { role: 'user', content: body.message }];
|
|
869
|
+
}
|
|
715
870
|
const agent = this.getAgentById(agentId);
|
|
716
871
|
if (agent.error) {
|
|
717
872
|
res.writeHead(404, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
@@ -722,8 +877,8 @@ class StudioServer {
|
|
|
722
877
|
agent.messageCount = (agent.messageCount || 0) + 1;
|
|
723
878
|
agent.lastActive = new Date().toISOString();
|
|
724
879
|
agent.updated = new Date().toISOString();
|
|
725
|
-
const
|
|
726
|
-
(0, fs_1.writeFileSync)(
|
|
880
|
+
const agentFilePath = (0, path_1.join)(this.getAgentsDir(), `${agentId}.json`);
|
|
881
|
+
(0, fs_1.writeFileSync)(agentFilePath, JSON.stringify(agent, null, 2));
|
|
727
882
|
// SSE streaming response
|
|
728
883
|
res.writeHead(200, {
|
|
729
884
|
'Content-Type': 'text/event-stream',
|
|
@@ -731,30 +886,31 @@ class StudioServer {
|
|
|
731
886
|
'Connection': 'keep-alive',
|
|
732
887
|
'Access-Control-Allow-Origin': '*',
|
|
733
888
|
});
|
|
734
|
-
const allMsgs = [{ role: 'system', content: agent.systemPrompt }, ...messages];
|
|
735
|
-
const lastMsg = allMsgs[allMsgs.length - 1]?.content || '';
|
|
736
889
|
// Use createProvider directly to call LLM
|
|
737
890
|
try {
|
|
738
891
|
const { createProvider } = require('../providers');
|
|
739
|
-
//
|
|
892
|
+
// Determine provider: agent config > OAD yaml > env > auto
|
|
740
893
|
let providerName = agent.provider || process.env.OPC_LLM_PROVIDER;
|
|
741
894
|
if (!providerName) {
|
|
742
|
-
// Try reading from oad.yaml
|
|
743
895
|
try {
|
|
744
|
-
const
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
896
|
+
for (const fname of ['oad.yaml', 'agent.yaml']) {
|
|
897
|
+
const oadPath = (0, path_1.join)(this.config.agentDir, fname);
|
|
898
|
+
if ((0, fs_1.existsSync)(oadPath)) {
|
|
899
|
+
const yaml = require('js-yaml');
|
|
900
|
+
const oad = yaml.load((0, fs_1.readFileSync)(oadPath, 'utf-8'));
|
|
901
|
+
providerName = oad?.spec?.provider?.default;
|
|
902
|
+
if (providerName)
|
|
903
|
+
break;
|
|
904
|
+
}
|
|
749
905
|
}
|
|
750
906
|
}
|
|
751
907
|
catch { }
|
|
752
908
|
}
|
|
753
|
-
providerName = providerName || '
|
|
909
|
+
providerName = providerName || 'auto';
|
|
754
910
|
const provider = createProvider(providerName, agent.model);
|
|
755
911
|
let fullText = '';
|
|
756
912
|
try {
|
|
757
|
-
for await (const chunk of provider.chatStream(
|
|
913
|
+
for await (const chunk of provider.chatStream(messages, agent.systemPrompt)) {
|
|
758
914
|
const sseData = JSON.stringify({
|
|
759
915
|
choices: [{ delta: { content: chunk }, index: 0 }],
|
|
760
916
|
});
|
|
@@ -764,8 +920,9 @@ class StudioServer {
|
|
|
764
920
|
}
|
|
765
921
|
catch (streamErr) {
|
|
766
922
|
if (!fullText) {
|
|
767
|
-
|
|
768
|
-
|
|
923
|
+
const errData = JSON.stringify({
|
|
924
|
+
choices: [{ delta: { content: `⚠️ LLM Error: ${streamErr.message}` }, index: 0 }],
|
|
925
|
+
});
|
|
769
926
|
res.write(`data: ${errData}\n\n`);
|
|
770
927
|
}
|
|
771
928
|
}
|
|
@@ -773,8 +930,13 @@ class StudioServer {
|
|
|
773
930
|
res.end();
|
|
774
931
|
}
|
|
775
932
|
catch (err) {
|
|
776
|
-
//
|
|
777
|
-
|
|
933
|
+
// Provider creation failed — send error as SSE so frontend can display it
|
|
934
|
+
const errData = JSON.stringify({
|
|
935
|
+
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 }],
|
|
936
|
+
});
|
|
937
|
+
res.write(`data: ${errData}\n\n`);
|
|
938
|
+
res.write('data: [DONE]\n\n');
|
|
939
|
+
res.end();
|
|
778
940
|
}
|
|
779
941
|
}
|
|
780
942
|
sendSimulatedResponse(res, lastMsg, agent) {
|
|
@@ -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
|
}
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -771,7 +771,7 @@ program
|
|
|
771
771
|
let model: string | undefined;
|
|
772
772
|
let agentName = 'Agent';
|
|
773
773
|
let agentVersion = '1.0.0';
|
|
774
|
-
let providerName = '
|
|
774
|
+
let providerName = 'auto';
|
|
775
775
|
let skillNames: string[] = [];
|
|
776
776
|
|
|
777
777
|
// Try loading SOUL.md and CONTEXT.md for enriched system prompt
|
|
@@ -796,7 +796,7 @@ program
|
|
|
796
796
|
// Prepend SOUL.md and CONTEXT.md to system prompt
|
|
797
797
|
systemPrompt = [soulContent, contextContent, systemPrompt].filter(Boolean).join('\n\n');
|
|
798
798
|
|
|
799
|
-
const provider = createProvider(
|
|
799
|
+
const provider = createProvider(providerName, model);
|
|
800
800
|
const history: { role: 'user' | 'assistant' | 'system'; content: string }[] = [];
|
|
801
801
|
|
|
802
802
|
// Print startup banner
|
package/src/core/runtime.ts
CHANGED
|
@@ -44,6 +44,24 @@ export class AgentRuntime {
|
|
|
44
44
|
private evolveScheduler: any = null;
|
|
45
45
|
|
|
46
46
|
async loadConfig(filePath: string): Promise<OADDocument> {
|
|
47
|
+
const fs = require('fs');
|
|
48
|
+
if (!fs.existsSync(filePath)) {
|
|
49
|
+
// Auto-create a minimal oad.yaml with auto-detect provider
|
|
50
|
+
const yaml = require('js-yaml');
|
|
51
|
+
const defaultOAD = {
|
|
52
|
+
apiVersion: 'opc/v1',
|
|
53
|
+
kind: 'Agent',
|
|
54
|
+
metadata: { name: 'my-agent', version: '1.0.0', description: 'OPC Agent' },
|
|
55
|
+
spec: {
|
|
56
|
+
model: 'auto',
|
|
57
|
+
provider: { default: 'auto' },
|
|
58
|
+
systemPrompt: 'You are a helpful AI assistant.',
|
|
59
|
+
channels: [{ type: 'web', config: { port: 3000 } }],
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
fs.writeFileSync(filePath, yaml.dump(defaultOAD, { lineWidth: 120 }));
|
|
63
|
+
this.logger.info('Created default oad.yaml (no config file found)');
|
|
64
|
+
}
|
|
47
65
|
this.config = loadOAD(filePath);
|
|
48
66
|
this.logger.info('Config loaded', { name: this.config.metadata.name });
|
|
49
67
|
return this.config;
|
package/src/providers/index.ts
CHANGED
|
@@ -330,7 +330,17 @@ class ClaudeCLIProvider implements LLMProvider {
|
|
|
330
330
|
private model: string;
|
|
331
331
|
|
|
332
332
|
constructor(model?: string) {
|
|
333
|
-
|
|
333
|
+
// Claude CLI uses short model names; don't pass API-style model names
|
|
334
|
+
// Let Claude CLI use its default model unless explicitly set to a known CLI model
|
|
335
|
+
const cliModels = ['sonnet', 'opus', 'haiku', 'claude-sonnet-4-20250514', 'claude-opus-4-20250514'];
|
|
336
|
+
if (model && !cliModels.includes(model)) {
|
|
337
|
+
// Map common patterns
|
|
338
|
+
if (model.includes('opus')) this.model = 'opus';
|
|
339
|
+
else if (model.includes('haiku')) this.model = 'haiku';
|
|
340
|
+
else this.model = ''; // let CLI choose default
|
|
341
|
+
} else {
|
|
342
|
+
this.model = model || '';
|
|
343
|
+
}
|
|
334
344
|
}
|
|
335
345
|
|
|
336
346
|
async chat(messages: Message[], systemPrompt?: string, options?: ChatOptions): Promise<string> {
|
|
@@ -349,7 +359,7 @@ class ClaudeCLIProvider implements LLMProvider {
|
|
|
349
359
|
prompt += buildToolPrompt(options.tools);
|
|
350
360
|
}
|
|
351
361
|
|
|
352
|
-
const args = ['-p'
|
|
362
|
+
const args = ['-p'];
|
|
353
363
|
// Write system prompt to temp file to avoid shell escaping issues
|
|
354
364
|
let tmpFile: string | undefined;
|
|
355
365
|
if (systemPrompt) {
|
|
@@ -410,7 +420,7 @@ class ClaudeCLIProvider implements LLMProvider {
|
|
|
410
420
|
}
|
|
411
421
|
|
|
412
422
|
async *chatStream(messages: Message[], systemPrompt?: string): AsyncIterable<string> {
|
|
413
|
-
const args = ['-p', '--
|
|
423
|
+
const args = ['-p', '--verbose', '--output-format', 'stream-json'];
|
|
414
424
|
if (this.model) {
|
|
415
425
|
args.push('--model', this.model);
|
|
416
426
|
}
|
|
@@ -451,14 +461,24 @@ class ClaudeCLIProvider implements LLMProvider {
|
|
|
451
461
|
if (!trimmed) continue;
|
|
452
462
|
try {
|
|
453
463
|
const event = JSON.parse(trimmed);
|
|
454
|
-
//
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
464
|
+
// Claude CLI stream-json format:
|
|
465
|
+
// {"type":"assistant","message":{"content":[{"type":"text","text":"..."}]}}
|
|
466
|
+
if (event.type === 'assistant' && event.message?.content) {
|
|
467
|
+
for (const block of event.message.content) {
|
|
468
|
+
if (block.type === 'text' && block.text) {
|
|
469
|
+
const newContent = block.text;
|
|
470
|
+
if (newContent.length > lastContent.length) {
|
|
471
|
+
yield newContent.slice(lastContent.length);
|
|
472
|
+
lastContent = newContent;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
460
475
|
}
|
|
461
476
|
}
|
|
477
|
+
// Also handle result type for final content
|
|
478
|
+
if (event.type === 'result' && event.result) {
|
|
479
|
+
const remaining = event.result.slice(lastContent.length);
|
|
480
|
+
if (remaining) yield remaining;
|
|
481
|
+
}
|
|
462
482
|
// Handle assistant message with content array
|
|
463
483
|
if (event.type === 'assistant' && event.message?.content) {
|
|
464
484
|
for (const block of event.message.content) {
|
|
@@ -524,10 +544,14 @@ function detectClaudeCLI(): boolean {
|
|
|
524
544
|
|
|
525
545
|
function detectOllama(): boolean {
|
|
526
546
|
try {
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
547
|
+
// Use node http instead of curl for Windows compatibility
|
|
548
|
+
const { execSync: es } = require('child_process');
|
|
549
|
+
// Quick check: try to connect to Ollama API via node
|
|
550
|
+
const result = es(
|
|
551
|
+
`node -e "const h=require('http');const r=h.get('http://localhost:11434/api/tags',{timeout:2000},s=>{let d='';s.on('data',c=>d+=c);s.on('end',()=>{process.stdout.write(d.includes('models')?'1':'0')})});r.on('error',()=>process.stdout.write('0'));r.on('timeout',()=>{r.destroy();process.stdout.write('0')})"`,
|
|
552
|
+
{ stdio: 'pipe', timeout: 5000 }
|
|
553
|
+
);
|
|
554
|
+
return result.toString().trim() === '1';
|
|
531
555
|
} catch { return false; }
|
|
532
556
|
}
|
|
533
557
|
|
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
|
}
|