godcrm/backend/routes/v3/labs/execution-ops.js
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

322 lines
9 KiB
JavaScript

/**
* Labs Node Execution Operations Routes
* Rerun and split operations for lab nodes
* @see ADR-043: MindWorkflow Integration
*/
import { Router } from 'express';
import crypto from 'crypto';
import { apiLogger } from '../../../utils/logger.js';
import { success } from '../../../utils/response.js';
import { LabsService } from '../../../services/labs/index.js';
import {
getTableRows,
insertRow,
updateRowByField
} from './_helpers.js';
const router = Router();
/**
* POST /api/v3/labs/:labTableId/nodes/:nodeId/rerun
* Re-execute node with optional clone
*/
router.post('/:labTableId/nodes/:nodeId/rerun', async (req, res) => {
const startTime = Date.now();
const runId = crypto.randomUUID();
const logs = [];
try {
const { labTableId, nodeId } = req.params;
const { input, context = {}, clone = false } = req.body;
logs.push(`[${new Date().toISOString()}] Starting node re-execution (clone: ${clone})`);
// Get the node data
const rows = await getTableRows(labTableId);
const nodeRow = rows.find(row => row.node_id === nodeId);
if (!nodeRow) {
logs.push(`[${new Date().toISOString()}] Node not found: ${nodeId}`);
return success(res, {
status: 'error',
nodeId,
content: null,
contentType: null,
logs,
runId,
cloned: false,
executionTime: Date.now() - startTime
});
}
logs.push(`[${new Date().toISOString()}] Found node: ${nodeRow.title} (type: ${nodeRow.type_key})`);
let targetNodeId = nodeId;
let cloned = false;
// If clone requested, create a copy of the node
if (clone) {
const newNodeId = crypto.randomUUID();
const clonedNodeData = {
node_id: newNodeId,
type_key: nodeRow.type_key,
title: `${nodeRow.title} (copy)`,
content: nodeRow.content || '',
position_x: (nodeRow.position_x || 0) + 50,
position_y: (nodeRow.position_y || 0) + 50,
width: nodeRow.width,
height: nodeRow.height,
edges: [], // Don't copy edges for cloned node
ai_agent_id: nodeRow.ai_agent_id,
config: nodeRow.config,
order_index: (nodeRow.order_index || 0) + 1
};
await insertRow(labTableId, clonedNodeData, req.user?.id);
targetNodeId = newNodeId;
cloned = true;
logs.push(`[${new Date().toISOString()}] Created clone: ${newNodeId}`);
}
// Parse config if it's a string
if (typeof nodeRow.config === 'string') {
try {
nodeRow.config = JSON.parse(nodeRow.config);
} catch (e) {
nodeRow.config = {};
}
}
// Enhanced context with input
const enhancedContext = {
...context,
input
};
// Execute the node
logs.push(`[${new Date().toISOString()}] Executing node...`);
const result = await LabsService.executeNode(nodeRow, enhancedContext);
const executionTime = Date.now() - startTime;
logs.push(`[${new Date().toISOString()}] Execution completed in ${executionTime}ms`);
if (result.success) {
logs.push(`[${new Date().toISOString()}] Result: success`);
// Update the target node with the result
if (result.output || result.content) {
await updateRowByField(labTableId, 'node_id', targetNodeId, {
content: result.output || result.content
});
logs.push(`[${new Date().toISOString()}] Updated node content`);
}
} else {
logs.push(`[${new Date().toISOString()}] Result: error - ${result.error || 'Unknown error'}`);
}
apiLogger.info({
nodeId,
targetNodeId,
runId,
type: nodeRow.type_key,
success: result.success,
cloned,
tokensUsed: result.tokensUsed,
executionTime
}, 'Node rerun completed');
// Return MindWorkflow RunResponse format
success(res, {
status: result.success ? 'success' : 'error',
nodeId,
targetNodeId,
content: result.output || result.content || null,
contentType: result.contentType || nodeRow.type_key || null,
logs,
runId,
cloned,
tokensUsed: result.tokensUsed || result.usage?.totalTokens || undefined,
executionTime,
provider: result.provider || undefined,
model: result.model || undefined
});
} catch (err) {
const executionTime = Date.now() - startTime;
logs.push(`[${new Date().toISOString()}] Error: ${err.message}`);
apiLogger.error({ err, runId }, 'Failed to rerun lab node');
success(res, {
status: 'error',
nodeId: req.params.nodeId,
content: null,
contentType: null,
logs,
runId,
cloned: false,
executionTime
});
}
});
/**
* POST /api/v3/labs/:labTableId/nodes/:nodeId/split
* Split text node into multiple nodes
*/
router.post('/:labTableId/nodes/:nodeId/split', async (req, res) => {
const startTime = Date.now();
const runId = crypto.randomUUID();
const logs = [];
try {
const { labTableId, nodeId } = req.params;
const { separator = '\n\n' } = req.body;
logs.push(`[${new Date().toISOString()}] Starting node split (separator: ${JSON.stringify(separator)})`);
// Get the node data
const rows = await getTableRows(labTableId);
const nodeRow = rows.find(row => row.node_id === nodeId);
if (!nodeRow) {
logs.push(`[${new Date().toISOString()}] Node not found: ${nodeId}`);
return success(res, {
status: 'error',
nodeId,
content: null,
contentType: null,
logs,
runId,
createdNodes: [],
executionTime: Date.now() - startTime
});
}
logs.push(`[${new Date().toISOString()}] Found node: ${nodeRow.title} (type: ${nodeRow.type_key})`);
// Get content to split
const content = nodeRow.content || '';
if (!content.trim()) {
logs.push(`[${new Date().toISOString()}] Node has no content to split`);
return success(res, {
status: 'error',
nodeId,
content: null,
contentType: null,
logs,
runId,
createdNodes: [],
executionTime: Date.now() - startTime
});
}
// Split content
const parts = content.split(separator).filter(part => part.trim());
logs.push(`[${new Date().toISOString()}] Split into ${parts.length} parts`);
if (parts.length <= 1) {
logs.push(`[${new Date().toISOString()}] Content cannot be split (only 1 part)`);
return success(res, {
status: 'success',
nodeId,
content: content,
contentType: nodeRow.type_key,
logs,
runId,
createdNodes: [],
executionTime: Date.now() - startTime
});
}
// Parse config if it's a string
let nodeConfig = nodeRow.config;
if (typeof nodeConfig === 'string') {
try {
nodeConfig = JSON.parse(nodeConfig);
} catch (e) {
nodeConfig = {};
}
}
// Create new nodes for each part
const createdNodes = [];
const baseX = nodeRow.position_x || 0;
const baseY = nodeRow.position_y || 0;
const offsetY = (nodeRow.height || 150) + 30; // Vertical spacing
for (let i = 0; i < parts.length; i++) {
const newNodeId = crypto.randomUUID();
const partContent = parts[i].trim();
// Generate title from first line or first 50 chars
const firstLine = partContent.split('\n')[0];
const title = firstLine.length > 50
? firstLine.substring(0, 47) + '...'
: firstLine || `Part ${i + 1}`;
const newNodeData = {
node_id: newNodeId,
type_key: nodeRow.type_key,
title: title,
content: partContent,
position_x: baseX,
position_y: baseY + (offsetY * (i + 1)),
width: nodeRow.width,
height: nodeRow.height,
edges: [],
ai_agent_id: nodeRow.ai_agent_id,
config: nodeConfig,
order_index: (nodeRow.order_index || 0) + i + 1
};
await insertRow(labTableId, newNodeData, req.user?.id);
createdNodes.push({
node_id: newNodeId,
type: nodeRow.type_key,
title: title
});
logs.push(`[${new Date().toISOString()}] Created node ${i + 1}: ${newNodeId}`);
}
const executionTime = Date.now() - startTime;
logs.push(`[${new Date().toISOString()}] Split completed in ${executionTime}ms`);
apiLogger.info({
nodeId,
runId,
partsCount: parts.length,
createdNodesCount: createdNodes.length,
executionTime
}, 'Node split completed');
// Return MindWorkflow RunResponse format
success(res, {
status: 'success',
nodeId,
content: content,
contentType: nodeRow.type_key,
logs,
runId,
createdNodes,
executionTime
});
} catch (err) {
const executionTime = Date.now() - startTime;
logs.push(`[${new Date().toISOString()}] Error: ${err.message}`);
apiLogger.error({ err, runId }, 'Failed to split lab node');
success(res, {
status: 'error',
nodeId: req.params.nodeId,
content: null,
contentType: null,
logs,
runId,
createdNodes: [],
executionTime
});
}
});
export default router;