libre-webui 0.7.1 → 0.8.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/backend/dist/index.d.ts.map +1 -1
- package/backend/dist/index.js +426 -57
- package/backend/dist/index.js.map +1 -1
- package/backend/dist/routes/plugins.d.ts.map +1 -1
- package/backend/dist/routes/plugins.js +17 -0
- package/backend/dist/routes/plugins.js.map +1 -1
- package/backend/dist/services/openclawSessionService.d.ts +63 -0
- package/backend/dist/services/openclawSessionService.d.ts.map +1 -0
- package/backend/dist/services/openclawSessionService.js +307 -0
- package/backend/dist/services/openclawSessionService.js.map +1 -0
- package/backend/dist/services/pluginService.d.ts +20 -0
- package/backend/dist/services/pluginService.d.ts.map +1 -1
- package/backend/dist/services/pluginService.js +250 -6
- package/backend/dist/services/pluginService.js.map +1 -1
- package/frontend/dist/assets/index-BHPEXBOC.js +51 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/js/{ArtifactContainer-CnO8nWfm.js → ArtifactContainer-CR_N0zJa.js} +2 -2
- package/frontend/dist/js/{ArtifactDemoPage-m4dZIjIM.js → ArtifactDemoPage-B4i0y1r8.js} +1 -1
- package/frontend/dist/js/{ChatPage-BkvB6a5u.js → ChatPage-CNZjHhUr.js} +12 -12
- package/frontend/dist/js/{GalleryPage-C6V5NU58.js → GalleryPage-BwPcUdbX.js} +1 -1
- package/frontend/dist/js/ModelsPage-2EzbE4Sf.js +1 -0
- package/frontend/dist/js/PersonasPage-CZrFe-EX.js +13 -0
- package/frontend/dist/js/{UserManagementPage-DBm-9utt.js → UserManagementPage-doHbL51e.js} +1 -1
- package/frontend/dist/js/{ui-vendor-DxZsuKzb.js → ui-vendor-BxYipEVo.js} +1 -1
- package/package.json +1 -1
- package/plugins/openclaw-agent.json +69 -0
- package/frontend/dist/assets/index-B4PmhQ5N.js +0 -51
- package/frontend/dist/js/ModelsPage-BGbYUYAI.js +0 -1
- package/frontend/dist/js/PersonasPage-567IlJm4.js +0 -13
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAkBA,OAAO,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAkBA,OAAO,UAAU,CAAC;AAkFlB,QAAA,MAAM,GAAG,6CAAY,CAAC;AAkgDtB,eAAe,GAAG,CAAC"}
|
package/backend/dist/index.js
CHANGED
|
@@ -76,6 +76,7 @@ import { HuggingFaceOAuthService } from './services/simpleHuggingFaceOAuth.js';
|
|
|
76
76
|
import pluginService from './services/pluginService.js';
|
|
77
77
|
import preferencesService from './services/preferencesService.js';
|
|
78
78
|
import documentService from './services/documentService.js';
|
|
79
|
+
import openclawSessionService, { extractTextFromMessage, } from './services/openclawSessionService.js';
|
|
79
80
|
import { mergeGenerationOptions } from './utils/generationUtils.js';
|
|
80
81
|
import { verifyToken } from './utils/jwt.js';
|
|
81
82
|
const app = express();
|
|
@@ -564,48 +565,224 @@ wss.on('connection', (ws, req) => {
|
|
|
564
565
|
console.log(`[WebSocket] Found plugin:`, activePlugin ? activePlugin.id : 'none');
|
|
565
566
|
if (activePlugin) {
|
|
566
567
|
console.log(`[WebSocket] Using plugin ${activePlugin.id} for model ${actualModelName}`);
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
568
|
+
// Load plugin variables early — needed for session mode check
|
|
569
|
+
const pluginVars = pluginService.getPluginVariables(activePlugin);
|
|
570
|
+
// ---------------------------------------------------------------
|
|
571
|
+
// OpenClaw Session Mode — route through WebSocket gateway
|
|
572
|
+
// ---------------------------------------------------------------
|
|
573
|
+
// OpenClaw session routing now handled via x-openclaw-session-key HTTP header
|
|
574
|
+
// in pluginService — no WebSocket needed
|
|
575
|
+
const isOpenClawSession = false; // Disabled: WS approach replaced by HTTP header
|
|
576
|
+
if (isOpenClawSession) {
|
|
577
|
+
console.log('[WebSocket] OpenClaw session mode: routing through gateway WS');
|
|
578
|
+
try {
|
|
579
|
+
// Ensure the gateway WS connection is established
|
|
580
|
+
const endpoint = pluginVars.endpoint ||
|
|
581
|
+
activePlugin.endpoint ||
|
|
582
|
+
'http://127.0.0.1:18789/v1/chat/completions';
|
|
583
|
+
const apiKey = pluginService.getApiKey(activePlugin) || '';
|
|
584
|
+
const ocSessionKey = pluginVars.session_key || 'main';
|
|
585
|
+
if (!openclawSessionService.isConnected) {
|
|
586
|
+
openclawSessionService.connect({
|
|
587
|
+
gatewayUrl: endpoint,
|
|
588
|
+
token: apiKey,
|
|
589
|
+
sessionKey: ocSessionKey,
|
|
590
|
+
});
|
|
591
|
+
// Wait a bit for connection
|
|
592
|
+
await new Promise(resolve => {
|
|
593
|
+
const maxWait = 8000;
|
|
594
|
+
const start = Date.now();
|
|
595
|
+
const check = () => {
|
|
596
|
+
if (openclawSessionService.isConnected) {
|
|
597
|
+
resolve();
|
|
598
|
+
}
|
|
599
|
+
else if (Date.now() - start > maxWait) {
|
|
600
|
+
resolve(); // proceed anyway, will error on send
|
|
601
|
+
}
|
|
602
|
+
else {
|
|
603
|
+
setTimeout(check, 200);
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
check();
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
if (!openclawSessionService.isConnected) {
|
|
610
|
+
throw new Error('Failed to connect to OpenClaw gateway WebSocket');
|
|
611
|
+
}
|
|
612
|
+
// Build the message to send
|
|
613
|
+
const messageText = userMessage?.content || content;
|
|
614
|
+
// Set up event listener for this run
|
|
615
|
+
let totalContent = '';
|
|
616
|
+
let currentRunId = null;
|
|
617
|
+
let runDone = false;
|
|
618
|
+
const toolCalls = [];
|
|
619
|
+
const cleanup = openclawSessionService.subscribe((type, data) => {
|
|
620
|
+
if (runDone)
|
|
621
|
+
return;
|
|
622
|
+
if (type === 'chat') {
|
|
623
|
+
const chatEvent = data;
|
|
624
|
+
if (currentRunId &&
|
|
625
|
+
chatEvent.runId &&
|
|
626
|
+
chatEvent.runId !== currentRunId)
|
|
627
|
+
return;
|
|
628
|
+
if (chatEvent.state === 'delta') {
|
|
629
|
+
const text = extractTextFromMessage(chatEvent.message);
|
|
630
|
+
if (typeof text === 'string' &&
|
|
631
|
+
text.length > totalContent.length) {
|
|
632
|
+
// Send incremental delta
|
|
633
|
+
const newContent = text.slice(totalContent.length);
|
|
634
|
+
totalContent = text;
|
|
635
|
+
try {
|
|
636
|
+
ws.send(JSON.stringify({
|
|
637
|
+
type: 'assistant_chunk',
|
|
638
|
+
data: {
|
|
639
|
+
content: newContent,
|
|
640
|
+
total: totalContent,
|
|
641
|
+
done: false,
|
|
642
|
+
messageId: assistantMessageId,
|
|
643
|
+
},
|
|
644
|
+
}));
|
|
645
|
+
}
|
|
646
|
+
catch {
|
|
647
|
+
/* ws closed */
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
else if (chatEvent.state === 'final' ||
|
|
652
|
+
chatEvent.state === 'aborted' ||
|
|
653
|
+
chatEvent.state === 'error') {
|
|
654
|
+
runDone = true;
|
|
655
|
+
if (chatEvent.state === 'error') {
|
|
656
|
+
const errMsg = chatEvent.errorMessage || 'Agent error';
|
|
657
|
+
if (!totalContent)
|
|
658
|
+
totalContent = `Error: ${errMsg}`;
|
|
659
|
+
}
|
|
660
|
+
// Append tool call summaries
|
|
661
|
+
if (toolCalls.length > 0) {
|
|
662
|
+
let toolContent = '\n\n---\n**🔧 Tools Used:**\n';
|
|
663
|
+
for (const tc of toolCalls) {
|
|
664
|
+
const statusIcon = tc.phase === 'result' ? '✅' : '⏳';
|
|
665
|
+
toolContent += `\n${statusIcon} **${tc.name}**`;
|
|
666
|
+
if (tc.result) {
|
|
667
|
+
const resultStr = typeof tc.result === 'string'
|
|
668
|
+
? tc.result
|
|
669
|
+
: JSON.stringify(tc.result);
|
|
670
|
+
if (resultStr.length <= 500) {
|
|
671
|
+
toolContent += `\n<details><summary>Result</summary>\n\n\`\`\`\n${resultStr}\n\`\`\`\n</details>\n`;
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
toolContent += `\n<details><summary>Result (${resultStr.length} chars)</summary>\n\n\`\`\`\n${resultStr.slice(0, 500)}…\n\`\`\`\n</details>\n`;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
totalContent += toolContent;
|
|
679
|
+
}
|
|
680
|
+
// Send final chunk
|
|
681
|
+
try {
|
|
682
|
+
ws.send(JSON.stringify({
|
|
683
|
+
type: 'assistant_chunk',
|
|
684
|
+
data: {
|
|
685
|
+
content: '',
|
|
686
|
+
total: totalContent,
|
|
687
|
+
done: true,
|
|
688
|
+
messageId: assistantMessageId,
|
|
689
|
+
},
|
|
690
|
+
}));
|
|
691
|
+
}
|
|
692
|
+
catch {
|
|
693
|
+
/* ws closed */
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
else if (type === 'tool') {
|
|
698
|
+
const toolEvent = data;
|
|
699
|
+
// Track tool calls
|
|
700
|
+
const existing = toolCalls.find(t => t.id === toolEvent.toolCallId);
|
|
701
|
+
if (existing) {
|
|
702
|
+
existing.phase = toolEvent.phase;
|
|
703
|
+
if (toolEvent.result)
|
|
704
|
+
existing.result =
|
|
705
|
+
typeof toolEvent.result === 'string'
|
|
706
|
+
? toolEvent.result
|
|
707
|
+
: JSON.stringify(toolEvent.result);
|
|
708
|
+
}
|
|
709
|
+
else {
|
|
710
|
+
toolCalls.push({
|
|
711
|
+
id: toolEvent.toolCallId,
|
|
712
|
+
name: toolEvent.name,
|
|
713
|
+
phase: toolEvent.phase,
|
|
714
|
+
args: toolEvent.args
|
|
715
|
+
? JSON.stringify(toolEvent.args)
|
|
716
|
+
: undefined,
|
|
717
|
+
result: toolEvent.result
|
|
718
|
+
? typeof toolEvent.result === 'string'
|
|
719
|
+
? toolEvent.result
|
|
720
|
+
: JSON.stringify(toolEvent.result)
|
|
721
|
+
: undefined,
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
// Send tool status to frontend
|
|
725
|
+
try {
|
|
726
|
+
const toolStatusMsg = toolEvent.phase === 'start'
|
|
727
|
+
? `\n\n🔧 *Using tool: ${toolEvent.name}…*\n`
|
|
728
|
+
: '';
|
|
729
|
+
if (toolStatusMsg) {
|
|
730
|
+
totalContent += toolStatusMsg;
|
|
731
|
+
ws.send(JSON.stringify({
|
|
732
|
+
type: 'assistant_chunk',
|
|
733
|
+
data: {
|
|
734
|
+
content: toolStatusMsg,
|
|
735
|
+
total: totalContent,
|
|
736
|
+
done: false,
|
|
737
|
+
messageId: assistantMessageId,
|
|
738
|
+
},
|
|
739
|
+
}));
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
catch {
|
|
743
|
+
/* ws closed */
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
// Send the message
|
|
748
|
+
const result = await openclawSessionService.sendMessage(messageText, ocSessionKey);
|
|
749
|
+
currentRunId = result.runId;
|
|
750
|
+
// Wait for completion (max 5 minutes)
|
|
751
|
+
await new Promise(resolve => {
|
|
752
|
+
const timeout = setTimeout(() => {
|
|
753
|
+
runDone = true;
|
|
754
|
+
resolve();
|
|
755
|
+
}, 300000);
|
|
756
|
+
const checkDone = setInterval(() => {
|
|
757
|
+
if (runDone) {
|
|
758
|
+
clearInterval(checkDone);
|
|
759
|
+
clearTimeout(timeout);
|
|
760
|
+
resolve();
|
|
761
|
+
}
|
|
762
|
+
}, 100);
|
|
763
|
+
});
|
|
764
|
+
// Clean up listener
|
|
765
|
+
cleanup();
|
|
766
|
+
// Save the assistant message
|
|
767
|
+
assistantContent = totalContent;
|
|
768
|
+
}
|
|
769
|
+
catch (error) {
|
|
770
|
+
console.error('[WebSocket] OpenClaw session error:', error);
|
|
771
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
772
|
+
assistantContent = `Error: ${errorMsg}`;
|
|
590
773
|
ws.send(JSON.stringify({
|
|
591
774
|
type: 'assistant_chunk',
|
|
592
775
|
data: {
|
|
593
|
-
content:
|
|
594
|
-
total:
|
|
595
|
-
done:
|
|
776
|
+
content: assistantContent,
|
|
777
|
+
total: assistantContent,
|
|
778
|
+
done: true,
|
|
596
779
|
messageId: assistantMessageId,
|
|
597
780
|
},
|
|
598
781
|
}));
|
|
599
|
-
// Small delay to simulate streaming but with better batching
|
|
600
|
-
if (!isLast) {
|
|
601
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
602
|
-
}
|
|
603
782
|
}
|
|
604
|
-
// Save
|
|
783
|
+
// Save assistant message from session mode
|
|
605
784
|
if (assistantContent && assistantMessageId) {
|
|
606
785
|
if (isPrivate) {
|
|
607
|
-
// For private sessions, just send completion without saving
|
|
608
|
-
console.log('Backend: Private session - skipping message save');
|
|
609
786
|
ws.send(JSON.stringify({
|
|
610
787
|
type: 'assistant_complete',
|
|
611
788
|
data: {
|
|
@@ -618,43 +795,235 @@ wss.on('connection', (ws, req) => {
|
|
|
618
795
|
}));
|
|
619
796
|
}
|
|
620
797
|
else {
|
|
621
|
-
console.log('Backend: Saving complete assistant message with ID:', assistantMessageId, 'regenerate:', !!regenerate);
|
|
622
|
-
// Calculate branching fields if this is a regeneration
|
|
623
|
-
let branchingFields = {};
|
|
624
|
-
if (regenerate && originalMessageId) {
|
|
625
|
-
// Find the original message to get its parentId or use its ID as parent
|
|
626
|
-
const originalMsg = session.messages.find(m => m.id === originalMessageId);
|
|
627
|
-
const parentId = originalMsg?.parentId || originalMessageId;
|
|
628
|
-
// Count existing siblings to determine branch index
|
|
629
|
-
const siblingCount = session.messages.filter(m => m.id === parentId || m.parentId === parentId).length;
|
|
630
|
-
branchingFields = {
|
|
631
|
-
parentId,
|
|
632
|
-
branchIndex: siblingCount, // New branch gets next index
|
|
633
|
-
isActive: true,
|
|
634
|
-
};
|
|
635
|
-
console.log('Backend: Setting branching fields:', branchingFields);
|
|
636
|
-
}
|
|
637
798
|
const assistantMessage = chatService.addMessage(sessionId, {
|
|
638
799
|
role: 'assistant',
|
|
639
800
|
content: assistantContent,
|
|
640
801
|
model: session.model,
|
|
641
802
|
id: assistantMessageId,
|
|
642
|
-
...branchingFields,
|
|
643
803
|
}, userId);
|
|
644
|
-
console.log('Backend: Assistant message saved:', !!assistantMessage);
|
|
645
|
-
// Send completion signal
|
|
646
804
|
ws.send(JSON.stringify({
|
|
647
805
|
type: 'assistant_complete',
|
|
648
806
|
data: assistantMessage,
|
|
649
807
|
}));
|
|
650
808
|
}
|
|
651
809
|
}
|
|
652
|
-
return; // Exit early
|
|
653
|
-
}
|
|
654
|
-
catch (pluginError) {
|
|
655
|
-
console.error('Plugin failed, falling back to Ollama:', pluginError);
|
|
656
|
-
// Continue to Ollama fallback below
|
|
810
|
+
return; // Exit early — handled via OpenClaw session
|
|
657
811
|
}
|
|
812
|
+
else {
|
|
813
|
+
// ---------------------------------------------------------------
|
|
814
|
+
// Standard plugin path (stateless HTTP completions)
|
|
815
|
+
// ---------------------------------------------------------------
|
|
816
|
+
try {
|
|
817
|
+
// Get user's preferred generation options
|
|
818
|
+
const userGenerationOptions = preferencesService.getGenerationOptions();
|
|
819
|
+
// Merge user preferences with request options
|
|
820
|
+
const mergedOptions = mergeGenerationOptions(userGenerationOptions, options);
|
|
821
|
+
// Get messages for context
|
|
822
|
+
const contextMessages = chatService.getMessagesForContext(sessionId);
|
|
823
|
+
// For regenerations, the user message is already in context; for new messages, we need to add it
|
|
824
|
+
let messagesForPlugin = regenerate
|
|
825
|
+
? contextMessages
|
|
826
|
+
: contextMessages.concat([userMessage]);
|
|
827
|
+
const systemPromptPrefix = pluginVars.system_prompt_prefix || '';
|
|
828
|
+
const userName = pluginVars.user_name || '';
|
|
829
|
+
// Prepend identity system message if configured
|
|
830
|
+
if (systemPromptPrefix || userName) {
|
|
831
|
+
let identityMsg = systemPromptPrefix;
|
|
832
|
+
if (userName) {
|
|
833
|
+
identityMsg = identityMsg
|
|
834
|
+
? `${identityMsg}\n\nThe user's name is: ${userName}`
|
|
835
|
+
: `The user's name is: ${userName}`;
|
|
836
|
+
}
|
|
837
|
+
messagesForPlugin = [
|
|
838
|
+
{
|
|
839
|
+
id: 'system-identity',
|
|
840
|
+
role: 'system',
|
|
841
|
+
content: identityMsg,
|
|
842
|
+
timestamp: Date.now(),
|
|
843
|
+
},
|
|
844
|
+
...messagesForPlugin,
|
|
845
|
+
];
|
|
846
|
+
}
|
|
847
|
+
const shouldStream = pluginVars.stream ?? false;
|
|
848
|
+
if (shouldStream) {
|
|
849
|
+
// Real SSE streaming from plugin
|
|
850
|
+
let totalContent = '';
|
|
851
|
+
const toolCalls = [];
|
|
852
|
+
for await (const chunk of pluginService.executePluginStreamRequest(actualModelName, messagesForPlugin, mergedOptions)) {
|
|
853
|
+
if (chunk.type === 'content' && chunk.content) {
|
|
854
|
+
totalContent += chunk.content;
|
|
855
|
+
ws.send(JSON.stringify({
|
|
856
|
+
type: 'assistant_chunk',
|
|
857
|
+
data: {
|
|
858
|
+
content: chunk.content,
|
|
859
|
+
total: totalContent,
|
|
860
|
+
done: false,
|
|
861
|
+
messageId: assistantMessageId,
|
|
862
|
+
},
|
|
863
|
+
}));
|
|
864
|
+
}
|
|
865
|
+
else if (chunk.type === 'tool_call' && chunk.toolCall) {
|
|
866
|
+
toolCalls.push(chunk.toolCall);
|
|
867
|
+
}
|
|
868
|
+
else if (chunk.type === 'done') {
|
|
869
|
+
// Append tool call info to content if present
|
|
870
|
+
if (toolCalls.length > 0) {
|
|
871
|
+
let toolContent = '\n\n---\n**🔧 Tool Calls:**\n';
|
|
872
|
+
for (const tc of toolCalls) {
|
|
873
|
+
let argsFormatted = tc.arguments;
|
|
874
|
+
try {
|
|
875
|
+
argsFormatted = JSON.stringify(JSON.parse(tc.arguments), null, 2);
|
|
876
|
+
}
|
|
877
|
+
catch {
|
|
878
|
+
/* keep raw */
|
|
879
|
+
}
|
|
880
|
+
toolContent += `\n**${tc.name}** (\`${tc.id}\`)\n\`\`\`json\n${argsFormatted}\n\`\`\`\n`;
|
|
881
|
+
}
|
|
882
|
+
totalContent += toolContent;
|
|
883
|
+
}
|
|
884
|
+
// Send final chunk
|
|
885
|
+
ws.send(JSON.stringify({
|
|
886
|
+
type: 'assistant_chunk',
|
|
887
|
+
data: {
|
|
888
|
+
content: '',
|
|
889
|
+
total: totalContent,
|
|
890
|
+
done: true,
|
|
891
|
+
messageId: assistantMessageId,
|
|
892
|
+
},
|
|
893
|
+
}));
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
assistantContent = totalContent;
|
|
897
|
+
}
|
|
898
|
+
else {
|
|
899
|
+
// Non-streaming: use original request method
|
|
900
|
+
const pluginResponse = await pluginService.executePluginRequest(actualModelName, messagesForPlugin, mergedOptions);
|
|
901
|
+
if (!pluginResponse?.choices?.length) {
|
|
902
|
+
throw new Error('Plugin returned empty or invalid response');
|
|
903
|
+
}
|
|
904
|
+
const choice = pluginResponse.choices[0];
|
|
905
|
+
// Handle content that may be a string or array of content blocks
|
|
906
|
+
const rawContent = choice?.message?.content;
|
|
907
|
+
if (Array.isArray(rawContent)) {
|
|
908
|
+
// Multimodal response: convert content blocks to markdown
|
|
909
|
+
const parts = [];
|
|
910
|
+
for (const block of rawContent) {
|
|
911
|
+
if (block.type === 'text' && block.text) {
|
|
912
|
+
parts.push(block.text);
|
|
913
|
+
}
|
|
914
|
+
else if (block.type === 'image_url' &&
|
|
915
|
+
block.image_url?.url) {
|
|
916
|
+
parts.push(``);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
assistantContent = parts.join('\n\n');
|
|
920
|
+
}
|
|
921
|
+
else {
|
|
922
|
+
assistantContent = rawContent || '';
|
|
923
|
+
}
|
|
924
|
+
// Render tool_calls from non-streaming response
|
|
925
|
+
const msgAny = choice?.message;
|
|
926
|
+
if (msgAny?.tool_calls &&
|
|
927
|
+
Array.isArray(msgAny.tool_calls) &&
|
|
928
|
+
msgAny.tool_calls.length > 0) {
|
|
929
|
+
let toolContent = '\n\n---\n**🔧 Tool Calls:**\n';
|
|
930
|
+
for (const tc of msgAny.tool_calls) {
|
|
931
|
+
const name = tc.function?.name || 'unknown';
|
|
932
|
+
const id = tc.id || '';
|
|
933
|
+
let args = tc.function?.arguments || '';
|
|
934
|
+
try {
|
|
935
|
+
args = JSON.stringify(JSON.parse(args), null, 2);
|
|
936
|
+
}
|
|
937
|
+
catch {
|
|
938
|
+
/* keep raw */
|
|
939
|
+
}
|
|
940
|
+
toolContent += `\n**${name}** (\`${id}\`)\n\`\`\`json\n${args}\n\`\`\`\n`;
|
|
941
|
+
}
|
|
942
|
+
assistantContent += toolContent;
|
|
943
|
+
}
|
|
944
|
+
// Send the complete response as chunks to simulate streaming
|
|
945
|
+
const words = assistantContent.split(' ');
|
|
946
|
+
const BATCH_SIZE = 3;
|
|
947
|
+
for (let i = 0; i < words.length; i += BATCH_SIZE) {
|
|
948
|
+
const batch = words.slice(i, i + BATCH_SIZE);
|
|
949
|
+
const chunk = words.slice(0, i + batch.length).join(' ');
|
|
950
|
+
const isLast = i + BATCH_SIZE >= words.length;
|
|
951
|
+
ws.send(JSON.stringify({
|
|
952
|
+
type: 'assistant_chunk',
|
|
953
|
+
data: {
|
|
954
|
+
content: batch.join(' ') + (isLast ? '' : ' '),
|
|
955
|
+
total: chunk,
|
|
956
|
+
done: isLast,
|
|
957
|
+
messageId: assistantMessageId,
|
|
958
|
+
},
|
|
959
|
+
}));
|
|
960
|
+
if (!isLast) {
|
|
961
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
// Save the complete assistant message (skip for private sessions)
|
|
966
|
+
if (assistantContent && assistantMessageId) {
|
|
967
|
+
if (isPrivate) {
|
|
968
|
+
// For private sessions, just send completion without saving
|
|
969
|
+
console.log('Backend: Private session - skipping message save');
|
|
970
|
+
ws.send(JSON.stringify({
|
|
971
|
+
type: 'assistant_complete',
|
|
972
|
+
data: {
|
|
973
|
+
id: assistantMessageId,
|
|
974
|
+
role: 'assistant',
|
|
975
|
+
content: assistantContent,
|
|
976
|
+
model: session.model,
|
|
977
|
+
timestamp: Date.now(),
|
|
978
|
+
},
|
|
979
|
+
}));
|
|
980
|
+
}
|
|
981
|
+
else {
|
|
982
|
+
console.log('Backend: Saving complete assistant message with ID:', assistantMessageId, 'regenerate:', !!regenerate);
|
|
983
|
+
// Calculate branching fields if this is a regeneration
|
|
984
|
+
let branchingFields = {};
|
|
985
|
+
if (regenerate && originalMessageId) {
|
|
986
|
+
// Find the original message to get its parentId or use its ID as parent
|
|
987
|
+
const originalMsg = session.messages.find(m => m.id === originalMessageId);
|
|
988
|
+
const parentId = originalMsg?.parentId || originalMessageId;
|
|
989
|
+
// Count existing siblings to determine branch index
|
|
990
|
+
const siblingCount = session.messages.filter(m => m.id === parentId || m.parentId === parentId).length;
|
|
991
|
+
branchingFields = {
|
|
992
|
+
parentId,
|
|
993
|
+
branchIndex: siblingCount, // New branch gets next index
|
|
994
|
+
isActive: true,
|
|
995
|
+
};
|
|
996
|
+
console.log('Backend: Setting branching fields:', branchingFields);
|
|
997
|
+
}
|
|
998
|
+
const assistantMessage = chatService.addMessage(sessionId, {
|
|
999
|
+
role: 'assistant',
|
|
1000
|
+
content: assistantContent,
|
|
1001
|
+
model: session.model,
|
|
1002
|
+
id: assistantMessageId,
|
|
1003
|
+
...branchingFields,
|
|
1004
|
+
}, userId);
|
|
1005
|
+
console.log('Backend: Assistant message saved:', !!assistantMessage);
|
|
1006
|
+
// Send completion signal
|
|
1007
|
+
ws.send(JSON.stringify({
|
|
1008
|
+
type: 'assistant_complete',
|
|
1009
|
+
data: assistantMessage,
|
|
1010
|
+
}));
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
return; // Exit early since we handled the request via plugin
|
|
1014
|
+
}
|
|
1015
|
+
catch (pluginError) {
|
|
1016
|
+
console.error('Plugin failed, falling back to Ollama:', pluginError?.message || pluginError);
|
|
1017
|
+
if (pluginError?.response) {
|
|
1018
|
+
console.error('Plugin HTTP response status:', pluginError.response.status);
|
|
1019
|
+
console.error('Plugin HTTP response data:', JSON.stringify(pluginError.response.data));
|
|
1020
|
+
}
|
|
1021
|
+
if (pluginError?.cause) {
|
|
1022
|
+
console.error('Plugin error cause:', pluginError.cause);
|
|
1023
|
+
}
|
|
1024
|
+
// Continue to Ollama fallback below
|
|
1025
|
+
}
|
|
1026
|
+
} // close else (standard plugin path)
|
|
658
1027
|
}
|
|
659
1028
|
console.log(`[WebSocket] No plugin found or plugin failed, using Ollama for model: ${actualModelName}`);
|
|
660
1029
|
// Reuse the actualModelName variable that was already resolved above
|